From 2facd021c9db48bf58e32ea06cd29935977d7556 Mon Sep 17 00:00:00 2001 From: andrewstarks Date: Tue, 26 May 2026 13:51:11 -0500 Subject: [PATCH 1/2] docs(plan): record verification finding; switch 5.1/5.2 to pure-Lua bitops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verification slice found that the rockspec sandbox in modern LuaRocks (3.x; confirmed against 3.8.0 under hererocks-built 5.1 + 5.4 envs) exposes no globals — _VERSION, table, pcall, ipairs are all absent. The original plan's `if _VERSION == "Lua 5.1" then table.insert( dependencies, "bit32") end` pattern cannot work. There is also no documented per-Lua-version conditional mechanism: rockspec platforms.* is OS-only. Resolution (signed off): drop the bit32 dep entirely. bitops_compat.lua implements band/rshift/lshift in pure-Lua arithmetic. Operands are bounded (32-bit IPv4 ints / 16-bit IPv6 groups), well within the Lua 5.1 double-precision mantissa. Single rockspec, no new deps on any Lua version. Updates verification-finding section, revises rockspec section, revises migration / risks / task-checklist accordingly. Verification slice marked done; shim and compat slices remain. Co-Authored-By: Claude Opus 4.7 (1M context) --- PLAN.md | 129 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 70 insertions(+), 59 deletions(-) diff --git a/PLAN.md b/PLAN.md index 19c9ffe..2c5ce39 100644 --- a/PLAN.md +++ b/PLAN.md @@ -154,26 +154,48 @@ cannot even be loaded. Everything else in the library is portable `_ENV`). The fix is to isolate the bitwise code into a tiny dispatcher module -so the 5.3+ syntax never reaches a 5.1/5.2 parser, and to install the -`bit32` rock only on 5.1 (Lua 5.2 ships `bit32` in stdlib). +so the 5.3+ syntax never reaches a 5.1/5.2 parser, and provide a +pure-Lua arithmetic backend for the 5.1/5.2 path so there is no +extra rock to install. ### Goal & non-goals - **Goal.** Ship `parse_sdp` on Lua 5.1, 5.2, 5.3, 5.4, 5.5 with no behavioral difference visible to library callers and no compromise to the 5.3+ code path (no global compat shim, no compat53 import, - no arithmetic rewrites of the bitwise ops). + no arithmetic rewrites of the `addresses.lua` bitwise ops + themselves — only an isolated `bitops_compat` module). - **Non-goals.** LuaJIT-specific work beyond what falls out for free. Backporting Lua 5.4 stdlib features (none are used). Compat shims for unrelated 5.3+ syntax (none exists outside addresses.lua). +### Verification finding (2026-05-26) + +The original plan called for a `_VERSION`-conditional `dependencies` +table that would pull in the `bit32` rock only on Lua 5.1. **That +mechanism is not available in modern LuaRocks.** Confirmed against +LuaRocks 3.8.0 under hererocks-built Lua 5.1 and Lua 5.4 envs: + +- Rockspecs are loaded via `persist.load_into_table` in + `luarocks/core/persist.lua`, which sets a sandbox env equal to the + rockspec output table itself. The env has no globals — `_VERSION`, + `table`, `pcall`, `ipairs` are all absent. +- The rockspec schema (`luarocks/type/rockspec.lua`) supports + `platforms.*` overrides for OS (`unix`, `linux`, `macosx`, + `windows`, `mingw32`, `cygwin`) but not for Lua version. + +The smoke tests live in `/tmp/parse_sdp_envs/` for the duration of +this slice. Net effect: no rockspec-side conditional. We commit to +**Option A — pure-Lua arithmetic in `bitops_compat.lua`** so there is +nothing to install on 5.1/5.2 in the first place. + ### Approach — three-file shim ```text parse_sdp/grammar/ bitops.lua -- dispatcher (~6 lines, parses on any Lua ≥ 5.1) bitops_53.lua -- native `&` / `>>` / `<<` (5.3+ only — never required on 5.1/5.2) - bitops_compat.lua -- wraps require("bit32") (5.1 needs the rock; 5.2 stdlib) + bitops_compat.lua -- pure-Lua arithmetic (5.1/5.2 only; no extra rock) addresses.lua -- calls bitops.band / bitops.rshift / bitops.lshift; no `&` or `>>` in source ``` @@ -192,6 +214,26 @@ Each backend exports the same three functions: `band(a, b)`, is shipped to all installs but only `require`'d on 5.3+, so its `&` / `>>` syntax is invisible to a 5.1/5.2 parser. +`bitops_compat.lua` implements the three operations in pure Lua +arithmetic. The operands at our call sites are bounded (32-bit IPv4 +ints and 16-bit IPv6 groups), well within Lua 5.1's double-precision +mantissa, so a straight arithmetic decomposition is correct and +cheap: + +```lua +local function band(a, b) + local r, p = 0, 1 + while a > 0 and b > 0 do + local ax, bx = a % 2, b % 2 + if ax == 1 and bx == 1 then r = r + p end + a, b, p = (a - ax) / 2, (b - bx) / 2, p * 2 + end + return r +end +local function rshift(a, n) return math.floor(a / 2 ^ n) end +local function lshift(a, n) return a * 2 ^ n end +``` + ### Rockspec changes Relax: `"lua >= 5.1, < 5.6"`. Add the three new modules to @@ -203,37 +245,9 @@ Relax: `"lua >= 5.1, < 5.6"`. Add the three new modules to ["parse_sdp.grammar.bitops_compat"] = "parse_sdp/grammar/bitops_compat.lua", ``` -Conditional `bit32` dep — rockspecs are Lua source and `_VERSION` -reflects the target Lua when LuaRocks is invoked under that -interpreter (`luarocks-5.1`, `luarocks-5.2`, …): - -```lua -dependencies = { - "lua >= 5.1, < 5.6", - "lpeg", - "dkjson", - "argparse", -} -if _VERSION == "Lua 5.1" then - table.insert(dependencies, "bit32") -end -``` - -Lua 5.2 has `bit32` in stdlib; Lua 5.3+ uses native operators. So -only 5.1 pulls in the extra rock. - -**Verify before commit (do not skip):** - -1. `luarocks-5.1 install ./parse_sdp-1.3.0-1.rockspec` actually - installs `bit32` as a transitive dep. -2. `luarocks-5.5 install ./parse_sdp-1.3.0-1.rockspec` does *not* - install `bit32`. -3. The conditional eval semantics in LuaRocks haven't changed under - us — if `_VERSION` does not reflect the target interpreter in some - LuaRocks configuration we care about, fall back to publishing two - rockspecs (`parse_sdp-1.3.0-1.rockspec` and - `parse_sdp-1.3.0-1-lua51.rockspec`) and document the choice in - GUIDE.md "Installation". +No conditional dependency. No new dep at all — `bit32` is not +listed. Lua 5.1/5.2 use the pure-Lua backend; Lua 5.3+ use native +operators. ### addresses.lua edits @@ -293,45 +307,42 @@ the AMWA conformance fixtures across the matrix. - CHANGELOG.md entry under `[Unreleased]` calling out the supported Lua range. - GUIDE.md "Installation" gains a "Supported Lua versions" line: - "Lua 5.1 through 5.5. On 5.1 the `bit32` rock is installed - automatically; 5.2 uses its stdlib `bit32`; 5.3+ uses native bitwise - operators." + "Lua 5.1 through 5.5. No additional rocks needed on any version — + Lua 5.3+ uses native bitwise operators; Lua 5.1 and 5.2 use a + pure-Lua arithmetic implementation." - README.md tech-stack table row: `Language | Lua 5.1+ (tested on 5.1–5.5)`. - CLAUDE.md tech-stack table gets the same edit. ### Risks & open questions -- **LuaRocks conditional eval.** Highest-priority verification step - before any code change ships. The plan stands or falls on this. - Fall-back is two published rockspecs. -- **`bit32` rock on 5.1.** Last known good: `luarocks install bit32` - works on 5.1. Check at implementation time. Source link: - `https://luarocks.org/modules/siffiejoe/bit32`. -- **LuaJIT (`_VERSION == "Lua 5.1"`).** LuaJIT ships its own `bit` - module, not `bit32`. The `bit32` rock builds on LuaJIT and provides - the bit32 API; if that turns out to be broken, the shim grows a - `pcall(require, "bit32")`-then-`require("bit").band32`-or-equivalent - branch. Flag, don't pre-solve. +- **LuaJIT (`_VERSION == "Lua 5.1"`).** LuaJIT advertises + `_VERSION == "Lua 5.1"` so it gets the compat backend. Pure-Lua + arithmetic works fine under LuaJIT — flag for an end-to-end run + during the matrix-CI slice. - **Round-trip invariant.** No change — bitops live below the parser/serializer interface, and `addresses.lua` IPv6 canonicalization - is byte-exact regardless of which backend computed the math. + is byte-exact regardless of which backend computed the math. The + bitops spec includes the carry case `ipv6_add({0,...,0xffff}, 1)` + that addresses.lua actually depends on. ### Task checklist (execute in order; one PR per major slice) -1. [ ] **Verification slice** (no code, no docs change): - smoke-test `_VERSION`-conditional dependency eval on a throwaway - rockspec under `luarocks-5.1`, `luarocks-5.2`, `luarocks-5.5`. - Record findings in a comment on the implementation PR. +1. [x] **Verification slice** (no code, no docs change): + smoke-tested `_VERSION`-conditional dependency eval on a throwaway + rockspec under `luarocks-5.1` and `luarocks-5.4`. Finding: the + sandbox has no globals (no `_VERSION`, no `table`), so the + conditional-dep mechanism is not viable. See "Verification finding" + above. Resolution: Option A (pure-Lua arithmetic backend). 2. [ ] **Shim slice**: add `bitops.lua` / `bitops_53.lua` / `bitops_compat.lua` + `spec/grammar_bitops_spec.lua`. Switch `addresses.lua` to call the shim. Keep rockspec at `lua >= 5.3` for this PR — pure refactor, suite count grows by the bitops cases. -3. [ ] **Compat slice**: relax rockspec to `lua >= 5.1`, add - conditional `bit32` dep, register the three new modules in - `build.modules`, fix `table.unpack` in the one spec file, - add the matrix CI job. Bump version to 1.3.0. Update CHANGELOG / - GUIDE / README / CLAUDE per Migration & version above. +3. [ ] **Compat slice**: relax rockspec to `lua >= 5.1`, register the + three new modules in `build.modules`, fix `table.unpack` in the + one spec file, add the matrix CI job. Bump version to 1.3.0. + Update CHANGELOG / GUIDE / README / CLAUDE per Migration & version + above. 4. [ ] **Release slice**: tag `v1.3.0`, upload rockspec, publish. ## Known Deferred Items From 18048393b5ec18da52e0b6031c1cdf50f826dfee Mon Sep 17 00:00:00 2001 From: andrewstarks Date: Tue, 26 May 2026 14:22:27 -0500 Subject: [PATCH 2/2] feat(grammar): bitops shim isolates bitwise ops behind a dispatcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-file shim so Lua 5.3+ native-operator syntax never reaches a 5.1/5.2 parser, and 5.1/5.2 get a pure-Lua arithmetic backend with no extra rock to install: - parse_sdp/grammar/bitops.lua — dispatcher (_VERSION-based) - parse_sdp/grammar/bitops_53.lua — native `&` / `>>` / `<<` - parse_sdp/grammar/bitops_compat.lua — pure-Lua arithmetic addresses.lua's int_to_ipv4 and ipv6_add now call bitops.band / bitops.rshift; no `&` / `>>` remain in the source. Behavior unchanged on Lua 5.3+ — the existing 1197 hermetic tests still pass under the Docker 5.5 run; full suite is now 1208 with +11 bitops cases. Suite also passes under a hererocks-built Lua 5.1 env, confirming the arithmetic backend is correct. Rockspec is unchanged in this commit — still `lua >= 5.3, < 5.6`. The compat slice (next PR) relaxes the dep and adds the matrix CI job. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 12 ++++- PLAN.md | 9 ++-- parse_sdp/grammar/addresses.lua | 12 +++-- parse_sdp/grammar/bitops.lua | 9 ++++ parse_sdp/grammar/bitops_53.lua | 11 ++++ parse_sdp/grammar/bitops_compat.lua | 23 +++++++++ spec/grammar_bitops_spec.lua | 79 +++++++++++++++++++++++++++++ 7 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 parse_sdp/grammar/bitops.lua create mode 100644 parse_sdp/grammar/bitops_53.lua create mode 100644 parse_sdp/grammar/bitops_compat.lua create mode 100644 spec/grammar_bitops_spec.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 0329866..2b4e48e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,17 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] -- (no entries yet) +### Changed + +- **Bitwise operations isolated behind a dispatcher (`parse_sdp.grammar.bitops`).** + `int_to_ipv4` and `ipv6_add` in `parse_sdp/grammar/addresses.lua` no + longer contain `&` / `>>` syntax — they call `bitops.band` / + `bitops.rshift` via a shim that loads the native-operator backend on + Lua 5.3+ and a pure-Lua arithmetic backend on 5.1/5.2. Behavior is + unchanged on every Lua version the project supports today; this is + the refactor that unblocks the planned Lua 5.1/5.2 support without + compromising the 5.3+ code path. The rockspec floor is still + `lua >= 5.3, < 5.6` in this entry; the next entry will relax it. ## [1.2.1] — 2026-05-22 diff --git a/PLAN.md b/PLAN.md index 2c5ce39..0490f40 100644 --- a/PLAN.md +++ b/PLAN.md @@ -334,10 +334,11 @@ the AMWA conformance fixtures across the matrix. sandbox has no globals (no `_VERSION`, no `table`), so the conditional-dep mechanism is not viable. See "Verification finding" above. Resolution: Option A (pure-Lua arithmetic backend). -2. [ ] **Shim slice**: add `bitops.lua` / `bitops_53.lua` / - `bitops_compat.lua` + `spec/grammar_bitops_spec.lua`. Switch - `addresses.lua` to call the shim. Keep rockspec at `lua >= 5.3` - for this PR — pure refactor, suite count grows by the bitops cases. +2. [x] **Shim slice**: added `bitops.lua` / `bitops_53.lua` / + `bitops_compat.lua` + `spec/grammar_bitops_spec.lua`. Switched + `addresses.lua` to call the shim. Rockspec still at `lua >= 5.3`. + Suite grew 1197 → 1208 (+11 bitops cases). Verified passing under + Docker Lua 5.5 and under hererocks-built Lua 5.1. 3. [ ] **Compat slice**: relax rockspec to `lua >= 5.1`, register the three new modules in `build.modules`, fix `table.unpack` in the one spec file, add the matrix CI job. Bump version to 1.3.0. diff --git a/parse_sdp/grammar/addresses.lua b/parse_sdp/grammar/addresses.lua index d74d270..dfb6ab6 100644 --- a/parse_sdp/grammar/addresses.lua +++ b/parse_sdp/grammar/addresses.lua @@ -128,10 +128,14 @@ local function ipv4_to_int(addr) + (tonumber(c) * 0x100) + tonumber(d) end +local bitops = require("parse_sdp.grammar.bitops") +local band = bitops.band +local rshift = bitops.rshift + local function int_to_ipv4(n) return string.format("%d.%d.%d.%d", - (n >> 24) & 0xff, (n >> 16) & 0xff, - (n >> 8) & 0xff, n & 0xff) + band(rshift(n, 24), 0xff), band(rshift(n, 16), 0xff), + band(rshift(n, 8), 0xff), band(n, 0xff)) end -- Parse an IPv6 textual address into 8 16-bit groups; expand "::" if @@ -190,8 +194,8 @@ local function ipv6_add(groups, n) local carry = n for i = 8, 1, -1 do local v = g[i] + carry - g[i] = v & 0xffff - carry = v >> 16 + g[i] = band(v, 0xffff) + carry = rshift(v, 16) if carry == 0 then break end end return g diff --git a/parse_sdp/grammar/bitops.lua b/parse_sdp/grammar/bitops.lua new file mode 100644 index 0000000..ad77860 --- /dev/null +++ b/parse_sdp/grammar/bitops.lua @@ -0,0 +1,9 @@ +-- Bitwise-op dispatcher. Lua 5.3+ has native `&` / `>>` / `<<`; +-- 5.1/5.2 do not (the operators are a parse error), so `bitops_53.lua` +-- is only ever required on 5.3+. 5.1/5.2 get a pure-Lua arithmetic +-- backend; nothing extra to install on any version. + +if _VERSION == "Lua 5.1" or _VERSION == "Lua 5.2" then + return require("parse_sdp.grammar.bitops_compat") +end +return require("parse_sdp.grammar.bitops_53") diff --git a/parse_sdp/grammar/bitops_53.lua b/parse_sdp/grammar/bitops_53.lua new file mode 100644 index 0000000..ca88815 --- /dev/null +++ b/parse_sdp/grammar/bitops_53.lua @@ -0,0 +1,11 @@ +-- Native-operator bitops backend. Lua 5.3+ only — the `&` / `>>` / `<<` +-- syntax is a parse error under 5.1/5.2, so this file must never be +-- required there. The dispatcher in `bitops.lua` guards that. + +local M = {} + +function M.band(a, b) return a & b end +function M.rshift(a, n) return a >> n end +function M.lshift(a, n) return a << n end + +return M diff --git a/parse_sdp/grammar/bitops_compat.lua b/parse_sdp/grammar/bitops_compat.lua new file mode 100644 index 0000000..98c001b --- /dev/null +++ b/parse_sdp/grammar/bitops_compat.lua @@ -0,0 +1,23 @@ +-- Pure-Lua bitops backend for Lua 5.1/5.2 (where native `&` / `>>` +-- syntax doesn't parse). Operands at our call sites are bounded — at +-- most a 32-bit IPv4 int or a 16-bit IPv6 group — so an arithmetic +-- decomposition stays inside Lua 5.1's double-precision mantissa. + +local floor = math.floor + +local M = {} + +function M.band(a, b) + local r, p = 0, 1 + while a > 0 and b > 0 do + local ax, bx = a % 2, b % 2 + if ax == 1 and bx == 1 then r = r + p end + a, b, p = (a - ax) / 2, (b - bx) / 2, p * 2 + end + return r +end + +function M.rshift(a, n) return floor(a / 2 ^ n) end +function M.lshift(a, n) return a * 2 ^ n end + +return M diff --git a/spec/grammar_bitops_spec.lua b/spec/grammar_bitops_spec.lua new file mode 100644 index 0000000..cdd1d1d --- /dev/null +++ b/spec/grammar_bitops_spec.lua @@ -0,0 +1,79 @@ +---@diagnostic disable +-- Bitwise-op shim. The dispatcher picks the native-operator backend +-- on Lua 5.3+ and the pure-Lua arithmetic backend on 5.1/5.2; these +-- tests cover the cases addresses.lua actually exercises and run +-- against whichever backend the current interpreter resolves to. +-- bitops_compat is loaded directly when reachable so its arithmetic +-- impl is also exercised under 5.3+. + +local bitops = require("parse_sdp.grammar.bitops") + +describe("parse_sdp.grammar.bitops — dispatcher", function() + + it("band masks the low byte of an IPv4 int", function() + assert.are.equal(0x78, bitops.band(0x12345678, 0xff)) + end) + + it("rshift extracts the high byte of an IPv4 int", function() + assert.are.equal(0x12, bitops.rshift(0x12345678, 24)) + end) + + it("rshift composes with band to extract any byte", function() + assert.are.equal(0x34, bitops.band(bitops.rshift(0x12345678, 16), 0xff)) + assert.are.equal(0x56, bitops.band(bitops.rshift(0x12345678, 8), 0xff)) + end) + + it("band masks an IPv6 group to 16 bits", function() + assert.are.equal(0xffff, bitops.band(0x1ffff, 0xffff)) + assert.are.equal(0xabcd, bitops.band(0xabcd, 0xffff)) + end) + + it("rshift carries an IPv6 group overflow into the next group", function() + -- ipv6_add does `v = g[i] + carry; g[i] = v & 0xffff; carry = v >> 16`. + -- 0xffff + 1 = 0x10000 — the carry-out is 1, the resulting group is 0. + local v = 0xffff + 1 + assert.are.equal(0, bitops.band(v, 0xffff)) + assert.are.equal(1, bitops.rshift(v, 16)) + end) + + it("lshift round-trips with rshift for the values we use", function() + assert.are.equal(0xab00, bitops.lshift(0xab, 8)) + assert.are.equal(0xab, bitops.rshift(bitops.lshift(0xab, 8), 8)) + end) + + it("band of zero is zero", function() + assert.are.equal(0, bitops.band(0, 0xffffffff)) + assert.are.equal(0, bitops.band(0xffffffff, 0)) + end) + + it("rshift past width returns zero", function() + assert.are.equal(0, bitops.rshift(0xff, 32)) + end) + +end) + +describe("parse_sdp.grammar.bitops_compat — pure-Lua backend", function() + -- Exercised directly even on 5.3+ so the compat path is covered + -- in CI without needing every job to run a 5.1/5.2 interpreter. + + local compat = require("parse_sdp.grammar.bitops_compat") + + it("band matches reference values across our call sites", function() + assert.are.equal(0x78, compat.band(0x12345678, 0xff)) + assert.are.equal(0xffff, compat.band(0x1ffff, 0xffff)) + assert.are.equal(0, compat.band(0xaaaaaaaa, 0x55555555)) + assert.are.equal(0xff, compat.band(0xff, 0xff)) + end) + + it("rshift matches reference values", function() + assert.are.equal(0x12, compat.rshift(0x12345678, 24)) + assert.are.equal(1, compat.rshift(0x10000, 16)) + assert.are.equal(0, compat.rshift(0xff, 32)) + end) + + it("lshift matches reference values", function() + assert.are.equal(0xab00, compat.lshift(0xab, 8)) + assert.are.equal(0x100, compat.lshift(1, 8)) + end) + +end)