From 0666a6485f47f04c36fb0f7ff3db68d7b0406470 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Sat, 23 May 2026 23:13:59 +0300 Subject: [PATCH] lsp: annotate codec / grpc / json / wkt public surface (74c) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase-2 of the LSP / LLM affordance work (phase-1 landed annotations on init.lua + lazy.lua). Adds ---@param / ---@return on the public-surface entry points so editors and LLM assistants see typed signatures on hover. - codec.lua: encode_message, decode_message, encode_field, compile_writers, compile_readers, merge_message. - grpc.lua: loopback, multiplex, new_stream_pair, wrap_call, wrap_server_stream, wrap_server_view. - json.lua: M.encode, M.decode. New pb.JsonEncodeOpts and pb.JsonDecodeOpts @class blocks in _types.lua document the opts fields the implementation actually consults (use_proto_names, emit_defaults / always_emit_zero_value alias, indent; ignore_unknown_fields on decode). - wkt.lua: register, lookup, any_pack, any_unpack. New pb.AnyMessage @class for the {type_url, value} shape. Also corrects two pre-existing signature lies in _types.lua: - pb.register is `(desc): pb.Descriptor`, not `(full_name, desc)` — the implementation has always derived the key from desc.name and all callers pass a single arg. - pb.any.pack is `(desc, t, type_url_prefix?)`, not `(t, type_url)`. Pure metadata — `just test` stays at 752 and `just test-c` at 1043. bd-74c --- runtime/pb/_types.lua | 33 ++++++++++++++++++++++++--------- runtime/pb/codec.lua | 15 +++++++++++++++ runtime/pb/grpc.lua | 19 +++++++++++++++++++ runtime/pb/json.lua | 8 ++++++++ runtime/pb/wkt.lua | 11 +++++++++++ 5 files changed, 77 insertions(+), 9 deletions(-) diff --git a/runtime/pb/_types.lua b/runtime/pb/_types.lua index f346d6ca02ce6230f029e6d11b88f7d2a49caeb4..44c7eb8b100d931c3e78dd39143c33ae24708445 100644 --- a/runtime/pb/_types.lua +++ b/runtime/pb/_types.lua @@ -81,8 +81,8 @@ ---@field codec table # internal: shared with generated code ---@field wkt pb.Wkt ---@field NULL userdata # google.protobuf.Value null sentinel ----@field register fun(full_name: string, desc: pb.Descriptor) ----@field lookup fun(full_name: string): pb.Descriptor? +---@field register fun(desc: pb.Descriptor): pb.Descriptor +---@field lookup fun(name_or_url: string): pb.Descriptor? ---@field any pb.Any ---@field grpc pb.Grpc ---@field c_runtime? table # set when PB_ENABLE_C=1 and load succeeds @@ -104,18 +104,33 @@ ---@field finalize_message fun(desc: pb.Descriptor): pb.Descriptor ---@field register_extension fun(extendee_desc: pb.Descriptor, ext: pb.Field) +---@class pb.AnyMessage +---@field type_url string +---@field value string + ---@class pb.Any ----@field pack fun(t: table, type_url: string): table # returns google.protobuf.Any shape ----@field unpack fun(any_msg: table): table?, pb.Descriptor? +---@field pack fun(desc: pb.Descriptor, t: table, type_url_prefix?: string): pb.AnyMessage +---@field unpack fun(any_msg: pb.AnyMessage, desc_override?: pb.Descriptor): table ---@class pb.Wkt ----@field NULL userdata ----@field register fun(full_name: string, desc: pb.Descriptor) ----@field lookup fun(full_name: string): pb.Descriptor? +---@field NULL userdata +---@field register fun(desc: pb.Descriptor): pb.Descriptor # key is desc.name; also indexes the default type.googleapis.com/ URL +---@field lookup fun(name_or_url: string): pb.Descriptor? +---@field any_pack fun(desc: pb.Descriptor, t: table, type_url_prefix?: string): pb.AnyMessage +---@field any_unpack fun(any_msg: pb.AnyMessage, desc_override?: pb.Descriptor): table + +---@class pb.JsonEncodeOpts +---@field use_proto_names? boolean emit snake_case field names instead of camelCase +---@field emit_defaults? boolean emit fields equal to proto3 defaults (alias: always_emit_zero_value) +---@field always_emit_zero_value? boolean deprecated alias of emit_defaults +---@field indent? string non-empty string ⇒ pretty-print with that indent unit + +---@class pb.JsonDecodeOpts +---@field ignore_unknown_fields? boolean silently drop unknown JSON fields instead of erroring ---@class pb.Json ----@field encode fun(desc: pb.Descriptor, t: table, opts?: table): string ----@field decode fun(desc: pb.Descriptor, s: string, opts?: table): table +---@field encode fun(desc: pb.Descriptor, t: table, opts?: pb.JsonEncodeOpts): string +---@field decode fun(desc: pb.Descriptor, s: string, opts?: pb.JsonDecodeOpts): table ---@class pb.Text ---@field encode fun(desc: pb.Descriptor, t: table, opts?: pb.TextOpts): string diff --git a/runtime/pb/codec.lua b/runtime/pb/codec.lua index 6724e6c2564b72ca010310049813492720b6f805..73c8a3715619f9f3c78539dce078472960645e33 100644 --- a/runtime/pb/codec.lua +++ b/runtime/pb/codec.lua @@ -80,6 +80,9 @@ local decode_group -- - sub-message fields: recursive merge -- Sub-messages whose descriptor carries a custom decode (WKT) are replaced -- wholesale because their decoded value is not a generic Lua table. +---@param desc pb.Descriptor +---@param prev table decoded message being accumulated +---@param decoded table newly-decoded copy to merge into `prev` in place local function merge_message(desc, prev, decoded) for i = 1, #desc.fields do local f = desc.fields[i] @@ -625,12 +628,17 @@ end -- without walking a full message (e.g. lazy passthrough re-encode, which -- splices original wire segments for untouched fields and calls -- encode_field for the dirty ones). +---@param field pb.Field +---@param value any +---@param out string[] table.concat-friendly chunk buffer; encoded bytes are appended +---@param force? boolean bypass proto3 default-value elision (used for extensions and inside oneofs) M.encode_field = function(field, value, out, force) return encode_field(field, value, out, force) end -- compile_writers attaches `f._writer` to each field where the shape is -- specialized. Called from pb.finalize_message after the oneof flatten. +---@param desc pb.Descriptor function M.compile_writers(desc) for _, f in ipairs(desc.fields) do f._writer = build_writer(f, desc.name) @@ -863,12 +871,16 @@ local function build_reader(f) return nil end +---@param desc pb.Descriptor function M.compile_readers(desc) for _, f in ipairs(desc.fields) do f._reader = build_reader(f) end end +---@param desc pb.Descriptor +---@param data table message contents keyed by proto field name +---@return string wire-format bytes (proto3 / proto2) encode_message = function(desc, data) if type(data) ~= 'table' then error(("expected table for message %s, got %s"):format(desc.name, type(data)), 0) @@ -1129,6 +1141,9 @@ decode_extension = function(ext, buf, pos, wt, result) error("decode_extension: unknown kind " .. tostring(kind), 0) end +---@param desc pb.Descriptor +---@param buf string wire-format bytes +---@return table decoded message; unknown fields go in `_unknown_fields`, extensions in `_extensions` decode_message = function(desc, buf) if type(buf) ~= 'string' then error(("expected string for decode of %s, got %s"):format(desc.name, type(buf)), 0) diff --git a/runtime/pb/grpc.lua b/runtime/pb/grpc.lua index 83b5eb641b6041586faa8c3515720e8ca7b858a8..924bb03a323545a6b0e0bce39d9e0357fd98213a 100644 --- a/runtime/pb/grpc.lua +++ b/runtime/pb/grpc.lua @@ -65,6 +65,10 @@ end -- loopback to bridge an in-process server fiber with a client caller. -- The returned `internal_state` is exposed so the transport (not the -- caller) can flag errors and trigger close. +---@param buf_size? integer fiber.channel capacity; defaults to DEFAULT_BUFFER +---@return table client speaks send / close_send / recv / cancel +---@return table server speaks recv / send / _finish / _force_close_recv +---@return table internal_state shared {canceled, server_err} accessed by the transport function M.new_stream_pair(buf_size) buf_size = buf_size or DEFAULT_BUFFER local c2s = fiber.channel(buf_size) -- client -> server @@ -189,6 +193,8 @@ end -- loopback(server) bridges an in-process M._server(impl) result -- into the transport contract. Streaming methods run their handler on a -- worker fiber and communicate via fiber.channel. +---@param server pb.GrpcServer output of `M._server(impl)` +---@return pb.GrpcTransport function M.loopback(server) if type(server) ~= 'table' or type(server.methods) ~= 'table' then error("pb.grpc.loopback: expected a server table from M._server()", 0) @@ -213,6 +219,8 @@ end -- multiplex({server1, server2, ...}) merges several servers' methods + -- streams under a single transport. Errors on duplicate paths. +---@param servers pb.GrpcServer[] +---@return pb.GrpcTransport function M.multiplex(servers) local methods, streams = {}, {} for _, srv in ipairs(servers) do @@ -254,6 +262,9 @@ end -- means we can refactor the streaming surface without re-running protoc. -- Wrap a server-streaming call: caller calls stream:recv() until nil. +---@param raw table transport-side stream view (bytes) +---@param output_decode fun(bytes: string): table per-message decoder for the typed view +---@return table {recv(self): msg?, err?; cancel(self)} function M.wrap_server_stream(raw, output_decode) return { recv = function(_) @@ -266,6 +277,10 @@ function M.wrap_server_stream(raw, output_decode) end -- Wrap a client-streaming or bidi call: caller sends + recvs. +---@param raw table transport-side stream view (bytes) +---@param input_encode fun(msg: table): string per-message encoder for the typed view +---@param output_decode fun(bytes: string): table per-message decoder for the typed view +---@return table {send, close_send, recv, cancel} function M.wrap_call(raw, input_encode, output_decode) return { send = function(_, msg) @@ -284,6 +299,10 @@ end -- Wrap a server-side stream view for the generated server handler: -- the user-supplied impl is called with a stream that speaks decoded -- messages, hiding the per-message encode/decode boundary. +---@param raw table server-side stream view (bytes) +---@param input_decode? fun(bytes: string): table decoder for inbound messages (nil ⇒ server_stream: no inbound) +---@param output_encode? fun(msg: table): string encoder for outbound messages (nil ⇒ client_stream: no outbound) +---@return table {recv?, send?, close_send?, cancel} function M.wrap_server_view(raw, input_decode, output_encode) local wrapped = {} if input_decode ~= nil then diff --git a/runtime/pb/json.lua b/runtime/pb/json.lua index 57975ff7dd38f02809cdffbe0d6b67bebd233196..e03d15624b8340a35de8737339c1204d95ca8d66 100644 --- a/runtime/pb/json.lua +++ b/runtime/pb/json.lua @@ -967,6 +967,10 @@ end to_json_value = encode_message +---@param desc pb.Descriptor +---@param t table +---@param opts? pb.JsonEncodeOpts +---@return string function M.encode(desc, t, opts) if opts ~= nil and type(opts) ~= 'table' then error('pb.json.encode: opts must be a table, got ' .. type(opts), 0) @@ -1485,6 +1489,10 @@ local function find_duplicate_json_keys(s) end end +---@param desc pb.Descriptor +---@param s string +---@param opts? pb.JsonDecodeOpts +---@return table function M.decode(desc, s, opts) local dup_err = find_duplicate_json_keys(s) if dup_err ~= nil then error(dup_err, 0) end diff --git a/runtime/pb/wkt.lua b/runtime/pb/wkt.lua index 22b5db3f20902397ad72974e7648fdcd0b22365b..5ac017fb409dd4d303d58a1abb8ccf2e56203b38 100644 --- a/runtime/pb/wkt.lua +++ b/runtime/pb/wkt.lua @@ -546,6 +546,8 @@ local function type_url_full_name(url) return url:match('([^/]+)$') or url end +---@param desc pb.Descriptor +---@return pb.Descriptor M.register = function(desc) if type(desc) ~= 'table' or desc.name == nil then error('pb.register: expected a descriptor with a `name` field', 0) @@ -555,11 +557,17 @@ M.register = function(desc) return desc end +---@param name_or_url string bare full name (`pkg.Foo`) or a type URL (`type.googleapis.com/pkg.Foo`) +---@return pb.Descriptor? M.lookup = function(name_or_url) return REGISTRY[name_or_url] or REGISTRY[type_url_full_name(name_or_url)] end -- Pack a Lua message table into an opaque Any form. +---@param desc pb.Descriptor +---@param t table +---@param type_url_prefix? string defaults to `type.googleapis.com/` +---@return pb.AnyMessage M.any_pack = function(desc, t, type_url_prefix) if desc == nil or desc.name == nil then error('pb.any.pack: descriptor must have a `name`', 0) @@ -571,6 +579,9 @@ M.any_pack = function(desc, t, type_url_prefix) end -- Unpack an Any table. `desc_or_nil` overrides the registry lookup. +---@param any_t pb.AnyMessage google.protobuf.Any-shaped table {type_url=..., value=...} +---@param desc_or_nil? pb.Descriptor override the registry lookup +---@return table M.any_unpack = function(any_t, desc_or_nil) if type(any_t) ~= 'table' then error('pb.any.unpack: expected Any table', 0)