diff --git a/cmd/protoc-gen-tarantool/internal/gen/inline.go b/cmd/protoc-gen-tarantool/internal/gen/inline.go index b37029aee3afafbd74f985664ee3150d3c80b002..f484950fad89474d6ed124eca303331aeb7c1dfe 100644 --- a/cmd/protoc-gen-tarantool/internal/gen/inline.go +++ b/cmd/protoc-gen-tarantool/internal/gen/inline.go @@ -668,12 +668,20 @@ // scalarNotDefaultExpr returns a Lua expression that evaluates true when // the value `v` is NOT the proto3 default for the given scalar type. // Default is elided on encode. +// +// Floats and doubles need a sign-bit guard: -0.0 == 0.0 in IEEE, but +// they aren't the proto3 default (the wire bytes differ, and the +// TextFormatInput conformance corpus pins this). `1/v == -math.huge` +// is the standard sign-bit probe — division by +0 yields +inf, by -0 +// yields -inf, and any non-zero value short-circuits via `v ~= 0`. func scalarNotDefaultExpr(scalar, v string) string { switch scalar { case "string", "bytes": return v + ` ~= ''` case "bool": return v + ` ~= false` + case "float", "double": + return "(" + v + ` ~= 0 or 1/` + v + " == -math.huge)" } return v + ` ~= 0` } diff --git a/examples/expected/full/hello/hello_pb.lua b/examples/expected/full/hello/hello_pb.lua index cb8ba6a8263931c3cb05d95204dfbf91fc69783e..84a726663930122415876dff565193bd776fcc7f 100644 --- a/examples/expected/full/hello/hello_pb.lua +++ b/examples/expected/full/hello/hello_pb.lua @@ -758,7 +758,7 @@ n = n + 1; out[n] = wire.encode_sint32(v) end -- field 11: weight_kg v = t.weight_kg - if v ~= nil and v ~= 0 then + if v ~= nil and (v ~= 0 or 1/v == -math.huge) then n = n + 1; out[n] = "\x59" n = n + 1; out[n] = wire.encode_double(v) end diff --git a/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua b/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua index 9203803c8a8e90f14a33c01482a5ff7eb6c53791..55278963b5920baaa5021cc5200065cd5e2c3c88 100644 --- a/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua +++ b/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua @@ -496,13 +496,13 @@ n = n + 1; out[n] = wire.encode_sfixed64(v) end -- field 11: optional_float v = t.optional_float - if v ~= nil and v ~= 0 then + if v ~= nil and (v ~= 0 or 1/v == -math.huge) then n = n + 1; out[n] = "\x5d" n = n + 1; out[n] = wire.encode_float(v) end -- field 12: optional_double v = t.optional_double - if v ~= nil and v ~= 0 then + if v ~= nil and (v ~= 0 or 1/v == -math.huge) then n = n + 1; out[n] = "\x61" n = n + 1; out[n] = wire.encode_double(v) end @@ -1360,7 +1360,7 @@ if _k ~= 0 then _m = _m + 1; entry[_m] = _ktag _m = _m + 1; entry[_m] = wire.encode_int32(_k) end - if _val ~= 0 then + if (_val ~= 0 or 1/_val == -math.huge) then _m = _m + 1; entry[_m] = _vtag _m = _m + 1; entry[_m] = wire.encode_float(_val) end @@ -1378,7 +1378,7 @@ if _k ~= 0 then _m = _m + 1; entry[_m] = _ktag _m = _m + 1; entry[_m] = wire.encode_int32(_k) end - if _val ~= 0 then + if (_val ~= 0 or 1/_val == -math.huge) then _m = _m + 1; entry[_m] = _vtag _m = _m + 1; entry[_m] = wire.encode_double(_val) end diff --git a/runtime/pb/codec.lua b/runtime/pb/codec.lua index a9549193bd4e765ee331ae568b0eaf2db65967b8..b94d6facf2859f0e3a610486b193feee56328608 100644 --- a/runtime/pb/codec.lua +++ b/runtime/pb/codec.lua @@ -44,7 +44,11 @@ return v == '' elseif proto_type == 'bool' then return v == false elseif proto_type == 'float' or proto_type == 'double' then - return v == 0 + -- -0.0 == 0.0 in IEEE but they encode to different bytes; the + -- proto3 conformance suite pins that -0.0 round-trips intact. + -- `1/v == math.huge` is the sign-bit probe — +0 yields +inf, + -- -0 yields -inf. + return v == 0 and 1 / v == math.huge elseif proto_type == 'int64' or proto_type == 'uint64' or proto_type == 'sint64' or proto_type == 'fixed64' or proto_type == 'sfixed64' then @@ -440,6 +444,21 @@ -- fixed32/sfixed32/fixed64/sfixed64/float/double). For cdata -- 64-bit values, `v == 0` is the LuaJIT-canonical default -- check — it works across UINT64 / INT64 because cdata-to- -- number comparison normalizes via int64. + -- + -- Float/double need a sign-bit guard so -0.0 isn't elided. `1/v + -- == -math.huge` short-circuits via the prior `v == 0` test, so + -- the cost lands only on the rare zero-valued field — and the + -- proto3 conformance corpus pins that -0 must survive a + -- round-trip. + if proto_type == 'float' or proto_type == 'double' then + return function(data, out) + local v = data[fname] + if v == nil or (v == 0 and 1 / v == math.huge) then return end + local n = #out + out[n + 1] = tag_bytes + out[n + 2] = encode_value(v) + end + end return function(data, out) local v = data[fname] if v == nil or v == 0 then return end diff --git a/runtime/pb/text.lua b/runtime/pb/text.lua index 5b7551ad0fda33e3a31203691a74c8ce2c4b199e..dfa3d31f9fe5b52f195b53386fddf2e2a3456743 100644 --- a/runtime/pb/text.lua +++ b/runtime/pb/text.lua @@ -104,6 +104,12 @@ if f.kind == 'scalar' then local pt = f.proto_type if pt == 'string' or pt == 'bytes' then return v == '' end if pt == 'bool' then return v == false end + if pt == 'float' or pt == 'double' then + -- -0.0 == 0.0 in IEEE but proto3 only elides *positive* zero + -- (the wire bytes for -0 differ and the conformance suite + -- pins it). `1/v == math.huge` is the sign-bit probe. + return v == 0 and 1 / v == math.huge + end if type(v) == 'cdata' then return v == INT64_ZERO or v == UINT64_ZERO end diff --git a/test/conformance/known_failures_text.txt b/test/conformance/known_failures_text.txt index b65812bc2aecba1982745410edbe14d21d36ec13..2b3caf5f270f55b9ab410147f011caa6e32c9513 100644 --- a/test/conformance/known_failures_text.txt +++ b/test/conformance/known_failures_text.txt @@ -1,31 +1,6 @@ # conformance_test_runner --text_format_failure_list # -# After the pb.text.decode slice landed, the text-format INPUT path is -# wired through pb.text.decode in cmd/conformance/core.lua. The proto3 -# TextFormatInput suite climbed from 8 ✓ / 426 skipped to 406 ✓ / 18 -# skipped (the residual 18 are the proto2 message-type bucket — those -# round-trip through TestAllTypesProto2 which we don't generate Lua for). -# -# The entries below are the 10 known failures that survive. All ten -# share the same root cause: proto3's "default value is implicit-absence" -# rule, applied uniformly by the codec, drops a singular float/double -# field whose value is *negative zero* — `-0.0 == 0.0` in IEEE, so the -# `if v ~= 0 then emit` guard elides it. Mainline's reference output -# keeps the field, producing a `optional_float: -0` line we don't emit. -# -# This is a wire-codec quirk, not a text-format-decoder bug — the -# decoder correctly parses `-0` and `-1e-50` to a float with the sign -# bit set (verified by inspecting the FFI uint32 reinterpretation). The -# fix lives in `runtime/pb/codec.lua` (and the inline-mode codegen) and -# needs a "sign bit set" test for floats/doubles in addition to the -# `v ~= 0` check. Out of scope for the pb.text.decode work. -Required.Proto3.TextFormatInput.FloatFieldNegativeZero.ProtobufOutput -Required.Proto3.TextFormatInput.FloatFieldNegativeZero.TextFormatOutput -Required.Proto3.TextFormatInput.FloatFieldNegativeZero_F.ProtobufOutput -Required.Proto3.TextFormatInput.FloatFieldNegativeZero_F.TextFormatOutput -Required.Proto3.TextFormatInput.FloatFieldNegativeZero_f.ProtobufOutput -Required.Proto3.TextFormatInput.FloatFieldNegativeZero_f.TextFormatOutput -Required.Proto3.TextFormatInput.NegDoubleFieldLargeNegativeExponentParsesAsNegZero.ProtobufOutput -Required.Proto3.TextFormatInput.NegDoubleFieldLargeNegativeExponentParsesAsNegZero.TextFormatOutput -Required.Proto3.TextFormatInput.NegFloatFieldLargeNegativeExponentParsesAsNegZero.ProtobufOutput -Required.Proto3.TextFormatInput.NegFloatFieldLargeNegativeExponentParsesAsNegZero.TextFormatOutput +# pb.text.decode + the negative-zero codec fix close the proto3 +# TextFormatInput suite: 416 ✓ / 18 skipped / 0 expected failures. +# The 18 skipped tests are the residual proto2 message-type bucket +# (TestAllTypesProto2 — we don't generate Lua for it). diff --git a/test/protobuf_test.lua b/test/protobuf_test.lua index 82fbcf55791207ab06799bc1551894c750b73b77..dad03234a77c0abe227246f4ea8618518c1b7a96 100644 --- a/test/protobuf_test.lua +++ b/test/protobuf_test.lua @@ -324,6 +324,31 @@ local enc = hello.Person_encode({emails = {'a', 'b', 'c'}}) -- 3 occurrences of: tag(field=3, LEN)=0x1a, len=1, ascii. t.assert_equals(hex(enc), '1a01611a01621a0163') end + + g.test_negative_zero_float_preserved = function() + -- Proto3 default-elision treats `v == 0` as default, but IEEE + -- `-0.0 == 0.0`. The codec must not drop -0.0 — the wire bytes + -- differ (high bit set) and the TextFormatInput conformance + -- suite pins this. Both float and double paths apply. + -- + -- Compute -0.0 at runtime (`-0.0 * 1` or `0/-math.huge`); the + -- literal `-0.0` can be constant-folded to integer 0 by the + -- LuaJIT parser in some load paths, which would hide the bug. + local neg_zero = 0 / -math.huge + local enc = hello.Person_encode({weight_kg = neg_zero}) + t.assert(#enc > 0, 'expected -0.0 to be emitted, got empty payload') + local dec = hello.Person_decode(enc) + t.assert_equals(dec.weight_kg, 0) + t.assert_equals(1 / dec.weight_kg, -math.huge, + 'expected sign bit preserved through round-trip') + end + + g.test_positive_zero_float_still_elided = function() + -- +0.0 IS the proto3 default — the codec should drop it (only the + -- sign-bit-set case earns presence). + local enc = hello.Person_encode({weight_kg = 0.0}) + t.assert_equals(#enc, 0) + end end -- ---------------------------------------------------------------------------