Skip to content

Commit d829ef8

Browse files
committed
fix packed fields not packed & default values being serialized
1 parent 9f3b352 commit d829ef8

5 files changed

Lines changed: 237 additions & 20 deletions

File tree

Protobuf/Notation/Basic.lean

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ meta def mkFreshUserName (n : Name) : CommandElabM Name := do
5151
structure Options where
5252
raw : Array (Ident × Term)
5353
entries : Std.HashMap Name (Array Term)
54-
deriving Inhabited
54+
deriving Inhabited, Repr
5555

5656
-- TODO: maybe force this?
5757
private def Options.recognized : Array Name :=
@@ -85,7 +85,7 @@ private def Options.first? (options : Options) (x : Name) : Option Term :=
8585
private def Options.is_true? (options : Options) (x : Name) : Option Bool :=
8686
if let some y := options.first? x then
8787
y matches `(true)
88-
else false
88+
else none
8989

9090
syntax options_entry := ident " = " term
9191

Protobuf/Notation/Message.lean

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -367,10 +367,10 @@ def computeMData.map [Monad m] [MonadQuotation m] [MonadError m] [MonadEnv m] [M
367367
}
368368

369369
def computeMData.ordinary.computeShape [Monad m] [MonadQuotation m] [MonadError m] [MonadEnv m] [MonadOptions m] [MonadLog m] [MonadRef m] [AddMessageContext m] [MonadResolveName m]
370-
(mod? : Modifier) (is_scalar : Bool) (lean_type_inner : Ident) : m (TSyntax `term × LeanShape) := do
370+
(mod? : Modifier) (internal_type? : Option InternalType) (enum_type? : Option Name) (lean_type_inner : Ident) : m (TSyntax `term × LeanShape) := do
371371
match mod? with
372372
| .default | .required =>
373-
if is_scalar then
373+
if internal_type?.isSome || enum_type?.isSome then
374374
pure (← `($lean_type_inner), LeanShape.strict)
375375
else
376376
pure (← `(Option $lean_type_inner), LeanShape.option)
@@ -424,7 +424,7 @@ def computeMData.ordinary [Monad m] [MonadQuotation m] [MonadError m] [MonadEnv
424424
let (is_scalar, internal_type?, enum_type?, oneof_type?) ← getProtoTypeMData mutEnums mutOneofs messages proto_type
425425
if oneof_type?.isSome && !(mod? matches .default) then
426426
throwErrorAt name "oneof field cannot have cardinality modifier: {oneof_type?.get!}"
427-
let (lean_type, lean_shape) ← computeMData.ordinary.computeShape mod? is_scalar lean_type_inner
427+
let (lean_type, lean_shape) ← computeMData.ordinary.computeShape mod? internal_type? enum_type? lean_type_inner
428428
let builder? ← internal_type?.mapM InternalType.builder
429429
let builder? := if oneof_type?.isNone then some (builder?.getD (mkIdentFrom proto_type (proto_type.getId.str "builder"))) else none
430430
let toMessage? := if is_scalar then none else some (mkIdentFrom proto_type (proto_type.getId.str "toMessage"))
@@ -551,7 +551,7 @@ end
551551
private def construct_toMessage (name : Ident) (push_name : String → Ident) (fields : Array ProtoFieldMData) : CommandElabM (Ident × Command) := do
552552
let msg ← mkIdent <$> mkFreshUserName `msg
553553
let val ← mkIdent <$> mkFreshUserName `val
554-
let toMessageBody ← fields.mapM fun {mod, field_proj, field_num, options, is_scalar, builder?, oneof_type?, toMessage?, test_unset, map_info?, ..} => do
554+
let toMessageBody ← fields.mapM fun {mod, field_proj, field_num, options, is_scalar, internal_type?, builder?, enum_type?, oneof_type?, toMessage?, test_unset, map_info?, ..} => do
555555
if let some map_info := map_info? then
556556
let entries ← mkIdent <$> mkFreshUserName `entries
557557
let submsg ← mkIdent <$> mkFreshUserName `submsg
@@ -586,7 +586,7 @@ private def construct_toMessage (name : Ident) (push_name : String → Ident) (f
586586
let builder := builder?.get!
587587
match mod with
588588
| .default =>
589-
if is_scalar then
589+
if internal_type?.isSome || enum_type?.isSome then
590590
`(Parser.Term.doSeqItem| let $msg ← do
591591
if $test_unset ($field_proj $val) then
592592
pure $msg
@@ -595,7 +595,7 @@ private def construct_toMessage (name : Ident) (push_name : String → Ident) (f
595595
else
596596
`(Parser.Term.doSeqItem| let $msg ← $field_num:num <~? (Option.mapM $builder ($field_proj $val)) # $msg)
597597
| .required =>
598-
if is_scalar then
598+
if internal_type?.isSome || enum_type?.isSome then
599599
`(Parser.Term.doSeqItem| let $msg ← do
600600
if $test_unset ($field_proj $val) then
601601
pure $msg
@@ -612,10 +612,20 @@ private def construct_toMessage (name : Ident) (push_name : String → Ident) (f
612612
| .optional =>
613613
`(Parser.Term.doSeqItem| let $msg ← $field_num:num <~? (Option.mapM $builder ($field_proj $val)) # $msg)
614614
| .repeated =>
615-
if options.packed?.getD is_scalar then
616-
`(Parser.Term.doSeqItem| let $msg ← $field_num:num <~p (Array.mapM $builder ($field_proj $val)) # $msg)
615+
if options.packed?.isEqSome true then
616+
`(Parser.Term.doSeqItem|
617+
let $msg ← do
618+
if $test_unset ($field_proj $val) then
619+
pure $msg
620+
else
621+
$field_num:num <~p (Array.mapM $builder ($field_proj $val)) # $msg)
617622
else
618-
`(Parser.Term.doSeqItem| let $msg ← $field_num:num <~f (Array.mapM $builder ($field_proj $val)) # $msg)
623+
`(Parser.Term.doSeqItem|
624+
let $msg ← do
625+
if $test_unset ($field_proj $val) then
626+
pure $msg
627+
else
628+
$field_num:num <~f (Array.mapM $builder ($field_proj $val)) # $msg)
619629
let toMessageId := push_name "toMessage"
620630
let toMessage ← `(partial def $toMessageId:ident : $name → Except Protobuf.Encoding.ProtoError Protobuf.Encoding.Message := fun $val => do
621631
let $msg:ident := Protobuf.Encoding.Message.emptyWithCapacity $(quote fields.size)
@@ -637,7 +647,7 @@ private def construct_builder (name : Ident) (push_name : String → Ident) (toM
637647
private def construct_fromMessage (name : Ident) (push_name : String → Ident) (fields : Array ProtoFieldMData) : CommandElabM (Ident × Command) := do
638648
let msg ← mkIdent <$> mkFreshUserName `msg
639649
let ns := fields.map ProtoFieldMData.field_num
640-
let decoder ← fields.mapM (β := (Ident × TSyntax ``Parser.Term.doSeqItem)) fun {mod, field_name, field_proj, field_num, options, is_scalar, oneof_type?, decoder??, decoder_rep?, decoder_rep_packed?, fromMessage??, map_info?, ..} => do
650+
let decoder ← fields.mapM (β := (Ident × TSyntax ``Parser.Term.doSeqItem)) fun {mod, field_name, field_proj, field_num, options, is_scalar, internal_type?, enum_type?, oneof_type?, decoder??, decoder_rep?, decoder_rep_packed?, fromMessage??, map_info?, ..} => do
641651
let var ← mkIdent <$> mkFreshUserName (field_name.getId)
642652
if let some map_info := map_info? then
643653
let key_decoder? := map_info.key_decoder?
@@ -668,12 +678,12 @@ private def construct_fromMessage (name : Ident) (push_name : String → Ident)
668678
let decoder? := decoder??.get!
669679
let stx ← match mod with
670680
| .default =>
671-
if is_scalar then
681+
if internal_type?.isSome || enum_type?.isSome then
672682
`(Parser.Term.doSeqItem| let $var ← ($decoder? $msg $field_num:num <&> (fun x => Option.getD x Inhabited.default)))
673683
else
674684
`(Parser.Term.doSeqItem| let $var ← ($decoder? $msg $field_num:num))
675685
| .required =>
676-
if is_scalar then
686+
if internal_type?.isSome || enum_type?.isSome then
677687
`(Parser.Term.doSeqItem| let $var ← ($decoder? $msg $field_num:num >>= (fun x => Option.getDM x
678688
(throw (Protobuf.Encoding.ProtoError.missingRequiredField s!"required field `{$(quote field_proj.getId.toString)}` is missing when decoding the message")))))
679689
else
@@ -686,7 +696,7 @@ private def construct_fromMessage (name : Ident) (push_name : String → Ident)
686696
| .optional =>
687697
`(Parser.Term.doSeqItem| let $var ← ($decoder? $msg $field_num:num))
688698
| .repeated =>
689-
if options.packed?.getD is_scalar then
699+
if options.packed?.isEqSome true then
690700
assert! decoder_rep_packed?.isSome
691701
let decoder_rep_packed := decoder_rep_packed?.get!
692702
`(Parser.Term.doSeqItem| let $var ← ($decoder_rep_packed $msg $field_num:num))
@@ -730,7 +740,7 @@ private def construct_decoder_rep (name : Ident) (push_name : String → Ident)
730740
private def construct_merge (name : Ident) (push_name : String → Ident) (fields : Array ProtoFieldMData) : CommandElabM (Ident × Command) := do
731741
let a ← mkIdent <$> mkFreshUserName `a
732742
let b ← mkIdent <$> mkFreshUserName `b
733-
let mergeBody ← fields.mapM (β := (Ident × TSyntax ``Parser.Term.doSeqItem)) fun {mod, proto_type, field_name, field_proj := field_proj, is_scalar, oneof_type?, map_info?, ..} => do
743+
let mergeBody ← fields.mapM (β := (Ident × TSyntax ``Parser.Term.doSeqItem)) fun {mod, proto_type, field_name, field_proj, internal_type?, enum_type?, oneof_type?, map_info?, ..} => do
734744
let var ← mkIdent <$> mkFreshUserName (field_name.getId)
735745
let va ← `($field_proj $a)
736746
let vb ← `($field_proj $b)
@@ -744,18 +754,16 @@ private def construct_merge (name : Ident) (push_name : String → Ident) (field
744754
else
745755
let stx ← match mod with
746756
| .default | .required =>
747-
if is_scalar then
757+
if internal_type?.isSome || enum_type?.isSome then
748758
`(Parser.Term.doSeqItem| let $var := $vb)
749-
else if (proto_type matches `(string) | `(bytes)) then
750-
`(Parser.Term.doSeqItem| let $var := $vb <|> $va) -- optional, last first
751759
else
752760
`(Parser.Term.doSeqItem| let $var := match $va:term, $vb:term with
753761
| Option.some x, Option.some y => Option.some ($merger x y)
754762
| Option.some x, _ => Option.some x
755763
| _, Option.some y => Option.some y
756764
| _, _ => Option.none)
757765
| .optional =>
758-
if is_scalar || (proto_type matches `(string) | `(bytes)) then
766+
if internal_type?.isSome || enum_type?.isSome then
759767
`(Parser.Term.doSeqItem| let $var := $vb <|> $va)
760768
else
761769
`(Parser.Term.doSeqItem| let $var := match $va:term, $vb:term with

Protobuf/Versions/Proto3.lean

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,15 @@ private def field_modifier? (field : FieldDescriptorProto) : M (Option (TSyntax
206206
else
207207
return none
208208

209+
set_option hygiene false in
209210
private def field_options? (field : FieldDescriptorProto) : M (Option (TSyntax ``options)) := do
210211
let mut entries := #[]
211212
if let some packed := field.options&.packed then
212213
entries := entries.push (← `(options_entry| packed = $(quote packed)))
214+
else
215+
if let some type := field.type then
216+
unless type matches .TYPE_STRING | .TYPE_GROUP | .TYPE_MESSAGE | .TYPE_BYTES do
217+
entries := entries.push (← `(options_entry| packed = true)) -- NOTE: proto3 defaults to packed
213218
if !! field.options&.deprecated then
214219
entries := entries.push (← `(options_entry| deprecated = true))
215220
if entries.isEmpty then

Test/Proto3.lean

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
module
2+
3+
import Protobuf.Encoding
4+
meta import Protobuf.Notation
5+
meta import Protobuf.Elab
6+
7+
open Protobuf Encoding
8+
open scoped Protobuf.Notation
9+
10+
set_option protobuf.trace.notation true
11+
12+
#load_proto_file "Test/Proto3.proto"
13+
14+
def ofExcept {α} (e : Except ProtoError α) : IO α := do
15+
match e with
16+
| .ok v => pure v
17+
| .error err => throw (IO.userError err.toString)
18+
19+
def assert (cond : Bool) (msg : String) : IO Unit := do
20+
if cond then pure () else throw (IO.userError msg)
21+
22+
def assertEq [BEq α] (a b : α) (msg : String) : IO Unit := do
23+
assert (a == b) msg
24+
25+
def testDefaults : IO Unit := do
26+
let val : _root_.test.proto3.All := default
27+
let msg ← ofExcept (_root_.test.proto3.All.toMessage val)
28+
assert msg.records.isEmpty "proto3 defaults should not serialize"
29+
let decoded ← ofExcept (_root_.test.proto3.All.fromMessage Message.empty)
30+
assertEq decoded.int32_field (0 : Int32) "default int32_field mismatch"
31+
assertEq decoded.string_field "" "default string_field mismatch"
32+
assertEq decoded.bool_field false "default bool_field mismatch"
33+
assertEq decoded.bytes_field ByteArray.empty "default bytes_field mismatch"
34+
assertEq decoded.float_field (0 : Float32) "default float_field mismatch"
35+
assertEq decoded.double_field (0 : Float) "default double_field mismatch"
36+
assertEq decoded.uint32_field (0 : UInt32) "default uint32_field mismatch"
37+
assertEq decoded.uint64_field (0 : UInt64) "default uint64_field mismatch"
38+
assertEq decoded.sint32_field (0 : Int32) "default sint32_field mismatch"
39+
assertEq decoded.sint64_field (0 : Int64) "default sint64_field mismatch"
40+
assertEq decoded.fixed32_field (0 : UInt32) "default fixed32_field mismatch"
41+
assertEq decoded.fixed64_field (0 : UInt64) "default fixed64_field mismatch"
42+
assertEq decoded.sfixed32_field (0 : Int32) "default sfixed32_field mismatch"
43+
assertEq decoded.sfixed64_field (0 : Int64) "default sfixed64_field mismatch"
44+
assertEq decoded.color _root_.test.proto3.Color.COLOR_UNSPECIFIED "default enum mismatch"
45+
assert decoded.sub.isNone "default sub mismatch"
46+
assertEq decoded.opt_int32 none "default opt_int32 mismatch"
47+
assertEq decoded.rep_int32 #[] "default rep_int32 mismatch"
48+
assertEq decoded.rep_int32_unpacked #[] "default rep_int32_unpacked mismatch"
49+
assertEq decoded.rep_color #[] "default rep_color mismatch"
50+
assert (decoded.map_str_int32.size == 0) "default map mismatch"
51+
assert decoded.choice.isNone "default choice mismatch"
52+
assert decoded.rep_sub.isEmpty "default rep_sub mismatch"
53+
54+
def testOptionalPresence : IO Unit := do
55+
let base : _root_.test.proto3.All := default
56+
let val : _root_.test.proto3.All := { base with opt_int32 := some 0 }
57+
let msg ← ofExcept (_root_.test.proto3.All.toMessage val)
58+
assert ((Message.getRecordsOf msg 17).size == 1) "optional field should serialize when set"
59+
let decoded ← ofExcept (_root_.test.proto3.All.fromMessage msg)
60+
assertEq decoded.opt_int32 (some (0 : Int32)) "optional presence mismatch"
61+
62+
def testSubPresence : IO Unit := do
63+
let sub : _root_.test.proto3.Sub := { id := 0 }
64+
let base : _root_.test.proto3.All := default
65+
let val : _root_.test.proto3.All := { base with sub := some sub }
66+
let msg ← ofExcept (_root_.test.proto3.All.toMessage val)
67+
assert ((Message.getRecordsOf msg 16).size == 1) "message field should serialize when present"
68+
let decoded ← ofExcept (_root_.test.proto3.All.fromMessage msg)
69+
match decoded.sub with
70+
| some v => assertEq v.id (0 : Int32) "message field presence mismatch"
71+
| none => throw (IO.userError "message field presence mismatch")
72+
73+
def testOneof : IO Unit := do
74+
let choice : _root_.test.proto3.All.choice_Type := _root_.test.proto3.All.choice_Type.oneof_int32 7
75+
let base : _root_.test.proto3.All := default
76+
let val : _root_.test.proto3.All := { base with choice := some choice }
77+
let msg ← ofExcept (_root_.test.proto3.All.toMessage val)
78+
assert ((Message.getRecordsOf msg 22).size == 1) "oneof field did not serialize"
79+
let decoded ← ofExcept (_root_.test.proto3.All.fromMessage msg)
80+
match decoded.choice with
81+
| some (.oneof_int32 v) => assertEq v (7 : Int32) "oneof int32 value mismatch"
82+
| _ => throw (IO.userError "oneof decode mismatch")
83+
84+
def testPackedAndUnpacked : IO Unit := do
85+
let base : _root_.test.proto3.All := default
86+
let val : _root_.test.proto3.All := {
87+
base with
88+
rep_int32 := #[(1 : Int32), 2],
89+
rep_int32_unpacked := #[(3 : Int32), 4]
90+
}
91+
let msg ← ofExcept (_root_.test.proto3.All.toMessage val)
92+
let repPacked := Message.getRecordsOf msg 18
93+
assert (repPacked.size == 1) "packed repeated field should be a single record"
94+
match repPacked[0]!.value with
95+
| .LEN _ => pure ()
96+
| _ => throw (IO.userError "packed repeated field should use LEN wire type")
97+
let repUnpacked := Message.getRecordsOf msg 19
98+
assert (repUnpacked.size == 2) "unpacked repeated field should be multiple records"
99+
for r in repUnpacked do
100+
match r.value with
101+
| .VARINT _ => pure ()
102+
| _ => throw (IO.userError "unpacked repeated field should use VARINT wire type")
103+
let decoded ← ofExcept (_root_.test.proto3.All.fromMessage msg)
104+
assertEq decoded.rep_int32 #[(1 : Int32), 2] "packed repeated decode mismatch"
105+
assertEq decoded.rep_int32_unpacked #[(3 : Int32), 4] "unpacked repeated decode mismatch"
106+
107+
def testPackedAcceptsUnpacked : IO Unit := do
108+
let msg := Message.set Message.empty 18 (.VARINT 1)
109+
let msg := Message.set msg 18 (.VARINT 2)
110+
let decoded ← ofExcept (_root_.test.proto3.All.fromMessage msg)
111+
assertEq decoded.rep_int32 #[(1 : Int32), 2] "packed field should accept unpacked encoding"
112+
113+
def testMapRoundtrip : IO Unit := do
114+
let map := Std.HashMap.ofList [("a", (1 : Int32)), ("b", (2 : Int32))]
115+
let base : _root_.test.proto3.All := default
116+
let val : _root_.test.proto3.All := { base with map_str_int32 := map }
117+
let msg ← ofExcept (_root_.test.proto3.All.toMessage val)
118+
let decoded ← ofExcept (_root_.test.proto3.All.fromMessage msg)
119+
assertEq (decoded.map_str_int32.get? "a") (some (1 : Int32)) "map value mismatch for key a"
120+
assertEq (decoded.map_str_int32.get? "b") (some (2 : Int32)) "map value mismatch for key b"
121+
122+
def testUnknownEnum : IO Unit := do
123+
let msg := Message.set Message.empty 15 (.VARINT 9)
124+
let decoded ← ofExcept (_root_.test.proto3.All.fromMessage msg)
125+
match decoded.color with
126+
| _root_.test.proto3.Color.«Unknown.Value» raw =>
127+
assertEq raw (9 : Int32) "unknown enum raw value mismatch"
128+
| _ => throw (IO.userError "unknown enum value should be preserved")
129+
130+
def testUnknownFields : IO Unit := do
131+
let msg := Message.set Message.empty 99 (.VARINT 123)
132+
let decoded ← ofExcept (_root_.test.proto3.All.fromMessage msg)
133+
match decoded.«Unknown.Fields».get? 99 with
134+
| some vals => assert (vals.size == 1) "unknown field should be preserved"
135+
| none => throw (IO.userError "unknown field missing")
136+
let roundtrip ← ofExcept (_root_.test.proto3.All.toMessage decoded)
137+
assert ((Message.getRecordsOf roundtrip 99).size == 1) "unknown field should round-trip"
138+
139+
def runTest (name : String) (t : IO Unit) (errs : IO.Ref (Array String)) : IO Unit := do
140+
try
141+
t
142+
catch e =>
143+
errs.modify (·.push s!"{name}: {e.toString}")
144+
145+
def testProto3 : IO Unit := do
146+
let errs ← IO.mkRef #[]
147+
runTest "testDefaults" testDefaults errs
148+
runTest "testOptionalPresence" testOptionalPresence errs
149+
runTest "testSubPresence" testSubPresence errs
150+
runTest "testOneof" testOneof errs
151+
runTest "testPackedAndUnpacked" testPackedAndUnpacked errs
152+
runTest "testPackedAcceptsUnpacked" testPackedAcceptsUnpacked errs
153+
runTest "testMapRoundtrip" testMapRoundtrip errs
154+
runTest "testUnknownEnum" testUnknownEnum errs
155+
runTest "testUnknownFields" testUnknownFields errs
156+
let failures ← errs.get
157+
unless failures.isEmpty do
158+
let msg := String.intercalate "\n" failures.toList
159+
throw (IO.userError msg)
160+
161+
#eval! testProto3

Test/Proto3.proto

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
syntax = "proto3";
2+
3+
package test.proto3;
4+
5+
message Sub {
6+
int32 id = 1;
7+
}
8+
9+
enum Color {
10+
COLOR_UNSPECIFIED = 0;
11+
RED = 1;
12+
GREEN = 2;
13+
}
14+
15+
message All {
16+
int32 int32_field = 1;
17+
string string_field = 2;
18+
bool bool_field = 3;
19+
bytes bytes_field = 4;
20+
float float_field = 5;
21+
double double_field = 6;
22+
uint32 uint32_field = 7;
23+
uint64 uint64_field = 8;
24+
sint32 sint32_field = 9;
25+
sint64 sint64_field = 10;
26+
fixed32 fixed32_field = 11;
27+
fixed64 fixed64_field = 12;
28+
sfixed32 sfixed32_field = 13;
29+
sfixed64 sfixed64_field = 14;
30+
Color color = 15;
31+
Sub sub = 16;
32+
optional int32 opt_int32 = 17;
33+
repeated int32 rep_int32 = 18;
34+
repeated int32 rep_int32_unpacked = 19 [packed = false];
35+
repeated Color rep_color = 20;
36+
map<string, int32> map_str_int32 = 21;
37+
oneof choice {
38+
int32 oneof_int32 = 22;
39+
string oneof_string = 23;
40+
Sub oneof_sub = 24;
41+
}
42+
repeated Sub rep_sub = 25;
43+
}

0 commit comments

Comments
 (0)