fix(java): dispatch fromLua on actual Lua type tag, not coercion (#175) - #176
Merged
Conversation
luaj's isnumber()/isstring() implement Lua coercion semantics, not type
identity: LuaString.isnumber() is true for any numeric-looking string,
so fromLua routed Lua strings through the number branch. That lost
string identity for every numeric string ("42" -> 42, "007" -> 7) and
corrupted values past 2^53 through the todouble() round-trip
("1777288596209286259" -> 1777288596209286144) — silently diverging
from Go (gopher-lua/wangshu type switch) and C++ (lua_type switch),
which both dispatch on the actual type tag.
Swapping the check order would break the reverse direction
(LuaNumber.isstring() is also true); v.type() == TNUMBER/TSTRING is the
only unambiguous dispatch, and the same function's table-key check
already used it.
TransformByLuaTypeIdentityTest pins the returned Java class — which
fixture comparators cannot see (they stringify) — across the danger
zones: 19-digit IDs, 2^53+1, leading zeros, table mixes, the toLua
round-trip, and real numbers/arithmetic coercion staying numeric.
Four operator-level cases (19-digit ID, 2^53+1, leading zeros, table of numeric strings) and one pipeline fixture running both item and common mode. The pipeline fixture feeds cross-validate sections 3/9, so the Go/Java/C++ outputs are compared byte-exact — the divergence class from issue #175 was invisible to every existing fixture because none returned a numeric-looking string from Lua. Verified red on the pre-fix Java runtime (PipelineFixtureTest fails, RunCli emits 1777288596209286144 where Go emits the string) and green on all three runtimes after the fix.
Add the three #175 danger-zone strings (19-digit ID, 2^53+1, "007") to EDGE_SCALARS and to the tag/name item-field pool, plus an identity pass-through Lua function so generated pipelines can route those strings through toLua -> fromLua. Before this the fuzzer could not express the divergence class at all: every LUA_* function returned arithmetic results, and no generated scalar was a numeric-looking string. The combination (identity function x numeric-string item_name) fires in roughly 0.25% of rounds — a background-fuzz safety net behind the deterministic fixtures, not the primary regression gate.
…le (review)
Local blind review flagged the identity path's ~0.25% trigger rate.
Investigating exposed a deeper blind spot: generated configs carried no
flow_contract, so every engine projected common/items down to {} — the
differential comparison saw exit codes, error text, and item counts,
but never a single computed field VALUE. The #175 corruption could not
reach the output diff at all.
Two changes: ~40% of rounds now emit a flow_contract projecting all
accumulated outputs (output-side only; empty inputs keep request
validation out of the way, dedicated error-path rounds cover that), and
item-mode Lua rounds force the identity pass-through on an upstream
name/tag field 25% of the time instead of relying on pool selection.
Verified: pre-fix Java + this generator diverges from Go on a real
generated round (seed 424242 round 2066, normalize_json unequal);
fixed Java matches; 500 fresh rounds pass with no false positives.
The comment claimed a 'guaranteed per-round injection'; the actual behavior forces the identity pass-through with 25% probability and only when an item-mode round has a name/tag field upstream. Comment-only.
Two optional-but-cheap hardenings from the final full-range review: - transform_by_lua_edge_cases.json gains a _comment stating its layer boundary: the operator-fixture comparators stringify non-float values, so these cases pin value-level parity only; type identity lives in the Java unit test and the pipeline fixture (verified the unknown key is tolerated by both Go and Java fixture loaders). - lua_string_number_identity.json seeds two items instead of one so the item-mode path proves per-item type preservation, not just a single invocation. Re-verified: Go both Lua backends, Java, and a raw three-runtime byte comparison all pass.
) Reflection: luaj's is*() family implements coercion queries, not type predicates — same-looking APIs across the three Lua host libraries have opposite semantics (gopher-lua type switch / wangshu kind tags = tags, luaj is* = coercion). The deeper lesson: the differential fuzzer never emitted flow_contract, so engines projected all outputs to {} and the comparison never saw a computed field value — measure effective visibility at the comparison surface, not shape occurrence. Promoted to stable docs: operator-contract.md (scalar dispatch must use real type tags, three-host-library semantics table), ci-quality-baseline.md (new-fuzz-dimension signal-reaches-comparison-surface rule + red/green verification recipe), cross-layer-validation.md (fixture comparator semantics define what each layer can pin). index.md synced.
Contributor
Codex PR 审查
审查截止: 0d2af3c 实现正确地改用 Lua 实际类型标签进行分派,并通过单元测试、流水线 fixture 和可观测的差分 fuzz 路径覆盖了该回归,未发现合入风险。
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
TransformByLua.fromLuadispatched Lua scalars on luaj'sisnumber()/isstring(), which implement Lua coercion semantics, not type identity:LuaString.isnumber()is true for any numeric-looking string, andLuaNumber.isstring()is true for every number. A Lua string like"1777288596209286259"therefore took the number branch — losing string type identity ("42"→42,"007"→7) and, past 2^53, corrupting the value itself through thetodouble()round-trip ("1777288596209286259"→1777288596209286144) — silently diverging from Go (gopher-lua type switch, wangshu kind tags) and C++ (lua_type()switch), which dispatch on actual type tags.Fix: dispatch on
v.type() == TNUMBER / TSTRING— the only unambiguous dispatch (swapping the check order would break the reverse direction), and the same pattern the function's own table-key check already used.Closes #175.
Changes
fromLuascalar dispatch on real type tags;TransformByLuaTypeIdentityTest(8 cases) pins the returned Java class (assertInstanceOf) across the danger zones — 19-digit IDs, 2^53+1, leading zeros, table mixes, thetoLuaround-trip, and real numbers / arithmetic coercion staying numeric. Verified red on pre-fix code (6/8 fail), green after.fixtures/pipelines/lua_string_number_identity.json(item + common mode, 2 seed items for per-item coverage) — feeds cross-validate sections 3/9's type-preserving byte-exact comparison across all three runtimes. A_commentdocuments the fixture layering boundary (operator-fixture comparators stringify, so they pin value-level parity only).flow_contract, so every engine projected outputs to{}— the comparison saw exit codes and counts but never a computed field value, making this entire bug class invisible by construction. Now ~40% of rounds project all accumulated outputs, and the Lua identity pass-through is forced (25%) when a name/tag field is upstream. Detection verified end-to-end: pre-fix Java diverges from Go on a real generated round; fixed Java matches; 500 fresh rounds with no false positives.is*semantics table; "fuzz signal must reach the comparison surface" rule; fixture-comparator layering guide).Review
Closed a full local independent review loop (close-local-code-review): 5 blind reviewer runs in fixed-commit snapshot clones (no inherited context) + history supplement + terminal audit receipt — final full-range blind report 0 blocking / 0 important / 0 minor. All 6 interim findings fixed or independently disproved; the one pre-existing out-of-scope item (
snapshotKeyscoercionisstring()atTransformByLua.java:429) is noted for a separate issue.Testing
mvn testgreen (310); new test red-before/green-after verifiedgo test ./...green on both Lua backends (wangshu +lua_gopher)make bumpfull validation 306 PASS