diff --git a/README.md b/README.md
index f2f351e2..19a6d99a 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
-
+
- A Kotlin Multiplatform implementation of an embeddable Lua 5.2 runtime.
+ A Kotlin Multiplatform implementation of an embeddable Lua 5.5.1 runtime.
@@ -12,65 +12,65 @@
-
+
+> **Rebranding in progress.** BlueLuaK is becoming **Basalt Luak** (or simply Luak). The name is moving ahead of everything else: releases still publish to Blueva's Maven repository, the coordinates are still `net.blueva:blueluak-*`, and the packages are still `net.blueva.luak`. The install instructions below are the ones that work today.
+
## Overview
-BlueLuaK is a Kotlin-first fork of [LuaJ 3.0.2](https://github.com/luaj/luaj), rebuilt as a **Kotlin Multiplatform** library. Its shared module currently targets:
+Basalt Luak (or simply Luak) is a Kotlin-first implementation of an embeddable Lua runtime, built as a **Kotlin Multiplatform** library. Its shared module currently targets:
- **JVM 17+**
- **JavaScript IR**, tested on Node.js
- **WebAssembly**, tested on Node.js
- **Kotlin/Native** for Linux x64, Windows x64, macOS x64, and macOS ARM64
-The Lua runtime, value model, bytecode compiler, AST, standard libraries, and ANTLR Kotlin parser live in `commonMain`. JVM-specific integration is isolated from the shared runtime.
+The Lua runtime, value model, bytecode compiler, and standard libraries live in `commonMain`. JVM-specific integration is isolated from the shared runtime.
-BlueLuaK currently implements Lua 5.2 and provides:
+Luak currently implements **Lua 5.5.1** and provides:
- An embeddable Lua VM written entirely in Kotlin.
-- Lua source parsing through ANTLR Kotlin, without JavaCC or generated Java.
- Lua bytecode compilation and execution across the configured KMP targets.
-- Tables, metatables, functions, coroutines, and Lua 5.2 standard libraries.
+- Tables, metatables, functions, coroutines, and Lua 5.5.1 standard libraries.
- `LuaPlatform.standardGlobals()`, one entry point that builds a fully loaded `Globals` on every target.
- A shared `io` library (`io.open`, `io.lines`, `io.tmpfile`, file handles, `os.remove`/`rename`/`tmpname`) on every target, not just the JVM.
-- Shared tests for the runtime, compiler, and parser across KMP targets.
+- Shared tests for the runtime, compiler, and libraries across KMP targets.
- JVM integrations for processes, Java reflection, script engines, and `luajava`.
-BlueLuaK is no longer source-compatible with LuaJ: modules, packages, platform classes, and APIs use BlueLuaK naming under `net.blueva.luak`.
+You can find more information on our website: [luaklang.org](https://luaklang.org)
## Multiplatform Architecture
| Source set or module | Purpose |
|---|---|
-| [`blueluak-core/src/commonMain/kotlin/`](blueluak-core/src/commonMain/kotlin/) | Shared Lua runtime, compiler, AST, parser, and libraries |
-| [`blueluak-core/src/jvmMain/kotlin/`](blueluak-core/src/jvmMain/kotlin/) | JVM implementations of platform abstractions |
-| [`blueluak-core/src/nonJvmMain/kotlin/`](blueluak-core/src/nonJvmMain/kotlin/) | Portable implementations shared by JavaScript and Wasm |
-| [`blueluak-core/src/jsHostMain/kotlin/`](blueluak-core/src/jsHostMain/kotlin/) | JavaScript-host implementations (`node:fs`, `process`) for the JS and Wasm-JS targets |
-| [`blueluak-core/src/wasmWasiMain/kotlin/`](blueluak-core/src/wasmWasiMain/kotlin/) | WASI implementations over raw `wasi_snapshot_preview1` syscalls |
-| [`blueluak-core/src/nativeMain/kotlin/`](blueluak-core/src/nativeMain/kotlin/) | Kotlin/Native implementations of platform abstractions |
-| [`blueluak-core/src/nativePosixMain/kotlin/`](blueluak-core/src/nativePosixMain/kotlin/) | 64-bit file offsets for Linux and macOS |
-| [`blueluak-core/src/nativeWindowsMain/kotlin/`](blueluak-core/src/nativeWindowsMain/kotlin/) | 64-bit file offsets for Windows |
-| [`blueluak-core/src/commonTest/kotlin/`](blueluak-core/src/commonTest/kotlin/) | Tests shared by all core targets |
-| [`blueluak-jvm/src/main/kotlin/`](blueluak-jvm/src/main/kotlin/) | JVM-only integrations and command-line tooling |
-| [`grammar/`](grammar/) | ANTLR Kotlin lexer and parser grammars for Lua 5.2 |
+| [`luak-core/src/commonMain/kotlin/`](luak-core/src/commonMain/kotlin/) | Shared Lua runtime, compiler, and libraries |
+| [`luak-core/src/jvmMain/kotlin/`](luak-core/src/jvmMain/kotlin/) | JVM implementations of platform abstractions |
+| [`luak-core/src/nonJvmMain/kotlin/`](luak-core/src/nonJvmMain/kotlin/) | Portable implementations shared by JavaScript and Wasm |
+| [`luak-core/src/jsHostMain/kotlin/`](luak-core/src/jsHostMain/kotlin/) | JavaScript-host implementations (`node:fs`, `process`) for the JS and Wasm-JS targets |
+| [`luak-core/src/wasmWasiMain/kotlin/`](luak-core/src/wasmWasiMain/kotlin/) | WASI implementations over raw `wasi_snapshot_preview1` syscalls |
+| [`luak-core/src/nativeMain/kotlin/`](luak-core/src/nativeMain/kotlin/) | Kotlin/Native implementations of platform abstractions |
+| [`luak-core/src/nativePosixMain/kotlin/`](luak-core/src/nativePosixMain/kotlin/) | 64-bit file offsets for Linux and macOS |
+| [`luak-core/src/nativeWindowsMain/kotlin/`](luak-core/src/nativeWindowsMain/kotlin/) | 64-bit file offsets for Windows |
+| [`luak-core/src/commonTest/kotlin/`](luak-core/src/commonTest/kotlin/) | Tests shared by all core targets |
+| [`luak-jvm/src/main/kotlin/`](luak-jvm/src/main/kotlin/) | JVM-only integrations and command-line tooling |
| [`examples/`](examples/) | Kotlin and Lua usage examples |
Gradle modules:
| Module | Targets | Purpose |
|---|---|---|
-| `blueluak-core` | JVM, JavaScript IR, Wasm, Kotlin/Native | Multiplatform Lua runtime, compiler, and parser |
-| `blueluak-jvm` | JVM | JVM platform adapters, `luajava`, scripting, CLI, and JIT support |
+| `luak-core` | JVM, JavaScript IR, Wasm, Kotlin/Native | Multiplatform Lua runtime, compiler, and libraries |
+| `luak-jvm` | JVM | JVM platform adapters, `luajava`, scripting, CLI, and JIT support |
-Platform-dependent functionality is exposed through `expect`/`actual` implementations. Code intended to run on every target belongs in `commonMain`; Java and JVM APIs remain confined to JVM source sets and `blueluak-jvm`. No type in the public `commonMain` API is platform-specific.
+Platform-dependent functionality is exposed through `expect`/`actual` implementations. Code intended to run on every target belongs in `commonMain`; Java and JVM APIs remain confined to JVM source sets and `luak-jvm`. No type in the public `commonMain` API is platform-specific.
-The host surface every shared library is built on is deliberately small: console streams, resource lookup, a random-access file handle, delete/rename/temp-name, environment variables, exit, GC, and weak references. Everything else (the value model, the compiler, the parser, and all nine standard libraries) is shared code.
+The host surface every shared library is built on is deliberately small: console streams, resource lookup, a random-access file handle, delete/rename/temp-name, environment variables, exit, GC, and weak references. Everything else (the value model, the compiler, and all nine standard libraries) is shared code.
## Installation
-Releases publish to [repo.blueva.net](https://repo.blueva.net/releases), a public Maven repository, so no authentication is needed to depend on BlueLuaK.
+Releases publish to [repo.blueva.net](https://repo.blueva.net/releases), a public Maven repository, so no authentication is needed to depend on Luak.
### JVM projects
@@ -79,7 +79,7 @@ Two artifacts are available. Pick one:
| Artifact | Contains | Use it when |
|---|---|---|
| `blueluak-jvm` | The multiplatform core (as a compile dependency) plus `JvmPlatform.standardGlobals()`, `luajava`, `io.popen`/`os.execute`, the `luajc` JIT compiler, CLI tooling, and `javax.script` integration | You want a ready-to-use Lua runtime, the common case |
-| `blueluak-core-jvm` | Just the shared runtime, compiler, AST, parser, and standard libraries on the JVM target, including `LuaPlatform.standardGlobals()`, but without `luajava`, `io.popen`, `os.execute`, or the JIT | You don't need the JVM-only integrations, or want the smallest possible footprint |
+| `blueluak-core-jvm` | Just the shared runtime, compiler, and standard libraries on the JVM target, including `LuaPlatform.standardGlobals()`, but without `luajava`, `io.popen`, `os.execute`, or the JIT | You don't need the JVM-only integrations, or want the smallest possible footprint |
`blueluak-jvm` pulls in `blueluak-core-jvm` transitively, so depending on it alone is enough for most projects.
@@ -127,13 +127,13 @@ globals.load("print('hello, world')")!!.call()
`LuaPlatform.debugGlobals()` adds the `debug` library. Loading the individual classes in `net.blueva.luak.lib` (`BaseLib`, `PackageLib`, `StringLib`, `TableLib`, `MathLib`, `CoroutineLib`, `OsLib`, `IoLib`, `Bit32Lib`) by hand remains available when you want a smaller footprint.
-Add the `repo.blueva.net/releases` repository shown above at the project level, then depend on the shared `net.blueva:blueluak-core:26.5
+Add the `repo.blueva.net/releases` repository shown above at the project level, then depend on the shared `net.blueva:blueluak-core:26.5` module.
| Target | Gradle target function | Source set | Tested on |
|---|---|---|---|
| JavaScript IR | `js { nodejs() }` | `jsMain` | Node.js |
| WebAssembly | `wasmJs { nodejs() }` | `wasmJsMain` | Node.js |
-| WebAssembly (WASI) | `wasmWasi { nodejs() }` | `wasmWasiMain` | Node.js's experimental `node:wasi` (raw `wasi_snapshot_preview1` syscalls, no host-specific APIs, so wasmtime/wasmer should work too, though only Node has been verified so far) |
+| WebAssembly (WASI) | `wasmWasi { nodejs() }` | `wasmWasiMain` | Node.js's experimental `node:wasi` |
| Kotlin/Native | `linuxX64()`, `mingwX64()`, `macosX64()`, `macosArm64()` | `linuxX64Main`, `mingwX64Main`, `macosX64Main`, `macosArm64Main` | Matching GitHub Actions runners in CI |
```kotlin
@@ -151,8 +151,6 @@ kotlin {
sourceSets {
commonMain {
dependencies {
- // Resolves to blueluak-core-js, -wasm-js, -wasm-wasi, -linuxx64,
- // -macosarm64, etc. automatically for each target above.
implementation("net.blueva:blueluak-core:26.5")
}
}
@@ -171,16 +169,16 @@ Build every target and module from a clean checkout:
Build only the multiplatform core:
```bash
-./gradlew :blueluak-core:build
+./gradlew :luak-core:build
```
Compile an individual target:
```bash
-./gradlew :blueluak-core:compileKotlinJvm
-./gradlew :blueluak-core:compileKotlinJs
-./gradlew :blueluak-core:compileKotlinWasmJs
-./gradlew :blueluak-core:compileKotlinMacosArm64
+./gradlew :luak-core:compileKotlinJvm
+./gradlew :luak-core:compileKotlinJs
+./gradlew :luak-core:compileKotlinWasmJs
+./gradlew :luak-core:compileKotlinMacosArm64
```
## Testing
@@ -188,20 +186,20 @@ Compile an individual target:
Run every test suite available on the current host:
```bash
-./gradlew :blueluak-core:allTests
+./gradlew :luak-core:allTests
```
Run an individual target suite:
```bash
-./gradlew :blueluak-core:jvmTest
-./gradlew :blueluak-core:jsNodeTest
-./gradlew :blueluak-core:wasmJsNodeTest
-./gradlew :blueluak-core:wasmWasiNodeTest
-./gradlew :blueluak-core:macosArm64Test
+./gradlew :luak-core:jvmTest
+./gradlew :luak-core:jsNodeTest
+./gradlew :luak-core:wasmJsNodeTest
+./gradlew :luak-core:wasmWasiNodeTest
+./gradlew :luak-core:macosArm64Test
```
-Native tests can only run on their matching host. Cross-platform Native compilation remains available from supported hosts. The full build also runs the inherited JVM regression suite; it is green with no `ignoreFailures` exemptions, so any real regression fails the build.
+Native tests can only run on their matching host. Cross-platform Native compilation remains available from supported hosts.
## Requirements
@@ -217,7 +215,7 @@ Use the included wrapper rather than a system Gradle installation.
## Platform Support and Limitations
-The shared runtime, compiler, parser, and standard libraries behave identically on every target. What differs is what the *host* can provide, and BlueLuaK reports those gaps the way Lua does, returning `nil` plus a message or raising an ordinary Lua error, rather than omitting functions:
+The shared runtime, compiler, and standard libraries behave identically on every target. What differs is what the *host* can provide, and Luak reports those gaps the way Lua does, returning `nil` plus a message or raising an ordinary Lua error, rather than omitting functions:
| Capability | JVM | Kotlin/Native | JavaScript / Wasm-JS | Wasm-WASI |
|---|---|---|---|---|
@@ -229,16 +227,8 @@ The shared runtime, compiler, parser, and standard libraries behave identically
| Weak tables (`__mode`) | Yes | Yes | No (no weak references in the host) | No |
| `os.date` / `os.time` | UTC | UTC | UTC | UTC |
-Where a host grants no filesystem at all, `io.open` returns `nil` and a message and the rest of the library keeps working. `io.popen` behaves the same way outside `blueluak-jvm`.
-
-Versions follow a `year.build` scheme: `26.5` is the fifth release of 2026, and the build number restarts when the year changes. A release is cut automatically for every push to `master` that does not carry `[skip ci]`. The public API is still being refined, so **binary compatibility between releases is not guaranteed**. BlueLuaK is not source- or binary-compatible with LuaJ, and reintroducing `org.luaj.vm2` naming is out of scope.
-
-## Roadmap
-
-Current priorities are:
-
-1. Modernize the Lua implementation beyond 5.2.
+Where a host grants no filesystem at all, `io.open` returns `nil` and a message and the rest of the library keeps working. `io.popen` behaves the same way outside `luak-jvm`.
## License
-BlueLuaK is distributed under the [MIT License](LICENSE).
+Basalt Luak is distributed under the [MIT License](LICENSE).
diff --git a/blueluak-core/build.gradle.kts b/blueluak-core/build.gradle.kts
index 83b64ea3..fa2570dd 100644
--- a/blueluak-core/build.gradle.kts
+++ b/blueluak-core/build.gradle.kts
@@ -1,15 +1,12 @@
-import com.strumenta.antlrkotlin.gradle.AntlrKotlinTask
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import java.time.Duration
plugins {
kotlin("multiplatform")
- id("com.strumenta.antlr-kotlin")
`maven-publish`
}
val generatedBuildInfo = layout.buildDirectory.dir("generated-src/build-info/commonMain/kotlin")
-val generatedGrammar = layout.buildDirectory.dir("generated-src/antlr/commonMain/kotlin")
val generateBuildInfo = tasks.register("generateBuildInfo") {
group = "build"
@@ -33,18 +30,6 @@ val generateBuildInfo = tasks.register("generateBuildInfo") {
}
}
-val generateKotlinGrammarSource = tasks.register("generateKotlinGrammarSource") {
- source = fileTree(rootProject.layout.projectDirectory.dir("grammar")) {
- include("LuaLexer.g4", "LuaParser.g4")
- }
- packageName = "net.blueva.luak.parser.antlr"
- arguments = listOf("-visitor", "-no-listener")
- outputDirectory = generatedGrammar
- .map { it.dir("net/blueva/luak/parser/antlr") }
- .get()
- .asFile
-}
-
@OptIn(ExperimentalWasmDsl::class)
kotlin {
jvm()
@@ -113,10 +98,6 @@ kotlin {
}
commonMain {
kotlin.srcDir(generatedBuildInfo)
- kotlin.srcDir(generatedGrammar)
- dependencies {
- implementation("com.strumenta:antlr-kotlin-runtime:1.0.13")
- }
}
commonTest {
dependencies {
@@ -127,7 +108,7 @@ kotlin {
}
tasks.matching { it.name.startsWith("compile") && it.name.contains("Kotlin") }.configureEach {
- dependsOn(generateBuildInfo, generateKotlinGrammarSource)
+ dependsOn(generateBuildInfo)
}
// Bounds every test task so an unresumed coroutine continuation fails
@@ -138,7 +119,7 @@ tasks.matching { it.name.endsWith("Test") }.configureEach {
}
tasks.matching { it.name.endsWith("SourcesJar", ignoreCase = true) }.configureEach {
- dependsOn(generateBuildInfo, generateKotlinGrammarSource)
+ dependsOn(generateBuildInfo)
}
publishing {
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/DecimalFormat.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/DecimalFormat.kt
new file mode 100644
index 00000000..6276194d
--- /dev/null
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/DecimalFormat.kt
@@ -0,0 +1,398 @@
+/******************************************************************************
+ * ____ _ _ _ __
+ * | __ )| |_ _ ___| | _ _ __ _| |/ /
+ * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
+ * | |_) | | |_| | __/ |__| |_| | (_| | . \
+ * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
+ *
+ * BlueLuaK
+ * https://github.com/BluevaDevelopment/BlueLuaK
+ *
+ * Copyright (c) 2026 Blueva Development
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+package net.blueva.luak
+
+/**
+ * Exact decimal rendering of doubles, equivalent to C's `%e`, `%f`, and `%g`.
+ *
+ * Lua's own `tostring` for a float, and `string.format`'s float conversions,
+ * are specified in terms of C's `printf`. Kotlin has no `printf` in common
+ * code, and `Double.toString` answers something different: the *shortest*
+ * decimal that round-trips, which is not the same as a fixed number of
+ * significant digits. `1/3` prints as `0.3333333333333333` there and as
+ * `0.33333333333333331` in Lua.
+ *
+ * So the digits are derived exactly rather than approximated. Every finite
+ * double is `mantissa * 2^exponent` with a 53-bit mantissa, which is exactly
+ * `N * 10^shift` for an integer `N`:
+ *
+ * * when the exponent is positive, `N = mantissa * 2^exponent` and `shift = 0`;
+ * * when it is negative, `1/2^k` is `5^k/10^k`, so `N = mantissa * 5^k` and
+ * `shift = -k`.
+ *
+ * `N` needs more than 64 bits, so it is held as base-10^9 chunks. The work is
+ * bounded by the exponent range: the widest case, a subnormal, needs about
+ * eighty multiply passes over roughly ninety chunks.
+ */
+internal object DecimalFormat {
+
+ /** Chunk base; 10^9 keeps a chunk-by-small-int product inside a Long. */
+ private const val BASE = 1_000_000_000L
+ private const val BASE_DIGITS = 9
+
+ /** Exact digits of a finite, non-zero double, most significant first. */
+ private class Exact(val digits: String, val pointExponent: Int) {
+ /** Exponent `X` in `d.ddd * 10^X`. */
+ val scientificExponent: Int get() = digits.length - 1 + pointExponent
+ }
+
+ /**
+ * C's `%.Pg` for [value].
+ *
+ * Chooses between `%e` and `%f` the way C does, on the decimal exponent,
+ * and drops trailing fractional zeros.
+ */
+ fun g(value: Double, precision: Int): String {
+ if (value.isNaN() || value.isInfinite()) return nonFinite(value, upper = false)
+ val p = if (precision <= 0) 1 else precision
+ if (value == 0.0) return if (1 / value < 0) "-0" else "0"
+ val negative = value < 0
+ val exact = exactDigits(if (negative) -value else value)
+ val rounded = round(exact, p)
+ val exponent = rounded.scientificExponent
+ val text = if (exponent < -4 || exponent >= p) {
+ scientific(rounded, stripZeros = true)
+ } else {
+ plain(rounded, stripZeros = true)
+ }
+ return if (negative) "-$text" else text
+ }
+
+ /**
+ * Lua's `tostring` for a float.
+ *
+ * Upstream formats with `%.15g`, reads the result back, and reformats with
+ * `%.17g` when it did not round-trip; then appends `.0` if what came out
+ * looks like an integer, so a float never prints as one.
+ */
+ fun luaFloat(value: Double): String {
+ if (value.isNaN()) return if (isNegativeNaN(value)) "-nan" else "nan"
+ if (value.isInfinite()) return if (value < 0) "-inf" else "inf"
+ var text = g(value, 15)
+ if (text.toDouble() != value) text = g(value, 17)
+ return if (looksLikeInteger(text)) "$text.0" else text
+ }
+
+ /**
+ * C's `%a`: the exact value in hexadecimal, `0x1.p`.
+ *
+ * Every double has an exact hexadecimal form, so this is the notation to
+ * reach for when a value has to survive being written out and read back -
+ * which is what `string.format("%q", x)` needs.
+ */
+ fun hex(value: Double, upper: Boolean, precision: Int = -1): String {
+ if (value.isNaN() || value.isInfinite()) return nonFinite(value, upper)
+ val bits: Long = value.toRawBits()
+ val negative: Boolean = bits < 0
+ val exponentField: Int = ((bits ushr 52) and 0x7FF).toInt()
+ val mantissaField: Long = bits and 0x000FFFFFFFFFFFFFL
+ val lead: Int
+ val exponent: Int
+ if (exponentField == 0) {
+ // Zero and the subnormals, which have no implicit leading one.
+ lead = 0
+ exponent = if (mantissaField == 0L) 0 else -1022
+ } else {
+ lead = 1
+ exponent = exponentField - 1023
+ }
+ var fraction: String = mantissaField.toString(16).padStart(13, '0')
+ if (precision >= 0) {
+ // A precision on %a counts hexadecimal digits after the point.
+ if (precision < fraction.length) {
+ val cut: Char = fraction[precision]
+ fraction = fraction.substring(0, precision)
+ if (cut >= '8' && fraction.isNotEmpty()) {
+ fraction = incrementHex(fraction)
+ }
+ } else {
+ fraction = fraction.padEnd(precision, '0')
+ }
+ } else {
+ fraction = fraction.trimEnd('0')
+ }
+ val body: String = buildString {
+ if (negative) append('-')
+ append("0x")
+ append(lead)
+ if (fraction.isNotEmpty()) {
+ append('.')
+ append(fraction)
+ }
+ append('p')
+ if (exponent >= 0) append('+')
+ append(exponent)
+ }
+ return if (upper) body.uppercase() else body
+ }
+
+ /** Adds one to a hexadecimal string, keeping its length. */
+ private fun incrementHex(digits: String): String {
+ val out = digits.toCharArray()
+ var index = out.size - 1
+ while (index >= 0) {
+ val value: Int = hexValue(out[index]) + 1
+ if (value < 16) {
+ out[index] = "0123456789abcdef"[value]
+ return out.concatToString()
+ }
+ out[index] = '0'
+ index--
+ }
+ return out.concatToString()
+ }
+
+ private fun hexValue(c: Char): Int = when {
+ c in '0'..'9' -> c - '0'
+ else -> c - 'a' + 10
+ }
+
+ /** C's `%.Pe`. */
+ fun e(value: Double, precision: Int, upper: Boolean): String {
+ if (value.isNaN() || value.isInfinite()) return nonFinite(value, upper)
+ val negative = value < 0 || (value == 0.0 && 1 / value < 0)
+ val magnitude = if (value < 0) -value else value
+ val body = if (magnitude == 0.0) {
+ val digits = buildString {
+ append('0')
+ if (precision > 0) {
+ append('.')
+ repeat(precision) { append('0') }
+ }
+ append(if (upper) "E+00" else "e+00")
+ }
+ digits
+ } else {
+ val rounded = round(exactDigits(magnitude), precision + 1)
+ scientific(rounded, stripZeros = false, upper = upper)
+ }
+ return if (negative) "-$body" else body
+ }
+
+ /** C's `%.Pf`. */
+ fun f(value: Double, precision: Int): String {
+ if (value.isNaN() || value.isInfinite()) return nonFinite(value, false)
+ val negative = value < 0 || (value == 0.0 && 1 / value < 0)
+ val magnitude = if (value < 0) -value else value
+ val body = if (magnitude == 0.0) {
+ buildString {
+ append('0')
+ if (precision > 0) {
+ append('.')
+ repeat(precision) { append('0') }
+ }
+ }
+ } else {
+ val exact = exactDigits(magnitude)
+ // Round at a fixed number of fractional digits rather than
+ // significant ones: keep everything down to 10^-precision.
+ val keep = exact.digits.length + exact.pointExponent + precision
+ val rounded = if (keep <= 0) Exact("0", -precision) else round(exact, keep)
+ plain(rounded, stripZeros = false, minFraction = precision)
+ }
+ return if (negative) "-$body" else body
+ }
+
+ private fun nonFinite(value: Double, upper: Boolean): String {
+ val text = when {
+ value.isNaN() -> "nan"
+ value < 0 -> "-inf"
+ else -> "inf"
+ }
+ return if (upper) text.uppercase() else text
+ }
+
+ /** True when [text] carries no '.', exponent, or other non-digit mark. */
+ private fun looksLikeInteger(text: String): Boolean =
+ text.all { it == '-' || (it in '0'..'9') }
+
+ private fun isNegativeNaN(value: Double): Boolean = value.toRawBits() < 0
+
+ /** Renders as `d.dddde+XX`. */
+ private fun scientific(value: Exact, stripZeros: Boolean, upper: Boolean = false): String {
+ var fraction = value.digits.substring(1)
+ if (stripZeros) fraction = fraction.trimEnd('0')
+ val exponent = value.scientificExponent
+ val sign = if (exponent < 0) '-' else '+'
+ val magnitude = if (exponent < 0) -exponent else exponent
+ val exponentText = if (magnitude < 10) "0$magnitude" else magnitude.toString()
+ return buildString {
+ append(value.digits[0])
+ if (fraction.isNotEmpty()) {
+ append('.')
+ append(fraction)
+ }
+ append(if (upper) 'E' else 'e')
+ append(sign)
+ append(exponentText)
+ }
+ }
+
+ /** Renders without an exponent. */
+ private fun plain(value: Exact, stripZeros: Boolean, minFraction: Int = 0): String {
+ val digits = value.digits
+ val point = digits.length + value.pointExponent // digits before the '.'
+ val whole: String
+ var fraction: String
+ when {
+ point <= 0 -> {
+ whole = "0"
+ fraction = "0".repeat(-point) + digits
+ }
+
+ point >= digits.length -> {
+ whole = digits + "0".repeat(point - digits.length)
+ fraction = ""
+ }
+
+ else -> {
+ whole = digits.substring(0, point)
+ fraction = digits.substring(point)
+ }
+ }
+ if (stripZeros) fraction = fraction.trimEnd('0')
+ while (fraction.length < minFraction) fraction += "0"
+ return if (fraction.isEmpty()) whole else "$whole.$fraction"
+ }
+
+ /** Rounds [value] to [significant] digits, half-to-even on an exact tie. */
+ private fun round(value: Exact, significant: Int): Exact {
+ val digits = value.digits
+ if (significant >= digits.length) return value
+ if (significant <= 0) return Exact("0", value.scientificExponent + 1)
+
+ val kept = digits.substring(0, significant)
+ val dropped = digits.substring(significant)
+ val first = dropped[0]
+ val restNonZero = dropped.drop(1).any { it != '0' }
+ val roundUp = when {
+ first > '5' -> true
+ first < '5' -> false
+ restNonZero -> true
+ else -> (kept.last() - '0') % 2 == 1 // exact tie: to even
+ }
+
+ val newPointExponent = value.pointExponent + dropped.length
+ if (!roundUp) return Exact(kept, newPointExponent)
+
+ val bumped = increment(kept)
+ return if (bumped.length > kept.length) {
+ // Carried past the leading digit, as 999 -> 1000.
+ Exact(bumped.substring(0, kept.length), newPointExponent + 1)
+ } else {
+ Exact(bumped, newPointExponent)
+ }
+ }
+
+ /** Adds one to a decimal string, growing it if it carries out. */
+ private fun increment(digits: String): String {
+ val out = digits.toCharArray()
+ var index = out.size - 1
+ while (index >= 0) {
+ if (out[index] != '9') {
+ out[index] = out[index] + 1
+ return out.concatToString()
+ }
+ out[index] = '0'
+ index--
+ }
+ return "1" + out.concatToString()
+ }
+
+ /** The exact decimal digits of a finite, positive double. */
+ private fun exactDigits(value: Double): Exact {
+ val bits = value.toRawBits()
+ val exponentField = ((bits ushr 52) and 0x7FF).toInt()
+ val mantissaField = bits and 0x000FFFFFFFFFFFFFL
+ val mantissa: Long
+ val exponent: Int
+ if (exponentField == 0) {
+ mantissa = mantissaField // subnormal: no implicit leading bit
+ exponent = -1074
+ } else {
+ mantissa = mantissaField or 0x0010000000000000L
+ exponent = exponentField - 1075
+ }
+
+ var chunks = fromLong(mantissa)
+ val pointExponent: Int
+ if (exponent >= 0) {
+ // 2^29 is the largest power of two that keeps chunk * factor in a Long.
+ var remaining = exponent
+ while (remaining > 0) {
+ val step = if (remaining > 29) 29 else remaining
+ chunks = multiply(chunks, 1L shl step)
+ remaining -= step
+ }
+ pointExponent = 0
+ } else {
+ // 1/2^k == 5^k / 10^k, so scale by 5^k and shift the point by k.
+ var remaining = -exponent
+ while (remaining > 0) {
+ val step = if (remaining > 13) 13 else remaining
+ chunks = multiply(chunks, pow5(step))
+ remaining -= step
+ }
+ pointExponent = exponent
+ }
+ return Exact(toDecimalString(chunks), pointExponent)
+ }
+
+ private fun pow5(exponent: Int): Long {
+ var result = 1L
+ repeat(exponent) { result *= 5L }
+ return result
+ }
+
+ private fun fromLong(value: Long): LongArray {
+ if (value == 0L) return longArrayOf(0L)
+ var remaining = value
+ val chunks = ArrayList(3)
+ while (remaining > 0) {
+ chunks.add(remaining % BASE)
+ remaining /= BASE
+ }
+ return chunks.toLongArray()
+ }
+
+ /** Little-endian base-10^9 multiply by a factor small enough to stay exact. */
+ private fun multiply(chunks: LongArray, factor: Long): LongArray {
+ val out = LongArray(chunks.size + 3)
+ var carry = 0L
+ for (index in chunks.indices) {
+ val product = chunks[index] * factor + carry
+ out[index] = product % BASE
+ carry = product / BASE
+ }
+ var index = chunks.size
+ while (carry > 0) {
+ out[index++] = carry % BASE
+ carry /= BASE
+ }
+ var size = out.size
+ while (size > 1 && out[size - 1] == 0L) size--
+ return out.copyOf(size)
+ }
+
+ /** Most-significant-first decimal digits, without leading zeros. */
+ private fun toDecimalString(chunks: LongArray): String = buildString {
+ append(chunks[chunks.size - 1].toString())
+ for (index in chunks.size - 2 downTo 0) {
+ val chunk = chunks[index].toString()
+ repeat(BASE_DIGITS - chunk.length) { append('0') }
+ append(chunk)
+ }
+ }
+}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Globals.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Globals.kt
index f3aa2016..574dbce7 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Globals.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Globals.kt
@@ -135,6 +135,78 @@ class Globals : LuaTable() {
/** The DebugLib instance loaded into this Globals, or null if debugging is not enabled */
var debuglib: DebugLib? = null
+ /**
+ * Objects the host has reclaimed whose `__gc` handler has still to run.
+ *
+ * Filled by the host, off whatever thread it reclaims on, and emptied here
+ * where Lua code can safely be run - which is what [runfinalizers] does.
+ */
+ internal val finalized: MutableList = ArrayList()
+
+ /** True once anything at all has been marked for finalization. */
+ internal var marksfinalizers: Boolean = false
+
+ /** True while a finalizer runs, so that one cannot set off another. */
+ private var finalizing: Boolean = false
+
+ /**
+ * Marks [target] to have its `__gc` handler run once it is unreachable.
+ *
+ * As in Lua this happens when the metatable is set, and only then: a
+ * `__gc` added to a metatable that is already in use has no effect on
+ * objects that were given it earlier.
+ */
+ internal fun markforfinalization(target: LuaValue) {
+ if (target.gckeeper != null) return
+ val keeper: Any? = watchForFinalization(target, finalized)
+ if (keeper == null) return // a host that cannot finalize at all
+ target.gckeeper = keeper
+ marksfinalizers = true
+ }
+
+ /**
+ * Runs the `__gc` handler of everything the host has reclaimed.
+ *
+ * Called where the interpreter allocates, which is where Lua runs a step
+ * of its own collector, and again whenever `collectgarbage` is asked to
+ * collect. A handler that raises is reported as a warning and does not
+ * disturb what was running, which is what Lua does with one.
+ */
+ internal fun runfinalizers() {
+ if (!marksfinalizers || finalizing) return
+ var due: List = takeFinalized(finalized)
+ if (due.isEmpty()) {
+ // Nothing reclaimed yet. Where enough has been allocated that Lua
+ // would have run a cycle of its own by now, the host is asked for
+ // one: a program waiting for a finalizer to run has nothing else
+ // to wait for, and the host collects when it sees fit rather than
+ // when Lua would.
+ if (Memory.sincecollect < Memory.COLLECT_EVERY) return
+ Memory.collected()
+ platformCollectGarbage()
+ due = takeFinalized(finalized)
+ if (due.isEmpty()) return
+ }
+ finalizing = true
+ try {
+ for (target in due) {
+ val handler: LuaValue = target.metatag(LuaValue.GC)
+ if (handler.isnil()) continue
+ val state: LuaThread.State = running.state
+ state.finalizerframepending = true
+ try {
+ handler.call(target)
+ } catch (failure: LuaError) {
+ baselib?.warning("error in __gc metamethod (" + failure.message + ")")
+ } finally {
+ state.finalizerframepending = false
+ }
+ }
+ } finally {
+ finalizing = false
+ }
+ }
+
/** Interface for module that converts a Prototype into a LuaFunction with an environment. */
interface Loader {
/** Convert the prototype into a LuaFunction with the supplied environment. */
@@ -185,6 +257,10 @@ class Globals : LuaTable() {
try {
val stream = finder?.findResource(filename) ?: throw LuaError("load $filename: no resource")
return load(stream, "@" + filename, "bt", this)
+ } catch (l: LuaError) {
+ // Already says what is wrong in Lua's own words - where in the
+ // file, and what about it - so nothing is added to it here.
+ throw l
} catch (e: Exception) {
return error("load " + filename + ": " + e)
}
@@ -256,7 +332,12 @@ class Globals : LuaTable() {
fun load(`is`: InputStream, chunkname: String?, mode: String, environment: LuaValue?): LuaValue? {
try {
val p: Prototype? = loadPrototype(`is`, chunkname, mode)
- return loader!!.load(p, chunkname, environment)
+ val loaded: LuaValue? = loader!!.load(p, chunkname, environment)
+ // A chunk given an environment of its own still runs in this
+ // state: what it reads its globals from and what it runs in are
+ // two different things.
+ if (loaded is LuaClosure && loaded.globals == null) loaded.globals = this
+ return loaded
} catch (l: LuaError) {
throw l
} catch (e: Exception) {
@@ -274,19 +355,25 @@ class Globals : LuaTable() {
@kotlin.Throws(IOException::class)
fun loadPrototype(`is`: InputStream, chunkname: String?, mode: String): Prototype? {
var `is`: InputStream = `is`
- if (mode.indexOf('b') >= 0) {
+ if (!`is`.markSupported()) `is` = net.blueva.luak.Globals.BufferedStream(`is`)
+ `is`.mark(4)
+ val first: Int = `is`.read()
+ `is`.reset()
+ // The signature byte says which kind of chunk is really there, so the
+ // mode is checked against that rather than against what the caller
+ // hoped for: asking for text and handing over a dump is refused, not
+ // parsed as source.
+ if (first == LoadState.LUA_SIGNATURE[0].toInt()) {
+ if (mode.indexOf('b') < 0) {
+ error("attempt to load a binary chunk (mode is '" + mode + "')")
+ }
if (undumper == null) error("No undumper.")
- if (!`is`.markSupported()) `is` = net.blueva.luak.Globals.BufferedStream(`is`)
- `is`.mark(4)
- val p: Prototype? = undumper!!.undump(`is`, chunkname)
- if (p != null) return p
- `is`.reset()
+ return undumper!!.undump(`is`, chunkname)
}
- if (mode.indexOf('t') >= 0) {
- return compilePrototype(`is`, chunkname)
+ if (mode.indexOf('t') < 0) {
+ error("attempt to load a text chunk (mode is '" + mode + "')")
}
- error("Failed to load prototype " + chunkname + " using mode '" + mode + "'")
- return null
+ return compilePrototype(`is`, chunkname)
}
/** Compile lua source from a Reader into a Prototype. The characters in the reader
@@ -319,13 +406,13 @@ class Globals : LuaTable() {
* @return Values supplied as arguments to the resume() call that reactivates this thread.
*/
fun yield(args: Varargs?): Varargs {
- if (running.isMainThread) throw LuaError("cannot yield main thread")
+ if (running.isMainThread) throw LuaError("attempt to yield from outside a coroutine")
return runLuaSync { yieldSuspend(args) }
}
/** Suspending counterpart to [yield]; see its doc for details. */
suspend fun yieldSuspend(args: Varargs?): Varargs {
- if (running.isMainThread) throw LuaError("cannot yield main thread")
+ if (running.isMainThread) throw LuaError("attempt to yield from outside a coroutine")
val s: LuaThread.State = running.state
return s.lua_yield(args)
}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LoadState.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LoadState.kt
index e9e726aa..2c6ee77e 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LoadState.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LoadState.kt
@@ -17,6 +17,7 @@
package net.blueva.luak
import net.blueva.luak.io.DataInputStream
+import net.blueva.luak.io.EOFException
import net.blueva.luak.io.IOException
import net.blueva.luak.io.InputStream
@@ -148,13 +149,24 @@ class LoadState private constructor(
/** Load a lua strin gvalue from the input stream
* @return the [LuaString] value laoded.
*/
+ /** Every string read so far, in the order they were written; see DumpState.dumpString. */
+ private val read: ArrayList = ArrayList()
+
@kotlin.Throws(IOException::class)
fun loadString(): LuaString? {
val size = if (this.luacSizeofSizeT == 8) loadInt64().toInt() else loadInt()
if (size == 0) return null
+ if (size < 0) {
+ // A string written once and pointed at since.
+ val at: Int = -size
+ if (at > read.size) badformat("corrupted chunk")
+ return read[at - 1]
+ }
val bytes = ByteArray(size)
`is`.readFully(bytes, 0, size)
- return LuaString.valueUsing(bytes, 0, bytes.size - 1)
+ val string: LuaString = LuaString.valueUsing(bytes, 0, bytes.size - 1)
+ read.add(string)
+ return string
}
/**
@@ -188,6 +200,7 @@ class LoadState private constructor(
(if (0 != `is`.readUnsignedByte()) LuaValue.TRUE else LuaValue.FALSE)
net.blueva.luak.LoadState.Companion.LUA_TINT -> values[i] = LuaInteger.valueOf(loadInt())!!
+ net.blueva.luak.LoadState.Companion.LUA_TNUMINT -> values[i] = LuaInteger.valueOf(loadInt64())!!
net.blueva.luak.LoadState.Companion.LUA_TNUMBER -> values[i] = loadNumber()
net.blueva.luak.LoadState.Companion.LUA_TSTRING -> values[i] = loadString()
else -> throw IllegalStateException("bad constant")
@@ -221,7 +234,6 @@ class LoadState private constructor(
*/
@kotlin.Throws(IOException::class)
fun loadDebug(f: Prototype) {
- f.source = loadString()
f.lineinfo = loadIntArray()
var n = loadInt()
f.locvars = if (n > 0) arrayOfNulls(n) else net.blueva.luak.LoadState.Companion.NOLOCVARS
@@ -245,10 +257,10 @@ class LoadState private constructor(
@kotlin.Throws(IOException::class)
fun loadFunction(p: LuaString?): Prototype {
val f: Prototype = Prototype()
- //// this.L.push(f);
-// f.source = loadString();
-// if ( f.source == null )
-// f.source = p;
+ // Nothing written for the source means the function came from the same
+ // text as the one around it; see DumpState.dumpFunction.
+ f.source = loadString()
+ if (f.source == null) f.source = p
f.linedefined = loadInt()
f.lastlinedefined = loadInt()
f.numparams = `is`.readUnsignedByte()
@@ -273,18 +285,84 @@ class LoadState private constructor(
* @throws IOException if an i/o exception occurs.
*/
@kotlin.Throws(IOException::class)
+ /** Reads back what [net.blueva.luak.compiler.DumpState.dumpHeader] wrote, refusing anything else. */
fun loadHeader() {
- luacVersion = `is`.readByte().toInt()
- luacFormat = `is`.readByte().toInt()
- luacLittleEndian = (0 != `is`.readByte().toInt())
- luacSizeofInt = `is`.readByte().toInt()
- luacSizeofSizeT = `is`.readByte().toInt()
- luacSizeofInstruction = `is`.readByte().toInt()
- luacSizeofLuaNumber = `is`.readByte().toInt()
- luacNumberFormat = `is`.readByte().toInt()
- for (i in net.blueva.luak.LoadState.Companion.LUAC_TAIL.indices) if (`is`.readByte().toInt() != net.blueva.luak.LoadState.Companion.LUAC_TAIL[i].toInt()) throw LuaError(
- "Unexpeted byte in luac tail of header, index=" + i
- )
+ // Set before anything of more than one byte is read: the header says
+ // what the rest of the chunk looks like by carrying one value of each
+ // kind, and those values are read the way this build writes them.
+ luacSizeofInt = 4
+ luacSizeofSizeT = 4
+ luacSizeofInstruction = 4
+ luacVersion = `is`.readByte().toInt() and 0xFF
+ if (luacVersion != net.blueva.luak.LoadState.Companion.LUAC_VERSION) {
+ badformat("version mismatch")
+ }
+ luacFormat = `is`.readByte().toInt() and 0xFF
+ if (luacFormat != net.blueva.luak.LoadState.Companion.LUAC_FORMAT) {
+ badformat("format mismatch")
+ }
+ for (i in net.blueva.luak.LoadState.Companion.LUAC_TAIL.indices) {
+ if (`is`.readByte() != net.blueva.luak.LoadState.Companion.LUAC_TAIL[i]) {
+ badformat("corrupted chunk")
+ }
+ }
+ checksize(4, "int")
+ // Which way round the bytes go is not written down: it shows in the
+ // known value itself, which only comes back whole when it is read the
+ // way it was written.
+ `is`.readFully(buf, 0, 4)
+ val little: Int = (buf[3].toInt() shl 24) or ((0xff and buf[2].toInt()) shl 16) or
+ ((0xff and buf[1].toInt()) shl 8) or (0xff and buf[0].toInt())
+ val big: Int = (buf[0].toInt() shl 24) or ((0xff and buf[1].toInt()) shl 16) or
+ ((0xff and buf[2].toInt()) shl 8) or (0xff and buf[3].toInt())
+ luacLittleEndian = when (net.blueva.luak.LoadState.Companion.LUAC_INT) {
+ little -> true
+ big -> false
+ else -> badformat("corrupted chunk")
+ }
+ checksize(4, "instruction")
+ checkvalue(loadInt().toLong(), net.blueva.luak.LoadState.Companion.LUAC_INST.toLong())
+ checksize(8, "integer")
+ checkvalue(loadInt64(), net.blueva.luak.LoadState.Companion.LUAC_INT.toLong())
+ // The size a number takes is also what says whether this chunk holds
+ // floats at all; a build that keeps every number as an integer wrote
+ // one of those here instead.
+ luacSizeofLuaNumber = `is`.readByte().toInt() and 0xFF
+ when (luacSizeofLuaNumber) {
+ 4 -> {
+ luacNumberFormat = net.blueva.luak.LoadState.Companion.NUMBER_FORMAT_INTS_ONLY
+ checkvalue(loadInt().toLong(), net.blueva.luak.LoadState.Companion.LUAC_INT.toLong())
+ }
+
+ 8 -> {
+ luacNumberFormat = net.blueva.luak.LoadState.Companion.NUMBER_FORMAT_FLOATS_OR_DOUBLES
+ if (loadInt64() != net.blueva.luak.LoadState.Companion.LUAC_NUM.toBits()) {
+ badformat("float format mismatch")
+ }
+ }
+
+ else -> badformat("number size mismatch")
+ }
+ }
+
+ /** Refuses a chunk whose values are not the size this build writes. */
+ private fun checksize(expected: Int, what: String) {
+ if ((`is`.readByte().toInt() and 0xFF) != expected) badformat(what + " size mismatch")
+ }
+
+ /** Refuses a chunk whose known value did not come back unchanged. */
+ private fun checkvalue(read: Long, expected: Long) {
+ if (read != expected) badformat("corrupted chunk")
+ }
+
+ /**
+ * Refuses the chunk, saying which part of it could not be read.
+ *
+ * Written the way Lua writes it, since a program that loads a chunk it
+ * did not write reads the message to find out what it was given.
+ */
+ internal fun badformat(why: String): Nothing {
+ throw LuaError((name ?: "?") + ": bad binary format (" + why + ")")
}
/** Private constructor for create a load state */
@@ -319,6 +397,16 @@ class LoadState private constructor(
const val LUA_TBOOLEAN: Int = 1
const val LUA_TLIGHTUSERDATA: Int = 2
const val LUA_TNUMBER: Int = 3
+
+ /**
+ * Constant tag for the integer subtype of a number.
+ *
+ * Upstream tags a dumped constant with its full type tag, of which the
+ * number type has two variants: `LUA_VNUMFLT` is the plain number tag
+ * and `LUA_VNUMINT` sets the variant bit. Without the distinction a
+ * dumped chunk cannot say which subtype a numeral had.
+ */
+ const val LUA_TNUMINT: Int = 3 or (1 shl 4)
const val LUA_TSTRING: Int = 4
const val LUA_TTABLE: Int = 5
const val LUA_TFUNCTION: Int = 6
@@ -348,8 +436,17 @@ class LoadState private constructor(
val SOURCE_BINARY_STRING: String = "binary string"
- /** for header of binary files -- this is Lua 5.2 */
- const val LUAC_VERSION: Int = 0x52
+ /** for header of binary files -- this is Lua 5.5 */
+ const val LUAC_VERSION: Int = 0x55
+
+ /** A known integer, written and read back to check how one is stored. */
+ const val LUAC_INT: Int = -0x5678
+
+ /** A known instruction word, for the same reason as [LUAC_INT]. */
+ const val LUAC_INST: Int = 0x12345678
+
+ /** A known float, for the same reason as [LUAC_INT]. */
+ const val LUAC_NUM: Double = -370.5
/** for header of binary files -- this is the official format */
const val LUAC_FORMAT: Int = 0
@@ -374,22 +471,10 @@ class LoadState private constructor(
* @return [LuaInteger] or [LuaDouble] whose value corresponds to the bits provided.
*/
fun longBitsToLuaNumber(bits: Long): LuaValue {
- if ((bits and ((1L shl 63) - 1)) == 0L) {
- return LuaValue.ZERO!!
- }
-
- val e = ((bits shr 52) and 0x7ffL).toInt() - 1023
-
- if (e >= 0 && e < 31) {
- val f = bits and 0xFFFFFFFFFFFFFL
- val shift = 52 - e
- val intPrecMask = (1L shl shift) - 1
- if ((f and intPrecMask) == 0L) {
- val intValue = (f shr shift).toInt() or (1 shl e)
- return LuaInteger.valueOf(if ((bits shr 63) != 0L) -intValue else intValue)!!
- }
- }
-
+ // A float constant stays a float. This used to hand back an integer
+ // whenever the double had no fractional part, which turned a dumped
+ // `2.0` into a `2` on the way back in, and a dumped `-0.0` into a
+ // plain zero.
return LuaValue.valueOf(Double.fromBits(bits))
}
@@ -402,21 +487,32 @@ class LoadState private constructor(
*/
@kotlin.Throws(IOException::class)
fun undump(stream: InputStream, chunkname: String): Prototype? {
+ val sname: String? = net.blueva.luak.LoadState.Companion.getSourceName(chunkname)
// check rest of signature
- if (stream.read() != LUA_SIGNATURE[0].toInt() || stream.read() != LUA_SIGNATURE[1].toInt() || stream.read() != LUA_SIGNATURE[2].toInt() || stream.read() != LUA_SIGNATURE[3].toInt()) return null
-
+ for (i in LUA_SIGNATURE.indices) {
+ val read: Int = stream.read()
+ // Nothing left to read is a chunk that was cut short; a byte
+ // that is simply not the one expected is not a chunk at all.
+ if (read < 0) {
+ throw LuaError((sname ?: "?") + ": bad binary format (truncated chunk)")
+ }
+ if (read != LUA_SIGNATURE[i].toInt()) return null
+ }
// load file as a compiled chunk
- val sname: String? = net.blueva.luak.LoadState.Companion.getSourceName(chunkname)
val s: LoadState = net.blueva.luak.LoadState(stream, sname)
- s.loadHeader()
-
- // check format
- when (s.luacNumberFormat) {
- net.blueva.luak.LoadState.Companion.NUMBER_FORMAT_FLOATS_OR_DOUBLES, net.blueva.luak.LoadState.Companion.NUMBER_FORMAT_INTS_ONLY, net.blueva.luak.LoadState.Companion.NUMBER_FORMAT_NUM_PATCH_INT32 -> {}
- else -> throw LuaError("unsupported int size")
+ try {
+ s.loadHeader()
+ // A binary chunk carries its own source, and where it does not
+ // - a chunk dumped without debug information - it stays
+ // without one: the name this was loaded under says where the
+ // bytes came from, not where the code was written.
+ return s.loadFunction(null)
+ } catch (short: EOFException) {
+ // Read past the end: what there was of the chunk was read as
+ // far as it went, and it went no further.
+ s.badformat("truncated chunk")
}
- return s.loadFunction(LuaString.valueOf(sname!!))
}
/**
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Lua.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Lua.kt
index f3ba5956..66a0d7c8 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Lua.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Lua.kt
@@ -26,12 +26,35 @@ package net.blueva.luak
*/
open class Lua {
companion object {
- /** version is supplied by ant build task */
- val _VERSION: String = BuildInfo.VERSION
+ /** The Lua *language* version this runtime implements, as scripts see it in
+ * the `_VERSION` global. Lua programs branch on this
+ * (`if _VERSION == "Lua 5.5" then ...`) and the reference test suite reads
+ * it, so it must name the language and not the implementation. See
+ * [BLUELUAK_VERSION] for BlueLuaK's own release number.
+ *
+ * One 5.5 language feature is still missing behind this: a named vararg
+ * parameter, `function f(...t)`, whose table shares storage with `...` and
+ * so needs the 5.5 vararg model rather than the 5.2-shaped one the port is
+ * still on. */
+ val _VERSION: String = "Lua 5.5"
+
+ /** BlueLuaK's own release, such as `"BlueLuaK 26.5"`. This is what tooling
+ * should report as the *engine* version; [_VERSION] is the language. */
+ val BLUELUAK_VERSION: String = BuildInfo.VERSION
/** use return values from previous op */
val LUA_MULTRET: Int = -1
+ /**
+ * Bit in `Prototype.is_vararg` marking a named vararg parameter.
+ *
+ * `function f(a, ...t)`, from Lua 5.5, binds the extra arguments to a
+ * table. The table and `...` are the same storage, so assigning `t[1]`
+ * changes what `...` yields, which is why the table is built once on entry
+ * and `...` is read back out of it.
+ */
+ const val VARARG_NAMED: Int = 2
+
// from lopcodes.h
/*===========================================================================
@@ -225,7 +248,30 @@ open class Lua {
const val OP_EXTRAARG: Int = 39 /* Ax extra (larger) argument for previous opcode */
- val NUM_OPCODES: Int = net.blueva.luak.Lua.OP_EXTRAARG + 1
+ /* Opcodes added by the port past Lua 5.2. They are appended rather than
+ slotted into upstream's order so existing 5.2 bytecode keeps loading;
+ renumbering to match 5.5 belongs with the instruction-set rewrite. */
+ const val OP_IDIV: Int = 40 /* A B C R(A) := RK(B) // RK(C) */
+ const val OP_BAND: Int = 41 /* A B C R(A) := RK(B) & RK(C) */
+ const val OP_BOR: Int = 42 /* A B C R(A) := RK(B) | RK(C) */
+ const val OP_BXOR: Int = 43 /* A B C R(A) := RK(B) ~ RK(C) */
+ const val OP_SHL: Int = 44 /* A B C R(A) := RK(B) << RK(C) */
+ const val OP_SHR: Int = 45 /* A B C R(A) := RK(B) >> RK(C) */
+ const val OP_BNOT: Int = 46 /* A B R(A) := ~R(B) */
+
+ /** `A` - mark R(A) as a to-be-closed variable, from Lua 5.4's ``. */
+ const val OP_TBC: Int = 47 /* A mark R(A) "to be closed" */
+
+ /**
+ * `A Bx` - raise an error if R(A) is not nil, from Lua 5.5's `global`.
+ *
+ * A `global x = v` declaration checks that the global is still unset
+ * before assigning it. `Kst(Bx - 1)` is the global's name, and `Bx == 0`
+ * means the name did not fit in the constant table.
+ */
+ const val OP_ERRNNIL: Int = 48 /* A Bx if R(A) ~= nil then error */
+
+ val NUM_OPCODES: Int = net.blueva.luak.Lua.OP_ERRNNIL + 1
/* pseudo-opcodes used in parsing only. */
const val OP_GT: Int = 63 // >
@@ -309,6 +355,15 @@ open class Lua {
(0 shl 7) or (1 shl 6) or (net.blueva.luak.Lua.OpArgU shl 4) or (net.blueva.luak.Lua.OpArgN shl 2) or (net.blueva.luak.Lua.iABx), /* OP_CLOSURE */
(0 shl 7) or (1 shl 6) or (net.blueva.luak.Lua.OpArgU shl 4) or (net.blueva.luak.Lua.OpArgN shl 2) or (net.blueva.luak.Lua.iABC), /* OP_VARARG */
(0 shl 7) or (0 shl 6) or (net.blueva.luak.Lua.OpArgU shl 4) or (net.blueva.luak.Lua.OpArgU shl 2) or (net.blueva.luak.Lua.iAx), /* OP_EXTRAARG */
+ (0 shl 7) or (1 shl 6) or (net.blueva.luak.Lua.OpArgK shl 4) or (net.blueva.luak.Lua.OpArgK shl 2) or (net.blueva.luak.Lua.iABC), /* OP_IDIV */
+ (0 shl 7) or (1 shl 6) or (net.blueva.luak.Lua.OpArgK shl 4) or (net.blueva.luak.Lua.OpArgK shl 2) or (net.blueva.luak.Lua.iABC), /* OP_BAND */
+ (0 shl 7) or (1 shl 6) or (net.blueva.luak.Lua.OpArgK shl 4) or (net.blueva.luak.Lua.OpArgK shl 2) or (net.blueva.luak.Lua.iABC), /* OP_BOR */
+ (0 shl 7) or (1 shl 6) or (net.blueva.luak.Lua.OpArgK shl 4) or (net.blueva.luak.Lua.OpArgK shl 2) or (net.blueva.luak.Lua.iABC), /* OP_BXOR */
+ (0 shl 7) or (1 shl 6) or (net.blueva.luak.Lua.OpArgK shl 4) or (net.blueva.luak.Lua.OpArgK shl 2) or (net.blueva.luak.Lua.iABC), /* OP_SHL */
+ (0 shl 7) or (1 shl 6) or (net.blueva.luak.Lua.OpArgK shl 4) or (net.blueva.luak.Lua.OpArgK shl 2) or (net.blueva.luak.Lua.iABC), /* OP_SHR */
+ (0 shl 7) or (1 shl 6) or (net.blueva.luak.Lua.OpArgR shl 4) or (net.blueva.luak.Lua.OpArgN shl 2) or (net.blueva.luak.Lua.iABC), /* OP_BNOT */
+ (0 shl 7) or (1 shl 6) or (net.blueva.luak.Lua.OpArgN shl 4) or (net.blueva.luak.Lua.OpArgN shl 2) or (net.blueva.luak.Lua.iABC), /* OP_TBC */
+ (0 shl 7) or (0 shl 6) or (net.blueva.luak.Lua.OpArgN shl 4) or (net.blueva.luak.Lua.OpArgN shl 2) or (net.blueva.luak.Lua.iABx), /* OP_ERRNNIL */
)
fun getOpMode(m: Int): Int {
@@ -334,22 +389,43 @@ open class Lua {
/* number of list items to accumulate before a SETLIST instruction */
const val LFIELDS_PER_FLUSH: Int = 50
- private const val MAXSRC = 80
+ /** Room for the identifier a chunk is named by, upstream's `LUA_IDSIZE`. */
+ private const val LUA_IDSIZE = 60
+
+ private const val CHUNKID_ELLIPSIS = "..."
+ private const val CHUNKID_PREFIX = "[string \""
+ private const val CHUNKID_SUFFIX = "\"]"
+ /**
+ * The name a chunk goes by in error messages, upstream's `luaO_chunkid`.
+ *
+ * A `=` source is taken literally, a `@` source is a file name and keeps
+ * its tail since the directories in front of it matter less, and anything
+ * else is source text, quoted and cut short at its first line.
+ */
fun chunkid(source: String): String {
- var source = source
- if (source.startsWith("=")) return source.substring(1)
- var end = ""
+ val bufflen: Int = net.blueva.luak.Lua.LUA_IDSIZE
+ if (source.startsWith("=")) {
+ return if (source.length <= bufflen) source.substring(1) else source.substring(1, bufflen)
+ }
if (source.startsWith("@")) {
- source = source.substring(1)
- } else {
- source = "[string \"" + source
- end = "\"]"
+ if (source.length <= bufflen) return source.substring(1)
+ return net.blueva.luak.Lua.CHUNKID_ELLIPSIS +
+ source.substring(1 + source.length - (bufflen - net.blueva.luak.Lua.CHUNKID_ELLIPSIS.length))
+ }
+ val newline: Int = source.indexOf('\n')
+ val room: Int = bufflen - (
+ net.blueva.luak.Lua.CHUNKID_PREFIX.length +
+ net.blueva.luak.Lua.CHUNKID_ELLIPSIS.length +
+ net.blueva.luak.Lua.CHUNKID_SUFFIX.length
+ ) - 1
+ if (source.length < room && newline < 0) {
+ return net.blueva.luak.Lua.CHUNKID_PREFIX + source + net.blueva.luak.Lua.CHUNKID_SUFFIX
}
- val n: Int = source.length + end.length
- if (n > net.blueva.luak.Lua.MAXSRC) source =
- source.substring(0, net.blueva.luak.Lua.MAXSRC - end.length - 3) + "..."
- return source + end
+ var length: Int = if (newline >= 0) newline else source.length
+ if (length > room) length = room
+ return net.blueva.luak.Lua.CHUNKID_PREFIX + source.substring(0, length) +
+ net.blueva.luak.Lua.CHUNKID_ELLIPSIS + net.blueva.luak.Lua.CHUNKID_SUFFIX
}
}
}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaClosure.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaClosure.kt
index f3a02e77..dcae0a9a 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaClosure.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaClosure.kt
@@ -28,6 +28,14 @@ import kotlin.coroutines.startCoroutine
* called from a library function like `table.sort`'s comparator calls
* `coroutine.yield()` - that correctly surfaces as a boundary error, exactly
* like real Lua's C-call boundary restriction. */
+/**
+ * How many frames a host stack overflow unwinds before it is reported.
+ *
+ * Enough room for a message handler - `debug.traceback` above all - to run in
+ * without running out of stack all over again.
+ */
+private const val STACK_UNWIND_HEADROOM: Int = 64
+
internal fun runLuaSync(block: suspend () -> T): T {
var outcome: Result? = null
block.startCoroutine(Continuation(EmptyCoroutineContext) { outcome = it })
@@ -97,7 +105,16 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
lateinit var upValues: Array
- val globals: Globals?
+ /**
+ * The state this closure belongs to, which is where the debug library,
+ * the running thread and the rest of what a program needs are kept.
+ *
+ * Not the same thing as the table the chunk reads its globals from: a
+ * chunk loaded with an environment of its own still runs in the state
+ * that loaded it, which is what fills this in.
+ */
+ var globals: Globals? = null
+ internal set
/** Create a closure around a Prototype with a specific environment.
* If the prototype has upvalues, the environment will be written into the first upvalue.
@@ -108,14 +125,27 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
this.p = p
this.initupvalue1(env)
globals = env as? Globals
+ Memory.account(Memory.CLOSURE + Memory.UPVALUE * upValues.size)
+ }
+
+ /** As the two-argument form, for a chunk whose environment is its own. */
+ constructor(p: Prototype, env: LuaValue?, state: Globals?) : this(p, env) {
+ if (globals == null) globals = state
}
override fun initupvalue1(env: LuaValue?) {
- if (p.upvalues == null || p.upvalues!!.size === 0) this.upValues =
- net.blueva.luak.LuaClosure.Companion.NOUPVALUES
- else {
- this.upValues = arrayOfNulls(p.upvalues!!.size)
- this.upValues[0] = UpValue(arrayOf(env), 0)
+ val descs: Array? = p.upvalues
+ if (descs == null || descs.isEmpty()) {
+ this.upValues = net.blueva.luak.LuaClosure.Companion.NOUPVALUES
+ return
+ }
+ // Every slot gets storage of its own, not only the first: a closure
+ // loaded from a dump has upvalues nobody has set yet, and reading one
+ // before then has to answer nil rather than fall over.
+ this.upValues = arrayOfNulls(descs.size)
+ this.upValues[0] = UpValue(arrayOf(env), 0)
+ for (index in 1..(NIL), 0)
}
}
@@ -209,14 +239,71 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
return execute(stack, (if (p.is_vararg !== 0) varargs.subargs(p.numparams + 1) else NONE)!!)
}
- override fun call(): LuaValue = runLuaSync { call0() }
- override fun call(arg: LuaValue?): LuaValue = runLuaSync { call1(arg) }
- override fun call(arg1: LuaValue?, arg2: LuaValue?): LuaValue = runLuaSync { call2(arg1, arg2) }
+ /**
+ * Runs [block], with yielding shut off while it does.
+ *
+ * These are the entry points a library function reaches Lua code through,
+ * and there is nowhere for a yield inside one to suspend to: it is the
+ * C-call boundary, and the coroutine counts as non-yieldable while the
+ * call is in progress.
+ */
+ private fun runAcrossBoundary(block: suspend () -> T): T {
+ val state: LuaThread.State = globals?.running?.state ?: return runLuaSync(block)
+ // A call in from outside is a protected boundary of its own, so the
+ // tally goes back to what it was however this ends.
+ val outer: Int = state.foreigncalls
+ state.noyield++
+ try {
+ enterforeign(state)
+ val answer: T = runLuaSync(block)
+ // Put back only on the way out with an answer: an error leaves the
+ // tally where it was, so that whatever handles it is working in
+ // the room above the ceiling rather than starting again from
+ // below it. A protected call is what puts it back then.
+ state.foreigncalls = outer
+ return answer
+ } finally {
+ state.noyield--
+ }
+ }
+
+ /**
+ * Counts one call that leaves Lua, refusing to go deeper than Lua does.
+ *
+ * See [LuaThread.State.foreigncalls]: this is the step that costs host
+ * stack, so it is the one with a ceiling on it.
+ */
+ private fun enterforeign(state: LuaThread.State) {
+ // Counted first and left counted if it fails: the tally stays where it
+ // was until a protected call puts it back, so an error raised at the
+ // ceiling does not make room for the next one on its way out.
+ enterForeignCall(state)
+ }
+
+ /**
+ * Calls a metamethod, counting the re-entry into the interpreter.
+ *
+ * A call the interpreter makes for an instruction of its own loops inside
+ * the loop; one made from here recurses on the host stack, which is what
+ * Lua counts and puts a ceiling on.
+ */
+ private suspend fun callmeta(h: LuaValue, a: LuaValue, b: LuaValue): LuaValue {
+ val state: LuaThread.State = globals?.running?.state ?: return h.callSuspend(a, b)!!
+ enterforeign(state)
+ val result: LuaValue = h.callSuspend(a, b)!!
+ // Only on the way out that worked: see enterforeign.
+ state.foreigncalls--
+ return result
+ }
+
+ override fun call(): LuaValue = runAcrossBoundary { call0() }
+ override fun call(arg: LuaValue?): LuaValue = runAcrossBoundary { call1(arg) }
+ override fun call(arg1: LuaValue?, arg2: LuaValue?): LuaValue = runAcrossBoundary { call2(arg1, arg2) }
override fun call(arg1: LuaValue?, arg2: LuaValue?, arg3: LuaValue?): LuaValue =
- runLuaSync { call3(arg1, arg2, arg3) }
+ runAcrossBoundary { call3(arg1, arg2, arg3) }
- override fun invoke(varargs: Varargs): Varargs = runLuaSync { onInvokeImpl(varargs)!!.evalSuspend() }
- override fun onInvoke(varargs: Varargs): Varargs? = runLuaSync { onInvokeImpl(varargs) }
+ override fun invoke(varargs: Varargs): Varargs = runAcrossBoundary { onInvokeImpl(varargs)!!.evalSuspend() }
+ override fun onInvoke(varargs: Varargs): Varargs? = runAcrossBoundary { onInvokeImpl(varargs) }
override suspend fun callSuspend(): LuaValue? = call0()
override suspend fun callSuspend(arg: LuaValue?): LuaValue? = call1(arg)
@@ -236,25 +323,82 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
* wholesale. Returns false for the multiple-result and vararg shapes, which
* [execute] keeps because they also update its `v` and `top`.
*/
- private suspend fun callFixedArity(stack: Array, i: Int, a: Int): Boolean {
- when (i and (Lua.MASK_B or Lua.MASK_C)) {
- (1 shl Lua.POS_B) or (1 shl Lua.POS_C) -> stack[a].callSuspend()
- (2 shl Lua.POS_B) or (1 shl Lua.POS_C) -> stack[a].callSuspend(stack[a + 1])
- (3 shl Lua.POS_B) or (1 shl Lua.POS_C) -> stack[a].callSuspend(stack[a + 1], stack[a + 2])
+ private suspend fun callFixedArity(
+ stack: Array,
+ i: Int,
+ a: Int,
+ debuglib: DebugLib?,
+ ): Boolean {
+ // Everything above the arguments is free at a call, and what a
+ // finished statement left in those registers would otherwise go on
+ // holding an object nothing else refers to. Lua's collector reaches
+ // the same conclusion by only looking at a stack up to the top of the
+ // call in progress; here the registers are emptied instead.
+ val b: Int = (i ushr 23) and 0x1ff
+ // Nothing written for the count means the arguments run to the top of
+ // what the frame is using, which is not known here.
+ if (b > 0) {
+ var free: Int = a + b
+ while (free < stack.size) {
+ stack[free] = LuaValue.NIL
+ free++
+ }
+ }
+ // A call is a place a collection can happen, which is where anything
+ // waiting to be finalized gets its turn: a loop that allocates
+ // without ever building a table would otherwise never let one run.
+ val g: Globals? = globals
+ if (g != null && g.marksfinalizers) g.runfinalizers()
+ // A library function has no frame of its own to push, so the caller
+ // pushes one for it: without that a traceback would not name it and
+ // the call and return hooks would never fire for it.
+ // Only a function of the library's own gets a frame pushed for it
+ // here: a Lua closure pushes its own, and anything reached through
+ // __call is not what ends up running.
+ val callee: LuaValue = stack[a]
+ val traced: Boolean = debuglib != null && callee is LuaFunction && callee !is LuaClosure
+ if (traced) debuglib!!.onCall(callee as LuaFunction, fixedArityArgs(stack, i, a))
+ try {
+ val produced: LuaValue? = callFixedArityValues(stack, i, a)
+ if (traced && produced != null) debuglib!!.onResults(produced)
+ return produced != null
+ } catch (le: LuaError) {
+ // Still standing on the frame that raised, which is the last
+ // chance a coroutine has to write its stack down.
+ if (traced) debuglib!!.notestack(le)
+ throw le
+ } finally {
+ if (traced) debuglib!!.onReturn()
+ }
+ }
+
+ /** The call shapes themselves, without the bookkeeping around them. */
+ /**
+ * @return what the call produced, or null for a shape this does not handle
+ */
+ private suspend fun callFixedArityValues(stack: Array, i: Int, a: Int): LuaValue? {
+ val produced: LuaValue = when (i and (Lua.MASK_B or Lua.MASK_C)) {
+ (1 shl Lua.POS_B) or (1 shl Lua.POS_C) -> stack[a].callSuspend() ?: NIL
+ (2 shl Lua.POS_B) or (1 shl Lua.POS_C) -> stack[a].callSuspend(stack[a + 1]) ?: NIL
+ (3 shl Lua.POS_B) or (1 shl Lua.POS_C) ->
+ stack[a].callSuspend(stack[a + 1], stack[a + 2]) ?: NIL
+
(4 shl Lua.POS_B) or (1 shl Lua.POS_C) ->
- stack[a].callSuspend(stack[a + 1], stack[a + 2], stack[a + 3])
+ stack[a].callSuspend(stack[a + 1], stack[a + 2], stack[a + 3]) ?: NIL
+
+ (1 shl Lua.POS_B) or (2 shl Lua.POS_C) -> stack[a].callSuspend()!!.also { stack[a] = it }
+ (2 shl Lua.POS_B) or (2 shl Lua.POS_C) ->
+ stack[a].callSuspend(stack[a + 1])!!.also { stack[a] = it }
- (1 shl Lua.POS_B) or (2 shl Lua.POS_C) -> stack[a] = stack[a].callSuspend()!!
- (2 shl Lua.POS_B) or (2 shl Lua.POS_C) -> stack[a] = stack[a].callSuspend(stack[a + 1])!!
(3 shl Lua.POS_B) or (2 shl Lua.POS_C) ->
- stack[a] = stack[a].callSuspend(stack[a + 1], stack[a + 2])!!
+ stack[a].callSuspend(stack[a + 1], stack[a + 2])!!.also { stack[a] = it }
(4 shl Lua.POS_B) or (2 shl Lua.POS_C) ->
- stack[a] = stack[a].callSuspend(stack[a + 1], stack[a + 2], stack[a + 3])!!
+ stack[a].callSuspend(stack[a + 1], stack[a + 2], stack[a + 3])!!.also { stack[a] = it }
- else -> return false
+ else -> return null
}
- return true
+ return produced
}
/**
@@ -289,14 +433,30 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
// TODO: use linked list.
val openups: Array? = if (p.p!!.size > 0) arrayOfNulls(stack.size) else null
+ // Stack slots holding to-be-closed variables, outermost first. Stays
+ // null for the overwhelming majority of functions, which declare none.
+ var tbc: ArrayList? = null
// Resolved once per frame rather than per instruction: the per-opcode
// "globals != null && globals.debuglib != null" reload was two field
// loads and two branches on the hottest path in the interpreter.
val debuglib: DebugLib? = globals?.debuglib
+ // With the debug library watching a vararg function, the arguments get
+ // storage of their own so debug.setlocal can write through to what
+ // '...' reads. Nothing else pays for it.
+ val tracked: Array? =
+ if (debuglib != null && p.is_vararg != 0) copyArgs(varargs) else null
+ // ArrayVarargs directly rather than varargsOf, which for one or two
+ // values hands back something that no longer shares the array.
+ val args: Varargs = if (tracked != null) Varargs.ArrayVarargs(tracked, NONE!!) else varargs
+
+ // A named vararg parameter is a table over the extra arguments, built
+ // once here so that it and '...' read the same storage.
+ if (p.is_vararg and Lua.VARARG_NAMED != 0) buildVarargTable(args, p, stack)
+
// allow for debug hooks
- if (debuglib != null) debuglib.onCall(this, varargs, stack as Array)
+ if (debuglib != null) debuglib.onCall(this, args, stack as Array, tracked)
// process instructions
try {
@@ -311,14 +471,12 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
// process the op code
when (i and 0x3f) {
- Lua.OP_MOVE -> {
- stack[a] = stack[i ushr 23]
- ++pc
- continue
- }
-
- Lua.OP_LOADK -> {
- stack[a] = k[i ushr 14]!!
+ Lua.OP_MOVE, Lua.OP_LOADK, Lua.OP_LOADNIL, Lua.OP_GETUPVAL,
+ Lua.OP_SETUPVAL, Lua.OP_NEWTABLE,
+ -> {
+ // Lifted out of this method to keep it under the JVM's
+ // 8000-bytecode JIT limit - see execute()'s doc.
+ loadOpcode(stack, i, a, k)
++pc
continue
}
@@ -345,162 +503,30 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
continue
}
- Lua.OP_LOADNIL -> {
- b = i ushr 23
- while (b-- >= 0) {
- stack[a++] = LuaValue.NIL
- }
+ Lua.OP_GETTABUP, Lua.OP_GETTABLE, Lua.OP_SETTABUP,
+ Lua.OP_SETTABLE, Lua.OP_SELF,
+ -> {
+ // Also lifted out because an __index or __newindex
+ // called from here may yield.
+ tableOpcode(stack, i, a, k)
++pc
continue
}
- Lua.OP_GETUPVAL -> {
- stack[a] = upValues[i ushr 23]!!.getValue()!!
+ Lua.OP_ADD, Lua.OP_SUB, Lua.OP_MUL, Lua.OP_DIV, Lua.OP_IDIV,
+ Lua.OP_BAND, Lua.OP_BOR, Lua.OP_BXOR, Lua.OP_SHL, Lua.OP_SHR,
+ Lua.OP_MOD, Lua.OP_POW, Lua.OP_CONCAT,
+ -> {
+ // Lifted out of this method to keep it under the JVM's
+ // 8000-bytecode JIT limit, and because a metamethod
+ // called from there may yield - see execute()'s doc.
+ binaryOpcode(stack, i, a, k)
++pc
continue
}
- Lua.OP_GETTABUP -> {
- c = (i shr 14) and 0x1ff
- stack[a] = upValues[i ushr 23]!!.getValue()!!
- .get(if (c > 0xff) k[c and 0x0ff]!! else stack[c])
- ++pc
- continue
- }
-
- Lua.OP_GETTABLE -> {
- c = (i shr 14) and 0x1ff
- stack[a] = stack[i ushr 23].get(if (c > 0xff) k[c and 0x0ff]!! else stack[c])
- ++pc
- continue
- }
-
- Lua.OP_SETTABUP -> {
- b = i ushr 23
- c = (i shr 14) and 0x1ff
- upValues[a]!!.getValue()!!
- .set(
- if (b > 0xff) k[b and 0x0ff] else stack[b],
- if (c > 0xff) k[c and 0x0ff] else stack[c]
- )
- ++pc
- continue
- }
-
- Lua.OP_SETUPVAL -> {
- upValues[i ushr 23]!!.setValue(stack[a])
- ++pc
- continue
- }
-
- Lua.OP_SETTABLE -> {
- b = i ushr 23
- c = (i shr 14) and 0x1ff
- stack[a].set(
- if (b > 0xff) k[b and 0x0ff] else stack[b],
- if (c > 0xff) k[c and 0x0ff] else stack[c]
- )
- ++pc
- continue
- }
-
- Lua.OP_NEWTABLE -> {
- stack[a] = LuaTable(i ushr 23, (i shr 14) and 0x1ff)
- ++pc
- continue
- }
-
- Lua.OP_SELF -> {
- o = stack[i ushr 23]
- stack[a + 1] = o
- c = (i shr 14) and 0x1ff
- stack[a] = o.get(if (c > 0xff) k[c and 0x0ff]!! else stack[c])
- ++pc
- continue
- }
-
- Lua.OP_ADD -> {
- b = i ushr 23
- c = (i shr 14) and 0x1ff
- stack[a] = (if (b > 0xff) k[b and 0x0ff]!! else stack[b])
- .add(if (c > 0xff) k[c and 0x0ff]!! else stack[c])
- ++pc
- continue
- }
-
- Lua.OP_SUB -> {
- b = i ushr 23
- c = (i shr 14) and 0x1ff
- stack[a] = (if (b > 0xff) k[b and 0x0ff]!! else stack[b])
- .sub(if (c > 0xff) k[c and 0x0ff]!! else stack[c])
- ++pc
- continue
- }
-
- Lua.OP_MUL -> {
- b = i ushr 23
- c = (i shr 14) and 0x1ff
- stack[a] = (if (b > 0xff) k[b and 0x0ff]!! else stack[b])
- .mul(if (c > 0xff) k[c and 0x0ff]!! else stack[c])
- ++pc
- continue
- }
-
- Lua.OP_DIV -> {
- b = i ushr 23
- c = (i shr 14) and 0x1ff
- stack[a] = (if (b > 0xff) k[b and 0x0ff]!! else stack[b])
- .div(if (c > 0xff) k[c and 0x0ff]!! else stack[c])
- ++pc
- continue
- }
-
- Lua.OP_MOD -> {
- b = i ushr 23
- c = (i shr 14) and 0x1ff
- stack[a] = (if (b > 0xff) k[b and 0x0ff]!! else stack[b])
- .mod(if (c > 0xff) k[c and 0x0ff]!! else stack[c])
- ++pc
- continue
- }
-
- Lua.OP_POW -> {
- b = i ushr 23
- c = (i shr 14) and 0x1ff
- stack[a] = (if (b > 0xff) k[b and 0x0ff]!! else stack[b])
- .pow(if (c > 0xff) k[c and 0x0ff]!! else stack[c])
- ++pc
- continue
- }
-
- Lua.OP_UNM -> {
- stack[a] = stack[i ushr 23].neg()
- ++pc
- continue
- }
-
- Lua.OP_NOT -> {
- stack[a] = stack[i ushr 23].not()!!
- ++pc
- continue
- }
-
- Lua.OP_LEN -> {
- stack[a] = stack[i ushr 23].len()
- ++pc
- continue
- }
-
- Lua.OP_CONCAT -> {
- b = i ushr 23
- c = (i shr 14) and 0x1ff
- if (c > b + 1) {
- val sb: Buffer = stack[c].buffer()!!
- while (--c >= b) sb.concatTo(stack[c])
- stack[a] = sb.value()!!
- } else {
- stack[a] = stack[c - 1].concat(stack[c])
- }
+ Lua.OP_UNM, Lua.OP_NOT, Lua.OP_LEN, Lua.OP_BNOT -> {
+ unaryOpcode(stack, i, a)
++pc
continue
}
@@ -509,7 +535,12 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
pc += (i ushr 14) - 0x1ffff
if (a > 0) {
--a
- b = openups!!.size
+ if (tbc != null) closeToBeClosed(tbc, stack, a, null)?.let { throw it }
+ if (openups == null) {
+ ++pc
+ continue
+ }
+ b = openups.size
while (--b >= 0) {
if (openups[b] != null && openups[b]!!.index >= a) {
openups[b]!!.close()
@@ -521,29 +552,10 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
continue
}
- Lua.OP_EQ -> {
- b = i ushr 23
- c = (i shr 14) and 0x1ff
- if ((if (b > 0xff) k[b and 0x0ff]!! else stack[b])
- .eq_b(if (c > 0xff) k[c and 0x0ff] else stack[c])!! !== (a != 0)) ++pc
- ++pc
- continue
- }
-
- Lua.OP_LT -> {
- b = i ushr 23
- c = (i shr 14) and 0x1ff
- if ((if (b > 0xff) k[b and 0x0ff]!! else stack[b])
- .lt_b(if (c > 0xff) k[c and 0x0ff]!! else stack[c]) !== (a != 0)) ++pc
- ++pc
- continue
- }
-
- Lua.OP_LE -> {
- b = i ushr 23
- c = (i shr 14) and 0x1ff
- if ((if (b > 0xff) k[b and 0x0ff]!! else stack[b])
- .lteq_b(if (c > 0xff) k[c and 0x0ff]!! else stack[c]) !== (a != 0)) ++pc
+ Lua.OP_EQ, Lua.OP_LT, Lua.OP_LE -> {
+ // Lifted out because a comparison metamethod called
+ // from here may yield - see execute()'s doc.
+ if (compareOpcode(stack, i, k) != (a != 0)) ++pc
++pc
continue
}
@@ -564,16 +576,20 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
continue
}
- Lua.OP_CALL -> when (i and (Lua.MASK_B or Lua.MASK_C)) {
+ Lua.OP_CALL -> {
+ // How many __call handlers stand between here and what
+ // actually runs, so its frame can report them.
+ if (debuglib != null) debuglib.notecallchain(stack[a])
+ when (i and (Lua.MASK_B or Lua.MASK_C)) {
(1 shl Lua.POS_B) or (0 shl Lua.POS_C) -> {
- v = stack[a].invokeSuspend((NONE)!!)
+ v = invokeTraced(stack[a], (NONE)!!, debuglib)
top = a + v.narg()
++pc
continue
}
(2 shl Lua.POS_B) or (0 shl Lua.POS_C) -> {
- v = stack[a].invokeSuspend(stack[a + 1])
+ v = invokeTraced(stack[a], stack[a + 1], debuglib)
top = a + v.narg()
++pc
continue
@@ -583,15 +599,17 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
// The fixed-arity shapes touch nothing but stack[a], so
// they live in callFixedArity() to keep this method under
// the JVM's 8000-bytecode JIT limit - see execute()'s doc.
- if (callFixedArity(stack, i, a)) {
+ if (callFixedArity(stack, i, a, debuglib)) {
++pc
continue
}
b = i ushr 23
c = (i shr 14) and 0x1ff
- v = stack[a].invokeSuspend(
+ v = invokeTraced(
+ stack[a],
if (b > 0) varargsOf(stack, a + 1, b - 1) else // exact arg count
- varargsOf(stack, a + 1, top - v.narg() - (a + 1), v)
+ varargsOf(stack, a + 1, top - v.narg() - (a + 1), v),
+ debuglib,
) // from prev top
if (c > 0) {
v.copyto(stack as Array, a, c - 1)
@@ -604,55 +622,73 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
continue
}
}
+ }
- Lua.OP_TAILCALL -> when (i and Lua.MASK_B) {
- (1 shl Lua.POS_B) -> return TailcallVarargs(stack[a], NONE)
- (2 shl Lua.POS_B) -> return TailcallVarargs(stack[a], stack[a + 1])
- (3 shl Lua.POS_B) -> return TailcallVarargs(stack[a], varargsOf(stack[a + 1], stack[a + 2]))
- (4 shl Lua.POS_B) -> return TailcallVarargs(
- stack[a],
- varargsOf(stack[a + 1], stack[a + 2], stack[a + 3])
- )
-
- else -> {
- b = i ushr 23
- v = if (b > 0) varargsOf(stack, a + 1, b - 1) else // exact arg count
- varargsOf(stack, a + 1, top - v.narg() - (a + 1), v) // from prev top
- return TailcallVarargs(stack[a], v)
+ Lua.OP_TAILCALL -> {
+ val args: Varargs = when (i and Lua.MASK_B) {
+ (1 shl Lua.POS_B) -> NONE!!
+ (2 shl Lua.POS_B) -> stack[a + 1]
+ (3 shl Lua.POS_B) -> varargsOf(stack[a + 1], stack[a + 2])
+ (4 shl Lua.POS_B) -> varargsOf(stack[a + 1], stack[a + 2], stack[a + 3])
+ else -> {
+ b = i ushr 23
+ if (b > 0) varargsOf(stack, a + 1, b - 1) // exact arg count
+ else varargsOf(stack, a + 1, top - v.narg() - (a + 1), v)
+ }
}
+ // A tail call leaves this frame before it is made, so
+ // anything that cannot be called has to be reported
+ // here while the instruction is still known, and a
+ // chain of __call handlers is followed here rather
+ // than by nesting one call inside the next.
+ if (debuglib != null) {
+ debuglib.notecallchain(stack[a])
+ // The frame this makes takes this one's place.
+ debuglib.ontailcall()
+ }
+ val prefix: ArrayList = ArrayList()
+ val target: LuaValue = resolveTailcall(stack, a, prefix)
+ // See LuaValue.invoke: outermost first, so the
+ // innermost handler's own value leads the arguments.
+ var callArgs: Varargs = args
+ for (self in prefix) callArgs = varargsOf(self, callArgs)
+ // A library function is called from here rather than
+ // handed back as a tail call: it has no frame of its
+ // own to reuse, and calling it here is what gives it
+ // one for a traceback to name.
+ if (target !is LuaClosure) return invokeTraced(target, callArgs, debuglib)
+ return TailcallVarargs(target, callArgs)
}
Lua.OP_RETURN -> {
b = i ushr 23
- when (b) {
- 0 -> return varargsOf(stack, a, top - v.narg() - a, v)
- 1 -> return NONE
- 2 -> return stack[a]
- else -> return varargsOf(stack, a, b - 1)
+ // Before the results are read off the stack, as upstream
+ // closes at the return rather than after it.
+ if (tbc != null) closeToBeClosed(tbc, stack, 0, null)?.let { throw it }
+ val results: Varargs = when (b) {
+ 0 -> varargsOf(stack, a, top - v.narg() - a, v)
+ 1 -> NONE!!
+ 2 -> stack[a]
+ else -> varargsOf(stack, a, b - 1)
}
+ // What it hands back, so a return hook can read them.
+ if (debuglib != null) debuglib.onResults(results)
+ return results
}
Lua.OP_FORLOOP -> {
- val limit: LuaValue? = stack[a + 1]
- val step: LuaValue = stack[a + 2]
- val idx: LuaValue = stack[a].add(step)
- if (if (step.gt_b(0)) idx.lteq_b((limit)!!) else idx.gteq_b((limit)!!)) {
- stack[a] = idx
- stack[a + 3] = idx
- pc += (i ushr 14) - 0x1ffff
- }
+ // Lifted out to keep this method under the JVM's
+ // 8000-bytecode JIT limit - see execute()'s doc.
+ if (forLoop(stack, a)) pc += (i ushr 14) - 0x1ffff
++pc
continue
}
Lua.OP_FORPREP -> {
- val init: LuaValue = stack[a].checknumber("'for' initial value must be a number")!!
- val limit: LuaValue? = stack[a + 1].checknumber("'for' limit must be a number")
- val step: LuaValue? = stack[a + 2].checknumber("'for' step must be a number")
- stack[a] = init.sub((step)!!)
- stack[a + 1] = limit!!
- stack[a + 2] = step
- pc += (i ushr 14) - 0x1ffff
+ // The jump goes past the loop's own closing
+ // instruction; a loop that does run falls through into
+ // its body with the control variable already set.
+ if (forPrep(stack, a)) pc += (i ushr 14) - 0x1ffff + 1
++pc
continue
}
@@ -660,15 +696,19 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
Lua.OP_TFORCALL -> {
v = stack[a].invokeSuspend((varargsOf(stack[a + 1], stack[a + 2]))!!)
c = (i shr 14) and 0x1ff
- while (--c >= 0) stack[a + 3 + c] = v.arg(c + 1)
+ // Four control values now, so the results start one
+ // slot further along than they did in 5.2.
+ while (--c >= 0) stack[a + 4 + c] = v.arg(c + 1)
v = NONE
++pc
continue
}
Lua.OP_TFORLOOP -> {
- if (!stack[a + 1].isnil()) { /* continue loop? */
- stack[a] = stack[a + 1] /* save control varible. */
+ // R(A) is the control value and R(A+2) the first result,
+ // with the closing value in between.
+ if (!stack[a + 2].isnil()) { /* continue loop? */
+ stack[a] = stack[a + 2] /* save control variable */
pc += (i ushr 14) - 0x1ffff
}
++pc
@@ -693,6 +733,11 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
o.set(offset + j, v.arg(j - m))
j++
}
+ // Let go of what the call handed over: the values
+ // are in the table now, and holding them here
+ // would keep alive what the program has finished
+ // with. See the end of this instruction.
+ v = NONE!!
} else {
o.presize(offset + b)
var j = 1
@@ -701,38 +746,31 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
j++
}
}
+ // Let go of the table: this is the last instruction of
+ // a constructor, and holding it here would keep alive
+ // something the program has already finished with.
+ o = NIL
++pc
continue
}
Lua.OP_CLOSURE -> {
- val newp: Prototype = p.p!![i ushr 14]!!
- val ncl: LuaClosure = net.blueva.luak.LuaClosure(newp, globals)
- val uv: Array = newp.upvalues!!
- var j = 0
- val nup = uv.size
- while (j < nup) {
- if (uv[j]!!.instack) /* upvalue refes to local variable? */
- ncl.upValues[j] = findupval(stack, uv[j]!!.idx, openups!!)
- else /* get upvalue from enclosing function */
- ncl.upValues[j] = upValues[(uv[j]!!.idx).toInt()]
- ++j
- }
- stack[a] = ncl
+ stack[a] = makeclosure(stack, i, openups)
++pc
continue
}
Lua.OP_VARARG -> {
+ val source: Varargs = varargSource(args, p, stack)
b = i ushr 23
if (b == 0) {
- b = varargs.narg()
+ b = source.narg()
top = a + b
- v = varargs
+ v = source
} else {
var j = 1
while (j < b) {
- stack[a + j - 1] = varargs.arg(j)
+ stack[a + j - 1] = source.arg(j)
++j
}
}
@@ -740,6 +778,18 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
continue
}
+ Lua.OP_TBC -> {
+ tbc = markToBeClosed(tbc, stack[a], a, p, pc)
+ ++pc
+ continue
+ }
+
+ Lua.OP_ERRNNIL -> {
+ if (!stack[a].isnil()) errorAlreadyDefined(k, i ushr 14)
+ ++pc
+ continue
+ }
+
Lua.OP_EXTRAARG -> throw IllegalArgumentException("Uexecutable opcode: OP_EXTRAARG")
else -> throw IllegalArgumentException("Illegal opcode: " + (i and 0x3f))
@@ -747,16 +797,39 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
++pc
}
} catch (le: LuaError) {
- if (le.traceback == null) {
- enrichArgError(le, p, pc)
- processErrorHooks(le, p, pc)
+ // Unwinding past a to-be-closed variable still closes it, and the
+ // handler is told which error it is unwinding from.
+ // A closer that raises replaces the error being unwound, so what
+ // leaves here is not always what arrived.
+ // Suspending here too: a handler may yield while the error is on
+ // its way out, which is what a coroutine's own pcall allows.
+ if (debuglib != null && tbc != null) debuglib.hidetopframe()
+ val outgoing: LuaError = try {
+ if (tbc == null) le else closeToBeClosed(tbc, stack, 0, le) ?: le
+ } finally {
+ if (debuglib != null && tbc != null) debuglib.showtopframe()
}
- throw le
+ if (debuglib != null) debuglib.notestack(outgoing)
+ if (outgoing.traceback == null) {
+ enrichArgError(outgoing, p, pc, stack)
+ enrichOperandError(outgoing, p, pc, stack)
+ enrichCallError(outgoing, p, pc)
+ enrichIndexError(outgoing, p, pc)
+ processErrorHooks(outgoing, p, pc)
+ }
+ throw outgoing
} catch (e: Exception) {
val le: LuaError = LuaError(e)
processErrorHooks(le, p, pc)
throw le
+ } catch (t: Throwable) {
+ // The host running out of stack, and nothing else: a coroutine
+ // being closed travels as an Error too and has to pass through.
+ val le: LuaError = overflow(t) ?: throw t
+ processErrorHooks(le, p, pc)
+ throw le
} finally {
+ if (tbc != null) runLuaSync { closeToBeClosed(tbc, stack, 0, null) }?.let { throw it }
if (openups != null) {
var u = openups.size
while (--u >= 0) {
@@ -767,24 +840,234 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
}
}
+ /**
+ * Turns a host stack overflow into the Lua error it stands for.
+ *
+ * Reported only once the unwinding is a little way back from the edge:
+ * where it was noticed there is no room left to build a message in, let
+ * alone run a handler that walks the stack. The frames given up to make
+ * that room are gone from the traceback, which is a report of a stack too
+ * deep to print whole in any case.
+ *
+ * @return the error to raise, or null to let [t] carry on unwinding
+ */
+ private fun overflow(t: Throwable): LuaError? {
+ if (!platformIsStackOverflow(t)) return null
+ val state: LuaThread.State? = globals?.running?.state
+ if (state != null) {
+ if (++state.unwinding < STACK_UNWIND_HEADROOM) return null
+ state.unwinding = 0
+ // Running out of stack in the room kept for handling one is a
+ // failure of the handling; see LuaThread.State.inhandler. It says
+ // no more than that, with no place: the handling is what failed,
+ // not anything the program wrote.
+ if (state.inhandler > 0) {
+ val failed = LuaError("error in error handling")
+ failed.nowhere = true
+ return failed
+ }
+ }
+ return LuaError("stack overflow")
+ }
+
/**
* Run the error hook if there is one
* @param msg the message to use in error hook processing.
*/
- fun errorHook(msg: String?, level: Int): String? {
- if (globals == null) return msg
+ /**
+ * Runs the message handler an `xpcall` installed, if there is one.
+ *
+ * The handler is shown the error object as it stands - a table stays a
+ * table - and whatever it answers becomes the error from here on. Without
+ * a handler nothing happens: Lua only builds a traceback when one asks for
+ * it, as `xpcall(f, debug.traceback)` does, and putting one into the
+ * message a plain `pcall` hands back is not what the caller asked for.
+ */
+ fun errorHook(le: LuaError) {
+ val globals: Globals = this.globals ?: return
val r: LuaThread = globals.running
- if (r.errorfunc == null) return if (globals.debuglib != null) msg.toString() + "\n" + globals.debuglib!!.traceback(
- level
- ) else msg
+ if (r.errorfunc == null) return
val e: LuaValue = r.errorfunc!!
- r.errorfunc = null
- try {
- return e.call(LuaValue.valueOf(msg))!!.tojstring()
+ // Running the handler is itself a call out of Lua, and it is made in
+ // the room Lua keeps above the ordinary ceiling for exactly this. Past
+ // that room there is nothing left to report but the failure of the
+ // handling, and the handler is not called again.
+ if (r.state.foreigncalls >= LuaThread.State.MAX_HANDLER_CALLS) {
+ le.replaceMessage(LuaValue.valueOf("error in error handling")!!)
+ le.traceback = "error in error handling"
+ return
+ }
+ // A handler written in Lua pushes its own frame; one from the library,
+ // debug.traceback most of all, needs one pushed for it so the levels it
+ // counts line up with what a Lua handler would see.
+ val debuglib: net.blueva.luak.lib.DebugLib? =
+ if (e !is LuaClosure) globals.debuglib else null
+ if (debuglib != null) debuglib.onCall(e as? LuaFunction)
+ // The call itself is counted where it re-enters the interpreter, so
+ // nothing is added here.
+ // A handler written in Lua is counted where it enters the
+ // interpreter; one of the library's own never does, and is counted
+ // here so that a chain of them cannot go round for ever.
+ val outer: Int = r.state.foreigncalls
+ if (e !is LuaClosure) r.state.foreigncalls++
+ r.state.inhandler++
+ val handled: LuaValue = try {
+ e.call(le.messageObject ?: NIL)!!
+ } catch (nested: LuaError) {
+ // The handler raised in its turn. Lua hands that to the same
+ // handler, again and again, until the room kept for handling runs
+ // out and the failure of the handling is what is left to report.
+ // An error raised by Lua code has already been through the handler
+ // on its way out of that code; one raised by the runtime itself,
+ // which is what running out of room looks like, has not.
+ if (nested.traceback == null) errorHook(nested)
+ nested.messageObject ?: NIL
} catch (t: Throwable) {
- return "error in error handling"
+ LuaValue.valueOf("error in error handling")!!
} finally {
- r.errorfunc = e
+ r.state.inhandler--
+ r.state.foreigncalls = outer
+ if (debuglib != null) debuglib.onReturn()
+ }
+ le.replaceMessage(handled)
+ // Doubles as the mark that the handler has already run.
+ le.traceback = handled.tojstring()
+ }
+
+ /**
+ * Says where the thing that could not be indexed came from.
+ *
+ * "attempt to index a nil value" becomes "... (field 'x')" once the
+ * instruction is read back to see which register or upvalue held it.
+ */
+ private fun enrichIndexError(le: LuaError, p: Prototype, pc: Int) {
+ if (le.argMessageOverride != null) return
+ val m: String = le.message ?: return
+ if (!Regex("^attempt to index a .+ value$").matches(m)) return
+ val code: IntArray = p.code ?: return
+ if (pc < 0 || pc >= code.size) return
+ val instr: Int = code[pc]
+ val kind: String
+ val name: String
+ when (Lua.GET_OPCODE(instr)) {
+ Lua.OP_GETTABLE, Lua.OP_SELF -> {
+ val found = net.blueva.luak.lib.DebugLib.getobjname(p, pc, Lua.GETARG_B(instr)) ?: return
+ kind = found.namewhat
+ name = found.name
+ }
+
+ Lua.OP_SETTABLE -> {
+ val found = net.blueva.luak.lib.DebugLib.getobjname(p, pc, Lua.GETARG_A(instr)) ?: return
+ kind = found.namewhat
+ name = found.name
+ }
+
+ Lua.OP_GETTABUP -> {
+ val up = p.upvalues?.getOrNull(Lua.GETARG_B(instr)) ?: return
+ kind = "upvalue"
+ name = up.name?.tojstring() ?: return
+ }
+
+ Lua.OP_SETTABUP -> {
+ val up = p.upvalues?.getOrNull(Lua.GETARG_A(instr)) ?: return
+ kind = "upvalue"
+ name = up.name?.tojstring() ?: return
+ }
+
+ else -> return
+ }
+ le.argMessageOverride = m + " (" + kind + " '" + name + "')"
+ }
+
+ /**
+ * Says how the program named the thing it tried to call.
+ *
+ * "attempt to call a nil value" becomes "... (field 'bbbb')" once the call
+ * instruction is read back, which is usually the whole of what the reader
+ * needs to spot a misspelling.
+ */
+ private fun enrichCallError(le: LuaError, p: Prototype, pc: Int) {
+ if (le.argMessageOverride != null) return
+ val m: String = le.message ?: return
+ if (!Regex("^attempt to call a .+ value$").matches(m)) return
+ val code: IntArray = p.code ?: return
+ if (pc < 0 || pc >= code.size) return
+ val instr: Int = code[pc]
+ val opcode: Int = Lua.GET_OPCODE(instr)
+ val found = when (opcode) {
+ Lua.OP_CALL, Lua.OP_TAILCALL ->
+ net.blueva.luak.lib.DebugLib.getobjname(p, pc, Lua.GETARG_A(instr))
+ // The one other instruction that calls: a generic `for` steps its
+ // iterator, and Lua names that as what it is.
+ Lua.OP_TFORCALL -> net.blueva.luak.lib.DebugLib.NameWhat("for iterator", "for iterator")
+ else -> return
+ } ?: return
+ le.argMessageOverride = m + " (" + found.namewhat + " '" + found.name + "')"
+ }
+
+ /**
+ * Says where a rejected operand came from, as Lua's `varinfo` does.
+ *
+ * "attempt to perform arithmetic on a nil value" becomes "... (field 'x')"
+ * once the instruction is read back to see which operand had that type and
+ * how the program named it. Without this the message says what went wrong
+ * but not which of the two values was at fault.
+ */
+ private fun enrichOperandError(le: LuaError, p: Prototype, pc: Int, stack: Array) {
+ if (le.argMessageOverride != null) return
+ val m: String = le.message ?: return
+ // Two messages carry a varinfo: one names the type it could not work
+ // on, the other says a number was not a whole one.
+ val wanted: String
+ val insertAt: Int
+ val operand = Regex("^attempt to perform (?:arithmetic|bitwise operation) on a (.+) value$")
+ .find(m)
+ if (operand != null) {
+ wanted = operand.groupValues[1]
+ insertAt = m.length
+ } else if (m == "number has no integer representation") {
+ wanted = "number"
+ insertAt = "number".length
+ } else {
+ return
+ }
+ val code: IntArray = p.code ?: return
+ if (pc < 0 || pc >= code.size) return
+ val instr: Int = code[pc]
+ val operands: IntArray = when (Lua.GET_OPCODE(instr)) {
+ Lua.OP_ADD, Lua.OP_SUB, Lua.OP_MUL, Lua.OP_DIV, Lua.OP_MOD, Lua.OP_POW,
+ Lua.OP_IDIV, Lua.OP_BAND, Lua.OP_BOR, Lua.OP_BXOR, Lua.OP_SHL, Lua.OP_SHR,
+ -> intArrayOf(Lua.GETARG_B(instr), Lua.GETARG_C(instr))
+
+ Lua.OP_UNM, Lua.OP_BNOT, Lua.OP_LEN -> intArrayOf(Lua.GETARG_B(instr))
+ else -> return
+ }
+ for (rk in operands) {
+ val value: LuaValue = if (Lua.ISK(rk)) {
+ p.k?.getOrNull(Lua.INDEXK(rk)) ?: continue
+ } else {
+ if (rk >= stack.size) continue
+ stack[rk]
+ }
+ // Compared by the name the message used, which a __name field
+ // may have replaced.
+ if (value.objtypename() != wanted) continue
+ // For the "not a whole number" message, the operand to blame is
+ // the one that is not whole - the other may well be an integer.
+ if (insertAt != m.length && net.blueva.luak.luaHasIntegerRepresentation(value)) continue
+ val kind: String
+ val name: String
+ if (Lua.ISK(rk)) {
+ kind = "constant"
+ name = net.blueva.luak.lib.DebugLib.kname(p, pc, rk)
+ } else {
+ val found = net.blueva.luak.lib.DebugLib.getobjname(p, pc, rk) ?: return
+ kind = found.namewhat
+ name = found.name
+ }
+ val varinfo = " (" + kind + " '" + name + "')"
+ le.argMessageOverride = m.substring(0, insertAt) + varinfo + m.substring(insertAt)
+ return
}
}
@@ -796,17 +1079,25 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
* CALL/TAILCALL instruction that invoked the failing callee, since the
* throw unwound before the loop's `++pc`.
*/
- private fun enrichArgError(le: LuaError, p: Prototype, pc: Int) {
- val m = le.message ?: return
- val match = Regex("^bad argument #(\\d+): ([\\s\\S]*)$").find(m) ?: return
+ private fun enrichArgError(le: LuaError, p: Prototype, pc: Int, stack: Array) {
+ var m = le.message ?: return
val code = p.code ?: return
if (pc < 0 || pc >= code.size) return
val instr = code[pc]
val opcode = Lua.GET_OPCODE(instr)
if (opcode != Lua.OP_CALL && opcode != Lua.OP_TAILCALL) return
+ val a = Lua.GETARG_A(instr)
+ // A check made on a value alone cannot know which argument it came
+ // from. For a function that takes one argument there is only one it
+ // could have been, so the index can be filled in here.
+ if (m.startsWith("bad argument: ") && a < stack.size &&
+ stack[a] is net.blueva.luak.lib.OneArgFunction
+ ) {
+ m = "bad argument #1: " + m.removePrefix("bad argument: ")
+ }
+ val match = Regex("^bad argument #(\\d+): ([\\s\\S]*)$").find(m) ?: return
var argIndex = match.groupValues[1].toIntOrNull() ?: return
val detail = match.groupValues[2]
- val a = Lua.GETARG_A(instr)
val nw = net.blueva.luak.lib.DebugLib.getobjname(p, pc, a)
if (nw != null && nw.namewhat == "method") {
argIndex--
@@ -820,12 +1111,32 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
}
private fun processErrorHooks(le: LuaError, p: Prototype, pc: Int) {
+ // Done once, where the error was raised: every function it unwinds
+ // through afterwards would count its levels from itself and answer
+ // with its own line. See [LuaError.positioned].
+ if (le.positioned) return
+ le.positioned = true
+ // Raised where Lua adds no place; see LuaError.nowhere.
+ if (le.nowhere) {
+ errorHook(le)
+ return
+ }
+ // A level of zero says the message is complete as it stands, which is
+ // what `error(msg, 0)` asks for.
+ if (le.level <= 0) {
+ errorHook(le)
+ return
+ }
var file: String? = "?"
var line = -1
run {
var frame: CallFrame? = null
- if (globals != null && globals.debuglib != null) {
- frame = globals.debuglib!!.getCallFrame(le.level)
+ val debuglib: net.blueva.luak.lib.DebugLib? = globals?.debuglib
+ if (debuglib != null) {
+ // The library function that raised has already been popped, so
+ // level 1 - the function the error is reported against - is
+ // the frame at the top from here.
+ frame = debuglib.getCallFrame(le.level - 1)
if (frame != null) {
val src: String? = frame.shortsource()
file = if (src != null) src else "?"
@@ -833,12 +1144,638 @@ class LuaClosure(p: Prototype, env: LuaValue?) : LuaFunction() {
}
}
if (frame == null) {
- file = if (p.source != null) p.source!!.tojstring() else "?"
+ // Shortened the way Lua shortens it, so a long path or a chunk
+ // given as text does not run away with the message.
+ file = p.shortsource()
line = if (p.lineinfo != null && pc >= 0 && pc < p.lineinfo!!.size) p.lineinfo!![pc] else -1
}
}
- le.fileline = file.toString() + ":" + line
- le.traceback = errorHook(le.message, le.level)
+ // A chunk whose debug information was stripped has neither a name nor
+ // a line to report, and Lua writes both as a question mark.
+ le.fileline = file.toString() + ":" + (if (line < 0) "?" else line.toString())
+ errorHook(le)
+ }
+
+ /**
+ * Prepares a numeric `for`, upstream's `forprep`.
+ *
+ * An integer loop works out how many passes it has before it starts and
+ * keeps the count where the limit was: adding the step to the index can
+ * wrap around, but a count cannot, so a loop that walks the whole integer
+ * range still ends.
+ *
+ * @return true when the loop does not run at all
+ */
+ private fun forPrep(stack: Array, a: Int): Boolean {
+ // Checked in upstream's order - limit, step, then the initial value -
+ // so a loop with more than one bad bound names the same one Lua would.
+ val limit: LuaValue = forNumber(stack[a + 1], "limit")
+ val step: LuaValue = forNumber(stack[a + 2], "step")
+ val init: LuaValue = forNumber(stack[a], "initial value")
+ if (init is LuaInteger && step is LuaInteger) {
+ val start: Long = init.tolong()
+ val by: Long = step.tolong()
+ if (by == 0L) LuaValue.error("'for' step is zero")
+ val bound: Long = forLimit(limit, start, by) ?: return true
+ val passes: ULong = if (by > 0L) {
+ val span: ULong = bound.toULong() - start.toULong()
+ if (by == 1L) span else span / by.toULong()
+ } else {
+ val span: ULong = start.toULong() - bound.toULong()
+ // Negating math.mininteger would overflow, so the magnitude is
+ // built from '-(by + 1)' instead.
+ span / ((-(by + 1L)).toULong() + 1uL)
+ }
+ stack[a] = init
+ stack[a + 1] = LuaValue.valueOf(passes.toLong())
+ stack[a + 2] = step
+ stack[a + 3] = init
+ return false
+ }
+ // A float loop has no count to work out and compares against the limit
+ // on every pass instead.
+ val start: Double = init.todouble()
+ val by: Double = step.todouble()
+ val bound: Double = limit.todouble()
+ if (by == 0.0) LuaValue.error("'for' step is zero")
+ if (if (by > 0.0) start > bound else start < bound) return true
+ val first: LuaValue = LuaValue.valueOf(start)
+ stack[a] = first
+ stack[a + 1] = LuaValue.valueOf(bound)
+ stack[a + 2] = LuaValue.valueOf(by)
+ stack[a + 3] = first
+ return false
+ }
+
+ /**
+ * The integer a `for` loop counts up or down to, upstream's `forlimit`.
+ *
+ * A float limit is rounded towards the loop's direction; one beyond the
+ * integer range is either the far end of it or, when it lies the wrong way
+ * round, a loop that never runs.
+ *
+ * @return the limit, or null when the loop does not run at all
+ */
+ private fun forLimit(limit: LuaValue, init: Long, step: Long): Long? {
+ val bound: Long
+ if (limit is LuaInteger) {
+ bound = limit.tolong()
+ } else {
+ val value: Double = limit.todouble()
+ val rounded: Double =
+ if (step < 0L) kotlin.math.ceil(value) else kotlin.math.floor(value)
+ if (rounded >= -9223372036854775808.0 && rounded < 9223372036854775808.0) {
+ bound = rounded.toLong()
+ } else if (rounded > 0.0) {
+ // Too large to reach; a descending loop never gets there.
+ if (step < 0L) return null
+ bound = Long.MAX_VALUE
+ } else {
+ if (step > 0L) return null
+ bound = Long.MIN_VALUE
+ }
+ }
+ return if (if (step > 0L) init > bound else init < bound) null else bound
+ }
+
+ /**
+ * The function a tail call really reaches, following `__call` handlers.
+ *
+ * Following the chain here rather than letting each handler call the next
+ * keeps a tail call flat, so a loop written as `return t()` over a
+ * `__call` table runs as long as one written over a function.
+ */
+ private fun resolveTailcall(stack: Array, a: Int, prefix: ArrayList): LuaValue =
+ stack[a].resolvecall(prefix)
+
+ /**
+ * Calls [f], giving a library function a frame of its own while it runs.
+ *
+ * A Lua function pushes its own on the way in; anything else has none, and
+ * without one a traceback would not name it and the call and return hooks
+ * would never fire for it.
+ */
+ private suspend fun invokeTraced(f: LuaValue, args: Varargs, debuglib: DebugLib?): Varargs {
+ if (debuglib == null || f !is LuaFunction || f is LuaClosure) return f.invokeSuspend(args)
+ debuglib.onCall(f, copyArgs(args))
+ try {
+ val results: Varargs = f.invokeSuspend(args)
+ // What it hands back, so a return hook can read the results.
+ debuglib.onResults(results)
+ return results
+ } catch (le: LuaError) {
+ debuglib.notestack(le)
+ throw le
+ } finally {
+ debuglib.onReturn()
+ }
+ }
+
+ /**
+ * One pass of a numeric `for`, upstream's `forloop`.
+ *
+ * @return true when the loop should go round again
+ */
+ private fun forLoop(stack: Array, a: Int): Boolean {
+ val step: LuaValue = stack[a + 2]
+ if (step is LuaInteger) {
+ // Read as unsigned: only the test against zero and the decrement
+ // matter, and both are the same bits.
+ val remaining: Long = stack[a + 1].tolong()
+ if (remaining == 0L) return false
+ stack[a + 1] = LuaValue.valueOf(remaining - 1L)
+ val next: LuaValue = LuaValue.valueOf(stack[a].tolong() + step.tolong())
+ stack[a] = next
+ stack[a + 3] = next
+ return true
+ }
+ val by: Double = step.todouble()
+ val next: Double = stack[a].todouble() + by
+ val limit: Double = stack[a + 1].todouble()
+ if (if (by > 0.0) next > limit else limit > next) return false
+ val value: LuaValue = LuaValue.valueOf(next)
+ stack[a] = value
+ stack[a + 3] = value
+ return true
+ }
+
+ /** Builds the closure an OP_CLOSURE asks for, binding its upvalues. */
+ private fun makeclosure(stack: Array, i: Int, openups: Array?): LuaClosure {
+ val newp: Prototype = p.p!![i ushr 14]!!
+ val ncl = net.blueva.luak.LuaClosure(newp, globals)
+ val uv: Array = newp.upvalues!!
+ var j = 0
+ while (j < uv.size) {
+ ncl.upValues[j] = if (uv[j]!!.instack) {
+ findupval(stack, uv[j]!!.idx, openups!!)
+ } else {
+ upValues[(uv[j]!!.idx).toInt()]
+ }
+ ++j
+ }
+ // Making a function is an allocation like any other, and so a place a
+ // collection can happen; see callFixedArity.
+ val g: Globals? = globals
+ if (g != null && g.marksfinalizers) g.runfinalizers()
+ return ncl
+ }
+
+ /** The opcodes that only move a value about, with nothing to dispatch. */
+ private fun loadOpcode(stack: Array, i: Int, a: Int, k: Array) {
+ when (i and 0x3f) {
+ Lua.OP_MOVE -> stack[a] = stack[i ushr 23]
+ Lua.OP_LOADK -> stack[a] = k[i ushr 14]!!
+ Lua.OP_LOADNIL -> {
+ var slot: Int = a
+ var count: Int = i ushr 23
+ while (count-- >= 0) stack[slot++] = LuaValue.NIL
+ }
+
+ Lua.OP_GETUPVAL -> stack[a] = upValues[i ushr 23]!!.getValue()!!
+ Lua.OP_SETUPVAL -> upValues[i ushr 23]!!.setValue(stack[a])
+ else -> {
+ stack[a] = LuaTable(i ushr 23, (i shr 14) and 0x1ff)
+ // Allocating is where Lua runs a step of its collector, and so
+ // where anything waiting to be finalized gets its turn.
+ val g: Globals? = globals
+ if (g != null && g.marksfinalizers) {
+ // This instruction always writes to the first free
+ // register, so nothing above it is live any more. Lua's
+ // collector reaches the same conclusion by only looking at
+ // a stack up to its top; here the registers are emptied,
+ // so that what a finished statement left behind stops
+ // holding an object that is due to be finalized.
+ var slot: Int = a + 1
+ while (slot < stack.size) {
+ stack[slot] = LuaValue.NIL
+ slot++
+ }
+ g.runfinalizers()
+ }
+ }
+ }
+ }
+
+ /**
+ * The opcodes that read or write a field.
+ *
+ * A table answers from its own storage; anything else, or a miss with an
+ * `__index`, goes through the metamethod, which may yield.
+ */
+ private suspend fun tableOpcode(stack: Array, i: Int, a: Int, k: Array) {
+ val c: Int = (i shr 14) and 0x1ff
+ when (i and 0x3f) {
+ Lua.OP_GETTABUP -> {
+ val target: LuaValue = upValues[i ushr 23]!!.getValue()!!
+ stack[a] = index(target, operand(stack, k, c))
+ }
+
+ Lua.OP_GETTABLE -> stack[a] = index(stack[i ushr 23], operand(stack, k, c))
+
+ Lua.OP_SELF -> {
+ val target: LuaValue = stack[i ushr 23]
+ stack[a + 1] = target
+ stack[a] = index(target, operand(stack, k, c))
+ }
+
+ Lua.OP_SETTABUP -> {
+ val target: LuaValue = upValues[a]!!.getValue()!!
+ newindex(target, operand(stack, k, i ushr 23), operand(stack, k, c))
+ }
+
+ else -> newindex(stack[a], operand(stack, k, i ushr 23), operand(stack, k, c))
+ }
+ }
+
+ /**
+ * Reads `target[key]`, following `__index` as far as it leads.
+ *
+ * The handler is called from here rather than from inside the value, so a
+ * coroutine can yield out of one.
+ */
+ private suspend fun index(target: LuaValue, key: LuaValue): LuaValue {
+ var value: LuaValue = target
+ var loop = 0
+ while (loop++ < LuaValue.MAXTAGLOOP) {
+ val handler: LuaValue
+ if (value.istable()) {
+ val found: LuaValue = value.rawget(key)
+ if (!found.isnil()) return found
+ handler = value.metatag(LuaValue.INDEX)
+ if (handler.isnil()) return found
+ } else {
+ handler = value.metatag(LuaValue.INDEX)
+ if (handler.isnil()) return value.get(key) // reports what it is
+ }
+ if (handler.isfunction()) return callmeta(handler, value, key)
+ value = handler
+ }
+ LuaValue.error("loop in gettable")
+ return LuaValue.NIL
+ }
+
+ /** Writes `target[key]`, following `__newindex` as far as it leads. */
+ private suspend fun newindex(target: LuaValue, key: LuaValue, value: LuaValue) {
+ var holder: LuaValue = target
+ var loop = 0
+ while (loop++ < LuaValue.MAXTAGLOOP) {
+ val handler: LuaValue
+ if (holder.istable()) {
+ if (!holder.rawget(key).isnil()) {
+ holder.rawset(key, value)
+ return
+ }
+ handler = holder.metatag(LuaValue.NEWINDEX)
+ if (handler.isnil()) {
+ holder.set(key, value) // reports a bad key as Lua does
+ return
+ }
+ } else {
+ handler = holder.metatag(LuaValue.NEWINDEX)
+ if (handler.isnil()) {
+ holder.set(key, value) // reports what it is
+ return
+ }
+ }
+ if (handler.isfunction()) {
+ handler.callSuspend(holder, key, value)
+ return
+ }
+ holder = handler
+ }
+ LuaValue.error("loop in settable")
+ }
+
+ /**
+ * The two operands of a binary opcode, constants and registers alike.
+ */
+ private fun operand(stack: Array, k: Array, rk: Int): LuaValue =
+ if (rk > 0xff) k[rk and 0x0ff]!! else stack[rk]
+
+ /**
+ * Runs `__add` and its kin, letting the handler yield if it wants to.
+ *
+ * The handler is looked for on the left operand and then on the right, as
+ * Lua looks for it, and calling it here rather than from inside the value
+ * itself is what lets a coroutine yield out of one.
+ */
+ private suspend fun binmeta(tag: LuaValue, lhs: LuaValue, rhs: LuaValue): LuaValue {
+ var h: LuaValue = lhs.metatag(tag)
+ if (h.isnil()) h = rhs.metatag(tag)
+ if (h.isnil()) LuaValue.operandError(tag, lhs, rhs)
+ lhs.checkcallable(tag, h)
+ return callmeta(h, lhs, rhs)
+ }
+
+ /**
+ * The arithmetic, bitwise and concatenation opcodes.
+ *
+ * Two numbers are worked out here and now; anything else goes through the
+ * metamethod, which may yield, which is why this is a suspending method of
+ * its own rather than part of [execute].
+ */
+ /**
+ * The comparison opcodes.
+ *
+ * Two numbers or two strings are compared here and now; anything else goes
+ * through the metamethod, which may yield.
+ */
+ private suspend fun compareOpcode(stack: Array, i: Int, k: Array): Boolean {
+ val lhs: LuaValue = operand(stack, k, i ushr 23)
+ val rhs: LuaValue = operand(stack, k, (i shr 14) and 0x1ff)
+ val opcode: Int = i and 0x3f
+ if (opcode == Lua.OP_EQ) {
+ if (lhs.raweq(rhs)) return true
+ // Only two tables or two full userdata have an __eq to consult.
+ if (lhs.type() != rhs.type()) return false
+ if (!lhs.istable() && !lhs.isuserdata()) return false
+ var h: LuaValue = lhs.metatag(LuaValue.EQ)
+ if (h.isnil()) h = rhs.metatag(LuaValue.EQ)
+ if (h.isnil()) return false
+ lhs.checkcallable(LuaValue.EQ, h)
+ return callmeta(h, lhs, rhs).toboolean()
+ }
+ val primitive: Boolean =
+ (lhs is LuaNumber && rhs is LuaNumber) || (lhs is LuaString && rhs is LuaString)
+ if (primitive) {
+ return if (opcode == Lua.OP_LT) lhs.lt_b(rhs) else lhs.lteq_b(rhs)
+ }
+ val tag: LuaValue = if (opcode == Lua.OP_LT) LuaValue.LT else LuaValue.LE
+ var h: LuaValue = lhs.metatag(tag)
+ if (h.isnil()) h = rhs.metatag(tag)
+ if (h.isnil()) {
+ // Lua 5.4 dropped the "not (b < a)" stand-in for a missing __le,
+ // so there is nothing left to try.
+ lhs.ordererror(lhs, rhs)
+ }
+ lhs.checkcallable(tag, h)
+ return callmeta(h, lhs, rhs).toboolean()
+ }
+
+ private suspend fun binaryOpcode(stack: Array, i: Int, a: Int, k: Array) {
+ val opcode: Int = i and 0x3f
+ if (opcode == Lua.OP_CONCAT) {
+ var b: Int = i ushr 23
+ var c: Int = (i shr 14) and 0x1ff
+ while (c > b) {
+ val left: LuaValue = stack[c - 1]
+ val right: LuaValue = stack[c]
+ stack[c - 1] = if (left.isstring() && right.isstring()) {
+ left.concat(right)
+ } else {
+ concatmeta(left, right)
+ }
+ c--
+ }
+ stack[a] = stack[b]
+ return
+ }
+ val lhs: LuaValue = operand(stack, k, i ushr 23)
+ val rhs: LuaValue = operand(stack, k, (i shr 14) and 0x1ff)
+ if (lhs is LuaNumber && rhs is LuaNumber) {
+ stack[a] = when (opcode) {
+ Lua.OP_ADD -> lhs.add(rhs)
+ Lua.OP_SUB -> lhs.sub(rhs)
+ Lua.OP_MUL -> lhs.mul(rhs)
+ Lua.OP_DIV -> lhs.div(rhs)
+ Lua.OP_IDIV -> lhs.idiv(rhs)
+ Lua.OP_MOD -> lhs.mod(rhs)
+ Lua.OP_POW -> lhs.pow(rhs)
+ Lua.OP_BAND -> lhs.band(rhs)
+ Lua.OP_BOR -> lhs.bor(rhs)
+ Lua.OP_BXOR -> lhs.bxor(rhs)
+ Lua.OP_SHL -> lhs.shl(rhs)
+ else -> lhs.shr(rhs)
+ }
+ return
+ }
+ val tag: LuaValue = when (opcode) {
+ Lua.OP_ADD -> LuaValue.ADD
+ Lua.OP_SUB -> LuaValue.SUB
+ Lua.OP_MUL -> LuaValue.MUL
+ Lua.OP_DIV -> LuaValue.DIV
+ Lua.OP_IDIV -> LuaValue.IDIV
+ Lua.OP_MOD -> LuaValue.MOD
+ Lua.OP_POW -> LuaValue.POW
+ Lua.OP_BAND -> LuaValue.BAND
+ Lua.OP_BOR -> LuaValue.BOR
+ Lua.OP_BXOR -> LuaValue.BXOR
+ Lua.OP_SHL -> LuaValue.SHL
+ else -> LuaValue.SHR
+ }
+ stack[a] = binmeta(tag, lhs, rhs)
+ }
+
+ /** `__concat`, which may yield, blaming the operand that is not a string. */
+ private suspend fun concatmeta(lhs: LuaValue, rhs: LuaValue): LuaValue {
+ var h: LuaValue = lhs.metatag(LuaValue.CONCAT)
+ if (h.isnil()) h = rhs.metatag(LuaValue.CONCAT)
+ if (h.isnil()) {
+ val culprit: LuaValue = if (!lhs.isstring() || lhs is LuaTable) lhs else rhs
+ LuaValue.error("attempt to concatenate a " + culprit.objtypename() + " value")
+ }
+ lhs.checkcallable(LuaValue.CONCAT, h)
+ return callmeta(h, lhs, rhs)
+ }
+
+ /** The unary opcodes, whose metamethods may yield in the same way. */
+ private suspend fun unaryOpcode(stack: Array, i: Int, a: Int) {
+ val operand: LuaValue = stack[i ushr 23]
+ when (i and 0x3f) {
+ Lua.OP_NOT -> stack[a] = operand.not()!!
+ Lua.OP_UNM -> stack[a] =
+ if (operand is LuaNumber) operand.neg() else unmeta(LuaValue.UNM, operand)
+
+ Lua.OP_BNOT -> stack[a] =
+ if (operand is LuaNumber) operand.bnot() else unmeta(LuaValue.BNOT, operand)
+
+ else -> stack[a] =
+ if (operand is LuaString) operand.len() else unmeta(LuaValue.LEN, operand)
+ }
+ }
+
+ /**
+ * A unary metamethod, which Lua hands its operand twice.
+ *
+ * A table answers `#t` from its own length when it has no `__len`, which is
+ * the one case that is not an error without a handler.
+ */
+ private suspend fun unmeta(tag: LuaValue, operand: LuaValue): LuaValue {
+ val h: LuaValue = operand.metatag(tag)
+ if (h.isnil()) {
+ if (tag == LuaValue.LEN && operand is LuaTable) return operand.len()
+ if (tag == LuaValue.LEN) {
+ LuaValue.error("attempt to get length of a " + operand.objtypename() + " value")
+ }
+ LuaValue.operandError(tag, operand, operand)
+ }
+ operand.checkcallable(tag, h)
+ return callmeta(h, operand, operand)
+ }
+
+ /** The arguments of a fixed-arity call, for the debug library to report. */
+ private fun fixedArityArgs(stack: Array, i: Int, a: Int): Array {
+ val count: Int = ((i ushr 23) and 0x1ff) - 1
+ if (count <= 0) return arrayOfNulls(0)
+ val values: Array = arrayOfNulls(count)
+ for (index in 0.. {
+ val n: Int = varargs.narg()
+ val values: Array = arrayOfNulls(n)
+ for (index in 0.., bx: Int): Nothing {
+ val name: String = if (bx > 0 && bx - 1 < k.size) k[bx - 1]!!.tojstring() else "?"
+ LuaValue.error("global '" + name + "' already defined")
+ throw IllegalStateException()
+ }
+
+ /** As many values as a vararg table may claim, mirroring Lua's stack cap. */
+ private val MAX_VARARG_TABLE: Long = 1000000L
+
+ /**
+ * Fills the register of a named vararg parameter with its table.
+ *
+ * The table holds the extra arguments at `1..n` and their count at `n`,
+ * which is what makes `t.n` right even when an argument was nil.
+ */
+ private fun buildVarargTable(varargs: Varargs, p: Prototype, stack: Array) {
+ val count: Int = varargs.narg()
+ val before: Long = Memory.accounted
+ val table = LuaTable(count, 1)
+ for (i in 1..count) table.set(i, varargs.arg(i)!!)
+ table.set("n", count)
+ // The arguments of a call are not an allocation of the program's; see
+ // Memory.uncount.
+ Memory.uncount(Memory.accounted - before)
+ stack[p.numparams] = table
+ }
+
+ /**
+ * Where `...` reads from.
+ *
+ * Ordinarily the arguments the call arrived with; in a function that named
+ * them, the table they were put in, so a change made through the name is
+ * visible through `...` as well.
+ */
+ private fun varargSource(varargs: Varargs, p: Prototype, stack: Array): Varargs {
+ if (p.is_vararg and Lua.VARARG_NAMED == 0) return varargs
+ val table: LuaValue = stack[p.numparams]
+ val declared: LuaValue = table.get("n")!!
+ // The table's 'n' says how many values '...' has, so a program that
+ // sets it to something that is not a sensible count has broken the
+ // link rather than resized it.
+ if (!declared.isnumber() || !declared.isinttype()) {
+ LuaValue.error("vararg table has no proper 'n'")
+ }
+ val n: Long = declared.tolong()
+ if (n < 0 || n > MAX_VARARG_TABLE) LuaValue.error("vararg table has no proper 'n'")
+ val count: Int = n.toInt()
+ if (count <= 0) return NONE!!
+ val out: Array = arrayOfNulls(count)
+ for (i in 0..`.
+ *
+ * A false or nil value is not closed and not remembered, which is what lets
+ * `local f = io.open(...)` be written without a separate check.
+ * Anything else has to answer a `__close` metamethod, and the complaint
+ * comes at the declaration rather than at the end of the block.
+ *
+ * @return the list to keep, which is created on the first such variable
+ */
+ private fun markToBeClosed(
+ list: ArrayList?,
+ value: LuaValue,
+ slot: Int,
+ p: Prototype,
+ pc: Int,
+ ): ArrayList? {
+ if (!value.toboolean()) return list
+ if (value.metatag(LuaValue.CLOSE).isnil()) {
+ val name: LuaString? = p.getlocalname(slot + 1, pc)
+ LuaValue.error(
+ "variable '" + (name?.tojstring() ?: "?") + "' got a non-closable value",
+ )
+ }
+ val out: ArrayList = list ?: ArrayList(1)
+ out.add(slot)
+ return out
+ }
+
+ /**
+ * Closes the to-be-closed variables at or above [level], innermost first.
+ *
+ * Each is dropped from the list as it is closed, so a later pass - the
+ * `finally` after an error has already unwound one - does not close it
+ * twice.
+ *
+ * A handler that raises does not stop the ones outside it: its error
+ * becomes what they are told about, and the last one raised is what
+ * leaves here.
+ *
+ * @param error the error being unwound from, or null on an ordinary exit
+ * @return the error to carry on with, or null if none is outstanding
+ */
+ private suspend fun closeToBeClosed(
+ list: ArrayList,
+ stack: Array,
+ level: Int,
+ error: LuaError?,
+ ): LuaError? {
+ var pending: LuaError? = error
+ var index = list.size
+ while (--index >= 0) {
+ val slot: Int = list[index]
+ if (slot < level) break
+ list.removeAt(index)
+ val value: LuaValue = stack[slot]
+ val close: LuaValue = value.metatag(LuaValue.CLOSE)
+ // The handler may have been taken away since the variable was
+ // marked, so what is there now still has to be callable.
+ value.checkcallable(LuaValue.CLOSE, close)
+ try {
+ // With no error to report the handler is called with the value
+ // alone: a trailing nil would be an argument the language does
+ // not pass, and '...' inside the handler would count it.
+ val raised: LuaError? = pending
+ // Suspending, so a handler may yield on the ordinary ways out
+ // of a block. The paths that close while an error is unwinding
+ // reach this through runLuaSync instead, which is where Lua
+ // also refuses to yield.
+ if (raised == null) {
+ close.callSuspend(value)
+ } else {
+ close.callSuspend(value, raised.messageObject ?: NIL)
+ }
+ } catch (failure: LuaError) {
+ pending = failure
+ }
+ }
+ return pending
}
private fun findupval(stack: Array, idx: Short, openups: Array): UpValue? {
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaDouble.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaDouble.kt
index 5334ce54..2e76028b 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaDouble.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaDouble.kt
@@ -100,20 +100,23 @@ class LuaDouble
return v
}
+ // The opt forms differ from the check forms only in what a missing
+ // argument does, which cannot happen once there is a value here, so they
+ // hold this float to the same standard.
override fun optint(defval: Int): Int {
- return v.toLong().toInt()
+ return checkint()
}
override fun optinteger(defval: LuaInteger?): LuaInteger {
- return (LuaInteger.valueOf(v.toLong().toInt()))!!
+ return checkinteger()
}
override fun optlong(defval: Long): Long {
- return v.toLong()
+ return checklong()
}
override fun checkinteger(): LuaInteger {
- return (LuaInteger.valueOf(v.toLong().toInt()))!!
+ return (LuaInteger.valueOf(checklong()))!!
}
// unary operators
@@ -148,9 +151,8 @@ class LuaDouble
return v == `val`
}
- override fun raweq(`val`: Int): Boolean {
- val `val` = `val`!!
- return v == `val`.toDouble()
+ override fun raweq(`val`: Long): Boolean {
+ return luaIntegerEqualsFloat(`val`, v)
}
// basic binary arithmetic
@@ -162,6 +164,10 @@ class LuaDouble
return (net.blueva.luak.LuaDouble.Companion.valueOf(lhs + v))!!
}
+ override fun add(rhs: Long): LuaValue {
+ return (net.blueva.luak.LuaDouble.Companion.valueOf(rhs + v))!!
+ }
+
override fun sub(rhs: LuaValue): LuaValue {
return rhs.subFrom(v)
}
@@ -170,7 +176,7 @@ class LuaDouble
return net.blueva.luak.LuaDouble.Companion.valueOf(v - rhs)
}
- override fun sub(rhs: Int): LuaValue? {
+ override fun sub(rhs: Long): LuaValue? {
return net.blueva.luak.LuaDouble.Companion.valueOf(v - rhs)
}
@@ -178,6 +184,10 @@ class LuaDouble
return (net.blueva.luak.LuaDouble.Companion.valueOf(lhs - v))!!
}
+ override fun subFrom(lhs: Long): LuaValue {
+ return (net.blueva.luak.LuaDouble.Companion.valueOf(lhs - v))!!
+ }
+
override fun mul(rhs: LuaValue): LuaValue {
return rhs.mul(v)
}
@@ -186,7 +196,7 @@ class LuaDouble
return (net.blueva.luak.LuaDouble.Companion.valueOf(lhs * v))!!
}
- override fun mul(lhs: Int): LuaValue {
+ override fun mul(lhs: Long): LuaValue {
return (net.blueva.luak.LuaDouble.Companion.valueOf(lhs * v))!!
}
@@ -198,7 +208,7 @@ class LuaDouble
return MathLib.dpow(v, rhs)
}
- override fun pow(rhs: Int): LuaValue {
+ override fun pow(rhs: Long): LuaValue {
return MathLib.dpow(v, (rhs).toDouble())
}
@@ -206,10 +216,37 @@ class LuaDouble
return MathLib.dpow(lhs, v)
}
- override fun powWith(lhs: Int): LuaValue {
+ override fun powWith(lhs: Long): LuaValue {
return MathLib.dpow((lhs).toDouble(), v)
}
+ override fun band(rhs: LuaValue): LuaValue = bitwise(net.blueva.luak.LuaValue.Companion.BAND, rhs)
+ override fun bor(rhs: LuaValue): LuaValue = bitwise(net.blueva.luak.LuaValue.Companion.BOR, rhs)
+ override fun bxor(rhs: LuaValue): LuaValue = bitwise(net.blueva.luak.LuaValue.Companion.BXOR, rhs)
+ override fun shl(rhs: LuaValue): LuaValue = bitwise(net.blueva.luak.LuaValue.Companion.SHL, rhs)
+ override fun shr(rhs: LuaValue): LuaValue = bitwise(net.blueva.luak.LuaValue.Companion.SHR, rhs)
+
+ override fun bnot(): LuaValue = LuaValue.valueOf(luaBitwiseOperand(this).inv())
+
+ private fun bitwise(tag: LuaString, rhs: LuaValue): LuaValue {
+ if (!rhs.isnumber() || rhs is LuaString) return arithmt(tag, rhs)
+ val x: Long = luaBitwiseOperand(this)
+ val y: Long = luaBitwiseOperand(rhs)
+ return when (tag) {
+ net.blueva.luak.LuaValue.Companion.BAND -> LuaValue.valueOf(x and y)
+ net.blueva.luak.LuaValue.Companion.BOR -> LuaValue.valueOf(x or y)
+ net.blueva.luak.LuaValue.Companion.BXOR -> LuaValue.valueOf(x xor y)
+ net.blueva.luak.LuaValue.Companion.SHL -> LuaValue.valueOf(luaShiftLeft(x, y))
+ else -> LuaValue.valueOf(luaShiftLeft(x, -y))
+ }
+ }
+
+ override fun idiv(rhs: LuaValue): LuaValue {
+ val other: LuaValue = rhs.tonumber()
+ if (other.isnil()) return arithmt(net.blueva.luak.LuaValue.Companion.IDIV, rhs)
+ return luaFloorDiv(this, other)
+ }
+
override fun div(rhs: LuaValue): LuaValue {
return rhs.divInto(v)
}
@@ -218,7 +255,7 @@ class LuaDouble
return net.blueva.luak.LuaDouble.Companion.ddiv(v, rhs)
}
- override fun div(rhs: Int): LuaValue? {
+ override fun div(rhs: Long): LuaValue? {
return net.blueva.luak.LuaDouble.Companion.ddiv(v, rhs.toDouble())
}
@@ -234,7 +271,7 @@ class LuaDouble
return net.blueva.luak.LuaDouble.Companion.dmod(v, rhs)
}
- override fun mod(rhs: Int): LuaValue? {
+ override fun mod(rhs: Long): LuaValue? {
return net.blueva.luak.LuaDouble.Companion.dmod(v, rhs.toDouble())
}
@@ -252,16 +289,16 @@ class LuaDouble
return (if (v < rhs) TRUE else FALSE)!!
}
- override fun lt(rhs: Int): LuaValue {
- return (if (v < rhs) TRUE else FALSE)!!
+ override fun lt(rhs: Long): LuaValue {
+ return (if (luaFloatLessThanInteger(v, rhs)) TRUE else FALSE)!!
}
override fun lt_b(rhs: LuaValue): Boolean {
return if (rhs is LuaNumber) rhs.gt_b(v) else super.lt_b(rhs)
}
- override fun lt_b(rhs: Int): Boolean {
- return v < rhs
+ override fun lt_b(rhs: Long): Boolean {
+ return luaFloatLessThanInteger(v, rhs)
}
override fun lt_b(rhs: Double): Boolean {
@@ -276,16 +313,16 @@ class LuaDouble
return (if (v <= rhs) TRUE else FALSE)!!
}
- override fun lteq(rhs: Int): LuaValue {
- return (if (v <= rhs) TRUE else FALSE)!!
+ override fun lteq(rhs: Long): LuaValue {
+ return (if (luaFloatLessOrEqualInteger(v, rhs)) TRUE else FALSE)!!
}
override fun lteq_b(rhs: LuaValue): Boolean {
return if (rhs is LuaNumber) rhs.gteq_b(v) else super.lteq_b(rhs)
}
- override fun lteq_b(rhs: Int): Boolean {
- return v <= rhs
+ override fun lteq_b(rhs: Long): Boolean {
+ return luaFloatLessOrEqualInteger(v, rhs)
}
override fun lteq_b(rhs: Double): Boolean {
@@ -300,16 +337,16 @@ class LuaDouble
return (if (v > rhs) TRUE else FALSE)!!
}
- override fun gt(rhs: Int): LuaValue {
- return (if (v > rhs) TRUE else FALSE)!!
+ override fun gt(rhs: Long): LuaValue {
+ return (if (luaIntegerLessThanFloat(rhs, v)) TRUE else FALSE)!!
}
override fun gt_b(rhs: LuaValue): Boolean {
return if (rhs is LuaNumber) rhs.lt_b(v) else super.gt_b(rhs)
}
- override fun gt_b(rhs: Int): Boolean {
- return v > rhs
+ override fun gt_b(rhs: Long): Boolean {
+ return luaIntegerLessThanFloat(rhs, v)
}
override fun gt_b(rhs: Double): Boolean {
@@ -324,16 +361,16 @@ class LuaDouble
return (if (v >= rhs) TRUE else FALSE)!!
}
- override fun gteq(rhs: Int): LuaValue {
- return (if (v >= rhs) TRUE else FALSE)!!
+ override fun gteq(rhs: Long): LuaValue {
+ return (if (luaIntegerLessOrEqualFloat(rhs, v)) TRUE else FALSE)!!
}
override fun gteq_b(rhs: LuaValue): Boolean {
return if (rhs is LuaNumber) rhs.lteq_b(v) else super.gteq_b(rhs)
}
- override fun gteq_b(rhs: Int): Boolean {
- return v >= rhs
+ override fun gteq_b(rhs: Long): Boolean {
+ return luaIntegerLessOrEqualFloat(rhs, v)
}
override fun gteq_b(rhs: Double): Boolean {
@@ -347,22 +384,7 @@ class LuaDouble
}
override fun tojstring(): String {
- /*
- if ( v == 0.0 ) { // never occurs in J2me
- long bits = ( v ).toBits();
- return ( bits >> 63 == 0 ) ? "0" : "-0";
- }
- */
- val l = v.toLong()
- if (l.toDouble() == v) return l.toString()
- if ((v).isNaN()) return net.blueva.luak.LuaDouble.Companion.JSTR_NAN
- if ((v).isInfinite()) return (if (v < 0) net.blueva.luak.LuaDouble.Companion.JSTR_NEGINF else net.blueva.luak.LuaDouble.Companion.JSTR_POSINF)
- val f = v.toFloat()
- // v is finite but exceeds Float range: narrowing to Float would wrongly
- // produce "Infinity". Fall back to full double precision instead of
- // reporting a finite number as infinite.
- if (f.isInfinite()) return v.toString()
- return f.toString()
+ return DecimalFormat.luaFloat(v)
}
override fun strvalue(): LuaString {
@@ -398,11 +420,19 @@ class LuaDouble
}
override fun checkint(): Int {
- return v.toLong().toInt()
+ return checklong().toInt()
}
+ /**
+ * The integer this float denotes, or an error if it denotes none.
+ *
+ * Truncating silently would let `string.rep("x", 2.5)` mean `2`, where Lua
+ * requires a value that is exactly an integer.
+ */
override fun checklong(): Long {
- return v.toLong()
+ val whole: Long = v.toLong()
+ if (whole.toDouble() != v) LuaValue.error("number has no integer representation")
+ return whole
}
override fun checknumber(): LuaNumber {
@@ -444,9 +474,17 @@ class LuaDouble
/** Constant String representation for negative infinity, "-inf" */
val JSTR_NEGINF: String = "-inf"
+ /**
+ * A float stays a float.
+ *
+ * BlueLuaK inherited LuaJ's habit of folding a double with an integral
+ * value into a [LuaInteger], which made sense when Lua 5.2 had a single
+ * number type. Since 5.3 the two subtypes are distinguishable from Lua
+ * (`math.type`, `2.0` printing as `2.0`, `1 // 0.0` giving `inf`), so
+ * the fold has to go.
+ */
fun valueOf(d: Double): LuaNumber? {
- val id = d.toInt()
- return if (d == id.toDouble()) LuaInteger.valueOf(id) as LuaNumber? else net.blueva.luak.LuaDouble(d) as LuaNumber
+ return net.blueva.luak.LuaDouble(d)
}
/** Divide two double numbers according to lua math, and return a [LuaValue] result.
@@ -457,7 +495,9 @@ class LuaDouble
* @see .ddiv_d
*/
fun ddiv(lhs: Double, rhs: Double): LuaValue? {
- return if (rhs != 0.0) net.blueva.luak.LuaDouble.Companion.valueOf(lhs / rhs) else if (lhs > 0) net.blueva.luak.LuaDouble.Companion.POSINF else if (lhs == 0.0) net.blueva.luak.LuaDouble.Companion.NAN else net.blueva.luak.LuaDouble.Companion.NEGINF
+ // Plain IEEE division. Special-casing a zero divisor lost the sign
+ // of the zero, so 1/-0.0 came out positive.
+ return net.blueva.luak.LuaDouble.Companion.valueOf(lhs / rhs)
}
/** Divide two double numbers according to lua math, and return a double result.
@@ -467,7 +507,7 @@ class LuaDouble
* @see .ddiv
*/
fun ddiv_d(lhs: Double, rhs: Double): Double {
- return if (rhs != 0.0) lhs / rhs else if (lhs > 0) Double.POSITIVE_INFINITY else if (lhs == 0.0) Double.NaN else Double.NEGATIVE_INFINITY
+ return lhs / rhs
}
/** Take modulo double numbers according to lua math, and return a [LuaValue] result.
@@ -476,20 +516,10 @@ class LuaDouble
* @return [LuaValue] for the result of the modulo,
* using lua's rules for modulo
* @see .dmod_d
+ * @see luaFloatMod
*/
fun dmod(lhs: Double, rhs: Double): LuaValue? {
- if (rhs == 0.0 || lhs == Double.POSITIVE_INFINITY || lhs == Double.NEGATIVE_INFINITY) return net.blueva.luak.LuaDouble.Companion.NAN
- if (rhs == Double.POSITIVE_INFINITY) {
- return if (lhs < 0) net.blueva.luak.LuaDouble.Companion.POSINF else net.blueva.luak.LuaDouble.Companion.valueOf(
- lhs
- )
- }
- if (rhs == Double.NEGATIVE_INFINITY) {
- return if (lhs > 0) net.blueva.luak.LuaDouble.Companion.NEGINF else net.blueva.luak.LuaDouble.Companion.valueOf(
- lhs
- )
- }
- return net.blueva.luak.LuaDouble.Companion.valueOf(lhs - rhs * kotlin.math.floor(lhs / rhs))
+ return net.blueva.luak.LuaDouble.Companion.valueOf(luaFloatMod(lhs, rhs))
}
/** Take modulo for double numbers according to lua math, and return a double result.
@@ -500,14 +530,7 @@ class LuaDouble
* @see .dmod
*/
fun dmod_d(lhs: Double, rhs: Double): Double {
- if (rhs == 0.0 || lhs == Double.POSITIVE_INFINITY || lhs == Double.NEGATIVE_INFINITY) return Double.NaN
- if (rhs == Double.POSITIVE_INFINITY) {
- return if (lhs < 0) Double.POSITIVE_INFINITY else lhs
- }
- if (rhs == Double.NEGATIVE_INFINITY) {
- return if (lhs > 0) Double.NEGATIVE_INFINITY else lhs
- }
- return lhs - rhs * kotlin.math.floor(lhs / rhs)
+ return luaFloatMod(lhs, rhs)
}
}
}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaError.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaError.kt
index e7459caf..e26bdd75 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaError.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaError.kt
@@ -43,6 +43,35 @@ class LuaError : RuntimeException {
internal var traceback: String? = null
+ /**
+ * True once the error has been given the place it was raised.
+ *
+ * Only the first function the error unwinds through decides that, since it
+ * is the only one still standing on the whole stack the level counts
+ * from; every function after it would answer with itself.
+ */
+ internal var positioned: Boolean = false
+
+ /**
+ * True when this error is to carry no place at all.
+ *
+ * Lua points at the line that called the function which raised, and where
+ * that caller is not written in Lua there is no line to point at: an
+ * error raised in a function a library called stands on its own.
+ */
+ internal var nowhere: Boolean = false
+
+ /**
+ * The stack this error was raised on, one entry per frame.
+ *
+ * A coroutine keeps the stack it died on so a later
+ * `debug.traceback(co)` can still show it; by the time the error reaches
+ * whoever resumed the coroutine the frames themselves are gone, so they
+ * are written down here on the way out. Only errors leaving a coroutine
+ * carry this: nothing can look at the main thread's stack after the fact.
+ */
+ internal var luastack: List? = null
+
/**
* Get the cause, if any.
*/
@@ -68,10 +97,24 @@ class LuaError : RuntimeException {
if (traceback != null) return traceback
val m: String? = argMessageOverride ?: super.message
if (m == null) return null
- if (fileline != null) return fileline.toString() + " " + m
+ // "chunk:line: message", the shape luaG_addinfo gives every
+ // positioned error; scripts match on the colon.
+ if (fileline != null) return fileline.toString() + ": " + m
return m
}
+ /**
+ * Replaces what this error carries with what a message handler answered.
+ *
+ * The handler's result is the error from here on, whatever its type, so
+ * nothing that described the old one is kept.
+ */
+ internal fun replaceMessage(value: LuaValue) {
+ this.`object` = value
+ this.argMessageOverride = null
+ this.fileline = null
+ }
+
val messageObject: LuaValue?
/** Get the LuaValue that was provided in the constructor, or
* a LuaString containing the message if it was a string error argument.
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaFunction.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaFunction.kt
index 854972f7..ca3d83b8 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaFunction.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaFunction.kt
@@ -34,6 +34,27 @@ package net.blueva.luak
*/
abstract
class LuaFunction : LuaValue() {
+ /** See [LuaValue.pinned]; a function can be a weak key. */
+ internal override var pinned: Any? = null
+
+ /**
+ * How many pieces of state this function carries: its upvalues.
+ *
+ * A Lua closure counts what it captured. A function of the library's own
+ * counts the state it was built with, which is none for most of them and
+ * one for the few - an iterator, a wrapper - that exist only to carry a
+ * position or a handle from one call to the next.
+ */
+ open fun nupvalues(): Int = 0
+
+ /**
+ * The state behind upvalue [n], counted from one.
+ *
+ * Only its identity is meant to be used: it is what `debug.upvalueid`
+ * answers with, so two functions can be found to share state.
+ */
+ open fun upvaluestate(n: Int): Any? = null
+
override fun type(): Int {
return TFUNCTION
}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaInteger.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaInteger.kt
index 83f4691a..eca4f6ed 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaInteger.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaInteger.kt
@@ -46,7 +46,7 @@ class LuaInteger
* @see LuaValue.valueOf
*/ internal constructor(
/** The value being held by this instance. */
- val v: Int
+ val v: Long
) : LuaNumber() {
override fun isint(): Boolean {
return true
@@ -65,7 +65,7 @@ class LuaInteger
}
override fun tochar(): Char {
- return v.toChar()
+ return v.toInt().toChar()
}
override fun todouble(): Double {
@@ -77,11 +77,11 @@ class LuaInteger
}
override fun toint(): Int {
- return v
+ return v.toInt()
}
override fun tolong(): Long {
- return v.toLong()
+ return v
}
override fun toshort(): Short {
@@ -93,7 +93,7 @@ class LuaInteger
}
override fun optint(defval: Int): Int {
- return v
+ return v.toInt()
}
override fun optinteger(defval: LuaInteger?): LuaInteger {
@@ -101,7 +101,7 @@ class LuaInteger
}
override fun optlong(defval: Long): Long {
- return v.toLong()
+ return v
}
override fun tojstring(): String {
@@ -133,12 +133,12 @@ class LuaInteger
}
override fun hashCode(): Int {
- return v
+ return net.blueva.luak.LuaInteger.Companion.hashCode(v)
}
// unary operators
override fun neg(): LuaValue {
- return (net.blueva.luak.LuaInteger.Companion.valueOf(-v.toLong()))!!
+ return (net.blueva.luak.LuaInteger.Companion.valueOf(-v))!!
}
// object equality, used for key comparison
@@ -164,11 +164,10 @@ class LuaInteger
}
override fun raweq(`val`: Double): Boolean {
- val `val` = `val`!!
- return v.toDouble() == `val`
+ return luaIntegerEqualsFloat(v, `val`)
}
- override fun raweq(`val`: Int): Boolean {
+ override fun raweq(`val`: Long): Boolean {
val `val` = `val`!!
return v == `val`
}
@@ -182,7 +181,7 @@ class LuaInteger
return (LuaDouble.valueOf(lhs + v))!!
}
- override fun add(lhs: Int): LuaValue {
+ override fun add(lhs: Long): LuaValue {
return (net.blueva.luak.LuaInteger.Companion.valueOf(lhs + v.toLong()))!!
}
@@ -194,15 +193,15 @@ class LuaInteger
return (LuaDouble.valueOf(v - rhs))!!
}
- override fun sub(rhs: Int): LuaValue {
- return (LuaDouble.valueOf((v - rhs).toDouble()))!!
+ override fun sub(rhs: Long): LuaValue {
+ return (net.blueva.luak.LuaInteger.Companion.valueOf(v - rhs))!!
}
override fun subFrom(lhs: Double): LuaValue {
return (LuaDouble.valueOf(lhs - v))!!
}
- override fun subFrom(lhs: Int): LuaValue {
+ override fun subFrom(lhs: Long): LuaValue {
return (net.blueva.luak.LuaInteger.Companion.valueOf(lhs - v.toLong()))!!
}
@@ -214,7 +213,7 @@ class LuaInteger
return (LuaDouble.valueOf(lhs * v))!!
}
- override fun mul(lhs: Int): LuaValue {
+ override fun mul(lhs: Long): LuaValue {
return (net.blueva.luak.LuaInteger.Companion.valueOf(lhs * v.toLong()))!!
}
@@ -226,7 +225,7 @@ class LuaInteger
return MathLib.dpow((v).toDouble(), rhs)
}
- override fun pow(rhs: Int): LuaValue {
+ override fun pow(rhs: Long): LuaValue {
return MathLib.dpow((v).toDouble(), (rhs).toDouble())
}
@@ -234,19 +233,50 @@ class LuaInteger
return MathLib.dpow(lhs, (v).toDouble())
}
- override fun powWith(lhs: Int): LuaValue {
+ override fun powWith(lhs: Long): LuaValue {
return MathLib.dpow((lhs).toDouble(), (v).toDouble())
}
+ override fun band(rhs: LuaValue): LuaValue = bitwise(net.blueva.luak.LuaValue.Companion.BAND, rhs)
+ override fun bor(rhs: LuaValue): LuaValue = bitwise(net.blueva.luak.LuaValue.Companion.BOR, rhs)
+ override fun bxor(rhs: LuaValue): LuaValue = bitwise(net.blueva.luak.LuaValue.Companion.BXOR, rhs)
+ override fun shl(rhs: LuaValue): LuaValue = bitwise(net.blueva.luak.LuaValue.Companion.SHL, rhs)
+ override fun shr(rhs: LuaValue): LuaValue = bitwise(net.blueva.luak.LuaValue.Companion.SHR, rhs)
+
+ override fun bnot(): LuaValue = LuaValue.valueOf(luaBitwiseOperand(this).inv())
+
+ private fun bitwise(tag: LuaString, rhs: LuaValue): LuaValue {
+ if (!rhs.isnumber() || rhs is LuaString) return arithmt(tag, rhs)
+ val x: Long = luaBitwiseOperand(this)
+ val y: Long = luaBitwiseOperand(rhs)
+ return when (tag) {
+ net.blueva.luak.LuaValue.Companion.BAND -> LuaValue.valueOf(x and y)
+ net.blueva.luak.LuaValue.Companion.BOR -> LuaValue.valueOf(x or y)
+ net.blueva.luak.LuaValue.Companion.BXOR -> LuaValue.valueOf(x xor y)
+ net.blueva.luak.LuaValue.Companion.SHL -> LuaValue.valueOf(luaShiftLeft(x, y))
+ else -> LuaValue.valueOf(luaShiftLeft(x, -y))
+ }
+ }
+
+ override fun idiv(rhs: LuaValue): LuaValue {
+ val other: LuaValue = rhs.tonumber()
+ if (other.isnil()) return arithmt(net.blueva.luak.LuaValue.Companion.IDIV, rhs)
+ return luaFloorDiv(this, other)
+ }
+
override fun div(rhs: LuaValue): LuaValue {
- return rhs.divInto((v).toDouble())
+ // Read as a number first so a metamethod on the other side is handed
+ // this operand as the integer it is, rather than a float of it.
+ val other: LuaValue = rhs.tonumber()
+ if (other.isnil()) return arithmt(net.blueva.luak.LuaValue.Companion.DIV, rhs)
+ return (LuaDouble.ddiv((v).toDouble(), other.todouble()))!!
}
override fun div(rhs: Double): LuaValue {
return (LuaDouble.ddiv((v).toDouble(), rhs))!!
}
- override fun div(rhs: Int): LuaValue {
+ override fun div(rhs: Long): LuaValue {
return (LuaDouble.ddiv((v).toDouble(), (rhs).toDouble()))!!
}
@@ -255,15 +285,17 @@ class LuaInteger
}
override fun mod(rhs: LuaValue): LuaValue {
- return rhs.modFrom((v).toDouble())
+ val other: LuaValue = rhs.tonumber()
+ if (other.isnil()) return arithmt(net.blueva.luak.LuaValue.Companion.MOD, rhs)
+ return luaMod(this, other)
}
override fun mod(rhs: Double): LuaValue {
return (LuaDouble.dmod((v).toDouble(), rhs))!!
}
- override fun mod(rhs: Int): LuaValue {
- return (LuaDouble.dmod((v).toDouble(), (rhs).toDouble()))!!
+ override fun mod(rhs: Long): LuaValue {
+ return (net.blueva.luak.LuaInteger.Companion.valueOf(luaIntegerMod(v, rhs)))!!
}
override fun modFrom(lhs: Double): LuaValue {
@@ -276,10 +308,10 @@ class LuaInteger
}
override fun lt(rhs: Double): LuaValue {
- return (if (v < rhs) TRUE else FALSE)!!
+ return (if (luaIntegerLessThanFloat(v, rhs)) TRUE else FALSE)!!
}
- override fun lt(rhs: Int): LuaValue {
+ override fun lt(rhs: Long): LuaValue {
return (if (v < rhs) TRUE else FALSE)!!
}
@@ -287,12 +319,12 @@ class LuaInteger
return if (rhs is LuaNumber) rhs.gt_b(v) else super.lt_b(rhs)
}
- override fun lt_b(rhs: Int): Boolean {
+ override fun lt_b(rhs: Long): Boolean {
return v < rhs
}
override fun lt_b(rhs: Double): Boolean {
- return v < rhs
+ return luaIntegerLessThanFloat(v, rhs)
}
override fun lteq(rhs: LuaValue): LuaValue {
@@ -300,10 +332,10 @@ class LuaInteger
}
override fun lteq(rhs: Double): LuaValue {
- return (if (v <= rhs) TRUE else FALSE)!!
+ return (if (luaIntegerLessOrEqualFloat(v, rhs)) TRUE else FALSE)!!
}
- override fun lteq(rhs: Int): LuaValue {
+ override fun lteq(rhs: Long): LuaValue {
return (if (v <= rhs) TRUE else FALSE)!!
}
@@ -311,12 +343,12 @@ class LuaInteger
return if (rhs is LuaNumber) rhs.gteq_b(v) else super.lteq_b(rhs)
}
- override fun lteq_b(rhs: Int): Boolean {
+ override fun lteq_b(rhs: Long): Boolean {
return v <= rhs
}
override fun lteq_b(rhs: Double): Boolean {
- return v <= rhs
+ return luaIntegerLessOrEqualFloat(v, rhs)
}
override fun gt(rhs: LuaValue): LuaValue {
@@ -324,10 +356,10 @@ class LuaInteger
}
override fun gt(rhs: Double): LuaValue {
- return (if (v > rhs) TRUE else FALSE)!!
+ return (if (luaFloatLessThanInteger(rhs, v)) TRUE else FALSE)!!
}
- override fun gt(rhs: Int): LuaValue {
+ override fun gt(rhs: Long): LuaValue {
return (if (v > rhs) TRUE else FALSE)!!
}
@@ -335,12 +367,12 @@ class LuaInteger
return if (rhs is LuaNumber) rhs.lt_b(v) else super.gt_b(rhs)
}
- override fun gt_b(rhs: Int): Boolean {
+ override fun gt_b(rhs: Long): Boolean {
return v > rhs
}
override fun gt_b(rhs: Double): Boolean {
- return v > rhs
+ return luaFloatLessThanInteger(rhs, v)
}
override fun gteq(rhs: LuaValue): LuaValue {
@@ -348,10 +380,10 @@ class LuaInteger
}
override fun gteq(rhs: Double): LuaValue {
- return (if (v >= rhs) TRUE else FALSE)!!
+ return (if (luaFloatLessOrEqualInteger(rhs, v)) TRUE else FALSE)!!
}
- override fun gteq(rhs: Int): LuaValue {
+ override fun gteq(rhs: Long): LuaValue {
return (if (v >= rhs) TRUE else FALSE)!!
}
@@ -359,12 +391,12 @@ class LuaInteger
return if (rhs is LuaNumber) rhs.lteq_b(v) else super.gteq_b(rhs)
}
- override fun gteq_b(rhs: Int): Boolean {
+ override fun gteq_b(rhs: Long): Boolean {
return v >= rhs
}
override fun gteq_b(rhs: Double): Boolean {
- return v >= rhs
+ return luaFloatLessOrEqualInteger(rhs, v)
}
// string comparison
@@ -374,7 +406,7 @@ class LuaInteger
}
override fun checkint(): Int {
- return v
+ return v.toInt()
}
override fun checklong(): Long {
@@ -394,29 +426,38 @@ class LuaInteger
}
companion object {
+ private const val CACHE_LOW = -256L
+ private const val CACHE_HIGH = 255L
private val intValues = arrayOfNulls(512)
init {
- for (i in 0..511) net.blueva.luak.LuaInteger.Companion.intValues[i] = net.blueva.luak.LuaInteger(i - 256)
+ for (i in 0..511) {
+ net.blueva.luak.LuaInteger.Companion.intValues[i] = net.blueva.luak.LuaInteger((i - 256).toLong())
+ }
}
- fun valueOf(i: Int): LuaInteger? {
- return if (i <= 255 && i >= -256) net.blueva.luak.LuaInteger.Companion.intValues[i + 256] else net.blueva.luak.LuaInteger(
- i
- )
- } // TODO consider moving this to LuaValue
+ fun valueOf(i: Int): LuaInteger? =
+ net.blueva.luak.LuaInteger.Companion.valueOf(i.toLong())
- /** Return a LuaNumber that represents the value provided
+ /** Return the LuaInteger that represents the value provided.
+ *
+ * Since Lua 5.3 the integer subtype is 64 bits wide, so every [Long] is
+ * representable and none of them degrade to a float.
+ *
* @param l long value to represent.
- * @return LuaNumber that is eithe LuaInteger or LuaDouble representing l
- * @see LuaValue.valueOf
+ * @return LuaInteger representing l
* @see LuaValue.valueOf
*/
- fun valueOf(l: Long): LuaNumber? {
- val i = l.toInt()
- return if (l == i.toLong()) (if (i <= 255 && i >= -256) net.blueva.luak.LuaInteger.Companion.intValues[i + 256] else net.blueva.luak.LuaInteger(
- i
- ) as LuaNumber) else LuaDouble.valueOf((l).toDouble()) as LuaNumber?
+ fun valueOf(l: Long): LuaInteger? {
+ if (l in CACHE_LOW..CACHE_HIGH) {
+ return net.blueva.luak.LuaInteger.Companion.intValues[(l - CACHE_LOW).toInt()]
+ }
+ return net.blueva.luak.LuaInteger(l)
+ }
+
+ /** Hash of an integer key, matching what [LuaInteger.hashCode] produces. */
+ fun hashCode(x: Long): Int {
+ return x.toInt()
}
fun hashCode(x: Int): Int {
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaLightUserdata.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaLightUserdata.kt
new file mode 100644
index 00000000..949c7bd0
--- /dev/null
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaLightUserdata.kt
@@ -0,0 +1,46 @@
+/******************************************************************************
+ * ____ _ _ _ __
+ * | __ )| |_ _ ___| | _ _ __ _| |/ /
+ * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
+ * | |_) | | |_| | __/ |__| |_| | (_| | . \
+ * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
+ *
+ * BlueLuaK
+ * https://github.com/BluevaDevelopment/BlueLuaK
+ *
+ * Copyright (c) 2026 Blueva Development
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+package net.blueva.luak
+
+/**
+ * A bare host reference: Lua's light userdata.
+ *
+ * Unlike a full userdata it is only an identity - no metatable of its own and
+ * no value attached to it - which is what `debug.upvalueid` hands back so that
+ * two upvalues can be told apart or found to be the same one. `type()` reports
+ * it as `userdata`, as Lua does; only an argument error names it as light,
+ * which is how a script finds out it was given one where a full userdata was
+ * wanted.
+ */
+class LuaLightUserdata(obj: Any) : LuaUserdata(obj) {
+ override fun tojstring(): String = "userdata: 0x" + m_instance.hashCode().toString(16)
+
+ override fun getmetatable(): LuaValue? = null
+
+ override fun setmetatable(metatable: LuaValue?): LuaValue? {
+ LuaValue.error("cannot change a light userdata's metatable")
+ return null
+ }
+
+ // Identity, not equality: two light userdata are the same one only when
+ // they point at the same thing.
+ override fun raweq(`val`: LuaValue?): Boolean =
+ `val` is LuaLightUserdata && `val`.m_instance === m_instance
+
+ override fun raweq(`val`: LuaUserdata?): Boolean =
+ `val` is LuaLightUserdata && `val`.m_instance === m_instance
+
+ override fun hashCode(): Int = m_instance.hashCode()
+}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaNumber.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaNumber.kt
index 0e993831..6b490c3d 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaNumber.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaNumber.kt
@@ -87,3 +87,168 @@ class LuaNumber : LuaValue() {
var s_metatable: LuaValue? = null
}
}
+/**
+ * `//` over two values already known to be numbers.
+ *
+ * Two integers floor-divide as integers (rounding towards negative infinity,
+ * and wrapping for `mininteger // -1` exactly as the reference does); anything
+ * else is computed as a float. Kept next to the number types because
+ * [LuaInteger], [LuaDouble], and [LuaString] all need the same answer.
+ */
+/**
+ * Lua's `%`, which is a floored modulo rather than C's truncated one.
+ *
+ * Two integers give an integer; anything else gives a float. The sign of the
+ * result follows the divisor, so `-5 % 3` is `1`, not `-2`.
+ */
+internal fun luaMod(lhs: LuaValue, rhs: LuaValue): LuaValue {
+ if (lhs.isinttype() && rhs.isinttype()) {
+ return LuaValue.valueOf(luaIntegerMod(lhs.tolong(), rhs.tolong()))
+ }
+ return LuaValue.valueOf(luaFloatMod(lhs.todouble(), rhs.todouble()))
+}
+
+/** Floored modulo of two integers, raising on a zero divisor as Lua does. */
+internal fun luaIntegerMod(x: Long, y: Long): Long {
+ if (y == 0L) LuaValue.error("attempt to perform 'n%0'")
+ if (y == -1L) return 0L // avoids overflow on the minimum integer
+ val remainder = x % y
+ // C truncates towards zero, so a remainder whose sign disagrees with the
+ // divisor is one divisor short of the floored answer.
+ return if (remainder != 0L && (remainder xor y) < 0L) remainder + y else remainder
+}
+
+/** Floored modulo of two floats, matching upstream's `luai_nummod`. */
+internal fun luaFloatMod(x: Double, y: Double): Double {
+ var remainder = x % y
+ if (if (remainder > 0) y < 0 else (remainder < 0 && y > 0)) remainder += y
+ return remainder
+}
+
+internal fun luaFloorDiv(lhs: LuaValue, rhs: LuaValue): LuaValue {
+ if (lhs.isinttype() && rhs.isinttype()) {
+ val x: Long = lhs.tolong()
+ val y: Long = rhs.tolong()
+ if (y == 0L) LuaValue.error("attempt to divide by zero")
+ if (y == -1L) return LuaValue.valueOf(-x) // avoids overflow on mininteger
+ var quotient = x / y
+ if ((x xor y) < 0L && quotient * y != x) quotient--
+ return LuaValue.valueOf(quotient)
+ }
+ return LuaValue.valueOf(kotlin.math.floor(lhs.todouble() / rhs.todouble()))
+}
+
+/**
+ * The integer a bitwise operand denotes, or an error if it denotes none.
+ *
+ * Since 5.3 the bitwise operators work on 64-bit integers. A float is accepted
+ * when its value is integral (`3.0 & 1` is fine) and rejected otherwise; unlike
+ * the arithmetic operators, strings are not coerced, a restriction 5.4 made
+ * explicit.
+ */
+internal fun luaBitwiseOperand(value: LuaValue): Long {
+ if (value.isinttype()) return value.tolong()
+ if (value.isnumber() && value !is LuaString) {
+ val asDouble: Double = value.todouble()
+ if (fitsInteger(asDouble)) return asDouble.toLong()
+ LuaValue.error("number has no integer representation")
+ }
+ LuaValue.error("attempt to perform bitwise operation on a " + value.objtypename() + " value")
+ return 0L
+}
+
+/**
+ * `x << y`, with `x >> y` expressed as `luaShiftLeft(x, -y)`.
+ *
+ * A shift of 64 bits or more clears the value, and a negative count reverses
+ * the direction. Right shifts are logical, not arithmetic, which is why
+ * `-1 >> 1` is `maxinteger` rather than `-1`.
+ */
+internal fun luaShiftLeft(x: Long, y: Long): Long {
+ if (y < 0L) {
+ if (y <= -64L) return 0L
+ return x ushr (-y).toInt()
+ }
+ if (y >= 64L) return 0L
+ return x shl y.toInt()
+}
+
+/**
+ * Whether [value] denotes an integer, without raising if it does not.
+ *
+ * The compiler needs this to decide whether a bitwise expression can be folded:
+ * `1.5 & 1` must compile and fail at run time, where `pcall` can see it, rather
+ * than failing the compilation.
+ */
+internal fun luaHasIntegerRepresentation(value: LuaValue): Boolean {
+ if (value.isinttype()) return true
+ if (!value.isnumber() || value is LuaString) return false
+ return fitsInteger(value.todouble())
+}
+
+/**
+ * True when [value] is exactly some 64-bit integer.
+ *
+ * The range has to be checked as well as the round trip: converting a double
+ * outside it saturates at the nearest end, and converting that back lands on
+ * the same double again, so a round trip alone would accept `2^63`.
+ */
+private fun fitsInteger(value: Double): Boolean {
+ if (value < -9.2233720368547758E18 || value >= 9.2233720368547758E18) return false
+ return value.toLong().toDouble() == value
+}
+
+/**
+ * Whether the integer [i] and the float [f] denote the same number.
+ *
+ * Going through `i.toDouble()` would be wrong past 2^53, where distinct
+ * integers share a double; the comparison is done in integer space instead,
+ * which is exact for every value a Long can hold.
+ */
+internal fun luaIntegerEqualsFloat(i: Long, f: Double): Boolean {
+ if (f.isNaN() || f.isInfinite()) return false
+ if (f != kotlin.math.floor(f)) return false
+ // 2^63 is the first float above the integer range; -2^63 is representable.
+ if (f < -9.2233720368547758E18 || f >= 9.2233720368547758E18) return false
+ return f.toLong() == i
+}
+
+/**
+ * Exact ordering between the two number subtypes.
+ *
+ * Converting the integer to a double first would be wrong past 2^53, where the
+ * conversion rounds; comparing against the float's floor or ceiling keeps every
+ * case exact. Which of the two to use differs per operator: `i < f` holds when
+ * `i` is below the smallest integer at or above `f`, while `i <= f` holds when
+ * `i` is at most the largest integer at or below it.
+ */
+internal fun luaIntegerLessThanFloat(i: Long, f: Double): Boolean {
+ if (f.isNaN()) return false
+ if (f >= 9.2233720368547758E18) return true // above every representable integer
+ if (f < -9.2233720368547758E18) return false
+ return i < kotlin.math.ceil(f).toLong() || (kotlin.math.ceil(f) != f && i == kotlin.math.floor(f).toLong())
+}
+
+/** Whether the integer [i] is less than or equal to the float [f], exactly. */
+internal fun luaIntegerLessOrEqualFloat(i: Long, f: Double): Boolean {
+ if (f.isNaN()) return false
+ if (f >= 9.2233720368547758E18) return true
+ if (f < -9.2233720368547758E18) return false
+ return i <= kotlin.math.floor(f).toLong()
+}
+
+/** Whether the float [f] is strictly less than the integer [i], exactly. */
+internal fun luaFloatLessThanInteger(f: Double, i: Long): Boolean {
+ if (f.isNaN()) return false
+ if (f >= 9.2233720368547758E18) return false
+ if (f < -9.2233720368547758E18) return true
+ return kotlin.math.floor(f).toLong() < i
+}
+
+/** Whether the float [f] is less than or equal to the integer [i], exactly. */
+internal fun luaFloatLessOrEqualInteger(f: Double, i: Long): Boolean {
+ if (f.isNaN()) return false
+ if (f >= 9.2233720368547758E18) return false
+ if (f < -9.2233720368547758E18) return true
+ return kotlin.math.ceil(f).toLong() <= i
+}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaString.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaString.kt
index 6f06f42f..de2c032f 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaString.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaString.kt
@@ -96,6 +96,7 @@ class LuaString private constructor(
*/
init {
this.m_hashcode = net.blueva.luak.LuaString.Companion.hashCode(m_bytes, m_offset, m_length)
+ Memory.account(Memory.STRING + m_length)
}
override fun isstring(): Boolean {
@@ -118,181 +119,264 @@ class LuaString private constructor(
return net.blueva.luak.LuaString.Companion.decodeAsUtf8(m_bytes, m_offset, m_length)
}
+ /**
+ * The numeral this string denotes, for use in arithmetic.
+ *
+ * A string operand is converted to a number and the operation is then
+ * redone on that number, rather than on a double standing in for it. That
+ * keeps the subtype: `"10" + 5` is the integer `15`, while `"10.0" + 5` is
+ * the float `15.0`.
+ *
+ * A string that is not a numeral answers `nil`, and the caller hands the
+ * operation to the metatable, where `StringLib` has registered handlers
+ * that report the failure the way Lua does and give the other operand's
+ * own metamethod a turn.
+ */
+ private fun arithNumeral(): LuaValue = tonumber()
+
// unary operators
override fun neg(): LuaValue {
- val d = scannumber()
- return if ((d).isNaN()) super.neg() else valueOf(-d)
+ val numeral: LuaValue = tonumber()
+ return if (numeral.isnil()) super.neg() else numeral.neg()
}
// basic binary arithmetic
override fun add(rhs: LuaValue): LuaValue {
- val d = scannumber()
- return if ((d).isNaN()) arithmt(ADD, rhs) else rhs.add(d)
+ // Both operands have to be numerals for the shortcut. If the other one
+ // is not, the metatable handler takes over: it is the one that knows
+ // how to name both types in the error and how to offer the other
+ // operand its own metamethod.
+ val numeral: LuaValue = tonumber()
+ return if (numeral.isnil() || rhs.tonumber().isnil()) arithmt(ADD, rhs) else numeral.add(rhs)
}
override fun add(rhs: Double): LuaValue {
- return valueOf(checkarith() + rhs)
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmtwith(ADD, (rhs).toDouble()) else numeral.add(rhs)
}
- override fun add(rhs: Int): LuaValue {
- return valueOf(checkarith() + rhs)
+ override fun add(rhs: Long): LuaValue {
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmtwith(ADD, (rhs).toDouble()) else numeral.add(rhs)
}
override fun sub(rhs: LuaValue): LuaValue {
- val d = scannumber()
- return if ((d).isNaN()) arithmt(SUB, rhs) else rhs.subFrom(d)
+ // Both operands have to be numerals for the shortcut. If the other one
+ // is not, the metatable handler takes over: it is the one that knows
+ // how to name both types in the error and how to offer the other
+ // operand its own metamethod.
+ val numeral: LuaValue = tonumber()
+ return if (numeral.isnil() || rhs.tonumber().isnil()) arithmt(SUB, rhs) else numeral.sub(rhs)
}
- override fun sub(rhs: Double): LuaValue? {
- return valueOf(checkarith() - rhs)
+ override fun sub(rhs: Double): LuaValue {
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmt(SUB, valueOf(rhs)) else numeral.sub(rhs)!!
}
- override fun sub(rhs: Int): LuaValue? {
- return valueOf(checkarith() - rhs)
+ override fun sub(rhs: Long): LuaValue {
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmt(SUB, valueOf(rhs)) else numeral.sub(rhs)!!
}
override fun subFrom(lhs: Double): LuaValue {
- return valueOf(lhs - checkarith())
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmtwith(SUB, (lhs).toDouble()) else valueOf(lhs).sub(numeral)
+ }
+
+ override fun subFrom(lhs: Long): LuaValue {
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmtwith(SUB, (lhs).toDouble()) else valueOf(lhs).sub(numeral)
}
override fun mul(rhs: LuaValue): LuaValue {
- val d = scannumber()
- return if ((d).isNaN()) arithmt(MUL, rhs) else rhs.mul(d)
+ // Both operands have to be numerals for the shortcut. If the other one
+ // is not, the metatable handler takes over: it is the one that knows
+ // how to name both types in the error and how to offer the other
+ // operand its own metamethod.
+ val numeral: LuaValue = tonumber()
+ return if (numeral.isnil() || rhs.tonumber().isnil()) arithmt(MUL, rhs) else numeral.mul(rhs)
}
override fun mul(rhs: Double): LuaValue {
- return valueOf(checkarith() * rhs)
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmtwith(MUL, (rhs).toDouble()) else numeral.mul(rhs)
}
- override fun mul(rhs: Int): LuaValue {
- return valueOf(checkarith() * rhs)
+ override fun mul(rhs: Long): LuaValue {
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmtwith(MUL, (rhs).toDouble()) else numeral.mul(rhs)
}
override fun pow(rhs: LuaValue): LuaValue {
- val d = scannumber()
- return if ((d).isNaN()) arithmt(POW, rhs) else rhs.powWith(d)
+ // Both operands have to be numerals for the shortcut. If the other one
+ // is not, the metatable handler takes over: it is the one that knows
+ // how to name both types in the error and how to offer the other
+ // operand its own metamethod.
+ val numeral: LuaValue = tonumber()
+ return if (numeral.isnil() || rhs.tonumber().isnil()) arithmt(POW, rhs) else numeral.pow(rhs)
}
override fun pow(rhs: Double): LuaValue {
- return MathLib.dpow(checkarith(), rhs)
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmt(POW, valueOf(rhs)) else numeral.pow(rhs)!!
}
- override fun pow(rhs: Int): LuaValue {
- return MathLib.dpow(checkarith(), (rhs).toDouble())
+ override fun pow(rhs: Long): LuaValue {
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmt(POW, valueOf(rhs)) else numeral.pow(rhs)!!
}
override fun powWith(lhs: Double): LuaValue {
- return MathLib.dpow(lhs, checkarith())
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmtwith(POW, (lhs).toDouble()) else valueOf(lhs).pow(numeral)
}
- override fun powWith(lhs: Int): LuaValue {
- return MathLib.dpow((lhs).toDouble(), checkarith())
+ override fun powWith(lhs: Long): LuaValue {
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmtwith(POW, (lhs).toDouble()) else valueOf(lhs).pow(numeral)
}
override fun div(rhs: LuaValue): LuaValue {
- val d = scannumber()
- return if ((d).isNaN()) arithmt(DIV, rhs) else rhs.divInto(d)
+ // Both operands have to be numerals for the shortcut. If the other one
+ // is not, the metatable handler takes over: it is the one that knows
+ // how to name both types in the error and how to offer the other
+ // operand its own metamethod.
+ val numeral: LuaValue = tonumber()
+ return if (numeral.isnil() || rhs.tonumber().isnil()) arithmt(DIV, rhs) else numeral.div(rhs)
}
override fun div(rhs: Double): LuaValue {
- return (LuaDouble.ddiv(checkarith(), rhs))!!
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmt(DIV, valueOf(rhs)) else numeral.div(rhs)!!
}
- override fun div(rhs: Int): LuaValue {
- return (LuaDouble.ddiv(checkarith(), (rhs).toDouble()))!!
+ override fun div(rhs: Long): LuaValue {
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmt(DIV, valueOf(rhs)) else numeral.div(rhs)!!
}
override fun divInto(lhs: Double): LuaValue {
- return (LuaDouble.ddiv(lhs, checkarith()))!!
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmtwith(DIV, (lhs).toDouble()) else valueOf(lhs).div(numeral)
+ }
+
+ // A string is coerced for arithmetic but never for a bitwise operation:
+ // upstream reads bitwise operands straight off the stack as integers, so a
+ // string there is an error rather than something to convert.
+ override fun idiv(rhs: LuaValue): LuaValue {
+ // Both operands have to be numerals for the shortcut. If the other one
+ // is not, the metatable handler takes over: it is the one that knows
+ // how to name both types in the error and how to offer the other
+ // operand its own metamethod.
+ val numeral: LuaValue = tonumber()
+ return if (numeral.isnil() || rhs.tonumber().isnil()) arithmt(IDIV, rhs) else numeral.idiv(rhs)
}
override fun mod(rhs: LuaValue): LuaValue {
- val d = scannumber()
- return if ((d).isNaN()) arithmt(MOD, rhs) else rhs.modFrom(d)
+ // Both operands have to be numerals for the shortcut. If the other one
+ // is not, the metatable handler takes over: it is the one that knows
+ // how to name both types in the error and how to offer the other
+ // operand its own metamethod.
+ val numeral: LuaValue = tonumber()
+ return if (numeral.isnil() || rhs.tonumber().isnil()) arithmt(MOD, rhs) else numeral.mod(rhs)
}
override fun mod(rhs: Double): LuaValue {
- return (LuaDouble.dmod(checkarith(), rhs))!!
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmt(MOD, valueOf(rhs)) else numeral.mod(rhs)!!
}
- override fun mod(rhs: Int): LuaValue {
- return (LuaDouble.dmod(checkarith(), (rhs).toDouble()))!!
+ override fun mod(rhs: Long): LuaValue {
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmt(MOD, valueOf(rhs)) else numeral.mod(rhs)!!
}
override fun modFrom(lhs: Double): LuaValue {
- return (LuaDouble.dmod(lhs, checkarith()))!!
+ val numeral: LuaValue = arithNumeral()
+ return if (numeral.isnil()) arithmtwith(MOD, (lhs).toDouble()) else valueOf(lhs).mod(numeral)
}
- // relational operators, these only work with other strings
+ // Relational operators only work between two strings: a number is a
+ // string as far as 'isstring' is concerned, but ordering one against a
+ // string is an error rather than a coercion.
override fun lt(rhs: LuaValue): LuaValue {
- return (if (rhs.isstring()) (if (rhs.strcmp(this) > 0) LuaValue.TRUE else FALSE) else super.lt(rhs))!!
+ return (if (rhs is LuaString) (if (rhs.strcmp(this) > 0) LuaValue.TRUE else FALSE) else super.lt(rhs))!!
}
override fun lt_b(rhs: LuaValue): Boolean {
- return if (rhs.isstring()) rhs.strcmp(this) > 0 else super.lt_b(rhs)
+ return if (rhs is LuaString) rhs.strcmp(this) > 0 else super.lt_b(rhs)
}
- override fun lt_b(rhs: Int): Boolean {
- typerror("attempt to compare string with number")
+ override fun lt_b(rhs: Long): Boolean {
+ LuaValue.error("attempt to compare string with number")
return false
}
override fun lt_b(rhs: Double): Boolean {
- typerror("attempt to compare string with number")
+ LuaValue.error("attempt to compare string with number")
return false
}
override fun lteq(rhs: LuaValue): LuaValue {
- return (if (rhs.isstring()) (if (rhs.strcmp(this) >= 0) LuaValue.TRUE else FALSE) else super.lteq(rhs))!!
+ return (if (rhs is LuaString) (if (rhs.strcmp(this) >= 0) LuaValue.TRUE else FALSE) else super.lteq(rhs))!!
}
override fun lteq_b(rhs: LuaValue): Boolean {
- return if (rhs.isstring()) rhs.strcmp(this) >= 0 else super.lteq_b(rhs)
+ return if (rhs is LuaString) rhs.strcmp(this) >= 0 else super.lteq_b(rhs)
}
- override fun lteq_b(rhs: Int): Boolean {
- typerror("attempt to compare string with number")
+ override fun lteq_b(rhs: Long): Boolean {
+ LuaValue.error("attempt to compare string with number")
return false
}
override fun lteq_b(rhs: Double): Boolean {
- typerror("attempt to compare string with number")
+ LuaValue.error("attempt to compare string with number")
return false
}
override fun gt(rhs: LuaValue): LuaValue {
- return (if (rhs.isstring()) (if (rhs.strcmp(this) < 0) LuaValue.TRUE else FALSE) else super.gt(rhs))!!
+ return (if (rhs is LuaString) (if (rhs.strcmp(this) < 0) LuaValue.TRUE else FALSE) else super.gt(rhs))!!
}
override fun gt_b(rhs: LuaValue): Boolean {
- return if (rhs.isstring()) rhs.strcmp(this) < 0 else super.gt_b(rhs)
+ return if (rhs is LuaString) rhs.strcmp(this) < 0 else super.gt_b(rhs)
}
- override fun gt_b(rhs: Int): Boolean {
- typerror("attempt to compare string with number")
+ override fun gt_b(rhs: Long): Boolean {
+ // The compiler turns 'a > b' into 'b < a', so the number is the one
+ // named first.
+ LuaValue.error("attempt to compare number with string")
return false
}
override fun gt_b(rhs: Double): Boolean {
- typerror("attempt to compare string with number")
+ // The compiler turns 'a > b' into 'b < a', so the number is the one
+ // named first.
+ LuaValue.error("attempt to compare number with string")
return false
}
override fun gteq(rhs: LuaValue): LuaValue {
- return (if (rhs.isstring()) (if (rhs.strcmp(this) <= 0) LuaValue.TRUE else FALSE) else super.gteq(rhs))!!
+ return (if (rhs is LuaString) (if (rhs.strcmp(this) <= 0) LuaValue.TRUE else FALSE) else super.gteq(rhs))!!
}
override fun gteq_b(rhs: LuaValue): Boolean {
- return if (rhs.isstring()) rhs.strcmp(this) <= 0 else super.gteq_b(rhs)
+ return if (rhs is LuaString) rhs.strcmp(this) <= 0 else super.gteq_b(rhs)
}
- override fun gteq_b(rhs: Int): Boolean {
- typerror("attempt to compare string with number")
+ override fun gteq_b(rhs: Long): Boolean {
+ // The compiler turns 'a > b' into 'b < a', so the number is the one
+ // named first.
+ LuaValue.error("attempt to compare number with string")
return false
}
override fun gteq_b(rhs: Double): Boolean {
- typerror("attempt to compare string with number")
+ // The compiler turns 'a > b' into 'b < a', so the number is the one
+ // named first.
+ LuaValue.error("attempt to compare number with string")
return false
}
@@ -343,16 +427,28 @@ class LuaString private constructor(
return d
}
+ /** The numeral this string denotes, or an argument error if it is none. */
+ private fun checknumeral(message: String?): LuaValue {
+ val numeral: LuaValue = tonumber()
+ if (numeral.isnil()) {
+ if (message == null) argerror("number") else error(message)
+ }
+ return numeral
+ }
+
override fun checkint(): Int {
- return checkdouble().toLong().toInt()
+ return checklong().toInt()
}
override fun checkinteger(): LuaInteger? {
- return valueOf(checkint())
+ return net.blueva.luak.LuaInteger.valueOf(checklong())
}
override fun checklong(): Long {
- return checkdouble().toLong()
+ // Through the numeral rather than through a double, so a 64-bit
+ // integer written out in full does not lose its low bits on the way,
+ // and a fractional one is rejected rather than truncated.
+ return checknumeral(null).checklong()
}
override fun checkdouble(): Double {
@@ -362,13 +458,11 @@ class LuaString private constructor(
}
override fun checknumber(): LuaNumber? {
- return valueOf(checkdouble())
+ return checknumeral(null) as LuaNumber
}
override fun checknumber(msg: String?): LuaNumber? {
- val d = scannumber()
- if ((d).isNaN()) error(msg)
- return valueOf(d)
+ return checknumeral(msg) as LuaNumber
}
override fun isnumber(): Boolean {
@@ -377,17 +471,18 @@ class LuaString private constructor(
}
override fun isint(): Boolean {
- val d = scannumber()
- if ((d).isNaN()) return false
- val i = d.toInt()
- return i.toDouble() == d
+ val numeral: LuaValue = tonumber()
+ if (numeral.isnil()) return false
+ val d: Double = numeral.todouble()
+ return d.toInt().toDouble() == d
}
override fun islong(): Boolean {
- val d = scannumber()
- if ((d).isNaN()) return false
- val l = d.toLong()
- return l.toDouble() == d
+ val numeral: LuaValue = tonumber()
+ if (numeral.isnil()) return false
+ if (numeral.isinttype()) return true
+ val d: Double = numeral.todouble()
+ return d.toLong().toDouble() == d
}
override fun tobyte(): Byte {
@@ -412,7 +507,7 @@ class LuaString private constructor(
}
override fun tolong(): Long {
- return todouble().toLong()
+ return (tonumber().takeUnless { it.isnil() } ?: return 0L).tolong()
}
override fun toshort(): Short {
@@ -666,8 +761,7 @@ class LuaString private constructor(
* @see LuaValue.tonumber
*/
override fun tonumber(): LuaValue {
- val d = scannumber()
- return if ((d).isNaN()) NIL else valueOf(d)
+ return scannumeral() ?: NIL
}
/**
@@ -677,8 +771,8 @@ class LuaString private constructor(
* @see LuaValue.tonumber
*/
fun tonumber(base: Int): LuaValue? {
- val d = scannumber(base)
- return if ((d).isNaN()) NIL else valueOf(d)
+ val value: Long = net.blueva.luak.NumberParser.parseInteger(tojstring(), base) ?: return NIL
+ return valueOf(value)
}
/**
@@ -687,81 +781,47 @@ class LuaString private constructor(
* @return double value if conversion is valid, or Double.NaN if not
*/
fun scannumber(): Double {
- var i = m_offset
- var j = m_offset + m_length
- while (i < j && m_bytes[i] == ' '.code.toByte()) ++i
- while (i < j && m_bytes[j - 1] == ' '.code.toByte()) --j
- if (i >= j) return Double.NaN
- if (m_bytes[i] == '0'.code.toByte() && i + 1 < j && (m_bytes[i + 1] == 'x'.code.toByte() || m_bytes[i + 1] == 'X'.code.toByte())) return scanlong(
- 16,
- i + 2,
- j
- )
- val l = scanlong(10, i, j)
- return if ((l).isNaN()) scandouble(i, j) else l
+ val numeral: LuaValue = scannumeral() ?: return Double.NaN
+ return numeral.todouble()
}
/**
- * Convert to a number in a base, or return Double.NaN if not a number.
- * @param base the base to use between 2 and 36
- * @return double value if conversion is valid, or Double.NaN if not
+ * The numeral this string denotes, keeping its subtype, or `null`.
+ *
+ * @return a [LuaInteger] or a [LuaDouble], or `null` if this is not a numeral
*/
- fun scannumber(base: Int): Double {
- if (base < 2 || base > 36) return Double.NaN
+ private fun scannumeral(): LuaValue? {
+ // Rule out the common non-numeric string before decoding it: a numeral
+ // can only start with a digit, a sign, or a decimal point.
var i = m_offset
- var j = m_offset + m_length
- while (i < j && m_bytes[i] == ' '.code.toByte()) ++i
- while (i < j && m_bytes[j - 1] == ' '.code.toByte()) --j
- if (i >= j) return Double.NaN
- return scanlong(base, i, j)
+ val end = m_offset + m_length
+ while (i < end && isSpaceByte(m_bytes[i])) ++i
+ if (i >= end) return null
+ val first = m_bytes[i].toInt()
+ val plausible = (first >= '0'.code && first <= '9'.code) ||
+ first == '-'.code || first == '+'.code || first == '.'.code
+ if (!plausible) return null
+ return net.blueva.luak.NumberParser.parse(tojstring())
}
- /**
- * Scan and convert a long value, or return Double.NaN if not found.
- * @param base the base to use, such as 10
- * @param start the index to start searching from
- * @param end the first index beyond the search range
- * @return double value if conversion is valid,
- * or Double.NaN if not
- */
- private fun scanlong(base: Int, start: Int, end: Int): Double {
- var x: Long = 0
- val neg = (m_bytes[start] == '-'.code.toByte())
- for (i in (if (neg) start + 1 else start)..= '0'.code.toByte() && m_bytes[i] <= '9'.code.toByte())) '0'.code else if (m_bytes[i] >= 'A'.code.toByte() && m_bytes[i] <= 'Z'.code.toByte()) ('A'.code - 10) else ('a'.code - 10))
- if (digit < 0 || digit >= base) return Double.NaN
- x = x * base + digit
- if (x < 0) return Double.NaN // overflow
- }
- return (if (neg) -x else x).toDouble()
+ private fun isSpaceByte(b: Byte): Boolean {
+ val c = b.toInt()
+ return c == ' '.code || c == 0x09 || c == 0x0A || c == 0x0B || c == 0x0C || c == 0x0D
}
/**
- * Scan and convert a double value, or return Double.NaN if not a double.
- * @param start the index to start searching from
- * @param end the first index beyond the search range
- * @return double value if conversion is valid,
- * or Double.NaN if not
+ * Convert to a number in a base, or return Double.NaN if not a number.
+ * @param base the base to use between 2 and 36
+ * @return double value if conversion is valid, or Double.NaN if not
*/
- private fun scandouble(start: Int, end: Int): Double {
- var end = end
- if (end > start + 64) end = start + 64
- for (i in start.. {}
- else -> return Double.NaN
- }
- }
- val c = CharArray(end - start)
- for (i in start..
@@ -78,6 +84,7 @@ open class LuaTable : LuaValue, Metatable {
constructor() {
array = NOVALS
hash = net.blueva.luak.LuaTable.Companion.NOBUCKETS
+ Memory.account(Memory.TABLE)
}
/**
@@ -156,12 +163,23 @@ open class LuaTable : LuaValue, Metatable {
}
override fun presize(narray: Int) {
- if (narray > array.size) array =
- net.blueva.luak.LuaTable.Companion.resize(array, 1 shl net.blueva.luak.LuaTable.Companion.log2(narray))
+ if (narray > MAX_PART) LuaValue.error("table overflow")
+ if (narray > array.size) {
+ val was: Int = array.size
+ array =
+ net.blueva.luak.LuaTable.Companion.resize(array, 1 shl net.blueva.luak.LuaTable.Companion.log2(narray))
+ Memory.account(Memory.SLOT * (array.size - was))
+ }
}
fun presize(narray: Int, nhash: Int) {
var nhash = nhash
+ // Rounded up to a power of two below, which is where a size close to
+ // the largest a host array can be would wrap around into a negative
+ // one. Lua refuses the same way.
+ if (narray > MAX_PART || nhash > MAX_PART) LuaValue.error("table overflow")
+ // Counted here rather than in each constructor, since every one of
+ // them that asks for room of its own comes through here.
if (nhash > 0 && nhash < net.blueva.luak.LuaTable.Companion.MIN_HASH_CAPACITY) nhash =
net.blueva.luak.LuaTable.Companion.MIN_HASH_CAPACITY
// Size of both parts must be a power of two.
@@ -170,6 +188,7 @@ open class LuaTable : LuaValue, Metatable {
hash =
(if (nhash > 0) arrayOfNulls(1 shl net.blueva.luak.LuaTable.Companion.log2(nhash)) else net.blueva.luak.LuaTable.Companion.NOBUCKETS)
hashEntries = 0
+ Memory.account(Memory.TABLE + Memory.SLOT * array.size + Memory.NODE * hash.size)
}
protected val arrayLengthValue: Int
@@ -197,12 +216,28 @@ open class LuaTable : LuaValue, Metatable {
if ((hadWeakKeys != (m_metatable != null && m_metatable!!.useWeakKeys())) ||
(hadWeakValues != (m_metatable != null && m_metatable!!.useWeakValues()))
) {
- // force a rehash
- rehash(0)
+ // How weakly an entry is held is decided when it is stored, so the
+ // entries already there have to go in again to be held the new way.
+ restore()
}
return this
}
+ /** Puts every entry back in, so each is held the way the metatable asks. */
+ private fun restore() {
+ val keys: ArrayList = ArrayList()
+ val values: ArrayList = ArrayList()
+ var entry: Varargs = next(NIL)
+ while (!entry.arg1()!!.isnil()) {
+ val key: LuaValue = entry.arg1()!!
+ keys.add(key)
+ values.add(entry.arg(2)!!)
+ entry = next(key)
+ }
+ presize(keys.size, keys.size)
+ for (index in keys.indices) rawset(keys[index], values[index])
+ }
+
override fun get(key: Int): LuaValue {
val v: LuaValue = rawget(key)
return if (v.isnil() && m_metatable != null) LuaValue.gettable(this, valueOf(key)) else v
@@ -217,13 +252,31 @@ open class LuaTable : LuaValue, Metatable {
val key = key!!
if (key > 0 && key <= array.size) {
val v: LuaValue? = if (m_metatable == null) array[key - 1] else m_metatable!!.arrayget(array, key - 1)
- return if (v != null) v else NIL
+ // An empty array slot is not proof the key is absent: growing the
+ // array part leaves earlier entries where they were, so the hash
+ // part still has to be asked.
+ if (v != null) return v
}
return hashget((LuaInteger.valueOf(key))!!)
}
+ /**
+ * The key a lookup should really use.
+ *
+ * A float whose value is integral names the same slot as that integer, so
+ * `t[2.0]` and `t[2]` are one entry. With the two subtypes now distinct
+ * this has to be done explicitly; before the split the value model folded
+ * them together on its own.
+ */
+ private fun normalizeKey(key: LuaValue): LuaValue {
+ if (key.type() != TNUMBER || key.isinttype()) return key
+ val asDouble: Double = key.todouble()
+ val asLong: Long = asDouble.toLong()
+ return if (asLong.toDouble() == asDouble && !asDouble.isInfinite()) valueOf(asLong) else key
+ }
+
override fun rawget(key: LuaValue?): LuaValue {
- val key = key!!
+ val key = normalizeKey(key!!)
if (key.isinttype()) {
val ikey: Int = key.toint()
if (ikey > 0 && ikey <= array.size) {
@@ -275,14 +328,23 @@ open class LuaTable : LuaValue, Metatable {
/** caller must ensure key is not nil */
override fun rawset(key: LuaValue?, value: LuaValue?) {
- val key = key!!
+ val key = normalizeKey(key!!)
val value = value!!
if (!key.isinttype() || !arrayset(key.toint(), value)) hashset(key, value)
}
- /** Set an array element */
+ /**
+ * Sets an array element.
+ *
+ * @return false when the key is not one the array part holds, so the
+ * caller has to go to the hash part instead. Erasing a slot that is
+ * already empty counts as that: the key may be sitting in the hash with
+ * an index the array part happens to cover, and that is where it has to
+ * be removed from.
+ */
private fun arrayset(key: Int, value: LuaValue): Boolean {
if (key > 0 && key <= array.size) {
+ if (value.isnil() && array[key - 1] == null) return false
array[key - 1] = if (value.isnil()) null else (if (m_metatable != null) m_metatable!!.wrap(value) else value)
return true
}
@@ -298,14 +360,16 @@ open class LuaTable : LuaValue, Metatable {
var pos = pos
val n = length()
if (pos == 0) pos = n
- else if (pos > n) return NONE
+ // Removing at #t+1 is allowed and answers what was there, which is
+ // nil - a value, not an absence, so the caller still gets one result.
+ else if (pos > n) return get(pos)
val v: LuaValue = get(pos)
var r: LuaValue = v
while (!r.isnil()) {
r = get(pos + 1)
set(pos++, r)
}
- return if (v.isnil()) NONE else v
+ return v
}
/** Insert an element at a position in a list-table
@@ -355,7 +419,8 @@ open class LuaTable : LuaValue, Metatable {
override fun len(): LuaValue {
val h: LuaValue = metatag(LEN)
- if (h.toboolean()) return h.call(this)!!
+ // Lua hands a unary operator its operand twice.
+ if (h.toboolean()) return h.call(this, this)!!
return (LuaInteger.valueOf(rawlen()))!!
}
@@ -697,9 +762,15 @@ open class LuaTable : LuaValue, Metatable {
}
}
+ val wasarray: Int = array.size
+ val washash: Int = hash.size
hash = newHash
array = newArray
hashEntries -= movingToArray
+ // Only what the table grew by: what it gave up is for the host to
+ // reclaim, and the tally does not go down until a collection ends.
+ if (array.size > wasarray) Memory.account(Memory.SLOT * (array.size - wasarray))
+ if (hash.size > washash) Memory.account(Memory.NODE * (hash.size - washash))
}
override fun entry(key: LuaValue?, value: LuaValue?): Slot? {
@@ -715,50 +786,104 @@ open class LuaTable : LuaValue, Metatable {
/** Sort the table using a comparator.
* @param comparator [LuaValue] to be called to compare elements.
*/
- fun sort(comparator: LuaValue) {
+ @kotlin.jvm.JvmOverloads
+ fun sort(comparator: LuaValue, debuglib: net.blueva.luak.lib.DebugLib? = null) {
if (len().tolong() >= Int.MAX_VALUE.toLong()) throw LuaError("array too big: " + len().tolong())
if (m_metatable != null && m_metatable!!.useWeakValues()) {
dropWeakArrayValues()
}
val n = length()
- if (n > 1) heapSort(n, if (comparator.isnil()) null else comparator)
+ if (n > 1) auxsort(1, n, if (comparator.isnil()) null else comparator, debuglib)
}
- private fun heapSort(count: Int, cmpfunc: LuaValue?) {
- heapify(count, cmpfunc)
- var end = count
- while (end > 1) {
- val a: LuaValue = get(end) // swap(end, 1)
- set(end, get(1))
- set(1, a)
- siftDown(1, --end, cmpfunc)
+ /**
+ * The quicksort Lua sorts with, over `1..n` of this table.
+ *
+ * Written as Lua writes it, down to the median of three it takes its
+ * pivot from and the two ends it walks towards each other, because that
+ * is what lets it notice an order function that contradicts itself: a
+ * walk that runs past the pivot can only mean the answers it was given
+ * cannot all be true, and Lua says so rather than reading past the part
+ * of the table it was given.
+ *
+ * The larger half is looped on rather than recursed into, so what is on
+ * the host stack stays within the logarithm of the size.
+ */
+ private fun auxsort(from: Int, to: Int, cmpfunc: LuaValue?, debuglib: net.blueva.luak.lib.DebugLib?) {
+ var lo = from
+ var up = to
+ while (lo < up) {
+ /* sort elements 'lo', 'p', and 'up' */
+ if (compare(up, lo, cmpfunc, debuglib)) swap(lo, up)
+ if (up - lo == 1) return /* only 2 elements */
+ var p: Int = lo + (up - lo) / 2 /* middle point */
+ if (compare(p, lo, cmpfunc, debuglib)) swap(p, lo)
+ else if (compare(up, p, cmpfunc, debuglib)) swap(p, up)
+ if (up - lo == 2) return /* only 3 elements */
+ swap(p, up - 1) /* the pivot goes next to the end */
+ p = partition(lo, up, cmpfunc, debuglib)
+ /* a[lo .. p - 1] <= a[p] <= a[p + 1 .. up] */
+ if (p - lo < up - p) {
+ auxsort(lo, p - 1, cmpfunc, debuglib)
+ lo = p + 1
+ } else {
+ auxsort(p + 1, up, cmpfunc, debuglib)
+ up = p - 1
+ }
}
}
- private fun heapify(count: Int, cmpfunc: LuaValue?) {
- for (start in count / 2 downTo 1) siftDown(start, count, cmpfunc)
+ /**
+ * Puts everything below the pivot before it and everything above after.
+ *
+ * The pivot is at `up - 1` when this starts, and at the index answered
+ * when it ends.
+ */
+ private fun partition(
+ lo: Int,
+ up: Int,
+ cmpfunc: LuaValue?,
+ debuglib: net.blueva.luak.lib.DebugLib?,
+ ): Int {
+ val pivot: Int = up - 1
+ var i: Int = lo
+ var j: Int = up - 1
+ while (true) {
+ /* repeat ++i while a[i] < P */
+ while (compare(++i, pivot, cmpfunc, debuglib)) {
+ if (i == up - 1) LuaValue.error("invalid order function for sorting")
+ }
+ /* repeat --j while P < a[j] */
+ while (compare(pivot, --j, cmpfunc, debuglib)) {
+ if (j < i) LuaValue.error("invalid order function for sorting")
+ }
+ if (j < i) {
+ swap(up - 1, i) /* the pivot takes its place */
+ return i
+ }
+ swap(i, j)
+ }
}
- private fun siftDown(start: Int, end: Int, cmpfunc: LuaValue?) {
- var root = start
- while (root * 2 <= end) {
- var child = root * 2
- if (child < end && compare(child, child + 1, cmpfunc)) ++child
- if (compare(root, child, cmpfunc)) {
- val a: LuaValue = get(root) // swap(root, child)
- set(root, get(child))
- set(child, a)
- root = child
- } else return
- }
+ private fun swap(i: Int, j: Int) {
+ val held: LuaValue = get(i)
+ set(i, get(j))
+ set(j, held)
}
- private fun compare(i: Int, j: Int, cmpfunc: LuaValue?): Boolean {
+ private fun compare(
+ i: Int,
+ j: Int,
+ cmpfunc: LuaValue?,
+ debuglib: net.blueva.luak.lib.DebugLib? = null,
+ ): Boolean {
val a: LuaValue? = get(i)
val b: LuaValue? = get(j)
if (a == null || b == null) return false
if (cmpfunc != null) {
- return cmpfunc.call(a, b)!!.toboolean()
+ // Through the library's own way of calling back, so that an order
+ // function of the library's own can be named in an error.
+ return net.blueva.luak.lib.callback(debuglib, cmpfunc, varargsOf(a, b)!!).arg1()!!.toboolean()
} else {
return a.lt_b(b)
}
@@ -804,9 +929,8 @@ open class LuaTable : LuaValue, Metatable {
override fun eq_b(`val`: LuaValue?): Boolean {
val `val` = `val`!!
if (this === `val`) return true
- if (m_metatable == null || !`val`.istable()) return false
- val valmt: LuaValue? = `val`.getmetatable()
- return valmt != null && LuaValue.eqmtcall(this, (m_metatable!!.toLuaValue())!!, `val`, valmt)
+ if (!`val`.istable()) return false
+ return LuaValue.eqmtcall(this, `val`)
}
/** Unpack all the elements of this table */
@@ -1111,7 +1235,7 @@ open class LuaTable : LuaValue, Metatable {
}
}
- private class IntKeyEntry(private val key: Int, value: LuaValue?) : Entry() {
+ private class IntKeyEntry(private val key: Long, value: LuaValue?) : Entry() {
private var value: LuaValue?
init {
@@ -1123,7 +1247,7 @@ open class LuaTable : LuaValue, Metatable {
}
override fun arraykey(max: Int): Int {
- return if (key >= 1 && key <= max) key else 0
+ return if (key >= 1L && key <= max.toLong()) key.toInt() else 0
}
override fun value(): LuaValue {
@@ -1145,7 +1269,12 @@ open class LuaTable : LuaValue, Metatable {
}
/**
- * Entry class used with numeric values, but only when the key is not an integer.
+ * Entry class used with float values, but only when the key is not an integer.
+ *
+ * Unpacking the number into a raw `double` field saves an object, but it can
+ * only hold a float: an integer stored this way comes back rounded once it
+ * exceeds 2^53, and loses its subtype in every case. Integers therefore go
+ * to [NormalEntry] instead.
*/
private class NumberValueEntry(key: LuaValue, value: Double) : Entry() {
private var value: Double
@@ -1165,7 +1294,7 @@ open class LuaTable : LuaValue, Metatable {
}
public override fun set(value: LuaValue?): Entry {
- if (value!!.type() === TNUMBER) {
+ if (value!!.type() === TNUMBER && !value.isinttype()) {
val n: LuaValue = value!!.tonumber()
if (!n.isnil()) {
this.value = n.todouble()
@@ -1241,13 +1370,17 @@ open class LuaTable : LuaValue, Metatable {
return if (next != null) next!!.add(newEntry) else newEntry
}
- override fun remove(target: StrongSlot?): Slot {
+ override fun remove(target: StrongSlot?): Slot? {
+ // The rest of the chain is searched either way: dropping this
+ // placeholder must not drop the removal along with it.
+ val rest: Slot? = next?.remove(target)
if (key() != null) {
- next = next!!.remove(target)
+ // The key is still reachable, so it can still be handed to
+ // next(), and this placeholder has to stay to answer it.
+ next = rest
return this
- } else {
- return (next)!!
}
+ return rest
}
override fun relink(rest: Slot?): Slot? {
@@ -1294,6 +1427,9 @@ open class LuaTable : LuaValue, Metatable {
companion object {
private const val MIN_HASH_CAPACITY = 2
+
+ /** The largest either part of a table can be asked for. */
+ internal const val MAX_PART: Int = 1 shl 30
private val N: LuaString? = valueOf("n")
/** Resize the table */
@@ -1378,8 +1514,8 @@ open class LuaTable : LuaValue, Metatable {
internal fun defaultEntry(key: LuaValue, value: LuaValue): Entry {
if (key.isinttype()) {
- return net.blueva.luak.LuaTable.IntKeyEntry(key.toint(), value)
- } else if (value.type() === TNUMBER) {
+ return net.blueva.luak.LuaTable.IntKeyEntry(key.tolong(), value)
+ } else if (value.type() === TNUMBER && !value.isinttype()) {
return net.blueva.luak.LuaTable.NumberValueEntry(key, value.todouble())
} else {
return net.blueva.luak.LuaTable.NormalEntry(key, value)
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaThread.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaThread.kt
index 9d23ee6b..23255747 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaThread.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaThread.kt
@@ -72,7 +72,29 @@ import kotlin.coroutines.suspendCoroutine
*
* @see net.blueva.luak.lib.CoroutineLib
*/
+/**
+ * Counts a call that recurses on the host stack, refusing it past the ceiling.
+ *
+ * The interpreter runs on the host's own stack, so a Lua program that calls
+ * out and back in without bound would exhaust it. Lua counts those calls
+ * instead and stops at a ceiling of its own, keeping a little room above it
+ * for whatever handles the failure; running out of that room too is a failure
+ * of the handling rather than of the call.
+ */
+internal fun enterForeignCall(state: LuaThread.State) {
+ if (++state.foreigncalls < LuaThread.State.MAX_FOREIGN_CALLS) return
+ if (state.foreigncalls == LuaThread.State.MAX_FOREIGN_CALLS) {
+ LuaValue.error("C stack overflow")
+ }
+ if (state.foreigncalls >= LuaThread.State.MAX_HANDLER_CALLS) {
+ LuaValue.error("error in error handling")
+ }
+}
+
class LuaThread : LuaValue {
+ /** See [LuaValue.pinned]; a coroutine can be a weak key. */
+ internal override var pinned: Any? = null
+
val state: State
/** Thread-local used by DebugLib to store debugging state.
@@ -140,6 +162,36 @@ class LuaThread : LuaValue {
return s.lua_resume(this, args)
}
+ /**
+ * `coroutine.close`: ends this coroutine, running its pending closers.
+ *
+ * A suspended coroutine may be holding to-be-closed variables partway
+ * through its body. Closing it unwinds from the point it yielded at, which
+ * is what runs their `__close` handlers; an error raised by one of those is
+ * reported rather than thrown.
+ *
+ * @return `true`, or `false` plus the error a closer raised
+ */
+ fun close(): Varargs {
+ // Raised rather than reported: only a suspended or dead coroutine can
+ // be closed, so anything else is a mistake in the call itself. The
+ // status is looked at before the thread's identity, since the main
+ // thread is "normal" while whatever it resumed is running.
+ val s = this.state
+ if (s.status == net.blueva.luak.LuaThread.Companion.STATUS_NORMAL) {
+ LuaValue.error("cannot close a normal coroutine")
+ }
+ if (s.status == net.blueva.luak.LuaThread.Companion.STATUS_RUNNING) {
+ if (this.isMainThread) LuaValue.error("cannot close main thread")
+ // A coroutine closing itself is ended on the spot: this never
+ // returns, so nothing written after the call runs. Its pending
+ // to-be-closed variables are handled on the way out, by the
+ // `finally` blocks the unwinding passes through.
+ throw ClosedCoroutine()
+ }
+ return s.lua_close(this)
+ }
+
class State internal constructor(globals: Globals, lua_thread: LuaThread, function: LuaValue?) {
private val globals: Globals
val lua_thread: WeakReference
@@ -156,6 +208,78 @@ class LuaThread : LuaValue {
var hookrtrn: Boolean = false
var hookcount: Int = 0
var inhook: Boolean = false
+
+ /** True while a hook has been entered but its frame is not on yet. */
+ var hookframepending: Boolean = false
+
+ /**
+ * How many frames a host stack overflow has unwound so far.
+ *
+ * The interpreter runs on the host's own stack, so an overflow is
+ * noticed with no room left to report it in. Counting the frames it
+ * unwinds through lets it be turned into an ordinary Lua error a
+ * little way back from the edge, where there is room again for a
+ * message handler to run.
+ */
+ var unwinding: Int = 0
+
+ /**
+ * How many message handlers are running on this thread.
+ *
+ * A reference build gives a handler a stack of its own to work in,
+ * past the one the program ran out of; running out again in there is
+ * a failure of the handling rather than another overflow.
+ */
+ var inhandler: Int = 0
+
+ /**
+ * Set while a `__gc` handler is being called, so the frame it pushes
+ * can be marked as a finalizer's; see [DebugLib.CallFrame.finalizer].
+ */
+ var finalizerframepending: Boolean = false
+
+ /** The `__call` chain length the next frame pushed should report. */
+ var pendingextraargs: Int = 0
+
+ /** True when the next frame pushed is one a tail call is making. */
+ var pendingtailcall: Boolean = false
+
+ /**
+ * How many calls that are not Lua-to-Lua are in progress.
+ *
+ * A Lua function calling another loops inside the interpreter, but a
+ * call that goes out to the library and back in recurses on the host
+ * stack. Lua counts exactly those and refuses to go deeper than
+ * [MAX_FOREIGN_CALLS], which is what keeps a runaway
+ * `pcall`-through-`pcall` from having to exhaust the whole stack
+ * before it is stopped.
+ */
+ var foreigncalls: Int = 0
+
+
+ companion object {
+ /** As many nested calls out of Lua as Lua allows, `LUAI_MAXCCALLS`. */
+ const val MAX_FOREIGN_CALLS: Int = 200
+
+ /**
+ * Past this, handling an error is itself given up on.
+ *
+ * Lua leaves a tenth of the allowance above the ordinary ceiling
+ * so that a message handler still has room to run; a handler that
+ * keeps failing eats through it, and then there is nothing left to
+ * report but the failure of the handling itself.
+ */
+ const val MAX_HANDLER_CALLS: Int = MAX_FOREIGN_CALLS * 11 / 10
+
+ }
+
+ /**
+ * How many library calls into Lua code are in progress on this thread.
+ *
+ * While any of them is, there is nowhere for a yield to suspend to and
+ * the coroutine reports itself as not yieldable.
+ */
+ var noyield: Int = 0
var lastline: Int = 0
var bytecodes: Int = 0
@@ -194,6 +318,11 @@ class LuaThread : LuaValue {
fun lua_resume(new_thread: LuaThread, args: Varargs?): Varargs {
val previous_thread: LuaThread = globals.running
+ // A resumed coroutine runs on the host stack the resuming one is
+ // standing on, so it carries on counting from there; see
+ // enterForeignCall.
+ val outer: Int = previous_thread.state.foreigncalls
+ foreigncalls = outer
try {
globals.running = new_thread
// Mark the resuming thread NORMAL before running the resumed
@@ -215,7 +344,23 @@ class LuaThread : LuaValue {
return if (finished) {
val r = finalResult!!
val err = r.exceptionOrNull()
- if (err != null) LuaValue.varargsOf(LuaValue.FALSE, LuaValue.valueOf(err.message))!!
+ if (err != null) {
+ // The coroutine died: what it failed with is kept, so a
+ // later coroutine.close can report it once.
+ // A coroutine that closed itself did not fail: it
+ // ended, and a resume sees an ordinary empty return.
+ if (err is ClosedCoroutine) LuaValue.TRUE!!
+ else {
+ deadError = err
+ // The stack it died on outlives the frames
+ // themselves, so a traceback can still show it.
+ new_thread.callstack?.let { stack ->
+ (stack as net.blueva.luak.lib.DebugLib.CallStack).frozen =
+ (err as? LuaError)?.luastack
+ }
+ LuaValue.varargsOf(LuaValue.FALSE, errorObject(err))!!
+ }
+ }
else LuaValue.varargsOf(LuaValue.TRUE, r.getOrThrow())!!
} else {
status = net.blueva.luak.LuaThread.Companion.STATUS_SUSPENDED
@@ -226,9 +371,101 @@ class LuaThread : LuaValue {
pendingYieldValues = null
globals.running = previous_thread
globals.running.state.status = net.blueva.luak.LuaThread.Companion.STATUS_RUNNING
+ previous_thread.state.foreigncalls = outer
}
}
+ /**
+ * What a coroutine died of, until a `coroutine.close` reports it.
+ *
+ * Lua hands the error back once more when the dead coroutine is
+ * closed, and answers plainly the next time it is asked.
+ */
+ private var deadError: Throwable? = null
+
+ /**
+ * The value a failure should be reported as.
+ *
+ * A Lua error carries its own object, which may be any value; anything
+ * else can only be described by its text.
+ */
+ private fun errorObject(err: Throwable): LuaValue {
+ if (err is LuaError) {
+ val message: LuaValue? = err.messageObject
+ if (message != null) return message
+ }
+ // Asked first: running out of stack can surface with a message of
+ // the host's own, which says nothing useful to a Lua program.
+ if (platformIsStackOverflow(err)) return LuaValue.valueOf("C stack overflow")!!
+ return LuaValue.valueOf(err.message ?: err.toString())!!
+ }
+
+ /** Unwinds a suspended coroutine so its pending closers run. */
+ fun lua_close(closing: LuaThread): Varargs {
+ // Closing runs the coroutine's pending handlers, which recurse on
+ // the host stack the way any other call out of Lua does: a chain
+ // of coroutines each closing the one before it is what a ceiling
+ // on that is for. The tally goes back where it was afterwards,
+ // since a close reports what went wrong rather than raising it.
+ // Counted against the thread that asked, not the one being
+ // closed: the handlers run on the host stack the asking thread is
+ // already standing on, and the thread being closed carries on
+ // counting from there.
+ val caller: State = globals.running.state
+ val outer: Int = caller.foreigncalls
+ try {
+ enterForeignCall(caller)
+ } catch (deep: LuaError) {
+ status = net.blueva.luak.LuaThread.Companion.STATUS_DEAD
+ return LuaValue.varargsOf(LuaValue.FALSE, errorObject(deep))!!
+ }
+ foreigncalls = caller.foreigncalls
+ try {
+ return closing(closing)
+ } finally {
+ caller.foreigncalls = outer
+ }
+ }
+
+ /** What [lua_close] does once the call has been counted. */
+ private fun closing(closing: LuaThread): Varargs {
+ val continuation = yieldContinuation
+ yieldContinuation = null
+ if (continuation == null) {
+ // Never started, or already finished: nothing is on its stack.
+ status = net.blueva.luak.LuaThread.Companion.STATUS_DEAD
+ // A coroutine that died of an error reports it once more here,
+ // and nothing on any close after that.
+ val died: Throwable? = deadError
+ deadError = null
+ if (died != null) {
+ return LuaValue.varargsOf(LuaValue.FALSE, errorObject(died))!!
+ }
+ return LuaValue.TRUE!!
+ }
+ val previous_thread: LuaThread = globals.running
+ try {
+ globals.running = closing
+ previous_thread.state.status = net.blueva.luak.LuaThread.Companion.STATUS_NORMAL
+ status = net.blueva.luak.LuaThread.Companion.STATUS_RUNNING
+ finished = false
+ finalResult = null
+ // An Error rather than an Exception, so the interpreter's
+ // catch-all leaves it alone and only the finally blocks - the
+ // ones that close variables - run on the way out.
+ continuation.resumeWithException(ClosedCoroutine())
+ } finally {
+ status = net.blueva.luak.LuaThread.Companion.STATUS_DEAD
+ globals.running = previous_thread
+ globals.running.state.status = net.blueva.luak.LuaThread.Companion.STATUS_RUNNING
+ }
+ val result = finalResult
+ finalResult = null
+ val failure: Throwable? = result?.exceptionOrNull()
+ if (failure == null || failure is ClosedCoroutine) return LuaValue.TRUE!!
+ return LuaValue.varargsOf(LuaValue.FALSE, errorObject(failure))!!
+ }
+
suspend fun lua_yield(args: Varargs?): Varargs {
status = net.blueva.luak.LuaThread.Companion.STATUS_SUSPENDED
pendingYieldValues = args ?: LuaValue.NONE
@@ -237,6 +474,9 @@ class LuaThread : LuaValue {
}
}
+ /** Thrown into a suspended coroutine by [close] to unwind it. */
+ internal class ClosedCoroutine : Error("coroutine closed")
+
companion object {
/** Shared metatable for lua threads. */
var s_metatable: LuaValue? = null
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaUserdata.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaUserdata.kt
index 2b3c83c4..69f02493 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaUserdata.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaUserdata.kt
@@ -19,6 +19,12 @@ package net.blueva.luak
import kotlin.reflect.KClass
open class LuaUserdata : LuaValue {
+ /** See [LuaValue.gckeeper]; a userdata is one of the two kinds that can have one. */
+ internal override var gckeeper: Any? = null
+
+ /** See [LuaValue.pinned]; a value of this kind can be a weak key. */
+ internal override var pinned: Any? = null
+
var m_instance: Any
var m_metatable: LuaValue? = null
@@ -118,9 +124,8 @@ open class LuaUserdata : LuaValue {
override fun eq_b(`val`: LuaValue?): Boolean {
val `val` = `val`!!
if (`val`.raweq(this)) return true
- if (m_metatable == null || !`val`.isuserdata()) return false
- val valmt: LuaValue? = `val`.getmetatable()
- return valmt != null && LuaValue.eqmtcall(this, m_metatable!!, `val`, valmt)
+ if (!`val`.isuserdata()) return false
+ return LuaValue.eqmtcall(this, `val`)
}
// equality w/o metatable processing
@@ -136,11 +141,6 @@ open class LuaUserdata : LuaValue {
// __eq metatag processing
fun eqmt(`val`: LuaValue): Boolean {
- return if (m_metatable != null && `val`.isuserdata()) LuaValue.eqmtcall(
- this,
- m_metatable!!,
- `val`,
- `val`.getmetatable()!!
- ) else false
+ return `val`.isuserdata() && LuaValue.eqmtcall(this, `val`)
}
}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaValue.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaValue.kt
index 4790d297..5b504f60 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaValue.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/LuaValue.kt
@@ -101,6 +101,34 @@ import kotlin.reflect.KClass
*/
abstract
open class LuaValue : Varargs() {
+ /**
+ * What keeps this value watched for finalization, or null.
+ *
+ * Only a table or a userdata can have a `__gc` handler, so only those keep
+ * one; everything else answers null and ignores what it is given. Holding
+ * it here is the point: the keeper has to live exactly as long as the
+ * value does. See [Globals.markforfinalization].
+ */
+ internal open var gckeeper: Any?
+ get() = null
+ set(value) {}
+
+ /**
+ * What this value keeps alive by being the key of a weak-key table.
+ *
+ * Such a table holds its value only for as long as the key lives, and it
+ * is the key itself that holds it: an entry whose key can be reached only
+ * through its own value is then a ring that nothing outside refers to, and
+ * goes as a whole. That is what makes a weak-key table an ephemeron table
+ * rather than one that keeps its keys alive through their values.
+ *
+ * Holds one value, or a list of them for a key used in several tables.
+ * Only the kinds that can be a weak key keep one; see [WeakTable].
+ */
+ internal open var pinned: Any?
+ get() = null
+ set(value) {}
+
// type
/** Get the enumeration value for the type of this value.
* @return value for this type, one of
@@ -1055,7 +1083,23 @@ open class LuaValue : Varargs() {
* @throws LuaError in all cases
*/
protected fun argerror(expected: String?): LuaValue? {
- throw LuaError("bad argument: " + expected + " expected, got " + typename())
+ throw LuaError("bad argument: " + expected + " expected, got " + argtypename())
+ }
+
+ /**
+ * The type name an argument error calls this value.
+ *
+ * A `__name` field renames the type, and a light userdata is named as one
+ * so a script told "userdata expected" can see what it actually passed.
+ */
+ internal fun argtypename(): String {
+ val mt: LuaValue? = getmetatable()
+ if (mt != null) {
+ val name: LuaValue = mt.rawget(net.blueva.luak.LuaValue.Companion.NAME)
+ if (name.type() == net.blueva.luak.LuaValue.Companion.TSTRING) return name.tojstring()
+ }
+ if (this is LuaLightUserdata) return "light userdata"
+ return typename()!!
}
/**
@@ -1064,7 +1108,7 @@ open class LuaValue : Varargs() {
* @throws LuaError in all cases
*/
protected fun typerror(expected: String?): LuaValue? {
- throw LuaError(expected.toString() + " expected, got " + typename())
+ throw LuaError(expected.toString() + " expected, got " + argtypename())
}
/**
@@ -1500,7 +1544,7 @@ open class LuaValue : Varargs() {
* @see .method
*/
open fun call(): LuaValue? {
- return callmt().call(this)
+ return invoke(net.blueva.luak.LuaValue.Companion.NONE!!).arg1()!!
}
/** Call `this` with 1 argument, including metatag processing,
@@ -1531,7 +1575,7 @@ open class LuaValue : Varargs() {
* @see .method
*/
open fun call(arg: LuaValue?): LuaValue? {
- return callmt().call(this, arg)
+ return invoke(arg!!).arg1()!!
}
/** Convenience function which calls a luavalue with a single, string argument.
@@ -1572,7 +1616,7 @@ open class LuaValue : Varargs() {
* @see .method
*/
open fun call(arg1: LuaValue?, arg2: LuaValue?): LuaValue? {
- return callmt().call(this, arg1, arg2)
+ return invoke(net.blueva.luak.LuaValue.Companion.varargsOf(arg1, arg2!!)).arg1()!!
}
/** Call `this` with 3 arguments, including metatag processing,
@@ -1605,7 +1649,9 @@ open class LuaValue : Varargs() {
* @see .invokemethod
*/
open fun call(arg1: LuaValue?, arg2: LuaValue?, arg3: LuaValue?): LuaValue? {
- return (callmt().invoke(arrayOf(this, arg1, arg2, arg3))!!.arg1())!!
+ return invoke(
+ net.blueva.luak.LuaValue.Companion.varargsOf(arrayOf(arg1, arg2, arg3)),
+ ).arg1()!!
}
/** Suspending counterpart to the `call`/`invoke`/`onInvoke` family, used by
@@ -1881,7 +1927,13 @@ open class LuaValue : Varargs() {
* @see .invokemethod
*/
open fun invoke(args: Varargs): Varargs {
- return callmt().invoke(this, args)
+ val prefix: ArrayList = ArrayList()
+ val target: LuaValue = resolvecall(prefix)
+ // Prepended outermost first, so the innermost handler's own value
+ // ends up in front: Lua hands them over in the order it walked them.
+ var all: Varargs = args
+ for (self in prefix) all = net.blueva.luak.LuaValue.Companion.varargsOf(self, all)
+ return target.invoke(all)
}
/** Suspending counterpart to [.invoke]; see [.callSuspend] for why this
@@ -2227,6 +2279,31 @@ open class LuaValue : Varargs() {
* @return [LuaValue] value if metatag is defined
* @throws LuaError if [.CALL] metatag is not defined.
*/
+ /**
+ * The function a call on this value really reaches, through `__call`.
+ *
+ * Each handler on the way takes the value it was found on as its first
+ * argument, so [prefix] collects them in order. The chain is bounded: Lua
+ * allows a fixed number of handlers between the value that was written and
+ * the function that runs, and refuses a longer one rather than following
+ * it forever.
+ */
+ internal fun resolvecall(prefix: ArrayList): LuaValue {
+ var target: LuaValue = this
+ var depth = 0
+ while (target !is LuaFunction) {
+ val handler: LuaValue = target.metatag(net.blueva.luak.LuaValue.Companion.CALL)
+ // Nothing to call: reported against the value as it was written.
+ if (handler.isnil()) target.callmt()
+ if (++depth > net.blueva.luak.LuaValue.Companion.MAX_CALL_CHAIN) {
+ net.blueva.luak.LuaValue.Companion.error("'__call' chain too long")
+ }
+ prefix.add(target)
+ target = handler
+ }
+ return target
+ }
+
protected fun callmt(): LuaValue {
return checkmetatag(net.blueva.luak.LuaValue.Companion.CALL, "attempt to call ")
}
@@ -2245,7 +2322,11 @@ open class LuaValue : Varargs() {
* @throws LuaError if `this` is not a table or string, and has no [.UNM] metatag
*/
open fun neg(): LuaValue {
- return checkmetatag(net.blueva.luak.LuaValue.Companion.UNM, "attempt to perform arithmetic on ").call(this)!!
+ // Lua hands a unary operator its operand twice.
+ return checkmetatag(
+ net.blueva.luak.LuaValue.Companion.UNM,
+ "attempt to perform arithmetic on ",
+ ).call(this, this)!!
}
/** Length operator: return lua length of object `(#this)` including metatag processing as java int
@@ -2254,7 +2335,8 @@ open class LuaValue : Varargs() {
* @throws LuaError if `this` is not a table or string, and has no [.LEN] metatag
*/
open fun len(): LuaValue {
- return checkmetatag(net.blueva.luak.LuaValue.Companion.LEN, "attempt to get length of ").call(this)!!
+ // Lua hands a unary operator its operand twice.
+ return checkmetatag(net.blueva.luak.LuaValue.Companion.LEN, "attempt to get length of ").call(this, this)!!
}
/** Length operator: return lua length of object `(#this)` including metatag processing as java int
@@ -2401,7 +2483,7 @@ open class LuaValue : Varargs() {
* whose value equals val,
* otherwise false
*/
- open fun raweq(`val`: Int): Boolean {
+ open fun raweq(`val`: Long): Boolean {
return false
}
@@ -2451,8 +2533,8 @@ open class LuaValue : Varargs() {
* @throws LuaError if `this` is not a number or string convertible to number
* @see .add
*/
- open fun add(rhs: Int): LuaValue {
- return add(rhs.toDouble())
+ open fun add(rhs: Long): LuaValue {
+ return arithmtwith(net.blueva.luak.LuaValue.Companion.ADD, rhs)
}
/** Subtract: Perform numeric subtract operation with another value
@@ -2502,7 +2584,7 @@ open class LuaValue : Varargs() {
* @throws LuaError if `this` is not a number or string convertible to number
* @see .sub
*/
- open fun sub(rhs: Int): LuaValue? {
+ open fun sub(rhs: Long): LuaValue? {
return aritherror("sub")
}
@@ -2541,8 +2623,8 @@ open class LuaValue : Varargs() {
* @see .sub
* @see .sub
*/
- open fun subFrom(lhs: Int): LuaValue {
- return subFrom(lhs.toDouble())
+ open fun subFrom(lhs: Long): LuaValue {
+ return arithmtwith(net.blueva.luak.LuaValue.Companion.SUB, lhs)
}
/** Multiply: Perform numeric multiply operation with another value
@@ -2592,8 +2674,8 @@ open class LuaValue : Varargs() {
* @throws LuaError if `this` is not a number or string convertible to number
* @see .mul
*/
- open fun mul(rhs: Int): LuaValue {
- return mul(rhs.toDouble())
+ open fun mul(rhs: Long): LuaValue {
+ return arithmtwith(net.blueva.luak.LuaValue.Companion.MUL, rhs)
}
/** Raise to power: Raise this value to a power
@@ -2642,7 +2724,7 @@ open class LuaValue : Varargs() {
* @throws LuaError if `this` is not a number or string convertible to number
* @see .pow
*/
- open fun pow(rhs: Int): LuaValue? {
+ open fun pow(rhs: Long): LuaValue? {
return aritherror("pow")
}
@@ -2678,8 +2760,8 @@ open class LuaValue : Varargs() {
* @see .pow
* @see .pow
*/
- open fun powWith(lhs: Int): LuaValue {
- return powWith(lhs.toDouble())
+ open fun powWith(lhs: Long): LuaValue {
+ return arithmtwith(net.blueva.luak.LuaValue.Companion.POW, lhs)
}
/** Divide: Perform numeric divide operation by another value
@@ -2735,10 +2817,111 @@ open class LuaValue : Varargs() {
* @throws LuaError if `this` is not a number or string convertible to number
* @see .div
*/
- open fun div(rhs: Int): LuaValue? {
+ open fun div(rhs: Long): LuaValue? {
return aritherror("div")
}
+ /** Floor divide: perform the `//` operation with metatag processing.
+ *
+ * Integer operands produce an integer, rounding towards negative infinity;
+ * if either side is a float the result is a float. Dividing an integer by
+ * an integer zero is an error, while the float form yields an infinity, all
+ * as specified since Lua 5.3.
+ *
+ * @param rhs the right-hand-side value
+ * @return value of `(this // rhs)`
+ * @throws LuaError if either operand is not a number and has no `__idiv`
+ */
+ open fun idiv(rhs: LuaValue): LuaValue {
+ return arithmt(net.blueva.luak.LuaValue.Companion.IDIV, rhs)
+ }
+
+
+ /** Bitwise and: perform the `&` operation with metatag processing.
+ *
+ * Both operands must denote integers; see `luaBitwiseOperand`.
+ *
+ * @param rhs the right-hand-side value
+ * @return value of `(this & rhs)`
+ * @throws LuaError if either operand has no integer representation and
+ * there is no `__band` metamethod
+ */
+ open fun band(rhs: LuaValue): LuaValue {
+ return arithmt(net.blueva.luak.LuaValue.Companion.BAND, rhs)
+ }
+
+ /** Bitwise or: perform the `|` operation with metatag processing.
+ *
+ * Both operands must denote integers; see `luaBitwiseOperand`.
+ *
+ * @param rhs the right-hand-side value
+ * @return value of `(this | rhs)`
+ * @throws LuaError if either operand has no integer representation and
+ * there is no `__bor` metamethod
+ */
+ open fun bor(rhs: LuaValue): LuaValue {
+ return arithmt(net.blueva.luak.LuaValue.Companion.BOR, rhs)
+ }
+
+ /** Bitwise exclusive or: perform the `~` operation with metatag processing.
+ *
+ * Both operands must denote integers; see `luaBitwiseOperand`.
+ *
+ * @param rhs the right-hand-side value
+ * @return value of `(this ~ rhs)`
+ * @throws LuaError if either operand has no integer representation and
+ * there is no `__bxor` metamethod
+ */
+ open fun bxor(rhs: LuaValue): LuaValue {
+ return arithmt(net.blueva.luak.LuaValue.Companion.BXOR, rhs)
+ }
+
+ /** Bitwise left shift: perform the `<<` operation with metatag processing.
+ *
+ * Both operands must denote integers; see `luaBitwiseOperand`.
+ *
+ * @param rhs the right-hand-side value
+ * @return value of `(this << rhs)`
+ * @throws LuaError if either operand has no integer representation and
+ * there is no `__shl` metamethod
+ */
+ open fun shl(rhs: LuaValue): LuaValue {
+ return arithmt(net.blueva.luak.LuaValue.Companion.SHL, rhs)
+ }
+
+ /** Bitwise right shift: perform the `>>` operation with metatag processing.
+ *
+ * Both operands must denote integers; see `luaBitwiseOperand`.
+ *
+ * @param rhs the right-hand-side value
+ * @return value of `(this >> rhs)`
+ * @throws LuaError if either operand has no integer representation and
+ * there is no `__shr` metamethod
+ */
+ open fun shr(rhs: LuaValue): LuaValue {
+ return arithmt(net.blueva.luak.LuaValue.Companion.SHR, rhs)
+ }
+
+ /** Bitwise not: perform the unary `~` operation with metatag processing.
+ *
+ * @return value of `(~this)`
+ * @throws LuaError if this has no integer representation and there is no
+ * `__bnot` metamethod
+ */
+ open fun bnot(): LuaValue {
+ val h: LuaValue = metatag(net.blueva.luak.LuaValue.Companion.BNOT)
+ if (h.isnil()) {
+ net.blueva.luak.LuaValue.Companion.operandError(
+ net.blueva.luak.LuaValue.Companion.BNOT,
+ this,
+ this,
+ )
+ }
+ // Lua hands a unary operator its operand twice.
+ checkcallable(net.blueva.luak.LuaValue.Companion.BNOT, h)
+ return h.call(this, this)!!
+ }
+
/** Reverse-divide: Perform numeric divide operation into another value
* with metatag processing
*
@@ -2810,7 +2993,7 @@ open class LuaValue : Varargs() {
* @throws LuaError if `this` is not a number or string convertible to number
* @see .mod
*/
- open fun mod(rhs: Int): LuaValue? {
+ open fun mod(rhs: Long): LuaValue? {
return aritherror("mod")
}
@@ -2863,11 +3046,60 @@ open class LuaValue : Varargs() {
var h = this.metatag(tag)
if (h.isnil()) {
h = op2.metatag(tag)
- if (h.isnil()) net.blueva.luak.LuaValue.Companion.error("attempt to perform arithmetic " + tag + " on " + typename() + " and " + op2.typename())
+ if (h.isnil()) net.blueva.luak.LuaValue.Companion.operandError(tag, this, op2)
}
+ checkcallable(tag, h)
return h.call(this, op2)!!
}
+ /**
+ * Refuses a metamethod that cannot be called, naming it as Lua does.
+ *
+ * Whatever sits under a metatag is called without being looked at first,
+ * so on its own the failure would read `attempt to call a number value`
+ * and say nothing about which metamethod was reached for.
+ */
+ /**
+ * The type name Lua puts in messages, which a metatable can override.
+ *
+ * A `__name` field lets a library give its own objects a name: a file
+ * handle reports itself as `FILE*` rather than as a bare `userdata`.
+ */
+ fun objtypename(): String {
+ val mt: LuaValue? = getmetatable()
+ if (mt != null) {
+ val name: LuaValue = mt.rawget(net.blueva.luak.LuaValue.Companion.NAME)
+ if (name.type() == net.blueva.luak.LuaValue.Companion.TSTRING) return name.tojstring()
+ }
+ return typename()!!
+ }
+
+ /**
+ * Refuses a comparison between two values Lua cannot order.
+ *
+ * The two type names are given in the order the operands were written,
+ * which for `a > b` is the order of the `b < a` the compiler turned it
+ * into, and a pair of the same type is said once rather than twice.
+ */
+ internal fun ordererror(op1: LuaValue, op2: LuaValue): LuaValue? {
+ val first: String = op1.objtypename()
+ val second: String = op2.objtypename()
+ return if (first == second) {
+ net.blueva.luak.LuaValue.Companion.error("attempt to compare two " + first + " values")
+ } else {
+ net.blueva.luak.LuaValue.Companion.error("attempt to compare " + first + " with " + second)
+ }
+ }
+
+ internal fun checkcallable(tag: LuaValue?, h: LuaValue) {
+ if (h.isfunction()) return
+ if (!h.metatag(net.blueva.luak.LuaValue.Companion.CALL).isnil()) return
+ net.blueva.luak.LuaValue.Companion.error(
+ "attempt to call a " + h.objtypename() + " value (metamethod '" +
+ tag!!.tojstring().substring(2) + "')",
+ )
+ }
+
/** Perform metatag processing for arithmetic operations when the left-hand-side is a number.
*
*
@@ -2895,9 +3127,35 @@ open class LuaValue : Varargs() {
*
* @see .MOD
*/
+ /**
+ * As [arithmtwith], for an integer left-hand side.
+ *
+ * Kept apart from the float version so the metamethod sees the operand
+ * with the subtype it was written with: `5 + t` hands it an integer.
+ */
+ protected fun arithmtwith(tag: LuaValue?, op1: Long): LuaValue {
+ val h = metatag(tag)
+ if (h.isnil()) {
+ net.blueva.luak.LuaValue.Companion.operandError(
+ tag,
+ net.blueva.luak.LuaValue.Companion.valueOf(op1),
+ this,
+ )
+ }
+ checkcallable(tag, h)
+ return h.call(net.blueva.luak.LuaValue.Companion.valueOf(op1), this)!!
+ }
+
protected fun arithmtwith(tag: LuaValue?, op1: Double): LuaValue {
val h = metatag(tag)
- if (h.isnil()) net.blueva.luak.LuaValue.Companion.error("attempt to perform arithmetic " + tag + " on number and " + typename())
+ if (h.isnil()) {
+ net.blueva.luak.LuaValue.Companion.operandError(
+ tag,
+ net.blueva.luak.LuaValue.Companion.valueOf(op1),
+ this,
+ )
+ }
+ checkcallable(tag, h)
return h.call(net.blueva.luak.LuaValue.Companion.valueOf(op1), this)!!
}
@@ -2955,7 +3213,7 @@ open class LuaValue : Varargs() {
* @see .gteq_b
* @see .comparemt
*/
- open fun lt(rhs: Int): LuaValue? {
+ open fun lt(rhs: Long): LuaValue? {
return compareerror("number")
}
@@ -2995,7 +3253,7 @@ open class LuaValue : Varargs() {
* @see .gteq
* @see .comparemt
*/
- open fun lt_b(rhs: Int): Boolean {
+ open fun lt_b(rhs: Long): Boolean {
compareerror("number")
return false
}
@@ -3075,7 +3333,7 @@ open class LuaValue : Varargs() {
* @see .gteq_b
* @see .comparemt
*/
- open fun lteq(rhs: Int): LuaValue? {
+ open fun lteq(rhs: Long): LuaValue? {
return compareerror("number")
}
@@ -3115,7 +3373,7 @@ open class LuaValue : Varargs() {
* @see .gteq
* @see .comparemt
*/
- open fun lteq_b(rhs: Int): Boolean {
+ open fun lteq_b(rhs: Long): Boolean {
compareerror("number")
return false
}
@@ -3195,7 +3453,7 @@ open class LuaValue : Varargs() {
* @see .gteq_b
* @see .comparemt
*/
- open fun gt(rhs: Int): LuaValue? {
+ open fun gt(rhs: Long): LuaValue? {
return compareerror("number")
}
@@ -3235,7 +3493,7 @@ open class LuaValue : Varargs() {
* @see .gteq
* @see .comparemt
*/
- open fun gt_b(rhs: Int): Boolean {
+ open fun gt_b(rhs: Long): Boolean {
compareerror("number")
return false
}
@@ -3315,7 +3573,7 @@ open class LuaValue : Varargs() {
* @see .gteq_b
* @see .comparemt
*/
- open fun gteq(rhs: Int): LuaValue? {
+ open fun gteq(rhs: Long): LuaValue? {
return net.blueva.luak.LuaValue.Companion.valueOf(todouble() >= rhs)
}
@@ -3355,7 +3613,7 @@ open class LuaValue : Varargs() {
* @see .gteq
* @see .comparemt
*/
- open fun gteq_b(rhs: Int): Boolean {
+ open fun gteq_b(rhs: Long): Boolean {
compareerror("number")
return false
}
@@ -3401,13 +3659,19 @@ open class LuaValue : Varargs() {
var h: LuaValue?
if (!(metatag(tag).also { h = it }).isnil() || !(op1.metatag(tag)
.also { h = it }).isnil()
- ) return h!!.call(this, op1)
+ ) {
+ checkcallable(tag, h!!)
+ return h!!.call(this, op1)
+ }
if (net.blueva.luak.LuaValue.Companion.LE.raweq(tag) && (!(metatag(net.blueva.luak.LuaValue.Companion.LT).also {
h = it
}).isnil() || !(op1.metatag(net.blueva.luak.LuaValue.Companion.LT)
.also { h = it }).isnil())
- ) return h!!.call(op1, this)!!.not()
- return net.blueva.luak.LuaValue.Companion.error("attempt to compare " + tag + " on " + typename() + " and " + op1.typename())
+ ) {
+ checkcallable(net.blueva.luak.LuaValue.Companion.LT, h!!)
+ return h!!.call(op1, this)!!.not()
+ }
+ return ordererror(this, op1)
}
/** Perform string comparison with another value
@@ -3554,7 +3818,15 @@ open class LuaValue : Varargs() {
var h = metatag(net.blueva.luak.LuaValue.Companion.CONCAT)
if (h.isnil() && (rhs.metatag(net.blueva.luak.LuaValue.Companion.CONCAT)
.also { h = it }).isnil()
- ) net.blueva.luak.LuaValue.Companion.error("attempt to concatenate " + typename() + " and " + rhs.typename())
+ ) {
+ // Blame the operand that is not concatenable, the way Lua does,
+ // rather than naming both.
+ val culprit: LuaValue = if (!this.isstring() || this is LuaTable) this else rhs
+ net.blueva.luak.LuaValue.Companion.error(
+ "attempt to concatenate a " + culprit.objtypename() + " value",
+ )
+ }
+ checkcallable(net.blueva.luak.LuaValue.Companion.CONCAT, h)
return h.call(this, rhs)!!
}
@@ -3632,15 +3904,21 @@ open class LuaValue : Varargs() {
*/
protected fun checkmetatag(tag: LuaValue?, reason: String?): LuaValue {
val h = this.metatag(tag)
- if (h.isnil()) throw LuaError(reason.toString() + "a " + typename() + " value")
+ if (h.isnil()) throw LuaError(reason.toString() + "a " + objtypename() + " value")
return h
}
/** Throw [LuaError] indicating index was attempted on illegal type
* @throws LuaError when called.
*/
+ /**
+ * Reports indexing something that cannot be indexed.
+ *
+ * The key is not named here: Lua names where the *value* came from, which
+ * only the interpreter can work out, and it adds that afterwards.
+ */
private fun indexerror(key: String?) {
- net.blueva.luak.LuaValue.Companion.error("attempt to index ? (a " + typename() + " value) with key '" + key + "'")
+ net.blueva.luak.LuaValue.Companion.error("attempt to index a " + objtypename() + " value")
}
/**
@@ -3852,6 +4130,35 @@ open class LuaValue : Varargs() {
val DIV: LuaString
get() = net.blueva.luak.LuaValue.Companion.valueOf("__div")
+ /** LuaString constant with value "__idiv" for use as metatag */
+ val IDIV: LuaString
+ get() = net.blueva.luak.LuaValue.Companion.valueOf("__idiv")
+
+
+ /** LuaString constant with value "__band" for use as metatag */
+ val BAND: LuaString
+ get() = net.blueva.luak.LuaValue.Companion.valueOf("__band")
+
+ /** LuaString constant with value "__bor" for use as metatag */
+ val BOR: LuaString
+ get() = net.blueva.luak.LuaValue.Companion.valueOf("__bor")
+
+ /** LuaString constant with value "__bxor" for use as metatag */
+ val BXOR: LuaString
+ get() = net.blueva.luak.LuaValue.Companion.valueOf("__bxor")
+
+ /** LuaString constant with value "__shl" for use as metatag */
+ val SHL: LuaString
+ get() = net.blueva.luak.LuaValue.Companion.valueOf("__shl")
+
+ /** LuaString constant with value "__shr" for use as metatag */
+ val SHR: LuaString
+ get() = net.blueva.luak.LuaValue.Companion.valueOf("__shr")
+
+ /** LuaString constant with value "__bnot" for use as metatag */
+ val BNOT: LuaString
+ get() = net.blueva.luak.LuaValue.Companion.valueOf("__bnot")
+
/** LuaString constant with value "__mul" for use as metatag */
val MUL: LuaString
get() = net.blueva.luak.LuaValue.Companion.valueOf("__mul")
@@ -3868,6 +4175,24 @@ open class LuaValue : Varargs() {
val UNM: LuaString
get() = net.blueva.luak.LuaValue.Companion.valueOf("__unm")
+ /* see gckeeper */
+
+ /** LuaString constant with value "__gc" for use as metatag */
+ val GC: LuaString
+ get() = net.blueva.luak.LuaValue.Companion.valueOf("__gc")
+
+ /** LuaString constant with value "__close" for use as metatag */
+ val CLOSE: LuaString
+ get() = net.blueva.luak.LuaValue.Companion.valueOf("__close")
+
+ /** LuaString constant with value "__pairs" for use as metatag */
+ val PAIRS: LuaString
+ get() = net.blueva.luak.LuaValue.Companion.valueOf("__pairs")
+
+ /** LuaString constant with value "__name" for use as metatag */
+ val NAME: LuaString
+ get() = net.blueva.luak.LuaValue.Companion.valueOf("__name")
+
/** LuaString constant with value "__len" for use as metatag */
val LEN: LuaString
get() = net.blueva.luak.LuaValue.Companion.valueOf("__len")
@@ -3936,28 +4261,54 @@ open class LuaValue : Varargs() {
* @param msg String providing information about the invalid argument
* @throws LuaError in all cases
*/
+ /**
+ * Reports an operator applied to something it cannot work on.
+ *
+ * The blame goes to the first operand that is not a number, which is
+ * the one the reader needs to know about; a bitwise operator says so
+ * rather than calling itself arithmetic.
+ */
+ fun operandError(tag: LuaValue?, op1: LuaValue, op2: LuaValue): Nothing {
+ val bitwise: Boolean = tag != null && (
+ tag == net.blueva.luak.LuaValue.Companion.BAND ||
+ tag == net.blueva.luak.LuaValue.Companion.BOR ||
+ tag == net.blueva.luak.LuaValue.Companion.BXOR ||
+ tag == net.blueva.luak.LuaValue.Companion.SHL ||
+ tag == net.blueva.luak.LuaValue.Companion.SHR ||
+ tag == net.blueva.luak.LuaValue.Companion.BNOT
+ )
+ val what: String = if (bitwise) "perform bitwise operation on" else "perform arithmetic on"
+ val culprit: LuaValue =
+ if (op1.type() != net.blueva.luak.LuaValue.Companion.TNUMBER) op1 else op2
+ net.blueva.luak.LuaValue.Companion.error(
+ "attempt to " + what + " a " + culprit.objtypename() + " value",
+ )
+ throw IllegalStateException()
+ }
+
fun argerror(iarg: Int, msg: String?): LuaValue? {
throw LuaError("bad argument #" + iarg + ": " + msg)
}
- /** Perform equality testing metatag processing
- * @param lhs left-hand-side of equality expression
- * @param lhsmt metatag value for left-hand-side
- * @param rhs right-hand-side of equality expression
- * @param rhsmt metatag value for right-hand-side
- * @return true if metatag processing result is not [.NIL] or [.FALSE]
- * @throws LuaError if metatag was not defined for either operand
+ /**
+ * Runs `__eq` for two values raw equality has already turned down.
+ *
+ * The handler is the left operand's, or the right one's when the left
+ * has none: the two metatables no longer have to agree on it, as they
+ * did before Lua 5.3.
+ *
+ * @return true if the handler returned anything other than nil or false
* @see .equals
* @see .eq
* @see .raweq
* @see .EQ
*/
- fun eqmtcall(lhs: LuaValue?, lhsmt: LuaValue, rhs: LuaValue?, rhsmt: LuaValue): Boolean {
- val h: LuaValue = lhsmt.rawget(net.blueva.luak.LuaValue.Companion.EQ)
- return if (h.isnil() || h !== rhsmt.rawget(net.blueva.luak.LuaValue.Companion.EQ)) false else h.call(
- lhs,
- rhs
- )!!.toboolean()
+ fun eqmtcall(lhs: LuaValue, rhs: LuaValue): Boolean {
+ var h: LuaValue = lhs.metatag(net.blueva.luak.LuaValue.Companion.EQ)
+ if (h.isnil()) h = rhs.metatag(net.blueva.luak.LuaValue.Companion.EQ)
+ if (h.isnil()) return false
+ h.checkcallable(net.blueva.luak.LuaValue.Companion.EQ, h)
+ return h.call(lhs, rhs)!!.toboolean()
}
/** Convert java boolean to a [LuaValue].
@@ -3980,6 +4331,19 @@ open class LuaValue : Varargs() {
return (LuaInteger.valueOf(i))!!
}
+ /** Convert a long to a [LuaValue].
+ *
+ * Lua's integer subtype is 64 bits wide, so every long is representable
+ * exactly and this never yields a float.
+ *
+ * @param l long value to convert
+ * @return [LuaInteger] instance, possibly pooled, whose value is l
+ */
+ @kotlin.jvm.JvmStatic
+ fun valueOf(l: Long): LuaInteger {
+ return (LuaInteger.valueOf(l))!!
+ }
+
/** Convert java double to a [LuaValue].
* This may return a [LuaInteger] or [LuaDouble] depending
* on the value supplied.
@@ -4130,7 +4494,10 @@ open class LuaValue : Varargs() {
}
/** Constant limiting metatag loop processing */
- private const val MAXTAGLOOP = 100
+ internal const val MAXTAGLOOP = 100
+
+ /** As many `__call` handlers as Lua follows before refusing the chain. */
+ const val MAX_CALL_CHAIN: Int = 15
/**
* Return value for field reference including metatag processing, or [NIL] if it doesn't exist.
@@ -4182,7 +4549,7 @@ open class LuaValue : Varargs() {
}
} else if ((t.metatag(net.blueva.luak.LuaValue.Companion.NEWINDEX)
.also { tm = it }).isnil()
- ) throw LuaError("table expected for set index ('" + key + "') value, got " + t.typename())
+ ) throw LuaError("attempt to index a " + t.objtypename() + " value")
if (tm!!.isfunction()) {
tm.call(t, key, value)
return true
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Memory.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Memory.kt
new file mode 100644
index 00000000..f4dfb319
--- /dev/null
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Memory.kt
@@ -0,0 +1,105 @@
+/******************************************************************************
+ * ____ _ _ _ __
+ * | __ )| |_ _ ___| | _ _ __ _| |/ /
+ * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
+ * | |_) | | |_| | __/ |__| |_| | (_| | . \
+ * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
+ *
+ * BlueLuaK
+ * https://github.com/BluevaDevelopment/BlueLuaK
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+package net.blueva.luak
+
+/**
+ * What `collectgarbage("count")` answers with.
+ *
+ * The host's own collector is the one that reclaims memory here, and what it
+ * reports - a heap shared with everything else the host is doing - says
+ * nothing about how much of it is Lua's. So Lua's own objects are counted as
+ * they are made, the way a reference build counts what it allocates, and the
+ * tally goes back to nothing when a collection finishes: what is left after
+ * one is not known object by object, and a program that watches this number
+ * is watching it grow with what it allocates and drop when that is reclaimed.
+ *
+ * The sizes are the ones a reference build would use, so a program that works
+ * out how much a table of a given shape costs gets the answer it expects.
+ */
+internal object Memory {
+ /** What a table costs before any of its storage. */
+ const val TABLE: Long = 56
+
+ /** One slot of a table's array part. */
+ const val SLOT: Long = 16
+
+ /** One entry of a table's hash part. */
+ const val NODE: Long = 32
+
+ /** What a string costs beyond its own bytes. */
+ const val STRING: Long = 24
+
+ /** What a function written in Lua costs before its upvalues. */
+ const val CLOSURE: Long = 32
+
+ /** One upvalue of such a function. */
+ const val UPVALUE: Long = 8
+
+ /** What Lua holds with nothing allocated, so a count is never nothing. */
+ private const val BASE: Long = 32 * 1024
+
+ /** Where the collector would have run of its own accord. */
+ private const val THRESHOLD: Long = 1024 * 1024
+
+ /** Bytes of Lua's own objects made since the last collection. */
+ var accounted: Long = 0
+ private set
+
+ /**
+ * Bytes made since the host was last asked to collect.
+ *
+ * Unlike [accounted] this is not reset by a cycle going by on its own:
+ * it says how much has been allocated since anything was actually
+ * reclaimed, which is what decides when a program waiting on a finalizer
+ * is worth interrupting for.
+ */
+ var sincecollect: Long = 0
+ private set
+
+ /** How much may be allocated before the host is asked to collect. */
+ const val COLLECT_EVERY: Long = 1024 * 1024
+
+ /** False while `collectgarbage("stop")` is in force. */
+ var running: Boolean = true
+
+ /** Notes [bytes] just allocated, collecting if that is now overdue. */
+ fun account(bytes: Long) {
+ accounted += bytes
+ sincecollect += bytes
+ // The host reclaims on its own; what happens here is only that the
+ // tally starts again, which is what a finished cycle looks like from
+ // a program watching the count.
+ if (running && accounted > THRESHOLD) accounted = 0
+ }
+
+ /**
+ * Takes back [bytes] just counted, for something that is not an object.
+ *
+ * A reference build keeps a named vararg parameter on the stack rather
+ * than in an object of its own, so what stands in for it here is not
+ * something a program should see the cost of.
+ */
+ fun uncount(bytes: Long) {
+ accounted -= bytes
+ if (accounted < 0) accounted = 0
+ }
+
+ /** Ends a collection cycle: nothing made since the last one still counts. */
+ fun collected() {
+ accounted = 0
+ sincecollect = 0
+ }
+
+ /** Bytes in use, as `collectgarbage("count")` reports them. */
+ fun used(): Long = BASE + accounted
+}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/NumberParser.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/NumberParser.kt
new file mode 100644
index 00000000..c89561c5
--- /dev/null
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/NumberParser.kt
@@ -0,0 +1,284 @@
+/******************************************************************************
+ * ____ _ _ _ __
+ * | __ )| |_ _ ___| | _ _ __ _| |/ /
+ * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
+ * | |_) | | |_| | __/ |__| |_| | (_| | . \
+ * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
+ *
+ * BlueLuaK
+ * https://github.com/BluevaDevelopment/BlueLuaK
+ *
+ * Copyright (c) 2026 Blueva Development
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+package net.blueva.luak
+
+/**
+ * Reads a Lua numeral out of text, keeping the integer/float distinction.
+ *
+ * Both the lexer and the string-to-number coercion behind `tonumber` and
+ * arithmetic on strings go through here, so a literal and the same text passed
+ * to `tonumber` cannot disagree.
+ *
+ * The order matters and is the one upstream uses: a numeral is tried as an
+ * integer first and only then as a float, so `"3"` is the integer `3` while
+ * `"3.0"` and `"3e0"` are floats. A decimal integer too large for the 64-bit
+ * subtype is not an error - it becomes a float. A *hexadecimal* one wraps
+ * around instead, which is what the manual points at for code that wants the
+ * pre-5.3 behaviour.
+ */
+internal object NumberParser {
+
+ /** Digits past which the hex-float accumulator cannot stay exact. */
+ private const val MAX_SIGNIFICANT_HEX_DIGITS = 30
+
+ /**
+ * The numeral in [text], or `null` when it is not one.
+ *
+ * @return a [LuaInteger] or a [LuaDouble], never any other type
+ */
+ fun parse(text: String): LuaValue? {
+ parseInteger(text)?.let { return LuaValue.valueOf(it) }
+ return parseFloat(text)?.let { LuaValue.valueOf(it) }
+ }
+
+ /**
+ * The integer denoted by [text], or `null` if it denotes something else.
+ *
+ * A decimal numeral that overflows answers `null` so the caller can retry
+ * it as a float; a hexadecimal one wraps and always succeeds.
+ */
+ fun parseInteger(text: String): Long? {
+ var index = skipSpaces(text, 0)
+ var negative = false
+ if (index < text.length && (text[index] == '-' || text[index] == '+')) {
+ negative = text[index] == '-'
+ index++
+ }
+ var accumulator = 0L // unsigned; overflow past the sign is intended in hex
+ var empty = true
+ if (index + 1 < text.length && text[index] == '0' &&
+ (text[index + 1] == 'x' || text[index + 1] == 'X')
+ ) {
+ index += 2
+ while (index < text.length && isHexDigit(text[index])) {
+ accumulator = accumulator * 16L + hexValue(text[index])
+ empty = false
+ index++
+ }
+ } else {
+ val limit = Long.MAX_VALUE / 10L
+ val lastDigit = (Long.MAX_VALUE % 10L).toInt()
+ while (index < text.length && text[index] in '0'..'9') {
+ val digit = text[index] - '0'
+ // One digit short of the limit the sign decides, since the
+ // negative range reaches one further than the positive one.
+ if (accumulator >= limit &&
+ (accumulator > limit || digit > lastDigit + (if (negative) 1 else 0))
+ ) {
+ return null
+ }
+ accumulator = accumulator * 10L + digit
+ empty = false
+ index++
+ }
+ }
+ index = skipSpaces(text, index)
+ if (empty || index != text.length) return null
+ return if (negative) -accumulator else accumulator
+ }
+
+ /**
+ * The float denoted by [text], or `null` if it denotes something else.
+ *
+ * `inf` and `nan` are deliberately not accepted: Lua has no literal for
+ * either, and letting them through here would invent one.
+ */
+ fun parseFloat(text: String): Double? {
+ val mode = text.firstOrNull { it == '.' || it == 'x' || it == 'X' || it == 'n' || it == 'N' }
+ if (mode == 'n' || mode == 'N') return null
+ return if (mode == 'x' || mode == 'X') parseHexFloat(text) else parseDecimalFloat(text)
+ }
+
+ /** C's `strtod` on a plain decimal numeral, with nothing left over. */
+ private fun parseDecimalFloat(text: String): Double? {
+ var index = skipSpaces(text, 0)
+ val start = index
+ if (index < text.length && (text[index] == '-' || text[index] == '+')) index++
+ var digits = 0
+ while (index < text.length && text[index] in '0'..'9') {
+ index++
+ digits++
+ }
+ if (index < text.length && text[index] == '.') {
+ index++
+ while (index < text.length && text[index] in '0'..'9') {
+ index++
+ digits++
+ }
+ }
+ if (digits == 0) return null
+ if (index < text.length && (text[index] == 'e' || text[index] == 'E')) {
+ var lookahead = index + 1
+ if (lookahead < text.length && (text[lookahead] == '-' || text[lookahead] == '+')) lookahead++
+ var exponentDigits = 0
+ while (lookahead < text.length && text[lookahead] in '0'..'9') {
+ lookahead++
+ exponentDigits++
+ }
+ // An 'e' with no digits after it is not part of the numeral, so it
+ // is left behind for the trailing-character check to reject.
+ if (exponentDigits > 0) index = lookahead
+ }
+ val numeral = text.substring(start, index)
+ if (skipSpaces(text, index) != text.length) return null
+ // The numeral is already known to be well formed, so the platform
+ // parser is only being asked to round it correctly.
+ return numeral.toDoubleOrNull()
+ }
+
+ /** C's `strtod` on a hexadecimal numeral, as `lua_strx2number` reads it. */
+ private fun parseHexFloat(text: String): Double? {
+ var index = skipSpaces(text, 0)
+ var negative = false
+ if (index < text.length && (text[index] == '-' || text[index] == '+')) {
+ negative = text[index] == '-'
+ index++
+ }
+ if (index + 1 >= text.length || text[index] != '0') return null
+ if (text[index + 1] != 'x' && text[index + 1] != 'X') return null
+ index += 2
+
+ var mantissa = 0.0
+ var significant = 0
+ var insignificant = 0
+ var exponent = 0
+ var seenDot = false
+ while (index < text.length) {
+ val c = text[index]
+ if (c == '.') {
+ if (seenDot) break
+ seenDot = true
+ } else if (isHexDigit(c)) {
+ if (significant == 0 && c == '0') {
+ insignificant++
+ } else if (++significant <= MAX_SIGNIFICANT_HEX_DIGITS) {
+ mantissa = mantissa * 16.0 + hexValue(c)
+ } else {
+ // Past the accumulator's reach: the digit still shifts the
+ // value even though its own contribution is lost.
+ exponent++
+ }
+ if (seenDot) exponent--
+ } else {
+ break
+ }
+ index++
+ }
+ if (significant + insignificant == 0) return null
+ exponent *= 4 // each hex digit is four binary ones
+
+ if (index < text.length && (text[index] == 'p' || text[index] == 'P')) {
+ index++
+ var negativeExponent = false
+ if (index < text.length && (text[index] == '-' || text[index] == '+')) {
+ negativeExponent = text[index] == '-'
+ index++
+ }
+ if (index >= text.length || text[index] !in '0'..'9') return null
+ var value = 0
+ while (index < text.length && text[index] in '0'..'9') {
+ value = value * 10 + (text[index] - '0')
+ index++
+ }
+ exponent += if (negativeExponent) -value else value
+ }
+ if (skipSpaces(text, index) != text.length) return null
+ if (negative) mantissa = -mantissa
+ return ldexp(mantissa, exponent)
+ }
+
+ /**
+ * The integer [text] denotes in [base], wrapping on overflow.
+ *
+ * This is `tonumber`'s two-argument form, which unlike the one-argument
+ * form never produces a float.
+ */
+ fun parseInteger(text: String, base: Int): Long? {
+ if (base < 2 || base > 36) return null
+ var index = skipSpaces(text, 0)
+ var negative = false
+ if (index < text.length && (text[index] == '-' || text[index] == '+')) {
+ negative = text[index] == '-'
+ index++
+ }
+ var accumulator = 0L
+ var empty = true
+ while (index < text.length) {
+ val digit = digitValue(text[index])
+ if (digit < 0 || digit >= base) break
+ accumulator = accumulator * base + digit
+ empty = false
+ index++
+ }
+ index = skipSpaces(text, index)
+ if (empty || index != text.length) return null
+ return if (negative) -accumulator else accumulator
+ }
+
+ /** `value * 2^exponent`, split so no single step leaves the double range. */
+ private fun ldexp(value: Double, exponent: Int): Double {
+ var result = value
+ var remaining = exponent
+ while (remaining > 1000) {
+ result *= TWO_POW_1000
+ remaining -= 1000
+ }
+ while (remaining < -1000) {
+ result /= TWO_POW_1000
+ remaining += 1000
+ }
+ var step = 1.0
+ var factor = 2.0
+ var count = if (remaining < 0) -remaining else remaining
+ while (count > 0) {
+ if (count and 1 == 1) step *= factor
+ factor *= factor
+ count = count shr 1
+ }
+ return if (remaining < 0) result / step else result * step
+ }
+
+ private val TWO_POW_1000: Double = run {
+ var result = 1.0
+ repeat(1000) { result *= 2.0 }
+ result
+ }
+
+ private fun skipSpaces(text: String, from: Int): Int {
+ var index = from
+ while (index < text.length && isSpace(text[index])) index++
+ return index
+ }
+
+ /** The characters C's `isspace` accepts, which is what `strtod` skips. */
+ private fun isSpace(c: Char): Boolean =
+ c == ' ' || c == '\t' || c == '\n' || c == '\r' || c.code == 0x0B || c.code == 0x0C
+
+ private fun isHexDigit(c: Char): Boolean =
+ c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F'
+
+ private fun hexValue(c: Char): Int = when {
+ c in '0'..'9' -> c - '0'
+ c in 'a'..'f' -> c - 'a' + 10
+ else -> c - 'A' + 10
+ }
+
+ private fun digitValue(c: Char): Int = when {
+ c in '0'..'9' -> c - '0'
+ c in 'a'..'z' -> c - 'a' + 10
+ c in 'A'..'Z' -> c - 'A' + 10
+ else -> -1
+ }
+}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Platform.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Platform.kt
index 9bbe6750..cad865a8 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Platform.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Platform.kt
@@ -23,6 +23,34 @@ internal expect fun platformProperty(name: String): String?
internal expect fun platformEnvironment(name: String): String?
internal expect fun platformExit(code: Int)
internal expect fun platformCollectGarbage()
+
+/**
+ * Watches [target] so that it joins [pending] once nothing refers to it.
+ *
+ * This is what stands in for Lua marking an object for finalization. The
+ * answer is a keeper the caller has to hang on to from [target] itself: it
+ * lives exactly as long as the object does, and hands the object back when
+ * that ends, which is the resurrection a `__gc` handler needs to be given the
+ * object it is finalizing.
+ *
+ * Only a host that can resurrect an object it is about to reclaim can do this;
+ * where the host cannot, the answer is null and `__gc` never runs.
+ */
+internal expect fun watchForFinalization(target: LuaValue, pending: MutableList): Any?
+
+/** Takes what has been collected out of [pending], emptying it. */
+internal expect fun takeFinalized(pending: MutableList): List
+
+/**
+ * True when [failure] is the host running out of call stack.
+ *
+ * The interpreter recurses on the host's stack, so a Lua program that recurses
+ * without bound exhausts that rather than a stack of Lua's own. That stack is
+ * this port's counterpart of the C stack a reference build runs out of, so it
+ * is reported the same way - "C stack overflow", which a `pcall` can catch -
+ * instead of letting a host error escape.
+ */
+internal expect fun platformIsStackOverflow(failure: Throwable): Boolean
internal expect fun platformUsedMemory(): Long
internal expect fun platformLoadLibrary(className: String, globals: Globals): LuaValue?
internal expect fun platformTypeName(type: KClass<*>): String
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Print.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Print.kt
index 07a38e0c..059f16df 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Print.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Print.kt
@@ -78,6 +78,15 @@ class Print : Lua() {
"CLOSURE",
"VARARG",
"EXTRAARG",
+ "IDIV",
+ "BAND",
+ "BOR",
+ "BXOR",
+ "SHL",
+ "SHR",
+ "BNOT",
+ "TBC",
+ "ERRNNIL",
null,
)
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Prototype.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Prototype.kt
index aa0e11a9..0aeb8cec 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Prototype.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Prototype.kt
@@ -123,12 +123,16 @@ class Prototype {
return null /* not found */
}
- fun shortsource(): String {
- var name: String = source?.tojstring() ?: "?"
- if (name.startsWith("@") || name.startsWith("=")) name = name.substring(1)
- else if (name.startsWith("\u001b")) name = "binary string"
- return name
- }
+ /**
+ * The source name as it appears in an error message or a traceback.
+ *
+ * This is upstream's `luaO_chunkid`. A name given as `@file` or `=text`
+ * loses its marker and is shortened from the front or the back as needed;
+ * anything else is the chunk's own text, which is quoted as
+ * `[string "..."]` and cut at the first newline so a message stays on one
+ * line.
+ */
+ fun shortsource(): String = Lua.chunkid(source?.tojstring() ?: "=?")
companion object {
private val NOUPVALUES: Array = arrayOf()
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Upvaldesc.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Upvaldesc.kt
index 5047b0e2..5e20da13 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Upvaldesc.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Upvaldesc.kt
@@ -16,7 +16,7 @@
******************************************************************************/
package net.blueva.luak
-class Upvaldesc(name: LuaString?, instack: Boolean, idx: Int) {
+class Upvaldesc(name: LuaString?, instack: Boolean, idx: Int, kind: Int = 0) {
/* upvalue name (for debug information) */
var name: LuaString?
@@ -26,10 +26,19 @@ class Upvaldesc(name: LuaString?, instack: Boolean, idx: Int) {
/* index of upvalue (in stack or in outer function's list) */
val idx: Short
+ /**
+ * How the captured variable was declared: plain, `` or ``.
+ *
+ * Carried through so that assigning to a `` from an inner function -
+ * where it is an upvalue rather than a local - is caught just the same.
+ */
+ val kind: Int
+
init {
this.name = name
this.instack = instack
this.idx = idx.toShort()
+ this.kind = kind
}
override fun toString(): String {
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Varargs.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Varargs.kt
index d488cf16..81899370 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Varargs.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/Varargs.kt
@@ -24,12 +24,25 @@ import kotlin.reflect.KClass
* so it reads like real Lua's "bad argument #N: ...".
* The interpreter may further enrich it with the calling function's name.
*/
-private inline fun withArgIndex(i: Int, block: () -> T): T {
+/**
+ * Runs [block], stamping argument index [i] onto any argument error it raises.
+ *
+ * A check made on a value alone - `arg.checkdouble()` rather than
+ * `args.checkdouble(1)` - has no way to know which argument the value came
+ * from, so the index is attached here, where it is known.
+ */
+/** What a conversion says when a float denotes no integer. */
+private const val NOT_AN_INTEGER = "number has no integer representation"
+
+internal inline fun withArgIndex(i: Int, block: () -> T): T {
try {
return block()
} catch (e: LuaError) {
+ // Only the complaints a conversion raises about the value itself.
+ // Anything else the call raised is its own error and must not be
+ // relabelled as a complaint about the arguments it was given.
val m = e.message
- if (m != null && !m.startsWith("bad argument #")) {
+ if (m != null && (m.startsWith("bad argument: ") || m == NOT_AN_INTEGER)) {
e.argMessageOverride = "bad argument #" + i + ": " + m.removePrefix("bad argument: ")
}
throw e
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/WeakTable.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/WeakTable.kt
index d16cbcd7..95298ac4 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/WeakTable.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/WeakTable.kt
@@ -177,6 +177,16 @@ class WeakTable(private val weakkeys: Boolean, private val weakvalues: Boolean,
protected abstract fun copy(next: Slot?): WeakSlot?
}
+ /**
+ * An entry of a table whose keys are weak, which is to say an ephemeron.
+ *
+ * The value is held weakly here and strongly by the key, so that it lives
+ * exactly as long as the key does. Holding it here instead would keep
+ * alive every key its value happens to refer to, which is the difference
+ * between a weak-key table and one that is merely inconvenient: a chain
+ * of entries each pointing at the key of the next would then never go,
+ * however little else referred to it. See [LuaValue.pinned].
+ */
internal class WeakKeySlot : WeakSlot {
private val keyhash: Int
@@ -184,8 +194,13 @@ class WeakTable(private val weakkeys: Boolean, private val weakvalues: Boolean,
key: LuaValue,
value: LuaValue?,
next: Slot?
- ) : super(net.blueva.luak.WeakTable.Companion.weaken(key), value, next) {
+ ) : super(
+ net.blueva.luak.WeakTable.Companion.weaken(key),
+ net.blueva.luak.WeakTable.Companion.weaken(value!!),
+ next,
+ ) {
keyhash = key.hashCode()
+ net.blueva.luak.WeakTable.Companion.pin(key, value)
}
protected constructor(copyFrom: WeakKeySlot, next: Slot?) : super(copyFrom.key, copyFrom.value, next) {
@@ -197,7 +212,15 @@ class WeakTable(private val weakkeys: Boolean, private val weakvalues: Boolean,
}
override fun set(value: LuaValue?): Slot? {
- this.value = value
+ val key: LuaValue? = strongkey()
+ if (key != null) {
+ net.blueva.luak.WeakTable.Companion.unpin(
+ key,
+ net.blueva.luak.WeakTable.Companion.strengthen(this.value),
+ )
+ if (value != null) net.blueva.luak.WeakTable.Companion.pin(key, value)
+ }
+ this.value = if (value == null) null else net.blueva.luak.WeakTable.Companion.weaken(value)
return this
}
@@ -205,6 +228,10 @@ class WeakTable(private val weakkeys: Boolean, private val weakvalues: Boolean,
return net.blueva.luak.WeakTable.Companion.strengthen(key)
}
+ override fun strongvalue(): LuaValue? {
+ return net.blueva.luak.WeakTable.Companion.strengthen(value)
+ }
+
override fun copy(rest: Slot?): WeakSlot {
return net.blueva.luak.WeakTable.WeakKeySlot(this, rest)
}
@@ -378,6 +405,45 @@ class WeakTable(private val weakkeys: Boolean, private val weakvalues: Boolean,
* @param value value to convert
* @return [LuaValue] that is a strong or weak reference, depending on type of `value`
*/
+ /**
+ * Has [key] hold [value] for as long as it lives; see [WeakKeySlot].
+ *
+ * A key used in more than one table holds a list of what it keeps.
+ */
+ @Suppress("UNCHECKED_CAST")
+ internal fun pin(key: LuaValue, value: LuaValue) {
+ when (val held: Any? = key.pinned) {
+ null -> key.pinned = value
+ is ArrayList<*> -> (held as ArrayList).add(value)
+ else -> {
+ val list: ArrayList = ArrayList(2)
+ list.add(held as LuaValue)
+ list.add(value)
+ key.pinned = list
+ }
+ }
+ }
+
+ /** Undoes one [pin]; the entry it belonged to is gone or replaced. */
+ @Suppress("UNCHECKED_CAST")
+ internal fun unpin(key: LuaValue, value: LuaValue?) {
+ if (value == null) return
+ val held: Any? = key.pinned
+ if (held === value) {
+ key.pinned = null
+ } else if (held is ArrayList<*>) {
+ val list: ArrayList = held as ArrayList
+ var i = 0
+ while (i < list.size) {
+ if (list[i] === value) {
+ list.removeAt(i)
+ return
+ }
+ i++
+ }
+ }
+ }
+
protected fun weaken(value: LuaValue): LuaValue {
when (value.type()) {
LuaValue.TFUNCTION, LuaValue.TTHREAD, LuaValue.TTABLE -> return net.blueva.luak.WeakTable.WeakValue(
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Block.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Block.kt
deleted file mode 100644
index b9f98e11..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Block.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Based on LuaJ (https://luaj.org)
- * Original work Copyright (c) 2009 Luaj.org
- * Modifications Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.ast
-
-class Block : Stat() {
- var stats: MutableList = ArrayList()
- var scope: NameScope? = null
-
- fun add(s: Stat?) {
- if (s == null) return
- stats.add(s)
- }
-
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Chunk.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Chunk.kt
deleted file mode 100644
index 2b01d9f5..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Chunk.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Based on LuaJ (https://luaj.org)
- * Original work Copyright (c) 2009 Luaj.org
- * Modifications Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.ast
-
-class Chunk(val block: Block?) : SyntaxElement() {
- fun accept(visitor: Visitor) {
- visitor.visit(this)
- }
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Exp.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Exp.kt
deleted file mode 100644
index d3627dc7..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Exp.kt
+++ /dev/null
@@ -1,257 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Based on LuaJ (https://luaj.org)
- * Original work Copyright (c) 2009 Luaj.org
- * Modifications Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.ast
-
-import net.blueva.luak.Lua
-import net.blueva.luak.LuaValue
-
-abstract
-class Exp : SyntaxElement() {
- abstract fun accept(visitor: Visitor?)
-
- open fun isvarexp(): Boolean {
- return false
- }
-
- open fun isfunccall(): Boolean {
- return false
- }
-
- open fun isvarargexp(): Boolean {
- return false
- }
-
- abstract class PrimaryExp : Exp() {
- override fun isvarexp(): Boolean {
- return false
- }
-
- override fun isfunccall(): Boolean {
- return false
- }
- }
-
- abstract class VarExp : PrimaryExp() {
- override fun isvarexp(): Boolean {
- return true
- }
-
- open fun markHasAssignment() {
- }
- }
-
- class NameExp(name: String?) : VarExp() {
- val name: Name
-
- init {
- this.name = Name(name)
- }
-
- override fun markHasAssignment() {
- name.variable!!.hasassignments = true
- }
-
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class ParensExp(val exp: Exp?) : PrimaryExp() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class FieldExp(val lhs: PrimaryExp?, name: String?) : VarExp() {
- val name: Name
-
- init {
- this.name = Name(name)
- }
-
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class IndexExp(val lhs: PrimaryExp?, val exp: Exp?) : VarExp() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- open class FuncCall(val lhs: PrimaryExp?, val args: FuncArgs?) : PrimaryExp() {
- override fun isfunccall(): Boolean {
- return true
- }
-
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
-
- override fun isvarargexp(): Boolean {
- return true
- }
- }
-
- class MethodCall(lhs: PrimaryExp?, val name: String, args: FuncArgs?) : FuncCall(lhs, args) {
- override fun isfunccall(): Boolean {
- return true
- }
-
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class Constant(val value: LuaValue?) : Exp() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class VarargsExp : Exp() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
-
- override fun isvarargexp(): Boolean {
- return true
- }
- }
-
- class UnopExp(val op: Int, val rhs: Exp?) : Exp() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class BinopExp(val lhs: Exp?, val op: Int, val rhs: Exp?) : Exp() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class AnonFuncDef(val body: FuncBody?) : Exp() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- companion object {
- fun constant(value: LuaValue?): Exp {
- return Constant(value)
- }
-
- fun numberconstant(token: String?): Exp {
- return Constant(LuaValue.valueOf(token).tonumber())
- }
-
- fun varargs(): Exp {
- return VarargsExp()
- }
-
- fun tableconstructor(tc: TableConstructor?): Exp? {
- return tc
- }
-
- fun unaryexp(op: Int, rhs: Exp?): Exp? {
- if (rhs is BinopExp) {
- val b = rhs
- if (precedence(op) > precedence(b.op)) return binaryexp(unaryexp(op, b.lhs), b.op, b.rhs)
- }
- return UnopExp(op, rhs)
- }
-
- fun binaryexp(lhs: Exp?, op: Int, rhs: Exp?): Exp? {
- if (lhs is UnopExp) {
- val u = lhs
- if (precedence(op) > precedence(u.op)) return unaryexp(u.op, binaryexp(u.rhs, op, rhs))
- }
- // TODO: cumulate string concatenations together
- // TODO: constant folding
- if (lhs is BinopExp) {
- val b = lhs
- if ((precedence(op) > precedence(b.op)) ||
- ((precedence(op) == precedence(b.op)) && isrightassoc(op))
- ) return binaryexp(b.lhs, b.op, binaryexp(b.rhs, op, rhs))
- }
- if (rhs is BinopExp) {
- val b = rhs
- if ((precedence(op) > precedence(b.op)) ||
- ((precedence(op) == precedence(b.op)) && !isrightassoc(op))
- ) return binaryexp(binaryexp(lhs, op, b.lhs), b.op, b.rhs)
- }
- return BinopExp(lhs, op, rhs)
- }
-
- fun isrightassoc(op: Int): Boolean {
- when (op) {
- Lua.OP_CONCAT, Lua.OP_POW -> return true
- else -> return false
- }
- }
-
- fun precedence(op: Int): Int {
- when (op) {
- Lua.OP_OR -> return 0
- Lua.OP_AND -> return 1
- Lua.OP_LT, Lua.OP_GT, Lua.OP_LE, Lua.OP_GE, Lua.OP_NEQ, Lua.OP_EQ -> return 2
- Lua.OP_CONCAT -> return 3
- Lua.OP_ADD, Lua.OP_SUB -> return 4
- Lua.OP_MUL, Lua.OP_DIV, Lua.OP_MOD -> return 5
- Lua.OP_NOT, Lua.OP_UNM, Lua.OP_LEN -> return 6
- Lua.OP_POW -> return 7
- else -> throw IllegalStateException("precedence of bad op " + op)
- }
- }
-
- fun anonymousfunction(funcbody: FuncBody?): Exp {
- return AnonFuncDef(funcbody)
- }
-
- /** foo */
- fun nameprefix(name: String?): NameExp {
- return NameExp(name)
- }
-
- /** ( foo.bar ) */
- fun parensprefix(exp: Exp?): ParensExp {
- return ParensExp(exp)
- }
-
- /** foo[exp] */
- fun indexop(lhs: PrimaryExp?, exp: Exp?): IndexExp {
- return IndexExp(lhs, exp)
- }
-
- /** foo.bar */
- fun fieldop(lhs: PrimaryExp?, name: String?): FieldExp {
- return FieldExp(lhs, name)
- }
-
- /** foo(2,3) */
- fun functionop(lhs: PrimaryExp?, args: FuncArgs?): FuncCall {
- return FuncCall(lhs, args)
- }
-
- /** foo:bar(4,5) */
- fun methodop(lhs: PrimaryExp?, name: String, args: FuncArgs?): MethodCall {
- return MethodCall(lhs, name, args)
- }
- }
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/FuncArgs.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/FuncArgs.kt
deleted file mode 100644
index 5ee79ead..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/FuncArgs.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Based on LuaJ (https://luaj.org)
- * Original work Copyright (c) 2009 Luaj.org
- * Modifications Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.ast
-
-import net.blueva.luak.LuaString
-
-class FuncArgs : SyntaxElement {
- val exps: MutableList?
-
- constructor(exps: MutableList?) {
- this.exps = exps
- }
-
- constructor(string: LuaString?) {
- this.exps = ArrayList()
- this.exps.add(Exp.Companion.constant(string))
- }
-
- constructor(table: TableConstructor?) {
- this.exps = ArrayList()
- this.exps.add(table)
- }
-
- fun accept(visitor: Visitor) {
- visitor.visit(this)
- }
-
- companion object {
- /** exp1,exp2... */
- fun explist(explist: MutableList?): FuncArgs {
- return FuncArgs(explist)
- }
-
- /** {...} */
- fun tableconstructor(table: TableConstructor?): FuncArgs {
- return FuncArgs(table)
- }
-
- /** "mylib" */
- fun string(string: LuaString?): FuncArgs {
- return FuncArgs(string)
- }
- }
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/FuncBody.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/FuncBody.kt
deleted file mode 100644
index ab9f0d7d..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/FuncBody.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Based on LuaJ (https://luaj.org)
- * Original work Copyright (c) 2009 Luaj.org
- * Modifications Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.ast
-
-class FuncBody(parlist: ParList?, var block: Block?) : SyntaxElement() {
- var parlist: ParList?
- var scope: NameScope? = null
-
- init {
- this.parlist = if (parlist != null) parlist else ParList.Companion.EMPTY_PARLIST
- }
-
- fun accept(visitor: Visitor) {
- visitor.visit(this)
- }
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/FuncName.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/FuncName.kt
deleted file mode 100644
index 304d9a58..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/FuncName.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Based on LuaJ (https://luaj.org)
- * Original work Copyright (c) 2009 Luaj.org
- * Modifications Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.ast
-
-class FuncName(name: String?) : SyntaxElement() {
- // example: a.b.c.d:e
- // initial base name: "a"
- val name: Name
-
- // intermediate field accesses: "b", "c", "d"
- var dots: MutableList? = null
-
- // optional final method name: "e"
- var method: String? = null
-
- init {
- this.name = Name(name)
- }
-
- fun adddot(dot: String?) {
- if (dots == null) dots = ArrayList()
- dots!!.add(dot)
- }
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Name.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Name.kt
deleted file mode 100644
index 81299208..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Name.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Based on LuaJ (https://luaj.org)
- * Original work Copyright (c) 2009 Luaj.org
- * Modifications Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.ast
-
-
-class Name(val name: String?) {
- var variable: Variable? = null
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/NameResolver.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/NameResolver.kt
deleted file mode 100644
index 5543735f..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/NameResolver.kt
+++ /dev/null
@@ -1,145 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Based on LuaJ (https://luaj.org)
- * Original work Copyright (c) 2009 Luaj.org
- * Modifications Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.ast
-
-import net.blueva.luak.LuaValue
-import net.blueva.luak.ast.Exp.NameExp
-import net.blueva.luak.ast.Exp.VarExp
-import net.blueva.luak.ast.Stat.*
-
-/**
- * Visitor that resolves names to scopes.
- * Each Name is resolved to a NamedVarible, possibly in a NameScope
- * if it is a local, or in no named scope if it is a global.
- */
-class NameResolver : Visitor() {
- private var scope: NameScope? = null
-
- private fun pushScope() {
- scope = NameScope(scope)
- }
-
- private fun popScope() {
- scope = scope!!.outerScope
- }
-
- override fun visit(scope: NameScope?) {
- }
-
- override fun visit(block: Block) {
- pushScope()
- block.scope = scope
- super.visit(block)
- popScope()
- }
-
- override fun visit(body: FuncBody) {
- pushScope()
- scope!!.functionNestingCount++
- body.scope = scope
- super.visit(body)
- popScope()
- }
-
- override fun visit(stat: LocalFuncDef) {
- defineLocalVar(stat.name)
- super.visit(stat)
- }
-
- override fun visit(stat: NumericFor) {
- pushScope()
- stat.scope = scope
- defineLocalVar(stat.name)
- super.visit(stat)
- popScope()
- }
-
- override fun visit(stat: GenericFor) {
- pushScope()
- stat.scope = scope
- stat.names?.let { defineLocalVars(it) }
- super.visit(stat)
- popScope()
- }
-
- override fun visit(exp: NameExp) {
- exp.name.variable = resolveNameReference(exp.name)
- super.visit(exp)
- }
-
- override fun visit(stat: FuncDef) {
- stat.name?.let {
- it.name.variable = resolveNameReference(it.name)
- it.name.variable!!.hasassignments = true
- }
- super.visit(stat)
- }
-
- override fun visit(stat: Assign) {
- super.visit(stat)
- val vars = stat.vars ?: return
- var i = 0
- val n = vars.size
- while (i < n) {
- val v = vars[i] as VarExp
- v.markHasAssignment()
- i++
- }
- }
-
- override fun visit(stat: LocalAssign) {
- visitExps(stat.values)
- stat.names?.let { defineLocalVars(it) }
- val names = stat.names ?: return
- val values = stat.values
- val n = names.size
- val m = values?.size ?: 0
- val isvarlist = m > 0 && m < n && (values!![m - 1] as Exp).isvarargexp()
- var i = 0
- while (i < n && i < (if (isvarlist) m - 1 else m)) {
- if (values!![i] is Exp.Constant) (names[i] as Name).variable!!.initialValue =
- (values[i] as Exp.Constant).value
- i++
- }
- if (!isvarlist) for (j in m..) {
- var i = 0
- val n = names.size
- while (i < n) {
- defineLocalVar(names[i] as Name)
- i++
- }
- }
-
- protected fun defineLocalVar(name: Name) {
- name.variable = scope!!.define(name.name)
- }
-
- protected fun resolveNameReference(name: Name): Variable {
- val v = scope!!.find(name.name)!!
- if (v.isLocal && scope!!.functionNestingCount != v.definingScope!!.functionNestingCount) v.isupvalue = true
- return v
- }
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/NameScope.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/NameScope.kt
deleted file mode 100644
index 510c3edb..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/NameScope.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Based on LuaJ (https://luaj.org)
- * Original work Copyright (c) 2009 Luaj.org
- * Modifications Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.ast
-
-class NameScope {
- val namedVariables: MutableMap = HashMap()
-
- val outerScope: NameScope?
-
- var functionNestingCount: Int
-
- /** Construct default names scope */
- constructor() {
- this.outerScope = null
- this.functionNestingCount = 0
- }
-
- /** Construct name scope within another scope */
- constructor(outerScope: NameScope?) {
- this.outerScope = outerScope
- this.functionNestingCount = if (outerScope != null) outerScope.functionNestingCount else 0
- }
-
- /** Look up a name. If it is a global name, then throw IllegalArgumentException. */
- @Throws(IllegalArgumentException::class)
- fun find(name: String?): Variable? {
- validateIsNotKeyword(name)
- var n: NameScope? = this
- while (n != null) {
- if (n.namedVariables.containsKey(name)) return n.namedVariables.get(name)
- n = n.outerScope
- }
- val value = Variable(name)
- this.namedVariables.put(name, value)
- return value
- }
-
- /** Define a name in this scope. If it is a global name, then throw IllegalArgumentException. */
- @Throws(IllegalStateException::class, IllegalArgumentException::class)
- fun define(name: String?): Variable {
- validateIsNotKeyword(name)
- val value = Variable(name, this)
- this.namedVariables.put(name, value)
- return value
- }
-
- private fun validateIsNotKeyword(name: String?) {
- require(!LUA_KEYWORDS.contains(name)) { "name is a keyword: '" + name + "'" }
- }
-
- companion object {
- private val LUA_KEYWORDS: MutableSet = HashSet()
-
- init {
- val k: Array = arrayOf(
- "and", "break", "do", "else", "elseif", "end",
- "false", "for", "function", "if", "in", "local",
- "nil", "not", "or", "repeat", "return",
- "then", "true", "until", "while"
- )
- for (i in k.indices) LUA_KEYWORDS.add(k[i])
- }
- }
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/ParList.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/ParList.kt
deleted file mode 100644
index 97d17811..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/ParList.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Based on LuaJ (https://luaj.org)
- * Original work Copyright (c) 2009 Luaj.org
- * Modifications Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.ast
-
-class ParList(val names: MutableList?, val isvararg: Boolean) : SyntaxElement() {
- fun accept(visitor: Visitor) {
- visitor.visit(this)
- }
-
- companion object {
- val EMPTY_NAMELIST: MutableList = ArrayList()
- val EMPTY_PARLIST: ParList = ParList(EMPTY_NAMELIST, false)
- }
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Stat.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Stat.kt
deleted file mode 100644
index 365c30b6..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Stat.kt
+++ /dev/null
@@ -1,201 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Based on LuaJ (https://luaj.org)
- * Original work Copyright (c) 2009 Luaj.org
- * Modifications Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.ast
-
-import net.blueva.luak.ast.Exp.FuncCall
-import net.blueva.luak.ast.Exp.VarExp
-
-abstract
-class Stat : SyntaxElement() {
- abstract fun accept(visitor: Visitor?)
-
- class Goto(val name: String?) : Stat() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class Label(val name: String?) : Stat() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class Assign(val vars: MutableList?, val exps: MutableList?) : Stat() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class WhileDo(val exp: Exp?, val block: Block?) : Stat() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class RepeatUntil(val block: Block?, val exp: Exp?) : Stat() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class Break : Stat() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class Return(val values: MutableList?) : Stat() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
-
- fun nreturns(): Int {
- var n = if (values != null) values.size else 0
- if (n > 0 && (values!![n - 1] as Exp).isvarargexp()) n = -1
- return n
- }
- }
-
- class FuncCallStat(val funccall: FuncCall?) : Stat() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class LocalFuncDef(name: String?, val body: FuncBody?) : Stat() {
- val name: Name
-
- init {
- this.name = Name(name)
- }
-
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class FuncDef(val name: FuncName?, val body: FuncBody?) : Stat() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class GenericFor(var names: MutableList?, var exps: MutableList?, var block: Block?) : Stat() {
- var scope: NameScope? = null
-
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class NumericFor(name: String?, val initial: Exp?, val limit: Exp?, val step: Exp?, val block: Block?) : Stat() {
- val name: Name
- var scope: NameScope? = null
-
- init {
- this.name = Name(name)
- }
-
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class LocalAssign(val names: MutableList?, val values: MutableList?) : Stat() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- class IfThenElse(
- val ifexp: Exp?, val ifblock: Block?, val elseifexps: MutableList?,
- val elseifblocks: MutableList?, val elseblock: Block?
- ) : Stat() {
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
- }
-
- companion object {
- fun block(block: Block?): Stat? {
- return block
- }
-
- fun whiledo(exp: Exp?, block: Block?): Stat {
- return WhileDo(exp, block)
- }
-
- fun repeatuntil(block: Block?, exp: Exp?): Stat {
- return RepeatUntil(block, exp)
- }
-
- fun breakstat(): Stat {
- return Break()
- }
-
- fun returnstat(exps: MutableList?): Stat {
- return Return(exps)
- }
-
- fun assignment(vars: MutableList?, exps: MutableList?): Stat {
- return Assign(vars, exps)
- }
-
- fun functioncall(funccall: FuncCall?): Stat {
- return FuncCallStat(funccall)
- }
-
- fun localfunctiondef(name: String?, funcbody: FuncBody?): Stat {
- return LocalFuncDef(name, funcbody)
- }
-
- fun fornumeric(name: String?, initial: Exp?, limit: Exp?, step: Exp?, block: Block?): Stat {
- return NumericFor(name, initial, limit, step, block)
- }
-
- fun functiondef(funcname: FuncName?, funcbody: FuncBody?): Stat {
- return FuncDef(funcname, funcbody)
- }
-
- fun forgeneric(names: MutableList?, exps: MutableList?, block: Block?): Stat {
- return GenericFor(names, exps, block)
- }
-
- fun localassignment(names: MutableList?, values: MutableList?): Stat {
- return LocalAssign(names, values)
- }
-
- fun ifthenelse(
- ifexp: Exp?,
- ifblock: Block?,
- elseifexps: MutableList?,
- elseifblocks: MutableList?,
- elseblock: Block?
- ): Stat {
- return IfThenElse(ifexp, ifblock, elseifexps, elseifblocks, elseblock)
- }
-
- fun gotostat(name: String?): Stat {
- return Goto(name)
- }
-
- fun labelstat(name: String?): Stat {
- return Label(name)
- }
- }
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Str.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Str.kt
deleted file mode 100644
index 034626ae..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Str.kt
+++ /dev/null
@@ -1,137 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Based on LuaJ (https://luaj.org)
- * Original work Copyright (c) 2009 Luaj.org
- * Modifications Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.ast
-
-import net.blueva.luak.LuaString
-import net.blueva.luak.io.ByteArrayOutputStream
-
-object Str {
- fun quoteString(image: String): LuaString {
- val s = image.substring(1, image.length - 1)
- val bytes = unquote(s)
- return LuaString.valueUsing(bytes)
- }
-
- fun charString(image: String): LuaString {
- val s = image.substring(1, image.length - 1)
- val bytes = unquote(s)
- return LuaString.valueUsing(bytes)
- }
-
- fun longString(image: String): LuaString {
- val i = image.indexOf('[', image.indexOf('[') + 1) + 1
- val s = image.substring(i, image.length - i)
- val b = iso88591bytes(s)
- return LuaString.valueUsing(b)
- }
-
- fun iso88591bytes(s: String): ByteArray {
- return ByteArray(s.length) { index -> s[index].code.toByte() }
- }
-
- fun unquote(s: String): ByteArray {
- val baos = ByteArrayOutputStream()
- val c = s.toCharArray()
- val n = c.size
- var i = 0
- while (i < n) {
- if (c[i] == '\\' && i < n) {
- when (c[++i]) {
- '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> {
- var d = (c[i++].code - '0'.code)
- var j = 0
- while (i < n && j < 2 && c[i] >= '0' && c[i] <= '9') {
- d = d * 10 + (c[i].code - '0'.code)
- i++
- j++
- }
- baos.write(d.toByte().toInt())
- --i
- i++
- continue
- }
-
- 'a' -> {
- baos.write(7.toByte().toInt())
- i++
- continue
- }
-
- 'b' -> {
- baos.write('\b'.code.toByte().toInt())
- i++
- continue
- }
-
- 'f' -> {
- baos.write(0x0C)
- i++
- continue
- }
-
- 'n' -> {
- baos.write('\n'.code.toByte().toInt())
- i++
- continue
- }
-
- 'r' -> {
- baos.write('\r'.code.toByte().toInt())
- i++
- continue
- }
-
- 't' -> {
- baos.write('\t'.code.toByte().toInt())
- i++
- continue
- }
-
- 'v' -> {
- baos.write(11.toByte().toInt())
- i++
- continue
- }
-
- '"' -> {
- baos.write('"'.code.toByte().toInt())
- i++
- continue
- }
-
- '\'' -> {
- baos.write('\''.code.toByte().toInt())
- i++
- continue
- }
-
- '\\' -> {
- baos.write('\\'.code.toByte().toInt())
- i++
- continue
- }
-
- else -> baos.write(c[i].code.toByte().toInt())
- }
- } else {
- baos.write(c[i].code.toByte().toInt())
- }
- i++
- }
- return baos.toByteArray()
- }
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/SyntaxElement.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/SyntaxElement.kt
deleted file mode 100644
index 8b346e39..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/SyntaxElement.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Based on LuaJ (https://luaj.org)
- * Original work Copyright (c) 2009 Luaj.org
- * Modifications Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.ast
-
-/** Base class for syntax elements of the parse tree that appear in source files.
- * The LuaParser class will fill these values out during parsing for use in
- * syntax highlighting, for example.
- */
-open class SyntaxElement {
- /** The line number on which the element begins. */
- var beginLine: Int = 0
-
- /** The column at which the element begins. */
- var beginColumn: Short = 0
-
- /** The line number on which the element ends. */
- var endLine: Int = 0
-
- /** The column at which the element ends. */
- var endColumn: Short = 0
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/TableConstructor.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/TableConstructor.kt
deleted file mode 100644
index f91d9383..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/TableConstructor.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Based on LuaJ (https://luaj.org)
- * Original work Copyright (c) 2009 Luaj.org
- * Modifications Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.ast
-
-class TableConstructor : Exp() {
- var fields: MutableList? = null
-
- override fun accept(visitor: Visitor?) {
- visitor?.visit(this)
- }
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/TableField.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/TableField.kt
deleted file mode 100644
index b3f24769..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/TableField.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Based on LuaJ (https://luaj.org)
- * Original work Copyright (c) 2009 Luaj.org
- * Modifications Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.ast
-
-class TableField(val index: Exp?, val name: String?, val rhs: Exp?) : SyntaxElement() {
- fun accept(visitor: Visitor) {
- visitor.visit(this)
- }
-
- companion object {
- fun keyedField(index: Exp?, rhs: Exp?): TableField {
- return TableField(index, null, rhs)
- }
-
- fun namedField(name: String?, rhs: Exp?): TableField {
- return TableField(null, name, rhs)
- }
-
- fun listField(rhs: Exp?): TableField {
- return TableField(null, null, rhs)
- }
- }
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Variable.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Variable.kt
deleted file mode 100644
index 0e00afdc..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Variable.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Based on LuaJ (https://luaj.org)
- * Original work Copyright (c) 2009 Luaj.org
- * Modifications Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.ast
-
-import net.blueva.luak.LuaValue
-
-/** Variable is created lua name scopes, and is a named, lua variable that
- * either refers to a lua local, global, or upvalue storage location.
- */
-class Variable {
- /** The name as it appears in lua source code */
- val name: String?
-
- /** The lua scope in which this variable is defined. */
- val definingScope: NameScope?
-
- /** true if this variable is an upvalue */
- var isupvalue: Boolean = false
-
- /** true if there are assignments made to this variable */
- var hasassignments: Boolean = false
-
- /** When hasassignments == false, and the initial value is a constant, this is the initial value */
- var initialValue: LuaValue? = null
-
- /** Global is named variable not associated with a defining scope */
- constructor(name: String?) {
- this.name = name
- this.definingScope = null
- }
-
- constructor(name: String?, definingScope: NameScope?) {
- /** Local variable is defined in a particular scope. */
- this.name = name
- this.definingScope = definingScope
- }
-
- val isLocal: Boolean
- get() = this.definingScope != null
- val isConstant: Boolean
- get() = !hasassignments && initialValue != null
-}
\ No newline at end of file
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Visitor.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Visitor.kt
deleted file mode 100644
index e9cf3345..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/ast/Visitor.kt
+++ /dev/null
@@ -1,235 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Based on LuaJ (https://luaj.org)
- * Original work Copyright (c) 2009 Luaj.org
- * Modifications Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.ast
-
-import net.blueva.luak.ast.Exp.*
-import net.blueva.luak.ast.Stat.*
-
-abstract class Visitor {
- fun visit(chunk: Chunk) {
- chunk.block?.accept(this)
- }
-
- open fun visit(block: Block) {
- visit(block.scope)
- if (block.stats != null) {
- var i = 0
- val n = block.stats.size
- while (i < n) {
- (block.stats[i] as Stat).accept(this)
- i++
- }
- }
- }
-
- open fun visit(stat: Assign) {
- visitVars(stat.vars)
- visitExps(stat.exps)
- }
-
- fun visit(breakstat: Stat.Break?) {
- }
-
- fun visit(stat: FuncCallStat) {
- stat.funccall?.accept(this)
- }
-
- open fun visit(stat: FuncDef) {
- stat.body?.accept(this)
- }
-
- open fun visit(stat: GenericFor) {
- visit(stat.scope)
- visitNames(stat.names)
- visitExps(stat.exps)
- stat.block?.accept(this)
- }
-
- fun visit(stat: IfThenElse) {
- stat.ifexp?.accept(this)
- stat.ifblock?.accept(this)
- if (stat.elseifblocks != null && stat.elseifexps != null) {
- var i = 0
- val n = stat.elseifblocks.size
- while (i < n) {
- (stat.elseifexps[i] as Exp).accept(this)
- (stat.elseifblocks[i] as Block).accept(this)
- i++
- }
- }
- if (stat.elseblock != null) visit(stat.elseblock)
- }
-
- open fun visit(stat: LocalAssign) {
- visitNames(stat.names)
- visitExps(stat.values)
- }
-
- open fun visit(stat: LocalFuncDef) {
- visit(stat.name)
- stat.body?.accept(this)
- }
-
- open fun visit(stat: NumericFor) {
- visit(stat.scope)
- visit(stat.name)
- stat.initial?.accept(this)
- stat.limit?.accept(this)
- stat.step?.accept(this)
- stat.block?.accept(this)
- }
-
- fun visit(stat: RepeatUntil) {
- stat.block?.accept(this)
- stat.exp?.accept(this)
- }
-
- fun visit(stat: Stat.Return) {
- visitExps(stat.values)
- }
-
- fun visit(stat: WhileDo) {
- stat.exp?.accept(this)
- stat.block?.accept(this)
- }
-
- open fun visit(body: FuncBody) {
- visit(body.scope)
- body.parlist?.accept(this)
- body.block?.accept(this)
- }
-
- fun visit(args: FuncArgs) {
- visitExps(args.exps)
- }
-
- fun visit(field: TableField) {
- if (field.name != null) visit(field.name)
- field.index?.accept(this)
- field.rhs?.accept(this)
- }
-
- open fun visit(exp: AnonFuncDef) {
- exp.body?.accept(this)
- }
-
- fun visit(exp: BinopExp) {
- exp.lhs?.accept(this)
- exp.rhs?.accept(this)
- }
-
- fun visit(exp: Exp.Constant?) {
- }
-
- fun visit(exp: FieldExp) {
- exp.lhs?.accept(this)
- visit(exp.name)
- }
-
- fun visit(exp: FuncCall) {
- exp.lhs?.accept(this)
- exp.args?.accept(this)
- }
-
- fun visit(exp: IndexExp) {
- exp.lhs?.accept(this)
- exp.exp?.accept(this)
- }
-
- fun visit(exp: Exp.MethodCall) {
- exp.lhs?.accept(this)
- visit(exp.name)
- exp.args?.accept(this)
- }
-
- open fun visit(exp: NameExp) {
- visit(exp.name)
- }
-
- fun visit(exp: ParensExp) {
- exp.exp?.accept(this)
- }
-
- fun visit(exp: UnopExp) {
- exp.rhs?.accept(this)
- }
-
- fun visit(exp: VarargsExp?) {
- }
-
- open fun visit(pars: ParList) {
- visitNames(pars.names)
- }
-
- fun visit(table: TableConstructor) {
- val fields = table.fields ?: return
- var i = 0
- val n = fields.size
- while (i < n) {
- (fields[i] as TableField).accept(this)
- i++
- }
- }
-
- fun visitVars(vars: MutableList?) {
- if (vars != null) {
- var i = 0
- val n = vars.size
- while (i < n) {
- (vars[i] as VarExp).accept(this)
- i++
- }
- }
- }
-
- fun visitExps(exps: MutableList?) {
- if (exps != null) {
- var i = 0
- val n = exps.size
- while (i < n) {
- (exps[i] as Exp).accept(this)
- i++
- }
- }
- }
-
- fun visitNames(names: MutableList?) {
- if (names != null) {
- var i = 0
- val n = names.size
- while (i < n) {
- visit(names[i])
- i++
- }
- }
- }
-
- fun visit(name: Name?) {
- }
-
- fun visit(name: String?) {
- }
-
- open fun visit(scope: NameScope?) {
- }
-
- fun visit(gotostat: Goto?) {
- }
-
- fun visit(label: Stat.Label?) {
- }
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/compiler/DumpState.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/compiler/DumpState.kt
index a335855f..c10a15b3 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/compiler/DumpState.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/compiler/DumpState.kt
@@ -87,17 +87,44 @@ class DumpState(w: OutputStream?, strip: Boolean) {
}
}
+ /**
+ * Every string written so far, and where it was written.
+ *
+ * A string that appears more than once in a chunk - a constant several
+ * nested functions share, a name repeated in the debug information - is
+ * written once and pointed at afterwards. A chunk of any size is largely
+ * made of repeated names, so this is most of what keeps one small.
+ */
+ private val written: MutableMap = HashMap()
+
@kotlin.Throws(IOException::class)
- fun dumpString(s: LuaString) {
+ fun dumpString(s: LuaString?) {
+ // A chunk that was loaded without debug information has nothing to
+ // say here, and a length of zero is how the format says so.
+ if (s == null) {
+ dumpInt(0)
+ return
+ }
+ val already: Int? = written[s]
+ if (already != null) {
+ // Written before: where it was, rather than what it is.
+ dumpInt(-already)
+ return
+ }
val len: Int = s.len().toint()
dumpInt(len + 1)
s.write((writer)!!, 0, len)
writer!!.write(0)
+ written[s] = written.size + 1
}
@kotlin.Throws(IOException::class)
fun dumpDouble(d: Double) {
- val l: Long = (d).toBits()
+ dumpLong((d).toBits())
+ }
+
+ @kotlin.Throws(IOException::class)
+ fun dumpLong(l: Long) {
if (IS_LITTLE_ENDIAN) {
dumpInt(l.toInt())
dumpInt((l shr 32).toInt())
@@ -131,10 +158,16 @@ class DumpState(w: OutputStream?, strip: Boolean) {
}
LuaValue.TNUMBER -> when (NUMBER_FORMAT) {
- net.blueva.luak.compiler.DumpState.Companion.NUMBER_FORMAT_FLOATS_OR_DOUBLES -> {
- writer!!.write(LuaValue.TNUMBER)
- dumpDouble(o.todouble())
- }
+ net.blueva.luak.compiler.DumpState.Companion.NUMBER_FORMAT_FLOATS_OR_DOUBLES ->
+ if (o.isinttype()) {
+ // Tagged apart from a float, or the subtype would
+ // not survive the round trip.
+ writer!!.write(net.blueva.luak.LoadState.LUA_TNUMINT)
+ dumpLong(o.tolong())
+ } else {
+ writer!!.write(LuaValue.TNUMBER)
+ dumpDouble(o.todouble())
+ }
net.blueva.luak.compiler.DumpState.Companion.NUMBER_FORMAT_INTS_ONLY -> {
kotlin.require(!(!net.blueva.luak.compiler.DumpState.Companion.ALLOW_INTEGER_CASTING && !o.isint())) { "not an integer: " + o }
@@ -166,7 +199,7 @@ class DumpState(w: OutputStream?, strip: Boolean) {
dumpInt(n)
i = 0
while (i < n) {
- dumpFunction((f.p!![i])!!)
+ dumpFunction((f.p!![i])!!, f.source)
i++
}
}
@@ -185,8 +218,6 @@ class DumpState(w: OutputStream?, strip: Boolean) {
fun dumpDebug(f: Prototype) {
var i: Int
var n: Int
- if (strip) dumpInt(0)
- else dumpString((f.source)!!)
n = if (strip) 0 else f.lineinfo!!.size
dumpInt(n)
i = 0
@@ -199,7 +230,7 @@ class DumpState(w: OutputStream?, strip: Boolean) {
i = 0
while (i < n) {
val lvi: LocVars = f.locvars[i]!!
- dumpString((lvi.varname)!!)
+ dumpString(lvi.varname)
dumpInt(lvi.startpc)
dumpInt(lvi.endpc)
i++
@@ -208,13 +239,21 @@ class DumpState(w: OutputStream?, strip: Boolean) {
dumpInt(n)
i = 0
while (i < n) {
- dumpString((f.upvalues!![i]!!.name)!!)
+ dumpString(f.upvalues!![i]!!.name)
i++
}
}
@kotlin.Throws(IOException::class)
- fun dumpFunction(f: Prototype) {
+ @kotlin.jvm.JvmOverloads
+ fun dumpFunction(f: Prototype, psource: LuaString? = null) {
+ // Written before anything else, so that a nested function can be given
+ // it as it is read. A nested function almost always came from the same
+ // text as the one around it, and a chunk of any size would otherwise
+ // carry the same name once per function in it: nothing written here
+ // means "the same as the function this one is inside".
+ if (strip || f.source == psource) dumpInt(0)
+ else dumpString(f.source)
dumpInt(f.linedefined)
dumpInt(f.lastlinedefined)
dumpChar(f.numparams)
@@ -227,17 +266,35 @@ class DumpState(w: OutputStream?, strip: Boolean) {
}
@kotlin.Throws(IOException::class)
+ /**
+ * The head of a binary chunk, byte for byte as Lua 5.5 writes it.
+ *
+ * After the signature and the two bytes that say which Lua and which
+ * format wrote it comes a run of bytes chosen to be spoiled by anything
+ * that rewrites a file it does not understand, and then one value of each
+ * kind the rest of the chunk is written in: the size each takes and a
+ * known value of it, so a chunk written by a build that counts or orders
+ * bytes differently is refused rather than misread.
+ */
fun dumpHeader() {
writer!!.write(LoadState.LUA_SIGNATURE)
writer!!.write(LoadState.LUAC_VERSION)
writer!!.write(LoadState.LUAC_FORMAT)
- writer!!.write(if (IS_LITTLE_ENDIAN) 1 else 0)
+ writer!!.write(LoadState.LUAC_TAIL)
writer!!.write(net.blueva.luak.compiler.DumpState.Companion.SIZEOF_INT)
- writer!!.write(net.blueva.luak.compiler.DumpState.Companion.SIZEOF_SIZET)
+ dumpInt(LoadState.LUAC_INT)
writer!!.write(net.blueva.luak.compiler.DumpState.Companion.SIZEOF_INSTRUCTION)
+ dumpInt(LoadState.LUAC_INST)
+ writer!!.write(net.blueva.luak.compiler.DumpState.Companion.SIZEOF_LUA_INTEGER)
+ dumpLong(LoadState.LUAC_INT.toLong())
writer!!.write(SIZEOF_LUA_NUMBER)
- writer!!.write(NUMBER_FORMAT)
- writer!!.write(LoadState.LUAC_TAIL)
+ // A build that keeps every number as an integer has no float to check
+ // with, and says so by the size it just wrote.
+ if (NUMBER_FORMAT == net.blueva.luak.compiler.DumpState.Companion.NUMBER_FORMAT_INTS_ONLY) {
+ dumpInt(LoadState.LUAC_INT)
+ } else {
+ dumpDouble(LoadState.LUAC_NUM)
+ }
}
companion object {
@@ -257,6 +314,9 @@ class DumpState(w: OutputStream?, strip: Boolean) {
val NUMBER_FORMAT_DEFAULT: Int = net.blueva.luak.compiler.DumpState.Companion.NUMBER_FORMAT_FLOATS_OR_DOUBLES
private const val SIZEOF_INT = 4
+
+ /** How many bytes a Lua integer takes in a chunk. */
+ private const val SIZEOF_LUA_INTEGER = 8
private const val SIZEOF_SIZET = 4
private const val SIZEOF_INSTRUCTION = 4
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/compiler/FuncState.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/compiler/FuncState.kt
index 5f13b3ca..092956e3 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/compiler/FuncState.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/compiler/FuncState.kt
@@ -30,10 +30,19 @@ import net.blueva.luak.compiler.LexState.expdesc
internal class FuncState internal constructor() : Constants() {
internal class BlockCnt {
var previous: BlockCnt? = null /* chain */
+ var firstglobal: Int = 0 /* number of global declarations outside the block */
var firstlabel: Short = 0 /* index of first label in this block */
var firstgoto: Short = 0 /* index of first pending goto in this block */
var nactvar: Short = 0 /* # active locals outside the breakable structure */
var upval: Boolean = false /* true if some variable in the block is an upvalue */
+
+ /**
+ * True inside the scope of a to-be-closed variable.
+ *
+ * A return from here cannot be a tail call: the frame has to stay
+ * around long enough to run the pending `__close` handlers.
+ */
+ var insidetbc: Boolean = false
var isloop: Boolean = false /* true if `block' is a loop */
}
@@ -48,6 +57,7 @@ internal class FuncState internal constructor() : Constants() {
var nk: Int = 0 /* number of elements in `k' */
var np: Int = 0 /* number of elements in `p' */
var firstlocal: Int = 0 /* index of first local var (in Dyndata array) */
+ var firstlabel: Int = 0 /* index of first label of this function */
var nlocvars: Short = 0 /* number of elements in `locvars' */
var nactvar: Short = 0 /* number of active local variables */
var nups: Short = 0 /* number of upvalues */
@@ -76,14 +86,20 @@ internal class FuncState internal constructor() : Constants() {
// =============================================================
// from lparser.c
// =============================================================
- /* check for repeated labels on the same block */
+ /**
+ * Rejects a label already defined anywhere in the current function.
+ *
+ * The search starts at the function's first label rather than the block's:
+ * an inner block can see a label declared outside it, so repeating the name
+ * there would leave two candidates for the same `goto`.
+ */
fun checkrepeated(ll: Array, ll_n: Int, label: LuaString) {
var i: Int
- i = bl!!.firstlabel.toInt()
+ i = firstlabel
while (i < ll_n) {
if (label.eq_b(ll[i]!!.name)) {
val msg: String? = ls!!.L!!.pushfstring(
- "label '" + label + " already defined on line " + ll[i]!!.line
+ "label '" + label + "' already defined on line " + ll[i]!!.line
)
ls!!.semerror(msg)
}
@@ -96,13 +112,16 @@ internal class FuncState internal constructor() : Constants() {
if (v > l) errorlimit(l, msg)
}
+ /**
+ * Refuses a function that needs more of something than Lua allows.
+ *
+ * The message names the function it is about, since a limit is reached by
+ * the shape of a whole function rather than at any one place in it.
+ */
fun errorlimit(limit: Int, what: String?) {
- // TODO: report message logic.
- val msg: String? =
- if (f!!.linedefined === 0) ls!!.L!!.pushfstring("main function has more than " + limit + " " + what) else ls!!.L!!.pushfstring(
- "function at line " + f!!.linedefined + " has more than " + limit + " " + what
- )
- ls!!.lexerror(msg, 0)
+ val line: Int = f!!.linedefined
+ val where: String = if (line == 0) "main function" else "function at line " + line
+ ls!!.syntaxerror("too many " + what + " (limit is " + limit + ") in " + where)
}
fun getlocvar(i: Int): LocVars {
@@ -128,14 +147,22 @@ internal class FuncState internal constructor() : Constants() {
return -1 /* not found */
}
- fun newupvalue(name: LuaString?, v: expdesc): Int {
+ fun newupvalue(name: LuaString?, v: expdesc, kind: Int = 0): Int {
checklimit(nups + 1, LUAI_MAXUPVAL, "upvalues")
if (f!!.upvalues == null || nups + 1 > f!!.upvalues!!.size) f!!.upvalues =
realloc(f!!.upvalues, if (nups > 0) nups * 2 else 1)
- f!!.upvalues!![nups.toInt()] = Upvaldesc(name, v.k === LexState.VLOCAL, v.u.info)
+ f!!.upvalues!![nups.toInt()] = Upvaldesc(name, v.k === LexState.VLOCAL, v.u.info, kind)
return (nups++).toInt()
}
+ /** How the local at [index] of this function was declared. */
+ fun localkind(index: Int): Int {
+ val vars: Array = ls?.dyd?.actvar ?: return 0
+ val at: Int = firstlocal + index
+ if (at < 0 || at >= vars.size) return 0
+ return vars[at]?.kind ?: 0
+ }
+
fun searchvar(n: LuaString): Int {
var i: Int
i = nactvar - 1
@@ -176,23 +203,85 @@ internal class FuncState internal constructor() : Constants() {
fun enterblock(bl: BlockCnt, isloop: Boolean) {
bl.isloop = isloop
bl.nactvar = nactvar
+ bl.firstglobal = globals.size
bl.firstlabel = ls!!.dyd.n_label.toShort()
bl.firstgoto = ls!!.dyd.n_gt.toShort()
bl.upval = false
+ bl.insidetbc = this.bl?.insidetbc ?: false
bl.previous = this.bl
this.bl = bl
_assert(this.freereg == this.nactvar)
}
+ /**
+ * One `global` declaration in scope.
+ *
+ * A [name] of `null` is the collective form, `global *`, which declares
+ * every global at once. [readonly] comes from a `` attribute and
+ * makes assignment to the global a compile error.
+ */
+ /**
+ * @param nactvar how many locals were in scope when this was declared, so
+ * a declaration can be told apart from a local of the same name that came
+ * before it - `local X` then `global X` means the global from there on
+ */
+ internal class Globaldesc(val name: LuaString?, val readonly: Boolean, val nactvar: Int = 0)
+
+ /**
+ * How far a name search has got, across the chain of enclosing functions.
+ *
+ * A `global` declaration is not confined to the function it appears
+ * in - an inner function sees the declarations around it - so the
+ * three things a search learns on the way have to survive the step
+ * from one [FuncState] to the next.
+ */
+ internal class Globalsearch {
+ /** The innermost `global *` seen, which declares every name. */
+ var collective: Globaldesc? = null
+
+ /** Whether a `global` named something other than what is sought. */
+ var named: Boolean = false
+
+ /** The declaration that names the variable, once one turns up. */
+ var found: Globaldesc? = null
+ }
+
+
+ /**
+ * The `global` declarations in scope, outermost first.
+ *
+ * Kept apart from the local variables rather than interleaved with them as
+ * upstream does: a declaration takes no register, and the rest of this
+ * compiler reads `nactvar` as the register level.
+ */
+ internal val globals: ArrayList = ArrayList()
+
+ /**
+ * Marks the current block as one that has to be left through a closing jump.
+ *
+ * That is the same jump [leaveblock] already emits when a block holds an
+ * upvalue, and reusing it means every way out of the block - falling off
+ * the end, `break`, or a `goto` - passes through the instruction that runs
+ * the pending `__close` handlers.
+ */
+ fun markblocktobeclosed() {
+ this.bl!!.upval = true
+ this.bl!!.insidetbc = true
+ }
+
fun leaveblock() {
val bl: BlockCnt = this.bl!!
+ // The break label goes in first, so a 'break' lands on the closing
+ // jump rather than past it: leaving a loop early still closes what the
+ // block was holding.
+ if (bl.isloop) ls!!.breaklabel() /* close pending breaks */
if (bl.previous != null && bl.upval) {
/* create a 'jump to here' to close upvalues */
val j = this.jump()
this.patchclose(j, bl.nactvar.toInt())
this.patchtohere(j)
}
- if (bl.isloop) ls!!.breaklabel() /* close pending breaks */
+ while (globals.size > bl.firstglobal) globals.removeAt(globals.size - 1)
this.bl = bl.previous
this.removevars(bl.nactvar.toInt())
_assert(bl.nactvar == this.nactvar)
@@ -268,6 +357,7 @@ internal class FuncState internal constructor() : Constants() {
}
fun ret(first: Int, nret: Int) {
+ checklimit(nret + 1, MAX_RETURNS, "returns")
this.codeABC(OP_RETURN, first, nret + 1, 0)
}
@@ -405,11 +495,13 @@ internal class FuncState internal constructor() : Constants() {
fun checkstack(n: Int) {
val newstack = this.freereg + n
if (newstack > this.f!!.maxstacksize) {
- if (newstack >= MAXSTACK) ls!!.syntaxerror("function or expression too complex")
+ checklimit(newstack, MAX_FSTACK, "registers")
this.f!!.maxstacksize = newstack
}
}
+
+
fun reserveregs(n: Int) {
this.checkstack(n)
this.freereg = (this.freereg + n).toShort()
@@ -443,12 +535,10 @@ internal class FuncState internal constructor() : Constants() {
}
fun numberK(r: LuaValue): Int {
- var r: LuaValue = r
- if (r is LuaDouble) {
- val d: Double = r.todouble()
- val i = d.toInt()
- if (d == i.toDouble()) r = LuaInteger.valueOf(i)!!
- }
+ // A float constant stays a float. Folding 2.0 onto the integer 2 here
+ // was safe while Lua had one number type; since 5.3 it would make the
+ // constant's subtype depend on its value, so `2.0` would report as an
+ // integer and print without its fractional part.
return this.addk(r)
}
@@ -683,9 +773,19 @@ internal class FuncState internal constructor() : Constants() {
val func: Int
this.exp2anyreg(e)
this.freeexp(e)
+ val receiver: Int = e.u.info
func = this.freereg.toInt()
this.reserveregs(2)
- this.codeABC(OP_SELF, func, e.u.info, this.exp2RK(key))
+ val rk: Int = this.exp2RK(key)
+ if (ISK(rk)) {
+ this.codeABC(OP_SELF, func, receiver, rk)
+ } else {
+ // The method name did not fit in the instruction's constant
+ // operand, so the call is built the long way: the receiver is
+ // copied into place and the method looked up as an ordinary field.
+ this.codeABC(OP_MOVE, func + 1, receiver, 0)
+ this.codeABC(OP_GETTABLE, func, receiver, rk)
+ }
this.freeexp(key)
e.u.info = func
e.k = LexState.VNONRELOC
@@ -799,6 +899,9 @@ internal class FuncState internal constructor() : Constants() {
fun indexed(t: expdesc, k: expdesc) {
t.u.ind_t = t.u.info.toShort()
+ // Indexing a read-only global yields an ordinary table access: it is
+ // `t.field` that is being assigned, not the variable `t`.
+ t.readonlyGlobal = null
t.u.ind_idx = this.exp2RK(k).toShort()
_assert(t.k === LexState.VUPVAL || net.blueva.luak.compiler.FuncState.Companion.vkisinreg(t.k))
t.u.ind_vt = (if (t.k === LexState.VUPVAL) LexState.VUPVAL else LexState.VLOCAL).toShort()
@@ -810,16 +913,33 @@ internal class FuncState internal constructor() : Constants() {
val v2: LuaValue
var r: LuaValue? = null
if (!e1.isnumeral() || !e2.isnumeral()) return false
- if ((op == OP_DIV || op == OP_MOD) && e2.u.nval()
+ if ((op == OP_DIV || op == OP_MOD || op == OP_IDIV) && e2.u.nval()
!!.eq_b(LuaValue.ZERO)
) return false /* do not attempt to divide by 0 */
v1 = e1.u.nval()!!
v2 = e2.u.nval()!!
+ // A bitwise operand that denotes no integer is a run-time error, not a
+ // compile-time one: leave it for the VM so pcall can catch it.
+ when (op) {
+ OP_BAND, OP_BOR, OP_BXOR, OP_SHL, OP_SHR ->
+ if (!net.blueva.luak.luaHasIntegerRepresentation(v1) ||
+ !net.blueva.luak.luaHasIntegerRepresentation(v2)
+ ) return false
+
+ OP_BNOT -> if (!net.blueva.luak.luaHasIntegerRepresentation(v1)) return false
+ }
when (op) {
OP_ADD -> r = v1.add(v2)
OP_SUB -> r = v1.sub(v2)
OP_MUL -> r = v1.mul(v2)
OP_DIV -> r = v1.div(v2)
+ OP_IDIV -> r = v1.idiv(v2)
+ OP_BAND -> r = v1.band(v2)
+ OP_BOR -> r = v1.bor(v2)
+ OP_BXOR -> r = v1.bxor(v2)
+ OP_SHL -> r = v1.shl(v2)
+ OP_SHR -> r = v1.shr(v2)
+ OP_BNOT -> r = v1.bnot()
OP_MOD -> r = v1.mod(v2)
OP_POW -> r = v1.pow(v2)
OP_UNM -> r = v1.neg()
@@ -831,7 +951,15 @@ internal class FuncState internal constructor() : Constants() {
r = null
}
}
- if ((r!!.todouble()).isNaN()) return false /* do not attempt to produce NaN */
+ if (!r!!.isinttype()) {
+ // Neither NaN nor a zero float is folded. NaN has no literal to
+ // fold into, and the constant pool compares floats with `==`, under
+ // which -0.0 and 0.0 are the same key: folding `-0.0` would let it
+ // share a slot with a plain `0.0` elsewhere in the chunk and flip
+ // the sign of whichever one was written second.
+ val d: Double = r.todouble()
+ if (d.isNaN() || d == 0.0) return false
+ }
e1.u.setNval(r)
return true
}
@@ -839,7 +967,9 @@ internal class FuncState internal constructor() : Constants() {
fun codearith(op: Int, e1: expdesc, e2: expdesc, line: Int) {
if (constfolding(op, e1, e2)) return
else {
- val o2 = if (op != OP_UNM && op != OP_LEN)
+ // The unary opcodes take no C operand; emitting one trips the
+ // operand-mode assertion in codeABC.
+ val o2 = if (op != OP_UNM && op != OP_LEN && op != OP_BNOT)
this.exp2RK(e2)
else
0
@@ -875,15 +1005,18 @@ internal class FuncState internal constructor() : Constants() {
}
fun prefix( /* UnOpr */op: Int, e: expdesc, line: Int) {
+ // A stand-in second operand, as upstream keeps, so the unary operators
+ // fold through constfolding and inherit its guards instead of carrying
+ // their own weaker copies.
val e2: expdesc = expdesc()
e2.init(LexState.VKNUM, 0)
+ e2.u.setNval(LuaValue.ZERO)
when (op) {
- LexState.OPR_MINUS -> {
- if (e.isnumeral()) /* minus constant? */
- e.u.setNval(e.u.nval()!!.neg()) /* fold it */
- else {
+ LexState.OPR_MINUS, LexState.OPR_BNOT -> {
+ val opcode = if (op == LexState.OPR_MINUS) OP_UNM else OP_BNOT
+ if (!this.constfolding(opcode, e, e2)) {
this.exp2anyreg(e)
- this.codearith(OP_UNM, e, e2, line)
+ this.codearith(opcode, e, e2, line)
}
}
@@ -911,7 +1044,9 @@ internal class FuncState internal constructor() : Constants() {
this.exp2nextreg(v) /* operand must be on the `stack' */
}
- LexState.OPR_ADD, LexState.OPR_SUB, LexState.OPR_MUL, LexState.OPR_DIV, LexState.OPR_MOD, LexState.OPR_POW -> {
+ LexState.OPR_ADD, LexState.OPR_SUB, LexState.OPR_MUL, LexState.OPR_DIV, LexState.OPR_MOD, LexState.OPR_POW,
+ LexState.OPR_IDIV, LexState.OPR_BAND, LexState.OPR_BOR, LexState.OPR_BXOR,
+ LexState.OPR_SHL, LexState.OPR_SHR -> {
if (!v.isnumeral()) this.exp2RK(v)
}
@@ -960,6 +1095,12 @@ internal class FuncState internal constructor() : Constants() {
LexState.OPR_SUB -> this.codearith(OP_SUB, e1, e2, line)
LexState.OPR_MUL -> this.codearith(OP_MUL, e1, e2, line)
LexState.OPR_DIV -> this.codearith(OP_DIV, e1, e2, line)
+ LexState.OPR_IDIV -> this.codearith(OP_IDIV, e1, e2, line)
+ LexState.OPR_BAND -> this.codearith(OP_BAND, e1, e2, line)
+ LexState.OPR_BOR -> this.codearith(OP_BOR, e1, e2, line)
+ LexState.OPR_BXOR -> this.codearith(OP_BXOR, e1, e2, line)
+ LexState.OPR_SHL -> this.codearith(OP_SHL, e1, e2, line)
+ LexState.OPR_SHR -> this.codearith(OP_SHR, e1, e2, line)
LexState.OPR_MOD -> this.codearith(OP_MOD, e1, e2, line)
LexState.OPR_POW -> this.codearith(OP_POW, e1, e2, line)
LexState.OPR_EQ -> this.codecomp(OP_EQ, 1, e1, e2)
@@ -1036,27 +1177,75 @@ internal class FuncState internal constructor() : Constants() {
}
companion object {
- fun singlevaraux(fs: FuncState?, n: LuaString, `var`: expdesc, base: Int): Int {
+ /** As many registers as one function may use, as Lua allows. */
+ const val MAX_FSTACK: Int = 255
+
+ /** As many values as one `return` may hand back, as Lua allows. */
+ const val MAX_RETURNS: Int = 255
+
+ /**
+ * Looks for [n] among one function's variables, innermost first.
+ *
+ * Locals and `global` declarations are walked together in the order
+ * they were written, since a `global x` after a `local x` refers to
+ * the global from there on and the other way round.
+ *
+ * @return [LexState.VLOCAL] when a local matched, -1 when nothing did;
+ * a global declaration reports itself through [search] instead
+ */
+ private fun searchvaraux(fs: FuncState, n: LuaString, `var`: expdesc, search: Globalsearch): Int {
+ var globalIndex = fs.globals.size
+ var localIndex = fs.nactvar - 1
+ while (globalIndex > 0 || localIndex >= 0) {
+ // A declaration made when this many locals were in scope is
+ // the more recent of the two whenever the counts are level.
+ val takeGlobal: Boolean = globalIndex > 0 &&
+ (localIndex < 0 || fs.globals[globalIndex - 1].nactvar >= localIndex + 1)
+ if (takeGlobal) {
+ val declaration: Globaldesc = fs.globals[--globalIndex]
+ val name: LuaString? = declaration.name
+ if (name == null) {
+ if (search.collective == null) search.collective = declaration
+ } else if (name == n) {
+ search.found = declaration
+ return -1
+ } else {
+ search.named = true
+ }
+ } else {
+ if (n.eq_b(fs.getlocvar(localIndex).varname)) {
+ `var`.init(LexState.VLOCAL, localIndex)
+ return LexState.VLOCAL
+ }
+ localIndex--
+ }
+ }
+ return -1 /* not found */
+ }
+
+ fun singlevaraux(fs: FuncState?, n: LuaString, `var`: expdesc, base: Int, search: Globalsearch): Int {
if (fs == null) /* no more levels? */
return LexState.VVOID /* default is global */
- val v = fs.searchvar(n) /* look up at current level */
+ val v = searchvaraux(fs, n, `var`, search) /* look up at current level */
+ if (search.found != null) /* a global declaration names it? */
+ return LexState.VVOID
if (v >= 0) {
- `var`.init(LexState.VLOCAL, v)
- if (base == 0) fs.markupval(v) /* local will be used as an upval */
+ if (base == 0) fs.markupval(`var`.u.info) /* local will be used as an upval */
return LexState.VLOCAL
} else { /* not found at current level; try upvalues */
var idx = fs.searchupvalue(n) /* try existing upvalues */
if (idx < 0) { /* not found? */
- if (net.blueva.luak.compiler.FuncState.Companion.singlevaraux(
- fs.prev,
- n,
- `var`,
- 0
- ) == LexState.VVOID
- ) /* try upper levels */
+ if (singlevaraux(fs.prev, n, `var`, 0, search) == LexState.VVOID)
return LexState.VVOID /* not found; is a global */
/* else was LOCAL or UPVAL */
- idx = fs.newupvalue(n, `var`) /* will be a new upvalue */
+ // The declaration kind travels with the upvalue, so an
+ // inner function still knows the variable is read-only.
+ val kind: Int = if (`var`.k == LexState.VLOCAL) {
+ fs.prev?.localkind(`var`.u.info) ?: 0
+ } else {
+ fs.prev?.f?.upvalues?.getOrNull(`var`.u.info)?.kind ?: 0
+ }
+ idx = fs.newupvalue(n, `var`, kind) /* will be a new upvalue */
}
`var`.init(LexState.VUPVAL, idx)
return LexState.VUPVAL
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/compiler/LexState.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/compiler/LexState.kt
index fe061ea9..9cffbe42 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/compiler/LexState.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/compiler/LexState.kt
@@ -59,6 +59,9 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
internal var dyd: Dyndata = net.blueva.luak.compiler.LexState.Dyndata() /* dynamic structures used by the parser */
var source: LuaString? = null /* current source name */
var envn: LuaString? = null /* environment variable name */
+
+ /** The name `global`, recognised as a statement without being reserved. */
+ private val glbn: LuaString = LuaString.valueOf("global")
var decpoint: Byte = 0 /* locale decimal point */
private fun isalnum(c: Int): Boolean {
@@ -113,20 +116,34 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
}
- fun token2str(token: Int): String? {
+ /**
+ * How an error message names [token].
+ *
+ * Symbols and reserved words appear in quotes, since they are literal text
+ * a program could have written. The four that stand for a whole class of
+ * token - ``, ``, ``, `` - do not, because the
+ * angle brackets already mark them as descriptions rather than text.
+ */
+ fun token2str(token: Int): String {
if (token < net.blueva.luak.compiler.LexState.Companion.FIRST_RESERVED) {
- return if (net.blueva.luak.compiler.LexState.Companion.iscntrl(token)) L!!.pushfstring("char(" + token + ")") else L!!.pushfstring(
- (token.toChar()).toString()
- )
- } else {
- return net.blueva.luak.compiler.LexState.Companion.luaX_tokens!![token - net.blueva.luak.compiler.LexState.Companion.FIRST_RESERVED]
+ return if (net.blueva.luak.compiler.LexState.Companion.iscntrl(token)) "'<\\" + token + ">'" else "'" + token.toChar() + "'"
}
+ val text: String = net.blueva.luak.compiler.LexState.Companion.luaX_tokens!![token - net.blueva.luak.compiler.LexState.Companion.FIRST_RESERVED]!!
+ val describes: Boolean = token >= net.blueva.luak.compiler.LexState.Companion.TK_EOS && token <= net.blueva.luak.compiler.LexState.Companion.TK_STRING
+ return if (describes) text else "'" + text + "'"
}
- fun txtToken(token: Int): String? {
+ /**
+ * The text to put after "near" in an error message.
+ *
+ * A name, string or numeral is quoted as it was actually written, taken
+ * from the buffer the lexer has been filling; everything else is named the
+ * way [token2str] names it.
+ */
+ fun txtToken(token: Int): String {
when (token) {
net.blueva.luak.compiler.LexState.Companion.TK_NAME, net.blueva.luak.compiler.LexState.Companion.TK_STRING, net.blueva.luak.compiler.LexState.Companion.TK_NUMBER ->
- return buff.concatToString(0, nbuff)
+ return "'" + buff.concatToString(0, nbuff) + "'"
else -> return token2str(token)
}
@@ -134,9 +151,11 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
fun lexerror(msg: String?, token: Int) {
val cid: String? = Lua.chunkid(source!!.tojstring())
- L!!.pushfstring(cid.toString() + ":" + linenumber + ": " + msg)
- if (token != 0) L!!.pushfstring("syntax error: " + msg + " near " + txtToken(token))
- throw LuaError(cid.toString() + ":" + linenumber + ": " + msg)
+ var full: String = cid.toString() + ":" + linenumber + ": " + msg
+ // "near " says where the compiler was when it gave up, which is
+ // what the reader needs to find the problem.
+ if (token != 0) full = full + " near " + txtToken(token)
+ throw LuaError(full)
}
fun syntaxerror(msg: String?) {
@@ -175,8 +194,20 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
this.skipShebang()
}
+ /**
+ * Skips a `#!` line, but only in a chunk that came from a file.
+ *
+ * Lua strips it while reading the file, before the lexer ever sees it, so
+ * `load("#=1")` is an ordinary chunk that starts with the length operator
+ * and fails to parse. A source name beginning with `@` is what marks a
+ * chunk as having been read from a file.
+ */
private fun skipShebang() {
- if (current == '#'.code) while (!currIsNewline() && current != net.blueva.luak.compiler.LexState.Companion.EOZ) nextChar()
+ val name: String = source?.tojstring() ?: return
+ if (!name.startsWith("@")) return
+ if (current == '#'.code) {
+ while (!currIsNewline() && current != net.blueva.luak.compiler.LexState.Companion.EOZ) nextChar()
+ }
}
@@ -197,64 +228,19 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
while ((--n) >= 0) if (p[n] == from) p[n] = to
}
- internal fun strx2number(str: String, seminfo: SemInfo?): LuaValue {
- val c: CharArray = str.toCharArray()
- var s = 0
- while (s < c.size && isspace(c[s].code)) ++s
- // Check for negative sign
- var sgn = 1.0
- if (s < c.size && c[s] == '-') {
- sgn = -1.0
- ++s
- }
- /* Check for "0x" */
- if (s + 2 >= c.size) return (LuaValue.ZERO)!!
- if (c[s++] != '0') return (LuaValue.ZERO)!!
- if (c[s] != 'x' && c[s] != 'X') return (LuaValue.ZERO)!!
- ++s
-
- // read integer part.
- var m = 0.0
- var e = 0
- while (s < c.size && isxdigit(c[s].code)) m = (m * 16) + hexvalue(c[s++].code)
- if (s < c.size && c[s] == '.') {
- ++s // skip dot
- while (s < c.size && isxdigit(c[s].code)) {
- m = (m * 16) + hexvalue(c[s++].code)
- e -= 4 // Each fractional part shifts right by 2^4
- }
- }
- if (s < c.size && (c[s] == 'p' || c[s] == 'P')) {
- ++s
- var exp1 = 0
- var neg1 = false
- if (s < c.size && c[s] == '-') {
- neg1 = true
- ++s
- }
- while (s < c.size && isdigit(c[s].code)) exp1 = exp1 * 10 + c[s++].code - '0'.code
- if (neg1) exp1 = -exp1
- e += exp1
- }
- return LuaValue.valueOf(sgn * m * MathLib.dpow_d(2.0, (e).toDouble()))
- }
-
internal fun str2d(str: String, seminfo: SemInfo): Boolean {
- if (str.indexOf('n') >= 0 || str.indexOf('N') >= 0) seminfo.r = LuaValue.ZERO
- else if (str.indexOf('x') >= 0 || str.indexOf('X') >= 0) seminfo.r = strx2number(str, seminfo)
- else {
- try {
- seminfo.r = LuaValue.valueOf((str.trim()).toDouble())
- } catch (e: NumberFormatException) {
- lexerror(
- "malformed number (" + e.message + ")",
- net.blueva.luak.compiler.LexState.Companion.TK_NUMBER
- )
+ // The same reader the `tonumber` coercion uses, so a literal and its
+ // text form cannot disagree about whether they are integers.
+ val numeral: LuaValue = net.blueva.luak.NumberParser.parse(str.trim())
+ ?: run {
+ lexerror("malformed number", net.blueva.luak.compiler.LexState.Companion.TK_NUMBER)
+ return false
}
- }
+ seminfo.r = numeral
return true
}
+
internal fun read_numeral(seminfo: SemInfo) {
var expo = "Ee"
val first = current
@@ -266,6 +252,9 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
if (isxdigit(current) || current == '.'.code) save_and_next()
else break
}
+ // A letter touching the numeral is part of the mistake, so it is taken
+ // into the token and the message can name what was actually written.
+ if (isalpha(current)) save_and_next()
val str = buff.concatToString(0, nbuff)
str2d(str, seminfo)
}
@@ -337,16 +326,63 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
return if (c <= '9'.code) c - '0'.code else if (c <= 'F'.code) c + 10 - 'A'.code else c + 10 - 'a'.code
}
+ /** Drops the last [n] characters the lexer saved. */
+ private fun buffremove(n: Int) {
+ nbuff -= n
+ }
+
+ /**
+ * Fails with [msg] unless [ok], keeping the offending character.
+ *
+ * The character is added to the buffer first so that the "near" part of
+ * the message shows what was actually written.
+ */
+ private fun esccheck(ok: Boolean, msg: String) {
+ if (!ok) {
+ if (current != net.blueva.luak.compiler.LexState.Companion.EOZ) save_and_next()
+ lexerror(msg, net.blueva.luak.compiler.LexState.Companion.TK_STRING)
+ }
+ }
+
+ /** One hexadecimal digit of an escape, left in the buffer for errors. */
+ private fun gethexa(): Int {
+ save_and_next()
+ esccheck(isxdigit(current), "hexadecimal digit expected")
+ return hexvalue(current)
+ }
+
fun readhexaesc(): Int {
- nextChar()
- val c1 = current
- nextChar()
- val c2 = current
- if (!isxdigit(c1) || !isxdigit(c2)) lexerror(
- "hexadecimal digit expected 'x" + (c1.toChar()) + (c2.toChar()),
- net.blueva.luak.compiler.LexState.Companion.TK_STRING
- )
- return (hexvalue(c1) shl 4) + hexvalue(c2)
+ var r: Int = gethexa()
+ r = (r shl 4) + gethexa()
+ buffremove(2) // the two digits were only kept in case of an error
+ return r
+ }
+
+ /**
+ * Reads a `\u{XXX}` escape and saves its UTF-8 encoding.
+ *
+ * Added in Lua 5.3. The braces hold at least one hexadecimal digit, and the
+ * value may reach `0x7FFFFFFF`, which needs the six-byte form the encoder
+ * in [net.blueva.luak.lib.Utf8Lib] also produces.
+ */
+ internal fun readutf8esc() {
+ var removed = 4 /* '\\', 'u', '{', and the first digit */
+ save_and_next() /* keep 'u' */
+ esccheck(current == '{'.code, "missing '{' in \\u{xxxx}")
+ var value: Long = gethexa().toLong() /* at least one digit is required */
+ while (true) {
+ save_and_next()
+ if (!isxdigit(current)) break
+ removed++
+ esccheck(value <= (0x7FFFFFFFL shr 4), "UTF-8 value too large")
+ value = (value shl 4) + hexvalue(current).toLong()
+ }
+ esccheck(current == '}'.code, "missing '}'")
+ nextChar() /* skip '}' */
+ buffremove(removed)
+ val encoded = ArrayList()
+ net.blueva.luak.lib.Utf8Lib.encode(value, encoded, 1)
+ for (b in encoded) save(b.toInt() and 0xFF)
}
internal fun read_string(del: Int, seminfo: SemInfo) {
@@ -365,7 +401,7 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
'\\'.code -> {
var c: Int
- nextChar() /* do not save the `\' */
+ save_and_next() /* keep the backslash for error messages */
when (current) {
'a'.code -> c = '\u0007'.code
'b'.code -> c = '\b'.code
@@ -374,16 +410,30 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
'r'.code -> c = '\r'.code
't'.code -> c = '\t'.code
'v'.code -> c = '\u000B'.code
- 'x'.code -> c = readhexaesc()
+ 'x'.code -> {
+ c = readhexaesc()
+ nextChar()
+ buffremove(1) /* the backslash */
+ save(c)
+ continue
+ }
+
+ 'u'.code -> {
+ readutf8esc()
+ continue
+ }
+
'\n'.code, '\r'.code -> {
- save('\n'.code)
inclinenumber()
+ buffremove(1)
+ save('\n'.code)
continue
}
net.blueva.luak.compiler.LexState.Companion.EOZ -> continue /* will raise an error next loop */
'z'.code -> {
/* zap following span of spaces */
+ buffremove(1) /* the backslash */
nextChar() /* skip the 'z' */
while (isspace(current)) {
if (currIsNewline()) inclinenumber()
@@ -393,25 +443,32 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
}
else -> {
- if (!isdigit(current)) save_and_next() /* handles \\, \", \', and \? */
- else { /* \xxx */
+ if (!isdigit(current)) {
+ esccheck(
+ current == '\\'.code || current == '"'.code ||
+ current == '\''.code,
+ "invalid escape sequence",
+ )
+ buffremove(1) /* the backslash */
+ save_and_next() /* handles \\, \" and \' */
+ } else { /* \ddd */
var i = 0
c = 0
- do {
+ while (i < 3 && isdigit(current)) {
c = 10 * c + (current - '0'.code)
- nextChar()
- } while (++i < 3 && isdigit(current))
- if (c > net.blueva.luak.compiler.LexState.Companion.UCHAR_MAX) lexerror(
- "escape sequence too large",
- net.blueva.luak.compiler.LexState.Companion.TK_STRING
- )
+ save_and_next()
+ i++
+ }
+ esccheck(c <= net.blueva.luak.compiler.LexState.Companion.UCHAR_MAX, "decimal escape too large")
+ buffremove(i + 1) /* the digits and the backslash */
save(c)
}
continue
}
}
- save(c)
nextChar()
+ buffremove(1) /* the backslash */
+ save(c)
continue
}
@@ -488,19 +545,32 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
'<'.code -> {
nextChar()
- if (current != '='.code) return '<'.code
- else {
+ if (current == '='.code) {
nextChar()
return net.blueva.luak.compiler.LexState.Companion.TK_LE
- }
+ } else if (current == '<'.code) {
+ nextChar()
+ return net.blueva.luak.compiler.LexState.Companion.TK_SHL
+ } else return '<'.code
}
'>'.code -> {
nextChar()
- if (current != '='.code) return '>'.code
- else {
+ if (current == '='.code) {
nextChar()
return net.blueva.luak.compiler.LexState.Companion.TK_GE
+ } else if (current == '>'.code) {
+ nextChar()
+ return net.blueva.luak.compiler.LexState.Companion.TK_SHR
+ } else return '>'.code
+ }
+
+ '/'.code -> {
+ nextChar()
+ if (current != '/'.code) return '/'.code
+ else {
+ nextChar()
+ return net.blueva.luak.compiler.LexState.Companion.TK_IDIV
}
}
@@ -609,11 +679,22 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
val u: U = net.blueva.luak.compiler.LexState.expdesc.U()
val t: IntPtr = IntPtr() /* patch list of `exit when true' */
val f: IntPtr = IntPtr() /* patch list of `exit when false' */
+
+ /**
+ * The name of the `global ` this expression reads, if any.
+ *
+ * A read-only global is an ordinary `_ENV[name]` index once compiled,
+ * so the only place the restriction survives is here, on the
+ * expression the parser hands to [check_readonly].
+ */
+ var readonlyGlobal: LuaString? = null
+
fun init(k: Int, i: Int) {
this.f.i = net.blueva.luak.compiler.LexState.Companion.NO_JUMP
this.t.i = net.blueva.luak.compiler.LexState.Companion.NO_JUMP
this.k = k
this.u.info = i
+ this.readonlyGlobal = null
}
fun hasjumps(): Boolean {
@@ -625,6 +706,7 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
}
fun setvalue(other: expdesc) {
+ this.readonlyGlobal = other.readonlyGlobal
this.f.i = other.f.i
this.k = other.k
this.t.i = other.t.i
@@ -641,6 +723,9 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
internal class Vardesc(idx: Int) {
val idx: Short /* variable index in stack */
+ /** How the variable was declared: plain, ``, or ``. */
+ var kind: Int = net.blueva.luak.compiler.LexState.Companion.VDKREG
+
init {
this.idx = idx.toShort()
}
@@ -648,17 +733,27 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
/* description of pending goto statements and label statements */
- internal class Labeldesc(name: LuaString?, pc: Int, line: Int, nactvar: Short) {
+ internal class Labeldesc(
+ name: LuaString?,
+ pc: Int,
+ line: Int,
+ nactvar: Short,
+ nglobals: Int = 0,
+ ) {
var name: LuaString? /* label identifier */
var pc: Int /* position in code */
var line: Int /* line where it appeared */
var nactvar: Short /* local level where it appears in current block */
+ /** How many `global` declarations were in scope where this appeared. */
+ var nglobals: Int
+
init {
this.name = name
this.pc = pc
this.line = line
this.nactvar = nactvar
+ this.nglobals = nglobals
}
}
@@ -694,15 +789,16 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
/* semantic error */
fun semerror(msg: String?) {
t.token = 0 /* remove 'near to' from final message */
+ // Something already read is what is wrong, not whatever the lexer has
+ // gone on to look at: a complaint about the end of a statement belongs
+ // on the line the statement is on.
+ linenumber = lastline
syntaxerror(msg)
}
fun error_expected(token: Int) {
- syntaxerror(
- L!!.pushfstring(
- net.blueva.luak.compiler.LexState.Companion.LUA_QS(token2str(token)).toString() + " expected"
- )
- )
+ // token2str already quotes whatever needs quoting.
+ syntaxerror(token2str(token) + " expected")
}
fun testnext(c: Int): Boolean {
@@ -730,13 +826,12 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
if (!testnext(what)) {
if (where == linenumber) error_expected(what)
else {
+ // token2str already quotes what it names, so nothing is
+ // added around it here.
syntaxerror(
L!!.pushfstring(
- (net.blueva.luak.compiler.LexState.Companion.LUA_QS(token2str(what))
- .toString() + " expected " + "(to close " + net.blueva.luak.compiler.LexState.Companion.LUA_QS(
- token2str(who)
- )
- + " at line " + where + ")")
+ token2str(what) + " expected (to close " + token2str(who) +
+ " at line " + where + ")",
)
)
}
@@ -770,7 +865,10 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
fun new_localvar(name: LuaString?) {
val reg = registerlocalvar(name)
- fs!!.checklimit(dyd.n_actvar + 1, LUAI_MAXVARS, "local variables")
+ // Counted within this function alone: the array holds the variables
+ // of every function being compiled, and a function nested in another
+ // starts where the one around it left off.
+ fs!!.checklimit(dyd.n_actvar + 1 - fs!!.firstlocal, LUAI_MAXVARS, "local variables")
if (dyd.actvar == null || dyd.n_actvar + 1 > dyd.actvar!!.size) dyd.actvar =
realloc(dyd.actvar, maxOf(1, dyd.n_actvar * 2))
dyd.actvar!![dyd.n_actvar++] = net.blueva.luak.compiler.LexState.Vardesc(reg)
@@ -781,6 +879,12 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
new_localvar(ts)
}
+ /** Declares a local with a kind other than the plain one. */
+ fun new_varkind(name: LuaString?, kind: Int) {
+ new_localvar(name)
+ dyd.actvar!![dyd.n_actvar - 1]!!.kind = kind
+ }
+
fun adjustlocalvars(nvars: Int) {
var nvars = nvars
val fs: FuncState = this.fs!!
@@ -800,21 +904,50 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
}
internal fun singlevar(`var`: expdesc) {
- val varname: LuaString? = this.str_checkname()
+ this.buildvar(this.str_checkname()!!, `var`)
+ }
+
+ /**
+ * Resolves [varname], which may turn out to be a local, an upvalue or a
+ * global, and leaves the expression for it in [var].
+ *
+ * With no `global` declaration anywhere in scope every name that is not a
+ * local is a global, which is how Lua has always behaved. Once a `global`
+ * statement names anything, the rest of the scope - inner functions
+ * included - has to declare what it uses, unless a collective `global *`
+ * is also in scope, which puts the default back.
+ */
+ internal fun buildvar(varname: LuaString, `var`: expdesc) {
+ val fs: FuncState = this.fs!!
+ val search = FuncState.Globalsearch()
+ val resolved: Int = FuncState.singlevaraux(fs, varname, `var`, 1, search)
+ if (resolved != net.blueva.luak.compiler.LexState.Companion.VVOID) return
+ val declaration: FuncState.Globaldesc? = search.found ?: search.collective
+ if (declaration == null && search.named) {
+ this.semerror("variable '" + varname.tojstring() + "' not declared")
+ }
+ this.buildglobal(varname, `var`)
+ if (declaration != null && declaration.readonly) `var`.readonlyGlobal = varname
+ }
+
+ /**
+ * Builds the expression `_ENV[varname]`, which is what a global name is.
+ */
+ private fun buildglobal(varname: LuaString, `var`: expdesc) {
val fs: FuncState = this.fs!!
- if (FuncState.singlevaraux(
- fs,
- (varname)!!,
- `var`,
- 1
- ) === net.blueva.luak.compiler.LexState.Companion.VVOID
- ) { /* global name? */
- val key: expdesc = net.blueva.luak.compiler.LexState.expdesc()
- FuncState.singlevaraux(fs, (this.envn)!!, `var`, 1) /* get environment variable */
- _assert(`var`.k == net.blueva.luak.compiler.LexState.Companion.VLOCAL || `var`.k == net.blueva.luak.compiler.LexState.Companion.VUPVAL)
- this.codestring(key, varname) /* key is variable name */
- fs.indexed(`var`, key) /* env[varname] */
+ val key: expdesc = net.blueva.luak.compiler.LexState.expdesc()
+ val search = FuncState.Globalsearch()
+ // Every global is read through _ENV, so _ENV itself cannot be one: the
+ // lookup would have nowhere to start.
+ if (FuncState.singlevaraux(fs, (this.envn)!!, `var`, 1, search) ==
+ net.blueva.luak.compiler.LexState.Companion.VVOID
+ ) {
+ this.semerror(
+ "_ENV is global when accessing variable '" + varname.tojstring() + "'",
+ )
}
+ this.codestring(key, varname) /* key is variable name */
+ fs.indexed(`var`, key) /* env[varname] */
}
internal fun adjust_assign(nvars: Int, nexps: Int, e: expdesc) {
@@ -854,11 +987,20 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
val gl = this.dyd.gt
val gt = gl[g]!!
_assert(gt.name!!.eq_b(label.name))
+ // A `global` declaration opens a scope of its own, so jumping past one
+ // is as wrong as jumping past a local.
+ if (gt.nglobals < label.nglobals) {
+ val declared: LuaString? = fs.globals[gt.nglobals].name
+ semerror(
+ " at line " + gt.line +
+ " jumps into the scope of '" + (declared?.tojstring() ?: "*") + "'",
+ )
+ }
if (gt.nactvar < label.nactvar) {
val vname: LuaString = fs.getlocvar((gt.nactvar).toInt()).varname!!
val msg: String? = L!!.pushfstring(
(" at line "
- + gt.line + " jumps into the scope of local '"
+ + gt.line + " jumps into the scope of '"
+ vname.tojstring() + "'")
)
semerror(msg)
@@ -895,7 +1037,13 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
/* Caller must grow() the vector before calling this. */
internal fun newlabelentry(l: Array, index: Int, name: LuaString?, line: Int, pc: Int): Int {
- l[index] = net.blueva.luak.compiler.LexState.Labeldesc(name, pc, line, fs!!.nactvar)
+ l[index] = net.blueva.luak.compiler.LexState.Labeldesc(
+ name,
+ pc,
+ line,
+ fs!!.nactvar,
+ fs!!.globals.size,
+ )
return index
}
@@ -967,6 +1115,7 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
fs.nlocvars = 0
fs.nactvar = 0
fs.firstlocal = dyd.n_actvar
+ fs.firstlabel = dyd.n_label
fs.bl = null
fs.f!!.source = this.source
fs.f!!.maxstacksize = 2 /* registers 0/1 are always valid */
@@ -1114,9 +1263,25 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
}
net.blueva.luak.compiler.LexState.Companion.TK_DOTS -> {
- /* param . `...' */
+ /* param . `...' or `...NAME' */
this.next()
f.is_vararg = 1
+ if (this.t.token == net.blueva.luak.compiler.LexState.Companion.TK_NAME) {
+ // The extra arguments also get a name, holding them
+ // as a table alongside '...'. The name is read-only:
+ // rebinding it would break the link to '...'.
+ this.new_localvar(this.str_checkname())
+ this.dyd!!.actvar!![this.dyd!!.n_actvar - 1]!!.kind =
+ net.blueva.luak.compiler.LexState.Companion.RDKCONST
+ f.is_vararg = 1 or Lua.VARARG_NAMED
+ } else {
+ // The slot exists either way, named or not: a
+ // vararg function always has somewhere to put the
+ // table, and code that walks the locals sees it.
+ this.new_localvarliteral(
+ net.blueva.luak.compiler.LexState.Companion.RESERVED_LOCAL_VAR_FOR_VARARGS,
+ )
+ }
}
else -> this.syntaxerror(" or " + net.blueva.luak.compiler.LexState.Companion.LUA_QL("...") + " expected")
@@ -1124,7 +1289,16 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
} while ((f.is_vararg === 0) && this.testnext(','.code))
}
this.adjustlocalvars(nparams)
+ // The vararg table is a local of its own, and comes into scope after
+ // the count of declared parameters has been taken.
f.numparams = fs.nactvar.toInt()
+ if (f.is_vararg != 0) {
+ this.adjustlocalvars(1)
+ // In scope only once the call has been set up, which is when the
+ // extra arguments exist: asking a function value for its locals
+ // reads them at the very start and must not see this one.
+ fs.getlocvar(fs.nactvar - 1).startpc = 1
+ }
fs.reserveregs((fs.nactvar).toInt()) /* reserve register for parameters */
}
@@ -1167,8 +1341,12 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
}
- internal fun funcargs(f: expdesc, line: Int) {
+ internal fun funcargs(f: expdesc) {
val fs: FuncState = this.fs!!
+ // Where the arguments start, which is the line a call reports itself
+ // on: a call written over several lines is the one at its '(', not the
+ // one where the expression naming the function began.
+ val line: Int = linenumber
val args: expdesc = net.blueva.luak.compiler.LexState.expdesc()
val base: Int
val nparams: Int
@@ -1238,7 +1416,8 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
}
else -> {
- this.syntaxerror("unexpected symbol " + t.token + " (" + (t.token.toChar()) + ")")
+ // The offending token is named by the "near" part already.
+ this.syntaxerror("unexpected symbol")
return
}
}
@@ -1248,7 +1427,6 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
internal fun suffixedexp(v: expdesc) {
/* suffixedexp ->
primaryexp { '.' NAME | '[' exp ']' | ':' NAME funcargs | funcargs } */
- val line = linenumber
primaryexp(v)
while (true) {
when (t.token) {
@@ -1271,13 +1449,13 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
this.next()
this.checkname(key)
fs!!.self(v, key)
- this.funcargs(v, line)
+ this.funcargs(v)
}
'('.code, net.blueva.luak.compiler.LexState.Companion.TK_STRING, '{'.code -> {
/* funcargs */
fs!!.exp2nextreg(v)
- this.funcargs(v, line)
+ this.funcargs(v)
}
else -> return
@@ -1349,6 +1527,7 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
net.blueva.luak.compiler.LexState.Companion.TK_NOT -> return net.blueva.luak.compiler.LexState.Companion.OPR_NOT
'-'.code -> return net.blueva.luak.compiler.LexState.Companion.OPR_MINUS
'#'.code -> return net.blueva.luak.compiler.LexState.Companion.OPR_LEN
+ '~'.code -> return net.blueva.luak.compiler.LexState.Companion.OPR_BNOT
else -> return net.blueva.luak.compiler.LexState.Companion.OPR_NOUNOPR
}
}
@@ -1360,6 +1539,12 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
'-'.code -> return net.blueva.luak.compiler.LexState.Companion.OPR_SUB
'*'.code -> return net.blueva.luak.compiler.LexState.Companion.OPR_MUL
'/'.code -> return net.blueva.luak.compiler.LexState.Companion.OPR_DIV
+ net.blueva.luak.compiler.LexState.Companion.TK_IDIV -> return net.blueva.luak.compiler.LexState.Companion.OPR_IDIV
+ '&'.code -> return net.blueva.luak.compiler.LexState.Companion.OPR_BAND
+ '|'.code -> return net.blueva.luak.compiler.LexState.Companion.OPR_BOR
+ '~'.code -> return net.blueva.luak.compiler.LexState.Companion.OPR_BXOR
+ net.blueva.luak.compiler.LexState.Companion.TK_SHL -> return net.blueva.luak.compiler.LexState.Companion.OPR_SHL
+ net.blueva.luak.compiler.LexState.Companion.TK_SHR -> return net.blueva.luak.compiler.LexState.Companion.OPR_SHR
'%'.code -> return net.blueva.luak.compiler.LexState.Companion.OPR_MOD
'^'.code -> return net.blueva.luak.compiler.LexState.Companion.OPR_POW
net.blueva.luak.compiler.LexState.Companion.TK_CONCAT -> return net.blueva.luak.compiler.LexState.Companion.OPR_CONCAT
@@ -1507,6 +1692,7 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
net.blueva.luak.compiler.LexState.Companion.VLOCAL <= lh.v.k && lh.v.k <= net.blueva.luak.compiler.LexState.Companion.VINDEXED,
"syntax error"
)
+ this.check_readonly(lh.v)
if (this.testnext(','.code)) { /* assignment -> `,' primaryexp assignment */
val nv: LHS_assign = net.blueva.luak.compiler.LexState.LHS_assign()
nv.prev = lh
@@ -1566,8 +1752,12 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
fun labelstat(label: LuaString?, line: Int) {
/* label -> '::' NAME '::' */
val l: Int /* index of new label being created */
- fs!!.checkrepeated(dyd.label, dyd.n_label, (label)!!) /* check for repeated labels */
checknext(net.blueva.luak.compiler.LexState.Companion.TK_DBCOLON) /* skip double colon */
+ // Read before the label is checked or entered: a run of labels one
+ // after another is a single no-op, and the one that ends up entered is
+ // the last of them.
+ skipnoopstat() /* skip other no-op statements */
+ fs!!.checkrepeated(dyd.label, dyd.n_label, (label)!!) /* check for repeated labels */
/* create new entry for this label */
l = newlabelentry(
grow(dyd.label, dyd.n_label + 1).also { dyd.label = it },
@@ -1576,7 +1766,6 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
line,
fs!!.getlabel()
)
- skipnoopstat() /* skip other no-op statements */
if (block_follow(false)) { /* label is last no-op statement in the block? */
/* assume that locals are already out of scope */
dyd.label[l]!!.nactvar = fs!!.bl!!.nactvar
@@ -1649,7 +1838,16 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
val fs: FuncState = this.fs!!
val prep: Int
val endfor: Int
- this.adjustlocalvars(3) /* control variables */
+ // A generic for has a fourth control value, closed when the loop ends,
+ // which is how it can own the resource its iterator walks.
+ this.adjustlocalvars(if (isnum) 3 else 4) /* control variables */
+ // The mark goes in whatever the iterator turns out to return, since
+ // how many values it produces is not known until it runs; a fourth
+ // value of nil or false is skipped when the instruction executes.
+ if (!isnum) {
+ fs.markblocktobeclosed()
+ fs.codeABC(Lua.OP_TBC, base + 3, 0, 0)
+ }
this.checknext(net.blueva.luak.compiler.LexState.Companion.TK_DO)
prep = if (isnum) fs.codeAsBx(
Lua.OP_FORPREP,
@@ -1678,10 +1876,12 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
/* fornum -> NAME = exp1,exp1[,exp1] forbody */
val fs: FuncState = this.fs!!
val base: Int = fs.freereg.toInt()
+ // The loop's own variable is a constant: Lua 5.5 refuses an
+ // assignment to it, since the loop overwrites it every pass anyway.
this.new_localvarliteral(net.blueva.luak.compiler.LexState.Companion.RESERVED_LOCAL_VAR_FOR_INDEX)
this.new_localvarliteral(net.blueva.luak.compiler.LexState.Companion.RESERVED_LOCAL_VAR_FOR_LIMIT)
this.new_localvarliteral(net.blueva.luak.compiler.LexState.Companion.RESERVED_LOCAL_VAR_FOR_STEP)
- this.new_localvar(varname)
+ this.new_varkind(varname, net.blueva.luak.compiler.LexState.Companion.RDKCONST)
this.checknext('='.code)
this.exp1() /* initial value */
this.checknext(','.code)
@@ -1699,24 +1899,28 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
/* forlist -> NAME {,NAME} IN explist1 forbody */
val fs: FuncState = this.fs!!
val e: expdesc = net.blueva.luak.compiler.LexState.expdesc()
- var nvars = 4 /* gen, state, control, plus at least one declared var */
+ var nvars = 5 /* gen, state, control, closing, plus one declared var */
val line: Int
val base: Int = fs.freereg.toInt()
/* create control variables */
this.new_localvarliteral(net.blueva.luak.compiler.LexState.Companion.RESERVED_LOCAL_VAR_FOR_GENERATOR)
this.new_localvarliteral(net.blueva.luak.compiler.LexState.Companion.RESERVED_LOCAL_VAR_FOR_STATE)
this.new_localvarliteral(net.blueva.luak.compiler.LexState.Companion.RESERVED_LOCAL_VAR_FOR_CONTROL)
+ this.new_localvarliteral(net.blueva.luak.compiler.LexState.Companion.RESERVED_LOCAL_VAR_FOR_CLOSING)
/* create declared variables */
- this.new_localvar(indexname)
+ // The first one is the control variable, which the loop overwrites
+ // every pass, so Lua 5.5 refuses an assignment to it.
+ this.new_varkind(indexname, net.blueva.luak.compiler.LexState.Companion.RDKCONST)
while (this.testnext(','.code)) {
this.new_localvar(this.str_checkname())
++nvars
}
this.checknext(net.blueva.luak.compiler.LexState.Companion.TK_IN)
line = this.linenumber
- this.adjust_assign(3, this.explist(e), e)
+ val nexps: Int = this.explist(e)
+ this.adjust_assign(4, nexps, e)
fs.checkstack(3) /* extra space to call generator */
- this.forbody(base, line, nvars - 3, false)
+ this.forbody(base, line, nvars - 4, false)
}
@@ -1745,36 +1949,28 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
}
+ /**
+ * `test_then_block -> [IF | ELSEIF] cond THEN block`
+ *
+ * Lua 5.2 had a special case here for a `goto` or `break` written as the
+ * first statement after `then`, which registered any labels that followed
+ * it before emitting the jump that skips the block - so a label could end
+ * up pointing at that jump. 5.5 dropped it; the ordinary block handles the
+ * same code correctly.
+ */
fun test_then_block(escapelist: IntPtr?) {
- /* test_then_block -> [IF | ELSEIF] cond THEN block */
val v: expdesc = net.blueva.luak.compiler.LexState.expdesc()
- val bl: BlockCnt = BlockCnt()
- val jf: Int /* instruction to skip 'then' code (if condition is false) */
this.next() /* skip IF or ELSEIF */
- expr(v) /* read expression */
+ expr(v) /* read condition */
+ if (v.k == net.blueva.luak.compiler.LexState.Companion.VNIL) v.k = net.blueva.luak.compiler.LexState.Companion.VFALSE /* 'falses' are all equal here */
+ fs!!.goiftrue(v)
+ val condtrue: Int = v.f.i
this.checknext(net.blueva.luak.compiler.LexState.Companion.TK_THEN)
- if (t.token == net.blueva.luak.compiler.LexState.Companion.TK_GOTO || t.token == net.blueva.luak.compiler.LexState.Companion.TK_BREAK) {
- fs!!.goiffalse(v) /* will jump to label if condition is true */
- fs!!.enterblock(bl, false) /* must enter block before 'goto' */
- gotostat(v.t.i) /* handle goto/break */
- skipnoopstat() /* skip other no-op statements */
- if (block_follow(false)) { /* 'goto' is the entire block? */
- fs!!.leaveblock()
- return /* and that is it */
- } else /* must skip over 'then' part if condition is false */
- jf = fs!!.jump()
- } else { /* regular case (not goto/break) */
- fs!!.goiftrue(v) /* skip over block if condition is false */
- fs!!.enterblock(bl, false)
- jf = v.f.i
- }
- statlist() /* `then' part */
- fs!!.leaveblock()
- if (t.token == net.blueva.luak.compiler.LexState.Companion.TK_ELSE || t.token == net.blueva.luak.compiler.LexState.Companion.TK_ELSEIF) fs!!.concat(
- (escapelist)!!,
- fs!!.jump()
- ) /* must jump over it */
- fs!!.patchtohere(jf)
+ this.block() /* 'then' part */
+ if (t.token == net.blueva.luak.compiler.LexState.Companion.TK_ELSE || t.token == net.blueva.luak.compiler.LexState.Companion.TK_ELSEIF) {
+ fs!!.concat((escapelist)!!, fs!!.jump()) /* must jump over it */
+ }
+ fs!!.patchtohere(condtrue)
}
@@ -1804,12 +2000,26 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
fun localstat() {
- /* stat -> LOCAL NAME {`,' NAME} [`=' explist1] */
+ /* stat -> LOCAL attrib NAME attrib {`,' NAME attrib} [`=' explist1] */
+ val fs: FuncState = this.fs!!
var nvars = 0
+ var toclose = -1 /* index, among the new variables, of the one */
val nexps: Int
val e: expdesc = net.blueva.luak.compiler.LexState.expdesc()
+ /* an attribute before the names is the default for all of them */
+ val defaultkind: Int = this.getlocalattribute(net.blueva.luak.compiler.LexState.Companion.VDKREG)
do {
this.new_localvar(this.str_checkname())
+ val kind = this.getlocalattribute(defaultkind)
+ this.dyd!!.actvar!![this.dyd!!.n_actvar - 1]!!.kind = kind
+ if (kind == net.blueva.luak.compiler.LexState.Companion.RDKTOCLOSE) {
+ // One per statement: closing runs in reverse declaration order,
+ // which a single statement has no way to express for two.
+ if (toclose != -1) {
+ this.semerror("multiple to-be-closed variables in local list")
+ }
+ toclose = fs.nactvar + nvars
+ }
++nvars
} while (this.testnext(','.code))
if (this.testnext('='.code)) nexps = this.explist(e)
@@ -1819,8 +2029,200 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
}
this.adjust_assign(nvars, nexps, e)
this.adjustlocalvars(nvars)
+ if (toclose != -1) {
+ // The enclosing block has to be left through a closing jump now,
+ // the same one that closes upvalues, so leaving it by any route
+ // runs the variable's __close.
+ fs.markblocktobeclosed()
+ fs.codeABC(Lua.OP_TBC, toclose, 0, 0)
+ }
+ }
+
+
+ /**
+ * `attrib -> ['<' NAME '>']`, giving the kind of the local just declared.
+ *
+ * `` marks the variable read-only, which is enforced in
+ * [check_readonly]. `` does the same and additionally has the value
+ * registered as to-be-closed, so that leaving the block by any route runs
+ * its `__close` metamethod.
+ */
+ /** True when what follows `global` can only be a declaration. */
+ private fun startsglobalstat(): Boolean {
+ this.lookahead()
+ val next: Int = this.lookahead.token
+ return next == '<'.code || next == '*'.code ||
+ next == net.blueva.luak.compiler.LexState.Companion.TK_NAME ||
+ next == net.blueva.luak.compiler.LexState.Companion.TK_FUNCTION
+ }
+
+ /**
+ * `globalstatfunc -> GLOBAL (globalfunc | globalstat)`, from Lua 5.5.
+ *
+ * A `global` declaration says which globals a chunk means to use. Once one
+ * names anything, every other free name in the scope has to be declared as
+ * well, which turns a misspelt global from a silent nil into a compile
+ * error. `global *` declares them all and puts the old default back.
+ */
+ internal fun globalstatfunc(line: Int) {
+ this.next() /* skip 'global' */
+ if (this.testnext(net.blueva.luak.compiler.LexState.Companion.TK_FUNCTION)) this.globalfunc(line)
+ else this.globalstat()
+ }
+
+ /**
+ * `globalstat -> attrib '*' | attrib NAME attrib {',' NAME attrib} ['=' explist]`
+ */
+ private fun globalstat() {
+ val fs: FuncState = this.fs!!
+ /* an attribute before the names is the default for all of them */
+ val defaultkind: Int = this.getglobalattribute(net.blueva.luak.compiler.LexState.Companion.VDKREG)
+ if (this.testnext('*'.code)) {
+ fs.globals.add(
+ FuncState.Globaldesc(
+ null,
+ defaultkind == net.blueva.luak.compiler.LexState.Companion.RDKCONST,
+ fs.nactvar.toInt(),
+ ),
+ )
+ return
+ }
+ val names: ArrayList = ArrayList()
+ val readonly: ArrayList = ArrayList()
+ do {
+ val varname: LuaString = this.str_checkname()!!
+ val kind: Int = this.getglobalattribute(defaultkind)
+ names.add(varname)
+ readonly.add(kind == net.blueva.luak.compiler.LexState.Companion.RDKCONST)
+ } while (this.testnext(','.code))
+ if (this.testnext('='.code)) this.initglobal(names, 0, this.linenumber)
+ /* the names come into scope only after their own initializers */
+ for (i in names.indices) {
+ fs.globals.add(FuncState.Globaldesc(names[i], readonly[i], fs.nactvar.toInt()))
+ }
+ }
+
+ /**
+ * Assigns an initializer list to freshly declared globals.
+ *
+ * The targets have to be built before the values are read, and the values
+ * are then taken off the stack from the top down, so the recursion walks
+ * out to the last name, reads the expression list there, and assigns on the
+ * way back.
+ */
+ private fun initglobal(names: ArrayList, index: Int, line: Int) {
+ if (index == names.size) {
+ val e: expdesc = net.blueva.luak.compiler.LexState.expdesc()
+ val nexps: Int = this.explist(e)
+ this.adjust_assign(names.size, nexps, e)
+ return
+ }
+ val fs: FuncState = this.fs!!
+ val target: expdesc = net.blueva.luak.compiler.LexState.expdesc()
+ this.buildglobal(names[index], target)
+ this.enterlevel()
+ this.initglobal(names, index + 1, line)
+ this.leavelevel()
+ this.checkglobal(names[index], line)
+ val value: expdesc = net.blueva.luak.compiler.LexState.expdesc()
+ value.init(net.blueva.luak.compiler.LexState.Companion.VNONRELOC, fs.freereg - 1)
+ fs.storevar(target, value)
+ }
+
+ /**
+ * Emits the check that a global being declared with a value is still unset.
+ *
+ * Declaring the same global twice is nearly always a mistake, and it can
+ * only be caught when the chunk runs, since another chunk may have set it.
+ */
+ private fun checkglobal(varname: LuaString, line: Int) {
+ val fs: FuncState = this.fs!!
+ val `var`: expdesc = net.blueva.luak.compiler.LexState.expdesc()
+ this.buildglobal(varname, `var`)
+ val nameindex: Int = fs.stringK(varname)
+ fs.exp2anyreg(`var`)
+ fs.fixline(line)
+ fs.codeABx(
+ Lua.OP_ERRNNIL,
+ `var`.u.info,
+ if (nameindex >= Lua.MAXARG_Bx) 0 else nameindex + 1,
+ )
+ fs.fixline(line)
+ fs.freeexp(`var`)
}
+ /** `globalfunc -> GLOBAL FUNCTION NAME body` */
+ private fun globalfunc(line: Int) {
+ val fs: FuncState = this.fs!!
+ val fname: LuaString = this.str_checkname()!!
+ fs.globals.add(FuncState.Globaldesc(fname, false, fs.nactvar.toInt()))
+ val `var`: expdesc = net.blueva.luak.compiler.LexState.expdesc()
+ this.buildglobal(fname, `var`)
+ val b: expdesc = net.blueva.luak.compiler.LexState.expdesc()
+ this.body(b, false, this.linenumber)
+ this.checkglobal(fname, line)
+ fs.storevar(`var`, b)
+ }
+
+ /**
+ * `attrib` on a global, which accepts `` but not ``.
+ *
+ * There is no scope for a global to be closed at the end of, so ``
+ * is rejected rather than quietly treated as ``.
+ */
+ private fun getglobalattribute(default: Int): Int {
+ if (this.t.token != '<'.code) return default
+ val kind: Int = this.getlocalattribute(default)
+ if (kind == net.blueva.luak.compiler.LexState.Companion.RDKTOCLOSE) {
+ this.semerror("global variables cannot be to-be-closed")
+ }
+ return kind
+ }
+
+ internal fun getlocalattribute(default: Int): Int {
+ if (this.testnext('<'.code)) {
+ val attribute: String? = this.str_checkname()?.tojstring()
+ this.checknext('>'.code)
+ if ("const" == attribute) return net.blueva.luak.compiler.LexState.Companion.RDKCONST
+ if ("close" == attribute) return net.blueva.luak.compiler.LexState.Companion.RDKTOCLOSE
+ this.lexerror("unknown attribute '" + attribute + "'", net.blueva.luak.compiler.LexState.Companion.TK_NAME)
+ }
+ return default
+ }
+
+ /** Rejects an assignment to a `` or `` local, or a `` global. */
+ internal fun check_readonly(e: expdesc) {
+ val globalname: LuaString? = e.readonlyGlobal
+ if (globalname != null) {
+ this.semerror("attempt to assign to const variable '" + globalname.tojstring() + "'")
+ }
+ if (e.k == net.blueva.luak.compiler.LexState.Companion.VUPVAL) {
+ // The same variable seen from an inner function.
+ val up = this.fs?.f?.upvalues?.getOrNull(e.u.info) ?: return
+ if (up.kind != net.blueva.luak.compiler.LexState.Companion.VDKREG) {
+ this.semerror(
+ "attempt to assign to const variable '" + (up.name?.tojstring() ?: "?") + "'",
+ )
+ }
+ return
+ }
+ if (e.k != net.blueva.luak.compiler.LexState.Companion.VLOCAL) return
+ val fs: FuncState = this.fs!!
+ val index: Int = fs.firstlocal + e.u.info
+ val vars: Array = this.dyd?.actvar ?: return
+ if (index < 0 || index >= vars.size) return
+ // A variable is read-only too: the value it holds is the one
+ // that will be closed, so it must be the one it was given.
+ val kind: Int = vars[index]?.kind ?: return
+ if (kind == net.blueva.luak.compiler.LexState.Companion.RDKCONST ||
+ kind == net.blueva.luak.compiler.LexState.Companion.RDKTOCLOSE
+ ) {
+ val name: String = fs.getlocvar(e.u.info).varname?.tojstring() ?: "?"
+ // A semantic error, not a lexical one: the offending token has
+ // already been read, so there is no "near" to report.
+ this.semerror("attempt to assign to const variable '" + name + "'")
+ }
+ }
internal fun funcname(v: expdesc): Boolean {
/* funcname -> NAME {field} [`:' NAME] */
@@ -1842,6 +2244,9 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
val b: expdesc = net.blueva.luak.compiler.LexState.expdesc()
this.next() /* skip FUNCTION */
needself = this.funcname(v)
+ // "function f() end" assigns to f, so a read-only f is as much an
+ // error here as it would be written out as an assignment.
+ this.check_readonly(v)
this.body(b, needself, line)
fs!!.storevar(v, b)
fs!!.fixline(line) /* definition `happens' in the first line */
@@ -1875,7 +2280,11 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
nret = this.explist(e) /* optional return values */
if (hasmultret(e.k)) {
fs.setmultret(e)
- if (e.k == net.blueva.luak.compiler.LexState.Companion.VCALL && nret == 1) { /* tail call? */
+ // Inside the scope of a to-be-closed variable the frame has
+ // to outlive the call, so the return stays an ordinary one.
+ if (e.k == net.blueva.luak.compiler.LexState.Companion.VCALL && nret == 1 &&
+ !fs.bl!!.insidetbc
+ ) { /* tail call? */
SET_OPCODE((fs.getcodePtr(e))!!, Lua.OP_TAILCALL)
_assert(Lua.GETARG_A(fs.getcode(e)) == fs.nactvar.toInt())
}
@@ -1964,6 +2373,17 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
this.gotostat(fs!!.jump())
}
+ net.blueva.luak.compiler.LexState.Companion.TK_NAME -> {
+ // 'global' is a statement, not a reserved word: a program that
+ // already uses it as a name keeps working, and only the shapes
+ // a declaration can take are read as one.
+ if (this.t.seminfo.ts == this.glbn && this.startsglobalstat()) {
+ this.globalstatfunc(line)
+ } else {
+ this.exprstat()
+ }
+ }
+
else -> {
this.exprstat()
}
@@ -2006,8 +2426,17 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
companion object {
protected val RESERVED_LOCAL_VAR_FOR_CONTROL: String = "(for control)"
+
+ /** The slot every vararg function keeps for the table form of `...`. */
+ protected val RESERVED_LOCAL_VAR_FOR_VARARGS: String = "(vararg table)"
+
+ // The iterator, the state and the value the loop closes at the end all
+ // go by one name, as they do upstream, so code that walks a frame's
+ // locals counts them the way Lua's own test suite expects: the third
+ // "(for state)" is the closing one.
+ protected val RESERVED_LOCAL_VAR_FOR_CLOSING: String = "(for state)"
protected val RESERVED_LOCAL_VAR_FOR_STATE: String = "(for state)"
- protected val RESERVED_LOCAL_VAR_FOR_GENERATOR: String = "(for generator)"
+ protected val RESERVED_LOCAL_VAR_FOR_GENERATOR: String = "(for state)"
protected val RESERVED_LOCAL_VAR_FOR_STEP: String = "(for step)"
protected val RESERVED_LOCAL_VAR_FOR_LIMIT: String = "(for limit)"
protected val RESERVED_LOCAL_VAR_FOR_INDEX: String = "(for index)"
@@ -2015,6 +2444,7 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
// keywords array
protected val RESERVED_LOCAL_VAR_KEYWORDS: Array = arrayOf(
net.blueva.luak.compiler.LexState.Companion.RESERVED_LOCAL_VAR_FOR_CONTROL,
+ net.blueva.luak.compiler.LexState.Companion.RESERVED_LOCAL_VAR_FOR_CLOSING,
net.blueva.luak.compiler.LexState.Companion.RESERVED_LOCAL_VAR_FOR_GENERATOR,
net.blueva.luak.compiler.LexState.Companion.RESERVED_LOCAL_VAR_FOR_INDEX,
net.blueva.luak.compiler.LexState.Companion.RESERVED_LOCAL_VAR_FOR_LIMIT,
@@ -2074,12 +2504,19 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
const val OPR_GE: Int = 12
const val OPR_AND: Int = 13
const val OPR_OR: Int = 14
- const val OPR_NOBINOPR: Int = 15
+ const val OPR_IDIV: Int = 15
+ const val OPR_BAND: Int = 16
+ const val OPR_BOR: Int = 17
+ const val OPR_BXOR: Int = 18
+ const val OPR_SHL: Int = 19
+ const val OPR_SHR: Int = 20
+ const val OPR_NOBINOPR: Int = 21
const val OPR_MINUS: Int = 0
const val OPR_NOT: Int = 1
const val OPR_LEN: Int = 2
- const val OPR_NOUNOPR: Int = 3
+ const val OPR_BNOT: Int = 3
+ const val OPR_NOUNOPR: Int = 4
/* exp kind */
const val VVOID: Int = 0 /* no value */
@@ -2104,7 +2541,7 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
"in", "local", "nil", "not", "or", "repeat",
"return", "then", "true", "until", "while",
"..", "...", "==", ">=", "<=", "~=",
- "::", "", "", "", "", "",
+ "::", "", "", "", "", "//", "<<", ">>",
)
const val /* terminal symbols denoted by reserved words */TK_AND: Int = 257
@@ -2142,6 +2579,9 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
const val TK_NUMBER: Int = 287
const val TK_NAME: Int = 288
const val TK_STRING: Int = 289
+ const val TK_IDIV: Int = 290
+ const val TK_SHL: Int = 291
+ const val TK_SHR: Int = 292
val FIRST_RESERVED: Int = net.blueva.luak.compiler.LexState.Companion.TK_AND
val NUM_RESERVED: Int =
@@ -2160,8 +2600,15 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
}
}
+ /**
+ * True for a byte that cannot be shown as itself.
+ *
+ * Only the printable ASCII range is shown as a character; anything
+ * else, a byte past 127 included, is written as its number, since
+ * what it looks like depends on how the text is being read.
+ */
private fun iscntrl(token: Int): Boolean {
- return token < ' '.code
+ return token < ' '.code || token > '~'.code
}
// =============================================================
@@ -2195,14 +2642,17 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
}
+ // Levels follow Lua 5.5's table so the operators added by the port have
+ // room between comparison and concatenation. The relative order of the
+ // operators that already existed is unchanged.
internal var priority: Array =
- arrayOf( /* ORDER OPR */net.blueva.luak.compiler.LexState.Priority(6, 6),
- net.blueva.luak.compiler.LexState.Priority(6, 6),
- net.blueva.luak.compiler.LexState.Priority(7, 7),
- net.blueva.luak.compiler.LexState.Priority(7, 7),
- net.blueva.luak.compiler.LexState.Priority(7, 7), /* `+' `-' `/' `%' */
- net.blueva.luak.compiler.LexState.Priority(10, 9),
- net.blueva.luak.compiler.LexState.Priority(5, 4), /* power and concat (right associative) */
+ arrayOf( /* ORDER OPR */net.blueva.luak.compiler.LexState.Priority(10, 10),
+ net.blueva.luak.compiler.LexState.Priority(10, 10), /* `+' `-' */
+ net.blueva.luak.compiler.LexState.Priority(11, 11),
+ net.blueva.luak.compiler.LexState.Priority(11, 11),
+ net.blueva.luak.compiler.LexState.Priority(11, 11), /* `*' `/' `%' */
+ net.blueva.luak.compiler.LexState.Priority(14, 13),
+ net.blueva.luak.compiler.LexState.Priority(9, 8), /* power and concat (right associative) */
net.blueva.luak.compiler.LexState.Priority(3, 3),
net.blueva.luak.compiler.LexState.Priority(3, 3), /* equality and inequality */
net.blueva.luak.compiler.LexState.Priority(3, 3),
@@ -2210,9 +2660,20 @@ internal class LexState internal constructor(state: LuaC.CompileState?, stream:
net.blueva.luak.compiler.LexState.Priority(3, 3),
net.blueva.luak.compiler.LexState.Priority(3, 3), /* order */
net.blueva.luak.compiler.LexState.Priority(2, 2),
- net.blueva.luak.compiler.LexState.Priority(1, 1) /* logical (and/or) */
+ net.blueva.luak.compiler.LexState.Priority(1, 1), /* logical (and/or) */
+ net.blueva.luak.compiler.LexState.Priority(11, 11), /* `//' */
+ net.blueva.luak.compiler.LexState.Priority(6, 6), /* `&' */
+ net.blueva.luak.compiler.LexState.Priority(4, 4), /* `|' */
+ net.blueva.luak.compiler.LexState.Priority(5, 5), /* `~' */
+ net.blueva.luak.compiler.LexState.Priority(7, 7),
+ net.blueva.luak.compiler.LexState.Priority(7, 7) /* `<<' `>>' */
)
- const val UNARY_PRIORITY: Int = 8 /* priority for unary operators */
+ const val UNARY_PRIORITY: Int = 12 /* priority for unary operators */
+
+ /* kinds of local variable, from the attribute in its declaration */
+ const val VDKREG: Int = 0 /* regular */
+ const val RDKCONST: Int = 1 /* */
+ const val RDKTOCLOSE: Int = 2 /* */
}
}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/BaseLib.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/BaseLib.kt
index 4f8b1408..595c9204 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/BaseLib.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/BaseLib.kt
@@ -86,6 +86,21 @@ import net.blueva.luak.io.platformStandardInput
open class BaseLib : TwoArgFunction(), ResourceFinder {
var globals: Globals? = null
+ /** Whether `warn` currently emits anything; warnings start switched off. */
+ internal var warningsOn: Boolean = false
+
+ /**
+ * Emits [text] as a warning, the way `warn` would.
+ *
+ * The runtime reports what it cannot raise - an error inside a `__gc`
+ * handler, which has no caller to raise to - and, like `warn`, says
+ * nothing at all until warnings have been switched on.
+ */
+ internal fun warning(text: String) {
+ if (!warningsOn) return
+ globals!!.STDERR!!.println("Lua warning: " + text)
+ }
+
/** Perform one-time initialization on the library by adding base functions
* to the supplied environment, and returning it as the return value.
@@ -100,7 +115,8 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
env!!.set("_G", env)
env!!.set("_VERSION", Lua._VERSION)
env!!.set("assert", net.blueva.luak.lib.BaseLib._assert())
- env!!.set("collectgarbage", net.blueva.luak.lib.BaseLib.collectgarbage())
+ env!!.set("collectgarbage", net.blueva.luak.lib.BaseLib.collectgarbage(this))
+ env!!.set("warn", warn(this))
env!!.set("dofile", dofile())
env!!.set("error", net.blueva.luak.lib.BaseLib.error())
env!!.set("getmetatable", net.blueva.luak.lib.BaseLib.getmetatable())
@@ -113,7 +129,7 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
env!!.set("rawlen", net.blueva.luak.lib.BaseLib.rawlen())
env!!.set("rawset", net.blueva.luak.lib.BaseLib.rawset())
env!!.set("select", net.blueva.luak.lib.BaseLib.select())
- env!!.set("setmetatable", net.blueva.luak.lib.BaseLib.setmetatable())
+ env!!.set("setmetatable", net.blueva.luak.lib.BaseLib.setmetatable(this))
env!!.set("tonumber", net.blueva.luak.lib.BaseLib.tonumber())
env!!.set("tostring", net.blueva.luak.lib.BaseLib.tostring())
env!!.set("type", net.blueva.luak.lib.BaseLib.type())
@@ -164,29 +180,133 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
// "assert", // ( v [,message] ) -> v, message | ERR
internal class _assert : VarArgFunction() {
override fun invoke(args: Varargs): Varargs {
- if (!args.arg1()!!.toboolean()) error(
- if (args.narg() > 1) args.optjstring(
- 2,
- "assertion failed!"
- ) else "assertion failed!"
- )
- return args
+ if (args.arg1()!!.toboolean()) return args
+ // There has to be something to test in the first place, which is
+ // the one complaint `assert` makes about its own arguments.
+ if (args.narg() == 0) argerror(1, "value expected")
+ // Whatever was given as the message is raised as it stands - only
+ // the second argument, and only when there was one - so a table
+ // reaches the caller as a table. Without one, the message is the
+ // usual text, which being a string is given a place to point at.
+ val message: LuaValue = if (args.narg() > 1) args.arg(2)!! else valueOf("assertion failed!")!!
+ // A nil message becomes text where it is raised, as `error` does
+ // with one, so that a handler always has something to report.
+ if (message.isnil()) {
+ val failure = LuaError(valueOf(""))
+ failure.level = 0
+ throw failure
+ }
+ if (message.type() != LuaValue.TSTRING) {
+ val failure = LuaError(message)
+ failure.level = 0
+ throw failure
+ }
+ throw LuaError(message.tojstring(), 1)
}
}
// "collectgarbage", // ( opt [,arg] ) -> value
- internal class collectgarbage : VarArgFunction() {
+ /**
+ * `warn (msg1, ...)`, from Lua 5.4.
+ *
+ * Emits a warning built by joining the arguments. Warnings start switched
+ * off and are turned on and off by the control messages `"@on"` and
+ * `"@off"`, which are single arguments beginning with `@` and are never
+ * shown themselves.
+ */
+ internal class warn(private val baselib: BaseLib) : VarArgFunction() {
+ override fun invoke(args: Varargs): Varargs {
+ val n: Int = args.narg()
+ for (i in 1..n) args.checkstring(i)
+ if (n == 1) {
+ val control: String = args.checkjstring(1)!!
+ if (control.startsWith("@")) {
+ if (control == "@on") baselib.warningsOn = true
+ else if (control == "@off") baselib.warningsOn = false
+ return NONE!!
+ }
+ }
+ if (!baselib.warningsOn) return NONE!!
+ val message: StringBuilder = StringBuilder("Lua warning: ")
+ for (i in 1..n) message.append(args.checkjstring(i))
+ baselib.globals!!.STDERR!!.println(message.toString())
+ return NONE!!
+ }
+ }
+
+ internal class collectgarbage(private val baselib: BaseLib) : VarArgFunction() {
+ companion object {
+ /** The collector mode last asked for; 5.5 starts generational. */
+ var mode: String = "generational"
+
+ /** How much of a cycle the steps asked for so far add up to. */
+ var stepped: Int = 0
+
+ /** What a cycle's worth of steps comes to. */
+ const val CYCLE: Int = 100
+
+ /** The tunables and their Lua 5.5 defaults. */
+ val parameters: MutableMap = mutableMapOf(
+ "minormul" to 20L,
+ "majorminor" to 50L,
+ "pause" to 250L,
+ "stepmul" to 200L,
+ "stepsize" to 9600L,
+ )
+ }
+
override fun invoke(args: Varargs): Varargs {
val s: String? = args.optjstring(1, "collect")
if ("collect".equals(s)) {
platformCollectGarbage()
+ baselib.globals!!.runfinalizers()
+ net.blueva.luak.Memory.collected()
return (ZERO)!!
} else if ("count".equals(s)) {
- val used: Long = platformUsedMemory()
+ val used: Long = net.blueva.luak.Memory.used()
return (varargsOf(valueOf(used / 1024.0), valueOf((used % 1024).toInt())))!!
} else if ("step".equals(s)) {
+ // A step of the size asked for, and the answer says whether it
+ // was the one that finished a cycle. The host collector runs
+ // whole cycles of its own, so what is stepped through here is
+ // the debt Lua would have worked off before running one.
platformCollectGarbage()
+ baselib.globals!!.runfinalizers()
+ val size: Int = args.optint(2, 0)
+ net.blueva.luak.lib.BaseLib.collectgarbage.stepped += if (size > 0) size else 1
+ if (net.blueva.luak.lib.BaseLib.collectgarbage.stepped <
+ net.blueva.luak.lib.BaseLib.collectgarbage.CYCLE
+ ) {
+ return (LuaValue.FALSE)!!
+ }
+ net.blueva.luak.lib.BaseLib.collectgarbage.stepped = 0
+ net.blueva.luak.Memory.collected()
return (LuaValue.TRUE)!!
+ } else if ("isrunning".equals(s)) {
+ return (valueOf(net.blueva.luak.Memory.running))!!
+ } else if ("stop".equals(s)) {
+ net.blueva.luak.Memory.running = false
+ return (ZERO)!!
+ } else if ("restart".equals(s)) {
+ net.blueva.luak.Memory.running = true
+ return (ZERO)!!
+ } else if ("param".equals(s)) {
+ // The host collector is not tunable from here, so a parameter
+ // is only remembered. Lua answers the value that was in force.
+ val name: String = args.checkjstring(2)!!
+ val previous: Long = net.blueva.luak.lib.BaseLib.collectgarbage.parameters[name]
+ ?: return (argerror(2, "invalid option '" + name + "'"))!!
+ if (!args.isnoneornil(3)) {
+ net.blueva.luak.lib.BaseLib.collectgarbage.parameters[name] = args.checklong(3)
+ }
+ return valueOf(previous)!!
+ } else if ("generational".equals(s) || "incremental".equals(s)) {
+ // The host collector picks its own strategy, so the mode is
+ // only remembered, not applied. Lua answers the mode that was
+ // in force before the call.
+ val previous: String = net.blueva.luak.lib.BaseLib.collectgarbage.mode
+ net.blueva.luak.lib.BaseLib.collectgarbage.mode = s!!
+ return valueOf(previous)!!
} else {
argerror(1, "invalid option '" + s + "'")
}
@@ -207,14 +327,138 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
) else loadFile(args.checkjstring(1), "bt", globals)
return (if (v.isnil(1)) error(v.tojstring(2)) else v.arg1()!!.invoke())!!
}
+
+ // The chunk runs as part of this call, so a yield inside it has to
+ // pass through; see BaseLib.pcall.invokeSuspend().
+ override suspend fun invokeSuspend(args: Varargs): Varargs {
+ args.argcheck(args.isstring(1) || args.isnil(1), 1, "filename must be string or nil")
+ val filename: String? = if (args.isstring(1)) args.tojstring(1) else null
+ val v: Varargs = if (filename == null) loadStream(
+ globals!!.STDIN,
+ "=stdin",
+ "bt",
+ globals
+ ) else loadFile(args.checkjstring(1), "bt", globals)
+ return (if (v.isnil(1)) error(v.tojstring(2)) else v.arg1()!!.invokeSuspend(NONE!!))!!
+ }
+ }
+
+ /**
+ * Names the function in a bare "bad argument #N" message.
+ *
+ * A protected call reaches its callee directly, so there is no call site
+ * for the interpreter to read a name from and the message arrives without
+ * one. Lua falls back to the name the function goes by in
+ * `package.loaded`, which is how `pcall(string.pack, ...)` still reports
+ * "bad argument #2 to 'string.pack'".
+ */
+ internal fun nameArgumentError(failure: LuaError, called: LuaValue) {
+ // An index may already have been stamped on; a name has not, and that
+ // is what is missing when the call came in through here.
+ if (failure.argMessageOverride?.contains(" to '") == true) return
+ val message: String = failure.message ?: return
+ val match = Regex("^bad argument #(\\d+): ([\\s\\S]*)$").find(message) ?: return
+ val name: String = loadedName(called) ?: "?"
+ failure.argMessageOverride =
+ "bad argument #" + match.groupValues[1] + " to '" + name + "' (" + match.groupValues[2] + ")"
+ }
+
+ /**
+ * The name [target] goes by in `package.loaded`, such as `"string.pack"`.
+ *
+ * Only the modules themselves and their immediate fields are searched, as
+ * upstream searches, and a function of `_G` keeps its bare name.
+ */
+ private fun loadedName(target: LuaValue): String? {
+ val loaded: LuaValue = globals?.get("package")?.get("loaded") ?: return null
+ if (!loaded.istable()) return null
+ var moduleKey: Varargs = loaded.next(NIL)!!
+ while (!moduleKey.arg1()!!.isnil()) {
+ val key: LuaValue = moduleKey.arg1()!!
+ val module: LuaValue = moduleKey.arg(2)!!
+ if (key.isstring()) {
+ val prefix: String = key.tojstring()
+ if (module === target) return prefix
+ if (module.istable()) {
+ var fieldKey: Varargs = module.next(NIL)!!
+ while (!fieldKey.arg1()!!.isnil()) {
+ if (fieldKey.arg(2) === target && fieldKey.arg1()!!.isstring()) {
+ val field: String = fieldKey.arg1()!!.tojstring()
+ return if (prefix == "_G") field else prefix + "." + field
+ }
+ fieldKey = module.next(fieldKey.arg1()!!)!!
+ }
+ }
+ }
+ moduleKey = loaded.next(key)!!
+ }
+ return null
+ }
+
+ companion object {
+ /**
+ * Lua's own rendering of a value as a string, upstream's `luaL_tolstring`.
+ *
+ * A `__tostring` metamethod decides if there is one, and must answer
+ * something string-like. Otherwise a value with no text of its own is
+ * named by its `__name` metafield, falling back to its type, followed
+ * by an identity.
+ */
+ internal fun tolstring(value: LuaValue): LuaValue {
+ val handler: LuaValue = value.metatag(TOSTRING)
+ if (!handler.isnil()) {
+ val rendered: LuaValue = handler.call(value)!!
+ if (!rendered.isstring()) LuaValue.error("'__tostring' must return a string")
+ return rendered
+ }
+ when (value.type()) {
+ // A string is already its own rendering, and handing back the
+ // same bytes matters: decoding and re-encoding them would
+ // mangle any that are not valid UTF-8.
+ LuaValue.TSTRING -> return value
+
+ // The other primitives render as themselves, whatever metatable
+ // a shared one may carry.
+ LuaValue.TNIL, LuaValue.TBOOLEAN, LuaValue.TNUMBER ->
+ return valueOf(value.tojstring())!!
+ }
+ val own: LuaValue = value.tostring()
+ if (!own.isnil()) return own
+ val name: LuaValue = value.metatag(net.blueva.luak.LuaValue.NAME)
+ if (name.isstring()) {
+ val rendered: String = value.tojstring()
+ return valueOf(name.tojstring() + ": " + rendered.substringAfter(": ", rendered))!!
+ }
+ // Otherwise the value's own rendering, which on this runtime is
+ // already "type: identity" and, for a Java object behind a
+ // userdata, that object's own text.
+ return valueOf(value.tojstring())!!
+ }
}
// "error", // ( message [,level] ) -> ERR
internal class error : TwoArgFunction() {
override fun call(arg1: LuaValue?, arg2: LuaValue?): LuaValue? {
- if (arg1!!.isnil()) throw LuaError(NIL)
- if (!arg1!!.isstring() || arg2!!.optint(1) === 0) throw LuaError(arg1)
- throw LuaError(arg1!!.tojstring(), arg2!!.optint(1))
+ // A nil error object becomes text at the point it is raised, so a
+ // handler always has something to report.
+ if (arg1!!.isnil()) throw LuaError(valueOf(""))
+ val level: Int = arg2!!.optint(1)
+ if (arg1.type() != LuaValue.TSTRING) {
+ // Only a string ever gets a position: anything else is the
+ // error object itself and has to reach the handler untouched.
+ val failure = LuaError(arg1)
+ failure.level = 0
+ throw failure
+ }
+ if (level == 0) {
+ // Level 0 asks for the message exactly as written, with no
+ // position added to it - not even by the interpreter's own
+ // error hook, which is what the level records for it.
+ val failure = LuaError(arg1)
+ failure.level = 0
+ throw failure
+ }
+ throw LuaError(arg1.tojstring(), level)
}
}
@@ -239,6 +483,8 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
}
val source: String? = args.optjstring(2, if (ld.isstring()) ld.tojstring() else "=(load)")
val mode: String? = args.optjstring(3, "bt")
+ // 'B' asks for a fixed buffer, which only the C API can supply.
+ if (mode != null && mode.indexOf('B') >= 0) argerror(3, "invalid mode")
val env: LuaValue? = args.optvalue(4, globals)
return loadStream(
if (ld.isstring()) ld.strvalue()
@@ -269,7 +515,6 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
internal inner class pcall : VarArgFunction() {
override fun invoke(args: Varargs): Varargs {
val func: LuaValue = args.checkvalue(1)!!
- if (globals != null && globals!!.debuglib != null) globals!!.debuglib!!.onCall(this)
// Shadow any outer xpcall's message handler while this pcall's own
// protected region is active: an error raised in here belongs to
// THIS pcall, not to an unrelated, further-out xpcall, matching
@@ -281,16 +526,24 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
val preverror: LuaValue? = t?.errorfunc
if (t != null) t.errorfunc = null
try {
- return (varargsOf(TRUE, (func.invoke((args.subargs(2))!!))!!))!!
+ return (varargsOf(TRUE, (protectedCall(t, func, args.subargs(2)!!))!!))!!
} catch (le: LuaError) {
+ nameArgumentError(le, func)
val m: LuaValue? = le.messageObject
return (varargsOf(FALSE, if (m != null) m else NIL))!!
} catch (e: Exception) {
val m: String? = e.message
return (varargsOf(FALSE, valueOf(if (m != null) m else e.toString())))!!
+ } catch (deep: Throwable) {
+ // Unbounded recursion exhausts the host's stack rather than a
+ // stack of Lua's own; a protected call is where that becomes
+ // the ordinary Lua error the caller expects. The conversion
+ // happens here, not deeper in, because building the error needs
+ // some stack back.
+ if (!net.blueva.luak.platformIsStackOverflow(deep)) throw deep
+ return (varargsOf(FALSE, valueOf(overflowmessage(t))))!!
} finally {
if (t != null) t.errorfunc = preverror
- if (globals != null && globals!!.debuglib != null) globals!!.debuglib!!.onReturn()
}
}
@@ -300,22 +553,29 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
// instead of hitting the C-call boundary error.
override suspend fun invokeSuspend(args: Varargs): Varargs {
val func: LuaValue = args.checkvalue(1)!!
- if (globals != null && globals!!.debuglib != null) globals!!.debuglib!!.onCall(this)
// See the non-suspend invoke() above for why errorfunc is shadowed.
val t: LuaThread? = globals?.running
val preverror: LuaValue? = t?.errorfunc
if (t != null) t.errorfunc = null
try {
- return (varargsOf(TRUE, (func.invokeSuspend((args.subargs(2))!!))!!))!!
+ return (varargsOf(TRUE, (protectedCallSuspend(t, func, args.subargs(2)!!))!!))!!
} catch (le: LuaError) {
+ nameArgumentError(le, func)
val m: LuaValue? = le.messageObject
return (varargsOf(FALSE, if (m != null) m else NIL))!!
} catch (e: Exception) {
val m: String? = e.message
return (varargsOf(FALSE, valueOf(if (m != null) m else e.toString())))!!
+ } catch (deep: Throwable) {
+ // Unbounded recursion exhausts the host's stack rather than a
+ // stack of Lua's own; a protected call is where that becomes
+ // the ordinary Lua error the caller expects. The conversion
+ // happens here, not deeper in, because building the error needs
+ // some stack back.
+ if (!net.blueva.luak.platformIsStackOverflow(deep)) throw deep
+ return (varargsOf(FALSE, valueOf(overflowmessage(t))))!!
} finally {
if (t != null) t.errorfunc = preverror
- if (globals != null && globals!!.debuglib != null) globals!!.debuglib!!.onReturn()
}
}
}
@@ -323,12 +583,14 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
// "print", // (...) -> void
internal inner class print(val baselib: BaseLib) : VarArgFunction() {
override fun invoke(args: Varargs): Varargs {
- val tostring: LuaValue = globals!!.get("tostring")!!
+ // Rendered here rather than through the global `tostring`: Lua
+ // uses its own conversion, so replacing that global changes what
+ // scripts see from `tostring` and leaves `print` alone.
var i = 1
val n: Int = args.narg()
while (i <= n) {
if (i > 1) globals!!.STDOUT!!.print('\t')
- val s: LuaString = tostring.call(args.arg(i))!!.strvalue()!!
+ val s: LuaString = net.blueva.luak.lib.BaseLib.tolstring(args.arg(i)!!).strvalue()!!
globals!!.STDOUT!!.print(s.tojstring())
i++
}
@@ -402,15 +664,33 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
}
// "setmetatable", // (table, metatable) -> table
- internal class setmetatable : TableLibFunction() {
+ internal class setmetatable(private val baselib: BaseLib) : TableLibFunction() {
override fun call(table: LuaValue?): LuaValue? {
+ // What it was given is looked at first, as Lua looks at it: being
+ // handed something that is not a table is the more useful thing to
+ // be told about than the missing second argument. Named as the
+ // argument it is, so that a library function given this one as a
+ // callback can be told which argument was wrong.
+ if (!table!!.istable()) {
+ argerror(1, "table expected, got " + table.argtypename())
+ }
return (argerror(2, "nil or table expected"))!!
}
override fun call(table: LuaValue?, metatable: LuaValue?): LuaValue? {
+ // Named as the argument it is, so that a library function given
+ // this one as a callback can be told which argument was wrong.
+ if (!table!!.istable()) {
+ argerror(1, "table expected, got " + table.argtypename())
+ }
val mt0: LuaValue? = table!!.checktable()!!.getmetatable()
if (mt0 != null && !mt0.rawget(METATABLE).isnil()) error("cannot change a protected metatable")
- return (table!!.setmetatable(if (metatable!!.isnil()) null else metatable!!.checktable()))!!
+ val mt: LuaValue? = if (metatable!!.isnil()) null else metatable!!.checktable()
+ val answer: LuaValue = table!!.setmetatable(mt)!!
+ // Setting the metatable is where Lua decides an object is to be
+ // finalized, and the only place it decides it.
+ if (mt != null && !mt.rawget(GC).isnil()) baselib.globals!!.markforfinalization(table)
+ return answer
}
}
@@ -431,11 +711,7 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
// "tostring", // (e) -> value
internal class tostring : LibFunction() {
override fun call(arg: LuaValue?): LuaValue? {
- val h: LuaValue = arg!!.metatag(TOSTRING)
- if (!h.isnil()) return h.call(arg)
- val v: LuaValue = arg!!.tostring()
- if (!v.isnil()) return v
- return valueOf(arg!!.tojstring())
+ return net.blueva.luak.lib.BaseLib.tolstring(arg!!)
}
}
@@ -453,10 +729,10 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
val preverror: LuaValue? = t.errorfunc
t.errorfunc = args.checkvalue(2)
try {
- if (globals != null && globals!!.debuglib != null) globals!!.debuglib!!.onCall(this)
try {
- return (varargsOf(TRUE, (args.arg1()!!.invoke((args.subargs(3))!!))!!))!!
+ return (varargsOf(TRUE, (protectedCall(t, args.arg1()!!, args.subargs(3)!!))!!))!!
} catch (le: LuaError) {
+ nameArgumentError(le, args.arg1()!!)
if (le.traceback == null) {
// Error raised directly from native/library code (e.g. calling a
// non-function) never passed through a LuaClosure's error hook, so
@@ -470,8 +746,12 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
} catch (e: Exception) {
val m: String? = e.message
return (varargsOf(FALSE, valueOf(if (m != null) m else e.toString())))!!
- } finally {
- if (globals != null && globals!!.debuglib != null) globals!!.debuglib!!.onReturn()
+ } catch (overflow: Throwable) {
+ // See pcall: a host stack overflow becomes a Lua error here.
+ if (!net.blueva.luak.platformIsStackOverflow(overflow)) throw overflow
+ // The stack has unwound by the time this is reached, so
+ // there is room to run the handler over it.
+ return (varargsOf(FALSE, runMessageHandler(t, valueOf(overflowmessage(t)))))!!
}
} finally {
t.errorfunc = preverror
@@ -486,9 +766,8 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
val preverror: LuaValue? = t.errorfunc
t.errorfunc = args.checkvalue(2)
try {
- if (globals != null && globals!!.debuglib != null) globals!!.debuglib!!.onCall(this)
try {
- return (varargsOf(TRUE, (args.arg1()!!.invokeSuspend((args.subargs(3))!!))!!))!!
+ return (varargsOf(TRUE, (protectedCallSuspend(t, args.arg1()!!, args.subargs(3)!!))!!))!!
} catch (le: LuaError) {
if (le.traceback == null) {
return (varargsOf(FALSE, runMessageHandler(t, le.messageObject ?: NIL)))!!
@@ -498,8 +777,12 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
} catch (e: Exception) {
val m: String? = e.message
return (varargsOf(FALSE, valueOf(if (m != null) m else e.toString())))!!
- } finally {
- if (globals != null && globals!!.debuglib != null) globals!!.debuglib!!.onReturn()
+ } catch (overflow: Throwable) {
+ // See pcall: a host stack overflow becomes a Lua error here.
+ if (!net.blueva.luak.platformIsStackOverflow(overflow)) throw overflow
+ // The stack has unwound by the time this is reached, so
+ // there is room to run the handler over it.
+ return (varargsOf(FALSE, runMessageHandler(t, valueOf(overflowmessage(t)))))!!
}
} finally {
t.errorfunc = preverror
@@ -508,29 +791,69 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
private fun runMessageHandler(t: LuaThread, errval: LuaValue): LuaValue {
val handler = t.errorfunc ?: return errval
- t.errorfunc = null
+ // The handler stays installed while it runs, so an error it raises
+ // is handled in its turn, and the nesting is bounded rather than
+ // left to run away; see LuaClosure.errorHook.
+ if (t.state.foreigncalls >= LuaThread.State.MAX_HANDLER_CALLS) {
+ return valueOf("error in error handling")!!
+ }
+ // A handler written in Lua is counted where it enters the
+ // interpreter; one of the library's own never does, and is
+ // counted here so that a chain of them cannot go round for ever.
+ val outer: Int = t.state.foreigncalls
try {
+ if (handler !is net.blueva.luak.LuaClosure) t.state.foreigncalls++
+ t.state.inhandler++
return handler.call(errval) ?: NIL
+ } catch (nested: LuaError) {
+ // The handler raised in its turn. An error that has already
+ // been through the handler is the answer as it stands; one
+ // that has not goes through it now, until the room kept for
+ // handling runs out.
+ if (nested.traceback != null) return nested.messageObject ?: NIL
+ return runMessageHandler(t, nested.messageObject ?: NIL)
} catch (ignored: Throwable) {
- return valueOf("error in error handling")
+ return valueOf("error in error handling")!!
} finally {
- t.errorfunc = handler
+ t.state.inhandler--
+ t.state.foreigncalls = outer
}
}
}
- // "pairs" (t) -> iter-func, t, nil
+ // "pairs" (t) -> iter-func, t, nil, closing
internal class pairs(val next: BaseLib.next) : VarArgFunction() {
override fun invoke(args: Varargs): Varargs {
- return varargsOf(next, args.checktable(1), NIL)
+ val self: LuaValue = args.checkvalue(1)!!
+ val handler: LuaValue = self.metatag(LuaValue.PAIRS)
+ if (handler.isnil()) return varargsOf(next, args.checktable(1), NIL)
+ // A __pairs metamethod supplies the whole loop, four values and no
+ // more: the fourth is what the generic for closes when it ends.
+ return four(handler.invoke(self))
+ }
+
+ // The metamethod may yield, so a coroutine's 'for' can be driven from
+ // inside it; see BaseLib.pcall.invokeSuspend().
+ override suspend fun invokeSuspend(args: Varargs): Varargs {
+ val self: LuaValue = args.checkvalue(1)!!
+ val handler: LuaValue = self.metatag(LuaValue.PAIRS)
+ if (handler.isnil()) return varargsOf(next, args.checktable(1), NIL)
+ return four(handler.invokeSuspend(self))
}
+
+ /** The first four of [supplied], padded with nil, as Lua asks for. */
+ private fun four(supplied: Varargs): Varargs = varargsOf(
+ arrayOf(supplied.arg(1), supplied.arg(2), supplied.arg(3), supplied.arg(4)),
+ )!!
}
// // "ipairs", // (t) -> iter-func, t, 0
internal class ipairs : VarArgFunction() {
var inext: inext = net.blueva.luak.lib.BaseLib.inext()
override fun invoke(args: Varargs): Varargs {
- return varargsOf(inext, args.checktable(1), (ZERO)!!)
+ // Anything indexable will do: the iterator reads with ordinary
+ // indexing, so an __index metamethod is honoured.
+ return varargsOf(inext, args.checkvalue(1), (ZERO)!!)
}
}
@@ -542,9 +865,19 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
}
// "inext" ( table, [int-index] ) -> next-index, next-value
+ /**
+ * The iterator `ipairs` hands back.
+ *
+ * Counts up from the given index and stops at the first nil. The step
+ * wraps around at the maximum integer, as every integer operation in Lua
+ * does, so an iteration that reaches it ends rather than trapping.
+ */
internal class inext : VarArgFunction() {
override fun invoke(args: Varargs): Varargs {
- return (args.checktable(1).inext(args.arg(2)))!!
+ val list: LuaValue = args.checkvalue(1)!!
+ val index: Long = args.checklong(2) + 1L
+ val value: LuaValue = list.get(valueOf(index))
+ return if (value.isnil()) NIL else varargsOf(valueOf(index), value)!!
}
}
@@ -605,3 +938,64 @@ open class BaseLib : TwoArgFunction(), ResourceFinder {
}
}
}
+
+/**
+ * Runs the protected function, counting the re-entry into Lua.
+ *
+ * See [LuaThread.State.foreigncalls]: a protected call recurses on the
+ * host stack, so Lua counts it and stops before the stack is gone.
+ */
+private fun protectedCall(t: LuaThread?, f: LuaValue, args: Varargs): Varargs {
+ val state: LuaThread.State = t?.state ?: return f.invoke(args)
+ // A protected call is where the tally goes back to what it was, however
+ // the call ends; see LuaClosure.enterforeign.
+ val outer: Int = state.foreigncalls
+ val debuglib: DebugLib? = frameFor(t, f)
+ if (debuglib != null) debuglib.onCall(f as net.blueva.luak.LuaFunction)
+ try {
+ enterForeign(state)
+ return f.invoke(args)
+ } finally {
+ state.foreigncalls = outer
+ if (debuglib != null) debuglib.onReturn()
+ }
+}
+
+private suspend fun protectedCallSuspend(t: LuaThread?, f: LuaValue, args: Varargs): Varargs {
+ val state: LuaThread.State = t?.state ?: return f.invokeSuspend(args)
+ val outer: Int = state.foreigncalls
+ val debuglib: DebugLib? = frameFor(t, f)
+ if (debuglib != null) debuglib.onCall(f as net.blueva.luak.LuaFunction)
+ try {
+ enterForeign(state)
+ return f.invokeSuspend(args)
+ } finally {
+ state.foreigncalls = outer
+ if (debuglib != null) debuglib.onReturn()
+ }
+}
+
+/**
+ * The debug library to push a frame on for [f], or null for none.
+ *
+ * A function written in Lua pushes its own frame as it starts; one of the
+ * library's own has none, so whoever calls it pushes one for it. Without that
+ * the levels a traceback counts would skip it.
+ */
+private fun frameFor(t: LuaThread?, f: LuaValue): DebugLib? {
+ if (f !is net.blueva.luak.LuaFunction || f is net.blueva.luak.LuaClosure) return null
+ return t?.globals?.debuglib
+}
+
+/**
+ * What a host stack overflow is reported as on [t].
+ *
+ * Running out of stack in the room kept for handling an error is a failure of
+ * the handling; see LuaThread.State.inhandler.
+ */
+private fun overflowmessage(t: LuaThread?): String =
+ if ((t?.state?.inhandler ?: 0) > 0) "error in error handling" else "C stack overflow"
+
+private fun enterForeign(state: LuaThread.State) {
+ net.blueva.luak.enterForeignCall(state)
+}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/CoroutineLib.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/CoroutineLib.kt
index 9933eb36..dd4c9d1e 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/CoroutineLib.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/CoroutineLib.kt
@@ -81,6 +81,8 @@ class CoroutineLib : TwoArgFunction() {
coroutine.set("status", net.blueva.luak.lib.CoroutineLib.status())
coroutine.set("yield", YieldFunction())
coroutine.set("wrap", wrap())
+ coroutine.set("close", close())
+ coroutine.set("isyieldable", isyieldable())
env!!.set("coroutine", coroutine)
if (!env!!.get("package")!!.isnil()) env!!.get("package")!!.get("loaded")!!.set("coroutine", coroutine)
return coroutine
@@ -113,6 +115,35 @@ class CoroutineLib : TwoArgFunction() {
}
}
+ /**
+ * `coroutine.close (co)`, from Lua 5.4.
+ *
+ * Ends a suspended or dead coroutine, running any to-be-closed variables it
+ * was still holding.
+ */
+ internal inner class close : VarArgFunction() {
+ override fun invoke(args: Varargs): Varargs {
+ // With no argument it is the running coroutine that is closed.
+ val thread: LuaThread =
+ if (args.isnoneornil(1)) globals!!.running else args.checkthread(1)
+ return thread.close()
+ }
+ }
+
+ /**
+ * `coroutine.isyieldable ([co])`, from Lua 5.2.
+ *
+ * True when [co], or the running coroutine if none is given, could yield -
+ * that is, when it is not the main thread and no library call it is inside
+ * of stands in the way.
+ */
+ internal inner class isyieldable : VarArgFunction() {
+ override fun invoke(args: Varargs): Varargs {
+ val thread: LuaThread = if (args.isnoneornil(1)) globals!!.running else args.checkthread(1)
+ return valueOf(!thread.isMainThread && thread.state.noyield == 0)!!
+ }
+ }
+
internal inner class YieldFunction : VarArgFunction() {
// Reached only when yield() is called from outside the suspend-aware
// interpreter dispatch (e.g. from a library function's own callback,
@@ -141,14 +172,25 @@ class CoroutineLib : TwoArgFunction() {
this.luathread = luathread
}
+ // The coroutine is what this wrapper carries, which is what an upvalue
+ // is.
+ override fun nupvalues(): Int = 1
+
+ override fun upvaluestate(n: Int): Any? = if (n == 1) luathread else null
+
override fun invoke(args: Varargs): Varargs {
val result: Varargs = luathread.resume(args)
if (result.arg1()!!.toboolean()) {
return (result.subargs(2))!!
- } else {
- return (error(result.arg(2)!!.tojstring()))!!
}
+ // Raised as it stands, object and all: a wrapped coroutine passes
+ // its failure straight on to whoever called it.
+ throw LuaError(result.arg(2)!!)
}
+
+ // The coroutine's own run is where a yield inside it belongs, so this
+ // must not stand in the way; see BaseLib.pcall.invokeSuspend().
+ override suspend fun invokeSuspend(args: Varargs): Varargs = invoke(args)
}
companion object {
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/DebugLib.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/DebugLib.kt
index b2598404..1fb9862f 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/DebugLib.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/DebugLib.kt
@@ -21,6 +21,7 @@ import net.blueva.luak.arrayCopy
import net.blueva.luak.Globals
import net.blueva.luak.Lua
import net.blueva.luak.LuaBoolean
+import net.blueva.luak.LuaLightUserdata
import net.blueva.luak.LuaClosure
import net.blueva.luak.LuaError
import net.blueva.luak.LuaFunction
@@ -123,11 +124,13 @@ class DebugLib : TwoArgFunction() {
override fun invoke(args: Varargs): Varargs {
val t: LuaThread = if (args.narg() > 0) args.checkthread(1) else globals!!.running
val s: LuaThread.State = t.state
- return varargsOf(
- if (s.hookfunc != null) s.hookfunc else NIL,
- valueOf((if (s.hookcall) "c" else "") + (if (s.hookline) "l" else "") + (if (s.hookrtrn) "r" else "")),
- valueOf(s.hookcount)
- )
+ // Nothing installed: one answer, not an empty mask and a zero.
+ if (s.hookfunc == null) return NIL!!
+ // The letters in the order Lua writes them.
+ val mask: String = (if (s.hookcall) "c" else "") +
+ (if (s.hookrtrn) "r" else "") +
+ (if (s.hookline) "l" else "")
+ return varargsOf(hookfunction(t), valueOf(mask), valueOf(s.hookcount))
}
}
@@ -138,6 +141,14 @@ class DebugLib : TwoArgFunction() {
val thread: LuaThread = if (args.isthread(a)) args.checkthread(a++) else globals!!.running
var func: LuaValue? = args.arg(a++)
val what: String = args.optjstring(a++, "flnStu")!!
+ // Every letter has to name something this can report; a stray one
+ // is a mistake in the call rather than a request to be ignored. A
+ // leading '>' is called out on its own, as it means something in
+ // the C API that has no equivalent here.
+ if (what.startsWith(">")) return (argerror(a - 1, "invalid option '>'"))!!
+ for (letter in what) {
+ if (letter !in "flnStuLr") return (argerror(a - 1, "invalid option"))!!
+ }
val callstack = callstack(thread)
// find the stack info
@@ -156,7 +167,8 @@ class DebugLib : TwoArgFunction() {
val ar = callstack.auxgetinfo(what, func as LuaFunction?, frame)
val info: LuaTable = LuaTable()
if (what.indexOf('S') >= 0) {
- info.set(net.blueva.luak.lib.DebugLib.Companion.WHAT, net.blueva.luak.lib.DebugLib.Companion.LUA)
+ // What the function actually is, rather than always "Lua".
+ info.set(net.blueva.luak.lib.DebugLib.Companion.WHAT, valueOf(ar.what))
info.set(net.blueva.luak.lib.DebugLib.Companion.SOURCE, valueOf(ar.source))
info.set(net.blueva.luak.lib.DebugLib.Companion.SHORT_SRC, valueOf(ar.short_src))
info.set(net.blueva.luak.lib.DebugLib.Companion.LINEDEFINED, valueOf(ar.linedefined))
@@ -168,27 +180,34 @@ class DebugLib : TwoArgFunction() {
if (what.indexOf('u') >= 0) {
info.set(net.blueva.luak.lib.DebugLib.Companion.NUPS, valueOf(ar.nups.toInt()))
info.set(net.blueva.luak.lib.DebugLib.Companion.NPARAMS, valueOf(ar.nparams.toInt()))
- info.set(net.blueva.luak.lib.DebugLib.Companion.ISVARARG, if (ar.isvararg) ONE else ZERO)
+ info.set(net.blueva.luak.lib.DebugLib.Companion.ISVARARG, valueOf(ar.isvararg)!!)
}
if (what.indexOf('n') >= 0) {
- info.set(
- net.blueva.luak.lib.DebugLib.Companion.NAME,
- LuaValue.valueOf(if (ar.name != null) ar.name else "?")
- )
+ // A function looked up by value has no call to be named from,
+ // and then the field is absent rather than a placeholder.
+ val named: String? = ar.name
+ if (named != null) {
+ info.set(net.blueva.luak.lib.DebugLib.Companion.NAME, LuaValue.valueOf(named))
+ }
info.set(net.blueva.luak.lib.DebugLib.Companion.NAMEWHAT, LuaValue.valueOf(ar.namewhat))
}
+ if (what.indexOf('r') >= 0) {
+ info.set(net.blueva.luak.lib.DebugLib.Companion.FTRANSFER, valueOf(ar.ftransfer))
+ info.set(net.blueva.luak.lib.DebugLib.Companion.NTRANSFER, valueOf(ar.ntransfer))
+ }
if (what.indexOf('t') >= 0) {
- info.set(net.blueva.luak.lib.DebugLib.Companion.ISTAILCALL, ZERO)
+ info.set(net.blueva.luak.lib.DebugLib.Companion.ISTAILCALL, valueOf(ar.istailcall)!!)
+ info.set(net.blueva.luak.lib.DebugLib.Companion.EXTRAARGS, valueOf(ar.extraargs))
}
- if (what.indexOf('L') >= 0) {
+ // A function that is not written in Lua has no lines to report,
+ // and leaves the field absent rather than empty.
+ if (what.indexOf('L') >= 0 && func != null && func.isclosure()) {
val lines: LuaTable = LuaTable()
+ // Every line an instruction was compiled onto, as a set: the
+ // ones a breakpoint can usefully be put on.
+ val lineinfo: IntArray? = func.checkclosure()!!.p.lineinfo
+ if (lineinfo != null) for (line in lineinfo) lines.set(line, TRUE!!)
info.set(net.blueva.luak.lib.DebugLib.Companion.ACTIVELINES, lines)
- var cf: CallFrame?
- var l = 1
- while ((callstack.getCallFrame(l).also { cf = it }) != null) {
- if (cf!!.f === func) lines.insert(-1, valueOf(cf.currentline()))
- ++l
- }
}
if (what.indexOf('f') >= 0) {
if (func != null) info.set(net.blueva.luak.lib.DebugLib.Companion.FUNC, func)
@@ -202,10 +221,22 @@ class DebugLib : TwoArgFunction() {
override fun invoke(args: Varargs): Varargs {
var a = 1
val thread: LuaThread = if (args.isthread(a)) args.checkthread(a++) else globals!!.running
+ // A function where a level would go asks for a parameter's name,
+ // which lives in the prototype rather than on any stack, so only
+ // the name comes back and no value with it.
+ if (args.isfunction(a)) {
+ val func: LuaValue = args.checkfunction(a)!!
+ val index: Int = args.checkint(a + 1)
+ if (func !is LuaClosure) return NIL!!
+ return (func.p.getlocalname(index, 0) ?: NIL)!!
+ }
val level: Int = args.checkint(a++)
val local: Int = args.checkint(a++)
val f = callstack(thread).getCallFrame(level)
- return (if (f != null) f.getLocal(local) else NONE)!!
+ // A level that names no frame is a mistake in the call, not a
+ // question with a nil answer.
+ if (f == null) LuaValue.argerror(a - 2, "level out of range")
+ return (f!!.getLocal(local))!!
}
}
@@ -219,11 +250,38 @@ class DebugLib : TwoArgFunction() {
// debug.getregistry ()
internal inner class getregistry : ZeroArgFunction() {
- override fun call(): LuaValue? {
- return (globals)!!
- }
+ override fun call(): LuaValue? = registry()
}
+ /**
+ * The registry: a table of the runtime's own, not the globals.
+ *
+ * It holds what the library needs to keep alongside a program without
+ * putting it where the program can trip over it - the hook functions,
+ * under `_HOOKKEY`, keyed weakly by thread so that a coroutine's hook
+ * goes when the coroutine does.
+ */
+ fun registry(): LuaTable {
+ val existing: LuaTable? = registryTable
+ if (existing != null) return existing
+ val made = LuaTable()
+ val hooks = LuaTable()
+ hooks.setmetatable(tableOf(arrayOf(valueOf("__mode"), valueOf("k"))))
+ made.set(net.blueva.luak.lib.DebugLib.Companion.HOOKKEY!!, hooks)
+ registryTable = made
+ return made
+ }
+
+ /** Where the hook functions are kept, keyed by the thread they belong to. */
+ private fun hooks(): LuaTable =
+ registry().get(net.blueva.luak.lib.DebugLib.Companion.HOOKKEY!!)!! as LuaTable
+
+ /** The hook installed on [thread], or nil. */
+ internal fun hookfunction(thread: LuaThread): LuaValue =
+ hooks().get(thread)
+
+ private var registryTable: LuaTable? = null
+
// debug.getupvalue (f, up)
internal class getupvalue : VarArgFunction() {
override fun invoke(args: Varargs): Varargs {
@@ -235,16 +293,26 @@ class DebugLib : TwoArgFunction() {
if (name != null) {
return (varargsOf(name, (c.upValues[up - 1]!!.getValue())!!))!!
}
+ return NIL
}
+ // What a function of the library's own carries has no name of its
+ // own, which is what Lua answers for one: the empty string.
+ val carried: Any? = (func as? LuaFunction)?.upvaluestate(up)
+ if (carried != null) return (varargsOf(valueOf(""), LuaLightUserdata(carried)))!!
return NIL
}
}
// debug.getuservalue (u)
- internal class getuservalue : LibFunction() {
- override fun call(u: LuaValue?): LuaValue? {
- return if (u!!.isuserdata()) u else NIL
- }
+ /**
+ * `debug.getuservalue (u [, n])`.
+ *
+ * A userdata here carries the host object it wraps and nothing else: there
+ * are no user values attached to one, so there is never an nth to answer
+ * with.
+ */
+ internal class getuservalue : VarArgFunction() {
+ override fun invoke(args: Varargs): Varargs = NIL!!
}
@@ -265,7 +333,15 @@ class DebugLib : TwoArgFunction() {
'r' -> rtrn = true
}
val s: LuaThread.State = t.state
- s.hookfunc = func
+ // A hook with nothing to fire on is no hook at all, which is what
+ // Lua takes an empty mask and a zero count to mean.
+ val installed: LuaValue? =
+ if (call || line || rtrn || count > 0) func else null
+ // The function itself lives in the registry, keyed by its thread;
+ // the flags stay on the thread so the interpreter's
+ // per-instruction check costs nothing.
+ hooks().set(t, if (installed == null) NIL!! else installed)
+ s.hookfunc = installed
s.hookcall = call
s.hookline = line
s.hookcount = count
@@ -283,7 +359,8 @@ class DebugLib : TwoArgFunction() {
val local: Int = args.checkint(a++)
val value: LuaValue? = args.arg(a++)
val f = callstack(thread).getCallFrame(level)
- return (if (f != null) f.setLocal(local, value) else NONE)!!
+ if (f == null) LuaValue.argerror(a - 3, "level out of range")
+ return (f!!.setLocal(local, value))!!
}
}
@@ -325,12 +402,15 @@ class DebugLib : TwoArgFunction() {
// debug.setuservalue (udata, value)
internal class setuservalue : VarArgFunction() {
override fun invoke(args: Varargs): Varargs {
- val o: Any? = args.checkuserdata(1)
- val v: LuaValue = args.checkvalue(2)!!
- val u: LuaUserdata = args.arg1() as LuaUserdata
- u.m_instance = v.checkuserdata()!!
- u.m_metatable = v.getmetatable()
- return (NONE)!!
+ // It still has to be a full userdata, but there is nowhere to put
+ // the value: a userdata here carries the host object it wraps and
+ // nothing else, so this always answers that it did not fit.
+ val target: LuaValue = args.arg1()!!
+ if (target.type() != LuaValue.TUSERDATA || target is LuaLightUserdata) {
+ LuaValue.argerror(1, "userdata expected, got " + target.argtypename())
+ }
+ args.checkvalue(2)
+ return (FALSE)!!
}
}
@@ -339,8 +419,14 @@ class DebugLib : TwoArgFunction() {
override fun invoke(args: Varargs): Varargs {
var a = 1
val thread: LuaThread = if (args.isthread(a)) args.checkthread(a++) else globals!!.running
+ val given: LuaValue = args.arg(a)!!
+ // Anything that is not text is handed straight back: a message
+ // object of a program's own is not something to build on.
+ if (!given.isnil() && !given.isstring()) return given
val message: String? = args.optjstring(a++, null)
- val level: Int = args.optint(a++, 1)
+ // Level 1 is the function that asked, so the traceback does not
+ // start by naming this one.
+ val level: Int = args.optint(a++, if (thread === globals!!.running) 1 else 0)
val tb = callstack(thread).traceback(level)
return valueOf(if (message != null) message.toString() + "\n" + tb else tb)
}
@@ -351,13 +437,17 @@ class DebugLib : TwoArgFunction() {
override fun invoke(args: Varargs): Varargs {
val func: LuaValue? = args.checkfunction(1)
val up: Int = args.checkint(2)
+ // A bare reference to the storage itself, so two upvalues can be
+ // compared: the same one answers the same value every time, and
+ // two different ones never collide.
if (func is LuaClosure) {
- val c: LuaClosure = func as LuaClosure
- if (c.upValues != null && up > 0 && up <= c.upValues.size) {
- return valueOf(c.upValues[up - 1].hashCode())
+ if (up > 0 && up <= func.upValues.size) {
+ return LuaLightUserdata(func.upValues[up - 1]!!)
}
+ return NIL
}
- return NIL
+ val carried: Any? = (func as? LuaFunction)?.upvaluestate(up)
+ return if (carried != null) LuaLightUserdata(carried) else NIL
}
}
@@ -376,49 +466,291 @@ class DebugLib : TwoArgFunction() {
}
fun onCall(f: LuaFunction?) {
+ onCall(f, null)
+ }
+
+ /** As [onCall], remembering the arguments a library function was given. */
+ fun onCall(f: LuaFunction?, args: Array?) {
val s: LuaThread.State = globals!!.running.state
- if (s.inhook) return
- callstack().onCall(f)
- if (s.hookcall) callHook(s, net.blueva.luak.lib.DebugLib.Companion.CALL, NIL)
+ // The frame goes on even inside a hook: code the hook runs counts
+ // levels from itself, and skipping the bookkeeping would make it
+ // count one too few. Only the hook callback is left out, since a hook
+ // must not call itself.
+ val frames: CallStack = callstack()
+ frames.onCall(f)
+ val pushed: CallFrame = frames.frame!![frames.calls - 1]!!
+ pushed.extraargs = s.pendingextraargs
+ pushed.args = args
+ // What the call is handing over, for a hook to read back.
+ pushed.ftransfer = 1
+ pushed.ntransfer = args?.size ?: 0
+ val tail: Boolean = s.pendingtailcall
+ pushed.istailcall = tail
+ s.pendingtailcall = false
+ s.pendingextraargs = 0
+ markhookframe(s)
+ // A tail call is its own kind of event, since the frame it makes takes
+ // the place of the one that called it.
+ if (!s.inhook && s.hookcall) {
+ callHook(
+ s,
+ if (tail) net.blueva.luak.lib.DebugLib.Companion.TAILCALL
+ else net.blueva.luak.lib.DebugLib.Companion.CALL,
+ NIL,
+ )
+ }
}
fun onCall(c: LuaClosure?, varargs: Varargs?, stack: Array?) {
+ onCall(c, varargs, stack, null)
+ }
+
+ /**
+ * As [onCall], with the call's arguments in storage of their own.
+ *
+ * `debug.getlocal` and `debug.setlocal` reach the extra arguments of a
+ * vararg function through negative indices, and writing to one has to show
+ * through to the `...` the function itself reads, so both work on the same
+ * array.
+ */
+ fun onCall(c: LuaClosure?, varargs: Varargs?, stack: Array?, args: Array?) {
val s: LuaThread.State = globals!!.running.state
- if (s.inhook) return
- callstack().onCall(c, varargs, stack)
- if (s.hookcall) callHook(s, net.blueva.luak.lib.DebugLib.Companion.CALL, NIL)
+ val frames: CallStack = callstack()
+ frames.onCall(c, varargs, stack)
+ val pushed: CallFrame = frames.frame!![frames.calls - 1]!!
+ pushed.args = args
+ // The declared parameters are what a call hands a Lua function.
+ pushed.ftransfer = 1
+ pushed.ntransfer = c?.p?.numparams ?: 0
+ val tail: Boolean = s.pendingtailcall
+ pushed.istailcall = tail
+ s.pendingtailcall = false
+ pushed.extraargs = s.pendingextraargs
+ s.pendingextraargs = 0
+ markhookframe(s)
+ // A tail call is its own kind of event, since the frame it makes takes
+ // the place of the one that called it.
+ if (!s.inhook && s.hookcall) {
+ callHook(
+ s,
+ if (tail) net.blueva.luak.lib.DebugLib.Companion.TAILCALL
+ else net.blueva.luak.lib.DebugLib.Companion.CALL,
+ NIL,
+ )
+ }
}
fun onInstruction(pc: Int, v: Varargs?, top: Int) {
val s: LuaThread.State = globals!!.running.state
- if (s.inhook) return
+ // Where a frame is stays up to date even inside a hook; only the hook
+ // callbacks are held back, since a hook must not call itself.
callstack().onInstruction(pc, v, top)
- if (s.hookfunc == null) return
+ if (s.inhook || s.hookfunc == null) return
if (s.hookcount > 0) if (++s.bytecodes % s.hookcount === 0) callHook(
s,
net.blueva.luak.lib.DebugLib.Companion.COUNT,
NIL
)
if (s.hookline) {
- val newline = callstack().currentline()
- if (newline != s.lastline) {
+ val frames: CallStack = callstack()
+ val frame: CallFrame? = if (frames.calls > 0) frames.frame!![frames.calls - 1] else null
+ if (frame != null && frame.reachedNewLine()) {
+ val newline: Int = frame.currentline()
s.lastline = newline
- callHook(s, net.blueva.luak.lib.DebugLib.Companion.LINE, LuaValue.valueOf(newline))
+ // Code with no line information has no line to report, and
+ // nil is what Lua hands the hook in its place.
+ callHook(
+ s,
+ net.blueva.luak.lib.DebugLib.Companion.LINE,
+ if (newline >= 0) LuaValue.valueOf(newline) else NIL,
+ )
}
}
}
+ /**
+ * Counts the `__call` handlers standing in front of what is about to run.
+ *
+ * Each of them puts the value it was found on in front of the real
+ * arguments, and the frame that ends up running reports how many, which is
+ * what `debug.getinfo(f, "t").extraargs` answers with.
+ */
+ fun notecallchain(target: LuaValue) {
+ if (target is LuaFunction) return
+ val s: LuaThread.State = globals!!.running.state
+ var value: LuaValue = target
+ var chain = 0
+ while (value !is LuaFunction) {
+ val handler: LuaValue = value.metatag(LuaValue.CALL)
+ // Not callable, or longer than Lua follows: either way the call
+ // itself is what reports it, so nothing is noted here.
+ if (handler.isnil()) return
+ if (++chain > LuaValue.MAX_CALL_CHAIN) return
+ value = handler
+ }
+ s.pendingextraargs = chain
+ }
+
+ /** Says that the next frame pushed is one a tail call is making. */
+ fun ontailcall() {
+ globals!!.running.state.pendingtailcall = true
+ }
+
+ /** Marks the frame just pushed as the hook's own, when it is one. */
+ private fun markhookframe(s: LuaThread.State) {
+ if (s.finalizerframepending) {
+ s.finalizerframepending = false
+ val frames: CallStack = callstack()
+ if (frames.calls > 0) frames.frame!![frames.calls - 1]!!.finalizer = true
+ }
+ if (!s.hookframepending) return
+ s.hookframepending = false
+ val frames: CallStack = callstack()
+ if (frames.calls > 0) frames.frame!![frames.calls - 1]!!.hooked = true
+ }
+
+ /**
+ * Records the values a call is about to hand back, for a return hook.
+ *
+ * They take the place of the arguments on the frame, which is where Lua
+ * leaves them too: a hook reads either with `debug.getlocal`.
+ */
+ fun onResults(results: Varargs?) {
+ val frames: CallStack = callstack()
+ if (frames.calls == 0) return
+ val frame: CallFrame = frames.frame!![frames.calls - 1]!!
+ val count: Int = results?.narg() ?: 0
+ val values: Array = arrayOfNulls(count)
+ for (index in 0.. withoutTopFrame(body: () -> T): T {
+ hidetopframe()
+ try {
+ return body()
+ } finally {
+ showtopframe()
+ }
+ }
+
+ /** The frames hidden by [hidetopframe], innermost last. */
+ private val hidden: ArrayList>> = ArrayList()
+
+ /** Takes the innermost frame out of sight; see [withoutTopFrame]. */
+ fun hidetopframe() {
+ val stack: CallStack = callstack()
+ if (stack.calls == 0) return
+ // The slot is not just hidden but reused by whatever runs next, so
+ // what was in it has to be kept and put back afterwards.
+ val frame: CallFrame = stack.frame!![stack.calls - 1]!!
+ hidden.add(Pair(frame, frame.snapshot()))
+ stack.calls--
+ }
+
+ /** Puts back what [hidetopframe] took out of sight. */
+ fun showtopframe() {
+ if (hidden.isEmpty()) return
+ val (frame, saved) = hidden.removeAt(hidden.size - 1)
+ callstack().calls++
+ frame.restore(saved)
}
fun traceback(level: Int): String {
return callstack().traceback(level)
}
+ /**
+ * Names the function a library called back into, for an error it raised.
+ *
+ * A function of the library's own that a library function called has no
+ * call in any Lua code to be named after, so Lua names it by where a
+ * library keeps it, and reports the error with no place at all: the
+ * caller is not written in Lua and has no line to point at.
+ */
+ fun notecallback(le: LuaError) {
+ if (le.positioned) return
+ le.nowhere = true
+ val message: String = le.message ?: return
+ val match = Regex("^bad argument #(\\d+): ([\\s\\S]*)$").find(message) ?: return
+ val frames: CallStack = callstack()
+ if (frames.calls == 0) return
+ val name: String = globalfuncname(frames.frame!![frames.calls - 1]!!.f) ?: "?"
+ le.argMessageOverride =
+ "bad argument #" + match.groupValues[1] + " to '" + name + "' (" + match.groupValues[2] + ")"
+ }
+
+ /**
+ * The name [f] answers to in the loaded libraries, or null.
+ *
+ * A function of the library's own has no name of its own to give: what a
+ * traceback calls it is where it is kept, so `print` is "print" and
+ * `string.rep` is "string.rep". Only what a library table holds directly
+ * is looked at, which is as far as Lua looks.
+ */
+ fun globalfuncname(f: LuaValue?): String? {
+ if (f == null || globals == null) return null
+ val loaded: LuaValue = globals!!.get("package")?.get("loaded") ?: return null
+ if (!loaded.istable()) return null
+ var key: LuaValue = NIL
+ while (true) {
+ val entry: Varargs = loaded.next(key) ?: return null
+ key = entry.arg1() ?: return null
+ if (key.isnil()) return null
+ if (key.type() != LuaValue.TSTRING) continue
+ val module: LuaValue = entry.arg(2) ?: continue
+ if (module === f) return key.tojstring()
+ if (!module.istable()) continue
+ var field: LuaValue = NIL
+ while (true) {
+ val held: Varargs = module.next(field) ?: break
+ field = held.arg1() ?: break
+ if (field.isnil()) break
+ if (field.type() != LuaValue.TSTRING) continue
+ if (held.arg(2) !== f) continue
+ val name: String = key.tojstring() + "." + field.tojstring()
+ // The globals are kept under "_G", which nobody writes.
+ return if (name.startsWith("_G.")) name.substring(3) else name
+ }
+ }
+ }
+
+ /**
+ * Writes down the stack [le] was raised on, if it is worth keeping.
+ *
+ * Called where the error is still standing on it. Only a coroutine's is
+ * kept, and only the first time: the frames are popped as the error
+ * unwinds, and after that the only way to show them is from here.
+ */
+ fun notestack(le: LuaError) {
+ if (le.luastack != null) return
+ if (globals!!.running.isMainThread) return
+ le.luastack = callstack().tracebacklines(0)
+ }
+
fun getCallFrame(level: Int): CallFrame? {
return callstack().getCallFrame(level)
}
@@ -426,6 +758,10 @@ class DebugLib : TwoArgFunction() {
fun callHook(s: LuaThread.State, type: LuaValue?, arg: LuaValue?) {
if (s.inhook || s.hookfunc == null) return
s.inhook = true
+ // The hook's own frame is the one its call pushes; it is only marked
+ // as a hook here, so that code inside it counts levels from itself
+ // and reports itself as a hook rather than as an ordinary call.
+ s.hookframepending = true
try {
s.hookfunc!!.call(type, arg)
} catch (e: LuaError) {
@@ -433,19 +769,33 @@ class DebugLib : TwoArgFunction() {
} catch (e: RuntimeException) {
throw LuaError(e)
} finally {
+ s.hookframepending = false
s.inhook = false
}
}
@kotlin.jvm.JvmOverloads
fun callstack(t: LuaThread = globals!!.running): CallStack {
- if (t.callstack == null) t.callstack = net.blueva.luak.lib.DebugLib.CallStack()
+ if (t.callstack == null) {
+ val made = net.blueva.luak.lib.DebugLib.CallStack()
+ made.main = t.isMainThread
+ made.owner = this
+ t.callstack = made
+ }
return t.callstack as CallStack
}
class DebugInfo {
var name: String? = null /* (n) */
var namewhat: String? = null /* (n) 'global', 'local', 'field', 'method' */
+
+ /** (t) how many arguments a `__call` chain put in front of the real ones. */
+ var extraargs: Int = 0
+
+ /** (r) the first value being handed over, and how many there are. */
+ var ftransfer: Int = 0
+
+ var ntransfer: Int = 0
var what: String? = null /* (S) 'Lua', 'C', 'main', 'tail' */
var source: String? = null /* (S) */
var currentline: Int = 0 /* (l) */
@@ -467,19 +817,47 @@ class DebugLib : TwoArgFunction() {
this.what = if (this.linedefined == 0) "main" else "Lua"
this.short_src = p.shortsource()
} else {
- this.source = "=[Java]"
+ // Reported as Lua reports a function that is not written in
+ // Lua. Saying "Java" would be more literal but no portable
+ // script looks for it, and every one of them looks for "C".
+ this.source = "=[C]"
this.linedefined = -1
this.lastlinedefined = -1
- this.what = "Java"
- this.short_src = f.name()
+ this.what = "C"
+ // The source of a function that is not written in Lua is the
+ // runtime itself, which Lua names "[C]" whatever the host is.
+ this.short_src = "[C]"
}
}
}
class CallStack internal constructor() {
+ /** How many innermost levels a traceback writes before leaving a gap. */
+ private val LEVELS1: Int = 10
+
+ /** How many outermost levels it writes after the gap. */
+ private val LEVELS2: Int = 11
+
var frame: Array? = net.blueva.luak.lib.DebugLib.CallStack.Companion.EMPTY
var calls: Int = 0
+ /**
+ * True for the stack a program was started on.
+ *
+ * Only that one has the host below it, which is the frame a traceback
+ * ends with; a coroutine's stack ends where its body does.
+ */
+ var main: Boolean = false
+
+ /**
+ * The stack of a coroutine that died of an error, kept for a later
+ * traceback; see [LuaError.luastack].
+ */
+ var frozen: List? = null
+
+ /** The debug library this stack belongs to, for the names it knows. */
+ var owner: DebugLib? = null
+
fun currentline(): Int {
return if (calls > 0) frame!![calls - 1]!!.currentline() else -1
}
@@ -519,41 +897,102 @@ class DebugLib : TwoArgFunction() {
* @return String containing the traceback.
*/
fun traceback(level: Int): String {
- var level = level
val sb: StringBuilder = StringBuilder()
sb.append("stack traceback:")
- var c: CallFrame?
- while ((getCallFrame(level++).also { c = it }) != null) {
+ for (line in tracebacklines(level)) {
sb.append("\n\t")
- sb.append(c!!.shortsource())
+ sb.append(line)
+ }
+ return sb.toString()
+ }
+
+ /**
+ * One entry per frame from [level] down, as [traceback] writes them.
+ *
+ * A stack deeper than the two parts together is written with its
+ * middle left out, since a traceback is read by a person: the
+ * outermost frames say where the trouble is and the innermost say
+ * where it came from, and a note stands in for everything between.
+ */
+ fun tracebacklines(level: Int): List {
+ frozen?.let { return if (level <= 0) it else it.drop(level) }
+ val lines: ArrayList = ArrayList()
+ // Counted the way Lua counts it: the host below the main thread is
+ // a level of its own, and is where the last line comes from.
+ val last: Int = calls - 1 + (if (main) 1 else 0)
+ var level = level
+ var show: Int = if (last - level > LEVELS1 + LEVELS2) LEVELS1 else -1
+ while (level <= last) {
+ val here: Int = level
+ level++
+ if (show-- == 0) {
+ val skipped: Int = last - level - LEVELS2 + 1
+ lines.add("...\t(skipping " + skipped + " levels)")
+ level += skipped
+ continue
+ }
+ val c: CallFrame? = getCallFrame(here)
+ if (c == null) {
+ // Below the main thread is the host that started it, which
+ // is where a reference build shows its own C entry point.
+ lines.add("[C]: in ?")
+ continue
+ }
+ val sb: StringBuilder = StringBuilder()
+ sb.append(c.shortsource())
sb.append(':')
if (c.currentline() > 0) sb.append(c.currentline().toString() + ":")
sb.append(" in ")
+ // Named the way Lua names one, in that order: how the code
+ // reached it, then the main chunk, then where a library keeps
+ // it, then where it was written, then nothing at all.
val ar = auxgetinfo("n", c.f, c)
- if (c.linedefined() == 0) sb.append("main chunk")
- else if (ar.name != null) {
- sb.append("function '")
+ val namewhat: String = ar.namewhat.orEmpty()
+ val known: String? = owner?.globalfuncname(c.f)
+ if (ar.name != null && namewhat.isNotEmpty()) {
+ sb.append(namewhat)
+ sb.append(" '")
sb.append(ar.name)
sb.append('\'')
- } else {
+ } else if (c.f!!.isclosure() && c.linedefined() == 0) {
+ sb.append("main chunk")
+ } else if (known != null) {
+ sb.append("function '")
+ sb.append(known)
+ sb.append('\'')
+ } else if (c.f!!.isclosure()) {
sb.append("function <")
sb.append(c.shortsource())
sb.append(':')
sb.append(c.linedefined())
sb.append('>')
+ } else {
+ sb.append('?')
}
+ lines.add(sb.toString())
+ // A tail call left no frame of its own behind, and the gap it
+ // leaves is where Lua says so.
+ if (c.istailcall) lines.add("(...tail calls...)")
}
- sb.append("\n\t[Java]: in ?")
- return sb.toString()
+ return lines
}
fun getCallFrame(level: Int): CallFrame? {
- if (level < 1 || level > calls) return null
- return frame!![calls - level]
+ // Level 0 is the function asking, as it is in Lua: every library
+ // function has a frame of its own, so the one that called
+ // debug.getinfo is one step further down.
+ if (level < 0 || level >= calls) return null
+ return frame!![calls - 1 - level]
}
fun findCallFrame(func: LuaValue?): CallFrame? {
- for (i in 1..calls) if (frame!![calls - i]!!.f === func) return frame!![i]
+ // Innermost first, and the frame that matched is the one to hand
+ // back: returning frame[i] instead was reaching a different one,
+ // sometimes an unused slot with no function in it at all.
+ for (i in 1..calls) {
+ val candidate: CallFrame = frame!![calls - i]!!
+ if (candidate.f === func) return candidate
+ }
return null
}
@@ -572,15 +1011,34 @@ class DebugLib : TwoArgFunction() {
ar.nparams = p.numparams.toShort()
ar.isvararg = p.is_vararg !== 0
} else {
- ar.nups = 0
+ // A function of the library's own takes whatever it is
+ // given and carries whatever state it was built with.
+ ar.nups = ((f as? LuaFunction)?.nupvalues() ?: 0).toShort()
ar.isvararg = true
ar.nparams = 0
}
- 't' -> ar.istailcall = false
+ 't' -> {
+ ar.istailcall = ci?.istailcall ?: false
+ ar.extraargs = ci?.extraargs ?: 0
+ }
+
+ 'r' -> {
+ ar.ftransfer = ci?.ftransfer ?: 0
+ ar.ntransfer = ci?.ntransfer ?: 0
+ }
'n' -> {
- /* calling function is a known Lua function? */
- if (ci != null && ci.previous != null) {
+ // A hook was not called from any instruction, so there
+ // is no call site to read a name from.
+ if (ci != null && ci.hooked) {
+ ar.name = "?"
+ ar.namewhat = "hook"
+ } else if (ci != null && ci.finalizer) {
+ // The collector called it, and Lua names it after
+ // the metamethod that put it there.
+ ar.name = "__gc"
+ ar.namewhat = "metamethod"
+ } else if (ci != null && ci.previous != null) {
if (ci.previous!!.f!!.isclosure()) {
val nw: NameWhat? = net.blueva.luak.lib.DebugLib.Companion.getfuncname(ci.previous!!)
if (nw != null) {
@@ -611,6 +1069,52 @@ class DebugLib : TwoArgFunction() {
class CallFrame {
var f: LuaFunction? = null
var pc: Int = 0
+
+ /**
+ * Where this frame was one instruction ago, upstream's `oldpc`.
+ *
+ * The line hook fires when execution reaches a line other than the one
+ * this points at, or jumps backwards, which is how a loop body on a
+ * single line still reports every pass.
+ */
+ var oldpc: Int = 0
+
+ /**
+ * Every argument the call was given, when the debug library is
+ * watching a vararg function, in storage `debug.setlocal` can write
+ * through to. Null for every other call.
+ */
+ var args: Array? = null
+
+ /** True when this frame is a hook the runtime called, not a Lua call. */
+ var hooked: Boolean = false
+
+ /**
+ * True for the frame of a `__gc` handler, which no instruction called
+ * and which therefore has no call site to be named from.
+ */
+ var finalizer: Boolean = false
+
+ /** True when a tail call made this frame, taking its caller's place. */
+ var istailcall: Boolean = false
+
+ /** How many `__call` handlers put a value in front of the real arguments. */
+ var extraargs: Int = 0
+
+ /**
+ * The first of the values being handed over, and how many there are.
+ *
+ * At a call that is the arguments, at a return the results; it is what
+ * `debug.getinfo(f, "r")` reports so a hook can read them back out
+ * with `debug.getlocal`.
+ */
+ var ftransfer: Int = 0
+
+ var ntransfer: Int = 0
+
+ /** The values being handed back, once a call has produced them. */
+ var results: Array? = null
+
var top: Int = 0
var v: Varargs? = null
var stack: Array? = null
@@ -622,7 +1126,7 @@ class DebugLib : TwoArgFunction() {
}
fun shortsource(): String? {
- return if (f!!.isclosure()) f!!.checkclosure()!!.p.shortsource() else "[Java]"
+ return if (f!!.isclosure()) f!!.checkclosure()!!.p.shortsource() else "[C]"
}
fun set(function: LuaFunction?) {
@@ -633,25 +1137,92 @@ class DebugLib : TwoArgFunction() {
this.f = null
this.v = null
this.stack = null
+ this.pc = 0
+ this.oldpc = 0
+ this.args = null
+ this.hooked = false
+ this.finalizer = false
+ this.istailcall = false
+ this.extraargs = 0
+ this.ftransfer = 0
+ this.ntransfer = 0
+ this.results = null
+ }
+
+ /** Everything [restore] needs to put this frame back as it is now. */
+ internal fun snapshot(): Array = arrayOf(f, pc, top, v, stack, oldpc, args)
+
+ /** Puts back a frame that something else was allowed to overwrite. */
+ @Suppress("UNCHECKED_CAST")
+ internal fun restore(saved: Array) {
+ f = saved[0] as LuaFunction?
+ pc = saved[1] as Int
+ top = saved[2] as Int
+ v = saved[3] as Varargs?
+ stack = saved[4] as Array?
+ oldpc = saved[5] as Int
+ args = saved[6] as Array?
}
fun instr(pc: Int, v: Varargs?, top: Int) {
+ this.oldpc = this.pc
this.pc = pc
this.v = v
this.top = top
if (net.blueva.luak.lib.DebugLib.Companion.TRACE) Print.printState((f!!.checkclosure())!!, pc, stack!!, top, v)
}
+ /** The slot a negative local index names, or -1 if there is none. */
+ private fun extraArg(i: Int): Int {
+ val values: Array = args ?: return -1
+ // The declared parameters are already on the stack by the time a
+ // frame is pushed, so what is kept here is the extras alone and
+ // -1 names the first of them.
+ val slot: Int = -i - 1
+ return if (slot in values.indices) slot else -1
+ }
+
fun getLocal(i: Int): Varargs {
+ // Once a call has produced its results they are what the indices in
+ // the transfer range name, which is what a return hook reads.
+ val produced: Array? = results
+ if (produced != null && i >= ftransfer && i < ftransfer + ntransfer) {
+ return varargsOf(valueOf("(temporary)"), produced[i - ftransfer] ?: NIL)!!
+ }
+ // A function of the library's own has no registers, only the
+ // arguments it was handed, which is what Lua reports for one.
+ if (f?.isclosure() != true) {
+ val given: Array = args ?: return NIL!!
+ if (i < 1 || i > given.size) return NIL!!
+ return varargsOf(valueOf("(C temporary)"), given[i - 1] ?: NIL)!!
+ }
+ if (i < 0) {
+ val slot: Int = extraArg(i)
+ if (slot < 0) return NIL!!
+ return varargsOf(valueOf("(vararg)"), args!![slot] ?: NIL)!!
+ }
val name: LuaString? = getlocalname(i)
- if (i >= 1 && i <= stack!!.size && stack!![i - 1] != null) return varargsOf(
- if (name == null) NIL else name,
- stack!![i - 1]!!
- )!!
- else return NIL!!
+ if (i >= 1 && i <= livelimit() && stack!![i - 1] != null) {
+ // A register the function is using but has not named yet holds
+ // a temporary, which is what Lua calls it.
+ return varargsOf(name ?: valueOf("(temporary)"), stack!![i - 1]!!)!!
+ }
+ return NIL!!
}
fun setLocal(i: Int, value: LuaValue?): Varargs? {
+ if (f?.isclosure() != true) {
+ val given: Array = args ?: return NIL
+ if (i < 1 || i > given.size) return NIL
+ given[i - 1] = value
+ return valueOf("(C temporary)")
+ }
+ if (i < 0) {
+ val slot: Int = extraArg(i)
+ if (slot < 0) return NIL
+ args!![slot] = value
+ return valueOf("(vararg)")
+ }
val name: LuaString? = getlocalname(i)
if (i >= 1 && i <= stack!!.size && stack!![i - 1] != null) {
stack!![i - 1] = value
@@ -661,6 +1232,43 @@ class DebugLib : TwoArgFunction() {
}
}
+ /**
+ * How many registers of this frame hold something worth looking at.
+ *
+ * While a call is under way the function being called sits just past
+ * them, and what is beyond that belongs to the call, not to this
+ * frame.
+ */
+ private fun livelimit(): Int {
+ val room: Int = stack?.size ?: 0
+ val closure: LuaClosure = f?.checkclosure() ?: return room
+ val code: IntArray = closure.p.code ?: return room
+ if (pc < 0 || pc >= code.size) return room
+ return when (Lua.GET_OPCODE(code[pc])) {
+ Lua.OP_CALL, Lua.OP_TAILCALL -> Lua.GETARG_A(code[pc])
+ else -> room
+ }
+ }
+
+ /**
+ * True when the line hook should fire for the instruction about to run.
+ *
+ * That is when it sits on a different line from the one before it, or
+ * when the jump went backwards: a loop written on one line still
+ * reports each pass that way.
+ */
+ internal fun reachedNewLine(): Boolean {
+ if (!f!!.isclosure()) return false
+ // Asked before the line numbers are, since it is also true of the
+ // first instruction of a call: a chunk loaded without debug
+ // information has no lines to change, and this is the one report
+ // it still makes.
+ if (pc <= oldpc) return true
+ val li: IntArray = f!!.checkclosure()!!.p.lineinfo ?: return false
+ if (pc < 0 || pc >= li.size) return false
+ return oldpc < 0 || oldpc >= li.size || li[pc] != li[oldpc]
+ }
+
fun currentline(): Int {
if (!f!!.isclosure()) return -1
val li: IntArray? = f!!.checkclosure()!!.p.lineinfo
@@ -702,6 +1310,7 @@ class DebugLib : TwoArgFunction() {
val LUA: LuaString? = valueOf("Lua")
private val QMARK: LuaString? = valueOf("?")
private val CALL: LuaString? = valueOf("call")
+ private val TAILCALL: LuaString? = valueOf("tail call")
private val LINE: LuaString? = valueOf("line")
private val COUNT: LuaString? = valueOf("count")
private val RETURN: LuaString? = valueOf("return")
@@ -713,6 +1322,13 @@ class DebugLib : TwoArgFunction() {
val NPARAMS: LuaString? = valueOf("nparams")
val NAME: LuaString? = valueOf("name")
val NAMEWHAT: LuaString? = valueOf("namewhat")
+ val EXTRAARGS: LuaString? = valueOf("extraargs")
+
+ val FTRANSFER: LuaString? = valueOf("ftransfer")
+ val NTRANSFER: LuaString? = valueOf("ntransfer")
+
+ /** Where the registry keeps the hook functions, as Lua names it. */
+ val HOOKKEY: LuaString? = valueOf("_HOOKKEY")
val WHAT: LuaString? = valueOf("what")
val SOURCE: LuaString? = valueOf("source")
val SHORT_SRC: LuaString? = valueOf("short_src")
@@ -723,12 +1339,20 @@ class DebugLib : TwoArgFunction() {
fun findupvalue(c: LuaClosure, up: Int): LuaString? {
if (c.upValues != null && up > 0 && up <= c.upValues.size) {
- if (c.p.upvalues != null && up <= c.p.upvalues!!.size) return c.p.upvalues!![up - 1]!!.name
+ if (c.p.upvalues != null && up <= c.p.upvalues!!.size) {
+ // A chunk loaded without debug information still has its
+ // upvalues, it just cannot say what they were called, and
+ // Lua answers that in so many words.
+ return c.p.upvalues!![up - 1]!!.name ?: NO_NAME
+ }
else return LuaString.valueOf("." + up)
}
return null
}
+ /** What an upvalue is called where the name was stripped out. */
+ private val NO_NAME: LuaString = LuaString.valueOf("(no name)")
+
fun lua_assert(x: Boolean) {
if (!x) throw RuntimeException("lua_assert failed")
}
@@ -747,7 +1371,9 @@ class DebugLib : TwoArgFunction() {
Lua.GETARG_A(i)
)
- Lua.OP_TFORCALL -> return net.blueva.luak.lib.DebugLib.NameWhat("(for iterator)", "(for iterator")
+ // Both halves read the same, which is how Lua names the
+ // function a generic `for` is stepping.
+ Lua.OP_TFORCALL -> return net.blueva.luak.lib.DebugLib.NameWhat("for iterator", "for iterator")
Lua.OP_SELF, Lua.OP_GETTABUP, Lua.OP_GETTABLE -> tm = LuaValue.INDEX
Lua.OP_SETTABUP, Lua.OP_SETTABLE -> tm = LuaValue.NEWINDEX
Lua.OP_EQ -> tm = LuaValue.EQ
@@ -755,6 +1381,13 @@ class DebugLib : TwoArgFunction() {
Lua.OP_SUB -> tm = LuaValue.SUB
Lua.OP_MUL -> tm = LuaValue.MUL
Lua.OP_DIV -> tm = LuaValue.DIV
+ Lua.OP_IDIV -> tm = LuaValue.IDIV
+ Lua.OP_BAND -> tm = LuaValue.BAND
+ Lua.OP_BOR -> tm = LuaValue.BOR
+ Lua.OP_BXOR -> tm = LuaValue.BXOR
+ Lua.OP_SHL -> tm = LuaValue.SHL
+ Lua.OP_SHR -> tm = LuaValue.SHR
+ Lua.OP_BNOT -> tm = LuaValue.BNOT
Lua.OP_MOD -> tm = LuaValue.MOD
Lua.OP_POW -> tm = LuaValue.POW
Lua.OP_UNM -> tm = LuaValue.UNM
@@ -762,9 +1395,14 @@ class DebugLib : TwoArgFunction() {
Lua.OP_LT -> tm = LuaValue.LT
Lua.OP_LE -> tm = LuaValue.LE
Lua.OP_CONCAT -> tm = LuaValue.CONCAT
+ // Leaving a block or a function is where a to-be-closed
+ // variable's handler runs, so a frame reached from there is
+ // that handler.
+ Lua.OP_JMP, Lua.OP_RETURN -> tm = LuaValue.CLOSE
else -> return null /* else no useful name can be found */
}
- return net.blueva.luak.lib.DebugLib.NameWhat(tm.tojstring(), "metamethod")
+ // The metatag is spelled "__close"; the name reported is "close".
+ return net.blueva.luak.lib.DebugLib.NameWhat(tm.tojstring().substring(2), "metamethod")
}
// return NameWhat if found, null if not
@@ -903,3 +1541,26 @@ class DebugLib : TwoArgFunction() {
}
}
}
+
+/**
+ * Calls [f] as a library function calls back into another function.
+ *
+ * A function of the library's own has no frame of its own to push, so one is
+ * pushed for it here: without that a traceback would not name it, the levels
+ * a hook counts would skip it, and an error it raises could not say which
+ * function it was in. See [DebugLib.notecallback].
+ */
+internal fun callback(debuglib: DebugLib?, f: LuaValue, args: Varargs): Varargs {
+ if (debuglib == null || f !is net.blueva.luak.LuaFunction || f is net.blueva.luak.LuaClosure) {
+ return f.invoke(args)!!
+ }
+ debuglib.onCall(f)
+ try {
+ return f.invoke(args)!!
+ } catch (le: LuaError) {
+ debuglib.notecallback(le)
+ throw le
+ } finally {
+ debuglib.onReturn()
+ }
+}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/IoLib.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/IoLib.kt
index b44654fa..986e9661 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/IoLib.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/IoLib.kt
@@ -126,6 +126,16 @@ open class IoLib : TwoArgFunction() {
return filemethods!!.get(key)
}
+ /**
+ * The table of file methods, which doubles as the handle's metatable.
+ *
+ * A script reads the type's name back from `getmetatable(f).__name`,
+ * and that is where Lua keeps it.
+ */
+ override fun getmetatable(): LuaValue? {
+ return filemethods
+ }
+
// essentially a userdata instance
override fun type(): Int {
return LuaValue.TUSERDATA
@@ -135,9 +145,24 @@ open class IoLib : TwoArgFunction() {
return "userdata"
}
- // displays as "file" type
+ /**
+ * How a file handle prints, which says whether it is still open.
+ *
+ * A closed one has no identity worth showing, so Lua prints the word
+ * instead of an address.
+ */
override fun tojstring(): String {
- return "file: " + hashCode().toString(16)
+ return if (isclosed()) "file (closed)" else "file (0x" + hashCode().toString(16) + ")"
+ }
+
+ /**
+ * The handle's own rendering, so `tostring` uses it.
+ *
+ * Without this the generic conversion would fall back to the type's
+ * `__name` and print "FILE*: file (...)".
+ */
+ override fun tostring(): LuaValue {
+ return valueOf(tojstring())
}
fun finalize() {
@@ -200,7 +225,13 @@ open class IoLib : TwoArgFunction() {
appendMode -> if (updateMode) PlatformFileMode.READ_APPEND else PlatformFileMode.APPEND
else -> if (updateMode) PlatformFileMode.READ_WRITE_TRUNCATE else PlatformFileMode.WRITE
}
- return HostFile(platformOpenFile(path, mode), path, deleteOnClose = false)
+ return HostFile(
+ platformOpenFile(path, mode),
+ path,
+ deleteOnClose = false,
+ readable = readMode || updateMode,
+ writable = !readMode || updateMode,
+ )
}
/**
@@ -211,7 +242,13 @@ open class IoLib : TwoArgFunction() {
@kotlin.Throws(IOException::class)
protected open fun tmpFile(): File? {
val path: String = platformTempFilePath()
- return HostFile(platformOpenFile(path, PlatformFileMode.READ_WRITE_TRUNCATE), path, deleteOnClose = true)
+ return HostFile(
+ platformOpenFile(path, PlatformFileMode.READ_WRITE_TRUNCATE),
+ path,
+ deleteOnClose = true,
+ readable = true,
+ writable = true,
+ )
}
/**
@@ -234,19 +271,27 @@ open class IoLib : TwoArgFunction() {
private val handle: PlatformFileHandle,
private val path: String,
private val deleteOnClose: Boolean,
+ /** Whether the mode it was opened in allows reading. */
+ private val readable: Boolean,
+ /** Whether the mode it was opened in allows writing. */
+ private val writable: Boolean,
) : File() {
private var closed = false
private var nobuffer = false
// Identity, not the path: two handles on the same file are two distinct
// Lua values and must not share a tostring().
- override fun tojstring(): String = "file (" + (if (closed) "closed" else hashCode().toString()) + ")"
+ override fun tojstring(): String =
+ "file (" + (if (closed) "closed" else "0x" + hashCode().toString(16)) + ")"
override fun isstdfile(): Boolean = false
override fun isclosed(): Boolean = closed
@kotlin.Throws(IOException::class)
override fun write(string: LuaString?) {
+ // What the host reports for the wrong end of a handle, which is
+ // the failure the caller is told about.
+ if (!writable) throw IOException(BAD_DESCRIPTOR)
val s: LuaString = string ?: return
handle.write(s.m_bytes, s.m_offset, s.m_length)
if (nobuffer) flush()
@@ -297,12 +342,14 @@ open class IoLib : TwoArgFunction() {
@kotlin.Throws(IOException::class, EOFException::class)
override fun read(): Int {
+ if (!readable) throw IOException(BAD_DESCRIPTOR)
val byte = ByteArray(1)
return if (handle.read(byte, 0, 1) < 0) -1 else byte[0].toInt() and 0xff
}
@kotlin.Throws(IOException::class)
override fun read(bytes: ByteArray?, offset: Int, length: Int): Int {
+ if (!readable) throw IOException(BAD_DESCRIPTOR)
val target: ByteArray = bytes ?: return -1
return handle.read(target, offset, length)
}
@@ -310,7 +357,7 @@ open class IoLib : TwoArgFunction() {
/** `io.stdout` / `io.stderr`, writing through the [Globals] streams. */
private inner class StandardOutputFile(private val fileType: Int) : File() {
- override fun tojstring(): String = "file (" + hashCode().toString() + ")"
+ override fun tojstring(): String = "file (0x" + hashCode().toString(16) + ")"
private fun stream() = if (fileType == FTYPE_STDERR) globals?.STDERR else globals?.STDOUT
@@ -335,7 +382,15 @@ open class IoLib : TwoArgFunction() {
override fun isclosed(): Boolean = false
@kotlin.Throws(IOException::class)
- override fun seek(option: String?, bytecount: Int): Int = 0
+ /**
+ * Always fails: a standard stream has no position to move.
+ *
+ * The exception becomes the `nil, message, code` an io function
+ * answers a failure with.
+ */
+ override fun seek(option: String?, bytecount: Int): Int {
+ throw IOException("Illegal seek")
+ }
override fun setvbuf(mode: String?, size: Int) = Unit
@@ -359,7 +414,7 @@ open class IoLib : TwoArgFunction() {
private inner class StandardInputFile : File() {
private var pushback = -1
- override fun tojstring(): String = "file (" + hashCode().toString() + ")"
+ override fun tojstring(): String = "file (0x" + hashCode().toString(16) + ")"
private fun stream(): InputStream? = globals?.STDIN ?: platformStandardInput()
@@ -379,7 +434,15 @@ open class IoLib : TwoArgFunction() {
override fun isclosed(): Boolean = false
@kotlin.Throws(IOException::class)
- override fun seek(option: String?, bytecount: Int): Int = 0
+ /**
+ * Always fails: a standard stream has no position to move.
+ *
+ * The exception becomes the `nil, message, code` an io function
+ * answers a failure with.
+ */
+ override fun seek(option: String?, bytecount: Int): Int {
+ throw IOException("Illegal seek")
+ }
override fun setvbuf(mode: String?, size: Int) = Unit
@@ -459,10 +522,31 @@ open class IoLib : TwoArgFunction() {
// all functions link to library instance
setLibInstance(t)
setLibInstance((filemethods)!!)
+ // Lua names the file handle type, which is what a script reads back
+ // from getmetatable(f).__name. Set after the binding pass, which walks
+ // the table expecting every value to be one of the library functions.
+ filemethods!!.set("__name", "FILE*")
+ // A file handle is closable, so `local f = io.open(...)` closes
+ // it on the way out of the block whichever way the block is left. A
+ // handle that was closed by hand first is left alone rather than
+ // complained about, which is what lets both forms be written together.
+ val closer = closehandle()
+ filemethods!!.set("__close", closer)
+ // Lua's file metatable names the same function under __gc, since that
+ // is what would close the handle if a collector ran finalizers. This
+ // runtime never does - for a file or for anything else - so the field
+ // says what closing means here rather than promising it will happen.
+ filemethods!!.set("__gc", closer)
setLibInstance(mt)
// return the table
+ // The three standard streams, which Lua exposes as ready-made file
+ // handles rather than only through io.read and io.write.
+ t.set("stdin", ioopenfile(net.blueva.luak.lib.IoLib.Companion.FTYPE_STDIN, "-", "r")!!)
+ t.set("stdout", ioopenfile(net.blueva.luak.lib.IoLib.Companion.FTYPE_STDOUT, "-", "w")!!)
+ t.set("stderr", ioopenfile(net.blueva.luak.lib.IoLib.Companion.FTYPE_STDERR, "-", "w")!!)
+
env!!.set("io", t)
if (!env!!.get("package")!!.isnil()) env!!.get("package")!!.get("loaded")!!.set("io", t)
return t
@@ -575,7 +659,7 @@ open class IoLib : TwoArgFunction() {
// io.flush() -> bool
@kotlin.Throws(IOException::class)
fun _io_flush(): Varargs {
- net.blueva.luak.lib.IoLib.Companion.checkopen(output())
+ net.blueva.luak.lib.IoLib.Companion.checkdefault(output(), "output")
outfile!!.flush()
return (LuaValue.TRUE)!!
}
@@ -638,6 +722,13 @@ open class IoLib : TwoArgFunction() {
// io.lines(filename, ...) -> iterator
fun _io_lines(args: Varargs): Varargs? {
+ // Every format is held on the stack while the iterator runs, so Lua
+ // puts a ceiling on how many there may be.
+ args.argcheck(
+ args.narg() - 1 <= net.blueva.luak.lib.IoLib.Companion.MAX_LINE_FORMATS,
+ net.blueva.luak.lib.IoLib.Companion.MAX_LINE_FORMATS + 2,
+ "too many arguments",
+ )
val filename: String? = args.optjstring(1, null)
val infile = if (filename == null) input() else ioopenfile(
net.blueva.luak.lib.IoLib.Companion.FTYPE_NAMED,
@@ -651,14 +742,14 @@ open class IoLib : TwoArgFunction() {
// io.read(...) -> (...)
@kotlin.Throws(IOException::class)
fun _io_read(args: Varargs): Varargs {
- net.blueva.luak.lib.IoLib.Companion.checkopen((input())!!)
+ net.blueva.luak.lib.IoLib.Companion.checkdefault((input())!!, "input")
return ioread(infile!!, args)
}
// io.write(...) -> void
@kotlin.Throws(IOException::class)
fun _io_write(args: Varargs): Varargs {
- net.blueva.luak.lib.IoLib.Companion.checkopen(output())
+ net.blueva.luak.lib.IoLib.Companion.checkdefault(output(), "output")
return net.blueva.luak.lib.IoLib.Companion.iowrite((outfile)!!, args)
}
@@ -668,6 +759,28 @@ open class IoLib : TwoArgFunction() {
return net.blueva.luak.lib.IoLib.Companion.ioclose(net.blueva.luak.lib.IoLib.Companion.checkfile(file))
}
+ /**
+ * `__close` for a file handle, upstream's `f_gc`.
+ *
+ * Unlike `file:close()` this says nothing about a handle that is already
+ * closed: leaving the block is not a request to close it a second time.
+ */
+ internal class closehandle : VarArgFunction() {
+ override fun invoke(args: Varargs): Varargs {
+ val handle: LuaValue? = args.arg1()
+ val file: File? = net.blueva.luak.lib.IoLib.Companion.optfile(handle)
+ // It still has to be given a handle; what it tolerates is one that
+ // has already been closed.
+ if (file == null) {
+ val got: String =
+ if (handle == null || handle.isnil()) "no value" else handle.argtypename()
+ LuaValue.argerror(1, "FILE* expected, got " + got)
+ }
+ if (file!!.isclosed()) return (LuaValue.TRUE)!!
+ return net.blueva.luak.lib.IoLib.Companion.ioclose(file)
+ }
+ }
+
// file:flush() -> void
@kotlin.Throws(IOException::class)
fun _file_flush(file: LuaValue?): Varargs {
@@ -761,8 +874,8 @@ open class IoLib : TwoArgFunction() {
}
private fun lines(f: File?, toclose: Boolean, args: Varargs): Varargs? {
- try {
- return net.blueva.luak.lib.IoLib.IoLibV(
+ val iterator: LuaValue = try {
+ net.blueva.luak.lib.IoLib.IoLibV(
f,
"lnext",
net.blueva.luak.lib.IoLib.Companion.LINES_ITER,
@@ -773,6 +886,11 @@ open class IoLib : TwoArgFunction() {
} catch (e: Exception) {
return error("lines: " + e)
}
+ // A file this call opened is handed back as the loop's fourth value,
+ // so the generic for closes it however the loop is left. A file the
+ // caller already had is left alone.
+ if (!toclose) return iterator
+ return varargsOf(arrayOf(iterator, NIL, NIL, f))
}
@kotlin.Throws(IOException::class)
@@ -795,8 +913,12 @@ open class IoLib : TwoArgFunction() {
LuaValue.TSTRING -> {
fmt = ai!!.checkstring()!!
- if (fmt.m_length >= 2 && fmt.m_bytes[fmt.m_offset] == '*'.code.toByte()) {
- when (fmt.m_bytes[fmt.m_offset + 1]) {
+ // Since 5.3 the leading '*' is optional, so "n" and "*n"
+ // name the same format.
+ val star: Int =
+ if (fmt.m_length >= 1 && fmt.m_bytes[fmt.m_offset] == '*'.code.toByte()) 1 else 0
+ if (fmt.m_length >= star + 1) {
+ when (fmt.m_bytes[fmt.m_offset + star]) {
'n'.code.toByte() -> {
vi = net.blueva.luak.lib.IoLib.Companion.freadnumber(f)
return@item
@@ -884,6 +1006,15 @@ open class IoLib : TwoArgFunction() {
private val STDOUT: LuaValue? = valueOf("stdout")
private val STDERR: LuaValue? = valueOf("stderr")
private val FILE: LuaValue? = valueOf("file")
+
+ /** C's ENOENT: no such file or directory. */
+ private const val ENOENT: Int = 2
+
+ /** C's EBADF: the handle is not open for what was asked of it. */
+ internal const val EBADF: Int = 9
+
+ /** What the host says when a handle is used the wrong way round. */
+ internal const val BAD_DESCRIPTOR: String = "Bad file descriptor"
private val CLOSED_FILE: LuaValue? = valueOf("closed file")
private const val IO_CLOSE = 0
@@ -934,7 +1065,7 @@ open class IoLib : TwoArgFunction() {
)
@kotlin.Throws(IOException::class)
- private fun ioclose(f: File): Varargs {
+ internal fun ioclose(f: File): Varargs {
if (f.isstdfile()) return net.blueva.luak.lib.IoLib.Companion.errorresult("cannot close standard file")
else {
f.close()
@@ -948,11 +1079,23 @@ open class IoLib : TwoArgFunction() {
fun errorresult(ioe: Exception): Varargs {
val s: String? = ioe.message
- return net.blueva.luak.lib.IoLib.Companion.errorresult("io error: " + (if (s != null) s else ioe.toString()))
+ // nil, message, errno - the shape every io function answers with,
+ // so a caller can branch on the number without parsing the text.
+ return net.blueva.luak.lib.IoLib.Companion.errorresult(
+ if (s != null) s else ioe.toString(),
+ )
}
+ /**
+ * The `nil, message, errno` an io function answers a failure with.
+ *
+ * There is no errno to read from a host exception here, so the number
+ * is the one C uses for a file that is not there - which is what a
+ * caller branching on it is almost always looking for.
+ */
private fun errorresult(errortext: String?): Varargs {
- return (varargsOf(NIL, valueOf(errortext)))!!
+ val errno: Int = if (errortext == BAD_DESCRIPTOR) EBADF else ENOENT
+ return (varargsOf(NIL, valueOf(errortext), valueOf(errno)))!!
}
@kotlin.Throws(IOException::class)
@@ -968,12 +1111,20 @@ open class IoLib : TwoArgFunction() {
private fun checkfile(`val`: LuaValue?): File {
val f: File? = net.blueva.luak.lib.IoLib.Companion.optfile(`val`)
- if (f == null) argerror(1, "file")
+ // Worded the way Lua words it, including what was there instead,
+ // since calling a file method with no self is the usual mistake.
+ if (f == null) {
+ // The name the value's own metatable gives it, if it has one,
+ // so a script that passed the wrong handle sees which.
+ val got: String =
+ if (`val` == null || `val`.isnil()) "no value" else `val`.argtypename()
+ argerror(1, "FILE* expected, got " + got)
+ }
net.blueva.luak.lib.IoLib.Companion.checkopen((f)!!)
return f!!
}
- private fun optfile(`val`: LuaValue?): File? {
+ internal fun optfile(`val`: LuaValue?): File? {
return if (`val` is File) `val` as File? else null
}
@@ -982,6 +1133,17 @@ open class IoLib : TwoArgFunction() {
return file
}
+ /**
+ * The default input or output file, refused if it has been closed.
+ *
+ * Named in the message, since a script that closed `io.input()` needs
+ * to know which of the two defaults it is being told about.
+ */
+ private fun checkdefault(file: File, which: String): File {
+ if (file.isclosed()) error("default " + which + " file is closed")
+ return file
+ }
+
// ------------- file reading utilitied ------------------
@kotlin.Throws(IOException::class)
fun freadbytes(f: File, count: Int): LuaValue {
@@ -1033,35 +1195,93 @@ open class IoLib : TwoArgFunction() {
}
}
+ /**
+ * `io.read("n")`: the next numeral in the stream, or nil.
+ *
+ * The numeral is assembled one piece at a time, the way upstream's
+ * `read_number` does, so no more of the stream is consumed than the
+ * numeral itself. Hexadecimal numerals and exponents are read too, and
+ * the result keeps its subtype: `12345` comes back as an integer, not
+ * as the float a single decimal conversion would give.
+ */
@kotlin.Throws(IOException::class)
fun freadnumber(f: File): LuaValue {
val baos: ByteArrayOutputStream = ByteArrayOutputStream()
+ var length = 0
+ var overflowed = false
+
+ /** Consumes one character out of [chars], if the next one is in it. */
+ fun one(chars: String): Boolean {
+ if (overflowed) return false
+ val c: Int = f.peek()
+ if (c < 0 || chars.indexOf(c.toChar()) < 0) return false
+ // Once the numeral is as long as Lua allows the read stops
+ // here, leaving the rest of it in the stream for whoever reads
+ // next, and the result is refused.
+ if (length >= net.blueva.luak.lib.IoLib.Companion.MAX_NUMERAL_LENGTH) {
+ overflowed = true
+ return false
+ }
+ f.read()
+ baos.write(c)
+ length++
+ return true
+ }
+
+ fun many(chars: String): Int {
+ var count = 0
+ while (one(chars)) count++
+ return count
+ }
+
net.blueva.luak.lib.IoLib.Companion.freadchars(f, " \t\r\n", null)
- net.blueva.luak.lib.IoLib.Companion.freadchars(f, "-+", baos)
- //freadchars(f,"0",baos);
- //freadchars(f,"xX",baos);
- net.blueva.luak.lib.IoLib.Companion.freadchars(f, "0123456789", baos)
- net.blueva.luak.lib.IoLib.Companion.freadchars(f, ".", baos)
- net.blueva.luak.lib.IoLib.Companion.freadchars(f, "0123456789", baos)
- //freadchars(f,"eEfFgG",baos);
- // freadchars(f,"+-",baos);
- //freadchars(f,"0123456789",baos);
+ one("-+")
+ var hexadecimal = false
+ var digits = 0
+ if (one("0")) {
+ if (one("xX")) hexadecimal = true else digits = 1
+ }
+ val digitChars = if (hexadecimal) "0123456789abcdefABCDEF" else "0123456789"
+ digits += many(digitChars)
+ if (one(".")) digits += many(digitChars)
+ if (digits > 0 && one(if (hexadecimal) "pP" else "eE")) {
+ one("-+")
+ many("0123456789")
+ }
+ if (overflowed) return NIL
// decodeToString(), not toString(): only the JVM's
// ByteArrayOutputStream renders its own bytes as text.
val s: String = baos.toByteArray().decodeToString()
- return if (s.length > 0) valueOf((s).toDouble()) else NIL
+ return net.blueva.luak.NumberParser.parse(s) ?: NIL
}
+ /** As long as a numeral read from a file may be, upstream's `L_MAXLENNUM`. */
+ private const val MAX_NUMERAL_LENGTH = 200
+
+ /** As many formats as `io.lines` takes, upstream's `MAXARGLINE`. */
+ internal const val MAX_LINE_FORMATS = 250
+
+ /** Consumes one character out of [chars], if the next one is in it. */
@kotlin.Throws(IOException::class)
- private fun freadchars(f: File, chars: String, baos: ByteArrayOutputStream?) {
+ private fun freadone(f: File, chars: String, baos: ByteArrayOutputStream?): Boolean {
+ val c: Int = f.peek()
+ if (c < 0 || chars.indexOf(c.toChar()) < 0) return false
+ f.read()
+ baos?.write(c)
+ return true
+ }
+
+ private fun freadchars(f: File, chars: String, baos: ByteArrayOutputStream?): Int {
+ var count = 0
var c: Int
while (true) {
c = f.peek()
- if (chars.indexOf(c.toChar()) < 0) {
- return
+ if (c < 0 || chars.indexOf(c.toChar()) < 0) {
+ return count
}
f.read()
if (baos != null) baos.write(c)
+ count++
}
}
}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/LuaPlatform.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/LuaPlatform.kt
index 5c770aba..94909b29 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/LuaPlatform.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/LuaPlatform.kt
@@ -21,7 +21,7 @@ import net.blueva.luak.compiler.LuaC
/**
* Builds a ready-to-use [Globals] on any Kotlin Multiplatform target.
*
- * This is the entry point to reach for first: it loads the Lua 5.2 standard
+ * This is the entry point to reach for first: it loads the Lua 5.5 standard
* libraries in the right order and installs both the source compiler and the
* binary-chunk undumper, so `load`, `loadfile`, `require`, and `string.dump`
* round-trips all work out of the box.
@@ -48,10 +48,15 @@ import net.blueva.luak.compiler.LuaC
*/
object LuaPlatform {
/**
- * Creates a [Globals] with the Lua 5.2 standard libraries: `base`,
- * `package`, `bit32`, `table`, `string`, `coroutine`, `math`, `io`, and
+ * Creates a [Globals] with the Lua 5.5 standard libraries: `base`,
+ * `package`, `table`, `string`, `coroutine`, `math`, `utf8`, `io`, and
* `os`, plus the [LuaC] compiler and the [LoadState] undumper.
*
+ * `bit32` is not among them: it was deprecated in 5.3 and removed in 5.4,
+ * having no purpose once integers are 64 bits wide and the operators are
+ * built into the language. [Bit32Lib] is still there for an embedder that
+ * wants to load it back.
+ *
* @return globals initialized with the standard libraries
* @see debugGlobals
*/
@@ -59,11 +64,11 @@ object LuaPlatform {
val globals = Globals()
globals.load(BaseLib())
globals.load(PackageLib())
- globals.load(Bit32Lib())
globals.load(TableLib())
globals.load(StringLib())
globals.load(CoroutineLib())
globals.load(MathLib())
+ globals.load(Utf8Lib())
globals.load(IoLib())
globals.load(OsLib())
LoadState.install(globals)
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/MathLib.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/MathLib.kt
index 0fa41912..de9e8240 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/MathLib.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/MathLib.kt
@@ -85,32 +85,35 @@ open class MathLib : TwoArgFunction() {
math.set("asin", net.blueva.luak.lib.MathLib.asin())
val atan: LuaValue = net.blueva.luak.lib.MathLib.atan()
math.set("atan", atan)
- math.set("atan2", atan)
math.set("ceil", net.blueva.luak.lib.MathLib.ceil())
math.set("cos", net.blueva.luak.lib.MathLib.cos())
- math.set("cosh", net.blueva.luak.lib.MathLib.cosh())
math.set("deg", net.blueva.luak.lib.MathLib.deg())
math.set("exp", net.blueva.luak.lib.MathLib.exp())
math.set("floor", net.blueva.luak.lib.MathLib.floor())
math.set("fmod", net.blueva.luak.lib.MathLib.fmod())
math.set("frexp", net.blueva.luak.lib.MathLib.frexp())
math.set("huge", LuaDouble.POSINF)
+ math.set("maxinteger", LuaValue.valueOf(Long.MAX_VALUE))
+ math.set("mininteger", LuaValue.valueOf(Long.MIN_VALUE))
math.set("ldexp", net.blueva.luak.lib.MathLib.ldexp())
math.set("log", net.blueva.luak.lib.MathLib.log())
math.set("max", net.blueva.luak.lib.MathLib.max())
math.set("min", net.blueva.luak.lib.MathLib.min())
math.set("modf", net.blueva.luak.lib.MathLib.modf())
math.set("pi", kotlin.math.PI)
- math.set("pow", net.blueva.luak.lib.MathLib.pow())
val r: random?
math.set("random", net.blueva.luak.lib.MathLib.random().also { r = it })
math.set("randomseed", net.blueva.luak.lib.MathLib.randomseed((r)!!))
math.set("rad", net.blueva.luak.lib.MathLib.rad())
math.set("sin", net.blueva.luak.lib.MathLib.sin())
- math.set("sinh", net.blueva.luak.lib.MathLib.sinh())
math.set("sqrt", net.blueva.luak.lib.MathLib.sqrt())
math.set("tan", net.blueva.luak.lib.MathLib.tan())
- math.set("tanh", net.blueva.luak.lib.MathLib.tanh())
+ math.set("tointeger", net.blueva.luak.lib.MathLib.tointeger())
+ math.set("type", net.blueva.luak.lib.MathLib.type())
+ math.set("ult", net.blueva.luak.lib.MathLib.ult())
+ // math.atan2, math.cosh, math.pow, math.sinh and math.tanh were
+ // deprecated in 5.3 and removed in 5.4; the classes behind them stay
+ // for embedders that want to put them back.
env!!.set("math", math)
if (!env!!.get("package")!!.isnil()) env!!.get("package")!!.get("loaded")!!.set("math", math)
return math
@@ -132,9 +135,54 @@ open class MathLib : TwoArgFunction() {
protected abstract fun call(x: Double, y: Double): Double
}
- internal class abs : UnaryOp() {
- override fun call(d: Double): Double {
- return kotlin.math.abs(d)
+ /** `math.abs`; an integer argument gives an integer, wrapping on mininteger. */
+ internal class abs : OneArgFunction() {
+ override fun call(arg: LuaValue?): LuaValue? {
+ val x: LuaValue = arg!!
+ if (x.isinttype()) {
+ val v: Long = x.tolong()
+ return valueOf(if (v < 0L) -v else v) // -mininteger wraps, as in C
+ }
+ return valueOf(kotlin.math.abs(x.checkdouble()))
+ }
+ }
+
+ /** `math.type`: `"integer"`, `"float"`, or nil for anything else. */
+ internal class type : OneArgFunction() {
+ override fun call(arg: LuaValue?): LuaValue? {
+ val x: LuaValue = arg!!
+ if (!x.isnumber() || x.isstring() && !x.isnumber()) return NIL
+ if (x.type() != LuaValue.TNUMBER) return NIL
+ return valueOf(if (x.isinttype()) "integer" else "float")
+ }
+ }
+
+ /** `math.tointeger`: the integer a value denotes exactly, or nil. */
+ internal class tointeger : OneArgFunction() {
+ override fun call(arg: LuaValue?): LuaValue? {
+ val x: LuaValue = arg!!
+ if (x.isinttype()) return x
+ val n: LuaValue = x.tonumber()
+ if (n.isnil()) return NIL
+ // A numeral that already denotes an integer is one, whatever its
+ // magnitude; only a float has to be checked for a whole value.
+ if (n.isinttype()) return n
+ val d: Double = n.todouble()
+ // Range as well as round trip: converting a double past the
+ // integer range saturates, and converting that back matches.
+ if (d < -9.2233720368547758E18 || d >= 9.2233720368547758E18) return NIL
+ val l: Long = d.toLong()
+ return if (l.toDouble() == d) valueOf(l) else NIL
+ }
+ }
+
+ /** `math.ult`: compares two integers as unsigned. */
+ internal class ult : TwoArgFunction() {
+ override fun call(x: LuaValue?, y: LuaValue?): LuaValue? {
+ val a: Long = x!!.checklong()
+ val b: Long = y!!.checklong()
+ // Flipping the sign bit orders the values as if unsigned.
+ return valueOf((a xor Long.MIN_VALUE) < (b xor Long.MIN_VALUE))
}
}
@@ -187,9 +235,12 @@ open class MathLib : TwoArgFunction() {
}
}
- internal class ceil : UnaryOp() {
- override fun call(d: Double): Double {
- return kotlin.math.ceil(d)
+ /** `math.ceil`; the result is an integer whenever it fits in one. */
+ internal class ceil : OneArgFunction() {
+ override fun call(arg: LuaValue?): LuaValue? {
+ val x: LuaValue = arg!!
+ if (x.isinttype()) return x
+ return net.blueva.luak.lib.MathLib.Companion.narrowToInteger(kotlin.math.ceil(x.checkdouble()))
}
}
@@ -205,9 +256,12 @@ open class MathLib : TwoArgFunction() {
}
}
- internal class floor : UnaryOp() {
- override fun call(d: Double): Double {
- return kotlin.math.floor(d)
+ /** `math.floor`; the result is an integer whenever it fits in one. */
+ internal class floor : OneArgFunction() {
+ override fun call(arg: LuaValue?): LuaValue? {
+ val x: LuaValue = arg!!
+ if (x.isinttype()) return x
+ return net.blueva.luak.lib.MathLib.Companion.narrowToInteger(kotlin.math.floor(x.checkdouble()))
}
}
@@ -243,17 +297,47 @@ open class MathLib : TwoArgFunction() {
internal class fmod : TwoArgFunction() {
override fun call(xv: LuaValue?, yv: LuaValue?): LuaValue? {
- if (xv!!.islong() && yv!!.islong() && yv!!.tolong() != 0L) {
- return valueOf((xv!!.tolong() % yv!!.tolong()).toDouble())
+ if (xv!!.isinttype() && yv!!.isinttype()) {
+ // Two integers give an integer, and there is no integer answer
+ // to a division by zero - unlike the float case, which has NaN.
+ val y: Long = yv.tolong()
+ if (y == 0L) LuaValue.argerror(2, "zero")
+ if (y == -1L) return valueOf(0L) // avoids overflow on the minimum
+ // Long remainder already takes the sign of the dividend, like C fmod.
+ return valueOf(xv.tolong() % y)
}
- return valueOf(xv!!.checkdouble() % yv!!.checkdouble())
+ return valueOf(xv.checkdouble() % yv!!.checkdouble())
}
}
+ /**
+ * `math.ldexp (m, e)`: `m * 2^e`.
+ *
+ * Split into steps that each stay inside the double range, so a large
+ * exponent does not go through an infinity on the way and lose the value.
+ */
internal class ldexp : BinaryOp() {
override fun call(x: Double, y: Double): Double {
- // This is the behavior on os-x, windows differs in rounding behavior.
- return x * Double.fromBits(((y.toLong()) + 1023) shl 52)
+ if (x == 0.0 || x.isNaN() || x.isInfinite()) return x
+ var result: Double = x
+ var remaining: Int = y.toInt()
+ while (remaining > 1000) {
+ result *= net.blueva.luak.lib.MathLib.Companion.TWO_POW_1000
+ remaining -= 1000
+ }
+ while (remaining < -1000) {
+ result /= net.blueva.luak.lib.MathLib.Companion.TWO_POW_1000
+ remaining += 1000
+ }
+ var step = 1.0
+ var factor = 2.0
+ var count: Int = if (remaining < 0) -remaining else remaining
+ while (count > 0) {
+ if (count and 1 == 1) step *= factor
+ factor *= factor
+ count = count shr 1
+ }
+ return if (remaining < 0) result / step else result * step
}
}
@@ -267,6 +351,9 @@ open class MathLib : TwoArgFunction() {
override fun invoke(args: Varargs): Varargs {
val x: Double = args.checkdouble(1)
if (x == 0.0) return (varargsOf(ZERO, (ZERO)!!))!!
+ // An infinity and a NaN have no mantissa and exponent to split
+ // into; C answers the value itself with a zero exponent.
+ if (x.isNaN() || x.isInfinite()) return (varargsOf(valueOf(x), (ZERO)!!))!!
val bits: Long = (x).toBits()
val m =
((bits and ((-1L shl 52).inv()).toLong()) + (1L shl 52)) * (if (bits >= 0) (.5 / (1L shl 52)) else (-.5 / (1L shl 52)))
@@ -277,6 +364,9 @@ open class MathLib : TwoArgFunction() {
internal class max : VarArgFunction() {
override fun invoke(args: Varargs): Varargs {
+ // With nothing to compare there is no answer, and the complaint is
+ // about the missing argument rather than about its type.
+ if (args.narg() < 1) LuaValue.argerror(1, "value expected")
var m: LuaValue = args.checknumber(1)
var i = 2
val n: Int = args.narg()
@@ -291,6 +381,9 @@ open class MathLib : TwoArgFunction() {
internal class min : VarArgFunction() {
override fun invoke(args: Varargs): Varargs {
+ // With nothing to compare there is no answer, and the complaint is
+ // about the missing argument rather than about its type.
+ if (args.narg() < 1) LuaValue.argerror(1, "value expected")
var m: LuaValue = args.checknumber(1)
var i = 2
val n: Int = args.narg()
@@ -317,31 +410,136 @@ open class MathLib : TwoArgFunction() {
}
}
- internal class random : LibFunction() {
- var random: Random = Random.Default
- override fun call(): LuaValue? {
- return valueOf(random.nextDouble())
+ /**
+ * The generator Lua 5.5 uses: xoshiro256**.
+ *
+ * Reproduced exactly, seeding included, so a chunk that seeds the generator
+ * and records what came out gets the same sequence here as it would from
+ * the reference interpreter.
+ */
+ internal class Xoshiro256 {
+ private var s0: Long = 0
+ private var s1: Long = 0
+ private var s2: Long = 0
+ private var s3: Long = 0
+
+ init {
+ // Something varying, so an unseeded program does not repeat itself.
+ seed(Random.Default.nextLong(), Random.Default.nextLong())
+ }
+
+ fun seed(n1: Long, n2: Long) {
+ s0 = n1
+ s1 = 0xFF // never all-zero, which the generator cannot leave
+ s2 = n2
+ s3 = 0
+ // Discarded, to spread the seed through the whole state.
+ repeat(16) { next() }
}
- override fun call(a: LuaValue?): LuaValue? {
- val m: Int = a!!.checkint()
- if (m < 1) argerror(1, "interval is empty")
- return valueOf(1 + random.nextInt(m))
+ fun next(): Long {
+ val result: Long = rotl(s1 * 5L, 7) * 9L
+ val t: Long = s1 shl 17
+ s2 = s2 xor s0
+ s3 = s3 xor s1
+ s1 = s1 xor s2
+ s0 = s0 xor s3
+ s2 = s2 xor t
+ s3 = rotl(s3, 45)
+ return result
}
- override fun call(a: LuaValue?, b: LuaValue?): LuaValue? {
- val m: Int = a!!.checkint()
- val n: Int = b!!.checkint()
- if (n < m) argerror(2, "interval is empty")
- return valueOf(m + random.nextInt(n + 1 - m))
+ /** A float in `[0,1)`, taking the top 53 bits - a double's whole mantissa. */
+ fun nextDouble(): Double = (next() ushr 11).toDouble() * SCALE
+
+ private fun rotl(x: Long, n: Int): Long = (x shl n) or (x ushr (64 - n))
+
+ private companion object {
+ /** 2^-53: one unit in the last place of the mantissa. */
+ const val SCALE: Double = 1.0 / 9007199254740992.0
}
}
- internal class randomseed(val random: MathLib.random) : OneArgFunction() {
- override fun call(arg: LuaValue?): LuaValue? {
- val seed: Long = arg!!.checklong()
- random.random = kotlin.random.Random(seed)
- return (NONE)!!
+ /**
+ * `math.random ([m [, n]])`.
+ *
+ * With no argument a float in `[0,1)`; with one, an integer in `[1,m]`;
+ * with two, one in `[m,n]`. The whole 64-bit range is available, so
+ * `math.random(1, math.maxinteger)` works, and `math.random(0)` answers an
+ * integer with every bit drawn at random.
+ */
+ internal class random : VarArgFunction() {
+ var generator: Xoshiro256 = Xoshiro256()
+
+ override fun invoke(args: Varargs): Varargs {
+ // Drawn before the arguments are examined, as upstream draws it, so
+ // the sequence does not depend on how the call was written.
+ val draw: Long = generator.next()
+ val low: Long
+ val high: Long
+ when (args.narg()) {
+ 0 -> return valueOf((draw ushr 11).toDouble() * (1.0 / 9007199254740992.0))!!
+ 1 -> {
+ val m: Long = args.checklong(1)
+ // random(0) is the one case that is not a range: it asks
+ // for an integer with all of its bits set at random.
+ if (m == 0L) return valueOf(draw)!!
+ low = 1L
+ high = m
+ }
+
+ 2 -> {
+ low = args.checklong(1)
+ high = args.checklong(2)
+ }
+
+ else -> return LuaValue.error("wrong number of arguments")!!
+ }
+ args.argcheck(low <= high, 1, "interval is empty")
+ return valueOf(low + project(draw, high - low))!!
+ }
+
+ /**
+ * An unbiased draw in `[0, span]`, treating both as unsigned.
+ *
+ * Taking a remainder would favour the low end of the range, so the
+ * draw is masked down to the next power of two minus one and retried
+ * until it lands inside, as upstream does.
+ */
+ private fun project(draw: Long, span: Long): Long {
+ if (span and (span + 1) == 0L) return draw and span // span + 1 is a power of two
+ var limit = span
+ limit = limit or (limit ushr 1)
+ limit = limit or (limit ushr 2)
+ limit = limit or (limit ushr 4)
+ limit = limit or (limit ushr 8)
+ limit = limit or (limit ushr 16)
+ limit = limit or (limit ushr 32)
+ var value = draw and limit
+ while (value.toULong() > span.toULong()) value = generator.next() and limit
+ return value
+ }
+ }
+
+ /**
+ * `math.randomseed ([x [, y]])`.
+ *
+ * Seeds the generator and answers the two halves of the seed it used, so a
+ * run that wants to be repeatable can record them.
+ */
+ internal class randomseed(val random: MathLib.random) : VarArgFunction() {
+ override fun invoke(args: Varargs): Varargs {
+ val x: Long
+ val y: Long
+ if (args.isnoneornil(1)) {
+ x = Random.Default.nextLong()
+ y = random.generator.next()
+ } else {
+ x = args.checklong(1)
+ y = args.optlong(2, 0L)
+ }
+ random.generator.seed(x, y)
+ return varargsOf(valueOf(x), valueOf(y))!!
}
}
@@ -354,6 +552,30 @@ open class MathLib : TwoArgFunction() {
}
companion object {
+ /** 2^1000, the largest step ldexp can take without leaving the range. */
+ internal val TWO_POW_1000: Double = run {
+ var result = 1.0
+ repeat(1000) { result *= 2.0 }
+ result
+ }
+
+ /** A float result becomes an integer when it is representable as one. */
+ /**
+ * The integer [value] denotes, or the float itself when it denotes none.
+ *
+ * The range has to be checked as well as the round trip: converting a
+ * double past the integer range saturates at the end, and converting
+ * that back lands on the same double, so `2^63` would otherwise look
+ * like an integer.
+ */
+ internal fun narrowToInteger(value: Double): LuaValue {
+ if (value < -9.2233720368547758E18 || value >= 9.2233720368547758E18) {
+ return LuaValue.valueOf(value)
+ }
+ val asLong: Long = value.toLong()
+ return if (asLong.toDouble() == value) LuaValue.valueOf(asLong) else LuaValue.valueOf(value)
+ }
+
/** Pointer to the latest MathLib instance, used only to dispatch
* math.exp to tha correct platform math library.
*/
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/OneArgFunction.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/OneArgFunction.kt
index b7cdd619..95c689fe 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/OneArgFunction.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/OneArgFunction.kt
@@ -66,6 +66,8 @@ abstract class OneArgFunction
}
override fun invoke(varargs: Varargs): Varargs {
- return call(varargs.arg1())!!
+ // A one-argument function can only ever be complaining about argument
+ // one, so the index is attached here rather than left off the message.
+ return net.blueva.luak.withArgIndex(1) { call(varargs.arg1())!! }
}
}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/OsLib.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/OsLib.kt
index 88a36b15..fecbd240 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/OsLib.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/OsLib.kt
@@ -139,7 +139,12 @@ open class OsLib
)
)
- net.blueva.luak.lib.OsLib.Companion.EXECUTE -> return execute(args.optjstring(1, null))
+ net.blueva.luak.lib.OsLib.Companion.EXECUTE -> {
+ // Asked with nothing to run, the question is only
+ // whether there is anything to run commands with.
+ val command: String? = args.optjstring(1, null)
+ return if (command == null) valueOf(hasshell())!! else execute(command)
+ }
net.blueva.luak.lib.OsLib.Companion.EXIT -> {
exit(args.optint(1, 0))
return (NONE)!!
@@ -272,6 +277,14 @@ open class OsLib
return varargsOf(NIL, valueOf("exit"), (ONE)!!)
}
+ /**
+ * True where the host can run a command for `os.execute`.
+ *
+ * Answered by `os.execute()` with nothing to run, which is how a program
+ * asks whether running anything is possible at all.
+ */
+ protected open fun hasshell(): Boolean = false
+
/**
* Calls the C function exit, with an optional code, to terminate the host program.
* @param code
@@ -341,7 +354,12 @@ open class OsLib
* cannot be honored.
*/
protected fun setlocale(locale: String?, category: String?): String? {
- return "C"
+ // This runtime has one locale and it is "C": numbers and dates are
+ // formatted the same way everywhere it runs. Reporting success for a
+ // locale that is not in force would tell a caller it can expect, say, a
+ // comma decimal separator that it will never get.
+ if (locale == null || locale == "C" || locale.isEmpty()) return "C"
+ return null
}
/**
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/PackageLib.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/PackageLib.kt
index ac6ab2e2..eeaca7bb 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/PackageLib.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/PackageLib.kt
@@ -120,8 +120,15 @@ class PackageLib : TwoArgFunction() {
searchers.set(2, lua_searcher().also { luaSearcher = it })
searchers.set(3, java_searcher().also { javaSearcher = it })
package_!!.set(net.blueva.luak.lib.PackageLib.Companion._SEARCHERS, searchers)
+ // No C loader here, so the path for one is empty rather than absent:
+ // a chunk that reads package.cpath still finds a string.
+ package_!!.set("cpath", "")
package_!!.set("config", net.blueva.luak.lib.PackageLib.Companion.FILE_SEP.toString() + "\n;\n?\n!\n-\n")
package_!!.get((net.blueva.luak.lib.PackageLib.Companion._LOADED)!!).set("package", package_)
+ // The globals table is a loaded module too, under the name Lua gives
+ // it. Among other things that is where an error message looks to find
+ // out what a plain global function is called.
+ package_!!.get((net.blueva.luak.lib.PackageLib.Companion._LOADED)!!).set("_G", env)
env!!.set("package", package_)
globals!!.package_ = this
return env
@@ -171,33 +178,46 @@ class PackageLib : TwoArgFunction() {
* If there is any error loading or running the module, or if it cannot find any loader for the module,
* then require raises an error.
*/
- inner class require : OneArgFunction() {
- override fun call(arg: LuaValue?): LuaValue? {
- val name: LuaString? = arg!!.checkstring()
+ inner class require : VarArgFunction() {
+ override fun invoke(args: Varargs): Varargs {
+ val arg: LuaValue = args.checkvalue(1)!!
+ val name: LuaString? = arg.checkstring()
val loaded: LuaValue = package_!!.get((net.blueva.luak.lib.PackageLib.Companion._LOADED)!!)
var result: LuaValue = loaded.get((name)!!)
if (result.toboolean()) {
if (result === net.blueva.luak.lib.PackageLib.Companion._SENTINEL) error("loop or previous error loading module '" + name + "'")
+ // Already loaded: the module and nothing else, since there was
+ // no search this time to say where it came from.
return result
}
/* else must load it; iterate over available loaders */
- val tbl: LuaTable = package_!!.get((net.blueva.luak.lib.PackageLib.Companion._SEARCHERS)!!).checktable()!!
+ val searchers: LuaValue = package_!!.get((net.blueva.luak.lib.PackageLib.Companion._SEARCHERS)!!)
+ // Where the searchers are is set up by whoever built the state, so
+ // this is a fault in that rather than in the module being asked for.
+ if (!searchers.istable()) LuaValue.error("'package.searchers' must be a table")
+ val tbl: LuaTable = searchers.checktable()!!
val sb: StringBuilder = StringBuilder()
var loader: Varargs? = null
var i = 1
while (true) {
val searcher: LuaValue = tbl.get(i)
if (searcher.isnil()) {
- error("module '" + name + "' not found: " + name + sb)
+ // One line per searcher that had nothing, and no repeat of
+ // the name: the searchers already say what they looked for.
+ error("module '" + name + "' not found:" + sb)
}
/* call loader with module name as argument */
loader = searcher.invoke((name)!!)
if (loader!!.isfunction(1)) break
- if (loader!!.isstring(1)) sb.append(loader!!.tojstring(1))
+ if (loader!!.isstring(1)) {
+ val report: String = loader!!.tojstring(1)
+ if (!report.startsWith("\n")) sb.append("\n\t")
+ sb.append(report)
+ }
i++
}
@@ -209,7 +229,10 @@ class PackageLib : TwoArgFunction() {
else if ((loaded.get((name)!!)
.also { result = it }) === net.blueva.luak.lib.PackageLib.Companion._SENTINEL
) loaded.set(name, LuaValue.TRUE!!.also { result = it })
- return result
+ // What the search found alongside the loader - the file it came
+ // from - is handed back with it, which is what lets a module say
+ // where it was loaded from.
+ return varargsOf(result, loader!!.arg(2)!!)!!
}
}
@@ -224,7 +247,10 @@ class PackageLib : TwoArgFunction() {
override fun invoke(args: Varargs): Varargs {
val name: LuaString? = args.checkstring(1)
val `val`: LuaValue = package_!!.get((net.blueva.luak.lib.PackageLib.Companion._PRELOAD)!!).get((name)!!)
- return if (`val`.isnil()) valueOf("\n\tno field package.preload['" + name + "']") else `val`
+ if (`val`.isnil()) return valueOf("\n\tno field package.preload['" + name + "']")!!
+ // Where it came from, for a module that wants to know: nowhere in
+ // particular, which Lua says in so many words.
+ return varargsOf(`val`, valueOf(":preload:"))!!
}
}
@@ -235,7 +261,10 @@ class PackageLib : TwoArgFunction() {
// get package path
val path: LuaValue = package_!!.get((net.blueva.luak.lib.PackageLib.Companion._PATH)!!)
- if (!path.isstring()) return valueOf("package.path is not a string")
+ // Not something to look in, so nothing was looked in: this is a
+ // fault in how the search was set up rather than a module that
+ // could not be found.
+ if (!path.isstring()) LuaValue.error("'package.path' must be a string")
// get the searchpath function.
@@ -244,20 +273,32 @@ class PackageLib : TwoArgFunction() {
// Did we get a result?
+ // searchpath already lists one "no file" line per template.
if (!v.isstring(1)) return v.arg(2)!!.tostring()
val filename: LuaString = v.arg1()!!.strvalue()!!
// Try to load the file.
- v = globals!!.loadfile(filename.tojstring())!!
- if (v.arg1().isfunction()) return (LuaValue.varargsOf(v.arg1(), filename))!!
-
+ val loaded: Varargs = try {
+ globals!!.loadfile(filename.tojstring()) ?: NIL
+ } catch (le: net.blueva.luak.LuaError) {
+ badmodule(name, filename, le.message)
+ }
+ if (loaded.arg1().isfunction()) return (LuaValue.varargsOf(loaded.arg1(), filename))!!
- // report error
- return (varargsOf(NIL, valueOf("'" + filename + "': " + v.arg(2)!!.tojstring())))!!
+ // A file that is there but cannot be loaded is not a module that
+ // was not found: the search is over and this is what went wrong.
+ badmodule(name, filename, loaded.arg(2)!!.tojstring())
}
}
+ /** Refuses a module that is there but cannot be loaded, as Lua words it. */
+ private fun badmodule(name: LuaString?, filename: LuaString, why: String?): Nothing {
+ throw net.blueva.luak.LuaError(
+ "error loading module '" + name + "' from file '" + filename + "':\n\t" + why
+ )
+ }
+
inner class searchpath : VarArgFunction() {
override fun invoke(args: Varargs): Varargs {
var name: String = args.checkjstring(1)
@@ -270,7 +311,9 @@ class PackageLib : TwoArgFunction() {
var e = -1
val n: Int = path.length
var sb: StringBuilder? = null
- name = name.replace(sep[0], rep[0])
+ // The separator is a piece of text, not a character: what is
+ // replaced is every run of it, and an empty one replaces nothing.
+ if (sep.isNotEmpty() && name.contains(sep)) name = name.replace(sep, rep)
while (e < n) {
// find next template
@@ -280,12 +323,9 @@ class PackageLib : TwoArgFunction() {
val template: String = path.substring(b, e)
- // create filename
- val q: Int = template.indexOf('?')
- var filename = template
- if (q >= 0) {
- filename = template.substring(0, q) + name + template.substring(q + 1)
- }
+ // create filename: every '?' stands for the name, not just
+ // the first one
+ val filename: String = template.replace("?", name)
// try opening the file
@@ -301,22 +341,47 @@ class PackageLib : TwoArgFunction() {
// report error
if (sb == null) sb = StringBuilder()
- sb.append("\n\t" + filename)
+ // One line per template that did not match, worded the way Lua
+ // words it so a caller can read the list back.
+ if (sb.isNotEmpty()) sb.append("\n\t")
+ sb.append("no file '" + filename + "'")
}
return (varargsOf(NIL, valueOf(sb.toString())))!!
}
}
+ /**
+ * The searcher for a module that is not written in Lua.
+ *
+ * A reference build looks for a library to load through `package.cpath`;
+ * there is none to load here, so what stands in its place is a class of
+ * the host's, looked up by the module's name. The places `package.cpath`
+ * names are still walked and still reported, so a module that is nowhere
+ * to be found says where it was looked for in the words Lua uses.
+ */
inner class java_searcher : VarArgFunction() {
override fun invoke(args: Varargs): Varargs {
- val className = toClassname(args.checkjstring(1))!!
- return try {
+ val name: String = args.checkjstring(1)
+ val className = toClassname(name)!!
+ try {
val value = platformLoadLibrary(className, globals!!)
- ?: return valueOf("\n\tno class '$className'")
- varargsOf(value, globals!!)!!
+ if (value != null) return varargsOf(value, globals!!)!!
} catch (error: Throwable) {
- valueOf("\n\tclass load failed on '$className', $error")
+ // Nothing of the host's under that name either; fall through
+ // to reporting where it was looked for.
+ }
+ val cpath: LuaValue = package_!!.get("cpath")!!
+ if (!cpath.isstring()) LuaValue.error("'package.cpath' must be a string")
+ val found: Varargs = package_!!.get((net.blueva.luak.lib.PackageLib.Companion._SEARCHPATH)!!)
+ .invoke((varargsOf(valueOf(name), cpath))!!)
+ // A file that is there is still not something that can be loaded,
+ // so what comes back is only ever the list of places looked in.
+ if (found.isstring(1)) {
+ return valueOf("\n\tno file '" + found.arg1()!!.tojstring() + "'")
}
+ val why: LuaValue = found.arg(2)!!
+ if (why.isnil()) return valueOf("\n\tno class '$className'")
+ return valueOf("\n\t" + why.tojstring())
}
}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/StringLib.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/StringLib.kt
index f5ac10f5..7ab6c1a1 100644
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/StringLib.kt
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/StringLib.kt
@@ -86,11 +86,14 @@ open class StringLib
string.set("find", net.blueva.luak.lib.StringLib.find())
string.set("format", format())
string.set("gmatch", net.blueva.luak.lib.StringLib.gmatch())
- string.set("gsub", net.blueva.luak.lib.StringLib.gsub())
+ string.set("gsub", net.blueva.luak.lib.StringLib.gsub(env as? net.blueva.luak.Globals))
string.set("len", net.blueva.luak.lib.StringLib.len())
string.set("lower", net.blueva.luak.lib.StringLib.lower())
string.set("match", net.blueva.luak.lib.StringLib.match())
+ string.set("pack", net.blueva.luak.lib.StringLib.pack())
+ string.set("packsize", net.blueva.luak.lib.StringLib.packsize())
string.set("rep", net.blueva.luak.lib.StringLib.rep())
+ string.set("unpack", net.blueva.luak.lib.StringLib.unpack())
string.set("reverse", net.blueva.luak.lib.StringLib.reverse())
string.set("sub", net.blueva.luak.lib.StringLib.sub())
string.set("upper", net.blueva.luak.lib.StringLib.upper())
@@ -98,11 +101,83 @@ open class StringLib
env!!.set("string", string)
if (!env!!.get("package")!!.isnil()) env!!.get("package")!!.get("loaded")!!.set("string", string)
if (LuaString.s_metatable == null) {
- LuaString.s_metatable = LuaValue.tableOf(arrayOf(INDEX, string))
+ val metatable: LuaTable = LuaValue.tableOf(arrayOf(INDEX, string))!!
+ // Since 5.4 the arithmetic coercion of strings lives here rather
+ // than in the VM, which is what makes `"a" + 1` report "attempt to
+ // add a 'string' with a 'number'" instead of a generic arithmetic
+ // error, and what lets the other operand's metamethod have a turn.
+ metatable.set(ADD, StringArith(ADD, "add"))
+ metatable.set(SUB, StringArith(SUB, "sub"))
+ metatable.set(MUL, StringArith(MUL, "mul"))
+ metatable.set(MOD, StringArith(MOD, "mod"))
+ metatable.set(POW, StringArith(POW, "pow"))
+ metatable.set(DIV, StringArith(DIV, "div"))
+ metatable.set(IDIV, StringArith(IDIV, "idiv"))
+ metatable.set(UNM, StringArith(UNM, "unm"))
+ LuaString.s_metatable = metatable
}
return string
}
+ /** `string.pack (fmt, v1, v2, ...)`, from Lua 5.3. */
+ internal class pack : VarArgFunction() {
+ override fun invoke(args: Varargs): Varargs = net.blueva.luak.lib.StringPack.pack(args)
+ }
+
+ /** `string.packsize (fmt)`, from Lua 5.3. */
+ internal class packsize : VarArgFunction() {
+ override fun invoke(args: Varargs): Varargs = net.blueva.luak.lib.StringPack.packsize(args)
+ }
+
+ /** `string.unpack (fmt, s [, pos])`, from Lua 5.3. */
+ internal class unpack : VarArgFunction() {
+ override fun invoke(args: Varargs): Varargs = net.blueva.luak.lib.StringPack.unpack(args)
+ }
+
+ /**
+ * One arithmetic metamethod of the string metatable.
+ *
+ * It mirrors upstream's `arith` in `lstrlib.c`: if both operands denote
+ * numbers the operation goes ahead on those numbers, and otherwise the
+ * right-hand operand is offered its own metamethod - unless it is a string
+ * too, in which case there is nothing left to try and the operation is an
+ * error naming both types.
+ *
+ * @param event the metatag this handler is registered under
+ * @param opname the name that appears in the error message
+ */
+ internal class StringArith(private val event: LuaString, private val opname: String) : TwoArgFunction() {
+ override fun call(arg1: LuaValue?, arg2: LuaValue?): LuaValue {
+ val left: LuaValue = arg1 ?: NIL
+ // Lua hands a unary operator its operand twice, so a caller that
+ // passed only one gets the same value for both.
+ // Compared by value: the metatag constants are getters that build a
+ // fresh LuaString on every read, so identity never holds.
+ val right: LuaValue = if (event == UNM) left else (arg2 ?: NIL)
+ val leftNumber: LuaValue = left.tonumber()
+ val rightNumber: LuaValue = right.tonumber()
+ if (!leftNumber.isnil() && !rightNumber.isnil()) return apply(leftNumber, rightNumber)
+ if (right.type() != LuaValue.TSTRING) {
+ val handler: LuaValue = right.metatag(event)
+ if (!handler.isnil()) return handler.call(left, right)!!
+ }
+ return LuaValue.error(
+ "attempt to " + opname + " a '" + left.typename() + "' with a '" + right.typename() + "'",
+ )!!
+ }
+
+ private fun apply(left: LuaValue, right: LuaValue): LuaValue = when (event) {
+ ADD -> left.add(right)
+ SUB -> left.sub(right)
+ MUL -> left.mul(right)
+ MOD -> left.mod(right)
+ POW -> left.pow(right)
+ DIV -> left.div(right)
+ IDIV -> left.idiv(right)
+ else -> left.neg()
+ }
+ }
+
/**
* string.byte (s [, i [, j]])
*
@@ -156,8 +231,10 @@ open class StringLib
var i = 0
var a = 1
while (i < n) {
- val c: Int = args.checkint(a)
- if (c < 0 || c >= 256) argerror(a, "invalid value for string.char [0; 255]: " + c)
+ // Checked as a whole integer, so a value far out of range is
+ // rejected rather than wrapping into an acceptable byte.
+ val c: Long = args.checklong(a)
+ if (c < 0 || c > 255) argerror(a, "value out of range")
bytes[i] = c.toByte()
i++
a++
@@ -173,16 +250,21 @@ open class StringLib
* so that a later loadstring on this string returns a copy of the function.
* function must be a Lua function without upvalues.
* Boolean param stripDebug - true to strip debugging info, false otherwise.
- * The default value for stripDebug is true.
+ * The default value for stripDebug is false.
*
* TODO: port dumping code as optional add-on
*/
internal class dump : VarArgFunction() {
override fun invoke(args: Varargs): Varargs {
val f: LuaValue = args.checkfunction(1)
+ // Only a Lua function has bytecode to write out; anything from the
+ // library is native and has none.
+ if (f !is LuaClosure) LuaValue.argerror(1, "Lua function expected")
val baos: ByteArrayOutputStream = ByteArrayOutputStream()
try {
- DumpState.dump((f as LuaClosure).p, baos, args.optboolean(2, true))
+ // Debug information is kept unless the caller asks for it to
+ // go: a dump that still names its upvalues is the useful one.
+ DumpState.dump((f as LuaClosure).p, baos, args.optboolean(2, false))
return LuaString.valueUsing(baos.toByteArray())
} catch (e: IOException) {
return (error(e.message))!!
@@ -252,25 +334,93 @@ open class StringLib
++i
result.append(net.blueva.luak.lib.StringLib.Companion.L_ESC.toByte())
} else {
- arg++
- val fdsc: FormatDesc = FormatDesc(args, fmt, i)
+ // A missing argument is reported before the
+ // specification is even looked at, as upstream does.
+ if (++arg > args.narg()) LuaValue.argerror(arg, "no value")
+ val fdsc: FormatDesc = FormatDesc(fmt, i)
i += fdsc.length
when (fdsc.conversion) {
- 'c'.code -> fdsc.format(result, args.checkint(arg).toByte())
- 'i'.code, 'd'.code -> fdsc.format(result, args.checklong(arg))
- 'o'.code, 'u'.code, 'x'.code, 'X'.code -> fdsc.format(result, args.checklong(arg))
- 'e'.code, 'E'.code, 'f'.code, 'g'.code, 'G'.code -> fdsc.format(result, args.checkdouble(arg))
- 'q'.code -> net.blueva.luak.lib.StringLib.Companion.addquoted(result, args.checkstring(arg))
+ 'c'.code -> {
+ fdsc.check(FLAGS_C, precision = false)
+ fdsc.format(result, args.checkint(arg).toByte())
+ }
+
+ 'i'.code, 'd'.code -> {
+ val value: Long = args.checklong(arg)
+ fdsc.check(FLAGS_I, precision = true)
+ fdsc.format(result, value)
+ }
+
+ 'u'.code -> {
+ val value: Long = args.checklong(arg)
+ fdsc.check(FLAGS_U, precision = true)
+ fdsc.format(result, value)
+ }
+
+ 'o'.code, 'x'.code, 'X'.code -> {
+ val value: Long = args.checklong(arg)
+ fdsc.check(FLAGS_X, precision = true)
+ fdsc.format(result, value)
+ }
+
+ 'e'.code, 'E'.code, 'f'.code, 'g'.code, 'G'.code -> {
+ val value: Double = args.checkdouble(arg)
+ fdsc.check(FLAGS_F, precision = true)
+ fdsc.format(result, value)
+ }
+
+ 'a'.code, 'A'.code -> {
+ fdsc.check(FLAGS_F, precision = true)
+ fdsc.formathex(
+ result,
+ args.checkdouble(arg),
+ fdsc.conversion == 'A'.code,
+ )
+ }
+
+ 'p'.code -> {
+ val text: LuaString =
+ net.blueva.luak.lib.StringLib.Companion.pointer(args.arg(arg)!!)
+ fdsc.check(FLAGS_C, precision = false)
+ fdsc.format(result, text)
+ }
+
+ 'q'.code -> {
+ if (fdsc.hasmodifiers) error("specifier '%q' cannot have modifiers")
+ net.blueva.luak.lib.StringLib.Companion.addliteral(
+ result,
+ args.arg(arg)!!,
+ )
+ }
+
's'.code -> {
- val s: LuaString = args.checkstring(arg)
- if (fdsc.precision == -1 && s.length() >= 100) {
+ // Lua's own conversion, so %s accepts a nil
+ // or a table with __tostring.
+ val s: LuaString = net.blueva.luak.lib.BaseLib
+ .tolstring(args.arg(arg)!!).strvalue()!!
+ if (!fdsc.hasmodifiers) {
+ // Passed through whole, embedded zeros
+ // and all: there is nothing to line up.
result.append(s)
} else {
- fdsc.format(result, s)
+ args.argcheck(
+ s.indexOf(0.toByte(), 0) < 0,
+ arg,
+ "string contains zeros",
+ )
+ fdsc.check(FLAGS_C, precision = true)
+ // Without a precision there is nothing
+ // to truncate, and a long string is
+ // cheaper to pass through than to pad.
+ if (fdsc.precision < 0 && s.length() >= 100) {
+ result.append(s)
+ } else {
+ fdsc.format(result, s)
+ }
}
}
- else -> error("invalid option '%" + fdsc.conversion.toChar() + "' to 'format'")
+ else -> error("invalid conversion '" + fdsc.src + "' to 'format'")
}
}
}
@@ -283,7 +433,28 @@ open class StringLib
}
}
- internal inner class FormatDesc(args: Varargs?, strfrmt: LuaString, start: Int) {
+ /** As long as a conversion specification may be, upstream's `MAX_FORMAT`. */
+ private val MAX_FORMAT_LENGTH: Int = 32
+
+ /** Flags for `%a`, `%A`, `%e`, `%E`, `%f`, `%g` and `%G`. */
+ private val FLAGS_F: String = "-+#0 "
+
+ /** Flags for `%o`, `%x` and `%X`. */
+ private val FLAGS_X: String = "-#0"
+
+ /** Flags for `%d` and `%i`. */
+ private val FLAGS_I: String = "-+0 "
+
+ /** Flags for `%u`. */
+ private val FLAGS_U: String = "-0"
+
+ /** Flags for `%c`, `%p` and `%s`. */
+ private val FLAGS_C: String = "-"
+
+ /** ASCII only, since a conversion letter is never a byte above 127. */
+ private fun isAsciiLetter(c: Char): Boolean = c in 'a'..'z' || c in 'A'..'Z'
+
+ internal inner class FormatDesc(strfrmt: LuaString, start: Int) {
private var leftAdjust = false
private var zeroPad: Boolean = false
private var explicitPlus = false
@@ -295,60 +466,98 @@ open class StringLib
val conversion: Int
val length: Int
- val src: String?
+ /** The specification as written, `%` and conversion letter included. */
+ val src: String
init {
- var p = start
val n: Int = strfrmt.length()
- var c = 0
-
- var moreFlags = true
- while (moreFlags) {
- when ((if (p < n) strfrmt.luaByte(p++) else 0).also { c = it }) {
+ // Flags, width and precision are read as one span, exactly as
+ // upstream's 'getformat' reads them: nothing is judged here, so a
+ // malformed specification is still available to be quoted back.
+ var p = start
+ while (p < n && net.blueva.luak.lib.StringLib.Companion.isSpecSpan(strfrmt.luaByte(p))) p++
+ // Upstream counts the conversion letter itself, and over there the
+ // string is NUL-terminated, so a specification that runs off the
+ // end still counts one character.
+ if (p - start + 1 >= MAX_FORMAT_LENGTH - 10) error("invalid format (too long)")
+ conversion = if (p < n) strfrmt.luaByte(p) else 0
+ length = p - start + 1
+ src = "%" + strfrmt.substring(start, if (p < n) p + 1 else n).tojstring()
+
+ var scan = start
+ var reading = true
+ while (reading && scan < p) {
+ when (strfrmt.luaByte(scan)) {
'-'.code -> leftAdjust = true
'+'.code -> explicitPlus = true
' '.code -> space = true
'#'.code -> alternateForm = true
'0'.code -> zeroPad = true
- else -> moreFlags = false
+ else -> reading = false
}
+ if (reading) scan++
}
- if (p - start > 5) error("invalid format (repeated flags)")
width = -1
- if (c.toChar().isDigit()) {
- width = c - '0'.code
- c = (if (p < n) strfrmt.luaByte(p++) else 0)
- if (c.toChar().isDigit()) {
- width = width * 10 + (c - '0'.code)
- c = (if (p < n) strfrmt.luaByte(p++) else 0)
+ if (scan < p && strfrmt.luaByte(scan).toChar() in '0'..'9') {
+ width = strfrmt.luaByte(scan++) - '0'.code
+ if (scan < p && strfrmt.luaByte(scan).toChar() in '0'..'9') {
+ width = width * 10 + (strfrmt.luaByte(scan++) - '0'.code)
}
}
precision = -1
- if (c == '.'.code) {
- c = (if (p < n) strfrmt.luaByte(p++) else 0)
- if (c.toChar().isDigit()) {
- precision = c - '0'.code
- c = (if (p < n) strfrmt.luaByte(p++) else 0)
- if (c.toChar().isDigit()) {
- precision = precision * 10 + (c - '0'.code)
- c = (if (p < n) strfrmt.luaByte(p++) else 0)
+ if (scan < p && strfrmt.luaByte(scan) == '.'.code) {
+ scan++
+ // A bare '.' is a precision of zero, not an absent one.
+ precision = 0
+ if (scan < p && strfrmt.luaByte(scan).toChar() in '0'..'9') {
+ precision = strfrmt.luaByte(scan++) - '0'.code
+ if (scan < p && strfrmt.luaByte(scan).toChar() in '0'..'9') {
+ precision = precision * 10 + (strfrmt.luaByte(scan++) - '0'.code)
}
}
}
- if (c.toChar().isDigit()) error("invalid format (width or precision too long)")
-
zeroPad = zeroPad and !leftAdjust // '-' overrides '0'
- conversion = c
- length = p - start
- src = strfrmt.substring(start - 1, p).tojstring()
+ }
+
+ /**
+ * Refuses a specification C's `printf` would not accept.
+ *
+ * [flags] are the ones this conversion takes and [precision] says
+ * whether it takes one at all; what is left over after them, a width of
+ * at most two digits and a precision of at most two more, has to be the
+ * conversion letter itself. A width cannot start with a zero, since
+ * that reads as the padding flag.
+ */
+ fun check(flags: String, precision: Boolean) {
+ var i = 1 // past the '%'
+ while (i < src.length && src[i] in flags) i++
+ if (i < src.length && src[i] != '0') {
+ i = twoDigits(i)
+ if (precision && i < src.length && src[i] == '.') i = twoDigits(i + 1)
+ }
+ if (i >= src.length || !isAsciiLetter(src[i])) {
+ error("invalid conversion specification: '" + src + "'")
+ }
+ }
+
+ private fun twoDigits(from: Int): Int {
+ var i = from
+ if (i < src.length && src[i] in '0'..'9') {
+ i++
+ if (i < src.length && src[i] in '0'..'9') i++
+ }
+ return i
}
fun format(buf: Buffer, c: Byte) {
- // TODO: not clear that any of width, precision, or flags apply here.
+ // A width pads the single character, on whichever side the flags ask.
+ val padding: Int = width - 1
+ if (padding > 0 && !leftAdjust) pad(buf, ' ', padding)
buf.append(c)
+ if (padding > 0 && leftAdjust) pad(buf, ' ', padding)
}
fun format(buf: Buffer, number: Long) {
@@ -358,20 +567,36 @@ open class StringLib
digits = ""
} else {
val radix: Int
+ val unsigned: Boolean
when (conversion) {
- 'x'.code, 'X'.code -> radix = 16
- 'o'.code -> radix = 8
- else -> radix = 10
+ 'x'.code, 'X'.code -> { radix = 16; unsigned = true }
+ 'o'.code -> { radix = 8; unsigned = true }
+ 'u'.code -> { radix = 10; unsigned = true }
+ else -> { radix = 10; unsigned = false }
}
- digits = number.toString(radix)
+ // Hexadecimal and octal read the value as unsigned, the way C
+ // does, so -1 comes out as all ones rather than with a sign.
+ digits = if (unsigned) number.toULong().toString(radix) else number.toString(radix)
if (conversion == 'X'.code) digits = digits.uppercase()
+ // The '#' flag asks for the form a Lua numeral would take, so
+ // the base is spelled out: 0 for octal, 0x or 0X for hex.
+ if (alternateForm && number != 0L) {
+ digits = when (conversion) {
+ 'o'.code -> "0" + digits
+ 'x'.code -> "0x" + digits
+ 'X'.code -> "0X" + digits
+ else -> digits
+ }
+ }
}
var minwidth: Int = digits.length
var ndigits = minwidth
val nzeros: Int
- if (number < 0) {
+ if (number < 0 && !digits.startsWith("-")) {
+ // Nothing to do: an unsigned conversion has no sign to skip.
+ } else if (number < 0) {
ndigits--
} else if (explicitPlus || space) {
minwidth++
@@ -405,14 +630,78 @@ open class StringLib
}
fun format(buf: Buffer, x: Double) {
- buf.append(this@StringLib.format(src, x))
+ // C's float conversions, rendered from the exact decimal digits of
+ // the double. Going through a host formatter instead would follow
+ // the host's locale, so a machine set to a comma decimal separator
+ // produced "3,14" where Lua specifies "3.14".
+ val digits: Int = if (precision < 0) 6 else precision
+ var text: String = when (conversion.toChar()) {
+ 'e' -> net.blueva.luak.DecimalFormat.e(x, digits, upper = false)
+ 'E' -> net.blueva.luak.DecimalFormat.e(x, digits, upper = true)
+ 'f', 'F' -> {
+ val rendered: String = net.blueva.luak.DecimalFormat.f(x, digits)
+ // The '#' flag keeps the decimal point even with no digits
+ // after it, so the value still reads as a float.
+ if (alternateForm && digits == 0 && !rendered.contains('.')) {
+ rendered + "."
+ } else {
+ rendered
+ }
+ }
+ 'G' -> net.blueva.luak.DecimalFormat.g(x, digits).uppercase()
+ else -> net.blueva.luak.DecimalFormat.g(x, digits)
+ }
+ if (!text.startsWith("-")) {
+ if (explicitPlus) text = "+" + text else if (space) text = " " + text
+ }
+ val padding: Int = width - text.length
+ if (padding > 0) {
+ when {
+ leftAdjust -> text = text + " ".repeat(padding)
+ // Zero padding goes after the sign, and never applies to
+ // 'inf' or 'nan', which have no digits to pad.
+ zeroPad && x.isFinite() -> {
+ val signLength: Int = if (text[0] == '-' || text[0] == '+' || text[0] == ' ') 1 else 0
+ text = text.substring(0, signLength) + "0".repeat(padding) + text.substring(signLength)
+ }
+
+ else -> text = " ".repeat(padding) + text
+ }
+ }
+ buf.append(text)
+ }
+
+ /** True when anything was written between the `%` and the letter. */
+ val hasmodifiers: Boolean
+ get() = src.length > 2
+
+ /**
+ * `%a`: the value in hexadecimal, with this descriptor's sign and width.
+ *
+ * A precision here counts hexadecimal digits after the point rather
+ * than characters, so the padding is applied separately from the
+ * rendering.
+ */
+ fun formathex(buf: Buffer, value: Double, upper: Boolean) {
+ var text: String = net.blueva.luak.DecimalFormat.hex(value, upper, precision)
+ if (!text.startsWith("-")) {
+ if (explicitPlus) text = "+" + text else if (space) text = " " + text
+ }
+ val padding: Int = width - text.length
+ if (padding > 0) {
+ text = if (leftAdjust) text + " ".repeat(padding) else " ".repeat(padding) + text
+ }
+ buf.append(text)
}
fun format(buf: Buffer, s: LuaString) {
var s: LuaString = s
- val nullindex: Int = s.indexOf('\u0000'.code.toByte(), 0)
- if (nullindex != -1) s = s.substring(0, nullindex)
+ // A precision on %s is a maximum length, and a width pads to it.
+ if (precision >= 0 && s.length() > precision) s = s.substring(0, precision)
+ val padding: Int = width - s.length()
+ if (padding > 0 && !leftAdjust) pad(buf, ' ', padding)
buf.append(s)
+ if (padding > 0 && leftAdjust) pad(buf, ' ', padding)
}
fun pad(buf: Buffer, c: Char, n: Int) {
@@ -423,10 +712,6 @@ open class StringLib
}
- protected open fun format(src: String?, x: Double): String {
- return (x).toString()
- }
-
/**
* string.gmatch (s, pattern)
*
@@ -455,22 +740,41 @@ open class StringLib
override fun invoke(args: Varargs): Varargs {
val src: LuaString = args.checkstring(1)
val pat: LuaString = args.checkstring(2)
- return net.blueva.luak.lib.StringLib.GMatchAux(args, src, pat)
+ // Since 5.4 a third argument says where to start, counted the way
+ // string.find counts, so a negative one is from the end.
+ val init: Int = net.blueva.luak.lib.StringLib.Companion.posrelat(
+ args.optint(3, 1),
+ src.length(),
+ )
+ val start: Int = when {
+ init < 1 -> 0
+ init > src.length() + 1 -> src.length() + 1
+ else -> init - 1
+ }
+ return net.blueva.luak.lib.StringLib.GMatchAux(args, src, pat, start)
}
}
- internal class GMatchAux(args: Varargs, src: LuaString, pat: LuaString) : VarArgFunction() {
+ internal class GMatchAux(args: Varargs, src: LuaString, pat: LuaString, start: Int = 0) :
+ VarArgFunction() {
private val srclen: Int
private val ms: MatchState
- private var soffset = 0
+ private var soffset: Int
private var lastmatch: Int
init {
this.srclen = src.length()
this.ms = net.blueva.luak.lib.StringLib.MatchState(args, src, pat)
+ this.soffset = start
this.lastmatch = -1
}
+ // The match state is what this iterator carries between calls, which
+ // is what an upvalue is.
+ override fun nupvalues(): Int = 1
+
+ override fun upvaluestate(n: Int): Any? = if (n == 1) ms else null
+
override fun invoke(args: Varargs): Varargs {
while (soffset <= srclen) {
ms.reset()
@@ -533,7 +837,7 @@ open class StringLib
* x = string.gsub("$name-$version.tar.gz", "%$(%w+)", t)
* --> x="lua-5.1.tar.gz"
*/
- internal class gsub : VarArgFunction() {
+ internal class gsub(private val globals: net.blueva.luak.Globals?) : VarArgFunction() {
override fun invoke(args: Varargs): Varargs {
val src: LuaString = args.checkstring(1)
val srclen: Int = src.length()
@@ -548,12 +852,16 @@ open class StringLib
var soffset = 0
var n = 0
+ // Whether anything was actually replaced. When nothing was, the
+ // original string is handed back rather than an equal copy, which
+ // is what lets a caller compare identities to detect a no-op.
+ var changed = false
while (n < max_s) {
ms.reset()
val res = ms.match(soffset, if (anchor) 1 else 0)
if (res != -1 && res != lastmatch) { /* match? */
n++
- ms.add_value(lbuf, soffset, res, repl) /* add replacement to buffer */
+ if (ms.add_value(lbuf, soffset, res, repl, globals?.debuglib)) changed = true
lastmatch = res
soffset = lastmatch
} else if (soffset < srclen) /* otherwise, skip one character */
@@ -561,6 +869,7 @@ open class StringLib
else break /* end of subject */
if (anchor) break
}
+ if (!changed) return (varargsOf(src, valueOf(n)))!!
lbuf.append(src.substring(soffset, srclen))
return (varargsOf(lbuf.tostring(), valueOf(n)))!!
}
@@ -611,16 +920,39 @@ open class StringLib
*
* Returns a string that is the concatenation of n copies of the string s.
*/
+ /**
+ * `string.rep (s, n [, sep])`.
+ *
+ * The separator, from Lua 5.2, goes between copies and not around them, so
+ * `("x"):rep(3, "-")` is `"x-x-x"`.
+ */
internal class rep : VarArgFunction() {
override fun invoke(args: Varargs): Varargs {
val s: LuaString = args.checkstring(1)
- val n: Int = args.checkint(2)
- val bytes = ByteArray(s.length() * n)
+ val n: Long = args.checklong(2)
+ val sep: LuaString = if (args.isnoneornil(3)) EMPTYSTRING!! else args.checkstring(3)
+ if (n <= 0L) return EMPTYSTRING!!
val len: Int = s.length()
+ val seplen: Int = sep.length()
+ // Checked by division rather than by multiplying out, which would
+ // wrap around and let an impossible size look acceptable.
+ val perCopy: Long = len.toLong() + seplen.toLong()
+ if (perCopy != 0L && n > Int.MAX_VALUE.toLong() / perCopy) {
+ LuaValue.error("resulting string too large")
+ }
+ val total: Long = len.toLong() * n + seplen.toLong() * (n - 1)
+ if (total > Int.MAX_VALUE.toLong()) LuaValue.error("resulting string too large")
+ val bytes = ByteArray(total.toInt())
var offset = 0
- while (offset < bytes.size) {
+ var copies = 0L
+ while (copies < n) {
+ if (copies > 0 && seplen > 0) {
+ sep.copyInto(0, bytes, offset, seplen)
+ offset += seplen
+ }
s.copyInto(0, bytes, offset, len)
offset += len
+ copies++
}
return LuaString.valueUsing(bytes)
}
@@ -743,30 +1075,49 @@ open class StringLib
}
}
- fun add_value(lbuf: Buffer, soffset: Int, end: Int, repl: LuaValue) {
+ /**
+ * Appends the replacement for one match.
+ *
+ * @return true when something was actually replaced; a function or
+ * table that answers nil or false leaves the matched text as it was
+ */
+ @kotlin.jvm.JvmOverloads
+ fun add_value(
+ lbuf: Buffer,
+ soffset: Int,
+ end: Int,
+ repl: LuaValue,
+ debuglib: DebugLib? = null,
+ ): Boolean {
var repl: LuaValue = repl
when (repl.type()) {
LuaValue.TSTRING, LuaValue.TNUMBER -> {
add_s(lbuf, (repl.strvalue())!!, soffset, end)
- return
+ return true
}
- LuaValue.TFUNCTION -> repl = repl.invoke(push_captures(true, soffset, end))!!.arg1()
+ // Through the library's own way of calling back, so that a
+ // function of the library's own can be named in an error.
+ LuaValue.TFUNCTION -> repl =
+ callback(debuglib, repl, push_captures(true, soffset, end)!!).arg1()!!
LuaValue.TTABLE -> // Need to call push_onecapture here for the error checking
repl = repl.get(push_onecapture(0, soffset, end))
else -> {
error("bad argument: string/function/table expected")
- return
+ return false
}
}
if (!repl.toboolean()) {
- repl = s.substring(soffset, end)
- } else if (!repl.isstring()) {
+ lbuf.append(s.substring(soffset, end))
+ return false
+ }
+ if (!repl.isstring()) {
error("invalid replacement value (a " + repl.typename() + ")")
}
lbuf.append((repl.strvalue())!!)
+ return true
}
fun push_captures(wholeMatch: Boolean, soff: Int, end: Int): Varargs {
@@ -1077,6 +1428,72 @@ open class StringLib
}
companion object {
+ /**
+ * The identity `%p` reports, or `(null)` for a value that has none.
+ *
+ * Only the reference types have one; a number, string, boolean or nil
+ * is its own value rather than something living at an address.
+ */
+ fun pointer(value: LuaValue): LuaString {
+ return when (value.type()) {
+ // A string has an identity of its own, and two equal strings
+ // need not share it, so the bytes behind it are what answers.
+ LuaValue.TSTRING -> LuaString.valueOf(
+ "0x" + (value as LuaString).m_bytes.hashCode().toString(16),
+ )
+
+ LuaValue.TTABLE, LuaValue.TFUNCTION, LuaValue.TTHREAD, LuaValue.TUSERDATA -> {
+ val rendered: String = value.tojstring()
+ LuaString.valueOf("0x" + rendered.substringAfter(": ", rendered))
+ }
+
+ else -> LuaString.valueOf("(null)")
+ }
+ }
+
+ /**
+ * `%q`: writes [value] so that reading it back gives the same value.
+ *
+ * A string is quoted and escaped; a float goes out in hexadecimal so no
+ * digits are lost, with the values that have no literal - the
+ * infinities and NaN - written as expressions that produce them.
+ */
+ fun addliteral(buf: Buffer, value: LuaValue) {
+ when (value.type()) {
+ LuaValue.TSTRING -> {
+ net.blueva.luak.lib.StringLib.Companion.addquoted(buf, value.checkstring()!!)
+ return
+ }
+
+ LuaValue.TNUMBER -> {
+ if (value.isinttype()) {
+ val n: Long = value.tolong()
+ // The minimum integer has no positive literal to negate,
+ // so it is written in hexadecimal.
+ buf.append(if (n == Long.MIN_VALUE) "0x8000000000000000" else n.toString())
+ } else {
+ val d: Double = value.todouble()
+ buf.append(
+ when {
+ d.isNaN() -> "(0/0)"
+ d == Double.POSITIVE_INFINITY -> "1e9999"
+ d == Double.NEGATIVE_INFINITY -> "-1e9999"
+ else -> net.blueva.luak.DecimalFormat.hex(d, upper = false)
+ },
+ )
+ }
+ return
+ }
+
+ LuaValue.TNIL, LuaValue.TBOOLEAN -> {
+ buf.append(value.tojstring())
+ return
+ }
+
+ else -> LuaValue.error("value has no literal form")
+ }
+ }
+
fun addquoted(buf: Buffer, s: LuaString) {
var c: Int
buf.append('"'.code.toByte())
@@ -1118,7 +1535,10 @@ open class StringLib
var init: Int = args.optint(3, 1)
if (init > 0) {
- init = minOf(init - 1, s.length())
+ // Starting past the end finds nothing, not even the empty
+ // pattern: there is no position there to match at.
+ if (init > s.length() + 1) return (if (find) NIL else NIL)!!
+ init -= 1
} else if (init < 0) {
init = maxOf(0, s.length() + init)
}
@@ -1163,6 +1583,16 @@ open class StringLib
// Pattern matching implementation
private val L_ESC: Int = '%'.code
+
+ /**
+ * The bytes a conversion specification may hold before its letter.
+ *
+ * Flags, width and precision all live in here, which is why a run of
+ * six zeros is a long specification rather than a repeated flag.
+ */
+ internal fun isSpecSpan(byte: Int): Boolean =
+ byte == '-'.code || byte == '+'.code || byte == '#'.code || byte == '0'.code ||
+ byte == ' '.code || byte == '.'.code || (byte >= '1'.code && byte <= '9'.code)
private val SPECIALS: LuaString? = valueOf("^$*+?.([%-")
private const val MAX_CAPTURES = 32
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/StringPack.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/StringPack.kt
new file mode 100644
index 00000000..b9e49378
--- /dev/null
+++ b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/lib/StringPack.kt
@@ -0,0 +1,448 @@
+/******************************************************************************
+ * ____ _ _ _ __
+ * | __ )| |_ _ ___| | _ _ __ _| |/ /
+ * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
+ * | |_) | | |_| | __/ |__| |_| | (_| | . \
+ * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
+ *
+ * BlueLuaK
+ * https://github.com/BluevaDevelopment/BlueLuaK
+ *
+ * Copyright (c) 2026 Blueva Development
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+package net.blueva.luak.lib
+
+import net.blueva.luak.LuaString
+import net.blueva.luak.LuaValue
+import net.blueva.luak.Varargs
+
+/**
+ * `string.pack`, `string.unpack`, and `string.packsize`, from Lua 5.3.
+ *
+ * These read and write the binary layouts a C program would produce, driven by
+ * a format string: `"i4"` is a four-byte integer, `"= text.length()
+ fun peek(): Int = if (atEnd()) -1 else text.luaByte(index)
+ fun next(): Int = text.luaByte(index++)
+ }
+
+ /** Bytes being assembled by `pack`. */
+ private class Packed {
+ var bytes: ByteArray = ByteArray(32)
+ var length: Int = 0
+
+ fun add(b: Byte) {
+ if (length == bytes.size) bytes = bytes.copyOf(bytes.size * 2)
+ bytes[length++] = b
+ }
+
+ fun add(value: LuaString) {
+ for (i in 0.. Int.MAX_VALUE.toLong() - total - option.toalign) {
+ args.argcheck(false, 1, "result too long")
+ }
+ total += option.toalign + option.size
+ out.pad(option.toalign)
+ arg++
+ when (option.kind) {
+ Kind.INT -> {
+ val width: Int = option.size.toInt()
+ val n: Long = args.checklong(arg)
+ if (width < LUA_INTEGER_SIZE) {
+ val limit: Long = 1L shl (width * 8 - 1)
+ args.argcheck(-limit <= n && n < limit, arg, "integer overflow")
+ }
+ packInteger(out, n, header.little, width, n < 0)
+ }
+
+ Kind.UINT -> {
+ val width: Int = option.size.toInt()
+ val n: Long = args.checklong(arg)
+ if (width < LUA_INTEGER_SIZE) {
+ val limit: Long = 1L shl (width * 8)
+ args.argcheck(n >= 0 && n < limit, arg, "unsigned overflow")
+ }
+ packInteger(out, n, header.little, width, false)
+ }
+
+ Kind.FLOAT -> packBits(
+ out,
+ (args.checkdouble(arg).toFloat().toRawBits().toLong() and 0xFFFFFFFFL),
+ header.little,
+ 4,
+ )
+
+ Kind.NUMBER, Kind.DOUBLE ->
+ packBits(out, args.checkdouble(arg).toRawBits(), header.little, 8)
+
+ Kind.CHAR -> {
+ val s: LuaString = args.checkstring(arg)!!
+ args.argcheck(s.length() <= option.size, arg, "string longer than given size")
+ // A size no buffer could hold is refused before anything is
+ // written, rather than after trying to pad to it.
+ args.argcheck(option.size <= Int.MAX_VALUE.toLong(), 1, "result too long")
+ out.add(s)
+ out.pad((option.size - s.length()).toInt())
+ }
+
+ Kind.STRING -> {
+ val s: LuaString = args.checkstring(arg)!!
+ val width: Int = option.size.toInt()
+ args.argcheck(
+ width >= LUA_INTEGER_SIZE ||
+ s.length().toLong() < (1L shl (width * 8)),
+ arg,
+ "string length does not fit in given size",
+ )
+ packInteger(out, s.length().toLong(), header.little, width, false)
+ out.add(s)
+ total += s.length()
+ }
+
+ Kind.ZSTR -> {
+ val s: LuaString = args.checkstring(arg)!!
+ args.argcheck(s.indexOf(0.toByte(), 0) < 0, arg, "string contains zeros")
+ out.add(s)
+ out.add(0)
+ total += s.length() + 1
+ }
+
+ Kind.PADDING -> {
+ out.add(PAD_BYTE)
+ arg--
+ }
+
+ Kind.PADDALIGN, Kind.NOP -> arg--
+ }
+ }
+ return out.result()
+ }
+
+ fun packsize(args: Varargs): Varargs {
+ val format = Format(args.checkstring(1)!!)
+ val header = Header()
+ var total = 0L
+ while (!format.atEnd()) {
+ val option: Option = details(header, total, format)
+ args.argcheck(
+ option.kind != Kind.STRING && option.kind != Kind.ZSTR,
+ 1,
+ "variable-length format",
+ )
+ // The total is checked as it grows: two sizes that each fit can
+ // still add up to something no size can name.
+ if (option.size > Long.MAX_VALUE - total - option.toalign) {
+ args.argcheck(false, 1, "format result too large")
+ }
+ total += option.toalign + option.size
+ }
+ return LuaValue.valueOf(total)
+ }
+
+ fun unpack(args: Varargs): Varargs {
+ val format = Format(args.checkstring(1)!!)
+ val data: LuaString = args.checkstring(2)!!
+ val length: Int = data.length()
+ var position: Int = positionOf(args.optlong(3, 1L), length)
+ args.argcheck(position <= length, 3, "initial position out of string")
+ val header = Header()
+ val results: ArrayList = ArrayList()
+ while (!format.atEnd()) {
+ val option: Option = details(header, position.toLong(), format)
+ args.argcheck(
+ option.toalign.toLong() + option.size <= (length - position).toLong(),
+ 2,
+ "data string too short",
+ )
+ position += option.toalign
+ val width: Int = if (option.size <= Int.MAX_VALUE.toLong()) option.size.toInt() else 0
+ when (option.kind) {
+ Kind.INT, Kind.UINT -> results.add(
+ LuaValue.valueOf(
+ unpackInteger(data, position, header.little, width, option.kind == Kind.INT),
+ ),
+ )
+
+ Kind.FLOAT -> results.add(
+ LuaValue.valueOf(
+ Float.fromBits(unpackBits(data, position, header.little, 4).toInt()).toDouble(),
+ ),
+ )
+
+ Kind.NUMBER, Kind.DOUBLE -> results.add(
+ LuaValue.valueOf(Double.fromBits(unpackBits(data, position, header.little, 8))),
+ )
+
+ Kind.CHAR -> results.add(data.substring(position, position + width))
+
+ Kind.STRING -> {
+ val size: Long = unpackInteger(data, position, header.little, width, false)
+ args.argcheck(
+ size >= 0 && size <= (length - position - width).toLong(),
+ 2,
+ "data string too short",
+ )
+ val start: Int = position + width
+ results.add(data.substring(start, start + size.toInt()))
+ position += size.toInt()
+ }
+
+ Kind.ZSTR -> {
+ val end: Int = data.indexOf(0.toByte(), position)
+ args.argcheck(end >= 0, 2, "unfinished string for format 'z'")
+ results.add(data.substring(position, end))
+ position = end + 1
+ }
+
+ Kind.PADDALIGN, Kind.PADDING, Kind.NOP -> {}
+ }
+ position += width
+ }
+ results.add(LuaValue.valueOf((position + 1).toLong()))
+ return LuaValue.varargsOf(results.toTypedArray())!!
+ }
+
+ /** Turns a possibly negative or zero index into a one-based offset. */
+ private fun positionOf(position: Long, length: Int): Int {
+ if (position > 0) return (position - 1).toInt()
+ if (position == 0L) return 0
+ return if (-position > length) 0 else (length + position).toInt()
+ }
+
+ /** Classifies the next option and works out the padding it needs. */
+ private fun details(header: Header, total: Long, format: Format): Option {
+ val classified: Pair = option(header, format)
+ val kind: Kind = classified.first
+ val size: Long = classified.second
+ var align: Long = size
+ if (kind == Kind.PADDALIGN) {
+ // 'X' has no size of its own: it takes its alignment from whatever
+ // option comes next, which is then discarded.
+ if (format.atEnd()) LuaValue.Companion.argerror(1, "invalid next option for option 'X'")
+ val following: Pair = option(header, format)
+ align = following.second
+ if (following.first == Kind.CHAR || align == 0L) {
+ LuaValue.Companion.argerror(1, "invalid next option for option 'X'")
+ }
+ }
+ if (align <= 1 || kind == Kind.CHAR) return Option(kind, size, 0)
+ if (align > header.maxalign) align = header.maxalign.toLong()
+ if (align and (align - 1) != 0L) {
+ LuaValue.Companion.argerror(1, "format asks for alignment not power of 2")
+ }
+ val over: Long = total and (align - 1)
+ return Option(kind, size, ((align - over) and (align - 1)).toInt())
+ }
+
+ /** Reads one option letter and whatever size numeral follows it. */
+ private fun option(header: Header, format: Format): Pair {
+ when (format.next()) {
+ 'b'.code -> return Pair(Kind.INT, 1L)
+ 'B'.code -> return Pair(Kind.UINT, 1L)
+ 'h'.code -> return Pair(Kind.INT, 2L)
+ 'H'.code -> return Pair(Kind.UINT, 2L)
+ 'l'.code, 'j'.code -> return Pair(Kind.INT, 8L)
+ 'L'.code, 'J'.code, 'T'.code -> return Pair(Kind.UINT, 8L)
+ 'f'.code -> return Pair(Kind.FLOAT, 4L)
+ 'n'.code -> return Pair(Kind.NUMBER, 8L)
+ 'd'.code -> return Pair(Kind.DOUBLE, 8L)
+ 'i'.code -> return Pair(Kind.INT, limitedNumeral(format, 4).toLong())
+ 'I'.code -> return Pair(Kind.UINT, limitedNumeral(format, 4).toLong())
+ 's'.code -> return Pair(Kind.STRING, limitedNumeral(format, 8).toLong())
+ 'c'.code -> {
+ val size: Long = numeral(format, -1L)
+ if (size < 0) LuaValue.Companion.error("missing size for format option 'c'")
+ return Pair(Kind.CHAR, size)
+ }
+
+ 'z'.code -> return Pair(Kind.ZSTR, 0L)
+ 'x'.code -> return Pair(Kind.PADDING, 1L)
+ 'X'.code -> return Pair(Kind.PADDALIGN, 0L)
+ ' '.code -> return Pair(Kind.NOP, 0L)
+ '<'.code -> {
+ header.little = true
+ return Pair(Kind.NOP, 0L)
+ }
+
+ '>'.code -> {
+ header.little = false
+ return Pair(Kind.NOP, 0L)
+ }
+
+ '='.code -> {
+ header.little = true
+ return Pair(Kind.NOP, 0L)
+ }
+
+ '!'.code -> {
+ header.maxalign = limitedNumeral(format, MAX_ALIGNMENT)
+ return Pair(Kind.NOP, 0L)
+ }
+
+ else -> {
+ val letter: Char = format.text.luaByte(format.index - 1).toChar()
+ LuaValue.Companion.error("invalid format option '" + letter + "'")
+ return Pair(Kind.NOP, 0L)
+ }
+ }
+ }
+
+ /** A decimal numeral in the format string, or [default] if there is none. */
+ private fun numeral(format: Format, default: Long): Long {
+ if (format.peek() < '0'.code || format.peek() > '9'.code) return default
+ var value = 0L
+ while (!format.atEnd() && format.peek() >= '0'.code && format.peek() <= '9'.code) {
+ val digit: Int = format.next() - '0'.code
+ value = value * 10 + digit
+ // Stops once another digit could not fit, leaving the rest of the
+ // numeral in the format - where it is read as an option and
+ // reported as the invalid one it is.
+ if (value > (Long.MAX_VALUE - 9) / 10) break
+ }
+ return value
+ }
+
+ /** A numeral that names an integer width, which has a hard upper bound. */
+ private fun limitedNumeral(format: Format, default: Int): Int {
+ val size: Long = numeral(format, default.toLong())
+ if (size < 1 || size > MAX_INTEGER_SIZE) {
+ LuaValue.Companion.error(
+ "integral size (" + size + ") out of limits [1," + MAX_INTEGER_SIZE + "]",
+ )
+ }
+ return size.toInt()
+ }
+
+ /** Writes [n] over [size] bytes, sign-extending past a Lua integer. */
+ private fun packInteger(out: Packed, n: Long, little: Boolean, size: Int, negative: Boolean) {
+ val bytes = ByteArray(size)
+ var value = n
+ for (i in 0.. LUA_INTEGER_SIZE) {
+ for (i in LUA_INTEGER_SIZE.. LUA_INTEGER_SIZE) {
+ val fill: Int = if (!signed || value >= 0) 0 else 0xFF
+ for (i in limit.. string
- internal class concat : TableLibFunction() {
- override fun call(list: LuaValue?): LuaValue? {
- return list!!.checktable()!!.concat(EMPTYSTRING, 1, list!!.length())
+ /**
+ * `table.concat (list [, sep [, i [, j]]])`.
+ *
+ * Written against the argument list rather than against fixed arities, so
+ * a bad index is reported with its position: "bad argument #3 to
+ * 'table.concat'" rather than a message that says only what was wrong.
+ * Anything indexable will do, as upstream allows, and an element that is
+ * neither a string nor a number names its own index.
+ */
+ internal class concat : VarArgFunction() {
+ /** Appends `list[index]`, refusing anything that is not a string. */
+ private fun addfield(out: Buffer, list: LuaValue, index: Long) {
+ val element: LuaValue = list.get(LuaValue.valueOf(index))
+ if (!element.isstring()) {
+ LuaValue.error(
+ "invalid value (" + element.typename() +
+ ") at index " + index + " in table for 'concat'",
+ )
+ }
+ out.append(element.strvalue()!!)
}
- override fun call(list: LuaValue?, sep: LuaValue?): LuaValue? {
- return list!!.checktable()!!.concat(sep!!.checkstring(), 1, list!!.length())
+ override fun invoke(args: Varargs): Varargs {
+ val list: LuaValue = checkindexable(args)
+ val separator: LuaString = if (args.isnoneornil(2)) EMPTYSTRING!! else args.checkstring(2)
+ val first: Long = args.optlong(3, 1L)
+ val last: Long = if (args.isnoneornil(4)) list.length().toLong() else args.checklong(4)
+ val out: Buffer = Buffer()
+ var index: Long = first
+ // The last element is added outside the loop, so the counter never
+ // has to step past it: with a range ending at math.maxinteger,
+ // one more increment would wrap around and read the table again.
+ while (index < last) {
+ addfield(out, list, index)
+ out.append(separator)
+ index++
+ }
+ if (index == last) addfield(out, list, index)
+ return out.tostring()
}
+ }
- override fun call(list: LuaValue?, sep: LuaValue?, i: LuaValue?): LuaValue? {
- return list!!.checktable()!!.concat(sep!!.checkstring(), i!!.checkint(), list!!.length())
+ // "insert" (table, [pos,] value)
+ /**
+ * `table.create (nseq [, nrec])`, from Lua 5.5.
+ *
+ * Answers an empty table sized in advance for `nseq` entries in its array
+ * part and `nrec` in its hash part. The sizes are a hint about what is
+ * about to be put in, not content: the table starts empty either way.
+ */
+ internal class create : VarArgFunction() {
+ override fun invoke(args: Varargs): Varargs {
+ val sequence: Long = args.checklong(1)
+ val records: Long = args.optlong(2, 0L)
+ args.argcheck(sequence >= 0 && sequence <= Int.MAX_VALUE, 1, "out of range")
+ args.argcheck(records >= 0 && records <= Int.MAX_VALUE, 2, "out of range")
+ return LuaTable(sequence.toInt(), records.toInt())
}
+ }
- override fun call(list: LuaValue?, sep: LuaValue?, i: LuaValue?, j: LuaValue?): LuaValue? {
- return list!!.checktable()!!.concat(sep!!.checkstring(), i!!.checkint(), j!!.checkint())
+ /**
+ * `table.move (a1, f, e, t [,a2])`, from Lua 5.3.
+ *
+ * Moves `a1[f..e]` to `a2[t..]`, answering `a2`. Source and destination may
+ * be the same table and may overlap, so the direction of the copy is chosen
+ * to keep the elements that have not been read yet.
+ */
+ internal class move : VarArgFunction() {
+ override fun invoke(args: Varargs): Varargs {
+ val source: LuaValue = args.checktable(1)!!
+ val from: Long = args.checklong(2)
+ val to: Long = args.checklong(3)
+ val target: Long = args.checklong(4)
+ val destination: LuaValue = if (args.isnoneornil(5)) source else args.checktable(5)!!
+ if (to >= from) {
+ argcheck(
+ from > 0 || to < Long.MAX_VALUE + from,
+ 3,
+ "too many elements to move",
+ )
+ val count: Long = to - from + 1
+ argcheck(target <= Long.MAX_VALUE - count + 1, 4, "destination wrap around")
+ // Copy backwards when the ranges overlap forwards, so a source
+ // element is never overwritten before it has been read.
+ if (target > from && target <= to && source === destination) {
+ var i: Long = count - 1
+ while (i >= 0) {
+ destination.set(
+ LuaValue.valueOf(target + i),
+ source.get(LuaValue.valueOf(from + i)),
+ )
+ i--
+ }
+ } else {
+ var i = 0L
+ while (i < count) {
+ destination.set(
+ LuaValue.valueOf(target + i),
+ source.get(LuaValue.valueOf(from + i)),
+ )
+ i++
+ }
+ }
+ }
+ return destination
}
}
- // "insert" (table, [pos,] value)
internal class insert : VarArgFunction() {
override fun invoke(args: Varargs): Varargs {
+ val list: LuaValue = checkindexable(args, writable = true)
+ // The first free slot. A length of math.maxinteger leaves no room
+ // for one more, and the count wraps round rather than overflowing,
+ // which is what Lua does here.
+ val empty: Long = lengthofValue(list) + 1L
+ val pos: Long
when (args.narg()) {
- 2 -> {
- val table: LuaTable = args.checktable(1)
- table.insert(table.length() + 1, (args.arg(2))!!)
- return (NONE)!!
- }
+ 2 -> pos = empty
3 -> {
- val table: LuaTable = args.checktable(1)
- val pos: Int = args.checkint(2)
- val max: Int = table.length() + 1
- if (pos < 1 || pos > max) argerror(
+ pos = args.checklong(2)
+ // Read unsigned, so a position of zero or a negative one
+ // is out of bounds without a separate test.
+ args.argcheck(
+ (pos - 1L).toULong() < empty.toULong(),
2,
- "position out of bounds: " + pos + " not between 1 and " + max
+ "position out of bounds",
)
- table.insert(pos, (args.arg(3))!!)
- return (NONE)!!
+ var index: Long = empty
+ while (index > pos) {
+ list.set(LuaValue.valueOf(index), list.get(LuaValue.valueOf(index - 1L)))
+ index--
+ }
}
- else -> {
- return (error("wrong number of arguments to 'table.insert': " + args.narg() + " (must be 2 or 3)"))!!
- }
+ else -> return (error("wrong number of arguments to 'insert'"))!!
}
+ list.set(LuaValue.valueOf(pos), (args.arg(args.narg()))!!)
+ return (NONE)!!
}
}
@@ -145,10 +244,11 @@ class TableLib : TwoArgFunction() {
}
// "sort" (table [, comp])
- internal class sort : VarArgFunction() {
+ internal class sort(private val globals: net.blueva.luak.Globals?) : VarArgFunction() {
override fun invoke(args: Varargs): Varargs {
args.checktable(1).sort(
- if (args.isnil(2)) NIL else args.checkfunction(2)
+ if (args.isnil(2)) NIL else args.checkfunction(2),
+ globals?.debuglib,
)
return (NONE)!!
}
@@ -156,12 +256,75 @@ class TableLib : TwoArgFunction() {
// "unpack", // (list [,i [,j]]) -> result1, ...
+ /**
+ * `table.unpack (list [, i [, j]])`.
+ *
+ * The list only has to be indexable, not a table, which is what lets
+ * `table.unpack(s, i, j)` read through an `__index` rather than only from
+ * a table's own array part.
+ */
internal class unpack : VarArgFunction() {
override fun invoke(args: Varargs): Varargs {
- val t: LuaTable = args.checktable(1)
- // do not waste resource for calc rawlen if arg3 is not nil
- val len = if (args.arg(3)!!.isnil()) t.length() else 0
- return t.unpack(args.optint(2, 1), args.optint(3, len))
+ val list: LuaValue = checkindexable(args)
+ val first: Long = args.optlong(2, 1L)
+ // Only work out the length when it is going to be used as the end.
+ val last: Long = if (args.isnoneornil(3)) list.length().toLong() else args.checklong(3)
+ if (last < first) return NONE!!
+ val count: Long = last - first + 1
+ // The cap is the stack size Lua allows, and asking for exactly
+ // that many leaves no room for the call itself, so it is refused
+ // too.
+ if (count <= 0 || count >= MAX_UNPACK) LuaValue.error("too many results to unpack")
+ val out: Array = arrayOfNulls(count.toInt())
+ for (offset in 0..()
+ for (i in 1..args.narg()) {
+ encode(args.checklong(i), bytes, i)
+ }
+ return LuaString.valueUsing(bytes.toByteArray())
+ }
+ }
+
+ /** `utf8.codepoint(s [, i [, j [, lax]]])`. */
+ internal class codepoint : VarArgFunction() {
+ override fun invoke(args: Varargs): Varargs {
+ val s: LuaString = args.checkstring(1)!!
+ val length: Int = s.m_length
+ val first: Int = position(args.optint(2, 1), length, 1)
+ val last: Int = position(args.optint(3, first), length, 1)
+ val lax: Boolean = args.optboolean(4, false)
+ if (first < 1) LuaValue.argerror(2, "out of bounds")
+ if (last > length) LuaValue.argerror(3, "out of bounds")
+
+ val points = ArrayList()
+ var at = first
+ while (at <= last) {
+ val decoded: Long = decode(s, at, lax)
+ ?: LuaValue.error("invalid UTF-8 code").let { return NONE!! }
+ points.add(LuaValue.valueOf(decoded))
+ at += sequenceLength(s, at)
+ }
+ return LuaValue.varargsOf(points.toTypedArray())!!
+ }
+ }
+
+ /** `utf8.len(s [, i [, j [, lax]]])`, answering nil plus a position on failure. */
+ internal class len : VarArgFunction() {
+ override fun invoke(args: Varargs): Varargs {
+ val s: LuaString = args.checkstring(1)!!
+ val length: Int = s.m_length
+ // Zero is out of bounds rather than a stand-in for one: a
+ // position counts from 1, or from the end when negative.
+ val first: Int = position(args.optint(2, 1), length, 0)
+ val last: Int = position(args.optint(3, -1), length, 0)
+ val lax: Boolean = args.optboolean(4, false)
+ if (first < 1 || first > length + 1) LuaValue.argerror(2, "initial position out of bounds")
+ if (last > length) LuaValue.argerror(3, "final position out of bounds")
+
+ var count = 0
+ var at = first
+ while (at <= last) {
+ if (decode(s, at, lax) == null) {
+ return LuaValue.varargsOf(NIL, LuaValue.valueOf(at.toLong()))!!
+ }
+ at += sequenceLength(s, at)
+ count++
+ }
+ return LuaValue.valueOf(count.toLong())
+ }
+ }
+
+ /** `utf8.offset(s, n [, i])`, returning the start and end of the encoding. */
+ internal class offset : VarArgFunction() {
+ override fun invoke(args: Varargs): Varargs {
+ val s: LuaString = args.checkstring(1)!!
+ val length: Int = s.m_length
+ val n: Int = args.checkint(2)
+ val default: Int = if (n >= 0) 1 else length + 1
+ var at: Int = position(args.optint(3, default), length, default)
+ if (at < 1 || at > length + 1) LuaValue.argerror(3, "position out of bounds")
+
+ var remaining = n
+ if (remaining == 0) {
+ // Back up to the start of whatever character contains byte i.
+ while (at > 1 && isContinuation(s, at)) at--
+ return span(s, at, length)
+ }
+ if (remaining > 0) {
+ if (isContinuation(s, at)) LuaValue.error("initial position is a continuation byte")
+ remaining--
+ while (remaining > 0 && at <= length) {
+ at++
+ while (at <= length && isContinuation(s, at)) at++
+ remaining--
+ }
+ if (remaining > 0) return NIL
+ return span(s, at, length)
+ }
+ if (at <= length && isContinuation(s, at)) LuaValue.error("initial position is a continuation byte")
+ while (remaining < 0 && at > 1) {
+ at--
+ while (at > 1 && isContinuation(s, at)) at--
+ remaining++
+ }
+ if (remaining < 0) return NIL
+ return span(s, at, length)
+ }
+
+ private fun span(s: LuaString, start: Int, length: Int): Varargs {
+ // Landing on a continuation byte means the walk went past the start
+ // of the string and there is no character here to report.
+ if (start in 1..length && isContinuation(s, start)) {
+ LuaValue.error("initial position is a continuation byte")
+ }
+ // The end is found by following the continuation bytes that are
+ // actually there, not by trusting the length the lead byte claims:
+ // a truncated sequence reports what the string does contain.
+ var end: Int = start
+ if (start <= length) {
+ while (end + 1 <= length && isContinuation(s, end + 1)) end++
+ }
+ return LuaValue.varargsOf(LuaValue.valueOf(start.toLong()), LuaValue.valueOf(end.toLong()))!!
+ }
+ }
+
+ /** `utf8.codes(s [, lax])`, returning the iterator triple. */
+ internal class codes : VarArgFunction() {
+ override fun invoke(args: Varargs): Varargs {
+ val s: LuaString = args.checkstring(1)!!
+ val lax: Boolean = args.optboolean(2, false)
+ // A string that begins mid-sequence has no first character to
+ // report, so the mistake is in the argument rather than in the
+ // iteration that would follow.
+ args.argcheck(s.m_length == 0 || !isContinuation(s, 1), 1, "invalid UTF-8 code")
+ return LuaValue.varargsOf(iterator(lax), s, LuaValue.valueOf(0L))!!
+ }
+ }
+
+ /** The stateless iterator `utf8.codes` hands back. */
+ internal class iterator(private val lax: Boolean) : VarArgFunction() {
+ override fun invoke(args: Varargs): Varargs {
+ val s: LuaString = args.checkstring(1)!!
+ val length: Int = s.m_length
+ var at: Int = args.checkint(2)
+ // Skip the character the previous step reported.
+ if (at > 0) {
+ at++
+ while (at <= length && isContinuation(s, at)) at++
+ } else {
+ at = 1
+ }
+ if (at > length) return NIL
+ val decoded: Long = decode(s, at, lax)
+ ?: LuaValue.error("invalid UTF-8 code").let { return NONE!! }
+ // A continuation byte where the next character should start means
+ // the sequence just read was followed by stray bytes.
+ val after: Int = at + sequenceLength(s, at)
+ if (after <= length && isContinuation(s, after)) LuaValue.error("invalid UTF-8 code")
+ return LuaValue.varargsOf(LuaValue.valueOf(at.toLong()), LuaValue.valueOf(decoded))!!
+ }
+ }
+
+ companion object {
+ /**
+ * Matches exactly one UTF-8 sequence in a well-formed subject:
+ * `"[\0-\x7F\xC2-\xFD][\x80-\xBF]*"`.
+ *
+ * Built from raw bytes rather than from a Kotlin string, because the
+ * bytes above 0x7F would otherwise be UTF-8 encoded into two bytes each
+ * and the pattern would not match what upstream's does.
+ */
+ val CHAR_PATTERN: LuaString = LuaString.valueUsing(
+ byteArrayOf(
+ '['.code.toByte(), 0x00, '-'.code.toByte(), 0x7F.toByte(),
+ 0xC2.toByte(), '-'.code.toByte(), 0xFD.toByte(), ']'.code.toByte(),
+ '['.code.toByte(), 0x80.toByte(), '-'.code.toByte(), 0xBF.toByte(),
+ ']'.code.toByte(), '*'.code.toByte(),
+ ),
+ )
+
+ private const val MAX_STRICT: Long = 0x10FFFF
+ private const val MAX_LAX: Long = 0x7FFFFFFF
+
+ /** Byte at one-based [at], or -1 past the end. */
+ private fun byteAt(s: LuaString, at: Int): Int =
+ if (at < 1 || at > s.m_length) -1 else s.m_bytes[s.m_offset + at - 1].toInt() and 0xFF
+
+ private fun isContinuation(s: LuaString, at: Int): Boolean {
+ val b: Int = byteAt(s, at)
+ return b in 0x80..0xBF
+ }
+
+ /** Bytes in the sequence starting at [at]; 1 for anything ill-formed. */
+ internal fun sequenceLength(s: LuaString, at: Int): Int {
+ val b: Int = byteAt(s, at)
+ return when {
+ b < 0x80 -> 1
+ b < 0xC0 -> 1
+ b < 0xE0 -> 2
+ b < 0xF0 -> 3
+ b < 0xF8 -> 4
+ b < 0xFC -> 5
+ else -> 6
+ }
+ }
+
+ /** The code point starting at [at], or null if the sequence is invalid. */
+ internal fun decode(s: LuaString, at: Int, lax: Boolean): Long? {
+ val first: Int = byteAt(s, at)
+ if (first < 0) return null
+ if (first < 0x80) return first.toLong()
+ if (first < 0xC0) return null // a continuation byte cannot start a sequence
+ val count: Int = sequenceLength(s, at)
+ var value: Long = (first and (0x7F shr count)).toLong()
+ for (offset in 1 until count) {
+ val next: Int = byteAt(s, at + offset)
+ if (next < 0x80 || next > 0xBF) return null
+ value = (value shl 6) or (next and 0x3F).toLong()
+ }
+ val limit: Long = if (lax) MAX_LAX else MAX_STRICT
+ if (value > limit) return null
+ if (!lax && value in 0xD800..0xDFFF) return null // surrogates
+ if (count > 1 && value < MINIMUM[count]) return null // overlong encoding
+ return value
+ }
+
+ /** Smallest code point each sequence length is allowed to encode. */
+ private val MINIMUM = longArrayOf(0, 0, 0x80, 0x800, 0x10000, 0x200000, 0x4000000)
+
+ /** Appends the UTF-8 encoding of [value] to [out]. */
+ internal fun encode(value: Long, out: MutableList, argument: Int) {
+ if (value < 0 || value > MAX_LAX) LuaValue.argerror(argument, "value out of range")
+ when {
+ value < 0x80 -> out.add(value.toByte())
+ value < 0x800 -> {
+ out.add((0xC0 or (value ushr 6).toInt()).toByte())
+ out.add(continuation(value, 0))
+ }
+
+ value < 0x10000 -> {
+ out.add((0xE0 or (value ushr 12).toInt()).toByte())
+ out.add(continuation(value, 6))
+ out.add(continuation(value, 0))
+ }
+
+ value < 0x200000 -> {
+ out.add((0xF0 or (value ushr 18).toInt()).toByte())
+ out.add(continuation(value, 12))
+ out.add(continuation(value, 6))
+ out.add(continuation(value, 0))
+ }
+
+ value < 0x4000000 -> {
+ out.add((0xF8 or (value ushr 24).toInt()).toByte())
+ out.add(continuation(value, 18))
+ out.add(continuation(value, 12))
+ out.add(continuation(value, 6))
+ out.add(continuation(value, 0))
+ }
+
+ else -> {
+ out.add((0xFC or (value ushr 30).toInt()).toByte())
+ out.add(continuation(value, 24))
+ out.add(continuation(value, 18))
+ out.add(continuation(value, 12))
+ out.add(continuation(value, 6))
+ out.add(continuation(value, 0))
+ }
+ }
+ }
+
+ private fun continuation(value: Long, shift: Int): Byte =
+ (0x80 or ((value ushr shift).toInt() and 0x3F)).toByte()
+
+ /** Turns a Lua string position, possibly negative, into a byte index. */
+ internal fun position(given: Int, length: Int, whenZero: Int): Int = when {
+ given > 0 -> given
+ given == 0 -> whenZero
+ -given > length -> 0
+ else -> length + given + 1
+ }
+ }
+}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/parser/LuaAstBuilder.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/parser/LuaAstBuilder.kt
deleted file mode 100644
index 5de22b62..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/parser/LuaAstBuilder.kt
+++ /dev/null
@@ -1,351 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.parser
-
-import net.blueva.luak.Lua
-import net.blueva.luak.LuaValue
-import net.blueva.luak.ast.Block
-import net.blueva.luak.ast.Chunk
-import net.blueva.luak.ast.Exp
-import net.blueva.luak.ast.FuncArgs
-import net.blueva.luak.ast.FuncBody
-import net.blueva.luak.ast.FuncName
-import net.blueva.luak.ast.Name
-import net.blueva.luak.ast.ParList
-import net.blueva.luak.ast.Stat
-import net.blueva.luak.ast.Str
-import net.blueva.luak.ast.SyntaxElement
-import net.blueva.luak.ast.TableConstructor
-import net.blueva.luak.ast.TableField
-import net.blueva.luak.parser.antlr.LuaParser
-import org.antlr.v4.kotlinruntime.ParserRuleContext
-
-internal class LuaAstBuilder {
- fun chunk(ctx: LuaParser.ChunkContext): Chunk =
- located(Chunk(block(ctx.block())), ctx)
-
- private fun block(ctx: LuaParser.BlockContext): Block {
- val result = Block()
- ctx.stat().mapNotNullTo(result.stats, ::stat)
- ctx.retstat()?.let { result.add(retstat(it)) }
- return located(result, ctx)
- }
-
- private fun stat(ctx: LuaParser.StatContext): Stat? {
- ctx.SEMI()?.let { return null }
- ctx.label()?.let { return located(Stat.labelstat(it.NAME().text), ctx) }
- ctx.BREAK()?.let { return located(Stat.breakstat(), ctx) }
- ctx.GOTO()?.let { return located(Stat.gotostat(ctx.NAME()!!.text), ctx) }
- ctx.WHILE()?.let {
- return located(Stat.whiledo(exp(ctx.exp(0)!!), block(ctx.block(0)!!)), ctx)
- }
- ctx.REPEAT()?.let {
- return located(Stat.repeatuntil(block(ctx.block(0)!!), exp(ctx.exp(0)!!)), ctx)
- }
- ctx.IF()?.let {
- val expressions = ctx.exp().map(::exp)
- val blocks = ctx.block().map(::block)
- val elseifCount = ctx.ELSEIF().size
- return located(
- Stat.ifthenelse(
- expressions.first(),
- blocks.first(),
- expressions.drop(1).take(elseifCount).toMutableList(),
- blocks.drop(1).take(elseifCount).toMutableList(),
- if (ctx.ELSE() != null) blocks.last() else null,
- ),
- ctx,
- )
- }
- ctx.FOR()?.let {
- if (ctx.ASSIGN() != null) {
- val expressions = ctx.exp().map(::exp)
- return located(
- Stat.fornumeric(
- ctx.NAME()!!.text,
- expressions[0],
- expressions[1],
- expressions.getOrNull(2),
- block(ctx.block(0)!!),
- ),
- ctx,
- )
- }
- return located(
- Stat.forgeneric(
- namelist(ctx.namelist()!!),
- explist(ctx.explist()!!),
- block(ctx.block(0)!!),
- ),
- ctx,
- )
- }
- ctx.FUNCTION()?.let {
- if (ctx.LOCAL() != null) {
- return located(
- Stat.localfunctiondef(ctx.NAME()!!.text, funcbody(ctx.funcbody()!!)),
- ctx,
- )
- }
- return located(
- Stat.functiondef(funcname(ctx.funcname()!!), funcbody(ctx.funcbody()!!)),
- ctx,
- )
- }
- ctx.LOCAL()?.let {
- return located(
- Stat.localassignment(
- namelist(ctx.namelist()!!),
- ctx.explist()?.let(::explist),
- ),
- ctx,
- )
- }
- ctx.varlist()?.let {
- return located(
- Stat.assignment(
- it.variable().map(::variable).toMutableList(),
- explist(ctx.explist()!!),
- ),
- ctx,
- )
- }
- ctx.functioncall()?.let {
- return located(Stat.functioncall(functioncall(it)), ctx)
- }
- ctx.DO()?.let {
- return located(block(ctx.block(0)!!), ctx)
- }
- throw ParseException("Unsupported statement at ${ctx.start?.line}:${ctx.start?.charPositionInLine}")
- }
-
- private fun retstat(ctx: LuaParser.RetstatContext): Stat =
- located(Stat.returnstat(ctx.explist()?.let(::explist)), ctx)
-
- private fun funcname(ctx: LuaParser.FuncnameContext): FuncName {
- val names = ctx.NAME()
- val result = FuncName(names.first().text)
- val dotCount = ctx.DOT().size
- names.drop(1).take(dotCount).forEach { result.adddot(it.text) }
- if (ctx.COLON() != null) result.method = names.last().text
- return located(result, ctx)
- }
-
- private fun namelist(ctx: LuaParser.NamelistContext): MutableList =
- ctx.NAME().map { Name(it.text) }.toMutableList()
-
- private fun explist(ctx: LuaParser.ExplistContext): MutableList =
- ctx.exp().map(::exp).toMutableList()
-
- private fun exp(ctx: LuaParser.ExpContext): Exp = orExp(ctx.orExp())
-
- private fun orExp(ctx: LuaParser.OrExpContext): Exp =
- foldBinary(ctx.andExp().map(::andExp), List(ctx.OR().size) { Lua.OP_OR })
-
- private fun andExp(ctx: LuaParser.AndExpContext): Exp =
- foldBinary(ctx.compareExp().map(::compareExp), List(ctx.AND().size) { Lua.OP_AND })
-
- private fun compareExp(ctx: LuaParser.CompareExpContext): Exp {
- val values = ctx.concatExp().map(::concatExp)
- val operators = (1 until ctx.childCount step 2).map { binaryOperator(ctx.getChild(it)!!.text) }
- return foldBinary(values, operators)
- }
-
- private fun concatExp(ctx: LuaParser.ConcatExpContext): Exp {
- val lhs = addExp(ctx.addExp())
- val rhs = ctx.concatExp()?.let(::concatExp) ?: return lhs
- return Exp.binaryexp(lhs, Lua.OP_CONCAT, rhs)!!
- }
-
- private fun addExp(ctx: LuaParser.AddExpContext): Exp {
- val values = ctx.multiplyExp().map(::multiplyExp)
- val operators = (1 until ctx.childCount step 2).map { binaryOperator(ctx.getChild(it)!!.text) }
- return foldBinary(values, operators)
- }
-
- private fun multiplyExp(ctx: LuaParser.MultiplyExpContext): Exp {
- val values = ctx.unaryExp().map(::unaryExp)
- val operators = (1 until ctx.childCount step 2).map { binaryOperator(ctx.getChild(it)!!.text) }
- return foldBinary(values, operators)
- }
-
- private fun unaryExp(ctx: LuaParser.UnaryExpContext): Exp {
- ctx.powerExp()?.let { return powerExp(it) }
- return Exp.unaryexp(unaryOperator(ctx.getChild(0)!!.text), unaryExp(ctx.unaryExp()!!))!!
- }
-
- private fun powerExp(ctx: LuaParser.PowerExpContext): Exp {
- val lhs = simpleexp(ctx.simpleexp())
- val rhs = ctx.unaryExp()?.let(::unaryExp) ?: return lhs
- return Exp.binaryexp(lhs, Lua.OP_POW, rhs)!!
- }
-
- private fun simpleexp(ctx: LuaParser.SimpleexpContext): Exp {
- ctx.NIL()?.let { return located(Exp.constant(LuaValue.NIL), ctx) }
- ctx.FALSE()?.let { return located(Exp.constant(LuaValue.FALSE), ctx) }
- ctx.TRUE()?.let { return located(Exp.constant(LuaValue.TRUE), ctx) }
- ctx.NUMBER()?.let { return located(Exp.numberconstant(it.text), ctx) }
- ctx.string()?.let { return located(Exp.constant(string(it)), ctx) }
- ctx.ELLIPSIS()?.let { return located(Exp.varargs(), ctx) }
- ctx.functiondef()?.let {
- return located(Exp.anonymousfunction(funcbody(it.funcbody())), ctx)
- }
- ctx.prefixexp()?.let { return prefixexp(it) }
- ctx.tableconstructor()?.let { return tableconstructor(it) }
- throw ParseException("Unsupported expression at ${ctx.start?.line}:${ctx.start?.charPositionInLine}")
- }
-
- private fun prefixexp(ctx: LuaParser.PrefixexpContext): Exp.PrimaryExp {
- var result = initialPrimary(ctx.NAME()?.text, ctx.exp())
- ctx.postfix().forEach { result = postfix(result, it) }
- return located(result, ctx)
- }
-
- private fun functioncall(ctx: LuaParser.FunctioncallContext): Exp.FuncCall {
- var result = initialPrimary(ctx.NAME()?.text, ctx.exp())
- ctx.postfix().forEach { result = postfix(result, it) }
- result = callpostfix(result, ctx.callpostfix())
- return result as? Exp.FuncCall
- ?: throw ParseException("expected function call")
- }
-
- private fun variable(ctx: LuaParser.VariableContext): Exp.VarExp {
- if (ctx.NAME().size == 1 && ctx.postfix().isEmpty() && ctx.exp().isEmpty()) {
- return located(Exp.nameprefix(ctx.NAME(0)!!.text), ctx)
- }
- var result = initialPrimary(ctx.NAME(0)?.text, ctx.exp().firstOrNull())
- ctx.postfix().forEach { result = postfix(result, it) }
- result = if (ctx.LBRACK() != null) {
- Exp.indexop(result, exp(ctx.exp().last()))
- } else {
- Exp.fieldop(result, ctx.NAME().last().text)
- }
- return located(
- result as? Exp.VarExp ?: throw ParseException("expected variable"),
- ctx,
- )
- }
-
- private fun initialPrimary(name: String?, expression: LuaParser.ExpContext?): Exp.PrimaryExp =
- if (name != null) Exp.nameprefix(name) else Exp.parensprefix(exp(expression!!))
-
- private fun postfix(lhs: Exp.PrimaryExp, ctx: LuaParser.PostfixContext): Exp.PrimaryExp =
- when {
- ctx.LBRACK() != null -> Exp.indexop(lhs, exp(ctx.exp()!!))
- ctx.DOT() != null -> Exp.fieldop(lhs, ctx.NAME()!!.text)
- ctx.COLON() != null -> Exp.methodop(lhs, ctx.NAME()!!.text, args(ctx.args()!!))
- else -> Exp.functionop(lhs, args(ctx.args()!!))
- }
-
- private fun callpostfix(
- lhs: Exp.PrimaryExp,
- ctx: LuaParser.CallpostfixContext,
- ): Exp.PrimaryExp =
- if (ctx.COLON() != null) {
- Exp.methodop(lhs, ctx.NAME()!!.text, args(ctx.args()))
- } else {
- Exp.functionop(lhs, args(ctx.args()))
- }
-
- private fun args(ctx: LuaParser.ArgsContext): FuncArgs {
- val result = when {
- ctx.LPAREN() != null -> FuncArgs.explist(ctx.explist()?.let(::explist))
- ctx.tableconstructor() != null -> FuncArgs.tableconstructor(tableconstructor(ctx.tableconstructor()!!))
- else -> FuncArgs.string(string(ctx.string()!!))
- }
- return located(result, ctx)
- }
-
- private fun funcbody(ctx: LuaParser.FuncbodyContext): FuncBody =
- located(FuncBody(ctx.parlist()?.let(::parlist), block(ctx.block())), ctx)
-
- private fun parlist(ctx: LuaParser.ParlistContext): ParList =
- located(
- ParList(
- ctx.namelist()?.let(::namelist),
- ctx.ELLIPSIS() != null,
- ),
- ctx,
- )
-
- private fun tableconstructor(ctx: LuaParser.TableconstructorContext): TableConstructor {
- val result = TableConstructor()
- result.fields = ctx.fieldlist()?.field()?.map(::field)?.toMutableList()
- return located(result, ctx)
- }
-
- private fun field(ctx: LuaParser.FieldContext): TableField {
- val expressions = ctx.exp()
- val result = when {
- ctx.LBRACK() != null -> TableField.keyedField(exp(expressions[0]), exp(expressions[1]))
- ctx.NAME() != null -> TableField.namedField(ctx.NAME()!!.text, exp(expressions[0]))
- else -> TableField.listField(exp(expressions[0]))
- }
- return located(result, ctx)
- }
-
- private fun string(ctx: LuaParser.StringContext) =
- when {
- ctx.NORMAL_STRING() != null -> Str.quoteString(ctx.text)
- ctx.CHAR_STRING() != null -> Str.charString(ctx.text)
- else -> Str.longString(ctx.text)
- }
-
- private fun foldBinary(values: List, operators: List): Exp {
- var result = values.first()
- operators.forEachIndexed { index, operator ->
- result = Exp.binaryexp(result, operator, values[index + 1])!!
- }
- return result
- }
-
- private fun binaryOperator(operator: String): Int =
- when (operator) {
- "+" -> Lua.OP_ADD
- "-" -> Lua.OP_SUB
- "*" -> Lua.OP_MUL
- "/" -> Lua.OP_DIV
- "%" -> Lua.OP_MOD
- "^" -> Lua.OP_POW
- ".." -> Lua.OP_CONCAT
- "<" -> Lua.OP_LT
- "<=" -> Lua.OP_LE
- ">" -> Lua.OP_GT
- ">=" -> Lua.OP_GE
- "==" -> Lua.OP_EQ
- "~=" -> Lua.OP_NEQ
- "and" -> Lua.OP_AND
- "or" -> Lua.OP_OR
- else -> throw ParseException("Unknown binary operator: $operator")
- }
-
- private fun unaryOperator(operator: String): Int =
- when (operator) {
- "-" -> Lua.OP_UNM
- "not" -> Lua.OP_NOT
- "#" -> Lua.OP_LEN
- else -> throw ParseException("Unknown unary operator: $operator")
- }
-
- private fun located(element: T, ctx: ParserRuleContext): T {
- val start = ctx.start
- val stop = ctx.stop
- element.beginLine = start?.line ?: 0
- element.beginColumn = ((start?.charPositionInLine ?: 0) + 1).toShort()
- element.endLine = stop?.line ?: element.beginLine
- element.endColumn = ((stop?.charPositionInLine ?: 0) + (stop?.text?.length ?: 0)).toShort()
- return element
- }
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/parser/LuaParser.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/parser/LuaParser.kt
deleted file mode 100644
index 8a367161..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/parser/LuaParser.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.parser
-
-import net.blueva.luak.ast.Chunk
-import net.blueva.luak.parser.antlr.LuaLexer
-import net.blueva.luak.parser.antlr.LuaParser as AntlrLuaParser
-import org.antlr.v4.kotlinruntime.BaseErrorListener
-import org.antlr.v4.kotlinruntime.CharStreams
-import org.antlr.v4.kotlinruntime.CommonTokenStream
-import org.antlr.v4.kotlinruntime.RecognitionException
-import org.antlr.v4.kotlinruntime.Recognizer
-class LuaParser(
- private val source: String,
-) {
- @Throws(ParseException::class)
- fun Chunk(): Chunk {
- try {
- val lexer = LuaLexer(CharStreams.fromString(source))
- lexer.removeErrorListeners()
- lexer.addErrorListener(ThrowingErrorListener)
-
- val parser = AntlrLuaParser(CommonTokenStream(lexer))
- parser.removeErrorListeners()
- parser.addErrorListener(ThrowingErrorListener)
-
- return LuaAstBuilder().chunk(parser.chunk())
- } catch (e: ParseException) {
- throw e
- } catch (e: Exception) {
- throw ParseException(e.message ?: "Unable to parse Lua source", e)
- }
- }
-
- private object ThrowingErrorListener : BaseErrorListener() {
- override fun syntaxError(
- recognizer: Recognizer<*, *>,
- offendingSymbol: Any?,
- line: Int,
- charPositionInLine: Int,
- msg: String,
- e: RecognitionException?,
- ) {
- throw ParseException("line $line:${charPositionInLine + 1} $msg", e)
- }
- }
-}
diff --git a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/parser/ParseException.kt b/blueluak-core/src/commonMain/kotlin/net/blueva/luak/parser/ParseException.kt
deleted file mode 100644
index 6c51db30..00000000
--- a/blueluak-core/src/commonMain/kotlin/net/blueva/luak/parser/ParseException.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-/******************************************************************************
- * ____ _ _ _ __
- * | __ )| |_ _ ___| | _ _ __ _| |/ /
- * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
- * | |_) | | |_| | __/ |__| |_| | (_| | . \
- * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
- *
- * BlueLuaK
- * https://github.com/BluevaDevelopment/BlueLuaK
- *
- * Copyright (c) 2026 Blueva Development
- *
- * SPDX-License-Identifier: MIT
- ******************************************************************************/
-package net.blueva.luak.parser
-
-class ParseException(
- message: String,
- cause: Throwable? = null,
-) : Exception(message, cause)
diff --git a/blueluak-core/src/commonTest/kotlin/net/blueva/luak/BitwiseOperatorTest.kt b/blueluak-core/src/commonTest/kotlin/net/blueva/luak/BitwiseOperatorTest.kt
new file mode 100644
index 00000000..5640b086
--- /dev/null
+++ b/blueluak-core/src/commonTest/kotlin/net/blueva/luak/BitwiseOperatorTest.kt
@@ -0,0 +1,131 @@
+/******************************************************************************
+ * ____ _ _ _ __
+ * | __ )| |_ _ ___| | _ _ __ _| |/ /
+ * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
+ * | |_) | | |_| | __/ |__| |_| | (_| | . \
+ * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
+ *
+ * BlueLuaK
+ * https://github.com/BluevaDevelopment/BlueLuaK
+ *
+ * Copyright (c) 2026 Blueva Development
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+package net.blueva.luak
+
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import net.blueva.luak.lib.LuaPlatform
+
+/**
+ * The bitwise operators `& | ~ << >>` and unary `~`, added in Lua 5.3.
+ *
+ * Every expectation was taken from the reference interpreter (`lua-5.5.1`).
+ * They operate on the 64-bit integer subtype, which is why they could not
+ * exist while numbers were 32-bit.
+ */
+class BitwiseOperatorTest {
+ private lateinit var globals: Globals
+
+ @BeforeTest
+ fun buildGlobals() {
+ globals = LuaPlatform.standardGlobals()
+ }
+
+ private fun eval(script: String): LuaValue = globals.load("return $script", "bitwise-test")!!.call()!!
+
+ @Test
+ fun andOrExclusiveOrAndNot() {
+ assertEquals(48L, eval("0xF0 & 0x3C").tolong())
+ assertEquals(255L, eval("0xF0 | 0x0F").tolong())
+ assertEquals(15L, eval("0xF0 ~ 0xFF").tolong())
+ assertEquals(-1L, eval("~0").tolong())
+ assertEquals(-6L, eval("~5").tolong())
+ }
+
+ @Test
+ fun shiftsAreLogicalNotArithmetic() {
+ assertEquals(16L, eval("1 << 4").tolong())
+ assertEquals(16L, eval("256 >> 4").tolong())
+ // The sign bit is shifted in as a zero, so this is maxinteger.
+ assertEquals(9223372036854775807L, eval("-1 >> 1").tolong())
+ }
+
+ @Test
+ fun oversizedAndNegativeShiftCounts() {
+ assertEquals(0L, eval("1 << 64").tolong())
+ assertEquals(Long.MIN_VALUE, eval("1 << 63").tolong())
+ // A negative count reverses the direction rather than erroring.
+ assertEquals(0L, eval("1 << -1").tolong())
+ assertEquals(2L, eval("1 >> -1").tolong())
+ }
+
+ @Test
+ fun precedenceMatchesTheReference() {
+ assertEquals(9L, eval("5 & 3 | 8").tolong()) // '&' binds tighter than '|'
+ assertEquals(8L, eval("1 << 2 + 1").tolong()) // '+' binds tighter than '<<'
+ assertEquals(3L, eval("2 ~ 3 & 1").tolong()) // '&' binds tighter than binary '~'
+ }
+
+ @Test
+ fun floatsWithAnIntegralValueAreAccepted() {
+ assertEquals(1L, eval("3.0 & 1").tolong())
+ }
+
+ @Test
+ fun floatsWithAFractionalPartFailAtRunTime() {
+ // Crucially at run time, not compile time: constant folding must leave
+ // this alone so pcall can catch it.
+ val result = globals.load("return pcall(function() return 1.5 & 1 end)", "bitwise-frac")!!.invoke()
+ assertFalse(result.arg(1).toboolean())
+ assertTrue(result.checkjstring(2).contains("no integer representation"))
+ }
+
+ @Test
+ fun stringsAreNotCoercedForBitwiseOperations() {
+ // Arithmetic coerces numeric strings; bitwise does not, as of 5.4.
+ val result = globals.load("return pcall(function() return '3' & 1 end)", "bitwise-string")!!.invoke()
+ assertFalse(result.arg(1).toboolean())
+ }
+
+ @Test
+ fun fallsBackToTheBitwiseMetamethods() {
+ val script = """
+ local t = setmetatable({}, {
+ __band = function() return "band" end,
+ __bor = function() return "bor" end,
+ __bxor = function() return "bxor" end,
+ __shl = function() return "shl" end,
+ __shr = function() return "shr" end,
+ __bnot = function() return "bnot" end,
+ })
+ return t & 1, t | 1, t ~ 1, t << 1, t >> 1, ~t
+ """.trimIndent()
+ val result = globals.load(script, "bitwise-mm")!!.invoke()
+ assertEquals("band", result.checkjstring(1))
+ assertEquals("bor", result.checkjstring(2))
+ assertEquals("bxor", result.checkjstring(3))
+ assertEquals("shl", result.checkjstring(4))
+ assertEquals("shr", result.checkjstring(5))
+ assertEquals("bnot", result.checkjstring(6))
+ }
+
+ @Test
+ fun worksOnValuesFromRegistersNotJustConstants() {
+ // Constant folding covers the literal form; this exercises the VM path.
+ val script = """
+ local x = 0xFF
+ local y = 4
+ return x & 0x0F, x >> y, x << 1, ~x
+ """.trimIndent()
+ val result = globals.load(script, "bitwise-registers")!!.invoke()
+ assertEquals(15L, result.arg(1).tolong())
+ assertEquals(15L, result.arg(2).tolong())
+ assertEquals(510L, result.arg(3).tolong())
+ assertEquals(-256L, result.arg(4).tolong())
+ }
+}
diff --git a/blueluak-core/src/commonTest/kotlin/net/blueva/luak/FloorDivisionTest.kt b/blueluak-core/src/commonTest/kotlin/net/blueva/luak/FloorDivisionTest.kt
new file mode 100644
index 00000000..942eed62
--- /dev/null
+++ b/blueluak-core/src/commonTest/kotlin/net/blueva/luak/FloorDivisionTest.kt
@@ -0,0 +1,101 @@
+/******************************************************************************
+ * ____ _ _ _ __
+ * | __ )| |_ _ ___| | _ _ __ _| |/ /
+ * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
+ * | |_) | | |_| | __/ |__| |_| | (_| | . \
+ * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
+ *
+ * BlueLuaK
+ * https://github.com/BluevaDevelopment/BlueLuaK
+ *
+ * Copyright (c) 2026 Blueva Development
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+package net.blueva.luak
+
+import kotlin.test.BeforeTest
+import kotlin.test.Ignore
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import net.blueva.luak.lib.LuaPlatform
+
+/**
+ * Floor division `//`, added to the language in Lua 5.3.
+ *
+ * Every expectation was taken from the reference interpreter (`lua-5.5.1`).
+ *
+ * ### Known gap
+ *
+ * The float cases are still wrong, and not because of `//` itself: BlueLuaK
+ * inherited LuaJ's habit of collapsing a float with an integral value into an
+ * integer, so `0.0` *is* an integer at runtime and `1 // 0.0` raises the
+ * integer divide-by-zero error instead of yielding `inf`. Breaking that
+ * collapse is the next step of the port; see [floatOperandsProduceFloats],
+ * which is ignored until then.
+ */
+class FloorDivisionTest {
+ private lateinit var globals: Globals
+
+ @BeforeTest
+ fun buildGlobals() {
+ globals = LuaPlatform.standardGlobals()
+ }
+
+ private fun eval(script: String): LuaValue = globals.load("return $script", "idiv-test")!!.call()!!
+
+ @Test
+ fun integersFloorTowardsNegativeInfinity() {
+ assertEquals(3L, eval("7 // 2").tolong())
+ assertEquals(-4L, eval("-7 // 2").tolong())
+ assertEquals(-4L, eval("7 // -2").tolong())
+ assertEquals(3L, eval("-7 // -2").tolong())
+ }
+
+ @Test
+ fun dividingAnIntegerByIntegerZeroIsAnError() {
+ val result = globals.load("return pcall(function() return 1 // 0 end)", "idiv-zero")!!.invoke()
+ assertTrue(result.arg(1).toboolean().not(), "1 // 0 must fail")
+ assertTrue(result.checkjstring(2).contains("zero") || result.checkjstring(2).contains("n//0"))
+ }
+
+ @Test
+ fun minIntegerDividedByMinusOneWrapsRatherThanOverflowing() {
+ // -(-2^63) is not representable, so the reference wraps back to itself.
+ assertEquals(Long.MIN_VALUE, eval("(-9223372036854775807 - 1) // -1").tolong())
+ }
+
+ @Test
+ fun bindsAsTightlyAsDivisionAndIsLeftAssociative() {
+ assertEquals(2L, eval("9 // 2 // 2").tolong())
+ assertEquals(6L, eval("2 + 8 // 2").tolong())
+ assertEquals(5L, eval("(2 + 8) // 2").tolong())
+ }
+
+ @Test
+ fun foldsAtCompileTimeLikeTheOtherArithmetic() {
+ // Constant folding must agree with the runtime path.
+ assertEquals(3L, eval("7 // 2").tolong())
+ assertEquals(-4L, eval("-7 // 2").tolong())
+ }
+
+ @Test
+ fun fallsBackToTheIdivMetamethod() {
+ val script = """
+ local t = setmetatable({}, { __idiv = function(a, b) return "idiv-mm" end })
+ return t // 2
+ """.trimIndent()
+ assertEquals("idiv-mm", globals.load(script, "idiv-mm")!!.call()!!.tojstring())
+ }
+
+ @Test
+ @Ignore // Blocked on the integer/float collapse; see the class documentation.
+ fun floatOperandsProduceFloats() {
+ assertEquals("3.0", eval("tostring(7.0 // 2)").tojstring())
+ assertEquals("3.0", eval("tostring(7 // 2.0)").tojstring())
+ assertEquals("inf", eval("tostring(1 // 0.0)").tojstring())
+ assertEquals("-inf", eval("tostring(-1 // 0.0)").tojstring())
+ assertEquals("-4.0", eval("tostring(-7.5 // 2)").tojstring())
+ }
+}
diff --git a/blueluak-core/src/commonTest/kotlin/net/blueva/luak/GlobalDeclarationTest.kt b/blueluak-core/src/commonTest/kotlin/net/blueva/luak/GlobalDeclarationTest.kt
new file mode 100644
index 00000000..08108255
--- /dev/null
+++ b/blueluak-core/src/commonTest/kotlin/net/blueva/luak/GlobalDeclarationTest.kt
@@ -0,0 +1,128 @@
+/******************************************************************************
+ * ____ _ _ _ __
+ * | __ )| |_ _ ___| | _ _ __ _| |/ /
+ * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
+ * | |_) | | |_| | __/ |__| |_| | (_| | . \
+ * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
+ *
+ * BlueLuaK
+ * https://github.com/BluevaDevelopment/BlueLuaK
+ *
+ * Copyright (c) 2026 Blueva Development
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+package net.blueva.luak
+
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlin.test.fail
+import net.blueva.luak.lib.LuaPlatform
+
+/**
+ * `global` declarations, new in Lua 5.5.
+ *
+ * Every expectation was taken from the reference interpreter (`lua-5.5.1`).
+ */
+class GlobalDeclarationTest {
+ private lateinit var globals: Globals
+
+ @BeforeTest
+ fun buildGlobals() {
+ globals = LuaPlatform.standardGlobals()
+ }
+
+ /** Runs [source], returning the error message if it does not get through. */
+ private fun failureOf(source: String): String? = try {
+ globals.load(source, "global-test")?.call()
+ null
+ } catch (failure: LuaError) {
+ failure.message
+ }
+
+ private fun eval(source: String): String =
+ globals.load(source, "global-test")!!.call()!!.tojstring()
+
+ @Test
+ fun withNoDeclarationEveryNameIsStillAGlobal() {
+ assertEquals("nil", eval("return tostring(neverAssigned)"))
+ assertEquals("7", eval("assigned = 7 return tostring(assigned)"))
+ }
+
+ @Test
+ fun anUndeclaredNameIsRejectedOnceSomethingIsDeclared() {
+ val message = failureOf("global x; y = 1")
+ ?: fail("an undeclared global must not compile")
+ assertTrue(message.contains("variable 'y' not declared"), message)
+ }
+
+ @Test
+ fun theCollectiveFormPutsTheDefaultBack() {
+ assertEquals("1", eval("global *; y = 1 return tostring(y)"))
+ // A collective declaration keeps covering names declared after it.
+ assertEquals("ok", eval("global *; global q; q = 1; r = 2 return 'ok'"))
+ }
+
+ @Test
+ fun aDeclarationLastsOnlyToTheEndOfItsBlock() {
+ val script = """
+ do
+ global a
+ a = 1
+ end
+ b = 2
+ return tostring(a) .. "," .. tostring(b)
+ """.trimIndent()
+ assertEquals("1,2", eval(script))
+ }
+
+ @Test
+ fun aDeclarationDoesNotEscapeItsFunction() {
+ val message = failureOf("local function g() global z; z = 1; w = 2 end g()")
+ ?: fail("an undeclared global must not compile")
+ assertTrue(message.contains("variable 'w' not declared"), message)
+ }
+
+ @Test
+ fun aDeclarationCanCarryAnInitializer() {
+ assertEquals("10,20", eval("global *; global a, b = 10, 20 return a .. ',' .. b"))
+ }
+
+ @Test
+ fun declaringTheSameGlobalTwiceIsReportedWhenItRuns() {
+ val message = failureOf("global n = 1; global n = 2")
+ ?: fail("a second declaration of an assigned global must be reported")
+ assertTrue(message.contains("global 'n' already defined"), message)
+ }
+
+ @Test
+ fun aConstGlobalCannotBeAssigned() {
+ val message = failureOf("global c = 1; c = 2")
+ ?: fail("assigning to a const global must not compile")
+ assertTrue(
+ message.contains("const variable") && message.contains("'c'"),
+ message,
+ )
+ }
+
+ @Test
+ fun globalFunctionDeclaresAndDefinesInOneStep() {
+ assertEquals("7", eval("global *; global function f() return 7 end return tostring(f())"))
+ }
+
+ @Test
+ fun aGlobalCannotBeToBeClosed() {
+ val message = failureOf("global x = nil")
+ ?: fail(" on a global must not compile")
+ assertTrue(message.contains("global variables cannot be to-be-closed"), message)
+ }
+
+ @Test
+ fun globalIsStillAnOrdinaryNameWhereNoDeclarationFollows() {
+ // The word is not reserved, so code that already uses it keeps working.
+ assertEquals("3", eval("global = 3 return tostring(global)"))
+ assertEquals("6", eval("local global = 5 return tostring(global + 1)"))
+ }
+}
diff --git a/blueluak-core/src/commonTest/kotlin/net/blueva/luak/IntegerSubtypeTest.kt b/blueluak-core/src/commonTest/kotlin/net/blueva/luak/IntegerSubtypeTest.kt
new file mode 100644
index 00000000..49fbf8b1
--- /dev/null
+++ b/blueluak-core/src/commonTest/kotlin/net/blueva/luak/IntegerSubtypeTest.kt
@@ -0,0 +1,101 @@
+/******************************************************************************
+ * ____ _ _ _ __
+ * | __ )| |_ _ ___| | _ _ __ _| |/ /
+ * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
+ * | |_) | | |_| | __/ |__| |_| | (_| | . \
+ * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
+ *
+ * BlueLuaK
+ * https://github.com/BluevaDevelopment/BlueLuaK
+ *
+ * Copyright (c) 2026 Blueva Development
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+package net.blueva.luak
+
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import net.blueva.luak.lib.LuaPlatform
+
+/**
+ * Lua's 64-bit integer subtype, introduced in 5.3.
+ *
+ * BlueLuaK inherited LuaJ's 32-bit `LuaInteger`, which silently degraded any
+ * value outside `Int` range to a float. Every expectation below was taken from
+ * the reference interpreter (`lua-5.5.1`), not derived from this code.
+ */
+class IntegerSubtypeTest {
+ private lateinit var globals: Globals
+
+ @BeforeTest
+ fun buildGlobals() {
+ globals = LuaPlatform.standardGlobals()
+ }
+
+ private fun eval(script: String): LuaValue = globals.load("return $script", "integer-test")!!.call()!!
+
+ @Test
+ fun integerLiteralsKeepAllSixtyFourBits() {
+ assertEquals(9223372036854775807L, eval("9223372036854775807").tolong())
+ assertEquals(1234567890123456789L, eval("1234567890123456789").tolong())
+ assertEquals(-9223372036854775807L - 1L, eval("-9223372036854775807 - 1").tolong())
+ }
+
+ @Test
+ fun integerLiteralsRoundTripThroughTostring() {
+ // The old 32-bit path lost precision here, printing ...768.
+ assertEquals("1234567890123456789", eval("tostring(1234567890123456789)").tojstring())
+ assertEquals("4294967296", eval("tostring(4294967296)").tojstring())
+ }
+
+ @Test
+ fun integerArithmeticWrapsAroundRatherThanOverflowingToInfinity() {
+ // Lua 5.2 produced inf here because every number was a float.
+ assertEquals(-9223372036854775807L - 1L, eval("9223372036854775807 + 1").tolong())
+ assertEquals(0L, eval("4294967296 * 4294967296").tolong())
+ assertEquals(9223372036854775807L, eval("-9223372036854775807 - 2").tolong())
+ }
+
+ @Test
+ fun hexadecimalIntegerLiteralsWrapAround() {
+ // The manual points at hex notation as the way to keep wrap-around.
+ assertEquals(-1L, eval("0xFFFFFFFFFFFFFFFF").tolong())
+ assertEquals(4294967296L, eval("0x100000000").tolong())
+ }
+
+ @Test
+ fun largeIntegersWorkAsTableKeys() {
+ val script = """
+ local t = {}
+ t[4294967296] = "big"
+ t[-4294967296] = "negative"
+ return t[4294967296], t[-4294967296]
+ """.trimIndent()
+ val result = globals.load(script, "integer-keys")!!.invoke()
+ assertEquals("big", result.checkjstring(1))
+ assertEquals("negative", result.checkjstring(2))
+ }
+
+ @Test
+ fun distinctLargeIntegersAreDistinctKeys() {
+ // A 32-bit key would have collapsed these two onto one slot.
+ val script = """
+ local t = {}
+ t[4294967296] = "a"
+ t[8589934592] = "b"
+ return t[4294967296], t[8589934592]
+ """.trimIndent()
+ val result = globals.load(script, "integer-key-collision")!!.invoke()
+ assertEquals("a", result.checkjstring(1))
+ assertEquals("b", result.checkjstring(2))
+ }
+
+ @Test
+ fun valueOfLongNeverDegradesToAFloat() {
+ val big = LuaValue.valueOf(1L shl 62)
+ assertEquals(1L shl 62, big.tolong())
+ assertEquals(1L shl 62, (big as LuaInteger).v)
+ }
+}
diff --git a/blueluak-core/src/commonTest/kotlin/net/blueva/luak/LanguageVersionTest.kt b/blueluak-core/src/commonTest/kotlin/net/blueva/luak/LanguageVersionTest.kt
new file mode 100644
index 00000000..2aa6a090
--- /dev/null
+++ b/blueluak-core/src/commonTest/kotlin/net/blueva/luak/LanguageVersionTest.kt
@@ -0,0 +1,54 @@
+/******************************************************************************
+ * ____ _ _ _ __
+ * | __ )| |_ _ ___| | _ _ __ _| |/ /
+ * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
+ * | |_) | | |_| | __/ |__| |_| | (_| | . \
+ * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
+ *
+ * BlueLuaK
+ * https://github.com/BluevaDevelopment/BlueLuaK
+ *
+ * Copyright (c) 2026 Blueva Development
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+package net.blueva.luak
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import net.blueva.luak.lib.LuaPlatform
+
+/**
+ * `_VERSION` names the Lua *language*, never the implementation.
+ *
+ * Programs in the wild branch on it (`if _VERSION == "Lua 5.4" then ...`) and
+ * the reference test suite reads it to decide which cases apply. BlueLuaK used
+ * to report its own release here (`"BlueLuaK 26.5"`), which no conforming
+ * program can make sense of; these tests keep it from drifting back.
+ */
+class LanguageVersionTest {
+ @Test
+ fun versionGlobalNamesTheLanguage() {
+ val globals = LuaPlatform.standardGlobals()
+ val version = globals.get("_VERSION")!!.checkjstring()!!
+ assertTrue(
+ Regex("""^Lua \d+\.\d+$""").matches(version),
+ "_VERSION must look like \"Lua 5.4\", was \"$version\"",
+ )
+ }
+
+ @Test
+ fun versionGlobalMatchesTheConstant() {
+ val globals = LuaPlatform.standardGlobals()
+ assertEquals(Lua._VERSION, globals.get("_VERSION")!!.checkjstring())
+ }
+
+ @Test
+ fun theImplementationVersionIsReportedSeparately() {
+ // Both exist and say different things; conflating them is the bug this
+ // suite guards against.
+ assertTrue(Lua.BLUELUAK_VERSION.startsWith("BlueLuaK"), Lua.BLUELUAK_VERSION)
+ assertTrue(Lua._VERSION.startsWith("Lua "), Lua._VERSION)
+ }
+}
diff --git a/blueluak-core/src/commonTest/kotlin/net/blueva/luak/LocalAttributeTest.kt b/blueluak-core/src/commonTest/kotlin/net/blueva/luak/LocalAttributeTest.kt
new file mode 100644
index 00000000..8e7eb659
--- /dev/null
+++ b/blueluak-core/src/commonTest/kotlin/net/blueva/luak/LocalAttributeTest.kt
@@ -0,0 +1,180 @@
+/******************************************************************************
+ * ____ _ _ _ __
+ * | __ )| |_ _ ___| | _ _ __ _| |/ /
+ * | _ \| | | | |/ _ \ | | | | |/ _` | ' /
+ * | |_) | | |_| | __/ |__| |_| | (_| | . \
+ * |____/|_|\__,_|\___|_____\__,_|\__,_|_|\_\
+ *
+ * BlueLuaK
+ * https://github.com/BluevaDevelopment/BlueLuaK
+ *
+ * Copyright (c) 2026 Blueva Development
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+package net.blueva.luak
+
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlin.test.fail
+import net.blueva.luak.lib.LuaPlatform
+
+/**
+ * Local variable attributes, `local x ` and `local x `, from
+ * Lua 5.4.
+ *
+ * Every expectation was taken from the reference interpreter (`lua-5.5.1`).
+ */
+class LocalAttributeTest {
+ private lateinit var globals: Globals
+
+ @BeforeTest
+ fun buildGlobals() {
+ globals = LuaPlatform.standardGlobals()
+ }
+
+ /** Compiles [source], returning the error message if it does not compile. */
+ private fun compileError(source: String): String? = try {
+ globals.load(source, "attribute-test")
+ null
+ } catch (failure: LuaError) {
+ failure.message
+ }
+
+ @Test
+ fun constLocalsBehaveLikeOrdinaryLocals() {
+ val script = """
+ local answer = 42
+ local other , plain = 1, 2
+ return answer, answer + 1, other + plain
+ """.trimIndent()
+ val result = globals.load(script, "const-read")!!.invoke()
+ assertEquals(42L, result.arg(1).tolong())
+ assertEquals(43L, result.arg(2).tolong())
+ assertEquals(3L, result.arg(3).tolong())
+ }
+
+ @Test
+ fun assigningToAConstLocalIsACompileError() {
+ val message = compileError("local x = 42; x = 1")
+ ?: fail("assigning to a const local must not compile")
+ assertTrue(
+ message.contains("const variable") && message.contains("'x'"),
+ "message should name the variable, was: $message",
+ )
+ }
+
+ @Test
+ fun assigningToAConstLocalIsCaughtInAMultipleAssignment() {
+ val message = compileError("local a = 1; local b = 2; b, a = 3, 4")
+ ?: fail("assigning to a const local must not compile")
+ assertTrue(message.contains("const variable"), message)
+ }
+
+ @Test
+ fun aPlainLocalIsStillAssignable() {
+ val script = """
+ local x = 1
+ x = x + 1
+ return x
+ """.trimIndent()
+ assertEquals(2L, globals.load(script, "plain-local")!!.call()!!.tolong())
+ }
+
+ @Test
+ fun unknownAttributesAreRejected() {
+ val message = compileError("local x = 1")
+ ?: fail("an unknown attribute must not compile")
+ assertTrue(message.contains("unknown attribute") && message.contains("bogus"), message)
+ }
+
+ @Test
+ fun closeVariablesAreClosedInReverseOrderOnLeavingTheBlock() {
+ val script = """
+ local log = {}
+ local function res(name)
+ return setmetatable({}, {__close = function() log[#log + 1] = name end})
+ end
+ do
+ local a = res("a")
+ local b = res("b")
+ log[#log + 1] = "body"
+ end
+ return table.concat(log, ",")
+ """.trimIndent()
+ assertEquals("body,b,a", globals.load(script, "close-order")!!.call()!!.tojstring())
+ }
+
+ @Test
+ fun closeHandlersRunWhileAnErrorUnwindsAndSeeIt() {
+ val script = """
+ local seen
+ local ok, err = pcall(function()
+ local a = setmetatable({}, {__close = function(_, e) seen = e end})
+ error("boom", 0)
+ end)
+ return tostring(ok) .. "|" .. tostring(err) .. "|" .. tostring(seen)
+ """.trimIndent()
+ assertEquals("false|boom|boom", globals.load(script, "close-error")!!.call()!!.tojstring())
+ }
+
+ @Test
+ fun breakAndReturnBothCloseOnTheWayOut() {
+ val script = """
+ local log = {}
+ local function res(name)
+ return setmetatable({}, {__close = function() log[#log + 1] = name end})
+ end
+ for i = 1, 3 do
+ local a = res("loop" .. i)
+ if i == 2 then break end
+ end
+ local function f()
+ local a = res("ret")
+ return "value"
+ end
+ local v = f()
+ return v .. "|" .. table.concat(log, ",")
+ """.trimIndent()
+ assertEquals("value|loop1,loop2,ret", globals.load(script, "close-exits")!!.call()!!.tojstring())
+ }
+
+ @Test
+ fun falseAndNilNeedNoCloseMetamethod() {
+ val script = """
+ do
+ local a = nil
+ local b = false
+ end
+ return "ok"
+ """.trimIndent()
+ assertEquals("ok", globals.load(script, "close-falsy")!!.call()!!.tojstring())
+ }
+
+ @Test
+ fun aValueWithNoCloseMetamethodIsRejectedAtTheDeclaration() {
+ val failure = runCatching { globals.load("local x = 42", "close-bad")!!.call() }
+ .exceptionOrNull() as? LuaError
+ ?: fail("a non-closable value must be reported")
+ assertTrue(
+ failure.message!!.contains("variable 'x' got a non-closable value"),
+ "message should name the variable, was: ${failure.message}",
+ )
+ }
+
+ @Test
+ fun onlyOneCloseVariablePerLocalStatement() {
+ val message = compileError("local x , y = a, b")
+ ?: fail("two variables in one statement must not compile")
+ assertTrue(message.contains("multiple to-be-closed variables"), message)
+ }
+
+ @Test
+ fun assigningToACloseLocalIsACompileError() {
+ val message = compileError("local x