~bigbes/tarantool

tarantool-protobuf

b75b87912eb710e71f73380e6b98c312f25a74d5 — Eugene Blikh 3 months ago d6570ec
codegen: EmmyLua type annotations for messages, enums, wrappers

Generated Lua modules now carry lua-language-server type annotations:
  ---@alias <full.Enum> integer            per enum
  ---@class <full.Message>                  per message
  ---@field <name> <type>                   per field
  ---@param / ---@return                    per wrapper

Mappings:
  bool                       -> boolean
  string / bytes             -> string
  float / double             -> number
  all int kinds              -> integer  (64-bit cdata typed as integer;
                                          LSP has no cdata model)
  enum / message             -> <full.Name>  (resolves to declared alias/class)
  repeated T                 -> T[]
  map<K,V>                   -> table<K, V>

Presence markers (trailing `?` on field name):
  proto3 explicit optional
  oneof branches             (only one is set at a time)

Wrapper signatures cover _new / _encode / _decode / _decode_lazy plus
_has_<field> / _clear_<field> on optional fields. _decode_lazy returns
pb.MessageView, which is declared inline in runtime/pb/lazy.lua along
with pb.ArrayView and pb.MapView so cross-file references resolve in
any project that requires('pb.lazy').

Class identifiers use proto full names verbatim (e.g. `hello.Person`)
so cross-file imports and WKT references both resolve to a single
declared `---@class` block — no per-module renaming needed.

Pure comment addition: 300/300 luatest + 19/19 jit-trace gate stay
green. Generated examples regenerated and committed.
M PLAN.md => PLAN.md +11 -1
@@ 212,8 212,18 @@ fiber and bridges client ↔ handler via `fiber.channel`. All four flavors

### M7 — Developer ergonomics

- [ ] Generated EmmyLua / lua-language-server type annotations so
- [x] Generated EmmyLua / lua-language-server type annotations so
      `t:Person_encode({name=...})` autocompletes in editors.
      Codegen emits `---@class <full.Name>` per message (with one
      `---@field` per field), `---@alias <full.Name> integer` per
      enum, and `---@param` / `---@return` on every `_new`, `_encode`,
      `_decode`, `_decode_lazy`, `_has_*`, `_clear_*` wrapper. Class
      identifiers use proto full names verbatim so cross-file
      references resolve. Lazy view types (`pb.MessageView`,
      `pb.ArrayView`, `pb.MapView`) are declared inline in
      `runtime/pb/lazy.lua` so the LSP sees them. Pure comment
      addition — no runtime impact, 300/300 luatest + 19/19
      jit-trace gate stay green.
- [ ] `pb.from_pb(file_descriptor_set)` — runtime descriptor parser, lets
      apps load schemas at runtime without protoc-time codegen.
- [ ] JSON encoding per the [proto3 JSON spec][proto3json] (canonical and

A cmd/protoc-gen-tarantool/internal/gen/emmylua.go => cmd/protoc-gen-tarantool/internal/gen/emmylua.go +209 -0
@@ 0,0 1,209 @@
// EmmyLua / lua-language-server annotation emission.
//
// Annotations are pure comments — they have no runtime effect — but
// they teach the Lua language server about generated message shapes,
// enum aliases, and wrapper function signatures. Users of generated
// code get autocomplete, type-checking, and rename refactoring for
// free in any editor that consumes EmmyLua (VS Code + sumneko, Neovim
// LSP, JetBrains EmmyLua plugin).
//
// Output (per file):
//   1. ---@alias <pkg.Enum> integer        (one per enum)
//   2. ---@class <pkg.Message>             (one per message)
//      ---@field <name> <type>
//      ...
//   3. ---@param / ---@return on each emitted M.<Name>_* function.
//
// All three blocks are emitted by gen.go at well-defined points so
// they live alongside the code they describe.
//
// Type mapping (proto -> EmmyLua):
//   - bool                -> boolean
//   - string / bytes      -> string
//   - float / double      -> number
//   - all int kinds       -> integer  (64-bit are uint64_t/int64_t cdata
//                                     at runtime; LSP has no cdata model,
//                                     so they're typed as integer with
//                                     an inline note)
//   - enum<Name>          -> <Name>          (alias declared elsewhere)
//   - message<Name>       -> <Name>          (class declared elsewhere)
//   - repeated<T>         -> T[]
//   - map<K,V>            -> table<K, V>
//
// Field presence:
//   - proto3 explicit `optional`   -> trailing `?` on the field name
//   - oneof branch                 -> trailing `?` (only one is set at a time)
//   - everything else              -> no marker (proto3 defaults imply
//                                     a typed zero value if absent)

package gen

import (
	"fmt"
	"strings"

	"google.golang.org/protobuf/compiler/protogen"
	"google.golang.org/protobuf/reflect/protoreflect"
)

// emmyTypeName returns the qualified EmmyLua class/alias identifier for a
// message or enum. We use the proto full name verbatim (with dots), which
// lua-language-server accepts as a class identifier. This keeps cross-file
// references straightforward — every file referring to "hello.Person"
// resolves to the same `---@class hello.Person` block, regardless of which
// generated `.lua` declared it.
func emmyTypeName(td protoreflect.Descriptor) string {
	return string(td.FullName())
}

// emmyScalarType maps a proto3 scalar kind to its EmmyLua surface type.
func emmyScalarType(k protoreflect.Kind) string {
	switch k {
	case protoreflect.BoolKind:
		return "boolean"
	case protoreflect.StringKind, protoreflect.BytesKind:
		return "string"
	case protoreflect.FloatKind, protoreflect.DoubleKind:
		return "number"
	default:
		// All int kinds. 64-bit are int64_t/uint64_t cdata at runtime,
		// but EmmyLua has no model for that; users either compare via
		// pb.to_int64/pb.to_uint64 or convert with tonumber for small
		// values. Typing as integer matches user mental model.
		return "integer"
	}
}

// emmyFieldType returns the surface type for one field, accounting for
// kind (scalar/enum/message/map) and the repeated modifier.
func emmyFieldType(f *protogen.Field) string {
	if f.Desc.IsMap() {
		kf := f.Message.Fields[0]
		vf := f.Message.Fields[1]
		return fmt.Sprintf("table<%s, %s>",
			emmyFieldTypeSingular(kf), emmyFieldTypeSingular(vf))
	}
	t := emmyFieldTypeSingular(f)
	if f.Desc.IsList() {
		return t + "[]"
	}
	return t
}

func emmyFieldTypeSingular(f *protogen.Field) string {
	switch {
	case f.Message != nil:
		return emmyTypeName(f.Message.Desc)
	case f.Enum != nil:
		return emmyTypeName(f.Enum.Desc)
	default:
		return emmyScalarType(f.Desc.Kind())
	}
}

// emmyFieldOptional reports whether the field should carry a trailing `?`.
// Two cases: proto3 explicit optional, and oneof branches (only one is
// set at a time, so every branch is presence-tracked).
func emmyFieldOptional(f *protogen.Field) bool {
	if f.Desc.HasOptionalKeyword() {
		return true
	}
	if f.Oneof != nil && !f.Desc.HasOptionalKeyword() {
		return true
	}
	return false
}

// emitEmmyEnumAlias emits `---@alias <pkg.Enum> integer` for each enum.
// Could be tightened to `<value> | <value> | ...` but that locks the
// schema in the annotation; users typically write `M.Status.OK` (an
// integer literal) so `integer` is the honest type.
func emitEmmyEnumAlias(w *writer, e *protogen.Enum) {
	w.line("---@alias %s integer", emmyTypeName(e.Desc))
}

// emitEmmyMessageClass emits the `---@class` block for one message,
// listing every field with its surface type and optional marker.
func emitEmmyMessageClass(w *writer, m *protogen.Message) {
	w.line("---@class %s", emmyTypeName(m.Desc))
	for _, f := range m.Fields {
		name := string(f.Desc.Name())
		if emmyFieldOptional(f) {
			name = name + "?"
		}
		w.line("---@field %s %s", name, emmyFieldType(f))
	}
}

// emitEmmyWrappersHeader prefaces the M.<Name>_new / _encode / _decode /
// _decode_lazy / has_/clear_ stubs with their EmmyLua annotations. Each
// wrapper gets its own `---@param` / `---@return` lines printed
// immediately before the corresponding `function M.<Name>_*(...)` line.
//
// emitMessageWrappers (runtime mode) and emitInlineMessage (full mode)
// each call into this for the typed prelude; the function bodies stay
// where they were.
func emitEmmyWrapperAnnotations(w *writer, name string, fullName string, kind emmyWrapperKind) {
	t := fullName
	switch kind {
	case wrapperNew:
		w.line("---@param t? %s", t)
		w.line("---@return %s", t)
	case wrapperEncode:
		w.line("---@param t %s", t)
		w.line("---@return string")
	case wrapperDecode:
		w.line("---@param b string")
		w.line("---@return %s", t)
	case wrapperDecodeLazy:
		w.line("---@param b string")
		w.line("---@return pb.MessageView")
	case wrapperHas:
		w.line("---@param t %s", t)
		w.line("---@return boolean")
	case wrapperClear:
		w.line("---@param t %s", t)
	}
}

type emmyWrapperKind int

const (
	wrapperNew emmyWrapperKind = iota
	wrapperEncode
	wrapperDecode
	wrapperDecodeLazy
	wrapperHas
	wrapperClear
)

// emitEmmyTypesBlock emits the file's whole types section: every enum
// alias, every message class. Called once per file by GenerateFile,
// after the descriptor pre-declarations and before the wrappers.
func emitEmmyTypesBlock(w *writer, enums []*protogen.Enum, msgs []*protogen.Message) {
	if len(enums) == 0 && len(msgs) == 0 {
		return
	}
	w.line("-- EmmyLua / lua-language-server type annotations.")
	w.line("-- These are comments — no runtime effect. They give editors")
	w.line("-- autocomplete and type-checking for the generated wrappers.")
	for _, e := range enums {
		emitEmmyEnumAlias(w, e)
	}
	if len(enums) > 0 {
		w.line("")
	}
	for i, m := range msgs {
		emitEmmyMessageClass(w, m)
		if i < len(msgs)-1 {
			w.line("")
		}
	}
	w.line("")
}

// emmyMessageFullName is a convenience wrapper used by the wrapper
// emitters to resolve the right `---@class` identifier for a message.
func emmyMessageFullName(m *protogen.Message) string {
	return strings.TrimSpace(string(m.Desc.FullName()))
}

M cmd/protoc-gen-tarantool/internal/gen/gen.go => cmd/protoc-gen-tarantool/internal/gen/gen.go +15 -2
@@ 92,6 92,11 @@ func GenerateFile(plug *protogen.Plugin, file *protogen.File, cfg Config) error 
		emitMessageFields(w, file, m, imports, cfg.Prefix)
	}

	// 3a) EmmyLua type annotations (---@class per message, ---@alias per
	// enum). Emitted between the descriptors and the wrappers so the
	// wrapper annotations a few lines down can reference these class names.
	emitEmmyTypesBlock(w, allEnums, allMsgs)

	// 4) Wrappers: _new / _encode / _decode.
	for _, m := range allMsgs {
		switch cfg.Mode {


@@ 375,23 380,31 @@ func typeRef(file *protogen.File, td protoreflect.Descriptor, selfPath string, i
// has_<field> / clear_<field> for each explicit-optional field.
func emitMessageWrappers(w *writer, file *protogen.File, m *protogen.Message) {
	name := luaTypeName(m.Desc.FullName(), file.Desc.Package())
	full := emmyMessageFullName(m)

	emitEmmyWrapperAnnotations(w, name, full, wrapperNew)
	w.line("function M.%s_new(t) return t or {} end", name)
	emitEmmyWrapperAnnotations(w, name, full, wrapperEncode)
	w.line("function M.%s_encode(t) return pb.encode(M.%s_descriptor, t) end", name, name)
	emitEmmyWrapperAnnotations(w, name, full, wrapperDecode)
	w.line("function M.%s_decode(b) return pb.decode(M.%s_descriptor, b) end", name, name)
	emitEmmyWrapperAnnotations(w, name, full, wrapperDecodeLazy)
	w.line("function M.%s_decode_lazy(b) return pb.decode_lazy(M.%s_descriptor, b) end", name, name)
	emitOptionalAccessors(w, name, m)
	emitOptionalAccessors(w, name, m, full)
	w.line("")
}

// emitOptionalAccessors writes M.<Name>_has_<field>(t) and _clear_<field>(t)
// for every field marked with proto3 explicit `optional`.
func emitOptionalAccessors(w *writer, name string, m *protogen.Message) {
func emitOptionalAccessors(w *writer, name string, m *protogen.Message, fullName string) {
	for _, f := range m.Fields {
		if !f.Desc.HasOptionalKeyword() {
			continue
		}
		fname := string(f.Desc.Name())
		emitEmmyWrapperAnnotations(w, name, fullName, wrapperHas)
		w.line("function M.%s_has_%s(t) return t.%s ~= nil end", name, fname, fname)
		emitEmmyWrapperAnnotations(w, name, fullName, wrapperClear)
		w.line("function M.%s_clear_%s(t) t.%s = nil end", name, fname, fname)
	}
}

M cmd/protoc-gen-tarantool/internal/gen/inline.go => cmd/protoc-gen-tarantool/internal/gen/inline.go +6 -1
@@ 13,19 13,23 @@ import (
// each scalar field's encode/decode call resolves to one wire.<typed> call.
func emitInlineMessage(w *writer, file *protogen.File, m *protogen.Message, imports map[string]string, prefix string) {
	name := luaTypeName(m.Desc.FullName(), file.Desc.Package())
	full := emmyMessageFullName(m)
	selfPath := luaPackagePath(file.Desc, prefix)

	emitEmmyWrapperAnnotations(w, name, full, wrapperNew)
	w.line("function M.%s_new(t) return t or {} end", name)
	w.line("")

	emitInlineEncode(w, name, m, file, selfPath, imports, prefix)
	emitInlineDecode(w, name, m, file, selfPath, imports, prefix)
	emitEmmyWrapperAnnotations(w, name, full, wrapperDecodeLazy)
	w.line("function M.%s_decode_lazy(b) return pb.decode_lazy(M.%s_descriptor, b) end", name, name)
	emitOptionalAccessors(w, name, m)
	emitOptionalAccessors(w, name, m, full)
	w.line("")
}

func emitInlineEncode(w *writer, name string, m *protogen.Message, file *protogen.File, selfPath string, imports map[string]string, prefix string) {
	emitEmmyWrapperAnnotations(w, name, emmyMessageFullName(m), wrapperEncode)
	w.line("function M.%s_encode(t)", name)
	w.line("    if type(t) ~= 'table' then")
	w.line("        error(\"expected table for %s, got \" .. type(t), 0)", m.Desc.FullName())


@@ 196,6 200,7 @@ func emitInlineEncodeRepeated(w *writer, f *protogen.Field, tag, fname string, f
}

func emitInlineDecode(w *writer, name string, m *protogen.Message, file *protogen.File, selfPath string, imports map[string]string, prefix string) {
	emitEmmyWrapperAnnotations(w, name, emmyMessageFullName(m), wrapperDecode)
	w.line("function M.%s_decode(buf)", name)
	w.line("    if type(buf) ~= 'string' then")
	w.line("        error(\"expected string for %s decode, got \" .. type(buf), 0)", m.Desc.FullName())

M examples/expected/full/conformance/conformance_pb.lua => examples/expected/full/conformance/conformance_pb.lua +79 -0
@@ 90,8 90,51 @@ M.JspbEncodingConfig_descriptor.fields = {
}
pb.finalize_message(M.JspbEncodingConfig_descriptor)

-- EmmyLua / lua-language-server type annotations.
-- These are comments — no runtime effect. They give editors
-- autocomplete and type-checking for the generated wrappers.
---@alias conformance.WireFormat integer
---@alias conformance.TestCategory integer

---@class conformance.TestStatus
---@field name string
---@field failure_message string
---@field matched_name string

---@class conformance.FailureSet
---@field test conformance.TestStatus[]

---@class conformance.ConformanceRequest
---@field protobuf_payload? string
---@field json_payload? string
---@field jspb_payload? string
---@field text_payload? string
---@field requested_output_format conformance.WireFormat
---@field message_type string
---@field test_category conformance.TestCategory
---@field jspb_encoding_options conformance.JspbEncodingConfig
---@field print_unknown_fields boolean

---@class conformance.ConformanceResponse
---@field parse_error? string
---@field serialize_error? string
---@field timeout_error? string
---@field runtime_error? string
---@field protobuf_payload? string
---@field json_payload? string
---@field skipped? string
---@field jspb_payload? string
---@field text_payload? string

---@class conformance.JspbEncodingConfig
---@field use_jspb_array_any_format boolean

---@param t? conformance.TestStatus
---@return conformance.TestStatus
function M.TestStatus_new(t) return t or {} end

---@param t conformance.TestStatus
---@return string
function M.TestStatus_encode(t)
    if type(t) ~= 'table' then
        error("expected table for conformance.TestStatus, got " .. type(t), 0)


@@ 121,6 164,8 @@ function M.TestStatus_encode(t)
    return table.concat(out)
end

---@param b string
---@return conformance.TestStatus
function M.TestStatus_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for conformance.TestStatus decode, got " .. type(buf), 0)


@@ 154,10 199,16 @@ function M.TestStatus_decode(buf)
    return result
end

---@param b string
---@return pb.MessageView
function M.TestStatus_decode_lazy(b) return pb.decode_lazy(M.TestStatus_descriptor, b) end

---@param t? conformance.FailureSet
---@return conformance.FailureSet
function M.FailureSet_new(t) return t or {} end

---@param t conformance.FailureSet
---@return string
function M.FailureSet_encode(t)
    if type(t) ~= 'table' then
        error("expected table for conformance.FailureSet, got " .. type(t), 0)


@@ 178,6 229,8 @@ function M.FailureSet_encode(t)
    return table.concat(out)
end

---@param b string
---@return conformance.FailureSet
function M.FailureSet_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for conformance.FailureSet decode, got " .. type(buf), 0)


@@ 205,10 258,16 @@ function M.FailureSet_decode(buf)
    return result
end

---@param b string
---@return pb.MessageView
function M.FailureSet_decode_lazy(b) return pb.decode_lazy(M.FailureSet_descriptor, b) end

---@param t? conformance.ConformanceRequest
---@return conformance.ConformanceRequest
function M.ConformanceRequest_new(t) return t or {} end

---@param t conformance.ConformanceRequest
---@return string
function M.ConformanceRequest_encode(t)
    if type(t) ~= 'table' then
        error("expected table for conformance.ConformanceRequest, got " .. type(t), 0)


@@ 293,6 352,8 @@ function M.ConformanceRequest_encode(t)
    return table.concat(out)
end

---@param b string
---@return conformance.ConformanceRequest
function M.ConformanceRequest_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for conformance.ConformanceRequest decode, got " .. type(buf), 0)


@@ 368,10 429,16 @@ function M.ConformanceRequest_decode(buf)
    return result
end

---@param b string
---@return pb.MessageView
function M.ConformanceRequest_decode_lazy(b) return pb.decode_lazy(M.ConformanceRequest_descriptor, b) end

---@param t? conformance.ConformanceResponse
---@return conformance.ConformanceResponse
function M.ConformanceResponse_new(t) return t or {} end

---@param t conformance.ConformanceResponse
---@return string
function M.ConformanceResponse_encode(t)
    if type(t) ~= 'table' then
        error("expected table for conformance.ConformanceResponse, got " .. type(t), 0)


@@ 447,6 514,8 @@ function M.ConformanceResponse_encode(t)
    return table.concat(out)
end

---@param b string
---@return conformance.ConformanceResponse
function M.ConformanceResponse_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for conformance.ConformanceResponse decode, got " .. type(buf), 0)


@@ 576,10 645,16 @@ function M.ConformanceResponse_decode(buf)
    return result
end

---@param b string
---@return pb.MessageView
function M.ConformanceResponse_decode_lazy(b) return pb.decode_lazy(M.ConformanceResponse_descriptor, b) end

---@param t? conformance.JspbEncodingConfig
---@return conformance.JspbEncodingConfig
function M.JspbEncodingConfig_new(t) return t or {} end

---@param t conformance.JspbEncodingConfig
---@return string
function M.JspbEncodingConfig_encode(t)
    if type(t) ~= 'table' then
        error("expected table for conformance.JspbEncodingConfig, got " .. type(t), 0)


@@ 597,6 672,8 @@ function M.JspbEncodingConfig_encode(t)
    return table.concat(out)
end

---@param b string
---@return conformance.JspbEncodingConfig
function M.JspbEncodingConfig_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for conformance.JspbEncodingConfig decode, got " .. type(buf), 0)


@@ 622,6 699,8 @@ function M.JspbEncodingConfig_decode(buf)
    return result
end

---@param b string
---@return pb.MessageView
function M.JspbEncodingConfig_decode_lazy(b) return pb.decode_lazy(M.JspbEncodingConfig_descriptor, b) end

return M

M examples/expected/full/hello/hello_pb.lua => examples/expected/full/hello/hello_pb.lua +104 -0
@@ 93,8 93,65 @@ M.Person_descriptor.fields = {
}
pb.finalize_message(M.Person_descriptor)

-- EmmyLua / lua-language-server type annotations.
-- These are comments — no runtime effect. They give editors
-- autocomplete and type-checking for the generated wrappers.
---@alias hello.Status integer

---@class hello.Result
---@field id integer
---@field text? string
---@field code? integer
---@field details? hello.Address

---@class hello.HelloRequest
---@field name string

---@class hello.HelloReply
---@field greeting string

---@class hello.Event
---@field title string
---@field created_at google.protobuf.Timestamp
---@field duration google.protobuf.Duration
---@field ack google.protobuf.Empty
---@field retry_count google.protobuf.Int32Value
---@field note google.protobuf.StringValue
---@field is_admin google.protobuf.BoolValue
---@field payload google.protobuf.Struct
---@field attribute google.protobuf.Value
---@field tags google.protobuf.ListValue
---@field extension google.protobuf.Any
---@field update_mask google.protobuf.FieldMask

---@class hello.Address
---@field street string
---@field city string
---@field zip integer
---@field apartment? string

---@class hello.Person
---@field name string
---@field age integer
---@field emails string[]
---@field status hello.Status
---@field address hello.Address
---@field friends hello.Person[]
---@field lucky_numbers integer[]
---@field avatar string
---@field user_id integer
---@field balance integer
---@field weight_kg number
---@field ages_by_nickname table<string, integer>
---@field nickname_by_age table<integer, string>
---@field addresses_by_label table<string, hello.Address>

---@param t? hello.Result
---@return hello.Result
function M.Result_new(t) return t or {} end

---@param t hello.Result
---@return string
function M.Result_encode(t)
    if type(t) ~= 'table' then
        error("expected table for hello.Result, got " .. type(t), 0)


@@ 134,6 191,8 @@ function M.Result_encode(t)
    return table.concat(out)
end

---@param b string
---@return hello.Result
function M.Result_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for hello.Result decode, got " .. type(buf), 0)


@@ 177,10 236,16 @@ function M.Result_decode(buf)
    return result
end

---@param b string
---@return pb.MessageView
function M.Result_decode_lazy(b) return pb.decode_lazy(M.Result_descriptor, b) end

---@param t? hello.HelloRequest
---@return hello.HelloRequest
function M.HelloRequest_new(t) return t or {} end

---@param t hello.HelloRequest
---@return string
function M.HelloRequest_encode(t)
    if type(t) ~= 'table' then
        error("expected table for hello.HelloRequest, got " .. type(t), 0)


@@ 198,6 263,8 @@ function M.HelloRequest_encode(t)
    return table.concat(out)
end

---@param b string
---@return hello.HelloRequest
function M.HelloRequest_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for hello.HelloRequest decode, got " .. type(buf), 0)


@@ 223,10 290,16 @@ function M.HelloRequest_decode(buf)
    return result
end

---@param b string
---@return pb.MessageView
function M.HelloRequest_decode_lazy(b) return pb.decode_lazy(M.HelloRequest_descriptor, b) end

---@param t? hello.HelloReply
---@return hello.HelloReply
function M.HelloReply_new(t) return t or {} end

---@param t hello.HelloReply
---@return string
function M.HelloReply_encode(t)
    if type(t) ~= 'table' then
        error("expected table for hello.HelloReply, got " .. type(t), 0)


@@ 244,6 317,8 @@ function M.HelloReply_encode(t)
    return table.concat(out)
end

---@param b string
---@return hello.HelloReply
function M.HelloReply_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for hello.HelloReply decode, got " .. type(buf), 0)


@@ 269,10 344,16 @@ function M.HelloReply_decode(buf)
    return result
end

---@param b string
---@return pb.MessageView
function M.HelloReply_decode_lazy(b) return pb.decode_lazy(M.HelloReply_descriptor, b) end

---@param t? hello.Event
---@return hello.Event
function M.Event_new(t) return t or {} end

---@param t hello.Event
---@return string
function M.Event_encode(t)
    if type(t) ~= 'table' then
        error("expected table for hello.Event, got " .. type(t), 0)


@@ 356,6 437,8 @@ function M.Event_encode(t)
    return table.concat(out)
end

---@param b string
---@return hello.Event
function M.Event_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for hello.Event decode, got " .. type(buf), 0)


@@ 491,10 574,16 @@ function M.Event_decode(buf)
    return result
end

---@param b string
---@return pb.MessageView
function M.Event_decode_lazy(b) return pb.decode_lazy(M.Event_descriptor, b) end

---@param t? hello.Address
---@return hello.Address
function M.Address_new(t) return t or {} end

---@param t hello.Address
---@return string
function M.Address_encode(t)
    if type(t) ~= 'table' then
        error("expected table for hello.Address, got " .. type(t), 0)


@@ 530,6 619,8 @@ function M.Address_encode(t)
    return table.concat(out)
end

---@param b string
---@return hello.Address
function M.Address_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for hello.Address decode, got " .. type(buf), 0)


@@ 567,12 658,21 @@ function M.Address_decode(buf)
    return result
end

---@param b string
---@return pb.MessageView
function M.Address_decode_lazy(b) return pb.decode_lazy(M.Address_descriptor, b) end
---@param t hello.Address
---@return boolean
function M.Address_has_apartment(t) return t.apartment ~= nil end
---@param t hello.Address
function M.Address_clear_apartment(t) t.apartment = nil end

---@param t? hello.Person
---@return hello.Person
function M.Person_new(t) return t or {} end

---@param t hello.Person
---@return string
function M.Person_encode(t)
    if type(t) ~= 'table' then
        error("expected table for hello.Person, got " .. type(t), 0)


@@ 721,6 821,8 @@ function M.Person_encode(t)
    return table.concat(out)
end

---@param b string
---@return hello.Person
function M.Person_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for hello.Person decode, got " .. type(buf), 0)


@@ 868,6 970,8 @@ function M.Person_decode(buf)
    return result
end

---@param b string
---@return pb.MessageView
function M.Person_decode_lazy(b) return pb.decode_lazy(M.Person_descriptor, b) end

-- Service: hello.Greeter

M examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua => examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua +214 -0
@@ 234,8 234,186 @@ M.EnumOnlyProto3_descriptor.fields = {
}
pb.finalize_message(M.EnumOnlyProto3_descriptor)

-- EmmyLua / lua-language-server type annotations.
-- These are comments — no runtime effect. They give editors
-- autocomplete and type-checking for the generated wrappers.
---@alias protobuf_test_messages.proto3.ForeignEnum integer
---@alias protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum integer
---@alias protobuf_test_messages.proto3.TestAllTypesProto3.AliasedEnum integer
---@alias protobuf_test_messages.proto3.EnumOnlyProto3.Bool integer

---@class protobuf_test_messages.proto3.TestAllTypesProto3
---@field optional_int32 integer
---@field optional_int64 integer
---@field optional_uint32 integer
---@field optional_uint64 integer
---@field optional_sint32 integer
---@field optional_sint64 integer
---@field optional_fixed32 integer
---@field optional_fixed64 integer
---@field optional_sfixed32 integer
---@field optional_sfixed64 integer
---@field optional_float number
---@field optional_double number
---@field optional_bool boolean
---@field optional_string string
---@field optional_bytes string
---@field optional_nested_message protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage
---@field optional_foreign_message protobuf_test_messages.proto3.ForeignMessage
---@field optional_nested_enum protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum
---@field optional_foreign_enum protobuf_test_messages.proto3.ForeignEnum
---@field optional_aliased_enum protobuf_test_messages.proto3.TestAllTypesProto3.AliasedEnum
---@field optional_string_piece string
---@field optional_cord string
---@field recursive_message protobuf_test_messages.proto3.TestAllTypesProto3
---@field repeated_int32 integer[]
---@field repeated_int64 integer[]
---@field repeated_uint32 integer[]
---@field repeated_uint64 integer[]
---@field repeated_sint32 integer[]
---@field repeated_sint64 integer[]
---@field repeated_fixed32 integer[]
---@field repeated_fixed64 integer[]
---@field repeated_sfixed32 integer[]
---@field repeated_sfixed64 integer[]
---@field repeated_float number[]
---@field repeated_double number[]
---@field repeated_bool boolean[]
---@field repeated_string string[]
---@field repeated_bytes string[]
---@field repeated_nested_message protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage[]
---@field repeated_foreign_message protobuf_test_messages.proto3.ForeignMessage[]
---@field repeated_nested_enum protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum[]
---@field repeated_foreign_enum protobuf_test_messages.proto3.ForeignEnum[]
---@field repeated_string_piece string[]
---@field repeated_cord string[]
---@field packed_int32 integer[]
---@field packed_int64 integer[]
---@field packed_uint32 integer[]
---@field packed_uint64 integer[]
---@field packed_sint32 integer[]
---@field packed_sint64 integer[]
---@field packed_fixed32 integer[]
---@field packed_fixed64 integer[]
---@field packed_sfixed32 integer[]
---@field packed_sfixed64 integer[]
---@field packed_float number[]
---@field packed_double number[]
---@field packed_bool boolean[]
---@field packed_nested_enum protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum[]
---@field unpacked_int32 integer[]
---@field unpacked_int64 integer[]
---@field unpacked_uint32 integer[]
---@field unpacked_uint64 integer[]
---@field unpacked_sint32 integer[]
---@field unpacked_sint64 integer[]
---@field unpacked_fixed32 integer[]
---@field unpacked_fixed64 integer[]
---@field unpacked_sfixed32 integer[]
---@field unpacked_sfixed64 integer[]
---@field unpacked_float number[]
---@field unpacked_double number[]
---@field unpacked_bool boolean[]
---@field unpacked_nested_enum protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum[]
---@field map_int32_int32 table<integer, integer>
---@field map_int64_int64 table<integer, integer>
---@field map_uint32_uint32 table<integer, integer>
---@field map_uint64_uint64 table<integer, integer>
---@field map_sint32_sint32 table<integer, integer>
---@field map_sint64_sint64 table<integer, integer>
---@field map_fixed32_fixed32 table<integer, integer>
---@field map_fixed64_fixed64 table<integer, integer>
---@field map_sfixed32_sfixed32 table<integer, integer>
---@field map_sfixed64_sfixed64 table<integer, integer>
---@field map_int32_float table<integer, number>
---@field map_int32_double table<integer, number>
---@field map_bool_bool table<boolean, boolean>
---@field map_string_string table<string, string>
---@field map_string_bytes table<string, string>
---@field map_string_nested_message table<string, protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage>
---@field map_string_foreign_message table<string, protobuf_test_messages.proto3.ForeignMessage>
---@field map_string_nested_enum table<string, protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum>
---@field map_string_foreign_enum table<string, protobuf_test_messages.proto3.ForeignEnum>
---@field oneof_uint32? integer
---@field oneof_nested_message? protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage
---@field oneof_string? string
---@field oneof_bytes? string
---@field oneof_bool? boolean
---@field oneof_uint64? integer
---@field oneof_float? number
---@field oneof_double? number
---@field oneof_enum? protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum
---@field oneof_null_value? google.protobuf.NullValue
---@field optional_bool_wrapper google.protobuf.BoolValue
---@field optional_int32_wrapper google.protobuf.Int32Value
---@field optional_int64_wrapper google.protobuf.Int64Value
---@field optional_uint32_wrapper google.protobuf.UInt32Value
---@field optional_uint64_wrapper google.protobuf.UInt64Value
---@field optional_float_wrapper google.protobuf.FloatValue
---@field optional_double_wrapper google.protobuf.DoubleValue
---@field optional_string_wrapper google.protobuf.StringValue
---@field optional_bytes_wrapper google.protobuf.BytesValue
---@field repeated_bool_wrapper google.protobuf.BoolValue[]
---@field repeated_int32_wrapper google.protobuf.Int32Value[]
---@field repeated_int64_wrapper google.protobuf.Int64Value[]
---@field repeated_uint32_wrapper google.protobuf.UInt32Value[]
---@field repeated_uint64_wrapper google.protobuf.UInt64Value[]
---@field repeated_float_wrapper google.protobuf.FloatValue[]
---@field repeated_double_wrapper google.protobuf.DoubleValue[]
---@field repeated_string_wrapper google.protobuf.StringValue[]
---@field repeated_bytes_wrapper google.protobuf.BytesValue[]
---@field optional_duration google.protobuf.Duration
---@field optional_timestamp google.protobuf.Timestamp
---@field optional_field_mask google.protobuf.FieldMask
---@field optional_struct google.protobuf.Struct
---@field optional_any google.protobuf.Any
---@field optional_value google.protobuf.Value
---@field optional_null_value google.protobuf.NullValue
---@field optional_empty google.protobuf.Empty
---@field repeated_duration google.protobuf.Duration[]
---@field repeated_timestamp google.protobuf.Timestamp[]
---@field repeated_fieldmask google.protobuf.FieldMask[]
---@field repeated_struct google.protobuf.Struct[]
---@field repeated_any google.protobuf.Any[]
---@field repeated_value google.protobuf.Value[]
---@field repeated_list_value google.protobuf.ListValue[]
---@field repeated_empty google.protobuf.Empty[]
---@field fieldname1 integer
---@field field_name2 integer
---@field _field_name3 integer
---@field field__name4_ integer
---@field field0name5 integer
---@field field_0_name6 integer
---@field fieldName7 integer
---@field FieldName8 integer
---@field field_Name9 integer
---@field Field_Name10 integer
---@field FIELD_NAME11 integer
---@field FIELD_name12 integer
---@field __field_name13 integer
---@field __Field_name14 integer
---@field field__name15 integer
---@field field__Name16 integer
---@field field_name17__ integer
---@field Field_name18__ integer

---@class protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage
---@field a integer
---@field corecursive protobuf_test_messages.proto3.TestAllTypesProto3

---@class protobuf_test_messages.proto3.ForeignMessage
---@field c integer

---@class protobuf_test_messages.proto3.NullHypothesisProto3

---@class protobuf_test_messages.proto3.EnumOnlyProto3

---@param t? protobuf_test_messages.proto3.TestAllTypesProto3
---@return protobuf_test_messages.proto3.TestAllTypesProto3
function M.TestAllTypesProto3_new(t) return t or {} end

---@param t protobuf_test_messages.proto3.TestAllTypesProto3
---@return string
function M.TestAllTypesProto3_encode(t)
    if type(t) ~= 'table' then
        error("expected table for protobuf_test_messages.proto3.TestAllTypesProto3, got " .. type(t), 0)


@@ 1704,6 1882,8 @@ function M.TestAllTypesProto3_encode(t)
    return table.concat(out)
end

---@param b string
---@return protobuf_test_messages.proto3.TestAllTypesProto3
function M.TestAllTypesProto3_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for protobuf_test_messages.proto3.TestAllTypesProto3 decode, got " .. type(buf), 0)


@@ 3439,10 3619,16 @@ function M.TestAllTypesProto3_decode(buf)
    return result
end

---@param b string
---@return pb.MessageView
function M.TestAllTypesProto3_decode_lazy(b) return pb.decode_lazy(M.TestAllTypesProto3_descriptor, b) end

---@param t? protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage
---@return protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage
function M.TestAllTypesProto3_NestedMessage_new(t) return t or {} end

---@param t protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage
---@return string
function M.TestAllTypesProto3_NestedMessage_encode(t)
    if type(t) ~= 'table' then
        error("expected table for protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage, got " .. type(t), 0)


@@ 3466,6 3652,8 @@ function M.TestAllTypesProto3_NestedMessage_encode(t)
    return table.concat(out)
end

---@param b string
---@return protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage
function M.TestAllTypesProto3_NestedMessage_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage decode, got " .. type(buf), 0)


@@ 3501,10 3689,16 @@ function M.TestAllTypesProto3_NestedMessage_decode(buf)
    return result
end

---@param b string
---@return pb.MessageView
function M.TestAllTypesProto3_NestedMessage_decode_lazy(b) return pb.decode_lazy(M.TestAllTypesProto3_NestedMessage_descriptor, b) end

---@param t? protobuf_test_messages.proto3.ForeignMessage
---@return protobuf_test_messages.proto3.ForeignMessage
function M.ForeignMessage_new(t) return t or {} end

---@param t protobuf_test_messages.proto3.ForeignMessage
---@return string
function M.ForeignMessage_encode(t)
    if type(t) ~= 'table' then
        error("expected table for protobuf_test_messages.proto3.ForeignMessage, got " .. type(t), 0)


@@ 3522,6 3716,8 @@ function M.ForeignMessage_encode(t)
    return table.concat(out)
end

---@param b string
---@return protobuf_test_messages.proto3.ForeignMessage
function M.ForeignMessage_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for protobuf_test_messages.proto3.ForeignMessage decode, got " .. type(buf), 0)


@@ 3547,10 3743,16 @@ function M.ForeignMessage_decode(buf)
    return result
end

---@param b string
---@return pb.MessageView
function M.ForeignMessage_decode_lazy(b) return pb.decode_lazy(M.ForeignMessage_descriptor, b) end

---@param t? protobuf_test_messages.proto3.NullHypothesisProto3
---@return protobuf_test_messages.proto3.NullHypothesisProto3
function M.NullHypothesisProto3_new(t) return t or {} end

---@param t protobuf_test_messages.proto3.NullHypothesisProto3
---@return string
function M.NullHypothesisProto3_encode(t)
    if type(t) ~= 'table' then
        error("expected table for protobuf_test_messages.proto3.NullHypothesisProto3, got " .. type(t), 0)


@@ 3562,6 3764,8 @@ function M.NullHypothesisProto3_encode(t)
    return table.concat(out)
end

---@param b string
---@return protobuf_test_messages.proto3.NullHypothesisProto3
function M.NullHypothesisProto3_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for protobuf_test_messages.proto3.NullHypothesisProto3 decode, got " .. type(buf), 0)


@@ 3584,10 3788,16 @@ function M.NullHypothesisProto3_decode(buf)
    return result
end

---@param b string
---@return pb.MessageView
function M.NullHypothesisProto3_decode_lazy(b) return pb.decode_lazy(M.NullHypothesisProto3_descriptor, b) end

---@param t? protobuf_test_messages.proto3.EnumOnlyProto3
---@return protobuf_test_messages.proto3.EnumOnlyProto3
function M.EnumOnlyProto3_new(t) return t or {} end

---@param t protobuf_test_messages.proto3.EnumOnlyProto3
---@return string
function M.EnumOnlyProto3_encode(t)
    if type(t) ~= 'table' then
        error("expected table for protobuf_test_messages.proto3.EnumOnlyProto3, got " .. type(t), 0)


@@ 3599,6 3809,8 @@ function M.EnumOnlyProto3_encode(t)
    return table.concat(out)
end

---@param b string
---@return protobuf_test_messages.proto3.EnumOnlyProto3
function M.EnumOnlyProto3_decode(buf)
    if type(buf) ~= 'string' then
        error("expected string for protobuf_test_messages.proto3.EnumOnlyProto3 decode, got " .. type(buf), 0)


@@ 3621,6 3833,8 @@ function M.EnumOnlyProto3_decode(buf)
    return result
end

---@param b string
---@return pb.MessageView
function M.EnumOnlyProto3_decode_lazy(b) return pb.decode_lazy(M.EnumOnlyProto3_descriptor, b) end

return M

M examples/expected/runtime/conformance/conformance_pb.lua => examples/expected/runtime/conformance/conformance_pb.lua +79 -0
@@ 90,29 90,108 @@ M.JspbEncodingConfig_descriptor.fields = {
}
pb.finalize_message(M.JspbEncodingConfig_descriptor)

-- EmmyLua / lua-language-server type annotations.
-- These are comments — no runtime effect. They give editors
-- autocomplete and type-checking for the generated wrappers.
---@alias conformance.WireFormat integer
---@alias conformance.TestCategory integer

---@class conformance.TestStatus
---@field name string
---@field failure_message string
---@field matched_name string

---@class conformance.FailureSet
---@field test conformance.TestStatus[]

---@class conformance.ConformanceRequest
---@field protobuf_payload? string
---@field json_payload? string
---@field jspb_payload? string
---@field text_payload? string
---@field requested_output_format conformance.WireFormat
---@field message_type string
---@field test_category conformance.TestCategory
---@field jspb_encoding_options conformance.JspbEncodingConfig
---@field print_unknown_fields boolean

---@class conformance.ConformanceResponse
---@field parse_error? string
---@field serialize_error? string
---@field timeout_error? string
---@field runtime_error? string
---@field protobuf_payload? string
---@field json_payload? string
---@field skipped? string
---@field jspb_payload? string
---@field text_payload? string

---@class conformance.JspbEncodingConfig
---@field use_jspb_array_any_format boolean

---@param t? conformance.TestStatus
---@return conformance.TestStatus
function M.TestStatus_new(t) return t or {} end
---@param t conformance.TestStatus
---@return string
function M.TestStatus_encode(t) return pb.encode(M.TestStatus_descriptor, t) end
---@param b string
---@return conformance.TestStatus
function M.TestStatus_decode(b) return pb.decode(M.TestStatus_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.TestStatus_decode_lazy(b) return pb.decode_lazy(M.TestStatus_descriptor, b) end

---@param t? conformance.FailureSet
---@return conformance.FailureSet
function M.FailureSet_new(t) return t or {} end
---@param t conformance.FailureSet
---@return string
function M.FailureSet_encode(t) return pb.encode(M.FailureSet_descriptor, t) end
---@param b string
---@return conformance.FailureSet
function M.FailureSet_decode(b) return pb.decode(M.FailureSet_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.FailureSet_decode_lazy(b) return pb.decode_lazy(M.FailureSet_descriptor, b) end

---@param t? conformance.ConformanceRequest
---@return conformance.ConformanceRequest
function M.ConformanceRequest_new(t) return t or {} end
---@param t conformance.ConformanceRequest
---@return string
function M.ConformanceRequest_encode(t) return pb.encode(M.ConformanceRequest_descriptor, t) end
---@param b string
---@return conformance.ConformanceRequest
function M.ConformanceRequest_decode(b) return pb.decode(M.ConformanceRequest_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.ConformanceRequest_decode_lazy(b) return pb.decode_lazy(M.ConformanceRequest_descriptor, b) end

---@param t? conformance.ConformanceResponse
---@return conformance.ConformanceResponse
function M.ConformanceResponse_new(t) return t or {} end
---@param t conformance.ConformanceResponse
---@return string
function M.ConformanceResponse_encode(t) return pb.encode(M.ConformanceResponse_descriptor, t) end
---@param b string
---@return conformance.ConformanceResponse
function M.ConformanceResponse_decode(b) return pb.decode(M.ConformanceResponse_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.ConformanceResponse_decode_lazy(b) return pb.decode_lazy(M.ConformanceResponse_descriptor, b) end

---@param t? conformance.JspbEncodingConfig
---@return conformance.JspbEncodingConfig
function M.JspbEncodingConfig_new(t) return t or {} end
---@param t conformance.JspbEncodingConfig
---@return string
function M.JspbEncodingConfig_encode(t) return pb.encode(M.JspbEncodingConfig_descriptor, t) end
---@param b string
---@return conformance.JspbEncodingConfig
function M.JspbEncodingConfig_decode(b) return pb.decode(M.JspbEncodingConfig_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.JspbEncodingConfig_decode_lazy(b) return pb.decode_lazy(M.JspbEncodingConfig_descriptor, b) end

return M

M examples/expected/runtime/hello/hello_pb.lua => examples/expected/runtime/hello/hello_pb.lua +104 -0
@@ 93,36 93,140 @@ M.Person_descriptor.fields = {
}
pb.finalize_message(M.Person_descriptor)

-- EmmyLua / lua-language-server type annotations.
-- These are comments — no runtime effect. They give editors
-- autocomplete and type-checking for the generated wrappers.
---@alias hello.Status integer

---@class hello.Result
---@field id integer
---@field text? string
---@field code? integer
---@field details? hello.Address

---@class hello.HelloRequest
---@field name string

---@class hello.HelloReply
---@field greeting string

---@class hello.Event
---@field title string
---@field created_at google.protobuf.Timestamp
---@field duration google.protobuf.Duration
---@field ack google.protobuf.Empty
---@field retry_count google.protobuf.Int32Value
---@field note google.protobuf.StringValue
---@field is_admin google.protobuf.BoolValue
---@field payload google.protobuf.Struct
---@field attribute google.protobuf.Value
---@field tags google.protobuf.ListValue
---@field extension google.protobuf.Any
---@field update_mask google.protobuf.FieldMask

---@class hello.Address
---@field street string
---@field city string
---@field zip integer
---@field apartment? string

---@class hello.Person
---@field name string
---@field age integer
---@field emails string[]
---@field status hello.Status
---@field address hello.Address
---@field friends hello.Person[]
---@field lucky_numbers integer[]
---@field avatar string
---@field user_id integer
---@field balance integer
---@field weight_kg number
---@field ages_by_nickname table<string, integer>
---@field nickname_by_age table<integer, string>
---@field addresses_by_label table<string, hello.Address>

---@param t? hello.Result
---@return hello.Result
function M.Result_new(t) return t or {} end
---@param t hello.Result
---@return string
function M.Result_encode(t) return pb.encode(M.Result_descriptor, t) end
---@param b string
---@return hello.Result
function M.Result_decode(b) return pb.decode(M.Result_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.Result_decode_lazy(b) return pb.decode_lazy(M.Result_descriptor, b) end

---@param t? hello.HelloRequest
---@return hello.HelloRequest
function M.HelloRequest_new(t) return t or {} end
---@param t hello.HelloRequest
---@return string
function M.HelloRequest_encode(t) return pb.encode(M.HelloRequest_descriptor, t) end
---@param b string
---@return hello.HelloRequest
function M.HelloRequest_decode(b) return pb.decode(M.HelloRequest_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.HelloRequest_decode_lazy(b) return pb.decode_lazy(M.HelloRequest_descriptor, b) end

---@param t? hello.HelloReply
---@return hello.HelloReply
function M.HelloReply_new(t) return t or {} end
---@param t hello.HelloReply
---@return string
function M.HelloReply_encode(t) return pb.encode(M.HelloReply_descriptor, t) end
---@param b string
---@return hello.HelloReply
function M.HelloReply_decode(b) return pb.decode(M.HelloReply_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.HelloReply_decode_lazy(b) return pb.decode_lazy(M.HelloReply_descriptor, b) end

---@param t? hello.Event
---@return hello.Event
function M.Event_new(t) return t or {} end
---@param t hello.Event
---@return string
function M.Event_encode(t) return pb.encode(M.Event_descriptor, t) end
---@param b string
---@return hello.Event
function M.Event_decode(b) return pb.decode(M.Event_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.Event_decode_lazy(b) return pb.decode_lazy(M.Event_descriptor, b) end

---@param t? hello.Address
---@return hello.Address
function M.Address_new(t) return t or {} end
---@param t hello.Address
---@return string
function M.Address_encode(t) return pb.encode(M.Address_descriptor, t) end
---@param b string
---@return hello.Address
function M.Address_decode(b) return pb.decode(M.Address_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.Address_decode_lazy(b) return pb.decode_lazy(M.Address_descriptor, b) end
---@param t hello.Address
---@return boolean
function M.Address_has_apartment(t) return t.apartment ~= nil end
---@param t hello.Address
function M.Address_clear_apartment(t) t.apartment = nil end

---@param t? hello.Person
---@return hello.Person
function M.Person_new(t) return t or {} end
---@param t hello.Person
---@return string
function M.Person_encode(t) return pb.encode(M.Person_descriptor, t) end
---@param b string
---@return hello.Person
function M.Person_decode(b) return pb.decode(M.Person_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.Person_decode_lazy(b) return pb.decode_lazy(M.Person_descriptor, b) end

-- Service: hello.Greeter

M examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua => examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua +214 -0
@@ 234,29 234,243 @@ M.EnumOnlyProto3_descriptor.fields = {
}
pb.finalize_message(M.EnumOnlyProto3_descriptor)

-- EmmyLua / lua-language-server type annotations.
-- These are comments — no runtime effect. They give editors
-- autocomplete and type-checking for the generated wrappers.
---@alias protobuf_test_messages.proto3.ForeignEnum integer
---@alias protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum integer
---@alias protobuf_test_messages.proto3.TestAllTypesProto3.AliasedEnum integer
---@alias protobuf_test_messages.proto3.EnumOnlyProto3.Bool integer

---@class protobuf_test_messages.proto3.TestAllTypesProto3
---@field optional_int32 integer
---@field optional_int64 integer
---@field optional_uint32 integer
---@field optional_uint64 integer
---@field optional_sint32 integer
---@field optional_sint64 integer
---@field optional_fixed32 integer
---@field optional_fixed64 integer
---@field optional_sfixed32 integer
---@field optional_sfixed64 integer
---@field optional_float number
---@field optional_double number
---@field optional_bool boolean
---@field optional_string string
---@field optional_bytes string
---@field optional_nested_message protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage
---@field optional_foreign_message protobuf_test_messages.proto3.ForeignMessage
---@field optional_nested_enum protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum
---@field optional_foreign_enum protobuf_test_messages.proto3.ForeignEnum
---@field optional_aliased_enum protobuf_test_messages.proto3.TestAllTypesProto3.AliasedEnum
---@field optional_string_piece string
---@field optional_cord string
---@field recursive_message protobuf_test_messages.proto3.TestAllTypesProto3
---@field repeated_int32 integer[]
---@field repeated_int64 integer[]
---@field repeated_uint32 integer[]
---@field repeated_uint64 integer[]
---@field repeated_sint32 integer[]
---@field repeated_sint64 integer[]
---@field repeated_fixed32 integer[]
---@field repeated_fixed64 integer[]
---@field repeated_sfixed32 integer[]
---@field repeated_sfixed64 integer[]
---@field repeated_float number[]
---@field repeated_double number[]
---@field repeated_bool boolean[]
---@field repeated_string string[]
---@field repeated_bytes string[]
---@field repeated_nested_message protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage[]
---@field repeated_foreign_message protobuf_test_messages.proto3.ForeignMessage[]
---@field repeated_nested_enum protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum[]
---@field repeated_foreign_enum protobuf_test_messages.proto3.ForeignEnum[]
---@field repeated_string_piece string[]
---@field repeated_cord string[]
---@field packed_int32 integer[]
---@field packed_int64 integer[]
---@field packed_uint32 integer[]
---@field packed_uint64 integer[]
---@field packed_sint32 integer[]
---@field packed_sint64 integer[]
---@field packed_fixed32 integer[]
---@field packed_fixed64 integer[]
---@field packed_sfixed32 integer[]
---@field packed_sfixed64 integer[]
---@field packed_float number[]
---@field packed_double number[]
---@field packed_bool boolean[]
---@field packed_nested_enum protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum[]
---@field unpacked_int32 integer[]
---@field unpacked_int64 integer[]
---@field unpacked_uint32 integer[]
---@field unpacked_uint64 integer[]
---@field unpacked_sint32 integer[]
---@field unpacked_sint64 integer[]
---@field unpacked_fixed32 integer[]
---@field unpacked_fixed64 integer[]
---@field unpacked_sfixed32 integer[]
---@field unpacked_sfixed64 integer[]
---@field unpacked_float number[]
---@field unpacked_double number[]
---@field unpacked_bool boolean[]
---@field unpacked_nested_enum protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum[]
---@field map_int32_int32 table<integer, integer>
---@field map_int64_int64 table<integer, integer>
---@field map_uint32_uint32 table<integer, integer>
---@field map_uint64_uint64 table<integer, integer>
---@field map_sint32_sint32 table<integer, integer>
---@field map_sint64_sint64 table<integer, integer>
---@field map_fixed32_fixed32 table<integer, integer>
---@field map_fixed64_fixed64 table<integer, integer>
---@field map_sfixed32_sfixed32 table<integer, integer>
---@field map_sfixed64_sfixed64 table<integer, integer>
---@field map_int32_float table<integer, number>
---@field map_int32_double table<integer, number>
---@field map_bool_bool table<boolean, boolean>
---@field map_string_string table<string, string>
---@field map_string_bytes table<string, string>
---@field map_string_nested_message table<string, protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage>
---@field map_string_foreign_message table<string, protobuf_test_messages.proto3.ForeignMessage>
---@field map_string_nested_enum table<string, protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum>
---@field map_string_foreign_enum table<string, protobuf_test_messages.proto3.ForeignEnum>
---@field oneof_uint32? integer
---@field oneof_nested_message? protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage
---@field oneof_string? string
---@field oneof_bytes? string
---@field oneof_bool? boolean
---@field oneof_uint64? integer
---@field oneof_float? number
---@field oneof_double? number
---@field oneof_enum? protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum
---@field oneof_null_value? google.protobuf.NullValue
---@field optional_bool_wrapper google.protobuf.BoolValue
---@field optional_int32_wrapper google.protobuf.Int32Value
---@field optional_int64_wrapper google.protobuf.Int64Value
---@field optional_uint32_wrapper google.protobuf.UInt32Value
---@field optional_uint64_wrapper google.protobuf.UInt64Value
---@field optional_float_wrapper google.protobuf.FloatValue
---@field optional_double_wrapper google.protobuf.DoubleValue
---@field optional_string_wrapper google.protobuf.StringValue
---@field optional_bytes_wrapper google.protobuf.BytesValue
---@field repeated_bool_wrapper google.protobuf.BoolValue[]
---@field repeated_int32_wrapper google.protobuf.Int32Value[]
---@field repeated_int64_wrapper google.protobuf.Int64Value[]
---@field repeated_uint32_wrapper google.protobuf.UInt32Value[]
---@field repeated_uint64_wrapper google.protobuf.UInt64Value[]
---@field repeated_float_wrapper google.protobuf.FloatValue[]
---@field repeated_double_wrapper google.protobuf.DoubleValue[]
---@field repeated_string_wrapper google.protobuf.StringValue[]
---@field repeated_bytes_wrapper google.protobuf.BytesValue[]
---@field optional_duration google.protobuf.Duration
---@field optional_timestamp google.protobuf.Timestamp
---@field optional_field_mask google.protobuf.FieldMask
---@field optional_struct google.protobuf.Struct
---@field optional_any google.protobuf.Any
---@field optional_value google.protobuf.Value
---@field optional_null_value google.protobuf.NullValue
---@field optional_empty google.protobuf.Empty
---@field repeated_duration google.protobuf.Duration[]
---@field repeated_timestamp google.protobuf.Timestamp[]
---@field repeated_fieldmask google.protobuf.FieldMask[]
---@field repeated_struct google.protobuf.Struct[]
---@field repeated_any google.protobuf.Any[]
---@field repeated_value google.protobuf.Value[]
---@field repeated_list_value google.protobuf.ListValue[]
---@field repeated_empty google.protobuf.Empty[]
---@field fieldname1 integer
---@field field_name2 integer
---@field _field_name3 integer
---@field field__name4_ integer
---@field field0name5 integer
---@field field_0_name6 integer
---@field fieldName7 integer
---@field FieldName8 integer
---@field field_Name9 integer
---@field Field_Name10 integer
---@field FIELD_NAME11 integer
---@field FIELD_name12 integer
---@field __field_name13 integer
---@field __Field_name14 integer
---@field field__name15 integer
---@field field__Name16 integer
---@field field_name17__ integer
---@field Field_name18__ integer

---@class protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage
---@field a integer
---@field corecursive protobuf_test_messages.proto3.TestAllTypesProto3

---@class protobuf_test_messages.proto3.ForeignMessage
---@field c integer

---@class protobuf_test_messages.proto3.NullHypothesisProto3

---@class protobuf_test_messages.proto3.EnumOnlyProto3

---@param t? protobuf_test_messages.proto3.TestAllTypesProto3
---@return protobuf_test_messages.proto3.TestAllTypesProto3
function M.TestAllTypesProto3_new(t) return t or {} end
---@param t protobuf_test_messages.proto3.TestAllTypesProto3
---@return string
function M.TestAllTypesProto3_encode(t) return pb.encode(M.TestAllTypesProto3_descriptor, t) end
---@param b string
---@return protobuf_test_messages.proto3.TestAllTypesProto3
function M.TestAllTypesProto3_decode(b) return pb.decode(M.TestAllTypesProto3_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.TestAllTypesProto3_decode_lazy(b) return pb.decode_lazy(M.TestAllTypesProto3_descriptor, b) end

---@param t? protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage
---@return protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage
function M.TestAllTypesProto3_NestedMessage_new(t) return t or {} end
---@param t protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage
---@return string
function M.TestAllTypesProto3_NestedMessage_encode(t) return pb.encode(M.TestAllTypesProto3_NestedMessage_descriptor, t) end
---@param b string
---@return protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage
function M.TestAllTypesProto3_NestedMessage_decode(b) return pb.decode(M.TestAllTypesProto3_NestedMessage_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.TestAllTypesProto3_NestedMessage_decode_lazy(b) return pb.decode_lazy(M.TestAllTypesProto3_NestedMessage_descriptor, b) end

---@param t? protobuf_test_messages.proto3.ForeignMessage
---@return protobuf_test_messages.proto3.ForeignMessage
function M.ForeignMessage_new(t) return t or {} end
---@param t protobuf_test_messages.proto3.ForeignMessage
---@return string
function M.ForeignMessage_encode(t) return pb.encode(M.ForeignMessage_descriptor, t) end
---@param b string
---@return protobuf_test_messages.proto3.ForeignMessage
function M.ForeignMessage_decode(b) return pb.decode(M.ForeignMessage_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.ForeignMessage_decode_lazy(b) return pb.decode_lazy(M.ForeignMessage_descriptor, b) end

---@param t? protobuf_test_messages.proto3.NullHypothesisProto3
---@return protobuf_test_messages.proto3.NullHypothesisProto3
function M.NullHypothesisProto3_new(t) return t or {} end
---@param t protobuf_test_messages.proto3.NullHypothesisProto3
---@return string
function M.NullHypothesisProto3_encode(t) return pb.encode(M.NullHypothesisProto3_descriptor, t) end
---@param b string
---@return protobuf_test_messages.proto3.NullHypothesisProto3
function M.NullHypothesisProto3_decode(b) return pb.decode(M.NullHypothesisProto3_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.NullHypothesisProto3_decode_lazy(b) return pb.decode_lazy(M.NullHypothesisProto3_descriptor, b) end

---@param t? protobuf_test_messages.proto3.EnumOnlyProto3
---@return protobuf_test_messages.proto3.EnumOnlyProto3
function M.EnumOnlyProto3_new(t) return t or {} end
---@param t protobuf_test_messages.proto3.EnumOnlyProto3
---@return string
function M.EnumOnlyProto3_encode(t) return pb.encode(M.EnumOnlyProto3_descriptor, t) end
---@param b string
---@return protobuf_test_messages.proto3.EnumOnlyProto3
function M.EnumOnlyProto3_decode(b) return pb.decode(M.EnumOnlyProto3_descriptor, b) end
---@param b string
---@return pb.MessageView
function M.EnumOnlyProto3_decode_lazy(b) return pb.decode_lazy(M.EnumOnlyProto3_descriptor, b) end

return M

M runtime/pb/lazy.lua => runtime/pb/lazy.lua +24 -0
@@ 19,6 19,30 @@
-- wrapped in an EagerView with the same getter surface, so callers
-- don't have to special-case Timestamp/Duration/Struct/etc.

---@class pb.MessageView
---@field get fun(self: pb.MessageView, name: string): any
---@field has fun(self: pb.MessageView, name: string): boolean
---@field which fun(self: pb.MessageView, oneof_name: string): string?
---@field iter fun(self: pb.MessageView): fun(): string?, any
---@field names fun(self: pb.MessageView): fun(): string?
---@field set fun(self: pb.MessageView, name: string, value: any)
---@field is_dirty fun(self: pb.MessageView): boolean
---@field totable fun(self: pb.MessageView): table
---@field encode fun(self: pb.MessageView): string
---
---@class pb.ArrayView
---@field len fun(self: pb.ArrayView): integer
---@field at fun(self: pb.ArrayView, i: integer): any
---@field iter fun(self: pb.ArrayView): fun(): integer?, any
---@field tolist fun(self: pb.ArrayView): any[]
---
---@class pb.MapView
---@field get fun(self: pb.MapView, k: any): any
---@field has fun(self: pb.MapView, k: any): boolean
---@field keys fun(self: pb.MapView): any[]
---@field iter fun(self: pb.MapView): fun(): any?, any
---@field totable fun(self: pb.MapView): table

local wire  = require('pb.wire')
local codec = require('pb.codec')