From f380b9054e6e7cc3aef0db0384a5840fd6d5cd2a Mon Sep 17 00:00:00 2001 From: Julian Avar Date: Thu, 24 Sep 2026 11:49:24 -0400 Subject: [PATCH 01/13] lua: Lua 5.5 on Kore, with io and os over the task's own filesystem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The engine, its freestanding host and the `kore.*` standard library move out of Kore's `user/apps/utils/lua` into a crate any libkore consumer can embed, beside `kore-wasi` and on the same seam. `io` and `os` are written here rather than vendored: Lua's `liolib.c` and `loslib.c` are not compiled at all, because both are libc over POSIX and this target has neither. They are a veneer over one implementation, not a second one — `io.open` and `kore.fs.open` answer the same file object, and one failure is rendered two ways, `nil, message, errno` on the stock surface and `nil, {code = "not-found"}` on the kore one, both derived from `FsError` rather than invented. Twenty of the twenty-two members the manual lists are present, with all seven `file:` methods. `os.execute` and `io.popen` are the two absent: both launch a process, which is a service face the embedding program holds and this crate deliberately does not, so they arrive through `Extension` like `kore.accel`. The stream position and its read-ahead live in the file object rather than beside the view, so there is one authority for it; the host side is purely positional. A handle is an index and a generation, so a use after close is `InvalidHandle` rather than a read of whatever took the slot. Where the manual leaves a detail to the system this build chooses and says so in KORE-VENDOR.md: `os.date` computes civil time directly, there being no `gmtime` or timezone database below it, so the one clock reads UTC; `os.clock` is monotonic since the VM started; `os.getenv` answers nil for every name, a task's authority arriving as capabilities rather than strings; `os.tmpname` creates its file exclusively, and `io.tmpfile` removes its own when the handle retires. The transport grows one borrowed byte window so a path or a write's bytes cross without a copy; no new word state, and a verb is still a constant and an arm. `Extension` loses its `'static` bound, which forbade the one shape an embedder has — a face borrowed from the `Rt` and transport its `main` owns — and bought nothing, the worker reading only `LIBRARIES`. The host smoke now runs the whole surface on every build, including the calendar, which is pure computation and needs no machine. It also gets the right prototype for `kore_lua_new`: it declared three parameters against a definition taking five, so every smoke run passed two garbage registers that `luaopen_kore` then walked as a library array. Gates: host smoke, host clippy `-D warnings`, unit tests and `cargo fmt` clean; `make cross` clean on x86_64, aarch64 and riscv64gc, and now includes this crate, so a target-only regression in the amalgamation meets a compiler instead of a keytest. Co-Authored-By: Claude Opus 5 (1M context) --- Cargo.lock | 37 + Cargo.toml | 4 +- Makefile | 13 +- crates/lua/Cargo.toml | 28 + crates/lua/KORE-VENDOR.md | 113 + crates/lua/build.rs | 236 ++ crates/lua/src/bridge.rs | 1124 +++++++ crates/lua/src/files.rs | 628 ++++ crates/lua/src/kore_lua.c | 2662 +++++++++++++++++ crates/lua/src/lib.rs | 221 ++ crates/lua/src/numeric.rs | 323 ++ crates/lua/src/shim.rs | 257 ++ crates/lua/src/vm.rs | 344 +++ crates/lua/tests/host_smoke.c | 323 ++ crates/lua/tests/run_setjmp_targets.py | 236 ++ .../lua/tests/setjmp-runtime/aarch64-start.s | 22 + crates/lua/tests/setjmp-runtime/aarch64.ld | 10 + crates/lua/tests/setjmp-runtime/harness.c | 92 + .../lua/tests/setjmp-runtime/riscv64-start.S | 20 + crates/lua/tests/setjmp-runtime/riscv64.ld | 10 + crates/lua/tests/target_shim.rs | 243 ++ crates/lua/vendor/lua/README.md | 7 + crates/lua/vendor/lua/lapi.c | 1477 +++++++++ crates/lua/vendor/lua/lapi.h | 65 + crates/lua/vendor/lua/lauxlib.c | 1214 ++++++++ crates/lua/vendor/lua/lauxlib.h | 269 ++ crates/lua/vendor/lua/lbaselib.c | 551 ++++ crates/lua/vendor/lua/lcode.c | 1970 ++++++++++++ crates/lua/vendor/lua/lcode.h | 105 + crates/lua/vendor/lua/lcorolib.c | 224 ++ crates/lua/vendor/lua/lctype.c | 64 + crates/lua/vendor/lua/lctype.h | 100 + crates/lua/vendor/lua/ldebug.c | 978 ++++++ crates/lua/vendor/lua/ldebug.h | 65 + crates/lua/vendor/lua/ldo.c | 1190 ++++++++ crates/lua/vendor/lua/ldo.h | 98 + crates/lua/vendor/lua/ldump.c | 306 ++ crates/lua/vendor/lua/lfunc.c | 313 ++ crates/lua/vendor/lua/lfunc.h | 65 + crates/lua/vendor/lua/lgc.c | 1802 +++++++++++ crates/lua/vendor/lua/lgc.h | 268 ++ crates/lua/vendor/lua/ljumptab.h | 114 + crates/lua/vendor/lua/llex.c | 603 ++++ crates/lua/vendor/lua/llex.h | 93 + crates/lua/vendor/lua/llimits.h | 356 +++ crates/lua/vendor/lua/lmathlib.c | 764 +++++ crates/lua/vendor/lua/lmem.c | 215 ++ crates/lua/vendor/lua/lmem.h | 95 + crates/lua/vendor/lua/lobject.c | 717 +++++ crates/lua/vendor/lua/lobject.h | 864 ++++++ crates/lua/vendor/lua/lopcodes.c | 130 + crates/lua/vendor/lua/lopcodes.h | 444 +++ crates/lua/vendor/lua/lopnames.h | 104 + crates/lua/vendor/lua/lparser.c | 2192 ++++++++++++++ crates/lua/vendor/lua/lparser.h | 197 ++ crates/lua/vendor/lua/lprefix.h | 44 + crates/lua/vendor/lua/lstate.c | 424 +++ crates/lua/vendor/lua/lstate.h | 450 +++ crates/lua/vendor/lua/lstring.c | 352 +++ crates/lua/vendor/lua/lstring.h | 73 + crates/lua/vendor/lua/lstrlib.c | 1899 ++++++++++++ crates/lua/vendor/lua/ltable.c | 1355 +++++++++ crates/lua/vendor/lua/ltable.h | 184 ++ crates/lua/vendor/lua/ltablib.c | 428 +++ crates/lua/vendor/lua/ltests.h | 166 + crates/lua/vendor/lua/ltm.c | 363 +++ crates/lua/vendor/lua/ltm.h | 105 + crates/lua/vendor/lua/lua.h | 547 ++++ crates/lua/vendor/lua/luaconf.h | 758 +++++ crates/lua/vendor/lua/lualib.h | 65 + crates/lua/vendor/lua/lundump.c | 420 +++ crates/lua/vendor/lua/lundump.h | 40 + crates/lua/vendor/lua/lutf8lib.c | 293 ++ crates/lua/vendor/lua/lvm.c | 1978 ++++++++++++ crates/lua/vendor/lua/lvm.h | 136 + crates/lua/vendor/lua/lzio.c | 89 + crates/lua/vendor/lua/lzio.h | 67 + crates/lua/vendor/musl-setjmp/COPYRIGHT | 193 ++ .../lua/vendor/musl-setjmp/aarch64/longjmp.s | 23 + .../lua/vendor/musl-setjmp/aarch64/setjmp.s | 24 + .../lua/vendor/musl-setjmp/riscv64/longjmp.S | 42 + .../lua/vendor/musl-setjmp/riscv64/setjmp.S | 41 + .../lua/vendor/musl-setjmp/x86_64/longjmp.s | 18 + crates/lua/vendor/musl-setjmp/x86_64/setjmp.s | 22 + native/Cargo.toml | 2 + native/crates/late/Cargo.toml | 17 + native/crates/late/src/lib.rs | 350 +++ native/crates/late/src/metal.rs | 493 +++ native/crates/ld/Cargo.toml | 21 + native/crates/ld/src/apply.rs | 1006 +++++++ native/crates/ld/src/check.rs | 605 ++++ native/crates/ld/src/functions.rs | 95 + native/crates/ld/src/late.rs | 248 ++ native/crates/ld/src/lib.rs | 63 + native/crates/ld/src/link.rs | 1242 ++++++++ native/crates/ld/src/parse.rs | 853 ++++++ native/crates/ld/src/place.rs | 443 +++ native/crates/ld/src/reloc.rs | 275 ++ native/crates/ld/src/stage.rs | 351 +++ native/crates/ld/src/store.rs | 236 ++ native/crates/ld/src/stub.rs | 154 + native/crates/ld/tests/apply_engine.rs | 817 +++++ native/crates/ld/tests/built_images.rs | 340 +++ native/crates/ld/tests/check_gate.rs | 717 +++++ native/crates/ld/tests/common/mod.rs | 656 ++++ native/crates/ld/tests/late_layout.rs | 143 + native/crates/ld/tests/link_engine.rs | 1431 +++++++++ native/crates/ld/tests/shipping_list.rs | 403 +++ native/crates/ld/tests/static_boot_images.rs | 54 + native/provenance.json | 28 +- 110 files changed, 46599 insertions(+), 8 deletions(-) create mode 100644 crates/lua/Cargo.toml create mode 100644 crates/lua/KORE-VENDOR.md create mode 100644 crates/lua/build.rs create mode 100644 crates/lua/src/bridge.rs create mode 100644 crates/lua/src/files.rs create mode 100644 crates/lua/src/kore_lua.c create mode 100644 crates/lua/src/lib.rs create mode 100644 crates/lua/src/numeric.rs create mode 100644 crates/lua/src/shim.rs create mode 100644 crates/lua/src/vm.rs create mode 100644 crates/lua/tests/host_smoke.c create mode 100644 crates/lua/tests/run_setjmp_targets.py create mode 100644 crates/lua/tests/setjmp-runtime/aarch64-start.s create mode 100644 crates/lua/tests/setjmp-runtime/aarch64.ld create mode 100644 crates/lua/tests/setjmp-runtime/harness.c create mode 100644 crates/lua/tests/setjmp-runtime/riscv64-start.S create mode 100644 crates/lua/tests/setjmp-runtime/riscv64.ld create mode 100644 crates/lua/tests/target_shim.rs create mode 100644 crates/lua/vendor/lua/README.md create mode 100644 crates/lua/vendor/lua/lapi.c create mode 100644 crates/lua/vendor/lua/lapi.h create mode 100644 crates/lua/vendor/lua/lauxlib.c create mode 100644 crates/lua/vendor/lua/lauxlib.h create mode 100644 crates/lua/vendor/lua/lbaselib.c create mode 100644 crates/lua/vendor/lua/lcode.c create mode 100644 crates/lua/vendor/lua/lcode.h create mode 100644 crates/lua/vendor/lua/lcorolib.c create mode 100644 crates/lua/vendor/lua/lctype.c create mode 100644 crates/lua/vendor/lua/lctype.h create mode 100644 crates/lua/vendor/lua/ldebug.c create mode 100644 crates/lua/vendor/lua/ldebug.h create mode 100644 crates/lua/vendor/lua/ldo.c create mode 100644 crates/lua/vendor/lua/ldo.h create mode 100644 crates/lua/vendor/lua/ldump.c create mode 100644 crates/lua/vendor/lua/lfunc.c create mode 100644 crates/lua/vendor/lua/lfunc.h create mode 100644 crates/lua/vendor/lua/lgc.c create mode 100644 crates/lua/vendor/lua/lgc.h create mode 100644 crates/lua/vendor/lua/ljumptab.h create mode 100644 crates/lua/vendor/lua/llex.c create mode 100644 crates/lua/vendor/lua/llex.h create mode 100644 crates/lua/vendor/lua/llimits.h create mode 100644 crates/lua/vendor/lua/lmathlib.c create mode 100644 crates/lua/vendor/lua/lmem.c create mode 100644 crates/lua/vendor/lua/lmem.h create mode 100644 crates/lua/vendor/lua/lobject.c create mode 100644 crates/lua/vendor/lua/lobject.h create mode 100644 crates/lua/vendor/lua/lopcodes.c create mode 100644 crates/lua/vendor/lua/lopcodes.h create mode 100644 crates/lua/vendor/lua/lopnames.h create mode 100644 crates/lua/vendor/lua/lparser.c create mode 100644 crates/lua/vendor/lua/lparser.h create mode 100644 crates/lua/vendor/lua/lprefix.h create mode 100644 crates/lua/vendor/lua/lstate.c create mode 100644 crates/lua/vendor/lua/lstate.h create mode 100644 crates/lua/vendor/lua/lstring.c create mode 100644 crates/lua/vendor/lua/lstring.h create mode 100644 crates/lua/vendor/lua/lstrlib.c create mode 100644 crates/lua/vendor/lua/ltable.c create mode 100644 crates/lua/vendor/lua/ltable.h create mode 100644 crates/lua/vendor/lua/ltablib.c create mode 100644 crates/lua/vendor/lua/ltests.h create mode 100644 crates/lua/vendor/lua/ltm.c create mode 100644 crates/lua/vendor/lua/ltm.h create mode 100644 crates/lua/vendor/lua/lua.h create mode 100644 crates/lua/vendor/lua/luaconf.h create mode 100644 crates/lua/vendor/lua/lualib.h create mode 100644 crates/lua/vendor/lua/lundump.c create mode 100644 crates/lua/vendor/lua/lundump.h create mode 100644 crates/lua/vendor/lua/lutf8lib.c create mode 100644 crates/lua/vendor/lua/lvm.c create mode 100644 crates/lua/vendor/lua/lvm.h create mode 100644 crates/lua/vendor/lua/lzio.c create mode 100644 crates/lua/vendor/lua/lzio.h create mode 100644 crates/lua/vendor/musl-setjmp/COPYRIGHT create mode 100644 crates/lua/vendor/musl-setjmp/aarch64/longjmp.s create mode 100644 crates/lua/vendor/musl-setjmp/aarch64/setjmp.s create mode 100644 crates/lua/vendor/musl-setjmp/riscv64/longjmp.S create mode 100644 crates/lua/vendor/musl-setjmp/riscv64/setjmp.S create mode 100644 crates/lua/vendor/musl-setjmp/x86_64/longjmp.s create mode 100644 crates/lua/vendor/musl-setjmp/x86_64/setjmp.s create mode 100644 native/crates/late/Cargo.toml create mode 100644 native/crates/late/src/lib.rs create mode 100644 native/crates/late/src/metal.rs create mode 100644 native/crates/ld/Cargo.toml create mode 100644 native/crates/ld/src/apply.rs create mode 100644 native/crates/ld/src/check.rs create mode 100644 native/crates/ld/src/functions.rs create mode 100644 native/crates/ld/src/late.rs create mode 100644 native/crates/ld/src/lib.rs create mode 100644 native/crates/ld/src/link.rs create mode 100644 native/crates/ld/src/parse.rs create mode 100644 native/crates/ld/src/place.rs create mode 100644 native/crates/ld/src/reloc.rs create mode 100644 native/crates/ld/src/stage.rs create mode 100644 native/crates/ld/src/store.rs create mode 100644 native/crates/ld/src/stub.rs create mode 100644 native/crates/ld/tests/apply_engine.rs create mode 100644 native/crates/ld/tests/built_images.rs create mode 100644 native/crates/ld/tests/check_gate.rs create mode 100644 native/crates/ld/tests/common/mod.rs create mode 100644 native/crates/ld/tests/late_layout.rs create mode 100644 native/crates/ld/tests/link_engine.rs create mode 100644 native/crates/ld/tests/shipping_list.rs create mode 100644 native/crates/ld/tests/static_boot_images.rs diff --git a/Cargo.lock b/Cargo.lock index 1815225..5005078 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -456,6 +456,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +[[package]] +name = "elf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55dd888a213fc57e957abf2aa305ee3e8a28dbe05687a251f33b637cd46b0070" + [[package]] name = "embedded-io" version = "0.4.0" @@ -851,6 +857,37 @@ dependencies = [ "raw-cpuid", ] +[[package]] +name = "kore-late" +version = "0.1.0" +dependencies = [ + "heapless", + "kore-abi", + "kore-dagfs", + "kore-ld", + "kore-rt", +] + +[[package]] +name = "kore-ld" +version = "0.1.1" +dependencies = [ + "bitflags", + "elf", + "heapless", + "kore-abi", + "zerocopy", +] + +[[package]] +name = "kore-lua" +version = "0.1.0-alpha.1" +dependencies = [ + "kore-late", + "kore-std", + "libm", +] + [[package]] name = "kore-ring" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 2051048..201698d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/MicroPerceptron/libkore" [workspace] resolver = "3" -members = ["tests/consumer", "crates/ffi", "crates/entry", "crates/wasi"] +members = ["tests/consumer", "crates/ffi", "crates/entry", "crates/wasi", "crates/lua"] exclude = ["native"] [features] @@ -31,4 +31,4 @@ kore-driver = { path = "native/crates/driver", optional = true } [package.metadata.kore] # Used by tools/local-kore.py to construct one coherent local source override. -owner-paths = { kore-start = "crates/start", kore-abi = "crates/abi", kore-app = "crates/app", kore-rt = "crates/rt", kore-dagfs = "crates/dagfs", kore-driver = "crates/driver" } +owner-paths = { kore-start = "crates/start", kore-abi = "crates/abi", kore-app = "crates/app", kore-rt = "crates/rt", kore-dagfs = "crates/dagfs", kore-driver = "crates/driver", kore-late = "crates/late" } diff --git a/Makefile b/Makefile index 12fe779..5b653e1 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ check: python3 tools/check-native.py fmt: - cargo fmt -p kore-std -p libkore-consumer -p kore-ffi -p kore-entry -p kore-wasi --check + cargo fmt -p kore-std -p libkore-consumer -p kore-ffi -p kore-entry -p kore-wasi -p kore-lua --check $(ZIG) fmt --check build.zig build.zig.zon crates/ffi/tests/consumer.zig extern/zig/src/root.zig extern/zig/src/allocator.zig docs: @@ -27,14 +27,17 @@ owners: python3 tools/check-owners.py < target/owners.json # Actual Kore user targets, including its hardware-float x86 ABI. A stock -# x86_64-unknown-none check alone cannot certify this interface. `kore-wasi` -# is absent: Wasmtime on a Kore target needs Kore's vendored patches -# (target-lexicon's triple, the no_std compiler split, the fiber switch), so +# x86_64-unknown-none check alone cannot certify this interface. `kore-lua` +# is here because its engine exists only on these targets — its host build +# carries the logic and none of the C — so a Kore-only regression in the +# amalgamation would otherwise reach a keytest before it reached a +# compiler. `kore-wasi` is absent: Wasmtime on a Kore target needs Kore's +# vendored patches (target-lexicon's triple, the no_std compiler split, the fiber switch), so # its Kore build is certified in the consumer that carries them — Kore's # userland workspace, whose `keytest-wasm-x86` runs the fixture on Kore. cross: @set -eu; for arch in x86_64 aarch64 riscv64gc; do \ - cargo check --locked -p kore-std -p kore-ffi -p kore-entry --all-features \ + cargo check --locked -p kore-std -p kore-ffi -p kore-entry -p kore-lua --all-features \ --target "targets/$$arch-unknown-kore-user.json" \ -Z build-std=core,alloc,compiler_builtins \ -Z build-std-features=compiler-builtins-mem; \ diff --git a/crates/lua/Cargo.toml b/crates/lua/Cargo.toml new file mode 100644 index 0000000..9c23d48 --- /dev/null +++ b/crates/lua/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "kore-lua" +version = "0.1.0-alpha.1" +edition = "2024" +publish = false +description = "Lua 5.5 on Kore: the pinned engine, its freestanding host, and the kore.* standard library over native owners" +build = "build.rs" +autotests = false + +[lib] +name = "kore_lua" +path = "src/lib.rs" +# The host build carries no engine (see lib.rs's gating), but it does carry +# `files`' logic — path splitting, handle encoding, the error vocabulary — +# and those are worth a unit test that runs without a machine. +test = true + +[dependencies] +# The app facade: `kore::fs` for `require`'s source modules, `kore::time` +# and `kore::random` for the standard library, `kore::runtime` for the +# worker thread and the doorbell the bridge parks on. +kore = { package = "kore-std", path = "../..", features = ["fs", "heap"] } +# Runtime linking for `require ".so"`: a store object stamped +# EXECUTABLE_IMAGE, checked, placed in this task's JIT arena and bound +# against the exported Lua C API (Kore `res/IMAGE.md` §8). +kore-late = { path = "../../native/crates/late" } +# The engine's libm seam; the C side reaches it through src/shim.rs. +libm = { version = "0.2.16", default-features = false } diff --git a/crates/lua/KORE-VENDOR.md b/crates/lua/KORE-VENDOR.md new file mode 100644 index 0000000..387ff2e --- /dev/null +++ b/crates/lua/KORE-VENDOR.md @@ -0,0 +1,113 @@ +# Lua source provenance + +`vendor/lua` contains the Lua 5.5.1 core, auxiliary library, and the selected +base/coroutine/math/string/table/UTF-8 libraries copied from +`MicroPerceptron/lua` commit +`7579fc9d7ed90240487251dfb69168f8e64e9294`, with ONE kore edit: +`luaconf.h` guards its `#define LUA_API extern` with `#if !defined(LUA_API)` +so `src/kore_lua.c` can predefine `LUA_API` as default-visibility `extern`. +The engine is compiled `-fvisibility=hidden`; that predefinition plus the +linker's `--export-dynamic-symbol=lua_*` / `luaL_*` is what puts the C API +in the host's `.dynsym`, where a native module loaded by `kore-late` +(`require "lua_demo"`) binds to it. The exported set is the module ABI. + +The copied source is intentionally pinned in-tree: a kore image build must not +fetch source or execute a moving upstream build. `src/kore_lua.c` is the +kore-owned amalgamation/configuration layer; it excludes Lua's POSIX I/O, OS, +package loader, and debug libraries — `liolib.c`, `loslib.c`, `loadlib.c` and +`ldblib.c` are not compiled at all. + +`io` and `os` are nonetheless *present*, written by kore over its own +filesystem face rather than over a libc that does not exist here. They are a +veneer, not a second implementation: `io.open` and `kore.fs.open` answer the +same file object, and one failure is rendered two ways — `nil, message, errno` +on the stock surface and `nil, {code = "not-found"}` on the kore one, both +derived from `crates/dagfs/src/error.rs`'s `FsError` rather than invented. + +The same rule that keeps `string.format` out governs what those two tables +contain: **a member either conforms fully or is absent.** Measured against +the Lua manual §6.8 and §6.9, twenty of the twenty-two members are present, +along with all seven `file:` methods: + +- `io.close`, `io.flush`, `io.input`, `io.lines`, `io.open`, `io.output`, + `io.read`, `io.tmpfile`, `io.type`, `io.write` +- `file:close`, `file:flush`, `file:lines`, `file:read`, `file:seek`, + `file:setvbuf`, `file:write` +- `os.clock`, `os.date`, `os.difftime`, `os.exit`, `os.getenv`, `os.remove`, + `os.rename`, `os.setlocale`, `os.time`, `os.tmpname` + +Where the manual leaves a detail to the system, this build chooses and says +so: + +- `os.date` computes civil time directly (Howard Hinnant's + `civil_from_days`). There is no `gmtime` and no timezone database below + it, so kore's one clock reads UTC: `os.date` answers the same thing with + and without the `!` prefix and reports `isdst = false`, and `os.time` + reads a table as UTC. +- `os.clock` reports monotonic time since the VM started. kore accounts a + task no separate CPU time, and the manual asks only for "an + approximation" — which this is exactly, for the single-threaded script + that is the only thing measuring it. +- `os.tmpname` *creates* its file exclusively before answering the name, + which is the POSIX behaviour the manual describes, rather than handing + back a name another task could already hold. `io.tmpfile` opens the same + draw in update mode and removes the file when the handle is retired — + sooner than "when the program ends", and by the same path, since the + VM's teardown retires every handle. +- `os.getenv` answers `nil` for every name: a kore task has no environment, + authority arrives as capabilities in its launch, and an unset variable is + exactly what `nil` means. +- `io.stdin` is a real file object that is always at end of file, the + conforming shape for a stream with nothing behind it. `io.stdin`, + `io.stdout` and `io.stderr` refuse `:close()` with + `cannot close standard file` — "the I/O library never closes these + files". +- `io.input`, `io.output` and `io.lines` raise on a failed open + (`cannot open file 'NAME' (REASON)`) rather than answering a code, as the + manual specifies for those three alone. + +`os.execute` and `io.popen` are the two absent members. Both launch a +process, which is a service face the embedding program holds and this crate +deliberately does not; Kore's own `lua` binary supplies them through +`Extension`, the same seam `kore.accel` arrives on. + +The selected string library deliberately omits `string.format`. kore has no +hosted `printf`, and exposing a hand-written subset would make accepted format +strings silently non-conforming. Core number-to-string conversion remains +available: the Rust shim implements Lua's `%.15g`/`%.17g` two-pass contract, +while integer and pointer formatting are restricted to Lua core's audited +internal formats. This build accepts text chunks only and omits base +`load`/`loadfile`/`dofile` plus `string.dump`: upstream Lua deliberately has no +bytecode verifier, so an untrusted binary chunk is not a safe capability-task +input. The base library also omits `warn`: the selected freestanding host has +no warning sink, so retaining an API that silently discards every message +would be misleading. + +Lua is Copyright (C) 1994-2026 Lua.org, PUC-Rio and distributed under the MIT +license. The full upstream copyright and license notice is preserved at the +end of `vendor/lua/lua.h`. + +`vendor/musl-setjmp` contains only musl's six architecture-specific +`setjmp`/`longjmp` assembly files for x86_64, AArch64, and RV64, copied from +the musl source bundled by Zig `0.16.0-dev.2193+fc517bd01`. The build accepts +the Zig `0.16.x` family, including stable `0.16.0`. The C amalgamation asserts +that the toolchain's musl `jmp_buf` still begins with the exact 8-, 22-, or +26-word register-storage prefix consumed by the x86_64, AArch64, or RV64 +assembly, so compatibility follows the consumed ABI rather than one Zig build +identifier. +The six vendored routines and those three header dimensions were verified +unchanged in stable Zig `0.16.0`. +Lua requires non-local error +recovery and Clang's restricted builtins are not implemented on all three +targets. The corresponding musl MIT notice is preserved in +`vendor/musl-setjmp/COPYRIGHT`; no other musl runtime is linked. + +The exact AArch64 and RV64 assembly is runtime-tested under full-system QEMU +without a hosted libc: + +```sh +python3 crates/lua/tests/run_setjmp_targets.py +``` + +That probe crosses a deeper stack frame and checks both a nonzero long-jump +value and C's required `longjmp(..., 0) -> 1` normalization. diff --git a/crates/lua/build.rs b/crates/lua/build.rs new file mode 100644 index 0000000..e3d0372 --- /dev/null +++ b/crates/lua/build.rs @@ -0,0 +1,236 @@ +use std::{ + env, + ffi::OsStr, + fs, + path::{Path, PathBuf}, + process::{Command, ExitStatus}, +}; + +const SUPPORTED_ZIG_MAJOR: u64 = 0; +const SUPPORTED_ZIG_MINOR: u64 = 16; + +fn zig_release_line(version: &str) -> Option<(u64, u64)> { + let release = version + .split_once('-') + .map_or(version, |(release, _)| release); + let release = release + .split_once('+') + .map_or(release, |(release, _)| release); + let mut parts = release.split('.'); + let major = parts.next()?.parse().ok()?; + let minor = parts.next()?.parse().ok()?; + let _patch: u64 = parts.next()?.parse().ok()?; + if parts.next().is_some() { + return None; + } + Some((major, minor)) +} + +fn zig_version_supported(version: &str) -> bool { + zig_release_line(version) == Some((SUPPORTED_ZIG_MAJOR, SUPPORTED_ZIG_MINOR)) +} + +fn run(mut command: Command, what: &str) -> ExitStatus { + let status = command + .status() + .unwrap_or_else(|error| panic!("could not run {what}: {error}")); + if !status.success() { + panic!("{what} failed with {status}"); + } + status +} + +fn zig_command(zig: &OsStr, out: &Path) -> Command { + let mut command = Command::new(zig); + command + .env("ZIG_LOCAL_CACHE_DIR", out.join("zig-cache")) + .env("ZIG_GLOBAL_CACHE_DIR", out.join("zig-global-cache")); + command +} + +fn main() { + println!("cargo:rerun-if-env-changed=KORE_ZIG"); + println!("cargo:rerun-if-env-changed=KORE_HOST_CC"); + println!("cargo:rerun-if-changed=src/numeric.rs"); + println!("cargo:rerun-if-changed=src/kore_lua.c"); + println!("cargo:rerun-if-changed=tests/host_smoke.c"); + println!("cargo:rerun-if-changed=tests/target_shim.rs"); + println!("cargo:rerun-if-changed=vendor/lua"); + println!("cargo:rerun-if-changed=vendor/musl-setjmp"); + + let out = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR")); + fs::create_dir_all(&out).expect("create Lua build output"); + let arch = env::var("CARGO_CFG_TARGET_ARCH").expect("target architecture"); + let c_target = match arch.as_str() { + "x86_64" => "x86_64-linux-musl", + "aarch64" => "aarch64-linux-musl", + "riscv64" => "riscv64-linux-musl", + other => panic!("Lua has no kore C ABI mapping for {other}"), + }; + let zig = env::var_os("KORE_ZIG").unwrap_or_else(|| "zig".into()); + let zig_version = Command::new(&zig) + .arg("version") + .output() + .expect("could not query the supported Zig toolchain"); + assert!(zig_version.status.success(), "`zig version` failed"); + let zig_version = std::str::from_utf8(&zig_version.stdout) + .expect("`zig version` returned non-UTF-8 output") + .trim(); + assert!( + zig_version_supported(zig_version), + "Lua requires Zig {SUPPORTED_ZIG_MAJOR}.{SUPPORTED_ZIG_MINOR}.x; found {zig_version}. \ + The target C build separately verifies the musl jmp_buf ABI used by the vendored assembly" + ); + let object = out.join("kore_lua.o"); + let archive = out.join("libkore_lua.a"); + + // Exercise the same amalgamation through a native protected-call cycle + // on every clean build. This catches C-ABI/longjmp regressions before a + // boot image can merely prove that the freestanding object linked. + let host = env::var("HOST").expect("host triple"); + let host_cc = env::var_os("KORE_HOST_CC").unwrap_or_else(|| { + if host.contains("apple-darwin") { + "/usr/bin/clang".into() + } else { + "cc".into() + } + }); + let host_smoke = out.join("lua-host-smoke"); + let mut smoke_cc = Command::new(&host_cc); + smoke_cc + .args(["-std=c11", "-O2", "-Ivendor/lua"]) + .args(["src/kore_lua.c", "tests/host_smoke.c", "-lm", "-o"]) + .arg(&host_smoke); + run(smoke_cc, "native Lua protected-call smoke compilation"); + run( + Command::new(&host_smoke), + "native Lua protected-call smoke execution", + ); + + // Compile the C engine a second time without its libc numeric oracle, then + // link it beneath the target's exact Rust numeric source. This exercises + // the real C/Rust ABI and Lua two-pass caller, not merely each half alone. + let target_engine_object = out.join("kore-lua-target-engine.o"); + let mut target_engine_cc = Command::new(&host_cc); + target_engine_cc + .args([ + "-std=c11", + "-O2", + "-fPIC", + "-Ivendor/lua", + "-c", + "src/kore_lua.c", + "-o", + ]) + .arg(&target_engine_object); + run( + target_engine_cc, + "native Lua target-engine object compilation", + ); + + let target_smoke_object = out.join("kore-lua-target-smoke.o"); + let mut target_smoke_cc = Command::new(&host_cc); + target_smoke_cc + .args([ + "-std=c11", + "-O2", + "-fPIC", + "-DKORE_LUA_USE_TARGET_NUMERIC=1", + "-Ivendor/lua", + "-c", + "tests/host_smoke.c", + "-o", + ]) + .arg(&target_smoke_object); + run( + target_smoke_cc, + "native Lua target-smoke object compilation", + ); + + // The same harness also checks the bounded ABI directly and compares both + // %g passes against host libc over a deterministic random corpus. + let target_shim_smoke = out.join("lua-target-shim-smoke"); + let rustc = env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); + let mut shim_rustc = Command::new(rustc); + shim_rustc + .args([ + "--edition=2024", + "--cfg", + "kore_lua_c_engine", + "-O", + "tests/target_shim.rs", + "-o", + ]) + .arg(&target_shim_smoke) + .arg("-C") + .arg(format!("link-arg={}", target_engine_object.display())) + .arg("-C") + .arg(format!("link-arg={}", target_smoke_object.display())) + .args(["-C", "link-arg=-lm"]); + run(shim_rustc, "native Lua target-shim smoke compilation"); + run( + Command::new(&target_shim_smoke), + "native Lua target-shim smoke execution", + ); + + let mut cc = zig_command(&zig, &out); + cc.arg("cc") + // Zig's musl target supplies ISO C headers only. The resulting object + // is still freestanding and links no musl runtime into the kore image. + .args(["-target", c_target, "-std=c11", "-O2"]) + .args([ + "-ffreestanding", + "-fno-stack-protector", + "-fno-unwind-tables", + "-fno-asynchronous-unwind-tables", + "-ffunction-sections", + "-fdata-sections", + "-fPIC", + "-fvisibility=hidden", + "-DKORE_LUA_ZIG_MUSL_ABI=1", + "-Wall", + "-Wextra", + "-Werror=implicit-function-declaration", + ]) + .arg("-Ivendor/lua"); + match arch.as_str() { + "x86_64" => { + cc.args(["-mno-red-zone", "-mno-mmx"]); + } + "riscv64" => { + // Zig's riscv64-musl baseline is rv64gc. The explicit ABI keeps + // the object identical to kore's custom Rust target. + cc.arg("-mabi=lp64d"); + } + _ => {} + } + cc.args(["-c", "src/kore_lua.c", "-o"]).arg(&object); + run(cc, "Zig Lua C compilation"); + + let asm_extension = if arch == "riscv64" { "S" } else { "s" }; + let mut asm_objects = Vec::new(); + for stem in ["setjmp", "longjmp"] { + let source = format!("vendor/musl-setjmp/{arch}/{stem}.{asm_extension}"); + let output = out.join(format!("{stem}.o")); + let mut assembler = zig_command(&zig, &out); + assembler.args(["cc", "-target", c_target, "-fPIC"]); + if arch == "riscv64" { + assembler.arg("-mabi=lp64d"); + } else if arch == "x86_64" { + assembler.arg("-mno-red-zone"); + } + assembler.args(["-c", &source, "-o"]).arg(&output); + run(assembler, "Zig setjmp assembly"); + asm_objects.push(output); + } + + let mut ar = zig_command(&zig, &out); + ar.args(["ar", "rcs"]).arg(&archive).arg(&object); + for object in &asm_objects { + ar.arg(object); + } + run(ar, "Zig Lua archive creation"); + + println!("cargo:rustc-link-search=native={}", out.display()); + println!("cargo:rustc-link-lib=static=kore_lua"); +} diff --git a/crates/lua/src/bridge.rs b/crates/lua/src/bridge.rs new file mode 100644 index 0000000..2d5bda5 --- /dev/null +++ b/crates/lua/src/bridge.rs @@ -0,0 +1,1124 @@ +//! The VM's thread and the host-call bridge (`res/DAGFS.md` §4). +//! +//! Lua is a synchronous C engine: `require` must return a module to the +//! C frame that called it, and that frame may sit inside a coroutine, a +//! metamethod, or a `pcall`. The store, however, is reached only through +//! the task's async filesystem client on its executor. So the VM runs on a +//! kore-rt **worker thread** and the executor stays on thread 0; the two +//! share one request cell and one word: +//! +//! ```text +//! thread 0 (executor) worker (VM) +//! ─────────────────── ─────────── +//! write eval → EVAL_POSTED, kick ──────────▶ wait(EVAL_DONE) returns +//! park on the wake channel runs the chunk … +//! … C `require` → write op +//! ◀──────── CALL_POSTED, kick_parent ─────── wait(CALL_POSTED) +//! serve the op (async) +//! write reply → CALL_SERVED, kick ─────────▶ returns; EVAL_POSTED +//! park on the wake channel +//! … chunk ends +//! ◀──────── EVAL_DONE, kick_parent ──────── wait(EVAL_DONE) +//! take stdout/stderr +//! ``` +//! +//! **One transport, many ops.** `require`, `kore.time.sleep`, and every +//! later verb that needs a service — this crate's own or an embedder's, +//! through [`Extension`] — are the same round trip with a different +//! payload, so they share one word-state pair +//! (`CALL_POSTED` / `CALL_SERVED`), one worker-side helper ([`host_call`]), +//! and one thread-0 dispatcher ([`serve_call`]). Adding a verb is a new op +//! constant and a new arm — never a new state, and never another copy of +//! the ordering discipline below. Verbs that need *no* service do not come +//! here at all: `kore.time.now` reads the clock page and `kore.random` +//! draws from the CPU, both directly on the VM thread. +//! +//! Every park is a doorbell park; nothing spins. The worker parks with +//! `futex::wait` on the word (its own doorbell); thread 0 parks on the +//! **wake channel** the worker holds a send to (`DoorbellWorker::wake_recv`, +//! `executor::next_wake_on`) — a launched task has no self-kick for a +//! worker to inherit, so `futex::wake` could not reach it. Lost wakeups +//! are excluded by discipline, not luck: the worker stores the word, then +//! kicks; thread 0 stands its post, then re-loads the word, then parks. A +//! kick that found no standing post was refused, and that ordering means +//! the store it announced is visible to the re-load. The +//! word's holder owns the cell: the worker while it reads `EVAL_POSTED` / +//! `CALL_SERVED` / `QUIT`, thread 0 while it reads `EVAL_DONE` / +//! `CALL_POSTED` / `WORKER_GONE`. Buffers cross the boundary as owned +//! `Vec`s — the heap frees a block from any thread onto its owning shard's +//! remote list — or as pointers into a frame the owner keeps alive across +//! the whole phase (the eval's script). +//! +//! Resolution order (`SEARCH`): the task's own tree, then `libs/`; a source +//! module `.lua` wins over a native `.so`. Native modules are +//! store objects stamped `EXECUTABLE_IMAGE` that `kore-late` checks, places +//! in the JIT arena, and links against this executable's exported C API. + +use alloc::vec::Vec; +use core::cell::UnsafeCell; +use core::ffi::{CStr, c_char, c_int}; +use core::sync::atomic::{AtomicU32, Ordering}; + +use kore::abi::clock::TimerError; +use kore::abi::session::Status; +use kore::abi::svc::FS_NAME_BYTES; +use kore::fs::Fs; +use kore::runtime::{ + chan::{NoticeRx, RecvError}, + entry::Rt, + futex::{self, WaitOutcome}, + thread::{self, DoorbellWorker}, + transport::TrapTransport, +}; +use kore_late::{LoadError, Loader, Search}; + +use crate::vm::{ReplResult, Vm}; +use crate::{Answer, Extension, OP_EMBEDDER_BASE}; + +/// The bound on a source module (`.lua`). Sized like a generous +/// script, not like a library: the whole buffer is staged in the task's +/// 2 MiB launch heap beside the VM, and a module is read whole before the +/// engine sees it. Larger sources belong in the store as several modules. +pub const MAX_SOURCE_MODULE_BYTES: usize = 64 * 1024; + +/// Where `require` looks, in order: the task's own tree (its cwd view), +/// then the READ-only `libs/` view every task carries. +const SEARCH: &[&[u8]] = &[b"", b"libs"]; + +/// The prefix a native module's opener carries: `luaopen_`. +const OPENER_PREFIX: &[u8] = b"luaopen_"; + +// The word's states. See the module doc for who owns the cell in each. +const EVAL_DONE: u32 = 0; +const EVAL_POSTED: u32 = 1; +/// The worker posted a host call — a `require`, a sleep, an embedder's +/// library call, any operation that needs thread 0's executor. Thread 0 +/// owns the cell until it stores [`CALL_SERVED`]. One pair serves every +/// op: the VM is a synchronous C engine and every service face lives on +/// thread 0, so the transport is identical and only the payload differs. +const CALL_POSTED: u32 = 2; +const CALL_SERVED: u32 = 3; +const QUIT: u32 = 4; +const WORKER_GONE: u32 = 5; + +/// The host-call op vocabulary, as the C side names them (`kore_lua.c`). +/// One flat namespace over one transport; a new standard-library verb is a +/// new constant and a new `serve_call` arm, never a new word state. An +/// embedder's numbers start at [`OP_EMBEDDER_BASE`]. +/// +/// `require ""` — the module name is in `name`, not `args`, and the +/// reply is a chunk or an opener rather than words. +const OP_REQUIRE: u32 = 7; +/// `kore.time.sleep(seconds)` — arg0 is the delay in nanoseconds. +const OP_SLEEP: u32 = 16; +/// `kore.proc.args` — the launch argument bytes, answered as text. +const OP_PROC_ARGS: u32 = 17; + +// `kore.fs`, and so `io` and the filesystem half of `os` — one verb each, +// all over the one transport. `arg0` is a handle wherever a verb takes +// one; `input` carries a path or the bytes of a write. Every verb is +// positional: the stream position and its read-ahead belong to the file +// object a script holds, which is in C because `io.open` and +// `kore.fs.open` answer the same object (`crate::files`). +const OP_FS_OPEN: u32 = 18; +const OP_FS_CLOSE: u32 = 19; +const OP_FS_READ: u32 = 20; +const OP_FS_WRITE: u32 = 21; +const OP_FS_SYNC: u32 = 23; +const OP_FS_RESIZE: u32 = 24; +const OP_FS_STAT: u32 = 25; +const OP_FS_REMOVE: u32 = 26; +const OP_FS_RENAME: u32 = 27; +const OP_FS_LIST: u32 = 28; +/// `os.exit(code)` — records the code, which the embedder reads once the +/// evaluation has unwound. Recording rather than exiting is the point: the +/// VM still has C frames below it and `` handlers above it, and +/// leaving from here would run neither. +const OP_PROC_EXIT: u32 = 29; + +/// Host-call outcomes: the value(s) are good; the service refused (the +/// text names the refusal); the call could not be made at all (the text +/// says why — no face, no lease, a broken ring). +/// +/// Every op answers one of these three, so a script that learned one +/// module's error handling has learned them all. +const STATUS_OK: c_int = 0; +const STATUS_REFUSED: c_int = 1; +const STATUS_UNAVAILABLE: c_int = 2; + +const KIND_CHUNK: u32 = 0; +const KIND_REPL: u32 = 1; + +const REPLY_ERROR: c_int = 0; +const REPLY_SOURCE: c_int = 1; +const REPLY_NATIVE: c_int = 2; + +/// `lua_CFunction`. +type LuaCFunction = unsafe extern "C" fn(*mut c_void) -> c_int; +use core::ffi::c_void; + +/// How one evaluation ended, as the VM reports it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Outcome { + Ok, + Failed, + /// REPL only: the chunk needs a continuation line. + Incomplete, + /// REPL only: the expression buffer could not be allocated. + ResourceRefused, + /// The worker could not construct a VM. + Startup, +} + +/// The bridge is broken: the worker is gone, a kick was refused, or a park +/// is impossible. The host exits; nothing here is retried. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Fatal; + +struct Payload { + // thread 0 → worker: the evaluation. + kind: u32, + script: *const u8, + script_len: usize, + chunk_name: *const u8, + chunk_name_len: usize, + // worker → thread 0: its result. + outcome: Outcome, + stdout: Vec, + stderr: Vec, + // worker → thread 0: the host call. `op` selects the arm; `args` and + // `name` carry its arguments (a `require` names a module, the value + // ops pass words). One shape because one transport serves them all. + op: u32, + args: [u64; 3], + name: [u8; FS_NAME_BYTES], + name_len: usize, + // worker → thread 0: the call's byte argument — a path, the bytes a + // write is handing over. A *borrow* into the Lua frame, not a copy: + // the worker is parked for the whole call, so the string the argument + // names cannot move or be collected, and a `Vec` here would copy every + // written buffer twice for nothing (`res/SYSTEM_LIBRARY.md`). Null + // and zero when the op takes no bytes. + input: *const u8, + input_len: usize, + // thread 0 → worker: a `require`'s answer. Its reply is a function + // pointer or owned bytes, not words, so it keeps its own fields — that + // shape is the native-module ABI (`res/IMAGE.md` §8), not a detail. + reply: c_int, + source: Vec, + opener: Option, + error: *const c_char, + // thread 0 → worker: every other op's outcome. `text` is the refusal, + // reason, or byte answer, alive until the next call (C copies it into + // a Lua string before returning). + status: c_int, + results: [u64; 2], + text: Vec, +} + +struct Cell { + state: AtomicU32, + payload: UnsafeCell, +} + +// SAFETY: `payload` is touched only by the thread the word's current state +// hands the cell to (module doc); every hand-over is a Release store on +// `state` matched by the other side's Acquire load. +unsafe impl Sync for Cell {} + +static CELL: Cell = Cell { + state: AtomicU32::new(EVAL_DONE), + payload: UnsafeCell::new(Payload { + kind: KIND_CHUNK, + script: core::ptr::null(), + script_len: 0, + chunk_name: core::ptr::null(), + chunk_name_len: 0, + outcome: Outcome::Ok, + stdout: Vec::new(), + stderr: Vec::new(), + op: 0, + args: [0; 3], + name: [0; FS_NAME_BYTES], + name_len: 0, + input: core::ptr::null(), + input_len: 0, + reply: REPLY_ERROR, + source: Vec::new(), + opener: None, + error: core::ptr::null(), + status: STATUS_UNAVAILABLE, + results: [0; 2], + text: Vec::new(), + }), +}; + +/// The cell, for the side the word currently hands it to. +/// +/// # Safety +/// The caller has observed (Acquire) a state that makes it the owner, and +/// holds no other reference across the store that hands the cell back. +unsafe fn payload<'a>() -> &'a mut Payload { + unsafe { &mut *CELL.payload.get() } +} + +// ---------------------------------------------------------------- worker + +/// The worker's entry: build the VM, then serve evaluations until `QUIT`. +fn vm_thread() { + let Some(mut vm) = Vm::new(E::LIBRARIES) else { + // SAFETY: the worker owns the cell while the word reads EVAL_DONE + // and thread 0 has not yet posted (it posts only after spawn). + unsafe { payload().outcome = Outcome::Startup }; + CELL.state.store(WORKER_GONE, Ordering::Release); + let _ = thread::kick_parent(); + return; + }; + loop { + match CELL.state.load(Ordering::Acquire) { + EVAL_POSTED => { + run_eval(&mut vm); + CELL.state.store(EVAL_DONE, Ordering::Release); + let _ = thread::kick_parent(); + } + QUIT => { + drop(vm); + CELL.state.store(WORKER_GONE, Ordering::Release); + let _ = thread::kick_parent(); + return; + } + WORKER_GONE => return, + other => { + if futex::wait(&CELL.state, other, None) == WaitOutcome::Unsupported { + // No doorbell to park on: a spin would be the only + // alternative, and that is the one thing this bridge + // refuses to be. Leave; thread 0 reads WORKER_GONE. + drop(vm); + CELL.state.store(WORKER_GONE, Ordering::Release); + let _ = thread::kick_parent(); + return; + } + } + } + } +} + +fn run_eval(vm: &mut Vm) { + // SAFETY: EVAL_POSTED — the worker owns the cell; thread 0 keeps the + // script and chunk name alive until it reads EVAL_DONE. + let cell = unsafe { payload() }; + let script = if cell.script.is_null() { + &[][..] + } else { + unsafe { core::slice::from_raw_parts(cell.script, cell.script_len) } + }; + cell.outcome = match cell.kind { + KIND_REPL => match vm.eval_repl(script) { + ReplResult::Complete(true) => Outcome::Ok, + ReplResult::Complete(false) => Outcome::Failed, + ReplResult::Incomplete => Outcome::Incomplete, + ReplResult::ResourceRefused => Outcome::ResourceRefused, + }, + _ => { + let name: &'static [u8] = + unsafe { core::slice::from_raw_parts(cell.chunk_name, cell.chunk_name_len) }; + if vm.eval(script, name) { + Outcome::Ok + } else { + Outcome::Failed + } + } + }; + cell.stdout = vm.take_stdout(); + cell.stderr = vm.take_stderr(); +} + +/// The transport every host call shares: write the request, hand the cell +/// to thread 0, park on the doorbell, read the response back, and resume +/// the evaluation. One function owns the whole ordering so no caller can +/// get it wrong, and a new standard-library verb is a `write`/`read` pair +/// rather than another copy of this discipline. +/// +/// `Err` means the bridge is gone and nothing was served; the cell is not +/// handed back and the evaluation is not resumed. +/// +/// # Safety +/// Worker thread only, inside an evaluation (the word reads `EVAL_POSTED`). +unsafe fn host_call( + write: impl FnOnce(&mut Payload), + read: impl FnOnce(&Payload) -> T, +) -> Result { + if CELL.state.load(Ordering::Acquire) != EVAL_POSTED { + return Err(c"host call outside an evaluation"); + } + // SAFETY: EVAL_POSTED — the worker owns the cell. + write(unsafe { payload() }); + CELL.state.store(CALL_POSTED, Ordering::Release); + let _ = thread::kick_parent(); + loop { + match CELL.state.load(Ordering::Acquire) { + CALL_SERVED => break, + WORKER_GONE | QUIT => return Err(c"host is shutting down"), + other => { + if futex::wait(&CELL.state, other, None) == WaitOutcome::Unsupported { + return Err(c"worker cannot park"); + } + } + } + } + // SAFETY: CALL_SERVED — the worker owns the cell again. + let value = read(unsafe { payload() }); + // Back inside the evaluation. No kick: thread 0 does not wait on this + // word — it parks on its wake doorbell (`park_until_kicked`) and reads + // `CALL_SERVED` and `EVAL_POSTED` alike as "the worker is busy". It + // has nothing to do until the worker's next post, and both of those + // (`CALL_POSTED`, `EVAL_DONE`) kick. + CELL.state.store(EVAL_POSTED, Ordering::Release); + Ok(value) +} + +/// C `require`'s half (`kore_lua.c`): post the name, park until served, +/// hand back what thread 0 stored. Called on the worker, inside an +/// evaluation, so the word reads `EVAL_POSTED` on entry. +/// +/// # Safety +/// `name` points at `len` bytes; the out-pointers are valid. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn kore_lua_host_require( + name: *const c_char, + len: usize, + bytes: *mut *const c_char, + bytes_len: *mut usize, + opener: *mut Option, +) -> c_int { + let fail = |message: &'static CStr| -> c_int { + unsafe { + *bytes = message.as_ptr(); + *bytes_len = message.count_bytes(); + *opener = None; + } + REPLY_ERROR + }; + if name.is_null() || len == 0 || len > FS_NAME_BYTES { + return fail(c"module name is not a store name"); + } + let requested = unsafe { core::slice::from_raw_parts(name.cast::(), len) }; + let served = unsafe { + host_call( + |cell| { + cell.op = OP_REQUIRE; + cell.name[..len].copy_from_slice(requested); + cell.name_len = len; + cell.reply = REPLY_ERROR; + cell.opener = None; + cell.error = core::ptr::null(); + }, + |cell| match cell.reply { + REPLY_SOURCE => ( + REPLY_SOURCE, + cell.source.as_ptr().cast(), + cell.source.len(), + None, + ), + REPLY_NATIVE => (REPLY_NATIVE, core::ptr::null(), 0, cell.opener), + other => (other, cell.error, 0, None), + }, + ) + }; + let (reply, reply_bytes, reply_len, reply_opener) = match served { + Ok(served) => served, + Err(message) => return fail(message), + }; + unsafe { + *bytes = reply_bytes; + *bytes_len = reply_len; + *opener = reply_opener; + } + reply +} + +// ------------------------------------------------ synchronous leaf calls +// +// Verbs that need no service at all: a clock-page projection and a CPU +// entropy draw. These run straight on the VM thread. Crossing the bridge +// for them would buy a park, a kick and a scheduling round trip in exchange +// for nothing — the answer is already readable from here. + +/// Why a clock read could not be answered. An uncalibrated clock is a +/// refusal, never a zero timestamp: `kore.time` would otherwise report the +/// monotonic epoch as a successful reading. +fn clock_text(error: TimerError) -> &'static CStr { + match error { + TimerError::Unsupported => c"this machine exposes no user-readable counter", + TimerError::Unconfigured => c"the clock is not configured", + TimerError::Uncalibrated => c"the clock is not calibrated yet", + TimerError::WrongCpu => c"the clock belongs to another CPU", + TimerError::ControllerUnavailable => c"the timer controller is unavailable", + TimerError::OutOfRange => c"the reading does not fit the counter width", + } +} + +/// Answer one `TimerResult` into the C out-parameters. +/// +/// # Safety +/// Both out-pointers are valid for writes. +unsafe fn answer_clock( + reading: Result, + out: *mut u64, + text: *mut *const c_char, +) -> c_int { + match reading { + Ok(nanoseconds) => { + unsafe { + *out = nanoseconds; + *text = core::ptr::null(); + } + STATUS_OK + } + Err(error) => { + let message = clock_text(error); + unsafe { + *out = 0; + *text = message.as_ptr(); + } + STATUS_UNAVAILABLE + } + } +} + +/// `kore.time.now` / `kore.time.mono_ns`: nanoseconds since the monotonic +/// epoch, projected from this thread's calibration page. +/// +/// # Safety +/// Both out-pointers are valid for writes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn kore_lua_host_uptime_ns(out: *mut u64, text: *mut *const c_char) -> c_int { + unsafe { + answer_clock( + kore::runtime::clock::uptime().map(|at| at.as_nanos()), + out, + text, + ) + } +} + +/// `kore.time.wall`: nanoseconds since the UNIX epoch. Separate from the +/// monotonic read because the two fail independently — a machine can have a +/// good counter and no wall anchor. +/// +/// # Safety +/// Both out-pointers are valid for writes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn kore_lua_host_wall_ns(out: *mut u64, text: *mut *const c_char) -> c_int { + unsafe { + answer_clock( + kore::runtime::clock::wall().map(|at| at.as_nanos()), + out, + text, + ) + } +} + +/// `kore.random.bytes`: fill `len` bytes from the CPU's entropy source. +/// +/// # Safety +/// `dst` points at `len` writable bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn kore_lua_host_random( + dst: *mut u8, + len: usize, + text: *mut *const c_char, +) -> c_int { + // Answer the empty draw before touching `dst`: a zero-length Lua buffer + // may hand us a null pointer, and `from_raw_parts_mut` demands a + // non-null one even for an empty slice. Zero bytes drawn is a success. + if len == 0 { + unsafe { *text = core::ptr::null() }; + return STATUS_OK; + } + if dst.is_null() { + unsafe { *text = c"no destination".as_ptr() }; + return STATUS_UNAVAILABLE; + } + let buffer = unsafe { core::slice::from_raw_parts_mut(dst, len) }; + match kore::runtime::random::fill(buffer) { + Ok(()) => { + unsafe { *text = core::ptr::null() }; + STATUS_OK + } + Err(_) => { + // Leave nothing half-drawn for a caller that ignores the status. + buffer.fill(0); + unsafe { *text = c"this machine exposes no entropy source".as_ptr() }; + STATUS_UNAVAILABLE + } + } +} + +/// `kore.random.available`: whether a draw can succeed at all, so a caller +/// can decide before doing work whose result it would have to throw away. +#[unsafe(no_mangle)] +pub extern "C" fn kore_lua_host_random_available() -> c_int { + c_int::from(kore::runtime::random::available()) +} + +/// The VM's seed (`luai_makeseed`, and `lua_newstate`'s own argument). +/// +/// It seeds string hashing, table iteration order, and — through +/// `luaL_makeseed` — `math.random`'s initial state. Drawing it from +/// hardware is what makes stock `math.random` behave the way every Lua +/// program assumes; the fixed constant it replaces produced one identical +/// sequence on every boot. A machine with no entropy source keeps that +/// constant, because a predictable seed is still a working VM. +#[unsafe(no_mangle)] +pub extern "C" fn kore_lua_host_seed() -> u32 { + seed() +} + +/// The seed a machine with no entropy source gets: ASCII `"KORE"`. +/// +/// This was the *only* seed before hardware entropy was wired in, so every +/// boot produced one identical `math.random` sequence and one identical +/// table iteration order. It survives as the fallback because a predictable +/// VM is still a working VM — a REPL should not fail to start because the +/// CPU exposes no generator. `kore_lua.c`'s weak `kore_lua_host_seed` keeps +/// the same value so a host smoke build reproduces target behaviour. +const UNSEEDED: u32 = 0x4b4f_5245; + +/// [`kore_lua_host_seed`] for the Rust side — `lua_newstate` takes the same +/// word as an argument, so both spellings draw from one place. +pub fn seed() -> u32 { + kore::runtime::random::u64().map_or(UNSEEDED, |word| word as u32) +} + +/// Release the source buffer a `REPLY_SOURCE` handed to C, once the chunk +/// is compiled. Worker-side, inside the evaluation. +#[unsafe(no_mangle)] +pub extern "C" fn kore_lua_host_release() { + if CELL.state.load(Ordering::Acquire) == EVAL_POSTED { + // SAFETY: EVAL_POSTED — the worker owns the cell. + let cell = unsafe { payload() }; + cell.source = Vec::new(); + } +} + +/// Every value-returning host call's half (`kore_lua.c`): post the op and +/// its words, park until served, hand back the outcome. Called on the +/// worker, inside an evaluation. +/// +/// Answers one of [`STATUS_OK`], [`STATUS_REFUSED`], [`STATUS_UNAVAILABLE`], +/// up to two result words, and a text that lives until the next call (C +/// copies it into a Lua string before returning). +/// +/// # Safety +/// The out-pointers are valid for writes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn kore_lua_host_call( + op: u32, + arg0: u64, + arg1: u64, + arg2: u64, + input: *const u8, + input_len: usize, + result: *mut u64, + result2: *mut u64, + text: *mut *const c_char, + text_len: *mut usize, +) -> c_int { + let fail = |message: &'static CStr| -> c_int { + unsafe { + *result = 0; + *result2 = 0; + *text = message.as_ptr(); + *text_len = message.count_bytes(); + } + STATUS_UNAVAILABLE + }; + let served = unsafe { + host_call( + |cell| { + cell.op = op; + cell.args = [arg0, arg1, arg2]; + cell.input = input; + cell.input_len = if input.is_null() { 0 } else { input_len }; + cell.status = STATUS_UNAVAILABLE; + cell.results = [0; 2]; + cell.text = Vec::new(); + }, + |cell| { + ( + cell.status, + cell.results, + cell.text.as_ptr().cast::(), + cell.text.len(), + ) + }, + ) + }; + let (status, results, reply_text, reply_len) = match served { + Ok(served) => served, + Err(message) => return fail(message), + }; + unsafe { + *result = results[0]; + *result2 = results[1]; + *text = reply_text; + *text_len = reply_len; + } + status +} + +// -------------------------------------------------------------- thread 0 + +/// What thread 0 reaches the store with, attached lazily on the first +/// `require` (a chunk that never requires never opens the filesystem). +pub struct Services<'r, 't> { + rt: &'r Rt, + transport: &'t TrapTransport, + /// The launch arguments, as `main` read them from ProcessIo. Owned + /// because the VM may ask for them at any point in a long REPL, long + /// after the caller's own borrow would have ended. + args: Vec, + fs: Option>, + loader: Option, + /// The exit code `os.exit` asked for, if a script asked. Read by the + /// embedder after the evaluation returns — `os.exit` cannot leave from + /// inside the VM without skipping every `` handler above it. + exit_request: Option, + /// The VM's open files. Beside the session rather than inside it: + /// `Fs` is the store's client and knows nothing of streams, and a + /// handle must not outlive the session that could close it. + files: crate::files::Files, +} + +impl<'r, 't> Services<'r, 't> { + pub fn new(rt: &'r Rt, transport: &'t TrapTransport, args: Vec) -> Self { + Self { + rt, + transport, + args, + fs: None, + loader: None, + exit_request: None, + files: crate::files::Files::default(), + } + } + + /// The exit code a script asked for with `os.exit`, if it did. + /// + /// The embedder checks this after each evaluation: a REPL stops reading + /// and a batch run leaves with it. + pub fn exit_request(&self) -> Option { + self.exit_request + } + + /// Close every file the VM still holds. + /// + /// The REPL's teardown calls this: a script that ended without + /// `:close()` has left session handles open, and the store must get + /// them back even though the `__gc` that would have returned them will + /// never run once the VM is gone. + pub async fn close_open_files(&mut self) { + if self.files.is_empty() { + return; + } + let views = self.files.drain(); + let Some(fs) = self.fs.as_mut() else { + return; + }; + for view in views { + fs.close(view).await; + } + } + + /// The filesystem client, attached on first use. + pub async fn fs(&mut self) -> Option<&mut Fs<'r, 't>> { + if self.fs.is_none() { + self.fs = Some(Fs::attach(self.rt, self.transport).await?); + } + self.fs.as_mut() + } +} + +/// The running VM: the worker, its doorbell, and the wake channel thread 0 +/// parks on. +pub struct Host<'t> { + worker: DoorbellWorker, + wake: NoticeRx<'t, TrapTransport>, +} + +impl<'t> Host<'t> { + /// Spawn the VM's worker. `None` when the task holds no channel space + /// (its profile lacks `channels`) or the thread cannot be founded. + pub fn start(transport: &'t TrapTransport) -> Option { + let worker = thread::spawn_with_doorbell(vm_thread::, &[])?; + let wake = NoticeRx::new(transport, worker.wake_recv()); + Some(Self { worker, wake }) + } + + /// Park until the worker kicks (or a spurious executor wake), having + /// first stood the wake post and re-read the word — the module doc's + /// discipline. `seen` is the state thread 0 last observed. + async fn park_until_kicked(&mut self, seen: u32) -> Result<(), Fatal> { + loop { + match self.wake.try_recv() { + // A kick landed; the caller re-reads the word. + Ok(Some(_)) => return Ok(()), + Ok(None) => { + if CELL.state.load(Ordering::Acquire) != seen { + return Ok(()); + } + let Some(post) = self.wake.standing_post() else { + return Err(Fatal); + }; + let _ = kore::runtime::executor::next_wake_on(post).await; + } + Err(RecvError::Refused(Status::WouldBlock)) => { + let _ = kore::runtime::executor::next_wake().await; + } + Err(_) => return Err(Fatal), + } + } + } + + /// Run `script` on the VM and collect its outcome and output, serving + /// every `require` it makes on the way. `chunk_name` is Lua's chunk + /// name (NUL-terminated, static); `repl` selects the REPL's + /// expression-then-statement grammar. + pub async fn eval( + &mut self, + services: &mut Services<'_, '_>, + extension: &mut E, + script: &[u8], + chunk_name: &'static [u8], + repl: bool, + ) -> Result<(Outcome, Vec, Vec), Fatal> { + match CELL.state.load(Ordering::Acquire) { + EVAL_DONE => {} + WORKER_GONE => { + // SAFETY: WORKER_GONE — thread 0 owns the cell. + let outcome = unsafe { payload().outcome }; + return Ok((outcome, Vec::new(), Vec::new())); + } + _ => return Err(Fatal), + } + { + // SAFETY: EVAL_DONE — thread 0 owns the cell. + let cell = unsafe { payload() }; + cell.kind = if repl { KIND_REPL } else { KIND_CHUNK }; + cell.script = script.as_ptr(); + cell.script_len = script.len(); + cell.chunk_name = chunk_name.as_ptr(); + cell.chunk_name_len = chunk_name.len(); + } + CELL.state.store(EVAL_POSTED, Ordering::Release); + // A refused kick is not a failure: it means no post stood, and the + // worker re-tests the word after it arms one (`futex::wait`'s + // contract), so the store above reaches it regardless. + let _ = self.worker.kick(); + loop { + match CELL.state.load(Ordering::Acquire) { + state @ (EVAL_POSTED | CALL_SERVED) => { + self.park_until_kicked(state).await?; + } + CALL_POSTED => { + serve_call(services, extension).await; + CELL.state.store(CALL_SERVED, Ordering::Release); + let _ = self.worker.kick(); + } + EVAL_DONE => { + // SAFETY: EVAL_DONE — thread 0 owns the cell; the + // script pointer is dead from here (its frame is ours). + let cell = unsafe { payload() }; + cell.script = core::ptr::null(); + cell.script_len = 0; + return Ok(( + cell.outcome, + core::mem::take(&mut cell.stdout), + core::mem::take(&mut cell.stderr), + )); + } + _ => return Err(Fatal), + } + } + } + + /// Stop the worker and join it, so the task's last thread is thread 0. + /// Joins only a worker that acknowledged `QUIT` (or is already gone): + /// a worker mid-evaluation is parked on the bridge and would never + /// return, so it is left to task teardown rather than waited on. + pub async fn quit(mut self) { + match CELL.state.load(Ordering::Acquire) { + EVAL_DONE => { + CELL.state.store(QUIT, Ordering::Release); + let _ = self.worker.kick(); + loop { + match CELL.state.load(Ordering::Acquire) { + WORKER_GONE => break, + state => { + if self.park_until_kicked(state).await.is_err() { + return; + } + } + } + } + } + WORKER_GONE => {} + _ => return, + } + let _ = self.worker.join(); + } +} + +/// Answer the host call the cell holds. CALL_POSTED — thread 0 owns the +/// cell for the whole call. One dispatcher: a new standard-library verb +/// is an arm here and nothing about the transport changes. +async fn serve_call(services: &mut Services<'_, '_>, extension: &mut E) { + // SAFETY: CALL_POSTED. + let op = unsafe { payload() }.op; + if op == OP_REQUIRE { + serve_require(services).await; + } else { + serve_value(services, extension, op).await; + } +} + +/// Answer the `require` the cell holds: read the source, or link the +/// native module and look its opener up. +async fn serve_require(services: &mut Services<'_, '_>) { + let (name, len) = { + // SAFETY: CALL_POSTED. + let cell = unsafe { payload() }; + (cell.name, cell.name_len) + }; + let reply = resolve(services, &name[..len.min(FS_NAME_BYTES)]).await; + // SAFETY: CALL_POSTED. + let cell = unsafe { payload() }; + match reply { + Reply::Source(source) => { + cell.reply = REPLY_SOURCE; + cell.source = source; + cell.opener = None; + cell.error = core::ptr::null(); + } + Reply::Native(opener) => { + cell.reply = REPLY_NATIVE; + cell.source = Vec::new(); + cell.opener = Some(opener); + cell.error = core::ptr::null(); + } + Reply::Error(message) => { + cell.reply = REPLY_ERROR; + cell.source = Vec::new(); + cell.opener = None; + cell.error = message.as_ptr(); + } + } +} + +/// Answer every value-returning op. The outcome is written back as a +/// status, up to two words, and a text; nothing here panics on a refusal. +async fn serve_value(services: &mut Services<'_, '_>, extension: &mut E, op: u32) { + let args = { + // SAFETY: CALL_POSTED. + unsafe { payload() }.args + }; + // The call's byte argument, borrowed from the Lua frame the parked + // worker is holding open. Read here, once, so no arm below repeats + // the reasoning about why this pointer is live. + // + // SAFETY: CALL_POSTED — thread 0 owns the cell, and the worker set + // this window from a Lua value on its own stack before parking. It + // stays parked, and so the value stays rooted, until CALL_SERVED. + let input: &[u8] = unsafe { + let cell = payload(); + if cell.input.is_null() || cell.input_len == 0 { + &[] + } else { + core::slice::from_raw_parts(cell.input, cell.input_len) + } + }; + if (OP_FS_OPEN..=OP_FS_LIST).contains(&op) { + let answer = serve_fs(services, op, args, input).await; + return finish_answer(answer); + } + let answer = match op { + // The executor's own timer — the one verb `kore.time` cannot serve + // from the VM thread, because parking is what it is for. + OP_SLEEP => { + kore::runtime::executor::sleep_for(args[0]).await; + Answer::Value(0, 0) + } + // The launch arguments, answered as text. A REPL was launched + // with none, so an empty answer is the honest one, not a refusal. + OP_PROC_ARGS => { + let launch = services.args.clone(); + return finish(STATUS_OK, [0; 2], launch); + } + // Recorded, not acted on: the raise that follows this call is what + // unwinds the evaluation, running every `` on the way out. + OP_PROC_EXIT => { + services.exit_request = Some(args[0] as i32); + Answer::Value(0, 0) + } + // Anything this crate does not own belongs to the embedding + // program, which was launched holding the face it needs. + op if op >= OP_EMBEDDER_BASE => extension.serve(op, args).await, + _ => Answer::Unavailable(alloc::string::String::from("unknown host call")), + }; + finish_answer(answer) +} + +/// Write one [`Answer`] back into the cell. Every op ends here, so the +/// three shapes a script sees are decided in exactly one place. +fn finish_answer(answer: Answer) { + match answer { + Answer::Value(result, result2) => finish(STATUS_OK, [result, result2], Vec::new()), + Answer::Bytes(count, bytes) => finish(STATUS_OK, [count, bytes.len() as u64], bytes), + Answer::Refused(text) => finish(STATUS_REFUSED, [0; 2], text.into_bytes()), + Answer::Unavailable(text) => finish(STATUS_UNAVAILABLE, [0; 2], text.into_bytes()), + } +} + +/// The `kore.fs` arms. +/// +/// Every one needs the filesystem session, so it is attached once here +/// rather than in eleven places, and a task whose launch conveyed no +/// filesystem face answers "unavailable" — rule 6's honest absence, not a +/// refusal that would read as though the file were the problem. +async fn serve_fs( + services: &mut Services<'_, '_>, + op: u32, + args: [u64; 3], + input: &[u8], +) -> Answer { + // Split the borrow: `files` and `fs` are distinct fields, and every + // verb below needs both at once. + let files = &mut services.files; + let fs = match services.fs.as_mut() { + Some(fs) => fs, + None => { + let Some(attached) = Fs::attach(services.rt, services.transport).await else { + return Answer::Unavailable(alloc::string::String::from( + "this task holds no filesystem face", + )); + }; + services.fs.insert(attached) + } + }; + match op { + OP_FS_OPEN => crate::files::open(fs, files, input, args[0]).await, + OP_FS_CLOSE => crate::files::close(fs, files, args[0]).await, + OP_FS_READ => crate::files::read(fs, files, args[0], args[1], args[2]).await, + OP_FS_WRITE => crate::files::write(fs, files, args[0], args[1], input).await, + OP_FS_SYNC => crate::files::sync(fs, files, args[0]).await, + OP_FS_RESIZE => crate::files::resize(fs, files, args[0], args[1]).await, + OP_FS_STAT => crate::files::stat(fs, files, args[0], input).await, + OP_FS_REMOVE => crate::files::remove(fs, input).await, + // `rename` carries two paths in one window, separated by a NUL — + // a name may not contain one, so the split is unambiguous. + OP_FS_RENAME => match input.iter().position(|byte| *byte == 0) { + Some(cut) => crate::files::rename(fs, &input[..cut], &input[cut + 1..]).await, + None => Answer::Refused(alloc::string::String::from("invalid-name")), + }, + OP_FS_LIST => crate::files::list(fs, input).await, + _ => Answer::Unavailable(alloc::string::String::from("unknown filesystem call")), + } +} + +/// Write one value-op outcome back into the cell. Every op answers the +/// same three things, so every arm ends here. +fn finish(status: c_int, results: [u64; 2], text: Vec) { + // SAFETY: CALL_POSTED. + let cell = unsafe { payload() }; + cell.status = status; + cell.results = results; + cell.text = text; +} + +enum Reply { + Source(Vec), + Native(LuaCFunction), + Error(&'static CStr), +} + +async fn resolve(services: &mut Services<'_, '_>, stem: &[u8]) -> Reply { + let search = Search { prefixes: SEARCH }; + let Some(fs) = services.fs().await else { + return Reply::Error(c"filesystem face is unavailable"); + }; + let mut buf = Vec::new(); + if buf.try_reserve_exact(MAX_SOURCE_MODULE_BYTES).is_err() { + return Reply::Error(c"source buffer allocation refused"); + } + buf.resize(MAX_SOURCE_MODULE_BYTES, 0); + match Loader::read_source(fs, search, stem, &mut buf).await { + Ok(len) => { + buf.truncate(len); + return Reply::Source(buf); + } + Err(LoadError::NotFound) => {} + Err(error) => return Reply::Error(describe(error)), + } + drop(buf); + if services.loader.is_none() { + match Loader::new(services.rt) { + Ok(loader) => services.loader = Some(loader), + Err(error) => return Reply::Error(describe(error)), + } + } + let (Some(fs), Some(loader)) = (services.fs.as_mut(), services.loader.as_mut()) else { + return Reply::Error(c"filesystem face is unavailable"); + }; + let id = match loader.load_native(fs, search, stem).await { + Ok(id) => id, + Err(error) => return Reply::Error(describe(error)), + }; + let mut symbol = [0u8; OPENER_PREFIX.len() + FS_NAME_BYTES]; + symbol[..OPENER_PREFIX.len()].copy_from_slice(OPENER_PREFIX); + symbol[OPENER_PREFIX.len()..OPENER_PREFIX.len() + stem.len()].copy_from_slice(stem); + match loader.lookup_executable(id, &symbol[..OPENER_PREFIX.len() + stem.len()]) { + // SAFETY: the address of an exported function in a module this + // task linked and published executable; the C ABI is the host's. + Some(address) => { + Reply::Native(unsafe { core::mem::transmute::<*const u8, LuaCFunction>(address) }) + } + None => Reply::Error(c"module exports no executable luaopen_"), + } +} + +/// One line per refusal class — what the Lua error says after +/// `module '' not found:`. +fn describe(error: LoadError) -> &'static CStr { + match error { + LoadError::Name => c"module name is not a store name", + LoadError::NotFound => c"no .lua in the task tree and no libs/.so", + LoadError::NotExecutableImage => c"libs/ member is not stamped EXECUTABLE_IMAGE", + LoadError::TooLarge => c"module exceeds the bounded loader", + LoadError::TooMany => c"module table is full", + LoadError::Fs(_) | LoadError::Pair => c"filesystem refused", + LoadError::Check(_) => c"module failed the image check", + LoadError::Plan(_) => c"module layout is not late-linkable (TLS, or data below code)", + LoadError::HostTls => c"host carries TLS; a late scope cannot reproduce it", + LoadError::NoRecord(1) => c"host has no .kore.ld block", + LoadError::NoRecord(2) => c"host has no passive module page at the fixed address", + LoadError::NoRecord(3) => c"passive page's first record is not this executable", + LoadError::NoRecord(4) => c"host geometry has no LINK window", + LoadError::NoRecord(5) => c"passive page address overflowed", + LoadError::NoRecord(6) => c"passive page records no live segment", + LoadError::NoRecord(_) => c"host segment could not be viewed", + LoadError::NoWindow | LoadError::NoSupply | LoadError::Map | LoadError::Publish => { + c"arena refused the module" + } + LoadError::Link(_) => c"module did not link against the host", + LoadError::InitializerArray => c"module initializer array is malformed", + LoadError::Seal => c"module RELRO protection was refused", + LoadError::Initializer(kore_late::FunctionError::Shape) => { + c"module initializer array is malformed" + } + LoadError::Initializer(kore_late::FunctionError::Target(_)) => { + c"module initializer is outside loaded executable text" + } + LoadError::TooManyInitializers => c"module has too many initializers", + } +} diff --git a/crates/lua/src/files.rs b/crates/lua/src/files.rs new file mode 100644 index 0000000..10ecbfe --- /dev/null +++ b/crates/lua/src/files.rs @@ -0,0 +1,628 @@ +//! `kore.fs`, and everything `io` and `os` are spellings of. +//! +//! The filesystem session lives on thread 0 and a [`View`] is affine, so +//! Lua never holds one. It holds a *handle* — an index and a generation +//! into the [`Files`] table below — and every verb here resolves that +//! handle back to the view before touching the store. A handle whose slot +//! was recycled names a generation that no longer matches, so a use after +//! close is [`FsError::InvalidHandle`] rather than a read of somebody +//! else's file. +//! +//! Every verb here is *positional*. DagFS addresses bytes absolutely +//! (`read_at` / `write_at`), and a stream position is a POSIX idea that +//! belongs to the file object a script holds — which is in C, because +//! `io.open` and `kore.fs.open` answer the *same* object and that object +//! also owns the read-ahead buffer. Keeping a second cursor here would be +//! two authorities for one number, and they would drift the first time a +//! buffered read looked further ahead than the script had consumed. +//! +//! Every verb answers in the three shapes [`Answer`] names, and an expected +//! failure — a missing file, a denied right — is a `Refused` carrying the +//! kebab-case spelling of the [`FsError`] that caused it, never a raise. +//! One vocabulary, two spellings: `kore.fs` renders it as a table and `io` +//! as a message and an errno (`res/SYSTEM_LIBRARY.md`, the six rules). + +use alloc::{string::String, vec, vec::Vec}; + +use kore::fs::{Fs, FsError, Name, Refusal, Rights, View, dag::ObjectKind}; + +use crate::Answer; + +/// How many files one VM may hold open at once. +/// +/// A bound, not a policy: the table is a `Vec` that grows to this and no +/// further, so a script that leaks handles is refused at a stated ceiling +/// instead of consuming the task's heap until something else fails. The +/// store's own session handle table is the real limit; this one exists so +/// the refusal names the script's mistake. +const MAX_OPEN_FILES: usize = 256; + +/// The largest single read a verb will answer. +/// +/// `f:read("a")` on a big file loops rather than asking for all of it at +/// once: the reply crosses the bridge as one buffer, and an unbounded one +/// would be an unbounded allocation on thread 0 driven by a script. +const MAX_READ_BYTES: usize = 1 << 20; + +/// One open file the VM holds a handle to. +struct OpenFile { + view: View, + /// Bumped when the slot is reused, so a handle from a closed file is + /// detectably stale rather than an alias of whatever took its place. + generation: u32, + readable: bool, + writable: bool, + /// Every write goes to the current end, whatever offset the caller + /// passed — `io.open`'s `"a"` modes. + append: bool, +} + +/// The VM's open files. +/// +/// Lives in `Services`, so it is created with the filesystem session and +/// retired with it: a handle cannot outlive the session that could close +/// it. +#[derive(Default)] +pub struct Files { + slots: Vec>, + /// Monotonic across the whole table, so a generation is unique to one + /// open and not merely to one slot's history. + next_generation: u32, +} + +/// A Lua-visible handle: the slot index and the generation that slot held +/// when the file was opened. +/// +/// One word, because the transport carries words. The generation is the +/// high half so a handle printed in a diagnostic is legible as a pair. +fn encode(index: usize, generation: u32) -> u64 { + (u64::from(generation) << 32) | index as u64 +} + +fn decode(handle: u64) -> (usize, u32) { + ((handle & 0xffff_ffff) as usize, (handle >> 32) as u32) +} + +impl Files { + fn resolve(&mut self, handle: u64) -> Result<&mut OpenFile, FsError> { + let (index, generation) = decode(handle); + match self.slots.get_mut(index) { + Some(Some(file)) if file.generation == generation => Ok(file), + _ => Err(FsError::InvalidHandle), + } + } + + fn insert(&mut self, view: View, readable: bool, writable: bool, append: bool) -> Option { + let generation = self.next_generation; + let file = OpenFile { + view, + generation, + readable, + writable, + append, + }; + let index = match self.slots.iter().position(Option::is_none) { + Some(index) => { + self.slots[index] = Some(file); + index + } + None => { + if self.slots.len() >= MAX_OPEN_FILES { + return None; + } + self.slots.push(Some(file)); + self.slots.len() - 1 + } + }; + self.next_generation = self.next_generation.wrapping_add(1); + Some(encode(index, generation)) + } + + /// Take the file out of the table, leaving the slot free. + /// + /// The caller still owes the store a `close`: the view is affine and + /// dropping it would retire the handle locally while the session still + /// believes it is open. + fn take(&mut self, handle: u64) -> Result { + let (index, generation) = decode(handle); + match self.slots.get_mut(index) { + Some(slot @ Some(_)) if slot.as_ref().is_some_and(|f| f.generation == generation) => { + Ok(slot.take().expect("just matched Some").view) + } + _ => Err(FsError::InvalidHandle), + } + } + + /// Whether any file is still open — the REPL's teardown asks, so a + /// script that forgot `:close()` is closed for it rather than leaking + /// a session handle. + pub fn is_empty(&self) -> bool { + self.slots.iter().all(Option::is_none) + } + + /// Every live handle, for teardown. + pub fn drain(&mut self) -> Vec { + self.slots + .iter_mut() + .filter_map(|slot| slot.take().map(|file| file.view)) + .collect() + } +} + +/// The kebab-case spelling of a filesystem error. +/// +/// Derived from `FsError`'s own variants rather than invented, which is +/// rule 2: the same word appears in a `kore.fs` error table, in an `io` +/// message, and in the C ABI's `KoreFsError`. The match is exhaustive on +/// purpose — a new variant upstream must be spelled here consciously. +pub fn code(error: FsError) -> &'static str { + match error { + FsError::NotFound => "not-found", + FsError::AlreadyExists => "already-exists", + FsError::NotDirectory => "not-directory", + FsError::IsDirectory => "is-directory", + FsError::InvalidName => "invalid-name", + FsError::InvalidHandle => "invalid-handle", + FsError::AccessDenied => "access-denied", + FsError::AccessLost => "access-lost", + FsError::Conflict => "conflict", + FsError::Cycle => "cycle", + FsError::NotEmpty => "not-empty", + FsError::OutOfObjects => "out-of-objects", + FsError::OutOfEdges => "out-of-edges", + FsError::OutOfHandles => "out-of-handles", + FsError::OutOfStorage => "out-of-storage", + FsError::OutOfPlacements => "out-of-placements", + FsError::OutOfVersionLeases => "out-of-version-leases", + FsError::OutOfIndexSlots => "out-of-index-slots", + FsError::Range => "range", + FsError::BudgetExceeded => "budget-exceeded", + FsError::IndexIncomplete => "index-incomplete", + FsError::Unsupported => "unsupported", + FsError::NoWindow => "no-window", + FsError::Busy => "busy", + FsError::ProviderFailure(_) => "provider-failure", + } +} + +/// A refusal in the one vocabulary, whatever layer produced it. +/// +/// A transport fault is not an `FsError` — the request may even have +/// committed — so it keeps its own word rather than being folded into the +/// nearest filesystem code, which would tell a caller something untrue +/// about what happened to its write. +fn refusal(error: Refusal) -> Answer { + Answer::Refused(String::from(match error { + Refusal::Fs(error) => code(error), + Refusal::Pair => "transport", + Refusal::Usage(_) => "usage", + })) +} + +fn refused(error: FsError) -> Answer { + Answer::Refused(String::from(code(error))) +} + +/// The open modes, as the C side packs them into `arg0`. +/// +/// Bits rather than a string because the transport carries words; the +/// spelling (`"r"`, `"w+"`, `"a"`) is parsed in C where the Lua argument +/// already is. +pub mod mode { + pub const READ: u64 = 1 << 0; + pub const WRITE: u64 = 1 << 1; + /// Create the file when it does not exist. + pub const CREATE: u64 = 1 << 2; + /// Empty it when it does. + pub const TRUNCATE: u64 = 1 << 3; + /// Every write goes to the end. + pub const APPEND: u64 = 1 << 4; + /// Refuse when it already exists (`os.tmpname`'s guarantee). + pub const EXCLUSIVE: u64 = 1 << 5; + /// Make a directory rather than a file. + pub const DIRECTORY: u64 = 1 << 6; +} + +/// Split a path into its parent and final component. +/// +/// `None` when the path names the root itself, which has no parent to +/// mutate — refusing that is what stops `os.remove("/")`. +fn split(path: &[u8]) -> Option<(&[u8], &[u8])> { + let trimmed = path.strip_suffix(b"/").unwrap_or(path); + if trimmed.is_empty() { + return None; + } + match trimmed.iter().rposition(|byte| *byte == b'/') { + Some(0) => Some((b"/", &trimmed[1..])), + Some(cut) => Some((&trimmed[..cut], &trimmed[cut + 1..])), + None => Some((b".", trimmed)), + } +} + +fn name(bytes: &[u8]) -> Result { + Name::from_bytes(bytes) +} + +/// Open — the one verb that creates a handle. +pub async fn open(fs: &mut Fs<'_, '_>, files: &mut Files, path: &[u8], flags: u64) -> Answer { + let readable = flags & mode::READ != 0; + let writable = flags & mode::WRITE != 0; + let append = flags & mode::APPEND != 0; + let mut rights = Rights::NONE; + if readable { + rights = rights.union(Rights::READ); + } + if writable { + rights = rights.union(Rights::WRITE); + } + // A directory is made, never opened for bytes: `kore.fs.mkdir` is this + // verb with DIRECTORY set, so one arm creates both kinds and the + // already-exists answer is identical for each. + if flags & mode::DIRECTORY != 0 { + return match create(fs, path, ObjectKind::Directory).await { + Ok(view) => { + fs.close(view).await; + Answer::Value(0, 0) + } + Err(error) => refusal(error), + }; + } + let existing = fs.open(path, rights.union(Rights::READ)).await; + let view = match existing { + Ok(view) if flags & mode::EXCLUSIVE != 0 => { + // The file is there and the caller demanded it not be. Close + // the view before answering: the refusal must not also leak a + // session handle. + fs.close(view).await; + return refused(FsError::AlreadyExists); + } + Ok(view) => view, + Err(Refusal::Fs(FsError::NotFound)) if flags & mode::CREATE != 0 => { + match create(fs, path, ObjectKind::File).await { + Ok(view) => view, + Err(error) => return refusal(error), + } + } + Err(error) => return refusal(error), + }; + if view.is_directory() { + fs.close(view).await; + return refused(FsError::IsDirectory); + } + if flags & mode::TRUNCATE != 0 + && let Err(error) = fs.resize(&view, 0).await + { + fs.close(view).await; + return refusal(error); + } + let length = view.len(); + match files.insert(view, readable, writable, append) { + // The length rides back beside the handle: an append stream starts + // its position at the end, and the object in C would otherwise + // have to stat immediately just to learn where it is. + Some(handle) => Answer::Value( + handle, + if flags & mode::TRUNCATE != 0 { + 0 + } else { + length + }, + ), + None => { + // The table is full. Give the store its handle back rather + // than holding one the VM can no longer name. + if let Ok(view) = files.take(u64::MAX) { + fs.close(view).await; + } + refused(FsError::OutOfHandles) + } + } +} + +/// Create the final component of `path` beneath its parent. +async fn create(fs: &mut Fs<'_, '_>, path: &[u8], kind: ObjectKind) -> Result { + let Some((parent, leaf)) = split(path) else { + return Err(Refusal::Fs(FsError::AlreadyExists)); + }; + let leaf = name(leaf).map_err(Refusal::Fs)?; + let parent = fs + .open(parent, Rights::READ.union(Rights::MUTATE_NAMESPACE)) + .await?; + let made = fs.create(&parent, leaf, kind).await; + fs.close(parent).await; + made +} + +pub async fn close(fs: &mut Fs<'_, '_>, files: &mut Files, handle: u64) -> Answer { + match files.take(handle) { + Ok(view) => { + // `close_checked` so a refused close is reported rather than + // silently dropping a view the session still holds. + match fs.close_checked(view).await { + Ok(()) => Answer::Value(0, 0), + Err(failure) => { + // The store kept it. Drop ours — retrying from Lua + // would need a handle we have already retired. + fs.close(failure.view).await; + refusal(failure.error) + } + } + } + Err(error) => refused(error), + } +} + +/// Read up to `count` bytes from `offset`. +/// +/// Short is not an error: a read that reaches the end answers the bytes it +/// got, and a read entirely past the end answers zero of them. `results[0]` +/// states the count, so a caller tells "read nothing" from "at end of file" +/// without a second call. +pub async fn read( + fs: &mut Fs<'_, '_>, + files: &mut Files, + handle: u64, + offset: u64, + count: u64, +) -> Answer { + let file = match files.resolve(handle) { + Ok(file) => file, + Err(error) => return refused(error), + }; + if !file.readable { + return refused(FsError::AccessDenied); + } + let count = (count as usize).min(MAX_READ_BYTES); + let mut buffer = vec![0u8; count]; + let mut filled = 0; + while filled < count { + match fs + .read_at(&file.view, offset + filled as u64, &mut buffer[filled..]) + .await + { + Ok(0) => break, + Ok(got) => filled += got, + Err(error) => return refusal(error), + } + } + buffer.truncate(filled); + let length = buffer.len() as u64; + Answer::Bytes(length, buffer) +} + +/// Write `bytes` at `offset`, answering how many were acknowledged and +/// where that leaves the end of the written run. +/// +/// An append file ignores `offset` entirely and writes at the current end, +/// re-read now rather than remembered from open: another writer may have +/// extended the file since, and an append that overwrote their bytes would +/// not be an append. +pub async fn write( + fs: &mut Fs<'_, '_>, + files: &mut Files, + handle: u64, + offset: u64, + bytes: &[u8], +) -> Answer { + let file = match files.resolve(handle) { + Ok(file) => file, + Err(error) => return refused(error), + }; + if !file.writable { + return refused(FsError::AccessDenied); + } + let start = if file.append { + match fs.stat(&file.view).await { + Ok(stat) => stat.len, + Err(error) => return refusal(error), + } + } else { + offset + }; + finish_write(fs, files, handle, start, bytes).await +} + +async fn finish_write( + fs: &mut Fs<'_, '_>, + files: &mut Files, + handle: u64, + start: u64, + bytes: &[u8], +) -> Answer { + let mut written = 0; + while written < bytes.len() { + let view = match files.resolve(handle) { + Ok(file) => &file.view, + Err(error) => return refused(error), + }; + // The view is borrowed per chunk rather than across the loop: the + // store's window bounds each transfer, so the call is already a + // loop and holding the borrow would only forbid the resolve. + match fs + .write_at(view, start + written as u64, &bytes[written..]) + .await + { + Ok(0) => break, + Ok(count) => written += count, + Err(error) => { + // A short write is a fact, not a detail: report the + // acknowledged prefix so a caller knows what committed. + if written == 0 { + return refusal(error); + } + break; + } + } + } + Answer::Value(written as u64, start + written as u64) +} + +pub async fn sync(fs: &mut Fs<'_, '_>, files: &mut Files, handle: u64) -> Answer { + let view = match files.resolve(handle) { + Ok(file) => &file.view, + Err(error) => return refused(error), + }; + match fs.sync(view).await { + Ok(()) => Answer::Value(0, 0), + Err(error) => refusal(error), + } +} + +pub async fn resize(fs: &mut Fs<'_, '_>, files: &mut Files, handle: u64, length: u64) -> Answer { + let file = match files.resolve(handle) { + Ok(file) => file, + Err(error) => return refused(error), + }; + if !file.writable { + return refused(FsError::AccessDenied); + } + match fs.resize(&file.view, length).await { + Ok(_) => Answer::Value(length, 0), + Err(error) => refusal(error), + } +} + +/// `stat`, by open handle when `path` is empty and by path otherwise. +/// +/// Both spellings are needed and neither is redundant: `f:seek("end")` has +/// a handle and must not re-walk a path that may since have been renamed, +/// while `os.rename` wants to know a destination exists without opening it. +/// +/// Answers the length and a kind word: 0 file, 1 directory. +pub async fn stat(fs: &mut Fs<'_, '_>, files: &mut Files, handle: u64, path: &[u8]) -> Answer { + if path.is_empty() { + let view = match files.resolve(handle) { + Ok(file) => &file.view, + Err(error) => return refused(error), + }; + let directory = view.is_directory(); + return match fs.stat(view).await { + Ok(stat) => Answer::Value(stat.len, u64::from(directory)), + Err(error) => refusal(error), + }; + } + let view = match fs.open(path, Rights::READ).await { + Ok(view) => view, + Err(error) => return refusal(error), + }; + let directory = view.is_directory(); + let answer = match fs.stat(&view).await { + Ok(stat) => Answer::Value(stat.len, u64::from(directory)), + Err(error) => refusal(error), + }; + fs.close(view).await; + answer +} + +pub async fn remove(fs: &mut Fs<'_, '_>, path: &[u8]) -> Answer { + let Some((parent, leaf)) = split(path) else { + return refused(FsError::AccessDenied); + }; + let leaf = match name(leaf) { + Ok(leaf) => leaf, + Err(error) => return refused(error), + }; + let parent = match fs + .open(parent, Rights::READ.union(Rights::MUTATE_NAMESPACE)) + .await + { + Ok(view) => view, + Err(error) => return refusal(error), + }; + let answer = match fs.unlink(&parent, leaf).await { + Ok(()) => Answer::Value(0, 0), + Err(error) => refusal(error), + }; + fs.close(parent).await; + answer +} + +/// `os.rename`, including across directories — `move_path` is the store's +/// own verb for that, so this is not a copy-and-unlink and it does not +/// lose the object's identity or its grants. +pub async fn rename(fs: &mut Fs<'_, '_>, from: &[u8], to: &[u8]) -> Answer { + match fs.move_path(from, to).await { + Ok(_) => Answer::Value(0, 0), + Err(error) => refusal(error), + } +} + +/// List a directory as NUL-separated names. +/// +/// One buffer rather than a cursor per call: a directory listing is the +/// kind of thing a script iterates once, and paging it would put a second +/// piece of mutable state in the handle table for no gain at these sizes. +pub async fn list(fs: &mut Fs<'_, '_>, path: &[u8]) -> Answer { + let mut names: Vec = Vec::new(); + let mut overflow = false; + let listed = fs + .list_directory(path, |entry| { + // Stop at the cap rather than growing without bound; the + // caller is told by `Range` that the listing was not complete. + if names.len() + entry.name.len() + 1 > MAX_READ_BYTES { + overflow = true; + return Err(Refusal::Fs(FsError::Range)); + } + names.extend_from_slice(entry.name.as_bytes()); + names.push(0); + Ok(()) + }) + .await; + match listed { + _ if overflow => refused(FsError::Range), + Ok(()) => { + let count = names.iter().filter(|byte| **byte == 0).count() as u64; + Answer::Bytes(count, names) + } + Err(error) => refusal(error), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_path_splits_into_a_parent_and_a_leaf() { + assert_eq!(split(b"/etc/hosts"), Some((&b"/etc"[..], &b"hosts"[..]))); + assert_eq!(split(b"/hosts"), Some((&b"/"[..], &b"hosts"[..]))); + assert_eq!(split(b"hosts"), Some((&b"."[..], &b"hosts"[..]))); + // A trailing slash names the same directory, not an empty leaf. + assert_eq!(split(b"/etc/"), Some((&b"/"[..], &b"etc"[..]))); + } + + #[test] + fn the_root_has_no_parent_to_mutate() { + // This is what refuses `os.remove("/")` before it reaches the store. + assert_eq!(split(b"/"), None); + assert_eq!(split(b""), None); + } + + #[test] + fn a_handle_survives_a_round_trip_and_a_recycled_slot_does_not_alias() { + let handle = encode(3, 7); + assert_eq!(decode(handle), (3, 7)); + // Same slot, later generation: the old handle must not resolve. + assert_ne!(encode(3, 8), handle); + } + + #[test] + fn every_filesystem_error_has_a_kebab_case_spelling() { + // Rule 2: derived, not invented. A code with an underscore or a + // capital would mean someone spelled one by hand. + for error in [ + FsError::NotFound, + FsError::AlreadyExists, + FsError::InvalidHandle, + FsError::OutOfVersionLeases, + ] { + let spelling = code(error); + assert!(!spelling.is_empty()); + assert!( + spelling + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte == b'-') + ); + } + } +} diff --git a/crates/lua/src/kore_lua.c b/crates/lua/src/kore_lua.c new file mode 100644 index 0000000..14d88a2 --- /dev/null +++ b/crates/lua/src/kore_lua.c @@ -0,0 +1,2662 @@ +/* + * The kore-owned Lua amalgamation. Zig supplies ISO C declarations while the + * final image deliberately links no hosted libc. Every live external symbol + * is satisfied by Rust compiler-builtins or by the Rust shim modules. + */ +#include "lprefix.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(KORE_LUA_ZIG_MUSL_ABI) +/* + * Lua's error path enters the musl jmp_buf supplied by Zig's target headers, + * then returns through the pinned assembly in vendor/musl-setjmp. Admit the + * Zig 0.16 toolchain family only when that header retains the exact register + * prefix encoded by the assembly; a version string alone is not an ABI proof. + */ +#if defined(__x86_64__) +#define KORE_LUA_JMP_WORDS 8u +#elif defined(__aarch64__) +#define KORE_LUA_JMP_WORDS 22u +#elif defined(__riscv) && __riscv_xlen == 64 +#define KORE_LUA_JMP_WORDS 26u +#else +#error unsupported musl setjmp target +#endif + +_Static_assert(sizeof(unsigned long) == 8u, + "musl setjmp assembly requires 64-bit words"); +_Static_assert(offsetof(struct __jmp_buf_tag, __jb) == 0u, + "musl jmp_buf register state must be its first field"); +_Static_assert(sizeof(((struct __jmp_buf_tag *)0)->__jb) == + KORE_LUA_JMP_WORDS * sizeof(unsigned long), + "Zig musl jmp_buf no longer matches vendored setjmp assembly"); +_Static_assert(_Alignof(jmp_buf) >= 8u, + "musl setjmp assembly requires 8-byte jmp_buf alignment"); +#endif + +/* + * Export the C API. A native module `require`d into this task binds its + * lua_* / luaL_* imports to these symbols when kore-late links it + * (res/IMAGE.md §8); the engine is otherwise compiled -fvisibility=hidden + * and luaconf.h's guarded default yields to this definition. The linker + * keeps the set in .dynsym with --export-dynamic-symbol=lua_* / luaL_*. + * The exported set is the module ABI. + */ +#define LUA_API __attribute__((visibility("default"))) extern +/* + * The VM runs on a kore_rt worker thread with a 64 KiB stack and a guard + * page (crates/rt/src/thread.rs STACK_BYTES) — a quarter of what Lua's + * stock cap of 200 nested C levels assumes. LUAI_MAXCCALLS bounds every + * recursion implemented in C (parser nesting, metamethod chains, C-function + * re-entry); budgeting 1 KiB of C stack per level, 60 levels leave the + * remaining ~4 KiB-per-level of headroom to the host frames beneath the + * engine. Exceeding it is a Lua "C stack overflow" error, never a fault. + */ +#define LUAI_MAXCCALLS 60 +#define LUA_CORE +#define LUA_LIB +#include "luaconf.h" + +/* A kore process has no ambient locale or stdio. */ +#undef lua_getlocaledecpoint +#define lua_getlocaledecpoint() '.' +#define l_strcoll strcmp +#define lua_writestring(s, l) ((void)(s), (void)(l)) +#define lua_writeline() ((void)0) +#define lua_writestringerror(s, p) ((void)(s), (void)(p)) + +/* Rust owns the deterministic numeric/formatting and fatal seams. */ +extern double kore_lua_strtod(const char *text, char **end); +extern size_t kore_lua_format_number(char *dst, size_t cap, double value, + size_t precision); +extern void kore_lua_abort(void) __attribute__((noreturn)); + +#undef lua_str2number +#define lua_str2number(s, p) kore_lua_strtod((s), (p)) +/* Lua's own freestanding hexadecimal reader/writer avoids libc %a support. */ +#undef lua_strx2number +#undef lua_number2strx + +static int kore_lua_snprintf(char *dst, size_t cap, const char *format, ...); +#undef l_sprintf +#define l_sprintf(s, sz, f, i) kore_lua_snprintf((s), (sz), (f), (i)) + +/* `setjmp`/`longjmp` are the only non-local control transfer used by Lua. */ +#define abort kore_lua_abort +/* + * The VM seed: string hashing, table iteration order, and — through + * `luaL_makeseed` — `math.random`'s initial state (lmathlib.c's + * `setseed(..., luaL_makeseed(L), 0)`). Upstream derives it from ASLR and + * `time()`, neither of which a kore process has; the Rust host draws it + * from the CPU instead, and falls back to the fixed constant this replaced + * only where the machine exposes no entropy source at all. + */ +extern unsigned kore_lua_host_seed(void); +/* ASCII "KORE" — the value src/bridge.rs calls UNSEEDED and falls back to when + the machine exposes no generator. Kept identical here so a host smoke + build, which links no Rust host, reproduces that same unseeded VM. */ +#define KORE_LUA_UNSEEDED 0x4b4f5245u +__attribute__((weak)) unsigned kore_lua_host_seed(void) { return KORE_LUA_UNSEEDED; } +#define luai_makeseed() kore_lua_host_seed() + +/* Keep internal engine names local to this one object. */ +#undef LUAI_FUNC +#undef LUAI_DDEC +#undef LUAI_DDEF +#define LUAI_FUNC static +#define LUAI_DDEC(def) +#define LUAI_DDEF static + +/* Core. */ +#include "lzio.c" +#include "lctype.c" +#include "lopcodes.c" +#include "lmem.c" +#include "lundump.c" +#include "ldump.c" +#include "lstate.c" +#include "lgc.c" +#include "llex.c" +#include "lcode.c" +#include "lparser.c" +#include "ldebug.c" +#include "lfunc.c" +#include "lobject.c" +#include "ltm.c" +#include "lstring.c" +#include "ltable.c" +#include "ldo.c" +#include "lvm.c" +#include "lapi.c" + +/* + * Auxiliary and deliberately selected standard libraries. Five auxiliary + * functions are hosted-libc only (FILE*, errno, strerror, realloc/free): + * renamed out of the luaL_* namespace so the exported-API glob does not + * make them link roots (nothing in a kore image satisfies fopen, stdin, + * strerror, or realloc; before the export they were unreferenced and + * --gc-sections dropped them). A native module therefore cannot reach + * them, which is the honest ABI: states come from the host's lua_newstate + * with the Rust allocator, never luaL_newstate. + */ +#define luaL_fileresult kore_lua_aux_fileresult_unavailable +#define luaL_execresult kore_lua_aux_execresult_unavailable +#define luaL_loadfilex kore_lua_aux_loadfilex_unavailable +#define luaL_newstate kore_lua_aux_newstate_unavailable +#define luaL_alloc kore_lua_aux_alloc_unavailable +#include "lauxlib.c" +#undef luaL_loadfilex + +/* The host owns source loading; base cannot reach ambient FILE* or bytecode. */ +static int kore_lua_loadfilex_disabled(lua_State *L, const char *filename, + const char *mode) { + (void)filename; (void)mode; + return luaL_error(L, "host filesystem loading is unavailable"); +} +#define luaL_loadfilex kore_lua_loadfilex_disabled +#include "lbaselib.c" +#undef luaL_loadfilex +#include "lcorolib.c" +#include "lmathlib.c" +#include "lstrlib.c" +#include "ltablib.c" +#include "lutf8lib.c" + +/* + * This is deliberately not a partial printf implementation. Lua core needs + * exactly three conversions outside string.format: its configured integer + * and number formats, plus the implementation-defined pointer spelling used + * in diagnostics. string.format itself is removed from the exported library. + * + * The return value is the number of bytes actually stored. In particular, it + * can never make a caller treat a truncated would-write length as initialized. + */ +static size_t kore_store(char *dst, size_t cap, const char *src, size_t len) { + if (dst == NULL || cap == 0) return 0; + size_t stored = len < cap - 1 ? len : cap - 1; + if (stored != 0) memcpy(dst, src, stored); + dst[stored] = '\0'; + return stored; +} + +static size_t kore_unsigned(char *dst, unsigned long long value, + unsigned base) { + static const char digits[] = "0123456789abcdef"; + char reverse[65]; + size_t n = 0; + do { + reverse[n++] = digits[value % base]; + value /= base; + } while (value != 0); + for (size_t i = 0; i < n; i++) + dst[i] = reverse[n - i - 1]; + return n; +} + +static int kore_lua_snprintf(char *dst, size_t cap, const char *format, ...) { + char item[LUA_N2SBUFFSZ]; + size_t item_len = 0; + va_list args; + va_start(args, format); + if (format != NULL && strcmp(format, LUA_INTEGER_FMT) == 0) { + lua_Integer value = va_arg(args, LUAI_UACINT); + unsigned long long magnitude = value < 0 + ? 0ULL - (unsigned long long)value : (unsigned long long)value; + if (value < 0) item[item_len++] = '-'; + item_len += kore_unsigned(item + item_len, magnitude, 10); + } + else if (format != NULL && strcmp(format, LUA_NUMBER_FMT) == 0) { + double value = va_arg(args, LUAI_UACNUMBER); + item_len = kore_lua_format_number(item, sizeof(item), value, 15); + } + else if (format != NULL && strcmp(format, LUA_NUMBER_FMT_N) == 0) { + double value = va_arg(args, LUAI_UACNUMBER); + item_len = kore_lua_format_number(item, sizeof(item), value, 17); + } + else if (format != NULL && strcmp(format, "%p") == 0) { + uintptr_t value = (uintptr_t)va_arg(args, const void *); + item[item_len++] = '0'; item[item_len++] = 'x'; + item_len += kore_unsigned(item + item_len, value, 16); + } + else { + /* An added call site must extend this audited core contract explicitly. */ + item[item_len++] = '?'; + } + va_end(args); + return (int)kore_store(dst, cap, item, item_len); +} + +/* + * `require`, over the executor (src/host.rs; res/DAGFS.md §4). The VM runs + * on a worker thread, so a synchronous C `require` cannot drive the async + * filesystem: it hands the name to Rust, which posts a request to the + * task's executor thread and parks this thread on its doorbell until the + * answer is stored. The answer is a text chunk the host owns until + * kore_lua_host_release (source module, `.lua` in the task's tree), + * a C opener kore-late linked into this task (native module, + * `libs/.so`), or a NUL-terminated error. Loaded modules are cached + * in the registry's LUA_LOADED_TABLE exactly as stock `package` does, so + * `require "string"` answers the standard library the opener registered. + */ +enum { + KORE_LUA_MODULE_ERROR = 0, + KORE_LUA_MODULE_SOURCE = 1, + KORE_LUA_MODULE_NATIVE = 2 +}; +extern int kore_lua_host_require(const char *name, size_t len, + const char **bytes, size_t *bytes_len, + lua_CFunction *opener); +extern void kore_lua_host_release(void); + +/* + * Weak fallbacks: the native smoke builds (build.rs) link this engine with + * no Rust host, and there `require` answers "no host". On the kore target + * src/host.rs defines both strongly and the linker takes those. + */ +__attribute__((weak)) int kore_lua_host_require(const char *name, size_t len, + const char **bytes, + size_t *bytes_len, + lua_CFunction *opener) { + static const char message[] = "no module host"; + (void)name; (void)len; + *bytes = message; + *bytes_len = sizeof(message) - 1; + *opener = NULL; + return KORE_LUA_MODULE_ERROR; +} +__attribute__((weak)) void kore_lua_host_release(void) {} + +/* The rest of the host-call op vocabulary; src/bridge.rs holds the other half of + every one of these numbers; an embedder's own start at 64. */ +enum { + KORE_LUA_OP_SLEEP = 16, + KORE_LUA_OP_PROC_ARGS = 17, + /* `kore.fs`, and so `io` and the filesystem half of `os`. */ + KORE_LUA_OP_FS_OPEN = 18, + KORE_LUA_OP_FS_CLOSE = 19, + KORE_LUA_OP_FS_READ = 20, + KORE_LUA_OP_FS_WRITE = 21, + /* 22 was a host-side seek. The stream position lives in the file object + below, so there is nothing for the host to seek; the number is left + unused rather than recycled, because src/bridge.rs must agree. */ KORE_LUA_OP_FS_SYNC = 23, + KORE_LUA_OP_FS_RESIZE = 24, + KORE_LUA_OP_FS_STAT = 25, + KORE_LUA_OP_FS_REMOVE = 26, + KORE_LUA_OP_FS_RENAME = 27, + KORE_LUA_OP_FS_LIST = 28, + /* `os.exit(code)` — records the code with the host, which reads it once + the evaluation has unwound. */ + KORE_LUA_OP_PROC_EXIT = 29 +}; +/* The error `os.exit` raises to unwind the evaluation. The host matches it + exactly and reports the recorded code instead of an error; anything else + is a genuine error and is reported as one. */ +#define KORE_LUA_EXIT_SENTINEL "kore:exit" +/* The open-mode bits src/files.rs reads out of arg0. */ +enum { + KORE_LUA_O_READ = 1 << 0, + KORE_LUA_O_WRITE = 1 << 1, + KORE_LUA_O_CREATE = 1 << 2, + KORE_LUA_O_TRUNCATE = 1 << 3, + KORE_LUA_O_APPEND = 1 << 4, + KORE_LUA_O_EXCLUSIVE = 1 << 5, + KORE_LUA_O_DIRECTORY = 1 << 6 +}; +/* "wherever this handle's cursor is" — src/files.rs `FROM_CURSOR`. Not a + reachable file offset, so one word tells a stream read from a positional + one and `f:read` and `kore.fs.read_at` share an arm. */ +#define KORE_LUA_FROM_CURSOR 0xffffffffffffffffull +enum { + KORE_LUA_STATUS_OK = 0, + KORE_LUA_STATUS_REFUSED = 1, + KORE_LUA_STATUS_UNAVAILABLE = 2 +}; +/* + * Every value-returning host call: three argument words in, a status, two + * result words and a text out. One entry point for `kore.time`, an + * embedder's libraries, and every later standard-library verb, because they are all the same + * round trip over src/bridge.rs's one transport (`host_call` there). + */ +extern int kore_lua_host_call(unsigned op, unsigned long long arg0, + unsigned long long arg1, unsigned long long arg2, + const void *input, size_t input_len, + unsigned long long *result, + unsigned long long *result2, const char **text, + size_t *text_len); +__attribute__((weak)) int kore_lua_host_call(unsigned op, + unsigned long long arg0, + unsigned long long arg1, + unsigned long long arg2, + const void *input, size_t input_len, + unsigned long long *result, + unsigned long long *result2, + const char **text, size_t *text_len) { + static const char message[] = "no kore host"; + (void)op; (void)arg0; (void)arg1; (void)arg2; (void)input; (void)input_len; + *result = 0; *result2 = 0; + *text = message; *text_len = sizeof(message) - 1; + return KORE_LUA_STATUS_UNAVAILABLE; +} + +/* + * Push the `nil, why` failure pair for a non-OK status; returns 2. Every + * kore library answers failure this way, so a script that learned one + * module's error handling has learned them all. + */ +static int kore_lua_fail(lua_State *L, int status, const char *text, size_t text_len) { + lua_pushnil(L); + if (status == KORE_LUA_STATUS_REFUSED) { + lua_pushfstring(L, "refused: %s", text != NULL ? text : "?"); + if (text_len == 0) { /* keep the format's copy */ } + } else { + lua_pushlstring(L, text != NULL ? text : "unavailable", text_len); + } + return 2; +} + +/* + * The same failure, in `kore.*`'s spelling: `nil` and a *table* carrying the + * bare code (`"not-found"`), the reason it came back under, and a + * `__tostring` so `print(err)` and `assert(f())` still read well. + * + * One failure, two spellings. The stock `io` surface renders this same + * status as `nil, message, errno` a few hundred lines below; a script that + * wants to branch on *which* failure gets a word it can compare, instead of + * matching a sentence. + */ +static int kore_lua_error_tostring(lua_State *L) { + lua_getfield(L, 1, "code"); + return 1; +} + +static int kore_lua_fail_table(lua_State *L, int status, const char *text, + size_t text_len) { + lua_pushnil(L); + lua_createtable(L, 0, 2); + lua_pushlstring(L, text != NULL ? text : "unavailable", text_len); + lua_setfield(L, -2, "code"); + lua_pushstring(L, status == KORE_LUA_STATUS_REFUSED ? "refused" : "unavailable"); + lua_setfield(L, -2, "kind"); + lua_createtable(L, 0, 1); + lua_pushcfunction(L, kore_lua_error_tostring); + lua_setfield(L, -2, "__tostring"); + lua_setmetatable(L, -2); + return 2; +} + +/* + * [`kore_lua_fail`] for a NUL-terminated reason. The bridge answers a text + * with an explicit length (it carries arbitrary bytes); the synchronous + * leaf calls answer a C string, and passing those a length of zero would + * push an empty reason beside the nil. + */ +static int kore_lua_fail_cstr(lua_State *L, int status, const char *text) { + return kore_lua_fail(L, status, text, text != NULL ? strlen(text) : 0); +} + +/* + * The shared shape for every `nil, why` library below: on a non-OK status + * push the failure pair and return 2, else return 0 with the result words + * and the answer text filled. Callers add their own values after a 0. + */ +static int kore_lua_host_op(lua_State *L, unsigned op, unsigned long long arg0, + unsigned long long arg1, unsigned long long arg2, + unsigned long long *result, + unsigned long long *result2, const char **text, + size_t *text_len) { + const char *why = NULL; + size_t why_len = 0; + int status = kore_lua_host_call(op, arg0, arg1, arg2, NULL, 0, result, result2, + &why, &why_len); + if (text != NULL) *text = why; + if (text_len != NULL) *text_len = why_len; + if (status != KORE_LUA_STATUS_OK) + return kore_lua_fail(L, status, why, why_len); + return 0; +} + +/* + * The byte-carrying form, and the one every filesystem verb uses. `input` + * is handed straight to the bridge as a borrow of the Lua string the caller + * passed: the worker parks for the whole call, so the value stays rooted on + * its stack and nothing is copied on the way down. + * + * `render` chooses the failure spelling — [`kore_lua_fail_table`] for + * `kore.fs`, the `io` renderer for the stock surface — so one call site + * serves both surfaces over one op. + */ +typedef int (*kore_lua_render_fail)(lua_State *, int, const char *, size_t); + +static int kore_lua_host_bytes(lua_State *L, unsigned op, + unsigned long long arg0, unsigned long long arg1, + unsigned long long arg2, const void *input, + size_t input_len, unsigned long long *result, + unsigned long long *result2, const char **text, + size_t *text_len, kore_lua_render_fail render) { + const char *why = NULL; + size_t why_len = 0; + int status = kore_lua_host_call(op, arg0, arg1, arg2, input, input_len, result, + result2, &why, &why_len); + if (text != NULL) *text = why; + if (text_len != NULL) *text_len = why_len; + if (status != KORE_LUA_STATUS_OK) return render(L, status, why, why_len); + return 0; +} + +/* + * ------------------------------------------------------------ kore.time + * + * Seconds are floats; anything else carries its unit in the name. A clock + * that cannot be read answers `nil, why` — never a zero timestamp, which a + * caller would read as "the monotonic epoch" rather than "no reading". + */ +extern int kore_lua_host_uptime_ns(unsigned long long *out, const char **text); +extern int kore_lua_host_wall_ns(unsigned long long *out, const char **text); + +__attribute__((weak)) int kore_lua_host_uptime_ns(unsigned long long *out, + const char **text) { + static const char message[] = "no kore host"; + *out = 0; *text = message; + return KORE_LUA_STATUS_UNAVAILABLE; +} +__attribute__((weak)) int kore_lua_host_wall_ns(unsigned long long *out, + const char **text) { + static const char message[] = "no kore host"; + *out = 0; *text = message; + return KORE_LUA_STATUS_UNAVAILABLE; +} + +static int kore_lua_clock(lua_State *L, int wall, int as_seconds) { + unsigned long long ns = 0; + const char *why = NULL; + int status = wall ? kore_lua_host_wall_ns(&ns, &why) + : kore_lua_host_uptime_ns(&ns, &why); + if (status != KORE_LUA_STATUS_OK) return kore_lua_fail_cstr(L, status, why); + /* + * Seconds are a float, which is what a script wants to subtract and + * compare. A double holds 53 bits of mantissa, so a wall reading (~1.8e18 + * ns since the epoch) keeps roughly 256 ns of resolution and a monotonic + * one keeps far more; `mono_ns` / `wall_ns` answer the exact integer for + * callers that need every nanosecond. + */ + if (as_seconds) + lua_pushnumber(L, (lua_Number)ns / 1e9); + else + lua_pushinteger(L, (lua_Integer)ns); + return 1; +} + +static int kore_lua_time_now(lua_State *L) { return kore_lua_clock(L, 0, 1); } +static int kore_lua_time_mono_ns(lua_State *L) { return kore_lua_clock(L, 0, 0); } +static int kore_lua_time_wall(lua_State *L) { return kore_lua_clock(L, 1, 1); } +static int kore_lua_time_wall_ns(lua_State *L) { return kore_lua_clock(L, 1, 0); } + +static int kore_lua_time_sleep(lua_State *L) { + lua_Number seconds = luaL_checknumber(L, 1); + luaL_argcheck(L, seconds >= 0, 1, "delay must not be negative"); + /* Saturate rather than wrap: an absurd delay is a long sleep, not a short + one. The bound is the u64 nanosecond range the executor's timer takes. */ + lua_Number ns = seconds * 1e9; + unsigned long long delay = ns >= 1.8446744e19 ? 0xffffffffffffffffull + : (unsigned long long)ns; + unsigned long long r = 0, r2 = 0; + int n = kore_lua_host_op(L, KORE_LUA_OP_SLEEP, delay, 0, 0, &r, &r2, NULL, NULL); + if (n) return n; + lua_pushboolean(L, 1); + return 1; +} + +static const luaL_Reg kore_lua_time_lib[] = { + {"now", kore_lua_time_now}, + {"mono_ns", kore_lua_time_mono_ns}, + {"wall", kore_lua_time_wall}, + {"wall_ns", kore_lua_time_wall_ns}, + {"sleep", kore_lua_time_sleep}, + {NULL, NULL} +}; + +/* + * ---------------------------------------------------------- kore.random + * + * A source, not a stream: every byte is a fresh CPU draw. A script wanting + * many cheap values should seed `math.random` (which this build now seeds + * from the same source at startup) rather than loop here. + */ +extern int kore_lua_host_random(unsigned char *dst, size_t len, const char **text); +extern int kore_lua_host_random_available(void); + +__attribute__((weak)) int kore_lua_host_random(unsigned char *dst, size_t len, + const char **text) { + static const char message[] = "no kore host"; + if (dst != NULL && len != 0) memset(dst, 0, len); + *text = message; + return KORE_LUA_STATUS_UNAVAILABLE; +} +__attribute__((weak)) int kore_lua_host_random_available(void) { return 0; } + +/* Bounded by one Lua buffer's worth: a draw is a syscall-free CPU loop, and + an unbounded request would be an unbounded one. */ +#define KORE_LUA_RANDOM_MAX 4096 + +static int kore_lua_random_bytes(lua_State *L) { + lua_Integer count = luaL_checkinteger(L, 1); + luaL_argcheck(L, count >= 0 && count <= KORE_LUA_RANDOM_MAX, 1, + "byte count is 0..4096"); + luaL_Buffer buffer; + char *dst = luaL_buffinitsize(L, &buffer, (size_t)count); + const char *why = NULL; + int status = kore_lua_host_random((unsigned char *)dst, (size_t)count, &why); + if (status != KORE_LUA_STATUS_OK) { + luaL_pushresultsize(&buffer, 0); + lua_pop(L, 1); + return kore_lua_fail_cstr(L, status, why); + } + luaL_pushresultsize(&buffer, (size_t)count); + return 1; +} + +static int kore_lua_random_integer(lua_State *L) { + lua_Integer low = luaL_checkinteger(L, 1); + lua_Integer high = luaL_checkinteger(L, 2); + luaL_argcheck(L, low <= high, 2, "empty interval"); + lua_Unsigned span = (lua_Unsigned)high - (lua_Unsigned)low; + /* + * The engine's own projection (lmathlib.c `project`): mask the draw down + * to the smallest Mersenne number not smaller than the span, and redraw + * while it lands outside. Masking rather than a modulo fold keeps every + * value equally likely, needs no division — so a full-width interval is + * not a special case that could divide by zero — and makes + * `kore.random.integer` and `math.random` agree on what uniform means. + * Acceptance is at least one draw in two, and a dead entropy source ends + * the loop with its refusal rather than spinning. + */ + lua_Unsigned mask = span; + for (int shift = 1; (mask & (mask + 1)) != 0; shift *= 2) + mask |= (mask >> shift); + for (;;) { + unsigned long long draw = 0; + const char *why = NULL; + int status = kore_lua_host_random((unsigned char *)&draw, sizeof draw, &why); + if (status != KORE_LUA_STATUS_OK) return kore_lua_fail_cstr(L, status, why); + lua_Unsigned candidate = (lua_Unsigned)draw & mask; + if (candidate <= span) { + lua_pushinteger(L, (lua_Integer)(candidate + (lua_Unsigned)low)); + return 1; + } + } +} + +static int kore_lua_random_available(lua_State *L) { + lua_pushboolean(L, kore_lua_host_random_available()); + return 1; +} + +static const luaL_Reg kore_lua_random_lib[] = { + {"bytes", kore_lua_random_bytes}, + {"integer", kore_lua_random_integer}, + {"available", kore_lua_random_available}, + {NULL, NULL} +}; + +/* + * ------------------------------------------------------------ kore.proc + * + * The launch arguments. `args` is a lazy field rather than a call, because + * that is how a Lua program expects to read them — the metatable below + * fetches it once, on first touch, and caches it. + */ +static int kore_lua_proc_args(lua_State *L) { + unsigned long long r = 0, r2 = 0; + const char *text = NULL; + size_t text_len = 0; + int n = kore_lua_host_op(L, KORE_LUA_OP_PROC_ARGS, 0, 0, 0, &r, &r2, &text, + &text_len); + if (n) return n; + lua_pushlstring(L, text != NULL ? text : "", text_len); + return 1; +} + +/* `kore.proc.args` on first touch; thereafter the cached table. */ +static int kore_lua_proc_index(lua_State *L) { + /* A missing key answers nil for any key type, as an ordinary table does. + Raising here would make `kore.proc[1]` an error where `{}[1]` is nil. */ + const char *key = lua_type(L, 2) == LUA_TSTRING ? lua_tostring(L, 2) : NULL; + if (key == NULL || strcmp(key, "args") != 0) { lua_pushnil(L); return 1; } + if (kore_lua_proc_args(L) != 1) { lua_pushnil(L); return 1; } + /* Split the launch bytes on ASCII whitespace into a 1-based array, the + shape a Lua program indexes and iterates with ipairs. */ + size_t len = 0; + const char *bytes = lua_tolstring(L, -1, &len); + lua_newtable(L); + lua_Integer index = 0; + size_t start = 0; + for (size_t i = 0; i <= len; i++) { + int boundary = i == len || bytes[i] == ' ' || bytes[i] == '\t' || + bytes[i] == '\n' || bytes[i] == '\r'; + if (!boundary) continue; + if (i > start) { + lua_pushlstring(L, bytes + start, i - start); + lua_rawseti(L, -2, ++index); + } + start = i + 1; + } + /* Cache on the module table itself, so the next read is a plain rawget + and the bridge is crossed exactly once per process. */ + lua_pushvalue(L, -1); + lua_setfield(L, 1, "args"); + return 1; +} + +/* + * `exit` is deliberately absent until the host owns it. Termination has to + * unwind the evaluation, run `` handlers and let the REPL flush + * before the task leaves; a verb that raised instead would be a member that + * does not do what its name says, which is the one thing this build does + * not ship (see KORE-VENDOR.md on `string.format`). + */ +static const luaL_Reg kore_lua_proc_lib[] = { + {NULL, NULL} +}; + +/* Narrow wrappers keep Rust independent of Lua's preprocessor-only API. */ +typedef int (*kore_lua_append)(void *ud, const char *bytes, size_t len, + int stderr_stream); + +typedef struct kore_lua_writer { + kore_lua_append append; + void *ud; +} kore_lua_writer; + +/* + * -------------------------------------------------------------- kore.fs + * + * The file object, and the one place a stream position lives. + * + * `io.open` and `kore.fs.open` answer the *same* userdata: two spellings + * over one implementation is the whole point of having both surfaces, and + * a script that got a file from one must be able to hand it to the other. + * src/files.rs holds the view and answers positionally; the position, the + * read-ahead buffer and the mode flags are here, because they are stream + * ideas and the store has no streams. + * + * Rule 4 — one handle protocol: `:close()` is idempotent, `:closed()` + * answers without raising, `__close` and `__gc` both retire the handle, + * and `__tostring` prints an opaque identity (the slot and generation the + * bridge assigned), never an address. + */ +#define KORE_LUA_FILE "kore.file" +/* One page. A line read must not be a bridge crossing per byte: the park + and the kick dominate everything else, so reads go a page at a time and + `f:read("l")` is a memchr over bytes already here. */ +#define KORE_LUA_BUFSIZE 4096 + +enum { + KORE_LUA_FILE_STORE = 0, /* a file in the store */ + KORE_LUA_FILE_OUT = 1, /* io.stdout */ + KORE_LUA_FILE_ERR = 2, /* io.stderr */ + KORE_LUA_FILE_IN = 3 /* io.stdin */ +}; + +typedef struct { + unsigned long long handle; + /* The stream position: where the next unbuffered read or write lands. */ + unsigned long long position; + int kind; + int closed; + int readable; + int writable; + /* Read-ahead. `buf[0]` is file offset `base`; `taken` bytes of the + `have` valid ones have been handed to the script, so the script's + logical position is `base + taken` whenever the buffer is live. */ + unsigned long long base; + size_t have; + size_t taken; + int eof; + /* `io.tmpfile`: the file unlinks itself when it closes, so `path` is the + name to remove. Nothing else records a path — an ordinary file is + reached only through its handle, and remembering where it came from + would be a second, staler authority over the same object. */ + int temporary; + char path[48]; + char buf[KORE_LUA_BUFSIZE]; +} kore_lua_file; + +/* The registry key the host writer is stashed under, so `io.write` reaches + the same sink `print` does rather than opening a second one. */ +static const char kore_lua_writer_key = 0; + +static kore_lua_writer *kore_lua_sink(lua_State *L) { + kore_lua_writer *writer; + lua_rawgetp(L, LUA_REGISTRYINDEX, &kore_lua_writer_key); + writer = (kore_lua_writer *)lua_touserdata(L, -1); + lua_pop(L, 1); + return writer; +} + +/* + * The stock `io` surface's failure spelling: `nil, message, errno`. + * + * The errno is derived from the same kebab-case code `kore.fs` hands back + * as a table, so the two surfaces cannot disagree about what happened — + * they only disagree about how to say it. A code with no POSIX analogue + * answers `EIO`, which is the honest "something below failed". + */ +static int kore_lua_errno(const char *code, size_t len) { + struct { const char *code; int number; } static const table[] = { + {"not-found", 2}, /* ENOENT */ + {"already-exists", 17}, /* EEXIST */ + {"not-directory", 20}, /* ENOTDIR */ + {"is-directory", 21}, /* EISDIR */ + {"invalid-name", 22}, /* EINVAL */ + {"invalid-handle", 9}, /* EBADF */ + {"access-denied", 13}, /* EACCES */ + {"access-lost", 9}, /* EBADF */ + {"not-empty", 39}, /* ENOTEMPTY */ + {"out-of-storage", 28}, /* ENOSPC */ + {"out-of-handles", 24}, /* EMFILE */ + {"range", 34}, /* ERANGE */ + {"unsupported", 38}, /* ENOSYS */ + {"busy", 16}, /* EBUSY */ + {"cycle", 40}, /* ELOOP */ + {NULL, 0} + }; + for (int i = 0; table[i].code != NULL; i++) + if (strlen(table[i].code) == len && memcmp(table[i].code, code, len) == 0) + return table[i].number; + return 5; /* EIO */ +} + +static int kore_lua_fail_io(lua_State *L, int status, const char *text, + size_t text_len) { + (void)status; + lua_pushnil(L); + lua_pushlstring(L, text != NULL ? text : "unavailable", text_len); + lua_pushinteger(L, kore_lua_errno(text != NULL ? text : "", text_len)); + return 3; +} + +static kore_lua_file *kore_lua_file_check(lua_State *L, int index) { + return (kore_lua_file *)luaL_checkudata(L, index, KORE_LUA_FILE); +} + +/* + * A closed file is a *programming* error, not an expected failure, so it + * raises — exactly as stock Lua does, and exactly as rule 1 divides the + * two cases. + */ +static kore_lua_file *kore_lua_file_live(lua_State *L, int index) { + kore_lua_file *file = kore_lua_file_check(L, index); + if (file->closed) luaL_error(L, "attempt to use a closed file"); + return file; +} + +static kore_lua_file *kore_lua_file_push(lua_State *L, unsigned long long handle, + int kind, int readable, int writable, + unsigned long long position) { + kore_lua_file *file = + (kore_lua_file *)lua_newuserdatauv(L, sizeof(kore_lua_file), 0); + file->handle = handle; + file->position = position; + file->kind = kind; + file->closed = 0; + file->readable = readable; + file->writable = writable; + file->base = position; + file->have = 0; + file->taken = 0; + file->eof = 0; + file->temporary = 0; + file->path[0] = '\0'; + luaL_setmetatable(L, KORE_LUA_FILE); + return file; +} + +/* Drop the read-ahead. Called before anything that moves or invalidates + the position: a seek, a write, a close. */ +static void kore_lua_file_drop(kore_lua_file *file) { + file->have = 0; + file->taken = 0; + file->eof = 0; +} + +/* + * Parse an `io.open` mode string into the bits src/files.rs reads. + * Returns 0 when the spelling is not one Lua defines. + */ +static unsigned long long kore_lua_mode(const char *mode) { + unsigned long long flags = 0; + size_t len = strlen(mode); + size_t i = 0; + if (len == 0) return 0; + switch (mode[0]) { + case 'r': flags = KORE_LUA_O_READ; break; + case 'w': flags = KORE_LUA_O_WRITE | KORE_LUA_O_CREATE | KORE_LUA_O_TRUNCATE; break; + case 'a': flags = KORE_LUA_O_WRITE | KORE_LUA_O_CREATE | KORE_LUA_O_APPEND; break; + default: return 0; + } + for (i = 1; i < len; i++) { + /* `b` is accepted and ignored: kore has no text mode to differ from, + so a binary flag would be a distinction without one. */ + if (mode[i] == 'b') continue; + if (mode[i] == '+') { + flags |= KORE_LUA_O_READ | KORE_LUA_O_WRITE; + if (mode[0] == 'r') continue; /* r+ does not create */ + flags |= KORE_LUA_O_CREATE; + continue; + } + return 0; + } + return flags; +} + +/* + * Open, by value. Takes the path and mode as C values rather than reading + * fixed stack slots, because two of its three callers do not have them at + * index 1 and 2 — `io.lines(name)` has the name at 1 and its read formats + * after it, and `io.input(name)` has only the name. Answers 1 with the + * file pushed, or `render`'s failure count. + */ +static int kore_lua_open_named(lua_State *L, const char *path, size_t path_len, + const char *mode, int mode_index, + kore_lua_render_fail render) { + unsigned long long flags = kore_lua_mode(mode); + unsigned long long handle = 0, length = 0; + int n; + if (flags == 0) return luaL_argerror(L, mode_index, "invalid file mode"); + n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_OPEN, flags, 0, 0, path, path_len, + &handle, &length, NULL, NULL, render); + if (n) return n; + /* An append stream starts at the end, so `f:seek()` before the first + write answers where those bytes will actually land. */ + kore_lua_file_push(L, handle, KORE_LUA_FILE_STORE, + (flags & KORE_LUA_O_READ) != 0, + (flags & KORE_LUA_O_WRITE) != 0, + (flags & KORE_LUA_O_APPEND) ? length : 0); + return 1; +} + +/* `kore.fs.open` and `io.open`'s spelling: the path and mode are the call's + own first two arguments; `render` picks the surface's failure shape. */ +static int kore_lua_open_file(lua_State *L, kore_lua_render_fail render) { + size_t path_len = 0; + const char *path = luaL_checklstring(L, 1, &path_len); + const char *mode = luaL_optstring(L, 2, "r"); + return kore_lua_open_named(L, path, path_len, mode, 2, render); +} + +/* + * `io.input`, `io.output` and `io.lines` "raise the error, instead of + * returning an error code" (§6.8). Upstream's wording is + * `cannot open file 'NAME' (REASON)`, and a script that matches on it — or + * a person reading a traceback — should see the same sentence here. + * + * Leaves the new file on the stack; never returns on failure. + */ +static void kore_lua_opencheck(lua_State *L, int name_index, + const char *mode) { + size_t len = 0; + const char *name = luaL_checklstring(L, name_index, &len); + if (kore_lua_open_named(L, name, len, mode, name_index, kore_lua_fail_io) != 1) { + /* The failure triple is on the stack: nil, message, errno. */ + const char *why = lua_tostring(L, -2); + luaL_error(L, "cannot open file '%s' (%s)", name, + why != NULL ? why : "unavailable"); + } +} + +/* + * Close. Idempotent by contract (rule 4): closing twice is not an error, + * and neither is closing a standard stream — it simply does not retire a + * store handle, because there is none. + */ +/* + * An `io.tmpfile` removes itself once its handle is retired. Best effort + * and deliberately silent: the file is already closed, the caller asked to + * be rid of it, and a failure here has no answer a script could act on. + */ +static void kore_lua_file_unlink(kore_lua_file *file) { + unsigned long long r = 0, r2 = 0; + const char *why = NULL; + size_t why_len = 0; + if (!file->temporary || file->path[0] == '\0') return; + (void)kore_lua_host_call(KORE_LUA_OP_FS_REMOVE, 0, 0, 0, file->path, + strlen(file->path), &r, &r2, &why, &why_len); + file->temporary = 0; + file->path[0] = '\0'; +} + +static int kore_lua_file_do_close(lua_State *L, kore_lua_file *file, + kore_lua_render_fail render) { + unsigned long long r = 0, r2 = 0; + int n; + /* "The I/O library never closes these files" (§6.8). Upstream answers + the refusal rather than raising, and so does this: a script that closes + whatever `io.output()` happens to be must not die because today it is + the terminal. */ + if (file->kind != KORE_LUA_FILE_STORE) { + /* Exactly two values, as `io_noclose` answers: this refusal carries no + errno because nothing was attempted below. */ + (void)render; + lua_pushnil(L); + lua_pushliteral(L, "cannot close standard file"); + return 2; + } + if (file->closed) { + lua_pushboolean(L, 1); + return 1; + } + kore_lua_file_drop(file); + /* Marked closed before the call, not after: a refused close still + retires the handle here, and leaving it live would let a script + re-close a handle the host has already taken back. */ + file->closed = 1; + n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_CLOSE, file->handle, 0, 0, NULL, 0, + &r, &r2, NULL, NULL, render); + kore_lua_file_unlink(file); + if (n) return n; + lua_pushboolean(L, 1); + return 1; +} + +static int kore_lua_file_close(lua_State *L) { + return kore_lua_file_do_close(L, kore_lua_file_check(L, 1), kore_lua_fail_io); +} + +static int kore_lua_file_closed(lua_State *L) { + lua_pushboolean(L, kore_lua_file_check(L, 1)->closed); + return 1; +} + +/* `__gc` and `__close` both retire the handle and neither may raise: a + metamethod that threw here would unwind out of the collector. */ +static int kore_lua_file_release(lua_State *L) { + kore_lua_file *file = kore_lua_file_check(L, 1); + /* A standard stream is never retired, not even by the collector: the + host owns those, and the next chunk still writes to them. */ + if (file->kind != KORE_LUA_FILE_STORE) return 0; + if (!file->closed) { + unsigned long long r = 0, r2 = 0; + const char *why = NULL; + size_t why_len = 0; + file->closed = 1; + kore_lua_file_drop(file); + (void)kore_lua_host_call(KORE_LUA_OP_FS_CLOSE, file->handle, 0, 0, NULL, 0, + &r, &r2, &why, &why_len); + } + file->closed = 1; + kore_lua_file_unlink(file); + return 0; +} + +/* An opaque identity: the slot and generation the bridge assigned, never + an address (rule 4). */ +static int kore_lua_file_tostring(lua_State *L) { + kore_lua_file *file = kore_lua_file_check(L, 1); + static const char *const names[] = {"file", "stdout", "stderr", "stdin"}; + const char *name = names[file->kind & 3]; + if (file->closed) + lua_pushfstring(L, "closed %s", name); + else if (file->kind != KORE_LUA_FILE_STORE) + lua_pushfstring(L, "%s", name); + else + lua_pushfstring(L, "file #%d.%d", (int)(file->handle & 0xffffffffu), + (int)(file->handle >> 32)); + return 1; +} + +/* + * Refill the read-ahead from the stream position. Answers 1 on progress, + * 0 at end of file, and -1 on a failure it has already pushed onto the + * stack in `render`'s spelling. + */ +static int kore_lua_file_fill(lua_State *L, kore_lua_file *file, int *pushed, + kore_lua_render_fail render) { + unsigned long long count = 0, len = 0; + const char *bytes = NULL; + size_t bytes_len = 0; + int n; + *pushed = 0; + if (file->taken < file->have) return 1; /* still buffered */ + if (file->eof) return 0; + if (file->kind != KORE_LUA_FILE_STORE) { /* stdin: always at end */ + file->eof = 1; + return 0; + } + file->base = file->position; + file->have = 0; + file->taken = 0; + n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_READ, file->handle, file->position, + KORE_LUA_BUFSIZE, NULL, 0, &count, &len, &bytes, + &bytes_len, render); + if (n) { *pushed = n; return -1; } + if (bytes_len > KORE_LUA_BUFSIZE) bytes_len = KORE_LUA_BUFSIZE; + if (bytes_len != 0) memcpy(file->buf, bytes, bytes_len); + file->have = bytes_len; + if (bytes_len == 0) { file->eof = 1; return 0; } + return 1; +} + +/* Hand out `want` bytes (or everything buffered when `want` is 0 and + `all` is set), appending to `b`. Answers how many were appended. */ +static size_t kore_lua_file_take(kore_lua_file *file, luaL_Buffer *b, + size_t want) { + size_t ready = file->have - file->taken; + size_t take = want < ready ? want : ready; + luaL_addlstring(b, file->buf + file->taken, take); + file->taken += take; + file->position = file->base + file->taken; + return take; +} + +/* `f:read("l")` / `"L"`: up to and including the newline. */ +static int kore_lua_read_line(lua_State *L, kore_lua_file *file, int keep, + kore_lua_render_fail render) { + luaL_Buffer b; + int got = 0; + luaL_buffinit(L, &b); + for (;;) { + int pushed = 0; + int state = kore_lua_file_fill(L, file, &pushed, render); + if (state < 0) { luaL_pushresult(&b); lua_pop(L, 1); return pushed; } + if (state == 0) break; + { + const char *from = file->buf + file->taken; + size_t ready = file->have - file->taken; + const char *nl = (const char *)memchr(from, '\n', ready); + if (nl != NULL) { + size_t upto = (size_t)(nl - from); + luaL_addlstring(&b, from, keep ? upto + 1 : upto); + file->taken += upto + 1; + file->position = file->base + file->taken; + got = 1; + break; + } + luaL_addlstring(&b, from, ready); + file->taken += ready; + file->position = file->base + file->taken; + got = got || ready != 0; + } + } + if (!got) { luaL_pushresult(&b); lua_pop(L, 1); lua_pushnil(L); return 1; } + luaL_pushresult(&b); + return 1; +} + +static int kore_lua_read_all(lua_State *L, kore_lua_file *file, + kore_lua_render_fail render) { + luaL_Buffer b; + luaL_buffinit(L, &b); + for (;;) { + int pushed = 0; + int state = kore_lua_file_fill(L, file, &pushed, render); + if (state < 0) { luaL_pushresult(&b); lua_pop(L, 1); return pushed; } + if (state == 0) break; + kore_lua_file_take(file, &b, file->have - file->taken); + } + /* "a" never fails: at end of file it answers the empty string, which is + the one read format that does. */ + luaL_pushresult(&b); + return 1; +} + +static int kore_lua_read_count(lua_State *L, kore_lua_file *file, size_t want, + kore_lua_render_fail render) { + luaL_Buffer b; + size_t total = 0; + if (want == 0) { + /* Lua's zero-length read is a probe: the empty string when there is + anything left, nil at end of file. */ + int pushed = 0; + int state = kore_lua_file_fill(L, file, &pushed, render); + if (state < 0) return pushed; + if (state == 0) { lua_pushnil(L); return 1; } + lua_pushliteral(L, ""); + return 1; + } + luaL_buffinit(L, &b); + while (total < want) { + int pushed = 0; + int state = kore_lua_file_fill(L, file, &pushed, render); + if (state < 0) { luaL_pushresult(&b); lua_pop(L, 1); return pushed; } + if (state == 0) break; + total += kore_lua_file_take(file, &b, want - total); + } + if (total == 0) { luaL_pushresult(&b); lua_pop(L, 1); lua_pushnil(L); return 1; } + luaL_pushresult(&b); + return 1; +} + +/* `f:read("n")`: the longest prefix that is a numeral, via the engine's own + reader so Lua and kore agree on what a number looks like. */ +static int kore_lua_read_number(lua_State *L, kore_lua_file *file, + kore_lua_render_fail render) { + char text[64]; + size_t len = 0; + int seen_digit = 0; + for (;;) { + int pushed = 0; + int state = kore_lua_file_fill(L, file, &pushed, render); + if (state < 0) return pushed; + if (state == 0) break; + { + char c = file->buf[file->taken]; + if (len == 0 && (c == ' ' || c == '\t' || c == '\n' || c == '\r')) { + file->taken++; file->position = file->base + file->taken; continue; + } + int digit = c >= '0' && c <= '9'; + if (!(digit || c == '+' || c == '-' || c == '.' || c == 'e' || + c == 'E' || c == 'x' || c == 'X' || (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'))) break; + if (len + 1 >= sizeof text) break; + if (digit) seen_digit = 1; + text[len++] = c; + file->taken++; + file->position = file->base + file->taken; + } + } + text[len] = '\0'; + if (len == 0 || !seen_digit) { lua_pushnil(L); return 1; } + if (lua_stringtonumber(L, text) == 0) { lua_pushnil(L); return 1; } + return 1; +} + +/* One read format; answers how many values it pushed, or a negative count + meaning "a failure of `-n` values is already on the stack". */ +static int kore_lua_read_one(lua_State *L, kore_lua_file *file, int index, + kore_lua_render_fail render) { + if (lua_type(L, index) == LUA_TNUMBER) { + lua_Integer want = luaL_checkinteger(L, index); + luaL_argcheck(L, want >= 0, index, "byte count must not be negative"); + return kore_lua_read_count(L, file, (size_t)want, render); + } + { + const char *format = luaL_checkstring(L, index); + if (*format == '*') format++; /* the 5.2 spelling, still accepted */ + switch (*format) { + case 'l': return kore_lua_read_line(L, file, 0, render); + case 'L': return kore_lua_read_line(L, file, 1, render); + case 'a': return kore_lua_read_all(L, file, render); + case 'n': return kore_lua_read_number(L, file, render); + default: + return luaL_argerror(L, index, "invalid format"); + } + } +} + +static int kore_lua_file_read_with(lua_State *L, kore_lua_file *file, int first, + int last, kore_lua_render_fail render) { + int top = last; + int pushed = 0; + if (!file->readable) { + return render(L, KORE_LUA_STATUS_REFUSED, "access-denied", + sizeof "access-denied" - 1); + } + if (first > top) return kore_lua_read_line(L, file, 0, render); + for (int index = first; index <= top; index++) { + int n = kore_lua_read_one(L, file, index, render); + pushed += n; + /* Stop at the first format that answered nil: the rest cannot succeed + when this one hit end of file, and Lua stops too. */ + if (lua_isnil(L, -1)) break; + } + return pushed; +} + +static int kore_lua_file_read(lua_State *L) { + return kore_lua_file_read_with(L, kore_lua_file_live(L, 1), 2, lua_gettop(L), + kore_lua_fail_io); +} + +/* + * Write. A standard stream goes to the host sink `print` uses; a store + * file crosses the bridge at the stream position (or, for an append file, + * wherever src/files.rs finds the end now). + */ +static int kore_lua_file_write_with(lua_State *L, kore_lua_file *file, int first, + int last, int self_index, + kore_lua_render_fail render) { + int top = last; + if (!file->writable) { + return render(L, KORE_LUA_STATUS_REFUSED, "access-denied", + sizeof "access-denied" - 1); + } + for (int index = first; index <= top; index++) { + size_t len = 0; + const char *bytes; + /* A number is written as a number, not via tostring: that is the one + coercion Lua's `file:write` defines, and everything else is an + argument error rather than a silent `tostring`. */ + if (lua_type(L, index) == LUA_TNUMBER) { + lua_pushvalue(L, index); + bytes = lua_tolstring(L, -1, &len); + } else { + bytes = luaL_checklstring(L, index, &len); + lua_pushvalue(L, index); + } + if (file->kind != KORE_LUA_FILE_STORE) { + kore_lua_writer *writer = kore_lua_sink(L); + if (writer != NULL && writer->append != NULL && + !writer->append(writer->ud, bytes, len, + file->kind == KORE_LUA_FILE_ERR)) + return luaL_error(L, "host output limit exceeded"); + lua_pop(L, 1); + continue; + } + { + unsigned long long written = 0, end = 0; + int n; + kore_lua_file_drop(file); + n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_WRITE, file->handle, + file->position, 0, bytes, len, &written, &end, + NULL, NULL, render); + if (n) return n; + file->position = end; + file->base = end; + lua_pop(L, 1); + if (written != (unsigned long long)len) { + /* A short write is not silently successful. */ + return render(L, KORE_LUA_STATUS_REFUSED, "out-of-storage", + sizeof "out-of-storage" - 1); + } + } + } + /* Lua answers the file itself, so writes chain. */ + lua_pushvalue(L, self_index); + return 1; +} + +static int kore_lua_file_write(lua_State *L) { + return kore_lua_file_write_with(L, kore_lua_file_live(L, 1), 2, lua_gettop(L), + 1, kore_lua_fail_io); +} + +static int kore_lua_file_seek(lua_State *L) { + kore_lua_file *file = kore_lua_file_live(L, 1); + static const char *const whences[] = {"set", "cur", "end", NULL}; + int whence = luaL_checkoption(L, 2, "cur", whences); + lua_Integer delta = (lua_Integer)luaL_optinteger(L, 3, 0); + long long base = 0; + if (file->kind != KORE_LUA_FILE_STORE) { + /* A stream has no position to move; `seek()` on one answers its + refusal rather than pretending a move happened. */ + return kore_lua_fail_io(L, KORE_LUA_STATUS_REFUSED, "unsupported", + sizeof "unsupported" - 1); + } + switch (whence) { + case 0: base = 0; break; + case 1: base = (long long)file->position; break; + default: { + unsigned long long length = 0, kind = 0; + int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_STAT, file->handle, 0, 0, + NULL, 0, &length, &kind, NULL, NULL, + kore_lua_fail_io); + if (n) return n; + base = (long long)length; + break; + } + } + if (base + (long long)delta < 0) + return kore_lua_fail_io(L, KORE_LUA_STATUS_REFUSED, "range", + sizeof "range" - 1); + kore_lua_file_drop(file); + file->position = (unsigned long long)(base + (long long)delta); + file->base = file->position; + lua_pushinteger(L, (lua_Integer)file->position); + return 1; +} + +static int kore_lua_file_flush(lua_State *L) { + kore_lua_file *file = kore_lua_file_live(L, 1); + unsigned long long r = 0, r2 = 0; + /* Writes are already acknowledged by the store before `f:write` returns, + so this is a durability barrier, not a buffer drain — and on a + standard stream there is nothing below to bar. */ + if (file->kind == KORE_LUA_FILE_STORE) { + int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_SYNC, file->handle, 0, 0, + NULL, 0, &r, &r2, NULL, NULL, kore_lua_fail_io); + if (n) return n; + } + /* `true`, not the file: `f_flush` answers `luaL_fileresult`, and only + `f:write` is special-cased to hand the file back for chaining. */ + lua_pushboolean(L, 1); + return 1; +} + +/* + * `setvbuf` is accepted and has no effect: this build's reads are already + * buffered a page at a time and its writes are acknowledged individually, + * so there is no mode for it to select. Answering `true` rather than + * raising is the conforming choice — the manual lets an implementation + * ignore the hint, and a script that sets a mode it does not depend on + * should not fail. + */ +static int kore_lua_file_setvbuf(lua_State *L) { + static const char *const modes[] = {"no", "full", "line", NULL}; + kore_lua_file_live(L, 1); + (void)luaL_checkoption(L, 2, NULL, modes); + lua_pushboolean(L, 1); + return 1; +} + +/* The `f:lines(...)` iterator: the file and its formats as upvalues, so + the closure holds no state of its own beyond what the file already is. */ +static int kore_lua_lines_step(lua_State *L) { + kore_lua_file *file = + (kore_lua_file *)lua_touserdata(L, lua_upvalueindex(1)); + int formats = (int)lua_tointeger(L, lua_upvalueindex(2)); + int close_at_end = (int)lua_tointeger(L, lua_upvalueindex(3)); + int pushed; + if (file == NULL || file->closed) + return luaL_error(L, "file is already closed"); + for (int i = 0; i < formats; i++) lua_pushvalue(L, lua_upvalueindex(4 + i)); + pushed = formats == 0 + ? kore_lua_read_line(L, file, 0, kore_lua_fail_io) + : kore_lua_file_read_with(L, file, lua_gettop(L) - formats + 1, + lua_gettop(L), kore_lua_fail_io); + if (close_at_end && lua_isnil(L, -1)) + (void)kore_lua_file_do_close(L, file, kore_lua_fail_io), lua_pop(L, 1); + return pushed; +} + +static int kore_lua_lines_push(lua_State *L, int file_index, int first, + int close_at_end) { + int top = lua_gettop(L); + int formats = top - first + 1; + if (formats < 0) formats = 0; + if (formats > 250) return luaL_error(L, "too many read formats"); + lua_pushvalue(L, file_index); + lua_pushinteger(L, formats); + lua_pushinteger(L, close_at_end); + for (int i = 0; i < formats; i++) lua_pushvalue(L, first + i); + lua_pushcclosure(L, kore_lua_lines_step, 3 + formats); + return 1; +} + +static int kore_lua_file_lines(lua_State *L) { + kore_lua_file_live(L, 1); + return kore_lua_lines_push(L, 1, 2, 0); +} + +/* The file object's methods. `kore.fs` and `io` share every one of them: + the object is the same, so its protocol is too. */ +static const luaL_Reg kore_lua_file_methods[] = { + {"read", kore_lua_file_read}, + {"write", kore_lua_file_write}, + {"lines", kore_lua_file_lines}, + {"seek", kore_lua_file_seek}, + {"flush", kore_lua_file_flush}, + {"setvbuf", kore_lua_file_setvbuf}, + {"close", kore_lua_file_close}, + {"closed", kore_lua_file_closed}, + {NULL, NULL} +}; + +static void kore_lua_file_metatable(lua_State *L) { + if (luaL_newmetatable(L, KORE_LUA_FILE)) { + luaL_newlib(L, kore_lua_file_methods); + lua_setfield(L, -2, "__index"); + lua_pushcfunction(L, kore_lua_file_release); + lua_setfield(L, -2, "__gc"); + /* Deterministic close is primary; `__gc` is the backstop (rule 4). */ + lua_pushcfunction(L, kore_lua_file_release); + lua_setfield(L, -2, "__close"); + lua_pushcfunction(L, kore_lua_file_tostring); + lua_setfield(L, -2, "__tostring"); + lua_pushstring(L, "FILE*"); + lua_setfield(L, -2, "__name"); + /* A metatable a script could rewrite would let it forge a file. */ + lua_pushboolean(L, 0); + lua_setfield(L, -2, "__metatable"); + } + lua_pop(L, 1); +} + +/* + * `kore.fs`: the same object, the kebab-code failure table, and the + * positional verbs the stream surface is built out of. + */ +static int kore_lua_fs_open(lua_State *L) { + return kore_lua_open_file(L, kore_lua_fail_table); +} + +/* One shape for every path verb that answers `true` or a failure. */ +static int kore_lua_fs_path_op(lua_State *L, unsigned op) { + size_t len = 0; + const char *path = luaL_checklstring(L, 1, &len); + unsigned long long r = 0, r2 = 0; + int n = kore_lua_host_bytes(L, op, 0, 0, 0, path, len, &r, &r2, NULL, NULL, + kore_lua_fail_table); + if (n) return n; + lua_pushboolean(L, 1); + return 1; +} + +static int kore_lua_fs_remove(lua_State *L) { + return kore_lua_fs_path_op(L, KORE_LUA_OP_FS_REMOVE); +} + +static int kore_lua_fs_mkdir(lua_State *L) { + size_t len = 0; + const char *path = luaL_checklstring(L, 1, &len); + unsigned long long r = 0, r2 = 0; + int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_OPEN, KORE_LUA_O_DIRECTORY, 0, 0, + path, len, &r, &r2, NULL, NULL, kore_lua_fail_table); + if (n) return n; + lua_pushboolean(L, 1); + return 1; +} + +/* Two paths in one window, NUL-separated: a name may not contain a NUL, so + the split cannot be ambiguous. */ +static int kore_lua_fs_rename(lua_State *L) { + size_t from_len = 0, to_len = 0; + const char *from = luaL_checklstring(L, 1, &from_len); + const char *to = luaL_checklstring(L, 2, &to_len); + unsigned long long r = 0, r2 = 0; + luaL_Buffer b; + char *joined; + int n; + luaL_buffinit(L, &b); + joined = luaL_prepbuffsize(&b, from_len + to_len + 1); + memcpy(joined, from, from_len); + joined[from_len] = '\0'; + memcpy(joined + from_len + 1, to, to_len); + n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_RENAME, 0, 0, 0, joined, + from_len + to_len + 1, &r, &r2, NULL, NULL, + kore_lua_fail_table); + if (n) return n; + lua_pushboolean(L, 1); + return 1; +} + +static int kore_lua_fs_stat(lua_State *L) { + size_t len = 0; + const char *path = luaL_checklstring(L, 1, &len); + unsigned long long length = 0, kind = 0; + int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_STAT, 0, 0, 0, path, len, + &length, &kind, NULL, NULL, kore_lua_fail_table); + if (n) return n; + lua_createtable(L, 0, 3); + /* Rule 5: a size carries its unit in the name. */ + lua_pushinteger(L, (lua_Integer)length); + lua_setfield(L, -2, "size_bytes"); + lua_pushstring(L, kind ? "directory" : "file"); + lua_setfield(L, -2, "kind"); + lua_pushboolean(L, kind != 0); + lua_setfield(L, -2, "directory"); + return 1; +} + +/* `exists` is a question, not a failure, so it answers a boolean and never + a `nil, err` pair a caller would have to unpack to learn "no". */ +static int kore_lua_fs_exists(lua_State *L) { + size_t len = 0; + const char *path = luaL_checklstring(L, 1, &len); + unsigned long long length = 0, kind = 0; + const char *why = NULL; + size_t why_len = 0; + int status = kore_lua_host_call(KORE_LUA_OP_FS_STAT, 0, 0, 0, path, len, + &length, &kind, &why, &why_len); + lua_pushboolean(L, status == KORE_LUA_STATUS_OK); + return 1; +} + +static int kore_lua_fs_list(lua_State *L) { + size_t len = 0; + const char *path = luaL_checklstring(L, 1, &len); + unsigned long long count = 0, bytes_len = 0; + const char *bytes = NULL; + size_t text_len = 0; + int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_LIST, 0, 0, 0, path, len, &count, + &bytes_len, &bytes, &text_len, kore_lua_fail_table); + if (n) return n; + lua_createtable(L, (int)count, 0); + { + lua_Integer index = 0; + size_t start = 0; + for (size_t i = 0; i < text_len; i++) { + if (bytes[i] != '\0') continue; + lua_pushlstring(L, bytes + start, i - start); + lua_rawseti(L, -2, ++index); + start = i + 1; + } + } + return 1; +} + +/* Read a whole file without a handle — the shape a script reaches for far + more often than open/read/close, and the one `io.lines(name)` is built + on. Bounded by the host's single-reply cap. */ +static int kore_lua_fs_read_file(lua_State *L) { + size_t path_len = 0; + const char *path = luaL_checklstring(L, 1, &path_len); + unsigned long long handle = 0, length = 0; + int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_OPEN, KORE_LUA_O_READ, 0, 0, + path, path_len, &handle, &length, NULL, NULL, + kore_lua_fail_table); + if (n) return n; + { + kore_lua_file *file = kore_lua_file_push(L, handle, KORE_LUA_FILE_STORE, 1, 0, 0); + int pushed = kore_lua_read_all(L, file, kore_lua_fail_table); + /* The handle is retired here whatever the read did: this verb never + hands the object out, so nothing else can close it. */ + lua_pushvalue(L, -1 - pushed); + (void)kore_lua_file_do_close(L, file, kore_lua_fail_table); + lua_pop(L, 2); + return pushed; + } +} + +static int kore_lua_fs_write_file(lua_State *L) { + size_t path_len = 0, bytes_len = 0; + const char *path = luaL_checklstring(L, 1, &path_len); + const char *bytes = luaL_checklstring(L, 2, &bytes_len); + unsigned long long handle = 0, length = 0, written = 0, end = 0; + int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_OPEN, + KORE_LUA_O_WRITE | KORE_LUA_O_CREATE | + KORE_LUA_O_TRUNCATE, + 0, 0, path, path_len, &handle, &length, NULL, + NULL, kore_lua_fail_table); + if (n) return n; + n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_WRITE, handle, 0, 0, bytes, + bytes_len, &written, &end, NULL, NULL, + kore_lua_fail_table); + { + unsigned long long r = 0, r2 = 0; + const char *why = NULL; + size_t why_len = 0; + (void)kore_lua_host_call(KORE_LUA_OP_FS_CLOSE, handle, 0, 0, NULL, 0, &r, + &r2, &why, &why_len); + } + if (n) return n; + lua_pushinteger(L, (lua_Integer)written); + return 1; +} + +static const luaL_Reg kore_lua_fs_lib[] = { + {"open", kore_lua_fs_open}, + {"read_file", kore_lua_fs_read_file}, + {"write_file", kore_lua_fs_write_file}, + {"remove", kore_lua_fs_remove}, + {"rename", kore_lua_fs_rename}, + {"mkdir", kore_lua_fs_mkdir}, + {"stat", kore_lua_fs_stat}, + {"exists", kore_lua_fs_exists}, + {"list", kore_lua_fs_list}, + {NULL, NULL} +}; + +/* + * ----------------------------------------------------------------- io + * + * Lua's own surface, exactly as the manual specifies it, over the objects + * above. `io.open` and `kore.fs.open` answer the same userdata; only the + * failure spelling differs (`nil, message, errno` here, a table there). + * + * The default input and output are kept in the registry rather than in the + * `io` table, so a script that overwrites `io.stdout` changes what its own + * name refers to and not where `io.write` goes — which is what the manual + * means by `io.output()` being the default, and `io.stdout` being a file. + */ +static const char kore_lua_input_key = 0; +static const char kore_lua_output_key = 0; + +static kore_lua_file *kore_lua_default(lua_State *L, const void *key) { + kore_lua_file *file; + lua_rawgetp(L, LUA_REGISTRYINDEX, key); + file = (kore_lua_file *)luaL_checkudata(L, -1, KORE_LUA_FILE); + return file; /* left on the stack: the caller pops or reuses it */ +} + +/* `io.input()` / `io.output()`: answer the default, or set it from a file + or a name and answer that. */ +static int kore_lua_io_default(lua_State *L, const void *key, const char *mode) { + if (!lua_isnoneornil(L, 1)) { + if (lua_type(L, 1) == LUA_TSTRING) { + lua_settop(L, 1); + kore_lua_opencheck(L, 1, mode); + } else { + (void)kore_lua_file_check(L, 1); + lua_pushvalue(L, 1); + } + lua_pushvalue(L, -1); + lua_rawsetp(L, LUA_REGISTRYINDEX, key); + return 1; + } + (void)kore_lua_default(L, key); + return 1; +} + +static int kore_lua_io_input(lua_State *L) { + return kore_lua_io_default(L, &kore_lua_input_key, "r"); +} + +static int kore_lua_io_output(lua_State *L) { + return kore_lua_io_default(L, &kore_lua_output_key, "w"); +} + +static int kore_lua_io_open(lua_State *L) { + return kore_lua_open_file(L, kore_lua_fail_io); +} + +static int kore_lua_io_read(lua_State *L) { + int last = lua_gettop(L); + kore_lua_file *file = kore_lua_default(L, &kore_lua_input_key); + lua_pop(L, 1); /* the registry still anchors it */ + if (file->closed) return luaL_error(L, "attempt to use a closed file"); + return kore_lua_file_read_with(L, file, 1, last, kore_lua_fail_io); +} + +static int kore_lua_io_write(lua_State *L) { + /* The arguments end where they ended before the default was pushed; the + file itself is not one of them. */ + int last = lua_gettop(L); + kore_lua_file *file = kore_lua_default(L, &kore_lua_output_key); + int self_index = lua_gettop(L); + if (file->closed) return luaL_error(L, "attempt to use a closed file"); + return kore_lua_file_write_with(L, file, 1, last, self_index, + kore_lua_fail_io); +} + +static int kore_lua_io_close(lua_State *L) { + if (lua_isnoneornil(L, 1)) { + kore_lua_file *file = kore_lua_default(L, &kore_lua_output_key); + return kore_lua_file_do_close(L, file, kore_lua_fail_io); + } + return kore_lua_file_do_close(L, kore_lua_file_check(L, 1), kore_lua_fail_io); +} + +static int kore_lua_io_flush(lua_State *L) { + /* Drop any arguments first, so the default output lands at index 1 where + `f:flush` looks for it. Replacing slot 1 with a value that IS slot 1 + would pop it and leave nothing. */ + lua_settop(L, 0); + kore_lua_default(L, &kore_lua_output_key); + return kore_lua_file_flush(L); +} + +/* `io.type`: "file", "closed file", or nil for anything that is not one. + It must not raise on a foreign value — that is what it is for. */ +static int kore_lua_io_type(lua_State *L) { + luaL_checkany(L, 1); + if (lua_getmetatable(L, 1) == 0) { lua_pushnil(L); return 1; } + luaL_getmetatable(L, KORE_LUA_FILE); + if (!lua_rawequal(L, -1, -2)) { lua_pushnil(L); return 1; } + lua_pop(L, 2); + lua_pushstring(L, ((kore_lua_file *)lua_touserdata(L, 1))->closed + ? "closed file" : "file"); + return 1; +} + +/* + * `io.lines()` iterates the default input and leaves it open. `io.lines(name)` + * opens its own, and answers **four** values — the iterator, a nil state, a + * nil control, and the file itself as the generic `for`'s to-be-closed + * variable. That fourth value is what closes the file when the loop leaves + * early, by `break` or by an error, and not only at end of file; the engine + * is 5.5, so `` is there to honour it. + */ +static int kore_lua_io_lines(lua_State *L) { + int toclose; + /* Guarantee a slot 1 to replace, exactly as `io_lines` does: `io.lines()` + with no argument at all would otherwise replace the stack top with + itself and leave nothing behind. */ + if (lua_isnone(L, 1)) lua_pushnil(L); + if (lua_isnil(L, 1)) { + kore_lua_default(L, &kore_lua_input_key); + lua_replace(L, 1); + toclose = 0; + } else { + kore_lua_opencheck(L, 1, "r"); + lua_replace(L, 1); + toclose = 1; + } + kore_lua_lines_push(L, 1, 2, toclose); + if (toclose) { + lua_pushnil(L); /* state */ + lua_pushnil(L); /* control */ + lua_pushvalue(L, 1); /* the to-be-closed file */ + return 4; + } + return 1; +} + +/* + * Draw an unused temporary name into `dst` (at least 32 bytes) and create + * the file exclusively, so the name answered is one nothing else can + * already hold. Answers the open handle, or 0 with `why` set. + * + * `os.tmpname` and `io.tmpfile` are the same act — a name nobody else has — + * differing only in what they hand back, so they share this. + */ +static unsigned long long kore_lua_tempfile(char *dst, size_t *len_out, + unsigned long long flags, + const char **why) { + static const char digits[] = "0123456789abcdef"; + *why = NULL; + for (int attempt = 0; attempt < 16; attempt++) { + unsigned char draw[8]; + unsigned long long handle = 0, length = 0; + size_t why_len = 0; + size_t len = 0; + int status; + if (kore_lua_host_random(draw, sizeof draw, why) != KORE_LUA_STATUS_OK) + return 0; + memcpy(dst, "/tmp/lua_", 9); + len = 9; + for (size_t i = 0; i < sizeof draw; i++) { + dst[len++] = digits[draw[i] >> 4]; + dst[len++] = digits[draw[i] & 15]; + } + dst[len] = '\0'; + status = kore_lua_host_call(KORE_LUA_OP_FS_OPEN, + flags | KORE_LUA_O_CREATE | + KORE_LUA_O_EXCLUSIVE, + 0, 0, dst, len, &handle, &length, why, + &why_len); + if (status == KORE_LUA_STATUS_OK) { + if (len_out != NULL) *len_out = len; + return handle + 1; /* biased so 0 means failure */ + } + /* Only a collision is worth another draw; anything else is the answer. */ + if (!(why_len == sizeof "already-exists" - 1 && + memcmp(*why, "already-exists", why_len) == 0)) + return 0; + } + *why = "no unused temporary name was found"; + return 0; +} + +/* + * `io.tmpfile`: a file opened in update mode that removes itself when its + * handle is retired. The manual says "automatically removed when the + * program ends"; this removes it at close or collection instead, which is + * strictly sooner and needs no exit hook — and the VM's teardown retires + * every handle, so the end of the program is covered by the same path. + */ +static int kore_lua_io_tmpfile(lua_State *L) { + char path[48]; + size_t len = 0; + const char *why = NULL; + unsigned long long biased = + kore_lua_tempfile(path, &len, KORE_LUA_O_READ | KORE_LUA_O_WRITE, &why); + if (biased == 0) + return kore_lua_fail_io(L, KORE_LUA_STATUS_UNAVAILABLE, + why != NULL ? why : "unavailable", + why != NULL ? strlen(why) : 0); + { + kore_lua_file *file = + kore_lua_file_push(L, biased - 1, KORE_LUA_FILE_STORE, 1, 1, 0); + file->temporary = 1; + memcpy(file->path, path, len + 1); + } + return 1; +} + +static const luaL_Reg kore_lua_io_lib[] = { + {"tmpfile", kore_lua_io_tmpfile}, + {"open", kore_lua_io_open}, + {"close", kore_lua_io_close}, + {"read", kore_lua_io_read}, + {"write", kore_lua_io_write}, + {"lines", kore_lua_io_lines}, + {"input", kore_lua_io_input}, + {"output", kore_lua_io_output}, + {"flush", kore_lua_io_flush}, + {"type", kore_lua_io_type}, + {NULL, NULL} +}; + +/* + * ----------------------------------------------------------------- os + * + * The calendar is ours. There is no libc `gmtime` here and no timezone + * database, so `os.date` computes civil time from the wall clock directly + * — Howard Hinnant's `civil_from_days`, which is exact for every day the + * proleptic Gregorian calendar defines and needs no table. + * + * kore keeps one clock and it reads UTC. `os.date("%c")` and + * `os.date("!%c")` therefore answer the same thing, `os.date` reports + * `isdst = false`, and `os.time` treats a table as UTC. That is a real + * limitation, stated rather than papered over: a local time that silently + * meant UTC would be worse than one that says so. + */ +typedef struct { + int year; /* the full year, e.g. 2026 */ + int month; /* 1..12 */ + int day; /* 1..31 */ + int hour; + int minute; + int second; + int weekday; /* 0 = Sunday */ + int yearday; /* 1..366 */ +} kore_lua_civil; + +/* Days since 1970-01-01 → year/month/day. Shifts the era to start in + March so the leap day lands at the end of a cycle and no month table is + needed. */ +static void kore_lua_civil_from_days(long long z, kore_lua_civil *out) { + long long era, doe, yoe, y, doy, mp, d, m; + z += 719468; + era = (z >= 0 ? z : z - 146096) / 146097; + doe = z - era * 146097; /* [0, 146096] */ + yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + y = yoe + era * 400; + doy = doe - (365 * yoe + yoe / 4 - yoe / 100); /* [0, 365] */ + mp = (5 * doy + 2) / 153; /* [0, 11] */ + d = doy - (153 * mp + 2) / 5 + 1; /* [1, 31] */ + m = mp < 10 ? mp + 3 : mp - 9; /* [1, 12] */ + out->year = (int)(y + (m <= 2)); + out->month = (int)m; + out->day = (int)d; +} + +/* The exact inverse, so `os.time(os.date("*t", t)) == t`. */ +static long long kore_lua_days_from_civil(int year, int month, int day) { + long long y = year, era, yoe, doy, doe; + y -= month <= 2; + era = (y >= 0 ? y : y - 399) / 400; + yoe = y - era * 400; /* [0, 399] */ + doy = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1; + doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return era * 146097 + doe - 719468; +} + +/* Floor division: a pre-1970 timestamp must round toward the earlier day, + not toward zero, or the hour comes out negative. */ +static long long kore_lua_floor_div(long long a, long long b) { + long long q = a / b; + if ((a % b != 0) && ((a < 0) != (b < 0))) q--; + return q; +} + +static void kore_lua_break_down(long long seconds, kore_lua_civil *out) { + long long days = kore_lua_floor_div(seconds, 86400); + long long rest = seconds - days * 86400; + kore_lua_civil_from_days(days, out); + out->hour = (int)(rest / 3600); + out->minute = (int)((rest % 3600) / 60); + out->second = (int)(rest % 60); + /* 1970-01-01 was a Thursday (4). */ + out->weekday = (int)(((days % 7) + 11) % 7); + out->yearday = + (int)(days - kore_lua_days_from_civil(out->year, 1, 1) + 1); +} + +/* The wall clock, as whole seconds since the epoch; `ok` says whether the + machine could answer at all. */ +static long long kore_lua_wall_seconds(int *ok) { + unsigned long long ns = 0; + const char *why = NULL; + int status = kore_lua_host_wall_ns(&ns, &why); + *ok = status == KORE_LUA_STATUS_OK; + return (long long)(ns / 1000000000ull); +} + +static void kore_lua_pad2(char *dst, int value) { + dst[0] = (char)('0' + (value / 10) % 10); + dst[1] = (char)('0' + value % 10); +} + +/* `%Y` and friends, written out: there is no `strftime` below us, and a + partial one that silently ignored an unknown specifier would be the + `string.format` mistake again. An unsupported specifier is an argument + error, so a script learns at the call rather than from wrong output. */ +static int kore_lua_strftime(lua_State *L, const char *format, + const kore_lua_civil *t) { + static const char *const wdays[] = {"Sunday", "Monday", "Tuesday", + "Wednesday", "Thursday", "Friday", + "Saturday"}; + static const char *const months[] = {"January", "February", "March", "April", + "May", "June", "July", "August", + "September", "October", "November", + "December"}; + luaL_Buffer b; + luaL_buffinit(L, &b); + for (const char *p = format; *p != '\0'; p++) { + char pad[4]; + if (*p != '%') { luaL_addchar(&b, *p); continue; } + p++; + switch (*p) { + case '\0': return luaL_error(L, "invalid conversion specifier '%%'"); + case '%': luaL_addchar(&b, '%'); break; + case 'a': luaL_addlstring(&b, wdays[t->weekday], 3); break; + case 'A': luaL_addstring(&b, wdays[t->weekday]); break; + case 'b': case 'h': luaL_addlstring(&b, months[t->month - 1], 3); break; + case 'B': luaL_addstring(&b, months[t->month - 1]); break; + case 'd': kore_lua_pad2(pad, t->day); luaL_addlstring(&b, pad, 2); break; + case 'e': + pad[0] = t->day < 10 ? ' ' : (char)('0' + t->day / 10); + pad[1] = (char)('0' + t->day % 10); + luaL_addlstring(&b, pad, 2); + break; + case 'H': kore_lua_pad2(pad, t->hour); luaL_addlstring(&b, pad, 2); break; + case 'I': { + int hour = t->hour % 12; + kore_lua_pad2(pad, hour == 0 ? 12 : hour); + luaL_addlstring(&b, pad, 2); + break; + } + case 'j': { + char day[3]; + day[0] = (char)('0' + (t->yearday / 100) % 10); + kore_lua_pad2(day + 1, t->yearday % 100); + luaL_addlstring(&b, day, 3); + break; + } + case 'm': kore_lua_pad2(pad, t->month); luaL_addlstring(&b, pad, 2); break; + case 'M': kore_lua_pad2(pad, t->minute); luaL_addlstring(&b, pad, 2); break; + case 'n': luaL_addchar(&b, '\n'); break; + case 'p': luaL_addlstring(&b, t->hour < 12 ? "AM" : "PM", 2); break; + case 'S': kore_lua_pad2(pad, t->second); luaL_addlstring(&b, pad, 2); break; + case 't': luaL_addchar(&b, '\t'); break; + case 'w': luaL_addchar(&b, (char)('0' + t->weekday)); break; + case 'y': kore_lua_pad2(pad, ((t->year % 100) + 100) % 100); + luaL_addlstring(&b, pad, 2); break; + case 'Y': { + char year[16]; + int len = 0; + int value = t->year; + int negative = value < 0; + unsigned magnitude = (unsigned)(negative ? -value : value); + char digits[12]; + int count = 0; + do { digits[count++] = (char)('0' + magnitude % 10); magnitude /= 10; } + while (magnitude != 0); + if (negative) year[len++] = '-'; + while (count > 0) year[len++] = digits[--count]; + luaL_addlstring(&b, year, (size_t)len); + break; + } + /* The one timezone kore has. */ + case 'Z': luaL_addlstring(&b, "UTC", 3); break; + case 'z': luaL_addlstring(&b, "+0000", 5); break; + case 'D': case 'x': { + char date[8]; + kore_lua_pad2(date, t->month); date[2] = '/'; + kore_lua_pad2(date + 3, t->day); date[5] = '/'; + kore_lua_pad2(date + 6, ((t->year % 100) + 100) % 100); + luaL_addlstring(&b, date, 8); + break; + } + case 'F': { + luaL_addstring(&b, ""); + { + char date[10]; + int y = t->year; + date[0] = (char)('0' + (y / 1000) % 10); + date[1] = (char)('0' + (y / 100) % 10); + kore_lua_pad2(date + 2, y % 100); + date[4] = '-'; + kore_lua_pad2(date + 5, t->month); + date[7] = '-'; + kore_lua_pad2(date + 8, t->day); + luaL_addlstring(&b, date, 10); + } + break; + } + case 'R': case 'T': case 'X': { + char clock[8]; + kore_lua_pad2(clock, t->hour); clock[2] = ':'; + kore_lua_pad2(clock + 3, t->minute); + if (*p == 'R') { luaL_addlstring(&b, clock, 5); break; } + clock[5] = ':'; + kore_lua_pad2(clock + 6, t->second); + luaL_addlstring(&b, clock, 8); + break; + } + case 'c': { + char line[24]; + int len = 0; + memcpy(line + len, wdays[t->weekday], 3); len += 3; + line[len++] = ' '; + memcpy(line + len, months[t->month - 1], 3); len += 3; + line[len++] = ' '; + line[len] = t->day < 10 ? ' ' : (char)('0' + t->day / 10); + line[len + 1] = (char)('0' + t->day % 10); + len += 2; + line[len++] = ' '; + kore_lua_pad2(line + len, t->hour); len += 2; + line[len++] = ':'; + kore_lua_pad2(line + len, t->minute); len += 2; + line[len++] = ':'; + kore_lua_pad2(line + len, t->second); len += 2; + line[len++] = ' '; + { + int y = t->year; + line[len++] = (char)('0' + (y / 1000) % 10); + line[len++] = (char)('0' + (y / 100) % 10); + kore_lua_pad2(line + len, y % 100); len += 2; + } + luaL_addlstring(&b, line, (size_t)len); + break; + } + default: + return luaL_error(L, "invalid conversion specifier '%%%c'", *p); + } + } + luaL_pushresult(&b); + return 1; +} + +/* + * A missing required field names itself, as `getfield` does — `checkinteger` + * alone would blame "field ?" and leave the caller hunting. + */ +static lua_Integer kore_lua_datefield(lua_State *L, const char *name, + lua_Integer fallback, int required) { + lua_Integer value; + if (lua_getfield(L, 1, name) == LUA_TNIL) { + lua_pop(L, 1); + if (required) + return luaL_error(L, "field '%s' missing in date table", name); + return fallback; + } + value = luaL_checkinteger(L, -1); + lua_pop(L, 1); + return value; +} + +static void kore_lua_setfield(lua_State *L, const char *name, lua_Integer v) { + lua_pushinteger(L, v); + lua_setfield(L, 1, name); +} + +static int kore_lua_os_time(lua_State *L) { + long long when; + if (lua_isnoneornil(L, 1)) { + int ok = 0; + when = kore_lua_wall_seconds(&ok); + /* A clock that cannot be read answers `nil, why` — never 0, which a + caller would read as 1970 rather than as "no reading". */ + if (!ok) return kore_lua_fail_cstr(L, KORE_LUA_STATUS_UNAVAILABLE, + "the wall clock is not set"); + lua_pushinteger(L, (lua_Integer)when); + return 1; + } + luaL_checktype(L, 1, LUA_TTABLE); + lua_settop(L, 1); /* the table is the only thing `setfield` may write to */ + { + /* Out-of-range fields normalize, exactly as `mktime` does: `month = 13` + is January of the next year, which is what makes date arithmetic by + field addition work. */ + lua_Integer year = kore_lua_datefield(L, "year", 0, 1); + lua_Integer month = kore_lua_datefield(L, "month", 0, 1); + lua_Integer day = kore_lua_datefield(L, "day", 0, 1); + lua_Integer hour = kore_lua_datefield(L, "hour", 12, 0); + lua_Integer minute = kore_lua_datefield(L, "min", 0, 0); + lua_Integer second = kore_lua_datefield(L, "sec", 0, 0); + long long carry = kore_lua_floor_div((long long)month - 1, 12); + long long days; + kore_lua_civil t; + year += carry; + month = (long long)month - 1 - carry * 12 + 1; + days = kore_lua_days_from_civil((int)year, (int)month, 1) + (day - 1); + when = days * 86400 + hour * 3600 + minute * 60 + second; + /* `mktime` normalizes the caller's table in place and fills in the + fields it derived; `setallfields` is what a script reads back to + learn the weekday it never supplied. */ + kore_lua_break_down(when, &t); + kore_lua_setfield(L, "year", t.year); + kore_lua_setfield(L, "month", t.month); + kore_lua_setfield(L, "day", t.day); + kore_lua_setfield(L, "hour", t.hour); + kore_lua_setfield(L, "min", t.minute); + kore_lua_setfield(L, "sec", t.second); + kore_lua_setfield(L, "wday", t.weekday + 1); + kore_lua_setfield(L, "yday", t.yearday); + lua_pushboolean(L, 0); + lua_setfield(L, 1, "isdst"); + lua_pushinteger(L, (lua_Integer)when); + return 1; + } +} + +static int kore_lua_os_date(lua_State *L) { + const char *format = luaL_optstring(L, 1, "%c"); + long long when; + kore_lua_civil t; + if (lua_isnoneornil(L, 2)) { + int ok = 0; + when = kore_lua_wall_seconds(&ok); + if (!ok) return kore_lua_fail_cstr(L, KORE_LUA_STATUS_UNAVAILABLE, + "the wall clock is not set"); + } else { + when = (long long)luaL_checkinteger(L, 2); + } + /* kore keeps one clock and it reads UTC, so the `!` prefix selects the + same calendar it would without it. Accepted rather than rejected: a + portable script writes `!` and must not fail here for doing so. */ + if (*format == '!') format++; + kore_lua_break_down(when, &t); + if (strcmp(format, "*t") == 0 || strcmp(format, "!*t") == 0) { + lua_createtable(L, 0, 9); + lua_pushinteger(L, t.year); lua_setfield(L, -2, "year"); + lua_pushinteger(L, t.month); lua_setfield(L, -2, "month"); + lua_pushinteger(L, t.day); lua_setfield(L, -2, "day"); + lua_pushinteger(L, t.hour); lua_setfield(L, -2, "hour"); + lua_pushinteger(L, t.minute); lua_setfield(L, -2, "min"); + lua_pushinteger(L, t.second); lua_setfield(L, -2, "sec"); + lua_pushinteger(L, t.weekday + 1); lua_setfield(L, -2, "wday"); + lua_pushinteger(L, t.yearday); lua_setfield(L, -2, "yday"); + /* kore has no timezone database, so there is no daylight rule to be + inside of. `false`, not `nil`: the field is answered, not missing. */ + lua_pushboolean(L, 0); lua_setfield(L, -2, "isdst"); + return 1; + } + return kore_lua_strftime(L, format, &t); +} + +static int kore_lua_os_difftime(lua_State *L) { + lua_Number later = luaL_checknumber(L, 1); + lua_Number earlier = luaL_optnumber(L, 2, 0); + lua_pushnumber(L, later - earlier); + return 1; +} + +/* + * `os.clock`: seconds of processor time. kore gives a task a monotonic + * counter, not a per-process CPU accounting, so this is time since the VM + * started — the approximation the manual allows, and an exact one for the + * single-threaded script that is the only thing measuring it. + */ +static int kore_lua_os_clock(lua_State *L) { + unsigned long long ns = 0; + const char *why = NULL; + int status = kore_lua_host_uptime_ns(&ns, &why); + if (status != KORE_LUA_STATUS_OK) return kore_lua_fail_cstr(L, status, why); + { + unsigned long long started = + (unsigned long long)lua_tointeger(L, lua_upvalueindex(1)); + lua_pushnumber(L, (lua_Number)(ns - started) / 1e9); + } + return 1; +} + +static int kore_lua_os_remove(lua_State *L) { + size_t len = 0; + const char *path = luaL_checklstring(L, 1, &len); + unsigned long long r = 0, r2 = 0; + int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_REMOVE, 0, 0, 0, path, len, &r, + &r2, NULL, NULL, kore_lua_fail_io); + if (n) return n; + lua_pushboolean(L, 1); + return 1; +} + +static int kore_lua_os_rename(lua_State *L) { + /* The same joined window `kore.fs.rename` builds, in `io`'s spelling. */ + size_t from_len = 0, to_len = 0; + const char *from = luaL_checklstring(L, 1, &from_len); + const char *to = luaL_checklstring(L, 2, &to_len); + unsigned long long r = 0, r2 = 0; + luaL_Buffer b; + char *joined; + int n; + luaL_buffinit(L, &b); + joined = luaL_prepbuffsize(&b, from_len + to_len + 1); + memcpy(joined, from, from_len); + joined[from_len] = '\0'; + memcpy(joined + from_len + 1, to, to_len); + n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_RENAME, 0, 0, 0, joined, + from_len + to_len + 1, &r, &r2, NULL, NULL, + kore_lua_fail_io); + if (n) return n; + lua_pushboolean(L, 1); + return 1; +} + +/* + * `os.tmpname`. Upstream's answers a name and leaves the caller to race + * for it; this one *creates* the file exclusively and then answers its + * name, so the name it returns is one nothing else can already hold. That + * is a deliberate strengthening, stated here because a caller that expects + * the file to be absent would otherwise be surprised: the manual requires + * the caller to remove the file, which this satisfies either way. + */ +static int kore_lua_os_tmpname(lua_State *L) { + char path[48]; + size_t len = 0; + const char *why = NULL; + unsigned long long biased = + kore_lua_tempfile(path, &len, KORE_LUA_O_WRITE, &why); + if (biased == 0) + return kore_lua_fail_cstr(L, KORE_LUA_STATUS_UNAVAILABLE, + why != NULL ? why : "unavailable"); + { + /* The name is what was asked for, not the handle: close it and leave + the file in place, which is the POSIX behaviour the manual describes + ("this function also creates a file with that name, to avoid + security risks"). The caller opens and removes it. */ + unsigned long long r = 0, r2 = 0; + const char *ignored = NULL; + size_t ignored_len = 0; + (void)kore_lua_host_call(KORE_LUA_OP_FS_CLOSE, biased - 1, 0, 0, NULL, 0, + &r, &r2, &ignored, &ignored_len); + } + lua_pushlstring(L, path, len); + return 1; +} + +/* + * `os.getenv` answers nil for every name, because a kore task has no + * environment: authority arrives as capabilities in its launch, not as + * inherited strings. Present and conforming rather than absent — an unset + * variable is exactly what `nil` means, and a script that probes for one + * gets the right answer instead of a missing-function error. + */ +static int kore_lua_os_getenv(lua_State *L) { + (void)luaL_checkstring(L, 1); + lua_pushnil(L); + return 1; +} + +/* One locale, named as C names it. */ +static int kore_lua_os_setlocale(lua_State *L) { + const char *locale = luaL_optstring(L, 1, NULL); + static const char *const categories[] = {"all", "collate", "ctype", + "monetary", "numeric", "time", NULL}; + (void)luaL_checkoption(L, 2, "all", categories); + if (locale == NULL || strcmp(locale, "C") == 0 || *locale == '\0') + lua_pushliteral(L, "C"); + else + lua_pushnil(L); /* no other locale can be set */ + return 1; +} + +/* + * `os.exit`. The task cannot simply leave: the evaluation has C frames + * below it, `` variables above it, and a REPL that still owes its + * caller the output collected so far. So this records the code with the + * host and then raises a sentinel, which unwinds through `lua_pcall` + * running every `__close` on the way — and the host reads the record when + * the evaluation returns. A verb that longjmp'd past the handlers would + * be a member that does not do what its name says. + */ +static int kore_lua_os_exit(lua_State *L) { + unsigned long long code = 0, r2 = 0; + const char *why = NULL; + size_t why_len = 0; + int requested = 0; + if (lua_isboolean(L, 1)) + requested = lua_toboolean(L, 1) ? 0 : 1; + else + requested = (int)luaL_optinteger(L, 1, 0); + /* `close` is the second argument upstream; here the VM is always closed + by the host after the evaluation, so it is accepted and ignored. */ + (void)lua_toboolean(L, 2); + (void)kore_lua_host_call(KORE_LUA_OP_PROC_EXIT, (unsigned long long)requested, + 0, 0, NULL, 0, &code, &r2, &why, &why_len); + return luaL_error(L, KORE_LUA_EXIT_SENTINEL); +} + +static const luaL_Reg kore_lua_os_lib[] = { + {"time", kore_lua_os_time}, + {"date", kore_lua_os_date}, + {"difftime", kore_lua_os_difftime}, + {"remove", kore_lua_os_remove}, + {"rename", kore_lua_os_rename}, + {"tmpname", kore_lua_os_tmpname}, + {"getenv", kore_lua_os_getenv}, + {"setlocale", kore_lua_os_setlocale}, + {"exit", kore_lua_os_exit}, + {NULL, NULL} +}; + +/* + * ----------------------------------------------------------------- kore + * + * One table, provisioned from what this process actually holds. It is not + * a lookup service for authority: every submodule below is present because + * the image's launch profile conveyed the face it needs, and a submodule + * whose authority is absent is absent too, so `if kore.fs then` is the + * idiom rather than a call that always fails. + */ +static int kore_lua_has(lua_State *L) { + const char *name = luaL_checkstring(L, 1); + lua_getfield(L, lua_upvalueindex(1), name); + lua_pushboolean(L, !lua_isnil(L, -1)); + return 1; +} + +/* + * `caps()` answers the names in registration order, taken from the list + * built at open time rather than from a table walk: a walk follows hash + * order, which now varies per boot because the VM's seed does, and a caps + * line that reorders itself between boots is not something a gate can pin. + * Building it at registration also means an embedder's libraries list + * themselves — there is no second place to remember to update. + */ +static int kore_lua_caps(lua_State *L) { + lua_newtable(L); + lua_Integer count = (lua_Integer)lua_rawlen(L, lua_upvalueindex(1)); + for (lua_Integer i = 1; i <= count; i++) { + lua_rawgeti(L, lua_upvalueindex(1), i); + lua_rawseti(L, -2, i); + } + return 1; +} + +/* + * The embedder's libraries, handed down from Rust (`Vm::new`): a name and + * an opener per entry, registered beneath `kore` after this crate's own. + * Ops they post start at 64 (`OP_EMBEDDER_BASE`), which is what keeps one + * transport serving both halves. + * + * They reach `luaopen_kore` as closure upvalues rather than through a file + * static: the VM is per-thread, and a static would be a second, untyped + * channel for something the constructor is already passing. + */ +typedef struct { + const char *name; + lua_CFunction opener; +} kore_lua_library; + +/* Register one module and record its name for `caps()`; the caps array is + at the top of the stack, the kore table just below it. */ +static void kore_lua_module(lua_State *L, const char *name, lua_Integer *index) { + lua_setfield(L, -3, name); + lua_pushstring(L, name); + lua_rawseti(L, -2, ++(*index)); +} + +static int luaopen_kore(lua_State *L) { + const kore_lua_library *libraries = + (const kore_lua_library *)lua_touserdata(L, lua_upvalueindex(1)); + size_t count = (size_t)lua_tointeger(L, lua_upvalueindex(2)); + lua_newtable(L); /* the kore table */ + lua_newtable(L); /* the caps array, in registration order */ + lua_Integer listed = 0; + + luaL_newlib(L, kore_lua_time_lib); + kore_lua_module(L, "time", &listed); + + luaL_newlib(L, kore_lua_random_lib); + kore_lua_module(L, "random", &listed); + + luaL_newlib(L, kore_lua_fs_lib); + kore_lua_module(L, "fs", &listed); + + luaL_newlib(L, kore_lua_proc_lib); + lua_newtable(L); + lua_pushcfunction(L, kore_lua_proc_index); + lua_setfield(L, -2, "__index"); + lua_setmetatable(L, -2); + kore_lua_module(L, "proc", &listed); + + for (size_t i = 0; libraries != NULL && i < count; i++) { + const kore_lua_library *library = &libraries[i]; + if (library->name == NULL || library->opener == NULL) continue; + lua_pushcfunction(L, library->opener); + lua_call(L, 0, 1); + kore_lua_module(L, library->name, &listed); + } + + /* `caps` closes over the array; `has` over the table it describes. */ + lua_pushcclosure(L, kore_lua_caps, 1); + lua_setfield(L, -2, "caps"); + lua_pushvalue(L, -1); + lua_pushcclosure(L, kore_lua_has, 1); + lua_setfield(L, -2, "has"); + + return 1; +} + +static int kore_lua_require(lua_State *L) { + size_t len = 0; + const char *name = luaL_checklstring(L, 1, &len); + const char *bytes = NULL; + size_t bytes_len = 0; + lua_CFunction opener = NULL; + int kind; + lua_settop(L, 1); + luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE); /* 2: LOADED */ + lua_getfield(L, 2, name); + if (lua_toboolean(L, -1)) return 1; /* already loaded */ + lua_pop(L, 1); + kind = kore_lua_host_require(name, len, &bytes, &bytes_len, &opener); + if (kind == KORE_LUA_MODULE_SOURCE) { + const char *chunkname = lua_pushfstring(L, "=%s", name); + int status = luaL_loadbufferx(L, bytes, bytes_len, chunkname, "t"); + kore_lua_host_release(); + if (status != LUA_OK) return lua_error(L); + lua_remove(L, -2); /* the chunk name */ + } + else if (kind == KORE_LUA_MODULE_NATIVE && opener != NULL) { + lua_pushcfunction(L, opener); + } + else { + return luaL_error(L, "module '%s' not found: %s", name, + bytes != NULL ? bytes : "no route"); + } + lua_pushstring(L, name); + lua_call(L, 1, 1); /* loader(name) */ + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_pushboolean(L, 1); + } + lua_pushvalue(L, -1); + lua_setfield(L, 2, name); /* LOADED[name] = result */ + return 1; +} + +/* + * Conversion remains wholly below lua_pcall's C recovery frame. A hostile + * __tostring may throw, but it never jumps across a Rust callback frame: the + * append callback receives already-materialized bytes and never re-enters Lua. + */ +static int kore_lua_print(lua_State *L) { + kore_lua_writer *writer = (kore_lua_writer *)lua_touserdata( + L, lua_upvalueindex(1)); + int top = lua_gettop(L); + if (writer == NULL || writer->append == NULL) return 0; + for (int index = 1; index <= top; index++) { + size_t len = 0; + const char *text; + if (index != 1 && !writer->append(writer->ud, "\t", 1, 0)) + return luaL_error(L, "host output limit exceeded"); + text = luaL_tolstring(L, index, &len); + if (!writer->append(writer->ud, text, len, 0)) + return luaL_error(L, "host output limit exceeded"); + lua_pop(L, 1); + } + if (!writer->append(writer->ud, "\n", 1, 0)) + return luaL_error(L, "host output limit exceeded"); + return 0; +} + +/* + * Every library opener can allocate and therefore throw. Keep the complete + * setup sequence below a C recovery frame so constructor OOM is reported as + * a null state instead of longjmping through the Rust caller. + */ +/* + * The stock `io` table, its three standard streams, and the defaults + * `io.read` / `io.write` act on. + * + * `io.stdin` is a real file object that is always at end of file: a REPL's + * input is the lines the host is already feeding the evaluator, so there + * is nothing left for a chunk to read from underneath it. Present and + * answering nil is the conforming shape — that is exactly what a stream at + * end of file does — where an absent `io.stdin` would break every script + * that merely mentions it. + */ +static int luaopen_kore_io(lua_State *L) { + luaL_newlib(L, kore_lua_io_lib); + + kore_lua_file_push(L, 0, KORE_LUA_FILE_IN, 1, 0, 0); + lua_pushvalue(L, -1); + lua_rawsetp(L, LUA_REGISTRYINDEX, &kore_lua_input_key); + lua_setfield(L, -2, "stdin"); + + kore_lua_file_push(L, 0, KORE_LUA_FILE_OUT, 0, 1, 0); + lua_pushvalue(L, -1); + lua_rawsetp(L, LUA_REGISTRYINDEX, &kore_lua_output_key); + lua_setfield(L, -2, "stdout"); + + kore_lua_file_push(L, 0, KORE_LUA_FILE_ERR, 0, 1, 0); + lua_setfield(L, -2, "stderr"); + return 1; +} + +/* + * The stock `os` table. + * + * `os.clock` closes over the moment the VM started, because it reports + * time *used by the program* and the machine's counter does not start + * there. `os.execute` and `io.popen` are absent from this crate: both + * need to launch a process, which is a service face the embedding program + * holds and this crate deliberately does not (see `Extension`). + */ +static int luaopen_kore_os(lua_State *L) { + unsigned long long started = 0; + const char *why = NULL; + luaL_newlib(L, kore_lua_os_lib); + if (kore_lua_host_uptime_ns(&started, &why) != KORE_LUA_STATUS_OK) started = 0; + lua_pushinteger(L, (lua_Integer)started); + lua_pushcclosure(L, kore_lua_os_clock, 1); + lua_setfield(L, -2, "clock"); + return 1; +} + +static int kore_lua_open_libraries(lua_State *L) { + luaL_requiref(L, LUA_GNAME, luaopen_base, 1); + /* Omit inert stdio/warning APIs and the unverified binary-chunk loader. */ + static const char *const omitted_base[] = { + "dofile", "load", "loadfile", "warn", NULL + }; + for (const char *const *name = omitted_base; *name != NULL; name++) { + lua_pushnil(L); lua_setfield(L, -2, *name); + } + /* The one loader the host offers: over the store, through the executor. */ + lua_pushcfunction(L, kore_lua_require); + lua_setfield(L, -2, "require"); + lua_pop(L, 1); + luaL_requiref(L, LUA_COLIBNAME, luaopen_coroutine, 1); lua_pop(L, 1); + luaL_requiref(L, LUA_TABLIBNAME, luaopen_table, 1); lua_pop(L, 1); + luaL_requiref(L, LUA_STRLIBNAME, luaopen_string, 1); + /* No partial printf and no producer for unsupported binary chunks. */ + lua_pushnil(L); lua_setfield(L, -2, "format"); lua_pop(L, 1); + lua_getglobal(L, LUA_STRLIBNAME); + lua_pushnil(L); lua_setfield(L, -2, "dump"); lua_pop(L, 1); + luaL_requiref(L, LUA_MATHLIBNAME, luaopen_math, 1); lua_pop(L, 1); + luaL_requiref(L, LUA_UTF8LIBNAME, luaopen_utf8, 1); lua_pop(L, 1); + /* The file object's metatable, before anything that can create one. */ + kore_lua_file_metatable(L); + luaL_requiref(L, LUA_IOLIBNAME, luaopen_kore_io, 1); lua_pop(L, 1); + luaL_requiref(L, LUA_OSLIBNAME, luaopen_kore_os, 1); lua_pop(L, 1); + /* kore's own standard library, provisioned from this process's faces. + One table rather than a scatter of globals: `require "kore"` answers + it, and an embedder's own libraries sit beside them. Registered by + hand rather than through luaL_requiref, which has no way to hand the + opener the embedder's table; the two steps below are exactly what + requiref does once the opener has run. */ + lua_pushlightuserdata(L, lua_touserdata(L, 1)); + lua_pushvalue(L, 2); + lua_pushcclosure(L, luaopen_kore, 2); + lua_call(L, 0, 1); + luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE); + lua_pushvalue(L, -2); + lua_setfield(L, -2, "kore"); + lua_pop(L, 1); + lua_setglobal(L, "kore"); + return 0; +} + +lua_State *kore_lua_new(lua_Alloc alloc, void *ud, unsigned seed, + const kore_lua_library *libraries, size_t count) { + lua_State *L = lua_newstate(alloc, ud, seed); + if (L == NULL) return NULL; + lua_pushcfunction(L, kore_lua_open_libraries); + lua_pushlightuserdata(L, (void *)(uintptr_t)libraries); + lua_pushinteger(L, (lua_Integer)count); + if (lua_pcall(L, 2, 0, 0) != LUA_OK) { + lua_close(L); + return NULL; + } + return L; +} + +static int kore_lua_install_print_inner(lua_State *L) { + const kore_lua_writer *config = (const kore_lua_writer *)lua_touserdata(L, 1); + if (config == NULL || config->append == NULL) + return luaL_error(L, "invalid host output callback"); + kore_lua_writer *writer = (kore_lua_writer *)lua_newuserdatauv( + L, sizeof(kore_lua_writer), 0); + *writer = *config; + /* The same sink, reachable by name: `io.write` and `io.stderr:write` go + where `print` goes, rather than each opening a channel of its own. */ + lua_pushvalue(L, -1); + lua_rawsetp(L, LUA_REGISTRYINDEX, &kore_lua_writer_key); + lua_pushcclosure(L, kore_lua_print, 1); + lua_setglobal(L, "print"); + return 0; +} + +/* + * The light-userdata argument is borrowed only for this synchronous pcall; + * the inner function copies it into Lua-owned storage before returning. + */ +int kore_lua_install_print(lua_State *L, kore_lua_append append, void *ud) { + kore_lua_writer config = { append, ud }; + lua_pushcfunction(L, kore_lua_install_print_inner); + lua_pushlightuserdata(L, &config); + return lua_pcall(L, 1, 0, 0); +} + +int kore_lua_load(lua_State *L, const char *bytes, size_t len, + const char *name) { + return luaL_loadbufferx(L, bytes, len, name, "t"); +} + +int kore_lua_call(lua_State *L, int arguments, int results) { + return lua_pcall(L, arguments, results, 0); +} + +static int kore_lua_print_results_inner(lua_State *L) { + int results = lua_gettop(L); + lua_getglobal(L, "print"); + lua_insert(L, 1); + lua_call(L, results, 0); + return 0; +} + +int kore_lua_print_results(lua_State *L) { + int results = lua_gettop(L); + if (results == 0) return LUA_OK; + /* + * Both the global lookup (including a hostile _G.__index) and the call are + * below this C recovery frame. No Lua longjmp can cross the Rust caller. + */ + lua_pushcfunction(L, kore_lua_print_results_inner); + lua_insert(L, 1); + return lua_pcall(L, results, 0, 0); +} + +int kore_lua_top(lua_State *L) { return lua_gettop(L); } +void kore_lua_set_top(lua_State *L, int top) { lua_settop(L, top); } +int kore_lua_type(lua_State *L, int index) { return lua_type(L, index); } +const char *kore_lua_error_string(lua_State *L, size_t *len) { + static const char fallback[] = "Lua error"; + if (lua_type(L, -1) == LUA_TSTRING) + return lua_tolstring(L, -1, len); + *len = sizeof(fallback) - 1; + return fallback; +} +void kore_lua_close(lua_State *L) { lua_close(L); } diff --git a/crates/lua/src/lib.rs b/crates/lua/src/lib.rs new file mode 100644 index 0000000..1285f51 --- /dev/null +++ b/crates/lua/src/lib.rs @@ -0,0 +1,221 @@ +//! Lua 5.5 on Kore. +//! +//! The pinned engine (`vendor/lua`, `KORE-VENDOR.md`), its freestanding +//! host, and the `kore.*` standard library a script sees. An embedding +//! program supplies the process around it — arguments, streams, a REPL or a +//! batch run — and, through [`Extension`], any library this crate does not +//! own. +//! +//! # Two surfaces, one implementation +//! +//! A script reaches the same code two ways, because the two audiences want +//! different things and neither should be made to use the other's. +//! +//! ```lua +//! local kore = require "kore" -- also a global +//! kore.time.now() -- monotonic seconds, float +//! kore.time.sleep(0.25) +//! kore.random.bytes(32) +//! kore.proc.args -- a 1-based array, read once +//! kore.fs.read_file "/etc/hosts" -- value, err +//! kore.has "fs" kore.caps() -- honest provisioning +//! ``` +//! +//! ```lua +//! local f = assert(io.open("/tmp/log", "a")) -- the SAME object +//! f:write("hello\n"); f:close() +//! for line in io.lines "/etc/hosts" do print(line) end +//! print(os.date("!%Y-%m-%dT%H:%M:%SZ")) +//! ``` +//! +//! `io.open` and [`files::open`]'s `kore.fs.open` answer one userdata; +//! only the failure spelling differs — `nil, message, errno` on the stock +//! surface, `nil, {code = "not-found"}` on the kore one, both derived from +//! the same [`kore::fs::FsError`]. A stock member either conforms fully or +//! is absent, never in between. +//! +//! Plus Lua's own base, coroutine, math, string, table and UTF-8 libraries, +//! and `require` over the task's store (`.lua`, then `.so`). +//! +//! # What is absent, and why +//! +//! - `os.execute`, `io.popen` — both launch a process, which is a service +//! face the embedding program holds and this crate deliberately does not. +//! An embedder adds them through [`Extension`]. +//! - `string.format` — kore has no hosted `printf`, and a hand-written +//! subset would accept format strings it silently did not conform to +//! (`KORE-VENDOR.md`). +//! - `load`, `loadfile`, `dofile`, `string.dump` — this build accepts text +//! chunks only; upstream Lua has no bytecode verifier. +//! +//! `os.getenv` is *present* and answers `nil` for every name: a kore task +//! has no environment, authority arrives as capabilities in its launch, +//! and an unset variable is exactly what `nil` means. +//! +//! # The six rules +//! +//! Every `kore.*` module obeys all six, so one module teaches the rest: +//! one result convention (`value, err`, never a raise for an expected +//! failure); one error vocabulary; one options convention (a trailing table +//! with named keys); one handle protocol; one unit convention (seconds are +//! floats, anything else carries its unit in the name); one availability +//! convention (`kore.has`/`kore.caps`, and an unheld module is `nil`). +//! +//! # Threads +//! +//! Lua is a synchronous C engine, so the VM runs on a [`kore::thread`] +//! worker and the executor stays on thread 0. [`bridge`] documents the one +//! transport between them. Verbs needing no service — `kore.time.now`, +//! `kore.random`, and the whole calendar — never cross it. +//! +//! # Time +//! +//! kore keeps one clock and it reads UTC. `os.date` therefore answers the +//! same thing with and without the `!` prefix, reports `isdst = false`, and +//! `os.time` reads a table as UTC. The calendar itself is computed, not +//! tabulated: there is no `gmtime` below this and no timezone database. +//! A clock that cannot be read answers `nil, why` and never a zero +//! timestamp, which a caller would read as 1970 rather than as "no +//! reading". + +#![no_std] +// `shim`'s allocator seam, which only the engine's build has. +#![cfg_attr(target_os = "none", feature(allocator_api))] + +extern crate alloc; + +// The engine and its transport exist only where the engine does: the C +// amalgamation, the worker thread and the doorbell are all `target_os = +// "none"` surfaces, and `kore::runtime`'s thread verbs are themselves +// gated that way. Gating here rather than leaving the crate target-only is +// what lets `files`'s logic — path splitting, handle encoding, the error +// vocabulary — be checked and unit-tested by the host workspace. +#[cfg(target_os = "none")] +pub mod bridge; +/// `kore.fs`'s thread-0 half: the open-file table, the positional verbs, +/// and the error vocabulary. Public because the vocabulary is part of the +/// contract — an embedder whose own library can fail a filesystem failure +/// renders it with [`files::code`] rather than spelling one of its own. +pub mod files; +#[cfg(target_os = "none")] +mod numeric; +#[cfg(target_os = "none")] +mod shim; +#[cfg(target_os = "none")] +mod vm; + +use alloc::string::String; +use core::ffi::{CStr, c_int, c_void}; + +#[cfg(target_os = "none")] +pub use bridge::{Fatal, Host, Outcome, Services}; + +/// A `lua_CFunction`: the C signature every Lua-callable function has. +/// +/// An [`Extension`] writes these in ordinary Rust — `extern "C" fn` over +/// the exported `lua_*` / `luaL_*` API — so an embedder adds a library +/// without compiling C of its own. +pub type LuaOpener = unsafe extern "C" fn(*mut c_void) -> c_int; + +/// One library an embedder adds beneath the `kore` table. +/// +/// `name` is the field it lands on (`kore.`); `opener` is called once +/// at VM construction and must leave exactly one value — the module table — +/// on the stack, exactly as `luaL_newlib` does. +/// +/// `#[repr(C)]` with a thin name pointer so a `&'static [Library]` *is* the +/// array the C amalgamation walks; nothing is marshalled at startup. +#[repr(C)] +pub struct Library { + name: *const core::ffi::c_char, + opener: LuaOpener, +} + +impl Library { + /// Name a library and the opener that builds it. + pub const fn new(name: &'static CStr, opener: LuaOpener) -> Self { + Self { + name: name.as_ptr(), + opener, + } + } +} + +// SAFETY: both fields are `'static` — a string literal's bytes and a +// function pointer. The table is read, never written, and the VM thread is +// the only reader. +unsafe impl Sync for Library {} + +/// What an embedder-served op answers, in the three shapes every `kore.*` +/// call already uses: a value, a refusal the service named, or a statement +/// that the call could not be made at all. +pub enum Answer { + /// Up to two result words. Lua sees whatever the opener pushes. + Value(u64, u64), + /// A byte answer — a read's bytes, a directory's names — with one + /// result word beside it (how many bytes, how many entries). Bytes and + /// a refusal ride the same reply field, so a verb that sometimes + /// answers data and sometimes a reason needs no second channel. + Bytes(u64, alloc::vec::Vec), + /// The service refused. Lua sees `nil, "refused: "`. + Refused(String), + /// No face, no lease, a broken ring. Lua sees `nil, ""`. + Unavailable(String), +} + +/// The first op number an embedder may use. +/// +/// Below this the vocabulary is this crate's (`require`, `kore.time.sleep`, +/// `kore.proc.args`); at or above it [`Extension::serve`] is asked. Keeping +/// one numbering over one transport is what lets an embedder add a library +/// without this crate learning that the library exists. +pub const OP_EMBEDDER_BASE: u32 = 64; + +/// Libraries and ops the embedding program adds to the VM. +/// +/// The split is deliberate: this crate owns the engine, the transport and +/// everything reachable through libkore's own facade; anything needing a +/// Kore *service* — an accelerator face, a device — belongs to the program +/// that was launched holding it. Kore's `lua` binary implements this for +/// `kore.accel`; the shape mirrors `kore-wasi`'s embedder-composed +/// interfaces. +/// +/// Selection is by type instantiation, not runtime dispatch: [`Host::start`] +/// is generic over `E`, so `LIBRARIES` reaches the worker thread as +/// monomorphized static data with no global to publish it. +/// +/// There is deliberately **no `'static` bound**. The worker thread reads +/// only [`Extension::LIBRARIES`], which is `&'static` in its own right; +/// [`Extension::serve`] runs on thread 0 against a `&mut E` that never +/// leaves that thread. Requiring `'static` of the whole type would forbid +/// the one shape an embedder actually has — a service face borrowed from +/// the `Rt` and the transport its `main` owns. +pub trait Extension { + /// Registered beneath `kore` at VM construction, in this order. + const LIBRARIES: &'static [Library] = &[]; + + /// Serve one op at or above [`OP_EMBEDDER_BASE`], on the executor + /// thread, with the cell's three argument words. + fn serve(&mut self, op: u32, args: [u64; 3]) -> impl Future; +} + +/// An embedder that adds nothing: Lua plus `kore.*` and no more. +pub struct NoExtension; + +impl Extension for NoExtension { + async fn serve(&mut self, _op: u32, _args: [u64; 3]) -> Answer { + Answer::Unavailable(String::from("this program registered no such library")) + } +} + +/// The Lua C API an [`Extension`]'s openers call. +/// +/// Deliberately the subset an ordinary library binding needs — arguments +/// in, values out, a table built, an error raised — rather than the whole +/// engine. Every one of these is exported from this crate's image +/// (`--export-dynamic-symbol=lua_*` / `luaL_*`), which is also the ABI a +/// native `require`d module binds against. +#[cfg(target_os = "none")] +pub mod api { + pub use crate::vm::api::*; +} diff --git a/crates/lua/src/numeric.rs b/crates/lua/src/numeric.rs new file mode 100644 index 0000000..22ee7c5 --- /dev/null +++ b/crates/lua/src/numeric.rs @@ -0,0 +1,323 @@ +//! Freestanding decimal parsing and correctly rounded `%g` formatting. + +use core::{ + ffi::{c_char, c_double}, + fmt::{self, Write}, + ptr, slice, + str::FromStr, +}; + +/// Largest output produced by Rust's `LowerExp` formatter for an `f64`. +/// +/// The proven implementation currently needs at most 24 bytes. Keep slack so +/// this adapter remains safe if that private spelling changes in Rust core. +const RAW_NUMBER_BYTES: usize = 32; +const LUA_NUMBER_BYTES: usize = 64; + +/// ISO C locale whitespace. Rust's ASCII predicate deliberately omits VT +/// (`0x0b`), while Lua's lexer, patterns, and `strtod` contract include it. +pub(crate) const fn c_locale_whitespace(byte: u8) -> bool { + matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r') +} + +struct Buffer<'a> { + bytes: &'a mut [u8], + len: usize, +} + +impl Buffer<'_> { + fn push(&mut self, byte: u8) -> bool { + let Some(slot) = self.bytes.get_mut(self.len) else { + return false; + }; + *slot = byte; + self.len += 1; + true + } + + fn extend(&mut self, bytes: &[u8]) -> bool { + let Some(end) = self.len.checked_add(bytes.len()) else { + return false; + }; + let Some(dst) = self.bytes.get_mut(self.len..end) else { + return false; + }; + dst.copy_from_slice(bytes); + self.len = end; + true + } + + fn repeat(&mut self, byte: u8, count: usize) -> bool { + for _ in 0..count { + if !self.push(byte) { + return false; + } + } + true + } +} + +impl Write for Buffer<'_> { + fn write_str(&mut self, text: &str) -> fmt::Result { + self.extend(text.as_bytes()).then_some(()).ok_or(fmt::Error) + } +} + +/// Parse the decimal grammar Lua asks of `strtod`, retaining C's end pointer. +/// +/// # Safety +/// +/// `text` must point to a readable NUL-terminated C string. If non-null, +/// `end` must point to writable storage for one pointer. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn kore_lua_strtod(text: *const c_char, end: *mut *mut c_char) -> c_double { + if text.is_null() { + return 0.0; + } + let mut len = 0usize; + // SAFETY: the C contract above guarantees a readable string through its + // terminator. Unlike a token-only ceiling, this also preserves public + // `tonumber` behavior for long, valid Lua strings. + while unsafe { *text.add(len) } != 0 { + len += 1; + } + // SAFETY: the scan above established this live prefix. + let bytes = unsafe { slice::from_raw_parts(text.cast::(), len) }; + let mut at = 0; + while bytes.get(at).is_some_and(|byte| c_locale_whitespace(*byte)) { + at += 1; + } + let number_start = at; + if bytes + .get(at) + .is_some_and(|byte| matches!(*byte, b'+' | b'-')) + { + at += 1; + } + let mut digits = 0usize; + while bytes.get(at).is_some_and(u8::is_ascii_digit) { + at += 1; + digits += 1; + } + if bytes.get(at) == Some(&b'.') { + at += 1; + while bytes.get(at).is_some_and(u8::is_ascii_digit) { + at += 1; + digits += 1; + } + } + if digits == 0 { + if !end.is_null() { + // SAFETY: guaranteed by the caller contract. + unsafe { *end = text.cast_mut() }; + } + return 0.0; + } + let mantissa_end = at; + if bytes + .get(at) + .is_some_and(|byte| matches!(*byte, b'e' | b'E')) + { + let exponent = at; + at += 1; + if bytes + .get(at) + .is_some_and(|byte| matches!(*byte, b'+' | b'-')) + { + at += 1; + } + let exponent_start = at; + while bytes.get(at).is_some_and(u8::is_ascii_digit) { + at += 1; + } + if at == exponent_start { + at = exponent; + } + } + let parsed_end = at.max(mantissa_end); + let Ok(number) = core::str::from_utf8(&bytes[number_start..parsed_end]) else { + if !end.is_null() { + // SAFETY: guaranteed by the caller contract. + unsafe { *end = text.cast_mut() }; + } + return 0.0; + }; + let Ok(value) = f64::from_str(number) else { + if !end.is_null() { + // SAFETY: guaranteed by the caller contract. + unsafe { *end = text.cast_mut() }; + } + return 0.0; + }; + if !end.is_null() { + // SAFETY: `parsed_end <= len`, so this remains within the caller's + // live C string (or one past its non-NUL prefix at the terminator). + unsafe { *end = text.add(parsed_end).cast_mut() }; + } + value +} + +fn parse_exponent(bytes: &[u8]) -> Option { + let (&first, rest) = bytes.split_first()?; + let (negative, digits) = match first { + b'-' => (true, rest), + b'+' => (false, rest), + _ => (false, bytes), + }; + if digits.is_empty() { + return None; + } + let mut value = 0i32; + for &digit in digits { + if !digit.is_ascii_digit() { + return None; + } + value = value + .checked_mul(10)? + .checked_add(i32::from(digit - b'0'))?; + } + Some(if negative { -value } else { value }) +} + +fn write_exponent(out: &mut Buffer<'_>, exponent: i32) -> bool { + if !out.push(b'e') || !out.push(if exponent < 0 { b'-' } else { b'+' }) { + return false; + } + let magnitude = exponent.unsigned_abs(); + if magnitude >= 100 { + out.push(b'0' + (magnitude / 100) as u8) + && out.push(b'0' + ((magnitude / 10) % 10) as u8) + && out.push(b'0' + (magnitude % 10) as u8) + } else { + out.push(b'0' + (magnitude / 10) as u8) && out.push(b'0' + (magnitude % 10) as u8) + } +} + +/// Normalize Rust's correctly rounded scientific representation to `%g`. +/// +/// Rust core supplies the difficult, proven binary-to-decimal conversion. This +/// adapter only moves the decimal point and spells an exponent with the C/Lua +/// sign and minimum-two-digit convention. +fn format_number(value: f64, precision: usize, dst: &mut [u8; LUA_NUMBER_BYTES]) -> Option { + let precision = precision.clamp(1, 17); + let fractional_digits = precision - 1; + let mut raw = [0u8; RAW_NUMBER_BYTES]; + // Scoped so the writer's borrow of `raw` ends before the shared read + // below, which is what the block is for — `Buffer` has no `Drop`, so a + // `drop()` call here would be a move that says nothing. + let raw_len = { + let mut raw_out = Buffer { + bytes: &mut raw, + len: 0, + }; + write!(&mut raw_out, "{value:.fractional_digits$e}").ok()?; + raw_out.len + }; + let raw = &raw[..raw_len]; + + let special: Option<&[u8]> = match raw { + b"NaN" => Some(b"nan"), + b"inf" => Some(b"inf"), + b"-inf" => Some(b"-inf"), + _ => None, + }; + if let Some(special) = special { + dst[..special.len()].copy_from_slice(special); + return Some(special.len()); + } + + let (negative, unsigned) = raw + .strip_prefix(b"-") + .map_or((false, raw), |unsigned| (true, unsigned)); + let e_at = unsigned.iter().position(|&byte| byte == b'e')?; + let exponent = parse_exponent(&unsigned[e_at + 1..])?; + let mut digits = [0u8; 17]; + let mut digit_count = 0usize; + for &byte in &unsigned[..e_at] { + if byte == b'.' { + continue; + } + if !byte.is_ascii_digit() || digit_count == digits.len() { + return None; + } + digits[digit_count] = byte; + digit_count += 1; + } + if digit_count == 0 { + return None; + } + while digit_count > 1 && digits[digit_count - 1] == b'0' { + digit_count -= 1; + } + + let scientific = exponent < -4 || exponent >= precision as i32; + let mut out = Buffer { bytes: dst, len: 0 }; + if negative && !out.push(b'-') { + return None; + } + if scientific { + if !out.push(digits[0]) + || (digit_count > 1 && (!out.push(b'.') || !out.extend(&digits[1..digit_count]))) + || !write_exponent(&mut out, exponent) + { + return None; + } + } else if exponent < 0 { + if !out.extend(b"0.") + || !out.repeat(b'0', (-exponent - 1) as usize) + || !out.extend(&digits[..digit_count]) + { + return None; + } + } else { + let integer_digits = exponent as usize + 1; + if integer_digits >= digit_count { + if !out.extend(&digits[..digit_count]) + || !out.repeat(b'0', integer_digits - digit_count) + { + return None; + } + } else if !out.extend(&digits[..integer_digits]) + || !out.push(b'.') + || !out.extend(&digits[integer_digits..digit_count]) + { + return None; + } + } + Some(out.len) +} + +/// Write Lua's core number representation and return bytes actually stored. +/// +/// This is intentionally not `snprintf`: a short destination truncates safely +/// and the returned length never exceeds `cap - 1`. Lua's core buffer is large +/// enough for every result, while the stricter contract prevents callers from +/// treating an unstored would-write length as initialized memory. +/// +/// # Safety +/// +/// When `cap != 0`, `dst` must point to `cap` writable bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn kore_lua_format_number( + dst: *mut c_char, + cap: usize, + value: c_double, + precision: usize, +) -> usize { + if dst.is_null() || cap == 0 { + return 0; + } + let mut formatted = [0u8; LUA_NUMBER_BYTES]; + let Some(len) = format_number(value, precision, &mut formatted) else { + // SAFETY: the caller supplied at least one writable byte. + unsafe { dst.write(0) }; + return 0; + }; + let stored = len.min(cap - 1); + // SAFETY: the caller supplied `cap` writable bytes and `stored < cap`. + unsafe { + ptr::copy_nonoverlapping(formatted.as_ptr(), dst.cast::(), stored); + dst.add(stored).write(0); + } + stored +} diff --git a/crates/lua/src/shim.rs b/crates/lua/src/shim.rs new file mode 100644 index 0000000..aad4cdc --- /dev/null +++ b/crates/lua/src/shim.rs @@ -0,0 +1,257 @@ +//! The deliberately small hosted-C surface retained by the selected Lua core. + +use core::{ + alloc::Layout, + ffi::{c_char, c_double, c_int, c_uchar, c_void}, + ptr, slice, +}; + +const LUA_ALIGNMENT: usize = 16; + +/// Lua's per-state allocator, backed by this task's capability-grown heap. +pub unsafe extern "C" fn allocate( + _ud: *mut c_void, + ptr: *mut c_void, + old_size: usize, + new_size: usize, +) -> *mut c_void { + if new_size == 0 { + if !ptr.is_null() + && let Ok(layout) = Layout::from_size_align(old_size, LUA_ALIGNMENT) + { + // SAFETY: every pointer reaching this branch was returned below + // with the same size/alignment, and Lua transfers it exactly once. + unsafe { alloc::alloc::dealloc(ptr.cast(), layout) }; + } + return ptr::null_mut(); + } + if ptr.is_null() { + let Ok(layout) = Layout::from_size_align(new_size, LUA_ALIGNMENT) else { + return ptr::null_mut(); + }; + // SAFETY: the global allocator is kore-rt's task-local Heap. + return unsafe { alloc::alloc::alloc(layout) }.cast(); + } + let Ok(_new_layout) = Layout::from_size_align(new_size, LUA_ALIGNMENT) else { + return ptr::null_mut(); + }; + let Ok(layout) = Layout::from_size_align(old_size, LUA_ALIGNMENT) else { + return ptr::null_mut(); + }; + // SAFETY: `ptr` and `layout` describe the live allocation supplied by the + // preceding callback; realloc preserves its contents to the shorter size. + unsafe { alloc::alloc::realloc(ptr.cast(), layout, new_size) }.cast() +} + +#[unsafe(no_mangle)] +extern "C" fn kore_lua_abort() -> ! { + kore::runtime::thread::exit(kore::runtime::ExitCode::ABORTED) +} + +macro_rules! unary_math { + ($($name:ident),* $(,)?) => {$( + #[unsafe(no_mangle)] + extern "C" fn $name(value: c_double) -> c_double { libm::$name(value) } + )*}; +} + +unary_math!( + acos, asin, ceil, cos, cosh, exp, fabs, floor, log, log10, log2, sin, sinh, sqrt, tan, tanh, + trunc +); + +#[unsafe(no_mangle)] +extern "C" fn atan2(y: c_double, x: c_double) -> c_double { + libm::atan2(y, x) +} +#[unsafe(no_mangle)] +extern "C" fn fmod(x: c_double, y: c_double) -> c_double { + libm::fmod(x, y) +} +#[unsafe(no_mangle)] +extern "C" fn pow(x: c_double, y: c_double) -> c_double { + libm::pow(x, y) +} +#[unsafe(no_mangle)] +unsafe extern "C" fn frexp(value: c_double, exponent: *mut c_int) -> c_double { + let (fraction, exp) = libm::frexp(value); + if !exponent.is_null() { + unsafe { *exponent = exp }; + } + fraction +} +#[unsafe(no_mangle)] +extern "C" fn ldexp(value: c_double, exponent: c_int) -> c_double { + libm::ldexp(value, exponent) +} + +#[unsafe(no_mangle)] +extern "C" fn abs(value: c_int) -> c_int { + value.wrapping_abs() +} + +unsafe fn c_len(mut text: *const c_char) -> usize { + let mut len = 0; + while unsafe { *text } != 0 { + len += 1; + text = unsafe { text.add(1) }; + } + len +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn strnlen(text: *const c_char, maximum: usize) -> usize { + let mut len = 0; + while len < maximum && unsafe { *text.add(len) } != 0 { + len += 1; + } + len +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn strcmp(left: *const c_char, right: *const c_char) -> c_int { + unsafe { strncmp(left, right, usize::MAX) } +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn strncmp(left: *const c_char, right: *const c_char, maximum: usize) -> c_int { + for index in 0..maximum { + let a = unsafe { *left.add(index) } as c_uchar; + let b = unsafe { *right.add(index) } as c_uchar; + if a != b { + return c_int::from(a) - c_int::from(b); + } + if a == 0 { + return 0; + } + } + 0 +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn strcpy(dst: *mut c_char, src: *const c_char) -> *mut c_char { + let len = unsafe { c_len(src) }; + unsafe { ptr::copy_nonoverlapping(src, dst, len + 1) }; + dst +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn strchr(text: *const c_char, needle: c_int) -> *mut c_char { + let needle = needle as c_char; + let mut at = text; + loop { + let byte = unsafe { *at }; + if byte == needle { + return at.cast_mut(); + } + if byte == 0 { + return ptr::null_mut(); + } + at = unsafe { at.add(1) }; + } +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn strspn(text: *const c_char, accept: *const c_char) -> usize { + let mut len = 0; + while unsafe { *text.add(len) } != 0 { + if unsafe { strchr(accept, c_int::from(*text.add(len))) }.is_null() { + break; + } + len += 1; + } + len +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn strcspn(text: *const c_char, reject: *const c_char) -> usize { + let mut len = 0; + while unsafe { *text.add(len) } != 0 { + if !unsafe { strchr(reject, c_int::from(*text.add(len))) }.is_null() { + break; + } + len += 1; + } + len +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn strpbrk(text: *const c_char, accept: *const c_char) -> *mut c_char { + let offset = unsafe { strcspn(text, accept) }; + let result = unsafe { text.add(offset) }; + if unsafe { *result } == 0 { + ptr::null_mut() + } else { + result.cast_mut() + } +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn strstr(haystack: *const c_char, needle: *const c_char) -> *mut c_char { + let needle_len = unsafe { c_len(needle) }; + if needle_len == 0 { + return haystack.cast_mut(); + } + let haystack_len = unsafe { c_len(haystack) }; + if needle_len > haystack_len { + return ptr::null_mut(); + } + for offset in 0..=haystack_len - needle_len { + if unsafe { strncmp(haystack.add(offset), needle, needle_len) } == 0 { + return unsafe { haystack.add(offset) }.cast_mut(); + } + } + ptr::null_mut() +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn memchr(memory: *const c_void, needle: c_int, len: usize) -> *mut c_void { + let bytes = unsafe { slice::from_raw_parts(memory.cast::(), len) }; + match bytes.iter().position(|byte| *byte == needle as u8) { + Some(offset) => unsafe { memory.cast::().add(offset) }.cast_mut().cast(), + None => ptr::null_mut(), + } +} + +fn ascii(value: c_int) -> Option { + u8::try_from(value).ok().filter(u8::is_ascii) +} + +macro_rules! ctype { + ($name:ident, $predicate:ident) => { + #[unsafe(no_mangle)] + extern "C" fn $name(value: c_int) -> c_int { + ascii(value).is_some_and(|byte| byte.$predicate()) as c_int + } + }; +} + +ctype!(isalnum, is_ascii_alphanumeric); +ctype!(isalpha, is_ascii_alphabetic); +ctype!(iscntrl, is_ascii_control); +ctype!(isdigit, is_ascii_digit); +ctype!(isgraph, is_ascii_graphic); +ctype!(islower, is_ascii_lowercase); +ctype!(ispunct, is_ascii_punctuation); +ctype!(isupper, is_ascii_uppercase); +ctype!(isxdigit, is_ascii_hexdigit); + +#[unsafe(no_mangle)] +extern "C" fn isspace(value: c_int) -> c_int { + ascii(value).is_some_and(crate::numeric::c_locale_whitespace) as c_int +} + +#[unsafe(no_mangle)] +extern "C" fn isprint(value: c_int) -> c_int { + ascii(value).is_some_and(|byte| byte == b' ' || byte.is_ascii_graphic()) as c_int +} + +#[unsafe(no_mangle)] +extern "C" fn tolower(value: c_int) -> c_int { + ascii(value).map_or(value, |byte| c_int::from(byte.to_ascii_lowercase())) +} + +#[unsafe(no_mangle)] +extern "C" fn toupper(value: c_int) -> c_int { + ascii(value).map_or(value, |byte| c_int::from(byte.to_ascii_uppercase())) +} diff --git a/crates/lua/src/vm.rs b/crates/lua/src/vm.rs new file mode 100644 index 0000000..b532778 --- /dev/null +++ b/crates/lua/src/vm.rs @@ -0,0 +1,344 @@ +//! Thin ownership wrapper over the pinned Lua C engine. + +use alloc::{boxed::Box, vec::Vec}; +use core::{ + ffi::{c_char, c_int, c_void}, + ptr::{self, NonNull}, + slice, +}; +use kore::abi::process::PROCESS_OUTPUT_BYTES; + +use crate::Library; +use crate::shim; + +const LUA_OK: c_int = 0; +const LUA_MULTRET: c_int = -1; + +#[repr(C)] +struct LuaState { + _private: [u8; 0], +} + +type LuaAlloc = unsafe extern "C" fn(*mut c_void, *mut c_void, usize, usize) -> *mut c_void; +type LuaAppend = unsafe extern "C" fn(*mut c_void, *const c_char, usize, c_int) -> c_int; + +unsafe extern "C" { + fn kore_lua_new( + alloc: LuaAlloc, + ud: *mut c_void, + seed: u32, + libraries: *const Library, + library_count: usize, + ) -> *mut LuaState; + fn kore_lua_install_print(state: *mut LuaState, append: LuaAppend, ud: *mut c_void) -> c_int; + fn kore_lua_load( + state: *mut LuaState, + bytes: *const c_char, + len: usize, + name: *const c_char, + ) -> c_int; + fn kore_lua_call(state: *mut LuaState, arguments: c_int, results: c_int) -> c_int; + fn kore_lua_print_results(state: *mut LuaState) -> c_int; + fn kore_lua_top(state: *mut LuaState) -> c_int; + fn kore_lua_set_top(state: *mut LuaState, top: c_int); + fn kore_lua_error_string(state: *mut LuaState, len: *mut usize) -> *const c_char; + fn kore_lua_close(state: *mut LuaState); +} + +#[derive(Default)] +struct Capture { + stdout: Vec, + stderr: Vec, +} + +unsafe extern "C" fn append( + ud: *mut c_void, + bytes: *const c_char, + len: usize, + stderr: c_int, +) -> c_int { + let capture = ud.cast::(); + if capture.is_null() || bytes.is_null() { + return 0; + } + let bytes = unsafe { slice::from_raw_parts(bytes.cast::(), len) }; + let used = unsafe { + (*capture) + .stdout + .len() + .saturating_add((*capture).stderr.len()) + }; + if bytes.len() > PROCESS_OUTPUT_BYTES.saturating_sub(used) { + return 0; + } + let output = if stderr == 0 { + unsafe { &mut (*capture).stdout } + } else { + unsafe { &mut (*capture).stderr } + }; + if output.try_reserve(bytes.len()).is_err() { + return 0; + } + output.extend_from_slice(bytes); + 1 +} + +pub enum ReplResult { + Complete(bool), + Incomplete, + ResourceRefused, +} + +pub struct Vm { + state: NonNull, + capture: Box, +} + +impl Vm { + /// Build a VM, registering `libraries` beneath the `kore` table + /// after this crate's own modules. + pub fn new(libraries: &'static [Library]) -> Option { + let mut capture = Box::try_new(Capture::default()).ok()?; + // Seed from the CPU, not from a constant: this word decides string + // hashing and table iteration order, and a fixed one made every + // boot's ordering identical. `math.random`'s own seed comes from + // the same source, through `luai_makeseed` (`kore_lua.c`). + let state = NonNull::new(unsafe { + kore_lua_new( + shim::allocate, + ptr::null_mut(), + crate::bridge::seed(), + libraries.as_ptr(), + libraries.len(), + ) + })?; + let status = unsafe { + kore_lua_install_print( + state.as_ptr(), + append, + (&mut *capture as *mut Capture).cast(), + ) + }; + if status != LUA_OK { + // Installation runs below a C recovery frame, so closing here + // cannot double-own the state or unwind through this Rust frame. + unsafe { kore_lua_close(state.as_ptr()) }; + return None; + } + Some(Self { state, capture }) + } + + fn clear_stack(&mut self) { + unsafe { kore_lua_set_top(self.state.as_ptr(), 0) }; + } + + fn load(&mut self, bytes: &[u8], name: &'static [u8]) -> c_int { + unsafe { + kore_lua_load( + self.state.as_ptr(), + bytes.as_ptr().cast(), + bytes.len(), + name.as_ptr().cast(), + ) + } + } + + fn error_parts(&self) -> (*const u8, usize) { + let mut len = 0; + // This wrapper only borrows an existing Lua string. It deliberately + // never invokes __tostring outside lua_pcall's recovery frame. + let text = unsafe { kore_lua_error_string(self.state.as_ptr(), &mut len) }; + if text.is_null() { + (b"Lua error".as_ptr(), b"Lua error".len()) + } else { + (text.cast(), len) + } + } + + fn publish_error(&mut self) { + let (error, len) = self.error_parts(); + let used = self + .capture + .stdout + .len() + .saturating_add(self.capture.stderr.len()); + let remaining = PROCESS_OUTPUT_BYTES.saturating_sub(used); + if remaining != 0 { + // Always reserve the final byte for one deterministic line + // terminator. An attacker-controlled Lua error therefore cannot + // bypass the aggregate ProcessIo output bound or trigger a large + // temporary allocation while it is being reported. + let error = unsafe { slice::from_raw_parts(error, len) }; + let body = error.strip_suffix(b"\n").unwrap_or(error); + let copied = body.len().min(remaining - 1); + if self.capture.stderr.try_reserve(copied + 1).is_ok() { + self.capture.stderr.extend_from_slice(&body[..copied]); + self.capture.stderr.push(b'\n'); + } + } + self.clear_stack(); + } + + fn execute_loaded(&mut self, print_results: bool) -> bool { + if unsafe { kore_lua_call(self.state.as_ptr(), 0, LUA_MULTRET) } != LUA_OK { + self.publish_error(); + return false; + } + if print_results + && unsafe { kore_lua_top(self.state.as_ptr()) } > 0 + && unsafe { kore_lua_print_results(self.state.as_ptr()) } != LUA_OK + { + self.publish_error(); + return false; + } + self.clear_stack(); + true + } + + pub fn eval(&mut self, bytes: &[u8], name: &'static [u8]) -> bool { + self.clear_stack(); + if self.load(bytes, name) != LUA_OK { + self.publish_error(); + false + } else { + self.execute_loaded(false) + } + } + + /// Stock Lua REPL semantics: expression first, then statement, with an + /// `` syntax error requesting a continuation line. + pub fn eval_repl(&mut self, bytes: &[u8]) -> ReplResult { + self.clear_stack(); + let Some(capacity) = bytes.len().checked_add(8) else { + return ReplResult::ResourceRefused; + }; + let mut expression = Vec::new(); + if expression.try_reserve(capacity).is_err() { + return ReplResult::ResourceRefused; + } + expression.extend_from_slice(b"return "); + expression.extend_from_slice(bytes); + if self.load(&expression, b"=stdin\0") == LUA_OK { + drop(expression); + return ReplResult::Complete(self.execute_loaded(true)); + } + self.clear_stack(); + drop(expression); + if self.load(bytes, b"=stdin\0") == LUA_OK { + return ReplResult::Complete(self.execute_loaded(true)); + } + let (error, len) = self.error_parts(); + let error = unsafe { slice::from_raw_parts(error, len) }; + if error.windows(5).any(|part| part == b"") { + self.clear_stack(); + ReplResult::Incomplete + } else { + self.publish_error(); + ReplResult::Complete(false) + } + } + + pub fn take_stdout(&mut self) -> Vec { + core::mem::take(&mut self.capture.stdout) + } + + pub fn take_stderr(&mut self) -> Vec { + core::mem::take(&mut self.capture.stderr) + } +} + +impl Drop for Vm { + fn drop(&mut self) { + unsafe { kore_lua_close(self.state.as_ptr()) }; + } +} + +/// The Lua C API an [`Extension`](crate::Extension) opener calls. +/// +/// Deliberately the subset an ordinary library binding needs — arguments +/// in, values out, a table built, a refusal raised — rather than the whole +/// engine. Every symbol here is one the image already exports as the +/// native-module ABI (`--export-dynamic-symbol=lua_*` / `luaL_*`), so an +/// embedder writes its library in Rust and compiles no C of its own. +/// +/// # Safety +/// +/// Every function takes the raw `lua_State` an opener was handed, and is +/// valid only on the VM thread inside a call from the engine. Index and +/// type discipline is the caller's, exactly as it is in C: these are thin +/// declarations, not a checked binding. +pub mod api { + use core::ffi::{CStr, c_char, c_int, c_void}; + + /// The engine's `lua_State`, opaque by design. + pub type State = c_void; + /// Lua's integer type on every Kore target. + pub type Integer = i64; + + unsafe extern "C" { + pub fn lua_settop(state: *mut State, index: c_int); + pub fn lua_gettop(state: *mut State) -> c_int; + pub fn lua_type(state: *mut State, index: c_int) -> c_int; + pub fn lua_createtable(state: *mut State, array: c_int, record: c_int); + pub fn lua_setfield(state: *mut State, index: c_int, name: *const c_char); + pub fn lua_pushnil(state: *mut State); + pub fn lua_pushboolean(state: *mut State, value: c_int); + pub fn lua_pushinteger(state: *mut State, value: Integer); + pub fn lua_pushlstring( + state: *mut State, + bytes: *const c_char, + len: usize, + ) -> *const c_char; + pub fn lua_pushcclosure(state: *mut State, function: crate::LuaOpener, upvalues: c_int); + pub fn luaL_checkinteger(state: *mut State, index: c_int) -> Integer; + pub fn luaL_checklstring(state: *mut State, index: c_int, len: *mut usize) + -> *const c_char; + pub fn luaL_argerror(state: *mut State, index: c_int, message: *const c_char) -> c_int; + } + + /// Push a string. + /// + /// # Safety + /// `state` is a live Lua state on this thread. + pub unsafe fn push_bytes(state: *mut State, bytes: &[u8]) { + unsafe { lua_pushlstring(state, bytes.as_ptr().cast::(), bytes.len()) }; + } + + /// Set `table[name]` from the value on top, `table` being at `index`. + /// + /// # Safety + /// `state` is live; `index` names a table. + pub unsafe fn set_field(state: *mut State, index: c_int, name: &CStr) { + unsafe { lua_setfield(state, index, name.as_ptr()) }; + } + + /// Build a module table from `(name, function)` pairs — the Rust + /// spelling of `luaL_newlib`. Leaves exactly one value on the stack, + /// which is what an opener must return. + /// + /// # Safety + /// `state` is a live Lua state on this thread. + pub unsafe fn new_lib(state: *mut State, entries: &[(&CStr, crate::LuaOpener)]) { + unsafe { + lua_createtable(state, 0, entries.len() as c_int); + for (name, function) in entries { + lua_pushcclosure(state, *function, 0); + lua_setfield(state, -2, name.as_ptr()); + } + } + } + + /// Read a string argument. + /// + /// # Safety + /// `state` is live and inside a call. Raises if the argument is not a + /// string, unwinding through the engine's own protected frame. + pub unsafe fn check_bytes<'a>(state: *mut State, index: c_int) -> &'a [u8] { + let mut len = 0usize; + let bytes = unsafe { luaL_checklstring(state, index, &raw mut len) }; + if bytes.is_null() { + return &[]; + } + unsafe { core::slice::from_raw_parts(bytes.cast::(), len) } + } +} diff --git a/crates/lua/tests/host_smoke.c b/crates/lua/tests/host_smoke.c new file mode 100644 index 0000000..7e76fe0 --- /dev/null +++ b/crates/lua/tests/host_smoke.c @@ -0,0 +1,323 @@ +/* Host-only smoke test for the pinned C engine; the metal ABI is Rust-owned. */ +#include +#include +#include +#include + +#include "lua.h" +#include "lauxlib.h" + +/* The engine's real arity. This prototype MUST match src/kore_lua.c: a + short one leaves the library array and its count as whatever the call's + argument registers happened to hold, which `luaopen_kore` then walks. */ +typedef struct { const char *name; lua_CFunction opener; } kore_lua_library; +extern lua_State *kore_lua_new(lua_Alloc, void *, unsigned, + const kore_lua_library *, size_t); +typedef int (*kore_lua_append)(void *, const char *, size_t, int); +extern int kore_lua_install_print(lua_State *, kore_lua_append, void *); +extern int kore_lua_load(lua_State *, const char *, size_t, const char *); +extern int kore_lua_call(lua_State *, int, int); +extern int kore_lua_print_results(lua_State *); +extern void kore_lua_set_top(lua_State *, int); +extern void kore_lua_close(lua_State *); + +#if !defined(KORE_LUA_USE_TARGET_NUMERIC) +double kore_lua_strtod(const char *text, char **end) { return strtod(text, end); } + +size_t kore_lua_format_number(char *dst, size_t cap, double value, + size_t precision) { + /* Match the target shim's canonical spelling even when the host FPU/libc + preserves a negative NaN sign (as x86_64 glibc does for 0.0 / 0.0). */ + if (isnan(value)) value = fabs(value); + int written = snprintf(dst, cap, "%.*g", (int)precision, value); + if (written < 0 || cap == 0) return 0; + return (size_t)written < cap ? (size_t)written : cap - 1; +} +#endif + +void kore_lua_abort(void) { abort(); } + +static void *host_alloc(void *ud, void *ptr, size_t old_size, size_t new_size) { + (void)ud; (void)old_size; + if (new_size == 0) { free(ptr); return NULL; } + return realloc(ptr, new_size); +} + +struct fault_alloc { + size_t calls; + size_t refuse_at; +}; + +static void *fault_allocator(void *ud, void *ptr, size_t old_size, + size_t new_size) { + struct fault_alloc *fault = ud; + (void)old_size; + if (new_size == 0) { free(ptr); return NULL; } + fault->calls++; + if (fault->refuse_at != 0 && fault->calls >= fault->refuse_at) + return NULL; + return realloc(ptr, new_size); +} + +struct capture { char bytes[128]; size_t len; }; + +static int capture_append(void *ud, const char *bytes, size_t len, int err) { + struct capture *capture = ud; + (void)err; + if (len > sizeof(capture->bytes) - capture->len) return 0; + memcpy(capture->bytes + capture->len, bytes, len); + capture->len += len; + return 1; +} + +static int run(lua_State *L, const char *source, int print_results) { + if (kore_lua_load(L, source, strlen(source), "=host-smoke") != LUA_OK) + return 0; + if (kore_lua_call(L, 0, LUA_MULTRET) != LUA_OK) + return 0; + return !print_results || kore_lua_print_results(L) == LUA_OK; +} + +static int protected_setup_smoke(void) { + struct fault_alloc baseline = {0, 0}; + lua_State *L = kore_lua_new(fault_allocator, &baseline, 0x4b4f5245u, NULL, 0); + if (L == NULL) return 1; + size_t constructor_calls = baseline.calls; + kore_lua_close(L); + + /* The final constructor allocation occurs inside library opening. */ + struct fault_alloc open_oom = {0, constructor_calls}; + L = kore_lua_new(fault_allocator, &open_oom, 0x4b4f5245u, NULL, 0); + if (L != NULL) { kore_lua_close(L); return 2; } + if (open_oom.calls < open_oom.refuse_at) return 3; + + struct fault_alloc install_oom = {0, 0}; + L = kore_lua_new(fault_allocator, &install_oom, 0x4b4f5245u, NULL, 0); + if (L == NULL) return 4; + install_oom.refuse_at = install_oom.calls + 1; + struct capture capture = {{0}, 0}; + int status = kore_lua_install_print(L, capture_append, &capture); + if (status == LUA_OK) { kore_lua_close(L); return 5; } + if (install_oom.calls < install_oom.refuse_at) { + kore_lua_close(L); return 6; + } + kore_lua_close(L); + + struct fault_alloc lookup_oom = {0, 0}; + L = kore_lua_new(fault_allocator, &lookup_oom, 0x4b4f5245u, NULL, 0); + if (L == NULL) return 7; + struct capture lookup_capture = {{0}, 0}; + if (kore_lua_install_print(L, capture_append, &lookup_capture) != LUA_OK) { + kore_lua_close(L); return 8; + } + if (!run(L, + "print=nil; setmetatable(_G,{__index=function() return {} end}); " + "return 6*7", + 0)) { + kore_lua_close(L); return 9; + } + lookup_oom.refuse_at = lookup_oom.calls + 1; + if (kore_lua_print_results(L) == LUA_OK) { + kore_lua_close(L); return 10; + } + if (lookup_oom.calls < lookup_oom.refuse_at) { + kore_lua_close(L); return 11; + } + kore_lua_close(L); + return 0; +} + +#if defined(KORE_LUA_USE_TARGET_NUMERIC) +int kore_lua_host_smoke(void) { +#else +int main(void) { +#endif + static const char selftest[] = + "local co=coroutine.create(function() coroutine.yield(6) end)\n" + "local ok,value=coroutine.resume(co)\n" + "assert(ok and value==6)\n" + "assert(string.upper('lua')=='LUA')\n" + "assert(table.concat({4,2})=='42')\n" + "assert(math.floor(42.9)==42)\n" + "assert(tostring(42.5)=='42.5')\n" + "assert(string.format==nil)\n" + "assert(string.dump==nil)\n" + "assert(load==nil and loadfile==nil and dofile==nil)\n" + "assert(warn==nil)\n" + "assert(tostring(1e100)=='1e+100')\n" + "assert(tostring(1e308)=='1e+308')\n" + "assert(tostring(5e-324)=='4.94065645841247e-324')\n" + "assert(tostring(-0.0)=='-0.0')\n" + "assert(tostring(1e15)=='1e+15')\n" + "assert(tostring(1234567890123456.0)=='1234567890123456.0')\n" + "assert(tostring(math.huge)=='inf')\n" + "assert(tostring(-math.huge)=='-inf')\n" + "assert(tostring(0.0/0.0)=='nan')\n" + "assert(utf8.len('\\u{3bb}')==1)\n" + "local bad=setmetatable({},{__tostring=function() error('bad') end})\n" + "assert(not pcall(print,bad))\n" + "assert(not pcall(print,string.rep('x',4000)))\n" + /* The standard library registers, and the two surfaces are one + implementation: io.open and kore.fs.open are the same function. */ + "assert(type(io)=='table' and type(os)=='table')\n" + "assert(type(kore.fs)=='table')\n" + "assert(io.type(io.stdout)=='file')\n" + /* "The I/O library never closes these files" (manual 6.8): the close + is refused, and the stream is still usable afterwards. */ + "local ok,why=io.stdout:close()\n" + "assert(ok==nil and why=='cannot close standard file')\n" + "assert(io.type(io.stdout)=='file')\n" + /* Every member the manual lists is either present or absent for a + stated reason; none is present-but-partial. */ + "for _,name in ipairs{'close','flush','input','lines','open','output',\n" + " 'read','tmpfile','type','write'} do\n" + " assert(type(io[name])=='function', 'io.'..name)\n" + "end\n" + "for _,name in ipairs{'clock','date','difftime','exit','getenv',\n" + " 'remove','rename','setlocale','time',\n" + " 'tmpname'} do\n" + " assert(type(os[name])=='function', 'os.'..name)\n" + "end\n" + "local h=io.open('/nowhere','r')\n" + "assert(h==nil)\n" + "for _,name in ipairs{'close','flush','lines','read','seek',\n" + " 'setvbuf','write'} do\n" + " assert(type(io.stdout[name])=='function', 'file:'..name)\n" + "end\n" + /* luaL_fileresult's exact shapes: a standard close answers two values + and no errno; flush answers true; write answers the file so writes + chain. Writes here are empty so the capture assertions below still + see only what print produced. */ + "assert(select('#', io.stdout:close())==2)\n" + "local ok,why,err=io.stdout:close()\n" + "assert(ok==nil and why=='cannot close standard file' and err==nil)\n" + "assert(io.stdout:flush()==true and io.flush()==true)\n" + "assert(io.stdout:write('')==io.stdout)\n" + "assert(io.write('')==io.stdout)\n" + "assert(not pcall(io.write,{}) and not pcall(io.write,true))\n" + "assert(io.stdout:setvbuf('full')==true)\n" + "assert(not pcall(io.stdout.setvbuf,io.stdout,'bogus'))\n" + "assert(io.input()==io.stdin and io.output()==io.stdout)\n" + "assert(io.output(io.stderr)==io.stderr)\n" + "io.output(io.stdout)\n" + /* io.lines() over the default input answers one value and leaves it + open; io.lines(name) answers four, the last to-be-closed. */ + "assert(select('#', io.lines())==1)\n" + "for _ in io.lines() do error('stdin is at end of file') end\n" + "assert(io.type(io.stdin)=='file')\n" + /* Every read format at end of file, including the zero-length probe + and the 5.2 star spelling. */ + "assert(io.stdin:read(0)==nil and io.stdin:read('a')=='')\n" + "assert(io.stdin:read('l')==nil and io.stdin:read('L')==nil)\n" + "assert(io.stdin:read('n')==nil and io.stdin:read(5)==nil)\n" + "assert(io.stdin:read('*l')==nil)\n" + "assert(not pcall(io.stdin.read,io.stdin,'z'))\n" + "assert(select(2, io.stdout:seek())~=nil)\n" + /* os.time normalizes its table in place and fills the derived + fields, as mktime/setallfields do. */ + "local t={year=2023,month=11,day=14,hour=22,min=13,sec=20}\n" + "assert(os.time(t)==1700000000)\n" + "assert(t.wday==3 and t.yday==318 and t.isdst==false)\n" + "local roll={year=2023,month=13,day=1,hour=0}\n" + "assert(os.time(roll)==os.time{year=2024,month=1,day=1,hour=0})\n" + "assert(roll.year==2024 and roll.month==1)\n" + "local named=select(2, pcall(os.time,{month=1,day=1}))\n" + "assert(named:find(\"field 'year' missing\"), named)\n" + "for _,x in ipairs{0,1,951782400,1700000000,-86400} do\n" + " assert(os.time(os.date('*t',x))==x, x)\n" + "end\n" + /* io.input/io.output/io.lines raise rather than answer a code. */ + "local raised=select(2, pcall(io.lines,'/nowhere'))\n" + "assert(type(raised)=='string' and raised:find('cannot open file'),\n" + " tostring(raised))\n" + "assert(not pcall(io.input,'/nowhere'))\n" + "assert(not pcall(io.output,'/nowhere'))\n" + "assert(io.type(42)==nil and io.type('x')==nil)\n" + "assert(io.stdin:read('a')=='')\n" + "assert(io.stdin:read('l')==nil)\n" + /* The calendar is pure computation, so a host build checks it in + full. These are the dates every off-by-one shows up in: the + epoch, a leap day, a century that is not a leap year, one that + is, and a pre-epoch instant whose seconds must floor rather than + truncate toward zero. */ + "assert(os.date('%Y-%m-%d',0)=='1970-01-01')\n" + "assert(os.date('%H:%M:%S',0)=='00:00:00')\n" + "assert(os.date('%A',0)=='Thursday')\n" + "assert(os.date('%Y-%m-%d',951782400)=='2000-02-29')\n" + "assert(os.date('%Y-%m-%d',68169600)=='1972-02-29')\n" + "assert(os.date('%Y-%m-%d',4107542400)=='2100-03-01')\n" + "assert(os.date('%Y-%m-%d %H:%M:%S',-1)=='1969-12-31 23:59:59')\n" + "assert(os.date('%Y-%m-%d',-86400)=='1969-12-31')\n" + "assert(os.date('%j',0)=='001')\n" + "assert(os.date('%F',1700000000)=='2023-11-14')\n" + "assert(os.date('%T',1700000000)=='22:13:20')\n" + "assert(os.date('%p',0)=='AM' and os.date('%p',43200)=='PM')\n" + "assert(os.date('%I',0)=='12' and os.date('%I',43200)=='12')\n" + "assert(os.date('%%',0)=='%')\n" + "assert(not pcall(os.date,'%Q',0))\n" + /* `*t` and `os.time` are exact inverses, which is what makes date + arithmetic by field addition work. */ + "local t=os.date('*t',1700000000)\n" + "assert(t.year==2023 and t.month==11 and t.day==14)\n" + "assert(t.hour==22 and t.min==13 and t.sec==20)\n" + "assert(t.wday==3 and t.yday==318 and t.isdst==false)\n" + "assert(os.time(t)==1700000000)\n" + "assert(os.time{year=1970,month=1,day=1,hour=0}==0)\n" + /* An out-of-range field normalizes, as mktime does. */ + "assert(os.time{year=2023,month=13,day=1,hour=0}==\n" + " os.time{year=2024,month=1,day=1,hour=0})\n" + "assert(os.time{year=2024,month=0,day=1,hour=0}==\n" + " os.time{year=2023,month=12,day=1,hour=0})\n" + "assert(os.difftime(10,4)==6.0)\n" + /* A kore task has no environment, so every name is unset. */ + "assert(os.getenv('PATH')==nil)\n" + "assert(os.setlocale()=='C' and os.setlocale('fr_FR')==nil)\n" + /* Absent, and absent for a stated reason: both need to launch a + process, which is the embedding program's face, not this crate's. */ + "assert(os.execute==nil and io.popen==nil)\n" + /* With no host linked, a store verb answers its reason rather than + pretending; the failure carries an errno on the io surface and a + code on the kore one. */ + "local f,why,err=io.open('/nowhere','r')\n" + "assert(f==nil and type(why)=='string' and math.type(err)=='integer')\n" + "local g,e=kore.fs.open('/nowhere','r')\n" + "assert(g==nil and type(e)=='table' and type(e.code)=='string')\n" + "assert(tostring(e)==e.code)\n" + "print(6*7)\n"; + int setup_status = protected_setup_smoke(); + if (setup_status != 0) return 10 + setup_status; + struct capture capture = {{0}, 0}; + lua_State *L = kore_lua_new(host_alloc, NULL, 0x4b4f5245u, NULL, 0); + if (L == NULL) return 1; + if (kore_lua_install_print(L, capture_append, &capture) != LUA_OK) { + kore_lua_close(L); return 2; + } + if (!run(L, selftest, 0)) return 2; + if (capture.len != 3 || memcmp(capture.bytes, "42\n", 3) != 0) return 3; + + /* Saved-script regression: a local closure adds mixed integer/float + * arguments. This runs with both libc and the target numeric shim. */ + static const char saved_script[] = + "print(\"hello\")\n" + "local function add(x, y)\n return x + y\nend\n" + "local x = 100\nlocal y = 9.9\nprint(add(x, y))\n"; + if (!run(L, saved_script, 0)) return 9; + if (capture.len != 15 || memcmp(capture.bytes + 3, "hello\n109.9\n", 12) != 0) return 10; + capture.len = 3; + + /* Protected errors must return through the selected setjmp seam. */ + if (kore_lua_load(L, "error('expected')", 17, "=error") != LUA_OK) return 4; + if (kore_lua_call(L, 0, LUA_MULTRET) == LUA_OK) return 5; + kore_lua_set_top(L, 0); + if (!run(L, "return 6*7", 1)) return 6; + if (capture.len != 6 || memcmp(capture.bytes + 3, "42\n", 3) != 0) return 7; + kore_lua_set_top(L, 0); + if (run(L, + "print=nil; setmetatable(_G,{__index=function(_,key) " + "if key=='print' then error('hostile print lookup') end end}); " + "return 6*7", + 1)) + return 8; + kore_lua_close(L); + return 0; +} diff --git a/crates/lua/tests/run_setjmp_targets.py b/crates/lua/tests/run_setjmp_targets.py new file mode 100644 index 0000000..8393247 --- /dev/null +++ b/crates/lua/tests/run_setjmp_targets.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Boot the vendored AArch64 and RV64 setjmp seams under full-system QEMU.""" + +from __future__ import annotations + +import argparse +import os +from pathlib import Path +import shlex +import shutil +import subprocess +import tempfile + + +SUPPORTED_ZIG_LINE = (0, 16) +SUCCESS = "setjmp-runtime: ok" + + +def zig_release_line(version: str) -> tuple[int, int] | None: + release = version.split("-", 1)[0].split("+", 1)[0] + parts = release.split(".") + if len(parts) != 3: + return None + try: + major, minor, _patch = (int(part) for part in parts) + except ValueError: + return None + return major, minor + + +def resolve_tool(value: str) -> str: + resolved = shutil.which(value) + if resolved is None: + raise SystemExit(f"required tool not found: {value}") + return resolved + + +def execute( + command: list[str], *, environment: dict[str, str] | None = None +) -> str: + print("+", shlex.join(command), flush=True) + try: + result = subprocess.run( + command, + env=environment, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + timeout=15, + check=False, + ) + except subprocess.TimeoutExpired as error: + output = error.stdout or "" + if isinstance(output, bytes): + output = output.decode(errors="replace") + print(output, end="") + raise SystemExit(f"command timed out: {shlex.join(command)}") from error + print(result.stdout, end="") + if result.returncode != 0: + raise SystemExit( + f"command exited with status {result.returncode}: " + f"{shlex.join(command)}" + ) + return result.stdout + + +def compile_probe( + *, + zig: str, + lua_dir: Path, + output_dir: Path, + architecture: str, +) -> Path: + probe_dir = lua_dir / "tests" / "setjmp-runtime" + if architecture == "aarch64": + target = "aarch64-linux-musl" + start = probe_dir / "aarch64-start.s" + linker = probe_dir / "aarch64.ld" + assembly_dir = lua_dir / "vendor" / "musl-setjmp" / "aarch64" + assembly = [assembly_dir / "setjmp.s", assembly_dir / "longjmp.s"] + target_flags: list[str] = [] + else: + target = "riscv64-linux-musl" + start = probe_dir / "riscv64-start.S" + linker = probe_dir / "riscv64.ld" + assembly_dir = lua_dir / "vendor" / "musl-setjmp" / "riscv64" + assembly = [assembly_dir / "setjmp.S", assembly_dir / "longjmp.S"] + target_flags = ["-mabi=lp64d", "-mcmodel=medany"] + + output = output_dir / f"setjmp-{architecture}.elf" + harness = output_dir / f"setjmp-harness-{architecture}.o" + environment = os.environ.copy() + environment["ZIG_LOCAL_CACHE_DIR"] = str(output_dir / f"zig-cache-{architecture}") + environment["ZIG_GLOBAL_CACHE_DIR"] = str( + output_dir / f"zig-global-cache-{architecture}" + ) + common = [ + zig, + "cc", + "-target", + target, + *target_flags, + "-fPIC", + ] + compile_harness = [ + *common, + "-O2", + "-ffreestanding", + "-fno-stack-protector", + "-fno-builtin", + "-fno-unwind-tables", + "-fno-asynchronous-unwind-tables", + "-Wall", + "-Wextra", + "-Werror", + "-Wno-unused-command-line-argument", + "-c", + str(probe_dir / "harness.c"), + "-o", + str(harness), + ] + execute(compile_harness, environment=environment) + + link_probe = [ + *common, + "-nostdlib", + "-static", + "-Wno-unused-command-line-argument", + "-T", + str(linker), + str(start), + str(harness), + *(str(path) for path in assembly), + "-o", + str(output), + ] + execute(link_probe, environment=environment) + return output + + +def boot_aarch64(qemu: str, image: Path) -> None: + output = execute( + [ + qemu, + "-M", + "virt", + "-cpu", + "max", + "-accel", + "tcg", + "-m", + "128M", + "-display", + "none", + "-monitor", + "none", + "-serial", + "stdio", + "-semihosting-config", + "enable=on,target=native", + "-device", + f"loader,file={image},cpu-num=0", + ] + ) + if SUCCESS not in output: + raise SystemExit("AArch64 probe exited without its success marker") + + +def boot_riscv64(qemu: str, image: Path) -> None: + output = execute( + [ + qemu, + "-M", + "virt", + "-cpu", + "rv64", + "-accel", + "tcg", + "-m", + "128M", + "-display", + "none", + "-monitor", + "none", + "-serial", + "stdio", + "-bios", + "none", + "-device", + f"loader,file={image},cpu-num=0", + ] + ) + if SUCCESS not in output: + raise SystemExit("RV64 probe exited without its success marker") + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--zig", default=os.environ.get("KORE_ZIG", "zig")) + parser.add_argument("--qemu-aarch64", default="qemu-system-aarch64") + parser.add_argument("--qemu-riscv64", default="qemu-system-riscv64") + arguments = parser.parse_args() + + zig = resolve_tool(arguments.zig) + qemu_aarch64 = resolve_tool(arguments.qemu_aarch64) + qemu_riscv64 = resolve_tool(arguments.qemu_riscv64) + version = execute([zig, "version"]).strip() + if zig_release_line(version) != SUPPORTED_ZIG_LINE: + raise SystemExit( + f"expected Zig {SUPPORTED_ZIG_LINE[0]}.{SUPPORTED_ZIG_LINE[1]}.x, " + f"found {version or ''}" + ) + + lua_dir = Path(__file__).resolve().parent.parent + with tempfile.TemporaryDirectory(prefix="kore-lua-setjmp-") as temporary: + output_dir = Path(temporary) + aarch64 = compile_probe( + zig=zig, + lua_dir=lua_dir, + output_dir=output_dir, + architecture="aarch64", + ) + riscv64 = compile_probe( + zig=zig, + lua_dir=lua_dir, + output_dir=output_dir, + architecture="riscv64", + ) + boot_aarch64(qemu_aarch64, aarch64) + boot_riscv64(qemu_riscv64, riscv64) + + print("Lua target setjmp runtime probes: ok") + + +if __name__ == "__main__": + main() diff --git a/crates/lua/tests/setjmp-runtime/aarch64-start.s b/crates/lua/tests/setjmp-runtime/aarch64-start.s new file mode 100644 index 0000000..888473c --- /dev/null +++ b/crates/lua/tests/setjmp-runtime/aarch64-start.s @@ -0,0 +1,22 @@ +.section .text.start,"ax",%progbits +.global _start +.type _start,%function +_start: + /* QEMU's generic loader enters at EL1 with FP/SIMD disabled. */ + mrs x1, cpacr_el1 + mov x2, #0x300000 + orr x1, x1, x2 + msr cpacr_el1, x1 + isb + ldr x0, =__stack_top + mov sp, x0 + bl harness_start +1: + wfe + b 1b + +.section .bss.stack,"aw",%nobits +.balign 16 +__stack_bottom: + .space 16384 +__stack_top: diff --git a/crates/lua/tests/setjmp-runtime/aarch64.ld b/crates/lua/tests/setjmp-runtime/aarch64.ld new file mode 100644 index 0000000..372c636 --- /dev/null +++ b/crates/lua/tests/setjmp-runtime/aarch64.ld @@ -0,0 +1,10 @@ +ENTRY(_start) +SECTIONS { + /* Outside the virt DTB reservation, inside its first RAM bank. */ + . = 0x41000000; + .text : { KEEP(*(.text.start)) *(.text .text.*) } + .rodata : ALIGN(16) { *(.rodata .rodata.*) } + .data : ALIGN(16) { *(.data .data.*) *(.sdata .sdata.*) *(.got .got.*) } + .bss : ALIGN(16) { *(.bss .bss.*) *(.sbss .sbss.*) COMMON } + /DISCARD/ : { *(.comment) *(.note*) *(.eh_frame*) } +} diff --git a/crates/lua/tests/setjmp-runtime/harness.c b/crates/lua/tests/setjmp-runtime/harness.c new file mode 100644 index 0000000..780482c --- /dev/null +++ b/crates/lua/tests/setjmp-runtime/harness.c @@ -0,0 +1,92 @@ +/* Freestanding runtime probe for the exact musl setjmp assembly linked by Lua. */ +#include + +typedef unsigned long usize; + +static jmp_buf environment; +static jmp_buf zero_environment; +static volatile unsigned phase; + +#if defined(__aarch64__) + +static void put_byte(unsigned char byte) { + volatile unsigned *const uart_data = (volatile unsigned *)0x09000000ul; + volatile unsigned *const uart_flags = (volatile unsigned *)0x09000018ul; + while ((*uart_flags & (1u << 5)) != 0) {} + *uart_data = byte; +} + +__attribute__((noreturn)) static void finish(int status) { + usize reason[2] = {0x20026ul, (usize)status}; + register usize operation __asm__("x0") = 0x20; + register usize argument __asm__("x1") = (usize)reason; + __asm__ volatile("hlt #0xf000" + : "+r"(operation) + : "r"(argument) + : "memory"); + for (;;) __asm__ volatile("wfe"); +} + +#elif defined(__riscv) + +static void put_byte(unsigned char byte) { + volatile unsigned char *const uart_data = + (volatile unsigned char *)0x10000000ul; + volatile unsigned char *const uart_status = + (volatile unsigned char *)0x10000005ul; + while ((*uart_status & (1u << 5)) == 0) {} + *uart_data = byte; +} + +__attribute__((noreturn)) static void finish(int status) { + volatile unsigned *const test = (volatile unsigned *)0x100000ul; + *test = status == 0 ? 0x5555u : 0x3333u; + for (;;) __asm__ volatile("wfi"); +} + +#else +#error unsupported target +#endif + +static void write_text(const char *text) { + while (*text != 0) put_byte((unsigned char)*text++); +} + +__attribute__((noinline, noreturn)) static void jump_nonzero(void) { + volatile usize scratch[64]; + for (usize index = 0; index < 64; ++index) + scratch[index] = index ^ 0x5a5aul; + longjmp(environment, 42); +} + +__attribute__((noinline, noreturn)) static void jump_zero(void) { + volatile usize scratch[64]; + for (usize index = 0; index < 64; ++index) + scratch[index] = index ^ 0xa5a5ul; + longjmp(zero_environment, 0); +} + +__attribute__((noreturn)) void harness_start(void) { + int value = setjmp(environment); + if (value == 0) { + phase = 1; + jump_nonzero(); + } + if (value != 42 || phase != 1) { + write_text("setjmp-runtime: nonzero failed\n"); + finish(1); + } + + value = setjmp(zero_environment); + if (value == 0) { + phase = 2; + jump_zero(); + } + if (value != 1 || phase != 2) { + write_text("setjmp-runtime: zero failed\n"); + finish(1); + } + + write_text("setjmp-runtime: ok\n"); + finish(0); +} diff --git a/crates/lua/tests/setjmp-runtime/riscv64-start.S b/crates/lua/tests/setjmp-runtime/riscv64-start.S new file mode 100644 index 0000000..2092feb --- /dev/null +++ b/crates/lua/tests/setjmp-runtime/riscv64-start.S @@ -0,0 +1,20 @@ +.section .text.start,"ax",@progbits +.global _start +.type _start,@function +_start: + /* The generic loader enters in M-mode with floating-point state off. */ + csrr t0, mstatus + li t1, 0x6000 + or t0, t0, t1 + csrw mstatus, t0 + la sp, __stack_top + call harness_start +1: + wfi + j 1b + +.section .bss.stack,"aw",@nobits +.balign 16 +__stack_bottom: + .space 16384 +__stack_top: diff --git a/crates/lua/tests/setjmp-runtime/riscv64.ld b/crates/lua/tests/setjmp-runtime/riscv64.ld new file mode 100644 index 0000000..3bdbcc0 --- /dev/null +++ b/crates/lua/tests/setjmp-runtime/riscv64.ld @@ -0,0 +1,10 @@ +ENTRY(_start) +SECTIONS { + /* QEMU virt DRAM starts at 0x80000000; leave its reset payload clear. */ + . = 0x80200000; + .text : { KEEP(*(.text.start)) *(.text .text.*) } + .rodata : ALIGN(16) { *(.rodata .rodata.*) } + .data : ALIGN(16) { *(.data .data.*) *(.sdata .sdata.*) *(.got .got.*) } + .bss : ALIGN(16) { *(.bss .bss.*) *(.sbss .sbss.*) COMMON } + /DISCARD/ : { *(.comment) *(.note*) *(.eh_frame*) } +} diff --git a/crates/lua/tests/target_shim.rs b/crates/lua/tests/target_shim.rs new file mode 100644 index 0000000..544da41 --- /dev/null +++ b/crates/lua/tests/target_shim.rs @@ -0,0 +1,243 @@ +//! Host harness for the exact freestanding numeric shim used by the target. + +#[path = "../src/numeric.rs"] +mod numeric; + +use core::ffi::{c_char, c_double, c_int}; +use std::{ffi::CStr, ptr}; + +unsafe extern "C" { + fn snprintf(dst: *mut c_char, cap: usize, format: *const c_char, ...) -> c_int; + #[cfg(kore_lua_c_engine)] + fn kore_lua_host_smoke() -> c_int; +} + +fn target_format(value: f64, precision: usize) -> String { + let mut bytes = [0xa5u8; 80]; + // SAFETY: `bytes` is a writable destination of the supplied capacity. + let len = unsafe { + numeric::kore_lua_format_number(bytes.as_mut_ptr().cast(), 64, value as c_double, precision) + }; + assert!(len < 64, "formatter returned an unstored length {len}"); + assert_eq!(bytes[len], 0, "formatter omitted its terminator"); + assert!(bytes[64..].iter().all(|&byte| byte == 0xa5)); + std::str::from_utf8(&bytes[..len]).unwrap().to_owned() +} + +fn libc_format(value: f64, precision: usize) -> String { + let mut bytes = [0 as c_char; 64]; + // SAFETY: the format has the matching promoted `int`, `double` arguments + // and `bytes` is a writable C destination. + let len = unsafe { + snprintf( + bytes.as_mut_ptr(), + bytes.len(), + b"%.*g\0".as_ptr().cast(), + precision as c_int, + value as c_double, + ) + }; + assert!(len >= 0 && (len as usize) < bytes.len()); + // SAFETY: successful snprintf always terminates this sufficiently large + // destination. + unsafe { CStr::from_ptr(bytes.as_ptr()) } + .to_str() + .unwrap() + .to_owned() +} + +fn target_parse(text: &str) -> (f64, usize) { + let mut bytes = text.as_bytes().to_vec(); + bytes.push(0); + let mut end = ptr::null_mut(); + // SAFETY: `bytes` is NUL-terminated and `end` is writable. + let value = unsafe { numeric::kore_lua_strtod(bytes.as_ptr().cast(), &mut end) as f64 }; + let offset = unsafe { end.offset_from(bytes.as_ptr().cast_mut().cast::()) }; + assert!(offset >= 0); + (value, offset as usize) +} + +fn same_lua_number(left: f64, right: f64) -> bool { + left == right || (left.is_nan() && right.is_nan()) +} + +fn assert_format_matches_libc(value: f64) { + for precision in [15, 17] { + let target = target_format(value, precision); + let libc = libc_format(value, precision); + assert_eq!( + target, + libc, + "format mismatch for bits={:016x}, precision={precision}", + value.to_bits() + ); + } + + let first = target_format(value, 15); + let (check, consumed) = target_parse(&first); + if value.is_finite() { + assert_eq!(consumed, first.len()); + } + let target_two_pass = if check != value { + target_format(value, 17) + } else { + first + }; + + let libc_first = libc_format(value, 15); + let (check, consumed) = target_parse(&libc_first); + if value.is_finite() { + assert_eq!(consumed, libc_first.len()); + } + let libc_two_pass = if check != value { + libc_format(value, 17) + } else { + libc_first + }; + assert_eq!( + target_two_pass, + libc_two_pass, + "two-pass mismatch for bits={:016x}", + value.to_bits() + ); +} + +fn main() { + let corpus = [ + 0.0, + -0.0, + 1.0, + -1.0, + 42.5, + 1e-4, + 1e-5, + 1e14, + 1e15, + 1e16, + 1e100, + 1e308, + f64::from_bits(1), + f64::from_bits(2), + f64::MIN_POSITIVE, + f64::MAX, + f64::INFINITY, + f64::NEG_INFINITY, + f64::from_bits(0x3fefffffffffffff), + f64::from_bits(0x3ff0000000000001), + 999_999_999_999_999.9, + 1_234_567_890_123_456.0, + 12_345_678_901_234_568.0, + f64::from_bits(0xc366ae2c20764753), + f64::from_bits(0xce6336ec93771f1d), + ]; + for value in corpus { + assert_format_matches_libc(value); + } + + assert_eq!(target_format(f64::NAN, 15), "nan"); + assert_eq!(target_format(f64::INFINITY, 15), "inf"); + assert_eq!(target_format(f64::NEG_INFINITY, 15), "-inf"); + assert_eq!(target_format(-0.0, 15), "-0"); + assert_eq!(target_format(1e-5, 15), "1e-05"); + assert_eq!(target_format(1e15, 15), "1e+15"); + assert_eq!(target_format(1e100, 15), "1e+100"); + assert_eq!(target_format(1e308, 15), "1e+308"); + assert_eq!( + target_format(f64::from_bits(1), 15), + "4.94065645841247e-324" + ); + let transition = 1_234_567_890_123_456.0; + let first = target_format(transition, 15); + assert_eq!(first, "1.23456789012346e+15"); + assert_ne!(target_parse(&first).0, transition); + assert_eq!(target_format(transition, 17), "1234567890123456"); + + /* A fixed-seed corpus catches rounding and notation threshold drift. */ + let mut state = 0x243f_6a88_85a3_08d3u64; + for _ in 0..100_000 { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + let value = f64::from_bits(state); + if !value.is_nan() { + assert_format_matches_libc(value); + } + } + + /* Precision 17 must round-trip every finite binary64 through our parser. */ + state = 0x1319_8a2e_0370_7344; + for _ in 0..100_000 { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + let value = f64::from_bits(state); + if value.is_finite() { + let text = target_format(value, 17); + let (parsed, consumed) = target_parse(&text); + assert_eq!(consumed, text.len()); + assert!( + same_lua_number(parsed, value), + "round-trip mismatch: bits={:016x}, text={text}, parsed={:016x}", + value.to_bits(), + parsed.to_bits() + ); + } + } + + /* The FFI length is always initialized length, even under truncation. */ + let mut short = [0xa5u8; 8]; + // SAFETY: each nonzero capacity describes a prefix of `short`. + unsafe { + assert_eq!( + numeric::kore_lua_format_number(ptr::null_mut(), 0, 1e100, 15), + 0 + ); + assert_eq!( + numeric::kore_lua_format_number(short.as_mut_ptr().cast(), 0, 1e100, 15), + 0 + ); + assert!(short.iter().all(|&byte| byte == 0xa5)); + assert_eq!( + numeric::kore_lua_format_number(short.as_mut_ptr().cast(), 1, 1e100, 15), + 0 + ); + assert_eq!(short[0], 0); + short.fill(0xa5); + assert_eq!( + numeric::kore_lua_format_number(short.as_mut_ptr().cast(), 5, 1e100, 15), + 4 + ); + } + assert_eq!(&short[..5], b"1e+1\0"); + assert!(short[5..].iter().all(|&byte| byte == 0xa5)); + + /* Public tonumber strings are not limited to the lexer token ceiling. */ + let mut long_number = " ".repeat(400); + long_number.push_str("-1.25e2tail"); + let (parsed, consumed) = target_parse(&long_number); + assert_eq!(parsed, -125.0); + assert_eq!(consumed, 407); + + let parse_cases: [(&str, f64, usize); 6] = [ + (" +12.5tail", 12.5, 7), + ("\x0b1.5tail", 1.5, 4), + (".5x", 0.5, 2), + ("1e+tail", 1.0, 1), + ("-0", -0.0, 2), + ("not-a-number", 0.0, 0), + ]; + for (text, expected, consumed) in parse_cases { + let (parsed, actual_consumed) = target_parse(text); + assert_eq!( + parsed.to_bits(), + expected.to_bits(), + "parse mismatch: {text}" + ); + assert_eq!(actual_consumed, consumed, "end mismatch: {text}"); + } + + /* Link the C engine against these exact exports in the build-script run. */ + #[cfg(kore_lua_c_engine)] + // SAFETY: the linked host harness takes no arguments and owns its VM. + assert_eq!(unsafe { kore_lua_host_smoke() }, 0); +} diff --git a/crates/lua/vendor/lua/README.md b/crates/lua/vendor/lua/README.md new file mode 100644 index 0000000..5bc0ee7 --- /dev/null +++ b/crates/lua/vendor/lua/README.md @@ -0,0 +1,7 @@ +# Lua + +This is the repository of Lua development code, as seen by the Lua team. It contains the full history of all commits but is mirrored irregularly. For complete information about Lua, visit [Lua.org](https://www.lua.org/). + +Please **do not** send pull requests. To report issues, post a message to the [Lua mailing list](https://www.lua.org/lua-l.html). + +Download official Lua releases from [Lua.org](https://www.lua.org/download.html). diff --git a/crates/lua/vendor/lua/lapi.c b/crates/lua/vendor/lua/lapi.c new file mode 100644 index 0000000..f9193ac --- /dev/null +++ b/crates/lua/vendor/lua/lapi.c @@ -0,0 +1,1477 @@ +/* +** $Id: lapi.c $ +** Lua API +** See Copyright Notice in lua.h +*/ + +#define lapi_c +#define LUA_CORE + +#include "lprefix.h" + + +#include +#include +#include + +#include "lua.h" + +#include "lapi.h" +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" +#include "lundump.h" +#include "lvm.h" + + + +const char lua_ident[] = + "$LuaVersion: " LUA_COPYRIGHT " $" + "$LuaAuthors: " LUA_AUTHORS " $"; + + + +/* +** Test for a valid index (one that is not the 'nilvalue'). +*/ +#define isvalid(L, o) ((o) != &G(L)->nilvalue) + + +/* test for pseudo index */ +#define ispseudo(i) ((i) <= LUA_REGISTRYINDEX) + +/* test for upvalue */ +#define isupvalue(i) ((i) < LUA_REGISTRYINDEX) + + +/* +** Convert an acceptable index to a pointer to its respective value. +** Non-valid indices return the special nil value 'G(L)->nilvalue'. +*/ +static TValue *index2value (lua_State *L, int idx) { + CallInfo *ci = L->ci; + if (idx > 0) { + StkId o = ci->func.p + idx; + api_check(L, idx <= ci->top.p - (ci->func.p + 1), "unacceptable index"); + if (o >= L->top.p) return &G(L)->nilvalue; + else return s2v(o); + } + else if (!ispseudo(idx)) { /* negative index */ + api_check(L, idx != 0 && -idx <= L->top.p - (ci->func.p + 1), + "invalid index"); + return s2v(L->top.p + idx); + } + else if (idx == LUA_REGISTRYINDEX) + return &G(L)->l_registry; + else { /* upvalues */ + idx = LUA_REGISTRYINDEX - idx; + api_check(L, idx <= MAXUPVAL + 1, "upvalue index too large"); + if (ttisCclosure(s2v(ci->func.p))) { /* C closure? */ + CClosure *func = clCvalue(s2v(ci->func.p)); + return (idx <= func->nupvalues) ? &func->upvalue[idx-1] + : &G(L)->nilvalue; + } + else { /* light C function or Lua function (through a hook)?) */ + api_check(L, ttislcf(s2v(ci->func.p)), "caller not a C function"); + return &G(L)->nilvalue; /* no upvalues */ + } + } +} + + + +/* +** Convert a valid actual index (not a pseudo-index) to its address. +*/ +static StkId index2stack (lua_State *L, int idx) { + CallInfo *ci = L->ci; + if (idx > 0) { + StkId o = ci->func.p + idx; + api_check(L, o < L->top.p, "invalid index"); + return o; + } + else { /* non-positive index */ + api_check(L, idx != 0 && -idx <= L->top.p - (ci->func.p + 1), + "invalid index"); + api_check(L, !ispseudo(idx), "invalid index"); + return L->top.p + idx; + } +} + + +LUA_API int lua_checkstack (lua_State *L, int n) { + int res; + CallInfo *ci; + lua_lock(L); + ci = L->ci; + api_check(L, n >= 0, "negative 'n'"); + if (L->stack_last.p - L->top.p > n) /* stack large enough? */ + res = 1; /* yes; check is OK */ + else /* need to grow stack */ + res = luaD_growstack(L, n, 0); + if (res && ci->top.p < L->top.p + n) + ci->top.p = L->top.p + n; /* adjust frame top */ + lua_unlock(L); + return res; +} + + +LUA_API void lua_xmove (lua_State *from, lua_State *to, int n) { + int i; + if (from == to) return; + lua_lock(to); + api_checkpop(from, n); + api_check(from, G(from) == G(to), "moving among independent states"); + api_check(from, to->ci->top.p - to->top.p >= n, "stack overflow"); + from->top.p -= n; + for (i = 0; i < n; i++) { + setobjs2s(to, to->top.p, from->top.p + i); + to->top.p++; /* stack already checked by previous 'api_check' */ + } + lua_unlock(to); +} + + +LUA_API lua_CFunction lua_atpanic (lua_State *L, lua_CFunction panicf) { + lua_CFunction old; + lua_lock(L); + old = G(L)->panic; + G(L)->panic = panicf; + lua_unlock(L); + return old; +} + + +LUA_API lua_Number lua_version (lua_State *L) { + UNUSED(L); + return LUA_VERSION_NUM; +} + + + +/* +** basic stack manipulation +*/ + + +/* +** convert an acceptable stack index into an absolute index +*/ +LUA_API int lua_absindex (lua_State *L, int idx) { + return (idx > 0 || ispseudo(idx)) + ? idx + : cast_int(L->top.p - L->ci->func.p) + idx; +} + + +LUA_API int lua_gettop (lua_State *L) { + return cast_int(L->top.p - (L->ci->func.p + 1)); +} + + +LUA_API void lua_settop (lua_State *L, int idx) { + CallInfo *ci; + StkId func, newtop; + ptrdiff_t diff; /* difference for new top */ + lua_lock(L); + ci = L->ci; + func = ci->func.p; + if (idx >= 0) { + api_check(L, idx <= ci->top.p - (func + 1), "new top too large"); + diff = ((func + 1) + idx) - L->top.p; + for (; diff > 0; diff--) + setnilvalue2s(L->top.p++); /* clear new slots */ + } + else { + api_check(L, -(idx+1) <= (L->top.p - (func + 1)), "invalid new top"); + diff = idx + 1; /* will "subtract" index (as it is negative) */ + } + newtop = L->top.p + diff; + if (diff < 0 && L->tbclist.p >= newtop) { + lua_assert(ci->callstatus & CIST_TBC); + newtop = luaF_close(L, newtop, CLOSEKTOP, 0); + } + L->top.p = newtop; /* correct top only after closing any upvalue */ + lua_unlock(L); +} + + +LUA_API void lua_closeslot (lua_State *L, int idx) { + StkId level; + lua_lock(L); + level = index2stack(L, idx); + api_check(L, (L->ci->callstatus & CIST_TBC) && (L->tbclist.p == level), + "no variable to close at given level"); + level = luaF_close(L, level, CLOSEKTOP, 0); + setnilvalue2s(level); + lua_unlock(L); +} + + +/* +** Reverse the stack segment from 'from' to 'to' +** (auxiliary to 'lua_rotate') +** Note that we move(copy) only the value inside the stack. +** (We do not move additional fields that may exist.) +*/ +static void reverse (lua_State *L, StkId from, StkId to) { + for (; from < to; from++, to--) { + TValue temp; + setobj(L, &temp, s2v(from)); + setobjs2s(L, from, to); + setobj2s(L, to, &temp); + } +} + + +/* +** Let x = AB, where A is a prefix of length 'n'. Then, +** rotate x n == BA. But BA == (A^r . B^r)^r. +*/ +LUA_API void lua_rotate (lua_State *L, int idx, int n) { + StkId p, t, m; + lua_lock(L); + t = L->top.p - 1; /* end of stack segment being rotated */ + p = index2stack(L, idx); /* start of segment */ + api_check(L, L->tbclist.p < p, "moving a to-be-closed slot"); + api_check(L, (n >= 0 ? n : -n) <= (t - p + 1), "invalid 'n'"); + m = (n >= 0 ? t - n : p - n - 1); /* end of prefix */ + reverse(L, p, m); /* reverse the prefix with length 'n' */ + reverse(L, m + 1, t); /* reverse the suffix */ + reverse(L, p, t); /* reverse the entire segment */ + lua_unlock(L); +} + + +LUA_API void lua_copy (lua_State *L, int fromidx, int toidx) { + TValue *fr, *to; + lua_lock(L); + fr = index2value(L, fromidx); + to = index2value(L, toidx); + api_check(L, isvalid(L, to), "invalid index"); + setobj(L, to, fr); + if (isupvalue(toidx)) /* function upvalue? */ + luaC_barrier(L, clCvalue(s2v(L->ci->func.p)), fr); + /* LUA_REGISTRYINDEX does not need gc barrier + (collector revisits it before finishing collection) */ + lua_unlock(L); +} + + +LUA_API void lua_pushvalue (lua_State *L, int idx) { + lua_lock(L); + setobj2s(L, L->top.p, index2value(L, idx)); + api_incr_top(L); + lua_unlock(L); +} + + + +/* +** access functions (stack -> C) +*/ + + +LUA_API int lua_type (lua_State *L, int idx) { + const TValue *o = index2value(L, idx); + return (isvalid(L, o) ? ttype(o) : LUA_TNONE); +} + + +LUA_API const char *lua_typename (lua_State *L, int t) { + UNUSED(L); + api_check(L, LUA_TNONE <= t && t < LUA_NUMTYPES, "invalid type"); + return ttypename(t); +} + + +LUA_API int lua_iscfunction (lua_State *L, int idx) { + const TValue *o = index2value(L, idx); + return (ttislcf(o) || (ttisCclosure(o))); +} + + +LUA_API int lua_isinteger (lua_State *L, int idx) { + const TValue *o = index2value(L, idx); + return ttisinteger(o); +} + + +LUA_API int lua_isnumber (lua_State *L, int idx) { + lua_Number n; + const TValue *o = index2value(L, idx); + return tonumber(o, &n); +} + + +LUA_API int lua_isstring (lua_State *L, int idx) { + const TValue *o = index2value(L, idx); + return (ttisstring(o) || cvt2str(o)); +} + + +LUA_API int lua_isuserdata (lua_State *L, int idx) { + const TValue *o = index2value(L, idx); + return (ttisfulluserdata(o) || ttislightuserdata(o)); +} + + +LUA_API int lua_rawequal (lua_State *L, int index1, int index2) { + const TValue *o1 = index2value(L, index1); + const TValue *o2 = index2value(L, index2); + return (isvalid(L, o1) && isvalid(L, o2)) ? luaV_rawequalobj(o1, o2) : 0; +} + + +LUA_API void lua_arith (lua_State *L, int op) { + lua_lock(L); + if (op != LUA_OPUNM && op != LUA_OPBNOT) + api_checkpop(L, 2); /* all other operations expect two operands */ + else { /* for unary operations, add fake 2nd operand */ + api_checkpop(L, 1); + setobjs2s(L, L->top.p, L->top.p - 1); + api_incr_top(L); + } + /* first operand at top - 2, second at top - 1; result go to top - 2 */ + luaO_arith(L, op, s2v(L->top.p - 2), s2v(L->top.p - 1), L->top.p - 2); + L->top.p--; /* pop second operand */ + lua_unlock(L); +} + + +LUA_API int lua_compare (lua_State *L, int index1, int index2, int op) { + const TValue *o1; + const TValue *o2; + int i = 0; + lua_lock(L); /* may call tag method */ + o1 = index2value(L, index1); + o2 = index2value(L, index2); + if (isvalid(L, o1) && isvalid(L, o2)) { + switch (op) { + case LUA_OPEQ: i = luaV_equalobj(L, o1, o2); break; + case LUA_OPLT: i = luaV_lessthan(L, o1, o2); break; + case LUA_OPLE: i = luaV_lessequal(L, o1, o2); break; + default: api_check(L, 0, "invalid option"); + } + } + lua_unlock(L); + return i; +} + + +LUA_API unsigned lua_numbertocstring (lua_State *L, int idx, char *buff) { + const TValue *o = index2value(L, idx); + if (ttisnumber(o)) { + unsigned len = luaO_tostringbuff(o, buff); + buff[len++] = '\0'; /* add final zero */ + return len; + } + else + return 0; +} + + +LUA_API size_t lua_stringtonumber (lua_State *L, const char *s) { + size_t sz = luaO_str2num(s, s2v(L->top.p)); + if (sz != 0) + api_incr_top(L); + return sz; +} + + +LUA_API lua_Number lua_tonumberx (lua_State *L, int idx, int *pisnum) { + lua_Number n = 0; + const TValue *o = index2value(L, idx); + int isnum = tonumber(o, &n); + if (pisnum) + *pisnum = isnum; + return n; +} + + +LUA_API lua_Integer lua_tointegerx (lua_State *L, int idx, int *pisnum) { + lua_Integer res = 0; + const TValue *o = index2value(L, idx); + int isnum = tointeger(o, &res); + if (pisnum) + *pisnum = isnum; + return res; +} + + +LUA_API int lua_toboolean (lua_State *L, int idx) { + const TValue *o = index2value(L, idx); + return !l_isfalse(o); +} + + +LUA_API const char *lua_tolstring (lua_State *L, int idx, size_t *len) { + TValue *o; + lua_lock(L); + o = index2value(L, idx); + if (!ttisstring(o)) { + if (!cvt2str(o)) { /* not convertible? */ + if (len != NULL) *len = 0; + lua_unlock(L); + return NULL; + } + luaO_tostring(L, o); + luaC_checkGC(L); + o = index2value(L, idx); /* previous call may reallocate the stack */ + } + lua_unlock(L); + if (len != NULL) + return getlstr(tsvalue(o), *len); + else + return getstr(tsvalue(o)); +} + + +LUA_API lua_Unsigned lua_rawlen (lua_State *L, int idx) { + const TValue *o = index2value(L, idx); + switch (ttypetag(o)) { + case LUA_VSHRSTR: return cast(lua_Unsigned, tsvalue(o)->shrlen); + case LUA_VLNGSTR: return cast(lua_Unsigned, tsvalue(o)->u.lnglen); + case LUA_VUSERDATA: return cast(lua_Unsigned, uvalue(o)->len); + case LUA_VTABLE: { + lua_Unsigned res; + lua_lock(L); + res = luaH_getn(L, hvalue(o)); + lua_unlock(L); + return res; + } + default: return 0; + } +} + + +LUA_API lua_CFunction lua_tocfunction (lua_State *L, int idx) { + const TValue *o = index2value(L, idx); + if (ttislcf(o)) return fvalue(o); + else if (ttisCclosure(o)) + return clCvalue(o)->f; + else return NULL; /* not a C function */ +} + + +l_sinline void *touserdata (const TValue *o) { + switch (ttype(o)) { + case LUA_TUSERDATA: return getudatamem(uvalue(o)); + case LUA_TLIGHTUSERDATA: return pvalue(o); + default: return NULL; + } +} + + +LUA_API void *lua_touserdata (lua_State *L, int idx) { + const TValue *o = index2value(L, idx); + return touserdata(o); +} + + +LUA_API lua_State *lua_tothread (lua_State *L, int idx) { + const TValue *o = index2value(L, idx); + return (!ttisthread(o)) ? NULL : thvalue(o); +} + + +/* +** Returns a pointer to the internal representation of an object. +** Note that ISO C does not allow the conversion of a pointer to +** function to a 'void*', so the conversion here goes through +** a 'size_t'. (As the returned pointer is only informative, this +** conversion should not be a problem.) +*/ +LUA_API const void *lua_topointer (lua_State *L, int idx) { + const TValue *o = index2value(L, idx); + switch (ttypetag(o)) { + case LUA_VLCF: return cast_voidp(cast_sizet(fvalue(o))); + case LUA_VUSERDATA: case LUA_VLIGHTUSERDATA: + return touserdata(o); + default: { + if (iscollectable(o)) + return gcvalue(o); + else + return NULL; + } + } +} + + + +/* +** push functions (C -> stack) +*/ + + +LUA_API void lua_pushnil (lua_State *L) { + lua_lock(L); + setnilvalue2s(L->top.p); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API void lua_pushnumber (lua_State *L, lua_Number n) { + lua_lock(L); + setfltvalue(s2v(L->top.p), n); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API void lua_pushinteger (lua_State *L, lua_Integer n) { + lua_lock(L); + setivalue(s2v(L->top.p), n); + api_incr_top(L); + lua_unlock(L); +} + + +/* +** Pushes on the stack a string with given length. Avoid using 's' when +** 'len' == 0 (as 's' can be NULL in that case), due to later use of +** 'memcmp' and 'memcpy'. +*/ +LUA_API const char *lua_pushlstring (lua_State *L, const char *s, size_t len) { + TString *ts; + lua_lock(L); + ts = (len == 0) ? luaS_new(L, "") : luaS_newlstr(L, s, len); + setsvalue2s(L, L->top.p, ts); + api_incr_top(L); + luaC_checkGC(L); + lua_unlock(L); + return getstr(ts); +} + + +LUA_API const char *lua_pushexternalstring (lua_State *L, + const char *s, size_t len, lua_Alloc falloc, void *ud) { + TString *ts; + lua_lock(L); + api_check(L, len <= MAX_SIZE, "string too large"); + api_check(L, s[len] == '\0', "string not ending with zero"); + ts = luaS_newextlstr (L, s, len, falloc, ud); + setsvalue2s(L, L->top.p, ts); + api_incr_top(L); + luaC_checkGC(L); + lua_unlock(L); + return getstr(ts); +} + + +LUA_API const char *lua_pushstring (lua_State *L, const char *s) { + lua_lock(L); + if (s == NULL) + setnilvalue2s(L->top.p); + else { + TString *ts; + ts = luaS_new(L, s); + setsvalue2s(L, L->top.p, ts); + s = getstr(ts); /* internal copy's address */ + } + api_incr_top(L); + luaC_checkGC(L); + lua_unlock(L); + return s; +} + + +LUA_API const char *lua_pushvfstring (lua_State *L, const char *fmt, + va_list argp) { + const char *ret; + lua_lock(L); + ret = luaO_pushvfstring(L, fmt, argp); + luaC_checkGC(L); + lua_unlock(L); + return ret; +} + + +LUA_API const char *lua_pushfstring (lua_State *L, const char *fmt, ...) { + const char *ret; + va_list argp; + lua_lock(L); + pushvfstring(L, argp, fmt, ret); + luaC_checkGC(L); + lua_unlock(L); + return ret; +} + + +LUA_API void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n) { + lua_lock(L); + if (n == 0) { + setfvalue(s2v(L->top.p), fn); + api_incr_top(L); + } + else { + int i; + CClosure *cl; + api_checkpop(L, n); + api_check(L, n <= MAXUPVAL, "upvalue index too large"); + cl = luaF_newCclosure(L, n); + cl->f = fn; + for (i = 0; i < n; i++) { + setobj2n(L, &cl->upvalue[i], s2v(L->top.p - n + i)); + /* does not need barrier because closure is white */ + lua_assert(iswhite(cl)); + } + L->top.p -= n; + setclCvalue(L, s2v(L->top.p), cl); + api_incr_top(L); + luaC_checkGC(L); + } + lua_unlock(L); +} + + +LUA_API void lua_pushboolean (lua_State *L, int b) { + lua_lock(L); + if (b) + setbtvalue(s2v(L->top.p)); + else + setbfvalue(s2v(L->top.p)); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API void lua_pushlightuserdata (lua_State *L, void *p) { + lua_lock(L); + setpvalue(s2v(L->top.p), p); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API int lua_pushthread (lua_State *L) { + lua_lock(L); + setthvalue(L, s2v(L->top.p), L); + api_incr_top(L); + lua_unlock(L); + return (mainthread(G(L)) == L); +} + + + +/* +** get functions (Lua -> stack) +*/ + + +static int auxgetstr (lua_State *L, const TValue *t, const char *k) { + lu_byte tag; + TString *str = luaS_new(L, k); + luaV_fastget(t, str, s2v(L->top.p), luaH_getstr, tag); + if (!tagisempty(tag)) + api_incr_top(L); + else { + setsvalue2s(L, L->top.p, str); + api_incr_top(L); + tag = luaV_finishget(L, t, s2v(L->top.p - 1), L->top.p - 1, tag); + } + lua_unlock(L); + return novariant(tag); +} + + +/* +** The following function assumes that the registry cannot be a weak +** table; so, an emergency collection while using the global table +** cannot collect it. +*/ +static void getGlobalTable (lua_State *L, TValue *gt) { + Table *registry = hvalue(&G(L)->l_registry); + lu_byte tag = luaH_getint(registry, LUA_RIDX_GLOBALS, gt); + (void)tag; /* avoid not-used warnings when checks are off */ + api_check(L, novariant(tag) == LUA_TTABLE, "global table must exist"); +} + + +LUA_API int lua_getglobal (lua_State *L, const char *name) { + TValue gt; + lua_lock(L); + getGlobalTable(L, >); + return auxgetstr(L, >, name); +} + + +LUA_API int lua_gettable (lua_State *L, int idx) { + lu_byte tag; + TValue *t; + lua_lock(L); + api_checkpop(L, 1); + t = index2value(L, idx); + luaV_fastget(t, s2v(L->top.p - 1), s2v(L->top.p - 1), luaH_get, tag); + if (tagisempty(tag)) + tag = luaV_finishget(L, t, s2v(L->top.p - 1), L->top.p - 1, tag); + lua_unlock(L); + return novariant(tag); +} + + +LUA_API int lua_getfield (lua_State *L, int idx, const char *k) { + lua_lock(L); + return auxgetstr(L, index2value(L, idx), k); +} + + +LUA_API int lua_geti (lua_State *L, int idx, lua_Integer n) { + TValue *t; + lu_byte tag; + lua_lock(L); + t = index2value(L, idx); + luaV_fastgeti(t, n, s2v(L->top.p), tag); + if (tagisempty(tag)) { + TValue key; + setivalue(&key, n); + tag = luaV_finishget(L, t, &key, L->top.p, tag); + } + api_incr_top(L); + lua_unlock(L); + return novariant(tag); +} + + +static int finishrawget (lua_State *L, lu_byte tag) { + if (tagisempty(tag)) /* avoid copying empty items to the stack */ + setnilvalue2s(L->top.p); + api_incr_top(L); + lua_unlock(L); + return novariant(tag); +} + + +l_sinline Table *gettable (lua_State *L, int idx) { + TValue *t = index2value(L, idx); + api_check(L, ttistable(t), "table expected"); + return hvalue(t); +} + + +LUA_API int lua_rawget (lua_State *L, int idx) { + Table *t; + lu_byte tag; + lua_lock(L); + api_checkpop(L, 1); + t = gettable(L, idx); + tag = luaH_get(t, s2v(L->top.p - 1), s2v(L->top.p - 1)); + L->top.p--; /* pop key */ + return finishrawget(L, tag); +} + + +LUA_API int lua_rawgeti (lua_State *L, int idx, lua_Integer n) { + Table *t; + lu_byte tag; + lua_lock(L); + t = gettable(L, idx); + luaH_fastgeti(t, n, s2v(L->top.p), tag); + return finishrawget(L, tag); +} + + +LUA_API int lua_rawgetp (lua_State *L, int idx, const void *p) { + Table *t; + TValue k; + lua_lock(L); + t = gettable(L, idx); + setpvalue(&k, cast_voidp(p)); + return finishrawget(L, luaH_get(t, &k, s2v(L->top.p))); +} + + +LUA_API void lua_createtable (lua_State *L, int narray, int nrec) { + Table *t; + lua_lock(L); + t = luaH_new(L); + sethvalue2s(L, L->top.p, t); + api_incr_top(L); + if (narray > 0 || nrec > 0) + luaH_resize(L, t, cast_uint(narray), cast_uint(nrec)); + luaC_checkGC(L); + lua_unlock(L); +} + + +LUA_API int lua_getmetatable (lua_State *L, int objindex) { + const TValue *obj; + Table *mt; + int res = 0; + lua_lock(L); + obj = index2value(L, objindex); + switch (ttype(obj)) { + case LUA_TTABLE: + mt = hvalue(obj)->metatable; + break; + case LUA_TUSERDATA: + mt = uvalue(obj)->metatable; + break; + default: + mt = G(L)->mt[ttype(obj)]; + break; + } + if (mt != NULL) { + sethvalue2s(L, L->top.p, mt); + api_incr_top(L); + res = 1; + } + lua_unlock(L); + return res; +} + + +LUA_API int lua_getiuservalue (lua_State *L, int idx, int n) { + TValue *o; + int t; + lua_lock(L); + o = index2value(L, idx); + api_check(L, ttisfulluserdata(o), "full userdata expected"); + if (n <= 0 || n > uvalue(o)->nuvalue) { + setnilvalue2s(L->top.p); + t = LUA_TNONE; + } + else { + setobj2s(L, L->top.p, &uvalue(o)->uv[n - 1].uv); + t = ttype(s2v(L->top.p)); + } + api_incr_top(L); + lua_unlock(L); + return t; +} + + +/* +** set functions (stack -> Lua) +*/ + +/* +** t[k] = value at the top of the stack (where 'k' is a string) +*/ +static void auxsetstr (lua_State *L, const TValue *t, const char *k) { + int hres; + TString *str = luaS_new(L, k); + api_checkpop(L, 1); + luaV_fastset(t, str, s2v(L->top.p - 1), hres, luaH_psetstr); + if (hres == HOK) { + luaV_finishfastset(L, t, s2v(L->top.p - 1)); + L->top.p--; /* pop value */ + } + else { + setsvalue2s(L, L->top.p, str); /* push 'str' (to make it a TValue) */ + api_incr_top(L); + luaV_finishset(L, t, s2v(L->top.p - 1), s2v(L->top.p - 2), hres); + L->top.p -= 2; /* pop value and key */ + } + lua_unlock(L); /* lock done by caller */ +} + + +LUA_API void lua_setglobal (lua_State *L, const char *name) { + TValue gt; + lua_lock(L); /* unlock done in 'auxsetstr' */ + getGlobalTable(L, >); + auxsetstr(L, >, name); +} + + +LUA_API void lua_settable (lua_State *L, int idx) { + TValue *t; + int hres; + lua_lock(L); + api_checkpop(L, 2); + t = index2value(L, idx); + luaV_fastset(t, s2v(L->top.p - 2), s2v(L->top.p - 1), hres, luaH_pset); + if (hres == HOK) + luaV_finishfastset(L, t, s2v(L->top.p - 1)); + else + luaV_finishset(L, t, s2v(L->top.p - 2), s2v(L->top.p - 1), hres); + L->top.p -= 2; /* pop index and value */ + lua_unlock(L); +} + + +LUA_API void lua_setfield (lua_State *L, int idx, const char *k) { + lua_lock(L); /* unlock done in 'auxsetstr' */ + auxsetstr(L, index2value(L, idx), k); +} + + +LUA_API void lua_seti (lua_State *L, int idx, lua_Integer n) { + TValue *t; + int hres; + lua_lock(L); + api_checkpop(L, 1); + t = index2value(L, idx); + luaV_fastseti(t, n, s2v(L->top.p - 1), hres); + if (hres == HOK) + luaV_finishfastset(L, t, s2v(L->top.p - 1)); + else { + TValue temp; + setivalue(&temp, n); + luaV_finishset(L, t, &temp, s2v(L->top.p - 1), hres); + } + L->top.p--; /* pop value */ + lua_unlock(L); +} + + +static void aux_rawset (lua_State *L, int idx, TValue *key, int n) { + Table *t; + lua_lock(L); + api_checkpop(L, n); + t = gettable(L, idx); + luaH_set(L, t, key, s2v(L->top.p - 1)); + invalidateTMcache(t); + luaC_barrierback(L, obj2gco(t), s2v(L->top.p - 1)); + L->top.p -= n; + lua_unlock(L); +} + + +LUA_API void lua_rawset (lua_State *L, int idx) { + aux_rawset(L, idx, s2v(L->top.p - 2), 2); +} + + +LUA_API void lua_rawsetp (lua_State *L, int idx, const void *p) { + TValue k; + setpvalue(&k, cast_voidp(p)); + aux_rawset(L, idx, &k, 1); +} + + +LUA_API void lua_rawseti (lua_State *L, int idx, lua_Integer n) { + Table *t; + lua_lock(L); + api_checkpop(L, 1); + t = gettable(L, idx); + luaH_setint(L, t, n, s2v(L->top.p - 1)); + luaC_barrierback(L, obj2gco(t), s2v(L->top.p - 1)); + L->top.p--; + lua_unlock(L); +} + + +LUA_API int lua_setmetatable (lua_State *L, int objindex) { + TValue *obj; + Table *mt; + lua_lock(L); + api_checkpop(L, 1); + obj = index2value(L, objindex); + if (ttisnil(s2v(L->top.p - 1))) + mt = NULL; + else { + api_check(L, ttistable(s2v(L->top.p - 1)), "table expected"); + mt = hvalue(s2v(L->top.p - 1)); + } + switch (ttype(obj)) { + case LUA_TTABLE: { + hvalue(obj)->metatable = mt; + if (mt) { + luaC_objbarrier(L, gcvalue(obj), mt); + luaC_checkfinalizer(L, gcvalue(obj), mt); + } + break; + } + case LUA_TUSERDATA: { + uvalue(obj)->metatable = mt; + if (mt) { + luaC_objbarrier(L, uvalue(obj), mt); + luaC_checkfinalizer(L, gcvalue(obj), mt); + } + break; + } + default: { + G(L)->mt[ttype(obj)] = mt; + break; + } + } + L->top.p--; + lua_unlock(L); + return 1; +} + + +LUA_API int lua_setiuservalue (lua_State *L, int idx, int n) { + TValue *o; + int res; + lua_lock(L); + api_checkpop(L, 1); + o = index2value(L, idx); + api_check(L, ttisfulluserdata(o), "full userdata expected"); + if (!(cast_uint(n) - 1u < cast_uint(uvalue(o)->nuvalue))) + res = 0; /* 'n' not in [1, uvalue(o)->nuvalue] */ + else { + setobj(L, &uvalue(o)->uv[n - 1].uv, s2v(L->top.p - 1)); + luaC_barrierback(L, gcvalue(o), s2v(L->top.p - 1)); + res = 1; + } + L->top.p--; + lua_unlock(L); + return res; +} + + +/* +** 'load' and 'call' functions (run Lua code) +*/ + + +#define checkresults(L,na,nr) \ + (api_check(L, (nr) == LUA_MULTRET \ + || (L->ci->top.p - L->top.p >= (nr) - (na)), \ + "results from function overflow current stack size"), \ + api_check(L, LUA_MULTRET <= (nr) && (nr) <= MAXRESULTS, \ + "invalid number of results")) + + +LUA_API void lua_callk (lua_State *L, int nargs, int nresults, + lua_KContext ctx, lua_KFunction k) { + StkId func; + lua_lock(L); + api_check(L, k == NULL || !isLua(L->ci), + "cannot use continuations inside hooks"); + api_checkpop(L, nargs + 1); + api_check(L, L->status == LUA_OK, "cannot do calls on non-normal thread"); + checkresults(L, nargs, nresults); + func = L->top.p - (nargs+1); + if (k != NULL && yieldable(L)) { /* need to prepare continuation? */ + L->ci->u.c.k = k; /* save continuation */ + L->ci->u.c.ctx = ctx; /* save context */ + luaD_call(L, func, nresults); /* do the call */ + } + else /* no continuation or no yieldable */ + luaD_callnoyield(L, func, nresults); /* just do the call */ + adjustresults(L, nresults); + lua_unlock(L); +} + + + +/* +** Execute a protected call. +*/ +struct CallS { /* data to 'f_call' */ + StkId func; + int nresults; +}; + + +static void f_call (lua_State *L, void *ud) { + struct CallS *c = cast(struct CallS *, ud); + luaD_callnoyield(L, c->func, c->nresults); +} + + + +LUA_API int lua_pcallk (lua_State *L, int nargs, int nresults, int errfunc, + lua_KContext ctx, lua_KFunction k) { + struct CallS c; + TStatus status; + ptrdiff_t func; + lua_lock(L); + api_check(L, k == NULL || !isLua(L->ci), + "cannot use continuations inside hooks"); + api_checkpop(L, nargs + 1); + api_check(L, L->status == LUA_OK, "cannot do calls on non-normal thread"); + checkresults(L, nargs, nresults); + if (errfunc == 0) + func = 0; + else { + StkId o = index2stack(L, errfunc); + api_check(L, ttisfunction(s2v(o)), "error handler must be a function"); + func = savestack(L, o); + } + c.func = L->top.p - (nargs+1); /* function to be called */ + if (k == NULL || !yieldable(L)) { /* no continuation or no yieldable? */ + c.nresults = nresults; /* do a 'conventional' protected call */ + status = luaD_pcall(L, f_call, &c, savestack(L, c.func), func); + } + else { /* prepare continuation (call is already protected by 'resume') */ + CallInfo *ci = L->ci; + ci->u.c.k = k; /* save continuation */ + ci->u.c.ctx = ctx; /* save context */ + /* save information for error recovery */ + ci->u2.funcidx = cast_int(savestack(L, c.func)); + ci->u.c.old_errfunc = L->errfunc; + L->errfunc = func; + setoah(ci, L->allowhook); /* save value of 'allowhook' */ + ci->callstatus |= CIST_YPCALL; /* function can do error recovery */ + luaD_call(L, c.func, nresults); /* do the call */ + ci->callstatus &= ~CIST_YPCALL; + L->errfunc = ci->u.c.old_errfunc; + status = LUA_OK; /* if it is here, there were no errors */ + } + adjustresults(L, nresults); + lua_unlock(L); + return APIstatus(status); +} + + +LUA_API int lua_load (lua_State *L, lua_Reader reader, void *data, + const char *chunkname, const char *mode) { + ZIO z; + TStatus status; + lua_lock(L); + luaC_checkGC(L); + if (!chunkname) chunkname = "?"; + luaZ_init(L, &z, reader, data); + status = luaD_protectedparser(L, &z, chunkname, mode); + if (status == LUA_OK) { /* no errors? */ + LClosure *f = clLvalue(s2v(L->top.p - 1)); /* get new function */ + if (f->nupvalues >= 1) { /* does it have an upvalue? */ + /* get global table from registry */ + TValue gt; + getGlobalTable(L, >); + /* set global table as 1st upvalue of 'f' (may be LUA_ENV) */ + setobj(L, f->upvals[0]->v.p, >); + luaC_barrier(L, f->upvals[0], >); + } + } + lua_unlock(L); + return APIstatus(status); +} + + +/* +** Dump a Lua function, calling 'writer' to write its parts. Ensure +** the stack returns with its original size. +*/ +LUA_API int lua_dump (lua_State *L, lua_Writer writer, void *data, int strip) { + int status; + ptrdiff_t otop = savestack(L, L->top.p); /* original top */ + TValue *f = s2v(L->top.p - 1); /* function to be dumped */ + lua_lock(L); + api_checkpop(L, 1); + api_check(L, isLfunction(f), "Lua function expected"); + status = luaU_dump(L, clLvalue(f)->p, writer, data, strip); + L->top.p = restorestack(L, otop); /* restore top */ + lua_unlock(L); + return status; +} + + +LUA_API int lua_status (lua_State *L) { + return APIstatus(L->status); +} + + +/* +** Garbage-collection function +*/ +LUA_API int lua_gc (lua_State *L, int what, ...) { + va_list argp; + int res = 0; + global_State *g = G(L); + if (g->gcstp & (GCSTPGC | GCSTPCLS)) /* internal stop? */ + return -1; /* all options are invalid when stopped */ + lua_lock(L); + va_start(argp, what); + switch (what) { + case LUA_GCSTOP: { + g->gcstp = GCSTPUSR; /* stopped by the user */ + break; + } + case LUA_GCRESTART: { + luaE_setdebt(g, 0); + g->gcstp = 0; /* (other bits must be zero here) */ + break; + } + case LUA_GCCOLLECT: { + luaC_fullgc(L, 0); + break; + } + case LUA_GCCOUNT: { + /* GC values are expressed in Kbytes: #bytes/2^10 */ + res = cast_int(gettotalbytes(g) >> 10); + break; + } + case LUA_GCCOUNTB: { + res = cast_int(gettotalbytes(g) & 0x3ff); + break; + } + case LUA_GCSTEP: { + lu_byte oldstp = g->gcstp; + l_mem n = cast(l_mem, va_arg(argp, size_t)); + l_mem newdebt; + int work = 0; /* true if GC did some work */ + g->gcstp = 0; /* allow GC to run (other bits must be zero here) */ + if (n <= 0) + newdebt = 0; /* force to run one basic step */ + else if (g->GCdebt >= n - MAX_LMEM) /* no overflow? */ + newdebt = g->GCdebt - n; + else /* overflow */ + newdebt = -MAX_LMEM; /* set debt to mininum value */ + luaE_setdebt(g, newdebt); + luaC_condGC(L, (void)0, work = 1); + if (work && g->gcstate == GCSpause) /* end of cycle? */ + res = 1; /* signal it */ + g->gcstp = oldstp; /* restore previous state */ + break; + } + case LUA_GCISRUNNING: { + res = gcrunning(g); + break; + } + case LUA_GCGEN: { + res = (g->gckind == KGC_INC) ? LUA_GCINC : LUA_GCGEN; + luaC_changemode(L, KGC_GENMINOR); + break; + } + case LUA_GCINC: { + res = (g->gckind == KGC_INC) ? LUA_GCINC : LUA_GCGEN; + luaC_changemode(L, KGC_INC); + break; + } + case LUA_GCPARAM: { + int param = va_arg(argp, int); + int value = va_arg(argp, int); + api_check(L, 0 <= param && param < LUA_GCPN, "invalid parameter"); + res = cast_int(luaO_applyparam(g->gcparams[param], 100)); + if (value >= 0) + g->gcparams[param] = luaO_codeparam(cast_uint(value)); + break; + } + default: res = -1; /* invalid option */ + } + va_end(argp); + lua_unlock(L); + return res; +} + + + +/* +** miscellaneous functions +*/ + + +LUA_API int lua_error (lua_State *L) { + TValue *errobj; + lua_lock(L); + errobj = s2v(L->top.p - 1); + api_checkpop(L, 1); + /* error object is the memory error message? */ + if (ttisshrstring(errobj) && eqshrstr(tsvalue(errobj), G(L)->memerrmsg)) + luaM_error(L); /* raise a memory error */ + else + luaG_errormsg(L); /* raise a regular error */ + /* code unreachable; will unlock when control actually leaves the kernel */ + return 0; /* to avoid warnings */ +} + + +LUA_API int lua_next (lua_State *L, int idx) { + Table *t; + int more; + lua_lock(L); + api_checkpop(L, 1); + t = gettable(L, idx); + more = luaH_next(L, t, L->top.p - 1); + if (more) + api_incr_top(L); + else /* no more elements */ + L->top.p--; /* pop key */ + lua_unlock(L); + return more; +} + + +LUA_API void lua_toclose (lua_State *L, int idx) { + StkId o; + lua_lock(L); + o = index2stack(L, idx); + api_check(L, L->tbclist.p < o, "given index below or equal a marked one"); + luaF_newtbcupval(L, o); /* create new to-be-closed upvalue */ + L->ci->callstatus |= CIST_TBC; /* mark that function has TBC slots */ + lua_unlock(L); +} + + +LUA_API void lua_concat (lua_State *L, int n) { + lua_lock(L); + if (n > 0) { + api_checkpop(L, n); + luaV_concat(L, n); + luaC_checkGC(L); + } + else { /* nothing to concatenate */ + setsvalue2s(L, L->top.p, luaS_newlstr(L, "", 0)); /* push empty string */ + api_incr_top(L); + } + lua_unlock(L); +} + + +LUA_API void lua_len (lua_State *L, int idx) { + TValue *t; + lua_lock(L); + t = index2value(L, idx); + luaV_objlen(L, L->top.p, t); + api_incr_top(L); + lua_unlock(L); +} + + +LUA_API lua_Alloc lua_getallocf (lua_State *L, void **ud) { + lua_Alloc f; + lua_lock(L); + if (ud) *ud = G(L)->ud; + f = G(L)->frealloc; + lua_unlock(L); + return f; +} + + +LUA_API void lua_setallocf (lua_State *L, lua_Alloc f, void *ud) { + lua_lock(L); + G(L)->ud = ud; + G(L)->frealloc = f; + lua_unlock(L); +} + + +void lua_setwarnf (lua_State *L, lua_WarnFunction f, void *ud) { + lua_lock(L); + G(L)->ud_warn = ud; + G(L)->warnf = f; + lua_unlock(L); +} + + +void lua_warning (lua_State *L, const char *msg, int tocont) { + lua_lock(L); + luaE_warning(L, msg, tocont); + lua_unlock(L); +} + + + +LUA_API void *lua_newuserdatauv (lua_State *L, size_t size, int nuvalue) { + Udata *u; + lua_lock(L); + api_check(L, 0 <= nuvalue && nuvalue < SHRT_MAX, "invalid value"); + u = luaS_newudata(L, size, cast(unsigned short, nuvalue)); + setuvalue(L, s2v(L->top.p), u); + api_incr_top(L); + luaC_checkGC(L); + lua_unlock(L); + return getudatamem(u); +} + + + +static const char *aux_upvalue (TValue *fi, int n, TValue **val, + GCObject **owner) { + switch (ttypetag(fi)) { + case LUA_VCCL: { /* C closure */ + CClosure *f = clCvalue(fi); + if (!(cast_uint(n) - 1u < cast_uint(f->nupvalues))) + return NULL; /* 'n' not in [1, f->nupvalues] */ + *val = &f->upvalue[n-1]; + if (owner) *owner = obj2gco(f); + return ""; + } + case LUA_VLCL: { /* Lua closure */ + LClosure *f = clLvalue(fi); + TString *name; + Proto *p = f->p; + if (!(cast_uint(n) - 1u < cast_uint(p->sizeupvalues))) + return NULL; /* 'n' not in [1, p->sizeupvalues] */ + *val = f->upvals[n-1]->v.p; + if (owner) *owner = obj2gco(f->upvals[n - 1]); + name = p->upvalues[n-1].name; + return (name == NULL) ? "(no name)" : getstr(name); + } + default: return NULL; /* not a closure */ + } +} + + +LUA_API const char *lua_getupvalue (lua_State *L, int funcindex, int n) { + const char *name; + TValue *val = NULL; /* to avoid warnings */ + lua_lock(L); + name = aux_upvalue(index2value(L, funcindex), n, &val, NULL); + if (name) { + setobj2s(L, L->top.p, val); + api_incr_top(L); + } + lua_unlock(L); + return name; +} + + +LUA_API const char *lua_setupvalue (lua_State *L, int funcindex, int n) { + const char *name; + TValue *val = NULL; /* to avoid warnings */ + GCObject *owner = NULL; /* to avoid warnings */ + TValue *fi; + lua_lock(L); + fi = index2value(L, funcindex); + api_checkpop(L, 1); + name = aux_upvalue(fi, n, &val, &owner); + if (name) { + L->top.p--; + setobj(L, val, s2v(L->top.p)); + luaC_barrier(L, owner, val); + } + lua_unlock(L); + return name; +} + + +static UpVal **getupvalref (lua_State *L, int fidx, int n, LClosure **pf) { + static const UpVal *const nullup = NULL; + LClosure *f; + TValue *fi = index2value(L, fidx); + api_check(L, ttisLclosure(fi), "Lua function expected"); + f = clLvalue(fi); + if (pf) *pf = f; + if (1 <= n && n <= f->p->sizeupvalues) + return &f->upvals[n - 1]; /* get its upvalue pointer */ + else + return (UpVal**)&nullup; +} + + +LUA_API void *lua_upvalueid (lua_State *L, int fidx, int n) { + TValue *fi = index2value(L, fidx); + switch (ttypetag(fi)) { + case LUA_VLCL: { /* lua closure */ + return *getupvalref(L, fidx, n, NULL); + } + case LUA_VCCL: { /* C closure */ + CClosure *f = clCvalue(fi); + if (1 <= n && n <= f->nupvalues) + return &f->upvalue[n - 1]; + /* else */ + } /* FALLTHROUGH */ + case LUA_VLCF: + return NULL; /* light C functions have no upvalues */ + default: { + api_check(L, 0, "function expected"); + return NULL; + } + } +} + + +LUA_API void lua_upvaluejoin (lua_State *L, int fidx1, int n1, + int fidx2, int n2) { + LClosure *f1; + UpVal **up1 = getupvalref(L, fidx1, n1, &f1); + UpVal **up2 = getupvalref(L, fidx2, n2, NULL); + api_check(L, *up1 != NULL && *up2 != NULL, "invalid upvalue index"); + *up1 = *up2; + luaC_objbarrier(L, f1, *up1); +} diff --git a/crates/lua/vendor/lua/lapi.h b/crates/lua/vendor/lua/lapi.h new file mode 100644 index 0000000..9b54534 --- /dev/null +++ b/crates/lua/vendor/lua/lapi.h @@ -0,0 +1,65 @@ +/* +** $Id: lapi.h $ +** Auxiliary functions from Lua API +** See Copyright Notice in lua.h +*/ + +#ifndef lapi_h +#define lapi_h + + +#include "llimits.h" +#include "lstate.h" + + +#if defined(LUA_USE_APICHECK) +#include +#define api_check(l,e,msg) assert(e) +#else /* for testing */ +#define api_check(l,e,msg) ((void)(l), lua_assert((e) && msg)) +#endif + + + +/* Increments 'L->top.p', checking for stack overflows */ +#define api_incr_top(L) \ + (L->top.p++, api_check(L, L->top.p <= L->ci->top.p, "stack overflow")) + + +/* +** macros that are executed whenever program enters the Lua core +** ('lua_lock') and leaves the core ('lua_unlock') +*/ +#if !defined(lua_lock) +#define lua_lock(L) ((void) 0) +#define lua_unlock(L) ((void) 0) +#endif + + + +/* +** If a call returns too many multiple returns, the callee may not have +** stack space to accommodate all results. In this case, this macro +** increases its stack space ('L->ci->top.p'). +*/ +#define adjustresults(L,nres) \ + { if ((nres) <= LUA_MULTRET && L->ci->top.p < L->top.p) \ + L->ci->top.p = L->top.p; } + + +/* Ensure the stack has at least 'n' elements */ +#define api_checknelems(L,n) \ + api_check(L, (n) < (L->top.p - L->ci->func.p), \ + "not enough elements in the stack") + + +/* Ensure the stack has at least 'n' elements to be popped. (Some +** functions only update a slot after checking it for popping, but that +** is only an optimization for a pop followed by a push.) +*/ +#define api_checkpop(L,n) \ + api_check(L, (n) < L->top.p - L->ci->func.p && \ + L->tbclist.p < L->top.p - (n), \ + "not enough free elements in the stack") + +#endif diff --git a/crates/lua/vendor/lua/lauxlib.c b/crates/lua/vendor/lua/lauxlib.c new file mode 100644 index 0000000..8b53f53 --- /dev/null +++ b/crates/lua/vendor/lua/lauxlib.c @@ -0,0 +1,1214 @@ +/* +** $Id: lauxlib.c $ +** Auxiliary functions for building Lua libraries +** See Copyright Notice in lua.h +*/ + +#define lauxlib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include +#include +#include +#include +#include + + +/* +** This file uses only the official API of Lua. +** Any function declared here could be written as an application function. +*/ + +#include "lua.h" + +#include "lauxlib.h" +#include "llimits.h" + + +/* +** {====================================================== +** Traceback +** ======================================================= +*/ + + +#define LEVELS1 10 /* size of the first part of the stack */ +#define LEVELS2 11 /* size of the second part of the stack */ + + + +/* +** Search for 'objidx' in table at index -1. ('objidx' must be an +** absolute index.) Return 1 + string at top if it found a good name. +*/ +static int findfield (lua_State *L, int objidx, int level) { + if (level == 0 || !lua_istable(L, -1)) + return 0; /* not found */ + lua_pushnil(L); /* start 'next' loop */ + while (lua_next(L, -2)) { /* for each pair in table */ + if (lua_type(L, -2) == LUA_TSTRING) { /* ignore non-string keys */ + if (lua_rawequal(L, objidx, -1)) { /* found object? */ + lua_pop(L, 1); /* remove value (but keep name) */ + return 1; + } + else if (findfield(L, objidx, level - 1)) { /* try recursively */ + /* stack: lib_name, lib_table, field_name (top) */ + lua_pushliteral(L, "."); /* place '.' between the two names */ + lua_replace(L, -3); /* (in the slot occupied by table) */ + lua_concat(L, 3); /* lib_name.field_name */ + return 1; + } + } + lua_pop(L, 1); /* remove value */ + } + return 0; /* not found */ +} + + +/* +** Search for a name for a function in all loaded modules +*/ +static int pushglobalfuncname (lua_State *L, lua_Debug *ar) { + int top = lua_gettop(L); + lua_getinfo(L, "f", ar); /* push function */ + lua_getfield(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE); + luaL_checkstack(L, 6, "not enough stack"); /* slots for 'findfield' */ + if (findfield(L, top + 1, 2)) { + const char *name = lua_tostring(L, -1); + if (strncmp(name, LUA_GNAME ".", 3) == 0) { /* name start with '_G.'? */ + lua_pushstring(L, name + 3); /* push name without prefix */ + lua_remove(L, -2); /* remove original name */ + } + lua_copy(L, -1, top + 1); /* copy name to proper place */ + lua_settop(L, top + 1); /* remove table "loaded" and name copy */ + return 1; + } + else { + lua_settop(L, top); /* remove function and global table */ + return 0; + } +} + + +static void pushfuncname (lua_State *L, lua_Debug *ar) { + if (*ar->namewhat != '\0') /* is there a name from code? */ + lua_pushfstring(L, "%s '%s'", ar->namewhat, ar->name); /* use it */ + else if (*ar->what == 'm') /* main? */ + lua_pushliteral(L, "main chunk"); + else if (pushglobalfuncname(L, ar)) { /* try a global name */ + lua_pushfstring(L, "function '%s'", lua_tostring(L, -1)); + lua_remove(L, -2); /* remove name */ + } + else if (*ar->what != 'C') /* for Lua functions, use */ + lua_pushfstring(L, "function <%s:%d>", ar->short_src, ar->linedefined); + else /* nothing left... */ + lua_pushliteral(L, "?"); +} + + +static int lastlevel (lua_State *L) { + lua_Debug ar; + int li = 1, le = 1; + /* find an upper bound */ + while (lua_getstack(L, le, &ar)) { li = le; le *= 2; } + /* do a binary search */ + while (li < le) { + int m = (li + le)/2; + if (lua_getstack(L, m, &ar)) li = m + 1; + else le = m; + } + return le - 1; +} + + +LUALIB_API void luaL_traceback (lua_State *L, lua_State *L1, + const char *msg, int level) { + luaL_Buffer b; + lua_Debug ar; + int last = lastlevel(L1); + int limit2show = (last - level > LEVELS1 + LEVELS2) ? LEVELS1 : -1; + luaL_buffinit(L, &b); + if (msg) { + luaL_addstring(&b, msg); + luaL_addchar(&b, '\n'); + } + luaL_addstring(&b, "stack traceback:"); + while (lua_getstack(L1, level++, &ar)) { + if (limit2show-- == 0) { /* too many levels? */ + int n = last - level - LEVELS2 + 1; /* number of levels to skip */ + lua_pushfstring(L, "\n\t...\t(skipping %d levels)", n); + luaL_addvalue(&b); /* add warning about skip */ + level += n; /* and skip to last levels */ + } + else { + lua_getinfo(L1, "Slnt", &ar); + if (ar.currentline <= 0) + lua_pushfstring(L, "\n\t%s: in ", ar.short_src); + else + lua_pushfstring(L, "\n\t%s:%d: in ", ar.short_src, ar.currentline); + luaL_addvalue(&b); + pushfuncname(L, &ar); + luaL_addvalue(&b); + if (ar.istailcall) + luaL_addstring(&b, "\n\t(...tail calls...)"); + } + } + luaL_pushresult(&b); +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Error-report functions +** ======================================================= +*/ + +LUALIB_API int luaL_argerror (lua_State *L, int arg, const char *extramsg) { + lua_Debug ar; + const char *argword; + if (!lua_getstack(L, 0, &ar)) /* no stack frame? */ + return luaL_error(L, "bad argument #%d (%s)", arg, extramsg); + lua_getinfo(L, "nt", &ar); + if (arg <= ar.extraargs) /* error in an extra argument? */ + argword = "extra argument"; + else { + arg -= ar.extraargs; /* do not count extra arguments */ + if (strcmp(ar.namewhat, "method") == 0) { /* colon syntax? */ + arg--; /* do not count (extra) self argument */ + if (arg == 0) /* error in self argument? */ + return luaL_error(L, "calling '%s' on bad self (%s)", + ar.name, extramsg); + /* else go through; error in a regular argument */ + } + argword = "argument"; + } + if (ar.name == NULL) + ar.name = (pushglobalfuncname(L, &ar)) ? lua_tostring(L, -1) : "?"; + return luaL_error(L, "bad %s #%d to '%s' (%s)", + argword, arg, ar.name, extramsg); +} + + +LUALIB_API int luaL_typeerror (lua_State *L, int arg, const char *tname) { + const char *msg; + const char *typearg; /* name for the type of the actual argument */ + if (luaL_getmetafield(L, arg, "__name") == LUA_TSTRING) + typearg = lua_tostring(L, -1); /* use the given type name */ + else if (lua_type(L, arg) == LUA_TLIGHTUSERDATA) + typearg = "light userdata"; /* special name for messages */ + else + typearg = luaL_typename(L, arg); /* standard name */ + msg = lua_pushfstring(L, "%s expected, got %s", tname, typearg); + return luaL_argerror(L, arg, msg); +} + + +static void tag_error (lua_State *L, int arg, int tag) { + luaL_typeerror(L, arg, lua_typename(L, tag)); +} + + +/* +** The use of 'lua_pushfstring' ensures this function does not +** need reserved stack space when called. +*/ +LUALIB_API void luaL_where (lua_State *L, int level) { + lua_Debug ar; + if (lua_getstack(L, level, &ar)) { /* check function at level */ + lua_getinfo(L, "Sl", &ar); /* get info about it */ + if (ar.currentline > 0) { /* is there info? */ + lua_pushfstring(L, "%s:%d: ", ar.short_src, ar.currentline); + return; + } + } + lua_pushfstring(L, ""); /* else, no information available... */ +} + + +/* +** Again, the use of 'lua_pushvfstring' ensures this function does +** not need reserved stack space when called. (At worst, it generates +** a memory error instead of the given message.) +*/ +LUALIB_API int luaL_error (lua_State *L, const char *fmt, ...) { + va_list argp; + va_start(argp, fmt); + luaL_where(L, 1); + lua_pushvfstring(L, fmt, argp); + va_end(argp); + lua_concat(L, 2); + return lua_error(L); +} + + +LUALIB_API int luaL_fileresult (lua_State *L, int stat, const char *fname) { + int en = errno; /* calls to Lua API may change this value */ + if (stat) { + lua_pushboolean(L, 1); + return 1; + } + else { + const char *msg; + luaL_pushfail(L); + msg = (en != 0) ? strerror(en) : "(no extra info)"; + if (fname) + lua_pushfstring(L, "%s: %s", fname, msg); + else + lua_pushstring(L, msg); + lua_pushinteger(L, en); + return 3; + } +} + + +#if !defined(l_inspectstat) /* { */ + +#if defined(LUA_USE_POSIX) + +#include + +/* +** use appropriate macros to interpret 'pclose' return status +*/ +#define l_inspectstat(stat,what) \ + if (WIFEXITED(stat)) { stat = WEXITSTATUS(stat); } \ + else if (WIFSIGNALED(stat)) { stat = WTERMSIG(stat); what = "signal"; } + +#else + +#define l_inspectstat(stat,what) /* no op */ + +#endif + +#endif /* } */ + + +LUALIB_API int luaL_execresult (lua_State *L, int stat) { + if (stat != 0 && errno != 0) /* error with an 'errno'? */ + return luaL_fileresult(L, 0, NULL); + else { + const char *what = "exit"; /* type of termination */ + l_inspectstat(stat, what); /* interpret result */ + if (*what == 'e' && stat == 0) /* successful termination? */ + lua_pushboolean(L, 1); + else + luaL_pushfail(L); + lua_pushstring(L, what); + lua_pushinteger(L, stat); + return 3; /* return true/fail,what,code */ + } +} + +/* }====================================================== */ + + + +/* +** {====================================================== +** Userdata's metatable manipulation +** ======================================================= +*/ + +LUALIB_API int luaL_newmetatable (lua_State *L, const char *tname) { + if (luaL_getmetatable(L, tname) != LUA_TNIL) /* name already in use? */ + return 0; /* leave previous value on top, but return 0 */ + lua_pop(L, 1); + lua_createtable(L, 0, 2); /* create metatable */ + lua_pushstring(L, tname); + lua_setfield(L, -2, "__name"); /* metatable.__name = tname */ + lua_pushvalue(L, -1); + lua_setfield(L, LUA_REGISTRYINDEX, tname); /* registry.name = metatable */ + return 1; +} + + +LUALIB_API void luaL_setmetatable (lua_State *L, const char *tname) { + luaL_getmetatable(L, tname); + lua_setmetatable(L, -2); +} + + +LUALIB_API void *luaL_testudata (lua_State *L, int ud, const char *tname) { + void *p = lua_touserdata(L, ud); + if (p != NULL) { /* value is a userdata? */ + if (lua_getmetatable(L, ud)) { /* does it have a metatable? */ + luaL_getmetatable(L, tname); /* get correct metatable */ + if (!lua_rawequal(L, -1, -2)) /* not the same? */ + p = NULL; /* value is a userdata with wrong metatable */ + lua_pop(L, 2); /* remove both metatables */ + return p; + } + } + return NULL; /* value is not a userdata with a metatable */ +} + + +LUALIB_API void *luaL_checkudata (lua_State *L, int ud, const char *tname) { + void *p = luaL_testudata(L, ud, tname); + luaL_argexpected(L, p != NULL, ud, tname); + return p; +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Argument check functions +** ======================================================= +*/ + +LUALIB_API int luaL_checkoption (lua_State *L, int arg, const char *def, + const char *const lst[]) { + const char *name = (def) ? luaL_optstring(L, arg, def) : + luaL_checkstring(L, arg); + int i; + for (i=0; lst[i]; i++) + if (strcmp(lst[i], name) == 0) + return i; + return luaL_argerror(L, arg, + lua_pushfstring(L, "invalid option '%s'", name)); +} + + +/* +** Ensures the stack has at least 'space' extra slots, raising an error +** if it cannot fulfill the request. (The error handling needs a few +** extra slots to format the error message. In case of an error without +** this extra space, Lua will generate the same 'stack overflow' error, +** but without 'msg'.) +*/ +LUALIB_API void luaL_checkstack (lua_State *L, int space, const char *msg) { + if (l_unlikely(!lua_checkstack(L, space))) { + if (msg) + luaL_error(L, "stack overflow (%s)", msg); + else + luaL_error(L, "stack overflow"); + } +} + + +LUALIB_API void luaL_checktype (lua_State *L, int arg, int t) { + if (l_unlikely(lua_type(L, arg) != t)) + tag_error(L, arg, t); +} + + +LUALIB_API void luaL_checkany (lua_State *L, int arg) { + if (l_unlikely(lua_type(L, arg) == LUA_TNONE)) + luaL_argerror(L, arg, "value expected"); +} + + +LUALIB_API const char *luaL_checklstring (lua_State *L, int arg, size_t *len) { + const char *s = lua_tolstring(L, arg, len); + if (l_unlikely(!s)) tag_error(L, arg, LUA_TSTRING); + return s; +} + + +LUALIB_API const char *luaL_optlstring (lua_State *L, int arg, + const char *def, size_t *len) { + if (lua_isnoneornil(L, arg)) { + if (len) + *len = (def ? strlen(def) : 0); + return def; + } + else return luaL_checklstring(L, arg, len); +} + + +LUALIB_API lua_Number luaL_checknumber (lua_State *L, int arg) { + int isnum; + lua_Number d = lua_tonumberx(L, arg, &isnum); + if (l_unlikely(!isnum)) + tag_error(L, arg, LUA_TNUMBER); + return d; +} + + +LUALIB_API lua_Number luaL_optnumber (lua_State *L, int arg, lua_Number def) { + return luaL_opt(L, luaL_checknumber, arg, def); +} + + +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); +} + + +LUALIB_API lua_Integer luaL_checkinteger (lua_State *L, int arg) { + int isnum; + lua_Integer d = lua_tointegerx(L, arg, &isnum); + if (l_unlikely(!isnum)) { + interror(L, arg); + } + return d; +} + + +LUALIB_API lua_Integer luaL_optinteger (lua_State *L, int arg, + lua_Integer def) { + return luaL_opt(L, luaL_checkinteger, arg, def); +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Generic Buffer manipulation +** ======================================================= +*/ + +/* userdata to box arbitrary data */ +typedef struct UBox { + void *box; + size_t bsize; +} UBox; + + +/* Resize the buffer used by a box. Optimize for the common case of +** resizing to the old size. (For instance, __gc will resize the box +** to 0 even after it was closed. 'pushresult' may also resize it to a +** final size that is equal to the one set when the buffer was created.) +*/ +static void *resizebox (lua_State *L, int idx, size_t newsize) { + UBox *box = (UBox *)lua_touserdata(L, idx); + if (box->bsize == newsize) /* not changing size? */ + return box->box; /* keep the buffer */ + else { + void *ud; + lua_Alloc allocf = lua_getallocf(L, &ud); + void *temp = allocf(ud, box->box, box->bsize, newsize); + if (l_unlikely(temp == NULL && newsize > 0)) { /* allocation error? */ + lua_pushliteral(L, "not enough memory"); + lua_error(L); /* raise a memory error */ + } + box->box = temp; + box->bsize = newsize; + return temp; + } +} + + +static int boxgc (lua_State *L) { + resizebox(L, 1, 0); + return 0; +} + + +static const luaL_Reg boxmt[] = { /* box metamethods */ + {"__gc", boxgc}, + {"__close", boxgc}, + {NULL, NULL} +}; + + +/* +** Get/create metatable (MT) for boxes +*/ +static void getBoxMT (lua_State *L) { + const char *BOXMT = "_UBOX*"; /* key for the metatable */ + if (luaL_getmetatable(L, BOXMT) == LUA_TNIL) { /* MT not created yet? */ + luaL_newlibtable(L, boxmt); /* create it */ + luaL_setfuncs(L, boxmt, 0); /* initialize it */ + lua_copy(L, -1, -2); /* change stack from nil,MT to MT,MT */ + lua_setfield(L, LUA_REGISTRYINDEX, BOXMT); /* store MT in the registry */ + } +} + + +static void newbox (lua_State *L) { + UBox *box = (UBox *)lua_newuserdatauv(L, sizeof(UBox), 0); + box->box = NULL; + box->bsize = 0; + getBoxMT(L); + lua_setmetatable(L, -2); +} + + +/* +** check whether buffer is using a userdata on the stack as a temporary +** buffer +*/ +#define buffonstack(B) ((B)->b != (B)->init.b) + + +/* +** Whenever buffer is accessed, slot 'idx' must either be a box (which +** cannot be NULL) or it is a placeholder for the buffer. +*/ +#define checkbufferlevel(B,idx) \ + lua_assert(buffonstack(B) ? lua_touserdata(B->L, idx) != NULL \ + : lua_touserdata(B->L, idx) == (void*)B) + + +/* +** Compute new size for buffer 'B', enough to accommodate extra 'sz' +** bytes plus one for a terminating zero. +*/ +static size_t newbuffsize (luaL_Buffer *B, size_t sz) { + size_t newsize = B->size; + if (l_unlikely(sz >= MAX_SIZE - B->n)) + return cast_sizet(luaL_error(B->L, "resulting string too large")); + /* else B->n + sz + 1 <= MAX_SIZE */ + if (newsize <= MAX_SIZE/3 * 2) /* no overflow? */ + newsize += (newsize >> 1); /* new size *= 1.5 */ + if (newsize < B->n + sz + 1) /* not big enough? */ + newsize = B->n + sz + 1; + return newsize; +} + + +/* +** Returns a pointer to a free area with at least 'sz' bytes in buffer +** 'B'. 'boxidx' is the relative position in the stack where is the +** buffer's box or its placeholder. +*/ +static char *prepbuffsize (luaL_Buffer *B, size_t sz, int boxidx) { + checkbufferlevel(B, boxidx); + if (B->size - B->n >= sz) /* enough space? */ + return B->b + B->n; + else { + lua_State *L = B->L; + char *newbuff; + size_t newsize = newbuffsize(B, sz); + /* create larger buffer */ + if (buffonstack(B)) /* buffer already has a box? */ + newbuff = (char *)resizebox(L, boxidx, newsize); /* resize it */ + else { /* no box yet */ + lua_remove(L, boxidx); /* remove placeholder */ + newbox(L); /* create a new box */ + lua_insert(L, boxidx); /* move box to its intended position */ + lua_toclose(L, boxidx); + newbuff = (char *)resizebox(L, boxidx, newsize); + memcpy(newbuff, B->b, B->n * sizeof(char)); /* copy original content */ + } + B->b = newbuff; + B->size = newsize; + return newbuff + B->n; + } +} + +/* +** returns a pointer to a free area with at least 'sz' bytes +*/ +LUALIB_API char *luaL_prepbuffsize (luaL_Buffer *B, size_t sz) { + return prepbuffsize(B, sz, -1); +} + + +LUALIB_API void luaL_addlstring (luaL_Buffer *B, const char *s, size_t l) { + if (l > 0) { /* avoid 'memcpy' when 's' can be NULL */ + char *b = prepbuffsize(B, l, -1); + memcpy(b, s, l * sizeof(char)); + luaL_addsize(B, l); + } +} + + +LUALIB_API void luaL_addstring (luaL_Buffer *B, const char *s) { + luaL_addlstring(B, s, strlen(s)); +} + + +LUALIB_API void luaL_pushresult (luaL_Buffer *B) { + lua_State *L = B->L; + checkbufferlevel(B, -1); + if (!buffonstack(B)) /* using static buffer? */ + lua_pushlstring(L, B->b, B->n); /* save result as regular string */ + else { /* reuse buffer already allocated */ + UBox *box = (UBox *)lua_touserdata(L, -1); + void *ud; + lua_Alloc allocf = lua_getallocf(L, &ud); /* function to free buffer */ + size_t len = B->n; /* final string length */ + char *s; + resizebox(L, -1, len + 1); /* adjust box size to content size */ + s = (char*)box->box; /* final buffer address */ + s[len] = '\0'; /* add ending zero */ + /* clear box, as Lua will take control of the buffer */ + box->bsize = 0; box->box = NULL; + lua_pushexternalstring(L, s, len, allocf, ud); + lua_closeslot(L, -2); /* close the box */ + lua_gc(L, LUA_GCSTEP, len); + } + lua_remove(L, -2); /* remove box or placeholder from the stack */ +} + + +LUALIB_API void luaL_pushresultsize (luaL_Buffer *B, size_t sz) { + luaL_addsize(B, sz); + luaL_pushresult(B); +} + + +/* +** 'luaL_addvalue' is the only function in the Buffer system where the +** box (if existent) is not on the top of the stack. So, instead of +** calling 'luaL_addlstring', it replicates the code using -2 as the +** last argument to 'prepbuffsize', signaling that the box is (or will +** be) below the string being added to the buffer. (Box creation can +** trigger an emergency GC, so we should not remove the string from the +** stack before we have the space guaranteed.) +*/ +LUALIB_API void luaL_addvalue (luaL_Buffer *B) { + lua_State *L = B->L; + size_t len; + const char *s = lua_tolstring(L, -1, &len); + char *b = prepbuffsize(B, len, -2); + memcpy(b, s, len * sizeof(char)); + luaL_addsize(B, len); + lua_pop(L, 1); /* pop string */ +} + + +LUALIB_API void luaL_buffinit (lua_State *L, luaL_Buffer *B) { + B->L = L; + B->b = B->init.b; + B->n = 0; + B->size = LUAL_BUFFERSIZE; + lua_pushlightuserdata(L, (void*)B); /* push placeholder */ +} + + +LUALIB_API char *luaL_buffinitsize (lua_State *L, luaL_Buffer *B, size_t sz) { + luaL_buffinit(L, B); + return prepbuffsize(B, sz, -1); +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Reference system +** ======================================================= +*/ + +/* +** The previously freed references form a linked list: t[1] is the index +** of a first free index, t[t[1]] is the index of the second element, +** etc. A zero signals the end of the list. +*/ +LUALIB_API int luaL_ref (lua_State *L, int t) { + int ref; + if (lua_isnil(L, -1)) { + lua_pop(L, 1); /* remove from stack */ + return LUA_REFNIL; /* 'nil' has a unique fixed reference */ + } + t = lua_absindex(L, t); + if (lua_rawgeti(L, t, 1) == LUA_TNUMBER) /* already initialized? */ + ref = (int)lua_tointeger(L, -1); /* ref = t[1] */ + else { /* first access */ + lua_assert(!lua_toboolean(L, -1)); /* must be nil or false */ + ref = 0; /* list is empty */ + lua_pushinteger(L, 0); /* initialize as an empty list */ + lua_rawseti(L, t, 1); /* ref = t[1] = 0 */ + } + lua_pop(L, 1); /* remove element from stack */ + if (ref != 0) { /* any free element? */ + lua_rawgeti(L, t, ref); /* remove it from list */ + lua_rawseti(L, t, 1); /* (t[1] = t[ref]) */ + } + else /* no free elements */ + ref = (int)lua_rawlen(L, t) + 1; /* get a new reference */ + lua_rawseti(L, t, ref); + return ref; +} + + +LUALIB_API void luaL_unref (lua_State *L, int t, int ref) { + if (ref >= 0) { + t = lua_absindex(L, t); + lua_rawgeti(L, t, 1); + lua_assert(lua_isinteger(L, -1)); + lua_rawseti(L, t, ref); /* t[ref] = t[1] */ + lua_pushinteger(L, ref); + lua_rawseti(L, t, 1); /* t[1] = ref */ + } +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Load functions +** ======================================================= +*/ + +typedef struct LoadF { + unsigned n; /* number of pre-read characters */ + FILE *f; /* file being read */ + char buff[BUFSIZ]; /* area for reading file */ +} LoadF; + + +static const char *getF (lua_State *L, void *ud, size_t *size) { + LoadF *lf = (LoadF *)ud; + UNUSED(L); + if (lf->n > 0) { /* are there pre-read characters to be read? */ + *size = lf->n; /* return them (chars already in buffer) */ + lf->n = 0; /* no more pre-read characters */ + } + else { /* read a block from file */ + /* 'fread' can return > 0 *and* set the EOF flag. If next call to + 'getF' called 'fread', it might still wait for user input. + The next check avoids this problem. */ + if (feof(lf->f)) return NULL; + *size = fread(lf->buff, 1, sizeof(lf->buff), lf->f); /* read block */ + } + return lf->buff; +} + + +static int errfile (lua_State *L, const char *what, int fnameindex) { + int err = errno; + const char *filename = lua_tostring(L, fnameindex) + 1; + if (err != 0) + lua_pushfstring(L, "cannot %s %s: %s", what, filename, strerror(err)); + else + lua_pushfstring(L, "cannot %s %s", what, filename); + lua_remove(L, fnameindex); + return LUA_ERRFILE; +} + + +/* +** Skip an optional BOM at the start of a stream. If there is an +** incomplete BOM (the first character is correct but the rest is +** not), returns the first character anyway to force an error +** (as no chunk can start with 0xEF). +*/ +static int skipBOM (FILE *f) { + int c = getc(f); /* read first character */ + if (c == 0xEF && getc(f) == 0xBB && getc(f) == 0xBF) /* correct BOM? */ + return getc(f); /* ignore BOM and return next char */ + else /* no (valid) BOM */ + return c; /* return first character */ +} + + +/* +** reads the first character of file 'f' and skips an optional BOM mark +** in its beginning plus its first line if it starts with '#'. Returns +** true if it skipped the first line. In any case, '*cp' has the +** first "valid" character of the file (after the optional BOM and +** a first-line comment). +*/ +static int skipcomment (FILE *f, int *cp) { + int c = *cp = skipBOM(f); + if (c == '#') { /* first line is a comment (Unix exec. file)? */ + do { /* skip first line */ + c = getc(f); + } while (c != EOF && c != '\n'); + *cp = getc(f); /* next character after comment, if present */ + return 1; /* there was a comment */ + } + else return 0; /* no comment */ +} + + +LUALIB_API int luaL_loadfilex (lua_State *L, const char *filename, + const char *mode) { + LoadF lf; + int status, readstatus; + int c; + int fnameindex = lua_gettop(L) + 1; /* index of filename on the stack */ + if (filename == NULL) { + lua_pushliteral(L, "=stdin"); + lf.f = stdin; + } + else { + lua_pushfstring(L, "@%s", filename); + errno = 0; + lf.f = fopen(filename, "r"); + if (lf.f == NULL) return errfile(L, "open", fnameindex); + } + lf.n = 0; + if (skipcomment(lf.f, &c)) /* read initial portion */ + lf.buff[lf.n++] = '\n'; /* add newline to correct line numbers */ + if (c == LUA_SIGNATURE[0]) { /* binary file? */ + lf.n = 0; /* remove possible newline */ + if (filename) { /* "real" file? */ + errno = 0; + lf.f = freopen(filename, "rb", lf.f); /* reopen in binary mode */ + if (lf.f == NULL) return errfile(L, "reopen", fnameindex); + skipcomment(lf.f, &c); /* re-read initial portion */ + } + } + if (c != EOF) + lf.buff[lf.n++] = cast_char(c); /* 'c' is the first character */ + status = lua_load(L, getF, &lf, lua_tostring(L, -1), mode); + readstatus = ferror(lf.f); + errno = 0; /* no useful error number until here */ + if (filename) fclose(lf.f); /* close file (even in case of errors) */ + if (readstatus) { + lua_settop(L, fnameindex); /* ignore results from 'lua_load' */ + return errfile(L, "read", fnameindex); + } + lua_remove(L, fnameindex); + return status; +} + + +typedef struct LoadS { + const char *s; + size_t size; +} LoadS; + + +static const char *getS (lua_State *L, void *ud, size_t *size) { + LoadS *ls = (LoadS *)ud; + UNUSED(L); + if (ls->size == 0) return NULL; + *size = ls->size; + ls->size = 0; + return ls->s; +} + + +LUALIB_API int luaL_loadbufferx (lua_State *L, const char *buff, size_t size, + const char *name, const char *mode) { + LoadS ls; + ls.s = buff; + ls.size = size; + return lua_load(L, getS, &ls, name, mode); +} + + +LUALIB_API int luaL_loadstring (lua_State *L, const char *s) { + return luaL_loadbufferx(L, s, strlen(s), s, "t"); +} + +/* }====================================================== */ + + + +LUALIB_API int luaL_getmetafield (lua_State *L, int obj, const char *event) { + if (!lua_getmetatable(L, obj)) /* no metatable? */ + return LUA_TNIL; + else { + int tt; + lua_pushstring(L, event); + tt = lua_rawget(L, -2); + if (tt == LUA_TNIL) /* is metafield nil? */ + lua_pop(L, 2); /* remove metatable and metafield */ + else + lua_remove(L, -2); /* remove only metatable */ + return tt; /* return metafield type */ + } +} + + +LUALIB_API int luaL_callmeta (lua_State *L, int obj, const char *event) { + obj = lua_absindex(L, obj); + if (luaL_getmetafield(L, obj, event) == LUA_TNIL) /* no metafield? */ + return 0; + lua_pushvalue(L, obj); + lua_call(L, 1, 1); + return 1; +} + + +LUALIB_API lua_Integer luaL_len (lua_State *L, int idx) { + lua_Integer l; + int isnum; + lua_len(L, idx); + l = lua_tointegerx(L, -1, &isnum); + if (l_unlikely(!isnum)) + luaL_error(L, "object length is not an integer"); + lua_pop(L, 1); /* remove object */ + return l; +} + + +LUALIB_API const char *luaL_tolstring (lua_State *L, int idx, size_t *len) { + idx = lua_absindex(L,idx); + if (luaL_callmeta(L, idx, "__tostring")) { /* metafield? */ + if (!lua_isstring(L, -1)) + luaL_error(L, "'__tostring' must return a string"); + } + else { + switch (lua_type(L, idx)) { + case LUA_TNUMBER: { + char buff[LUA_N2SBUFFSZ]; + lua_numbertocstring(L, idx, buff); + lua_pushstring(L, buff); + break; + } + case LUA_TSTRING: + lua_pushvalue(L, idx); + break; + case LUA_TBOOLEAN: + lua_pushstring(L, (lua_toboolean(L, idx) ? "true" : "false")); + break; + case LUA_TNIL: + lua_pushliteral(L, "nil"); + break; + default: { + int tt = luaL_getmetafield(L, idx, "__name"); /* try name */ + const char *kind = (tt == LUA_TSTRING) ? lua_tostring(L, -1) : + luaL_typename(L, idx); + lua_pushfstring(L, "%s: %p", kind, lua_topointer(L, idx)); + if (tt != LUA_TNIL) + lua_remove(L, -2); /* remove '__name' */ + break; + } + } + } + return lua_tolstring(L, -1, len); +} + + +/* +** set functions from list 'l' into table at top - 'nup'; each +** function gets the 'nup' elements at the top as upvalues. +** Returns with only the table at the stack. +*/ +LUALIB_API void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) { + luaL_checkstack(L, nup, "too many upvalues"); + for (; l->name != NULL; l++) { /* fill the table with given functions */ + if (l->func == NULL) /* placeholder? */ + lua_pushboolean(L, 0); + else { + int i; + for (i = 0; i < nup; i++) /* copy upvalues to the top */ + lua_pushvalue(L, -nup); + lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */ + } + lua_setfield(L, -(nup + 2), l->name); + } + lua_pop(L, nup); /* remove upvalues */ +} + + +/* +** ensure that stack[idx][fname] has a table and push that table +** into the stack +*/ +LUALIB_API int luaL_getsubtable (lua_State *L, int idx, const char *fname) { + if (lua_getfield(L, idx, fname) == LUA_TTABLE) + return 1; /* table already there */ + else { + lua_pop(L, 1); /* remove previous result */ + idx = lua_absindex(L, idx); + lua_newtable(L); + lua_pushvalue(L, -1); /* copy to be left at top */ + lua_setfield(L, idx, fname); /* assign new table to field */ + return 0; /* false, because did not find table there */ + } +} + + +/* +** Stripped-down 'require': After checking "loaded" table, calls 'openf' +** to open a module, registers the result in 'package.loaded' table and, +** if 'glb' is true, also registers the result in the global table. +** Leaves resulting module on the top. +*/ +LUALIB_API void luaL_requiref (lua_State *L, const char *modname, + lua_CFunction openf, int glb) { + luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE); + lua_getfield(L, -1, modname); /* LOADED[modname] */ + if (!lua_toboolean(L, -1)) { /* package not already loaded? */ + lua_pop(L, 1); /* remove field */ + lua_pushcfunction(L, openf); + lua_pushstring(L, modname); /* argument to open function */ + lua_call(L, 1, 1); /* call 'openf' to open module */ + lua_pushvalue(L, -1); /* make copy of module (call result) */ + lua_setfield(L, -3, modname); /* LOADED[modname] = module */ + } + lua_remove(L, -2); /* remove LOADED table */ + if (glb) { + lua_pushvalue(L, -1); /* copy of module */ + lua_setglobal(L, modname); /* _G[modname] = module */ + } +} + + +LUALIB_API void luaL_addgsub (luaL_Buffer *b, const char *s, + const char *p, const char *r) { + const char *wild; + size_t l = strlen(p); + while ((wild = strstr(s, p)) != NULL) { + luaL_addlstring(b, s, ct_diff2sz(wild - s)); /* push prefix */ + luaL_addstring(b, r); /* push replacement in place of pattern */ + s = wild + l; /* continue after 'p' */ + } + luaL_addstring(b, s); /* push last suffix */ +} + + +LUALIB_API const char *luaL_gsub (lua_State *L, const char *s, + const char *p, const char *r) { + luaL_Buffer b; + luaL_buffinit(L, &b); + luaL_addgsub(&b, s, p, r); + luaL_pushresult(&b); + return lua_tostring(L, -1); +} + + +void *luaL_alloc (void *ud, void *ptr, size_t osize, size_t nsize) { + UNUSED(ud); UNUSED(osize); + if (nsize == 0) { + free(ptr); + return NULL; + } + else + return realloc(ptr, nsize); +} + + +/* +** Standard panic function just prints an error message. The test +** with 'lua_type' avoids possible memory errors in 'lua_tostring'. +*/ +static int panic (lua_State *L) { + const char *msg = (lua_type(L, -1) == LUA_TSTRING) + ? lua_tostring(L, -1) + : "error object is not a string"; + lua_writestringerror("PANIC: unprotected error in call to Lua API (%s)\n", + msg); + return 0; /* return to Lua to abort */ +} + + +/* +** Warning functions: +** warnfoff: warning system is off +** warnfon: ready to start a new message +** warnfcont: previous message is to be continued +*/ +static void warnfoff (void *ud, const char *message, int tocont); +static void warnfon (void *ud, const char *message, int tocont); +static void warnfcont (void *ud, const char *message, int tocont); + + +/* +** Check whether message is a control message. If so, execute the +** control or ignore it if unknown. +*/ +static int checkcontrol (lua_State *L, const char *message, int tocont) { + if (tocont || *(message++) != '@') /* not a control message? */ + return 0; + else { + if (strcmp(message, "off") == 0) + lua_setwarnf(L, warnfoff, L); /* turn warnings off */ + else if (strcmp(message, "on") == 0) + lua_setwarnf(L, warnfon, L); /* turn warnings on */ + return 1; /* it was a control message */ + } +} + + +static void warnfoff (void *ud, const char *message, int tocont) { + checkcontrol((lua_State *)ud, message, tocont); +} + + +/* +** Writes the message and handle 'tocont', finishing the message +** if needed and setting the next warn function. +*/ +static void warnfcont (void *ud, const char *message, int tocont) { + lua_State *L = (lua_State *)ud; + lua_writestringerror("%s", message); /* write message */ + if (tocont) /* not the last part? */ + lua_setwarnf(L, warnfcont, L); /* to be continued */ + else { /* last part */ + lua_writestringerror("%s", "\n"); /* finish message with end-of-line */ + lua_setwarnf(L, warnfon, L); /* next call is a new message */ + } +} + + +static void warnfon (void *ud, const char *message, int tocont) { + if (checkcontrol((lua_State *)ud, message, tocont)) /* control message? */ + return; /* nothing else to be done */ + lua_writestringerror("%s", "Lua warning: "); /* start a new warning */ + warnfcont(ud, message, tocont); /* finish processing */ +} + + + +/* +** A function to compute an unsigned int with some level of +** randomness. Rely on Address Space Layout Randomization (if present) +** and the current time. +*/ +#if !defined(luai_makeseed) + +#include + + +/* Size for the buffer, in bytes */ +#define BUFSEEDB (sizeof(void*) + sizeof(time_t)) + +/* Size for the buffer in int's, rounded up */ +#define BUFSEED ((BUFSEEDB + sizeof(int) - 1) / sizeof(int)) + +/* +** Copy the contents of variable 'v' into the buffer pointed by 'b'. +** (The '&b[0]' disguises 'b' to fix an absurd warning from clang.) +*/ +#define addbuff(b,v) (memcpy(&b[0], &(v), sizeof(v)), b += sizeof(v)) + + +static unsigned int luai_makeseed (void) { + unsigned int buff[BUFSEED]; + unsigned int res; + unsigned int i; + time_t t = time(NULL); + char *b = (char*)buff; + addbuff(b, b); /* local variable's address */ + addbuff(b, t); /* time */ + /* fill (rare but possible) remain of the buffer with zeros */ + memset(b, 0, sizeof(buff) - BUFSEEDB); + res = buff[0]; + for (i = 1; i < BUFSEED; i++) + res ^= (res >> 3) + (res << 7) + buff[i]; + return res; +} + +#endif + + +LUALIB_API unsigned int luaL_makeseed (lua_State *L) { + UNUSED(L); + return luai_makeseed(); +} + + +/* +** Use the name with parentheses so that headers can redefine it +** as a macro. +*/ +LUALIB_API lua_State *(luaL_newstate) (void) { + lua_State *L = lua_newstate(luaL_alloc, NULL, luaL_makeseed(NULL)); + if (l_likely(L)) { + lua_atpanic(L, &panic); + lua_setwarnf(L, warnfon, L); + } + return L; +} + + +LUALIB_API void luaL_checkversion_ (lua_State *L, lua_Number ver, size_t sz) { + lua_Number v = lua_version(L); + if (sz != LUAL_NUMSIZES) /* check numeric types */ + luaL_error(L, "core and library have incompatible numeric types"); + else if (v != ver) + luaL_error(L, "version mismatch: app. needs %f, Lua core provides %f", + (LUAI_UACNUMBER)ver, (LUAI_UACNUMBER)v); +} diff --git a/crates/lua/vendor/lua/lauxlib.h b/crates/lua/vendor/lua/lauxlib.h new file mode 100644 index 0000000..c05ee12 --- /dev/null +++ b/crates/lua/vendor/lua/lauxlib.h @@ -0,0 +1,269 @@ +/* +** $Id: lauxlib.h $ +** Auxiliary functions for building Lua libraries +** See Copyright Notice in lua.h +*/ + + +#ifndef lauxlib_h +#define lauxlib_h + + +#include +#include + +#include "luaconf.h" +#include "lua.h" + + +/* global table */ +#define LUA_GNAME "_G" + + +typedef struct luaL_Buffer luaL_Buffer; + + +/* extra error code for 'luaL_loadfilex' */ +#define LUA_ERRFILE (LUA_ERRERR+1) + + +/* key, in the registry, for table of loaded modules */ +#define LUA_LOADED_TABLE "_LOADED" + + +/* key, in the registry, for table of preloaded loaders */ +#define LUA_PRELOAD_TABLE "_PRELOAD" + + +typedef struct luaL_Reg { + const char *name; + lua_CFunction func; +} luaL_Reg; + + +#define LUAL_NUMSIZES (sizeof(lua_Integer)*16 + sizeof(lua_Number)) + +LUALIB_API void (luaL_checkversion_) (lua_State *L, lua_Number ver, size_t sz); +#define luaL_checkversion(L) \ + luaL_checkversion_(L, LUA_VERSION_NUM, LUAL_NUMSIZES) + +LUALIB_API int (luaL_getmetafield) (lua_State *L, int obj, const char *e); +LUALIB_API int (luaL_callmeta) (lua_State *L, int obj, const char *e); +LUALIB_API const char *(luaL_tolstring) (lua_State *L, int idx, size_t *len); +LUALIB_API int (luaL_argerror) (lua_State *L, int arg, const char *extramsg); +LUALIB_API int (luaL_typeerror) (lua_State *L, int arg, const char *tname); +LUALIB_API const char *(luaL_checklstring) (lua_State *L, int arg, + size_t *l); +LUALIB_API const char *(luaL_optlstring) (lua_State *L, int arg, + const char *def, size_t *l); +LUALIB_API lua_Number (luaL_checknumber) (lua_State *L, int arg); +LUALIB_API lua_Number (luaL_optnumber) (lua_State *L, int arg, lua_Number def); + +LUALIB_API lua_Integer (luaL_checkinteger) (lua_State *L, int arg); +LUALIB_API lua_Integer (luaL_optinteger) (lua_State *L, int arg, + lua_Integer def); + +LUALIB_API void (luaL_checkstack) (lua_State *L, int sz, const char *msg); +LUALIB_API void (luaL_checktype) (lua_State *L, int arg, int t); +LUALIB_API void (luaL_checkany) (lua_State *L, int arg); + +LUALIB_API int (luaL_newmetatable) (lua_State *L, const char *tname); +LUALIB_API void (luaL_setmetatable) (lua_State *L, const char *tname); +LUALIB_API void *(luaL_testudata) (lua_State *L, int ud, const char *tname); +LUALIB_API void *(luaL_checkudata) (lua_State *L, int ud, const char *tname); + +LUALIB_API void (luaL_where) (lua_State *L, int lvl); +LUALIB_API int (luaL_error) (lua_State *L, const char *fmt, ...); + +LUALIB_API int (luaL_checkoption) (lua_State *L, int arg, const char *def, + const char *const lst[]); + +LUALIB_API int (luaL_fileresult) (lua_State *L, int stat, const char *fname); +LUALIB_API int (luaL_execresult) (lua_State *L, int stat); + +LUALIB_API void *(luaL_alloc) (void *ud, void *ptr, size_t osize, + size_t nsize); + + +/* predefined references */ +#define LUA_NOREF (-2) +#define LUA_REFNIL (-1) + +LUALIB_API int (luaL_ref) (lua_State *L, int t); +LUALIB_API void (luaL_unref) (lua_State *L, int t, int ref); + +LUALIB_API int (luaL_loadfilex) (lua_State *L, const char *filename, + const char *mode); + +#define luaL_loadfile(L,f) luaL_loadfilex(L,f,NULL) + +LUALIB_API int (luaL_loadbufferx) (lua_State *L, const char *buff, size_t sz, + const char *name, const char *mode); +LUALIB_API int (luaL_loadstring) (lua_State *L, const char *s); + +LUALIB_API lua_State *(luaL_newstate) (void); + +LUALIB_API unsigned (luaL_makeseed) (lua_State *L); + +LUALIB_API lua_Integer (luaL_len) (lua_State *L, int idx); + +LUALIB_API void (luaL_addgsub) (luaL_Buffer *b, const char *s, + const char *p, const char *r); +LUALIB_API const char *(luaL_gsub) (lua_State *L, const char *s, + const char *p, const char *r); + +LUALIB_API void (luaL_setfuncs) (lua_State *L, const luaL_Reg *l, int nup); + +LUALIB_API int (luaL_getsubtable) (lua_State *L, int idx, const char *fname); + +LUALIB_API void (luaL_traceback) (lua_State *L, lua_State *L1, + const char *msg, int level); + +LUALIB_API void (luaL_requiref) (lua_State *L, const char *modname, + lua_CFunction openf, int glb); + +/* +** =============================================================== +** some useful macros +** =============================================================== +*/ + + +#define luaL_newlibtable(L,l) \ + lua_createtable(L, 0, sizeof(l)/sizeof((l)[0]) - 1) + +#define luaL_newlib(L,l) \ + (luaL_checkversion(L), luaL_newlibtable(L,l), luaL_setfuncs(L,l,0)) + +#define luaL_argcheck(L, cond,arg,extramsg) \ + ((void)(luai_likely(cond) || luaL_argerror(L, (arg), (extramsg)))) + +#define luaL_argexpected(L,cond,arg,tname) \ + ((void)(luai_likely(cond) || luaL_typeerror(L, (arg), (tname)))) + +#define luaL_checkstring(L,n) (luaL_checklstring(L, (n), NULL)) +#define luaL_optstring(L,n,d) (luaL_optlstring(L, (n), (d), NULL)) + +#define luaL_typename(L,i) lua_typename(L, lua_type(L,(i))) + +#define luaL_dofile(L, fn) \ + (luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0)) + +#define luaL_dostring(L, s) \ + (luaL_loadstring(L, s) || lua_pcall(L, 0, LUA_MULTRET, 0)) + +#define luaL_getmetatable(L,n) (lua_getfield(L, LUA_REGISTRYINDEX, (n))) + +#define luaL_opt(L,f,n,d) (lua_isnoneornil(L,(n)) ? (d) : f(L,(n))) + +#define luaL_loadbuffer(L,s,sz,n) luaL_loadbufferx(L,s,sz,n,NULL) + + +/* +** Perform arithmetic operations on lua_Integer values with wrap-around +** semantics, as the Lua core does. +*/ +#define luaL_intop(op,v1,v2) \ + ((lua_Integer)((lua_Unsigned)(v1) op (lua_Unsigned)(v2))) + + +/* push the value used to represent failure/error */ +#if defined(LUA_FAILISFALSE) +#define luaL_pushfail(L) lua_pushboolean(L, 0) +#else +#define luaL_pushfail(L) lua_pushnil(L) +#endif + + + +/* +** {====================================================== +** Generic Buffer manipulation +** ======================================================= +*/ + +struct luaL_Buffer { + char *b; /* buffer address */ + size_t size; /* buffer size */ + size_t n; /* number of characters in buffer */ + lua_State *L; + union { + LUAI_MAXALIGN; /* ensure maximum alignment for buffer */ + char b[LUAL_BUFFERSIZE]; /* initial buffer */ + } init; +}; + + +#define luaL_bufflen(bf) ((bf)->n) +#define luaL_buffaddr(bf) ((bf)->b) + + +#define luaL_addchar(B,c) \ + ((void)((B)->n < (B)->size || luaL_prepbuffsize((B), 1)), \ + ((B)->b[(B)->n++] = (c))) + +#define luaL_addsize(B,s) ((B)->n += (s)) + +#define luaL_buffsub(B,s) ((B)->n -= (s)) + +LUALIB_API void (luaL_buffinit) (lua_State *L, luaL_Buffer *B); +LUALIB_API char *(luaL_prepbuffsize) (luaL_Buffer *B, size_t sz); +LUALIB_API void (luaL_addlstring) (luaL_Buffer *B, const char *s, size_t l); +LUALIB_API void (luaL_addstring) (luaL_Buffer *B, const char *s); +LUALIB_API void (luaL_addvalue) (luaL_Buffer *B); +LUALIB_API void (luaL_pushresult) (luaL_Buffer *B); +LUALIB_API void (luaL_pushresultsize) (luaL_Buffer *B, size_t sz); +LUALIB_API char *(luaL_buffinitsize) (lua_State *L, luaL_Buffer *B, size_t sz); + +#define luaL_prepbuffer(B) luaL_prepbuffsize(B, LUAL_BUFFERSIZE) + +/* }====================================================== */ + + + +/* +** {====================================================== +** File handles for IO library +** ======================================================= +*/ + +/* +** A file handle is a userdata with metatable 'LUA_FILEHANDLE' and +** initial structure 'luaL_Stream' (it may contain other fields +** after that initial structure). +*/ + +#define LUA_FILEHANDLE "FILE*" + + +typedef struct luaL_Stream { + FILE *f; /* stream (NULL for incompletely created streams) */ + lua_CFunction closef; /* to close stream (NULL for closed streams) */ +} luaL_Stream; + +/* }====================================================== */ + + +/* +** {============================================================ +** Compatibility with deprecated conversions +** ============================================================= +*/ +#if defined(LUA_COMPAT_APIINTCASTS) + +#define luaL_checkunsigned(L,a) ((lua_Unsigned)luaL_checkinteger(L,a)) +#define luaL_optunsigned(L,a,d) \ + ((lua_Unsigned)luaL_optinteger(L,a,(lua_Integer)(d))) + +#define luaL_checkint(L,n) ((int)luaL_checkinteger(L, (n))) +#define luaL_optint(L,n,d) ((int)luaL_optinteger(L, (n), (d))) + +#define luaL_checklong(L,n) ((long)luaL_checkinteger(L, (n))) +#define luaL_optlong(L,n,d) ((long)luaL_optinteger(L, (n), (d))) + +#endif +/* }============================================================ */ + + + +#endif diff --git a/crates/lua/vendor/lua/lbaselib.c b/crates/lua/vendor/lua/lbaselib.c new file mode 100644 index 0000000..35569ef --- /dev/null +++ b/crates/lua/vendor/lua/lbaselib.c @@ -0,0 +1,551 @@ +/* +** $Id: lbaselib.c $ +** Basic library +** See Copyright Notice in lua.h +*/ + +#define lbaselib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include +#include +#include +#include + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" +#include "llimits.h" + + +static int luaB_print (lua_State *L) { + int n = lua_gettop(L); /* number of arguments */ + int i; + for (i = 1; i <= n; i++) { /* for each argument */ + size_t l; + const char *s = luaL_tolstring(L, i, &l); /* convert it to string */ + if (i > 1) /* not the first element? */ + lua_writestring("\t", 1); /* add a tab before it */ + lua_writestring(s, l); /* print it */ + lua_pop(L, 1); /* pop result */ + } + lua_writeline(); + return 0; +} + + +/* +** Creates a warning with all given arguments. +** Check first for errors; otherwise an error may interrupt +** the composition of a warning, leaving it unfinished. +*/ +static int luaB_warn (lua_State *L) { + int n = lua_gettop(L); /* number of arguments */ + int i; + luaL_checkstring(L, 1); /* at least one argument */ + for (i = 2; i <= n; i++) + luaL_checkstring(L, i); /* make sure all arguments are strings */ + for (i = 1; i < n; i++) /* compose warning */ + lua_warning(L, lua_tostring(L, i), 1); + lua_warning(L, lua_tostring(L, n), 0); /* close warning */ + return 0; +} + + +#define SPACECHARS " \f\n\r\t\v" + +static const char *b_str2int (const char *s, unsigned base, lua_Integer *pn) { + lua_Unsigned n = 0; + int neg = 0; + s += strspn(s, SPACECHARS); /* skip initial spaces */ + if (*s == '-') { s++; neg = 1; } /* handle sign */ + else if (*s == '+') s++; + if (!isalnum(cast_uchar(*s))) /* no digit? */ + return NULL; + do { + unsigned digit = cast_uint(isdigit(cast_uchar(*s)) + ? *s - '0' + : (toupper(cast_uchar(*s)) - 'A') + 10); + if (digit >= base) return NULL; /* invalid numeral */ + n = n * base + digit; + s++; + } while (isalnum(cast_uchar(*s))); + s += strspn(s, SPACECHARS); /* skip trailing spaces */ + *pn = (lua_Integer)((neg) ? (0u - n) : n); + return s; +} + + +static int luaB_tonumber (lua_State *L) { + if (lua_isnoneornil(L, 2)) { /* standard conversion? */ + if (lua_type(L, 1) == LUA_TNUMBER) { /* already a number? */ + lua_settop(L, 1); /* yes; return it */ + return 1; + } + else { + size_t l; + const char *s = lua_tolstring(L, 1, &l); + if (s != NULL && lua_stringtonumber(L, s) == l + 1) + return 1; /* successful conversion to number */ + /* else not a number */ + luaL_checkany(L, 1); /* (but there must be some parameter) */ + } + } + else { + size_t l; + const char *s; + lua_Integer n = 0; /* to avoid warnings */ + lua_Integer base = luaL_checkinteger(L, 2); + luaL_checktype(L, 1, LUA_TSTRING); /* no numbers as strings */ + s = lua_tolstring(L, 1, &l); + luaL_argcheck(L, 2 <= base && base <= 36, 2, "base out of range"); + if (b_str2int(s, cast_uint(base), &n) == s + l) { + lua_pushinteger(L, n); + return 1; + } /* else not a number */ + } /* else not a number */ + luaL_pushfail(L); /* not a number */ + return 1; +} + + +static int luaB_error (lua_State *L) { + int level = (int)luaL_optinteger(L, 2, 1); + lua_settop(L, 1); + if (lua_type(L, 1) == LUA_TSTRING && level > 0) { + luaL_where(L, level); /* add extra information */ + lua_pushvalue(L, 1); + lua_concat(L, 2); + } + return lua_error(L); +} + + +static int luaB_getmetatable (lua_State *L) { + luaL_checkany(L, 1); + if (!lua_getmetatable(L, 1)) { + lua_pushnil(L); + return 1; /* no metatable */ + } + luaL_getmetafield(L, 1, "__metatable"); + return 1; /* returns either __metatable field (if present) or metatable */ +} + + +static int luaB_setmetatable (lua_State *L) { + int t = lua_type(L, 2); + luaL_checktype(L, 1, LUA_TTABLE); + luaL_argexpected(L, t == LUA_TNIL || t == LUA_TTABLE, 2, "nil or table"); + if (l_unlikely(luaL_getmetafield(L, 1, "__metatable") != LUA_TNIL)) + return luaL_error(L, "cannot change a protected metatable"); + lua_settop(L, 2); + lua_setmetatable(L, 1); + return 1; +} + + +static int luaB_rawequal (lua_State *L) { + luaL_checkany(L, 1); + luaL_checkany(L, 2); + lua_pushboolean(L, lua_rawequal(L, 1, 2)); + return 1; +} + + +static int luaB_rawlen (lua_State *L) { + int t = lua_type(L, 1); + luaL_argexpected(L, t == LUA_TTABLE || t == LUA_TSTRING, 1, + "table or string"); + lua_pushinteger(L, l_castU2S(lua_rawlen(L, 1))); + return 1; +} + + +static int luaB_rawget (lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + luaL_checkany(L, 2); + lua_settop(L, 2); + lua_rawget(L, 1); + return 1; +} + +static int luaB_rawset (lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + luaL_checkany(L, 2); + luaL_checkany(L, 3); + lua_settop(L, 3); + lua_rawset(L, 1); + return 1; +} + + +static int pushmode (lua_State *L, int oldmode) { + if (oldmode == -1) + luaL_pushfail(L); /* invalid call to 'lua_gc' */ + else + lua_pushstring(L, (oldmode == LUA_GCINC) ? "incremental" + : "generational"); + return 1; +} + + +/* +** check whether call to 'lua_gc' was valid (not inside a finalizer) +*/ +#define checkvalres(res) { if (res == -1) break; } + +static int luaB_collectgarbage (lua_State *L) { + static const char *const opts[] = {"stop", "restart", "collect", + "count", "step", "isrunning", "generational", "incremental", + "param", NULL}; + static const char optsnum[] = {LUA_GCSTOP, LUA_GCRESTART, LUA_GCCOLLECT, + LUA_GCCOUNT, LUA_GCSTEP, LUA_GCISRUNNING, LUA_GCGEN, LUA_GCINC, + LUA_GCPARAM}; + int o = optsnum[luaL_checkoption(L, 1, "collect", opts)]; + switch (o) { + case LUA_GCCOUNT: { + int k = lua_gc(L, o); + int b = lua_gc(L, LUA_GCCOUNTB); + checkvalres(k); + lua_pushnumber(L, (lua_Number)k + ((lua_Number)b/1024)); + return 1; + } + case LUA_GCSTEP: { + lua_Integer n = luaL_optinteger(L, 2, 0); + int res = lua_gc(L, o, cast_sizet(n)); + checkvalres(res); + lua_pushboolean(L, res); + return 1; + } + case LUA_GCISRUNNING: { + int res = lua_gc(L, o); + checkvalres(res); + lua_pushboolean(L, res); + return 1; + } + case LUA_GCGEN: { + return pushmode(L, lua_gc(L, o)); + } + case LUA_GCINC: { + return pushmode(L, lua_gc(L, o)); + } + case LUA_GCPARAM: { + static const char *const params[] = { + "minormul", "majorminor", "minormajor", + "pause", "stepmul", "stepsize", NULL}; + static const char pnum[] = { + LUA_GCPMINORMUL, LUA_GCPMAJORMINOR, LUA_GCPMINORMAJOR, + LUA_GCPPAUSE, LUA_GCPSTEPMUL, LUA_GCPSTEPSIZE}; + int p = pnum[luaL_checkoption(L, 2, NULL, params)]; + lua_Integer value = luaL_optinteger(L, 3, -1); + lua_pushinteger(L, lua_gc(L, o, p, (int)value)); + return 1; + } + default: { + int res = lua_gc(L, o); + checkvalres(res); + lua_pushinteger(L, res); + return 1; + } + } + luaL_pushfail(L); /* invalid call (inside a finalizer) */ + return 1; +} + + +static int luaB_type (lua_State *L) { + int t = lua_type(L, 1); + luaL_argcheck(L, t != LUA_TNONE, 1, "value expected"); + lua_pushstring(L, lua_typename(L, t)); + return 1; +} + + +static int luaB_next (lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + lua_settop(L, 2); /* create a 2nd argument if there isn't one */ + if (lua_next(L, 1)) + return 2; + else { + lua_pushnil(L); + return 1; + } +} + + +static int pairscont (lua_State *L, int status, lua_KContext k) { + (void)L; (void)status; (void)k; /* unused */ + return 4; /* __pairs did all the work, just return its results */ +} + +static int luaB_pairs (lua_State *L) { + luaL_checkany(L, 1); + if (luaL_getmetafield(L, 1, "__pairs") == LUA_TNIL) { /* no metamethod? */ + lua_pushcfunction(L, luaB_next); /* will return generator and */ + lua_pushvalue(L, 1); /* state */ + lua_pushnil(L); /* initial value */ + lua_pushnil(L); /* to-be-closed object */ + } + else { + lua_pushvalue(L, 1); /* argument 'self' to metamethod */ + lua_callk(L, 1, 4, 0, pairscont); /* get 4 values from metamethod */ + } + return 4; +} + + +/* +** Traversal function for 'ipairs' +*/ +static int ipairsaux (lua_State *L) { + lua_Integer i = luaL_checkinteger(L, 2); + i = luaL_intop(+, i, 1); + lua_pushinteger(L, i); + return (lua_geti(L, 1, i) == LUA_TNIL) ? 1 : 2; +} + + +/* +** 'ipairs' function. Returns 'ipairsaux', given "table", 0. +** (The given "table" may not be a table.) +*/ +static int luaB_ipairs (lua_State *L) { + luaL_checkany(L, 1); + lua_pushcfunction(L, ipairsaux); /* iteration function */ + lua_pushvalue(L, 1); /* state */ + lua_pushinteger(L, 0); /* initial value */ + return 3; +} + + +static int load_aux (lua_State *L, int status, int envidx) { + if (l_likely(status == LUA_OK)) { + if (envidx != 0) { /* 'env' parameter? */ + lua_pushvalue(L, envidx); /* environment for loaded function */ + if (!lua_setupvalue(L, -2, 1)) /* set it as 1st upvalue */ + lua_pop(L, 1); /* remove 'env' if not used by previous call */ + } + return 1; + } + else { /* error (message is on top of the stack) */ + luaL_pushfail(L); + lua_insert(L, -2); /* put before error message */ + return 2; /* return fail plus error message */ + } +} + + +static const char *getMode (lua_State *L, int idx) { + const char *mode = luaL_optstring(L, idx, NULL); + if (mode != NULL && strchr(mode, 'B') != NULL) { + /* Lua code cannot use fixed buffers */ + luaL_argerror(L, idx, "invalid mode"); + } + return mode; +} + + +static int luaB_loadfile (lua_State *L) { + const char *fname = luaL_optstring(L, 1, NULL); + const char *mode = getMode(L, 2); + int env = (!lua_isnone(L, 3) ? 3 : 0); /* 'env' index or 0 if no 'env' */ + int status = luaL_loadfilex(L, fname, mode); + return load_aux(L, status, env); +} + + +/* +** {====================================================== +** Generic Read function +** ======================================================= +*/ + + +/* +** Reader for generic 'load' function. +*/ +static const char *generic_reader (lua_State *L, void *ud, size_t *size) { + int *firstcall = cast(int *, ud); + luaL_checkstack(L, 2, "too many nested functions"); + if (*firstcall) + *firstcall = 0; + else + lua_pop(L, 1); /* remove previous result */ + lua_pushvalue(L, 1); /* get function */ + lua_call(L, 0, 1); /* call it */ + if (lua_isnil(L, -1)) { + *size = 0; + return NULL; + } + else if (l_unlikely(!lua_isstring(L, -1))) + luaL_error(L, "reader function must return a string"); + return lua_tolstring(L, -1, size); +} + + +static int luaB_load (lua_State *L) { + int status; + size_t l; + const char *s = lua_tolstring(L, 1, &l); + const char *mode = getMode(L, 3); + int env = (!lua_isnone(L, 4) ? 4 : 0); /* 'env' index or 0 if no 'env' */ + if (s != NULL) { /* loading a string? */ + const char *chunkname = luaL_optstring(L, 2, s); + status = luaL_loadbufferx(L, s, l, chunkname, mode); + } + else { /* loading from a reader function */ + int firstcall = 1; /* userdata for generic_reader */ + const char *chunkname = luaL_optstring(L, 2, "=(load)"); + luaL_checktype(L, 1, LUA_TFUNCTION); + status = lua_load(L, generic_reader, &firstcall, chunkname, mode); + } + return load_aux(L, status, env); +} + +/* }====================================================== */ + + +static int dofilecont (lua_State *L, int d1, lua_KContext d2) { + (void)d1; (void)d2; /* only to match 'lua_Kfunction' prototype */ + return lua_gettop(L) - 1; +} + + +static int luaB_dofile (lua_State *L) { + const char *fname = luaL_optstring(L, 1, NULL); + lua_settop(L, 1); + if (l_unlikely(luaL_loadfilex(L, fname, "bt") != LUA_OK)) + return lua_error(L); + lua_callk(L, 0, LUA_MULTRET, 0, dofilecont); + return dofilecont(L, 0, 0); +} + + +static int luaB_assert (lua_State *L) { + if (l_likely(lua_toboolean(L, 1))) /* condition is true? */ + return lua_gettop(L); /* return all arguments */ + else { /* error */ + luaL_checkany(L, 1); /* there must be a condition */ + lua_remove(L, 1); /* remove it */ + lua_pushliteral(L, "assertion failed!"); /* default message */ + lua_settop(L, 1); /* leave only message (default if no other one) */ + return luaB_error(L); /* call 'error' */ + } +} + + +static int luaB_select (lua_State *L) { + int n = lua_gettop(L); + if (lua_type(L, 1) == LUA_TSTRING && *lua_tostring(L, 1) == '#') { + lua_pushinteger(L, n-1); + return 1; + } + else { + lua_Integer i = luaL_checkinteger(L, 1); + if (i < 0) i = n + i; + else if (i > n) i = n; + luaL_argcheck(L, 1 <= i, 1, "index out of range"); + return n - (int)i; + } +} + + +/* +** Continuation function for 'pcall' and 'xpcall'. Both functions +** already pushed a 'true' before doing the call, so in case of success +** 'finishpcall' only has to return everything in the stack minus +** 'extra' values (where 'extra' is exactly the number of items to be +** ignored). +*/ +static int finishpcall (lua_State *L, int status, lua_KContext extra) { + if (l_unlikely(status != LUA_OK && status != LUA_YIELD)) { /* error? */ + lua_pushboolean(L, 0); /* first result (false) */ + lua_pushvalue(L, -2); /* error message */ + return 2; /* return false, msg */ + } + else + return lua_gettop(L) - (int)extra; /* return all results */ +} + + +static int luaB_pcall (lua_State *L) { + int status; + luaL_checkany(L, 1); + lua_pushboolean(L, 1); /* first result if no errors */ + lua_insert(L, 1); /* put it in place */ + status = lua_pcallk(L, lua_gettop(L) - 2, LUA_MULTRET, 0, 0, finishpcall); + return finishpcall(L, status, 0); +} + + +/* +** Do a protected call with error handling. After 'lua_rotate', the +** stack will have ; so, the function passes +** 2 to 'finishpcall' to skip the 2 first values when returning results. +*/ +static int luaB_xpcall (lua_State *L) { + int status; + int n = lua_gettop(L); + luaL_checktype(L, 2, LUA_TFUNCTION); /* check error function */ + lua_pushboolean(L, 1); /* first result */ + lua_pushvalue(L, 1); /* function */ + lua_rotate(L, 3, 2); /* move them below function's arguments */ + status = lua_pcallk(L, n - 2, LUA_MULTRET, 2, 2, finishpcall); + return finishpcall(L, status, 2); +} + + +static int luaB_tostring (lua_State *L) { + luaL_checkany(L, 1); + luaL_tolstring(L, 1, NULL); + return 1; +} + + +static const luaL_Reg base_funcs[] = { + {"assert", luaB_assert}, + {"collectgarbage", luaB_collectgarbage}, + {"dofile", luaB_dofile}, + {"error", luaB_error}, + {"getmetatable", luaB_getmetatable}, + {"ipairs", luaB_ipairs}, + {"loadfile", luaB_loadfile}, + {"load", luaB_load}, + {"next", luaB_next}, + {"pairs", luaB_pairs}, + {"pcall", luaB_pcall}, + {"print", luaB_print}, + {"warn", luaB_warn}, + {"rawequal", luaB_rawequal}, + {"rawlen", luaB_rawlen}, + {"rawget", luaB_rawget}, + {"rawset", luaB_rawset}, + {"select", luaB_select}, + {"setmetatable", luaB_setmetatable}, + {"tonumber", luaB_tonumber}, + {"tostring", luaB_tostring}, + {"type", luaB_type}, + {"xpcall", luaB_xpcall}, + /* placeholders */ + {LUA_GNAME, NULL}, + {"_VERSION", NULL}, + {NULL, NULL} +}; + + +LUAMOD_API int luaopen_base (lua_State *L) { + /* open lib into global table */ + lua_pushglobaltable(L); + luaL_setfuncs(L, base_funcs, 0); + /* set global _G */ + lua_pushvalue(L, -1); + lua_setfield(L, -2, LUA_GNAME); + /* set global _VERSION */ + lua_pushliteral(L, LUA_VERSION); + lua_setfield(L, -2, "_VERSION"); + return 1; +} diff --git a/crates/lua/vendor/lua/lcode.c b/crates/lua/vendor/lua/lcode.c new file mode 100644 index 0000000..8b61d5d --- /dev/null +++ b/crates/lua/vendor/lua/lcode.c @@ -0,0 +1,1970 @@ +/* +** $Id: lcode.c $ +** Code generator for Lua +** See Copyright Notice in lua.h +*/ + +#define lcode_c +#define LUA_CORE + +#include "lprefix.h" + + +#include +#include +#include +#include + +#include "lua.h" + +#include "lcode.h" +#include "ldebug.h" +#include "ldo.h" +#include "lgc.h" +#include "llex.h" +#include "lmem.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lparser.h" +#include "lstring.h" +#include "ltable.h" +#include "lvm.h" + + +/* (note that expressions VJMP also have jumps.) */ +#define hasjumps(e) ((e)->t != (e)->f) + + +static int codesJ (FuncState *fs, OpCode o, int sj, int k); + + + +/* semantic error */ +l_noret luaK_semerror (LexState *ls, const char *fmt, ...) { + const char *msg; + va_list argp; + pushvfstring(ls->L, argp, fmt, msg); + ls->t.token = 0; /* remove "near " from final message */ + ls->linenumber = ls->lastline; /* back to line of last used token */ + luaX_syntaxerror(ls, msg); +} + + +/* +** If expression is a numeric constant, fills 'v' with its value +** and returns 1. Otherwise, returns 0. +*/ +static int tonumeral (const expdesc *e, TValue *v) { + if (hasjumps(e)) + return 0; /* not a numeral */ + switch (e->k) { + case VKINT: + if (v) setivalue(v, e->u.ival); + return 1; + case VKFLT: + if (v) setfltvalue(v, e->u.nval); + return 1; + default: return 0; + } +} + + +/* +** Get the constant value from a constant expression +*/ +static TValue *const2val (FuncState *fs, const expdesc *e) { + lua_assert(e->k == VCONST); + return &fs->ls->dyd->actvar.arr[e->u.info].k; +} + + +/* +** If expression is a constant, fills 'v' with its value +** and returns 1. Otherwise, returns 0. +*/ +int luaK_exp2const (FuncState *fs, const expdesc *e, TValue *v) { + if (hasjumps(e)) + return 0; /* not a constant */ + switch (e->k) { + case VFALSE: + setbfvalue(v); + return 1; + case VTRUE: + setbtvalue(v); + return 1; + case VNIL: + setnilvalue(v); + return 1; + case VKSTR: { + setsvalue(fs->ls->L, v, e->u.strval); + return 1; + } + case VCONST: { + setobj(fs->ls->L, v, const2val(fs, e)); + return 1; + } + default: return tonumeral(e, v); + } +} + + +/* +** Return the previous instruction of the current code. If there +** may be a jump target between the current instruction and the +** previous one, return an invalid instruction (to avoid wrong +** optimizations). +*/ +static Instruction *previousinstruction (FuncState *fs) { + static const Instruction invalidinstruction = ~(Instruction)0; + if (fs->pc > fs->lasttarget) + return &fs->f->code[fs->pc - 1]; /* previous instruction */ + else + return cast(Instruction*, &invalidinstruction); +} + + +/* +** Create a OP_LOADNIL instruction, but try to optimize: if the previous +** instruction is also OP_LOADNIL and ranges are compatible, adjust +** range of previous instruction instead of emitting a new one. (For +** instance, 'local a; local b' will generate a single opcode.) +*/ +void luaK_nil (FuncState *fs, int from, int n) { + int l = from + n - 1; /* last register to set nil */ + Instruction *previous = previousinstruction(fs); + if (GET_OPCODE(*previous) == OP_LOADNIL) { /* previous is LOADNIL? */ + int pfrom = GETARG_A(*previous); /* get previous range */ + int pl = pfrom + GETARG_B(*previous); + if ((pfrom <= from && from <= pl + 1) || + (from <= pfrom && pfrom <= l + 1)) { /* can connect both? */ + if (pfrom < from) from = pfrom; /* from = min(from, pfrom) */ + if (pl > l) l = pl; /* l = max(l, pl) */ + SETARG_A(*previous, from); + SETARG_B(*previous, l - from); + return; + } /* else go through */ + } + luaK_codeABC(fs, OP_LOADNIL, from, n - 1, 0); /* else no optimization */ +} + + +/* +** Gets the destination address of a jump instruction. Used to traverse +** a list of jumps. +*/ +static int getjump (FuncState *fs, int pc) { + int offset = GETARG_sJ(fs->f->code[pc]); + if (offset == NO_JUMP) /* point to itself represents end of list */ + return NO_JUMP; /* end of list */ + else + return (pc+1)+offset; /* turn offset into absolute position */ +} + + +/* +** Fix jump instruction at position 'pc' to jump to 'dest'. +** (Jump addresses are relative in Lua) +*/ +static void fixjump (FuncState *fs, int pc, int dest) { + Instruction *jmp = &fs->f->code[pc]; + int offset = dest - (pc + 1); + lua_assert(dest != NO_JUMP); + if (!(-OFFSET_sJ <= offset && offset <= MAXARG_sJ - OFFSET_sJ)) + luaX_syntaxerror(fs->ls, "control structure too long"); + lua_assert(GET_OPCODE(*jmp) == OP_JMP); + SETARG_sJ(*jmp, offset); +} + + +/* +** Concatenate jump-list 'l2' into jump-list 'l1' +*/ +void luaK_concat (FuncState *fs, int *l1, int l2) { + if (l2 == NO_JUMP) return; /* nothing to concatenate? */ + else if (*l1 == NO_JUMP) /* no original list? */ + *l1 = l2; /* 'l1' points to 'l2' */ + else { + int list = *l1; + int next; + while ((next = getjump(fs, list)) != NO_JUMP) /* find last element */ + list = next; + fixjump(fs, list, l2); /* last element links to 'l2' */ + } +} + + +/* +** Create a jump instruction and return its position, so its destination +** can be fixed later (with 'fixjump'). +*/ +int luaK_jump (FuncState *fs) { + return codesJ(fs, OP_JMP, NO_JUMP, 0); +} + + +/* +** Code a 'return' instruction +*/ +void luaK_ret (FuncState *fs, int first, int nret) { + OpCode op; + switch (nret) { + case 0: op = OP_RETURN0; break; + case 1: op = OP_RETURN1; break; + default: op = OP_RETURN; break; + } + luaY_checklimit(fs, nret + 1, MAXARG_B, "returns"); + luaK_codeABC(fs, op, first, nret + 1, 0); +} + + +/* +** Code a "conditional jump", that is, a test or comparison opcode +** followed by a jump. Return jump position. +*/ +static int condjump (FuncState *fs, OpCode op, int A, int B, int C, int k) { + luaK_codeABCk(fs, op, A, B, C, k); + return luaK_jump(fs); +} + + +/* +** returns current 'pc' and marks it as a jump target (to avoid wrong +** optimizations with consecutive instructions not in the same basic block). +*/ +int luaK_getlabel (FuncState *fs) { + fs->lasttarget = fs->pc; + return fs->pc; +} + + +/* +** Returns the position of the instruction "controlling" a given +** jump (that is, its condition), or the jump itself if it is +** unconditional. +*/ +static Instruction *getjumpcontrol (FuncState *fs, int pc) { + Instruction *pi = &fs->f->code[pc]; + if (pc >= 1 && testTMode(GET_OPCODE(*(pi-1)))) + return pi-1; + else + return pi; +} + + +/* +** Patch destination register for a TESTSET instruction. +** If instruction in position 'node' is not a TESTSET, return 0 ("fails"). +** Otherwise, if 'reg' is not 'NO_REG', set it as the destination +** register. Otherwise, change instruction to a simple 'TEST' (produces +** no register value) +*/ +static int patchtestreg (FuncState *fs, int node, int reg) { + Instruction *i = getjumpcontrol(fs, node); + if (GET_OPCODE(*i) != OP_TESTSET) + return 0; /* cannot patch other instructions */ + if (reg != NO_REG && reg != GETARG_B(*i)) + SETARG_A(*i, reg); + else { + /* no register to put value or register already has the value; + change instruction to simple test */ + *i = CREATE_ABCk(OP_TEST, GETARG_B(*i), 0, 0, GETARG_k(*i)); + } + return 1; +} + + +/* +** Traverse a list of tests ensuring no one produces a value +*/ +static void removevalues (FuncState *fs, int list) { + for (; list != NO_JUMP; list = getjump(fs, list)) + patchtestreg(fs, list, NO_REG); +} + + +/* +** Traverse a list of tests, patching their destination address and +** registers: tests producing values jump to 'vtarget' (and put their +** values in 'reg'), other tests jump to 'dtarget'. +*/ +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); /* jump to default target */ + list = next; + } +} + + +/* +** Path all jumps in 'list' to jump to 'target'. +** (The assert means that we cannot fix a jump to a forward address +** because we only know addresses once code is generated.) +*/ +void luaK_patchlist (FuncState *fs, int list, int target) { + lua_assert(target <= fs->pc); + patchlistaux(fs, list, target, NO_REG, target); +} + + +void luaK_patchtohere (FuncState *fs, int list) { + int hr = luaK_getlabel(fs); /* mark "here" as a jump target */ + luaK_patchlist(fs, list, hr); +} + + +/* limit for difference between lines in relative line info. */ +#define LIMLINEDIFF 0x80 + + +/* +** Save line info for a new instruction. If difference from last line +** does not fit in a byte, of after that many instructions, save a new +** absolute line info; (in that case, the special value 'ABSLINEINFO' +** in 'lineinfo' signals the existence of this absolute information.) +** Otherwise, store the difference from last line in 'lineinfo'. +*/ +static void savelineinfo (FuncState *fs, Proto *f, int line) { + int linedif = line - fs->previousline; + int pc = fs->pc - 1; /* last instruction coded */ + if (abs(linedif) >= LIMLINEDIFF || fs->iwthabs++ >= MAXIWTHABS) { + luaM_growvector(fs->ls->L, f->abslineinfo, fs->nabslineinfo, + f->sizeabslineinfo, AbsLineInfo, INT_MAX, "lines"); + f->abslineinfo[fs->nabslineinfo].pc = pc; + f->abslineinfo[fs->nabslineinfo++].line = line; + linedif = ABSLINEINFO; /* signal that there is absolute information */ + fs->iwthabs = 1; /* restart counter */ + } + luaM_growvector(fs->ls->L, f->lineinfo, pc, f->sizelineinfo, ls_byte, + INT_MAX, "opcodes"); + f->lineinfo[pc] = cast(ls_byte, linedif); + fs->previousline = line; /* last line saved */ +} + + +/* +** Remove line information from the last instruction. +** If line information for that instruction is absolute, set 'iwthabs' +** above its max to force the new (replacing) instruction to have +** absolute line info, too. +*/ +static void removelastlineinfo (FuncState *fs) { + Proto *f = fs->f; + int pc = fs->pc - 1; /* last instruction coded */ + if (f->lineinfo[pc] != ABSLINEINFO) { /* relative line info? */ + fs->previousline -= f->lineinfo[pc]; /* correct last line saved */ + fs->iwthabs--; /* undo previous increment */ + } + else { /* absolute line information */ + lua_assert(f->abslineinfo[fs->nabslineinfo - 1].pc == pc); + fs->nabslineinfo--; /* remove it */ + fs->iwthabs = MAXIWTHABS + 1; /* force next line info to be absolute */ + } +} + + +/* +** Remove the last instruction created, correcting line information +** accordingly. +*/ +static void removelastinstruction (FuncState *fs) { + removelastlineinfo(fs); + fs->pc--; +} + + +/* +** Emit instruction 'i', checking for array sizes and saving also its +** line information. Return 'i' position. +*/ +int luaK_code (FuncState *fs, Instruction i) { + Proto *f = fs->f; + /* put new instruction in code array */ + luaM_growvector(fs->ls->L, f->code, fs->pc, f->sizecode, Instruction, + INT_MAX, "opcodes"); + f->code[fs->pc++] = i; + savelineinfo(fs, f, fs->ls->lastline); + return fs->pc - 1; /* index of new instruction */ +} + + +/* +** Format and emit an 'iABC' instruction. (Assertions check consistency +** of parameters versus opcode.) +*/ +int luaK_codeABCk (FuncState *fs, OpCode o, int A, int B, int C, int k) { + lua_assert(getOpMode(o) == iABC); + lua_assert(A <= MAXARG_A && B <= MAXARG_B && + C <= MAXARG_C && (k & ~1) == 0); + return luaK_code(fs, CREATE_ABCk(o, A, B, C, k)); +} + + +int luaK_codevABCk (FuncState *fs, OpCode o, int A, int B, int C, int k) { + lua_assert(getOpMode(o) == ivABC); + lua_assert(A <= MAXARG_A && B <= MAXARG_vB && + C <= MAXARG_vC && (k & ~1) == 0); + return luaK_code(fs, CREATE_vABCk(o, A, B, C, k)); +} + + +/* +** Format and emit an 'iABx' instruction. +*/ +int luaK_codeABx (FuncState *fs, OpCode o, int A, int Bc) { + lua_assert(getOpMode(o) == iABx); + lua_assert(A <= MAXARG_A && Bc <= MAXARG_Bx); + return luaK_code(fs, CREATE_ABx(o, A, Bc)); +} + + +/* +** Format and emit an 'iAsBx' instruction. +*/ +static int codeAsBx (FuncState *fs, OpCode o, int A, int Bc) { + int b = Bc + OFFSET_sBx; + lua_assert(getOpMode(o) == iAsBx); + lua_assert(A <= MAXARG_A && b <= MAXARG_Bx); + return luaK_code(fs, CREATE_ABx(o, A, b)); +} + + +/* +** Format and emit an 'isJ' instruction. +*/ +static int codesJ (FuncState *fs, OpCode o, int sj, int k) { + int j = sj + OFFSET_sJ; + lua_assert(getOpMode(o) == isJ); + lua_assert(j <= MAXARG_sJ && (k & ~1) == 0); + return luaK_code(fs, CREATE_sJ(o, j, k)); +} + + +/* +** Emit an "extra argument" instruction (format 'iAx') +*/ +static int codeextraarg (FuncState *fs, int A) { + lua_assert(A <= MAXARG_Ax); + return luaK_code(fs, CREATE_Ax(OP_EXTRAARG, A)); +} + + +/* +** Emit a "load constant" instruction, using either 'OP_LOADK' +** (if constant index 'k' fits in 18 bits) or an 'OP_LOADKX' +** instruction with "extra argument". +*/ +static int luaK_codek (FuncState *fs, int reg, int k) { + if (k <= MAXARG_Bx) + return luaK_codeABx(fs, OP_LOADK, reg, k); + else { + int p = luaK_codeABx(fs, OP_LOADKX, reg, 0); + codeextraarg(fs, k); + return p; + } +} + + +/* +** Check register-stack level, keeping track of its maximum size +** in field 'maxstacksize' +*/ +void luaK_checkstack (FuncState *fs, int n) { + int newstack = fs->freereg + n; + if (newstack > fs->f->maxstacksize) { + luaY_checklimit(fs, newstack, MAX_FSTACK, "registers"); + fs->f->maxstacksize = cast_byte(newstack); + } +} + + +/* +** Reserve 'n' registers in register stack +*/ +void luaK_reserveregs (FuncState *fs, int n) { + luaK_checkstack(fs, n); + fs->freereg = cast_byte(fs->freereg + n); +} + + +/* +** Free register 'reg', if it is neither a constant index nor +** a local variable. +) +*/ +static void freereg (FuncState *fs, int reg) { + if (reg >= luaY_nvarstack(fs)) { + fs->freereg--; + lua_assert(reg == fs->freereg); + } +} + + +/* +** Free two registers in proper order +*/ +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); + } +} + + +/* +** Free register used by expression 'e' (if any) +*/ +static void freeexp (FuncState *fs, expdesc *e) { + if (e->k == VNONRELOC) + freereg(fs, e->u.info); +} + + +/* +** Free registers used by expressions 'e1' and 'e2' (if any) in proper +** order. +*/ +static void freeexps (FuncState *fs, expdesc *e1, expdesc *e2) { + int r1 = (e1->k == VNONRELOC) ? e1->u.info : -1; + int r2 = (e2->k == VNONRELOC) ? e2->u.info : -1; + freeregs(fs, r1, r2); +} + + +/* +** Add constant 'v' to prototype's list of constants (field 'k'). +*/ +static int addk (FuncState *fs, Proto *f, TValue *v) { + lua_State *L = fs->ls->L; + int oldsize = f->sizek; + int k = fs->nk; + luaM_growvector(L, f->k, k, f->sizek, TValue, MAXARG_Ax, "constants"); + while (oldsize < f->sizek) + setnilvalue(&f->k[oldsize++]); + setobj(L, &f->k[k], v); + fs->nk++; + luaC_barrier(L, f, v); + return k; +} + + +/* +** Use scanner's table to cache position of constants in constant list +** and try to reuse constants. Because some values should not be used +** as keys (nil cannot be a key, integer keys can collapse with float +** keys), the caller must provide a useful 'key' for indexing the cache. +*/ +static int k2proto (FuncState *fs, TValue *key, TValue *v) { + TValue val; + Proto *f = fs->f; + int tag = luaH_get(fs->kcache, key, &val); /* query scanner table */ + if (!tagisempty(tag)) { /* is there an index there? */ + int k = cast_int(ivalue(&val)); + /* collisions can happen only for float keys */ + lua_assert(ttisfloat(key) || luaV_rawequalobj(&f->k[k], v)); + return k; /* reuse index */ + } + else { /* constant not found; create a new entry */ + int k = addk(fs, f, v); + /* cache it for reuse; numerical value does not need GC barrier; + table is not a metatable, so it does not need to invalidate cache */ + setivalue(&val, k); + luaH_set(fs->ls->L, fs->kcache, key, &val); + return k; + } +} + + +/* +** Add a string to list of constants and return its index. +*/ +static int stringK (FuncState *fs, TString *s) { + TValue o; + setsvalue(fs->ls->L, &o, s); + return k2proto(fs, &o, &o); /* use string itself as key */ +} + + +/* +** Add an integer to list of constants and return its index. +*/ +static int luaK_intK (FuncState *fs, lua_Integer n) { + TValue o; + setivalue(&o, n); + return k2proto(fs, &o, &o); /* use integer itself as key */ +} + +/* +** Add a float to list of constants and return its index. Floats +** with integral values need a different key, to avoid collision +** with actual integers. To that end, we add to the number its smaller +** power-of-two fraction that is still significant in its scale. +** (For doubles, the fraction would be 2^-52). +** This method is not bulletproof: different numbers may generate the +** same key (e.g., very large numbers will overflow to 'inf') and for +** floats larger than 2^53 the result is still an integer. For those +** cases, just generate a new entry. At worst, this only wastes an entry +** with a duplicate. +*/ +static int luaK_numberK (FuncState *fs, lua_Number r) { + TValue o, kv; + setfltvalue(&o, r); /* value as a TValue */ + if (r == 0) { /* handle zero as a special case */ + setpvalue(&kv, fs); /* use FuncState as index */ + return k2proto(fs, &kv, &o); /* cannot collide */ + } + else { + const int nbm = l_floatatt(MANT_DIG); + const lua_Number q = l_mathop(ldexp)(l_mathop(1.0), -nbm + 1); + const lua_Number k = r * (1 + q); /* key */ + lua_Integer ik; + setfltvalue(&kv, k); /* key as a TValue */ + if (!luaV_flttointeger(k, &ik, F2Ieq)) { /* not an integer value? */ + int n = k2proto(fs, &kv, &o); /* use key */ + if (luaV_rawequalobj(&fs->f->k[n], &o)) /* correct value? */ + return n; + } + /* else, either key is still an integer or there was a collision; + anyway, do not try to reuse constant; instead, create a new one */ + return addk(fs, fs->f, &o); + } +} + + +/* +** Add a false to list of constants and return its index. +*/ +static int boolF (FuncState *fs) { + TValue o; + setbfvalue(&o); + return k2proto(fs, &o, &o); /* use boolean itself as key */ +} + + +/* +** Add a true to list of constants and return its index. +*/ +static int boolT (FuncState *fs) { + TValue o; + setbtvalue(&o); + return k2proto(fs, &o, &o); /* use boolean itself as key */ +} + + +/* +** Add nil to list of constants and return its index. +*/ +static int nilK (FuncState *fs) { + lua_State *L = fs->ls->L; + TValue k; + /* cannot use nil as key; instead use table itself */ + sethvalue(L, &k, fs->kcache); + return k2proto(fs, &k, &G(L)->nilvalue); +} + + +/* +** Check whether 'i' can be stored in an 'sC' operand. Equivalent to +** (0 <= int2sC(i) && int2sC(i) <= MAXARG_C) but without risk of +** overflows in the hidden addition inside 'int2sC'. +*/ +static int fitsC (lua_Integer i) { + return (l_castS2U(i) + OFFSET_sC <= cast_uint(MAXARG_C)); +} + + +/* +** Check whether 'i' can be stored in an 'sBx' operand. +*/ +static int fitsBx (lua_Integer i) { + return (-OFFSET_sBx <= i && i <= MAXARG_Bx - OFFSET_sBx); +} + + +void luaK_int (FuncState *fs, int reg, lua_Integer i) { + if (fitsBx(i)) + codeAsBx(fs, OP_LOADI, reg, cast_int(i)); + else + luaK_codek(fs, reg, luaK_intK(fs, i)); +} + + +static void luaK_float (FuncState *fs, int reg, lua_Number f) { + lua_Integer fi; + if (luaV_flttointeger(f, &fi, F2Ieq) && fitsBx(fi)) + codeAsBx(fs, OP_LOADF, reg, cast_int(fi)); + else + luaK_codek(fs, reg, luaK_numberK(fs, f)); +} + + +/* +** Get the value of 'var' in a register and generate an opcode to check +** whether that register is nil. 'k' is the index of the variable name +** in the list of constants. If its value cannot be encoded in Bx, a 0 +** will use '?' for the name. +*/ +void luaK_codecheckglobal (FuncState *fs, expdesc *var, int k, int line) { + luaK_exp2anyreg(fs, var); + luaK_fixline(fs, line); + k = (k >= MAXARG_Bx) ? 0 : k + 1; + luaK_codeABx(fs, OP_ERRNNIL, var->u.info, k); + luaK_fixline(fs, line); + freeexp(fs, var); +} + + +/* +** Convert a constant in 'v' into an expression description 'e' +*/ +static void const2exp (TValue *v, expdesc *e) { + switch (ttypetag(v)) { + case LUA_VNUMINT: + e->k = VKINT; e->u.ival = ivalue(v); + break; + case LUA_VNUMFLT: + e->k = VKFLT; e->u.nval = fltvalue(v); + break; + case LUA_VFALSE: + e->k = VFALSE; + break; + case LUA_VTRUE: + e->k = VTRUE; + break; + case LUA_VNIL: + e->k = VNIL; + break; + case LUA_VSHRSTR: case LUA_VLNGSTR: + e->k = VKSTR; e->u.strval = tsvalue(v); + break; + default: lua_assert(0); + } +} + + +/* +** Fix an expression to return the number of results 'nresults'. +** 'e' must be a multi-ret expression (function call or vararg). +*/ +void luaK_setreturns (FuncState *fs, expdesc *e, int nresults) { + Instruction *pc = &getinstruction(fs, e); + luaY_checklimit(fs, nresults + 1, MAXARG_C, "multiple results"); + if (e->k == VCALL) /* expression is an open function call? */ + SETARG_C(*pc, nresults + 1); + else { + lua_assert(e->k == VVARARG); + SETARG_C(*pc, nresults + 1); + SETARG_A(*pc, fs->freereg); + luaK_reserveregs(fs, 1); + } +} + + +/* +** Convert a VKSTR to a VK +*/ +static int str2K (FuncState *fs, expdesc *e) { + lua_assert(e->k == VKSTR); + e->u.info = stringK(fs, e->u.strval); + e->k = VK; + return e->u.info; +} + + +/* +** Fix an expression to return one result. +** If expression is not a multi-ret expression (function call or +** vararg), it already returns one result, so nothing needs to be done. +** Function calls become VNONRELOC expressions (as its result comes +** fixed in the base register of the call), while vararg expressions +** become VRELOC (as OP_VARARG puts its results where it wants). +** (Calls are created returning one result, so that does not need +** to be fixed.) +*/ +void luaK_setoneret (FuncState *fs, expdesc *e) { + if (e->k == VCALL) { /* expression is an open function call? */ + /* already returns 1 value */ + lua_assert(GETARG_C(getinstruction(fs, e)) == 2); + e->k = VNONRELOC; /* result has fixed position */ + e->u.info = GETARG_A(getinstruction(fs, e)); + } + else if (e->k == VVARARG) { + SETARG_C(getinstruction(fs, e), 2); + e->k = VRELOC; /* can relocate its simple result */ + } +} + +/* +** Change a vararg parameter into a regular local variable +*/ +void luaK_vapar2local (FuncState *fs, expdesc *var) { + needvatab(fs->f); /* function will need a vararg table */ + /* now a vararg parameter is equivalent to a regular local variable */ + var->k = VLOCAL; +} + + +/* +** Ensure that expression 'e' is not a variable (nor a ). +** (Expression still may have jump lists.) +*/ +void luaK_dischargevars (FuncState *fs, expdesc *e) { + switch (e->k) { + case VCONST: { + const2exp(const2val(fs, e), e); + break; + } + case VVARGVAR: { + luaK_vapar2local(fs, e); /* turn it into a local variable */ + } /* FALLTHROUGH */ + case VLOCAL: { /* already in a register */ + int temp = e->u.var.ridx; + e->u.info = temp; /* (avoid a direct assignment; values overlap) */ + e->k = VNONRELOC; /* becomes a non-relocatable value */ + break; + } + case VUPVAL: { /* move value to some (pending) register */ + e->u.info = luaK_codeABC(fs, OP_GETUPVAL, 0, e->u.info, 0); + e->k = VRELOC; + break; + } + case VINDEXUP: { + e->u.info = luaK_codeABC(fs, OP_GETTABUP, 0, e->u.ind.t, e->u.ind.idx); + e->k = VRELOC; + break; + } + case VINDEXI: { + freereg(fs, e->u.ind.t); + e->u.info = luaK_codeABC(fs, OP_GETI, 0, e->u.ind.t, e->u.ind.idx); + e->k = VRELOC; + break; + } + case VINDEXSTR: { + freereg(fs, e->u.ind.t); + e->u.info = luaK_codeABC(fs, OP_GETFIELD, 0, e->u.ind.t, e->u.ind.idx); + e->k = VRELOC; + break; + } + case VINDEXED: { + freeregs(fs, e->u.ind.t, e->u.ind.idx); + e->u.info = luaK_codeABC(fs, OP_GETTABLE, 0, e->u.ind.t, e->u.ind.idx); + e->k = VRELOC; + break; + } + case VVARGIND: { + freeregs(fs, e->u.ind.t, e->u.ind.idx); + e->u.info = luaK_codeABC(fs, OP_GETVARG, 0, e->u.ind.t, e->u.ind.idx); + e->k = VRELOC; + break; + } + case VVARARG: case VCALL: { + luaK_setoneret(fs, e); + break; + } + default: break; /* there is one value available (somewhere) */ + } +} + + +/* +** Ensure expression value is in register 'reg', making 'e' a +** non-relocatable expression. +** (Expression still may have jump lists.) +*/ +static void discharge2reg (FuncState *fs, expdesc *e, int reg) { + luaK_dischargevars(fs, e); + switch (e->k) { + case VNIL: { + luaK_nil(fs, reg, 1); + break; + } + case VFALSE: { + luaK_codeABC(fs, OP_LOADFALSE, reg, 0, 0); + break; + } + case VTRUE: { + luaK_codeABC(fs, OP_LOADTRUE, reg, 0, 0); + break; + } + case VKSTR: { + str2K(fs, e); + } /* FALLTHROUGH */ + case VK: { + luaK_codek(fs, reg, e->u.info); + break; + } + case VKFLT: { + luaK_float(fs, reg, e->u.nval); + break; + } + case VKINT: { + luaK_int(fs, reg, e->u.ival); + break; + } + case VRELOC: { + Instruction *pc = &getinstruction(fs, e); + SETARG_A(*pc, reg); /* instruction will put result in 'reg' */ + break; + } + case VNONRELOC: { + if (reg != e->u.info) + luaK_codeABC(fs, OP_MOVE, reg, e->u.info, 0); + break; + } + default: { + lua_assert(e->k == VJMP); + return; /* nothing to do... */ + } + } + e->u.info = reg; + e->k = VNONRELOC; +} + + +/* +** Ensure expression value is in a register, making 'e' a +** non-relocatable expression. +** (Expression still may have jump lists.) +*/ +static void discharge2anyreg (FuncState *fs, expdesc *e) { + if (e->k != VNONRELOC) { /* no fixed register yet? */ + luaK_reserveregs(fs, 1); /* get a register */ + discharge2reg(fs, e, fs->freereg-1); /* put value there */ + } +} + + +static int code_loadbool (FuncState *fs, int A, OpCode op) { + luaK_getlabel(fs); /* those instructions may be jump targets */ + return luaK_codeABC(fs, op, A, 0, 0); +} + + +/* +** check whether list has any jump that do not produce a value +** or produce an inverted value +*/ +static int need_value (FuncState *fs, int list) { + for (; list != NO_JUMP; list = getjump(fs, list)) { + Instruction i = *getjumpcontrol(fs, list); + if (GET_OPCODE(i) != OP_TESTSET) return 1; + } + return 0; /* not found */ +} + + +/* +** Ensures final expression result (which includes results from its +** jump lists) is in register 'reg'. +** If expression has jumps, need to patch these jumps either to +** its final position or to "load" instructions (for those tests +** that do not produce values). +*/ +static void exp2reg (FuncState *fs, expdesc *e, int reg) { + discharge2reg(fs, e, reg); + if (e->k == VJMP) /* expression itself is a test? */ + luaK_concat(fs, &e->t, e->u.info); /* put this jump in 't' list */ + if (hasjumps(e)) { + int final; /* position after whole expression */ + int p_f = NO_JUMP; /* position of an eventual LOAD false */ + int p_t = NO_JUMP; /* position of an eventual LOAD true */ + if (need_value(fs, e->t) || need_value(fs, e->f)) { + int fj = (e->k == VJMP) ? NO_JUMP : luaK_jump(fs); + p_f = code_loadbool(fs, reg, OP_LFALSESKIP); /* skip next inst. */ + p_t = code_loadbool(fs, reg, OP_LOADTRUE); + /* jump around these booleans if 'e' is not a test */ + luaK_patchtohere(fs, fj); + } + final = luaK_getlabel(fs); + patchlistaux(fs, e->f, final, reg, p_f); + patchlistaux(fs, e->t, final, reg, p_t); + } + e->f = e->t = NO_JUMP; + e->u.info = reg; + e->k = VNONRELOC; +} + + +/* +** Ensures final expression result is in next available register. +*/ +void luaK_exp2nextreg (FuncState *fs, expdesc *e) { + luaK_dischargevars(fs, e); + freeexp(fs, e); + luaK_reserveregs(fs, 1); + exp2reg(fs, e, fs->freereg - 1); +} + + +/* +** Ensures final expression result is in some (any) register +** and return that register. +*/ +int luaK_exp2anyreg (FuncState *fs, expdesc *e) { + luaK_dischargevars(fs, e); + if (e->k == VNONRELOC) { /* expression already has a register? */ + if (!hasjumps(e)) /* no jumps? */ + return e->u.info; /* result is already in a register */ + if (e->u.info >= luaY_nvarstack(fs)) { /* reg. is not a local? */ + exp2reg(fs, e, e->u.info); /* put final result in it */ + return e->u.info; + } + /* else expression has jumps and cannot change its register + to hold the jump values, because it is a local variable. + Go through to the default case. */ + } + luaK_exp2nextreg(fs, e); /* default: use next available register */ + return e->u.info; +} + + +/* +** Ensures final expression result is either in a register, +** in an upvalue, or it is the vararg parameter. +*/ +void luaK_exp2anyregup (FuncState *fs, expdesc *e) { + if ((e->k != VUPVAL && e->k != VVARGVAR) || hasjumps(e)) + luaK_exp2anyreg(fs, e); +} + + +/* +** Ensures final expression result is either in a register +** or it is a constant. +*/ +void luaK_exp2val (FuncState *fs, expdesc *e) { + if (e->k == VJMP || hasjumps(e)) + luaK_exp2anyreg(fs, e); + else + luaK_dischargevars(fs, e); +} + + +/* +** Try to make 'e' a K expression with an index in the range of R/K +** indices. Return true iff succeeded. +*/ +static int luaK_exp2K (FuncState *fs, expdesc *e) { + if (!hasjumps(e)) { + int info; + switch (e->k) { /* move constants to 'k' */ + case VTRUE: info = boolT(fs); break; + case VFALSE: info = boolF(fs); break; + case VNIL: info = nilK(fs); break; + case VKINT: info = luaK_intK(fs, e->u.ival); break; + case VKFLT: info = luaK_numberK(fs, e->u.nval); break; + case VKSTR: info = stringK(fs, e->u.strval); break; + case VK: info = e->u.info; break; + default: return 0; /* not a constant */ + } + if (info <= MAXINDEXRK) { /* does constant fit in 'argC'? */ + e->k = VK; /* make expression a 'K' expression */ + e->u.info = info; + return 1; + } + } + /* else, expression doesn't fit; leave it unchanged */ + return 0; +} + + +/* +** Ensures final expression result is in a valid R/K index +** (that is, it is either in a register or in 'k' with an index +** in the range of R/K indices). +** Returns 1 iff expression is K. +*/ +static int exp2RK (FuncState *fs, expdesc *e) { + if (luaK_exp2K(fs, e)) + return 1; + else { /* not a constant in the right range: put it in a register */ + luaK_exp2anyreg(fs, e); + return 0; + } +} + + +static void codeABRK (FuncState *fs, OpCode o, int A, int B, + expdesc *ec) { + int k = exp2RK(fs, ec); + luaK_codeABCk(fs, o, A, B, ec->u.info, k); +} + + +/* +** Generate code to store result of expression 'ex' into variable 'var'. +*/ +void luaK_storevar (FuncState *fs, expdesc *var, expdesc *ex) { + switch (var->k) { + case VLOCAL: { + freeexp(fs, ex); + exp2reg(fs, ex, var->u.var.ridx); /* compute 'ex' into proper place */ + return; + } + case VUPVAL: { + int e = luaK_exp2anyreg(fs, ex); + luaK_codeABC(fs, OP_SETUPVAL, e, var->u.info, 0); + break; + } + case VINDEXUP: { + codeABRK(fs, OP_SETTABUP, var->u.ind.t, var->u.ind.idx, ex); + break; + } + case VINDEXI: { + codeABRK(fs, OP_SETI, var->u.ind.t, var->u.ind.idx, ex); + break; + } + case VINDEXSTR: { + codeABRK(fs, OP_SETFIELD, var->u.ind.t, var->u.ind.idx, ex); + break; + } + case VVARGIND: { + needvatab(fs->f); /* function will need a vararg table */ + /* now, assignment is to a regular table */ + } /* FALLTHROUGH */ + case VINDEXED: { + codeABRK(fs, OP_SETTABLE, var->u.ind.t, var->u.ind.idx, ex); + break; + } + default: lua_assert(0); /* invalid var kind to store */ + } + freeexp(fs, ex); +} + + +/* +** Negate condition 'e' (where 'e' is a comparison). +*/ +static void negatecondition (FuncState *fs, expdesc *e) { + Instruction *pc = getjumpcontrol(fs, e->u.info); + lua_assert(testTMode(GET_OPCODE(*pc)) && GET_OPCODE(*pc) != OP_TESTSET && + GET_OPCODE(*pc) != OP_TEST); + SETARG_k(*pc, (GETARG_k(*pc) ^ 1)); +} + + +/* +** Emit instruction to jump if 'e' is 'cond' (that is, if 'cond' +** is true, code will jump if 'e' is true.) Return jump position. +** Optimize when 'e' is 'not' something, inverting the condition +** and removing the 'not'. +*/ +static int jumponcond (FuncState *fs, expdesc *e, int cond) { + if (e->k == VRELOC) { + Instruction ie = getinstruction(fs, e); + if (GET_OPCODE(ie) == OP_NOT) { + removelastinstruction(fs); /* remove previous OP_NOT */ + return condjump(fs, OP_TEST, GETARG_B(ie), 0, 0, !cond); + } + /* else go through */ + } + discharge2anyreg(fs, e); + freeexp(fs, e); + return condjump(fs, OP_TESTSET, NO_REG, e->u.info, 0, cond); +} + + +/* +** Emit code to go through if 'e' is true, jump otherwise. +*/ +void luaK_goiftrue (FuncState *fs, expdesc *e) { + int pc; /* pc of new jump */ + luaK_dischargevars(fs, e); + switch (e->k) { + case VJMP: { /* condition? */ + negatecondition(fs, e); /* jump when it is false */ + pc = e->u.info; /* save jump position */ + break; + } + case VK: case VKFLT: case VKINT: case VKSTR: case VTRUE: { + pc = NO_JUMP; /* always true; do nothing */ + break; + } + default: { + pc = jumponcond(fs, e, 0); /* jump when false */ + break; + } + } + luaK_concat(fs, &e->f, pc); /* insert new jump in false list */ + luaK_patchtohere(fs, e->t); /* true list jumps to here (to go through) */ + e->t = NO_JUMP; +} + + +/* +** Emit code to go through if 'e' is false, jump otherwise. +*/ +static void luaK_goiffalse (FuncState *fs, expdesc *e) { + int pc; /* pc of new jump */ + luaK_dischargevars(fs, e); + switch (e->k) { + case VJMP: { + pc = e->u.info; /* already jump if true */ + break; + } + case VNIL: case VFALSE: { + pc = NO_JUMP; /* always false; do nothing */ + break; + } + default: { + pc = jumponcond(fs, e, 1); /* jump if true */ + break; + } + } + luaK_concat(fs, &e->t, pc); /* insert new jump in 't' list */ + luaK_patchtohere(fs, e->f); /* false list jumps to here (to go through) */ + e->f = NO_JUMP; +} + + +/* +** Code 'not e', doing constant folding. +*/ +static void codenot (FuncState *fs, expdesc *e) { + switch (e->k) { + case VNIL: case VFALSE: { + e->k = VTRUE; /* true == not nil == not false */ + break; + } + case VK: case VKFLT: case VKINT: case VKSTR: case VTRUE: { + e->k = VFALSE; /* false == not "x" == not 0.5 == not 1 == not true */ + break; + } + case VJMP: { + negatecondition(fs, e); + break; + } + case VRELOC: + case VNONRELOC: { + discharge2anyreg(fs, e); + freeexp(fs, e); + e->u.info = luaK_codeABC(fs, OP_NOT, 0, e->u.info, 0); + e->k = VRELOC; + break; + } + default: lua_assert(0); /* cannot happen */ + } + /* interchange true and false lists */ + { int temp = e->f; e->f = e->t; e->t = temp; } + removevalues(fs, e->f); /* values are useless when negated */ + removevalues(fs, e->t); +} + + +/* +** Check whether expression 'e' is a short literal string +*/ +static int isKstr (FuncState *fs, expdesc *e) { + return (e->k == VK && !hasjumps(e) && e->u.info <= MAXINDEXRK && + ttisshrstring(&fs->f->k[e->u.info])); +} + +/* +** Check whether expression 'e' is a literal integer. +*/ +static int isKint (expdesc *e) { + return (e->k == VKINT && !hasjumps(e)); +} + + +/* +** Check whether expression 'e' is a literal integer in +** proper range to fit in register C +*/ +static int isCint (expdesc *e) { + return isKint(e) && (l_castS2U(e->u.ival) <= l_castS2U(MAXARG_C)); +} + + +/* +** Check whether expression 'e' is a literal integer in +** proper range to fit in register sC +*/ +static int isSCint (expdesc *e) { + return isKint(e) && fitsC(e->u.ival); +} + + +/* +** Check whether expression 'e' is a literal integer or float in +** proper range to fit in a register (sB or sC). +*/ +static int isSCnumber (expdesc *e, int *pi, int *isfloat) { + lua_Integer i; + if (e->k == VKINT) + i = e->u.ival; + else if (e->k == VKFLT && luaV_flttointeger(e->u.nval, &i, F2Ieq)) + *isfloat = 1; + else + return 0; /* not a number */ + if (!hasjumps(e) && fitsC(i)) { + *pi = int2sC(cast_int(i)); + return 1; + } + else + return 0; +} + + +/* +** Emit SELF instruction or equivalent: the code will convert +** expression 'e' into 'e.key(e,'. +*/ +void luaK_self (FuncState *fs, expdesc *e, expdesc *key) { + int ereg, base; + luaK_exp2anyreg(fs, e); + ereg = e->u.info; /* register where 'e' (the receiver) was placed */ + freeexp(fs, e); + base = e->u.info = fs->freereg; /* base register for op_self */ + e->k = VNONRELOC; /* self expression has a fixed register */ + luaK_reserveregs(fs, 2); /* method and 'self' produced by op_self */ + lua_assert(key->k == VKSTR); + /* is method name a short string in a valid K index? */ + if (strisshr(key->u.strval) && luaK_exp2K(fs, key)) { + /* can use 'self' opcode */ + luaK_codeABCk(fs, OP_SELF, base, ereg, key->u.info, 0); + } + else { /* cannot use 'self' opcode; use move+gettable */ + luaK_exp2anyreg(fs, key); /* put method name in a register */ + luaK_codeABC(fs, OP_MOVE, base + 1, ereg, 0); /* copy self to base+1 */ + luaK_codeABC(fs, OP_GETTABLE, base, ereg, key->u.info); /* get method */ + } + freeexp(fs, key); +} + + +/* auxiliary function to define indexing expressions */ +static void fillidxk (expdesc *t, int idx, expkind k) { + t->u.ind.idx = cast_byte(idx); + t->k = k; +} + + +/* +** Create expression 't[k]'. 't' must have its final result already in a +** register or upvalue. Upvalues can only be indexed by literal strings. +** Keys can be literal strings in the constant table or arbitrary +** values in registers. +*/ +void luaK_indexed (FuncState *fs, expdesc *t, expdesc *k) { + int keystr = -1; + if (k->k == VKSTR) + keystr = str2K(fs, k); + lua_assert(!hasjumps(t) && + (t->k == VLOCAL || t->k == VVARGVAR || + t->k == VNONRELOC || t->k == VUPVAL)); + if (t->k == VUPVAL && !isKstr(fs, k)) /* upvalue indexed by non 'Kstr'? */ + luaK_exp2anyreg(fs, t); /* put it in a register */ + if (t->k == VUPVAL) { + lu_byte temp = cast_byte(t->u.info); /* upvalue index */ + t->u.ind.t = temp; /* (avoid a direct assignment; values overlap) */ + lua_assert(isKstr(fs, k)); + fillidxk(t, k->u.info, VINDEXUP); /* literal short string */ + } + else if (t->k == VVARGVAR) { /* indexing the vararg parameter? */ + int kreg = luaK_exp2anyreg(fs, k); /* put key in some register */ + lu_byte vreg = cast_byte(t->u.var.ridx); /* register with vararg param. */ + lua_assert(vreg == fs->f->numparams); + t->u.ind.t = vreg; /* (avoid a direct assignment; values may overlap?) */ + fillidxk(t, kreg, VVARGIND); /* 't' represents 'vararg[k]' */ + } + else { + /* register index of the table */ + lu_byte temp = cast_byte((t->k == VLOCAL) ? t->u.var.ridx: t->u.info); + t->u.ind.t = temp; /* (avoid a direct assignment; values may overlap?) */ + if (isKstr(fs, k)) + fillidxk(t, k->u.info, VINDEXSTR); /* literal short string */ + else if (isCint(k)) /* int. constant in proper range? */ + fillidxk(t, cast_int(k->u.ival), VINDEXI); + else + fillidxk(t, luaK_exp2anyreg(fs, k), VINDEXED); /* register */ + } + t->u.ind.keystr = keystr; /* string index in 'k' */ + t->u.ind.ro = 0; /* by default, not read-only */ +} + + +/* +** Return false if folding can raise an error. +** Bitwise operations need operands convertible to integers; division +** operations cannot have 0 as divisor. +*/ +static int validop (int op, TValue *v1, TValue *v2) { + switch (op) { + case LUA_OPBAND: case LUA_OPBOR: case LUA_OPBXOR: + case LUA_OPSHL: case LUA_OPSHR: case LUA_OPBNOT: { /* conversion errors */ + lua_Integer i; + return (luaV_tointegerns(v1, &i, LUA_FLOORN2I) && + luaV_tointegerns(v2, &i, LUA_FLOORN2I)); + } + case LUA_OPDIV: case LUA_OPIDIV: case LUA_OPMOD: /* division by 0 */ + return (nvalue(v2) != 0); + default: return 1; /* everything else is valid */ + } +} + + +/* +** Try to "constant-fold" an operation; return 1 iff successful. +** (In this case, 'e1' has the final result.) +*/ +static int constfolding (FuncState *fs, int op, expdesc *e1, + const expdesc *e2) { + TValue v1, v2, res; + if (!tonumeral(e1, &v1) || !tonumeral(e2, &v2) || !validop(op, &v1, &v2)) + return 0; /* non-numeric operands or not safe to fold */ + luaO_rawarith(fs->ls->L, op, &v1, &v2, &res); /* does operation */ + if (ttisinteger(&res)) { + e1->k = VKINT; + e1->u.ival = ivalue(&res); + } + else { /* folds neither NaN nor 0.0 (to avoid problems with -0.0) */ + lua_Number n = fltvalue(&res); + if (luai_numisnan(n) || n == 0) + return 0; + e1->k = VKFLT; + e1->u.nval = n; + } + return 1; +} + + +/* +** Convert a BinOpr to an OpCode (ORDER OPR - ORDER OP) +*/ +l_sinline OpCode binopr2op (BinOpr opr, BinOpr baser, OpCode base) { + lua_assert(baser <= opr && + ((baser == OPR_ADD && opr <= OPR_SHR) || + (baser == OPR_LT && opr <= OPR_LE))); + return cast(OpCode, (cast_int(opr) - cast_int(baser)) + cast_int(base)); +} + + +/* +** Convert a UnOpr to an OpCode (ORDER OPR - ORDER OP) +*/ +l_sinline OpCode unopr2op (UnOpr opr) { + return cast(OpCode, (cast_int(opr) - cast_int(OPR_MINUS)) + + cast_int(OP_UNM)); +} + + +/* +** Convert a BinOpr to a tag method (ORDER OPR - ORDER TM) +*/ +l_sinline TMS binopr2TM (BinOpr opr) { + lua_assert(OPR_ADD <= opr && opr <= OPR_SHR); + return cast(TMS, (cast_int(opr) - cast_int(OPR_ADD)) + cast_int(TM_ADD)); +} + + +/* +** Emit code for unary expressions that "produce values" +** (everything but 'not'). +** Expression to produce final result will be encoded in 'e'. +*/ +static void codeunexpval (FuncState *fs, OpCode op, expdesc *e, int line) { + int r = luaK_exp2anyreg(fs, e); /* opcodes operate only on registers */ + freeexp(fs, e); + e->u.info = luaK_codeABC(fs, op, 0, r, 0); /* generate opcode */ + e->k = VRELOC; /* all those operations are relocatable */ + luaK_fixline(fs, line); +} + + +/* +** Emit code for binary expressions that "produce values" +** (everything but logical operators 'and'/'or' and comparison +** operators). +** Expression to produce final result will be encoded in 'e1'. +*/ +static void finishbinexpval (FuncState *fs, expdesc *e1, expdesc *e2, + OpCode op, int v2, int flip, int line, + OpCode mmop, TMS event) { + int v1 = luaK_exp2anyreg(fs, e1); + int pc = luaK_codeABCk(fs, op, 0, v1, v2, 0); + freeexps(fs, e1, e2); + e1->u.info = pc; + e1->k = VRELOC; /* all those operations are relocatable */ + luaK_fixline(fs, line); + luaK_codeABCk(fs, mmop, v1, v2, cast_int(event), flip); /* metamethod */ + luaK_fixline(fs, line); +} + + +/* +** Emit code for binary expressions that "produce values" over +** two registers. +*/ +static void codebinexpval (FuncState *fs, BinOpr opr, + expdesc *e1, expdesc *e2, int line) { + OpCode op = binopr2op(opr, OPR_ADD, OP_ADD); + int v2 = luaK_exp2anyreg(fs, e2); /* make sure 'e2' is in a register */ + /* 'e1' must be already in a register or it is a constant */ + lua_assert((VNIL <= e1->k && e1->k <= VKSTR) || + e1->k == VNONRELOC || e1->k == VRELOC); + lua_assert(OP_ADD <= op && op <= OP_SHR); + finishbinexpval(fs, e1, e2, op, v2, 0, line, OP_MMBIN, binopr2TM(opr)); +} + + +/* +** Code binary operators with immediate operands. +*/ +static void codebini (FuncState *fs, OpCode op, + expdesc *e1, expdesc *e2, int flip, int line, + TMS event) { + int v2 = int2sC(cast_int(e2->u.ival)); /* immediate operand */ + lua_assert(e2->k == VKINT); + finishbinexpval(fs, e1, e2, op, v2, flip, line, OP_MMBINI, event); +} + + +/* +** Code binary operators with K operand. +*/ +static void codebinK (FuncState *fs, BinOpr opr, + expdesc *e1, expdesc *e2, int flip, int line) { + TMS event = binopr2TM(opr); + int v2 = e2->u.info; /* K index */ + OpCode op = binopr2op(opr, OPR_ADD, OP_ADDK); + finishbinexpval(fs, e1, e2, op, v2, flip, line, OP_MMBINK, event); +} + + +/* Try to code a binary operator negating its second operand. +** For the metamethod, 2nd operand must keep its original value. +*/ +static int finishbinexpneg (FuncState *fs, expdesc *e1, expdesc *e2, + OpCode op, int line, TMS event) { + if (!isKint(e2)) + return 0; /* not an integer constant */ + else { + lua_Integer i2 = e2->u.ival; + if (!(fitsC(i2) && fitsC(-i2))) + return 0; /* not in the proper range */ + else { /* operating a small integer constant */ + int v2 = cast_int(i2); + finishbinexpval(fs, e1, e2, op, int2sC(-v2), 0, line, OP_MMBINI, event); + /* correct metamethod argument */ + SETARG_B(fs->f->code[fs->pc - 1], int2sC(v2)); + return 1; /* successfully coded */ + } + } +} + + +static void swapexps (expdesc *e1, expdesc *e2) { + expdesc temp = *e1; *e1 = *e2; *e2 = temp; /* swap 'e1' and 'e2' */ +} + + +/* +** Code binary operators with no constant operand. +*/ +static void codebinNoK (FuncState *fs, BinOpr opr, + expdesc *e1, expdesc *e2, int flip, int line) { + if (flip) + swapexps(e1, e2); /* back to original order */ + codebinexpval(fs, opr, e1, e2, line); /* use standard operators */ +} + + +/* +** Code arithmetic operators ('+', '-', ...). If second operand is a +** constant in the proper range, use variant opcodes with K operands. +*/ +static void codearith (FuncState *fs, BinOpr opr, + expdesc *e1, expdesc *e2, int flip, int line) { + if (tonumeral(e2, NULL) && luaK_exp2K(fs, e2)) /* K operand? */ + codebinK(fs, opr, e1, e2, flip, line); + else /* 'e2' is neither an immediate nor a K operand */ + codebinNoK(fs, opr, e1, e2, flip, line); +} + + +/* +** Code commutative operators ('+', '*'). If first operand is a +** numeric constant, change order of operands to try to use an +** immediate or K operator. +*/ +static void codecommutative (FuncState *fs, BinOpr op, + expdesc *e1, expdesc *e2, int line) { + int flip = 0; + if (tonumeral(e1, NULL)) { /* is first operand a numeric constant? */ + swapexps(e1, e2); /* change order */ + flip = 1; + } + if (op == OPR_ADD && isSCint(e2)) /* immediate operand? */ + codebini(fs, OP_ADDI, e1, e2, flip, line, TM_ADD); + else + codearith(fs, op, e1, e2, flip, line); +} + + +/* +** Code bitwise operations; they are all commutative, so the function +** tries to put an integer constant as the 2nd operand (a K operand). +*/ +static void codebitwise (FuncState *fs, BinOpr opr, + expdesc *e1, expdesc *e2, int line) { + int flip = 0; + if (e1->k == VKINT) { + swapexps(e1, e2); /* 'e2' will be the constant operand */ + flip = 1; + } + if (e2->k == VKINT && luaK_exp2K(fs, e2)) /* K operand? */ + codebinK(fs, opr, e1, e2, flip, line); + else /* no constants */ + codebinNoK(fs, opr, e1, e2, flip, line); +} + + +/* +** Emit code for order comparisons. When using an immediate operand, +** 'isfloat' tells whether the original value was a float. +*/ +static void codeorder (FuncState *fs, BinOpr opr, expdesc *e1, expdesc *e2) { + int r1, r2; + int im; + int isfloat = 0; + OpCode op; + if (isSCnumber(e2, &im, &isfloat)) { + /* use immediate operand */ + r1 = luaK_exp2anyreg(fs, e1); + r2 = im; + op = binopr2op(opr, OPR_LT, OP_LTI); + } + else if (isSCnumber(e1, &im, &isfloat)) { + /* transform (A < B) to (B > A) and (A <= B) to (B >= A) */ + r1 = luaK_exp2anyreg(fs, e2); + r2 = im; + op = binopr2op(opr, OPR_LT, OP_GTI); + } + else { /* regular case, compare two registers */ + r1 = luaK_exp2anyreg(fs, e1); + r2 = luaK_exp2anyreg(fs, e2); + op = binopr2op(opr, OPR_LT, OP_LT); + } + freeexps(fs, e1, e2); + e1->u.info = condjump(fs, op, r1, r2, isfloat, 1); + e1->k = VJMP; +} + + +/* +** Emit code for equality comparisons ('==', '~='). +** 'e1' was already put as RK by 'luaK_infix'. +*/ +static void codeeq (FuncState *fs, BinOpr opr, expdesc *e1, expdesc *e2) { + int r1, r2; + int im; + int isfloat = 0; /* not needed here, but kept for symmetry */ + OpCode op; + if (e1->k != VNONRELOC) { + lua_assert(e1->k == VK || e1->k == VKINT || e1->k == VKFLT); + swapexps(e1, e2); + } + r1 = luaK_exp2anyreg(fs, e1); /* 1st expression must be in register */ + if (isSCnumber(e2, &im, &isfloat)) { + op = OP_EQI; + r2 = im; /* immediate operand */ + } + else if (exp2RK(fs, e2)) { /* 2nd expression is constant? */ + op = OP_EQK; + r2 = e2->u.info; /* constant index */ + } + else { + op = OP_EQ; /* will compare two registers */ + r2 = luaK_exp2anyreg(fs, e2); + } + freeexps(fs, e1, e2); + e1->u.info = condjump(fs, op, r1, r2, isfloat, (opr == OPR_EQ)); + e1->k = VJMP; +} + + +/* +** Apply prefix operation 'op' to expression 'e'. +*/ +void luaK_prefix (FuncState *fs, UnOpr opr, expdesc *e, int line) { + static const expdesc ef = {VKINT, {0}, NO_JUMP, NO_JUMP}; + luaK_dischargevars(fs, e); + switch (opr) { + case OPR_MINUS: case OPR_BNOT: /* use 'ef' as fake 2nd operand */ + if (constfolding(fs, cast_int(opr + LUA_OPUNM), e, &ef)) + break; + /* else */ /* FALLTHROUGH */ + case OPR_LEN: + codeunexpval(fs, unopr2op(opr), e, line); + break; + case OPR_NOT: codenot(fs, e); break; + default: lua_assert(0); + } +} + + +/* +** Process 1st operand 'v' of binary operation 'op' before reading +** 2nd operand. +*/ +void luaK_infix (FuncState *fs, BinOpr op, expdesc *v) { + luaK_dischargevars(fs, v); + switch (op) { + case OPR_AND: { + luaK_goiftrue(fs, v); /* go ahead only if 'v' is true */ + break; + } + case OPR_OR: { + luaK_goiffalse(fs, v); /* go ahead only if 'v' is false */ + break; + } + case OPR_CONCAT: { + luaK_exp2nextreg(fs, v); /* operand must be on the stack */ + 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 (!tonumeral(v, NULL)) + luaK_exp2anyreg(fs, v); + /* else keep numeral, which may be folded or used as an immediate + operand */ + break; + } + case OPR_EQ: case OPR_NE: { + if (!tonumeral(v, NULL)) + exp2RK(fs, v); + /* else keep numeral, which may be an immediate operand */ + break; + } + case OPR_LT: case OPR_LE: + case OPR_GT: case OPR_GE: { + int dummy, dummy2; + if (!isSCnumber(v, &dummy, &dummy2)) + luaK_exp2anyreg(fs, v); + /* else keep numeral, which may be an immediate operand */ + break; + } + default: lua_assert(0); + } +} + +/* +** Create code for '(e1 .. e2)'. +** For '(e1 .. e2.1 .. e2.2)' (which is '(e1 .. (e2.1 .. e2.2))', +** because concatenation is right associative), merge both CONCATs. +*/ +static void codeconcat (FuncState *fs, expdesc *e1, expdesc *e2, int line) { + Instruction *ie2 = previousinstruction(fs); + if (GET_OPCODE(*ie2) == OP_CONCAT) { /* is 'e2' a concatenation? */ + int n = GETARG_B(*ie2); /* # of elements concatenated in 'e2' */ + lua_assert(e1->u.info + 1 == GETARG_A(*ie2)); + freeexp(fs, e2); + SETARG_A(*ie2, e1->u.info); /* correct first element ('e1') */ + SETARG_B(*ie2, n + 1); /* will concatenate one more element */ + } + else { /* 'e2' is not a concatenation */ + luaK_codeABC(fs, OP_CONCAT, e1->u.info, 2, 0); /* new concat opcode */ + freeexp(fs, e2); + luaK_fixline(fs, line); + } +} + + +/* +** Finalize code for binary operation, after reading 2nd operand. +*/ +void luaK_posfix (FuncState *fs, BinOpr opr, + expdesc *e1, expdesc *e2, int line) { + luaK_dischargevars(fs, e2); + if (foldbinop(opr) && constfolding(fs, cast_int(opr + LUA_OPADD), e1, e2)) + return; /* done by folding */ + switch (opr) { + case OPR_AND: { + lua_assert(e1->t == NO_JUMP); /* list closed by 'luaK_infix' */ + luaK_concat(fs, &e2->f, e1->f); + *e1 = *e2; + break; + } + case OPR_OR: { + lua_assert(e1->f == NO_JUMP); /* list closed by 'luaK_infix' */ + luaK_concat(fs, &e2->t, e1->t); + *e1 = *e2; + break; + } + case OPR_CONCAT: { /* e1 .. e2 */ + luaK_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, OP_ADDI, line, TM_SUB)) + break; /* coded as (r1 + -I) */ + /* ELSE */ + } /* 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, OP_SHLI, e1, e2, 1, line, TM_SHL); /* I << r2 */ + } + else if (finishbinexpneg(fs, e1, e2, OP_SHRI, line, TM_SHL)) { + /* coded as (r1 >> -I) */; + } + else /* regular case (two registers) */ + codebinexpval(fs, opr, e1, e2, line); + break; + } + case OPR_SHR: { + if (isSCint(e2)) + codebini(fs, OP_SHRI, e1, e2, 0, line, TM_SHR); /* r1 >> I */ + else /* regular case (two registers) */ + 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 = cast(BinOpr, (opr - OPR_GT) + OPR_LT); + } /* FALLTHROUGH */ + case OPR_LT: case OPR_LE: { + codeorder(fs, opr, e1, e2); + break; + } + default: lua_assert(0); + } +} + + +/* +** Change line information associated with current position, by removing +** previous info and adding it again with new line. +*/ +void luaK_fixline (FuncState *fs, int line) { + removelastlineinfo(fs); + savelineinfo(fs, fs->f, line); +} + + +void luaK_settablesize (FuncState *fs, int pc, int ra, int asize, int hsize) { + Instruction *inst = &fs->f->code[pc]; + int extra = asize / (MAXARG_vC + 1); /* higher bits of array size */ + int rc = asize % (MAXARG_vC + 1); /* lower bits of array size */ + int k = (extra > 0); /* true iff needs extra argument */ + hsize = (hsize != 0) ? luaO_ceillog2(cast_uint(hsize)) + 1 : 0; + *inst = CREATE_vABCk(OP_NEWTABLE, ra, hsize, rc, k); + *(inst + 1) = CREATE_Ax(OP_EXTRAARG, extra); +} + + +/* +** Emit a SETLIST instruction. +** 'base' is register that keeps table; +** 'nelems' is #table plus those to be stored now; +** 'tostore' is number of values (in registers 'base + 1',...) to add to +** table (or LUA_MULTRET to add up to stack top). +*/ +void luaK_setlist (FuncState *fs, int base, int nelems, int tostore) { + lua_assert(tostore != 0); + if (tostore == LUA_MULTRET) + tostore = 0; + if (nelems <= MAXARG_vC) + luaK_codevABCk(fs, OP_SETLIST, base, tostore, nelems, 0); + else { + int extra = nelems / (MAXARG_vC + 1); + nelems %= (MAXARG_vC + 1); + luaK_codevABCk(fs, OP_SETLIST, base, tostore, nelems, 1); + codeextraarg(fs, extra); + } + fs->freereg = cast_byte(base + 1); /* free registers with list values */ +} + + +/* +** return the final target of a jump (skipping jumps to jumps) +*/ +static int finaltarget (Instruction *code, int i) { + int count; + for (count = 0; count < 100; count++) { /* avoid infinite loops */ + Instruction pc = code[i]; + if (GET_OPCODE(pc) != OP_JMP) + break; + else + i += GETARG_sJ(pc) + 1; + } + return i; +} + + +/* +** Do a final pass over the code of a function, doing small peephole +** optimizations and adjustments. +*/ +#include "lopnames.h" +void luaK_finish (FuncState *fs) { + int i; + Proto *p = fs->f; + if (p->flag & PF_VATAB) /* will it use a vararg table? */ + p->flag &= cast_byte(~PF_VAHID); /* then it will not use hidden args. */ + for (i = 0; i < fs->pc; i++) { + Instruction *pc = &p->code[i]; + lua_assert(i == 0 || luaP_isOT(*(pc - 1)) == luaP_isIT(*pc)); + switch (GET_OPCODE(*pc)) { + case OP_RETURN0: case OP_RETURN1: { + if (!(fs->needclose || (p->flag & PF_VAHID))) + break; /* no extra work */ + /* else use OP_RETURN to do the extra work */ + SET_OPCODE(*pc, OP_RETURN); + } /* FALLTHROUGH */ + case OP_RETURN: case OP_TAILCALL: { + if (fs->needclose) + SETARG_k(*pc, 1); /* signal that it needs to close */ + if (p->flag & PF_VAHID) /* does it use hidden arguments? */ + SETARG_C(*pc, p->numparams + 1); /* signal that */ + break; + } + case OP_GETVARG: { + if (p->flag & PF_VATAB) /* function has a vararg table? */ + SET_OPCODE(*pc, OP_GETTABLE); /* must get vararg there */ + break; + } + case OP_VARARG: { + if (p->flag & PF_VATAB) /* function has a vararg table? */ + SETARG_k(*pc, 1); /* must get vararg there */ + break; + } + case OP_JMP: { /* to optimize jumps to jumps */ + int target = finaltarget(p->code, i); + fixjump(fs, i, target); /* jump directly to final target */ + break; + } + default: break; + } + } +} diff --git a/crates/lua/vendor/lua/lcode.h b/crates/lua/vendor/lua/lcode.h new file mode 100644 index 0000000..09e5c80 --- /dev/null +++ b/crates/lua/vendor/lua/lcode.h @@ -0,0 +1,105 @@ +/* +** $Id: lcode.h $ +** Code generator for Lua +** See Copyright Notice in lua.h +*/ + +#ifndef lcode_h +#define lcode_h + +#include "llex.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lparser.h" + + +/* +** Marks the end of a patch list. It is an invalid value both as an absolute +** address, and as a list link (would link an element to itself). +*/ +#define NO_JUMP (-1) + + +/* +** grep "ORDER OPR" if you change these enums (ORDER OP) +*/ +typedef enum BinOpr { + /* arithmetic operators */ + OPR_ADD, OPR_SUB, OPR_MUL, OPR_MOD, OPR_POW, + OPR_DIV, OPR_IDIV, + /* bitwise operators */ + OPR_BAND, OPR_BOR, OPR_BXOR, + OPR_SHL, OPR_SHR, + /* string operator */ + OPR_CONCAT, + /* comparison operators */ + OPR_EQ, OPR_LT, OPR_LE, + OPR_NE, OPR_GT, OPR_GE, + /* logical operators */ + OPR_AND, OPR_OR, + OPR_NOBINOPR +} BinOpr; + + +/* true if operation is foldable (that is, it is arithmetic or bitwise) */ +#define foldbinop(op) ((op) <= OPR_SHR) + + +#define luaK_codeABC(fs,o,a,b,c) luaK_codeABCk(fs,o,a,b,c,0) + + +typedef enum UnOpr { OPR_MINUS, OPR_BNOT, OPR_NOT, OPR_LEN, OPR_NOUNOPR } UnOpr; + + +/* get (pointer to) instruction of given 'expdesc' */ +#define getinstruction(fs,e) ((fs)->f->code[(e)->u.info]) + + +#define luaK_setmultret(fs,e) luaK_setreturns(fs, e, LUA_MULTRET) + +#define luaK_jumpto(fs,t) luaK_patchlist(fs, luaK_jump(fs), t) + +LUAI_FUNC int luaK_code (FuncState *fs, Instruction i); +LUAI_FUNC int luaK_codeABx (FuncState *fs, OpCode o, int A, int Bx); +LUAI_FUNC int luaK_codeABCk (FuncState *fs, OpCode o, int A, int B, int C, + int k); +LUAI_FUNC int luaK_codevABCk (FuncState *fs, OpCode o, int A, int B, int C, + int k); +LUAI_FUNC int luaK_exp2const (FuncState *fs, const expdesc *e, TValue *v); +LUAI_FUNC void luaK_fixline (FuncState *fs, int line); +LUAI_FUNC void luaK_nil (FuncState *fs, int from, int n); +LUAI_FUNC void luaK_codecheckglobal (FuncState *fs, expdesc *var, int k, + int line); +LUAI_FUNC void luaK_reserveregs (FuncState *fs, int n); +LUAI_FUNC void luaK_checkstack (FuncState *fs, int n); +LUAI_FUNC void luaK_int (FuncState *fs, int reg, lua_Integer n); +LUAI_FUNC void luaK_vapar2local (FuncState *fs, expdesc *var); +LUAI_FUNC void luaK_dischargevars (FuncState *fs, expdesc *e); +LUAI_FUNC int luaK_exp2anyreg (FuncState *fs, expdesc *e); +LUAI_FUNC void luaK_exp2anyregup (FuncState *fs, expdesc *e); +LUAI_FUNC void luaK_exp2nextreg (FuncState *fs, expdesc *e); +LUAI_FUNC void luaK_exp2val (FuncState *fs, expdesc *e); +LUAI_FUNC void luaK_self (FuncState *fs, expdesc *e, expdesc *key); +LUAI_FUNC void luaK_indexed (FuncState *fs, expdesc *t, expdesc *k); +LUAI_FUNC void luaK_goiftrue (FuncState *fs, expdesc *e); +LUAI_FUNC void luaK_storevar (FuncState *fs, expdesc *var, expdesc *e); +LUAI_FUNC void luaK_setreturns (FuncState *fs, expdesc *e, int nresults); +LUAI_FUNC void luaK_setoneret (FuncState *fs, expdesc *e); +LUAI_FUNC int luaK_jump (FuncState *fs); +LUAI_FUNC void luaK_ret (FuncState *fs, int first, int nret); +LUAI_FUNC void luaK_patchlist (FuncState *fs, int list, int target); +LUAI_FUNC void luaK_patchtohere (FuncState *fs, int list); +LUAI_FUNC void luaK_concat (FuncState *fs, int *l1, int l2); +LUAI_FUNC int luaK_getlabel (FuncState *fs); +LUAI_FUNC void luaK_prefix (FuncState *fs, UnOpr op, expdesc *v, int line); +LUAI_FUNC void luaK_infix (FuncState *fs, BinOpr op, expdesc *v); +LUAI_FUNC void luaK_posfix (FuncState *fs, BinOpr op, expdesc *v1, + expdesc *v2, int line); +LUAI_FUNC void luaK_settablesize (FuncState *fs, int pc, + int ra, int asize, int hsize); +LUAI_FUNC void luaK_setlist (FuncState *fs, int base, int nelems, int tostore); +LUAI_FUNC void luaK_finish (FuncState *fs); +LUAI_FUNC l_noret luaK_semerror (LexState *ls, const char *fmt, ...); + + +#endif diff --git a/crates/lua/vendor/lua/lcorolib.c b/crates/lua/vendor/lua/lcorolib.c new file mode 100644 index 0000000..b778939 --- /dev/null +++ b/crates/lua/vendor/lua/lcorolib.c @@ -0,0 +1,224 @@ +/* +** $Id: lcorolib.c $ +** Coroutine Library +** See Copyright Notice in lua.h +*/ + +#define lcorolib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" +#include "llimits.h" + + +static lua_State *getco (lua_State *L) { + lua_State *co = lua_tothread(L, 1); + luaL_argexpected(L, co, 1, "thread"); + return co; +} + + +/* +** Resumes a coroutine. Returns the number of results for non-error +** cases or -1 for errors. +*/ +static int auxresume (lua_State *L, lua_State *co, int narg) { + int status, nres; + if (l_unlikely(!lua_checkstack(co, narg))) { + lua_pushliteral(L, "too many arguments to resume"); + return -1; /* error flag */ + } + lua_xmove(L, co, narg); + status = lua_resume(co, L, narg, &nres); + if (l_likely(status == LUA_OK || status == LUA_YIELD)) { + if (l_unlikely(!lua_checkstack(L, nres + 1))) { + lua_pop(co, nres); /* remove results anyway */ + lua_pushliteral(L, "too many results to resume"); + return -1; /* error flag */ + } + lua_xmove(co, L, nres); /* move yielded values */ + return nres; + } + else { + lua_xmove(co, L, 1); /* move error message */ + return -1; /* error flag */ + } +} + + +static int luaB_coresume (lua_State *L) { + lua_State *co = getco(L); + int r; + r = auxresume(L, co, lua_gettop(L) - 1); + if (l_unlikely(r < 0)) { + lua_pushboolean(L, 0); + lua_insert(L, -2); + return 2; /* return false + error message */ + } + else { + lua_pushboolean(L, 1); + lua_insert(L, -(r + 1)); + return r + 1; /* return true + 'resume' returns */ + } +} + + +static int luaB_auxwrap (lua_State *L) { + lua_State *co = lua_tothread(L, lua_upvalueindex(1)); + int r = auxresume(L, co, lua_gettop(L)); + if (l_unlikely(r < 0)) { /* error? */ + int stat = lua_status(co); + if (stat != LUA_OK && stat != LUA_YIELD) { /* error in the coroutine? */ + stat = lua_closethread(co, L); /* close its tbc variables */ + lua_assert(stat != LUA_OK); + lua_xmove(co, L, 1); /* move error message to the caller */ + } + if (stat != LUA_ERRMEM && /* not a memory error and ... */ + lua_type(L, -1) == LUA_TSTRING) { /* ... error object is a string? */ + luaL_where(L, 1); /* add extra info, if available */ + lua_insert(L, -2); + lua_concat(L, 2); + } + return lua_error(L); /* propagate error */ + } + return r; +} + + +static int luaB_cocreate (lua_State *L) { + lua_State *NL; + luaL_checktype(L, 1, LUA_TFUNCTION); + NL = lua_newthread(L); + lua_pushvalue(L, 1); /* move function to top */ + lua_xmove(L, NL, 1); /* move function from L to NL */ + return 1; +} + + +static int luaB_cowrap (lua_State *L) { + luaB_cocreate(L); + lua_pushcclosure(L, luaB_auxwrap, 1); + return 1; +} + + +static int luaB_yield (lua_State *L) { + return lua_yield(L, lua_gettop(L)); +} + + +#define COS_RUN 0 +#define COS_DEAD 1 +#define COS_YIELD 2 +#define COS_NORM 3 + + +static const char *const statname[] = + {"running", "dead", "suspended", "normal"}; + + +static int auxstatus (lua_State *L, lua_State *co) { + if (L == co) return COS_RUN; + else { + switch (lua_status(co)) { + case LUA_YIELD: + return COS_YIELD; + case LUA_OK: { + lua_Debug ar; + if (lua_getstack(co, 0, &ar)) /* does it have frames? */ + return COS_NORM; /* it is running */ + else if (lua_gettop(co) == 0) + return COS_DEAD; + else + return COS_YIELD; /* initial state */ + } + default: /* some error occurred */ + return COS_DEAD; + } + } +} + + +static int luaB_costatus (lua_State *L) { + lua_State *co = getco(L); + lua_pushstring(L, statname[auxstatus(L, co)]); + return 1; +} + + +static lua_State *getoptco (lua_State *L) { + return (lua_isnone(L, 1) ? L : getco(L)); +} + + +static int luaB_yieldable (lua_State *L) { + lua_State *co = getoptco(L); + lua_pushboolean(L, lua_isyieldable(co)); + return 1; +} + + +static int luaB_corunning (lua_State *L) { + int ismain = lua_pushthread(L); + lua_pushboolean(L, ismain); + return 2; +} + + +static int luaB_close (lua_State *L) { + lua_State *co = getoptco(L); + int status = auxstatus(L, co); + switch (status) { + case COS_DEAD: case COS_YIELD: { + status = lua_closethread(co, L); + if (status == LUA_OK) { + lua_pushboolean(L, 1); + return 1; + } + else { + lua_pushboolean(L, 0); + lua_xmove(co, L, 1); /* move error message */ + return 2; + } + } + case COS_NORM: + return luaL_error(L, "cannot close a %s coroutine", statname[status]); + case COS_RUN: + lua_geti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD); /* get main */ + if (lua_tothread(L, -1) == co) + return luaL_error(L, "cannot close main thread"); + lua_closethread(co, L); /* close itself */ + /* previous call does not return *//* FALLTHROUGH */ + default: + lua_assert(0); + return 0; + } +} + + +static const luaL_Reg co_funcs[] = { + {"create", luaB_cocreate}, + {"resume", luaB_coresume}, + {"running", luaB_corunning}, + {"status", luaB_costatus}, + {"wrap", luaB_cowrap}, + {"yield", luaB_yield}, + {"isyieldable", luaB_yieldable}, + {"close", luaB_close}, + {NULL, NULL} +}; + + + +LUAMOD_API int luaopen_coroutine (lua_State *L) { + luaL_newlib(L, co_funcs); + return 1; +} diff --git a/crates/lua/vendor/lua/lctype.c b/crates/lua/vendor/lua/lctype.c new file mode 100644 index 0000000..b1a43e4 --- /dev/null +++ b/crates/lua/vendor/lua/lctype.c @@ -0,0 +1,64 @@ +/* +** $Id: lctype.c $ +** 'ctype' functions for Lua +** See Copyright Notice in lua.h +*/ + +#define lctype_c +#define LUA_CORE + +#include "lprefix.h" + + +#include "lctype.h" + +#if !LUA_USE_CTYPE /* { */ + +#include + + +#if defined (LUA_UCID) /* accept UniCode IDentifiers? */ +/* consider all non-ASCII codepoints to be alphabetic */ +#define NONA 0x01 +#else +#define NONA 0x00 /* default */ +#endif + + +LUAI_DDEF const lu_byte luai_ctype_[UCHAR_MAX + 2] = { + 0x00, /* EOZ */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 0. */ + 0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 1. */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0c, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, /* 2. */ + 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, 0x16, /* 3. */ + 0x16, 0x16, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x05, /* 4. */ + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, /* 5. */ + 0x05, 0x05, 0x05, 0x04, 0x04, 0x04, 0x04, 0x05, + 0x04, 0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x05, /* 6. */ + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, /* 7. */ + 0x05, 0x05, 0x05, 0x04, 0x04, 0x04, 0x04, 0x00, + NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* 8. */ + NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, + NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* 9. */ + NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, + NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* a. */ + NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, + NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* b. */ + NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, + 0x00, 0x00, NONA, NONA, NONA, NONA, NONA, NONA, /* c. */ + NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, + NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* d. */ + NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, + NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, /* e. */ + NONA, NONA, NONA, NONA, NONA, NONA, NONA, NONA, + NONA, NONA, NONA, NONA, NONA, 0x00, 0x00, 0x00, /* f. */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +#endif /* } */ diff --git a/crates/lua/vendor/lua/lctype.h b/crates/lua/vendor/lua/lctype.h new file mode 100644 index 0000000..ca2e862 --- /dev/null +++ b/crates/lua/vendor/lua/lctype.h @@ -0,0 +1,100 @@ +/* +** $Id: lctype.h $ +** 'ctype' functions for Lua +** See Copyright Notice in lua.h +*/ + +#ifndef lctype_h +#define lctype_h + +#include "lua.h" + + +/* +** WARNING: the functions defined here do not necessarily correspond +** to the similar functions in the standard C ctype.h. They are +** optimized for the specific needs of Lua. +*/ + +#if !defined(LUA_USE_CTYPE) + +#if 'A' == 65 && '0' == 48 +/* ASCII case: can use its own tables; faster and fixed */ +#define LUA_USE_CTYPE 0 +#else +/* must use standard C ctype */ +#define LUA_USE_CTYPE 1 +#endif + +#endif + + +#if !LUA_USE_CTYPE /* { */ + +#include + +#include "llimits.h" + + +#define ALPHABIT 0 +#define DIGITBIT 1 +#define PRINTBIT 2 +#define SPACEBIT 3 +#define XDIGITBIT 4 + + +#define MASK(B) (1 << (B)) + + +/* +** add 1 to char to allow index -1 (EOZ) +*/ +#define testprop(c,p) (luai_ctype_[(c)+1] & (p)) + +/* +** 'lalpha' (Lua alphabetic) and 'lalnum' (Lua alphanumeric) both include '_' +*/ +#define lislalpha(c) testprop(c, MASK(ALPHABIT)) +#define lislalnum(c) testprop(c, (MASK(ALPHABIT) | MASK(DIGITBIT))) +#define lisdigit(c) testprop(c, MASK(DIGITBIT)) +#define lisspace(c) testprop(c, MASK(SPACEBIT)) +#define lisprint(c) testprop(c, MASK(PRINTBIT)) +#define lisxdigit(c) testprop(c, MASK(XDIGITBIT)) + + +/* +** In ASCII, this 'ltolower' is correct for alphabetic characters and +** for '.'. That is enough for Lua needs. ('check_exp' ensures that +** the character either is an upper-case letter or is unchanged by +** the transformation, which holds for lower-case letters and '.'.) +*/ +#define ltolower(c) \ + check_exp(('A' <= (c) && (c) <= 'Z') || (c) == ((c) | ('A' ^ 'a')), \ + (c) | ('A' ^ 'a')) + + +/* one entry for each character and for -1 (EOZ) */ +LUAI_DDEC(const lu_byte luai_ctype_[UCHAR_MAX + 2];) + + +#else /* }{ */ + +/* +** use standard C ctypes +*/ + +#include + + +#define lislalpha(c) (isalpha(c) || (c) == '_') +#define lislalnum(c) (isalnum(c) || (c) == '_') +#define lisdigit(c) (isdigit(c)) +#define lisspace(c) (isspace(c)) +#define lisprint(c) (isprint(c)) +#define lisxdigit(c) (isxdigit(c)) + +#define ltolower(c) (tolower(c)) + +#endif /* } */ + +#endif diff --git a/crates/lua/vendor/lua/ldebug.c b/crates/lua/vendor/lua/ldebug.c new file mode 100644 index 0000000..d3782f2 --- /dev/null +++ b/crates/lua/vendor/lua/ldebug.c @@ -0,0 +1,978 @@ +/* +** $Id: ldebug.c $ +** Debug Interface +** See Copyright Notice in lua.h +*/ + +#define ldebug_c +#define LUA_CORE + +#include "lprefix.h" + + +#include +#include +#include + +#include "lua.h" + +#include "lapi.h" +#include "lcode.h" +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" +#include "lvm.h" + + + +#define LuaClosure(f) ((f) != NULL && (f)->c.tt == LUA_VLCL) + +static const char strlocal[] = "local"; +static const char strupval[] = "upvalue"; + +static const char *funcnamefromcall (lua_State *L, CallInfo *ci, + const char **name); + + +static int currentpc (CallInfo *ci) { + lua_assert(isLua(ci)); + return pcRel(ci->u.l.savedpc, ci_func(ci)->p); +} + + +/* +** Get a "base line" to find the line corresponding to an instruction. +** Base lines are regularly placed at MAXIWTHABS intervals, so usually +** an integer division gets the right place. When the source file has +** large sequences of empty/comment lines, it may need extra entries, +** so the original estimate needs a correction. +** If the original estimate is -1, the initial 'if' ensures that the +** 'while' will run at least once. +** The assertion that the estimate is a lower bound for the correct base +** is valid as long as the debug info has been generated with the same +** value for MAXIWTHABS or smaller. (Previous releases use a little +** smaller value.) +*/ +static int getbaseline (const Proto *f, int pc, int *basepc) { + if (f->sizeabslineinfo == 0 || pc < f->abslineinfo[0].pc) { + *basepc = -1; /* start from the beginning */ + return f->linedefined; + } + else { + int i = pc / MAXIWTHABS - 1; /* get an estimate */ + /* estimate must be a lower bound of the correct base */ + lua_assert(i < 0 || + (i < f->sizeabslineinfo && f->abslineinfo[i].pc <= pc)); + while (i + 1 < f->sizeabslineinfo && pc >= f->abslineinfo[i + 1].pc) + i++; /* low estimate; adjust it */ + *basepc = f->abslineinfo[i].pc; + return f->abslineinfo[i].line; + } +} + + +/* +** Get the line corresponding to instruction 'pc' in function 'f'; +** first gets a base line and from there does the increments until +** the desired instruction. +*/ +int luaG_getfuncline (const Proto *f, int pc) { + if (f->lineinfo == NULL) /* no debug information? */ + return -1; + else { + int basepc; + int baseline = getbaseline(f, pc, &basepc); + while (basepc++ < pc) { /* walk until given instruction */ + lua_assert(f->lineinfo[basepc] != ABSLINEINFO); + baseline += f->lineinfo[basepc]; /* correct line */ + } + return baseline; + } +} + + +static int getcurrentline (CallInfo *ci) { + return luaG_getfuncline(ci_func(ci)->p, currentpc(ci)); +} + + +/* +** Set 'trap' for all active Lua frames. +** This function can be called during a signal, under "reasonable" +** assumptions. A new 'ci' is completely linked in the list before it +** becomes part of the "active" list, and we assume that pointers are +** atomic; see comment in next function. +** (A compiler doing interprocedural optimizations could, theoretically, +** reorder memory writes in such a way that the list could be +** temporarily broken while inserting a new element. We simply assume it +** has no good reasons to do that.) +*/ +static void settraps (CallInfo *ci) { + for (; ci != NULL; ci = ci->previous) + if (isLua(ci)) + ci->u.l.trap = 1; +} + + +/* +** This function can be called during a signal, under "reasonable" +** assumptions. +** Fields 'basehookcount' and 'hookcount' (set by 'resethookcount') +** are for debug only, and it is no problem if they get arbitrary +** values (causes at most one wrong hook call). 'hookmask' is an atomic +** value. We assume that pointers are atomic too (e.g., gcc ensures that +** for all platforms where it runs). Moreover, 'hook' is always checked +** before being called (see 'luaD_hook'). +*/ +LUA_API void lua_sethook (lua_State *L, lua_Hook func, int mask, int count) { + if (func == NULL || mask == 0) { /* turn off hooks? */ + mask = 0; + func = NULL; + } + L->hook = func; + L->basehookcount = count; + resethookcount(L); + L->hookmask = cast_byte(mask); + if (mask) + settraps(L->ci); /* to trace inside 'luaV_execute' */ +} + + +LUA_API lua_Hook lua_gethook (lua_State *L) { + return L->hook; +} + + +LUA_API int lua_gethookmask (lua_State *L) { + return L->hookmask; +} + + +LUA_API int lua_gethookcount (lua_State *L) { + return L->basehookcount; +} + + +LUA_API int lua_getstack (lua_State *L, int level, lua_Debug *ar) { + int status; + CallInfo *ci; + if (level < 0) return 0; /* invalid (negative) level */ + lua_lock(L); + for (ci = L->ci; level > 0 && ci != &L->base_ci; ci = ci->previous) + level--; + if (level == 0 && ci != &L->base_ci) { /* level found? */ + status = 1; + ar->i_ci = ci; + } + else status = 0; /* no such level */ + lua_unlock(L); + return status; +} + + +static const char *upvalname (const Proto *p, int uv) { + TString *s = check_exp(uv < p->sizeupvalues, p->upvalues[uv].name); + if (s == NULL) return "?"; + else return getstr(s); +} + + +static const char *findvararg (CallInfo *ci, int n, StkId *pos) { + if (clLvalue(s2v(ci->func.p))->p->flag & PF_VAHID) { + int nextra = ci->u.l.nextraargs; + if (n >= -nextra) { /* 'n' is negative */ + *pos = ci->func.p - nextra - (n + 1); + return "(vararg)"; /* generic name for any vararg */ + } + } + return NULL; /* no such vararg */ +} + + +const char *luaG_findlocal (lua_State *L, CallInfo *ci, int n, StkId *pos) { + StkId base = ci->func.p + 1; + const char *name = NULL; + if (isLua(ci)) { + if (n < 0) /* access to vararg values? */ + return findvararg(ci, n, pos); + else + name = luaF_getlocalname(ci_func(ci)->p, n, currentpc(ci)); + } + if (name == NULL) { /* no 'standard' name? */ + StkId limit = (ci == L->ci) ? L->top.p : ci->next->func.p; + if (limit - base >= n && n > 0) { /* is 'n' inside 'ci' stack? */ + /* generic name for any valid slot */ + name = isLua(ci) ? "(temporary)" : "(C temporary)"; + } + else + return NULL; /* no name */ + } + if (pos) + *pos = base + (n - 1); + return name; +} + + +LUA_API const char *lua_getlocal (lua_State *L, const lua_Debug *ar, int n) { + const char *name; + lua_lock(L); + if (ar == NULL) { /* information about non-active function? */ + if (!isLfunction(s2v(L->top.p - 1))) /* not a Lua function? */ + name = NULL; + else /* consider live variables at function start (parameters) */ + name = luaF_getlocalname(clLvalue(s2v(L->top.p - 1))->p, n, 0); + } + else { /* active function; get information through 'ar' */ + StkId pos = NULL; /* to avoid warnings */ + name = luaG_findlocal(L, ar->i_ci, n, &pos); + if (name) { + setobjs2s(L, L->top.p, pos); + api_incr_top(L); + } + } + lua_unlock(L); + return name; +} + + +LUA_API const char *lua_setlocal (lua_State *L, const lua_Debug *ar, int n) { + StkId pos = NULL; /* to avoid warnings */ + const char *name; + lua_lock(L); + name = luaG_findlocal(L, ar->i_ci, n, &pos); + if (name) { + api_checkpop(L, 1); + setobjs2s(L, pos, L->top.p - 1); + L->top.p--; /* pop value */ + } + lua_unlock(L); + return name; +} + + +static void funcinfo (lua_Debug *ar, Closure *cl) { + if (!LuaClosure(cl)) { + ar->source = "=[C]"; + ar->srclen = LL("=[C]"); + ar->linedefined = -1; + ar->lastlinedefined = -1; + ar->what = "C"; + } + else { + const Proto *p = cl->l.p; + if (p->source) { + ar->source = getlstr(p->source, ar->srclen); + } + else { + ar->source = "=?"; + ar->srclen = LL("=?"); + } + ar->linedefined = p->linedefined; + ar->lastlinedefined = p->lastlinedefined; + ar->what = (ar->linedefined == 0) ? "main" : "Lua"; + } + luaO_chunkid(ar->short_src, ar->source, ar->srclen); +} + + +static int nextline (const Proto *p, int currentline, int pc) { + if (p->lineinfo[pc] != ABSLINEINFO) + return currentline + p->lineinfo[pc]; + else + return luaG_getfuncline(p, pc); +} + + +static void collectvalidlines (lua_State *L, Closure *f) { + if (!LuaClosure(f)) { + setnilvalue2s(L->top.p); + api_incr_top(L); + } + else { + const Proto *p = f->l.p; + int currentline = p->linedefined; + Table *t = luaH_new(L); /* new table to store active lines */ + sethvalue2s(L, L->top.p, t); /* push it on stack */ + api_incr_top(L); + if (p->lineinfo != NULL) { /* proto with debug information? */ + int i; + TValue v; + setbtvalue(&v); /* boolean 'true' to be the value of all indices */ + if (!(isvararg(p))) /* regular function? */ + i = 0; /* consider all instructions */ + else { /* vararg function */ + lua_assert(GET_OPCODE(p->code[0]) == OP_VARARGPREP); + currentline = nextline(p, currentline, 0); + i = 1; /* skip first instruction (OP_VARARGPREP) */ + } + for (; i < p->sizelineinfo; i++) { /* for each instruction */ + currentline = nextline(p, currentline, i); /* get its line */ + luaH_setint(L, t, currentline, &v); /* table[line] = true */ + } + } + } +} + + +static const char *getfuncname (lua_State *L, CallInfo *ci, const char **name) { + /* calling function is a known function? */ + if (ci != NULL && !(ci->callstatus & CIST_TAIL)) + return funcnamefromcall(L, ci->previous, name); + else return NULL; /* no way to find a name */ +} + + +static int auxgetinfo (lua_State *L, const char *what, lua_Debug *ar, + Closure *f, CallInfo *ci) { + int status = 1; + for (; *what; what++) { + switch (*what) { + case 'S': { + funcinfo(ar, f); + break; + } + case 'l': { + ar->currentline = (ci && isLua(ci)) ? getcurrentline(ci) : -1; + break; + } + case 'u': { + ar->nups = (f == NULL) ? 0 : f->c.nupvalues; + if (!LuaClosure(f)) { + ar->isvararg = 1; + ar->nparams = 0; + } + else { + ar->isvararg = (isvararg(f->l.p)) ? 1 : 0; + ar->nparams = f->l.p->numparams; + } + break; + } + case 't': { + if (ci != NULL) { + ar->istailcall = !!(ci->callstatus & CIST_TAIL); + ar->extraargs = + cast_uchar((ci->callstatus & MAX_CCMT) >> CIST_CCMT); + } + else { + ar->istailcall = 0; + ar->extraargs = 0; + } + break; + } + case 'n': { + ar->namewhat = getfuncname(L, ci, &ar->name); + if (ar->namewhat == NULL) { + ar->namewhat = ""; /* not found */ + ar->name = NULL; + } + break; + } + case 'r': { + if (ci == NULL || !(ci->callstatus & CIST_HOOKED)) + ar->ftransfer = ar->ntransfer = 0; + else { + ar->ftransfer = L->transferinfo.ftransfer; + ar->ntransfer = L->transferinfo.ntransfer; + } + break; + } + case 'L': + case 'f': /* handled by lua_getinfo */ + break; + default: status = 0; /* invalid option */ + } + } + return status; +} + + +LUA_API int lua_getinfo (lua_State *L, const char *what, lua_Debug *ar) { + int status; + Closure *cl; + CallInfo *ci; + TValue *func; + lua_lock(L); + if (*what == '>') { + ci = NULL; + func = s2v(L->top.p - 1); + api_check(L, ttisfunction(func), "function expected"); + what++; /* skip the '>' */ + L->top.p--; /* pop function */ + } + else { + ci = ar->i_ci; + func = s2v(ci->func.p); + lua_assert(ttisfunction(func)); + } + cl = ttisclosure(func) ? clvalue(func) : NULL; + status = auxgetinfo(L, what, ar, cl, ci); + if (strchr(what, 'f')) { + setobj2s(L, L->top.p, func); + api_incr_top(L); + } + if (strchr(what, 'L')) + collectvalidlines(L, cl); + lua_unlock(L); + return status; +} + + +/* +** {====================================================== +** Symbolic Execution +** ======================================================= +*/ + + +static int filterpc (int pc, int jmptarget) { + if (pc < jmptarget) /* is code conditional (inside a jump)? */ + return -1; /* cannot know who sets that register */ + else return pc; /* current position sets that register */ +} + + +/* +** Try to find last instruction before 'lastpc' that modified register 'reg'. +*/ +static int findsetreg (const Proto *p, int lastpc, int reg) { + int pc; + int setreg = -1; /* keep last instruction that changed 'reg' */ + int jmptarget = 0; /* any code before this address is conditional */ + if (testMMMode(GET_OPCODE(p->code[lastpc]))) + lastpc--; /* previous instruction was not actually executed */ + for (pc = 0; pc < lastpc; pc++) { + Instruction i = p->code[pc]; + OpCode op = GET_OPCODE(i); + int a = GETARG_A(i); + int change; /* true if current instruction changed 'reg' */ + switch (op) { + case OP_LOADNIL: { /* set registers from 'a' to 'a+b' */ + int b = GETARG_B(i); + change = (a <= reg && reg <= a + b); + break; + } + case OP_TFORCALL: { /* affect all regs above its base */ + change = (reg >= a + 2); + break; + } + case OP_CALL: + case OP_TAILCALL: { /* affect all registers above base */ + change = (reg >= a); + break; + } + case OP_JMP: { /* doesn't change registers, but changes 'jmptarget' */ + int b = GETARG_sJ(i); + int dest = pc + 1 + b; + /* jump does not skip 'lastpc' and is larger than current one? */ + if (dest <= lastpc && dest > jmptarget) + jmptarget = dest; /* update 'jmptarget' */ + change = 0; + break; + } + default: /* any instruction that sets A */ + change = (testAMode(op) && reg == a); + break; + } + if (change) + setreg = filterpc(pc, jmptarget); + } + return setreg; +} + + +/* +** Find a "name" for the constant 'c'. +*/ +static const char *kname (const Proto *p, int index, const char **name) { + TValue *kvalue = &p->k[index]; + if (ttisstring(kvalue)) { + *name = getstr(tsvalue(kvalue)); + return "constant"; + } + else { + *name = "?"; + return NULL; + } +} + + +static const char *basicgetobjname (const Proto *p, int *ppc, int reg, + const char **name) { + int pc = *ppc; + *name = luaF_getlocalname(p, reg + 1, pc); + if (*name) /* is a local? */ + return strlocal; + /* else try symbolic execution */ + *ppc = pc = findsetreg(p, pc, reg); + if (pc != -1) { /* could find instruction? */ + Instruction i = p->code[pc]; + OpCode op = GET_OPCODE(i); + switch (op) { + case OP_MOVE: { + int b = GETARG_B(i); /* move from 'b' to 'a' */ + if (b < GETARG_A(i)) + return basicgetobjname(p, ppc, b, name); /* get name for 'b' */ + break; + } + case OP_GETUPVAL: { + *name = upvalname(p, GETARG_B(i)); + return strupval; + } + case OP_LOADK: return kname(p, GETARG_Bx(i), name); + case OP_LOADKX: return kname(p, GETARG_Ax(p->code[pc + 1]), name); + default: break; + } + } + return NULL; /* could not find reasonable name */ +} + + +/* +** Find a "name" for the register 'c'. +*/ +static void rname (const Proto *p, int pc, int c, const char **name) { + const char *what = basicgetobjname(p, &pc, c, name); /* search for 'c' */ + if (!(what && *what == 'c')) /* did not find a constant name? */ + *name = "?"; +} + + +/* +** Check whether table being indexed by instruction 'i' is the +** environment '_ENV' +*/ +static const char *isEnv (const Proto *p, int pc, Instruction i, int isup) { + int t = GETARG_B(i); /* table index */ + const char *name; /* name of indexed variable */ + if (isup) /* is 't' an upvalue? */ + name = upvalname(p, t); + else { /* 't' is a register */ + const char *what = basicgetobjname(p, &pc, t, &name); + /* 'name' must be the name of a local variable (at the current + level or an upvalue) */ + if (what != strlocal && what != strupval) + name = NULL; /* cannot be the variable _ENV */ + } + return (name && strcmp(name, LUA_ENV) == 0) ? "global" : "field"; +} + + +/* +** Extend 'basicgetobjname' to handle table accesses +*/ +static const char *getobjname (const Proto *p, int lastpc, int reg, + const char **name) { + const char *kind = basicgetobjname(p, &lastpc, reg, name); + if (kind != NULL) + return kind; + else if (lastpc != -1) { /* could find instruction? */ + Instruction i = p->code[lastpc]; + OpCode op = GET_OPCODE(i); + switch (op) { + case OP_GETTABUP: { + int k = GETARG_C(i); /* key index */ + kname(p, k, name); + return isEnv(p, lastpc, i, 1); + } + case OP_GETTABLE: case OP_GETVARG: { + int k = GETARG_C(i); /* key index */ + rname(p, lastpc, k, name); + return isEnv(p, lastpc, i, 0); + } + case OP_GETI: { + *name = "integer index"; + return "field"; + } + case OP_GETFIELD: { + int k = GETARG_C(i); /* key index */ + kname(p, k, name); + return isEnv(p, lastpc, i, 0); + } + case OP_SELF: { + int k = GETARG_C(i); /* key index */ + kname(p, k, name); + return "method"; + } + default: break; /* go through to return NULL */ + } + } + return NULL; /* could not find reasonable name */ +} + + +/* +** Try to find a name for a function based on the code that called it. +** (Only works when function was called by a Lua function.) +** Returns what the name is (e.g., "for iterator", "method", +** "metamethod") and sets '*name' to point to the name. +*/ +static const char *funcnamefromcode (lua_State *L, const Proto *p, + int pc, const char **name) { + TMS tm = (TMS)0; /* (initial value avoids warnings) */ + Instruction i = p->code[pc]; /* calling instruction */ + switch (GET_OPCODE(i)) { + case OP_CALL: + case OP_TAILCALL: + return getobjname(p, pc, GETARG_A(i), name); /* get function name */ + case OP_TFORCALL: { /* for iterator */ + *name = "for iterator"; + return "for iterator"; + } + /* other instructions can do calls through metamethods */ + case OP_SELF: case OP_GETTABUP: case OP_GETTABLE: + case OP_GETI: case OP_GETFIELD: + tm = TM_INDEX; + break; + case OP_SETTABUP: case OP_SETTABLE: case OP_SETI: case OP_SETFIELD: + tm = TM_NEWINDEX; + break; + case OP_MMBIN: case OP_MMBINI: case OP_MMBINK: { + tm = cast(TMS, GETARG_C(i)); + break; + } + case OP_UNM: tm = TM_UNM; break; + case OP_BNOT: tm = TM_BNOT; break; + case OP_LEN: tm = TM_LEN; break; + case OP_CONCAT: tm = TM_CONCAT; break; + case OP_EQ: tm = TM_EQ; break; + /* no cases for OP_EQI and OP_EQK, as they don't call metamethods */ + case OP_LT: case OP_LTI: case OP_GTI: tm = TM_LT; break; + case OP_LE: case OP_LEI: case OP_GEI: tm = TM_LE; break; + case OP_CLOSE: case OP_RETURN: tm = TM_CLOSE; break; + default: + return NULL; /* cannot find a reasonable name */ + } + *name = getshrstr(G(L)->tmname[tm]) + 2; + return "metamethod"; +} + + +/* +** Try to find a name for a function based on how it was called. +*/ +static const char *funcnamefromcall (lua_State *L, CallInfo *ci, + const char **name) { + if (ci->callstatus & CIST_HOOKED) { /* was it called inside a hook? */ + *name = "?"; + return "hook"; + } + else if (ci->callstatus & CIST_FIN) { /* was it called as a finalizer? */ + *name = "__gc"; + return "metamethod"; /* report it as such */ + } + else if (isLua(ci)) + return funcnamefromcode(L, ci_func(ci)->p, currentpc(ci), name); + else + return NULL; +} + +/* }====================================================== */ + + + +/* +** Check whether pointer 'o' points to some value in the stack frame of +** the current function and, if so, returns its index. Because 'o' may +** not point to a value in this stack, we cannot compare it with the +** region boundaries (undefined behavior in ISO C). +*/ +static int instack (CallInfo *ci, const TValue *o) { + int pos; + StkId base = ci->func.p + 1; + for (pos = 0; base + pos < ci->top.p; pos++) { + if (o == s2v(base + pos)) + return pos; + } + return -1; /* not found */ +} + + +/* +** Checks whether value 'o' came from an upvalue. (That can only happen +** with instructions OP_GETTABUP/OP_SETTABUP, which operate directly on +** upvalues.) +*/ +static const char *getupvalname (CallInfo *ci, const TValue *o, + const char **name) { + LClosure *c = ci_func(ci); + int i; + for (i = 0; i < c->nupvalues; i++) { + if (c->upvals[i]->v.p == o) { + *name = upvalname(c->p, i); + return strupval; + } + } + return NULL; +} + + +static const char *formatvarinfo (lua_State *L, const char *kind, + const char *name) { + if (kind == NULL) + return ""; /* no information */ + else + return luaO_pushfstring(L, " (%s '%s')", kind, name); +} + +/* +** Build a string with a "description" for the value 'o', such as +** "variable 'x'" or "upvalue 'y'". +*/ +static const char *varinfo (lua_State *L, const TValue *o) { + CallInfo *ci = L->ci; + const char *name = NULL; /* to avoid warnings */ + const char *kind = NULL; + if (isLua(ci)) { + kind = getupvalname(ci, o, &name); /* check whether 'o' is an upvalue */ + if (!kind) { /* not an upvalue? */ + int reg = instack(ci, o); /* try a register */ + if (reg >= 0) /* is 'o' a register? */ + kind = getobjname(ci_func(ci)->p, currentpc(ci), reg, &name); + } + } + return formatvarinfo(L, kind, name); +} + + +/* +** Raise a type error +*/ +static l_noret typeerror (lua_State *L, const TValue *o, const char *op, + const char *extra) { + const char *t = luaT_objtypename(L, o); + luaG_runerror(L, "attempt to %s a %s value%s", op, t, extra); +} + + +/* +** Raise a type error with "standard" information about the faulty +** object 'o' (using 'varinfo'). +*/ +l_noret luaG_typeerror (lua_State *L, const TValue *o, const char *op) { + typeerror(L, o, op, varinfo(L, o)); +} + + +/* +** Raise an error for calling a non-callable object. Try to find a name +** for the object based on how it was called ('funcnamefromcall'); if it +** cannot get a name there, try 'varinfo'. +*/ +l_noret luaG_callerror (lua_State *L, const TValue *o) { + CallInfo *ci = L->ci; + const char *name = NULL; /* to avoid warnings */ + const char *kind = funcnamefromcall(L, ci, &name); + const char *extra = kind ? formatvarinfo(L, kind, name) : varinfo(L, o); + typeerror(L, o, "call", extra); +} + + +l_noret luaG_forerror (lua_State *L, const TValue *o, const char *what) { + luaG_runerror(L, "bad 'for' %s (number expected, got %s)", + what, luaT_objtypename(L, o)); +} + + +l_noret luaG_concaterror (lua_State *L, const TValue *p1, const TValue *p2) { + if (ttisstring(p1) || cvt2str(p1)) p1 = p2; + luaG_typeerror(L, p1, "concatenate"); +} + + +l_noret luaG_opinterror (lua_State *L, const TValue *p1, + const TValue *p2, const char *msg) { + if (!ttisnumber(p1)) /* first operand is wrong? */ + p2 = p1; /* now second is wrong */ + luaG_typeerror(L, p2, msg); +} + + +/* +** Error when both values are convertible to numbers, but not to integers +*/ +l_noret 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)); +} + + +l_noret luaG_ordererror (lua_State *L, const TValue *p1, const TValue *p2) { + const char *t1 = luaT_objtypename(L, p1); + const char *t2 = luaT_objtypename(L, p2); + if (strcmp(t1, t2) == 0) + luaG_runerror(L, "attempt to compare two %s values", t1); + else + luaG_runerror(L, "attempt to compare %s with %s", t1, t2); +} + + +l_noret luaG_errnnil (lua_State *L, LClosure *cl, int k) { + const char *globalname = "?"; /* default name if k == 0 */ + if (k > 0) + kname(cl->p, k - 1, &globalname); + luaG_runerror(L, "global '%s' already defined", globalname); +} + + +/* add src:line information to 'msg' */ +const char *luaG_addinfo (lua_State *L, const char *msg, TString *src, + int line) { + if (src == NULL) /* no debug information? */ + return luaO_pushfstring(L, "?:?: %s", msg); + else { + char buff[LUA_IDSIZE]; + size_t idlen; + const char *id = getlstr(src, idlen); + luaO_chunkid(buff, id, idlen); + return luaO_pushfstring(L, "%s:%d: %s", buff, line, msg); + } +} + + +l_noret luaG_errormsg (lua_State *L) { + if (L->errfunc != 0) { /* is there an error handling function? */ + StkId errfunc = restorestack(L, L->errfunc); + lua_assert(ttisfunction(s2v(errfunc))); + setobjs2s(L, L->top.p, L->top.p - 1); /* move argument */ + setobjs2s(L, L->top.p - 1, errfunc); /* push function */ + L->top.p++; /* assume EXTRA_STACK */ + luaD_callnoyield(L, L->top.p - 2, 1); /* call it */ + } + if (ttisnil(s2v(L->top.p - 1))) { /* error object is nil? */ + /* change it to a proper message */ + setsvalue2s(L, L->top.p - 1, luaS_newliteral(L, "")); + } + luaD_throw(L, LUA_ERRRUN); +} + + +l_noret luaG_runerror (lua_State *L, const char *fmt, ...) { + CallInfo *ci = L->ci; + const char *msg; + va_list argp; + luaC_checkGC(L); /* error message uses memory */ + pushvfstring(L, argp, fmt, msg); + if (isLua(ci)) { /* Lua function? */ + /* add source:line information */ + luaG_addinfo(L, msg, ci_func(ci)->p->source, getcurrentline(ci)); + setobjs2s(L, L->top.p - 2, L->top.p - 1); /* remove 'msg' */ + L->top.p--; + } + luaG_errormsg(L); +} + + +/* +** Check whether new instruction 'newpc' is in a different line from +** previous instruction 'oldpc'. More often than not, 'newpc' is only +** one or a few instructions after 'oldpc' (it must be after, see +** caller), so try to avoid calling 'luaG_getfuncline'. If they are +** too far apart, there is a good chance of a ABSLINEINFO in the way, +** so it goes directly to 'luaG_getfuncline'. +*/ +static int changedline (const Proto *p, int oldpc, int newpc) { + if (p->lineinfo == NULL) /* no debug information? */ + return 0; + if (newpc - oldpc < MAXIWTHABS / 2) { /* not too far apart? */ + int delta = 0; /* line difference */ + int pc = oldpc; + for (;;) { + int lineinfo = p->lineinfo[++pc]; + if (lineinfo == ABSLINEINFO) + break; /* cannot compute delta; fall through */ + delta += lineinfo; + if (pc == newpc) + return (delta != 0); /* delta computed successfully */ + } + } + /* either instructions are too far apart or there is an absolute line + info in the way; compute line difference explicitly */ + return (luaG_getfuncline(p, oldpc) != luaG_getfuncline(p, newpc)); +} + + +/* +** Traces Lua calls. If code is running the first instruction of a function, +** and function is not vararg, and it is not coming from an yield, +** calls 'luaD_hookcall'. (Vararg functions will call 'luaD_hookcall' +** after adjusting its variable arguments; otherwise, they could call +** a line/count hook before the call hook. Functions coming from +** an yield already called 'luaD_hookcall' before yielding.) +*/ +int luaG_tracecall (lua_State *L) { + CallInfo *ci = L->ci; + Proto *p = ci_func(ci)->p; + ci->u.l.trap = 1; /* ensure hooks will be checked */ + if (ci->u.l.savedpc == p->code) { /* first instruction (not resuming)? */ + if (isvararg(p)) + return 0; /* hooks will start at VARARGPREP instruction */ + else if (!(ci->callstatus & CIST_HOOKYIELD)) /* not yielded? */ + luaD_hookcall(L, ci); /* check 'call' hook */ + } + return 1; /* keep 'trap' on */ +} + + +/* +** Traces the execution of a Lua function. Called before the execution +** of each opcode, when debug is on. 'L->oldpc' stores the last +** instruction traced, to detect line changes. When entering a new +** function, 'npci' will be zero and will test as a new line whatever +** the value of 'oldpc'. Some exceptional conditions may return to +** a function without setting 'oldpc'. In that case, 'oldpc' may be +** invalid; if so, use zero as a valid value. (A wrong but valid 'oldpc' +** at most causes an extra call to a line hook.) +** This function is not "Protected" when called, so it should correct +** 'L->top.p' before calling anything that can run the GC. +*/ +int luaG_traceexec (lua_State *L, const Instruction *pc) { + CallInfo *ci = L->ci; + lu_byte mask = cast_byte(L->hookmask); + const Proto *p = ci_func(ci)->p; + int counthook; + if (!(mask & (LUA_MASKLINE | LUA_MASKCOUNT))) { /* no hooks? */ + ci->u.l.trap = 0; /* don't need to stop again */ + return 0; /* turn off 'trap' */ + } + pc++; /* reference is always next instruction */ + ci->u.l.savedpc = pc; /* save 'pc' */ + counthook = (mask & LUA_MASKCOUNT) && (--L->hookcount == 0); + if (counthook) + resethookcount(L); /* reset count */ + else if (!(mask & LUA_MASKLINE)) + return 1; /* no line hook and count != 0; nothing to be done now */ + if (ci->callstatus & CIST_HOOKYIELD) { /* hook yielded last time? */ + ci->callstatus &= ~CIST_HOOKYIELD; /* erase mark */ + return 1; /* do not call hook again (VM yielded, so it did not move) */ + } + if (!luaP_isIT(*(ci->u.l.savedpc - 1))) /* top not being used? */ + L->top.p = ci->top.p; /* correct top */ + if (counthook) + luaD_hook(L, LUA_HOOKCOUNT, -1, 0, 0); /* call count hook */ + if (mask & LUA_MASKLINE) { + /* 'L->oldpc' may be invalid; use zero in this case */ + int oldpc = (L->oldpc < p->sizecode) ? L->oldpc : 0; + int npci = pcRel(pc, p); + if (npci <= oldpc || /* call hook when jump back (loop), */ + changedline(p, oldpc, npci)) { /* or when enter new line */ + int newline = luaG_getfuncline(p, npci); + luaD_hook(L, LUA_HOOKLINE, newline, 0, 0); /* call line hook */ + } + L->oldpc = npci; /* 'pc' of last call to line hook */ + } + if (L->status == LUA_YIELD) { /* did hook yield? */ + if (counthook) + L->hookcount = 1; /* undo decrement to zero */ + ci->callstatus |= CIST_HOOKYIELD; /* mark that it yielded */ + luaD_throw(L, LUA_YIELD); + } + return 1; /* keep 'trap' on */ +} diff --git a/crates/lua/vendor/lua/ldebug.h b/crates/lua/vendor/lua/ldebug.h new file mode 100644 index 0000000..20d0781 --- /dev/null +++ b/crates/lua/vendor/lua/ldebug.h @@ -0,0 +1,65 @@ +/* +** $Id: ldebug.h $ +** Auxiliary functions from Debug Interface module +** See Copyright Notice in lua.h +*/ + +#ifndef ldebug_h +#define ldebug_h + + +#include "lstate.h" + + +#define pcRel(pc, p) (cast_int((pc) - (p)->code) - 1) + + +/* Active Lua function (given call info) */ +#define ci_func(ci) (clLvalue(s2v((ci)->func.p))) + + +#define resethookcount(L) (L->hookcount = L->basehookcount) + +/* +** mark for entries in 'lineinfo' array that has absolute information in +** 'abslineinfo' array +*/ +#define ABSLINEINFO (-0x80) + + +/* +** MAXimum number of successive Instructions WiTHout ABSolute line +** information. (A power of two allows fast divisions.) +*/ +#if !defined(MAXIWTHABS) +#define MAXIWTHABS 128 +#endif + + +LUAI_FUNC int luaG_getfuncline (const Proto *f, int pc); +LUAI_FUNC const char *luaG_findlocal (lua_State *L, CallInfo *ci, int n, + StkId *pos); +LUAI_FUNC l_noret luaG_typeerror (lua_State *L, const TValue *o, + const char *opname); +LUAI_FUNC l_noret luaG_callerror (lua_State *L, const TValue *o); +LUAI_FUNC l_noret luaG_forerror (lua_State *L, const TValue *o, + const char *what); +LUAI_FUNC l_noret luaG_concaterror (lua_State *L, const TValue *p1, + const TValue *p2); +LUAI_FUNC l_noret luaG_opinterror (lua_State *L, const TValue *p1, + const TValue *p2, + const char *msg); +LUAI_FUNC l_noret luaG_tointerror (lua_State *L, const TValue *p1, + const TValue *p2); +LUAI_FUNC l_noret luaG_ordererror (lua_State *L, const TValue *p1, + const TValue *p2); +LUAI_FUNC l_noret luaG_errnnil (lua_State *L, LClosure *cl, int k); +LUAI_FUNC l_noret luaG_runerror (lua_State *L, const char *fmt, ...); +LUAI_FUNC const char *luaG_addinfo (lua_State *L, const char *msg, + TString *src, int line); +LUAI_FUNC l_noret luaG_errormsg (lua_State *L); +LUAI_FUNC int luaG_traceexec (lua_State *L, const Instruction *pc); +LUAI_FUNC int luaG_tracecall (lua_State *L); + + +#endif diff --git a/crates/lua/vendor/lua/ldo.c b/crates/lua/vendor/lua/ldo.c new file mode 100644 index 0000000..5b25d95 --- /dev/null +++ b/crates/lua/vendor/lua/ldo.c @@ -0,0 +1,1190 @@ +/* +** $Id: ldo.c $ +** Stack and Call structure of Lua +** See Copyright Notice in lua.h +*/ + +#define ldo_c +#define LUA_CORE + +#include "lprefix.h" + + +#include +#include +#include + +#include "lua.h" + +#include "lapi.h" +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lparser.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" +#include "lundump.h" +#include "lvm.h" +#include "lzio.h" + + + +#define errorstatus(s) ((s) > LUA_YIELD) + + +/* +** these macros allow user-specific actions when a thread is +** resumed/yielded. +*/ +#if !defined(luai_userstateresume) +#define luai_userstateresume(L,n) ((void)L) +#endif + +#if !defined(luai_userstateyield) +#define luai_userstateyield(L,n) ((void)L) +#endif + + +/* +** {====================================================== +** Error-recovery functions +** ======================================================= +*/ + +/* chained list of long jump buffers */ +typedef struct lua_longjmp { + struct lua_longjmp *previous; + jmp_buf b; + volatile TStatus status; /* error code */ +} lua_longjmp; + + +/* +** LUAI_THROW/LUAI_TRY define how Lua does exception handling. By +** default, Lua handles errors with exceptions when compiling as +** C++ code, with _longjmp/_setjmp when available (POSIX), and with +** longjmp/setjmp otherwise. +*/ +#if !defined(LUAI_THROW) /* { */ + +#if defined(__cplusplus) && !defined(LUA_USE_LONGJMP) /* { */ + +/* C++ exceptions */ +#define LUAI_THROW(L,c) throw(c) + +static void LUAI_TRY (lua_State *L, lua_longjmp *c, Pfunc f, void *ud) { + try { + f(L, ud); /* call function protected */ + } + catch (lua_longjmp *c1) { /* Lua error */ + if (c1 != c) /* not the correct level? */ + throw; /* rethrow to upper level */ + } + catch (...) { /* non-Lua exception */ + c->status = -1; /* create some error code */ + } +} + + +#elif defined(LUA_USE_POSIX) /* }{ */ + +/* in POSIX, use _longjmp/_setjmp (more efficient) */ +#define LUAI_THROW(L,c) _longjmp((c)->b, 1) +#define LUAI_TRY(L,c,f,ud) if (_setjmp((c)->b) == 0) ((f)(L, ud)) + +#else /* }{ */ + +/* ISO C handling with long jumps */ +#define LUAI_THROW(L,c) longjmp((c)->b, 1) +#define LUAI_TRY(L,c,f,ud) if (setjmp((c)->b) == 0) ((f)(L, ud)) + +#endif /* } */ + +#endif /* } */ + + +void luaD_seterrorobj (lua_State *L, TStatus errcode, StkId oldtop) { + if (errcode == LUA_ERRMEM) { /* memory error? */ + setsvalue2s(L, oldtop, G(L)->memerrmsg); /* reuse preregistered msg. */ + } + else { + lua_assert(errorstatus(errcode)); /* must be a real error */ + lua_assert(!ttisnil(s2v(L->top.p - 1))); /* with a non-nil object */ + setobjs2s(L, oldtop, L->top.p - 1); /* move it to 'oldtop' */ + } + L->top.p = oldtop + 1; /* top goes back to old top plus error object */ +} + + +l_noret luaD_throw (lua_State *L, TStatus errcode) { + if (L->errorJmp) { /* thread has an error handler? */ + L->errorJmp->status = errcode; /* set status */ + LUAI_THROW(L, L->errorJmp); /* jump to it */ + } + else { /* thread has no error handler */ + global_State *g = G(L); + lua_State *mainth = mainthread(g); + errcode = luaE_resetthread(L, errcode); /* close all upvalues */ + L->status = errcode; + if (mainth->errorJmp) { /* main thread has a handler? */ + setobjs2s(L, mainth->top.p++, L->top.p - 1); /* copy error obj. */ + luaD_throw(mainth, errcode); /* re-throw in main thread */ + } + else { /* no handler at all; abort */ + if (g->panic) { /* panic function? */ + lua_unlock(L); + g->panic(L); /* call panic function (last chance to jump out) */ + } + abort(); + } + } +} + + +l_noret luaD_throwbaselevel (lua_State *L, TStatus errcode) { + if (L->errorJmp) { + /* unroll error entries up to the first level */ + while (L->errorJmp->previous != NULL) + L->errorJmp = L->errorJmp->previous; + } + luaD_throw(L, errcode); +} + + +TStatus luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud) { + l_uint32 oldnCcalls = L->nCcalls; + lua_longjmp lj; + lj.status = LUA_OK; + lj.previous = L->errorJmp; /* chain new error handler */ + L->errorJmp = &lj; + LUAI_TRY(L, &lj, f, ud); /* call 'f' catching errors */ + L->errorJmp = lj.previous; /* restore old error handler */ + L->nCcalls = oldnCcalls; + return lj.status; +} + +/* }====================================================== */ + + +/* +** {================================================================== +** Stack reallocation +** =================================================================== +*/ + +/* some stack space for error handling */ +#define STACKERRSPACE 200 + + +/* +** LUAI_MAXSTACK limits the size of the Lua stack. +** It must fit into INT_MAX/2. +*/ + +#if !defined(LUAI_MAXSTACK) +#if 1000000 < (INT_MAX / 2) +#define LUAI_MAXSTACK 1000000 +#else +#define LUAI_MAXSTACK (INT_MAX / 2u) +#endif +#endif + + +/* maximum stack size that respects size_t */ +#define MAXSTACK_BYSIZET ((MAX_SIZET / sizeof(StackValue)) - STACKERRSPACE) + +/* +** Minimum between LUAI_MAXSTACK and MAXSTACK_BYSIZET +** (Maximum size for the stack must respect size_t.) +*/ +#define MAXSTACK cast_int(LUAI_MAXSTACK < MAXSTACK_BYSIZET \ + ? LUAI_MAXSTACK : MAXSTACK_BYSIZET) + + +/* stack size with extra space for error handling */ +#define ERRORSTACKSIZE (MAXSTACK + STACKERRSPACE) + + +/* raise a stack error while running the message handler */ +l_noret luaD_errerr (lua_State *L) { + TString *msg = luaS_newliteral(L, "error in error handling"); + setsvalue2s(L, L->top.p, msg); + L->top.p++; /* assume EXTRA_STACK */ + luaD_throw(L, LUA_ERRERR); +} + + +/* +** Check whether stacks have enough space to run a simple function (such +** as a finalizer): At least BASIC_STACK_SIZE in the Lua stack, two +** available CallInfos, and two "slots" in the C stack. +*/ +int luaD_checkminstack (lua_State *L) { + if (getCcalls(L) >= LUAI_MAXCCALLS - 2) + return 0; /* not enough C-stack slots */ + if (L->ci->next == NULL && luaE_extendCI(L, 0) == NULL) + return 0; /* unable to allocate first ci */ + if (L->ci->next->next == NULL && luaE_extendCI(L, 0) == NULL) + return 0; /* unable to allocate second ci */ + if (L->stack_last.p - L->top.p >= BASIC_STACK_SIZE) + return 1; /* enough (BASIC_STACK_SIZE) free slots in the Lua stack */ + else /* try to grow stack to a size with enough free slots */ + return luaD_growstack(L, BASIC_STACK_SIZE, 0); +} + + +/* +** In ISO C, any pointer use after the pointer has been deallocated is +** undefined behavior. So, before a stack reallocation, all pointers +** should be changed to offsets, and after the reallocation they should +** be changed back to pointers. As during the reallocation the pointers +** are invalid, the reallocation cannot run emergency collections. +** Alternatively, we can use the old address after the deallocation. +** That is not strict ISO C, but seems to work fine everywhere. +** The following macro chooses how strict is the code. +*/ +#if !defined(LUAI_STRICT_ADDRESS) +#define LUAI_STRICT_ADDRESS 1 +#endif + +#if LUAI_STRICT_ADDRESS +/* +** Change all pointers to the stack into offsets. +*/ +static void relstack (lua_State *L) { + CallInfo *ci; + UpVal *up; + L->top.offset = savestack(L, L->top.p); + L->tbclist.offset = savestack(L, L->tbclist.p); + for (up = L->openupval; up != NULL; up = up->u.open.next) + up->v.offset = savestack(L, uplevel(up)); + for (ci = L->ci; ci != NULL; ci = ci->previous) { + ci->top.offset = savestack(L, ci->top.p); + ci->func.offset = savestack(L, ci->func.p); + } +} + + +/* +** Change back all offsets into pointers. +*/ +static void correctstack (lua_State *L, StkId oldstack) { + CallInfo *ci; + UpVal *up; + UNUSED(oldstack); + L->top.p = restorestack(L, L->top.offset); + L->tbclist.p = restorestack(L, L->tbclist.offset); + for (up = L->openupval; up != NULL; up = up->u.open.next) + up->v.p = s2v(restorestack(L, up->v.offset)); + for (ci = L->ci; ci != NULL; ci = ci->previous) { + ci->top.p = restorestack(L, ci->top.offset); + ci->func.p = restorestack(L, ci->func.offset); + if (isLua(ci)) + ci->u.l.trap = 1; /* signal to update 'trap' in 'luaV_execute' */ + } +} + +#else +/* +** Assume that it is fine to use an address after its deallocation, +** as long as we do not dereference it. +*/ + +static void relstack (lua_State *L) { UNUSED(L); } /* do nothing */ + + +/* +** Correct pointers into 'oldstack' to point into 'L->stack'. +*/ +static void correctstack (lua_State *L, StkId oldstack) { + CallInfo *ci; + UpVal *up; + StkId newstack = L->stack.p; + if (oldstack == newstack) + return; + L->top.p = L->top.p - oldstack + newstack; + L->tbclist.p = L->tbclist.p - oldstack + newstack; + for (up = L->openupval; up != NULL; up = up->u.open.next) + up->v.p = s2v(uplevel(up) - oldstack + newstack); + for (ci = L->ci; ci != NULL; ci = ci->previous) { + ci->top.p = ci->top.p - oldstack + newstack; + ci->func.p = ci->func.p - oldstack + newstack; + if (isLua(ci)) + ci->u.l.trap = 1; /* signal to update 'trap' in 'luaV_execute' */ + } +} +#endif + + +/* +** Reallocate the stack to a new size, correcting all pointers into it. +** In case of allocation error, raise an error or return false according +** to 'raiseerror'. +*/ +int luaD_reallocstack (lua_State *L, int newsize, int raiseerror) { + int oldsize = stacksize(L); + int i; + StkId newstack; + StkId oldstack = L->stack.p; + lu_byte oldgcstop = G(L)->gcstopem; + lua_assert(newsize <= MAXSTACK || newsize == ERRORSTACKSIZE); + relstack(L); /* change pointers to offsets */ + G(L)->gcstopem = 1; /* stop emergency collection */ + newstack = luaM_reallocvector(L, oldstack, oldsize + EXTRA_STACK, + newsize + EXTRA_STACK, StackValue); + G(L)->gcstopem = oldgcstop; /* restore emergency collection */ + if (l_unlikely(newstack == NULL)) { /* reallocation failed? */ + correctstack(L, oldstack); /* change offsets back to pointers */ + if (raiseerror) + luaM_error(L); + else return 0; /* do not raise an error */ + } + L->stack.p = newstack; + correctstack(L, oldstack); /* change offsets back to pointers */ + L->stack_last.p = L->stack.p + newsize; + for (i = oldsize + EXTRA_STACK; i < newsize + EXTRA_STACK; i++) + setnilvalue2s(newstack + i); /* erase new segment */ + return 1; +} + + +/* +** Try to grow the stack by at least 'n' elements. When 'raiseerror' +** is true, raises any error; otherwise, return 0 in case of errors. +*/ +int luaD_growstack (lua_State *L, int n, int raiseerror) { + int size = stacksize(L); + if (l_unlikely(size > MAXSTACK)) { + /* if stack is larger than maximum, thread is already using the + extra space reserved for errors, that is, thread is handling + a stack error; cannot grow further than that. */ + lua_assert(stacksize(L) == ERRORSTACKSIZE); + if (raiseerror) + luaD_errerr(L); /* stack error inside message handler */ + return 0; /* if not 'raiseerror', just signal it */ + } + else if (n < MAXSTACK) { /* avoids arithmetic overflows */ + int newsize = size + (size >> 1); /* tentative new size (size * 1.5) */ + int needed = cast_int(L->top.p - L->stack.p) + n; + if (newsize > MAXSTACK) /* cannot cross the limit */ + newsize = MAXSTACK; + if (newsize < needed) /* but must respect what was asked for */ + newsize = needed; + if (l_likely(newsize <= MAXSTACK)) + return luaD_reallocstack(L, newsize, raiseerror); + } + /* else stack overflow */ + /* add extra size to be able to handle the error message */ + luaD_reallocstack(L, ERRORSTACKSIZE, raiseerror); + if (raiseerror) + luaG_runerror(L, "stack overflow"); + return 0; +} + + +/* +** Compute how much of the stack is being used, by computing the +** maximum top of all call frames in the stack and the current top. +*/ +static int stackinuse (lua_State *L) { + CallInfo *ci; + int res; + StkId lim = L->top.p; + for (ci = L->ci; ci != NULL; ci = ci->previous) { + if (lim < ci->top.p) lim = ci->top.p; + } + lua_assert(lim <= L->stack_last.p + EXTRA_STACK); + res = cast_int(lim - L->stack.p) + 1; /* part of stack in use */ + if (res < LUA_MINSTACK) + res = LUA_MINSTACK; /* ensure a minimum size */ + return res; +} + + +/* +** If stack size is more than 3 times the current use, reduce that size +** to twice the current use. (So, the final stack size is at most 2/3 the +** previous size, and half of its entries are empty.) +** As a particular case, if stack was handling a stack overflow and now +** it is not, 'max' (limited by MAXSTACK) will be smaller than +** stacksize (equal to ERRORSTACKSIZE in this case), and so the stack +** will be reduced to a "regular" size. +*/ +void luaD_shrinkstack (lua_State *L) { + int inuse = stackinuse(L); + int max = (inuse > MAXSTACK / 3) ? MAXSTACK : inuse * 3; + /* if thread is currently not handling a stack overflow and its + size is larger than maximum "reasonable" size, shrink it */ + if (inuse <= MAXSTACK && stacksize(L) > max) { + int nsize = (inuse > MAXSTACK / 2) ? MAXSTACK : inuse * 2; + luaD_reallocstack(L, nsize, 0); /* ok if that fails */ + } + else /* don't change stack */ + condmovestack(L,(void)0,(void)0); /* (change only for debugging) */ + luaE_shrinkCI(L); /* shrink CI list */ +} + +/* }================================================================== */ + + +/* +** Call a hook for the given event. Make sure there is a hook to be +** called. (Both 'L->hook' and 'L->hookmask', which trigger this +** function, can be changed asynchronously by signals.) +*/ +void luaD_hook (lua_State *L, int event, int line, + int ftransfer, int ntransfer) { + lua_Hook hook = L->hook; + if (hook && L->allowhook) { /* make sure there is a hook */ + CallInfo *ci = L->ci; + ptrdiff_t top = savestack(L, L->top.p); /* preserve original 'top' */ + ptrdiff_t ci_top = savestack(L, ci->top.p); /* idem for 'ci->top' */ + lua_Debug ar; + ar.event = event; + ar.currentline = line; + ar.i_ci = ci; + L->transferinfo.ftransfer = ftransfer; + L->transferinfo.ntransfer = ntransfer; + if (isLua(ci) && L->top.p < ci->top.p) + L->top.p = ci->top.p; /* protect entire activation register */ + luaD_checkstack(L, LUA_MINSTACK); /* ensure minimum stack size */ + if (ci->top.p < L->top.p + LUA_MINSTACK) + ci->top.p = L->top.p + LUA_MINSTACK; + L->allowhook = 0; /* cannot call hooks inside a hook */ + ci->callstatus |= CIST_HOOKED; + lua_unlock(L); + (*hook)(L, &ar); + lua_lock(L); + lua_assert(!L->allowhook); + L->allowhook = 1; + ci->top.p = restorestack(L, ci_top); + L->top.p = restorestack(L, top); + ci->callstatus &= ~CIST_HOOKED; + } +} + + +/* +** Executes a call hook for Lua functions. This function is called +** whenever 'hookmask' is not zero, so it checks whether call hooks are +** active. +*/ +void luaD_hookcall (lua_State *L, CallInfo *ci) { + L->oldpc = 0; /* set 'oldpc' for new function */ + if (L->hookmask & LUA_MASKCALL) { /* is call hook on? */ + int event = (ci->callstatus & CIST_TAIL) ? LUA_HOOKTAILCALL + : LUA_HOOKCALL; + Proto *p = ci_func(ci)->p; + ci->u.l.savedpc++; /* hooks assume 'pc' is already incremented */ + luaD_hook(L, event, -1, 1, p->numparams); + ci->u.l.savedpc--; /* correct 'pc' */ + } +} + + +/* +** Executes a return hook for Lua and C functions and sets/corrects +** 'oldpc'. (Note that this correction is needed by the line hook, so it +** is done even when return hooks are off.) +*/ +static void rethook (lua_State *L, CallInfo *ci, int nres) { + if (L->hookmask & LUA_MASKRET) { /* is return hook on? */ + StkId firstres = L->top.p - nres; /* index of first result */ + int delta = 0; /* correction for vararg functions */ + int ftransfer; + if (isLua(ci)) { + Proto *p = ci_func(ci)->p; + if (p->flag & PF_VAHID) + delta = ci->u.l.nextraargs + p->numparams + 1; + } + ci->func.p += delta; /* if vararg, back to virtual 'func' */ + ftransfer = cast_int(firstres - ci->func.p); + luaD_hook(L, LUA_HOOKRET, -1, ftransfer, nres); /* call it */ + ci->func.p -= delta; + } + if (isLua(ci = ci->previous)) + L->oldpc = pcRel(ci->u.l.savedpc, ci_func(ci)->p); /* set 'oldpc' */ +} + + +/* +** Check whether 'func' has a '__call' metafield. If so, put it in the +** stack, below original 'func', so that 'luaD_precall' can call it. +** Raise an error if there is no '__call' metafield. +** Bits CIST_CCMT in status count how many _call metamethods were +** invoked and how many corresponding extra arguments were pushed. +** (This count will be saved in the 'callstatus' of the call). +** Raise an error if this counter overflows. +*/ +static unsigned tryfuncTM (lua_State *L, StkId func, unsigned status) { + const TValue *tm; + StkId p; + tm = luaT_gettmbyobj(L, s2v(func), TM_CALL); + if (l_unlikely(ttisnil(tm))) /* no metamethod? */ + luaG_callerror(L, s2v(func)); + for (p = L->top.p; p > func; p--) /* open space for metamethod */ + setobjs2s(L, p, p-1); + L->top.p++; /* stack space pre-allocated by the caller */ + setobj2s(L, func, tm); /* metamethod is the new function to be called */ + if ((status & MAX_CCMT) == MAX_CCMT) /* is counter full? */ + luaG_runerror(L, "'__call' chain too long"); + return status + (1u << CIST_CCMT); /* increment counter */ +} + + +/* Generic case for 'moveresult' */ +l_sinline void genmoveresults (lua_State *L, StkId res, int nres, + int wanted) { + StkId firstresult = L->top.p - nres; /* index of first result */ + int i; + if (nres > wanted) /* extra results? */ + nres = wanted; /* don't need them */ + for (i = 0; i < nres; i++) /* move all results to correct place */ + setobjs2s(L, res + i, firstresult + i); + for (; i < wanted; i++) /* complete wanted number of results */ + setnilvalue2s(res + i); + L->top.p = res + wanted; /* top points after the last result */ +} + + +/* +** Given 'nres' results at 'firstResult', move 'fwanted-1' of them +** to 'res'. Handle most typical cases (zero results for commands, +** one result for expressions, multiple results for tail calls/single +** parameters) separated. The flag CIST_TBC in 'fwanted', if set, +** forces the switch to go to the default case. +*/ +l_sinline void moveresults (lua_State *L, StkId res, int nres, + l_uint32 fwanted) { + switch (fwanted) { /* handle typical cases separately */ + case 0 + 1: /* no values needed */ + L->top.p = res; + return; + case 1 + 1: /* one value needed */ + if (nres == 0) /* no results? */ + setnilvalue2s(res); /* adjust with nil */ + else /* at least one result */ + setobjs2s(L, res, L->top.p - nres); /* move it to proper place */ + L->top.p = res + 1; + return; + case LUA_MULTRET + 1: + genmoveresults(L, res, nres, nres); /* we want all results */ + break; + default: { /* two/more results and/or to-be-closed variables */ + int wanted = get_nresults(fwanted); + if (fwanted & CIST_TBC) { /* to-be-closed variables? */ + L->ci->u2.nres = nres; + L->ci->callstatus |= CIST_CLSRET; /* in case of yields */ + res = luaF_close(L, res, CLOSEKTOP, 1); + L->ci->callstatus &= ~CIST_CLSRET; + if (L->hookmask) { /* if needed, call hook after '__close's */ + ptrdiff_t savedres = savestack(L, res); + rethook(L, L->ci, nres); + res = restorestack(L, savedres); /* hook can move stack */ + } + if (wanted == LUA_MULTRET) + wanted = nres; /* we want all results */ + } + genmoveresults(L, res, nres, wanted); + break; + } + } +} + + +/* +** Finishes a function call: calls hook if necessary, moves current +** number of results to proper place, and returns to previous call +** info. If function has to close variables, hook must be called after +** that. +*/ +void luaD_poscall (lua_State *L, CallInfo *ci, int nres) { + l_uint32 fwanted = ci->callstatus & (CIST_TBC | CIST_NRESULTS); + if (l_unlikely(L->hookmask) && !(fwanted & CIST_TBC)) + rethook(L, ci, nres); + /* move results to proper place */ + moveresults(L, ci->func.p, nres, fwanted); + /* function cannot be in any of these cases when returning */ + lua_assert(!(ci->callstatus & + (CIST_HOOKED | CIST_YPCALL | CIST_FIN | CIST_CLSRET))); + L->ci = ci->previous; /* back to caller (after closing variables) */ +} + + + +#define next_ci(L) (L->ci->next ? L->ci->next : luaE_extendCI(L, 1)) + + +/* +** Allocate and initialize CallInfo structure. At this point, the +** only valid fields in the call status are number of results, +** CIST_C (if it's a C function), and number of extra arguments. +** (All these bit-fields fit in 16-bit values.) +*/ +l_sinline CallInfo *prepCallInfo (lua_State *L, StkId func, unsigned status, + StkId top) { + CallInfo *ci = L->ci = next_ci(L); /* new frame */ + ci->func.p = func; + lua_assert((status & ~(CIST_NRESULTS | CIST_C | MAX_CCMT)) == 0); + ci->callstatus = status; + ci->top.p = top; + return ci; +} + + +/* +** precall for C functions +*/ +l_sinline int precallC (lua_State *L, StkId func, unsigned status, + lua_CFunction f) { + int n; /* number of returns */ + CallInfo *ci; + checkstackp(L, LUA_MINSTACK, func); /* ensure minimum stack size */ + L->ci = ci = prepCallInfo(L, func, status | CIST_C, + L->top.p + LUA_MINSTACK); + lua_assert(ci->top.p <= L->stack_last.p); + if (l_unlikely(L->hookmask & LUA_MASKCALL)) { + int narg = cast_int(L->top.p - func) - 1; + luaD_hook(L, LUA_HOOKCALL, -1, 1, narg); + } + lua_unlock(L); + n = (*f)(L); /* do the actual call */ + lua_lock(L); + api_checknelems(L, n); + luaD_poscall(L, ci, n); + return n; +} + + +/* +** Prepare a function for a tail call, building its call info on top +** of the current call info. 'narg1' is the number of arguments plus 1 +** (so that it includes the function itself). Return the number of +** results, if it was a C function, or -1 for a Lua function. +*/ +int luaD_pretailcall (lua_State *L, CallInfo *ci, StkId func, + int narg1, int delta) { + unsigned status = LUA_MULTRET + 1; + retry: + switch (ttypetag(s2v(func))) { + case LUA_VCCL: /* C closure */ + return precallC(L, func, status, clCvalue(s2v(func))->f); + case LUA_VLCF: /* light C function */ + return precallC(L, func, status, fvalue(s2v(func))); + case LUA_VLCL: { /* Lua function */ + Proto *p = clLvalue(s2v(func))->p; + int fsize = p->maxstacksize; /* frame size */ + int nfixparams = p->numparams; + int i; + checkstackp(L, fsize - delta, func); + ci->func.p -= delta; /* restore 'func' (if vararg) */ + for (i = 0; i < narg1; i++) /* move down function and arguments */ + setobjs2s(L, ci->func.p + i, func + i); + func = ci->func.p; /* moved-down function */ + for (; narg1 <= nfixparams; narg1++) + setnilvalue2s(func + narg1); /* complete missing arguments */ + ci->top.p = func + 1 + fsize; /* top for new function */ + lua_assert(ci->top.p <= L->stack_last.p); + ci->u.l.savedpc = p->code; /* starting point */ + ci->callstatus |= CIST_TAIL; + L->top.p = func + narg1; /* set top */ + return -1; + } + default: { /* not a function */ + checkstackp(L, 1, func); /* space for metamethod */ + status = tryfuncTM(L, func, status); /* try '__call' metamethod */ + narg1++; + goto retry; /* try again */ + } + } +} + + +/* +** Prepares the call to a function (C or Lua). For C functions, also do +** the call. The function to be called is at '*func'. The arguments +** are on the stack, right after the function. Returns the CallInfo +** to be executed, if it was a Lua function. Otherwise (a C function) +** returns NULL, with all the results on the stack, starting at the +** original function position. +*/ +CallInfo *luaD_precall (lua_State *L, StkId func, int nresults) { + unsigned status = cast_uint(nresults + 1); + lua_assert(status <= MAXRESULTS + 1); + retry: + switch (ttypetag(s2v(func))) { + case LUA_VCCL: /* C closure */ + precallC(L, func, status, clCvalue(s2v(func))->f); + return NULL; + case LUA_VLCF: /* light C function */ + precallC(L, func, status, fvalue(s2v(func))); + return NULL; + case LUA_VLCL: { /* Lua function */ + CallInfo *ci; + Proto *p = clLvalue(s2v(func))->p; + int narg = cast_int(L->top.p - func) - 1; /* number of real arguments */ + int nfixparams = p->numparams; + int fsize = p->maxstacksize; /* frame size */ + checkstackp(L, fsize, func); + L->ci = ci = prepCallInfo(L, func, status, func + 1 + fsize); + ci->u.l.savedpc = p->code; /* starting point */ + for (; narg < nfixparams; narg++) + setnilvalue2s(L->top.p++); /* complete missing arguments */ + lua_assert(ci->top.p <= L->stack_last.p); + return ci; + } + default: { /* not a function */ + checkstackp(L, 1, func); /* space for metamethod */ + status = tryfuncTM(L, func, status); /* try '__call' metamethod */ + goto retry; /* try again with metamethod */ + } + } +} + + +/* +** Call a function (C or Lua) through C. 'inc' can be 1 (increment +** number of recursive invocations in the C stack) or nyci (the same +** plus increment number of non-yieldable calls). +** This function can be called with some use of EXTRA_STACK, so it should +** check the stack before doing anything else. 'luaD_precall' already +** does that. +*/ +l_sinline void ccall (lua_State *L, StkId func, int nResults, l_uint32 inc) { + CallInfo *ci; + L->nCcalls += inc; + if (l_unlikely(getCcalls(L) >= LUAI_MAXCCALLS)) { + checkstackp(L, 0, func); /* free any use of EXTRA_STACK */ + luaE_checkcstack(L); + } + if ((ci = luaD_precall(L, func, nResults)) != NULL) { /* Lua function? */ + ci->callstatus |= CIST_FRESH; /* mark that it is a "fresh" execute */ + luaV_execute(L, ci); /* call it */ + } + L->nCcalls -= inc; +} + + +/* +** External interface for 'ccall' +*/ +void luaD_call (lua_State *L, StkId func, int nResults) { + ccall(L, func, nResults, 1); +} + + +/* +** Similar to 'luaD_call', but does not allow yields during the call. +*/ +void luaD_callnoyield (lua_State *L, StkId func, int nResults) { + ccall(L, func, nResults, nyci); +} + + +/* +** Finish the job of 'lua_pcallk' after it was interrupted by an yield. +** (The caller, 'finishCcall', does the final call to 'adjustresults'.) +** The main job is to complete the 'luaD_pcall' called by 'lua_pcallk'. +** If a '__close' method yields here, eventually control will be back +** to 'finishCcall' (when that '__close' method finally returns) and +** 'finishpcallk' will run again and close any still pending '__close' +** methods. Similarly, if a '__close' method errs, 'precover' calls +** 'unroll' which calls ''finishCcall' and we are back here again, to +** close any pending '__close' methods. +** Note that, up to the call to 'luaF_close', the corresponding +** 'CallInfo' is not modified, so that this repeated run works like the +** first one (except that it has at least one less '__close' to do). In +** particular, field CIST_RECST preserves the error status across these +** multiple runs, changing only if there is a new error. +*/ +static TStatus finishpcallk (lua_State *L, CallInfo *ci) { + TStatus status = getcistrecst(ci); /* get original status */ + if (l_likely(status == LUA_OK)) /* no error? */ + status = LUA_YIELD; /* was interrupted by an yield */ + else { /* error */ + StkId func = restorestack(L, ci->u2.funcidx); + L->allowhook = getoah(ci); /* restore 'allowhook' */ + func = luaF_close(L, func, status, 1); /* can yield or raise an error */ + luaD_seterrorobj(L, status, func); + luaD_shrinkstack(L); /* restore stack size in case of overflow */ + setcistrecst(ci, LUA_OK); /* clear original status */ + } + ci->callstatus &= ~CIST_YPCALL; + L->errfunc = ci->u.c.old_errfunc; + /* if it is here, there were errors or yields; unlike 'lua_pcallk', + do not change status */ + return status; +} + + +/* +** Completes the execution of a C function interrupted by an yield. +** The interruption must have happened while the function was either +** closing its tbc variables in 'moveresults' or executing +** 'lua_callk'/'lua_pcallk'. In the first case, it just redoes +** 'luaD_poscall'. In the second case, the call to 'finishpcallk' +** finishes the interrupted execution of 'lua_pcallk'. After that, it +** calls the continuation of the interrupted function and finally it +** completes the job of the 'luaD_call' that called the function. In +** the call to 'adjustresults', we do not know the number of results +** of the function called by 'lua_callk'/'lua_pcallk', so we are +** conservative and use LUA_MULTRET (always adjust). +*/ +static void finishCcall (lua_State *L, CallInfo *ci) { + int n; /* actual number of results from C function */ + if (ci->callstatus & CIST_CLSRET) { /* was closing TBC variable? */ + lua_assert(ci->callstatus & CIST_TBC); + n = ci->u2.nres; /* just redo 'luaD_poscall' */ + /* don't need to reset CIST_CLSRET, as it will be set again anyway */ + } + else { + TStatus status = LUA_YIELD; /* default if there were no errors */ + lua_KFunction kf = ci->u.c.k; /* continuation function */ + /* must have a continuation and must be able to call it */ + lua_assert(kf != NULL && yieldable(L)); + if (ci->callstatus & CIST_YPCALL) /* was inside a 'lua_pcallk'? */ + status = finishpcallk(L, ci); /* finish it */ + adjustresults(L, LUA_MULTRET); /* finish 'lua_callk' */ + lua_unlock(L); + n = (*kf)(L, APIstatus(status), ci->u.c.ctx); /* call continuation */ + lua_lock(L); + api_checknelems(L, n); + } + luaD_poscall(L, ci, n); /* finish 'luaD_call' */ +} + + +/* +** Executes "full continuation" (everything in the stack) of a +** previously interrupted coroutine until the stack is empty (or another +** interruption long-jumps out of the loop). +*/ +static void unroll (lua_State *L, void *ud) { + CallInfo *ci; + UNUSED(ud); + while ((ci = L->ci) != &L->base_ci) { /* something in the stack */ + if (!isLua(ci)) /* C function? */ + finishCcall(L, ci); /* complete its execution */ + else { /* Lua function */ + luaV_finishOp(L); /* finish interrupted instruction */ + luaV_execute(L, ci); /* execute down to higher C 'boundary' */ + } + } +} + + +/* +** Try to find a suspended protected call (a "recover point") for the +** given thread. +*/ +static CallInfo *findpcall (lua_State *L) { + CallInfo *ci; + for (ci = L->ci; ci != NULL; ci = ci->previous) { /* search for a pcall */ + if (ci->callstatus & CIST_YPCALL) + return ci; + } + return NULL; /* no pending pcall */ +} + + +/* +** Signal an error in the call to 'lua_resume', not in the execution +** of the coroutine itself. (Such errors should not be handled by any +** coroutine error handler and should not kill the coroutine.) +*/ +static int resume_error (lua_State *L, const char *msg, int narg) { + api_checkpop(L, narg); + L->top.p -= narg; /* remove args from the stack */ + setsvalue2s(L, L->top.p, luaS_new(L, msg)); /* push error message */ + api_incr_top(L); + lua_unlock(L); + return LUA_ERRRUN; +} + + +/* +** Do the work for 'lua_resume' in protected mode. Most of the work +** depends on the status of the coroutine: initial state, suspended +** inside a hook, or regularly suspended (optionally with a continuation +** function), plus erroneous cases: non-suspended coroutine or dead +** coroutine. +*/ +static void resume (lua_State *L, void *ud) { + int n = *(cast(int*, ud)); /* number of arguments */ + StkId firstArg = L->top.p - n; /* first argument */ + CallInfo *ci = L->ci; + if (L->status == LUA_OK) /* starting a coroutine? */ + ccall(L, firstArg - 1, LUA_MULTRET, 0); /* just call its body */ + else { /* resuming from previous yield */ + lua_assert(L->status == LUA_YIELD); + L->status = LUA_OK; /* mark that it is running (again) */ + if (isLua(ci)) { /* yielded inside a hook? */ + /* undo increment made by 'luaG_traceexec': instruction was not + executed yet */ + lua_assert(ci->callstatus & CIST_HOOKYIELD); + ci->u.l.savedpc--; + L->top.p = firstArg; /* discard arguments */ + luaV_execute(L, ci); /* just continue running Lua code */ + } + else { /* 'common' yield */ + if (ci->u.c.k != NULL) { /* does it have a continuation function? */ + lua_unlock(L); + n = (*ci->u.c.k)(L, LUA_YIELD, ci->u.c.ctx); /* call continuation */ + lua_lock(L); + api_checknelems(L, n); + } + luaD_poscall(L, ci, n); /* finish 'luaD_call' */ + } + unroll(L, NULL); /* run continuation */ + } +} + + +/* +** Unrolls a coroutine in protected mode while there are recoverable +** errors, that is, errors inside a protected call. (Any error +** interrupts 'unroll', and this loop protects it again so it can +** continue.) Stops with a normal end (status == LUA_OK), an yield +** (status == LUA_YIELD), or an unprotected error ('findpcall' doesn't +** find a recover point). +*/ +static TStatus precover (lua_State *L, TStatus status) { + CallInfo *ci; + while (errorstatus(status) && (ci = findpcall(L)) != NULL) { + L->ci = ci; /* go down to recovery functions */ + setcistrecst(ci, status); /* status to finish 'pcall' */ + status = luaD_rawrunprotected(L, unroll, NULL); + } + return status; +} + + +LUA_API int lua_resume (lua_State *L, lua_State *from, int nargs, + int *nresults) { + TStatus status; + lua_lock(L); + if (L->status == LUA_OK) { /* may be starting a coroutine */ + if (L->ci != &L->base_ci) /* not in base level? */ + return resume_error(L, "cannot resume non-suspended coroutine", nargs); + else if (L->top.p - (L->ci->func.p + 1) == nargs) /* no function? */ + return resume_error(L, "cannot resume dead coroutine", nargs); + } + else if (L->status != LUA_YIELD) /* ended with errors? */ + return resume_error(L, "cannot resume dead coroutine", nargs); + L->nCcalls = (from) ? getCcalls(from) : 0; + if (getCcalls(L) >= LUAI_MAXCCALLS) + return resume_error(L, "C stack overflow", nargs); + L->nCcalls++; + luai_userstateresume(L, nargs); + api_checkpop(L, (L->status == LUA_OK) ? nargs + 1 : nargs); + status = luaD_rawrunprotected(L, resume, &nargs); + /* continue running after recoverable errors */ + status = precover(L, status); + if (l_likely(!errorstatus(status))) + lua_assert(status == L->status); /* normal end or yield */ + else { /* unrecoverable error */ + L->status = status; /* mark thread as 'dead' */ + luaD_seterrorobj(L, status, L->top.p); /* push error message */ + L->ci->top.p = L->top.p; + } + *nresults = (status == LUA_YIELD) ? L->ci->u2.nyield + : cast_int(L->top.p - (L->ci->func.p + 1)); + lua_unlock(L); + return APIstatus(status); +} + + +LUA_API int lua_isyieldable (lua_State *L) { + return yieldable(L); +} + + +LUA_API int lua_yieldk (lua_State *L, int nresults, lua_KContext ctx, + lua_KFunction k) { + CallInfo *ci; + luai_userstateyield(L, nresults); + lua_lock(L); + ci = L->ci; + api_checkpop(L, nresults); + if (l_unlikely(!yieldable(L))) { + if (L != mainthread(G(L))) + luaG_runerror(L, "attempt to yield across a C-call boundary"); + else + luaG_runerror(L, "attempt to yield from outside a coroutine"); + } + L->status = LUA_YIELD; + ci->u2.nyield = nresults; /* save number of results */ + if (isLua(ci)) { /* inside a hook? */ + lua_assert(!isLuacode(ci)); + api_check(L, nresults == 0, "hooks cannot yield values"); + api_check(L, k == NULL, "hooks cannot continue after yielding"); + } + else { + if ((ci->u.c.k = k) != NULL) /* is there a continuation? */ + ci->u.c.ctx = ctx; /* save context */ + luaD_throw(L, LUA_YIELD); + } + lua_assert(ci->callstatus & CIST_HOOKED); /* must be inside a hook */ + lua_unlock(L); + return 0; /* return to 'luaD_hook' */ +} + + +/* +** Auxiliary structure to call 'luaF_close' in protected mode. +*/ +struct CloseP { + StkId level; + TStatus status; +}; + + +/* +** Auxiliary function to call 'luaF_close' in protected mode. +*/ +static void closepaux (lua_State *L, void *ud) { + struct CloseP *pcl = cast(struct CloseP *, ud); + luaF_close(L, pcl->level, pcl->status, 0); +} + + +/* +** Calls 'luaF_close' in protected mode. Return the original status +** or, in case of errors, the new status. +*/ +TStatus luaD_closeprotected (lua_State *L, ptrdiff_t level, TStatus status) { + CallInfo *old_ci = L->ci; + lu_byte old_allowhooks = L->allowhook; + for (;;) { /* keep closing upvalues until no more errors */ + struct CloseP pcl; + pcl.level = restorestack(L, level); pcl.status = status; + status = luaD_rawrunprotected(L, &closepaux, &pcl); + if (l_likely(status == LUA_OK)) /* no more errors? */ + return pcl.status; + else { /* an error occurred; restore saved state and repeat */ + L->ci = old_ci; + L->allowhook = old_allowhooks; + } + } +} + + +/* +** Call the C function 'func' in protected mode, restoring basic +** thread information ('allowhook', etc.) and in particular +** its stack level in case of errors. +*/ +TStatus luaD_pcall (lua_State *L, Pfunc func, void *u, ptrdiff_t old_top, + ptrdiff_t ef) { + TStatus status; + CallInfo *old_ci = L->ci; + lu_byte old_allowhooks = L->allowhook; + ptrdiff_t old_errfunc = L->errfunc; + L->errfunc = ef; + status = luaD_rawrunprotected(L, func, u); + if (l_unlikely(status != LUA_OK)) { /* an error occurred? */ + L->ci = old_ci; + L->allowhook = old_allowhooks; + status = luaD_closeprotected(L, old_top, status); + luaD_seterrorobj(L, status, restorestack(L, old_top)); + luaD_shrinkstack(L); /* restore stack size in case of overflow */ + } + L->errfunc = old_errfunc; + return status; +} + + + +/* +** Execute a protected parser. +*/ +struct SParser { /* data to 'f_parser' */ + ZIO *z; + Mbuffer buff; /* dynamic structure used by the scanner */ + Dyndata dyd; /* dynamic structures used by the parser */ + const char *mode; + const char *name; +}; + + +static void checkmode (lua_State *L, const char *mode, const char *x) { + if (strchr(mode, x[0]) == NULL) { + luaO_pushfstring(L, + "attempt to load a %s chunk (mode is '%s')", x, mode); + luaD_throw(L, LUA_ERRSYNTAX); + } +} + + +/* +** Before the first call to the reader function, Lua reserves a slot +** with a table for anchoring stuff. +*/ +static void f_parser (lua_State *L, void *ud) { + LClosure *cl; + struct SParser *p = cast(struct SParser *, ud); + const char *mode = p->mode ? p->mode : "bt"; + int c; + Table *anchor; + ptrdiff_t otop = savestack(L, L->top.p); /* original top */ + luaD_checkstack(L, 2); + anchor = luaH_new(L); /* create the anchor table */ + sethvalue2s(L, L->top.p++, anchor); /* anchor the anchor table */ + c = zgetc(p->z); /* read first character */ + if (c == LUA_SIGNATURE[0]) { + int fixed = 0; + if (strchr(mode, 'B') != NULL) + fixed = 1; + else + checkmode(L, mode, "binary"); + cl = luaU_undump(L, p->z, anchor, p->name, fixed); + } + else { + checkmode(L, mode, "text"); + cl = luaY_parser(L, p->z, anchor, &p->buff, &p->dyd, p->name, c); + } + L->top.p = restorestack(L, otop); /* restore stack */ + setclLvalue2s(L, L->top.p++, cl); /* push closure */ + lua_assert(cl->nupvalues == cl->p->sizeupvalues); + luaF_initupvals(L, cl); +} + + +/* +** Anchor an object in a table in the stack. First, anchor the object +** temporarily in the stack, as luaH_set may call an emergency GC. +** Then, add it in the table with itself as its key. +*/ +void luaD_anchorobj (lua_State *L, Table *anchor, GCObject *obj) { + setgcovalue(L, s2v(L->top.p++), obj); /* temporary anchor in the stack */ + luaH_set(L, anchor, s2v(L->top.p - 1), s2v(L->top.p - 1)); + /* Because this is a new key, luaH_set will call the GC barrier, so + we don't need to call the barrier again here */ + L->top.p--; +} + + +TStatus luaD_protectedparser (lua_State *L, ZIO *z, const char *name, + const char *mode) { + struct SParser p; + TStatus status; + incnny(L); /* cannot yield during parsing */ + p.z = z; p.name = name; p.mode = mode; + p.dyd.actvar.arr = NULL; p.dyd.actvar.size = 0; + p.dyd.gt.arr = NULL; p.dyd.gt.size = 0; + p.dyd.label.arr = NULL; p.dyd.label.size = 0; + luaZ_initbuffer(L, &p.buff); + status = luaD_pcall(L, f_parser, &p, savestack(L, L->top.p), L->errfunc); + luaZ_freebuffer(L, &p.buff); + luaM_freearray(L, p.dyd.actvar.arr, cast_sizet(p.dyd.actvar.size)); + luaM_freearray(L, p.dyd.gt.arr, cast_sizet(p.dyd.gt.size)); + luaM_freearray(L, p.dyd.label.arr, cast_sizet(p.dyd.label.size)); + decnny(L); + return status; +} diff --git a/crates/lua/vendor/lua/ldo.h b/crates/lua/vendor/lua/ldo.h new file mode 100644 index 0000000..5f59466 --- /dev/null +++ b/crates/lua/vendor/lua/ldo.h @@ -0,0 +1,98 @@ +/* +** $Id: ldo.h $ +** Stack and Call structure of Lua +** See Copyright Notice in lua.h +*/ + +#ifndef ldo_h +#define ldo_h + + +#include "llimits.h" +#include "lobject.h" +#include "lstate.h" +#include "lzio.h" + + +/* +** Macro to check stack size and grow stack if needed. Parameters +** 'pre'/'pos' allow the macro to preserve a pointer into the +** stack across reallocations, doing the work only when needed. +** It also allows the running of one GC step when the stack is +** reallocated. +** 'condmovestack' is used in heavy tests to force a stack reallocation +** at every check. +*/ + +#if !defined(HARDSTACKTESTS) +#define condmovestack(L,pre,pos) ((void)0) +#else +/* realloc stack keeping its size */ +#define condmovestack(L,pre,pos) \ + { int sz_ = stacksize(L); pre; luaD_reallocstack((L), sz_, 0); pos; } +#endif + +#define luaD_checkstackaux(L,n,pre,pos) \ + if (l_unlikely(L->stack_last.p - L->top.p <= (n))) \ + { pre; luaD_growstack(L, n, 1); pos; } \ + else { condmovestack(L,pre,pos); } + +/* In general, 'pre'/'pos' are empty (nothing to save) */ +#define luaD_checkstack(L,n) luaD_checkstackaux(L,n,(void)0,(void)0) + + + +#define savestack(L,pt) (cast_charp(pt) - cast_charp(L->stack.p)) +#define restorestack(L,n) cast(StkId, cast_charp(L->stack.p) + (n)) + + +/* macro to check stack size, preserving 'p' */ +#define checkstackp(L,n,p) \ + luaD_checkstackaux(L, n, \ + ptrdiff_t t__ = savestack(L, p), /* save 'p' */ \ + p = restorestack(L, t__)) /* 'pos' part: restore 'p' */ + + +/* +** Maximum depth for nested C calls, syntactical nested non-terminals, +** and other features implemented through recursion in C. (Value must +** fit in a 16-bit unsigned integer. It must also be compatible with +** the size of the C stack.) +*/ +#if !defined(LUAI_MAXCCALLS) +#define LUAI_MAXCCALLS 200 +#endif + + +/* type of protected functions, to be ran by 'runprotected' */ +typedef void (*Pfunc) (lua_State *L, void *ud); + +LUAI_FUNC l_noret luaD_errerr (lua_State *L); +LUAI_FUNC void luaD_seterrorobj (lua_State *L, TStatus errcode, StkId oldtop); +LUAI_FUNC TStatus luaD_protectedparser (lua_State *L, ZIO *z, + const char *name, + const char *mode); +LUAI_FUNC void luaD_hook (lua_State *L, int event, int line, + int fTransfer, int nTransfer); +LUAI_FUNC void luaD_hookcall (lua_State *L, CallInfo *ci); +LUAI_FUNC int luaD_pretailcall (lua_State *L, CallInfo *ci, StkId func, + int narg1, int delta); +LUAI_FUNC CallInfo *luaD_precall (lua_State *L, StkId func, int nResults); +LUAI_FUNC void luaD_call (lua_State *L, StkId func, int nResults); +LUAI_FUNC void luaD_callnoyield (lua_State *L, StkId func, int nResults); +LUAI_FUNC TStatus luaD_closeprotected (lua_State *L, ptrdiff_t level, + TStatus status); +LUAI_FUNC TStatus luaD_pcall (lua_State *L, Pfunc func, void *u, + ptrdiff_t oldtop, ptrdiff_t ef); +LUAI_FUNC void luaD_poscall (lua_State *L, CallInfo *ci, int nres); +LUAI_FUNC int luaD_reallocstack (lua_State *L, int newsize, int raiseerror); +LUAI_FUNC int luaD_growstack (lua_State *L, int n, int raiseerror); +LUAI_FUNC void luaD_shrinkstack (lua_State *L); +LUAI_FUNC int luaD_checkminstack (lua_State *L); +LUAI_FUNC void luaD_anchorobj (lua_State *L, Table *anchor, GCObject *obj); + +LUAI_FUNC l_noret luaD_throw (lua_State *L, TStatus errcode); +LUAI_FUNC l_noret luaD_throwbaselevel (lua_State *L, TStatus errcode); +LUAI_FUNC TStatus luaD_rawrunprotected (lua_State *L, Pfunc f, void *ud); + +#endif diff --git a/crates/lua/vendor/lua/ldump.c b/crates/lua/vendor/lua/ldump.c new file mode 100644 index 0000000..650738a --- /dev/null +++ b/crates/lua/vendor/lua/ldump.c @@ -0,0 +1,306 @@ +/* +** $Id: ldump.c $ +** save precompiled Lua chunks +** See Copyright Notice in lua.h +*/ + +#define ldump_c +#define LUA_CORE + +#include "lprefix.h" + + +#include +#include + +#include "lua.h" + +#include "lapi.h" +#include "lgc.h" +#include "lobject.h" +#include "lstate.h" +#include "ltable.h" +#include "lundump.h" + + +typedef struct { + lua_State *L; + lua_Writer writer; + void *data; + size_t offset; /* current position relative to beginning of dump */ + int strip; + int status; + Table *h; /* table to track saved strings */ + lua_Unsigned nstr; /* counter for counting saved strings */ +} DumpState; + + +/* +** All high-level dumps go through dumpVector; you can change it to +** change the endianness of the result +*/ +#define dumpVector(D,v,n) dumpBlock(D,v,(n)*sizeof((v)[0])) + +#define dumpLiteral(D, s) dumpBlock(D,s,sizeof(s) - sizeof(char)) + + +/* +** Dump the block of memory pointed by 'b' with given 'size'. +** 'b' should not be NULL, except for the last call signaling the end +** of the dump. +*/ +static void dumpBlock (DumpState *D, const void *b, size_t size) { + if (D->status == 0) { /* do not write anything after an error */ + lua_unlock(D->L); + D->status = (*D->writer)(D->L, b, size, D->data); + lua_lock(D->L); + D->offset += size; + } +} + + +/* +** Dump enough zeros to ensure that current position is a multiple of +** 'align'. +*/ +static void dumpAlign (DumpState *D, unsigned align) { + unsigned padding = align - cast_uint(D->offset % align); + if (padding < align) { /* padding == align means no padding */ + static lua_Integer paddingContent = 0; + lua_assert(align <= sizeof(lua_Integer)); + dumpBlock(D, &paddingContent, padding); + } + lua_assert(D->offset % align == 0); +} + + +#define dumpVar(D,x) dumpVector(D,&x,1) + + +static void dumpByte (DumpState *D, int y) { + lu_byte x = (lu_byte)y; + dumpVar(D, x); +} + + +/* +** size for 'dumpVarint' buffer: each byte can store up to 7 bits. +** (The "+6" rounds up the division.) +*/ +#define DIBS ((l_numbits(lua_Unsigned) + 6) / 7) + +/* +** Dumps an unsigned integer using the MSB Varint encoding +*/ +static void dumpVarint (DumpState *D, lua_Unsigned x) { + lu_byte buff[DIBS]; + unsigned n = 1; + buff[DIBS - 1] = x & 0x7f; /* fill least-significant byte */ + while ((x >>= 7) != 0) /* fill other bytes in reverse order */ + buff[DIBS - (++n)] = cast_byte((x & 0x7f) | 0x80); + dumpVector(D, buff + DIBS - n, n); +} + + +static void dumpSize (DumpState *D, size_t sz) { + dumpVarint(D, cast(lua_Unsigned, sz)); +} + + +static void dumpInt (DumpState *D, int x) { + lua_assert(x >= 0); + dumpVarint(D, cast_uint(x)); +} + + +static void dumpNumber (DumpState *D, lua_Number x) { + dumpVar(D, x); +} + + +/* +** Signed integers are coded to keep small values small. (Coding -1 as +** 0xfff...fff would use too many bytes to save a quite common value.) +** A non-negative x is coded as 2x; a negative x is coded as -2x - 1. +** (0 => 0; -1 => 1; 1 => 2; -2 => 3; 2 => 4; ...) +*/ +static void dumpInteger (DumpState *D, lua_Integer x) { + lua_Unsigned cx = (x >= 0) ? 2u * l_castS2U(x) + : (2u * ~l_castS2U(x)) + 1; + dumpVarint(D, cx); +} + + +/* +** Dump a String. First dump its "size": +** size==0 is followed by an index and means "reuse saved string with +** that index"; index==0 means NULL. +** size>=1 is followed by the string contents with real size==size-1 and +** means that string, which will be saved with the next available index. +** The real size does not include the ending '\0' (which is not dumped), +** so adding 1 to it cannot overflow a size_t. +*/ +static void dumpString (DumpState *D, TString *ts) { + if (ts == NULL) { + dumpVarint(D, 0); /* will "reuse" NULL */ + dumpVarint(D, 0); /* special index for NULL */ + } + else { + TValue idx; + int tag = luaH_getstr(D->h, ts, &idx); + if (!tagisempty(tag)) { /* string already saved? */ + dumpVarint(D, 0); /* reuse a saved string */ + dumpVarint(D, l_castS2U(ivalue(&idx))); /* index of saved string */ + } + else { /* must write and save the string */ + TValue key, value; /* to save the string in the hash */ + size_t size; + const char *s = getlstr(ts, size); + dumpSize(D, size + 1); + dumpVector(D, s, size + 1); /* include ending '\0' */ + D->nstr++; /* one more saved string */ + setsvalue(D->L, &key, ts); /* the string is the key */ + setivalue(&value, l_castU2S(D->nstr)); /* its index is the value */ + luaH_set(D->L, D->h, &key, &value); /* h[ts] = nstr */ + /* integer value does not need barrier */ + } + } +} + + +static void dumpCode (DumpState *D, const Proto *f) { + dumpInt(D, f->sizecode); + dumpAlign(D, sizeof(f->code[0])); + lua_assert(f->code != NULL); + dumpVector(D, f->code, cast_uint(f->sizecode)); +} + + +static void dumpFunction (DumpState *D, const Proto *f); + +static void dumpConstants (DumpState *D, const Proto *f) { + int i; + int n = f->sizek; + dumpInt(D, n); + for (i = 0; i < n; i++) { + const TValue *o = &f->k[i]; + int tt = ttypetag(o); + dumpByte(D, tt); + switch (tt) { + case LUA_VNUMFLT: + dumpNumber(D, fltvalue(o)); + break; + case LUA_VNUMINT: + dumpInteger(D, ivalue(o)); + break; + case LUA_VSHRSTR: + case LUA_VLNGSTR: + dumpString(D, tsvalue(o)); + break; + default: + lua_assert(tt == LUA_VNIL || tt == LUA_VFALSE || tt == LUA_VTRUE); + } + } +} + + +static void dumpProtos (DumpState *D, const Proto *f) { + int i; + int n = f->sizep; + dumpInt(D, n); + for (i = 0; i < n; i++) + dumpFunction(D, f->p[i]); +} + + +static void dumpUpvalues (DumpState *D, const Proto *f) { + int i, n = f->sizeupvalues; + dumpInt(D, n); + for (i = 0; i < n; i++) { + dumpByte(D, f->upvalues[i].instack); + dumpByte(D, f->upvalues[i].idx); + dumpByte(D, f->upvalues[i].kind); + } +} + + +static void dumpDebug (DumpState *D, const Proto *f) { + int i, n; + n = (D->strip) ? 0 : f->sizelineinfo; + dumpInt(D, n); + if (f->lineinfo != NULL) + dumpVector(D, f->lineinfo, cast_uint(n)); + n = (D->strip) ? 0 : f->sizeabslineinfo; + dumpInt(D, n); + if (n > 0) { + /* 'abslineinfo' is an array of structures of int's */ + dumpAlign(D, sizeof(int)); + dumpVector(D, f->abslineinfo, cast_uint(n)); + } + n = (D->strip) ? 0 : f->sizelocvars; + dumpInt(D, n); + for (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 (i = 0; i < n; i++) + dumpString(D, f->upvalues[i].name); +} + + +static void dumpFunction (DumpState *D, const Proto *f) { + dumpInt(D, f->linedefined); + dumpInt(D, f->lastlinedefined); + dumpByte(D, f->numparams); + dumpByte(D, f->flag); + dumpByte(D, f->maxstacksize); + dumpCode(D, f); + dumpConstants(D, f); + dumpUpvalues(D, f); + dumpProtos(D, f); + dumpString(D, D->strip ? NULL : f->source); + dumpDebug(D, f); +} + + +#define dumpNumInfo(D, tvar, value) \ + { tvar i = value; dumpByte(D, sizeof(tvar)); dumpVar(D, i); } + + +static void dumpHeader (DumpState *D) { + dumpLiteral(D, LUA_SIGNATURE); + dumpByte(D, LUAC_VERSION); + dumpByte(D, LUAC_FORMAT); + dumpLiteral(D, LUAC_DATA); + dumpNumInfo(D, int, LUAC_INT); + dumpNumInfo(D, Instruction, LUAC_INST); + dumpNumInfo(D, lua_Integer, LUAC_INT); + dumpNumInfo(D, lua_Number, LUAC_NUM); +} + + +/* +** dump Lua function as precompiled chunk +*/ +int luaU_dump (lua_State *L, const Proto *f, lua_Writer w, void *data, + int strip) { + DumpState D; + D.h = luaH_new(L); /* aux. table to keep strings already dumped */ + sethvalue2s(L, L->top.p, D.h); /* anchor it */ + L->top.p++; + D.L = L; + D.writer = w; + D.offset = 0; + D.data = data; + D.strip = strip; + D.status = 0; + D.nstr = 0; + dumpHeader(&D); + dumpByte(&D, f->sizeupvalues); + dumpFunction(&D, f); + dumpBlock(&D, NULL, 0); /* signal end of dump */ + return D.status; +} diff --git a/crates/lua/vendor/lua/lfunc.c b/crates/lua/vendor/lua/lfunc.c new file mode 100644 index 0000000..e64fbad --- /dev/null +++ b/crates/lua/vendor/lua/lfunc.c @@ -0,0 +1,313 @@ +/* +** $Id: lfunc.c $ +** Auxiliary functions to manipulate prototypes and closures +** See Copyright Notice in lua.h +*/ + +#define lfunc_c +#define LUA_CORE + +#include "lprefix.h" + + +#include + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" + + + +CClosure *luaF_newCclosure (lua_State *L, int nupvals) { + GCObject *o = luaC_newobj(L, LUA_VCCL, sizeCclosure(nupvals)); + CClosure *c = gco2ccl(o); + c->nupvalues = cast_byte(nupvals); + return c; +} + + +LClosure *luaF_newLclosure (lua_State *L, int nupvals) { + GCObject *o = luaC_newobj(L, LUA_VLCL, sizeLclosure(nupvals)); + LClosure *c = gco2lcl(o); + c->p = NULL; + c->nupvalues = cast_byte(nupvals); + while (nupvals--) c->upvals[nupvals] = NULL; + return c; +} + + +/* +** fill a closure with new closed upvalues +*/ +void luaF_initupvals (lua_State *L, LClosure *cl) { + int i; + for (i = 0; i < cl->nupvalues; i++) { + GCObject *o = luaC_newobj(L, LUA_VUPVAL, sizeof(UpVal)); + UpVal *uv = gco2upv(o); + uv->v.p = &uv->u.value; /* make it closed */ + setnilvalue(uv->v.p); + cl->upvals[i] = uv; + luaC_objbarrier(L, cl, uv); + } +} + + +/* +** Create a new upvalue at the given level, and link it to the list of +** open upvalues of 'L' after entry 'prev'. +**/ +static UpVal *newupval (lua_State *L, StkId level, UpVal **prev) { + GCObject *o = luaC_newobj(L, LUA_VUPVAL, sizeof(UpVal)); + UpVal *uv = gco2upv(o); + UpVal *next = *prev; + uv->v.p = s2v(level); /* current value lives in the stack */ + uv->u.open.next = next; /* link it to list of open upvalues */ + uv->u.open.previous = prev; + if (next) + next->u.open.previous = &uv->u.open.next; + *prev = uv; + if (!isintwups(L)) { /* thread not in list of threads with upvalues? */ + L->twups = G(L)->twups; /* link it to the list */ + G(L)->twups = L; + } + return uv; +} + + +/* +** Find and reuse, or create if it does not exist, an upvalue +** at the given level. +*/ +UpVal *luaF_findupval (lua_State *L, StkId level) { + UpVal **pp = &L->openupval; + UpVal *p; + lua_assert(isintwups(L) || L->openupval == NULL); + while ((p = *pp) != NULL && uplevel(p) >= level) { /* search for it */ + lua_assert(!isdead(G(L), p)); + if (uplevel(p) == level) /* corresponding upvalue? */ + return p; /* return it */ + pp = &p->u.open.next; + } + /* not found: create a new upvalue after 'pp' */ + return newupval(L, level, pp); +} + + +/* +** Call closing method for object 'obj' with error object 'err'. The +** boolean 'yy' controls whether the call is yieldable. +** (This function assumes EXTRA_STACK.) +*/ +static void callclosemethod (lua_State *L, TValue *obj, TValue *err, int yy) { + StkId top = L->top.p; + StkId func = top; + const TValue *tm = luaT_gettmbyobj(L, obj, TM_CLOSE); + setobj2s(L, top++, tm); /* will call metamethod... */ + setobj2s(L, top++, obj); /* with 'self' as the 1st argument */ + if (err != NULL) /* if there was an error... */ + setobj2s(L, top++, err); /* then error object will be 2nd argument */ + L->top.p = top; /* add function and arguments */ + if (yy) + luaD_call(L, func, 0); + else + luaD_callnoyield(L, func, 0); +} + + +/* +** Check whether object at given level has a close metamethod and raise +** an error if not. +*/ +static void checkclosemth (lua_State *L, StkId level) { + const TValue *tm = luaT_gettmbyobj(L, s2v(level), TM_CLOSE); + if (ttisnil(tm)) { /* no metamethod? */ + int idx = cast_int(level - L->ci->func.p); /* variable index */ + const char *vname = luaG_findlocal(L, L->ci, idx, NULL); + if (vname == NULL) vname = "?"; + luaG_runerror(L, "variable '%s' got a non-closable value", vname); + } +} + + +/* +** Prepare and call a closing method. +** If status is CLOSEKTOP, the call to the closing method will be pushed +** at the top of the stack. Otherwise, values can be pushed right after +** the 'level' of the upvalue being closed, as everything after that +** won't be used again. +*/ +static void prepcallclosemth (lua_State *L, StkId level, TStatus status, + int yy) { + TValue *uv = s2v(level); /* value being closed */ + TValue *errobj; + switch (status) { + case LUA_OK: + L->top.p = level + 1; /* call will be at this level */ + /* FALLTHROUGH */ + case CLOSEKTOP: /* don't need to change top */ + errobj = NULL; /* no error object */ + break; + default: /* 'luaD_seterrorobj' will set top to level + 2 */ + errobj = s2v(level + 1); /* error object goes after 'uv' */ + luaD_seterrorobj(L, status, level + 1); /* set error object */ + break; + } + callclosemethod(L, uv, errobj, yy); +} + + +/* Maximum value for deltas in 'tbclist' */ +#define MAXDELTA USHRT_MAX + + +/* +** Insert a variable in the list of to-be-closed variables. +*/ +void luaF_newtbcupval (lua_State *L, StkId level) { + lua_assert(level > L->tbclist.p); + if (l_isfalse(s2v(level))) + return; /* false doesn't need to be closed */ + checkclosemth(L, level); /* value must have a close method */ + while (cast_uint(level - L->tbclist.p) > MAXDELTA) { + L->tbclist.p += MAXDELTA; /* create a dummy node at maximum delta */ + L->tbclist.p->tbclist.delta = 0; + } + level->tbclist.delta = cast(unsigned short, level - L->tbclist.p); + L->tbclist.p = level; +} + + +void luaF_unlinkupval (UpVal *uv) { + lua_assert(upisopen(uv)); + *uv->u.open.previous = uv->u.open.next; + if (uv->u.open.next) + uv->u.open.next->u.open.previous = uv->u.open.previous; +} + + +/* +** Close all upvalues up to the given stack level. +*/ +void luaF_closeupval (lua_State *L, StkId level) { + UpVal *uv; + while ((uv = L->openupval) != NULL && uplevel(uv) >= level) { + TValue *slot = &uv->u.value; /* new position for value */ + lua_assert(uplevel(uv) < L->top.p); + luaF_unlinkupval(uv); /* remove upvalue from 'openupval' list */ + setobj(L, slot, uv->v.p); /* move value to upvalue slot */ + uv->v.p = slot; /* now current value lives here */ + if (!iswhite(uv)) { /* neither white nor dead? */ + nw2black(uv); /* closed upvalues cannot be gray */ + luaC_barrier(L, uv, slot); + } + } +} + + +/* +** Remove first element from the tbclist plus its dummy nodes. +*/ +static void poptbclist (lua_State *L) { + StkId tbc = L->tbclist.p; + lua_assert(tbc->tbclist.delta > 0); /* first element cannot be dummy */ + tbc -= tbc->tbclist.delta; + while (tbc > L->stack.p && tbc->tbclist.delta == 0) + tbc -= MAXDELTA; /* remove dummy nodes */ + L->tbclist.p = tbc; +} + + +/* +** Close all upvalues and to-be-closed variables up to the given stack +** level. Return restored 'level'. +*/ +StkId luaF_close (lua_State *L, StkId level, TStatus status, int yy) { + ptrdiff_t levelrel = savestack(L, level); + luaF_closeupval(L, level); /* first, close the upvalues */ + while (L->tbclist.p >= level) { /* traverse tbc's down to that level */ + StkId tbc = L->tbclist.p; /* get variable index */ + poptbclist(L); /* remove it from list */ + prepcallclosemth(L, tbc, status, yy); /* close variable */ + level = restorestack(L, levelrel); + } + return level; +} + + +Proto *luaF_newproto (lua_State *L) { + GCObject *o = luaC_newobj(L, LUA_VPROTO, sizeof(Proto)); + Proto *f = gco2p(o); + f->k = NULL; + f->sizek = 0; + f->p = NULL; + f->sizep = 0; + f->code = NULL; + f->sizecode = 0; + f->lineinfo = NULL; + f->sizelineinfo = 0; + f->abslineinfo = NULL; + f->sizeabslineinfo = 0; + f->upvalues = NULL; + f->sizeupvalues = 0; + f->numparams = 0; + f->flag = 0; + f->maxstacksize = 0; + f->locvars = NULL; + f->sizelocvars = 0; + f->linedefined = 0; + f->lastlinedefined = 0; + f->source = NULL; + return f; +} + + +lu_mem luaF_protosize (Proto *p) { + lu_mem sz = cast(lu_mem, sizeof(Proto)) + + cast_uint(p->sizep) * sizeof(Proto*) + + cast_uint(p->sizek) * sizeof(TValue) + + cast_uint(p->sizelocvars) * sizeof(LocVar) + + cast_uint(p->sizeupvalues) * sizeof(Upvaldesc); + if (!(p->flag & PF_FIXED)) { + sz += cast_uint(p->sizecode) * sizeof(Instruction); + sz += cast_uint(p->sizelineinfo) * sizeof(lu_byte); + sz += cast_uint(p->sizeabslineinfo) * sizeof(AbsLineInfo); + } + return sz; +} + + +void luaF_freeproto (lua_State *L, Proto *f) { + if (!(f->flag & PF_FIXED)) { + luaM_freearray(L, f->code, cast_sizet(f->sizecode)); + luaM_freearray(L, f->lineinfo, cast_sizet(f->sizelineinfo)); + luaM_freearray(L, f->abslineinfo, cast_sizet(f->sizeabslineinfo)); + } + luaM_freearray(L, f->p, cast_sizet(f->sizep)); + luaM_freearray(L, f->k, cast_sizet(f->sizek)); + luaM_freearray(L, f->locvars, cast_sizet(f->sizelocvars)); + luaM_freearray(L, f->upvalues, cast_sizet(f->sizeupvalues)); + luaM_free(L, f); +} + + +/* +** Look for n-th local variable at line 'line' in function 'func'. +** Returns NULL if not found. +*/ +const char *luaF_getlocalname (const Proto *f, int local_number, int pc) { + int i; + for (i = 0; isizelocvars && f->locvars[i].startpc <= pc; i++) { + if (pc < f->locvars[i].endpc) { /* is variable active? */ + local_number--; + if (local_number == 0) + return getstr(f->locvars[i].varname); + } + } + return NULL; /* not found */ +} diff --git a/crates/lua/vendor/lua/lfunc.h b/crates/lua/vendor/lua/lfunc.h new file mode 100644 index 0000000..d6aad3a --- /dev/null +++ b/crates/lua/vendor/lua/lfunc.h @@ -0,0 +1,65 @@ +/* +** $Id: lfunc.h $ +** Auxiliary functions to manipulate prototypes and closures +** See Copyright Notice in lua.h +*/ + +#ifndef lfunc_h +#define lfunc_h + + +#include "lobject.h" + + +#define sizeCclosure(n) \ + (offsetof(CClosure, upvalue) + sizeof(TValue) * cast_uint(n)) + +#define sizeLclosure(n) \ + (offsetof(LClosure, upvals) + sizeof(UpVal *) * cast_uint(n)) + + +/* test whether thread is in 'twups' list */ +#define isintwups(L) (L->twups != L) + + +/* +** maximum number of upvalues in a closure (both C and Lua). (Value +** must fit in a VM register.) +*/ +#define MAXUPVAL 255 + + +#define upisopen(up) ((up)->v.p != &(up)->u.value) + + +#define uplevel(up) check_exp(upisopen(up), cast(StkId, (up)->v.p)) + + +/* +** maximum number of misses before giving up the cache of closures +** in prototypes +*/ +#define MAXMISS 10 + + + +/* special status to close upvalues preserving the top of the stack */ +#define CLOSEKTOP (LUA_ERRERR + 1) + + +LUAI_FUNC Proto *luaF_newproto (lua_State *L); +LUAI_FUNC CClosure *luaF_newCclosure (lua_State *L, int nupvals); +LUAI_FUNC LClosure *luaF_newLclosure (lua_State *L, int nupvals); +LUAI_FUNC void luaF_initupvals (lua_State *L, LClosure *cl); +LUAI_FUNC UpVal *luaF_findupval (lua_State *L, StkId level); +LUAI_FUNC void luaF_newtbcupval (lua_State *L, StkId level); +LUAI_FUNC void luaF_closeupval (lua_State *L, StkId level); +LUAI_FUNC StkId luaF_close (lua_State *L, StkId level, TStatus status, int yy); +LUAI_FUNC void luaF_unlinkupval (UpVal *uv); +LUAI_FUNC lu_mem luaF_protosize (Proto *p); +LUAI_FUNC void luaF_freeproto (lua_State *L, Proto *f); +LUAI_FUNC const char *luaF_getlocalname (const Proto *func, int local_number, + int pc); + + +#endif diff --git a/crates/lua/vendor/lua/lgc.c b/crates/lua/vendor/lua/lgc.c new file mode 100644 index 0000000..2d8a641 --- /dev/null +++ b/crates/lua/vendor/lua/lgc.c @@ -0,0 +1,1802 @@ +/* +** $Id: lgc.c $ +** Garbage Collector +** See Copyright Notice in lua.h +*/ + +#define lgc_c +#define LUA_CORE + +#include "lprefix.h" + +#include + + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" + + +/* +** Maximum number of elements to sweep in each single step. +** (Large enough to dissipate fixed overheads but small enough +** to allow small steps for the collector.) +*/ +#define GCSWEEPMAX 20 + + +/* +** Cost (in work units) of running one finalizer. +*/ +#define CWUFIN 10 + + +/* mask with all color bits */ +#define maskcolors (bitmask(BLACKBIT) | WHITEBITS) + +/* mask with all GC bits */ +#define maskgcbits (maskcolors | AGEBITS) + + +/* macro to erase all color bits then set only the current white bit */ +#define makewhite(g,x) \ + (x->marked = cast_byte((x->marked & ~maskcolors) | luaC_white(g))) + +/* make an object gray (neither white nor black) */ +#define set2gray(x) resetbits(x->marked, maskcolors) + + +/* make an object black (coming from any color) */ +#define set2black(x) \ + (x->marked = cast_byte((x->marked & ~WHITEBITS) | bitmask(BLACKBIT))) + + +#define valiswhite(x) (iscollectable(x) && iswhite(gcvalue(x))) + +#define keyiswhite(n) (keyiscollectable(n) && iswhite(gckey(n))) + + +/* +** Protected access to objects in values +*/ +#define gcvalueN(o) (iscollectable(o) ? gcvalue(o) : NULL) + + +/* +** Access to collectable objects in array part of tables +*/ +#define gcvalarr(t,i) \ + ((*getArrTag(t,i) & BIT_ISCOLLECTABLE) ? getArrVal(t,i)->gc : NULL) + + +#define markvalue(g,o) { checkliveness(mainthread(g),o); \ + if (valiswhite(o)) reallymarkobject(g,gcvalue(o)); } + +#define markkey(g, n) { if keyiswhite(n) reallymarkobject(g,gckey(n)); } + +#define markobject(g,t) { if (iswhite(t)) reallymarkobject(g, obj2gco(t)); } + +/* +** mark an object that can be NULL (either because it is really optional, +** or it was stripped as debug info, or inside an uncompleted structure) +*/ +#define markobjectN(g,t) { if (t) markobject(g,t); } + + +static void reallymarkobject (global_State *g, GCObject *o); +static void atomic (lua_State *L); +static void entersweep (lua_State *L); + + +/* +** {====================================================== +** Generic functions +** ======================================================= +*/ + + +/* +** one after last element in a hash array +*/ +#define gnodelast(h) gnode(h, cast_sizet(sizenode(h))) + + +static l_mem objsize (GCObject *o) { + lu_mem res; + switch (o->tt) { + case LUA_VTABLE: { + res = luaH_size(gco2t(o)); + break; + } + case LUA_VLCL: { + LClosure *cl = gco2lcl(o); + res = sizeLclosure(cl->nupvalues); + break; + } + case LUA_VCCL: { + CClosure *cl = gco2ccl(o); + res = sizeCclosure(cl->nupvalues); + break; + } + case LUA_VUSERDATA: { + Udata *u = gco2u(o); + res = sizeudata(u->nuvalue, u->len); + break; + } + case LUA_VPROTO: { + res = luaF_protosize(gco2p(o)); + break; + } + case LUA_VTHREAD: { + res = luaE_threadsize(gco2th(o)); + break; + } + case LUA_VSHRSTR: { + TString *ts = gco2ts(o); + res = sizestrshr(cast_uint(ts->shrlen)); + break; + } + case LUA_VLNGSTR: { + TString *ts = gco2ts(o); + res = luaS_sizelngstr(ts->u.lnglen, ts->shrlen); + break; + } + case LUA_VUPVAL: { + res = sizeof(UpVal); + break; + } + default: res = 0; lua_assert(0); + } + return cast(l_mem, res); +} + + +static GCObject **getgclist (GCObject *o) { + switch (o->tt) { + case LUA_VTABLE: return &gco2t(o)->gclist; + case LUA_VLCL: return &gco2lcl(o)->gclist; + case LUA_VCCL: return &gco2ccl(o)->gclist; + case LUA_VTHREAD: return &gco2th(o)->gclist; + case LUA_VPROTO: return &gco2p(o)->gclist; + case LUA_VUSERDATA: { + Udata *u = gco2u(o); + lua_assert(u->nuvalue > 0); + return &u->gclist; + } + default: lua_assert(0); return 0; + } +} + + +/* +** Link a collectable object 'o' with a known type into the list 'p'. +** (Must be a macro to access the 'gclist' field in different types.) +*/ +#define linkgclist(o,p) linkgclist_(obj2gco(o), &(o)->gclist, &(p)) + +static void linkgclist_ (GCObject *o, GCObject **pnext, GCObject **list) { + lua_assert(!isgray(o)); /* cannot be in a gray list */ + *pnext = *list; + *list = o; + set2gray(o); /* now it is */ +} + + +/* +** Link a generic collectable object 'o' into the list 'p'. +*/ +#define linkobjgclist(o,p) linkgclist_(obj2gco(o), getgclist(o), &(p)) + + + +/* +** Clear keys for empty entries in tables. If entry is empty, mark its +** entry as dead. This allows the collection of the key, but keeps its +** entry in the table: its removal could break a chain and could break +** a table traversal. Other places never manipulate dead keys, because +** its associated empty value is enough to signal that the entry is +** logically empty. +*/ +static void clearkey (Node *n) { + lua_assert(isempty(gval(n))); + if (keyiscollectable(n)) + setdeadkey(n); /* unused key; remove it */ +} + + +/* +** tells whether a key or value can be cleared from a weak +** table. Non-collectable objects are never removed from weak +** tables. Strings behave as 'values', so are never removed too. for +** other objects: if really collected, cannot keep them; for objects +** being finalized, keep them in keys, but not in values +*/ +static int iscleared (global_State *g, const GCObject *o) { + if (o == NULL) return 0; /* non-collectable value */ + else if (novariant(o->tt) == LUA_TSTRING) { + markobject(g, o); /* strings are 'values', so are never weak */ + return 0; + } + else return iswhite(o); +} + + +/* +** Barrier that moves collector forward, that is, marks the white object +** 'v' being pointed by the black object 'o'. In the generational +** mode, 'v' must also become old, if 'o' is old; however, it cannot +** be changed directly to OLD, because it may still point to non-old +** objects. So, it is marked as OLD0. In the next cycle it will become +** OLD1, and in the next it will finally become OLD (regular old). By +** then, any object it points to will also be old. If called in the +** incremental sweep phase, it clears the black object to white (sweep +** it) to avoid other barrier calls for this same object. (That cannot +** be done is generational mode, as its sweep does not distinguish +** white from dead.) +*/ +void luaC_barrier_ (lua_State *L, GCObject *o, GCObject *v) { + global_State *g = G(L); + lua_assert(isblack(o) && iswhite(v) && !isdead(g, v) && !isdead(g, o)); + if (keepinvariant(g)) { /* must keep invariant? */ + reallymarkobject(g, v); /* restore invariant */ + if (isold(o)) { + lua_assert(!isold(v)); /* white object could not be old */ + setage(v, G_OLD0); /* restore generational invariant */ + } + } + else { /* sweep phase */ + lua_assert(issweepphase(g)); + if (g->gckind != KGC_GENMINOR) /* incremental mode? */ + makewhite(g, o); /* mark 'o' as white to avoid other barriers */ + } +} + + +/* +** barrier that moves collector backward, that is, mark the black object +** pointing to a white object as gray again. +*/ +void luaC_barrierback_ (lua_State *L, GCObject *o) { + global_State *g = G(L); + lua_assert(isblack(o) && !isdead(g, o)); + lua_assert((g->gckind != KGC_GENMINOR) + || (isold(o) && getage(o) != G_TOUCHED1)); + if (getage(o) == G_TOUCHED2) /* already in gray list? */ + set2gray(o); /* make it gray to become touched1 */ + else /* link it in 'grayagain' and paint it gray */ + linkobjgclist(o, g->grayagain); + if (isold(o)) /* generational mode? */ + setage(o, G_TOUCHED1); /* touched in current cycle */ +} + + +void luaC_fix (lua_State *L, GCObject *o) { + global_State *g = G(L); + lua_assert(g->allgc == o); /* object must be 1st in 'allgc' list! */ + set2gray(o); /* they will be gray forever */ + setage(o, G_OLD); /* and old forever */ + g->allgc = o->next; /* remove object from 'allgc' list */ + o->next = g->fixedgc; /* link it to 'fixedgc' list */ + g->fixedgc = o; +} + + +/* +** create a new collectable object (with given type, size, and offset) +** and link it to 'allgc' list. +*/ +GCObject *luaC_newobjdt (lua_State *L, lu_byte tt, size_t sz, size_t offset) { + global_State *g = G(L); + char *p = cast_charp(luaM_newobject(L, novariant(tt), sz)); + GCObject *o = cast(GCObject *, p + offset); + o->marked = luaC_white(g); + o->tt = tt; + o->next = g->allgc; + g->allgc = o; + return o; +} + + +/* +** create a new collectable object with no offset. +*/ +GCObject *luaC_newobj (lua_State *L, lu_byte tt, size_t sz) { + return luaC_newobjdt(L, tt, sz, 0); +} + +/* }====================================================== */ + + + +/* +** {====================================================== +** Mark functions +** ======================================================= +*/ + + +/* +** Mark an object. Userdata with no user values, strings, and closed +** upvalues are visited and turned black here. Open upvalues are +** already indirectly linked through their respective threads in the +** 'twups' list, so they don't go to the gray list; nevertheless, they +** are kept gray to avoid barriers, as their values will be revisited +** by the thread or by 'remarkupvals'. Other objects are added to the +** gray list to be visited (and turned black) later. Both userdata and +** upvalues can call this function recursively, but this recursion goes +** for at most two levels: An upvalue cannot refer to another upvalue +** (only closures can), and a userdata's metatable must be a table. +*/ +static void reallymarkobject (global_State *g, GCObject *o) { + g->GCmarked += objsize(o); + switch (o->tt) { + case LUA_VSHRSTR: + case LUA_VLNGSTR: { + set2black(o); /* nothing to visit */ + break; + } + case LUA_VUPVAL: { + UpVal *uv = gco2upv(o); + if (upisopen(uv)) + set2gray(uv); /* open upvalues are kept gray */ + else + set2black(uv); /* closed upvalues are visited here */ + markvalue(g, uv->v.p); /* mark its content */ + break; + } + case LUA_VUSERDATA: { + Udata *u = gco2u(o); + if (u->nuvalue == 0) { /* no user values? */ + markobjectN(g, u->metatable); /* mark its metatable */ + set2black(u); /* nothing else to mark */ + break; + } + /* else... */ + } /* FALLTHROUGH */ + case LUA_VLCL: case LUA_VCCL: case LUA_VTABLE: + case LUA_VTHREAD: case LUA_VPROTO: { + linkobjgclist(o, g->gray); /* to be visited later */ + break; + } + default: lua_assert(0); break; + } +} + + +/* +** mark metamethods for basic types +*/ +static void markmt (global_State *g) { + int i; + for (i=0; i < LUA_NUMTYPES; i++) + markobjectN(g, g->mt[i]); +} + + +/* +** mark all objects in list of being-finalized +*/ +static void markbeingfnz (global_State *g) { + GCObject *o; + for (o = g->tobefnz; o != NULL; o = o->next) + markobject(g, o); +} + + +/* +** For each non-marked thread, simulates a barrier between each open +** upvalue and its value. (If the thread is collected, the value will be +** assigned to the upvalue, but then it can be too late for the barrier +** to act. The "barrier" does not need to check colors: A non-marked +** thread must be young; upvalues cannot be older than their threads; so +** any visited upvalue must be young too.) Also removes the thread from +** the list, as it was already visited. Removes also threads with no +** upvalues, as they have nothing to be checked. (If the thread gets an +** upvalue later, it will be linked in the list again.) +*/ +static void remarkupvals (global_State *g) { + lua_State *thread; + lua_State **p = &g->twups; + while ((thread = *p) != NULL) { + if (!iswhite(thread) && thread->openupval != NULL) + p = &thread->twups; /* keep marked thread with upvalues in the list */ + else { /* thread is not marked or without upvalues */ + UpVal *uv; + lua_assert(!isold(thread) || thread->openupval == NULL); + *p = thread->twups; /* remove thread from the list */ + thread->twups = thread; /* mark that it is out of list */ + for (uv = thread->openupval; uv != NULL; uv = uv->u.open.next) { + lua_assert(getage(uv) <= getage(thread)); + if (!iswhite(uv)) { /* upvalue already visited? */ + lua_assert(upisopen(uv) && isgray(uv)); + markvalue(g, uv->v.p); /* mark its value */ + } + } + } + } +} + + +static void cleargraylists (global_State *g) { + g->gray = g->grayagain = NULL; + g->weak = g->allweak = g->ephemeron = NULL; +} + + +/* +** mark root set and reset all gray lists, to start a new collection. +** 'GCmarked' is initialized to count the total number of live bytes +** during a cycle. +*/ +static void restartcollection (global_State *g) { + cleargraylists(g); + g->GCmarked = 0; + markobject(g, mainthread(g)); + markvalue(g, &g->l_registry); + markmt(g); + markbeingfnz(g); /* mark any finalizing object left from previous cycle */ +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Traverse functions +** ======================================================= +*/ + + +/* +** Check whether object 'o' should be kept in the 'grayagain' list for +** post-processing by 'correctgraylist'. (It could put all old objects +** in the list and leave all the work to 'correctgraylist', but it is +** more efficient to avoid adding elements that will be removed.) Only +** TOUCHED1 objects need to be in the list. TOUCHED2 doesn't need to go +** back to a gray list, but then it must become OLD. (That is what +** 'correctgraylist' does when it finds a TOUCHED2 object.) +** This function is a no-op in incremental mode, as objects cannot be +** marked as touched in that mode. +*/ +static void genlink (global_State *g, GCObject *o) { + lua_assert(isblack(o)); + if (getage(o) == G_TOUCHED1) { /* touched in this cycle? */ + linkobjgclist(o, g->grayagain); /* link it back in 'grayagain' */ + } /* everything else do not need to be linked back */ + else if (getage(o) == G_TOUCHED2) + setage(o, G_OLD); /* advance age */ +} + + +/* +** Traverse a table with weak values and link it to proper list. During +** propagate phase, keep it in 'grayagain' list, to be revisited in the +** atomic phase. In the atomic phase, if table has any white value, +** put it in 'weak' list, to be cleared; otherwise, call 'genlink' +** to check table age in generational mode. +*/ +static void traverseweakvalue (global_State *g, Table *h) { + Node *n, *limit = gnodelast(h); + /* if there is array part, assume it may have white values (it is not + worth traversing it now just to check) */ + int hasclears = (h->asize > 0); + for (n = gnode(h, 0); n < limit; n++) { /* traverse hash part */ + if (isempty(gval(n))) /* entry is empty? */ + clearkey(n); /* clear its key */ + else { + lua_assert(!keyisnil(n)); + markkey(g, n); + if (!hasclears && iscleared(g, gcvalueN(gval(n)))) /* a white value? */ + hasclears = 1; /* table will have to be cleared */ + } + } + if (g->gcstate == GCSpropagate) + linkgclist(h, g->grayagain); /* must retraverse it in atomic phase */ + else if (hasclears) + linkgclist(h, g->weak); /* has to be cleared later */ + else + genlink(g, obj2gco(h)); +} + + +/* +** Traverse the array part of a table. +*/ +static int traversearray (global_State *g, Table *h) { + unsigned asize = h->asize; + int marked = 0; /* true if some object is marked in this traversal */ + unsigned i; + for (i = 0; i < asize; i++) { + GCObject *o = gcvalarr(h, i); + if (o != NULL && iswhite(o)) { + marked = 1; + reallymarkobject(g, o); + } + } + return marked; +} + + +/* +** Traverse an ephemeron table and link it to proper list. Returns true +** iff any object was marked during this traversal (which implies that +** convergence has to continue). During propagation phase, keep table +** in 'grayagain' list, to be visited again in the atomic phase. In +** the atomic phase, if table has any white->white entry, it has to +** be revisited during ephemeron convergence (as that key may turn +** black). Otherwise, if it has any white key, table has to be cleared +** (in the atomic phase). In generational mode, some tables +** must be kept in some gray list for post-processing; this is done +** by 'genlink'. +*/ +static int traverseephemeron (global_State *g, Table *h, int inv) { + int hasclears = 0; /* true if table has white keys */ + int hasww = 0; /* true if table has entry "white-key -> white-value" */ + unsigned int i; + unsigned int nsize = sizenode(h); + int marked = traversearray(g, h); /* traverse array part */ + /* traverse hash part; if 'inv', traverse descending + (see 'convergeephemerons') */ + for (i = 0; i < nsize; i++) { + Node *n = inv ? gnode(h, nsize - 1 - i) : gnode(h, i); + if (isempty(gval(n))) /* entry is empty? */ + clearkey(n); /* clear its key */ + else if (iscleared(g, gckeyN(n))) { /* key is not marked (yet)? */ + hasclears = 1; /* table must be cleared */ + if (valiswhite(gval(n))) /* value not marked yet? */ + hasww = 1; /* white-white entry */ + } + else if (valiswhite(gval(n))) { /* value not marked yet? */ + marked = 1; + reallymarkobject(g, gcvalue(gval(n))); /* mark it now */ + } + } + /* link table into proper list */ + if (g->gcstate == GCSpropagate) + linkgclist(h, g->grayagain); /* must retraverse it in atomic phase */ + else if (hasww) /* table has white->white entries? */ + linkgclist(h, g->ephemeron); /* have to propagate again */ + else if (hasclears) /* table has white keys? */ + linkgclist(h, g->allweak); /* may have to clean white keys */ + else + genlink(g, obj2gco(h)); /* check whether collector still needs to see it */ + return marked; +} + + +static void traversestrongtable (global_State *g, Table *h) { + Node *n, *limit = gnodelast(h); + traversearray(g, h); + for (n = gnode(h, 0); n < limit; n++) { /* traverse hash part */ + if (isempty(gval(n))) /* entry is empty? */ + clearkey(n); /* clear its key */ + else { + lua_assert(!keyisnil(n)); + markkey(g, n); + markvalue(g, gval(n)); + } + } + genlink(g, obj2gco(h)); +} + + +/* +** (result & 1) iff weak values; (result & 2) iff weak keys. +*/ +static int getmode (global_State *g, Table *h) { + const TValue *mode = gfasttm(g, h->metatable, TM_MODE); + if (mode == NULL || !ttisstring(mode)) + return 0; /* ignore non-string modes */ + else { + const char *smode = getstr(tsvalue(mode)); + const char *weakkey = strchr(smode, 'k'); + const char *weakvalue = strchr(smode, 'v'); + return ((weakkey != NULL) << 1) | (weakvalue != NULL); + } +} + + +static l_mem traversetable (global_State *g, Table *h) { + markobjectN(g, h->metatable); + switch (getmode(g, h)) { + case 0: /* not weak */ + traversestrongtable(g, h); + break; + case 1: /* weak values */ + traverseweakvalue(g, h); + break; + case 2: /* weak keys */ + traverseephemeron(g, h, 0); + break; + case 3: /* all weak; nothing to traverse */ + if (g->gcstate == GCSpropagate) + linkgclist(h, g->grayagain); /* must visit again its metatable */ + else + linkgclist(h, g->allweak); /* must clear collected entries */ + break; + } + return cast(l_mem, 1 + 2*sizenode(h) + h->asize); +} + + +static l_mem traverseudata (global_State *g, Udata *u) { + int i; + markobjectN(g, u->metatable); /* mark its metatable */ + for (i = 0; i < u->nuvalue; i++) + markvalue(g, &u->uv[i].uv); + genlink(g, obj2gco(u)); + return 1 + u->nuvalue; +} + + +/* +** Traverse a prototype. (While a prototype is being build, its +** arrays can be larger than needed; the extra slots are filled with +** NULL, so the use of 'markobjectN') +*/ +static l_mem traverseproto (global_State *g, Proto *f) { + int i; + markobjectN(g, f->source); + for (i = 0; i < f->sizek; i++) /* mark literals */ + markvalue(g, &f->k[i]); + for (i = 0; i < f->sizeupvalues; i++) /* mark upvalue names */ + markobjectN(g, f->upvalues[i].name); + for (i = 0; i < f->sizep; i++) /* mark nested protos */ + markobjectN(g, f->p[i]); + for (i = 0; i < f->sizelocvars; i++) /* mark local-variable names */ + markobjectN(g, f->locvars[i].varname); + return 1 + f->sizek + f->sizeupvalues + f->sizep + f->sizelocvars; +} + + +static l_mem traverseCclosure (global_State *g, CClosure *cl) { + int i; + for (i = 0; i < cl->nupvalues; i++) /* mark its upvalues */ + markvalue(g, &cl->upvalue[i]); + return 1 + cl->nupvalues; +} + +/* +** Traverse a Lua closure, marking its prototype and its upvalues. +** (Both can be NULL while closure is being created.) +*/ +static l_mem traverseLclosure (global_State *g, LClosure *cl) { + int i; + markobjectN(g, cl->p); /* mark its prototype */ + for (i = 0; i < cl->nupvalues; i++) { /* visit its upvalues */ + UpVal *uv = cl->upvals[i]; + markobjectN(g, uv); /* mark upvalue */ + } + return 1 + cl->nupvalues; +} + + +/* +** Traverse a thread, marking the elements in the stack up to its top +** and cleaning the rest of the stack in the final traversal. That +** ensures that the entire stack have valid (non-dead) objects. +** Threads have no barriers. In gen. mode, old threads must be visited +** at every cycle, because they might point to young objects. In inc. +** mode, the thread can still be modified before the end of the cycle, +** and therefore it must be visited again in the atomic phase. To ensure +** these visits, threads must return to a gray list if they are not new +** (which can only happen in generational mode) or if the traverse is in +** the propagate phase (which can only happen in incremental mode). +*/ +static l_mem traversethread (global_State *g, lua_State *th) { + UpVal *uv; + StkId o = th->stack.p; + if (isold(th) || g->gcstate == GCSpropagate) + linkgclist(th, g->grayagain); /* insert into 'grayagain' list */ + if (o == NULL) + return 0; /* stack not completely built yet */ + lua_assert(g->gcstate == GCSatomic || + th->openupval == NULL || isintwups(th)); + for (; o < th->top.p; o++) /* mark live elements in the stack */ + markvalue(g, s2v(o)); + for (uv = th->openupval; uv != NULL; uv = uv->u.open.next) + markobject(g, uv); /* open upvalues cannot be collected */ + if (g->gcstate == GCSatomic) { /* final traversal? */ + if (!g->gcemergency) + luaD_shrinkstack(th); /* do not change stack in emergency cycle */ + for (o = th->top.p; o < th->stack_last.p + EXTRA_STACK; o++) + setnilvalue2s(o); /* clear dead stack slice */ + /* 'remarkupvals' may have removed thread from 'twups' list */ + if (!isintwups(th) && th->openupval != NULL) { + th->twups = g->twups; /* link it back to the list */ + g->twups = th; + } + } + return 1 + (th->top.p - th->stack.p); +} + + +/* +** traverse one gray object, turning it to black. Return an estimate +** of the number of slots traversed. +*/ +static l_mem propagatemark (global_State *g) { + GCObject *o = g->gray; + nw2black(o); + g->gray = *getgclist(o); /* remove from 'gray' list */ + switch (o->tt) { + case LUA_VTABLE: return traversetable(g, gco2t(o)); + case LUA_VUSERDATA: return traverseudata(g, gco2u(o)); + case LUA_VLCL: return traverseLclosure(g, gco2lcl(o)); + case LUA_VCCL: return traverseCclosure(g, gco2ccl(o)); + case LUA_VPROTO: return traverseproto(g, gco2p(o)); + case LUA_VTHREAD: return traversethread(g, gco2th(o)); + default: lua_assert(0); return 0; + } +} + + +static void propagateall (global_State *g) { + while (g->gray) + propagatemark(g); +} + + +/* +** Traverse all ephemeron tables propagating marks from keys to values. +** Repeat until it converges, that is, nothing new is marked. 'dir' +** inverts the direction of the traversals, trying to speed up +** convergence on chains in the same table. +*/ +static void convergeephemerons (global_State *g) { + int changed; + int dir = 0; + do { + GCObject *w; + GCObject *next = g->ephemeron; /* get ephemeron list */ + g->ephemeron = NULL; /* tables may return to this list when traversed */ + changed = 0; + while ((w = next) != NULL) { /* for each ephemeron table */ + Table *h = gco2t(w); + next = h->gclist; /* list is rebuilt during loop */ + nw2black(h); /* out of the list (for now) */ + if (traverseephemeron(g, h, dir)) { /* marked some value? */ + propagateall(g); /* propagate changes */ + changed = 1; /* will have to revisit all ephemeron tables */ + } + } + dir = !dir; /* invert direction next time */ + } while (changed); /* repeat until no more changes */ +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Sweep Functions +** ======================================================= +*/ + + +/* +** clear entries with unmarked keys from all weaktables in list 'l' +*/ +static void clearbykeys (global_State *g, GCObject *l) { + for (; l; l = gco2t(l)->gclist) { + Table *h = gco2t(l); + Node *limit = gnodelast(h); + Node *n; + for (n = gnode(h, 0); n < limit; n++) { + if (iscleared(g, gckeyN(n))) /* unmarked key? */ + setempty(gval(n)); /* remove entry */ + if (isempty(gval(n))) /* is entry empty? */ + clearkey(n); /* clear its key */ + } + } +} + + +/* +** clear entries with unmarked values from all weaktables in list 'l' up +** to element 'f' +*/ +static void clearbyvalues (global_State *g, GCObject *l, GCObject *f) { + for (; l != f; l = gco2t(l)->gclist) { + Table *h = gco2t(l); + Node *n, *limit = gnodelast(h); + unsigned int i; + unsigned int asize = h->asize; + for (i = 0; i < asize; i++) { + GCObject *o = gcvalarr(h, i); + if (iscleared(g, o)) /* value was collected? */ + *getArrTag(h, i) = LUA_VEMPTY; /* remove entry */ + } + for (n = gnode(h, 0); n < limit; n++) { + if (iscleared(g, gcvalueN(gval(n)))) /* unmarked value? */ + setempty(gval(n)); /* remove entry */ + if (isempty(gval(n))) /* is entry empty? */ + clearkey(n); /* clear its key */ + } + } +} + + +static void freeupval (lua_State *L, UpVal *uv) { + if (upisopen(uv)) + luaF_unlinkupval(uv); + luaM_free(L, uv); +} + + +static void freeobj (lua_State *L, GCObject *o) { + assert_code(l_mem newmem = gettotalbytes(G(L)) - objsize(o)); + switch (o->tt) { + case LUA_VPROTO: + luaF_freeproto(L, gco2p(o)); + break; + case LUA_VUPVAL: + freeupval(L, gco2upv(o)); + break; + case LUA_VLCL: { + LClosure *cl = gco2lcl(o); + luaM_freemem(L, cl, sizeLclosure(cl->nupvalues)); + break; + } + case LUA_VCCL: { + CClosure *cl = gco2ccl(o); + luaM_freemem(L, cl, sizeCclosure(cl->nupvalues)); + break; + } + case LUA_VTABLE: + luaH_free(L, gco2t(o)); + break; + case LUA_VTHREAD: + luaE_freethread(L, gco2th(o)); + break; + case LUA_VUSERDATA: { + Udata *u = gco2u(o); + luaM_freemem(L, o, sizeudata(u->nuvalue, u->len)); + break; + } + case LUA_VSHRSTR: { + TString *ts = gco2ts(o); + luaS_remove(L, ts); /* remove it from hash table */ + luaM_freemem(L, ts, sizestrshr(cast_uint(ts->shrlen))); + break; + } + case LUA_VLNGSTR: { + TString *ts = gco2ts(o); + if (ts->shrlen == LSTRMEM) /* must free external string? */ + (*ts->falloc)(ts->ud, ts->contents, ts->u.lnglen + 1, 0); + luaM_freemem(L, ts, luaS_sizelngstr(ts->u.lnglen, ts->shrlen)); + break; + } + default: lua_assert(0); + } + lua_assert(gettotalbytes(G(L)) == newmem); +} + + +/* +** sweep at most 'countin' elements from a list of GCObjects erasing dead +** objects, where a dead object is one marked with the old (non current) +** white; change all non-dead objects back to white (and new), preparing +** for next collection cycle. Return where to continue the traversal or +** NULL if list is finished. +*/ +static GCObject **sweeplist (lua_State *L, GCObject **p, l_mem countin) { + global_State *g = G(L); + int ow = otherwhite(g); + int white = luaC_white(g); /* current white */ + while (*p != NULL && countin-- > 0) { + GCObject *curr = *p; + int marked = curr->marked; + if (isdeadm(ow, marked)) { /* is 'curr' dead? */ + *p = curr->next; /* remove 'curr' from list */ + freeobj(L, curr); /* erase 'curr' */ + } + else { /* change mark to 'white' and age to 'new' */ + curr->marked = cast_byte((marked & ~maskgcbits) | white | G_NEW); + p = &curr->next; /* go to next element */ + } + } + return (*p == NULL) ? NULL : p; +} + + +/* +** sweep a list until a live object (or end of list) +*/ +static GCObject **sweeptolive (lua_State *L, GCObject **p) { + GCObject **old = p; + do { + p = sweeplist(L, p, 1); + } while (p == old); + return p; +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Finalization +** ======================================================= +*/ + +/* +** If possible, shrink string table. +*/ +static void checkSizes (lua_State *L, global_State *g) { + if (!g->gcemergency) { + if (g->strt.nuse < g->strt.size / 4) /* string table too big? */ + luaS_resize(L, g->strt.size / 2); + } +} + + +/* +** Get the next udata to be finalized from the 'tobefnz' list, and +** link it back into the 'allgc' list. +*/ +static GCObject *udata2finalize (global_State *g) { + GCObject *o = g->tobefnz; /* get first element */ + lua_assert(tofinalize(o)); + g->tobefnz = o->next; /* remove it from 'tobefnz' list */ + o->next = g->allgc; /* return it to 'allgc' list */ + g->allgc = o; + resetbit(o->marked, FINALIZEDBIT); /* object is "normal" again */ + if (issweepphase(g)) + makewhite(g, o); /* "sweep" object */ + else if (getage(o) == G_OLD1) + g->firstold1 = o; /* it is the first OLD1 object in the list */ + return o; +} + + +static void dothecall (lua_State *L, void *ud) { + UNUSED(ud); + luaD_callnoyield(L, L->top.p - 2, 0); +} + + +static void GCTM (lua_State *L) { + global_State *g = G(L); + const TValue *tm; + TValue v; + lua_assert(!g->gcemergency); + setgcovalue(L, &v, udata2finalize(g)); + tm = luaT_gettmbyobj(L, &v, TM_GC); + if (!notm(tm)) { /* is there a finalizer? */ + TStatus status; + lu_byte oldah = L->allowhook; + lu_byte oldgcstp = g->gcstp; + g->gcstp |= GCSTPGC; /* avoid GC steps */ + L->allowhook = 0; /* stop debug hooks during GC metamethod */ + setobj2s(L, L->top.p++, tm); /* push finalizer... */ + setobj2s(L, L->top.p++, &v); /* ... and its argument */ + L->ci->callstatus |= CIST_FIN; /* will run a finalizer */ + status = luaD_pcall(L, dothecall, NULL, savestack(L, L->top.p - 2), 0); + L->ci->callstatus &= ~CIST_FIN; /* not running a finalizer anymore */ + L->allowhook = oldah; /* restore hooks */ + g->gcstp = oldgcstp; /* restore state */ + if (l_unlikely(status != LUA_OK)) { /* error while running __gc? */ + luaE_warnerror(L, "__gc"); + L->top.p--; /* pops error object */ + } + } +} + + +/* +** call all pending finalizers +*/ +static void callallpendingfinalizers (lua_State *L) { + global_State *g = G(L); + while (g->tobefnz) + GCTM(L); +} + + +/* +** find last 'next' field in list 'p' list (to add elements in its end) +*/ +static GCObject **findlast (GCObject **p) { + while (*p != NULL) + p = &(*p)->next; + return p; +} + + +/* +** Move all unreachable objects (or 'all' objects) that need +** finalization from list 'finobj' to list 'tobefnz' (to be finalized). +** (Note that objects after 'finobjold1' cannot be white, so they +** don't need to be traversed. In incremental mode, 'finobjold1' is NULL, +** so the whole list is traversed.) +*/ +static void separatetobefnz (global_State *g, int all) { + GCObject *curr; + GCObject **p = &g->finobj; + GCObject **lastnext = findlast(&g->tobefnz); + while ((curr = *p) != g->finobjold1) { /* traverse all finalizable objects */ + lua_assert(tofinalize(curr)); + if (!(iswhite(curr) || all)) /* not being collected? */ + p = &curr->next; /* don't bother with it */ + else { + if (curr == g->finobjsur) /* removing 'finobjsur'? */ + g->finobjsur = curr->next; /* correct it */ + *p = curr->next; /* remove 'curr' from 'finobj' list */ + curr->next = *lastnext; /* link at the end of 'tobefnz' list */ + *lastnext = curr; + lastnext = &curr->next; + } + } +} + + +/* +** If pointer 'p' points to 'o', move it to the next element. +*/ +static void checkpointer (GCObject **p, GCObject *o) { + if (o == *p) + *p = o->next; +} + + +/* +** Correct pointers to objects inside 'allgc' list when +** object 'o' is being removed from the list. +*/ +static void correctpointers (global_State *g, GCObject *o) { + checkpointer(&g->survival, o); + checkpointer(&g->old1, o); + checkpointer(&g->reallyold, o); + checkpointer(&g->firstold1, o); +} + + +/* +** if object 'o' has a finalizer, remove it from 'allgc' list (must +** search the list to find it) and link it in 'finobj' list. +*/ +void luaC_checkfinalizer (lua_State *L, GCObject *o, Table *mt) { + global_State *g = G(L); + if (tofinalize(o) || /* obj. is already marked... */ + gfasttm(g, mt, TM_GC) == NULL || /* or has no finalizer... */ + (g->gcstp & GCSTPCLS)) /* or closing state? */ + return; /* nothing to be done */ + else { /* move 'o' to 'finobj' list */ + GCObject **p; + if (issweepphase(g)) { + makewhite(g, o); /* "sweep" object 'o' */ + if (g->sweepgc == &o->next) /* should not remove 'sweepgc' object */ + g->sweepgc = sweeptolive(L, g->sweepgc); /* change 'sweepgc' */ + } + else + correctpointers(g, o); + /* search for pointer pointing to 'o' */ + for (p = &g->allgc; *p != o; p = &(*p)->next) { /* empty */ } + *p = o->next; /* remove 'o' from 'allgc' list */ + o->next = g->finobj; /* link it in 'finobj' list */ + g->finobj = o; + l_setbit(o->marked, FINALIZEDBIT); /* mark it as such */ + } +} + +/* }====================================================== */ + + +/* +** {====================================================== +** Generational Collector +** ======================================================= +*/ + +/* +** Fields 'GCmarked' and 'GCmajorminor' are used to control the pace and +** the mode of the collector. They play several roles, depending on the +** mode of the collector: +** * KGC_INC: +** GCmarked: number of marked bytes during a cycle. +** GCmajorminor: not used. +** * KGC_GENMINOR +** GCmarked: number of bytes that became old since last major collection. +** GCmajorminor: number of bytes marked in last major collection. +** * KGC_GENMAJOR +** GCmarked: number of bytes that became old since last major collection. +** GCmajorminor: number of bytes marked in last major collection. +*/ + + +/* +** Set the "time" to wait before starting a new incremental cycle; +** cycle will start when number of bytes in use hits the threshold of +** approximately (marked * pause / 100). +*/ +static void setpause (global_State *g) { + l_mem threshold = applygcparam(g, PAUSE, g->GCmarked); + l_mem debt = threshold - gettotalbytes(g); + if (debt < 0) debt = 0; + luaE_setdebt(g, debt); +} + + +/* +** Sweep a list of objects to enter generational mode. Deletes dead +** objects and turns the non dead to old. All non-dead threads---which +** are now old---must be in a gray list. Everything else is not in a +** gray list. Open upvalues are also kept gray. +*/ +static void sweep2old (lua_State *L, GCObject **p) { + GCObject *curr; + global_State *g = G(L); + while ((curr = *p) != NULL) { + if (iswhite(curr)) { /* is 'curr' dead? */ + lua_assert(isdead(g, curr)); + *p = curr->next; /* remove 'curr' from list */ + freeobj(L, curr); /* erase 'curr' */ + } + else { /* all surviving objects become old */ + setage(curr, G_OLD); + if (curr->tt == LUA_VTHREAD) { /* threads must be watched */ + lua_State *th = gco2th(curr); + linkgclist(th, g->grayagain); /* insert into 'grayagain' list */ + } + else if (curr->tt == LUA_VUPVAL && upisopen(gco2upv(curr))) + set2gray(curr); /* open upvalues are always gray */ + else /* everything else is black */ + nw2black(curr); + p = &curr->next; /* go to next element */ + } + } +} + + +/* +** Sweep for generational mode. Delete dead objects. (Because the +** collection is not incremental, there are no "new white" objects +** during the sweep. So, any white object must be dead.) For +** non-dead objects, advance their ages and clear the color of +** new objects. (Old objects keep their colors.) +** The ages of G_TOUCHED1 and G_TOUCHED2 objects cannot be advanced +** here, because these old-generation objects are usually not swept +** here. They will all be advanced in 'correctgraylist'. That function +** will also remove objects turned white here from any gray list. +*/ +static GCObject **sweepgen (lua_State *L, global_State *g, GCObject **p, + GCObject *limit, GCObject **pfirstold1, + l_mem *paddedold) { + static const lu_byte nextage[] = { + G_SURVIVAL, /* from G_NEW */ + G_OLD1, /* from G_SURVIVAL */ + G_OLD1, /* from G_OLD0 */ + G_OLD, /* from G_OLD1 */ + G_OLD, /* from G_OLD (do not change) */ + G_TOUCHED1, /* from G_TOUCHED1 (do not change) */ + G_TOUCHED2 /* from G_TOUCHED2 (do not change) */ + }; + l_mem addedold = 0; + int white = luaC_white(g); + GCObject *curr; + while ((curr = *p) != limit) { + if (iswhite(curr)) { /* is 'curr' dead? */ + lua_assert(!isold(curr) && isdead(g, curr)); + *p = curr->next; /* remove 'curr' from list */ + freeobj(L, curr); /* erase 'curr' */ + } + else { /* correct mark and age */ + int age = getage(curr); + if (age == G_NEW) { /* new objects go back to white */ + int marked = curr->marked & ~maskgcbits; /* erase GC bits */ + curr->marked = cast_byte(marked | G_SURVIVAL | white); + } + else { /* all other objects will be old, and so keep their color */ + lua_assert(age != G_OLD1); /* advanced in 'markold' */ + setage(curr, nextage[age]); + if (getage(curr) == G_OLD1) { + addedold += objsize(curr); /* bytes becoming old */ + if (*pfirstold1 == NULL) + *pfirstold1 = curr; /* first OLD1 object in the list */ + } + } + p = &curr->next; /* go to next element */ + } + } + *paddedold += addedold; + return p; +} + + +/* +** Correct a list of gray objects. Return a pointer to the last element +** left on the list, so that we can link another list to the end of +** this one. +** Because this correction is done after sweeping, young objects might +** be turned white and still be in the list. They are only removed. +** 'TOUCHED1' objects are advanced to 'TOUCHED2' and remain on the list; +** Non-white threads also remain on the list. 'TOUCHED2' objects and +** anything else become regular old, are marked black, and are removed +** from the list. +*/ +static GCObject **correctgraylist (GCObject **p) { + GCObject *curr; + while ((curr = *p) != NULL) { + GCObject **next = getgclist(curr); + if (iswhite(curr)) + goto remove; /* remove all white objects */ + else if (getage(curr) == G_TOUCHED1) { /* touched in this cycle? */ + lua_assert(isgray(curr)); + nw2black(curr); /* make it black, for next barrier */ + setage(curr, G_TOUCHED2); + goto remain; /* keep it in the list and go to next element */ + } + else if (curr->tt == LUA_VTHREAD) { + lua_assert(isgray(curr)); + goto remain; /* keep non-white threads on the list */ + } + else { /* everything else is removed */ + lua_assert(isold(curr)); /* young objects should be white here */ + if (getage(curr) == G_TOUCHED2) /* advance from TOUCHED2... */ + setage(curr, G_OLD); /* ... to OLD */ + nw2black(curr); /* make object black (to be removed) */ + goto remove; + } + remove: *p = *next; continue; + remain: p = next; continue; + } + return p; +} + + +/* +** Correct all gray lists, coalescing them into 'grayagain'. +*/ +static void correctgraylists (global_State *g) { + GCObject **list = correctgraylist(&g->grayagain); + *list = g->weak; g->weak = NULL; + list = correctgraylist(list); + *list = g->allweak; g->allweak = NULL; + list = correctgraylist(list); + *list = g->ephemeron; g->ephemeron = NULL; + correctgraylist(list); +} + + +/* +** Mark black 'OLD1' objects when starting a new young collection. +** Gray objects are already in some gray list, and so will be visited in +** the atomic step. +*/ +static void markold (global_State *g, GCObject *from, GCObject *to) { + GCObject *p; + for (p = from; p != to; p = p->next) { + if (getage(p) == G_OLD1) { + lua_assert(!iswhite(p)); + setage(p, G_OLD); /* now they are old */ + if (isblack(p)) + reallymarkobject(g, p); + } + } +} + + +/* +** Finish a young-generation collection. +*/ +static void finishgencycle (lua_State *L, global_State *g) { + correctgraylists(g); + checkSizes(L, g); + g->gcstate = GCSpropagate; /* skip restart */ + if (g->tobefnz != NULL && !g->gcemergency && luaD_checkminstack(L)) + callallpendingfinalizers(L); +} + + +/* +** Shifts from a minor collection to major collections. It starts in +** the "sweep all" state to clear all objects, which are mostly black +** in generational mode. +*/ +static void minor2inc (lua_State *L, global_State *g, lu_byte kind) { + g->GCmajorminor = g->GCmarked; /* number of live bytes */ + g->gckind = kind; + g->reallyold = g->old1 = g->survival = NULL; + g->finobjrold = g->finobjold1 = g->finobjsur = NULL; + entersweep(L); /* continue as an incremental cycle */ + /* set a debt equal to the step size */ + luaE_setdebt(g, applygcparam(g, STEPSIZE, 100)); +} + + +/* +** Decide whether to shift to major mode. It shifts if the accumulated +** number of added old bytes (counted in 'GCmarked') is larger than +** 'minormajor'% of the number of lived bytes after the last major +** collection. (This number is kept in 'GCmajorminor'.) +*/ +static int checkminormajor (global_State *g) { + l_mem limit = applygcparam(g, MINORMAJOR, g->GCmajorminor); + if (limit == 0) + return 0; /* special case: 'minormajor' 0 stops major collections */ + return (g->GCmarked >= limit); +} + +/* +** Does a young collection. First, mark 'OLD1' objects. Then does the +** atomic step. Then, check whether to continue in minor mode. If so, +** sweep all lists and advance pointers. Finally, finish the collection. +*/ +static void youngcollection (lua_State *L, global_State *g) { + l_mem addedold1 = 0; + l_mem marked = g->GCmarked; /* preserve 'g->GCmarked' */ + GCObject **psurvival; /* to point to first non-dead survival object */ + GCObject *dummy; /* dummy out parameter to 'sweepgen' */ + lua_assert(g->gcstate == GCSpropagate); + if (g->firstold1) { /* are there regular OLD1 objects? */ + markold(g, g->firstold1, g->reallyold); /* mark them */ + g->firstold1 = NULL; /* no more OLD1 objects (for now) */ + } + markold(g, g->finobj, g->finobjrold); + markold(g, g->tobefnz, NULL); + + atomic(L); /* will lose 'g->marked' */ + + /* sweep nursery and get a pointer to its last live element */ + g->gcstate = GCSswpallgc; + psurvival = sweepgen(L, g, &g->allgc, g->survival, &g->firstold1, &addedold1); + /* sweep 'survival' */ + sweepgen(L, g, psurvival, g->old1, &g->firstold1, &addedold1); + g->reallyold = g->old1; + g->old1 = *psurvival; /* 'survival' survivals are old now */ + g->survival = g->allgc; /* all news are survivals */ + + /* repeat for 'finobj' lists */ + dummy = NULL; /* no 'firstold1' optimization for 'finobj' lists */ + psurvival = sweepgen(L, g, &g->finobj, g->finobjsur, &dummy, &addedold1); + /* sweep 'survival' */ + sweepgen(L, g, psurvival, g->finobjold1, &dummy, &addedold1); + g->finobjrold = g->finobjold1; + g->finobjold1 = *psurvival; /* 'survival' survivals are old now */ + g->finobjsur = g->finobj; /* all news are survivals */ + + sweepgen(L, g, &g->tobefnz, NULL, &dummy, &addedold1); + + /* keep total number of added old1 bytes */ + g->GCmarked = marked + addedold1; + + /* decide whether to shift to major mode */ + if (checkminormajor(g)) { + minor2inc(L, g, KGC_GENMAJOR); /* go to major mode */ + g->GCmarked = 0; /* avoid pause in first major cycle (see 'setpause') */ + } + else + finishgencycle(L, g); /* still in minor mode; finish it */ +} + + +/* +** Clears all gray lists, sweeps objects, and prepare sublists to enter +** generational mode. The sweeps remove dead objects and turn all +** surviving objects to old. Threads go back to 'grayagain'; everything +** else is turned black (not in any gray list). +*/ +static void atomic2gen (lua_State *L, global_State *g) { + cleargraylists(g); + /* sweep all elements making them old */ + g->gcstate = GCSswpallgc; + sweep2old(L, &g->allgc); + /* everything alive now is old */ + g->reallyold = g->old1 = g->survival = g->allgc; + g->firstold1 = NULL; /* there are no OLD1 objects anywhere */ + + /* repeat for 'finobj' lists */ + sweep2old(L, &g->finobj); + g->finobjrold = g->finobjold1 = g->finobjsur = g->finobj; + + sweep2old(L, &g->tobefnz); + + g->gckind = KGC_GENMINOR; + g->GCmajorminor = g->GCmarked; /* "base" for number of bytes */ + g->GCmarked = 0; /* to count the number of added old1 bytes */ + finishgencycle(L, g); +} + + +/* +** Set debt for the next minor collection, which will happen when +** total number of bytes grows 'genminormul'% in relation to +** the base, GCmajorminor, which is the number of bytes being used +** after the last major collection. +*/ +static void setminordebt (global_State *g) { + luaE_setdebt(g, applygcparam(g, MINORMUL, g->GCmajorminor)); +} + + +/* +** Enter generational mode. Must go until the end of an atomic cycle +** to ensure that all objects are correctly marked and weak tables +** are cleared. Then, turn all objects into old and finishes the +** collection. +*/ +static void entergen (lua_State *L, global_State *g) { + luaC_runtilstate(L, GCSpause, 1); /* prepare to start a new cycle */ + luaC_runtilstate(L, GCSpropagate, 1); /* start new cycle */ + atomic(L); /* propagates all and then do the atomic stuff */ + atomic2gen(L, g); + setminordebt(g); /* set debt assuming next cycle will be minor */ +} + + +/* +** Change collector mode to 'newmode'. +*/ +void luaC_changemode (lua_State *L, int newmode) { + global_State *g = G(L); + if (g->gckind == KGC_GENMAJOR) /* doing major collections? */ + g->gckind = KGC_INC; /* already incremental but in name */ + if (newmode != g->gckind) { /* does it need to change? */ + if (newmode == KGC_INC) /* entering incremental mode? */ + minor2inc(L, g, KGC_INC); /* entering incremental mode */ + else { + lua_assert(newmode == KGC_GENMINOR); + entergen(L, g); + } + } +} + + +/* +** Does a full collection in generational mode. +*/ +static void fullgen (lua_State *L, global_State *g) { + minor2inc(L, g, KGC_INC); + entergen(L, g); +} + + +/* +** After an atomic incremental step from a major collection, +** check whether collector could return to minor collections. +** It checks whether the number of bytes 'tobecollected' +** is greater than 'majorminor'% of the number of bytes added +** since the last collection ('addedbytes'). +*/ +static int checkmajorminor (lua_State *L, global_State *g) { + if (g->gckind == KGC_GENMAJOR) { /* generational mode? */ + l_mem numbytes = gettotalbytes(g); + l_mem addedbytes = numbytes - g->GCmajorminor; + l_mem limit = applygcparam(g, MAJORMINOR, addedbytes); + l_mem tobecollected = numbytes - g->GCmarked; + if (tobecollected > limit) { + atomic2gen(L, g); /* return to generational mode */ + setminordebt(g); + return 1; /* exit incremental collection */ + } + } + g->GCmajorminor = g->GCmarked; /* prepare for next collection */ + return 0; /* stay doing incremental collections */ +} + +/* }====================================================== */ + + +/* +** {====================================================== +** GC control +** ======================================================= +*/ + + +/* +** Enter first sweep phase. +** The call to 'sweeptolive' makes the pointer point to an object +** inside the list (instead of to the header), so that the real sweep do +** not need to skip objects created between "now" and the start of the +** real sweep. +*/ +static void entersweep (lua_State *L) { + global_State *g = G(L); + g->gcstate = GCSswpallgc; + lua_assert(g->sweepgc == NULL); + g->sweepgc = sweeptolive(L, &g->allgc); +} + + +/* +** Delete all objects in list 'p' until (but not including) object +** 'limit'. +*/ +static void deletelist (lua_State *L, GCObject *p, GCObject *limit) { + while (p != limit) { + GCObject *next = p->next; + freeobj(L, p); + p = next; + } +} + + +/* +** Call all finalizers of the objects in the given Lua state, and +** then free all objects, except for the main thread. +*/ +void luaC_freeallobjects (lua_State *L) { + global_State *g = G(L); + g->gcstp = GCSTPCLS; /* no extra finalizers after here */ + luaC_changemode(L, KGC_INC); + separatetobefnz(g, 1); /* separate all objects with finalizers */ + lua_assert(g->finobj == NULL); + callallpendingfinalizers(L); + deletelist(L, g->allgc, obj2gco(mainthread(g))); + lua_assert(g->finobj == NULL); /* no new finalizers */ + deletelist(L, g->fixedgc, NULL); /* collect fixed objects */ + lua_assert(g->strt.nuse == 0); +} + + +static void atomic (lua_State *L) { + global_State *g = G(L); + GCObject *origweak, *origall; + GCObject *grayagain = g->grayagain; /* save original list */ + g->grayagain = NULL; + lua_assert(g->ephemeron == NULL && g->weak == NULL); + lua_assert(!iswhite(mainthread(g))); + g->gcstate = GCSatomic; + markobject(g, L); /* mark running thread */ + /* registry and global metatables may be changed by API */ + markvalue(g, &g->l_registry); + markmt(g); /* mark global metatables */ + propagateall(g); /* empties 'gray' list */ + /* remark occasional upvalues of (maybe) dead threads */ + remarkupvals(g); + propagateall(g); /* propagate changes */ + g->gray = grayagain; + propagateall(g); /* traverse 'grayagain' list */ + convergeephemerons(g); + /* at this point, all strongly accessible objects are marked. */ + /* Clear values from weak tables, before checking finalizers */ + clearbyvalues(g, g->weak, NULL); + clearbyvalues(g, g->allweak, NULL); + origweak = g->weak; origall = g->allweak; + separatetobefnz(g, 0); /* separate objects to be finalized */ + markbeingfnz(g); /* mark objects that will be finalized */ + propagateall(g); /* remark, to propagate 'resurrection' */ + convergeephemerons(g); + /* at this point, all resurrected objects are marked. */ + /* remove dead objects from weak tables */ + clearbykeys(g, g->ephemeron); /* clear keys from all ephemeron */ + clearbykeys(g, g->allweak); /* clear keys from all 'allweak' */ + /* clear values from resurrected weak tables */ + clearbyvalues(g, g->weak, origweak); + clearbyvalues(g, g->allweak, origall); + luaS_clearcache(g); + g->currentwhite = cast_byte(otherwhite(g)); /* flip current white */ + lua_assert(g->gray == NULL); +} + + +/* +** Do a sweep step. The normal case (not fast) sweeps at most GCSWEEPMAX +** elements. The fast case sweeps the whole list. +*/ +static void sweepstep (lua_State *L, global_State *g, + lu_byte nextstate, GCObject **nextlist, int fast) { + if (g->sweepgc) + g->sweepgc = sweeplist(L, g->sweepgc, fast ? MAX_LMEM : GCSWEEPMAX); + else { /* enter next state */ + g->gcstate = nextstate; + g->sweepgc = nextlist; + } +} + + +/* +** Performs one incremental "step" in an incremental garbage collection. +** For indivisible work, a step goes to the next state. When marking +** (propagating), a step traverses one object. When sweeping, a step +** sweeps GCSWEEPMAX objects, to avoid a big overhead for sweeping +** objects one by one. (Sweeping is inexpensive, no matter the +** object.) When 'fast' is true, 'singlestep' tries to finish a state +** "as fast as possible". In particular, it skips the propagation +** phase and leaves all objects to be traversed by the atomic phase: +** That avoids traversing twice some objects, such as threads and +** weak tables. +*/ + +#define step2pause -3 /* finished collection; entered pause state */ +#define atomicstep -2 /* atomic step */ +#define step2minor -1 /* moved to minor collections */ + + +static l_mem singlestep (lua_State *L, int fast) { + global_State *g = G(L); + l_mem stepresult; + lua_assert(!g->gcstopem); /* collector is not reentrant */ + g->gcstopem = 1; /* no emergency collections while collecting */ + switch (g->gcstate) { + case GCSpause: { + restartcollection(g); + g->gcstate = GCSpropagate; + stepresult = 1; + break; + } + case GCSpropagate: { + if (fast || g->gray == NULL) { + g->gcstate = GCSenteratomic; /* finish propagate phase */ + stepresult = 1; + } + else + stepresult = propagatemark(g); /* traverse one gray object */ + break; + } + case GCSenteratomic: { + atomic(L); + if (checkmajorminor(L, g)) + stepresult = step2minor; + else { + entersweep(L); + stepresult = atomicstep; + } + break; + } + case GCSswpallgc: { /* sweep "regular" objects */ + sweepstep(L, g, GCSswpfinobj, &g->finobj, fast); + stepresult = GCSWEEPMAX; + break; + } + case GCSswpfinobj: { /* sweep objects with finalizers */ + sweepstep(L, g, GCSswptobefnz, &g->tobefnz, fast); + stepresult = GCSWEEPMAX; + break; + } + case GCSswptobefnz: { /* sweep objects to be finalized */ + sweepstep(L, g, GCSswpend, NULL, fast); + stepresult = GCSWEEPMAX; + break; + } + case GCSswpend: { /* finish sweeps */ + checkSizes(L, g); + g->gcstate = GCScallfin; + stepresult = GCSWEEPMAX; + break; + } + case GCScallfin: { /* call finalizers */ + if (g->tobefnz && !g->gcemergency && luaD_checkminstack(L)) { + g->gcstopem = 0; /* ok collections during finalizers */ + GCTM(L); /* call one finalizer */ + stepresult = CWUFIN; + } + else { /* no more finalizers or emergency mode or not enough stack + to run finalizers */ + g->gcstate = GCSpause; /* finish collection */ + stepresult = step2pause; + } + break; + } + default: lua_assert(0); return 0; + } + g->gcstopem = 0; + return stepresult; +} + + +/* +** Advances the garbage collector until it reaches the given state. +** (The option 'fast' is only for testing; in normal code, 'fast' +** here is always true.) +*/ +void luaC_runtilstate (lua_State *L, int state, int fast) { + global_State *g = G(L); + lua_assert(g->gckind == KGC_INC); + while (state != g->gcstate) + singlestep(L, fast); +} + + + +/* +** Performs a basic incremental step. The step size is +** converted from bytes to "units of work"; then the function loops +** running single steps until adding that many units of work or +** finishing a cycle (pause state). Finally, it sets the debt that +** controls when next step will be performed. +*/ +static void incstep (lua_State *L, global_State *g) { + l_mem stepsize = applygcparam(g, STEPSIZE, 100); + l_mem work2do = applygcparam(g, STEPMUL, stepsize / cast_int(sizeof(void*))); + l_mem stres; + int fast = (work2do == 0); /* special case: do a full collection */ + do { /* repeat until enough work */ + stres = singlestep(L, fast); /* perform one single step */ + if (stres == step2minor) /* returned to minor collections? */ + return; /* nothing else to be done here */ + else if (stres == step2pause || (stres == atomicstep && !fast)) + break; /* end of cycle or atomic */ + else + work2do -= stres; + } while (fast || work2do > 0); + if (g->gcstate == GCSpause) + setpause(g); /* pause until next cycle */ + else + luaE_setdebt(g, stepsize); +} + + +#if !defined(luai_tracegc) +#define luai_tracegc(L,f) ((void)0) +#endif + +/* +** Performs a basic GC step if collector is running. (If collector was +** stopped by the user, set a reasonable debt to avoid it being called +** at every single check.) +*/ +void luaC_step (lua_State *L) { + global_State *g = G(L); + lua_assert(!g->gcemergency); + if (!gcrunning(g)) { /* not running? */ + if (g->gcstp & GCSTPUSR) /* stopped by the user? */ + luaE_setdebt(g, 20000); + } + else { + luai_tracegc(L, 1); /* for internal debugging */ + switch (g->gckind) { + case KGC_INC: case KGC_GENMAJOR: + incstep(L, g); + break; + case KGC_GENMINOR: + youngcollection(L, g); + setminordebt(g); + break; + } + luai_tracegc(L, 0); /* for internal debugging */ + } +} + + +/* +** Perform a full collection in incremental mode. +** Before running the collection, check 'keepinvariant'; if it is true, +** there may be some objects marked as black, so the collector has +** to sweep all objects to turn them back to white (as white has not +** changed, nothing will be collected). +*/ +static void fullinc (lua_State *L, global_State *g) { + if (keepinvariant(g)) /* black objects? */ + entersweep(L); /* sweep everything to turn them back to white */ + /* finish any pending sweep phase to start a new cycle */ + luaC_runtilstate(L, GCSpause, 1); + luaC_runtilstate(L, GCScallfin, 1); /* run up to finalizers */ + luaC_runtilstate(L, GCSpause, 1); /* finish collection */ + setpause(g); +} + + +/* +** Performs a full GC cycle; if 'isemergency', set a flag to avoid +** some operations which could change the interpreter state in some +** unexpected ways (running finalizers and shrinking some structures). +*/ +void luaC_fullgc (lua_State *L, int isemergency) { + global_State *g = G(L); + lua_assert(!g->gcemergency); + g->gcemergency = cast_byte(isemergency); /* set flag */ + switch (g->gckind) { + case KGC_GENMINOR: fullgen(L, g); break; + case KGC_INC: fullinc(L, g); break; + case KGC_GENMAJOR: + g->gckind = KGC_INC; + fullinc(L, g); + g->gckind = KGC_GENMAJOR; + break; + } + g->gcemergency = 0; +} + +/* }====================================================== */ diff --git a/crates/lua/vendor/lua/lgc.h b/crates/lua/vendor/lua/lgc.h new file mode 100644 index 0000000..ee05417 --- /dev/null +++ b/crates/lua/vendor/lua/lgc.h @@ -0,0 +1,268 @@ +/* +** $Id: lgc.h $ +** Garbage Collector +** See Copyright Notice in lua.h +*/ + +#ifndef lgc_h +#define lgc_h + + +#include + + +#include "lobject.h" +#include "lstate.h" + +/* +** Collectable objects may have one of three colors: white, which means +** the object is not marked; gray, which means the object is marked, but +** its references may be not marked; and black, which means that the +** object and all its references are marked. The main invariant of the +** garbage collector, while marking objects, is that a black object can +** never point to a white one. Moreover, any gray object must be in a +** "gray list" (gray, grayagain, weak, allweak, ephemeron) so that it +** can be visited again before finishing the collection cycle. (Open +** upvalues are an exception to this rule, as they are attached to +** a corresponding thread.) These lists have no meaning when the +** invariant is not being enforced (e.g., sweep phase). +*/ + + +/* +** Possible states of the Garbage Collector +*/ +#define GCSpropagate 0 +#define GCSenteratomic 1 +#define GCSatomic 2 +#define GCSswpallgc 3 +#define GCSswpfinobj 4 +#define GCSswptobefnz 5 +#define GCSswpend 6 +#define GCScallfin 7 +#define GCSpause 8 + + +#define issweepphase(g) \ + (GCSswpallgc <= (g)->gcstate && (g)->gcstate <= GCSswpend) + + +/* +** macro to tell when main invariant (white objects cannot point to black +** ones) must be kept. During a collection, the sweep phase may break +** the invariant, as objects turned white may point to still-black +** objects. The invariant is restored when sweep ends and all objects +** are white again. +*/ + +#define keepinvariant(g) ((g)->gcstate <= GCSatomic) + + +/* +** some useful bit tricks +*/ +#define resetbits(x,m) ((x) &= cast_byte(~(m))) +#define setbits(x,m) ((x) |= (m)) +#define testbits(x,m) ((x) & (m)) +#define bitmask(b) (1<<(b)) +#define bit2mask(b1,b2) (bitmask(b1) | bitmask(b2)) +#define l_setbit(x,b) setbits(x, bitmask(b)) +#define resetbit(x,b) resetbits(x, bitmask(b)) +#define testbit(x,b) testbits(x, bitmask(b)) + + +/* +** Layout for bit use in 'marked' field. First three bits are +** used for object "age" in generational mode. Last bit is used +** by tests. +*/ +#define WHITE0BIT 3 /* object is white (type 0) */ +#define WHITE1BIT 4 /* object is white (type 1) */ +#define BLACKBIT 5 /* object is black */ +#define FINALIZEDBIT 6 /* object has been marked for finalization */ + +#define TESTBIT 7 + + + +#define WHITEBITS bit2mask(WHITE0BIT, WHITE1BIT) + + +#define iswhite(x) testbits((x)->marked, WHITEBITS) +#define isblack(x) testbit((x)->marked, BLACKBIT) +#define isgray(x) /* neither white nor black */ \ + (!testbits((x)->marked, WHITEBITS | bitmask(BLACKBIT))) + +#define tofinalize(x) testbit((x)->marked, FINALIZEDBIT) + +#define otherwhite(g) ((g)->currentwhite ^ WHITEBITS) +#define isdeadm(ow,m) ((m) & (ow)) +#define isdead(g,v) isdeadm(otherwhite(g), (v)->marked) + +#define changewhite(x) ((x)->marked ^= WHITEBITS) +#define nw2black(x) \ + check_exp(!iswhite(x), l_setbit((x)->marked, BLACKBIT)) + +#define luaC_white(g) cast_byte((g)->currentwhite & WHITEBITS) + + +/* object age in generational mode */ +#define G_NEW 0 /* created in current cycle */ +#define G_SURVIVAL 1 /* created in previous cycle */ +#define G_OLD0 2 /* marked old by frw. barrier in this cycle */ +#define G_OLD1 3 /* first full cycle as old */ +#define G_OLD 4 /* really old object (not to be visited) */ +#define G_TOUCHED1 5 /* old object touched this cycle */ +#define G_TOUCHED2 6 /* old object touched in previous cycle */ + +#define AGEBITS 7 /* all age bits (111) */ + +#define getage(o) ((o)->marked & AGEBITS) +#define setage(o,a) ((o)->marked = cast_byte(((o)->marked & (~AGEBITS)) | a)) +#define isold(o) (getage(o) > G_SURVIVAL) + + +/* +** In generational mode, objects are created 'new'. After surviving one +** cycle, they become 'survival'. Both 'new' and 'survival' can point +** to any other object, as they are traversed at the end of the cycle. +** We call them both 'young' objects. +** If a survival object survives another cycle, it becomes 'old1'. +** 'old1' objects can still point to survival objects (but not to +** new objects), so they still must be traversed. After another cycle +** (that, being old, 'old1' objects will "survive" no matter what) +** finally the 'old1' object becomes really 'old', and then they +** are no more traversed. +** +** To keep its invariants, the generational mode uses the same barriers +** also used by the incremental mode. If a young object is caught in a +** forward barrier, it cannot become old immediately, because it can +** still point to other young objects. Instead, it becomes 'old0', +** which in the next cycle becomes 'old1'. So, 'old0' objects is +** old but can point to new and survival objects; 'old1' is old +** but cannot point to new objects; and 'old' cannot point to any +** young object. +** +** If any old object ('old0', 'old1', 'old') is caught in a back +** barrier, it becomes 'touched1' and goes into a gray list, to be +** visited at the end of the cycle. There it evolves to 'touched2', +** which can point to survivals but not to new objects. In yet another +** cycle then it becomes 'old' again. +** +** The generational mode must also control the colors of objects, +** because of the barriers. While the mutator is running, young objects +** are kept white. 'old', 'old1', and 'touched2' objects are kept black, +** as they cannot point to new objects; exceptions are threads and open +** upvalues, which age to 'old1' and 'old' but are kept gray. 'old0' +** objects may be gray or black, as in the incremental mode. 'touched1' +** objects are kept gray, as they must be visited again at the end of +** the cycle. +*/ + + +/* +** {====================================================== +** Default Values for GC parameters +** ======================================================= +*/ + +/* +** Minor collections will shift to major ones after LUAI_MINORMAJOR% +** bytes become old. +*/ +#define LUAI_MINORMAJOR 70 + +/* +** Major collections will shift to minor ones after a collection +** collects at least LUAI_MAJORMINOR% of the new bytes. +*/ +#define LUAI_MAJORMINOR 50 + +/* +** A young (minor) collection will run after creating LUAI_GENMINORMUL% +** new bytes. +*/ +#define LUAI_GENMINORMUL 20 + + +/* incremental */ + +/* Number of bytes must be LUAI_GCPAUSE% before starting new cycle */ +#define LUAI_GCPAUSE 250 + +/* +** Step multiplier: The collector handles LUAI_GCMUL% work units for +** each new allocated word. (Each "work unit" corresponds roughly to +** sweeping one object or traversing one slot.) +*/ +#define LUAI_GCMUL 200 + +/* How many bytes to allocate before next GC step */ +#define LUAI_GCSTEPSIZE (200 * sizeof(Table)) + + +#define setgcparam(g,p,v) (g->gcparams[LUA_GCP##p] = luaO_codeparam(v)) +#define applygcparam(g,p,x) luaO_applyparam(g->gcparams[LUA_GCP##p], x) + +/* }====================================================== */ + + +/* +** Control when GC is running: +*/ +#define GCSTPUSR 1 /* bit true when GC stopped by user */ +#define GCSTPGC 2 /* bit true when GC stopped by itself */ +#define GCSTPCLS 4 /* bit true when closing Lua state */ +#define gcrunning(g) ((g)->gcstp == 0) + + +/* +** Does one step of collection when debt becomes zero. 'pre'/'pos' +** allows some adjustments to be done only when needed. macro +** 'condchangemem' is used only for heavy tests (forcing a full +** GC cycle on every opportunity) +*/ + +#if !defined(HARDMEMTESTS) +#define condchangemem(L,pre,pos,emg) ((void)0) +#else +#define condchangemem(L,pre,pos,emg) \ + { if (gcrunning(G(L))) { pre; luaC_fullgc(L, emg); pos; } } +#endif + +#define luaC_condGC(L,pre,pos) \ + { if (G(L)->GCdebt <= 0) { pre; luaC_step(L); pos;}; \ + condchangemem(L,pre,pos,0); } + +/* more often than not, 'pre'/'pos' are empty */ +#define luaC_checkGC(L) luaC_condGC(L,(void)0,(void)0) + + +#define luaC_objbarrier(L,p,o) ( \ + (isblack(p) && iswhite(o)) ? \ + luaC_barrier_(L,obj2gco(p),obj2gco(o)) : cast_void(0)) + +#define luaC_barrier(L,p,v) ( \ + iscollectable(v) ? luaC_objbarrier(L,p,gcvalue(v)) : cast_void(0)) + +#define luaC_objbarrierback(L,p,o) ( \ + (isblack(p) && iswhite(o)) ? luaC_barrierback_(L,p) : cast_void(0)) + +#define luaC_barrierback(L,p,v) ( \ + iscollectable(v) ? luaC_objbarrierback(L, p, gcvalue(v)) : cast_void(0)) + +LUAI_FUNC void luaC_fix (lua_State *L, GCObject *o); +LUAI_FUNC void luaC_freeallobjects (lua_State *L); +LUAI_FUNC void luaC_step (lua_State *L); +LUAI_FUNC void luaC_runtilstate (lua_State *L, int state, int fast); +LUAI_FUNC void luaC_fullgc (lua_State *L, int isemergency); +LUAI_FUNC GCObject *luaC_newobj (lua_State *L, lu_byte tt, size_t sz); +LUAI_FUNC GCObject *luaC_newobjdt (lua_State *L, lu_byte tt, size_t sz, + size_t offset); +LUAI_FUNC void luaC_barrier_ (lua_State *L, GCObject *o, GCObject *v); +LUAI_FUNC void luaC_barrierback_ (lua_State *L, GCObject *o); +LUAI_FUNC void luaC_checkfinalizer (lua_State *L, GCObject *o, Table *mt); +LUAI_FUNC void luaC_changemode (lua_State *L, int newmode); + + +#endif diff --git a/crates/lua/vendor/lua/ljumptab.h b/crates/lua/vendor/lua/ljumptab.h new file mode 100644 index 0000000..52fa6d7 --- /dev/null +++ b/crates/lua/vendor/lua/ljumptab.h @@ -0,0 +1,114 @@ +/* +** $Id: ljumptab.h $ +** Jump Table for the Lua interpreter +** See Copyright Notice in lua.h +*/ + + +#undef vmdispatch +#undef vmcase +#undef vmbreak + +#define vmdispatch(x) goto *disptab[x]; + +#define vmcase(l) L_##l: + +#define vmbreak vmfetch(); vmdispatch(GET_OPCODE(i)); + + +static const void *const disptab[NUM_OPCODES] = { + +#if 0 +** you can update the following list with this command: +** +** sed -n '/^OP_/!d; s/OP_/\&\&L_OP_/ ; s/,.*/,/ ; s/\/.*// ; p' lopcodes.h +** +#endif + +&&L_OP_MOVE, +&&L_OP_LOADI, +&&L_OP_LOADF, +&&L_OP_LOADK, +&&L_OP_LOADKX, +&&L_OP_LOADFALSE, +&&L_OP_LFALSESKIP, +&&L_OP_LOADTRUE, +&&L_OP_LOADNIL, +&&L_OP_GETUPVAL, +&&L_OP_SETUPVAL, +&&L_OP_GETTABUP, +&&L_OP_GETTABLE, +&&L_OP_GETI, +&&L_OP_GETFIELD, +&&L_OP_SETTABUP, +&&L_OP_SETTABLE, +&&L_OP_SETI, +&&L_OP_SETFIELD, +&&L_OP_NEWTABLE, +&&L_OP_SELF, +&&L_OP_ADDI, +&&L_OP_ADDK, +&&L_OP_SUBK, +&&L_OP_MULK, +&&L_OP_MODK, +&&L_OP_POWK, +&&L_OP_DIVK, +&&L_OP_IDIVK, +&&L_OP_BANDK, +&&L_OP_BORK, +&&L_OP_BXORK, +&&L_OP_SHLI, +&&L_OP_SHRI, +&&L_OP_ADD, +&&L_OP_SUB, +&&L_OP_MUL, +&&L_OP_MOD, +&&L_OP_POW, +&&L_OP_DIV, +&&L_OP_IDIV, +&&L_OP_BAND, +&&L_OP_BOR, +&&L_OP_BXOR, +&&L_OP_SHL, +&&L_OP_SHR, +&&L_OP_MMBIN, +&&L_OP_MMBINI, +&&L_OP_MMBINK, +&&L_OP_UNM, +&&L_OP_BNOT, +&&L_OP_NOT, +&&L_OP_LEN, +&&L_OP_CONCAT, +&&L_OP_CLOSE, +&&L_OP_TBC, +&&L_OP_JMP, +&&L_OP_EQ, +&&L_OP_LT, +&&L_OP_LE, +&&L_OP_EQK, +&&L_OP_EQI, +&&L_OP_LTI, +&&L_OP_LEI, +&&L_OP_GTI, +&&L_OP_GEI, +&&L_OP_TEST, +&&L_OP_TESTSET, +&&L_OP_CALL, +&&L_OP_TAILCALL, +&&L_OP_RETURN, +&&L_OP_RETURN0, +&&L_OP_RETURN1, +&&L_OP_FORLOOP, +&&L_OP_FORPREP, +&&L_OP_TFORPREP, +&&L_OP_TFORCALL, +&&L_OP_TFORLOOP, +&&L_OP_SETLIST, +&&L_OP_CLOSURE, +&&L_OP_VARARG, +&&L_OP_GETVARG, +&&L_OP_ERRNNIL, +&&L_OP_VARARGPREP, +&&L_OP_EXTRAARG + +}; diff --git a/crates/lua/vendor/lua/llex.c b/crates/lua/vendor/lua/llex.c new file mode 100644 index 0000000..7d1316c --- /dev/null +++ b/crates/lua/vendor/lua/llex.c @@ -0,0 +1,603 @@ +/* +** $Id: llex.c $ +** Lexical Analyzer +** See Copyright Notice in lua.h +*/ + +#define llex_c +#define LUA_CORE + +#include "lprefix.h" + + +#include +#include + +#include "lua.h" + +#include "lctype.h" +#include "ldebug.h" +#include "ldo.h" +#include "lgc.h" +#include "llex.h" +#include "lobject.h" +#include "lparser.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "lzio.h" + + + +#define next(ls) (ls->current = zgetc(ls->z)) + + +/* minimum size for string buffer */ +#if !defined(LUA_MINBUFFER) +#define LUA_MINBUFFER 32 +#endif + + +#define currIsNewline(ls) (ls->current == '\n' || ls->current == '\r') + + +/* ORDER RESERVED */ +static const char *const luaX_tokens [] = { + "and", "break", "do", "else", "elseif", + "end", "false", "for", "function", "global", "goto", "if", + "in", "local", "nil", "not", "or", "repeat", + "return", "then", "true", "until", "while", + "//", "..", "...", "==", ">=", "<=", "~=", + "<<", ">>", "::", "", + "", "", "", "" +}; + + +#define save_and_next(ls) (save(ls, ls->current), next(ls)) + + +static l_noret lexerror (LexState *ls, const char *msg, int token); + + +static void save (LexState *ls, int c) { + Mbuffer *b = ls->buff; + if (luaZ_bufflen(b) + 1 > luaZ_sizebuffer(b)) { + size_t newsize = luaZ_sizebuffer(b); /* get old size */; + if (newsize >= (MAX_SIZE/3 * 2)) /* larger than MAX_SIZE/1.5 ? */ + lexerror(ls, "lexical element too long", 0); + newsize += (newsize >> 1); /* new size is 1.5 times the old one */ + luaZ_resizebuffer(ls->L, b, newsize); + } + b->buffer[luaZ_bufflen(b)++] = cast_char(c); +} + + +void luaX_init (lua_State *L) { + int i; + TString *e = luaS_newliteral(L, LUA_ENV); /* create env name */ + luaC_fix(L, obj2gco(e)); /* never collect this name */ + for (i=0; iextra = cast_byte(i+1); /* reserved word */ + } +} + + +const char *luaX_token2str (LexState *ls, int token) { + if (token < FIRST_RESERVED) { /* single-byte symbols? */ + if (lisprint(token)) + return luaO_pushfstring(ls->L, "'%c'", token); + else /* control character */ + return luaO_pushfstring(ls->L, "'<\\%d>'", token); + } + else { + const char *s = luaX_tokens[token - FIRST_RESERVED]; + if (token < TK_EOS) /* fixed format (symbols and reserved words)? */ + return luaO_pushfstring(ls->L, "'%s'", s); + else /* names, strings, and numerals */ + return s; + } +} + + +static const char *txtToken (LexState *ls, int token) { + switch (token) { + case TK_NAME: case TK_STRING: + case TK_FLT: case TK_INT: + save(ls, '\0'); + return luaO_pushfstring(ls->L, "'%s'", luaZ_buffer(ls->buff)); + default: + return luaX_token2str(ls, token); + } +} + + +static l_noret lexerror (LexState *ls, const char *msg, int token) { + msg = luaG_addinfo(ls->L, msg, ls->source, ls->linenumber); + if (token) + luaO_pushfstring(ls->L, "%s near %s", msg, txtToken(ls, token)); + luaD_throw(ls->L, LUA_ERRSYNTAX); +} + + +l_noret luaX_syntaxerror (LexState *ls, const char *msg) { + lexerror(ls, msg, ls->t.token); +} + + +/* +** Anchors a string in scanner's table so that it will not be collected +** until the end of the compilation; by that time it should be anchored +** somewhere. It also internalizes long strings, ensuring there is only +** one copy of each unique string. +*/ +static TString *anchorstr (LexState *ls, TString *ts) { + lua_State *L = ls->L; + TValue oldts; + int tag = luaH_getstr(ls->h, ts, &oldts); + if (!tagisempty(tag)) /* string already present? */ + return tsvalue(&oldts); /* use stored value */ + else { /* create a new entry */ + TValue *stv = s2v(L->top.p++); /* reserve stack space for string */ + setsvalue(L, stv, ts); /* push (anchor) the string on the stack */ + luaH_set(L, ls->h, stv, stv); /* t[string] = string */ + /* table is not a metatable, so it does not need to invalidate cache */ + luaC_checkGC(L); + L->top.p--; /* remove string from stack */ + return ts; + } +} + + +/* +** Creates a new string and anchors it in scanner's table. +*/ +TString *luaX_newstring (LexState *ls, const char *str, size_t l) { + return anchorstr(ls, luaS_newlstr(ls->L, str, l)); +} + + +/* +** increment line number and skips newline sequence (any of +** \n, \r, \n\r, or \r\n) +*/ +static void inclinenumber (LexState *ls) { + int old = ls->current; + lua_assert(currIsNewline(ls)); + next(ls); /* skip '\n' or '\r' */ + if (currIsNewline(ls) && ls->current != old) + next(ls); /* skip '\n\r' or '\r\n' */ + if (++ls->linenumber >= INT_MAX) + lexerror(ls, "chunk has too many lines", 0); +} + + +void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, TString *source, + int firstchar) { + ls->t.token = 0; + ls->L = L; + ls->current = firstchar; + ls->lookahead.token = TK_EOS; /* no look-ahead token */ + ls->z = z; + ls->fs = NULL; + ls->linenumber = 1; + ls->lastline = 1; + ls->source = source; + /* all three strings here ("_ENV", "break", "global") were fixed, + so they cannot be collected */ + ls->envn = luaS_newliteral(L, LUA_ENV); /* get env string */ + ls->brkn = luaS_newliteral(L, "break"); /* get "break" string */ +#if LUA_COMPAT_GLOBAL + /* compatibility mode: "global" is not a reserved word */ + ls->glbn = luaS_newliteral(L, "global"); /* get "global" string */ + ls->glbn->extra = 0; /* mark it as not reserved */ +#endif + luaZ_resizebuffer(ls->L, ls->buff, LUA_MINBUFFER); /* initialize buffer */ +} + + + +/* +** ======================================================= +** LEXICAL ANALYZER +** ======================================================= +*/ + + +static int check_next1 (LexState *ls, int c) { + if (ls->current == c) { + next(ls); + return 1; + } + else return 0; +} + + +/* +** Check whether current char is in set 'set' (with two chars) and +** saves it +*/ +static int check_next2 (LexState *ls, const char *set) { + lua_assert(set[2] == '\0'); + if (ls->current == set[0] || ls->current == set[1]) { + save_and_next(ls); + return 1; + } + else return 0; +} + + +/* LUA_NUMBER */ +/* +** This function is quite liberal in what it accepts, as 'luaO_str2num' +** will reject ill-formed numerals. Roughly, it accepts the following +** pattern: +** +** %d(%x|%.|([Ee][+-]?))* | 0[Xx](%x|%.|([Pp][+-]?))* +** +** The only tricky part is to accept [+-] only after a valid exponent +** mark, to avoid reading '3-4' or '0xe+1' as a single number. +** +** The caller might have already read an initial dot. +*/ +static int read_numeral (LexState *ls, SemInfo *seminfo) { + TValue obj; + const char *expo = "Ee"; + int first = ls->current; + lua_assert(lisdigit(ls->current)); + save_and_next(ls); + if (first == '0' && check_next2(ls, "xX")) /* hexadecimal? */ + expo = "Pp"; + for (;;) { + if (check_next2(ls, expo)) /* exponent mark? */ + check_next2(ls, "-+"); /* optional exponent sign */ + else if (lisxdigit(ls->current) || ls->current == '.') /* '%x|%.' */ + save_and_next(ls); + else break; + } + if (lislalpha(ls->current)) /* is numeral touching a letter? */ + save_and_next(ls); /* force an error */ + save(ls, '\0'); + if (luaO_str2num(luaZ_buffer(ls->buff), &obj) == 0) /* format error? */ + lexerror(ls, "malformed number", TK_FLT); + if (ttisinteger(&obj)) { + seminfo->i = ivalue(&obj); + return TK_INT; + } + else { + lua_assert(ttisfloat(&obj)); + seminfo->r = fltvalue(&obj); + return TK_FLT; + } +} + + +/* +** read a sequence '[=*[' or ']=*]', leaving the last bracket. If +** sequence is well formed, return its number of '='s + 2; otherwise, +** return 1 if it is a single bracket (no '='s and no 2nd bracket); +** otherwise (an unfinished '[==...') return 0. +*/ +static size_t skip_sep (LexState *ls) { + size_t count = 0; + int s = ls->current; + lua_assert(s == '[' || s == ']'); + save_and_next(ls); + while (ls->current == '=') { + save_and_next(ls); + count++; + } + return (ls->current == s) ? count + 2 + : (count == 0) ? 1 + : 0; +} + + +static void read_long_string (LexState *ls, SemInfo *seminfo, size_t sep) { + int line = ls->linenumber; /* initial line (for error message) */ + save_and_next(ls); /* skip 2nd '[' */ + if (currIsNewline(ls)) /* string starts with a newline? */ + inclinenumber(ls); /* skip it */ + for (;;) { + switch (ls->current) { + case EOZ: { /* error */ + const char *what = (seminfo ? "string" : "comment"); + const char *msg = luaO_pushfstring(ls->L, + "unfinished long %s (starting at line %d)", what, line); + lexerror(ls, msg, TK_EOS); + break; /* to avoid warnings */ + } + case ']': { + if (skip_sep(ls) == sep) { + save_and_next(ls); /* skip 2nd ']' */ + goto endloop; + } + break; + } + case '\n': case '\r': { + save(ls, '\n'); + inclinenumber(ls); + if (!seminfo) luaZ_resetbuffer(ls->buff); /* avoid wasting space */ + break; + } + default: { + if (seminfo) save_and_next(ls); + else next(ls); + } + } + } endloop: + if (seminfo) + seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff) + sep, + luaZ_bufflen(ls->buff) - 2 * sep); +} + + +static void esccheck (LexState *ls, int c, const char *msg) { + if (!c) { + if (ls->current != EOZ) + save_and_next(ls); /* add current to buffer for error message */ + lexerror(ls, msg, TK_STRING); + } +} + + +static int gethexa (LexState *ls) { + save_and_next(ls); + esccheck (ls, lisxdigit(ls->current), "hexadecimal digit expected"); + return luaO_hexavalue(ls->current); +} + + +static int readhexaesc (LexState *ls) { + int r = gethexa(ls); + r = (r << 4) + gethexa(ls); + luaZ_buffremove(ls->buff, 2); /* remove saved chars from buffer */ + return r; +} + + +/* +** When reading a UTF-8 escape sequence, save everything to the buffer +** for error reporting in case of errors; 'i' counts the number of +** saved characters, so that they can be removed if case of success. +*/ +static l_uint32 readutf8esc (LexState *ls) { + l_uint32 r; + int i = 4; /* number of chars to be removed: start with #"\u{X" */ + save_and_next(ls); /* skip 'u' */ + esccheck(ls, ls->current == '{', "missing '{'"); + r = cast_uint(gethexa(ls)); /* must have at least one digit */ + while (cast_void(save_and_next(ls)), lisxdigit(ls->current)) { + i++; + esccheck(ls, r <= (0x7FFFFFFFu >> 4), "UTF-8 value too large"); + r = (r << 4) + luaO_hexavalue(ls->current); + } + esccheck(ls, ls->current == '}', "missing '}'"); + next(ls); /* skip '}' */ + luaZ_buffremove(ls->buff, i); /* remove saved chars from buffer */ + return r; +} + + +static void utf8esc (LexState *ls) { + char buff[UTF8BUFFSZ]; + int n = luaO_utf8esc(buff, readutf8esc(ls)); + for (; n > 0; n--) /* add 'buff' to string */ + save(ls, buff[UTF8BUFFSZ - n]); +} + + +static int readdecesc (LexState *ls) { + int i; + int r = 0; /* result accumulator */ + for (i = 0; i < 3 && lisdigit(ls->current); i++) { /* read up to 3 digits */ + r = 10*r + ls->current - '0'; + save_and_next(ls); + } + esccheck(ls, r <= UCHAR_MAX, "decimal escape too large"); + luaZ_buffremove(ls->buff, i); /* remove read digits from buffer */ + return r; +} + + +static void read_string (LexState *ls, int del, SemInfo *seminfo) { + save_and_next(ls); /* keep delimiter (for error messages) */ + while (ls->current != del) { + switch (ls->current) { + case EOZ: + lexerror(ls, "unfinished string", TK_EOS); + break; /* to avoid warnings */ + case '\n': + case '\r': + lexerror(ls, "unfinished string", TK_STRING); + break; /* to avoid warnings */ + case '\\': { /* escape sequences */ + int c; /* final character to be saved */ + save_and_next(ls); /* keep '\\' for error messages */ + switch (ls->current) { + case 'a': c = '\a'; goto read_save; + case 'b': c = '\b'; goto read_save; + case 'f': c = '\f'; goto read_save; + case 'n': c = '\n'; goto read_save; + case 'r': c = '\r'; goto read_save; + case 't': c = '\t'; goto read_save; + case 'v': c = '\v'; goto read_save; + case 'x': c = readhexaesc(ls); goto read_save; + case 'u': utf8esc(ls); goto no_save; + case '\n': case '\r': + inclinenumber(ls); c = '\n'; goto only_save; + case '\\': case '\"': case '\'': + c = ls->current; goto read_save; + case EOZ: goto no_save; /* will raise an error next loop */ + case 'z': { /* zap following span of spaces */ + luaZ_buffremove(ls->buff, 1); /* remove '\\' */ + next(ls); /* skip the 'z' */ + while (lisspace(ls->current)) { + if (currIsNewline(ls)) inclinenumber(ls); + else next(ls); + } + goto no_save; + } + default: { + esccheck(ls, lisdigit(ls->current), "invalid escape sequence"); + c = readdecesc(ls); /* digital escape '\ddd' */ + goto only_save; + } + } + read_save: + next(ls); + /* go through */ + only_save: + luaZ_buffremove(ls->buff, 1); /* remove '\\' */ + save(ls, c); + /* go through */ + no_save: break; + } + default: + save_and_next(ls); + } + } + save_and_next(ls); /* skip delimiter */ + seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff) + 1, + luaZ_bufflen(ls->buff) - 2); +} + + +static int llex (LexState *ls, SemInfo *seminfo) { + luaZ_resetbuffer(ls->buff); + for (;;) { + switch (ls->current) { + case '\n': case '\r': { /* line breaks */ + inclinenumber(ls); + break; + } + case ' ': case '\f': case '\t': case '\v': { /* spaces */ + next(ls); + break; + } + case '-': { /* '-' or '--' (comment) */ + next(ls); + if (ls->current != '-') return '-'; + /* else is a comment */ + next(ls); + if (ls->current == '[') { /* long comment? */ + size_t sep = skip_sep(ls); + luaZ_resetbuffer(ls->buff); /* 'skip_sep' may dirty the buffer */ + if (sep >= 2) { + read_long_string(ls, NULL, sep); /* skip long comment */ + luaZ_resetbuffer(ls->buff); /* previous call may dirty the buff. */ + break; + } + } + /* else short comment */ + while (!currIsNewline(ls) && ls->current != EOZ) + next(ls); /* skip until end of line (or end of file) */ + break; + } + case '[': { /* long string or simply '[' */ + size_t sep = skip_sep(ls); + if (sep >= 2) { + read_long_string(ls, seminfo, sep); + return TK_STRING; + } + else if (sep == 0) /* '[=...' missing second bracket? */ + lexerror(ls, "invalid long string delimiter", TK_STRING); + return '['; + } + case '=': { + next(ls); + if (check_next1(ls, '=')) return TK_EQ; /* '==' */ + else return '='; + } + case '<': { + next(ls); + if (check_next1(ls, '=')) return TK_LE; /* '<=' */ + else if (check_next1(ls, '<')) return TK_SHL; /* '<<' */ + else return '<'; + } + case '>': { + next(ls); + if (check_next1(ls, '=')) return TK_GE; /* '>=' */ + else if (check_next1(ls, '>')) return TK_SHR; /* '>>' */ + else return '>'; + } + case '/': { + next(ls); + if (check_next1(ls, '/')) return TK_IDIV; /* '//' */ + else return '/'; + } + case '~': { + next(ls); + if (check_next1(ls, '=')) return TK_NE; /* '~=' */ + else return '~'; + } + case ':': { + next(ls); + if (check_next1(ls, ':')) return TK_DBCOLON; /* '::' */ + else return ':'; + } + case '"': case '\'': { /* short literal strings */ + read_string(ls, ls->current, seminfo); + return TK_STRING; + } + case '.': { /* '.', '..', '...', or number */ + save_and_next(ls); + if (check_next1(ls, '.')) { + if (check_next1(ls, '.')) + return TK_DOTS; /* '...' */ + else return TK_CONCAT; /* '..' */ + } + else if (!lisdigit(ls->current)) return '.'; + else return read_numeral(ls, seminfo); + } + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': { + return read_numeral(ls, seminfo); + } + case EOZ: { + return TK_EOS; + } + default: { + if (lislalpha(ls->current)) { /* identifier or reserved word? */ + TString *ts; + do { + save_and_next(ls); + } while (lislalnum(ls->current)); + /* find or create string */ + ts = luaS_newlstr(ls->L, luaZ_buffer(ls->buff), + luaZ_bufflen(ls->buff)); + if (isreserved(ts)) /* reserved word? */ + return ts->extra - 1 + FIRST_RESERVED; + else { + seminfo->ts = anchorstr(ls, ts); + return TK_NAME; + } + } + else { /* single-char tokens ('+', '*', '%', '{', '}', ...) */ + int c = ls->current; + next(ls); + return c; + } + } + } + } +} + + +void luaX_next (LexState *ls) { + ls->lastline = ls->linenumber; + if (ls->lookahead.token != TK_EOS) { /* is there a look-ahead token? */ + ls->t = ls->lookahead; /* use this one */ + ls->lookahead.token = TK_EOS; /* and discharge it */ + } + else + ls->t.token = llex(ls, &ls->t.seminfo); /* read next token */ +} + + +int luaX_lookahead (LexState *ls) { + lua_assert(ls->lookahead.token == TK_EOS); + ls->lookahead.token = llex(ls, &ls->lookahead.seminfo); + return ls->lookahead.token; +} diff --git a/crates/lua/vendor/lua/llex.h b/crates/lua/vendor/lua/llex.h new file mode 100644 index 0000000..37016e8 --- /dev/null +++ b/crates/lua/vendor/lua/llex.h @@ -0,0 +1,93 @@ +/* +** $Id: llex.h $ +** Lexical Analyzer +** See Copyright Notice in lua.h +*/ + +#ifndef llex_h +#define llex_h + +#include + +#include "lobject.h" +#include "lzio.h" + + +/* +** Single-char tokens (terminal symbols) are represented by their own +** numeric code. Other tokens start at the following value. +*/ +#define FIRST_RESERVED (UCHAR_MAX + 1) + + +#if !defined(LUA_ENV) +#define LUA_ENV "_ENV" +#endif + + +/* +* WARNING: if you change the order of this enumeration, +* grep "ORDER RESERVED" +*/ +enum RESERVED { + /* terminal symbols denoted by reserved words */ + TK_AND = FIRST_RESERVED, TK_BREAK, + TK_DO, TK_ELSE, TK_ELSEIF, TK_END, TK_FALSE, TK_FOR, TK_FUNCTION, + TK_GLOBAL, TK_GOTO, TK_IF, TK_IN, TK_LOCAL, TK_NIL, TK_NOT, TK_OR, + TK_REPEAT, TK_RETURN, TK_THEN, TK_TRUE, TK_UNTIL, TK_WHILE, + /* other terminal symbols */ + TK_IDIV, TK_CONCAT, TK_DOTS, TK_EQ, TK_GE, TK_LE, TK_NE, + TK_SHL, TK_SHR, + TK_DBCOLON, TK_EOS, + TK_FLT, TK_INT, TK_NAME, TK_STRING +}; + +/* number of reserved words */ +#define NUM_RESERVED (cast_int(TK_WHILE-FIRST_RESERVED + 1)) + + +typedef union { + lua_Number r; + lua_Integer i; + TString *ts; +} SemInfo; /* semantics information */ + + +typedef struct Token { + int token; + SemInfo seminfo; +} Token; + + +/* state of the scanner plus state of the parser when shared by all + functions */ +typedef struct LexState { + int current; /* current character (charint) */ + int linenumber; /* input line counter */ + int lastline; /* line of last token 'consumed' */ + Token t; /* current token */ + Token lookahead; /* look ahead token */ + struct FuncState *fs; /* current function (parser) */ + struct lua_State *L; + ZIO *z; /* input stream */ + Mbuffer *buff; /* buffer for tokens */ + Table *h; /* to avoid collection/reuse strings */ + struct Dyndata *dyd; /* dynamic structures used by the parser */ + TString *source; /* current source name */ + TString *envn; /* environment variable name */ + TString *brkn; /* "break" name (used as a label) */ + TString *glbn; /* "global" name (when not a reserved word) */ +} LexState; + + +LUAI_FUNC void luaX_init (lua_State *L); +LUAI_FUNC void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, + TString *source, int firstchar); +LUAI_FUNC TString *luaX_newstring (LexState *ls, const char *str, size_t l); +LUAI_FUNC void luaX_next (LexState *ls); +LUAI_FUNC int luaX_lookahead (LexState *ls); +LUAI_FUNC l_noret luaX_syntaxerror (LexState *ls, const char *s); +LUAI_FUNC const char *luaX_token2str (LexState *ls, int token); + + +#endif diff --git a/crates/lua/vendor/lua/llimits.h b/crates/lua/vendor/lua/llimits.h new file mode 100644 index 0000000..b1eb41a --- /dev/null +++ b/crates/lua/vendor/lua/llimits.h @@ -0,0 +1,356 @@ +/* +** $Id: llimits.h $ +** Limits, basic types, and some other 'installation-dependent' definitions +** See Copyright Notice in lua.h +*/ + +#ifndef llimits_h +#define llimits_h + + +#include +#include + + +#include "lua.h" + + +#define l_numbits(t) cast_int(sizeof(t) * CHAR_BIT) + +/* +** 'l_mem' is a signed integer big enough to count the total memory +** used by Lua. (It is signed due to the use of debt in several +** computations.) 'lu_mem' is a corresponding unsigned type. Usually, +** 'ptrdiff_t' should work, but we use 'long' for 16-bit machines. +*/ +#if defined(LUAI_MEM) /* { external definitions? */ +typedef LUAI_MEM l_mem; +typedef LUAI_UMEM lu_mem; +#elif LUAI_IS32INT /* }{ */ +typedef ptrdiff_t l_mem; +typedef size_t lu_mem; +#else /* 16-bit ints */ /* }{ */ +typedef long l_mem; +typedef unsigned long lu_mem; +#endif /* } */ + +#define MAX_LMEM \ + cast(l_mem, (cast(lu_mem, 1) << (l_numbits(l_mem) - 1)) - 1) + + +/* chars used as small naturals (so that 'char' is reserved for characters) */ +typedef unsigned char lu_byte; +typedef signed char ls_byte; + + +/* Type for thread status/error codes */ +typedef lu_byte TStatus; + +/* The C API still uses 'int' for status/error codes */ +#define APIstatus(st) cast_int(st) + +/* maximum value for size_t */ +#define MAX_SIZET ((size_t)(~(size_t)0)) + +/* +** Maximum size for strings and userdata visible for Lua; should be +** representable as a lua_Integer and as a size_t. +*/ +#define MAX_SIZE (sizeof(size_t) < sizeof(lua_Integer) ? MAX_SIZET \ + : cast_sizet(LUA_MAXINTEGER)) + +/* +** test whether an unsigned value is a power of 2 (or zero) +*/ +#define ispow2(x) (((x) & ((x) - 1)) == 0) + + +/* number of chars of a literal string without the ending \0 */ +#define LL(x) (sizeof(x)/sizeof(char) - 1) + + +/* +** conversion of pointer to unsigned integer: this is for hashing only; +** there is no problem if the integer cannot hold the whole pointer +** value. (In strict ISO C this may cause undefined behavior, but no +** actual machine seems to bother.) +*/ +#if !defined(LUA_USE_C89) && defined(__STDC_VERSION__) && \ + __STDC_VERSION__ >= 199901L +#include +#if defined(UINTPTR_MAX) /* even in C99 this type is optional */ +#define L_P2I uintptr_t +#else /* no 'intptr'? */ +#define L_P2I uintmax_t /* use the largest available integer */ +#endif +#else /* C89 option */ +#define L_P2I size_t +#endif + +#define point2uint(p) cast_uint((L_P2I)(p) & UINT_MAX) + + + +/* types of 'usual argument conversions' for lua_Number and lua_Integer */ +typedef LUAI_UACNUMBER l_uacNumber; +typedef LUAI_UACINT l_uacInt; + + +/* +** Internal assertions for in-house debugging +*/ +#if defined LUAI_ASSERT +#undef NDEBUG +#include +#define lua_assert(c) assert(c) +#define assert_code(c) c +#endif + +#if defined(lua_assert) +#else +#define lua_assert(c) ((void)0) +#define assert_code(c) ((void)0) +#endif + +#define check_exp(c,e) (lua_assert(c), (e)) +/* to avoid problems with conditions too long */ +#define lua_longassert(c) assert_code((c) ? (void)0 : lua_assert(0)) + + +/* macro to avoid warnings about unused variables */ +#if !defined(UNUSED) +#define UNUSED(x) ((void)(x)) +#endif + + +/* type casts (a macro highlights casts in the code) */ +#define cast(t, exp) ((t)(exp)) + +#define cast_void(i) cast(void, (i)) +#define cast_voidp(i) cast(void *, (i)) +#define cast_num(i) cast(lua_Number, (i)) +#define cast_int(i) cast(int, (i)) +#define cast_short(i) cast(short, (i)) +#define cast_uint(i) cast(unsigned int, (i)) +#define cast_byte(i) cast(lu_byte, (i)) +#define cast_uchar(i) cast(unsigned char, (i)) +#define cast_char(i) cast(char, (i)) +#define cast_charp(i) cast(char *, (i)) +#define cast_sizet(i) cast(size_t, (i)) +#define cast_Integer(i) cast(lua_Integer, (i)) +#define cast_Inst(i) cast(Instruction, (i)) + + +/* cast a signed lua_Integer to lua_Unsigned */ +#if !defined(l_castS2U) +#define l_castS2U(i) ((lua_Unsigned)(i)) +#endif + +/* +** cast a lua_Unsigned to a signed lua_Integer; this cast is +** not strict ISO C, but two-complement architectures should +** work fine. +*/ +#if !defined(l_castU2S) +#define l_castU2S(i) ((lua_Integer)(i)) +#endif + +/* +** cast a size_t to lua_Integer: These casts are always valid for +** sizes of Lua objects (see MAX_SIZE) +*/ +#define cast_st2S(sz) ((lua_Integer)(sz)) + +/* Cast a ptrdiff_t to size_t, when it is known that the minuend +** comes from the subtrahend (the base) +*/ +#define ct_diff2sz(df) ((size_t)(df)) + +/* ptrdiff_t to lua_Integer */ +#define ct_diff2S(df) cast_st2S(ct_diff2sz(df)) + +/* +** Special type equivalent to '(void*)' for functions (to suppress some +** warnings when converting function pointers) +*/ +typedef void (*voidf)(void); + +/* +** Macro to convert pointer-to-void* to pointer-to-function. This cast +** is undefined according to ISO C, but POSIX assumes that it works. +** (The '__extension__' in gnu compilers is only to avoid warnings.) +*/ +#if defined(__GNUC__) +#define cast_func(p) (__extension__ (voidf)(p)) +#else +#define cast_func(p) ((voidf)(p)) +#endif + + + +/* +** non-return type +*/ +#if !defined(l_noret) + +#if defined(__GNUC__) +#define l_noret void __attribute__((noreturn)) +#elif defined(_MSC_VER) && _MSC_VER >= 1200 +#define l_noret void __declspec(noreturn) +#else +#define l_noret void +#endif + +#endif + + +/* +** Inline functions +*/ +#if !defined(LUA_USE_C89) +#define l_inline inline +#elif defined(__GNUC__) +#define l_inline __inline__ +#else +#define l_inline /* empty */ +#endif + +#define l_sinline static l_inline + + +/* +** An unsigned with (at least) 4 bytes +*/ +#if LUAI_IS32INT +typedef unsigned int l_uint32; +#else +typedef unsigned long l_uint32; +#endif + + +/* +** The luai_num* macros define the primitive operations over numbers. +*/ + +/* floor division (defined as 'floor(a/b)') */ +#if !defined(luai_numidiv) +#define luai_numidiv(L,a,b) l_floor(luai_numdiv(L,a,b)) +#endif + +/* float division */ +#if !defined(luai_numdiv) +#define luai_numdiv(L,a,b) ((void)L, (a)/(b)) +#endif + +/* +** modulo: defined as 'a - floor(a/b)*b'; the direct computation +** using this definition has several problems with rounding errors, +** so it is better to use 'fmod'. 'fmod' gives the result of +** 'a - trunc(a/b)*b', and therefore must be corrected when +** 'trunc(a/b) ~= floor(a/b)'. That happens when the division has a +** non-integer negative result: non-integer result is equivalent to +** a non-zero remainder 'm'; negative result is equivalent to 'a' and +** 'b' with different signs, or 'm' and 'b' with different signs +** (as the result 'm' of 'fmod' has the same sign of 'a'). +*/ +#if !defined(luai_nummod) +#define luai_nummod(L,a,b,m) \ + { (void)L; (m) = l_mathop(fmod)(a,b); \ + if (((m) > 0) ? (b) < 0 : ((m) < 0 && (b) > 0)) (m) += (b); } +#endif + +/* exponentiation */ +#if !defined(luai_numpow) +#define luai_numpow(L,a,b) \ + ((void)L, (b == 2) ? (a)*(a) : l_mathop(pow)(a,b)) +#endif + +/* the others are quite standard operations */ +#if !defined(luai_numadd) +#define luai_numadd(L,a,b) ((void)L, (a)+(b)) +#define luai_numsub(L,a,b) ((void)L, (a)-(b)) +#define luai_nummul(L,a,b) ((void)L, (a)*(b)) +#define luai_numunm(L,a) ((void)L, -(a)) +#define luai_numeq(a,b) ((a)==(b)) +#define luai_numlt(a,b) ((a)<(b)) +#define luai_numle(a,b) ((a)<=(b)) +#define luai_numgt(a,b) ((a)>(b)) +#define luai_numge(a,b) ((a)>=(b)) +#define luai_numisnan(a) (!luai_numeq((a), (a))) +#endif + + + +/* +** lua_numbertointeger converts a float number with an integral value +** to an integer, or returns 0 if the float is not within the range of +** a lua_Integer. (The range comparisons are tricky because of +** rounding. The tests here assume a two-complement representation, +** where MININTEGER always has an exact representation as a float; +** MAXINTEGER may not have one, and therefore its conversion to float +** may have an ill-defined value.) +*/ +#define lua_numbertointeger(n,p) \ + ((n) >= (LUA_NUMBER)(LUA_MININTEGER) && \ + (n) < -(LUA_NUMBER)(LUA_MININTEGER) && \ + (*(p) = (LUA_INTEGER)(n), 1)) + + + +/* +** LUAI_FUNC is a mark for all extern functions that are not to be +** exported to outside modules. +** LUAI_DDEF and LUAI_DDEC are marks for all extern (const) variables, +** none of which to be exported to outside modules (LUAI_DDEF for +** definitions and LUAI_DDEC for declarations). +** Elf and MACH/gcc (versions 3.2 and later) mark them as "hidden" to +** optimize access when Lua is compiled as a shared library. Not all elf +** targets support this attribute. Unfortunately, gcc does not offer +** a way to check whether the target offers that support, and those +** without support give a warning about it. To avoid these warnings, +** change to the default definition. +*/ +#if !defined(LUAI_FUNC) + +#if defined(__GNUC__) && ((__GNUC__*100 + __GNUC_MINOR__) >= 302) && \ + (defined(__ELF__) || defined(__MACH__)) +#define LUAI_FUNC __attribute__((visibility("internal"))) extern +#else +#define LUAI_FUNC extern +#endif + +#define LUAI_DDEC(dec) LUAI_FUNC dec +#define LUAI_DDEF /* empty */ + +#endif + + +/* Give these macros simpler names for internal use */ +#define l_likely(x) luai_likely(x) +#define l_unlikely(x) luai_unlikely(x) + +/* +** {================================================================== +** "Abstraction Layer" for basic report of messages and errors +** =================================================================== +*/ + +/* print a string */ +#if !defined(lua_writestring) +#define lua_writestring(s,l) fwrite((s), sizeof(char), (l), stdout) +#endif + +/* print a newline and flush the output */ +#if !defined(lua_writeline) +#define lua_writeline() (lua_writestring("\n", 1), fflush(stdout)) +#endif + +/* print an error message */ +#if !defined(lua_writestringerror) +#define lua_writestringerror(s,p) \ + (fprintf(stderr, (s), (p)), fflush(stderr)) +#endif + +/* }================================================================== */ + +#endif diff --git a/crates/lua/vendor/lua/lmathlib.c b/crates/lua/vendor/lua/lmathlib.c new file mode 100644 index 0000000..3fa895c --- /dev/null +++ b/crates/lua/vendor/lua/lmathlib.c @@ -0,0 +1,764 @@ +/* +** $Id: lmathlib.c $ +** Standard mathematical library +** See Copyright Notice in lua.h +*/ + +#define lmathlib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include +#include +#include +#include +#include + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" +#include "llimits.h" + + +#undef PI +#define PI (l_mathop(3.141592653589793238462643383279502884)) + + +static int math_abs (lua_State *L) { + if (lua_isinteger(L, 1)) { + lua_Integer n = lua_tointeger(L, 1); + if (n < 0) n = (lua_Integer)(0u - (lua_Unsigned)n); + lua_pushinteger(L, n); + } + else + lua_pushnumber(L, l_mathop(fabs)(luaL_checknumber(L, 1))); + return 1; +} + + +static int math_sin (lua_State *L) { + lua_pushnumber(L, l_mathop(sin)(luaL_checknumber(L, 1))); + return 1; +} + + +static int math_cos (lua_State *L) { + lua_pushnumber(L, l_mathop(cos)(luaL_checknumber(L, 1))); + return 1; +} + + +static int math_tan (lua_State *L) { + lua_pushnumber(L, l_mathop(tan)(luaL_checknumber(L, 1))); + return 1; +} + + +static int math_asin (lua_State *L) { + lua_pushnumber(L, l_mathop(asin)(luaL_checknumber(L, 1))); + return 1; +} + + +static int math_acos (lua_State *L) { + lua_pushnumber(L, l_mathop(acos)(luaL_checknumber(L, 1))); + return 1; +} + + +static int math_atan (lua_State *L) { + lua_Number y = luaL_checknumber(L, 1); + lua_Number x = luaL_optnumber(L, 2, 1); + lua_pushnumber(L, l_mathop(atan2)(y, x)); + return 1; +} + + +static int math_toint (lua_State *L) { + int valid; + lua_Integer n = lua_tointegerx(L, 1, &valid); + if (l_likely(valid)) + lua_pushinteger(L, n); + else { + luaL_checkany(L, 1); + luaL_pushfail(L); /* value is not convertible to integer */ + } + return 1; +} + + +static void pushnumint (lua_State *L, lua_Number d) { + lua_Integer n; + if (lua_numbertointeger(d, &n)) /* does 'd' fit in an integer? */ + lua_pushinteger(L, n); /* result is integer */ + else + lua_pushnumber(L, d); /* result is float */ +} + + +static int math_floor (lua_State *L) { + if (lua_isinteger(L, 1)) + lua_settop(L, 1); /* integer is its own floor */ + else { + lua_Number d = l_mathop(floor)(luaL_checknumber(L, 1)); + pushnumint(L, d); + } + return 1; +} + + +static int math_ceil (lua_State *L) { + if (lua_isinteger(L, 1)) + lua_settop(L, 1); /* integer is its own ceiling */ + else { + lua_Number d = l_mathop(ceil)(luaL_checknumber(L, 1)); + pushnumint(L, d); + } + return 1; +} + + +static int math_fmod (lua_State *L) { + if (lua_isinteger(L, 1) && lua_isinteger(L, 2)) { + lua_Integer d = lua_tointeger(L, 2); + if ((lua_Unsigned)d + 1u <= 1u) { /* special cases: -1 or 0 */ + luaL_argcheck(L, d != 0, 2, "zero"); + lua_pushinteger(L, 0); /* avoid overflow with 0x80000... / -1 */ + } + else + lua_pushinteger(L, lua_tointeger(L, 1) % d); + } + else + lua_pushnumber(L, l_mathop(fmod)(luaL_checknumber(L, 1), + luaL_checknumber(L, 2))); + return 1; +} + + +/* +** next function does not use 'modf', avoiding problems with 'double*' +** (which is not compatible with 'float*') when lua_Number is not +** 'double'. +*/ +static int math_modf (lua_State *L) { + if (lua_isinteger(L ,1)) { + lua_settop(L, 1); /* number is its own integer part */ + lua_pushnumber(L, 0); /* no fractional part */ + } + else { + lua_Number n = luaL_checknumber(L, 1); + /* integer part (rounds toward zero) */ + lua_Number ip = (n < 0) ? l_mathop(ceil)(n) : l_mathop(floor)(n); + pushnumint(L, ip); + /* fractional part (test needed for inf/-inf) */ + lua_pushnumber(L, (n == ip) ? l_mathop(0.0) : (n - ip)); + } + return 2; +} + + +static int math_sqrt (lua_State *L) { + lua_pushnumber(L, l_mathop(sqrt)(luaL_checknumber(L, 1))); + return 1; +} + + +static int math_ult (lua_State *L) { + lua_Integer a = luaL_checkinteger(L, 1); + lua_Integer b = luaL_checkinteger(L, 2); + lua_pushboolean(L, (lua_Unsigned)a < (lua_Unsigned)b); + return 1; +} + + +static int math_log (lua_State *L) { + lua_Number x = luaL_checknumber(L, 1); + lua_Number res; + if (lua_isnoneornil(L, 2)) + res = l_mathop(log)(x); + else { + lua_Number base = luaL_checknumber(L, 2); +#if !defined(LUA_USE_C89) + if (base == l_mathop(2.0)) + res = l_mathop(log2)(x); + else +#endif + if (base == l_mathop(10.0)) + res = l_mathop(log10)(x); + else + res = l_mathop(log)(x)/l_mathop(log)(base); + } + lua_pushnumber(L, res); + return 1; +} + + +static int math_exp (lua_State *L) { + lua_pushnumber(L, l_mathop(exp)(luaL_checknumber(L, 1))); + return 1; +} + + +static int math_deg (lua_State *L) { + lua_pushnumber(L, luaL_checknumber(L, 1) * (l_mathop(180.0) / PI)); + return 1; +} + + +static int math_rad (lua_State *L) { + lua_pushnumber(L, luaL_checknumber(L, 1) * (PI / l_mathop(180.0))); + return 1; +} + + +static int math_frexp (lua_State *L) { + lua_Number x = luaL_checknumber(L, 1); + int ep; + lua_pushnumber(L, l_mathop(frexp)(x, &ep)); + lua_pushinteger(L, ep); + return 2; +} + + +static int math_ldexp (lua_State *L) { + lua_Number x = luaL_checknumber(L, 1); + int ep = (int)luaL_checkinteger(L, 2); + lua_pushnumber(L, l_mathop(ldexp)(x, ep)); + return 1; +} + + +static int math_min (lua_State *L) { + int n = lua_gettop(L); /* number of arguments */ + int imin = 1; /* index of current minimum value */ + int i; + luaL_argcheck(L, n >= 1, 1, "value expected"); + for (i = 2; i <= n; i++) { + if (lua_compare(L, i, imin, LUA_OPLT)) + imin = i; + } + lua_pushvalue(L, imin); + return 1; +} + + +static int math_max (lua_State *L) { + int n = lua_gettop(L); /* number of arguments */ + int imax = 1; /* index of current maximum value */ + int i; + luaL_argcheck(L, n >= 1, 1, "value expected"); + for (i = 2; i <= n; i++) { + if (lua_compare(L, imax, i, LUA_OPLT)) + imax = i; + } + lua_pushvalue(L, imax); + return 1; +} + + +static int math_type (lua_State *L) { + if (lua_type(L, 1) == LUA_TNUMBER) + lua_pushstring(L, (lua_isinteger(L, 1)) ? "integer" : "float"); + else { + luaL_checkany(L, 1); + luaL_pushfail(L); + } + return 1; +} + + + +/* +** {================================================================== +** Pseudo-Random Number Generator based on 'xoshiro256**'. +** =================================================================== +*/ + +/* +** This code uses lots of shifts. ISO C does not allow shifts greater +** than or equal to the width of the type being shifted, so some shifts +** are written in convoluted ways to match that restriction. For +** preprocessor tests, it assumes a width of 32 bits, so the maximum +** shift there is 31 bits. +*/ + + +/* number of binary digits in the mantissa of a float */ +#define FIGS l_floatatt(MANT_DIG) + +#if FIGS > 64 +/* there are only 64 random bits; use them all */ +#undef FIGS +#define FIGS 64 +#endif + + +/* +** LUA_RAND32 forces the use of 32-bit integers in the implementation +** of the PRN generator (mainly for testing). +*/ +#if !defined(LUA_RAND32) && !defined(Rand64) + +/* try to find an integer type with at least 64 bits */ + +#if ((ULONG_MAX >> 31) >> 31) >= 3 + +/* 'long' has at least 64 bits */ +#define Rand64 unsigned long +#define SRand64 long + +#elif !defined(LUA_USE_C89) && defined(LLONG_MAX) + +/* there is a 'long long' type (which must have at least 64 bits) */ +#define Rand64 unsigned long long +#define SRand64 long long + +#elif ((LUA_MAXUNSIGNED >> 31) >> 31) >= 3 + +/* 'lua_Unsigned' has at least 64 bits */ +#define Rand64 lua_Unsigned +#define SRand64 lua_Integer + +#endif + +#endif + + +#if defined(Rand64) /* { */ + +/* +** Standard implementation, using 64-bit integers. +** If 'Rand64' has more than 64 bits, the extra bits do not interfere +** with the 64 initial bits, except in a right shift. Moreover, the +** final result has to discard the extra bits. +*/ + +/* avoid using extra bits when needed */ +#define trim64(x) ((x) & 0xffffffffffffffffu) + + +/* rotate left 'x' by 'n' bits */ +static Rand64 rotl (Rand64 x, int n) { + return (x << n) | (trim64(x) >> (64 - n)); +} + +static Rand64 nextrand (Rand64 *state) { + Rand64 state0 = state[0]; + Rand64 state1 = state[1]; + Rand64 state2 = state[2] ^ state0; + Rand64 state3 = state[3] ^ state1; + Rand64 res = rotl(state1 * 5, 7) * 9; + state[0] = state0 ^ state3; + state[1] = state1 ^ state2; + state[2] = state2 ^ (state1 << 17); + state[3] = rotl(state3, 45); + return res; +} + + +/* +** Convert bits from a random integer into a float in the +** interval [0,1), getting the higher FIG bits from the +** random unsigned integer and converting that to a float. +** Some old Microsoft compilers cannot cast an unsigned long +** to a floating-point number, so we use a signed long as an +** intermediary. When lua_Number is float or double, the shift ensures +** that 'sx' is non negative; in that case, a good compiler will remove +** the correction. +*/ + +/* must throw out the extra (64 - FIGS) bits */ +#define shift64_FIG (64 - FIGS) + +/* 2^(-FIGS) == 2^-1 / 2^(FIGS-1) */ +#define scaleFIG (l_mathop(0.5) / ((Rand64)1 << (FIGS - 1))) + +static lua_Number I2d (Rand64 x) { + SRand64 sx = (SRand64)(trim64(x) >> shift64_FIG); + lua_Number res = (lua_Number)(sx) * scaleFIG; + if (sx < 0) + res += l_mathop(1.0); /* correct the two's complement if negative */ + lua_assert(0 <= res && res < 1); + return res; +} + +/* convert a 'Rand64' to a 'lua_Unsigned' */ +#define I2UInt(x) ((lua_Unsigned)trim64(x)) + +/* convert a 'lua_Unsigned' to a 'Rand64' */ +#define Int2I(x) ((Rand64)(x)) + + +#else /* no 'Rand64' }{ */ + +/* +** Use two 32-bit integers to represent a 64-bit quantity. +*/ +typedef struct Rand64 { + l_uint32 h; /* higher half */ + l_uint32 l; /* lower half */ +} Rand64; + + +/* +** If 'l_uint32' has more than 32 bits, the extra bits do not interfere +** with the 32 initial bits, except in a right shift and comparisons. +** Moreover, the final result has to discard the extra bits. +*/ + +/* avoid using extra bits when needed */ +#define trim32(x) ((x) & 0xffffffffu) + + +/* +** basic operations on 'Rand64' values +*/ + +/* build a new Rand64 value */ +static Rand64 packI (l_uint32 h, l_uint32 l) { + Rand64 result; + result.h = h; + result.l = l; + return result; +} + +/* return i << n */ +static Rand64 Ishl (Rand64 i, int n) { + lua_assert(n > 0 && n < 32); + return packI((i.h << n) | (trim32(i.l) >> (32 - n)), i.l << n); +} + +/* i1 ^= i2 */ +static void Ixor (Rand64 *i1, Rand64 i2) { + i1->h ^= i2.h; + i1->l ^= i2.l; +} + +/* return i1 + i2 */ +static Rand64 Iadd (Rand64 i1, Rand64 i2) { + Rand64 result = packI(i1.h + i2.h, i1.l + i2.l); + if (trim32(result.l) < trim32(i1.l)) /* carry? */ + result.h++; + return result; +} + +/* return i * 5 */ +static Rand64 times5 (Rand64 i) { + return Iadd(Ishl(i, 2), i); /* i * 5 == (i << 2) + i */ +} + +/* return i * 9 */ +static Rand64 times9 (Rand64 i) { + return Iadd(Ishl(i, 3), i); /* i * 9 == (i << 3) + i */ +} + +/* return 'i' rotated left 'n' bits */ +static Rand64 rotl (Rand64 i, int n) { + lua_assert(n > 0 && n < 32); + return packI((i.h << n) | (trim32(i.l) >> (32 - n)), + (trim32(i.h) >> (32 - n)) | (i.l << n)); +} + +/* for offsets larger than 32, rotate right by 64 - offset */ +static Rand64 rotl1 (Rand64 i, int n) { + lua_assert(n > 32 && n < 64); + n = 64 - n; + return packI((trim32(i.h) >> n) | (i.l << (32 - n)), + (i.h << (32 - n)) | (trim32(i.l) >> n)); +} + +/* +** implementation of 'xoshiro256**' algorithm on 'Rand64' values +*/ +static Rand64 nextrand (Rand64 *state) { + Rand64 res = times9(rotl(times5(state[1]), 7)); + Rand64 t = Ishl(state[1], 17); + Ixor(&state[2], state[0]); + Ixor(&state[3], state[1]); + Ixor(&state[1], state[2]); + Ixor(&state[0], state[3]); + Ixor(&state[2], t); + state[3] = rotl1(state[3], 45); + return res; +} + + +/* +** Converts a 'Rand64' into a float. +*/ + +/* an unsigned 1 with proper type */ +#define UONE ((l_uint32)1) + + +#if FIGS <= 32 + +/* 2^(-FIGS) */ +#define scaleFIG (l_mathop(0.5) / (UONE << (FIGS - 1))) + +/* +** get up to 32 bits from higher half, shifting right to +** throw out the extra bits. +*/ +static lua_Number I2d (Rand64 x) { + lua_Number h = (lua_Number)(trim32(x.h) >> (32 - FIGS)); + return h * scaleFIG; +} + +#else /* 32 < FIGS <= 64 */ + +/* 2^(-FIGS) = 1.0 / 2^30 / 2^3 / 2^(FIGS-33) */ +#define scaleFIG \ + (l_mathop(1.0) / (UONE << 30) / l_mathop(8.0) / (UONE << (FIGS - 33))) + +/* +** use FIGS - 32 bits from lower half, throwing out the other +** (32 - (FIGS - 32)) = (64 - FIGS) bits +*/ +#define shiftLOW (64 - FIGS) + +/* +** higher 32 bits go after those (FIGS - 32) bits: shiftHI = 2^(FIGS - 32) +*/ +#define shiftHI ((lua_Number)(UONE << (FIGS - 33)) * l_mathop(2.0)) + + +static lua_Number I2d (Rand64 x) { + lua_Number h = (lua_Number)trim32(x.h) * shiftHI; + lua_Number l = (lua_Number)(trim32(x.l) >> shiftLOW); + return (h + l) * scaleFIG; +} + +#endif + + +/* convert a 'Rand64' to a 'lua_Unsigned' */ +static lua_Unsigned I2UInt (Rand64 x) { + return (((lua_Unsigned)trim32(x.h) << 31) << 1) | (lua_Unsigned)trim32(x.l); +} + +/* convert a 'lua_Unsigned' to a 'Rand64' */ +static Rand64 Int2I (lua_Unsigned n) { + return packI((l_uint32)((n >> 31) >> 1), (l_uint32)n); +} + +#endif /* } */ + + +/* +** A state uses four 'Rand64' values. +*/ +typedef struct { + Rand64 s[4]; +} RanState; + + +/* +** Project the random integer 'ran' into the interval [0, n]. +** Because 'ran' has 2^B possible values, the projection can only be +** uniform when the size of the interval is a power of 2 (exact +** division). So, to get a uniform projection into [0, n], we +** first compute 'lim', the smallest Mersenne number not smaller than +** 'n'. We then project 'ran' into the interval [0, lim]. If the result +** is inside [0, n], we are done. Otherwise, we try with another 'ran', +** until we have a result inside the interval. +*/ +static lua_Unsigned project (lua_Unsigned ran, lua_Unsigned n, + RanState *state) { + lua_Unsigned lim = n; /* to compute the Mersenne number */ + int sh; /* how much to spread bits to the right in 'lim' */ + /* spread '1' bits in 'lim' until it becomes a Mersenne number */ + for (sh = 1; (lim & (lim + 1)) != 0; sh *= 2) + lim |= (lim >> sh); /* spread '1's to the right */ + while ((ran &= lim) > n) /* project 'ran' into [0..lim] and test */ + ran = I2UInt(nextrand(state->s)); /* not inside [0..n]? try again */ + return ran; +} + + +static int math_random (lua_State *L) { + lua_Integer low, up; + lua_Unsigned p; + RanState *state = (RanState *)lua_touserdata(L, lua_upvalueindex(1)); + Rand64 rv = nextrand(state->s); /* next pseudo-random value */ + switch (lua_gettop(L)) { /* check number of arguments */ + case 0: { /* no arguments */ + lua_pushnumber(L, I2d(rv)); /* float between 0 and 1 */ + return 1; + } + case 1: { /* only upper limit */ + low = 1; + up = luaL_checkinteger(L, 1); + if (up == 0) { /* single 0 as argument? */ + lua_pushinteger(L, l_castU2S(I2UInt(rv))); /* full random integer */ + return 1; + } + break; + } + case 2: { /* lower and upper limits */ + low = luaL_checkinteger(L, 1); + up = luaL_checkinteger(L, 2); + break; + } + default: return luaL_error(L, "wrong number of arguments"); + } + /* random integer in the interval [low, up] */ + luaL_argcheck(L, low <= up, 1, "interval is empty"); + /* project random integer into the interval [0, up - low] */ + p = project(I2UInt(rv), l_castS2U(up) - l_castS2U(low), state); + lua_pushinteger(L, l_castU2S(p + l_castS2U(low))); + return 1; +} + + +static void setseed (lua_State *L, Rand64 *state, + lua_Unsigned n1, lua_Unsigned n2) { + int i; + state[0] = Int2I(n1); + state[1] = Int2I(0xff); /* avoid a zero state */ + state[2] = Int2I(n2); + state[3] = Int2I(0); + for (i = 0; i < 16; i++) + nextrand(state); /* discard initial values to "spread" seed */ + lua_pushinteger(L, l_castU2S(n1)); + lua_pushinteger(L, l_castU2S(n2)); +} + + +static int math_randomseed (lua_State *L) { + RanState *state = (RanState *)lua_touserdata(L, lua_upvalueindex(1)); + lua_Unsigned n1, n2; + if (lua_isnone(L, 1)) { + n1 = luaL_makeseed(L); /* "random" seed */ + n2 = I2UInt(nextrand(state->s)); /* in case seed is not that random... */ + } + else { + n1 = l_castS2U(luaL_checkinteger(L, 1)); + n2 = l_castS2U(luaL_optinteger(L, 2, 0)); + } + setseed(L, state->s, n1, n2); + return 2; /* return seeds */ +} + + +static const luaL_Reg randfuncs[] = { + {"random", math_random}, + {"randomseed", math_randomseed}, + {NULL, NULL} +}; + + +/* +** Register the random functions and initialize their state. +*/ +static void setrandfunc (lua_State *L) { + RanState *state = (RanState *)lua_newuserdatauv(L, sizeof(RanState), 0); + setseed(L, state->s, luaL_makeseed(L), 0); /* initialize with random seed */ + lua_pop(L, 2); /* remove pushed seeds */ + luaL_setfuncs(L, randfuncs, 1); +} + +/* }================================================================== */ + + +/* +** {================================================================== +** Deprecated functions (for compatibility only) +** =================================================================== +*/ +#if defined(LUA_COMPAT_MATHLIB) + +static int math_cosh (lua_State *L) { + lua_pushnumber(L, l_mathop(cosh)(luaL_checknumber(L, 1))); + return 1; +} + +static int math_sinh (lua_State *L) { + lua_pushnumber(L, l_mathop(sinh)(luaL_checknumber(L, 1))); + return 1; +} + +static int math_tanh (lua_State *L) { + lua_pushnumber(L, l_mathop(tanh)(luaL_checknumber(L, 1))); + return 1; +} + +static int math_pow (lua_State *L) { + lua_Number x = luaL_checknumber(L, 1); + lua_Number y = luaL_checknumber(L, 2); + lua_pushnumber(L, l_mathop(pow)(x, y)); + return 1; +} + +static int math_log10 (lua_State *L) { + lua_pushnumber(L, l_mathop(log10)(luaL_checknumber(L, 1))); + return 1; +} + +#endif +/* }================================================================== */ + + + +static const luaL_Reg mathlib[] = { + {"abs", math_abs}, + {"acos", math_acos}, + {"asin", math_asin}, + {"atan", math_atan}, + {"ceil", math_ceil}, + {"cos", math_cos}, + {"deg", math_deg}, + {"exp", math_exp}, + {"tointeger", math_toint}, + {"floor", math_floor}, + {"fmod", math_fmod}, + {"frexp", math_frexp}, + {"ult", math_ult}, + {"ldexp", math_ldexp}, + {"log", math_log}, + {"max", math_max}, + {"min", math_min}, + {"modf", math_modf}, + {"rad", math_rad}, + {"sin", math_sin}, + {"sqrt", math_sqrt}, + {"tan", math_tan}, + {"type", math_type}, +#if defined(LUA_COMPAT_MATHLIB) + {"atan2", math_atan}, + {"cosh", math_cosh}, + {"sinh", math_sinh}, + {"tanh", math_tanh}, + {"pow", math_pow}, + {"log10", math_log10}, +#endif + /* placeholders */ + {"random", NULL}, + {"randomseed", NULL}, + {"pi", NULL}, + {"huge", NULL}, + {"maxinteger", NULL}, + {"mininteger", NULL}, + {NULL, NULL} +}; + + +/* +** Open math library +*/ +LUAMOD_API int luaopen_math (lua_State *L) { + luaL_newlib(L, mathlib); + lua_pushnumber(L, PI); + lua_setfield(L, -2, "pi"); + lua_pushnumber(L, (lua_Number)HUGE_VAL); + lua_setfield(L, -2, "huge"); + lua_pushinteger(L, LUA_MAXINTEGER); + lua_setfield(L, -2, "maxinteger"); + lua_pushinteger(L, LUA_MININTEGER); + lua_setfield(L, -2, "mininteger"); + setrandfunc(L); + return 1; +} diff --git a/crates/lua/vendor/lua/lmem.c b/crates/lua/vendor/lua/lmem.c new file mode 100644 index 0000000..de8503d --- /dev/null +++ b/crates/lua/vendor/lua/lmem.c @@ -0,0 +1,215 @@ +/* +** $Id: lmem.c $ +** Interface to Memory Manager +** See Copyright Notice in lua.h +*/ + +#define lmem_c +#define LUA_CORE + +#include "lprefix.h" + + +#include + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" + + + +/* +** About the realloc function: +** void *frealloc (void *ud, void *ptr, size_t osize, size_t nsize); +** ('osize' is the old size, 'nsize' is the new size) +** +** - frealloc(ud, p, x, 0) frees the block 'p' and returns NULL. +** Particularly, frealloc(ud, NULL, 0, 0) does nothing, +** which is equivalent to free(NULL) in ISO C. +** +** - frealloc(ud, NULL, x, s) creates a new block of size 's' +** (no matter 'x'). Returns NULL if it cannot create the new block. +** +** - otherwise, frealloc(ud, b, x, y) reallocates the block 'b' from +** size 'x' to size 'y'. Returns NULL if it cannot reallocate the +** block to the new size. +*/ + + +/* +** Macro to call the allocation function. +*/ +#define callfrealloc(g,block,os,ns) ((*g->frealloc)(g->ud, block, os, ns)) + + +/* +** When an allocation fails, it will try again after an emergency +** collection, except when it cannot run a collection. The GC should +** not be called while the state is not fully built, as the collector +** is not yet fully initialized. Also, it should not be called when +** 'gcstopem' is true, because then the interpreter is in the middle of +** a collection step. +*/ +#define cantryagain(g) (completestate(g) && !g->gcstopem) + + + + +#if defined(EMERGENCYGCTESTS) +/* +** First allocation will fail except when freeing a block (frees never +** fail) and when it cannot try again; this fail will trigger 'tryagain' +** and a full GC cycle at every allocation. +*/ +static void *firsttry (global_State *g, void *block, size_t os, size_t ns) { + if (ns > 0 && cantryagain(g)) + return NULL; /* fail */ + else /* normal allocation */ + return callfrealloc(g, block, os, ns); +} +#else +#define firsttry(g,block,os,ns) callfrealloc(g, block, os, ns) +#endif + + + + + +/* +** {================================================================== +** Functions to allocate/deallocate arrays for the Parser +** =================================================================== +*/ + +/* +** Minimum size for arrays during parsing, to avoid overhead of +** reallocating to size 1, then 2, and then 4. All these arrays +** will be reallocated to exact sizes or erased when parsing ends. +*/ +#define MINSIZEARRAY 4 + + +void *luaM_growaux_ (lua_State *L, void *block, int nelems, int *psize, + unsigned size_elems, int limit, const char *what) { + void *newblock; + int size = *psize; + if (nelems + 1 <= size) /* does one extra element still fit? */ + return block; /* nothing to be done */ + if (size >= limit / 2) { /* cannot double it? */ + if (l_unlikely(size >= limit)) /* cannot grow even a little? */ + luaG_runerror(L, "too many %s (limit is %d)", what, limit); + size = limit; /* still have at least one free place */ + } + else { + size *= 2; + if (size < MINSIZEARRAY) + size = MINSIZEARRAY; /* minimum size */ + } + lua_assert(nelems + 1 <= size && size <= limit); + /* 'limit' ensures that multiplication will not overflow */ + newblock = luaM_saferealloc_(L, block, cast_sizet(*psize) * size_elems, + cast_sizet(size) * size_elems); + *psize = size; /* update only when everything else is OK */ + return newblock; +} + + +/* +** In prototypes, the size of the array is also its number of +** elements (to save memory). So, if it cannot shrink an array +** to its number of elements, the only option is to raise an +** error. +*/ +void *luaM_shrinkvector_ (lua_State *L, void *block, int *size, + int final_n, unsigned size_elem) { + void *newblock; + size_t oldsize = cast_sizet(*size) * size_elem; + size_t newsize = cast_sizet(final_n) * size_elem; + lua_assert(newsize <= oldsize); + newblock = luaM_saferealloc_(L, block, oldsize, newsize); + *size = final_n; + return newblock; +} + +/* }================================================================== */ + + +l_noret luaM_toobig (lua_State *L) { + luaG_runerror(L, "memory allocation error: block too big"); +} + + +/* +** Free memory +*/ +void luaM_free_ (lua_State *L, void *block, size_t osize) { + global_State *g = G(L); + lua_assert((osize == 0) == (block == NULL)); + callfrealloc(g, block, osize, 0); + g->GCdebt += cast(l_mem, osize); +} + + +/* +** In case of allocation fail, this function will do an emergency +** collection to free some memory and then try the allocation again. +*/ +static void *tryagain (lua_State *L, void *block, + size_t osize, size_t nsize) { + global_State *g = G(L); + if (cantryagain(g)) { + luaC_fullgc(L, 1); /* try to free some memory... */ + return callfrealloc(g, block, osize, nsize); /* try again */ + } + else return NULL; /* cannot run an emergency collection */ +} + + +/* +** Generic allocation routine. +*/ +void *luaM_realloc_ (lua_State *L, void *block, size_t osize, size_t nsize) { + void *newblock; + global_State *g = G(L); + lua_assert((osize == 0) == (block == NULL)); + newblock = firsttry(g, block, osize, nsize); + if (l_unlikely(newblock == NULL && nsize > 0)) { + newblock = tryagain(L, block, osize, nsize); + if (newblock == NULL) /* still no memory? */ + return NULL; /* do not update 'GCdebt' */ + } + lua_assert((nsize == 0) == (newblock == NULL)); + g->GCdebt -= cast(l_mem, nsize) - cast(l_mem, osize); + return newblock; +} + + +void *luaM_saferealloc_ (lua_State *L, void *block, size_t osize, + size_t nsize) { + void *newblock = luaM_realloc_(L, block, osize, nsize); + if (l_unlikely(newblock == NULL && nsize > 0)) /* allocation failed? */ + luaM_error(L); + return newblock; +} + + +void *luaM_malloc_ (lua_State *L, size_t size, int tag) { + if (size == 0) + return NULL; /* that's all */ + else { + global_State *g = G(L); + void *newblock = firsttry(g, NULL, cast_sizet(tag), size); + if (l_unlikely(newblock == NULL)) { + newblock = tryagain(L, NULL, cast_sizet(tag), size); + if (newblock == NULL) + luaM_error(L); + } + g->GCdebt -= cast(l_mem, size); + return newblock; + } +} diff --git a/crates/lua/vendor/lua/lmem.h b/crates/lua/vendor/lua/lmem.h new file mode 100644 index 0000000..39beacf --- /dev/null +++ b/crates/lua/vendor/lua/lmem.h @@ -0,0 +1,95 @@ +/* +** $Id: lmem.h $ +** Interface to Memory Manager +** See Copyright Notice in lua.h +*/ + +#ifndef lmem_h +#define lmem_h + + +#include + +#include "llimits.h" +#include "lua.h" + + +#define luaM_error(L) luaD_throw(L, LUA_ERRMEM) + + +/* +** This macro tests whether it is safe to multiply 'n' by the size of +** type 't' without overflows. Because 'e' is always constant, it avoids +** the runtime division MAX_SIZET/(e). +** (The macro is somewhat complex to avoid warnings: The 'sizeof' +** comparison avoids a runtime comparison when overflow cannot occur. +** The compiler should be able to optimize the real test by itself, but +** when it does it, it may give a warning about "comparison is always +** false due to limited range of data type"; the +1 tricks the compiler, +** avoiding this warning but also this optimization.) +*/ +#define luaM_testsize(n,e) \ + (sizeof(n) >= sizeof(size_t) && cast_sizet((n)) + 1 > MAX_SIZET/(e)) + +#define luaM_checksize(L,n,e) \ + (luaM_testsize(n,e) ? luaM_toobig(L) : cast_void(0)) + + +/* +** Computes the minimum between 'n' and 'MAX_SIZET/sizeof(t)', so that +** the result is not larger than 'n' and cannot overflow a 'size_t' +** when multiplied by the size of type 't'. (Assumes that 'n' is an +** 'int' and that 'int' is not larger than 'size_t'.) +*/ +#define luaM_limitN(n,t) \ + ((cast_sizet(n) <= MAX_SIZET/sizeof(t)) ? (n) : \ + cast_int((MAX_SIZET/sizeof(t)))) + + +/* +** Arrays of chars do not need any test +*/ +#define luaM_reallocvchar(L,b,on,n) \ + cast_charp(luaM_saferealloc_(L, (b), (on)*sizeof(char), (n)*sizeof(char))) + +#define luaM_freemem(L, b, s) luaM_free_(L, (b), (s)) +#define luaM_free(L, b) luaM_free_(L, (b), sizeof(*(b))) +#define luaM_freearray(L, b, n) luaM_free_(L, (b), (n)*sizeof(*(b))) + +#define luaM_new(L,t) cast(t*, luaM_malloc_(L, sizeof(t), 0)) +#define luaM_newvector(L,n,t) \ + cast(t*, luaM_malloc_(L, cast_sizet(n)*sizeof(t), 0)) +#define luaM_newvectorchecked(L,n,t) \ + (luaM_checksize(L,n,sizeof(t)), luaM_newvector(L,n,t)) + +#define luaM_newobject(L,tag,s) luaM_malloc_(L, (s), tag) + +#define luaM_newblock(L, size) luaM_newvector(L, size, char) + +#define luaM_growvector(L,v,nelems,size,t,limit,e) \ + ((v)=cast(t *, luaM_growaux_(L,v,nelems,&(size),sizeof(t), \ + luaM_limitN(limit,t),e))) + +#define luaM_reallocvector(L, v,oldn,n,t) \ + (cast(t *, luaM_realloc_(L, v, cast_sizet(oldn) * sizeof(t), \ + cast_sizet(n) * sizeof(t)))) + +#define luaM_shrinkvector(L,v,size,fs,t) \ + ((v)=cast(t *, luaM_shrinkvector_(L, v, &(size), fs, sizeof(t)))) + +LUAI_FUNC l_noret luaM_toobig (lua_State *L); + +/* not to be called directly */ +LUAI_FUNC void *luaM_realloc_ (lua_State *L, void *block, size_t oldsize, + size_t size); +LUAI_FUNC void *luaM_saferealloc_ (lua_State *L, void *block, size_t oldsize, + size_t size); +LUAI_FUNC void luaM_free_ (lua_State *L, void *block, size_t osize); +LUAI_FUNC void *luaM_growaux_ (lua_State *L, void *block, int nelems, + int *size, unsigned size_elem, int limit, + const char *what); +LUAI_FUNC void *luaM_shrinkvector_ (lua_State *L, void *block, int *nelem, + int final_n, unsigned size_elem); +LUAI_FUNC void *luaM_malloc_ (lua_State *L, size_t size, int tag); + +#endif diff --git a/crates/lua/vendor/lua/lobject.c b/crates/lua/vendor/lua/lobject.c new file mode 100644 index 0000000..b3009f3 --- /dev/null +++ b/crates/lua/vendor/lua/lobject.c @@ -0,0 +1,717 @@ +/* +** $Id: lobject.c $ +** Some generic functions over Lua objects +** See Copyright Notice in lua.h +*/ + +#define lobject_c +#define LUA_CORE + +#include "lprefix.h" + + +#include +#include +#include +#include +#include +#include +#include + +#include "lua.h" + +#include "lctype.h" +#include "ldebug.h" +#include "ldo.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "lvm.h" + + +/* +** Computes ceil(log2(x)), which is the smallest integer n such that +** x <= (1 << n). +*/ +lu_byte luaO_ceillog2 (unsigned int x) { + static const lu_byte log_2[256] = { /* log_2[i - 1] = ceil(log2(i)) */ + 0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8 + }; + int l = 0; + x--; + while (x >= 256) { l += 8; x >>= 8; } + return cast_byte(l + log_2[x]); +} + +/* +** Encodes 'p'% as a floating-point byte, represented as (eeeexxxx). +** The exponent is represented using excess-7. Mimicking IEEE 754, the +** representation normalizes the number when possible, assuming an extra +** 1 before the mantissa (xxxx) and adding one to the exponent (eeee) +** to signal that. So, the real value is (1xxxx) * 2^(eeee - 7 - 1) if +** eeee != 0, and (xxxx) * 2^-7 otherwise (subnormal numbers). +*/ +lu_byte luaO_codeparam (unsigned int p) { + if (p >= (cast(lu_mem, 0x1F) << (0xF - 7 - 1)) * 100u) /* overflow? */ + return 0xFF; /* return maximum value */ + else { + p = (cast(l_uint32, p) * 128 + 99) / 100; /* round up the division */ + if (p < 0x10) { /* subnormal number? */ + /* exponent bits are already zero; nothing else to do */ + return cast_byte(p); + } + else { /* p >= 0x10 implies ceil(log2(p + 1)) >= 5 */ + /* preserve 5 bits in 'p' */ + unsigned log = luaO_ceillog2(p + 1) - 5u; + return cast_byte(((p >> log) - 0x10) | ((log + 1) << 4)); + } + } +} + + +/* +** Computes 'p' times 'x', where 'p' is a floating-point byte. Roughly, +** we have to multiply 'x' by the mantissa and then shift accordingly to +** the exponent. If the exponent is positive, both the multiplication +** and the shift increase 'x', so we have to care only about overflows. +** For negative exponents, however, multiplying before the shift keeps +** more significant bits, as long as the multiplication does not +** overflow, so we check which order is best. +*/ +l_mem luaO_applyparam (lu_byte p, l_mem x) { + int m = p & 0xF; /* mantissa */ + int e = (p >> 4); /* exponent */ + if (e > 0) { /* normalized? */ + e--; /* correct exponent */ + m += 0x10; /* correct mantissa; maximum value is 0x1F */ + } + e -= 7; /* correct excess-7 */ + if (e >= 0) { + if (x < (MAX_LMEM / 0x1F) >> e) /* no overflow? */ + return (x * m) << e; /* order doesn't matter here */ + else /* real overflow */ + return MAX_LMEM; + } + else { /* negative exponent */ + e = -e; + if (x < MAX_LMEM / 0x1F) /* multiplication cannot overflow? */ + return (x * m) >> e; /* multiplying first gives more precision */ + else if ((x >> e) < MAX_LMEM / 0x1F) /* cannot overflow after shift? */ + return (x >> e) * m; + else /* real overflow */ + return MAX_LMEM; + } +} + + +static lua_Integer intarith (lua_State *L, int op, lua_Integer v1, + lua_Integer v2) { + switch (op) { + case LUA_OPADD: return intop(+, v1, v2); + case LUA_OPSUB:return intop(-, v1, v2); + case LUA_OPMUL:return intop(*, v1, v2); + case LUA_OPMOD: return luaV_mod(L, v1, v2); + case LUA_OPIDIV: return luaV_idiv(L, v1, v2); + case LUA_OPBAND: return intop(&, v1, v2); + case LUA_OPBOR: return intop(|, v1, v2); + case LUA_OPBXOR: return intop(^, v1, v2); + case LUA_OPSHL: return luaV_shiftl(v1, v2); + case LUA_OPSHR: return luaV_shiftr(v1, v2); + case LUA_OPUNM: return intop(-, 0, v1); + case LUA_OPBNOT: return intop(^, ~l_castS2U(0), v1); + default: lua_assert(0); return 0; + } +} + + +static lua_Number numarith (lua_State *L, int op, lua_Number v1, + lua_Number v2) { + switch (op) { + case LUA_OPADD: return luai_numadd(L, v1, v2); + case LUA_OPSUB: return luai_numsub(L, v1, v2); + case LUA_OPMUL: return luai_nummul(L, v1, v2); + case LUA_OPDIV: return luai_numdiv(L, v1, v2); + case LUA_OPPOW: return luai_numpow(L, v1, v2); + case LUA_OPIDIV: return luai_numidiv(L, v1, v2); + case LUA_OPUNM: return luai_numunm(L, v1); + case LUA_OPMOD: return luaV_modf(L, v1, v2); + default: lua_assert(0); return 0; + } +} + + +int luaO_rawarith (lua_State *L, int op, const TValue *p1, const TValue *p2, + TValue *res) { + switch (op) { + case LUA_OPBAND: case LUA_OPBOR: case LUA_OPBXOR: + case LUA_OPSHL: case LUA_OPSHR: + case LUA_OPBNOT: { /* operate only on integers */ + lua_Integer i1; lua_Integer i2; + if (tointegerns(p1, &i1) && tointegerns(p2, &i2)) { + setivalue(res, intarith(L, op, i1, i2)); + return 1; + } + else return 0; /* fail */ + } + case LUA_OPDIV: case LUA_OPPOW: { /* operate only on floats */ + lua_Number n1; lua_Number n2; + if (tonumberns(p1, n1) && tonumberns(p2, n2)) { + setfltvalue(res, numarith(L, op, n1, n2)); + return 1; + } + else return 0; /* fail */ + } + default: { /* other operations */ + lua_Number n1; lua_Number n2; + if (ttisinteger(p1) && ttisinteger(p2)) { + setivalue(res, intarith(L, op, ivalue(p1), ivalue(p2))); + return 1; + } + else if (tonumberns(p1, n1) && tonumberns(p2, n2)) { + setfltvalue(res, numarith(L, op, n1, n2)); + return 1; + } + else return 0; /* fail */ + } + } +} + + +void luaO_arith (lua_State *L, int op, const TValue *p1, const TValue *p2, + StkId res) { + if (!luaO_rawarith(L, op, p1, p2, s2v(res))) { + /* could not perform raw operation; try metamethod */ + luaT_trybinTM(L, p1, p2, res, cast(TMS, (op - LUA_OPADD) + TM_ADD)); + } +} + + +lu_byte luaO_hexavalue (int c) { + lua_assert(lisxdigit(c)); + if (lisdigit(c)) return cast_byte(c - '0'); + else return cast_byte((ltolower(c) - 'a') + 10); +} + + +static int isneg (const char **s) { + if (**s == '-') { (*s)++; return 1; } + else if (**s == '+') (*s)++; + return 0; +} + + + +/* +** {================================================================== +** Lua's implementation for 'lua_strx2number' +** =================================================================== +*/ + +#if !defined(lua_strx2number) + +/* maximum number of significant digits to read (to avoid overflows + even with single floats) */ +#define MAXSIGDIG 30 + +/* +** convert a hexadecimal numeric string to a number, following +** C99 specification for 'strtod' +*/ +static lua_Number lua_strx2number (const char *s, char **endptr) { + int dot = lua_getlocaledecpoint(); + lua_Number r = l_mathop(0.0); /* result (accumulator) */ + int sigdig = 0; /* number of significant digits */ + int nosigdig = 0; /* number of non-significant digits */ + int e = 0; /* exponent correction */ + int neg; /* 1 if number is negative */ + int hasdot = 0; /* true after seen a dot */ + *endptr = cast_charp(s); /* nothing is valid yet */ + while (lisspace(cast_uchar(*s))) s++; /* skip initial spaces */ + neg = isneg(&s); /* check sign */ + if (!(*s == '0' && (*(s + 1) == 'x' || *(s + 1) == 'X'))) /* check '0x' */ + return l_mathop(0.0); /* invalid format (no '0x') */ + for (s += 2; ; s++) { /* skip '0x' and read numeral */ + if (*s == dot) { + if (hasdot) break; /* second dot? stop loop */ + else hasdot = 1; + } + else if (lisxdigit(cast_uchar(*s))) { + if (sigdig == 0 && *s == '0') /* non-significant digit (zero)? */ + nosigdig++; + else if (++sigdig <= MAXSIGDIG) /* can read it without overflow? */ + r = (r * l_mathop(16.0)) + luaO_hexavalue(*s); + else e++; /* too many digits; ignore, but still count for exponent */ + if (hasdot) e--; /* decimal digit? correct exponent */ + } + else break; /* neither a dot nor a digit */ + } + if (nosigdig + sigdig == 0) /* no digits? */ + return l_mathop(0.0); /* invalid format */ + *endptr = cast_charp(s); /* valid up to here */ + e *= 4; /* each digit multiplies/divides value by 2^4 */ + if (*s == 'p' || *s == 'P') { /* exponent part? */ + int exp1 = 0; /* exponent value */ + int neg1; /* exponent sign */ + s++; /* skip 'p' */ + neg1 = isneg(&s); /* sign */ + if (!lisdigit(cast_uchar(*s))) + return l_mathop(0.0); /* invalid; must have at least one digit */ + while (lisdigit(cast_uchar(*s))) /* read exponent */ + exp1 = exp1 * 10 + *(s++) - '0'; + if (neg1) exp1 = -exp1; + e += exp1; + *endptr = cast_charp(s); /* valid up to here */ + } + if (neg) r = -r; + return l_mathop(ldexp)(r, e); +} + +#endif +/* }====================================================== */ + + +/* maximum length of a numeral to be converted to a number */ +#if !defined (L_MAXLENNUM) +#define L_MAXLENNUM 200 +#endif + +/* +** Convert string 's' to a Lua number (put in 'result'). Return NULL on +** fail or the address of the ending '\0' on success. ('mode' == 'x') +** means a hexadecimal numeral. +*/ +static const char *l_str2dloc (const char *s, lua_Number *result, int mode) { + char *endptr; + *result = (mode == 'x') ? lua_strx2number(s, &endptr) /* try to convert */ + : lua_str2number(s, &endptr); + if (endptr == s) return NULL; /* nothing recognized? */ + while (lisspace(cast_uchar(*endptr))) endptr++; /* skip trailing spaces */ + return (*endptr == '\0') ? endptr : NULL; /* OK iff no trailing chars */ +} + + +/* +** Convert string 's' to a Lua number (put in 'result') handling the +** current locale. +** This function accepts both the current locale or a dot as the radix +** mark. If the conversion fails, it may mean number has a dot but +** locale accepts something else. In that case, the code copies 's' +** to a buffer (because 's' is read-only), changes the dot to the +** current locale radix mark, and tries to convert again. +** The variable 'mode' checks for special characters in the string: +** - 'n' means 'inf' or 'nan' (which should be rejected) +** - 'x' means a hexadecimal numeral +** - '.' just optimizes the search for the common case (no special chars) +*/ +static const char *l_str2d (const char *s, lua_Number *result) { + const char *endptr; + const char *pmode = strpbrk(s, ".xXnN"); /* look for special chars */ + int mode = pmode ? ltolower(cast_uchar(*pmode)) : 0; + if (mode == 'n') /* reject 'inf' and 'nan' */ + return NULL; + endptr = l_str2dloc(s, result, mode); /* try to convert */ + if (endptr == NULL) { /* failed? may be a different locale */ + char buff[L_MAXLENNUM + 1]; + const char *pdot = strchr(s, '.'); + if (pdot == NULL || strlen(s) > L_MAXLENNUM) + return NULL; /* string too long or no dot; fail */ + strcpy(buff, s); /* copy string to buffer */ + buff[pdot - s] = lua_getlocaledecpoint(); /* correct decimal point */ + endptr = l_str2dloc(buff, result, mode); /* try again */ + if (endptr != NULL) + endptr = s + (endptr - buff); /* make relative to 's' */ + } + return endptr; +} + + +#define MAXBY10 cast(lua_Unsigned, LUA_MAXINTEGER / 10) +#define MAXLASTD cast_int(LUA_MAXINTEGER % 10) + +static const char *l_str2int (const char *s, lua_Integer *result) { + lua_Unsigned a = 0; + int empty = 1; + int neg; + while (lisspace(cast_uchar(*s))) s++; /* skip initial spaces */ + neg = isneg(&s); + if (s[0] == '0' && + (s[1] == 'x' || s[1] == 'X')) { /* hex? */ + s += 2; /* skip '0x' */ + for (; lisxdigit(cast_uchar(*s)); s++) { + a = a * 16 + luaO_hexavalue(*s); + empty = 0; + } + } + else { /* decimal */ + for (; lisdigit(cast_uchar(*s)); s++) { + int d = *s - '0'; + if (a >= MAXBY10 && (a > MAXBY10 || d > MAXLASTD + neg)) /* overflow? */ + return NULL; /* do not accept it (as integer) */ + a = a * 10 + cast_uint(d); + empty = 0; + } + } + while (lisspace(cast_uchar(*s))) s++; /* skip trailing spaces */ + if (empty || *s != '\0') return NULL; /* something wrong in the numeral */ + else { + *result = l_castU2S((neg) ? 0u - a : a); + return s; + } +} + + +size_t luaO_str2num (const char *s, TValue *o) { + lua_Integer i; lua_Number n; + const char *e; + if ((e = l_str2int(s, &i)) != NULL) { /* try as an integer */ + setivalue(o, i); + } + else if ((e = l_str2d(s, &n)) != NULL) { /* else try as a float */ + setfltvalue(o, n); + } + else + return 0; /* conversion failed */ + return ct_diff2sz(e - s) + 1; /* success; return string size */ +} + + +int luaO_utf8esc (char *buff, l_uint32 x) { + int n = 1; /* number of bytes put in buffer (backwards) */ + lua_assert(x <= 0x7FFFFFFFu); + if (x < 0x80) /* ASCII? */ + buff[UTF8BUFFSZ - 1] = cast_char(x); + else { /* need continuation bytes */ + unsigned int mfb = 0x3f; /* maximum that fits in first byte */ + do { /* add continuation bytes */ + buff[UTF8BUFFSZ - (n++)] = cast_char(0x80 | (x & 0x3f)); + x >>= 6; /* remove added bits */ + mfb >>= 1; /* now there is one less bit available in first byte */ + } while (x > mfb); /* still needs continuation byte? */ + buff[UTF8BUFFSZ - n] = cast_char((~mfb << 1) | x); /* add first byte */ + } + return n; +} + + +/* +** The size of the buffer for the conversion of a number to a string +** 'LUA_N2SBUFFSZ' must be enough to accommodate both LUA_INTEGER_FMT +** and LUA_NUMBER_FMT. For a long long int, this is 19 digits plus a +** sign and a final '\0', adding to 21. For a long double, it can go to +** a sign, the dot, an exponent letter, an exponent sign, 4 exponent +** digits, the final '\0', plus the significant digits, which are +** approximately the *_DIG attribute. +*/ +#if LUA_N2SBUFFSZ < (20 + l_floatatt(DIG)) +#error "invalid value for LUA_N2SBUFFSZ" +#endif + + +/* +** Convert a float to a string, adding it to a buffer. First try with +** a not too large number of digits, to avoid noise (for instance, +** 1.1 going to "1.1000000000000001"). If that lose precision, so +** that reading the result back gives a different number, then do the +** conversion again with extra precision. Moreover, if the numeral looks +** like an integer (without a decimal point or an exponent), add ".0" to +** its end. +*/ +static int tostringbuffFloat (lua_Number n, char *buff) { + /* first conversion */ + int len = l_sprintf(buff, LUA_N2SBUFFSZ, LUA_NUMBER_FMT, + (LUAI_UACNUMBER)n); + lua_Number check = lua_str2number(buff, NULL); /* read it back */ + if (check != n) { /* not enough precision? */ + /* convert again with more precision */ + len = l_sprintf(buff, LUA_N2SBUFFSZ, LUA_NUMBER_FMT_N, + (LUAI_UACNUMBER)n); + } + /* looks like an integer? */ + if (buff[strspn(buff, "-0123456789")] == '\0') { + buff[len++] = lua_getlocaledecpoint(); + buff[len++] = '0'; /* adds '.0' to result */ + } + return len; +} + + +/* +** Convert a number object to a string, adding it to a buffer. +*/ +unsigned luaO_tostringbuff (const TValue *obj, char *buff) { + int len; + lua_assert(ttisnumber(obj)); + if (ttisinteger(obj)) + len = lua_integer2str(buff, LUA_N2SBUFFSZ, ivalue(obj)); + else + len = tostringbuffFloat(fltvalue(obj), buff); + lua_assert(len < LUA_N2SBUFFSZ); + return cast_uint(len); +} + + +/* +** Convert a number object to a Lua string, replacing the value at 'obj' +*/ +void luaO_tostring (lua_State *L, TValue *obj) { + char buff[LUA_N2SBUFFSZ]; + unsigned len = luaO_tostringbuff(obj, buff); + setsvalue(L, obj, luaS_newlstr(L, buff, len)); +} + + + + +/* +** {================================================================== +** 'luaO_pushvfstring' +** =================================================================== +*/ + +/* +** Size for buffer space used by 'luaO_pushvfstring'. It should be +** (LUA_IDSIZE + LUA_N2SBUFFSZ) + a minimal space for basic messages, +** so that 'luaG_addinfo' can work directly on the static buffer. +*/ +#define BUFVFS cast_uint(LUA_IDSIZE + LUA_N2SBUFFSZ + 95) + +/* +** Buffer used by 'luaO_pushvfstring'. 'err' signals an error while +** building result (memory error [1] or buffer overflow [2]). +*/ +typedef struct BuffFS { + lua_State *L; + char *b; + size_t buffsize; + size_t blen; /* length of string in 'buff' */ + int err; + char space[BUFVFS]; /* initial buffer */ +} BuffFS; + + +static void initbuff (lua_State *L, BuffFS *buff) { + buff->L = L; + buff->b = buff->space; + buff->buffsize = sizeof(buff->space); + buff->blen = 0; + buff->err = 0; +} + + +/* +** Push final result from 'luaO_pushvfstring'. This function may raise +** errors explicitly or through memory errors, so it must run protected. +*/ +static void pushbuff (lua_State *L, void *ud) { + BuffFS *buff = cast(BuffFS*, ud); + switch (buff->err) { + case 1: /* memory error */ + luaD_throw(L, LUA_ERRMEM); + break; + case 2: /* length overflow: Add "..." at the end of result */ + if (buff->buffsize - buff->blen < 3) + strcpy(buff->b + buff->blen - 3, "..."); /* 'blen' must be > 3 */ + else { /* there is enough space left for the "..." */ + strcpy(buff->b + buff->blen, "..."); + buff->blen += 3; + } + /* FALLTHROUGH */ + default: { /* no errors, but it can raise one creating the new string */ + TString *ts = luaS_newlstr(L, buff->b, buff->blen); + setsvalue2s(L, L->top.p, ts); + L->top.p++; + } + } +} + + +static const char *clearbuff (BuffFS *buff) { + lua_State *L = buff->L; + const char *res; + if (luaD_rawrunprotected(L, pushbuff, buff) != LUA_OK) /* errors? */ + res = NULL; /* error message is on the top of the stack */ + else + res = getstr(tsvalue(s2v(L->top.p - 1))); + if (buff->b != buff->space) /* using dynamic buffer? */ + luaM_freearray(L, buff->b, buff->buffsize); /* free it */ + return res; +} + + +static void addstr2buff (BuffFS *buff, const char *str, size_t slen) { + size_t left = buff->buffsize - buff->blen; /* space left in the buffer */ + if (buff->err) /* do nothing else after an error */ + return; + if (slen > left) { /* new string doesn't fit into current buffer? */ + if (slen > ((MAX_SIZE/2) - buff->blen)) { /* overflow? */ + memcpy(buff->b + buff->blen, str, left); /* copy what it can */ + buff->blen = buff->buffsize; + buff->err = 2; /* doesn't add anything else */ + return; + } + else { + size_t newsize = buff->buffsize + slen; /* limited to MAX_SIZE/2 */ + char *newb = + (buff->b == buff->space) /* still using static space? */ + ? luaM_reallocvector(buff->L, NULL, 0, newsize, char) + : luaM_reallocvector(buff->L, buff->b, buff->buffsize, newsize, + char); + if (newb == NULL) { /* allocation error? */ + buff->err = 1; /* signal a memory error */ + return; + } + if (buff->b == buff->space) /* new buffer (not reallocated)? */ + memcpy(newb, buff->b, buff->blen); /* copy previous content */ + buff->b = newb; /* set new (larger) buffer... */ + buff->buffsize = newsize; /* ...and its new size */ + } + } + memcpy(buff->b + buff->blen, str, slen); /* copy new content */ + buff->blen += slen; +} + + +/* +** Add a numeral to the buffer. +*/ +static void addnum2buff (BuffFS *buff, TValue *num) { + char numbuff[LUA_N2SBUFFSZ]; + unsigned len = luaO_tostringbuff(num, numbuff); + addstr2buff(buff, numbuff, len); +} + + +/* +** this function handles only '%d', '%c', '%f', '%p', '%s', and '%%' + conventional formats, plus Lua-specific '%I' and '%U' +*/ +const char *luaO_pushvfstring (lua_State *L, const char *fmt, va_list argp) { + BuffFS buff; /* holds last part of the result */ + const char *e; /* points to next '%' */ + initbuff(L, &buff); + while ((e = strchr(fmt, '%')) != NULL) { + addstr2buff(&buff, fmt, ct_diff2sz(e - fmt)); /* add 'fmt' up to '%' */ + switch (*(e + 1)) { /* conversion specifier */ + case 's': { /* zero-terminated string */ + const char *s = va_arg(argp, char *); + if (s == NULL) s = "(null)"; + addstr2buff(&buff, s, strlen(s)); + break; + } + case 'c': { /* an 'int' as a character */ + char c = cast_char(va_arg(argp, int)); + addstr2buff(&buff, &c, sizeof(char)); + break; + } + case 'd': { /* an 'int' */ + TValue num; + setivalue(&num, va_arg(argp, int)); + addnum2buff(&buff, &num); + break; + } + case 'I': { /* a 'lua_Integer' */ + TValue num; + setivalue(&num, cast_Integer(va_arg(argp, l_uacInt))); + addnum2buff(&buff, &num); + break; + } + case 'f': { /* a 'lua_Number' */ + TValue num; + setfltvalue(&num, cast_num(va_arg(argp, l_uacNumber))); + addnum2buff(&buff, &num); + break; + } + case 'p': { /* a pointer */ + char bf[LUA_N2SBUFFSZ]; /* enough space for '%p' */ + void *p = va_arg(argp, void *); + int len = lua_pointer2str(bf, LUA_N2SBUFFSZ, p); + addstr2buff(&buff, bf, cast_uint(len)); + break; + } + case 'U': { /* an 'unsigned long' as a UTF-8 sequence */ + char bf[UTF8BUFFSZ]; + unsigned long arg = va_arg(argp, unsigned long); + int len = luaO_utf8esc(bf, cast(l_uint32, arg)); + addstr2buff(&buff, bf + UTF8BUFFSZ - len, cast_uint(len)); + break; + } + case '%': { + addstr2buff(&buff, "%", 1); + break; + } + default: { + addstr2buff(&buff, e, 2); /* keep unknown format in the result */ + break; + } + } + fmt = e + 2; /* skip '%' and the specifier */ + } + addstr2buff(&buff, fmt, strlen(fmt)); /* rest of 'fmt' */ + return clearbuff(&buff); /* empty buffer into a new string */ +} + + +const char *luaO_pushfstring (lua_State *L, const char *fmt, ...) { + const char *msg; + va_list argp; + va_start(argp, fmt); + msg = luaO_pushvfstring(L, fmt, argp); + va_end(argp); + if (msg == NULL) /* error? */ + luaD_throw(L, LUA_ERRMEM); + return msg; +} + +/* }================================================================== */ + + +#define RETS "..." +#define PRE "[string \"" +#define POS "\"]" + +#define addstr(a,b,l) ( memcpy(a,b,(l) * sizeof(char)), a += (l) ) + +void luaO_chunkid (char *out, const char *source, size_t srclen) { + size_t bufflen = LUA_IDSIZE; /* free space in buffer */ + if (*source == '=') { /* 'literal' source */ + if (srclen <= bufflen) /* small enough? */ + memcpy(out, source + 1, srclen * sizeof(char)); + else { /* truncate it */ + addstr(out, source + 1, bufflen - 1); + *out = '\0'; + } + } + else if (*source == '@') { /* file name */ + if (srclen <= bufflen) /* small enough? */ + memcpy(out, source + 1, srclen * sizeof(char)); + else { /* add '...' before rest of name */ + addstr(out, RETS, LL(RETS)); + bufflen -= LL(RETS); + memcpy(out, source + 1 + srclen - bufflen, bufflen * sizeof(char)); + } + } + else { /* string; format as [string "source"] */ + const char *nl = strchr(source, '\n'); /* find first new line (if any) */ + addstr(out, PRE, LL(PRE)); /* add prefix */ + bufflen -= LL(PRE RETS POS) + 1; /* save space for prefix+suffix+'\0' */ + if (srclen < bufflen && nl == NULL) { /* small one-line source? */ + addstr(out, source, srclen); /* keep it */ + } + else { + if (nl != NULL) + srclen = ct_diff2sz(nl - source); /* stop at first newline */ + if (srclen > bufflen) srclen = bufflen; + addstr(out, source, srclen); + addstr(out, RETS, LL(RETS)); + } + memcpy(out, POS, (LL(POS) + 1) * sizeof(char)); + } +} diff --git a/crates/lua/vendor/lua/lobject.h b/crates/lua/vendor/lua/lobject.h new file mode 100644 index 0000000..e794041 --- /dev/null +++ b/crates/lua/vendor/lua/lobject.h @@ -0,0 +1,864 @@ +/* +** $Id: lobject.h $ +** Type definitions for Lua objects +** See Copyright Notice in lua.h +*/ + + +#ifndef lobject_h +#define lobject_h + + +#include + + +#include "llimits.h" +#include "lua.h" + + +/* +** Extra types for collectable non-values +*/ +#define LUA_TUPVAL LUA_NUMTYPES /* upvalues */ +#define LUA_TPROTO (LUA_NUMTYPES+1) /* function prototypes */ +#define LUA_TDEADKEY (LUA_NUMTYPES+2) /* removed keys in tables */ + + + +/* +** number of all possible types (including LUA_TNONE but excluding DEADKEY) +*/ +#define LUA_TOTALTYPES (LUA_TPROTO + 2) + + +/* +** tags for Tagged Values have the following use of bits: +** bits 0-3: actual tag (a LUA_T* constant) +** bits 4-5: variant bits +** bit 6: whether value is collectable +*/ + +/* add variant bits to a type */ +#define makevariant(t,v) ((t) | ((v) << 4)) + + + +/* +** Union of all Lua values +*/ +typedef union Value { + struct GCObject *gc; /* collectable objects */ + void *p; /* light userdata */ + lua_CFunction f; /* light C functions */ + lua_Integer i; /* integer numbers */ + lua_Number n; /* float numbers */ + /* not used, but may avoid warnings for uninitialized value */ + lu_byte ub; +} Value; + + +/* +** Tagged Values. This is the basic representation of values in Lua: +** an actual value plus a tag with its type. +*/ + +#define TValuefields Value value_; lu_byte tt_ + +typedef struct TValue { + TValuefields; +} TValue; + + +#define val_(o) ((o)->value_) +#define valraw(o) (val_(o)) + + +/* raw type tag of a TValue */ +#define rawtt(o) ((o)->tt_) + +/* tag with no variants (bits 0-3) */ +#define novariant(t) ((t) & 0x0F) + +/* type tag of a TValue (bits 0-3 for tags + variant bits 4-5) */ +#define withvariant(t) ((t) & 0x3F) +#define ttypetag(o) withvariant(rawtt(o)) + +/* type of a TValue */ +#define ttype(o) (novariant(rawtt(o))) + + +/* Macros to test type */ +#define checktag(o,t) (rawtt(o) == (t)) +#define checktype(o,t) (ttype(o) == (t)) + + +/* Macros for internal tests */ + +/* collectable object has the same tag as the original value */ +#define righttt(obj) (ttypetag(obj) == gcvalue(obj)->tt) + +/* +** Any value being manipulated by the program either is non +** collectable, or the collectable object has the right tag +** and it is not dead. The option 'L == NULL' allows other +** macros using this one to be used where L is not available. +*/ +#define checkliveness(L,obj) \ + ((void)L, lua_longassert(!iscollectable(obj) || \ + (righttt(obj) && (L == NULL || !isdead(G(L),gcvalue(obj)))))) + + +/* Macros to set values */ + +/* set a value's tag */ +#define settt_(o,t) ((o)->tt_=(t)) + + +/* main macro to copy values (from 'obj2' to 'obj1') */ +#define setobj(L,obj1,obj2) \ + { TValue *io1=(obj1); const TValue *io2=(obj2); \ + io1->value_ = io2->value_; settt_(io1, io2->tt_); \ + checkliveness(L,io1); lua_assert(!isnonstrictnil(io1)); } + +/* +** Different types of assignments, according to source and destination. +** (They are mostly equal now, but may be different in the future.) +*/ + +/* from stack to stack */ +#define setobjs2s(L,o1,o2) setobj(L,s2v(o1),s2v(o2)) +/* to stack (not from same stack) */ +#define setobj2s(L,o1,o2) setobj(L,s2v(o1),o2) +/* from table to same table */ +#define setobjt2t setobj +/* to new object */ +#define setobj2n setobj +/* to table */ +#define setobj2t setobj + + +/* +** Entries in a Lua stack. Field 'tbclist' forms a list of all +** to-be-closed variables active in this stack. Dummy entries are +** used when the distance between two tbc variables does not fit +** in an unsigned short. They are represented by delta==0, and +** their real delta is always the maximum value that fits in +** that field. +*/ +typedef union StackValue { + TValue val; + struct { + TValuefields; + unsigned short delta; + } tbclist; +} StackValue; + + +/* index to stack elements */ +typedef StackValue *StkId; + + +/* +** When reallocating the stack, change all pointers to the stack into +** proper offsets. +*/ +typedef union { + StkId p; /* actual pointer */ + ptrdiff_t offset; /* used while the stack is being reallocated */ +} StkIdRel; + + +/* convert a 'StackValue' to a 'TValue' */ +#define s2v(o) (&(o)->val) + + + +/* +** {================================================================== +** Nil +** =================================================================== +*/ + +/* Standard nil */ +#define LUA_VNIL makevariant(LUA_TNIL, 0) + +/* Empty slot (which might be different from a slot containing nil) */ +#define LUA_VEMPTY makevariant(LUA_TNIL, 1) + +/* Value returned for a key not found in a table (absent key) */ +#define LUA_VABSTKEY makevariant(LUA_TNIL, 2) + +/* Special variant to signal that a fast get is accessing a non-table */ +#define LUA_VNOTABLE makevariant(LUA_TNIL, 3) + + +/* macro to test for (any kind of) nil */ +#define ttisnil(v) checktype((v), LUA_TNIL) + +/* +** Macro to test the result of a table access. Formally, it should +** distinguish between LUA_VEMPTY/LUA_VABSTKEY/LUA_VNOTABLE and +** other tags. As currently nil is equivalent to LUA_VEMPTY, it is +** simpler to just test whether the value is nil. +*/ +#define tagisempty(tag) (novariant(tag) == LUA_TNIL) + + +/* macro to test for a standard nil */ +#define ttisstrictnil(o) checktag((o), LUA_VNIL) + + +#define setnilvalue(obj) settt_(obj, LUA_VNIL) +#define setnilvalue2s(stk) setnilvalue(s2v(stk)) + + +#define isabstkey(v) checktag((v), LUA_VABSTKEY) + + +/* +** macro to detect non-standard nils (used only in assertions) +*/ +#define isnonstrictnil(v) (ttisnil(v) && !ttisstrictnil(v)) + + +/* +** By default, entries with any kind of nil are considered empty. +** (In any definition, values associated with absent keys must also +** be accepted as empty.) +*/ +#define isempty(v) ttisnil(v) + + +/* macro defining a value corresponding to an absent key */ +#define ABSTKEYCONSTANT {NULL}, LUA_VABSTKEY + + +/* mark an entry as empty */ +#define setempty(v) settt_(v, LUA_VEMPTY) + + + +/* }================================================================== */ + + +/* +** {================================================================== +** Booleans +** =================================================================== +*/ + + +#define LUA_VFALSE makevariant(LUA_TBOOLEAN, 0) +#define LUA_VTRUE makevariant(LUA_TBOOLEAN, 1) + +#define ttisboolean(o) checktype((o), LUA_TBOOLEAN) +#define ttisfalse(o) checktag((o), LUA_VFALSE) +#define ttistrue(o) checktag((o), LUA_VTRUE) + + +#define l_isfalse(o) (ttisfalse(o) || ttisnil(o)) +#define tagisfalse(t) ((t) == LUA_VFALSE || novariant(t) == LUA_TNIL) + + + +#define setbfvalue(obj) settt_(obj, LUA_VFALSE) +#define setbtvalue(obj) settt_(obj, LUA_VTRUE) + +/* }================================================================== */ + + +/* +** {================================================================== +** Threads +** =================================================================== +*/ + +#define LUA_VTHREAD makevariant(LUA_TTHREAD, 0) + +#define ttisthread(o) checktag((o), ctb(LUA_VTHREAD)) + +#define thvalue(o) check_exp(ttisthread(o), gco2th(val_(o).gc)) + +#define setthvalue(L,obj,x) \ + { TValue *io = (obj); lua_State *x_ = (x); \ + val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_VTHREAD)); \ + checkliveness(L,io); } + +#define setthvalue2s(L,o,t) setthvalue(L,s2v(o),t) + +/* }================================================================== */ + + +/* +** {================================================================== +** Collectable Objects +** =================================================================== +*/ + +/* +** Common Header for all collectable objects (in macro form, to be +** included in other objects) +*/ +#define CommonHeader struct GCObject *next; lu_byte tt; lu_byte marked + + +/* Common type for all collectable objects */ +typedef struct GCObject { + CommonHeader; +} GCObject; + + +/* Bit mark for collectable types */ +#define BIT_ISCOLLECTABLE (1 << 6) + +#define iscollectable(o) (rawtt(o) & BIT_ISCOLLECTABLE) + +/* mark a tag as collectable */ +#define ctb(t) ((t) | BIT_ISCOLLECTABLE) + +#define gcvalue(o) check_exp(iscollectable(o), val_(o).gc) + +#define gcvalueraw(v) ((v).gc) + +#define setgcovalue(L,obj,x) \ + { TValue *io = (obj); GCObject *i_g=(x); \ + val_(io).gc = i_g; settt_(io, ctb(i_g->tt)); } + +/* }================================================================== */ + + +/* +** {================================================================== +** Numbers +** =================================================================== +*/ + +/* Variant tags for numbers */ +#define LUA_VNUMINT makevariant(LUA_TNUMBER, 0) /* integer numbers */ +#define LUA_VNUMFLT makevariant(LUA_TNUMBER, 1) /* float numbers */ + +#define ttisnumber(o) checktype((o), LUA_TNUMBER) +#define ttisfloat(o) checktag((o), LUA_VNUMFLT) +#define ttisinteger(o) checktag((o), LUA_VNUMINT) + +#define nvalue(o) check_exp(ttisnumber(o), \ + (ttisinteger(o) ? cast_num(ivalue(o)) : fltvalue(o))) +#define fltvalue(o) check_exp(ttisfloat(o), val_(o).n) +#define ivalue(o) check_exp(ttisinteger(o), val_(o).i) + +#define fltvalueraw(v) ((v).n) +#define ivalueraw(v) ((v).i) + +#define setfltvalue(obj,x) \ + { TValue *io=(obj); val_(io).n=(x); settt_(io, LUA_VNUMFLT); } + +#define chgfltvalue(obj,x) \ + { TValue *io=(obj); lua_assert(ttisfloat(io)); val_(io).n=(x); } + +#define setivalue(obj,x) \ + { TValue *io=(obj); val_(io).i=(x); settt_(io, LUA_VNUMINT); } + +#define chgivalue(obj,x) \ + { TValue *io=(obj); lua_assert(ttisinteger(io)); val_(io).i=(x); } + +/* }================================================================== */ + + +/* +** {================================================================== +** Strings +** =================================================================== +*/ + +/* Variant tags for strings */ +#define LUA_VSHRSTR makevariant(LUA_TSTRING, 0) /* short strings */ +#define LUA_VLNGSTR makevariant(LUA_TSTRING, 1) /* long strings */ + +#define ttisstring(o) checktype((o), LUA_TSTRING) +#define ttisshrstring(o) checktag((o), ctb(LUA_VSHRSTR)) +#define ttislngstring(o) checktag((o), ctb(LUA_VLNGSTR)) + +#define tsvalueraw(v) (gco2ts((v).gc)) + +#define tsvalue(o) check_exp(ttisstring(o), gco2ts(val_(o).gc)) + +#define setsvalue(L,obj,x) \ + { TValue *io = (obj); TString *x_ = (x); \ + val_(io).gc = obj2gco(x_); settt_(io, ctb(x_->tt)); \ + checkliveness(L,io); } + +/* set a string to the stack */ +#define setsvalue2s(L,o,s) setsvalue(L,s2v(o),s) + +/* set a string to a new object */ +#define setsvalue2n setsvalue + + +/* Kinds of long strings (stored in 'shrlen') */ +#define LSTRREG -1 /* regular long string */ +#define LSTRFIX -2 /* fixed external long string */ +#define LSTRMEM -3 /* external long string with deallocation */ + + +/* +** Header for a string value. +*/ +typedef struct TString { + CommonHeader; + lu_byte extra; /* reserved words for short strings; "has hash" for longs */ + ls_byte shrlen; /* length for short strings, negative for long strings */ + unsigned int hash; + union { + size_t lnglen; /* length for long strings */ + struct TString *hnext; /* linked list for hash table */ + } u; + char *contents; /* pointer to content in long strings */ + lua_Alloc falloc; /* deallocation function for external strings */ + void *ud; /* user data for external strings */ +} TString; + + +#define strisshr(ts) ((ts)->shrlen >= 0) +#define isextstr(ts) (ttislngstring(ts) && tsvalue(ts)->shrlen != LSTRREG) + + +/* +** Get the actual string (array of bytes) from a 'TString'. (Generic +** version and specialized versions for long and short strings.) +*/ +#define rawgetshrstr(ts) (cast_charp(&(ts)->contents)) +#define getshrstr(ts) check_exp(strisshr(ts), rawgetshrstr(ts)) +#define getlngstr(ts) check_exp(!strisshr(ts), (ts)->contents) +#define getstr(ts) (strisshr(ts) ? rawgetshrstr(ts) : (ts)->contents) + + +/* get string length from 'TString *ts' */ +#define tsslen(ts) \ + (strisshr(ts) ? cast_sizet((ts)->shrlen) : (ts)->u.lnglen) + +/* +** Get string and length */ +#define getlstr(ts, len) \ + (strisshr(ts) \ + ? (cast_void((len) = cast_sizet((ts)->shrlen)), rawgetshrstr(ts)) \ + : (cast_void((len) = (ts)->u.lnglen), (ts)->contents)) + +/* }================================================================== */ + + +/* +** {================================================================== +** Userdata +** =================================================================== +*/ + + +/* +** Light userdata should be a variant of userdata, but for compatibility +** reasons they are also different types. +*/ +#define LUA_VLIGHTUSERDATA makevariant(LUA_TLIGHTUSERDATA, 0) + +#define LUA_VUSERDATA makevariant(LUA_TUSERDATA, 0) + +#define ttislightuserdata(o) checktag((o), LUA_VLIGHTUSERDATA) +#define ttisfulluserdata(o) checktag((o), ctb(LUA_VUSERDATA)) + +#define pvalue(o) check_exp(ttislightuserdata(o), val_(o).p) +#define uvalue(o) check_exp(ttisfulluserdata(o), gco2u(val_(o).gc)) + +#define pvalueraw(v) ((v).p) + +#define setpvalue(obj,x) \ + { TValue *io=(obj); val_(io).p=(x); settt_(io, LUA_VLIGHTUSERDATA); } + +#define setuvalue(L,obj,x) \ + { TValue *io = (obj); Udata *x_ = (x); \ + val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_VUSERDATA)); \ + checkliveness(L,io); } + + +/* Ensures that addresses after this type are always fully aligned. */ +typedef union UValue { + TValue uv; + LUAI_MAXALIGN; /* ensures maximum alignment for udata bytes */ +} UValue; + + +/* +** Header for userdata with user values; +** memory area follows the end of this structure. +*/ +typedef struct Udata { + CommonHeader; + unsigned short nuvalue; /* number of user values */ + size_t len; /* number of bytes */ + struct Table *metatable; + GCObject *gclist; + UValue uv[1]; /* user values */ +} Udata; + + +/* +** Header for userdata with no user values. These userdata do not need +** to be gray during GC, and therefore do not need a 'gclist' field. +** To simplify, the code always use 'Udata' for both kinds of userdata, +** making sure it never accesses 'gclist' on userdata with no user values. +** This structure here is used only to compute the correct size for +** this representation. (The 'bindata' field in its end ensures correct +** alignment for binary data following this header.) +*/ +typedef struct Udata0 { + CommonHeader; + unsigned short nuvalue; /* number of user values */ + size_t len; /* number of bytes */ + struct Table *metatable; + union {LUAI_MAXALIGN;} bindata; +} Udata0; + + +/* compute the offset of the memory area of a userdata */ +#define udatamemoffset(nuv) \ + ((nuv) == 0 ? offsetof(Udata0, bindata) \ + : offsetof(Udata, uv) + (sizeof(UValue) * (nuv))) + +/* get the address of the memory block inside 'Udata' */ +#define getudatamem(u) (cast_charp(u) + udatamemoffset((u)->nuvalue)) + +/* compute the size of a userdata */ +#define sizeudata(nuv,nb) (udatamemoffset(nuv) + (nb)) + +/* }================================================================== */ + + +/* +** {================================================================== +** Prototypes +** =================================================================== +*/ + +#define LUA_VPROTO makevariant(LUA_TPROTO, 0) + + +typedef l_uint32 Instruction; + + +/* +** Description of an upvalue for function prototypes +*/ +typedef struct Upvaldesc { + TString *name; /* upvalue name (for debug information) */ + lu_byte instack; /* whether it is in stack (register) */ + lu_byte idx; /* index of upvalue (in stack or in outer function's list) */ + lu_byte kind; /* kind of corresponding variable */ +} Upvaldesc; + + +/* +** Description of a local variable for function prototypes +** (used for debug information) +*/ +typedef struct LocVar { + TString *varname; + int startpc; /* first point where variable is active */ + int endpc; /* first point where variable is dead */ +} LocVar; + + +/* +** Associates the absolute line source for a given instruction ('pc'). +** The array 'lineinfo' gives, for each instruction, the difference in +** lines from the previous instruction. When that difference does not +** fit into a byte, Lua saves the absolute line for that instruction. +** (Lua also saves the absolute line periodically, to speed up the +** computation of a line number: we can use binary search in the +** absolute-line array, but we must traverse the 'lineinfo' array +** linearly to compute a line.) +*/ +typedef struct AbsLineInfo { + int pc; + int line; +} AbsLineInfo; + + +/* +** Flags in Prototypes +*/ +#define PF_VAHID 1 /* function has hidden vararg arguments */ +#define PF_VATAB 2 /* function has vararg table */ +#define PF_FIXED 4 /* prototype has parts in fixed memory */ + +/* a vararg function either has hidden args. or a vararg table */ +#define isvararg(p) ((p)->flag & (PF_VAHID | PF_VATAB)) + +/* +** mark that a function needs a vararg table. (The flag PF_VAHID will +** be cleared later.) +*/ +#define needvatab(p) ((p)->flag |= PF_VATAB) + +/* +** Function Prototypes +*/ +typedef struct Proto { + CommonHeader; + lu_byte numparams; /* number of fixed (named) parameters */ + lu_byte flag; + lu_byte maxstacksize; /* number of registers needed by this function */ + int sizeupvalues; /* size of 'upvalues' */ + int sizek; /* size of 'k' */ + int sizecode; + int sizelineinfo; + int sizep; /* size of 'p' */ + int sizelocvars; + int sizeabslineinfo; /* size of 'abslineinfo' */ + int linedefined; /* debug information */ + int lastlinedefined; /* debug information */ + TValue *k; /* constants used by the function */ + Instruction *code; /* opcodes */ + struct Proto **p; /* functions defined inside the function */ + Upvaldesc *upvalues; /* upvalue information */ + ls_byte *lineinfo; /* information about source lines (debug information) */ + AbsLineInfo *abslineinfo; /* idem */ + LocVar *locvars; /* information about local variables (debug information) */ + TString *source; /* used for debug information */ + GCObject *gclist; +} Proto; + +/* }================================================================== */ + + +/* +** {================================================================== +** Functions +** =================================================================== +*/ + +#define LUA_VUPVAL makevariant(LUA_TUPVAL, 0) + + +/* Variant tags for functions */ +#define LUA_VLCL makevariant(LUA_TFUNCTION, 0) /* Lua closure */ +#define LUA_VLCF makevariant(LUA_TFUNCTION, 1) /* light C function */ +#define LUA_VCCL makevariant(LUA_TFUNCTION, 2) /* C closure */ + +#define ttisfunction(o) checktype(o, LUA_TFUNCTION) +#define ttisLclosure(o) checktag((o), ctb(LUA_VLCL)) +#define ttislcf(o) checktag((o), LUA_VLCF) +#define ttisCclosure(o) checktag((o), ctb(LUA_VCCL)) +#define ttisclosure(o) (ttisLclosure(o) || ttisCclosure(o)) + + +#define isLfunction(o) ttisLclosure(o) + +#define clvalue(o) check_exp(ttisclosure(o), gco2cl(val_(o).gc)) +#define clLvalue(o) check_exp(ttisLclosure(o), gco2lcl(val_(o).gc)) +#define fvalue(o) check_exp(ttislcf(o), val_(o).f) +#define clCvalue(o) check_exp(ttisCclosure(o), gco2ccl(val_(o).gc)) + +#define fvalueraw(v) ((v).f) + +#define setclLvalue(L,obj,x) \ + { TValue *io = (obj); LClosure *x_ = (x); \ + val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_VLCL)); \ + checkliveness(L,io); } + +#define setclLvalue2s(L,o,cl) setclLvalue(L,s2v(o),cl) + +#define setfvalue(obj,x) \ + { TValue *io=(obj); val_(io).f=(x); settt_(io, LUA_VLCF); } + +#define setclCvalue(L,obj,x) \ + { TValue *io = (obj); CClosure *x_ = (x); \ + val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_VCCL)); \ + checkliveness(L,io); } + + +/* +** Upvalues for Lua closures +*/ +typedef struct UpVal { + CommonHeader; + union { + TValue *p; /* points to stack or to its own value */ + ptrdiff_t offset; /* used while the stack is being reallocated */ + } v; + union { + struct { /* (when open) */ + struct UpVal *next; /* linked list */ + struct UpVal **previous; + } open; + TValue value; /* the value (when closed) */ + } u; +} UpVal; + + + +#define ClosureHeader \ + CommonHeader; lu_byte nupvalues; GCObject *gclist + +typedef struct CClosure { + ClosureHeader; + lua_CFunction f; + TValue upvalue[1]; /* list of upvalues */ +} CClosure; + + +typedef struct LClosure { + ClosureHeader; + struct Proto *p; + UpVal *upvals[1]; /* list of upvalues */ +} LClosure; + + +typedef union Closure { + CClosure c; + LClosure l; +} Closure; + + +#define getproto(o) (clLvalue(o)->p) + +/* }================================================================== */ + + +/* +** {================================================================== +** Tables +** =================================================================== +*/ + +#define LUA_VTABLE makevariant(LUA_TTABLE, 0) + +#define ttistable(o) checktag((o), ctb(LUA_VTABLE)) + +#define hvalue(o) check_exp(ttistable(o), gco2t(val_(o).gc)) + +#define sethvalue(L,obj,x) \ + { TValue *io = (obj); Table *x_ = (x); \ + val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_VTABLE)); \ + checkliveness(L,io); } + +#define sethvalue2s(L,o,h) sethvalue(L,s2v(o),h) + + +/* +** Nodes for Hash tables: A pack of two TValue's (key-value pairs) +** plus a 'next' field to link colliding entries. The distribution +** of the key's fields ('key_tt' and 'key_val') not forming a proper +** 'TValue' allows for a smaller size for 'Node' both in 4-byte +** and 8-byte alignments. +*/ +typedef union Node { + struct NodeKey { + TValuefields; /* fields for value */ + lu_byte key_tt; /* key type */ + int next; /* for chaining */ + Value key_val; /* key value */ + } u; + TValue i_val; /* direct access to node's value as a proper 'TValue' */ +} Node; + + +/* copy a value into a key */ +#define setnodekey(node,obj) \ + { Node *n_=(node); const TValue *io_=(obj); \ + n_->u.key_val = io_->value_; n_->u.key_tt = io_->tt_; } + + +/* copy a value from a key */ +#define getnodekey(L,obj,node) \ + { TValue *io_=(obj); const Node *n_=(node); \ + io_->value_ = n_->u.key_val; io_->tt_ = n_->u.key_tt; \ + checkliveness(L,io_); } + + + +typedef struct Table { + CommonHeader; + lu_byte flags; /* 1<

u.key_tt) +#define keyval(node) ((node)->u.key_val) + +#define keyisnil(node) (keytt(node) == LUA_TNIL) +#define keyisinteger(node) (keytt(node) == LUA_VNUMINT) +#define keyival(node) (keyval(node).i) +#define keyisshrstr(node) (keytt(node) == ctb(LUA_VSHRSTR)) +#define keystrval(node) (gco2ts(keyval(node).gc)) + +#define setnilkey(node) (keytt(node) = LUA_TNIL) + +#define keyiscollectable(n) (keytt(n) & BIT_ISCOLLECTABLE) + +#define gckey(n) (keyval(n).gc) +#define gckeyN(n) (keyiscollectable(n) ? gckey(n) : NULL) + + +/* +** Dead keys in tables have the tag DEADKEY but keep their original +** gcvalue. This distinguishes them from regular keys but allows them to +** be found when searched in a special way. ('next' needs that to find +** keys removed from a table during a traversal.) +*/ +#define setdeadkey(node) (keytt(node) = LUA_TDEADKEY) +#define keyisdead(node) (keytt(node) == LUA_TDEADKEY) + +/* }================================================================== */ + + + +/* +** 'module' operation for hashing (size is always a power of 2) +*/ +#define lmod(s,size) \ + (check_exp((size&(size-1))==0, (cast_uint(s) & cast_uint((size)-1)))) + + +#define twoto(x) (1u<<(x)) +#define sizenode(t) (twoto((t)->lsizenode)) + + +/* size of buffer for 'luaO_utf8esc' function */ +#define UTF8BUFFSZ 8 + + +/* macro to call 'luaO_pushvfstring' correctly */ +#define pushvfstring(L, argp, fmt, msg) \ + { va_start(argp, fmt); \ + msg = luaO_pushvfstring(L, fmt, argp); \ + va_end(argp); \ + if (msg == NULL) luaD_throw(L, LUA_ERRMEM); /* only after 'va_end' */ } + + +LUAI_FUNC int luaO_utf8esc (char *buff, l_uint32 x); +LUAI_FUNC lu_byte luaO_ceillog2 (unsigned int x); +LUAI_FUNC lu_byte luaO_codeparam (unsigned int p); +LUAI_FUNC l_mem luaO_applyparam (lu_byte p, l_mem x); + +LUAI_FUNC int luaO_rawarith (lua_State *L, int op, const TValue *p1, + const TValue *p2, TValue *res); +LUAI_FUNC void luaO_arith (lua_State *L, int op, const TValue *p1, + const TValue *p2, StkId res); +LUAI_FUNC size_t luaO_str2num (const char *s, TValue *o); +LUAI_FUNC unsigned luaO_tostringbuff (const TValue *obj, char *buff); +LUAI_FUNC lu_byte luaO_hexavalue (int c); +LUAI_FUNC void luaO_tostring (lua_State *L, TValue *obj); +LUAI_FUNC const char *luaO_pushvfstring (lua_State *L, const char *fmt, + va_list argp); +LUAI_FUNC const char *luaO_pushfstring (lua_State *L, const char *fmt, ...); +LUAI_FUNC void luaO_chunkid (char *out, const char *source, size_t srclen); + + +#endif diff --git a/crates/lua/vendor/lua/lopcodes.c b/crates/lua/vendor/lua/lopcodes.c new file mode 100644 index 0000000..aaa6dca --- /dev/null +++ b/crates/lua/vendor/lua/lopcodes.c @@ -0,0 +1,130 @@ +/* +** $Id: lopcodes.c $ +** Opcodes for Lua virtual machine +** See Copyright Notice in lua.h +*/ + +#define lopcodes_c +#define LUA_CORE + +#include "lprefix.h" + + +#include "lopcodes.h" + + +#define opmode(mm,ot,it,t,a,m) \ + (((mm) << 7) | ((ot) << 6) | ((it) << 5) | ((t) << 4) | ((a) << 3) | (m)) + + +/* ORDER OP */ + +LUAI_DDEF const lu_byte luaP_opmodes[NUM_OPCODES] = { +/* MM OT IT T A mode opcode */ + 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, ivABC) /* 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_SHLI */ + ,opmode(0, 0, 0, 0, 1, iABC) /* OP_SHRI */ + ,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, ivABC) /* OP_SETLIST */ + ,opmode(0, 0, 0, 0, 1, iABx) /* OP_CLOSURE */ + ,opmode(0, 1, 0, 0, 1, iABC) /* OP_VARARG */ + ,opmode(0, 0, 0, 0, 1, iABC) /* OP_GETVARG */ + ,opmode(0, 0, 0, 0, 0, iABx) /* OP_ERRNNIL */ + ,opmode(0, 0, 0, 0, 0, iABC) /* OP_VARARGPREP */ + ,opmode(0, 0, 0, 0, 0, iAx) /* OP_EXTRAARG */ +}; + + +#define testITMode(m) (luaP_opmodes[m] & (1 << 5)) + + +/* +** Check whether instruction uses top. That happens for OP_VARARGPREP +** and for instructions that use multiple values set by the previous +** instruction. +*/ +int luaP_isIT (Instruction i) { + OpCode op = GET_OPCODE(i); + switch (op) { + case OP_SETLIST: + return GETARG_vB(i) == 0; + case OP_VARARGPREP: + return 1; + default: + return testITMode(GET_OPCODE(i)) && GETARG_B(i) == 0; + } +} diff --git a/crates/lua/vendor/lua/lopcodes.h b/crates/lua/vendor/lua/lopcodes.h new file mode 100644 index 0000000..86cff06 --- /dev/null +++ b/crates/lua/vendor/lua/lopcodes.h @@ -0,0 +1,444 @@ +/* +** $Id: lopcodes.h $ +** Opcodes for Lua virtual machine +** See Copyright Notice in lua.h +*/ + +#ifndef lopcodes_h +#define lopcodes_h + +#include "llimits.h" +#include "lobject.h" + + +/*=========================================================================== + We assume that instructions are unsigned 32-bit integers. + All instructions have an opcode in the first 7 bits. + Instructions can have the following formats: + + 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 + 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 +iABC C(8) | B(8) |k| A(8) | Op(7) | +ivABC vC(10) | vB(6) |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) | + + ('v' stands for "variant", 's' for "signed", 'x' for "extended".) + A signed argument is represented in excess K: The represented value is + the written unsigned value minus K, where K is half (rounded down) the + maximum value for the corresponding unsigned argument. +===========================================================================*/ + + +/* basic instruction formats */ +enum OpMode {iABC, ivABC, iABx, iAsBx, iAx, isJ}; + + +/* +** size and position of opcode arguments. +*/ +#define SIZE_C 8 +#define SIZE_vC 10 +#define SIZE_B 8 +#define SIZE_vB 6 +#define SIZE_Bx (SIZE_C + SIZE_B + 1) +#define SIZE_A 8 +#define SIZE_Ax (SIZE_Bx + SIZE_A) +#define SIZE_sJ (SIZE_Bx + SIZE_A) + +#define SIZE_OP 7 + +#define POS_OP 0 + +#define POS_A (POS_OP + SIZE_OP) +#define POS_k (POS_A + SIZE_A) +#define POS_B (POS_k + 1) +#define POS_vB (POS_k + 1) +#define POS_C (POS_B + SIZE_B) +#define POS_vC (POS_vB + SIZE_vB) + +#define POS_Bx POS_k + +#define POS_Ax POS_A + +#define POS_sJ POS_A + + +/* +** limits for opcode arguments. +** we use (signed) 'int' to manipulate most arguments, +** so they must fit in ints. +*/ + +/* +** Check whether type 'int' has at least 'b' + 1 bits. +** 'b' < 32; +1 for the sign bit. +*/ +#define L_INTHASBITS(b) ((UINT_MAX >> (b)) >= 1) + + +#if L_INTHASBITS(SIZE_Bx) +#define MAXARG_Bx ((1<>1) /* 'sBx' is signed */ + + +#if L_INTHASBITS(SIZE_Ax) +#define MAXARG_Ax ((1<> 1) + + +#define MAXARG_A ((1<> 1) + +#define int2sC(i) ((i) + OFFSET_sC) +#define sC2int(i) ((i) - OFFSET_sC) + + +/* creates a mask with 'n' 1 bits at position 'p' */ +#define MASK1(n,p) ((~((~(Instruction)0)<<(n)))<<(p)) + +/* creates a mask with 'n' 0 bits at position 'p' */ +#define MASK0(n,p) (~MASK1(n,p)) + +/* +** the following macros help to manipulate instructions +*/ + +#define GET_OPCODE(i) (cast(OpCode, ((i)>>POS_OP) & MASK1(SIZE_OP,0))) +#define SET_OPCODE(i,o) ((i) = (((i)&MASK0(SIZE_OP,POS_OP)) | \ + ((cast_Inst(o)<>(pos)) & MASK1(size,0))) +#define setarg(i,v,pos,size) ((i) = (((i)&MASK0(size,pos)) | \ + ((cast_Inst(v)<> sC */ + +OP_ADD,/* A B C R[A] := R[B] + R[C] */ +OP_SUB,/* A B C R[A] := R[B] - R[C] */ +OP_MUL,/* A B C R[A] := R[B] * R[C] */ +OP_MOD,/* A B C R[A] := R[B] % R[C] */ +OP_POW,/* A B C R[A] := R[B] ^ R[C] */ +OP_DIV,/* A B C R[A] := R[B] / R[C] */ +OP_IDIV,/* A B C R[A] := R[B] // R[C] */ + +OP_BAND,/* A B C R[A] := R[B] & R[C] */ +OP_BOR,/* A B C R[A] := R[B] | R[C] */ +OP_BXOR,/* A B C R[A] := R[B] ~ R[C] */ +OP_SHL,/* A B C R[A] := R[B] << R[C] */ +OP_SHR,/* A B C R[A] := R[B] >> R[C] */ + +OP_MMBIN,/* A B C call C metamethod over R[A] and R[B] */ +OP_MMBINI,/* A sB C k call C metamethod over R[A] and sB */ +OP_MMBINK,/* A B C k call C metamethod over R[A] and K[B] */ + +OP_UNM,/* A B R[A] := -R[B] */ +OP_BNOT,/* A B R[A] := ~R[B] */ +OP_NOT,/* A B R[A] := not R[B] */ +OP_LEN,/* A B R[A] := #R[B] (length operator) */ + +OP_CONCAT,/* A B R[A] := R[A].. ... ..R[A + B - 1] */ + +OP_CLOSE,/* A close all upvalues >= R[A] */ +OP_TBC,/* A mark variable A "to be closed" */ +OP_JMP,/* sJ pc += sJ */ +OP_EQ,/* A B k if ((R[A] == R[B]) ~= k) then pc++ */ +OP_LT,/* A B k if ((R[A] < R[B]) ~= k) then pc++ */ +OP_LE,/* A B k if ((R[A] <= R[B]) ~= k) then pc++ */ + +OP_EQK,/* A B k if ((R[A] == K[B]) ~= k) then pc++ */ +OP_EQI,/* A sB k if ((R[A] == sB) ~= k) then pc++ */ +OP_LTI,/* A sB k if ((R[A] < sB) ~= k) then pc++ */ +OP_LEI,/* A sB k if ((R[A] <= sB) ~= k) then pc++ */ +OP_GTI,/* A sB k if ((R[A] > sB) ~= k) then pc++ */ +OP_GEI,/* A sB k if ((R[A] >= sB) ~= k) then pc++ */ + +OP_TEST,/* A k if (not R[A] == k) then pc++ */ +OP_TESTSET,/* A B k if (not R[B] == k) then pc++ else R[A] := R[B] */ + +OP_CALL,/* A B C R[A], ... ,R[A+C-2] := R[A](R[A+1], ... ,R[A+B-1]) */ +OP_TAILCALL,/* A B C k return R[A](R[A+1], ... ,R[A+B-1]) */ + +OP_RETURN,/* A B C k return R[A], ... ,R[A+B-2] */ +OP_RETURN0,/* return */ +OP_RETURN1,/* A return R[A] */ + +OP_FORLOOP,/* A Bx update counters; if loop continues then pc-=Bx; */ +OP_FORPREP,/* A Bx ; + if not to run then pc+=Bx+1; */ + +OP_TFORPREP,/* A Bx create upvalue for R[A + 3]; pc+=Bx */ +OP_TFORCALL,/* A C R[A+4], ... ,R[A+3+C] := R[A](R[A+1], R[A+2]); */ +OP_TFORLOOP,/* A Bx if R[A+2] ~= nil then { R[A]=R[A+2]; pc -= Bx } */ + +OP_SETLIST,/* A vB vC k R[A][vC+i] := R[A+i], 1 <= i <= vB */ + +OP_CLOSURE,/* A Bx R[A] := closure(KPROTO[Bx]) */ + +OP_VARARG,/* A B C k R[A], ..., R[A+C-2] = varargs */ + +OP_GETVARG, /* A B C R[A] := R[B][R[C]], R[B] is vararg parameter */ + +OP_ERRNNIL,/* A Bx raise error if R[A] ~= nil (K[Bx - 1] is global name)*/ + +OP_VARARGPREP,/* (adjust varargs) */ + +OP_EXTRAARG/* Ax extra (larger) argument for previous opcode */ +} OpCode; + + +#define NUM_OPCODES ((int)(OP_EXTRAARG) + 1) + + + +/*=========================================================================== + Notes: + + (*) Opcode OP_LFALSESKIP is used to convert a condition to a boolean + value, in a code equivalent to (not cond ? false : true). (It + produces false and skips the next instruction producing true.) + + (*) Opcodes OP_MMBIN and variants follow each arithmetic and + bitwise opcode. If the operation succeeds, it skips this next + opcode. Otherwise, this opcode calls the corresponding metamethod. + + (*) Opcode OP_TESTSET is used in short-circuit expressions that need + both to jump and to produce a value, such as (a = b or c). + + (*) In OP_CALL, if (B == 0) then B = top - A. If (C == 0), then + 'top' is set to last_result+1, so next open instruction (OP_CALL, + OP_RETURN*, OP_SETLIST) may use 'top'. + + (*) In OP_VARARG, if (C == 0) then use actual number of varargs and + set top (like in OP_CALL with C == 0). 'k' means function has a + vararg table, which is in R[B]. + + (*) In OP_RETURN, if (B == 0) then return up to 'top'. + + (*) In OP_LOADKX and OP_NEWTABLE, the next instruction is always + OP_EXTRAARG. + + (*) In OP_SETLIST, if (B == 0) then real B = 'top'; if k, then + real C = EXTRAARG _ C (the bits of EXTRAARG concatenated with the + bits of C). + + (*) In OP_NEWTABLE, vB is log2 of the hash size (which is always a + power of 2) plus 1, or zero for size zero. If not k, the array size + is vC. Otherwise, the array size is EXTRAARG _ vC. + + (*) In OP_ERRNNIL, (Bx == 0) means index of global name doesn't + fit in Bx. (So, that name is not available for the error message.) + + (*) For comparisons, k specifies what condition the test should accept + (true or false). + + (*) In OP_MMBINI/OP_MMBINK, k means the arguments were flipped + (the constant is the first operand). + + (*) All comparison and test instructions assume that the instruction + being skipped (pc++) is a jump. + + (*) In instructions OP_RETURN/OP_TAILCALL, 'k' specifies that the + function builds upvalues, which may need to be closed. C > 0 means + the function has hidden vararg arguments, so that its 'func' must be + corrected before returning; in this case, (C - 1) is its number of + fixed parameters. + + (*) In comparisons with an immediate operand, C signals whether the + original operand was a float. (It must be corrected in case of + metamethods.) + +===========================================================================*/ + + +/* +** masks for instruction properties. The format is: +** bits 0-2: op mode +** bit 3: instruction set register A +** bit 4: operator is a test (next instruction must be a jump) +** bit 5: used by 'luaP_isIT' +** bit 6: used by 'luaP_isOT' +** bit 7: instruction is an MM instruction (call a metamethod) +*/ + +LUAI_DDEC(const lu_byte luaP_opmodes[NUM_OPCODES];) + +#define getOpMode(m) (cast(enum OpMode, luaP_opmodes[m] & 7)) +#define testAMode(m) (luaP_opmodes[m] & (1 << 3)) +#define testTMode(m) (luaP_opmodes[m] & (1 << 4)) +#define testMMMode(m) (luaP_opmodes[m] & (1 << 7)) + + +/* Check whether instruction sets top for next instruction, that is, +** it results in multiple values. Used only for tests. +*/ +#define luaP_isOT(i) \ + (GET_OPCODE(i) == OP_TAILCALL || \ + ((luaP_opmodes[GET_OPCODE(i)] & (1 << 6)) && GETARG_C(i) == 0)) + + +LUAI_FUNC int luaP_isIT (Instruction i); + + +#endif diff --git a/crates/lua/vendor/lua/lopnames.h b/crates/lua/vendor/lua/lopnames.h new file mode 100644 index 0000000..0614c27 --- /dev/null +++ b/crates/lua/vendor/lua/lopnames.h @@ -0,0 +1,104 @@ +/* +** $Id: lopnames.h $ +** Opcode names +** See Copyright Notice in lua.h +*/ + +#if !defined(lopnames_h) +#define lopnames_h + +#include + + +/* ORDER OP */ + +static const char *const opnames[] = { + "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", + "SHLI", + "SHRI", + "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", + "GETVARG", + "ERRNNIL", + "VARARGPREP", + "EXTRAARG", + NULL +}; + +#endif diff --git a/crates/lua/vendor/lua/lparser.c b/crates/lua/vendor/lua/lparser.c new file mode 100644 index 0000000..3c20dd2 --- /dev/null +++ b/crates/lua/vendor/lua/lparser.c @@ -0,0 +1,2192 @@ +/* +** $Id: lparser.c $ +** Lua Parser +** See Copyright Notice in lua.h +*/ + +#define lparser_c +#define LUA_CORE + +#include "lprefix.h" + + +#include +#include + +#include "lua.h" + +#include "lcode.h" +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "llex.h" +#include "lmem.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lparser.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" + + + +/* maximum number of variable declarations per function (must be + smaller than 250, due to the bytecode format) */ +#define MAXVARS 200 + + +#define hasmultret(k) ((k) == VCALL || (k) == VVARARG) + + +/* because all strings are unified by the scanner, the parser + can use pointer equality for string equality */ +#define eqstr(a,b) ((a) == (b)) + + +/* +** nodes for block list (list of active blocks) +*/ +typedef struct BlockCnt { + struct BlockCnt *previous; /* chain */ + int firstlabel; /* index of first label in this block */ + int firstgoto; /* index of first pending goto in this block */ + short nactvar; /* number of active declarations at block entry */ + lu_byte upval; /* true if some variable in the block is an upvalue */ + lu_byte isloop; /* 1 if 'block' is a loop; 2 if it has pending breaks */ + lu_byte insidetbc; /* true if inside the scope of a to-be-closed var. */ +} BlockCnt; + + + +/* +** prototypes for recursive non-terminal functions +*/ +static void statement (LexState *ls); +static void expr (LexState *ls, expdesc *v); + + +static l_noret error_expected (LexState *ls, int token) { + luaX_syntaxerror(ls, + luaO_pushfstring(ls->L, "%s expected", luaX_token2str(ls, token))); +} + + +static l_noret errorlimit (FuncState *fs, int limit, const char *what) { + lua_State *L = fs->ls->L; + const char *msg; + int line = fs->f->linedefined; + const char *where = (line == 0) + ? "main function" + : luaO_pushfstring(L, "function at line %d", line); + msg = luaO_pushfstring(L, "too many %s (limit is %d) in %s", + what, limit, where); + luaX_syntaxerror(fs->ls, msg); +} + + +void luaY_checklimit (FuncState *fs, int v, int l, const char *what) { + if (l_unlikely(v > l)) errorlimit(fs, l, what); +} + + +/* +** Test whether next token is 'c'; if so, skip it. +*/ +static int testnext (LexState *ls, int c) { + if (ls->t.token == c) { + luaX_next(ls); + return 1; + } + else return 0; +} + + +/* +** Check that next token is 'c'. +*/ +static void check (LexState *ls, int c) { + if (ls->t.token != c) + error_expected(ls, c); +} + + +/* +** Check that next token is 'c' and skip it. +*/ +static void checknext (LexState *ls, int c) { + check(ls, c); + luaX_next(ls); +} + + +#define check_condition(ls,c,msg) { if (!(c)) luaX_syntaxerror(ls, msg); } + + +/* +** Check that next token is 'what' and skip it. In case of error, +** raise an error that the expected 'what' should match a 'who' +** in line 'where' (if that is not the current line). +*/ +static void check_match (LexState *ls, int what, int who, int where) { + if (l_unlikely(!testnext(ls, what))) { + if (where == ls->linenumber) /* all in the same line? */ + error_expected(ls, what); /* do not need a complex message */ + else { + luaX_syntaxerror(ls, luaO_pushfstring(ls->L, + "%s expected (to close %s at line %d)", + luaX_token2str(ls, what), luaX_token2str(ls, who), where)); + } + } +} + + +static TString *str_checkname (LexState *ls) { + TString *ts; + check(ls, TK_NAME); + ts = ls->t.seminfo.ts; + luaX_next(ls); + return ts; +} + + +static void init_exp (expdesc *e, expkind k, int i) { + e->f = e->t = NO_JUMP; + e->k = k; + e->u.info = i; +} + + +static void codestring (expdesc *e, TString *s) { + e->f = e->t = NO_JUMP; + e->k = VKSTR; + e->u.strval = s; +} + + +static void codename (LexState *ls, expdesc *e) { + codestring(e, str_checkname(ls)); +} + + +/* +** Register a new local variable in the active 'Proto' (for debug +** information). +*/ +static short registerlocalvar (LexState *ls, FuncState *fs, + TString *varname) { + Proto *f = fs->f; + int oldsize = f->sizelocvars; + luaM_growvector(ls->L, f->locvars, fs->ndebugvars, f->sizelocvars, + LocVar, SHRT_MAX, "local variables"); + while (oldsize < f->sizelocvars) + f->locvars[oldsize++].varname = NULL; + f->locvars[fs->ndebugvars].varname = varname; + f->locvars[fs->ndebugvars].startpc = fs->pc; + luaC_objbarrier(ls->L, f, varname); + return fs->ndebugvars++; +} + + +/* +** Create a new variable with the given 'name' and given 'kind'. +** Return its index in the function. +*/ +static int new_varkind (LexState *ls, TString *name, lu_byte kind) { + lua_State *L = ls->L; + FuncState *fs = ls->fs; + Dyndata *dyd = ls->dyd; + Vardesc *var; + luaM_growvector(L, dyd->actvar.arr, dyd->actvar.n + 1, + dyd->actvar.size, Vardesc, SHRT_MAX, "variable declarations"); + var = &dyd->actvar.arr[dyd->actvar.n++]; + var->vd.kind = kind; /* default */ + var->vd.name = name; + return dyd->actvar.n - 1 - fs->firstlocal; +} + + +/* +** Create a new local variable with the given 'name' and regular kind. +*/ +static int new_localvar (LexState *ls, TString *name) { + return new_varkind(ls, name, VDKREG); +} + +#define new_localvarliteral(ls,v) \ + new_localvar(ls, \ + luaX_newstring(ls, "" v, (sizeof(v)/sizeof(char)) - 1)); + + + +/* +** Return the "variable description" (Vardesc) of a given variable. +** (Unless noted otherwise, all variables are referred to by their +** compiler indices.) +*/ +static Vardesc *getlocalvardesc (FuncState *fs, int vidx) { + return &fs->ls->dyd->actvar.arr[fs->firstlocal + vidx]; +} + + +/* +** Convert 'nvar', a compiler index level, to its corresponding +** register. For that, search for the highest variable below that level +** that is in a register and uses its register index ('ridx') plus one. +*/ +static lu_byte reglevel (FuncState *fs, int nvar) { + while (nvar-- > 0) { + Vardesc *vd = getlocalvardesc(fs, nvar); /* get previous variable */ + if (varinreg(vd)) /* is in a register? */ + return cast_byte(vd->vd.ridx + 1); + } + return 0; /* no variables in registers */ +} + + +/* +** Return the number of variables in the register stack for the given +** function. +*/ +lu_byte luaY_nvarstack (FuncState *fs) { + return reglevel(fs, fs->nactvar); +} + + +/* +** Get the debug-information entry for current variable 'vidx'. +*/ +static LocVar *localdebuginfo (FuncState *fs, int vidx) { + Vardesc *vd = getlocalvardesc(fs, vidx); + if (!varinreg(vd)) + return NULL; /* no debug info. for constants */ + else { + int idx = vd->vd.pidx; + lua_assert(idx < fs->ndebugvars); + return &fs->f->locvars[idx]; + } +} + + +/* +** Create an expression representing variable 'vidx' +*/ +static void init_var (FuncState *fs, expdesc *e, int vidx) { + e->f = e->t = NO_JUMP; + e->k = VLOCAL; + e->u.var.vidx = cast_short(vidx); + e->u.var.ridx = getlocalvardesc(fs, vidx)->vd.ridx; +} + + +/* +** Raises an error if variable described by 'e' is read only; moreover, +** if 'e' is t[exp] where t is the vararg parameter, change it to index +** a real table. (Virtual vararg tables cannot be changed.) +*/ +static void check_readonly (LexState *ls, expdesc *e) { + FuncState *fs = ls->fs; + TString *varname = NULL; /* to be set if variable is const */ + switch (e->k) { + case VCONST: { + varname = ls->dyd->actvar.arr[e->u.info].vd.name; + break; + } + case VLOCAL: case VVARGVAR: { + Vardesc *vardesc = getlocalvardesc(fs, e->u.var.vidx); + if (vardesc->vd.kind != VDKREG) /* not a regular variable? */ + varname = vardesc->vd.name; + break; + } + case VUPVAL: { + Upvaldesc *up = &fs->f->upvalues[e->u.info]; + if (up->kind != VDKREG) + varname = up->name; + break; + } + case VVARGIND: { + needvatab(fs->f); /* function will need a vararg table */ + e->k = VINDEXED; + } /* FALLTHROUGH */ + case VINDEXUP: case VINDEXSTR: case VINDEXED: { /* global variable */ + if (e->u.ind.ro) /* read-only? */ + varname = tsvalue(&fs->f->k[e->u.ind.keystr]); + break; + } + default: + lua_assert(e->k == VINDEXI); /* this one doesn't need any check */ + return; /* integer index cannot be read-only */ + } + if (varname) + luaK_semerror(ls, "attempt to assign to const variable '%s'", + getstr(varname)); +} + + +/* +** Start the scope for the last 'nvars' created variables. +*/ +static void adjustlocalvars (LexState *ls, int nvars) { + FuncState *fs = ls->fs; + int reglevel = luaY_nvarstack(fs); + int i; + for (i = 0; i < nvars; i++) { + int vidx = fs->nactvar++; + Vardesc *var = getlocalvardesc(fs, vidx); + var->vd.ridx = cast_byte(reglevel++); + var->vd.pidx = registerlocalvar(ls, fs, var->vd.name); + luaY_checklimit(fs, reglevel, MAXVARS, "local variables"); + } +} + + +/* +** Close the scope for all variables up to level 'tolevel'. +** (debug info.) +*/ +static void removevars (FuncState *fs, int tolevel) { + fs->ls->dyd->actvar.n -= (fs->nactvar - tolevel); + while (fs->nactvar > tolevel) { + LocVar *var = localdebuginfo(fs, --fs->nactvar); + if (var) /* does it have debug information? */ + var->endpc = fs->pc; + } +} + + +/* +** Search the upvalues of the function 'fs' for one +** with the given 'name'. +*/ +static int searchupvalue (FuncState *fs, TString *name) { + int i; + Upvaldesc *up = fs->f->upvalues; + for (i = 0; i < fs->nups; i++) { + if (eqstr(up[i].name, name)) return i; + } + return -1; /* not found */ +} + + +static Upvaldesc *allocupvalue (FuncState *fs) { + Proto *f = fs->f; + int oldsize = f->sizeupvalues; + luaY_checklimit(fs, fs->nups + 1, MAXUPVAL, "upvalues"); + luaM_growvector(fs->ls->L, f->upvalues, fs->nups, f->sizeupvalues, + Upvaldesc, MAXUPVAL, "upvalues"); + while (oldsize < f->sizeupvalues) + f->upvalues[oldsize++].name = NULL; + return &f->upvalues[fs->nups++]; +} + + +static int newupvalue (FuncState *fs, TString *name, expdesc *v) { + Upvaldesc *up = allocupvalue(fs); + FuncState *prev = fs->prev; + if (v->k == VLOCAL) { + up->instack = 1; + up->idx = v->u.var.ridx; + up->kind = getlocalvardesc(prev, v->u.var.vidx)->vd.kind; + lua_assert(eqstr(name, getlocalvardesc(prev, v->u.var.vidx)->vd.name)); + } + else { + up->instack = 0; + up->idx = cast_byte(v->u.info); + up->kind = prev->f->upvalues[v->u.info].kind; + lua_assert(eqstr(name, prev->f->upvalues[v->u.info].name)); + } + up->name = name; + luaC_objbarrier(fs->ls->L, fs->f, name); + return fs->nups - 1; +} + + +/* +** Look for an active variable with the name 'n' in the +** function 'fs'. If found, initialize 'var' with it and return +** its expression kind; otherwise return -1. While searching, +** var->u.info==-1 means that the preambular global declaration is +** active (the default while there is no other global declaration); +** var->u.info==-2 means there is no active collective declaration +** (some previous global declaration but no collective declaration); +** and var->u.info>=0 points to the inner-most (the first one found) +** collective declaration, if there is one. +*/ +static int searchvar (FuncState *fs, TString *n, expdesc *var) { + int i; + for (i = cast_int(fs->nactvar) - 1; i >= 0; i--) { + Vardesc *vd = getlocalvardesc(fs, i); + if (varglobal(vd)) { /* global declaration? */ + if (vd->vd.name == NULL) { /* collective declaration? */ + if (var->u.info < 0) /* no previous collective declaration? */ + var->u.info = fs->firstlocal + i; /* this is the first one */ + } + else { /* global name */ + if (eqstr(n, vd->vd.name)) { /* found? */ + init_exp(var, VGLOBAL, fs->firstlocal + i); + return VGLOBAL; + } + else if (var->u.info == -1) /* active preambular declaration? */ + var->u.info = -2; /* invalidate preambular declaration */ + } + } + else if (eqstr(n, vd->vd.name)) { /* found? */ + if (vd->vd.kind == RDKCTC) /* compile-time constant? */ + init_exp(var, VCONST, fs->firstlocal + i); + else { /* local variable */ + init_var(fs, var, i); + if (vd->vd.kind == RDKVAVAR) /* vararg parameter? */ + var->k = VVARGVAR; + } + return cast_int(var->k); + } + } + return -1; /* not found */ +} + + +/* +** Mark block where variable at given level was defined +** (to emit close instructions later). +*/ +static void markupval (FuncState *fs, int level) { + BlockCnt *bl = fs->bl; + while (bl->nactvar > level) + bl = bl->previous; + bl->upval = 1; + fs->needclose = 1; +} + + +/* +** Mark that current block has a to-be-closed variable. +*/ +static void marktobeclosed (FuncState *fs) { + BlockCnt *bl = fs->bl; + bl->upval = 1; + bl->insidetbc = 1; + fs->needclose = 1; +} + + +/* +** Find a variable with the given name 'n'. If it is an upvalue, add +** this upvalue into all intermediate functions. If it is a global, set +** 'var' as 'void' as a flag. +*/ +static void singlevaraux (FuncState *fs, TString *n, expdesc *var, int base) { + int v = searchvar(fs, n, var); /* look up variables at current level */ + if (v >= 0) { /* found? */ + if (!base) { + if (var->k == VVARGVAR) /* vararg parameter? */ + luaK_vapar2local(fs, var); /* change it to a regular local */ + if (var->k == VLOCAL) + markupval(fs, var->u.var.vidx); /* will be used as an upvalue */ + } + /* else nothing else to be done */ + } + else { /* not found at current level; try upvalues */ + int idx = searchupvalue(fs, n); /* try existing upvalues */ + if (idx < 0) { /* not found? */ + if (fs->prev != NULL) /* more levels? */ + singlevaraux(fs->prev, n, var, 0); /* try upper levels */ + if (var->k == VLOCAL || var->k == VUPVAL) /* local or upvalue? */ + idx = newupvalue(fs, n, var); /* will be a new upvalue */ + else /* it is a global or a constant */ + return; /* don't need to do anything at this level */ + } + init_exp(var, VUPVAL, idx); /* new or old upvalue */ + } +} + + +static void buildglobal (LexState *ls, TString *varname, expdesc *var) { + FuncState *fs = ls->fs; + expdesc key; + init_exp(var, VGLOBAL, -1); /* global by default */ + singlevaraux(fs, ls->envn, var, 1); /* get environment variable */ + if (var->k == VGLOBAL) + luaK_semerror(ls, "%s is global when accessing variable '%s'", + LUA_ENV, getstr(varname)); + luaK_exp2anyregup(fs, var); /* _ENV could be a constant */ + codestring(&key, varname); /* key is variable name */ + luaK_indexed(fs, var, &key); /* 'var' represents _ENV[varname] */ +} + + +/* +** Find a variable with the given name 'n', handling global variables +** too. +*/ +static void buildvar (LexState *ls, TString *varname, expdesc *var) { + FuncState *fs = ls->fs; + init_exp(var, VGLOBAL, -1); /* global by default */ + singlevaraux(fs, varname, var, 1); + if (var->k == VGLOBAL) { /* global name? */ + int info = var->u.info; + /* global by default in the scope of a global declaration? */ + if (info == -2) + luaK_semerror(ls, "variable '%s' not declared", getstr(varname)); + buildglobal(ls, varname, var); + if (info != -1 && ls->dyd->actvar.arr[info].vd.kind == GDKCONST) + var->u.ind.ro = 1; /* mark variable as read-only */ + else /* anyway must be a global */ + lua_assert(info == -1 || ls->dyd->actvar.arr[info].vd.kind == GDKREG); + } +} + + +static void singlevar (LexState *ls, expdesc *var) { + buildvar(ls, str_checkname(ls), var); +} + + +/* +** Adjust the number of results from an expression list 'e' with 'nexps' +** expressions to 'nvars' values. +*/ +static void adjust_assign (LexState *ls, int nvars, int nexps, expdesc *e) { + FuncState *fs = ls->fs; + int needed = nvars - nexps; /* extra values needed */ + luaK_checkstack(fs, needed); + if (hasmultret(e->k)) { /* last expression has multiple returns? */ + int extra = needed + 1; /* discount last expression itself */ + if (extra < 0) + extra = 0; + luaK_setreturns(fs, e, extra); /* last exp. provides the difference */ + } + else { + if (e->k != VVOID) /* at least one expression? */ + luaK_exp2nextreg(fs, e); /* close last expression */ + if (needed > 0) /* missing values? */ + luaK_nil(fs, fs->freereg, needed); /* complete with nils */ + } + if (needed > 0) + luaK_reserveregs(fs, needed); /* registers for extra values */ + else /* adding 'needed' is actually a subtraction */ + fs->freereg = cast_byte(fs->freereg + needed); /* remove extra values */ +} + + +#define enterlevel(ls) luaE_incCstack(ls->L) + + +#define leavelevel(ls) ((ls)->L->nCcalls--) + + +/* +** Generates an error that a goto jumps into the scope of some +** variable declaration. +*/ +static l_noret jumpscopeerror (LexState *ls, Labeldesc *gt) { + TString *tsname = getlocalvardesc(ls->fs, gt->nactvar)->vd.name; + const char *varname = (tsname != NULL) ? getstr(tsname) : "*"; + luaK_semerror(ls, + " at line %d jumps into the scope of '%s'", + getstr(gt->name), gt->line, varname); /* raise the error */ +} + + +/* +** Closes the goto at index 'g' to given 'label' and removes it +** from the list of pending gotos. +** If it jumps into the scope of some variable, raises an error. +** The goto needs a CLOSE if it jumps out of a block with upvalues, +** or out of the scope of some variable and the block has upvalues +** (signaled by parameter 'bup'). +*/ +static void closegoto (LexState *ls, int g, Labeldesc *label, int bup) { + int i; + FuncState *fs = ls->fs; + Labellist *gl = &ls->dyd->gt; /* list of gotos */ + Labeldesc *gt = &gl->arr[g]; /* goto to be resolved */ + lua_assert(eqstr(gt->name, label->name)); + if (l_unlikely(gt->nactvar < label->nactvar)) /* enter some scope? */ + jumpscopeerror(ls, gt); + if (gt->close || + (label->nactvar < gt->nactvar && bup)) { /* needs close? */ + lu_byte stklevel = reglevel(fs, label->nactvar); + /* move jump to CLOSE position */ + fs->f->code[gt->pc + 1] = fs->f->code[gt->pc]; + /* put CLOSE instruction at original position */ + fs->f->code[gt->pc] = CREATE_ABCk(OP_CLOSE, stklevel, 0, 0, 0); + gt->pc++; /* must point to jump instruction */ + } + luaK_patchlist(ls->fs, gt->pc, label->pc); /* goto jumps to label */ + for (i = g; i < gl->n - 1; i++) /* remove goto from pending list */ + gl->arr[i] = gl->arr[i + 1]; + gl->n--; +} + + +/* +** Search for an active label with the given name, starting at +** index 'ilb' (so that it can search for all labels in current block +** or all labels in current function). +*/ +static Labeldesc *findlabel (LexState *ls, TString *name, int ilb) { + Dyndata *dyd = ls->dyd; + for (; ilb < dyd->label.n; ilb++) { + Labeldesc *lb = &dyd->label.arr[ilb]; + if (eqstr(lb->name, name)) /* correct label? */ + return lb; + } + return NULL; /* label not found */ +} + + +/* +** Adds a new label/goto in the corresponding list. +*/ +static int newlabelentry (LexState *ls, Labellist *l, TString *name, + int line, int pc) { + int n = l->n; + luaM_growvector(ls->L, l->arr, n, l->size, + Labeldesc, SHRT_MAX, "labels/gotos"); + l->arr[n].name = name; + l->arr[n].line = line; + l->arr[n].nactvar = ls->fs->nactvar; + l->arr[n].close = 0; + l->arr[n].pc = pc; + l->n = n + 1; + return n; +} + + +/* +** Create an entry for the goto and the code for it. As it is not known +** at this point whether the goto may need a CLOSE, the code has a jump +** followed by an CLOSE. (As the CLOSE comes after the jump, it is a +** dead instruction; it works as a placeholder.) When the goto is closed +** against a label, if it needs a CLOSE, the two instructions swap +** positions, so that the CLOSE comes before the jump. +*/ +static int newgotoentry (LexState *ls, TString *name, int line) { + FuncState *fs = ls->fs; + int pc = luaK_jump(fs); /* create jump */ + luaK_codeABC(fs, OP_CLOSE, 0, 1, 0); /* spaceholder, marked as dead */ + return newlabelentry(ls, &ls->dyd->gt, name, line, pc); +} + + +/* +** Create a new label with the given 'name' at the given 'line'. +** 'last' tells whether label is the last non-op statement in its +** block. Solves all pending gotos to this new label and adds +** a close instruction if necessary. +** Returns true iff it added a close instruction. +*/ +static void createlabel (LexState *ls, TString *name, int line, int last) { + FuncState *fs = ls->fs; + Labellist *ll = &ls->dyd->label; + int l = newlabelentry(ls, ll, name, line, luaK_getlabel(fs)); + if (last) { /* label is last no-op statement in the block? */ + /* assume that locals are already out of scope */ + ll->arr[l].nactvar = fs->bl->nactvar; + } +} + + +/* +** Traverse the pending gotos of the finishing block checking whether +** each match some label of that block. Those that do not match are +** "exported" to the outer block, to be solved there. In particular, +** its 'nactvar' is updated with the level of the inner block, +** as the variables of the inner block are now out of scope. +*/ +static void solvegotos (FuncState *fs, BlockCnt *bl) { + LexState *ls = fs->ls; + Labellist *gl = &ls->dyd->gt; + int outlevel = reglevel(fs, bl->nactvar); /* level outside the block */ + int igt = bl->firstgoto; /* first goto in the finishing block */ + while (igt < gl->n) { /* for each pending goto */ + Labeldesc *gt = &gl->arr[igt]; + /* search for a matching label in the current block */ + Labeldesc *lb = findlabel(ls, gt->name, bl->firstlabel); + if (lb != NULL) /* found a match? */ + closegoto(ls, igt, lb, bl->upval); /* close and remove goto */ + else { /* adjust 'goto' for outer block */ + /* block has variables to be closed and goto escapes the scope of + some variable? */ + if (bl->upval && reglevel(fs, gt->nactvar) > outlevel) + gt->close = 1; /* jump may need a close */ + gt->nactvar = bl->nactvar; /* correct level for outer block */ + igt++; /* go to next goto */ + } + } + ls->dyd->label.n = bl->firstlabel; /* remove local labels */ +} + + +static void enterblock (FuncState *fs, BlockCnt *bl, lu_byte isloop) { + bl->isloop = isloop; + bl->nactvar = fs->nactvar; + bl->firstlabel = fs->ls->dyd->label.n; + bl->firstgoto = fs->ls->dyd->gt.n; + bl->upval = 0; + /* inherit 'insidetbc' from enclosing block */ + bl->insidetbc = (fs->bl != NULL && fs->bl->insidetbc); + bl->previous = fs->bl; /* link block in function's block list */ + fs->bl = bl; + lua_assert(fs->freereg == luaY_nvarstack(fs)); +} + + +/* +** generates an error for an undefined 'goto'. +*/ +static l_noret undefgoto (LexState *ls, Labeldesc *gt) { + /* breaks are checked when created, cannot be undefined */ + lua_assert(!eqstr(gt->name, ls->brkn)); + luaK_semerror(ls, "no visible label '%s' for at line %d", + getstr(gt->name), gt->line); +} + + +static void leaveblock (FuncState *fs) { + BlockCnt *bl = fs->bl; + LexState *ls = fs->ls; + lu_byte stklevel = reglevel(fs, bl->nactvar); /* level outside block */ + if (bl->previous && bl->upval) /* need a 'close'? */ + luaK_codeABC(fs, OP_CLOSE, stklevel, 0, 0); + fs->freereg = stklevel; /* free registers */ + removevars(fs, bl->nactvar); /* remove block locals */ + lua_assert(bl->nactvar == fs->nactvar); /* back to level on entry */ + if (bl->isloop == 2) /* has to fix pending breaks? */ + createlabel(ls, ls->brkn, 0, 0); + solvegotos(fs, bl); + if (bl->previous == NULL) { /* was it the last block? */ + if (bl->firstgoto < ls->dyd->gt.n) /* still pending gotos? */ + undefgoto(ls, &ls->dyd->gt.arr[bl->firstgoto]); /* error */ + } + fs->bl = bl->previous; /* current block now is previous one */ +} + + +/* +** adds a new prototype into list of prototypes +*/ +static Proto *addprototype (LexState *ls) { + Proto *clp; + lua_State *L = ls->L; + FuncState *fs = ls->fs; + Proto *f = fs->f; /* prototype of current function */ + if (fs->np >= f->sizep) { + int oldsize = f->sizep; + luaM_growvector(L, f->p, fs->np, f->sizep, Proto *, MAXARG_Bx, "functions"); + while (oldsize < f->sizep) + f->p[oldsize++] = NULL; + } + f->p[fs->np++] = clp = luaF_newproto(L); + luaC_objbarrier(L, f, clp); + return clp; +} + + +/* +** codes instruction to create new closure in parent function. +** The OP_CLOSURE instruction uses the last available register, +** so that, if it invokes the GC, the GC knows which registers +** are in use at that time. + +*/ +static void codeclosure (LexState *ls, expdesc *v) { + FuncState *fs = ls->fs->prev; + init_exp(v, VRELOC, luaK_codeABx(fs, OP_CLOSURE, 0, fs->np - 1)); + luaK_exp2nextreg(fs, v); /* fix it at the last register */ +} + + +static void open_func (LexState *ls, FuncState *fs, BlockCnt *bl) { + lua_State *L = ls->L; + Proto *f = fs->f; + fs->prev = ls->fs; /* linked list of funcstates */ + fs->ls = ls; + ls->fs = fs; + fs->pc = 0; + fs->previousline = f->linedefined; + fs->iwthabs = 0; + fs->lasttarget = 0; + fs->freereg = 0; + fs->nk = 0; + fs->nabslineinfo = 0; + fs->np = 0; + fs->nups = 0; + fs->ndebugvars = 0; + fs->nactvar = 0; + fs->needclose = 0; + fs->firstlocal = ls->dyd->actvar.n; + fs->firstlabel = ls->dyd->label.n; + fs->bl = NULL; + f->source = ls->source; + luaC_objbarrier(L, f, f->source); + f->maxstacksize = 2; /* registers 0/1 are always valid */ + fs->kcache = luaH_new(L); /* create table for function */ + luaD_anchorobj(L, ls->h, obj2gco(fs->kcache)); /* anchor it */ + enterblock(fs, bl, 0); +} + + +static void close_func (LexState *ls) { + lua_State *L = ls->L; + FuncState *fs = ls->fs; + Proto *f = fs->f; + TValue temp; + luaK_ret(fs, luaY_nvarstack(fs), 0); /* final return */ + leaveblock(fs); + lua_assert(fs->bl == NULL); + luaK_finish(fs); + luaM_shrinkvector(L, f->code, f->sizecode, fs->pc, Instruction); + luaM_shrinkvector(L, f->lineinfo, f->sizelineinfo, fs->pc, ls_byte); + luaM_shrinkvector(L, f->abslineinfo, f->sizeabslineinfo, + fs->nabslineinfo, AbsLineInfo); + luaM_shrinkvector(L, f->k, f->sizek, fs->nk, TValue); + luaM_shrinkvector(L, f->p, f->sizep, fs->np, Proto *); + luaM_shrinkvector(L, f->locvars, f->sizelocvars, fs->ndebugvars, LocVar); + luaM_shrinkvector(L, f->upvalues, f->sizeupvalues, fs->nups, Upvaldesc); + /* remove kcache table from scanner table ("weigh" its anchor) */ + sethvalue(L, &temp, fs->kcache); /* key to be set to nil */ + luaH_set(L, ls->h, &temp, &G(L)->nilvalue); + ls->fs = fs->prev; + luaC_checkGC(L); +} + + +/* +** {====================================================================== +** GRAMMAR RULES +** ======================================================================= +*/ + + +/* +** check whether current token is in the follow set of a block. +** 'until' closes syntactical blocks, but do not close scope, +** so it is handled in separate. +*/ +static int block_follow (LexState *ls, int withuntil) { + switch (ls->t.token) { + case TK_ELSE: case TK_ELSEIF: + case TK_END: case TK_EOS: + return 1; + case TK_UNTIL: return withuntil; + default: return 0; + } +} + + +static void statlist (LexState *ls) { + /* statlist -> { stat [';'] } */ + while (!block_follow(ls, 1)) { + if (ls->t.token == TK_RETURN) { + statement(ls); + return; /* 'return' must be last statement */ + } + statement(ls); + } +} + + +static void fieldsel (LexState *ls, expdesc *v) { + /* fieldsel -> ['.' | ':'] NAME */ + FuncState *fs = ls->fs; + expdesc key; + luaK_exp2anyregup(fs, v); + luaX_next(ls); /* skip the dot or colon */ + codename(ls, &key); + luaK_indexed(fs, v, &key); +} + + +static void yindex (LexState *ls, expdesc *v) { + /* index -> '[' expr ']' */ + luaX_next(ls); /* skip the '[' */ + expr(ls, v); + luaK_exp2val(ls->fs, v); + checknext(ls, ']'); +} + + +/* +** {====================================================================== +** Rules for Constructors +** ======================================================================= +*/ + +typedef struct ConsControl { + expdesc v; /* last list item read */ + expdesc *t; /* table descriptor */ + int nh; /* total number of 'record' elements */ + int na; /* number of array elements already stored */ + int tostore; /* number of array elements pending to be stored */ + int maxtostore; /* maximum number of pending elements */ +} ConsControl; + + +/* +** Maximum number of elements in a constructor, to control the following: +** * counter overflows; +** * overflows in 'extra' for OP_NEWTABLE and OP_SETLIST; +** * overflows when adding multiple returns in OP_SETLIST. +*/ +#define MAX_CNST (INT_MAX/2) +#if MAX_CNST/(MAXARG_vC + 1) > MAXARG_Ax +#undef MAX_CNST +#define MAX_CNST (MAXARG_Ax * (MAXARG_vC + 1)) +#endif + + +static void recfield (LexState *ls, ConsControl *cc) { + /* recfield -> (NAME | '['exp']') = exp */ + FuncState *fs = ls->fs; + lu_byte reg = ls->fs->freereg; + expdesc tab, key, val; + if (ls->t.token == TK_NAME) + codename(ls, &key); + else /* ls->t.token == '[' */ + yindex(ls, &key); + cc->nh++; + checknext(ls, '='); + tab = *cc->t; + luaK_indexed(fs, &tab, &key); + expr(ls, &val); + luaK_storevar(fs, &tab, &val); + fs->freereg = reg; /* free registers */ +} + + +static void closelistfield (FuncState *fs, ConsControl *cc) { + lua_assert(cc->tostore > 0); + luaK_exp2nextreg(fs, &cc->v); + cc->v.k = VVOID; + if (cc->tostore >= cc->maxtostore) { + luaK_setlist(fs, cc->t->u.info, cc->na, cc->tostore); /* flush */ + cc->na += cc->tostore; + cc->tostore = 0; /* no more items pending */ + } +} + + +static void lastlistfield (FuncState *fs, ConsControl *cc) { + if (cc->tostore == 0) return; + if (hasmultret(cc->v.k)) { + luaK_setmultret(fs, &cc->v); + luaK_setlist(fs, cc->t->u.info, cc->na, LUA_MULTRET); + cc->na--; /* do not count last expression (unknown number of elements) */ + } + else { + if (cc->v.k != VVOID) + luaK_exp2nextreg(fs, &cc->v); + luaK_setlist(fs, cc->t->u.info, cc->na, cc->tostore); + } + cc->na += cc->tostore; +} + + +static void listfield (LexState *ls, ConsControl *cc) { + /* listfield -> exp */ + expr(ls, &cc->v); + cc->tostore++; +} + + +static void field (LexState *ls, ConsControl *cc) { + /* field -> listfield | recfield */ + switch(ls->t.token) { + case TK_NAME: { /* may be 'listfield' or 'recfield' */ + if (luaX_lookahead(ls) != '=') /* expression? */ + listfield(ls, cc); + else + recfield(ls, cc); + break; + } + case '[': { + recfield(ls, cc); + break; + } + default: { + listfield(ls, cc); + break; + } + } +} + + +/* +** Compute a limit for how many registers a constructor can use before +** emitting a 'SETLIST' instruction, based on how many registers are +** available. +*/ +static int maxtostore (FuncState *fs) { + int numfreeregs = MAX_FSTACK - fs->freereg; + if (numfreeregs >= 160) /* "lots" of registers? */ + return numfreeregs / 5; /* use up to 1/5 of them */ + else if (numfreeregs >= 80) /* still "enough" registers? */ + return 10; /* one 'SETLIST' instruction for each 10 values */ + else /* save registers for potential more nesting */ + return 1; +} + + +static void constructor (LexState *ls, expdesc *t) { + /* constructor -> '{' [ field { sep field } [sep] ] '}' + sep -> ',' | ';' */ + FuncState *fs = ls->fs; + int line = ls->linenumber; + int pc = luaK_codevABCk(fs, OP_NEWTABLE, 0, 0, 0, 0); + ConsControl cc; + luaK_code(fs, 0); /* space for extra arg. */ + cc.na = cc.nh = cc.tostore = 0; + cc.t = t; + init_exp(t, VNONRELOC, fs->freereg); /* table will be at stack top */ + luaK_reserveregs(fs, 1); + init_exp(&cc.v, VVOID, 0); /* no value (yet) */ + checknext(ls, '{' /*}*/); + cc.maxtostore = maxtostore(fs); + do { + if (ls->t.token == /*{*/ '}') break; + if (cc.v.k != VVOID) /* is there a previous list item? */ + closelistfield(fs, &cc); /* close it */ + field(ls, &cc); + luaY_checklimit(fs, cc.tostore + cc.na + cc.nh, MAX_CNST, + "items in a constructor"); + } while (testnext(ls, ',') || testnext(ls, ';')); + check_match(ls, /*{*/ '}', '{' /*}*/, line); + lastlistfield(fs, &cc); + luaK_settablesize(fs, pc, t->u.info, cc.na, cc.nh); +} + +/* }====================================================================== */ + + +static void setvararg (FuncState *fs) { + fs->f->flag |= PF_VAHID; /* by default, use hidden vararg arguments */ + luaK_codeABC(fs, OP_VARARGPREP, 0, 0, 0); +} + + +static void parlist (LexState *ls) { + /* parlist -> [ {NAME ','} (NAME | '...') ] */ + FuncState *fs = ls->fs; + Proto *f = fs->f; + int nparams = 0; + int varargk = 0; + if (ls->t.token != ')') { /* is 'parlist' not empty? */ + do { + switch (ls->t.token) { + case TK_NAME: { + new_localvar(ls, str_checkname(ls)); + nparams++; + break; + } + case TK_DOTS: { + varargk = 1; + luaX_next(ls); /* skip '...' */ + if (ls->t.token == TK_NAME) + new_varkind(ls, str_checkname(ls), RDKVAVAR); + else + new_localvarliteral(ls, "(vararg table)"); + break; + } + default: luaX_syntaxerror(ls, " or '...' expected"); + } + } while (!varargk && testnext(ls, ',')); + } + adjustlocalvars(ls, nparams); + f->numparams = cast_byte(fs->nactvar); + if (varargk) { + setvararg(fs); /* declared vararg */ + adjustlocalvars(ls, 1); /* vararg parameter */ + } + /* reserve registers for parameters (plus vararg parameter, if present) */ + luaK_reserveregs(fs, fs->nactvar); +} + + +static void body (LexState *ls, expdesc *e, int ismethod, int line) { + /* body -> '(' parlist ')' block END */ + FuncState new_fs; + BlockCnt bl; + new_fs.f = addprototype(ls); + new_fs.f->linedefined = line; + open_func(ls, &new_fs, &bl); + checknext(ls, '('); + if (ismethod) { + new_localvarliteral(ls, "self"); /* create 'self' parameter */ + adjustlocalvars(ls, 1); + } + parlist(ls); + checknext(ls, ')'); + statlist(ls); + new_fs.f->lastlinedefined = ls->linenumber; + check_match(ls, TK_END, TK_FUNCTION, line); + codeclosure(ls, e); + close_func(ls); +} + + +static int explist (LexState *ls, expdesc *v) { + /* explist -> expr { ',' expr } */ + int n = 1; /* at least one expression */ + expr(ls, v); + while (testnext(ls, ',')) { + luaK_exp2nextreg(ls->fs, v); + expr(ls, v); + n++; + } + return n; +} + + +static void funcargs (LexState *ls, expdesc *f) { + FuncState *fs = ls->fs; + expdesc args; + int base, nparams; + int line = ls->linenumber; + switch (ls->t.token) { + case '(': { /* funcargs -> '(' [ explist ] ')' */ + luaX_next(ls); + if (ls->t.token == ')') /* arg list is empty? */ + args.k = VVOID; + else { + explist(ls, &args); + if (hasmultret(args.k)) + luaK_setmultret(fs, &args); + } + check_match(ls, ')', '(', line); + break; + } + case '{' /*}*/: { /* funcargs -> constructor */ + constructor(ls, &args); + break; + } + case TK_STRING: { /* funcargs -> STRING */ + codestring(&args, ls->t.seminfo.ts); + luaX_next(ls); /* must use 'seminfo' before 'next' */ + break; + } + default: { + luaX_syntaxerror(ls, "function arguments expected"); + return; /* to avoid warnings */ + } + } + lua_assert(f->k == VNONRELOC); + base = f->u.info; /* base register for call */ + if (hasmultret(args.k)) + nparams = LUA_MULTRET; /* open call */ + else { + if (args.k != VVOID) + luaK_exp2nextreg(fs, &args); /* close last argument */ + nparams = fs->freereg - (base+1); + } + init_exp(f, VCALL, luaK_codeABC(fs, OP_CALL, base, nparams+1, 2)); + luaK_fixline(fs, line); + /* call removes function and arguments and leaves one result (unless + changed later) */ + fs->freereg = cast_byte(base + 1); +} + + + + +/* +** {====================================================================== +** Expression parsing +** ======================================================================= +*/ + + +static void primaryexp (LexState *ls, expdesc *v) { + /* primaryexp -> NAME | '(' expr ')' */ + switch (ls->t.token) { + case '(': { + int line = ls->linenumber; + luaX_next(ls); + expr(ls, v); + check_match(ls, ')', '(', line); + luaK_dischargevars(ls->fs, v); + return; + } + case TK_NAME: { + singlevar(ls, v); + return; + } + default: { + luaX_syntaxerror(ls, "unexpected symbol"); + } + } +} + + +static void suffixedexp (LexState *ls, expdesc *v) { + /* suffixedexp -> + primaryexp { '.' NAME | '[' exp ']' | ':' NAME funcargs | funcargs } */ + FuncState *fs = ls->fs; + primaryexp(ls, v); + for (;;) { + switch (ls->t.token) { + case '.': { /* fieldsel */ + fieldsel(ls, v); + break; + } + case '[': { /* '[' exp ']' */ + expdesc key; + luaK_exp2anyregup(fs, v); + yindex(ls, &key); + luaK_indexed(fs, v, &key); + break; + } + case ':': { /* ':' NAME funcargs */ + expdesc key; + luaX_next(ls); + codename(ls, &key); + luaK_self(fs, v, &key); + funcargs(ls, v); + break; + } + case '(': case TK_STRING: case '{' /*}*/: { /* funcargs */ + luaK_exp2nextreg(fs, v); + funcargs(ls, v); + break; + } + default: return; + } + } +} + + +static void simpleexp (LexState *ls, expdesc *v) { + /* simpleexp -> FLT | INT | STRING | NIL | TRUE | FALSE | ... | + constructor | FUNCTION body | suffixedexp */ + switch (ls->t.token) { + case TK_FLT: { + init_exp(v, VKFLT, 0); + v->u.nval = ls->t.seminfo.r; + break; + } + case TK_INT: { + init_exp(v, VKINT, 0); + v->u.ival = ls->t.seminfo.i; + break; + } + case TK_STRING: { + codestring(v, ls->t.seminfo.ts); + break; + } + case TK_NIL: { + init_exp(v, VNIL, 0); + break; + } + case TK_TRUE: { + init_exp(v, VTRUE, 0); + break; + } + case TK_FALSE: { + init_exp(v, VFALSE, 0); + break; + } + case TK_DOTS: { /* vararg */ + FuncState *fs = ls->fs; + check_condition(ls, isvararg(fs->f), + "cannot use '...' outside a vararg function"); + init_exp(v, VVARARG, luaK_codeABC(fs, OP_VARARG, 0, fs->f->numparams, 1)); + break; + } + case '{' /*}*/: { /* constructor */ + constructor(ls, v); + return; + } + case TK_FUNCTION: { + luaX_next(ls); + body(ls, v, 0, ls->linenumber); + return; + } + default: { + suffixedexp(ls, v); + return; + } + } + luaX_next(ls); +} + + +static UnOpr getunopr (int op) { + switch (op) { + case TK_NOT: return OPR_NOT; + case '-': return OPR_MINUS; + case '~': return OPR_BNOT; + case '#': return OPR_LEN; + default: return OPR_NOUNOPR; + } +} + + +static BinOpr getbinopr (int op) { + switch (op) { + case '+': return OPR_ADD; + case '-': return OPR_SUB; + case '*': return OPR_MUL; + case '%': return OPR_MOD; + case '^': return OPR_POW; + case '/': return OPR_DIV; + case TK_IDIV: return OPR_IDIV; + case '&': return OPR_BAND; + case '|': return OPR_BOR; + case '~': return OPR_BXOR; + case TK_SHL: return OPR_SHL; + case TK_SHR: return OPR_SHR; + case TK_CONCAT: return OPR_CONCAT; + case TK_NE: return OPR_NE; + case TK_EQ: return OPR_EQ; + case '<': return OPR_LT; + case TK_LE: return OPR_LE; + case '>': return OPR_GT; + case TK_GE: return OPR_GE; + case TK_AND: return OPR_AND; + case TK_OR: return OPR_OR; + default: return OPR_NOBINOPR; + } +} + + +/* +** Priority table for binary operators. +*/ +static const struct { + lu_byte left; /* left priority for each binary operator */ + lu_byte right; /* right priority */ +} priority[] = { /* ORDER OPR */ + {10, 10}, {10, 10}, /* '+' '-' */ + {11, 11}, {11, 11}, /* '*' '%' */ + {14, 13}, /* '^' (right associative) */ + {11, 11}, {11, 11}, /* '/' '//' */ + {6, 6}, {4, 4}, {5, 5}, /* '&' '|' '~' */ + {7, 7}, {7, 7}, /* '<<' '>>' */ + {9, 8}, /* '..' (right associative) */ + {3, 3}, {3, 3}, {3, 3}, /* ==, <, <= */ + {3, 3}, {3, 3}, {3, 3}, /* ~=, >, >= */ + {2, 2}, {1, 1} /* and, or */ +}; + +#define UNARY_PRIORITY 12 /* priority for unary operators */ + + +/* +** subexpr -> (simpleexp | unop subexpr) { binop subexpr } +** where 'binop' is any binary operator with a priority higher than 'limit' +*/ +static BinOpr subexpr (LexState *ls, expdesc *v, int limit) { + BinOpr op; + UnOpr uop; + enterlevel(ls); + uop = getunopr(ls->t.token); + if (uop != OPR_NOUNOPR) { /* prefix (unary) operator? */ + int line = ls->linenumber; + luaX_next(ls); /* skip operator */ + subexpr(ls, v, UNARY_PRIORITY); + luaK_prefix(ls->fs, uop, v, line); + } + else simpleexp(ls, v); + /* expand while operators have priorities higher than 'limit' */ + op = getbinopr(ls->t.token); + while (op != OPR_NOBINOPR && priority[op].left > limit) { + expdesc v2; + BinOpr nextop; + int line = ls->linenumber; + luaX_next(ls); /* skip operator */ + luaK_infix(ls->fs, op, v); + /* read sub-expression with higher priority */ + nextop = subexpr(ls, &v2, priority[op].right); + luaK_posfix(ls->fs, op, v, &v2, line); + op = nextop; + } + leavelevel(ls); + return op; /* return first untreated operator */ +} + + +static void expr (LexState *ls, expdesc *v) { + subexpr(ls, v, 0); +} + +/* }==================================================================== */ + + + +/* +** {====================================================================== +** Rules for Statements +** ======================================================================= +*/ + + +static void block (LexState *ls) { + /* block -> statlist */ + FuncState *fs = ls->fs; + BlockCnt bl; + enterblock(fs, &bl, 0); + statlist(ls); + leaveblock(fs); +} + + +/* +** structure to chain all variables in the left-hand side of an +** assignment +*/ +struct LHS_assign { + struct LHS_assign *prev; + expdesc v; /* variable (global, local, upvalue, or indexed) */ +}; + + +/* +** check whether, in an assignment to an upvalue/local variable, the +** upvalue/local variable is begin used in a previous assignment to a +** table. If so, save original upvalue/local value in a safe place and +** use this safe copy in the previous assignment. +*/ +static void check_conflict (LexState *ls, struct LHS_assign *lh, expdesc *v) { + FuncState *fs = ls->fs; + lu_byte extra = fs->freereg; /* eventual position to save local variable */ + int conflict = 0; + for (; lh; lh = lh->prev) { /* check all previous assignments */ + if (vkisindexed(lh->v.k)) { /* assignment to table field? */ + if (lh->v.k == VINDEXUP) { /* is table an upvalue? */ + if (v->k == VUPVAL && lh->v.u.ind.t == v->u.info) { + conflict = 1; /* table is the upvalue being assigned now */ + lh->v.k = VINDEXSTR; + lh->v.u.ind.t = extra; /* assignment will use safe copy */ + } + } + else { /* table is a register */ + if (v->k == VLOCAL && lh->v.u.ind.t == v->u.var.ridx) { + conflict = 1; /* table is the local being assigned now */ + lh->v.u.ind.t = extra; /* assignment will use safe copy */ + } + /* is index the local being assigned? */ + if (lh->v.k == VINDEXED && v->k == VLOCAL && + lh->v.u.ind.idx == v->u.var.ridx) { + conflict = 1; + lh->v.u.ind.idx = extra; /* previous assignment will use safe copy */ + } + } + } + } + if (conflict) { + /* copy upvalue/local value to a temporary (in position 'extra') */ + if (v->k == VLOCAL) + luaK_codeABC(fs, OP_MOVE, extra, v->u.var.ridx, 0); + else + luaK_codeABC(fs, OP_GETUPVAL, extra, v->u.info, 0); + luaK_reserveregs(fs, 1); + } +} + + +/* Create code to store the "top" register in 'var' */ +static void storevartop (FuncState *fs, expdesc *var) { + expdesc e; + init_exp(&e, VNONRELOC, fs->freereg - 1); + luaK_storevar(fs, var, &e); /* will also free the top register */ +} + + +/* +** Parse and compile a multiple assignment. The first "variable" +** (a 'suffixedexp') was already read by the caller. +** +** assignment -> suffixedexp restassign +** restassign -> ',' suffixedexp restassign | '=' explist +*/ +static void restassign (LexState *ls, struct LHS_assign *lh, int nvars) { + expdesc e; + check_condition(ls, vkisvar(lh->v.k), "syntax error"); + check_readonly(ls, &lh->v); + if (testnext(ls, ',')) { /* restassign -> ',' suffixedexp restassign */ + struct LHS_assign nv; + nv.prev = lh; + suffixedexp(ls, &nv.v); + if (!vkisindexed(nv.v.k)) + check_conflict(ls, lh, &nv.v); + enterlevel(ls); /* control recursion depth */ + restassign(ls, &nv, nvars+1); + leavelevel(ls); + } + else { /* restassign -> '=' explist */ + int nexps; + checknext(ls, '='); + nexps = explist(ls, &e); + if (nexps != nvars) + adjust_assign(ls, nvars, nexps, &e); + else { + luaK_setoneret(ls->fs, &e); /* close last expression */ + luaK_storevar(ls->fs, &lh->v, &e); + return; /* avoid default */ + } + } + storevartop(ls->fs, &lh->v); /* default assignment */ +} + + +static int cond (LexState *ls) { + /* cond -> exp */ + expdesc v; + expr(ls, &v); /* read condition */ + if (v.k == VNIL) v.k = VFALSE; /* 'falses' are all equal here */ + luaK_goiftrue(ls->fs, &v); + return v.f; +} + + +static void gotostat (LexState *ls, int line) { + TString *name = str_checkname(ls); /* label's name */ + newgotoentry(ls, name, line); +} + + +/* +** Break statement. Semantically equivalent to "goto break". +*/ +static void breakstat (LexState *ls, int line) { + BlockCnt *bl; /* to look for an enclosing loop */ + for (bl = ls->fs->bl; bl != NULL; bl = bl->previous) { + if (bl->isloop) /* found one? */ + goto ok; + } + luaX_syntaxerror(ls, "break outside loop"); + ok: + bl->isloop = 2; /* signal that block has pending breaks */ + luaX_next(ls); /* skip break */ + newgotoentry(ls, ls->brkn, line); +} + + +/* +** Check whether there is already a label with the given 'name' at +** current function. +*/ +static void checkrepeated (LexState *ls, TString *name) { + Labeldesc *lb = findlabel(ls, name, ls->fs->firstlabel); + if (l_unlikely(lb != NULL)) /* already defined? */ + luaK_semerror(ls, "label '%s' already defined on line %d", + getstr(name), lb->line); /* error */ +} + + +static void labelstat (LexState *ls, TString *name, int line) { + /* label -> '::' NAME '::' */ + checknext(ls, TK_DBCOLON); /* skip double colon */ + while (ls->t.token == ';' || ls->t.token == TK_DBCOLON) + statement(ls); /* skip other no-op statements */ + checkrepeated(ls, name); /* check for repeated labels */ + createlabel(ls, name, line, block_follow(ls, 0)); +} + + +static void whilestat (LexState *ls, int line) { + /* whilestat -> WHILE cond DO block END */ + FuncState *fs = ls->fs; + int whileinit; + int condexit; + BlockCnt bl; + luaX_next(ls); /* skip WHILE */ + whileinit = luaK_getlabel(fs); + condexit = cond(ls); + enterblock(fs, &bl, 1); + checknext(ls, TK_DO); + block(ls); + luaK_jumpto(fs, whileinit); + check_match(ls, TK_END, TK_WHILE, line); + leaveblock(fs); + luaK_patchtohere(fs, condexit); /* false conditions finish the loop */ +} + + +static void repeatstat (LexState *ls, int line) { + /* repeatstat -> REPEAT block UNTIL cond */ + int condexit; + FuncState *fs = ls->fs; + int repeat_init = luaK_getlabel(fs); + BlockCnt bl1, bl2; + enterblock(fs, &bl1, 1); /* loop block */ + enterblock(fs, &bl2, 0); /* scope block */ + luaX_next(ls); /* skip REPEAT */ + statlist(ls); + check_match(ls, TK_UNTIL, TK_REPEAT, line); + condexit = cond(ls); /* read condition (inside scope block) */ + if (bl2.upval) { /* upvalues? */ + int exit = luaK_jump(fs); /* normal exit must jump over fix */ + luaK_patchtohere(fs, condexit); /* repetition must close upvalues */ + luaK_codeABC(fs, OP_CLOSE, reglevel(fs, bl2.nactvar), 0, 0); + condexit = luaK_jump(fs); /* repeat after closing upvalues */ + luaK_patchtohere(fs, exit); /* normal exit comes to here */ + } + luaK_patchlist(fs, condexit, repeat_init); /* close the loop */ + leaveblock(fs); /* finish scope */ + leaveblock(fs); /* finish loop */ +} + + +/* +** Read an expression and generate code to put its results in next +** stack slot. +** +*/ +static void exp1 (LexState *ls) { + expdesc e; + expr(ls, &e); + luaK_exp2nextreg(ls->fs, &e); + lua_assert(e.k == VNONRELOC); +} + + +/* +** Fix for instruction at position 'pc' to jump to 'dest'. +** (Jump addresses are relative in Lua). 'back' true means +** a back jump. +*/ +static void fixforjump (FuncState *fs, int pc, int dest, int back) { + Instruction *jmp = &fs->f->code[pc]; + int offset = dest - (pc + 1); + if (back) + offset = -offset; + if (l_unlikely(offset > MAXARG_Bx)) + luaX_syntaxerror(fs->ls, "control structure too long"); + SETARG_Bx(*jmp, offset); +} + + +/* +** Generate code for a 'for' loop. +*/ +static void forbody (LexState *ls, int base, int line, int nvars, int isgen) { + /* forbody -> DO block */ + static const OpCode forprep[2] = {OP_FORPREP, OP_TFORPREP}; + static const OpCode forloop[2] = {OP_FORLOOP, OP_TFORLOOP}; + BlockCnt bl; + FuncState *fs = ls->fs; + int prep, endfor; + checknext(ls, TK_DO); + prep = luaK_codeABx(fs, forprep[isgen], base, 0); + fs->freereg--; /* both 'forprep' remove one register from the stack */ + enterblock(fs, &bl, 0); /* scope for declared variables */ + adjustlocalvars(ls, nvars); + luaK_reserveregs(fs, nvars); + block(ls); + leaveblock(fs); /* end of scope for declared variables */ + fixforjump(fs, prep, luaK_getlabel(fs), 0); + if (isgen) { /* generic for? */ + luaK_codeABC(fs, OP_TFORCALL, base, 0, nvars); + luaK_fixline(fs, line); + } + endfor = luaK_codeABx(fs, forloop[isgen], base, 0); + fixforjump(fs, endfor, prep + 1, 1); + luaK_fixline(fs, line); +} + + +static void fornum (LexState *ls, TString *varname, int line) { + /* fornum -> NAME = exp,exp[,exp] forbody */ + FuncState *fs = ls->fs; + int base = fs->freereg; + new_localvarliteral(ls, "(for state)"); + new_localvarliteral(ls, "(for state)"); + new_varkind(ls, varname, RDKCONST); /* control variable */ + checknext(ls, '='); + exp1(ls); /* initial value */ + checknext(ls, ','); + exp1(ls); /* limit */ + if (testnext(ls, ',')) + exp1(ls); /* optional step */ + else { /* default step = 1 */ + luaK_int(fs, fs->freereg, 1); + luaK_reserveregs(fs, 1); + } + adjustlocalvars(ls, 2); /* start scope for internal variables */ + forbody(ls, base, line, 1, 0); +} + + +static void forlist (LexState *ls, TString *indexname) { + /* forlist -> NAME {,NAME} IN explist forbody */ + FuncState *fs = ls->fs; + expdesc e; + int nvars = 4; /* function, state, closing, control */ + int line; + int base = fs->freereg; + /* create internal variables */ + new_localvarliteral(ls, "(for state)"); /* iterator function */ + new_localvarliteral(ls, "(for state)"); /* state */ + new_localvarliteral(ls, "(for state)"); /* closing var. (after swap) */ + new_varkind(ls, indexname, RDKCONST); /* control variable */ + /* other declared variables */ + while (testnext(ls, ',')) { + new_localvar(ls, str_checkname(ls)); + nvars++; + } + checknext(ls, TK_IN); + line = ls->linenumber; + adjust_assign(ls, 4, explist(ls, &e), &e); + adjustlocalvars(ls, 3); /* start scope for internal variables */ + marktobeclosed(fs); /* last internal var. must be closed */ + luaK_checkstack(fs, 2); /* extra space to call iterator */ + forbody(ls, base, line, nvars - 3, 1); +} + + +static void forstat (LexState *ls, int line) { + /* forstat -> FOR (fornum | forlist) END */ + FuncState *fs = ls->fs; + TString *varname; + BlockCnt bl; + enterblock(fs, &bl, 1); /* scope for loop and control variables */ + luaX_next(ls); /* skip 'for' */ + varname = str_checkname(ls); /* first variable name */ + switch (ls->t.token) { + case '=': fornum(ls, varname, line); break; + case ',': case TK_IN: forlist(ls, varname); break; + default: luaX_syntaxerror(ls, "'=' or 'in' expected"); + } + check_match(ls, TK_END, TK_FOR, line); + leaveblock(fs); /* loop scope ('break' jumps to this point) */ +} + + +static void test_then_block (LexState *ls, int *escapelist) { + /* test_then_block -> [IF | ELSEIF] cond THEN block */ + FuncState *fs = ls->fs; + int condtrue; + luaX_next(ls); /* skip IF or ELSEIF */ + condtrue = cond(ls); /* read condition */ + checknext(ls, TK_THEN); + block(ls); /* 'then' part */ + if (ls->t.token == TK_ELSE || + ls->t.token == TK_ELSEIF) /* followed by 'else'/'elseif'? */ + luaK_concat(fs, escapelist, luaK_jump(fs)); /* must jump over it */ + luaK_patchtohere(fs, condtrue); +} + + +static void ifstat (LexState *ls, int line) { + /* ifstat -> IF cond THEN block {ELSEIF cond THEN block} [ELSE block] END */ + FuncState *fs = ls->fs; + int escapelist = NO_JUMP; /* exit list for finished parts */ + test_then_block(ls, &escapelist); /* IF cond THEN block */ + while (ls->t.token == TK_ELSEIF) + test_then_block(ls, &escapelist); /* ELSEIF cond THEN block */ + if (testnext(ls, TK_ELSE)) + block(ls); /* 'else' part */ + check_match(ls, TK_END, TK_IF, line); + luaK_patchtohere(fs, escapelist); /* patch escape list to 'if' end */ +} + + +static void localfunc (LexState *ls) { + expdesc b; + FuncState *fs = ls->fs; + int fvar = fs->nactvar; /* function's variable index */ + new_localvar(ls, str_checkname(ls)); /* new local variable */ + adjustlocalvars(ls, 1); /* enter its scope */ + body(ls, &b, 0, ls->linenumber); /* function created in next register */ + /* debug information will only see the variable after this point! */ + localdebuginfo(fs, fvar)->startpc = fs->pc; +} + + +static lu_byte getvarattribute (LexState *ls, lu_byte df) { + /* attrib -> ['<' NAME '>'] */ + if (testnext(ls, '<')) { + TString *ts = str_checkname(ls); + const char *attr = getstr(ts); + checknext(ls, '>'); + if (strcmp(attr, "const") == 0) + return RDKCONST; /* read-only variable */ + else if (strcmp(attr, "close") == 0) + return RDKTOCLOSE; /* to-be-closed variable */ + else + luaK_semerror(ls, "unknown attribute '%s'", attr); + } + return df; /* return default value */ +} + + +static void checktoclose (FuncState *fs, int level) { + if (level != -1) { /* is there a to-be-closed variable? */ + marktobeclosed(fs); + luaK_codeABC(fs, OP_TBC, reglevel(fs, level), 0, 0); + } +} + + +static void localstat (LexState *ls) { + /* stat -> LOCAL NAME attrib { ',' NAME attrib } ['=' explist] */ + FuncState *fs = ls->fs; + int toclose = -1; /* index of to-be-closed variable (if any) */ + Vardesc *var; /* last variable */ + int vidx; /* index of last variable */ + int nvars = 0; + int nexps; + expdesc e; + /* get prefixed attribute (if any); default is regular local variable */ + lu_byte defkind = getvarattribute(ls, VDKREG); + do { /* for each variable */ + TString *vname = str_checkname(ls); /* get its name */ + lu_byte kind = getvarattribute(ls, defkind); /* postfixed attribute */ + vidx = new_varkind(ls, vname, kind); /* predeclare it */ + if (kind == RDKTOCLOSE) { /* to-be-closed? */ + if (toclose != -1) /* one already present? */ + luaK_semerror(ls, "multiple to-be-closed variables in local list"); + toclose = fs->nactvar + nvars; + } + nvars++; + } while (testnext(ls, ',')); + if (testnext(ls, '=')) /* initialization? */ + nexps = explist(ls, &e); + else { + e.k = VVOID; + nexps = 0; + } + var = getlocalvardesc(fs, vidx); /* retrieve last variable */ + if (nvars == nexps && /* no adjustments? */ + var->vd.kind == RDKCONST && /* last variable is const? */ + luaK_exp2const(fs, &e, &var->k)) { /* compile-time constant? */ + var->vd.kind = RDKCTC; /* variable is a compile-time constant */ + adjustlocalvars(ls, nvars - 1); /* exclude last variable */ + fs->nactvar++; /* but count it */ + } + else { + adjust_assign(ls, nvars, nexps, &e); + adjustlocalvars(ls, nvars); + } + checktoclose(fs, toclose); +} + + +static lu_byte getglobalattribute (LexState *ls, lu_byte df) { + lu_byte kind = getvarattribute(ls, df); + switch (kind) { + case RDKTOCLOSE: + luaK_semerror(ls, "global variables cannot be to-be-closed"); + return kind; /* to avoid warnings */ + case RDKCONST: + return GDKCONST; /* adjust kind for global variable */ + default: + return kind; + } +} + + +static void checkglobal (LexState *ls, TString *varname, int line) { + FuncState *fs = ls->fs; + expdesc var; + int k; + buildglobal(ls, varname, &var); /* create global variable in 'var' */ + k = var.u.ind.keystr; /* index of global name in 'k' */ + luaK_codecheckglobal(fs, &var, k, line); +} + + +/* +** Recursively traverse list of globals to be initalized. When +** going, generate table description for the global. In the end, +** after all indices have been generated, read list of initializing +** expressions. When returning, generate the assignment of the value on +** the stack to the corresponding table description. 'n' is the variable +** being handled, range [0, nvars - 1]. +*/ +static void initglobal (LexState *ls, int nvars, int firstidx, int n, + int line) { + if (n == nvars) { /* traversed all variables? */ + expdesc e; + int nexps = explist(ls, &e); /* read list of expressions */ + adjust_assign(ls, nvars, nexps, &e); + } + else { /* handle variable 'n' */ + FuncState *fs = ls->fs; + expdesc var; + TString *varname = getlocalvardesc(fs, firstidx + n)->vd.name; + buildglobal(ls, varname, &var); /* create global variable in 'var' */ + enterlevel(ls); /* control recursion depth */ + initglobal(ls, nvars, firstidx, n + 1, line); + leavelevel(ls); + checkglobal(ls, varname, line); + storevartop(fs, &var); + } +} + + +static void globalnames (LexState *ls, lu_byte defkind) { + FuncState *fs = ls->fs; + int nvars = 0; + int lastidx; /* index of last registered variable */ + do { /* for each name */ + TString *vname = str_checkname(ls); + lu_byte kind = getglobalattribute(ls, defkind); + lastidx = new_varkind(ls, vname, kind); + nvars++; + } while (testnext(ls, ',')); + if (testnext(ls, '=')) /* initialization? */ + initglobal(ls, nvars, lastidx - nvars + 1, 0, ls->linenumber); + fs->nactvar = cast_short(fs->nactvar + nvars); /* activate declaration */ +} + + +static void globalstat (LexState *ls) { + /* globalstat -> (GLOBAL) attrib '*' + globalstat -> (GLOBAL) attrib NAME attrib {',' NAME attrib} */ + FuncState *fs = ls->fs; + /* get prefixed attribute (if any); default is regular global variable */ + lu_byte defkind = getglobalattribute(ls, GDKREG); + if (!testnext(ls, '*')) + globalnames(ls, defkind); + else { + /* use NULL as name to represent '*' entries */ + new_varkind(ls, NULL, defkind); + fs->nactvar++; /* activate declaration */ + } +} + + +static void globalfunc (LexState *ls, int line) { + /* globalfunc -> (GLOBAL FUNCTION) NAME body */ + expdesc var, b; + FuncState *fs = ls->fs; + TString *fname = str_checkname(ls); + new_varkind(ls, fname, GDKREG); /* declare global variable */ + fs->nactvar++; /* enter its scope */ + buildglobal(ls, fname, &var); + body(ls, &b, 0, ls->linenumber); /* compile and return closure in 'b' */ + checkglobal(ls, fname, line); + luaK_storevar(fs, &var, &b); + luaK_fixline(fs, line); /* definition "happens" in the first line */ +} + + +static void globalstatfunc (LexState *ls, int line) { + /* stat -> GLOBAL globalfunc | GLOBAL globalstat */ + luaX_next(ls); /* skip 'global' */ + if (testnext(ls, TK_FUNCTION)) + globalfunc(ls, line); + else + globalstat(ls); +} + + +static int funcname (LexState *ls, expdesc *v) { + /* funcname -> NAME {fieldsel} [':' NAME] */ + int ismethod = 0; + singlevar(ls, v); + while (ls->t.token == '.') + fieldsel(ls, v); + if (ls->t.token == ':') { + ismethod = 1; + fieldsel(ls, v); + } + return ismethod; +} + + +static void funcstat (LexState *ls, int line) { + /* funcstat -> FUNCTION funcname body */ + int ismethod; + expdesc v, b; + luaX_next(ls); /* skip FUNCTION */ + ismethod = funcname(ls, &v); + check_readonly(ls, &v); + body(ls, &b, ismethod, line); + luaK_storevar(ls->fs, &v, &b); + luaK_fixline(ls->fs, line); /* definition "happens" in the first line */ +} + + +static void exprstat (LexState *ls) { + /* stat -> func | assignment */ + FuncState *fs = ls->fs; + struct LHS_assign v; + suffixedexp(ls, &v.v); + if (ls->t.token == '=' || ls->t.token == ',') { /* stat -> assignment ? */ + v.prev = NULL; + restassign(ls, &v, 1); + } + else { /* stat -> func */ + Instruction *inst; + check_condition(ls, v.v.k == VCALL, "syntax error"); + inst = &getinstruction(fs, &v.v); + SETARG_C(*inst, 1); /* call statement uses no results */ + } +} + + +static void retstat (LexState *ls) { + /* stat -> RETURN [explist] [';'] */ + FuncState *fs = ls->fs; + expdesc e; + int nret; /* number of values being returned */ + int first = luaY_nvarstack(fs); /* first slot to be returned */ + if (block_follow(ls, 1) || ls->t.token == ';') + nret = 0; /* return no values */ + else { + nret = explist(ls, &e); /* optional return values */ + if (hasmultret(e.k)) { + luaK_setmultret(fs, &e); + if (e.k == VCALL && nret == 1 && !fs->bl->insidetbc) { /* tail call? */ + SET_OPCODE(getinstruction(fs,&e), OP_TAILCALL); + lua_assert(GETARG_A(getinstruction(fs,&e)) == luaY_nvarstack(fs)); + } + nret = LUA_MULTRET; /* return all values */ + } + else { + if (nret == 1) /* only one single value? */ + first = luaK_exp2anyreg(fs, &e); /* can use original slot */ + else { /* values must go to the top of the stack */ + luaK_exp2nextreg(fs, &e); + lua_assert(nret == fs->freereg - first); + } + } + } + luaK_ret(fs, first, nret); + testnext(ls, ';'); /* skip optional semicolon */ +} + + +static void statement (LexState *ls) { + int line = ls->linenumber; /* may be needed for error messages */ + enterlevel(ls); + switch (ls->t.token) { + case ';': { /* stat -> ';' (empty statement) */ + luaX_next(ls); /* skip ';' */ + break; + } + case TK_IF: { /* stat -> ifstat */ + ifstat(ls, line); + break; + } + case TK_WHILE: { /* stat -> whilestat */ + whilestat(ls, line); + break; + } + case TK_DO: { /* stat -> DO block END */ + luaX_next(ls); /* skip DO */ + block(ls); + check_match(ls, TK_END, TK_DO, line); + break; + } + case TK_FOR: { /* stat -> forstat */ + forstat(ls, line); + break; + } + case TK_REPEAT: { /* stat -> repeatstat */ + repeatstat(ls, line); + break; + } + case TK_FUNCTION: { /* stat -> funcstat */ + funcstat(ls, line); + break; + } + case TK_LOCAL: { /* stat -> localstat */ + luaX_next(ls); /* skip LOCAL */ + if (testnext(ls, TK_FUNCTION)) /* local function? */ + localfunc(ls); + else + localstat(ls); + break; + } + case TK_GLOBAL: { /* stat -> globalstatfunc */ + globalstatfunc(ls, line); + break; + } + case TK_DBCOLON: { /* stat -> label */ + luaX_next(ls); /* skip double colon */ + labelstat(ls, str_checkname(ls), line); + break; + } + case TK_RETURN: { /* stat -> retstat */ + luaX_next(ls); /* skip RETURN */ + retstat(ls); + break; + } + case TK_BREAK: { /* stat -> breakstat */ + breakstat(ls, line); + break; + } + case TK_GOTO: { /* stat -> 'goto' NAME */ + luaX_next(ls); /* skip 'goto' */ + gotostat(ls, line); + break; + } +#if LUA_COMPAT_GLOBAL + case TK_NAME: { + /* compatibility code to parse global keyword when "global" + is not reserved */ + if (ls->t.seminfo.ts == ls->glbn) { /* current = "global"? */ + int lk = luaX_lookahead(ls); + if (lk == '<' || lk == TK_NAME || lk == '*' || lk == TK_FUNCTION) { + /* 'global ' or 'global name' or 'global *' or + 'global function' */ + globalstatfunc(ls, line); + break; + } + } /* else... */ + } +#endif + /* FALLTHROUGH */ + default: { /* stat -> func | assignment */ + exprstat(ls); + break; + } + } + lua_assert(ls->fs->f->maxstacksize >= ls->fs->freereg && + ls->fs->freereg >= luaY_nvarstack(ls->fs)); + ls->fs->freereg = luaY_nvarstack(ls->fs); /* free registers */ + leavelevel(ls); +} + +/* }====================================================================== */ + +/* }====================================================================== */ + + +/* +** compiles the main function, which is a regular vararg function with an +** upvalue named LUA_ENV +*/ +static void mainfunc (LexState *ls, FuncState *fs) { + BlockCnt bl; + Upvaldesc *env; + open_func(ls, fs, &bl); + setvararg(fs); /* main function is always vararg */ + env = allocupvalue(fs); /* ...set environment upvalue */ + env->instack = 1; + env->idx = 0; + env->kind = VDKREG; + env->name = ls->envn; + luaC_objbarrier(ls->L, fs->f, env->name); + luaX_next(ls); /* read first token */ + statlist(ls); /* parse main body */ + check(ls, TK_EOS); + close_func(ls); +} + + +LClosure *luaY_parser (lua_State *L, ZIO *z, Table *anchor, Mbuffer *buff, + Dyndata *dyd, const char *name, int firstchar) { + LexState lexstate; + FuncState funcstate; + LClosure *cl; + lexstate.h = anchor; /* table for scanner */ + cl = luaF_newLclosure(L, 1); /* create main closure */ + luaD_anchorobj(L, anchor, obj2gco(cl)); /* anchor it in scanner table */ + funcstate.f = cl->p = luaF_newproto(L); + luaC_objbarrier(L, cl, cl->p); + funcstate.f->source = luaS_new(L, name); /* create and anchor TString */ + luaC_objbarrier(L, funcstate.f, funcstate.f->source); + lexstate.buff = buff; + lexstate.dyd = dyd; + dyd->actvar.n = dyd->gt.n = dyd->label.n = 0; + luaX_setinput(L, &lexstate, z, funcstate.f->source, firstchar); + mainfunc(&lexstate, &funcstate); + lua_assert(!funcstate.prev && funcstate.nups == 1 && !lexstate.fs); + /* all scopes should be correctly finished */ + lua_assert(dyd->actvar.n == 0 && dyd->gt.n == 0 && dyd->label.n == 0); + return cl; +} diff --git a/crates/lua/vendor/lua/lparser.h b/crates/lua/vendor/lua/lparser.h new file mode 100644 index 0000000..4fad6bd --- /dev/null +++ b/crates/lua/vendor/lua/lparser.h @@ -0,0 +1,197 @@ +/* +** $Id: lparser.h $ +** Lua Parser +** See Copyright Notice in lua.h +*/ + +#ifndef lparser_h +#define lparser_h + +#include "llimits.h" +#include "lobject.h" +#include "lzio.h" + + +/* +** Expression and variable descriptor. +** Code generation for variables and expressions can be delayed to allow +** optimizations; An 'expdesc' structure describes a potentially-delayed +** variable/expression. It has a description of its "main" value plus a +** list of conditional jumps that can also produce its value (generated +** by short-circuit operators 'and'/'or'). +*/ + +/* kinds of variables/expressions */ +typedef enum { + VVOID, /* when 'expdesc' describes the last expression of a list, + this kind means an empty list (so, no expression) */ + VNIL, /* constant nil */ + VTRUE, /* constant true */ + VFALSE, /* constant false */ + VK, /* constant in 'k'; info = index of constant in 'k' */ + VKFLT, /* floating constant; nval = numerical float value */ + VKINT, /* integer constant; ival = numerical integer value */ + VKSTR, /* string constant; strval = TString address; + (string is fixed by the scanner) */ + VNONRELOC, /* expression has its value in a fixed register; + info = result register */ + VLOCAL, /* local variable; var.ridx = register index; + var.vidx = relative index in 'actvar.arr' */ + VVARGVAR, /* vararg parameter; var.ridx = register index; + var.vidx = relative index in 'actvar.arr' */ + VGLOBAL, /* global variable; + info = relative index in 'actvar.arr' (or -1 for + implicit declaration) */ + VUPVAL, /* upvalue variable; info = index of upvalue in 'upvalues' */ + VCONST, /* compile-time variable; + info = absolute index in 'actvar.arr' */ + VINDEXED, /* indexed variable; + ind.t = table register; + ind.idx = key's R index; + ind.ro = true if it represents a read-only global; + ind.keystr = if key is a string, index in 'k' of that string; + -1 if key is not a string */ + VVARGIND, /* indexed vararg parameter; + ind.* as in VINDEXED */ + VINDEXUP, /* indexed upvalue; + ind.idx = key's K index; + ind.* as in VINDEXED */ + VINDEXI, /* indexed variable with constant integer; + ind.t = table register; + ind.idx = key's value */ + VINDEXSTR, /* indexed variable with literal string; + ind.idx = key's K index; + ind.* as in VINDEXED */ + VJMP, /* expression is a test/comparison; + info = pc of corresponding jump instruction */ + VRELOC, /* expression can put result in any register; + info = instruction pc */ + VCALL, /* expression is a function call; info = instruction pc */ + VVARARG /* vararg expression; info = instruction pc */ +} expkind; + + +#define vkisvar(k) (VLOCAL <= (k) && (k) <= VINDEXSTR) +#define vkisindexed(k) (VINDEXED <= (k) && (k) <= VINDEXSTR) + + +typedef struct expdesc { + expkind k; + union { + lua_Integer ival; /* for VKINT */ + lua_Number nval; /* for VKFLT */ + TString *strval; /* for VKSTR */ + int info; /* for generic use */ + struct { /* for indexed variables */ + short idx; /* index (R or "long" K) */ + lu_byte t; /* table (register or upvalue) */ + lu_byte ro; /* true if variable is read-only */ + int keystr; /* index in 'k' of string key, or -1 if not a string */ + } ind; + struct { /* for local variables */ + lu_byte ridx; /* register holding the variable */ + short vidx; /* index in 'actvar.arr' */ + } var; + } u; + int t; /* patch list of 'exit when true' */ + int f; /* patch list of 'exit when false' */ +} expdesc; + + +/* kinds of variables */ +#define VDKREG 0 /* regular local */ +#define RDKCONST 1 /* local constant */ +#define RDKVAVAR 2 /* vararg parameter */ +#define RDKTOCLOSE 3 /* to-be-closed */ +#define RDKCTC 4 /* local compile-time constant */ +#define GDKREG 5 /* regular global */ +#define GDKCONST 6 /* global constant */ + +/* variables that live in registers */ +#define varinreg(v) ((v)->vd.kind <= RDKTOCLOSE) + +/* test for global variables */ +#define varglobal(v) ((v)->vd.kind >= GDKREG) + + +/* description of an active variable */ +typedef union Vardesc { + struct { + TValuefields; /* constant value (if it is a compile-time constant) */ + lu_byte kind; + lu_byte ridx; /* register holding the variable */ + short pidx; /* index of the variable in the Proto's 'locvars' array */ + TString *name; /* variable name */ + } vd; + TValue k; /* constant value (if any) */ +} Vardesc; + + + +/* description of pending goto statements and label statements */ +typedef struct Labeldesc { + TString *name; /* label identifier */ + int pc; /* position in code */ + int line; /* line where it appeared */ + short nactvar; /* number of active variables in that position */ + lu_byte close; /* true for goto that escapes upvalues */ +} Labeldesc; + + +/* list of labels or gotos */ +typedef struct Labellist { + Labeldesc *arr; /* array */ + int n; /* number of entries in use */ + int size; /* array size */ +} Labellist; + + +/* dynamic structures used by the parser */ +typedef struct Dyndata { + struct { /* list of all active local variables */ + Vardesc *arr; + int n; + int size; + } actvar; + Labellist gt; /* list of pending gotos */ + Labellist label; /* list of active labels */ +} Dyndata; + + +/* control of blocks */ +struct BlockCnt; /* defined in lparser.c */ + + +/* state needed to generate code for a given function */ +typedef struct FuncState { + Proto *f; /* current function header */ + struct FuncState *prev; /* enclosing function */ + struct LexState *ls; /* lexical state */ + struct BlockCnt *bl; /* chain of current blocks */ + Table *kcache; /* cache for reusing constants */ + int pc; /* next position to code (equivalent to 'ncode') */ + int lasttarget; /* 'label' of last 'jump label' */ + int previousline; /* last line that was saved in 'lineinfo' */ + int nk; /* number of elements in 'k' */ + int np; /* number of elements in 'p' */ + int nabslineinfo; /* number of elements in 'abslineinfo' */ + int firstlocal; /* index of first local var (in Dyndata array) */ + int firstlabel; /* index of first label (in 'dyd->label->arr') */ + short ndebugvars; /* number of elements in 'f->locvars' */ + short nactvar; /* number of active variable declarations */ + lu_byte nups; /* number of upvalues */ + lu_byte freereg; /* first free register */ + lu_byte iwthabs; /* instructions issued since last absolute line info */ + lu_byte needclose; /* function needs to close upvalues when returning */ +} FuncState; + + +LUAI_FUNC lu_byte luaY_nvarstack (FuncState *fs); +LUAI_FUNC void luaY_checklimit (FuncState *fs, int v, int l, + const char *what); +LUAI_FUNC LClosure *luaY_parser (lua_State *L, ZIO *z, Table *anchor, + Mbuffer *buff, Dyndata *dyd, + const char *name, int firstchar); + + +#endif diff --git a/crates/lua/vendor/lua/lprefix.h b/crates/lua/vendor/lua/lprefix.h new file mode 100644 index 0000000..93e1ea7 --- /dev/null +++ b/crates/lua/vendor/lua/lprefix.h @@ -0,0 +1,44 @@ +/* +** $Id: lprefix.h $ +** Definitions for Lua code that must come before any other header file +** See Copyright Notice in lua.h +*/ + +#ifndef lprefix_h +#define lprefix_h + + +/* +** Allows POSIX/XSI stuff +*/ +#if !defined(LUA_USE_C89) /* { */ + +#if !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 600 +#elif _XOPEN_SOURCE == 0 +#undef _XOPEN_SOURCE /* use -D_XOPEN_SOURCE=0 to undefine it */ +#endif + +/* +** Allows manipulation of large files in gcc and some other compilers +*/ +#if !defined(LUA_32BITS) && !defined(_FILE_OFFSET_BITS) +#define _LARGEFILE_SOURCE 1 +#define _FILE_OFFSET_BITS 64 +#endif + +#endif /* } */ + + +/* +** Windows stuff +*/ +#if defined(_WIN32) /* { */ + +#if !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS /* avoid warnings about ISO C functions */ +#endif + +#endif /* } */ + +#endif diff --git a/crates/lua/vendor/lua/lstate.c b/crates/lua/vendor/lua/lstate.c new file mode 100644 index 0000000..f0d500c --- /dev/null +++ b/crates/lua/vendor/lua/lstate.c @@ -0,0 +1,424 @@ +/* +** $Id: lstate.c $ +** Global State +** See Copyright Notice in lua.h +*/ + +#define lstate_c +#define LUA_CORE + +#include "lprefix.h" + + +#include +#include + +#include "lua.h" + +#include "lapi.h" +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lgc.h" +#include "llex.h" +#include "lmem.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" + + + +#define fromstate(L) (cast(LX *, cast(lu_byte *, (L)) - offsetof(LX, l))) + + +/* +** these macros allow user-specific actions when a thread is +** created/deleted +*/ +#if !defined(luai_userstateopen) +#define luai_userstateopen(L) ((void)L) +#endif + +#if !defined(luai_userstateclose) +#define luai_userstateclose(L) ((void)L) +#endif + +#if !defined(luai_userstatethread) +#define luai_userstatethread(L,L1) ((void)L) +#endif + +#if !defined(luai_userstatefree) +#define luai_userstatefree(L,L1) ((void)L) +#endif + + +/* +** set GCdebt to a new value keeping the real number of allocated +** objects (GCtotalobjs - GCdebt) invariant and avoiding overflows in +** 'GCtotalobjs'. +*/ +void luaE_setdebt (global_State *g, l_mem debt) { + l_mem tb = gettotalbytes(g); + lua_assert(tb > 0); + if (debt > MAX_LMEM - tb) + debt = MAX_LMEM - tb; /* will make GCtotalbytes == MAX_LMEM */ + g->GCtotalbytes = tb + debt; + g->GCdebt = debt; +} + + +CallInfo *luaE_extendCI (lua_State *L, int err) { + CallInfo *ci; + ci = luaM_reallocvector(L, NULL, 0, 1, CallInfo); + if (l_unlikely(ci == NULL)) { /* allocation failed? */ + if (err) + luaM_error(L); /* raise the error */ + return NULL; /* else only report it */ + } + ci->next = L->ci->next; + ci->previous = L->ci; + L->ci->next = ci; + if (ci->next) + ci->next->previous = ci; + ci->u.l.trap = 0; + L->nci++; + return ci; +} + + +/* +** free all CallInfo structures not in use by a thread +*/ +static void freeCI (lua_State *L) { + CallInfo *ci = L->ci; + CallInfo *next = ci->next; + ci->next = NULL; + while ((ci = next) != NULL) { + next = ci->next; + luaM_free(L, ci); + L->nci--; + } +} + + +/* +** free half of the CallInfo structures not in use by a thread, +** keeping the first one. +*/ +void luaE_shrinkCI (lua_State *L) { + CallInfo *ci = L->ci->next; /* first free CallInfo */ + CallInfo *next; + if (ci == NULL) + return; /* no extra elements */ + while ((next = ci->next) != NULL) { /* two extra elements? */ + CallInfo *next2 = next->next; /* next's next */ + ci->next = next2; /* remove next from the list */ + L->nci--; + luaM_free(L, next); /* free next */ + if (next2 == NULL) + break; /* no more elements */ + else { + next2->previous = ci; + ci = next2; /* continue */ + } + } +} + + +/* +** Called when 'getCcalls(L)' larger or equal to LUAI_MAXCCALLS. +** If equal, raises an overflow error. If value is larger than +** LUAI_MAXCCALLS (which means it is handling an overflow) but +** not much larger, does not report an error (to allow overflow +** handling to work). +*/ +void luaE_checkcstack (lua_State *L) { + if (getCcalls(L) == LUAI_MAXCCALLS) + luaG_runerror(L, "C stack overflow"); + else if (getCcalls(L) >= (LUAI_MAXCCALLS / 10 * 11)) + luaD_errerr(L); /* error while handling stack error */ +} + + +LUAI_FUNC void luaE_incCstack (lua_State *L) { + L->nCcalls++; + if (l_unlikely(getCcalls(L) >= LUAI_MAXCCALLS)) + luaE_checkcstack(L); +} + + +static void resetCI (lua_State *L) { + CallInfo *ci = L->ci = &L->base_ci; + ci->func.p = L->stack.p; + setnilvalue2s(ci->func.p); /* 'function' entry for basic 'ci' */ + ci->top.p = ci->func.p + 1 + LUA_MINSTACK; /* +1 for 'function' entry */ + ci->u.c.k = NULL; + ci->callstatus = CIST_C; + L->status = LUA_OK; + L->errfunc = 0; /* stack unwind can "throw away" the error function */ +} + + +static void stack_init (lua_State *L1, lua_State *L) { + int i; + /* initialize stack array */ + L1->stack.p = luaM_newvector(L, BASIC_STACK_SIZE + EXTRA_STACK, StackValue); + L1->tbclist.p = L1->stack.p; + for (i = 0; i < BASIC_STACK_SIZE + EXTRA_STACK; i++) + setnilvalue2s(L1->stack.p + i); /* erase new stack */ + L1->stack_last.p = L1->stack.p + BASIC_STACK_SIZE; + /* initialize first ci */ + resetCI(L1); + L1->top.p = L1->stack.p + 1; /* +1 for 'function' entry */ +} + + +static void freestack (lua_State *L) { + if (L->stack.p == NULL) + return; /* stack not completely built yet */ + L->ci = &L->base_ci; /* free the entire 'ci' list */ + freeCI(L); + lua_assert(L->nci == 0); + /* free stack */ + luaM_freearray(L, L->stack.p, cast_sizet(stacksize(L) + EXTRA_STACK)); +} + + +/* +** Create registry table and its predefined values +*/ +static void init_registry (lua_State *L, global_State *g) { + /* create registry */ + TValue aux; + Table *registry = luaH_new(L); + sethvalue(L, &g->l_registry, registry); + luaH_resize(L, registry, LUA_RIDX_LAST, 0); + /* registry[1] = false */ + setbfvalue(&aux); + luaH_setint(L, registry, 1, &aux); + /* registry[LUA_RIDX_MAINTHREAD] = L */ + setthvalue(L, &aux, L); + luaH_setint(L, registry, LUA_RIDX_MAINTHREAD, &aux); + /* registry[LUA_RIDX_GLOBALS] = new table (table of globals) */ + sethvalue(L, &aux, luaH_new(L)); + luaH_setint(L, registry, LUA_RIDX_GLOBALS, &aux); +} + + +/* +** open parts of the state that may cause memory-allocation errors. +*/ +static void f_luaopen (lua_State *L, void *ud) { + global_State *g = G(L); + UNUSED(ud); + stack_init(L, L); /* init stack */ + init_registry(L, g); + luaS_init(L); + luaT_init(L); + luaX_init(L); + g->gcstp = 0; /* allow gc */ + setnilvalue(&g->nilvalue); /* now state is complete */ + luai_userstateopen(L); +} + + +/* +** preinitialize a thread with consistent values without allocating +** any memory (to avoid errors) +*/ +static void preinit_thread (lua_State *L, global_State *g) { + G(L) = g; + L->stack.p = NULL; + L->ci = NULL; + L->nci = 0; + L->twups = L; /* thread has no upvalues */ + L->nCcalls = 0; + L->errorJmp = NULL; + L->hook = NULL; + L->hookmask = 0; + L->basehookcount = 0; + L->allowhook = 1; + resethookcount(L); + L->openupval = NULL; + L->status = LUA_OK; + L->errfunc = 0; + L->oldpc = 0; + L->base_ci.previous = L->base_ci.next = NULL; +} + + +lu_mem luaE_threadsize (lua_State *L) { + lu_mem sz = cast(lu_mem, sizeof(LX)) + + cast_uint(L->nci) * sizeof(CallInfo); + if (L->stack.p != NULL) + sz += cast_uint(stacksize(L) + EXTRA_STACK) * sizeof(StackValue); + return sz; +} + + +static void close_state (lua_State *L) { + global_State *g = G(L); + if (!completestate(g)) /* closing a partially built state? */ + luaC_freeallobjects(L); /* just collect its objects */ + else { /* closing a fully built state */ + resetCI(L); + luaD_closeprotected(L, 1, LUA_OK); /* close all upvalues */ + L->top.p = L->stack.p + 1; /* empty the stack to run finalizers */ + luaC_freeallobjects(L); /* collect all objects */ + luai_userstateclose(L); + } + luaM_freearray(L, G(L)->strt.hash, cast_sizet(G(L)->strt.size)); + freestack(L); + lua_assert(gettotalbytes(g) == sizeof(global_State)); + (*g->frealloc)(g->ud, g, sizeof(global_State), 0); /* free main block */ +} + + +LUA_API lua_State *lua_newthread (lua_State *L) { + global_State *g = G(L); + GCObject *o; + lua_State *L1; + lua_lock(L); + luaC_checkGC(L); + /* create new thread */ + o = luaC_newobjdt(L, LUA_TTHREAD, sizeof(LX), offsetof(LX, l)); + L1 = gco2th(o); + /* anchor it on L stack */ + setthvalue2s(L, L->top.p, L1); + api_incr_top(L); + preinit_thread(L1, g); + L1->hookmask = L->hookmask; + L1->basehookcount = L->basehookcount; + L1->hook = L->hook; + resethookcount(L1); + /* initialize L1 extra space */ + memcpy(lua_getextraspace(L1), lua_getextraspace(mainthread(g)), + LUA_EXTRASPACE); + luai_userstatethread(L, L1); + stack_init(L1, L); /* init stack */ + lua_unlock(L); + return L1; +} + + +void luaE_freethread (lua_State *L, lua_State *L1) { + LX *l = fromstate(L1); + luaF_closeupval(L1, L1->stack.p); /* close all upvalues */ + lua_assert(L1->openupval == NULL); + luai_userstatefree(L, L1); + freestack(L1); + luaM_free(L, l); +} + + +TStatus luaE_resetthread (lua_State *L, TStatus status) { + resetCI(L); + if (status == LUA_YIELD) + status = LUA_OK; + status = luaD_closeprotected(L, 1, status); + if (status != LUA_OK) /* errors? */ + luaD_seterrorobj(L, status, L->stack.p + 1); + else + L->top.p = L->stack.p + 1; + luaD_reallocstack(L, cast_int(L->ci->top.p - L->stack.p), 0); + return status; +} + + +LUA_API int lua_closethread (lua_State *L, lua_State *from) { + TStatus status; + lua_lock(L); + L->nCcalls = (from) ? getCcalls(from) : 0; + status = luaE_resetthread(L, L->status); + if (L == from) /* closing itself? */ + luaD_throwbaselevel(L, status); + lua_unlock(L); + return APIstatus(status); +} + + +LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud, unsigned seed) { + int i; + lua_State *L; + global_State *g = cast(global_State*, + (*f)(ud, NULL, LUA_TTHREAD, sizeof(global_State))); + if (g == NULL) return NULL; + L = &g->mainth.l; + L->tt = LUA_VTHREAD; + g->currentwhite = bitmask(WHITE0BIT); + L->marked = luaC_white(g); + preinit_thread(L, g); + g->allgc = obj2gco(L); /* by now, only object is the main thread */ + L->next = NULL; + incnny(L); /* main thread is always non yieldable */ + g->frealloc = f; + g->ud = ud; + g->warnf = NULL; + g->ud_warn = NULL; + g->seed = seed; + g->gcstp = GCSTPGC; /* no GC while building state */ + g->strt.size = g->strt.nuse = 0; + g->strt.hash = NULL; + setnilvalue(&g->l_registry); + g->panic = NULL; + g->gcstate = GCSpause; + g->gckind = KGC_INC; + g->gcstopem = 0; + g->gcemergency = 0; + g->finobj = g->tobefnz = g->fixedgc = NULL; + g->firstold1 = g->survival = g->old1 = g->reallyold = NULL; + g->finobjsur = g->finobjold1 = g->finobjrold = NULL; + g->sweepgc = NULL; + g->gray = g->grayagain = NULL; + g->weak = g->ephemeron = g->allweak = NULL; + g->twups = NULL; + g->GCtotalbytes = sizeof(global_State); + g->GCmarked = 0; + g->GCdebt = 0; + setivalue(&g->nilvalue, 0); /* to signal that state is not yet built */ + setgcparam(g, PAUSE, LUAI_GCPAUSE); + setgcparam(g, STEPMUL, LUAI_GCMUL); + setgcparam(g, STEPSIZE, LUAI_GCSTEPSIZE); + setgcparam(g, MINORMUL, LUAI_GENMINORMUL); + setgcparam(g, MINORMAJOR, LUAI_MINORMAJOR); + setgcparam(g, MAJORMINOR, LUAI_MAJORMINOR); + for (i=0; i < LUA_NUMTYPES; i++) g->mt[i] = NULL; + if (luaD_rawrunprotected(L, f_luaopen, NULL) != LUA_OK) { + /* memory allocation error: free partial state */ + close_state(L); + L = NULL; + } + return L; +} + + +LUA_API void lua_close (lua_State *L) { + lua_lock(L); + L = mainthread(G(L)); /* only the main thread can be closed */ + close_state(L); +} + + +void luaE_warning (lua_State *L, const char *msg, int tocont) { + lua_WarnFunction wf = G(L)->warnf; + if (wf != NULL) + wf(G(L)->ud_warn, msg, tocont); +} + + +/* +** Generate a warning from an error message +*/ +void luaE_warnerror (lua_State *L, const char *where) { + TValue *errobj = s2v(L->top.p - 1); /* error object */ + const char *msg = (ttisstring(errobj)) + ? getstr(tsvalue(errobj)) + : "error object is not a string"; + /* produce warning "error in %s (%s)" (where, msg) */ + luaE_warning(L, "error in ", 1); + luaE_warning(L, where, 1); + luaE_warning(L, " (", 1); + luaE_warning(L, msg, 1); + luaE_warning(L, ")", 0); +} diff --git a/crates/lua/vendor/lua/lstate.h b/crates/lua/vendor/lua/lstate.h new file mode 100644 index 0000000..d8a5219 --- /dev/null +++ b/crates/lua/vendor/lua/lstate.h @@ -0,0 +1,450 @@ +/* +** $Id: lstate.h $ +** Global State +** See Copyright Notice in lua.h +*/ + +#ifndef lstate_h +#define lstate_h + +#include "lua.h" + + +/* Some header files included here need this definition */ +typedef struct CallInfo CallInfo; + + +#include "lobject.h" +#include "ltm.h" +#include "lzio.h" + + +/* +** Some notes about garbage-collected objects: All objects in Lua must +** be kept somehow accessible until being freed, so all objects always +** belong to one (and only one) of these lists, using field 'next' of +** the 'CommonHeader' for the link: +** +** 'allgc': all objects not marked for finalization; +** 'finobj': all objects marked for finalization; +** 'tobefnz': all objects ready to be finalized; +** 'fixedgc': all objects that are not to be collected (currently +** only small strings, such as reserved words). +** +** For the generational collector, some of these lists have marks for +** generations. Each mark points to the first element in the list for +** that particular generation; that generation goes until the next mark. +** +** 'allgc' -> 'survival': new objects; +** 'survival' -> 'old': objects that survived one collection; +** 'old1' -> 'reallyold': objects that became old in last collection; +** 'reallyold' -> NULL: objects old for more than one cycle. +** +** 'finobj' -> 'finobjsur': new objects marked for finalization; +** 'finobjsur' -> 'finobjold1': survived """"; +** 'finobjold1' -> 'finobjrold': just old """"; +** 'finobjrold' -> NULL: really old """". +** +** All lists can contain elements older than their main ages, due +** to 'luaC_checkfinalizer' and 'udata2finalize', which move +** objects between the normal lists and the "marked for finalization" +** lists. Moreover, barriers can age young objects in young lists as +** OLD0, which then become OLD1. However, a list never contains +** elements younger than their main ages. +** +** The generational collector also uses a pointer 'firstold1', which +** points to the first OLD1 object in the list. It is used to optimize +** 'markold'. (Potentially OLD1 objects can be anywhere between 'allgc' +** and 'reallyold', but often the list has no OLD1 objects or they are +** after 'old1'.) Note the difference between it and 'old1': +** 'firstold1': no OLD1 objects before this point; there can be all +** ages after it. +** 'old1': no objects younger than OLD1 after this point. +*/ + +/* +** Moreover, there is another set of lists that control gray objects. +** These lists are linked by fields 'gclist'. (All objects that +** can become gray have such a field. The field is not the same +** in all objects, but it always has this name.) Any gray object +** must belong to one of these lists, and all objects in these lists +** must be gray (with two exceptions explained below): +** +** 'gray': regular gray objects, still waiting to be visited. +** 'grayagain': objects that must be revisited at the atomic phase. +** That includes +** - black objects got in a write barrier; +** - all kinds of weak tables during propagation phase; +** - all threads. +** 'weak': tables with weak values to be cleared; +** 'ephemeron': ephemeron tables with white->white entries; +** 'allweak': tables with weak keys and/or weak values to be cleared. +** +** The exceptions to that "gray rule" are: +** - TOUCHED2 objects in generational mode stay in a gray list (because +** they must be visited again at the end of the cycle), but they are +** marked black because assignments to them must activate barriers (to +** move them back to TOUCHED1). +** - Open upvalues are kept gray to avoid barriers, but they stay out +** of gray lists. (They don't even have a 'gclist' field.) +*/ + + + +/* +** About 'nCcalls': This count has two parts: the lower 16 bits counts +** the number of recursive invocations in the C stack; the higher +** 16 bits counts the number of non-yieldable calls in the stack. +** (They are together so that we can change and save both with one +** instruction.) +*/ + + +/* true if this thread does not have non-yieldable calls in the stack */ +#define yieldable(L) (((L)->nCcalls & 0xffff0000) == 0) + +/* real number of C calls */ +#define getCcalls(L) ((L)->nCcalls & 0xffff) + + +/* Increment the number of non-yieldable calls */ +#define incnny(L) ((L)->nCcalls += 0x10000) + +/* Decrement the number of non-yieldable calls */ +#define decnny(L) ((L)->nCcalls -= 0x10000) + +/* Non-yieldable call increment */ +#define nyci (0x10000 | 1) + + + + +struct lua_longjmp; /* defined in ldo.c */ + + +/* +** Atomic type (relative to signals) to better ensure that 'lua_sethook' +** is thread safe +*/ +#if !defined(l_signalT) +#include +#define l_signalT sig_atomic_t +#endif + + +/* +** Extra stack space to handle TM calls and some other extras. This +** space is not included in 'stack_last'. It is used only to avoid stack +** checks, either because the element will be promptly popped or because +** there will be a stack check soon after the push. Function frames +** never use this extra space, so it does not need to be kept clean. +*/ +#define EXTRA_STACK 5 + + +/* +** Size of cache for strings in the API. 'N' is the number of +** sets (better be a prime) and "M" is the size of each set. +** (M == 1 makes a direct cache.) +*/ +#if !defined(STRCACHE_N) +#define STRCACHE_N 53 +#define STRCACHE_M 2 +#endif + + +#define BASIC_STACK_SIZE (2*LUA_MINSTACK) + +#define stacksize(th) cast_int((th)->stack_last.p - (th)->stack.p) + + +/* kinds of Garbage Collection */ +#define KGC_INC 0 /* incremental gc */ +#define KGC_GENMINOR 1 /* generational gc in minor (regular) mode */ +#define KGC_GENMAJOR 2 /* generational in major mode */ + + +typedef struct stringtable { + TString **hash; /* array of buckets (linked lists of strings) */ + int nuse; /* number of elements */ + int size; /* number of buckets */ +} stringtable; + + +/* +** Information about a call. +** About union 'u': +** - field 'l' is used only for Lua functions; +** - field 'c' is used only for C functions. +** About union 'u2': +** - field 'funcidx' is used only by C functions while doing a +** protected call; +** - field 'nyield' is used only while a function is "doing" an +** yield (from the yield until the next resume); +** - field 'nres' is used only while closing tbc variables when +** returning from a function; +*/ +struct CallInfo { + StkIdRel func; /* function index in the stack */ + StkIdRel top; /* top for this function */ + struct CallInfo *previous, *next; /* dynamic call link */ + union { + struct { /* only for Lua functions */ + const Instruction *savedpc; + volatile l_signalT trap; /* function is tracing lines/counts */ + int nextraargs; /* # of extra arguments in vararg functions */ + } l; + struct { /* only for C functions */ + lua_KFunction k; /* continuation in case of yields */ + ptrdiff_t old_errfunc; + lua_KContext ctx; /* context info. in case of yields */ + } c; + } u; + union { + int funcidx; /* called-function index */ + int nyield; /* number of values yielded */ + int nres; /* number of values returned */ + } u2; + l_uint32 callstatus; +}; + + +/* +** Maximum expected number of results from a function +** (must fit in CIST_NRESULTS). +*/ +#define MAXRESULTS 250 + + +/* +** Bits in CallInfo status +*/ +/* bits 0-7 are the expected number of results from this function + 1 */ +#define CIST_NRESULTS 0xffu + +/* bits 8-11 count call metamethods (and their extra arguments) */ +#define CIST_CCMT 8 /* the offset, not the mask */ +#define MAX_CCMT (0xfu << CIST_CCMT) + +/* Bits 12-14 are used for CIST_RECST (see below) */ +#define CIST_RECST 12 /* the offset, not the mask */ + +/* call is running a C function (still in first 16 bits) */ +#define CIST_C (1u << (CIST_RECST + 3)) +/* call is on a fresh "luaV_execute" frame */ +#define CIST_FRESH (cast(l_uint32, CIST_C) << 1) +/* function is closing tbc variables */ +#define CIST_CLSRET (CIST_FRESH << 1) +/* function has tbc variables to close */ +#define CIST_TBC (CIST_CLSRET << 1) +/* original value of 'allowhook' */ +#define CIST_OAH (CIST_TBC << 1) +/* call is running a debug hook */ +#define CIST_HOOKED (CIST_OAH << 1) +/* doing a yieldable protected call */ +#define CIST_YPCALL (CIST_HOOKED << 1) +/* call was tail called */ +#define CIST_TAIL (CIST_YPCALL << 1) +/* last hook called yielded */ +#define CIST_HOOKYIELD (CIST_TAIL << 1) +/* function "called" a finalizer */ +#define CIST_FIN (CIST_HOOKYIELD << 1) + + +#define get_nresults(cs) (cast_int((cs) & CIST_NRESULTS) - 1) + +/* +** Field CIST_RECST stores the "recover status", used to keep the error +** status while closing to-be-closed variables in coroutines, so that +** Lua can correctly resume after an yield from a __close method called +** because of an error. (Three bits are enough for error status.) +*/ +#define getcistrecst(ci) (((ci)->callstatus >> CIST_RECST) & 7) +#define setcistrecst(ci,st) \ + check_exp(((st) & 7) == (st), /* status must fit in three bits */ \ + ((ci)->callstatus = ((ci)->callstatus & ~(7u << CIST_RECST)) \ + | (cast(l_uint32, st) << CIST_RECST))) + + +/* active function is a Lua function */ +#define isLua(ci) (!((ci)->callstatus & CIST_C)) + +/* call is running Lua code (not a hook) */ +#define isLuacode(ci) (!((ci)->callstatus & (CIST_C | CIST_HOOKED))) + + +#define setoah(ci,v) \ + ((ci)->callstatus = ((v) ? (ci)->callstatus | CIST_OAH \ + : (ci)->callstatus & ~CIST_OAH)) +#define getoah(ci) (((ci)->callstatus & CIST_OAH) ? 1 : 0) + + +/* +** 'per thread' state +*/ +struct lua_State { + CommonHeader; + lu_byte allowhook; + TStatus status; + StkIdRel top; /* first free slot in the stack */ + struct global_State *l_G; + CallInfo *ci; /* call info for current function */ + StkIdRel stack_last; /* end of stack (last element + 1) */ + StkIdRel stack; /* stack base */ + UpVal *openupval; /* list of open upvalues in this stack */ + StkIdRel tbclist; /* list of to-be-closed variables */ + GCObject *gclist; + struct lua_State *twups; /* list of threads with open upvalues */ + struct lua_longjmp *errorJmp; /* current error recover point */ + CallInfo base_ci; /* CallInfo for first level (C host) */ + volatile lua_Hook hook; + ptrdiff_t errfunc; /* current error handling function (stack index) */ + l_uint32 nCcalls; /* number of nested non-yieldable or C calls */ + int oldpc; /* last pc traced */ + int nci; /* number of items in 'ci' list */ + int basehookcount; + int hookcount; + volatile l_signalT hookmask; + struct { /* info about transferred values (for call/return hooks) */ + int ftransfer; /* offset of first value transferred */ + int ntransfer; /* number of values transferred */ + } transferinfo; +}; + + +/* +** thread state + extra space +*/ +typedef struct LX { + lu_byte extra_[LUA_EXTRASPACE]; + lua_State l; +} LX; + + +/* +** 'global state', shared by all threads of this state +*/ +typedef struct global_State { + lua_Alloc frealloc; /* function to reallocate memory */ + void *ud; /* auxiliary data to 'frealloc' */ + l_mem GCtotalbytes; /* number of bytes currently allocated + debt */ + l_mem GCdebt; /* bytes counted but not yet allocated */ + l_mem GCmarked; /* number of objects marked in a GC cycle */ + l_mem GCmajorminor; /* auxiliary counter to control major-minor shifts */ + stringtable strt; /* hash table for strings */ + TValue l_registry; + TValue nilvalue; /* a nil value */ + unsigned int seed; /* randomized seed for hashes */ + lu_byte gcparams[LUA_GCPN]; + lu_byte currentwhite; + lu_byte gcstate; /* state of garbage collector */ + lu_byte gckind; /* kind of GC running */ + lu_byte gcstopem; /* stops emergency collections */ + lu_byte gcstp; /* control whether GC is running */ + lu_byte gcemergency; /* true if this is an emergency collection */ + GCObject *allgc; /* list of all collectable objects */ + GCObject **sweepgc; /* current position of sweep in list */ + GCObject *finobj; /* list of collectable objects with finalizers */ + GCObject *gray; /* list of gray objects */ + GCObject *grayagain; /* list of objects to be traversed atomically */ + GCObject *weak; /* list of tables with weak values */ + GCObject *ephemeron; /* list of ephemeron tables (weak keys) */ + GCObject *allweak; /* list of all-weak tables */ + GCObject *tobefnz; /* list of userdata to be GC */ + GCObject *fixedgc; /* list of objects not to be collected */ + /* fields for generational collector */ + GCObject *survival; /* start of objects that survived one GC cycle */ + GCObject *old1; /* start of old1 objects */ + GCObject *reallyold; /* objects more than one cycle old ("really old") */ + GCObject *firstold1; /* first OLD1 object in the list (if any) */ + GCObject *finobjsur; /* list of survival objects with finalizers */ + GCObject *finobjold1; /* list of old1 objects with finalizers */ + GCObject *finobjrold; /* list of really old objects with finalizers */ + struct lua_State *twups; /* list of threads with open upvalues */ + lua_CFunction panic; /* to be called in unprotected errors */ + TString *memerrmsg; /* message for memory-allocation errors */ + TString *tmname[TM_N]; /* array with tag-method names */ + struct Table *mt[LUA_NUMTYPES]; /* metatables for basic types */ + TString *strcache[STRCACHE_N][STRCACHE_M]; /* cache for strings in API */ + lua_WarnFunction warnf; /* warning function */ + void *ud_warn; /* auxiliary data to 'warnf' */ + LX mainth; /* main thread of this state */ +} global_State; + + +#define G(L) (L->l_G) +#define mainthread(G) (&(G)->mainth.l) + +/* +** 'g->nilvalue' being a nil value flags that the state was completely +** build. +*/ +#define completestate(g) ttisnil(&g->nilvalue) + + +/* +** Union of all collectable objects +** ISO C99, 6.5.2.3 p.5: +** "if a union contains several structures that share a common initial +** sequence [...], and if the union object currently contains one +** of these structures, it is permitted to inspect the common initial +** part of any of them anywhere that a declaration of the complete type +** of the union is visible." +*/ +union GCUnion { + GCObject gc; /* common header */ + struct TString ts; + struct Udata u; + union Closure cl; + struct Table h; + struct Proto p; + struct lua_State th; /* thread */ + struct UpVal upv; +}; + + +/* macros to convert a GCObject into a specific value +** ISO C99, 6.3.2.2 p.7: +** "A pointer to an object or incomplete type may be converted to a +** pointer to a different object or incomplete type. If the resulting +** pointer is not correctly aligned for the pointed-to type, the +** behavior is undefined. Otherwise, when converted back again, the +** result shall compare equal to the original pointer." +*/ +#define gco2(v,T,o) check_exp((o)->tt == v, cast(T*, o)) +#define gco2nv(t,T,o) check_exp(novariant((o)->tt) == t, cast(T*, o)) +#define gco2ts(o) gco2nv(LUA_TSTRING, TString, o) +#define gco2u(o) gco2(LUA_VUSERDATA, Udata, o) +#define gco2lcl(o) (&gco2(LUA_VLCL, Closure, o)->l) +#define gco2ccl(o) (&gco2(LUA_VCCL, Closure, o)->c) +#define gco2cl(o) gco2nv(LUA_TFUNCTION, Closure, o) +#define gco2t(o) gco2(LUA_VTABLE, Table, o) +#define gco2p(o) gco2(LUA_VPROTO, Proto, o) +#define gco2th(o) gco2(LUA_VTHREAD, lua_State, o) +#define gco2upv(o) gco2(LUA_VUPVAL, UpVal, o) + + +/* +** macro to convert a Lua object into a GCObject +*/ +#define obj2gco(v) \ + check_exp(novariant((v)->tt) >= LUA_TSTRING, cast(GCObject*, v)) + + +/* actual number of total memory allocated */ +#define gettotalbytes(g) ((g)->GCtotalbytes - (g)->GCdebt) + + +LUAI_FUNC void luaE_setdebt (global_State *g, l_mem debt); +LUAI_FUNC void luaE_freethread (lua_State *L, lua_State *L1); +LUAI_FUNC lu_mem luaE_threadsize (lua_State *L); +LUAI_FUNC CallInfo *luaE_extendCI (lua_State *L, int err); +LUAI_FUNC void luaE_shrinkCI (lua_State *L); +LUAI_FUNC void luaE_checkcstack (lua_State *L); +LUAI_FUNC void luaE_incCstack (lua_State *L); +LUAI_FUNC void luaE_warning (lua_State *L, const char *msg, int tocont); +LUAI_FUNC void luaE_warnerror (lua_State *L, const char *where); +LUAI_FUNC TStatus luaE_resetthread (lua_State *L, TStatus status); + + +#endif diff --git a/crates/lua/vendor/lua/lstring.c b/crates/lua/vendor/lua/lstring.c new file mode 100644 index 0000000..59c5552 --- /dev/null +++ b/crates/lua/vendor/lua/lstring.c @@ -0,0 +1,352 @@ +/* +** $Id: lstring.c $ +** String table (keeps all strings handled by Lua) +** See Copyright Notice in lua.h +*/ + +#define lstring_c +#define LUA_CORE + +#include "lprefix.h" + + +#include + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" + + +/* +** Maximum size for string table. +*/ +#define MAXSTRTB cast_int(luaM_limitN(INT_MAX, TString*)) + +/* +** Initial size for the string table (must be power of 2). +** The Lua core alone registers ~50 strings (reserved words + +** metaevent keys + a few others). Libraries would typically add +** a few dozens more. +*/ +#if !defined(MINSTRTABSIZE) +#define MINSTRTABSIZE 128 +#endif + + +/* +** generic equality for strings +*/ +int luaS_eqstr (TString *a, TString *b) { + size_t len1, len2; + const char *s1 = getlstr(a, len1); + const char *s2 = getlstr(b, len2); + return ((len1 == len2) && /* equal length and ... */ + (memcmp(s1, s2, len1) == 0)); /* equal contents */ +} + + +static unsigned luaS_hash (const char *str, size_t l, unsigned seed) { + unsigned int h = seed ^ cast_uint(l); + for (; l > 0; l--) + h ^= ((h<<5) + (h>>2) + cast_byte(str[l - 1])); + return h; +} + + +unsigned luaS_hashlongstr (TString *ts) { + lua_assert(ts->tt == LUA_VLNGSTR); + if (ts->extra == 0) { /* no hash? */ + size_t len = ts->u.lnglen; + ts->hash = luaS_hash(getlngstr(ts), len, ts->hash); + ts->extra = 1; /* now it has its hash */ + } + return ts->hash; +} + + +static void tablerehash (TString **vect, int osize, int nsize) { + int i; + for (i = osize; i < nsize; i++) /* clear new elements */ + vect[i] = NULL; + for (i = 0; i < osize; i++) { /* rehash old part of the array */ + TString *p = vect[i]; + vect[i] = NULL; + while (p) { /* for each string in the list */ + TString *hnext = p->u.hnext; /* save next */ + unsigned int h = lmod(p->hash, nsize); /* new position */ + p->u.hnext = vect[h]; /* chain it into array */ + vect[h] = p; + p = hnext; + } + } +} + + +/* +** Resize the string table. If allocation fails, keep the current size. +** (This can degrade performance, but any non-zero size should work +** correctly.) +*/ +void luaS_resize (lua_State *L, int nsize) { + stringtable *tb = &G(L)->strt; + int osize = tb->size; + TString **newvect; + if (nsize < osize) /* shrinking table? */ + tablerehash(tb->hash, osize, nsize); /* depopulate shrinking part */ + newvect = luaM_reallocvector(L, tb->hash, osize, nsize, TString*); + if (l_unlikely(newvect == NULL)) { /* reallocation failed? */ + if (nsize < osize) /* was it shrinking table? */ + tablerehash(tb->hash, nsize, osize); /* restore to original size */ + /* leave table as it was */ + } + else { /* allocation succeeded */ + tb->hash = newvect; + tb->size = nsize; + if (nsize > osize) + tablerehash(newvect, osize, nsize); /* rehash for new size */ + } +} + + +/* +** Clear API string cache. (Entries cannot be empty, so fill them with +** a non-collectable string.) +*/ +void luaS_clearcache (global_State *g) { + int i, j; + for (i = 0; i < STRCACHE_N; i++) + for (j = 0; j < STRCACHE_M; j++) { + if (iswhite(g->strcache[i][j])) /* will entry be collected? */ + g->strcache[i][j] = g->memerrmsg; /* replace it with something fixed */ + } +} + + +/* +** Initialize the string table and the string cache +*/ +void luaS_init (lua_State *L) { + global_State *g = G(L); + int i, j; + stringtable *tb = &G(L)->strt; + tb->hash = luaM_newvector(L, MINSTRTABSIZE, TString*); + tablerehash(tb->hash, 0, MINSTRTABSIZE); /* clear array */ + tb->size = MINSTRTABSIZE; + /* pre-create memory-error message */ + g->memerrmsg = luaS_newliteral(L, MEMERRMSG); + luaC_fix(L, obj2gco(g->memerrmsg)); /* it should never be collected */ + for (i = 0; i < STRCACHE_N; i++) /* fill cache with valid strings */ + for (j = 0; j < STRCACHE_M; j++) + g->strcache[i][j] = g->memerrmsg; +} + + +size_t luaS_sizelngstr (size_t len, int kind) { + switch (kind) { + case LSTRREG: /* regular long string */ + /* don't need 'falloc'/'ud', but need space for content */ + return offsetof(TString, falloc) + (len + 1) * sizeof(char); + case LSTRFIX: /* fixed external long string */ + /* don't need 'falloc'/'ud' */ + return offsetof(TString, falloc); + default: /* external long string with deallocation */ + lua_assert(kind == LSTRMEM); + return sizeof(TString); + } +} + + +/* +** creates a new string object +*/ +static TString *createstrobj (lua_State *L, size_t totalsize, lu_byte tag, + unsigned h) { + TString *ts; + GCObject *o; + o = luaC_newobj(L, tag, totalsize); + ts = gco2ts(o); + ts->hash = h; + ts->extra = 0; + return ts; +} + + +TString *luaS_createlngstrobj (lua_State *L, size_t l) { + size_t totalsize = luaS_sizelngstr(l, LSTRREG); + TString *ts = createstrobj(L, totalsize, LUA_VLNGSTR, G(L)->seed); + ts->u.lnglen = l; + ts->shrlen = LSTRREG; /* signals that it is a regular long string */ + ts->contents = cast_charp(ts) + offsetof(TString, falloc); + ts->contents[l] = '\0'; /* ending 0 */ + return ts; +} + + +void luaS_remove (lua_State *L, TString *ts) { + stringtable *tb = &G(L)->strt; + TString **p = &tb->hash[lmod(ts->hash, tb->size)]; + while (*p != ts) /* find previous element */ + p = &(*p)->u.hnext; + *p = (*p)->u.hnext; /* remove element from its list */ + tb->nuse--; +} + + +static void growstrtab (lua_State *L, stringtable *tb) { + if (l_unlikely(tb->nuse == INT_MAX)) { /* too many strings? */ + luaC_fullgc(L, 1); /* try to free some... */ + if (tb->nuse == INT_MAX) /* still too many? */ + luaM_error(L); /* cannot even create a message... */ + } + if (tb->size <= MAXSTRTB / 2) /* can grow string table? */ + luaS_resize(L, tb->size * 2); +} + + +/* +** Checks whether short string exists and reuses it or creates a new one. +*/ +static TString *internshrstr (lua_State *L, const char *str, size_t l) { + TString *ts; + global_State *g = G(L); + stringtable *tb = &g->strt; + unsigned int h = luaS_hash(str, l, g->seed); + TString **list = &tb->hash[lmod(h, tb->size)]; + lua_assert(str != NULL); /* otherwise 'memcmp'/'memcpy' are undefined */ + for (ts = *list; ts != NULL; ts = ts->u.hnext) { + if (l == cast_uint(ts->shrlen) && + (memcmp(str, getshrstr(ts), l * sizeof(char)) == 0)) { + /* found! */ + if (isdead(g, ts)) /* dead (but not collected yet)? */ + changewhite(ts); /* resurrect it */ + return ts; + } + } + /* else must create a new string */ + if (tb->nuse >= tb->size) { /* need to grow string table? */ + growstrtab(L, tb); + list = &tb->hash[lmod(h, tb->size)]; /* rehash with new size */ + } + ts = createstrobj(L, sizestrshr(l), LUA_VSHRSTR, h); + ts->shrlen = cast(ls_byte, l); + getshrstr(ts)[l] = '\0'; /* ending 0 */ + memcpy(getshrstr(ts), str, l * sizeof(char)); + ts->u.hnext = *list; + *list = ts; + tb->nuse++; + return ts; +} + + +/* +** new string (with explicit length) +*/ +TString *luaS_newlstr (lua_State *L, const char *str, size_t l) { + if (l <= LUAI_MAXSHORTLEN) /* short string? */ + return internshrstr(L, str, l); + else { + TString *ts; + if (l_unlikely(l * sizeof(char) >= (MAX_SIZE - sizeof(TString)))) + luaM_toobig(L); + ts = luaS_createlngstrobj(L, l); + memcpy(getlngstr(ts), str, l * sizeof(char)); + return ts; + } +} + + +/* +** Create or reuse a zero-terminated string, first checking in the +** cache (using the string address as a key). The cache can contain +** only zero-terminated strings, so it is safe to use 'strcmp' to +** check hits. +*/ +TString *luaS_new (lua_State *L, const char *str) { + unsigned int i = point2uint(str) % STRCACHE_N; /* hash */ + int j; + TString **p = G(L)->strcache[i]; + for (j = 0; j < STRCACHE_M; j++) { + if (strcmp(str, getstr(p[j])) == 0) /* hit? */ + return p[j]; /* that is it */ + } + /* normal route */ + for (j = STRCACHE_M - 1; j > 0; j--) + p[j] = p[j - 1]; /* move out last element */ + /* new element is first in the list */ + p[0] = luaS_newlstr(L, str, strlen(str)); + return p[0]; +} + + +Udata *luaS_newudata (lua_State *L, size_t s, unsigned short nuvalue) { + Udata *u; + int i; + GCObject *o; + if (l_unlikely(s > MAX_SIZE - udatamemoffset(nuvalue))) + luaM_toobig(L); + o = luaC_newobj(L, LUA_VUSERDATA, sizeudata(nuvalue, s)); + u = gco2u(o); + u->len = s; + u->nuvalue = nuvalue; + u->metatable = NULL; + for (i = 0; i < nuvalue; i++) + setnilvalue(&u->uv[i].uv); + return u; +} + + +struct NewExt { + ls_byte kind; + const char *s; + size_t len; + TString *ts; /* output */ +}; + + +static void f_newext (lua_State *L, void *ud) { + struct NewExt *ne = cast(struct NewExt *, ud); + size_t size = luaS_sizelngstr(0, ne->kind); + ne->ts = createstrobj(L, size, LUA_VLNGSTR, G(L)->seed); +} + + +TString *luaS_newextlstr (lua_State *L, + const char *s, size_t len, lua_Alloc falloc, void *ud) { + struct NewExt ne; + if (!falloc) { + ne.kind = LSTRFIX; + f_newext(L, &ne); /* just create header */ + } + else { + ne.kind = LSTRMEM; + if (luaD_rawrunprotected(L, f_newext, &ne) != LUA_OK) { /* mem. error? */ + (*falloc)(ud, cast_voidp(s), len + 1, 0); /* free external string */ + luaM_error(L); /* re-raise memory error */ + } + ne.ts->falloc = falloc; + ne.ts->ud = ud; + } + ne.ts->shrlen = ne.kind; + ne.ts->u.lnglen = len; + ne.ts->contents = cast_charp(s); + return ne.ts; +} + + +/* +** Normalize an external string: If it is short, internalize it. +*/ +TString *luaS_normstr (lua_State *L, TString *ts) { + size_t len = ts->u.lnglen; + if (len > LUAI_MAXSHORTLEN) + return ts; /* long string; keep the original */ + else { + const char *str = getlngstr(ts); + return internshrstr(L, str, len); + } +} diff --git a/crates/lua/vendor/lua/lstring.h b/crates/lua/vendor/lua/lstring.h new file mode 100644 index 0000000..1643c3d --- /dev/null +++ b/crates/lua/vendor/lua/lstring.h @@ -0,0 +1,73 @@ +/* +** $Id: lstring.h $ +** String table (keep all strings handled by Lua) +** See Copyright Notice in lua.h +*/ + +#ifndef lstring_h +#define lstring_h + +#include "lgc.h" +#include "lobject.h" +#include "lstate.h" + + +/* +** Memory-allocation error message must be preallocated (it cannot +** be created after memory is exhausted) +*/ +#define MEMERRMSG "not enough memory" + + +/* +** Maximum length for short strings, that is, strings that are +** internalized. (Cannot be smaller than reserved words or tags for +** metamethods, as these strings must be internalized; +** #("function") = 8, #("__newindex") = 10.) +*/ +#if !defined(LUAI_MAXSHORTLEN) +#define LUAI_MAXSHORTLEN 40 +#endif + + +/* +** Size of a short TString: Size of the header plus space for the string +** itself (including final '\0'). +*/ +#define sizestrshr(l) \ + (offsetof(TString, contents) + ((l) + 1) * sizeof(char)) + + +#define luaS_newliteral(L, s) (luaS_newlstr(L, "" s, \ + (sizeof(s)/sizeof(char))-1)) + + +/* +** test whether a string is a reserved word +*/ +#define isreserved(s) (strisshr(s) && (s)->extra > 0) + + +/* +** equality for short strings, which are always internalized +*/ +#define eqshrstr(a,b) check_exp((a)->tt == LUA_VSHRSTR, (a) == (b)) + + +LUAI_FUNC unsigned luaS_hashlongstr (TString *ts); +LUAI_FUNC int luaS_eqstr (TString *a, TString *b); +LUAI_FUNC void luaS_resize (lua_State *L, int newsize); +LUAI_FUNC void luaS_clearcache (global_State *g); +LUAI_FUNC void luaS_init (lua_State *L); +LUAI_FUNC void luaS_remove (lua_State *L, TString *ts); +LUAI_FUNC Udata *luaS_newudata (lua_State *L, size_t s, + unsigned short nuvalue); +LUAI_FUNC TString *luaS_newlstr (lua_State *L, const char *str, size_t l); +LUAI_FUNC TString *luaS_new (lua_State *L, const char *str); +LUAI_FUNC TString *luaS_createlngstrobj (lua_State *L, size_t l); +LUAI_FUNC TString *luaS_newextlstr (lua_State *L, + const char *s, size_t len, lua_Alloc falloc, void *ud); +LUAI_FUNC size_t luaS_sizelngstr (size_t len, int kind); +LUAI_FUNC TString *luaS_normstr (lua_State *L, TString *ts); + +#endif diff --git a/crates/lua/vendor/lua/lstrlib.c b/crates/lua/vendor/lua/lstrlib.c new file mode 100644 index 0000000..2938ebc --- /dev/null +++ b/crates/lua/vendor/lua/lstrlib.c @@ -0,0 +1,1899 @@ +/* +** $Id: lstrlib.c $ +** Standard library for string operations and pattern-matching +** See Copyright Notice in lua.h +*/ + +#define lstrlib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" +#include "llimits.h" + + +/* +** maximum number of captures that a pattern can do during +** pattern-matching. This limit is arbitrary, but must fit in +** an unsigned char. +*/ +#if !defined(LUA_MAXCAPTURES) +#define LUA_MAXCAPTURES 32 +#endif + + +static int str_len (lua_State *L) { + size_t l; + luaL_checklstring(L, 1, &l); + lua_pushinteger(L, (lua_Integer)l); + return 1; +} + + +/* +** translate a relative initial string position +** (negative means back from end): clip result to [1, inf). +** The length of any string in Lua must fit in a lua_Integer, +** so there are no overflows in the casts. +** The inverted comparison avoids a possible overflow +** computing '-pos'. +*/ +static size_t posrelatI (lua_Integer pos, size_t len) { + if (pos > 0) + return (size_t)pos; + else if (pos == 0) + return 1; + else if (pos < -(lua_Integer)len) /* inverted comparison */ + return 1; /* clip to 1 */ + else return len + (size_t)pos + 1; +} + + +/* +** Gets an optional ending string position from argument 'arg', +** with default value 'def'. +** Negative means back from end: clip result to [0, len] +*/ +static size_t getendpos (lua_State *L, int arg, lua_Integer def, + size_t len) { + lua_Integer pos = luaL_optinteger(L, arg, def); + if (pos > (lua_Integer)len) + return len; + else if (pos >= 0) + return (size_t)pos; + else if (pos < -(lua_Integer)len) + return 0; + else return len + (size_t)pos + 1; +} + + +static int str_sub (lua_State *L) { + size_t l; + const char *s = luaL_checklstring(L, 1, &l); + size_t start = posrelatI(luaL_checkinteger(L, 2), l); + size_t end = getendpos(L, 3, -1, l); + if (start <= end) + lua_pushlstring(L, s + start - 1, (end - start) + 1); + else lua_pushliteral(L, ""); + return 1; +} + + +static int str_reverse (lua_State *L) { + size_t l, i; + luaL_Buffer b; + const char *s = luaL_checklstring(L, 1, &l); + char *p = luaL_buffinitsize(L, &b, l); + for (i = 0; i < l; i++) + p[i] = s[l - i - 1]; + luaL_pushresultsize(&b, l); + return 1; +} + + +static int str_lower (lua_State *L) { + size_t l; + size_t i; + luaL_Buffer b; + const char *s = luaL_checklstring(L, 1, &l); + char *p = luaL_buffinitsize(L, &b, l); + for (i=0; i MAX_SIZE - lsep || + cast_st2S(len + lsep) > cast_st2S(MAX_SIZE) / n)) + return luaL_error(L, "resulting string too large"); + else { + size_t totallen = (cast_sizet(n) * (len + lsep)) - lsep; + luaL_Buffer b; + char *p = luaL_buffinitsize(L, &b, totallen); + while (n-- > 1) { /* first n-1 copies (followed by separator) */ + memcpy(p, s, len * sizeof(char)); p += len; + if (lsep > 0) { /* empty 'memcpy' is not that cheap */ + memcpy(p, sep, lsep * sizeof(char)); p += lsep; + } + } + memcpy(p, s, len * sizeof(char)); /* last copy without separator */ + luaL_pushresultsize(&b, totallen); + } + return 1; +} + + +static int str_byte (lua_State *L) { + size_t l; + const char *s = luaL_checklstring(L, 1, &l); + lua_Integer pi = luaL_optinteger(L, 2, 1); + size_t posi = posrelatI(pi, l); + size_t pose = getendpos(L, 3, pi, l); + int n, i; + if (posi > pose) return 0; /* empty interval; return no values */ + if (l_unlikely(pose - posi >= (size_t)INT_MAX)) /* arithmetic overflow? */ + return luaL_error(L, "string slice too long"); + n = (int)(pose - posi) + 1; + luaL_checkstack(L, n, "string slice too long"); + for (i=0; iinit) { + state->init = 1; + luaL_buffinit(L, &state->B); + } + if (b == NULL) { /* finishing dump? */ + luaL_pushresult(&state->B); /* push result */ + lua_replace(L, 1); /* move it to reserved slot */ + } + else + luaL_addlstring(&state->B, (const char *)b, size); + return 0; +} + + +static int str_dump (lua_State *L) { + struct str_Writer state; + int strip = lua_toboolean(L, 2); + luaL_argcheck(L, lua_type(L, 1) == LUA_TFUNCTION && !lua_iscfunction(L, 1), + 1, "Lua function expected"); + /* ensure function is on the top of the stack and vacate slot 1 */ + lua_pushvalue(L, 1); + state.init = 0; + lua_dump(L, writer, &state, strip); + lua_settop(L, 1); /* leave final result on top */ + return 1; +} + + + +/* +** {====================================================== +** METAMETHODS +** ======================================================= +*/ + +#if defined(LUA_NOCVTS2N) /* { */ + +/* no coercion from strings to numbers */ + +static const luaL_Reg stringmetamethods[] = { + {"__index", NULL}, /* placeholder */ + {NULL, NULL} +}; + +#else /* }{ */ + +static int tonum (lua_State *L, int arg) { + if (lua_type(L, arg) == LUA_TNUMBER) { /* already a number? */ + lua_pushvalue(L, arg); + return 1; + } + else { /* check whether it is a numerical string */ + size_t len; + const char *s = lua_tolstring(L, arg, &len); + return (s != NULL && lua_stringtonumber(L, s) == len + 1); + } +} + + +/* +** To be here, either the first operand was a string or the first +** operand didn't have a corresponding metamethod. (Otherwise, that +** other metamethod would have been called.) So, if this metamethod +** doesn't work, the only other option would be for the second +** operand to have a different metamethod. +*/ +static void trymt (lua_State *L, const char *mtkey, const char *opname) { + lua_settop(L, 2); /* back to the original arguments */ + if (l_unlikely(lua_type(L, 2) == LUA_TSTRING || + !luaL_getmetafield(L, 2, mtkey))) + luaL_error(L, "attempt to %s a '%s' with a '%s'", opname, + luaL_typename(L, -2), luaL_typename(L, -1)); + lua_insert(L, -3); /* put metamethod before arguments */ + lua_call(L, 2, 1); /* call metamethod */ +} + + +static int arith (lua_State *L, int op, const char *mtname) { + if (tonum(L, 1) && tonum(L, 2)) + lua_arith(L, op); /* result will be on the top */ + else + trymt(L, mtname, mtname + 2); + return 1; +} + + +static int arith_add (lua_State *L) { + return arith(L, LUA_OPADD, "__add"); +} + +static int arith_sub (lua_State *L) { + return arith(L, LUA_OPSUB, "__sub"); +} + +static int arith_mul (lua_State *L) { + return arith(L, LUA_OPMUL, "__mul"); +} + +static int arith_mod (lua_State *L) { + return arith(L, LUA_OPMOD, "__mod"); +} + +static int arith_pow (lua_State *L) { + return arith(L, LUA_OPPOW, "__pow"); +} + +static int arith_div (lua_State *L) { + return arith(L, LUA_OPDIV, "__div"); +} + +static int arith_idiv (lua_State *L) { + return arith(L, LUA_OPIDIV, "__idiv"); +} + +static int arith_unm (lua_State *L) { + return arith(L, LUA_OPUNM, "__unm"); +} + + +static const luaL_Reg stringmetamethods[] = { + {"__add", arith_add}, + {"__sub", arith_sub}, + {"__mul", arith_mul}, + {"__mod", arith_mod}, + {"__pow", arith_pow}, + {"__div", arith_div}, + {"__idiv", arith_idiv}, + {"__unm", arith_unm}, + {"__index", NULL}, /* placeholder */ + {NULL, NULL} +}; + +#endif /* } */ + +/* }====================================================== */ + +/* +** {====================================================== +** PATTERN MATCHING +** ======================================================= +*/ + + +#define CAP_UNFINISHED (-1) +#define CAP_POSITION (-2) + + +typedef struct MatchState { + const char *src_init; /* init of source string */ + const char *src_end; /* end ('\0') of source string */ + const char *p_end; /* end ('\0') of pattern */ + lua_State *L; + int matchdepth; /* control for recursive depth (to avoid C stack overflow) */ + int level; /* total number of captures (finished or unfinished) */ + struct { + const char *init; + ptrdiff_t len; /* length or special value (CAP_*) */ + } capture[LUA_MAXCAPTURES]; +} MatchState; + + +/* recursive function */ +static const char *match (MatchState *ms, const char *s, const char *p); + + +/* maximum recursion depth for 'match' */ +#if !defined(MAXCCALLS) +#define MAXCCALLS 200 +#endif + + +#define L_ESC '%' +#define SPECIALS "^$*+?.([%-" + + +static int check_capture (MatchState *ms, int l) { + l -= '1'; + if (l_unlikely(l < 0 || l >= ms->level || + ms->capture[l].len == CAP_UNFINISHED)) + return luaL_error(ms->L, "invalid capture index %%%d", l + 1); + return l; +} + + +static int capture_to_close (MatchState *ms) { + int level = ms->level; + for (level--; level>=0; level--) + if (ms->capture[level].len == CAP_UNFINISHED) return level; + return luaL_error(ms->L, "invalid pattern capture"); +} + + +static const char *classend (MatchState *ms, const char *p) { + switch (*p++) { + case L_ESC: { + if (l_unlikely(p == ms->p_end)) + luaL_error(ms->L, "malformed pattern (ends with '%%')"); + return p+1; + } + case '[': { + if (*p == '^') p++; + do { /* look for a ']' */ + if (l_unlikely(p == ms->p_end)) + luaL_error(ms->L, "malformed pattern (missing ']')"); + if (*(p++) == L_ESC && p < ms->p_end) + p++; /* skip escapes (e.g. '%]') */ + } while (*p != ']'); + return p+1; + } + default: { + return p; + } + } +} + + +static int match_class (int c, int cl) { + int res; + switch (tolower(cl)) { + case 'a' : res = isalpha(c); break; + case 'c' : res = iscntrl(c); break; + case 'd' : res = isdigit(c); break; + case 'g' : res = isgraph(c); break; + case 'l' : res = islower(c); break; + case 'p' : res = ispunct(c); break; + case 's' : res = isspace(c); break; + case 'u' : res = isupper(c); break; + case 'w' : res = isalnum(c); break; + case 'x' : res = isxdigit(c); break; + case 'z' : res = (c == 0); break; /* deprecated option */ + default: return (cl == c); + } + return (islower(cl) ? res : !res); +} + + +static int matchbracketclass (int c, const char *p, const char *ec) { + int sig = 1; + if (*(p+1) == '^') { + sig = 0; + p++; /* skip the '^' */ + } + while (++p < ec) { + if (*p == L_ESC) { + p++; + if (match_class(c, cast_uchar(*p))) + return sig; + } + else if ((*(p+1) == '-') && (p+2 < ec)) { + p+=2; + if (cast_uchar(*(p-2)) <= c && c <= cast_uchar(*p)) + return sig; + } + else if (cast_uchar(*p) == c) return sig; + } + return !sig; +} + + +static int singlematch (MatchState *ms, const char *s, const char *p, + const char *ep) { + if (s >= ms->src_end) + return 0; + else { + int c = cast_uchar(*s); + switch (*p) { + case '.': return 1; /* matches any char */ + case L_ESC: return match_class(c, cast_uchar(*(p+1))); + case '[': return matchbracketclass(c, p, ep-1); + default: return (cast_uchar(*p) == c); + } + } +} + + +static const char *matchbalance (MatchState *ms, const char *s, + const char *p) { + if (l_unlikely(p >= ms->p_end - 1)) + luaL_error(ms->L, "malformed pattern (missing arguments to '%%b')"); + if (*s != *p) return NULL; + else { + int b = *p; + int e = *(p+1); + int cont = 1; + while (++s < ms->src_end) { + if (*s == e) { + if (--cont == 0) return s+1; + } + else if (*s == b) cont++; + } + } + return NULL; /* string ends out of balance */ +} + + +static const char *max_expand (MatchState *ms, const char *s, + const char *p, const char *ep) { + ptrdiff_t i = 0; /* counts maximum expand for item */ + while (singlematch(ms, s + i, p, ep)) + i++; + /* keeps trying to match with the maximum repetitions */ + while (i>=0) { + const char *res = match(ms, (s+i), ep+1); + if (res) return res; + i--; /* else didn't match; reduce 1 repetition to try again */ + } + return NULL; +} + + +static const char *min_expand (MatchState *ms, const char *s, + const char *p, const char *ep) { + for (;;) { + const char *res = match(ms, s, ep+1); + if (res != NULL) + return res; + else if (singlematch(ms, s, p, ep)) + s++; /* try with one more repetition */ + else return NULL; + } +} + + +static const char *start_capture (MatchState *ms, const char *s, + const char *p, int what) { + const char *res; + int level = ms->level; + if (level >= LUA_MAXCAPTURES) luaL_error(ms->L, "too many captures"); + ms->capture[level].init = s; + ms->capture[level].len = what; + ms->level = level+1; + if ((res=match(ms, s, p)) == NULL) /* match failed? */ + ms->level--; /* undo capture */ + return res; +} + + +static const char *end_capture (MatchState *ms, const char *s, + const char *p) { + int l = capture_to_close(ms); + const char *res; + ms->capture[l].len = s - ms->capture[l].init; /* close capture */ + if ((res = match(ms, s, p)) == NULL) /* match failed? */ + ms->capture[l].len = CAP_UNFINISHED; /* undo capture */ + return res; +} + + +static const char *match_capture (MatchState *ms, const char *s, int l) { + size_t len; + l = check_capture(ms, l); + len = cast_sizet(ms->capture[l].len); + if ((size_t)(ms->src_end-s) >= len && + memcmp(ms->capture[l].init, s, len) == 0) + return s+len; + else return NULL; +} + + +static const char *match (MatchState *ms, const char *s, const char *p) { + if (l_unlikely(ms->matchdepth-- == 0)) + luaL_error(ms->L, "pattern too complex"); + init: /* using goto to optimize tail recursion */ + if (p != ms->p_end) { /* end of pattern? */ + switch (*p) { + case '(': { /* start capture */ + if (*(p + 1) == ')') /* position capture? */ + s = start_capture(ms, s, p + 2, CAP_POSITION); + else + s = start_capture(ms, s, p + 1, CAP_UNFINISHED); + break; + } + case ')': { /* end capture */ + s = end_capture(ms, s, p + 1); + break; + } + case '$': { + if ((p + 1) != ms->p_end) /* is the '$' the last char in pattern? */ + goto dflt; /* no; go to default */ + s = (s == ms->src_end) ? s : NULL; /* check end of string */ + break; + } + case L_ESC: { /* escaped sequences not in the format class[*+?-]? */ + switch (*(p + 1)) { + case 'b': { /* balanced string? */ + s = matchbalance(ms, s, p + 2); + if (s != NULL) { + p += 4; goto init; /* return match(ms, s, p + 4); */ + } /* else fail (s == NULL) */ + break; + } + case 'f': { /* frontier? */ + const char *ep; char previous; + p += 2; + if (l_unlikely(*p != '[')) + luaL_error(ms->L, "missing '[' after '%%f' in pattern"); + ep = classend(ms, p); /* points to what is next */ + previous = (s == ms->src_init) ? '\0' : *(s - 1); + if (!matchbracketclass(cast_uchar(previous), p, ep - 1) && + matchbracketclass(cast_uchar(*s), p, ep - 1)) { + p = ep; goto init; /* return match(ms, s, ep); */ + } + s = NULL; /* match failed */ + break; + } + case '0': case '1': case '2': case '3': + case '4': case '5': case '6': case '7': + case '8': case '9': { /* capture results (%0-%9)? */ + s = match_capture(ms, s, cast_uchar(*(p + 1))); + if (s != NULL) { + p += 2; goto init; /* return match(ms, s, p + 2) */ + } + break; + } + default: goto dflt; + } + break; + } + default: dflt: { /* pattern class plus optional suffix */ + const char *ep = classend(ms, p); /* points to optional suffix */ + /* does not match at least once? */ + if (!singlematch(ms, s, p, ep)) { + if (*ep == '*' || *ep == '?' || *ep == '-') { /* accept empty? */ + p = ep + 1; goto init; /* return match(ms, s, ep + 1); */ + } + else /* '+' or no suffix */ + s = NULL; /* fail */ + } + else { /* matched once */ + switch (*ep) { /* handle optional suffix */ + case '?': { /* optional */ + const char *res; + if ((res = match(ms, s + 1, ep + 1)) != NULL) + s = res; + else { + p = ep + 1; goto init; /* else return match(ms, s, ep + 1); */ + } + break; + } + case '+': /* 1 or more repetitions */ + s++; /* 1 match already done */ + /* FALLTHROUGH */ + case '*': /* 0 or more repetitions */ + s = max_expand(ms, s, p, ep); + break; + case '-': /* 0 or more repetitions (minimum) */ + s = min_expand(ms, s, p, ep); + break; + default: /* no suffix */ + s++; p = ep; goto init; /* return match(ms, s + 1, ep); */ + } + } + break; + } + } + } + ms->matchdepth++; + return s; +} + + + +static const char *lmemfind (const char *s1, size_t l1, + const char *s2, size_t l2) { + if (l2 == 0) return s1; /* empty strings are everywhere */ + else if (l2 > l1) return NULL; /* avoids a negative 'l1' */ + else { + const char *init; /* to search for a '*s2' inside 's1' */ + l2--; /* 1st char will be checked by 'memchr' */ + l1 = l1-l2; /* 's2' cannot be found after that */ + while (l1 > 0 && (init = (const char *)memchr(s1, *s2, l1)) != NULL) { + init++; /* 1st char is already checked */ + if (memcmp(init, s2+1, l2) == 0) + return init-1; + else { /* correct 'l1' and 's1' to try again */ + l1 -= ct_diff2sz(init - s1); + s1 = init; + } + } + return NULL; /* not found */ + } +} + + +/* +** get information about the i-th capture. If there are no captures +** and 'i==0', return information about the whole match, which +** is the range 's'..'e'. If the capture is a string, return +** its length and put its address in '*cap'. If it is an integer +** (a position), push it on the stack and return CAP_POSITION. +*/ +static ptrdiff_t get_onecapture (MatchState *ms, int i, const char *s, + const char *e, const char **cap) { + if (i >= ms->level) { + if (l_unlikely(i != 0)) + luaL_error(ms->L, "invalid capture index %%%d", i + 1); + *cap = s; + return (e - s); + } + else { + ptrdiff_t capl = ms->capture[i].len; + *cap = ms->capture[i].init; + if (l_unlikely(capl == CAP_UNFINISHED)) + luaL_error(ms->L, "unfinished capture"); + else if (capl == CAP_POSITION) + lua_pushinteger(ms->L, + ct_diff2S(ms->capture[i].init - ms->src_init) + 1); + return capl; + } +} + + +/* +** Push the i-th capture on the stack. +*/ +static void push_onecapture (MatchState *ms, int i, const char *s, + const char *e) { + const char *cap; + ptrdiff_t l = get_onecapture(ms, i, s, e, &cap); + if (l != CAP_POSITION) + lua_pushlstring(ms->L, cap, cast_sizet(l)); + /* else position was already pushed */ +} + + +static int push_captures (MatchState *ms, const char *s, const char *e) { + int i; + int nlevels = (ms->level == 0 && s) ? 1 : ms->level; + luaL_checkstack(ms->L, nlevels, "too many captures"); + for (i = 0; i < nlevels; i++) + push_onecapture(ms, i, s, e); + return nlevels; /* number of strings pushed */ +} + + +/* check whether pattern has no special characters */ +static int nospecials (const char *p, size_t l) { + size_t upto = 0; + do { + if (strpbrk(p + upto, SPECIALS)) + return 0; /* pattern has a special character */ + upto += strlen(p + upto) + 1; /* may have more after \0 */ + } while (upto <= l); + return 1; /* no special chars found */ +} + + +/* +** Prepare state for matches. These fields are not affected by each match. +*/ +static void prepstate (MatchState *ms, lua_State *L, + const char *s, size_t ls, const char *p, size_t lp) { + ms->L = L; + ms->src_init = s; + ms->src_end = s + ls; + ms->p_end = p + lp; +} + + +/* +** (Re)prepare state for a match, setting fields that change during +** each match. +*/ +static void reprepstate (MatchState *ms) { + ms->matchdepth = MAXCCALLS; + ms->level = 0; +} + + +static int str_find_aux (lua_State *L, int find) { + size_t ls, lp; + const char *s = luaL_checklstring(L, 1, &ls); + const char *p = luaL_checklstring(L, 2, &lp); + size_t init = posrelatI(luaL_optinteger(L, 3, 1), ls) - 1; + if (init > ls) { /* start after string's end? */ + luaL_pushfail(L); /* cannot find anything */ + return 1; + } + /* explicit request or no special characters? */ + if (find && (lua_toboolean(L, 4) || nospecials(p, lp))) { + /* do a plain search */ + const char *s2 = lmemfind(s + init, ls - init, p, lp); + if (s2) { + lua_pushinteger(L, ct_diff2S(s2 - s) + 1); + lua_pushinteger(L, cast_st2S(ct_diff2sz(s2 - s) + lp)); + return 2; + } + } + else { + MatchState ms; + const char *s1 = s + init; + int anchor = (*p == '^'); + if (anchor) { + p++; lp--; /* skip anchor character */ + } + prepstate(&ms, L, s, ls, p, lp); + do { + const char *res; + reprepstate(&ms); + if ((res=match(&ms, s1, p)) != NULL) { + if (find) { + lua_pushinteger(L, ct_diff2S(s1 - s) + 1); /* start */ + lua_pushinteger(L, ct_diff2S(res - s)); /* end */ + return push_captures(&ms, NULL, 0) + 2; + } + else + return push_captures(&ms, s1, res); + } + } while (s1++ < ms.src_end && !anchor); + } + luaL_pushfail(L); /* not found */ + return 1; +} + + +static int str_find (lua_State *L) { + return str_find_aux(L, 1); +} + + +static int str_match (lua_State *L) { + return str_find_aux(L, 0); +} + + +/* state for 'gmatch' */ +typedef struct GMatchState { + const char *src; /* current position */ + const char *p; /* pattern */ + const char *lastmatch; /* end of last match */ + MatchState ms; /* match state */ +} GMatchState; + + +static int gmatch_aux (lua_State *L) { + GMatchState *gm = (GMatchState *)lua_touserdata(L, lua_upvalueindex(3)); + const char *src; + gm->ms.L = L; + for (src = gm->src; src <= gm->ms.src_end; src++) { + const char *e; + reprepstate(&gm->ms); + if ((e = match(&gm->ms, src, gm->p)) != NULL && e != gm->lastmatch) { + gm->src = gm->lastmatch = e; + return push_captures(&gm->ms, src, e); + } + } + return 0; /* not found */ +} + + +static int gmatch (lua_State *L) { + size_t ls, lp; + const char *s = luaL_checklstring(L, 1, &ls); + const char *p = luaL_checklstring(L, 2, &lp); + size_t init = posrelatI(luaL_optinteger(L, 3, 1), ls) - 1; + GMatchState *gm; + lua_settop(L, 2); /* keep strings on closure to avoid being collected */ + gm = (GMatchState *)lua_newuserdatauv(L, sizeof(GMatchState), 0); + if (init > ls) /* start after string's end? */ + init = ls + 1; /* avoid overflows in 's + init' */ + prepstate(&gm->ms, L, s, ls, p, lp); + gm->src = s + init; gm->p = p; gm->lastmatch = NULL; + lua_pushcclosure(L, gmatch_aux, 3); + return 1; +} + + +static void add_s (MatchState *ms, luaL_Buffer *b, const char *s, + const char *e) { + size_t l; + lua_State *L = ms->L; + const char *news = lua_tolstring(L, 3, &l); + const char *p; + while ((p = (char *)memchr(news, L_ESC, l)) != NULL) { + luaL_addlstring(b, news, ct_diff2sz(p - news)); + p++; /* skip ESC */ + if (*p == L_ESC) /* '%%' */ + luaL_addchar(b, *p); + else if (*p == '0') /* '%0' */ + luaL_addlstring(b, s, ct_diff2sz(e - s)); + else if (isdigit(cast_uchar(*p))) { /* '%n' */ + const char *cap; + ptrdiff_t resl = get_onecapture(ms, *p - '1', s, e, &cap); + if (resl == CAP_POSITION) + luaL_addvalue(b); /* add position to accumulated result */ + else + luaL_addlstring(b, cap, cast_sizet(resl)); + } + else + luaL_error(L, "invalid use of '%c' in replacement string", L_ESC); + l -= ct_diff2sz(p + 1 - news); + news = p + 1; + } + luaL_addlstring(b, news, l); +} + + +/* +** Add the replacement value to the string buffer 'b'. +** Return true if the original string was changed. (Function calls and +** table indexing resulting in nil or false do not change the subject.) +*/ +static int add_value (MatchState *ms, luaL_Buffer *b, const char *s, + const char *e, int tr) { + lua_State *L = ms->L; + switch (tr) { + case LUA_TFUNCTION: { /* call the function */ + int n; + lua_pushvalue(L, 3); /* push the function */ + n = push_captures(ms, s, e); /* all captures as arguments */ + lua_call(L, n, 1); /* call it */ + break; + } + case LUA_TTABLE: { /* index the table */ + push_onecapture(ms, 0, s, e); /* first capture is the index */ + lua_gettable(L, 3); + break; + } + default: { /* LUA_TNUMBER or LUA_TSTRING */ + add_s(ms, b, s, e); /* add value to the buffer */ + return 1; /* something changed */ + } + } + if (!lua_toboolean(L, -1)) { /* nil or false? */ + lua_pop(L, 1); /* remove value */ + luaL_addlstring(b, s, ct_diff2sz(e - s)); /* keep original text */ + return 0; /* no changes */ + } + else if (l_unlikely(!lua_isstring(L, -1))) + return luaL_error(L, "invalid replacement value (a %s)", + luaL_typename(L, -1)); + else { + luaL_addvalue(b); /* add result to accumulator */ + return 1; /* something changed */ + } +} + + +static int str_gsub (lua_State *L) { + size_t srcl, lp; + const char *src = luaL_checklstring(L, 1, &srcl); /* subject */ + const char *p = luaL_checklstring(L, 2, &lp); /* pattern */ + const char *lastmatch = NULL; /* end of last match */ + int tr = lua_type(L, 3); /* replacement type */ + /* max replacements */ + lua_Integer max_s = luaL_optinteger(L, 4, cast_st2S(srcl) + 1); + int anchor = (*p == '^'); + lua_Integer n = 0; /* replacement count */ + int changed = 0; /* change flag */ + MatchState ms; + luaL_Buffer b; + luaL_argexpected(L, tr == LUA_TNUMBER || tr == LUA_TSTRING || + tr == LUA_TFUNCTION || tr == LUA_TTABLE, 3, + "string/function/table"); + luaL_buffinit(L, &b); + if (anchor) { + p++; lp--; /* skip anchor character */ + } + prepstate(&ms, L, src, srcl, p, lp); + while (n < max_s) { + const char *e; + reprepstate(&ms); /* (re)prepare state for new match */ + if ((e = match(&ms, src, p)) != NULL && e != lastmatch) { /* match? */ + n++; + changed = add_value(&ms, &b, src, e, tr) || changed; + src = lastmatch = e; + } + else if (src < ms.src_end) /* otherwise, skip one character */ + luaL_addchar(&b, *src++); + else break; /* end of subject */ + if (anchor) break; + } + if (!changed) /* no changes? */ + lua_pushvalue(L, 1); /* return original string */ + else { /* something changed */ + luaL_addlstring(&b, src, ct_diff2sz(ms.src_end - src)); + luaL_pushresult(&b); /* create and return new string */ + } + lua_pushinteger(L, n); /* number of substitutions */ + return 2; +} + +/* }====================================================== */ + + + +/* +** {====================================================== +** STRING FORMAT +** ======================================================= +*/ + +#if !defined(lua_number2strx) /* { */ + +/* +** Hexadecimal floating-point formatter +*/ + +#define SIZELENMOD (sizeof(LUA_NUMBER_FRMLEN)/sizeof(char)) + + +/* +** Number of bits that goes into the first digit. It can be any value +** between 1 and 4; the following definition tries to align the number +** to nibble boundaries by making what is left after that first digit a +** multiple of 4. +*/ +#define L_NBFD ((l_floatatt(MANT_DIG) - 1)%4 + 1) + + +/* +** Add integer part of 'x' to buffer and return new 'x' +*/ +static lua_Number adddigit (char *buff, unsigned n, lua_Number x) { + lua_Number dd = l_mathop(floor)(x); /* get integer part from 'x' */ + int d = (int)dd; + buff[n] = cast_char(d < 10 ? d + '0' : d - 10 + 'a'); /* add to buffer */ + return x - dd; /* return what is left */ +} + + +static int num2straux (char *buff, unsigned sz, lua_Number x) { + /* if 'inf' or 'NaN', format it like '%g' */ + if (x != x || x == (lua_Number)HUGE_VAL || x == -(lua_Number)HUGE_VAL) + return l_sprintf(buff, sz, LUA_NUMBER_FMT, (LUAI_UACNUMBER)x); + else if (x == 0) { /* can be -0... */ + /* create "0" or "-0" followed by exponent */ + return l_sprintf(buff, sz, LUA_NUMBER_FMT "x0p+0", (LUAI_UACNUMBER)x); + } + else { + int e; + lua_Number m = l_mathop(frexp)(x, &e); /* 'x' fraction and exponent */ + unsigned n = 0; /* character count */ + if (m < 0) { /* is number negative? */ + buff[n++] = '-'; /* add sign */ + m = -m; /* make it positive */ + } + buff[n++] = '0'; buff[n++] = 'x'; /* add "0x" */ + m = adddigit(buff, n++, m * (1 << L_NBFD)); /* add first digit */ + e -= L_NBFD; /* this digit goes before the radix point */ + if (m > 0) { /* more digits? */ + buff[n++] = lua_getlocaledecpoint(); /* add radix point */ + do { /* add as many digits as needed */ + m = adddigit(buff, n++, m * 16); + } while (m > 0); + } + n += cast_uint(l_sprintf(buff + n, sz - n, "p%+d", e)); /* add exponent */ + lua_assert(n < sz); + return cast_int(n); + } +} + + +static int lua_number2strx (lua_State *L, char *buff, unsigned sz, + const char *fmt, lua_Number x) { + int n = num2straux(buff, sz, x); + if (fmt[SIZELENMOD] == 'A') { + int i; + for (i = 0; i < n; i++) + buff[i] = cast_char(toupper(cast_uchar(buff[i]))); + } + else if (l_unlikely(fmt[SIZELENMOD] != 'a')) + return luaL_error(L, "modifiers for format '%%a'/'%%A' not implemented"); + return n; +} + +#endif /* } */ + + +/* +** Maximum size for items formatted with '%f'. This size is produced +** by format('%.99f', -maxfloat), and is equal to 99 + 3 ('-', '.', +** and '\0') + number of decimal digits to represent maxfloat (which +** is maximum exponent + 1). (99+3+1, adding some extra, 110) +*/ +#define MAX_ITEMF (110 + l_floatatt(MAX_10_EXP)) + + +/* +** All formats except '%f' do not need that large limit. The other +** float formats use exponents, so that they fit in the 99 limit for +** significant digits; 's' for large strings and 'q' add items directly +** to the buffer; all integer formats also fit in the 99 limit. The +** worst case are floats: they may need 99 significant digits, plus +** '0x', '-', '.', 'e+XXXX', and '\0'. Adding some extra, 120. +*/ +#define MAX_ITEM 120 + + +/* valid flags in a format specification */ +#if !defined(L_FMTFLAGSF) + +/* valid flags for a, A, e, E, f, F, g, and G conversions */ +#define L_FMTFLAGSF "-+#0 " + +/* valid flags for o, x, and X conversions */ +#define L_FMTFLAGSX "-#0" + +/* valid flags for d and i conversions */ +#define L_FMTFLAGSI "-+0 " + +/* valid flags for u conversions */ +#define L_FMTFLAGSU "-0" + +/* valid flags for c, p, and s conversions */ +#define L_FMTFLAGSC "-" + +#endif + + +/* +** Maximum size of each format specification (such as "%-099.99d"): +** Initial '%', flags (up to 5), width (2), period, precision (2), +** length modifier (8), conversion specifier, and final '\0', plus some +** extra. +*/ +#define MAX_FORMAT 32 + + +static void addquoted (luaL_Buffer *b, const char *s, size_t len) { + luaL_addchar(b, '"'); + while (len--) { + if (*s == '"' || *s == '\\' || *s == '\n') { + luaL_addchar(b, '\\'); + luaL_addchar(b, *s); + } + else if (iscntrl(cast_uchar(*s))) { + char buff[10]; + if (!isdigit(cast_uchar(*(s+1)))) + l_sprintf(buff, sizeof(buff), "\\%d", (int)cast_uchar(*s)); + else + l_sprintf(buff, sizeof(buff), "\\%03d", (int)cast_uchar(*s)); + luaL_addstring(b, buff); + } + else + luaL_addchar(b, *s); + s++; + } + luaL_addchar(b, '"'); +} + + +/* +** Serialize a floating-point number in such a way that it can be +** scanned back by Lua. Use hexadecimal format for "common" numbers +** (to preserve precision); inf, -inf, and NaN are handled separately. +** (NaN cannot be expressed as a numeral, so we write '(0/0)' for it.) +*/ +static int quotefloat (lua_State *L, char *buff, lua_Number n) { + const char *s; /* for the fixed representations */ + if (n == (lua_Number)HUGE_VAL) /* inf? */ + s = "1e9999"; + else if (n == -(lua_Number)HUGE_VAL) /* -inf? */ + s = "-1e9999"; + else if (n != n) /* NaN? */ + s = "(0/0)"; + else { /* format number as hexadecimal */ + int nb = lua_number2strx(L, buff, MAX_ITEM, + "%" LUA_NUMBER_FRMLEN "a", n); + /* ensures that 'buff' string uses a dot as the radix character */ + if (memchr(buff, '.', cast_uint(nb)) == NULL) { /* no dot? */ + char point = lua_getlocaledecpoint(); /* try locale point */ + char *ppoint = (char *)memchr(buff, point, cast_uint(nb)); + if (ppoint) *ppoint = '.'; /* change it to a dot */ + } + return nb; + } + /* for the fixed representations */ + return l_sprintf(buff, MAX_ITEM, "%s", s); +} + + +static void addliteral (lua_State *L, luaL_Buffer *b, int arg) { + switch (lua_type(L, arg)) { + case LUA_TSTRING: { + size_t len; + const char *s = lua_tolstring(L, arg, &len); + addquoted(b, s, len); + break; + } + case LUA_TNUMBER: { + char *buff = luaL_prepbuffsize(b, MAX_ITEM); + int nb; + if (!lua_isinteger(L, arg)) /* float? */ + nb = quotefloat(L, buff, lua_tonumber(L, arg)); + else { /* integers */ + lua_Integer n = lua_tointeger(L, arg); + const char *format = (n == LUA_MININTEGER) /* corner case? */ + ? "0x%" LUA_INTEGER_FRMLEN "x" /* use hex */ + : LUA_INTEGER_FMT; /* else use default format */ + nb = l_sprintf(buff, MAX_ITEM, format, (LUAI_UACINT)n); + } + luaL_addsize(b, cast_uint(nb)); + break; + } + case LUA_TNIL: case LUA_TBOOLEAN: { + luaL_tolstring(L, arg, NULL); + luaL_addvalue(b); + break; + } + default: { + luaL_argerror(L, arg, "value has no literal form"); + } + } +} + + +static const char *get2digits (const char *s) { + if (isdigit(cast_uchar(*s))) { + s++; + if (isdigit(cast_uchar(*s))) s++; /* (2 digits at most) */ + } + return s; +} + + +/* +** Check whether a conversion specification is valid. When called, +** first character in 'form' must be '%' and last character must +** be a valid conversion specifier. 'flags' are the accepted flags; +** 'precision' signals whether to accept a precision. +*/ +static void checkformat (lua_State *L, const char *form, const char *flags, + int precision) { + const char *spec = form + 1; /* skip '%' */ + spec += strspn(spec, flags); /* skip flags */ + if (*spec != '0') { /* a width cannot start with '0' */ + spec = get2digits(spec); /* skip width */ + if (*spec == '.' && precision) { + spec++; + spec = get2digits(spec); /* skip precision */ + } + } + if (!isalpha(cast_uchar(*spec))) /* did not go to the end? */ + luaL_error(L, "invalid conversion specification: '%s'", form); +} + + +/* +** Get a conversion specification and copy it to 'form'. +** Return the address of its last character. +*/ +static const char *getformat (lua_State *L, const char *strfrmt, + char *form) { + /* spans flags, width, and precision ('0' is included as a flag) */ + size_t len = strspn(strfrmt, L_FMTFLAGSF "123456789."); + len++; /* adds following character (should be the specifier) */ + /* still needs space for '%', '\0', plus a length modifier */ + if (len >= MAX_FORMAT - 10) + luaL_error(L, "invalid format (too long)"); + *(form++) = '%'; + memcpy(form, strfrmt, len * sizeof(char)); + *(form + len) = '\0'; + return strfrmt + len - 1; +} + + +/* +** add length modifier into formats +*/ +static void addlenmod (char *form, const char *lenmod) { + size_t l = strlen(form); + size_t lm = strlen(lenmod); + char spec = form[l - 1]; + strcpy(form + l - 1, lenmod); + form[l + lm - 1] = spec; + form[l + lm] = '\0'; +} + + +static int str_format (lua_State *L) { + int top = lua_gettop(L); + int arg = 1; + size_t sfl; + const char *strfrmt = luaL_checklstring(L, arg, &sfl); + const char *strfrmt_end = strfrmt+sfl; + const char *flags; + luaL_Buffer b; + luaL_buffinit(L, &b); + while (strfrmt < strfrmt_end) { + if (*strfrmt != L_ESC) + luaL_addchar(&b, *strfrmt++); + else if (*++strfrmt == L_ESC) + luaL_addchar(&b, *strfrmt++); /* %% */ + else { /* format item */ + char form[MAX_FORMAT]; /* to store the format ('%...') */ + unsigned maxitem = MAX_ITEM; /* maximum length for the result */ + char *buff = luaL_prepbuffsize(&b, maxitem); /* to put result */ + int nb = 0; /* number of bytes in result */ + if (++arg > top) + return luaL_argerror(L, arg, "no value"); + strfrmt = getformat(L, strfrmt, form); + switch (*strfrmt++) { + case 'c': { + checkformat(L, form, L_FMTFLAGSC, 0); + nb = l_sprintf(buff, maxitem, form, (int)luaL_checkinteger(L, arg)); + break; + } + case 'd': case 'i': + flags = L_FMTFLAGSI; + goto intcase; + case 'u': + flags = L_FMTFLAGSU; + goto intcase; + case 'o': case 'x': case 'X': + flags = L_FMTFLAGSX; + intcase: { + lua_Integer n = luaL_checkinteger(L, arg); + checkformat(L, form, flags, 1); + addlenmod(form, LUA_INTEGER_FRMLEN); + nb = l_sprintf(buff, maxitem, form, (LUAI_UACINT)n); + break; + } + case 'a': case 'A': + checkformat(L, form, L_FMTFLAGSF, 1); + addlenmod(form, LUA_NUMBER_FRMLEN); + nb = lua_number2strx(L, buff, maxitem, form, + luaL_checknumber(L, arg)); + break; + case 'f': + maxitem = MAX_ITEMF; /* extra space for '%f' */ + buff = luaL_prepbuffsize(&b, maxitem); + /* FALLTHROUGH */ + case 'e': case 'E': case 'g': case 'G': { + lua_Number n = luaL_checknumber(L, arg); + checkformat(L, form, L_FMTFLAGSF, 1); + addlenmod(form, LUA_NUMBER_FRMLEN); + nb = l_sprintf(buff, maxitem, form, (LUAI_UACNUMBER)n); + break; + } + case 'p': { + const void *p = lua_topointer(L, arg); + checkformat(L, form, L_FMTFLAGSC, 0); + if (p == NULL) { /* avoid calling 'printf' with argument NULL */ + p = "(null)"; /* result */ + form[strlen(form) - 1] = 's'; /* format it as a string */ + } + nb = l_sprintf(buff, maxitem, form, p); + break; + } + case 'q': { + if (form[2] != '\0') /* modifiers? */ + return luaL_error(L, "specifier '%%q' cannot have modifiers"); + addliteral(L, &b, arg); + break; + } + case 's': { + size_t l; + const char *s = luaL_tolstring(L, arg, &l); + if (form[2] == '\0') /* no modifiers? */ + luaL_addvalue(&b); /* keep entire string */ + else { + luaL_argcheck(L, l == strlen(s), arg, "string contains zeros"); + checkformat(L, form, L_FMTFLAGSC, 1); + if (strchr(form, '.') == NULL && l >= 100) { + /* no precision and string is too long to be formatted */ + luaL_addvalue(&b); /* keep entire string */ + } + else { /* format the string into 'buff' */ + nb = l_sprintf(buff, maxitem, form, s); + lua_pop(L, 1); /* remove result from 'luaL_tolstring' */ + } + } + break; + } + default: { /* also treat cases 'pnLlh' */ + return luaL_error(L, "invalid conversion '%s' to 'format'", form); + } + } + lua_assert(cast_uint(nb) < maxitem); + luaL_addsize(&b, cast_uint(nb)); + } + } + luaL_pushresult(&b); + return 1; +} + +/* }====================================================== */ + + +/* +** {====================================================== +** PACK/UNPACK +** ======================================================= +*/ + + +/* value used for padding */ +#if !defined(LUAL_PACKPADBYTE) +#define LUAL_PACKPADBYTE 0x00 +#endif + +/* maximum size for the binary representation of an integer */ +#define MAXINTSIZE 16 + +/* number of bits in a character */ +#define NB CHAR_BIT + +/* mask for one character (NB 1's) */ +#define MC ((1 << NB) - 1) + +/* size of a lua_Integer */ +#define SZINT ((int)sizeof(lua_Integer)) + + +/* dummy union to get native endianness */ +static const union { + int dummy; + char little; /* true iff machine is little endian */ +} nativeendian = {1}; + + +/* +** information to pack/unpack stuff +*/ +typedef struct Header { + lua_State *L; + int islittle; + unsigned maxalign; +} Header; + + +/* +** options for pack/unpack +*/ +typedef enum KOption { + Kint, /* signed integers */ + Kuint, /* unsigned integers */ + Kfloat, /* single-precision floating-point numbers */ + Knumber, /* Lua "native" floating-point numbers */ + Kdouble, /* double-precision floating-point numbers */ + Kchar, /* fixed-length strings */ + Kstring, /* strings with prefixed length */ + Kzstr, /* zero-terminated strings */ + Kpadding, /* padding */ + Kpaddalign, /* padding for alignment */ + Knop /* no-op (configuration or spaces) */ +} KOption; + + +/* +** Read an integer numeral from string 'fmt' or return 'df' if +** there is no numeral +*/ +static int digit (int c) { return '0' <= c && c <= '9'; } + +static size_t getnum (const char **fmt, size_t df) { + if (!digit(**fmt)) /* no number? */ + return df; /* return default value */ + else { + size_t a = 0; + do { + a = a*10 + cast_uint(*((*fmt)++) - '0'); + } while (digit(**fmt) && a <= (MAX_SIZE - 9)/10); + return a; + } +} + + +/* +** Read an integer numeral and raises an error if it is larger +** than the maximum size of integers. +*/ +static unsigned getnumlimit (Header *h, const char **fmt, size_t df) { + size_t sz = getnum(fmt, df); + if (l_unlikely((sz - 1u) >= MAXINTSIZE)) + return cast_uint(luaL_error(h->L, + "integral size (%d) out of limits [1,%d]", sz, MAXINTSIZE)); + return cast_uint(sz); +} + + +/* +** Initialize Header +*/ +static void initheader (lua_State *L, Header *h) { + h->L = L; + h->islittle = nativeendian.little; + h->maxalign = 1; +} + + +/* +** Read and classify next option. 'size' is filled with option's size. +*/ +static KOption getoption (Header *h, const char **fmt, size_t *size) { + /* dummy structure to get native alignment requirements */ + struct cD { char c; union { LUAI_MAXALIGN; } u; }; + int opt = *((*fmt)++); + *size = 0; /* default */ + switch (opt) { + case 'b': *size = sizeof(char); return Kint; + case 'B': *size = sizeof(char); return Kuint; + case 'h': *size = sizeof(short); return Kint; + case 'H': *size = sizeof(short); return Kuint; + case 'l': *size = sizeof(long); return Kint; + case 'L': *size = sizeof(long); return Kuint; + case 'j': *size = sizeof(lua_Integer); return Kint; + case 'J': *size = sizeof(lua_Integer); return Kuint; + case 'T': *size = sizeof(size_t); return Kuint; + case 'f': *size = sizeof(float); return Kfloat; + case 'n': *size = sizeof(lua_Number); return Knumber; + case 'd': *size = sizeof(double); return Kdouble; + case 'i': *size = getnumlimit(h, fmt, sizeof(int)); return Kint; + case 'I': *size = getnumlimit(h, fmt, sizeof(int)); return Kuint; + case 's': *size = getnumlimit(h, fmt, sizeof(size_t)); return Kstring; + case 'c': + *size = getnum(fmt, cast_sizet(-1)); + if (l_unlikely(*size == cast_sizet(-1))) + luaL_error(h->L, "missing size for format option 'c'"); + return Kchar; + case 'z': return Kzstr; + case 'x': *size = 1; return Kpadding; + case 'X': return Kpaddalign; + case ' ': break; + case '<': h->islittle = 1; break; + case '>': h->islittle = 0; break; + case '=': h->islittle = nativeendian.little; break; + case '!': { + const size_t maxalign = offsetof(struct cD, u); + h->maxalign = getnumlimit(h, fmt, maxalign); + break; + } + default: luaL_error(h->L, "invalid format option '%c'", opt); + } + return Knop; +} + + +/* +** Read, classify, and fill other details about the next option. +** 'psize' is filled with option's size, 'notoalign' with its +** alignment requirements. +** Local variable 'size' gets the size to be aligned. (Kpadal option +** always gets its full alignment, other options are limited by +** the maximum alignment ('maxalign'). Kchar option needs no alignment +** despite its size. +*/ +static KOption getdetails (Header *h, size_t totalsize, const char **fmt, + size_t *psize, unsigned *ntoalign) { + KOption opt = getoption(h, fmt, psize); + size_t align = *psize; /* usually, alignment follows size */ + if (opt == Kpaddalign) { /* 'X' gets alignment from following option */ + if (**fmt == '\0' || getoption(h, fmt, &align) == Kchar || align == 0) + luaL_argerror(h->L, 1, "invalid next option for option 'X'"); + } + if (align <= 1 || opt == Kchar) /* need no alignment? */ + *ntoalign = 0; + else { + if (align > h->maxalign) /* enforce maximum alignment */ + align = h->maxalign; + if (l_unlikely(!ispow2(align))) { /* not a power of 2? */ + *ntoalign = 0; /* to avoid warnings */ + luaL_argerror(h->L, 1, "format asks for alignment not power of 2"); + } + else { + /* 'szmoda' = totalsize % align */ + unsigned szmoda = cast_uint(totalsize & (align - 1)); + *ntoalign = cast_uint((align - szmoda) & (align - 1)); + } + } + return opt; +} + + +/* +** Pack integer 'n' with 'size' bytes and 'islittle' endianness. +** The final 'if' handles the case when 'size' is larger than +** the size of a Lua integer, correcting the extra sign-extension +** bytes if necessary (by default they would be zeros). +*/ +static void packint (luaL_Buffer *b, lua_Unsigned n, + int islittle, unsigned size, int neg) { + char *buff = luaL_prepbuffsize(b, size); + unsigned i; + buff[islittle ? 0 : size - 1] = (char)(n & MC); /* first byte */ + for (i = 1; i < size; i++) { + n >>= NB; + buff[islittle ? i : size - 1 - i] = (char)(n & MC); + } + if (neg && size > SZINT) { /* negative number need sign extension? */ + for (i = SZINT; i < size; i++) /* correct extra bytes */ + buff[islittle ? i : size - 1 - i] = (char)MC; + } + luaL_addsize(b, size); /* add result to buffer */ +} + + +/* +** Copy 'size' bytes from 'src' to 'dest', correcting endianness if +** given 'islittle' is different from native endianness. +*/ +static void copywithendian (char *dest, const char *src, + unsigned size, int islittle) { + if (islittle == nativeendian.little) + memcpy(dest, src, size); + else { + dest += size - 1; + while (size-- != 0) + *(dest--) = *(src++); + } +} + + +static int str_pack (lua_State *L) { + luaL_Buffer b; + Header h; + const char *fmt = luaL_checkstring(L, 1); /* format string */ + int arg = 1; /* current argument to pack */ + size_t totalsize = 0; /* accumulate total size of result */ + initheader(L, &h); + lua_pushnil(L); /* mark to separate arguments from string buffer */ + luaL_buffinit(L, &b); + while (*fmt != '\0') { + unsigned ntoalign; + size_t size; + KOption opt = getdetails(&h, totalsize, &fmt, &size, &ntoalign); + luaL_argcheck(L, size + ntoalign <= MAX_SIZE - totalsize, arg, + "result too long"); + totalsize += ntoalign + size; + while (ntoalign-- > 0) + luaL_addchar(&b, LUAL_PACKPADBYTE); /* fill alignment */ + arg++; + switch (opt) { + case Kint: { /* signed integers */ + lua_Integer n = luaL_checkinteger(L, arg); + if (size < SZINT) { /* need overflow check? */ + lua_Integer lim = (lua_Integer)1 << ((size * NB) - 1); + luaL_argcheck(L, -lim <= n && n < lim, arg, "integer overflow"); + } + packint(&b, (lua_Unsigned)n, h.islittle, cast_uint(size), (n < 0)); + break; + } + case Kuint: { /* unsigned integers */ + lua_Integer n = luaL_checkinteger(L, arg); + if (size < SZINT) /* need overflow check? */ + luaL_argcheck(L, (lua_Unsigned)n < ((lua_Unsigned)1 << (size * NB)), + arg, "unsigned overflow"); + packint(&b, (lua_Unsigned)n, h.islittle, cast_uint(size), 0); + break; + } + case Kfloat: { /* C float */ + float f = (float)luaL_checknumber(L, arg); /* get argument */ + char *buff = luaL_prepbuffsize(&b, sizeof(f)); + /* move 'f' to final result, correcting endianness if needed */ + copywithendian(buff, (char *)&f, sizeof(f), h.islittle); + luaL_addsize(&b, size); + break; + } + case Knumber: { /* Lua float */ + lua_Number f = luaL_checknumber(L, arg); /* get argument */ + char *buff = luaL_prepbuffsize(&b, sizeof(f)); + /* move 'f' to final result, correcting endianness if needed */ + copywithendian(buff, (char *)&f, sizeof(f), h.islittle); + luaL_addsize(&b, size); + break; + } + case Kdouble: { /* C double */ + double f = (double)luaL_checknumber(L, arg); /* get argument */ + char *buff = luaL_prepbuffsize(&b, sizeof(f)); + /* move 'f' to final result, correcting endianness if needed */ + copywithendian(buff, (char *)&f, sizeof(f), h.islittle); + luaL_addsize(&b, size); + break; + } + case Kchar: { /* fixed-size string */ + size_t len; + const char *s = luaL_checklstring(L, arg, &len); + luaL_argcheck(L, len <= size, arg, "string longer than given size"); + luaL_addlstring(&b, s, len); /* add string */ + if (len < size) { /* does it need padding? */ + size_t psize = size - len; /* pad size */ + char *buff = luaL_prepbuffsize(&b, psize); + memset(buff, LUAL_PACKPADBYTE, psize); + luaL_addsize(&b, psize); + } + break; + } + case Kstring: { /* strings with length count */ + size_t len; + const char *s = luaL_checklstring(L, arg, &len); + luaL_argcheck(L, size >= sizeof(lua_Unsigned) || + len < ((lua_Unsigned)1 << (size * NB)), + arg, "string length does not fit in given size"); + /* pack length */ + packint(&b, (lua_Unsigned)len, h.islittle, cast_uint(size), 0); + luaL_addlstring(&b, s, len); + totalsize += len; + break; + } + case Kzstr: { /* zero-terminated string */ + size_t len; + const char *s = luaL_checklstring(L, arg, &len); + luaL_argcheck(L, strlen(s) == len, arg, "string contains zeros"); + luaL_addlstring(&b, s, len); + luaL_addchar(&b, '\0'); /* add zero at the end */ + totalsize += len + 1; + break; + } + case Kpadding: luaL_addchar(&b, LUAL_PACKPADBYTE); /* FALLTHROUGH */ + case Kpaddalign: case Knop: + arg--; /* undo increment */ + break; + } + } + luaL_pushresult(&b); + return 1; +} + + +static int str_packsize (lua_State *L) { + Header h; + const char *fmt = luaL_checkstring(L, 1); /* format string */ + size_t totalsize = 0; /* accumulate total size of result */ + initheader(L, &h); + while (*fmt != '\0') { + unsigned ntoalign; + size_t size; + KOption opt = getdetails(&h, totalsize, &fmt, &size, &ntoalign); + luaL_argcheck(L, opt != Kstring && opt != Kzstr, 1, + "variable-length format"); + size += ntoalign; /* total space used by option */ + luaL_argcheck(L, totalsize <= MAX_SIZE - size, + 1, "format result too large"); + totalsize += size; + } + lua_pushinteger(L, cast_st2S(totalsize)); + return 1; +} + + +/* +** Unpack an integer with 'size' bytes and 'islittle' endianness. +** If size is smaller than the size of a Lua integer and integer +** is signed, must do sign extension (propagating the sign to the +** higher bits); if size is larger than the size of a Lua integer, +** it must check the unread bytes to see whether they do not cause an +** overflow. +*/ +static lua_Integer unpackint (lua_State *L, const char *str, + int islittle, int size, int issigned) { + lua_Unsigned res = 0; + int i; + int limit = (size <= SZINT) ? size : SZINT; + for (i = limit - 1; i >= 0; i--) { + res <<= NB; + res |= (lua_Unsigned)(unsigned char)str[islittle ? i : size - 1 - i]; + } + if (size < SZINT) { /* real size smaller than lua_Integer? */ + if (issigned) { /* needs sign extension? */ + lua_Unsigned mask = (lua_Unsigned)1 << (size*NB - 1); + res = ((res ^ mask) - mask); /* do sign extension */ + } + } + else if (size > SZINT) { /* must check unread bytes */ + int mask = (!issigned || (lua_Integer)res >= 0) ? 0 : MC; + for (i = limit; i < size; i++) { + if (l_unlikely((unsigned char)str[islittle ? i : size - 1 - i] != mask)) + luaL_error(L, "%d-byte integer does not fit into Lua Integer", size); + } + } + return (lua_Integer)res; +} + + +static int str_unpack (lua_State *L) { + Header h; + const char *fmt = luaL_checkstring(L, 1); + size_t ld; + const char *data = luaL_checklstring(L, 2, &ld); + size_t pos = posrelatI(luaL_optinteger(L, 3, 1), ld) - 1; + int n = 0; /* number of results */ + luaL_argcheck(L, pos <= ld, 3, "initial position out of string"); + initheader(L, &h); + while (*fmt != '\0') { + unsigned ntoalign; + size_t size; + KOption opt = getdetails(&h, pos, &fmt, &size, &ntoalign); + luaL_argcheck(L, ntoalign + size <= ld - pos, 2, + "data string too short"); + pos += ntoalign; /* skip alignment */ + /* stack space for item + next position */ + luaL_checkstack(L, 2, "too many results"); + n++; + switch (opt) { + case Kint: + case Kuint: { + lua_Integer res = unpackint(L, data + pos, h.islittle, + cast_int(size), (opt == Kint)); + lua_pushinteger(L, res); + break; + } + case Kfloat: { + float f; + copywithendian((char *)&f, data + pos, sizeof(f), h.islittle); + lua_pushnumber(L, (lua_Number)f); + break; + } + case Knumber: { + lua_Number f; + copywithendian((char *)&f, data + pos, sizeof(f), h.islittle); + lua_pushnumber(L, f); + break; + } + case Kdouble: { + double f; + copywithendian((char *)&f, data + pos, sizeof(f), h.islittle); + lua_pushnumber(L, (lua_Number)f); + break; + } + case Kchar: { + lua_pushlstring(L, data + pos, size); + break; + } + case Kstring: { + lua_Unsigned len = (lua_Unsigned)unpackint(L, data + pos, + h.islittle, cast_int(size), 0); + luaL_argcheck(L, len <= ld - pos - size, 2, "data string too short"); + lua_pushlstring(L, data + pos + size, cast_sizet(len)); + pos += cast_sizet(len); /* skip string */ + break; + } + case Kzstr: { + size_t len = strlen(data + pos); + luaL_argcheck(L, pos + len < ld, 2, + "unfinished string for format 'z'"); + lua_pushlstring(L, data + pos, len); + pos += len + 1; /* skip string plus final '\0' */ + break; + } + case Kpaddalign: case Kpadding: case Knop: + n--; /* undo increment */ + break; + } + pos += size; + } + lua_pushinteger(L, cast_st2S(pos) + 1); /* next position */ + return n + 1; +} + +/* }====================================================== */ + + +static const luaL_Reg strlib[] = { + {"byte", str_byte}, + {"char", str_char}, + {"dump", str_dump}, + {"find", str_find}, + {"format", str_format}, + {"gmatch", gmatch}, + {"gsub", str_gsub}, + {"len", str_len}, + {"lower", str_lower}, + {"match", str_match}, + {"rep", str_rep}, + {"reverse", str_reverse}, + {"sub", str_sub}, + {"upper", str_upper}, + {"pack", str_pack}, + {"packsize", str_packsize}, + {"unpack", str_unpack}, + {NULL, NULL} +}; + + +static void createmetatable (lua_State *L) { + /* table to be metatable for strings */ + luaL_newlibtable(L, stringmetamethods); + luaL_setfuncs(L, stringmetamethods, 0); + lua_pushliteral(L, ""); /* dummy string */ + lua_pushvalue(L, -2); /* copy table */ + lua_setmetatable(L, -2); /* set table as metatable for strings */ + lua_pop(L, 1); /* pop dummy string */ + lua_pushvalue(L, -2); /* get string library */ + lua_setfield(L, -2, "__index"); /* metatable.__index = string */ + lua_pop(L, 1); /* pop metatable */ +} + + +/* +** Open string library +*/ +LUAMOD_API int luaopen_string (lua_State *L) { + luaL_newlib(L, strlib); + createmetatable(L); + return 1; +} diff --git a/crates/lua/vendor/lua/ltable.c b/crates/lua/vendor/lua/ltable.c new file mode 100644 index 0000000..2f2b5c1 --- /dev/null +++ b/crates/lua/vendor/lua/ltable.c @@ -0,0 +1,1355 @@ +/* +** $Id: ltable.c $ +** Lua tables (hash) +** See Copyright Notice in lua.h +*/ + +#define ltable_c +#define LUA_CORE + +#include "lprefix.h" + + +/* +** Implementation of tables (aka arrays, objects, or hash tables). +** Tables keep its elements in two parts: an array part and a hash part. +** Non-negative integer keys are all candidates to be kept in the array +** part. The actual size of the array is the largest 'n' such that +** more than half the slots between 1 and n are in use. +** Hash uses a mix of chained scatter table with Brent's variation. +** A main invariant of these tables is that, if an element is not +** in its main position (i.e. the 'original' position that its hash gives +** to it), then the colliding element is in its own main position. +** Hence even when the load factor reaches 100%, performance remains good. +*/ + +#include +#include +#include + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lgc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "lvm.h" + + +/* +** Only hash parts with at least 2^LIMFORLAST have a 'lastfree' field +** that optimizes finding a free slot. That field is stored just before +** the array of nodes, in the same block. Smaller tables do a complete +** search when looking for a free slot. +*/ +#define LIMFORLAST 3 /* log2 of real limit (8) */ + +/* +** The union 'Limbox' stores 'lastfree' and ensures that what follows it +** is properly aligned to store a Node. +*/ +typedef struct { Node *dummy; Node follows_pNode; } Limbox_aux; + +typedef union { + Node *lastfree; + char padding[offsetof(Limbox_aux, follows_pNode)]; +} Limbox; + +#define haslastfree(t) ((t)->lsizenode >= LIMFORLAST) +#define getlastfree(t) ((cast(Limbox *, (t)->node) - 1)->lastfree) + + +/* +** MAXABITS is the largest integer such that 2^MAXABITS fits in an +** unsigned int. +*/ +#define MAXABITS (l_numbits(int) - 1) + + +/* +** MAXASIZEB is the maximum number of elements in the array part such +** that the size of the array fits in 'size_t'. +*/ +#define MAXASIZEB (MAX_SIZET/(sizeof(Value) + 1)) + + +/* +** MAXASIZE is the maximum size of the array part. It is the minimum +** between 2^MAXABITS and MAXASIZEB. +*/ +#define MAXASIZE \ + (((1u << MAXABITS) < MAXASIZEB) ? (1u << MAXABITS) : cast_uint(MAXASIZEB)) + +/* +** MAXHBITS is the largest integer such that 2^MAXHBITS fits in a +** signed int. +*/ +#define MAXHBITS (MAXABITS - 1) + + +/* +** MAXHSIZE is the maximum size of the hash part. It is the minimum +** between 2^MAXHBITS and the maximum size such that, measured in bytes, +** it fits in a 'size_t'. +*/ +#define MAXHSIZE luaM_limitN(1 << MAXHBITS, Node) + + +/* +** When the original hash value is good, hashing by a power of 2 +** avoids the cost of '%'. +*/ +#define hashpow2(t,n) (gnode(t, lmod((n), sizenode(t)))) + +/* +** for other types, it is better to avoid modulo by power of 2, as +** they can have many 2 factors. +*/ +#define hashmod(t,n) (gnode(t, ((n) % ((sizenode(t)-1u)|1u)))) + + +#define hashstr(t,str) hashpow2(t, (str)->hash) +#define hashboolean(t,p) hashpow2(t, p) + + +#define hashpointer(t,p) hashmod(t, point2uint(p)) + + +#define dummynode (&dummynode_) + +/* +** Common hash part for tables with empty hash parts. That allows all +** tables to have a hash part, avoiding an extra check ("is there a hash +** part?") when indexing. Its sole node has an empty value and a key +** (DEADKEY, NULL) that is different from any valid TValue. +*/ +static const Node dummynode_ = { + {{NULL}, LUA_VEMPTY, /* value's value and type */ + LUA_TDEADKEY, 0, {NULL}} /* key type, next, and key value */ +}; + + +static const TValue absentkey = {ABSTKEYCONSTANT}; + + +/* +** Hash for integers. To allow a good hash, use the remainder operator +** ('%'). If integer fits as a non-negative int, compute an int +** remainder, which is faster. Otherwise, use an unsigned-integer +** remainder, which uses all bits and ensures a non-negative result. +*/ +static Node *hashint (const Table *t, lua_Integer i) { + lua_Unsigned ui = l_castS2U(i); + if (ui <= cast_uint(INT_MAX)) + return gnode(t, cast_int(ui) % cast_int((sizenode(t)-1) | 1)); + else + return hashmod(t, ui); +} + + +/* +** Hash for floating-point numbers. +** The main computation should be just +** n = frexp(n, &i); return (n * INT_MAX) + i +** but there are some numerical subtleties. +** In a two-complement representation, INT_MAX may not have an exact +** representation as a float, but INT_MIN does; because the absolute +** value of 'frexp' is smaller than 1 (unless 'n' is inf/NaN), the +** absolute value of the product 'frexp * -INT_MIN' is smaller or equal +** to INT_MAX. Next, the use of 'unsigned int' avoids overflows when +** adding 'i'; the use of '~u' (instead of '-u') avoids problems with +** INT_MIN. +*/ +#if !defined(l_hashfloat) +static unsigned l_hashfloat (lua_Number n) { + int i; + lua_Integer ni; + n = l_mathop(frexp)(n, &i) * -cast_num(INT_MIN); + if (!lua_numbertointeger(n, &ni)) { /* is 'n' inf/-inf/NaN? */ + lua_assert(luai_numisnan(n) || l_mathop(fabs)(n) == cast_num(HUGE_VAL)); + return 0; + } + else { /* normal case */ + unsigned int u = cast_uint(i) + cast_uint(ni); + return (u <= cast_uint(INT_MAX) ? u : ~u); + } +} +#endif + + +/* +** returns the 'main' position of an element in a table (that is, +** the index of its hash value). +*/ +static Node *mainpositionTV (const Table *t, const TValue *key) { + switch (ttypetag(key)) { + case LUA_VNUMINT: { + lua_Integer i = ivalue(key); + return hashint(t, i); + } + case LUA_VNUMFLT: { + lua_Number n = fltvalue(key); + return hashmod(t, l_hashfloat(n)); + } + case LUA_VSHRSTR: { + TString *ts = tsvalue(key); + return hashstr(t, ts); + } + case LUA_VLNGSTR: { + TString *ts = tsvalue(key); + return hashpow2(t, luaS_hashlongstr(ts)); + } + case LUA_VFALSE: + return hashboolean(t, 0); + case LUA_VTRUE: + return hashboolean(t, 1); + case LUA_VLIGHTUSERDATA: { + void *p = pvalue(key); + return hashpointer(t, p); + } + case LUA_VLCF: { + lua_CFunction f = fvalue(key); + return hashpointer(t, f); + } + default: { + GCObject *o = gcvalue(key); + return hashpointer(t, o); + } + } +} + + +l_sinline Node *mainpositionfromnode (const Table *t, Node *nd) { + TValue key; + getnodekey(cast(lua_State *, NULL), &key, nd); + return mainpositionTV(t, &key); +} + + +/* +** Check whether key 'k1' is equal to the key in node 'n2'. This +** equality is raw, so there are no metamethods. Floats with integer +** values have been normalized, so integers cannot be equal to +** floats. It is assumed that 'eqshrstr' is simply pointer equality, +** so that short strings are handled in the default case. The flag +** 'deadok' means to accept dead keys as equal to their original values. +** (Only collectable objects can produce dead keys.) Note that dead +** long strings are also compared by identity. Once a key is dead, +** its corresponding value may be collected, and then another value +** can be created with the same address. If this other value is given +** to 'next', 'equalkey' will signal a false positive. In a regular +** traversal, this situation should never happen, as all keys given to +** 'next' came from the table itself, and therefore could not have been +** collected. Outside a regular traversal, we have garbage in, garbage +** out. What is relevant is that this false positive does not break +** anything. (In particular, 'next' will return some other valid item +** on the table or nil.) +*/ +static int equalkey (const TValue *k1, const Node *n2, int deadok) { + if (rawtt(k1) != keytt(n2)) { /* not the same variants? */ + if (keyisshrstr(n2) && ttislngstring(k1)) { + /* an external string can be equal to a short-string key */ + return luaS_eqstr(tsvalue(k1), keystrval(n2)); + } + else if (deadok && keyisdead(n2) && iscollectable(k1)) { + /* a collectable value can be equal to a dead key */ + return gcvalue(k1) == gcvalueraw(keyval(n2)); + } + else + return 0; /* otherwise, different variants cannot be equal */ + } + else { /* equal variants */ + switch (keytt(n2)) { + case LUA_VNIL: case LUA_VFALSE: case LUA_VTRUE: + return 1; + case LUA_VNUMINT: + return (ivalue(k1) == keyival(n2)); + case LUA_VNUMFLT: + return luai_numeq(fltvalue(k1), fltvalueraw(keyval(n2))); + case LUA_VLIGHTUSERDATA: + return pvalue(k1) == pvalueraw(keyval(n2)); + case LUA_VLCF: + return fvalue(k1) == fvalueraw(keyval(n2)); + case ctb(LUA_VLNGSTR): + return luaS_eqstr(tsvalue(k1), keystrval(n2)); + default: + return gcvalue(k1) == gcvalueraw(keyval(n2)); + } + } +} + + +/* +** "Generic" get version. (Not that generic: not valid for integers, +** which may be in array part, nor for floats with integral values.) +** See explanation about 'deadok' in function 'equalkey'. +*/ +static const TValue *getgeneric (Table *t, const TValue *key, int deadok) { + Node *n = mainpositionTV(t, key); + for (;;) { /* check whether 'key' is somewhere in the chain */ + if (equalkey(key, n, deadok)) + return gval(n); /* that's it */ + else { + int nx = gnext(n); + if (nx == 0) + return &absentkey; /* not found */ + n += nx; + } + } +} + + +/* +** Return the index 'k' (converted to an unsigned) if it is inside +** the range [1, limit]. +*/ +static unsigned checkrange (lua_Integer k, unsigned limit) { + return (l_castS2U(k) - 1u < limit) ? cast_uint(k) : 0; +} + + +/* +** Return the index 'k' if 'k' is an appropriate key to live in the +** array part of a table, 0 otherwise. +*/ +#define arrayindex(k) checkrange(k, MAXASIZE) + + +/* +** Check whether an integer key is in the array part of a table and +** return its index there, or zero. +*/ +#define ikeyinarray(t,k) checkrange(k, t->asize) + + +/* +** Check whether a key is in the array part of a table and return its +** index there, or zero. +*/ +static unsigned keyinarray (Table *t, const TValue *key) { + return (ttisinteger(key)) ? ikeyinarray(t, ivalue(key)) : 0; +} + + +/* +** returns the index of a 'key' for table traversals. First goes all +** elements in the array part, then elements in the hash part. The +** beginning of a traversal is signaled by 0. +*/ +static unsigned findindex (lua_State *L, Table *t, TValue *key, + unsigned asize) { + unsigned int i; + if (ttisnil(key)) return 0; /* first iteration */ + i = keyinarray(t, key); + if (i != 0) /* is 'key' inside array part? */ + return i; /* yes; that's the index */ + else { + const TValue *n = getgeneric(t, key, 1); + if (l_unlikely(isabstkey(n))) + luaG_runerror(L, "invalid key to 'next'"); /* key not found */ + i = cast_uint(nodefromval(n) - gnode(t, 0)); /* key index in hash table */ + /* hash elements are numbered after array ones */ + return (i + 1) + asize; + } +} + + +int luaH_next (lua_State *L, Table *t, StkId key) { + unsigned int asize = t->asize; + unsigned int i = findindex(L, t, s2v(key), asize); /* find original key */ + for (; i < asize; i++) { /* try first array part */ + lu_byte tag = *getArrTag(t, i); + if (!tagisempty(tag)) { /* a non-empty entry? */ + setivalue(s2v(key), cast_int(i) + 1); + farr2val(t, i, tag, s2v(key + 1)); + return 1; + } + } + for (i -= asize; i < sizenode(t); i++) { /* hash part */ + if (!isempty(gval(gnode(t, i)))) { /* a non-empty entry? */ + Node *n = gnode(t, i); + getnodekey(L, s2v(key), n); + setobj2s(L, key + 1, gval(n)); + return 1; + } + } + return 0; /* no more elements */ +} + + +/* Extra space in Node array if it has a lastfree entry */ +#define extraLastfree(t) (haslastfree(t) ? sizeof(Limbox) : 0) + +/* 'node' size in bytes */ +static size_t sizehash (Table *t) { + return cast_sizet(sizenode(t)) * sizeof(Node) + extraLastfree(t); +} + + +static void freehash (lua_State *L, Table *t) { + if (!isdummy(t)) { + /* get pointer to the beginning of Node array */ + char *arr = cast_charp(t->node) - extraLastfree(t); + luaM_freearray(L, arr, sizehash(t)); + } +} + + +/* +** {============================================================= +** Rehash +** ============================================================== +*/ + +static int insertkey (Table *t, const TValue *key, TValue *value); +static void newcheckedkey (Table *t, const TValue *key, TValue *value); + + +/* +** Structure to count the keys in a table. +** 'total' is the total number of keys in the table. +** 'na' is the number of *array indices* in the table (see 'arrayindex'). +** 'deleted' is true if there are deleted nodes in the hash part. +** 'nums' is a "count array" where 'nums[i]' is the number of integer +** keys between 2^(i - 1) + 1 and 2^i. Note that 'na' is the summation +** of 'nums'. +*/ +typedef struct { + unsigned total; + unsigned na; + int deleted; + unsigned nums[MAXABITS + 1]; +} Counters; + + +/* +** Check whether it is worth to use 'na' array entries instead of 'nh' +** hash nodes. (A hash node uses ~3 times more memory than an array +** entry: Two values plus 'next' versus one value.) Evaluate with size_t +** to avoid overflows. +*/ +#define arrayXhash(na,nh) (cast_sizet(na) <= cast_sizet(nh) * 3) + +/* +** Compute the optimal size for the array part of table 't'. +** This size maximizes the number of elements going to the array part +** while satisfying the condition 'arrayXhash' with the use of memory if +** all those elements went to the hash part. +** 'ct->na' enters with the total number of array indices in the table +** and leaves with the number of keys that will go to the array part; +** return the optimal size for the array part. +*/ +static unsigned computesizes (Counters *ct) { + int i; + unsigned int twotoi; /* 2^i (candidate for optimal size) */ + unsigned int a = 0; /* number of elements smaller than 2^i */ + unsigned int na = 0; /* number of elements to go to array part */ + unsigned int optimal = 0; /* optimal size for array part */ + /* traverse slices while 'twotoi' does not overflow and total of array + indices still can satisfy 'arrayXhash' against the array size */ + for (i = 0, twotoi = 1; + twotoi > 0 && arrayXhash(twotoi, ct->na); + i++, twotoi *= 2) { + unsigned nums = ct->nums[i]; + a += nums; + if (nums > 0 && /* grows array only if it gets more elements... */ + arrayXhash(twotoi, a)) { /* ...while using "less memory" */ + optimal = twotoi; /* optimal size (till now) */ + na = a; /* all elements up to 'optimal' will go to array part */ + } + } + ct->na = na; + return optimal; +} + + +static void countint (lua_Integer key, Counters *ct) { + unsigned int k = arrayindex(key); + if (k != 0) { /* is 'key' an array index? */ + ct->nums[luaO_ceillog2(k)]++; /* count as such */ + ct->na++; + } +} + + +l_sinline int arraykeyisempty (const Table *t, unsigned key) { + int tag = *getArrTag(t, key - 1); + return tagisempty(tag); +} + + +/* +** Count keys in array part of table 't'. +*/ +static void numusearray (const Table *t, Counters *ct) { + int lg; + unsigned int ttlg; /* 2^lg */ + unsigned int ause = 0; /* summation of 'nums' */ + unsigned int i = 1; /* index to traverse all array keys */ + unsigned int asize = t->asize; + /* traverse each slice */ + for (lg = 0, ttlg = 1; lg <= MAXABITS; lg++, ttlg *= 2) { + unsigned int lc = 0; /* counter */ + unsigned int lim = ttlg; + if (lim > asize) { + lim = asize; /* adjust upper limit */ + if (i > lim) + break; /* no more elements to count */ + } + /* count elements in range (2^(lg - 1), 2^lg] */ + for (; i <= lim; i++) { + if (!arraykeyisempty(t, i)) + lc++; + } + ct->nums[lg] += lc; + ause += lc; + } + ct->total += ause; + ct->na += ause; +} + + +/* +** Count keys in hash part of table 't'. As this only happens during +** a rehash, all nodes have been used. A node can have a nil value only +** if it was deleted after being created. +*/ +static void numusehash (const Table *t, Counters *ct) { + unsigned i = sizenode(t); + unsigned total = 0; + while (i--) { + Node *n = &t->node[i]; + if (isempty(gval(n))) { + lua_assert(!keyisnil(n)); /* entry was deleted; key cannot be nil */ + ct->deleted = 1; + } + else { + total++; + if (keyisinteger(n)) + countint(keyival(n), ct); + } + } + ct->total += total; +} + + +/* +** Convert an "abstract size" (number of slots in an array) to +** "concrete size" (number of bytes in the array). +*/ +static size_t concretesize (unsigned int size) { + if (size == 0) + return 0; + else /* space for the two arrays plus an unsigned in between */ + return size * (sizeof(Value) + 1) + sizeof(unsigned); +} + + +/* +** Resize the array part of a table. If new size is equal to the old, +** do nothing. Else, if new size is zero, free the old array. (It must +** be present, as the sizes are different.) Otherwise, allocate a new +** array, move the common elements to new proper position, and then +** frees the old array. +** We could reallocate the array, but we still would need to move the +** elements to their new position, so the copy implicit in realloc is a +** waste. Moreover, most allocators will move the array anyway when the +** new size is double the old one (the most common case). +*/ +static Value *resizearray (lua_State *L , Table *t, + unsigned oldasize, + unsigned newasize) { + if (oldasize == newasize) + return t->array; /* nothing to be done */ + else if (newasize == 0) { /* erasing array? */ + Value *op = t->array - oldasize; /* original array's real address */ + luaM_freemem(L, op, concretesize(oldasize)); /* free it */ + return NULL; + } + else { + size_t newasizeb = concretesize(newasize); + Value *np = cast(Value *, + luaM_reallocvector(L, NULL, 0, newasizeb, lu_byte)); + if (np == NULL) /* allocation error? */ + return NULL; + np += newasize; /* shift pointer to the end of value segment */ + if (oldasize > 0) { + /* move common elements to new position */ + size_t oldasizeb = concretesize(oldasize); + Value *op = t->array; /* original array */ + unsigned tomove = (oldasize < newasize) ? oldasize : newasize; + size_t tomoveb = (oldasize < newasize) ? oldasizeb : newasizeb; + lua_assert(tomoveb > 0); + memcpy(np - tomove, op - tomove, tomoveb); + luaM_freemem(L, op - oldasize, oldasizeb); /* free old block */ + } + return np; + } +} + + +/* +** Creates an array for the hash part of a table with the given +** size, or reuses the dummy node if size is zero. +** The computation for size overflow is in two steps: the first +** comparison ensures that the shift in the second one does not +** overflow. +*/ +static void setnodevector (lua_State *L, Table *t, unsigned size) { + if (size == 0) { /* no elements to hash part? */ + t->node = cast(Node *, dummynode); /* use common 'dummynode' */ + t->lsizenode = 0; + setdummy(t); /* signal that it is using dummy node */ + } + else { + int i; + int lsize = luaO_ceillog2(size); + if (lsize > MAXHBITS || (1 << lsize) > MAXHSIZE) + luaG_runerror(L, "table overflow"); + size = twoto(lsize); + if (lsize < LIMFORLAST) /* no 'lastfree' field? */ + t->node = luaM_newvector(L, size, Node); + else { + size_t bsize = size * sizeof(Node) + sizeof(Limbox); + char *node = luaM_newblock(L, bsize); + t->node = cast(Node *, node + sizeof(Limbox)); + getlastfree(t) = gnode(t, size); /* all positions are free */ + } + t->lsizenode = cast_byte(lsize); + setnodummy(t); + for (i = 0; i < cast_int(size); i++) { + Node *n = gnode(t, i); + gnext(n) = 0; + setnilkey(n); + setempty(gval(n)); + } + } +} + + +/* +** (Re)insert all elements from the hash part of 'ot' into table 't'. +*/ +static void reinserthash (lua_State *L, Table *ot, Table *t) { + unsigned j; + unsigned size = sizenode(ot); + for (j = 0; j < size; j++) { + Node *old = gnode(ot, j); + if (!isempty(gval(old))) { + /* doesn't need barrier/invalidate cache, as entry was + already present in the table */ + TValue k; + getnodekey(L, &k, old); + newcheckedkey(t, &k, gval(old)); + } + } +} + + +/* +** Exchange the hash part of 't1' and 't2'. (In 'flags', only the dummy +** bit must be exchanged: The metamethod bits do not change during a +** resize, so the "real" table can keep their values.) +*/ +static void exchangehashpart (Table *t1, Table *t2) { + lu_byte lsizenode = t1->lsizenode; + Node *node = t1->node; + int bitdummy1 = t1->flags & BITDUMMY; + t1->lsizenode = t2->lsizenode; + t1->node = t2->node; + t1->flags = cast_byte((t1->flags & NOTBITDUMMY) | (t2->flags & BITDUMMY)); + t2->lsizenode = lsizenode; + t2->node = node; + t2->flags = cast_byte((t2->flags & NOTBITDUMMY) | bitdummy1); +} + + +/* +** Re-insert into the new hash part of a table the elements from the +** vanishing slice of the array part. +*/ +static void reinsertOldSlice (Table *t, unsigned oldasize, + unsigned newasize) { + unsigned i; + for (i = newasize; i < oldasize; i++) { /* traverse vanishing slice */ + lu_byte tag = *getArrTag(t, i); + if (!tagisempty(tag)) { /* a non-empty entry? */ + TValue key, aux; + setivalue(&key, l_castU2S(i) + 1); /* make the key */ + farr2val(t, i, tag, &aux); /* copy value into 'aux' */ + insertkey(t, &key, &aux); /* insert entry into the hash part */ + } + } +} + + +/* +** Clear new slice of the array. +*/ +static void clearNewSlice (Table *t, unsigned oldasize, unsigned newasize) { + for (; oldasize < newasize; oldasize++) + *getArrTag(t, oldasize) = LUA_VEMPTY; +} + + +/* +** Resize table 't' for the new given sizes. Both allocations (for +** the hash part and for the array part) can fail, which creates some +** subtleties. If the first allocation, for the hash part, fails, an +** error is raised and that is it. Otherwise, it copies the elements from +** the shrinking part of the array (if it is shrinking) into the new +** hash. Then it reallocates the array part. If that fails, the table +** is in its original state; the function frees the new hash part and then +** raises the allocation error. Otherwise, it sets the new hash part +** into the table, initializes the new part of the array (if any) with +** nils and reinserts the elements of the old hash back into the new +** parts of the table. +** Note that if the new size for the array part ('newasize') is equal to +** the old one ('oldasize'), this function will do nothing with that +** part. +*/ +void luaH_resize (lua_State *L, Table *t, unsigned newasize, + unsigned nhsize) { + Table newt; /* to keep the new hash part */ + unsigned oldasize = t->asize; + Value *newarray; + if (newasize > MAXASIZE) + luaG_runerror(L, "table overflow"); + /* create new hash part with appropriate size into 'newt' */ + newt.flags = 0; + setnodevector(L, &newt, nhsize); + if (newasize < oldasize) { /* will array shrink? */ + /* re-insert into the new hash the elements from vanishing slice */ + exchangehashpart(t, &newt); /* pretend table has new hash */ + reinsertOldSlice(t, oldasize, newasize); + exchangehashpart(t, &newt); /* restore old hash (in case of errors) */ + } + /* allocate new array */ + newarray = resizearray(L, t, oldasize, newasize); + if (l_unlikely(newarray == NULL && newasize > 0)) { /* allocation failed? */ + freehash(L, &newt); /* release new hash part */ + luaM_error(L); /* raise error (with array unchanged) */ + } + /* allocation ok; initialize new part of the array */ + exchangehashpart(t, &newt); /* 't' has the new hash ('newt' has the old) */ + t->array = newarray; /* set new array part */ + t->asize = newasize; + if (newarray != NULL) + *lenhint(t) = newasize / 2u; /* set an initial hint */ + clearNewSlice(t, oldasize, newasize); + /* re-insert elements from old hash part into new parts */ + reinserthash(L, &newt, t); /* 'newt' now has the old hash */ + freehash(L, &newt); /* free old hash part */ +} + + +void luaH_resizearray (lua_State *L, Table *t, unsigned int nasize) { + unsigned nsize = allocsizenode(t); + luaH_resize(L, t, nasize, nsize); +} + + +/* +** Rehash a table. First, count its keys. If there are array indices +** outside the array part, compute the new best size for that part. +** Then, resize the table. +*/ +static void rehash (lua_State *L, Table *t, const TValue *ek) { + unsigned asize; /* optimal size for array part */ + Counters ct; + unsigned i; + unsigned nsize; /* size for the hash part */ + /* reset counts */ + for (i = 0; i <= MAXABITS; i++) ct.nums[i] = 0; + ct.na = 0; + ct.deleted = 0; + ct.total = 1; /* count extra key */ + if (ttisinteger(ek)) + countint(ivalue(ek), &ct); /* extra key may go to array */ + numusehash(t, &ct); /* count keys in hash part */ + if (ct.na == 0) { + /* no new keys to enter array part; keep it with the same size */ + asize = t->asize; + } + else { /* compute best size for array part */ + numusearray(t, &ct); /* count keys in array part */ + asize = computesizes(&ct); /* compute new size for array part */ + } + /* all keys not in the array part go to the hash part */ + nsize = ct.total - ct.na; + if (ct.deleted) { /* table has deleted entries? */ + /* insertion-deletion-insertion: give hash some extra size to + avoid repeated resizings */ + nsize += nsize >> 2; + } + /* resize the table to new computed sizes */ + luaH_resize(L, t, asize, nsize); +} + +/* +** }============================================================= +*/ + + +Table *luaH_new (lua_State *L) { + GCObject *o = luaC_newobj(L, LUA_VTABLE, sizeof(Table)); + Table *t = gco2t(o); + t->metatable = NULL; + t->flags = maskflags; /* table has no metamethod fields */ + t->array = NULL; + t->asize = 0; + setnodevector(L, t, 0); + return t; +} + + +lu_mem luaH_size (Table *t) { + lu_mem sz = cast(lu_mem, sizeof(Table)) + concretesize(t->asize); + if (!isdummy(t)) + sz += sizehash(t); + return sz; +} + + +/* +** Frees a table. +*/ +void luaH_free (lua_State *L, Table *t) { + freehash(L, t); + resizearray(L, t, t->asize, 0); + luaM_free(L, t); +} + + +static Node *getfreepos (Table *t) { + if (haslastfree(t)) { /* does it have 'lastfree' information? */ + /* look for a spot before 'lastfree', updating 'lastfree' */ + while (getlastfree(t) > t->node) { + Node *free = --getlastfree(t); + if (keyisnil(free)) + return free; + } + } + else { /* no 'lastfree' information */ + unsigned i = sizenode(t); + while (i--) { /* do a linear search */ + Node *free = gnode(t, i); + if (keyisnil(free)) + return free; + } + } + return NULL; /* could not find a free place */ +} + + + +/* +** Inserts a new key into a hash table; first, check whether key's main +** position is free. If not, check whether colliding node is in its main +** position or not: if it is not, move colliding node to an empty place +** and put new key in its main position; otherwise (colliding node is in +** its main position), new key goes to an empty position. Return 0 if +** could not insert key (could not find a free space). +*/ +static int insertkey (Table *t, const TValue *key, TValue *value) { + Node *mp = mainpositionTV(t, key); + /* table cannot already contain the key */ + lua_assert(isabstkey(getgeneric(t, key, 0))); + if (!isempty(gval(mp)) || isdummy(t)) { /* main position is taken? */ + Node *othern; + Node *f = getfreepos(t); /* get a free place */ + if (f == NULL) /* cannot find a free place? */ + return 0; + lua_assert(!isdummy(t)); + othern = mainpositionfromnode(t, mp); + if (othern != mp) { /* is colliding node out of its main position? */ + /* yes; move colliding node into free position */ + while (othern + gnext(othern) != mp) /* find previous */ + othern += gnext(othern); + gnext(othern) = cast_int(f - othern); /* rechain to point to 'f' */ + *f = *mp; /* copy colliding node into free pos. (mp->next also goes) */ + if (gnext(mp) != 0) { + gnext(f) += cast_int(mp - f); /* correct 'next' */ + gnext(mp) = 0; /* now 'mp' is free */ + } + setempty(gval(mp)); + } + else { /* colliding node is in its own main position */ + /* new node will go into free position */ + if (gnext(mp) != 0) + gnext(f) = cast_int((mp + gnext(mp)) - f); /* chain new position */ + else lua_assert(gnext(f) == 0); + gnext(mp) = cast_int(f - mp); + mp = f; + } + } + setnodekey(mp, key); + lua_assert(isempty(gval(mp))); + setobj2t(cast(lua_State *, 0), gval(mp), value); + return 1; +} + + +/* +** Insert a key in a table where there is space for that key, the +** key is valid, and the value is not nil. +*/ +static void newcheckedkey (Table *t, const TValue *key, TValue *value) { + unsigned i = keyinarray(t, key); + if (i > 0) /* is key in the array part? */ + obj2arr(t, i - 1, value); /* set value in the array */ + else { + int done = insertkey(t, key, value); /* insert key in the hash part */ + lua_assert(done); /* it cannot fail */ + cast(void, done); /* to avoid warnings */ + } +} + + +static void luaH_newkey (lua_State *L, Table *t, const TValue *key, + TValue *value) { + if (!ttisnil(value)) { /* do not insert nil values */ + int done = insertkey(t, key, value); + if (!done) { /* could not find a free place? */ + rehash(L, t, key); /* grow table */ + newcheckedkey(t, key, value); /* insert key in grown table */ + } + luaC_barrierback(L, obj2gco(t), key); + /* for debugging only: any new key may force an emergency collection */ + condchangemem(L, (void)0, (void)0, 1); + } +} + + +static const TValue *getintfromhash (Table *t, lua_Integer key) { + Node *n = hashint(t, key); + lua_assert(!ikeyinarray(t, key)); + for (;;) { /* check whether 'key' is somewhere in the chain */ + if (keyisinteger(n) && keyival(n) == key) + return gval(n); /* that's it */ + else { + int nx = gnext(n); + if (nx == 0) break; + n += nx; + } + } + return &absentkey; +} + + +static int hashkeyisempty (Table *t, lua_Unsigned key) { + const TValue *val = getintfromhash(t, l_castU2S(key)); + return isempty(val); +} + + +static lu_byte finishnodeget (const TValue *val, TValue *res) { + if (!ttisnil(val)) { + setobj(((lua_State*)NULL), res, val); + } + return ttypetag(val); +} + + +lu_byte luaH_getint (Table *t, lua_Integer key, TValue *res) { + unsigned k = ikeyinarray(t, key); + if (k > 0) { + lu_byte tag = *getArrTag(t, k - 1); + if (!tagisempty(tag)) + farr2val(t, k - 1, tag, res); + return tag; + } + else + return finishnodeget(getintfromhash(t, key), res); +} + + +/* +** search function for short strings +*/ +const TValue *luaH_Hgetshortstr (Table *t, TString *key) { + Node *n = hashstr(t, key); + lua_assert(strisshr(key)); + for (;;) { /* check whether 'key' is somewhere in the chain */ + if (keyisshrstr(n) && eqshrstr(keystrval(n), key)) + return gval(n); /* that's it */ + else { + int nx = gnext(n); + if (nx == 0) + return &absentkey; /* not found */ + n += nx; + } + } +} + + +lu_byte luaH_getshortstr (Table *t, TString *key, TValue *res) { + return finishnodeget(luaH_Hgetshortstr(t, key), res); +} + + +static const TValue *Hgetlongstr (Table *t, TString *key) { + TValue ko; + lua_assert(!strisshr(key)); + setsvalue(cast(lua_State *, NULL), &ko, key); + return getgeneric(t, &ko, 0); /* for long strings, use generic case */ +} + + +static const TValue *Hgetstr (Table *t, TString *key) { + if (strisshr(key)) + return luaH_Hgetshortstr(t, key); + else + return Hgetlongstr(t, key); +} + + +lu_byte luaH_getstr (Table *t, TString *key, TValue *res) { + return finishnodeget(Hgetstr(t, key), res); +} + + +/* +** main search function +*/ +lu_byte luaH_get (Table *t, const TValue *key, TValue *res) { + const TValue *slot; + switch (ttypetag(key)) { + case LUA_VSHRSTR: + slot = luaH_Hgetshortstr(t, tsvalue(key)); + break; + case LUA_VNUMINT: + return luaH_getint(t, ivalue(key), res); + case LUA_VNIL: + slot = &absentkey; + break; + case LUA_VNUMFLT: { + lua_Integer k; + if (luaV_flttointeger(fltvalue(key), &k, F2Ieq)) /* integral index? */ + return luaH_getint(t, k, res); /* use specialized version */ + /* else... */ + } /* FALLTHROUGH */ + default: + slot = getgeneric(t, key, 0); + break; + } + return finishnodeget(slot, res); +} + + +/* +** When a 'pset' cannot be completed, this function returns an encoding +** of its result, to be used by 'luaH_finishset'. +*/ +static int retpsetcode (Table *t, const TValue *slot) { + if (isabstkey(slot)) + return HNOTFOUND; /* no slot with that key */ + else /* return node encoded */ + return cast_int((cast(Node*, slot) - t->node)) + HFIRSTNODE; +} + + +static int finishnodeset (Table *t, const TValue *slot, TValue *val) { + if (!ttisnil(slot)) { + setobj(((lua_State*)NULL), cast(TValue*, slot), val); + return HOK; /* success */ + } + else + return retpsetcode(t, slot); +} + + +static int rawfinishnodeset (const TValue *slot, TValue *val) { + if (isabstkey(slot)) + return 0; /* no slot with that key */ + else { + setobj(((lua_State*)NULL), cast(TValue*, slot), val); + return 1; /* success */ + } +} + + +int luaH_psetint (Table *t, lua_Integer key, TValue *val) { + lua_assert(!ikeyinarray(t, key)); + return finishnodeset(t, getintfromhash(t, key), val); +} + + +static int psetint (Table *t, lua_Integer key, TValue *val) { + int hres; + luaH_fastseti(t, key, val, hres); + return hres; +} + + +/* +** This function could be just this: +** return finishnodeset(t, luaH_Hgetshortstr(t, key), val); +** However, it optimizes the common case created by constructors (e.g., +** {x=1, y=2}), which creates a key in a table that has no metatable, +** it is not old/black, and it already has space for the key. +*/ + +int luaH_psetshortstr (Table *t, TString *key, TValue *val) { + const TValue *slot = luaH_Hgetshortstr(t, key); + if (!ttisnil(slot)) { /* key already has a value? (all too common) */ + setobj(((lua_State*)NULL), cast(TValue*, slot), val); /* update it */ + return HOK; /* done */ + } + else if (checknoTM(t->metatable, TM_NEWINDEX)) { /* no metamethod? */ + if (ttisnil(val)) /* new value is nil? */ + return HOK; /* done (value is already nil/absent) */ + if (isabstkey(slot) && /* key is absent? */ + !(isblack(t) && iswhite(key))) { /* and don't need barrier? */ + TValue tk; /* key as a TValue */ + setsvalue(cast(lua_State *, NULL), &tk, key); + if (insertkey(t, &tk, val)) { /* insert key, if there is space */ + invalidateTMcache(t); + return HOK; + } + } + } + /* Else, either table has new-index metamethod, or it needs barrier, + or it needs to rehash for the new key. In any of these cases, the + operation cannot be completed here. Return a code for the caller. */ + return retpsetcode(t, slot); +} + + +int luaH_psetstr (Table *t, TString *key, TValue *val) { + if (strisshr(key)) + return luaH_psetshortstr(t, key, val); + else + return finishnodeset(t, Hgetlongstr(t, key), val); +} + + +int luaH_pset (Table *t, const TValue *key, TValue *val) { + switch (ttypetag(key)) { + case LUA_VSHRSTR: return luaH_psetshortstr(t, tsvalue(key), val); + case LUA_VNUMINT: return psetint(t, ivalue(key), val); + case LUA_VNIL: return HNOTFOUND; + case LUA_VNUMFLT: { + lua_Integer k; + if (luaV_flttointeger(fltvalue(key), &k, F2Ieq)) /* integral index? */ + return psetint(t, k, val); /* use specialized version */ + /* else... */ + } /* FALLTHROUGH */ + default: + return finishnodeset(t, getgeneric(t, key, 0), val); + } +} + +/* +** Finish a raw "set table" operation, where 'hres' encodes where the +** value should have been (the result of a previous 'pset' operation). +** Beware: when using this function the caller probably need to check a +** GC barrier and invalidate the TM cache. +*/ +void luaH_finishset (lua_State *L, Table *t, const TValue *key, + TValue *value, int hres) { + lua_assert(hres != HOK); + if (hres == HNOTFOUND) { + TValue aux; + const TValue *actk = key; /* actual key to insert */ + if (l_unlikely(ttisnil(key))) + luaG_runerror(L, "table index is nil"); + else if (ttisfloat(key)) { + lua_Number f = fltvalue(key); + lua_Integer k; + if (luaV_flttointeger(f, &k, F2Ieq)) { /* is key equal to an integer? */ + setivalue(&aux, k); + actk = &aux; /* use the integer as the key */ + } + else if (l_unlikely(luai_numisnan(f))) + luaG_runerror(L, "table index is NaN"); + } + else if (isextstr(key)) { /* external string? */ + /* If string is short, must internalize it to be used as table key */ + TString *ts = luaS_normstr(L, tsvalue(key)); + setsvalue2s(L, L->top.p++, ts); /* anchor 'ts' (EXTRA_STACK) */ + luaH_newkey(L, t, s2v(L->top.p - 1), value); + L->top.p--; + return; + } + luaH_newkey(L, t, actk, value); + } + else if (hres > 0) { /* regular Node? */ + setobj2t(L, gval(gnode(t, hres - HFIRSTNODE)), value); + } + else { /* array entry */ + hres = ~hres; /* real index */ + obj2arr(t, cast_uint(hres), value); + } +} + + +/* +** beware: when using this function you probably need to check a GC +** barrier and invalidate the TM cache. +*/ +void luaH_set (lua_State *L, Table *t, const TValue *key, TValue *value) { + int hres = luaH_pset(t, key, value); + if (hres != HOK) + luaH_finishset(L, t, key, value, hres); +} + + +/* +** Ditto for a GC barrier. (No need to invalidate the TM cache, as +** integers cannot be keys to metamethods.) +*/ +void luaH_setint (lua_State *L, Table *t, lua_Integer key, TValue *value) { + unsigned ik = ikeyinarray(t, key); + if (ik > 0) + obj2arr(t, ik - 1, value); + else { + int ok = rawfinishnodeset(getintfromhash(t, key), value); + if (!ok) { + TValue k; + setivalue(&k, key); + luaH_newkey(L, t, &k, value); + } + } +} + + +/* +** Try to find a boundary in the hash part of table 't'. From the +** caller, we know that 'asize + 1' is present. We want to find a larger +** key that is absent from the table, so that we can do a binary search +** between the two keys to find a boundary. We keep doubling 'j' until +** we get an absent index. If the doubling would overflow, we try +** LUA_MAXINTEGER. If it is absent, we are ready for the binary search. +** ('j', being max integer, is larger or equal to 'i', but it cannot be +** equal because it is absent while 'i' is present.) Otherwise, 'j' is a +** boundary. ('j + 1' cannot be a present integer key because it is not +** a valid integer in Lua.) +** About 'rnd': If we used a fixed algorithm, a bad actor could fill +** a table with only the keys that would be probed, in such a way that +** a small table could result in a huge length. To avoid that, we use +** the state's seed as a source of randomness. For the first probe, +** we "randomly double" 'i' by adding to it a random number roughly its +** width. +*/ +static lua_Unsigned hash_search (lua_State *L, Table *t, unsigned asize) { + lua_Unsigned i = asize + 1; /* caller ensures t[i] is present */ + unsigned rnd = G(L)->seed; + int n = (asize > 0) ? luaO_ceillog2(asize) : 0; /* width of 'asize' */ + unsigned mask = (1u << n) - 1; /* 11...111 with the width of 'asize' */ + unsigned incr = (rnd & mask) + 1; /* first increment (at least 1) */ + lua_Unsigned j = (incr <= l_castS2U(LUA_MAXINTEGER) - i) ? i + incr : i + 1; + rnd >>= n; /* used 'n' bits from 'rnd' */ + while (!hashkeyisempty(t, j)) { /* repeat until an absent t[j] */ + i = j; /* 'i' is a present index */ + if (j <= l_castS2U(LUA_MAXINTEGER)/2 - 1) { + j = j*2 + (rnd & 1); /* try again with 2j or 2j+1 */ + rnd >>= 1; + } + else { + j = LUA_MAXINTEGER; + if (hashkeyisempty(t, j)) /* t[j] not present? */ + break; /* 'j' now is an absent index */ + else /* weird case */ + return j; /* well, max integer is a boundary... */ + } + } + /* i < j && t[i] present && t[j] absent */ + while (j - i > 1u) { /* do a binary search between them */ + lua_Unsigned m = (i + j) / 2; + if (hashkeyisempty(t, m)) j = m; + else i = m; + } + return i; +} + + +static unsigned int binsearch (Table *array, unsigned int i, unsigned int j) { + lua_assert(i <= j); + while (j - i > 1u) { /* binary search */ + unsigned int m = (i + j) / 2; + if (arraykeyisempty(array, m)) j = m; + else i = m; + } + return i; +} + + +/* return a border, saving it as a hint for next call */ +static lua_Unsigned newhint (Table *t, unsigned hint) { + lua_assert(hint <= t->asize); + *lenhint(t) = hint; + return hint; +} + + +/* +** Try to find a border in table 't'. (A 'border' is an integer index +** such that t[i] is present and t[i+1] is absent, or 0 if t[1] is absent, +** or 'maxinteger' if t[maxinteger] is present.) +** If there is an array part, try to find a border there. First try +** to find it in the vicinity of the previous result (hint), to handle +** cases like 't[#t + 1] = val' or 't[#t] = nil', that move the border +** by one entry. Otherwise, do a binary search to find the border. +** If there is no array part, or its last element is non empty, the +** border may be in the hash part. +*/ +lua_Unsigned luaH_getn (lua_State *L, Table *t) { + unsigned asize = t->asize; + if (asize > 0) { /* is there an array part? */ + const unsigned maxvicinity = 4; + unsigned limit = *lenhint(t); /* start with the hint */ + if (limit == 0) + limit = 1; /* make limit a valid index in the array */ + if (arraykeyisempty(t, limit)) { /* t[limit] empty? */ + /* there must be a border before 'limit' */ + unsigned i; + /* look for a border in the vicinity of the hint */ + for (i = 0; i < maxvicinity && limit > 1; i++) { + limit--; + if (!arraykeyisempty(t, limit)) + return newhint(t, limit); /* 'limit' is a border */ + } + /* t[limit] still empty; search for a border in [0, limit) */ + return newhint(t, binsearch(t, 0, limit)); + } + else { /* 'limit' is present in table; look for a border after it */ + unsigned i; + /* look for a border in the vicinity of the hint */ + for (i = 0; i < maxvicinity && limit < asize; i++) { + limit++; + if (arraykeyisempty(t, limit)) + return newhint(t, limit - 1); /* 'limit - 1' is a border */ + } + if (arraykeyisempty(t, asize)) { /* last element empty? */ + /* t[limit] not empty; search for a border in [limit, asize) */ + return newhint(t, binsearch(t, limit, asize)); + } + } + /* last element non empty; set a hint to speed up finding that again */ + /* (keys in the hash part cannot be hints) */ + *lenhint(t) = asize; + } + /* no array part or t[asize] is not empty; check the hash part */ + lua_assert(asize == 0 || !arraykeyisempty(t, asize)); + if (isdummy(t) || hashkeyisempty(t, asize + 1)) + return asize; /* 'asize + 1' is empty */ + else /* 'asize + 1' is also non empty */ + return hash_search(L, t, asize); +} + + + +#if defined(LUA_DEBUG) + +/* export this function for the test library */ + +Node *luaH_mainposition (const Table *t, const TValue *key) { + return mainpositionTV(t, key); +} + +#endif diff --git a/crates/lua/vendor/lua/ltable.h b/crates/lua/vendor/lua/ltable.h new file mode 100644 index 0000000..f3b7bc7 --- /dev/null +++ b/crates/lua/vendor/lua/ltable.h @@ -0,0 +1,184 @@ +/* +** $Id: ltable.h $ +** Lua tables (hash) +** See Copyright Notice in lua.h +*/ + +#ifndef ltable_h +#define ltable_h + +#include "lobject.h" + + +#define gnode(t,i) (&(t)->node[i]) +#define gval(n) (&(n)->i_val) +#define gnext(n) ((n)->u.next) + + +/* +** Clear all bits of fast-access metamethods, which means that the table +** may have any of these metamethods. (First access that fails after the +** clearing will set the bit again.) +*/ +#define invalidateTMcache(t) ((t)->flags &= cast_byte(~maskflags)) + + +/* +** Bit BITDUMMY set in 'flags' means the table is using the dummy node +** for its hash part. +*/ + +#define BITDUMMY (1 << 6) +#define NOTBITDUMMY cast_byte(~BITDUMMY) +#define isdummy(t) ((t)->flags & BITDUMMY) + +#define setnodummy(t) ((t)->flags &= NOTBITDUMMY) +#define setdummy(t) ((t)->flags |= BITDUMMY) + + + +/* allocated size for hash nodes */ +#define allocsizenode(t) (isdummy(t) ? 0 : sizenode(t)) + + +/* returns the Node, given the value of a table entry */ +#define nodefromval(v) cast(Node *, (v)) + + + +#define luaH_fastgeti(t,k,res,tag) \ + { Table *h = t; lua_Unsigned u = l_castS2U(k) - 1u; \ + if ((u < h->asize)) { \ + tag = *getArrTag(h, u); \ + if (!tagisempty(tag)) { farr2val(h, u, tag, res); }} \ + else { tag = luaH_getint(h, (k), res); }} + + +#define luaH_fastseti(t,k,val,hres) \ + { Table *h = t; lua_Unsigned u = l_castS2U(k) - 1u; \ + if ((u < h->asize)) { \ + lu_byte *tag = getArrTag(h, u); \ + if (checknoTM(h->metatable, TM_NEWINDEX) || !tagisempty(*tag)) \ + { fval2arr(h, u, tag, val); hres = HOK; } \ + else hres = ~cast_int(u); } \ + else { hres = luaH_psetint(h, k, val); }} + + +/* results from pset */ +#define HOK 0 +#define HNOTFOUND 1 +#define HNOTATABLE 2 +#define HFIRSTNODE 3 + +/* +** 'luaH_get*' operations set 'res', unless the value is absent, and +** return the tag of the result. +** The 'luaH_pset*' (pre-set) operations set the given value and return +** HOK, unless the original value is absent. In that case, if the key +** is really absent, they return HNOTFOUND. Otherwise, if there is a +** slot with that key but with no value, 'luaH_pset*' return an encoding +** of where the key is (usually called 'hres'). (pset cannot set that +** value because there might be a metamethod.) If the slot is in the +** hash part, the encoding is (HFIRSTNODE + hash index); if the slot is +** in the array part, the encoding is (~array index), a negative value. +** The value HNOTATABLE is used by the fast macros to signal that the +** value being indexed is not a table. +** (The size for the array part is limited by the maximum power of two +** that fits in an unsigned integer; that is INT_MAX+1. So, the C-index +** ranges from 0, which encodes to -1, to INT_MAX, which encodes to +** INT_MIN. The size of the hash part is limited by the maximum power of +** two that fits in a signed integer; that is (INT_MAX+1)/2. So, it is +** safe to add HFIRSTNODE to any index there.) +*/ + + +/* +** The array part of a table is represented by an inverted array of +** values followed by an array of tags, to avoid wasting space with +** padding. In between them there is an unsigned int, explained later. +** The 'array' pointer points between the two arrays, so that values are +** indexed with negative indices and tags with non-negative indices. + + Values Tags + -------------------------------------------------------- + ... | Value 1 | Value 0 |unsigned|0|1|... + -------------------------------------------------------- + ^ t->array + +** All accesses to 't->array' should be through the macros 'getArrTag' +** and 'getArrVal'. +*/ + +/* Computes the address of the tag for the abstract C-index 'k' */ +#define getArrTag(t,k) (cast(lu_byte*, (t)->array) + sizeof(unsigned) + (k)) + +/* Computes the address of the value for the abstract C-index 'k' */ +#define getArrVal(t,k) ((t)->array - 1 - (k)) + + +/* +** The unsigned between the two arrays is used as a hint for #t; +** see luaH_getn. It is stored there to avoid wasting space in +** the structure Table for tables with no array part. +*/ +#define lenhint(t) cast(unsigned*, (t)->array) + + +/* +** Move TValues to/from arrays, using C indices +*/ +#define arr2obj(h,k,val) \ + ((val)->tt_ = *getArrTag(h,(k)), (val)->value_ = *getArrVal(h,(k))) + +#define obj2arr(h,k,val) \ + (*getArrTag(h,(k)) = (val)->tt_, *getArrVal(h,(k)) = (val)->value_) + + +/* +** Often, we need to check the tag of a value before moving it. The +** following macros also move TValues to/from arrays, but receive the +** precomputed tag value or address as an extra argument. +*/ +#define farr2val(h,k,tag,res) \ + ((res)->tt_ = tag, (res)->value_ = *getArrVal(h,(k))) + +#define fval2arr(h,k,tag,val) \ + (*tag = (val)->tt_, *getArrVal(h,(k)) = (val)->value_) + + +LUAI_FUNC lu_byte luaH_get (Table *t, const TValue *key, TValue *res); +LUAI_FUNC lu_byte luaH_getshortstr (Table *t, TString *key, TValue *res); +LUAI_FUNC lu_byte luaH_getstr (Table *t, TString *key, TValue *res); +LUAI_FUNC lu_byte luaH_getint (Table *t, lua_Integer key, TValue *res); + +/* Special get for metamethods */ +LUAI_FUNC const TValue *luaH_Hgetshortstr (Table *t, TString *key); + +LUAI_FUNC int luaH_psetint (Table *t, lua_Integer key, TValue *val); +LUAI_FUNC int luaH_psetshortstr (Table *t, TString *key, TValue *val); +LUAI_FUNC int luaH_psetstr (Table *t, TString *key, TValue *val); +LUAI_FUNC int luaH_pset (Table *t, const TValue *key, TValue *val); + +LUAI_FUNC void luaH_setint (lua_State *L, Table *t, lua_Integer key, + TValue *value); +LUAI_FUNC void luaH_set (lua_State *L, Table *t, const TValue *key, + TValue *value); + +LUAI_FUNC void luaH_finishset (lua_State *L, Table *t, const TValue *key, + TValue *value, int hres); +LUAI_FUNC Table *luaH_new (lua_State *L); +LUAI_FUNC void luaH_resize (lua_State *L, Table *t, unsigned nasize, + unsigned nhsize); +LUAI_FUNC void luaH_resizearray (lua_State *L, Table *t, unsigned nasize); +LUAI_FUNC lu_mem luaH_size (Table *t); +LUAI_FUNC void luaH_free (lua_State *L, Table *t); +LUAI_FUNC int luaH_next (lua_State *L, Table *t, StkId key); +LUAI_FUNC lua_Unsigned luaH_getn (lua_State *L, Table *t); + + +#if defined(LUA_DEBUG) +LUAI_FUNC Node *luaH_mainposition (const Table *t, const TValue *key); +#endif + + +#endif diff --git a/crates/lua/vendor/lua/ltablib.c b/crates/lua/vendor/lua/ltablib.c new file mode 100644 index 0000000..b79f6ba --- /dev/null +++ b/crates/lua/vendor/lua/ltablib.c @@ -0,0 +1,428 @@ +/* +** $Id: ltablib.c $ +** Library for Table Manipulation +** See Copyright Notice in lua.h +*/ + +#define ltablib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include +#include +#include + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" +#include "llimits.h" + + +/* +** Operations that an object must define to mimic a table +** (some functions only need some of them) +*/ +#define TAB_R 1 /* read */ +#define TAB_W 2 /* write */ +#define TAB_L 4 /* length */ +#define TAB_RW (TAB_R | TAB_W) /* read/write */ + + +#define aux_getn(L,n,w) (checktab(L, n, (w) | TAB_L), luaL_len(L, n)) + + +static int checkfield (lua_State *L, const char *key, int n) { + lua_pushstring(L, key); + return (lua_rawget(L, -n) != LUA_TNIL); +} + + +/* +** Check that 'arg' either is a table or can behave like one (that is, +** has a metatable with the required metamethods). +*/ +static void checktab (lua_State *L, int arg, int what) { + int tp = lua_type(L, arg); + if (tp != LUA_TTABLE) { /* is it not a table? */ + int n = 1; /* number of elements to pop */ + if (lua_getmetatable(L, arg) && /* must have metatable */ + (!(what & TAB_R) || checkfield(L, "__index", ++n)) && + (!(what & TAB_W) || checkfield(L, "__newindex", ++n)) && + (!(what & TAB_L) || /* strings don't need '__len' to have a length */ + tp == LUA_TSTRING || checkfield(L, "__len", ++n))) { + lua_pop(L, n); /* pop metatable and tested metamethods */ + } + else + luaL_checktype(L, arg, LUA_TTABLE); /* force an error */ + } +} + + +static int tcreate (lua_State *L) { + lua_Unsigned sizeseq = (lua_Unsigned)luaL_checkinteger(L, 1); + lua_Unsigned sizerest = (lua_Unsigned)luaL_optinteger(L, 2, 0); + luaL_argcheck(L, sizeseq <= cast_uint(INT_MAX), 1, "out of range"); + luaL_argcheck(L, sizerest <= cast_uint(INT_MAX), 2, "out of range"); + lua_createtable(L, cast_int(sizeseq), cast_int(sizerest)); + return 1; +} + + +static int tinsert (lua_State *L) { + lua_Integer pos; /* where to insert new element */ + lua_Integer e = aux_getn(L, 1, TAB_RW); + e = luaL_intop(+, e, 1); /* first empty element */ + switch (lua_gettop(L)) { + case 2: { /* called with only 2 arguments */ + pos = e; /* insert new element at the end */ + break; + } + case 3: { + lua_Integer i; + pos = luaL_checkinteger(L, 2); /* 2nd argument is the position */ + /* check whether 'pos' is in [1, e] */ + luaL_argcheck(L, (lua_Unsigned)pos - 1u < (lua_Unsigned)e, 2, + "position out of bounds"); + for (i = e; i > pos; i--) { /* move up elements */ + lua_geti(L, 1, i - 1); + lua_seti(L, 1, i); /* t[i] = t[i - 1] */ + } + break; + } + default: { + return luaL_error(L, "wrong number of arguments to 'insert'"); + } + } + lua_seti(L, 1, pos); /* t[pos] = v */ + return 0; +} + + +static int tremove (lua_State *L) { + lua_Integer size = aux_getn(L, 1, TAB_RW); + lua_Integer pos = luaL_optinteger(L, 2, size); + if (pos != size) /* validate 'pos' if given */ + /* check whether 'pos' is in [1, size + 1] */ + luaL_argcheck(L, (lua_Unsigned)pos - 1u <= (lua_Unsigned)size, 2, + "position out of bounds"); + lua_geti(L, 1, pos); /* result = t[pos] */ + for ( ; pos < size; pos++) { + lua_geti(L, 1, pos + 1); + lua_seti(L, 1, pos); /* t[pos] = t[pos + 1] */ + } + lua_pushnil(L); + lua_seti(L, 1, pos); /* remove entry t[pos] */ + return 1; +} + + +/* +** Copy elements (1[f], ..., 1[e]) into (tt[t], tt[t+1], ...). Whenever +** possible, copy in increasing order, which is better for rehashing. +** "possible" means destination after original range, or smaller +** than origin, or copying to another table. +*/ +static int tmove (lua_State *L) { + lua_Integer f = luaL_checkinteger(L, 2); + lua_Integer e = luaL_checkinteger(L, 3); + lua_Integer t = luaL_checkinteger(L, 4); + int tt = !lua_isnoneornil(L, 5) ? 5 : 1; /* destination table */ + checktab(L, 1, TAB_R); + checktab(L, tt, TAB_W); + if (e >= f) { /* otherwise, nothing to move */ + lua_Integer n, i; + luaL_argcheck(L, f > 0 || e < LUA_MAXINTEGER + f, 3, + "too many elements to move"); + n = e - f + 1; /* number of elements to move */ + luaL_argcheck(L, t <= LUA_MAXINTEGER - n + 1, 4, + "destination wrap around"); + if (t > e || t <= f || (tt != 1 && !lua_compare(L, 1, tt, LUA_OPEQ))) { + for (i = 0; i < n; i++) { + lua_geti(L, 1, f + i); + lua_seti(L, tt, t + i); + } + } + else { + for (i = n - 1; i >= 0; i--) { + lua_geti(L, 1, f + i); + lua_seti(L, tt, t + i); + } + } + } + lua_pushvalue(L, tt); /* return destination table */ + return 1; +} + + +static void addfield (lua_State *L, luaL_Buffer *b, lua_Integer i) { + lua_geti(L, 1, i); + if (l_unlikely(!lua_isstring(L, -1))) + luaL_error(L, "invalid value (%s) at index %I in table for 'concat'", + luaL_typename(L, -1), (LUAI_UACINT)i); + luaL_addvalue(b); +} + + +static int tconcat (lua_State *L) { + luaL_Buffer b; + lua_Integer last = aux_getn(L, 1, TAB_R); + size_t lsep; + const char *sep = luaL_optlstring(L, 2, "", &lsep); + lua_Integer i = luaL_optinteger(L, 3, 1); + last = luaL_optinteger(L, 4, last); + luaL_buffinit(L, &b); + for (; i < last; i++) { + addfield(L, &b, i); + luaL_addlstring(&b, sep, lsep); + } + if (i == last) /* add last value (if interval was not empty) */ + addfield(L, &b, i); + luaL_pushresult(&b); + return 1; +} + + +/* +** {====================================================== +** Pack/unpack +** ======================================================= +*/ + +static int tpack (lua_State *L) { + int i; + int n = lua_gettop(L); /* number of elements to pack */ + lua_createtable(L, n, 1); /* create result table */ + lua_insert(L, 1); /* put it at index 1 */ + for (i = n; i >= 1; i--) /* assign elements */ + lua_seti(L, 1, i); + lua_pushinteger(L, n); + lua_setfield(L, 1, "n"); /* t.n = number of elements */ + return 1; /* return table */ +} + + +static int tunpack (lua_State *L) { + lua_Unsigned n; + lua_Integer len = aux_getn(L, 1, TAB_R); + lua_Integer i = luaL_optinteger(L, 2, 1); + lua_Integer e = luaL_opt(L, luaL_checkinteger, 3, len); + if (i > e) return 0; /* empty range */ + n = l_castS2U(e) - l_castS2U(i); /* number of elements minus 1 */ + if (l_unlikely(n >= (unsigned int)INT_MAX || + !lua_checkstack(L, (int)(++n)))) + return luaL_error(L, "too many results to unpack"); + for (; i < e; i++) { /* push arg[i..e - 1] (to avoid overflows) */ + lua_geti(L, 1, i); + } + lua_geti(L, 1, e); /* push last element */ + return (int)n; +} + +/* }====================================================== */ + + + +/* +** {====================================================== +** Quicksort +** (based on 'Algorithms in MODULA-3', Robert Sedgewick; +** Addison-Wesley, 1993.) +** ======================================================= +*/ + + +/* +** Type for array indices. These indices are always limited by INT_MAX, +** so it is safe to cast them to lua_Integer even for Lua 32 bits. +*/ +typedef unsigned int IdxT; + + +/* Versions of lua_seti/lua_geti specialized for IdxT */ +#define geti(L,idt,idx) lua_geti(L, idt, l_castU2S(idx)) +#define seti(L,idt,idx) lua_seti(L, idt, l_castU2S(idx)) + + +/* +** Produce a "random" 'unsigned int' to randomize pivot choice. This +** macro is used only when 'sort' detects a big imbalance in the result +** of a partition. (If you don't want/need this "randomness", ~0 is a +** good choice.) +*/ +#if !defined(l_randomizePivot) +#define l_randomizePivot(L) luaL_makeseed(L) +#endif /* } */ + + +/* arrays larger than 'RANLIMIT' may use randomized pivots */ +#define RANLIMIT 100u + + +static void set2 (lua_State *L, IdxT i, IdxT j) { + seti(L, 1, i); + seti(L, 1, j); +} + + +/* +** Return true iff value at stack index 'a' is less than the value at +** index 'b' (according to the order of the sort). +*/ +static int sort_comp (lua_State *L, int a, int b) { + if (lua_isnil(L, 2)) /* no function? */ + return lua_compare(L, a, b, LUA_OPLT); /* a < b */ + else { /* function */ + int res; + lua_pushvalue(L, 2); /* push function */ + lua_pushvalue(L, a-1); /* -1 to compensate function */ + lua_pushvalue(L, b-2); /* -2 to compensate function and 'a' */ + lua_call(L, 2, 1); /* call function */ + res = lua_toboolean(L, -1); /* get result */ + lua_pop(L, 1); /* pop result */ + return res; + } +} + + +/* +** Does the partition: Pivot P is at the top of the stack. +** precondition: a[lo] <= P == a[up-1] <= a[up], +** so it only needs to do the partition from lo + 1 to up - 2. +** Pos-condition: a[lo .. i - 1] <= a[i] == P <= a[i + 1 .. up] +** returns 'i'. +*/ +static IdxT partition (lua_State *L, IdxT lo, IdxT up) { + IdxT i = lo; /* will be incremented before first use */ + IdxT j = up - 1; /* will be decremented before first use */ + /* loop invariant: a[lo .. i] <= P <= a[j .. up] */ + for (;;) { + /* next loop: repeat ++i while a[i] < P */ + while ((void)geti(L, 1, ++i), sort_comp(L, -1, -2)) { + if (l_unlikely(i == up - 1)) /* a[up - 1] < P == a[up - 1] */ + luaL_error(L, "invalid order function for sorting"); + lua_pop(L, 1); /* remove a[i] */ + } + /* after the loop, a[i] >= P and a[lo .. i - 1] < P (a) */ + /* next loop: repeat --j while P < a[j] */ + while ((void)geti(L, 1, --j), sort_comp(L, -3, -1)) { + if (l_unlikely(j < i)) /* j <= i - 1 and a[j] > P, contradicts (a) */ + luaL_error(L, "invalid order function for sorting"); + lua_pop(L, 1); /* remove a[j] */ + } + /* after the loop, a[j] <= P and a[j + 1 .. up] >= P */ + if (j < i) { /* no elements out of place? */ + /* a[lo .. i - 1] <= P <= a[j + 1 .. i .. up] */ + lua_pop(L, 1); /* pop a[j] */ + /* swap pivot (a[up - 1]) with a[i] to satisfy pos-condition */ + set2(L, up - 1, i); + return i; + } + /* otherwise, swap a[i] - a[j] to restore invariant and repeat */ + set2(L, i, j); + } +} + + +/* +** Choose an element in the middle (2nd-3th quarters) of [lo,up] +** "randomized" by 'rnd' +*/ +static IdxT choosePivot (IdxT lo, IdxT up, unsigned int rnd) { + IdxT r4 = (up - lo) / 4; /* range/4 */ + IdxT p = (rnd ^ lo ^ up) % (r4 * 2) + (lo + r4); + lua_assert(lo + r4 <= p && p <= up - r4); + return p; +} + + +/* +** Quicksort algorithm (recursive function) +*/ +static void auxsort (lua_State *L, IdxT lo, IdxT up, unsigned rnd) { + while (lo < up) { /* loop for tail recursion */ + IdxT p; /* Pivot index */ + IdxT n; /* to be used later */ + /* sort elements 'lo', 'p', and 'up' */ + geti(L, 1, lo); + geti(L, 1, up); + if (sort_comp(L, -1, -2)) /* a[up] < a[lo]? */ + set2(L, lo, up); /* swap a[lo] - a[up] */ + else + lua_pop(L, 2); /* remove both values */ + if (up - lo == 1) /* only 2 elements? */ + return; /* already sorted */ + if (up - lo < RANLIMIT || rnd == 0) /* small interval or no randomize? */ + p = (lo + up)/2; /* middle element is a good pivot */ + else /* for larger intervals, it is worth a random pivot */ + p = choosePivot(lo, up, rnd); + geti(L, 1, p); + geti(L, 1, lo); + if (sort_comp(L, -2, -1)) /* a[p] < a[lo]? */ + set2(L, p, lo); /* swap a[p] - a[lo] */ + else { + lua_pop(L, 1); /* remove a[lo] */ + geti(L, 1, up); + if (sort_comp(L, -1, -2)) /* a[up] < a[p]? */ + set2(L, p, up); /* swap a[up] - a[p] */ + else + lua_pop(L, 2); + } + if (up - lo == 2) /* only 3 elements? */ + return; /* already sorted */ + geti(L, 1, p); /* get middle element (Pivot) */ + lua_pushvalue(L, -1); /* push Pivot */ + geti(L, 1, up - 1); /* push a[up - 1] */ + set2(L, p, up - 1); /* swap Pivot (a[p]) with a[up - 1] */ + p = partition(L, lo, up); + /* a[lo .. p - 1] <= a[p] == P <= a[p + 1 .. up] */ + if (p - lo < up - p) { /* lower interval is smaller? */ + auxsort(L, lo, p - 1, rnd); /* call recursively for lower interval */ + n = p - lo; /* size of smaller interval */ + lo = p + 1; /* tail call for [p + 1 .. up] (upper interval) */ + } + else { + auxsort(L, p + 1, up, rnd); /* call recursively for upper interval */ + n = up - p; /* size of smaller interval */ + up = p - 1; /* tail call for [lo .. p - 1] (lower interval) */ + } + if ((up - lo) / 128 > n) /* partition too imbalanced? */ + rnd = l_randomizePivot(L); /* try a new randomization */ + } /* tail call auxsort(L, lo, up, rnd) */ +} + + +static int sort (lua_State *L) { + lua_Integer n = aux_getn(L, 1, TAB_RW); + if (n > 1) { /* non-trivial interval? */ + luaL_argcheck(L, n < INT_MAX, 1, "array too big"); + if (!lua_isnoneornil(L, 2)) /* is there a 2nd argument? */ + luaL_checktype(L, 2, LUA_TFUNCTION); /* must be a function */ + lua_settop(L, 2); /* make sure there are two arguments */ + auxsort(L, 1, (IdxT)n, 0); + } + return 0; +} + +/* }====================================================== */ + + +static const luaL_Reg tab_funcs[] = { + {"concat", tconcat}, + {"create", tcreate}, + {"insert", tinsert}, + {"pack", tpack}, + {"unpack", tunpack}, + {"remove", tremove}, + {"move", tmove}, + {"sort", sort}, + {NULL, NULL} +}; + + +LUAMOD_API int luaopen_table (lua_State *L) { + luaL_newlib(L, tab_funcs); + return 1; +} diff --git a/crates/lua/vendor/lua/ltests.h b/crates/lua/vendor/lua/ltests.h new file mode 100644 index 0000000..63805e0 --- /dev/null +++ b/crates/lua/vendor/lua/ltests.h @@ -0,0 +1,166 @@ +/* +** $Id: ltests.h $ +** Internal Header for Debugging of the Lua Implementation +** See Copyright Notice in lua.h +*/ + +#ifndef ltests_h +#define ltests_h + + +#include +#include + +/* test Lua with compatibility code */ +#define LUA_COMPAT_MATHLIB +#undef LUA_COMPAT_GLOBAL +#define LUA_COMPAT_GLOBAL 0 + + +#define LUA_DEBUG + + +/* turn on assertions */ +#define LUAI_ASSERT + + +/* to avoid warnings, and to make sure value is really unused */ +#define UNUSED(x) (x=0, (void)(x)) + + +/* test for sizes in 'l_sprintf' (make sure whole buffer is available) */ +#undef l_sprintf +#if !defined(LUA_USE_C89) +#define l_sprintf(s,sz,f,i) (memset(s,0xAB,sz), snprintf(s,sz,f,i)) +#else +#define l_sprintf(s,sz,f,i) (memset(s,0xAB,sz), sprintf(s,f,i)) +#endif + + +/* get a chance to test code without jump tables */ +#define LUA_USE_JUMPTABLE 0 + + +/* use 32-bit integers in random generator */ +#define LUA_RAND32 + + +/* test stack reallocation without strict address use */ +#define LUAI_STRICT_ADDRESS 0 + + +/* memory-allocator control variables */ +typedef struct Memcontrol { + int failnext; + unsigned long numblocks; + unsigned long total; + unsigned long maxmem; + unsigned long memlimit; + unsigned long countlimit; + unsigned long objcount[LUA_NUMTYPES]; +} Memcontrol; + +LUA_API Memcontrol l_memcontrol; + + +#define luai_tracegc(L,f) luai_tracegctest(L, f) +extern void luai_tracegctest (lua_State *L, int first); + + +/* +** generic variable for debug tricks +*/ +extern void *l_Trick; + + +/* +** Function to traverse and check all memory used by Lua +*/ +extern int lua_checkmemory (lua_State *L); + +/* +** Function to print an object GC-friendly +*/ +struct GCObject; +extern void lua_printobj (lua_State *L, struct GCObject *o); + + +/* +** Function to print a value +*/ +struct TValue; +extern void lua_printvalue (struct TValue *v); + +/* +** Function to print the stack +*/ +extern void lua_printstack (lua_State *L); +extern int lua_printallstack (lua_State *L); + + +/* test for lock/unlock */ + +struct L_EXTRA { int lock; int *plock; }; +#undef LUA_EXTRASPACE +#define LUA_EXTRASPACE sizeof(struct L_EXTRA) +#define getlock(l) cast(struct L_EXTRA*, lua_getextraspace(l)) +#define luai_userstateopen(l) \ + (getlock(l)->lock = 0, getlock(l)->plock = &(getlock(l)->lock)) +#define luai_userstateclose(l) \ + lua_assert(getlock(l)->lock == 1 && getlock(l)->plock == &(getlock(l)->lock)) +#define luai_userstatethread(l,l1) \ + lua_assert(getlock(l1)->plock == getlock(l)->plock) +#define luai_userstatefree(l,l1) \ + lua_assert(getlock(l)->plock == getlock(l1)->plock) +#define lua_lock(l) lua_assert((*getlock(l)->plock)++ == 0) +#define lua_unlock(l) lua_assert(--(*getlock(l)->plock) == 0) + + + +LUA_API int luaB_opentests (lua_State *L); + +LUA_API void *debug_realloc (void *ud, void *block, + size_t osize, size_t nsize); + + +#define luaL_newstate() \ + lua_newstate(debug_realloc, &l_memcontrol, luaL_makeseed(NULL)) +#define luai_openlibs(L) \ + { luaL_openlibs(L); \ + luaL_requiref(L, "T", luaB_opentests, 1); \ + lua_pop(L, 1); } + + + + +/* change some sizes to give some bugs a chance */ + +#undef LUAL_BUFFERSIZE +#define LUAL_BUFFERSIZE 23 +#define MINSTRTABSIZE 2 +#define MAXIWTHABS 3 + +#define STRCACHE_N 23 +#define STRCACHE_M 5 + +#define MAXINDEXRK 1 + + +/* +** Reduce maximum stack size to make stack-overflow tests run faster. +** (But value is still large enough to overflow smaller integers.) +*/ +#define LUAI_MAXSTACK 68000 + + +/* test mode uses more stack space */ +#undef LUAI_MAXCCALLS +#define LUAI_MAXCCALLS 180 + + +/* force Lua to use its own implementations */ +#undef lua_strx2number +#undef lua_number2strx + + +#endif diff --git a/crates/lua/vendor/lua/ltm.c b/crates/lua/vendor/lua/ltm.c new file mode 100644 index 0000000..9f535f6 --- /dev/null +++ b/crates/lua/vendor/lua/ltm.c @@ -0,0 +1,363 @@ +/* +** $Id: ltm.c $ +** Tag methods +** See Copyright Notice in lua.h +*/ + +#define ltm_c +#define LUA_CORE + +#include "lprefix.h" + + +#include + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lgc.h" +#include "lobject.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" +#include "lvm.h" + + +static const char udatatypename[] = "userdata"; + +LUAI_DDEF const char *const luaT_typenames_[LUA_TOTALTYPES] = { + "no value", + "nil", "boolean", udatatypename, "number", + "string", "table", "function", udatatypename, "thread", + "upvalue", "proto" /* these last cases are used for tests only */ +}; + + +void luaT_init (lua_State *L) { + static const char *const luaT_eventname[] = { /* ORDER TM */ + "__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" + }; + int i; + for (i=0; itmname[i] = luaS_new(L, luaT_eventname[i]); + luaC_fix(L, obj2gco(G(L)->tmname[i])); /* never collect these names */ + } +} + + +/* +** function to be used with macro "fasttm": optimized for absence of +** tag methods +*/ +const TValue *luaT_gettm (Table *events, TMS event, TString *ename) { + const TValue *tm = luaH_Hgetshortstr(events, ename); + lua_assert(event <= TM_EQ); + if (notm(tm)) { /* no tag method? */ + events->flags |= cast_byte(1u<metatable; + break; + case LUA_TUSERDATA: + mt = uvalue(o)->metatable; + break; + default: + mt = G(L)->mt[ttype(o)]; + } + return (mt ? luaH_Hgetshortstr(mt, G(L)->tmname[event]) : &G(L)->nilvalue); +} + + +/* +** Return the name of the type of an object. For tables and userdata +** with metatable, use their '__name' metafield, if present. +*/ +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_Hgetshortstr(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 */ +} + + +void luaT_callTM (lua_State *L, const TValue *f, const TValue *p1, + const TValue *p2, const TValue *p3) { + StkId func = L->top.p; + setobj2s(L, func, f); /* push function (assume EXTRA_STACK) */ + setobj2s(L, func + 1, p1); /* 1st argument */ + setobj2s(L, func + 2, p2); /* 2nd argument */ + setobj2s(L, func + 3, p3); /* 3rd argument */ + L->top.p = func + 4; + /* metamethod may yield only when called from Lua code */ + if (isLuacode(L->ci)) + luaD_call(L, func, 0); + else + luaD_callnoyield(L, func, 0); +} + + +lu_byte luaT_callTMres (lua_State *L, const TValue *f, const TValue *p1, + const TValue *p2, StkId res) { + ptrdiff_t result = savestack(L, res); + StkId func = L->top.p; + setobj2s(L, func, f); /* push function (assume EXTRA_STACK) */ + setobj2s(L, func + 1, p1); /* 1st argument */ + setobj2s(L, func + 2, p2); /* 2nd argument */ + L->top.p += 3; + /* metamethod may yield only when called from Lua code */ + if (isLuacode(L->ci)) + luaD_call(L, func, 1); + else + luaD_callnoyield(L, func, 1); + res = restorestack(L, result); + setobjs2s(L, res, --L->top.p); /* move result to its place */ + return ttypetag(s2v(res)); /* return tag of the result */ +} + + +static int callbinTM (lua_State *L, const TValue *p1, const TValue *p2, + StkId res, TMS event) { + const TValue *tm = luaT_gettmbyobj(L, p1, event); /* try first operand */ + if (notm(tm)) + tm = luaT_gettmbyobj(L, p2, event); /* try second operand */ + if (notm(tm)) + return -1; /* tag method not found */ + else /* call tag method and return the tag of the result */ + return luaT_callTMres(L, tm, p1, p2, res); +} + + +void luaT_trybinTM (lua_State *L, const TValue *p1, const TValue *p2, + StkId res, TMS event) { + if (l_unlikely(callbinTM(L, p1, p2, res, event) < 0)) { + switch (event) { + case TM_BAND: case TM_BOR: case TM_BXOR: + case TM_SHL: case TM_SHR: case TM_BNOT: { + if (ttisnumber(p1) && ttisnumber(p2)) + luaG_tointerror(L, p1, p2); + else + luaG_opinterror(L, p1, p2, "perform bitwise operation on"); + } + /* calls never return, but to avoid warnings: *//* FALLTHROUGH */ + default: + luaG_opinterror(L, p1, p2, "perform arithmetic on"); + } + } +} + + +/* +** The use of 'p1' after 'callbinTM' is safe because, when a tag +** method is not found, 'callbinTM' cannot change the stack. +*/ +void luaT_tryconcatTM (lua_State *L) { + StkId p1 = L->top.p - 2; /* first argument */ + if (l_unlikely(callbinTM(L, s2v(p1), s2v(p1 + 1), p1, TM_CONCAT) < 0)) + luaG_concaterror(L, s2v(p1), s2v(p1 + 1)); +} + + +void luaT_trybinassocTM (lua_State *L, const TValue *p1, const TValue *p2, + int flip, StkId res, TMS event) { + if (flip) + luaT_trybinTM(L, p2, p1, res, event); + else + luaT_trybinTM(L, p1, p2, res, event); +} + + +void luaT_trybiniTM (lua_State *L, const TValue *p1, lua_Integer i2, + int flip, StkId res, TMS event) { + TValue aux; + setivalue(&aux, i2); + luaT_trybinassocTM(L, p1, &aux, flip, res, event); +} + + +/* +** Calls an order tag method. +*/ +int luaT_callorderTM (lua_State *L, const TValue *p1, const TValue *p2, + TMS event) { + int tag = callbinTM(L, p1, p2, L->top.p, event); /* try original event */ + if (tag >= 0) /* found tag method? */ + return !tagisfalse(tag); + luaG_ordererror(L, p1, p2); /* no metamethod found */ + return 0; /* to avoid warnings */ +} + + +int luaT_callorderiTM (lua_State *L, const TValue *p1, int v2, + int flip, int isfloat, TMS event) { + TValue aux; const TValue *p2; + if (isfloat) { + setfltvalue(&aux, cast_num(v2)); + } + else + setivalue(&aux, v2); + if (flip) { /* arguments were exchanged? */ + p2 = p1; p1 = &aux; /* correct them */ + } + else + p2 = &aux; + return luaT_callorderTM(L, p1, p2, event); +} + + +/* +** Create a vararg table at the top of the stack, with 'n' elements +** starting at 'f'. +*/ +static void createvarargtab (lua_State *L, StkId f, int n) { + int i; + TValue key, value; + Table *t = luaH_new(L); + sethvalue(L, s2v(L->top.p), t); + L->top.p++; + luaH_resize(L, t, cast_uint(n), 1); + setsvalue(L, &key, luaS_new(L, "n")); /* key is "n" */ + setivalue(&value, n); /* value is n */ + /* No need to anchor the key: Due to the resize, the next operation + cannot trigger a garbage collection */ + luaH_set(L, t, &key, &value); /* t.n = n */ + for (i = 0; i < n; i++) + luaH_setint(L, t, i + 1, s2v(f + i)); + luaC_checkGC(L); +} + + +/* +** initial stack: func arg1 ... argn extra1 ... +** ^ ci->func ^ L->top +** final stack: func nil ... nil extra1 ... func arg1 ... argn +** ^ ci->func +*/ +static void buildhiddenargs (lua_State *L, CallInfo *ci, const Proto *p, + int totalargs, int nfixparams, int nextra) { + int i; + ci->u.l.nextraargs = nextra; + luaD_checkstack(L, p->maxstacksize + 1); + /* copy function to the top of the stack, after extra arguments */ + setobjs2s(L, L->top.p++, ci->func.p); + /* move fixed parameters to after the copied function */ + for (i = 1; i <= nfixparams; i++) { + setobjs2s(L, L->top.p++, ci->func.p + i); + setnilvalue2s(ci->func.p + i); /* erase original parameter (for GC) */ + } + ci->func.p += totalargs + 1; /* 'func' now lives after hidden arguments */ + ci->top.p += totalargs + 1; +} + + +void luaT_adjustvarargs (lua_State *L, CallInfo *ci, const Proto *p) { + int totalargs = cast_int(L->top.p - ci->func.p) - 1; + int nfixparams = p->numparams; + int nextra = totalargs - nfixparams; /* number of extra arguments */ + if (p->flag & PF_VATAB) { /* does it need a vararg table? */ + lua_assert(!(p->flag & PF_VAHID)); + createvarargtab(L, ci->func.p + nfixparams + 1, nextra); + /* move table to proper place (last parameter) */ + setobjs2s(L, ci->func.p + nfixparams + 1, L->top.p - 1); + } + else { /* no table */ + lua_assert(p->flag & PF_VAHID); + buildhiddenargs(L, ci, p, totalargs, nfixparams, nextra); + /* set vararg parameter to nil */ + setnilvalue2s(ci->func.p + nfixparams + 1); + lua_assert(L->top.p <= ci->top.p && ci->top.p <= L->stack_last.p); + } +} + + +void luaT_getvararg (CallInfo *ci, StkId ra, TValue *rc) { + int nextra = ci->u.l.nextraargs; + lua_Integer n; + if (tointegerns(rc, &n)) { /* integral value? */ + if (l_castS2U(n) - 1 < cast_uint(nextra)) { + StkId slot = ci->func.p - nextra + cast_int(n) - 1; + setobjs2s(((lua_State*)NULL), ra, slot); + return; + } + } + else if (ttisstring(rc)) { /* string value? */ + size_t len; + const char *s = getlstr(tsvalue(rc), len); + if (len == 1 && s[0] == 'n') { /* key is "n"? */ + setivalue(s2v(ra), nextra); + return; + } + } + setnilvalue2s(ra); /* else produce nil */ +} + + +/* +** Get the number of extra arguments in a vararg function. If vararg +** table has been optimized away, that number is in the call info. +** Otherwise, get the field 'n' from the vararg table and check that it +** has a proper value (non-negative integer not larger than the stack +** limit). +*/ +static int getnumargs (lua_State *L, CallInfo *ci, Table *h) { + if (h == NULL) /* no vararg table? */ + return ci->u.l.nextraargs; + else { + TValue res; + if (luaH_getshortstr(h, luaS_new(L, "n"), &res) != LUA_VNUMINT || + l_castS2U(ivalue(&res)) > cast_uint(INT_MAX/2)) + luaG_runerror(L, "vararg table has no proper 'n'"); + return cast_int(ivalue(&res)); + } +} + + +/* +** Get 'wanted' vararg arguments and put them in 'where'. 'vatab' is +** the register of the vararg table or -1 if there is no vararg table. +*/ +void luaT_getvarargs (lua_State *L, CallInfo *ci, StkId where, int wanted, + int vatab) { + Table *h = (vatab < 0) ? NULL : hvalue(s2v(ci->func.p + vatab + 1)); + int nargs = getnumargs(L, ci, h); /* number of available vararg args. */ + int i, touse; /* 'touse' is minimum between 'wanted' and 'nargs' */ + if (wanted < 0) { + touse = wanted = nargs; /* get all extra arguments available */ + checkstackp(L, nargs, where); /* ensure stack space */ + L->top.p = where + nargs; /* next instruction will need top */ + } + else + touse = (nargs > wanted) ? wanted : nargs; + if (h == NULL) { /* no vararg table? */ + for (i = 0; i < touse; i++) /* get vararg values from the stack */ + setobjs2s(L, where + i, ci->func.p - nargs + i); + } + else { /* get vararg values from vararg table */ + for (i = 0; i < touse; i++) { + lu_byte tag = luaH_getint(h, i + 1, s2v(where + i)); + if (tagisempty(tag)) + setnilvalue2s(where + i); + } + } + for (; i < wanted; i++) /* complete required results with nil */ + setnilvalue2s(where + i); +} diff --git a/crates/lua/vendor/lua/ltm.h b/crates/lua/vendor/lua/ltm.h new file mode 100644 index 0000000..afc7ad0 --- /dev/null +++ b/crates/lua/vendor/lua/ltm.h @@ -0,0 +1,105 @@ +/* +** $Id: ltm.h $ +** Tag methods +** See Copyright Notice in lua.h +*/ + +#ifndef ltm_h +#define ltm_h + + +#include "lobject.h" + + +/* +* WARNING: if you change the order of this enumeration, +* grep "ORDER TM" and "ORDER OP" +*/ +typedef enum { + TM_INDEX, + TM_NEWINDEX, + TM_GC, + TM_MODE, + TM_LEN, + TM_EQ, /* last tag method with fast access */ + TM_ADD, + TM_SUB, + TM_MUL, + TM_MOD, + TM_POW, + TM_DIV, + TM_IDIV, + TM_BAND, + TM_BOR, + TM_BXOR, + TM_SHL, + TM_SHR, + TM_UNM, + TM_BNOT, + TM_LT, + TM_LE, + TM_CONCAT, + TM_CALL, + TM_CLOSE, + TM_N /* number of elements in the enum */ +} TMS; + + +/* +** Mask with 1 in all fast-access methods. A 1 in any of these bits +** in the flag of a (meta)table means the metatable does not have the +** corresponding metamethod field. (Bit 6 of the flag indicates that +** the table is using the dummy node.) +*/ +#define maskflags cast_byte(~(~0u << (TM_EQ + 1))) + + +/* +** Test whether there is no tagmethod. +** (Because tagmethods use raw accesses, the result may be an "empty" nil.) +*/ +#define notm(tm) ttisnil(tm) + +#define checknoTM(mt,e) ((mt) == NULL || (mt)->flags & (1u<<(e))) + +#define gfasttm(g,mt,e) \ + (checknoTM(mt, e) ? NULL : luaT_gettm(mt, e, (g)->tmname[e])) + +#define fasttm(l,mt,e) gfasttm(G(l), mt, e) + +#define ttypename(x) luaT_typenames_[(x) + 1] + +LUAI_DDEC(const char *const luaT_typenames_[LUA_TOTALTYPES];) + + +LUAI_FUNC const char *luaT_objtypename (lua_State *L, const TValue *o); + +LUAI_FUNC const TValue *luaT_gettm (Table *events, TMS event, TString *ename); +LUAI_FUNC const TValue *luaT_gettmbyobj (lua_State *L, const TValue *o, + TMS event); +LUAI_FUNC void luaT_init (lua_State *L); + +LUAI_FUNC void luaT_callTM (lua_State *L, const TValue *f, const TValue *p1, + const TValue *p2, const TValue *p3); +LUAI_FUNC lu_byte luaT_callTMres (lua_State *L, const TValue *f, + const TValue *p1, const TValue *p2, StkId p3); +LUAI_FUNC void luaT_trybinTM (lua_State *L, const TValue *p1, const TValue *p2, + StkId res, TMS event); +LUAI_FUNC void luaT_tryconcatTM (lua_State *L); +LUAI_FUNC void luaT_trybinassocTM (lua_State *L, const TValue *p1, + const TValue *p2, int inv, StkId res, TMS event); +LUAI_FUNC void luaT_trybiniTM (lua_State *L, const TValue *p1, lua_Integer i2, + int inv, StkId res, TMS event); +LUAI_FUNC int luaT_callorderTM (lua_State *L, const TValue *p1, + const TValue *p2, TMS event); +LUAI_FUNC int luaT_callorderiTM (lua_State *L, const TValue *p1, int v2, + int inv, int isfloat, TMS event); + +LUAI_FUNC void luaT_adjustvarargs (lua_State *L, struct CallInfo *ci, + const Proto *p); +LUAI_FUNC void luaT_getvararg (CallInfo *ci, StkId ra, TValue *rc); +LUAI_FUNC void luaT_getvarargs (lua_State *L, struct CallInfo *ci, StkId where, + int wanted, int vatab); + + +#endif diff --git a/crates/lua/vendor/lua/lua.h b/crates/lua/vendor/lua/lua.h new file mode 100644 index 0000000..6deaed4 --- /dev/null +++ b/crates/lua/vendor/lua/lua.h @@ -0,0 +1,547 @@ +/* +** $Id: lua.h $ +** Lua - A Scripting Language +** Lua.org, PUC-Rio, Brazil (www.lua.org) +** See Copyright Notice at the end of this file +*/ + + +#ifndef lua_h +#define lua_h + +#include +#include + + +#define LUA_COPYRIGHT LUA_RELEASE " Copyright (C) 1994-2026 Lua.org, PUC-Rio" +#define LUA_AUTHORS "R. Ierusalimschy, L. H. de Figueiredo, W. Celes" + + +#define LUA_VERSION_MAJOR_N 5 +#define LUA_VERSION_MINOR_N 5 +#define LUA_VERSION_RELEASE_N 1 + +#define LUA_VERSION_NUM (LUA_VERSION_MAJOR_N * 100 + LUA_VERSION_MINOR_N) +#define LUA_VERSION_RELEASE_NUM (LUA_VERSION_NUM * 100 + LUA_VERSION_RELEASE_N) + + +#include "luaconf.h" + + +/* mark for precompiled code ('Lua') */ +#define LUA_SIGNATURE "\x1bLua" + +/* option for multiple returns in 'lua_pcall' and 'lua_call' */ +#define LUA_MULTRET (-1) + + +/* +** Pseudo-indices +** (The stack size is limited to INT_MAX/2; we keep some free empty +** space after that to help overflow detection.) +*/ +#define LUA_REGISTRYINDEX (-(INT_MAX/2 + 1000)) +#define lua_upvalueindex(i) (LUA_REGISTRYINDEX - (i)) + + +/* thread status */ +#define LUA_OK 0 +#define LUA_YIELD 1 +#define LUA_ERRRUN 2 +#define LUA_ERRSYNTAX 3 +#define LUA_ERRMEM 4 +#define LUA_ERRERR 5 + + +typedef struct lua_State lua_State; + + +/* +** basic types +*/ +#define LUA_TNONE (-1) + +#define LUA_TNIL 0 +#define LUA_TBOOLEAN 1 +#define LUA_TLIGHTUSERDATA 2 +#define LUA_TNUMBER 3 +#define LUA_TSTRING 4 +#define LUA_TTABLE 5 +#define LUA_TFUNCTION 6 +#define LUA_TUSERDATA 7 +#define LUA_TTHREAD 8 + +#define LUA_NUMTYPES 9 + + + +/* minimum Lua stack available to a C function */ +#define LUA_MINSTACK 20 + + +/* predefined values in the registry */ +/* index 1 is reserved for the reference mechanism */ +#define LUA_RIDX_GLOBALS 2 +#define LUA_RIDX_MAINTHREAD 3 +#define LUA_RIDX_LAST 3 + + +/* type of numbers in Lua */ +typedef LUA_NUMBER lua_Number; + + +/* type for integer functions */ +typedef LUA_INTEGER lua_Integer; + +/* unsigned integer type */ +typedef LUA_UNSIGNED lua_Unsigned; + +/* type for continuation-function contexts */ +typedef LUA_KCONTEXT lua_KContext; + + +/* +** Type for C functions registered with Lua +*/ +typedef int (*lua_CFunction) (lua_State *L); + +/* +** Type for continuation functions +*/ +typedef int (*lua_KFunction) (lua_State *L, int status, lua_KContext ctx); + + +/* +** Type for functions that read/write blocks when loading/dumping Lua chunks +*/ +typedef const char * (*lua_Reader) (lua_State *L, void *ud, size_t *sz); + +typedef int (*lua_Writer) (lua_State *L, const void *p, size_t sz, void *ud); + + +/* +** Type for memory-allocation functions +*/ +typedef void * (*lua_Alloc) (void *ud, void *ptr, size_t osize, size_t nsize); + + +/* +** Type for warning functions +*/ +typedef void (*lua_WarnFunction) (void *ud, const char *msg, int tocont); + + +/* +** Type used by the debug API to collect debug information +*/ +typedef struct lua_Debug lua_Debug; + + +/* +** Functions to be called by the debugger in specific events +*/ +typedef void (*lua_Hook) (lua_State *L, lua_Debug *ar); + + +/* +** generic extra include file +*/ +#if defined(LUA_USER_H) +#include LUA_USER_H +#endif + + +/* +** RCS ident string +*/ +extern const char lua_ident[]; + + +/* +** state manipulation +*/ +LUA_API lua_State *(lua_newstate) (lua_Alloc f, void *ud, unsigned seed); +LUA_API void (lua_close) (lua_State *L); +LUA_API lua_State *(lua_newthread) (lua_State *L); +LUA_API int (lua_closethread) (lua_State *L, lua_State *from); + +LUA_API lua_CFunction (lua_atpanic) (lua_State *L, lua_CFunction panicf); + + +LUA_API lua_Number (lua_version) (lua_State *L); + + +/* +** basic stack manipulation +*/ +LUA_API int (lua_absindex) (lua_State *L, int idx); +LUA_API int (lua_gettop) (lua_State *L); +LUA_API void (lua_settop) (lua_State *L, int idx); +LUA_API void (lua_pushvalue) (lua_State *L, int idx); +LUA_API void (lua_rotate) (lua_State *L, int idx, int n); +LUA_API void (lua_copy) (lua_State *L, int fromidx, int toidx); +LUA_API int (lua_checkstack) (lua_State *L, int n); + +LUA_API void (lua_xmove) (lua_State *from, lua_State *to, int n); + + +/* +** access functions (stack -> C) +*/ + +LUA_API int (lua_isnumber) (lua_State *L, int idx); +LUA_API int (lua_isstring) (lua_State *L, int idx); +LUA_API int (lua_iscfunction) (lua_State *L, int idx); +LUA_API int (lua_isinteger) (lua_State *L, int idx); +LUA_API int (lua_isuserdata) (lua_State *L, int idx); +LUA_API int (lua_type) (lua_State *L, int idx); +LUA_API const char *(lua_typename) (lua_State *L, int tp); + +LUA_API lua_Number (lua_tonumberx) (lua_State *L, int idx, int *isnum); +LUA_API lua_Integer (lua_tointegerx) (lua_State *L, int idx, int *isnum); +LUA_API int (lua_toboolean) (lua_State *L, int idx); +LUA_API const char *(lua_tolstring) (lua_State *L, int idx, size_t *len); +LUA_API lua_Unsigned (lua_rawlen) (lua_State *L, int idx); +LUA_API lua_CFunction (lua_tocfunction) (lua_State *L, int idx); +LUA_API void *(lua_touserdata) (lua_State *L, int idx); +LUA_API lua_State *(lua_tothread) (lua_State *L, int idx); +LUA_API const void *(lua_topointer) (lua_State *L, int idx); + + +/* +** Comparison and arithmetic functions +*/ + +#define LUA_OPADD 0 /* ORDER TM, ORDER OP */ +#define LUA_OPSUB 1 +#define LUA_OPMUL 2 +#define LUA_OPMOD 3 +#define LUA_OPPOW 4 +#define LUA_OPDIV 5 +#define LUA_OPIDIV 6 +#define LUA_OPBAND 7 +#define LUA_OPBOR 8 +#define LUA_OPBXOR 9 +#define LUA_OPSHL 10 +#define LUA_OPSHR 11 +#define LUA_OPUNM 12 +#define LUA_OPBNOT 13 + +LUA_API void (lua_arith) (lua_State *L, int op); + +#define LUA_OPEQ 0 +#define LUA_OPLT 1 +#define LUA_OPLE 2 + +LUA_API int (lua_rawequal) (lua_State *L, int idx1, int idx2); +LUA_API int (lua_compare) (lua_State *L, int idx1, int idx2, int op); + + +/* +** push functions (C -> stack) +*/ +LUA_API void (lua_pushnil) (lua_State *L); +LUA_API void (lua_pushnumber) (lua_State *L, lua_Number n); +LUA_API void (lua_pushinteger) (lua_State *L, lua_Integer n); +LUA_API const char *(lua_pushlstring) (lua_State *L, const char *s, size_t len); +LUA_API const char *(lua_pushexternalstring) (lua_State *L, + const char *s, size_t len, lua_Alloc falloc, void *ud); +LUA_API const char *(lua_pushstring) (lua_State *L, const char *s); +LUA_API const char *(lua_pushvfstring) (lua_State *L, const char *fmt, + va_list argp); +LUA_API const char *(lua_pushfstring) (lua_State *L, const char *fmt, ...); +LUA_API void (lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n); +LUA_API void (lua_pushboolean) (lua_State *L, int b); +LUA_API void (lua_pushlightuserdata) (lua_State *L, void *p); +LUA_API int (lua_pushthread) (lua_State *L); + + +/* +** get functions (Lua -> stack) +*/ +LUA_API int (lua_getglobal) (lua_State *L, const char *name); +LUA_API int (lua_gettable) (lua_State *L, int idx); +LUA_API int (lua_getfield) (lua_State *L, int idx, const char *k); +LUA_API int (lua_geti) (lua_State *L, int idx, lua_Integer n); +LUA_API int (lua_rawget) (lua_State *L, int idx); +LUA_API int (lua_rawgeti) (lua_State *L, int idx, lua_Integer n); +LUA_API int (lua_rawgetp) (lua_State *L, int idx, const void *p); + +LUA_API void (lua_createtable) (lua_State *L, int narr, int nrec); +LUA_API void *(lua_newuserdatauv) (lua_State *L, size_t sz, int nuvalue); +LUA_API int (lua_getmetatable) (lua_State *L, int objindex); +LUA_API int (lua_getiuservalue) (lua_State *L, int idx, int n); + + +/* +** set functions (stack -> Lua) +*/ +LUA_API void (lua_setglobal) (lua_State *L, const char *name); +LUA_API void (lua_settable) (lua_State *L, int idx); +LUA_API void (lua_setfield) (lua_State *L, int idx, const char *k); +LUA_API void (lua_seti) (lua_State *L, int idx, lua_Integer n); +LUA_API void (lua_rawset) (lua_State *L, int idx); +LUA_API void (lua_rawseti) (lua_State *L, int idx, lua_Integer n); +LUA_API void (lua_rawsetp) (lua_State *L, int idx, const void *p); +LUA_API int (lua_setmetatable) (lua_State *L, int objindex); +LUA_API int (lua_setiuservalue) (lua_State *L, int idx, int n); + + +/* +** 'load' and 'call' functions (load and run Lua code) +*/ +LUA_API void (lua_callk) (lua_State *L, int nargs, int nresults, + lua_KContext ctx, lua_KFunction k); +#define lua_call(L,n,r) lua_callk(L, (n), (r), 0, NULL) + +LUA_API int (lua_pcallk) (lua_State *L, int nargs, int nresults, int errfunc, + lua_KContext ctx, lua_KFunction k); +#define lua_pcall(L,n,r,f) lua_pcallk(L, (n), (r), (f), 0, NULL) + +LUA_API int (lua_load) (lua_State *L, lua_Reader reader, void *dt, + const char *chunkname, const char *mode); + +LUA_API int (lua_dump) (lua_State *L, lua_Writer writer, void *data, int strip); + + +/* +** coroutine functions +*/ +LUA_API int (lua_yieldk) (lua_State *L, int nresults, lua_KContext ctx, + lua_KFunction k); +LUA_API int (lua_resume) (lua_State *L, lua_State *from, int narg, + int *nres); +LUA_API int (lua_status) (lua_State *L); +LUA_API int (lua_isyieldable) (lua_State *L); + +#define lua_yield(L,n) lua_yieldk(L, (n), 0, NULL) + + +/* +** Warning-related functions +*/ +LUA_API void (lua_setwarnf) (lua_State *L, lua_WarnFunction f, void *ud); +LUA_API void (lua_warning) (lua_State *L, const char *msg, int tocont); + + +/* +** garbage-collection options +*/ + +#define LUA_GCSTOP 0 +#define LUA_GCRESTART 1 +#define LUA_GCCOLLECT 2 +#define LUA_GCCOUNT 3 +#define LUA_GCCOUNTB 4 +#define LUA_GCSTEP 5 +#define LUA_GCISRUNNING 6 +#define LUA_GCGEN 7 +#define LUA_GCINC 8 +#define LUA_GCPARAM 9 + + +/* +** garbage-collection parameters +*/ +/* parameters for generational mode */ +#define LUA_GCPMINORMUL 0 /* control minor collections */ +#define LUA_GCPMAJORMINOR 1 /* control shift major->minor */ +#define LUA_GCPMINORMAJOR 2 /* control shift minor->major */ + +/* parameters for incremental mode */ +#define LUA_GCPPAUSE 3 /* size of pause between successive GCs */ +#define LUA_GCPSTEPMUL 4 /* GC "speed" */ +#define LUA_GCPSTEPSIZE 5 /* GC granularity */ + +/* number of parameters */ +#define LUA_GCPN 6 + + +LUA_API int (lua_gc) (lua_State *L, int what, ...); + + +/* +** miscellaneous functions +*/ + +LUA_API int (lua_error) (lua_State *L); + +LUA_API int (lua_next) (lua_State *L, int idx); + +LUA_API void (lua_concat) (lua_State *L, int n); +LUA_API void (lua_len) (lua_State *L, int idx); + +#define LUA_N2SBUFFSZ 64 +LUA_API unsigned (lua_numbertocstring) (lua_State *L, int idx, char *buff); +LUA_API size_t (lua_stringtonumber) (lua_State *L, const char *s); + +LUA_API lua_Alloc (lua_getallocf) (lua_State *L, void **ud); +LUA_API void (lua_setallocf) (lua_State *L, lua_Alloc f, void *ud); + +LUA_API void (lua_toclose) (lua_State *L, int idx); +LUA_API void (lua_closeslot) (lua_State *L, int idx); + + +/* +** {============================================================== +** some useful macros +** =============================================================== +*/ + +#define lua_getextraspace(L) ((void *)((char *)(L) - LUA_EXTRASPACE)) + +#define lua_tonumber(L,i) lua_tonumberx(L,(i),NULL) +#define lua_tointeger(L,i) lua_tointegerx(L,(i),NULL) + +#define lua_pop(L,n) lua_settop(L, -(n)-1) + +#define lua_newtable(L) lua_createtable(L, 0, 0) + +#define lua_register(L,n,f) (lua_pushcfunction(L, (f)), lua_setglobal(L, (n))) + +#define lua_pushcfunction(L,f) lua_pushcclosure(L, (f), 0) + +#define lua_isfunction(L,n) (lua_type(L, (n)) == LUA_TFUNCTION) +#define lua_istable(L,n) (lua_type(L, (n)) == LUA_TTABLE) +#define lua_islightuserdata(L,n) (lua_type(L, (n)) == LUA_TLIGHTUSERDATA) +#define lua_isnil(L,n) (lua_type(L, (n)) == LUA_TNIL) +#define lua_isboolean(L,n) (lua_type(L, (n)) == LUA_TBOOLEAN) +#define lua_isthread(L,n) (lua_type(L, (n)) == LUA_TTHREAD) +#define lua_isnone(L,n) (lua_type(L, (n)) == LUA_TNONE) +#define lua_isnoneornil(L, n) (lua_type(L, (n)) <= 0) + +#define lua_pushliteral(L, s) lua_pushstring(L, "" s) + +#define lua_pushglobaltable(L) \ + ((void)lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS)) + +#define lua_tostring(L,i) lua_tolstring(L, (i), NULL) + + +#define lua_insert(L,idx) lua_rotate(L, (idx), 1) + +#define lua_remove(L,idx) (lua_rotate(L, (idx), -1), lua_pop(L, 1)) + +#define lua_replace(L,idx) (lua_copy(L, -1, (idx)), lua_pop(L, 1)) + +/* }============================================================== */ + + +/* +** {============================================================== +** compatibility macros +** =============================================================== +*/ + +#define lua_newuserdata(L,s) lua_newuserdatauv(L,s,1) +#define lua_getuservalue(L,idx) lua_getiuservalue(L,idx,1) +#define lua_setuservalue(L,idx) lua_setiuservalue(L,idx,1) + +#define lua_resetthread(L) lua_closethread(L,NULL) + +/* }============================================================== */ + +/* +** {====================================================================== +** Debug API +** ======================================================================= +*/ + + +/* +** Event codes +*/ +#define LUA_HOOKCALL 0 +#define LUA_HOOKRET 1 +#define LUA_HOOKLINE 2 +#define LUA_HOOKCOUNT 3 +#define LUA_HOOKTAILCALL 4 + + +/* +** Event masks +*/ +#define LUA_MASKCALL (1 << LUA_HOOKCALL) +#define LUA_MASKRET (1 << LUA_HOOKRET) +#define LUA_MASKLINE (1 << LUA_HOOKLINE) +#define LUA_MASKCOUNT (1 << LUA_HOOKCOUNT) + + +LUA_API int (lua_getstack) (lua_State *L, int level, lua_Debug *ar); +LUA_API int (lua_getinfo) (lua_State *L, const char *what, lua_Debug *ar); +LUA_API const char *(lua_getlocal) (lua_State *L, const lua_Debug *ar, int n); +LUA_API const char *(lua_setlocal) (lua_State *L, const lua_Debug *ar, int n); +LUA_API const char *(lua_getupvalue) (lua_State *L, int funcindex, int n); +LUA_API const char *(lua_setupvalue) (lua_State *L, int funcindex, int n); + +LUA_API void *(lua_upvalueid) (lua_State *L, int fidx, int n); +LUA_API void (lua_upvaluejoin) (lua_State *L, int fidx1, int n1, + int fidx2, int n2); + +LUA_API void (lua_sethook) (lua_State *L, lua_Hook func, int mask, int count); +LUA_API lua_Hook (lua_gethook) (lua_State *L); +LUA_API int (lua_gethookmask) (lua_State *L); +LUA_API int (lua_gethookcount) (lua_State *L); + + +struct lua_Debug { + int event; + const char *name; /* (n) */ + const char *namewhat; /* (n) 'global', 'local', 'field', 'method' */ + const char *what; /* (S) 'Lua', 'C', 'main', 'tail' */ + const char *source; /* (S) */ + size_t srclen; /* (S) */ + int currentline; /* (l) */ + int linedefined; /* (S) */ + int lastlinedefined; /* (S) */ + unsigned char nups; /* (u) number of upvalues */ + unsigned char nparams;/* (u) number of parameters */ + char isvararg; /* (u) */ + unsigned char extraargs; /* (t) number of extra arguments */ + char istailcall; /* (t) */ + int ftransfer; /* (r) index of first value transferred */ + int ntransfer; /* (r) number of transferred values */ + char short_src[LUA_IDSIZE]; /* (S) */ + /* private part */ + struct CallInfo *i_ci; /* active function */ +}; + +/* }====================================================================== */ + + +#define LUAI_TOSTRAUX(x) #x +#define LUAI_TOSTR(x) LUAI_TOSTRAUX(x) + +#define LUA_VERSION_MAJOR LUAI_TOSTR(LUA_VERSION_MAJOR_N) +#define LUA_VERSION_MINOR LUAI_TOSTR(LUA_VERSION_MINOR_N) +#define LUA_VERSION_RELEASE LUAI_TOSTR(LUA_VERSION_RELEASE_N) + +#define LUA_VERSION "Lua " LUA_VERSION_MAJOR "." LUA_VERSION_MINOR +#define LUA_RELEASE LUA_VERSION "." LUA_VERSION_RELEASE + + +/****************************************************************************** +* Copyright (C) 1994-2026 Lua.org, PUC-Rio. +* +* Permission is hereby granted, free of charge, to any person obtaining +* a copy of this software and associated documentation files (the +* "Software"), to deal in the Software without restriction, including +* without limitation the rights to use, copy, modify, merge, publish, +* distribute, sublicense, and/or sell copies of the Software, and to +* permit persons to whom the Software is furnished to do so, subject to +* the following conditions: +* +* The above copyright notice and this permission notice shall be +* included in all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +******************************************************************************/ + + +#endif diff --git a/crates/lua/vendor/lua/luaconf.h b/crates/lua/vendor/lua/luaconf.h new file mode 100644 index 0000000..a70dd3c --- /dev/null +++ b/crates/lua/vendor/lua/luaconf.h @@ -0,0 +1,758 @@ +/* +** $Id: luaconf.h $ +** Configuration file for Lua +** See Copyright Notice in lua.h +*/ + + +#ifndef luaconf_h +#define luaconf_h + +#include +#include + + +/* +** =================================================================== +** General Configuration File for Lua +** +** Some definitions here can be changed externally, through the compiler +** (e.g., with '-D' options): They are commented out or protected +** by '#if !defined' guards. However, several other definitions +** should be changed directly here, either because they affect the +** Lua ABI (by making the changes here, you ensure that all software +** connected to Lua, such as C libraries, will be compiled with the same +** configuration); or because they are seldom changed. +** +** Search for "@@" to find all configurable definitions. +** =================================================================== +*/ + + +/* +** {==================================================================== +** System Configuration: macros to adapt (if needed) Lua to some +** particular platform, for instance restricting it to C89. +** ===================================================================== +*/ + +/* +@@ LUA_USE_C89 controls the use of non-ISO-C89 features. +** Define it if you want Lua to avoid the use of a few C99 features +** or Windows-specific features on Windows. +*/ +/* #define LUA_USE_C89 */ + + +/* +** By default, Lua on Windows use (some) specific Windows features +*/ +#if !defined(LUA_USE_C89) && defined(_WIN32) && !defined(_WIN32_WCE) +#define LUA_USE_WINDOWS /* enable goodies for regular Windows */ +#endif + + +#if defined(LUA_USE_WINDOWS) +#define LUA_DL_DLL /* enable support for DLL */ +#define LUA_USE_C89 /* broadly, Windows is C89 */ +#endif + + +/* +** When POSIX DLL ('LUA_USE_DLOPEN') is enabled, the Lua stand-alone +** application will try to dynamically link a 'readline' facility +** for its REPL. In that case, LUA_READLINELIB is the name of the +** library it will look for those facilities. If lua.c cannot open +** the specified library, it will generate a warning and then run +** without 'readline'. If that macro is not defined, lua.c will not +** use 'readline'. +*/ +#if defined(LUA_USE_LINUX) +#define LUA_USE_POSIX +#define LUA_USE_DLOPEN /* needs an extra library: -ldl */ +#if !defined(LUA_READLINELIB) +#define LUA_READLINELIB "libreadline.so" +#endif +#endif + + +#if defined(LUA_USE_MACOSX) +#define LUA_USE_POSIX +#define LUA_USE_DLOPEN /* macOS does not need -ldl */ +#define LUA_USE_READLINE /* needs an extra library: -lreadline */ +#endif + + +#if defined(LUA_USE_IOS) +#define LUA_USE_POSIX +#define LUA_USE_DLOPEN +#endif + + +#if defined(LUA_USE_C89) && defined(LUA_USE_POSIX) +#error "POSIX is not compatible with C89" +#endif + + +/* +@@ LUAI_IS32INT is true iff 'int' has (at least) 32 bits. +*/ +#define LUAI_IS32INT ((UINT_MAX >> 30) >= 3) + +/* }================================================================== */ + + + +/* +** {================================================================== +** Configuration for Number types. These options should not be +** set externally, because any other code connected to Lua must +** use the same configuration. +** =================================================================== +*/ + +/* +@@ LUA_INT_TYPE defines the type for Lua integers. +@@ LUA_FLOAT_TYPE defines the type for Lua floats. +** Lua should work fine with any mix of these options supported +** by your C compiler. The usual configurations are 64-bit integers +** and 'double' (the default), 32-bit integers and 'float' (for +** restricted platforms), and 'long'/'double' (for C compilers not +** compliant with C99, which may not have support for 'long long'). +*/ + +/* predefined options for LUA_INT_TYPE */ +#define LUA_INT_INT 1 +#define LUA_INT_LONG 2 +#define LUA_INT_LONGLONG 3 + +/* predefined options for LUA_FLOAT_TYPE */ +#define LUA_FLOAT_FLOAT 1 +#define LUA_FLOAT_DOUBLE 2 +#define LUA_FLOAT_LONGDOUBLE 3 + + +/* Default configuration ('long long' and 'double', for 64-bit Lua) */ +#define LUA_INT_DEFAULT LUA_INT_LONGLONG +#define LUA_FLOAT_DEFAULT LUA_FLOAT_DOUBLE + + +/* +@@ LUA_32BITS enables Lua with 32-bit integers and 32-bit floats. +*/ +/* #define LUA_32BITS */ + + +/* +@@ LUA_C89_NUMBERS ensures that Lua uses the largest types available for +** C89 ('long' and 'double'); Windows always has '__int64', so it does +** not need to use this case. +*/ +#if defined(LUA_USE_C89) && !defined(LUA_USE_WINDOWS) +#define LUA_C89_NUMBERS 1 +#else +#define LUA_C89_NUMBERS 0 +#endif + + +#if defined(LUA_32BITS) /* { */ +/* +** 32-bit integers and 'float' +*/ +#if LUAI_IS32INT /* use 'int' if big enough */ +#define LUA_INT_TYPE LUA_INT_INT +#else /* otherwise use 'long' */ +#define LUA_INT_TYPE LUA_INT_LONG +#endif +#define LUA_FLOAT_TYPE LUA_FLOAT_FLOAT + +#elif LUA_C89_NUMBERS /* }{ */ +/* +** largest types available for C89 ('long' and 'double') +*/ +#define LUA_INT_TYPE LUA_INT_LONG +#define LUA_FLOAT_TYPE LUA_FLOAT_DOUBLE + +#else /* }{ */ +/* use defaults */ + +#define LUA_INT_TYPE LUA_INT_DEFAULT +#define LUA_FLOAT_TYPE LUA_FLOAT_DEFAULT + +#endif /* } */ + + +/* }================================================================== */ + + + +/* +** {================================================================== +** Configuration for Paths. +** =================================================================== +*/ + +/* +** LUA_PATH_SEP is the character that separates templates in a path. +** LUA_PATH_MARK is the string that marks the substitution points in a +** template. +** LUA_EXEC_DIR in a Windows path is replaced by the executable's +** directory. +*/ +#define LUA_PATH_SEP ";" +#define LUA_PATH_MARK "?" +#define LUA_EXEC_DIR "!" + + +/* +@@ LUA_PATH_DEFAULT is the default path that Lua uses to look for +** Lua libraries. +@@ LUA_CPATH_DEFAULT is the default path that Lua uses to look for +** C libraries. +** CHANGE them if your machine has a non-conventional directory +** hierarchy or if you want to install your libraries in +** non-conventional directories. +*/ + +#define LUA_VDIR LUA_VERSION_MAJOR "." LUA_VERSION_MINOR +#if defined(_WIN32) /* { */ +/* +** In Windows, any exclamation mark ('!') in the path is replaced by the +** path of the directory of the executable file of the current process. +*/ +#define LUA_LDIR "!\\lua\\" +#define LUA_CDIR "!\\" +#define LUA_SHRDIR "!\\..\\share\\lua\\" LUA_VDIR "\\" + +#if !defined(LUA_PATH_DEFAULT) +#define LUA_PATH_DEFAULT \ + LUA_LDIR "?.lua;" LUA_LDIR "?\\init.lua;" \ + LUA_CDIR "?.lua;" LUA_CDIR "?\\init.lua;" \ + LUA_SHRDIR "?.lua;" LUA_SHRDIR "?\\init.lua;" \ + ".\\?.lua;" ".\\?\\init.lua" +#endif + +#if !defined(LUA_CPATH_DEFAULT) +#define LUA_CPATH_DEFAULT \ + LUA_CDIR "?.dll;" \ + LUA_CDIR "..\\lib\\lua\\" LUA_VDIR "\\?.dll;" \ + LUA_CDIR "loadall.dll;" ".\\?.dll" +#endif + +#else /* }{ */ + +#define LUA_ROOT "/usr/local/" +#define LUA_LDIR LUA_ROOT "share/lua/" LUA_VDIR "/" +#define LUA_CDIR LUA_ROOT "lib/lua/" LUA_VDIR "/" + +#if !defined(LUA_PATH_DEFAULT) +#define LUA_PATH_DEFAULT \ + LUA_LDIR "?.lua;" LUA_LDIR "?/init.lua;" \ + LUA_CDIR "?.lua;" LUA_CDIR "?/init.lua;" \ + "./?.lua;" "./?/init.lua" +#endif + +#if !defined(LUA_CPATH_DEFAULT) +#define LUA_CPATH_DEFAULT \ + LUA_CDIR "?.so;" LUA_CDIR "loadall.so;" "./?.so" +#endif + +#endif /* } */ + + +/* +@@ LUA_DIRSEP is the directory separator (for submodules). +** CHANGE it if your machine does not use "/" as the directory separator +** and is not Windows. (On Windows Lua automatically uses "\".) +*/ +#if !defined(LUA_DIRSEP) + +#if defined(_WIN32) +#define LUA_DIRSEP "\\" +#else +#define LUA_DIRSEP "/" +#endif + +#endif + + +/* +** LUA_IGMARK is a mark to ignore all after it when building the +** module name (e.g., used to build the luaopen_ function name). +** Typically, the suffix after the mark is the module version, +** as in "mod-v1.2.so". +*/ +#define LUA_IGMARK "-" + +/* }================================================================== */ + + +/* +** {================================================================== +** Marks for exported symbols in the C code +** =================================================================== +*/ + +/* +@@ LUA_API is a mark for all core API functions. +@@ LUALIB_API is a mark for all auxiliary library functions. +@@ LUAMOD_API is a mark for all standard library opening functions. +** CHANGE them if you need to define those functions in some special way. +** For instance, if you want to create one Windows DLL with the core and +** the libraries, you may want to use the following definition (define +** LUA_BUILD_AS_DLL to get it). +*/ +#if defined(LUA_BUILD_AS_DLL) /* { */ + +#if defined(LUA_CORE) || defined(LUA_LIB) /* { */ +#define LUA_API __declspec(dllexport) +#else /* }{ */ +#define LUA_API __declspec(dllimport) +#endif /* } */ + +#else /* }{ */ + +/* kore: the amalgamation predefines LUA_API to export the C API. */ +#if !defined(LUA_API) +#define LUA_API extern +#endif + +#endif /* } */ + + +/* +** More often than not the libs go together with the core. +*/ +#define LUALIB_API LUA_API + +#if defined(__cplusplus) +/* Lua uses the "C name" when calling open functions */ +#define LUAMOD_API extern "C" +#else +#define LUAMOD_API LUA_API +#endif + +/* }================================================================== */ + + +/* +** {================================================================== +** Compatibility with previous versions +** =================================================================== +*/ + +/* +@@ LUA_COMPAT_GLOBAL avoids 'global' being a reserved word +*/ +#if !defined(LUA_COMPAT_GLOBAL) +#define LUA_COMPAT_GLOBAL 1 +#endif + + +/* +@@ LUA_COMPAT_MATHLIB controls the presence of several deprecated +** functions in the mathematical library. +** (These functions were already officially removed in 5.3; +** nevertheless they are still available here.) +*/ +/* #define LUA_COMPAT_MATHLIB */ + + +/* +@@ The following macros supply trivial compatibility for some +** changes in the API. The macros themselves document how to +** change your code to avoid using them. +** (Once more, these macros were officially removed in 5.3, but they are +** still available here.) +*/ +#define lua_strlen(L,i) lua_rawlen(L, (i)) + +#define lua_objlen(L,i) lua_rawlen(L, (i)) + +#define lua_equal(L,idx1,idx2) lua_compare(L,(idx1),(idx2),LUA_OPEQ) +#define lua_lessthan(L,idx1,idx2) lua_compare(L,(idx1),(idx2),LUA_OPLT) + +/* }================================================================== */ + + + +/* +** {================================================================== +** Configuration for Numbers (low-level part). +** Change these definitions if no predefined LUA_FLOAT_* / LUA_INT_* +** satisfy your needs. +** =================================================================== +*/ + +/* +@@ LUAI_UACNUMBER is the result of a 'default argument promotion' +@@ over a floating number. +@@ l_floatatt(x) corrects float attribute 'x' to the proper float type +** by prefixing it with one of FLT/DBL/LDBL. +@@ LUA_NUMBER_FRMLEN is the length modifier for writing floats. +@@ LUA_NUMBER_FMT is the format for writing floats with the maximum +** number of digits that respects tostring(tonumber(numeral)) == numeral. +** (That would be floor(log10(2^n)), where n is the number of bits in +** the float mantissa.) +@@ LUA_NUMBER_FMT_N is the format for writing floats with the minimum +** number of digits that ensures tonumber(tostring(number)) == number. +** (That would be LUA_NUMBER_FMT+2.) +@@ l_mathop allows the addition of an 'l' or 'f' to all math operations. +@@ l_floor takes the floor of a float. +@@ lua_str2number converts a decimal numeral to a number. +*/ + + +/* The following definition is good for most cases here */ + +#define l_floor(x) (l_mathop(floor)(x)) + + +/* now the variable definitions */ + +#if LUA_FLOAT_TYPE == LUA_FLOAT_FLOAT /* { single float */ + +#define LUA_NUMBER float + +#define l_floatatt(n) (FLT_##n) + +#define LUAI_UACNUMBER double + +#define LUA_NUMBER_FRMLEN "" +#define LUA_NUMBER_FMT "%.7g" +#define LUA_NUMBER_FMT_N "%.9g" + +#define l_mathop(op) op##f + +#define lua_str2number(s,p) strtof((s), (p)) + + +#elif LUA_FLOAT_TYPE == LUA_FLOAT_LONGDOUBLE /* }{ long double */ + +#define LUA_NUMBER long double + +#define l_floatatt(n) (LDBL_##n) + +#define LUAI_UACNUMBER long double + +#define LUA_NUMBER_FRMLEN "L" +#define LUA_NUMBER_FMT "%.19Lg" +#define LUA_NUMBER_FMT_N "%.21Lg" + +#define l_mathop(op) op##l + +#define lua_str2number(s,p) strtold((s), (p)) + +#elif LUA_FLOAT_TYPE == LUA_FLOAT_DOUBLE /* }{ double */ + +#define LUA_NUMBER double + +#define l_floatatt(n) (DBL_##n) + +#define LUAI_UACNUMBER double + +#define LUA_NUMBER_FRMLEN "" +#define LUA_NUMBER_FMT "%.15g" +#define LUA_NUMBER_FMT_N "%.17g" + +#define l_mathop(op) op + +#define lua_str2number(s,p) strtod((s), (p)) + +#else /* }{ */ + +#error "numeric float type not defined" + +#endif /* } */ + + + +/* +@@ LUA_UNSIGNED is the unsigned version of LUA_INTEGER. +@@ LUAI_UACINT is the result of a 'default argument promotion' +@@ over a LUA_INTEGER. +@@ LUA_INTEGER_FRMLEN is the length modifier for reading/writing integers. +@@ LUA_INTEGER_FMT is the format for writing integers. +@@ LUA_MAXINTEGER is the maximum value for a LUA_INTEGER. +@@ LUA_MININTEGER is the minimum value for a LUA_INTEGER. +@@ LUA_MAXUNSIGNED is the maximum value for a LUA_UNSIGNED. +@@ lua_integer2str converts an integer to a string. +*/ + + +/* The following definitions are good for most cases here */ + +#define LUA_INTEGER_FMT "%" LUA_INTEGER_FRMLEN "d" + +#define LUAI_UACINT LUA_INTEGER + +#define lua_integer2str(s,sz,n) \ + l_sprintf((s), sz, LUA_INTEGER_FMT, (LUAI_UACINT)(n)) + +/* +** use LUAI_UACINT here to avoid problems with promotions (which +** can turn a comparison between unsigneds into a signed comparison) +*/ +#define LUA_UNSIGNED unsigned LUAI_UACINT + + +/* now the variable definitions */ + +#if LUA_INT_TYPE == LUA_INT_INT /* { int */ + +#define LUA_INTEGER int +#define LUA_INTEGER_FRMLEN "" + +#define LUA_MAXINTEGER INT_MAX +#define LUA_MININTEGER INT_MIN + +#define LUA_MAXUNSIGNED UINT_MAX + +#elif LUA_INT_TYPE == LUA_INT_LONG /* }{ long */ + +#define LUA_INTEGER long +#define LUA_INTEGER_FRMLEN "l" + +#define LUA_MAXINTEGER LONG_MAX +#define LUA_MININTEGER LONG_MIN + +#define LUA_MAXUNSIGNED ULONG_MAX + +#elif LUA_INT_TYPE == LUA_INT_LONGLONG /* }{ long long */ + +/* use presence of macro LLONG_MAX as proxy for C99 compliance */ +#if defined(LLONG_MAX) /* { */ +/* use ISO C99 stuff */ + +#define LUA_INTEGER long long +#define LUA_INTEGER_FRMLEN "ll" + +#define LUA_MAXINTEGER LLONG_MAX +#define LUA_MININTEGER LLONG_MIN + +#define LUA_MAXUNSIGNED ULLONG_MAX + +#elif defined(LUA_USE_WINDOWS) /* }{ */ +/* in Windows, can use specific Windows types */ + +#define LUA_INTEGER __int64 +#define LUA_INTEGER_FRMLEN "I64" + +#define LUA_MAXINTEGER _I64_MAX +#define LUA_MININTEGER _I64_MIN + +#define LUA_MAXUNSIGNED _UI64_MAX + +#else /* }{ */ + +#error "Compiler does not support 'long long'. Use option '-DLUA_32BITS' \ + or '-DLUA_C89_NUMBERS' (see file 'luaconf.h' for details)" + +#endif /* } */ + +#else /* }{ */ + +#error "numeric integer type not defined" + +#endif /* } */ + +/* }================================================================== */ + + +/* +** {================================================================== +** Dependencies with C99 and other C details +** =================================================================== +*/ + +/* +@@ l_sprintf is equivalent to 'snprintf' or 'sprintf' in C89. +** (All uses in Lua have only one format item.) +*/ +#if !defined(LUA_USE_C89) +#define l_sprintf(s,sz,f,i) snprintf(s,sz,f,i) +#else +#define l_sprintf(s,sz,f,i) ((void)(sz), sprintf(s,f,i)) +#endif + + +/* +@@ lua_strx2number converts a hexadecimal numeral to a number. +** In C99, 'strtod' does that conversion. Otherwise, you can +** leave 'lua_strx2number' undefined and Lua will provide its own +** implementation. +*/ +#if !defined(LUA_USE_C89) +#define lua_strx2number(s,p) lua_str2number(s,p) +#endif + + +/* +@@ lua_pointer2str converts a pointer to a readable string in a +** non-specified way. +*/ +#define lua_pointer2str(buff,sz,p) l_sprintf(buff,sz,"%p",p) + + +/* +@@ lua_number2strx converts a float to a hexadecimal numeral. +** In C99, 'sprintf' (with format specifiers '%a'/'%A') does that. +** Otherwise, you can leave 'lua_number2strx' undefined and Lua will +** provide its own implementation. +*/ +#if !defined(LUA_USE_C89) +#define lua_number2strx(L,b,sz,f,n) \ + ((void)L, l_sprintf(b,sz,f,(LUAI_UACNUMBER)(n))) +#endif + + +/* +** 'strtof' and 'opf' variants for math functions are not valid in +** C89. Otherwise, the macro 'HUGE_VALF' is a good proxy for testing the +** availability of these variants. ('math.h' is already included in +** all files that use these macros.) +*/ +#if defined(LUA_USE_C89) || (defined(HUGE_VAL) && !defined(HUGE_VALF)) +#undef l_mathop /* variants not available */ +#undef lua_str2number +#define l_mathop(op) (lua_Number)op /* no variant */ +#define lua_str2number(s,p) ((lua_Number)strtod((s), (p))) +#endif + + +/* +@@ LUA_KCONTEXT is the type of the context ('ctx') for continuation +** functions. It must be a numerical type; Lua will use 'intptr_t' if +** available, otherwise it will use 'ptrdiff_t' (the nearest thing to +** 'intptr_t' in C89) +*/ +#define LUA_KCONTEXT ptrdiff_t + +#if !defined(LUA_USE_C89) && defined(__STDC_VERSION__) && \ + __STDC_VERSION__ >= 199901L +#include +#if defined(INTPTR_MAX) /* even in C99 this type is optional */ +#undef LUA_KCONTEXT +#define LUA_KCONTEXT intptr_t +#endif +#endif + + +/* +@@ lua_getlocaledecpoint gets the locale "radix character" (decimal point). +** Change that if you do not want to use C locales. (Code using this +** macro must include the header 'locale.h'.) +*/ +#if !defined(lua_getlocaledecpoint) +#define lua_getlocaledecpoint() (localeconv()->decimal_point[0]) +#endif + + +/* +** macros to improve jump prediction, used mostly for error handling +** and debug facilities. (Some macros in the Lua API use these macros. +** Define LUA_NOBUILTIN if you do not want '__builtin_expect' in your +** code.) +*/ +#if !defined(luai_likely) + +#if !defined(LUA_NOBUILTIN) && defined(__GNUC__) && (__GNUC__ >= 3) +#define luai_likely(x) (__builtin_expect(((x) != 0), 1)) +#define luai_unlikely(x) (__builtin_expect(((x) != 0), 0)) +#else +#define luai_likely(x) (x) +#define luai_unlikely(x) (x) +#endif + +#endif + + + +/* }================================================================== */ + + +/* +** {================================================================== +** Language Variations +** ===================================================================== +*/ + +/* +@@ LUA_NOCVTN2S/LUA_NOCVTS2N control how Lua performs some +** coercions. Define LUA_NOCVTN2S to turn off automatic coercion from +** numbers to strings. Define LUA_NOCVTS2N to turn off automatic +** coercion from strings to numbers. +*/ +/* #define LUA_NOCVTN2S */ +/* #define LUA_NOCVTS2N */ + + +/* +@@ LUA_USE_APICHECK turns on several consistency checks on the C API. +** Define it as a help when debugging C code. +*/ +/* #define LUA_USE_APICHECK */ + +/* }================================================================== */ + + +/* +** {================================================================== +** Macros that affect the API and must be stable (that is, must be the +** same when you compile Lua and when you compile code that links to +** Lua). +** ===================================================================== +*/ + +/* +@@ LUA_EXTRASPACE defines the size of a raw memory area associated with +** a Lua state with very fast access. +** CHANGE it if you need a different size. +*/ +#define LUA_EXTRASPACE (sizeof(void *)) + + +/* +@@ LUA_IDSIZE gives the maximum size for the description of the source +** of a function in debug information. +** CHANGE it if you want a different size. +*/ +#define LUA_IDSIZE 60 + + +/* +@@ LUAL_BUFFERSIZE is the initial buffer size used by the lauxlib +** buffer system. +*/ +#define LUAL_BUFFERSIZE ((int)(16 * sizeof(void*) * sizeof(lua_Number))) + + +/* +@@ LUAI_MAXALIGN defines fields that ensure proper alignment for +** memory areas offered by Lua (e.g., userdata memory). +** Add fields to it if you need alignment for non-ISO objects. +*/ +#if defined(LLONG_MAX) +/* use ISO C99 stuff */ +#define LUAI_MAXALIGN long double u; void *s; long long l +#else +/* use only C89 stuff */ +#define LUAI_MAXALIGN lua_Number n; double u; void *s; lua_Integer i; long l +#endif + +/* }================================================================== */ + + + + + +/* =================================================================== */ + +/* +** Local configuration. You can use this space to add your redefinitions +** without modifying the main part of the file. +*/ + + + +#endif diff --git a/crates/lua/vendor/lua/lualib.h b/crates/lua/vendor/lua/lualib.h new file mode 100644 index 0000000..068f60a --- /dev/null +++ b/crates/lua/vendor/lua/lualib.h @@ -0,0 +1,65 @@ +/* +** $Id: lualib.h $ +** Lua standard libraries +** See Copyright Notice in lua.h +*/ + + +#ifndef lualib_h +#define lualib_h + +#include "lua.h" + + +/* version suffix for environment variable names */ +#define LUA_VERSUFFIX "_" LUA_VERSION_MAJOR "_" LUA_VERSION_MINOR + +#define LUA_GLIBK 1 +LUAMOD_API int (luaopen_base) (lua_State *L); + +#define LUA_LOADLIBNAME "package" +#define LUA_LOADLIBK (LUA_GLIBK << 1) +LUAMOD_API int (luaopen_package) (lua_State *L); + + +#define LUA_COLIBNAME "coroutine" +#define LUA_COLIBK (LUA_LOADLIBK << 1) +LUAMOD_API int (luaopen_coroutine) (lua_State *L); + +#define LUA_DBLIBNAME "debug" +#define LUA_DBLIBK (LUA_COLIBK << 1) +LUAMOD_API int (luaopen_debug) (lua_State *L); + +#define LUA_IOLIBNAME "io" +#define LUA_IOLIBK (LUA_DBLIBK << 1) +LUAMOD_API int (luaopen_io) (lua_State *L); + +#define LUA_MATHLIBNAME "math" +#define LUA_MATHLIBK (LUA_IOLIBK << 1) +LUAMOD_API int (luaopen_math) (lua_State *L); + +#define LUA_OSLIBNAME "os" +#define LUA_OSLIBK (LUA_MATHLIBK << 1) +LUAMOD_API int (luaopen_os) (lua_State *L); + +#define LUA_STRLIBNAME "string" +#define LUA_STRLIBK (LUA_OSLIBK << 1) +LUAMOD_API int (luaopen_string) (lua_State *L); + +#define LUA_TABLIBNAME "table" +#define LUA_TABLIBK (LUA_STRLIBK << 1) +LUAMOD_API int (luaopen_table) (lua_State *L); + +#define LUA_UTF8LIBNAME "utf8" +#define LUA_UTF8LIBK (LUA_TABLIBK << 1) +LUAMOD_API int (luaopen_utf8) (lua_State *L); + + +/* open selected libraries */ +LUALIB_API void (luaL_openselectedlibs) (lua_State *L, int load, int preload); + +/* open all libraries */ +#define luaL_openlibs(L) luaL_openselectedlibs(L, ~0, 0) + + +#endif diff --git a/crates/lua/vendor/lua/lundump.c b/crates/lua/vendor/lua/lundump.c new file mode 100644 index 0000000..b39dd8a --- /dev/null +++ b/crates/lua/vendor/lua/lundump.c @@ -0,0 +1,420 @@ +/* +** $Id: lundump.c $ +** load precompiled Lua chunks +** See Copyright Notice in lua.h +*/ + +#define lundump_c +#define LUA_CORE + +#include "lprefix.h" + + +#include +#include + +#include "lua.h" + +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lmem.h" +#include "lobject.h" +#include "lstring.h" +#include "ltable.h" +#include "lundump.h" +#include "lzio.h" + + +#if !defined(luai_verifycode) +#define luai_verifycode(L,f) /* empty */ +#endif + + +typedef struct { + lua_State *L; + ZIO *Z; + const char *name; + Table *h; /* list for string reuse */ + size_t offset; /* current position relative to beginning of dump */ + lua_Unsigned nstr; /* number of strings in the list */ + lu_byte fixed; /* dump is fixed in memory */ +} LoadState; + + +static l_noret error (LoadState *S, const char *why) { + luaO_pushfstring(S->L, "%s: bad binary format (%s)", S->name, why); + luaD_throw(S->L, LUA_ERRSYNTAX); +} + + +/* +** All high-level loads go through loadVector; you can change it to +** adapt to the endianness of the input +*/ +#define loadVector(S,b,n) loadBlock(S,b,cast_sizet(n)*sizeof((b)[0])) + +static void loadBlock (LoadState *S, void *b, size_t size) { + if (luaZ_read(S->Z, b, size) != 0) + error(S, "truncated chunk"); + S->offset += size; +} + + +static void loadAlign (LoadState *S, unsigned align) { + unsigned padding = align - cast_uint(S->offset % align); + if (padding < align) { /* (padding == align) means no padding */ + lua_Integer paddingContent; + loadBlock(S, &paddingContent, padding); + lua_assert(S->offset % align == 0); + } +} + + +#define getaddr(S,n,t) cast(t *, getaddr_(S,cast_sizet(n) * sizeof(t))) + +static const void *getaddr_ (LoadState *S, size_t size) { + const void *block = luaZ_getaddr(S->Z, size); + S->offset += size; + if (block == NULL) + error(S, "truncated fixed buffer"); + return block; +} + + +#define loadVar(S,x) loadVector(S,&x,1) + + +static lu_byte loadByte (LoadState *S) { + int b = zgetc(S->Z); + if (b == EOZ) + error(S, "truncated chunk"); + S->offset++; + return cast_byte(b); +} + + +static lua_Unsigned loadVarint (LoadState *S, lua_Unsigned limit) { + lua_Unsigned x = 0; + int b; + limit >>= 7; + do { + b = loadByte(S); + if (x > limit) + error(S, "integer overflow"); + x = (x << 7) | (b & 0x7f); + } while ((b & 0x80) != 0); + return x; +} + + +static size_t loadSize (LoadState *S) { + return cast_sizet(loadVarint(S, MAX_SIZE)); +} + + +static int loadInt (LoadState *S) { + return cast_int(loadVarint(S, cast_sizet(INT_MAX))); +} + + + +static lua_Number loadNumber (LoadState *S) { + lua_Number x; + loadVar(S, x); + return x; +} + + +static lua_Integer loadInteger (LoadState *S) { + lua_Unsigned cx = loadVarint(S, LUA_MAXUNSIGNED); + /* decode unsigned to signed */ + if ((cx & 1) != 0) + return l_castU2S(~(cx >> 1)); + else + return l_castU2S(cx >> 1); +} + + +/* +** Load a nullable string into slot 'sl' from prototype 'p'. The +** assignment to the slot and the barrier must be performed before any +** possible GC activity, to anchor the string. (Both 'loadVector' and +** 'luaH_setint' can call the GC.) +*/ +static void loadString (LoadState *S, Proto *p, TString **sl) { + lua_State *L = S->L; + TString *ts; + TValue sv; + size_t size = loadSize(S); + if (size == 0) { /* previously saved string? */ + lua_Unsigned idx = loadVarint(S, LUA_MAXUNSIGNED); /* get its index */ + TValue stv; + if (idx == 0) { /* no string? */ + lua_assert(*sl == NULL); /* must be prefilled */ + return; + } + if (novariant(luaH_getint(S->h, l_castU2S(idx), &stv)) != LUA_TSTRING) + error(S, "invalid string index"); + *sl = ts = tsvalue(&stv); /* get its value */ + luaC_objbarrier(L, p, ts); + return; /* do not save it again */ + } + else if ((size -= 1) <= LUAI_MAXSHORTLEN) { /* short string? */ + char buff[LUAI_MAXSHORTLEN + 1]; /* extra space for '\0' */ + loadVector(S, buff, size + 1); /* load string into buffer */ + *sl = ts = luaS_newlstr(L, buff, size); /* create string */ + luaC_objbarrier(L, p, ts); + } + else if (S->fixed) { /* for a fixed buffer, use a fixed string */ + const char *s = getaddr(S, size + 1, char); /* get content address */ + *sl = ts = luaS_newextlstr(L, s, size, NULL, NULL); + luaC_objbarrier(L, p, ts); + } + else { /* create internal copy */ + *sl = ts = luaS_createlngstrobj(L, size); /* create string */ + luaC_objbarrier(L, p, ts); + loadVector(S, getlngstr(ts), size + 1); /* load directly in final place */ + } + /* add string to list of saved strings */ + S->nstr++; + setsvalue(L, &sv, ts); + luaH_setint(L, S->h, l_castU2S(S->nstr), &sv); + luaC_objbarrierback(L, obj2gco(S->h), ts); +} + + +static void loadCode (LoadState *S, Proto *f) { + int n = loadInt(S); + loadAlign(S, sizeof(f->code[0])); + if (S->fixed) { + f->code = getaddr(S, n, Instruction); + f->sizecode = n; + } + else { + f->code = luaM_newvectorchecked(S->L, n, Instruction); + f->sizecode = n; + loadVector(S, f->code, n); + } +} + + +static void loadFunction(LoadState *S, Proto *f); + + +static void loadConstants (LoadState *S, Proto *f) { + int i; + int n = loadInt(S); + f->k = luaM_newvectorchecked(S->L, n, TValue); + f->sizek = n; + for (i = 0; i < n; i++) + setnilvalue(&f->k[i]); + for (i = 0; i < n; i++) { + TValue *o = &f->k[i]; + int t = loadByte(S); + switch (t) { + case LUA_VNIL: + setnilvalue(o); + break; + case LUA_VFALSE: + setbfvalue(o); + break; + case LUA_VTRUE: + setbtvalue(o); + break; + case LUA_VNUMFLT: + setfltvalue(o, loadNumber(S)); + break; + case LUA_VNUMINT: + setivalue(o, loadInteger(S)); + break; + case LUA_VSHRSTR: + case LUA_VLNGSTR: { + lua_assert(f->source == NULL); + loadString(S, f, &f->source); /* use 'source' to anchor string */ + if (f->source == NULL) + error(S, "bad format for constant string"); + setsvalue2n(S->L, o, f->source); /* save it in the right place */ + f->source = NULL; + break; + } + default: error(S, "invalid constant"); + } + } +} + + +static void loadProtos (LoadState *S, Proto *f) { + int i; + int n = loadInt(S); + f->p = luaM_newvectorchecked(S->L, n, Proto *); + f->sizep = n; + for (i = 0; i < n; i++) + f->p[i] = NULL; + for (i = 0; i < n; i++) { + f->p[i] = luaF_newproto(S->L); + luaC_objbarrier(S->L, f, f->p[i]); + loadFunction(S, f->p[i]); + } +} + + +/* +** Load the upvalues for a function. The names must be filled first, +** because the filling of the other fields can raise read errors and +** the creation of the error message can call an emergency collection; +** in that case all prototypes must be consistent for the GC. +*/ +static void loadUpvalues (LoadState *S, Proto *f) { + int i; + int n = loadInt(S); + f->upvalues = luaM_newvectorchecked(S->L, n, Upvaldesc); + f->sizeupvalues = n; + for (i = 0; i < n; i++) /* make array valid for GC */ + f->upvalues[i].name = NULL; + for (i = 0; i < n; i++) { /* following calls can raise errors */ + f->upvalues[i].instack = loadByte(S); + f->upvalues[i].idx = loadByte(S); + f->upvalues[i].kind = loadByte(S); + } +} + + +static void loadDebug (LoadState *S, Proto *f) { + int i; + int n = loadInt(S); + if (S->fixed) { + f->lineinfo = getaddr(S, n, ls_byte); + f->sizelineinfo = n; + } + else { + f->lineinfo = luaM_newvectorchecked(S->L, n, ls_byte); + f->sizelineinfo = n; + loadVector(S, f->lineinfo, n); + } + n = loadInt(S); + if (n > 0) { + loadAlign(S, sizeof(int)); + if (S->fixed) { + f->abslineinfo = getaddr(S, n, AbsLineInfo); + f->sizeabslineinfo = n; + } + else { + f->abslineinfo = luaM_newvectorchecked(S->L, n, AbsLineInfo); + f->sizeabslineinfo = n; + loadVector(S, f->abslineinfo, n); + } + } + n = loadInt(S); + f->locvars = luaM_newvectorchecked(S->L, n, LocVar); + f->sizelocvars = n; + for (i = 0; i < n; i++) + f->locvars[i].varname = NULL; + for (i = 0; i < n; i++) { + loadString(S, f, &f->locvars[i].varname); + f->locvars[i].startpc = loadInt(S); + f->locvars[i].endpc = loadInt(S); + } + n = loadInt(S); + if (n != 0) /* does it have debug information? */ + n = f->sizeupvalues; /* must be this many */ + for (i = 0; i < n; i++) + loadString(S, f, &f->upvalues[i].name); +} + + +static void loadFunction (LoadState *S, Proto *f) { + f->linedefined = loadInt(S); + f->lastlinedefined = loadInt(S); + f->numparams = loadByte(S); + /* get only the meaningful flags */ + f->flag = cast_byte(loadByte(S) & ~PF_FIXED); + if (S->fixed) + f->flag |= PF_FIXED; /* signal that code is fixed */ + f->maxstacksize = loadByte(S); + loadCode(S, f); + loadConstants(S, f); + loadUpvalues(S, f); + loadProtos(S, f); + loadString(S, f, &f->source); + loadDebug(S, f); +} + + +static void checkliteral (LoadState *S, const char *s, const char *msg) { + char buff[sizeof(LUA_SIGNATURE) + sizeof(LUAC_DATA)]; /* larger than both */ + size_t len = strlen(s); + loadVector(S, buff, len); + if (memcmp(s, buff, len) != 0) + error(S, msg); +} + + +static l_noret numerror (LoadState *S, const char *what, const char *tname) { + const char *msg = luaO_pushfstring(S->L, "%s %s mismatch", tname, what); + error(S, msg); +} + + +static void checknumsize (LoadState *S, int size, const char *tname) { + if (size != loadByte(S)) + numerror(S, "size", tname); +} + + +static void checknumformat (LoadState *S, int eq, const char *tname) { + if (!eq) + numerror(S, "format", tname); +} + + +#define checknum(S,tvar,value,tname) \ + { tvar i; checknumsize(S, sizeof(i), tname); \ + loadVar(S, i); \ + checknumformat(S, i == value, tname); } + + +static void checkHeader (LoadState *S) { + /* skip 1st char (already read and checked) */ + checkliteral(S, &LUA_SIGNATURE[1], "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"); + checknum(S, int, LUAC_INT, "int"); + checknum(S, Instruction, LUAC_INST, "instruction"); + checknum(S, lua_Integer, LUAC_INT, "Lua integer"); + checknum(S, lua_Number, LUAC_NUM, "Lua number"); +} + + +/* +** Load precompiled chunk. +*/ +LClosure *luaU_undump (lua_State *L, ZIO *Z, Table *anchor, const char *name, + int fixed) { + LoadState S; + LClosure *cl; + if (*name == '@' || *name == '=') + name = name + 1; + else if (*name == LUA_SIGNATURE[0]) + name = "binary string"; + S.name = name; + S.L = L; + S.Z = Z; + S.fixed = cast_byte(fixed); + S.offset = 1; /* fist byte was already read */ + checkHeader(&S); + S.h = anchor; + S.nstr = 0; + cl = luaF_newLclosure(L, loadByte(&S)); + luaD_anchorobj(L, anchor, obj2gco(cl)); + cl->p = luaF_newproto(L); + luaC_objbarrier(L, cl, cl->p); + loadFunction(&S, cl->p); + if (cl->nupvalues != cl->p->sizeupvalues) + error(&S, "corrupted chunk"); + luai_verifycode(L, cl->p); + return cl; +} diff --git a/crates/lua/vendor/lua/lundump.h b/crates/lua/vendor/lua/lundump.h new file mode 100644 index 0000000..186e25f --- /dev/null +++ b/crates/lua/vendor/lua/lundump.h @@ -0,0 +1,40 @@ +/* +** $Id: lundump.h $ +** load precompiled Lua chunks +** See Copyright Notice in lua.h +*/ + +#ifndef lundump_h +#define lundump_h + +#include + +#include "llimits.h" +#include "lobject.h" +#include "lzio.h" + + +/* data to catch conversion errors */ +#define LUAC_DATA "\x19\x93\r\n\x1a\n" + +#define LUAC_INT -0x5678 +#define LUAC_INST 0x12345678 +#define LUAC_NUM cast_num(-370.5) + +/* +** Encode major-minor version in one byte, one nibble for each +*/ +#define LUAC_VERSION (LUA_VERSION_MAJOR_N*16+LUA_VERSION_MINOR_N) + +#define LUAC_FORMAT 0 /* this is the official format */ + + +/* load one chunk; from lundump.c */ +LUAI_FUNC LClosure* luaU_undump (lua_State* L, ZIO* Z, Table *anchor, + const char* name, int fixed); + +/* dump one chunk; from ldump.c */ +LUAI_FUNC int luaU_dump (lua_State* L, const Proto* f, lua_Writer w, + void* data, int strip); + +#endif diff --git a/crates/lua/vendor/lua/lutf8lib.c b/crates/lua/vendor/lua/lutf8lib.c new file mode 100644 index 0000000..804e316 --- /dev/null +++ b/crates/lua/vendor/lua/lutf8lib.c @@ -0,0 +1,293 @@ +/* +** $Id: lutf8lib.c $ +** Standard library for UTF-8 manipulation +** See Copyright Notice in lua.h +*/ + +#define lutf8lib_c +#define LUA_LIB + +#include "lprefix.h" + + +#include +#include +#include + +#include "lua.h" + +#include "lauxlib.h" +#include "lualib.h" +#include "llimits.h" + + +#define MAXUNICODE 0x10FFFFu + +#define MAXUTF 0x7FFFFFFFu + + +#define MSGInvalid "invalid UTF-8 code" + + +#define iscont(c) (((c) & 0xC0) == 0x80) +#define iscontp(p) iscont(*(p)) + + +/* from strlib */ +/* translate a relative string position: negative means back from end */ +static lua_Integer u_posrelat (lua_Integer pos, size_t len) { + if (pos >= 0) return pos; + else if (0u - (size_t)pos > len) return 0; + else return (lua_Integer)len + pos + 1; +} + + +/* +** Decode one UTF-8 sequence, returning NULL if byte sequence is +** invalid. The array 'limits' stores the minimum value for each +** sequence length, to check for overlong representations. Its first +** entry forces an error for non-ASCII bytes with no continuation +** bytes (count == 0). +*/ +static const char *utf8_decode (const char *s, l_uint32 *val, int strict) { + static const l_uint32 limits[] = + {~(l_uint32)0, 0x80, 0x800, 0x10000u, 0x200000u, 0x4000000u}; + unsigned int c = (unsigned char)s[0]; + l_uint32 res = 0; /* final result */ + if (c < 0x80) /* ASCII? */ + res = c; + else if (c >= 0xfe) /* c >= 1111 1110b ? */ + return NULL; /* would need six or more continuation bytes */ + else { + int count = 0; /* to count number of continuation bytes */ + for (; c & 0x40; c <<= 1) { /* while it needs continuation bytes... */ + unsigned int cc = (unsigned char)s[++count]; /* read next byte */ + if (!iscont(cc)) /* not a continuation byte? */ + return NULL; /* invalid byte sequence */ + res = (res << 6) | (cc & 0x3F); /* add lower 6 bits from cont. byte */ + } + lua_assert(count <= 5); + res |= ((l_uint32)(c & 0x7F) << (count * 5)); /* add first byte */ + if (res > MAXUTF || res < limits[count]) + return NULL; /* invalid byte sequence */ + s += count; /* skip continuation bytes read */ + } + if (strict) { + /* check for invalid code points; too large or surrogates */ + if (res > MAXUNICODE || (0xD800u <= res && res <= 0xDFFFu)) + return NULL; + } + if (val) *val = res; + return s + 1; /* +1 to include first byte */ +} + + +/* +** utf8len(s [, i [, j [, lax]]]) --> number of characters that +** start in the range [i,j], or nil + current position if 's' is not +** well formed in that interval +*/ +static int utflen (lua_State *L) { + lua_Integer n = 0; /* counter for the number of characters */ + size_t len; /* string length in bytes */ + const char *s = luaL_checklstring(L, 1, &len); + lua_Integer posi = u_posrelat(luaL_optinteger(L, 2, 1), len); + lua_Integer posj = u_posrelat(luaL_optinteger(L, 3, -1), len); + int lax = lua_toboolean(L, 4); + luaL_argcheck(L, 1 <= posi && --posi <= (lua_Integer)len, 2, + "initial position out of bounds"); + luaL_argcheck(L, --posj < (lua_Integer)len, 3, + "final position out of bounds"); + while (posi <= posj) { + const char *s1 = utf8_decode(s + posi, NULL, !lax); + if (s1 == NULL) { /* conversion error? */ + luaL_pushfail(L); /* return fail ... */ + lua_pushinteger(L, posi + 1); /* ... and current position */ + return 2; + } + posi = ct_diff2S(s1 - s); + n++; + } + lua_pushinteger(L, n); + return 1; +} + + +/* +** codepoint(s, [i, [j [, lax]]]) -> returns codepoints for all +** characters that start in the range [i,j] +*/ +static int codepoint (lua_State *L) { + size_t len; + const char *s = luaL_checklstring(L, 1, &len); + lua_Integer posi = u_posrelat(luaL_optinteger(L, 2, 1), len); + lua_Integer pose = u_posrelat(luaL_optinteger(L, 3, posi), len); + int lax = lua_toboolean(L, 4); + int n; + const char *se; + luaL_argcheck(L, posi >= 1, 2, "out of bounds"); + luaL_argcheck(L, pose <= (lua_Integer)len, 3, "out of bounds"); + if (posi > pose) return 0; /* empty interval; return no values */ + if (pose - posi >= INT_MAX) /* (lua_Integer -> int) overflow? */ + return luaL_error(L, "string slice too long"); + n = (int)(pose - posi) + 1; /* upper bound for number of returns */ + luaL_checkstack(L, n, "string slice too long"); + n = 0; /* count the number of returns */ + se = s + pose; /* string end */ + for (s += posi - 1; s < se;) { + l_uint32 code; + s = utf8_decode(s, &code, !lax); + if (s == NULL) + return luaL_error(L, MSGInvalid); + lua_pushinteger(L, l_castU2S(code)); + n++; + } + return n; +} + + +static void pushutfchar (lua_State *L, int arg) { + lua_Unsigned code = (lua_Unsigned)luaL_checkinteger(L, arg); + luaL_argcheck(L, code <= MAXUTF, arg, "value out of range"); + lua_pushfstring(L, "%U", cast(unsigned long, code)); +} + + +/* +** utfchar(n1, n2, ...) -> char(n1)..char(n2)... +*/ +static int utfchar (lua_State *L) { + int n = lua_gettop(L); /* number of arguments */ + if (n == 1) /* optimize common case of single char */ + pushutfchar(L, 1); + else { + int i; + luaL_Buffer b; + luaL_buffinit(L, &b); + for (i = 1; i <= n; i++) { + pushutfchar(L, i); + luaL_addvalue(&b); + } + luaL_pushresult(&b); + } + return 1; +} + + +/* +** offset(s, n, [i]) -> indices where n-th character counting from +** position 'i' starts and ends; 0 means character at 'i'. +*/ +static int byteoffset (lua_State *L) { + size_t len; + const char *s = luaL_checklstring(L, 1, &len); + lua_Integer n = luaL_checkinteger(L, 2); + lua_Integer posi = (n >= 0) ? 1 : cast_st2S(len) + 1; + posi = u_posrelat(luaL_optinteger(L, 3, posi), len); + luaL_argcheck(L, 1 <= posi && --posi <= (lua_Integer)len, 3, + "position out of bounds"); + if (n == 0) { + /* find beginning of current byte sequence */ + while (posi > 0 && iscontp(s + posi)) posi--; + } + else { + if (iscontp(s + posi)) + return luaL_error(L, "initial position is a continuation byte"); + if (n < 0) { + while (n < 0 && posi > 0) { /* move back */ + do { /* find beginning of previous character */ + posi--; + } while (posi > 0 && iscontp(s + posi)); + n++; + } + } + else { + n--; /* do not move for 1st character */ + while (n > 0 && posi < (lua_Integer)len) { + do { /* find beginning of next character */ + posi++; + } while (iscontp(s + posi)); /* (cannot pass final '\0') */ + n--; + } + } + } + if (n != 0) { /* did not find given character? */ + luaL_pushfail(L); + return 1; + } + lua_pushinteger(L, posi + 1); /* initial position */ + if ((s[posi] & 0x80) != 0) { /* multi-byte character? */ + if (iscont(s[posi])) + return luaL_error(L, "initial position is a continuation byte"); + while (iscontp(s + posi + 1)) + posi++; /* skip to last continuation byte */ + } + /* else one-byte character: final position is the initial one */ + lua_pushinteger(L, posi + 1); /* 'posi' now is the final position */ + return 2; +} + + +static int iter_aux (lua_State *L, int strict) { + size_t len; + const char *s = luaL_checklstring(L, 1, &len); + lua_Unsigned n = (lua_Unsigned)lua_tointeger(L, 2); + if (n < len) { + while (iscontp(s + n)) n++; /* go to next character */ + } + if (n >= len) /* (also handles original 'n' being negative) */ + return 0; /* no more codepoints */ + else { + l_uint32 code; + const char *next = utf8_decode(s + n, &code, strict); + if (next == NULL || iscontp(next)) + return luaL_error(L, MSGInvalid); + lua_pushinteger(L, l_castU2S(n + 1)); + lua_pushinteger(L, l_castU2S(code)); + return 2; + } +} + + +static int iter_auxstrict (lua_State *L) { + return iter_aux(L, 1); +} + +static int iter_auxlax (lua_State *L) { + return iter_aux(L, 0); +} + + +static int iter_codes (lua_State *L) { + int lax = lua_toboolean(L, 2); + const char *s = luaL_checkstring(L, 1); + luaL_argcheck(L, !iscontp(s), 1, MSGInvalid); + lua_pushcfunction(L, lax ? iter_auxlax : iter_auxstrict); + lua_pushvalue(L, 1); + lua_pushinteger(L, 0); + return 3; +} + + +/* pattern to match a single UTF-8 character */ +#define UTF8PATT "[\0-\x7F\xC2-\xFD][\x80-\xBF]*" + + +static const luaL_Reg funcs[] = { + {"offset", byteoffset}, + {"codepoint", codepoint}, + {"char", utfchar}, + {"len", utflen}, + {"codes", iter_codes}, + /* placeholders */ + {"charpattern", NULL}, + {NULL, NULL} +}; + + +LUAMOD_API int luaopen_utf8 (lua_State *L) { + luaL_newlib(L, funcs); + lua_pushlstring(L, UTF8PATT, sizeof(UTF8PATT)/sizeof(char) - 1); + lua_setfield(L, -2, "charpattern"); + return 1; +} diff --git a/crates/lua/vendor/lua/lvm.c b/crates/lua/vendor/lua/lvm.c new file mode 100644 index 0000000..986c7db --- /dev/null +++ b/crates/lua/vendor/lua/lvm.c @@ -0,0 +1,1978 @@ +/* +** $Id: lvm.c $ +** Lua virtual machine +** See Copyright Notice in lua.h +*/ + +#define lvm_c +#define LUA_CORE + +#include "lprefix.h" + +#include +#include +#include +#include +#include +#include + +#include "lua.h" + +#include "lapi.h" +#include "ldebug.h" +#include "ldo.h" +#include "lfunc.h" +#include "lgc.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lstate.h" +#include "lstring.h" +#include "ltable.h" +#include "ltm.h" +#include "lvm.h" + + +/* +** By default, use jump tables in the main interpreter loop on gcc +** and compatible compilers. +*/ +#if !defined(LUA_USE_JUMPTABLE) +#if defined(__GNUC__) +#define LUA_USE_JUMPTABLE 1 +#else +#define LUA_USE_JUMPTABLE 0 +#endif +#endif + + + +/* limit for table tag-method chains (to avoid infinite loops) */ +#define MAXTAGLOOP 2000 + + +/* +** 'l_intfitsf' checks whether a given integer is in the range that +** can be converted to a float without rounding. Used in comparisons. +*/ + +/* number of bits in the mantissa of a float */ +#define NBM (l_floatatt(MANT_DIG)) + +/* +** Check whether some integers may not fit in a float, testing whether +** (maxinteger >> NBM) > 0. (That implies (1 << NBM) <= maxinteger.) +** (The shifts are done in parts, to avoid shifting by more than the size +** of an integer. In a worst case, NBM == 113 for long double and +** sizeof(long) == 32.) +*/ +#if ((((LUA_MAXINTEGER >> (NBM / 4)) >> (NBM / 4)) >> (NBM / 4)) \ + >> (NBM - (3 * (NBM / 4)))) > 0 + +/* limit for integers that fit in a float */ +#define MAXINTFITSF ((lua_Unsigned)1 << NBM) + +/* check whether 'i' is in the interval [-MAXINTFITSF, MAXINTFITSF] */ +#define l_intfitsf(i) ((MAXINTFITSF + l_castS2U(i)) <= (2 * MAXINTFITSF)) + +#else /* all integers fit in a float precisely */ + +#define l_intfitsf(i) 1 + +#endif + + +/* +** Try to convert a value from string to a number value. +** If the value is not a string or is a string not representing +** a valid numeral (or if coercions from strings to numbers +** are disabled via macro 'cvt2num'), do not modify 'result' +** and return 0. +*/ +static int l_strton (const TValue *obj, TValue *result) { + lua_assert(obj != result); + if (!cvt2num(obj)) /* is object not a string? */ + return 0; + else { + TString *st = tsvalue(obj); + size_t stlen; + const char *s = getlstr(st, stlen); + return (luaO_str2num(s, result) == stlen + 1); + } +} + + +/* +** Try to convert a value to a float. The float case is already handled +** by the macro 'tonumber'. +*/ +int luaV_tonumber_ (const TValue *obj, lua_Number *n) { + TValue v; + if (ttisinteger(obj)) { + *n = cast_num(ivalue(obj)); + return 1; + } + else if (l_strton(obj, &v)) { /* string coercible to number? */ + *n = nvalue(&v); /* convert result of 'luaO_str2num' to a float */ + return 1; + } + else + return 0; /* conversion failed */ +} + + +/* +** try to convert a float to an integer, rounding according to 'mode'. +*/ +int luaV_flttointeger (lua_Number n, lua_Integer *p, F2Imod mode) { + lua_Number f = l_floor(n); + if (n != f) { /* not an integral value? */ + if (mode == F2Ieq) return 0; /* fails if mode demands integral value */ + else if (mode == F2Iceil) /* needs ceiling? */ + f += 1; /* convert floor to ceiling (remember: n != f) */ + } + return lua_numbertointeger(f, p); +} + + +/* +** try to convert a value to an integer, rounding according to 'mode', +** without string coercion. +** ("Fast track" handled by macro 'tointegerns'.) +*/ +int luaV_tointegerns (const TValue *obj, lua_Integer *p, F2Imod mode) { + if (ttisfloat(obj)) + return luaV_flttointeger(fltvalue(obj), p, mode); + else if (ttisinteger(obj)) { + *p = ivalue(obj); + return 1; + } + else + return 0; +} + + +/* +** try to convert a value to an integer. +*/ +int luaV_tointeger (const TValue *obj, lua_Integer *p, F2Imod mode) { + TValue v; + if (l_strton(obj, &v)) /* does 'obj' point to a numerical string? */ + obj = &v; /* change it to point to its corresponding number */ + return luaV_tointegerns(obj, p, mode); +} + + +/* +** Try to convert a 'for' limit to an integer, preserving the semantics +** of the loop. Return true if the loop must not run; otherwise, '*p' +** gets the integer limit. +** (The following explanation assumes a positive step; it is valid for +** negative steps mutatis mutandis.) +** If the limit is an integer or can be converted to an integer, +** rounding down, that is the limit. +** Otherwise, check whether the limit can be converted to a float. If +** the float is too large, clip it to LUA_MAXINTEGER. If the float +** is too negative, the loop should not run, because any initial +** integer value is greater than such limit; so, the function returns +** true to signal that. (For this latter case, no integer limit would be +** correct; even a limit of LUA_MININTEGER would run the loop once for +** an initial value equal to LUA_MININTEGER.) +*/ +static int forlimit (lua_State *L, lua_Integer init, const TValue *lim, + lua_Integer *p, lua_Integer step) { + if (!luaV_tointeger(lim, p, (step < 0 ? F2Iceil : F2Ifloor))) { + /* not coercible to in integer */ + lua_Number flim; /* try to convert to float */ + if (!tonumber(lim, &flim)) /* cannot convert to float? */ + luaG_forerror(L, lim, "limit"); + /* else 'flim' is a float out of integer bounds */ + if (luai_numlt(0, flim)) { /* if it is positive, it is too large */ + if (step < 0) return 1; /* initial value must be less than it */ + *p = LUA_MAXINTEGER; /* truncate */ + } + else { /* it is less than min integer */ + if (step > 0) return 1; /* initial value must be greater than it */ + *p = LUA_MININTEGER; /* truncate */ + } + } + return (step > 0 ? init > *p : init < *p); /* not to run? */ +} + + +/* +** Prepare a numerical for loop (opcode OP_FORPREP). +** Before execution, stack is as follows: +** ra : initial value +** ra + 1 : limit +** ra + 2 : step +** Return true to skip the loop. Otherwise, +** after preparation, stack will be as follows: +** ra : loop counter (integer loops) or limit (float loops) +** ra + 1 : step +** ra + 2 : control variable +*/ +static int forprep (lua_State *L, StkId ra) { + TValue *pinit = s2v(ra); + TValue *plimit = s2v(ra + 1); + TValue *pstep = s2v(ra + 2); + if (ttisinteger(pinit) && ttisinteger(pstep)) { /* integer loop? */ + lua_Integer init = ivalue(pinit); + lua_Integer step = ivalue(pstep); + lua_Integer limit; + if (step == 0) + luaG_runerror(L, "'for' step is zero"); + if (forlimit(L, init, plimit, &limit, step)) + return 1; /* skip the loop */ + else { /* prepare loop counter */ + lua_Unsigned count; + if (step > 0) { /* ascending loop? */ + count = l_castS2U(limit) - l_castS2U(init); + if (step != 1) /* avoid division in the too common case */ + count /= l_castS2U(step); + } + else { /* step < 0; descending loop */ + count = l_castS2U(init) - l_castS2U(limit); + /* 'step+1' avoids negating 'mininteger' */ + count /= l_castS2U(-(step + 1)) + 1u; + } + /* use 'chgivalue' for places that for sure had integers */ + chgivalue(s2v(ra), l_castU2S(count)); /* change init to count */ + setivalue(s2v(ra + 1), step); /* change limit to step */ + chgivalue(s2v(ra + 2), init); /* change step to init */ + } + } + else { /* try making all values floats */ + lua_Number init; lua_Number limit; lua_Number step; + if (l_unlikely(!tonumber(plimit, &limit))) + luaG_forerror(L, plimit, "limit"); + if (l_unlikely(!tonumber(pstep, &step))) + luaG_forerror(L, pstep, "step"); + if (l_unlikely(!tonumber(pinit, &init))) + luaG_forerror(L, pinit, "initial value"); + if (step == 0) + luaG_runerror(L, "'for' step is zero"); + if (luai_numlt(0, step) ? luai_numlt(limit, init) + : luai_numlt(init, limit)) + return 1; /* skip the loop */ + else { + /* make sure all values are floats */ + setfltvalue(s2v(ra), limit); + setfltvalue(s2v(ra + 1), step); + setfltvalue(s2v(ra + 2), init); /* control variable */ + } + } + return 0; +} + + +/* +** Execute a step of a float numerical for loop, returning +** true iff the loop must continue. (The integer case is +** written inline with opcode OP_FORLOOP, for performance.) +*/ +static int floatforloop (lua_State *L, StkId ra) { + lua_Number step = fltvalue(s2v(ra + 1)); + lua_Number limit = fltvalue(s2v(ra)); + lua_Number idx = fltvalue(s2v(ra + 2)); /* control variable */ + idx = luai_numadd(L, idx, step); /* increment index */ + if (luai_numlt(0, step) ? luai_numle(idx, limit) + : luai_numle(limit, idx)) { + chgfltvalue(s2v(ra + 2), idx); /* update control variable */ + return 1; /* jump back */ + } + else + return 0; /* finish the loop */ +} + + +/* +** Finish the table access 'val = t[key]' and return the tag of the result. +*/ +lu_byte luaV_finishget (lua_State *L, const TValue *t, TValue *key, + StkId val, lu_byte tag) { + int loop; /* counter to avoid infinite loops */ + const TValue *tm; /* metamethod */ + for (loop = 0; loop < MAXTAGLOOP; loop++) { + if (tag == LUA_VNOTABLE) { /* 't' is not a table? */ + lua_assert(!ttistable(t)); + tm = luaT_gettmbyobj(L, t, TM_INDEX); + if (l_unlikely(notm(tm))) + luaG_typeerror(L, t, "index"); /* no metamethod */ + /* else will try the metamethod */ + } + else { /* 't' is a table */ + tm = fasttm(L, hvalue(t)->metatable, TM_INDEX); /* table's metamethod */ + if (tm == NULL) { /* no metamethod? */ + setnilvalue2s(val); /* result is nil */ + return LUA_VNIL; + } + /* else will try the metamethod */ + } + if (ttisfunction(tm)) { /* is metamethod a function? */ + tag = luaT_callTMres(L, tm, t, key, val); /* call it */ + return tag; /* return tag of the result */ + } + t = tm; /* else try to access 'tm[key]' */ + luaV_fastget(t, key, s2v(val), luaH_get, tag); + if (!tagisempty(tag)) + return tag; /* done */ + /* else repeat (tail call 'luaV_finishget') */ + } + luaG_runerror(L, "'__index' chain too long; possible loop"); + return 0; /* to avoid warnings */ +} + + +/* +** Finish a table assignment 't[key] = val'. +** About anchoring the table before the call to 'luaH_finishset': +** This call may trigger an emergency collection. When loop>0, +** the table being accessed is a field in some metatable. If this +** metatable is weak and the table is not anchored, this collection +** could collect that table while it is being updated. +*/ +void luaV_finishset (lua_State *L, const TValue *t, TValue *key, + TValue *val, int hres) { + int loop; /* counter to avoid infinite loops */ + for (loop = 0; loop < MAXTAGLOOP; loop++) { + const TValue *tm; /* '__newindex' metamethod */ + if (hres != HNOTATABLE) { /* is 't' a table? */ + Table *h = hvalue(t); /* save 't' table */ + tm = fasttm(L, h->metatable, TM_NEWINDEX); /* get metamethod */ + if (tm == NULL) { /* no metamethod? */ + sethvalue2s(L, L->top.p, h); /* anchor 't' */ + L->top.p++; /* assume EXTRA_STACK */ + luaH_finishset(L, h, key, val, hres); /* set new value */ + L->top.p--; + invalidateTMcache(h); + luaC_barrierback(L, obj2gco(h), val); + return; + } + /* else will try the metamethod */ + } + else { /* not a table; check metamethod */ + tm = luaT_gettmbyobj(L, t, TM_NEWINDEX); + if (l_unlikely(notm(tm))) + luaG_typeerror(L, t, "index"); + } + /* try the metamethod */ + if (ttisfunction(tm)) { + luaT_callTM(L, tm, t, key, val); + return; + } + t = tm; /* else must repeat assignment over 'tm' */ + /* do the equivalent to 'luaV_fastset', but saving 'h' */ + if (!ttistable(t)) + hres = HNOTATABLE; + else { + Table *h = hvalue(t); /* next call can change the value at 't' */ + hres = luaH_pset(h, key, val); + if (hres == HOK) { + luaC_barrierback(L, obj2gco(h), val); /* luaV_finishfastset */ + return; /* done */ + } + } + /* else 'return luaV_finishset(L, t, key, val, hres)' (loop) */ + } + luaG_runerror(L, "'__newindex' chain too long; possible loop"); +} + + +/* +** Function to be used for 0-terminated string order comparison +*/ +#if !defined(l_strcoll) +#define l_strcoll strcoll +#endif + + +/* +** Compare two strings 'ts1' x 'ts2', returning an integer less-equal- +** -greater than zero if 'ts1' is less-equal-greater than 'ts2'. +** The code is a little tricky because it allows '\0' in the strings +** and it uses 'strcoll' (to respect locales) for each segment +** of the strings. Note that segments can compare equal but still +** have different lengths. +*/ +static int l_strcmp (const TString *ts1, const TString *ts2) { + size_t rl1; /* real length */ + const char *s1 = getlstr(ts1, rl1); + size_t rl2; + const char *s2 = getlstr(ts2, rl2); + for (;;) { /* for each segment */ + int temp = l_strcoll(s1, s2); + if (temp != 0) /* not equal? */ + return temp; /* done */ + else { /* strings are equal up to a '\0' */ + size_t zl1 = strlen(s1); /* index of first '\0' in 's1' */ + size_t zl2 = strlen(s2); /* index of first '\0' in 's2' */ + if (zl2 == rl2) /* 's2' is finished? */ + return (zl1 == rl1) ? 0 : 1; /* check 's1' */ + else if (zl1 == rl1) /* 's1' is finished? */ + return -1; /* 's1' is less than 's2' ('s2' is not finished) */ + /* both strings longer than 'zl'; go on comparing after the '\0' */ + zl1++; zl2++; + s1 += zl1; rl1 -= zl1; s2 += zl2; rl2 -= zl2; + } + } +} + + +/* +** Check whether integer 'i' is less than float 'f'. If 'i' has an +** exact representation as a float ('l_intfitsf'), compare numbers as +** floats. Otherwise, use the equivalence 'i < f <=> i < ceil(f)'. +** If 'ceil(f)' is out of integer range, either 'f' is greater than +** all integers or less than all integers. +** (The test with 'l_intfitsf' is only for performance; the else +** case is correct for all values, but it is slow due to the conversion +** from float to int.) +** When 'f' is NaN, comparisons must result in false. +*/ +l_sinline int LTintfloat (lua_Integer i, lua_Number f) { + if (l_intfitsf(i)) + return luai_numlt(cast_num(i), f); /* compare them as floats */ + else { /* i < f <=> i < ceil(f) */ + lua_Integer fi; + if (luaV_flttointeger(f, &fi, F2Iceil)) /* fi = ceil(f) */ + return i < fi; /* compare them as integers */ + else /* 'f' is either greater or less than all integers */ + return f > 0; /* greater? */ + } +} + + +/* +** Check whether integer 'i' is less than or equal to float 'f'. +** See comments on previous function. +*/ +l_sinline int LEintfloat (lua_Integer i, lua_Number f) { + if (l_intfitsf(i)) + return luai_numle(cast_num(i), f); /* compare them as floats */ + else { /* i <= f <=> i <= floor(f) */ + lua_Integer fi; + if (luaV_flttointeger(f, &fi, F2Ifloor)) /* fi = floor(f) */ + return i <= fi; /* compare them as integers */ + else /* 'f' is either greater or less than all integers */ + return f > 0; /* greater? */ + } +} + + +/* +** Check whether float 'f' is less than integer 'i'. +** See comments on previous function. +*/ +l_sinline int LTfloatint (lua_Number f, lua_Integer i) { + if (l_intfitsf(i)) + return luai_numlt(f, cast_num(i)); /* compare them as floats */ + else { /* f < i <=> floor(f) < i */ + lua_Integer fi; + if (luaV_flttointeger(f, &fi, F2Ifloor)) /* fi = floor(f) */ + return fi < i; /* compare them as integers */ + else /* 'f' is either greater or less than all integers */ + return f < 0; /* less? */ + } +} + + +/* +** Check whether float 'f' is less than or equal to integer 'i'. +** See comments on previous function. +*/ +l_sinline int LEfloatint (lua_Number f, lua_Integer i) { + if (l_intfitsf(i)) + return luai_numle(f, cast_num(i)); /* compare them as floats */ + else { /* f <= i <=> ceil(f) <= i */ + lua_Integer fi; + if (luaV_flttointeger(f, &fi, F2Iceil)) /* fi = ceil(f) */ + return fi <= i; /* compare them as integers */ + else /* 'f' is either greater or less than all integers */ + return f < 0; /* less? */ + } +} + + +/* +** Return 'l < r', for numbers. +*/ +l_sinline int LTnum (const TValue *l, const TValue *r) { + lua_assert(ttisnumber(l) && ttisnumber(r)); + if (ttisinteger(l)) { + lua_Integer li = ivalue(l); + if (ttisinteger(r)) + return li < ivalue(r); /* both are integers */ + else /* 'l' is int and 'r' is float */ + return LTintfloat(li, fltvalue(r)); /* l < r ? */ + } + else { + lua_Number lf = fltvalue(l); /* 'l' must be float */ + if (ttisfloat(r)) + return luai_numlt(lf, fltvalue(r)); /* both are float */ + else /* 'l' is float and 'r' is int */ + return LTfloatint(lf, ivalue(r)); + } +} + + +/* +** Return 'l <= r', for numbers. +*/ +l_sinline int LEnum (const TValue *l, const TValue *r) { + lua_assert(ttisnumber(l) && ttisnumber(r)); + if (ttisinteger(l)) { + lua_Integer li = ivalue(l); + if (ttisinteger(r)) + return li <= ivalue(r); /* both are integers */ + else /* 'l' is int and 'r' is float */ + return LEintfloat(li, fltvalue(r)); /* l <= r ? */ + } + else { + lua_Number lf = fltvalue(l); /* 'l' must be float */ + if (ttisfloat(r)) + return luai_numle(lf, fltvalue(r)); /* both are float */ + else /* 'l' is float and 'r' is int */ + return LEfloatint(lf, ivalue(r)); + } +} + + +/* +** return 'l < r' for non-numbers. +*/ +static int lessthanothers (lua_State *L, const TValue *l, const TValue *r) { + lua_assert(!ttisnumber(l) || !ttisnumber(r)); + if (ttisstring(l) && ttisstring(r)) /* both are strings? */ + return l_strcmp(tsvalue(l), tsvalue(r)) < 0; + else + return luaT_callorderTM(L, l, r, TM_LT); +} + + +/* +** Main operation less than; return 'l < r'. +*/ +int luaV_lessthan (lua_State *L, const TValue *l, const TValue *r) { + if (ttisnumber(l) && ttisnumber(r)) /* both operands are numbers? */ + return LTnum(l, r); + else return lessthanothers(L, l, r); +} + + +/* +** return 'l <= r' for non-numbers. +*/ +static int lessequalothers (lua_State *L, const TValue *l, const TValue *r) { + lua_assert(!ttisnumber(l) || !ttisnumber(r)); + if (ttisstring(l) && ttisstring(r)) /* both are strings? */ + return l_strcmp(tsvalue(l), tsvalue(r)) <= 0; + else + return luaT_callorderTM(L, l, r, TM_LE); +} + + +/* +** Main operation less than or equal to; return 'l <= r'. +*/ +int luaV_lessequal (lua_State *L, const TValue *l, const TValue *r) { + if (ttisnumber(l) && ttisnumber(r)) /* both operands are numbers? */ + return LEnum(l, r); + else return lessequalothers(L, l, r); +} + + +/* +** Main operation for equality of Lua values; return 't1 == t2'. +** L == NULL means raw equality (no metamethods) +*/ +int luaV_equalobj (lua_State *L, const TValue *t1, const TValue *t2) { + const TValue *tm; + if (ttype(t1) != ttype(t2)) /* not the same type? */ + return 0; + else if (ttypetag(t1) != ttypetag(t2)) { + switch (ttypetag(t1)) { + case LUA_VNUMINT: { /* integer == float? */ + /* integer and float can only be equal if float has an integer + value equal to the integer */ + lua_Integer i2; + return (luaV_flttointeger(fltvalue(t2), &i2, F2Ieq) && + ivalue(t1) == i2); + } + case LUA_VNUMFLT: { /* float == integer? */ + lua_Integer i1; /* see comment in previous case */ + return (luaV_flttointeger(fltvalue(t1), &i1, F2Ieq) && + i1 == ivalue(t2)); + } + case LUA_VSHRSTR: case LUA_VLNGSTR: { + /* compare two strings with different variants: they can be + equal when one string is a short string and the other is + an external string */ + return luaS_eqstr(tsvalue(t1), tsvalue(t2)); + } + default: + /* only numbers (integer/float) and strings (long/short) can have + equal values with different variants */ + return 0; + } + } + else { /* equal variants */ + switch (ttypetag(t1)) { + case LUA_VNIL: case LUA_VFALSE: case LUA_VTRUE: + return 1; + case LUA_VNUMINT: + return (ivalue(t1) == ivalue(t2)); + case LUA_VNUMFLT: + return (fltvalue(t1) == fltvalue(t2)); + case LUA_VLIGHTUSERDATA: return pvalue(t1) == pvalue(t2); + case LUA_VSHRSTR: + return eqshrstr(tsvalue(t1), tsvalue(t2)); + case LUA_VLNGSTR: + return luaS_eqstr(tsvalue(t1), tsvalue(t2)); + case LUA_VUSERDATA: { + if (uvalue(t1) == uvalue(t2)) return 1; + else if (L == NULL) return 0; + tm = fasttm(L, uvalue(t1)->metatable, TM_EQ); + if (tm == NULL) + tm = fasttm(L, uvalue(t2)->metatable, TM_EQ); + break; /* will try TM */ + } + case LUA_VTABLE: { + if (hvalue(t1) == hvalue(t2)) return 1; + else if (L == NULL) return 0; + tm = fasttm(L, hvalue(t1)->metatable, TM_EQ); + if (tm == NULL) + tm = fasttm(L, hvalue(t2)->metatable, TM_EQ); + break; /* will try TM */ + } + case LUA_VLCF: + return (fvalue(t1) == fvalue(t2)); + default: /* functions and threads */ + return (gcvalue(t1) == gcvalue(t2)); + } + if (tm == NULL) /* no TM? */ + return 0; /* objects are different */ + else { + int tag = luaT_callTMres(L, tm, t1, t2, L->top.p); /* call TM */ + return !tagisfalse(tag); + } + } +} + + +/* macro used by 'luaV_concat' to ensure that element at 'o' is a string */ +#define tostring(L,o) \ + (ttisstring(o) || (cvt2str(o) && (luaO_tostring(L, o), 1))) + +/* +** Check whether object is a short empty string to optimize concatenation. +** (External strings can be empty too; they will be concatenated like +** non-empty ones.) +*/ +#define isemptystr(o) (ttisshrstring(o) && tsvalue(o)->shrlen == 0) + +/* copy strings in stack from top - n up to top - 1 to buffer */ +static void copy2buff (StkId top, int n, char *buff) { + size_t tl = 0; /* size already copied */ + do { + TString *st = tsvalue(s2v(top - n)); + size_t l; /* length of string being copied */ + const char *s = getlstr(st, l); + memcpy(buff + tl, s, l * sizeof(char)); + tl += l; + } while (--n > 0); +} + + +/* +** Main operation for concatenation: concat 'total' values in the stack, +** from 'L->top.p - total' up to 'L->top.p - 1'. +*/ +void luaV_concat (lua_State *L, int total) { + if (total == 1) + return; /* "all" values already concatenated */ + do { + StkId top = L->top.p; + int n = 2; /* number of elements handled in this pass (at least 2) */ + if (!(ttisstring(s2v(top - 2)) || cvt2str(s2v(top - 2))) || + !tostring(L, s2v(top - 1))) + luaT_tryconcatTM(L); /* may invalidate 'top' */ + else if (isemptystr(s2v(top - 1))) /* second operand is empty? */ + cast_void(tostring(L, s2v(top - 2))); /* result is first operand */ + else if (isemptystr(s2v(top - 2))) { /* first operand is empty string? */ + setobjs2s(L, top - 2, top - 1); /* result is second op. */ + } + else { + /* at least two string values; get as many as possible */ + size_t tl = tsslen(tsvalue(s2v(top - 1))); /* total length */ + TString *ts; + /* collect total length and number of strings */ + for (n = 1; n < total && tostring(L, s2v(top - n - 1)); n++) { + size_t l = tsslen(tsvalue(s2v(top - n - 1))); + if (l_unlikely(l >= MAX_SIZE - sizeof(TString) - tl)) { + L->top.p = top - total; /* pop strings to avoid wasting stack */ + luaG_runerror(L, "string length overflow"); + } + tl += l; + } + if (tl <= LUAI_MAXSHORTLEN) { /* is result a short string? */ + char buff[LUAI_MAXSHORTLEN]; + copy2buff(top, n, buff); /* copy strings to buffer */ + ts = luaS_newlstr(L, buff, tl); + } + else { /* long string; copy strings directly to final result */ + ts = luaS_createlngstrobj(L, tl); + copy2buff(top, n, getlngstr(ts)); + } + setsvalue2s(L, top - n, ts); /* create result */ + } + total -= n - 1; /* got 'n' strings to create one new */ + L->top.p -= n - 1; /* popped 'n' strings and pushed one */ + } while (total > 1); /* repeat until only 1 result left */ +} + + +/* +** Main operation 'ra = #rb'. +*/ +void luaV_objlen (lua_State *L, StkId ra, const TValue *rb) { + const TValue *tm; + switch (ttypetag(rb)) { + case LUA_VTABLE: { + Table *h = hvalue(rb); + tm = fasttm(L, h->metatable, TM_LEN); + if (tm) break; /* metamethod? break switch to call it */ + setivalue(s2v(ra), l_castU2S(luaH_getn(L, h))); /* else primitive len */ + return; + } + case LUA_VSHRSTR: { + setivalue(s2v(ra), tsvalue(rb)->shrlen); + return; + } + case LUA_VLNGSTR: { + setivalue(s2v(ra), cast_st2S(tsvalue(rb)->u.lnglen)); + return; + } + default: { /* try metamethod */ + tm = luaT_gettmbyobj(L, rb, TM_LEN); + if (l_unlikely(notm(tm))) /* no metamethod? */ + luaG_typeerror(L, rb, "get length of"); + break; + } + } + luaT_callTMres(L, tm, rb, rb, ra); +} + + +/* +** Integer division; return 'm // n', that is, floor(m/n). +** C division truncates its result (rounds towards zero). +** 'floor(q) == trunc(q)' when 'q >= 0' or when 'q' is integer, +** otherwise 'floor(q) == trunc(q) - 1'. +*/ +lua_Integer luaV_idiv (lua_State *L, lua_Integer m, lua_Integer n) { + if (l_unlikely(l_castS2U(n) + 1u <= 1u)) { /* special cases: -1 or 0 */ + if (n == 0) + luaG_runerror(L, "attempt to divide by zero"); + return intop(-, 0, m); /* n==-1; avoid overflow with 0x80000...//-1 */ + } + else { + lua_Integer q = m / n; /* perform C division */ + if ((m ^ n) < 0 && m % n != 0) /* 'm/n' would be negative non-integer? */ + q -= 1; /* correct result for different rounding */ + return q; + } +} + + +/* +** Integer modulus; return 'm % n'. (Assume that C '%' with +** negative operands follows C99 behavior. See previous comment +** about luaV_idiv.) +*/ +lua_Integer luaV_mod (lua_State *L, lua_Integer m, lua_Integer n) { + if (l_unlikely(l_castS2U(n) + 1u <= 1u)) { /* special cases: -1 or 0 */ + if (n == 0) + luaG_runerror(L, "attempt to perform 'n%%0'"); + return 0; /* m % -1 == 0; avoid overflow with 0x80000...%-1 */ + } + else { + lua_Integer r = m % n; + if (r != 0 && (r ^ n) < 0) /* 'm/n' would be non-integer negative? */ + r += n; /* correct result for different rounding */ + return r; + } +} + + +/* +** Float modulus +*/ +lua_Number luaV_modf (lua_State *L, lua_Number m, lua_Number n) { + lua_Number r; + luai_nummod(L, m, n, r); + return r; +} + + +/* number of bits in an integer */ +#define NBITS l_numbits(lua_Integer) + + +/* +** Shift left operation. (Shift right just negates 'y'.) +*/ +lua_Integer luaV_shiftl (lua_Integer x, lua_Integer y) { + if (y < 0) { /* shift right? */ + if (y <= -NBITS) return 0; + else return intop(>>, x, -y); + } + else { /* shift left */ + if (y >= NBITS) return 0; + else return intop(<<, x, y); + } +} + + +/* +** create a new Lua closure, push it in the stack, and initialize +** its upvalues. +*/ +static void pushclosure (lua_State *L, Proto *p, UpVal **encup, StkId base, + StkId ra) { + int nup = p->sizeupvalues; + Upvaldesc *uv = p->upvalues; + int i; + LClosure *ncl = luaF_newLclosure(L, nup); + ncl->p = p; + setclLvalue2s(L, ra, ncl); /* anchor new closure in stack */ + for (i = 0; i < nup; i++) { /* fill in its upvalues */ + if (uv[i].instack) /* upvalue refers to local variable? */ + ncl->upvals[i] = luaF_findupval(L, base + uv[i].idx); + else /* get upvalue from enclosing function */ + ncl->upvals[i] = encup[uv[i].idx]; + luaC_objbarrier(L, ncl, ncl->upvals[i]); + } +} + + +/* +** finish execution of an opcode interrupted by a yield +*/ +void luaV_finishOp (lua_State *L) { + CallInfo *ci = L->ci; + StkId base = ci->func.p + 1; + Instruction inst = *(ci->u.l.savedpc - 1); /* interrupted instruction */ + OpCode op = GET_OPCODE(inst); + switch (op) { /* finish its execution */ + case OP_MMBIN: case OP_MMBINI: case OP_MMBINK: { + setobjs2s(L, base + GETARG_A(*(ci->u.l.savedpc - 2)), --L->top.p); + break; + } + case OP_UNM: case OP_BNOT: case OP_LEN: + case OP_GETTABUP: case OP_GETTABLE: case OP_GETI: + case OP_GETFIELD: case OP_SELF: { + setobjs2s(L, base + GETARG_A(inst), --L->top.p); + break; + } + case OP_LT: case OP_LE: + case OP_LTI: case OP_LEI: + case OP_GTI: case OP_GEI: + case OP_EQ: { /* note that 'OP_EQI'/'OP_EQK' cannot yield */ + int res = !l_isfalse(s2v(L->top.p - 1)); + L->top.p--; + lua_assert(GET_OPCODE(*ci->u.l.savedpc) == OP_JMP); + if (res != GETARG_k(inst)) /* condition failed? */ + ci->u.l.savedpc++; /* skip jump instruction */ + break; + } + case OP_CONCAT: { + StkId top = L->top.p - 1; /* top when 'luaT_tryconcatTM' was called */ + int a = GETARG_A(inst); /* first element to concatenate */ + int total = cast_int(top - 1 - (base + a)); /* yet to concatenate */ + setobjs2s(L, top - 2, top); /* put TM result in proper position */ + L->top.p = top - 1; /* top is one after last element (at top-2) */ + luaV_concat(L, total); /* concat them (may yield again) */ + break; + } + case OP_CLOSE: { /* yielded closing variables */ + ci->u.l.savedpc--; /* repeat instruction to close other vars. */ + break; + } + case OP_RETURN: { /* yielded closing variables */ + StkId ra = base + GETARG_A(inst); + /* adjust top to signal correct number of returns, in case the + return is "up to top" ('isIT') */ + L->top.p = ra + ci->u2.nres; + /* repeat instruction to close other vars. and complete the return */ + ci->u.l.savedpc--; + break; + } + default: { + /* only these other opcodes can yield */ + lua_assert(op == OP_TFORCALL || op == OP_CALL || + op == OP_TAILCALL || op == OP_SETTABUP || op == OP_SETTABLE || + op == OP_SETI || op == OP_SETFIELD); + break; + } + } +} + + + + +/* +** {================================================================== +** Macros for arithmetic/bitwise/comparison opcodes in 'luaV_execute' +** +** All these macros are to be used exclusively inside the main +** interpreter loop (function luaV_execute) and may access directly +** the local variables of that function (L, i, pc, ci, etc.). +** =================================================================== +*/ + +#define l_addi(L,a,b) intop(+, a, b) +#define l_subi(L,a,b) intop(-, a, b) +#define l_muli(L,a,b) intop(*, a, b) +#define l_band(a,b) intop(&, a, b) +#define l_bor(a,b) intop(|, a, b) +#define l_bxor(a,b) intop(^, a, b) + +#define l_lti(a,b) (a < b) +#define l_lei(a,b) (a <= b) +#define l_gti(a,b) (a > b) +#define l_gei(a,b) (a >= b) + + +/* +** Arithmetic operations with immediate operands. 'iop' is the integer +** operation, 'fop' is the float operation. +*/ +#define op_arithI(L,iop,fop) { \ + TValue *ra = vRA(i); \ + TValue *v1 = vRB(i); \ + int imm = GETARG_sC(i); \ + if (ttisinteger(v1)) { \ + lua_Integer iv1 = ivalue(v1); \ + pc++; setivalue(ra, iop(L, iv1, imm)); \ + } \ + else if (ttisfloat(v1)) { \ + lua_Number nb = fltvalue(v1); \ + lua_Number fimm = cast_num(imm); \ + pc++; setfltvalue(ra, fop(L, nb, fimm)); \ + }} + + +/* +** Auxiliary function for arithmetic operations over floats and others +** with two operands. +*/ +#define op_arithf_aux(L,v1,v2,fop) { \ + lua_Number n1; lua_Number n2; \ + if (tonumberns(v1, n1) && tonumberns(v2, n2)) { \ + StkId ra = RA(i); \ + pc++; setfltvalue(s2v(ra), fop(L, n1, n2)); \ + }} + + +/* +** Arithmetic operations over floats and others with register operands. +*/ +#define op_arithf(L,fop) { \ + TValue *v1 = vRB(i); \ + TValue *v2 = vRC(i); \ + op_arithf_aux(L, v1, v2, fop); } + + +/* +** Arithmetic operations with K operands for floats. +*/ +#define op_arithfK(L,fop) { \ + TValue *v1 = vRB(i); \ + TValue *v2 = KC(i); lua_assert(ttisnumber(v2)); \ + op_arithf_aux(L, v1, v2, fop); } + + +/* +** Arithmetic operations over integers and floats. +*/ +#define op_arith_aux(L,v1,v2,iop,fop) { \ + if (ttisinteger(v1) && ttisinteger(v2)) { \ + StkId ra = RA(i); \ + lua_Integer i1 = ivalue(v1); lua_Integer i2 = ivalue(v2); \ + pc++; setivalue(s2v(ra), iop(L, i1, i2)); \ + } \ + else op_arithf_aux(L, v1, v2, fop); } + + +/* +** Arithmetic operations with register operands. +*/ +#define op_arith(L,iop,fop) { \ + TValue *v1 = vRB(i); \ + TValue *v2 = vRC(i); \ + op_arith_aux(L, v1, v2, iop, fop); } + + +/* +** Arithmetic operations with K operands. +*/ +#define op_arithK(L,iop,fop) { \ + TValue *v1 = vRB(i); \ + TValue *v2 = KC(i); lua_assert(ttisnumber(v2)); \ + op_arith_aux(L, v1, v2, iop, fop); } + + +/* +** Bitwise operations with constant operand. +*/ +#define op_bitwiseK(L,op) { \ + TValue *v1 = vRB(i); \ + TValue *v2 = KC(i); \ + lua_Integer i1; \ + lua_Integer i2 = ivalue(v2); \ + if (tointegerns(v1, &i1)) { \ + StkId ra = RA(i); \ + pc++; setivalue(s2v(ra), op(i1, i2)); \ + }} + + +/* +** Bitwise operations with register operands. +*/ +#define op_bitwise(L,op) { \ + TValue *v1 = vRB(i); \ + TValue *v2 = vRC(i); \ + lua_Integer i1; lua_Integer i2; \ + if (tointegerns(v1, &i1) && tointegerns(v2, &i2)) { \ + StkId ra = RA(i); \ + pc++; setivalue(s2v(ra), op(i1, i2)); \ + }} + + +/* +** Order operations with register operands. 'opn' actually works +** for all numbers, but the fast track improves performance for +** integers. +*/ +#define op_order(L,opi,opn,other) { \ + TValue *ra = vRA(i); \ + int cond; \ + TValue *rb = vRB(i); \ + if (ttisinteger(ra) && ttisinteger(rb)) { \ + lua_Integer ia = ivalue(ra); \ + lua_Integer ib = ivalue(rb); \ + cond = opi(ia, ib); \ + } \ + else if (ttisnumber(ra) && ttisnumber(rb)) \ + cond = opn(ra, rb); \ + else \ + Protect(cond = other(L, ra, rb)); \ + docondjump(); } + + +/* +** Order operations with immediate operand. (Immediate operand is +** always small enough to have an exact representation as a float.) +*/ +#define op_orderI(L,opi,opf,inv,tm) { \ + TValue *ra = vRA(i); \ + int cond; \ + int im = GETARG_sB(i); \ + if (ttisinteger(ra)) \ + cond = opi(ivalue(ra), im); \ + else if (ttisfloat(ra)) { \ + lua_Number fa = fltvalue(ra); \ + lua_Number fim = cast_num(im); \ + cond = opf(fa, fim); \ + } \ + else { \ + int isf = GETARG_C(i); \ + Protect(cond = luaT_callorderiTM(L, ra, im, inv, isf, tm)); \ + } \ + docondjump(); } + +/* }================================================================== */ + + +/* +** {================================================================== +** Function 'luaV_execute': main interpreter loop +** =================================================================== +*/ + +/* +** some macros for common tasks in 'luaV_execute' +*/ + + +#define RA(i) (base+GETARG_A(i)) +#define vRA(i) s2v(RA(i)) +#define RB(i) (base+GETARG_B(i)) +#define vRB(i) s2v(RB(i)) +#define KB(i) (k+GETARG_B(i)) +#define RC(i) (base+GETARG_C(i)) +#define vRC(i) s2v(RC(i)) +#define KC(i) (k+GETARG_C(i)) +#define RKC(i) ((TESTARG_k(i)) ? k + GETARG_C(i) : s2v(base + GETARG_C(i))) + + + +#define updatetrap(ci) (trap = ci->u.l.trap) + +#define updatebase(ci) (base = ci->func.p + 1) + + +#define updatestack(ci) \ + { if (l_unlikely(trap)) { updatebase(ci); ra = RA(i); } } + + +/* +** Execute a jump instruction. The 'updatetrap' allows signals to stop +** tight loops. (Without it, the local copy of 'trap' could never change.) +*/ +#define dojump(ci,i,e) { pc += GETARG_sJ(i) + e; updatetrap(ci); } + + +/* for test instructions, execute the jump instruction that follows it */ +#define donextjump(ci) { Instruction ni = *pc; dojump(ci, ni, 1); } + +/* +** do a conditional jump: skip next instruction if 'cond' is not what +** was expected (parameter 'k'), else do next instruction, which must +** be a jump. +*/ +#define docondjump() if (cond != GETARG_k(i)) pc++; else donextjump(ci); + + +/* +** Correct global 'pc'. +*/ +#define savepc(ci) (ci->u.l.savedpc = pc) + + +/* +** Whenever code can raise errors, the global 'pc' and the global +** 'top' must be correct to report occasional errors. +*/ +#define savestate(L,ci) (savepc(ci), L->top.p = ci->top.p) + + +/* +** Protect code that, in general, can raise errors, reallocate the +** stack, and change the hooks. +*/ +#define Protect(exp) (savestate(L,ci), (exp), updatetrap(ci)) + +/* special version that does not change the top */ +#define ProtectNT(exp) (savepc(ci), (exp), updatetrap(ci)) + +/* +** Protect code that can only raise errors. (That is, it cannot change +** the stack or hooks.) +*/ +#define halfProtect(exp) (savestate(L,ci), (exp)) + +/* +** macro executed during Lua functions at points where the +** function can yield. +*/ +#if !defined(luai_threadyield) +#define luai_threadyield(L) {lua_unlock(L); lua_lock(L);} +#endif + +/* 'c' is the limit of live values in the stack */ +#define checkGC(L,c) \ + { luaC_condGC(L, (savepc(ci), L->top.p = (c)), \ + updatetrap(ci)); \ + luai_threadyield(L); } + + +/* fetch an instruction and prepare its execution */ +#define vmfetch() { \ + if (l_unlikely(trap)) { /* stack reallocation or hooks? */ \ + trap = luaG_traceexec(L, pc); /* handle hooks */ \ + updatebase(ci); /* correct stack */ \ + } \ + i = *(pc++); \ +} + +#define vmdispatch(o) switch(o) +#define vmcase(l) case l: +#define vmbreak break + + +void luaV_execute (lua_State *L, CallInfo *ci) { + LClosure *cl; + TValue *k; + StkId base; + const Instruction *pc; + int trap; +#if LUA_USE_JUMPTABLE +#include "ljumptab.h" +#endif + startfunc: + trap = L->hookmask; + returning: /* trap already set */ + cl = ci_func(ci); + k = cl->p->k; + pc = ci->u.l.savedpc; + if (l_unlikely(trap)) + trap = luaG_tracecall(L); + base = ci->func.p + 1; + /* main loop of interpreter */ + for (;;) { + Instruction i; /* instruction being executed */ + vmfetch(); + #if 0 + { /* low-level line tracing for debugging Lua */ + #include "lopnames.h" + int pcrel = pcRel(pc, cl->p); + printf("line: %d; %s (%d)\n", luaG_getfuncline(cl->p, pcrel), + opnames[GET_OPCODE(i)], pcrel); + } + #endif + lua_assert(base == ci->func.p + 1); + lua_assert(base <= L->top.p && L->top.p <= L->stack_last.p); + /* for tests, invalidate top for instructions not expecting it */ + lua_assert(luaP_isIT(i) || (cast_void(L->top.p = base), 1)); + vmdispatch (GET_OPCODE(i)) { + vmcase(OP_MOVE) { + StkId ra = RA(i); + setobjs2s(L, ra, RB(i)); + vmbreak; + } + vmcase(OP_LOADI) { + StkId ra = RA(i); + lua_Integer b = GETARG_sBx(i); + setivalue(s2v(ra), b); + vmbreak; + } + vmcase(OP_LOADF) { + StkId ra = RA(i); + int b = GETARG_sBx(i); + setfltvalue(s2v(ra), cast_num(b)); + vmbreak; + } + vmcase(OP_LOADK) { + StkId ra = RA(i); + TValue *rb = k + GETARG_Bx(i); + setobj2s(L, ra, rb); + vmbreak; + } + vmcase(OP_LOADKX) { + StkId ra = RA(i); + TValue *rb; + rb = k + GETARG_Ax(*pc); pc++; + setobj2s(L, ra, rb); + vmbreak; + } + vmcase(OP_LOADFALSE) { + StkId ra = RA(i); + setbfvalue(s2v(ra)); + vmbreak; + } + vmcase(OP_LFALSESKIP) { + StkId ra = RA(i); + setbfvalue(s2v(ra)); + pc++; /* skip next instruction */ + vmbreak; + } + vmcase(OP_LOADTRUE) { + StkId ra = RA(i); + setbtvalue(s2v(ra)); + vmbreak; + } + vmcase(OP_LOADNIL) { + StkId ra = RA(i); + int b = GETARG_B(i); + do { + setnilvalue(s2v(ra++)); + } while (b--); + vmbreak; + } + vmcase(OP_GETUPVAL) { + StkId ra = RA(i); + int b = GETARG_B(i); + setobj2s(L, ra, cl->upvals[b]->v.p); + vmbreak; + } + vmcase(OP_SETUPVAL) { + StkId ra = RA(i); + UpVal *uv = cl->upvals[GETARG_B(i)]; + setobj(L, uv->v.p, s2v(ra)); + luaC_barrier(L, uv, s2v(ra)); + vmbreak; + } + vmcase(OP_GETTABUP) { + StkId ra = RA(i); + TValue *upval = cl->upvals[GETARG_B(i)]->v.p; + TValue *rc = KC(i); + TString *key = tsvalue(rc); /* key must be a short string */ + lu_byte tag; + luaV_fastget(upval, key, s2v(ra), luaH_getshortstr, tag); + if (tagisempty(tag)) + Protect(luaV_finishget(L, upval, rc, ra, tag)); + vmbreak; + } + vmcase(OP_GETTABLE) { + StkId ra = RA(i); + TValue *rb = vRB(i); + TValue *rc = vRC(i); + lu_byte tag; + if (ttisinteger(rc)) { /* fast track for integers? */ + luaV_fastgeti(rb, ivalue(rc), s2v(ra), tag); + } + else + luaV_fastget(rb, rc, s2v(ra), luaH_get, tag); + if (tagisempty(tag)) + Protect(luaV_finishget(L, rb, rc, ra, tag)); + vmbreak; + } + vmcase(OP_GETI) { + StkId ra = RA(i); + TValue *rb = vRB(i); + int c = GETARG_C(i); + lu_byte tag; + luaV_fastgeti(rb, c, s2v(ra), tag); + if (tagisempty(tag)) { + TValue key; + setivalue(&key, c); + Protect(luaV_finishget(L, rb, &key, ra, tag)); + } + vmbreak; + } + vmcase(OP_GETFIELD) { + StkId ra = RA(i); + TValue *rb = vRB(i); + TValue *rc = KC(i); + TString *key = tsvalue(rc); /* key must be a short string */ + lu_byte tag; + luaV_fastget(rb, key, s2v(ra), luaH_getshortstr, tag); + if (tagisempty(tag)) + Protect(luaV_finishget(L, rb, rc, ra, tag)); + vmbreak; + } + vmcase(OP_SETTABUP) { + int hres; + TValue *upval = cl->upvals[GETARG_A(i)]->v.p; + TValue *rb = KB(i); + TValue *rc = RKC(i); + TString *key = tsvalue(rb); /* key must be a short string */ + luaV_fastset(upval, key, rc, hres, luaH_psetshortstr); + if (hres == HOK) + luaV_finishfastset(L, upval, rc); + else + Protect(luaV_finishset(L, upval, rb, rc, hres)); + vmbreak; + } + vmcase(OP_SETTABLE) { + StkId ra = RA(i); + int hres; + TValue *rb = vRB(i); /* key (table is in 'ra') */ + TValue *rc = RKC(i); /* value */ + if (ttisinteger(rb)) { /* fast track for integers? */ + luaV_fastseti(s2v(ra), ivalue(rb), rc, hres); + } + else { + luaV_fastset(s2v(ra), rb, rc, hres, luaH_pset); + } + if (hres == HOK) + luaV_finishfastset(L, s2v(ra), rc); + else + Protect(luaV_finishset(L, s2v(ra), rb, rc, hres)); + vmbreak; + } + vmcase(OP_SETI) { + StkId ra = RA(i); + int hres; + int b = GETARG_B(i); + TValue *rc = RKC(i); + luaV_fastseti(s2v(ra), b, rc, hres); + if (hres == HOK) + luaV_finishfastset(L, s2v(ra), rc); + else { + TValue key; + setivalue(&key, b); + Protect(luaV_finishset(L, s2v(ra), &key, rc, hres)); + } + vmbreak; + } + vmcase(OP_SETFIELD) { + StkId ra = RA(i); + int hres; + TValue *rb = KB(i); + TValue *rc = RKC(i); + TString *key = tsvalue(rb); /* key must be a short string */ + luaV_fastset(s2v(ra), key, rc, hres, luaH_psetshortstr); + if (hres == HOK) + luaV_finishfastset(L, s2v(ra), rc); + else + Protect(luaV_finishset(L, s2v(ra), rb, rc, hres)); + vmbreak; + } + vmcase(OP_NEWTABLE) { + StkId ra = RA(i); + unsigned b = cast_uint(GETARG_vB(i)); /* log2(hash size) + 1 */ + unsigned c = cast_uint(GETARG_vC(i)); /* array size */ + Table *t; + if (b > 0) + b = 1u << (b - 1); /* hash size is 2^(b - 1) */ + if (TESTARG_k(i)) { /* non-zero extra argument? */ + lua_assert(GETARG_Ax(*pc) != 0); + /* add it to array size */ + c += cast_uint(GETARG_Ax(*pc)) * (MAXARG_vC + 1); + } + pc++; /* skip extra argument */ + L->top.p = ra + 1; /* correct top in case of emergency GC */ + t = luaH_new(L); /* memory allocation */ + sethvalue2s(L, ra, t); + if (b != 0 || c != 0) + luaH_resize(L, t, c, b); /* idem */ + checkGC(L, ra + 1); + vmbreak; + } + vmcase(OP_SELF) { + StkId ra = RA(i); + lu_byte tag; + TValue *rb = vRB(i); + TValue *rc = KC(i); + TString *key = tsvalue(rc); /* key must be a short string */ + setobj2s(L, ra + 1, rb); + luaV_fastget(rb, key, s2v(ra), luaH_getshortstr, tag); + if (tagisempty(tag)) + Protect(luaV_finishget(L, rb, rc, ra, tag)); + vmbreak; + } + vmcase(OP_ADDI) { + op_arithI(L, l_addi, luai_numadd); + vmbreak; + } + vmcase(OP_ADDK) { + op_arithK(L, l_addi, luai_numadd); + vmbreak; + } + vmcase(OP_SUBK) { + op_arithK(L, l_subi, luai_numsub); + vmbreak; + } + vmcase(OP_MULK) { + op_arithK(L, l_muli, luai_nummul); + vmbreak; + } + vmcase(OP_MODK) { + savestate(L, ci); /* in case of division by 0 */ + op_arithK(L, luaV_mod, luaV_modf); + vmbreak; + } + vmcase(OP_POWK) { + op_arithfK(L, luai_numpow); + vmbreak; + } + vmcase(OP_DIVK) { + op_arithfK(L, luai_numdiv); + vmbreak; + } + vmcase(OP_IDIVK) { + savestate(L, ci); /* in case of division by 0 */ + op_arithK(L, luaV_idiv, luai_numidiv); + vmbreak; + } + vmcase(OP_BANDK) { + op_bitwiseK(L, l_band); + vmbreak; + } + vmcase(OP_BORK) { + op_bitwiseK(L, l_bor); + vmbreak; + } + vmcase(OP_BXORK) { + op_bitwiseK(L, l_bxor); + vmbreak; + } + vmcase(OP_SHLI) { + StkId ra = RA(i); + TValue *rb = vRB(i); + int ic = GETARG_sC(i); + lua_Integer ib; + if (tointegerns(rb, &ib)) { + pc++; setivalue(s2v(ra), luaV_shiftl(ic, ib)); + } + vmbreak; + } + vmcase(OP_SHRI) { + StkId ra = RA(i); + TValue *rb = vRB(i); + int ic = GETARG_sC(i); + lua_Integer ib; + if (tointegerns(rb, &ib)) { + pc++; setivalue(s2v(ra), luaV_shiftl(ib, -ic)); + } + vmbreak; + } + vmcase(OP_ADD) { + op_arith(L, l_addi, luai_numadd); + vmbreak; + } + vmcase(OP_SUB) { + op_arith(L, l_subi, luai_numsub); + vmbreak; + } + vmcase(OP_MUL) { + op_arith(L, l_muli, luai_nummul); + vmbreak; + } + vmcase(OP_MOD) { + savestate(L, ci); /* in case of division by 0 */ + op_arith(L, luaV_mod, luaV_modf); + vmbreak; + } + vmcase(OP_POW) { + op_arithf(L, luai_numpow); + vmbreak; + } + vmcase(OP_DIV) { /* float division (always with floats) */ + op_arithf(L, luai_numdiv); + vmbreak; + } + vmcase(OP_IDIV) { /* floor division */ + savestate(L, ci); /* in case of division by 0 */ + op_arith(L, luaV_idiv, luai_numidiv); + vmbreak; + } + vmcase(OP_BAND) { + op_bitwise(L, l_band); + vmbreak; + } + vmcase(OP_BOR) { + op_bitwise(L, l_bor); + vmbreak; + } + vmcase(OP_BXOR) { + op_bitwise(L, l_bxor); + vmbreak; + } + vmcase(OP_SHL) { + op_bitwise(L, luaV_shiftl); + vmbreak; + } + vmcase(OP_SHR) { + op_bitwise(L, luaV_shiftr); + vmbreak; + } + vmcase(OP_MMBIN) { + StkId ra = RA(i); + Instruction pi = *(pc - 2); /* original arith. expression */ + TValue *rb = vRB(i); + TMS tm = (TMS)GETARG_C(i); + StkId result = RA(pi); + lua_assert(OP_ADD <= GET_OPCODE(pi) && GET_OPCODE(pi) <= OP_SHR); + Protect(luaT_trybinTM(L, s2v(ra), rb, result, tm)); + vmbreak; + } + vmcase(OP_MMBINI) { + StkId ra = RA(i); + Instruction pi = *(pc - 2); /* original arith. expression */ + int imm = GETARG_sB(i); + TMS tm = (TMS)GETARG_C(i); + int flip = GETARG_k(i); + StkId result = RA(pi); + Protect(luaT_trybiniTM(L, s2v(ra), imm, flip, result, tm)); + vmbreak; + } + vmcase(OP_MMBINK) { + StkId ra = RA(i); + Instruction pi = *(pc - 2); /* original arith. expression */ + TValue *imm = KB(i); + TMS tm = (TMS)GETARG_C(i); + int flip = GETARG_k(i); + StkId result = RA(pi); + Protect(luaT_trybinassocTM(L, s2v(ra), imm, flip, result, tm)); + vmbreak; + } + vmcase(OP_UNM) { + StkId ra = RA(i); + TValue *rb = vRB(i); + lua_Number nb; + if (ttisinteger(rb)) { + lua_Integer ib = ivalue(rb); + setivalue(s2v(ra), intop(-, 0, ib)); + } + else if (tonumberns(rb, nb)) { + setfltvalue(s2v(ra), luai_numunm(L, nb)); + } + else + Protect(luaT_trybinTM(L, rb, rb, ra, TM_UNM)); + vmbreak; + } + vmcase(OP_BNOT) { + StkId ra = RA(i); + TValue *rb = vRB(i); + lua_Integer ib; + if (tointegerns(rb, &ib)) { + setivalue(s2v(ra), intop(^, ~l_castS2U(0), ib)); + } + else + Protect(luaT_trybinTM(L, rb, rb, ra, TM_BNOT)); + vmbreak; + } + vmcase(OP_NOT) { + StkId ra = RA(i); + TValue *rb = vRB(i); + if (l_isfalse(rb)) + setbtvalue(s2v(ra)); + else + setbfvalue(s2v(ra)); + vmbreak; + } + vmcase(OP_LEN) { + StkId ra = RA(i); + Protect(luaV_objlen(L, ra, vRB(i))); + vmbreak; + } + vmcase(OP_CONCAT) { + StkId ra = RA(i); + int n = GETARG_B(i); /* number of elements to concatenate */ + L->top.p = ra + n; /* mark the end of concat operands */ + ProtectNT(luaV_concat(L, n)); + checkGC(L, L->top.p); /* 'luaV_concat' ensures correct top */ + vmbreak; + } + vmcase(OP_CLOSE) { + StkId ra = RA(i); + lua_assert(!GETARG_B(i)); /* 'close must be alive */ + Protect(luaF_close(L, ra, LUA_OK, 1)); + vmbreak; + } + vmcase(OP_TBC) { + StkId ra = RA(i); + /* create new to-be-closed upvalue */ + halfProtect(luaF_newtbcupval(L, ra)); + vmbreak; + } + vmcase(OP_JMP) { + dojump(ci, i, 0); + vmbreak; + } + vmcase(OP_EQ) { + StkId ra = RA(i); + int cond; + TValue *rb = vRB(i); + Protect(cond = luaV_equalobj(L, s2v(ra), rb)); + docondjump(); + vmbreak; + } + vmcase(OP_LT) { + op_order(L, l_lti, LTnum, lessthanothers); + vmbreak; + } + vmcase(OP_LE) { + op_order(L, l_lei, LEnum, lessequalothers); + vmbreak; + } + vmcase(OP_EQK) { + StkId ra = RA(i); + TValue *rb = KB(i); + /* basic types do not use '__eq'; we can use raw equality */ + int cond = luaV_rawequalobj(s2v(ra), rb); + docondjump(); + vmbreak; + } + vmcase(OP_EQI) { + StkId ra = RA(i); + int cond; + int im = GETARG_sB(i); + if (ttisinteger(s2v(ra))) + cond = (ivalue(s2v(ra)) == im); + else if (ttisfloat(s2v(ra))) + cond = luai_numeq(fltvalue(s2v(ra)), cast_num(im)); + else + cond = 0; /* other types cannot be equal to a number */ + docondjump(); + vmbreak; + } + vmcase(OP_LTI) { + op_orderI(L, l_lti, luai_numlt, 0, TM_LT); + vmbreak; + } + vmcase(OP_LEI) { + op_orderI(L, l_lei, luai_numle, 0, TM_LE); + vmbreak; + } + vmcase(OP_GTI) { + op_orderI(L, l_gti, luai_numgt, 1, TM_LT); + vmbreak; + } + vmcase(OP_GEI) { + op_orderI(L, l_gei, luai_numge, 1, TM_LE); + vmbreak; + } + vmcase(OP_TEST) { + StkId ra = RA(i); + int cond = !l_isfalse(s2v(ra)); + docondjump(); + vmbreak; + } + vmcase(OP_TESTSET) { + StkId ra = RA(i); + TValue *rb = vRB(i); + if (l_isfalse(rb) == GETARG_k(i)) + pc++; + else { + setobj2s(L, ra, rb); + donextjump(ci); + } + vmbreak; + } + vmcase(OP_CALL) { + StkId ra = RA(i); + CallInfo *newci; + int b = GETARG_B(i); + int nresults = GETARG_C(i) - 1; + if (b != 0) /* fixed number of arguments? */ + L->top.p = ra + b; /* top signals number of arguments */ + /* else previous instruction set top */ + savepc(ci); /* in case of errors */ + if ((newci = luaD_precall(L, ra, nresults)) == NULL) + updatetrap(ci); /* C call; nothing else to be done */ + else { /* Lua call: run function in this same C frame */ + ci = newci; + goto startfunc; + } + vmbreak; + } + vmcase(OP_TAILCALL) { + StkId ra = RA(i); + int b = GETARG_B(i); /* number of arguments + 1 (function) */ + int n; /* number of results when calling a C function */ + int nparams1 = GETARG_C(i); + /* delta is virtual 'func' - real 'func' (vararg functions) */ + int delta = (nparams1) ? ci->u.l.nextraargs + nparams1 : 0; + if (b != 0) + L->top.p = ra + b; + else /* previous instruction set top */ + b = cast_int(L->top.p - ra); + savepc(ci); /* several calls here can raise errors */ + if (TESTARG_k(i)) { + luaF_closeupval(L, base); /* close upvalues from current call */ + lua_assert(L->tbclist.p < base); /* no pending tbc variables */ + lua_assert(base == ci->func.p + 1); + } + if ((n = luaD_pretailcall(L, ci, ra, b, delta)) < 0) /* Lua function? */ + goto startfunc; /* execute the callee */ + else { /* C function? */ + ci->func.p -= delta; /* restore 'func' (if vararg) */ + luaD_poscall(L, ci, n); /* finish caller */ + updatetrap(ci); /* 'luaD_poscall' can change hooks */ + goto ret; /* caller returns after the tail call */ + } + } + vmcase(OP_RETURN) { + StkId ra = RA(i); + int n = GETARG_B(i) - 1; /* number of results */ + int nparams1 = GETARG_C(i); + if (n < 0) /* not fixed? */ + n = cast_int(L->top.p - ra); /* get what is available */ + savepc(ci); + if (TESTARG_k(i)) { /* may there be open upvalues? */ + ci->u2.nres = n; /* save number of returns */ + if (L->top.p < ci->top.p) + L->top.p = ci->top.p; + luaF_close(L, base, CLOSEKTOP, 1); + updatetrap(ci); + updatestack(ci); + } + if (nparams1) /* vararg function? */ + ci->func.p -= ci->u.l.nextraargs + nparams1; + L->top.p = ra + n; /* set call for 'luaD_poscall' */ + luaD_poscall(L, ci, n); + updatetrap(ci); /* 'luaD_poscall' can change hooks */ + goto ret; + } + vmcase(OP_RETURN0) { + if (l_unlikely(L->hookmask)) { + StkId ra = RA(i); + L->top.p = ra; + savepc(ci); + luaD_poscall(L, ci, 0); /* no hurry... */ + trap = 1; + } + else { /* do the 'poscall' here */ + int nres = get_nresults(ci->callstatus); + L->ci = ci->previous; /* back to caller */ + L->top.p = base - 1; + for (; l_unlikely(nres > 0); nres--) + setnilvalue(s2v(L->top.p++)); /* all results are nil */ + } + goto ret; + } + vmcase(OP_RETURN1) { + if (l_unlikely(L->hookmask)) { + StkId ra = RA(i); + L->top.p = ra + 1; + savepc(ci); + luaD_poscall(L, ci, 1); /* no hurry... */ + trap = 1; + } + else { /* do the 'poscall' here */ + int nres = get_nresults(ci->callstatus); + L->ci = ci->previous; /* back to caller */ + if (nres == 0) + L->top.p = base - 1; /* asked for no results */ + else { + StkId ra = RA(i); + setobjs2s(L, base - 1, ra); /* at least this result */ + L->top.p = base; + for (; l_unlikely(nres > 1); nres--) + setnilvalue(s2v(L->top.p++)); /* complete missing results */ + } + } + ret: /* return from a Lua function */ + if (ci->callstatus & CIST_FRESH) + return; /* end this frame */ + else { + ci = ci->previous; + goto returning; /* continue running caller in this frame */ + } + } + vmcase(OP_FORLOOP) { + StkId ra = RA(i); + if (ttisinteger(s2v(ra + 1))) { /* integer loop? */ + lua_Unsigned count = l_castS2U(ivalue(s2v(ra))); + if (count > 0) { /* still more iterations? */ + lua_Integer step = ivalue(s2v(ra + 1)); + lua_Integer idx = ivalue(s2v(ra + 2)); /* control variable */ + chgivalue(s2v(ra), l_castU2S(count - 1)); /* update counter */ + idx = intop(+, idx, step); /* add step to index */ + chgivalue(s2v(ra + 2), idx); /* update control variable */ + pc -= GETARG_Bx(i); /* jump back */ + } + } + else if (floatforloop(L, ra)) /* float loop */ + pc -= GETARG_Bx(i); /* jump back */ + updatetrap(ci); /* allows a signal to break the loop */ + vmbreak; + } + vmcase(OP_FORPREP) { + StkId ra = RA(i); + savestate(L, ci); /* in case of errors */ + if (forprep(L, ra)) + pc += GETARG_Bx(i) + 1; /* skip the loop */ + vmbreak; + } + vmcase(OP_TFORPREP) { + /* before: 'ra' has the iterator function, 'ra + 1' has the state, + 'ra + 2' has the initial value for the control variable, and + 'ra + 3' has the closing variable. This opcode then swaps the + control and the closing variables and marks the closing variable + as to-be-closed. + */ + StkId ra = RA(i); + TValue temp; /* to swap control and closing variables */ + setobj(L, &temp, s2v(ra + 3)); + setobjs2s(L, ra + 3, ra + 2); + setobj2s(L, ra + 2, &temp); + /* create to-be-closed upvalue (if closing var. is not nil) */ + halfProtect(luaF_newtbcupval(L, ra + 2)); + pc += GETARG_Bx(i); /* go to end of the loop */ + i = *(pc++); /* fetch next instruction */ + lua_assert(GET_OPCODE(i) == OP_TFORCALL && ra == RA(i)); + goto l_tforcall; + } + vmcase(OP_TFORCALL) { + l_tforcall: { + /* 'ra' has the iterator function, 'ra + 1' has the state, + 'ra + 2' has the closing variable, and 'ra + 3' has the control + variable. The call will use the stack starting at 'ra + 3', + so that it preserves the first three values, and the first + return will be the new value for the control variable. + */ + StkId ra = RA(i); + setobjs2s(L, ra + 5, ra + 3); /* copy the control variable */ + setobjs2s(L, ra + 4, ra + 1); /* copy state */ + setobjs2s(L, ra + 3, ra); /* copy function */ + L->top.p = ra + 3 + 3; + ProtectNT(luaD_call(L, ra + 3, GETARG_C(i))); /* do the call */ + updatestack(ci); /* stack may have changed */ + i = *(pc++); /* go to next instruction */ + lua_assert(GET_OPCODE(i) == OP_TFORLOOP && ra == RA(i)); + goto l_tforloop; + }} + vmcase(OP_TFORLOOP) { + l_tforloop: { + StkId ra = RA(i); + if (!ttisnil(s2v(ra + 3))) /* continue loop? */ + pc -= GETARG_Bx(i); /* jump back */ + vmbreak; + }} + vmcase(OP_SETLIST) { + StkId ra = RA(i); + unsigned n = cast_uint(GETARG_vB(i)); + unsigned last = cast_uint(GETARG_vC(i)); + Table *h = hvalue(s2v(ra)); + if (n == 0) + n = cast_uint(L->top.p - ra) - 1; /* get up to the top */ + else + L->top.p = ci->top.p; /* correct top in case of emergency GC */ + last += n; + if (TESTARG_k(i)) { + last += cast_uint(GETARG_Ax(*pc)) * (MAXARG_vC + 1); + pc++; + } + /* when 'n' is known, table should have proper size */ + if (last > h->asize) { /* needs more space? */ + /* fixed-size sets should have space preallocated */ + lua_assert(GETARG_vB(i) == 0); + luaH_resizearray(L, h, last); /* preallocate it at once */ + } + for (; n > 0; n--) { + TValue *val = s2v(ra + n); + obj2arr(h, last - 1, val); + last--; + luaC_barrierback(L, obj2gco(h), val); + } + vmbreak; + } + vmcase(OP_CLOSURE) { + StkId ra = RA(i); + Proto *p = cl->p->p[GETARG_Bx(i)]; + halfProtect(pushclosure(L, p, cl->upvals, base, ra)); + checkGC(L, ra + 1); + vmbreak; + } + vmcase(OP_VARARG) { + StkId ra = RA(i); + int n = GETARG_C(i) - 1; /* required results (-1 means all) */ + int vatab = GETARG_k(i) ? GETARG_B(i) : -1; + Protect(luaT_getvarargs(L, ci, ra, n, vatab)); + vmbreak; + } + vmcase(OP_GETVARG) { + StkId ra = RA(i); + TValue *rc = vRC(i); + luaT_getvararg(ci, ra, rc); + vmbreak; + } + vmcase(OP_ERRNNIL) { + TValue *ra = vRA(i); + if (!ttisnil(ra)) + halfProtect(luaG_errnnil(L, cl, GETARG_Bx(i))); + vmbreak; + } + vmcase(OP_VARARGPREP) { + ProtectNT(luaT_adjustvarargs(L, ci, cl->p)); + if (l_unlikely(trap)) { /* previous "Protect" updated trap */ + luaD_hookcall(L, ci); + L->oldpc = 1; /* next opcode will be seen as a "new" line */ + } + updatebase(ci); /* function has new base after adjustment */ + vmbreak; + } + vmcase(OP_EXTRAARG) { + lua_assert(0); + vmbreak; + } + } + } +} + +/* }================================================================== */ diff --git a/crates/lua/vendor/lua/lvm.h b/crates/lua/vendor/lua/lvm.h new file mode 100644 index 0000000..be7b9cb --- /dev/null +++ b/crates/lua/vendor/lua/lvm.h @@ -0,0 +1,136 @@ +/* +** $Id: lvm.h $ +** Lua virtual machine +** See Copyright Notice in lua.h +*/ + +#ifndef lvm_h +#define lvm_h + + +#include "ldo.h" +#include "lobject.h" +#include "ltm.h" + + +#if !defined(LUA_NOCVTN2S) +#define cvt2str(o) ttisnumber(o) +#else +#define cvt2str(o) 0 /* no conversion from numbers to strings */ +#endif + + +#if !defined(LUA_NOCVTS2N) +#define cvt2num(o) ttisstring(o) +#else +#define cvt2num(o) 0 /* no conversion from strings to numbers */ +#endif + + +/* +** You can define LUA_FLOORN2I if you want to convert floats to integers +** by flooring them (instead of raising an error if they are not +** integral values) +*/ +#if !defined(LUA_FLOORN2I) +#define LUA_FLOORN2I F2Ieq +#endif + + +/* +** Rounding modes for float->integer coercion + */ +typedef enum { + F2Ieq, /* no rounding; accepts only integral values */ + F2Ifloor, /* takes the floor of the number */ + F2Iceil /* takes the ceiling of the number */ +} F2Imod; + + +/* convert an object to a float (including string coercion) */ +#define tonumber(o,n) \ + (ttisfloat(o) ? (*(n) = fltvalue(o), 1) : luaV_tonumber_(o,n)) + + +/* convert an object to a float (without string coercion) */ +#define tonumberns(o,n) \ + (ttisfloat(o) ? ((n) = fltvalue(o), 1) : \ + (ttisinteger(o) ? ((n) = cast_num(ivalue(o)), 1) : 0)) + + +/* convert an object to an integer (including string coercion) */ +#define tointeger(o,i) \ + (l_likely(ttisinteger(o)) ? (*(i) = ivalue(o), 1) \ + : luaV_tointeger(o,i,LUA_FLOORN2I)) + + +/* convert an object to an integer (without string coercion) */ +#define tointegerns(o,i) \ + (l_likely(ttisinteger(o)) ? (*(i) = ivalue(o), 1) \ + : luaV_tointegerns(o,i,LUA_FLOORN2I)) + + +#define intop(op,v1,v2) l_castU2S(l_castS2U(v1) op l_castS2U(v2)) + +#define luaV_rawequalobj(t1,t2) luaV_equalobj(NULL,t1,t2) + + +/* +** fast track for 'gettable' +*/ +#define luaV_fastget(t,k,res,f, tag) \ + (tag = (!ttistable(t) ? LUA_VNOTABLE : f(hvalue(t), k, res))) + + +/* +** Special case of 'luaV_fastget' for integers, inlining the fast case +** of 'luaH_getint'. +*/ +#define luaV_fastgeti(t,k,res,tag) \ + if (!ttistable(t)) tag = LUA_VNOTABLE; \ + else { luaH_fastgeti(hvalue(t), k, res, tag); } + + +#define luaV_fastset(t,k,val,hres,f) \ + (hres = (!ttistable(t) ? HNOTATABLE : f(hvalue(t), k, val))) + +#define luaV_fastseti(t,k,val,hres) \ + if (!ttistable(t)) hres = HNOTATABLE; \ + else { luaH_fastseti(hvalue(t), k, val, hres); } + + +/* +** Finish a fast set operation (when fast set succeeds). +*/ +#define luaV_finishfastset(L,t,v) luaC_barrierback(L, gcvalue(t), v) + + +/* +** Shift right is the same as shift left with a negative 'y' +*/ +#define luaV_shiftr(x,y) luaV_shiftl(x,intop(-, 0, y)) + + + +LUAI_FUNC int luaV_equalobj (lua_State *L, const TValue *t1, const TValue *t2); +LUAI_FUNC int luaV_lessthan (lua_State *L, const TValue *l, const TValue *r); +LUAI_FUNC int luaV_lessequal (lua_State *L, const TValue *l, const TValue *r); +LUAI_FUNC int luaV_tonumber_ (const TValue *obj, lua_Number *n); +LUAI_FUNC int luaV_tointeger (const TValue *obj, lua_Integer *p, F2Imod mode); +LUAI_FUNC int luaV_tointegerns (const TValue *obj, lua_Integer *p, + F2Imod mode); +LUAI_FUNC int luaV_flttointeger (lua_Number n, lua_Integer *p, F2Imod mode); +LUAI_FUNC lu_byte luaV_finishget (lua_State *L, const TValue *t, TValue *key, + StkId val, lu_byte tag); +LUAI_FUNC void luaV_finishset (lua_State *L, const TValue *t, TValue *key, + TValue *val, int aux); +LUAI_FUNC void luaV_finishOp (lua_State *L); +LUAI_FUNC void luaV_execute (lua_State *L, CallInfo *ci); +LUAI_FUNC void luaV_concat (lua_State *L, int total); +LUAI_FUNC lua_Integer luaV_idiv (lua_State *L, lua_Integer x, lua_Integer y); +LUAI_FUNC lua_Integer luaV_mod (lua_State *L, lua_Integer x, lua_Integer y); +LUAI_FUNC lua_Number luaV_modf (lua_State *L, lua_Number x, lua_Number y); +LUAI_FUNC lua_Integer luaV_shiftl (lua_Integer x, lua_Integer y); +LUAI_FUNC void luaV_objlen (lua_State *L, StkId ra, const TValue *rb); + +#endif diff --git a/crates/lua/vendor/lua/lzio.c b/crates/lua/vendor/lua/lzio.c new file mode 100644 index 0000000..301df4b --- /dev/null +++ b/crates/lua/vendor/lua/lzio.c @@ -0,0 +1,89 @@ +/* +** $Id: lzio.c $ +** Buffered streams +** See Copyright Notice in lua.h +*/ + +#define lzio_c +#define LUA_CORE + +#include "lprefix.h" + + +#include + +#include "lua.h" + +#include "lapi.h" +#include "llimits.h" +#include "lmem.h" +#include "lstate.h" +#include "lzio.h" + + +int luaZ_fill (ZIO *z) { + size_t size; + lua_State *L = z->L; + const char *buff; + lua_unlock(L); + buff = z->reader(L, z->data, &size); + lua_lock(L); + if (buff == NULL || size == 0) + return EOZ; + z->n = size - 1; /* discount char being returned */ + z->p = buff; + return cast_uchar(*(z->p++)); +} + + +void luaZ_init (lua_State *L, ZIO *z, lua_Reader reader, void *data) { + z->L = L; + z->reader = reader; + z->data = data; + z->n = 0; + z->p = NULL; +} + + +/* --------------------------------------------------------------- read --- */ + +static int checkbuffer (ZIO *z) { + if (z->n == 0) { /* no bytes in buffer? */ + if (luaZ_fill(z) == EOZ) /* try to read more */ + return 0; /* no more input */ + else { + z->n++; /* luaZ_fill consumed first byte; put it back */ + z->p--; + } + } + return 1; /* now buffer has something */ +} + + +size_t luaZ_read (ZIO *z, void *b, size_t n) { + while (n) { + size_t m; + if (!checkbuffer(z)) + return n; /* no more input; return number of missing bytes */ + m = (n <= z->n) ? n : z->n; /* min. between n and z->n */ + memcpy(b, z->p, m); + z->n -= m; + z->p += m; + b = (char *)b + m; + n -= m; + } + return 0; +} + + +const void *luaZ_getaddr (ZIO* z, size_t n) { + const void *res; + if (!checkbuffer(z)) + return NULL; /* no more input */ + if (z->n < n) /* not enough bytes? */ + return NULL; /* block not whole; cannot give an address */ + res = z->p; /* get block address */ + z->n -= n; /* consume these bytes */ + z->p += n; + return res; +} diff --git a/crates/lua/vendor/lua/lzio.h b/crates/lua/vendor/lua/lzio.h new file mode 100644 index 0000000..49047c9 --- /dev/null +++ b/crates/lua/vendor/lua/lzio.h @@ -0,0 +1,67 @@ +/* +** $Id: lzio.h $ +** Buffered streams +** See Copyright Notice in lua.h +*/ + + +#ifndef lzio_h +#define lzio_h + +#include "lua.h" + +#include "lmem.h" + + +#define EOZ (-1) /* end of stream */ + +typedef struct Zio ZIO; + +#define zgetc(z) (((z)->n--)>0 ? cast_uchar(*(z)->p++) : luaZ_fill(z)) + + +typedef struct Mbuffer { + char *buffer; + size_t n; + size_t buffsize; +} Mbuffer; + +#define luaZ_initbuffer(L, buff) ((buff)->buffer = NULL, (buff)->buffsize = 0) + +#define luaZ_buffer(buff) ((buff)->buffer) +#define luaZ_sizebuffer(buff) ((buff)->buffsize) +#define luaZ_bufflen(buff) ((buff)->n) + +#define luaZ_buffremove(buff,i) ((buff)->n -= cast_sizet(i)) +#define luaZ_resetbuffer(buff) ((buff)->n = 0) + + +#define luaZ_resizebuffer(L, buff, size) \ + ((buff)->buffer = luaM_reallocvchar(L, (buff)->buffer, \ + (buff)->buffsize, size), \ + (buff)->buffsize = size) + +#define luaZ_freebuffer(L, buff) luaZ_resizebuffer(L, buff, 0) + + +LUAI_FUNC void luaZ_init (lua_State *L, ZIO *z, lua_Reader reader, + void *data); +LUAI_FUNC size_t luaZ_read (ZIO* z, void *b, size_t n); /* read next n bytes */ + +LUAI_FUNC const void *luaZ_getaddr (ZIO* z, size_t n); + + +/* --------- Private Part ------------------ */ + +struct Zio { + size_t n; /* bytes still unread */ + const char *p; /* current position in buffer */ + lua_Reader reader; /* reader function */ + void *data; /* additional data */ + lua_State *L; /* Lua state (for reader) */ +}; + + +LUAI_FUNC int luaZ_fill (ZIO *z); + +#endif diff --git a/crates/lua/vendor/musl-setjmp/COPYRIGHT b/crates/lua/vendor/musl-setjmp/COPYRIGHT new file mode 100644 index 0000000..c1628e9 --- /dev/null +++ b/crates/lua/vendor/musl-setjmp/COPYRIGHT @@ -0,0 +1,193 @@ +musl as a whole is licensed under the following standard MIT license: + +---------------------------------------------------------------------- +Copyright © 2005-2020 Rich Felker, et al. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------------------------------------------------------------- + +Authors/contributors include: + +A. Wilcox +Ada Worcester +Alex Dowad +Alex Suykov +Alexander Monakov +Andre McCurdy +Andrew Kelley +Anthony G. Basile +Aric Belsito +Arvid Picciani +Bartosz Brachaczek +Benjamin Peterson +Bobby Bingham +Boris Brezillon +Brent Cook +Chris Spiegel +Clément Vasseur +Daniel Micay +Daniel Sabogal +Daurnimator +David Carlier +David Edelsohn +Denys Vlasenko +Dmitry Ivanov +Dmitry V. Levin +Drew DeVault +Emil Renner Berthing +Fangrui Song +Felix Fietkau +Felix Janda +Gianluca Anzolin +Hauke Mehrtens +He X +Hiltjo Posthuma +Isaac Dunham +Jaydeep Patil +Jens Gustedt +Jeremy Huntwork +Jo-Philipp Wich +Joakim Sindholt +John Spencer +Julien Ramseier +Justin Cormack +Kaarle Ritvanen +Khem Raj +Kylie McClain +Leah Neukirchen +Luca Barbato +Luka Perkov +M Farkas-Dyck (Strake) +Mahesh Bodapati +Markus Wichmann +Masanori Ogino +Michael Clark +Michael Forney +Mikhail Kremnyov +Natanael Copa +Nicholas J. Kain +orc +Pascal Cuoq +Patrick Oppenlander +Petr Hosek +Petr Skocik +Pierre Carrier +Reini Urban +Rich Felker +Richard Pennington +Ryan Fairfax +Samuel Holland +Segev Finer +Shiz +sin +Solar Designer +Stefan Kristiansson +Stefan O'Rear +Szabolcs Nagy +Timo Teräs +Trutz Behn +Valentin Ochs +Will Dietz +William Haddon +William Pitcock + +Portions of this software are derived from third-party works licensed +under terms compatible with the above MIT license: + +The TRE regular expression implementation (src/regex/reg* and +src/regex/tre*) is Copyright © 2001-2008 Ville Laurikari and licensed +under a 2-clause BSD license (license text in the source files). The +included version has been heavily modified by Rich Felker in 2012, in +the interests of size, simplicity, and namespace cleanliness. + +Much of the math library code (src/math/* and src/complex/*) is +Copyright © 1993,2004 Sun Microsystems or +Copyright © 2003-2011 David Schultz or +Copyright © 2003-2009 Steven G. Kargl or +Copyright © 2003-2009 Bruce D. Evans or +Copyright © 2008 Stephen L. Moshier or +Copyright © 2017-2018 Arm Limited +and labelled as such in comments in the individual source files. All +have been licensed under extremely permissive terms. + +The ARM memcpy code (src/string/arm/memcpy.S) is Copyright © 2008 +The Android Open Source Project and is licensed under a two-clause BSD +license. It was taken from Bionic libc, used on Android. + +The AArch64 memcpy and memset code (src/string/aarch64/*) are +Copyright © 1999-2019, Arm Limited. + +The implementation of DES for crypt (src/crypt/crypt_des.c) is +Copyright © 1994 David Burren. It is licensed under a BSD license. + +The implementation of blowfish crypt (src/crypt/crypt_blowfish.c) was +originally written by Solar Designer and placed into the public +domain. The code also comes with a fallback permissive license for use +in jurisdictions that may not recognize the public domain. + +The smoothsort implementation (src/stdlib/qsort.c) is Copyright © 2011 +Valentin Ochs and is licensed under an MIT-style license. + +The x86_64 port was written by Nicholas J. Kain and is licensed under +the standard MIT terms. + +The mips and microblaze ports were originally written by Richard +Pennington for use in the ellcc project. The original code was adapted +by Rich Felker for build system and code conventions during upstream +integration. It is licensed under the standard MIT terms. + +The mips64 port was contributed by Imagination Technologies and is +licensed under the standard MIT terms. + +The powerpc port was also originally written by Richard Pennington, +and later supplemented and integrated by John Spencer. It is licensed +under the standard MIT terms. + +All other files which have no copyright comments are original works +produced specifically for use as part of this library, written either +by Rich Felker, the main author of the library, or by one or more +contibutors listed above. Details on authorship of individual files +can be found in the git version control history of the project. The +omission of copyright and license comments in each file is in the +interest of source tree size. + +In addition, permission is hereby granted for all public header files +(include/* and arch/*/bits/*) and crt files intended to be linked into +applications (crt/*, ldso/dlstart.c, and arch/*/crt_arch.h) to omit +the copyright notice and permission notice otherwise required by the +license, and to use these files without any requirement of +attribution. These files include substantial contributions from: + +Bobby Bingham +John Spencer +Nicholas J. Kain +Rich Felker +Richard Pennington +Stefan Kristiansson +Szabolcs Nagy + +all of whom have explicitly granted such permission. + +This file previously contained text expressing a belief that most of +the files covered by the above exception were sufficiently trivial not +to be subject to copyright, resulting in confusion over whether it +negated the permissions granted in the license. In the spirit of +permissive licensing, and of not having licensing issues being an +obstacle to adoption, that text has been removed. diff --git a/crates/lua/vendor/musl-setjmp/aarch64/longjmp.s b/crates/lua/vendor/musl-setjmp/aarch64/longjmp.s new file mode 100644 index 0000000..0af9c50 --- /dev/null +++ b/crates/lua/vendor/musl-setjmp/aarch64/longjmp.s @@ -0,0 +1,23 @@ +.global _longjmp +.global longjmp +.type _longjmp,%function +.type longjmp,%function +_longjmp: +longjmp: + // IHI0055B_aapcs64.pdf 5.1.1, 5.1.2 callee saved registers + ldp x19, x20, [x0,#0] + ldp x21, x22, [x0,#16] + ldp x23, x24, [x0,#32] + ldp x25, x26, [x0,#48] + ldp x27, x28, [x0,#64] + ldp x29, x30, [x0,#80] + ldr x2, [x0,#104] + mov sp, x2 + ldp d8 , d9, [x0,#112] + ldp d10, d11, [x0,#128] + ldp d12, d13, [x0,#144] + ldp d14, d15, [x0,#160] + + cmp w1, 0 + csinc w0, w1, wzr, ne + br x30 diff --git a/crates/lua/vendor/musl-setjmp/aarch64/setjmp.s b/crates/lua/vendor/musl-setjmp/aarch64/setjmp.s new file mode 100644 index 0000000..f49288a --- /dev/null +++ b/crates/lua/vendor/musl-setjmp/aarch64/setjmp.s @@ -0,0 +1,24 @@ +.global __setjmp +.global _setjmp +.global setjmp +.type __setjmp,@function +.type _setjmp,@function +.type setjmp,@function +__setjmp: +_setjmp: +setjmp: + // IHI0055B_aapcs64.pdf 5.1.1, 5.1.2 callee saved registers + stp x19, x20, [x0,#0] + stp x21, x22, [x0,#16] + stp x23, x24, [x0,#32] + stp x25, x26, [x0,#48] + stp x27, x28, [x0,#64] + stp x29, x30, [x0,#80] + mov x2, sp + str x2, [x0,#104] + stp d8, d9, [x0,#112] + stp d10, d11, [x0,#128] + stp d12, d13, [x0,#144] + stp d14, d15, [x0,#160] + mov x0, #0 + ret diff --git a/crates/lua/vendor/musl-setjmp/riscv64/longjmp.S b/crates/lua/vendor/musl-setjmp/riscv64/longjmp.S new file mode 100644 index 0000000..41e2d21 --- /dev/null +++ b/crates/lua/vendor/musl-setjmp/riscv64/longjmp.S @@ -0,0 +1,42 @@ +.global __longjmp +.global _longjmp +.global longjmp +.type __longjmp, %function +.type _longjmp, %function +.type longjmp, %function +__longjmp: +_longjmp: +longjmp: + ld s0, 0(a0) + ld s1, 8(a0) + ld s2, 16(a0) + ld s3, 24(a0) + ld s4, 32(a0) + ld s5, 40(a0) + ld s6, 48(a0) + ld s7, 56(a0) + ld s8, 64(a0) + ld s9, 72(a0) + ld s10, 80(a0) + ld s11, 88(a0) + ld sp, 96(a0) + ld ra, 104(a0) + +#ifndef __riscv_float_abi_soft + fld fs0, 112(a0) + fld fs1, 120(a0) + fld fs2, 128(a0) + fld fs3, 136(a0) + fld fs4, 144(a0) + fld fs5, 152(a0) + fld fs6, 160(a0) + fld fs7, 168(a0) + fld fs8, 176(a0) + fld fs9, 184(a0) + fld fs10, 192(a0) + fld fs11, 200(a0) +#endif + + seqz a0, a1 + add a0, a0, a1 + ret diff --git a/crates/lua/vendor/musl-setjmp/riscv64/setjmp.S b/crates/lua/vendor/musl-setjmp/riscv64/setjmp.S new file mode 100644 index 0000000..5124967 --- /dev/null +++ b/crates/lua/vendor/musl-setjmp/riscv64/setjmp.S @@ -0,0 +1,41 @@ +.global __setjmp +.global _setjmp +.global setjmp +.type __setjmp, %function +.type _setjmp, %function +.type setjmp, %function +__setjmp: +_setjmp: +setjmp: + sd s0, 0(a0) + sd s1, 8(a0) + sd s2, 16(a0) + sd s3, 24(a0) + sd s4, 32(a0) + sd s5, 40(a0) + sd s6, 48(a0) + sd s7, 56(a0) + sd s8, 64(a0) + sd s9, 72(a0) + sd s10, 80(a0) + sd s11, 88(a0) + sd sp, 96(a0) + sd ra, 104(a0) + +#ifndef __riscv_float_abi_soft + fsd fs0, 112(a0) + fsd fs1, 120(a0) + fsd fs2, 128(a0) + fsd fs3, 136(a0) + fsd fs4, 144(a0) + fsd fs5, 152(a0) + fsd fs6, 160(a0) + fsd fs7, 168(a0) + fsd fs8, 176(a0) + fsd fs9, 184(a0) + fsd fs10, 192(a0) + fsd fs11, 200(a0) +#endif + + li a0, 0 + ret diff --git a/crates/lua/vendor/musl-setjmp/x86_64/longjmp.s b/crates/lua/vendor/musl-setjmp/x86_64/longjmp.s new file mode 100644 index 0000000..1b2661c --- /dev/null +++ b/crates/lua/vendor/musl-setjmp/x86_64/longjmp.s @@ -0,0 +1,18 @@ +/* Copyright 2011-2012 Nicholas J. Kain, licensed under standard MIT license */ +.global _longjmp +.global longjmp +.type _longjmp,@function +.type longjmp,@function +_longjmp: +longjmp: + xor %eax,%eax + cmp $1,%esi /* CF = val ? 0 : 1 */ + adc %esi,%eax /* eax = val + !val */ + mov (%rdi),%rbx /* rdi is the jmp_buf, restore regs from it */ + mov 8(%rdi),%rbp + mov 16(%rdi),%r12 + mov 24(%rdi),%r13 + mov 32(%rdi),%r14 + mov 40(%rdi),%r15 + mov 48(%rdi),%rsp + jmp *56(%rdi) /* goto saved address without altering rsp */ diff --git a/crates/lua/vendor/musl-setjmp/x86_64/setjmp.s b/crates/lua/vendor/musl-setjmp/x86_64/setjmp.s new file mode 100644 index 0000000..d95e485 --- /dev/null +++ b/crates/lua/vendor/musl-setjmp/x86_64/setjmp.s @@ -0,0 +1,22 @@ +/* Copyright 2011-2012 Nicholas J. Kain, licensed under standard MIT license */ +.global __setjmp +.global _setjmp +.global setjmp +.type __setjmp,@function +.type _setjmp,@function +.type setjmp,@function +__setjmp: +_setjmp: +setjmp: + mov %rbx,(%rdi) /* rdi is jmp_buf, move registers onto it */ + mov %rbp,8(%rdi) + mov %r12,16(%rdi) + mov %r13,24(%rdi) + mov %r14,32(%rdi) + mov %r15,40(%rdi) + lea 8(%rsp),%rdx /* this is our rsp WITHOUT current ret addr */ + mov %rdx,48(%rdi) + mov (%rsp),%rdx /* save return addr ptr for new rip */ + mov %rdx,56(%rdi) + xor %eax,%eax /* always return 0 */ + ret diff --git a/native/Cargo.toml b/native/Cargo.toml index 858b6b2..308239f 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -7,6 +7,8 @@ members = [ "crates/dagfs", "crates/driver", "crates/hint", + "crates/late", + "crates/ld", "crates/ring", "crates/rt", "crates/rt-macros", diff --git a/native/crates/late/Cargo.toml b/native/crates/late/Cargo.toml new file mode 100644 index 0000000..294cfd2 --- /dev/null +++ b/native/crates/late/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "kore-late" +version = "0.1.0" +edition = "2024" +publish = false +description = "Late binding: link a DagFS store object into the running task through the JIT arena, and look its symbols up — the `require` bridge (res/IMAGE.md §8)" + +[dependencies] +heapless = { version = "0.9.3", default-features = false } +kore-abi = { path = "../abi" } +kore-dagfs = { path = "../dagfs", default-features = false, features = ["runtime"] } +kore-ld = { path = "../ld", default-features = false } +kore-rt = { path = "../rt", default-features = false } + +[dev-dependencies] +kore-rt = { path = "../rt", features = ["std"] } +zerocopy = { version = "0.8", default-features = false } diff --git a/native/crates/late/src/lib.rs b/native/crates/late/src/lib.rs new file mode 100644 index 0000000..e78400f --- /dev/null +++ b/native/crates/late/src/lib.rs @@ -0,0 +1,350 @@ +//! Late binding — `res/IMAGE.md` §8's "separate library consuming the +//! passive record, never a resurrection of the stub". +//! +//! A running task links a shared object out of the DagFS store into its +//! **own** address space and calls into it. This is the `require` bridge +//! for an interpreter (`res/DAGFS.md` §4) and the seam any C engine's late +//! loading rides. No new kernel authority is involved: the bytes come +//! through an immutable version lease the task's filesystem view already +//! grants; the executable authority is the task's own writable→executable +//! finalization (§6); the placement is a window the task owns. +//! +//! **The gate.** A native load refuses unless the store object carries +//! [`ObjectSubtype::EXECUTABLE_IMAGE`] (the stamp exec's seeder writes) +//! *and* its bytes pass [`check_image`]. The stamp is a fact the store +//! carries, never authority — a plain file that happens to parse as an ELF +//! is still refused, and the subtype still grants nothing. +//! +//! **The search list is a capability list.** `require "x"` walks an +//! ordered set of path prefixes the task can already open — its own tree +//! (`""`) and the `libs` view — and "not found" is "no route". Source +//! modules are `.lua`, native ones `.so`. +//! +//! **Placement.** Modules go above everything the startup stub mapped +//! ([`ModulePage::high_water`]) inside `geom::LINK`, each at a 64 KiB +//! boundary, below the passive page itself. A module's text+rodata run +//! becomes one arena generation published whole (`initialize_executable_at` +//! + `map_rx_at`), its data union one exact writable mapping at its linked +//! offset (`kore_ld::late::LateLayout`), both carved off the top of the +//! task's heap supply (`kore_rt::heap::reserve_from_top`). Modules are +//! never unloaded. RELRO is sealed read-only after relocation and before +//! initializers run; a module with TLS, or a host with TLS, refuses. +//! +//! What is host-testable — names, paths, placement arithmetic, the host +//! module view over a descriptor and a page — lives beside the metal half +//! and is tested here; the arena and mapping half is proven by the boot +//! witness. + +#![no_std] + +extern crate alloc; + +use kore_abi::image::{ModuleDescriptor, ModulePage, seg}; +use kore_dagfs::{FsError, Name, Refusal, bounds::MAX_NAME_LEN, bounds::MAX_PATH_DEPTH}; +use kore_ld::apply::ApplyError; +use kore_ld::check::CheckError; +use kore_ld::late::LateError; + +/// Natively loaded modules one task may hold. Each costs one arena +/// lease, one data lease, and a slot here; eight covers an interpreter's +/// realistic native set and keeps the table on the stack. +pub const MAX_LOADED_MODULES: usize = 8; +/// The largest object the loader stages: it is read whole into a heap +/// buffer before `check_image` (which needs the headers and every table), +/// then copied into its arena, so three copies of it must fit beside the +/// engine in the launch heap (2 MiB, `LAYOUT.heap_bytes`). +pub const MAX_MODULE_BYTES: usize = 256 * 1024; +/// Prefixes one search may name. +pub const MAX_SEARCH_PREFIXES: usize = 2; +/// Every late module starts on this boundary: page-multiple, and coarse +/// enough that a module's placement reads on a serial photo. +pub const LATE_BASE_ALIGN: u64 = 0x1_0000; +/// One page. +const PAGE: u64 = 4096; +/// A composed search path: `/.`, bounded by the walk's +/// own limits (a prefix is at most `MAX_PATH_DEPTH - 1` components of +/// `MAX_NAME_LEN`). +pub const MAX_SEARCH_PATH: usize = MAX_PATH_DEPTH * (MAX_NAME_LEN + 1); + +/// What a module is loaded as. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ModuleKind { + /// Bytes for the interpreter to compile (`.lua`). + Source, + /// An ET_DYN linked into the task (`.so`). + Native, +} + +impl ModuleKind { + /// The object-name suffix. + pub const fn suffix(self) -> &'static [u8] { + match self { + Self::Source => b".lua", + Self::Native => b".so", + } + } +} + +/// Why a load did not happen. Every variant is a typed refusal; the +/// loader never spins, retries, or partially links. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LoadError { + /// The stem does not spell a store name once suffixed. + Name, + /// No prefix of the search list holds the object. + NotFound, + /// The object is not stamped `EXECUTABLE_IMAGE`. + NotExecutableImage, + /// The object exceeds [`MAX_MODULE_BYTES`], or the caller's buffer. + TooLarge, + /// The filesystem refused. + Fs(FsError), + /// The filesystem face answered out of contract. + Pair, + /// `check_image` refused the bytes. + Check(CheckError), + /// The checked image cannot be late-linked. + Plan(LateError), + /// The host image carries TLS: a late scope cannot reproduce the + /// thread layout the stub built. + HostTls, + /// The host cannot be described for a late scope; the payload names + /// what was missing: 1 no `.kore.ld` block, 2 no valid passive module + /// page at the fixed address, 3 the page's first record is not this + /// executable, 4 no `LINK` window in the geometry, 5 the page address + /// overflowed, 6 the page has no live segment (no high-water mark), + /// 7 a host segment could not be viewed. + NoRecord(u8), + /// The LINK window has no room above the last placement. + NoWindow, + /// The heap supply could not carve the arena or data lease. + NoSupply, + /// A mapping (writable, executable) was refused. + Map, + /// The code could not be published to instruction fetch. + Publish, + /// The relocated module's read-only-after-relocation protection refused. + Seal, + /// Relocation refused. + Link(ApplyError), + /// An initializer array is outside the writable mapping or malformed. + InitializerArray, + /// An initializer does not name executable text in the loaded scope. + Initializer(kore_ld::functions::FunctionError), + /// Initializers exceed the shared startup/late structural ceiling. + TooManyInitializers, + /// [`MAX_LOADED_MODULES`] reached. + TooMany, +} + +impl From for LoadError { + fn from(refusal: Refusal) -> Self { + match refusal { + Refusal::Fs(FsError::NotFound) => Self::NotFound, + Refusal::Fs(error) => Self::Fs(error), + _ => Self::Pair, + } + } +} + +/// The ordered prefixes a load may resolve under — each one a directory +/// the task can already open; `b""` is its own tree. +#[derive(Clone, Copy, Debug)] +pub struct Search<'a> { + pub prefixes: &'a [&'a [u8]], +} + +/// `` as a store name, or `None` past [`MAX_NAME_LEN`] or +/// for a stem that is not a name (empty, `/`, `.`, `..`, NUL). +pub fn object_name(kind: ModuleKind, stem: &[u8]) -> Option { + let suffix = kind.suffix(); + let mut buf = [0u8; MAX_NAME_LEN]; + let len = stem.len().checked_add(suffix.len())?; + if stem.is_empty() || len > MAX_NAME_LEN { + return None; + } + buf[..stem.len()].copy_from_slice(stem); + buf[stem.len()..len].copy_from_slice(suffix); + Name::from_bytes(&buf[..len]).ok() +} + +/// `/` into `out` (`` alone for an empty prefix). +pub fn compose_path<'b>( + out: &'b mut [u8; MAX_SEARCH_PATH], + prefix: &[u8], + name: &Name, +) -> Option<&'b [u8]> { + let name = name.as_bytes(); + let mut len = 0usize; + if !prefix.is_empty() { + if prefix.len() + 1 > out.len() { + return None; + } + out[..prefix.len()].copy_from_slice(prefix); + out[prefix.len()] = b'/'; + len = prefix.len() + 1; + } + let end = len.checked_add(name.len())?; + if end > out.len() { + return None; + } + out[len..end].copy_from_slice(name); + Some(&out[..end]) +} + + +/// The host image as the late scope's module 0: its base from the passive +/// page's first record (verified against the executable's own name), its +/// descriptor from `.kore.ld`. +#[derive(Clone, Copy, Debug)] +pub struct HostModule<'d> { + pub base: u64, + pub descriptor: &'d ModuleDescriptor, +} + +impl<'d> HostModule<'d> { + /// Resolve the host from the page and the executable's descriptor and + /// name hash; `None` when record 0 is not the executable. + pub fn from_page( + page: &ModulePage, + descriptor: &'d ModuleDescriptor, + exe_name_hash: u64, + ) -> Option { + let record = page.records.first()?; + if page.is_empty() || record.name_hash.get() != exe_name_hash { + return None; + } + Some(Self { + base: record.base.get(), + descriptor, + }) + } +} + +/// Build a `ModuleView`'s read half for a module mapped at `base`: one +/// slice per text/rodata segment, `filesz` bytes at the segment's +/// address. `slice_at(va, len)` is the caller's — a raw mapping on +/// metal, an owned buffer in tests — so this stays host-testable. +pub fn read_views<'a>( + descriptor: &ModuleDescriptor, + base: u64, + mut slice_at: impl FnMut(u64, u64) -> Option<&'a [u8]>, +) -> Option<[Option<&'a [u8]>; kore_abi::image::MAX_SEGMENTS]> { + let mut ro = [None; kore_abi::image::MAX_SEGMENTS]; + for (i, s) in descriptor.live_segs().iter().enumerate() { + if s.kind != seg::KIND_TEXT && s.kind != seg::KIND_RODATA { + continue; + } + let va = base.checked_add(s.vaddr.get())?; + ro[i] = Some(slice_at(va, s.filesz.get())?); + } + Some(ro) +} + +/// Round `v` up to a page. +pub const fn page_up(v: u64) -> Option { + match v.checked_add(PAGE - 1) { + Some(x) => Some(x & !(PAGE - 1)), + None => None, + } +} + +#[cfg(target_os = "none")] +mod metal; +#[cfg(target_os = "none")] +pub use metal::{Loaded, Loader, ModuleId}; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn object_names_take_the_kinds_suffix_and_stay_bounded() { + assert_eq!( + object_name(ModuleKind::Native, b"lua_demo") + .unwrap() + .as_bytes(), + b"lua_demo.so" + ); + assert_eq!( + object_name(ModuleKind::Source, b"mod").unwrap().as_bytes(), + b"mod.lua" + ); + assert!(object_name(ModuleKind::Source, b"").is_none()); + assert!(object_name(ModuleKind::Source, b"a/b").is_none()); + // `.so` is three bytes, `.lua` four: the stem may use the rest. + let exact_so = [b'x'; MAX_NAME_LEN - 3]; + assert!(object_name(ModuleKind::Native, &exact_so).is_some()); + assert!(object_name(ModuleKind::Source, &exact_so).is_none()); + let over = [b'x'; MAX_NAME_LEN - 2]; + assert!(object_name(ModuleKind::Native, &over).is_none()); + } + + #[test] + fn paths_compose_under_a_prefix_or_stand_alone() { + let name = object_name(ModuleKind::Native, b"demo").unwrap(); + let mut buf = [0u8; MAX_SEARCH_PATH]; + assert_eq!(compose_path(&mut buf, b"", &name), Some(&b"demo.so"[..])); + assert_eq!( + compose_path(&mut buf, b"libs", &name), + Some(&b"libs/demo.so"[..]) + ); + } + + /// The late loader's placement *policy* — modules land on + /// [`LATE_BASE_ALIGN`] and never reach the passive page. The + /// arithmetic itself belongs to `kore_ld::place` and is covered by its + /// own suite; this pins the boundary and the limit, which are ours. + #[test] + fn placement_rounds_to_the_late_boundary_and_stops_below_the_limit() { + use kore_ld::place::{PlaceError, Placer}; + + let limit = 0x5800_0000u64; + let mut from_unaligned = Placer::new(0x5001_0001, limit - 0x5001_0001); + assert_eq!(from_unaligned.choose(0x3000, LATE_BASE_ALIGN), Ok(0x5002_0000)); + + let mut from_aligned = Placer::new(0x5002_0000, limit - 0x5002_0000); + assert_eq!(from_aligned.choose(0x3000, LATE_BASE_ALIGN), Ok(0x5002_0000)); + + let mut exact = Placer::new(0x57ff_0000, limit - 0x57ff_0000); + assert_eq!(exact.choose(0x1_0000, LATE_BASE_ALIGN), Ok(0x57ff_0000)); + + let mut overshoot = Placer::new(0x57ff_0000, limit - 0x57ff_0000); + assert_eq!( + overshoot.choose(0x1_0001, LATE_BASE_ALIGN), + Err(PlaceError::Exhausted) + ); + + let mut ceiling = Placer::new(u64::MAX - 1, 1); + assert_eq!(ceiling.choose(1, LATE_BASE_ALIGN), Err(PlaceError::Exhausted)); + } + + #[test] + fn the_host_is_record_zero_only_when_it_names_the_executable() { + use kore_abi::image::{ImageName, ModuleRecord, ModuleSeg}; + use zerocopy::byteorder::little_endian::U64; + let exe = ImageName::new("lua").unwrap(); + let mut page = ModulePage::new(); + let mut record = ModuleRecord { + name_hash: U64::new(exe.fnv1a()), + base: U64::new(0x5000_0000), + entry: U64::new(0), + seg_count: 1, + kind: kore_abi::image::kind::EXECUTABLE, + reserved: [0; 6], + segs: [ModuleSeg::vacant(); kore_abi::image::MAX_SEGMENTS], + }; + record.segs[0].vaddr = U64::new(0x5000_1000); + record.segs[0].size = U64::new(0x3000); + assert!(page.push(record)); + let descriptor = ModuleDescriptor::empty(); + let host = HostModule::from_page(&page, &descriptor, exe.fnv1a()).unwrap(); + assert_eq!(host.base, 0x5000_0000); + assert!(HostModule::from_page(&page, &descriptor, 1).is_none()); + assert_eq!(page.high_water(), Some(0x5000_4000)); + assert!(HostModule::from_page(&ModulePage::new(), &descriptor, exe.fnv1a()).is_none()); + } +} + +/// Shared initializer-admission diagnostic for native loader consumers. +pub use kore_ld::functions::FunctionError; diff --git a/native/crates/late/src/metal.rs b/native/crates/late/src/metal.rs new file mode 100644 index 0000000..357dcfd --- /dev/null +++ b/native/crates/late/src/metal.rs @@ -0,0 +1,493 @@ +//! The metal half: read a version out of the store, carve the two +//! windows, publish, map, relocate, and remember the module. + +use alloc::boxed::Box; +use alloc::vec::Vec; +use core::mem::MaybeUninit; + +use kore_abi::image::{ImageName, ModuleDescriptor}; +use kore_abi::task::layout::Window; +use kore_dagfs::{Fs, FsRequest, FsResponse, Name, ObjectSubtype, Refusal, View, WALK_READ}; +use kore_ld::apply::{ + ModuleImage, ModuleView, SymbolScope, TlsLayout, apply_module, lookup_symbol, +}; +use kore_ld::check::check_image; +use kore_ld::late::LateLayout; +use kore_ld::reloc::HostArch; +use kore_ld::store::LinkArena; +use kore_rt::cap::verbs; +use kore_rt::cap::{ExecutableCode, ExecutableMapping, MappedLeaseView, StashedLease}; +use kore_rt::entry::Rt; + +use crate::{ + HostModule, LoadError, MAX_LOADED_MODULES, MAX_MODULE_BYTES, MAX_SEARCH_PATH, ModuleKind, + Search, compose_path, object_name, read_views, +}; + +/// Bytes the TLS layout's tables may carve for a scope of at most +/// `MAX_LOADED_MODULES + 1` TLS-free modules — a few words per module. +const LINK_ARENA_BYTES: usize = 1024; + +/// A loaded module's index in the loader. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ModuleId(u8); + +/// One linked module, alive for the task. +pub struct Loaded { + name: Name, + base: u64, + code_vaddr: u64, + descriptor: ModuleDescriptor, + /// The published generation. Leaked on purpose: a module is never + /// unloaded (v1), and the R|X mapping below borrows it for `'static`. + _code: &'static ExecutableCode, + mapping: ExecutableMapping<'static>, + data: Option, +} + +/// Owns the raw published allocation until the module enters the loader. +/// Its borrowed mappings must be declared later so rollback drops them first. +struct UnpublishedCode(*mut ExecutableCode); + +impl Drop for UnpublishedCode { + fn drop(&mut self) { + // SAFETY: link obtains this pointer from Box::into_raw exactly once. + // All borrowed mappings have already dropped; successful publication + // forgets this guard and leaves the allocation alive for the task. + unsafe { drop(Box::from_raw(self.0)) }; + } +} + +struct DataSpan { + _lease: StashedLease, + view: MappedLeaseView, + vaddr: u64, + bounds_len: u64, +} + +impl Loaded { + fn view(&self) -> Option> { + let code = self.mapping.as_ptr() as u64; + let code_len = self.mapping.len() as u64; + let ro = read_views(&self.descriptor, self.base, |va, len| { + let rel = va.checked_sub(self.base)?.checked_sub(self.code_vaddr)?; + if rel.checked_add(len)? > code_len { + return None; + } + let ptr = (code + rel) as *const u8; + // SAFETY: the R|X mapping covers `[code, code + code_len)` for + // the module's life; the range was bounds-checked above. + Some(unsafe { core::slice::from_raw_parts(ptr, len as usize) }) + })?; + Some(ModuleView { + base: self.base, + descriptor: &self.descriptor, + ro, + }) + } +} + +/// The task's late loader. +pub struct Loader { + host_base: u64, + host: &'static ModuleDescriptor, + /// Where the next module lands. One `kore_ld` placer over the link + /// window — the same type the startup stub places with, so module + /// addresses come from one implementation rather than a second copy + /// of the same rounding. + placer: kore_ld::place::Placer, + modules: heapless::Vec, +} + +impl Loader { + /// Read the passive page and the host descriptor; refuse a host with + /// TLS (its thread layout cannot be reproduced for a late scope). + pub fn new(rt: &Rt) -> Result { + let ld = rt.ld().ok_or(LoadError::NoRecord(1))?; + let page = rt.modules().ok_or(LoadError::NoRecord(2))?; + let host = HostModule::from_page(page, &ld.exe, ld.exe_name.fnv1a()) + .ok_or(LoadError::NoRecord(3))?; + if ld.exe.tls.memsz.get() != 0 { + return Err(LoadError::HostTls); + } + let (base, len) = rt + .geom() + .and_then(|g| g.window(kore_abi::task::geom::LINK)) + .ok_or(LoadError::NoRecord(4))?; + let limit = kore_abi::image::module_page_va(base, len).ok_or(LoadError::NoRecord(5))?; + // Above everything the stub placed — the records' segments AND the + // unrecorded range the placement high water covers (the copied + // template views, the static TLS block). The records alone put the + // first module on the next 64 KiB line above the image, which is + // inside that range once the image is large enough. + let next = page + .high_water() + .ok_or(LoadError::NoRecord(6))? + .max(page.placed_high_water(limit).unwrap_or(base)) + .max(base); + Ok(Self { + host_base: host.base, + host: host.descriptor, + placer: kore_ld::place::Placer::new(next, limit.saturating_sub(next)), + modules: heapless::Vec::new(), + }) + } + + /// A module already loaded under `stem`. + pub fn find(&self, stem: &[u8]) -> Option { + let name = object_name(ModuleKind::Native, stem)?; + self.modules + .iter() + .position(|m| m.name == name) + .map(|i| ModuleId(i as u8)) + } + + /// Read `.lua` through the search list into `buf`. + pub async fn read_source( + fs: &mut Fs<'_, '_>, + search: Search<'_>, + stem: &[u8], + buf: &mut [u8], + ) -> Result { + let name = object_name(ModuleKind::Source, stem).ok_or(LoadError::Name)?; + let view = open_in(fs, search, &name).await?; + let mut len = 0usize; + let result = loop { + let count = match fs.read_chunk(&view, len as u64).await { + Ok(0) => break Ok(len), + Ok(count) => count, + Err(error) => break Err(LoadError::from(error)), + }; + let end = match len.checked_add(count) { + Some(end) if end <= buf.len() => end, + _ => break Err(LoadError::TooLarge), + }; + buf[len..end].copy_from_slice(&fs.window()[..count]); + len = end; + }; + fs.close(view).await; + result + } + + /// Link `.so` from the search list into the task. + pub async fn load_native( + &mut self, + fs: &mut Fs<'_, '_>, + search: Search<'_>, + stem: &[u8], + ) -> Result { + if let Some(id) = self.find(stem) { + return Ok(id); + } + if self.modules.is_full() { + return Err(LoadError::TooMany); + } + let name = object_name(ModuleKind::Native, stem).ok_or(LoadError::Name)?; + let view = open_in(fs, search, &name).await?; + let bytes = match Self::stage(fs, &view).await { + Ok(bytes) => { + fs.close(view).await; + bytes + } + Err(error) => { + fs.close(view).await; + return Err(error); + } + }; + self.link(name, &bytes) + } + + /// Stat, lend, read the whole version, release. + async fn stage(fs: &mut Fs<'_, '_>, view: &View) -> Result, LoadError> { + let stat = fs.stat(view).await?; + if stat.subtype != ObjectSubtype::EXECUTABLE_IMAGE { + return Err(LoadError::NotExecutableImage); + } + let len = usize::try_from(stat.len).map_err(|_| LoadError::TooLarge)?; + if len > MAX_MODULE_BYTES { + return Err(LoadError::TooLarge); + } + let lease = match fs + .checked(&FsRequest::LendVersion { + handle: view.handle(), + window_offset: 0, + }) + .await? + { + FsResponse::VersionLent { lease } => lease, + _ => return Err(LoadError::Pair), + }; + let mut bytes = Vec::new(); + if bytes.try_reserve_exact(len).is_err() { + let _ = fs.checked(&FsRequest::ReleaseVersion { lease }).await; + return Err(LoadError::TooLarge); + } + let result = loop { + let remaining = len - bytes.len(); + if remaining == 0 { + break Ok(()); + } + let length = remaining.min(fs.chunk()) as u32; + match fs + .checked(&FsRequest::ReadVersion { + lease, + offset: bytes.len() as u64, + window_offset: 0, + length, + }) + .await + { + Ok(FsResponse::Transferred { count }) => { + let count = count as usize; + if count == 0 || count > length as usize { + break Err(LoadError::Pair); + } + bytes.extend_from_slice(&fs.window()[..count]); + } + Ok(_) => break Err(LoadError::Pair), + Err(error) => break Err(LoadError::from(error)), + } + }; + let _ = fs.checked(&FsRequest::ReleaseVersion { lease }).await; + result.map(|()| bytes) + } + + /// Check, plan, place, publish the code, map the data, relocate, run + /// the initializers, remember. + fn link(&mut self, name: Name, bytes: &[u8]) -> Result { + let checked = check_image::(bytes).map_err(LoadError::Check)?; + let plan = LateLayout::plan(&checked, MAX_MODULE_BYTES as u64, MAX_MODULE_BYTES as u64) + .map_err(LoadError::Plan)?; + // Placed on a copy: the window advances only when the module is + // remembered, so a refused load leaves its span for the next one. + let mut placer = self.placer; + let base = placer + .choose(plan.span, crate::LATE_BASE_ALIGN) + .map_err(|_| LoadError::NoWindow)?; + + // The code run: one arena generation, published whole. + let code_lease = + kore_rt::heap::reserve_from_top(plan.code_len).ok_or(LoadError::NoSupply)?; + let code_window = + Window::try_new(base + plan.code_vaddr, plan.code_len).ok_or(LoadError::NoWindow)?; + let copies = plan.code_copies; + let (code, ()) = code_lease + .initialize_executable_at(code_window, |w| { + w.fill(0); + for copy in copies.iter().flatten() { + let start = copy.file_off as usize; + let end = start.saturating_add(copy.len as usize).min(bytes.len()); + if start >= end { + continue; + } + w.write_at(copy.dst as usize, &bytes[start..end])?; + } + Ok::<(), kore_rt::cap::CodeWriteError>(()) + }) + .map_err(|error| match error { + kore_rt::cap::InitializeExecutableError::Initialize(_) => LoadError::Map, + kore_rt::cap::InitializeExecutableError::Map(_) => LoadError::Map, + _ => LoadError::Publish, + })?; + let rollback = UnpublishedCode(Box::into_raw(Box::new(code))); + // SAFETY: the allocation is stable. On refusal, all borrows below + // die before rollback; on success the module retains it for task life. + let code: &'static ExecutableCode = unsafe { &*rollback.0 }; + let mapping = code.map_rx_at(code_window).map_err(|_| LoadError::Map)?; + + // The data union: one exact writable mapping at its linked offset. + let data = match plan.data { + None => None, + Some(span) => { + let lease = kore_rt::heap::reserve_from_top(span.len).ok_or(LoadError::NoSupply)?; + let slot = lease.slot().ok_or(LoadError::NoSupply)?; + let view = verbs::map_owned_lease_at_rw(slot, base + span.vaddr, span.len) + .ok_or(LoadError::Map)?; + for copy in span.copies.iter().flatten() { + let start = copy.file_off as usize; + let end = start.saturating_add(copy.len as usize).min(bytes.len()); + if start >= end { + continue; + } + // SAFETY: the mapping is `span.len` writable bytes the + // plan bounded every copy inside; the supply pages are + // fresh (never handed out before), so bss is zero. + unsafe { + core::ptr::copy_nonoverlapping( + bytes[start..end].as_ptr(), + view.as_mut_ptr().add(copy.dst as usize), + end - start, + ); + } + } + Some(DataSpan { + _lease: lease, + view, + vaddr: span.vaddr, + bounds_len: span.bounds_len, + }) + } + }; + + let mut loaded = Loaded { + name, + base, + code_vaddr: plan.code_vaddr, + descriptor: checked.descriptor, + _code: code, + mapping, + data, + }; + self.relocate(&mut loaded, checked.relro)?; + if let Some((off, len)) = checked.relro { + let span = loaded.data.as_mut().ok_or(LoadError::Seal)?; + let offset = off.checked_sub(span.vaddr).ok_or(LoadError::Seal)?; + if !span.view.seal_read_only(offset as usize, len as usize) { + return Err(LoadError::Seal); + } + } + self.run_initializers(&loaded)?; + self.modules.push(loaded).map_err(|_| LoadError::TooMany)?; + core::mem::forget(rollback); + self.placer = placer; + Ok(ModuleId((self.modules.len() - 1) as u8)) + } + + /// Resolve the module against `[host, ...loaded, this]`. + fn relocate(&self, module: &mut Loaded, relro: Option<(u64, u64)>) -> Result<(), LoadError> { + let host_view = ModuleView { + base: self.host_base, + descriptor: self.host, + ro: read_views(self.host, self.host_base, |va, len| { + // SAFETY: the host's text/rodata are mapped R (or R|X) at + // these addresses for the task's life (the stub's shares). + Some(unsafe { core::slice::from_raw_parts(va as *const u8, len as usize) }) + }) + .ok_or(LoadError::NoRecord(7))?, + }; + let mut views: heapless::Vec, { MAX_LOADED_MODULES + 2 }> = + heapless::Vec::new(); + let _ = views.push(host_view); + for m in &self.modules { + let _ = views.push(m.view().ok_or(LoadError::Map)?); + } + let this_view = module.view().ok_or(LoadError::Map)?; + let _ = views.push(this_view); + let scope_index = views.len() - 1; + let scope = SymbolScope { modules: &views }; + + let mut arena_bytes = [MaybeUninit::::uninit(); LINK_ARENA_BYTES]; + let arena = LinkArena::new(&mut arena_bytes); + let tls = TlsLayout::build::(&views, &arena).map_err(|_| LoadError::HostTls)?; + + let mut empty = [0u8; 0]; + let data: &mut [u8] = match module.data.as_ref() { + Some(span) => { + // SAFETY: the writable mapping spans `span.len >= bounds_len` + // bytes owned by this module; nothing else references it. + unsafe { + core::slice::from_raw_parts_mut( + span.view.as_mut_ptr(), + span.bounds_len as usize, + ) + } + } + None => &mut empty, + }; + let mut image = ModuleImage { + scope_index, + view: this_view, + data, + relro, + }; + apply_module::(&mut image, &scope, &tls).map_err(LoadError::Link)?; + Ok(()) + } + + /// Validate and snapshot the whole relocated array before calling any code. + fn run_initializers(&self, module: &Loaded) -> Result<(), LoadError> { + let off = module.descriptor.dynz.init_array_off.get(); + let sz = module.descriptor.dynz.init_array_sz.get(); + if sz == 0 { + return Ok(()); + } + let span = module.data.as_ref().ok_or(LoadError::InitializerArray)?; + let rel = off + .checked_sub(span.vaddr) + .ok_or(LoadError::InitializerArray)?; + let end = rel.checked_add(sz).ok_or(LoadError::InitializerArray)?; + if end > span.bounds_len { + return Err(LoadError::InitializerArray); + } + // SAFETY: the complete array is inside the owned data mapping. This + // borrow ends before any initializer may modify its source storage. + let bytes = unsafe { + core::slice::from_raw_parts(span.view.as_ptr().add(rel as usize), sz as usize) + }; + let addresses = kore_ld::functions::function_addresses(bytes, |address| { + kore_ld::functions::contains_text(self.host, self.host_base, address) + || self + .modules + .iter() + .chain(core::iter::once(module)) + .any(|loaded| { + kore_ld::functions::contains_text(&loaded.descriptor, loaded.base, address) + }) + }) + .map_err(LoadError::Initializer)?; + let mut snapshot: heapless::Vec = + heapless::Vec::new(); + for address in addresses { + snapshot + .push(address) + .map_err(|_| LoadError::TooManyInitializers)?; + } + for entry in snapshot { + // SAFETY: every non-sentinel target was checked against loaded + // executable text before the first call; ELF initializers use this + // ABI. The snapshot cannot be rewritten through the module array. + let f: unsafe extern "C" fn() = unsafe { core::mem::transmute(entry as usize) }; + unsafe { f() }; + } + Ok(()) + } + + /// An exported symbol's address in a loaded module. + pub fn lookup(&self, id: ModuleId, symbol: &[u8]) -> Option<*const u8> { + let module = self.modules.get(usize::from(id.0))?; + let view = module.view()?; + lookup_symbol(&view, symbol) + .ok() + .flatten() + .map(|va| va as *const u8) + } + + /// An exported entry inside this module's file-backed executable text. + /// This proves executable placement, not its signature or ABI; callers + /// still establish those before converting the pointer to a function. + pub fn lookup_executable(&self, id: ModuleId, symbol: &[u8]) -> Option<*const u8> { + let address = self.lookup(id, symbol)?; + let module = self.modules.get(usize::from(id.0))?; + kore_ld::functions::contains_text(&module.descriptor, module.base, address as u64) + .then_some(address) + } + + /// The image name this loader links against (diagnostics). + pub fn host_name(rt: &Rt) -> Option { + rt.ld().map(|ld| ld.exe_name) + } +} + +/// Open `name` under the first prefix that holds it. +async fn open_in(fs: &mut Fs<'_, '_>, search: Search<'_>, name: &Name) -> Result { + let mut buf = [0u8; MAX_SEARCH_PATH]; + for prefix in search.prefixes.iter().take(crate::MAX_SEARCH_PREFIXES) { + let path = compose_path(&mut buf, prefix, name).ok_or(LoadError::Name)?; + match fs.open(path, WALK_READ).await { + Ok(view) => return Ok(view), + Err(Refusal::Fs(kore_dagfs::FsError::NotFound)) => continue, + Err(error) => return Err(LoadError::from(error)), + } + } + Err(LoadError::NotFound) +} diff --git a/native/crates/ld/Cargo.toml b/native/crates/ld/Cargo.toml new file mode 100644 index 0000000..02c8ad6 --- /dev/null +++ b/native/crates/ld/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "kore-ld" +version = "0.1.1" +edition = "2024" +publish = false + +[dependencies] +# The kore image/loader wire contract (res/IMAGE.md; #292). +kore-abi = { path = "../abi" } +# The one ELF parsing substrate (res/IMAGE.md §5): no_std, zero-alloc, +# zero-dep, zero-unsafe with default features off. Independently +# validated by kraph's klink and upstream elf_loader building on it. +elf = { version = "0.8.0", default-features = false } +heapless = { version = "0.9.2", default-features = false } +bitflags = { version = "2.13.0", default-features = false } +zerocopy = { version = "0.8", default-features = false, features = ["derive"] } + +[lib] +name = "kore_ld" +path = "src/lib.rs" +crate-type = ["rlib"] diff --git a/native/crates/ld/src/apply.rs b/native/crates/ld/src/apply.rs new file mode 100644 index 0000000..ec5751b --- /dev/null +++ b/native/crates/ld/src/apply.rs @@ -0,0 +1,1006 @@ +//! The eager relocation engine — the stub-side apply half +//! (`res/IMAGE.md` §5, §7), generic over [`RelocArch`]. +//! +//! ## Contract +//! +//! The caller (the startup stub) has already mapped a module's shares +//! and copied its data template; what it hands the engine is exactly the +//! authority the writes need — **its own writable segment slice** — +//! plus read views of the link inputs. There is no address arithmetic +//! the caller didn't authorize by constructing the views: +//! +//! - `ModuleImage<'a>` — one loaded module: chosen `base`, the +//! [`ModuleDescriptor`], read slices +//! for rodata-resident link inputs (symtab/strtab/gnu-hash/rela ranges +//! resolve inside them), and the **writable private data slice** (the +//! only write target; a reloc whose `r_offset` lands outside it +//! refuses — text/rodata are structurally unwritable here). +//! - `SymbolScope<'a>` — the resolution set: the module list in load +//! order (dependency post-order for init running). Lookup = +//! GNU-hash per module, strong-before-weak across the scope +//! (`-Bsymbolic-functions` has already bound most intra-module +//! references at link time). +//! - `TlsLayout` — the static-block builder's yield: per-module +//! `tp`-relative base offsets computed from +//! [`RelocArch::TLS`] (V1: positive, +//! after the TCB, alignment-rounded; V2: negative, blocks below `tp`), +//! plus the total block size/align the stub allocates per thread. +//! Built once for the whole closed load set (IE/LE legality). +//! +//! `apply_module::(module, scope, tls) -> Result` +//! walks `.rela.dyn`, `DT_RELR`, then `.rela.plt`, computing per +//! [`RelocKind`]: +//! +//! | kind | value | +//! |---|---| +//! | `Relative` | `base + addend` | +//! | `GlobDat`/`JumpSlot` | `S` | +//! | `Abs64` | `S + addend` | +//! | `TpRel` | `tls.tp_off(module, sym_off) + addend` | +//! +//! — all checked arithmetic (overflow refuses, never wraps), all writes +//! little-endian u64 through the data slice. After apply, the stub runs +//! `DT_INIT_ARRAY`s (post-order) and seals RELRO; both are stub verbs — +//! the engine only *reports* the RELRO window +//! ([`Applied::relro`]). +//! +//! Failure is total: one bad relocation refuses the module (no partial +//! GOT — a half-linked module is unrepresentable to the caller, the +//! bounded-session discipline applied to linking). + +use kore_abi::image::{MAX_SEGMENTS, ModuleDescriptor, seg}; +use zerocopy::FromBytes; +use zerocopy::byteorder::little_endian::{I64 as LeI64, U16 as LeU16, U32 as LeU32, U64 as LeU64}; + +use crate::reloc::{RelocArch, RelocKind, TlsRules}; + +/// Why apply refused. Diagnostics carry the relocation index — the +/// klink convention, kept. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ApplyError { + /// A relocation refused classification (already caught at `check`; + /// here for defense in depth). + Reloc(crate::reloc::RelocRefusal, usize), + /// `r_offset` (or a RELR target) lands outside the writable data + /// slice — the one memory-safety gate of the engine. + WriteOutOfRange(usize), + /// A needed symbol resolved nowhere in scope (strong or weak). + MissingSymbol(usize), + /// Checked arithmetic overflowed (a lying addend/base). + Overflow(usize), + /// A TpRel reloc against a module absent from the TLS layout. + Tls(usize), + /// Malformed link input (rela/symtab/strtab/hash range lies outside + /// the module's mapped views). + Input, +} + +/// Scope ceiling for **one load set** — a program plus the shared objects it +/// pulls in, not the image table as a whole. +/// +/// This used to say it mirrored the image service's total catalogue ceiling, +/// which was never true and drifted anyway; that catalogue is now +/// allocator-backed. The bound a load set must actually respect is its own +/// link, and every link is already held far below this by +/// `MAX_LINK_MODULES <= MAX_MODULE_RECORDS` (16) in `link.rs`. So this is +/// deliberately left at 32 while the image budget grows: it is a per-process +/// ceiling that scales with dependency depth, not with how many images the +/// machine ships, and raising it would change nothing observable. +pub const MAX_MODULES: usize = 32; + +// ELF64 symbol facts the resolver reads (numbers per the gABI). +const SHN_UNDEF: u16 = 0; +const SHN_ABS: u16 = 0xfff1; +const STB_GLOBAL: u8 = 1; +const STB_WEAK: u8 = 2; +const RELA_ENTRY_SIZE: usize = 24; +const SYM_ENTRY_SIZE: u64 = 24; + +/// One `Elf64_Rela`, read as frozen little-endian bytes (zerocopy — no +/// offset math against hostile input). +#[derive(FromBytes, zerocopy::KnownLayout, zerocopy::Immutable)] +#[repr(C)] +struct Rela64 { + r_offset: LeU64, + r_info: LeU64, + r_addend: LeI64, +} + +/// One `Elf64_Sym`, likewise. +#[derive(FromBytes, zerocopy::KnownLayout, zerocopy::Immutable)] +#[repr(C)] +struct Sym64 { + st_name: LeU32, + st_info: u8, + st_other: u8, + st_shndx: LeU16, + st_value: LeU64, + st_size: LeU64, +} + +// The wire shapes are ABI: a drifted layout would misread every image. +const _: () = { + use core::mem::size_of; + assert!(size_of::() == RELA_ENTRY_SIZE); + assert!(size_of::() == SYM_ENTRY_SIZE as usize); +}; + +/// One loaded module's *read* half: the chosen base, the descriptor it +/// was acquired with, and read views of its mapped segments. `ro` +/// parallels `descriptor.segs`; only [`seg::KIND_TEXT`] / +/// [`seg::KIND_RODATA`] entries are ever consulted (link inputs are +/// rodata-resident by the §7 layout — an input pointing into the data +/// segment refuses [`ApplyError::Input`]). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ModuleView<'a> { + /// The absolute load base this task chose for the module. + pub base: u64, + /// The acquired descriptor (image-relative geometry). + pub descriptor: &'a ModuleDescriptor, + /// Mapped read views, parallel to `descriptor.segs`. A view may be + /// the exact `filesz` bytes or the page-rounded mapping; reads are + /// bounded by the smaller of the two. + pub ro: [Option<&'a [u8]>; MAX_SEGMENTS], +} + +/// The resolution set: every module of the closed load set, in load +/// order (dependency post-order). Lookup walks GNU hash per module in +/// this order — first strong definition wins; failing that, the first +/// weak one ([`apply_module`]'s contract). +#[derive(Clone, Copy, Debug)] +pub struct SymbolScope<'a> { + /// The load set. Index positions here are the module indices the + /// TLS layout and `ModuleImage::scope_index` speak. + pub modules: &'a [ModuleView<'a>], +} + +/// The module being applied: its read half plus the one write authority +/// the engine accepts — the caller's own private data copy. +#[derive(Debug)] +pub struct ModuleImage<'a> { + /// This module's index in the [`SymbolScope`] (and so in the + /// [`TlsLayout`]) — the self-reference TLS relocations with symbol + /// index 0 resolve against. + pub scope_index: usize, + /// The read half (same content as the scope's entry for this module). + pub view: ModuleView<'a>, + /// The writable private data region: one union containing every + /// [`seg::KIND_DATA`] template at its declared offset, with bss and + /// inter-segment gaps zeroed. Relocations are still range-checked + /// against an individual declared data segment; this slice is the + /// only write authority. + pub data: &'a mut [u8], + /// The `PT_GNU_RELRO` window, image-relative `(offset, len)` — from + /// [`crate::check::CheckedImage::relro`]; it does not ride the wire + /// descriptor. `None` = nothing to seal. + pub relro: Option<(u64, u64)>, +} + +/// What [`apply_module`] did. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Applied { + /// Relocation writes performed (rela.dyn + RELR targets + rela.plt). + pub applied: usize, + /// The RELRO window to seal, **absolute** `(start, len)` (the + /// module's chosen base applied) — the stub's `unmap + re-map RO` + /// range. `None` when the image declared no `PT_GNU_RELRO`. + pub relro: Option<(u64, u64)>, +} + +/// A resolved definition: which module defines the symbol and the raw +/// `st_value`/`st_shndx` (absolute address derivation differs between +/// address and TLS uses, so both facts travel). +#[derive(Clone, Copy, Debug)] +struct Resolved { + module: usize, + st_value: u64, + st_shndx: u16, +} + +/// What a scope lookup yielded ([`resolve_for`]): a definition, or the +/// gABI's weak-undefined outcome — a **weak** reference that stayed +/// undefined across the whole scope resolves to absolute zero instead of +/// refusing (address semantics only; a TLS use of it still refuses — +/// there is no block for offset zero to mean). +#[derive(Clone, Copy, Debug)] +enum Resolution { + Def(Resolved), + WeakUndef, +} + +/// The static-TLS layout of the closed load set (`res/IMAGE.md` §7): +/// per-module `tp`-relative block bases plus the total block the stub +/// allocates per thread. Built once, before any module applies — IE/LE +/// legality is exactly "the whole set is known first". +/// +/// Capacity is the **caller's** (Phase A, `res/USERLAND.md` §7): the +/// per-module tables are allocated from `Alloc` — arena-backed in the +/// stub — sized *exactly* to the load set, never to the [`MAX_MODULES`] +/// ceiling (which remains the semantic bound the build refuses past). +#[derive(Debug)] +pub struct TlsLayout { + /// Per-module block base, `tp`-relative (V1 positive, V2 negative); + /// `None` = module has no TLS. + bases: crate::store::Slots, Alloc>, + /// Per-module block size (bounds `tp_off`'s `sym_off`). + sizes: crate::store::Slots, + /// Modules considered (the scope length at build). + len: usize, + /// Total static block size: V1 = bytes above `tp` (TCB included); + /// V2 = bytes below `tp`. + pub total: u64, + /// The strictest block alignment (≥ 1); `tp` itself must satisfy it. + pub align: u64, +} + +impl TlsLayout { + /// Lay out static TLS for the load set, in scope order, with the + /// tables carved from `alloc` (exact-size — the load set's length). + /// + /// V1 (aarch64 `tcb = 16`, riscv64 `tcb = 0`): blocks grow upward + /// from `tp`; the first base is `align_up(tcb, align₁)`, each next + /// `align_up(prev_end, alignᵢ)`; offsets are positive. + /// + /// V2 (x86_64): blocks grow downward; `off₁ = align_up(size₁, + /// align₁)`, `offᵢ = align_up(offᵢ₋₁ + sizeᵢ, alignᵢ)`, block `i` + /// based at `tp − offᵢ`; offsets are negative. + /// + /// All arithmetic checked — a lying `TlsDesc` refuses, never wraps; + /// an exhausted arena refuses [`ApplyError::Input`], never panics. + pub fn build( + modules: &[ModuleView<'_>], + alloc: Alloc, + ) -> Result { + if modules.len() > MAX_MODULES { + return Err(ApplyError::Input); + } + let mut layout = TlsLayout { + bases: crate::store::Slots::new_from(modules.len(), alloc, |_| None) + .map_err(|_| ApplyError::Input)?, + sizes: crate::store::Slots::new_from(modules.len(), alloc, |_| 0) + .map_err(|_| ApplyError::Input)?, + len: modules.len(), + total: 0, + align: 1, + }; + // V1: cursor = end of the last placed block (starts past the header + // reserved at `tp`). That header is the linker's to size, because + // the linker resolves every TPREL offset that reads it, so it is + // widened to hold the runtime's per-thread block + // ([`RUNTIME_TCB_BYTES`]) — the psABI minimum is a floor, not a + // ceiling, and a static block placed above it is unaffected. + // V2: cursor = distance of the deepest block below `tp`; there the + // runtime block extends the TCB *upward*, away from the blocks, so + // it does not enter this layout at all. + let mut cursor: u64 = match A::TLS { + TlsRules::V1 { tcb } => tcb.max(kore_abi::image::RUNTIME_TCB_BYTES), + TlsRules::V2 => 0, + }; + for (i, m) in modules.iter().enumerate() { + let size = m.descriptor.tls.memsz.get(); + if size == 0 { + continue; + } + let align = normalize_align(m.descriptor.tls.align.get())?; + let slot = layout + .bases + .as_mut_slice() + .get_mut(i) + .ok_or(ApplyError::Input)?; + match A::TLS { + TlsRules::V1 { .. } => { + let base = align_up(cursor, align).ok_or(ApplyError::Input)?; + *slot = Some(i64::try_from(base).map_err(|_| ApplyError::Input)?); + cursor = base.checked_add(size).ok_or(ApplyError::Input)?; + } + TlsRules::V2 => { + let off = cursor + .checked_add(size) + .and_then(|v| align_up(v, align)) + .ok_or(ApplyError::Input)?; + let off_i = i64::try_from(off).map_err(|_| ApplyError::Input)?; + *slot = Some(off_i.checked_neg().ok_or(ApplyError::Input)?); + cursor = off; + } + } + *layout + .sizes + .as_mut_slice() + .get_mut(i) + .ok_or(ApplyError::Input)? = size; + layout.align = layout.align.max(align); + } + layout.total = cursor; + Ok(layout) + } + + /// The `tp`-relative offset of `sym_off` inside `module`'s block; + /// `None` when the module has no block or the offset escapes it — + /// the caller maps that onto [`ApplyError::Tls`]. + pub fn tp_off(&self, module: usize, sym_off: u64) -> Option { + if module >= self.len { + return None; + } + let base = (*self.bases.as_slice().get(module)?)?; + let size = *self.sizes.as_slice().get(module)?; + // A symbol offset beyond the block is a lie about the template. + if sym_off > size { + return None; + } + base.checked_add(i64::try_from(sym_off).ok()?) + } +} + +/// Eagerly relocate one module (the module contract): `.rela.dyn`, then +/// `DT_RELR`, then `.rela.plt`, total-failure semantics. The +/// relocation index in errors counts rela entries and RELR words in +/// walk order. +pub fn apply_module( + module: &mut ModuleImage<'_>, + scope: &SymbolScope<'_>, + tls: &TlsLayout, +) -> Result { + let d = module.view.descriptor; + // Defense in depth: a corrupt or foreign descriptor refuses before + // any byte moves (the service validated content; this is the cheap + // consumer re-check). + if !d.validate() || d.machine.get() != A::MACHINE { + return Err(ApplyError::Input); + } + + // The one write authority: one private region spanning every writable + // LOAD, while target validation below remains per declared segment. + let data_bounds = data_bounds(d)?; + match data_bounds { + Some((_, len)) => { + if u64::try_from(module.data.len()).ok() != Some(len) { + return Err(ApplyError::Input); + } + } + None => { + if !module.data.is_empty() { + return Err(ApplyError::Input); + } + } + } + + let view = &module.view; + let data = &mut *module.data; + let mut idx: usize = 0; + let mut applied: usize = 0; + + walk_rela::( + view, + data, + d, + scope, + tls, + module.scope_index, + d.dynz.rela_off.get(), + d.dynz.rela_sz.get(), + &mut idx, + &mut applied, + )?; + walk_relr( + view, + data, + d, + d.dynz.relr_off.get(), + d.dynz.relr_sz.get(), + &mut idx, + &mut applied, + )?; + walk_rela::( + view, + data, + d, + scope, + tls, + module.scope_index, + d.dynz.jmprel_off.get(), + d.dynz.jmprel_sz.get(), + &mut idx, + &mut applied, + )?; + + let relro = match module.relro { + None => None, + Some((off, len)) => Some((view.base.checked_add(off).ok_or(ApplyError::Input)?, len)), + }; + + Ok(Applied { applied, relro }) +} + +/// Walk one RELA table (`.rela.dyn` or `.rela.plt`). +#[allow(clippy::too_many_arguments)] +fn walk_rela( + view: &ModuleView<'_>, + data: &mut [u8], + descriptor: &ModuleDescriptor, + scope: &SymbolScope<'_>, + tls: &TlsLayout, + self_index: usize, + off: u64, + sz: u64, + idx: &mut usize, + applied: &mut usize, +) -> Result<(), ApplyError> { + if sz == 0 { + return Ok(()); + } + if sz % (RELA_ENTRY_SIZE as u64) != 0 { + return Err(ApplyError::Input); + } + let table = ro_slice(view, off, sz)?; + for entry in table.chunks_exact(RELA_ENTRY_SIZE) { + let rela = Rela64::read_from_bytes(entry).map_err(|_| ApplyError::Input)?; + let r_info = rela.r_info.get(); + // ELF64 r_info = sym << 32 | type; the mask/shift casts are exact. + let r_type = (r_info & 0xffff_ffff) as u32; + let r_sym = (r_info >> 32) as u32; + let addend = rela.r_addend.get(); + + // Defense in depth behind `check`: the classification gate runs + // again here, so a foreign image applied directly still refuses. + let kind = A::classify(r_type).map_err(|r| ApplyError::Reloc(r, *idx))?; + + let value: u64 = match kind { + RelocKind::Relative => view + .base + .checked_add_signed(addend) + .ok_or(ApplyError::Overflow(*idx))?, + // GOT/PLT slots take the symbol address, addend ignored + // (the module-doc table; identical under `-z now`). A weak + // reference nothing defines takes S = 0 (the gABI outcome). + RelocKind::GlobDat | RelocKind::JumpSlot => { + match resolve_for(scope, view, r_sym, *idx)? { + Resolution::Def(sym) => absolute(scope, &sym, *idx)?, + Resolution::WeakUndef => 0, + } + } + RelocKind::Abs64 => { + let s = match resolve_for(scope, view, r_sym, *idx)? { + Resolution::Def(sym) => absolute(scope, &sym, *idx)?, + Resolution::WeakUndef => 0, + }; + s.checked_add_signed(addend) + .ok_or(ApplyError::Overflow(*idx))? + } + RelocKind::TpRel => { + // Symbol 0 = a module-local TLS reference: the offset is + // the addend against this module's own block. A weak- + // undefined TLS symbol refuses — a zero tp-offset would + // alias a real block, not stand for "absent". + let (m, sym_off) = if r_sym == 0 { + (self_index, 0) + } else { + match resolve_for(scope, view, r_sym, *idx)? { + Resolution::Def(sym) => (sym.module, sym.st_value), + Resolution::WeakUndef => return Err(ApplyError::Tls(*idx)), + } + }; + let tp = tls.tp_off(m, sym_off).ok_or(ApplyError::Tls(*idx))?; + let v = tp.checked_add(addend).ok_or(ApplyError::Overflow(*idx))?; + // The slot stores the two's-complement image of the + // (possibly negative) tp offset — a bit cast, not math. + v as u64 + } + }; + + write_word(data, descriptor, rela.r_offset.get(), value, *idx)?; + *applied = applied.checked_add(1).ok_or(ApplyError::Input)?; + *idx = idx.checked_add(1).ok_or(ApplyError::Input)?; + } + Ok(()) +} + +/// Decode + apply one `DT_RELR` table: an even entry relocates its own +/// address and re-anchors the cursor; an odd entry is a 63-bit bitmap +/// over the words following the anchor. Targets take `base + in-place +/// value` (RELR keeps the addend in the slot). +fn walk_relr( + view: &ModuleView<'_>, + data: &mut [u8], + descriptor: &ModuleDescriptor, + off: u64, + sz: u64, + idx: &mut usize, + applied: &mut usize, +) -> Result<(), ApplyError> { + if sz == 0 { + return Ok(()); + } + if sz % 8 != 0 { + return Err(ApplyError::Input); + } + let table = ro_slice(view, off, sz)?; + let mut anchor: u64 = 0; + for word in table.chunks_exact(8) { + let entry = word + .try_into() + .map(u64::from_le_bytes) + .map_err(|_| ApplyError::Input)?; + if entry & 1 == 0 { + relr_apply(view.base, data, descriptor, entry, *idx)?; + *applied = applied.checked_add(1).ok_or(ApplyError::Input)?; + anchor = entry.checked_add(8).ok_or(ApplyError::Overflow(*idx))?; + } else { + let mut bits = entry >> 1; + let mut at = anchor; + while bits != 0 { + if bits & 1 == 1 { + relr_apply(view.base, data, descriptor, at, *idx)?; + *applied = applied.checked_add(1).ok_or(ApplyError::Input)?; + } + bits >>= 1; + at = at.checked_add(8).ok_or(ApplyError::Overflow(*idx))?; + } + anchor = anchor + .checked_add(63 * 8) + .ok_or(ApplyError::Overflow(*idx))?; + } + *idx = idx.checked_add(1).ok_or(ApplyError::Input)?; + } + Ok(()) +} + +/// One RELR target: read the in-place addend, add the base, write back. +fn relr_apply( + base: u64, + data: &mut [u8], + descriptor: &ModuleDescriptor, + target: u64, + idx: usize, +) -> Result<(), ApplyError> { + let cur = read_word(data, descriptor, target, idx)?; + let value = base.checked_add(cur).ok_or(ApplyError::Overflow(idx))?; + write_word(data, descriptor, target, value, idx) +} + +/// Bounding allocation geometry for all data templates. This span is only +/// allocation geometry; [`data_range`] separately requires each write to fit +/// one explicitly declared data segment. +/// The writable data union a module needs: image-relative `(start, len)` +/// spanning every [`seg::KIND_DATA`] segment (bss included), or `None` +/// for a module with no writable segment. The length +/// [`apply_module`] demands of [`ModuleImage::data`], and the span a late +/// loader (`crate::late`) maps writable beside the module's code. +pub fn data_bounds(d: &ModuleDescriptor) -> Result, ApplyError> { + let mut start: Option = None; + let mut end: u64 = 0; + for s in d.live_segs().iter().filter(|s| s.kind == seg::KIND_DATA) { + start = Some(start.map_or(s.vaddr.get(), |at| at.min(s.vaddr.get()))); + end = end.max( + s.vaddr + .get() + .checked_add(s.memsz.get()) + .ok_or(ApplyError::Input)?, + ); + } + match start { + Some(start) => Ok(Some(( + start, + end.checked_sub(start).ok_or(ApplyError::Input)?, + ))), + None => Ok(None), + } +} + +/// Translate an image-relative target into the writable data union — the +/// engine's one memory-safety gate. The complete word must fit one declared +/// data segment; a target in a gap, text, or rodata refuses. +fn data_range( + data_len: usize, + descriptor: &ModuleDescriptor, + target: u64, + idx: usize, +) -> Result<(usize, usize), ApplyError> { + let target_end = target + .checked_add(8) + .ok_or(ApplyError::WriteOutOfRange(idx))?; + let declared = descriptor + .live_segs() + .iter() + .filter(|s| s.kind == seg::KIND_DATA) + .any(|s| { + s.vaddr + .get() + .checked_add(s.memsz.get()) + .is_some_and(|end| target >= s.vaddr.get() && target_end <= end) + }); + if !declared { + return Err(ApplyError::WriteOutOfRange(idx)); + } + let (start, _) = data_bounds(descriptor) + .map_err(|_| ApplyError::WriteOutOfRange(idx))? + .ok_or(ApplyError::WriteOutOfRange(idx))?; + let rel = target + .checked_sub(start) + .ok_or(ApplyError::WriteOutOfRange(idx))?; + let end = rel.checked_add(8).ok_or(ApplyError::WriteOutOfRange(idx))?; + let rel = usize::try_from(rel).map_err(|_| ApplyError::WriteOutOfRange(idx))?; + let end = usize::try_from(end).map_err(|_| ApplyError::WriteOutOfRange(idx))?; + if end > data_len { + return Err(ApplyError::WriteOutOfRange(idx)); + } + Ok((rel, end)) +} + +/// Little-endian u64 store through the data slice. +fn write_word( + data: &mut [u8], + descriptor: &ModuleDescriptor, + target: u64, + value: u64, + idx: usize, +) -> Result<(), ApplyError> { + let (a, b) = data_range(data.len(), descriptor, target, idx)?; + data.get_mut(a..b) + .ok_or(ApplyError::WriteOutOfRange(idx))? + .copy_from_slice(&value.to_le_bytes()); + Ok(()) +} + +/// Little-endian u64 load from the data slice (the RELR addend read). +fn read_word( + data: &[u8], + descriptor: &ModuleDescriptor, + target: u64, + idx: usize, +) -> Result { + let (a, b) = data_range(data.len(), descriptor, target, idx)?; + data.get(a..b) + .and_then(|s| s.try_into().ok()) + .map(u64::from_le_bytes) + .ok_or(ApplyError::WriteOutOfRange(idx)) +} + +// ============================================================================ +// Symbol resolution — GNU hash across the scope +// ============================================================================ + +/// Resolve the applying module's symbol `r_sym` across the scope: +/// read its name from the module's own symtab/strtab, then GNU-hash +/// walk every scope module in order — first strong definition wins, +/// else the first weak one. Nothing found: a **weak** undefined +/// reference yields [`Resolution::WeakUndef`] (S = 0, the gABI rule); +/// a strong one refuses [`ApplyError::MissingSymbol`] — the link fails +/// closed. +fn resolve_for( + scope: &SymbolScope<'_>, + view: &ModuleView<'_>, + r_sym: u32, + idx: usize, +) -> Result { + // Symbol 0 is the reserved undefined symbol — an address-bearing + // relocation against it names nothing resolvable. + if r_sym == 0 { + return Err(ApplyError::MissingSymbol(idx)); + } + let sym = read_sym(view, r_sym)?; + let name = sym_name(view, sym.st_name.get())?; + let h = gnu_hash(name); + + let mut first_weak: Option = None; + for (m_idx, m) in scope.modules.iter().enumerate() { + if let Some((resolved, binding)) = lookup_in(m, m_idx, name, h)? { + if binding == STB_GLOBAL { + return Ok(Resolution::Def(resolved)); + } + if binding == STB_WEAK && first_weak.is_none() { + first_weak = Some(resolved); + } + } + } + if let Some(w) = first_weak { + return Ok(Resolution::Def(w)); + } + // The weak-undef gate reads the *referencing* symbol: only a weak + // reference that is itself undefined earns the zero resolution. + if sym.st_info >> 4 == STB_WEAK && sym.st_shndx.get() == SHN_UNDEF { + return Ok(Resolution::WeakUndef); + } + Err(ApplyError::MissingSymbol(idx)) +} + +/// Look one exported symbol up in one module — the `dlsym` half of +/// late binding (`res/IMAGE.md` §8). GNU-hash walk of `view`'s own +/// tables: the first *defining* candidate wins (global before weak is +/// the table's own order); `None` when the module does not define +/// `name`; a module without `DT_GNU_HASH` or with lying tables refuses +/// [`ApplyError::Input`]. The address is absolute: `SHN_ABS` stands +/// alone, everything else rebases on `view.base`. +pub fn lookup_symbol(view: &ModuleView<'_>, name: &[u8]) -> Result, ApplyError> { + let h = gnu_hash(name); + match lookup_in(view, 0, name, h)? { + Some((found, _binding)) => { + if found.st_shndx == SHN_ABS { + return Ok(Some(found.st_value)); + } + view.base + .checked_add(found.st_value) + .map(Some) + .ok_or(ApplyError::Overflow(0)) + } + None => Ok(None), + } +} + +/// A definition's absolute address: `SHN_ABS` values stand alone; +/// everything else rebases on the defining module's chosen base. +fn absolute(scope: &SymbolScope<'_>, sym: &Resolved, idx: usize) -> Result { + if sym.st_shndx == SHN_ABS { + return Ok(sym.st_value); + } + let m = scope.modules.get(sym.module).ok_or(ApplyError::Input)?; + m.base + .checked_add(sym.st_value) + .ok_or(ApplyError::Overflow(idx)) +} + +/// GNU-hash lookup of `name` (pre-hashed `h`) in one module. Returns +/// the first *defining* candidate (`st_shndx != SHN_UNDEF`, binding +/// global or weak) or `None`; malformed tables refuse +/// [`ApplyError::Input`]. The bloom filter is honored when present — +/// a word of zeros hides every name, exactly as on-disk. +fn lookup_in( + m: &ModuleView<'_>, + m_idx: usize, + name: &[u8], + h: u32, +) -> Result, ApplyError> { + let gh_off = m.descriptor.dynz.gnu_hash_off.get(); + // Buckets + chains are mandatory (`res/IMAGE.md` §7): a module + // without `DT_GNU_HASH` cannot participate in resolution. + if gh_off == 0 { + return Err(ApplyError::Input); + } + let hash = ro_tail(m, gh_off)?; + + let nbuckets = u32_at(hash, 0)?; + let symoffset = u32_at(hash, 4)?; + let bloom_size = u32_at(hash, 8)?; + let bloom_shift = u32_at(hash, 12)?; + // Zero buckets divides by zero; symoffset 0 would hash the reserved + // null symbol — both mark a lying table. + if nbuckets == 0 || symoffset == 0 { + return Err(ApplyError::Input); + } + + // Bloom gate (64-bit words). + if bloom_size != 0 { + let word_idx = u64::from(h / 64) % u64::from(bloom_size); + let word_off = word_idx + .checked_mul(8) + .and_then(|o| o.checked_add(16)) + .ok_or(ApplyError::Input)?; + let word = u64_at(hash, word_off)?; + let bit_a = 1u64 << (h % 64); + // A lying shift (≥ 32) must not panic the shift op; the spec's + // shift is always < 32, so an out-of-range one just selects bit 0. + let bit_b = 1u64 << (h.checked_shr(bloom_shift).unwrap_or(0) % 64); + if word & bit_a == 0 || word & bit_b == 0 { + return Ok(None); + } + } + + let buckets_off = u64::from(bloom_size) + .checked_mul(8) + .and_then(|o| o.checked_add(16)) + .ok_or(ApplyError::Input)?; + let chains_off = u64::from(nbuckets) + .checked_mul(4) + .and_then(|o| o.checked_add(buckets_off)) + .ok_or(ApplyError::Input)?; + + let bucket_slot = u64::from(h % nbuckets) + .checked_mul(4) + .and_then(|o| o.checked_add(buckets_off)) + .ok_or(ApplyError::Input)?; + let first = u32_at(hash, bucket_slot)?; + if first == 0 { + return Ok(None); + } + if first < symoffset { + return Err(ApplyError::Input); + } + + let mut sym_idx = first; + loop { + let chain_slot = u64::from(sym_idx.checked_sub(symoffset).ok_or(ApplyError::Input)?) + .checked_mul(4) + .and_then(|o| o.checked_add(chains_off)) + .ok_or(ApplyError::Input)?; + let chain = u32_at(hash, chain_slot)?; + // Hash match ignores the low (end-of-chain) bit. + if chain | 1 == h | 1 { + let sym = read_sym(m, sym_idx)?; + if sym.st_shndx.get() != SHN_UNDEF && sym_name_matches(m, sym.st_name.get(), name)? { + let binding = sym.st_info >> 4; + if binding == STB_GLOBAL || binding == STB_WEAK { + return Ok(Some(( + Resolved { + module: m_idx, + st_value: sym.st_value.get(), + st_shndx: sym.st_shndx.get(), + }, + binding, + ))); + } + } + } + if chain & 1 == 1 { + return Ok(None); + } + // The chain walk is bounded: the index grows monotonically and + // every read above refuses once it escapes the hash view. + sym_idx = sym_idx.checked_add(1).ok_or(ApplyError::Input)?; + } +} + +/// Read symbol `idx` from the module's dynsym. +fn read_sym(m: &ModuleView<'_>, idx: u32) -> Result { + let symtab_off = m.descriptor.dynz.symtab_off.get(); + if symtab_off == 0 { + return Err(ApplyError::Input); + } + let off = u64::from(idx) + .checked_mul(SYM_ENTRY_SIZE) + .and_then(|o| o.checked_add(symtab_off)) + .ok_or(ApplyError::Input)?; + let bytes = ro_slice(m, off, SYM_ENTRY_SIZE)?; + Sym64::read_from_bytes(bytes).map_err(|_| ApplyError::Input) +} + +/// The NUL-terminated name at `st_name`, bounded by `strtab_sz`. +fn sym_name<'a>(m: &ModuleView<'a>, st_name: u32) -> Result<&'a [u8], ApplyError> { + let strtab_off = m.descriptor.dynz.strtab_off.get(); + let strtab_sz = m.descriptor.dynz.strtab_sz.get(); + let avail = strtab_sz + .checked_sub(u64::from(st_name)) + .ok_or(ApplyError::Input)?; + let start = strtab_off + .checked_add(u64::from(st_name)) + .ok_or(ApplyError::Input)?; + let tail = ro_slice(m, start, avail)?; + let len = tail.iter().position(|&b| b == 0).ok_or(ApplyError::Input)?; + tail.get(..len).ok_or(ApplyError::Input) +} + +/// Whether the module's string at `st_name` equals `needle` (bounded; +/// a too-short or unterminated tail is a mismatch, not a read). +fn sym_name_matches(m: &ModuleView<'_>, st_name: u32, needle: &[u8]) -> Result { + let strtab_sz = m.descriptor.dynz.strtab_sz.get(); + // st_name at or past the table end is a lying symbol. + let avail = strtab_sz + .checked_sub(u64::from(st_name)) + .ok_or(ApplyError::Input)?; + let need = u64::try_from(needle.len()) + .ok() + .and_then(|n| n.checked_add(1)) + .ok_or(ApplyError::Input)?; + if avail < need { + return Ok(false); + } + let start = m + .descriptor + .dynz + .strtab_off + .get() + .checked_add(u64::from(st_name)) + .ok_or(ApplyError::Input)?; + let s = ro_slice(m, start, need)?; + Ok(s.get(..needle.len()) == Some(needle) && s.get(needle.len()) == Some(&0)) +} + +/// The GNU hash function (dj2 33-multiplier, as the format pins it). +/// The arithmetic is *defined* modulo 2³² — wrapping here is the +/// algorithm, not an unchecked overflow. +fn gnu_hash(name: &[u8]) -> u32 { + let mut h: u32 = 5381; + for &b in name { + h = h.wrapping_mul(33).wrapping_add(u32::from(b)); + } + h +} + +// ============================================================================ +// Read-view plumbing +// ============================================================================ + +/// Resolve an image-relative range to bytes inside one of the module's +/// text/rodata read views. Reads are capped at the segment's `filesz` +/// AND the view's actual length; a range no view contains refuses +/// [`ApplyError::Input`]. +fn ro_slice<'a>(m: &ModuleView<'a>, off: u64, len: u64) -> Result<&'a [u8], ApplyError> { + for (i, s) in m.descriptor.live_segs().iter().enumerate() { + if s.kind != seg::KIND_TEXT && s.kind != seg::KIND_RODATA { + continue; + } + let Some(bytes) = m.ro.get(i).copied().flatten() else { + continue; + }; + let Some(rel) = off.checked_sub(s.vaddr.get()) else { + continue; + }; + let end = rel.checked_add(len).ok_or(ApplyError::Input)?; + let avail = s + .filesz + .get() + .min(u64::try_from(bytes.len()).map_err(|_| ApplyError::Input)?); + if end > avail { + continue; + } + let a = usize::try_from(rel).map_err(|_| ApplyError::Input)?; + let b = usize::try_from(end).map_err(|_| ApplyError::Input)?; + return bytes.get(a..b).ok_or(ApplyError::Input); + } + Err(ApplyError::Input) +} + +/// Resolve an image-relative offset to the remainder of its containing +/// text/rodata view (the unsized `DT_GNU_HASH` read path). +fn ro_tail<'a>(m: &ModuleView<'a>, off: u64) -> Result<&'a [u8], ApplyError> { + for (i, s) in m.descriptor.live_segs().iter().enumerate() { + if s.kind != seg::KIND_TEXT && s.kind != seg::KIND_RODATA { + continue; + } + let Some(bytes) = m.ro.get(i).copied().flatten() else { + continue; + }; + let Some(rel) = off.checked_sub(s.vaddr.get()) else { + continue; + }; + let avail = s + .filesz + .get() + .min(u64::try_from(bytes.len()).map_err(|_| ApplyError::Input)?); + if rel >= avail { + continue; + } + let a = usize::try_from(rel).map_err(|_| ApplyError::Input)?; + let b = usize::try_from(avail).map_err(|_| ApplyError::Input)?; + return bytes.get(a..b).ok_or(ApplyError::Input); + } + Err(ApplyError::Input) +} + +/// Bounded little-endian u32 read at `off` in a resolved view. +fn u32_at(bytes: &[u8], off: u64) -> Result { + let a = usize::try_from(off).map_err(|_| ApplyError::Input)?; + let b = a.checked_add(4).ok_or(ApplyError::Input)?; + bytes + .get(a..b) + .and_then(|s| s.try_into().ok()) + .map(u32::from_le_bytes) + .ok_or(ApplyError::Input) +} + +/// Bounded little-endian u64 read at `off` in a resolved view. +fn u64_at(bytes: &[u8], off: u64) -> Result { + let a = usize::try_from(off).map_err(|_| ApplyError::Input)?; + let b = a.checked_add(8).ok_or(ApplyError::Input)?; + bytes + .get(a..b) + .and_then(|s| s.try_into().ok()) + .map(u64::from_le_bytes) + .ok_or(ApplyError::Input) +} + +/// Normalize a TLS alignment: zero means byte-aligned; anything else +/// must be a power of two. +fn normalize_align(align: u64) -> Result { + if align == 0 { + return Ok(1); + } + if !align.is_power_of_two() { + return Err(ApplyError::Input); + } + Ok(align) +} + +/// Round up to a power-of-two alignment; `None` on wrap. +fn align_up(v: u64, align: u64) -> Option { + let mask = align.checked_sub(1)?; + v.checked_add(mask).map(|x| x & !mask) +} diff --git a/native/crates/ld/src/check.rs b/native/crates/ld/src/check.rs new file mode 100644 index 0000000..af893b1 --- /dev/null +++ b/native/crates/ld/src/check.rs @@ -0,0 +1,605 @@ +//! The `Register` gate + carve planner (`res/IMAGE.md` §2, §3): what the +//! image service runs before an image may become leases. +//! +//! ## Contract +//! +//! `check_image::(bytes) -> Result`: +//! +//! 1. [`crate::parse`] the image for `A::MACHINE` under whichever +//! addressing convention its header declares (aligned, bounded, +//! in-bounds — every [`ParseError`] refuses here). +//! 2. **Flag gate**: `DT_FLAGS`/`DT_FLAGS_1` must carry `BIND_NOW`; +//! `DF_TEXTREL` (or `DT_TEXTREL`) refuses. Relative images only — +//! an absolute one has no `.dynamic` to ask. +//! 3. **Reloc gate**: walk `.rela.dyn` + `.rela.plt` (+ `DT_RELR` +//! presence); every entry must classify via `A::classify` — one +//! reject-list or unknown type refuses the whole image. Relative +//! images only, for the same reason +//! ([`CheckError::Reloc`]). A TpRel reloc in an image with no +//! `PT_TLS` refuses. +//! 4. **Carve plan**: retain every bounded `PT_LOAD` plus the optional +//! `PT_TLS`, classified as text R|X, rodata R, data template R→private +//! copy, or TLS init. Kinds may repeat (full RELRO commonly emits two +//! adjacent writable LOADs); a segment whose flags fit no class (e.g. +//! W|X) refuses ([`CheckError::Segment`]). +//! 5. Emit the frozen [`ModuleDescriptor`] (image-relative, +//! `validate()`-clean by construction) + the carve plan +//! ([`CheckedImage::carve`]): the page-aligned `(file_off, len, +//! class)` windows the service turns into per-segment extents in +//! place. +//! +//! The descriptor is the *promise* the stub later links against, so +//! `check` is the single place where a hostile image is caught — the +//! stub re-checks only cheap invariants (defense in depth), never +//! re-derives trust. + +use elf::abi::{DF_1_NOW, DF_BIND_NOW, DF_TEXTREL, DT_NEEDED, DT_NULL}; +use kore_abi::image::{IMAGE_ABI, ImageName, MAX_DEPENDENCIES, ModuleDescriptor, kind, seg}; + +use crate::parse::{ + Addressing, DynRange, DynStart, LoadSegment, PAGE_SIZE, ParseError, ParsedImage, parse_any, +}; +use crate::reloc::{RelocArch, RelocKind, RelocRefusal}; + +/// One carve window: a page-aligned byte range of the registered image +/// that becomes one per-segment extent (in place, no copy). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CarveSeg { + /// Byte offset into the registered image (page-aligned). + pub file_off: u64, + /// Window length (page-multiple; the lease's backing size). + pub len: u64, + /// The [`kore_abi::image::seg`] kind this window backs. + pub kind: u8, +} + +/// The `Register` gate's yield: the frozen descriptor + the carve plan. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CheckedImage { + /// The image-relative promise (reply payload / `.kore.ld` body). + pub descriptor: ModuleDescriptor, + /// The in-place carve windows, one per live descriptor segment (same + /// order as `descriptor.segs`). + pub carve: [Option; kore_abi::image::MAX_SEGMENTS], + /// The `PT_GNU_RELRO` window, image-relative `(offset, len)`. Not + /// on the wire (`DynDesc` is frozen); it rides the checked view so + /// the apply side can report the seal range ([`crate::apply::Applied`]). + pub relro: Option<(u64, u64)>, + /// Direct `DT_NEEDED` names copied from the validated dynamic/string + /// tables. ImageService expands and freezes their transitive generation + /// closure at privileged installation. + pub needed: [Option; MAX_DEPENDENCIES], +} + +/// Why `Register` refused an image. Maps onto +/// [`ImageRefusal::BadImage`](kore_abi::image::ImageRefusal) at the service +/// boundary; the variant is the diagnostic. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CheckError { + /// The parse layer refused. + Parse(ParseError), + /// `BIND_NOW` missing, or text relocations declared. + Flags, + /// A relocation refused classification (reject-list or unknown). + Reloc(RelocRefusal), + /// A TLS reloc with no `PT_TLS`, or TLS geometry out of bounds. + Tls, + /// A load segment fits no carve class (e.g. `W|X`), or classes + /// repeat where they must not. + Segment, + /// RELRO is not an exact page range inside the private data union. + Relro, + /// The dynamic section promises a range the segments don't contain. + Range, +} + +/// The `Register` gate (contract steps 1–5): parse → flag gate → reloc +/// gate → carve plan → descriptor. One pass, total refusal — a hostile +/// image never yields a partial [`CheckedImage`]. +/// +/// ## Both conventions come through here +/// +/// The gate used to be `ET_DYN`-only, which left every fixed-address +/// image — the linker stub and all eight founded rows — reachable solely +/// through `parse_static_image` and a founder-written `.kore.ld`. So +/// there was no way for a `STATIC_EXECUTABLE` to acquire a descriptor +/// *and* the carve windows that back its segment leases, and the +/// degenerate static case of `res/IMAGE.md` §4 could not be assembled at +/// all. +/// +/// Admitting `ET_EXEC` relaxes no gate. The flag and relocation gates +/// ask questions about dynamic linking, and are **skipped** for an +/// absolute image because there is nothing to ask them of: no +/// `.dynamic`, so no `BIND_NOW` to demand (demanding it would refuse +/// every static image for lacking a section it is defined by not +/// having), and no relocation table to classify. Everything that +/// survives — the carve classes, the W^X refusal, page congruence, the +/// entry-inside-a-segment proof, the segment-count bound — applies to +/// both and is applied to both. +pub fn check_image(bytes: &[u8]) -> Result { + let (img, addressing) = parse_any(bytes, A::MACHINE).map_err(CheckError::Parse)?; + + if addressing == Addressing::Relative { + // -- Flag gate ----------------------------------------------------- + // Eager binding is mandatory (`-z now`): either flags word may + // carry it. Text relocations refuse in both spellings (tag and + // flag) — text is an immutable shared lease, there is nothing to + // relocate. + let bind_now = img.dynamic.flags & DF_BIND_NOW as u64 != 0 + || img.dynamic.flags1 & DF_1_NOW as u64 != 0; + if !bind_now { + return Err(CheckError::Flags); + } + if img.dynamic.textrel || img.dynamic.flags & DF_TEXTREL as u64 != 0 { + return Err(CheckError::Flags); + } + + // -- Reloc gate ---------------------------------------------------- + reloc_gate::(bytes, &img)?; + } + + // -- Carve plan + descriptor ------------------------------------------- + let needed = needed_names(bytes, &img)?; + carve(&img, addressing, needed) +} + +/// Copy the already parse-validated `DT_NEEDED` names into a bounded inert +/// table. Offsets are rechecked against the resolved string table so this +/// layer remains independently fail-closed. +fn needed_names( + bytes: &[u8], + img: &ParsedImage<'_>, +) -> Result<[Option; MAX_DEPENDENCIES], CheckError> { + let mut out = [None; MAX_DEPENDENCIES]; + if img.dynamic.needed_count == 0 { + return Ok(out); + } + let dynamic = table_bytes(bytes, img.dynamic.dynamic)?; + let strtab = table_bytes(bytes, img.dynamic.strtab.ok_or(CheckError::Range)?)?; + let mut count = 0usize; + for entry in dynamic.chunks_exact(16) { + let tag = i64::from_le_bytes(entry[..8].try_into().map_err(|_| CheckError::Range)?); + if tag == DT_NULL { + break; + } + if tag != DT_NEEDED { + continue; + } + let off = usize::try_from(u64::from_le_bytes( + entry[8..].try_into().map_err(|_| CheckError::Range)?, + )) + .map_err(|_| CheckError::Range)?; + let tail = strtab.get(off..).ok_or(CheckError::Range)?; + let len = tail + .iter() + .position(|byte| *byte == 0) + .ok_or(CheckError::Range)?; + let name = core::str::from_utf8(&tail[..len]) + .ok() + .and_then(ImageName::new) + .ok_or(CheckError::Range)?; + let slot = out.get_mut(count).ok_or(CheckError::Range)?; + *slot = Some(name); + count += 1; + } + if count != usize::from(img.dynamic.needed_count) { + return Err(CheckError::Range); + } + Ok(out) +} + +/// Walk `.rela.dyn` + `.rela.plt`: every `r_type` must classify under +/// `A`, and a TLS relocation without a `PT_TLS` template refuses (there +/// is no block for its offset to land in). `DT_RELR` needs no walk +/// here — its entries are all base-relative by construction; its shape +/// (word-multiple size) was the parse layer's gate. +fn reloc_gate(bytes: &[u8], img: &ParsedImage<'_>) -> Result<(), CheckError> { + for range in [img.dynamic.rela, img.dynamic.jmprel].into_iter().flatten() { + let table = table_bytes(bytes, range)?; + for entry in table.chunks_exact(24) { + // r_info = sym << 32 | type (ELF64); the mask cast is exact. + let r_info = entry + .get(8..16) + .and_then(|s| s.try_into().ok()) + .map(u64::from_le_bytes) + .ok_or(CheckError::Range)?; + let r_type = (r_info & 0xffff_ffff) as u32; + let kind = A::classify(r_type).map_err(CheckError::Reloc)?; + if kind == RelocKind::TpRel && img.tls.is_none() { + return Err(CheckError::Tls); + } + } + } + Ok(()) +} + +/// Re-slice a parse-resolved table range (the parse layer proved it in +/// bounds; the re-check keeps this layer honest on its own). +fn table_bytes(bytes: &[u8], range: DynRange) -> Result<&[u8], CheckError> { + let start = usize::try_from(range.file_off).map_err(|_| CheckError::Range)?; + let len = usize::try_from(range.size).map_err(|_| CheckError::Range)?; + let end = start.checked_add(len).ok_or(CheckError::Range)?; + bytes.get(start..end).ok_or(CheckError::Range) +} + +/// Carve class of one load segment (contract step 4). +#[derive(Clone, Copy, PartialEq, Eq)] +enum Class { + Text, + Rodata, + Data, +} + +fn class_of(s: &LoadSegment<'_>) -> Result { + match (s.flags.write, s.flags.exec) { + // W|X refuses outright — no segment may be both executable and + // writable, independent of how many LOADs share a class. + (true, true) => Err(CheckError::Segment), + (false, true) => Ok(Class::Text), + (true, false) => Ok(Class::Data), + (false, false) if s.flags.read => Ok(Class::Rodata), + // A no-access segment carries nothing mappable. + (false, false) => Err(CheckError::Segment), + } +} + +/// Fold the load segments into bounded `res/IMAGE.md` §2 entries and +/// emit the descriptor + carve windows, in canonical kind order +/// (text, rodata, data, TLS — `descriptor.segs` and `carve` stay +/// parallel). +fn carve( + img: &ParsedImage<'_>, + addressing: Addressing, + needed: [Option; MAX_DEPENDENCIES], +) -> Result { + // Validate the complete load set before emitting any descriptor. Kinds + // deliberately may repeat: lld's full-RELRO layout has one writable LOAD + // for RELRO and another for ordinary data/bss. + for s in img.segments.iter() { + let _ = class_of(s)?; + } + + // **What the descriptor's addresses are relative to.** For a PIE + // that is the lowest load vaddr — everything is stated as an offset + // from it, because per-task ASLR makes an absolute meaningless. For + // a fixed-address image it is nothing: its link address is its + // runtime address, so subtracting a base would turn the one number + // that is already final into an offset from a base no consumer will + // ever add back. + let base = match addressing { + // The engine maps lease windows, and a lease is page-granular. The + // load base is therefore the lowest LOAD rounded outward, not the raw + // `p_vaddr`: a legal ELF LOAD may begin part-way through its first + // page (`p_offset % page == p_vaddr % page`). + Addressing::Relative => align_down( + img.segments + .iter() + .map(|s| s.vaddr) + .min() + .ok_or(CheckError::Segment)?, + PAGE_SIZE, + ), + Addressing::Absolute => 0, + }; + + let mut d = ModuleDescriptor::empty(); + d.abi.set(IMAGE_ABI); + d.machine.set(img.machine); + d.needed_count.set(img.dynamic.needed_count); + + // Entry decides the image kind; an executable's entry must land in a + // load segment (a promise outside the image refuses). + if img.entry != 0 { + let in_a_segment = img.segments.iter().any(|s| { + class_of(s) == Ok(Class::Text) + && img.entry >= s.vaddr + && s.vaddr + .checked_add(s.memsz) + .is_some_and(|end| img.entry < end) + }); + if !in_a_segment { + return Err(CheckError::Range); + } + // Entry decides *executable vs library*; the addressing decides + // which executable convention. Both are recorded, because the + // consumer reads the kind before the address and the two answers + // are not derivable from each other. + d.kind = match addressing { + Addressing::Relative => kind::EXECUTABLE, + Addressing::Absolute => kind::STATIC_EXECUTABLE, + }; + d.entry + .set(img.entry.checked_sub(base).ok_or(CheckError::Range)?); + } else if addressing == Addressing::Absolute { + // `parse_static_image` proves the entry lies inside an + // executable load, so a zero here means it reached this gate + // some other way. Refused rather than filed as a library: a + // fixed-address image with nothing to enter is not one. + return Err(CheckError::Range); + } else { + d.kind = kind::LIBRARY; + } + + // Segment table + carve windows, canonical kind order. + let mut carve = [None; kore_abi::image::MAX_SEGMENTS]; + let mut n: usize = 0; + for (class, sk, access) in [ + (Class::Text, seg::KIND_TEXT, seg::ACCESS_R | seg::ACCESS_X), + (Class::Rodata, seg::KIND_RODATA, seg::ACCESS_R), + (Class::Data, seg::KIND_DATA, seg::ACCESS_R), + ] { + for s in img.segments.iter() { + if class_of(s)? != class { + continue; + } + let entry = d.segs.get_mut(n).ok_or(CheckError::Segment)?; + let raw_mem_end = s.vaddr.checked_add(s.memsz).ok_or(CheckError::Segment)?; + // Normalize the consumer-facing mapping outward to pages. The + // source lease starts at `align_down(p_offset)` and is installed at + // `align_down(p_vaddr)`; congruence, already proved by the parser, + // keeps every byte at the address the ELF declared. Carry the prefix + // in `filesz` and the rounded tail in `memsz` so the engine copies and + // maps the complete window rather than asking the kernel for an + // unaligned leaf. + let map_va = align_down(s.vaddr, PAGE_SIZE); + let map_end = align_up(raw_mem_end, PAGE_SIZE).ok_or(CheckError::Segment)?; + let file_start = align_down(s.file_off, PAGE_SIZE); + let file_end = s + .file_off + .checked_add(s.filesz) + .and_then(|end| align_up(end, PAGE_SIZE)) + .ok_or(CheckError::Segment)?; + let prefix = s.vaddr.checked_sub(map_va).ok_or(CheckError::Segment)?; + entry + .vaddr + .set(map_va.checked_sub(base).ok_or(CheckError::Segment)?); + entry + .memsz + .set(map_end.checked_sub(map_va).ok_or(CheckError::Segment)?); + entry + .filesz + .set(prefix.checked_add(s.filesz).ok_or(CheckError::Segment)?); + entry.kind = sk; + entry.access = access; + // The in-place window rounds `p_offset..p_offset+p_filesz` + // outward. `p_offset` itself need not be page-aligned. + let file_span = file_end + .checked_sub(file_start) + .ok_or(CheckError::Segment)?; + let map_span = map_end.checked_sub(map_va).ok_or(CheckError::Segment)?; + *carve.get_mut(n).ok_or(CheckError::Segment)? = Some(CarveSeg { + file_off: file_start, + // Immutable mappings install `memsz` whole, while data only + // reads the file prefix. Back the larger shape so the same + // lease contract is sound for both without reading padding + // past the module bytes. A bss-only LOAD therefore gets a + // zero source lease and participates in the same data union. + len: file_span.max(map_span), + kind: sk, + }); + n = n.checked_add(1).ok_or(CheckError::Segment)?; + } + } + + // lld owes the data class no adjacency: whether the RELRO LOAD's + // page-rounded end abuts the next RW LOAD is decided by section sizes + // (a build-root path-length shift flipped it and made one build's + // kore-svc-swap unbootable while its twin booted), and the descriptor + // contract requires the data union hole-free — the engine realizes it + // as ONE private mapping and must not hand out memory the descriptor + // never declared. Normalize at the one producer: grow the earlier + // data window's zero-fill tail (`memsz` past `filesz` is `.bss` by + // the `seg::KIND_DATA` contract) until it abuts its successor, so the + // page the linker skipped becomes DECLARED zeroed private data rather + // than a refusal. Real overlaps are still refused — the parser's + // pairwise sweep ran before the fold, and `validate()`'s disjointness + // sweep runs after; bridging only ever closes a gap. + { + let mut order: [usize; kore_abi::image::MAX_SEGMENTS] = [0; kore_abi::image::MAX_SEGMENTS]; + let mut m = 0usize; + for i in 0..n { + if d.segs[i].kind == seg::KIND_DATA { + order[m] = i; + m += 1; + } + } + // Appended in load order, but bridge by ascending vaddr — the + // ELF's phdr order is a convention, not a proof. + order[..m].sort_unstable_by_key(|&i| d.segs[i].vaddr.get()); + // Bridgeable gap ceiling. lld's congruence rounding opens at most + // alignment-sized holes (64 KiB covers every max-page-size the + // supported targets use); anything wider is not a linker artifact + // but a layout lying about its shape — left unbridged for + // `validate()` to refuse, so a hostile ELF cannot declare data at + // two far-apart addresses and have the bridge mint the span + // between them as a mappable bss. + const MAX_DATA_BRIDGE: u64 = 16 * PAGE_SIZE; + for w in 0..m.saturating_sub(1) { + let (a, b) = (order[w], order[w + 1]); + let a_end = d.segs[a] + .vaddr + .get() + .checked_add(d.segs[a].memsz.get()) + .ok_or(CheckError::Segment)?; + let b_va = d.segs[b].vaddr.get(); + if a_end < b_va + && b_va.checked_sub(a_end).ok_or(CheckError::Segment)? <= MAX_DATA_BRIDGE + { + let grown = b_va + .checked_sub(d.segs[a].vaddr.get()) + .ok_or(CheckError::Segment)?; + d.segs[a].memsz.set(grown); + // The parallel carve window backs the larger of the file + // and mapped shapes; keep it in step with the grown map. + if let Some(Some(c)) = carve.get_mut(a) { + c.len = c.len.max(grown); + } + } + } + } + + if let Some(tls) = &img.tls { + // TLS geometry sanity: the alignment must be a power of two (the + // layout builder rounds by it) and the init image must sit at or + // above the image base. + if tls.align != 0 && !tls.align.is_power_of_two() { + return Err(CheckError::Tls); + } + let rel = tls.vaddr.checked_sub(base).ok_or(CheckError::Tls)?; + // Pure .tbss has no bytes to grant. Its file offset need not be page + // congruent (LLD can place it between LOADs); carry only TLS geometry. + // Inventing a file-backed lease here would misrepresent an empty init. + if !tls.init.is_empty() { + // Page congruence: the init image reaches the consumer through a + // page-rounded lease window, and the stub recovers its in-window + // offset as `vaddr % PAGE` (the standard ELF mapping invariant + // `p_offset ≡ p_vaddr (mod page)`). An incongruent nonempty PT_TLS is + // lying about where its bytes live — refused here, at the one + // gate hostile images pass (`crates/ld/src/link.rs` trusts it). + if tls.file_off % PAGE_SIZE != tls.vaddr % PAGE_SIZE { + return Err(CheckError::Tls); + } + let entry = d.segs.get_mut(n).ok_or(CheckError::Segment)?; + entry.vaddr.set(rel); + entry.memsz.set(tls.memsz); + entry.filesz.set(tls.init.len() as u64); + entry.kind = seg::KIND_TLS; + entry.access = seg::ACCESS_R; + // The TLS init image is not page-aligned in the file (it lives + // inside the data segment's span) — the window rounds outward. + let start = align_down(tls.file_off, PAGE_SIZE); + let end = tls + .file_off + .checked_add(tls.init.len() as u64) + .and_then(|e| align_up(e, PAGE_SIZE)) + .ok_or(CheckError::Tls)?; + *carve.get_mut(n).ok_or(CheckError::Segment)? = Some(CarveSeg { + file_off: start, + len: end.checked_sub(start).ok_or(CheckError::Tls)?, + kind: seg::KIND_TLS, + }); + n = n.checked_add(1).ok_or(CheckError::Segment)?; + } + + d.tls.memsz.set(tls.memsz); + d.tls.align.set(tls.align); + d.tls.init_sz.set(tls.init.len() as u64); + } + d.seg_count = n as u8; + + // Dynamic geometry, image-relative (`DynDesc`'s space). + let dy = &img.dynamic; + set_range( + &mut d.dynz.dynamic_off, + &mut d.dynz.dynamic_sz, + Some(dy.dynamic), + base, + )?; + set_range(&mut d.dynz.rela_off, &mut d.dynz.rela_sz, dy.rela, base)?; + set_range(&mut d.dynz.relr_off, &mut d.dynz.relr_sz, dy.relr, base)?; + set_range( + &mut d.dynz.jmprel_off, + &mut d.dynz.jmprel_sz, + dy.jmprel, + base, + )?; + set_range( + &mut d.dynz.strtab_off, + &mut d.dynz.strtab_sz, + dy.strtab, + base, + )?; + set_range( + &mut d.dynz.init_array_off, + &mut d.dynz.init_array_sz, + dy.init_array, + base, + )?; + set_range( + &mut d.dynz.fini_array_off, + &mut d.dynz.fini_array_sz, + dy.fini_array, + base, + )?; + set_start(&mut d.dynz.symtab_off, dy.symtab, base)?; + set_start(&mut d.dynz.gnu_hash_off, dy.gnu_hash, base)?; + d.dynz.flags.set(dy.flags); + + // RELRO, image-relative — on the wire too (the pre-merge ABI + // amendment: the stub learns its seal window from the Acquire + // descriptor, `DynDesc::relro_*`), and on the checked view for the + // service side. + let relro = match img.relro { + None => None, + Some((vaddr, len)) => Some((vaddr.checked_sub(base).ok_or(CheckError::Range)?, len)), + }; + if let Some((off, sz)) = relro { + d.dynz.relro_off.set(off); + d.dynz.relro_sz.set(sz); + } + + // The contract's own promise is also enforced in release builds: this + // includes data-union contiguity and pairwise mapping disjointness. + if !d.validate() { + return Err(CheckError::Segment); + } + + // Shared startup/late admission: sealing may remove write permission only + // from complete pages in this module's validated private data union. + if let Some((off, len)) = relro { + let (start, size) = crate::apply::data_bounds(&d) + .map_err(|_| CheckError::Relro)? + .ok_or(CheckError::Relro)?; + let end = off.checked_add(len).ok_or(CheckError::Relro)?; + let data_end = start.checked_add(size).ok_or(CheckError::Relro)?; + if len == 0 || off % PAGE_SIZE != 0 || len % PAGE_SIZE != 0 || off < start || end > data_end + { + return Err(CheckError::Relro); + } + } + + Ok(CheckedImage { + descriptor: d, + carve, + relro, + needed, + }) +} + +/// Stamp an image-relative `(off, sz)` pair from a resolved range +/// (`(0, 0)` when absent — `DynDesc`'s vacant encoding). +fn set_range( + off: &mut zerocopy::byteorder::little_endian::U64, + sz: &mut zerocopy::byteorder::little_endian::U64, + range: Option, + base: u64, +) -> Result<(), CheckError> { + if let Some(r) = range { + off.set(r.vaddr.checked_sub(base).ok_or(CheckError::Range)?); + sz.set(r.size); + } + Ok(()) +} + +/// Stamp an image-relative unsized offset (zero when absent). +fn set_start( + off: &mut zerocopy::byteorder::little_endian::U64, + start: Option, + base: u64, +) -> Result<(), CheckError> { + if let Some(s) = start { + off.set(s.vaddr.checked_sub(base).ok_or(CheckError::Range)?); + } + Ok(()) +} + +/// Round up to an alignment (power of two); `None` on wrap. +fn align_up(v: u64, align: u64) -> Option { + let mask = align.checked_sub(1)?; + v.checked_add(mask).map(|x| x & !mask) +} + +/// Round down to an alignment (power of two, non-zero). +fn align_down(v: u64, align: u64) -> u64 { + v & !(align.saturating_sub(1)) +} diff --git a/native/crates/ld/src/functions.rs b/native/crates/ld/src/functions.rs new file mode 100644 index 0000000..afcd868 --- /dev/null +++ b/native/crates/ld/src/functions.rs @@ -0,0 +1,95 @@ +//! Shared init/fini array admission for startup and late linking. +//! Validation performs no calls; callers snapshot admitted addresses before +//! entering code that could mutate the original writable array. + +use kore_abi::image::{ModuleDescriptor, seg}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FunctionError { + /// The array is not composed of complete ELF64 pointers. + Shape, + /// A non-sentinel entry does not name loaded executable text. + Target(usize), +} + +/// Does an absolute address name file-backed text in this placed module? +/// Read-only data, writable data, padding beyond filesz and overflowing +/// placement geometry never count as executable targets. +pub fn contains_text(descriptor: &ModuleDescriptor, base: u64, address: u64) -> bool { + descriptor.live_segs().iter().any(|segment| { + if segment.kind != seg::KIND_TEXT { + return false; + } + let Some(start) = base.checked_add(segment.vaddr.get()) else { + return false; + }; + let Some(end) = start.checked_add(segment.filesz.get()) else { + return false; + }; + address >= start && address < end + }) +} + +/// Validate the complete array before exposing any address to the caller. +/// The caller supplies the actual loaded executable scope. Zero and all-ones +/// retain the established crt sentinel meaning and are omitted. +pub fn function_addresses( + bytes: &[u8], + is_executable: impl Fn(u64) -> bool, +) -> Result + '_, FunctionError> { + if bytes.len() % 8 != 0 { + return Err(FunctionError::Shape); + } + let words = || { + bytes.chunks_exact(8).map(|word| { + u64::from_le_bytes(word.try_into().expect("chunks_exact yields eight bytes")) + }) + }; + for (index, address) in words().enumerate() { + if address != 0 && address != u64::MAX && !is_executable(address) { + return Err(FunctionError::Target(index)); + } + } + Ok(words().filter(|address| *address != 0 && *address != u64::MAX)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validates_the_entire_array_before_exposing_an_entry() { + let mut bytes = [0u8; 32]; + bytes[8..16].copy_from_slice(&0x1000u64.to_le_bytes()); + bytes[16..24].copy_from_slice(&u64::MAX.to_le_bytes()); + bytes[24..].copy_from_slice(&0x2000u64.to_le_bytes()); + assert!(matches!( + function_addresses(&bytes, |p| p == 0x1000), + Err(FunctionError::Target(3)) + )); + let mut addresses = function_addresses(&bytes, |p| p == 0x1000 || p == 0x2000).unwrap(); + assert_eq!(addresses.next(), Some(0x1000)); + assert_eq!(addresses.next(), Some(0x2000)); + assert_eq!(addresses.next(), None); + assert!(matches!( + function_addresses(&bytes[..31], |_| true), + Err(FunctionError::Shape) + )); + } + + #[test] + fn text_membership_rejects_padding_other_segments_and_overflow() { + let mut descriptor = ModuleDescriptor::empty(); + descriptor.seg_count = 1; + descriptor.segs[0].kind = seg::KIND_TEXT; + descriptor.segs[0].vaddr.set(0x1000); + descriptor.segs[0].filesz.set(16); + descriptor.segs[0].memsz.set(4096); + assert!(contains_text(&descriptor, 0x4000, 0x5000)); + assert!(contains_text(&descriptor, 0x4000, 0x500f)); + assert!(!contains_text(&descriptor, 0x4000, 0x5010)); + assert!(!contains_text(&descriptor, u64::MAX - 8, 0x1000)); + descriptor.segs[0].kind = seg::KIND_RODATA; + assert!(!contains_text(&descriptor, 0x4000, 0x5000)); + } +} diff --git a/native/crates/ld/src/late.rs b/native/crates/ld/src/late.rs new file mode 100644 index 0000000..f771e12 --- /dev/null +++ b/native/crates/ld/src/late.rs @@ -0,0 +1,248 @@ +//! Late binding: the plan a task follows to link a shared object into +//! its **own** address space after start — `res/IMAGE.md` §8's "separate +//! library consuming the record, never a resurrection of the stub". +//! +//! The startup stub shares an image's segments from the image service's +//! leases. A late load has no such leases: the task holds the object's +//! bytes (read out of the DagFS store) and two things it can carve itself — +//! a writable→executable arena for the code (`JitArena`, §6a) and a plain +//! writable lease for the data. This module is the pure arithmetic between +//! a [`CheckedImage`] and those two windows: which file bytes land where, +//! and which shapes are refused. It touches no memory and holds no +//! capability, so it is host-testable against the same fixtures as the +//! check gate; the runtime half lives in `kore-late`. +//! +//! **Shape admitted.** A `kind::LIBRARY` whose text and rodata segments +//! form the *code run* — the page-rounded span from the lowest text/rodata +//! `vaddr` to the highest end — and whose writable segments all lie +//! **above** that run (lld's `-z separate-loadable-segments` layout: R, RX, +//! RW). The whole code run becomes one arena generation; the data union +//! ([`data_bounds`]) becomes one writable mapping at its linked offset, so +//! every PC-relative reference the linker fixed still lands. Refused, not +//! guessed: executables (they have an entry and a startup contract), TLS +//! (the task's thread-pointer layout was closed at start — §7), a data +//! segment below or inside the code run, and images too large for the +//! bounds the caller passes. + +use kore_abi::image::{MAX_SEGMENTS, ModuleDescriptor, kind, seg}; + +use crate::apply::data_bounds; +use crate::check::CheckedImage; +use crate::parse::PAGE_SIZE; + +/// Why a checked image cannot be late-linked. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LateError { + /// Not a `kind::LIBRARY` — executables carry an entry and a startup + /// contract the late path has no way to honor. + NotLibrary, + /// The image carries `PT_TLS`; a late object cannot join a thread + /// layout that was built and installed at start. + Tls, + /// No text segment — nothing to publish. + NoCode, + /// A writable segment lies below or inside the code run. + DataBelowCode, + /// The code run exceeds `max_code`, or the image span exceeds + /// `max_span`. + TooLarge, + /// The descriptor and carve plan disagree, or arithmetic overflowed. + Layout, + /// RELRO is not an exact page range inside the private data mapping. + Relro, +} + +/// One copy from the object's file bytes into a window: `dst` is relative +/// to the window's start, `file_off`/`len` name the source bytes. A copy +/// past the end of the bytes the caller holds is zero-filled by the caller +/// (the carve windows are page-rounded; the file need not be). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Copy { + pub dst: u64, + pub file_off: u64, + pub len: u64, +} + +/// The writable half: the data union at its linked offset. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct DataSpan { + /// Image-relative start of the union (`data_bounds`), page-aligned. + pub vaddr: u64, + /// Union length, page-rounded — the writable lease's length and the + /// exact `ModuleImage::data` length [`crate::apply::apply_module`] + /// demands is `bounds_len`, not this. + pub len: u64, + /// The exact union length the apply engine checks against. + pub bounds_len: u64, + /// Template copies (one per data segment with file bytes); the rest of + /// the mapping is bss and stays zero. + pub copies: [Option; MAX_SEGMENTS], +} + +/// The plan: where the code run and the data union sit relative to the +/// load base, and which bytes fill them. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct LateLayout { + /// Image-relative start of the code run (page-aligned). + pub code_vaddr: u64, + /// Code run length (page-multiple) — the arena lease's length. + pub code_len: u64, + /// Text/rodata copies into the arena, `dst` relative to `code_vaddr`. + pub code_copies: [Option; MAX_SEGMENTS], + /// The writable half, or `None` for an object with no data at all. + pub data: Option, + /// Image-relative one-past-the-end of everything placed — what the + /// next load base must clear. + pub span: u64, +} + +impl LateLayout { + /// Derive the plan from a checked image. `max_code` bounds the arena + /// lease, `max_span` the whole placement; both are the caller's + /// supply/window budgets, refused as [`LateError::TooLarge`]. + pub fn plan(checked: &CheckedImage, max_code: u64, max_span: u64) -> Result { + let d: &ModuleDescriptor = &checked.descriptor; + if d.kind != kind::LIBRARY { + return Err(LateError::NotLibrary); + } + if d.tls.memsz.get() != 0 || d.live_segs().iter().any(|s| s.kind == seg::KIND_TLS) { + return Err(LateError::Tls); + } + + // The code run: every text/rodata segment, page-rounded. + let mut code_start: Option = None; + let mut code_end: u64 = 0; + let mut has_text = false; + for s in d.live_segs() { + if s.kind != seg::KIND_TEXT && s.kind != seg::KIND_RODATA { + continue; + } + has_text |= s.kind == seg::KIND_TEXT; + let start = s.vaddr.get(); + let end = start + .checked_add(s.memsz.get().max(s.filesz.get())) + .ok_or(LateError::Layout)?; + code_start = Some(code_start.map_or(start, |at| at.min(start))); + code_end = code_end.max(end); + } + let (Some(code_start), true) = (code_start, has_text) else { + return Err(LateError::NoCode); + }; + let code_vaddr = align_down(code_start); + let code_end = align_up(code_end)?; + let code_len = code_end.checked_sub(code_vaddr).ok_or(LateError::Layout)?; + if code_len > max_code { + return Err(LateError::TooLarge); + } + + // Copies into the arena: each text/rodata carve window at its + // segment's offset from the run start. + let mut code_copies = [None; MAX_SEGMENTS]; + for (i, s) in d.live_segs().iter().enumerate() { + if s.kind != seg::KIND_TEXT && s.kind != seg::KIND_RODATA { + continue; + } + let carve = checked + .carve + .get(i) + .copied() + .flatten() + .ok_or(LateError::Layout)?; + if carve.kind != s.kind { + return Err(LateError::Layout); + } + let dst = s + .vaddr + .get() + .checked_sub(code_vaddr) + .ok_or(LateError::Layout)?; + if dst.checked_add(carve.len).ok_or(LateError::Layout)? > code_len { + return Err(LateError::Layout); + } + code_copies[i] = Some(Copy { + dst, + file_off: carve.file_off, + len: carve.len, + }); + } + + // The writable half, above the code run. + let data = match data_bounds(d).map_err(|_| LateError::Layout)? { + None => None, + Some((start, bounds_len)) => { + if start < code_end { + return Err(LateError::DataBelowCode); + } + if start % PAGE_SIZE != 0 { + return Err(LateError::Layout); + } + let len = align_up(bounds_len)?; + let mut copies = [None; MAX_SEGMENTS]; + for (i, s) in d.live_segs().iter().enumerate() { + if s.kind != seg::KIND_DATA || s.filesz.get() == 0 { + continue; + } + let carve = checked + .carve + .get(i) + .copied() + .flatten() + .ok_or(LateError::Layout)?; + let dst = s.vaddr.get().checked_sub(start).ok_or(LateError::Layout)?; + if dst.checked_add(s.filesz.get()).ok_or(LateError::Layout)? > bounds_len { + return Err(LateError::Layout); + } + copies[i] = Some(Copy { + dst, + file_off: carve.file_off, + len: s.filesz.get(), + }); + } + Some(DataSpan { + vaddr: start, + len, + bounds_len, + copies, + }) + } + }; + + if let Some((off, len)) = checked.relro { + let data = data.as_ref().ok_or(LateError::Relro)?; + let end = off.checked_add(len).ok_or(LateError::Relro)?; + let data_end = data.vaddr.checked_add(data.len).ok_or(LateError::Relro)?; + if len == 0 + || off % PAGE_SIZE != 0 + || len % PAGE_SIZE != 0 + || off < data.vaddr + || end > data_end + { + return Err(LateError::Relro); + } + } + let span = match data { + Some(ref span) => span.vaddr.checked_add(span.len).ok_or(LateError::Layout)?, + None => code_end, + }; + if span > max_span { + return Err(LateError::TooLarge); + } + Ok(Self { + code_vaddr, + code_len, + code_copies, + data, + span, + }) + } +} + +const fn align_down(v: u64) -> u64 { + v & !(PAGE_SIZE - 1) +} + +fn align_up(v: u64) -> Result { + v.checked_add(PAGE_SIZE - 1) + .map(align_down) + .ok_or(LateError::Layout) +} diff --git a/native/crates/ld/src/lib.rs b/native/crates/ld/src/lib.rs new file mode 100644 index 0000000..5c5c5d0 --- /dev/null +++ b/native/crates/ld/src/lib.rs @@ -0,0 +1,63 @@ +//! # kore-ld — the kore linker library (`res/IMAGE.md` §5; epic #291, P2 #293) +//! +//! Parse → plan → apply over ELF64 images, shaped by three constraints: +//! +//! - **no_std, no-alloc**: every buffer is fixed or caller-provided; the +//! image service's serve step and the startup stub both run inside +//! allocation-free contexts. +//! - **Semi-trusted input**: `check`/`parse` face bytes an adversary may +//! have authored. Malformed, truncated, overlapping, or lying +//! headers *refuse* with a typed error — they never panic, never +//! over-read, and never let a hostile image reach a lease. +//! - **Capability-shaped apply**: relocation writes go through the +//! caller's *own mapped, writable segment slices* — a `&mut [u8]` is +//! the authority. There is no raw-address mapper, no ambient write, +//! and text is never a write target (`DT_TEXTREL` refuses upstream). +//! +//! Ancestry: the parse layer descends from kraph's `klink` (the same +//! `elf`-crate substrate); the apply layer is new — klink's classical +//! half (raw-address `MemoryMapper`, RWX protection constants, +//! protect-before-relocate ordering, `#[cfg(target_arch)]` reloc +//! collapse) is exactly what `res/IMAGE.md` retires. +//! +//! Consumers: the **image service** calls [`check`] at `Register` (the +//! validation gate + the [`ModuleDescriptor`](kore_abi::image::ModuleDescriptor) +//! carve); the **startup stub** calls [`apply`] per module after mapping +//! its shares (map-RW → relocate → RELRO-seal — the seal itself is the +//! stub's verb, not kore-ld's). +//! +//! Arch dispatch is the [`reloc::RelocArch`] trait seam — one engine, +//! three data impls, selected by type instantiation. All three arches +//! compile everywhere (the service validates any-arch images; the host +//! test suite exercises all three tables); nothing here is `#[cfg]`-ed. +//! +//! ## The unsafe charter +//! +//! Every input-facing module (`parse`/`check`/`apply`/`link`/`reloc`) +//! is `unsafe`-free under the crate-wide `deny` — hostile bytes never +//! meet a raw pointer. The one audited exception is [`store`] (Phase +//! A, `res/USERLAND.md` §7): the caller-arena capacity plumbing +//! (`core::alloc::Allocator` is an unsafe-to-implement contract, and a +//! fixed-capacity table over an allocator block is raw-pointer +//! bookkeeping by nature). It never touches image bytes — capacity, +//! not content. + +#![no_std] +#![deny(unsafe_code)] +#![feature(allocator_api)] + +#[cfg(test)] +extern crate std; + +pub mod apply; +pub mod check; +pub mod functions; +pub mod late; +pub mod link; +pub mod parse; +pub mod place; +pub mod reloc; +pub mod stage; +#[allow(unsafe_code)] +pub mod store; +pub mod stub; diff --git a/native/crates/ld/src/link.rs b/native/crates/ld/src/link.rs new file mode 100644 index 0000000..6cd34bd --- /dev/null +++ b/native/crates/ld/src/link.rs @@ -0,0 +1,1242 @@ +//! The startup link engine — the in-process linker stub's brain +//! (`res/IMAGE.md` §4 phase 2, §8; epic #291, P5 #296), generic over a +//! **world seam** ([`LinkWorld`]). +//! +//! ## Shape +//! +//! One task, once, at startup: acquire the `DT_NEEDED` closure breadth- +//! first from the executable, place every module at world-chosen bases, +//! map shared segments (text R|X, rodata R), copy data templates into +//! private RW memory, eagerly relocate through [`crate::apply`], seal +//! each RELRO window, build the static TLS block and set the thread +//! pointer, surface the `DT_INIT_ARRAY`/`DT_FINI_ARRAY` pointers in +//! dependency order, and leave the read-only passive-ABI +//! [`ModulePage`] behind. The yield is a +//! [`LinkedProgram`]; the *caller* (P7's crt0) runs the init pointers +//! and jumps to `entry` — the engine never executes child code. +//! +//! ## The world seam +//! +//! Every side effect goes through [`LinkWorld`] — the capability-shaped +//! replacement for a classical linker's ambient-authority mapper. Ops +//! are **capability-moving**: a per-segment lease handle is affine +//! (mapping consumes it), placement is the world's act ([`choose_base`] +//! — per-task ASLR lives there, the engine only states size/alignment), +//! and the engine's write authority is exactly the RW windows the world +//! handed it. On real kore the world is the child-side session-verb +//! adapter (`Acquire` over `role::LOADER`, share/allocate/unmap/seal +//! session verbs); on the host it is a recording mock — the engine +//! cannot tell, which is the point. +//! +//! Where the executable comes from is also the world's business: the +//! engine calls [`acquire`] for it like any module, and the real world +//! answers *that one* name from the spawn endowment (`.kore.ld`'s +//! descriptor + the `role::IMAGE` lease caps) instead of the loader +//! channel — the degenerate static case (`res/IMAGE.md` §4) falls out +//! of the same seam. +//! +//! ## Regime (enforced, not extended) +//! +//! - Bounded everything: ≤ [`MAX_LINK_MODULES`] modules (refused, never +//! truncated), ≤ [`MAX_INIT_FNS`] init/fini pointers, fixed tables, +//! no allocation. +//! - Dedup by [`ImageName`]: a diamond's shared base loads **once**. +//! - A `DT_NEEDED` **cycle refuses** ([`LinkError::DependencyCycle`]): +//! under eager binding with init ordering there is no lawful order +//! for a cycle to run in, so it is a foreign layout, not a case. +//! - Resolution scope: executable first, then libraries in BFS load +//! order (module 0 first is **load-bearing**: the exe's local-exec +//! TLS offsets assume its block is the variant's first). +//! - Init order: dependency-before-dependent (libC before libA/libB +//! before the exe), ties broken by load order; `fini` is the exact +//! reverse. +//! - Map-RW → relocate → seal, never protect-before-relocate; the seal +//! range is the wire descriptor's `relro_off/relro_sz` rebased by +//! [`crate::apply`]. +//! +//! [`acquire`]: LinkWorld::acquire +//! [`choose_base`]: LinkWorld::choose_base + +use core::alloc::Allocator; + +use elf::abi::{DT_NEEDED, DT_NULL}; +use heapless::Vec as HVec; +use kore_abi::image::{ + ImageName, MAX_MODULE_RECORDS, MAX_SEGMENTS, ModuleDescriptor, ModulePage, ModuleRecord, + ModuleSeg, SegmentDesc, kind, seg, +}; +use zerocopy::IntoBytes; + +use crate::apply::{ + ApplyError, MAX_MODULES, ModuleImage, ModuleView, SymbolScope, TlsLayout, apply_module, +}; +use crate::parse::PAGE_SIZE; +use crate::reloc::{RelocArch, RelocKind, TlsRules}; +use crate::store::Slots; + +/// Load-set **structural ceiling** — the BootBlock-budget scale (16 +/// bootstrap entries, `res/IMAGE.md` §4), the width of the dependency +/// bitmasks, and the passive page's record capacity. The *working* +/// capacity is the caller's, per link ([`LinkCaps::max_modules`], `1..=` +/// this) — the tables are arena-carved to the caps, never to the +/// ceiling (Phase A, `res/USERLAND.md` §7). +pub const MAX_LINK_MODULES: usize = 16; + +const _: () = { + assert!(MAX_LINK_MODULES <= MAX_MODULE_RECORDS); + assert!(MAX_LINK_MODULES <= MAX_MODULES); + // The dependency masks are u16 — the bound may not outgrow them. + assert!(MAX_LINK_MODULES <= 16); +}; + +/// Init/fini pointer **structural ceiling** across the whole load set +/// (the [`LinkedProgram`] return arrays' inline capacity); the working +/// bound is [`LinkCaps::max_inits`] (`1..=` this) — overflow refuses +/// ([`LinkError::TooManyInits`]). +pub const MAX_INIT_FNS: usize = 32; + +/// The caller's **capacity decision** for one link (Phase A, +/// `res/USERLAND.md` §7): how many modules and init/fini pointers this +/// program may carry. The engine's tables are allocated from the +/// caller's arena at exactly these sizes — a two-module static program +/// pays for two modules' tables, not the ceiling's sixteen. Both dials +/// are validated against the structural ceilings +/// ([`MAX_LINK_MODULES`]/[`MAX_INIT_FNS`]); out-of-range caps refuse +/// [`LinkError::Scratch`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct LinkCaps { + /// Modules the load set may reach (`1..=MAX_LINK_MODULES`). + pub max_modules: usize, + /// Init/fini pointers across the set (`1..=MAX_INIT_FNS`). + pub max_inits: usize, +} + +impl LinkCaps { + /// The ceilings themselves — the pre-parameterization behavior. + pub const DEFAULT: Self = Self { + max_modules: MAX_LINK_MODULES, + max_inits: MAX_INIT_FNS, + }; + + fn valid(&self) -> bool { + (1..=MAX_LINK_MODULES).contains(&self.max_modules) + && (1..=MAX_INIT_FNS).contains(&self.max_inits) + } +} + +/// One `.dynamic` entry (ELF64) — the `DT_NEEDED` walk's stride. +const DYN_ENTRY: u64 = 16; + +/// What one [`LinkWorld::acquire`] yields: the frozen descriptor plus at +/// most one lease per descriptor segment, parallel to `descriptor.segs`. +pub type Acquired = (ModuleDescriptor, [Option; MAX_SEGMENTS]); + +/// Everything the engine may do to the world — the whole side-effect +/// surface of startup linking, capability-shaped (module docs). `Lease` +/// handles are **affine**: [`map_image`](Self::map_image) and +/// [`read_segment`](Self::read_segment) consume them, exactly as the +/// real share-derive consumes its staged capability. +pub trait LinkWorld { + /// World-side failure detail (an acquire refusal, a map failure…). + /// The engine wraps it in [`LinkError::World`] — typed fail-closed + /// end to end. + type Error; + /// One granted segment-lease capability (kore-rt: an executed + /// `LeasedToken`). + type Lease; + /// A mapped read view — durable for [`map_image`](Self::map_image) + /// (it *is* the segment's presence in the address space), transient + /// for [`read_segment`](Self::read_segment) (dropping it is the + /// unmap). + type Ro: AsRef<[u8]>; + /// A private zeroed RW window of **exactly** the requested length — + /// the engine's only write authority. Dropping the handle releases + /// nothing: the memory stays the task's (data regions, TLS, the + /// passive page all outlive the stub). + type Rw: AsRef<[u8]> + AsMut<[u8]>; + + /// One `Acquire` round-trip: name → the frozen descriptor + at most + /// one lease per descriptor segment, **parallel to + /// `descriptor.segs`** (the service's stash order). A refusal + /// grants nothing. + fn acquire(&mut self, name: &ImageName) -> Result, Self::Error>; + + /// Choose a fresh span: `len` bytes at an address satisfying `align` + /// (a power of two). Placement policy — ASLR, guard gaps, layout — + /// is entirely the world's; the engine re-checks the alignment and + /// otherwise trusts nothing else about the value. + fn choose_base(&mut self, len: u64, align: u64) -> Result; + + /// Durably share a segment lease at exactly `va` with `access` + /// ([`seg`] bits). The engine only ever asks `R|X` (text) or `R` + /// (rodata) — never `W`; a world may (and the mock does) refuse a + /// writable ask outright, the wire's W^X backstop. + fn map_image( + &mut self, + lease: Self::Lease, + va: u64, + access: u8, + ) -> Result; + + /// Transiently map a lease read-only wherever the world likes — the + /// copy source for data templates and TLS init images. + fn read_segment(&mut self, lease: Self::Lease) -> Result; + + /// Retire a transient view after its bytes have been copied. + /// + /// The default preserves the simple host-world contract: dropping the + /// handle performs (or models) the unmap. A session-backed world may queue + /// that affine retirement until [`finalize_link`](Self::finalize_link), + /// amortizing several independent revokes into one crossing. No placement + /// may reuse the VA before finalization. + fn retire_read(&mut self, view: Self::Ro) -> Result<(), Self::Error> { + drop(view); + Ok(()) + } + + /// Fresh private **zeroed** RW memory at exactly `va`, `len` bytes + /// (the kernel's zeroed-delegation discipline: no stale bytes reach + /// a task). `.bss` and `tbss` are the zeroing, not a memset here. + fn alloc_private(&mut self, va: u64, len: u64) -> Result; + + /// Retain immutable initialization bytes for future native threads before + /// any initial-thread TCB state is written. Worlds without thread creation + /// may discard this; their runtime must continue refusing TLS child spawn. + fn retain_tls_template( + &mut self, + _bytes: &[u8], + _tp_offset: u64, + _align: u64, + ) -> Result<(), Self::Error> { + Ok(()) + } + + /// Downgrade `[va, va + len)` to read-only — the RELRO seal and the + /// passive-page seal. Called only after every write into the range + /// has happened (the engine's ordering obligation). + fn seal_ro(&mut self, va: u64, len: u64) -> Result<(), Self::Error>; + + /// Install the initial thread pointer (the arch's TP register, via + /// whatever gate the world has — `res/IMAGE.md` §9g). + fn set_tp(&mut self, tp: u64) -> Result<(), Self::Error>; + + /// Commit every final protection change and, when present, install the + /// initial thread pointer as the last effect. + /// + /// The conservative default retains the original one-operation calls. + /// Metal worlds can issue the entire bounded list as one session after all + /// relocation/copy writes are complete. `set_tp` is deliberately last: + /// subsequent runtime verbs may no longer be able to address their old + /// per-thread transport block. + fn finalize_link(&mut self, seals: &[(u64, u64)], tp: Option) -> Result<(), Self::Error> { + for &(va, len) in seals { + self.seal_ro(va, len)?; + } + if let Some(tp) = tp { + self.set_tp(tp)?; + } + Ok(()) + } + + /// Where the passive-ABI page (`res/IMAGE.md` §8) goes. The default + /// is one more placement; a world whose task will read the page back + /// later — the startup stub, for the late loader — returns the one + /// **fixed** address the ABI names for it (`geom::LINK`'s last page), + /// because nothing else records where the page went. + fn module_page_base(&mut self, len: u64) -> Result { + self.choose_base(len, len) + } + + /// One past the highest address this world's placement has handed + /// out — every module, the TLS block, everything — or `None` for a + /// world that does not record placement (the test world). + /// + /// The engine stamps it into the passive page + /// (`ModulePage::placed`) so a later placer starts above what the + /// stub *mapped*, not only above what the page's records *name*. + fn placed_high_water(&self) -> Option { + None + } + + /// Provenance id of a lease for the passive ABI + /// ([`ModuleSeg::lease_id`]) — correlation, never authority; worlds + /// without one report 0. + fn lease_id(&self, lease: &Self::Lease) -> u32 { + let _ = lease; + 0 + } +} + +/// Why the link refused. Total failure: any error leaves no +/// half-linked program (mappings already made die with the task that +/// cannot start — the spawn session's failure path). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LinkError { + /// A world op failed (acquire refusal, map/alloc/seal failure) — + /// the world's own detail rides along. + World(E), + /// The `DT_NEEDED` closure exceeds [`MAX_LINK_MODULES`]. + TooManyModules, + /// The dependency graph has a cycle — no lawful init order exists. + DependencyCycle, + /// A module's descriptor lies: fails `validate()`, wrong machine, + /// wrong kind (library where an executable belongs or vice versa), + /// segment access outside the §2 table, or geometry that cannot be + /// placed. Carries the module's load index. + BadModule(usize), + /// A live descriptor segment arrived without its lease (the grant + /// budget promises one per segment). + MissingLease(usize), + /// The mapped `.dynamic`/strtab contents contradict the descriptor + /// (ranges escape the views, `DT_NEEDED` count mismatch). + BadDynamic(usize), + /// An init/fini entry lies outside the load set's executable text. + Function(crate::functions::FunctionError, usize), + /// A `DT_NEEDED` name does not fit [`ImageName`] (empty, > 64 + /// bytes, or embedded NUL). + NeededName(usize), + /// The relocation engine refused this module. + Apply(ApplyError, usize), + /// The static TLS layout or the block copy refused (lying geometry). + Tls(ApplyError), + /// More init/fini pointers than [`MAX_INIT_FNS`]. + TooManyInits, + /// Address arithmetic on the chosen base overflowed, or the world + /// returned a base violating the requested alignment. + Layout(usize), + /// The caller's scratch refused: the [`LinkCaps`] were out of range, + /// or the arena could not back the engine's tables — size the buffer + /// (or the caps) to the load set (Phase A, `res/USERLAND.md` §7). + Scratch, +} + +/// What the stub hands the real crt0: everything left to *do* after the +/// engine went dormant. Pointers are **absolute** in the linked task's +/// address space; the engine has executed none of them. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LinkedProgram { + /// The executable's absolute entry point (the jump target). + pub entry: u64, + /// The initial thread-pointer value for [`LinkWorld::set_tp`]'s + /// register (also recorded here for the caller); 0 when the load + /// set carries no TLS at all (no block was built, `set_tp` never + /// ran). + pub tp: u64, + /// Static TLS block size per thread (variant-shaped: + /// [`TlsLayout::total`]) — the static layout used by thread spawning. 0 = no TLS. + pub tls_total: u64, + /// Strictest TLS block alignment (≥ 1). + pub tls_align: u64, + /// Modules linked (the passive page's `count`). + pub module_count: usize, + /// Absolute VA of the sealed passive-ABI [`ModulePage`]. + pub module_page: u64, + /// `DT_INIT_ARRAY` pointers across the load set, **dependency- + /// before-dependent** (crt0 runs them in this order, then jumps). + pub init: HVec, + /// `DT_FINI_ARRAY` pointers, dependent-before-dependency (the exact + /// reverse walk) — collected now because the descriptors die with + /// the stub and nothing else records them. + pub fini: HVec, +} + +/// The loaded-module tables — the engine's read half during apply +/// (borrowed immutably while one module's private data is written). +/// Capacity is [`LinkCaps::max_modules`], arena-carved (Phase A). +struct Loaded { + names: Slots, + descs: Slots, + bases: Slots, + /// Durable read windows, parallel to each descriptor's `segs` + /// (text/rodata only — data is private, TLS is transient). + ro: Slots<[Option; MAX_SEGMENTS], A>, + /// Lease provenance ids, parallel to `segs` ([`ModuleSeg::lease_id`]). + ids: Slots<[u32; MAX_SEGMENTS], A>, + /// Dependency bitmask: bit `j` set in `needs[i]` = module `i` names + /// module `j` in `DT_NEEDED` (the init-order/cycle input). Inline — + /// the mask *width* is the [`MAX_LINK_MODULES`] pin itself. + needs: [u16; MAX_LINK_MODULES], + /// The caps this table set was carved for (`names.len()` etc.). + cap: usize, + count: usize, +} + +impl Loaded { + fn new(cap: usize, alloc: A) -> Result> { + Ok(Self { + names: Slots::new_from(cap, alloc, |_| ImageName::empty()) + .map_err(|_| LinkError::Scratch)?, + descs: Slots::new_from(cap, alloc, |_| ModuleDescriptor::empty()) + .map_err(|_| LinkError::Scratch)?, + bases: Slots::new_from(cap, alloc, |_| 0).map_err(|_| LinkError::Scratch)?, + ro: Slots::new_from(cap, alloc, |_| core::array::from_fn(|_| None)) + .map_err(|_| LinkError::Scratch)?, + ids: Slots::new_from(cap, alloc, |_| [0; MAX_SEGMENTS]) + .map_err(|_| LinkError::Scratch)?, + needs: [0; MAX_LINK_MODULES], + cap, + count: 0, + }) + } + + /// The already-loaded module named `name`, if any (the dedup gate — + /// a diamond's shared base resolves here the second time). + fn find(&self, name: &ImageName) -> Option { + self.names.as_slice()[..self.count] + .iter() + .position(|n| n == name) + } +} + +/// Link one program: BFS the `DT_NEEDED` closure from `exe`, place, map, +/// copy, relocate, seal, lay out TLS, record. The module contract's +/// entry point; the whole sequence is the module docs' regime. +/// +/// Capacity is the **caller's** (Phase A, `res/USERLAND.md` §7): the +/// engine's tables are carved from `alloc` — arena-backed, typically a +/// [`LinkArena`](crate::store::LinkArena) over a buffer the stub owns — +/// at exactly `caps`' sizes. [`LinkCaps::DEFAULT`] reproduces the +/// pre-parameterization ceilings. +pub fn link_program( + world: &mut W, + exe: &ImageName, + alloc: S, + caps: LinkCaps, +) -> Result> { + if !caps.valid() { + return Err(LinkError::Scratch); + } + let mut lt: Loaded = Loaded::new(caps.max_modules, alloc)?; + let mut data: Slots, S> = + Slots::new_from(caps.max_modules, alloc, |_| None).map_err(|_| LinkError::Scratch)?; + let mut tls_leases: Slots, S> = + Slots::new_from(caps.max_modules, alloc, |_| None).map_err(|_| LinkError::Scratch)?; + + // ── Phase 1: the BFS closure (dedup by name, bounded) ─────────────── + add_module::( + world, + &mut lt, + &mut data, + &mut tls_leases, + exe, + Role::Executable, + )?; + let mut cursor = 0; + while cursor < lt.count { + walk_needed::(world, &mut lt, &mut data, &mut tls_leases, cursor)?; + cursor += 1; + } + let count = lt.count; + + // ── Phase 2: init order + the cycle gate ──────────────────────────── + let order = init_order(<.needs, count).ok_or(LinkError::DependencyCycle)?; + + // ── Phase 3: eager relocation + deferred RELRO seal ───────────────── + // Scope views borrow the read half; each module's private data is + // the one write target — map-RW → relocate → seal, never the + // reverse. A module's GOT is complete after its own apply (cross- + // module relocations write only into the applying module). Seal ranges + // are retained in a bounded list and committed together at the final + // world crossing; no user code runs in between. + let views: Slots, S> = Slots::new_from(count, alloc, |i| { + let mut ro: [Option<&[u8]>; MAX_SEGMENTS] = [None; MAX_SEGMENTS]; + for (k, w) in lt.ro[i].iter().enumerate() { + ro[k] = w.as_ref().map(|w| w.as_ref()); + } + ModuleView { + base: lt.bases[i], + descriptor: <.descs[i], + ro, + } + }) + .map_err(|_| LinkError::Scratch)?; + let scope = SymbolScope { + modules: views.as_slice(), + }; + let tls_layout = TlsLayout::build::(views.as_slice(), alloc).map_err(LinkError::Tls)?; + + let mut final_seals: HVec<(u64, u64), { MAX_LINK_MODULES + 1 }> = HVec::new(); + for i in 0..count { + let d = <.descs[i]; + let relro = match d.dynz.relro_sz.get() { + 0 => None, + sz => Some((d.dynz.relro_off.get(), sz)), + }; + let mut module = ModuleImage { + scope_index: i, + view: views[i], + data: data[i].as_mut().map(|w| w.as_mut()).unwrap_or(&mut []), + relro, + }; + let applied = apply_module::(&mut module, &scope, &tls_layout) + .map_err(|e| LinkError::Apply(e, i))?; + if let Some((va, len)) = applied.relro { + final_seals + .push((va, len)) + .map_err(|_| LinkError::TooManyModules)?; + } + } + + // ── Phase 4: the static TLS block + TP ────────────────────────────── + // The layout (tp-relative offsets) fed the relocations above; the + // block itself is per-thread memory built once here for the initial + // thread. V1 quirk guarded: an empty layout still reports the TCB + // reserve as `total`, so presence is "any module has TLS". + let have_tls = (0..count).any(|i| lt.descs[i].tls.memsz.get() > 0); + let mut tp = 0u64; + if have_tls { + tp = build_tls_block::(world, <, &mut tls_leases, &tls_layout, count)?; + // The INSTALL is deferred to the very end of linking (after + // phase 6): on a session-backed world the thread-pointer move + // takes effect immediately, and every later world verb would + // dereference the NEW `%fs` — which no longer names the thread + // block the trap transport reads its scratch through. The + // layout, not the live TP, feeds the relocations above, so + // nothing before the tail observes the difference. + } + + // ── Phase 5: init/fini pointers, dependency-ordered ───────────────── + // Read from the relocated private copies: the array words are + // absolute function addresses by now. Sentinel entries (0 and + // ~0 — legacy crt glue) are skipped, as every runtime linker does. + let mut init: HVec = HVec::new(); + let mut fini: HVec = HVec::new(); + for &i in order[..count].iter() { + let d = <.descs[i as usize]; + collect_fns( + <, + &data, + i as usize, + d.dynz.init_array_off.get(), + d.dynz.init_array_sz.get(), + &mut init, + caps.max_inits, + )?; + } + for &i in order[..count].iter().rev() { + let d = <.descs[i as usize]; + let first = fini.len(); + collect_fns( + <, + &data, + i as usize, + d.dynz.fini_array_off.get(), + d.dynz.fini_array_sz.get(), + &mut fini, + caps.max_inits, + )?; + fini[first..].reverse(); + } + + // ── Phase 6: the passive ABI (`res/IMAGE.md` §8) ──────────────────── + let page_va = write_module_page::(world, <, count)?; + final_seals + .push((page_va, PAGE_SIZE)) + .map_err(|_| LinkError::TooManyModules)?; + + // The deferred TP install (phase 4's tail): the LAST world action, so + // no verb ever runs under the moved thread pointer. The program + // entered next reads its TLS through exactly this value. + world + .finalize_link(&final_seals, have_tls.then_some(tp)) + .map_err(LinkError::World)?; + + let entry = lt.bases[0] + .checked_add(lt.descs[0].entry.get()) + .ok_or(LinkError::Layout(0))?; + + Ok(LinkedProgram { + entry, + tp, + tls_total: if have_tls { tls_layout.total } else { 0 }, + tls_align: if have_tls { tls_layout.align } else { 1 }, + module_count: count, + module_page: page_va, + init, + fini, + }) +} + +/// What part a name plays in the load graph — the **caller's** knowledge, +/// which is the graph edge it walked to reach that name. +/// +/// Which descriptor *kinds* satisfy a role is the engine's rule, not the +/// caller's, and the two are not one-to-one: an executable slot admits +/// both addressing conventions — [`kind::EXECUTABLE`], the PIE whose +/// addresses are image-relative, and [`kind::STATIC_EXECUTABLE`], the +/// fixed-address image every founded row and the linker stub itself are — +/// while a `DT_NEEDED` name admits only a library. +/// +/// Naming the kind directly (as this once did) collapsed the two and made +/// the static case unrepresentable: the exe slot could name exactly one +/// convention, so a descriptor declaring the other was refused as a +/// foreign graph. Fail-closed, and wrong — `res/IMAGE.md` §4's degenerate +/// static case is precisely a `STATIC_EXECUTABLE` in the exe slot. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Role { + /// The program being linked. + Executable, + /// A name reached through `DT_NEEDED`. + Library, +} + +impl Role { + /// Whether a descriptor of `kind` may fill this role. An unknown + /// kind fills neither: the engine acts on the convention a descriptor + /// declares, and one it cannot name is not one it may guess. + const fn admits(self, kind: u8) -> bool { + match self { + Role::Executable => matches!(kind, kind::EXECUTABLE | kind::STATIC_EXECUTABLE), + Role::Library => kind == kind::LIBRARY, + } + } +} + +/// Acquire, validate, place, and map one module (BFS's per-node work). +/// Returns the module's load index. +fn add_module( + world: &mut W, + lt: &mut Loaded, + data: &mut Slots, S>, + tls_leases: &mut Slots, S>, + name: &ImageName, + role: Role, +) -> Result> { + if lt.count >= lt.cap { + return Err(LinkError::TooManyModules); + } + let idx = lt.count; + let (desc, mut leases) = world.acquire(name).map_err(LinkError::World)?; + + // The cheap consumer re-check (`apply` re-checks again — defense in + // depth): stamp, machine, and the *role* this name plays in the + // graph. A library with an entry point or an exe named in + // DT_NEEDED is a foreign graph, refused. + if !desc.validate() || desc.machine.get() != A::MACHINE || !role.admits(desc.kind) { + return Err(LinkError::BadModule(idx)); + } + let live = desc.seg_count as usize; + // The grant budget promises exactly one lease per live segment; a + // stray extra grant marks a lying world. + if leases[live..].iter().any(Option::is_some) { + return Err(LinkError::BadModule(idx)); + } + + // Address-space span: mappable segments only (KIND_TLS is a copy + // template, never a mapping — its geometry is per-thread, not + // address-space). + let mut span = 0u64; + let mut has_tls_seg = false; + for s in desc.live_segs() { + if s.kind == seg::KIND_TLS { + has_tls_seg = true; + continue; + } + let end = s + .vaddr + .get() + .checked_add(s.memsz.get()) + .and_then(|e| align_up(e, PAGE_SIZE)) + .ok_or(LinkError::BadModule(idx))?; + span = span.max(end); + } + if span == 0 { + return Err(LinkError::BadModule(idx)); + } + // Only nonempty initialization bytes need a source lease. Pure .tbss + // still allocates a private zeroed block through the TLS descriptor. + if desc.tls.init_sz.get() > desc.tls.memsz.get() + || (desc.tls.init_sz.get() > 0) != has_tls_seg + || desc.live_segs().iter().any(|s| { + s.kind == seg::KIND_TLS + && (s.filesz.get() != desc.tls.init_sz.get() + || s.memsz.get() != desc.tls.memsz.get()) + }) + { + return Err(LinkError::BadModule(idx)); + } + + // Placement is the world's act — for an image-relative module. An + // **absolute** descriptor's link address IS its runtime address + // ([`kind::is_absolute`]), so there is no base to choose and the + // world is not asked: its answer would be a perfectly legitimate + // address that maps the image somewhere its own relocationless code + // does not believe it is. Nothing later distinguishes that from a + // correct placement — the segments land, the seal succeeds, and the + // first absolute reference reads whatever else lives there. + // + // Zero rather than "the descriptor's own low address" because the + // engine adds `base + s.vaddr` uniformly below; the convention is + // carried by the base, not by a second code path. + let base = if kind::is_absolute(desc.kind) { + 0 + } else { + world + .choose_base(span, PAGE_SIZE) + .map_err(LinkError::World)? + }; + if base % PAGE_SIZE != 0 || base.checked_add(span).is_none() { + return Err(LinkError::Layout(idx)); + } + + // Every writable LOAD is realized inside one private, zeroed region. + // The union preserves the engine's single write authority; individual + // templates retain their own descriptor bounds and source leases. + let mut data_start: Option = None; + let mut data_end: u64 = 0; + for s in desc.live_segs().iter().filter(|s| s.kind == seg::KIND_DATA) { + data_start = Some(data_start.map_or(s.vaddr.get(), |at| at.min(s.vaddr.get()))); + data_end = data_end.max( + s.vaddr + .get() + .checked_add(s.memsz.get()) + .ok_or(LinkError::BadModule(idx))?, + ); + } + if let Some(start) = data_start { + let va = base.checked_add(start).ok_or(LinkError::Layout(idx))?; + let len = data_end + .checked_sub(start) + .ok_or(LinkError::BadModule(idx))?; + data[idx] = Some(world.alloc_private(va, len).map_err(LinkError::World)?); + } + + for k in 0..live { + let s: SegmentDesc = desc.segs[k]; + let lease = leases[k].take().ok_or(LinkError::MissingLease(idx))?; + lt.ids[idx][k] = world.lease_id(&lease); + let va = base + .checked_add(s.vaddr.get()) + .ok_or(LinkError::Layout(idx))?; + match s.kind { + // Text shares R|X off the X-blessed lease; rodata plain R. + // Access is pinned by kind, not echoed from the descriptor — + // a descriptor asking anything else (any W above all) lies. + seg::KIND_TEXT => { + if s.access != seg::ACCESS_R | seg::ACCESS_X { + return Err(LinkError::BadModule(idx)); + } + let w = world + .map_image(lease, va, seg::ACCESS_R | seg::ACCESS_X) + .map_err(LinkError::World)?; + lt.ro[idx][k] = Some(w); + } + seg::KIND_RODATA => { + if s.access != seg::ACCESS_R { + return Err(LinkError::BadModule(idx)); + } + let w = world + .map_image(lease, va, seg::ACCESS_R) + .map_err(LinkError::World)?; + lt.ro[idx][k] = Some(w); + } + // Each data template: transient read view → its declared window + // inside the module's single private data union. Bss remains + // zero from the allocation contract; validation permits no gaps. + seg::KIND_DATA => { + if s.access != seg::ACCESS_R { + return Err(LinkError::BadModule(idx)); + } + let template = world.read_segment(lease).map_err(LinkError::World)?; + let filesz = + usize::try_from(s.filesz.get()).map_err(|_| LinkError::BadModule(idx))?; + let start = data_start.ok_or(LinkError::BadModule(idx))?; + let dst_start = s + .vaddr + .get() + .checked_sub(start) + .and_then(|off| usize::try_from(off).ok()) + .ok_or(LinkError::BadModule(idx))?; + let dst_end = dst_start + .checked_add(filesz) + .ok_or(LinkError::BadModule(idx))?; + let src = template + .as_ref() + .get(..filesz) + .ok_or(LinkError::BadModule(idx))?; + data[idx] + .as_mut() + .ok_or(LinkError::BadModule(idx))? + .as_mut() + .get_mut(dst_start..dst_end) + .ok_or(LinkError::BadModule(idx))? + .copy_from_slice(src); + world.retire_read(template).map_err(LinkError::World)?; + } + // TLS init: held until the whole load set's layout exists + // (IE/LE legality is "the set is closed first"). + seg::KIND_TLS => { + if s.access != seg::ACCESS_R { + return Err(LinkError::BadModule(idx)); + } + tls_leases[idx] = Some(lease); + } + _ => return Err(LinkError::BadModule(idx)), + } + } + + lt.names[idx] = *name; + lt.descs[idx] = desc; + lt.bases[idx] = base; + lt.count += 1; + Ok(idx) +} + +/// Walk module `i`'s `PT_DYNAMIC` for `DT_NEEDED`, resolving each name +/// against the table (dedup) or loading it (BFS append). Names are +/// copied out before any mutation — the table's slots never move. +fn walk_needed( + world: &mut W, + lt: &mut Loaded, + data: &mut Slots, S>, + tls_leases: &mut Slots, S>, + i: usize, +) -> Result<(), LinkError> { + let dyn_off = lt.descs[i].dynz.dynamic_off.get(); + let dyn_sz = lt.descs[i].dynz.dynamic_sz.get(); + if dyn_sz % DYN_ENTRY != 0 { + return Err(LinkError::BadDynamic(i)); + } + let entries = dyn_sz / DYN_ENTRY; + let mut found: u16 = 0; + for e in 0..entries { + // Re-resolve per entry: the borrow must not outlive the + // `add_module` call a NEEDED entry may trigger. Checked walk — + // a lying `dynamic_off` must refuse, never wrap. + let at = e + .checked_mul(DYN_ENTRY) + .and_then(|o| dyn_off.checked_add(o)) + .ok_or(LinkError::BadDynamic(i))?; + let bytes = + module_bytes::(lt, data, i, at, DYN_ENTRY).ok_or(LinkError::BadDynamic(i))?; + let tag = i64::from_le_bytes( + bytes[..8] + .try_into() + .map_err(|_| LinkError::BadDynamic(i))?, + ); + let val = u64::from_le_bytes( + bytes[8..] + .try_into() + .map_err(|_| LinkError::BadDynamic(i))?, + ); + if tag == DT_NULL { + break; + } + if tag != DT_NEEDED { + continue; + } + let nm = needed_name::(lt, data, i, val).ok_or(LinkError::NeededName(i))?; + let j = match lt.find(&nm) { + Some(j) => j, + None => add_module::(world, lt, data, tls_leases, &nm, Role::Library)?, + }; + lt.needs[i] |= 1u16 << j; + found = found.checked_add(1).ok_or(LinkError::BadDynamic(i))?; + } + // The descriptor promised this count at Register; a mapped section + // saying otherwise marks a swapped or corrupt mapping. + if found != lt.descs[i].needed_count.get() { + return Err(LinkError::BadDynamic(i)); + } + Ok(()) +} + +/// The NUL-terminated strtab name at `off`, as an [`ImageName`]. +fn needed_name( + lt: &Loaded, + data: &Slots, S>, + i: usize, + off: u64, +) -> Option { + let strtab_off = lt.descs[i].dynz.strtab_off.get(); + let strtab_sz = lt.descs[i].dynz.strtab_sz.get(); + let avail = strtab_sz.checked_sub(off)?; + let tail = module_bytes::(lt, data, i, strtab_off.checked_add(off)?, avail)?; + let len = tail.iter().position(|&b| b == 0)?; + ImageName::new(core::str::from_utf8(&tail[..len]).ok()?) +} + +/// Resolve an image-relative range to mapped bytes: the module's +/// text/rodata read windows first (capped at `filesz` and the window's +/// real length), then its private data copy. A range nothing contains +/// refuses — a lying offset cannot escape the module's own memory. +fn module_bytes<'a, W: LinkWorld, S: Allocator + Copy>( + lt: &'a Loaded, + data: &'a Slots, S>, + i: usize, + off: u64, + len: u64, +) -> Option<&'a [u8]> { + let d = <.descs[i]; + for (k, s) in d.live_segs().iter().enumerate() { + let rel = match off.checked_sub(s.vaddr.get()) { + Some(r) => r, + None => continue, + }; + let end = rel.checked_add(len)?; + match s.kind { + seg::KIND_TEXT | seg::KIND_RODATA => { + let w = lt.ro[i].get(k)?.as_ref()?; + let bytes = w.as_ref(); + let avail = s.filesz.get().min(u64::try_from(bytes.len()).ok()?); + if end > avail { + continue; + } + return bytes.get(usize::try_from(rel).ok()?..usize::try_from(end).ok()?); + } + seg::KIND_DATA => { + if end > s.memsz.get() { + continue; + } + let bytes = data.as_slice().get(i)?.as_ref()?.as_ref(); + let data_start = d + .live_segs() + .iter() + .filter(|s| s.kind == seg::KIND_DATA) + .map(|s| s.vaddr.get()) + .min()?; + let union_rel = off.checked_sub(data_start)?; + let union_end = union_rel.checked_add(len)?; + return bytes + .get(usize::try_from(union_rel).ok()?..usize::try_from(union_end).ok()?); + } + _ => continue, + } + } + None +} + +/// Dependency-before-dependent order over the `needs` masks — Kahn's +/// walk, deterministic (lowest load index first among the ready), and +/// the cycle gate in one: `None` = no lawful order exists. +fn init_order(needs: &[u16; MAX_LINK_MODULES], count: usize) -> Option<[u8; MAX_LINK_MODULES]> { + let mut order = [0u8; MAX_LINK_MODULES]; + let mut emitted: u16 = 0; + let mut n = 0; + while n < count { + let ready = (0..count).find(|&i| emitted & (1 << i) == 0 && needs[i] & !emitted == 0)?; + order[n] = ready as u8; + emitted |= 1 << ready; + n += 1; + } + Some(order) +} + +/// Allocate the initial thread's static TLS block, copy every module's +/// init image into its layout slot, and yield the TP value. Variant +/// math per [`TlsRules`] (the psABI conventions [`TlsLayout`] pins): +/// +/// - **V2** (x86_64): blocks below `tp`; the window is +/// `align_up(total, align)` bytes of block + 8 bytes at `tp` holding +/// the TCB self-pointer (`%fs:0`); `tp = base + align_up(total, +/// align)`. +/// - **V1** (aarch64 tcb 16 / riscv64 tcb 0): `tp = base`; blocks above +/// at their positive offsets; the TCB reserve stays zeroed (no DTV +/// exists in this regime, ever). +/// +/// The allocation is over-aligned to `max(align, 16)` so `tp` satisfies +/// both the strictest block alignment and the ABI's own TP-alignment +/// floor. +fn build_tls_block( + world: &mut W, + lt: &Loaded, + tls_leases: &mut Slots, S>, + layout: &TlsLayout, + count: usize, +) -> Result> { + let align = layout.align.max(16); + let body = align_up(layout.total, align).ok_or(LinkError::Tls(ApplyError::Input))?; + // The block also carries the runtime's per-thread block at `tp` + // (`RUNTIME_TCB_BYTES`, res/IMAGE.md §7's TLS contract), so a linked + // program can reach its thread-locals *and* issue sessions. V2 reserves + // it above `tp`, past the static blocks that sit below; V1's reserve is + // already inside `body`, because `TlsLayout::build` started its cursor + // past it. + let (len, tp_off_in_block) = match A::TLS { + TlsRules::V2 => ( + body.checked_add(kore_abi::image::RUNTIME_TCB_BYTES) + .ok_or(LinkError::Tls(ApplyError::Input))?, + body, + ), + TlsRules::V1 { .. } => (body, 0), + }; + let base = world.choose_base(len, align).map_err(LinkError::World)?; + if base % align != 0 || base.checked_add(len).is_none() { + return Err(LinkError::Layout(0)); + } + let mut block = world.alloc_private(base, len).map_err(LinkError::World)?; + let tp = base + tp_off_in_block; + + for i in 0..count { + let d = <.descs[i]; + if d.tls.memsz.get() == 0 { + continue; + } + let init_sz = + usize::try_from(d.tls.init_sz.get()).map_err(|_| LinkError::Tls(ApplyError::Input))?; + if init_sz == 0 { + // Pure .tbss carries geometry but needs no initialization lease. + continue; + } + let lease = tls_leases[i].take().ok_or(LinkError::MissingLease(i))?; + let win = world.read_segment(lease).map_err(LinkError::World)?; + // The init image's offset inside the page-rounded lease window: + // `vaddr % page` — sound by the Register gate's page-congruence + // check (`crate::check`). + let tls_seg = d + .live_segs() + .iter() + .find(|s| s.kind == seg::KIND_TLS) + .ok_or(LinkError::Tls(ApplyError::Input))?; + let in_off = usize::try_from(tls_seg.vaddr.get() % PAGE_SIZE) + .map_err(|_| LinkError::Tls(ApplyError::Input))?; + let src = win + .as_ref() + .get( + in_off + ..in_off + .checked_add(init_sz) + .ok_or(LinkError::Tls(ApplyError::Input))?, + ) + .ok_or(LinkError::Tls(ApplyError::Input))?; + // The block slot: tp-relative offset folded into the window. + let off = layout + .tp_off(i, 0) + .ok_or(LinkError::Tls(ApplyError::Tls(i)))?; + let dst_off = i64::try_from(tp_off_in_block) + .ok() + .and_then(|tpo| tpo.checked_add(off)) + .and_then(|o| usize::try_from(o).ok()) + .ok_or(LinkError::Tls(ApplyError::Input))?; + block + .as_mut() + .get_mut( + dst_off + ..dst_off + .checked_add(init_sz) + .ok_or(LinkError::Tls(ApplyError::Input))?, + ) + .ok_or(LinkError::Tls(ApplyError::Input))? + .copy_from_slice(src); + world.retire_read(win).map_err(LinkError::World)?; + } + + world + .retain_tls_template(block.as_ref(), tp_off_in_block, align) + .map_err(LinkError::World)?; + + // V2's TCB self-pointer: `%fs:0` holds `tp` itself (psABI). + if matches!(A::TLS, TlsRules::V2) { + let at = usize::try_from(tp_off_in_block).map_err(|_| LinkError::Tls(ApplyError::Input))?; + block + .as_mut() + .get_mut(at..at + 8) + .ok_or(LinkError::Tls(ApplyError::Input))? + .copy_from_slice(&tp.to_le_bytes()); + } + Ok(tp) +} + +/// Append one module's init/fini array words (absolute by now — the +/// relocations ran first), skipping the 0 / ~0 sentinels. Bounded by +/// the caller's [`LinkCaps::max_inits`] dial (≤ the inline +/// [`MAX_INIT_FNS`] ceiling). +fn collect_fns( + lt: &Loaded, + data: &Slots, S>, + i: usize, + off: u64, + sz: u64, + out: &mut HVec, + max_inits: usize, +) -> Result<(), LinkError> { + if sz == 0 { + return Ok(()); + } + if sz % 8 != 0 { + return Err(LinkError::BadDynamic(i)); + } + let bytes = module_bytes::(lt, data, i, off, sz).ok_or(LinkError::BadDynamic(i))?; + let addresses = crate::functions::function_addresses(bytes, |address| { + lt.descs + .as_slice() + .iter() + .enumerate() + .any(|(index, descriptor)| { + crate::functions::contains_text(descriptor, lt.bases[index], address) + }) + }) + .map_err(|error| LinkError::Function(error, i))?; + for fn_addr in addresses { + if out.len() >= max_inits { + return Err(LinkError::TooManyInits); + } + out.push(fn_addr).map_err(|_| LinkError::TooManyInits)?; + } + Ok(()) +} + +/// Build, write, and seal the passive-ABI page (`res/IMAGE.md` §8): +/// one [`ModuleRecord`] per module, **placed** (absolute) geometry, the +/// private data copy recorded R|W (its RELRO subrange is already +/// sealed), KIND_TLS omitted — the record lists address-space presence, +/// and a TLS template's product is the per-thread block, not a mapping. +fn write_module_page( + world: &mut W, + lt: &Loaded, + count: usize, +) -> Result> { + use zerocopy::byteorder::little_endian::{U32, U64}; + let mut page = ModulePage::new(); + for i in 0..count { + let d = <.descs[i]; + let mut segs = [ModuleSeg::vacant(); MAX_SEGMENTS]; + let mut sc = 0u8; + for (k, s) in d.live_segs().iter().enumerate() { + if s.kind == seg::KIND_TLS { + continue; + } + let access = match s.kind { + seg::KIND_TEXT => seg::ACCESS_R | seg::ACCESS_X, + seg::KIND_RODATA => seg::ACCESS_R, + // The private copy actually installed is writable — the + // record reports the outcome, not the template's share. + _ => seg::ACCESS_R | seg::ACCESS_W, + }; + let entry = &mut segs[sc as usize]; + entry.vaddr = U64::new( + lt.bases[i] + .checked_add(s.vaddr.get()) + .ok_or(LinkError::Layout(i))?, + ); + entry.size = U64::new(align_up(s.memsz.get(), PAGE_SIZE).ok_or(LinkError::Layout(i))?); + entry.lease_id = U32::new(lt.ids[i][k]); + entry.kind = s.kind; + entry.access = access; + sc += 1; + } + let record = ModuleRecord { + name_hash: U64::new(lt.names[i].fnv1a()), + base: U64::new(lt.bases[i]), + // The entry means what the module's own convention says it + // means (`ModuleDescriptor::entry` — "read the kind before + // the address"). The absolute arm is not merely the + // relative one with a zero base: it states that the address + // rides through untouched, so a later change to how an + // absolute module is placed cannot silently start biasing an + // entry that was never an offset. + entry: U64::new(match d.kind { + kind::EXECUTABLE => lt.bases[i] + .checked_add(d.entry.get()) + .ok_or(LinkError::Layout(i))?, + kind::STATIC_EXECUTABLE => d.entry.get(), + // A library has nothing to transfer to. + _ => 0, + }), + seg_count: sc, + kind: d.kind, + reserved: [0; 6], + segs, + }; + // Unreachable by the MAX_LINK_MODULES ≤ MAX_MODULE_RECORDS pin; + // refused anyway — bounded emission, never silent. + if !page.push(record) { + return Err(LinkError::TooManyModules); + } + } + + let va = world + .module_page_base(PAGE_SIZE) + .map_err(LinkError::World)?; + if va % PAGE_SIZE != 0 { + return Err(LinkError::Layout(0)); + } + // Everything placed so far lies below the page: record how far, in + // pages below `va`. A world that reports a cursor above the page, off a + // page boundary, or further away than sixteen bits can count is lying + // about its own placement — refuse the link rather than stamp a floor + // a later placer would trust. + if let Some(high_water) = world.placed_high_water() { + let below = va.checked_sub(high_water).ok_or(LinkError::Layout(0))?; + if below % PAGE_SIZE != 0 { + return Err(LinkError::Layout(0)); + } + let pages = u16::try_from(below / PAGE_SIZE).map_err(|_| LinkError::Layout(0))?; + page.record_placed(pages); + } + let mut window = world + .alloc_private(va, PAGE_SIZE) + .map_err(LinkError::World)?; + let bytes = page.as_bytes(); + window + .as_mut() + .get_mut(..bytes.len()) + .ok_or(LinkError::Layout(0))? + .copy_from_slice(bytes); + drop(window); + Ok(va) +} + +// ============================================================================ +// Self-relocation (the P7 crt0 seam) +// ============================================================================ + +/// Why [`relocate_self`] refused. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SelfRelocError { + /// The rela table is not a whole number of entries. + Shape, + /// A non-`R_*_RELATIVE` entry (or one naming a symbol) — the + /// static-PIE stub may carry nothing else; anything else marks a + /// mislinked stub, refused before a single write. + Reloc(u32, usize), + /// `r_offset` escapes the image span. + Bounds(usize), + /// `base + addend` overflowed. + Overflow(usize), +} + +/// Apply a static-PIE image's own `R_*_RELATIVE` set — the one pass the +/// real stub must run over *itself* before calling anything relocatable +/// (`res/IMAGE.md` §4 phase 2's first step). P7's crt0 entry calls +/// this with its load base, its own `.rela.dyn` bytes, and a writable +/// view of its own image (`image[0]` at `load_base`; link base 0, so +/// `r_offset` indexes it directly); constructing those without a heap +/// or relocations is the ~20-line per-arch asm's job, **not** built +/// here. Host-tested over synthetic tables; returns the write count. +pub fn relocate_self( + load_base: u64, + rela: &[u8], + image: &mut [u8], +) -> Result { + if rela.len() % 24 != 0 { + return Err(SelfRelocError::Shape); + } + let mut applied = 0usize; + for (idx, e) in rela.chunks_exact(24).enumerate() { + let r_offset = u64::from_le_bytes(e[0..8].try_into().map_err(|_| SelfRelocError::Shape)?); + let r_info = u64::from_le_bytes(e[8..16].try_into().map_err(|_| SelfRelocError::Shape)?); + let addend = i64::from_le_bytes(e[16..24].try_into().map_err(|_| SelfRelocError::Shape)?); + let r_type = (r_info & 0xffff_ffff) as u32; + // RELATIVE only, symbol-free — everything else refuses. + if (r_info >> 32) != 0 || !matches!(A::classify(r_type), Ok(RelocKind::Relative)) { + return Err(SelfRelocError::Reloc(r_type, idx)); + } + let value = load_base + .checked_add_signed(addend) + .ok_or(SelfRelocError::Overflow(idx))?; + let at = usize::try_from(r_offset).map_err(|_| SelfRelocError::Bounds(idx))?; + image + .get_mut(at..at.checked_add(8).ok_or(SelfRelocError::Bounds(idx))?) + .ok_or(SelfRelocError::Bounds(idx))? + .copy_from_slice(&value.to_le_bytes()); + applied += 1; + } + Ok(applied) +} + +/// Round up to a power-of-two alignment; `None` on wrap. +fn align_up(v: u64, align: u64) -> Option { + let mask = align.checked_sub(1)?; + v.checked_add(mask).map(|x| x & !mask) +} diff --git a/native/crates/ld/src/parse.rs b/native/crates/ld/src/parse.rs new file mode 100644 index 0000000..fec007e --- /dev/null +++ b/native/crates/ld/src/parse.rs @@ -0,0 +1,853 @@ +//! ELF64 parsing over the `elf` crate — the klink-descended layer +//! (`res/IMAGE.md` §5): typed, bounds-checked views of a semi-trusted +//! image, refusing malformed input with [`ParseError`], never panicking +//! or over-reading. +//! +//! ## Contract (what [`crate::check`] consumes) +//! +//! - `parse_image(bytes, expected_machine) -> Result` +//! — validate `ElfBytes` (ELF64, little-endian, machine match, +//! **`ET_DYN` only** — `ET_EXEC` refuses: every kore image is PIE), +//! collect `PT_LOAD` segments (bounded by [`MAX_LOAD_SEGMENTS`]), +//! `PT_TLS`, and `PT_DYNAMIC`, each with `p_filesz <= p_memsz`, file +//! ranges inside `bytes`, and per-segment flag extraction. +//! - `walk_dynamic(...)` — the `DynamicInfo` gather: every `DT_*` tag +//! `res/IMAGE.md` §7 lists, with vaddr→file-offset resolution through +//! the collected load segments (a vaddr that resolves into no segment +//! refuses — a lying `.dynamic` cannot point the reader outside the +//! image). +//! +//! Segment slices borrow from the input image — zero-copy throughout; +//! the only fixed buffers are the `heapless::Vec` segment tables. + +use elf::ElfBytes; +use elf::abi::{ + DT_FINI_ARRAY, DT_FINI_ARRAYSZ, DT_FLAGS, DT_FLAGS_1, DT_GNU_HASH, DT_INIT_ARRAY, + DT_INIT_ARRAYSZ, DT_JMPREL, DT_NEEDED, DT_NULL, DT_PLTREL, DT_PLTRELSZ, DT_RELA, DT_RELAENT, + DT_RELASZ, DT_STRSZ, DT_STRTAB, DT_SYMENT, DT_SYMTAB, DT_TEXTREL, ET_DYN, ET_EXEC, PF_R, PF_W, + PF_X, PT_DYNAMIC, PT_GNU_RELRO, PT_LOAD, PT_TLS, +}; +use elf::endian::AnyEndian; +use elf::file::Class; +use heapless::Vec as HVec; + +/// Load-segment ceiling (klink's bound, kept: a legitimate PIE under +/// `-z separate-code` carries 3–5). +pub const MAX_LOAD_SEGMENTS: usize = 8; + +/// The carve granule (`res/IMAGE.md` §2): segment boundaries must sit on +/// 4 KiB pages or the in-place extent carve cannot honor them. +pub const PAGE_SIZE: u64 = 4096; + +// Packed-relative-relocation tags (`-z pack-relative-relocs`). The `elf` +// crate's abi module predates them; numbers are the generic ABI's +// (glibc `elf.h`): RELRSZ 35, RELR 36, RELRENT 37. +const DT_RELRSZ: i64 = 35; +const DT_RELR: i64 = 36; +const DT_RELRENT: i64 = 37; + +/// ELF64 fixed record sizes — the shapes the size-carrying `DT_*` tags +/// must divide by (a table that doesn't is lying about its contents). +const DYN_ENTRY_SIZE: u64 = 16; +const RELA_ENTRY_SIZE: u64 = 24; +const SYM_ENTRY_SIZE: u64 = 24; + +/// Why an image refused to parse. One vocabulary for the whole layer — +/// [`crate::check`] maps it onto `ImageRefusal::BadImage` with the +/// variant preserved for diagnostics. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ParseError { + /// Not an ELF the `elf` crate accepts (magic/header damage). + Header, + /// Not ELF64 little-endian. + Class, + /// `e_machine` differs from the arch this image was registered for. + Machine, + /// Not `ET_DYN` — kore images are PIE/shared-object only. + NotPie, + /// Not `ET_EXEC` — the bootstrap loader accepts only static executables. + NotExecutable, + /// Program headers missing or unreadable. + ProgramHeaders, + /// A segment's file range escapes the image bytes. + OutOfBounds, + /// `p_filesz > p_memsz`. + FileExceedsMem, + /// More than [`MAX_LOAD_SEGMENTS`] load segments. + TooManySegments, + /// No load segments at all. + NoSegments, + /// A `PT_LOAD` is not page-aligned (`-z separate-code` images always + /// are; misalignment breaks the in-place carve). + Misaligned, + /// The dynamic section is missing, malformed, or a tag's vaddr + /// resolves into no load segment. + Dynamic, + /// Overlapping load segments (a lying layout). + Overlap, + /// A bootstrap segment requests write and execute simultaneously. + WritableExecutable, + /// The executable entry does not lie in an executable load segment. + Entry, +} + +/// One `PT_LOAD`'s access flags (`p_flags`, decomposed). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SegFlags { + /// `PF_R`. + pub read: bool, + /// `PF_W`. + pub write: bool, + /// `PF_X`. + pub exec: bool, +} + +/// One validated `PT_LOAD`: geometry in both spaces (link vaddr and file +/// offset) plus the borrowed file bytes. Every field has been bounds- +/// checked against the image; `bytes.len() == filesz`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct LoadSegment<'a> { + /// Link-time virtual address (page-aligned — validated). + pub vaddr: u64, + /// In-memory size (`>= filesz`). + pub memsz: u64, + /// File-backed size. + pub filesz: u64, + /// File offset (page-aligned — validated; the in-place carve anchor). + pub file_off: u64, + /// `p_align` as declared (informational; the carve uses [`PAGE_SIZE`]). + pub align: u64, + /// Access flags. + pub flags: SegFlags, + /// The segment's file bytes, borrowed from the image. + pub bytes: &'a [u8], +} + +/// The `PT_TLS` initialization template (klink's shape, kept). Present +/// only when `p_memsz > 0`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TlsTemplate<'a> { + /// Link-time virtual address of the init image. + pub vaddr: u64, + /// Per-thread block size (`p_memsz`; the `memsz − filesz` tail is tbss). + pub memsz: u64, + /// Required block alignment (`p_align`). + pub align: u64, + /// File offset of the init image. + pub file_off: u64, + /// The init-image bytes, borrowed from the image (`len == p_filesz`). + pub init: &'a [u8], +} + +/// A sized dynamic range, held in **both** spaces: the tag's link vaddr +/// (what the descriptor mirrors — image-relative once `check` subtracts +/// the base) and the resolved file offset (what the service-side walk +/// reads through). Resolution proved `[vaddr, vaddr+size)` sits inside +/// one load segment's file-backed span — a lying tag never escapes. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct DynRange { + /// The tag's virtual address. + pub vaddr: u64, + /// Resolved file offset of `vaddr`. + pub file_off: u64, + /// The range's byte size. + pub size: u64, +} + +/// An unsized dynamic pointer (`DT_SYMTAB`, `DT_GNU_HASH` carry no size +/// tag): the vaddr, its resolved file offset, and the bytes *available* +/// from there to the containing segment's file-backed end — the hard +/// ceiling any later indexed read must stay under. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct DynStart { + /// The tag's virtual address. + pub vaddr: u64, + /// Resolved file offset of `vaddr`. + pub file_off: u64, + /// Bytes available to the containing segment's file end. + pub avail: u64, +} + +/// The gathered `.dynamic` facts — every tag `res/IMAGE.md` §7 consumes, +/// shape-validated (paired tags both present, sizes entry-aligned, +/// `DT_SYMENT == 24`, `DT_PLTREL == DT_RELA`) and range-resolved. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct DynamicInfo { + /// `PT_DYNAMIC` itself (the stub walks `DT_NEEDED` from it). + pub dynamic: DynRange, + /// `DT_RELA`/`DT_RELASZ` (`.rela.dyn`); `None` when absent or empty. + pub rela: Option, + /// `DT_RELR`/`DT_RELRSZ` (packed relative). + pub relr: Option, + /// `DT_JMPREL`/`DT_PLTRELSZ` (`.rela.plt`; eager under `-z now`). + pub jmprel: Option, + /// `DT_SYMTAB` (unsized; `DT_SYMENT` validated `== 24`, not carried). + pub symtab: Option, + /// `DT_STRTAB`/`DT_STRSZ`. + pub strtab: Option, + /// `DT_GNU_HASH` (unsized). + pub gnu_hash: Option, + /// `DT_INIT_ARRAY`/`DT_INIT_ARRAYSZ`. + pub init_array: Option, + /// `DT_FINI_ARRAY`/`DT_FINI_ARRAYSZ`. + pub fini_array: Option, + /// `DT_FLAGS` (zero when absent). + pub flags: u64, + /// `DT_FLAGS_1` (zero when absent). + pub flags1: u64, + /// `DT_NEEDED` entry count (each name offset validated `< strsz`). + pub needed_count: u16, + /// A `DT_TEXTREL` tag was present (the flag-form rides [`Self::flags`]). + pub textrel: bool, +} + +/// The parsed, validated image view [`crate::check`] consumes: entry + +/// machine, the bounded load-segment table, the TLS template, the +/// dynamic facts, and the `PT_GNU_RELRO` window (`(vaddr, len)`) — the +/// RELRO window rides here and [`crate::check::CheckedImage`], never the +/// frozen wire descriptor. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ParsedImage<'a> { + /// `e_entry` (raw link vaddr; zero = library). + pub entry: u64, + /// `e_machine` (equals the `expected_machine` argument — validated). + pub machine: u16, + /// The `PT_LOAD` table, in file order. + pub segments: HVec, MAX_LOAD_SEGMENTS>, + /// The `PT_TLS` template (`None` when absent or zero-sized). + pub tls: Option>, + /// The `.dynamic` gather. + pub dynamic: DynamicInfo, + /// `PT_GNU_RELRO` as `(vaddr, memsz)`, link space. + pub relro: Option<(u64, u64)>, +} + +/// A validated static bootstrap executable. +/// +/// Unlike [`ParsedImage`], this carries no dynamic-link metadata: the boot +/// founder accepts `ET_EXEC` images with no relocation step and maps the +/// validated load segments at their declared addresses. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StaticImage<'a> { + /// `e_entry`, proven to lie in an executable load segment. + pub entry: u64, + /// `e_machine`, equal to the expected machine. + pub machine: u16, + /// Page-separable, non-W+X load segments. + pub segments: HVec, MAX_LOAD_SEGMENTS>, +} + +/// Parse the static `ET_EXEC` subset accepted by the bootstrap founder. +/// +/// This is deliberately distinct from [`parse_image`]: the two conventions +/// have different gates, and neither is relaxed to accommodate the other. +/// [`parse_any`] is what picks between them on the header byte, so a caller +/// takes an image rather than a convention. The parser still faces +/// semi-trusted bytes and +/// therefore validates every range, page geometry, permission, overlap, and +/// entry-point property before the kernel allocates or maps anything. +pub fn parse_static_image( + bytes: &[u8], + expected_machine: u16, +) -> Result, ParseError> { + let elf = ElfBytes::::minimal_parse(bytes).map_err(|_| ParseError::Header)?; + if elf.ehdr.class != Class::ELF64 || elf.ehdr.endianness != AnyEndian::Little { + return Err(ParseError::Class); + } + if elf.ehdr.e_machine != expected_machine { + return Err(ParseError::Machine); + } + if elf.ehdr.e_type != ET_EXEC { + return Err(ParseError::NotExecutable); + } + let Some(phdrs) = elf.segments() else { + return Err(ParseError::ProgramHeaders); + }; + + let mut segments: HVec, MAX_LOAD_SEGMENTS> = HVec::new(); + for ph in phdrs.iter() { + if ph.p_type != PT_LOAD || ph.p_memsz == 0 { + continue; + } + if ph.p_filesz > ph.p_memsz { + return Err(ParseError::FileExceedsMem); + } + // A copied segment may begin within a page, but file and memory page + // offsets must agree. The founder maps the rounded page span, so + // incongruence would move bytes relative to their link addresses. + if ph.p_vaddr % PAGE_SIZE != ph.p_offset % PAGE_SIZE { + return Err(ParseError::Misaligned); + } + ph.p_vaddr + .checked_add(ph.p_memsz) + .ok_or(ParseError::OutOfBounds)?; + let segment_bytes = + subslice(bytes, ph.p_offset, ph.p_filesz).ok_or(ParseError::OutOfBounds)?; + let flags = SegFlags { + read: ph.p_flags & PF_R != 0, + write: ph.p_flags & PF_W != 0, + exec: ph.p_flags & PF_X != 0, + }; + if flags.write && flags.exec { + return Err(ParseError::WritableExecutable); + } + segments + .push(LoadSegment { + vaddr: ph.p_vaddr, + memsz: ph.p_memsz, + filesz: ph.p_filesz, + file_off: ph.p_offset, + align: ph.p_align, + flags, + bytes: segment_bytes, + }) + .map_err(|_| ParseError::TooManySegments)?; + } + if segments.is_empty() { + return Err(ParseError::NoSegments); + } + + // Permissions are installed page-wise. Rounded load spans must therefore + // be disjoint; two segments sharing a page would force a permission union. + for (i, a) in segments.iter().enumerate() { + let (a_start, a_end) = rounded_page_span(a)?; + for b in segments + .iter() + .skip(i.checked_add(1).ok_or(ParseError::OutOfBounds)?) + { + let (b_start, b_end) = rounded_page_span(b)?; + if a_start < b_end && b_start < a_end { + return Err(ParseError::Overlap); + } + } + } + + let entry = elf.ehdr.e_entry; + let entry_is_executable = segments.iter().any(|seg| { + seg.flags.exec + && seg + .vaddr + .checked_add(seg.memsz) + .is_some_and(|end| entry >= seg.vaddr && entry < end) + }); + if !entry_is_executable { + return Err(ParseError::Entry); + } + + Ok(StaticImage { + entry, + machine: elf.ehdr.e_machine, + segments, + }) +} + +/// Which addressing convention an image declares — the one fact the two +/// parsers disagree about, and the only one a consumer of either has to +/// carry forward. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Addressing { + /// `ET_DYN` — addresses are image-relative; a consumer picks a base. + Relative, + /// `ET_EXEC` — addresses are final. There is no base. + Absolute, +} + +impl<'a> StaticImage<'a> { + /// The static image in the shape [`crate::check`] consumes. + /// + /// Every dynamic-link fact is **absent**, not defaulted to something + /// plausible: a fixed-address executable has no `.dynamic`, so there + /// are no relocations to apply, no symbols to resolve, no + /// `DT_NEEDED` to walk and no RELRO to seal. A zeroed + /// [`DynamicInfo`] says exactly that, and every downstream consumer + /// is already driven off those fields rather than off the kind — so + /// the static case falls out of the same code instead of forking it. + pub fn as_parsed(&self) -> ParsedImage<'a> { + ParsedImage { + entry: self.entry, + machine: self.machine, + segments: self.segments.clone(), + tls: None, + dynamic: DynamicInfo::default(), + relro: None, + } + } +} + +/// Parse an image under **either** convention, reporting which one it +/// declared. +/// +/// The two parsers exist because the conventions are genuinely different +/// — [`parse_image`] demands a well-formed `.dynamic` and refuses +/// `ET_EXEC`; [`parse_static_image`] demands an entry inside an +/// executable load and refuses `ET_DYN` — and this is the one place that +/// knows both. Dispatching on `e_type` here rather than at each caller +/// keeps "which conventions exist" a fact of the parse layer: a consumer +/// asks for an image and is told how to read the addresses it got back. +/// +/// It is deliberately not a *third* parser. Neither gate is relaxed; the +/// header byte selects which one runs, and an image that satisfies +/// neither refuses with that parser's own error. +pub fn parse_any( + bytes: &[u8], + expected_machine: u16, +) -> Result<(ParsedImage<'_>, Addressing), ParseError> { + let elf = ElfBytes::::minimal_parse(bytes).map_err(|_| ParseError::Header)?; + match elf.ehdr.e_type { + ET_EXEC => { + let s = parse_static_image(bytes, expected_machine)?; + Ok((s.as_parsed(), Addressing::Absolute)) + } + // Not `ET_DYN` explicitly: `parse_image` owns that refusal + // (`NotPie`), and duplicating it here would give the same + // malformed image two different errors depending on the door. + _ => Ok((parse_image(bytes, expected_machine)?, Addressing::Relative)), + } +} + +/// Page-rounded virtual span occupied by one static load segment. +fn rounded_page_span(seg: &LoadSegment<'_>) -> Result<(u64, u64), ParseError> { + let start = seg.vaddr & !(PAGE_SIZE - 1); + let raw_end = seg + .vaddr + .checked_add(seg.memsz) + .ok_or(ParseError::OutOfBounds)?; + let end = raw_end + .checked_add(PAGE_SIZE - 1) + .ok_or(ParseError::OutOfBounds)? + & !(PAGE_SIZE - 1); + Ok((start, end)) +} + +/// Parse and validate a semi-trusted ELF64 image (the module contract's +/// entry point): ELF64/LE/`expected_machine`/`ET_DYN` gates, bounded and +/// page-aligned load segments, no overlaps, and the full +/// [`DynamicInfo`] walk. Refuses with a typed [`ParseError`]; never +/// panics, never reads outside `bytes`. +pub fn parse_image(bytes: &[u8], expected_machine: u16) -> Result, ParseError> { + let elf = ElfBytes::::minimal_parse(bytes).map_err(|_| ParseError::Header)?; + + if elf.ehdr.class != Class::ELF64 { + return Err(ParseError::Class); + } + if elf.ehdr.endianness != AnyEndian::Little { + return Err(ParseError::Class); + } + if elf.ehdr.e_machine != expected_machine { + return Err(ParseError::Machine); + } + // ET_DYN only: every kore image is PIE/shared-object. ET_EXEC is a + // deliberate refusal, not a supported legacy path (the klink delta). + if elf.ehdr.e_type != ET_DYN { + return Err(ParseError::NotPie); + } + + let Some(phdrs) = elf.segments() else { + return Err(ParseError::ProgramHeaders); + }; + + let mut segments: HVec, MAX_LOAD_SEGMENTS> = HVec::new(); + let mut tls: Option> = None; + let mut dynamic_ph: Option = None; + let mut relro: Option<(u64, u64)> = None; + + for ph in phdrs.iter() { + match ph.p_type { + PT_LOAD => { + if ph.p_filesz > ph.p_memsz { + return Err(ParseError::FileExceedsMem); + } + // Zero-extent loads carry nothing; skipping them keeps + // the carve honest (a lease of nothing is meaningless). + if ph.p_memsz == 0 { + continue; + } + // Page alignment on BOTH addresses: the in-place carve + // turns file windows into extents at these boundaries. + if ph.p_vaddr % PAGE_SIZE != 0 || ph.p_offset % PAGE_SIZE != 0 { + return Err(ParseError::Misaligned); + } + // The vaddr extent must not wrap. + ph.p_vaddr + .checked_add(ph.p_memsz) + .ok_or(ParseError::OutOfBounds)?; + let bytes = + subslice(bytes, ph.p_offset, ph.p_filesz).ok_or(ParseError::OutOfBounds)?; + segments + .push(LoadSegment { + vaddr: ph.p_vaddr, + memsz: ph.p_memsz, + filesz: ph.p_filesz, + file_off: ph.p_offset, + align: ph.p_align, + flags: SegFlags { + read: ph.p_flags & PF_R != 0, + write: ph.p_flags & PF_W != 0, + exec: ph.p_flags & PF_X != 0, + }, + bytes, + }) + .map_err(|_| ParseError::TooManySegments)?; + } + PT_TLS => { + // At most one TLS template is legal. + if tls.is_some() { + return Err(ParseError::ProgramHeaders); + } + if ph.p_filesz > ph.p_memsz { + return Err(ParseError::FileExceedsMem); + } + let init = + subslice(bytes, ph.p_offset, ph.p_filesz).ok_or(ParseError::OutOfBounds)?; + if ph.p_memsz > 0 { + tls = Some(TlsTemplate { + vaddr: ph.p_vaddr, + memsz: ph.p_memsz, + align: ph.p_align, + file_off: ph.p_offset, + init, + }); + } + } + PT_DYNAMIC => { + if dynamic_ph.is_some() { + return Err(ParseError::ProgramHeaders); + } + subslice(bytes, ph.p_offset, ph.p_filesz).ok_or(ParseError::OutOfBounds)?; + dynamic_ph = Some(DynRange { + vaddr: ph.p_vaddr, + file_off: ph.p_offset, + size: ph.p_filesz, + }); + } + PT_GNU_RELRO => { + if relro.is_some() { + return Err(ParseError::ProgramHeaders); + } + relro = Some((ph.p_vaddr, ph.p_memsz)); + } + _ => {} + } + } + + if segments.is_empty() { + return Err(ParseError::NoSegments); + } + + // Overlap gate: load extents (memsz space) must be pairwise disjoint — + // an overlapping layout is lying about what its bytes are, and the + // carve would double-lease them. ≤8 segments, so pairwise is fine. + for (i, a) in segments.iter().enumerate() { + // Ends were proven non-wrapping at collection. + let a_end = a + .vaddr + .checked_add(a.memsz) + .ok_or(ParseError::OutOfBounds)?; + for b in segments + .iter() + .skip(i.checked_add(1).ok_or(ParseError::OutOfBounds)?) + { + let b_end = b + .vaddr + .checked_add(b.memsz) + .ok_or(ParseError::OutOfBounds)?; + if a.vaddr < b_end && b.vaddr < a_end { + return Err(ParseError::Overlap); + } + } + } + + // An ET_DYN image without PT_DYNAMIC has nothing to link and nothing + // to validate `BIND_NOW` against — refused, not defaulted. + let dynamic_ph = dynamic_ph.ok_or(ParseError::Dynamic)?; + let dynamic = walk_dynamic(bytes, &segments, dynamic_ph)?; + + Ok(ParsedImage { + entry: elf.ehdr.e_entry, + machine: elf.ehdr.e_machine, + segments, + tls, + dynamic, + relro, + }) +} + +/// The raw first-pass tag accumulation (options only; shape rules apply +/// after the walk so a missing pair is a *typed* refusal, not a default). +#[derive(Clone, Copy, Debug, Default)] +struct RawDyn { + rela: Option, + relasz: Option, + relaent: Option, + relr: Option, + relrsz: Option, + relrent: Option, + jmprel: Option, + pltrelsz: Option, + pltrel: Option, + symtab: Option, + syment: Option, + strtab: Option, + strsz: Option, + gnu_hash: Option, + init_array: Option, + init_arraysz: Option, + fini_array: Option, + fini_arraysz: Option, + flags: Option, + flags1: Option, + textrel: bool, +} + +/// Gather [`DynamicInfo`] from `PT_DYNAMIC`: accumulate the §7 tag set, +/// enforce the pairing/shape rules, and resolve every vaddr range +/// through the load segments (a range no segment's file-backed span +/// contains refuses [`ParseError::Dynamic`] — a lying `.dynamic` cannot +/// point the reader outside the image). +fn walk_dynamic<'a>( + bytes: &'a [u8], + segments: &HVec, MAX_LOAD_SEGMENTS>, + dyn_ph: DynRange, +) -> Result { + let table = subslice(bytes, dyn_ph.file_off, dyn_ph.size).ok_or(ParseError::Dynamic)?; + if dyn_ph.size % DYN_ENTRY_SIZE != 0 { + return Err(ParseError::Dynamic); + } + + let mut raw = RawDyn::default(); + let mut needed_raw: u64 = 0; + let mut terminated = false; + for entry in table.chunks_exact(DYN_ENTRY_SIZE as usize) { + let (tag, val) = dyn_entry(entry)?; + if tag == DT_NULL { + terminated = true; + break; + } + match tag { + DT_RELA => raw.rela = Some(val), + DT_RELASZ => raw.relasz = Some(val), + DT_RELAENT => raw.relaent = Some(val), + DT_RELR => raw.relr = Some(val), + DT_RELRSZ => raw.relrsz = Some(val), + DT_RELRENT => raw.relrent = Some(val), + DT_JMPREL => raw.jmprel = Some(val), + DT_PLTRELSZ => raw.pltrelsz = Some(val), + DT_PLTREL => raw.pltrel = Some(val), + DT_SYMTAB => raw.symtab = Some(val), + DT_SYMENT => raw.syment = Some(val), + DT_STRTAB => raw.strtab = Some(val), + DT_STRSZ => raw.strsz = Some(val), + DT_GNU_HASH => raw.gnu_hash = Some(val), + DT_INIT_ARRAY => raw.init_array = Some(val), + DT_INIT_ARRAYSZ => raw.init_arraysz = Some(val), + DT_FINI_ARRAY => raw.fini_array = Some(val), + DT_FINI_ARRAYSZ => raw.fini_arraysz = Some(val), + DT_FLAGS => raw.flags = Some(val), + DT_FLAGS_1 => raw.flags1 = Some(val), + DT_TEXTREL => raw.textrel = true, + DT_NEEDED => { + needed_raw = needed_raw.checked_add(1).ok_or(ParseError::Dynamic)?; + } + _ => {} + } + } + // A dynamic section that runs off its segment end without DT_NULL is + // truncated or lying about its extent. + if !terminated { + return Err(ParseError::Dynamic); + } + + // Shape + resolution. Paired tags must both appear; fixed entry + // sizes must match ELF64; every range must land inside a segment. + let rela = sized_range(segments, raw.rela, raw.relasz, RELA_ENTRY_SIZE)?; + if let Some(ent) = raw.relaent + && ent != RELA_ENTRY_SIZE + { + return Err(ParseError::Dynamic); + } + let relr = sized_range(segments, raw.relr, raw.relrsz, 8)?; + if let Some(ent) = raw.relrent + && ent != 8 + { + return Err(ParseError::Dynamic); + } + // JMPREL needs its size AND the table-kind tag; only RELA tables are + // legal on the three ELF64 ABIs we serve (REL is an ELF32 shape). + let jmprel = match (raw.jmprel, raw.pltrelsz, raw.pltrel) { + (None, None, None) => None, + (Some(vaddr), Some(sz), Some(kind)) => { + if kind != DT_RELA as u64 { + return Err(ParseError::Dynamic); + } + sized_range(segments, Some(vaddr), Some(sz), RELA_ENTRY_SIZE)? + } + // PLTREL with no table (or any other partial combination) lies. + (None, None, Some(_)) => None, + _ => return Err(ParseError::Dynamic), + }; + let symtab = match raw.symtab { + None => None, + Some(vaddr) => { + // DT_SYMENT is validated here and not carried (DynDesc's + // documented posture): ELF64 symbols are exactly 24 bytes. + if raw.syment != Some(SYM_ENTRY_SIZE) { + return Err(ParseError::Dynamic); + } + Some(unsized_start(segments, vaddr)?) + } + }; + let strtab = match (raw.strtab, raw.strsz) { + (None, None) => None, + (Some(vaddr), Some(sz)) => { + let file_off = resolve_range(segments, vaddr, sz)?; + Some(DynRange { + vaddr, + file_off, + size: sz, + }) + } + _ => return Err(ParseError::Dynamic), + }; + let gnu_hash = match raw.gnu_hash { + None => None, + Some(vaddr) => { + let start = unsized_start(segments, vaddr)?; + // The fixed header (nbuckets/symoffset/bloom_size/shift) + // must at least fit. + if start.avail < 16 { + return Err(ParseError::Dynamic); + } + Some(start) + } + }; + let init_array = sized_range(segments, raw.init_array, raw.init_arraysz, 8)?; + let fini_array = sized_range(segments, raw.fini_array, raw.fini_arraysz, 8)?; + + // DT_NEEDED names must sit inside the string table; a second pass + // validates each offset now that strsz is known. + let needed_count = u16::try_from(needed_raw).map_err(|_| ParseError::Dynamic)?; + if needed_count > 0 { + let strsz = strtab.ok_or(ParseError::Dynamic)?.size; + for entry in table.chunks_exact(DYN_ENTRY_SIZE as usize) { + let (tag, val) = dyn_entry(entry)?; + if tag == DT_NULL { + break; + } + if tag == DT_NEEDED && val >= strsz { + return Err(ParseError::Dynamic); + } + } + } + + Ok(DynamicInfo { + dynamic: dyn_ph, + rela, + relr, + jmprel, + symtab, + strtab, + gnu_hash, + init_array, + fini_array, + flags: raw.flags.unwrap_or(0), + flags1: raw.flags1.unwrap_or(0), + needed_count, + textrel: raw.textrel, + }) +} + +/// One 16-byte `.dynamic` entry: `(d_tag, d_val)`, little-endian. +fn dyn_entry(entry: &[u8]) -> Result<(i64, u64), ParseError> { + let tag = entry + .get(0..8) + .and_then(|s| s.try_into().ok()) + .map(i64::from_le_bytes) + .ok_or(ParseError::Dynamic)?; + let val = entry + .get(8..16) + .and_then(|s| s.try_into().ok()) + .map(u64::from_le_bytes) + .ok_or(ParseError::Dynamic)?; + Ok((tag, val)) +} + +/// Fold a `(vaddr, size)` tag pair into a resolved [`DynRange`]: +/// both-or-neither, `size % ent == 0`, and the range inside a segment. +/// An empty table (`size == 0`) folds to `None`. +fn sized_range( + segments: &HVec, MAX_LOAD_SEGMENTS>, + vaddr: Option, + size: Option, + ent: u64, +) -> Result, ParseError> { + match (vaddr, size) { + (None, None) => Ok(None), + (Some(_), Some(0)) => Ok(None), + (Some(vaddr), Some(size)) => { + if size % ent != 0 { + return Err(ParseError::Dynamic); + } + let file_off = resolve_range(segments, vaddr, size)?; + Ok(Some(DynRange { + vaddr, + file_off, + size, + })) + } + _ => Err(ParseError::Dynamic), + } +} + +/// Resolve a sized vaddr range to its file offset: the whole range must +/// sit inside ONE load segment's file-backed span (link inputs never +/// straddle segments; a range that escapes every segment refuses). +fn resolve_range( + segments: &HVec, MAX_LOAD_SEGMENTS>, + vaddr: u64, + size: u64, +) -> Result { + for seg in segments.iter() { + let Some(rel) = vaddr.checked_sub(seg.vaddr) else { + continue; + }; + let end = rel.checked_add(size).ok_or(ParseError::Dynamic)?; + if end > seg.filesz { + continue; + } + return seg.file_off.checked_add(rel).ok_or(ParseError::Dynamic); + } + Err(ParseError::Dynamic) +} + +/// Resolve an unsized vaddr to `(file_off, avail)` — the bytes from it +/// to the containing segment's file-backed end (the ceiling later +/// indexed reads are bounded by). +fn unsized_start( + segments: &HVec, MAX_LOAD_SEGMENTS>, + vaddr: u64, +) -> Result { + for seg in segments.iter() { + let Some(rel) = vaddr.checked_sub(seg.vaddr) else { + continue; + }; + if rel >= seg.filesz { + continue; + } + let file_off = seg.file_off.checked_add(rel).ok_or(ParseError::Dynamic)?; + let avail = seg.filesz.checked_sub(rel).ok_or(ParseError::Dynamic)?; + return Ok(DynStart { + vaddr, + file_off, + avail, + }); + } + Err(ParseError::Dynamic) +} + +/// Checked image subslice: `bytes[off .. off+len]`, `None` on any +/// overflow or escape (the single bounds gate every file read uses). +fn subslice(bytes: &[u8], off: u64, len: u64) -> Option<&[u8]> { + let start = usize::try_from(off).ok()?; + let end = start.checked_add(usize::try_from(len).ok()?)?; + bytes.get(start..end) +} diff --git a/native/crates/ld/src/place.rs b/native/crates/ld/src/place.rs new file mode 100644 index 0000000..c6053b4 --- /dev/null +++ b/native/crates/ld/src/place.rs @@ -0,0 +1,443 @@ +//! Placement — **the world's half of a link**, factored out so it can be +//! tested. +//! +//! [`LinkWorld::choose_base`] is where a real linker's `mmap(NULL, …)` +//! would be: the engine states a size and an alignment, and the world +//! answers *where*. Its doc is explicit that this is policy, not +//! mechanism — "per-task ASLR lives there, the engine only states +//! size/alignment" — and that the engine "re-checks the alignment and +//! otherwise trusts nothing else about the value". +//! +//! That makes placement the one part of the metal adapter with no +//! capability in it: arithmetic over a window, answering an address. The +//! rest of the adapter is verb calls whose behaviour is only observable +//! on metal (`cap::verbs` answers a typed refusal off the userland +//! target, by the 2026-07-24 ruling that deleted the mock cap layer), so +//! this is the piece a host suite can actually hold to account — and the +//! piece whose bugs are silent rather than loud: an overlapping answer +//! does not refuse, it corrupts whichever mapping was there first. +//! +//! Lives in `kore-ld` for the same reason [`crate::stub`] does: `user/` +//! is a separate workspace `make test` never reaches, so logic left in +//! the stub binary would be the part nothing exercises. +//! +//! [`LinkWorld::choose_base`]: crate::link::LinkWorld::choose_base + +/// A bump placer over one contiguous window of a task's address space. +/// +/// Hands out non-overlapping, alignment-satisfying spans until the +/// window is exhausted, then refuses. **Monotone and never reused**: a +/// linker maps each module once and nothing here is freed, so the +/// simplest allocator that cannot alias is the correct one — a free list +/// would add a way to hand out a live address in exchange for a +/// generality the caller does not have. +/// +/// Placement is deliberately *not* randomized yet. ASLR belongs here and +/// the trait's doc says so, but entropy the stub cannot obtain honestly +/// would be worse than none: it runs before any runtime, so it has no +/// clock and no entropy source, and a "random" base derived from a +/// predictable seed reads as a defence while providing none. The seam is +/// where it goes when the child can be handed real entropy. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Placer { + /// Next free address in the window. + next: u64, + /// One past the window's last byte. + end: u64, +} + +/// Why a placement refused. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PlaceError { + /// `align` was not a power of two (`0` included) — a caller bug, and + /// the one input that would otherwise silently produce a misaligned + /// answer the engine then rejects with a less specific error. + BadAlign, + /// A zero-length span. Refused rather than answered, because the + /// address would be indistinguishable from the next real placement's + /// and two modules would believe they own it. + ZeroLen, + /// The window has no room for `len` at `align`. + Exhausted, +} + +impl Placer { + /// A placer over `[base, base + len)`. + pub const fn new(base: u64, len: u64) -> Self { + Self { + next: base, + end: base.saturating_add(len), + } + } + + /// Choose a span of `len` bytes at an address that is a multiple of + /// `align`, and consume it. + /// + /// Every arithmetic step is checked: this runs in a stub with no + /// runtime, where a wrap would answer a *low* address that is very + /// likely mapped by something else, and the caller has no way to tell + /// that from a legitimate answer. + pub fn choose(&mut self, len: u64, align: u64) -> Result { + let (at, after) = bump(self.next, self.end, len, align)?; + self.next = after; + Ok(at) + } + + /// Bytes still placeable, ignoring alignment loss — diagnostics, not + /// a promise: a subsequent [`choose`](Self::choose) may still refuse + /// after rounding up. + pub const fn remaining(&self) -> u64 { + self.end.saturating_sub(self.next) + } + + /// One past the highest address handed out so far — the placement + /// high-water mark a world reports through + /// `LinkWorld::placed_high_water`, so a later placer over the same + /// window can start above everything this one placed. + pub const fn cursor(&self) -> u64 { + self.next + } +} + +/// The one checked bump both placers use: round `next` up to `align`, +/// fit `len` below `end`, and answer `(at, after)`. +/// +/// Factored out so the [`Placer`] and [`SharedPlacer`] cannot drift: the +/// failure mode of a second copy is not a compile error but an +/// *overlapping address*, which this module's own doc calls out as +/// silent rather than loud. +const fn bump(next: u64, end: u64, len: u64, align: u64) -> Result<(u64, u64), PlaceError> { + if !align.is_power_of_two() { + return Err(PlaceError::BadAlign); + } + if len == 0 { + return Err(PlaceError::ZeroLen); + } + // Round up to `align`, without wrapping. + let Some(bumped) = next.checked_add(align - 1) else { + return Err(PlaceError::Exhausted); + }; + let at = bumped & !(align - 1); + let Some(after) = at.checked_add(len) else { + return Err(PlaceError::Exhausted); + }; + if after > end { + return Err(PlaceError::Exhausted); + } + Ok((at, after)) +} + +/// The `next` value meaning "no window published yet". Not a plausible +/// cursor: a window ending at `u64::MAX` is already refused by +/// [`Placer::new`]'s saturation, so this cannot collide with a live base. +const UNARMED: u64 = u64::MAX; + +/// A [`Placer`] several consumers share, without a lock. +/// +/// The owned `Placer` is `&mut` by design: one linker, one window, one +/// caller. A task that *also* hands its link window to a second consumer +/// — a wasm engine placing guest memory beside the late loader's modules +/// — cannot use two `Placer` values over one window, because each would +/// hand out the other's addresses. Two cursors over one window is the +/// bug this type exists to make unrepresentable. +/// +/// Monotone and lock-free, mimicking `kore_rt::heap`'s supply cursors: a +/// checked `try_update` means a losing racer retries against the winner's +/// mark rather than serialising behind it, and arithmetic that would wrap +/// refuses instead of answering a low, probably-mapped address. +/// Nothing is ever freed, so a compare-and-set that only advances is the +/// whole of the concurrency story. +pub struct SharedPlacer { + next: core::sync::atomic::AtomicU64, + /// One past the window's last byte, or `0` for *unarmed* — a placer + /// whose window is not known yet and which therefore refuses every + /// request. Fail-closed is the only safe default for a `static`: a + /// consumer that places before the window is published must get a + /// typed refusal, never address zero. + end: core::sync::atomic::AtomicU64, +} + +impl SharedPlacer { + /// A shared placer over `[base, base + len)`. + pub const fn new(base: u64, len: u64) -> Self { + Self { + next: core::sync::atomic::AtomicU64::new(base), + end: core::sync::atomic::AtomicU64::new(base.saturating_add(len)), + } + } + + /// An unarmed placer, for a `static` whose window is only known once + /// the task can read its own geometry. Refuses until [`arm`](Self::arm). + pub const fn unarmed() -> Self { + Self { + next: core::sync::atomic::AtomicU64::new(UNARMED), + end: core::sync::atomic::AtomicU64::new(0), + } + } + + /// Publish the window, once. A second call refuses and leaves the + /// first window **and its cursor** intact, so a second consumer cannot + /// silently re-base a cursor that earlier placements were already + /// derived from — every one of those addresses would become reusable. + /// + /// The claim is the compare-and-set on `next` away from [`UNARMED`], + /// so a losing arm writes nothing at all. `end` is released only after + /// that claim is won, and [`choose`](Self::choose) acquires `end` + /// first: observing an armed `end` therefore guarantees observing the + /// matching `next`. Publishing `next` first (the obvious order) is + /// wrong in both directions — a refused arm would clobber a live + /// cursor, and a racing `choose` could read the new base against the + /// old window. + pub fn arm(&self, base: u64, len: u64) -> Result<(), PlaceError> { + use core::sync::atomic::Ordering; + let end = base.saturating_add(len); + if end <= base { + return Err(PlaceError::ZeroLen); + } + if base == UNARMED { + return Err(PlaceError::Exhausted); + } + self.next + .compare_exchange(UNARMED, base, Ordering::AcqRel, Ordering::Acquire) + .map_err(|_| PlaceError::Exhausted)?; + self.end.store(end, Ordering::Release); + Ok(()) + } + + /// Whether a window has been published. + pub fn is_armed(&self) -> bool { + self.end.load(core::sync::atomic::Ordering::Acquire) != 0 + } + + /// Choose and consume a span, as [`Placer::choose`], from any thread. + /// + /// The closure re-runs [`bump`] against whatever mark won the race, so + /// a concurrent caller never receives an address derived from a stale + /// cursor. + pub fn choose(&self, len: u64, align: u64) -> Result { + use core::sync::atomic::Ordering; + let end = self.end.load(Ordering::Acquire); + if end == 0 { + return Err(PlaceError::Exhausted); + } + let mut refusal = PlaceError::Exhausted; + self.next + .try_update(Ordering::AcqRel, Ordering::Acquire, |next| { + match bump(next, end, len, align) { + Ok((_, after)) => Some(after), + Err(error) => { + refusal = error; + None + } + } + }) + .map_err(|_| refusal) + .and_then(|previous| bump(previous, end, len, align).map(|(at, _)| at)) + } + + /// Bytes still placeable, ignoring alignment loss — diagnostics only, + /// and immediately stale under concurrency. An unarmed placer has none. + pub fn remaining(&self) -> u64 { + use core::sync::atomic::Ordering; + self.end + .load(Ordering::Acquire) + .saturating_sub(self.next.load(Ordering::Acquire)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The property everything else rests on: successive placements never + /// overlap, and each satisfies the alignment it asked for. + #[test] + fn placements_are_aligned_and_disjoint() { + let mut p = Placer::new(0x1000_0000, 0x10_0000); + let a = p.choose(0x1234, 0x1000).expect("fits"); + let b = p.choose(0x1, 0x10_000).expect("fits"); + let c = p.choose(0x2000, 0x1000).expect("fits"); + + assert_eq!(a % 0x1000, 0); + assert_eq!(b % 0x10_000, 0); + assert_eq!(c % 0x1000, 0); + assert!(a + 0x1234 <= b, "a and b are disjoint"); + assert!(b + 0x1 <= c, "b and c are disjoint"); + } + + /// A window hands out what it has and then refuses — it does not wrap + /// to its base or answer past its end. + #[test] + fn exhaustion_refuses_rather_than_overrunning() { + let mut p = Placer::new(0x2000_0000, 0x2000); + assert!(p.choose(0x1000, 0x1000).is_ok()); + assert!(p.choose(0x1000, 0x1000).is_ok()); + assert_eq!(p.remaining(), 0); + assert_eq!(p.choose(0x1, 0x1), Err(PlaceError::Exhausted)); + } + + /// Alignment loss is charged against the window, so a placement that + /// only fits *unaligned* refuses instead of straddling the end. + #[test] + fn alignment_padding_cannot_push_a_span_past_the_end() { + let mut p = Placer::new(0x3000_0000, 0x3000); + assert!(p.choose(0x1, 0x1000).is_ok(), "consumes into page 0"); + // 0x2000 left, but a 2 MiB alignment lands past the window. + assert_eq!( + p.choose(0x1000, 0x20_0000), + Err(PlaceError::Exhausted), + "rounding up must not answer outside the window" + ); + } + + /// A non-power-of-two alignment is a caller bug, named as one. The + /// engine re-checks alignment, so answering something plausible here + /// would surface as a vaguer error further away. + #[test] + fn a_bad_alignment_is_refused_by_name() { + let mut p = Placer::new(0x4000_0000, 0x1_0000); + assert_eq!(p.choose(0x1000, 3), Err(PlaceError::BadAlign)); + assert_eq!(p.choose(0x1000, 0), Err(PlaceError::BadAlign)); + // ...and refusing consumed nothing. + assert_eq!(p.remaining(), 0x1_0000); + } + + /// **A zero-length span refuses.** Answered, it would hand two + /// modules the same address — the next placement starts exactly where + /// the empty one did — and both would believe they own it. + #[test] + fn a_zero_length_span_refuses_rather_than_aliasing() { + let mut p = Placer::new(0x5000_0000, 0x1_0000); + assert_eq!(p.choose(0, 0x1000), Err(PlaceError::ZeroLen)); + let a = p.choose(0x1000, 0x1000).expect("fits"); + assert_eq!(a, 0x5000_0000, "the refusal consumed nothing"); + } + + /// A window at the top of the address space must not wrap into a low + /// address — the failure mode with no symptom, since a low answer is + /// very likely mapped by something else and writing there corrupts + /// it. + #[test] + fn a_window_at_the_address_ceiling_cannot_wrap() { + let mut p = Placer::new(u64::MAX - 0x1000, 0x1000); + assert_eq!( + p.choose(0x2000, 0x1000), + Err(PlaceError::Exhausted), + "too large for the window" + ); + // An alignment round-up that would overflow is equally refused. + let mut q = Placer::new(u64::MAX - 0x10, 0x10); + assert_eq!(q.choose(0x8, 0x1000), Err(PlaceError::Exhausted)); + } + + /// `Placer::new` saturates rather than wrapping when base + len would + /// overflow, so a malformed window yields an empty placer instead of + /// one spanning the whole address space. + #[test] + fn an_overflowing_window_is_empty_not_unbounded() { + let mut p = Placer::new(u64::MAX - 0x10, u64::MAX); + assert_eq!(p.remaining(), 0x10); + assert_eq!(p.choose(0x100, 0x1), Err(PlaceError::Exhausted)); + } + /// The drift guard the shared placer exists for: for one request + /// sequence it must answer exactly what the owned placer answers. + /// If these ever diverge, two consumers of one window overlap, and + /// this module's doc says that failure is silent rather than loud. + #[test] + fn shared_and_owned_placers_agree() { + let requests = [(0x1234, 0x1000), (0x1, 0x1_0000), (0x2000, 0x1000), (0x8, 0x8)]; + let mut owned = Placer::new(0x1000_0000, 0x10_0000); + let shared = SharedPlacer::new(0x1000_0000, 0x10_0000); + for (len, align) in requests { + assert_eq!(owned.choose(len, align), shared.choose(len, align)); + } + assert_eq!(owned.remaining(), shared.remaining()); + } + + /// Refusals agree too — a shared placer must not turn a caller bug + /// into a different (or absent) error than the owned one. + #[test] + fn shared_placer_refuses_identically() { + let mut owned = Placer::new(0x2000_0000, 0x2000); + let shared = SharedPlacer::new(0x2000_0000, 0x2000); + for (len, align) in [(0x1000u64, 0x3u64), (0, 0x1000), (0x4000, 0x1000)] { + assert_eq!(owned.choose(len, align), shared.choose(len, align)); + } + // A refused choose consumes nothing, on either. + assert_eq!(owned.remaining(), 0x2000); + assert_eq!(shared.remaining(), 0x2000); + } + + /// The property that makes it shareable: concurrent callers receive + /// disjoint, aligned spans and none is handed out twice. + #[test] + fn concurrent_placements_never_overlap() { + use std::vec::Vec; + + const THREADS: usize = 8; + const EACH: usize = 64; + const LEN: u64 = 0x1000; + + let placer = std::sync::Arc::new(SharedPlacer::new(0x4000_0000, 0x100_0000)); + let mut handles = Vec::new(); + for _ in 0..THREADS { + let placer = std::sync::Arc::clone(&placer); + handles.push(std::thread::spawn(move || { + (0..EACH) + .map(|_| placer.choose(LEN, LEN).expect("window is large enough")) + .collect::>() + })); + } + let mut spans: Vec = handles + .into_iter() + .flat_map(|h| h.join().expect("no placer thread panics")) + .collect(); + + assert_eq!(spans.len(), THREADS * EACH); + spans.sort_unstable(); + spans.dedup(); + assert_eq!(spans.len(), THREADS * EACH, "an address was handed out twice"); + for pair in spans.windows(2) { + assert!(pair[0] + LEN <= pair[1], "spans overlap"); + } + assert!(spans.iter().all(|at| at % LEN == 0), "a span was misaligned"); + } + /// Fail-closed: a `static` placer refuses until its window is known, + /// rather than answering an address in an unpublished window. + #[test] + fn an_unarmed_placer_refuses_everything() { + let p = SharedPlacer::unarmed(); + assert!(!p.is_armed()); + assert_eq!(p.remaining(), 0); + assert_eq!(p.choose(0x1000, 0x1000), Err(PlaceError::Exhausted)); + } + + /// Arming publishes the window exactly once. A second arm refuses and + /// leaves the first window — otherwise a late consumer could re-base a + /// cursor that earlier placements were already derived from, and every + /// one of those addresses would silently become reusable. + #[test] + fn arming_is_once_and_the_first_window_wins() { + let p = SharedPlacer::unarmed(); + assert_eq!(p.arm(0x5000_0000, 0x10_0000), Ok(())); + assert!(p.is_armed()); + let first = p.choose(0x1000, 0x1000).expect("fits"); + + assert_eq!(p.arm(0x9000_0000, 0x10_0000), Err(PlaceError::Exhausted)); + let second = p.choose(0x1000, 0x1000).expect("fits"); + assert!( + (0x5000_0000..0x5010_0000).contains(&second), + "a refused re-arm must not move the cursor into the new window" + ); + assert!(second >= first + 0x1000, "the cursor kept advancing"); + } + + /// A degenerate window is refused at arm time rather than producing a + /// placer that answers its own base. + #[test] + fn arming_an_empty_window_refuses() { + let p = SharedPlacer::unarmed(); + assert_eq!(p.arm(0x5000_0000, 0), Err(PlaceError::ZeroLen)); + assert!(!p.is_armed()); + } +} diff --git a/native/crates/ld/src/reloc.rs b/native/crates/ld/src/reloc.rs new file mode 100644 index 0000000..6b009f7 --- /dev/null +++ b/native/crates/ld/src/reloc.rs @@ -0,0 +1,275 @@ +//! The relocation seam — one contract trait, three per-ISA data impls +//! (`res/IMAGE.md` §7; the doctrine-§3 shape klink's `#[cfg]` modules +//! are retired for). +//! +//! Under the pinned flag regime (`-z now -z relro -z text +//! -Bsymbolic-functions -fno-plt`, all-PIC, no ifuncs, LE/IE TLS) an +//! image may carry exactly **five relocation semantics** ([`RelocKind`]). +//! Everything else is either on the **reject list** (a hard, *named* +//! refusal — these are the relocations the toolchain regime promises +//! never to emit, so their presence marks a foreign or hostile image) or +//! **unknown** (refused likewise). The engine ([`crate::apply`]) is +//! generic over [`RelocArch`]; a new ISA is a new impl, never a branch. +//! +//! Every `R_*` number below is copied from its ABI supplement — the +//! x86-64 psABI, ARM AAELF64, and the RISC-V psABI — not inferred. + +/// The five eager semantics the engine computes (`res/IMAGE.md` §7). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RelocKind { + /// `B + A` — base plus addend (the static-PIE self-reloc class, and + /// the bulk of every PIE's `.rela.dyn`). + Relative, + /// `S` — a GOT data slot (symbol value; addend ignored per spec). + GlobDat, + /// `S` — a PLT slot. Identical to [`GlobDat`](Self::GlobDat) under + /// `-z now` (eager: no resolver, no lazy stub), kept distinct so a + /// mis-tagged image still classifies faithfully. + JumpSlot, + /// `S + A` — an absolute 64-bit data reference. + Abs64, + /// `tpoff(S) + A` — an initial-exec TLS slot: the thread-pointer- + /// relative offset of a symbol in the static TLS block + /// ([`TlsRules`] gives the per-arch layout). + TpRel, +} + +/// Why a relocation type refused ([`RelocArch::classify`]). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RelocRefusal { + /// On the reject list: a relocation the flag regime never emits — + /// `COPY` (breaks sharing), `IRELATIVE` (ifuncs are banned), + /// dynamic-TLS (`DTPMOD`/`DTPOFF`/`TLSDESC` — the load set is + /// closed, IE/LE only). Presence marks a foreign/hostile image. + Rejected(u32), + /// Not a relocation this ABI defines for the eager set — refused + /// identically (defense in depth), but distinguishable in + /// diagnostics. + Unknown(u32), +} + +/// Static-TLS placement rules — the per-arch facts the IE/LE layout +/// builder needs ([`crate::apply`]). Variant vocabulary per Drepper's +/// TLS paper as pinned by each ABI supplement. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TlsRules { + /// Variant 1: TLS blocks sit **above** the thread pointer, after a + /// `tcb`-byte reserved header (aarch64: 16; riscv64: 0). The first + /// block starts at `tp + max(tcb, align)`-rounded placement and + /// offsets are positive. + V1 { tcb: u64 }, + /// Variant 2 (x86_64): TLS blocks sit **below** the thread pointer + /// (offsets negative) and `tp` (`%fs:0`) holds the TCB self-pointer. + V2, +} + +/// The per-ISA relocation contract: classification tables + TLS layout +/// facts. Implementations are pure data — the engine owns all logic. +pub trait RelocArch { + /// `e_machine` this impl serves (cross-checked against the image). + const MACHINE: u16; + /// The ISA's static-TLS placement rules. + const TLS: TlsRules; + /// Classify a raw `r_type` into an eager semantic, or refuse. + fn classify(r_type: u32) -> Result; +} + +// ============================================================================ +// x86_64 — System V AMD64 psABI, table 4.10 "Relocation Types" +// ============================================================================ + +/// x86-64 relocation data (`EM_X86_64 = 62`). +pub struct X86_64; + +impl RelocArch for X86_64 { + const MACHINE: u16 = 62; + /// Variant 2: blocks below `tp`, `%fs:0` = self-pointer. + const TLS: TlsRules = TlsRules::V2; + + fn classify(r_type: u32) -> Result { + match r_type { + 8 => Ok(RelocKind::Relative), // R_X86_64_RELATIVE + 6 => Ok(RelocKind::GlobDat), // R_X86_64_GLOB_DAT + 7 => Ok(RelocKind::JumpSlot), // R_X86_64_JUMP_SLOT + 1 => Ok(RelocKind::Abs64), // R_X86_64_64 + 18 => Ok(RelocKind::TpRel), // R_X86_64_TPOFF64 + // The reject list (psABI numbers): + // 5 = COPY, 16 = DTPMOD64, 17 = DTPOFF64, 36 = TLSDESC, + // 37 = IRELATIVE. + 5 | 16 | 17 | 36 | 37 => Err(RelocRefusal::Rejected(r_type)), + other => Err(RelocRefusal::Unknown(other)), + } + } +} + +// ============================================================================ +// aarch64 — ARM AAELF64 (IHI 0056), §4.6 dynamic relocations +// ============================================================================ + +/// AArch64 relocation data (`EM_AARCH64 = 183`). +pub struct AArch64; + +impl RelocArch for AArch64 { + const MACHINE: u16 = 183; + /// Variant 1 with a 16-byte reserved TCB at `tp`; the first block is + /// placed at `tp + 16` rounded up to the segment alignment. + const TLS: TlsRules = TlsRules::V1 { tcb: 16 }; + + fn classify(r_type: u32) -> Result { + match r_type { + 1027 => Ok(RelocKind::Relative), // R_AARCH64_RELATIVE + 1025 => Ok(RelocKind::GlobDat), // R_AARCH64_GLOB_DAT + 1026 => Ok(RelocKind::JumpSlot), // R_AARCH64_JUMP_SLOT + 257 => Ok(RelocKind::Abs64), // R_AARCH64_ABS64 + 1030 => Ok(RelocKind::TpRel), // R_AARCH64_TLS_TPREL64 + // Reject list (AAELF64 numbers): + // 1024 = COPY, 1028 = TLS_DTPMOD64, 1029 = TLS_DTPREL64, + // 1031 = TLSDESC, 1032 = IRELATIVE. + 1024 | 1028 | 1029 | 1031 | 1032 => Err(RelocRefusal::Rejected(r_type)), + other => Err(RelocRefusal::Unknown(other)), + } + } +} + +// ============================================================================ +// riscv64 — RISC-V psABI, "Relocations" table +// ============================================================================ + +/// RV64 relocation data (`EM_RISCV = 243`). +pub struct Riscv64; + +impl RelocArch for Riscv64 { + const MACHINE: u16 = 243; + /// Variant 1 with **zero** reserved TCB: `tp` points one past the + /// TCB end, so the first block starts at `tp + 0` (the psABI's + /// "tp contains the address one past the end of the TCB"). The + /// `0x800` DTPREL bias applies only to dynamic-model relocs, which + /// the reject list bans — no bias appears in the IE/LE path. + const TLS: TlsRules = TlsRules::V1 { tcb: 0 }; + + fn classify(r_type: u32) -> Result { + match r_type { + 3 => Ok(RelocKind::Relative), // R_RISCV_RELATIVE + // RISC-V has no GLOB_DAT: GOT data slots are plain + // R_RISCV_64 (= S + A, addend honored). Classified Abs64 so + // the engine computes it faithfully. + 2 => Ok(RelocKind::Abs64), // R_RISCV_64 + 5 => Ok(RelocKind::JumpSlot), // R_RISCV_JUMP_SLOT + 11 => Ok(RelocKind::TpRel), // R_RISCV_TLS_TPREL64 + // Reject list (psABI numbers): + // 4 = COPY, 7 = TLS_DTPMOD64, 9 = TLS_DTPREL64, + // 12 = TLSDESC, 58 = IRELATIVE. + 4 | 7 | 9 | 12 | 58 => Err(RelocRefusal::Rejected(r_type)), + other => Err(RelocRefusal::Unknown(other)), + } + } +} + +// ============================================================================ +// Selection — the seam's other half +// ============================================================================ + +#[cfg(target_arch = "aarch64")] +pub use AArch64 as HostArch; +#[cfg(target_arch = "riscv64")] +pub use Riscv64 as HostArch; +/// The [`RelocArch`] for the ISA this build **executes on**. +/// +/// The three impls above are the contract's data; this alias is its +/// *selection*, and it is the only `#[cfg(target_arch)]` the seam is allowed +/// to carry. Every site that loads an image meant for the running machine — +/// the kernel's founding check, the exec service's founder, the `kore-ld` +/// stub — names `HostArch`, so adding an ISA is one `impl` plus one row here +/// rather than an edit at each call site. (Sites that deliberately serve +/// *any* ISA — the image service's cross-arch checker, which dispatches on the +/// image's own `e_machine` — keep naming the concretes; that is a different +/// question and rightly asks it explicitly.) +/// +/// Naming `X86_64` directly in a would-be-neutral site is how the aarch64 +/// boot came to refuse every one of its own service images with +/// `Parse(Machine)`: the images were correct, the loader was asking for 62. +#[cfg(target_arch = "x86_64")] +pub use X86_64 as HostArch; + +#[cfg(not(any( + target_arch = "x86_64", + target_arch = "aarch64", + target_arch = "riscv64" +)))] +compile_error!( + "kore-ld has no RelocArch impl for this target: add one beside X86_64 / \ + AArch64 / Riscv64 and give it a HostArch row" +); + +#[cfg(test)] +mod tests { + use super::*; + + /// The host alias resolves to the impl whose `MACHINE` this build's own + /// ELFs carry — the property every `HostArch` call site depends on. + #[test] + fn host_alias_matches_this_build_s_machine() { + let expected: u16 = if cfg!(target_arch = "x86_64") { + 62 + } else if cfg!(target_arch = "aarch64") { + 183 + } else { + 243 + }; + assert_eq!(HostArch::MACHINE, expected); + } + + /// The spec tables, pinned: a wrong number here is a security bug + /// (a reject-list reloc classified eager, or vice versa). + #[test] + fn x86_64_table_matches_psabi() { + assert_eq!(X86_64::classify(8), Ok(RelocKind::Relative)); + assert_eq!(X86_64::classify(6), Ok(RelocKind::GlobDat)); + assert_eq!(X86_64::classify(7), Ok(RelocKind::JumpSlot)); + assert_eq!(X86_64::classify(1), Ok(RelocKind::Abs64)); + assert_eq!(X86_64::classify(18), Ok(RelocKind::TpRel)); + for rejected in [5, 16, 17, 36, 37] { + assert_eq!( + X86_64::classify(rejected), + Err(RelocRefusal::Rejected(rejected)) + ); + } + // PC32 (2) is a link-time reloc — never eager; refuses as unknown. + assert_eq!(X86_64::classify(2), Err(RelocRefusal::Unknown(2))); + assert_eq!(X86_64::MACHINE, 62); + assert_eq!(X86_64::TLS, TlsRules::V2); + } + + #[test] + fn aarch64_table_matches_aaelf64() { + assert_eq!(AArch64::classify(1027), Ok(RelocKind::Relative)); + assert_eq!(AArch64::classify(1025), Ok(RelocKind::GlobDat)); + assert_eq!(AArch64::classify(1026), Ok(RelocKind::JumpSlot)); + assert_eq!(AArch64::classify(257), Ok(RelocKind::Abs64)); + assert_eq!(AArch64::classify(1030), Ok(RelocKind::TpRel)); + for rejected in [1024, 1028, 1029, 1031, 1032] { + assert_eq!( + AArch64::classify(rejected), + Err(RelocRefusal::Rejected(rejected)) + ); + } + assert_eq!(AArch64::MACHINE, 183); + assert_eq!(AArch64::TLS, TlsRules::V1 { tcb: 16 }); + } + + #[test] + fn riscv64_table_matches_psabi() { + assert_eq!(Riscv64::classify(3), Ok(RelocKind::Relative)); + assert_eq!(Riscv64::classify(2), Ok(RelocKind::Abs64)); + assert_eq!(Riscv64::classify(5), Ok(RelocKind::JumpSlot)); + assert_eq!(Riscv64::classify(11), Ok(RelocKind::TpRel)); + for rejected in [4, 7, 9, 12, 58] { + assert_eq!( + Riscv64::classify(rejected), + Err(RelocRefusal::Rejected(rejected)) + ); + } + assert_eq!(Riscv64::MACHINE, 243); + assert_eq!(Riscv64::TLS, TlsRules::V1 { tcb: 0 }); + } +} diff --git a/native/crates/ld/src/stage.rs b/native/crates/ld/src/stage.rs new file mode 100644 index 0000000..f8b6e9e --- /dev/null +++ b/native/crates/ld/src/stage.rs @@ -0,0 +1,351 @@ +//! **How a parsed image's `PT_LOAD`s group into leases** — the staging +//! seam's contract half. +//! +//! Turning a parsed image into filled leases happens in three places +//! today, with three different answers to the same question: +//! +//! - `found_service` takes one lease per `PT_LOAD`; +//! - `found_first_shell` folds page-adjacent same-access LOADs into +//! **runs** and takes one lease per run, because a conveyed lease costs +//! a convey-table entry and that table is 16 wide; +//! - the U1 boottest hand-builds a single share for the linker stub. +//! +//! Neither policy is wrong — they answer to different budgets — but +//! nothing said which was canonical, and the difference had already +//! drifted into two open-coded loops. Adding a fourth consumer (the stub, +//! staged like a service image) is what forced the question, and +//! `kore-design-doctrine` §2 answers it: standardise the seam at N, not +//! at N+1. +//! +//! **What varies is grouping, and only grouping.** The rest — allocate a +//! backing extent, zero it, copy the span's file bytes in, derive a +//! `MemLease` — is identical, and stays kernel-side because it needs +//! kernel capability types. What lives here is the pure decision, which +//! is also the part that was untested: the run-folding is fiddly (a +//! suffix test over page-rounded spans) and had no coverage at all while +//! it was a loop inside a 700-line founding routine. +//! +//! The seam's three parts, in `kore-trait-architecture`'s terms: the +//! contract is [`Grouping`]; the engine is the kernel-side stager that is +//! generic over it; selection is type instantiation — [`PerLoad`] or +//! [`PerAccessRun`] named at the call site, never a runtime branch. + +use heapless::Vec as HVec; + +use crate::parse::LoadSegment; + +/// Page granule the spans are rounded to. Mirrors `parse::PAGE_SIZE`; +/// a span is always a whole number of pages because a lease's backing is. +const PAGE: u64 = 4096; + +/// Access bits of a span — **re-exported from the share verbs' own +/// vocabulary**, never restated. +/// +/// These values are an ABI the kernel's share path reads; writing `1 / 2 +/// / 4` here would be a second spelling of someone else's constant, and +/// the kind that stays right until it doesn't. Citing them means a change +/// on that side is a compile error here rather than a child mapped with +/// the wrong permissions. +pub mod access { + pub use kore_abi::session::{SHARE_ACCESS_R as R, SHARE_ACCESS_W as W, SHARE_ACCESS_X as X}; +} + +/// One lease-sized group of an image: a page-rounded span of link +/// addresses plus the access mode every LOAD inside it shares. +/// +/// `[base, end)` is page-aligned at both ends, so it is exactly what a +/// backing extent and its `MemLease` cover. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ImageSpan { + /// Page-aligned first link address. + pub base: u64, + /// Page-aligned end (exclusive). + pub end: u64, + /// The [`access`] bits shared by every LOAD in the span. + pub access: u8, +} + +impl ImageSpan { + /// Byte length — a page multiple by construction. + pub fn len(&self) -> u64 { + self.end - self.base + } + + /// Never empty (a zero-length LOAD is dropped at parse), but the + /// convention pair keeps clippy honest. + pub fn is_empty(&self) -> bool { + self.end == self.base + } + + /// Whether this span must be minted exec-blessed. A blessed lease + /// refuses every WRITE share (the per-lease W^X gate in + /// `runtime::mm::lease`), which is why the grouping must never put + /// an X LOAD in the same span as a W one. + pub fn exec_blessed(&self) -> bool { + self.access & access::X != 0 + } +} + +/// Why a grouping refused. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum GroupError { + /// A segment's address or size does not fit the address space. + Overflow, + /// The image needs more spans than the caller's budget affords. + TooManySpans, + /// The image has no loadable segment. + Empty, +} + +/// **The contract**: how LOADs become lease-sized spans. +/// +/// One implementation per budget, selected by type instantiation at the +/// call site. A new policy is a new impl — never a branch in the stager. +pub trait Grouping { + /// Fold `segments` into spans, in ascending address order. + /// + /// `N` is the caller's span budget: the founder's staging stride for + /// a service, the convey table's width for a wire founding. + fn group( + segments: &[LoadSegment<'_>], + ) -> Result, GroupError>; +} + +/// Access bits of one parsed LOAD. +fn access_of(seg: &LoadSegment<'_>) -> u8 { + let mut a = 0; + if seg.flags.read { + a |= access::R; + } + if seg.flags.write { + a |= access::W; + } + if seg.flags.exec { + a |= access::X; + } + a +} + +/// The page-rounded span one LOAD occupies. +fn span_of(seg: &LoadSegment<'_>) -> Result<(u64, u64), GroupError> { + let base = seg.vaddr & !(PAGE - 1); + let end = seg + .vaddr + .checked_add(seg.memsz) + .ok_or(GroupError::Overflow)? + .next_multiple_of(PAGE); + Ok((base, end)) +} + +/// **One lease per `PT_LOAD`** — `found_service`'s policy. +/// +/// The founder stages each segment into its own slot of a fixed VA +/// stride, so segments cost founder slots rather than a shared table; +/// there is nothing to gain by folding and a per-LOAD lease keeps each +/// segment's access mode exactly as the linker emitted it. +pub struct PerLoad; + +impl Grouping for PerLoad { + fn group( + segments: &[LoadSegment<'_>], + ) -> Result, GroupError> { + let mut out: HVec = HVec::new(); + for seg in segments { + let (base, end) = span_of(seg)?; + out.push(ImageSpan { + base, + end, + access: access_of(seg), + }) + .map_err(|_| GroupError::TooManySpans)?; + } + if out.is_empty() { + return Err(GroupError::Empty); + } + Ok(out) + } +} + +/// **One lease per maximal run of page-adjacent same-access LOADs** — +/// the wire founding's policy. +/// +/// A conveyed lease costs a convey-table entry and that table is 16 +/// wide, so folding is what keeps a founding inside its budget: the +/// shell's four LOADs collapse to three runs (its two R|W segments are +/// contiguous). +/// +/// The fold is a **suffix test**, which is sound only because `kore-ld` +/// already proved the LOADs page-disjoint and ascending: a segment can +/// only ever extend the run it directly follows. It deliberately never +/// merges across an access change — an X run and a W run in one lease +/// would be unshareable, since the per-lease W^X gate refuses a WRITE +/// share off a blessed lease and an EXECUTE share off an unblessed one. +pub struct PerAccessRun; + +impl Grouping for PerAccessRun { + fn group( + segments: &[LoadSegment<'_>], + ) -> Result, GroupError> { + let mut out: HVec = HVec::new(); + for seg in segments { + let (base, end) = span_of(seg)?; + let acc = access_of(seg); + match out.last_mut() { + Some(run) if run.end == base && run.access == acc => run.end = end, + _ => out + .push(ImageSpan { + base, + end, + access: acc, + }) + .map_err(|_| GroupError::TooManySpans)?, + } + } + if out.is_empty() { + return Err(GroupError::Empty); + } + Ok(out) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parse::SegFlags; + + fn seg(vaddr: u64, memsz: u64, flags: SegFlags) -> LoadSegment<'static> { + LoadSegment { + vaddr, + memsz, + filesz: memsz, + file_off: vaddr, + align: PAGE, + flags, + bytes: &[], + } + } + + const R: SegFlags = SegFlags { + read: true, + write: false, + exec: false, + }; + const RX: SegFlags = SegFlags { + read: true, + write: false, + exec: true, + }; + const RW: SegFlags = SegFlags { + read: true, + write: true, + exec: false, + }; + + /// The committed shell's real shape: four LOADs, the last two R|W and + /// page-adjacent. This is the case the convey budget depends on — + /// four spans would put the founding at 17 of 16 conveys. + fn shell_like() -> [LoadSegment<'static>; 4] { + [ + seg(0x4000_0000, 0x123c, R), + seg(0x4000_2240, 0x9945, RX), + seg(0x4000_cb88, 0x478, RW), + seg(0x4000_de68, 0xb38, RW), + ] + } + + /// `PerAccessRun` folds the shell to three spans, and the two R|W + /// LOADs become one lease. + #[test] + fn access_runs_fold_the_shell_to_three() { + let runs = PerAccessRun::group::<8>(&shell_like()).expect("groups"); + assert_eq!(runs.len(), 3, "R, R|X, R|W"); + assert_eq!( + runs[0], + ImageSpan { + base: 0x4000_0000, + end: 0x4000_2000, + access: access::R + } + ); + assert_eq!( + runs[1], + ImageSpan { + base: 0x4000_2000, + end: 0x4000_c000, + access: access::R | access::X + } + ); + assert_eq!( + runs[2], + ImageSpan { + base: 0x4000_c000, + end: 0x4000_f000, + access: access::R | access::W + }, + "the two adjacent R|W LOADs share one lease" + ); + } + + /// The same image under the per-LOAD policy keeps four spans — the + /// policies genuinely differ, which is why this is a seam and not a + /// single algorithm someone forgot to share. + #[test] + fn per_load_keeps_every_segment_separate() { + let spans = PerLoad::group::<8>(&shell_like()).expect("groups"); + assert_eq!(spans.len(), 4); + assert_eq!(spans[2].access, access::R | access::W); + assert_eq!(spans[3].access, access::R | access::W); + } + + /// Only *adjacent* same-access LOADs fold. A gap between two R|W + /// segments must stay two leases — merging across it would hand the + /// child a mapping over addresses the image never claimed. + #[test] + fn a_gap_breaks_a_run() { + let segs = [seg(0x4000_0000, 0x100, RW), seg(0x4000_4000, 0x100, RW)]; + let runs = PerAccessRun::group::<8>(&segs).expect("groups"); + assert_eq!(runs.len(), 2, "non-adjacent spans do not merge"); + } + + /// An access change breaks a run even when the spans touch. Folding + /// X and W into one lease would be unshareable at both ends: the + /// per-lease W^X gate refuses a WRITE share off a blessed lease and + /// an EXECUTE share off an unblessed one. + #[test] + fn an_access_change_breaks_a_run_even_when_adjacent() { + let segs = [seg(0x4000_0000, 0x1000, RX), seg(0x4000_1000, 0x1000, RW)]; + let runs = PerAccessRun::group::<8>(&segs).expect("groups"); + assert_eq!(runs.len(), 2); + assert!(runs[0].exec_blessed()); + assert!(!runs[1].exec_blessed()); + } + + /// The budget is the caller's, and overflowing it refuses rather than + /// truncating — a dropped span is a segment the child never gets. + #[test] + fn an_overflowing_budget_refuses() { + assert_eq!( + PerLoad::group::<2>(&shell_like()), + Err(GroupError::TooManySpans) + ); + // The same image fits under the folding policy, which is the + // whole reason that policy exists. + assert!(PerAccessRun::group::<3>(&shell_like()).is_ok()); + } + + /// An image with no loadable segment is refused by both policies — + /// there is nothing to found from. + #[test] + fn no_loadable_segment_refuses() { + assert_eq!(PerLoad::group::<8>(&[]), Err(GroupError::Empty)); + assert_eq!(PerAccessRun::group::<8>(&[]), Err(GroupError::Empty)); + } + + /// A segment whose end wraps the address space refuses before any + /// span is produced. + #[test] + fn an_overflowing_segment_refuses() { + let segs = [seg(u64::MAX - 0xfff, 0x2000, R)]; + assert_eq!(PerAccessRun::group::<8>(&segs), Err(GroupError::Overflow)); + } +} diff --git a/native/crates/ld/src/store.rs b/native/crates/ld/src/store.rs new file mode 100644 index 0000000..28e88e6 --- /dev/null +++ b/native/crates/ld/src/store.rs @@ -0,0 +1,236 @@ +//! `store` — the link engine's **caller-provided capacity** (Phase A, +//! `res/USERLAND.md` §7): the fixed `[T; MAX_*]` tables become +//! [`Allocator`]-parameterized slots carved from an arena the *caller* +//! owns — the abi cap.rs TODO's direction, realized here first. +//! +//! Two pieces: +//! +//! - [`LinkArena`] — a bump allocator over a **borrowed buffer** +//! (`core::alloc::Allocator`, no `alloc` crate anywhere: the linker +//! stub must keep linking with **no global allocator at all**). The +//! stub sizes one buffer for its whole link; a test leaks one; either +//! way the capacity decision moves to the caller, and a small program +//! (a static exe, two modules) pays for two modules' tables, not +//! sixteen. +//! - `Slots` (crate-internal) — a fixed-capacity, fully-initialized +//! slice allocated from any `A: Allocator` (arena-backed in +//! practice), the engine's table primitive. Never grows; capacity is +//! decided at construction ([`LinkCaps`](crate::link::LinkCaps) sizes +//! it), so the bounded-everything regime is unchanged — only *who +//! chooses the bound* moved. +//! +//! Dropping a `Slots` drops its elements and returns the block to its +//! allocator (a no-op for the bump arena — the buffer frees wholesale +//! when the caller's borrow ends). + +use core::alloc::{AllocError, Allocator, Layout}; +use core::cell::Cell; +use core::marker::PhantomData; +use core::mem::MaybeUninit; +use core::ops::{Index, IndexMut}; +use core::ptr::NonNull; + +/// A bump allocator over a caller-borrowed byte buffer — the link +/// engine's arena. `allocate` bumps (aligned, bounds-checked, +/// fail-closed [`AllocError`] on exhaustion); `deallocate` is a no-op +/// (arena discipline: the buffer is reclaimed wholesale when the borrow +/// ends). Single-threaded (`Cell` cursor) — the linker stub is one +/// task, once, at startup. +pub struct LinkArena<'a> { + base: *mut u8, + len: usize, + cursor: Cell, + _buf: PhantomData<&'a mut [MaybeUninit]>, +} + +impl<'a> LinkArena<'a> { + /// An arena over `buf` — the caller's capacity decision, in bytes. + pub fn new(buf: &'a mut [MaybeUninit]) -> Self { + Self { + base: buf.as_mut_ptr() as *mut u8, + len: buf.len(), + cursor: Cell::new(0), + _buf: PhantomData, + } + } + + /// Bytes handed out so far (telemetry — what a stub's buffer must + /// at least hold for a given link). + pub fn used(&self) -> usize { + self.cursor.get() + } +} + +// SAFETY: `allocate` returns blocks that stay valid and disjoint until +// the arena's borrow ends (the buffer is exclusively borrowed for 'a, +// the cursor only grows, and deallocate never recycles); clones of the +// handle (`&LinkArena`) share the one cursor. +unsafe impl Allocator for LinkArena<'_> { + fn allocate(&self, layout: Layout) -> Result, AllocError> { + if layout.size() == 0 { + // A zero-size allocation is a dangling, aligned, valid-empty + // block (the Allocator contract's degenerate case). + let dangling = NonNull::new(layout.align() as *mut u8).ok_or(AllocError)?; + return Ok(NonNull::slice_from_raw_parts(dangling, 0)); + } + let cur = self.cursor.get(); + // Align the next block relative to the buffer's real address. + let at = (self.base as usize).checked_add(cur).ok_or(AllocError)?; + let aligned = at.checked_add(layout.align() - 1).ok_or(AllocError)? & !(layout.align() - 1); + let pad = aligned - at; + let start = cur.checked_add(pad).ok_or(AllocError)?; + let end = start.checked_add(layout.size()).ok_or(AllocError)?; + if end > self.len { + return Err(AllocError); // exhausted — fail closed, never wrap + } + self.cursor.set(end); + // SAFETY: `start..end` is in bounds of the borrowed buffer. + let ptr = unsafe { self.base.add(start) }; + Ok(NonNull::slice_from_raw_parts( + NonNull::new(ptr).ok_or(AllocError)?, + layout.size(), + )) + } + + unsafe fn deallocate(&self, _ptr: NonNull, _layout: Layout) { + // Bump arena: individual frees are no-ops; the buffer is + // reclaimed wholesale when the caller's borrow ends. + } +} + +/// A fixed-capacity, always-fully-initialized slice allocated from `A` +/// — the engine's table primitive (the `[T; MAX]` replacement). The +/// capacity is chosen at construction and never changes; element access +/// is the plain slice API. +pub(crate) struct Slots { + ptr: NonNull, + cap: usize, + alloc: A, +} + +impl Slots { + /// Allocate `cap` slots from `alloc`, initializing slot `i` with + /// `f(i)`. Zero-capacity allocates nothing (a dangling, never-read + /// pointer). + pub(crate) fn new_from( + cap: usize, + alloc: A, + mut f: impl FnMut(usize) -> T, + ) -> Result { + if cap == 0 { + return Ok(Self { + ptr: NonNull::dangling(), + cap: 0, + alloc, + }); + } + let layout = Layout::array::(cap).map_err(|_| AllocError)?; + let raw = alloc.allocate(layout)?; + let ptr = raw.cast::(); + for i in 0..cap { + // SAFETY: `i < cap`, inside the just-allocated block. + unsafe { ptr.add(i).write(f(i)) }; + } + Ok(Self { ptr, cap, alloc }) + } + + #[inline] + pub(crate) fn as_slice(&self) -> &[T] { + // SAFETY: `cap` elements were initialized at construction and + // stay owned by this value. + unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), self.cap) } + } + + #[inline] + pub(crate) fn as_mut_slice(&mut self) -> &mut [T] { + // SAFETY: as `as_slice`, with `&mut self` exclusivity. + unsafe { core::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.cap) } + } +} + +impl Drop for Slots { + fn drop(&mut self) { + if self.cap == 0 { + return; + } + // SAFETY: fully initialized; dropped exactly once here. + unsafe { + core::ptr::drop_in_place(core::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.cap)); + // Layout::array succeeded at construction for this cap. + let layout = Layout::array::(self.cap).unwrap_unchecked(); + self.alloc.deallocate(self.ptr.cast(), layout); + } + } +} + +impl Index for Slots { + type Output = T; + #[inline] + fn index(&self, i: usize) -> &T { + &self.as_slice()[i] + } +} + +impl IndexMut for Slots { + #[inline] + fn index_mut(&mut self, i: usize) -> &mut T { + &mut self.as_mut_slice()[i] + } +} + +impl core::fmt::Debug for Slots { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.as_slice().fmt(f) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn arena_bumps_aligned_and_fails_closed() { + let mut buf = [MaybeUninit::::uninit(); 256]; + let arena = LinkArena::new(&mut buf); + + let a = arena + .allocate(Layout::from_size_align(10, 1).unwrap()) + .expect("fits"); + let b = arena + .allocate(Layout::from_size_align(16, 16).unwrap()) + .expect("fits aligned"); + assert_eq!(b.cast::().as_ptr() as usize % 16, 0, "aligned bump"); + assert_ne!(a.cast::().as_ptr(), b.cast::().as_ptr()); + // Exhaustion refuses, never wraps. + assert!( + arena + .allocate(Layout::from_size_align(1024, 1).unwrap()) + .is_err(), + "over-ask fails closed" + ); + // Zero-size succeeds without consuming the buffer. + let used = arena.used(); + arena + .allocate(Layout::from_size_align(0, 8).unwrap()) + .expect("zero-size is the degenerate success"); + assert_eq!(arena.used(), used); + } + + #[test] + fn slots_init_index_and_drop() { + let mut buf = [MaybeUninit::::uninit(); 1024]; + let arena = LinkArena::new(&mut buf); + let mut s = Slots::::new_from(8, &arena, |i| i as u64 * 3).expect("slots"); + assert_eq!(s.as_slice().len(), 8); + assert_eq!(s[5], 15); + s[5] = 99; + assert_eq!(s.as_slice()[5], 99); + + // Zero-capacity is a valid empty table. + let z = Slots::::new_from(0, &arena, |_| 0).expect("empty"); + assert!(z.as_slice().is_empty()); + + // Exhaustion propagates as AllocError. + assert!(Slots::::new_from(4096, &arena, |_| 0).is_err()); + } +} diff --git a/native/crates/ld/src/stub.rs b/native/crates/ld/src/stub.rs new file mode 100644 index 0000000..b09d2e8 --- /dev/null +++ b/native/crates/ld/src/stub.rs @@ -0,0 +1,154 @@ +//! The linker stub's **decisions**, separated from its entry. +//! +//! `user/runtime/ld-stub` is the binary whose validated PT_LOADs the parent shares inside +//! `LAYOUT.stub` before entering its ELF entry in every two-phase founding +//! (`res/IMAGE.md` §4, phase 2). +//! Its `_start` is a handful of instructions — read a block, jump — but +//! the reading is a gate: an unstamped page, a library with no entry, and +//! a founding that passed no boot page must all refuse rather than jump +//! somewhere arbitrary, in a context with no console, no channel and no +//! runtime to report from. +//! +//! That gate lives here rather than in the binary because **`user/` is a +//! separate workspace, excluded from the root one on purpose** — `make +//! test` runs `test-kore test-crates` and reaches none of it (userland +//! behaviour is proven by the QEMU boot gates instead). A `#[cfg(test)]` +//! module inside `user/runtime/ld-stub` would never run, so the logic that decides +//! whether to jump would be the one part of the stub nothing exercises. +//! `kore-ld` is already in the root workspace and already owns image +//! shapes, so the decision is testable here and the binary keeps only the +//! `unsafe` it cannot avoid. + +use kore_abi::image::{IMAGE_ABI, LdBlock, kind}; +use kore_abi::task::BootBlock; + +/// Byte offset of the `.kore.ld` block within the boot page. +/// +/// Mirrors `kore_rt::entry::LD_BLOCK_AT`. Restated rather than imported +/// because the stub cannot depend on `kore-rt` — it runs *before* the +/// runtime it hands control to — and `kore-ld` must not either. +/// [`ld_block_offset_matches_the_runtime_layout`] pins the two together. +/// +/// [`ld_block_offset_matches_the_runtime_layout`]: self::tests +pub const LD_BLOCK_AT: usize = core::mem::size_of::(); + +/// The exe's entry VA from a parent-written `.kore.ld` block — a VA the +/// stub may jump to **directly**, or `None` meaning *park*. +/// +/// Three refusals, all of them "do not jump": +/// +/// - no [`IMAGE_ABI`] stamp — an unwritten boot page reads all-zero, so +/// the stamp separates "the parent wrote this" from "nobody did"; +/// - a kind whose addresses are not absolute ([`kind::is_absolute`]) — +/// an [`EXECUTABLE`](kind::EXECUTABLE) descriptor is image-relative and +/// its entry means nothing until a load base is chosen, which the stub +/// does not do until it maps the image itself. Reading the kind is +/// what keeps that a refusal instead of a jump to an offset; +/// - a zero entry — a library, which has nothing to transfer to. +pub fn exe_entry(ld: &LdBlock) -> Option { + if ld.abi.get() != IMAGE_ABI { + return None; + } + if !kind::is_absolute(ld.exe.kind) { + return None; + } + match ld.exe.entry.get() { + 0 => None, + entry => Some(entry), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use kore_abi::image::ModuleDescriptor; + + /// The stub reads `.kore.ld` at an offset it restates; a drift from + /// the runtime's layout would have it decode the *geometry* block as + /// a link block and jump to whatever that produced. + #[test] + fn ld_block_offset_matches_the_runtime_layout() { + assert_eq!( + LD_BLOCK_AT, + core::mem::size_of::(), + "`.kore.ld` sits immediately past `.kore.boot`" + ); + } + + /// An unwritten boot page is all-zero. Refusing it is the difference + /// between parking and jumping to address zero. + #[test] + fn an_unstamped_block_refuses() { + let blank = LdBlock { + abi: 0.into(), + flags: 0.into(), + reserved: 0.into(), + exe_name: kore_abi::image::ImageName::empty(), + exe: ModuleDescriptor::empty(), + }; + assert_eq!(exe_entry(&blank), None); + } + + /// A stamped block whose descriptor is a library names no entry. + /// Distinct from a failed read, and equally not jumpable. + #[test] + fn a_stamped_library_has_no_entry() { + let exe = ModuleDescriptor { + kind: kind::STATIC_EXECUTABLE, + entry: 0.into(), + ..ModuleDescriptor::empty() + }; + assert_eq!( + exe_entry(&LdBlock::new(kore_abi::image::ImageName::empty(), exe)), + None + ); + } + + /// **An image-relative descriptor refuses.** A PIE executable's + /// entry is an offset, not an address; jumping to it would land at a + /// low address that happens to be mapped by something else. The + /// descriptor says which convention it uses, so the stub can tell — + /// it cannot see the image, only the block. + #[test] + fn a_relocatable_executable_refuses_its_relative_entry() { + let exe = ModuleDescriptor { + kind: kind::EXECUTABLE, + entry: 0x1190.into(), + ..ModuleDescriptor::empty() + }; + assert_eq!( + exe_entry(&LdBlock::new(kore_abi::image::ImageName::empty(), exe)), + None, + "image-relative entries need a load base the stub has not chosen" + ); + } + + /// An unknown kind is refused, not assumed absolute — the fail-closed + /// direction for a vocabulary that grows. + #[test] + fn an_unknown_kind_refuses() { + let exe = ModuleDescriptor { + kind: 0xff, + entry: 0x4000_2240.into(), + ..ModuleDescriptor::empty() + }; + assert_eq!( + exe_entry(&LdBlock::new(kore_abi::image::ImageName::empty(), exe)), + None + ); + } + + /// The happy path yields exactly the entry the parent recorded. + #[test] + fn a_stamped_executable_yields_its_entry() { + let exe = ModuleDescriptor { + kind: kind::STATIC_EXECUTABLE, + entry: 0x4000_2240.into(), + ..ModuleDescriptor::empty() + }; + assert_eq!( + exe_entry(&LdBlock::new(kore_abi::image::ImageName::empty(), exe)), + Some(0x4000_2240) + ); + } +} diff --git a/native/crates/ld/tests/apply_engine.rs b/native/crates/ld/tests/apply_engine.rs new file mode 100644 index 0000000..a74d29e --- /dev/null +++ b/native/crates/ld/tests/apply_engine.rs @@ -0,0 +1,817 @@ +//! Apply-engine suite: value computation against hand-computed +//! expectations, cross-module GNU-hash resolution, RELR decode, the +//! refusal set (WriteOutOfRange / Overflow / MissingSymbol / Tls / +//! Reloc), and the psABI TLS layout numbers for all three variants. + +/// A leaked caller arena — the Phase A capacity seam's test face +/// (`res/USERLAND.md` §7: the tables are arena-carved, caller-sized). +fn arena() -> &'static kore_ld::store::LinkArena<'static> { + let buf = Box::leak(Box::new([core::mem::MaybeUninit::::uninit(); 1 << 16])); + Box::leak(Box::new(kore_ld::store::LinkArena::new(buf))) +} + +mod common; + +use common::*; +use kore_abi::image::{IMAGE_ABI, MAX_SEGMENTS, ModuleDescriptor, kind, seg}; +use kore_ld::apply::{ + Applied, ApplyError, ModuleImage, ModuleView, SymbolScope, TlsLayout, apply_module, + lookup_symbol, +}; +use kore_ld::check::{CheckedImage, check_image}; +use kore_ld::reloc::{AArch64, RelocRefusal, Riscv64, X86_64}; + +// --------------------------------------------------------------------------- +// Plumbing: build ModuleViews / data copies from a checked fixture. +// The fixtures link at base 0, so image-relative offsets equal file +// offsets and the raw image doubles as the mapped segments. +// --------------------------------------------------------------------------- + +fn view_of<'a>(img: &'a [u8], d: &'a ModuleDescriptor, base: u64) -> ModuleView<'a> { + let mut ro = [None; MAX_SEGMENTS]; + for i in 0..d.seg_count as usize { + let s = &d.segs[i]; + if s.kind == seg::KIND_TEXT || s.kind == seg::KIND_RODATA { + let a = s.vaddr.get() as usize; + let b = a + s.filesz.get() as usize; + ro[i] = Some(&img[a..b]); + } + } + ModuleView { + base, + descriptor: d, + ro, + } +} + +/// The consumer's private copy of the data template: filesz bytes from +/// the image, zero-extended to memsz (the .bss tail). +fn data_copy(img: &[u8], d: &ModuleDescriptor) -> Vec { + for i in 0..d.seg_count as usize { + let s = &d.segs[i]; + if s.kind == seg::KIND_DATA { + let a = s.vaddr.get() as usize; + let mut v = img[a..a + s.filesz.get() as usize].to_vec(); + v.resize(s.memsz.get() as usize, 0); + return v; + } + } + Vec::new() +} + +/// Raw u64 poke into a built image (post-`check` corruption for the +/// defense-in-depth cases). +fn write_raw64(img: &mut [u8], off: usize, v: u64) { + img[off..off + 8].copy_from_slice(&v.to_le_bytes()); +} + +const BASE_A: u64 = 0x7f00_0000; +const BASE_B: u64 = 0x0100_0000; +const BASE_C: u64 = 0x0300_0000; + +/// One-module apply: check the fixture, map views at `base`, run the +/// engine, return (Applied, data). +fn apply_single( + img: &[u8], + base: u64, +) -> Result<(Applied, Vec), ApplyError> { + let checked: CheckedImage = check_image::(img).expect("fixture must check"); + let d = checked.descriptor; + let views = [view_of(img, &d, base)]; + let scope = SymbolScope { modules: &views }; + let tls = TlsLayout::build::(&views, arena()).unwrap(); + let mut data = data_copy(img, &d); + let mut module = ModuleImage { + scope_index: 0, + view: view_of(img, &d, base), + data: &mut data, + relro: checked.relro, + }; + let applied = apply_module::(&mut module, &scope, &tls)?; + Ok((applied, data)) +} + +// --------------------------------------------------------------------------- +// Value computation (hand-computed expectations) +// --------------------------------------------------------------------------- + +#[test] +fn relative_value() { + let mut f = Fixture::new(EM_X86_64); + f.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: 8, + sym: 0, + addend: 0x123, + }]; + f.relro = Some((DATA_VADDR, 0x1000)); + let (applied, data) = apply_single::(&f.build(), BASE_A).unwrap(); + // B + A = 0x7f00_0000 + 0x123. + assert_eq!(read64(&data, 0x100), BASE_A + 0x123); + assert_eq!(applied.applied, 1); + // RELRO reported absolute: base + image-relative window. + assert_eq!(applied.relro, Some((BASE_A + DATA_VADDR, 0x1000))); +} + +#[test] +fn glob_dat_cross_module() { + // A imports beta_fn; B defines it at 0x1020. The GOT slot takes S + // (no addend): BASE_B + 0x1020. + let mut fa = Fixture::new(EM_X86_64); + fa.syms = vec![Sym::import("beta_fn")]; + fa.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: 6, + sym: 1, + addend: 0, + }]; + let img_a = fa.build(); + let mut fb = Fixture::new(EM_X86_64); + fb.syms = vec![Sym::global("beta_fn", TEXT_VADDR + 0x20)]; + let img_b = fb.build(); + + let da = check_image::(&img_a).unwrap().descriptor; + let db = check_image::(&img_b).unwrap().descriptor; + let views = [view_of(&img_a, &da, BASE_A), view_of(&img_b, &db, BASE_B)]; + let scope = SymbolScope { modules: &views }; + let tls = TlsLayout::build::(&views, arena()).unwrap(); + let mut data = data_copy(&img_a, &da); + let mut module = ModuleImage { + scope_index: 0, + view: view_of(&img_a, &da, BASE_A), + data: &mut data, + relro: None, + }; + apply_module::(&mut module, &scope, &tls).unwrap(); + assert_eq!(read64(&data, 0x100), BASE_B + TEXT_VADDR + 0x20); +} + +#[test] +fn jump_slot_via_plt() { + // Same shape through .rela.plt — identical semantics under -z now. + let mut fa = Fixture::new(EM_X86_64); + fa.syms = vec![Sym::import("beta_fn")]; + fa.plt_relas = vec![Rela { + offset: DATA_VADDR + 0x108, + rtype: 7, + sym: 1, + addend: 0, + }]; + let img_a = fa.build(); + let mut fb = Fixture::new(EM_X86_64); + fb.syms = vec![Sym::global("beta_fn", TEXT_VADDR + 0x40)]; + let img_b = fb.build(); + + let da = check_image::(&img_a).unwrap().descriptor; + let db = check_image::(&img_b).unwrap().descriptor; + let views = [view_of(&img_a, &da, BASE_A), view_of(&img_b, &db, BASE_B)]; + let scope = SymbolScope { modules: &views }; + let tls = TlsLayout::build::(&views, arena()).unwrap(); + let mut data = data_copy(&img_a, &da); + let mut module = ModuleImage { + scope_index: 0, + view: view_of(&img_a, &da, BASE_A), + data: &mut data, + relro: None, + }; + apply_module::(&mut module, &scope, &tls).unwrap(); + assert_eq!(read64(&data, 0x108), BASE_B + TEXT_VADDR + 0x40); +} + +#[test] +fn abs64_value_with_addend() { + // S + A: the module's own definition plus the addend. + let mut f = Fixture::new(EM_X86_64); + f.syms = vec![Sym::global("obj", DATA_VADDR + 0x300)]; + f.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: 1, + sym: 1, + addend: 0x10, + }]; + let (_, data) = apply_single::(&f.build(), BASE_A).unwrap(); + assert_eq!(read64(&data, 0x100), BASE_A + DATA_VADDR + 0x300 + 0x10); +} + +#[test] +fn engine_is_arch_generic() { + // The same semantics through the aarch64 and riscv64 tables. + for (machine, relative) in [(EM_AARCH64, 1027u32), (EM_RISCV, 3u32)] { + let mut f = Fixture::new(machine); + f.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: relative, + sym: 0, + addend: 0x55, + }]; + let img = f.build(); + let (applied, data) = match machine { + EM_AARCH64 => apply_single::(&img, BASE_A).unwrap(), + _ => apply_single::(&img, BASE_A).unwrap(), + }; + assert_eq!(read64(&data, 0x100), BASE_A + 0x55, "machine {machine}"); + assert_eq!(applied.applied, 1); + } +} + +// --------------------------------------------------------------------------- +// Resolution order +// --------------------------------------------------------------------------- + +#[test] +fn strong_preferred_over_earlier_weak() { + // B (earlier in scope) defines `dual` weak; C defines it strong. + // Strong wins across the whole scope, position notwithstanding. + let mut fa = Fixture::new(EM_X86_64); + fa.syms = vec![Sym::import("dual")]; + fa.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: 6, + sym: 1, + addend: 0, + }]; + let img_a = fa.build(); + let mut fb = Fixture::new(EM_X86_64); + fb.syms = vec![Sym::weak("dual", TEXT_VADDR + 0x30)]; + let img_b = fb.build(); + let mut fc = Fixture::new(EM_X86_64); + fc.syms = vec![Sym::global("dual", TEXT_VADDR + 0x40)]; + let img_c = fc.build(); + + let da = check_image::(&img_a).unwrap().descriptor; + let db = check_image::(&img_b).unwrap().descriptor; + let dc = check_image::(&img_c).unwrap().descriptor; + let views = [ + view_of(&img_a, &da, BASE_A), + view_of(&img_b, &db, BASE_B), + view_of(&img_c, &dc, BASE_C), + ]; + let scope = SymbolScope { modules: &views }; + let tls = TlsLayout::build::(&views, arena()).unwrap(); + let mut data = data_copy(&img_a, &da); + let mut module = ModuleImage { + scope_index: 0, + view: view_of(&img_a, &da, BASE_A), + data: &mut data, + relro: None, + }; + apply_module::(&mut module, &scope, &tls).unwrap(); + assert_eq!(read64(&data, 0x100), BASE_C + TEXT_VADDR + 0x40); +} + +#[test] +fn first_weak_wins_when_no_strong() { + let mut fa = Fixture::new(EM_X86_64); + fa.syms = vec![Sym::import("soft")]; + fa.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: 6, + sym: 1, + addend: 0, + }]; + let img_a = fa.build(); + let mut fb = Fixture::new(EM_X86_64); + fb.syms = vec![Sym::weak("soft", TEXT_VADDR + 0x50)]; + let img_b = fb.build(); + let mut fc = Fixture::new(EM_X86_64); + fc.syms = vec![Sym::weak("soft", TEXT_VADDR + 0x60)]; + let img_c = fc.build(); + + let da = check_image::(&img_a).unwrap().descriptor; + let db = check_image::(&img_b).unwrap().descriptor; + let dc = check_image::(&img_c).unwrap().descriptor; + let views = [ + view_of(&img_a, &da, BASE_A), + view_of(&img_b, &db, BASE_B), + view_of(&img_c, &dc, BASE_C), + ]; + let scope = SymbolScope { modules: &views }; + let tls = TlsLayout::build::(&views, arena()).unwrap(); + let mut data = data_copy(&img_a, &da); + let mut module = ModuleImage { + scope_index: 0, + view: view_of(&img_a, &da, BASE_A), + data: &mut data, + relro: None, + }; + apply_module::(&mut module, &scope, &tls).unwrap(); + assert_eq!(read64(&data, 0x100), BASE_B + TEXT_VADDR + 0x50); +} + +#[test] +fn missing_symbol_refuses() { + let mut f = Fixture::new(EM_X86_64); + f.syms = vec![Sym::import("ghost")]; + f.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: 6, + sym: 1, + addend: 0, + }]; + assert_eq!( + apply_single::(&f.build(), BASE_A).unwrap_err(), + ApplyError::MissingSymbol(0) + ); +} + +#[test] +fn weak_undef_resolves_to_zero() { + // The gABI rule: a *weak* reference nothing in scope defines takes + // S = 0 — GOT slots read zero, Abs64 keeps its addend (0 + A). + let mut f = Fixture::new(EM_X86_64); + f.syms = vec![Sym::weak_import("maybe")]; + f.relas = vec![ + Rela { + offset: DATA_VADDR + 0x100, + rtype: 6, + sym: 1, + addend: 0, + }, + Rela { + offset: DATA_VADDR + 0x108, + rtype: 1, + sym: 1, + addend: 5, + }, + ]; + let mut img = f.build(); + // Seed the slots nonzero to prove the writes happened. + write_raw64(&mut img, (DATA_VADDR + 0x100) as usize, 0xdead); + write_raw64(&mut img, (DATA_VADDR + 0x108) as usize, 0xdead); + let (applied, data) = apply_single::(&img, BASE_A).unwrap(); + assert_eq!(applied.applied, 2); + assert_eq!(read64(&data, 0x100), 0); + assert_eq!(read64(&data, 0x108), 5); +} + +#[test] +fn weak_undef_tls_still_refuses() { + // A weak-undefined *TLS* symbol earns no zero: a zero tp-offset + // would alias a real block, so the TpRel path fails closed. + let mut f = Fixture::new(EM_X86_64); + f.tls = Some(TlsSpec { + memsz: 0x10, + align: 8, + filesz: 0, + }); + f.syms = vec![Sym::weak_import("tmaybe")]; + f.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: 18, + sym: 1, + addend: 0, + }]; + assert_eq!( + apply_single::(&f.build(), BASE_A).unwrap_err(), + ApplyError::Tls(0) + ); +} + +#[test] +fn zero_bloom_hides_definitions() { + // The bloom filter is honored: a zeroed bloom word makes B's + // definition invisible, so resolution refuses. + let mut fa = Fixture::new(EM_X86_64); + fa.syms = vec![Sym::import("beta_fn")]; + fa.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: 6, + sym: 1, + addend: 0, + }]; + let img_a = fa.build(); + let mut fb = Fixture::new(EM_X86_64); + fb.syms = vec![Sym::global("beta_fn", TEXT_VADDR + 0x20)]; + fb.zero_bloom = true; + let img_b = fb.build(); + + let da = check_image::(&img_a).unwrap().descriptor; + let db = check_image::(&img_b).unwrap().descriptor; + let views = [view_of(&img_a, &da, BASE_A), view_of(&img_b, &db, BASE_B)]; + let scope = SymbolScope { modules: &views }; + let tls = TlsLayout::build::(&views, arena()).unwrap(); + let mut data = data_copy(&img_a, &da); + let mut module = ModuleImage { + scope_index: 0, + view: view_of(&img_a, &da, BASE_A), + data: &mut data, + relro: None, + }; + assert_eq!( + apply_module::(&mut module, &scope, &tls).unwrap_err(), + ApplyError::MissingSymbol(0) + ); +} + +// --------------------------------------------------------------------------- +// RELR +// --------------------------------------------------------------------------- + +#[test] +fn relr_decode_hand_built_bitmap() { + // Encoding (hand-derived): + // word 0 = 0x2100 (even) → relocate 0x2100; anchor = 0x2108 + // word 1 = ((1|1<<2) << 1) | 1 → bitmap: bit0 → 0x2108, + // bit2 → 0x2118 (0x2110 skipped) + let mut f = Fixture::new(EM_X86_64); + f.relr = vec![DATA_VADDR + 0x100, ((1 | 1 << 2) << 1) | 1]; + let mut img = f.build(); + // Seed the in-place addends RELR consumes. + write_raw64(&mut img, (DATA_VADDR + 0x100) as usize, 0x111); + write_raw64(&mut img, (DATA_VADDR + 0x108) as usize, 0x222); + write_raw64(&mut img, (DATA_VADDR + 0x110) as usize, 0xdead); + write_raw64(&mut img, (DATA_VADDR + 0x118) as usize, 0x333); + + let (applied, data) = apply_single::(&img, BASE_A).unwrap(); + assert_eq!(applied.applied, 3); + assert_eq!(read64(&data, 0x100), BASE_A + 0x111); + assert_eq!(read64(&data, 0x108), BASE_A + 0x222); + // The skipped word keeps its raw value — no base added. + assert_eq!(read64(&data, 0x110), 0xdead); + assert_eq!(read64(&data, 0x118), BASE_A + 0x333); +} + +#[test] +fn applied_counts_all_tables() { + // 1 (.rela.dyn) + 3 (RELR: anchor + two bitmap bits) + 1 (.rela.plt). + let mut f = Fixture::new(EM_X86_64); + f.relas = vec![Rela { + offset: DATA_VADDR + 0x200, + rtype: 8, + sym: 0, + addend: 1, + }]; + f.plt_relas = vec![Rela { + offset: DATA_VADDR + 0x208, + rtype: 8, + sym: 0, + addend: 2, + }]; + f.relr = vec![DATA_VADDR + 0x100, ((1 | 1 << 2) << 1) | 1]; + let (applied, _) = apply_single::(&f.build(), BASE_A).unwrap(); + assert_eq!(applied.applied, 5); +} + +// --------------------------------------------------------------------------- +// Refusals +// --------------------------------------------------------------------------- + +#[test] +fn write_out_of_range_refuses() { + // Into rodata (below the data window) … + let mut f = Fixture::new(EM_X86_64); + f.relas = vec![Rela { + offset: 0x500, + rtype: 8, + sym: 0, + addend: 0, + }]; + assert_eq!( + apply_single::(&f.build(), BASE_A).unwrap_err(), + ApplyError::WriteOutOfRange(0) + ); + // … and just past the data segment's memsz. + let mut f = Fixture::new(EM_X86_64); + f.relas = vec![Rela { + offset: DATA_VADDR + PAGE, + rtype: 8, + sym: 0, + addend: 0, + }]; + assert_eq!( + apply_single::(&f.build(), BASE_A).unwrap_err(), + ApplyError::WriteOutOfRange(0) + ); +} + +#[test] +fn overflow_refuses() { + // base + i64::MIN wraps below zero. + let mut f = Fixture::new(EM_X86_64); + f.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: 8, + sym: 0, + addend: i64::MIN, + }]; + assert_eq!( + apply_single::(&f.build(), BASE_A).unwrap_err(), + ApplyError::Overflow(0) + ); + // S + i64::MIN likewise (the addend-games case for Abs64). + let mut f = Fixture::new(EM_X86_64); + f.syms = vec![Sym::global("obj", DATA_VADDR + 0x300)]; + f.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: 1, + sym: 1, + addend: i64::MIN, + }]; + assert_eq!( + apply_single::(&f.build(), BASE_A).unwrap_err(), + ApplyError::Overflow(0) + ); +} + +/// Check the pristine image, corrupt the rela `r_info` afterwards, then +/// apply against the corrupted mapping — the post-`check` tamper shape +/// the engine's own gates must catch. +fn apply_tampered(new_r_info: u64) -> ApplyError { + let mut f = Fixture::new(EM_X86_64); + f.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: 8, + sym: 0, + addend: 0, + }]; + let mut img = f.build(); + let checked = check_image::(&img).unwrap(); + write_raw64(&mut img, RELA_OFF as usize + 8, new_r_info); + let d = checked.descriptor; + let views = [view_of(&img, &d, BASE_A)]; + let scope = SymbolScope { modules: &views }; + let tls = TlsLayout::build::(&views, arena()).unwrap(); + let mut data = data_copy(&img, &d); + let mut module = ModuleImage { + scope_index: 0, + view: view_of(&img, &d, BASE_A), + data: &mut data, + relro: checked.relro, + }; + apply_module::(&mut module, &scope, &tls).unwrap_err() +} + +#[test] +fn rejected_reloc_defense_in_depth() { + // A checked image whose rela table is corrupted afterwards (type → + // COPY): the engine's own classification gate must still refuse. + assert_eq!( + apply_tampered(5), // r_info: sym 0, type COPY + ApplyError::Reloc(RelocRefusal::Rejected(5), 0) + ); +} + +#[test] +fn tprel_without_layout_refuses() { + // A no-TLS image whose rela type is corrupted to TPOFF64 after + // check: the layout has no block for this module → Tls(0). + assert_eq!(apply_tampered(18), ApplyError::Tls(0)); // sym 0, TPOFF64 +} + +#[test] +fn machine_mismatch_refuses_at_apply() { + // The consumer-side cross-check: an x86_64 descriptor applied + // through the aarch64 engine refuses before any byte moves. + let f = Fixture::new(EM_X86_64); + let img = f.build(); + let checked = check_image::(&img).unwrap(); + let d = checked.descriptor; + let views = [view_of(&img, &d, BASE_A)]; + let scope = SymbolScope { modules: &views }; + let tls = TlsLayout::build::(&views, arena()).unwrap(); + let mut data = data_copy(&img, &d); + let mut module = ModuleImage { + scope_index: 0, + view: view_of(&img, &d, BASE_A), + data: &mut data, + relro: None, + }; + assert_eq!( + apply_module::(&mut module, &scope, &tls).unwrap_err(), + ApplyError::Input + ); +} + +// --------------------------------------------------------------------------- +// TLS layout (psABI hand-derivations in comments) +// --------------------------------------------------------------------------- + +/// A bare descriptor carrying only TLS geometry (layout-building needs +/// nothing else). +fn tls_descriptor(memsz: u64, align: u64) -> ModuleDescriptor { + let mut d = ModuleDescriptor::empty(); + d.abi.set(IMAGE_ABI); + d.kind = kind::LIBRARY; + d.tls.memsz.set(memsz); + d.tls.align.set(align); + d +} + +fn tls_views(descs: &[ModuleDescriptor]) -> Vec> { + descs + .iter() + .map(|d| ModuleView { + base: 0, + descriptor: d, + ro: [None; MAX_SEGMENTS], + }) + .collect() +} + +#[test] +fn tls_layout_v2_x86_64() { + // Variant 2 (x86-64 psABI §3.4.6): blocks below tp, offsets + // negative. Sizes 0x18/align 8 and 0x30/align 16: + // off₁ = align_up(0x18, 8) = 0x18 → base₁ = −0x18 + // off₂ = align_up(0x18 + 0x30, 16) = align_up(0x48, 16) = 0x50 + // → base₂ = −0x50 + // total = 0x50, align = 16. + let descs = [tls_descriptor(0x18, 8), tls_descriptor(0x30, 16)]; + let views = tls_views(&descs); + let tls = TlsLayout::build::(&views, arena()).unwrap(); + assert_eq!(tls.tp_off(0, 0), Some(-0x18)); + assert_eq!(tls.tp_off(0, 0x8), Some(-0x10)); + assert_eq!(tls.tp_off(1, 0), Some(-0x50)); + assert_eq!(tls.tp_off(1, 0x10), Some(-0x40)); + assert_eq!(tls.total, 0x50); + assert_eq!(tls.align, 16); +} + +#[test] +fn tls_layout_v1_aarch64() { + // Variant 1: blocks above tp after the reserved header, offsets + // positive. The header is `max(psABI tcb, RUNTIME_TCB_BYTES)` — the + // AAELF64 minimum of 16 is a floor, and the runtime's per-thread block + // lives at tp, so the reserve is a page: + // base₁ = align_up(0x1000, 8) = 0x1000; end = 0x1018 + // base₂ = align_up(0x1018, 16) = 0x1020; end = 0x1050 + // total = 0x1050 (header included), align = 16. + let reserve = kore_abi::image::RUNTIME_TCB_BYTES; + let descs = [tls_descriptor(0x18, 8), tls_descriptor(0x30, 16)]; + let views = tls_views(&descs); + let tls = TlsLayout::build::(&views, arena()).unwrap(); + assert_eq!(tls.tp_off(0, 0), Some(reserve as i64)); + assert_eq!(tls.tp_off(0, 0x8), Some(reserve as i64 + 0x8)); + assert_eq!(tls.tp_off(1, 0), Some(reserve as i64 + 0x20)); + assert_eq!(tls.tp_off(1, 0x10), Some(reserve as i64 + 0x30)); + assert_eq!(tls.total, reserve + 0x50); + assert_eq!(tls.align, 16); +} + +#[test] +fn tls_layout_v1_riscv64() { + // Variant 1 with a psABI tcb of 0 (tp points one past the TCB): the + // runtime reserve is what the header becomes, so riscv64 and aarch64 + // place identically once both are widened past their own minima. + // base₁ = align_up(0x1000, 8) = 0x1000; end = 0x1018 + // base₂ = align_up(0x1018, 16) = 0x1020; end = 0x1050 + let reserve = kore_abi::image::RUNTIME_TCB_BYTES; + let descs = [tls_descriptor(0x18, 8), tls_descriptor(0x30, 16)]; + let views = tls_views(&descs); + let tls = TlsLayout::build::(&views, arena()).unwrap(); + assert_eq!(tls.tp_off(0, 0), Some(reserve as i64)); + assert_eq!(tls.tp_off(0, 0x8), Some(reserve as i64 + 0x8)); + assert_eq!(tls.tp_off(1, 0), Some(reserve as i64 + 0x20)); + assert_eq!(tls.tp_off(1, 0x10), Some(reserve as i64 + 0x30)); + assert_eq!(tls.total, reserve + 0x50); + assert_eq!(tls.align, 16); +} + +#[test] +fn tls_layout_gaps_and_bounds() { + // A TLS-less module gets no block; offsets beyond a block refuse. + let descs = [tls_descriptor(0, 0), tls_descriptor(0x18, 8)]; + let views = tls_views(&descs); + let tls = TlsLayout::build::(&views, arena()).unwrap(); + assert_eq!(tls.tp_off(0, 0), None); + assert_eq!(tls.tp_off(1, 0x19), None); // past the block + assert_eq!(tls.tp_off(2, 0), None); // past the scope +} + +#[test] +fn tprel_end_to_end_x86_64() { + // Single module, TLS 0x30/align 16 (variant 2): + // off₁ = align_up(0x30, 16) = 0x30 → block base = tp − 0x30. + // `tvar` at block offset 8 → slot = −0x30 + 8 = −0x28. + // sym-0 form with addend 0x10 → slot = −0x30 + 0x10 = −0x20. + let mut f = Fixture::new(EM_X86_64); + f.tls = Some(TlsSpec { + memsz: 0x30, + align: 16, + filesz: 0, + }); + f.syms = vec![Sym { + name: "tvar".into(), + value: 8, + size: 8, + binding: STB_GLOBAL, + shndx: 1, + }]; + f.relas = vec![ + Rela { + offset: DATA_VADDR + 0x100, + rtype: 18, + sym: 1, + addend: 0, + }, + Rela { + offset: DATA_VADDR + 0x108, + rtype: 18, + sym: 0, + addend: 0x10, + }, + ]; + let (applied, data) = apply_single::(&f.build(), BASE_A).unwrap(); + assert_eq!(applied.applied, 2); + assert_eq!(read64(&data, 0x100), (-0x28i64) as u64); + assert_eq!(read64(&data, 0x108), (-0x20i64) as u64); +} + +#[test] +fn tprel_cross_module_aarch64() { + // A imports TLS `tv` from B (variant 1). Scope [A, B]; the reserved + // header at `tp` is `RUNTIME_TCB_BYTES` (the runtime's per-thread block + // lives there), so the first block starts at the reserve rather than at + // the psABI's 16. + let mut fa = Fixture::new(EM_AARCH64); + fa.syms = vec![Sym::import("tv")]; + fa.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: 1030, + sym: 1, + addend: 0, + }]; + // A TpRel reloc requires PT_TLS in its own image at check time; the + // psABI allows an exe referencing only foreign TLS, but the §7 gate + // is stricter — give A a tiny block of its own (size 8, align 8), + // which lands at the reserve and pushes B's to reserve + 8. + // Expected slot: reserve + 8 + 4. + fa.tls = Some(TlsSpec { + memsz: 8, + align: 8, + filesz: 0, + }); + let img_a = fa.build(); + let mut fb = Fixture::new(EM_AARCH64); + fb.syms = vec![Sym { + name: "tv".into(), + value: 4, + size: 4, + binding: STB_GLOBAL, + shndx: 1, + }]; + fb.tls = Some(TlsSpec { + memsz: 0x10, + align: 8, + filesz: 0, + }); + let img_b = fb.build(); + + let da = check_image::(&img_a).unwrap().descriptor; + let db = check_image::(&img_b).unwrap().descriptor; + let views = [view_of(&img_a, &da, BASE_A), view_of(&img_b, &db, BASE_B)]; + let scope = SymbolScope { modules: &views }; + let tls = TlsLayout::build::(&views, arena()).unwrap(); + let mut data = data_copy(&img_a, &da); + let mut module = ModuleImage { + scope_index: 0, + view: view_of(&img_a, &da, BASE_A), + data: &mut data, + relro: None, + }; + apply_module::(&mut module, &scope, &tls).unwrap(); + assert_eq!( + read64(&data, 0x100), + kore_abi::image::RUNTIME_TCB_BYTES + 0x8 + 4 + ); +} + +// --------------------------------------------------------------------------- +// Late binding's `dlsym` half: one module, one name, an absolute address. +// --------------------------------------------------------------------------- + +#[test] +fn lookup_symbol_rebases_a_definition_and_misses_honestly() { + let mut f = Fixture::new(EM_X86_64); + f.syms = vec![ + Sym::global("luaopen_demo", TEXT_VADDR + 0x40), + Sym::weak("weak_only", TEXT_VADDR + 0x80), + Sym::import("lua_pushinteger"), + ]; + let img = f.build(); + let checked = check_image::(&img).unwrap(); + let d = checked.descriptor; + let view = view_of(&img, &d, BASE_A); + assert_eq!( + lookup_symbol(&view, b"luaopen_demo"), + Ok(Some(BASE_A + TEXT_VADDR + 0x40)) + ); + assert_eq!( + lookup_symbol(&view, b"weak_only"), + Ok(Some(BASE_A + TEXT_VADDR + 0x80)) + ); + // An import defines nothing; an unknown name is simply absent. + assert_eq!(lookup_symbol(&view, b"lua_pushinteger"), Ok(None)); + assert_eq!(lookup_symbol(&view, b"nothing"), Ok(None)); +} + +#[test] +fn lookup_symbol_refuses_a_module_without_gnu_hash() { + let f = Fixture::new(EM_X86_64); + let img = f.build(); + let checked = check_image::(&img).unwrap(); + let mut d = checked.descriptor; + d.dynz.gnu_hash_off = 0.into(); + let view = view_of(&img, &d, BASE_A); + assert_eq!(lookup_symbol(&view, b"anything"), Err(ApplyError::Input)); +} diff --git a/native/crates/ld/tests/built_images.rs b/native/crates/ld/tests/built_images.rs new file mode 100644 index 0000000..fdfe086 --- /dev/null +++ b/native/crates/ld/tests/built_images.rs @@ -0,0 +1,340 @@ +//! Regime validation of the built userland images (`res/IMAGE.md` §7; +//! epic #291, P4 #295): the closed loop behind `make user-verify`. +//! +//! `KORE_USER_IMAGES` names the `user/` cargo target dir; for each +//! `/release/{libdemo.so,libpci_names.so,hello,time,sleep,status,lspci,help,window,clear,lua,liblua_demo.so,kore-editor}` present this asserts, +//! by raw ELF +//! inspection (the `elf` crate — the same substrate `kore_ld::parse` trusts): +//! +//! - `ET_DYN`, correct `e_machine`; +//! - every `PT_LOAD` page-aligned (vaddr *and* offset), page-rounded +//! non-overlapping, `p_filesz ≤ p_memsz`, and never `W+X`; +//! - `DT_FLAGS`/`DT_FLAGS_1` carry `BIND_NOW`/`NOW`; no `DT_TEXTREL`; +//! - every `.rela.*` entry classifies into the eager set via +//! [`kore_ld::reloc::RelocArch::classify`] — zero reject-list/unknown +//! types (and no non-empty `SHT_REL` section at all); +//! - `libdemo.so` carries `PT_TLS` and ≥1 TPREL-class reloc; +//! - `hello` names `libdemo.so`, while `status` and `lspci` name +//! `libpci_names.so` through `DT_NEEDED`, proving both shared-library +//! boundaries survive the build. +//! +//! `KORE_USER_IMAGE_ARCH`, when set, closes the walk to exactly one of +//! `x86_64`, `aarch64`, or `riscv64`. Production packaging uses that +//! selector so a partial build for another ISA cannot become a prerequisite +//! merely because its target directory exists. With no selector every ISA +//! present is checked, as in `make user-verify`. +//! +//! `KORE_USER_IMAGES` unset ⇒ skip: `make test` stays hermetic. An arch whose +//! spec dir is absent is skipped (a gated-out ISA), but a set root with *no* +//! selected images found fails — never silently green. + +use std::path::{Path, PathBuf}; + +use elf::ElfBytes; +use elf::abi; +use elf::endian::AnyEndian; +use kore_ld::reloc::{AArch64, RelocArch, RelocKind, Riscv64, X86_64}; + +const PAGE: u64 = 4096; + +/// Per-image census of eager relocation semantics — the printed +/// evidence that the §7 set (and nothing else) is what the toolchain +/// emitted. +#[derive(Default)] +struct Census { + relative: usize, + glob_dat: usize, + jump_slot: usize, + abs64: usize, + tp_rel: usize, +} + +impl Census { + fn bump(&mut self, kind: RelocKind) { + match kind { + RelocKind::Relative => self.relative += 1, + RelocKind::GlobDat => self.glob_dat += 1, + RelocKind::JumpSlot => self.jump_slot += 1, + RelocKind::Abs64 => self.abs64 += 1, + RelocKind::TpRel => self.tp_rel += 1, + } + } +} + +fn flags_str(p_flags: u32) -> String { + let mut s = String::new(); + s.push(if p_flags & abi::PF_R != 0 { 'R' } else { '-' }); + s.push(if p_flags & abi::PF_W != 0 { 'W' } else { '-' }); + s.push(if p_flags & abi::PF_X != 0 { 'X' } else { '-' }); + s +} + +fn verify_image(path: &Path, kind: ImageKind) { + let tag = path.display(); + let data = std::fs::read(path).unwrap_or_else(|e| panic!("{tag}: unreadable: {e}")); + let elf = ElfBytes::::minimal_parse(&data) + .unwrap_or_else(|e| panic!("{tag}: not parseable ELF: {e}")); + + // Identity: ET_DYN only (kore images are PIE/shared-object), right ISA. + assert_eq!(elf.ehdr.e_type, abi::ET_DYN, "{tag}: not ET_DYN"); + assert_eq!(elf.ehdr.e_machine, A::MACHINE, "{tag}: wrong e_machine"); + + // Load geometry: the §2 in-place carve's preconditions. + let segs = elf + .segments() + .unwrap_or_else(|| panic!("{tag}: no program headers")); + let mut loads: Vec<(u64, u64, u32)> = Vec::new(); + let mut has_tls = false; + for ph in segs.iter() { + match ph.p_type { + abi::PT_TLS => has_tls = true, + abi::PT_GNU_STACK => { + assert_eq!(ph.p_flags & abi::PF_X, 0, "{tag}: executable stack"); + } + abi::PT_LOAD => { + assert_eq!(ph.p_vaddr % PAGE, 0, "{tag}: PT_LOAD vaddr off-page"); + assert_eq!(ph.p_offset % PAGE, 0, "{tag}: PT_LOAD offset off-page"); + assert!(ph.p_filesz <= ph.p_memsz, "{tag}: filesz > memsz"); + let wx = abi::PF_W | abi::PF_X; + assert_ne!(ph.p_flags & wx, wx, "{tag}: W+X segment"); + loads.push((ph.p_vaddr, ph.p_memsz, ph.p_flags)); + } + _ => {} + } + } + assert!(!loads.is_empty(), "{tag}: no PT_LOAD"); + loads.sort_unstable(); + for w in loads.windows(2) { + let end = (w[0].0 + w[0].1).next_multiple_of(PAGE); + assert!(end <= w[1].0, "{tag}: PT_LOADs overlap (page-rounded)"); + } + + // Dynamic flags: eager binding declared, text relocations refused. + let dynamic = elf + .dynamic() + .unwrap_or_else(|e| panic!("{tag}: dynamic parse: {e}")) + .unwrap_or_else(|| panic!("{tag}: no PT_DYNAMIC")); + let (mut flags, mut flags1, mut textrel) = (0u64, 0u64, false); + let mut needed: Vec = Vec::new(); + for d in dynamic.iter() { + match d.d_tag { + abi::DT_FLAGS => flags = d.d_val(), + abi::DT_FLAGS_1 => flags1 = d.d_val(), + abi::DT_TEXTREL => textrel = true, + abi::DT_NEEDED => needed.push(d.d_val()), + _ => {} + } + } + // The `elf` crate types DF_* like d_tag (i64); the values are small + // positive bit flags, so the cast is lossless. + assert!( + flags & abi::DF_BIND_NOW as u64 != 0 || flags1 & abi::DF_1_NOW as u64 != 0, + "{tag}: neither DF_BIND_NOW nor DF_1_NOW" + ); + assert!( + !textrel && flags & abi::DF_TEXTREL as u64 == 0, + "{tag}: text relocations declared" + ); + + // Relocation gate: everything present classifies eager; REL never + // appears (all three ISAs are RELA-only under the regime). + let shdrs = elf + .section_headers() + .unwrap_or_else(|| panic!("{tag}: no section headers")); + let mut census = Census::default(); + for sh in shdrs.iter() { + assert!( + sh.sh_type != abi::SHT_REL || sh.sh_size == 0, + "{tag}: non-empty SHT_REL section" + ); + if sh.sh_type != abi::SHT_RELA { + continue; + } + for rela in elf + .section_data_as_relas(&sh) + .unwrap_or_else(|e| panic!("{tag}: rela parse: {e}")) + { + match A::classify(rela.r_type) { + Ok(kind) => census.bump(kind), + Err(refusal) => panic!("{tag}: non-eager relocation: {refusal:?}"), + } + } + } + + if matches!(kind, ImageKind::DemoLibrary) { + assert!(has_tls, "{tag}: libdemo.so must carry PT_TLS"); + assert!(census.tp_rel >= 1, "{tag}: no TPREL-class reloc"); + } + if let ImageKind::ExportsApi(symbol) | ImageKind::LateModule { opener: symbol, .. } = kind { + let (symbols, strings) = elf + .dynamic_symbol_table() + .unwrap_or_else(|e| panic!("{tag}: dynsym parse: {e}")) + .unwrap_or_else(|| panic!("{tag}: no .dynsym")); + let mut defined = false; + for sym in symbols.iter() { + let name = strings + .get(sym.st_name as usize) + .unwrap_or_else(|e| panic!("{tag}: dynsym strtab: {e}")); + if name == symbol { + assert_ne!(sym.st_shndx, abi::SHN_UNDEF, "{tag}: {symbol} is undefined"); + defined = true; + } + if let ImageKind::LateModule { host_prefix, .. } = kind + && sym.st_shndx == abi::SHN_UNDEF + && !name.is_empty() + { + assert!( + name.starts_with(host_prefix), + "{tag}: undefined {name} is not a {host_prefix}* host import" + ); + } + } + assert!(defined, "{tag}: .dynsym does not define {symbol}"); + if matches!(kind, ImageKind::LateModule { .. }) { + assert!(!has_tls, "{tag}: a late module may not carry PT_TLS"); + let checked = kore_ld::check::check_image::(&data) + .unwrap_or_else(|e| panic!("{tag}: late module check: {e:?}")); + assert!( + checked.relro.is_some(), + "{tag}: native witness must exercise RELRO sealing" + ); + kore_ld::late::LateLayout::plan(&checked, u64::MAX, u64::MAX) + .unwrap_or_else(|e| panic!("{tag}: late protection layout: {e:?}")); + } + } + if let ImageKind::NeedsLibrary(required) = kind { + let dynstr_hdr = elf + .section_header_by_name(".dynstr") + .unwrap_or_else(|e| panic!("{tag}: shdr walk: {e}")) + .unwrap_or_else(|| panic!("{tag}: no .dynstr")); + let dynstr = elf + .section_data_as_strtab(&dynstr_hdr) + .unwrap_or_else(|e| panic!("{tag}: dynstr parse: {e}")); + let names: Vec<&str> = needed + .iter() + .map(|&off| { + dynstr + .get(off as usize) + .unwrap_or_else(|e| panic!("{tag}: DT_NEEDED strtab: {e}")) + }) + .collect(); + assert!( + names.contains(&required), + "{tag}: DT_NEEDED missing {required} (got {names:?})" + ); + } + + // Exercise the actual registration gate too: structural ELF inspection + // alone missed a pure-.tbss image that the image service then refused. + if !matches!(kind, ImageKind::LateModule { .. }) { + kore_ld::check::check_image::(&data) + .unwrap_or_else(|error| panic!("{tag}: image registration refuses: {error:?}")); + } + + // Evidence for the human reading `make user-verify`. + for (vaddr, memsz, p_flags) in &loads { + println!(" load {:#08x}+{:#x} {}", vaddr, memsz, flags_str(*p_flags)); + } + println!( + " relocs: relative={} glob_dat={} jump_slot={} abs64={} tp_rel={}", + census.relative, census.glob_dat, census.jump_slot, census.abs64, census.tp_rel + ); +} + +#[derive(Clone, Copy)] +enum ImageKind { + DemoLibrary, + SharedLibrary, + NeedsLibrary(&'static str), + Executable, + /// An executable that exports a C API for late-linked modules: the + /// named symbol must be defined in `.dynsym`. + ExportsApi(&'static str), + /// A late-linked module (`res/IMAGE.md` §8): no PT_TLS, the named + /// opener defined, and every undefined dynamic symbol carrying the + /// host prefix — it binds to the host at `require`, never at load. + LateModule { + opener: &'static str, + host_prefix: &'static str, + }, +} + +/// Check one arch across its SIMD profile variants: the baseline spec +/// dir and its `-v2` / `-v3` / `-v4` siblings. Today v2 is the Oryon-1 +/// AArch64 profile (`USER_SIMD=v2`), while v3/v4 are the x86-64 psABI +/// classes (`USER_SIMD=v3|v4`). Every variant present is checked — +/// a stale baseline build beside a fresh machine profile both face the +/// regime; an absent variant is skipped (a gated-out ISA / an unselected +/// profile). +fn check_arch(root: &Path, spec: &str) -> usize { + let mut checked = 0; + for variant in [ + String::from(spec), + format!("{spec}-v2"), + format!("{spec}-v3"), + format!("{spec}-v4"), + ] { + let dir = root.join(&variant).join("release"); + if !dir.is_dir() { + println!("built_images: {variant} not built — skipped"); + continue; + } + for (name, kind) in [ + ("libdemo.so", ImageKind::DemoLibrary), + ("libpci_names.so", ImageKind::SharedLibrary), + ("hello", ImageKind::NeedsLibrary("libdemo.so")), + ("time", ImageKind::Executable), + ("sleep", ImageKind::Executable), + ("status", ImageKind::NeedsLibrary("libpci_names.so")), + ("lspci", ImageKind::NeedsLibrary("libpci_names.so")), + ("help", ImageKind::Executable), + ("window", ImageKind::Executable), + ("clear", ImageKind::Executable), + ("lua", ImageKind::ExportsApi("lua_pushcclosure")), + ( + "liblua_demo.so", + ImageKind::LateModule { + opener: "luaopen_lua_demo", + host_prefix: "lua_", + }, + ), + ("kore-terminal", ImageKind::Executable), + ("kore-editor", ImageKind::Executable), + ("kore-aftershock", ImageKind::Executable), + ("kore-skylilt", ImageKind::Executable), + ] { + println!("built_images: {variant}/{name}"); + verify_image::(&dir.join(name), kind); + } + checked += 16; + } + checked +} + +#[test] +fn built_images_satisfy_regime() { + let Some(root) = std::env::var_os("KORE_USER_IMAGES") else { + println!("built_images: KORE_USER_IMAGES unset — skipped (run `make user-verify`)"); + return; + }; + let root = PathBuf::from(root); + let requested = std::env::var("KORE_USER_IMAGE_ARCH").ok(); + let n = match requested.as_deref() { + None => { + check_arch::(&root, "x86_64-unknown-kore-user") + + check_arch::(&root, "aarch64-unknown-kore-user") + + check_arch::(&root, "riscv64gc-unknown-kore-user") + } + Some("x86_64") => check_arch::(&root, "x86_64-unknown-kore-user"), + Some("aarch64") => check_arch::(&root, "aarch64-unknown-kore-user"), + Some("riscv64") => check_arch::(&root, "riscv64gc-unknown-kore-user"), + Some(other) => panic!( + "KORE_USER_IMAGE_ARCH={other:?} is unsupported; expected x86_64, aarch64, or riscv64" + ), + }; + assert!( + n > 0, + "KORE_USER_IMAGES={} holds no built images", + root.display() + ); +} diff --git a/native/crates/ld/tests/check_gate.rs b/native/crates/ld/tests/check_gate.rs new file mode 100644 index 0000000..0028b20 --- /dev/null +++ b/native/crates/ld/tests/check_gate.rs @@ -0,0 +1,717 @@ +//! `check_image` gate suite: happy paths on all three ISAs, then the +//! adversarial corpus — every named refusal must be the *specific* +//! typed error, and no input (including every truncation length) may +//! panic. + +mod common; + +use common::*; +use kore_abi::image::{IMAGE_ABI, ImageName, kind, seg}; +use kore_ld::check::{CheckError, check_image}; +use kore_ld::parse::{ParseError, parse_static_image}; +use kore_ld::reloc::{AArch64, RelocRefusal, Riscv64, X86_64}; + +// --------------------------------------------------------------------------- +// Happy paths +// --------------------------------------------------------------------------- + +fn happy_fixture(arch: &ArchRelocs) -> Fixture { + let mut f = Fixture::new(arch.machine); + f.entry = TEXT_VADDR; + f.syms = vec![Sym::global("alpha", TEXT_VADDR + 0x10)]; + f.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: arch.relative, + sym: 0, + addend: 0x40, + }]; + f.plt_relas = vec![Rela { + offset: DATA_VADDR + 0x108, + rtype: arch.jump_slot, + sym: 1, + addend: 0, + }]; + f.needed = vec!["libkore".into(), "libm".into()]; + f.relro = Some((DATA_VADDR, 0x1000)); + f.bss = 0x800; + f +} + +fn assert_happy(arch: &ArchRelocs, checked: &kore_ld::check::CheckedImage) { + let d = &checked.descriptor; + assert!(d.validate()); + assert_eq!(d.abi.get(), IMAGE_ABI); + assert_eq!(d.kind, kind::EXECUTABLE); + assert_eq!(d.machine.get(), arch.machine); + assert_eq!(d.needed_count.get(), 2); + assert_eq!( + checked.needed[0].as_ref().and_then(ImageName::as_str), + Some("libkore") + ); + assert_eq!( + checked.needed[1].as_ref().and_then(ImageName::as_str), + Some("libm") + ); + assert!(checked.needed[2..].iter().all(Option::is_none)); + assert_eq!(d.entry.get(), TEXT_VADDR); + + // Canonical kind order: text, rodata, data. + assert_eq!(d.seg_count, 3); + assert_eq!(d.segs[0].kind, seg::KIND_TEXT); + assert_eq!(d.segs[0].vaddr.get(), TEXT_VADDR); + assert_eq!(d.segs[0].access, seg::ACCESS_R | seg::ACCESS_X); + assert_eq!(d.segs[1].kind, seg::KIND_RODATA); + assert_eq!(d.segs[1].vaddr.get(), RO_VADDR); + assert_eq!(d.segs[1].access, seg::ACCESS_R); + assert_eq!(d.segs[2].kind, seg::KIND_DATA); + assert_eq!(d.segs[2].vaddr.get(), DATA_VADDR); + assert_eq!(d.segs[2].memsz.get(), 2 * PAGE); + assert_eq!(d.segs[2].filesz.get(), PAGE); + assert_eq!(d.segs[2].access, seg::ACCESS_R); + + // Carve windows: parallel to segs, page-aligned, and large enough for + // both the file prefix and the complete mapped shape. + let text = checked.carve[0].unwrap(); + assert_eq!( + (text.file_off, text.len, text.kind), + (TEXT_VADDR, PAGE, seg::KIND_TEXT) + ); + let ro = checked.carve[1].unwrap(); + assert_eq!((ro.file_off, ro.len, ro.kind), (0, PAGE, seg::KIND_RODATA)); + let data = checked.carve[2].unwrap(); + assert_eq!( + (data.file_off, data.len, data.kind), + (DATA_VADDR, 2 * PAGE, seg::KIND_DATA) + ); + assert!(checked.carve[3].is_none()); + for (segment, carve) in d.live_segs().iter().zip(&checked.carve) { + assert_eq!( + segment.lease_len(), + carve.map(|window| window.len), + "the frozen descriptor derives the exact granted lease span" + ); + } + + // Dynamic geometry, image-relative. + assert_eq!(d.dynz.dynamic_off.get(), DYNAMIC_VADDR); + assert_eq!(d.dynz.rela_off.get(), RELA_OFF); + assert_eq!(d.dynz.rela_sz.get(), 24); + assert_eq!(d.dynz.jmprel_off.get(), PLT_RELA_OFF); + assert_eq!(d.dynz.jmprel_sz.get(), 24); + assert_eq!(d.dynz.symtab_off.get(), DYNSYM_OFF); + assert_eq!(d.dynz.strtab_off.get(), DYNSTR_OFF); + assert_eq!(d.dynz.gnu_hash_off.get(), GNU_HASH_OFF); + assert_eq!(d.dynz.flags.get(), DF_BIND_NOW); + + // RELRO rides the checked view, image-relative. + assert_eq!(checked.relro, Some((DATA_VADDR, 0x1000))); +} + +#[test] +fn happy_check_x86_64() { + let img = happy_fixture(&X86_RELOCS).build(); + let checked = check_image::(&img).unwrap(); + assert_happy(&X86_RELOCS, &checked); +} + +#[test] +fn happy_check_aarch64() { + let img = happy_fixture(&AARCH64_RELOCS).build(); + let checked = check_image::(&img).unwrap(); + assert_happy(&AARCH64_RELOCS, &checked); +} + +#[test] +fn happy_check_riscv64() { + let img = happy_fixture(&RISCV_RELOCS).build(); + let checked = check_image::(&img).unwrap(); + assert_happy(&RISCV_RELOCS, &checked); +} + +/// A pure-bss LOAD (`filesz = 0`, writable — what real linkers emit after +/// RELRO) remains a distinct data template. The linker maps all contiguous +/// data templates through one private union, but each keeps its own exact +/// geometry and source-lease provenance. +#[test] +fn bss_load_remains_a_distinct_data_template() { + let mut f = happy_fixture(&X86_RELOCS); + f.bss = 0; // a page-clean data end, so the tail is contiguous + f.bss_tail = 0x40; + let checked = check_image::(&f.build()).unwrap(); + let d = &checked.descriptor; + assert_eq!(d.seg_count, 4); + assert_eq!(d.segs[2].kind, seg::KIND_DATA); + assert_eq!(d.segs[2].memsz.get(), PAGE); + assert_eq!(d.segs[2].filesz.get(), PAGE); + assert_eq!(d.segs[3].kind, seg::KIND_DATA); + assert_eq!(d.segs[3].memsz.get(), PAGE); + assert_eq!(d.segs[3].filesz.get(), 0); + assert_eq!(checked.carve[3].unwrap().len, PAGE); +} + +/// A small hole between data LOADs is lld's congruence rounding, not a +/// malformed layout: whether the RELRO template's page-rounded end abuts +/// the next writable LOAD is decided by section sizes (a build-root +/// path-length shift flipped it on a real kore-svc-swap). The checker +/// bridges it into the earlier template's DECLARED zero-fill tail, so +/// the union stays hole-free without refusing a bootable image. +#[test] +fn bss_tail_small_gap_bridges_into_declared_zero_fill() { + let mut f = happy_fixture(&X86_RELOCS); + f.bss = 0; + f.bss_tail = 0x40; + f.bss_tail_gap_pages = 1; // one page past the template's rounded end + let checked = check_image::(&f.build()).unwrap(); + let d = &checked.descriptor; + assert_eq!(d.segs[2].kind, seg::KIND_DATA); + // The template grew across the gap page: map doubled + the gap page, + // file prefix untouched — the bridge is bss, never file bytes. + assert_eq!(d.segs[2].memsz.get(), 2 * PAGE); + assert_eq!(d.segs[2].filesz.get(), PAGE); + // The tail keeps its own exact geometry, now abutting the template. + assert_eq!(d.segs[3].kind, seg::KIND_DATA); + assert_eq!(d.segs[3].memsz.get(), PAGE); + assert_eq!( + d.segs[2].vaddr.get() + d.segs[2].memsz.get(), + d.segs[3].vaddr.get() + ); + // The carve lease backs the grown mapped shape. + assert_eq!(checked.carve[2].unwrap().len, 2 * PAGE); +} + +/// A wide hole is not a linker artifact — it is a layout lying about its +/// shape (or a hostile ELF minting a giant bss between two far-apart +/// declarations). Beyond the bridge ceiling the union stays holed and +/// the contract refuses, exactly as before the bridge existed. +#[test] +fn bss_tail_wide_gap_refuses() { + let mut f = happy_fixture(&X86_RELOCS); + f.bss = 0; + f.bss_tail = 0x40; + f.bss_tail_gap_pages = 17; // one past the 16-page bridge ceiling + let img = f.build(); + assert_eq!(check_image::(&img), Err(CheckError::Segment)); +} + +#[test] +fn library_kind_when_no_entry() { + let mut f = Fixture::new(EM_X86_64); + f.entry = 0; + let checked = check_image::(&f.build()).unwrap(); + assert_eq!(checked.descriptor.kind, kind::LIBRARY); + assert_eq!(checked.descriptor.entry.get(), 0); +} + +#[test] +fn tls_geometry_carried() { + let mut f = Fixture::new(EM_X86_64); + f.tls = Some(TlsSpec { + memsz: 0x30, + align: 0x10, + filesz: 0x10, + }); + let checked = check_image::(&f.build()).unwrap(); + let d = &checked.descriptor; + assert_eq!(d.tls.memsz.get(), 0x30); + assert_eq!(d.tls.align.get(), 0x10); + assert_eq!(d.tls.init_sz.get(), 0x10); + // The TLS seg entry follows data in canonical order. + assert_eq!(d.seg_count, 4); + assert_eq!(d.segs[3].kind, seg::KIND_TLS); + assert_eq!(d.segs[3].vaddr.get(), TLS_VADDR); + // The carve window rounds outward to pages around the init image. + let tls = checked.carve[3].unwrap(); + assert_eq!( + (tls.file_off, tls.len, tls.kind), + (DATA_VADDR, PAGE, seg::KIND_TLS) + ); + assert_eq!(d.segs[3].lease_len(), Some(tls.len)); +} + +#[test] +fn incongruent_tls_file_offset_refuses() { + // The stub recovers the init image's in-lease offset as `vaddr % + // page`, so `Register` pins the standard mapping invariant + // `p_offset ≡ p_vaddr (mod page)` on PT_TLS — an incongruent header + // is lying about where its bytes live. + let mut f = Fixture::new(EM_X86_64); + f.tls = Some(TlsSpec { + memsz: 0x10, + align: 8, + filesz: 8, + }); + let mut img = f.build(); + // PT_TLS is phdr 4 (after RO, TEXT, DATA, DYNAMIC); shift its file + // offset by 8 while the vaddr stays put. + poke64(&mut img, PH_DYNAMIC + 1, P_OFFSET, TLS_VADDR + 8); + assert_eq!(check_image::(&img).unwrap_err(), CheckError::Tls); +} + +#[test] +fn init_fini_arrays_carried() { + let mut f = Fixture::new(EM_X86_64); + f.init_fini = true; + let d = check_image::(&f.build()).unwrap().descriptor; + assert_eq!(d.dynz.init_array_off.get(), DATA_VADDR + 0x400); + assert_eq!(d.dynz.init_array_sz.get(), 16); + assert_eq!(d.dynz.fini_array_off.get(), DATA_VADDR + 0x440); + assert_eq!(d.dynz.fini_array_sz.get(), 8); +} + +#[test] +fn bind_now_via_flags1_accepts() { + let mut f = Fixture::new(EM_X86_64); + f.flags = 0; + f.flags1 = DF_1_NOW; + assert!(check_image::(&f.build()).is_ok()); +} + +#[test] +fn nonzero_link_base_rebases() { + // Same image shifted to link base 0x40000: the emitted descriptor + // must be identical to the base-0 one (image-relative throughout). + let base0 = check_image::(&happy_fixture(&X86_RELOCS).build()).unwrap(); + let mut f = happy_fixture(&X86_RELOCS); + f.vaddr_base = 0x40000; + let shifted = check_image::(&f.build()).unwrap(); + assert_eq!(base0.descriptor, shifted.descriptor); + assert_eq!(base0.relro, shifted.relro); +} + +// --------------------------------------------------------------------------- +// Adversarial corpus — each case must produce its specific typed error. +// --------------------------------------------------------------------------- + +#[test] +fn truncation_fuzz_never_panics() { + // Every prefix of a valid image must refuse with a typed error — + // never parse, never panic. catch_unwind proves the no-panic claim. + let img = happy_fixture(&X86_RELOCS).build(); + for len in 0..img.len() { + let slice = &img[..len]; + let result = std::panic::catch_unwind(|| check_image::(slice)); + let inner = result.unwrap_or_else(|_| panic!("panicked at truncation length {len}")); + assert!(inner.is_err(), "truncated image parsed at length {len}"); + } +} + +#[test] +fn filesz_exceeds_memsz_refuses() { + let mut img = happy_fixture(&X86_RELOCS).build(); + poke64(&mut img, PH_DATA, P_MEMSZ, 0x800); // filesz stays 0x1000 + assert_eq!( + check_image::(&img).unwrap_err(), + CheckError::Parse(ParseError::FileExceedsMem) + ); +} + +#[test] +fn out_of_bounds_p_offset_refuses() { + let mut img = happy_fixture(&X86_RELOCS).build(); + poke64(&mut img, PH_TEXT, P_OFFSET, 0x10_0000); + assert_eq!( + check_image::(&img).unwrap_err(), + CheckError::Parse(ParseError::OutOfBounds) + ); +} + +#[test] +fn overlapping_loads_refuse() { + let mut img = happy_fixture(&X86_RELOCS).build(); + // Slide the text load onto the RO load's page (still aligned). + poke64(&mut img, PH_TEXT, P_VADDR, RO_VADDR); + assert_eq!( + check_image::(&img).unwrap_err(), + CheckError::Parse(ParseError::Overlap) + ); +} + +#[test] +fn unaligned_vaddr_refuses() { + let mut img = happy_fixture(&X86_RELOCS).build(); + poke64(&mut img, PH_TEXT, P_VADDR, TEXT_VADDR + 0x10); + assert_eq!( + check_image::(&img).unwrap_err(), + CheckError::Parse(ParseError::Misaligned) + ); +} + +#[test] +fn unaligned_p_offset_refuses() { + let mut img = happy_fixture(&X86_RELOCS).build(); + poke64(&mut img, PH_TEXT, P_OFFSET, TEXT_VADDR + 0x10); + assert_eq!( + check_image::(&img).unwrap_err(), + CheckError::Parse(ParseError::Misaligned) + ); +} + +/// **`ET_EXEC` checks, as the absolute kind.** +/// +/// It used to refuse with `NotPie`, which left a fixed-address image +/// with no way to obtain a descriptor *and* the carve windows backing +/// its segment leases — the two halves `res/IMAGE.md` §4's degenerate +/// static case needs at once. +/// +/// The convention is what separates the two, and a link base is what +/// makes it visible: [`nonzero_link_base_rebases`] proves an `ET_DYN` +/// descriptor is *identical* at any link base, because every address is +/// stated as an offset from it. The absolute descriptor is the exact +/// opposite — the shift rides through untouched, which is the whole +/// reason a consumer reads `kind` before an address. +#[test] +fn et_exec_checks_as_the_absolute_kind() { + const BASE: u64 = 0x40000; + + let mut f = happy_fixture(&X86_RELOCS); + f.vaddr_base = BASE; + f.e_type = ET_EXEC; + let checked = check_image::(&f.build()).expect("a static executable checks"); + + let mut g = happy_fixture(&X86_RELOCS); + g.vaddr_base = BASE; + let relative = check_image::(&g.build()).expect("the PIE control checks"); + + assert_eq!(checked.descriptor.kind, kind::STATIC_EXECUTABLE); + assert!(kind::is_absolute(checked.descriptor.kind)); + assert_eq!(relative.descriptor.kind, kind::EXECUTABLE); + + assert_eq!( + checked.descriptor.entry.get(), + relative.descriptor.entry.get() + BASE, + "absolute: the entry is the address, not an offset from a base" + ); + assert_eq!( + checked.descriptor.segs[0].vaddr.get(), + relative.descriptor.segs[0].vaddr.get() + BASE, + "segments keep their link addresses" + ); + + // The carve windows are half the point — a descriptor alone cannot + // back a segment lease, and until now a static image could obtain + // neither. + assert!( + checked.carve[0].is_some(), + "the text segment has a carve window" + ); + // No dynamic facts, because there is no `.dynamic`: nothing to + // relocate, nothing to walk, nothing to seal. + assert_eq!(checked.descriptor.needed_count.get(), 0); + assert_eq!(checked.relro, None); +} + +/// An `ET_EXEC` whose entry is not inside an executable load is refused +/// by the static parser, not filed as a library — the gate the absolute +/// path keeps that the relative one expresses differently. +#[test] +fn et_exec_with_a_bad_entry_still_refuses() { + let mut f = happy_fixture(&X86_RELOCS); + f.e_type = ET_EXEC; + f.entry = DATA_VADDR; + assert_eq!( + check_image::(&f.build()).unwrap_err(), + CheckError::Parse(ParseError::Entry) + ); +} + +#[test] +fn static_boot_executable_uses_the_typed_segment_parser() { + let mut f = happy_fixture(&X86_RELOCS); + f.e_type = ET_EXEC; + let bytes = f.build(); + let image = parse_static_image(&bytes, EM_X86_64).unwrap(); + assert_eq!(image.entry, TEXT_VADDR); + assert_eq!(image.segments.len(), 3); + assert!(image.segments.iter().any(|seg| seg.flags.exec)); + assert!( + image + .segments + .iter() + .all(|seg| !(seg.flags.write && seg.flags.exec)) + ); +} + +#[test] +fn static_boot_executable_refuses_writable_text() { + let mut f = happy_fixture(&X86_RELOCS); + f.e_type = ET_EXEC; + let mut image = f.build(); + poke32(&mut image, PH_TEXT, P_FLAGS, PF_R | PF_W | PF_X); + assert_eq!( + parse_static_image(&image, EM_X86_64).unwrap_err(), + ParseError::WritableExecutable + ); +} + +#[test] +fn static_boot_executable_refuses_an_entry_outside_text() { + let mut f = happy_fixture(&X86_RELOCS); + f.e_type = ET_EXEC; + f.entry = DATA_VADDR; + assert_eq!( + parse_static_image(&f.build(), EM_X86_64).unwrap_err(), + ParseError::Entry + ); +} + +#[test] +fn wrong_machine_refuses() { + let img = happy_fixture(&X86_RELOCS).build(); + assert_eq!( + check_image::(&img).unwrap_err(), + CheckError::Parse(ParseError::Machine) + ); +} + +#[test] +fn missing_bind_now_refuses() { + let mut f = happy_fixture(&X86_RELOCS); + f.flags = 0; + f.flags1 = 0; + assert_eq!( + check_image::(&f.build()).unwrap_err(), + CheckError::Flags + ); +} + +#[test] +fn textrel_flag_refuses() { + let mut f = happy_fixture(&X86_RELOCS); + f.flags = DF_BIND_NOW | DF_TEXTREL; + assert_eq!( + check_image::(&f.build()).unwrap_err(), + CheckError::Flags + ); +} + +#[test] +fn textrel_tag_refuses() { + let mut f = happy_fixture(&X86_RELOCS); + f.textrel_tag = true; + assert_eq!( + check_image::(&f.build()).unwrap_err(), + CheckError::Flags + ); +} + +/// One test per rejected relocation type per arch (the §7 reject list): +/// each must refuse as `Reloc(Rejected(t))`, in both rela tables. +#[test] +fn reject_list_relocs_refuse_per_arch() { + fn run(arch: &ArchRelocs) { + for &t in &arch.rejected { + // In .rela.dyn … + let mut f = Fixture::new(arch.machine); + f.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: t, + sym: 0, + addend: 0, + }]; + assert_eq!( + check_image::(&f.build()).unwrap_err(), + CheckError::Reloc(RelocRefusal::Rejected(t)), + "machine {} type {t} (.rela.dyn)", + arch.machine, + ); + // … and in .rela.plt. + let mut f = Fixture::new(arch.machine); + f.plt_relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: t, + sym: 0, + addend: 0, + }]; + assert_eq!( + check_image::(&f.build()).unwrap_err(), + CheckError::Reloc(RelocRefusal::Rejected(t)), + "machine {} type {t} (.rela.plt)", + arch.machine, + ); + } + } + run::(&X86_RELOCS); + run::(&AARCH64_RELOCS); + run::(&RISCV_RELOCS); +} + +#[test] +fn unknown_reloc_refuses() { + let mut f = Fixture::new(EM_X86_64); + f.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: 9999, + sym: 0, + addend: 0, + }]; + assert_eq!( + check_image::(&f.build()).unwrap_err(), + CheckError::Reloc(RelocRefusal::Unknown(9999)) + ); +} + +#[test] +fn tprel_without_pt_tls_refuses() { + for (arch, tprel) in [ + (&X86_RELOCS, X86_RELOCS.tprel), + (&AARCH64_RELOCS, AARCH64_RELOCS.tprel), + (&RISCV_RELOCS, RISCV_RELOCS.tprel), + ] { + let mut f = Fixture::new(arch.machine); + f.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: tprel, + sym: 0, + addend: 0, + }]; + let img = f.build(); + let err = match arch.machine { + EM_X86_64 => check_image::(&img).unwrap_err(), + EM_AARCH64 => check_image::(&img).unwrap_err(), + _ => check_image::(&img).unwrap_err(), + }; + assert_eq!(err, CheckError::Tls, "machine {}", arch.machine); + } +} + +#[test] +fn dynamic_tag_outside_segments_refuses() { + // DT_STRTAB pointing where no load segment lives. + let mut f = happy_fixture(&X86_RELOCS); + f.strtab_vaddr_override = Some(0x8000); + assert_eq!( + check_image::(&f.build()).unwrap_err(), + CheckError::Parse(ParseError::Dynamic) + ); +} + +#[test] +fn rela_range_escaping_segment_refuses() { + // DT_RELA so close to the RO segment's file end that the table + // crosses it — the range resolution must refuse. + let mut f = happy_fixture(&X86_RELOCS); + f.rela_vaddr_override = Some(PAGE - 8); + assert_eq!( + check_image::(&f.build()).unwrap_err(), + CheckError::Parse(ParseError::Dynamic) + ); +} + +#[test] +fn wx_segment_refuses() { + let mut img = happy_fixture(&X86_RELOCS).build(); + poke32(&mut img, PH_DATA, P_FLAGS, PF_R | PF_W | PF_X); + assert_eq!( + check_image::(&img).unwrap_err(), + CheckError::Segment + ); +} + +#[test] +fn executable_entry_in_data_refuses() { + // Repeated data LOADs are legal, but an executable entry may still land + // only in an executable LOAD. + let mut img = happy_fixture(&X86_RELOCS).build(); + poke32(&mut img, PH_TEXT, P_FLAGS, PF_R | PF_W); + assert_eq!(check_image::(&img).unwrap_err(), CheckError::Range); +} + +#[test] +fn no_load_segments_refuses() { + let mut img = happy_fixture(&X86_RELOCS).build(); + poke32(&mut img, PH_RO, P_TYPE, PT_NULL); + poke32(&mut img, PH_TEXT, P_TYPE, PT_NULL); + poke32(&mut img, PH_DATA, P_TYPE, PT_NULL); + assert_eq!( + check_image::(&img).unwrap_err(), + CheckError::Parse(ParseError::NoSegments) + ); +} + +#[test] +fn too_many_load_segments_refuses() { + let mut f = Fixture::new(EM_X86_64); + f.extra_loads = 6; // 3 + 6 = 9 > MAX_LOAD_SEGMENTS (8) + assert_eq!( + check_image::(&f.build()).unwrap_err(), + CheckError::Parse(ParseError::TooManySegments) + ); +} + +#[test] +fn missing_pt_dynamic_refuses() { + let mut img = happy_fixture(&X86_RELOCS).build(); + poke32(&mut img, PH_DYNAMIC, P_TYPE, PT_NULL); + assert_eq!( + check_image::(&img).unwrap_err(), + CheckError::Parse(ParseError::Dynamic) + ); +} + +#[test] +fn entry_outside_segments_refuses() { + let mut f = happy_fixture(&X86_RELOCS); + f.entry = 0x9_0000; // no load segment there + assert_eq!( + check_image::(&f.build()).unwrap_err(), + CheckError::Range + ); +} + +#[test] +fn pure_tbss_needs_no_file_congruence_or_initialization_lease() { + for machine in [EM_X86_64, EM_AARCH64, EM_RISCV] { + let mut fixture = Fixture::new(machine); + fixture.entry = TEXT_VADDR; + fixture.tls = Some(TlsSpec { + memsz: 0x101a, + filesz: 0, + align: 8, + }); + let mut image = fixture.build(); + poke64(&mut image, PH_DYNAMIC + 1, P_OFFSET, TLS_VADDR + 8); + let checked = match machine { + EM_X86_64 => check_image::(&image), + EM_AARCH64 => check_image::(&image), + _ => check_image::(&image), + } + .unwrap(); + assert!(checked.descriptor.validate()); + assert_eq!(checked.descriptor.tls.memsz.get(), 0x101a); + assert_eq!(checked.descriptor.tls.init_sz.get(), 0); + assert!( + checked + .descriptor + .live_segs() + .iter() + .all(|s| s.kind != seg::KIND_TLS) + ); + assert!( + checked + .carve + .iter() + .flatten() + .all(|s| s.kind != seg::KIND_TLS && s.len != 0) + ); + } +} + +#[test] +fn relro_refuses_partial_pages_and_foreign_ranges_at_registration() { + for range in [ + (DATA_VADDR + 1, 4096), + (DATA_VADDR, 4095), + (DATA_VADDR, 0), + (TEXT_VADDR, 4096), + (RO_VADDR, 4096), + (DATA_VADDR, 0x10000), + (u64::MAX - 4095, 8192), + ] { + let mut f = happy_fixture(&X86_RELOCS); + f.relro = Some(range); + assert_eq!( + check_image::(&f.build()).unwrap_err(), + CheckError::Relro + ); + } +} diff --git a/native/crates/ld/tests/common/mod.rs b/native/crates/ld/tests/common/mod.rs new file mode 100644 index 0000000..a68288b --- /dev/null +++ b/native/crates/ld/tests/common/mod.rs @@ -0,0 +1,656 @@ +//! Synthetic-ELF fixture builder for the kore-ld host suite (std allowed in +//! tests). Assembles minimal but *structurally honest* ELF64 `ET_DYN` +//! images — ehdr + phdrs + `.dynamic` + `.dynsym`/`.dynstr` + GNU hash + +//! `.rela.dyn`/`.rela.plt`/`.relr.dyn` — parameterized by arch, so the +//! same builder drives the happy paths, the adversarial corpus (tests +//! corrupt the emitted bytes at known offsets), and the apply-engine +//! fixtures. +//! +//! Fixed layout (link base 0 unless `vaddr_base` shifts it): +//! +//! ```text +//! file/vaddr 0x0000..0x1000 R ehdr, phdrs, .dynstr, .dynsym, +//! .gnu.hash, .rela.dyn, .rela.plt, .relr +//! file/vaddr 0x1000..0x2000 R|X text (0x90 fill) +//! file/vaddr 0x2000..0x3000 R|W .dynamic @ 0x2000, data/GOT after, +//! TLS init image @ 0x2800 when present +//! ``` + +#![allow(dead_code)] + +// ---- ELF constants the builder needs (kept local: the builder is an +// ---- independent implementation, not a re-export of the code under test). +pub const ET_DYN: u16 = 3; +pub const ET_EXEC: u16 = 2; +pub const PT_NULL: u32 = 0; +pub const PT_LOAD: u32 = 1; +pub const PT_DYNAMIC: u32 = 2; +pub const PT_TLS: u32 = 7; +pub const PT_GNU_RELRO: u32 = 0x6474_e552; +pub const PF_X: u32 = 1; +pub const PF_W: u32 = 2; +pub const PF_R: u32 = 4; + +pub const DT_NULL: i64 = 0; +pub const DT_NEEDED: i64 = 1; +pub const DT_PLTRELSZ: i64 = 2; +pub const DT_STRTAB: i64 = 5; +pub const DT_SYMTAB: i64 = 6; +pub const DT_RELA: i64 = 7; +pub const DT_RELASZ: i64 = 8; +pub const DT_RELAENT: i64 = 9; +pub const DT_STRSZ: i64 = 10; +pub const DT_SYMENT: i64 = 11; +pub const DT_INIT_ARRAY: i64 = 25; +pub const DT_FINI_ARRAY: i64 = 26; +pub const DT_INIT_ARRAYSZ: i64 = 27; +pub const DT_FINI_ARRAYSZ: i64 = 28; +pub const DT_PLTREL: i64 = 20; +pub const DT_TEXTREL: i64 = 22; +pub const DT_JMPREL: i64 = 23; +pub const DT_FLAGS: i64 = 30; +pub const DT_RELRSZ: i64 = 35; +pub const DT_RELR: i64 = 36; +pub const DT_RELRENT: i64 = 37; +pub const DT_GNU_HASH: i64 = 0x6fff_fef5; +pub const DT_FLAGS_1: i64 = 0x6fff_fffb; + +pub const DF_TEXTREL: u64 = 0x4; +pub const DF_BIND_NOW: u64 = 0x8; +pub const DF_1_NOW: u64 = 0x1; + +pub const STB_GLOBAL: u8 = 1; +pub const STB_WEAK: u8 = 2; +pub const SHN_UNDEF: u16 = 0; + +pub const EM_X86_64: u16 = 62; +pub const EM_AARCH64: u16 = 183; +pub const EM_RISCV: u16 = 243; + +// ---- Layout anchors (see the module doc). +pub const PAGE: u64 = 0x1000; +pub const RO_VADDR: u64 = 0x0000; +pub const TEXT_VADDR: u64 = 0x1000; +pub const DATA_VADDR: u64 = 0x2000; +pub const DYNAMIC_VADDR: u64 = 0x2000; +pub const TLS_VADDR: u64 = 0x2800; +pub const IMAGE_LEN: usize = 0x3000; + +pub const DYNSTR_OFF: u64 = 0x300; +pub const DYNSYM_OFF: u64 = 0x400; +pub const GNU_HASH_OFF: u64 = 0x580; +pub const RELA_OFF: u64 = 0x680; +pub const PLT_RELA_OFF: u64 = 0x800; +pub const RELR_OFF: u64 = 0x8c0; + +pub const EHDR_SIZE: usize = 64; +pub const PHDR_SIZE: usize = 56; +pub const PHOFF: usize = 64; + +/// Byte offset of phdr `i`'s start (for post-build corruption). +pub fn phdr_off(i: usize) -> usize { + PHOFF + i * PHDR_SIZE +} + +/// Phdr indices in the emitted table (fixed emission order). +pub const PH_RO: usize = 0; +pub const PH_TEXT: usize = 1; +pub const PH_DATA: usize = 2; +pub const PH_DYNAMIC: usize = 3; +// PT_TLS / PT_GNU_RELRO / extra loads follow, in that order, when present. + +/// One relocation the builder emits. +#[derive(Clone, Copy, Debug)] +pub struct Rela { + pub offset: u64, + pub rtype: u32, + pub sym: u32, + pub addend: i64, +} + +/// One dynsym entry after the reserved null symbol (dynsym index = +/// position in `Fixture::syms` + 1). +#[derive(Clone, Debug)] +pub struct Sym { + pub name: String, + pub value: u64, + pub size: u64, + /// `st_info` = binding << 4 (type nibble left 0 — the resolver + /// doesn't consult it). + pub binding: u8, + /// `SHN_UNDEF` = import; anything else = definition. + pub shndx: u16, +} + +impl Sym { + pub fn global(name: &str, value: u64) -> Self { + Sym { + name: name.into(), + value, + size: 8, + binding: STB_GLOBAL, + shndx: 1, + } + } + pub fn weak(name: &str, value: u64) -> Self { + Sym { + name: name.into(), + value, + size: 8, + binding: STB_WEAK, + shndx: 1, + } + } + pub fn import(name: &str) -> Self { + Sym { + name: name.into(), + value: 0, + size: 0, + binding: STB_GLOBAL, + shndx: SHN_UNDEF, + } + } + pub fn weak_import(name: &str) -> Self { + Sym { + name: name.into(), + value: 0, + size: 0, + binding: STB_WEAK, + shndx: SHN_UNDEF, + } + } +} + +/// TLS geometry knob. +#[derive(Clone, Copy, Debug)] +pub struct TlsSpec { + pub memsz: u64, + pub align: u64, + pub filesz: u64, +} + +/// The parameterized image builder. +#[derive(Clone, Debug)] +pub struct Fixture { + pub machine: u16, + pub e_type: u16, + /// Zero = library; nonzero = executable entry (link vaddr). + pub entry: u64, + /// `DT_FLAGS` value (always emitted). + pub flags: u64, + /// `DT_FLAGS_1` value (emitted when nonzero). + pub flags1: u64, + /// Emit a bare `DT_TEXTREL` tag. + pub textrel_tag: bool, + pub relas: Vec, + pub plt_relas: Vec, + /// Raw RELR words (already encoded). + pub relr: Vec, + pub syms: Vec, + pub needed: Vec, + pub tls: Option, + /// `PT_GNU_RELRO` as `(vaddr, memsz)`. + pub relro: Option<(u64, u64)>, + /// Extra `.bss` on the RW load (`p_memsz = 0x1000 + bss`). + pub bss: u64, + /// Emit a separate **filesz-0 writable LOAD** of `bss_tail` bytes + /// right after the data load — the pure-bss tail real linkers emit + /// (folded into the data segment's consumer memsz by the checker, + /// never a carve class of its own). `0` = absent. + pub bss_tail: u64, + /// Extra page distance between the data load's rounded end and the + /// tail's vaddr (0 = contiguous; >0 = the non-contiguous malformed + /// shape the checker refuses). + pub bss_tail_gap_pages: u64, + /// Emit an all-zero bloom word (hides every symbol from GNU-hash + /// lookups that honor the filter). + pub zero_bloom: bool, + /// Emit `DT_INIT_ARRAY`/`DT_FINI_ARRAY` at fixed data offsets. + pub init_fini: bool, + /// Number of entries in the finalizer array. + pub fini_count: u64, + /// Shift every vaddr (loads, dynamic tags, entry, relro) by this + /// page-aligned amount — the nonzero-link-base variant. + pub vaddr_base: u64, + /// Extra degenerate PT_LOADs appended (for the segment-count bound). + pub extra_loads: usize, + /// Override the emitted `DT_STRTAB` vaddr (a lying tag). + pub strtab_vaddr_override: Option, + /// Override the emitted `DT_RELA` vaddr (a lying tag). + pub rela_vaddr_override: Option, +} + +impl Default for Fixture { + fn default() -> Self { + Fixture { + machine: EM_X86_64, + e_type: ET_DYN, + entry: 0, + flags: DF_BIND_NOW, + flags1: 0, + textrel_tag: false, + relas: Vec::new(), + plt_relas: Vec::new(), + relr: Vec::new(), + syms: Vec::new(), + needed: Vec::new(), + tls: None, + relro: None, + bss: 0, + bss_tail: 0, + bss_tail_gap_pages: 0, + zero_bloom: false, + init_fini: false, + fini_count: 1, + vaddr_base: 0, + extra_loads: 0, + strtab_vaddr_override: None, + rela_vaddr_override: None, + } + } +} + +fn w16(buf: &mut [u8], off: usize, v: u16) { + buf[off..off + 2].copy_from_slice(&v.to_le_bytes()); +} +fn w32(buf: &mut [u8], off: usize, v: u32) { + buf[off..off + 4].copy_from_slice(&v.to_le_bytes()); +} +fn w64(buf: &mut [u8], off: usize, v: u64) { + buf[off..off + 8].copy_from_slice(&v.to_le_bytes()); +} +fn wi64(buf: &mut [u8], off: usize, v: i64) { + buf[off..off + 8].copy_from_slice(&v.to_le_bytes()); +} + +/// The GNU hash function — the builder's independent copy (djb2/33). +pub fn gnu_hash(name: &[u8]) -> u32 { + let mut h: u32 = 5381; + for &b in name { + h = h.wrapping_mul(33).wrapping_add(u32::from(b)); + } + h +} + +impl Fixture { + pub fn new(machine: u16) -> Self { + Fixture { + machine, + ..Default::default() + } + } + + /// Assemble the image bytes. + pub fn build(&self) -> Vec { + let vb = self.vaddr_base; + let mut img = vec![0u8; IMAGE_LEN]; + + // ---- .dynstr: NUL, then every sym + needed name. + let mut dynstr = vec![0u8]; + let name_off = |dynstr: &mut Vec, name: &str| -> u64 { + let off = dynstr.len() as u64; + dynstr.extend_from_slice(name.as_bytes()); + dynstr.push(0); + off + }; + let sym_name_offs: Vec = self + .syms + .iter() + .map(|s| name_off(&mut dynstr, &s.name)) + .collect(); + let needed_offs: Vec = self + .needed + .iter() + .map(|n| name_off(&mut dynstr, n)) + .collect(); + assert!(dynstr.len() <= 0x100, "dynstr overflows its window"); + let dynstr_len = dynstr.len() as u64; + img[DYNSTR_OFF as usize..DYNSTR_OFF as usize + dynstr.len()].copy_from_slice(&dynstr); + + // ---- .dynsym: null entry + syms. + let nsyms = self.syms.len(); + assert!((nsyms + 1) * 24 <= 0x180, "dynsym overflows its window"); + for (i, s) in self.syms.iter().enumerate() { + let off = DYNSYM_OFF as usize + (i + 1) * 24; + w32(&mut img, off, sym_name_offs[i] as u32); + img[off + 4] = s.binding << 4; + img[off + 5] = 0; + w16(&mut img, off + 6, s.shndx); + w64(&mut img, off + 8, s.value); + w64(&mut img, off + 16, s.size); + } + + // ---- .gnu.hash: nbuckets=1, symoffset=1, bloom_size=1, + // bloom_shift=6; all syms (index 1..) hashed into bucket 0. + { + let off = GNU_HASH_OFF as usize; + w32(&mut img, off, 1); // nbuckets + w32(&mut img, off + 4, 1); // symoffset + w32(&mut img, off + 8, 1); // bloom_size + w32(&mut img, off + 12, 6); // bloom_shift + let bloom: u64 = if self.zero_bloom { 0 } else { u64::MAX }; + w64(&mut img, off + 16, bloom); + // bucket[0]: first hashed sym index, or 0 when none. + w32(&mut img, off + 24, if nsyms == 0 { 0 } else { 1 }); + for (i, s) in self.syms.iter().enumerate() { + let h = gnu_hash(s.name.as_bytes()); + let last = i == nsyms - 1; + let chain = (h & !1) | u32::from(last); + w32(&mut img, off + 28 + i * 4, chain); + } + assert!(28 + nsyms * 4 <= 0x100, "gnu.hash overflows its window"); + } + + // ---- rela tables + relr. + let write_relas = |img: &mut Vec, base_off: u64, relas: &[Rela], cap: usize| { + assert!(relas.len() * 24 <= cap, "rela table overflows its window"); + // (offsets shift with the link base like every other vaddr) + for (i, r) in relas.iter().enumerate() { + let off = base_off as usize + i * 24; + w64(img, off, r.offset.wrapping_add(vb)); + w64(img, off + 8, (u64::from(r.sym) << 32) | u64::from(r.rtype)); + wi64(img, off + 16, r.addend); + } + }; + write_relas(&mut img, RELA_OFF, &self.relas, 0x180); + write_relas(&mut img, PLT_RELA_OFF, &self.plt_relas, 0xc0); + assert!(self.relr.len() * 8 <= 0x40, "relr overflows its window"); + for (i, w) in self.relr.iter().enumerate() { + w64(&mut img, RELR_OFF as usize + i * 8, *w); + } + + // ---- text fill. + for b in &mut img[TEXT_VADDR as usize..(TEXT_VADDR + PAGE) as usize] { + *b = 0x90; + } + + // ---- .dynamic (at file 0x2000). + let mut dynamic: Vec<(i64, u64)> = Vec::new(); + dynamic.push((DT_FLAGS, self.flags)); + if self.flags1 != 0 { + dynamic.push((DT_FLAGS_1, self.flags1)); + } + if self.textrel_tag { + dynamic.push((DT_TEXTREL, 0)); + } + for off in &needed_offs { + dynamic.push((DT_NEEDED, *off)); + } + dynamic.push(( + DT_STRTAB, + self.strtab_vaddr_override.unwrap_or(DYNSTR_OFF + vb), + )); + dynamic.push((DT_STRSZ, dynstr_len)); + dynamic.push((DT_SYMTAB, DYNSYM_OFF + vb)); + dynamic.push((DT_SYMENT, 24)); + dynamic.push((DT_GNU_HASH, GNU_HASH_OFF + vb)); + if !self.relas.is_empty() { + dynamic.push((DT_RELA, self.rela_vaddr_override.unwrap_or(RELA_OFF + vb))); + dynamic.push((DT_RELASZ, self.relas.len() as u64 * 24)); + dynamic.push((DT_RELAENT, 24)); + } + if !self.plt_relas.is_empty() { + dynamic.push((DT_JMPREL, PLT_RELA_OFF + vb)); + dynamic.push((DT_PLTRELSZ, self.plt_relas.len() as u64 * 24)); + dynamic.push((DT_PLTREL, DT_RELA as u64)); + } + if !self.relr.is_empty() { + dynamic.push((DT_RELR, RELR_OFF + vb)); + dynamic.push((DT_RELRSZ, self.relr.len() as u64 * 8)); + dynamic.push((DT_RELRENT, 8)); + } + if self.init_fini { + dynamic.push((DT_INIT_ARRAY, DATA_VADDR + 0x400 + vb)); + dynamic.push((DT_INIT_ARRAYSZ, 16)); + dynamic.push((DT_FINI_ARRAY, DATA_VADDR + 0x440 + vb)); + dynamic.push((DT_FINI_ARRAYSZ, 8 * self.fini_count)); + } + dynamic.push((DT_NULL, 0)); + let dyn_len = dynamic.len() * 16; + assert!(dyn_len <= 0x200, "dynamic overflows its window"); + for (i, (tag, val)) in dynamic.iter().enumerate() { + let off = DYNAMIC_VADDR as usize + i * 16; + wi64(&mut img, off, *tag); + w64(&mut img, off + 8, *val); + } + + // ---- ehdr. + img[0..4].copy_from_slice(&[0x7f, b'E', b'L', b'F']); + img[4] = 2; // ELFCLASS64 + img[5] = 1; // ELFDATA2LSB + img[6] = 1; // EV_CURRENT + w16(&mut img, 16, self.e_type); + w16(&mut img, 18, self.machine); + w32(&mut img, 20, 1); // e_version + w64( + &mut img, + 24, + if self.entry == 0 { 0 } else { self.entry + vb }, + ); + w64(&mut img, 32, PHOFF as u64); // e_phoff + w64(&mut img, 40, 0); // e_shoff + w16(&mut img, 52, EHDR_SIZE as u16); + w16(&mut img, 54, PHDR_SIZE as u16); + let phnum = 4 + + usize::from(self.tls.is_some()) + + usize::from(self.relro.is_some()) + + usize::from(self.bss_tail != 0) + + self.extra_loads; + w16(&mut img, 56, phnum as u16); + + // ---- phdrs. + let ph = |img: &mut Vec, + i: usize, + p_type: u32, + p_flags: u32, + p_off: u64, + p_vaddr: u64, + p_filesz: u64, + p_memsz: u64, + p_align: u64| { + let off = phdr_off(i); + w32(img, off, p_type); + w32(img, off + 4, p_flags); + w64(img, off + 8, p_off); + w64(img, off + 16, p_vaddr); + w64(img, off + 24, p_vaddr); // p_paddr + w64(img, off + 32, p_filesz); + w64(img, off + 40, p_memsz); + w64(img, off + 48, p_align); + }; + ph( + &mut img, + PH_RO, + PT_LOAD, + PF_R, + 0, + RO_VADDR + vb, + PAGE, + PAGE, + PAGE, + ); + ph( + &mut img, + PH_TEXT, + PT_LOAD, + PF_R | PF_X, + TEXT_VADDR, + TEXT_VADDR + vb, + PAGE, + PAGE, + PAGE, + ); + ph( + &mut img, + PH_DATA, + PT_LOAD, + PF_R | PF_W, + DATA_VADDR, + DATA_VADDR + vb, + PAGE, + PAGE + self.bss, + PAGE, + ); + ph( + &mut img, + PH_DYNAMIC, + PT_DYNAMIC, + PF_R | PF_W, + DYNAMIC_VADDR, + DYNAMIC_VADDR + vb, + dyn_len as u64, + dyn_len as u64, + 8, + ); + let mut next = PH_DYNAMIC + 1; + if let Some(t) = self.tls { + assert!(t.filesz <= t.memsz && TLS_VADDR + t.filesz <= DATA_VADDR + PAGE); + ph( + &mut img, + next, + PT_TLS, + PF_R, + TLS_VADDR, + TLS_VADDR + vb, + t.filesz, + t.memsz, + t.align, + ); + next += 1; + } + if let Some((va, len)) = self.relro { + ph(&mut img, next, PT_GNU_RELRO, PF_R, va, va + vb, len, len, 1); + next += 1; + } + if self.bss_tail != 0 { + // The pure-bss tail: writable, byte-less, page-aligned and + // contiguous with the data load's page-rounded end (the + // shape the checker's fold accepts — the consumer's + // zero-fill runs straight on from the template). + let va = (DATA_VADDR + PAGE + self.bss).next_multiple_of(PAGE) + + self.bss_tail_gap_pages * PAGE; + ph( + &mut img, + next, + PT_LOAD, + PF_R | PF_W, + 0, + va + vb, + 0, + self.bss_tail, + PAGE, + ); + next += 1; + } + for i in 0..self.extra_loads { + // Degenerate but valid loads far away: file-empty, one page. + let va = 0x10_0000 + (i as u64) * PAGE; + ph(&mut img, next, PT_LOAD, PF_R, 0, va + vb, 0, PAGE, PAGE); + next += 1; + } + assert_eq!(next, phnum); + assert!( + PHOFF + phnum * PHDR_SIZE <= DYNSTR_OFF as usize, + "phdr table overflows into the dynstr window" + ); + + img + } +} + +/// A leaked private-memory window for mock link-worlds: `AsRef`/`AsMut` +/// materialize a reference only for the duration of each call, so the +/// world's own raw-pointer inspection of the same bytes (op-time +/// snapshots, post-link asserts) never overlaps a live `&mut` — the +/// aliasing discipline of kore-rt's mock `Window`, test-sized. +pub struct RawWin { + ptr: *mut u8, + len: usize, +} + +impl RawWin { + /// Leak a zeroed `len`-byte allocation; the paired raw pointer is + /// the world's inspection handle. + pub fn leak_zeroed(len: usize) -> (Self, *mut u8) { + let ptr = Box::into_raw(vec![0u8; len].into_boxed_slice()) as *mut u8; + (RawWin { ptr, len }, ptr) + } +} + +impl AsRef<[u8]> for RawWin { + fn as_ref(&self) -> &[u8] { + // SAFETY: leaked, in-bounds, exclusively the engine's to write. + unsafe { core::slice::from_raw_parts(self.ptr, self.len) } + } +} + +impl AsMut<[u8]> for RawWin { + fn as_mut(&mut self) -> &mut [u8] { + // SAFETY: as above; the borrow lives only for this call. + unsafe { core::slice::from_raw_parts_mut(self.ptr, self.len) } + } +} + +// ---- Post-build corruption helpers (phdr field offsets). +pub const P_TYPE: usize = 0; +pub const P_FLAGS: usize = 4; +pub const P_OFFSET: usize = 8; +pub const P_VADDR: usize = 16; +pub const P_FILESZ: usize = 32; +pub const P_MEMSZ: usize = 40; + +/// Overwrite a u64 phdr field in a built image. +pub fn poke64(img: &mut [u8], phdr: usize, field: usize, v: u64) { + let off = phdr_off(phdr) + field; + img[off..off + 8].copy_from_slice(&v.to_le_bytes()); +} + +/// Overwrite a u32 phdr field in a built image. +pub fn poke32(img: &mut [u8], phdr: usize, field: usize, v: u32) { + let off = phdr_off(phdr) + field; + img[off..off + 4].copy_from_slice(&v.to_le_bytes()); +} + +/// Read a little-endian u64 out of a byte slice (assertion helper). +pub fn read64(bytes: &[u8], off: usize) -> u64 { + u64::from_le_bytes(bytes[off..off + 8].try_into().unwrap()) +} + +// ---- Per-arch relocation numbers (the §7 table, test-side copy). +pub struct ArchRelocs { + pub machine: u16, + pub relative: u32, + pub glob_dat: u32, + pub jump_slot: u32, + pub abs64: u32, + pub tprel: u32, + pub rejected: [u32; 5], +} + +pub const X86_RELOCS: ArchRelocs = ArchRelocs { + machine: EM_X86_64, + relative: 8, + glob_dat: 6, + jump_slot: 7, + abs64: 1, + tprel: 18, + rejected: [5, 16, 17, 36, 37], +}; +pub const AARCH64_RELOCS: ArchRelocs = ArchRelocs { + machine: EM_AARCH64, + relative: 1027, + glob_dat: 1025, + jump_slot: 1026, + abs64: 257, + tprel: 1030, + rejected: [1024, 1028, 1029, 1031, 1032], +}; +pub const RISCV_RELOCS: ArchRelocs = ArchRelocs { + machine: EM_RISCV, + relative: 3, + glob_dat: 2, // R_RISCV_64 doubles as the GOT-slot form + jump_slot: 5, + abs64: 2, + tprel: 11, + rejected: [4, 7, 9, 12, 58], +}; diff --git a/native/crates/ld/tests/late_layout.rs b/native/crates/ld/tests/late_layout.rs new file mode 100644 index 0000000..8dbeffc --- /dev/null +++ b/native/crates/ld/tests/late_layout.rs @@ -0,0 +1,143 @@ +//! The late-binding plan (`kore_ld::late`): what a checked library maps +//! to when a task links it into its own space after start, and the shapes +//! it refuses rather than guesses. + +mod common; + +use common::*; +use kore_abi::image::seg; +use kore_ld::check::check_image; +use kore_ld::late::{LateError, LateLayout}; +use kore_ld::reloc::X86_64; + +const NO_LIMIT: u64 = u64::MAX; + +#[test] +fn relro_must_be_an_exact_page_range_inside_private_data() { + let mut checked = check_image::(&library().build()).unwrap(); + checked.relro = Some((DATA_VADDR, 4096)); + assert!(LateLayout::plan(&checked, NO_LIMIT, NO_LIMIT).is_ok()); + for range in [ + (DATA_VADDR, 0), + (DATA_VADDR + 8, 4096), + (DATA_VADDR, 4095), + (TEXT_VADDR, 4096), + (DATA_VADDR, 8192), + (u64::MAX - 4095, 4096), + ] { + checked.relro = Some(range); + assert_eq!( + LateLayout::plan(&checked, NO_LIMIT, NO_LIMIT), + Err(LateError::Relro) + ); + } +} + +fn library() -> Fixture { + let mut f = Fixture::new(EM_X86_64); + f.syms = vec![Sym::global("alpha", TEXT_VADDR + 0x10)]; + f.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: X86_RELOCS.relative, + sym: 0, + addend: 0x40, + }]; + f +} + +#[test] +fn a_library_plans_one_code_run_and_one_data_span_above_it() { + let img = library().build(); + let checked = check_image::(&img).unwrap(); + let plan = LateLayout::plan(&checked, NO_LIMIT, NO_LIMIT).unwrap(); + // The fixture's headers+rodata sit at 0, text at TEXT_VADDR: one run. + assert_eq!(plan.code_vaddr, 0); + assert_eq!(plan.code_len, DATA_VADDR); + let copies: Vec<_> = plan.code_copies.iter().flatten().collect(); + assert_eq!(copies.len(), 2); + assert!(copies.iter().any(|c| c.dst == TEXT_VADDR)); + // Every copy lands inside the run and names bytes inside the image. + for c in &copies { + assert!(c.dst + c.len <= plan.code_len); + assert!(c.file_off + c.len <= img.len() as u64); + } + let data = plan.data.expect("the fixture has a writable segment"); + assert_eq!(data.vaddr, DATA_VADDR); + assert!(data.vaddr >= plan.code_vaddr + plan.code_len); + assert_eq!(data.len % 4096, 0); + assert!(data.bounds_len <= data.len); + let template = data + .copies + .iter() + .flatten() + .next() + .expect("a template copy"); + assert_eq!(template.dst, 0); + assert_eq!(plan.span, data.vaddr + data.len); + // The data segment is the only writable one, and it is the last thing placed. + assert!( + checked + .descriptor + .live_segs() + .iter() + .filter(|s| s.kind == seg::KIND_DATA) + .count() + == 1 + ); +} + +#[test] +fn bss_widens_the_data_span_but_not_its_template() { + let mut f = library(); + f.bss = 0x800; + let img = f.build(); + let checked = check_image::(&img).unwrap(); + let plan = LateLayout::plan(&checked, NO_LIMIT, NO_LIMIT).unwrap(); + let data = plan.data.unwrap(); + // The check gate normalizes a segment's memsz outward to pages, so the + // union already ends on a page; the template stays the file's bytes. + assert_eq!(data.bounds_len, 0x2000); + assert_eq!(data.len, 0x2000); + let template = data.copies.iter().flatten().next().unwrap(); + assert_eq!(template.len, 0x1000); +} + +#[test] +fn an_executable_is_refused() { + let mut f = library(); + f.entry = TEXT_VADDR; + let checked = check_image::(&f.build()).unwrap(); + assert_eq!( + LateLayout::plan(&checked, NO_LIMIT, NO_LIMIT), + Err(LateError::NotLibrary) + ); +} + +#[test] +fn a_tls_carrying_library_is_refused() { + let mut f = library(); + f.tls = Some(TlsSpec { + memsz: 0x10, + align: 8, + filesz: 0x10, + }); + let checked = check_image::(&f.build()).unwrap(); + assert_eq!( + LateLayout::plan(&checked, NO_LIMIT, NO_LIMIT), + Err(LateError::Tls) + ); +} + +#[test] +fn the_callers_budgets_bound_the_plan() { + let checked = check_image::(&library().build()).unwrap(); + assert_eq!( + LateLayout::plan(&checked, DATA_VADDR - 1, NO_LIMIT), + Err(LateError::TooLarge) + ); + assert_eq!( + LateLayout::plan(&checked, NO_LIMIT, IMAGE_LEN as u64 - 1), + Err(LateError::TooLarge) + ); + assert!(LateLayout::plan(&checked, DATA_VADDR, NO_LIMIT).is_ok()); +} diff --git a/native/crates/ld/tests/link_engine.rs b/native/crates/ld/tests/link_engine.rs new file mode 100644 index 0000000..417cee4 --- /dev/null +++ b/native/crates/ld/tests/link_engine.rs @@ -0,0 +1,1431 @@ +//! Link-engine suite over a minimal recording world — the engine's pure +//! parts in isolation (graph walk/dedup/ordering, placement, TLS block +//! math, the passive page, `relocate_self`), no kore-rt anywhere. The mock +//! world serves `check_image`-validated fixtures by name; leases are +//! `(image, segment)` indices, windows are leaked slices — the affine +//! shape without the capability plumbing (that layer is kore-rt's e2e). + +/// A leaked caller arena — the Phase A capacity seam's test face +/// (`res/USERLAND.md` §7: the tables are arena-carved, caller-sized). +fn arena() -> &'static kore_ld::store::LinkArena<'static> { + let buf = Box::leak(Box::new([core::mem::MaybeUninit::::uninit(); 1 << 16])); + Box::leak(Box::new(kore_ld::store::LinkArena::new(buf))) +} + +mod common; + +use common::*; +use kore_abi::image::{IMAGE_ABI, ImageName, MAX_SEGMENTS, ModuleDescriptor, ModulePage, seg}; +use kore_ld::check::{CheckedImage, check_image}; +use kore_ld::link::{ + LinkCaps, LinkError, LinkWorld, LinkedProgram, MAX_LINK_MODULES, SelfRelocError, link_program, + relocate_self, +}; +use kore_ld::reloc::{AArch64, RelocArch, Riscv64, X86_64}; +use zerocopy::FromBytes; + +// --------------------------------------------------------------------------- +// The recording world +// --------------------------------------------------------------------------- + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum WErr { + /// No image of that name registered (the Acquire refusal). + Unknown, +} + +#[derive(Clone, Debug, PartialEq)] +enum Op { + Base { len: u64, align: u64, va: u64 }, + Map { va: u64, len: u64, access: u8 }, + Read { len: u64 }, + Alloc { va: u64, len: u64 }, + Seal { va: u64, len: u64 }, + SetTp(u64), +} + +struct TestWorld { + /// name → (leaked image bytes, its checked view). + images: Vec<(ImageName, &'static [u8], CheckedImage)>, + /// Every acquire, in order (the dedup evidence). + acquires: Vec, + ops: Vec, + /// Private allocations for post-link inspection: (va, len, ptr). + allocs: Vec<(u64, usize, *mut u8)>, + template: Option<(Vec, u64, u64)>, + next_base: u64, + /// A fixed home for the passive page, when the test names one. + page_at: Option, + /// Test knob: withhold segment `.1` of image `.0`'s leases. + withhold: Option<(ImageName, usize)>, + /// What `placed_high_water` answers; `None` = a world that does not + /// record placement (the default, so every existing test stamps 0). + placed_high_water: Option, +} + +impl TestWorld { + fn new() -> Self { + TestWorld { + images: Vec::new(), + acquires: Vec::new(), + ops: Vec::new(), + allocs: Vec::new(), + template: None, + next_base: 0x1000_0000, + page_at: None, + withhold: None, + placed_high_water: None, + } + } + + /// How many times the engine asked the world to place something. + fn base_calls(&self) -> usize { + self.ops + .iter() + .filter(|op| matches!(op, Op::Base { .. })) + .count() + } + + /// Whether a mapping was installed at exactly `va`. + fn mapped_at(&self, va: u64) -> bool { + self.ops + .iter() + .any(|op| matches!(op, Op::Map { va: v, .. } if *v == va)) + } + + fn register(&mut self, name: &str, fixture: &Fixture) { + self.register_bytes::(name, fixture.build()); + } + + fn register_bytes(&mut self, name: &str, bytes: Vec) { + let bytes: &'static [u8] = Box::leak(bytes.into_boxed_slice()); + let checked = check_image::(bytes).expect("fixture must check"); + self.images + .push((ImageName::new(name).unwrap(), bytes, checked)); + } + + /// The bytes a lease's carve window backs (the in-place carve: a + /// page-rounded file window of the registered image). + fn carve_bytes(&self, (img, k): (usize, usize)) -> &'static [u8] { + let (_, bytes, checked) = &self.images[img]; + let c = checked.carve[k].expect("live segment has a carve window"); + let a = c.file_off as usize; + let b = (c.file_off + c.len).min(bytes.len() as u64) as usize; + &bytes[a..b] + } + + /// The `i`-th `choose_base` result (call order: module loads in BFS + /// order, then the TLS block when present, then the passive page). + fn base(&self, i: usize) -> u64 { + self.ops + .iter() + .filter_map(|op| match op { + Op::Base { va, .. } => Some(*va), + _ => None, + }) + .nth(i) + .expect("base was chosen") + } + + /// Read one little-endian word out of a private allocation. + fn word_at(&self, va: u64) -> u64 { + let mut out = [0u8; 8]; + self.bytes_at(va, &mut out); + u64::from_le_bytes(out) + } + + /// Copy bytes out of a private allocation (post-link inspection — + /// the engine's window handles are dead by then). + fn bytes_at(&self, va: u64, out: &mut [u8]) { + let (base, len, ptr) = self + .allocs + .iter() + .copied() + .find(|&(b, l, _)| va >= b && va + out.len() as u64 <= b + l as u64) + .expect("va lands in a private allocation"); + let off = (va - base) as usize; + assert!(off + out.len() <= len); + // SAFETY: in-bounds within a leaked, initialized allocation; the + // engine's &mut window over it was dropped with the link. + unsafe { + core::ptr::copy_nonoverlapping(ptr.add(off), out.as_mut_ptr(), out.len()); + } + } +} + +impl LinkWorld for TestWorld { + type Error = WErr; + type Lease = (usize, usize); + type Ro = &'static [u8]; + type Rw = RawWin; + + fn placed_high_water(&self) -> Option { + self.placed_high_water + } + + fn acquire( + &mut self, + name: &ImageName, + ) -> Result<(ModuleDescriptor, [Option<(usize, usize)>; MAX_SEGMENTS]), WErr> { + self.acquires.push(*name); + let (img, entry) = self + .images + .iter() + .enumerate() + .find(|(_, (n, ..))| n == name) + .ok_or(WErr::Unknown)?; + let d = entry.2.descriptor; + let mut leases = [None; MAX_SEGMENTS]; + for k in 0..d.seg_count as usize { + if self.withhold == Some((*name, k)) { + continue; + } + leases[k] = Some((img, k)); + } + Ok((d, leases)) + } + + fn module_page_base(&mut self, len: u64) -> Result { + match self.page_at { + Some(va) => { + self.ops.push(Op::Base { + len, + align: len, + va, + }); + Ok(va) + } + None => self.choose_base(len, len), + } + } + + fn choose_base(&mut self, len: u64, align: u64) -> Result { + let align = align.max(0x1000); + let va = self.next_base.next_multiple_of(align); + self.next_base = va + len.next_multiple_of(0x1000) + 0x1_0000; + self.ops.push(Op::Base { len, align, va }); + Ok(va) + } + + fn map_image( + &mut self, + lease: (usize, usize), + va: u64, + access: u8, + ) -> Result<&'static [u8], WErr> { + assert_eq!( + access & seg::ACCESS_W, + 0, + "the engine never asks W on a share" + ); + let bytes = self.carve_bytes(lease); + self.ops.push(Op::Map { + va, + len: bytes.len() as u64, + access, + }); + Ok(bytes) + } + + fn read_segment(&mut self, lease: (usize, usize)) -> Result<&'static [u8], WErr> { + let bytes = self.carve_bytes(lease); + self.ops.push(Op::Read { + len: bytes.len() as u64, + }); + Ok(bytes) + } + + fn alloc_private(&mut self, va: u64, len: u64) -> Result { + let (win, ptr) = RawWin::leak_zeroed(len as usize); + self.allocs.push((va, len as usize, ptr)); + self.ops.push(Op::Alloc { va, len }); + Ok(win) + } + + fn retain_tls_template(&mut self, bytes: &[u8], tp: u64, align: u64) -> Result<(), WErr> { + assert!(self.template.is_none()); + self.template = Some((bytes.to_vec(), tp, align)); + Ok(()) + } + + fn seal_ro(&mut self, va: u64, len: u64) -> Result<(), WErr> { + self.ops.push(Op::Seal { va, len }); + Ok(()) + } + + fn set_tp(&mut self, tp: u64) -> Result<(), WErr> { + self.ops.push(Op::SetTp(tp)); + Ok(()) + } + + fn lease_id(&self, &(img, k): &(usize, usize)) -> u32 { + (img * MAX_SEGMENTS + k + 1) as u32 + } +} + +fn name(s: &str) -> ImageName { + ImageName::new(s).unwrap() +} + +// --------------------------------------------------------------------------- +// The diamond fixtures (link base 0; per-arch reloc numbers) +// --------------------------------------------------------------------------- + +/// exe → liba, libb; both → libc. Cross-image function refs (GLOB_DAT / +/// JUMP_SLOT), a cross-image data ref (Abs64 + addend), one RELATIVE, +/// init/fini arrays populated via RELATIVE relocs, RELRO on every module. +fn diamond(world: &mut TestWorld, arch: &ArchRelocs) { + let (mut exe, mut liba, mut libb, mut libc) = ( + Fixture::new(arch.machine), + Fixture::new(arch.machine), + Fixture::new(arch.machine), + Fixture::new(arch.machine), + ); + + exe.entry = TEXT_VADDR; + exe.needed = vec!["liba".into(), "libb".into()]; + exe.syms = vec![ + Sym::import("a_fn"), + Sym::import("b_fn"), + Sym::import("c_data"), + ]; + exe.relas = vec![ + Rela { + offset: DATA_VADDR + 0x100, + rtype: arch.glob_dat, + sym: 1, + addend: 0, + }, + Rela { + offset: DATA_VADDR + 0x108, + rtype: arch.glob_dat, + sym: 2, + addend: 0, + }, + Rela { + offset: DATA_VADDR + 0x110, + rtype: arch.abs64, + sym: 3, + addend: 4, + }, + Rela { + offset: DATA_VADDR + 0x118, + rtype: arch.relative, + sym: 0, + addend: 0x123, + }, + // Init/fini entries become absolute through RELATIVE relocs. + Rela { + offset: DATA_VADDR + 0x400, + rtype: arch.relative, + sym: 0, + addend: (TEXT_VADDR + 0x10) as i64, + }, + Rela { + offset: DATA_VADDR + 0x408, + rtype: arch.relative, + sym: 0, + addend: (TEXT_VADDR + 0x20) as i64, + }, + Rela { + offset: DATA_VADDR + 0x440, + rtype: arch.relative, + sym: 0, + addend: (TEXT_VADDR + 0x30) as i64, + }, + ]; + exe.fini_count = 2; + exe.relas.push(Rela { + offset: DATA_VADDR + 0x448, + rtype: arch.relative, + sym: 0, + addend: (TEXT_VADDR + 0x38) as i64, + }); + exe.init_fini = true; + exe.relro = Some((DATA_VADDR, 0x1000)); + + liba.needed = vec!["libc".into()]; + liba.syms = vec![Sym::global("a_fn", TEXT_VADDR + 0x10), Sym::import("c_fn")]; + liba.plt_relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: arch.jump_slot, + sym: 2, + addend: 0, + }]; + liba.relas = vec![ + Rela { + offset: DATA_VADDR + 0x400, + rtype: arch.relative, + sym: 0, + addend: (TEXT_VADDR + 0x40) as i64, + }, + Rela { + offset: DATA_VADDR + 0x408, + rtype: arch.relative, + sym: 0, + addend: (TEXT_VADDR + 0x50) as i64, + }, + Rela { + offset: DATA_VADDR + 0x440, + rtype: arch.relative, + sym: 0, + addend: (TEXT_VADDR + 0x60) as i64, + }, + ]; + liba.init_fini = true; + liba.relro = Some((DATA_VADDR, 0x1000)); + + libb.needed = vec!["libc".into()]; + libb.syms = vec![ + Sym::global("b_fn", TEXT_VADDR + 0x20), + Sym::import("c_data"), + ]; + libb.relas = vec![ + Rela { + offset: DATA_VADDR + 0x100, + rtype: arch.glob_dat, + sym: 2, + addend: 0, + }, + Rela { + offset: DATA_VADDR + 0x400, + rtype: arch.relative, + sym: 0, + addend: (TEXT_VADDR + 0x44) as i64, + }, + Rela { + offset: DATA_VADDR + 0x408, + rtype: arch.relative, + sym: 0, + addend: (TEXT_VADDR + 0x54) as i64, + }, + Rela { + offset: DATA_VADDR + 0x440, + rtype: arch.relative, + sym: 0, + addend: (TEXT_VADDR + 0x64) as i64, + }, + ]; + libb.init_fini = true; + libb.relro = Some((DATA_VADDR, 0x1000)); + + libc.syms = vec![ + Sym::global("c_fn", TEXT_VADDR + 0x40), + Sym::global("c_data", DATA_VADDR + 0x300), + ]; + libc.relas = vec![ + Rela { + offset: DATA_VADDR + 0x400, + rtype: arch.relative, + sym: 0, + addend: (TEXT_VADDR + 0x70) as i64, + }, + Rela { + offset: DATA_VADDR + 0x408, + rtype: arch.relative, + sym: 0, + addend: (TEXT_VADDR + 0x80) as i64, + }, + Rela { + offset: DATA_VADDR + 0x440, + rtype: arch.relative, + sym: 0, + addend: (TEXT_VADDR + 0x90) as i64, + }, + ]; + libc.init_fini = true; + libc.relro = Some((DATA_VADDR, 0x1000)); + + match arch.machine { + EM_X86_64 => { + world.register::("exe", &exe); + world.register::("liba", &liba); + world.register::("libb", &libb); + world.register::("libc", &libc); + } + EM_AARCH64 => { + world.register::("exe", &exe); + world.register::("liba", &liba); + world.register::("libb", &libb); + world.register::("libc", &libc); + } + _ => { + world.register::("exe", &exe); + world.register::("liba", &liba); + world.register::("libb", &libb); + world.register::("libc", &libc); + } + } +} + +fn link_diamond(arch: &ArchRelocs) -> (TestWorld, LinkedProgram) { + let mut world = TestWorld::new(); + diamond(&mut world, arch); + let prog = link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .expect("the diamond links"); + (world, prog) +} + +// --------------------------------------------------------------------------- +// The diamond: dedup, placement, values, ordering, the passive page +// --------------------------------------------------------------------------- + +#[test] +fn diamond_links_once_per_name_and_resolves_across_modules() { + let (world, prog) = link_diamond::(&X86_RELOCS); + + // Dedup: four acquires, one per name — libc exactly once although + // two modules need it. + assert_eq!(world.acquires.len(), 4); + for nm in ["exe", "liba", "libb", "libc"] { + assert_eq!( + world.acquires.iter().filter(|n| **n == name(nm)).count(), + 1, + "{nm} acquired exactly once" + ); + } + + // BFS placement order: exe, liba, libb, libc. + let (be, ba, bb, bc) = (world.base(0), world.base(1), world.base(2), world.base(3)); + assert_eq!(prog.module_count, 4); + assert_eq!(prog.entry, be + TEXT_VADDR); + + // Cross-module GOT/data words, checked as actual bytes in the + // private copies (the fixtures' link vaddrs are hand-known). + assert_eq!( + world.word_at(be + DATA_VADDR + 0x100), + ba + TEXT_VADDR + 0x10, + "exe→a_fn" + ); + assert_eq!( + world.word_at(be + DATA_VADDR + 0x108), + bb + TEXT_VADDR + 0x20, + "exe→b_fn" + ); + assert_eq!( + world.word_at(be + DATA_VADDR + 0x110), + bc + DATA_VADDR + 0x300 + 4, + "exe→c_data+4" + ); + assert_eq!( + world.word_at(be + DATA_VADDR + 0x118), + be + 0x123, + "exe relative" + ); + assert_eq!( + world.word_at(ba + DATA_VADDR + 0x100), + bc + TEXT_VADDR + 0x40, + "liba→c_fn (plt)" + ); + assert_eq!( + world.word_at(bb + DATA_VADDR + 0x100), + bc + DATA_VADDR + 0x300, + "libb→c_data" + ); + + // Init order: dependency-before-dependent — libc, liba, libb, exe; + // fini is the exact reverse. + let init: Vec = prog.init.iter().copied().collect(); + assert_eq!( + init, + vec![ + bc + TEXT_VADDR + 0x70, + bc + TEXT_VADDR + 0x80, + ba + TEXT_VADDR + 0x40, + ba + TEXT_VADDR + 0x50, + bb + TEXT_VADDR + 0x44, + bb + TEXT_VADDR + 0x54, + be + TEXT_VADDR + 0x10, + be + TEXT_VADDR + 0x20, + ] + ); + let fini: Vec = prog.fini.iter().copied().collect(); + assert_eq!( + fini, + vec![ + be + TEXT_VADDR + 0x38, + be + TEXT_VADDR + 0x30, + bb + TEXT_VADDR + 0x64, + ba + TEXT_VADDR + 0x60, + bc + TEXT_VADDR + 0x90, + ] + ); + + // No TLS in this diamond: no TP was installed. + assert_eq!(prog.tp, 0); + assert!(!world.ops.iter().any(|op| matches!(op, Op::SetTp(_)))); +} + +#[test] +fn diamond_maps_wx_clean_and_seals_after_relocation() { + let (world, prog) = link_diamond::(&X86_RELOCS); + let be = world.base(0); + + // Every share request: text R|X, rodata R — never W (the world + // asserts too); private allocations never overlap a shared mapping. + let mut text_maps = 0; + for op in &world.ops { + if let Op::Map { access, .. } = op { + assert!(access & seg::ACCESS_W == 0); + assert!(*access == seg::ACCESS_R || *access == (seg::ACCESS_R | seg::ACCESS_X)); + if access & seg::ACCESS_X != 0 { + text_maps += 1; + } + } + } + assert_eq!(text_maps, 4, "one executable mapping per module"); + for &(ava, alen, _) in &world.allocs { + for op in &world.ops { + if let Op::Map { va, len, .. } = op { + assert!( + ava + alen as u64 <= *va || va + len <= ava, + "private memory never overlaps a shared mapping" + ); + } + } + } + + // Seal ordering: every RELRO seal comes after every module mapping + // and allocation (relocation runs strictly between), and by seal + // time the sealed window already holds the final GOT — checked by + // reading the exe's GOT word (inside its RELRO span) after link and + // knowing the world snapshots nothing: bytes are only written by + // the engine, which cannot write after its windows died. + let first_seal = world + .ops + .iter() + .position(|op| matches!(op, Op::Seal { .. })) + .expect("RELRO seals happened"); + let last_module_setup = world + .ops + .iter() + .rposition(|op| matches!(op, Op::Map { .. } | Op::Read { .. })) + .expect("mappings happened"); + assert!( + last_module_setup < first_seal, + "all module mapping precedes the first seal (map-RW → relocate → seal)" + ); + // Four RELRO seals (one per module, at base+DATA_VADDR) + the + // passive-page seal. + let seals: Vec<(u64, u64)> = world + .ops + .iter() + .filter_map(|op| match op { + Op::Seal { va, len } => Some((*va, *len)), + _ => None, + }) + .collect(); + assert_eq!(seals.len(), 5); + assert!( + seals.contains(&(be + DATA_VADDR, 0x1000)), + "the exe's RELRO window" + ); + assert_eq!( + *seals.last().unwrap(), + (prog.module_page, PAGE), + "the page seal is last" + ); +} + +#[test] +fn diamond_writes_the_passive_page() { + let (world, prog) = link_diamond::(&X86_RELOCS); + let (be, bc) = (world.base(0), world.base(3)); + + // Decode through the frozen wire type rather than duplicating its size + // or record stride in the test. + let mut bytes = [0u8; core::mem::size_of::()]; + world.bytes_at(prog.module_page, &mut bytes); + let page = ModulePage::read_from_bytes(&bytes).unwrap(); + assert_eq!(page.abi.get(), IMAGE_ABI, "IMAGE_ABI stamp"); + assert_eq!(page.count.get(), 4); + + let r0 = &page.records[0]; + assert_eq!(r0.name_hash.get(), name("exe").fnv1a()); + assert_eq!(r0.base.get(), be); + assert_eq!(r0.entry.get(), be + TEXT_VADDR); + assert_eq!(r0.seg_count, 3, "text + rodata + data recorded"); + assert_eq!(r0.kind, kore_abi::image::kind::EXECUTABLE); + // Segment 0 (text): placed vaddr, page-rounded size, R|X, provenance. + let s0 = &r0.segs[0]; + assert_eq!(s0.vaddr.get(), be + TEXT_VADDR); + assert_eq!(s0.size.get(), PAGE); + assert_ne!(s0.lease_id.get(), 0, "lease provenance rode along"); + assert_eq!(s0.kind, seg::KIND_TEXT); + assert_eq!(s0.access, seg::ACCESS_R | seg::ACCESS_X); + // The data record reports the installed private copy: R|W. + let s2 = &r0.segs[2]; + assert_eq!(s2.kind, seg::KIND_DATA); + assert_eq!(s2.access, seg::ACCESS_R | seg::ACCESS_W); + + let r3 = &page.records[3]; + assert_eq!(r3.name_hash.get(), name("libc").fnv1a()); + assert_eq!(r3.base.get(), bc); + assert_eq!(r3.entry.get(), 0, "libraries have no entry"); + assert_eq!(r3.kind, kore_abi::image::kind::LIBRARY); +} + +#[test] +fn diamond_links_on_aarch64_and_riscv64_tables() { + let (world, prog) = link_diamond::(&AARCH64_RELOCS); + let (be, ba, bc) = (world.base(0), world.base(1), world.base(3)); + assert_eq!(prog.module_count, 4); + assert_eq!( + world.word_at(be + DATA_VADDR + 0x100), + ba + TEXT_VADDR + 0x10 + ); + assert_eq!( + world.word_at(ba + DATA_VADDR + 0x100), + bc + TEXT_VADDR + 0x40 + ); + + let (world, prog) = link_diamond::(&RISCV_RELOCS); + let (be, ba, bc) = (world.base(0), world.base(1), world.base(3)); + assert_eq!(prog.module_count, 4); + assert_eq!( + world.word_at(be + DATA_VADDR + 0x100), + ba + TEXT_VADDR + 0x10 + ); + assert_eq!( + world.word_at(ba + DATA_VADDR + 0x100), + bc + TEXT_VADDR + 0x40 + ); +} + +// --------------------------------------------------------------------------- +// TLS: block construction + TP per variant (hand-derived psABI numbers) +// --------------------------------------------------------------------------- + +/// exe (TLS 0x18 align 8, init 8 bytes) → libtls (TLS 0x10 align 16, +/// init 8 bytes, exporting `ctls` at block offset 8). The exe reads its +/// own block (sym-0 + addend 8) and the library's (`ctls` import). +fn tls_pair(world: &mut TestWorld, arch: &ArchRelocs) { + let mut exe = Fixture::new(arch.machine); + exe.entry = TEXT_VADDR; + exe.needed = vec!["libtls".into()]; + exe.tls = Some(TlsSpec { + memsz: 0x18, + align: 8, + filesz: 8, + }); + exe.syms = vec![Sym::import("ctls")]; + exe.relas = vec![ + Rela { + offset: DATA_VADDR + 0x120, + rtype: arch.tprel, + sym: 0, + addend: 8, + }, + Rela { + offset: DATA_VADDR + 0x128, + rtype: arch.tprel, + sym: 1, + addend: 0, + }, + ]; + let mut exe_img = exe.build(); + exe_img[TLS_VADDR as usize..TLS_VADDR as usize + 8] + .copy_from_slice(&[0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7]); + + let mut lib = Fixture::new(arch.machine); + lib.tls = Some(TlsSpec { + memsz: 0x10, + align: 16, + filesz: 8, + }); + lib.syms = vec![Sym { + name: "ctls".into(), + value: 8, + size: 8, + binding: STB_GLOBAL, + shndx: 1, + }]; + let mut lib_img = lib.build(); + lib_img[TLS_VADDR as usize..TLS_VADDR as usize + 8] + .copy_from_slice(&[0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7]); + + match arch.machine { + EM_X86_64 => { + world.register_bytes::("exe", exe_img); + world.register_bytes::("libtls", lib_img); + } + EM_AARCH64 => { + world.register_bytes::("exe", exe_img); + world.register_bytes::("libtls", lib_img); + } + _ => { + world.register_bytes::("exe", exe_img); + world.register_bytes::("libtls", lib_img); + } + } +} + +#[test] +fn tls_block_v2_x86_64() { + // Variant 2: blocks below tp. exe off = −align_up(0x18, 8) = −0x18; + // lib off = −align_up(0x18 + 0x10, 16) = −0x30; total 0x30, align 16. + // The window is align_up(0x30, 16) plus the runtime block reserved at + // tp (whose first eight bytes are the psABI self-pointer); tp sits + // 0x30 above its base, and the blocks below it are unaffected by the + // reserve. + let mut world = TestWorld::new(); + tls_pair(&mut world, &X86_RELOCS); + let prog = + link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT).unwrap(); + + let be = world.base(0); + // Base call order: exe, libtls, the TLS block, the passive page. + let tls_base = world.base(2); + assert_eq!(prog.tp, tls_base + 0x30); + assert_eq!( + prog.tp % 16, + 0, + "tp satisfies the strictest block alignment" + ); + assert_eq!(prog.tls_total, 0x30); + assert_eq!(prog.tls_align, 16); + assert!(world.ops.contains(&Op::SetTp(prog.tp))); + assert!(world.ops.contains(&Op::Alloc { + va: tls_base, + len: 0x30 + kore_abi::image::RUNTIME_TCB_BYTES + })); + + // Slots hold tp-relative offsets: own var −0x18 + 8; ctls −0x30 + 8. + assert_eq!(world.word_at(be + DATA_VADDR + 0x120), (-0x10i64) as u64); + assert_eq!(world.word_at(be + DATA_VADDR + 0x128), (-0x28i64) as u64); + + // Init images landed at their layout slots; %fs:0 = tp itself. + let mut got = [0u8; 8]; + world.bytes_at(prog.tp - 0x18, &mut got); + assert_eq!(got, [0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7]); + world.bytes_at(prog.tp - 0x30, &mut got); + assert_eq!(got, [0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7]); + assert_eq!(world.word_at(prog.tp), prog.tp, "the TCB self-pointer"); +} + +#[test] +fn tls_block_v1_aarch64() { + // Variant 1: the header reserved at tp is RUNTIME_TCB_BYTES (the + // runtime's per-thread block lives there), so the exe block sits at + // align_up(reserve, 8) = reserve, end reserve+0x18; lib at + // align_up(reserve+0x18, 16) = reserve+0x20, end reserve+0x30; + // total reserve+0x30; tp = base. + let reserve = kore_abi::image::RUNTIME_TCB_BYTES; + let mut world = TestWorld::new(); + tls_pair(&mut world, &AARCH64_RELOCS); + let prog = link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .unwrap(); + + let be = world.base(0); + let tls_base = world.base(2); + assert_eq!(prog.tp, tls_base); + assert_eq!(prog.tls_total, reserve + 0x30); + assert!(world.ops.contains(&Op::Alloc { + va: tls_base, + len: reserve + 0x30 + })); + assert_eq!(world.word_at(be + DATA_VADDR + 0x120), reserve + 8); + assert_eq!(world.word_at(be + DATA_VADDR + 0x128), reserve + 0x20 + 8); + let mut got = [0u8; 8]; + world.bytes_at(prog.tp + reserve, &mut got); + assert_eq!(got, [0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7]); + world.bytes_at(prog.tp + reserve + 0x20, &mut got); + assert_eq!(got, [0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7]); + // The reserve stays zeroed for the runtime to stamp (no DTV here). + world.bytes_at(prog.tp, &mut got); + assert_eq!(got, [0; 8]); +} + +#[test] +fn tls_block_v1_riscv64() { + // Variant 1: riscv64's psABI reserves nothing at tp, but the runtime + // block does, so the header is RUNTIME_TCB_BYTES here too — exe block + // at the reserve, end +0x18; lib at align_up(+0x18, 16) = +0x20, end + // +0x30; total reserve+0x30; tp = base. + let reserve = kore_abi::image::RUNTIME_TCB_BYTES; + let mut world = TestWorld::new(); + tls_pair(&mut world, &RISCV_RELOCS); + let prog = link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .unwrap(); + + let be = world.base(0); + assert_eq!(prog.tp, world.base(2)); + assert_eq!(prog.tls_total, reserve + 0x30); + assert_eq!(world.word_at(be + DATA_VADDR + 0x120), reserve + 8); + assert_eq!(world.word_at(be + DATA_VADDR + 0x128), reserve + 0x20 + 8); + let mut got = [0u8; 8]; + world.bytes_at(prog.tp + reserve, &mut got); + assert_eq!(got, [0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7]); +} + +// --------------------------------------------------------------------------- +// Refusals +// --------------------------------------------------------------------------- + +#[test] +fn unknown_needed_name_fails_closed() { + let mut world = TestWorld::new(); + let mut exe = Fixture::new(EM_X86_64); + exe.entry = TEXT_VADDR; + exe.needed = vec!["libghost".into()]; + world.register::("exe", &exe); + assert_eq!( + link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .unwrap_err(), + LinkError::World(WErr::Unknown) + ); +} + +#[test] +fn dependency_cycle_refuses() { + let mut world = TestWorld::new(); + let mut exe = Fixture::new(EM_X86_64); + exe.entry = TEXT_VADDR; + exe.needed = vec!["liba".into()]; + let mut liba = Fixture::new(EM_X86_64); + liba.needed = vec!["libb".into()]; + let mut libb = Fixture::new(EM_X86_64); + libb.needed = vec!["liba".into()]; + world.register::("exe", &exe); + world.register::("liba", &liba); + world.register::("libb", &libb); + assert_eq!( + link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .unwrap_err(), + LinkError::DependencyCycle + ); + // Dedup still held: three acquires, no loop. + assert_eq!(world.acquires.len(), 3); +} + +#[test] +fn overbound_graph_refuses() { + // A chain one longer than the table: exe → c00 → … → c15. + let mut world = TestWorld::new(); + let mut exe = Fixture::new(EM_X86_64); + exe.entry = TEXT_VADDR; + exe.needed = vec!["c00".into()]; + world.register::("exe", &exe); + for i in 0..MAX_LINK_MODULES { + let mut lib = Fixture::new(EM_X86_64); + if i + 1 < MAX_LINK_MODULES { + lib.needed = vec![format!("c{:02}", i + 1)]; + } + world.register::(&format!("c{i:02}"), &lib); + } + assert_eq!( + link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .unwrap_err(), + LinkError::TooManyModules + ); +} + +#[test] +fn kind_mismatch_refuses() { + // A DT_NEEDED name resolving to an *executable* image is a foreign + // graph; so is linking a library as the program. + let mut world = TestWorld::new(); + let mut exe = Fixture::new(EM_X86_64); + exe.entry = TEXT_VADDR; + exe.needed = vec!["liba".into()]; + let mut fake = Fixture::new(EM_X86_64); + fake.entry = TEXT_VADDR; // an exe registered under a library's name + world.register::("exe", &exe); + world.register::("liba", &fake); + assert_eq!( + link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .unwrap_err(), + LinkError::BadModule(1) + ); + + let mut world = TestWorld::new(); + let lib = Fixture::new(EM_X86_64); + world.register::("lib", &lib); + assert_eq!( + link_program::(&mut world, &name("lib"), arena(), LinkCaps::DEFAULT) + .unwrap_err(), + LinkError::BadModule(0) + ); +} + +#[test] +fn missing_lease_refuses() { + let mut world = TestWorld::new(); + let mut exe = Fixture::new(EM_X86_64); + exe.entry = TEXT_VADDR; + world.register::("exe", &exe); + world.withhold = Some((name("exe"), 0)); + assert_eq!( + link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .unwrap_err(), + LinkError::MissingLease(0) + ); +} + +#[test] +fn unresolved_strong_symbol_refuses_the_link() { + let mut world = TestWorld::new(); + let mut exe = Fixture::new(EM_X86_64); + exe.entry = TEXT_VADDR; + exe.syms = vec![Sym::import("ghost")]; + exe.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: X86_RELOCS.glob_dat, + sym: 1, + addend: 0, + }]; + world.register::("exe", &exe); + assert!(matches!( + link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .unwrap_err(), + LinkError::Apply(kore_ld::apply::ApplyError::MissingSymbol(_), 0) + )); +} + +#[test] +fn single_static_module_is_first_class() { + // The degenerate no-DT_NEEDED case: one module, one record, entry + // rebased, no TLS, no TP. + let mut world = TestWorld::new(); + let mut exe = Fixture::new(EM_X86_64); + exe.entry = TEXT_VADDR + 0x8; + exe.relas = vec![Rela { + offset: DATA_VADDR + 0x100, + rtype: X86_RELOCS.relative, + sym: 0, + addend: 1, + }]; + world.register::("exe", &exe); + let prog = + link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT).unwrap(); + assert_eq!(prog.module_count, 1); + assert_eq!(prog.entry, world.base(0) + TEXT_VADDR + 0x8); + assert_eq!( + world.word_at(world.base(0) + DATA_VADDR + 0x100), + world.base(0) + 1 + ); + assert_eq!(prog.tp, 0); + assert!(prog.init.is_empty()); +} + +#[test] +fn initializer_in_writable_data_refuses_before_finalization() { + let mut world = TestWorld::new(); + let mut exe = Fixture::new(EM_X86_64); + exe.entry = TEXT_VADDR; + exe.init_fini = true; + exe.relas = vec![Rela { + offset: DATA_VADDR + 0x400, + rtype: X86_RELOCS.relative, + sym: 0, + addend: (DATA_VADDR + 0x100) as i64, + }]; + world.register::("exe", &exe); + assert!(matches!( + link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT), + Err(LinkError::Function( + kore_ld::functions::FunctionError::Target(0), + 0 + )) + )); + assert!( + !world + .ops + .iter() + .any(|op| matches!(op, Op::Seal { .. } | Op::SetTp(_))) + ); +} + +/// **The absolute case places itself.** +/// +/// `kind::STATIC_EXECUTABLE` is every founded row and the linker stub: +/// its link address IS its runtime address, so the engine must not ask +/// the world where to put it. Getting this wrong is not a refusal — a +/// world answers a perfectly legitimate address, the segments map, the +/// seal succeeds, and the image sits somewhere its own relocationless +/// code does not believe it is. Nothing downstream distinguishes that +/// from a correct placement, which is why the assertion is on the +/// *absence* of the call rather than on the resulting base. +#[test] +fn an_absolute_module_places_itself_without_asking_the_world() { + let mut world = TestWorld::new(); + let mut exe = Fixture::new(EM_X86_64); + // A real fixed-address image, through the real gate — no relocations + // (a static executable has none by construction) and no world knob: + // `check_image` produces this descriptor, so the test exercises the + // path the founder and the image service take. + exe.e_type = ET_EXEC; + exe.entry = TEXT_VADDR + 0x8; + world.register::("exe", &exe); + + let prog = link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .expect("an absolute executable links"); + + assert_eq!(prog.module_count, 1); + // The entry rides through unchanged — an address, not an offset. + assert_eq!(prog.entry, TEXT_VADDR + 0x8); + // Exactly one placement was requested: the passive module page, + // which is genuinely the world's to choose. Two would mean the + // module's base came from the world. + assert_eq!( + world.base_calls(), + 1, + "only the passive page is placed, never the module" + ); + // ...and the text landed at the descriptor's own address. + assert!( + world.mapped_at(TEXT_VADDR), + "text maps at its link address, not at a chosen base" + ); +} + +/// The same descriptor is still refused where it cannot belong: an +/// absolute executable reached through `DT_NEEDED` is a foreign graph. +/// The role admits two *executable* conventions, not two roles. +#[test] +fn an_absolute_module_is_still_refused_in_a_library_slot() { + let mut world = TestWorld::new(); + let mut exe = Fixture::new(EM_X86_64); + exe.entry = TEXT_VADDR; + exe.needed = vec!["dep".into()]; + let mut dep = Fixture::new(EM_X86_64); + dep.e_type = ET_EXEC; + dep.entry = TEXT_VADDR; + world.register::("exe", &exe); + world.register::("dep", &dep); + + assert_eq!( + link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .unwrap_err(), + LinkError::BadModule(1), + "a DT_NEEDED name admits only a library" + ); +} + +// --------------------------------------------------------------------------- +// relocate_self (the P7 crt0 seam) +// --------------------------------------------------------------------------- + +fn rela_entry(offset: u64, rtype: u32, sym: u32, addend: i64) -> [u8; 24] { + let mut e = [0u8; 24]; + e[0..8].copy_from_slice(&offset.to_le_bytes()); + e[8..16].copy_from_slice(&((u64::from(sym) << 32) | u64::from(rtype)).to_le_bytes()); + e[16..24].copy_from_slice(&addend.to_le_bytes()); + e +} + +#[test] +fn relocate_self_applies_relative_entries() { + let base = 0x7f00_0000u64; + let mut table = Vec::new(); + table.extend_from_slice(&rela_entry(0x10, 8, 0, 0x123)); + table.extend_from_slice(&rela_entry(0x20, 8, 0, -0x8)); + let mut image = [0u8; 0x40]; + assert_eq!(relocate_self::(base, &table, &mut image), Ok(2)); + assert_eq!(read64(&image, 0x10), base + 0x123); + assert_eq!(read64(&image, 0x20), base - 8); + // Untouched words stay zero. + assert_eq!(read64(&image, 0x18), 0); + + // The aarch64 and riscv64 tables drive the same engine. + let t = rela_entry(0x8, 1027, 0, 0x40).to_vec(); + let mut image = [0u8; 0x20]; + assert_eq!(relocate_self::(base, &t, &mut image), Ok(1)); + assert_eq!(read64(&image, 0x8), base + 0x40); + let t = rela_entry(0x8, 3, 0, 0x40).to_vec(); + let mut image = [0u8; 0x20]; + assert_eq!(relocate_self::(base, &t, &mut image), Ok(1)); + assert_eq!(read64(&image, 0x8), base + 0x40); +} + +#[test] +fn relocate_self_refuses_everything_else() { + let mut image = [0u8; 0x40]; + // A non-RELATIVE type (GLOB_DAT) refuses before any write. + let t = rela_entry(0x10, 6, 0, 0).to_vec(); + assert_eq!( + relocate_self::(0, &t, &mut image), + Err(SelfRelocError::Reloc(6, 0)) + ); + // A RELATIVE naming a symbol is malformed. + let t = rela_entry(0x10, 8, 1, 0).to_vec(); + assert_eq!( + relocate_self::(0, &t, &mut image), + Err(SelfRelocError::Reloc(8, 0)) + ); + // A ragged table refuses whole. + assert_eq!( + relocate_self::(0, &[0u8; 23], &mut image), + Err(SelfRelocError::Shape) + ); + // An escaping offset refuses; nothing before it survives partially — + // the caller treats any error as fatal (the stub cannot start). + let t = rela_entry(0x39, 8, 0, 0).to_vec(); + assert_eq!( + relocate_self::(0, &t, &mut image), + Err(SelfRelocError::Bounds(0)) + ); + // base + addend overflow refuses. + let t = rela_entry(0x10, 8, 0, -1).to_vec(); + assert_eq!( + relocate_self::(0, &t, &mut image), + Err(SelfRelocError::Overflow(0)) + ); + assert_eq!(read64(&image, 0x10), 0, "refusals wrote nothing"); +} + +// --------------------------------------------------------------------------- +// The passive page's home is the world's to name (the stub names the +// LINK window's last page so a late loader can read it back). +// --------------------------------------------------------------------------- + +#[test] +fn the_passive_page_lands_where_the_world_names_it() { + let mut world = TestWorld::new(); + world.page_at = Some(0x5fff_f000); + diamond(&mut world, &X86_RELOCS); + let prog = link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .expect("the diamond links"); + assert_eq!(prog.module_page, 0x5fff_f000); + // No module was placed on top of it. + for op in &world.ops { + if let Op::Base { va, len, .. } = op { + if *va != 0x5fff_f000 { + assert!(va + len <= 0x5fff_f000 || *va > 0x5fff_f000); + } + } + } +} + +#[test] +fn retained_tls_template_is_pristine_for_all_architectures() { + for machine in [EM_X86_64, EM_AARCH64, EM_RISCV] { + let mut world = TestWorld::new(); + match machine { + EM_X86_64 => { + tls_pair(&mut world, &X86_RELOCS); + link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .unwrap(); + } + EM_AARCH64 => { + tls_pair(&mut world, &AARCH64_RELOCS); + link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .unwrap(); + } + _ => { + tls_pair(&mut world, &RISCV_RELOCS); + link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .unwrap(); + } + } + let (bytes, tp, align) = world.template.unwrap(); + assert!(align.is_power_of_two()); + assert!( + bytes[tp as usize..tp as usize + kore_abi::image::RUNTIME_TCB_BYTES as usize] + .iter() + .all(|b| *b == 0) + ); + assert!(bytes.iter().any(|b| *b != 0)); + } +} + +#[test] +fn pure_tbss_allocates_zeroed_tls_without_a_source_lease_on_every_architecture() { + for machine in [EM_X86_64, EM_AARCH64, EM_RISCV] { + let mut world = TestWorld::new(); + let mut image = Fixture::new(machine); + image.entry = TEXT_VADDR; + image.tls = Some(TlsSpec { + memsz: 0x101a, + filesz: 0, + align: 8, + }); + match machine { + EM_X86_64 => { + world.register::("exe", &image); + link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .unwrap(); + } + EM_AARCH64 => { + world.register::("exe", &image); + link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .unwrap(); + } + _ => { + world.register::("exe", &image); + link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .unwrap(); + } + } + let (bytes, _, _) = world.template.unwrap(); + assert!(bytes.len() >= 0x101a); + assert!(bytes.iter().all(|byte| *byte == 0)); + } +} + +// --------------------------------------------------------------------------- +// The built images, through the engine the startup stub actually runs +// --------------------------------------------------------------------------- + +/// The startup stub's caller arena, to the byte. +/// +/// `user/runtime/ld-stub` links with a 4 KiB stack-local arena, not this +/// suite's generous 64 KiB one. A fixture that links here and refuses there +/// is the failure this whole test exists to catch *before* a QEMU run, so +/// the capacity has to match the stub rather than the suite. +fn stub_arena() -> &'static kore_ld::store::LinkArena<'static> { + let buf = Box::leak(Box::new([core::mem::MaybeUninit::::uninit(); 4096])); + Box::leak(Box::new(kore_ld::store::LinkArena::new(buf))) +} + +/// The caps `user/runtime/ld-stub` passes: a program is itself plus at most +/// one library, with a single init array. +const STUB_CAPS: LinkCaps = LinkCaps { + max_modules: 2, + max_inits: 1, +}; + +/// **Every built executable must link under the stub's own caps.** +/// +/// `built_images.rs` asserts the *regime* — segment shape, flags, relocation +/// classes. It does not link, so an image can satisfy every rule it checks +/// and still be refused by `link_program` at boot. That refusal is close to +/// invisible on metal: `refuse_link` keeps the structured diagnostic on the +/// readiness-NACK path, so an exec-spawned foreground program exits with a +/// bare `ExitCode::LINK_FAILED` (`-1`) and the typed `LinkError` is dropped. +/// +/// This closes that gap on the host, where the error survives. +/// +/// `KORE_USER_IMAGES` unset ⇒ skip, so `make test` stays hermetic. +#[test] +fn built_executables_link_under_the_stub_caps() { + let Some(root) = std::env::var_os("KORE_USER_IMAGES") else { + return; + }; + let dir = std::path::Path::new(&root) + .join("x86_64-unknown-kore-user") + .join("release"); + if !dir.is_dir() { + return; + } + + // Collect every image present — libraries too, since a `DT_NEEDED` + // consumer cannot link without the library it names. + let mut images: Vec<(String, Vec)> = Vec::new(); + let mut executables = Vec::new(); + let mut entries: Vec<_> = std::fs::read_dir(&dir) + .expect("image dir is readable") + .filter_map(Result::ok) + .collect(); + entries.sort_by_key(std::fs::DirEntry::file_name); + for entry in entries { + let path = entry.path(); + if !path.is_file() { + continue; + } + let Some(stem) = path.file_name().and_then(|s| s.to_str()) else { + continue; + }; + // Cargo leaves `.d` files and build dirs beside the images. + if stem.contains('.') && !stem.ends_with(".so") { + continue; + } + let Ok(bytes) = std::fs::read(&path) else { + continue; + }; + if check_image::(&bytes).is_err() || ImageName::new(stem).is_none() { + continue; + } + if !stem.ends_with(".so") { + executables.push(stem.to_string()); + } + images.push((stem.to_string(), bytes)); + } + + // `built_images.rs`'s rule: a set root that selected nothing is a broken + // harness, not a pass. Silent green here would hide exactly the class of + // failure this test exists to catch. + assert!( + !executables.is_empty(), + "KORE_USER_IMAGES={} selected no linkable executables", + dir.display() + ); + println!("linking {} built executables: {}", executables.len(), executables.join(", ")); + + // A fresh world per link: the engine consumes leases, so a world that + // already linked one program is not a clean stand-in for the founding of + // the next. + let mut refused = Vec::new(); + for exe in &executables { + let mut world = TestWorld::new(); + for (stem, bytes) in &images { + world.register_bytes::(stem, bytes.clone()); + } + if let Err(error) = + link_program::(&mut world, &name(exe), stub_arena(), STUB_CAPS) + { + refused.push(format!("{exe}: {error:?}")); + } + } + + assert!( + refused.is_empty(), + "these built images link on the host but would exit LINK_FAILED (-1) on \ + metal with no diagnostic:\n {}", + refused.join("\n ") + ); +} + +// --------------------------------------------------------------------------- +// The placement high water stamped into the passive page +// --------------------------------------------------------------------------- + +/// A world that reports where its placement ended gets that mark stamped +/// into the page, as pages below the page — and it reads back as the +/// address a late placer must start above. +#[test] +fn a_reported_placement_cursor_is_stamped_below_the_page() { + let mut world = TestWorld::new(); + diamond(&mut world, &X86_RELOCS); + let page_va = 0x5fff_f000; + world.page_at = Some(page_va); + world.placed_high_water = Some(page_va - 3 * PAGE); + let prog = link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .expect("links"); + assert_eq!(prog.module_page, page_va); + + let mut bytes = [0u8; core::mem::size_of::()]; + world.bytes_at(prog.module_page, &mut bytes); + let page = ModulePage::read_from_bytes(&bytes).unwrap(); + assert_eq!(page.placed.get(), 3); + assert_eq!(page.placed_high_water(prog.module_page), Some(page_va - 3 * PAGE)); +} + +/// A world that does not record placement stamps nothing, and the page +/// says *unrecorded* rather than answering an address. +#[test] +fn a_world_without_placement_stamps_unrecorded() { + let (world, prog) = link_diamond::(&X86_RELOCS); + let mut bytes = [0u8; core::mem::size_of::()]; + world.bytes_at(prog.module_page, &mut bytes); + let page = ModulePage::read_from_bytes(&bytes).unwrap(); + assert_eq!(page.placed.get(), 0); + assert_eq!(page.placed_high_water(prog.module_page), None); +} + +/// A cursor the engine cannot represent — above the page, or off a page +/// boundary — refuses the link. Stamping a floor a later placer would +/// trust is the one thing worse than not stamping. +#[test] +fn an_unrepresentable_placement_cursor_refuses_the_link() { + let page_va = 0x5fff_f000; + for bad in [page_va + PAGE, page_va - PAGE / 2] { + let mut world = TestWorld::new(); + diamond(&mut world, &X86_RELOCS); + world.page_at = Some(page_va); + world.placed_high_water = Some(bad); + let err = link_program::(&mut world, &name("exe"), arena(), LinkCaps::DEFAULT) + .expect_err("a lying placement cursor refuses"); + assert!(matches!(err, LinkError::Layout(_)), "{err:?}"); + } +} diff --git a/native/crates/ld/tests/shipping_list.rs b/native/crates/ld/tests/shipping_list.rs new file mode 100644 index 0000000..10dbf43 --- /dev/null +++ b/native/crates/ld/tests/shipping_list.rs @@ -0,0 +1,403 @@ +//! Every service crate must say how it reaches metal — or say that it doesn't. +//! +//! A `services/*` crate is a workspace member, so `make check-*` compiles it +//! and `make test` runs its unit tests. Neither asks the question that +//! actually matters: **does anything found it?** A service that is superseded, +//! or built ahead of its wiring, therefore keeps every gate green while being +//! unreachable on the metal target — it looks alive because it compiles. +//! +//! The declaration already exists: a founded service carries +//! `services//boot.toml`, and `res/boot.toml` names the image the boot +//! chain measures and starts. What was missing is anything requiring a crate +//! to be in that set or to admit it is not. This test is that requirement, +//! so an unshipped service is a written-down fact with a reason rather than +//! a silence nobody can see. +//! +//! Adding a service crate therefore means one of two things: +//! +//! - it ships — give it `services//boot.toml` and list its image in +//! `res/boot.toml`; or +//! - it does not ship yet — add it to [`UNSHIPPED`] with the reason, which is +//! a line in a review rather than a discovery six months later. +//! +//! It lives beside `built_images`, which validates the shipped images +//! themselves, because this validates the *list* of them — and because the +//! kernel package is `#![no_main]`, so an integration test cannot live at the +//! repo root where the tree it reads begins. + +use std::collections::BTreeSet; +use std::path::{Path, PathBuf}; + +/// Service crates that deliberately do not appear in the boot set, each with +/// the reason it is out. +/// +/// This is a *declaration*, not an exemption: an entry here is a claim that +/// the crate's absence from boot is intended, and the reason is what a +/// reviewer checks. Entries should leave — by being wired up or by being +/// deleted — rather than accumulate. +const UNSHIPPED: &[(&str, &str)] = &[ + ( + "display", + "backend crate: owns the physical display claim and aperture; linked \ + into user/services/compositor, whose founded image is kore-svc-compositor", + ), + // Reached on demand instead of at boot: the broker and its parsers are + // packed into the image archive and founded per job by the worker + // supervisor, so they are shipped without being in the boot set. + ("extract", "archive-resident; founded per job by the broker"), + ( + "extract-worker-c", + "archive-resident; founded per job by the broker", + ), + ( + "extract-worker-rust", + "archive-resident; founded per job by the broker", + ), + // Genuinely unreachable today. Recorded here so the state is visible in + // review instead of being inferred from a missing file. + ( + "output", + "superseded: kore-svc-compositor serves OutReqWire/OutChunk; \ + nothing founds kore-svc-output", + ), + ( + "accel-ivpu", + "backend crate: the Lunar Lake NPU `Accelerator` impl linked into \ + user/services/accel; the founded image is kore-svc-accel (services/accel), \ + not a boot row of its own", + ), + ( + "accel-spmd", + "backend crate: the CPU-SIMD `Accelerator` impl over kore-rt's SPMD \ + region launch, the third tenant crates/abi/src/svc.rs declares. No boot \ + row yet — its compute step (`SpmdBackend::run_pending`) needs a serve \ + loop to drive it, and that lands with the row", + ), + ( + "swap-nvme", + "not in USER_SERVICES and not founded; \ + res/x86_64/kore-svc-swap-nvme.elf is a stale committed artifact", + ), +]; + +fn repo_root() -> PathBuf { + // `crates/ld` → the tree. Resolved from the manifest dir rather than the + // cwd so the test does not depend on where cargo was invoked. + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("crates/ld sits two levels below the repo root") + .to_path_buf() +} + +/// Every directory under `services/` that is a crate. +fn service_crates(root: &Path) -> BTreeSet { + let mut found = BTreeSet::new(); + let dir = root.join("services"); + for entry in std::fs::read_dir(&dir).expect("services/ is readable") { + let entry = entry.expect("a readable services/ entry"); + if !entry.file_type().expect("a file type").is_dir() { + continue; + } + if !entry.path().join("Cargo.toml").is_file() { + continue; + } + found.insert(entry.file_name().to_string_lossy().into_owned()); + } + assert!(!found.is_empty(), "no service crates found under services/"); + found +} + +/// Services carrying the per-service boot manifest — the founded set as the +/// crates themselves declare it. +fn declares_boot_manifest(root: &Path, name: &str) -> bool { + root.join("services").join(name).join("boot.toml").is_file() +} + +/// Whether `res/boot.toml` names this service's image, which is what the boot +/// chain measures and starts. +fn named_in_boot_manifest(boot_toml: &str, name: &str) -> bool { + boot_toml.contains(&format!("\"kore-svc-{name}\"")) +} + +/// Service-shaped committed artifacts that intentionally are not founded. +/// Keep this narrower than [`UNSHIPPED`]: most backend/library crates have no +/// standalone ELF at all. An entry here is a temporary packaging exception +/// whose stale bytes would otherwise look like a bootable service. +const NON_FOUNDED_SERVICE_ARTIFACTS: &[(&str, &str)] = &[( + "kore-svc-swap-nvme", + "legacy claimant image retained while its standalone founder row is unwired", +)]; + +fn founded_image_names(boot_toml: &str) -> BTreeSet { + boot_toml + .lines() + .map(|line| line.trim().trim_end_matches(',')) + .filter_map(|line| line.strip_prefix('"')?.strip_suffix('"')) + .filter(|name| name.starts_with("kore-svc-")) + .map(str::to_owned) + .collect() +} + +/// Whether a founded image's fragment founds on `arch`: an `arch = [...]` +/// line lists the ISAs, and an undeclared one means every ISA (the manifest +/// vocabulary in res/boot.toml's header). Read from the fragment beside the +/// crate, the same file `tools/boot_manifest.py --images-for` reads. +fn founds_on(root: &Path, image: &str, arch: &str) -> bool { + let name = image.strip_prefix("kore-svc-").unwrap_or(image); + let Ok(fragment) = std::fs::read_to_string(root.join("services").join(name).join("boot.toml")) + else { + return true; + }; + let Some(list) = fragment + .lines() + .map(str::trim) + .filter(|line| !line.starts_with('#')) + .find_map(|line| line.strip_prefix("arch")?.trim_start().strip_prefix('=')) + else { + return true; + }; + list.split('"') + .skip(1) + .step_by(2) + .any(|target| target == arch) +} + +fn service_artifacts(root: &Path, arch: &str) -> BTreeSet { + let mut found = BTreeSet::new(); + let dir = root.join("res").join(arch); + for entry in std::fs::read_dir(&dir) + .unwrap_or_else(|error| panic!("{} is unreadable: {error}", dir.display())) + { + let entry = entry.expect("a readable service artifact entry"); + if !entry.file_type().expect("an artifact file type").is_file() { + continue; + } + let name = entry.file_name(); + let name = name.to_string_lossy(); + let Some(stem) = name + .strip_prefix("kore-svc-") + .and_then(|stem| stem.strip_suffix(".elf")) + else { + continue; + }; + found.insert(format!("kore-svc-{stem}")); + } + found +} + +#[test] +fn every_service_crate_is_founded_or_declared_unshipped() { + let root = repo_root(); + let unshipped: BTreeSet<&str> = UNSHIPPED.iter().map(|(n, _)| *n).collect(); + + let mut unaccounted = Vec::new(); + for name in service_crates(&root) { + let founded = declares_boot_manifest(&root, &name); + if founded || unshipped.contains(name.as_str()) { + continue; + } + unaccounted.push(name); + } + + assert!( + unaccounted.is_empty(), + "these service crates are neither founded nor declared unshipped: {unaccounted:?}\n\ + Give each one `services//boot.toml` plus an image in res/boot.toml, \ + or add it to UNSHIPPED in this test with the reason it stays out." + ); +} + +/// The two halves of "founded" must agree: a per-service manifest without an +/// image in `res/boot.toml` builds a service the boot chain never starts. +#[test] +fn a_services_boot_manifest_matches_the_boot_chains_image_list() { + let root = repo_root(); + let boot_toml = std::fs::read_to_string(root.join("res/boot.toml")).expect("res/boot.toml"); + + let mut disagreed = Vec::new(); + for name in service_crates(&root) { + let declares = declares_boot_manifest(&root, &name); + let named = named_in_boot_manifest(&boot_toml, &name); + if declares != named { + disagreed.push((name, declares, named)); + } + } + + assert!( + disagreed.is_empty(), + "service boot.toml and res/boot.toml disagree (name, has services//boot.toml, \ + named in res/boot.toml): {disagreed:?}" + ); +} + +/// Each architecture's committed service-shaped ELF set must match the +/// founded image list **for that architecture**: a row whose fragment +/// declares `arch = [...]` founds only there, so its artifact is expected on +/// those ISAs and is a stray everywhere else. This catches renamed services +/// whose old binary remains in `res/`, new rows whose measured artifact +/// was never added, and a silicon-bound row's image left behind on an ISA +/// that never founds it. +#[test] +fn committed_service_artifacts_match_the_founded_set() { + let root = repo_root(); + let boot_toml = std::fs::read_to_string(root.join("res/boot.toml")).expect("res/boot.toml"); + let every_arch = founded_image_names(&boot_toml); + let allowed: BTreeSet<&str> = NON_FOUNDED_SERVICE_ARTIFACTS + .iter() + .map(|(name, _)| *name) + .collect(); + + for arch in ["x86_64", "aarch64"] { + let founded: BTreeSet = every_arch + .iter() + .filter(|image| founds_on(&root, image, arch)) + .cloned() + .collect(); + assert!( + founded.len() + 2 >= every_arch.len(), + "res/{arch}: the arch filter dropped more rows than the manifest gates — \ + the fragment parse is wrong, not the manifest" + ); + let actual = service_artifacts(&root, arch); + let missing: Vec<&String> = founded.difference(&actual).collect(); + let stray: Vec<&String> = actual + .difference(&founded) + .filter(|name| !allowed.contains(name.as_str())) + .collect(); + assert!( + missing.is_empty() && stray.is_empty(), + "res/{arch} service artifacts disagree with the founded set: \ + missing={missing:?}, stray={stray:?}. Remove renamed/stale ELFs or \ + add the founded row and its measured artifact." + ); + } + + for (name, reason) in NON_FOUNDED_SERVICE_ARTIFACTS { + assert!( + !reason.trim().is_empty(), + "artifact exception `{name}` has no reason" + ); + assert!( + !every_arch.contains(*name), + "artifact exception `{name}` is founded now; remove the exception" + ); + } +} + +/// The arch column parses the way the generator reads it: the two Intel +/// accelerator rows found on x86_64 only, every other row everywhere. +#[test] +fn the_arch_column_gates_exactly_the_silicon_bound_rows() { + let root = repo_root(); + assert!(founds_on(&root, "kore-svc-accel", "x86_64")); + assert!(!founds_on(&root, "kore-svc-accel", "aarch64")); + assert!(!founds_on(&root, "kore-svc-accel-xe", "aarch64")); + assert!(founds_on(&root, "kore-svc-mm", "aarch64")); + assert!(founds_on(&root, "kore-svc-device", "riscv64")); +} + +/// Modules the bootloader carries that are **not** service rows, so the +/// cross-check below expects them beyond `res/boot.toml`'s `services` list. +/// +/// The linker stub is deliberately absent: `iso-boot-x86` appends its +/// `module_string` at ISO time rather than declaring it in the checked-in +/// config, so a copy here would be loaded twice. +const NON_ROW_MODULES: &[&str] = &["kore-image-archive"]; + +/// Every founded row's image must be a module the **bootloader actually +/// carries**, and every service module it carries must be a founded row. +/// +/// This is the third question in "how does this reach metal?", and the one +/// nothing was asking. `res/boot.toml` says which rows found; `USER_SERVICES` stages +/// their ELFs into the ISO; but Limine only loads what `boot/limine.conf` +/// names, and the founder resolves a row to its module **by name** +/// (`boot_modules().find(|m| m.cmdline() == name)`). A row present in the +/// manifest and absent from the config compiles, tests, measures and stages +/// perfectly — and then the machine boots DEGRADED with +/// ` module not shipped by the bootloader`, which is a QEMU round to +/// discover. The frame row cost exactly that. +#[test] +fn every_founded_row_is_carried_by_the_bootloader() { + let root = repo_root(); + let boot_toml = std::fs::read_to_string(root.join("res/boot.toml")).expect("res/boot.toml"); + let limine = std::fs::read_to_string(root.join("boot/limine.conf")).expect("boot/limine.conf"); + + // What the bootloader is told to load. + let carried: BTreeSet<&str> = limine + .lines() + .filter_map(|line| line.trim().strip_prefix("module_string:")) + .map(str::trim) + .collect(); + assert!( + !carried.is_empty(), + "boot/limine.conf declares no modules at all — the parse is wrong, not the config" + ); + + // What the boot chain founds. Read from the `services` list rather than + // the crate tree: it is the founder's own ordered source of truth, and it + // is what the module names must match. + let founded: BTreeSet<&str> = boot_toml + .lines() + .map(|line| line.trim().trim_end_matches(',')) + .filter_map(|line| line.strip_prefix('"')?.strip_suffix('"')) + .filter(|name| name.starts_with("kore-svc-")) + .collect(); + assert!( + founded.len() > 5, + "res/boot.toml yielded only {} founded rows — the parse is wrong, not the manifest", + founded.len() + ); + + let missing: Vec<&&str> = founded.iter().filter(|n| !carried.contains(*n)).collect(); + assert!( + missing.is_empty(), + "these rows are founded by res/boot.toml but no bootloader module carries them: \ + {missing:?}\nAdd a `module_path` / `module_string` pair to boot/limine.conf — \ + without it the row founds FAILED and the boot goes DEGRADED." + ); + + let stray: Vec<&&str> = carried + .iter() + .filter(|n| n.starts_with("kore-svc-")) + .filter(|n| !founded.contains(*n)) + .collect(); + assert!( + stray.is_empty(), + "boot/limine.conf carries service modules that res/boot.toml does not found: \ + {stray:?}\nDrop them, or add the row to the `services` list." + ); + + for extra in carried.iter().filter(|n| !n.starts_with("kore-svc-")) { + assert!( + NON_ROW_MODULES.contains(extra), + "boot/limine.conf carries `{extra}`, which is neither a founded row nor a \ + declared non-row module — add it to NON_ROW_MODULES with its reason" + ); + } +} + +/// An `UNSHIPPED` entry that no longer names a service crate is stale — the +/// crate was wired up or deleted and the declaration outlived it. +#[test] +fn no_unshipped_entry_outlives_its_crate() { + let root = repo_root(); + let crates = service_crates(&root); + + for (name, reason) in UNSHIPPED { + assert!( + crates.contains(*name), + "UNSHIPPED names `{name}` ({reason}) but services/{name} does not exist — \ + drop the entry" + ); + assert!( + !declares_boot_manifest(&root, name), + "`{name}` is declared UNSHIPPED but carries services/{name}/boot.toml — \ + it is founded now, so drop the entry" + ); + assert!( + !reason.trim().is_empty(), + "UNSHIPPED entry `{name}` has no reason" + ); + } +} diff --git a/native/crates/ld/tests/static_boot_images.rs b/native/crates/ld/tests/static_boot_images.rs new file mode 100644 index 0000000..81b0e3d --- /dev/null +++ b/native/crates/ld/tests/static_boot_images.rs @@ -0,0 +1,54 @@ +//! Validate the exact static images a boot target is about to ship. +//! +//! `KORE_STATIC_BOOT_ARCH` selects the existing relocation contract used by +//! the loader. It defaults to `x86_64` for older callers and accepts +//! `aarch64` for the corresponding production-media gate. + +use std::path::{Path, PathBuf}; + +use kore_abi::image::seg; +use kore_ld::check::check_image; +use kore_ld::reloc::{AArch64, RelocArch, X86_64}; + +fn check_configured_images(images: &str) { + let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("kore-ld must live under /crates/ld") + .to_owned(); + for path in images.split(':').filter(|path| !path.is_empty()) { + let resolved = workspace.join(path); + let bytes = std::fs::read(&resolved) + .unwrap_or_else(|err| panic!("{}: read failed: {err}", Path::new(path).display())); + let image = check_image::(&bytes).unwrap_or_else(|err| { + panic!( + "{}: startup linker refusal: {err:?}", + Path::new(path).display() + ) + }); + assert!( + image + .descriptor + .live_segs() + .iter() + .filter(|s| s.kind != seg::KIND_TLS) + .all(|s| s.vaddr.get() % 4096 == 0 && s.memsz.get() % 4096 == 0), + "{}: Register emitted a non-page-granular mapping", + Path::new(path).display(), + ); + } +} + +#[test] +fn configured_static_boot_images_are_loader_compatible() { + let Ok(images) = std::env::var("KORE_STATIC_BOOT_IMAGES") else { + return; + }; + match std::env::var("KORE_STATIC_BOOT_ARCH").as_deref() { + Ok("aarch64") => check_configured_images::(&images), + Ok("x86_64") | Err(_) => check_configured_images::(&images), + Ok(other) => { + panic!("KORE_STATIC_BOOT_ARCH={other:?} is unsupported; expected x86_64 or aarch64") + } + } +} diff --git a/native/provenance.json b/native/provenance.json index e3d94b1..5ca6863 100644 --- a/native/provenance.json +++ b/native/provenance.json @@ -8,6 +8,8 @@ "crates/dagfs": "kore-dagfs", "crates/driver": "kore-driver", "crates/hint": "kore-hint", + "crates/late": "kore-late", + "crates/ld": "kore-ld", "crates/ring": "kore-ring", "crates/rt": "kore-rt", "crates/rt-macros": "kore-rt-macros", @@ -19,7 +21,7 @@ "services/extract-worker-rust": "kore-svc-extract-worker-rust", "services/output": "kore-svc-output" }, - "workspace_sha256": "0c679968940112a7baab2a2a697dc02369880a341804ac9fc766a3c6c2093efc", + "workspace_sha256": "f66a2093652e6dfdcd4d04c3558d8447385d3608730af28fb642931a4ff4a395", "files": { "LICENSE": "c9b2b12d3bad9080090798a442b4b2a77c48312ae75089f041281b7c8ad21738", "crates/abi/Cargo.toml": "f9a4811b4b6d06947a47a763354cad3bc739a5ea387194a4dcf22409351f3c7b", @@ -125,6 +127,30 @@ "crates/hint/src/lib.rs": "bebbb8dc72e2765485bfd55996f0b8d9d863e34b0c26fd6b1f84da422583d998", "crates/hint/src/riscv64.rs": "5af5308b7241a52402a05c638f04a95457f257f6f41d1e766449562934dd4872", "crates/hint/src/x86_64.rs": "9f0e4316f70023277b016b9e6e812a3da0e021f3f8cbbe9ef3c3ccb660e4d85a", + "crates/late/Cargo.toml": "3d49205399539aeced9240f2902b3be715ee5baa28d1ecde826fb42515b95e83", + "crates/late/src/lib.rs": "4ede5f17bb0872c57fcec6bb00c5c1aa4efdb0e65074ced38fd97c244ca3b7d9", + "crates/late/src/metal.rs": "0aec71e435af4a5c7c58f2b9efa6689bb6c4f3ec46cebfaa5d54298259f28e69", + "crates/ld/Cargo.toml": "bf8a6b091682174695d21d9082243086989b03b79fb38df3cd16a7e8a8d8bd92", + "crates/ld/src/apply.rs": "8b6a8217bd38b1bb2d900bb386c7d90845dc2ce4cfb18ded19ff486cf39a327b", + "crates/ld/src/check.rs": "c28cddf7980dfb6d6ff3d8ca74d1255f9dd75db5791028bd58ce096f2bb945cf", + "crates/ld/src/functions.rs": "cb1b1bd0f6218383d69e6287ca1096963729c03345b468be581dc82db9c79064", + "crates/ld/src/late.rs": "c1a4812101294bedf78e575c6d27fa389884fbacbd7dc55871914a0c24f49ca4", + "crates/ld/src/lib.rs": "c8b510448ab682116cb1ca3248cff1266c7f1bd24450fefe4f650b21aa6eac49", + "crates/ld/src/link.rs": "b193fbd296d0ed84237387682095c9b640c05619a68c74b993aceb62f86d5713", + "crates/ld/src/parse.rs": "b1cc85112cf1a6274acd7a9bd14d15380e6dd2c347ddd82063cb7425f76cc75a", + "crates/ld/src/place.rs": "c9260c6b0e1af861aaa7ea82107fa36350e5020fd39f0312469db0326a0ac32e", + "crates/ld/src/reloc.rs": "d3ef8b6f7deba2bab0f9b89eca554b96cd6d10d0522ccb09fe9cb909d26e6b4a", + "crates/ld/src/stage.rs": "569ba1bd846cb5a6fbcedcebd075169523fe176c05e0a5714fbce210c0e18877", + "crates/ld/src/store.rs": "ba5a432a2a3e34e64cdd8f02dccc2c69bab5719b6a4223393c72beca0c7a2b18", + "crates/ld/src/stub.rs": "b670715026a889f5baaf233a35d3d24c1891223a0e57665782ee326d90e49f5e", + "crates/ld/tests/apply_engine.rs": "e01f5f805f34d614717ceb297275d65936c1b48989df90079fcd2f8ec83b6b1c", + "crates/ld/tests/built_images.rs": "9c6a75df826a85cd0e08a2bb4aec7181e3ddf967bc116c8aee562ee3bff31586", + "crates/ld/tests/check_gate.rs": "0ead133777f18f929d5e9b0353a289a5b4fa9e2af929155f525524096ee5578c", + "crates/ld/tests/common/mod.rs": "1e4bdfa37511848d9e9458d9a550a1ff2410154411c6d026e9106035044ea438", + "crates/ld/tests/late_layout.rs": "9bfc460d8036d69c1acdc52c430ee475e25062519cdc7dd30f235b882414a339", + "crates/ld/tests/link_engine.rs": "1bb5320f1fb4596cc0c2d133290f3a4af0542597a8aa8b805a4f91148674877d", + "crates/ld/tests/shipping_list.rs": "113bf634f5a8f83103e266149c2c8daf834f3c99b86d7e3e0693f520283a1469", + "crates/ld/tests/static_boot_images.rs": "25fb47a0a8e5b11dca5b8c31f64b90e8d6a6edc3c4d81b0fb5e1733afea3ebe3", "crates/ring/Cargo.toml": "b0a6e8d0f919494ef9e2f8c229ff64e24709347ca76c3d7f5a6e413387c20bcb", "crates/ring/src/event.rs": "c33884f4f8b651bcd28368ea580cf18888e986645d35bc1aeea1957e5f704d14", "crates/ring/src/lib.rs": "ae9ea82c43efb01c0c3f2c04a90540d64fead78bfef7072499bf97a76b95396c", From 1be3c2717481dcf60c55adf53c079512fd4d96d7 Mon Sep 17 00:00:00 2001 From: Julian Avar Date: Tue, 29 Sep 2026 11:26:03 -0400 Subject: [PATCH 02/13] lua: the fs surface under a host C smoke, and no Zig on a host build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `kore-lua`'s build script queried Zig before anything else, so every host `cargo test`/`cargo miri test` of the workspace failed on a machine with no Zig — including CI's `host` and `miri` jobs, which install no Zig at all. The engine exists only where `src/lib.rs` compiles its C half (`target_os = "none"`), so a host build has no object to link and no reason to need Zig: return before the toolchain query when the target is not Kore's. The C smoke moves ahead of that early return and becomes skippable: it needs only a host C compiler, and a consumer with neither toolchain must still be able to build this workspace. A compiler that is present but fails is still fatal. `tests/host_fs.c` gives the smoke an in-memory store, so the whole `io`/`os`/`kore.fs` surface — `stat` included — runs without a machine. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 17 +- crates/lua/KORE-VENDOR.md | 49 +++ crates/lua/build.rs | 129 +++++--- crates/lua/src/bridge.rs | 25 +- crates/lua/src/files.rs | 88 +++--- crates/lua/src/kore_lua.c | 367 ++++++++++++++++++----- crates/lua/src/lib.rs | 26 +- crates/lua/src/vm.rs | 70 ++++- crates/lua/tests/host_fs.c | 549 ++++++++++++++++++++++++++++++++++ crates/lua/tests/host_smoke.c | 520 +++++++++++++++++++++++++++++++- docs/filesystem.md | 7 +- docs/languages.md | 9 +- 12 files changed, 1670 insertions(+), 186 deletions(-) create mode 100644 crates/lua/tests/host_fs.c diff --git a/README.md b/README.md index 31bf65f..c4bb494 100644 --- a/README.md +++ b/README.md @@ -91,13 +91,22 @@ a local Kore tree, follow [local integration](https://github.com/MicroPerceptron | Rust | Working foundation with native ownership types | | C | Generated filesystem, time, random, task and joined worker APIs | | Zig | Target-specific C translation and generated module namespaces | -| Lua / Python | Planned handwritten ownership/scheduling adapters | +| Lua | Lua 5.5 on a worker thread, with `io`/`os` over the task's own filesystem | +| Python | Planned handwritten ownership/scheduling adapter | Filesystem support remains a requirement for **every initial language surface**. A clocks-only foreign binding would not satisfy that requirement. The C/Zig owner uses scoped generational handles and bounded copied buffers; -callbacks return to the native executor for each request. Standalone foreign -entry/CRT, allocator adapters and the bounded Lua driver experiment interface -remain follow-on work. See [the language contract](https://github.com/MicroPerceptron/libkore/blob/alpha/docs/languages.md) for ownership, cancellation and validation boundaries. +callbacks return to the native executor for each request. The `kore-lua` package +carries the pinned Lua 5.5 engine, its freestanding host and the `kore.*` +standard library a script sees; `io` and `os` are written over the task's own +filesystem rather than vendored, because both of Lua's are libc over POSIX and +this target has neither. `os.execute` and `io.popen` are absent, and arrive +through `Extension` like `kore.accel`: both launch a process, which is a service +face the embedding program holds. See +[`crates/lua/KORE-VENDOR.md`](crates/lua/KORE-VENDOR.md) for the provenance, the +selected libraries and every place the manual leaves a detail to the system. +Standalone foreign entry/CRT, allocator adapters and the bounded Lua driver +experiment interface remain follow-on work. See [the language contract](https://github.com/MicroPerceptron/libkore/blob/alpha/docs/languages.md) for ownership, cancellation and validation boundaries. The previous sketches and address/collection experiments remain in Git history at `8a6381c`; their parallel runtime, allocator placeholder and empty language diff --git a/crates/lua/KORE-VENDOR.md b/crates/lua/KORE-VENDOR.md index 387ff2e..9255590 100644 --- a/crates/lua/KORE-VENDOR.md +++ b/crates/lua/KORE-VENDOR.md @@ -36,6 +36,13 @@ along with all seven `file:` methods: - `os.clock`, `os.date`, `os.difftime`, `os.exit`, `os.getenv`, `os.remove`, `os.rename`, `os.setlocale`, `os.time`, `os.tmpname` +The file object carries a `closed` method beside the manual's seven, and +`kore.fs` adds the verbs the stock surface has no spelling for — +`read_file`, `write_file`, `mkdir`, `resize`, `stat`, `exists`, `list`. +Those are additions on kore's own surface, never changes to a stock member: +a file `kore.fs.open` answers is the same object `io.open` answers, and its +seven methods render failure the same way on both. + Where the manual leaves a detail to the system, this build chooses and says so: @@ -57,6 +64,14 @@ so: - `os.getenv` answers `nil` for every name: a kore task has no environment, authority arrives as capabilities in its launch, and an unset variable is exactly what `nil` means. +- `os.date` and `os.time` answer for years 0000 through 9999 and refuse + anything outside that with the manual's own + `date result cannot be represented in this installation`. The civil + conversion is exact for every day the proleptic Gregorian calendar + defines, but the broken-down year is an `int` and `%Y` spells four + digits, so the range is stated rather than left to truncate a huge year + into a plausible-looking date. The bound is computed from the same + `days_from_civil` the conversion uses, so the two cannot disagree. - `io.stdin` is a real file object that is always at end of file, the conforming shape for a stream with nothing behind it. `io.stdin`, `io.stdout` and `io.stderr` refuse `:close()` with @@ -111,3 +126,37 @@ python3 crates/lua/tests/run_setjmp_targets.py That probe crosses a deeper stack frame and checks both a nonzero long-jump value and C's required `longjmp(..., 0) -> 1` normalization. + +## What the build tests + +`build.rs` compiles and runs `tests/host_smoke.c` on every build that has a +host C compiler, and `make cross` therefore runs it for all three targets. +It is skipped, with a `cargo:warning`, where no C compiler is present: an +ordinary `cargo check`/`cargo test` of this workspace must not require a C +or Zig toolchain, and the Zig steps that build the freestanding object run +only for `target_os = "none"`. + +The harness has two passes over the same engine: + +- The **selftest** runs with `src/kore_lua.c`'s weak host stubs, which is + what a build with no Rust host links. It covers the engine, the + calendar, the number formatting, the selected and omitted library + members, and the shapes every `io`/`os` member answers with nothing + behind them. +- The **filesystem suite** runs against `tests/host_fs.c`, an in-memory + store behind the *real* host transport, and covers `io`, `os` and + `kore.fs` end to end: open modes including append and update, seeks, + every read format, multi-page reads, `lines` and its to-be-closed + variable, the default input and output, directories and listings, + `resize`, `os.remove`/`rename`/`tmpname`, `io.tmpfile`'s self-removal, + `os.exit`'s recorded code, the `__gc` backstop, and both failure + spellings. + +Two properties of that store are deliberate and load-bearing. Its reply is +**length-delimited with no terminator and a poisoned byte after it**, which +is the bridge's actual contract (`src/bridge.rs` answers a Rust `Vec`), +so anything that reaches for `strlen` over a reply fails a test rather than +passing by luck. And it injects faults — a refused read after *n* reads, a +refused close, a refused sleep — which is the only way to reach the paths +where a failure lands after a partial read, with `luaL_Buffer` already +grown onto the stack as a box. diff --git a/crates/lua/build.rs b/crates/lua/build.rs index e3d0372..9208316 100644 --- a/crates/lua/build.rs +++ b/crates/lua/build.rs @@ -48,46 +48,27 @@ fn zig_command(zig: &OsStr, out: &Path) -> Command { command } -fn main() { - println!("cargo:rerun-if-env-changed=KORE_ZIG"); - println!("cargo:rerun-if-env-changed=KORE_HOST_CC"); - println!("cargo:rerun-if-changed=src/numeric.rs"); - println!("cargo:rerun-if-changed=src/kore_lua.c"); - println!("cargo:rerun-if-changed=tests/host_smoke.c"); - println!("cargo:rerun-if-changed=tests/target_shim.rs"); - println!("cargo:rerun-if-changed=vendor/lua"); - println!("cargo:rerun-if-changed=vendor/musl-setjmp"); - - let out = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR")); - fs::create_dir_all(&out).expect("create Lua build output"); - let arch = env::var("CARGO_CFG_TARGET_ARCH").expect("target architecture"); - let c_target = match arch.as_str() { - "x86_64" => "x86_64-linux-musl", - "aarch64" => "aarch64-linux-musl", - "riscv64" => "riscv64-linux-musl", - other => panic!("Lua has no kore C ABI mapping for {other}"), - }; - let zig = env::var_os("KORE_ZIG").unwrap_or_else(|| "zig".into()); - let zig_version = Command::new(&zig) - .arg("version") - .output() - .expect("could not query the supported Zig toolchain"); - assert!(zig_version.status.success(), "`zig version` failed"); - let zig_version = std::str::from_utf8(&zig_version.stdout) - .expect("`zig version` returned non-UTF-8 output") - .trim(); - assert!( - zig_version_supported(zig_version), - "Lua requires Zig {SUPPORTED_ZIG_MAJOR}.{SUPPORTED_ZIG_MINOR}.x; found {zig_version}. \ - The target C build separately verifies the musl jmp_buf ABI used by the vendored assembly" - ); - let object = out.join("kore_lua.o"); - let archive = out.join("libkore_lua.a"); +/// Whether a C compiler answers at all. +/// +/// Deliberately `--version` and not a probe compile: the question is +/// "is there a toolchain here", and a compiler that exists but cannot +/// build the engine must fail loudly rather than be skipped. +fn compiler_present(cc: &OsStr) -> bool { + Command::new(cc) + .arg("--version") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .is_ok_and(|status| status.success()) +} - // Exercise the same amalgamation through a native protected-call cycle - // on every clean build. This catches C-ABI/longjmp regressions before a - // boot image can merely prove that the freestanding object linked. - let host = env::var("HOST").expect("host triple"); +/// Build and run the C smoke tests with a host C compiler. +/// +/// Answers `false` when no C compiler is available, which is a skip and +/// not a failure: an ordinary host `cargo check`/`cargo test` of this +/// workspace must not require a C toolchain. A compiler that is present +/// but fails still panics — a broken smoke is a real regression. +fn host_smoke(out: &Path, host: &str) -> bool { let host_cc = env::var_os("KORE_HOST_CC").unwrap_or_else(|| { if host.contains("apple-darwin") { "/usr/bin/clang".into() @@ -95,6 +76,9 @@ fn main() { "cc".into() } }); + if !compiler_present(&host_cc) { + return false; + } let host_smoke = out.join("lua-host-smoke"); let mut smoke_cc = Command::new(&host_cc); smoke_cc @@ -173,6 +157,73 @@ fn main() { "native Lua target-shim smoke execution", ); + true +} + +fn main() { + println!("cargo:rerun-if-env-changed=KORE_ZIG"); + println!("cargo:rerun-if-env-changed=KORE_HOST_CC"); + println!("cargo:rerun-if-changed=src/numeric.rs"); + println!("cargo:rerun-if-changed=src/kore_lua.c"); + println!("cargo:rerun-if-changed=tests/host_smoke.c"); + println!("cargo:rerun-if-changed=tests/host_fs.c"); + println!("cargo:rerun-if-changed=tests/target_shim.rs"); + println!("cargo:rerun-if-changed=vendor/lua"); + println!("cargo:rerun-if-changed=vendor/musl-setjmp"); + + let out = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR")); + fs::create_dir_all(&out).expect("create Lua build output"); + + // The C smoke tests need only a host C compiler, which is worth having + // on every build: they run the whole engine and the whole `io`/`os`/ + // `kore.fs` surface against the in-memory store in `tests/host_fs.c`, + // and they are the only place that happens without a machine. + // + // They are skipped rather than fatal when no C compiler is present: + // `cargo check`/`cargo test` of this workspace must work for a + // consumer who has neither toolchain, which is what the README + // promises and AGENTS.md requires. `make cross` builds for the Kore + // targets and therefore always runs them. + let host = env::var("HOST").expect("host triple"); + if !host_smoke(&out, &host) { + println!( + "cargo:warning=kore-lua: no host C compiler; skipped the engine smoke tests. \ + Set KORE_HOST_CC to run them." + ); + } + + // The engine itself exists only where `src/lib.rs` compiles its Rust + // half: the amalgamation, the worker thread and the doorbell are all + // `target_os = "none"` surfaces. A host build carries `files`' logic + // and none of the C, so there is no object for it to link and no + // reason to require Zig. + if env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() != "none" { + return; + } + + let arch = env::var("CARGO_CFG_TARGET_ARCH").expect("target architecture"); + let c_target = match arch.as_str() { + "x86_64" => "x86_64-linux-musl", + "aarch64" => "aarch64-linux-musl", + "riscv64" => "riscv64-linux-musl", + other => panic!("Lua has no kore C ABI mapping for {other}"), + }; + let zig = env::var_os("KORE_ZIG").unwrap_or_else(|| "zig".into()); + let zig_version = Command::new(&zig) + .arg("version") + .output() + .expect("could not query the supported Zig toolchain"); + assert!(zig_version.status.success(), "`zig version` failed"); + let zig_version = std::str::from_utf8(&zig_version.stdout) + .expect("`zig version` returned non-UTF-8 output") + .trim(); + assert!( + zig_version_supported(zig_version), + "Lua requires Zig {SUPPORTED_ZIG_MAJOR}.{SUPPORTED_ZIG_MINOR}.x; found {zig_version}. \ + The target C build separately verifies the musl jmp_buf ABI used by the vendored assembly" + ); + let object = out.join("kore_lua.o"); + let archive = out.join("libkore_lua.a"); let mut cc = zig_command(&zig, &out); cc.arg("cc") // Zig's musl target supplies ISO C headers only. The resulting object diff --git a/crates/lua/src/bridge.rs b/crates/lua/src/bridge.rs index 2d5bda5..1c32957 100644 --- a/crates/lua/src/bridge.rs +++ b/crates/lua/src/bridge.rs @@ -72,7 +72,7 @@ use kore::runtime::{ }; use kore_late::{LoadError, Loader, Search}; -use crate::vm::{ReplResult, Vm}; +use crate::vm::{EvalResult, ReplResult, Vm}; use crate::{Answer, Extension, OP_EMBEDDER_BASE}; /// The bound on a source module (`.lua`). Sized like a generous @@ -162,6 +162,12 @@ use core::ffi::c_void; pub enum Outcome { Ok, Failed, + /// The chunk ran `os.exit`. The sentinel it raised unwound every + /// `` handler on the way out, so this is a completed evaluation + /// and not a failure; the code is in + /// [`Services::exit_request`]. A batch run leaves with it and a REPL + /// stops reading. + Exited, /// REPL only: the chunk needs a continuation line. Incomplete, /// REPL only: the expression buffer could not be allocated. @@ -316,16 +322,20 @@ fn run_eval(vm: &mut Vm) { KIND_REPL => match vm.eval_repl(script) { ReplResult::Complete(true) => Outcome::Ok, ReplResult::Complete(false) => Outcome::Failed, + ReplResult::Exited => Outcome::Exited, ReplResult::Incomplete => Outcome::Incomplete, ReplResult::ResourceRefused => Outcome::ResourceRefused, }, _ => { + // SAFETY: thread 0 posts a non-empty `&'static [u8]` chunk + // name with every KIND_CHUNK evaluation, so this is never the + // dangling-null case `from_raw_parts` forbids. let name: &'static [u8] = unsafe { core::slice::from_raw_parts(cell.chunk_name, cell.chunk_name_len) }; - if vm.eval(script, name) { - Outcome::Ok - } else { - Outcome::Failed + match vm.eval(script, name) { + EvalResult::Ok => Outcome::Ok, + EvalResult::Failed => Outcome::Failed, + EvalResult::Exited => Outcome::Exited, } } }; @@ -706,8 +716,9 @@ impl<'r, 't> Services<'r, 't> { /// The exit code a script asked for with `os.exit`, if it did. /// - /// The embedder checks this after each evaluation: a REPL stops reading - /// and a batch run leaves with it. + /// The embedder checks this after each evaluation, which + /// [`Outcome::Exited`] tells it to do: a REPL stops reading and a batch + /// run leaves with it. pub fn exit_request(&self) -> Option { self.exit_request } diff --git a/crates/lua/src/files.rs b/crates/lua/src/files.rs index 10ecbfe..327d2c3 100644 --- a/crates/lua/src/files.rs +++ b/crates/lua/src/files.rs @@ -1,12 +1,18 @@ //! `kore.fs`, and everything `io` and `os` are spellings of. //! -//! The filesystem session lives on thread 0 and a [`View`] is affine, so -//! Lua never holds one. It holds a *handle* — an index and a generation -//! into the [`Files`] table below — and every verb here resolves that -//! handle back to the view before touching the store. A handle whose slot -//! was recycled names a generation that no longer matches, so a use after -//! close is [`FsError::InvalidHandle`] rather than a read of somebody -//! else's file. +//! `kore.fs`'s thread-0 half: the open-file table, the positional verbs, +//! and the error vocabulary. Public because the vocabulary is part of the +//! contract — an embedder whose own library can fail a filesystem failure +//! renders it with [`code`] rather than spelling one of its own. +//! +//! The filesystem session lives on thread 0 and a [`View`] +//! is affine, so Lua never holds one. It holds a *handle* — an index and a +//! generation into the [`Files`] table below — and every verb here +//! resolves that handle back to the view before touching the store. A +//! handle whose slot was recycled names a generation that no longer +//! matches, so a use after close is +//! [`FsError::InvalidHandle`] rather +//! than a read of somebody else's file. //! //! Every verb here is *positional*. DagFS addresses bytes absolutely //! (`read_at` / `write_at`), and a stream position is a POSIX idea that @@ -16,13 +22,14 @@ //! two authorities for one number, and they would drift the first time a //! buffered read looked further ahead than the script had consumed. //! -//! Every verb answers in the three shapes [`Answer`] names, and an expected -//! failure — a missing file, a denied right — is a `Refused` carrying the -//! kebab-case spelling of the [`FsError`] that caused it, never a raise. +//! Every verb answers in the three shapes [`Answer`] names, +//! and an expected failure — a missing file, a denied right — is a +//! `Refused` carrying the kebab-case spelling of the +//! [`FsError`] that caused it, never a raise. //! One vocabulary, two spellings: `kore.fs` renders it as a table and `io` //! as a message and an errno (`res/SYSTEM_LIBRARY.md`, the six rules). -use alloc::{string::String, vec, vec::Vec}; +use alloc::{string::String, vec::Vec}; use kore::fs::{Fs, FsError, Name, Refusal, Rights, View, dag::ObjectKind}; @@ -92,30 +99,38 @@ impl Files { } } - fn insert(&mut self, view: View, readable: bool, writable: bool, append: bool) -> Option { + /// Take the view into a free slot and answer its handle. + /// + /// `Err` hands the view **back**, because it is affine: the store still + /// believes the handle is open, and a refusal that dropped it here + /// would retire it locally and leak it remotely. The caller owes it a + /// `close` either way, which is the whole reason this cannot answer a + /// bare `Option`. + fn insert( + &mut self, + view: View, + readable: bool, + writable: bool, + append: bool, + ) -> Result { + let index = match self.slots.iter().position(Option::is_none) { + Some(index) => index, + None if self.slots.len() < MAX_OPEN_FILES => { + self.slots.push(None); + self.slots.len() - 1 + } + None => return Err(view), + }; let generation = self.next_generation; - let file = OpenFile { + self.slots[index] = Some(OpenFile { view, generation, readable, writable, append, - }; - let index = match self.slots.iter().position(Option::is_none) { - Some(index) => { - self.slots[index] = Some(file); - index - } - None => { - if self.slots.len() >= MAX_OPEN_FILES { - return None; - } - self.slots.push(Some(file)); - self.slots.len() - 1 - } - }; + }); self.next_generation = self.next_generation.wrapping_add(1); - Some(encode(index, generation)) + Ok(encode(index, generation)) } /// Take the file out of the table, leaving the slot free. @@ -300,7 +315,7 @@ pub async fn open(fs: &mut Fs<'_, '_>, files: &mut Files, path: &[u8], flags: u6 // The length rides back beside the handle: an append stream starts // its position at the end, and the object in C would otherwise // have to stat immediately just to learn where it is. - Some(handle) => Answer::Value( + Ok(handle) => Answer::Value( handle, if flags & mode::TRUNCATE != 0 { 0 @@ -308,12 +323,10 @@ pub async fn open(fs: &mut Fs<'_, '_>, files: &mut Files, path: &[u8], flags: u6 length }, ), - None => { + Err(view) => { // The table is full. Give the store its handle back rather // than holding one the VM can no longer name. - if let Ok(view) = files.take(u64::MAX) { - fs.close(view).await; - } + fs.close(view).await; refused(FsError::OutOfHandles) } } @@ -373,7 +386,14 @@ pub async fn read( return refused(FsError::AccessDenied); } let count = (count as usize).min(MAX_READ_BYTES); - let mut buffer = vec![0u8; count]; + // `try_reserve`, not `vec![0; count]`: the size is a script's argument + // and the refusal belongs to the script, not to the task. An abort + // here would take thread 0 with it. + let mut buffer = Vec::new(); + if buffer.try_reserve_exact(count).is_err() { + return refused(FsError::OutOfStorage); + } + buffer.resize(count, 0); let mut filled = 0; while filled < count { match fs diff --git a/crates/lua/src/kore_lua.c b/crates/lua/src/kore_lua.c index 14d88a2..08300c4 100644 --- a/crates/lua/src/kore_lua.c +++ b/crates/lua/src/kore_lua.c @@ -294,7 +294,8 @@ enum { KORE_LUA_OP_FS_WRITE = 21, /* 22 was a host-side seek. The stream position lives in the file object below, so there is nothing for the host to seek; the number is left - unused rather than recycled, because src/bridge.rs must agree. */ KORE_LUA_OP_FS_SYNC = 23, + unused rather than recycled, because src/bridge.rs must agree. */ + KORE_LUA_OP_FS_SYNC = 23, KORE_LUA_OP_FS_RESIZE = 24, KORE_LUA_OP_FS_STAT = 25, KORE_LUA_OP_FS_REMOVE = 26, @@ -304,9 +305,11 @@ enum { the evaluation has unwound. */ KORE_LUA_OP_PROC_EXIT = 29 }; -/* The error `os.exit` raises to unwind the evaluation. The host matches it - exactly and reports the recorded code instead of an error; anything else - is a genuine error and is reported as one. */ +/* The error `os.exit` raises to unwind the evaluation. `luaL_error` puts + the chunk and line in front of it, so the host matches this as a *suffix* + and reports the recorded code instead of an error; anything else is a + genuine error and is reported as one. src/vm.rs holds the other half of + this spelling, as src/bridge.rs holds the other half of the op numbers. */ #define KORE_LUA_EXIT_SENTINEL "kore:exit" /* The open-mode bits src/files.rs reads out of arg0. */ enum { @@ -318,10 +321,6 @@ enum { KORE_LUA_O_EXCLUSIVE = 1 << 5, KORE_LUA_O_DIRECTORY = 1 << 6 }; -/* "wherever this handle's cursor is" — src/files.rs `FROM_CURSOR`. Not a - reachable file offset, so one word tells a stream read from a positional - one and `f:read` and `kore.fs.read_at` share an arm. */ -#define KORE_LUA_FROM_CURSOR 0xffffffffffffffffull enum { KORE_LUA_STATUS_OK = 0, KORE_LUA_STATUS_REFUSED = 1, @@ -361,11 +360,19 @@ __attribute__((weak)) int kore_lua_host_call(unsigned op, */ static int kore_lua_fail(lua_State *L, int status, const char *text, size_t text_len) { lua_pushnil(L); + if (text == NULL) { + text = "unavailable"; + text_len = sizeof "unavailable" - 1; + } if (status == KORE_LUA_STATUS_REFUSED) { - lua_pushfstring(L, "refused: %s", text != NULL ? text : "?"); - if (text_len == 0) { /* keep the format's copy */ } + /* Concatenated rather than `lua_pushfstring(L, "refused: %s", text)`: + the bridge's text is length-delimited and carries no terminator, so + `%s` would run off the end of the reply buffer. */ + lua_pushliteral(L, "refused: "); + lua_pushlstring(L, text, text_len); + lua_concat(L, 2); } else { - lua_pushlstring(L, text != NULL ? text : "unavailable", text_len); + lua_pushlstring(L, text, text_len); } return 2; } @@ -401,10 +408,15 @@ static int kore_lua_fail_table(lua_State *L, int status, const char *text, } /* - * [`kore_lua_fail`] for a NUL-terminated reason. The bridge answers a text - * with an explicit length (it carries arbitrary bytes); the synchronous - * leaf calls answer a C string, and passing those a length of zero would - * push an empty reason beside the nil. + * [`kore_lua_fail`] for a NUL-terminated reason. + * + * The two host contracts differ and the difference is load-bearing: + * `kore_lua_host_call` answers a text with an explicit length and **no + * terminator** (it carries arbitrary bytes out of a Rust `Vec`), while the + * synchronous leaf calls — `kore_lua_host_uptime_ns`, `..._wall_ns`, + * `..._random` — answer a `&'static CStr`. Only the latter may reach this + * function; a length-carrying text must go to [`kore_lua_fail`] with the + * length the bridge gave it, or `strlen` reads past the reply buffer. */ static int kore_lua_fail_cstr(lua_State *L, int status, const char *text) { return kore_lua_fail(L, status, text, text != NULL ? strlen(text) : 0); @@ -664,11 +676,10 @@ static int kore_lua_proc_index(lua_State *L) { } /* - * `exit` is deliberately absent until the host owns it. Termination has to - * unwind the evaluation, run `` handlers and let the REPL flush - * before the task leaves; a verb that raised instead would be a member that - * does not do what its name says, which is the one thing this build does - * not ship (see KORE-VENDOR.md on `string.format`). + * `kore.proc` carries no functions: `args` is the lazy field above, and + * termination is `os.exit`, which belongs on the stock surface where a + * script already looks for it. The empty list is what `luaL_newlib` wants + * for a table whose only member arrives through `__index`. */ static const luaL_Reg kore_lua_proc_lib[] = { {NULL, NULL} @@ -758,7 +769,7 @@ static kore_lua_writer *kore_lua_sink(lua_State *L) { * answers `EIO`, which is the honest "something below failed". */ static int kore_lua_errno(const char *code, size_t len) { - struct { const char *code; int number; } static const table[] = { + static const struct { const char *code; int number; } table[] = { {"not-found", 2}, /* ENOENT */ {"already-exists", 17}, /* EEXIST */ {"not-directory", 20}, /* ENOTDIR */ @@ -1022,16 +1033,27 @@ static int kore_lua_file_tostring(lua_State *L) { /* * Refill the read-ahead from the stream position. Answers 1 on progress, - * 0 at end of file, and -1 on a failure it has already pushed onto the - * stack in `render`'s spelling. + * 0 at end of file, and -1 on a failure whose status and text it hands + * back *without pushing anything*. + * + * Not rendering here is the whole point of the out-parameters: every + * caller but one has a live `luaL_Buffer`, and once that buffer outgrows + * `LUAL_BUFFERSIZE` it keeps a box userdata on the stack at a level + * `luaL_pushresult` re-checks. Pushing a failure triple on top of it + * would leave the box below values it does not expect. The caller + * finalizes its buffer first and renders afterwards, which is safe because + * `*why` stays alive until the *next* host call and finishing a buffer + * makes none. */ -static int kore_lua_file_fill(lua_State *L, kore_lua_file *file, int *pushed, - kore_lua_render_fail render) { +static int kore_lua_file_fill(lua_State *L, kore_lua_file *file, int *status, + const char **why, size_t *why_len) { unsigned long long count = 0, len = 0; const char *bytes = NULL; size_t bytes_len = 0; - int n; - *pushed = 0; + (void)L; + *status = KORE_LUA_STATUS_OK; + *why = NULL; + *why_len = 0; if (file->taken < file->have) return 1; /* still buffered */ if (file->eof) return 0; if (file->kind != KORE_LUA_FILE_STORE) { /* stdin: always at end */ @@ -1041,10 +1063,15 @@ static int kore_lua_file_fill(lua_State *L, kore_lua_file *file, int *pushed, file->base = file->position; file->have = 0; file->taken = 0; - n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_READ, file->handle, file->position, - KORE_LUA_BUFSIZE, NULL, 0, &count, &len, &bytes, - &bytes_len, render); - if (n) { *pushed = n; return -1; } + *status = kore_lua_host_call(KORE_LUA_OP_FS_READ, file->handle, + file->position, KORE_LUA_BUFSIZE, NULL, 0, + &count, &len, &bytes, &bytes_len); + if (*status != KORE_LUA_STATUS_OK) { + *why = bytes; + *why_len = bytes_len; + return -1; + } + if (bytes == NULL) bytes_len = 0; if (bytes_len > KORE_LUA_BUFSIZE) bytes_len = KORE_LUA_BUFSIZE; if (bytes_len != 0) memcpy(file->buf, bytes, bytes_len); file->have = bytes_len; @@ -1064,16 +1091,32 @@ static size_t kore_lua_file_take(kore_lua_file *file, luaL_Buffer *b, return take; } +/* + * Drop a partial read's buffer and answer the failure in `render`'s + * spelling, as a negative count. The buffer is finished and popped before + * anything is pushed, so `render`'s values never land above a live box. + */ +static int kore_lua_read_failed(lua_State *L, luaL_Buffer *b, int status, + const char *why, size_t why_len, + kore_lua_render_fail render) { + luaL_pushresult(b); + lua_pop(L, 1); + return -render(L, status, why, why_len); +} + /* `f:read("l")` / `"L"`: up to and including the newline. */ static int kore_lua_read_line(lua_State *L, kore_lua_file *file, int keep, kore_lua_render_fail render) { luaL_Buffer b; int got = 0; + int status = KORE_LUA_STATUS_OK; + const char *why = NULL; + size_t why_len = 0; luaL_buffinit(L, &b); for (;;) { - int pushed = 0; - int state = kore_lua_file_fill(L, file, &pushed, render); - if (state < 0) { luaL_pushresult(&b); lua_pop(L, 1); return pushed; } + int state = kore_lua_file_fill(L, file, &status, &why, &why_len); + if (state < 0) + return kore_lua_read_failed(L, &b, status, why, why_len, render); if (state == 0) break; { const char *from = file->buf + file->taken; @@ -1101,11 +1144,14 @@ static int kore_lua_read_line(lua_State *L, kore_lua_file *file, int keep, static int kore_lua_read_all(lua_State *L, kore_lua_file *file, kore_lua_render_fail render) { luaL_Buffer b; + int status = KORE_LUA_STATUS_OK; + const char *why = NULL; + size_t why_len = 0; luaL_buffinit(L, &b); for (;;) { - int pushed = 0; - int state = kore_lua_file_fill(L, file, &pushed, render); - if (state < 0) { luaL_pushresult(&b); lua_pop(L, 1); return pushed; } + int state = kore_lua_file_fill(L, file, &status, &why, &why_len); + if (state < 0) + return kore_lua_read_failed(L, &b, status, why, why_len, render); if (state == 0) break; kore_lua_file_take(file, &b, file->have - file->taken); } @@ -1119,21 +1165,24 @@ static int kore_lua_read_count(lua_State *L, kore_lua_file *file, size_t want, kore_lua_render_fail render) { luaL_Buffer b; size_t total = 0; + int status = KORE_LUA_STATUS_OK; + const char *why = NULL; + size_t why_len = 0; if (want == 0) { /* Lua's zero-length read is a probe: the empty string when there is - anything left, nil at end of file. */ - int pushed = 0; - int state = kore_lua_file_fill(L, file, &pushed, render); - if (state < 0) return pushed; + anything left, nil at end of file. No buffer is live here, so the + failure renders directly. */ + int state = kore_lua_file_fill(L, file, &status, &why, &why_len); + if (state < 0) return -render(L, status, why, why_len); if (state == 0) { lua_pushnil(L); return 1; } lua_pushliteral(L, ""); return 1; } luaL_buffinit(L, &b); while (total < want) { - int pushed = 0; - int state = kore_lua_file_fill(L, file, &pushed, render); - if (state < 0) { luaL_pushresult(&b); lua_pop(L, 1); return pushed; } + int state = kore_lua_file_fill(L, file, &status, &why, &why_len); + if (state < 0) + return kore_lua_read_failed(L, &b, status, why, why_len, render); if (state == 0) break; total += kore_lua_file_take(file, &b, want - total); } @@ -1149,10 +1198,14 @@ static int kore_lua_read_number(lua_State *L, kore_lua_file *file, char text[64]; size_t len = 0; int seen_digit = 0; + int status = KORE_LUA_STATUS_OK; + const char *why = NULL; + size_t why_len = 0; for (;;) { - int pushed = 0; - int state = kore_lua_file_fill(L, file, &pushed, render); - if (state < 0) return pushed; + int state = kore_lua_file_fill(L, file, &status, &why, &why_len); + /* The numeral is accumulated in `text`, not a `luaL_Buffer`, so this + one renders directly. */ + if (state < 0) return -render(L, status, why, why_len); if (state == 0) break; { char c = file->buf[file->taken]; @@ -1177,7 +1230,9 @@ static int kore_lua_read_number(lua_State *L, kore_lua_file *file, } /* One read format; answers how many values it pushed, or a negative count - meaning "a failure of `-n` values is already on the stack". */ + meaning "a failure of `-n` values is already on the stack". A format + that merely found nothing is *not* a failure: it answers 1 with nil on + top, which is what end of file looks like to a script. */ static int kore_lua_read_one(lua_State *L, kore_lua_file *file, int index, kore_lua_render_fail render) { if (lua_type(L, index) == LUA_TNUMBER) { @@ -1207,9 +1262,22 @@ static int kore_lua_file_read_with(lua_State *L, kore_lua_file *file, int first, return render(L, KORE_LUA_STATUS_REFUSED, "access-denied", sizeof "access-denied" - 1); } - if (first > top) return kore_lua_read_line(L, file, 0, render); + if (first > top) { + int n = kore_lua_read_line(L, file, 0, render); + return n < 0 ? -n : n; + } for (int index = first; index <= top; index++) { int n = kore_lua_read_one(L, file, index, render); + if (n < 0) { + /* The read failed. Stock `g_read` answers `luaL_fileresult` alone + in this case, so drop whatever the earlier formats managed to + read: the failure is the whole answer, not a tail on a partial + one. The failure's `-n` values are on top; each `lua_remove` + takes the value directly beneath them. */ + int failed = -n; + for (int drop = 0; drop < pushed; drop++) lua_remove(L, -failed - 1); + return failed; + } pushed += n; /* Stop at the first format that answered nil: the rest cannot succeed when this one hit end of file, and Lua stops too. */ @@ -1365,12 +1433,31 @@ static int kore_lua_lines_step(lua_State *L) { if (file == NULL || file->closed) return luaL_error(L, "file is already closed"); for (int i = 0; i < formats; i++) lua_pushvalue(L, lua_upvalueindex(4 + i)); - pushed = formats == 0 - ? kore_lua_read_line(L, file, 0, kore_lua_fail_io) - : kore_lua_file_read_with(L, file, lua_gettop(L) - formats + 1, - lua_gettop(L), kore_lua_fail_io); - if (close_at_end && lua_isnil(L, -1)) - (void)kore_lua_file_do_close(L, file, kore_lua_fail_io), lua_pop(L, 1); + if (formats == 0) { + pushed = kore_lua_read_line(L, file, 0, kore_lua_fail_io); + if (pushed < 0) pushed = -pushed; + } else { + pushed = kore_lua_file_read_with(L, file, lua_gettop(L) - formats + 1, + lua_gettop(L), kore_lua_fail_io); + } + /* + * The *first* result decides, not the last: with several formats the + * tail can be nil while the head read fine, and `io_readline` looks at + * the head. A falsy head with a message beside it is a read failure, + * which stock Lua raises rather than ending the loop on — a `for` that + * simply stopped would make an I/O error look like end of file. + */ + if (!lua_toboolean(L, -pushed)) { + if (pushed > 1) { + const char *why = lua_tostring(L, -pushed + 1); + return luaL_error(L, "%s", why != NULL ? why : "read failed"); + } + if (close_at_end) { + int top = lua_gettop(L); + (void)kore_lua_file_do_close(L, file, kore_lua_fail_io); + lua_settop(L, top); + } + } return pushed; } @@ -1485,6 +1572,37 @@ static int kore_lua_fs_rename(lua_State *L) { return 1; } +/* + * `kore.fs.resize(file, bytes)` — the one verb that shortens or extends a + * file without rewriting it. + * + * On `kore.fs` and not on the file object: `io`'s seven methods are what + * the manual lists and a stock `file:` gains nothing here, while truncation + * is exactly the kind of thing the kore surface exists to expose. A + * shortened file drags the stream position back with it — the next write + * must not land past a hole the caller never asked for. + */ +static int kore_lua_fs_resize(lua_State *L) { + kore_lua_file *file = kore_lua_file_live(L, 1); + lua_Integer length = luaL_checkinteger(L, 2); + unsigned long long r = 0, r2 = 0; + int n; + luaL_argcheck(L, length >= 0, 2, "length must not be negative"); + if (file->kind != KORE_LUA_FILE_STORE) + return kore_lua_fail_table(L, KORE_LUA_STATUS_REFUSED, "unsupported", + sizeof "unsupported" - 1); + kore_lua_file_drop(file); + n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_RESIZE, file->handle, + (unsigned long long)length, 0, NULL, 0, &r, &r2, + NULL, NULL, kore_lua_fail_table); + if (n) return n; + if (file->position > (unsigned long long)length) + file->position = (unsigned long long)length; + file->base = file->position; + lua_pushboolean(L, 1); + return 1; +} + static int kore_lua_fs_stat(lua_State *L) { size_t len = 0; const char *path = luaL_checklstring(L, 1, &len); @@ -1552,13 +1670,22 @@ static int kore_lua_fs_read_file(lua_State *L) { kore_lua_fail_table); if (n) return n; { + /* `base + 1` is the file object; the read's results sit above it. */ + int base = lua_gettop(L); kore_lua_file *file = kore_lua_file_push(L, handle, KORE_LUA_FILE_STORE, 1, 0, 0); - int pushed = kore_lua_read_all(L, file, kore_lua_fail_table); - /* The handle is retired here whatever the read did: this verb never - hands the object out, so nothing else can close it. */ - lua_pushvalue(L, -1 - pushed); + int read = kore_lua_read_all(L, file, kore_lua_fail_table); + int pushed = read < 0 ? -read : read; + /* + * The handle is retired here whatever the read did: this verb never + * hands the object out, so nothing else can close it. The close's own + * answer is discarded — the read's is what the caller asked for — and + * it is discarded by level rather than by a fixed count, because + * `kore_lua_file_do_close` pushes one value on success and two on a + * refusal. + */ (void)kore_lua_file_do_close(L, file, kore_lua_fail_table); - lua_pop(L, 2); + lua_settop(L, base + 1 + pushed); + lua_remove(L, base + 1); return pushed; } } @@ -1596,6 +1723,7 @@ static const luaL_Reg kore_lua_fs_lib[] = { {"remove", kore_lua_fs_remove}, {"rename", kore_lua_fs_rename}, {"mkdir", kore_lua_fs_mkdir}, + {"resize", kore_lua_fs_resize}, {"stat", kore_lua_fs_stat}, {"exists", kore_lua_fs_exists}, {"list", kore_lua_fs_list}, @@ -1740,24 +1868,34 @@ static int kore_lua_io_lines(lua_State *L) { /* * Draw an unused temporary name into `dst` (at least 32 bytes) and create * the file exclusively, so the name answered is one nothing else can - * already hold. Answers the open handle, or 0 with `why` set. + * already hold. Answers the open handle, or 0 with `why` / `why_len` set. + * + * The length is an out-parameter and not a `strlen` at the call site: a + * reason coming back from `kore_lua_host_call` is length-delimited with no + * terminator, and only the entropy draw's reason is a C string. Both are + * normalized to a length here so neither caller has to know which it got. * * `os.tmpname` and `io.tmpfile` are the same act — a name nobody else has — * differing only in what they hand back, so they share this. */ static unsigned long long kore_lua_tempfile(char *dst, size_t *len_out, unsigned long long flags, - const char **why) { + const char **why, + size_t *why_len_out) { static const char digits[] = "0123456789abcdef"; *why = NULL; + *why_len_out = 0; for (int attempt = 0; attempt < 16; attempt++) { unsigned char draw[8]; unsigned long long handle = 0, length = 0; size_t why_len = 0; size_t len = 0; int status; - if (kore_lua_host_random(draw, sizeof draw, why) != KORE_LUA_STATUS_OK) + if (kore_lua_host_random(draw, sizeof draw, why) != KORE_LUA_STATUS_OK) { + /* A leaf call's reason is a `&'static CStr`. */ + *why_len_out = *why != NULL ? strlen(*why) : 0; return 0; + } memcpy(dst, "/tmp/lua_", 9); len = 9; for (size_t i = 0; i < sizeof draw; i++) { @@ -1775,11 +1913,14 @@ static unsigned long long kore_lua_tempfile(char *dst, size_t *len_out, return handle + 1; /* biased so 0 means failure */ } /* Only a collision is worth another draw; anything else is the answer. */ - if (!(why_len == sizeof "already-exists" - 1 && - memcmp(*why, "already-exists", why_len) == 0)) + if (!(*why != NULL && why_len == sizeof "already-exists" - 1 && + memcmp(*why, "already-exists", why_len) == 0)) { + *why_len_out = why_len; return 0; + } } *why = "no unused temporary name was found"; + *why_len_out = sizeof "no unused temporary name was found" - 1; return 0; } @@ -1792,14 +1933,14 @@ static unsigned long long kore_lua_tempfile(char *dst, size_t *len_out, */ static int kore_lua_io_tmpfile(lua_State *L) { char path[48]; - size_t len = 0; + size_t len = 0, why_len = 0; const char *why = NULL; - unsigned long long biased = - kore_lua_tempfile(path, &len, KORE_LUA_O_READ | KORE_LUA_O_WRITE, &why); + unsigned long long biased = kore_lua_tempfile( + path, &len, KORE_LUA_O_READ | KORE_LUA_O_WRITE, &why, &why_len); if (biased == 0) return kore_lua_fail_io(L, KORE_LUA_STATUS_UNAVAILABLE, why != NULL ? why : "unavailable", - why != NULL ? strlen(why) : 0); + why != NULL ? why_len : sizeof "unavailable" - 1); { kore_lua_file *file = kore_lua_file_push(L, biased - 1, KORE_LUA_FILE_STORE, 1, 1, 0); @@ -1899,6 +2040,35 @@ static void kore_lua_break_down(long long seconds, kore_lua_civil *out) { (int)(days - kore_lua_days_from_civil(out->year, 1, 1) + 1); } +/* + * The instants this calendar answers for. + * + * `civil_from_days` is exact for every day the proleptic Gregorian + * calendar defines, but `kore_lua_civil` holds an `int` year and `%Y` + * spells four digits, so the range is stated rather than left to overflow + * into a truncated year. Computed from the calendar itself rather than + * tabulated, so the bound and the conversion cannot disagree: years + * 0000-01-01 through 9999-12-31, which is also the range `os.time` accepts + * a table for. + */ +#define KORE_LUA_YEAR_MIN 0 +#define KORE_LUA_YEAR_MAX 9999 + +static int kore_lua_time_representable(long long seconds) { + long long low = kore_lua_days_from_civil(KORE_LUA_YEAR_MIN, 1, 1) * 86400; + long long high = + kore_lua_days_from_civil(KORE_LUA_YEAR_MAX, 12, 31) * 86400 + 86399; + return seconds >= low && seconds <= high; +} + +/* + * Every `os.time` field is bounded before any arithmetic, so a + * normalization cannot overflow on its way to the check above. Generous + * by design — far more than date arithmetic by field addition needs — and + * it is `kore_lua_time_representable` that decides the answer. + */ +#define KORE_LUA_DATE_FIELD_MAX 1000000000ll + /* The wall clock, as whole seconds since the epoch; `ok` says whether the machine could answer at all. */ static long long kore_lua_wall_seconds(int *ok) { @@ -2087,6 +2257,9 @@ static int kore_lua_os_time(lua_State *L) { caller would read as 1970 rather than as "no reading". */ if (!ok) return kore_lua_fail_cstr(L, KORE_LUA_STATUS_UNAVAILABLE, "the wall clock is not set"); + if (!kore_lua_time_representable(when)) + return luaL_error( + L, "time result cannot be represented in this installation"); lua_pushinteger(L, (lua_Integer)when); return 1; } @@ -2102,13 +2275,37 @@ static int kore_lua_os_time(lua_State *L) { lua_Integer hour = kore_lua_datefield(L, "hour", 12, 0); lua_Integer minute = kore_lua_datefield(L, "min", 0, 0); lua_Integer second = kore_lua_datefield(L, "sec", 0, 0); - long long carry = kore_lua_floor_div((long long)month - 1, 12); + long long carry; long long days; kore_lua_civil t; + /* + * Bound the fields before any arithmetic, then bound the result. + * + * Two checks, not three: the field bound is what makes `(int)year` + * exact — a year normalized from fields this size stays under 1.1e9 + * and an `int` holds it — and what keeps `days * 86400 + ...` from + * overflowing, since the widest reachable value is about 3.4e16. + * With overflow and truncation both excluded, `time_representable` is + * the one thing that decides whether there is an answer. + */ + if (day < -KORE_LUA_DATE_FIELD_MAX || day > KORE_LUA_DATE_FIELD_MAX || + hour < -KORE_LUA_DATE_FIELD_MAX || hour > KORE_LUA_DATE_FIELD_MAX || + minute < -KORE_LUA_DATE_FIELD_MAX || + minute > KORE_LUA_DATE_FIELD_MAX || + second < -KORE_LUA_DATE_FIELD_MAX || + second > KORE_LUA_DATE_FIELD_MAX || + month < -KORE_LUA_DATE_FIELD_MAX || month > KORE_LUA_DATE_FIELD_MAX || + year < -KORE_LUA_DATE_FIELD_MAX || year > KORE_LUA_DATE_FIELD_MAX) + return luaL_error( + L, "time result cannot be represented in this installation"); + carry = kore_lua_floor_div((long long)month - 1, 12); year += carry; month = (long long)month - 1 - carry * 12 + 1; days = kore_lua_days_from_civil((int)year, (int)month, 1) + (day - 1); when = days * 86400 + hour * 3600 + minute * 60 + second; + if (!kore_lua_time_representable(when)) + return luaL_error( + L, "time result cannot be represented in this installation"); /* `mktime` normalizes the caller's table in place and fills in the fields it derived; `setallfields` is what a script reads back to learn the weekday it never supplied. */ @@ -2144,6 +2341,9 @@ static int kore_lua_os_date(lua_State *L) { same calendar it would without it. Accepted rather than rejected: a portable script writes `!` and must not fail here for doing so. */ if (*format == '!') format++; + if (!kore_lua_time_representable(when)) + return luaL_error(L, + "date result cannot be represented in this installation"); kore_lua_break_down(when, &t); if (strcmp(format, "*t") == 0 || strcmp(format, "!*t") == 0) { lua_createtable(L, 0, 9); @@ -2232,13 +2432,14 @@ static int kore_lua_os_rename(lua_State *L) { */ static int kore_lua_os_tmpname(lua_State *L) { char path[48]; - size_t len = 0; + size_t len = 0, why_len = 0; const char *why = NULL; unsigned long long biased = - kore_lua_tempfile(path, &len, KORE_LUA_O_WRITE, &why); + kore_lua_tempfile(path, &len, KORE_LUA_O_WRITE, &why, &why_len); if (biased == 0) - return kore_lua_fail_cstr(L, KORE_LUA_STATUS_UNAVAILABLE, - why != NULL ? why : "unavailable"); + return kore_lua_fail(L, KORE_LUA_STATUS_UNAVAILABLE, + why != NULL ? why : "unavailable", + why != NULL ? why_len : sizeof "unavailable" - 1); { /* The name is what was asked for, not the handle: close it and leave the file in place, which is the POSIX behaviour the manual describes @@ -2439,11 +2640,19 @@ static int kore_lua_require(lua_State *L) { lua_remove(L, -2); /* the chunk name */ } else if (kind == KORE_LUA_MODULE_NATIVE && opener != NULL) { + /* Nothing is borrowed for a native answer — the opener is an address + in this task's JIT arena — but the release is unconditional so the + host has exactly one rule: every answer is released once. */ + kore_lua_host_release(); lua_pushcfunction(L, opener); } else { - return luaL_error(L, "module '%s' not found: %s", name, - bytes != NULL ? bytes : "no route"); + /* `%s`, not a length: unlike `kore_lua_host_call`'s reply, a + `require` failure is a `&'static CStr` from src/bridge.rs's + `describe`, so it is terminated and outlives the release. */ + const char *why = bytes != NULL ? bytes : "no route"; + kore_lua_host_release(); + return luaL_error(L, "module '%s' not found: %s", name, why); } lua_pushstring(L, name); lua_call(L, 1, 1); /* loader(name) */ @@ -2552,9 +2761,9 @@ static int kore_lua_open_libraries(lua_State *L) { luaL_requiref(L, LUA_TABLIBNAME, luaopen_table, 1); lua_pop(L, 1); luaL_requiref(L, LUA_STRLIBNAME, luaopen_string, 1); /* No partial printf and no producer for unsupported binary chunks. */ - lua_pushnil(L); lua_setfield(L, -2, "format"); lua_pop(L, 1); - lua_getglobal(L, LUA_STRLIBNAME); - lua_pushnil(L); lua_setfield(L, -2, "dump"); lua_pop(L, 1); + lua_pushnil(L); lua_setfield(L, -2, "format"); + lua_pushnil(L); lua_setfield(L, -2, "dump"); + lua_pop(L, 1); luaL_requiref(L, LUA_MATHLIBNAME, luaopen_math, 1); lua_pop(L, 1); luaL_requiref(L, LUA_UTF8LIBNAME, luaopen_utf8, 1); lua_pop(L, 1); /* The file object's metatable, before anything that can create one. */ diff --git a/crates/lua/src/lib.rs b/crates/lua/src/lib.rs index 1285f51..62f616e 100644 --- a/crates/lua/src/lib.rs +++ b/crates/lua/src/lib.rs @@ -18,9 +18,14 @@ //! kore.random.bytes(32) //! kore.proc.args -- a 1-based array, read once //! kore.fs.read_file "/etc/hosts" -- value, err +//! kore.fs.stat "/etc/hosts" -- size_bytes, kind, directory //! kore.has "fs" kore.caps() -- honest provisioning //! ``` //! +//! `kore.fs` adds only what the stock surface has no spelling for — +//! `read_file`, `write_file`, `mkdir`, `resize`, `stat`, `exists`, `list` +//! — and never changes a stock member. +//! //! ```lua //! local f = assert(io.open("/tmp/log", "a")) -- the SAME object //! f:write("hello\n"); f:close() @@ -64,9 +69,15 @@ //! # Threads //! //! Lua is a synchronous C engine, so the VM runs on a [`kore::thread`] -//! worker and the executor stays on thread 0. [`bridge`] documents the one -//! transport between them. Verbs needing no service — `kore.time.now`, -//! `kore.random`, and the whole calendar — never cross it. +//! worker and the executor stays on thread 0. The `bridge` module +//! documents the one transport between them. Verbs needing no service — +//! `kore.time.now`, `kore.random`, and the whole calendar — never cross +//! it. +//! +//! `bridge`, and with it `Host`, `Services` and `Outcome`, exist only +//! where the engine does. They are named in prose rather than linked +//! throughout these docs: a host build compiles no engine, so the items +//! are absent and an intra-doc link to one would not resolve. //! //! # Time //! @@ -92,10 +103,9 @@ extern crate alloc; // vocabulary — be checked and unit-tested by the host workspace. #[cfg(target_os = "none")] pub mod bridge; -/// `kore.fs`'s thread-0 half: the open-file table, the positional verbs, -/// and the error vocabulary. Public because the vocabulary is part of the -/// contract — an embedder whose own library can fail a filesystem failure -/// renders it with [`files::code`] rather than spelling one of its own. +// Documented by its own `//!` header. Keeping the module's docs in one +// file is what lets rustdoc resolve their intra-doc links against that +// file's imports. pub mod files; #[cfg(target_os = "none")] mod numeric; @@ -180,7 +190,7 @@ pub const OP_EMBEDDER_BASE: u32 = 64; /// `kore.accel`; the shape mirrors `kore-wasi`'s embedder-composed /// interfaces. /// -/// Selection is by type instantiation, not runtime dispatch: [`Host::start`] +/// Selection is by type instantiation, not runtime dispatch: `Host::start` /// is generic over `E`, so `LIBRARIES` reaches the worker thread as /// monomorphized static data with no global to publish it. /// diff --git a/crates/lua/src/vm.rs b/crates/lua/src/vm.rs index b532778..b2ec5d8 100644 --- a/crates/lua/src/vm.rs +++ b/crates/lua/src/vm.rs @@ -14,6 +14,17 @@ use crate::shim; const LUA_OK: c_int = 0; const LUA_MULTRET: c_int = -1; +/// The error `os.exit` raises to unwind the evaluation, as `kore_lua.c`'s +/// `KORE_LUA_EXIT_SENTINEL` spells it. +/// +/// Matched as a *suffix*, because `luaL_error` puts the chunk name and line +/// in front of it. `os.exit` cannot leave from inside the VM — there are C +/// frames below it and `` handlers above it — so it records the code +/// with the host and raises this; the raise is the unwind, not a failure, +/// and the code is read afterwards through +/// [`Services::exit_request`](crate::Services::exit_request). +const EXIT_SENTINEL: &[u8] = b"kore:exit"; + #[repr(C)] struct LuaState { _private: [u8; 0], @@ -85,10 +96,21 @@ unsafe extern "C" fn append( pub enum ReplResult { Complete(bool), + /// The chunk ran `os.exit`. + Exited, Incomplete, ResourceRefused, } +/// How one chunk ended. +pub enum EvalResult { + Ok, + Failed, + /// The chunk ran `os.exit`: the sentinel unwound every `` on the + /// way out and the recorded code is the answer, not the raise. + Exited, +} + pub struct Vm { state: NonNull, capture: Box, @@ -155,6 +177,19 @@ impl Vm { } } + /// Whether the pending error is `os.exit`'s unwind rather than a + /// failure. `luaL_error` prefixes the chunk and line, so this is a + /// suffix match on [`EXIT_SENTINEL`]. + fn error_is_exit(&self) -> bool { + let (error, len) = self.error_parts(); + // SAFETY: `error_parts` answers a live Lua string or a static. + let error = unsafe { slice::from_raw_parts(error, len) }; + error + .strip_suffix(b"\n") + .unwrap_or(error) + .ends_with(EXIT_SENTINEL) + } + fn publish_error(&mut self) { let (error, len) = self.error_parts(); let used = self @@ -179,32 +214,51 @@ impl Vm { self.clear_stack(); } - fn execute_loaded(&mut self, print_results: bool) -> bool { + fn execute_loaded(&mut self, print_results: bool) -> EvalResult { if unsafe { kore_lua_call(self.state.as_ptr(), 0, LUA_MULTRET) } != LUA_OK { + // `os.exit`'s unwind reaches here as an error, because that is + // how it ran the `` handlers on its way out. It is not + // one, so it is not reported as one. + if self.error_is_exit() { + self.clear_stack(); + return EvalResult::Exited; + } self.publish_error(); - return false; + return EvalResult::Failed; } if print_results && unsafe { kore_lua_top(self.state.as_ptr()) } > 0 && unsafe { kore_lua_print_results(self.state.as_ptr()) } != LUA_OK { + if self.error_is_exit() { + self.clear_stack(); + return EvalResult::Exited; + } self.publish_error(); - return false; + return EvalResult::Failed; } self.clear_stack(); - true + EvalResult::Ok } - pub fn eval(&mut self, bytes: &[u8], name: &'static [u8]) -> bool { + pub fn eval(&mut self, bytes: &[u8], name: &'static [u8]) -> EvalResult { self.clear_stack(); if self.load(bytes, name) != LUA_OK { self.publish_error(); - false + EvalResult::Failed } else { self.execute_loaded(false) } } + fn repl_result(result: EvalResult) -> ReplResult { + match result { + EvalResult::Ok => ReplResult::Complete(true), + EvalResult::Failed => ReplResult::Complete(false), + EvalResult::Exited => ReplResult::Exited, + } + } + /// Stock Lua REPL semantics: expression first, then statement, with an /// `` syntax error requesting a continuation line. pub fn eval_repl(&mut self, bytes: &[u8]) -> ReplResult { @@ -220,12 +274,12 @@ impl Vm { expression.extend_from_slice(bytes); if self.load(&expression, b"=stdin\0") == LUA_OK { drop(expression); - return ReplResult::Complete(self.execute_loaded(true)); + return Self::repl_result(self.execute_loaded(true)); } self.clear_stack(); drop(expression); if self.load(bytes, b"=stdin\0") == LUA_OK { - return ReplResult::Complete(self.execute_loaded(true)); + return Self::repl_result(self.execute_loaded(true)); } let (error, len) = self.error_parts(); let error = unsafe { slice::from_raw_parts(error, len) }; diff --git a/crates/lua/tests/host_fs.c b/crates/lua/tests/host_fs.c new file mode 100644 index 0000000..5f6fae9 --- /dev/null +++ b/crates/lua/tests/host_fs.c @@ -0,0 +1,549 @@ +/* + * An in-memory store behind the real host transport. + * + * `src/kore_lua.c` declares every host entry point weak so a build with no + * Rust host still links; with only those weak stubs, though, every verb + * that touches a file answers "no kore host" and the whole `io` / `os` / + * `kore.fs` surface is covered only as far as "the member exists". That + * is exactly the half where the interesting mistakes live: stack + * discipline around `luaL_Buffer`, handle lifetime, the two failure + * spellings, append and truncate, the temporary-file draw. + * + * So this file defines those symbols *strongly* and puts a small store + * behind them. It is not a filesystem: it is the smallest thing that can + * answer the ten filesystem ops faithfully enough that the C above it is + * exercised for real. Faithfully means, specifically: + * + * - The reply text is **length-delimited and not NUL-terminated**, and + * the byte after it is poisoned. That is the bridge's actual contract + * (`src/bridge.rs` answers a Rust `Vec`), and anything above that + * reaches for `strlen` on it reads poison and fails a test rather than + * passing by luck on a host where the next byte happened to be zero. + * - Refusals carry the kebab-case vocabulary `src/files.rs` derives from + * `FsError`, so the errno table and the `kore.fs` error table are + * tested against the words they will really see. + * - A handle is an index and a generation, encoded as `src/files.rs` + * encodes it, so a use after close is `invalid-handle` here too. + * - Reads are short by design: the store answers at most one page, which + * is what makes the read-ahead loop in `kore_lua.c` run more than once. + * + * Included by tests/host_smoke.c rather than compiled separately, so both + * the libc-numeric and target-numeric builds of that harness get it. + */ + +#include +#include +#include + +/* Must agree with src/kore_lua.c's enum and src/bridge.rs's constants. */ +enum { + HOST_FS_OP_SLEEP = 16, + HOST_FS_OP_PROC_ARGS = 17, + HOST_FS_OP_OPEN = 18, + HOST_FS_OP_CLOSE = 19, + HOST_FS_OP_READ = 20, + HOST_FS_OP_WRITE = 21, + HOST_FS_OP_SYNC = 23, + HOST_FS_OP_RESIZE = 24, + HOST_FS_OP_STAT = 25, + HOST_FS_OP_REMOVE = 26, + HOST_FS_OP_RENAME = 27, + HOST_FS_OP_LIST = 28, + HOST_FS_OP_PROC_EXIT = 29 +}; + +enum { + HOST_FS_O_READ = 1 << 0, + HOST_FS_O_WRITE = 1 << 1, + HOST_FS_O_CREATE = 1 << 2, + HOST_FS_O_TRUNCATE = 1 << 3, + HOST_FS_O_APPEND = 1 << 4, + HOST_FS_O_EXCLUSIVE = 1 << 5, + HOST_FS_O_DIRECTORY = 1 << 6 +}; + +enum { HOST_FS_OK = 0, HOST_FS_REFUSED = 1, HOST_FS_UNAVAILABLE = 2 }; + +/* One page, so `f:read("a")` over a larger file refills more than once. */ +#define HOST_FS_READ_MAX 4096 +#define HOST_FS_MAX_OBJECTS 32 +#define HOST_FS_MAX_HANDLES 16 +#define HOST_FS_PATH_MAX 64 +#define HOST_FS_BYTES_MAX 16384 + +struct host_fs_object { + char path[HOST_FS_PATH_MAX]; + unsigned char bytes[HOST_FS_BYTES_MAX]; + size_t len; + int directory; + int used; +}; + +struct host_fs_handle { + int object; + unsigned generation; + int readable; + int writable; + int append; + int used; +}; + +static struct { + struct host_fs_object objects[HOST_FS_MAX_OBJECTS]; + struct host_fs_handle handles[HOST_FS_MAX_HANDLES]; + unsigned next_generation; + /* The reply window. One buffer, overwritten by each call, exactly as + the bridge's `cell.text` is: C copies it into a Lua string before it + makes another call. The poison byte is the point — see the header. */ + unsigned char reply[HOST_FS_READ_MAX + 1]; + size_t reply_len; + /* What the harness inspects afterwards. */ + int exit_requested; + int exit_code; + unsigned long long slept_ns; + /* A deterministic draw, so a temporary name is reproducible. */ + unsigned long long entropy; + int entropy_available; + const char *args; + /* Fault injection. `kore.time.sleep` is the shortest path to + `kore_lua_fail`'s REFUSED arm, which is the only place a refusal's + text is *concatenated* rather than pushed whole — and so the only + place a `%s` over the length-delimited reply would run into poison. */ + int refuse_sleep; + /* Refuse every read past the first `reads_allowed`, once armed. This is + the only way to reach a failure that lands *after* a partial read: by + then `luaL_Buffer` has grown onto the stack as a box, and a failure + pushed above it would leave the box at a level `luaL_pushresult` no + longer expects. */ + int reads_armed; + int reads_allowed; + /* Refuse every close. The slot is still freed, so the harness's + leak accounting stays meaningful; what is under test is the *caller's* + stack discipline, because `kore_lua_file_do_close` pushes one value on + success and two on a refusal. */ + int refuse_close; +} host_fs; + +/* ------------------------------------------------------------- replies */ + +/* + * Publish `len` bytes as the reply and poison the byte after them. + * + * The poison is load-bearing: the bridge's text is a Rust `Vec` with + * no terminator, so any caller that reaches for `strlen` must produce a + * visibly wrong answer here rather than a lucky one. + */ +static void host_fs_reply(const void *bytes, size_t len, + const char **text, size_t *text_len) { + if (len > HOST_FS_READ_MAX) len = HOST_FS_READ_MAX; + if (len != 0 && bytes != NULL) memcpy(host_fs.reply, bytes, len); + host_fs.reply[len] = 0xff; + host_fs.reply_len = len; + if (text != NULL) *text = (const char *)host_fs.reply; + if (text_len != NULL) *text_len = len; +} + +static int host_fs_refuse(const char *code, const char **text, + size_t *text_len) { + host_fs_reply(code, strlen(code), text, text_len); + return HOST_FS_REFUSED; +} + +/* ------------------------------------------------------------ the store */ + +static int host_fs_find(const char *path, size_t len) { + if (len >= HOST_FS_PATH_MAX) return -1; + for (int i = 0; i < HOST_FS_MAX_OBJECTS; i++) + if (host_fs.objects[i].used && strlen(host_fs.objects[i].path) == len && + memcmp(host_fs.objects[i].path, path, len) == 0) + return i; + return -1; +} + +/* The parent of `path`, as src/files.rs `split` computes it: "/" for a + top-level name, "." for a bare one. */ +static void host_fs_parent(const char *path, size_t len, char *out, + size_t *out_len) { + while (len > 1 && path[len - 1] == '/') len--; + size_t cut = len; + while (cut > 0 && path[cut - 1] != '/') cut--; + if (cut == 0) { memcpy(out, ".", 1); *out_len = 1; return; } + if (cut == 1) { memcpy(out, "/", 1); *out_len = 1; return; } + memcpy(out, path, cut - 1); + *out_len = cut - 1; +} + +static int host_fs_parent_ok(const char *path, size_t len) { + char parent[HOST_FS_PATH_MAX]; + size_t parent_len = 0; + host_fs_parent(path, len, parent, &parent_len); + if (parent_len == 1 && parent[0] == '.') return 1; /* the task's cwd */ + int at = host_fs_find(parent, parent_len); + return at >= 0 && host_fs.objects[at].directory; +} + +static int host_fs_make(const char *path, size_t len, int directory) { + if (len == 0 || len >= HOST_FS_PATH_MAX) return -1; + for (int i = 0; i < HOST_FS_MAX_OBJECTS; i++) { + if (host_fs.objects[i].used) continue; + memcpy(host_fs.objects[i].path, path, len); + host_fs.objects[i].path[len] = '\0'; + host_fs.objects[i].len = 0; + host_fs.objects[i].directory = directory; + host_fs.objects[i].used = 1; + return i; + } + return -1; +} + +/* A handle is `(generation << 32) | index`, as src/files.rs encodes it. */ +static unsigned long long host_fs_encode(int index, unsigned generation) { + return ((unsigned long long)generation << 32) | (unsigned long long)index; +} + +static struct host_fs_handle *host_fs_resolve(unsigned long long handle) { + unsigned index = (unsigned)(handle & 0xffffffffu); + unsigned generation = (unsigned)(handle >> 32); + if (index >= HOST_FS_MAX_HANDLES) return NULL; + struct host_fs_handle *slot = &host_fs.handles[index]; + if (!slot->used || slot->generation != generation) return NULL; + return slot; +} + +/* ---------------------------------------------------------- the harness */ + +void host_fs_reset(const char *args) { + memset(&host_fs, 0, sizeof host_fs); + host_fs.entropy = 0x243f6a8885a308d3ull; /* any fixed, well-mixed word */ + host_fs.entropy_available = 1; + host_fs.args = args != NULL ? args : ""; + /* The two directories a task starts with. */ + host_fs_make("/", 1, 1); + host_fs_make("/tmp", 4, 1); +} + +void host_fs_set_entropy(int available) { + host_fs.entropy_available = available; +} + +void host_fs_set_refuse_sleep(int refuse) { host_fs.refuse_sleep = refuse; } + +void host_fs_set_refuse_close(int refuse) { host_fs.refuse_close = refuse; } + +/* Let `allowed` more reads through, then refuse; a negative count disarms. */ +void host_fs_fail_reads_after(int allowed) { + host_fs.reads_armed = allowed >= 0; + host_fs.reads_allowed = allowed; +} + +int host_fs_exit_requested(void) { return host_fs.exit_requested; } +int host_fs_exit_code(void) { return host_fs.exit_code; } +unsigned long long host_fs_slept_ns(void) { return host_fs.slept_ns; } + +/* Every handle the store still believes is open — what a leak looks like. */ +int host_fs_open_handles(void) { + int live = 0; + for (int i = 0; i < HOST_FS_MAX_HANDLES; i++) + if (host_fs.handles[i].used) live++; + return live; +} + +int host_fs_exists(const char *path) { + return host_fs_find(path, strlen(path)) >= 0; +} + +/* Seed a file directly, so a test can read one it did not have to write. */ +int host_fs_put(const char *path, const char *bytes, size_t len) { + size_t path_len = strlen(path); + int at = host_fs_find(path, path_len); + if (at < 0) at = host_fs_make(path, path_len, 0); + if (at < 0 || len > HOST_FS_BYTES_MAX) return 0; + memcpy(host_fs.objects[at].bytes, bytes, len); + host_fs.objects[at].len = len; + return 1; +} + +/* ------------------------------------------------------- the entry points */ + +int kore_lua_host_random(unsigned char *dst, size_t len, const char **text) { + if (!host_fs.entropy_available) { + if (dst != NULL && len != 0) memset(dst, 0, len); + *text = "this machine exposes no entropy source"; + return HOST_FS_UNAVAILABLE; + } + for (size_t i = 0; i < len; i++) { + /* splitmix64, so a temporary name is deterministic across runs. */ + host_fs.entropy += 0x9e3779b97f4a7c15ull; + unsigned long long z = host_fs.entropy; + z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ull; + z = (z ^ (z >> 27)) * 0x94d049bb133111ebull; + dst[i] = (unsigned char)((z ^ (z >> 31)) & 0xff); + } + *text = NULL; + return HOST_FS_OK; +} + +int kore_lua_host_random_available(void) { return host_fs.entropy_available; } + +int kore_lua_host_uptime_ns(unsigned long long *out, const char **text) { + static unsigned long long tick; + tick += 1000000ull; /* a monotonic counter, one millisecond a read */ + *out = tick; + *text = NULL; + return HOST_FS_OK; +} + +int kore_lua_host_wall_ns(unsigned long long *out, const char **text) { + *out = 1700000000ull * 1000000000ull; /* 2023-11-14T22:13:20Z */ + *text = NULL; + return HOST_FS_OK; +} + +static int host_fs_do_open(unsigned long long flags, const char *path, + size_t path_len, unsigned long long *result, + unsigned long long *result2, const char **text, + size_t *text_len) { + if (path_len == 0 || path_len >= HOST_FS_PATH_MAX) + return host_fs_refuse("invalid-name", text, text_len); + int at = host_fs_find(path, path_len); + if (flags & HOST_FS_O_DIRECTORY) { + if (at >= 0) return host_fs_refuse("already-exists", text, text_len); + if (!host_fs_parent_ok(path, path_len)) + return host_fs_refuse("not-found", text, text_len); + if (host_fs_make(path, path_len, 1) < 0) + return host_fs_refuse("out-of-storage", text, text_len); + *result = 0; + *result2 = 0; + return HOST_FS_OK; + } + if (at >= 0 && (flags & HOST_FS_O_EXCLUSIVE)) + return host_fs_refuse("already-exists", text, text_len); + if (at < 0) { + if (!(flags & HOST_FS_O_CREATE)) + return host_fs_refuse("not-found", text, text_len); + if (!host_fs_parent_ok(path, path_len)) + return host_fs_refuse("not-found", text, text_len); + at = host_fs_make(path, path_len, 0); + if (at < 0) return host_fs_refuse("out-of-storage", text, text_len); + } + if (host_fs.objects[at].directory) + return host_fs_refuse("is-directory", text, text_len); + if (flags & HOST_FS_O_TRUNCATE) host_fs.objects[at].len = 0; + for (int i = 0; i < HOST_FS_MAX_HANDLES; i++) { + if (host_fs.handles[i].used) continue; + host_fs.handles[i].object = at; + host_fs.handles[i].generation = host_fs.next_generation++; + host_fs.handles[i].readable = (flags & HOST_FS_O_READ) != 0; + host_fs.handles[i].writable = (flags & HOST_FS_O_WRITE) != 0; + host_fs.handles[i].append = (flags & HOST_FS_O_APPEND) != 0; + host_fs.handles[i].used = 1; + *result = host_fs_encode(i, host_fs.handles[i].generation); + *result2 = (flags & HOST_FS_O_TRUNCATE) + ? 0 : (unsigned long long)host_fs.objects[at].len; + return HOST_FS_OK; + } + return host_fs_refuse("out-of-handles", text, text_len); +} + +int kore_lua_host_call(unsigned op, unsigned long long arg0, + unsigned long long arg1, unsigned long long arg2, + const void *input, size_t input_len, + unsigned long long *result, unsigned long long *result2, + const char **text, size_t *text_len) { + const char *path = (const char *)input; + *result = 0; + *result2 = 0; + host_fs_reply(NULL, 0, text, text_len); + + switch (op) { + case HOST_FS_OP_SLEEP: + if (host_fs.refuse_sleep) + return host_fs_refuse("the timer is busy", text, text_len); + host_fs.slept_ns += arg0; + return HOST_FS_OK; + + case HOST_FS_OP_PROC_ARGS: + host_fs_reply(host_fs.args, strlen(host_fs.args), text, text_len); + return HOST_FS_OK; + + case HOST_FS_OP_PROC_EXIT: + host_fs.exit_requested = 1; + host_fs.exit_code = (int)arg0; + return HOST_FS_OK; + + case HOST_FS_OP_OPEN: + return host_fs_do_open(arg0, path, input_len, result, result2, text, + text_len); + + case HOST_FS_OP_CLOSE: { + struct host_fs_handle *slot = host_fs_resolve(arg0); + if (slot == NULL) return host_fs_refuse("invalid-handle", text, text_len); + slot->used = 0; + if (host_fs.refuse_close) + return host_fs_refuse("access-lost", text, text_len); + return HOST_FS_OK; + } + + case HOST_FS_OP_READ: { + struct host_fs_handle *slot = host_fs_resolve(arg0); + if (slot == NULL) return host_fs_refuse("invalid-handle", text, text_len); + if (!slot->readable) + return host_fs_refuse("access-denied", text, text_len); + if (host_fs.reads_armed) { + if (host_fs.reads_allowed == 0) + return host_fs_refuse("access-lost", text, text_len); + host_fs.reads_allowed--; + } + struct host_fs_object *object = &host_fs.objects[slot->object]; + unsigned long long offset = arg1; + size_t want = (size_t)(arg2 < HOST_FS_READ_MAX ? arg2 : HOST_FS_READ_MAX); + if (offset >= object->len) { *result = 0; return HOST_FS_OK; } + size_t available = object->len - (size_t)offset; + if (want > available) want = available; + host_fs_reply(object->bytes + offset, want, text, text_len); + *result = want; + *result2 = want; + return HOST_FS_OK; + } + + case HOST_FS_OP_WRITE: { + struct host_fs_handle *slot = host_fs_resolve(arg0); + if (slot == NULL) return host_fs_refuse("invalid-handle", text, text_len); + if (!slot->writable) + return host_fs_refuse("access-denied", text, text_len); + struct host_fs_object *object = &host_fs.objects[slot->object]; + /* An append handle writes at the end as it is *now*, which is what + src/files.rs does — it re-stats rather than trusting the offset. */ + size_t at = slot->append ? object->len : (size_t)arg1; + if (at > HOST_FS_BYTES_MAX || input_len > HOST_FS_BYTES_MAX - at) + return host_fs_refuse("out-of-storage", text, text_len); + if (at > object->len) + memset(object->bytes + object->len, 0, at - object->len); + if (input_len != 0) memcpy(object->bytes + at, input, input_len); + if (at + input_len > object->len) object->len = at + input_len; + *result = input_len; + *result2 = at + input_len; + return HOST_FS_OK; + } + + case HOST_FS_OP_SYNC: { + struct host_fs_handle *slot = host_fs_resolve(arg0); + if (slot == NULL) return host_fs_refuse("invalid-handle", text, text_len); + return HOST_FS_OK; + } + + case HOST_FS_OP_RESIZE: { + struct host_fs_handle *slot = host_fs_resolve(arg0); + if (slot == NULL) return host_fs_refuse("invalid-handle", text, text_len); + if (!slot->writable) + return host_fs_refuse("access-denied", text, text_len); + struct host_fs_object *object = &host_fs.objects[slot->object]; + size_t length = (size_t)arg1; + if (length > HOST_FS_BYTES_MAX) + return host_fs_refuse("out-of-storage", text, text_len); + if (length > object->len) + memset(object->bytes + object->len, 0, length - object->len); + object->len = length; + *result = length; + return HOST_FS_OK; + } + + case HOST_FS_OP_STAT: { + struct host_fs_object *object; + if (input_len == 0) { + struct host_fs_handle *slot = host_fs_resolve(arg0); + if (slot == NULL) + return host_fs_refuse("invalid-handle", text, text_len); + object = &host_fs.objects[slot->object]; + } else { + int at = host_fs_find(path, input_len); + if (at < 0) return host_fs_refuse("not-found", text, text_len); + object = &host_fs.objects[at]; + } + *result = object->len; + *result2 = (unsigned long long)object->directory; + return HOST_FS_OK; + } + + case HOST_FS_OP_REMOVE: { + /* The root has no parent to mutate; src/files.rs refuses it before + the store ever sees it, and so does this. */ + if (input_len == 0 || (input_len == 1 && path[0] == '/')) + return host_fs_refuse("access-denied", text, text_len); + int at = host_fs_find(path, input_len); + if (at < 0) return host_fs_refuse("not-found", text, text_len); + if (host_fs.objects[at].directory) { + for (int i = 0; i < HOST_FS_MAX_OBJECTS; i++) { + if (!host_fs.objects[i].used || i == at) continue; + char parent[HOST_FS_PATH_MAX]; + size_t parent_len = 0; + host_fs_parent(host_fs.objects[i].path, + strlen(host_fs.objects[i].path), parent, &parent_len); + if (parent_len == input_len && memcmp(parent, path, input_len) == 0) + return host_fs_refuse("not-empty", text, text_len); + } + } + host_fs.objects[at].used = 0; + return HOST_FS_OK; + } + + case HOST_FS_OP_RENAME: { + /* Two paths in one window, NUL-separated. */ + size_t cut = 0; + while (cut < input_len && path[cut] != '\0') cut++; + if (cut == input_len) + return host_fs_refuse("invalid-name", text, text_len); + const char *to = path + cut + 1; + size_t to_len = input_len - cut - 1; + int at = host_fs_find(path, cut); + if (at < 0) return host_fs_refuse("not-found", text, text_len); + if (to_len == 0 || to_len >= HOST_FS_PATH_MAX) + return host_fs_refuse("invalid-name", text, text_len); + if (!host_fs_parent_ok(to, to_len)) + return host_fs_refuse("not-found", text, text_len); + int existing = host_fs_find(to, to_len); + if (existing >= 0) host_fs.objects[existing].used = 0; + memcpy(host_fs.objects[at].path, to, to_len); + host_fs.objects[at].path[to_len] = '\0'; + return HOST_FS_OK; + } + + case HOST_FS_OP_LIST: { + int at = host_fs_find(path, input_len); + if (at < 0) return host_fs_refuse("not-found", text, text_len); + if (!host_fs.objects[at].directory) + return host_fs_refuse("not-directory", text, text_len); + unsigned char names[HOST_FS_READ_MAX]; + size_t len = 0; + unsigned long long count = 0; + for (int i = 0; i < HOST_FS_MAX_OBJECTS; i++) { + if (!host_fs.objects[i].used || i == at) continue; + char parent[HOST_FS_PATH_MAX]; + size_t parent_len = 0; + size_t whole = strlen(host_fs.objects[i].path); + host_fs_parent(host_fs.objects[i].path, whole, parent, &parent_len); + if (parent_len != input_len || memcmp(parent, path, input_len) != 0) + continue; + size_t cut = whole; + while (cut > 0 && host_fs.objects[i].path[cut - 1] != '/') cut--; + size_t leaf = whole - cut; + if (len + leaf + 1 > sizeof names) + return host_fs_refuse("range", text, text_len); + memcpy(names + len, host_fs.objects[i].path + cut, leaf); + len += leaf; + names[len++] = '\0'; + count++; + } + host_fs_reply(names, len, text, text_len); + *result = count; + *result2 = len; + return HOST_FS_OK; + } + + default: + host_fs_reply("unknown host call", sizeof "unknown host call" - 1, text, + text_len); + return HOST_FS_UNAVAILABLE; + } + (void)arg2; +} diff --git a/crates/lua/tests/host_smoke.c b/crates/lua/tests/host_smoke.c index 7e76fe0..db51f15 100644 --- a/crates/lua/tests/host_smoke.c +++ b/crates/lua/tests/host_smoke.c @@ -7,6 +7,11 @@ #include "lua.h" #include "lauxlib.h" +/* The in-memory store behind the real host transport. Included rather + than compiled separately so both this harness's builds — libc numeric + and target numeric — get the same store. */ +#include "host_fs.c" + /* The engine's real arity. This prototype MUST match src/kore_lua.c: a short one leaves the library array and its count as whatever the call's argument registers happened to hold, which `luaopen_kore` then walks. */ @@ -127,6 +132,493 @@ static int protected_setup_smoke(void) { return 0; } +/* + * The fault hooks, reachable from Lua as `kore.test.*`. + * + * Registered through the same `libraries` array an embedder uses, so the + * suite also proves that path: a name and an opener handed to + * `kore_lua_new` land beneath `kore` and list themselves in `kore.caps()`. + */ +static int test_fail_reads_after(lua_State *L) { + host_fs_fail_reads_after((int)luaL_checkinteger(L, 1)); + return 0; +} + +static int test_fail_closes(lua_State *L) { + host_fs_set_refuse_close(lua_toboolean(L, 1)); + return 0; +} + +static int test_open_library(lua_State *L) { + static const luaL_Reg entries[] = { + {"fail_reads_after", test_fail_reads_after}, + {"fail_closes", test_fail_closes}, + {NULL, NULL} + }; + luaL_newlib(L, entries); + return 1; +} + +static const kore_lua_library test_libraries[] = { + {"test", test_open_library} +}; + +/* + * Run a chunk and report the Lua error on failure, so a broken assertion + * names itself instead of arriving as a numeric exit code. + */ +static int run_reporting(lua_State *L, const char *source, const char *what) { + if (kore_lua_load(L, source, strlen(source), "=host-smoke") != LUA_OK || + kore_lua_call(L, 0, LUA_MULTRET) != LUA_OK) { + const char *why = lua_tostring(L, -1); + fprintf(stderr, "%s: %s\n", what, why != NULL ? why : "(no message)"); + return 0; + } + return 1; +} + +/* + * The filesystem surface, over the in-memory store in tests/host_fs.c. + * + * Everything here needs a host to answer, which is why it is a separate + * pass: the selftest below runs with whatever the weak stubs do, and this + * one runs the same engine against a store that actually holds bytes. + * Between them they cover `io`, `os` and `kore.fs` end to end. + */ +static int filesystem_smoke(void) { + static const char suite[] = + /* ---------------------------------------------- open, write, read */ + "local f = assert(io.open('/hello.txt','w'))\n" + "assert(io.type(f)=='file')\n" + "assert(f:write('hello ')==f)\n" + "assert(f:write('world\\n')==f)\n" + "assert(f:close()==true)\n" + "assert(io.type(f)=='closed file')\n" + /* A second close is idempotent, never an error (rule 4). */ + "assert(f:close()==true)\n" + /* A closed file raises on use: a programming error, not a failure. */ + "assert(not pcall(f.read, f, 'a'))\n" + + "local r = assert(io.open('/hello.txt','r'))\n" + "assert(r:read('a')=='hello world\\n')\n" + "assert(r:seek('set')==0)\n" + "assert(r:read('l')=='hello world')\n" + "assert(r:read('l')==nil)\n" + "assert(r:seek('set')==0)\n" + "assert(r:read('L')=='hello world\\n')\n" + "assert(r:seek('set',6)==6)\n" + "assert(r:read(5)=='world')\n" + "assert(r:seek('cur')==11)\n" + "assert(r:seek('end')==12)\n" + "assert(r:seek('set',0)==0)\n" + "assert(r:read(0)=='')\n" + "assert(r:seek('end')==12 and r:read(0)==nil)\n" + /* A negative resulting position is a refusal, not a wrap. */ + "local ok,why,err = r:seek('set',-1)\n" + "assert(ok==nil and why=='range' and err==34)\n" + "assert(r:close()==true)\n" + + /* ------------------------------------------------ the `a` append mode */ + "local a = assert(io.open('/hello.txt','a'))\n" + /* An append stream starts at the end, so a seek before the first + write reports where those bytes will actually land. */ + "assert(a:seek('cur')==12)\n" + "assert(a:write('again\\n'))\n" + "assert(a:close())\n" + "assert(kore.fs.read_file('/hello.txt')=='hello world\\nagain\\n')\n" + + /* ----------------------------------------------------- update modes */ + "local u = assert(io.open('/update.txt','w+'))\n" + "assert(u:write('abcdef'))\n" + "assert(u:seek('set',2)==2)\n" + "assert(u:read(2)=='cd')\n" + "assert(u:seek('set',0)==0)\n" + "assert(u:write('ZY'))\n" + "assert(u:seek('set',0)==0 and u:read('a')=='ZYcdef')\n" + "assert(u:close())\n" + /* `r+` reads and writes but does not create. */ + "assert(io.open('/absent.txt','r+')==nil)\n" + "local p = assert(io.open('/update.txt','r+'))\n" + "assert(p:read(2)=='ZY')\n" + "assert(p:close())\n" + /* A mode Lua does not define is an argument error, not a silent open. */ + "assert(not pcall(io.open,'/update.txt','q'))\n" + "assert(not pcall(io.open,'/update.txt',''))\n" + /* `b` is accepted and means nothing here. */ + "assert(io.open('/update.txt','rb')):close()\n" + + /* ------------------------------------------------- the two spellings */ + /* One failure, two renderings, both derived from the same FsError. */ + "local h,why,err = io.open('/nowhere.txt','r')\n" + "assert(h==nil and why=='not-found' and err==2)\n" + "local g,e = kore.fs.open('/nowhere.txt','r')\n" + "assert(g==nil and e.code=='not-found' and e.kind=='refused')\n" + "assert(tostring(e)=='not-found')\n" + /* The reply the bridge hands back is length-delimited and carries no + terminator; the store poisons the byte after it. An exact match + here is what proves nothing above reached for `strlen`. */ + "assert(#why==9)\n" + + /* ------------------------------------------------- read-only handles */ + "local ro = assert(io.open('/hello.txt','r'))\n" + "local ok2,why2,err2 = ro:write('x')\n" + "assert(ok2==nil and why2=='access-denied' and err2==13)\n" + "assert(ro:close())\n" + "local wo = assert(io.open('/writeonly.txt','w'))\n" + "local ok3,why3 = wo:read('a')\n" + "assert(ok3==nil and why3=='access-denied')\n" + "assert(wo:close())\n" + + /* ------------------------------------------------- multi-page reads */ + /* Larger than the store's single-reply cap, so the read-ahead in + kore_lua.c must loop rather than assume one refill is enough. */ + "local big = assert(io.open('/big.txt','w'))\n" + "local chunk = string.rep('0123456789', 100)\n" + "for _ = 1, 10 do assert(big:write(chunk)) end\n" + "assert(big:close())\n" + "local rb = assert(io.open('/big.txt','r'))\n" + "local whole = rb:read('a')\n" + "assert(#whole==10000, #whole)\n" + "assert(whole==string.rep(chunk,10))\n" + "assert(rb:seek('set')==0)\n" + "assert(#rb:read(6000)==6000)\n" + "assert(rb:close())\n" + + /* ------------------------------------------------------ read formats */ + "assert(kore.fs.write_file('/nums.txt','12 -3.5 0x10\\nrest\\n')>0)\n" + "local n = assert(io.open('/nums.txt','r'))\n" + "assert(n:read('n')==12)\n" + "assert(n:read('n')==-3.5)\n" + "assert(n:read('n')==16)\n" + "assert(n:read('l')=='')\n" + "assert(n:read('l')=='rest')\n" + "assert(n:close())\n" + /* Several formats in one call, and the 5.2 star spelling. */ + "local m = assert(io.open('/hello.txt','r'))\n" + "local one,two = m:read(5,'*l')\n" + "assert(one=='hello' and two==' world')\n" + "assert(m:close())\n" + + /* ------------------------------------------------------------ lines */ + "assert(kore.fs.write_file('/lines.txt','a\\nb\\nc\\n')>0)\n" + "local seen = {}\n" + "for line in io.lines('/lines.txt') do seen[#seen+1] = line end\n" + "assert(#seen==3 and seen[1]=='a' and seen[3]=='c')\n" + /* io.lines(name) answers four values, the last to-be-closed. */ + "local n1,n2,n3,n4 = io.lines('/lines.txt')\n" + "assert(type(n1)=='function' and n2==nil and n3==nil)\n" + "assert(io.type(n4)=='file' and n4:close())\n" + "local it,_,_,tbc = io.lines('/lines.txt')\n" + "assert(io.type(tbc)=='file' and it()=='a')\n" + "assert(tbc:close())\n" + /* f:lines leaves the file open, and closing it stops the iterator. */ + "local lf = assert(io.open('/lines.txt','r'))\n" + "local step = lf:lines('L')\n" + "assert(step()=='a\\n')\n" + "assert(lf:close())\n" + "assert(not pcall(step))\n" + + /* ------------------------------------------------ default input/output */ + "local infile = io.input('/lines.txt')\n" + "assert(io.type(infile)=='file')\n" + "assert(io.read('l')=='a')\n" + "assert(io.read('l')=='b')\n" + "io.input(io.stdin)\n" + "assert(io.read('l')==nil)\n" + "assert(infile:close())\n" + + /* -------------------------------------------------------- kore.fs */ + "assert(kore.fs.exists('/hello.txt'))\n" + "assert(not kore.fs.exists('/nowhere.txt'))\n" + "local st = assert(kore.fs.stat('/hello.txt'))\n" + "assert(st.size_bytes==18 and st.kind=='file' and st.directory==false)\n" + "assert(kore.fs.mkdir('/dir'))\n" + "local dst = assert(kore.fs.stat('/dir'))\n" + "assert(dst.kind=='directory' and dst.directory==true)\n" + /* A directory that exists is `already-exists`, not a silent success. */ + "local md,mderr = kore.fs.mkdir('/dir')\n" + "assert(md==nil and mderr.code=='already-exists')\n" + "assert(kore.fs.write_file('/dir/one','1')==1)\n" + "assert(kore.fs.write_file('/dir/two','22')==2)\n" + "local names = assert(kore.fs.list('/dir'))\n" + "table.sort(names)\n" + "assert(#names==2 and names[1]=='one' and names[2]=='two')\n" + /* A non-empty directory is not removable. */ + "local rm,rmerr = kore.fs.remove('/dir')\n" + "assert(rm==nil and rmerr.code=='not-empty')\n" + "assert(kore.fs.remove('/dir/one'))\n" + "assert(kore.fs.rename('/dir/two','/dir/three'))\n" + "assert(kore.fs.read_file('/dir/three')=='22')\n" + "assert(kore.fs.remove('/dir/three'))\n" + "assert(kore.fs.remove('/dir'))\n" + "assert(not kore.fs.exists('/dir'))\n" + /* `read_file` on a missing path answers the kore spelling. */ + "local rf,rferr = kore.fs.read_file('/nowhere.txt')\n" + "assert(rf==nil and rferr.code=='not-found')\n" + /* The root has no parent to mutate. */ + "local root,rooterr = kore.fs.remove('/')\n" + "assert(root==nil and rooterr.code=='access-denied')\n" + + /* ------------------------------------------------------ kore.fs.resize */ + "local rz = assert(kore.fs.open('/resize.txt','w+'))\n" + "assert(rz:write('abcdefgh'))\n" + "assert(kore.fs.resize(rz,3))\n" + /* A truncation drags the position back with it. */ + "assert(rz:seek('cur')==3)\n" + "assert(rz:seek('set')==0 and rz:read('a')=='abc')\n" + "assert(kore.fs.resize(rz,5))\n" + "assert(rz:seek('end')==5)\n" + "assert(not pcall(kore.fs.resize, rz, -1))\n" + "assert(rz:close())\n" + + /* ----------------------------------------------------------- os verbs */ + "assert(kore.fs.write_file('/gone.txt','x')==1)\n" + "assert(os.remove('/gone.txt')==true)\n" + "local orm,ormwhy,ormerr = os.remove('/gone.txt')\n" + "assert(orm==nil and ormwhy=='not-found' and ormerr==2)\n" + "assert(kore.fs.write_file('/from.txt','y')==1)\n" + "assert(os.rename('/from.txt','/to.txt')==true)\n" + "assert(kore.fs.read_file('/to.txt')=='y')\n" + "assert(os.remove('/to.txt'))\n" + + /* --------------------------------------------------------- temporaries */ + /* os.tmpname creates its file, so the name it answers is one nothing + else can already hold. */ + "local name = assert(os.tmpname())\n" + "assert(name:sub(1,9)=='/tmp/lua_')\n" + "assert(kore.fs.exists(name))\n" + "local name2 = assert(os.tmpname())\n" + "assert(name2~=name)\n" + "assert(os.remove(name) and os.remove(name2))\n" + /* io.tmpfile removes its own file when the handle retires. */ + "local tmp = assert(io.tmpfile())\n" + "assert(tmp:write('scratch'))\n" + "assert(tmp:seek('set')==0 and tmp:read('a')=='scratch')\n" + "assert(tmp:close())\n" + + /* ------------------------------------------------------- kore.proc */ + "assert(type(kore.proc.args)=='table')\n" + "assert(kore.proc.args[1]=='one' and kore.proc.args[2]=='two')\n" + "assert(#kore.proc.args==2)\n" + /* A missing key answers nil, as an ordinary table does. */ + "assert(kore.proc.nothing==nil and kore.proc[1]==nil)\n" + + /* ------------------------------------------------------- kore.time */ + "assert(kore.time.sleep(0.001)==true)\n" + "assert(type(kore.time.now())=='number')\n" + "assert(math.type(kore.time.mono_ns())=='integer')\n" + "assert(kore.time.wall()==1700000000.0)\n" + "assert(not pcall(kore.time.sleep,-1))\n" + /* os.time with no argument reads the same wall clock. */ + "assert(os.time()==1700000000)\n" + "assert(os.date('!%Y-%m-%dT%H:%M:%SZ')=='2023-11-14T22:13:20Z')\n" + + /* ----------------------------------------------------- kore.random */ + "assert(kore.random.available()==true)\n" + "assert(#kore.random.bytes(32)==32)\n" + "assert(#kore.random.bytes(0)==0)\n" + "assert(not pcall(kore.random.bytes,-1))\n" + "assert(not pcall(kore.random.bytes,4097))\n" + "local lo = kore.random.integer(5,5)\n" + "assert(lo==5)\n" + "for _ = 1, 50 do\n" + " local v = kore.random.integer(-3,3)\n" + " assert(v>=-3 and v<=3, v)\n" + "end\n" + "assert(not pcall(kore.random.integer,3,-3))\n" + + /* --------------------------------------------------------- kore.caps */ + "local caps = kore.caps()\n" + "assert(caps[1]=='time' and caps[2]=='random' and caps[3]=='fs')\n" + "assert(caps[4]=='proc' and caps[5]=='test')\n" + "assert(#caps==5 and type(kore.test)=='table')\n" + "assert(kore.has('fs')==true and kore.has('nothing')==false)\n" + + /* ------------------------------------------------ the `__gc` backstop */ + /* Deterministic close is primary, but a script that simply drops a + file must still give the store its handle back (rule 4). The + open-handle count the harness checks next is what proves it. */ + "do local abandoned = assert(io.open('/hello.txt','r'))\n" + " assert(abandoned:read(1)=='h') end\n" + "collectgarbage('collect')\n" + "collectgarbage('collect')\n" + "return true\n"; + + /* + * A refused call renders through `kore_lua_fail`'s concatenating arm. + * The store poisons the byte past its reply, so an exact match is what + * proves the refusal was not spelled with `%s` over a buffer that has + * no terminator. + */ + static const char refusal_suite[] = + "local ok,why = kore.time.sleep(0.5)\n" + "assert(ok==nil, 'the sleep should have been refused')\n" + "assert(why=='refused: the timer is busy', why)\n" + "return true\n"; + + /* + * A read that fails partway answers the failure alone, as stock `g_read` + * does — not a partial result with a failure stapled to the end. + */ + static const char read_failure_suite[] = + /* A failure on the very first refill: the whole answer is the + failure triple, in `io`'s spelling. */ + "local f = assert(io.open('/hello.txt','r'))\n" + "kore.test.fail_reads_after(0)\n" + "local a,b,c,d = f:read('l','l')\n" + "assert(a==nil, 'a failed read answers nil first')\n" + "assert(b=='access-lost', tostring(b))\n" + "assert(c==9, tostring(c))\n" + "assert(d==nil, 'the failure is the whole answer')\n" + "kore.test.fail_reads_after(-1)\n" + "assert(f:close())\n" + /* A failure *after* a partial read, which is the interesting one: + 6000 bytes needs two refills, so by the time the third is refused + the `luaL_Buffer` has moved onto the stack as a box. The failure + must replace the partial result rather than trail it, and must not + be pushed above the box. */ + "local big = assert(io.open('/big.txt','r'))\n" + "kore.test.fail_reads_after(2)\n" + "local e,g,h,i = big:read(6000,'l')\n" + "assert(e==nil, 'the failure replaces the partial read')\n" + "assert(g=='access-lost', tostring(g))\n" + "assert(h==9, tostring(h))\n" + "assert(i==nil, 'nothing trails the failure')\n" + "kore.test.fail_reads_after(-1)\n" + "assert(big:close())\n" + /* The same shape through `read('a')`, whose buffer is the largest. */ + "local all = assert(io.open('/big.txt','r'))\n" + "kore.test.fail_reads_after(1)\n" + "local j,k = all:read('a')\n" + "assert(j==nil and k=='access-lost', tostring(k))\n" + "kore.test.fail_reads_after(-1)\n" + "assert(all:close())\n" + /* A file `kore.fs.open` answered is the *same* object, so its methods + render the same way: only the two surfaces' own verbs differ, and + `file:read` is one of `io`'s seven. `kore.fs.open`'s failure is a + table; `f:read`'s is a message and an errno, whichever surface + opened the file. */ + "local kf = assert(kore.fs.open('/big.txt','r'))\n" + "kore.test.fail_reads_after(0)\n" + "local l,m,n2 = kf:read('a')\n" + "assert(l==nil and m=='access-lost' and n2==9, tostring(m))\n" + "kore.test.fail_reads_after(-1)\n" + "assert(kf:close())\n" + /* A read failure inside `io.lines` raises, as `io_readline` does: a + `for` loop that merely stopped would read as end of file. */ + "local lines = io.lines('/big.txt')\n" + "kore.test.fail_reads_after(0)\n" + "local raised = select(2, pcall(lines))\n" + "kore.test.fail_reads_after(-1)\n" + "assert(type(raised)=='string' and raised:find('access%-lost'), \n" + " tostring(raised))\n" + + /* ------------------------------------------- a close that also fails */ + /* `kore.fs.read_file` opens, reads and retires the handle itself. + A refused close pushes two values where success pushes one, so a + caller that popped a fixed count would hand back the wrong thing + entirely — the file object, or the error table, instead of the + bytes it read. */ + "kore.test.fail_closes(true)\n" + "local body, bodyerr = kore.fs.read_file('/hello.txt')\n" + "assert(body=='hello world\\nagain\\n', tostring(body))\n" + "assert(bodyerr==nil, 'the close is not the caller\\'s answer')\n" + /* And when the read failed too, the read's failure is what survives. */ + "kore.test.fail_reads_after(0)\n" + "local none, noneerr = kore.fs.read_file('/hello.txt')\n" + "assert(none==nil, tostring(none))\n" + "assert(type(noneerr)=='table' and noneerr.code=='access-lost',\n" + " tostring(noneerr))\n" + "kore.test.fail_reads_after(-1)\n" + /* A refused close on the file object answers the refusal itself, and + the handle is retired either way: a re-close is still idempotent. */ + "local rc = assert(io.open('/hello.txt','r'))\n" + "local cok, cwhy, cerr = rc:close()\n" + "assert(cok==nil and cwhy=='access-lost' and cerr==9, tostring(cwhy))\n" + "assert(rc:close()==true)\n" + "kore.test.fail_closes(false)\n" + + /* ------------------------------------ a temporary draw that cannot land */ + /* With no `/tmp` to create in, the exclusive open answers `not-found` + and both verbs render that reason exactly. Exact is the point: the + store poisons the byte after its reply, so a `strlen` over it would + answer a longer, garbled string. */ + "assert(kore.fs.remove('/tmp'))\n" + "local tn, tnwhy = os.tmpname()\n" + "assert(tn==nil and tnwhy=='not-found', tostring(tnwhy))\n" + "local tf, tfwhy, tferr = io.tmpfile()\n" + "assert(tf==nil and tfwhy=='not-found' and tferr==2, tostring(tfwhy))\n" + "assert(kore.fs.mkdir('/tmp'))\n" + "return true\n"; + + /* `os.exit` records its code and unwinds; it is not an error. */ + static const char exit_suite[] = + "local closed = false\n" + "do\n" + " local guard = setmetatable({}, {__close=function() closed=true end})\n" + " os.exit(7)\n" + "end\n" + "error('os.exit did not unwind')\n"; + + host_fs_reset("one two"); + struct capture capture = {{0}, 0}; + lua_State *L = kore_lua_new(host_alloc, NULL, 0x4b4f5245u, test_libraries, + sizeof test_libraries / sizeof test_libraries[0]); + if (L == NULL) return 1; + if (kore_lua_install_print(L, capture_append, &capture) != LUA_OK) { + kore_lua_close(L); return 2; + } + if (!run_reporting(L, suite, "filesystem suite")) { kore_lua_close(L); return 3; } + kore_lua_set_top(L, 0); + + /* Every handle the suite opened was closed: a leak here means a verb + kept one the script can no longer name. */ + if (host_fs_open_handles() != 0) { kore_lua_close(L); return 4; } + /* io.tmpfile's file went with its handle, and os.tmpname's did not. */ + if (host_fs_exists("/tmp/lua_")) { kore_lua_close(L); return 5; } + + host_fs_set_refuse_sleep(1); + if (!run_reporting(L, refusal_suite, "refusal rendering")) { + kore_lua_close(L); return 6; + } + host_fs_set_refuse_sleep(0); + kore_lua_set_top(L, 0); + + if (!run_reporting(L, read_failure_suite, "read failure")) { + kore_lua_close(L); return 7; + } + kore_lua_set_top(L, 0); + kore_lua_close(L); + + /* A fresh VM: the exit suite ends its own evaluation. */ + host_fs_reset(""); + struct capture exit_capture = {{0}, 0}; + L = kore_lua_new(host_alloc, NULL, 0x4b4f5245u, NULL, 0); + if (L == NULL) return 8; + if (kore_lua_install_print(L, capture_append, &exit_capture) != LUA_OK) { + kore_lua_close(L); return 9; + } + if (kore_lua_load(L, exit_suite, strlen(exit_suite), "=exit") != LUA_OK) { + kore_lua_close(L); return 10; + } + /* The sentinel reaches the host as an error — that is how it ran the + `` handler on its way out. src/vm.rs is what recognizes it; + here the contract under test is that the code was recorded and the + raise carries the sentinel the host matches on. */ + if (kore_lua_call(L, 0, LUA_MULTRET) == LUA_OK) { kore_lua_close(L); return 11; } + { + const char *why = lua_tostring(L, -1); + if (why == NULL || strstr(why, "kore:exit") == NULL) { + kore_lua_close(L); return 12; + } + } + if (!host_fs_exit_requested() || host_fs_exit_code() != 7) { + kore_lua_close(L); return 13; + } + kore_lua_close(L); + return 0; +} + #if defined(KORE_LUA_USE_TARGET_NUMERIC) int kore_lua_host_smoke(void) { #else @@ -231,7 +723,10 @@ int main(void) { "assert(type(raised)=='string' and raised:find('cannot open file'),\n" " tostring(raised))\n" "assert(not pcall(io.input,'/nowhere'))\n" - "assert(not pcall(io.output,'/nowhere'))\n" + /* `io.output` opens for writing, so it fails only where it cannot + create: a parent that is not there. */ + "assert(not pcall(io.output,'/nodir/file'))\n" + "assert(not pcall(io.lines,'/nodir/file'))\n" "assert(io.type(42)==nil and io.type('x')==nil)\n" "assert(io.stdin:read('a')=='')\n" "assert(io.stdin:read('l')==nil)\n" @@ -255,6 +750,21 @@ int main(void) { "assert(os.date('%I',0)=='12' and os.date('%I',43200)=='12')\n" "assert(os.date('%%',0)=='%')\n" "assert(not pcall(os.date,'%Q',0))\n" + /* The calendar states its range instead of truncating a huge year + into a plausible-looking date: the broken-down year is an `int` + and `%F` spells four digits. The bounds are the first and last + instants of years 0000 and 9999. */ + "assert(os.date('%F',253402300799)=='9999-12-31')\n" + "assert(os.date('%T',253402300799)=='23:59:59')\n" + "assert(os.date('%F',-62167219200)=='0000-01-01')\n" + "local over = select(2, pcall(os.date,'%F',253402300800))\n" + "assert(over:find('cannot be represented'), tostring(over))\n" + "assert(not pcall(os.date,'%F',-62167219201))\n" + "assert(not pcall(os.date,'*t',1e18))\n" + "assert(not pcall(os.date,'%F',-1e18))\n" + "assert(not pcall(os.time,{year=99999,month=1,day=1,hour=0}))\n" + "assert(not pcall(os.time,{year=2023,month=1,day=1e15,hour=0}))\n" + "assert(not pcall(os.time,{year=-5,month=1,day=1,hour=0}))\n" /* `*t` and `os.time` are exact inverses, which is what makes date arithmetic by field addition work. */ "local t=os.date('*t',1700000000)\n" @@ -286,13 +796,19 @@ int main(void) { "print(6*7)\n"; int setup_status = protected_setup_smoke(); if (setup_status != 0) return 10 + setup_status; + /* A store with nothing in it: the selftest below asserts what an absent + path answers, and the pass after it fills one in. */ + host_fs_reset(""); + int fs_status = filesystem_smoke(); + if (fs_status != 0) return 30 + fs_status; + host_fs_reset(""); struct capture capture = {{0}, 0}; lua_State *L = kore_lua_new(host_alloc, NULL, 0x4b4f5245u, NULL, 0); if (L == NULL) return 1; if (kore_lua_install_print(L, capture_append, &capture) != LUA_OK) { kore_lua_close(L); return 2; } - if (!run(L, selftest, 0)) return 2; + if (!run_reporting(L, selftest, "selftest")) return 2; if (capture.len != 3 || memcmp(capture.bytes, "42\n", 3) != 0) return 3; /* Saved-script regression: a local closure adds mixed integer/float diff --git a/docs/filesystem.md b/docs/filesystem.md index db26946..987112d 100644 --- a/docs/filesystem.md +++ b/docs/filesystem.md @@ -47,5 +47,8 @@ owner-scoped generational handles. It drives one native request at a time, returns completions through a callback, and quarantines an uncertain/cancelled owner until task retirement. It never represents `Drop` as acknowledged cleanup. The callback-only pointer and host integration contracts are specified in -[languages.md](languages.md). Lua/Python adapters still need their own script -lifetime and scheduling integration above this native owner. +[languages.md](languages.md). `kore-lua` supplies that integration for Lua: its +open-file table holds the affine views and Lua holds generational handles, so a +use after close is `invalid-handle` rather than a read of a recycled slot. A +Python adapter still needs its own script lifetime and scheduling integration +above this native owner. diff --git a/docs/languages.md b/docs/languages.md index ced700d..07bd6ae 100644 --- a/docs/languages.md +++ b/docs/languages.md @@ -85,9 +85,12 @@ The context and completion pointers exist only for one callback. They may not be retained, accessed by another thread, used for re-entry, or crossed by unwinding/longjmp. C/Zig callers must supply valid, aligned, non-overlapping buffers. This is the usual memory-unsafe FFI boundary, not an intra-process -sandbox; the process's Kore capabilities remain the security boundary. Lua and -Python adapters can later own this lifecycle instead of exposing these pointers -to scripts. +sandbox; the process's Kore capabilities remain the security boundary. + +`kore-lua` already owns this lifecycle rather than exposing these pointers to a +script: the VM runs on a worker thread and reaches every service through one +request cell served on thread 0, so a script holds a handle — an index and a +generation — and never a native view. A Python adapter can do the same. ## Build and verify From fc0d9633131ad787a5af9b0daf2d9b33c1fba325 Mon Sep 17 00:00:00 2001 From: Julian Avar Date: Tue, 29 Sep 2026 11:26:13 -0400 Subject: [PATCH 03/13] native: lock the re-exported `kore-ld` and `kore-late` members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 9e4172bc2 re-export added both crates to `native/Cargo.toml` without their lockfile entries, so every `--locked` command against the native workspace refused to run — `make test`'s second invocation, and Audit's lockfile-currency gate. Resolution adds `elf 0.8.0`; everything else the two crates want was already locked. Co-Authored-By: Claude Opus 5 (1M context) --- native/Cargo.lock | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/native/Cargo.lock b/native/Cargo.lock index e81240b..6b94cf8 100644 --- a/native/Cargo.lock +++ b/native/Cargo.lock @@ -123,6 +123,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" +[[package]] +name = "elf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55dd888a213fc57e957abf2aa305ee3e8a28dbe05687a251f33b637cd46b0070" + [[package]] name = "embedded-graphics" version = "0.8.2" @@ -273,6 +279,29 @@ dependencies = [ "raw-cpuid", ] +[[package]] +name = "kore-late" +version = "0.1.0" +dependencies = [ + "heapless", + "kore-abi", + "kore-dagfs", + "kore-ld", + "kore-rt", + "zerocopy", +] + +[[package]] +name = "kore-ld" +version = "0.1.1" +dependencies = [ + "bitflags", + "elf", + "heapless", + "kore-abi", + "zerocopy", +] + [[package]] name = "kore-ring" version = "0.0.0" From 4865e0a20d456b006a82150d9e433d603c87b7cf Mon Sep 17 00:00:00 2001 From: Julian Avar Date: Tue, 29 Sep 2026 11:26:13 -0400 Subject: [PATCH 04/13] wasi: Wasmtime 48.0.3 for RUSTSEC-2026-0315 and -0316 Two advisories landed against Wasmtime 47 on 2026-09-24: `call_ref` and exception `catch` can drop fuel accounting (RUSTSEC-2026-0315, medium), and dynamic record lifting can allocate past the hostcall fuel limit (RUSTSEC-2026-0316). Both are fixed in 48.0.3. `cargo audit --deny warnings` fails the Audit workflow until the bump, and it needs no source change: the runtime, component-model and async surfaces this host uses are unchanged across 47 -> 48. Co-Authored-By: Claude Opus 5 (1M context) --- Cargo.lock | 193 +++++++++++++++++++++++------------------ crates/wasi/Cargo.toml | 6 +- 2 files changed, 113 insertions(+), 86 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5005078..8fa0420 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,27 +207,27 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.134.4" +version = "0.135.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c273ba1bfc3a1cb27cb4f83df27134f3cb638a61f5ed79ab22cf86eb13da0d" +checksum = "9d620e7c8e86c48ff33f750fd49a2f3fa39529e6bfe59639dfdfcbf81329f737" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.134.4" +version = "0.135.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf8909326a466739e83ffe11e74c0c8c6b4bfa911b612aece915daa746cff58" +checksum = "75015effc364f66dfda9f45f3d78b5509957303258521143839adfd762eade60" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.134.4" +version = "0.135.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb26b06b54d8b2f8cdf4d440a32fc3b3b91c71c162333a69a70a66fa265fc45" +checksum = "e18dd511b085013add81cd0b5381207451c791c93f8487c800ec71455287446e" dependencies = [ "cranelift-entity", "wasmtime-internal-core", @@ -235,9 +235,9 @@ dependencies = [ [[package]] name = "cranelift-bitset" -version = "0.134.4" +version = "0.135.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ce4e57dbb7f73d08011808a19787c3a03c3e7854b2a479ed0b788915fb671be" +checksum = "a92f504aa9105998333f8416a7d32dac037313e2103649f91a143f383d445fee" dependencies = [ "serde", "serde_derive", @@ -246,9 +246,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.134.4" +version = "0.135.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b0a8968f33d0bc13bc86fe70dd3947e93acc9f358ed9848e9988448972a9be" +checksum = "f26f0f8551262c68d14cbb5c60e805394f38c7492f2a02c3a4592cef40b342e9" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -277,9 +277,9 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.134.4" +version = "0.135.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f69e8e2fe9deb48ef7b8d0c61612f8c1e2277d0421c201e30a4c844a4ebac698" +checksum = "6672eac6d00c944a01712109d376019ab46ff4c3b37bb3419fa350b25523b1ad" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -290,24 +290,24 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.134.4" +version = "0.135.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad4db1e87a65e9c5a832e3ede160830d4e34df938ac38b5604819f8825687e73" +checksum = "2f2bd3116e2cdef47a5755c484fa5feca18bd77ea7db418e492f72faaa7582f2" [[package]] name = "cranelift-control" -version = "0.134.4" +version = "0.135.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9248cd37bb6ec460981f57ead368e32b1eca7a207ad50e3e9c7adc3b161f20e9" +checksum = "9706989f98e3f7f1b1bc16450089c0393b141f79701590f7bf830c3a531454a2" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.134.4" +version = "0.135.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b8bc91c0a3530d132acbf118965dabe9a084029eb0fa74b64760360dec3316" +checksum = "26094b0d1072871e07bcb4f53982744ee42657fb40c2175081a10d71f06f8546" dependencies = [ "cranelift-bitset", "serde", @@ -317,9 +317,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.134.4" +version = "0.135.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5086f2ebc084a98b387e522680f62ef65a512444d8382151e03088716d3a1714" +checksum = "247fa35f45fd4477ca045e23f14b2aafe6bdbe9df3d754b1429e0587b356116b" dependencies = [ "cranelift-codegen", "hashbrown 0.17.1", @@ -330,15 +330,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.134.4" +version = "0.135.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6a658670f779afc2df083be201ebe54805f69b7b9ffa8dab04142b72ed751c9" +checksum = "ad1e07f7c111564a5deda3ca9e4ac2f043f8e1fc844a62d3a3feb2be26b49274" [[package]] name = "cranelift-native" -version = "0.134.4" +version = "0.135.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a41083b1fd953debd6f32d21fcda765b258f323ce907fdd6b4715f2769c478" +checksum = "4d0ee388c00a256fee3241f6c8d93e9b8b4175bac3a5527c1dc80169e047643e" dependencies = [ "cranelift-codegen", "libc", @@ -347,9 +347,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.134.4" +version = "0.135.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "244d9cd0759b0a3ccdd587cafb13434f9ea43b5c9d07701d277daf5cd0f7b7c1" +checksum = "1f739117ccbeec70314433f304ced82abe89e75ff32ab676c2d7da661358af1e" [[package]] name = "crc" @@ -1135,9 +1135,9 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "47.0.4" +version = "48.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bd641cb6a3ec5bffe60405568933163e0ea619da0943e9d2a417ceaa7fe82" +checksum = "76169f68135d6e5b956787c0c20954b40ae30bf60c46dde8e887ee78c1bee63a" dependencies = [ "cranelift-bitset", "log", @@ -1147,9 +1147,9 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "47.0.4" +version = "48.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd4024d1e559eaf5579bc8ad84a2b5915f34b47acc13a06ffba7dac401b166" +checksum = "be2b53c01b72bd94f0a2982f9ef503d5d547b4cace3d771fee05831379e7cac2" dependencies = [ "proc-macro2", "quote", @@ -1646,9 +1646,9 @@ dependencies = [ [[package]] name = "wasm-compose" -version = "0.252.0" +version = "0.254.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59b710751a35d54732a63851cdfacbfb2266b7160ce174faf269d3f4e84e31b" +checksum = "8fd717357bff09b5f1ea49aea8ed1cd149b0c521823a1cc70bc2ac16b90b651a" dependencies = [ "anyhow", "heck", @@ -1656,19 +1656,19 @@ dependencies = [ "log", "petgraph", "smallvec", - "wasm-encoder 0.252.0", - "wasmparser 0.252.0", + "wasm-encoder 0.254.0", + "wasmparser 0.254.0", "wat", ] [[package]] name = "wasm-encoder" -version = "0.252.0" +version = "0.254.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" +checksum = "09480d646178e5fdd12bb06e812d0af9a3a191dbc9cd697fdc86687beade7393" dependencies = [ "leb128fmt", - "wasmparser 0.252.0", + "wasmparser 0.254.0", ] [[package]] @@ -1681,11 +1681,23 @@ dependencies = [ "wasmparser 0.259.0", ] +[[package]] +name = "wasm-metadata" +version = "0.254.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b01df5f3b4ca7881e843f3bc0fb8a3905d79c68692250dcb8e33e698705ccdb6" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.254.0", + "wasmparser 0.254.0", +] + [[package]] name = "wasmparser" -version = "0.252.0" +version = "0.254.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" +checksum = "d5769a29f799fbab136aaf65b4fe5384cd7d93fe6fc9ba0dcb6c8382a1f16e27" dependencies = [ "bitflags", "hashbrown 0.17.1", @@ -1707,27 +1719,26 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.252.0" +version = "0.254.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7142797de29b35ab8dbf15c00f55fda75d409da4c423a8ab8bd6b667a785824b" +checksum = "64e3ba11e024f504698f87b629b2b66c22a1231758de7607754963f7d198be39" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.252.0", + "wasmparser 0.254.0", ] [[package]] name = "wasmtime" -version = "47.0.4" +version = "48.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cdaf5b8af5713146e3a62d940c71e3eb2ad1ebacfa6b55f0c8509a5b4b87718" +checksum = "8ec755a941ae13c7f54210be272d77c4282a271d853558332827d0c8cd5dfba0" dependencies = [ "addr2line", "async-trait", "bitflags", "bumpalo", "cc", - "cfg-if", "encoding_rs", "futures", "fxprof-processed-profile", @@ -1751,8 +1762,8 @@ dependencies = [ "target-lexicon", "tempfile", "wasm-compose", - "wasm-encoder 0.252.0", - "wasmparser 0.252.0", + "wasm-encoder 0.254.0", + "wasmparser 0.254.0", "wasmtime-environ", "wasmtime-internal-cache", "wasmtime-internal-component-macro", @@ -1771,9 +1782,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "47.0.4" +version = "48.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58793271eab7ec26bab99ca497131f8b37702e7dc39e2f2d18293e3e915b4964" +checksum = "9dca6a4a583fe56081cac202f7850b9d987d72bbb6cec8ac6503d3d6d65b165e" dependencies = [ "anyhow", "cpp_demangle", @@ -1793,8 +1804,8 @@ dependencies = [ "sha2", "smallvec", "target-lexicon", - "wasm-encoder 0.252.0", - "wasmparser 0.252.0", + "wasm-encoder 0.254.0", + "wasmparser 0.254.0", "wasmprinter", "wasmtime-internal-component-util", "wasmtime-internal-core", @@ -1802,9 +1813,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-cache" -version = "47.0.4" +version = "48.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede02ba77442cab88a2ddd5d16373e8bc450b55e05de8be039b024987a53d5a7" +checksum = "fde154a8c7afe7611c489fa7a578755e2b8e947a1ce8b70088cf486d43edd0a1" dependencies = [ "base64", "directories-next", @@ -1822,9 +1833,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" -version = "47.0.4" +version = "48.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5bba4eff4804620bae04d7793308d97a0c5a8a97ea19e635452518f84560f84" +checksum = "0363b11ef205e5518739a0e3cd3f54c9b0a2ab6c495e0ba4776a78154722b896" dependencies = [ "anyhow", "proc-macro2", @@ -1837,15 +1848,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" -version = "47.0.4" +version = "48.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7032ff6b4d7a45e05be1cb32ec1756df5f2669f08296dbe2c61c3789dead2c" +checksum = "0b9bfdaa8f3e07e78c8efac1cc8bbbc0af3607d0b412c80e9cea77d18adfa9f1" [[package]] name = "wasmtime-internal-core" -version = "47.0.4" +version = "48.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f41d3b2cb9c3de7b696690af070d408b5e357011832f8b8bb7fbb315dbb620ed" +checksum = "72250d72e27bea7d614af6742ed185d3e9d39de471f33e177c71874a7184586d" dependencies = [ "anyhow", "hashbrown 0.17.1", @@ -1855,11 +1866,10 @@ dependencies = [ [[package]] name = "wasmtime-internal-cranelift" -version = "47.0.4" +version = "48.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d5cda5b90247978dd2ba6ca4e309508da19e9244445671d2d0e6a34e5c3b847" +checksum = "52abc6a4ac7ab272fb1de7ed5040e0b6aa8cc49a74016588cf53f2e27366ad43" dependencies = [ - "cfg-if", "cranelift-codegen", "cranelift-control", "cranelift-entity", @@ -1873,7 +1883,7 @@ dependencies = [ "smallvec", "target-lexicon", "thiserror 2.0.20", - "wasmparser 0.252.0", + "wasmparser 0.254.0", "wasmtime-environ", "wasmtime-internal-core", "wasmtime-internal-unwinder", @@ -1882,12 +1892,11 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "47.0.4" +version = "48.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d126366176553e8f304e8ac2078a4cc37134fe8f19d4d4695a09c5a2f577f10" +checksum = "818599f226d134c04432c4d7a34541687a6d965fbfa9bc73b95a2d4b7ae99c8a" dependencies = [ "cc", - "cfg-if", "libc", "rustix", "wasmtime-environ", @@ -1897,9 +1906,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "47.0.4" +version = "48.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1d28ff131bc40e11d84bc3a9fa0d264506439ffbbc89f3562fbb529d1caa617" +checksum = "0a0c01126eeaa5d432df800b2a1952f21868cc7fb61c4b452dbfcf2fa14ebdb1" dependencies = [ "cc", "object", @@ -1909,11 +1918,10 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "47.0.4" +version = "48.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a52de8b6afbfcd618073f592c1450cb661574e7061f3f4a44f1f7ec0c7f7909" +checksum = "4d42e1fc31e9e23ced0136cf271deb931a7375f54307f90c27cc271740fa938f" dependencies = [ - "cfg-if", "libc", "wasmtime-internal-core", "windows-sys", @@ -1921,11 +1929,10 @@ dependencies = [ [[package]] name = "wasmtime-internal-unwinder" -version = "47.0.4" +version = "48.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5524fbaaf3d2134dd6229164d7fc81e443cce06d1e0576524d5f4e86e747cb13" +checksum = "71f967bac5a11867a374b0d2f0f1febc087ebfdaebd4797a382cbb80d8a3dfb2" dependencies = [ - "cfg-if", "cranelift-codegen", "log", "object", @@ -1934,9 +1941,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "47.0.4" +version = "48.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93bb9ae112a80618510f938275a5d88637b656e7d0a021fc906f52206c708ad" +checksum = "65b0174e50f6f882498eb55ff2dd5c666cf3700ad00a05a608b8f83ec18a03e0" dependencies = [ "proc-macro2", "quote", @@ -1945,22 +1952,23 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" -version = "47.0.4" +version = "48.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdbdd9d6fab1ecb67a7dc189e32c9e341444945fb709437bf8aab25d9a88459" +checksum = "62a5137cc642ddf52082099b6aa9d38bed5105cb2918fe1c8779013842132022" dependencies = [ "anyhow", "bitflags", "heck", "indexmap", + "wit-component", "wit-parser", ] [[package]] name = "wasmtime-wasi-io" -version = "47.0.4" +version = "48.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b3c9b39b1a1b3029f80e614623e182f165133dfaca8610f156450343f080a4" +checksum = "a17ae3ef0574f5e94d55648c324f31d2f6964b22c857f4cce8c711920a21db51" dependencies = [ "async-trait", "bytes", @@ -2052,11 +2060,30 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-component" +version = "0.254.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0e65bb94c369b3c4741ce3d1d2704b1fec93db7c540df0e521a097e7ceeb5be" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.254.0", + "wasm-metadata", + "wasmparser 0.254.0", + "wit-parser", +] + [[package]] name = "wit-parser" -version = "0.252.0" +version = "0.254.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4266bea110371c620ccf3201c5023676046bc4556e5c7cfb5d500bda5ebc162d" +checksum = "1655131e4f7d3f0cb141f6eca71315ca40eff0f3d4de7cff0a82bacedd8c89b4" dependencies = [ "anyhow", "hashbrown 0.17.1", @@ -2068,7 +2095,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-ident", - "wasmparser 0.252.0", + "wasmparser 0.254.0", ] [[package]] diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index e89b21e..d8c6615 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -15,16 +15,16 @@ kore = { package = "kore-std", path = "../..", features = ["heap"] } # Components, and async host imports: a guest's call into WASI suspends its # fiber and the Kore executor runs until the import's future is ready. The # engine's platform (memory, fibers, ISA) is the embedding program's choice. -wasmtime = { version = "47", default-features = false, features = [ +wasmtime = { version = "48.0.3", default-features = false, features = [ "runtime", "component-model", "async", ] } # WASI 0.2's `wasi:io` (pollables, input and output streams) as traits the # native process stream implements; `no_std` by design. -wasmtime-wasi-io = { version = "47", default-features = false } +wasmtime-wasi-io = { version = "48.0.3", default-features = false } # Host tests resolve the guest fixture against the linker with a std # Wasmtime: the Kore-side engine has no host to run on. [dev-dependencies] -wasmtime = { version = "47", features = ["std", "cranelift", "component-model"] } +wasmtime = { version = "48.0.3", features = ["std", "cranelift", "component-model"] } From 22408ead4389ec5ae8e6d6374f3df180a12a8815 Mon Sep 17 00:00:00 2001 From: Julian Avar Date: Sun, 4 Oct 2026 12:43:22 -0400 Subject: [PATCH 05/13] lua: reads large enough for the facade to stream, and one seam under them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The facade streams a read through this launch's storage ring only when it is handed a destination of at least 256 KiB (`FACADE_MIN_BYTES`). Every read this surface made was 4096 bytes — `KORE_LUA_BUFSIZE`, one page per bridge crossing — so no file, however large, was ever read through a ring. `f:read("a")` and `kore.fs.read_file` were that same page loop. Three changes, and the sizes that decide them: - `kore.fs.read_file` is one host call (`OP_FS_READ_FILE`) over `Fs::read_file`, which opens, streams and closes under one pinned version. Assembled here from positional reads it could not be: each of those pins and releases its own version, so a concurrent writer could leave the answer a mix of revisions, and no call was large enough to stream besides. Bounded by `MAX_FILE_BYTES`, refused `range`. - A read of at least `KORE_LUA_STREAM_MIN` bypasses the page buffer and asks for the whole remainder at once. Both `f:read(n)` and `f:read("a")` go through it; the page buffer takes over again once the remainder is small, so both halves run in one large read. The buffer itself is not grown — it is a fixed array in all 256 open-file slots. - `KORE_LUA_FACADE_STREAM_MIN` and `files::FACADE_STREAM_BYTES` pin the facade's threshold on each side of the bridge, `kore_lua_read_geometry` reports what the engine actually compiled, and the store records the largest destination any read asked for. A cap lowered for an unrelated reason now fails a build or a test rather than silently costing every read its ring. Both positional loops move behind one `Transfer` seam, which is what makes the rule they encode testable: a refusal answers *beside* the prefix that already moved, never instead of it. Reaching that state needs a device under the session, so it was previously unreachable from a host build and untested in either direction. With it comes the correction `Fs::read_at` now requires: a streamed read writes into caller storage as it lands, so the acknowledged prefix bounds what was read and says nothing about which bytes past it are still the caller's. `TransferError::completed` and `docs/filesystem.md` say so. Co-Authored-By: Claude Opus 5 (1M context) --- crates/lua/src/bridge.rs | 12 +- crates/lua/src/files.rs | 394 ++++++++++++++++++++++++++++++---- crates/lua/src/kore_lua.c | 243 +++++++++++++++++---- crates/lua/tests/host_fs.c | 129 +++++++++-- crates/lua/tests/host_smoke.c | 158 +++++++++++++- docs/filesystem.md | 19 +- src/fs.rs | 25 ++- 7 files changed, 869 insertions(+), 111 deletions(-) diff --git a/crates/lua/src/bridge.rs b/crates/lua/src/bridge.rs index 1c32957..18fc023 100644 --- a/crates/lua/src/bridge.rs +++ b/crates/lua/src/bridge.rs @@ -135,6 +135,14 @@ const OP_FS_LIST: u32 = 28; /// VM still has C frames below it and `` handlers above it, and /// leaving from here would run neither. const OP_PROC_EXIT: u32 = 29; +/// `kore.fs.read_file(path)` — a whole file, opened, streamed and closed +/// here. The one filesystem verb that is *not* positional, and +/// deliberately so: only a read the host performs end to end can hold one +/// pinned version for the whole file, or hand the facade a destination +/// large enough to stream through the storage ring. Numbered past +/// `OP_PROC_EXIT` rather than filling the retired 22, because a recycled op +/// number is one a stale half of the bridge would answer wrongly. +const OP_FS_READ_FILE: u32 = 30; /// Host-call outcomes: the value(s) are good; the service refused (the /// text names the refusal); the call could not be made at all (the text @@ -949,7 +957,8 @@ async fn serve_value(services: &mut Services<'_, '_>, extension: & core::slice::from_raw_parts(cell.input, cell.input_len) } }; - if (OP_FS_OPEN..=OP_FS_LIST).contains(&op) { + // The contiguous block, plus the one verb numbered past `os.exit`. + if (OP_FS_OPEN..=OP_FS_LIST).contains(&op) || op == OP_FS_READ_FILE { let answer = serve_fs(services, op, args, input).await; return finish_answer(answer); } @@ -1021,6 +1030,7 @@ async fn serve_fs( OP_FS_OPEN => crate::files::open(fs, files, input, args[0]).await, OP_FS_CLOSE => crate::files::close(fs, files, args[0]).await, OP_FS_READ => crate::files::read(fs, files, args[0], args[1], args[2]).await, + OP_FS_READ_FILE => crate::files::read_file(fs, input).await, OP_FS_WRITE => crate::files::write(fs, files, args[0], args[1], input).await, OP_FS_SYNC => crate::files::sync(fs, files, args[0]).await, OP_FS_RESIZE => crate::files::resize(fs, files, args[0], args[1]).await, diff --git a/crates/lua/src/files.rs b/crates/lua/src/files.rs index 327d2c3..c565dc0 100644 --- a/crates/lua/src/files.rs +++ b/crates/lua/src/files.rs @@ -46,11 +46,46 @@ const MAX_OPEN_FILES: usize = 256; /// The largest single read a verb will answer. /// -/// `f:read("a")` on a big file loops rather than asking for all of it at -/// once: the reply crosses the bridge as one buffer, and an unbounded one -/// would be an unbounded allocation on thread 0 driven by a script. +/// A positional read that wants more asks again, so this bounds one reply +/// and not one file: the reply crosses the bridge as one buffer, and an +/// unbounded one would be an unbounded allocation on thread 0 driven by a +/// script. const MAX_READ_BYTES: usize = 1 << 20; +/// The largest whole file [`read_file`] will answer. +/// +/// [`MAX_READ_BYTES`] bounds a verb a script can call again; this bounds +/// one that has no "again", so it is the ceiling on a single Lua string +/// the store will produce in one call. Past it the answer is +/// [`FsError::Range`]: the file is real and readable, and it is having all +/// of it at once that is refused. +const MAX_FILE_BYTES: usize = 16 << 20; + +/// The destination size at which the native facade streams a read through +/// this launch's storage ring instead of copying it a window at a time. +/// +/// Not used here — the decision is made in C, where the read-ahead buffer +/// is (`KORE_LUA_STREAM_MIN` in `src/kore_lua.c`) — but pinned here +/// because this is the side that knows what the facade promises. A read +/// below it is a window copy however large the file is, so a surface that +/// only ever asks for a page can never stream. +/// +/// The C half pins the same number as `KORE_LUA_FACADE_STREAM_MIN` and its +/// harness asserts the engine compiled it, so a drift on either side fails +/// a build rather than quietly costing every read its ring. +pub const FACADE_STREAM_BYTES: u64 = 256 * 1024; + +// A ceiling below the threshold would mean no read this surface can +// express is ever large enough to stream — the defect this path exists to +// remove, and an easy one to reintroduce by lowering a cap for an +// unrelated reason. Compile-time, because a cap is a compile-time choice +// and a test would only catch it after the build that shipped it. +const _: () = assert!(MAX_READ_BYTES as u64 >= FACADE_STREAM_BYTES); +const _: () = assert!(MAX_FILE_BYTES as u64 >= FACADE_STREAM_BYTES); +// A whole file is the verb with no "again", so its ceiling must not be the +// stricter of the two. +const _: () = assert!(MAX_FILE_BYTES >= MAX_READ_BYTES); + /// One open file the VM holds a handle to. struct OpenFile { view: View, @@ -365,6 +400,105 @@ pub async fn close(fs: &mut Fs<'_, '_>, files: &mut Files, handle: u64) -> Answe } } +/// Where the positional verbs move their bytes. +/// +/// A seam, not an abstraction: the only implementation outside this file's +/// tests is [`Session`], which is the task's filesystem face and one handle +/// in the table above. It exists because the rule the two loops below +/// encode — what a refusal answers once some bytes have *already* moved — +/// is reachable only from a session with a device under it, and it is +/// exactly the rule worth pinning. +// One caller, one thread, never spawned: a `Send` bound is not wanted here, +// which is the whole of what this lint is for. +#[allow(async_fn_in_trait)] +trait Transfer { + /// Read at most one transfer's worth into `dst`, answering how many + /// bytes arrived. Zero is end of file. + async fn read_at(&mut self, offset: u64, dst: &mut [u8]) -> Result; + + /// Write at most one transfer's worth, answering how many bytes the + /// store acknowledged. + async fn write_at(&mut self, offset: u64, src: &[u8]) -> Result; +} + +/// The real one: the filesystem face, and one handle resolved per +/// transfer. +/// +/// Per transfer rather than once across a loop because the store's window +/// bounds each one anyway, so the call is already a loop and holding the +/// borrow would only forbid the resolve. +struct Session<'a, 'w, 't> { + fs: &'a mut Fs<'w, 't>, + files: &'a mut Files, + handle: u64, +} + +impl Transfer for Session<'_, '_, '_> { + async fn read_at(&mut self, offset: u64, dst: &mut [u8]) -> Result { + let view = &self.files.resolve(self.handle).map_err(Refusal::Fs)?.view; + self.fs.read_at(view, offset, dst).await + } + + async fn write_at(&mut self, offset: u64, src: &[u8]) -> Result { + let view = &self.files.resolve(self.handle).map_err(Refusal::Fs)?.view; + self.fs.write_at(view, offset, src).await + } +} + +/// Fill `dst` from `offset`, answering the prefix that arrived and the +/// refusal that stopped it, if one did. +/// +/// **A refusal is reported beside the prefix, never instead of it.** Bytes +/// the store already moved are the caller's; answering only the refusal +/// would throw them away. Nothing is hidden by that — the caller's next +/// call starts at a zero prefix and meets the same refusal — it is only +/// deferred past the bytes that are real. +/// +/// The prefix is what was *acknowledged*, which is not the same as what +/// was initialized: a read large enough to stream through the storage ring +/// writes into `dst` as it lands, so a refusal part way can leave bytes +/// past the prefix clobbered. It is a count, never a replay offset. +async fn fill( + transfer: &mut T, + offset: u64, + dst: &mut [u8], +) -> (usize, Option) { + let mut filled = 0; + while filled < dst.len() { + match transfer + .read_at(offset + filled as u64, &mut dst[filled..]) + .await + { + Ok(0) => break, + Ok(count) => filled += count, + Err(error) => return (filled, Some(error)), + } + } + (filled, None) +} + +/// Write all of `src` at `offset`, answering the acknowledged prefix and +/// the refusal that stopped it, if one did. +/// +/// [`fill`]'s rule, outward — with one asymmetry that matters: on a +/// transport failure the current unacknowledged chunk may *also* have +/// committed, so the prefix is a lower bound on what changed, not a +/// description of it. +async fn drain(transfer: &mut T, offset: u64, src: &[u8]) -> (usize, Option) { + let mut written = 0; + while written < src.len() { + match transfer + .write_at(offset + written as u64, &src[written..]) + .await + { + Ok(0) => break, + Ok(count) => written += count, + Err(error) => return (written, Some(error)), + } + } + (written, None) +} + /// Read up to `count` bytes from `offset`. /// /// Short is not an error: a read that reaches the end answers the bytes it @@ -394,22 +528,59 @@ pub async fn read( return refused(FsError::OutOfStorage); } buffer.resize(count, 0); - let mut filled = 0; - while filled < count { - match fs - .read_at(&file.view, offset + filled as u64, &mut buffer[filled..]) - .await - { - Ok(0) => break, - Ok(got) => filled += got, - Err(error) => return refusal(error), - } + let mut session = Session { fs, files, handle }; + let (filled, stopped) = fill(&mut session, offset, &mut buffer).await; + if filled == 0 + && let Some(error) = stopped + { + return refusal(error); } buffer.truncate(filled); let length = buffer.len() as u64; Answer::Bytes(length, buffer) } +/// A whole file by path, in one call. +/// +/// Not a convenience over [`read`]. `Fs::read_file` opens, streams and +/// closes under **one pinned version**, so the answer is one revision of +/// the file rather than a seam of several; and a file of at least +/// [`FACADE_STREAM_BYTES`] crosses by DMA through this launch's storage +/// ring instead of a window at a time. Neither is reachable by looping +/// [`read`] — each call there pins its own version, and a page-sized +/// destination is never large enough to stream. +/// +/// Bounded by `MAX_FILE_BYTES` (16 MiB), refused as `range` rather than +/// grown without a ceiling. +pub async fn read_file(fs: &mut Fs<'_, '_>, path: &[u8]) -> Answer { + let mut bytes: Vec = Vec::new(); + let mut overflow = false; + let read = fs + .read_file(path, |chunk| { + if bytes.len() + chunk.len() > MAX_FILE_BYTES { + overflow = true; + return Err(Refusal::Fs(FsError::Range)); + } + // The size is the file's, not this task's choice, so a heap + // that cannot hold it refuses the script rather than aborting + // thread 0 — `read`'s rule, at a larger scale. + bytes + .try_reserve(chunk.len()) + .map_err(|_| Refusal::Fs(FsError::OutOfStorage))?; + bytes.extend_from_slice(chunk); + Ok(()) + }) + .await; + match read { + _ if overflow => refused(FsError::Range), + Ok(()) => { + let length = bytes.len() as u64; + Answer::Bytes(length, bytes) + } + Err(error) => refusal(error), + } +} + /// Write `bytes` at `offset`, answering how many were acknowledged and /// where that leaves the end of the written run. /// @@ -449,30 +620,12 @@ async fn finish_write( start: u64, bytes: &[u8], ) -> Answer { - let mut written = 0; - while written < bytes.len() { - let view = match files.resolve(handle) { - Ok(file) => &file.view, - Err(error) => return refused(error), - }; - // The view is borrowed per chunk rather than across the loop: the - // store's window bounds each transfer, so the call is already a - // loop and holding the borrow would only forbid the resolve. - match fs - .write_at(view, start + written as u64, &bytes[written..]) - .await - { - Ok(0) => break, - Ok(count) => written += count, - Err(error) => { - // A short write is a fact, not a detail: report the - // acknowledged prefix so a caller knows what committed. - if written == 0 { - return refusal(error); - } - break; - } - } + let mut session = Session { fs, files, handle }; + let (written, stopped) = drain(&mut session, start, bytes).await; + if written == 0 + && let Some(error) = stopped + { + return refusal(error); } Answer::Value(written as u64, start + written as u64) } @@ -602,6 +755,173 @@ pub async fn list(fs: &mut Fs<'_, '_>, path: &[u8]) -> Answer { mod tests { use super::*; + use alloc::vec; + use core::future::Future; + use core::task::{Context, Poll, Waker}; + + /// Drive a future that never pends. + /// + /// [`Scripted`] answers from a list, so one poll always finishes. A + /// `Pending` here would mean the test grew a real await and needs a + /// real executor — worth a panic rather than a hang. + fn answer(future: F) -> F::Output { + let mut future = core::pin::pin!(future); + match future + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())) + { + Poll::Ready(value) => value, + Poll::Pending => panic!("the scripted transfer never pends"), + } + } + + /// A [`Transfer`] that answers a written script. + /// + /// The point is the states a real session cannot be put into on + /// demand: a refusal that lands *after* some bytes have moved, a store + /// that accepts nothing without refusing, a window that answers short + /// twice running. Each step is one transfer's answer, in order. + struct Scripted { + steps: Vec>, + /// Stamped into a read's destination, so a test can tell bytes + /// that were delivered from bytes that were only zeroed. + mark: u8, + /// What each transfer was actually asked for: `(offset, len)`. + asked: Vec<(u64, usize)>, + } + + impl Scripted { + fn new(steps: Vec>) -> Self { + Self { + steps, + mark: 0xab, + asked: Vec::new(), + } + } + + fn step(&mut self, offset: u64, len: usize) -> Result { + assert!(!self.steps.is_empty(), "the script ran out of answers"); + self.asked.push((offset, len)); + match self.steps.remove(0) { + // Never more than was asked for: a store that overran the + // destination would be a different bug, tested elsewhere. + Ok(count) => Ok(count.min(len)), + Err(error) => Err(error), + } + } + } + + impl Transfer for Scripted { + async fn read_at(&mut self, offset: u64, dst: &mut [u8]) -> Result { + let count = self.step(offset, dst.len())?; + dst[..count].fill(self.mark); + Ok(count) + } + + async fn write_at(&mut self, offset: u64, src: &[u8]) -> Result { + self.step(offset, src.len()) + } + } + + fn lost() -> Refusal { + Refusal::Fs(FsError::AccessLost) + } + + /// The rule S-L3 exists for, in the direction it was wrong. + #[test] + fn a_read_refused_after_some_bytes_answers_the_prefix_beside_the_refusal() { + let mut source = Scripted::new(vec![Ok(4), Err(lost())]); + let mut dst = [0u8; 8]; + let (filled, stopped) = answer(fill(&mut source, 0, &mut dst)); + assert_eq!(filled, 4); + assert!(stopped.is_some()); + // The prefix is the caller's: it must still be in the destination, + // not discarded along with the refusal. + assert_eq!(&dst[..4], &[0xab; 4]); + assert_eq!(&dst[4..], &[0; 4]); + } + + #[test] + fn a_read_refused_before_anything_moved_answers_only_the_refusal() { + let mut source = Scripted::new(vec![Err(lost())]); + let mut dst = [0u8; 8]; + let (filled, stopped) = answer(fill(&mut source, 0, &mut dst)); + // Zero and a reason is what `read` turns back into a refusal; a + // prefix of zero with a reason is the only case where it may. + assert_eq!(filled, 0); + assert!(stopped.is_some()); + assert_eq!(dst, [0; 8]); + } + + #[test] + fn a_read_stops_at_end_of_file_without_a_refusal() { + let mut source = Scripted::new(vec![Ok(3), Ok(0)]); + let mut dst = [0u8; 8]; + let (filled, stopped) = answer(fill(&mut source, 0, &mut dst)); + assert_eq!(filled, 3); + // Short is not an error. A caller that cannot tell these apart + // reports end of file as a failure. + assert!(stopped.is_none()); + } + + #[test] + fn a_short_read_resumes_where_the_prefix_left_off() { + let mut source = Scripted::new(vec![Ok(2), Ok(3), Ok(3)]); + let mut dst = [0u8; 8]; + let (filled, stopped) = answer(fill(&mut source, 100, &mut dst)); + assert_eq!((filled, stopped.is_none()), (8, true)); + // Each transfer asks from the caller's offset plus what has + // already arrived, for only what is left. + assert_eq!(source.asked, vec![(100, 8), (102, 6), (105, 3)]); + } + + /// The same rule outward. A short write is a fact, not a detail. + #[test] + fn a_write_refused_after_some_bytes_answers_the_acknowledged_prefix() { + let mut sink = Scripted::new(vec![Ok(5), Err(lost())]); + let (written, stopped) = answer(drain(&mut sink, 0, &[7; 9])); + assert_eq!(written, 5); + assert!(stopped.is_some()); + } + + #[test] + fn a_write_refused_before_anything_committed_answers_only_the_refusal() { + let mut sink = Scripted::new(vec![Err(lost())]); + let (written, stopped) = answer(drain(&mut sink, 0, &[7; 9])); + assert_eq!(written, 0); + assert!(stopped.is_some()); + } + + #[test] + fn a_store_that_takes_nothing_stops_rather_than_spinning() { + // Zero acknowledged and no refusal: the loop must end, or a store + // under pressure becomes a hang instead of a short write. + let mut sink = Scripted::new(vec![Ok(4), Ok(0)]); + let (written, stopped) = answer(drain(&mut sink, 0, &[7; 9])); + assert_eq!(written, 4); + assert!(stopped.is_none()); + } + + #[test] + fn a_write_resumes_where_the_acknowledged_prefix_left_off() { + let mut sink = Scripted::new(vec![Ok(2), Ok(7)]); + let (written, stopped) = answer(drain(&mut sink, 64, &[7; 9])); + assert_eq!((written, stopped.is_none()), (9, true)); + assert_eq!(sink.asked, vec![(64, 9), (66, 7)]); + } + + #[test] + fn an_empty_transfer_asks_the_store_nothing() { + let mut source = Scripted::new(Vec::new()); + let (filled, stopped) = answer(fill(&mut source, 0, &mut [])); + assert_eq!((filled, stopped.is_none()), (0, true)); + let mut sink = Scripted::new(Vec::new()); + let (written, stopped) = answer(drain(&mut sink, 0, &[])); + assert_eq!((written, stopped.is_none()), (0, true)); + // `Scripted` panics when asked past its script, so reaching here + // is the assertion: neither loop made a round trip. + } + #[test] fn a_path_splits_into_a_parent_and_a_leaf() { assert_eq!(split(b"/etc/hosts"), Some((&b"/etc"[..], &b"hosts"[..]))); diff --git a/crates/lua/src/kore_lua.c b/crates/lua/src/kore_lua.c index 08300c4..983582f 100644 --- a/crates/lua/src/kore_lua.c +++ b/crates/lua/src/kore_lua.c @@ -303,7 +303,13 @@ enum { KORE_LUA_OP_FS_LIST = 28, /* `os.exit(code)` — records the code with the host, which reads it once the evaluation has unwound. */ - KORE_LUA_OP_PROC_EXIT = 29 + KORE_LUA_OP_PROC_EXIT = 29, + /* A whole file by path, in one call: open, stream, close, all on thread + 0. Not a convenience over `KORE_LUA_OP_FS_READ` — the host's streaming + read is the only one that can hold a pinned version for the whole file, + and a read assembled here from page-sized calls could not (see + `kore_lua_file_fill`). */ + KORE_LUA_OP_FS_READ_FILE = 30 }; /* The error `os.exit` raises to unwind the evaluation. `luaL_error` puts the chunk and line in front of it, so the host matches this as a *suffix* @@ -717,6 +723,61 @@ typedef struct kore_lua_writer { `f:read("l")` is a memchr over bytes already here. */ #define KORE_LUA_BUFSIZE 4096 +/* + * The host facade's streaming threshold (`FACADE_MIN_BYTES` in kore-dagfs, + * `files::FACADE_STREAM_BYTES` on the Rust half of this bridge). + * + * A destination at least this large is read through the launch's storage + * ring from one pinned version; below it the facade copies a window at a + * time. It is a fact about the host, not a tunable here, and the two + * halves are pinned against each other by `kore_lua_read_geometry` below. + */ +#define KORE_LUA_FACADE_STREAM_MIN (256u * 1024u) + +/* + * A read of at least this much bypasses the page buffer and asks the host + * for the whole remainder in one call. + * + * Reading a large file a page at a time presents a page-sized destination + * on every call and so never reaches the threshold above, however large + * the file is. The decision therefore has to be made on this side, which + * is why the constant lives next to the buffer it bypasses. + * + * Separately defined from the facade's own number, and overridable, so a + * test can reach the bypass without a quarter-megabyte fixture — without + * that lowering what this file believes the facade promises. + */ +#ifndef KORE_LUA_STREAM_MIN +#define KORE_LUA_STREAM_MIN KORE_LUA_FACADE_STREAM_MIN +#endif + +_Static_assert(KORE_LUA_STREAM_MIN > KORE_LUA_BUFSIZE, + "a bypass no larger than the page buffer saves no round trip"); + +/* + * The sizes the read path's behaviour turns on, as this translation unit + * actually compiled them. + * + * A harness asserts against these rather than against a header, because + * the failure worth catching is exactly the one a header cannot show: an + * edit to `KORE_LUA_BUFSIZE` or to the threshold that silently stops every + * read from ever being large enough to stream. + */ +enum { + KORE_LUA_GEOMETRY_BUFSIZE = 0, + KORE_LUA_GEOMETRY_STREAM_MIN = 1, + KORE_LUA_GEOMETRY_FACADE_STREAM_MIN = 2 +}; + +unsigned long long kore_lua_read_geometry(int which) { + switch (which) { + case KORE_LUA_GEOMETRY_BUFSIZE: return KORE_LUA_BUFSIZE; + case KORE_LUA_GEOMETRY_STREAM_MIN: return KORE_LUA_STREAM_MIN; + case KORE_LUA_GEOMETRY_FACADE_STREAM_MIN: return KORE_LUA_FACADE_STREAM_MIN; + default: return 0; + } +} + enum { KORE_LUA_FILE_STORE = 0, /* a file in the store */ KORE_LUA_FILE_OUT = 1, /* io.stdout */ @@ -739,6 +800,12 @@ typedef struct { size_t have; size_t taken; int eof; + /* The length `open` answered, kept as a *hint* for sizing a whole-file + read and nothing else. It goes stale the moment anyone writes or + resizes, here or elsewhere, so no read trusts it: it only decides + whether to try one large ask first, and every read still loops until + the host answers zero. */ + unsigned long long length; /* `io.tmpfile`: the file unlinks itself when it closes, so `path` is the name to remove. Nothing else records a path — an ordinary file is reached only through its handle, and remembering where it came from @@ -832,6 +899,7 @@ static kore_lua_file *kore_lua_file_push(lua_State *L, unsigned long long handle file->have = 0; file->taken = 0; file->eof = 0; + file->length = 0; file->temporary = 0; file->path[0] = '\0'; luaL_setmetatable(L, KORE_LUA_FILE); @@ -895,10 +963,17 @@ static int kore_lua_open_named(lua_State *L, const char *path, size_t path_len, if (n) return n; /* An append stream starts at the end, so `f:seek()` before the first write answers where those bytes will actually land. */ - kore_lua_file_push(L, handle, KORE_LUA_FILE_STORE, - (flags & KORE_LUA_O_READ) != 0, - (flags & KORE_LUA_O_WRITE) != 0, - (flags & KORE_LUA_O_APPEND) ? length : 0); + { + kore_lua_file *file = + kore_lua_file_push(L, handle, KORE_LUA_FILE_STORE, + (flags & KORE_LUA_O_READ) != 0, + (flags & KORE_LUA_O_WRITE) != 0, + (flags & KORE_LUA_O_APPEND) ? length : 0); + /* The same word `open` already answered, kept so `f:read("a")` can size + one ask instead of discovering the end a page at a time. A hint, not + an authority — see the field. */ + file->length = length; + } return 1; } @@ -1091,6 +1166,66 @@ static size_t kore_lua_file_take(kore_lua_file *file, luaL_Buffer *b, return take; } +/* + * A large read, from the host straight into `b`, with no page buffer in the + * middle. + * + * One call for the whole `want` instead of `want`/`KORE_LUA_BUFSIZE` of + * them. That is the point: the host's facade streams a destination of at + * least `KORE_LUA_STREAM_MIN` through this launch's storage ring from one + * pinned version, and it can only do that for a destination it is handed + * whole. A page-at-a-time read is below the threshold on every call, so it + * never streams however large the file is. + * + * The caller must have drained the buffer first: `base`, `have` and `taken` + * describe a window this read moves past, and they are reset here. + * + * Answers how many bytes landed, or -1 with `*status`, `*why` and + * `*why_len` set. A short answer is neither an error nor necessarily the + * end: the host bounds one reply, so the caller loops until it has what it + * asked for or the answer is zero. Nothing is pushed on the Lua stack + * beyond `b`'s own growth, so a live `luaL_Buffer` stays at its level. + */ +static long long kore_lua_file_stream(kore_lua_file *file, luaL_Buffer *b, + unsigned long long want, int *status, + const char **why, size_t *why_len) { + unsigned long long count = 0, len = 0; + const char *bytes = NULL; + size_t bytes_len = 0; + *status = kore_lua_host_call(KORE_LUA_OP_FS_READ, file->handle, + file->position, want, NULL, 0, &count, &len, + &bytes, &bytes_len); + if (*status != KORE_LUA_STATUS_OK) { + *why = bytes; + *why_len = bytes_len; + return -1; + } + if (bytes == NULL) bytes_len = 0; + /* The host answers at most what was asked; clamp rather than trust, for + the same reason `kore_lua_file_fill` clamps to its buffer. */ + if ((unsigned long long)bytes_len > want) bytes_len = (size_t)want; + if (bytes_len != 0) luaL_addlstring(b, bytes, bytes_len); + file->position += bytes_len; + file->base = file->position; + file->have = 0; + file->taken = 0; + if (bytes_len == 0) file->eof = 1; + return (long long)bytes_len; +} + +/* + * Whether the next `want` bytes should bypass the page buffer. + * + * Only with the buffer already drained — bytes already here are cheaper + * than any round trip — only for a file in the store, and only when the ask + * is one the host could stream. + */ +static int kore_lua_file_streams(const kore_lua_file *file, + unsigned long long want) { + return file->kind == KORE_LUA_FILE_STORE && !file->eof && + file->have == file->taken && want >= KORE_LUA_STREAM_MIN; +} + /* * Drop a partial read's buffer and answer the failure in `render`'s * spelling, as a negative count. The buffer is finished and popped before @@ -1149,11 +1284,26 @@ static int kore_lua_read_all(lua_State *L, kore_lua_file *file, size_t why_len = 0; luaL_buffinit(L, &b); for (;;) { - int state = kore_lua_file_fill(L, file, &status, &why, &why_len); - if (state < 0) - return kore_lua_read_failed(L, &b, status, why, why_len, render); - if (state == 0) break; - kore_lua_file_take(file, &b, file->have - file->taken); + /* What `open` said was left, if anything did. A stale hint costs + nothing either way: too high and the host answers short, too low and + the fill below is the backstop that finds the real end. */ + unsigned long long left = + file->length > file->position ? file->length - file->position : 0; + if (kore_lua_file_streams(file, left)) { + long long got = + kore_lua_file_stream(file, &b, left, &status, &why, &why_len); + if (got < 0) + return kore_lua_read_failed(L, &b, status, why, why_len, render); + if (got == 0) break; + continue; + } + { + int state = kore_lua_file_fill(L, file, &status, &why, &why_len); + if (state < 0) + return kore_lua_read_failed(L, &b, status, why, why_len, render); + if (state == 0) break; + kore_lua_file_take(file, &b, file->have - file->taken); + } } /* "a" never fails: at end of file it answers the empty string, which is the one read format that does. */ @@ -1180,11 +1330,22 @@ static int kore_lua_read_count(lua_State *L, kore_lua_file *file, size_t want, } luaL_buffinit(L, &b); while (total < want) { - int state = kore_lua_file_fill(L, file, &status, &why, &why_len); - if (state < 0) - return kore_lua_read_failed(L, &b, status, why, why_len, render); - if (state == 0) break; - total += kore_lua_file_take(file, &b, want - total); + if (kore_lua_file_streams(file, want - total)) { + long long got = kore_lua_file_stream(file, &b, want - total, &status, + &why, &why_len); + if (got < 0) + return kore_lua_read_failed(L, &b, status, why, why_len, render); + if (got == 0) break; + total += (size_t)got; + continue; + } + { + int state = kore_lua_file_fill(L, file, &status, &why, &why_len); + if (state < 0) + return kore_lua_read_failed(L, &b, status, why, why_len, render); + if (state == 0) break; + total += kore_lua_file_take(file, &b, want - total); + } } if (total == 0) { luaL_pushresult(&b); lua_pop(L, 1); lua_pushnil(L); return 1; } luaL_pushresult(&b); @@ -1658,36 +1819,34 @@ static int kore_lua_fs_list(lua_State *L) { return 1; } -/* Read a whole file without a handle — the shape a script reaches for far - more often than open/read/close, and the one `io.lines(name)` is built - on. Bounded by the host's single-reply cap. */ +/* + * Read a whole file without a handle — the shape a script reaches for far + * more often than open/read/close. + * + * One host call, not open + read + close: the host opens, streams and + * closes on its own thread, which is the only arrangement that can hold a + * single pinned version for the whole file. Assembled here from + * `KORE_LUA_OP_FS_READ` calls it could not — each call pins and releases + * its own version, so a concurrent writer could leave this answer a mix of + * revisions, and no call would be large enough to stream besides. + * + * Bounded by the host's whole-file cap (`MAX_FILE_BYTES` in src/files.rs), + * which refuses `range` rather than growing without one. + */ static int kore_lua_fs_read_file(lua_State *L) { size_t path_len = 0; const char *path = luaL_checklstring(L, 1, &path_len); - unsigned long long handle = 0, length = 0; - int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_OPEN, KORE_LUA_O_READ, 0, 0, - path, path_len, &handle, &length, NULL, NULL, - kore_lua_fail_table); - if (n) return n; - { - /* `base + 1` is the file object; the read's results sit above it. */ - int base = lua_gettop(L); - kore_lua_file *file = kore_lua_file_push(L, handle, KORE_LUA_FILE_STORE, 1, 0, 0); - int read = kore_lua_read_all(L, file, kore_lua_fail_table); - int pushed = read < 0 ? -read : read; - /* - * The handle is retired here whatever the read did: this verb never - * hands the object out, so nothing else can close it. The close's own - * answer is discarded — the read's is what the caller asked for — and - * it is discarded by level rather than by a fixed count, because - * `kore_lua_file_do_close` pushes one value on success and two on a - * refusal. - */ - (void)kore_lua_file_do_close(L, file, kore_lua_fail_table); - lua_settop(L, base + 1 + pushed); - lua_remove(L, base + 1); - return pushed; - } + unsigned long long count = 0, len = 0; + const char *bytes = NULL; + size_t bytes_len = 0; + int status = kore_lua_host_call(KORE_LUA_OP_FS_READ_FILE, 0, 0, 0, path, + path_len, &count, &len, &bytes, &bytes_len); + if (status != KORE_LUA_STATUS_OK) + return kore_lua_fail_table(L, status, bytes, bytes_len); + /* The reply is length-delimited and not NUL-terminated; an empty file + answers an empty string, never nil, because it read successfully. */ + lua_pushlstring(L, bytes != NULL ? bytes : "", bytes != NULL ? bytes_len : 0); + return 1; } static int kore_lua_fs_write_file(lua_State *L) { diff --git a/crates/lua/tests/host_fs.c b/crates/lua/tests/host_fs.c index 5f6fae9..7b0096f 100644 --- a/crates/lua/tests/host_fs.c +++ b/crates/lua/tests/host_fs.c @@ -11,7 +11,7 @@ * * So this file defines those symbols *strongly* and puts a small store * behind them. It is not a filesystem: it is the smallest thing that can - * answer the ten filesystem ops faithfully enough that the C above it is + * answer the eleven filesystem ops faithfully enough that the C above it is * exercised for real. Faithfully means, specifically: * * - The reply text is **length-delimited and not NUL-terminated**, and @@ -24,8 +24,11 @@ * tested against the words they will really see. * - A handle is an index and a generation, encoded as `src/files.rs` * encodes it, so a use after close is `invalid-handle` here too. - * - Reads are short by design: the store answers at most one page, which - * is what makes the read-ahead loop in `kore_lua.c` run more than once. + * - Positional reads are short by design: the store answers at most one + * page whatever was asked for, which is what makes both the read-ahead + * loop in `kore_lua.c` and its large-read bypass run more than once. + * The whole-file op is the one exception, because its whole contract is + * that there is no second call. * * Included by tests/host_smoke.c rather than compiled separately, so both * the libc-numeric and target-numeric builds of that harness get it. @@ -49,7 +52,8 @@ enum { HOST_FS_OP_REMOVE = 26, HOST_FS_OP_RENAME = 27, HOST_FS_OP_LIST = 28, - HOST_FS_OP_PROC_EXIT = 29 + HOST_FS_OP_PROC_EXIT = 29, + HOST_FS_OP_READ_FILE = 30 }; enum { @@ -64,16 +68,23 @@ enum { enum { HOST_FS_OK = 0, HOST_FS_REFUSED = 1, HOST_FS_UNAVAILABLE = 2 }; -/* One page, so `f:read("a")` over a larger file refills more than once. */ +/* One page, so `f:read("a")` over a larger file refills more than once. + This bounds one *positional* read and nothing else: a short answer is + the store's whole point, and it is what makes both the page-buffer loop + and the large-read bypass in `kore_lua.c` run more than once. */ #define HOST_FS_READ_MAX 4096 #define HOST_FS_MAX_OBJECTS 32 #define HOST_FS_MAX_HANDLES 16 #define HOST_FS_PATH_MAX 64 -#define HOST_FS_BYTES_MAX 16384 +/* Past `KORE_LUA_STREAM_MIN`, so a fixture can be large enough for the C + side to take its bypass at the real threshold rather than a lowered one. + Object storage is allocated on first use, so an empty store costs + nothing for the capacity it does not hold. */ +#define HOST_FS_BYTES_MAX (512 * 1024) struct host_fs_object { char path[HOST_FS_PATH_MAX]; - unsigned char bytes[HOST_FS_BYTES_MAX]; + unsigned char *bytes; size_t len; int directory; int used; @@ -95,7 +106,7 @@ static struct { /* The reply window. One buffer, overwritten by each call, exactly as the bridge's `cell.text` is: C copies it into a Lua string before it makes another call. The poison byte is the point — see the header. */ - unsigned char reply[HOST_FS_READ_MAX + 1]; + unsigned char reply[HOST_FS_BYTES_MAX + 1]; size_t reply_len; /* What the harness inspects afterwards. */ int exit_requested; @@ -117,6 +128,11 @@ static struct { longer expects. */ int reads_armed; int reads_allowed; + /* The largest destination a positional read ever asked for, before this + store clamps it to one page. The clamp is why it has to be recorded + here: what the facade decides on is the *ask*, and after the clamp + nothing downstream can still see how big it was. */ + unsigned long long max_read_want; /* Refuse every close. The slot is still freed, so the harness's leak accounting stays meaningful; what is under test is the *caller's* stack discipline, because `kore_lua_file_do_close` pushes one value on @@ -133,9 +149,9 @@ static struct { * no terminator, so any caller that reaches for `strlen` must produce a * visibly wrong answer here rather than a lucky one. */ -static void host_fs_reply(const void *bytes, size_t len, - const char **text, size_t *text_len) { - if (len > HOST_FS_READ_MAX) len = HOST_FS_READ_MAX; +static void host_fs_reply_bounded(const void *bytes, size_t len, size_t cap, + const char **text, size_t *text_len) { + if (len > cap) len = cap; if (len != 0 && bytes != NULL) memcpy(host_fs.reply, bytes, len); host_fs.reply[len] = 0xff; host_fs.reply_len = len; @@ -143,6 +159,19 @@ static void host_fs_reply(const void *bytes, size_t len, if (text_len != NULL) *text_len = len; } +/* One page at most: the short answer every positional read gets. */ +static void host_fs_reply(const void *bytes, size_t len, + const char **text, size_t *text_len) { + host_fs_reply_bounded(bytes, len, HOST_FS_READ_MAX, text, text_len); +} + +/* A whole file in one reply — only `HOST_FS_OP_READ_FILE` answers this + way, because only that op promises the caller there is no "again". */ +static void host_fs_reply_whole(const void *bytes, size_t len, + const char **text, size_t *text_len) { + host_fs_reply_bounded(bytes, len, HOST_FS_BYTES_MAX, text, text_len); +} + static int host_fs_refuse(const char *code, const char **text, size_t *text_len) { host_fs_reply(code, strlen(code), text, text_len); @@ -182,6 +211,20 @@ static int host_fs_parent_ok(const char *path, size_t len) { return at >= 0 && host_fs.objects[at].directory; } +/* + * An object's bytes, allocated on first use. + * + * `HOST_FS_BYTES_MAX` is half a megabyte so a fixture can cross the C + * side's streaming threshold; holding that inline in every slot would be + * sixteen megabytes of store for a harness whose files are mostly a line + * long. `NULL` is an allocation failure, which the caller answers as + * `out-of-storage` — the same word the real store would use. + */ +static unsigned char *host_fs_bytes(struct host_fs_object *object) { + if (object->bytes == NULL) object->bytes = (unsigned char *)calloc(1, HOST_FS_BYTES_MAX); + return object->bytes; +} + static int host_fs_make(const char *path, size_t len, int directory) { if (len == 0 || len >= HOST_FS_PATH_MAX) return -1; for (int i = 0; i < HOST_FS_MAX_OBJECTS; i++) { @@ -213,6 +256,12 @@ static struct host_fs_handle *host_fs_resolve(unsigned long long handle) { /* ---------------------------------------------------------- the harness */ void host_fs_reset(const char *args) { + /* Before the wipe, or the pointers go with it: object storage is + allocated on first use and the struct is cleared wholesale. */ + for (int i = 0; i < HOST_FS_MAX_OBJECTS; i++) { + free(host_fs.objects[i].bytes); + host_fs.objects[i].bytes = NULL; + } memset(&host_fs, 0, sizeof host_fs); host_fs.entropy = 0x243f6a8885a308d3ull; /* any fixed, well-mixed word */ host_fs.entropy_available = 1; @@ -248,6 +297,12 @@ int host_fs_open_handles(void) { return live; } +/* The largest destination a positional read asked for since the last + reset — what proves a read was big enough for the facade to stream. */ +unsigned long long host_fs_max_read_want(void) { return host_fs.max_read_want; } + +void host_fs_reset_max_read_want(void) { host_fs.max_read_want = 0; } + int host_fs_exists(const char *path) { return host_fs_find(path, strlen(path)) >= 0; } @@ -258,7 +313,9 @@ int host_fs_put(const char *path, const char *bytes, size_t len) { int at = host_fs_find(path, path_len); if (at < 0) at = host_fs_make(path, path_len, 0); if (at < 0 || len > HOST_FS_BYTES_MAX) return 0; - memcpy(host_fs.objects[at].bytes, bytes, len); + unsigned char *store = host_fs_bytes(&host_fs.objects[at]); + if (store == NULL) return 0; + if (len != 0) memcpy(store, bytes, len); host_fs.objects[at].len = len; return 1; } @@ -396,16 +453,52 @@ int kore_lua_host_call(unsigned op, unsigned long long arg0, } struct host_fs_object *object = &host_fs.objects[slot->object]; unsigned long long offset = arg1; + if (arg2 > host_fs.max_read_want) host_fs.max_read_want = arg2; size_t want = (size_t)(arg2 < HOST_FS_READ_MAX ? arg2 : HOST_FS_READ_MAX); if (offset >= object->len) { *result = 0; return HOST_FS_OK; } size_t available = object->len - (size_t)offset; if (want > available) want = available; - host_fs_reply(object->bytes + offset, want, text, text_len); + unsigned char *store = host_fs_bytes(object); + if (store == NULL) return host_fs_refuse("out-of-storage", text, text_len); + host_fs_reply(store + offset, want, text, text_len); *result = want; *result2 = want; return HOST_FS_OK; } + /* + * A whole file by path, with no handle anywhere. The real host opens, + * streams and closes on its own thread under one pinned version; what + * matters here is only that it answers the whole file in one reply and + * leaves no handle behind, which is what the harness's leak accounting + * then proves. + * + * It counts as one read against `reads_armed`: the real host performs + * the reads itself, so fault injection has to be able to reach them + * here too, and the verb answers once either way. + */ + case HOST_FS_OP_READ_FILE: { + if (host_fs.reads_armed) { + if (host_fs.reads_allowed == 0) + return host_fs_refuse("access-lost", text, text_len); + host_fs.reads_allowed--; + } + if (input_len == 0 || input_len >= HOST_FS_PATH_MAX) + return host_fs_refuse("invalid-name", text, text_len); + int at = host_fs_find(path, input_len); + if (at < 0) return host_fs_refuse("not-found", text, text_len); + if (host_fs.objects[at].directory) + return host_fs_refuse("is-directory", text, text_len); + struct host_fs_object *object = &host_fs.objects[at]; + if (object->len > HOST_FS_BYTES_MAX) + return host_fs_refuse("range", text, text_len); + host_fs_reply_whole(object->len != 0 ? host_fs_bytes(object) : NULL, + object->len, text, text_len); + *result = (unsigned long long)object->len; + *result2 = (unsigned long long)object->len; + return HOST_FS_OK; + } + case HOST_FS_OP_WRITE: { struct host_fs_handle *slot = host_fs_resolve(arg0); if (slot == NULL) return host_fs_refuse("invalid-handle", text, text_len); @@ -417,9 +510,11 @@ int kore_lua_host_call(unsigned op, unsigned long long arg0, size_t at = slot->append ? object->len : (size_t)arg1; if (at > HOST_FS_BYTES_MAX || input_len > HOST_FS_BYTES_MAX - at) return host_fs_refuse("out-of-storage", text, text_len); + unsigned char *store = host_fs_bytes(object); + if (store == NULL) return host_fs_refuse("out-of-storage", text, text_len); if (at > object->len) - memset(object->bytes + object->len, 0, at - object->len); - if (input_len != 0) memcpy(object->bytes + at, input, input_len); + memset(store + object->len, 0, at - object->len); + if (input_len != 0) memcpy(store + at, input, input_len); if (at + input_len > object->len) object->len = at + input_len; *result = input_len; *result2 = at + input_len; @@ -441,8 +536,10 @@ int kore_lua_host_call(unsigned op, unsigned long long arg0, size_t length = (size_t)arg1; if (length > HOST_FS_BYTES_MAX) return host_fs_refuse("out-of-storage", text, text_len); + unsigned char *store = host_fs_bytes(object); + if (store == NULL) return host_fs_refuse("out-of-storage", text, text_len); if (length > object->len) - memset(object->bytes + object->len, 0, length - object->len); + memset(store + object->len, 0, length - object->len); object->len = length; *result = length; return HOST_FS_OK; diff --git a/crates/lua/tests/host_smoke.c b/crates/lua/tests/host_smoke.c index db51f15..241f454 100644 --- a/crates/lua/tests/host_smoke.c +++ b/crates/lua/tests/host_smoke.c @@ -25,6 +25,13 @@ extern int kore_lua_call(lua_State *, int, int); extern int kore_lua_print_results(lua_State *); extern void kore_lua_set_top(lua_State *, int); extern void kore_lua_close(lua_State *); +/* The sizes the read path turns on, as src/kore_lua.c compiled them. */ +extern unsigned long long kore_lua_read_geometry(int); +enum { + GEOMETRY_BUFSIZE = 0, + GEOMETRY_STREAM_MIN = 1, + GEOMETRY_FACADE_STREAM_MIN = 2 +}; #if !defined(KORE_LUA_USE_TARGET_NUMERIC) double kore_lua_strtod(const char *text, char **end) { return strtod(text, end); } @@ -149,10 +156,26 @@ static int test_fail_closes(lua_State *L) { return 0; } +/* The largest destination a positional read asked the store for — the one + thing a host build can observe about whether a read could have streamed, + since there is no device under it to watch. */ +static int test_max_read_want(lua_State *L) { + lua_pushinteger(L, (lua_Integer)host_fs_max_read_want()); + return 1; +} + +static int test_reset_max_read_want(lua_State *L) { + (void)L; + host_fs_reset_max_read_want(); + return 0; +} + static int test_open_library(lua_State *L) { static const luaL_Reg entries[] = { {"fail_reads_after", test_fail_reads_after}, {"fail_closes", test_fail_closes}, + {"max_read_want", test_max_read_want}, + {"reset_max_read_want", test_reset_max_read_want}, {NULL, NULL} }; luaL_newlib(L, entries); @@ -514,16 +537,17 @@ static int filesystem_smoke(void) { " tostring(raised))\n" /* ------------------------------------------- a close that also fails */ - /* `kore.fs.read_file` opens, reads and retires the handle itself. - A refused close pushes two values where success pushes one, so a - caller that popped a fixed count would hand back the wrong thing - entirely — the file object, or the error table, instead of the - bytes it read. */ + /* `kore.fs.read_file` is one host call, so it leaves no handle on + this side to close: a store refusing every close cannot reach its + answer at all. That is what moving it off open/read/close bought + — the stack discipline a fixed-count pop used to need here is now + absent rather than merely correct. */ "kore.test.fail_closes(true)\n" "local body, bodyerr = kore.fs.read_file('/hello.txt')\n" "assert(body=='hello world\\nagain\\n', tostring(body))\n" - "assert(bodyerr==nil, 'the close is not the caller\\'s answer')\n" - /* And when the read failed too, the read's failure is what survives. */ + "assert(bodyerr==nil, 'a close is not the caller\\'s answer')\n" + /* The read is still the caller's answer when it is the read that + failed: the host performs it, so fault injection reaches it. */ "kore.test.fail_reads_after(0)\n" "local none, noneerr = kore.fs.read_file('/hello.txt')\n" "assert(none==nil, tostring(none))\n" @@ -619,6 +643,124 @@ static int filesystem_smoke(void) { return 0; } + +/* The fixture, in bytes. Past any threshold this test will accept, so the + large-read bypass is genuinely taken; and not a multiple of the store's + page, so the last answer is short. */ +#define STREAM_FIXTURE_BYTES 300000 +#define STREAM_FIXTURE_UNIT "0123456789abcdef" + +/* + * Large reads, the whole-file verb, and the sizes both turn on. + * + * The thing under test is a *decision*, not a transfer: the host facade + * streams a read through this launch's storage ring only when it is handed + * a destination of at least `KORE_LUA_FACADE_STREAM_MIN`, so a surface that + * only ever asks for a page can never stream however large the file is. + * Nothing here can observe a ring — there is no device under a host build — + * but everything here can observe the ask, and the ask is what was wrong. + * + * The store still answers one page at a time, which is the point: the + * bypass must assemble a short answer correctly and hand back to the page + * buffer once the remainder is small, and both halves run in every read + * below. + */ +static int streaming_smoke(void) { + /* The geometry first, because every assertion after it is only + meaningful if the engine still believes these numbers. */ + if (kore_lua_read_geometry(GEOMETRY_BUFSIZE) != 4096) return 1; + if (kore_lua_read_geometry(GEOMETRY_FACADE_STREAM_MIN) != 256u * 1024u) return 2; + { + unsigned long long ask = kore_lua_read_geometry(GEOMETRY_STREAM_MIN); + /* A read must be at least the facade's threshold to stream at all, and + must still fit the fixture or this test stops covering the bypass. */ + if (ask < kore_lua_read_geometry(GEOMETRY_BUFSIZE)) return 3; + if (ask > STREAM_FIXTURE_BYTES) return 4; + } + + static const char suite[] = + "local unit = '" STREAM_FIXTURE_UNIT "'\n" + "local want = string.rep(unit, 300000 // #unit)\n" + "assert(#want == 300000, 'fixture length')\n" + + /* The whole-file verb: one host call, the whole file, no handle. */ + "assert(kore.fs.read_file('/big.bin') == want)\n" + + /* `f:read('a')` over the same file. The ask starts at the length + `open` answered and shrinks as the file is consumed, so this runs + the bypass first and the page buffer for the remainder. */ + "local f = assert(io.open('/big.bin','r'))\n" + "assert(f:read('a') == want)\n" + "assert(f:read('a') == '')\n" + + /* An explicit count, from the start and then from an offset that is + neither page nor threshold aligned. */ + "assert(f:seek('set') == 0)\n" + "assert(f:read(300000) == want)\n" + "assert(f:seek('set', 10) == 10)\n" + "assert(f:read(299990) == want:sub(11))\n" + + /* A large ask past the end answers what there is, and is not an + error: short is short, only zero is end of file. */ + "assert(f:seek('set') == 0)\n" + "assert(f:read(400000) == want)\n" + "assert(f:seek('end') == 300000)\n" + "assert(f:read(300000) == nil)\n" + + /* Small reads still take the page buffer, and still agree. */ + "assert(f:seek('set') == 0)\n" + "assert(f:read(100) == want:sub(1, 100))\n" + "assert(f:read('l') == want:sub(101))\n" + "assert(f:close())\n" + + /* The whole-file verb's refusals are `kore.fs`'s table spelling, not + `io`'s message-and-errno one. */ + "local v, e = kore.fs.read_file('/nowhere.bin')\n" + "assert(v == nil and e.code == 'not-found' and e.kind == 'refused')\n" + "local d, de = kore.fs.read_file('/tmp')\n" + "assert(d == nil and de.code == 'is-directory')\n" + /* An empty file reads successfully as an empty string, never nil. */ + "assert(kore.fs.write_file('/empty.bin', '') == 0)\n" + "assert(kore.fs.read_file('/empty.bin') == '')\n" + + /* Small reads must still be small: the bypass is for the reads that + can use it, not a blanket change of how every file is read. */ + "assert(kore.test.max_read_want() >= 262144, 'a read reached the facade threshold')\n" + "local s = assert(io.open('/hello.txt','w'))\n" + "assert(s:write('short'))\n" + "assert(s:close())\n" + "kore.test.reset_max_read_want()\n" + "local t = assert(io.open('/hello.txt','r'))\n" + "assert(t:read('a') == 'short')\n" + "assert(t:close())\n" + "assert(kore.test.max_read_want() <= 4096, 'a small file still reads a page at a time')\n"; + + host_fs_reset(""); + { + /* Seeded directly: the point is a file larger than the threshold, not + the writing of one. */ + static char fixture[STREAM_FIXTURE_BYTES]; + size_t unit = sizeof STREAM_FIXTURE_UNIT - 1; + for (size_t i = 0; i < sizeof fixture; i++) fixture[i] = STREAM_FIXTURE_UNIT[i % unit]; + if (!host_fs_put("/big.bin", fixture, sizeof fixture)) return 5; + } + + struct capture capture = {{0}, 0}; + lua_State *L = kore_lua_new(host_alloc, NULL, 0x4b4f5245u, test_libraries, + sizeof test_libraries / sizeof test_libraries[0]); + if (L == NULL) return 6; + if (kore_lua_install_print(L, capture_append, &capture) != LUA_OK) { + kore_lua_close(L); return 7; + } + if (!run_reporting(L, suite, "streaming suite")) { kore_lua_close(L); return 8; } + kore_lua_set_top(L, 0); + /* The whole-file verb holds no handle of its own, and every file the + suite opened was closed. */ + if (host_fs_open_handles() != 0) { kore_lua_close(L); return 9; } + kore_lua_close(L); + return 0; +} + #if defined(KORE_LUA_USE_TARGET_NUMERIC) int kore_lua_host_smoke(void) { #else @@ -801,6 +943,8 @@ int main(void) { host_fs_reset(""); int fs_status = filesystem_smoke(); if (fs_status != 0) return 30 + fs_status; + int stream_status = streaming_smoke(); + if (stream_status != 0) return 50 + stream_status; host_fs_reset(""); struct capture capture = {{0}, 0}; lua_State *L = kore_lua_new(host_alloc, NULL, 0x4b4f5245u, NULL, 0); diff --git a/docs/filesystem.md b/docs/filesystem.md index 987112d..ae8c017 100644 --- a/docs/filesystem.md +++ b/docs/filesystem.md @@ -20,11 +20,22 @@ The C/Zig callback adapter still uses its explicitly bounded 4 KiB scratch buffe a short read need not be EOF. Zero bytes read is EOF. The existing zero-copy `read_chunk` remains available when the owner can consume its borrowed window. +The window is not the only transport. A `read_at` whose destination is at +least **256 KiB**, and `read_file` over a file at least that large, may instead +stream through the launch's storage ring from one pinned version. This is a +property of the destination's size, not of the file's: a caller that loops over +a small buffer stays on the window however large the range is, and one whole-file +call holds a single version where a loop of positional reads pins one per call. +Whether a launch holds a ring at all is decided at founding. + `read_exact_at` and `write_all_at` loop bounded transfers without allocation. -They check the entire offset range before the first request. Read errors report -an initialized prefix. Write errors report only the acknowledged prefix: the -current unacknowledged chunk may already have committed. This is **not a retry -offset**. Neither helper automatically replays a mutation. +They check the entire offset range before the first request. Both report only +the **acknowledged** prefix. For a write, the current unacknowledged chunk may +already have committed. For a read, the destination may hold bytes past the +reported count — a streamed transfer writes into caller storage as it lands, +and a refusal part way leaves that prefix uncounted — so storage past the +acknowledged prefix is clobbered, not retained. Neither count is a **retry +offset**, and neither helper automatically replays a mutation. Offset writes preserve the old tail. `resize` is a separate operation; successful chunks followed by a refused resize leave a partially changed file. Sync and diff --git a/src/fs.rs b/src/fs.rs index e010ab2..aee8199 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -32,8 +32,17 @@ pub enum TransferStop { /// An acknowledged prefix followed by a failure. /// -/// On a transport failure the current write may also have committed. `completed` -/// is a confirmed prefix, not a safe replay offset. No automatic retries occur. +/// `completed` is what the owner acknowledged, on either direction. It is not +/// a safe replay offset and no automatic retries occur. +/// +/// On a write, the current unacknowledged chunk may also have committed. +/// +/// On a read, the destination may hold bytes *past* `completed`: a transfer +/// large enough to stream through the storage ring writes into the caller's +/// destination as it lands, and a refusal part way leaves that prefix behind +/// without counting it. So `completed` bounds what was *read*, and says +/// nothing about which bytes of the destination are still the caller's own. +/// Treat everything past it as clobbered, not as retained. #[derive(Debug)] pub struct TransferError { pub completed: usize, @@ -53,8 +62,16 @@ fn range() -> TransferError { } } -/// Fill `dst` from `offset`, reporting the initialized prefix on early EOF/error. -/// Allocation-free; each round trip uses the owner's existing bounded window. +/// Fill `dst` from `offset`, reporting the acknowledged prefix on early +/// EOF/error — see [`TransferError`], which is the acknowledged count and not +/// necessarily the initialized one. +/// +/// Allocation-free, but not necessarily one round trip per window: a `dst` of +/// at least 256 KiB is one the owner may stream through this launch's storage +/// ring instead, from a single pinned version. That is a property of the +/// destination's size, so a caller that loops over a small buffer never gets +/// it however large the range is. +/// /// Cancellation can leave the current request outstanding; keep its owner alive. pub async fn read_exact_at( fs: &mut Fs<'_, '_>, From 12bda36e10fe02d8f72189b36186d74330749fe2 Mon Sep 17 00:00:00 2001 From: Julian Avar Date: Sun, 4 Oct 2026 12:45:56 -0400 Subject: [PATCH 06/13] lua: a save is one commit, and a short write asks again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three defects on the write path, none of them pack-backed — S8's write grants have not landed upstream and nothing here streams a write. These are what the store already owns and this surface was not using. `kore.fs.write_file` was open + one positional write + close, and a positional write commits once per window: a save cost one image/flush/root/flush chain *per window*, not one for the save. It is now `OP_FS_SAVE` over `Fs::write_bytes` under `hold_commits`, which is the store's own `BeginReplace` transaction — one commit however many windows it takes. The release is the commit, so a refused release is the caller's answer even when every byte was acknowledged. The open no longer truncates. The replacement sets the whole contents, so truncating first was a wasted round trip — and it meant a save that then failed had already destroyed the previous contents. They now survive a refusal intact. `f:write` guessed at a short write. A short acknowledgement is a fact, not a refusal, and the old code turned every one of them into `out-of-storage` — a reason it did not know. It now asks again from where the store stopped, so a real refusal arrives in the store's own vocabulary on the next call. Only a store that acknowledges nothing without refusing has no reason to give, and that is the one case still spelled in C. The harness grows a short-write cap, because the store wrote everything or refused and so could not produce the state at all. `Fs::replace_file` also publishes the index for what it wrote and this does not: that verb is behind kore-dagfs's `index` feature, which pulls kore-svc-extract-worker-rust, and this crate enables only `runtime`. Turning it on is an image-size decision for the Lua target, not a side effect of this change. A save therefore leaves the index stale. Co-Authored-By: Claude Opus 5 (1M context) --- crates/lua/src/bridge.rs | 9 ++- crates/lua/src/files.rs | 54 +++++++++++++++++ crates/lua/src/kore_lua.c | 70 ++++++++++++++++------ crates/lua/tests/host_fs.c | 56 ++++++++++++++++-- crates/lua/tests/host_smoke.c | 108 +++++++++++++++++++++++++++++++++- 5 files changed, 270 insertions(+), 27 deletions(-) diff --git a/crates/lua/src/bridge.rs b/crates/lua/src/bridge.rs index 18fc023..b52db71 100644 --- a/crates/lua/src/bridge.rs +++ b/crates/lua/src/bridge.rs @@ -143,6 +143,12 @@ const OP_PROC_EXIT: u32 = 29; /// `OP_PROC_EXIT` rather than filling the retired 22, because a recycled op /// number is one a stale half of the bridge would answer wrongly. const OP_FS_READ_FILE: u32 = 30; +/// `kore.fs.write_file`'s second half — replace an open file's whole +/// contents (`arg0` the handle, `input` the bytes). Positional +/// [`OP_FS_WRITE`] commits once per window; this is the store's own +/// replacement under one held commit, which is the difference between one +/// image/flush/root/flush chain for a save and one per window. +const OP_FS_SAVE: u32 = 31; /// Host-call outcomes: the value(s) are good; the service refused (the /// text names the refusal); the call could not be made at all (the text @@ -958,7 +964,7 @@ async fn serve_value(services: &mut Services<'_, '_>, extension: & } }; // The contiguous block, plus the one verb numbered past `os.exit`. - if (OP_FS_OPEN..=OP_FS_LIST).contains(&op) || op == OP_FS_READ_FILE { + if (OP_FS_OPEN..=OP_FS_LIST).contains(&op) || matches!(op, OP_FS_READ_FILE | OP_FS_SAVE) { let answer = serve_fs(services, op, args, input).await; return finish_answer(answer); } @@ -1031,6 +1037,7 @@ async fn serve_fs( OP_FS_CLOSE => crate::files::close(fs, files, args[0]).await, OP_FS_READ => crate::files::read(fs, files, args[0], args[1], args[2]).await, OP_FS_READ_FILE => crate::files::read_file(fs, input).await, + OP_FS_SAVE => crate::files::save(fs, files, args[0], input).await, OP_FS_WRITE => crate::files::write(fs, files, args[0], args[1], input).await, OP_FS_SYNC => crate::files::sync(fs, files, args[0]).await, OP_FS_RESIZE => crate::files::resize(fs, files, args[0], args[1]).await, diff --git a/crates/lua/src/files.rs b/crates/lua/src/files.rs index c565dc0..4a2100d 100644 --- a/crates/lua/src/files.rs +++ b/crates/lua/src/files.rs @@ -630,6 +630,60 @@ async fn finish_write( Answer::Value(written as u64, start + written as u64) } +/// Replace an open file's whole contents in one transaction. +/// +/// Not a loop of [`write`](fn@write) with a truncation in front. `Fs::write_bytes` +/// opens the store's own replacement (`BeginReplace`), and the hold around +/// it is what makes every window-sized chunk land under **one** commit +/// instead of one commit each — the difference between a save costing one +/// image/flush/root/flush chain and one costing that per window. +/// +/// A refused hold is not a failed save: the chunks still commit one at a +/// time, exactly as they did before, so the hold is attempted and its +/// refusal left unreported. The release, though, *is* the commit — a save +/// whose bytes all landed but whose release was refused is not saved, and +/// says so. +/// +/// Nothing is bounded here and nothing is allocated: `bytes` is borrowed +/// from the Lua frame the parked worker is holding open and the store +/// chunks it, so the ceiling [`read_file`] needs has no counterpart — +/// there is no reply to build. +/// +/// `kore_dagfs::Fs::replace_file` additionally publishes the index for what +/// it wrote. That verb is behind the crate's `index` feature, which this +/// build does not carry, so a save here leaves the index stale. A stated +/// gap, not an oversight: see `docs/pack-backed-reads.md`. +pub async fn save(fs: &mut Fs<'_, '_>, files: &mut Files, handle: u64, bytes: &[u8]) -> Answer { + let writable = match files.resolve(handle) { + Ok(file) => file.writable, + Err(error) => return refused(error), + }; + if !writable { + return refused(FsError::AccessDenied); + } + let held = fs.hold_commits(true).await.is_ok(); + let view = match files.resolve(handle) { + Ok(file) => &file.view, + Err(error) => return refused(error), + }; + let written = fs.write_bytes(view, bytes).await; + // Released whatever the write did, so a refused replacement cannot + // leave this session holding commits for every later writer. + let released = if held { + fs.hold_commits(false).await + } else { + Ok(()) + }; + if let Err(error) = written { + return refusal(error); + } + if let Err(error) = released { + return refusal(error); + } + let length = bytes.len() as u64; + Answer::Value(length, length) +} + pub async fn sync(fs: &mut Fs<'_, '_>, files: &mut Files, handle: u64) -> Answer { let view = match files.resolve(handle) { Ok(file) => &file.view, diff --git a/crates/lua/src/kore_lua.c b/crates/lua/src/kore_lua.c index 983582f..59ad761 100644 --- a/crates/lua/src/kore_lua.c +++ b/crates/lua/src/kore_lua.c @@ -309,7 +309,12 @@ enum { read is the only one that can hold a pinned version for the whole file, and a read assembled here from page-sized calls could not (see `kore_lua_file_fill`). */ - KORE_LUA_OP_FS_READ_FILE = 30 + KORE_LUA_OP_FS_READ_FILE = 30, + /* Replace an open file's whole contents in one call. The store commits + a positional write once per window; this is its own replacement under + one held commit, so a save is one commit however many windows it + takes. */ + KORE_LUA_OP_FS_SAVE = 31 }; /* The error `os.exit` raises to unwind the evaluation. `luaL_error` puts the chunk and line in front of it, so the host matches this as a *suffix* @@ -1488,21 +1493,34 @@ static int kore_lua_file_write_with(lua_State *L, kore_lua_file *file, int first continue; } { - unsigned long long written = 0, end = 0; - int n; + /* + * A short write is not a failure and not a success: the store + * acknowledges what it took, and the rest is still owed. Ask again + * from where it stopped rather than guessing a reason — if there is + * a real one, the next call is what carries it, in the store's own + * vocabulary instead of this one's best guess. Only a store that + * acknowledges *nothing* without refusing has no reason to give, and + * that is the one case spelled here. + */ + size_t done = 0; kore_lua_file_drop(file); - n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_WRITE, file->handle, - file->position, 0, bytes, len, &written, &end, - NULL, NULL, render); - if (n) return n; - file->position = end; - file->base = end; - lua_pop(L, 1); - if (written != (unsigned long long)len) { - /* A short write is not silently successful. */ - return render(L, KORE_LUA_STATUS_REFUSED, "out-of-storage", - sizeof "out-of-storage" - 1); + while (done < len) { + unsigned long long written = 0, end = 0; + int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_WRITE, file->handle, + file->position, 0, bytes + done, len - done, + &written, &end, NULL, NULL, render); + if (n) return n; + /* An append file writes wherever the store finds the end now, so + the position to continue from is the one it answered. */ + file->position = end; + file->base = end; + if (written == 0) { + return render(L, KORE_LUA_STATUS_REFUSED, "out-of-storage", + sizeof "out-of-storage" - 1); + } + done += (size_t)written; } + lua_pop(L, 1); } } /* Lua answers the file itself, so writes chain. */ @@ -1849,20 +1867,34 @@ static int kore_lua_fs_read_file(lua_State *L) { return 1; } +/* + * Write a whole file by path, creating it if it is not there. + * + * The bytes go over in one `KORE_LUA_OP_FS_SAVE`, which is the store's own + * replacement under a held commit — one commit for the save rather than + * one per window, which is what a loop of positional writes costs. + * + * The open deliberately does **not** truncate. The replacement sets the + * whole contents, so truncating first would only add a round trip; and it + * would mean a save that then failed had already destroyed the previous + * contents, where this way they survive a refusal intact. + */ static int kore_lua_fs_write_file(lua_State *L) { size_t path_len = 0, bytes_len = 0; const char *path = luaL_checklstring(L, 1, &path_len); const char *bytes = luaL_checklstring(L, 2, &bytes_len); unsigned long long handle = 0, length = 0, written = 0, end = 0; int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_OPEN, - KORE_LUA_O_WRITE | KORE_LUA_O_CREATE | - KORE_LUA_O_TRUNCATE, - 0, 0, path, path_len, &handle, &length, NULL, - NULL, kore_lua_fail_table); + KORE_LUA_O_WRITE | KORE_LUA_O_CREATE, 0, 0, path, + path_len, &handle, &length, NULL, NULL, + kore_lua_fail_table); if (n) return n; - n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_WRITE, handle, 0, 0, bytes, + n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_SAVE, handle, 0, 0, bytes, bytes_len, &written, &end, NULL, NULL, kore_lua_fail_table); + /* The handle is retired whatever the save did: this verb never hands the + file out, so nothing else can close it. The close's own answer is + discarded — the save's is what the caller asked for. */ { unsigned long long r = 0, r2 = 0; const char *why = NULL; diff --git a/crates/lua/tests/host_fs.c b/crates/lua/tests/host_fs.c index 7b0096f..9548404 100644 --- a/crates/lua/tests/host_fs.c +++ b/crates/lua/tests/host_fs.c @@ -11,7 +11,7 @@ * * So this file defines those symbols *strongly* and puts a small store * behind them. It is not a filesystem: it is the smallest thing that can - * answer the eleven filesystem ops faithfully enough that the C above it is + * answer the twelve filesystem ops faithfully enough that the C above it is * exercised for real. Faithfully means, specifically: * * - The reply text is **length-delimited and not NUL-terminated**, and @@ -53,7 +53,8 @@ enum { HOST_FS_OP_RENAME = 27, HOST_FS_OP_LIST = 28, HOST_FS_OP_PROC_EXIT = 29, - HOST_FS_OP_READ_FILE = 30 + HOST_FS_OP_READ_FILE = 30, + HOST_FS_OP_SAVE = 31 }; enum { @@ -133,6 +134,13 @@ static struct { here: what the facade decides on is the *ask*, and after the clamp nothing downstream can still see how big it was. */ unsigned long long max_read_want; + /* Acknowledge at most this much per positional write, once armed: the + only way to reach a *short* write, which the store otherwise never + produces because it writes everything or refuses. Zero acknowledges + nothing at all without refusing — the one case the C side has no + reason to report and must spell itself. */ + int writes_armed; + int write_cap; /* Refuse every close. The slot is still freed, so the harness's leak accounting stays meaningful; what is under test is the *caller's* stack discipline, because `kore_lua_file_do_close` pushes one value on @@ -303,6 +311,13 @@ unsigned long long host_fs_max_read_want(void) { return host_fs.max_read_want; } void host_fs_reset_max_read_want(void) { host_fs.max_read_want = 0; } +/* Acknowledge at most `cap` bytes per positional write; a negative `cap` + disarms. Zero acknowledges nothing, without refusing. */ +void host_fs_short_writes(int cap) { + host_fs.writes_armed = cap >= 0; + host_fs.write_cap = cap > 0 ? cap : 0; +} + int host_fs_exists(const char *path) { return host_fs_find(path, strlen(path)) >= 0; } @@ -499,6 +514,30 @@ int kore_lua_host_call(unsigned op, unsigned long long arg0, return HOST_FS_OK; } + /* + * Replace an open file's whole contents. The real host does this in + * one held-commit transaction; what the harness can check is that it + * is one call, that it replaces rather than overwrites in place, and + * that the short-write cap does *not* apply to it — a save has no + * prefix to answer, it either replaced the file or did not. + */ + case HOST_FS_OP_SAVE: { + struct host_fs_handle *slot = host_fs_resolve(arg0); + if (slot == NULL) return host_fs_refuse("invalid-handle", text, text_len); + if (!slot->writable) + return host_fs_refuse("access-denied", text, text_len); + if (input_len > HOST_FS_BYTES_MAX) + return host_fs_refuse("out-of-storage", text, text_len); + struct host_fs_object *object = &host_fs.objects[slot->object]; + unsigned char *store = host_fs_bytes(object); + if (store == NULL) return host_fs_refuse("out-of-storage", text, text_len); + if (input_len != 0) memcpy(store, input, input_len); + object->len = input_len; + *result = (unsigned long long)input_len; + *result2 = (unsigned long long)input_len; + return HOST_FS_OK; + } + case HOST_FS_OP_WRITE: { struct host_fs_handle *slot = host_fs_resolve(arg0); if (slot == NULL) return host_fs_refuse("invalid-handle", text, text_len); @@ -512,12 +551,17 @@ int kore_lua_host_call(unsigned op, unsigned long long arg0, return host_fs_refuse("out-of-storage", text, text_len); unsigned char *store = host_fs_bytes(object); if (store == NULL) return host_fs_refuse("out-of-storage", text, text_len); + /* A short acknowledgement, once armed: the store took a prefix and + owes the rest, which is a fact and not a refusal. */ + size_t taken = input_len; + if (host_fs.writes_armed && (size_t)host_fs.write_cap < taken) + taken = (size_t)host_fs.write_cap; if (at > object->len) memset(store + object->len, 0, at - object->len); - if (input_len != 0) memcpy(store + at, input, input_len); - if (at + input_len > object->len) object->len = at + input_len; - *result = input_len; - *result2 = at + input_len; + if (taken != 0) memcpy(store + at, input, taken); + if (at + taken > object->len) object->len = at + taken; + *result = taken; + *result2 = at + taken; return HOST_FS_OK; } diff --git a/crates/lua/tests/host_smoke.c b/crates/lua/tests/host_smoke.c index 241f454..5e6320a 100644 --- a/crates/lua/tests/host_smoke.c +++ b/crates/lua/tests/host_smoke.c @@ -170,12 +170,19 @@ static int test_reset_max_read_want(lua_State *L) { return 0; } +/* Acknowledge at most n bytes per positional write; negative disarms. */ +static int test_short_writes(lua_State *L) { + host_fs_short_writes((int)luaL_checkinteger(L, 1)); + return 0; +} + static int test_open_library(lua_State *L) { static const luaL_Reg entries[] = { {"fail_reads_after", test_fail_reads_after}, {"fail_closes", test_fail_closes}, {"max_read_want", test_max_read_want}, {"reset_max_read_want", test_reset_max_read_want}, + {"short_writes", test_short_writes}, {NULL, NULL} }; luaL_newlib(L, entries); @@ -644,6 +651,103 @@ static int filesystem_smoke(void) { } +/* + * The write path: a whole-file save, and what a short write answers. + * + * Two rules, and the store has to be made to break for either to be + * reachable. A positional write that is acknowledged *short* is a fact + * and not a refusal, so `f:write` must ask again from where it stopped + * rather than declare a reason it does not know; and a store that + * acknowledges nothing without refusing has no reason to give, which is + * the one case `f:write` spells itself. + * + * `kore.fs.write_file` goes over as one save rather than a loop of + * positional writes, so the short-write cap must not shorten it: a save + * has no prefix to answer, it either replaced the file or it did not. + */ +static int write_smoke(void) { + static const char suite[] = + /* A save replaces, rather than overwriting in place: the old tail + must not survive a shorter save. */ + "assert(kore.fs.write_file('/w.txt','aaaaaaaaaaaa')==12)\n" + "assert(kore.fs.read_file('/w.txt')=='aaaaaaaaaaaa')\n" + "assert(kore.fs.write_file('/w.txt','bb')==2)\n" + "assert(kore.fs.read_file('/w.txt')=='bb')\n" + + /* A short acknowledgement is not a failure: the write asks again + from where it stopped, and all the bytes land. */ + "local f = assert(io.open('/short.txt','w'))\n" + "kore.test.short_writes(3)\n" + "assert(f:write('abcdefghij')==f)\n" + "kore.test.short_writes(-1)\n" + "assert(f:close())\n" + "assert(kore.fs.read_file('/short.txt')=='abcdefghij')\n" + + /* The same, spread over several arguments and a seek, so the + position the loop continues from is exercised too. */ + "local g = assert(io.open('/short2.txt','w'))\n" + "kore.test.short_writes(2)\n" + "assert(g:write('hello ','world',33))\n" + "kore.test.short_writes(-1)\n" + "assert(g:seek('set')==0)\n" + "assert(g:close())\n" + "assert(kore.fs.read_file('/short2.txt')=='hello world33')\n" + + /* An append file writes wherever the store finds the end now, so a + short acknowledgement must continue from the end it answered and + not from a remembered offset. */ + "local a = assert(io.open('/short.txt','a'))\n" + "kore.test.short_writes(3)\n" + "assert(a:write('KLMNOPQ'))\n" + "kore.test.short_writes(-1)\n" + "assert(a:close())\n" + "assert(kore.fs.read_file('/short.txt')=='abcdefghijKLMNOPQ')\n" + + /* A store that acknowledges nothing and does not refuse has no + reason to give; this is the one place `f:write` names one. */ + "local z = assert(io.open('/none.txt','w'))\n" + "kore.test.short_writes(0)\n" + "local ok, why, err = z:write('abc')\n" + "kore.test.short_writes(-1)\n" + "assert(ok==nil and why=='out-of-storage' and err==28, tostring(why))\n" + "assert(z:close())\n" + + /* A save is one call, so the cap that shortens positional writes + cannot shorten it. */ + "kore.test.short_writes(1)\n" + "assert(kore.fs.write_file('/whole.txt','0123456789')==10)\n" + "kore.test.short_writes(-1)\n" + "assert(kore.fs.read_file('/whole.txt')=='0123456789')\n" + + /* A save into a directory that is not there is refused in + `kore.fs`'s spelling, and creates nothing. */ + "local v, e = kore.fs.write_file('/nodir/x.txt','x')\n" + "assert(v==nil and type(e)=='table' and e.code=='not-found', tostring(e))\n" + /* Saving nothing is a save, and empties the file. */ + "assert(kore.fs.write_file('/w.txt','')==0)\n" + "assert(kore.fs.read_file('/w.txt')=='')\n" + /* A read-only handle refuses the save rather than silently dropping + it, and `io`'s surface renders the same refusal its own way. */ + "local r = assert(io.open('/w.txt','r'))\n" + "local wok, wwhy, werr = r:write('x')\n" + "assert(wok==nil and wwhy=='access-denied' and werr==13, tostring(wwhy))\n" + "assert(r:close())\n"; + + host_fs_reset(""); + struct capture capture = {{0}, 0}; + lua_State *L = kore_lua_new(host_alloc, NULL, 0x4b4f5245u, test_libraries, + sizeof test_libraries / sizeof test_libraries[0]); + if (L == NULL) return 1; + if (kore_lua_install_print(L, capture_append, &capture) != LUA_OK) { + kore_lua_close(L); return 2; + } + if (!run_reporting(L, suite, "write suite")) { kore_lua_close(L); return 3; } + kore_lua_set_top(L, 0); + if (host_fs_open_handles() != 0) { kore_lua_close(L); return 4; } + kore_lua_close(L); + return 0; +} + /* The fixture, in bytes. Past any threshold this test will accept, so the large-read bypass is genuinely taken; and not a multiple of the store's page, so the last answer is short. */ @@ -943,8 +1047,10 @@ int main(void) { host_fs_reset(""); int fs_status = filesystem_smoke(); if (fs_status != 0) return 30 + fs_status; + int write_status = write_smoke(); + if (write_status != 0) return 50 + write_status; int stream_status = streaming_smoke(); - if (stream_status != 0) return 50 + stream_status; + if (stream_status != 0) return 70 + stream_status; host_fs_reset(""); struct capture capture = {{0}, 0}; lua_State *L = kore_lua_new(host_alloc, NULL, 0x4b4f5245u, NULL, 0); From 4522206a7004cc5c03703a160e17ea40c0e92b69 Mon Sep 17 00:00:00 2001 From: Julian Avar Date: Sun, 4 Oct 2026 12:46:06 -0400 Subject: [PATCH 07/13] docs: the pack-backed read plan, and what has landed of it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Written to answer whether the filesystem API this surface uses is compatible with the DagFS upgrade in MicroPerceptron/kore#1064 (`ft/pack-tickets`). It is — proven by syncing a scratch export to the PR head and compiling — and that was the problem: the change is additive at the `Fs` facade, so nothing broke and nothing improved either. Records the evidence, why `res/STORAGE_TRANSPORT.md` §9's S8 makes this a deadline rather than a missed win, and the slices. S-L0 through S-L3 and the write slice are implemented and marked; S-L4 (a `pack-ring`-gated witness) and S-L5 (sync `native/`, flip it on, measure on Gigatron) are not. Nothing here has run on metal, so §7's acceptance is unmet. Two claims an earlier draft got wrong are corrected in place rather than quietly dropped: `io.lines(name)` reads lines and `require` reads through a 64 KiB `Loader::read_source` buffer, so neither was ever going to reach the threshold, and the whole-file cap is spelled `range` rather than `out-of-storage` — the file is readable, and it is having all of it at once that is refused. Co-Authored-By: Claude Opus 5 (1M context) --- docs/pack-backed-reads.md | 276 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 docs/pack-backed-reads.md diff --git a/docs/pack-backed-reads.md b/docs/pack-backed-reads.md new file mode 100644 index 0000000..336e814 --- /dev/null +++ b/docs/pack-backed-reads.md @@ -0,0 +1,276 @@ +# Pack-backed reads in the Lua surface + +**Status: S-L0 through S-L3 and S-L6 are implemented; S-L4 and S-L5 are not.** The +implemented slices are described in the past tense below and their contract +lives in `filesystem.md`; everything from S-L4 on is still aspirational. No +part of this has run on metal — §7's acceptance is unmet until S-L5. + +## 1. The question + +Is the filesystem API the Lua surface uses compatible with the DagFS upgrade in +[MicroPerceptron/kore#1064][pr]? + +**Yes, at the source level — and that is the problem.** It compiles unchanged, +runs unchanged, and reaches the pack-backed path exactly never. + +[pr]: https://github.com/MicroPerceptron/kore/pull/1064 + +### Evidence + +`#1064` is `ft/pack-tickets`, open, head `9b8c5d672` on main `fe9406151`. +`native/` here is pinned at Kore `9e4172bc2`. Syncing a scratch copy of this +repo to the PR head with `tools/sync-native.py` and compiling gives: + +| Crate | Result | +|---|---| +| `kore-std --features fs` | clean | +| `kore-lua --all-features` | clean | +| `kore-ffi --all-features` | clean | + +No owner package is added or removed — `kore-ring` was already in the export +closure — and the dagfs change is purely additive at the `Fs` facade: +`pub mod stor`, `RING_TARGET_RIGHTS`, `Fs::set_direct_polling`, and three +private fields on `Fs`. Every verb `crates/lua` calls keeps its signature. + +## 2. Why it is inert + +The ring engages on a size test. `Fs::read_at` streams through the facade ring +only when the destination is at least `FACADE_MIN_BYTES` (256 KiB); below that +it takes the pair's window exactly as today, and `read_range_by_ring` answers +`Ok(None)` with the note `"small read"`. + +The Lua surface never presents a destination that large: + +- `kore_lua.c` gives each open file a `KORE_LUA_BUFSIZE` (**4096**) read-ahead + buffer, and `kore_lua_file_fill` asks the bridge for exactly that much. +- `f:read("a")` (`kore_lua_read_all`) and `f:read(n)` (`kore_lua_read_count`) + both loop over `kore_lua_file_fill`, so they are 4 KiB at a time as well. +- `kore.fs.read_file` was built on `kore_lua_read_all`, so it was 4 KiB at a + time too. (`io.lines(name)` reads *lines*, and `require` reads a module + through `Loader::read_source` into a 64 KiB buffer — neither is a whole-file + read and neither reaches the threshold. An earlier draft of this plan + claimed both; it was wrong.) +- `crates/lua/src/files.rs::read` therefore allocates a 4096-byte `Vec` and + calls `Fs::read_at` with it. `MAX_READ_BYTES` (1 MiB) is a ceiling the + C side never approaches. + +A 100 MiB file read by `kore.fs.read_file` is ~25,600 bridge round trips and +~25,600 pair round trips, before and after #1064 alike. + +Exec does fund a ring for us: `augment_stor_ring_plan` runs for every launch +with `filesystem_requested`, moving `role::STOR_RING` and a 256 KiB +`role::STOR_TARGET` into the child. A Lua task gets a ring at founding and then +never submits to it. + +## 3. Why it becomes a break, not just a missed win + +`res/STORAGE_TRANSPORT.md` §9 lands **S8: "write grants and write consumers; the +window-copy verbs and `provider/block.rs` deleted."** The 4 KiB window verbs the +Lua surface is built on are scheduled for deletion. Staying chunk-at-a-time is +not a decision to forgo throughput; it is a decision to be ported under duress +later, by whoever is holding S8. + +The same section rules out the lazy fix: *"`read_chunk` stays on the window: its +contract is one window-sized chunk per call, so a ring would have to stay +attached between calls — the lifetime §12's kernel fix must make safe first."* +A ring cannot be held open across a 4 KiB read-ahead refill. The buffer shape +has to change; there is no transparent option. + +## 4. Three semantic deltas to absorb + +Independent of the ring, #1064 changes what a refusal means. + +1. **`read_at` no longer leaves the destination unchanged on refusal.** Its doc + moves from *"The destination is unchanged on refusal"* to *"On refusal the + destination may hold a prefix of the bytes."* `src/fs.rs::read_exact_at` + documents `TransferError::completed` as the initialized prefix; once a ring + read breaks off mid-stream that count undercounts what was written into + `dst`. `docs/filesystem.md` ("Read errors report an initialized prefix") + stops being true as written. +2. **`files.rs::read` discards its partial prefix.** The loop answers + `refusal(error)` and drops `filled`. That was correct while a refusal left + the destination untouched; it now throws away bytes that were delivered. +3. **No version is pinned across a Lua read.** `read_range_by_ring` pins one + version per call. Lua's loop issues an independent call per 4 KiB, so + `f:read("a")` can interleave bytes from several revisions under a concurrent + writer. This hazard exists today on the window path; the point is that + #1064 introduces the mechanism that would fix it, and the chunk loop is what + stops us taking it. + +## 5. The seam already exists + +The facade ring is internal to `Fs`. Nothing here needs to name +`kore_dagfs::stor`, hold a `StorRing`, or manage a grant. Two verbs are the +whole interface, and **both already exist at the pinned revision with the exact +signatures #1064 keeps**: + +```rust +pub async fn read_file(&mut self, path: &[u8], consume: F) -> Result<(), Refusal> + where F: FnMut(&[u8]) -> Result<(), Refusal>; // S5d: whole file, by path +pub async fn read_at(&mut self, file: &View, offset: u64, dst: &mut [u8]) + -> Result; // S5e: dst >= 256 KiB +``` + +So the work below is buildable, testable and mergeable **today against +`9e4172bc2`**, with no feature gate and no commented-out body. It is slower than +today's code by nothing and faster by nothing until `native/` syncs past #1064, +at which point the same source becomes pack-backed with no further edit. + +That is what a seam should buy. The alternative — commented-out function bodies +— cannot be type-checked, cannot be tested, and rots against the API it is +waiting for; AGENTS.md already rules that "empty headers, placeholder allocators +and mock implementations are not completed language APIs." + +The only genuinely unbuildable surface is observability (§6, S-L4), because +`Fs::facade_ring_note` and `Fs::facade_ring_files` do not exist before #1064. +That one gets a `#[cfg(feature = "pack-ring")]`, off by default — invisible to +the default build like a comment, but compiled in CI against a synced tree. + +## 6. Slices + +Each is its own PR, green on its own, in order. S-L1 through S-L3 need no +upstream change. + +### S-L0 — guard the claim *(done)* +`kore_lua_read_geometry` exports the sizes the engine actually compiled, and +`streaming_smoke` in `tests/host_smoke.c` asserts them before anything else. +Three guards, because the failure is silent otherwise: + +- the engine's compiled `KORE_LUA_BUFSIZE` (4096) and + `KORE_LUA_FACADE_STREAM_MIN` (256 KiB), checked from the harness; +- `const _: () = assert!(...)` in `files.rs` binding `MAX_READ_BYTES` and + `MAX_FILE_BYTES` to `FACADE_STREAM_BYTES`, so a cap lowered for an unrelated + reason fails the build rather than the next read; +- the store records the largest destination any read asked for + (`host_fs_max_read_want`), and the suite asserts a large read reached the + threshold and a small one did not. That is the only thing a host build can + observe about streaming — there is no device under it — and it is the + property that was actually wrong. + +Verified by negative control: stubbing `kore_lua_file_streams` to return 0 +fails the suite at that assertion. + +### S-L1 — a whole-file verb that is one call *(done)* +`OP_FS_READ_FILE` (30, past `OP_PROC_EXIT` rather than recycling the retired +22). `kore_lua_fs_read_file` is no longer open + `read_all` + close in C; it is +one bridge op, served on thread 0 by `files::read_file` over +`Fs::read_file(path, consume)`, appending into a `Vec` grown by `try_reserve`. + +The cap went the recommended way: `MAX_FILE_BYTES` = 16 MiB, separate from +`MAX_READ_BYTES`, refused as **`range`** rather than `out-of-storage` — the +file is real and readable, and it is having all of it at once that is refused; +`out-of-storage` stays for an actual allocation failure, as in `files::read`. +`list` already spells an exceeded reply cap this way. + +Reach is narrower than this plan first claimed: `kore.fs.read_file` and +anything built on it. See §2. + +### S-L2 — large reads on the handle path *(done)* +Keep the 4 KiB inline buffer for interactive and small reads; bypass it when the +want is large. + +- `kore_lua_file_stream` asks the host for a whole span straight into the + `luaL_Buffer`; `kore_lua_file_streams` decides when, and both + `kore_lua_read_count` and `kore_lua_read_all` go through them. A short answer + is not an error — the loop continues — and once the remainder falls below the + threshold the page buffer takes over again, so both halves run in one read. +- `KORE_LUA_STREAM_MIN` defaults to `KORE_LUA_FACADE_STREAM_MIN` but is + separately `#define`d and overridable, so a test can lower the ask without + lowering what the engine believes the facade promises. +- `kore_lua_read_all`: `open` already answers the file length beside the + handle, but today it is only used to seed an append position and is then + dropped. Carry it into `kore_lua_file` so `read_all` can ask for the whole + remainder in one op rather than looping. It is a hint, not an authority — a + short answer still ends the read, and a file that grew still needs the loop + as a backstop. +- Do **not** grow `KORE_LUA_BUFSIZE`. It is a fixed array in every + `kore_lua_file`, and `MAX_OPEN_FILES` is 256; a 256 KiB buffer would be + 64 MiB of per-VM arrays to serve a read pattern most files never use. + +`files.rs::read` needs no change beyond S-L3: a larger `count` already flows +into a larger `dst`. + +### S-L3 — partial-prefix semantics *(done)* +Make the three deltas in §4 true statements. + +- `files.rs::read`: on refusal with `filled > 0` it answers the prefix, as + `finish_write` already did for a short write. The caller's next call starts + at `filled == 0` and meets the same refusal, so nothing is hidden — only + deferred past the bytes that are real. Not covered by a test: `files::read` + needs a live `Fs`, and this repo has no mock for one. The C-side half of the + contract (a refusal after a partial read) is covered by the existing + `read_failure_suite`. +- `src/fs.rs`: `TransferError::completed` is now documented as the + acknowledged prefix on *both* directions — the distinction `write_all_at` + already drew — with storage past it called clobbered rather than retained. +- `docs/filesystem.md`: the transfer-and-failure section says the same, and + states the 256 KiB threshold as a property of the destination's size. + +### S-L6 — the write path *(done)* +Not pack-backed — S8's write grants have not landed upstream, and nothing +here streams a write. Three defects the read work made visible, fixed with +what the store already owns: + +- **`kore.fs.write_file` committed once per window.** It was open + a + positional write + close, and `files::write` loops `Fs::write_at`, each + call its own commit — one image/flush/root/flush chain per window of a + save. It is now `OP_FS_SAVE` (31) over `Fs::write_bytes` under + `hold_commits`, which is the store's own `BeginReplace` transaction: one + commit for the save however many windows it takes. The release *is* the + commit, so a refused release is the caller's answer even when every byte + was acknowledged. +- **The open no longer truncates.** The replacement sets the whole + contents, so truncating first was a wasted round trip — and it meant a + save that then failed had already destroyed the previous contents. They + now survive a refusal intact. +- **`f:write` guessed at a short write.** A short acknowledgement is a fact, + not a refusal, and the old code turned any of them into `out-of-storage` + — a reason it did not know. It now asks again from where the store + stopped, so a real refusal arrives in the store's own vocabulary on the + next call. Only a store that acknowledges *nothing* without refusing has + no reason to give, and that is the one case spelled in C. + +**Stated gap:** `kore_dagfs::Fs::replace_file` also publishes the index for +what it wrote, and `OP_FS_SAVE` does not. That verb is behind kore-dagfs's +`index` feature, which pulls `kore-svc-extract-worker-rust`; this crate +enables only `runtime`. Turning it on is an image-size decision for the Lua +target, not a side effect of this slice. Until then a save leaves the index +stale, and a later `CurrentOnly` search reports `IndexIncomplete` — the +designed signal, but worth knowing it is reachable from here. + +### S-L4 — the witness (`pack-ring` feature, off by default) +A `kore.fs` introspection verb answering `Fs::facade_ring_note()` and +`Fs::facade_ring_files()`, so a Lua keytest can assert *"this read streamed"* +rather than inferring it from a stopwatch. `#[cfg(feature = "pack-ring")]`; CI +builds it against a tree synced to #1064, so it cannot rot. + +### S-L5 — sync, flip, measure +Once #1064 merges: `tools/sync-native.py` to the merge commit, both `Cargo.lock` +files, `make fmt test check docs owners cross miri bindings-check`, +`tools/check-local.py`. Then enable `pack-ring` by default and measure on metal. +Per §4 of the doctrine this is the only acceptance criterion that counts — +simulation and argument are provisional. + +## 7. Acceptance + +- `kore.fs.read_file` on a file of at least 256 KiB reports + `facade_ring_note() == "streamed"`. +- `f:read("a")` over the same file issues one `OP_FS_READ` bridge op, not + *n*/4096 of them. +- A read refused mid-stream answers its delivered prefix, and the count it + answers is the count the script can trust. +- Measured decode-path read throughput on Gigatron, against the same file read + before the change. Not a benchmark on a host build. + +## 8. What this plan does not do + +It does not make writes pack-backed. S8 deletes the window-copy write verbs +too, and write grants are a separate upstream slice that has not landed; +positional writes stay chunk-at-a-time until they do. S-L6 fixes what could be +fixed without them — the commit count, the truncation, and the short-write +guess — and `OP_FS_SAVE` is the seam a write grant lands behind when it comes. + +It does not add a Lua-visible ring, grant or target API — the facade owns +those, and exposing them would be a second authority over one ring. + +It does not enable kore-dagfs's `index` feature. See S-L6's stated gap. From 58bbb4589a132bbff7fe5706974543145f4bca81 Mon Sep 17 00:00:00 2001 From: Julian Avar Date: Sun, 4 Oct 2026 21:21:31 -0400 Subject: [PATCH 08/13] lua: kore.fs answers the graph, not a POSIX projection of it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `kore.fs` was `io` with a different error table. Its whole vocabulary was open/read_file/write_file/remove/rename/mkdir/resize/stat/exists/list — POSIX verbs, kore spelling, and nothing of the store underneath. Meanwhile `src/fs.rs` already re-exports the entire graph vocabulary as `fs::dag` for Rust consumers, so Lua was the surface that had it worst. `Stat` carries eight facts and this surface passed on two of them. A script could not see the revision it was racing, the links keeping an object alive, the grants outstanding on it, or what its bytes were meant to be. `stat` now answers the whole record over a fixed little-endian layout whose length the C half checks before decoding a field. `entries` answers a directory's edges where `list` answers its names — each name with its target's class and subtype, so telling a native module from a plain file costs one call rather than a stat per name. The target's `ObjectId` is deliberately not in it: a service-local slot key is not a stable identity, and a script that stored one would be wrong in a way nothing would tell it. The stable one is `stat`'s, named as such. `rights` answers what this task may do at a path, in the store's own `rwns` spelling — `rights_label` printing what `rights_parse` reads — with the booleans derived from the letters rather than beside them. A second vocabulary in C would be a second authority over one set of bits. An unknown subtype is carried, never guessed: the store's rule is that a newer writer's subtype is data an older reader passes through, so `subtype` is nil where this build cannot name one and `subtype_bits` still reports it. None of this is authority. A stable id conveys none, a subtype gates nothing, and `rights` reports what a walk already found. Deriving a view, delegating one, and the grant vocabulary stay absent until MicroPerceptron/kore #986 and #991 rule the capability model for a userland authority domain; a surface invented here first would have to be retracted. Co-Authored-By: Claude Opus 5 (1M context) --- crates/lua/src/bridge.rs | 15 ++- crates/lua/src/files.rs | 113 +++++++++++++++++++- crates/lua/src/kore_lua.c | 195 +++++++++++++++++++++++++++++++--- crates/lua/src/lib.rs | 34 +++++- crates/lua/tests/host_fs.c | 103 +++++++++++++++++- crates/lua/tests/host_smoke.c | 114 ++++++++++++++++++++ 6 files changed, 551 insertions(+), 23 deletions(-) diff --git a/crates/lua/src/bridge.rs b/crates/lua/src/bridge.rs index b52db71..0932fff 100644 --- a/crates/lua/src/bridge.rs +++ b/crates/lua/src/bridge.rs @@ -149,6 +149,12 @@ const OP_FS_READ_FILE: u32 = 30; /// replacement under one held commit, which is the difference between one /// image/flush/root/flush chain for a save and one per window. const OP_FS_SAVE: u32 = 31; +/// `kore.fs.entries(path)` — a directory's edges with each target's class +/// and subtype, where [`OP_FS_LIST`] answers only the names. +const OP_FS_ENTRIES: u32 = 32; +/// `kore.fs.rights(path)` — the rights this task holds there, in the +/// store's own `rwns` spelling. Reports authority; confers none. +const OP_FS_RIGHTS: u32 = 33; /// Host-call outcomes: the value(s) are good; the service refused (the /// text names the refusal); the call could not be made at all (the text @@ -964,7 +970,12 @@ async fn serve_value(services: &mut Services<'_, '_>, extension: & } }; // The contiguous block, plus the one verb numbered past `os.exit`. - if (OP_FS_OPEN..=OP_FS_LIST).contains(&op) || matches!(op, OP_FS_READ_FILE | OP_FS_SAVE) { + if (OP_FS_OPEN..=OP_FS_LIST).contains(&op) + || matches!( + op, + OP_FS_READ_FILE | OP_FS_SAVE | OP_FS_ENTRIES | OP_FS_RIGHTS + ) + { let answer = serve_fs(services, op, args, input).await; return finish_answer(answer); } @@ -1050,6 +1061,8 @@ async fn serve_fs( None => Answer::Refused(alloc::string::String::from("invalid-name")), }, OP_FS_LIST => crate::files::list(fs, input).await, + OP_FS_ENTRIES => crate::files::entries(fs, input).await, + OP_FS_RIGHTS => crate::files::rights(fs, input).await, _ => Answer::Unavailable(alloc::string::String::from("unknown filesystem call")), } } diff --git a/crates/lua/src/files.rs b/crates/lua/src/files.rs index 4a2100d..54f202a 100644 --- a/crates/lua/src/files.rs +++ b/crates/lua/src/files.rs @@ -709,6 +709,45 @@ pub async fn resize(fs: &mut Fs<'_, '_>, files: &mut Files, handle: u64, length: } } +/// The fixed record [`stat`] answers, little-endian: +/// +/// ```text +/// 0 u64 len the object's size in bytes +/// 8 u64 revision monotonic, committed; 0 is the any-revision sentinel +/// 16 u64 stable volume-stable identity, 0 when the store has none +/// 24 u32 links names pointing at this object +/// 28 u32 grants grants outstanding on it +/// 32 u8 kind 0 file, 1 directory +/// 33 u8 subtype carried verbatim, known to this build or not +/// ``` +/// +/// A record rather than result words because there are eight facts and the +/// transport carries two. The C half checks the length before decoding, so +/// a bridge whose halves disagree refuses rather than reading a field that +/// moved. +const STAT_RECORD_BYTES: usize = 34; + +/// The store's own answer, whole. +/// +/// `Stat` carries eight facts and this surface used to answer two of them, +/// which is the sense in which it hid the graph: a script could not see +/// the revision it raced, the links that kept an object alive, or the +/// grants outstanding on it. None of this is authority — `StableObjectId` +/// "conveys no authority" and a subtype "gates nothing" — so reporting it +/// widens what a script can know without widening what it can do. +fn stat_record(stat: &kore::fs::Stat) -> Vec { + let mut record = Vec::with_capacity(STAT_RECORD_BYTES); + record.extend_from_slice(&stat.len.to_le_bytes()); + record.extend_from_slice(&stat.revision.to_le_bytes()); + record.extend_from_slice(&stat.stable_object.get().to_le_bytes()); + record.extend_from_slice(&stat.links.to_le_bytes()); + record.extend_from_slice(&stat.grants.to_le_bytes()); + record.push(u8::from(stat.kind.is_dir())); + record.push(stat.subtype.to_bits()); + debug_assert_eq!(record.len(), STAT_RECORD_BYTES); + record +} + /// `stat`, by open handle when `path` is empty and by path otherwise. /// /// Both spellings are needed and neither is redundant: `f:seek("end")` has @@ -722,9 +761,8 @@ pub async fn stat(fs: &mut Fs<'_, '_>, files: &mut Files, handle: u64, path: &[u Ok(file) => &file.view, Err(error) => return refused(error), }; - let directory = view.is_directory(); return match fs.stat(view).await { - Ok(stat) => Answer::Value(stat.len, u64::from(directory)), + Ok(stat) => Answer::Bytes(stat.len, stat_record(&stat)), Err(error) => refusal(error), }; } @@ -732,9 +770,8 @@ pub async fn stat(fs: &mut Fs<'_, '_>, files: &mut Files, handle: u64, path: &[u Ok(view) => view, Err(error) => return refusal(error), }; - let directory = view.is_directory(); let answer = match fs.stat(&view).await { - Ok(stat) => Answer::Value(stat.len, u64::from(directory)), + Ok(stat) => Answer::Bytes(stat.len, stat_record(&stat)), Err(error) => refusal(error), }; fs.close(view).await; @@ -774,6 +811,74 @@ pub async fn rename(fs: &mut Fs<'_, '_>, from: &[u8], to: &[u8]) -> Answer { } } +/// A directory's edges, with what each one points at. +/// +/// [`list`] answers the *namespace* — the names a directory holds — and is +/// the right question most of the time. This answers the *graph*: each +/// edge with its target's class and subtype, so a script can tell a +/// directory from a file, or a native module from a plain file, without +/// opening and statting every name in turn. +/// +/// One record per entry: a kind byte, a subtype byte, then the name and a +/// NUL. A name is UTF-8 and may contain neither NUL nor `/`, so the +/// terminator is unambiguous. +/// +/// The target's `ObjectId` is deliberately **not** here. It is a +/// service-local slot key — "possessing this naming value conveys no +/// authority", and a stale one is a refusal rather than a mistake — but it +/// is also not stable across a restart, and a script that stored one as an +/// identity would be wrong in a way nothing would tell it. The stable +/// identity is [`stat`]'s, where it is named as such. +pub async fn entries(fs: &mut Fs<'_, '_>, path: &[u8]) -> Answer { + let mut records: Vec = Vec::new(); + let mut overflow = false; + let listed = fs + .list_directory(path, |entry| { + let name = entry.name.as_bytes(); + if records.len() + name.len() + 3 > MAX_READ_BYTES { + overflow = true; + return Err(Refusal::Fs(FsError::Range)); + } + records.push(u8::from(entry.kind.is_dir())); + records.push(entry.subtype.to_bits()); + records.extend_from_slice(name); + records.push(0); + Ok(()) + }) + .await; + match listed { + _ if overflow => refused(FsError::Range), + Ok(()) => { + let count = records.iter().filter(|byte| **byte == 0).count() as u64; + Answer::Bytes(count, records) + } + Err(error) => refusal(error), + } +} + +/// The rights this task actually holds on `path`. +/// +/// Answered as the store's own compact spelling — a subset of `rwns`, the +/// vocabulary `kore_dagfs::rights_label` prints and `rights_parse` reads — +/// rather than a set of booleans invented here. That matters beyond +/// tidiness: what a listing prints is what a `share` accepts, so a script +/// that reads rights here and hands them back later is speaking the same +/// language the store does. The letters are the contract; the booleans the +/// C half derives from them are a convenience over it. +/// +/// This reports authority, it never confers any: `effective_rights` walks +/// to the object with the rights the task already holds, and answers what +/// survived the walk. +pub async fn rights(fs: &mut Fs<'_, '_>, path: &[u8]) -> Answer { + match fs.effective_rights(path).await { + Ok(rights) => Answer::Bytes( + u64::from(rights.bits()), + kore::fs::dag::rights_label(rights).to_vec(), + ), + Err(error) => refusal(error), + } +} + /// List a directory as NUL-separated names. /// /// One buffer rather than a cursor per call: a directory listing is the diff --git a/crates/lua/src/kore_lua.c b/crates/lua/src/kore_lua.c index 59ad761..6f2d5eb 100644 --- a/crates/lua/src/kore_lua.c +++ b/crates/lua/src/kore_lua.c @@ -314,7 +314,12 @@ enum { a positional write once per window; this is its own replacement under one held commit, so a save is one commit however many windows it takes. */ - KORE_LUA_OP_FS_SAVE = 31 + KORE_LUA_OP_FS_SAVE = 31, + /* A directory's edges with each target's class and subtype, where + `KORE_LUA_OP_FS_LIST` answers only the names. */ + KORE_LUA_OP_FS_ENTRIES = 32, + /* The rights this task holds at a path, in the store's `rwns` spelling. */ + KORE_LUA_OP_FS_RIGHTS = 33 }; /* The error `os.exit` raises to unwind the evaluation. `luaL_error` puts the chunk and line in front of it, so the host matches this as a *suffix* @@ -1782,26 +1787,188 @@ static int kore_lua_fs_resize(lua_State *L) { return 1; } +/* + * The fixed record `src/files.rs` answers a stat with, little-endian. The + * length is checked before anything is read: a bridge whose halves + * disagree about the layout must refuse, not decode a field that moved. + */ +#define KORE_LUA_STAT_RECORD 34 + +static unsigned long long kore_lua_le64(const unsigned char *at) { + unsigned long long value = 0; + for (int i = 7; i >= 0; i--) value = (value << 8) | at[i]; + return value; +} + +static unsigned long kore_lua_le32(const unsigned char *at) { + unsigned long value = 0; + for (int i = 3; i >= 0; i--) value = (value << 8) | at[i]; + return value; +} + +/* + * What an object *contains*, as `ObjectSubtype` spells it. + * + * An unknown byte is carried, never flattened to "plain": the store's own + * rule is that "a newer writer's subtype is data an older reader passes + * through intact". So a subtype this build does not know leaves `subtype` + * nil while `subtype_bits` still reports it, and a script can round-trip + * or forward a value it cannot name. + */ +static const char *kore_lua_subtype_name(unsigned char bits) { + switch (bits) { + case 0: return "plain"; + case 1: return "executable-image"; + case 2: return "package-manifest"; + default: return NULL; + } +} + +/* + * `kore.fs.stat` — the store's answer whole, not the two fields of it this + * surface used to pass on. + * + * None of the added facts is authority: a stable id "conveys no authority" + * and a subtype "gates nothing". They are what a script needs to see the + * graph it is standing in — the revision it may be racing, the links + * keeping an object alive, the grants outstanding on it. + */ static int kore_lua_fs_stat(lua_State *L) { size_t len = 0; const char *path = luaL_checklstring(L, 1, &len); - unsigned long long length = 0, kind = 0; + unsigned long long length = 0, record_len = 0; + const char *bytes = NULL; + size_t bytes_len = 0; int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_STAT, 0, 0, 0, path, len, - &length, &kind, NULL, NULL, kore_lua_fail_table); + &length, &record_len, &bytes, &bytes_len, + kore_lua_fail_table); if (n) return n; - lua_createtable(L, 0, 3); - /* Rule 5: a size carries its unit in the name. */ - lua_pushinteger(L, (lua_Integer)length); - lua_setfield(L, -2, "size_bytes"); - lua_pushstring(L, kind ? "directory" : "file"); - lua_setfield(L, -2, "kind"); - lua_pushboolean(L, kind != 0); - lua_setfield(L, -2, "directory"); + if (bytes == NULL || bytes_len != KORE_LUA_STAT_RECORD) { + return kore_lua_fail_table(L, KORE_LUA_STATUS_UNAVAILABLE, + "stat record is not this build's layout", + sizeof "stat record is not this build's layout" - 1); + } + { + const unsigned char *at = (const unsigned char *)bytes; + unsigned char kind = at[32], subtype = at[33]; + const char *named = kore_lua_subtype_name(subtype); + lua_createtable(L, 0, 8); + /* Rule 5: a size carries its unit in the name. */ + lua_pushinteger(L, (lua_Integer)kore_lua_le64(at)); + lua_setfield(L, -2, "size_bytes"); + lua_pushinteger(L, (lua_Integer)kore_lua_le64(at + 8)); + lua_setfield(L, -2, "revision"); + lua_pushinteger(L, (lua_Integer)kore_lua_le64(at + 16)); + lua_setfield(L, -2, "id"); + lua_pushinteger(L, (lua_Integer)kore_lua_le32(at + 24)); + lua_setfield(L, -2, "links"); + lua_pushinteger(L, (lua_Integer)kore_lua_le32(at + 28)); + lua_setfield(L, -2, "grants"); + lua_pushstring(L, kind ? "directory" : "file"); + lua_setfield(L, -2, "kind"); + lua_pushboolean(L, kind != 0); + lua_setfield(L, -2, "directory"); + lua_pushinteger(L, (lua_Integer)subtype); + lua_setfield(L, -2, "subtype_bits"); + if (named != NULL) { + lua_pushstring(L, named); + lua_setfield(L, -2, "subtype"); + } + } + return 1; +} + +/* + * `kore.fs.entries` — a directory's edges, each with what it points at. + * + * `kore.fs.list` answers the namespace and is the right question most of + * the time; this answers the graph, so telling a directory from a file, or + * a native module from a plain one, costs one call instead of a stat per + * name. One record per entry: a kind byte, a subtype byte, the name, a + * NUL. A name is UTF-8 containing neither NUL nor `/`, so the terminator + * cannot be part of one. + */ +static int kore_lua_fs_entries(lua_State *L) { + size_t len = 0; + const char *path = luaL_checklstring(L, 1, &len); + unsigned long long count = 0, record_len = 0; + const char *bytes = NULL; + size_t bytes_len = 0; + int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_ENTRIES, 0, 0, 0, path, len, + &count, &record_len, &bytes, &bytes_len, + kore_lua_fail_table); + if (n) return n; + lua_createtable(L, (int)count, 0); + { + lua_Integer index = 0; + size_t at = 0; + while (at + 2 < bytes_len) { + unsigned char kind = (unsigned char)bytes[at]; + unsigned char subtype = (unsigned char)bytes[at + 1]; + const char *name = bytes + at + 2; + size_t name_len = 0; + const char *named; + while (at + 2 + name_len < bytes_len && name[name_len] != '\0') name_len++; + named = kore_lua_subtype_name(subtype); + lua_createtable(L, 0, 4); + lua_pushlstring(L, name, name_len); + lua_setfield(L, -2, "name"); + lua_pushstring(L, kind ? "directory" : "file"); + lua_setfield(L, -2, "kind"); + lua_pushboolean(L, kind != 0); + lua_setfield(L, -2, "directory"); + lua_pushinteger(L, (lua_Integer)subtype); + lua_setfield(L, -2, "subtype_bits"); + if (named != NULL) { + lua_pushstring(L, named); + lua_setfield(L, -2, "subtype"); + } + lua_rawseti(L, -2, ++index); + at += 2 + name_len + 1; + } + } + return 1; +} + +/* + * `kore.fs.rights` — what this task may actually do at a path. + * + * The store's own compact spelling is the answer, and the booleans are + * derived from it rather than beside it: `rights_label` prints what + * `rights_parse` reads, so the letters a script sees here are the letters + * a `share` will accept. Inventing a second vocabulary in C would be a + * second authority over one set of bits. + * + * Reports authority; confers none. + */ +static int kore_lua_fs_rights(lua_State *L) { + size_t len = 0; + const char *path = luaL_checklstring(L, 1, &len); + unsigned long long bits = 0, label_len = 0; + const char *label = NULL; + size_t bytes_len = 0; + int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_RIGHTS, 0, 0, 0, path, len, + &bits, &label_len, &label, &bytes_len, + kore_lua_fail_table); + if (n) return n; + lua_createtable(L, 0, 5); + lua_pushlstring(L, label != NULL ? label : "", label != NULL ? bytes_len : 0); + lua_setfield(L, -2, "spelling"); + { + static const char letters[] = "rwns"; + static const char *const names[] = {"read", "write", "mutate_namespace", + "share"}; + for (size_t i = 0; i < sizeof letters - 1; i++) { + int held = 0; + for (size_t j = 0; j < bytes_len; j++) + if (label[j] == letters[i]) held = 1; + lua_pushboolean(L, held); + lua_setfield(L, -2, names[i]); + } + } return 1; } -/* `exists` is a question, not a failure, so it answers a boolean and never - a `nil, err` pair a caller would have to unpack to learn "no". */ static int kore_lua_fs_exists(lua_State *L) { size_t len = 0; const char *path = luaL_checklstring(L, 1, &len); @@ -1916,6 +2083,8 @@ static const luaL_Reg kore_lua_fs_lib[] = { {"mkdir", kore_lua_fs_mkdir}, {"resize", kore_lua_fs_resize}, {"stat", kore_lua_fs_stat}, + {"entries", kore_lua_fs_entries}, + {"rights", kore_lua_fs_rights}, {"exists", kore_lua_fs_exists}, {"list", kore_lua_fs_list}, {NULL, NULL} diff --git a/crates/lua/src/lib.rs b/crates/lua/src/lib.rs index 62f616e..db8c279 100644 --- a/crates/lua/src/lib.rs +++ b/crates/lua/src/lib.rs @@ -18,13 +18,41 @@ //! kore.random.bytes(32) //! kore.proc.args -- a 1-based array, read once //! kore.fs.read_file "/etc/hosts" -- value, err -//! kore.fs.stat "/etc/hosts" -- size_bytes, kind, directory +//! kore.fs.stat "/etc/hosts" -- the object's record, as a table +//! kore.fs.entries "/libs" -- the directory's edges +//! kore.fs.rights "/etc" -- what this task may do there //! kore.has "fs" kore.caps() -- honest provisioning //! ``` //! //! `kore.fs` adds only what the stock surface has no spelling for — -//! `read_file`, `write_file`, `mkdir`, `resize`, `stat`, `exists`, `list` -//! — and never changes a stock member. +//! `read_file`, `write_file`, `mkdir`, `resize`, `stat`, `exists`, `list`, +//! `entries`, `rights` — and never changes a stock member. +//! +//! # The graph, not a POSIX projection +//! +//! `io` answers the questions POSIX can ask. `kore.fs` answers the ones +//! the store can, and the difference is not cosmetic: DagFS is a graph +//! whose objects carry a committed revision, a volume-stable identity, a +//! link count, outstanding grants and a subtype, and a surface that +//! reported only a size and a kind was hiding all of it. +//! +//! - [`files::stat`] answers the object's whole record. `revision` moves +//! on every commit, so a script can tell that a write landed underneath +//! it; `id` is stable across a service restart, so it is the identity to +//! remember where a path is not. +//! - [`files::entries`] answers a directory's *edges* — each name with its +//! target's class and subtype — where [`files::list`] answers the names. +//! Telling a native module from a plain file costs one call, not a stat +//! per name. +//! - [`files::rights`] answers what this task may do at a path, in the +//! store's own `rwns` spelling. +//! +//! None of it is authority. A stable id "conveys no authority", a subtype +//! "gates nothing", and `rights` reports what a walk already found rather +//! than conferring anything. Deriving a view, delegating one, and the +//! grant vocabulary are deliberately **absent** until MicroPerceptron/kore +//! #986 and #991 rule the capability model for a userland authority +//! domain — a surface invented here first would have to be retracted. //! //! ```lua //! local f = assert(io.open("/tmp/log", "a")) -- the SAME object diff --git a/crates/lua/tests/host_fs.c b/crates/lua/tests/host_fs.c index 9548404..1eea2ff 100644 --- a/crates/lua/tests/host_fs.c +++ b/crates/lua/tests/host_fs.c @@ -54,7 +54,9 @@ enum { HOST_FS_OP_LIST = 28, HOST_FS_OP_PROC_EXIT = 29, HOST_FS_OP_READ_FILE = 30, - HOST_FS_OP_SAVE = 31 + HOST_FS_OP_SAVE = 31, + HOST_FS_OP_ENTRIES = 32, + HOST_FS_OP_RIGHTS = 33 }; enum { @@ -89,6 +91,13 @@ struct host_fs_object { size_t len; int directory; int used; + /* The graph facts a stat answers. Modelled rather than invented: a + revision that moves on every commit, a stable identity that is not the + slot, and a subtype carried verbatim — which is the behaviour worth + testing, because an unknown subtype must survive the round trip. */ + unsigned long long revision; + unsigned long long stable; + unsigned char subtype; }; struct host_fs_handle { @@ -104,6 +113,7 @@ static struct { struct host_fs_object objects[HOST_FS_MAX_OBJECTS]; struct host_fs_handle handles[HOST_FS_MAX_HANDLES]; unsigned next_generation; + unsigned next_stable; /* The reply window. One buffer, overwritten by each call, exactly as the bridge's `cell.text` is: C copies it into a Lua string before it makes another call. The poison byte is the point — see the header. */ @@ -148,6 +158,16 @@ static struct { int refuse_close; } host_fs; +/* ---------------------------------------------------- little-endian out */ + +static void host_fs_put_le64(unsigned char *at, unsigned long long value) { + for (int i = 0; i < 8; i++) at[i] = (unsigned char)(value >> (8 * i)); +} + +static void host_fs_put_le32(unsigned char *at, unsigned long value) { + for (int i = 0; i < 4; i++) at[i] = (unsigned char)(value >> (8 * i)); +} + /* ------------------------------------------------------------- replies */ /* @@ -241,6 +261,11 @@ static int host_fs_make(const char *path, size_t len, int directory) { host_fs.objects[i].path[len] = '\0'; host_fs.objects[i].len = 0; host_fs.objects[i].directory = directory; + /* Not the slot: a stable id must not be reconstructible from position, + or a test could pass while the real one was being read wrong. */ + host_fs.objects[i].stable = 0x51ab1e00ull + (unsigned long long)(++host_fs.next_stable); + host_fs.objects[i].revision = 1; + host_fs.objects[i].subtype = 0; host_fs.objects[i].used = 1; return i; } @@ -318,6 +343,15 @@ void host_fs_short_writes(int cap) { host_fs.write_cap = cap > 0 ? cap : 0; } +/* Stamp an object's subtype, so a test can reach one this build names and + one it deliberately does not. */ +int host_fs_set_subtype(const char *path, int subtype) { + int at = host_fs_find(path, strlen(path)); + if (at < 0) return 0; + host_fs.objects[at].subtype = (unsigned char)subtype; + return 1; +} + int host_fs_exists(const char *path) { return host_fs_find(path, strlen(path)) >= 0; } @@ -533,6 +567,7 @@ int kore_lua_host_call(unsigned op, unsigned long long arg0, if (store == NULL) return host_fs_refuse("out-of-storage", text, text_len); if (input_len != 0) memcpy(store, input, input_len); object->len = input_len; + object->revision++; *result = (unsigned long long)input_len; *result2 = (unsigned long long)input_len; return HOST_FS_OK; @@ -560,6 +595,7 @@ int kore_lua_host_call(unsigned op, unsigned long long arg0, memset(store + object->len, 0, at - object->len); if (taken != 0) memcpy(store + at, input, taken); if (at + taken > object->len) object->len = at + taken; + if (taken != 0) object->revision++; *result = taken; *result2 = at + taken; return HOST_FS_OK; @@ -585,6 +621,7 @@ int kore_lua_host_call(unsigned op, unsigned long long arg0, if (length > object->len) memset(store + object->len, 0, length - object->len); object->len = length; + object->revision++; *result = length; return HOST_FS_OK; } @@ -601,8 +638,70 @@ int kore_lua_host_call(unsigned op, unsigned long long arg0, if (at < 0) return host_fs_refuse("not-found", text, text_len); object = &host_fs.objects[at]; } + /* The fixed record src/files.rs encodes, little-endian; the C half + checks its length before reading a field. */ + unsigned char record[34]; + host_fs_put_le64(record, object->len); + host_fs_put_le64(record + 8, object->revision); + host_fs_put_le64(record + 16, object->stable); + host_fs_put_le32(record + 24, 1); /* one name per object here */ + host_fs_put_le32(record + 28, 0); /* the harness grants nothing */ + record[32] = (unsigned char)(object->directory != 0); + record[33] = object->subtype; + host_fs_reply(record, sizeof record, text, text_len); *result = object->len; - *result2 = (unsigned long long)object->directory; + *result2 = sizeof record; + return HOST_FS_OK; + } + + /* A directory's edges, each with its target's class and subtype. */ + case HOST_FS_OP_ENTRIES: { + int at = host_fs_find(path, input_len); + if (at < 0) return host_fs_refuse("not-found", text, text_len); + if (!host_fs.objects[at].directory) + return host_fs_refuse("not-directory", text, text_len); + unsigned char records[HOST_FS_READ_MAX]; + size_t len = 0; + unsigned long long count = 0; + for (int i = 0; i < HOST_FS_MAX_OBJECTS; i++) { + if (!host_fs.objects[i].used || i == at) continue; + char parent[HOST_FS_PATH_MAX]; + size_t parent_len = 0; + size_t whole = strlen(host_fs.objects[i].path); + host_fs_parent(host_fs.objects[i].path, whole, parent, &parent_len); + if (parent_len != input_len || memcmp(parent, path, input_len) != 0) + continue; + size_t cut = whole; + while (cut > 0 && host_fs.objects[i].path[cut - 1] != '/') cut--; + size_t leaf = whole - cut; + if (len + leaf + 3 > sizeof records) + return host_fs_refuse("range", text, text_len); + records[len++] = (unsigned char)(host_fs.objects[i].directory != 0); + records[len++] = host_fs.objects[i].subtype; + memcpy(records + len, host_fs.objects[i].path + cut, leaf); + len += leaf; + records[len++] = '\0'; + count++; + } + host_fs_reply(records, len, text, text_len); + *result = count; + *result2 = len; + return HOST_FS_OK; + } + + /* + * The rights the task holds at a path, in the store's own `rwns` + * spelling. The harness grants `rwn` on everything it holds and + * nothing on what it does not: enough for the C half's derivation of + * booleans from letters to be exercised, including a letter it must + * report as absent. + */ + case HOST_FS_OP_RIGHTS: { + int at = host_fs_find(path, input_len); + if (at < 0) return host_fs_refuse("not-found", text, text_len); + host_fs_reply("rwn", 3, text, text_len); + *result = 7; + *result2 = 3; return HOST_FS_OK; } diff --git a/crates/lua/tests/host_smoke.c b/crates/lua/tests/host_smoke.c index 5e6320a..a3e6beb 100644 --- a/crates/lua/tests/host_smoke.c +++ b/crates/lua/tests/host_smoke.c @@ -176,6 +176,13 @@ static int test_short_writes(lua_State *L) { return 0; } +/* Stamp a subtype, including one this build deliberately cannot name. */ +static int test_set_subtype(lua_State *L) { + lua_pushboolean(L, host_fs_set_subtype(luaL_checkstring(L, 1), + (int)luaL_checkinteger(L, 2))); + return 1; +} + static int test_open_library(lua_State *L) { static const luaL_Reg entries[] = { {"fail_reads_after", test_fail_reads_after}, @@ -183,6 +190,7 @@ static int test_open_library(lua_State *L) { {"max_read_want", test_max_read_want}, {"reset_max_read_want", test_reset_max_read_want}, {"short_writes", test_short_writes}, + {"set_subtype", test_set_subtype}, {NULL, NULL} }; luaL_newlib(L, entries); @@ -651,6 +659,110 @@ static int filesystem_smoke(void) { } +/* + * The graph a script can see. + * + * `kore.fs` answered a size and a kind where the store answers eight + * facts, which is the sense in which it hid the graph: a script could not + * see the revision it was racing, the links keeping an object alive, or + * the grants outstanding on it. None of that is authority — a stable id + * "conveys no authority", a subtype "gates nothing" — so this widens what + * a script may know without widening what it may do, which is the only + * reason it can land before the rights/grant rulings do. + */ +static int graph_smoke(void) { + static const char suite[] = + "assert(kore.fs.write_file('/g.txt','abc')==3)\n" + "local st = assert(kore.fs.stat('/g.txt'))\n" + "assert(st.size_bytes==3 and st.kind=='file' and st.directory==false)\n" + /* The facts this surface used to drop. */ + "assert(math.type(st.revision)=='integer' and st.revision>0)\n" + "assert(math.type(st.id)=='integer' and st.id>0)\n" + "assert(st.links==1 and st.grants==0)\n" + "assert(st.subtype=='plain' and st.subtype_bits==0)\n" + + /* A revision moves when the object commits, which is what makes it + usable for detecting a write that landed underneath you. */ + "local before = st.revision\n" + "assert(kore.fs.write_file('/g.txt','defg')==4)\n" + "local after = assert(kore.fs.stat('/g.txt'))\n" + "assert(after.revision>before, 'a commit must move the revision')\n" + /* The identity does not move with it: same object, later revision. */ + "assert(after.id==st.id, 'a write is not a new object')\n" + /* And a resize is a commit too. */ + "local h = assert(io.open('/g.txt','r+'))\n" + "assert(kore.fs.resize(h,2))\n" + "assert(h:close())\n" + "assert(assert(kore.fs.stat('/g.txt')).revision>after.revision)\n" + + /* A subtype this build names, and one it does not. The unknown one + must survive intact rather than be flattened to `plain`: a newer + writer's subtype is data an older reader passes through. */ + "assert(kore.test.set_subtype('/g.txt',1))\n" + "local exe = assert(kore.fs.stat('/g.txt'))\n" + "assert(exe.subtype=='executable-image' and exe.subtype_bits==1)\n" + "assert(kore.test.set_subtype('/g.txt',200))\n" + "local odd = assert(kore.fs.stat('/g.txt'))\n" + "assert(odd.subtype==nil, 'an unnamed subtype must not be guessed')\n" + "assert(odd.subtype_bits==200, 'but it must still be reported')\n" + "assert(kore.test.set_subtype('/g.txt',0))\n" + + /* A directory stats as one. */ + "assert(kore.fs.mkdir('/gd'))\n" + "local d = assert(kore.fs.stat('/gd'))\n" + "assert(d.kind=='directory' and d.directory==true and d.subtype=='plain')\n" + + /* ------------------------------------------------------- entries */ + "assert(kore.fs.write_file('/gd/one','1')==1)\n" + "assert(kore.fs.mkdir('/gd/sub'))\n" + "assert(kore.test.set_subtype('/gd/one',1))\n" + "local e = assert(kore.fs.entries('/gd'))\n" + "assert(#e==2, 'two edges')\n" + "local by = {}\n" + "for _,row in ipairs(e) do by[row.name]=row end\n" + "assert(by.one and by.one.kind=='file' and by.one.directory==false)\n" + "assert(by.one.subtype=='executable-image' and by.one.subtype_bits==1)\n" + "assert(by.sub and by.sub.kind=='directory' and by.sub.directory==true)\n" + /* The namespace verb still answers names, and the two agree. */ + "local names = assert(kore.fs.list('/gd'))\n" + "assert(#names==#e)\n" + "for _,name in ipairs(names) do assert(by[name], name) end\n" + /* An empty directory has no edges, which is not a failure. */ + "assert(kore.fs.mkdir('/empty'))\n" + "assert(#assert(kore.fs.entries('/empty'))==0)\n" + /* Asking a file for its edges, or a path that is not there. */ + "local ne, nerr = kore.fs.entries('/gd/one')\n" + "assert(ne==nil and nerr.code=='not-directory', tostring(nerr))\n" + "local me, merr = kore.fs.entries('/nowhere')\n" + "assert(me==nil and merr.code=='not-found', tostring(merr))\n" + + /* -------------------------------------------------------- rights */ + /* The store's own spelling is the answer; the booleans are derived + from it, so the letters a script reads are the letters a share + would accept. */ + "local r = assert(kore.fs.rights('/g.txt'))\n" + "assert(r.spelling=='rwn', tostring(r.spelling))\n" + "assert(r.read==true and r.write==true and r.mutate_namespace==true)\n" + "assert(r.share==false, 'a right not held must read false, not nil')\n" + "local rr, rerr = kore.fs.rights('/nowhere')\n" + "assert(rr==nil and rerr.code=='not-found', tostring(rerr))\n" + "return true\n"; + + host_fs_reset(""); + struct capture capture = {{0}, 0}; + lua_State *L = kore_lua_new(host_alloc, NULL, 0x4b4f5245u, test_libraries, + sizeof test_libraries / sizeof test_libraries[0]); + if (L == NULL) return 1; + if (kore_lua_install_print(L, capture_append, &capture) != LUA_OK) { + kore_lua_close(L); return 2; + } + if (!run_reporting(L, suite, "graph suite")) { kore_lua_close(L); return 3; } + kore_lua_set_top(L, 0); + if (host_fs_open_handles() != 0) { kore_lua_close(L); return 4; } + kore_lua_close(L); + return 0; +} + /* * The write path: a whole-file save, and what a short write answers. * @@ -1047,6 +1159,8 @@ int main(void) { host_fs_reset(""); int fs_status = filesystem_smoke(); if (fs_status != 0) return 30 + fs_status; + int graph_status = graph_smoke(); + if (graph_status != 0) return 90 + graph_status; int write_status = write_smoke(); if (write_status != 0) return 50 + write_status; int stream_status = streaming_smoke(); From 6b461720a45b5e260919bda6a644f29d3c2cb993 Mon Sep 17 00:00:00 2001 From: Julian Avar Date: Mon, 5 Oct 2026 12:45:25 -0400 Subject: [PATCH 09/13] docs: the Lua surface, both spellings side by side MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `docs/filesystem.md` documents the Rust facade and `languages.md` the C/Zig ABI; the Lua surface had only its crate's module header, which is the wrong place to look something up from a script. Organised around the thing that is actually true of it: `kore.*` is the store's vocabulary spelled for Lua, and `io`/`os` is the same code with the capabilities defaulted. So every verb is listed in both spellings, and the mode-string-to-rights mapping is stated once as a table rather than left to be inferred — including the two rights this surface never requests, `SHARE` and `BIND_EXECUTABLE`, which are the ceiling on everything a Lua script can reach. Per-verb sections carry the shapes a caller needs and the reasons where a reason exists: why `resize` takes a handle and not a path, why `entries` omits the target's object id, why an unknown subtype is absent rather than `"plain"`, why `exists` is the one verb with no failure spelling. Deviations from stock Lua are tabulated rather than scattered — `os.getenv` always nil, `os.clock` measured from VM start rather than process CPU, `os.exit` recording and unwinding, `f:flush` a durability barrier. Both error spellings and the full errno mapping are in one place. Marked aspirational, per AGENTS.md: a closing section lists what is not exposed and what blocks it — unblocked and wanted, blocked on MicroPerceptron/kore #986/#991, or blocked on the `index` feature. Storage-ring streaming is described as the shape that can carry it rather than as present behaviour, because the pinned export has no ring yet. Co-Authored-By: Claude Opus 5 (1M context) --- docs/lua.md | 413 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 docs/lua.md diff --git a/docs/lua.md b/docs/lua.md new file mode 100644 index 0000000..f90e110 --- /dev/null +++ b/docs/lua.md @@ -0,0 +1,413 @@ +# Lua on Kore + +Lua 5.5, freestanding, over the task's own filesystem face. A script sees two +surfaces and they are one implementation: **`kore.*` is the store's vocabulary +spelled for Lua, and `io`/`os` is the same code with the capabilities +defaulted.** `io.open` and `kore.fs.open` return the *same* object; only the +failure spelling differs. + +Which to reach for: `io`/`os` when a script is doing what a POSIX script does, +and `kore.*` when it needs something the store knows and POSIX cannot say — a +revision, a subtype, the rights it actually holds. + +- [The capability default](#the-capability-default) +- [Quick reference](#quick-reference) +- [`kore.fs`](#korefs) +- [The file object](#the-file-object) +- [`io` and `os`](#io-and-os) +- [`kore.time`](#koretime) +- [`kore.random`](#korerandom) +- [`kore.proc`](#koreproc) +- [`kore.has` and `kore.caps`](#korehas-and-korecaps) +- [`require`](#require) +- [Errors](#errors) +- [Absent, and why](#absent-and-why) +- [Not yet exposed](#not-yet-exposed) + +## The capability default + +DagFS authority is a right set on a route, not a mode bit on a file. The stock +surface has no way to say that, so it says it by convention instead: a mode +string picks the rights, and the surface never asks for more. + +| Spelling | Rights requested | On the parent | +|---|---|---| +| `"r"` | `READ` | — | +| `"w"` | `READ WRITE` + create + truncate | `READ MUTATE_NAMESPACE` to create | +| `"a"` | `READ WRITE` + create, writes at the end | `READ MUTATE_NAMESPACE` to create | +| `"r+"` | `READ WRITE`, does not create | — | +| `"w+"`, `"a+"` | `READ WRITE` + create | `READ MUTATE_NAMESPACE` to create | +| `"b"` | accepted, ignored — kore has no text mode to differ from | +| `remove`, `rename`, `mkdir` | — | `READ MUTATE_NAMESPACE` | + +The *route* carries `READ` in every row; the *file object* still remembers what +the mode asked for, so `f:read` on a `"w"` file is `access-denied` rather than a +read the route would in fact have permitted. The mode governs the object, the +rights govern the route, and they are not the same question. + +**`READ` is always requested**, because a route that cannot read cannot take a +single step. Two rights are **never** requested by this surface: `SHARE` and +`BIND_EXECUTABLE`. That is the ceiling of everything below — no Lua script can +re-share a route or bind an executable through these verbs, whatever the task +itself holds. + +`kore.fs.rights(path)` reports what the task actually has, which is not the +same question as what a mode string asked for. + +## Quick reference + +Every verb, both spellings. `—` means the surface has no spelling for it. + +### Bytes + +| What | `kore.*` | stock | +|---|---|---| +| Open a file | `kore.fs.open(path, mode)` | `io.open(path, mode)` | +| Read a whole file | `kore.fs.read_file(path)` | — | +| Write a whole file | `kore.fs.write_file(path, bytes)` | — | +| Read from a handle | `f:read(...)` | `f:read(...)`, `io.read(...)` | +| Write to a handle | `f:write(...)` | `f:write(...)`, `io.write(...)` | +| Iterate lines | `f:lines(...)` | `io.lines([path, ...])` | +| Move the position | `f:seek(whence, offset)` | same | +| Durability barrier | `f:flush()` | `io.flush()` | +| Truncate or extend | `kore.fs.resize(file, bytes)` | — | +| Close | `f:close()` | `io.close([file])` | +| Is it closed? | `f:closed()` | — | + +### Namespace and graph + +| What | `kore.*` | stock | +|---|---|---| +| Does it exist? | `kore.fs.exists(path)` | — | +| The object's record | `kore.fs.stat(path)` | — | +| Names in a directory | `kore.fs.list(path)` | — | +| Edges in a directory | `kore.fs.entries(path)` | — | +| Rights held there | `kore.fs.rights(path)` | — | +| Make a directory | `kore.fs.mkdir(path)` | — | +| Remove a name | `kore.fs.remove(path)` | `os.remove(path)` | +| Rename or move | `kore.fs.rename(from, to)` | `os.rename(from, to)` | +| A temporary name | — | `os.tmpname()`, `io.tmpfile()` | + +### Everything else + +| What | `kore.*` | stock | +|---|---|---| +| Monotonic seconds | `kore.time.now()` — since boot | `os.clock()` — since this VM started | +| Wall seconds | `kore.time.wall()` | `os.time()` — integer | +| Exact nanoseconds | `kore.time.mono_ns()`, `kore.time.wall_ns()` | — | +| Sleep | `kore.time.sleep(seconds)` | — | +| Format a time | — | `os.date(format, [time])` | +| Random bytes | `kore.random.bytes(n)` | — | +| Random integer | `kore.random.integer(lo, hi)` | `math.random(lo, hi)` | +| Launch arguments | `kore.proc.args` | — | +| Terminate | — | `os.exit([code])` | +| What is provisioned | `kore.has(name)`, `kore.caps()` | — | + +## `kore.fs` + +Every verb answers `value, err` on failure, where `err` is a table (see +[Errors](#errors)). Paths resolve inside the task's endowed view; there is no +global root. + +### `kore.fs.open(path, mode) -> file | nil, err` + +The same object `io.open` answers, with the table failure spelling. `mode` is +the stock Lua mode string ([the capability default](#the-capability-default)). +Opening a directory is `is-directory`; use `mkdir` to make one and `entries` or +`list` to read one. + +### `kore.fs.read_file(path) -> string | nil, err` + +A whole file in one host call — the host opens, reads and closes under **one +pinned version**, so the answer is one revision of the file rather than a seam +of several. Looping `f:read` cannot give you that: each call pins and releases +its own version, so a concurrent writer can leave the result a mix of revisions. + +It is also the shape that *can* stream. A whole-file read of at least 256 KiB +hands the facade a destination large enough to carry by DMA through the task's +storage ring instead of a window at a time — on a native owner that has one. +The export this build pins does not yet; see `docs/pack-backed-reads.md`. The +verb does not change when it lands. + +An empty file answers `""`, never `nil`. Bounded at 16 MiB, past which the +answer is `range` — the file is readable, it is having all of it at once that +is refused. + +### `kore.fs.write_file(path, bytes) -> count | nil, err` + +Replaces the file's whole contents, creating it if absent. One replacement +transaction under a held commit, so a large save is **one** commit rather than +one per window. The open deliberately does not truncate: a save that then fails +leaves the previous contents intact. + +### `kore.fs.resize(file, bytes) -> true | nil, err` + +Takes an **open file**, not a path — truncation is a handle operation. A +shortened file drags the stream position back with it, so the next write cannot +land past a hole nobody asked for. + +### `kore.fs.stat(path) -> table | nil, err` + +The object's whole record. None of it is authority — a stable id conveys none +and a subtype gates nothing. + +| Field | Meaning | +|---|---| +| `size_bytes` | integer | +| `kind` | `"file"` or `"directory"` | +| `directory` | boolean, the same fact as a predicate | +| `revision` | monotonic, moves on every commit | +| `id` | volume-stable identity; survives a service restart | +| `links` | names pointing at this object | +| `grants` | grants outstanding on it | +| `subtype` | `"plain"`, `"executable-image"`, `"package-manifest"`, or **absent** | +| `subtype_bits` | the raw byte, always present | + +`revision` is the field to remember if you intend to notice that a write landed +underneath you; `id` is the identity to remember where a path is not. + +`subtype` is absent — not `"plain"` — when this build cannot name the byte. A +newer writer's subtype is data an older reader passes through intact, so +`subtype_bits` still reports it and a script can forward a value it cannot name. + +### `kore.fs.entries(path) -> array | nil, err` + +A directory's **edges**, where `list` answers its **names**. Each element is +`{name, kind, directory, subtype?, subtype_bits}` — the same spelling `stat` +uses. Telling a native module from a plain file costs one call rather than a +stat per name. + +The target's object id is deliberately not here: it is a service-local slot key, +not stable across a restart, and a script that stored one as an identity would +be wrong in a way nothing would tell it. The stable one is `stat`'s. + +An empty directory answers an empty array. A file answers `not-directory`. + +### `kore.fs.rights(path) -> table | nil, err` + +What this task may actually do there. Reports authority; confers none. + +| Field | Meaning | +|---|---| +| `spelling` | the store's own compact form, a subset of `rwns` | +| `read`, `write`, `mutate_namespace`, `share` | booleans derived from the letters | + +`spelling` is the contract: what a listing prints is what a `share` accepts, so +rights read here can be handed back later unchanged. The booleans are a +convenience over it, not a second vocabulary. + +### `kore.fs.list(path) -> array | nil, err` + +The names a directory holds, as an array of strings. The namespace question; +`entries` is the graph one. + +### `kore.fs.exists(path) -> boolean` + +The one verb with no failure spelling: a path that cannot be stat'd does not +exist, as far as this task is concerned. + +### `kore.fs.mkdir(path) -> true | nil, err` + +Makes the final component. An existing name is `already-exists`; a missing +parent is `not-found`. + +### `kore.fs.remove(path)`, `kore.fs.rename(from, to) -> true | nil, err` + +`remove` detaches the name. `rename` is the store's own move, including across +directories — not a copy and unlink, so the object keeps its identity and its +grants. Removing the root is `access-denied`. + +## The file object + +One userdata, whichever surface opened it. Seven stock methods plus `closed`. + +| Method | Answers | +|---|---| +| `f:read(...)` | `"a"` whole rest, `"l"` line, `"L"` line with newline, `"n"` number, or a byte count | +| `f:write(...)` | the file, so writes chain | +| `f:lines(...)` | an iterator over the read formats | +| `f:seek([whence [, offset]])` | the new position; `"set"`, `"cur"`, `"end"` | +| `f:flush()` | `true` — a **durability barrier**, not a buffer drain | +| `f:setvbuf(mode)` | `true`; accepted and inert, kore has no stdio buffer to tune | +| `f:close()` | `true`; idempotent | +| `f:closed()` | boolean, without raising | + +The position is the file object's, not the store's: DagFS addresses bytes +absolutely, and a stream position is a Lua idea layered on top. Reads ahead by +a page — so `f:read("l")` is a scan over bytes already here — and a read of at +least 256 KiB bypasses that buffer entirely, asking for the whole remainder at +once. A page-sized ask can never be large enough for the facade to stream, +however large the file is, so the decision has to be made on this side. + +**Lifetime.** `__close` and `__gc` both retire the handle, with deterministic +close primary, so a to-be-closed local is the idiom: + +```lua +do + local f = assert(io.open("/etc/hosts")) + for line in f:lines() do print(line) end +end -- retired here, whatever happened +``` + +Using a closed file **raises** — it is a programming error, not an expected +failure. A handle whose slot was recycled is `invalid-handle`, never an alias of +whatever took its place. + +## `io` and `os` + +Stock Lua, conforming, with the deviations below. Every member either conforms +fully or is [absent for a stated reason](#absent-and-why) — never present and +partial. + +| Member | Deviation | +|---|---| +| `os.getenv(name)` | always `nil` — a kore task has no environment; authority arrives as capabilities, not inherited strings, and `nil` is exactly what an unset variable means | +| `os.date`, `os.time` | kore keeps one clock and it reads UTC, so `!` changes nothing and `isdst` is always `false`; the calendar is computed, not tabulated | +| `os.setlocale` | answers `"C"` and `nil` for anything else | +| `os.exit([code])` | **records** the code and unwinds by raising, so `` handlers and C frames below the VM still run; the embedder reads the code afterwards | +| `os.tmpname()` | draws a name under `/tmp`, creates the file exclusively, then closes it — the POSIX behaviour the manual describes | +| `io.lines(name)` | answers four values, the last to-be-closed | +| `f:flush()` | a durability barrier; writes are already acknowledged before `f:write` returns | +| `os.clock()` | elapsed monotonic seconds **since this VM started**, not process CPU time — kore meters no per-process CPU, and a number that silently meant something else would be worse than a different origin | + +## `kore.time` + +Seconds are floats; anything else carries its unit in its name. A clock that +cannot be read answers `nil, why` — never a zero timestamp, which a caller would +read as 1970 rather than as "no reading". + +| Verb | Answers | +|---|---| +| `kore.time.now()` | monotonic seconds, float | +| `kore.time.mono_ns()` | monotonic nanoseconds, integer | +| `kore.time.wall()` | wall seconds, float | +| `kore.time.wall_ns()` | wall nanoseconds, integer | +| `kore.time.sleep(seconds)` | `true`; parks the VM thread, never spins | + +A double holds 53 bits of mantissa, so a wall reading keeps roughly 256 ns of +resolution. Use the `_ns` forms when every nanosecond matters. + +## `kore.random` + +A source, not a stream: every byte is a fresh CPU draw. A script wanting many +cheap values should use `math.random`, which this build seeds from the same +source at startup. + +| Verb | Answers | +|---|---| +| `kore.random.bytes(n)` | `n` bytes as a string, `0 <= n <= 4096` | +| `kore.random.integer(lo, hi)` | a uniform integer in `[lo, hi]` | +| `kore.random.available()` | boolean | + +`integer` masks and redraws rather than folding with a modulo, so every value is +equally likely and `kore.random.integer` and `math.random` agree on what uniform +means. A dead entropy source ends the loop with its refusal rather than spinning. + +## `kore.proc` + +`kore.proc.args` is a **lazy field**, not a call: a 1-based array split from the +launch bytes on ASCII whitespace, fetched once on first touch and cached on the +module table. The bridge is crossed exactly once per process. + +Termination is `os.exit`, on the stock surface where a script already looks for +it. + +## `kore.has` and `kore.caps` + +Provisioning is honest: a submodule whose authority the launch did not convey is +**absent**, not present-and-always-failing. So `if kore.fs then` is the idiom. + +- `kore.has(name) -> boolean` +- `kore.caps() -> array` — the module names in registration order, which is + stable across boots where a table walk would not be. + +An embedder's own libraries list themselves here; there is no second place to +remember. + +## `require` + +`require ""` resolves against the task's own tree first, then `libs/`; a +source module `.lua` wins over a native `.so`. Source modules are +bounded at 64 KiB — larger sources belong in the store as several modules. + +A native module is a store object stamped `EXECUTABLE_IMAGE` that is checked, +placed in the JIT arena, and linked against this executable's exported C API. It +must export `luaopen_`. + +## Errors + +**One vocabulary, two spellings.** Both are derived from the same `FsError`, so +the surfaces cannot disagree about what happened — only about how to say it. + +```lua +local f, err = kore.fs.open("/nowhere", "r") +-- err.code == "not-found", err.kind == "refused", tostring(err) == "not-found" + +local g, why, errno = io.open("/nowhere", "r") +-- why == "not-found", errno == 2 +``` + +`err.kind` is `"refused"` when the service named a reason and `"unavailable"` +when the call could not be made at all — no filesystem face, a broken ring. The +distinction matters: `"unavailable"` is not the file's fault. + +Expected failures answer `nil, err`; programming errors raise. A missing file is +the first; using a closed file is the second. + +| Code | errno | Code | errno | +|---|---|---|---| +| `not-found` | `ENOENT` 2 | `out-of-storage` | `ENOSPC` 28 | +| `already-exists` | `EEXIST` 17 | `out-of-handles` | `EMFILE` 24 | +| `not-directory` | `ENOTDIR` 20 | `range` | `ERANGE` 34 | +| `is-directory` | `EISDIR` 21 | `unsupported` | `ENOSYS` 38 | +| `invalid-name` | `EINVAL` 22 | `busy` | `EBUSY` 16 | +| `invalid-handle` | `EBADF` 9 | `cycle` | `ELOOP` 40 | +| `access-denied` | `EACCES` 13 | `not-empty` | `ENOTEMPTY` 39 | +| `access-lost` | `EBADF` 9 | anything else | `EIO` 5 | + +A code with no POSIX analogue answers `EIO` — the honest "something below +failed". The kore spelling always carries the exact word. + +## Absent, and why + +Absent for a reason, never half-present. + +| Member | Why | +|---|---| +| `os.execute`, `io.popen` | both launch a process, which is a service face the embedding program holds and this crate deliberately does not. An embedder adds them through its own library. | +| `string.format` | kore has no hosted `printf`, and a hand-written subset would accept format strings it silently did not conform to | +| `load`, `loadfile`, `dofile`, `string.dump` | this build accepts text chunks only; upstream Lua has no bytecode verifier | + +Lua's base, coroutine, math, string, table and UTF-8 libraries are all present. + +## Not yet exposed + +**Aspirational — none of this is implemented.** Listed so the shape of the gap +is visible, not as a promise of spelling. + +Unblocked, and wanted: + +- `kore.fs.open` by **rights** (`"rwn"`) rather than a POSIX mode — the + surface's clearest remaining POSIX-ism, now that `rights` reports the same + vocabulary +- `search` — the query vocabulary is settled in the native export +- grant **listing** (what this task has shared out) — reporting, like `rights` +- creating a file with a subtype, so a Lua installer can write what `require` + loads +- holding commits across several writes, which `write_file` uses internally and + no script can ask for +- `stat` on an open file, which the bridge already supports + +Blocked upstream on MicroPerceptron/kore#986 and #991, which rule the capability +model for a userland authority domain — a spelling invented here first would +have to be retracted: + +- creating a grant, revoking one, opening a view through one +- attenuating a view (projection) + +Blocked on the `index` feature, an image-size decision for this target: + +- index publication on save, and derived-version queries + +See `docs/pack-backed-reads.md` for the storage-transport work these sit beside, +and `docs/filesystem.md` for the Rust facade underneath all of it. From 7d621ebe46ebb8143ee2fdc589ce19df14fa2e6d Mon Sep 17 00:00:00 2001 From: Julian Avar Date: Mon, 5 Oct 2026 14:33:47 -0400 Subject: [PATCH 10/13] lua: kore.fs.open takes rights, not a POSIX mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `kore.fs` has answered the graph since `stat`, `entries` and `rights` landed, but it still took authority in POSIX's spelling. `kore.fs.rights` prints `"rwn"`; `kore.fs.open` would not accept it. The surface reported rights in one vocabulary and accepted them in another. A mode string fuses two questions. `"w"` is write *and* create *and* truncate, which is a projection of a right set onto three behaviours. So they separate: authority is the rights spelling, behaviour is a trailing options table (`create`, `truncate`, `append`, `exclusive`). The spelling is not interpreted in C. It crosses packed into one argument word as ASCII and `kore_dagfs::rights_parse` — the parser whose output `rights_label` prints — does the mapping. A copy of that mapping in C would be a second authority over one set of bits, which is what `kore.fs.rights` already avoids by deriving its booleans from the store's own letters. `n` and `s` are refused by name. A route carrying `MUTATE_NAMESPACE` or `SHARE` is useless while no verb here can link or share, and accepting the letter would claim a reach this surface does not have. They unlock with their verbs, not before. `io.open` converges onto the same path: its mode is translated to a spelling and a set of options in C, so the capability default documented in `docs/lua.md` lives in one place rather than being derived again on the far side. Nothing about `io` changes, and its suite is the witness — it passes unaltered. The break is loud by construction. `"r"` is unchanged; `"w"` becomes a read-write route that does not create, so a missing file is `not-found`; `"a"`, `"r+"`, `"w+"` and `"rb"` raise, because `a`, `+` and `b` are not rights letters. The argument error names the syntax. `route_rights` is unit-tested directly — the host smoke reaches only the harness's model of the rule — and the harness now models a spelling it will not open a route with, so `usage` is reachable from a host build. Verified by negative control: accepting an unreachable right fails the open suite at its `'rn'` assertion. Co-Authored-By: Claude Opus 5 (1M context) --- crates/lua/src/bridge.rs | 5 +- crates/lua/src/files.rs | 131 +++++++++++++++++++++++-- crates/lua/src/kore_lua.c | 174 ++++++++++++++++++++++++++++++---- crates/lua/tests/host_fs.c | 46 +++++++++ crates/lua/tests/host_smoke.c | 122 +++++++++++++++++++++++- docs/lua.md | 61 +++++++++--- 6 files changed, 497 insertions(+), 42 deletions(-) diff --git a/crates/lua/src/bridge.rs b/crates/lua/src/bridge.rs index 0932fff..713bc9e 100644 --- a/crates/lua/src/bridge.rs +++ b/crates/lua/src/bridge.rs @@ -1044,7 +1044,10 @@ async fn serve_fs( } }; match op { - OP_FS_OPEN => crate::files::open(fs, files, input, args[0]).await, + // `arg0` is the handle's permissions and the open's behaviour; + // `arg1` is the route's rights, as packed ASCII the far side parses + // with the store's own `rights_parse`. + OP_FS_OPEN => crate::files::open(fs, files, input, args[0], args[1]).await, OP_FS_CLOSE => crate::files::close(fs, files, args[0]).await, OP_FS_READ => crate::files::read(fs, files, args[0], args[1], args[2]).await, OP_FS_READ_FILE => crate::files::read_file(fs, input).await, diff --git a/crates/lua/src/files.rs b/crates/lua/src/files.rs index 54f202a..885d8a4 100644 --- a/crates/lua/src/files.rs +++ b/crates/lua/src/files.rs @@ -273,6 +273,42 @@ pub mod mode { pub const DIRECTORY: u64 = 1 << 6; } +/// How many ASCII letters of a rights spelling one argument word carries. +/// +/// `rights_label` spells four bits, so eight is already twice what the +/// store's vocabulary can need. The spelling crosses as bytes rather than +/// as a bit pattern so that [`rights_parse`] stays the one thing that +/// decides what a letter means — the C half packs characters it does not +/// interpret. +/// +/// [`rights_parse`]: kore::fs::dag::rights_parse +const RIGHTS_SPELLING_BYTES: usize = 8; + +/// The route rights a spelling asks for. +/// +/// `None` is a spelling this surface will not open a route with, which the +/// caller renders as an argument error rather than a refusal: both a +/// malformed spelling and an unreachable right are mistakes in the script, +/// not conditions of the store. +/// +/// Unreachable, for now, is `MUTATE_NAMESPACE` and `SHARE`. A route +/// carrying either is useless while no verb here can link or share, and +/// accepting the letter would claim a reach this surface does not have. +/// They unlock with the verbs, not before (`docs/kore-fs-vocabulary.md`). +fn route_rights(word: u64) -> Option { + let bytes = word.to_le_bytes(); + let spelling = match bytes.iter().position(|byte| *byte == 0) { + Some(end) => &bytes[..end], + None => &bytes[..RIGHTS_SPELLING_BYTES], + }; + let rights = kore::fs::dag::rights_parse(spelling)?; + let unreachable = Rights::MUTATE_NAMESPACE.union(Rights::SHARE); + if rights.intersection(unreachable) != Rights::NONE { + return None; + } + Some(rights) +} + /// Split a path into its parent and final component. /// /// `None` when the path names the root itself, which has no parent to @@ -294,17 +330,24 @@ fn name(bytes: &[u8]) -> Result { } /// Open — the one verb that creates a handle. -pub async fn open(fs: &mut Fs<'_, '_>, files: &mut Files, path: &[u8], flags: u64) -> Answer { +pub async fn open( + fs: &mut Fs<'_, '_>, + files: &mut Files, + path: &[u8], + flags: u64, + spelling: u64, +) -> Answer { let readable = flags & mode::READ != 0; let writable = flags & mode::WRITE != 0; let append = flags & mode::APPEND != 0; - let mut rights = Rights::NONE; - if readable { - rights = rights.union(Rights::READ); - } - if writable { - rights = rights.union(Rights::WRITE); - } + // Two questions, not one. `rights` is what the *route* may do, from the + // caller's spelling; `readable`/`writable` are what the *handle* may do, + // from the mode or the same spelling. A write route must be able to + // read to take a step, which is why `io.open(p, "w")` opens a readable + // route and still answers an object that refuses `f:read`. + let Some(rights) = route_rights(spelling) else { + return Answer::Refused(String::from("usage")); + }; // A directory is made, never opened for bytes: `kore.fs.mkdir` is this // verb with DIRECTORY set, so one arm creates both kinds and the // already-exists answer is identical for each. @@ -317,7 +360,7 @@ pub async fn open(fs: &mut Fs<'_, '_>, files: &mut Files, path: &[u8], flags: u6 Err(error) => refusal(error), }; } - let existing = fs.open(path, rights.union(Rights::READ)).await; + let existing = fs.open(path, rights).await; let view = match existing { Ok(view) if flags & mode::EXCLUSIVE != 0 => { // The file is there and the caller demanded it not be. Close @@ -1081,6 +1124,76 @@ mod tests { // is the assertion: neither loop made a round trip. } + /// Pack a spelling the way the C half does, so these tests exercise + /// the same word the bridge actually carries. + fn word(spelling: &str) -> u64 { + let mut packed = 0u64; + for (index, byte) in spelling.bytes().enumerate() { + packed |= u64::from(byte) << (8 * index); + } + packed + } + + #[test] + fn a_spelling_means_what_the_store_says_it_means() { + assert_eq!(route_rights(word("r")), Some(Rights::READ)); + assert_eq!( + route_rights(word("rw")), + Some(Rights::READ.union(Rights::WRITE)) + ); + // `rights_parse` implies READ — a route that cannot read cannot + // take a step — so `w` alone is a readable write route. That rule + // is the store's, which is the whole reason this does not parse + // the letters itself. + assert_eq!( + route_rights(word("w")), + Some(Rights::READ.union(Rights::WRITE)) + ); + } + + #[test] + fn an_empty_spelling_is_a_read_only_route() { + // Zero is what an all-NUL word unpacks to, and it must mean the + // least authority rather than the most. + assert_eq!(route_rights(0), Some(Rights::READ)); + } + + #[test] + fn a_right_this_surface_cannot_exercise_is_refused() { + // The rule the whole slice turns on: a route carrying SHARE is + // useless while nothing here can share, and accepting the letter + // would claim a reach this surface does not have. These unlock + // with their verbs, not before. + assert_eq!(route_rights(word("rn")), None); + assert_eq!(route_rights(word("rs")), None); + assert_eq!(route_rights(word("rwns")), None); + } + + #[test] + fn a_posix_mode_is_not_a_rights_spelling() { + // Each of these is a mode string someone might reach for out of + // habit. None may quietly open something: `+`, `b` and `a` are not + // rights letters, so the caller is told rather than surprised. + for spelling in ["w+", "r+", "a", "rb", "a+"] { + assert_eq!(route_rights(word(spelling)), None, "{spelling}"); + } + } + + #[test] + fn a_letter_may_not_repeat_and_a_stray_one_is_not_guessed() { + assert_eq!(route_rights(word("rr")), None); + assert_eq!(route_rights(word("x")), None); + } + + #[test] + fn a_full_word_without_a_terminator_is_read_whole() { + // Eight letters and no NUL: the unpack must stop at the word's end + // rather than run past it. Eight distinct rights letters do not + // exist, so this can only be a refusal — the point is that it is + // a refusal and not a read of whatever followed. + assert_eq!(route_rights(u64::from_le_bytes(*b"rwrwrwrw")), None); + } + #[test] fn a_path_splits_into_a_parent_and_a_leaf() { assert_eq!(split(b"/etc/hosts"), Some((&b"/etc"[..], &b"hosts"[..]))); diff --git a/crates/lua/src/kore_lua.c b/crates/lua/src/kore_lua.c index 6f2d5eb..6131331 100644 --- a/crates/lua/src/kore_lua.c +++ b/crates/lua/src/kore_lua.c @@ -924,6 +924,28 @@ static void kore_lua_file_drop(kore_lua_file *file) { file->eof = 0; } +/* + * Pack a rights spelling into one argument word: ASCII, NUL-padded, which + * `src/files.rs` unpacks and hands to the store's own `rights_parse`. + * + * The letters are deliberately **not** interpreted here. What a letter + * means is the store's to say — `rights_label` prints what `rights_parse` + * reads — and a copy of that mapping in C would be a second authority over + * one set of bits, which is exactly what `kore.fs.rights` avoids by + * deriving its booleans from the store's own answer. + * + * `rights_parse` implies READ, so an empty spelling is a read-only route. + */ +#define KORE_LUA_RIGHTS_BYTES 8 + +static unsigned long long kore_lua_rights_word(const char *spelling, + size_t len) { + unsigned long long word = 0; + for (size_t i = 0; i < len && i < KORE_LUA_RIGHTS_BYTES; i++) + word |= (unsigned long long)(unsigned char)spelling[i] << (8 * i); + return word; +} + /* * Parse an `io.open` mode string into the bits src/files.rs reads. * Returns 0 when the spelling is not one Lua defines. @@ -961,15 +983,23 @@ static unsigned long long kore_lua_mode(const char *mode) { * after it, and `io.input(name)` has only the name. Answers 1 with the * file pushed, or `render`'s failure count. */ -static int kore_lua_open_named(lua_State *L, const char *path, size_t path_len, - const char *mode, int mode_index, - kore_lua_render_fail render) { - unsigned long long flags = kore_lua_mode(mode); +/* + * The one open. `flags` are the *handle's* permissions and the open's + * behaviour; `rights` is the *route's* authority, packed above. + * + * Two questions, not one. A write route must be able to read to take a + * step, so `io.open(p, "w")` opens a readable route and still answers an + * object that refuses `f:read` — the mode governs the object, the rights + * govern the route (`docs/kore-fs-vocabulary.md`). + */ +static int kore_lua_open_with(lua_State *L, const char *path, size_t path_len, + unsigned long long flags, + unsigned long long rights, + kore_lua_render_fail render, const char **why, + size_t *why_len) { unsigned long long handle = 0, length = 0; - int n; - if (flags == 0) return luaL_argerror(L, mode_index, "invalid file mode"); - n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_OPEN, flags, 0, 0, path, path_len, - &handle, &length, NULL, NULL, render); + int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_OPEN, flags, rights, 0, path, + path_len, &handle, &length, why, why_len, render); if (n) return n; /* An append stream starts at the end, so `f:seek()` before the first write answers where those bytes will actually land. */ @@ -987,8 +1017,26 @@ static int kore_lua_open_named(lua_State *L, const char *path, size_t path_len, return 1; } -/* `kore.fs.open` and `io.open`'s spelling: the path and mode are the call's - own first two arguments; `render` picks the surface's failure shape. */ +/* + * `io`'s spelling, translated. A mode string is a POSIX projection of a + * right set plus three behaviours, so it is unpacked into both here — + * which is what keeps the capability default documented in `docs/lua.md` + * in exactly one place rather than derived again on the far side. + */ +static int kore_lua_open_named(lua_State *L, const char *path, size_t path_len, + const char *mode, int mode_index, + kore_lua_render_fail render) { + unsigned long long flags = kore_lua_mode(mode); + const char *spelling; + if (flags == 0) return luaL_argerror(L, mode_index, "invalid file mode"); + /* A route that may write must also be able to read to take a step. */ + spelling = (flags & KORE_LUA_O_WRITE) ? "rw" : "r"; + return kore_lua_open_with(L, path, path_len, flags, + kore_lua_rights_word(spelling, strlen(spelling)), + render, NULL, NULL); +} + +/* `io.open`: the path and mode are the call's own first two arguments. */ static int kore_lua_open_file(lua_State *L, kore_lua_render_fail render) { size_t path_len = 0; const char *path = luaL_checklstring(L, 1, &path_len); @@ -1702,8 +1750,96 @@ static void kore_lua_file_metatable(lua_State *L) { * `kore.fs`: the same object, the kebab-code failure table, and the * positional verbs the stream surface is built out of. */ +/* The options `kore.fs.open` accepts; anything else is a typo, not a + silently ignored request. */ +static const char *const kore_lua_open_option_names[] = { + "create", "truncate", "append", "exclusive", NULL +}; + +static int kore_lua_open_option(lua_State *L, int index, const char *name) { + int on; + lua_getfield(L, index, name); + on = lua_toboolean(L, -1); + lua_pop(L, 1); + return on; +} + +/* + * `kore.fs.open(path [, rights [, options]])`. + * + * Authority is the rights string; behaviour is the options table. The mode + * string `io.open` takes is a POSIX projection that fuses the two — `"w"` + * is write *and* create *and* truncate — and the whole reason this surface + * exists is to say what the store says. + * + * Every mistake here **raises**: a malformed spelling, an unknown option, a + * combination that cannot mean anything. Rule 1 divides a programming + * error from an expected failure, and none of these is a condition of the + * store. A right the store would refuse is the other thing, and that comes + * back as `nil, err` like any refusal. + */ static int kore_lua_fs_open(lua_State *L) { - return kore_lua_open_file(L, kore_lua_fail_table); + size_t path_len = 0, rights_len = 0; + const char *path = luaL_checklstring(L, 1, &path_len); + const char *rights = luaL_optlstring(L, 2, "r", &rights_len); + unsigned long long flags = KORE_LUA_O_READ; /* `rights_parse` implies READ */ + const char *why = NULL; + size_t why_len = 0; + int n; + if (rights_len > KORE_LUA_RIGHTS_BYTES) + return luaL_argerror(L, 2, "rights spelling is too long"); + /* + * The handle's own permission, which is not the route's. `w` in the + * spelling is the only thing that makes the object writable; everything + * else about what a letter means is decided on the far side. + */ + if (memchr(rights, 'w', rights_len) != NULL) flags |= KORE_LUA_O_WRITE; + if (!lua_isnoneornil(L, 3)) { + luaL_checktype(L, 3, LUA_TTABLE); + /* An unknown key is a typo the script wants to hear about now. */ + lua_pushnil(L); + while (lua_next(L, 3) != 0) { + int known = 0; + if (lua_type(L, -2) == LUA_TSTRING) { + const char *key = lua_tostring(L, -2); + for (int i = 0; kore_lua_open_option_names[i] != NULL; i++) + if (strcmp(key, kore_lua_open_option_names[i]) == 0) known = 1; + if (!known) + return luaL_error(L, "unknown open option '%s'", key); + } else { + return luaL_argerror(L, 3, "options are named keys"); + } + lua_pop(L, 1); + } + if (kore_lua_open_option(L, 3, "create")) flags |= KORE_LUA_O_CREATE; + if (kore_lua_open_option(L, 3, "truncate")) flags |= KORE_LUA_O_TRUNCATE; + if (kore_lua_open_option(L, 3, "append")) flags |= KORE_LUA_O_APPEND; + if (kore_lua_open_option(L, 3, "exclusive")) flags |= KORE_LUA_O_EXCLUSIVE; + } + /* Combinations that cannot mean anything, named before the round trip. */ + if ((flags & (KORE_LUA_O_TRUNCATE | KORE_LUA_O_APPEND)) && + !(flags & KORE_LUA_O_WRITE)) + return luaL_argerror(L, 3, "truncate and append need 'w' in the rights"); + if ((flags & KORE_LUA_O_EXCLUSIVE) && !(flags & KORE_LUA_O_CREATE)) + return luaL_argerror(L, 3, "exclusive is about creating; it needs create"); + n = kore_lua_open_with(L, path, path_len, flags, + kore_lua_rights_word(rights, rights_len), + kore_lua_fail_table, &why, &why_len); + /* + * `usage` is the far side saying the spelling is not one it will open a + * route with — malformed, or naming a right this surface cannot yet + * exercise. Either way the script is wrong, so it raises rather than + * arriving as a refusal a caller might reasonably retry. + */ + if (n == 2 && why != NULL && why_len == 5 && memcmp(why, "usage", 5) == 0) { + lua_pop(L, 2); + return luaL_argerror( + L, 2, + "rights are a subset of 'rw', each letter at most once; " + "'n' and 's' are not reachable from this surface yet, and " + "io.open takes the POSIX mode string"); + } + return n; } /* One shape for every path verb that answers `true` or a failure. */ @@ -1726,8 +1862,9 @@ static int kore_lua_fs_mkdir(lua_State *L) { size_t len = 0; const char *path = luaL_checklstring(L, 1, &len); unsigned long long r = 0, r2 = 0; - int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_OPEN, KORE_LUA_O_DIRECTORY, 0, 0, - path, len, &r, &r2, NULL, NULL, kore_lua_fail_table); + int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_OPEN, KORE_LUA_O_DIRECTORY, + kore_lua_rights_word("r", 1), 0, path, len, &r, &r2, + NULL, NULL, kore_lua_fail_table); if (n) return n; lua_pushboolean(L, 1); return 1; @@ -2052,8 +2189,9 @@ static int kore_lua_fs_write_file(lua_State *L) { const char *bytes = luaL_checklstring(L, 2, &bytes_len); unsigned long long handle = 0, length = 0, written = 0, end = 0; int n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_OPEN, - KORE_LUA_O_WRITE | KORE_LUA_O_CREATE, 0, 0, path, - path_len, &handle, &length, NULL, NULL, + KORE_LUA_O_WRITE | KORE_LUA_O_CREATE, + kore_lua_rights_word("rw", 2), 0, path, path_len, + &handle, &length, NULL, NULL, kore_lua_fail_table); if (n) return n; n = kore_lua_host_bytes(L, KORE_LUA_OP_FS_SAVE, handle, 0, 0, bytes, @@ -2263,11 +2401,13 @@ static unsigned long long kore_lua_tempfile(char *dst, size_t *len_out, dst[len++] = digits[draw[i] & 15]; } dst[len] = '\0'; + /* Both callers write to what they draw, so the route carries WRITE + whatever `flags` says about the handle. */ status = kore_lua_host_call(KORE_LUA_OP_FS_OPEN, flags | KORE_LUA_O_CREATE | KORE_LUA_O_EXCLUSIVE, - 0, 0, dst, len, &handle, &length, why, - &why_len); + kore_lua_rights_word("rw", 2), 0, dst, len, + &handle, &length, why, &why_len); if (status == KORE_LUA_STATUS_OK) { if (len_out != NULL) *len_out = len; return handle + 1; /* biased so 0 means failure */ diff --git a/crates/lua/tests/host_fs.c b/crates/lua/tests/host_fs.c index 1eea2ff..1aa63a9 100644 --- a/crates/lua/tests/host_fs.c +++ b/crates/lua/tests/host_fs.c @@ -144,6 +144,9 @@ static struct { here: what the facade decides on is the *ask*, and after the clamp nothing downstream can still see how big it was. */ unsigned long long max_read_want; + /* The rights spelling the last open asked for, so a test can assert what + authority a mode string actually requested. */ + char last_rights[9]; /* Acknowledge at most this much per positional write, once armed: the only way to reach a *short* write, which the store otherwise never produces because it writes everything or refuses. Zero acknowledges @@ -206,6 +209,42 @@ static int host_fs_refuse(const char *code, const char **text, return HOST_FS_REFUSED; } +/* -------------------------------------------------------------- rights */ + +/* + * The spelling `src/files.rs` unpacks and hands to the store's own + * `rights_parse`: READ implied, a subset of `rwns`, each letter at most + * once — and, for now, no `n` or `s`, which name authority this surface + * cannot exercise. + * + * Modelled here rather than ignored. The C half's claim is that a + * spelling it will not open a route with comes back as `usage`, and a + * store that accepted everything could not show that. This is a test + * double of the rule, not a second authority over it: the product's one + * parser is the store's. + */ +static int host_fs_rights(unsigned long long word, char *out) { + unsigned seen = 0; + size_t len = 0; + for (int i = 0; i < 8; i++) { + unsigned char letter = (unsigned char)(word >> (8 * i)); + unsigned bit; + if (letter == 0) break; + switch (letter) { + case 'r': bit = 1; break; + case 'w': bit = 2; break; + case 'n': bit = 4; break; + case 's': bit = 8; break; + default: return 0; + } + if (seen & bit) return 0; /* each letter at most once */ + seen |= bit; + out[len++] = (char)letter; + } + out[len] = '\0'; + return (seen & (4 | 8)) == 0; /* not reachable from this surface yet */ +} + /* ------------------------------------------------------------ the store */ static int host_fs_find(const char *path, size_t len) { @@ -336,6 +375,9 @@ unsigned long long host_fs_max_read_want(void) { return host_fs.max_read_want; } void host_fs_reset_max_read_want(void) { host_fs.max_read_want = 0; } +/* The rights spelling the last open asked the store for. */ +const char *host_fs_last_rights(void) { return host_fs.last_rights; } + /* Acknowledge at most `cap` bytes per positional write; a negative `cap` disarms. Zero acknowledges nothing, without refusing. */ void host_fs_short_writes(int cap) { @@ -478,6 +520,10 @@ int kore_lua_host_call(unsigned op, unsigned long long arg0, return HOST_FS_OK; case HOST_FS_OP_OPEN: + /* The route's authority, before anything about the handle: a + spelling the far side would not open a route with is `usage`. */ + if (!host_fs_rights(arg1, host_fs.last_rights)) + return host_fs_refuse("usage", text, text_len); return host_fs_do_open(arg0, path, input_len, result, result2, text, text_len); diff --git a/crates/lua/tests/host_smoke.c b/crates/lua/tests/host_smoke.c index a3e6beb..fcf57c8 100644 --- a/crates/lua/tests/host_smoke.c +++ b/crates/lua/tests/host_smoke.c @@ -183,6 +183,12 @@ static int test_set_subtype(lua_State *L) { return 1; } +/* The rights spelling the last open asked the store for. */ +static int test_last_rights(lua_State *L) { + lua_pushstring(L, host_fs_last_rights()); + return 1; +} + static int test_open_library(lua_State *L) { static const luaL_Reg entries[] = { {"fail_reads_after", test_fail_reads_after}, @@ -191,6 +197,7 @@ static int test_open_library(lua_State *L) { {"reset_max_read_want", test_reset_max_read_want}, {"short_writes", test_short_writes}, {"set_subtype", test_set_subtype}, + {"last_rights", test_last_rights}, {NULL, NULL} }; luaL_newlib(L, entries); @@ -398,7 +405,7 @@ static int filesystem_smoke(void) { "assert(root==nil and rooterr.code=='access-denied')\n" /* ------------------------------------------------------ kore.fs.resize */ - "local rz = assert(kore.fs.open('/resize.txt','w+'))\n" + "local rz = assert(kore.fs.open('/resize.txt','rw',{create=true,truncate=true}))\n" "assert(rz:write('abcdefgh'))\n" "assert(kore.fs.resize(rz,3))\n" /* A truncation drags the position back with it. */ @@ -659,6 +666,117 @@ static int filesystem_smoke(void) { } +/* + * `kore.fs.open` in the store's own vocabulary. + * + * A mode string fuses authority with behaviour — `"w"` is write *and* + * create *and* truncate — and the whole reason this surface exists is to + * say what the store says. So: authority is the rights string, behaviour + * is the options table, and the two are asserted apart. + * + * The store records the spelling each open asked for, which is the only + * way to show that `io.open`'s mode still reaches the same authority after + * the two surfaces converged onto one path. + */ +static int open_smoke(void) { + static const char suite[] = + /* ------------------------------------------------- authority */ + "assert(kore.fs.write_file('/o.txt','abc')==3)\n" + "local r = assert(kore.fs.open('/o.txt'))\n" + "assert(kore.test.last_rights()=='r', kore.test.last_rights())\n" + "assert(r:read('a')=='abc')\n" + /* A read-only route answers a read-only object. */ + "local w, werr = r:write('x')\n" + "assert(w==nil and werr=='access-denied', tostring(werr))\n" + "assert(r:close())\n" + + "local rw = assert(kore.fs.open('/o.txt','rw'))\n" + "assert(kore.test.last_rights()=='rw')\n" + /* Asked for both, so the object permits both — unlike io's `\"w\"`. */ + "assert(rw:write('Z'))\n" + "assert(rw:seek('set')==0 and rw:read(3)=='Zbc')\n" + "assert(rw:close())\n" + + /* ------------------------------------------------- behaviour */ + /* Rights alone do not create: that is an option, and its absence is + a `not-found` rather than a silent create. */ + "local m, merr = kore.fs.open('/absent.txt','rw')\n" + "assert(m==nil and merr.code=='not-found', tostring(merr))\n" + "local c = assert(kore.fs.open('/absent.txt','rw',{create=true}))\n" + "assert(c:write('hello'))\n" + "assert(c:close())\n" + "assert(kore.fs.read_file('/absent.txt')=='hello')\n" + /* Truncate empties; without it the bytes stay. */ + "local k = assert(kore.fs.open('/absent.txt','rw'))\n" + "assert(k:close())\n" + "assert(kore.fs.read_file('/absent.txt')=='hello')\n" + "local t = assert(kore.fs.open('/absent.txt','rw',{truncate=true}))\n" + "assert(t:close())\n" + "assert(kore.fs.read_file('/absent.txt')=='')\n" + /* Append writes at the end, whatever the position says. */ + "assert(kore.fs.write_file('/ap.txt','one')==3)\n" + "local a = assert(kore.fs.open('/ap.txt','rw',{append=true}))\n" + "assert(a:write('two'))\n" + "assert(a:close())\n" + "assert(kore.fs.read_file('/ap.txt')=='onetwo')\n" + /* Exclusive refuses an existing name. */ + "local x, xerr = kore.fs.open('/ap.txt','rw',{create=true,exclusive=true})\n" + "assert(x==nil and xerr.code=='already-exists', tostring(xerr))\n" + + /* -------------------------------------- mistakes, which raise */ + /* A POSIX mode is not a rights spelling, and says so. */ + "assert(not pcall(kore.fs.open,'/o.txt','w+'))\n" + "assert(not pcall(kore.fs.open,'/o.txt','a'))\n" + "assert(not pcall(kore.fs.open,'/o.txt','rb'))\n" + /* A letter twice is not a subset. */ + "assert(not pcall(kore.fs.open,'/o.txt','rr'))\n" + /* Rights this surface cannot exercise are refused by name, not + silently dropped — that is the rule the whole slice turns on. */ + "assert(not pcall(kore.fs.open,'/o.txt','rn'))\n" + "assert(not pcall(kore.fs.open,'/o.txt','rs'))\n" + /* Options that cannot mean anything. */ + "assert(not pcall(kore.fs.open,'/o.txt','r',{truncate=true}))\n" + "assert(not pcall(kore.fs.open,'/o.txt','rw',{exclusive=true}))\n" + /* A typo is an error, not a silently ignored request. */ + "assert(not pcall(kore.fs.open,'/o.txt','r',{creat=true}))\n" + "assert(not pcall(kore.fs.open,'/o.txt','r',{[1]=true}))\n" + "assert(not pcall(kore.fs.open,'/o.txt','r','create'))\n" + + /* ------------------------- io's modes reach the same authority */ + /* The capability default in docs/lua.md, asserted rather than + described: a write mode opens a readable route, because a route + that cannot read cannot take a step. */ + "assert(io.open('/o.txt','r')):close()\n" + "assert(kore.test.last_rights()=='r')\n" + "assert(io.open('/o.txt','w')):close()\n" + "assert(kore.test.last_rights()=='rw')\n" + "assert(io.open('/o.txt','a')):close()\n" + "assert(kore.test.last_rights()=='rw')\n" + "assert(io.open('/o.txt','r+')):close()\n" + "assert(kore.test.last_rights()=='rw')\n" + /* And io still refuses the object what the mode withheld, even + though the route could have carried it. */ + "local iw = assert(io.open('/o.txt','w'))\n" + "local ir, irwhy = iw:read('a')\n" + "assert(ir==nil and irwhy=='access-denied', tostring(irwhy))\n" + "assert(iw:close())\n" + "return true\n"; + + host_fs_reset(""); + struct capture capture = {{0}, 0}; + lua_State *L = kore_lua_new(host_alloc, NULL, 0x4b4f5245u, test_libraries, + sizeof test_libraries / sizeof test_libraries[0]); + if (L == NULL) return 1; + if (kore_lua_install_print(L, capture_append, &capture) != LUA_OK) { + kore_lua_close(L); return 2; + } + if (!run_reporting(L, suite, "open suite")) { kore_lua_close(L); return 3; } + kore_lua_set_top(L, 0); + if (host_fs_open_handles() != 0) { kore_lua_close(L); return 4; } + kore_lua_close(L); + return 0; +} + /* * The graph a script can see. * @@ -1159,6 +1277,8 @@ int main(void) { host_fs_reset(""); int fs_status = filesystem_smoke(); if (fs_status != 0) return 30 + fs_status; + int open_status = open_smoke(); + if (open_status != 0) return 110 + open_status; int graph_status = graph_smoke(); if (graph_status != 0) return 90 + graph_status; int write_status = write_smoke(); diff --git a/docs/lua.md b/docs/lua.md index f90e110..223f723 100644 --- a/docs/lua.md +++ b/docs/lua.md @@ -26,9 +26,16 @@ revision, a subtype, the rights it actually holds. ## The capability default -DagFS authority is a right set on a route, not a mode bit on a file. The stock -surface has no way to say that, so it says it by convention instead: a mode -string picks the rights, and the surface never asks for more. +DagFS authority is a right set on a route, not a mode bit on a file. + +`kore.fs.open` says so directly — it takes a **rights spelling**, and +behaviour (create, truncate, append, exclusive) moves to an options table where +it belongs. The stock surface has no way to say any of that, so it says it by +convention instead: a mode string picks the rights, and the surface never asks +for more. + +Both reach the same place. A mode string is translated to a spelling and a set +of options before it crosses, so this table is the whole of the convention: | Spelling | Rights requested | On the parent | |---|---|---| @@ -45,6 +52,9 @@ the mode asked for, so `f:read` on a `"w"` file is `access-denied` rather than a read the route would in fact have permitted. The mode governs the object, the rights govern the route, and they are not the same question. +`kore.fs.open(path, "rw")` asks for both and gets an object that permits both, +which is the difference the two spellings exist to express. + **`READ` is always requested**, because a route that cannot read cannot take a single step. Two rights are **never** requested by this surface: `SHARE` and `BIND_EXECUTABLE`. That is the ceiling of everything below — no Lua script can @@ -62,7 +72,7 @@ Every verb, both spellings. `—` means the surface has no spelling for it. | What | `kore.*` | stock | |---|---|---| -| Open a file | `kore.fs.open(path, mode)` | `io.open(path, mode)` | +| Open a file | `kore.fs.open(path, rights [, options])` | `io.open(path, mode)` | | Read a whole file | `kore.fs.read_file(path)` | — | | Write a whole file | `kore.fs.write_file(path, bytes)` | — | | Read from a handle | `f:read(...)` | `f:read(...)`, `io.read(...)` | @@ -109,12 +119,37 @@ Every verb answers `value, err` on failure, where `err` is a table (see [Errors](#errors)). Paths resolve inside the task's endowed view; there is no global root. -### `kore.fs.open(path, mode) -> file | nil, err` +### `kore.fs.open(path [, rights [, options]]) -> file | nil, err` + +The same object `io.open` answers, with the table failure spelling — but asked +for in the store's vocabulary rather than POSIX's. + +```lua +local f = assert(kore.fs.open("/etc/hosts")) -- "r" +local g = assert(kore.fs.open("/var/log", "rw", {create = true})) +``` + +`rights` is a subset of the store's own `rwns` spelling, each letter at most +once, defaulting to `"r"`. `READ` is implied, so `"w"` is a readable write +route. **`n` and `s` are refused for now** — a route carrying authority nothing +here can exercise would be a claim this surface cannot make — and unlock with +the verbs that use them. + +`options` are `create`, `truncate`, `append`, `exclusive`. + +Everything that can only be a mistake **raises**: a malformed spelling, a right +that is not reachable, an unknown option key, `truncate` or `append` without +`w`, `exclusive` without `create`. A refusal from the store — `not-found`, +`access-denied` — is the other thing, and arrives as `nil, err`. + +Rights alone do not create: a missing file is `not-found` until `create` is +asked for. Opening a directory is `is-directory`; use `mkdir` to make one and +`entries` or `list` to read one. -The same object `io.open` answers, with the table failure spelling. `mode` is -the stock Lua mode string ([the capability default](#the-capability-default)). -Opening a directory is `is-directory`; use `mkdir` to make one and `entries` or -`list` to read one. +> **This moved.** `kore.fs.open(p, "r")` is unchanged, and `io.open` is +> untouched. Every other mode string now either means something different — +> `"w"` is a read-write route that does not create — or raises, because `a`, +> `+` and `b` are not rights letters. The argument error names the syntax. ### `kore.fs.read_file(path) -> string | nil, err` @@ -387,9 +422,6 @@ is visible, not as a promise of spelling. Unblocked, and wanted: -- `kore.fs.open` by **rights** (`"rwn"`) rather than a POSIX mode — the - surface's clearest remaining POSIX-ism, now that `rights` reports the same - vocabulary - `search` — the query vocabulary is settled in the native export - grant **listing** (what this task has shared out) — reporting, like `rights` - creating a file with a subtype, so a Lua installer can write what `require` @@ -409,5 +441,6 @@ Blocked on the `index` feature, an image-size decision for this target: - index publication on save, and derived-version queries -See `docs/pack-backed-reads.md` for the storage-transport work these sit beside, -and `docs/filesystem.md` for the Rust facade underneath all of it. +See `docs/kore-fs-vocabulary.md` for the plan these sit in, +`docs/pack-backed-reads.md` for the storage-transport work they sit beside, and +`docs/filesystem.md` for the Rust facade underneath all of it. From a195052aa46b6d203fc3fb80518f5c869777f2e7 Mon Sep 17 00:00:00 2001 From: Julian Avar Date: Mon, 5 Oct 2026 14:33:57 -0400 Subject: [PATCH 11/13] docs: the plan for kore.fs in the store's own vocabulary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `lua.md` is the reference and `pack-backed-reads.md` is the storage-transport plan; the arc that turns `kore.fs` from POSIX-with-kore-errors into the store's own surface had no written shape. States the rule the whole arc turns on — authority is a rights string, behaviour is an options table, and the surface never offers a right it cannot exercise — and why the third clause is what bounds the work: a letter unlocks when its verb lands, and until then it is refused by name rather than silently ignored. V1 is marked done and its break tabulated. V2 (create with a subtype, which is what a Lua installer needs to write what `require` loads) through V6 are aspirational, as is the blocked set: grant creation and projection on kore#986/#991, index publication on the `index` feature. Acceptance is unmet — nothing here has run on metal. Co-Authored-By: Claude Opus 5 (1M context) --- docs/kore-fs-vocabulary.md | 146 +++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 docs/kore-fs-vocabulary.md diff --git a/docs/kore-fs-vocabulary.md b/docs/kore-fs-vocabulary.md new file mode 100644 index 0000000..d3d4e0a --- /dev/null +++ b/docs/kore-fs-vocabulary.md @@ -0,0 +1,146 @@ +# `kore.fs` in the store's own vocabulary + +**Status: V1 is implemented; V2 onward are not.** Implemented slices are +described in the past tense and their contract lives in `lua.md`. Nothing here +has run on metal. + +## 1. The problem + +`kore.fs` exists because `io` cannot say what the store knows. It has stopped +being true in one direction — `stat`, `entries` and `rights` answer the graph — +and is still false in the other: **the verbs that take authority still spell it +as POSIX.** + +```lua +kore.fs.open(path, "w") -- "write, and create, and truncate": three things, one letter +``` + +A mode string is a POSIX projection of a right set. DagFS authority is a right +set on a route, and the store already has a textual spelling for it — +`rights_label` prints what `rights_parse` reads, and `kore.fs.rights` has +answered in that spelling since the graph slice landed. So the surface reports +rights in one vocabulary and accepts them in another. + +## 2. The rule + +**Authority is a rights string. Behaviour is an options table. The surface never +offers a right it cannot exercise.** + +The third clause is the one that bounds this work. A route carrying `SHARE` is +useless while nothing here can share, and a surface that accepts `s` would be +claiming a reach it does not have. So letters unlock as their verbs land, and +until then they are refused by name rather than silently ignored. + +| Letter | Right | Reachable when | +|---|---|---| +| `r` | `READ` | now | +| `w` | `WRITE` | now | +| `n` | `MUTATE_NAMESPACE` | `link`/`unlink` on a handle exist (V2) | +| `s` | `SHARE` | `share` exists — blocked on kore#986/#991 | + +`BIND_EXECUTABLE` has no letter: `rights_label` spells four bits and answers +`?` for anything else. That is the store's vocabulary, and this surface does not +extend it. + +## 3. Two things that are not one thing + +The rights string governs the **route**. The options table and the object's own +read/write permission govern the **handle**. They are not the same question, and +conflating them is what the mode string does: + +- `io.open(p, "w")` opens a route with `READ WRITE` — a write route must be able + to read to take a step — but hands back an object that refuses `f:read`. +- `kore.fs.open(p, "rw")` opens the same route and hands back an object that + permits both, because that is what was asked for. + +So `files::open` takes both: a packed rights spelling for the route, and flags +for the handle's permissions and the open's behaviour. + +## 4. Slices + +### V1 — `open` by rights *(done)* + +```lua +kore.fs.open(path) -- "r" +kore.fs.open(path, "rw") +kore.fs.open(path, "rw", {create = true, truncate = true}) +``` + +Options are `create`, `truncate`, `append`, `exclusive`. An unknown key, a +malformed spelling, or a combination that cannot mean anything — `truncate` +without `w`, `exclusive` without `create` — **raises**, because each is a +programming error and rule 1 divides those from expected failures. + +The spelling crosses the bridge packed into one word as ASCII, and +`kore_dagfs::rights_parse` does the mapping on the far side. C re-implementing +letter-to-bit would be a second authority over one set of bits, which is the +mistake `rights` already avoids by deriving its booleans from the store's own +letters. + +`io.open` converges onto the same path: its mode string is translated to a +spelling and a set of options in C, so the capability default documented in +`lua.md` lives in exactly one place. Nothing about `io` changes. + +**The break.** `kore.fs.open(p, "r")` is unchanged — `"r"` means `READ` in both +vocabularies. Everything else moves, and every case fails loudly rather than +silently: + +| Was | Now | +|---|---| +| `"w"` | parses as `READ WRITE`, does **not** create or truncate — a missing file is `not-found` | +| `"a"`, `"r+"`, `"w+"`, `"rb"` | raise: `a`, `+` and `b` are not rights letters | +| `"rn"`, `"rs"` | raise, by §2 — named as not yet reachable, not silently dropped | + +The argument error names the syntax, so a script written against the old +spelling is told what to write instead. + +### V2 — `create`, and the subtype + +`create_typed` is unreachable, so a Lua installer cannot write the +`EXECUTABLE_IMAGE` objects that `require` loads. A `kore.fs.create(path, opts)` +taking `{kind = "file" | "directory", subtype = ...}` gives it a home, makes +`mkdir` the one-liner over it rather than a separate path, and is what unlocks +`n` in §2 once `link`/`unlink` join it. + +### V3 — grant listing + +`shares` and `share_entry` list this task's authority edges. **Reporting, not +conferring** — the same rule that let `stat` and `rights` land ahead of the +capability rulings. Grant *creation* stays blocked. + +### V4 — `search` + +The query vocabulary is settled in the pinned export (`SearchQuery`, +`SearchPlane`, `FreshnessPolicy`, `SearchScore`). It is the largest piece of the +store a script still cannot reach. + +### V5 — batched commits + +`write_file` holds commits internally and no script can ask for the same across +several writes. A scoped form — the hold released on the way out, whatever +happened — is the only shape worth offering, which makes it a to-be-closed +value rather than a pair of calls. + +### V6 — the remaining small gaps + +`stat` on an open handle (the bridge already supports it), `open_root`, +`bind_cwd`. + +### Blocked + +- Grant creation, revocation, opening a view through one, and projection: + **kore#986** (grant boundary, route shape, rights vocabulary) and **kore#991** + (whether a userland authority domain gets a generic capability type). +- Index publication on save and derived-version queries: kore-dagfs's `index` + feature, which pulls `kore-svc-extract-worker-rust` — an image-size decision + for this target. + +## 5. Acceptance + +- Every rights spelling the store accepts round-trips: what `kore.fs.rights` + prints can be handed to `kore.fs.open`. +- A spelling this surface cannot exercise is refused by name, not ignored. +- `io`'s observable behaviour is unchanged across the convergence in V1 — its + suite is the witness. +- Measured on metal, with the rest of the Lua surface. Nothing here is proven + by a host build. From 994d315d8fdecac92d0026383b909b5f8bf66f8d Mon Sep 17 00:00:00 2001 From: Julian Avar Date: Mon, 5 Oct 2026 14:55:39 -0400 Subject: [PATCH 12/13] wasi: Wasmtime 48.0.5 for RUSTSEC-2026-0325, -0326 and -0327 Three advisories landed against Wasmtime 48.0.3 on 2026-10-02: mis-typed WebAssembly tag imports can corrupt the GC heap (RUSTSEC-2026-0325, medium), rooting for GC values live across `try_call` may be missing (-0326, medium), and a component async-lifted callback's result count is unvalidated, overflowing a native stack buffer (-0327, **critical**). All three are fixed in 48.0.4; 48.0.5 is the current patch of that line. `cargo audit --deny warnings` fails the Advisories workflow until the bump, over both this lockfile and `native/`'s. It needs no source change: a patch release within 48.0.x, and the runtime, component-model and async surfaces this host uses are untouched. Co-Authored-By: Claude Opus 5 (1M context) --- Cargo.lock | 172 ++++++++++++++++++++--------------------- crates/wasi/Cargo.toml | 6 +- 2 files changed, 89 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8fa0420..6b72e97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,27 +207,27 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.135.3" +version = "0.135.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d620e7c8e86c48ff33f750fd49a2f3fa39529e6bfe59639dfdfcbf81329f737" +checksum = "3bfbb8f30b72ef5cedf2dbd85ee2f167a33c888d492a40e999250187138663c9" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.135.3" +version = "0.135.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75015effc364f66dfda9f45f3d78b5509957303258521143839adfd762eade60" +checksum = "5715fdd9d897798ba2bb834df5d6c1129319d51e82ddcf81812c23231e9b852b" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.135.3" +version = "0.135.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e18dd511b085013add81cd0b5381207451c791c93f8487c800ec71455287446e" +checksum = "1a266da98b248dd98dfc8a46d45d57810db04a55b81a6d8c7a9c6cf0bd2db7c3" dependencies = [ "cranelift-entity", "wasmtime-internal-core", @@ -235,9 +235,9 @@ dependencies = [ [[package]] name = "cranelift-bitset" -version = "0.135.3" +version = "0.135.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92f504aa9105998333f8416a7d32dac037313e2103649f91a143f383d445fee" +checksum = "c67c1887ed2194ef3ecc283a9add4fe8a79d249a293063046f2adedc31ae7200" dependencies = [ "serde", "serde_derive", @@ -246,9 +246,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.135.3" +version = "0.135.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26f0f8551262c68d14cbb5c60e805394f38c7492f2a02c3a4592cef40b342e9" +checksum = "90103abeafa22cc578c577999bdf1b51c32b8a59e15a09789841b4e9a854588d" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -277,9 +277,9 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.135.3" +version = "0.135.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6672eac6d00c944a01712109d376019ab46ff4c3b37bb3419fa350b25523b1ad" +checksum = "d73eaff808bc302e2899e42d822c0e64df23f38facb98ecdf695d1817d2063a3" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -290,24 +290,24 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.135.3" +version = "0.135.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f2bd3116e2cdef47a5755c484fa5feca18bd77ea7db418e492f72faaa7582f2" +checksum = "de19c4fc55ac3cdeb8e153f35ef9f51db197da91a64c9811d0ddd21ed1b9d18d" [[package]] name = "cranelift-control" -version = "0.135.3" +version = "0.135.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9706989f98e3f7f1b1bc16450089c0393b141f79701590f7bf830c3a531454a2" +checksum = "9bbd919579d74a7212e6e849dc24ff015cb823e70051d01559eea828efaf773f" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.135.3" +version = "0.135.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26094b0d1072871e07bcb4f53982744ee42657fb40c2175081a10d71f06f8546" +checksum = "af201af93fa8a576ac4b04b901ac3f252545d7b703053c922526fce28f65a7be" dependencies = [ "cranelift-bitset", "serde", @@ -317,9 +317,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.135.3" +version = "0.135.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "247fa35f45fd4477ca045e23f14b2aafe6bdbe9df3d754b1429e0587b356116b" +checksum = "69395ceee43706b93bf869b3a6631ab8d9dcb2bf19f9af4d1d2db0cb9e18af18" dependencies = [ "cranelift-codegen", "hashbrown 0.17.1", @@ -330,15 +330,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.135.3" +version = "0.135.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e07f7c111564a5deda3ca9e4ac2f043f8e1fc844a62d3a3feb2be26b49274" +checksum = "8e8d98a81f71e2ddc61849ca1454ff424fbf42a3ee5529365be8c0859ddf5b9a" [[package]] name = "cranelift-native" -version = "0.135.3" +version = "0.135.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d0ee388c00a256fee3241f6c8d93e9b8b4175bac3a5527c1dc80169e047643e" +checksum = "273c79e0265b754f67ff76c42d6db4856dabf9101e3bb1b23c83255e2438dd02" dependencies = [ "cranelift-codegen", "libc", @@ -347,9 +347,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.135.3" +version = "0.135.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f739117ccbeec70314433f304ced82abe89e75ff32ab676c2d7da661358af1e" +checksum = "2b73d7e686f3518831f88ffe88be02c6c92be09930fc5ab90a3dfd9a4de3815e" [[package]] name = "crc" @@ -1135,9 +1135,9 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "48.0.3" +version = "48.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76169f68135d6e5b956787c0c20954b40ae30bf60c46dde8e887ee78c1bee63a" +checksum = "dfc9a7073f2530aab972ac0a7ce43624ff59de8ad06b76a4ef4021f7ddd1035a" dependencies = [ "cranelift-bitset", "log", @@ -1147,9 +1147,9 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "48.0.3" +version = "48.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2b53c01b72bd94f0a2982f9ef503d5d547b4cace3d771fee05831379e7cac2" +checksum = "864f92c9538de48325a4f8709ec83e56471446798d48a29073ca094c5447b549" dependencies = [ "proc-macro2", "quote", @@ -1646,9 +1646,9 @@ dependencies = [ [[package]] name = "wasm-compose" -version = "0.254.0" +version = "0.254.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd717357bff09b5f1ea49aea8ed1cd149b0c521823a1cc70bc2ac16b90b651a" +checksum = "4c0c1033c10d23d96f40e761ae66cc30c70cea3323a8edfe9159dac42108873f" dependencies = [ "anyhow", "heck", @@ -1656,19 +1656,19 @@ dependencies = [ "log", "petgraph", "smallvec", - "wasm-encoder 0.254.0", - "wasmparser 0.254.0", + "wasm-encoder 0.254.2", + "wasmparser 0.254.2", "wat", ] [[package]] name = "wasm-encoder" -version = "0.254.0" +version = "0.254.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09480d646178e5fdd12bb06e812d0af9a3a191dbc9cd697fdc86687beade7393" +checksum = "ebaca0b540bdb15d195ff34fb23074255948708b8916a11a339ba4513dc61906" dependencies = [ "leb128fmt", - "wasmparser 0.254.0", + "wasmparser 0.254.2", ] [[package]] @@ -1683,21 +1683,21 @@ dependencies = [ [[package]] name = "wasm-metadata" -version = "0.254.0" +version = "0.254.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b01df5f3b4ca7881e843f3bc0fb8a3905d79c68692250dcb8e33e698705ccdb6" +checksum = "5d1880dd9a848aa68f387966256c34ad49abb4607e3cfa35720d2723d0591cd3" dependencies = [ "anyhow", "indexmap", - "wasm-encoder 0.254.0", - "wasmparser 0.254.0", + "wasm-encoder 0.254.2", + "wasmparser 0.254.2", ] [[package]] name = "wasmparser" -version = "0.254.0" +version = "0.254.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5769a29f799fbab136aaf65b4fe5384cd7d93fe6fc9ba0dcb6c8382a1f16e27" +checksum = "c5ff8685b587722894b6f25958fc608d315b93b8d23647bfb567664e7006d757" dependencies = [ "bitflags", "hashbrown 0.17.1", @@ -1719,20 +1719,20 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.254.0" +version = "0.254.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e3ba11e024f504698f87b629b2b66c22a1231758de7607754963f7d198be39" +checksum = "8b03c8f270f3f384f70f42869b5ff7f6253fe4f0aa8590f7edd332c1943e5f51" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.254.0", + "wasmparser 0.254.2", ] [[package]] name = "wasmtime" -version = "48.0.3" +version = "48.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ec755a941ae13c7f54210be272d77c4282a271d853558332827d0c8cd5dfba0" +checksum = "b94eda6b1c1d8ade526a67e4ce50cc6234d1c7af68e5d7bf13f40bc07e70daba" dependencies = [ "addr2line", "async-trait", @@ -1762,8 +1762,8 @@ dependencies = [ "target-lexicon", "tempfile", "wasm-compose", - "wasm-encoder 0.254.0", - "wasmparser 0.254.0", + "wasm-encoder 0.254.2", + "wasmparser 0.254.2", "wasmtime-environ", "wasmtime-internal-cache", "wasmtime-internal-component-macro", @@ -1782,9 +1782,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "48.0.3" +version = "48.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dca6a4a583fe56081cac202f7850b9d987d72bbb6cec8ac6503d3d6d65b165e" +checksum = "84ca4865ece3a37abb34c2df96c741ff4aed308d35744d84095cff79848752d4" dependencies = [ "anyhow", "cpp_demangle", @@ -1804,8 +1804,8 @@ dependencies = [ "sha2", "smallvec", "target-lexicon", - "wasm-encoder 0.254.0", - "wasmparser 0.254.0", + "wasm-encoder 0.254.2", + "wasmparser 0.254.2", "wasmprinter", "wasmtime-internal-component-util", "wasmtime-internal-core", @@ -1813,9 +1813,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-cache" -version = "48.0.3" +version = "48.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde154a8c7afe7611c489fa7a578755e2b8e947a1ce8b70088cf486d43edd0a1" +checksum = "ad3c3dadc98434da27b591a62d06bd767f1ecfe9bbfa0414229be2c64ad8d6d4" dependencies = [ "base64", "directories-next", @@ -1833,9 +1833,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" -version = "48.0.3" +version = "48.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0363b11ef205e5518739a0e3cd3f54c9b0a2ab6c495e0ba4776a78154722b896" +checksum = "5ae080b05e5e0356a2167c478c264e26f3fe5dbaaf1b285f8f5501301b99facb" dependencies = [ "anyhow", "proc-macro2", @@ -1848,15 +1848,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" -version = "48.0.3" +version = "48.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9bfdaa8f3e07e78c8efac1cc8bbbc0af3607d0b412c80e9cea77d18adfa9f1" +checksum = "e8f014d9ad6f6acbc449e9d7c4e7b7757b4b6b1a84a3662f66ea6da2ebed788c" [[package]] name = "wasmtime-internal-core" -version = "48.0.3" +version = "48.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72250d72e27bea7d614af6742ed185d3e9d39de471f33e177c71874a7184586d" +checksum = "ecc0a0310fc4a3f545427c7880362657f72f838ffcfe4d3b8c864021df3b3488" dependencies = [ "anyhow", "hashbrown 0.17.1", @@ -1866,9 +1866,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-cranelift" -version = "48.0.3" +version = "48.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52abc6a4ac7ab272fb1de7ed5040e0b6aa8cc49a74016588cf53f2e27366ad43" +checksum = "cb6c8d2b6f8ed3a784fcd127a1936589db8051febbd228230660175e41b5751d" dependencies = [ "cranelift-codegen", "cranelift-control", @@ -1883,7 +1883,7 @@ dependencies = [ "smallvec", "target-lexicon", "thiserror 2.0.20", - "wasmparser 0.254.0", + "wasmparser 0.254.2", "wasmtime-environ", "wasmtime-internal-core", "wasmtime-internal-unwinder", @@ -1892,9 +1892,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "48.0.3" +version = "48.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818599f226d134c04432c4d7a34541687a6d965fbfa9bc73b95a2d4b7ae99c8a" +checksum = "92b558b5892ca405508103641fa3a3ec2e986b1cc679a083ed47e6bcb08ecf31" dependencies = [ "cc", "libc", @@ -1906,9 +1906,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "48.0.3" +version = "48.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a0c01126eeaa5d432df800b2a1952f21868cc7fb61c4b452dbfcf2fa14ebdb1" +checksum = "c0669ce70a8a8c92eb8ad148c1d7b9b771aab37787e9462efd75b2e2b4575406" dependencies = [ "cc", "object", @@ -1918,9 +1918,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "48.0.3" +version = "48.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d42e1fc31e9e23ced0136cf271deb931a7375f54307f90c27cc271740fa938f" +checksum = "6aa66405e95fc7b82adb195c14fa8b38f45046c3ad99a273b06262b8eb755e76" dependencies = [ "libc", "wasmtime-internal-core", @@ -1929,9 +1929,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-unwinder" -version = "48.0.3" +version = "48.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71f967bac5a11867a374b0d2f0f1febc087ebfdaebd4797a382cbb80d8a3dfb2" +checksum = "70555458e519d6fb8790e042b4274132ab58171ae0f2a6917c3dc29ded0184d0" dependencies = [ "cranelift-codegen", "log", @@ -1941,9 +1941,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "48.0.3" +version = "48.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65b0174e50f6f882498eb55ff2dd5c666cf3700ad00a05a608b8f83ec18a03e0" +checksum = "fca407e3e2809c0e6568be132065095a16f184c30c6bee4157dfdc497aee10a1" dependencies = [ "proc-macro2", "quote", @@ -1952,9 +1952,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" -version = "48.0.3" +version = "48.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62a5137cc642ddf52082099b6aa9d38bed5105cb2918fe1c8779013842132022" +checksum = "2aacdb42fb0b393353e402a1534e0b492a6b0fddf6528bfaec65c5f56b990c43" dependencies = [ "anyhow", "bitflags", @@ -1966,9 +1966,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi-io" -version = "48.0.3" +version = "48.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17ae3ef0574f5e94d55648c324f31d2f6964b22c857f4cce8c711920a21db51" +checksum = "1af4069fb7fc9cdc72410be96fb7f83b772e500cf4d0758d7fe6b55cefb303b8" dependencies = [ "async-trait", "bytes", @@ -2062,9 +2062,9 @@ dependencies = [ [[package]] name = "wit-component" -version = "0.254.0" +version = "0.254.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0e65bb94c369b3c4741ce3d1d2704b1fec93db7c540df0e521a097e7ceeb5be" +checksum = "1c96b3cea6838dbea55173baa56991fcb087896c321b0a770db201e5a0bd3506" dependencies = [ "anyhow", "bitflags", @@ -2073,17 +2073,17 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder 0.254.0", + "wasm-encoder 0.254.2", "wasm-metadata", - "wasmparser 0.254.0", + "wasmparser 0.254.2", "wit-parser", ] [[package]] name = "wit-parser" -version = "0.254.0" +version = "0.254.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1655131e4f7d3f0cb141f6eca71315ca40eff0f3d4de7cff0a82bacedd8c89b4" +checksum = "22672d61bfb9df46a16b46ce38522293aee1cc4749fc84a3464ce4083b3d5fd6" dependencies = [ "anyhow", "hashbrown 0.17.1", @@ -2095,7 +2095,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-ident", - "wasmparser 0.254.0", + "wasmparser 0.254.2", ] [[package]] diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index d8c6615..569bd4e 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -15,16 +15,16 @@ kore = { package = "kore-std", path = "../..", features = ["heap"] } # Components, and async host imports: a guest's call into WASI suspends its # fiber and the Kore executor runs until the import's future is ready. The # engine's platform (memory, fibers, ISA) is the embedding program's choice. -wasmtime = { version = "48.0.3", default-features = false, features = [ +wasmtime = { version = "48.0.5", default-features = false, features = [ "runtime", "component-model", "async", ] } # WASI 0.2's `wasi:io` (pollables, input and output streams) as traits the # native process stream implements; `no_std` by design. -wasmtime-wasi-io = { version = "48.0.3", default-features = false } +wasmtime-wasi-io = { version = "48.0.5", default-features = false } # Host tests resolve the guest fixture against the linker with a std # Wasmtime: the Kore-side engine has no host to run on. [dev-dependencies] -wasmtime = { version = "48.0.3", features = ["std", "cranelift", "component-model"] } +wasmtime = { version = "48.0.5", features = ["std", "cranelift", "component-model"] } From d819c6f71d573e61532622f7047d1d6f4d2fd4fb Mon Sep 17 00:00:00 2001 From: Julian Avar Date: Mon, 5 Oct 2026 20:52:09 -0400 Subject: [PATCH 13/13] docs: how a capability is held, on each of the four surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `lua.md` stated the mode-to-rights convention but never showed a script using one, and never placed the Lua surfaces against the other two. A reader could not tell why `kore.fs` exists beyond nicer error tables. A DagFS route is reached through an affine `View`, and only Rust can hold one: a Lua value can be copied into a table or captured by a closure, and a C struct can be memcpy'd, so neither language can be handed something affine. Both get a generation-checked handle instead, which is why `io`, `kore.fs` and `kore-ffi` all deal in handles while `kore::fs` deals in the capability itself. That is one table, and it explains the whole shape. Then what `io` cannot say, in code rather than prose: asking for less authority on purpose, asking what the task actually holds, and taking `"w"`'s three fused decisions apart so only two are made. Plus the two things only the kore surface can see — a `revision` that moves under you, and an `id` that answers "same object" where a path answers "same name". No surface can forge authority; the store resolves every route through a generational slot. What differs is how much of the vocabulary each can say, what it costs to say it, and how early a mistake is caught. Co-Authored-By: Claude Opus 5 (1M context) --- docs/lua.md | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/docs/lua.md b/docs/lua.md index 223f723..0030ac0 100644 --- a/docs/lua.md +++ b/docs/lua.md @@ -11,6 +11,7 @@ and `kore.*` when it needs something the store knows and POSIX cannot say — a revision, a subtype, the rights it actually holds. - [The capability default](#the-capability-default) +- [Capabilities in practice](#capabilities-in-practice) - [Quick reference](#quick-reference) - [`kore.fs`](#korefs) - [The file object](#the-file-object) @@ -64,6 +65,102 @@ itself holds. `kore.fs.rights(path)` reports what the task actually has, which is not the same question as what a mode string asked for. +## Capabilities in practice + +### Four ways to hold one + +A DagFS route is reached through a `View`, which is **affine**: it has one +owner, and using it after it moves is not a thing that may happen. Only one of +the four surfaces can hold one, because only one has a language that enforces +it. + +| Surface | What you hold | What stops a second use | Rights you can name | +|---|---|---|---| +| `io` / `os` | a file object | `__close`/`__gc`, plus a generation check | none; inferred from the mode string | +| `kore.fs` | the same file object | the same | `r`, `w` today; `n`, `s` as their verbs land | +| C (`kore-ffi`) | a `KoreFsHandle` | the adapter, at run time | `uint32_t`; `KORE_FS_READ`, `KORE_FS_WRITE` | +| Rust (`kore::fs`) | the `View` itself | the borrow checker, at compile time | the whole `Rights` type, and `fs::dag` | + +Lua and C are in the same position for the same reason: a Lua value can be +copied into a table or captured by a closure, and a C struct can be memcpy'd, +so neither language can be handed something affine. Both get a *handle* — an +index and a generation into a table thread 0 owns — and a stale one is +`invalid-handle` rather than a read of whatever took the slot. Rust is the only +surface that holds the capability itself, and `src/fs.rs` keeps a +`compile_fail` doctest to prove it. + +No surface can forge authority: the store resolves every route through a +generational slot, so fabricated or stale bits fail there whatever language +produced them. What differs between the four is how much of the vocabulary you +can say, how much ceremony saying it costs, and how early a mistake is caught — +Rust at compile time, the other three at run time, with C additionally carrying +the pointer lifetimes the adapter has to preserve by hand. + +### What `io` cannot say + +`io` is not a smaller filesystem. It is the same one with the capability +questions answered for you by convention: + +```lua +local f = assert(io.open("/var/log/app", "a")) -- rights: READ WRITE, create, append +``` + +That line asks for a write route and gets one. What it cannot do is ask for +*less*, ask for *more*, or find out what it got. + +```lua +-- Least authority, on purpose: a read route to a file this task may well +-- be able to write. `io.open(p, "r")` asks for the same thing, but the +-- rights are a consequence of the mode rather than the point of the call. +local f = assert(kore.fs.open("/etc/hosts", "r")) + +-- What does this task actually hold here? +local held = assert(kore.fs.rights("/var/log")) +if not held.write then + return nil, "this task was not endowed with write authority there" +end +``` + +`kore.fs` can express every route `io` can — the mapping is the table above — +and three things `io` has no spelling for: rights asked for by name, rights +reported back, and behaviour chosen separately from authority. + +```lua +-- io's "w" is three decisions fused into one letter. Taken apart: +local f = assert(kore.fs.open(path, "rw", {create = true, truncate = true})) + +-- ...so you can take only two of them. No io mode string means this. +local g = assert(kore.fs.open(path, "rw", {create = true})) -- keep what is there +``` + +### Lifetime + +A handle is retired deterministically or not at all. `` is the idiom on +both surfaces, and `__gc` is the backstop: + +```lua +do + local f = assert(kore.fs.open("/tmp/scratch", "rw", {create = true})) + f:write(payload) +end -- retired here, whether the block ended or threw +``` + +### Noticing someone else + +`revision` moves on every commit, so a script can tell that a write landed +underneath it. Nothing on the `io` surface can see this: + +```lua +local before = assert(kore.fs.stat("/conf")).revision +-- ... do work that depends on /conf ... +if assert(kore.fs.stat("/conf")).revision ~= before then + return nil, "/conf changed under us" +end +``` + +`id` is the companion: volume-stable, so it answers "is this the same object" +where a path only answers "is this the same name". + ## Quick reference Every verb, both spellings. `—` means the surface has no spelling for it.