diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index a8d866b..b6ae798 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -26,11 +26,11 @@ jobs: - name: Extract Lua source run: | cd lua_src - unzip -o lua-5.4.8.zip + unzip -o lua-5.5.0.zip - name: Build and Install Native Lua from source run: | - cd lua_src/lua-5.4.8 + cd lua_src/lua-5.5.0 make linux sudo make install INSTALL_TOP=/usr/local echo "/usr/local/bin" >> $GITHUB_PATH @@ -65,12 +65,12 @@ jobs: shell: pwsh run: | cd lua_src - Expand-Archive -Path lua-5.4.8.zip -DestinationPath . -Force + Expand-Archive -Path lua-5.5.0.zip -DestinationPath . -Force - name: Build Native Lua with CMake shell: pwsh run: | - cd lua_src/lua-5.4.8 + cd lua_src/lua-5.5.0 cmake -B build -DCMAKE_BUILD_TYPE=Release -DLUA_BUILD_INTERPRETER=ON cmake --build build --config Release # Add to PATH for this job @@ -116,11 +116,11 @@ jobs: - name: Extract Lua source run: | cd lua_src - unzip -o lua-5.4.8.zip + unzip -o lua-5.5.0.zip - name: Build and Install Native Lua from source run: | - cd lua_src/lua-5.4.8 + cd lua_src/lua-5.5.0 make macosx sudo make install INSTALL_TOP=/usr/local echo "/usr/local/bin" >> $GITHUB_PATH diff --git a/.gitignore b/.gitignore index 6c349a8..445de8c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target -lua_src/lua-5.4.8 \ No newline at end of file +bytecode_comparison_output +lua_src/lua-5.5.0 diff --git a/Cargo.lock b/Cargo.lock index c5edfd8..c6401e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,52 +2,12 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "arc-swap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" - -[[package]] -name = "base62" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10e52a7bcb1d6beebee21fb5053af9e3cbb7a7ed1a4909e534040e676437ab1f" -dependencies = [ - "rustversion", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -[[package]] -name = "bstr" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "bumpalo" version = "3.19.0" @@ -70,60 +30,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "countme" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "emmylua_parser" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cee7c333669bf966b4be6b558c0a6643777d2111d1a37386adf4f1fa645267a" -dependencies = [ - "rowan", - "rust-i18n", - "serde", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - [[package]] name = "errno" version = "0.3.14" @@ -152,83 +58,6 @@ dependencies = [ "wasip2", ] -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "globset" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" -dependencies = [ - "aho-corasick", - "bstr", - "log", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "globwalk" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" -dependencies = [ - "bitflags 1.3.2", - "ignore", - "walkdir", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" - -[[package]] -name = "ignore" -version = "0.4.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" -dependencies = [ - "crossbeam-deque", - "globset", - "log", - "memchr", - "regex-automata", - "same-file", - "walkdir", - "winapi-util", -] - -[[package]] -name = "indexmap" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" -dependencies = [ - "equivalent", - "hashbrown 0.15.5", -] - -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.15" @@ -245,12 +74,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" version = "0.2.177" @@ -273,20 +96,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - [[package]] name = "luars" version = "0.1.0" dependencies = [ - "emmylua_parser", "itoa", "libloading", - "rowan", "ryu", "tempfile", "tokio", @@ -310,21 +125,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "memchr" -version = "2.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "normpath" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed" -dependencies = [ - "windows-sys", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -361,114 +161,13 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - -[[package]] -name = "rowan" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" -dependencies = [ - "countme", - "hashbrown 0.14.5", - "rustc-hash", - "text-size", -] - -[[package]] -name = "rust-i18n" -version = "3.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda2551fdfaf6cc5ee283adc15e157047b92ae6535cf80f6d4962d05717dc332" -dependencies = [ - "globwalk", - "once_cell", - "regex", - "rust-i18n-macro", - "rust-i18n-support", - "smallvec", -] - -[[package]] -name = "rust-i18n-macro" -version = "3.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22baf7d7f56656d23ebe24f6bb57a5d40d2bce2a5f1c503e692b5b2fa450f965" -dependencies = [ - "glob", - "once_cell", - "proc-macro2", - "quote", - "rust-i18n-support", - "serde", - "serde_json", - "serde_yaml", - "syn", -] - -[[package]] -name = "rust-i18n-support" -version = "3.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940ed4f52bba4c0152056d771e563b7133ad9607d4384af016a134b58d758f19" -dependencies = [ - "arc-swap", - "base62", - "globwalk", - "itertools", - "lazy_static", - "normpath", - "once_cell", - "proc-macro2", - "regex", - "serde", - "serde_json", - "serde_yaml", - "siphasher", - "toml", - "triomphe", -] - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustix" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.10.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -487,97 +186,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.142" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "syn" version = "2.0.105" @@ -602,12 +210,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "text-size" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" - [[package]] name = "tokio" version = "1.48.0" @@ -629,80 +231,12 @@ dependencies = [ "syn", ] -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "triomphe" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" -dependencies = [ - "arc-swap", - "serde", - "stable_deref_trait", -] - [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -767,15 +301,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "windows-sys", -] - [[package]] name = "windows-link" version = "0.2.1" @@ -855,15 +380,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" -dependencies = [ - "memchr", -] - [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index 61c2ea6..6f25898 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ members = [ luars = { path = "crates/luars", version = "0.1.0" } # external -emmylua_parser = "0.19.0" libloading = "0.9" tokio = { version = "1.42", features = ["rt-multi-thread", "time", "sync", "macros"] } wasm-bindgen = "0.2" diff --git a/compare_all_testes.ps1 b/compare_all_testes.ps1 new file mode 100644 index 0000000..c25e5fd --- /dev/null +++ b/compare_all_testes.ps1 @@ -0,0 +1,189 @@ +#!/usr/bin/env pwsh + +param( + [switch]$Verbose, + [switch]$StopOnError +) + +$ErrorActionPreference = "Stop" + +$luacPath = "lua_src\lua-5.5.0\build\Release\luac.exe" +$bytecode_dumpPath = "target\release\bytecode_dump.exe" +$testesDir = "lua_tests\testes" +$outputDir = "bytecode_comparison_output" + +# 创建输出目录 +if (-not (Test-Path $outputDir)) { + New-Item -ItemType Directory -Path $outputDir | Out-Null +} + +# 检查工具是否存在 +if (-not (Test-Path $luacPath)) { + Write-Error "luac.exe not found: $luacPath" + exit 1 +} + +if (-not (Test-Path $bytecode_dumpPath)) { + Write-Host "bytecode_dump.exe not found. Building..." -ForegroundColor Yellow + cargo build --release --bin bytecode_dump +} + +# 获取所有 .lua 文件 +$luaFiles = Get-ChildItem -Path $testesDir -Filter "*.lua" | Sort-Object Name + +Write-Host "Found $($luaFiles.Count) Lua files in $testesDir" -ForegroundColor Cyan +Write-Host "" + +$totalFiles = 0 +$passedFiles = 0 +$failedFiles = 0 +$skippedFiles = 0 +$failedList = @() + +foreach ($file in $luaFiles) { + $totalFiles++ + $baseName = $file.BaseName + $filePath = $file.FullName + + Write-Host "[$totalFiles/$($luaFiles.Count)] Testing: $($file.Name)" -NoNewline + + try { + # 生成官方字节码 + $officialOutput = "$outputDir\${baseName}_official.txt" + & $luacPath -l $filePath 2>&1 | Out-File -FilePath $officialOutput -Encoding utf8 + + # 检查 luac 是否成功 + if ($LASTEXITCODE -ne 0) { + Write-Host " [SKIP - luac failed]" -ForegroundColor Yellow + $skippedFiles++ + continue + } + + # 生成我们的字节码 + $ourOutput = "$outputDir\${baseName}_ours.txt" + & $bytecode_dumpPath $filePath 2>&1 | Out-File -FilePath $ourOutput -Encoding utf8 + + # 检查我们的工具是否成功 + if ($LASTEXITCODE -ne 0) { + Write-Host " [FAIL - compilation error]" -ForegroundColor Red + $failedFiles++ + $failedList += @{ + File = $file.Name + Reason = "Compilation failed" + OurOutput = $ourOutput + } + + if ($StopOnError) { + Write-Host "" + Write-Host "Stopping due to error. Check: $ourOutput" -ForegroundColor Red + exit 1 + } + continue + } + + # 读取并规范化输出进行比较 + # 现在我们的输出格式与官方一致:数字 [数字] 指令 + $officialLines = Get-Content $officialOutput | Where-Object { $_ -match '^\s*\d+\s+\[\d+\]\s+\w+' } + $ourLines = Get-Content $ourOutput | Where-Object { $_ -match '^\s*\d+\s+\[\d+\]\s+\w+' } + + # 简单比较指令数量 + if ($officialLines.Count -ne $ourLines.Count) { + Write-Host " [FAIL - instruction count mismatch: $($officialLines.Count) vs $($ourLines.Count)]" -ForegroundColor Red + $failedFiles++ + $failedList += @{ + File = $file.Name + Reason = "Instruction count mismatch: official=$($officialLines.Count), ours=$($ourLines.Count)" + OfficialOutput = $officialOutput + OurOutput = $ourOutput + } + + if ($StopOnError) { + Write-Host "" + Write-Host "Official: $officialOutput" -ForegroundColor Yellow + Write-Host "Ours: $ourOutput" -ForegroundColor Yellow + exit 1 + } + continue + } + + # 详细比较每条指令 + $mismatch = $false + for ($i = 0; $i -lt $officialLines.Count; $i++) { + # 两边格式现在一致,都是:数字 [数字] 指令 参数 ; 注释 + $officialLine = $officialLines[$i] -replace '^\s*\d+\s+\[\d+\]\s+', '' -replace '\s+', ' ' -replace '\s*;.*$', '' + $ourLine = $ourLines[$i] -replace '^\s*\d+\s+\[\d+\]\s+', '' -replace '\s+', ' ' -replace '\s*;.*$', '' + + # 规范化指令名称(移除注释和额外空格) + $officialLine = $officialLine.Trim() + $ourLine = $ourLine.Trim() + + if ($officialLine -ne $ourLine) { + if (-not $mismatch) { + Write-Host " [FAIL - instruction mismatch at line $($i+1)]" -ForegroundColor Red + $mismatch = $true + } + + if ($Verbose) { + Write-Host " Line $($i+1):" -ForegroundColor Yellow + Write-Host " Official: $officialLine" -ForegroundColor Gray + Write-Host " Ours: $ourLine" -ForegroundColor Gray + } + } + } + + if ($mismatch) { + $failedFiles++ + $failedList += @{ + File = $file.Name + Reason = "Instruction mismatch" + OfficialOutput = $officialOutput + OurOutput = $ourOutput + } + + if ($StopOnError) { + Write-Host "" + Write-Host "Use: code --diff `"$officialOutput`" `"$ourOutput`"" -ForegroundColor Yellow + exit 1 + } + } else { + Write-Host " [PASS]" -ForegroundColor Green + $passedFiles++ + } + + } catch { + Write-Host " [ERROR - $($_.Exception.Message)]" -ForegroundColor Red + $failedFiles++ + $failedList += @{ + File = $file.Name + Reason = $_.Exception.Message + } + + if ($StopOnError) { + throw + } + } +} + +Write-Host "" +Write-Host "==================== SUMMARY ====================" -ForegroundColor Cyan +Write-Host "Total files: $totalFiles" -ForegroundColor White +Write-Host "Passed: $passedFiles" -ForegroundColor Green +Write-Host "Failed: $failedFiles" -ForegroundColor Red +Write-Host "Skipped: $skippedFiles" -ForegroundColor Yellow +Write-Host "=================================================" -ForegroundColor Cyan + +if ($failedFiles -gt 0) { + Write-Host "" + Write-Host "Failed files:" -ForegroundColor Red + foreach ($failed in $failedList) { + Write-Host " - $($failed.File): $($failed.Reason)" -ForegroundColor Yellow + if ($failed.OfficialOutput -and $failed.OurOutput) { + Write-Host " Diff: code --diff `"$($failed.OfficialOutput)`" `"$($failed.OurOutput)`"" -ForegroundColor Gray + } + } + exit 1 +} else { + Write-Host "" + Write-Host "All tests passed! 🎉" -ForegroundColor Green + exit 0 +} diff --git a/compare_bytecode.ps1 b/compare_bytecode.ps1 new file mode 100644 index 0000000..e30305f --- /dev/null +++ b/compare_bytecode.ps1 @@ -0,0 +1,49 @@ +#!/usr/bin/env pwsh + +param( + [Parameter(Mandatory=$true)] + [string]$LuaFile +) + +$ErrorActionPreference = "Stop" + +$baseName = [System.IO.Path]::GetFileNameWithoutExtension($LuaFile) +$luacPath = "lua_src\lua-5.4.8\build\Release\luac.exe" +$bytecode_dumpPath = "target\release\bytecode_dump.exe" + +# 检查文件是否存在 +if (-not (Test-Path $LuaFile)) { + Write-Error "Lua file not found: $LuaFile" + exit 1 +} + +if (-not (Test-Path $luacPath)) { + Write-Error "luac.exe not found: $luacPath" + exit 1 +} + +if (-not (Test-Path $bytecode_dumpPath)) { + Write-Error "bytecode_dump.exe not found. Building..." + cargo build --release --bin bytecode_dump +} + +# 生成官方 Lua 字节码 +Write-Host "Generating official Lua bytecode..." -ForegroundColor Green +& $luacPath -o "${baseName}_official.luac" $LuaFile + +# 使用官方 luac 列出字节码 +Write-Host "`n=== Official Lua Bytecode (luac -l) ===" -ForegroundColor Cyan +& $luacPath -l $LuaFile | Out-File -FilePath "${baseName}_official.txt" -Encoding utf8 +Get-Content "${baseName}_official.txt" + +# 使用我们的实现生成字节码 +Write-Host "`n=== Our Implementation Bytecode ===" -ForegroundColor Cyan +& $bytecode_dumpPath $LuaFile | Out-File -FilePath "${baseName}_ours.txt" -Encoding utf8 +Get-Content "${baseName}_ours.txt" + +# 提示用户对比 +Write-Host "`n=== Comparison ===" -ForegroundColor Yellow +Write-Host "Official output saved to: ${baseName}_official.txt" +Write-Host "Our output saved to: ${baseName}_ours.txt" +Write-Host "`nYou can compare them using:" +Write-Host " code --diff ${baseName}_official.txt ${baseName}_ours.txt" diff --git a/crates/luars/Cargo.toml b/crates/luars/Cargo.toml index ce67d1e..4f0795a 100644 --- a/crates/luars/Cargo.toml +++ b/crates/luars/Cargo.toml @@ -10,10 +10,9 @@ wasm = [] loadlib = ["dep:libloading"] [dependencies] -emmylua_parser.workspace = true libloading = { workspace = true, optional = true } tokio = { workspace = true, optional = true } itoa = "1.0" # Fast integer formatting (10x faster than format!) ryu = "1.0" # Fast float formatting tempfile = "3.10" # For io.tmpfile() -rowan = "0.16.1" + diff --git a/crates/luars/src/compiler/binop.rs b/crates/luars/src/compiler/binop.rs deleted file mode 100644 index 8df4b91..0000000 --- a/crates/luars/src/compiler/binop.rs +++ /dev/null @@ -1,287 +0,0 @@ -//! Binary operation compilation helpers -//! -//! This module consolidates the common patterns for compiling binary operations, -//! eliminating code duplication in expr.rs. - -use super::exp2reg::exp_to_any_reg; -use super::expdesc::*; -use super::helpers::*; -use super::{Compiler, TagMethod}; -use crate::lua_value::LuaValue; -use crate::lua_vm::{Instruction, OpCode}; -use emmylua_parser::BinaryOperator; - -/// Emit arithmetic binary operation (add, sub, mul, div, idiv, mod, pow) -pub fn emit_arith_op( - c: &mut Compiler, - op: BinaryOperator, - left_reg: u32, - right_desc: &mut ExpDesc, - can_reuse: bool, -) -> Result { - let result_reg = if can_reuse { left_reg } else { alloc_register(c) }; - - // Check if right is a constant - let is_const = matches!(right_desc.kind, ExpKind::VKInt | ExpKind::VKFlt | ExpKind::VK); - - // Check for small integer immediate - let imm_val = if let ExpKind::VKInt = right_desc.kind { - let v = right_desc.ival; - if v >= -256 && v <= 255 { Some(v) } else { None } - } else { - None - }; - - // Get opcode info - let (op_rr, op_rk, tm) = match op { - BinaryOperator::OpAdd => (OpCode::Add, Some(OpCode::AddK), TagMethod::Add), - BinaryOperator::OpSub => (OpCode::Sub, Some(OpCode::SubK), TagMethod::Sub), - BinaryOperator::OpMul => (OpCode::Mul, Some(OpCode::MulK), TagMethod::Mul), - BinaryOperator::OpDiv => (OpCode::Div, Some(OpCode::DivK), TagMethod::Div), - BinaryOperator::OpIDiv => (OpCode::IDiv, Some(OpCode::IDivK), TagMethod::IDiv), - BinaryOperator::OpMod => (OpCode::Mod, Some(OpCode::ModK), TagMethod::Mod), - BinaryOperator::OpPow => (OpCode::Pow, Some(OpCode::PowK), TagMethod::Pow), - _ => return Err(format!("Not an arithmetic operator: {:?}", op)), - }; - - // Try immediate form for ADD/SUB - if let Some(imm) = imm_val { - if matches!(op, BinaryOperator::OpAdd) { - let enc_imm = ((imm + 127) & 0xff) as u32; - let mm_imm = ((imm + 128) & 0xff) as u32; - emit(c, Instruction::encode_abc(OpCode::AddI, result_reg, left_reg, enc_imm)); - emit(c, Instruction::create_abck(OpCode::MmBinI, left_reg, mm_imm, tm.as_u32(), false)); - return Ok(result_reg); - } else if matches!(op, BinaryOperator::OpSub) { - // SUB uses ADDI with negated immediate - let neg_imm = ((-imm + 127) & 0xff) as u32; - let neg_mm = ((-imm + 128) & 0xff) as u32; - emit(c, Instruction::encode_abc(OpCode::AddI, result_reg, left_reg, neg_imm)); - emit(c, Instruction::create_abck(OpCode::MmBinI, left_reg, neg_mm, TagMethod::Sub.as_u32(), false)); - return Ok(result_reg); - } - } - - // Try constant form - if is_const { - if let Some(op_k) = op_rk { - let const_idx = ensure_constant_idx(c, right_desc); - emit(c, Instruction::encode_abc(op_k, result_reg, left_reg, const_idx)); - emit(c, Instruction::create_abck(OpCode::MmBinK, left_reg, const_idx, tm.as_u32(), false)); - return Ok(result_reg); - } - } - - // Fall back to register form - let right_reg = exp_to_any_reg(c, right_desc); - emit(c, Instruction::encode_abc(op_rr, result_reg, left_reg, right_reg)); - emit(c, Instruction::create_abck(OpCode::MmBin, left_reg, right_reg, tm.as_u32(), false)); - - Ok(result_reg) -} - -/// Emit bitwise binary operation (band, bor, bxor, shl, shr) -pub fn emit_bitwise_op( - c: &mut Compiler, - op: BinaryOperator, - left_reg: u32, - right_desc: &mut ExpDesc, - can_reuse: bool, -) -> Result { - let result_reg = if can_reuse { left_reg } else { alloc_register(c) }; - - // Check for constant - let is_const = matches!(right_desc.kind, ExpKind::VKInt | ExpKind::VK); - - // Check for shift immediate - let shift_imm = if let ExpKind::VKInt = right_desc.kind { - let v = right_desc.ival; - if v >= -128 && v <= 127 { Some(v) } else { None } - } else { - None - }; - - let (op_rr, op_rk) = match op { - BinaryOperator::OpBAnd => (OpCode::BAnd, Some(OpCode::BAndK)), - BinaryOperator::OpBOr => (OpCode::BOr, Some(OpCode::BOrK)), - BinaryOperator::OpBXor => (OpCode::BXor, Some(OpCode::BXorK)), - BinaryOperator::OpShl => (OpCode::Shl, None), - BinaryOperator::OpShr => (OpCode::Shr, None), - _ => return Err(format!("Not a bitwise operator: {:?}", op)), - }; - - // Try shift immediate - if let Some(imm) = shift_imm { - if matches!(op, BinaryOperator::OpShr) { - let enc = ((imm + 128) & 0xff) as u32; - emit(c, Instruction::encode_abc(OpCode::ShrI, result_reg, left_reg, enc)); - return Ok(result_reg); - } else if matches!(op, BinaryOperator::OpShl) { - let enc = ((imm + 128) & 0xff) as u32; - emit(c, Instruction::encode_abc(OpCode::ShlI, result_reg, left_reg, enc)); - return Ok(result_reg); - } - } - - // Try constant form for band/bor/bxor - if is_const { - if let Some(op_k) = op_rk { - let const_idx = ensure_constant_idx(c, right_desc); - emit(c, Instruction::encode_abc(op_k, result_reg, left_reg, const_idx)); - return Ok(result_reg); - } - } - - // Fall back to register form - let right_reg = exp_to_any_reg(c, right_desc); - emit(c, Instruction::encode_abc(op_rr, result_reg, left_reg, right_reg)); - - Ok(result_reg) -} - -/// Emit comparison operation -pub fn emit_cmp_op( - c: &mut Compiler, - op: BinaryOperator, - left_reg: u32, - right_reg: u32, - can_reuse: bool, -) -> u32 { - let result_reg = if can_reuse { left_reg } else { alloc_register(c) }; - - let (opcode, a, b) = match op { - BinaryOperator::OpEq | BinaryOperator::OpNe => (OpCode::Eq, left_reg, right_reg), - BinaryOperator::OpLt => (OpCode::Lt, left_reg, right_reg), - BinaryOperator::OpLe => (OpCode::Le, left_reg, right_reg), - BinaryOperator::OpGt => (OpCode::Lt, right_reg, left_reg), // Swap - BinaryOperator::OpGe => (OpCode::Le, right_reg, left_reg), // Swap - _ => unreachable!(), - }; - - emit(c, Instruction::encode_abc(opcode, result_reg, a, b)); - result_reg -} - -/// Helper to get constant index from ExpDesc -fn ensure_constant_idx(c: &mut Compiler, e: &ExpDesc) -> u32 { - match e.kind { - ExpKind::VKInt => add_constant_dedup(c, LuaValue::integer(e.ival)), - ExpKind::VKFlt => add_constant_dedup(c, LuaValue::number(e.nval)), - ExpKind::VK => e.info, - _ => 0, - } -} - -//====================================================================================== -// Register-based helpers for compile_binary_expr_to -//====================================================================================== - -/// Emit arithmetic operation with immediate constant (for compile_binary_expr_to) -/// Returns Some(result_reg) if immediate/constant form was used, None otherwise -pub fn emit_arith_imm( - c: &mut Compiler, - op: BinaryOperator, - left_reg: u32, - int_val: i64, - result_reg: u32, -) -> Option { - let imm = ((int_val + 127) & 0xff) as u32; - let imm_mmbini = ((int_val + 128) & 0xff) as u32; - - match op { - BinaryOperator::OpAdd => { - emit(c, Instruction::encode_abc(OpCode::AddI, result_reg, left_reg, imm)); - emit(c, Instruction::create_abck(OpCode::MmBinI, left_reg, imm_mmbini, TagMethod::Add.as_u32(), false)); - Some(result_reg) - } - BinaryOperator::OpSub => { - let neg_imm = ((-int_val + 127) & 0xff) as u32; - emit(c, Instruction::encode_abc(OpCode::AddI, result_reg, left_reg, neg_imm)); - emit(c, Instruction::create_abck(OpCode::MmBinI, left_reg, imm_mmbini, TagMethod::Sub.as_u32(), false)); - Some(result_reg) - } - _ => None, - } -} - -/// Emit arithmetic operation with constant from K table (for compile_binary_expr_to) -pub fn emit_arith_k( - c: &mut Compiler, - op: BinaryOperator, - left_reg: u32, - const_idx: u32, - result_reg: u32, -) -> Option { - let (op_k, tm) = match op { - BinaryOperator::OpAdd => (OpCode::AddK, TagMethod::Add), - BinaryOperator::OpSub => (OpCode::SubK, TagMethod::Sub), - BinaryOperator::OpMul => (OpCode::MulK, TagMethod::Mul), - BinaryOperator::OpDiv => (OpCode::DivK, TagMethod::Div), - BinaryOperator::OpIDiv => (OpCode::IDivK, TagMethod::IDiv), - BinaryOperator::OpMod => (OpCode::ModK, TagMethod::Mod), - BinaryOperator::OpPow => (OpCode::PowK, TagMethod::Pow), - _ => return None, - }; - - emit(c, Instruction::encode_abc(op_k, result_reg, left_reg, const_idx)); - emit(c, Instruction::create_abck(OpCode::MmBinK, left_reg, const_idx, tm.as_u32(), false)); - Some(result_reg) -} - -/// Emit shift operation with immediate (for compile_binary_expr_to) -pub fn emit_shift_imm( - c: &mut Compiler, - op: BinaryOperator, - left_reg: u32, - int_val: i64, - result_reg: u32, -) -> Option { - let imm = ((int_val + 127) & 0xff) as u32; - let imm_mmbini = ((int_val + 128) & 0xff) as u32; - - match op { - BinaryOperator::OpShr => { - emit(c, Instruction::encode_abc(OpCode::ShrI, result_reg, left_reg, imm)); - emit(c, Instruction::create_abck(OpCode::MmBinI, left_reg, imm_mmbini, TagMethod::Shr.as_u32(), false)); - Some(result_reg) - } - BinaryOperator::OpShl => { - // x << n is equivalent to x >> -n - let neg_imm = ((-int_val + 127) & 0xff) as u32; - emit(c, Instruction::encode_abc(OpCode::ShrI, result_reg, left_reg, neg_imm)); - emit(c, Instruction::create_abck(OpCode::MmBinI, left_reg, imm_mmbini, TagMethod::Shl.as_u32(), false)); - Some(result_reg) - } - _ => None, - } -} - -/// Emit register-register binary operation with MMBIN -pub fn emit_binop_rr( - c: &mut Compiler, - op: BinaryOperator, - left_reg: u32, - right_reg: u32, - result_reg: u32, -) { - let (opcode, tm) = match op { - BinaryOperator::OpAdd => (OpCode::Add, Some(TagMethod::Add)), - BinaryOperator::OpSub => (OpCode::Sub, Some(TagMethod::Sub)), - BinaryOperator::OpMul => (OpCode::Mul, Some(TagMethod::Mul)), - BinaryOperator::OpDiv => (OpCode::Div, Some(TagMethod::Div)), - BinaryOperator::OpIDiv => (OpCode::IDiv, Some(TagMethod::IDiv)), - BinaryOperator::OpMod => (OpCode::Mod, Some(TagMethod::Mod)), - BinaryOperator::OpPow => (OpCode::Pow, Some(TagMethod::Pow)), - BinaryOperator::OpBAnd => (OpCode::BAnd, Some(TagMethod::BAnd)), - BinaryOperator::OpBOr => (OpCode::BOr, Some(TagMethod::BOr)), - BinaryOperator::OpBXor => (OpCode::BXor, Some(TagMethod::BXor)), - BinaryOperator::OpShl => (OpCode::Shl, Some(TagMethod::Shl)), - BinaryOperator::OpShr => (OpCode::Shr, Some(TagMethod::Shr)), - _ => return, - }; - - emit(c, Instruction::encode_abc(opcode, result_reg, left_reg, right_reg)); - if let Some(tm) = tm { - emit(c, Instruction::create_abck(OpCode::MmBin, left_reg, right_reg, tm.as_u32(), false)); - } -} diff --git a/crates/luars/src/compiler/code.rs b/crates/luars/src/compiler/code.rs new file mode 100644 index 0000000..cc2bf58 --- /dev/null +++ b/crates/luars/src/compiler/code.rs @@ -0,0 +1,2313 @@ +// Code generation - Port from lcode.c (Lua 5.4.8) +// This file corresponds to lua-5.5.0/src/lcode.c +use crate::compiler::func_state::FuncState; +use crate::compiler::parser::BinaryOperator; +use crate::compiler::tm_kind::TmKind; +use crate::compiler::{ExpUnion, IndVars}; +use crate::lua_value::LuaValueKind; +use crate::lua_vm::{Instruction, OpCode}; + +// Port of int2sC from lcode.c (macro) +// Convert integer to sC format (with OFFSET_sC = 128) +fn int2sc(i: i32) -> u32 { + ((i as u32).wrapping_add(128)) & 0xFF +} + +// Port of fitsC from lcode.c:660-662 +// Check whether 'i' can be stored in an 'sC' operand +fn fits_c(i: i64) -> bool { + let offset_sc = 128i64; + let max_arg_c = 255u32; // MAXARG_C = 255 for 8-bit C field + (i.wrapping_add(offset_sc) as u64) <= (max_arg_c as u64) +} + +// Port of isSCnumber from lcode.c:1257-1271 +// Check whether expression 'e' is a literal integer or float in proper range to fit in sC +fn is_scnumber(e: &ExpDesc, pi: &mut i32, isfloat: &mut bool) -> bool { + let i = match e.kind { + ExpKind::VKINT => e.u.ival(), + ExpKind::VKFLT => { + // Try to convert float to integer + let fval = e.u.nval(); + let ival = fval as i64; + // Check if float is exactly equal to integer (F2Ieq mode) + if (ival as f64) == fval { + *isfloat = true; + ival + } else { + return false; // Not an integer-equivalent float + } + } + _ => return false, // Not a number + }; + + // Check if it has jumps and if it fits in C field + if !e.has_jumps() && fits_c(i) { + *pi = int2sc(i as i32) as i32; + true + } else { + false + } +} + +// Port of const2exp from lcode.c:693-720 +// Convert a constant value to an expression +pub fn const_to_exp(value: LuaValue, e: &mut ExpDesc) { + match value.kind() { + LuaValueKind::Integer => { + e.kind = ExpKind::VKINT; + e.u = ExpUnion::IVal(value.as_integer().unwrap_or(0)); + } + LuaValueKind::Float => { + e.kind = ExpKind::VKFLT; + e.u = ExpUnion::NVal(value.as_float().unwrap_or(0.0)); + } + LuaValueKind::Boolean => { + if value.as_boolean().unwrap_or(false) { + e.kind = ExpKind::VTRUE; + } else { + e.kind = ExpKind::VFALSE; + } + } + LuaValueKind::Nil => { + e.kind = ExpKind::VNIL; + } + LuaValueKind::String => { + e.kind = ExpKind::VKSTR; + e.u = ExpUnion::Info(value.as_string_id().unwrap_or(StringId(0)).0 as i32); + } + _ => { + // Other types shouldn't appear as compile-time constants + e.kind = ExpKind::VNIL; + } + } +} + +// Port of luaK_codeABC from lcode.c:397-402 +// int luaK_codeABCk (FuncState *fs, OpCode o, int a, int b, int c, int k) +pub fn code_abc(fs: &mut FuncState, op: OpCode, a: u32, b: u32, c: u32) -> usize { + let mut instr = (op as u32) << Instruction::POS_OP; + Instruction::set_a(&mut instr, a); + Instruction::set_b(&mut instr, b); + Instruction::set_c(&mut instr, c); + let pc = fs.pc; + + fs.chunk.code.push(instr); + fs.chunk.line_info.push(fs.lexer.lastline as u32); // Use lastline (lcode.c:389) + fs.pc += 1; + pc +} + +// Port of luaK_codeABx from lcode.c:409-414 +// int luaK_codeABx (FuncState *fs, OpCode o, int a, unsigned int bc) +pub fn code_abx(fs: &mut FuncState, op: OpCode, a: u32, bx: u32) -> usize { + let mut instr = (op as u32) << Instruction::POS_OP; + Instruction::set_a(&mut instr, a); + Instruction::set_bx(&mut instr, bx); + let pc = fs.pc; + fs.chunk.code.push(instr); + fs.chunk.line_info.push(fs.lexer.lastline as u32); // Use lastline (lcode.c:390) + fs.pc += 1; + pc +} + +// Port of codeAsBx from lcode.c:419-424 +// static int codeAsBx (FuncState *fs, OpCode o, int a, int bc) +pub fn code_asbx(fs: &mut FuncState, op: OpCode, a: u32, sbx: i32) -> usize { + let mut instr = (op as u32) << Instruction::POS_OP; + Instruction::set_a(&mut instr, a); + let bx = (sbx + Instruction::OFFSET_SBX) as u32; + Instruction::set_bx(&mut instr, bx); + let pc = fs.pc; + fs.chunk.code.push(instr); + fs.chunk.line_info.push(fs.lexer.lastline as u32); // Use lastline + fs.pc += 1; + pc +} + +// Port of luaK_codeABCk from lcode.c:397-402 +// int luaK_codeABCk (FuncState *fs, OpCode o, int a, int b, int c, int k) +pub fn code_abck(fs: &mut FuncState, op: OpCode, a: u32, b: u32, c: u32, k: bool) -> usize { + let mut instr = (op as u32) << Instruction::POS_OP; + Instruction::set_a(&mut instr, a); + Instruction::set_b(&mut instr, b); + Instruction::set_c(&mut instr, c); + Instruction::set_k(&mut instr, k); + let pc = fs.pc; + fs.chunk.code.push(instr); + fs.chunk.line_info.push(fs.lexer.lastline as u32); // Use lastline + fs.pc += 1; + pc +} + +// Generate instruction with sJ field (for JMP) +pub fn code_asj(fs: &mut FuncState, op: OpCode, sj: i32) -> usize { + let instr = Instruction::create_sj(op, sj); + let pc = fs.pc; + fs.chunk.code.push(instr); + fs.chunk.line_info.push(fs.lexer.lastline as u32); // Use lastline + fs.pc += 1; + pc +} + +use crate::compiler::expression::{ExpDesc, ExpKind}; +use crate::{LuaValue, StringId}; + +// Port of luaK_ret from lcode.c:207-214 +// void luaK_ret (FuncState *fs, int first, int nret) +pub fn ret(fs: &mut FuncState, first: u8, nret: u8) -> usize { + // Use optimized RETURN0/RETURN1 when possible + let op = match nret { + 0 => OpCode::Return0, + 1 => OpCode::Return1, + _ => OpCode::Return, + }; + code_abc(fs, op, first as u32, nret.wrapping_add(1) as u32, 0) +} + +// Port of luaK_finish from lcode.c:1847-1876 +// void luaK_finish (FuncState *fs) +pub fn finish(fs: &mut FuncState) { + let needclose = fs.needclose; + let is_vararg = fs.is_vararg; + let num_params = fs.chunk.param_count; + + for i in 0..fs.pc { + let instr = &mut fs.chunk.code[i]; + let opcode = OpCode::from(Instruction::get_opcode(*instr)); + + match opcode { + OpCode::Return0 | OpCode::Return1 => { + // lcode.c:1854-1859: Convert RETURN0/RETURN1 to RETURN if needed + if needclose || is_vararg { + // Convert to RETURN + let a = Instruction::get_a(*instr); + let b = Instruction::get_b(*instr); + let mut new_instr = Instruction::create_abck(OpCode::Return, a, b, 0, false); + + // lcode.c:1861-1865: Set k and C fields + if needclose { + Instruction::set_k(&mut new_instr, true); + } + if is_vararg { + Instruction::set_c(&mut new_instr, (num_params + 1) as u32); + } + + *instr = new_instr; + } + } + OpCode::Return | OpCode::TailCall => { + // lcode.c:1861-1865: Set k and C fields for existing RETURN/TAILCALL + if needclose { + Instruction::set_k(instr, true); + } + if is_vararg { + Instruction::set_c(instr, (num_params + 1) as u32); + } + } + OpCode::Jmp => { + // lcode.c:1867-1870: Fix jumps to final target + let target = finaltarget(&fs.chunk.code, i); + fixjump_at(fs, i, target); + } + _ => {} + } + } +} + +// Helper for finish: find final target of a jump chain +fn finaltarget(code: &[u32], mut pc: usize) -> usize { + let mut count = 0; + while count < 100 { + // Prevent infinite loops + if pc >= code.len() { + break; + } + let instr = code[pc]; + if OpCode::from(Instruction::get_opcode(instr)) != OpCode::Jmp { + break; + } + let offset = Instruction::get_sj(instr) as isize; + if offset == -1 { + break; + } + let next_pc = (pc as isize) + 1 + offset; + if next_pc < 0 || next_pc >= code.len() as isize { + break; + } + pc = next_pc as usize; + count += 1; + } + pc +} + +// Helper for finish: fix jump at specific pc +fn fixjump_at(fs: &mut FuncState, pc: usize, target: usize) { + let offset = (target as isize) - (pc as isize) - 1; + if offset < i32::MIN as isize || offset > i32::MAX as isize { + return; + } + + let instr = &mut fs.chunk.code[pc]; + Instruction::set_sj(instr, offset as i32); +} + +// Port of luaK_jump from lcode.c:200-202 +// int luaK_jump (FuncState *fs) +pub fn jump(fs: &mut FuncState) -> usize { + code_asj(fs, OpCode::Jmp, -1) +} + +// Port of luaK_jumpto macro from lcode.h:60 +// #define luaK_jumpto(fs,t) luaK_patchlist(fs, luaK_jump(fs), t) +pub fn jumpto(fs: &mut FuncState, target: usize) { + let jmp = jump(fs); + patchlist(fs, jmp as isize, target as isize); +} + +// Port of luaK_getlabel from lcode.c:234-237 +// int luaK_getlabel (FuncState *fs) +// Port of lcode.c:233-236 +pub fn get_label(fs: &mut FuncState) -> usize { + fs.last_target = fs.pc; + fs.pc +} + +// Port of luaK_patchtohere from lcode.c:312-315 +// void luaK_patchtohere (FuncState *fs, int list) +pub fn patchtohere(fs: &mut FuncState, list: isize) { + let here = get_label(fs) as isize; + patchlist(fs, list, here); +} + +// Port of luaK_concat from lcode.c:174-186 +// void luaK_concat (FuncState *fs, int *l1, int l2) +pub fn concat(fs: &mut FuncState, l1: &mut isize, l2: isize) { + if l2 == -1 { + return; + } + if *l1 == -1 { + *l1 = l2; + } else { + let mut list = *l1; + let mut next = get_jump(fs, list as usize); + while next != -1 { + list = next; + next = get_jump(fs, list as usize); + } + fix_jump(fs, list as usize, l2 as usize); + } +} + +// Port of luaK_getlabel from lcode.c:233-236 +// int luaK_getlabel (FuncState *fs) +// Marks current position as a jump target and returns the current pc. +// This updates lasttarget which prevents instruction merging/optimization +// at jump targets (e.g., loop entry points should not merge with previous LOADNIL). +pub fn getlabel(fs: &mut FuncState) -> usize { + fs.last_target = fs.pc; + fs.pc +} + +// Port of luaK_patchlist from lcode.c:307-310 +// void luaK_patchlist (FuncState *fs, int list, int target) +pub fn patchlist(fs: &mut FuncState, list: isize, target: isize) { + if target < 0 { + return; + } + // lcode.c:309: patchlistaux(fs, list, target, NO_REG, target); + patchlistaux(fs, list, target, NO_REG as u8, target); +} + +// Helper: get jump target from instruction +// Port of getjump from lcode.c:259-265 +fn get_jump(fs: &FuncState, pc: usize) -> isize { + if pc >= fs.chunk.code.len() { + return -1; + } + let instr = fs.chunk.code[pc]; + let offset = Instruction::get_sj(instr); + if offset == -1 { + -1 + } else { + let target = (pc as isize) + 1 + (offset as isize); + // Ensure target is valid (non-negative) + if target < 0 { -1 } else { target } + } +} + +pub fn fix_jump(fs: &mut FuncState, pc: usize, target: usize) { + if pc >= fs.chunk.code.len() { + return; + } + let offset = (target as isize) - (pc as isize) - 1; + let max_sj = (Instruction::MAX_SJ >> 1) as isize; + if offset < -max_sj || offset > max_sj { + // Error: jump too long + return; + } + let instr = &mut fs.chunk.code[pc]; + Instruction::set_sj(instr, offset as i32); +} + +// Port of need_value from lcode.c:900-908 +// static int need_value (FuncState *fs, int list) +// Check whether list has any jump that do not produce a value +fn need_value(fs: &FuncState, mut list: isize) -> bool { + const NO_JUMP: isize = -1; + while list != NO_JUMP { + let control_pc = get_jump_control(fs, list as usize); + let i = fs.chunk.code[control_pc]; + let op = OpCode::from(Instruction::get_opcode(i)); + if op != OpCode::TestSet { + return true; + } + list = get_jump(fs, list as usize); + } + false +} + +// Port of code_loadbool from lcode.c:889-892 +// static int code_loadbool (FuncState *fs, int A, OpCode op) +fn code_loadbool(fs: &mut FuncState, a: u8, op: OpCode) -> usize { + get_label(fs); // those instructions may be jump targets + code_abc(fs, op, a as u32, 0, 0) +} + +// Port of patchtestreg from lcode.c:260-273 +// static int patchtestreg (FuncState *fs, int node, int reg) +fn patchtestreg(fs: &mut FuncState, node: usize, reg: u8) -> bool { + let pc = get_jump_control(fs, node); + if pc >= fs.chunk.code.len() { + return false; + } + + let instr = fs.chunk.code[pc]; + if OpCode::from(Instruction::get_opcode(instr)) != OpCode::TestSet { + return false; // Not a TESTSET instruction + } + + let b = Instruction::get_b(instr); + if reg != NO_REG as u8 && reg != b as u8 { + // Set destination register + Instruction::set_a(&mut fs.chunk.code[pc], reg as u32); + } else { + // No register to put value or register already has the value + // Change instruction to simple TEST + let k = Instruction::get_k(instr); + fs.chunk.code[pc] = Instruction::create_abck(OpCode::Test, b, 0, 0, k); + } + true +} + +// Port of patchlistaux from lcode.c:271-292 +// static void patchlistaux (FuncState *fs, int list, int vtarget, int reg, int dtarget) +// Port of patchlistaux from lcode.c:285-298 +// Traverse a list of tests, patching their destination address and registers +// Tests producing values jump to 'vtarget' (and put their values in 'reg') +// Other tests jump to 'dtarget' +fn patchlistaux(fs: &mut FuncState, mut list: isize, vtarget: isize, reg: u8, dtarget: isize) { + const NO_JUMP: isize = -1; + while list != NO_JUMP { + let next = get_jump(fs, list as usize); + // lcode.c:293: if (patchtestreg(fs, list, reg)) + if patchtestreg(fs, list as usize, reg) { + // lcode.c:294: fixjump(fs, list, vtarget); + fix_jump(fs, list as usize, vtarget as usize); + } else { + // lcode.c:296: fixjump(fs, list, dtarget); + fix_jump(fs, list as usize, dtarget as usize); + } + list = next; + } +} + +// Port of luaK_exp2nextreg from lcode.c:944-949 +// void luaK_exp2nextreg (FuncState *fs, expdesc *e) +pub fn exp2nextreg(fs: &mut FuncState, e: &mut ExpDesc) -> u8 { + discharge_vars(fs, e); + free_exp(fs, e); + reserve_regs(fs, 1); + let reg = fs.freereg - 1; + exp2reg(fs, e, reg); + reg +} + +// Port of luaK_exp2anyreg from lcode.c:956-972 +// int luaK_exp2anyreg (FuncState *fs, expdesc *e) +pub fn exp2anyreg(fs: &mut FuncState, e: &mut ExpDesc) -> u8 { + discharge_vars(fs, e); + if e.kind == ExpKind::VNONRELOC { + if !e.has_jumps() { + return e.u.info() as u8; + } + if e.u.info() >= fs.nactvar as i32 { + exp2reg(fs, e, e.u.info() as u8); + return e.u.info() as u8; + } + } + exp2nextreg(fs, e) +} + +// Port of exp2reg from lcode.c:915-938 +// static void exp2reg (FuncState *fs, expdesc *e, int reg) +pub fn exp2reg(fs: &mut FuncState, e: &mut ExpDesc, reg: u8) { + // lcode.c:918: must check VJMP BEFORE discharge2reg, as it changes the kind! + let was_vjmp = e.kind == ExpKind::VJMP; + let vjmp_info = if was_vjmp { e.u.info() as isize } else { -1 }; + + discharge2reg(fs, e, reg); + + if was_vjmp { + // expression itself is a test - put this jump in 't' list + concat(fs, &mut e.t, vjmp_info); + } + if e.has_jumps() { + // lcode.c:919-934: need to patch jump lists + let mut p_f = -1_isize; // position of an eventual LOAD false + let mut p_t = -1_isize; // position of an eventual LOAD true + + if need_value(fs, e.t) || need_value(fs, e.f) { + // lcode.c:922-926 + // Note: must use was_vjmp, not e.kind, because discharge2reg already changed it! + let fj = if was_vjmp { -1 } else { jump(fs) as isize }; + p_f = code_loadbool(fs, reg, OpCode::LFalseSkip) as isize; + p_t = code_loadbool(fs, reg, OpCode::LoadTrue) as isize; + patchtohere(fs, fj); + } + let final_label = get_label(fs) as isize; + patchlistaux(fs, e.f, final_label, reg, p_f); + patchlistaux(fs, e.t, final_label, reg, p_t); + } + e.f = -1; + e.t = -1; + e.u = ExpUnion::Info(reg as i32); + e.kind = ExpKind::VNONRELOC; +} + +// Port of luaK_dischargevars from lcode.c:766-817 +// void luaK_dischargevars (FuncState *fs, expdesc *e) +pub fn discharge_vars(fs: &mut FuncState, e: &mut ExpDesc) { + match e.kind { + ExpKind::VCONST => { + // Convert const variable to its actual value + let vidx = e.u.info() as usize; + if let Some(var_desc) = fs.actvar.get(vidx) { + if let Some(value) = var_desc.const_value { + // Convert the const value to an expression + const_to_exp(value, e); + } + } + } + ExpKind::VLOCAL => { + e.u = ExpUnion::Info(e.u.var().ridx as i32); + e.kind = ExpKind::VNONRELOC; + } + ExpKind::VUPVAL => { + e.u = ExpUnion::Info(code_abc(fs, OpCode::GetUpval, 0, e.u.info() as u32, 0) as i32); + e.kind = ExpKind::VRELOC; + } + ExpKind::VINDEXUP => { + e.u = ExpUnion::Info(code_abc( + fs, + OpCode::GetTabUp, + 0, + e.u.ind().t as u32, + e.u.ind().idx as u32, + ) as i32); + e.kind = ExpKind::VRELOC; + } + ExpKind::VINDEXI => { + free_reg(fs, e.u.ind().t as u8); + e.u = ExpUnion::Info(code_abc( + fs, + OpCode::GetI, + 0, + e.u.ind().t as u32, + e.u.ind().idx as u32, + ) as i32); + e.kind = ExpKind::VRELOC; + } + ExpKind::VINDEXSTR => { + free_reg(fs, e.u.ind().t as u8); + e.u = ExpUnion::Info(code_abc( + fs, + OpCode::GetField, + 0, + e.u.ind().t as u32, + e.u.ind().idx as u32, + ) as i32); + e.kind = ExpKind::VRELOC; + } + ExpKind::VINDEXED => { + free_reg(fs, e.u.ind().idx as u8); + free_reg(fs, e.u.ind().t as u8); + e.u = ExpUnion::Info(code_abc( + fs, + OpCode::GetTable, + 0, + e.u.ind().t as u32, + e.u.ind().idx as u32, + ) as i32); + e.kind = ExpKind::VRELOC; + } + ExpKind::VVARARG | ExpKind::VCALL => { + setoneret(fs, e); + } + _ => {} + } +} + +// Port of discharge2reg from lcode.c:822-875 +// static void discharge2reg (FuncState *fs, expdesc *e, int reg) +pub fn discharge2reg(fs: &mut FuncState, e: &mut ExpDesc, reg: u8) { + discharge_vars(fs, e); + match e.kind { + ExpKind::VNIL => { + // lcode.c:831: luaK_nil(fs, reg, 1) + nil(fs, reg, 1); // 1 register + } + ExpKind::VFALSE => { + // lcode.c:832 + code_abc(fs, OpCode::LoadFalse, reg as u32, 0, 0); + } + ExpKind::VTRUE => { + // lcode.c:835 + code_abc(fs, OpCode::LoadTrue, reg as u32, 0, 0); + } + ExpKind::VKSTR => { + // lcode.c:838-839: str2K(fs, e); FALLTHROUGH to VK + str2k(fs, e); + // Now fall through to VK case + code_abx(fs, OpCode::LoadK, reg as u32, e.u.info() as u32); + } + ExpKind::VK => { + // lcode.c:841: luaK_codek(fs, reg, e->u.info); + code_abx(fs, OpCode::LoadK, reg as u32, e.u.info() as u32); + } + ExpKind::VKFLT => { + // lcode.c:681-687: luaK_float(fs, reg, e->u.nval); + // Try to use LOADF if float can be exactly represented as integer in sBx range + let val = e.u.nval(); + + // Check if float can be exactly converted to integer (no fractional part) + if val.fract() == 0.0 && val.is_finite() { + let int_val = val as i64; + // Check if integer fits in sBx range (17-bit signed): -65535 to 65535 + // Same range as LOADI (uses fitsBx in official Lua) + let max_sbx = (Instruction::MAX_BX - (Instruction::OFFSET_SBX as u32)) as i64; + let min_sbx = -(Instruction::OFFSET_SBX as i64); + if int_val >= min_sbx && int_val <= max_sbx { + // Use LOADF: encodes integer in sBx, loads as float at runtime + code_asbx(fs, OpCode::LoadF, reg as u32, int_val as i32); + } else { + // Value too large, use LOADK + let k_idx = number_k(fs, val); + code_abx(fs, OpCode::LoadK, reg as u32, k_idx as u32); + } + } else { + // Float has fractional part, use LOADK + let k_idx = number_k(fs, val); + code_abx(fs, OpCode::LoadK, reg as u32, k_idx as u32); + } + } + ExpKind::VKINT => { + // Check if value fits in sBx field (17-bit signed) + // Port of luaK_int from lcode.c:673-678 and fitsBx from lcode.c:668-670 + let ival = e.u.ival(); + // sBx range check: -OFFSET_sBx to (MAXARG_Bx - OFFSET_sBx) + // OFFSET_sBx = 65535, MAXARG_Bx = 131071, so range is -65535 to 65535 + let max_sbx = (Instruction::MAX_BX - (Instruction::OFFSET_SBX as u32)) as i64; + let min_sbx = -(Instruction::OFFSET_SBX as i64); + if ival >= min_sbx && ival <= max_sbx { + code_asbx(fs, OpCode::LoadI, reg as u32, ival as i32); + } else { + // Value too large for LOADI, must use LOADK + let k_idx = integer_k(fs, ival); + code_abx(fs, OpCode::LoadK, reg as u32, k_idx as u32); + } + } + ExpKind::VNONRELOC => { + if e.u.info() != reg as i32 { + code_abc(fs, OpCode::Move, reg as u32, e.u.info() as u32, 0); + } + } + ExpKind::VRELOC => { + let pc = e.u.info() as usize; + Instruction::set_a(&mut fs.chunk.code[pc], reg as u32); + } + _ => {} + } + e.kind = ExpKind::VNONRELOC; + e.u = ExpUnion::Info(reg as i32); +} + +// Port of freeexp from lcode.c:558-561 +// static void freeexp (FuncState *fs, expdesc *e) +pub fn free_exp(fs: &mut FuncState, e: &ExpDesc) { + if e.kind == ExpKind::VNONRELOC { + free_reg(fs, e.u.info() as u8); + } +} + +// Port of freeexps from lcode.c:567-572 +// Free registers used by expressions 'e1' and 'e2' (if any) in proper order +fn free_exps(fs: &mut FuncState, e1: &ExpDesc, e2: &ExpDesc) { + let r1 = if e1.kind == ExpKind::VNONRELOC { + e1.u.info() + } else { + -1 + }; + let r2 = if e2.kind == ExpKind::VNONRELOC { + e2.u.info() + } else { + -1 + }; + + // Free in proper order (higher register first) + if r1 > r2 { + if r1 >= 0 { + free_reg(fs, r1 as u8); + } + if r2 >= 0 { + free_reg(fs, r2 as u8); + } + } else { + if r2 >= 0 { + free_reg(fs, r2 as u8); + } + if r1 >= 0 { + free_reg(fs, r1 as u8); + } + } +} + +// Port of freereg from lcode.c:527-531 +// static void freereg (FuncState *fs, int reg) +// Port of freereg from lcode.c:492-498 +// static void freereg (FuncState *fs, int reg) +pub fn free_reg(fs: &mut FuncState, reg: u8) { + // lcode.c:493: if (reg >= luaY_nvarstack(fs)) + // Use nvarstack() which skips const variables, not nactvar + let nvars = fs.nvarstack(); + if reg >= nvars && reg < fs.freereg { + fs.freereg -= 1; + // lcode.c:495: lua_assert(reg == fs->freereg); + debug_assert_eq!( + reg, fs.freereg, + "freereg mismatch: expected reg {} to equal freereg {}", + reg, fs.freereg + ); + } +} + +// Port of luaK_reserveregs from lcode.c:511-516 +// void luaK_reserveregs (FuncState *fs, int n) +pub fn reserve_regs(fs: &mut FuncState, n: u8) { + fs.freereg = fs.freereg.saturating_add(n); + if (fs.freereg as usize) > fs.chunk.max_stack_size { + fs.chunk.max_stack_size = fs.freereg as usize; + } +} + +// Port of luaK_nil from lcode.c:136-155 +// void luaK_nil (FuncState *fs, int from, int n) +pub fn nil(fs: &mut FuncState, from: u8, n: u8) { + if n == 0 { + return; + } + + let pc = fs.pc; + + // Optimization: merge with previous LOADNIL if registers are contiguous + // Port of lcode.c:136-151 optimization logic + // Check if previous instruction exists and is not a jump target (lcode.c:117-123) + if pc > 0 && pc > fs.last_target { + let prev_pc = pc - 1; + let prev_instr = fs.chunk.code[prev_pc]; + + if Instruction::get_opcode(prev_instr) == OpCode::LoadNil { + let pfrom = Instruction::get_a(prev_instr) as u8; + let pl = pfrom + Instruction::get_b(prev_instr) as u8; // Last register in previous LOADNIL + let l = from + n - 1; // Last register in new LOADNIL + + // Check if ranges can be connected (lcode.c:139-140) + // Two cases: + // 1. New range is to the right: pfrom <= from && from <= pl + 1 + // 2. New range is to the left: from <= pfrom && pfrom <= l + 1 + if (pfrom <= from && from <= pl + 1) || (from <= pfrom && pfrom <= l + 1) { + // Merge: compute union of both ranges (lcode.c:141-143) + let new_from = pfrom.min(from); + let new_l = pl.max(l); + let new_b = new_l - new_from; + + // Update previous instruction (lcode.c:144-145) + Instruction::set_a(&mut fs.chunk.code[prev_pc], new_from as u32); + Instruction::set_b(&mut fs.chunk.code[prev_pc], new_b as u32); + return; + } + } + } + + // Cannot merge, emit a new LOADNIL instruction (lcode.c:149) + code_abc(fs, OpCode::LoadNil, from as u32, (n - 1) as u32, 0); +} + +// Port of luaK_setoneret from lcode.c:755-766 +// void luaK_setoneret (FuncState *fs, expdesc *e) +// Adjust a call/vararg expression to produce exactly one result. +// Calls are created returning one result (C=2), so they don't need fixing. +// For VCALL, just changes the expression type to VNONRELOC with fixed position. +// For VVARARG, sets C=2 (one result) and changes to VRELOC. +pub fn setoneret(fs: &mut FuncState, e: &mut ExpDesc) { + if e.kind == ExpKind::VCALL { + // lcode.c:758: already returns 1 value + // lcode.c:759: lua_assert(GETARG_C(getinstruction(fs, e)) == 2); + let pc = e.u.info() as usize; + debug_assert_eq!(Instruction::get_c(fs.chunk.code[pc]), 2); // Should already be 2 + + // lcode.c:760-761: e->k = VNONRELOC; e->u.info = GETARG_A(...) + e.kind = ExpKind::VNONRELOC; + e.u = ExpUnion::Info(Instruction::get_a(fs.chunk.code[pc]) as i32); + } else if e.kind == ExpKind::VVARARG { + // lcode.c:763: SETARG_C(getinstruction(fs, e), 2); + let pc = e.u.info() as usize; + Instruction::set_c(&mut fs.chunk.code[pc], 2); + // lcode.c:764: e->k = VRELOC; + e.kind = ExpKind::VRELOC; + } +} + +// Port of luaK_setreturns from lcode.c:722-732 +// void luaK_setreturns (FuncState *fs, expdesc *e, int nresults) +pub fn setreturns(fs: &mut FuncState, e: &mut ExpDesc, nresults: u8) { + let pc = e.u.info() as usize; + if e.kind == ExpKind::VCALL { + Instruction::set_c(&mut fs.chunk.code[pc], (nresults as u32) + 1); + } else { + // Must be VVARARG + Instruction::set_c(&mut fs.chunk.code[pc], (nresults as u32) + 1); + Instruction::set_a(&mut fs.chunk.code[pc], fs.freereg as u32); + reserve_regs(fs, 1); + } +} + +// Port of tonumeral from lcode.c:59-73 +// static int tonumeral (const expdesc *e, TValue *v) +fn tonumeral(e: &ExpDesc, _v: Option<&mut f64>) -> bool { + if e.has_jumps() { + return false; + } + match e.kind { + ExpKind::VKINT | ExpKind::VKFLT => true, + _ => false, + } +} + +const MAXINDEXRK: usize = 255; // Maximum index for R/K operands + +// Add boolean true to constants +// Port of boolT from lcode.c:636-640 +fn bool_t(fs: &mut FuncState) -> usize { + // Use boolean value itself as both key and value (lcode.c:639) + let val = LuaValue::boolean(true); + add_constant(fs, val) +} + +// Add boolean false to constants +// Port of boolF from lcode.c:626-630 +fn bool_f(fs: &mut FuncState) -> usize { + // Use boolean value itself as both key and value (lcode.c:629) + let val = LuaValue::boolean(false); + add_constant(fs, val) +} + +// Add nil to constants +// Port of nilK from lcode.c:646-652 +fn nil_k(fs: &mut FuncState) -> usize { + // Cannot use nil as key; use a special sentinel instead + // In Lua C, they use the hash table itself as key (lcode.c:650) + + // but we are rust! So we can just use LuaValue::nil() as both key and value + let val = LuaValue::nil(); + add_constant(fs, val) +} + +// Add integer to constants +fn int_k(fs: &mut FuncState, i: i64) -> usize { + let val = LuaValue::integer(i); + add_constant(fs, val) +} + +// Add number to constants +fn number_k(fs: &mut FuncState, n: f64) -> usize { + let val = LuaValue::float(n); + add_constant(fs, val) +} + +// Add an integer constant to the constant table +// Port of luaK_int from lcode.c:717 (constant table part) +fn integer_k(fs: &mut FuncState, i: i64) -> usize { + let val = LuaValue::integer(i); + add_constant(fs, val) +} + +// Port of stringK from lcode.c:576-580 +// static int stringK (FuncState *fs, TString *s) +pub fn string_k(fs: &mut FuncState, s: String) -> usize { + // Intern string to ObjectPool and get StringId + let (string_id, _) = fs.pool.create_string(&s); + + // Add LuaValue with StringId to constants (check for duplicates) + // For strings, key == value (strings are deduplicated globally) + let value = LuaValue::string(string_id); + add_constant(fs, value) +} + +// Port of str2K from lcode.c:738-742 +// static void str2K (FuncState *fs, expdesc *e) +// Convert a VKSTR to a VK +// static void str2K (FuncState *fs, expdesc *e) { +// lua_assert(e->k == VKSTR); +// e->u.info = stringK(fs, e->u.strval); +// e->k = VK; +// } +fn str2k(fs: &mut FuncState, e: &mut ExpDesc) { + if e.kind == ExpKind::VKSTR { + // String is already in constants (stored in e.u.info), just change kind + e.kind = ExpKind::VK; + let string_id = StringId(e.u.info() as u32); + let value = LuaValue::string(string_id); + // For strings, key == value + let const_idx = add_constant(fs, value); + e.u = ExpUnion::Info(const_idx as i32); + } +} + +// Helper to add constant to chunk +// Port of addk from lcode.c:544-571 +// key: The key used for lookup in scanner table (for deduplication) +// value: The actual value to store in the constant table +// +// For nil/false/true, key should be a special sentinel value to ensure global deduplication +// For strings, key == value (strings are deduplicated globally) +// For other types (numbers), key == value (numbers are deduplicated per-function) +// but we use rust's HashMap to handle that, so we just pass the same value for both key and value. +fn add_constant(fs: &mut FuncState, value: LuaValue) -> usize { + // Query scanner table with key (lcode.c:548) + // We need to manually search because long strings require content comparison via ObjectPool + let mut found_idx: Option = None; + + for (&candidate_value, &idx) in fs.compiler_state.scanner_table.iter() { + // Use raw_equal_with_pool for proper long string content comparison + if value.raw_equal(&candidate_value, fs.pool) { + found_idx = Some(idx); + break; + } + } + + if let Some(idx) = found_idx { + // Check if we can reuse this constant (lcode.c:550-555) + // Must check: within bounds, same type tag, and equal value + if idx < fs.chunk.constants.len() + && let Some(existing) = fs.chunk.constants.get(idx) + { + // Must match both type and value (distinguishes float from integer) + if existing.kind() == value.kind() && value.raw_equal(existing, fs.pool) { + return idx; + } + } + } + + // Constant not found or cannot be reused; create a new entry (lcode.c:558-569) + let idx = fs.chunk.constants.len(); + fs.chunk.constants.push(value.clone()); + + // Store key->index mapping in scanner table (lcode.c:562-563) + fs.compiler_state.scanner_table.insert(value.clone(), idx); + + // Also update local map for backward compatibility + fs.chunk_constants_map.insert(value.clone(), idx); + + idx +} + +// Port of luaK_exp2K from lcode.c:1000-1026 +// static int luaK_exp2K (FuncState *fs, expdesc *e) +fn exp2k(fs: &mut FuncState, e: &mut ExpDesc) -> bool { + if !e.has_jumps() { + // Handle VCONST: convert compile-time constant to actual constant expression + if e.kind == ExpKind::VCONST { + let vidx = e.u.info() as usize; + if let Some(var) = fs.actvar.get(vidx) { + if let Some(value) = var.const_value { + const_to_exp(value, e); + // Continue to process the converted expression + } else { + return false; + } + } else { + return false; + } + } + + // Handle VKSTR: convert to VK with proper constant index + if e.kind == ExpKind::VKSTR { + str2k(fs, e); + // Now e.kind is VK, continue below + } + + let info = match e.kind { + ExpKind::VTRUE => bool_t(fs), + ExpKind::VFALSE => bool_f(fs), + ExpKind::VNIL => nil_k(fs), + ExpKind::VKINT => int_k(fs, e.u.ival()), + ExpKind::VKFLT => number_k(fs, e.u.nval()), + ExpKind::VK => e.u.info() as usize, + _ => return false, + }; + + if info <= MAXINDEXRK { + e.kind = ExpKind::VK; + e.u = ExpUnion::Info(info as i32); + return true; + } + } + false +} + +// Port of exp2RK from lcode.c:1036-1042 +// static int exp2RK (FuncState *fs, expdesc *e) +fn exp2rk(fs: &mut FuncState, e: &mut ExpDesc) -> bool { + if exp2k(fs, e) { + true + } else { + exp2anyreg(fs, e); + false + } +} + +// Port of getjumpcontrol from lcode.c:245-250 +// static Instruction *getjumpcontrol (FuncState *fs, int pc) +fn get_jump_control(fs: &FuncState, pc: usize) -> usize { + if pc >= 1 && pc < fs.chunk.code.len() { + let prev_instr = fs.chunk.code[pc - 1]; + let prev_op = OpCode::from(Instruction::get_opcode(prev_instr)); + // Check if previous instruction is a test mode instruction + if matches!( + prev_op, + OpCode::Test + | OpCode::TestSet + | OpCode::Eq + | OpCode::Lt + | OpCode::Le + | OpCode::EqK + | OpCode::EqI + | OpCode::LtI + | OpCode::LeI + | OpCode::GtI + | OpCode::GeI + ) { + return pc - 1; + } + } + pc +} + +// Port of negatecondition from lcode.c:1103-1108 +// static void negatecondition (FuncState *fs, expdesc *e) +fn negatecondition(fs: &mut FuncState, e: &mut ExpDesc) { + let pc = get_jump_control(fs, e.u.info() as usize); + if pc < fs.chunk.code.len() { + let instr = &mut fs.chunk.code[pc]; + let k = Instruction::get_k(*instr); + Instruction::set_k(instr, !k); + } +} + +// Port of condjump from lcode.c:223-226 +// static int condjump (FuncState *fs, OpCode op, int A, int B, int C, int k) +fn condjump(fs: &mut FuncState, op: OpCode, a: u32, b: u32, c: u32, k: bool) -> isize { + code_abck(fs, op, a, b, c, k); + jump(fs) as isize +} + +// Port of discharge2anyreg from lcode.c:882-886 +// static void discharge2anyreg (FuncState *fs, expdesc *e) +fn discharge2anyreg(fs: &mut FuncState, e: &mut ExpDesc) { + if e.kind != ExpKind::VNONRELOC { + reserve_regs(fs, 1); + discharge2reg(fs, e, fs.freereg - 1); + } +} + +// Port of removelastinstruction from lcode.c (lines 373-376) +fn remove_last_instruction(fs: &mut FuncState) { + if fs.pc > 0 { + fs.pc -= 1; + fs.chunk.code.pop(); + fs.chunk.line_info.pop(); // Must also remove line info! + } +} + +const NO_REG: u32 = 255; + +// Port of jumponcond from lcode.c:1118-1130 +// static int jumponcond (FuncState *fs, expdesc *e, int cond) +fn jumponcond(fs: &mut FuncState, e: &mut ExpDesc, cond: bool) -> isize { + if e.kind == ExpKind::VRELOC { + let ie = fs.chunk.code[e.u.info() as usize]; + if OpCode::from(Instruction::get_opcode(ie)) == OpCode::Not { + remove_last_instruction(fs); + let b = Instruction::get_b(ie); + return condjump(fs, OpCode::Test, b, 0, 0, !cond); + } + } + discharge2anyreg(fs, e); + free_exp(fs, e); + condjump(fs, OpCode::TestSet, NO_REG, e.u.info() as u32, 0, cond) +} + +// Port of luaK_goiftrue from lcode.c:1135-1160 +// void luaK_goiftrue (FuncState *fs, expdesc *e) +pub fn goiftrue(fs: &mut FuncState, e: &mut ExpDesc) { + discharge_vars(fs, e); + let pc = match e.kind { + ExpKind::VJMP => { + negatecondition(fs, e); + e.u.info() as isize + } + ExpKind::VK | ExpKind::VKFLT | ExpKind::VKINT | ExpKind::VKSTR | ExpKind::VTRUE => { + -1 // NO_JUMP: always true + } + _ => jumponcond(fs, e, false), + }; + concat(fs, &mut e.f, pc); + patchtohere(fs, e.t); + e.t = -1; +} + +// Port of luaK_goiffalse from lcode.c:1165-1183 +// void luaK_goiffalse (FuncState *fs, expdesc *e) +pub fn goiffalse(fs: &mut FuncState, e: &mut ExpDesc) { + discharge_vars(fs, e); + let pc = match e.kind { + ExpKind::VJMP => e.u.info() as isize, + ExpKind::VNIL | ExpKind::VFALSE => -1, // NO_JUMP: always false + _ => jumponcond(fs, e, true), + }; + + concat(fs, &mut e.t, pc); + patchtohere(fs, e.f); + e.f = -1; +} + +// Port of luaK_infix from lcode.c:1637-1678 +// void luaK_infix (FuncState *fs, BinOpr op, expdesc *v) +pub fn infix(fs: &mut FuncState, op: BinaryOperator, v: &mut ExpDesc) { + discharge_vars(fs, v); + match op { + BinaryOperator::OpAnd => { + goiftrue(fs, v); + } + BinaryOperator::OpOr => { + goiffalse(fs, v); + } + BinaryOperator::OpConcat => { + exp2nextreg(fs, v); + } + BinaryOperator::OpAdd + | BinaryOperator::OpSub + | BinaryOperator::OpMul + | BinaryOperator::OpDiv + | BinaryOperator::OpIDiv + | BinaryOperator::OpMod + | BinaryOperator::OpPow + | BinaryOperator::OpBAnd + | BinaryOperator::OpBOr + | BinaryOperator::OpBXor + | BinaryOperator::OpShl + | BinaryOperator::OpShr => { + if !tonumeral(v, None) { + exp2anyreg(fs, v); + } + } + BinaryOperator::OpEq | BinaryOperator::OpNe => { + if !tonumeral(v, None) { + exp2rk(fs, v); + } + } + BinaryOperator::OpLt + | BinaryOperator::OpLe + | BinaryOperator::OpGt + | BinaryOperator::OpGe => { + // Port of lcode.c:1670-1676: Use isSCnumber, not tonumeral + let mut im: i32 = 0; + let mut isfloat: bool = false; + if !is_scnumber(v, &mut im, &mut isfloat) { + exp2anyreg(fs, v); + } + } + BinaryOperator::OpNop => {} + } +} + +// Port of swapexps from lcode.c:1588-1592 +fn swapexps(e1: &mut ExpDesc, e2: &mut ExpDesc) { + std::mem::swap(e1, e2); +} + +// Port of codeconcat from lcode.c:1686-1698 +// Create code for '(e1 .. e2)' +fn codeconcat(fs: &mut FuncState, e1: &mut ExpDesc, e2: &mut ExpDesc) { + // Check if previous instruction is CONCAT to merge multiple concatenations + if fs.pc > 0 { + let prev_pc = fs.pc - 1; + let prev_instr = fs.chunk.code[prev_pc]; + if OpCode::from(Instruction::get_opcode(prev_instr)) == OpCode::Concat { + let n = Instruction::get_b(prev_instr); + let a = Instruction::get_a(prev_instr); + if e1.u.info() as u32 + 1 == a { + free_exp(fs, e2); + Instruction::set_a(&mut fs.chunk.code[prev_pc], e1.u.info() as u32); + Instruction::set_b(&mut fs.chunk.code[prev_pc], n + 1); + return; + } + } + } + // New concat opcode + code_abc(fs, OpCode::Concat, e1.u.info() as u32, 2, 0); + free_exp(fs, e2); +} + +// Port of codeABRK from lcode.c:1040-1044 +// Generate instruction with A, B, and RK operand (Register or K constant) +// This allows SETFIELD/SETTABLE to use constant values directly +pub fn code_abrk(fs: &mut FuncState, opcode: OpCode, a: u32, b: u32, ec: &mut ExpDesc) { + let k = exp2rk(fs, ec); + let c = ec.u.info() as u32; + code_abck(fs, opcode, a, b, c, k); +} + +// Port of codeeq from lcode.c:1585-1612 +// Emit code for equality comparisons ('==', '~=') +// 'e1' was already put as RK by 'luaK_infix' +fn codeeq(fs: &mut FuncState, op: BinaryOperator, e1: &mut ExpDesc, e2: &mut ExpDesc) { + let r1: u32; + let r2: u32; + let mut im: i32 = 0; + let mut isfloat: bool = false; + let opcode: OpCode; + + // If e1 is not in a register, swap operands + if e1.kind != ExpKind::VNONRELOC { + // e1 is VK, VKINT, or VKFLT + swapexps(e1, e2); + } + + // First expression must be in register + r1 = exp2anyreg(fs, e1) as u32; + + // Check if e2 is a small constant number (fits in sC) + if is_scnumber(e2, &mut im, &mut isfloat) { + opcode = OpCode::EqI; + r2 = im as u32; // immediate operand + } + // Check if e2 is a constant (can use K operand) + else if exp2rk(fs, e2) { + opcode = OpCode::EqK; + r2 = e2.u.info() as u32; // constant index + } + // Regular case: compare two registers + else { + opcode = OpCode::Eq; + r2 = exp2anyreg(fs, e2) as u32; + } + + free_exps(fs, e1, e2); + + let k = op == BinaryOperator::OpEq; + let pc = condjump(fs, opcode, r1, r2, isfloat as u32, k); + + e1.u = ExpUnion::Info(pc as i32); + e1.kind = ExpKind::VJMP; +} + +// Port of codeorder from lcode.c:1553-1581 +// Emit code for order comparisons. When using an immediate operand, +// 'isfloat' tells whether the original value was a float. +fn codeorder(fs: &mut FuncState, op: BinaryOperator, e1: &mut ExpDesc, e2: &mut ExpDesc) { + let r1: u32; + let r2: u32; + let mut im: i32 = 0; + let mut isfloat: bool = false; + let opcode: OpCode; + + // Check if e2 is a small constant (can use immediate) + if is_scnumber(e2, &mut im, &mut isfloat) { + // Use immediate operand + r1 = exp2anyreg(fs, e1) as u32; + r2 = im as u32; + // Map operator to immediate version: < -> LTI, <= -> LEI + opcode = match op { + BinaryOperator::OpLt => OpCode::LtI, + BinaryOperator::OpLe => OpCode::LeI, + _ => unreachable!(), + }; + } + // Check if e1 is a small constant (transform and swap) + else if is_scnumber(e1, &mut im, &mut isfloat) { + // Transform (A < B) to (B > A) and (A <= B) to (B >= A) + r1 = exp2anyreg(fs, e2) as u32; + r2 = im as u32; + opcode = match op { + BinaryOperator::OpLt => OpCode::GtI, // A < B => B > A + BinaryOperator::OpLe => OpCode::GeI, // A <= B => B >= A + _ => unreachable!(), + }; + } + // Regular case: compare two registers + else { + r1 = exp2anyreg(fs, e1) as u32; + r2 = exp2anyreg(fs, e2) as u32; + opcode = match op { + BinaryOperator::OpLt => OpCode::Lt, + BinaryOperator::OpLe => OpCode::Le, + _ => unreachable!(), + }; + } + + free_exps(fs, e1, e2); + + let pc = condjump(fs, opcode, r1, r2, isfloat as u32, true); + + e1.u = ExpUnion::Info(pc as i32); + e1.kind = ExpKind::VJMP; +} + +// Check if operator is foldable (arithmetic or bitwise) +// Port of foldbinop macro from lcode.h:45 +fn foldbinop(op: BinaryOperator) -> bool { + use BinaryOperator::*; + matches!( + op, + OpAdd + | OpSub + | OpMul + | OpDiv + | OpIDiv + | OpMod + | OpPow + | OpBAnd + | OpBOr + | OpBXor + | OpShl + | OpShr + ) +} + +// Check if folding operation is valid and won't raise errors +// Port of validop from lcode.c:1316-1330 +// Note: Official takes TValue pointers, but we already have extracted values +fn validop(op: BinaryOperator, e1: &ExpDesc, e2: &ExpDesc) -> bool { + use BinaryOperator::*; + use ExpKind::{VKFLT, VKINT}; + + match op { + // Bitwise operations need integer-convertible operands + // Official: luaV_tointegerns checks if values can convert to integer + OpBAnd | OpBOr | OpBXor | OpShl | OpShr => { + // Both operands must be integers or floats that can convert to integers + let ok1 = match e1.kind { + VKINT => true, + VKFLT => { + let v = e1.u.nval(); + v.is_finite() + && v.fract() == 0.0 + && v >= i64::MIN as f64 + && v <= i64::MAX as f64 + } + _ => false, + }; + let ok2 = match e2.kind { + VKINT => true, + VKFLT => { + let v = e2.u.nval(); + v.is_finite() + && v.fract() == 0.0 + && v >= i64::MIN as f64 + && v <= i64::MAX as f64 + } + _ => false, + }; + ok1 && ok2 + } + // Division operations cannot have 0 divisor + // Official: nvalue(v2) != 0 + OpDiv | OpIDiv | OpMod => match e2.kind { + VKINT => e2.u.ival() != 0, + VKFLT => e2.u.nval() != 0.0, + _ => false, + }, + _ => true, // everything else is valid + } +} + +// Try to constant-fold a binary operation +// Port of constfolding from lcode.c:1337-1356 +// Mimics luaO_rawarith: if both operands are INTEGER type, result is INTEGER; otherwise FLOAT +fn constfolding(_fs: &FuncState, op: BinaryOperator, e1: &mut ExpDesc, e2: &ExpDesc) -> bool { + use BinaryOperator::*; + use ExpKind::{VKFLT, VKINT}; + + // Check original types (not whether values can be represented as integers) + let e1_is_int_type = matches!(e1.kind, VKINT); + let e2_is_int_type = matches!(e2.kind, VKINT); + + // Check if both operands are numeric constants and extract values + let (v1, i1) = match e1.kind { + VKINT => { + let iv = e1.u.ival(); + (iv as f64, iv) + } + VKFLT => { + let f = e1.u.nval(); + let as_int = f as i64; + (f, as_int) + } + _ => return false, + }; + + let (v2, i2) = match e2.kind { + VKINT => { + let iv = e2.u.ival(); + (iv as f64, iv) + } + VKFLT => { + let f = e2.u.nval(); + let as_int = f as i64; + (f, as_int) + } + _ => return false, + }; + + // Check if operation is valid (no division by zero, etc.) + if !validop(op, e1, e2) { + return false; + } + + // Bitwise operations: require both operands to be representable as integers + // IMPORTANT: Lua treats integers as UNSIGNED for bitwise operations (see lvm.h intop macro) + // intop(op,v1,v2) = l_castU2S(l_castS2U(v1) op l_castS2U(v2)) + match op { + OpBAnd | OpBOr | OpBXor => { + // Check if float values can be exactly converted to integers + let v1_can_be_int = if e1_is_int_type { + true + } else { + (i1 as f64) == v1 + }; + let v2_can_be_int = if e2_is_int_type { + true + } else { + (i2 as f64) == v2 + }; + + if !v1_can_be_int || !v2_can_be_int { + return false; + } + + e1.kind = VKINT; + // Cast to unsigned, perform operation, cast back to signed + let u1 = i1 as u64; + let u2 = i2 as u64; + e1.u = ExpUnion::IVal(match op { + OpBAnd => (u1 & u2) as i64, + OpBOr => (u1 | u2) as i64, + OpBXor => (u1 ^ u2) as i64, + _ => unreachable!(), + }); + return true; + } + OpShl | OpShr => { + let v1_can_be_int = if e1_is_int_type { + true + } else { + (i1 as f64) == v1 + }; + let v2_can_be_int = if e2_is_int_type { + true + } else { + (i2 as f64) == v2 + }; + + if !v1_can_be_int || !v2_can_be_int { + return false; + } + + e1.kind = VKINT; + // Port of luaV_shiftl from lvm.c:780-793 + // Lua uses unsigned shift (logical shift, not arithmetic) + let u1 = i1 as u64; + e1.u = ExpUnion::IVal(match op { + OpShl => { + if i2 < 0 { + // Shift right with negative y + let shift_amount = -i2; + if shift_amount >= 64 { + 0 + } else { + (u1 >> shift_amount) as i64 + } + } else { + // Shift left with positive y + if i2 >= 64 { 0 } else { (u1 << i2) as i64 } + } + } + OpShr => { + // luaV_shiftr(x,y) = luaV_shiftl(x, -y) + let neg_i2 = -i2; + if neg_i2 < 0 { + // Shift right + let shift_amount = -neg_i2; + if shift_amount >= 64 { + 0 + } else { + (u1 >> shift_amount) as i64 + } + } else { + // Shift left + if neg_i2 >= 64 { + 0 + } else { + (u1 << neg_i2) as i64 + } + } + } + _ => unreachable!(), + }); + return true; + } + _ => {} + } + + // Arithmetic operations follow luaO_rawarith logic: + // If both operands have INTEGER type, try integer arithmetic; otherwise use float + match op { + OpDiv | OpPow => { + // These operations always produce float results + let result = match op { + OpDiv => v1 / v2, + OpPow => v1.powf(v2), + _ => unreachable!(), + }; + + // Port of lcode.c:1348-1351: folds neither NaN nor 0.0 (to avoid problems with -0.0) + if result.is_nan() || result == 0.0 { + return false; + } + + e1.kind = VKFLT; + e1.u = ExpUnion::NVal(result); + return true; + } + OpAdd | OpSub | OpMul | OpIDiv | OpMod => { + // If both operands are INTEGER type, try integer operation + if e1_is_int_type && e2_is_int_type { + let int_result = match op { + OpAdd => i1.checked_add(i2), + OpSub => i1.checked_sub(i2), + OpMul => i1.checked_mul(i2), + OpIDiv => i1.checked_div(i2), + OpMod => { + // Use Lua's modulo definition: a % b = a - floor(a/b) * b + // NOT Rust's rem_euclid which differs for negative divisors + let quot = (i1 as f64) / (i2 as f64); + let floor_quot = quot.floor() as i64; + match floor_quot.checked_mul(i2) { + Some(prod) => i1.checked_sub(prod), + None => None, + } + } + _ => unreachable!(), + }; + + if let Some(res) = int_result { + e1.kind = VKINT; + e1.u = ExpUnion::IVal(res); + return true; + } + // Integer operation failed (overflow), fall through to float + } + + // At least one operand is FLOAT type, or integer operation overflowed + let result = match op { + OpAdd => v1 + v2, + OpSub => v1 - v2, + OpMul => v1 * v2, + OpIDiv => (v1 / v2).floor(), + OpMod => v1 - (v1 / v2).floor() * v2, + _ => unreachable!(), + }; + + // Port of lcode.c:1348-1351: folds neither NaN nor 0.0 (to avoid problems with -0.0) + if result.is_nan() || result == 0.0 { + return false; + } + + e1.kind = VKFLT; + e1.u = ExpUnion::NVal(result); + return true; + } + _ => return false, + } +} + +// Port of luaK_posfix from lcode.c:1706-1783 +// void luaK_posfix (FuncState *fs, BinOpr opr, expdesc *e1, expdesc *e2, int line) +pub fn posfix(fs: &mut FuncState, op: BinaryOperator, e1: &mut ExpDesc, e2: &mut ExpDesc) { + use BinaryOperator; + + discharge_vars(fs, e2); + + // Try constant folding first (lcode.c:1709) + if foldbinop(op) && constfolding(fs, op, e1, e2) { + return; // done by folding + } + + match op { + // lcode.c:1711-1715: OPR_AND + BinaryOperator::OpAnd => { + // e1.t should be NO_JUMP (closed by luaK_goiftrue in infix) + concat(fs, &mut e2.f, e1.f); + *e1 = e2.clone(); + } + // lcode.c:1716-1720: OPR_OR + BinaryOperator::OpOr => { + // e1.f should be NO_JUMP (closed by luaK_goiffalse in infix) + concat(fs, &mut e2.t, e1.t); + *e1 = e2.clone(); + } + // lcode.c:1721-1724: OPR_CONCAT + BinaryOperator::OpConcat => { + exp2nextreg(fs, e2); + codeconcat(fs, e1, e2); + } + // lcode.c:1762-1764: OPR_EQ, OPR_NE + BinaryOperator::OpEq | BinaryOperator::OpNe => { + codeeq(fs, op, e1, e2); + } + // lcode.c:1765-1775: OPR_GT, OPR_GE (convert to LT, LE by swapping) + BinaryOperator::OpGt | BinaryOperator::OpGe => { + // '(a > b)' <=> '(b < a)'; '(a >= b)' <=> '(b <= a)' + swapexps(e1, e2); + let new_op = if op == BinaryOperator::OpGt { + BinaryOperator::OpLt + } else { + BinaryOperator::OpLe + }; + codeorder(fs, new_op, e1, e2); + } + // lcode.c:1776-1778: OPR_LT, OPR_LE + BinaryOperator::OpLt | BinaryOperator::OpLe => { + codeorder(fs, op, e1, e2); + } + // All other arithmetic/bitwise operators + _ => { + // Port of codecommutative logic from lcode.c:1517-1527 + // For commutative operators, if first operand is numeric constant (tonumeral check), + // swap them to enable immediate/K optimizations on the second operand + let mut flip = false; + if matches!(op, BinaryOperator::OpAdd | BinaryOperator::OpMul) { + // Check if e1 is a numeric constant (tonumeral check in lcode.c:1520) + // tonumeral only accepts VKINT and VKFLT, NOT VK (which might be string) + if matches!(e1.kind, ExpKind::VKINT | ExpKind::VKFLT) { + // Swap operands to put constant on right side + swapexps(e1, e2); + flip = true; + } + } else if matches!( + op, + BinaryOperator::OpBAnd | BinaryOperator::OpBOr | BinaryOperator::OpBXor + ) { + // For bitwise operations, use codebitwise logic (lcode.c:1533-1549) + // Only check for VKINT (not VKFLT), swap to put it on right + if matches!(e1.kind, ExpKind::VKINT) { + swapexps(e1, e2); + flip = true; + } + } + + // Port of codebini from lcode.c:1572-1598 and finishbinexpneg:1464-1480 + // For ADD: Check if e2 is a small integer constant (isSCint check) - lcode.c:1523 + // For SUB: Check if can negate second operand (finishbinexpneg) - lcode.c:1733-1737 + if op == BinaryOperator::OpAdd { + if let ExpKind::VKINT = e2.kind { + let imm_val = e2.u.ival(); + // For ADD: only check if the value fits in signed 8-bit after encoding + if imm_val >= -128 && imm_val <= 127 { + // Use ADDI instruction + let r1 = exp2anyreg(fs, e1); + let enc_imm = ((imm_val + 127) & 0xff) as u32; + let pc = code_abc(fs, OpCode::AddI, 0, r1 as u32, enc_imm); + + free_exp(fs, e1); + free_exp(fs, e2); + + e1.kind = ExpKind::VRELOC; + e1.u = ExpUnion::Info(pc as i32); + + // Generate MMBINI for metamethod fallback + let mm_imm = ((imm_val + 128) & 0xff) as u32; + code_abck( + fs, + OpCode::MmBinI, + r1 as u32, + mm_imm, + TmKind::Add as u32, + flip, + ); + return; + } + } + } else if op == BinaryOperator::OpSub { + // SUB can be converted to ADDI with negated immediate (finishbinexpneg) + // But BOTH original and negated values must fit in range! + if let ExpKind::VKINT = e2.kind { + let imm_val = e2.u.ival(); + let neg_imm = -imm_val; + // Both values must fit in -128..127 range (fitsC check in lcode.c:1469) + if imm_val >= -128 && imm_val <= 127 && neg_imm >= -128 && neg_imm <= 127 { + // Use ADDI with negated immediate + let r1 = exp2anyreg(fs, e1); + let enc_imm = ((neg_imm + 127) & 0xff) as u32; // Encode negated value for ADDI + let pc = code_abc(fs, OpCode::AddI, 0, r1 as u32, enc_imm); + + free_exp(fs, e1); + free_exp(fs, e2); + + e1.kind = ExpKind::VRELOC; + e1.u = ExpUnion::Info(pc as i32); + + // Generate MMBINI with ORIGINAL value for metamethod + // (finishbinexpneg corrects the metamethod argument - lcode.c:1476) + let mm_imm = ((imm_val + 128) & 0xff) as u32; + code_abck( + fs, + OpCode::MmBinI, + r1 as u32, + mm_imm, + TmKind::Sub as u32, + flip, + ); + return; + } + } + } + + // Handle shift operations (lcode.c:1745-1760) + if op == BinaryOperator::OpShl { + // OPR_SHL has three cases: + // 1. e1 is small int -> SHLI (immediate << register) + // 2. e2 can be negated -> SHRI (a << b = a >> -b) + // 3. else -> regular SHL + if let ExpKind::VKINT = e1.kind { + let imm_val = e1.u.ival(); + if imm_val >= -128 && imm_val <= 127 { + // Case 1: SHLI (immediate << register) + swapexps(e1, e2); // Put immediate on right for processing + let r1 = exp2anyreg(fs, e1); + let enc_imm = ((imm_val + 127) & 0xff) as u32; + let pc = code_abc(fs, OpCode::ShlI, 0, r1 as u32, enc_imm); + + free_exp(fs, e1); + free_exp(fs, e2); + + e1.kind = ExpKind::VRELOC; + e1.u = ExpUnion::Info(pc as i32); + + // MMBINI with flip=1 since immediate was on left + let mm_imm = ((imm_val + 128) & 0xff) as u32; + code_abck( + fs, + OpCode::MmBinI, + r1 as u32, + mm_imm, + TmKind::Shl as u32, + true, + ); + return; + } + } else if let ExpKind::VKINT = e2.kind { + // Case 2: Check if e2 can be negated -> use SHRI + let imm_val = e2.u.ival(); + let neg_imm = -imm_val; + if imm_val >= -128 && imm_val <= 127 && neg_imm >= -128 && neg_imm <= 127 { + // Use SHRI with negated immediate (a << b = a >> -b) + let r1 = exp2anyreg(fs, e1); + let enc_imm = ((neg_imm + 127) & 0xff) as u32; + let pc = code_abc(fs, OpCode::ShrI, 0, r1 as u32, enc_imm); + + free_exp(fs, e1); + free_exp(fs, e2); + + e1.kind = ExpKind::VRELOC; + e1.u = ExpUnion::Info(pc as i32); + + // MMBINI with ORIGINAL value for TM_SHL + let mm_imm = ((imm_val + 128) & 0xff) as u32; + code_abck( + fs, + OpCode::MmBinI, + r1 as u32, + mm_imm, + TmKind::Shl as u32, + flip, + ); + return; + } + } + // Case 3: Fall through to regular codebinexpval + } else if op == BinaryOperator::OpShr { + // OPR_SHR: if e2 is small int -> SHRI, else regular SHR + if let ExpKind::VKINT = e2.kind { + let imm_val = e2.u.ival(); + if imm_val >= -128 && imm_val <= 127 { + let r1 = exp2anyreg(fs, e1); + let enc_imm = ((imm_val + 127) & 0xff) as u32; + let pc = code_abc(fs, OpCode::ShrI, 0, r1 as u32, enc_imm); + + free_exp(fs, e1); + free_exp(fs, e2); + + e1.kind = ExpKind::VRELOC; + e1.u = ExpUnion::Info(pc as i32); + + let mm_imm = ((imm_val + 128) & 0xff) as u32; + code_abck( + fs, + OpCode::MmBinI, + r1 as u32, + mm_imm, + TmKind::Shr as u32, + flip, + ); + return; + } + } + // Fall through to regular codebinexpval + } + + // Try to use K operand optimization + // For bitwise operations, only use K instructions if e2 is VKINT (lcode.c:1540) + // For arithmetic operations, use K if tonumeral(e2) succeeds (lcode.c:1505) + let use_k_instruction = match op { + // Bitwise operations: only VKINT can use K instructions + BinaryOperator::OpBAnd | BinaryOperator::OpBOr | BinaryOperator::OpBXor => { + matches!(e2.kind, ExpKind::VKINT) && exp2k(fs, e2) + } + // Arithmetic operations: VKINT or VKFLT can use K instructions + BinaryOperator::OpAdd + | BinaryOperator::OpSub + | BinaryOperator::OpMul + | BinaryOperator::OpDiv + | BinaryOperator::OpIDiv + | BinaryOperator::OpMod + | BinaryOperator::OpPow => { + matches!(e2.kind, ExpKind::VKINT | ExpKind::VKFLT) && exp2k(fs, e2) + } + _ => false, + }; + + if use_k_instruction { + // e2 is a valid K operand, generate K-series instruction + let k_idx = e2.u.info(); + let r1 = exp2anyreg(fs, e1); + + // Determine the K-series opcode + let opcode = match op { + BinaryOperator::OpAdd => OpCode::AddK, + BinaryOperator::OpSub => OpCode::SubK, + BinaryOperator::OpMul => OpCode::MulK, + BinaryOperator::OpDiv => OpCode::DivK, + BinaryOperator::OpIDiv => OpCode::IDivK, + BinaryOperator::OpMod => OpCode::ModK, + BinaryOperator::OpPow => OpCode::PowK, + BinaryOperator::OpBAnd => OpCode::BAndK, + BinaryOperator::OpBOr => OpCode::BOrK, + BinaryOperator::OpBXor => OpCode::BXorK, + _ => { + // No K version, fall back to normal instruction + let o2 = exp2anyreg(fs, e2); + free_reg(fs, o2); + free_reg(fs, r1); + + let res = fs.freereg; + reserve_regs(fs, 1); + + let opcode = match op { + BinaryOperator::OpShl => OpCode::Shl, + BinaryOperator::OpShr => OpCode::Shr, + _ => unreachable!("Invalid operator"), + }; + + code_abc(fs, opcode, res as u32, r1 as u32, o2 as u32); + e1.kind = ExpKind::VNONRELOC; + e1.u = ExpUnion::Info(res as i32); + return; + } + }; + + // Port of finishbinexpval from lcode.c:1407-1418 + // Generate K-series instruction with A=0, will be fixed by discharge2reg + let pc = code_abc(fs, opcode, 0, r1 as u32, k_idx as u32); + + // Free both operands (freeexps) - must use free_exps to maintain proper order + free_exps(fs, e1, e2); + + // Mark as relocatable - target register will be decided later + e1.kind = ExpKind::VRELOC; + e1.u = ExpUnion::Info(pc as i32); + + // Generate metamethod fallback instruction (MMBINK) + // Like finishbinexpval in lcode.c:1416 + // TM events from ltm.h (TM_ADD=6, TM_SUB=7, etc.) + let tm_event = match op { + BinaryOperator::OpAdd => TmKind::Add, // TM_ADD + BinaryOperator::OpSub => TmKind::Sub, // TM_SUB + BinaryOperator::OpMul => TmKind::Mul, // TM_MUL + BinaryOperator::OpMod => TmKind::Mod, // TM_MOD + BinaryOperator::OpPow => TmKind::Pow, // TM_POW + BinaryOperator::OpDiv => TmKind::Div, // TM_DIV + BinaryOperator::OpIDiv => TmKind::IDiv, // TM_IDIV + BinaryOperator::OpBAnd => TmKind::Band, // TM_BAND + BinaryOperator::OpBOr => TmKind::Bor, // TM_BOR + BinaryOperator::OpBXor => TmKind::Bxor, // TM_BXOR + _ => TmKind::N, // Invalid for other ops + }; + // Use code_abck to include flip bit (k-flag) like in MMBINI + code_abck( + fs, + OpCode::MmBinK, + r1 as u32, + k_idx as u32, + tm_event as u32, + flip, + ); + } else { + // Both operands in registers - port of codebinNoK (lcode.c:1490-1496) + // If we flipped operands for K optimization attempt, swap back to original order + if flip { + swapexps(e1, e2); + } + + // Now port of codebinexpval (lcode.c:1425-1434) + let o2 = exp2anyreg(fs, e2); + + // Determine instruction opcode + let opcode = match op { + BinaryOperator::OpAdd => OpCode::Add, + BinaryOperator::OpSub => OpCode::Sub, + BinaryOperator::OpMul => OpCode::Mul, + BinaryOperator::OpDiv => OpCode::Div, + BinaryOperator::OpIDiv => OpCode::IDiv, + BinaryOperator::OpMod => OpCode::Mod, + BinaryOperator::OpPow => OpCode::Pow, + BinaryOperator::OpShl => OpCode::Shl, + BinaryOperator::OpShr => OpCode::Shr, + BinaryOperator::OpBAnd => OpCode::BAnd, + BinaryOperator::OpBOr => OpCode::BOr, + BinaryOperator::OpBXor => OpCode::BXor, + _ => unreachable!("Invalid operator for opcode generation"), + }; + + // Port of finishbinexpval from lcode.c:1407-1418 + // Generate instruction with A=0, will be fixed by discharge2reg + let o1 = exp2anyreg(fs, e1); + let pc = code_abc(fs, opcode, 0, o1 as u32, o2 as u32); + + // Free both operands (freeexps) - must use free_exps to maintain proper order + free_exps(fs, e1, e2); + + // Mark as relocatable + e1.kind = ExpKind::VRELOC; + e1.u = ExpUnion::Info(pc as i32); + + // Generate metamethod fallback instruction (MMBIN) + // Like finishbinexpval in lcode.c:1416 + let tm_event = match op { + BinaryOperator::OpAdd => TmKind::Add, + BinaryOperator::OpSub => TmKind::Sub, + BinaryOperator::OpMul => TmKind::Mul, + BinaryOperator::OpMod => TmKind::Mod, + BinaryOperator::OpPow => TmKind::Pow, + BinaryOperator::OpDiv => TmKind::Div, + BinaryOperator::OpIDiv => TmKind::IDiv, + BinaryOperator::OpBAnd => TmKind::Band, + BinaryOperator::OpBOr => TmKind::Bor, + BinaryOperator::OpBXor => TmKind::Bxor, + BinaryOperator::OpShl => TmKind::Shl, + BinaryOperator::OpShr => TmKind::Shr, + _ => TmKind::N, + }; + code_abc(fs, OpCode::MmBin, o1 as u32, o2 as u32, tm_event as u32); + } + } + } +} + +// Port of codenot from lcode.c:1188-1214 +// static void codenot (FuncState *fs, expdesc *e) +fn codenot(fs: &mut FuncState, e: &mut ExpDesc) { + discharge_vars(fs, e); + match e.kind { + ExpKind::VNIL | ExpKind::VFALSE => { + // true == not nil == not false (lcode.c:1190) + e.kind = ExpKind::VTRUE; + } + ExpKind::VK | ExpKind::VKFLT | ExpKind::VKINT | ExpKind::VKSTR | ExpKind::VTRUE => { + // false == not "x" == not 0.5 == not 1 == not true (lcode.c:1194) + e.kind = ExpKind::VFALSE; + } + ExpKind::VJMP => { + // Negate the condition (lcode.c:1197) + negatecondition(fs, e); + } + ExpKind::VRELOC | ExpKind::VNONRELOC => { + // Generate NOT instruction (lcode.c:1200-1206) + discharge2anyreg(fs, e); + free_exp(fs, e); + let pc = code_abc(fs, OpCode::Not, 0, e.u.info() as u32, 0); + e.u = ExpUnion::Info(pc as i32); + e.kind = ExpKind::VRELOC; + } + _ => {} // Should not happen + } +} + +// Simplified implementation of luaK_prefix - generate unary operation +// Port of codeunexpval from lcode.c:1392-1398 +// Generate code for unary operation that produces a value +fn codeunexpval(fs: &mut FuncState, op: OpCode, e: &mut ExpDesc) { + let r = exp2anyreg(fs, e); // opcodes operate only on registers + free_exp(fs, e); + let pc = code_abc(fs, op, 0, r as u32, 0); // result register is 0 (will be relocated) + e.u = ExpUnion::Info(pc as i32); + e.kind = ExpKind::VRELOC; // all those operations are relocatable +} + +pub fn prefix(fs: &mut FuncState, op: OpCode, e: &mut ExpDesc) { + discharge_vars(fs, e); + + // Port of luaK_prefix from lcode.c:1616-1631 + match op { + OpCode::Unm => { + // Try constant folding for unary minus (lcode.c:1620-1622) + match e.kind { + ExpKind::VKINT => { + // Negate integer constant in place + let val = e.u.ival(); + e.u = ExpUnion::IVal(val.wrapping_neg()); + return; + } + ExpKind::VKFLT => { + // Negate float constant in place + let val = e.u.nval(); + let negated = -val; + // Check if result is -0.0 (bit pattern 0x8000000000000000) + // Official Lua doesn't fold -0.0 to constant because it needs special handling + if negated.to_bits() == 0x8000000000000000 { + // Don't fold, emit UNM instruction instead + codeunexpval(fs, op, e); + return; + } + e.u = ExpUnion::NVal(negated); + return; + } + _ => {} + } + // Otherwise fall through to codeunexpval + codeunexpval(fs, op, e); + } + OpCode::BNot => { + // Try constant folding for bitwise not (lcode.c:1620-1622) + if e.kind == ExpKind::VKINT { + let val = e.u.ival(); + e.u = ExpUnion::IVal(!val); + return; + } + // Otherwise fall through to codeunexpval + codeunexpval(fs, op, e); + } + OpCode::Len => { + // LEN operation (lcode.c:1625) + codeunexpval(fs, op, e); + } + OpCode::Not => { + // NOT operation (lcode.c:1627) + codenot(fs, e); + } + _ => unreachable!(), + } +} + +// Port of luaK_exp2anyregup from lcode.c:978-981 +// void luaK_exp2anyregup (FuncState *fs, expdesc *e) +pub fn exp2anyregup(fs: &mut FuncState, e: &mut ExpDesc) { + if e.kind != ExpKind::VUPVAL || e.has_jumps() { + exp2anyreg(fs, e); + } +} + +// Port of luaK_exp2val from lcode.c:988-993 +// void luaK_exp2val (FuncState *fs, expdesc *e) +pub fn exp2val(fs: &mut FuncState, e: &mut ExpDesc) { + if e.kind == ExpKind::VJMP || e.has_jumps() { + exp2anyreg(fs, e); + } else { + discharge_vars(fs, e); + } +} + +// Port of luaK_indexed from lcode.c:1280-1310 +// void luaK_indexed (FuncState *fs, expdesc *t, expdesc *k) +pub fn indexed(fs: &mut FuncState, t: &mut ExpDesc, k: &mut ExpDesc) { + // Convert string to constant if needed + if k.kind == ExpKind::VKSTR { + // str2K - convert to constant + str2k(fs, k); + } + + // Table must be in local/nonreloc/upval + if t.kind == ExpKind::VUPVAL && !is_kstr(fs, k) { + exp2anyreg(fs, t); + } + + if t.kind == ExpKind::VUPVAL { + let temp = t.u.info(); + t.u = ExpUnion::Ind(IndVars { + t: temp as i16, + idx: k.u.info() as i16, + }); + t.kind = ExpKind::VINDEXUP; + } else { + // Register index of the table + t.u.ind_mut().t = if t.kind == ExpKind::VLOCAL { + t.u.var().ridx as i16 + } else { + t.u.info() as i16 + }; + + if is_kstr(fs, k) { + t.u.ind_mut().idx = k.u.info() as i16; + t.kind = ExpKind::VINDEXSTR; + } else if is_cint(k) { + t.u.ind_mut().idx = k.u.ival() as i16; + t.kind = ExpKind::VINDEXI; + } else { + t.u.ind_mut().idx = exp2anyreg(fs, k) as i16; + t.kind = ExpKind::VINDEXED; + } + } +} + +#[inline] +fn hasjumps(e: &ExpDesc) -> bool { + e.t != e.f +} + +// Check if expression is a constant string +fn is_kstr(fs: &FuncState, e: &ExpDesc) -> bool { + let ok1 = e.kind == ExpKind::VK && !hasjumps(e) && e.u.info() <= Instruction::MAX_B as i32; + if !ok1 { + return false; + } + + let k_idx = e.u.info() as usize; + if k_idx >= fs.chunk.constants.len() { + return false; + } + + let const_val = &fs.chunk.constants[k_idx]; + if let Some(string_id) = const_val.as_string_id() { + return fs.pool.is_short_string(string_id); + } + + false +} + +fn is_kint(e: &ExpDesc) -> bool { + e.kind == ExpKind::VKINT && !hasjumps(e) +} + +// Check if expression is a constant integer in valid range for SETI +// SETI's B field is only 8 bits (0-255), matching Lua C's isCint check +fn is_cint(e: &ExpDesc) -> bool { + is_kint(e) && e.u.ival() as u64 <= Instruction::MAX_C as u64 +} + +// Port of luaK_self from lcode.c:1087-1097 +// void luaK_self (FuncState *fs, expdesc *e, expdesc *key) +pub fn self_op(fs: &mut FuncState, e: &mut ExpDesc, key: &mut ExpDesc) { + // Port of luaK_self from lcode.c:1087-1097 + let ereg = exp2anyreg(fs, e); + free_exp(fs, e); + + let base = fs.freereg; + e.u = ExpUnion::Info(base as i32); + e.kind = ExpKind::VNONRELOC; + reserve_regs(fs, 2); // function and 'self' + + // SELF A B C: R(A+1) := R(B); R(A) := R(B)[RK(C)] + // Use code_abrk to support constant keys + code_abrk(fs, OpCode::Self_, base as u32, ereg as u32, key); + free_exp(fs, key); +} + +// Port of luaK_setmultret - set call/vararg to return multiple values +pub fn setmultret(fs: &mut FuncState, e: &mut ExpDesc) { + // LUA_MULTRET = -1, which becomes 255 in u8 + // setreturns will do nresults+1, so 255+1=0 in u8 (wrapping), meaning multret + setreturns(fs, e, 255); +} + +// Port of luaK_fixline from lcode.c:1787-1790 +// void luaK_fixline (FuncState *fs, int line) +pub fn fixline(fs: &mut FuncState, line: usize) { + // Remove last line info and add it again with new line + if fs.chunk.line_info.len() > 0 { + let last_idx = fs.chunk.line_info.len() - 1; + fs.chunk.line_info[last_idx] = line as u32; + } +} + +// Port of luaK_exp2const from lcode.c:85-108 +// int luaK_exp2const (FuncState *fs, const expdesc *e, TValue *v) +pub fn exp2const(fs: &FuncState, e: &ExpDesc) -> Option { + if e.has_jumps() { + return None; + } + + let result = match e.kind { + ExpKind::VFALSE => Some(LuaValue::boolean(false)), + ExpKind::VTRUE => Some(LuaValue::boolean(true)), + ExpKind::VNIL => Some(LuaValue::nil()), + ExpKind::VKSTR => { + // String constant - already in constants + let idx = e.u.info() as usize; + if idx < fs.chunk.constants.len() { + Some(fs.chunk.constants[idx]) + } else { + None + } + } + ExpKind::VK => { + // Constant in K + let idx = e.u.info() as usize; + if idx < fs.chunk.constants.len() { + Some(fs.chunk.constants[idx]) + } else { + None + } + } + ExpKind::VKINT => Some(LuaValue::integer(e.u.ival())), + ExpKind::VKFLT => { + let val = e.u.nval(); + // Reject -0.0 as compile-time constant (like official Lua) + // Official Lua doesn't optimize -0.0 to RDKCTC because it needs special handling + if val.to_bits() == 0x8000000000000000 { + return None; + } + Some(LuaValue::float(val)) + } + ExpKind::VCONST => { + // Get from actvar array (port of const2val from lcode.c:75-78) + let vidx = e.u.info() as usize; + if let Some(var_desc) = fs.actvar.get(vidx) { + var_desc.const_value + } else { + None + } + } + _ => None, + }; + + result +} + +// LUA_MULTRET constant from lua.h +pub const LUA_MULTRET: u32 = u32::MAX; + +// Port of hasmultret from lcode.c:86-90 +pub fn hasmultret(e: &ExpDesc) -> bool { + matches!(e.kind, ExpKind::VCALL | ExpKind::VVARARG) +} + +// Port of luaK_setlist from lcode.c:1810-1823 +pub fn setlist(fs: &mut FuncState, base: u8, nelems: u32, tostore: u32) { + debug_assert!(tostore != 0); + + let c = if tostore == LUA_MULTRET { 0 } else { tostore }; + + const MAXARG_C: u32 = 255; // 8-bit C field + + if nelems <= MAXARG_C { + code_abc(fs, OpCode::SetList, base as u32, c, nelems); + } else { + // Need extra argument for large index + let extra = nelems / (MAXARG_C + 1); + let c_arg = nelems % (MAXARG_C + 1); + code_abck(fs, OpCode::SetList, base as u32, c, c_arg, true); + code_extraarg(fs, extra); + } + + fs.freereg = base + 1; // free registers with list values +} + +// Port of luaK_settablesize from lcode.c:1793-1801 +// void luaK_settablesize (FuncState *fs, int pc, int ra, int asize, int hsize) +pub fn settablesize(fs: &mut FuncState, pc: usize, ra: u8, asize: u32, hsize: u32) { + // B field: hash size (lcode.c:1795) + // rb = (hsize != 0) ? luaO_ceillog2(hsize) + 1 : 0 + let rb = if hsize != 0 { + // Ceiling of log2(hsize) + 1 + let bits = 32 - (hsize - 1).leading_zeros(); + bits + 1 + } else { + 0 + }; + + // C field: lower bits of array size (lcode.c:1797) + // rc = asize % (MAXARG_C + 1) + let rc = asize % (Instruction::MAX_C + 1); + + // EXTRAARG: higher bits of array size (lcode.c:1796) + // extra = asize / (MAXARG_C + 1) + let extra = asize / (Instruction::MAX_C + 1); + // k flag: true if needs EXTRAARG (lcode.c:1798) + let k = extra > 0; + + // Update the NEWTABLE instruction (lcode.c:1799) + let inst = &mut fs.chunk.code[pc]; + let opcode = Instruction::get_opcode(*inst); + debug_assert_eq!(opcode, OpCode::NewTable); + + *inst = Instruction::create_abck(OpCode::NewTable, ra as u32, rb, rc, k); + + // Update EXTRAARG instruction (lcode.c:1800) + // *(inst + 1) = CREATE_Ax(OP_EXTRAARG, extra); + if pc + 1 < fs.chunk.code.len() { + fs.chunk.code[pc + 1] = Instruction::create_ax(OpCode::ExtraArg, extra); + } +} + +// Port of luaK_codeextraarg from lcode.c:415-419 +pub fn code_extraarg(fs: &mut FuncState, a: u32) -> usize { + let inst = Instruction::create_ax(OpCode::ExtraArg, a); + let pc = fs.pc; + fs.chunk.code.push(inst); + fs.chunk.line_info.push(fs.lexer.lastline as u32); // Use lastline + fs.pc += 1; + pc +} + diff --git a/crates/luars/src/compiler/exp2reg.rs b/crates/luars/src/compiler/exp2reg.rs deleted file mode 100644 index eda0c28..0000000 --- a/crates/luars/src/compiler/exp2reg.rs +++ /dev/null @@ -1,342 +0,0 @@ -use super::Compiler; -/// Expression to register conversion functions -/// Mirrors Lua's code generation strategy with expdesc -use super::expdesc::*; -use super::helpers::*; -use crate::lua_value::LuaValue; -use crate::lua_vm::{Instruction, OpCode}; - -/// Discharge expression variables (convert to concrete value) -/// Lua equivalent: luaK_dischargevars -pub fn discharge_vars(c: &mut Compiler, e: &mut ExpDesc) { - match e.kind { - ExpKind::VLocal => { - // Local variable: convert to VNONRELOC with its register - e.kind = ExpKind::VNonReloc; - e.info = e.var.ridx; - } - ExpKind::VUpval => { - // Upvalue: generate GETUPVAL instruction - let reg = alloc_register(c); - emit(c, Instruction::encode_abc(OpCode::GetUpval, reg, e.info, 0)); - e.kind = ExpKind::VNonReloc; - e.info = reg; - } - ExpKind::VIndexUp => { - // Indexed upvalue: generate GETTABUP instruction - let reg = alloc_register(c); - emit( - c, - Instruction::create_abck(OpCode::GetTabUp, reg, e.ind.t, e.ind.idx, true), - ); - e.kind = ExpKind::VNonReloc; - e.info = reg; - } - ExpKind::VIndexI => { - // Integer indexed: generate GETI instruction - free_register(c, e.ind.t); - let reg = alloc_register(c); - emit( - c, - Instruction::encode_abc(OpCode::GetI, reg, e.ind.t, e.ind.idx), - ); - e.kind = ExpKind::VNonReloc; - e.info = reg; - } - ExpKind::VIndexStr => { - // String indexed: generate GETFIELD instruction - free_register(c, e.ind.t); - let reg = alloc_register(c); - emit( - c, - Instruction::create_abck(OpCode::GetField, reg, e.ind.t, e.ind.idx, true), - ); - e.kind = ExpKind::VNonReloc; - e.info = reg; - } - ExpKind::VIndexed => { - // General indexed: generate GETTABLE instruction - free_registers(c, e.ind.t, e.ind.idx); - let reg = alloc_register(c); - emit( - c, - Instruction::encode_abc(OpCode::GetTable, reg, e.ind.t, e.ind.idx), - ); - e.kind = ExpKind::VNonReloc; - e.info = reg; - } - ExpKind::VCall | ExpKind::VVararg => { - // These are already discharged by their generation - // Just set to VNONRELOC pointing to freereg-1 - if c.freereg > 0 { - e.kind = ExpKind::VNonReloc; - e.info = c.freereg - 1; - } - } - _ => { - // Other kinds don't need discharging - } - } -} - -/// Ensure expression is in any register -/// Lua equivalent: discharge2anyreg -#[allow(dead_code)] -fn discharge_to_any_reg(c: &mut Compiler, e: &mut ExpDesc) { - if e.kind != ExpKind::VNonReloc { - reserve_registers(c, 1); - discharge_to_reg(c, e, c.freereg - 1); - } -} - -/// Discharge expression to a specific register -/// Lua equivalent: discharge2reg -pub fn discharge_to_reg(c: &mut Compiler, e: &mut ExpDesc, reg: u32) { - discharge_vars(c, e); - - match e.kind { - ExpKind::VNil => { - emit(c, Instruction::encode_abc(OpCode::LoadNil, reg, 0, 0)); - } - ExpKind::VFalse => { - emit(c, Instruction::encode_abc(OpCode::LoadFalse, reg, 0, 0)); - } - ExpKind::VTrue => { - emit(c, Instruction::encode_abc(OpCode::LoadTrue, reg, 0, 0)); - } - ExpKind::VK => { - // Load constant from constant table - emit_loadk(c, reg, e.info); - } - ExpKind::VKInt => { - // Load integer immediate if in range, otherwise use constant table - if e.ival >= -(1 << 23) && e.ival < (1 << 23) { - emit( - c, - Instruction::encode_asbx(OpCode::LoadI, reg, e.ival as i32), - ); - } else { - let const_idx = add_constant_dedup(c, LuaValue::integer(e.ival)); - emit_loadk(c, reg, const_idx); - } - } - ExpKind::VKFlt => { - // Load float from constant table - let const_idx = add_constant_dedup(c, LuaValue::number(e.nval)); - emit_loadk(c, reg, const_idx); - } - ExpKind::VKStr => { - // Load string from constant table - emit_loadk(c, reg, e.info); - } - ExpKind::VNonReloc => { - if e.info != reg { - emit_move(c, reg, e.info); - } - } - ExpKind::VReloc => { - // Relocatable expression: patch instruction to use target register - let pc = e.info as usize; - if pc < c.chunk.code.len() { - // Patch the A field of the instruction - let mut instr = c.chunk.code[pc]; - Instruction::set_a(&mut instr, reg); - c.chunk.code[pc] = instr; - } - } - _ => { - // Should not happen if discharge_vars was called - } - } - - e.kind = ExpKind::VNonReloc; - e.info = reg; -} - -/// Compile expression to any available register -/// Lua equivalent: luaK_exp2anyreg -pub fn exp_to_any_reg(c: &mut Compiler, e: &mut ExpDesc) -> u32 { - discharge_vars(c, e); - - if e.kind == ExpKind::VNonReloc { - // Already in a register - // Check if the register is NOT a local variable (i.e., it's a temp register) - // If it's a local (info < nactvar), we MUST NOT reuse it - allocate a new register - let nactvar = nvarstack(c); - if e.info >= nactvar { - // It's a temp register, can return directly - return e.info; - } - // Fall through: it's a local variable, need to copy to new register - } - - // Need to allocate a new register - reserve_registers(c, 1); - discharge_to_reg(c, e, c.freereg - 1); - e.info -} - -/// Compile expression to next available register -/// Lua equivalent: luaK_exp2nextreg -pub fn exp_to_next_reg(c: &mut Compiler, e: &mut ExpDesc) { - discharge_vars(c, e); - free_exp(c, e); - reserve_registers(c, 1); - exp_to_reg(c, e, c.freereg - 1); -} - -/// Compile expression to a specific register -/// Lua equivalent: exp2reg -pub fn exp_to_reg(c: &mut Compiler, e: &mut ExpDesc, reg: u32) { - discharge_to_reg(c, e, reg); - - if e.kind == ExpKind::VJmp { - // TODO: Handle jump expressions (for boolean operators) - // concat_jump_lists(c, &mut e.t, e.info); - } - - if e.has_jumps() { - // TODO: Patch jump lists - // patch_list_to_here(c, e.f); - // patch_list_to_here(c, e.t); - } - - e.f = -1; - e.t = -1; - e.kind = ExpKind::VNonReloc; - e.info = reg; -} - -/// Free expression if it's in a temporary register -/// Lua equivalent: freeexp -pub fn free_exp(c: &mut Compiler, e: &ExpDesc) { - if e.kind == ExpKind::VNonReloc { - free_register(c, e.info); - } -} - -/// Free two expressions -/// Lua equivalent: freeexps -#[allow(dead_code)] -pub fn free_exps(c: &mut Compiler, e1: &ExpDesc, e2: &ExpDesc) { - let r1 = if e1.kind == ExpKind::VNonReloc { - e1.info as i32 - } else { - -1 - }; - let r2 = if e2.kind == ExpKind::VNonReloc { - e2.info as i32 - } else { - -1 - }; - - if r1 >= 0 && r2 >= 0 { - free_registers(c, r1 as u32, r2 as u32); - } else if r1 >= 0 { - free_register(c, r1 as u32); - } else if r2 >= 0 { - free_register(c, r2 as u32); - } -} - -/// Check if expression has jump lists -impl ExpDesc { - pub fn has_jumps(&self) -> bool { - self.t != -1 || self.f != -1 - } -} - -/// Store value from expression to a variable -/// Lua equivalent: luaK_storevar -#[allow(dead_code)] -pub fn store_var(c: &mut Compiler, var: &ExpDesc, ex: &mut ExpDesc) { - match var.kind { - ExpKind::VLocal => { - free_exp(c, ex); - exp_to_reg(c, ex, var.var.ridx); - } - ExpKind::VUpval => { - let e = exp_to_any_reg(c, ex); - emit(c, Instruction::encode_abc(OpCode::SetUpval, e, var.info, 0)); - free_exp(c, ex); - } - ExpKind::VIndexUp => { - code_abrk(c, OpCode::SetTabUp, var.ind.t, var.ind.idx, ex); - free_exp(c, ex); - } - ExpKind::VIndexI => { - code_abrk(c, OpCode::SetI, var.ind.t, var.ind.idx, ex); - free_exp(c, ex); - } - ExpKind::VIndexStr => { - code_abrk(c, OpCode::SetField, var.ind.t, var.ind.idx, ex); - free_exp(c, ex); - } - ExpKind::VIndexed => { - code_abrk(c, OpCode::SetTable, var.ind.t, var.ind.idx, ex); - free_exp(c, ex); - } - _ => { - // Should not happen - } - } -} - -/// Code ABRxK instruction (with RK operand) -/// Lua equivalent: codeABRK -#[allow(dead_code)] -fn code_abrk(c: &mut Compiler, op: OpCode, a: u32, b: u32, ec: &mut ExpDesc) { - let k = exp_to_rk(c, ec); - emit(c, Instruction::create_abck(op, a, b, ec.info, k)); -} - -/// Convert expression to RK operand (register or constant) -/// Lua equivalent: exp2RK -#[allow(dead_code)] -fn exp_to_rk(c: &mut Compiler, e: &mut ExpDesc) -> bool { - match e.kind { - ExpKind::VTrue | ExpKind::VFalse | ExpKind::VNil => { - // Small constants: can fit in instruction - if e.kind == ExpKind::VTrue { - e.info = 1; - } else { - e.info = 0; - } - e.kind = ExpKind::VK; - true - } - ExpKind::VKInt => { - // Try to fit integer in constant table - let const_idx = add_constant_dedup(c, LuaValue::integer(e.ival)); - if const_idx <= Instruction::MAX_C { - e.info = const_idx; - e.kind = ExpKind::VK; - return true; - } - // Fall through to allocate register - exp_to_any_reg(c, e); - false - } - ExpKind::VKFlt => { - let const_idx = add_constant_dedup(c, LuaValue::number(e.nval)); - if const_idx <= Instruction::MAX_C { - e.info = const_idx; - e.kind = ExpKind::VK; - return true; - } - exp_to_any_reg(c, e); - false - } - ExpKind::VK => { - if e.info <= Instruction::MAX_C { - return true; - } - exp_to_any_reg(c, e); - false - } - _ => { - exp_to_any_reg(c, e); - false - } - } -} diff --git a/crates/luars/src/compiler/expdesc.rs b/crates/luars/src/compiler/expdesc.rs deleted file mode 100644 index 12d52c4..0000000 --- a/crates/luars/src/compiler/expdesc.rs +++ /dev/null @@ -1,271 +0,0 @@ -/// Expression descriptor - tracks expression evaluation state -/// Mirrors Lua's expdesc structure for delayed code generation -/// This allows optimizations like register reuse and constant folding - -/// Expression kind - determines how the expression value is represented -#[derive(Debug, Clone, Copy, PartialEq)] -#[allow(unused)] -pub enum ExpKind { - /// No value (void expression) - VVoid, - /// Nil constant - VNil, - /// True constant - VTrue, - /// False constant - VFalse, - /// Constant in constant table (info = constant index) - VK, - /// Float constant (nval = float value) - VKFlt, - /// Integer constant (ival = integer value) - VKInt, - /// String constant (strval = string index in constant table) - VKStr, - /// Expression has value in a fixed register (info = register) - VNonReloc, - /// Local variable (info = register, vidx = local index) - VLocal, - /// Upvalue variable (info = upvalue index) - VUpval, - /// Indexed variable (ind.t = table reg, ind.idx = key reg) - VIndexed, - /// Indexed upvalue (ind.t = upvalue, ind.idx = key constant) - VIndexUp, - /// Indexed with constant integer (ind.t = table reg, ind.idx = int value) - VIndexI, - /// Indexed with literal string (ind.t = table reg, ind.idx = string constant) - VIndexStr, - /// Expression is a test/comparison (info = jump instruction pc) - VJmp, - /// Expression can put result in any register (info = instruction pc) - VReloc, - /// Expression is a function call (info = instruction pc) - VCall, - /// Vararg expression (info = instruction pc) - VVararg, -} - -/// Index information for indexed expressions -#[derive(Debug, Clone, Copy)] -pub struct IndexInfo { - pub t: u32, // Table register or upvalue - pub idx: u32, // Key register or constant index -} - -/// Local variable information -#[derive(Debug, Clone, Copy)] -pub struct VarInfo { - pub ridx: u32, // Register index - #[allow(unused)] - pub vidx: usize, // Variable index in locals array -} - -/// Expression descriptor -#[derive(Debug, Clone)] -pub struct ExpDesc { - pub kind: ExpKind, - /// Generic info field - meaning depends on kind - pub info: u32, - /// Integer value (for VKInt) - pub ival: i64, - /// Float value (for VKFlt) - pub nval: f64, - /// Index information (for VIndexed, VIndexUp, VIndexI, VIndexStr) - pub ind: IndexInfo, - /// Variable information (for VLocal) - pub var: VarInfo, - /// Patch list for 'exit when true' jumps - pub t: i32, - /// Patch list for 'exit when false' jumps - pub f: i32, -} - -impl ExpDesc { - /// Create a new void expression - #[allow(dead_code)] - pub fn new_void() -> Self { - ExpDesc { - kind: ExpKind::VVoid, - info: 0, - ival: 0, - nval: 0.0, - ind: IndexInfo { t: 0, idx: 0 }, - var: VarInfo { ridx: 0, vidx: 0 }, - t: -1, - f: -1, - } - } - - /// Create expression in a specific register - pub fn new_nonreloc(reg: u32) -> Self { - ExpDesc { - kind: ExpKind::VNonReloc, - info: reg, - ival: 0, - nval: 0.0, - ind: IndexInfo { t: 0, idx: 0 }, - var: VarInfo { ridx: 0, vidx: 0 }, - t: -1, - f: -1, - } - } - - /// Create local variable expression - #[allow(dead_code)] - pub fn new_local(reg: u32, vidx: usize) -> Self { - ExpDesc { - kind: ExpKind::VLocal, - info: 0, - ival: 0, - nval: 0.0, - ind: IndexInfo { t: 0, idx: 0 }, - var: VarInfo { ridx: reg, vidx }, - t: -1, - f: -1, - } - } - - /// Create integer constant expression - pub fn new_int(val: i64) -> Self { - ExpDesc { - kind: ExpKind::VKInt, - info: 0, - ival: val, - nval: 0.0, - ind: IndexInfo { t: 0, idx: 0 }, - var: VarInfo { ridx: 0, vidx: 0 }, - t: -1, - f: -1, - } - } - - /// Create float constant expression - pub fn new_float(val: f64) -> Self { - ExpDesc { - kind: ExpKind::VKFlt, - info: 0, - ival: 0, - nval: val, - ind: IndexInfo { t: 0, idx: 0 }, - var: VarInfo { ridx: 0, vidx: 0 }, - t: -1, - f: -1, - } - } - - /// Create constant table expression - pub fn new_k(const_idx: u32) -> Self { - ExpDesc { - kind: ExpKind::VK, - info: const_idx, - ival: 0, - nval: 0.0, - ind: IndexInfo { t: 0, idx: 0 }, - var: VarInfo { ridx: 0, vidx: 0 }, - t: -1, - f: -1, - } - } - - /// Create nil expression - pub fn new_nil() -> Self { - ExpDesc { - kind: ExpKind::VNil, - info: 0, - ival: 0, - nval: 0.0, - ind: IndexInfo { t: 0, idx: 0 }, - var: VarInfo { ridx: 0, vidx: 0 }, - t: -1, - f: -1, - } - } - - /// Create true expression - pub fn new_true() -> Self { - ExpDesc { - kind: ExpKind::VTrue, - info: 0, - ival: 0, - nval: 0.0, - ind: IndexInfo { t: 0, idx: 0 }, - var: VarInfo { ridx: 0, vidx: 0 }, - t: -1, - f: -1, - } - } - - /// Create false expression - pub fn new_false() -> Self { - ExpDesc { - kind: ExpKind::VFalse, - info: 0, - ival: 0, - nval: 0.0, - ind: IndexInfo { t: 0, idx: 0 }, - var: VarInfo { ridx: 0, vidx: 0 }, - t: -1, - f: -1, - } - } - - /// Check if expression is a variable - #[allow(dead_code)] - pub fn is_var(&self) -> bool { - matches!( - self.kind, - ExpKind::VLocal - | ExpKind::VUpval - | ExpKind::VIndexed - | ExpKind::VIndexUp - | ExpKind::VIndexI - | ExpKind::VIndexStr - ) - } - - /// Check if expression has multiple returns - #[allow(dead_code)] - pub fn has_multret(&self) -> bool { - matches!(self.kind, ExpKind::VCall | ExpKind::VVararg) - } - - /// Get the register number if expression is in a register - pub fn get_register(&self) -> Option { - match self.kind { - ExpKind::VNonReloc => Some(self.info), - ExpKind::VLocal => Some(self.var.ridx), - _ => None, - } - } -} - -/// Check if expression can be used as RK operand (register or constant) -#[allow(dead_code)] -pub fn is_rk(e: &ExpDesc) -> bool { - matches!( - e.kind, - ExpKind::VK | ExpKind::VKInt | ExpKind::VKFlt | ExpKind::VNonReloc | ExpKind::VLocal - ) -} - -/// Check if expression is a constant -#[allow(dead_code)] -pub fn is_const(e: &ExpDesc) -> bool { - matches!( - e.kind, - ExpKind::VNil - | ExpKind::VTrue - | ExpKind::VFalse - | ExpKind::VK - | ExpKind::VKInt - | ExpKind::VKFlt - | ExpKind::VKStr - ) -} - -/// Check if expression is a numeric constant -#[allow(dead_code)] -pub fn is_numeral(e: &ExpDesc) -> bool { - matches!(e.kind, ExpKind::VKInt | ExpKind::VKFlt) -} diff --git a/crates/luars/src/compiler/expr.rs b/crates/luars/src/compiler/expr.rs deleted file mode 100644 index fc6353d..0000000 --- a/crates/luars/src/compiler/expr.rs +++ /dev/null @@ -1,2529 +0,0 @@ -// Expression compilation - Using ExpDesc system (Lua 5.4 compatible) - -use super::Compiler; -use super::binop::{emit_arith_op, emit_bitwise_op, emit_cmp_op, emit_arith_imm, emit_arith_k, emit_shift_imm, emit_binop_rr}; -use super::exp2reg::*; -use super::expdesc::*; -use super::helpers::*; -use crate::compiler::compile_block; -use crate::lua_value::UpvalueDesc; -use crate::lua_value::{Chunk, LuaValue}; -use crate::lua_vm::{Instruction, OpCode}; -use emmylua_parser::LuaClosureExpr; -use emmylua_parser::LuaIndexExpr; -use emmylua_parser::LuaIndexKey; -use emmylua_parser::LuaParenExpr; -use emmylua_parser::LuaTableExpr; -use emmylua_parser::UnaryOperator; -use emmylua_parser::{ - BinaryOperator, LuaAstToken, LuaBinaryExpr, LuaCallExpr, LuaExpr, LuaLiteralExpr, LuaLiteralToken, - LuaNameExpr, LuaUnaryExpr, LuaVarExpr, -}; - -/// Core function: Compile expression and return ExpDesc -/// This is the NEW primary API that replaces the old u32-based compile_expr -pub fn compile_expr_desc(c: &mut Compiler, expr: &LuaExpr) -> Result { - match expr { - LuaExpr::LiteralExpr(e) => compile_literal_expr_desc(c, e), - LuaExpr::NameExpr(e) => compile_name_expr_desc(c, e), - LuaExpr::BinaryExpr(e) => compile_binary_expr_desc(c, e), - LuaExpr::UnaryExpr(e) => compile_unary_expr_desc(c, e), - LuaExpr::ParenExpr(e) => compile_paren_expr_desc(c, e), - LuaExpr::CallExpr(e) => compile_call_expr_desc(c, e), - LuaExpr::IndexExpr(e) => compile_index_expr_desc(c, e), - LuaExpr::TableExpr(e) => compile_table_expr_desc(c, e), - LuaExpr::ClosureExpr(e) => compile_closure_expr_desc(c, e), - } -} - -//====================================================================================== -// OLD API: Backward compatibility wrappers -//====================================================================================== - -/// OLD API: Compile any expression and return the register containing the result -/// This is now a WRAPPER around compile_expr_desc() + exp_to_any_reg() -/// If dest is Some(reg), try to compile directly into that register to avoid extra Move -pub fn compile_expr(c: &mut Compiler, expr: &LuaExpr) -> Result { - compile_expr_to(c, expr, None) -} - -/// NEW: Compile literal expression (returns ExpDesc) -fn compile_literal_expr_desc(c: &mut Compiler, expr: &LuaLiteralExpr) -> Result { - let literal_token = expr - .get_literal() - .ok_or("Literal expression missing token")?; - - match literal_token { - LuaLiteralToken::Bool(b) => { - if b.is_true() { - Ok(ExpDesc::new_true()) - } else { - Ok(ExpDesc::new_false()) - } - } - LuaLiteralToken::Nil(_) => Ok(ExpDesc::new_nil()), - LuaLiteralToken::Number(num) => { - // Get the raw text to handle hex numbers correctly - let text = num.get_text(); - - // Check if this is a hex integer literal (0x... without decimal point or exponent) - // emmylua_parser may incorrectly treat large hex numbers as floats - let is_hex_int = (text.starts_with("0x") || text.starts_with("0X")) - && !text.contains('.') - && !text.to_lowercase().contains('p'); - - // Check if text has decimal point or exponent (should be treated as float) - // This handles cases like 1.0e19 or 9223372036854775808.0 - let text_lower = text.to_lowercase(); - let has_decimal_or_exp = text.contains('.') || - (!text_lower.starts_with("0x") && text_lower.contains('e')); - - // Treat as integer only if: no decimal/exponent OR is hex int - if (!num.is_float() && !has_decimal_or_exp) || is_hex_int { - // Parse as integer - use our custom parser for hex numbers - // Lua 5.4: 0xFFFFFFFFFFFFFFFF should be interpreted as -1 (two's complement) - // parse_lua_int may return Float if the number overflows i64 range - match parse_lua_int(text) { - ParsedNumber::Int(int_val) => Ok(ExpDesc::new_int(int_val)), - ParsedNumber::Float(float_val) => Ok(ExpDesc::new_float(float_val)), - } - } else { - let float_val = num.get_float_value(); - // Use VKFlt for floats - Ok(ExpDesc::new_float(float_val)) - } - } - LuaLiteralToken::String(s) => { - // Add string to constant table - let lua_string = create_string_value(c, &s.get_value()); - let const_idx = add_constant_dedup(c, lua_string); - Ok(ExpDesc::new_k(const_idx)) - } - LuaLiteralToken::Dots(_) => { - // Variable arguments: ... - // Allocate register and emit VARARG - // VARARG A C: R(A), ..., R(A+C-2) = vararg - // C=0 means all varargs, C>0 means C-1 values - let reg = alloc_register(c); - emit(c, Instruction::encode_abc(OpCode::Vararg, reg, 0, 2)); - Ok(ExpDesc::new_nonreloc(reg)) - } - _ => Err("Unsupported literal type".to_string()), - } -} - -/// NEW: Compile name expression (returns ExpDesc) -fn compile_name_expr_desc(c: &mut Compiler, expr: &LuaNameExpr) -> Result { - let name = expr.get_name_text().unwrap_or("".to_string()); - - // Check if it's a local variable - if let Some(local) = resolve_local(c, &name) { - // Local variables: use VLocal - // vidx is the index in the current function's locals array - return Ok(ExpDesc { - kind: ExpKind::VLocal, - info: local.register, - ival: 0, - nval: 0.0, - ind: IndexInfo { t: 0, idx: 0 }, - var: VarInfo { - ridx: local.register, - vidx: local.register as usize, - }, - t: 0, - f: 0, - }); - } - - // Try to resolve as upvalue - if let Some(upvalue_index) = resolve_upvalue_from_chain(c, &name) { - return Ok(ExpDesc { - kind: ExpKind::VUpval, - info: upvalue_index as u32, - ival: 0, - nval: 0.0, - ind: IndexInfo { t: 0, idx: 0 }, - var: VarInfo { ridx: 0, vidx: 0 }, - t: 0, - f: 0, - }); - } - - // It's a global variable - return VIndexUp - // _ENV is at upvalue index 0 (standard Lua convention) - let lua_string = create_string_value(c, &name); - let key_const_idx = add_constant_dedup(c, lua_string); - Ok(ExpDesc { - kind: ExpKind::VIndexUp, - info: 0, - ival: 0, - nval: 0.0, - ind: IndexInfo { - t: 0, - idx: key_const_idx, - }, - var: VarInfo { ridx: 0, vidx: 0 }, - t: 0, - f: 0, - }) -} - -/// NEW: Compile binary expression (returns ExpDesc) -/// This is the CRITICAL optimization - uses delayed code generation -fn compile_binary_expr_desc(c: &mut Compiler, expr: &LuaBinaryExpr) -> Result { - // Get operands and operator - let (left, right) = expr - .get_exprs() - .ok_or("Binary expression missing operands")?; - let op = expr - .get_op_token() - .ok_or("Binary expression missing operator")?; - let op_kind = op.get_op(); - - // Compile left operand to ExpDesc - let mut left_desc = compile_expr_desc(c, &left)?; - - // Discharge left to any register (this will allocate if needed) - let left_reg = exp_to_any_reg(c, &mut left_desc); - - // CRITICAL: Ensure freereg is at least left_reg+1 to prevent right expression - // from overwriting left's register during nested compilation - if c.freereg <= left_reg { - c.freereg = left_reg + 1; - } - - // Determine if we can reuse left's register - // We can only reuse if left_reg is a temporary register (>= nactvar) - let nactvar = nvarstack(c) as u32; - let can_reuse_left = left_reg >= nactvar; - - // Compile right operand to ExpDesc - let mut right_desc = compile_expr_desc(c, &right)?; - - // Use helper functions for arithmetic and bitwise operations - match op_kind { - // Arithmetic operations - use emit_arith_op helper - BinaryOperator::OpAdd - | BinaryOperator::OpSub - | BinaryOperator::OpMul - | BinaryOperator::OpDiv - | BinaryOperator::OpIDiv - | BinaryOperator::OpMod - | BinaryOperator::OpPow => { - let result_reg = emit_arith_op(c, op_kind, left_reg, &mut right_desc, can_reuse_left)?; - free_exp(c, &right_desc); - return Ok(ExpDesc::new_nonreloc(result_reg)); - } - - // Bitwise operations - use emit_bitwise_op helper - BinaryOperator::OpBAnd - | BinaryOperator::OpBOr - | BinaryOperator::OpBXor - | BinaryOperator::OpShl - | BinaryOperator::OpShr => { - let result_reg = emit_bitwise_op(c, op_kind, left_reg, &mut right_desc, can_reuse_left)?; - free_exp(c, &right_desc); - return Ok(ExpDesc::new_nonreloc(result_reg)); - } - - // Comparison operations - use emit_cmp_op helper - BinaryOperator::OpEq - | BinaryOperator::OpNe - | BinaryOperator::OpLt - | BinaryOperator::OpLe - | BinaryOperator::OpGt - | BinaryOperator::OpGe => { - let right_reg = exp_to_any_reg(c, &mut right_desc); - let result_reg = emit_cmp_op(c, op_kind, left_reg, right_reg, can_reuse_left); - free_exp(c, &right_desc); - return Ok(ExpDesc::new_nonreloc(result_reg)); - } - - // Special cases handled inline - _ => {} - } - - // Discharge right for remaining operators - let right_reg = exp_to_any_reg(c, &mut right_desc); - let result_reg; - - match op_kind { - BinaryOperator::OpConcat => { - // CONCAT A B: concatenate R[A] to R[A+B], result in R[A] - if right_reg == left_reg + 1 { - result_reg = left_reg; - emit(c, Instruction::encode_abc(OpCode::Concat, result_reg, 1, 0)); - c.freereg = result_reg + 1; - } else { - let concat_base = c.freereg; - alloc_register(c); - alloc_register(c); - emit_move(c, concat_base, left_reg); - emit_move(c, concat_base + 1, right_reg); - emit(c, Instruction::encode_abc(OpCode::Concat, concat_base, 1, 0)); - result_reg = concat_base; - c.freereg = result_reg + 1; - } - } - - BinaryOperator::OpAnd | BinaryOperator::OpOr => { - result_reg = if can_reuse_left { left_reg } else { alloc_register(c) }; - let k_flag = matches!(op_kind, BinaryOperator::OpOr); - emit(c, Instruction::create_abck(OpCode::TestSet, result_reg, left_reg, 0, k_flag)); - let jump_pos = emit_jump(c, OpCode::Jmp); - emit(c, Instruction::create_abc(OpCode::Move, result_reg, right_reg, 0)); - patch_jump(c, jump_pos); - } - - BinaryOperator::OpNop => { - return Err("Invalid binary operator OpNop".to_string()); - } - - // Already handled above - _ => unreachable!("Operator {:?} should have been handled above", op_kind), - } - - free_exp(c, &right_desc); - Ok(ExpDesc::new_nonreloc(result_reg)) -} - -/// NEW: Compile unary expression (stub - uses old implementation) -fn compile_unary_expr_desc(c: &mut Compiler, expr: &LuaUnaryExpr) -> Result { - // For now, call old implementation - let reg = compile_unary_expr_to(c, expr, None)?; - Ok(ExpDesc::new_nonreloc(reg)) -} - -/// NEW: Compile parenthesized expression (stub) -fn compile_paren_expr_desc(c: &mut Compiler, expr: &LuaParenExpr) -> Result { - let reg = compile_paren_expr_to(c, expr, None)?; - Ok(ExpDesc::new_nonreloc(reg)) -} - -/// NEW: Compile function call (stub) -fn compile_call_expr_desc(c: &mut Compiler, expr: &LuaCallExpr) -> Result { - let reg = compile_call_expr_to(c, expr, None)?; - Ok(ExpDesc::new_nonreloc(reg)) -} - -/// NEW: Compile index expression (stub) -fn compile_index_expr_desc(c: &mut Compiler, expr: &LuaIndexExpr) -> Result { - let reg = compile_index_expr_to(c, expr, None)?; - Ok(ExpDesc::new_nonreloc(reg)) -} - -/// NEW: Compile table constructor (stub) -fn compile_table_expr_desc(c: &mut Compiler, expr: &LuaTableExpr) -> Result { - let reg = compile_table_expr_to(c, expr, None)?; - Ok(ExpDesc::new_nonreloc(reg)) -} - -/// NEW: Compile closure/function expression (stub) -fn compile_closure_expr_desc(c: &mut Compiler, expr: &LuaClosureExpr) -> Result { - let reg = compile_closure_expr_to(c, expr, None, false, None)?; - Ok(ExpDesc::new_nonreloc(reg)) -} - -//====================================================================================== -// OLD IMPLEMENTATIONS: Keep for backward compatibility -//====================================================================================== - -/// Compile expression to a specific destination register if possible -pub fn compile_expr_to(c: &mut Compiler, expr: &LuaExpr, dest: Option) -> Result { - match expr { - LuaExpr::LiteralExpr(e) => compile_literal_expr(c, e, dest), - LuaExpr::NameExpr(e) => compile_name_expr_to(c, e, dest), - LuaExpr::BinaryExpr(e) => compile_binary_expr_to(c, e, dest), - LuaExpr::UnaryExpr(e) => compile_unary_expr_to(c, e, dest), - LuaExpr::ParenExpr(e) => compile_paren_expr_to(c, e, dest), - LuaExpr::CallExpr(e) => compile_call_expr_to(c, e, dest), - LuaExpr::IndexExpr(e) => compile_index_expr_to(c, e, dest), - LuaExpr::TableExpr(e) => compile_table_expr_to(c, e, dest), - LuaExpr::ClosureExpr(e) => compile_closure_expr_to(c, e, dest, false, None), - } -} - -/// Compile literal expression (number, string, true, false, nil) -fn compile_literal_expr( - c: &mut Compiler, - expr: &LuaLiteralExpr, - dest: Option, -) -> Result { - let reg = get_result_reg(c, dest); - - let literal_token = expr - .get_literal() - .ok_or("Literal expression missing token")?; - match literal_token { - LuaLiteralToken::Bool(b) => { - emit_load_bool(c, reg, b.is_true()); - } - LuaLiteralToken::Nil(_) => { - emit_load_nil(c, reg); - } - LuaLiteralToken::Number(num) => { - // Get the raw text to handle hex numbers correctly - let text = num.get_text(); - - // Check if this is a hex integer literal (0x... without decimal point or exponent) - // emmylua_parser may incorrectly treat large hex numbers as floats - let is_hex_int = (text.starts_with("0x") || text.starts_with("0X")) - && !text.contains('.') - && !text.to_lowercase().contains('p'); - - // Check if text has decimal point or exponent (should be treated as float) - // This handles cases like 1.0e19 or 9223372036854775808.0 - let text_lower = text.to_lowercase(); - let has_decimal_or_exp = text.contains('.') || - (!text_lower.starts_with("0x") && text_lower.contains('e')); - - // Lua 5.4 optimization: Try LoadI for integers, LoadF for simple floats - // Treat as integer only if: parser says integer AND no decimal/exponent AND is hex int - if (!num.is_float() && !has_decimal_or_exp) || is_hex_int { - match parse_lua_int(text) { - ParsedNumber::Int(int_val) => { - // Try LoadI first (fast path for small integers) - if let Some(_) = emit_loadi(c, reg, int_val) { - return Ok(reg); - } - // LoadI failed, add to constant table - let const_idx = add_constant_dedup(c, LuaValue::integer(int_val)); - emit_loadk(c, reg, const_idx); - } - ParsedNumber::Float(float_val) => { - // Number overflowed i64, treat as float - if emit_loadf(c, reg, float_val).is_none() { - let const_idx = add_constant_dedup(c, LuaValue::float(float_val)); - emit_loadk(c, reg, const_idx); - } - } - } - } else { - let float_val = num.get_float_value(); - // Try LoadF for integer-representable floats - if emit_loadf(c, reg, float_val).is_none() { - // LoadF failed, add to constant table - let const_idx = add_constant_dedup(c, LuaValue::float(float_val)); - emit_loadk(c, reg, const_idx); - } - } - } - LuaLiteralToken::String(s) => { - let string_val = s.get_value(); - let lua_string = create_string_value(c, &string_val); - let const_idx = add_constant_dedup(c, lua_string); - emit_loadk(c, reg, const_idx); - } - LuaLiteralToken::Dots(_) => { - // Variable arguments: ... - // VARARG A C: R(A), ..., R(A+C-2) = vararg - // C=0 means all varargs, C>0 means C-1 values - // For expression context, we load 1 vararg into the register (C=2 means 1 value) - emit(c, Instruction::encode_abc(OpCode::Vararg, reg, 0, 2)); - } - _ => {} - } - - Ok(reg) -} - -fn compile_name_expr_to( - c: &mut Compiler, - expr: &LuaNameExpr, - dest: Option, -) -> Result { - // Get the identifier name - let name = expr.get_name_text().unwrap_or("".to_string()); - - // Check if it's a local variable - if let Some(local) = resolve_local(c, &name) { - // If local is already in dest register, no move needed - if let Some(dest_reg) = dest { - if local.register != dest_reg { - emit_move(c, dest_reg, local.register); - } - return Ok(dest_reg); - } - return Ok(local.register); - } - - // Try to resolve as upvalue from parent scope chain - if let Some(upvalue_index) = resolve_upvalue_from_chain(c, &name) { - let reg = get_result_reg(c, dest); - let instr = Instruction::encode_abc(OpCode::GetUpval, reg, upvalue_index as u32, 0); - c.chunk.code.push(instr); - return Ok(reg); - } - - // It's a global variable - let reg = get_result_reg(c, dest); - emit_get_global(c, &name, reg); - Ok(reg) -} - -/// Try to evaluate an expression as a constant integer (for SETI/GETI optimization) -/// Returns Some(int_value) if the expression is a compile-time constant integer -fn try_eval_const_int(expr: &LuaExpr) -> Option { - match expr { - LuaExpr::LiteralExpr(lit) => { - if let Some(LuaLiteralToken::Number(num)) = lit.get_literal() { - let text = num.get_text(); - - // Check if this is a hex integer - let is_hex_int = (text.starts_with("0x") || text.starts_with("0X")) - && !text.contains('.') - && !text.to_lowercase().contains('p'); - - // Check if has decimal or exponent - let text_lower = text.to_lowercase(); - let has_decimal_or_exp = text.contains('.') || - (!text_lower.starts_with("0x") && text_lower.contains('e')); - - if (!num.is_float() && !has_decimal_or_exp) || is_hex_int { - match parse_lua_int(text) { - ParsedNumber::Int(int_val) => return Some(int_val), - ParsedNumber::Float(_) => return None, // Overflowed, not an integer - } - } - } - None - } - LuaExpr::BinaryExpr(bin_expr) => { - // Try to evaluate binary expressions with constant operands - let (left, right) = bin_expr.get_exprs()?; - let left_val = try_eval_const_int(&left)?; - let right_val = try_eval_const_int(&right)?; - - let op = bin_expr.get_op_token()?.get_op(); - match op { - BinaryOperator::OpAdd => Some(left_val + right_val), - BinaryOperator::OpSub => Some(left_val - right_val), - BinaryOperator::OpMul => Some(left_val * right_val), - BinaryOperator::OpDiv => { - let result = left_val as f64 / right_val as f64; - if result.fract() == 0.0 { - Some(result as i64) - } else { - None - } - } - BinaryOperator::OpIDiv => Some(left_val / right_val), - BinaryOperator::OpMod => Some(left_val % right_val), - BinaryOperator::OpBAnd => Some(left_val & right_val), - BinaryOperator::OpBOr => Some(left_val | right_val), - BinaryOperator::OpBXor => Some(left_val ^ right_val), - BinaryOperator::OpShl => Some(lua_shl(left_val, right_val)), - BinaryOperator::OpShr => Some(lua_shr(left_val, right_val)), - _ => None, - } - } - LuaExpr::UnaryExpr(un_expr) => { - // Try to evaluate unary expressions - let operand = un_expr.get_expr()?; - let op_val = try_eval_const_int(&operand)?; - - let op = un_expr.get_op_token()?.get_op(); - match op { - UnaryOperator::OpUnm => Some(-op_val), - UnaryOperator::OpBNot => Some(!op_val), - _ => None, - } - } - _ => None, - } -} - -fn compile_binary_expr_to( - c: &mut Compiler, - expr: &LuaBinaryExpr, - dest: Option, -) -> Result { - // Get left and right expressions from children - let (left, right) = expr.get_exprs().ok_or("error")?; - let op = expr.get_op_token().ok_or("error")?; - let op_kind = op.get_op(); - - // CONSTANT FOLDING for boolean literals (and, or) - if matches!(op_kind, BinaryOperator::OpAnd | BinaryOperator::OpOr) { - // Check if left operand is a boolean literal - if let LuaExpr::LiteralExpr(left_lit) = &left { - if let Some(LuaLiteralToken::Bool(b)) = left_lit.get_literal() { - let result_reg = get_result_reg(c, dest); - - if op_kind == BinaryOperator::OpAnd { - // true and X -> X, false and X -> false - if b.is_true() { - // Result is right operand - return compile_expr_to(c, &right, Some(result_reg)); - } else { - // Result is false - emit( - c, - Instruction::encode_abc(OpCode::LoadFalse, result_reg, 0, 0), - ); - return Ok(result_reg); - } - } else { - // true or X -> true, false or X -> X - if b.is_true() { - // Result is true - emit( - c, - Instruction::encode_abc(OpCode::LoadTrue, result_reg, 0, 0), - ); - return Ok(result_reg); - } else { - // Result is right operand - return compile_expr_to(c, &right, Some(result_reg)); - } - } - } - } - } - - // CONSTANT FOLDING: Check if both operands are numeric constants (including nested expressions) - // This matches luac behavior: 1+1 -> 2, 1+2*3 -> 7, etc. - // Use try_eval_const_int to recursively evaluate constant expressions - if matches!( - op_kind, - BinaryOperator::OpAdd - | BinaryOperator::OpSub - | BinaryOperator::OpMul - | BinaryOperator::OpDiv - | BinaryOperator::OpIDiv - | BinaryOperator::OpMod - | BinaryOperator::OpPow - | BinaryOperator::OpBAnd - | BinaryOperator::OpBOr - | BinaryOperator::OpBXor - | BinaryOperator::OpShl - | BinaryOperator::OpShr - ) { - if let (Some(left_int), Some(right_int)) = - (try_eval_const_int(&left), try_eval_const_int(&right)) - { - let left_val = left_int as f64; - let right_val = right_int as f64; - - let result_opt: Option = match op_kind { - BinaryOperator::OpAdd => Some(left_val + right_val), - BinaryOperator::OpSub => Some(left_val - right_val), - BinaryOperator::OpMul => Some(left_val * right_val), - BinaryOperator::OpDiv => Some(left_val / right_val), - BinaryOperator::OpIDiv => Some((left_val / right_val).floor()), - // Lua modulo: a % b = a - floor(a/b) * b (same sign as divisor) - BinaryOperator::OpMod => Some(left_val - (left_val / right_val).floor() * right_val), - BinaryOperator::OpPow => Some(left_val.powf(right_val)), - BinaryOperator::OpBAnd => Some((left_int & right_int) as f64), - BinaryOperator::OpBOr => Some((left_int | right_int) as f64), - BinaryOperator::OpBXor => Some((left_int ^ right_int) as f64), - BinaryOperator::OpShl => Some(lua_shl(left_int, right_int) as f64), - BinaryOperator::OpShr => Some(lua_shr(left_int, right_int) as f64), - _ => None, - }; - - if let Some(result) = result_opt { - let result_reg = get_result_reg(c, dest); - - // Emit the folded constant as LOADI or LOADF - let result_int = result as i64; - if result == result_int as f64 { - // Integer result - try LOADI first - if emit_loadi(c, result_reg, result_int).is_none() { - // Too large for LOADI, use LOADK - let lua_val = LuaValue::integer(result_int); - let const_idx = add_constant(c, lua_val); - emit( - c, - Instruction::encode_abx(OpCode::LoadK, result_reg, const_idx as u32), - ); - } - } else { - // Float result - try LOADF first, then LOADK - if emit_loadf(c, result_reg, result).is_none() { - let lua_val = LuaValue::number(result); - let const_idx = add_constant(c, lua_val); - emit( - c, - Instruction::encode_abx(OpCode::LoadK, result_reg, const_idx as u32), - ); - } - } - return Ok(result_reg); - } - } - } - - // OLD CONSTANT FOLDING (literal-only, kept for compatibility) - // This is now redundant but kept as fallback - if let (LuaExpr::LiteralExpr(left_lit), LuaExpr::LiteralExpr(right_lit)) = (&left, &right) { - if let (Some(LuaLiteralToken::Number(left_num)), Some(LuaLiteralToken::Number(right_num))) = - (left_lit.get_literal(), right_lit.get_literal()) - { - let left_val = if left_num.is_float() { - left_num.get_float_value() - } else { - left_num.get_int_value() as f64 - }; - - let right_val = if right_num.is_float() { - right_num.get_float_value() - } else { - right_num.get_int_value() as f64 - }; - - // Calculate result based on operator - let result_opt: Option = match op_kind { - BinaryOperator::OpAdd => Some(left_val + right_val), - BinaryOperator::OpSub => Some(left_val - right_val), - BinaryOperator::OpMul => Some(left_val * right_val), - BinaryOperator::OpDiv => Some(left_val / right_val), - BinaryOperator::OpIDiv => Some((left_val / right_val).floor()), - // Lua modulo: a % b = a - floor(a/b) * b (same sign as divisor) - BinaryOperator::OpMod => Some(left_val - (left_val / right_val).floor() * right_val), - BinaryOperator::OpPow => Some(left_val.powf(right_val)), - // Bitwise operations require integers - BinaryOperator::OpBAnd - | BinaryOperator::OpBOr - | BinaryOperator::OpBXor - | BinaryOperator::OpShl - | BinaryOperator::OpShr => { - if !left_num.is_float() && !right_num.is_float() { - let left_int = left_num.get_int_value() as i64; - let right_int = right_num.get_int_value() as i64; - let result_int = match op_kind { - BinaryOperator::OpBAnd => left_int & right_int, - BinaryOperator::OpBOr => left_int | right_int, - BinaryOperator::OpBXor => left_int ^ right_int, - BinaryOperator::OpShl => lua_shl(left_int, right_int), - BinaryOperator::OpShr => lua_shr(left_int, right_int), - _ => unreachable!(), - }; - Some(result_int as f64) - } else { - None - } - } - _ => None, - }; - - if let Some(result) = result_opt { - let result_reg = get_result_reg(c, dest); - - // Emit the folded constant as LOADI or LOADK - let result_int = result as i64; - if result == result_int as f64 { - // Integer result - try LOADI first - if emit_loadi(c, result_reg, result_int).is_none() { - // Too large for LOADI, use LOADK - let lua_val = LuaValue::integer(result_int); - let const_idx = add_constant(c, lua_val); - emit( - c, - Instruction::encode_abx(OpCode::LoadK, result_reg, const_idx as u32), - ); - } - } else { - // Float result - use LOADK - let lua_val = LuaValue::number(result); - let const_idx = add_constant(c, lua_val); - emit( - c, - Instruction::encode_abx(OpCode::LoadK, result_reg, const_idx as u32), - ); - } - return Ok(result_reg); - } - } - } - - // Try to optimize with immediate operands (Lua 5.4 optimization) - // Check if right operand is a small integer constant - if let LuaExpr::LiteralExpr(lit) = &right { - if let Some(LuaLiteralToken::Number(num)) = lit.get_literal() { - if !num.is_float() { - let int_val = num.get_int_value(); - // Use signed 9-bit immediate: range [-256, 255] - if int_val >= -256 && int_val <= 255 { - // Helper to prepare operands and get result register - let prepare_regs = |c: &mut Compiler, dest: Option, left: &LuaExpr| -> Result<(u32, u32), String> { - if let Some(d) = dest { - ensure_register(c, d); - if c.freereg < d + 1 { - c.freereg = d + 1; - } - } - let left_reg = compile_expr(c, left)?; - let nvarstack = nvarstack(c); - let can_reuse_left = left_reg >= nvarstack; - let result_reg = dest.unwrap_or_else(|| { - if can_reuse_left { left_reg } else { alloc_register(c) } - }); - Ok((left_reg, result_reg)) - }; - - match op_kind { - // Immediate ADD/SUB - BinaryOperator::OpAdd | BinaryOperator::OpSub => { - let (left_reg, result_reg) = prepare_regs(c, dest, &left)?; - if emit_arith_imm(c, op_kind, left_reg, int_val, result_reg).is_some() { - return Ok(result_reg); - } - } - // Constant MUL/DIV/IDIV/MOD/POW - use *K instructions - BinaryOperator::OpMul | BinaryOperator::OpDiv | BinaryOperator::OpIDiv - | BinaryOperator::OpMod | BinaryOperator::OpPow => { - let const_idx = add_constant_dedup(c, LuaValue::integer(int_val)); - let (left_reg, result_reg) = prepare_regs(c, dest, &left)?; - if emit_arith_k(c, op_kind, left_reg, const_idx, result_reg).is_some() { - return Ok(result_reg); - } - } - // Immediate SHL/SHR - BinaryOperator::OpShl | BinaryOperator::OpShr => { - let (left_reg, result_reg) = prepare_regs(c, dest, &left)?; - if emit_shift_imm(c, op_kind, left_reg, int_val, result_reg).is_some() { - return Ok(result_reg); - } - } - // Immediate comparison operators - generate boolean result - BinaryOperator::OpEq | BinaryOperator::OpNe | BinaryOperator::OpLt - | BinaryOperator::OpLe | BinaryOperator::OpGt | BinaryOperator::OpGe => { - if int_val >= -128 && int_val <= 127 { - let left_reg = compile_expr(c, &left)?; - let result_reg = get_result_reg(c, dest); - return compile_comparison_imm_to_bool(c, op_kind, left_reg, result_reg, int_val as i32); - } - } - _ => {} - } - } - } - } - } - - // FLOAT CONSTANT OPTIMIZATION: Check if right operand is a float literal - // Generate *K instructions for MUL/DIV/MOD/POW with constant operands - if let LuaExpr::LiteralExpr(lit) = &right { - if let Some(LuaLiteralToken::Number(num)) = lit.get_literal() { - let is_float_lit = num.is_float(); - - // Skip ADD/SUB small integers (already handled by immediate optimization) - let skip_optimization = if !is_float_lit { - let int_val = num.get_int_value(); - int_val >= -256 && int_val <= 255 && matches!(op_kind, BinaryOperator::OpAdd | BinaryOperator::OpSub) - } else { - false - }; - - if !skip_optimization { - // Check if this is a *K-supported operation - let is_k_op = matches!(op_kind, - BinaryOperator::OpMul | BinaryOperator::OpDiv | BinaryOperator::OpMod - | BinaryOperator::OpPow | BinaryOperator::OpIDiv - ); - - // IDiv only for integer constants - let idiv_ok = op_kind != BinaryOperator::OpIDiv || !is_float_lit; - - if is_k_op && idiv_ok { - let const_val = if is_float_lit { - LuaValue::float(num.get_float_value()) - } else { - LuaValue::integer(num.get_int_value()) - }; - let const_idx = add_constant_dedup(c, const_val); - - // Prepare registers - if let Some(d) = dest { - ensure_register(c, d); - if c.freereg < d + 1 { - c.freereg = d + 1; - } - } - let left_reg = compile_expr(c, &left)?; - let result_reg = get_result_reg(c, dest); - - if emit_arith_k(c, op_kind, left_reg, const_idx, result_reg).is_some() { - return Ok(result_reg); - } - } - } - } - } - - // Fall back to normal two-operand instruction - // CRITICAL: If dest is specified, protect freereg BEFORE compiling operands - // This prevents nested expressions from allocating temps that conflict with dest - if let Some(d) = dest { - ensure_register(c, d); // Ensure max_stack_size is updated - if c.freereg < d + 1 { - c.freereg = d + 1; - } - } - - // Compile left and right first to get their registers - let left_reg = compile_expr(c, &left)?; - - // Ensure right doesn't overwrite left - if c.freereg <= left_reg { - c.freereg = left_reg + 1; - } - - let right_reg = compile_expr(c, &right)?; - // Then allocate result register - let result_reg = get_result_reg(c, dest); - - // Determine opcode - for arithmetic/bitwise ops, use emit_binop_rr at the end - match op_kind { - // Arithmetic and bitwise operators use emit_binop_rr helper - BinaryOperator::OpAdd | BinaryOperator::OpSub | BinaryOperator::OpMul | - BinaryOperator::OpDiv | BinaryOperator::OpIDiv | BinaryOperator::OpMod | - BinaryOperator::OpPow | BinaryOperator::OpBAnd | BinaryOperator::OpBOr | - BinaryOperator::OpBXor | BinaryOperator::OpShl | BinaryOperator::OpShr => { - emit_binop_rr(c, op_kind, left_reg, right_reg, result_reg); - return Ok(result_reg); - } - BinaryOperator::OpConcat => { - // CONCAT has special instruction format: CONCAT A B - // Concatenates R[A] through R[A+B] (B+1 values), result in R[A] - - // LUA 5.4 OPTIMIZATION: Merge consecutive CONCAT operations - // When compiling "e1 .. e2", if e2's code just generated a CONCAT instruction, - // we can merge them into a single CONCAT that concatenates all values at once. - // This is critical for performance with chains like "a" .. "b" .. "c" - - let code = &c.chunk.code; - if !code.is_empty() { - let prev_instr = code[code.len() - 1]; - let prev_opcode = Instruction::get_opcode(prev_instr); - - if prev_opcode == OpCode::Concat { - let prev_a = Instruction::get_a(prev_instr); - let prev_b = Instruction::get_b(prev_instr); - - // Check if right_reg is the result of previous CONCAT - // Previous CONCAT: R[prev_a] = R[prev_a]..R[prev_a+1]..R[prev_a+prev_b] - // If right_reg == prev_a and left_reg == prev_a - 1, we can merge - if right_reg as u32 == prev_a && left_reg as u32 + 1 == prev_a { - // Perfect! Extend the CONCAT to include left_reg - // New CONCAT: R[left_reg] = R[left_reg]..R[left_reg+1]..R[left_reg+1+prev_b] - let last_idx = code.len() - 1; - c.chunk.code[last_idx] = Instruction::encode_abc( - OpCode::Concat, - left_reg, // Start from left_reg instead - prev_b + 1, // Increase count by 1 - 0, - ); - - // BUGFIX: Respect dest parameter - if let Some(d) = dest { - if d != left_reg { - emit_move(c, d, left_reg); - return Ok(d); - } - } - return Ok(left_reg); - } - } - } - - // Standard case: No merge possible, emit new CONCAT - // Check if operands are already consecutive - if right_reg == left_reg + 1 { - // Perfect case: operands are consecutive - let concat_reg = left_reg; - emit(c, Instruction::encode_abc(OpCode::Concat, concat_reg, 1, 0)); - if let Some(d) = dest { - if d != concat_reg { - emit_move(c, d, concat_reg); - } - return Ok(d); - } else { - return Ok(concat_reg); - } - } else { - // Need to arrange operands into consecutive registers - // CRITICAL FIX: Use fresh registers starting from freereg to avoid - // overwriting already allocated values (like function references) - let concat_reg = c.freereg; - alloc_register(c); // for left operand copy - alloc_register(c); // for right operand - - emit_move(c, concat_reg, left_reg); - emit_move(c, concat_reg + 1, right_reg); - emit(c, Instruction::encode_abc(OpCode::Concat, concat_reg, 1, 0)); - - // Reset freereg (concat consumes right operand) - c.freereg = concat_reg + 1; - - if let Some(d) = dest { - if d != concat_reg { - emit_move(c, d, concat_reg); - } - return Ok(d); - } - return Ok(concat_reg); - } - } - - // Comparison operators need special handling - they don't produce values directly - // Instead, they skip the next instruction if the comparison is true - // We need to generate: CMP + JMP + LFALSESKIP + LOADTRUE pattern - BinaryOperator::OpEq - | BinaryOperator::OpNe - | BinaryOperator::OpLt - | BinaryOperator::OpLe - | BinaryOperator::OpGt - | BinaryOperator::OpGe => { - // Handle comparison operators with proper boolean result generation - return compile_comparison_to_bool(c, op_kind, left_reg, right_reg, result_reg); - } - - BinaryOperator::OpAnd | BinaryOperator::OpOr => { - // Boolean operators with proper short-circuit evaluation - // Pattern: TESTSET + JMP + MOVE - // and: if left is false, return left; else return right - // or: if left is true, return left; else return right - let k_flag = matches!(op_kind, BinaryOperator::OpOr); - - // TestSet: if (is_truthy == k) then R[A] := R[B] else pc++ - emit( - c, - Instruction::create_abck(OpCode::TestSet, result_reg, left_reg, 0, k_flag), - ); - // JMP: skip the MOVE if TestSet assigned the value - let jump_pos = emit_jump(c, OpCode::Jmp); - // MOVE: use right operand if TestSet didn't assign - emit( - c, - Instruction::create_abc(OpCode::Move, result_reg, right_reg, 0), - ); - // Patch the jump to point after MOVE - patch_jump(c, jump_pos); - - return Ok(result_reg); - } - - _ => return Err(format!("Unsupported binary operator: {:?}", op_kind)), - } -} - -/// Compile comparison operator to produce boolean result -/// Generates: CMP + JMP + LFALSESKIP + LOADTRUE pattern -fn compile_comparison_to_bool( - c: &mut Compiler, - op_kind: BinaryOperator, - left_reg: u32, - right_reg: u32, - result_reg: u32, -) -> Result { - // Pattern: CMP with k=1 (skip if true) + JMP to true_label + LFALSESKIP + LOADTRUE - // If comparison is true: skip JMP, execute LFALSESKIP (skip LOADTRUE), wait that's wrong... - // Actually: CMP with k=1 (skip if true) means "skip next if comparison IS true" - // So: CMP(k=1) + JMP(to after_false) + LFALSESKIP + LOADTRUE - // If true: skip JMP, go to LFALSESKIP... no that's still wrong. - - // Let me trace luac output again: - // EQI 0 8 1 # if (R[0] == 8) != 1 then skip; which means: if R[0] != 8 then skip - // JMP 1 # jump over LFALSESKIP - // LFALSESKIP 0 # R[0] = false, skip LOADTRUE - // LOADTRUE 0 # R[0] = true - - // So when R[0] == 8: - // - EQI: condition is true, DON'T skip (k=1 means skip if result != 1) - // - Execute JMP: jump to LOADTRUE - // - Execute LOADTRUE: R[0] = true ✓ - - // When R[0] != 8: - // - EQI: condition is false, skip JMP - // - Execute LFALSESKIP: R[0] = false, skip LOADTRUE ✓ - - let (cmp_opcode, swap_operands, negate) = match op_kind { - BinaryOperator::OpEq => (OpCode::Eq, false, false), - BinaryOperator::OpNe => (OpCode::Eq, false, true), - BinaryOperator::OpLt => (OpCode::Lt, false, false), - BinaryOperator::OpLe => (OpCode::Le, false, false), - BinaryOperator::OpGt => (OpCode::Lt, true, false), // a > b == b < a - BinaryOperator::OpGe => (OpCode::Le, true, false), // a >= b == b <= a - _ => unreachable!(), - }; - - let (op1, op2) = if swap_operands { - (right_reg, left_reg) - } else { - (left_reg, right_reg) - }; - - // k=1 means "skip if comparison is true", k=0 means "skip if comparison is false" - // For boolean result, we want: if true -> set true, if false -> set false - // So we use k=1 (skip if true) with the JMP pattern - let k = if negate { false } else { true }; // k=1 for normal comparison - - // EQ A B k: compare R[A] with R[B] - // Note: comparison instructions don't produce results, they only test and skip - emit(c, Instruction::create_abck(cmp_opcode, op1, op2, 0, k)); - - // JMP over LFALSESKIP (offset = 1) - emit(c, Instruction::create_sj(OpCode::Jmp, 1)); - - // LFALSESKIP: load false into result register and skip next instruction - emit( - c, - Instruction::encode_abc(OpCode::LFalseSkip, result_reg, 0, 0), - ); - - // LOADTRUE: load true into result register - emit( - c, - Instruction::encode_abc(OpCode::LoadTrue, result_reg, 0, 0), - ); - - Ok(result_reg) -} - -/// Compile comparison operator with immediate value to produce boolean result -/// Generates: CMPI + JMP + LFALSESKIP + LOADTRUE pattern -fn compile_comparison_imm_to_bool( - c: &mut Compiler, - op_kind: BinaryOperator, - operand_reg: u32, - result_reg: u32, - imm_val: i32, -) -> Result { - // Immediate comparison instructions: EQI, LTI, LEI, GTI, GEI - // Pattern same as register comparison: CMPI(k=1) + JMP + LFALSESKIP + LOADTRUE - - let (cmp_opcode, negate) = match op_kind { - BinaryOperator::OpEq => (OpCode::EqI, false), - BinaryOperator::OpNe => (OpCode::EqI, true), - BinaryOperator::OpLt => (OpCode::LtI, false), - BinaryOperator::OpLe => (OpCode::LeI, false), - BinaryOperator::OpGt => (OpCode::GtI, false), - BinaryOperator::OpGe => (OpCode::GeI, false), - _ => unreachable!(), - }; - - // Encode immediate value with OFFSET_SB = 128 for signed B field - let imm = ((imm_val + 128) & 0xFF) as u32; - - let k = if negate { false } else { true }; - - // EQI A sB k: compare R[A] with immediate sB, k controls skip behavior - emit( - c, - Instruction::create_abck(cmp_opcode, operand_reg, imm, 0, k), - ); - - // JMP over LFALSESKIP (offset = 1) - emit(c, Instruction::create_sj(OpCode::Jmp, 1)); - - // LFALSESKIP: load false and skip next instruction - emit( - c, - Instruction::encode_abc(OpCode::LFalseSkip, result_reg, 0, 0), - ); - - // LOADTRUE: load true - emit( - c, - Instruction::encode_abc(OpCode::LoadTrue, result_reg, 0, 0), - ); - - Ok(result_reg) -} - -fn compile_unary_expr_to( - c: &mut Compiler, - expr: &LuaUnaryExpr, - dest: Option, -) -> Result { - let result_reg = get_result_reg(c, dest); - - // Get operator from text - let op_token = expr.get_op_token().ok_or("error")?; - let op_kind = op_token.get_op(); - - let operand = expr.get_expr().ok_or("Unary expression missing operand")?; - - // Constant folding optimizations - if let LuaExpr::LiteralExpr(lit_expr) = &operand { - match lit_expr.get_literal() { - Some(LuaLiteralToken::Number(num_token)) => { - if op_kind == UnaryOperator::OpUnm { - // Negative number literal: emit LOADI/LOADK with negated value - let text = num_token.get_text(); - - // Check if this is a hex integer - let is_hex_int = (text.starts_with("0x") || text.starts_with("0X")) - && !text.contains('.') - && !text.to_lowercase().contains('p'); - - // Check if has decimal or exponent - let text_lower = text.to_lowercase(); - let has_decimal_or_exp = text.contains('.') || - (!text_lower.starts_with("0x") && text_lower.contains('e')); - - // Determine if should parse as integer - if (!num_token.is_float() && !has_decimal_or_exp) || is_hex_int { - match parse_lua_int(text) { - ParsedNumber::Int(int_val) => { - // Successfully parsed as integer, negate it - let neg_val = int_val.wrapping_neg(); - - // Use LOADI for small integers - if let Some(_) = emit_loadi(c, result_reg, neg_val) { - return Ok(result_reg); - } - // Large integer, add to constant table - let const_idx = add_constant(c, LuaValue::integer(neg_val)); - emit_loadk(c, result_reg, const_idx); - return Ok(result_reg); - } - ParsedNumber::Float(float_val) => { - // Number overflowed i64, use negated float - let neg_val = -float_val; - let const_idx = add_constant(c, LuaValue::number(neg_val)); - emit_loadk(c, result_reg, const_idx); - return Ok(result_reg); - } - } - } else { - // Float literal - let float_val = num_token.get_float_value(); - let neg_val = -float_val; - let const_idx = add_constant(c, LuaValue::number(neg_val)); - emit_loadk(c, result_reg, const_idx); - return Ok(result_reg); - } - } - } - Some(LuaLiteralToken::Bool(b)) => { - if op_kind == UnaryOperator::OpNot { - // not true -> LOADFALSE, not false -> LOADTRUE - if b.is_true() { - emit( - c, - Instruction::encode_abc(OpCode::LoadFalse, result_reg, 0, 0), - ); - } else { - emit( - c, - Instruction::encode_abc(OpCode::LoadTrue, result_reg, 0, 0), - ); - } - return Ok(result_reg); - } - } - Some(LuaLiteralToken::Nil(_)) => { - if op_kind == UnaryOperator::OpNot { - // not nil -> LOADTRUE - emit( - c, - Instruction::encode_abc(OpCode::LoadTrue, result_reg, 0, 0), - ); - return Ok(result_reg); - } - } - _ => {} - } - } - - // Regular unary operation - let operand_reg = compile_expr(c, &operand)?; - - match op_kind { - UnaryOperator::OpBNot => { - emit( - c, - Instruction::encode_abc(OpCode::BNot, result_reg, operand_reg, 0), - ); - } - UnaryOperator::OpUnm => { - emit( - c, - Instruction::encode_abc(OpCode::Unm, result_reg, operand_reg, 0), - ); - } - UnaryOperator::OpNot => { - emit( - c, - Instruction::encode_abc(OpCode::Not, result_reg, operand_reg, 0), - ); - } - UnaryOperator::OpLen => { - emit( - c, - Instruction::encode_abc(OpCode::Len, result_reg, operand_reg, 0), - ); - } - UnaryOperator::OpNop => { - // No operation, just move operand to result - if operand_reg != result_reg { - emit_move(c, result_reg, operand_reg); - } - } - } - - Ok(result_reg) -} - -fn compile_paren_expr_to( - c: &mut Compiler, - expr: &LuaParenExpr, - dest: Option, -) -> Result { - // Get inner expression from children - let inner_expr = expr.get_expr().ok_or("missing inner expr")?; - let reg = compile_expr_to(c, &inner_expr, dest)?; - Ok(reg) -} - -/// Compile function call expression -pub fn compile_call_expr(c: &mut Compiler, expr: &LuaCallExpr) -> Result { - // For statement context (discard returns), use num_returns = 0 - // This will generate CALL with C=1 (0 returns expected) - compile_call_expr_with_returns(c, expr, 0) -} - -/// Compile a call expression with specified number of expected return values (public API) -pub fn compile_call_expr_with_returns( - c: &mut Compiler, - expr: &LuaCallExpr, - num_returns: usize, -) -> Result { - compile_call_expr_with_returns_and_dest(c, expr, num_returns, None) -} - -fn compile_call_expr_to( - c: &mut Compiler, - expr: &LuaCallExpr, - dest: Option, -) -> Result { - // Compile call with specified destination - // The call handler will decide whether to use dest as func_reg or allocate fresh registers - compile_call_expr_with_returns_and_dest(c, expr, 1, dest) -} - -/// Compile a call expression with specified number of expected return values and optional dest -pub fn compile_call_expr_with_returns_and_dest( - c: &mut Compiler, - expr: &LuaCallExpr, - num_returns: usize, - dest: Option, -) -> Result { - use emmylua_parser::{LuaExpr, LuaIndexKey}; - - // Get prefix (function) and arguments from children - let prefix_expr = expr.get_prefix_expr().ok_or("missing prefix expr")?; - let arg_exprs = expr - .get_args_list() - .ok_or("missing args list")? - .get_args() - .collect::>(); - - // Check if this is a method call (obj:method syntax) - let is_method = if let LuaExpr::IndexExpr(index_expr) = &prefix_expr { - index_expr - .get_index_token() - .map(|t| t.is_colon()) - .unwrap_or(false) - } else { - false - }; - // Track if we need to move return values back to original dest - let mut need_move_to_dest = false; - let original_dest = dest; - - // Handle method call with SELF instruction - let func_reg = if is_method { - if let LuaExpr::IndexExpr(index_expr) = &prefix_expr { - // Method call: obj:method(args) → SELF instruction - // SELF A B C: R(A+1) = R(B); R(A) = R(B)[C] - // A = function register, A+1 = self parameter - let func_reg = get_result_reg(c, dest); - - // Ensure func_reg+1 is allocated for self parameter - while c.freereg <= func_reg + 1 { - alloc_register(c); - } - - // Compile object (table) - let obj_expr = index_expr - .get_prefix_expr() - .ok_or("Method call missing object")?; - let obj_reg = compile_expr(c, &obj_expr)?; - - // Get method name - let method_name = - if let Some(LuaIndexKey::Name(name_token)) = index_expr.get_index_key() { - name_token.get_name_text().to_string() - } else { - return Err("Method call requires name index".to_string()); - }; - - // Add method name to constants - let lua_str = create_string_value(c, &method_name); - let key_idx = add_constant_dedup(c, lua_str); - - // Emit SELF instruction: R(func_reg+1) = R(obj_reg); R(func_reg) = R(obj_reg)[key] - emit( - c, - Instruction::create_abck( - OpCode::Self_, - func_reg, - obj_reg, - key_idx, - true, // k=1: C is constant index - ), - ); - - func_reg - } else { - unreachable!("is_method but not IndexExpr") - } - } else { - // Regular call: compile function expression - // OPTIMIZATION: If dest is specified and safe (>= nactvar), compile function directly to dest - // This avoids unnecessary MOVE instructions - let nactvar = c.nactvar as u32; - - let func_reg = if let Some(d) = dest { - // Check if we can safely use dest for function - let args_start = d + 1; - if d >= nactvar && args_start >= nactvar { - // Safe to compile directly to dest - let temp_func_reg = compile_expr_to(c, &prefix_expr, Some(d))?; - // Ensure we got the register we asked for (or move if needed) - if temp_func_reg != d { - ensure_register(c, d); - emit_move(c, d, temp_func_reg); - } - // Reset freereg to just past func_reg - c.freereg = d + 1; - d - } else { - // dest < nactvar: we need to use a safe temporary register - let temp_func_reg = compile_expr(c, &prefix_expr)?; - let new_func_reg = if c.freereg < nactvar { - c.freereg = nactvar; - alloc_register(c) - } else { - alloc_register(c) - }; - if temp_func_reg != new_func_reg { - emit_move(c, new_func_reg, temp_func_reg); - } - need_move_to_dest = true; - new_func_reg - } - } else { - // No dest specified - use default behavior - let temp_func_reg = compile_expr(c, &prefix_expr)?; - - if num_returns > 0 { - // Expression context - need return values - // CRITICAL: Must preserve local variables! - let nactvar = c.nactvar as u32; - if temp_func_reg < nactvar { - // Function is a local variable - must preserve it! - let new_reg = alloc_register(c); - emit_move(c, new_reg, temp_func_reg); - new_reg - } else if temp_func_reg + 1 == c.freereg { - // Function was just loaded into a fresh temporary register - safe to reuse - temp_func_reg - } else { - // Function is in an "old" temporary register - must preserve it! - let new_reg = alloc_register(c); - emit_move(c, new_reg, temp_func_reg); - new_reg - } - } else { - // num_returns == 0: Statement context, no return values needed - // BUT we still need to ensure arguments don't overwrite local variables! - let nactvar = c.nactvar as u32; - let args_would_start_at = temp_func_reg + 1; - - if args_would_start_at < nactvar || temp_func_reg < nactvar { - // Arguments would overwrite local variables! - // Move function to a safe register (at or after nactvar) - let new_reg = if c.freereg < nactvar { - c.freereg = nactvar; - alloc_register(c) - } else { - alloc_register(c) - }; - emit_move(c, new_reg, temp_func_reg); - new_reg - } else { - // Safe - neither function nor arguments overlap with locals - temp_func_reg - } - } - }; - - func_reg - }; - - // Compile arguments into consecutive registers - // For method calls: func_reg+1 is self, args start at func_reg+2 - // For regular calls: args start at func_reg+1 - let args_start = if is_method { - func_reg + 2 - } else { - func_reg + 1 - }; - let mut arg_regs = Vec::new(); - let mut last_arg_is_call_all_out = false; - - // Save freereg before compiling arguments - // We'll reset it before each argument so they compile into consecutive registers - let saved_freereg = c.freereg; - - // CRITICAL: Pre-reserve all argument registers before compiling any arguments - // This prevents nested call expressions from overwriting earlier argument registers - let num_fixed_args = arg_exprs.len(); - let args_end = args_start + num_fixed_args as u32; - - // Allocate all argument registers upfront - while c.freereg < args_end { - alloc_register(c); - } - - // CRITICAL: Compile arguments directly to their target positions - // This is how standard Lua ensures arguments are in consecutive registers - // Each argument should be compiled to args_start + i - for (i, arg_expr) in arg_exprs.iter().enumerate() { - let is_last = i == arg_exprs.len() - 1; - let arg_dest = args_start + i as u32; - - // CRITICAL: Before compiling each argument, ensure freereg is beyond ALL argument slots - // This prevents expressions from allocating temps that conflict with argument positions - if c.freereg < args_end { - c.freereg = args_end; - } - - // Ensure max_stack_size can accommodate this register - if arg_dest as usize >= c.chunk.max_stack_size { - c.chunk.max_stack_size = (arg_dest + 1) as usize; - } - - // OPTIMIZATION: If last argument is ... (vararg), use "all out" mode - if is_last { - if let LuaExpr::LiteralExpr(lit_expr) = arg_expr { - if matches!(lit_expr.get_literal(), Some(LuaLiteralToken::Dots(_))) { - // Vararg as last argument: VARARG with C=0 (all out) - emit(c, Instruction::encode_abc(OpCode::Vararg, arg_dest, 0, 0)); - arg_regs.push(arg_dest); - last_arg_is_call_all_out = true; - break; - } - } - } - - // OPTIMIZATION: If last argument is a call, use "all out" mode - // Simply recursively compile the call with usize::MAX returns - do NOT manually handle inner args - if is_last && matches!(arg_expr, LuaExpr::CallExpr(_)) { - if let LuaExpr::CallExpr(inner_call) = arg_expr { - // Use a simple approach: compile inner call to arg_dest with "all out" mode - // The recursive call will handle everything including method calls and nested calls - let call_result = compile_call_expr_with_returns_and_dest(c, inner_call, usize::MAX, Some(arg_dest))?; - if call_result != arg_dest { - ensure_register(c, arg_dest); - emit_move(c, arg_dest, call_result); - } - arg_regs.push(arg_dest); - last_arg_is_call_all_out = true; - break; - } - } - - // Compile argument directly to its target position - let arg_reg = compile_expr_to(c, arg_expr, Some(arg_dest))?; - if arg_reg != arg_dest { - ensure_register(c, arg_dest); - emit_move(c, arg_dest, arg_reg); - } - arg_regs.push(arg_dest); - } - - // Restore freereg to saved value or update to after last argument - // whichever is higher (to account for any temporary registers used) - let after_args = args_start + arg_regs.len() as u32; - c.freereg = std::cmp::max(saved_freereg, after_args); - - // Check if arguments are already in the correct positions - let mut need_move = false; - if !last_arg_is_call_all_out { - for (i, &arg_reg) in arg_regs.iter().enumerate() { - if arg_reg != args_start + i as u32 { - need_move = true; - break; - } - } - } - - // If arguments are not in consecutive registers, we need to move them - // CRITICAL FIX: Move from back to front to avoid overwriting! - if need_move { - // Reserve registers for arguments - while c.freereg < args_start + arg_regs.len() as u32 { - alloc_register(c); - } - - // Move arguments to correct positions FROM BACK TO FRONT to avoid overwriting - for i in (0..arg_regs.len()).rev() { - let arg_reg = arg_regs[i]; - let target_reg = args_start + i as u32; - if arg_reg != target_reg { - emit_move(c, target_reg, arg_reg); - } - } - } - - // Emit call instruction - // A = function register - // B = number of arguments + 1, or 0 if last arg was "all out" call - // For method calls, B includes the implicit self parameter - // C = number of expected return values + 1 (1 means 0 returns, 2 means 1 return, 0 means all returns) - // SPECIAL: when num_returns = usize::MAX, it means "all out" mode (C=0) - let arg_count = arg_exprs.len(); - let b_param = if last_arg_is_call_all_out { - 0 // B=0: all in - } else { - // For method calls, add 1 for implicit self parameter - let total_args = if is_method { arg_count + 1 } else { arg_count }; - (total_args + 1) as u32 - }; - // C=0 means "all out", C=1 means 0 returns, C=2 means 1 return, etc. - // When caller passes num_returns=usize::MAX, they mean "all out" (C=0) - let c_param = if num_returns == usize::MAX { - 0 // C=0: all out (take all return values) - } else { - (num_returns + 1) as u32 - }; - - emit( - c, - Instruction::encode_abc(OpCode::Call, func_reg, b_param, c_param), - ); - - // After CALL: adjust freereg based on return values - // CALL places return values starting at func_reg - // If num_returns == 0, CALL discards all returns - // If num_returns > 0, return values are in func_reg .. func_reg + num_returns - 1 - // If num_returns == usize::MAX, it's "all out" mode - we don't know how many returns - // - // CRITICAL: freereg can only be set to func_reg + num_returns if that's >= nactvar - // We cannot reclaim registers occupied by active local variables! - if num_returns != usize::MAX { - let new_freereg = func_reg + num_returns as u32; - if new_freereg >= c.nactvar as u32 { - c.freereg = new_freereg; - } - } - // For "all out" mode (num_returns == usize::MAX), keep freereg unchanged - // The caller (table constructor, etc.) will handle the stack properly - - // If we had to move function to avoid conflicts, move return values back to original dest - if need_move_to_dest { - if let Some(d) = original_dest { - // Move return values from func_reg to original dest - // CRITICAL: Don't do this for "all out" mode - we don't know how many values - if num_returns != usize::MAX { - for i in 0..num_returns { - emit_move(c, d + i as u32, func_reg + i as u32); - } - } - return Ok(d); - } - } - - Ok(func_reg) -} - -fn compile_index_expr_to( - c: &mut Compiler, - expr: &LuaIndexExpr, - dest: Option, -) -> Result { - // Get prefix (table) expression - let prefix_expr = expr - .get_prefix_expr() - .ok_or("Index expression missing table")?; - - // Lua 5.4 optimization: For chained indexing like a.b.c, we want to reuse registers - // When dest is specified and prefix is not a local variable, compile prefix to dest - // This way: a.b.c with dest=R2 becomes: - // GETFIELD R2 R(a) "b" ; a.b -> R2 - // GETFIELD R2 R2 "c" ; R2.c -> R2 - let nvarstack = nvarstack(c); - - // Check if prefix is a simple name (local/upvalue) that we shouldn't overwrite - let prefix_is_var = matches!(&prefix_expr, LuaExpr::NameExpr(_)); - - // Compile prefix with dest optimization for chained indexing - let table_reg = if dest.is_some() && !prefix_is_var { - // For chained indexing, compile intermediate result to dest - compile_expr_to(c, &prefix_expr, dest)? - } else { - compile_expr(c, &prefix_expr)? - }; - - // Determine result register - // If we compiled prefix to dest, result should also be dest - // Otherwise use the standard reuse-temp-register optimization - let can_reuse_table = table_reg >= nvarstack && table_reg + 1 == c.freereg; - let result_reg = dest.unwrap_or_else(|| { - if can_reuse_table { - table_reg - } else { - alloc_register(c) - } - }); - - // Get index key and emit optimized instruction if possible - let key = expr.get_index_key().ok_or("Index expression missing key")?; - match key { - LuaIndexKey::Integer(number_token) => { - // Optimized: table[integer_literal] -> GetTableI - // C field is 9 bits, so max value is 511 - let int_value = number_token.get_int_value(); - if int_value >= 0 && int_value <= 511 { - // Use GetTableI: R(A) := R(B)[C] - emit( - c, - Instruction::encode_abc(OpCode::GetI, result_reg, table_reg, int_value as u32), - ); - return Ok(result_reg); - } - // Fallback for out-of-range integers - let num_value = LuaValue::integer(int_value); - let const_idx = add_constant(c, num_value); - let key_reg = alloc_register(c); - emit_load_constant(c, key_reg, const_idx); - emit( - c, - Instruction::encode_abc(OpCode::GetTable, result_reg, table_reg, key_reg), - ); - Ok(result_reg) - } - LuaIndexKey::Name(name_token) => { - // Optimized: table.field -> GetField - let field_name = name_token.get_name_text(); - let lua_str = create_string_value(c, field_name); - let const_idx = add_constant_dedup(c, lua_str); - // Use GetField: R(A) := R(B)[K(C)] with k=1 - // ABC format: A=dest, B=table, C=const_idx - if const_idx <= Instruction::MAX_B { - emit( - c, - Instruction::create_abck( - OpCode::GetField, - result_reg, - table_reg, - const_idx, - true, - ), - ); - return Ok(result_reg); - } - // Fallback for large const_idx - let key_reg = alloc_register(c); - emit_loadk(c, key_reg, const_idx); - emit( - c, - Instruction::encode_abc(OpCode::GetTable, result_reg, table_reg, key_reg), - ); - Ok(result_reg) - } - LuaIndexKey::String(string_token) => { - // Optimized: table["string"] -> GetField - let string_value = string_token.get_value(); - let lua_str = create_string_value(c, &string_value); - let const_idx = add_constant_dedup(c, lua_str); - if const_idx <= Instruction::MAX_B { - emit( - c, - Instruction::create_abck( - OpCode::GetField, - result_reg, - table_reg, - const_idx, - true, - ), - ); - return Ok(result_reg); - } - // Fallback - let key_reg = alloc_register(c); - emit_loadk(c, key_reg, const_idx); - emit( - c, - Instruction::encode_abc(OpCode::GetTable, result_reg, table_reg, key_reg), - ); - Ok(result_reg) - } - LuaIndexKey::Expr(key_expr) => { - // Generic: table[expr] -> GetTable - let key_reg = compile_expr(c, &key_expr)?; - emit( - c, - Instruction::encode_abc(OpCode::GetTable, result_reg, table_reg, key_reg), - ); - Ok(result_reg) - } - LuaIndexKey::Idx(_i) => { - // Fallback for other index types - Err("Unsupported index key type".to_string()) - } - } -} - -fn compile_table_expr_to( - c: &mut Compiler, - expr: &LuaTableExpr, - dest: Option, -) -> Result { - // Get all fields first to check if we need to use a temporary register - let fields: Vec<_> = expr.get_fields().collect(); - - // CRITICAL FIX: When dest is a local variable register (< nactvar) and we have - // non-empty table constructor, we must NOT use dest directly. This is because - // table elements will be compiled into consecutive registers starting from reg+1, - // which could overwrite other local variables. - // - // Example: `local a,b,c; a = {f()}` where a=R0, b=R1, c=R2 - // If we create table at R0, function and args go to R1, R2... overwriting b, c! - // - // Solution: When dest < nactvar AND table is non-empty, ignore dest and use - // a fresh temporary register. At the end, we move the result to dest. - let original_dest = dest; - let need_move_to_dest = if let Some(d) = dest { - !fields.is_empty() && d < c.nactvar as u32 - } else { - false - }; - - // If we need to protect locals, ignore dest and allocate a fresh register - let effective_dest = if need_move_to_dest { - None - } else { - dest - }; - - let reg = get_result_reg(c, effective_dest); - - // Fields already collected above - let fields: Vec<_> = expr.get_fields().collect(); - - // Separate array part from hash part to count sizes - let mut array_count = 0; - let mut hash_count = 0; - - for (i, field) in fields.iter().enumerate() { - if field.is_value_field() { - // Check if it's a simple value (not ... or call as last element) - if let Some(value_expr) = field.get_value_expr() { - let is_dots = is_vararg_expr(&value_expr); - let is_call = matches!(&value_expr, LuaExpr::CallExpr(_)); - let is_last = i == fields.len() - 1; - - // Stop counting if we hit ... or call as last element - if is_last && (is_dots || is_call) { - break; - } - } - array_count += 1; - } else { - // Hash field - hash_count += 1; - } - } - - // Helper function to compute ceil(log2(x)) + 1 for hash size encoding - // This matches Lua's encoding: rb = (hsize != 0) ? luaO_ceillog2(hsize) + 1 : 0 - fn ceillog2_plus1(x: usize) -> u32 { - if x == 0 { - 0 - } else if x == 1 { - 1 - } else { - // ceil(log2(x)) = number of bits needed to represent x-1, which is floor(log2(x-1)) + 1 - // For x > 1: ceil(log2(x)) = 32 - (x-1).leading_zeros() for u32 - let bits = usize::BITS - (x - 1).leading_zeros(); - bits + 1 // +1 as per Lua encoding - } - } - - // Create table with size hints - // NEWTABLE A B C k: B = log2(hash_size)+1, C = array_size % 256 - // EXTRAARG contains array_size / 256 when k=1 - const MAXARG_C: usize = 255; - let b_param = ceillog2_plus1(hash_count); - let extra = array_count / (MAXARG_C + 1); // higher bits of array size - let c_param = (array_count % (MAXARG_C + 1)) as u32; // lower bits of array size - let k = if extra > 0 { 1 } else { 0 }; - emit( - c, - Instruction::encode_abck(OpCode::NewTable, reg, b_param, c_param, k), - ); - - // EXTRAARG instruction for extended array size - emit(c, Instruction::create_ax(OpCode::ExtraArg, extra as u32)); - - if fields.is_empty() { - return Ok(reg); - } - - // Track array indices that need to be processed - let mut array_idx = 0; - let values_start = reg + 1; - let mut has_vararg_at_end = false; - - // CRITICAL: Pre-reserve registers for array elements BEFORE processing any fields. - // This prevents hash field value expressions (like `select('#', ...)`) from - // allocating temporary registers that conflict with array element positions. - // Without this, `{n = select('#', ...), ...}` would have `select` use reg+1, - // which should be reserved for the first vararg element. - let array_values_end = values_start + array_count as u32; - while c.freereg < array_values_end { - alloc_register(c); - } - let mut call_at_end_idx: Option = None; - - // Process all fields in source order - // Array elements are loaded to registers, hash fields are set immediately - for (field_idx, field) in fields.iter().enumerate() { - let is_last_field = field_idx == fields.len() - 1; - - if field.is_value_field() { - // Array element or special case (vararg/call at end) - if let Some(value_expr) = field.get_value_expr() { - let is_dots = is_vararg_expr(&value_expr); - let is_call = matches!(&value_expr, LuaExpr::CallExpr(_)); - - if is_last_field && is_dots { - // VarArg expansion: {...} or {a, b, ...} - // Will be handled after all hash fields - has_vararg_at_end = true; - continue; - } else if is_last_field && is_call { - // Call as last element: returns multiple values - // Will be handled after all hash fields - call_at_end_idx = Some(field_idx); - continue; - } - - // Regular array element: load to consecutive register - let target_reg = values_start + array_idx; - while c.freereg <= target_reg { - alloc_register(c); - } - let value_reg = compile_expr_to(c, &value_expr, Some(target_reg))?; - if value_reg != target_reg { - emit_move(c, target_reg, value_reg); - } - array_idx += 1; - } - } else { - // Hash field: process immediately with SETFIELD/SETI/SETTABLE - let Some(field_key) = field.get_field_key() else { - continue; - }; - - let key_reg = match field_key { - LuaIndexKey::Name(name_token) => { - // key is an identifier - use SetField optimization - let key_name = name_token.get_name_text(); - let lua_str = create_string_value(c, key_name); - let const_idx = add_constant_dedup(c, lua_str); - - // Try to compile value as constant first (for RK optimization) - let (value_operand, use_constant) = - if let Some(value_expr) = field.get_value_expr() { - if let Some(k_idx) = try_expr_as_constant(c, &value_expr) { - (k_idx, true) - } else { - (compile_expr(c, &value_expr)?, false) - } - } else { - let r = alloc_register(c); - emit_load_nil(c, r); - (r, false) - }; - - // Use SetField: R(A)[K(B)] := RK(C) - // k=1 means C is constant index, k=0 means C is register - emit( - c, - Instruction::create_abck( - OpCode::SetField, - reg, - const_idx, - value_operand, - use_constant, - ), - ); - - continue; // Skip the SetTable at the end - } - LuaIndexKey::String(string_token) => { - // key is a string literal - use SetField optimization - let string_value = string_token.get_value(); - let lua_str = create_string_value(c, &string_value); - let const_idx = add_constant_dedup(c, lua_str); - - // Try to compile value as constant first (for RK optimization) - let (value_operand, use_constant) = - if let Some(value_expr) = field.get_value_expr() { - if let Some(k_idx) = try_expr_as_constant(c, &value_expr) { - (k_idx, true) - } else { - (compile_expr(c, &value_expr)?, false) - } - } else { - let r = alloc_register(c); - emit_load_nil(c, r); - (r, false) - }; - - // Use SetField: R(A)[K(B)] := RK(C) - // k=1 means C is constant index, k=0 means C is register - emit( - c, - Instruction::create_abck( - OpCode::SetField, - reg, - const_idx, - value_operand, - use_constant, - ), - ); - - continue; // Skip the SetTable at the end - } - LuaIndexKey::Integer(number_token) => { - // key is a numeric literal - try SETI optimization - if !number_token.is_float() { - let int_value = number_token.get_int_value(); - // SETI: B field is unsigned byte, range 0-255 - if int_value >= 0 && int_value <= 255 { - // Try to compile value as constant first (for RK optimization) - let (value_operand, use_constant) = - if let Some(value_expr) = field.get_value_expr() { - if let Some(k_idx) = try_expr_as_constant(c, &value_expr) { - (k_idx, true) - } else { - (compile_expr(c, &value_expr)?, false) - } - } else { - let r = alloc_register(c); - emit_load_nil(c, r); - (r, false) - }; - - // Use SETI: R(A)[B] := RK(C) where B is unsigned byte - let encoded_b = int_value as u32; - emit( - c, - Instruction::create_abck( - OpCode::SetI, - reg, - encoded_b, - value_operand, - use_constant, - ), - ); - - continue; // Skip the SetTable at the end - } - } - - // Fall back to SETTABLE for floats or large integers - let const_idx = if number_token.is_float() { - let num_value = number_token.get_float_value(); - add_constant(c, LuaValue::number(num_value)) - } else { - let int_value = number_token.get_int_value(); - let num_value = LuaValue::integer(int_value); - add_constant(c, num_value) - }; - - let key_reg = alloc_register(c); - emit_load_constant(c, key_reg, const_idx); - key_reg - } - LuaIndexKey::Expr(key_expr) => { - // key is an expression - try to evaluate as constant integer for SETI - if let Some(int_val) = try_eval_const_int(&key_expr) { - // SETI: B field is unsigned byte, range 0-255 - if int_val >= 0 && int_val <= 255 { - // Use SETI for small integer keys - let (value_operand, use_constant) = - if let Some(value_expr) = field.get_value_expr() { - if let Some(k_idx) = try_expr_as_constant(c, &value_expr) { - (k_idx, true) - } else { - (compile_expr(c, &value_expr)?, false) - } - } else { - let r = alloc_register(c); - emit_load_nil(c, r); - (r, false) - }; - - // B is unsigned byte - let encoded_b = int_val as u32; - emit( - c, - Instruction::create_abck( - OpCode::SetI, - reg, - encoded_b, - value_operand, - use_constant, - ), - ); - - continue; // Skip the SetTable at the end - } - } - - // Fall back to compiling key as expression - compile_expr(c, &key_expr)? - } - LuaIndexKey::Idx(_i) => { - return Err("Unsupported table field key type".to_string()); - } - }; - - // Compile value expression - // Try to use constant optimization (RK operand) - let (value_operand, use_constant) = if let Some(value_expr) = field.get_value_expr() { - if let Some(k_idx) = try_expr_as_constant(c, &value_expr) { - (k_idx, true) - } else { - (compile_expr(c, &value_expr)?, false) - } - } else { - let r = alloc_register(c); - emit_load_nil(c, r); - (r, false) - }; - - // Set table field: table[key] = value - // Use k-suffix if value is a constant - emit( - c, - Instruction::create_abck( - OpCode::SetTable, - reg, - key_reg, - value_operand, - use_constant, - ), - ); - } - } - - // Handle vararg or call at end (after all hash fields) - if has_vararg_at_end { - // VarArg expansion: {...} or {a, b, ...} - emit( - c, - Instruction::encode_abc(OpCode::Vararg, values_start + array_idx, 0, 0), - ); - - // SetList with B=0 (all remaining values) - let c_param = (array_idx as usize / 50) as u32; - emit(c, Instruction::encode_abc(OpCode::SetList, reg, 0, c_param)); - - c.freereg = reg + 1; - // Move result to original destination if needed - if need_move_to_dest { - if let Some(d) = original_dest { - emit_move(c, d, reg); - return Ok(d); - } - } - return Ok(reg); - } - - if let Some(idx) = call_at_end_idx { - // Call as last element: compile call with all return values - let target_reg = values_start + array_idx; - while c.freereg <= target_reg { - alloc_register(c); - } - - // Get the call expression and compile it - if let Some(field) = fields.get(idx) { - if let Some(value_expr) = field.get_value_expr() { - if let LuaExpr::CallExpr(call_expr) = value_expr { - // Compile the call with all return values (usize::MAX means all) - compile_call_expr_with_returns_and_dest(c, &call_expr, usize::MAX, Some(target_reg))?; - } - } - } - - // SetList with B=0 (all remaining values including call returns) - let c_param = (array_idx as usize / 50) as u32; - emit(c, Instruction::encode_abc(OpCode::SetList, reg, 0, c_param)); - - c.freereg = reg + 1; - // Move result to original destination if needed - if need_move_to_dest { - if let Some(d) = original_dest { - emit_move(c, d, reg); - return Ok(d); - } - } - return Ok(reg); - } - - // Emit SETLIST for all array elements at the end - // Process in batches of 50 (LFIELDS_PER_FLUSH) - if array_idx > 0 { - const BATCH_SIZE: u32 = 50; - let mut batch_start = 0; - - while batch_start < array_idx { - let batch_end = (batch_start + BATCH_SIZE).min(array_idx); - let batch_count = batch_end - batch_start; - let c_param = (batch_start / BATCH_SIZE) as u32; - - emit( - c, - Instruction::encode_abc(OpCode::SetList, reg, batch_count, c_param), - ); - - batch_start = batch_end; - } - } - - // Free temporary registers used during table construction - // Reset to table_reg + 1 to match luac's register allocation behavior - c.freereg = reg + 1; - - // Move result to original destination if needed - if need_move_to_dest { - if let Some(d) = original_dest { - emit_move(c, d, reg); - return Ok(d); - } - } - - Ok(reg) -} - -/// Compile a variable expression for assignment -pub fn compile_var_expr(c: &mut Compiler, var: &LuaVarExpr, value_reg: u32) -> Result<(), String> { - match var { - LuaVarExpr::NameExpr(name_expr) => { - let name = name_expr.get_name_text().unwrap_or("".to_string()); - - // Check if it's a local variable - if let Some(local) = resolve_local(c, &name) { - // Move to local register - emit_move(c, local.register, value_reg); - return Ok(()); - } - - // Try to resolve as upvalue from parent scope chain - if let Some(upvalue_index) = resolve_upvalue_from_chain(c, &name) { - let instr = - Instruction::encode_abc(OpCode::SetUpval, value_reg, upvalue_index as u32, 0); - c.chunk.code.push(instr); - return Ok(()); - } - - // Set global - emit_set_global(c, &name, value_reg); - Ok(()) - } - LuaVarExpr::IndexExpr(index_expr) => { - // Get table and key expressions from children - let prefix_expr = index_expr - .get_prefix_expr() - .ok_or("Index expression missing table")?; - - let table_reg = compile_expr(c, &prefix_expr)?; - - // Determine key and emit optimized instruction if possible - let index_key = index_expr - .get_index_key() - .ok_or("Index expression missing key")?; - - match index_key { - LuaIndexKey::Integer(number_token) => { - // Optimized: table[integer] = value -> SETI A B C k - // B field is unsigned byte, range 0-255 - let int_value = number_token.get_int_value(); - if int_value >= 0 && int_value <= 255 { - // Use SETI: R(A)[B] := RK(C) - let encoded_b = int_value as u32; - emit( - c, - Instruction::encode_abc(OpCode::SetI, table_reg, encoded_b, value_reg), - ); - return Ok(()); - } - // Fallback for out-of-range integers - let num_value = LuaValue::integer(int_value); - let const_idx = add_constant(c, num_value); - let key_reg = alloc_register(c); - emit_load_constant(c, key_reg, const_idx); - emit( - c, - Instruction::encode_abc(OpCode::SetTable, table_reg, key_reg, value_reg), - ); - Ok(()) - } - LuaIndexKey::Name(name_token) => { - // Optimized: table.field = value -> SetField - let field_name = name_token.get_name_text().to_string(); - let lua_str = create_string_value(c, &field_name); - let const_idx = add_constant_dedup(c, lua_str); - // Use SetField: R(A)[K(B)] := RK(C) - // k=0 because value_reg is a register (already compiled) - if const_idx <= Instruction::MAX_B { - emit( - c, - Instruction::create_abck( - OpCode::SetField, - table_reg, - const_idx, - value_reg, - false, // k=0: C is register - ), - ); - return Ok(()); - } - // Fallback - let key_reg = alloc_register(c); - emit_load_constant(c, key_reg, const_idx); - emit( - c, - Instruction::encode_abc(OpCode::SetTable, table_reg, key_reg, value_reg), - ); - Ok(()) - } - LuaIndexKey::String(string_token) => { - // Optimized: table["string"] = value -> SETFIELD A B C k - // A: table register - // B: key (constant index) - // C: value (register or constant, determined by k) - let string_value = string_token.get_value(); - let lua_str = create_string_value(c, &string_value); - let const_idx = add_constant_dedup(c, lua_str); - if const_idx <= Instruction::MAX_B { - emit( - c, - Instruction::create_abck( - OpCode::SetField, - table_reg, - const_idx, - value_reg, - false, // k=0: C is register (value_reg is a register!) - ), - ); - return Ok(()); - } - // Fallback - let key_reg = alloc_register(c); - emit_load_constant(c, key_reg, const_idx); - emit( - c, - Instruction::encode_abc(OpCode::SetTable, table_reg, key_reg, value_reg), - ); - Ok(()) - } - LuaIndexKey::Expr(key_expr) => { - // Generic: table[expr] = value -> SetTable - let key_reg = compile_expr(c, &key_expr)?; - emit( - c, - Instruction::encode_abc(OpCode::SetTable, table_reg, key_reg, value_reg), - ); - Ok(()) - } - LuaIndexKey::Idx(_i) => Err("Unsupported index key type".to_string()), - } - } - } -} - -pub fn compile_closure_expr_to( - c: &mut Compiler, - closure: &LuaClosureExpr, - dest: Option, - is_method: bool, - func_name: Option, -) -> Result { - let params_list = closure - .get_params_list() - .ok_or("closure missing params list")?; - - let params = params_list.get_params().collect::>(); - - // Handle empty function body (e.g., function noop() end) - let has_body = closure.get_block().is_some(); - - // Create a new compiler for the function body with parent scope chain - // No need to sync anymore - scope_chain is already current - let mut func_compiler = - Compiler::new_with_parent(c.scope_chain.clone(), c.vm_ptr, c.line_index, c.last_line); - func_compiler.chunk.source_name = func_name; - // For methods (function defined with colon syntax), add implicit 'self' parameter - let mut param_offset = 0; - if is_method { - func_compiler - .scope_chain - .borrow_mut() - .locals - .push(super::Local { - name: "self".to_string(), - depth: 0, - register: 0, - is_const: false, - is_to_be_closed: false, - needs_close: false, - }); - func_compiler.chunk.locals.push("self".to_string()); - param_offset = 1; - } - - // Set up parameters as local variables - let mut has_vararg = false; - let mut regular_param_count = 0; - for (i, param) in params.iter().enumerate() { - // Check if this is a vararg parameter - if param.is_dots() { - has_vararg = true; - // Don't add ... to locals or count it as a regular parameter - continue; - } - - // Try to get parameter name - let param_name = if let Some(name_token) = param.get_name_token() { - name_token.get_name_text().to_string() - } else { - format!("param{}", i + 1) - }; - - let reg_index = (regular_param_count + param_offset) as u32; - func_compiler - .scope_chain - .borrow_mut() - .locals - .push(super::Local { - name: param_name.clone(), - depth: 0, - register: reg_index, - is_const: false, - is_to_be_closed: false, - needs_close: false, - }); - func_compiler.chunk.locals.push(param_name); - regular_param_count += 1; - } - - func_compiler.chunk.param_count = regular_param_count + param_offset; - func_compiler.chunk.is_vararg = has_vararg; - func_compiler.freereg = (regular_param_count + param_offset) as u32; - func_compiler.peak_freereg = func_compiler.freereg; // CRITICAL: Initialize peak_freereg with parameters! - func_compiler.nactvar = (regular_param_count + param_offset) as usize; - - // Emit VarargPrep instruction if function accepts varargs - // VARARGPREP A: A = number of fixed parameters (not counting ...) - if has_vararg { - let varargprep_instr = Instruction::encode_abc( - OpCode::VarargPrep, - (regular_param_count + param_offset) as u32, - 0, - 0, - ); - func_compiler.chunk.code.push(varargprep_instr); - } - - // Compile function body (skip if empty) - if has_body { - let body = closure.get_block().unwrap(); - compile_block(&mut func_compiler, &body)?; - } - - // Add implicit return if needed - // Lua 5.4 ALWAYS adds a final RETURN0 at the end of functions for safety - // This serves as a fallthrough in case execution reaches the end - if func_compiler.chunk.code.is_empty() { - // Empty function - use Return0 - let ret_instr = Instruction::encode_abc(OpCode::Return0, 0, 0, 0); - func_compiler.chunk.code.push(ret_instr); - } else { - let last_opcode = Instruction::get_opcode(*func_compiler.chunk.code.last().unwrap()); - if last_opcode != OpCode::Return0 { - // Always add final Return0 for fallthrough protection - let ret_instr = Instruction::encode_abc(OpCode::Return0, 0, 0, 0); - func_compiler.chunk.code.push(ret_instr); - } - } - - // Set max_stack_size to the maximum of peak_freereg and current max_stack_size - // peak_freereg tracks registers allocated via alloc_register() - // but max_stack_size may be higher due to direct register usage via dest parameter - func_compiler.chunk.max_stack_size = std::cmp::max( - func_compiler.peak_freereg as usize, - func_compiler.chunk.max_stack_size, - ); - - // Store upvalue information from scope_chain - let upvalues = func_compiler.scope_chain.borrow().upvalues.clone(); - func_compiler.chunk.upvalue_count = upvalues.len(); - func_compiler.chunk.upvalue_descs = upvalues - .iter() - .map(|uv| UpvalueDesc { - is_local: uv.is_local, - index: uv.index, - }) - .collect(); - - // Move child chunks from func_compiler to its own chunk's child_protos - let child_protos: Vec> = func_compiler - .child_chunks - .into_iter() - .map(std::rc::Rc::new) - .collect(); - func_compiler.chunk.child_protos = child_protos; - - // Add the function chunk to the parent compiler's child_chunks - let chunk_index = c.child_chunks.len(); - c.child_chunks.push(func_compiler.chunk); - - // Emit Closure instruction - use dest if provided - let dest_reg = dest.unwrap_or_else(|| { - let r = c.freereg; - c.freereg += 1; - r - }); - - // Update peak_freereg to account for this register - // This is crucial when dest is provided (e.g., in assignments) - if dest_reg + 1 > c.peak_freereg { - c.peak_freereg = dest_reg + 1; - } - - // Ensure max_stack_size accounts for this register - if (dest_reg + 1) as usize > c.chunk.max_stack_size { - c.chunk.max_stack_size = (dest_reg + 1) as usize; - } - - let closure_instr = Instruction::encode_abx(OpCode::Closure, dest_reg, chunk_index as u32); - c.chunk.code.push(closure_instr); - - // Note: Upvalue initialization is handled by the VM's exec_closure function - // using the upvalue_descs from the child chunk. No additional instructions needed. - - Ok(dest_reg) -} diff --git a/crates/luars/src/compiler/expr_parser.rs b/crates/luars/src/compiler/expr_parser.rs new file mode 100644 index 0000000..94424d5 --- /dev/null +++ b/crates/luars/src/compiler/expr_parser.rs @@ -0,0 +1,829 @@ +// Expression parsing - Port from lparser.c (Lua 5.4.8) +// This file corresponds to expression parsing parts of lua-5.5.0/src/lparser.c +use crate::compiler::expression::{ExpDesc, ExpKind, ExpUnion}; +use crate::compiler::func_state::{BlockCnt, FuncState}; +use crate::compiler::parse_literal::{ + NumberResult, parse_float_token_value, parse_int_token_value, parse_string_token_value, +}; +use crate::compiler::parser::{ + BinaryOperator, LuaTokenKind, UNARY_PRIORITY, UnaryOperator, to_binary_operator, + to_unary_operator, +}; +use crate::compiler::statement::{self, mark_upval}; +use crate::compiler::{VarKind, code, string_k}; +use crate::lua_value::UpvalueDesc; +use crate::lua_vm::OpCode; + +// From lopcodes.h - maximum list items per flush +const LFIELDS_PER_FLUSH: u32 = 50; + +// Port of init_exp from lparser.c +fn init_exp(e: &mut ExpDesc, kind: ExpKind, info: i32) { + e.kind = kind; + e.u = ExpUnion::Info(info); + e.t = -1; + e.f = -1; +} + +// Port of expr from lparser.c +pub fn expr(fs: &mut FuncState) -> Result { + let mut v = ExpDesc::new_void(); + subexpr(fs, &mut v, 0)?; // Discard returned operator + Ok(v) +} + +// Internal version that uses mutable reference +pub(crate) fn expr_internal(fs: &mut FuncState, v: &mut ExpDesc) -> Result<(), String> { + subexpr(fs, v, 0)?; // Discard returned operator + Ok(()) +} + +fn get_unary_opcode(op: UnaryOperator) -> OpCode { + match op { + UnaryOperator::OpBNot => OpCode::BNot, + UnaryOperator::OpNot => OpCode::Not, + UnaryOperator::OpLen => OpCode::Len, + UnaryOperator::OpUnm => OpCode::Unm, + UnaryOperator::OpNop => unreachable!("No opcode for OpNop"), + } +} + +// Port of subexpr from lparser.c +// Returns the first untreated operator (like Lua C implementation) +fn subexpr(fs: &mut FuncState, v: &mut ExpDesc, limit: i32) -> Result { + let uop = to_unary_operator(fs.lexer.current_token()); + if uop != UnaryOperator::OpNop { + let op = get_unary_opcode(uop); + fs.lexer.bump(); + let _ = subexpr(fs, v, UNARY_PRIORITY)?; // Discard returned op from recursive call + code::prefix(fs, op, v); + } else { + simpleexp(fs, v)?; + } + + // Expand while operators have priorities higher than limit + // Port of lparser.c:1273-1284 + let mut op = to_binary_operator(fs.lexer.current_token()); + while op != BinaryOperator::OpNop && op.get_priority().left > limit { + fs.lexer.bump(); + + // lcode.c:1637-1676: luaK_infix handles special cases like 'and', 'or' + code::infix(fs, op, v); + + let mut v2 = ExpDesc::new_void(); + // Recursive call returns next untreated operator (lparser.c:1283) + let nextop = subexpr(fs, &mut v2, op.get_priority().right)?; + + // lcode.c:1706-1783: luaK_posfix + // 'and' and 'or' don't generate opcodes - they use control flow + code::posfix(fs, op, v, &mut v2); + + op = nextop; // Use returned operator instead of re-checking token (lparser.c:1284) + } + + Ok(op) // Return first untreated operator (lparser.c:1286) +} + +// Port of simpleexp from lparser.c +fn simpleexp(fs: &mut FuncState, v: &mut ExpDesc) -> Result<(), String> { + match fs.lexer.current_token() { + LuaTokenKind::TkInt => { + // Parse integer literal using int_token_value + let text = fs.lexer.current_token_text(); + match parse_int_token_value(text) { + Ok(NumberResult::Int(val)) => { + *v = ExpDesc::new_int(val); + } + Ok(NumberResult::Uint(val)) => { + // Reinterpret unsigned as signed + *v = ExpDesc::new_int(val as i64); + } + Ok(NumberResult::Float(val)) => { + // Integer overflow, use float + *v = ExpDesc::new_float(val); + } + Err(e) => { + return Err(fs.syntax_error(&format!("invalid integer literal: {}", e))); + } + } + fs.lexer.bump(); + } + LuaTokenKind::TkFloat => { + // Parse float literal + let num_text = fs.lexer.current_token_text(); + match parse_float_token_value(num_text) { + Ok(val) => { + *v = ExpDesc::new_float(val); + } + Err(e) => { + return Err( + fs.syntax_error(&format!("invalid float literal '{}': {}", num_text, e)) + ); + } + } + fs.lexer.bump(); + } + LuaTokenKind::TkString | LuaTokenKind::TkLongString => { + // String constant - remove quotes + let text = fs.lexer.current_token_text(); + let string_content = parse_string_token_value(text, fs.lexer.current_token()); + match string_content { + Ok(s) => { + let idx = string_k(fs, s); + *v = ExpDesc::new_k(idx); + } + Err(e) => { + return Err(fs.syntax_error(&format!("invalid string literal: {}", e))); + } + } + fs.lexer.bump(); + } + LuaTokenKind::TkNil => { + *v = ExpDesc::new_nil(); + fs.lexer.bump(); + } + LuaTokenKind::TkTrue => { + *v = ExpDesc::new_bool(true); + fs.lexer.bump(); + } + LuaTokenKind::TkFalse => { + *v = ExpDesc::new_bool(false); + fs.lexer.bump(); + } + LuaTokenKind::TkDots => { + // lparser.c:1169-1173: vararg + // Check if inside vararg function + if !fs.is_vararg { + return Err(fs.syntax_error("cannot use '...' outside a vararg function")); + } + // lparser.c:1173: init_exp(v, VVARARG, luaK_codeABC(fs, OP_VARARG, 0, 0, 1)); + let pc = code::code_abc(fs, OpCode::Vararg, 0, 0, 1); + *v = ExpDesc::new_void(); + v.kind = ExpKind::VVARARG; + v.u = ExpUnion::Info(pc as i32); + fs.lexer.bump(); + } + LuaTokenKind::TkLeftBrace => { + // Table constructor + constructor(fs, v)?; + } + LuaTokenKind::TkFunction => { + // Anonymous function + fs.lexer.bump(); + body(fs, v, false)?; + } + _ => { + // Try suffixed expression (variables, function calls, indexing) + suffixedexp(fs, v)?; + } + } + Ok(()) +} + +// Port of primaryexp from lparser.c (lines 1080-1099) +// primaryexp -> NAME | '(' expr ')' +fn primaryexp(fs: &mut FuncState, v: &mut ExpDesc) -> Result<(), String> { + match fs.lexer.current_token() { + LuaTokenKind::TkLeftParen => { + // (expr) + fs.lexer.bump(); + expr_internal(fs, v)?; + expect(fs, LuaTokenKind::TkRightParen)?; + code::discharge_vars(fs, v); + } + LuaTokenKind::TkName => { + // Variable name + singlevar(fs, v)?; + } + _ => { + return Err(fs.token_error("unexpected symbol")); + } + } + Ok(()) +} + +// Port of suffixedexp from lparser.c (lines 1102-1136) +// suffixedexp -> primaryexp { '.' NAME | '[' exp ']' | ':' NAME funcargs | funcargs } +pub fn suffixedexp(fs: &mut FuncState, v: &mut ExpDesc) -> Result<(), String> { + primaryexp(fs, v)?; + + loop { + match fs.lexer.current_token() { + LuaTokenKind::TkDot => { + // fieldsel + fieldsel(fs, v)?; + } + LuaTokenKind::TkLeftBracket => { + // [exp] + let mut key = ExpDesc::new_void(); + code::exp2anyregup(fs, v); + yindex(fs, &mut key)?; + code::indexed(fs, v, &mut key); + } + LuaTokenKind::TkColon => { + // : NAME funcargs (method call) + fs.lexer.bump(); + let method_name = fs.lexer.current_token_text().to_string(); + expect(fs, LuaTokenKind::TkName)?; + + // self:method(...) is sugar for self.method(self, ...) + // Generate SELF instruction + // Create VKSTR expression for the method name + // Note: In official Lua, codestring sets e->u.strval = s, then exp2K + // calls stringK. But our ExpUnion doesn't have strval, so we call + // string_k directly here and create VK instead of VKSTR. + let k_idx = string_k(fs, method_name); + let mut key = ExpDesc { + kind: ExpKind::VK, + u: ExpUnion::Info(k_idx as i32), + t: -1, + f: -1, + }; + code::self_op(fs, v, &mut key); + + funcargs(fs, v)?; + } + LuaTokenKind::TkLeftParen + | LuaTokenKind::TkString + | LuaTokenKind::TkLongString + | LuaTokenKind::TkLeftBrace => { + // funcargs - must convert to register first + code::exp2nextreg(fs, v); + funcargs(fs, v)?; + } + _ => { + return Ok(()); + } + } + } +} + +// Port of funcargs from lparser.c (lines 1024-1065) +fn funcargs(fs: &mut FuncState, f: &mut ExpDesc) -> Result<(), String> { + use crate::compiler::expression::ExpKind; + + let mut args = ExpDesc::new_void(); + let line = fs.lexer.line; // Save line number before processing arguments (lparser.c:1028) + + match fs.lexer.current_token() { + LuaTokenKind::TkLeftParen => { + // funcargs -> '(' [ explist ] ')' + fs.lexer.bump(); + if fs.lexer.current_token() == LuaTokenKind::TkRightParen { + args.kind = ExpKind::VVOID; + } else { + crate::compiler::statement::explist(fs, &mut args)?; + if matches!(args.kind, ExpKind::VCALL | ExpKind::VVARARG) { + code::setmultret(fs, &mut args); + } + } + expect(fs, LuaTokenKind::TkRightParen)?; + } + LuaTokenKind::TkLeftBrace => { + // funcargs -> constructor (table constructor) + constructor(fs, &mut args)?; + } + LuaTokenKind::TkString | LuaTokenKind::TkLongString => { + // funcargs -> STRING + let text = fs.lexer.current_token_text(); + let string_content = parse_string_token_value(text, fs.lexer.current_token())?; + let k_idx = string_k(fs, string_content); + fs.lexer.bump(); + args = ExpDesc::new_k(k_idx); + } + _ => { + return Err("function arguments expected".to_string()); + } + } + + // Generate CALL instruction + if f.kind != ExpKind::VNONRELOC { + return Err("function must be in register".to_string()); + } + + let base = f.u.info() as u8; + let nparams = if matches!(args.kind, ExpKind::VCALL | ExpKind::VVARARG) { + 255 // LUA_MULTRET = -1, which is 255 in u8. nparams+1 will overflow to 0 + } else { + if args.kind != ExpKind::VVOID { + code::exp2nextreg(fs, &mut args); + } + fs.freereg - (base + 1) + }; + + let pc = code::code_abc( + fs, + OpCode::Call, + base as u32, + (nparams.wrapping_add(1)) as u32, + 2, + ); + + code::fixline(fs, line); // Fix line number for CALL instruction (lparser.c:1063) + f.kind = ExpKind::VCALL; + f.u = ExpUnion::Info(pc as i32); + fs.freereg = base + 1; // Call resets freereg to base+1 + + Ok(()) +} + +// Port of yindex from lparser.c +fn yindex(fs: &mut FuncState, v: &mut ExpDesc) -> Result<(), String> { + fs.lexer.bump(); // skip '[' + expr_internal(fs, v)?; + code::exp2val(fs, v); + expect(fs, LuaTokenKind::TkRightBracket)?; + Ok(()) +} + +// Port of singlevar from lparser.c (lines 463-474) +pub fn singlevar(fs: &mut FuncState, v: &mut ExpDesc) -> Result<(), String> { + let name = fs.lexer.current_token_text().to_string(); + fs.lexer.bump(); + + // Call singlevaraux with base=1 + singlevaraux(fs, &name, v, true); + + // If global name (VVOID), access through _ENV + if v.kind == ExpKind::VVOID { + // Get environment variable (_ENV) + singlevaraux(fs, "_ENV", v, true); + + // _ENV must exist + if v.kind == ExpKind::VVOID { + return Err("_ENV not found".to_string()); + } + + code::exp2anyregup(fs, v); + + // Key is variable name + let k_idx = string_k(fs, name); + let mut key = ExpDesc::new_k(k_idx); + + // env[varname] + code::indexed(fs, v, &mut key); + } + + Ok(()) +} + +// Port of singlevaraux from lparser.c (lines 435-456) +fn singlevaraux(fs: &mut FuncState, name: &str, var: &mut ExpDesc, base: bool) { + let vkind = fs.searchvar(name, var); + if vkind >= 0 { + if vkind == ExpKind::VLOCAL as i32 && !base { + // lparser.c:442: markupval(fs, var->u.var.vidx); /* local will be used as an upval */ + let vidx = var.u.var().vidx; + mark_upval(fs, vidx as u8); + } + // If it's VCONST, it stays VCONST - no change needed + } else { + let vidx = fs.searchupvalue(name); + if vidx < 0 { + if let Some(prev) = &mut fs.prev { + singlevaraux(prev, name, var, false); + + // Port of lparser.c:451-453: don't create upvalue for compile-time constants + // If the variable is a compile-time constant (VCONST), convert it to value immediately + // This enables constant folding in nested functions + if var.kind == ExpKind::VCONST { + // Convert VCONST to actual constant value (VKINT/VKFLT/etc) + // Port of const2exp from lcode.c:693-720 + let vidx = var.u.info() as usize; + if let Some(prev_var) = prev.actvar.get(vidx) { + if let Some(value) = prev_var.const_value { + code::const_to_exp(value, var); + } + } + } else if var.kind == ExpKind::VLOCAL || var.kind == ExpKind::VUPVAL { + let idx = fs.newupvalue(name, var) as u8; + init_exp(var, ExpKind::VUPVAL, idx as i32); + } + } else { + init_exp(var, ExpKind::VVOID, 0); + } + } else { + init_exp(var, ExpKind::VUPVAL, vidx as i32); + } + } +} + +// Port of fieldsel from lparser.c:811-819 +// fieldsel -> ['.' | ':'] NAME +pub fn fieldsel(fs: &mut FuncState, v: &mut ExpDesc) -> Result<(), String> { + // lparser.c:815: luaK_exp2anyregup(fs, v); + code::exp2anyregup(fs, v); + + // lparser.c:816: luaX_next(ls); /* skip the dot or colon */ + fs.lexer.bump(); + + // lparser.c:817: codename(ls, &key); + if fs.lexer.current_token() != LuaTokenKind::TkName { + return Err(fs.token_error("expected field name")); + } + + let field = fs.lexer.current_token_text().to_string(); + fs.lexer.bump(); + + // lparser.c:818: luaK_indexed(fs, v, &key); + // Create a string constant key + let idx = string_k(fs, field); + let mut key = ExpDesc::new_void(); + key.kind = ExpKind::VK; + key.u = ExpUnion::Info(idx as i32); + + // Call indexed to determine correct index type (VINDEXSTR vs VINDEXED) + code::indexed(fs, v, &mut key); + + Ok(()) +} + +// Port of ConsControl from lparser.c +struct ConsControl { + v: ExpDesc, // last list item read + table_reg: u8, // table register + na: u32, // number of array elements already stored + nh: u32, // total number of record elements + tostore: u32, // number of array elements pending to be stored +} + +impl ConsControl { + fn new(table_reg: u8) -> Self { + Self { + v: ExpDesc::new_void(), + table_reg, + na: 0, + nh: 0, + tostore: 0, + } + } +} + +// Port of closelistfield from lparser.c +fn closelistfield(fs: &mut FuncState, cc: &mut ConsControl) { + if cc.v.kind == ExpKind::VVOID { + return; // there is no list item + } + code::exp2nextreg(fs, &mut cc.v); + cc.v.kind = ExpKind::VVOID; + if cc.tostore == LFIELDS_PER_FLUSH { + code::setlist(fs, cc.table_reg, cc.na, cc.tostore); // flush + cc.na += cc.tostore; + cc.tostore = 0; // no more items pending + } +} + +// Port of lastlistfield from lparser.c +fn lastlistfield(fs: &mut FuncState, cc: &mut ConsControl) { + if cc.tostore == 0 { + return; + } + if code::hasmultret(&cc.v) { + code::setmultret(fs, &mut cc.v); + code::setlist(fs, cc.table_reg, cc.na, code::LUA_MULTRET); + // lparser.c:884: cc->na--; do not count last expression (unknown number of elements) + // Note: na should be > 0 here, but use saturating_sub to avoid panic + cc.na = cc.na.saturating_sub(1); + } else { + if cc.v.kind != ExpKind::VVOID { + code::exp2nextreg(fs, &mut cc.v); + } + code::setlist(fs, cc.table_reg, cc.na, cc.tostore); + } + cc.na += cc.tostore; +} + +// Port of listfield from lparser.c +fn listfield(fs: &mut FuncState, cc: &mut ConsControl) -> Result<(), String> { + expr_internal(fs, &mut cc.v)?; + cc.tostore += 1; + Ok(()) +} + +// Port of field from lparser.c +fn field(fs: &mut FuncState, cc: &mut ConsControl) -> Result<(), String> { + if fs.lexer.current_token() == LuaTokenKind::TkLeftBracket { + // [exp] = exp (general field) + // Port of recfield from lparser.c:917-935 + // Save freereg to restore after processing field + let saved_freereg = fs.freereg; + + fs.lexer.bump(); + let mut key = ExpDesc::new_void(); + expr_internal(fs, &mut key)?; + expect(fs, LuaTokenKind::TkRightBracket)?; + expect(fs, LuaTokenKind::TkAssign)?; + + let mut val = ExpDesc::new_void(); + expr_internal(fs, &mut val)?; + + // Port of recfield logic from lparser.c:847-867 + // Use indexed to determine VINDEXSTR vs VINDEXED based on key + let mut tab = ExpDesc::new_void(); + tab.kind = ExpKind::VNONRELOC; + tab.u = ExpUnion::Info(cc.table_reg as i32); + + code::indexed(fs, &mut tab, &mut key); + + // Generate appropriate store instruction based on tab.kind + match tab.kind { + ExpKind::VINDEXSTR => { + // String key with index <= 255, use SETFIELD + code::code_abrk( + fs, + OpCode::SetField, + tab.u.ind().t as u32, + tab.u.ind().idx as u32, + &mut val, + ); + } + ExpKind::VINDEXI => { + // Integer key in range 0-255, use SETI + code::code_abrk( + fs, + OpCode::SetI, + tab.u.ind().t as u32, + tab.u.ind().idx as u32, + &mut val, + ); + } + ExpKind::VINDEXED => { + // General case (string index > 255 or non-constant key), use SETTABLE + code::code_abrk( + fs, + OpCode::SetTable, + tab.u.ind().t as u32, + tab.u.ind().idx as u32, + &mut val, + ); + } + _ => { + panic!("Unexpected expression kind in table constructor [key]=value"); + } + } + cc.nh += 1; + + // Restore freereg - free temporary registers used for key/value + fs.freereg = saved_freereg; + } else if fs.lexer.current_token() == LuaTokenKind::TkName { + // Check if it's name = exp (record field) or just a list item + let next = fs.lexer.peek_next_token(); + if next == LuaTokenKind::TkAssign { + // name = exp (record field) + // Port of recfield from lparser.c:847-867 + let saved_freereg = fs.freereg; + + let field_name = fs.lexer.current_token_text().to_string(); + fs.lexer.bump(); + fs.lexer.bump(); // skip = + + // Create key expression (string constant) + let field_idx = string_k(fs, field_name); + let mut key = ExpDesc::new_void(); + key.kind = ExpKind::VK; + + key.u = ExpUnion::Info(field_idx as i32); + + // Create table expression + let mut tab = ExpDesc::new_void(); + tab.kind = ExpKind::VNONRELOC; + + tab.u = ExpUnion::Info(cc.table_reg as i32); + + // indexed will handle VINDEXSTR vs VINDEXED based on constant index size + // If field_idx > 255, it will set tab.kind = VINDEXED + // Otherwise, it will set tab.kind = VINDEXSTR + code::indexed(fs, &mut tab, &mut key); + + // Parse value expression + let mut val = ExpDesc::new_void(); + expr_internal(fs, &mut val)?; + + // Generate the appropriate store instruction based on tab.kind + match tab.kind { + ExpKind::VINDEXSTR => { + // Field index fits in B operand, use SETFIELD + code::code_abrk( + fs, + OpCode::SetField, + tab.u.ind().t as u32, + tab.u.ind().idx as u32, + &mut val, + ); + } + ExpKind::VINDEXED => { + // Field index too large, use SETTABLE + code::code_abrk( + fs, + OpCode::SetTable, + tab.u.ind().t as u32, + tab.u.ind().idx as u32, + &mut val, + ); + } + _ => { + // Should not happen in table constructor + panic!("Unexpected expression kind in table constructor"); + } + } + + cc.nh += 1; + fs.freereg = saved_freereg; + } else { + // Just a list item + listfield(fs, cc)?; + } + } else { + // List item + listfield(fs, cc)?; + } + + Ok(()) +} + +// Port of constructor from lparser.c +fn constructor(fs: &mut FuncState, v: &mut ExpDesc) -> Result<(), String> { + expect(fs, LuaTokenKind::TkLeftBrace)?; + + let table_reg = fs.freereg; + code::reserve_regs(fs, 1); + let pc = code::code_abc(fs, OpCode::NewTable, table_reg as u32, 0, 0); + code::code_extraarg(fs, 0); // space for extra arg + *v = ExpDesc::new_nonreloc(table_reg); + + let mut cc = ConsControl::new(table_reg); + + // Parse table fields + loop { + if fs.lexer.current_token() == LuaTokenKind::TkRightBrace { + break; + } + closelistfield(fs, &mut cc); + field(fs, &mut cc)?; + + if !matches!( + fs.lexer.current_token(), + LuaTokenKind::TkComma | LuaTokenKind::TkSemicolon + ) { + break; + } + fs.lexer.bump(); + } + + expect(fs, LuaTokenKind::TkRightBrace)?; + lastlistfield(fs, &mut cc); + code::settablesize(fs, pc, table_reg, cc.na, cc.nh); + Ok(()) +} + +// Port of body from lparser.c +pub fn body(fs: &mut FuncState, v: &mut ExpDesc, is_method: bool) -> Result<(), String> { + // Record the line where function is defined + let linedefined = fs.lexer.line; + + expect(fs, LuaTokenKind::TkLeftParen)?; + + // Determine if vararg before creating child + let mut is_vararg = false; + let mut params = Vec::new(); + + // Collect parameter names first + if is_method { + params.push("self".to_string()); + } + + if fs.lexer.current_token() != LuaTokenKind::TkRightParen { + loop { + if fs.lexer.current_token() == LuaTokenKind::TkName { + let param_name = fs.lexer.current_token_text().to_string(); + fs.lexer.bump(); + params.push(param_name); + } else if fs.lexer.current_token() == LuaTokenKind::TkDots { + fs.lexer.bump(); + is_vararg = true; + break; + } else { + return Err("expected parameter".to_string()); + } + + if fs.lexer.current_token() != LuaTokenKind::TkComma { + break; + } + fs.lexer.bump(); + } + } + + expect(fs, LuaTokenKind::TkRightParen)?; + + // Port of body function from lparser.c:989-1008 + // body -> '(' parlist ')' block END + + // lparser.c:993: Create new FuncState for nested function + // FuncState new_fs; new_fs.f = addprototype(ls); open_func(ls, &new_fs, &bl); + let fs_ptr = fs as *mut FuncState; + let mut child_fs = unsafe { FuncState::new_child(&mut *fs_ptr, is_vararg) }; + + // lparser.c:994: If vararg, generate VARARGPREP instruction + // A parameter should be the number of fixed parameters (lparser.c:954) + if is_vararg { + let nparams = params.len() as u32; + code::code_abc(&mut child_fs, OpCode::VarargPrep, nparams, 0, 0); + } + + // lparser.c:996-999: Register parameters as local variables + // parlist(ls); adjustlocalvars(ls, nparams); + for param in params { + child_fs.new_localvar(param, VarKind::VDKREG); + } + child_fs.adjust_local_vars(child_fs.actvar.len() as u8); + + // lparser.c:982: Set numparams after adjustlocalvars + // f->numparams = cast_byte(fs->nactvar); + let param_count = child_fs.nactvar as usize; + + // lparser.c:1001: luaK_reserveregs(fs, fs->nactvar); + // Reserve registers for parameters + let nactvar = child_fs.nactvar; + code::reserve_regs(&mut child_fs, nactvar as u8); + + // lparser.c:753: open_func calls enterblock(fs, bl, 0) - create function body block + // This is critical - every function body needs an outer block! + let func_bl_id = child_fs.compiler_state.alloc_blockcnt(BlockCnt { + previous: None, + first_label: 0, + first_goto: 0, + nactvar: child_fs.nactvar, + upval: false, + is_loop: false, + in_scope: true, + }); + statement::enterblock(&mut child_fs, func_bl_id, false); + + // lparser.c:1002: Parse function body statements + // statlist(ls); + statement::statlist(&mut child_fs)?; + + // Record the line where function ends (before consuming END token) + let lastlinedefined = child_fs.lexer.line; + + // lparser.c:1004: Expect END token + expect(&mut child_fs, LuaTokenKind::TkEnd)?; + + // Generate final RETURN instruction + // Port of lparser.c:765: luaK_ret(fs, luaY_nvarstack(fs), 0); + let first_reg = child_fs.nvarstack(); + code::ret(&mut child_fs, first_reg, 0); + + // lparser.c:760: close_func calls leaveblock(fs) - close function body block + statement::leaveblock(&mut child_fs); + + // Port of close_func from lparser.c:763 - finish code generation + code::finish(&mut child_fs); + + // Get completed child chunk and upvalue information + let mut child_chunk = child_fs.chunk; + child_chunk.is_vararg = child_fs.is_vararg; // Set vararg flag on chunk + // param_count excludes ... (vararg), only counts regular parameters + child_chunk.param_count = param_count; + child_chunk.linedefined = linedefined; + child_chunk.lastlinedefined = lastlinedefined; + child_chunk.source_name = Some(child_fs.source_name.clone()); + let child_upvalues = child_fs.upvalues; + + // Port of lparser.c:722-726 (codeclosure) + // In Lua 5.4, upvalue information is stored in Proto.upvalues[], NOT as pseudo-instructions + // This is different from Lua 5.1 which used pseudo-instructions after OP_CLOSURE + for upval in &child_upvalues { + child_chunk.upvalue_descs.push(UpvalueDesc { + is_local: upval.in_stack, // true if captures parent local + index: upval.idx as u32, // index in parent's register or upvalue array + }); + } + child_chunk.upvalue_count = child_upvalues.len(); + + // lparser.c:1005: Add child proto to parent (addprototype) + let proto_idx = fs.chunk.child_protos.len(); + fs.chunk.child_protos.push(std::rc::Rc::new(child_chunk)); + + // lparser.c:722-726: Generate CLOSURE instruction (codeclosure) + // static void codeclosure (LexState *ls, expdesc *v) { + // FuncState *fs = ls->fs->prev; + // init_exp(v, VRELOC, luaK_codeABx(fs, OP_CLOSURE, 0, fs->np - 1)); + // luaK_exp2nextreg(fs, v); /* fix it at the last register */ + // } + let pc = code::code_abx(fs, OpCode::Closure, 0, proto_idx as u32); + *v = ExpDesc::new_reloc(pc); + code::exp2nextreg(fs, v); + Ok(()) +} + +// Helper: expect a token +fn expect(fs: &mut FuncState, tk: LuaTokenKind) -> Result<(), String> { + if fs.lexer.current_token() == tk { + fs.lexer.bump(); + Ok(()) + } else { + Err(fs.token_error(&format!("expected '{:?}'", tk))) + } +} + diff --git a/crates/luars/src/compiler/expression.rs b/crates/luars/src/compiler/expression.rs new file mode 100644 index 0000000..8c4655d --- /dev/null +++ b/crates/luars/src/compiler/expression.rs @@ -0,0 +1,257 @@ +// Port of expdesc from lcode.h +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExpKind { + VVOID, // when 'expdesc' describes the last expression of a list, this kind means an empty list + VNIL, // constant nil + VTRUE, // constant true + VFALSE, // constant false + VK, // constant in 'k'; info = index of constant in 'k' + VKFLT, // floating constant; nval = numerical float value + VKINT, // integer constant; ival = numerical integer value + VKSTR, // string constant; strval = TString address + VNONRELOC, // expression has its value in a fixed register; info = result register + VLOCAL, // local variable; var.ridx = register index; var.vidx = relative index in 'actvar.arr' + VUPVAL, // upvalue variable; info = index of upvalue in 'upvalues' + VCONST, // compile-time variable; info = absolute index in 'actvar.arr' + VINDEXED, // indexed variable; ind.t = table register; ind.idx = key's R index + VINDEXUP, // indexed upvalue; ind.t = upvalue; ind.idx = key's K index + VINDEXI, // indexed variable with constant integer; ind.t = table register; ind.idx = key's value + VINDEXSTR, // indexed variable with literal string; ind.t = table register; ind.idx = key's K index + VJMP, // expression is a test/comparison; info = pc of corresponding jump instruction + VRELOC, // expression can put result in any register; info = instruction pc + VCALL, // expression is a function call; info = instruction pc + VVARARG, // vararg expression; info = instruction pc +} + +#[derive(Clone)] +pub struct ExpDesc { + pub kind: ExpKind, + pub u: ExpUnion, + pub t: isize, // patch list of 'exit when true' + pub f: isize, // patch list of 'exit when false' +} + +#[derive(Clone, Copy)] +pub enum ExpUnion { + // for generic use + Info(i32), + // for VKINT + IVal(i64), + // for VKFLT + NVal(f64), + // for indexed variables + Ind(IndVars), + // for local/upvalue variables + Var(VarVals), +} + +impl ExpUnion { + pub fn info(&self) -> i32 { + match self { + ExpUnion::Info(info) => *info, + _ => panic!("ExpUnion does not contain info"), + } + } + + pub fn ival(&self) -> i64 { + match self { + ExpUnion::IVal(ival) => *ival, + _ => panic!("ExpUnion does not contain ival"), + } + } + + pub fn nval(&self) -> f64 { + match self { + ExpUnion::NVal(nval) => *nval, + _ => panic!("ExpUnion does not contain nval"), + } + } + + pub fn ind(&self) -> IndVars { + match self { + ExpUnion::Ind(ind) => *ind, + _ => panic!("ExpUnion does not contain ind"), + } + } + + pub fn ind_mut(&mut self) -> &mut IndVars { + match self { + ExpUnion::Ind(ind) => ind, + _ => { + *self = ExpUnion::Ind(IndVars { t: -1, idx: -1 }); + if let ExpUnion::Ind(ind) = self { + ind + } else { + unreachable!() + } + } + } + } + + pub fn var(&self) -> VarVals { + match self { + ExpUnion::Var(var) => *var, + _ => panic!("ExpUnion does not contain var"), + } + } +} + +#[derive(Clone, Copy)] +pub struct IndVars { + pub t: i16, // table (register or upvalue) + pub idx: i16, // index (register or constant) +} + +#[derive(Clone, Copy)] +pub struct VarVals { + pub ridx: i16, // register holding the variable + pub vidx: u16, // compiler index (in 'actvar.arr' or 'upvalues') +} + +impl ExpDesc { + pub fn new_void() -> Self { + ExpDesc { + kind: ExpKind::VVOID, + u: ExpUnion::Info(0), + t: -1, + f: -1, + } + } + + pub fn new_nil() -> Self { + ExpDesc { + kind: ExpKind::VNIL, + u: ExpUnion::Info(0), + t: -1, + f: -1, + } + } + + pub fn new_int(val: i64) -> Self { + ExpDesc { + kind: ExpKind::VKINT, + u: ExpUnion::IVal(val), + t: -1, + f: -1, + } + } + + pub fn new_float(val: f64) -> Self { + ExpDesc { + kind: ExpKind::VKFLT, + u: ExpUnion::NVal(val), + t: -1, + f: -1, + } + } + + pub fn new_bool(val: bool) -> Self { + ExpDesc { + kind: if val { ExpKind::VTRUE } else { ExpKind::VFALSE }, + u: ExpUnion::Info(0), + t: -1, + f: -1, + } + } + + pub fn new_k(info: usize) -> Self { + ExpDesc { + kind: ExpKind::VK, + u: ExpUnion::Info(info as i32), + t: -1, + f: -1, + } + } + + pub fn new_vkstr(string_id: usize) -> Self { + ExpDesc { + kind: ExpKind::VKSTR, + u: ExpUnion::Info(string_id as i32), + t: -1, + f: -1, + } + } + + pub fn new_nonreloc(reg: u8) -> Self { + ExpDesc { + kind: ExpKind::VNONRELOC, + u: ExpUnion::Info(reg as i32), + t: -1, + f: -1, + } + } + + pub fn new_local(ridx: u8, vidx: u16) -> Self { + ExpDesc { + kind: ExpKind::VLOCAL, + u: ExpUnion::Var(VarVals { + ridx: ridx as i16, + vidx, + }), + t: -1, + f: -1, + } + } + + pub fn new_upval(idx: u8) -> Self { + ExpDesc { + kind: ExpKind::VUPVAL, + u: ExpUnion::Info(idx as i32), + t: -1, + f: -1, + } + } + + pub fn new_indexed(t: u8, idx: u8) -> Self { + ExpDesc { + kind: ExpKind::VINDEXED, + u: ExpUnion::Ind(IndVars { + t: t as i16, + idx: idx as i16, + }), + t: -1, + f: -1, + } + } + + pub fn new_reloc(pc: usize) -> Self { + ExpDesc { + kind: ExpKind::VRELOC, + u: ExpUnion::Info(pc as i32), + t: -1, + f: -1, + } + } + + pub fn new_call(pc: usize) -> Self { + ExpDesc { + kind: ExpKind::VCALL, + u: ExpUnion::Info(pc as i32), + t: -1, + f: -1, + } + } + + pub fn has_jumps(&self) -> bool { + // Port of hasjumps macro from lcode.c:58 + // #define hasjumps(e) ((e)->t != (e)->f) + self.t != self.f + } + + pub fn is_const(&self) -> bool { + matches!( + self.kind, + ExpKind::VNIL + | ExpKind::VTRUE + | ExpKind::VFALSE + | ExpKind::VK + | ExpKind::VKFLT + | ExpKind::VKINT + | ExpKind::VKSTR + ) + } + + pub fn is_numeral(&self) -> bool { + matches!(self.kind, ExpKind::VKINT | ExpKind::VKFLT) && !self.has_jumps() + } +} diff --git a/crates/luars/src/compiler/func_state.rs b/crates/luars/src/compiler/func_state.rs new file mode 100644 index 0000000..68144fb --- /dev/null +++ b/crates/luars/src/compiler/func_state.rs @@ -0,0 +1,379 @@ +use std::collections::HashMap; + +use crate::Chunk; +use crate::compiler::{ExpDesc, ExpKind, ExpUnion}; +// Port of FuncState and related structures from lparser.h +use crate::gc::ObjectPool; +use crate::{LuaValue, compiler::parser::LuaLexer}; + +// Upvalue descriptor +#[derive(Clone)] +pub struct Upvaldesc { + pub name: String, // upvalue name + pub in_stack: bool, // whether it is in stack (register) + pub idx: u8, // index of upvalue (in stack or in outer function's list) + pub kind: VarKind, // kind of variable +} + +// Port of FuncState from lparser.h +pub struct FuncState<'a> { + pub chunk: Chunk, + pub prev: Option<&'a mut FuncState<'a>>, // parent function state + pub lexer: &'a mut LuaLexer<'a>, + pub pool: &'a mut ObjectPool, + pub compiler_state: &'a mut CompilerState, + pub block_cnt_id: Option, + pub pc: usize, // next position to code (equivalent to pc) + pub last_target: usize, // label of last 'jump label' + pub pending_gotos: Vec, // list of pending gotos + pub labels: Vec, // list of active labels + pub actvar: Vec, // list of all variable descriptors (active and pending) + pub nactvar: u8, // number of active variables (actvar[0..nactvar] are active) + pub upvalues: Vec, // upvalue descriptors + pub nups: u8, // number of upvalues + pub freereg: u8, // first free register + pub iwthabs: u8, // instructions issued since last absolute line info + pub needclose: bool, // true if function needs to close upvalues when returning + pub is_vararg: bool, // true if function is vararg + pub first_local: usize, // index of first local variable in prev + pub source_name: String, // source file name for error messages + pub chunk_constants_map: HashMap, // constant to index mapping for chunk +} + +pub struct CompilerState { + // pool of BlockCnt structures (Option to allow safe take without invalidating indices) + pub block_cnt_pool: Vec>, + // pool of LhsAssign structures for assignment chain management + pub lhs_assign_pool: Vec>, + // Global scanner table for constant deduplication (corresponds to LexState.h in Lua C) + pub scanner_table: HashMap, +} + +impl CompilerState { + pub fn new() -> Self { + CompilerState { + block_cnt_pool: Vec::new(), + lhs_assign_pool: Vec::new(), + scanner_table: HashMap::new(), + } + } + + pub fn alloc_blockcnt(&mut self, block: BlockCnt) -> BlockCntId { + let id = BlockCntId(self.block_cnt_pool.len()); + self.block_cnt_pool.push(Some(block)); + id + } + + pub fn get_blockcnt_mut(&mut self, id: BlockCntId) -> Option<&mut BlockCnt> { + self.block_cnt_pool + .get_mut(id.0) + .and_then(|opt| opt.as_mut()) + } + + pub fn take_blockcnt(&mut self, id: BlockCntId) -> Option { + // Use take() instead of remove() to avoid invalidating subsequent BlockCntIds + self.block_cnt_pool.get_mut(id.0).and_then(|opt| opt.take()) + } + + pub fn alloc_lhs_assign(&mut self, lhs: LhsAssign) -> LhsAssignId { + let id = LhsAssignId(self.lhs_assign_pool.len()); + self.lhs_assign_pool.push(Some(lhs)); + id + } + + pub fn get_lhs_assign(&self, id: LhsAssignId) -> Option<&LhsAssign> { + self.lhs_assign_pool.get(id.0).and_then(|opt| opt.as_ref()) + } + + pub fn get_lhs_assign_mut(&mut self, id: LhsAssignId) -> Option<&mut LhsAssign> { + self.lhs_assign_pool + .get_mut(id.0) + .and_then(|opt| opt.as_mut()) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub struct BlockCntId(pub usize); + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub struct LhsAssignId(pub usize); + +// Port of LHS_assign from lparser.c +#[derive(Clone)] +pub struct LhsAssign { + pub prev: Option, + pub v: ExpDesc, +} + +// Port of BlockCnt from lparser.c +#[derive(Clone, Default)] +pub struct BlockCnt { + pub previous: Option, // link to the enclosing block + pub first_label: usize, // index of first label in this block + pub first_goto: usize, // index of first pending goto in this block + pub nactvar: u8, // number of active variables outside the block + pub upval: bool, // true if some variable in block is an upvalue + pub is_loop: bool, // true if 'block' is a loop + pub in_scope: bool, // true if 'block' is still in scope +} + +// Port of LabelDesc from lparser.c +#[derive(Clone)] +pub struct LabelDesc { + pub name: String, + pub pc: usize, + pub line: usize, + pub nactvar: u8, + pub stklevel: u8, // NEW: saved stack level at goto/label creation + pub close: bool, +} + +// Port of Dyndata from lparser.c +pub struct Dyndata { + pub actvar: Vec, // list of active local variables + pub gt: Vec, // pending gotos + pub label: Vec, // list of active labels +} + +// Port of Vardesc from lparser.c +// Variable kinds +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VarKind { + VDKREG = 0, // regular variable + RDKCONST = 1, // constant variable + RDKTOCLOSE = 2, // to-be-closed variable + RDKCTC = 3, // compile-time constant +} + +pub struct VarDesc { + pub name: String, + pub kind: VarKind, // variable kind + pub ridx: i16, // register holding the variable + pub vidx: u16, // compiler index + pub const_value: Option, // constant value for compile-time constants +} + +impl<'a> FuncState<'a> { + pub fn new( + lexer: &'a mut LuaLexer<'a>, + pool: &'a mut ObjectPool, + compiler_state: &'a mut CompilerState, + is_vararg: bool, + source_name: String, + ) -> Self { + FuncState { + chunk: Chunk::new(), + prev: None, + lexer, + pool, + compiler_state, + block_cnt_id: None, + pc: 0, + last_target: 0, + pending_gotos: Vec::new(), + labels: Vec::new(), + actvar: Vec::new(), + nactvar: 0, + upvalues: Vec::new(), + nups: 0, + freereg: 0, + iwthabs: 0, + needclose: false, + is_vararg, + source_name, + first_local: 0, + chunk_constants_map: HashMap::new(), + } + } + + // Unified error generation function (port of luaX_syntaxerror from llex.c) + pub fn syntax_error(&self, msg: &str) -> String { + let line = self.lexer.line; + format!("{}:{}: {}", self.source_name, line, msg) + } + + // Generate error with current token information + pub fn token_error(&self, msg: &str) -> String { + let token_text = self.lexer.current_token_text(); + let line = self.lexer.line; + format!( + "{}:{}: {} near '{}'", + self.source_name, line, msg, token_text + ) + } + + pub fn current_block_cnt(&mut self) -> Option<&mut BlockCnt> { + if let Some(bl_id) = &self.block_cnt_id { + self.compiler_state.get_blockcnt_mut(*bl_id) + } else { + None + } + } + + pub fn take_block_cnt(&mut self) -> Option { + if let Some(bl_id) = self.block_cnt_id.take() { + self.compiler_state.take_blockcnt(bl_id) + } else { + None + } + } + + // Create child function state + pub fn new_child(parent: &'a mut FuncState<'a>, is_vararg: bool) -> Self { + FuncState { + chunk: Chunk::new(), + prev: Some(unsafe { &mut *(parent as *mut FuncState<'a>) }), + lexer: parent.lexer, + pool: parent.pool, + compiler_state: parent.compiler_state, + block_cnt_id: None, + pc: 0, + last_target: 0, + pending_gotos: Vec::new(), + labels: Vec::new(), + actvar: Vec::new(), + nactvar: 0, + upvalues: Vec::new(), + nups: 0, + freereg: 0, + iwthabs: 0, + needclose: false, + is_vararg, + first_local: parent.actvar.len(), + source_name: parent.source_name.clone(), + chunk_constants_map: HashMap::new(), + } + } + + // Port of new_localvar from lparser.c + pub fn new_localvar(&mut self, name: String, kind: VarKind) -> u16 { + let vidx = self.actvar.len() as u16; + self.actvar.push(VarDesc { + name, + kind, + ridx: self.freereg as i16, + vidx, + const_value: None, // Initially no const value + }); + vidx + } + + // Get variable descriptor + pub fn get_local_var_desc(&mut self, vidx: u16) -> Option<&mut VarDesc> { + self.actvar.get_mut(vidx as usize) + } + + // Port of adjustlocalvars from lparser.c:311-321 + pub fn adjust_local_vars(&mut self, nvars: u8) { + // Variables have already been added to actvar by new_localvar + // This function assigns register indices to them and marks them as active + let mut reglevel = self.reglevel(self.nactvar); + for _ in 0..nvars { + let vidx = self.nactvar; + if let Some(var) = self.actvar.get_mut(vidx as usize) { + var.ridx = reglevel as i16; + reglevel += 1; + // Add variable name to chunk's locals for debugging + self.chunk.locals.push(var.name.clone()); + } + self.nactvar += 1; + } + } + + // Port of reglevel from lparser.c:229-237 + // Returns the register level for variables outside the block + pub fn reglevel(&self, nvar: u8) -> u8 { + let mut n = nvar as i32 - 1; + while n >= 0 { + if let Some(vd) = self.actvar.get(n as usize) { + if vd.kind != VarKind::RDKCTC { + return (vd.ridx + 1) as u8; + } + } + n -= 1; + } + 0 // no variables in registers + } + + // Port of luaY_nvarstack from lparser.c:332-334 + // Returns the number of registers used by active variables + pub fn nvarstack(&self) -> u8 { + self.reglevel(self.nactvar) + } + + // Port of removevars from lparser.c + pub fn remove_vars(&mut self, tolevel: u8) { + while self.nactvar > tolevel { + self.nactvar -= 1; + } + // Also truncate actvar Vec to remove pending variables that were never activated + self.actvar.truncate(self.nactvar as usize); + } + + // Port of searchvar from lparser.c (lines 390-404) + pub fn searchvar(&self, name: &str, var: &mut ExpDesc) -> i32 { + for i in (0..self.nactvar as usize).rev() { + if let Some(vd) = self.actvar.get(i) { + if vd.name == name { + if vd.kind == VarKind::RDKCTC { + // VCONST: store variable index in u.info for check_readonly + *var = ExpDesc::new_void(); + var.kind = ExpKind::VCONST; + var.u = ExpUnion::Info(i as i32); + return ExpKind::VCONST as i32; + } else { + // Get register index from variable descriptor + let ridx = vd.ridx as u8; + *var = ExpDesc::new_local(ridx, i as u16); + return ExpKind::VLOCAL as i32; + } + } + } + } + -1 + } + + // Port of searchupvalue from lparser.c (lines 340-351) + pub fn searchupvalue(&self, name: &str) -> i32 { + for i in 0..self.nups as usize { + if self.upvalues[i].name == name { + return i as i32; + } + } + -1 + } + + // Port of newupvalue from lparser.c (lines 364-382) + pub fn newupvalue(&mut self, name: &str, v: &ExpDesc) -> i32 { + let prev_ptr = match &self.prev { + Some(p) => *p as *const _ as *mut FuncState, + None => std::ptr::null_mut(), + }; + + let (in_stack, idx, kind) = unsafe { + if v.kind == ExpKind::VLOCAL { + let vidx = v.u.var().vidx; + let ridx = v.u.var().ridx; + let prev = &*prev_ptr; + let vd = &prev.actvar[vidx as usize]; + (true, ridx as u8, vd.kind) + } else { + let info = v.u.info() as usize; + let prev = &*prev_ptr; + let up = &prev.upvalues[info]; + (false, info as u8, up.kind) + } + }; + + self.upvalues.push(Upvaldesc { + name: name.to_string(), + in_stack, + idx, + kind, + }); + self.chunk.upvalue_count = self.upvalues.len(); + self.nups = self.upvalues.len() as u8; + + (self.nups - 1) as i32 + } +} diff --git a/crates/luars/src/compiler/helpers.rs b/crates/luars/src/compiler/helpers.rs deleted file mode 100644 index cd8c935..0000000 --- a/crates/luars/src/compiler/helpers.rs +++ /dev/null @@ -1,790 +0,0 @@ -// Compiler helper functions - -use emmylua_parser::{LuaExpr, LuaLiteralToken}; - -use super::{Compiler, Local, ScopeChain}; -use crate::lua_value::LuaValue; -use crate::lua_vm::{Instruction, OpCode}; -use std::cell::RefCell; -use std::rc::Rc; - -/// Create a string value using VM's string pool -pub fn create_string_value(c: &mut Compiler, s: &str) -> LuaValue { - unsafe { (*c.vm_ptr).create_string(s) } -} - -/// Emit an instruction and return its position -/// Automatically fills line_info with last_line -pub fn emit(c: &mut Compiler, instr: u32) -> usize { - c.chunk.code.push(instr); - // Fill line_info - use last_line or 0 if not set - c.chunk.line_info.push(c.last_line); - c.chunk.code.len() - 1 -} - -/// Emit a jump instruction and return its position for later patching -pub fn emit_jump(c: &mut Compiler, opcode: OpCode) -> usize { - // JMP uses sJ format, not AsBx - emit(c, Instruction::create_sj(opcode, 0)) -} - -/// Patch a jump instruction at the given position -pub fn patch_jump(c: &mut Compiler, pos: usize) { - let jump = (c.chunk.code.len() - pos - 1) as i32; - // JMP uses sJ format - c.chunk.code[pos] = Instruction::create_sj(OpCode::Jmp, jump); -} - -/// Add a constant to the constant pool (without deduplication) -pub fn add_constant(c: &mut Compiler, value: LuaValue) -> u32 { - c.chunk.constants.push(value); - (c.chunk.constants.len() - 1) as u32 -} - -/// Add a constant with deduplication (Lua 5.4 style) -/// Returns the index in the constant table -pub fn add_constant_dedup(c: &mut Compiler, value: LuaValue) -> u32 { - // Search for existing constant - for (i, existing) in c.chunk.constants.iter().enumerate() { - if values_equal(existing, &value) { - return i as u32; - } - } - // Not found, add new constant - add_constant(c, value) -} - -/// Helper: check if two LuaValues are equal for constant deduplication -fn values_equal(a: &LuaValue, b: &LuaValue) -> bool { - // Use LuaValue's type checking methods - if a.is_nil() && b.is_nil() { - return true; - } - if let (Some(a_bool), Some(b_bool)) = (a.as_bool(), b.as_bool()) { - return a_bool == b_bool; - } - if let (Some(a_int), Some(b_int)) = (a.as_integer(), b.as_integer()) { - return a_int == b_int; - } - if let (Some(a_num), Some(b_num)) = (a.as_float(), b.as_float()) { - return a_num == b_num; - } - // For strings, compare raw primary/secondary words to avoid unsafe - if a.is_string() && b.is_string() { - return a.primary == b.primary && a.secondary == b.secondary; - } - false -} - -/// Try to add a constant to K table (for RK instructions) -/// Returns Some(k_index) if successful, None if too many constants -/// In Lua 5.4, K indices are limited to MAXARG_B (255) -pub fn try_add_constant_k(c: &mut Compiler, value: LuaValue) -> Option { - let idx = add_constant_dedup(c, value); - if idx <= Instruction::MAX_B { - Some(idx) - } else { - None // Too many constants, must use register - } -} - -/// Allocate a new register -/// Lua equivalent: luaK_reserveregs(fs, 1) -#[track_caller] -pub fn alloc_register(c: &mut Compiler) -> u32 { - let reg = c.freereg; - c.freereg += 1; - // Track peak freereg for max_stack_size - if c.freereg > c.peak_freereg { - c.peak_freereg = c.freereg; - } - if c.freereg as usize > c.chunk.max_stack_size { - c.chunk.max_stack_size = c.freereg as usize; - } - reg -} - -/// Reserve N consecutive registers -/// Lua equivalent: luaK_reserveregs(fs, n) -#[allow(dead_code)] -pub fn reserve_registers(c: &mut Compiler, n: u32) { - c.freereg += n; - if c.freereg as usize > c.chunk.max_stack_size { - c.chunk.max_stack_size = c.freereg as usize; - } -} - -/// Ensure a specific register is available (update max_stack_size if needed) -/// This should be called when using a register directly without alloc_register -pub fn ensure_register(c: &mut Compiler, reg: u32) { - let min_stack = (reg + 1) as usize; - if min_stack > c.chunk.max_stack_size { - c.chunk.max_stack_size = min_stack; - } -} - -/// Get a register for result: use dest if provided, otherwise allocate new -/// This is the CORRECT way to handle dest parameter - always ensures max_stack_size is updated -/// and freereg is protected so subsequent allocations don't overwrite dest -#[inline] -pub fn get_result_reg(c: &mut Compiler, dest: Option) -> u32 { - match dest { - Some(reg) => { - ensure_register(c, reg); - // CRITICAL: Also update freereg to protect this register from being overwritten - // by subsequent allocations within the same expression - if c.freereg <= reg { - c.freereg = reg + 1; - } - reg - } - None => alloc_register(c), - } -} - -/// Free a register (only if >= nactvar) -/// Lua equivalent: freereg(fs, reg) -#[allow(dead_code)] -pub fn free_register(c: &mut Compiler, reg: u32) { - // Only free if register is beyond the active local variables - // This matches Lua's: if (reg >= luaY_nvarstack(fs)) fs->freereg--; - if reg >= nvarstack(c) && reg == c.freereg - 1 { - c.freereg -= 1; - } -} - -/// Free two registers in proper order -/// Lua equivalent: freeregs(fs, r1, r2) -#[allow(dead_code)] -pub fn free_registers(c: &mut Compiler, r1: u32, r2: u32) { - if r1 > r2 { - free_register(c, r1); - free_register(c, r2); - } else { - free_register(c, r2); - free_register(c, r1); - } -} - -/// Reset freereg to number of active local variables -/// Lua equivalent: fs->freereg = luaY_nvarstack(fs) -pub fn reset_freereg(c: &mut Compiler) { - c.freereg = nvarstack(c); -} - -/// Get the number of registers used by active local variables -/// Lua equivalent: luaY_nvarstack(fs) -pub fn nvarstack(c: &Compiler) -> u32 { - // Count non-const locals in current scope - // For simplicity, we use nactvar as the count - c.nactvar as u32 -} - -/// Add a local variable to the current scope -pub fn add_local(c: &mut Compiler, name: String, register: u32) { - add_local_with_attrs(c, name, register, false, false); -} - -/// Add a new local variable with and attributes -pub fn add_local_with_attrs( - c: &mut Compiler, - name: String, - register: u32, - is_const: bool, - is_to_be_closed: bool, -) { - let local = Local { - name: name.clone(), - depth: c.scope_depth, - register, - is_const, - is_to_be_closed, - needs_close: false, - }; - c.scope_chain.borrow_mut().locals.push(local); - - // Add to chunk's locals list for debugging/introspection - c.chunk.locals.push(name); - - // Increment nactvar for non-const locals - if !is_const { - c.nactvar += 1; - } - - // Emit TBC instruction for to-be-closed variables - if is_to_be_closed { - emit(c, Instruction::encode_abc(OpCode::Tbc, register, 0, 0)); - } -} - -/// Resolve a local variable by name (searches from innermost to outermost scope) -/// Now uses scope_chain directly -pub fn resolve_local<'a>(c: &'a Compiler, name: &str) -> Option { - // Search in current scope_chain's locals - let scope = c.scope_chain.borrow(); - scope.locals.iter().rev().find(|l| l.name == name).cloned() -} - -// add_upvalue function removed - logic inlined into resolve_upvalue_from_chain - -/// Resolve an upvalue by searching parent scopes through the scope chain -/// This is called when a variable is not found in local scope -/// Recursively searches through all ancestor scopes -pub fn resolve_upvalue_from_chain(c: &mut Compiler, name: &str) -> Option { - // Check if already in current upvalues - { - let scope = c.scope_chain.borrow(); - if let Some((idx, _)) = scope - .upvalues - .iter() - .enumerate() - .find(|(_, uv)| uv.name == name) - { - return Some(idx); - } - } - - // Get parent scope (clone to avoid borrow issues) - let parent = c.scope_chain.borrow().parent.clone()?; - - // Resolve from parent scope - this returns info about where the variable was found - let (is_local, index) = resolve_in_parent_scope(&parent, name)?; - - // Add upvalue to current scope - // Simply append in the order of first reference (Lua 5.4 behavior) - let upvalue_index = { - let mut scope = c.scope_chain.borrow_mut(); - scope.upvalues.push(super::Upvalue { - name: name.to_string(), - is_local, - index, - }); - scope.upvalues.len() - 1 - }; - - Some(upvalue_index) -} - -/// Recursively resolve variable in parent scope chain -/// Returns (is_local, index) where: -/// - is_local=true means found in direct parent's locals (index = register) -/// - is_local=false means found in ancestor's upvalue chain (index = upvalue index in parent) -fn resolve_in_parent_scope(scope: &Rc>, name: &str) -> Option<(bool, u32)> { - // First, search in this scope's locals - { - let mut scope_ref = scope.borrow_mut(); - if let Some(local) = scope_ref.locals.iter_mut().rev().find(|l| l.name == name) { - let register = local.register; - // Mark this local as captured by a closure - needs CLOSE on scope exit - local.needs_close = true; - // Found as local - return (true, register) - return Some((true, register)); - } - } - - // Check if already in this scope's upvalues - { - let scope_ref = scope.borrow(); - if let Some((idx, _)) = scope_ref - .upvalues - .iter() - .enumerate() - .find(|(_, uv)| uv.name == name) - { - // Found in this scope's existing upvalues - return (false, upvalue_index) - return Some((false, idx as u32)); - } - } - - // Not found in this scope - search in grandparent - let grandparent = scope.borrow().parent.clone()?; - - // Recursively resolve from grandparent - let (gp_is_local, gp_index) = resolve_in_parent_scope(&grandparent, name)?; - - // Add upvalue to this scope (intermediate scope between caller and where variable was found) - let upvalue_idx = { - let mut scope_mut = scope.borrow_mut(); - // Check if already in upvalues - if let Some((idx, _)) = scope_mut - .upvalues - .iter() - .enumerate() - .find(|(_, uv)| uv.name == name) - { - idx as u32 - } else { - // Add new upvalue - always false because we're capturing from ancestor - scope_mut.upvalues.push(super::Upvalue { - name: name.to_string(), - is_local: gp_is_local, - index: gp_index, - }); - (scope_mut.upvalues.len() - 1) as u32 - } - }; - - // Return (false, upvalue_idx) because caller needs to capture from our upvalue - Some((false, upvalue_idx)) -} - -/// Begin a new scope -pub fn begin_scope(c: &mut Compiler) { - c.scope_depth += 1; -} - -/// End the current scope -pub fn end_scope(c: &mut Compiler) { - // Before closing the scope, emit CLOSE instruction for to-be-closed variables - // Find the minimum register of all to-be-closed variables in the current scope - let mut min_tbc_reg: Option = None; - let mut removed_count = 0usize; - { - let scope = c.scope_chain.borrow(); - for local in scope.locals.iter().rev() { - if local.depth > c.scope_depth { - break; // Only check current scope - } - if local.depth == c.scope_depth { - if local.is_to_be_closed { - min_tbc_reg = Some(match min_tbc_reg { - None => local.register, - Some(min_reg) => min_reg.min(local.register), - }); - } - if !local.is_const { - removed_count += 1; - } - } - } - } - - // Emit CLOSE instruction if there are to-be-closed variables - if let Some(reg) = min_tbc_reg { - emit(c, Instruction::encode_abc(OpCode::Close, reg, 0, 0)); - } - - c.scope_depth -= 1; - - // Decrease nactvar by number of removed non-const locals - c.nactvar = c.nactvar.saturating_sub(removed_count); - - c.scope_chain - .borrow_mut() - .locals - .retain(|l| l.depth <= c.scope_depth); - - // Reset freereg after removing locals - reset_freereg(c); - - // Clear labels from the scope being closed - clear_scope_labels(c); -} - -/// Get a global variable (Lua 5.4 uses _ENV upvalue) -pub fn emit_get_global(c: &mut Compiler, name: &str, dest_reg: u32) { - // Ensure _ENV upvalue exists - ensure_env_upvalue(c); - - // Find _ENV's actual index in upvalues - let env_index = { - let scope = c.scope_chain.borrow(); - scope - .upvalues - .iter() - .position(|uv| uv.name == "_ENV") - .expect("_ENV upvalue should exist after ensure_env_upvalue") - }; - - let lua_str = create_string_value(c, name); - let const_idx = add_constant_dedup(c, lua_str); - // GetTabUp: R(A) := UpValue[B][K(C)] - // B is _ENV's upvalue index, C is constant index, k=1 - emit( - c, - Instruction::create_abck( - OpCode::GetTabUp, - dest_reg, - env_index as u32, - const_idx, - true, - ), - ); -} - -/// Set a global variable (Lua 5.4 uses _ENV upvalue) -pub fn emit_set_global(c: &mut Compiler, name: &str, src_reg: u32) { - // Ensure _ENV upvalue exists - ensure_env_upvalue(c); - - // Find _ENV's actual index in upvalues - let env_index = { - let scope = c.scope_chain.borrow(); - scope - .upvalues - .iter() - .position(|uv| uv.name == "_ENV") - .expect("_ENV upvalue should exist after ensure_env_upvalue") - }; - - let lua_str = create_string_value(c, name); - let const_idx = add_constant_dedup(c, lua_str); - // SetTabUp: UpValue[A][K(B)] := RK(C) - // A is _ENV's upvalue index, B is constant index for key, C is source register - // k=false means C is a register index (not constant) - emit( - c, - Instruction::create_abck( - OpCode::SetTabUp, - env_index as u32, - const_idx, - src_reg, - false, - ), - ); -} - -/// Ensure _ENV is in upvalue[0] -fn ensure_env_upvalue(c: &mut Compiler) { - let scope = c.scope_chain.borrow(); - - // Check if _ENV exists anywhere in upvalues - if scope.upvalues.iter().any(|uv| uv.name == "_ENV") { - // _ENV already exists - no need to add it again - return; - } - - // _ENV doesn't exist - need to resolve it from parent - drop(scope); - - // Try to resolve _ENV from parent scope chain - // This will add _ENV to upvalues in the order of first reference (Lua 5.4 behavior) - if let Some(_env_idx) = resolve_upvalue_from_chain(c, "_ENV") { - // Successfully added _ENV to upvalues - // No reordering needed - Lua 5.4 uses natural reference order - } else { - // Can't resolve _ENV from parent - this means we're in top-level chunk - // Top-level chunk should have _ENV as upvalue[0] from VM initialization - // We don't need to add it here - } -} - -/// Emit LoadK/LoadKX instruction (Lua 5.4 style) -/// Loads a constant from the constant table into a register -pub fn emit_loadk(c: &mut Compiler, dest: u32, const_idx: u32) -> usize { - if const_idx <= Instruction::MAX_BX { - emit(c, Instruction::encode_abx(OpCode::LoadK, dest, const_idx)) - } else { - // Use LoadKX + ExtraArg for large constant indices (> 131071) - let pos = emit(c, Instruction::encode_abx(OpCode::LoadKX, dest, 0)); - emit(c, Instruction::create_ax(OpCode::ExtraArg, const_idx)); - pos - } -} - -/// Emit LoadI instruction for small integers (Lua 5.4) -/// LoadI can encode integers directly in sBx field (-65536 to 65535) -/// Returns Some(pos) if successful, None if value too large -pub fn emit_loadi(c: &mut Compiler, dest: u32, value: i64) -> Option { - if value >= i32::MIN as i64 && value <= i32::MAX as i64 { - let sbx = value as i32; - // Check if fits in sBx field - if sbx >= -(Instruction::OFFSET_SBX) && sbx <= Instruction::OFFSET_SBX { - return Some(emit(c, Instruction::encode_asbx(OpCode::LoadI, dest, sbx))); - } - } - None // Value too large, must use LoadK -} - -/// Emit LoadF instruction for floats (Lua 5.4) -/// LoadF encodes small floats in sBx field (integer-representable floats only) -/// Returns Some(pos) if successful, None if must use LoadK -pub fn emit_loadf(c: &mut Compiler, dest: u32, value: f64) -> Option { - // For simplicity, only handle integer-representable floats - if value.fract() == 0.0 { - let int_val = value as i32; - if int_val as f64 == value { - if int_val >= -(Instruction::OFFSET_SBX) && int_val <= Instruction::OFFSET_SBX { - return Some(emit( - c, - Instruction::encode_asbx(OpCode::LoadF, dest, int_val), - )); - } - } - } - None // Complex float, must use LoadK -} - -/// Load nil into a register -pub fn emit_load_nil(c: &mut Compiler, reg: u32) { - emit(c, Instruction::encode_abc(OpCode::LoadNil, reg, 0, 0)); -} - -/// Load boolean into a register (Lua 5.4 uses LoadTrue/LoadFalse) -pub fn emit_load_bool(c: &mut Compiler, reg: u32, value: bool) { - if value { - emit(c, Instruction::encode_abc(OpCode::LoadTrue, reg, 0, 0)); - } else { - emit(c, Instruction::encode_abc(OpCode::LoadFalse, reg, 0, 0)); - } -} - -/// Load constant into a register -pub fn emit_load_constant(c: &mut Compiler, reg: u32, const_idx: u32) { - emit(c, Instruction::encode_abx(OpCode::LoadK, reg, const_idx)); -} - -/// Move value from one register to another -pub fn emit_move(c: &mut Compiler, dest: u32, src: u32) { - if dest != src { - emit(c, Instruction::encode_abc(OpCode::Move, dest, src, 0)); - } -} - -/// Try to compile expression as a constant, returns Some(const_idx) if successful -pub fn try_expr_as_constant(c: &mut Compiler, expr: &emmylua_parser::LuaExpr) -> Option { - // Only handle literal expressions that can be constants - if let LuaExpr::LiteralExpr(lit_expr) = expr { - if let Some(literal_token) = lit_expr.get_literal() { - match literal_token { - LuaLiteralToken::Bool(b) => { - return try_add_constant_k(c, LuaValue::boolean(b.is_true())); - } - LuaLiteralToken::Number(num) => { - let value = if num.is_float() { - LuaValue::float(num.get_float_value()) - } else { - LuaValue::integer(num.get_int_value()) - }; - return try_add_constant_k(c, value); - } - LuaLiteralToken::String(s) => { - let lua_str = create_string_value(c, &s.get_value()); - return try_add_constant_k(c, lua_str); - } - _ => {} - } - } - } - None -} - -/// Begin a new loop (for break statement support) -pub fn begin_loop(c: &mut Compiler) { - begin_loop_with_register(c, c.freereg); -} - -/// Begin a new loop with a specific first register for CLOSE -pub fn begin_loop_with_register(c: &mut Compiler, first_reg: u32) { - c.loop_stack.push(super::LoopInfo { - break_jumps: Vec::new(), - scope_depth: c.scope_depth, - first_local_register: first_reg, - }); -} - -/// End current loop and patch all break statements -pub fn end_loop(c: &mut Compiler) { - if let Some(loop_info) = c.loop_stack.pop() { - // Patch all break jumps to current position - for jump_pos in loop_info.break_jumps { - patch_jump(c, jump_pos); - } - } -} - -/// Emit a break statement (jump to end of current loop) -pub fn emit_break(c: &mut Compiler) -> Result<(), String> { - if c.loop_stack.is_empty() { - return Err("break statement outside loop".to_string()); - } - - // Before breaking, check if we need to close any captured upvalues - // This is needed when break jumps past the CLOSE instruction that would - // normally be executed at the end of each iteration - let loop_info = c.loop_stack.last().unwrap(); - let loop_scope_depth = loop_info.scope_depth; - let first_reg = loop_info.first_local_register; - - // Find minimum register of captured locals in the loop scope - let mut min_close_reg: Option = None; - { - let scope = c.scope_chain.borrow(); - for local in scope.locals.iter().rev() { - if local.depth < loop_scope_depth { - break; // Only check loop scope and nested scopes - } - if local.needs_close && local.register >= first_reg { - min_close_reg = Some(match min_close_reg { - None => local.register, - Some(min_reg) => min_reg.min(local.register), - }); - } - } - } - - // Emit CLOSE if needed - if let Some(reg) = min_close_reg { - emit(c, Instruction::encode_abc(OpCode::Close, reg, 0, 0)); - } - - let jump_pos = emit_jump(c, OpCode::Jmp); - c.loop_stack.last_mut().unwrap().break_jumps.push(jump_pos); - Ok(()) -} - -/// Define a label at current position -pub fn define_label(c: &mut Compiler, name: String) -> Result<(), String> { - // Check if label already exists in current scope - for label in &c.labels { - if label.name == name && label.scope_depth == c.scope_depth { - return Err(format!("label '{}' already defined", name)); - } - } - - let position = c.chunk.code.len(); - c.labels.push(super::Label { - name: name.clone(), - position, - scope_depth: c.scope_depth, - }); - - // Try to resolve any pending gotos to this label - resolve_pending_gotos(c, &name); - - Ok(()) -} - -/// Emit a goto statement -pub fn emit_goto(c: &mut Compiler, label_name: String) -> Result<(), String> { - // Check if label is already defined - for label in &c.labels { - if label.name == label_name { - // Label found - emit direct jump - let current_pos = c.chunk.code.len(); - let offset = label.position as i32 - current_pos as i32 - 1; - emit(c, Instruction::create_sj(OpCode::Jmp, offset)); - return Ok(()); - } - } - - // Label not yet defined - add to pending gotos - let jump_pos = emit_jump(c, OpCode::Jmp); - c.gotos.push(super::GotoInfo { - name: label_name, - jump_position: jump_pos, - scope_depth: c.scope_depth, - }); - - Ok(()) -} - -/// Resolve pending gotos for a newly defined label -fn resolve_pending_gotos(c: &mut Compiler, label_name: &str) { - let label_pos = c - .labels - .iter() - .find(|l| l.name == label_name) - .map(|l| l.position) - .unwrap(); - - // Find and patch all gotos to this label - let mut i = 0; - while i < c.gotos.len() { - if c.gotos[i].name == label_name { - let goto = c.gotos.remove(i); - let offset = label_pos as i32 - goto.jump_position as i32 - 1; - c.chunk.code[goto.jump_position] = Instruction::create_sj(OpCode::Jmp, offset); - } else { - i += 1; - } - } -} - -/// Check for unresolved gotos (call at end of compilation) -pub fn check_unresolved_gotos(c: &Compiler) -> Result<(), String> { - if !c.gotos.is_empty() { - let names: Vec<_> = c.gotos.iter().map(|g| g.name.as_str()).collect(); - return Err(format!("undefined label(s): {}", names.join(", "))); - } - Ok(()) -} - -/// Clear labels when leaving a scope -pub fn clear_scope_labels(c: &mut Compiler) { - c.labels.retain(|l| l.scope_depth < c.scope_depth); -} - -/// Check if an expression is a vararg (...) literal -pub fn is_vararg_expr(expr: &LuaExpr) -> bool { - if let LuaExpr::LiteralExpr(lit) = expr { - matches!(lit.get_literal(), Some(LuaLiteralToken::Dots(_))) - } else { - false - } -} - -/// Result of parsing a Lua number literal -#[derive(Debug, Clone, Copy)] -pub enum ParsedNumber { - /// Successfully parsed as integer - Int(i64), - /// Number is too large for i64, use float instead - Float(f64), -} - -/// Parse a Lua integer literal from text, handling hex numbers that overflow i64 -/// Lua treats 0xFFFFFFFFFFFFFFFF as -1 (two's complement interpretation) -/// For decimal numbers that overflow i64 range, returns Float instead -pub fn parse_lua_int(text: &str) -> ParsedNumber { - let text = text.trim(); - if text.starts_with("0x") || text.starts_with("0X") { - // Hex number - parse as u64 first, then reinterpret as i64 - // This handles the case like 0xFFFFFFFFFFFFFFFF which should be -1 - let hex_part = &text[2..]; - // Remove any trailing decimal part (e.g., 0xFF.0) - let hex_part = hex_part.split('.').next().unwrap_or(hex_part); - if let Ok(val) = u64::from_str_radix(hex_part, 16) { - return ParsedNumber::Int(val as i64); // Reinterpret bits as signed - } - } - // Decimal case: parse as i64 only, if overflow use float - // (Unlike hex, decimal numbers should NOT be reinterpreted as two's complement) - if let Ok(val) = text.parse::() { - return ParsedNumber::Int(val); - } - // Decimal number is too large for i64, parse as float - if let Ok(val) = text.parse::() { - return ParsedNumber::Float(val); - } - // Default fallback - ParsedNumber::Int(0) -} - -/// Lua left shift: x << n (returns 0 if |n| >= 64) -/// Negative n means right shift -#[inline(always)] -pub fn lua_shl(l: i64, r: i64) -> i64 { - if r >= 64 || r <= -64 { - 0 - } else if r >= 0 { - (l as u64).wrapping_shl(r as u32) as i64 - } else { - // Negative shift means right shift (logical) - (l as u64).wrapping_shr((-r) as u32) as i64 - } -} - -/// Lua right shift: x >> n (logical shift, returns 0 if |n| >= 64) -/// Negative n means left shift -#[inline(always)] -pub fn lua_shr(l: i64, r: i64) -> i64 { - if r >= 64 || r <= -64 { - 0 - } else if r >= 0 { - (l as u64).wrapping_shr(r as u32) as i64 - } else { - // Negative shift means left shift - (l as u64).wrapping_shl((-r) as u32) as i64 - } -} \ No newline at end of file diff --git a/crates/luars/src/compiler/mod.rs b/crates/luars/src/compiler/mod.rs index bedfd5e..b98c118 100644 --- a/crates/luars/src/compiler/mod.rs +++ b/crates/luars/src/compiler/mod.rs @@ -1,267 +1,134 @@ // Lua bytecode compiler - Main module -// Compiles Lua source code to bytecode using emmylua_parser -mod binop; -mod exp2reg; -mod expdesc; -mod expr; -mod helpers; -mod stmt; -mod tagmethod; - -use rowan::TextRange; -pub(crate) use tagmethod::TagMethod; - +// Port of Lua 5.4 lparser.c and lcode.c + +// Submodules +mod code; // Code generation (lcode.c) +mod expr_parser; // Expression parser functions (lparser.c) +mod expression; // Expression parsing and expdesc +mod func_state; // FuncState and related structures (lparser.h) +pub mod parse_literal; // Number parsing utilities +mod parser; // Lexer/token provider +mod statement; // Statement parsing (lparser.c) +mod tm_kind; + +// Re-exports +pub use code::*; +pub use expression::*; +pub use func_state::*; + +use crate::compiler::parser::{ + LuaLanguageLevel, LuaLexer, LuaTokenKind, LuaTokenize, Reader, TokensizeConfig, +}; +use crate::gc::ObjectPool; use crate::lua_value::Chunk; -use crate::lua_value::UpvalueDesc; -use crate::lua_vm::LuaVM; -use crate::lua_vm::{Instruction, OpCode}; -// use crate::optimizer::optimize_constants; // Disabled for now -use emmylua_parser::{LineIndex, LuaBlock, LuaChunk, LuaLanguageLevel, LuaParser, ParserConfig}; -use helpers::*; -use std::cell::RefCell; -use std::rc::Rc; -use stmt::*; - -/// Scope chain for variable and upvalue resolution -/// This allows efficient lookup through parent scopes without cloning -pub struct ScopeChain { - #[allow(private_interfaces)] - pub locals: Vec, - #[allow(private_interfaces)] - pub upvalues: Vec, - pub parent: Option>>, -} - -impl ScopeChain { - pub fn new() -> Rc> { - Rc::new(RefCell::new(ScopeChain { - locals: Vec::new(), - upvalues: Vec::new(), - parent: None, - })) - } +use crate::lua_vm::OpCode; - pub fn new_with_parent(parent: Rc>) -> Rc> { - Rc::new(RefCell::new(ScopeChain { - locals: Vec::new(), - upvalues: Vec::new(), - parent: Some(parent), - })) - } -} - -/// Compiler state -pub struct Compiler<'a> { - pub(crate) chunk: Chunk, - pub(crate) scope_depth: usize, - pub(crate) freereg: u32, // First free register (replaces next_register) - pub(crate) peak_freereg: u32, // Peak value of freereg (for max_stack_size) - pub(crate) nactvar: usize, // Number of active local variables - pub(crate) loop_stack: Vec, - pub(crate) labels: Vec