kobako 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +127 -0
  4. data/Cargo.lock +15 -5
  5. data/README.md +96 -44
  6. data/ROADMAP.md +1 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +44 -0
  8. data/crates/kobako-runtime/Cargo.toml +8 -1
  9. data/crates/kobako-runtime/README.md +1 -1
  10. data/crates/kobako-runtime/src/dispatch.rs +17 -11
  11. data/crates/kobako-runtime/src/error.rs +38 -6
  12. data/crates/kobako-runtime/src/lib.rs +13 -2
  13. data/crates/kobako-runtime/src/profile.rs +8 -0
  14. data/crates/kobako-runtime/src/runtime.rs +7 -2
  15. data/crates/kobako-runtime/src/snapshot.rs +13 -4
  16. data/crates/kobako-runtime/src/yielder.rs +4 -4
  17. data/crates/kobako-transport/CHANGELOG.md +31 -0
  18. data/crates/kobako-transport/Cargo.toml +27 -0
  19. data/crates/kobako-transport/README.md +43 -0
  20. data/crates/kobako-transport/src/abi.rs +89 -0
  21. data/crates/kobako-transport/src/envelope/bytes.rs +234 -0
  22. data/crates/kobako-transport/src/envelope/call.rs +222 -0
  23. data/crates/kobako-transport/src/envelope/error_record.rs +101 -0
  24. data/crates/kobako-transport/src/envelope/fault.rs +171 -0
  25. data/crates/kobako-transport/src/envelope/invocation_frames.rs +232 -0
  26. data/crates/kobako-transport/src/envelope/outcome.rs +261 -0
  27. data/crates/kobako-transport/src/envelope/reply.rs +240 -0
  28. data/crates/kobako-transport/src/envelope/run.rs +85 -0
  29. data/crates/kobako-transport/src/envelope.rs +73 -0
  30. data/crates/kobako-transport/src/lib.rs +21 -0
  31. data/crates/kobako-wasmtime/CHANGELOG.md +30 -0
  32. data/crates/kobako-wasmtime/Cargo.toml +5 -2
  33. data/crates/kobako-wasmtime/README.md +1 -1
  34. data/crates/kobako-wasmtime/src/config.rs +19 -12
  35. data/crates/kobako-wasmtime/src/dispatch.rs +15 -8
  36. data/crates/kobako-wasmtime/src/driver.rs +8 -27
  37. data/crates/kobako-wasmtime/src/frames.rs +11 -9
  38. data/crates/kobako-wasmtime/src/guest_mem.rs +7 -15
  39. data/crates/kobako-wasmtime/src/instance_pre.rs +2 -2
  40. data/crates/kobako-wasmtime/src/invocation.rs +1 -1
  41. data/crates/kobako-wasmtime/src/lib.rs +1 -1
  42. data/data/kobako.wasm +0 -0
  43. data/ext/kobako/Cargo.toml +7 -2
  44. data/ext/kobako/src/runtime/bridge.rs +103 -40
  45. data/ext/kobako/src/runtime/errors.rs +13 -5
  46. data/ext/kobako/src/runtime/gvl.rs +114 -0
  47. data/ext/kobako/src/runtime.rs +307 -207
  48. data/lib/kobako/catalog/extensions.rb +50 -43
  49. data/lib/kobako/catalog/handles.rb +7 -16
  50. data/lib/kobako/catalog/services.rb +24 -46
  51. data/lib/kobako/catalog/snippets.rb +16 -33
  52. data/lib/kobako/codec/decoder.rb +10 -13
  53. data/lib/kobako/codec/encoder.rb +7 -7
  54. data/lib/kobako/codec/error.rb +6 -6
  55. data/lib/kobako/codec/ext_types.rb +12 -72
  56. data/lib/kobako/codec/handle_walk.rb +6 -6
  57. data/lib/kobako/codec/state.rb +4 -56
  58. data/lib/kobako/codec/utils.rb +4 -4
  59. data/lib/kobako/codec.rb +11 -21
  60. data/lib/kobako/context.rb +208 -0
  61. data/lib/kobako/errors.rb +59 -16
  62. data/lib/kobako/execution.rb +52 -0
  63. data/lib/kobako/extension.rb +24 -10
  64. data/lib/kobako/outcome.rb +71 -128
  65. data/lib/kobako/payload/arguments.rb +59 -0
  66. data/lib/kobako/payload.rb +19 -0
  67. data/lib/kobako/pool.rb +1 -7
  68. data/lib/kobako/sandbox.rb +51 -191
  69. data/lib/kobako/sandbox_options.rb +24 -3
  70. data/lib/kobako/snippet/binary.rb +3 -4
  71. data/lib/kobako/snippet/source.rb +5 -6
  72. data/lib/kobako/snippet.rb +2 -2
  73. data/lib/kobako/transport/call.rb +21 -0
  74. data/lib/kobako/transport/dispatcher.rb +94 -142
  75. data/lib/kobako/transport/error.rb +21 -5
  76. data/lib/kobako/transport/reflection.rb +82 -0
  77. data/lib/kobako/transport/run.rb +25 -33
  78. data/lib/kobako/transport/yielder.rb +27 -36
  79. data/lib/kobako/transport.rb +11 -8
  80. data/lib/kobako/unresolved.rb +17 -0
  81. data/lib/kobako/usage.rb +7 -7
  82. data/lib/kobako/version.rb +1 -1
  83. data/lib/kobako.rb +1 -0
  84. data/release-please-config.json +47 -2
  85. data/sig/kobako/catalog/extensions.rbs +4 -6
  86. data/sig/kobako/catalog/handles.rbs +0 -2
  87. data/sig/kobako/catalog/services.rbs +4 -4
  88. data/sig/kobako/catalog/snippets.rbs +4 -2
  89. data/sig/kobako/codec/error.rbs +4 -4
  90. data/sig/kobako/codec/ext_types.rbs +0 -7
  91. data/sig/kobako/codec/state.rbs +0 -4
  92. data/sig/kobako/codec.rbs +4 -5
  93. data/sig/kobako/context.rbs +36 -0
  94. data/sig/kobako/errors.rbs +27 -6
  95. data/sig/kobako/execution.rbs +24 -0
  96. data/sig/kobako/extension.rbs +4 -2
  97. data/sig/kobako/outcome.rbs +12 -13
  98. data/sig/kobako/payload/arguments.rbs +26 -0
  99. data/sig/kobako/payload.rbs +4 -0
  100. data/sig/kobako/pool.rbs +0 -2
  101. data/sig/kobako/runtime.rbs +44 -14
  102. data/sig/kobako/sandbox.rbs +5 -24
  103. data/sig/kobako/sandbox_options.rbs +9 -2
  104. data/sig/kobako/snippet/binary.rbs +1 -1
  105. data/sig/kobako/snippet/source.rbs +1 -1
  106. data/sig/kobako/transport/call.rbs +21 -0
  107. data/sig/kobako/transport/dispatcher.rbs +9 -11
  108. data/sig/kobako/transport/error.rbs +11 -0
  109. data/sig/kobako/transport/reflection.rbs +15 -0
  110. data/sig/kobako/transport/run.rbs +1 -1
  111. data/sig/kobako/transport/yielder.rbs +2 -2
  112. data/sig/kobako/transport.rbs +8 -2
  113. data/sig/kobako/unresolved.rbs +4 -0
  114. metadata +30 -11
  115. data/lib/kobako/fault.rb +0 -38
  116. data/lib/kobako/outcome/panic.rb +0 -42
  117. data/lib/kobako/transport/request.rb +0 -76
  118. data/lib/kobako/transport/response.rb +0 -83
  119. data/lib/kobako/transport/yield.rb +0 -94
  120. data/sig/kobako/fault.rbs +0 -19
  121. data/sig/kobako/outcome/panic.rbs +0 -34
  122. data/sig/kobako/transport/request.rbs +0 -29
  123. data/sig/kobako/transport/response.rbs +0 -33
  124. data/sig/kobako/transport/yield.rbs +0 -34
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../unresolved"
4
+
3
5
  module Kobako
4
6
  module Catalog
5
7
  # Kobako::Catalog::Extensions — per-Sandbox registry of installed
@@ -7,7 +9,9 @@ module Kobako
7
9
  # install time (its +source+ into +Catalog::Snippets+, its +backend+
8
10
  # path into +Catalog::Services+), asserts declared dependencies are
9
11
  # present when the Sandbox seals, and resolves each callable-backed
10
- # path to a fresh object at the start of every invocation.
12
+ # path to a fresh object for every invocation — returning that map to the
13
+ # driving +Context+ rather than mutating the shared registry, so
14
+ # concurrent invocations stay shared-nothing.
11
15
  #
12
16
  # Sealing and the reject-after-install guard are governed by the owning
13
17
  # Sandbox through +Catalog::Services#sealed?+, the shared seal signal;
@@ -21,77 +25,87 @@ module Kobako
21
25
 
22
26
  # Install +extension+: validate its shape, register its +source+ as a
23
27
  # preloaded snippet, and — when it carries a +backend+ — reserve the
24
- # backend path in +services+. A callable provider reserves the path
25
- # with a placeholder the per-invocation refresh replaces; a fixed
26
- # provider binds its object directly. Raises +ArgumentError+ for the two
27
- # shapes +#validate!+ owns — +source+ not a String, or a present
28
- # +backend+ that omits +path+ / +provider+. The Extension readers are
29
- # duck-typed, so an object missing them surfaces the underlying
30
- # +NoMethodError+, and a malformed +name+ or +backend.path+ surfaces
31
- # through the +snippets+ / +services+ registration it routes to.
28
+ # backend path in +services+. A static +object:+ binds directly; a
29
+ # per-invocation +provider:+ or a fillable backend reserves the path with
30
+ # +Kobako::Unresolved+ (the per-invocation result is layered onto the
31
+ # Context, a fillable stays Unresolved until filled). Raises
32
+ # +ArgumentError+ for the two shapes +#validate!+ owns — +source+ not a
33
+ # String, or a present +backend+ that omits +path+ / +object+ /
34
+ # +provider+. The Extension readers are duck-typed, so an object missing
35
+ # them surfaces the underlying +NoMethodError+, and a malformed +name+ or
36
+ # +backend.path+ surfaces through the +snippets+ / +services+
37
+ # registration it routes to.
32
38
  def install(extension, snippets:, services:)
33
39
  validate!(extension)
34
40
  snippets.register(code: extension.source, name: extension.name)
35
41
  backend = extension.backend
36
- services.bind(backend.path, initial_object(backend.provider)) if backend
42
+ services.bind(backend.path, install_object(backend)) if backend
37
43
  @entries << extension
38
44
  self
39
45
  end
40
46
 
41
47
  # Assert every installed Extension's +depends_on+ names a fellow
42
- # installed Extension. Runs once, when the Sandbox first seals; an
43
- # unmet dependency raises +ArgumentError+ naming the gap, before the
44
- # guest runs. Idempotent across later invocations.
48
+ # installed Extension. Runs once, when the Sandbox first seals
49
+ # successfully; an unmet dependency raises +ArgumentError+ naming the
50
+ # gap, before the guest runs. The asserted flag flips only on success,
51
+ # so a seal that failed re-checks on the next attempt rather than
52
+ # silently passing a broken Sandbox. Idempotent across later invocations.
45
53
  def seal!
46
54
  return self if @asserted
47
55
 
48
- @asserted = true
49
56
  assert_dependencies!
57
+ @asserted = true
50
58
  self
51
59
  end
52
60
 
53
- # Resolve each callable-backed path to this invocation's object and
54
- # refresh it behind its already-sealed path in +services+. Distinct
55
- # providers yield distinct objects; one provider shared by several
56
- # Extensions is invoked once and its result shared, so provider
57
- # identity is resource identity. Fixed providers are left untouched —
58
- # they stay the object bound at install.
59
- def refresh_backends!(services)
60
- resolved = {} # : Hash[untyped, untyped]
61
- resolved.compare_by_identity
62
- @entries.each { |extension| refresh_backend(extension, services, resolved) }
63
- self
61
+ # Resolve each +provider:+-backed path to this invocation's object and
62
+ # return the +path => object+ map the driving +Context+ layers over the
63
+ # static base bindings. Distinct providers yield distinct objects; one
64
+ # provider shared by several Extensions is invoked once and its result
65
+ # shared, so provider identity is resource identity. Static and fillable
66
+ # backends are omitted — they stay the object bound at install in the
67
+ # base registry (+Kobako::Unresolved+ for a fillable).
68
+ def resolve
69
+ resolved = {} # : Hash[String, untyped]
70
+ by_provider = {} # : Hash[untyped, untyped]
71
+ by_provider.compare_by_identity
72
+ @entries.each { |extension| resolve_backend(extension, resolved, by_provider) }
73
+ resolved
64
74
  end
65
75
 
66
76
  private
67
77
 
68
- # Resolve one Extension's callable-backed path against the shared
69
- # per-invocation +resolved+ cache (keyed by provider identity) and
70
- # refresh it in +services+. A fixed provider is skipped — its object
71
- # stays as bound at install.
72
- def refresh_backend(extension, services, resolved)
78
+ # The object a backend binds at install: a static +object:+ directly, or
79
+ # +Kobako::Unresolved+ as the placeholder for a per-invocation +provider:+
80
+ # (layered onto the Context) or a fillable (neither keyword).
81
+ def install_object(backend) = backend.object.nil? ? Kobako::Unresolved : backend.object
82
+
83
+ # Resolve one Extension's +provider:+-backed path against the shared
84
+ # per-invocation identity cache (keyed by provider identity) and record
85
+ # it in +resolved+. A static or fillable backend is skipped — its object
86
+ # stays as bound at install in the base registry.
87
+ def resolve_backend(extension, resolved, by_provider)
73
88
  backend = extension.backend
74
89
  return unless backend
75
90
 
76
91
  provider = backend.provider
77
- return unless callable?(provider)
92
+ return if provider.nil?
78
93
 
79
- object = resolved.fetch(provider) { resolved[provider] = provider.call }
80
- services.refresh(backend.path, object)
94
+ resolved[backend.path.to_s] = by_provider.fetch(provider) { by_provider[provider] = provider.call }
81
95
  end
82
96
 
83
97
  # Enforce the Extension-shape checks +#preload+ / +#bind+ do not: a
84
98
  # mandatory String +source+ (the install/bind boundary) and a
85
- # +backend+ that, when present, exposes +path+ and +provider+.
99
+ # +backend+ that, when present, exposes +path+ / +object+ / +provider+.
86
100
  def validate!(extension)
87
101
  source = extension.source
88
102
  raise ArgumentError, "Extension #source must be a String, got #{source.class}" unless source.is_a?(String)
89
103
 
90
104
  backend = extension.backend
91
105
  return if backend.nil?
92
- return if backend.respond_to?(:path) && backend.respond_to?(:provider)
106
+ return if backend.respond_to?(:path) && backend.respond_to?(:object) && backend.respond_to?(:provider)
93
107
 
94
- raise ArgumentError, "Extension #backend must expose #path and #provider"
108
+ raise ArgumentError, "Extension #backend must expose #path, #object, and #provider"
95
109
  end
96
110
 
97
111
  def assert_dependencies!
@@ -110,13 +124,6 @@ module Kobako
110
124
  # of a constant token are interchangeable; a value that is neither
111
125
  # falls through unchanged to the not-installed path.
112
126
  def symbolize(name) = name.is_a?(String) ? name.to_sym : name
113
-
114
- def callable?(provider) = provider.respond_to?(:call)
115
-
116
- # A callable provider reserves its path with a placeholder that the
117
- # per-invocation refresh replaces before any dispatch; a fixed
118
- # provider is the bound object itself.
119
- def initial_object(provider) = callable?(provider) ? nil : provider
120
127
  end
121
128
  end
122
129
  end
@@ -5,10 +5,10 @@ require_relative "../handle"
5
5
  module Kobako
6
6
  module Catalog
7
7
  # Host-side mapping from opaque integer Handle IDs to Ruby objects.
8
- # The table is owned by +Kobako::Sandbox+ and injected
9
- # into the per-Sandbox +Kobako::Catalog::Services+ so guest→host dispatch
10
- # resolves Handle targets and arguments against the same table that
11
- # host→guest wire encoding allocates into.
8
+ # Each invocation's +Kobako::Context+ mints its own table and hands it to
9
+ # the dispatcher, so guest→host dispatch resolves Handle targets and
10
+ # arguments against the same table that host→guest wire encoding allocates
11
+ # into for that run.
12
12
  #
13
13
  # Lifecycle invariants:
14
14
  #
@@ -17,10 +17,9 @@ module Kobako
17
17
  # invocation is 1; ID 0 is reserved as the invalid sentinel and is
18
18
  # never returned by +#alloc+.
19
19
  #
20
- # - At every invocation boundary (via +#reset!+), every Handle issued
21
- # under the old state becomes invalid. Reset applies uniformly
22
- # regardless of allocation source (Service return or host-injected
23
- # argument).
20
+ # - A fresh table backs each invocation, so a Handle issued in one
21
+ # invocation resolves in no other — uniformly regardless of
22
+ # allocation source (Service return or host-injected argument).
24
23
  #
25
24
  # - The cap is +0x7fff_ffff+ (2³¹ − 1). Allocation beyond the cap
26
25
  # raises immediately — no silent truncation, no wrap, no ID reuse.
@@ -63,14 +62,6 @@ module Kobako
63
62
  @entries[id]
64
63
  end
65
64
 
66
- # Clear all entries AND reset the counter to 1. Called at the
67
- # per-invocation boundary by +Kobako::Sandbox+. Returns +self+.
68
- def reset!
69
- @entries.clear
70
- @next_id = 1
71
- self
72
- end
73
-
74
65
  # Number of currently-bound entries. Used by tests of the Dispatcher
75
66
  # and Codec::HandleWalk#deep_wrap to observe whether each path allocates
76
67
  # exactly the Handle entries it should — the +Handles+ table itself never
@@ -1,42 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "handles"
4
- require_relative "../codec"
5
3
  require_relative "../errors"
6
4
 
7
5
  module Kobako
8
6
  module Catalog
9
7
  # Kobako::Catalog::Services — per-Sandbox registry of Service
10
8
  # bindings keyed by their constant-path name. Holds the flat
11
- # path→object table and the preamble emitted on Frame 1.
9
+ # path→object table and the declared path set every invocation
10
+ # announces on Frame 1.
12
11
  #
13
12
  # Public API:
14
13
  #
15
14
  # services = Kobako::Catalog::Services.new
16
15
  # services.bind("MyService::KV", kv_object) # => services (chainable)
17
- # services.encode # => msgpack bytes for Frame 1
16
+ # services.paths # => ["MyService::KV"]
18
17
  # services.lookup("MyService::KV") # => kv_object
19
18
  #
20
19
  # Per-dispatch routing is +Kobako::Transport::Dispatcher+'s
21
- # responsibility — the Dispatcher receives this registry and the
22
- # +Catalog::Handles+ as arguments from the +Runtime#on_dispatch+ Proc
23
- # that +Kobako::Sandbox#initialize+ installs. The registry holds an
24
- # injected +Catalog::Handles+ reference so dispatch target resolution
25
- # and host→guest auto-wrap share the same Sandbox-owned allocator.
20
+ # responsibility — the Dispatcher resolves a path against the invocation's
21
+ # +Kobako::Context+ (which layers per-invocation providers and +ctx.bind+
22
+ # overrides over these base bindings) and the +Catalog::Handles+, both
23
+ # passed in the per-invocation dispatch Proc the Context hands to
24
+ # +Runtime#eval+ / +#run+.
26
25
  class Services
27
26
  # Ruby constant-name pattern each +::+-separated bind-path segment
28
27
  # must match.
29
28
  NAME_PATTERN = /\A[A-Z]\w*\z/
30
29
 
31
- # Build a fresh registry. +handler+ is an internal seam that injects
32
- # a pre-configured +Catalog::Handles+; tests pass one whose +next_id+
33
- # is pinned near +MAX_ID+ to exercise the cap-exhaustion path
34
- # without 2³¹ allocations. Production callers leave it at the default.
35
- def initialize(handler: Catalog::Handles.new)
30
+ # Build a fresh registry.
31
+ def initialize
36
32
  @bindings = {} # : Hash[String, untyped]
37
- @handler = handler
38
33
  @sealed = false
39
- @encoded = nil # : String?
40
34
  end
41
35
 
42
36
  # Bind +object+ as the Service reachable at +path+ — a +Symbol+ or
@@ -56,6 +50,12 @@ module Kobako
56
50
  self
57
51
  end
58
52
 
53
+ # Returns +true+ iff a Service is bound at +path+ — the declared (Frame 1)
54
+ # key set a per-invocation +ctx.bind+ override may target.
55
+ def bound?(path)
56
+ @bindings.key?(path.to_s)
57
+ end
58
+
59
59
  # Resolve a +target+ constant path to the bound Service. Raises
60
60
  # +KeyError+ when no Service is bound at +target+.
61
61
  def lookup(target)
@@ -65,38 +65,16 @@ module Kobako
65
65
  @bindings[target_str]
66
66
  end
67
67
 
68
- # Replace the object bound at an already-registered +path+ without the
69
- # collision or seal checks +#bind+ applies. +Catalog::Extensions+ calls
70
- # this during +begin_invocation!+ to swap a callable backend provider's
71
- # per-invocation result behind a path whose Frame 1 membership was
72
- # fixed at the seal. A path that was never registered is left untouched,
73
- # so the sealed key set can never grow here.
74
- def refresh(path, object)
75
- key = path.to_s
76
- @bindings[key] = object if @bindings.key?(key)
77
- self
78
- end
79
-
80
- # Encode the preamble as msgpack bytes for stdin Frame 1 delivery —
81
- # a flat array of the bound constant paths, in bind order:
82
- # +["MyService::KV", "File"]+. Routes through Kobako::Codec::Encoder
83
- # like every other host-side wire encode; the preamble carries only
84
- # Strings, so none of the kobako ext types fire. Returns a binary
85
- # +String+ of msgpack bytes.
86
- #
87
- # Once sealed, the bytes are computed once and reused for every
88
- # subsequent invocation: sealing freezes Service registration at the
89
- # first invocation, so a bind reaching the registry after the seal
90
- # raises +ArgumentError+ and never alters Frame 1.
91
- def encode
92
- return @encoded if @encoded
93
-
94
- bytes = Codec::Encoder.encode(@bindings.keys).freeze
95
- @encoded = bytes if @sealed
96
- bytes
68
+ # The bound constant paths in bind order — +["MyService::KV",
69
+ # "File"]+ — which +Runtime#eval+ / +#run+ frame into the Frame 1
70
+ # preamble. The registry holds the bindings; the wire layout is the
71
+ # native side's.
72
+ def paths
73
+ @bindings.keys
97
74
  end
98
75
 
99
- # Mark the registry as sealed. Called by +Sandbox+ on the first
76
+ # Mark the registry as sealed so registration can never alter the
77
+ # declared path set afterwards. Called by +Sandbox+ on the first
100
78
  # invocation; afterwards #bind raises ArgumentError. Idempotent;
101
79
  # returns +self+.
102
80
  def seal!
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../codec"
4
3
  require_relative "../snippet"
5
4
 
6
5
  module Kobako
@@ -28,30 +27,16 @@ module Kobako
28
27
 
29
28
  def initialize
30
29
  @entries = [] # : Array[Kobako::Snippet::Source | Kobako::Snippet::Binary]
31
- @encoded = nil # : String?
32
30
  end
33
31
 
34
- # Serialize the registered snippets to wire bytes. Each entry
35
- # contributes a msgpack map shape; the collection rides as a single
36
- # msgpack array. An empty registry serializes to an empty array, never
37
- # absent. The wire codec is an implementation detail — callers
38
- # receive a binary +String+ that the +Kobako::Runtime+ layer ships
39
- # through the invocation channel. The entry value objects stay pure
40
- # carriers — this collection-tier method reads their attributes
41
- # externally via +entry_payload+ rather than asking each entry to
42
- # self-encode.
43
- #
44
- # The bytes are memoized — the table is replayed verbatim on every
45
- # invocation after sealing, so Frame 3 never changes between
46
- # encodes; #register drops the memo while the table is still open.
47
- # Unlike +Catalog::Services#encode+, which gates its memo on the
48
- # seal, this one can fill eagerly and invalidate in +#register+
49
- # because every mutation funnels through that single method — there is
50
- # no out-of-sight child object to change the result behind its back.
51
- def encode
52
- return @encoded if @encoded
53
-
54
- @encoded = Codec::Encoder.encode(@entries.map { |entry| entry_payload(entry) }).freeze
32
+ # The registered snippets in insertion order, each projected as the
33
+ # +[kind, name, body]+ triple +Runtime#eval+ / +#run+ frame into
34
+ # Frame 3 — +kind+ names the form as a Symbol, +name+ is +nil+ for
35
+ # the bytecode form. The entry value objects stay pure carriers, so
36
+ # this collection-tier method reads their attributes externally
37
+ # rather than asking each entry to project itself.
38
+ def entries
39
+ @entries.map { |entry| entry_tuple(entry) }
55
40
  end
56
41
 
57
42
  # Register one preloaded snippet in either of two forms.
@@ -73,7 +58,6 @@ module Kobako
73
58
  # missing keywords, wrong types, malformed +name+, or
74
59
  # duplicate +code:+ +name+.
75
60
  def register(code: nil, name: nil, binary: nil)
76
- @encoded = nil
77
61
  if binary
78
62
  raise ArgumentError, "cannot combine binary: with code: / name:" if code || name
79
63
 
@@ -123,17 +107,16 @@ module Kobako
123
107
  nil
124
108
  end
125
109
 
126
- # Build the msgpack-ready Hash for one entry. Source entries
127
- # contribute their host-side +name+; Binary entries omit it
128
- # because the canonical name lives in the bytecode's embedded
129
- # +debug_info+ and is read by the guest at load time
130
- # ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md]).
131
- def entry_payload(entry)
110
+ # Project one entry as its +[kind, name, body]+ triple. Source
111
+ # entries contribute their host-side +name+; Binary entries carry
112
+ # +nil+ because the canonical name lives in the bytecode's embedded
113
+ # +debug_info+ and is read by the guest at load time.
114
+ def entry_tuple(entry)
132
115
  case entry
133
116
  when Snippet::Source
134
- { "name" => entry.name.to_s, "kind" => Snippet::Source::KIND, "body" => entry.body }
135
- when Snippet::Binary
136
- { "kind" => Snippet::Binary::KIND, "body" => entry.body }
117
+ [Snippet::Source::KIND, entry.name.to_s, entry.body]
118
+ else
119
+ [Snippet::Binary::KIND, nil, entry.body]
137
120
  end
138
121
  end
139
122
 
@@ -9,10 +9,10 @@ require_relative "utils"
9
9
  module Kobako
10
10
  module Codec
11
11
  # Module-level entry point for the host side of the kobako wire
12
- # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type Mapping).
12
+ # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type Mapping).
13
13
  #
14
14
  # Translates msgpack gem exceptions into the kobako error taxonomy
15
- # (Truncated, InvalidType, InvalidEncoding, UnsupportedType) so
15
+ # (TruncatedInputError, InvalidTypeError, InvalidEncodingError, UnsupportedTypeError) so
16
16
  # callers can pattern-match on the SPEC's wire-violation categories
17
17
  # without leaking the gem's internal exception classes.
18
18
  #
@@ -21,14 +21,14 @@ module Kobako
21
21
  # because callers always decode exactly one wire value at a time.
22
22
  module Decoder
23
23
  # Decode +bytes+ into one Ruby value and validate transitively
24
- # against the SPEC type mapping. Raises Truncated, InvalidType,
25
- # or InvalidEncoding on wire violations.
24
+ # against the SPEC type mapping. Raises TruncatedInputError, InvalidTypeError,
25
+ # or InvalidEncodingError on wire violations.
26
26
  #
27
27
  # When a block is given, the decoded value is yielded and the block's
28
28
  # result is returned — wire Value Objects use this to build themselves
29
29
  # from the decoded payload. The block runs inside this method's
30
30
  # rescue, so a Value Object's +ArgumentError+ invariant failure
31
- # surfaces as InvalidType without a separate Utils.with_boundary
31
+ # surfaces as InvalidTypeError without a separate Utils.with_boundary
32
32
  # wrapper at the call site.
33
33
  def self.decode(bytes)
34
34
  value = FACTORY.load(bytes.b)
@@ -37,26 +37,23 @@ module Kobako
37
37
  # msgpack gem raises the format/type errors below; +ArgumentError+
38
38
  # comes from our ext-type validators (Handle id range, Exception type
39
39
  # whitelist) and from a yielded block's Value Object invariants — both
40
- # are wire violations, so both map to InvalidType.
40
+ # are wire violations, so both map to InvalidTypeError.
41
41
  rescue ::MessagePack::UnknownExtTypeError, ::MessagePack::MalformedFormatError,
42
42
  ::MessagePack::StackError, ::ArgumentError => e
43
- raise InvalidType, e.message
43
+ raise InvalidTypeError, e.message
44
44
  # +UnpackError+ is the gem's umbrella class for short-read /
45
45
  # incomplete-buffer faults; +EOFError+ covers underflow at the
46
46
  # buffer edge.
47
47
  rescue ::MessagePack::UnpackError, ::EOFError => e
48
- raise Truncated, e.message
48
+ raise TruncatedInputError, e.message
49
49
  end
50
50
 
51
51
  # SPEC pins +str+ family payloads to UTF-8
52
- # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § str/bin
52
+ # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § str/bin
53
53
  # Encoding Rules). The msgpack gem returns UTF-8-tagged Strings for
54
54
  # str family but does not validate the bytes; +bin+ family decodes
55
55
  # to ASCII-8BIT. Walk the tree once and reject invalid UTF-8 in any
56
- # str-typed leaf via Utils.assert_utf8!. Kobako::Fault
57
- # payloads are validated transitively: +ExtTypes#unpack_fault+
58
- # feeds the inner ext-0x02 bytes back through this Decoder, so their
59
- # +str+ fields are already covered by the time control returns here.
56
+ # str-typed leaf via Utils.assert_utf8!.
60
57
  class << self
61
58
  private
62
59
 
@@ -8,13 +8,13 @@ require_relative "ext_types"
8
8
  module Kobako
9
9
  module Codec
10
10
  # Module-level entry point for the host side of the kobako wire
11
- # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type Mapping).
11
+ # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type Mapping).
12
12
  #
13
13
  # The codec backbone is the official +msgpack+ gem: integers, floats,
14
14
  # strings, arrays, and maps go through the gem's narrowest-encoding
15
- # logic; the three kobako-specific ext types (0x00 Symbol, 0x01
16
- # Capability Handle, 0x02 Exception envelope) are registered by
17
- # ExtTypes on the process-wide factory.
15
+ # logic; the two kobako-specific ext types (0x00 Symbol, 0x01
16
+ # Capability Handle) are registered by ExtTypes on the process-wide
17
+ # factory.
18
18
  #
19
19
  # Public API is a single function — +.encode+. The codec is stateless;
20
20
  # there is no buffer accumulator and no streaming write API. Callers
@@ -22,8 +22,8 @@ module Kobako
22
22
  # themselves.
23
23
  module Encoder
24
24
  # Encode +value+ to wire bytes (binary-encoded String).
25
- # SPEC's 12-entry type mapping is a closed set: a value outside it is
26
- # rejected as +UnsupportedType+ by the factory's +BasicObject+ guard
25
+ # SPEC's 11-entry type mapping is a closed set: a value outside it is
26
+ # rejected as +UnsupportedTypeError+ by the factory's +BasicObject+ guard
27
27
  # (ExtTypes#register_unrepresentable), which raises before the msgpack
28
28
  # gem can route the value through +to_msgpack+ — so a permissive
29
29
  # +method_missing+ object cannot answer that probe and mis-encode. The
@@ -33,7 +33,7 @@ module Kobako
33
33
  def self.encode(value)
34
34
  FACTORY.dump(value)
35
35
  rescue ::RangeError, ::NoMethodError => e
36
- raise UnsupportedType, e.message
36
+ raise UnsupportedTypeError, e.message
37
37
  end
38
38
  end
39
39
  end
@@ -5,7 +5,7 @@ module Kobako
5
5
  # Base class for all wire-codec faults raised by the pure-Ruby host codec.
6
6
  #
7
7
  # The wire codec implements the binary contract pinned in
8
- # {docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type Mapping.
8
+ # {docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type Mapping.
9
9
  # Every wire violation surfaces as a
10
10
  # subclass of Error so callers can pattern-match on the specific
11
11
  # fault while still rescuing all codec faults via this base class.
@@ -15,20 +15,20 @@ module Kobako
15
15
  class Error < StandardError; end
16
16
 
17
17
  # Input ended before the type prefix or payload was fully consumed.
18
- class Truncated < Error; end
18
+ class TruncatedInputError < Error; end
19
19
 
20
- # The type byte at the current position is not in the 12-entry kobako
20
+ # The type byte at the current position is not in the 11-entry kobako
21
21
  # type mapping (e.g. an unknown ext code, or a reserved msgpack tag).
22
- class InvalidType < Error; end
22
+ class InvalidTypeError < Error; end
23
23
 
24
24
  # A msgpack +str+ payload was not valid UTF-8, or an ext 0x00 Symbol
25
25
  # payload was not valid UTF-8 — both are wire violations per SPEC.
26
- class InvalidEncoding < Error; end
26
+ class InvalidEncodingError < Error; end
27
27
 
28
28
  # The encoder was handed a Ruby object whose type has no wire
29
29
  # representation (e.g. Range, Time). Higher layers may catch this
30
30
  # and re-route the value through Handle allocation, but at the
31
31
  # codec level it is a hard error.
32
- class UnsupportedType < Error; end
32
+ class UnsupportedTypeError < Error; end
33
33
  end
34
34
  end