kobako 0.4.0 → 0.5.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -0
  3. data/CHANGELOG.md +29 -0
  4. data/Cargo.lock +1 -1
  5. data/README.md +0 -1
  6. data/data/kobako.wasm +0 -0
  7. data/ext/kobako/Cargo.toml +1 -1
  8. data/ext/kobako/src/lib.rs +4 -2
  9. data/ext/kobako/src/{wasm → runtime}/cache.rs +12 -16
  10. data/ext/kobako/src/runtime/capture.rs +91 -0
  11. data/ext/kobako/src/runtime/config.rs +26 -0
  12. data/ext/kobako/src/runtime/dispatch.rs +211 -0
  13. data/ext/kobako/src/runtime/exports.rs +51 -0
  14. data/ext/kobako/src/runtime/guest_mem.rs +228 -0
  15. data/ext/kobako/src/{wasm/host_state.rs → runtime/invocation.rs} +94 -86
  16. data/ext/kobako/src/runtime/trap.rs +134 -0
  17. data/ext/kobako/src/runtime.rs +782 -0
  18. data/ext/kobako/src/snapshot.rs +110 -0
  19. data/lib/kobako/capture.rb +11 -16
  20. data/lib/kobako/catalog/handles.rb +107 -0
  21. data/lib/kobako/catalog/namespaces.rb +99 -0
  22. data/lib/kobako/{snippet/table.rb → catalog/snippets.rb} +37 -62
  23. data/lib/kobako/catalog.rb +18 -0
  24. data/lib/kobako/codec/decoder.rb +13 -5
  25. data/lib/kobako/codec/factory.rb +12 -12
  26. data/lib/kobako/codec/utils.rb +56 -59
  27. data/lib/kobako/codec.rb +6 -3
  28. data/lib/kobako/errors.rb +45 -28
  29. data/lib/kobako/fault.rb +40 -0
  30. data/lib/kobako/handle.rb +4 -6
  31. data/lib/kobako/namespace.rb +67 -0
  32. data/lib/kobako/outcome.rb +31 -35
  33. data/lib/kobako/runtime.rb +30 -0
  34. data/lib/kobako/sandbox.rb +83 -72
  35. data/lib/kobako/sandbox_options.rb +6 -9
  36. data/lib/kobako/snapshot.rb +40 -0
  37. data/lib/kobako/snippet/binary.rb +6 -7
  38. data/lib/kobako/snippet/source.rb +8 -8
  39. data/lib/kobako/snippet.rb +7 -9
  40. data/lib/kobako/transport/dispatcher.rb +195 -0
  41. data/lib/kobako/{rpc/wire_error.rb → transport/error.rb} +7 -6
  42. data/lib/kobako/transport/request.rb +78 -0
  43. data/lib/kobako/transport/response.rb +69 -0
  44. data/lib/kobako/transport/run.rb +141 -0
  45. data/lib/kobako/transport/yield.rb +91 -0
  46. data/lib/kobako/transport/yielder.rb +89 -0
  47. data/lib/kobako/transport.rb +24 -0
  48. data/lib/kobako/version.rb +1 -1
  49. data/lib/kobako.rb +4 -4
  50. data/release-please-config.json +24 -0
  51. data/sig/kobako/capture.rbs +0 -2
  52. data/sig/kobako/catalog/handles.rbs +19 -0
  53. data/sig/kobako/catalog/namespaces.rbs +17 -0
  54. data/sig/kobako/{snippet/table.rbs → catalog/snippets.rbs} +2 -11
  55. data/sig/kobako/{rpc.rbs → catalog.rbs} +1 -1
  56. data/sig/kobako/codec/decoder.rbs +2 -1
  57. data/sig/kobako/codec/factory.rbs +2 -2
  58. data/sig/kobako/codec/utils.rbs +5 -5
  59. data/sig/kobako/errors.rbs +7 -7
  60. data/sig/kobako/fault.rbs +19 -0
  61. data/sig/kobako/handle.rbs +2 -3
  62. data/sig/kobako/namespace.rbs +19 -0
  63. data/sig/kobako/outcome.rbs +2 -2
  64. data/sig/kobako/runtime.rbs +23 -0
  65. data/sig/kobako/sandbox.rbs +5 -8
  66. data/sig/kobako/snapshot.rbs +15 -0
  67. data/sig/kobako/transport/dispatcher.rbs +34 -0
  68. data/sig/kobako/transport/error.rbs +6 -0
  69. data/sig/kobako/transport/request.rbs +32 -0
  70. data/sig/kobako/transport/response.rbs +30 -0
  71. data/sig/kobako/transport/run.rbs +27 -0
  72. data/sig/kobako/transport/yield.rbs +34 -0
  73. data/sig/kobako/transport/yielder.rbs +21 -0
  74. data/sig/kobako/transport.rbs +4 -0
  75. metadata +48 -30
  76. data/ext/kobako/src/wasm/dispatch.rs +0 -162
  77. data/ext/kobako/src/wasm/instance.rs +0 -873
  78. data/ext/kobako/src/wasm.rs +0 -126
  79. data/lib/kobako/handle_table.rb +0 -119
  80. data/lib/kobako/invocation.rb +0 -143
  81. data/lib/kobako/rpc/dispatcher.rb +0 -171
  82. data/lib/kobako/rpc/envelope.rb +0 -118
  83. data/lib/kobako/rpc/fault.rb +0 -41
  84. data/lib/kobako/rpc/namespace.rb +0 -74
  85. data/lib/kobako/rpc/server.rb +0 -146
  86. data/lib/kobako/rpc.rb +0 -11
  87. data/lib/kobako/wasm.rb +0 -25
  88. data/sig/kobako/handle_table.rbs +0 -23
  89. data/sig/kobako/invocation.rbs +0 -25
  90. data/sig/kobako/rpc/dispatcher.rbs +0 -33
  91. data/sig/kobako/rpc/envelope.rbs +0 -51
  92. data/sig/kobako/rpc/fault.rbs +0 -20
  93. data/sig/kobako/rpc/namespace.rbs +0 -24
  94. data/sig/kobako/rpc/server.rbs +0 -31
  95. data/sig/kobako/rpc/wire_error.rbs +0 -6
  96. data/sig/kobako/wasm.rbs +0 -41
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../handle"
4
+ require_relative "../codec"
5
+
6
+ module Kobako
7
+ # See lib/kobako/transport.rb for the umbrella module doc; this file
8
+ # owns the Request value object and its +#encode+ / +.decode+ codec,
9
+ # plus the +STATUS_OK+ / +STATUS_ERROR+ constants shared with Response.
10
+ module Transport
11
+ # ---------------- Response status bytes (docs/wire-contract.md § Response Shape) ---
12
+
13
+ # Response variant marker for the success branch.
14
+ STATUS_OK = 0
15
+ # Response variant marker for the fault branch.
16
+ STATUS_ERROR = 1
17
+
18
+ # Value object for a single guest-initiated Transport Request
19
+ # ({docs/wire-codec.md Envelope Encoding → Request}[link:../../../docs/wire-codec.md]).
20
+ #
21
+ # 5-element msgpack array:
22
+ # +[target, method_name, args, kwargs, block_given]+. +target+ is
23
+ # either a +String+ (+"Namespace::Member"+) or a {Handle}. SPEC pins
24
+ # +kwargs+ map keys to ext 0x00 Symbol; enforced at construction so
25
+ # the Value Object is the single source of truth. +block_given+ is a
26
+ # Boolean signalling whether the guest call site supplied a block
27
+ # (B-23); the block body itself never crosses the wire.
28
+ #
29
+ # Built on the +class X < Data.define(...)+ subclass form so the
30
+ # class body is fully Steep-visible; see +lib/kobako/outcome/panic.rb+
31
+ # for the rationale.
32
+ class Request < Data.define(:target, :method_name, :args, :kwargs, :block_given)
33
+ def initialize(target:, method_name:, args: [], kwargs: {}, block_given: false)
34
+ unless target.is_a?(String) || target.is_a?(Kobako::Handle)
35
+ raise ArgumentError, "Request target must be String or Kobako::Handle, got #{target.class}"
36
+ end
37
+ raise ArgumentError, "Request method_name must be String" unless method_name.is_a?(String)
38
+ raise ArgumentError, "Request args must be Array" unless args.is_a?(Array)
39
+ unless block_given.is_a?(TrueClass) || block_given.is_a?(FalseClass)
40
+ raise ArgumentError, "Request block_given must be Boolean, got #{block_given.class}"
41
+ end
42
+
43
+ validate_kwargs!(kwargs)
44
+ super
45
+ end
46
+
47
+ # Encode this Request to msgpack bytes. The Value Object's own
48
+ # invariants are the contract; this method does not re-check the shape.
49
+ def encode
50
+ Codec::Encoder.encode([target, method_name, args, kwargs, block_given])
51
+ end
52
+
53
+ # Decode +bytes+ into a {Request}. Raises +Codec::InvalidType+ when the
54
+ # envelope is not the expected 5-element msgpack array, or when the
55
+ # Value Object's construction invariants reject the decoded fields.
56
+ def self.decode(bytes)
57
+ Codec::Decoder.decode(bytes) do |arr|
58
+ unless arr.is_a?(Array) && arr.length == 5
59
+ raise Codec::InvalidType, "Request envelope is malformed (expected a 5-element array)"
60
+ end
61
+
62
+ target, method_name, args, kwargs, block_given = arr
63
+ new(target: target, method_name: method_name, args: args, kwargs: kwargs, block_given: block_given)
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def validate_kwargs!(kwargs)
70
+ raise ArgumentError, "Request kwargs must be Hash" unless kwargs.is_a?(Hash)
71
+
72
+ kwargs.each_key do |k|
73
+ raise ArgumentError, "Request kwargs keys must be Symbol, got #{k.class}" unless k.is_a?(Symbol)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../codec"
4
+ require_relative "../fault"
5
+ require_relative "request"
6
+
7
+ module Kobako
8
+ # See lib/kobako/transport.rb for the umbrella module doc; this file
9
+ # owns the Response value object and its +#encode+ / +.decode+ codec.
10
+ module Transport
11
+ # Value object for a single host-side Transport Response
12
+ # ({docs/wire-codec.md Envelope Encoding → Response}[link:../../../docs/wire-codec.md]).
13
+ #
14
+ # 2-element msgpack array: +[status, value-or-fault]+. +status+ is 0
15
+ # (success) or 1 (fault). For success the second element is the return
16
+ # value; for fault it is a {Fault} (ext 0x02 envelope).
17
+ #
18
+ # Built on the +class X < Data.define(...)+ subclass form so the
19
+ # class body is fully Steep-visible; see +lib/kobako/outcome/panic.rb+
20
+ # for the rationale.
21
+ class Response < Data.define(:status, :payload)
22
+ def self.ok(value)
23
+ new(status: STATUS_OK, payload: value)
24
+ end
25
+
26
+ def self.error(fault)
27
+ unless fault.is_a?(Kobako::Fault)
28
+ raise ArgumentError, "Response.error requires Kobako::Fault, got #{fault.class}"
29
+ end
30
+
31
+ new(status: STATUS_ERROR, payload: fault)
32
+ end
33
+
34
+ # Decode +bytes+ into a {Response}. Raises +Codec::InvalidType+ when the
35
+ # envelope is not the expected 2-element msgpack array, or when the
36
+ # Value Object's construction invariants reject the decoded fields.
37
+ def self.decode(bytes)
38
+ Codec::Decoder.decode(bytes) do |arr|
39
+ unless arr.is_a?(Array) && arr.length == 2
40
+ raise Codec::InvalidType, "Response envelope is malformed (expected a 2-element array)"
41
+ end
42
+
43
+ status, payload = arr
44
+ new(status: status, payload: payload)
45
+ end
46
+ end
47
+
48
+ def initialize(status:, payload:)
49
+ unless [STATUS_OK, STATUS_ERROR].include?(status)
50
+ raise ArgumentError, "Response status must be 0 (ok) or 1 (error), got #{status.inspect}"
51
+ end
52
+ if status == STATUS_ERROR && !payload.is_a?(Kobako::Fault)
53
+ raise ArgumentError, "Response with error status must carry a Kobako::Fault payload"
54
+ end
55
+
56
+ super
57
+ end
58
+
59
+ def ok? = status == STATUS_OK
60
+ def error? = status == STATUS_ERROR
61
+
62
+ # Encode this Response to msgpack bytes as the 2-element
63
+ # +[status, payload]+ array.
64
+ def encode
65
+ Codec::Encoder.encode([status, payload])
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../handle"
4
+ require_relative "../codec"
5
+
6
+ module Kobako
7
+ # See lib/kobako/transport.rb for the umbrella module doc; this file
8
+ # owns the +Run+ envelope value object — the host→guest request shape
9
+ # consumed by +__kobako_run+.
10
+ module Transport
11
+ # Host-side value object for a single +Sandbox#run+ invocation
12
+ # ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md];
13
+ # {docs/behavior.md B-31}[link:../../../docs/behavior.md]).
14
+ #
15
+ # A Run captures the host-layer concept of "a single +#run+
16
+ # call": the entrypoint constant name plus its positional and keyword
17
+ # arguments. Host pre-flight (E-24 / E-25 / E-29 / E-30) is enforced at
18
+ # construction so the Value Object is the single source of truth —
19
+ # anything that passes +Run.new+ is safe to encode and ship to
20
+ # the guest.
21
+ #
22
+ # Run is the host→guest entrypoint dispatch envelope (the +#run+
23
+ # request shape), the symmetric counterpart to the guest→host
24
+ # +Request+ envelope. +#encode+ takes the Sandbox's
25
+ # +Catalog::Handles+ and routes any non-wire-representable +args+ /
26
+ # +kwargs+ leaf through it as a +Kobako::Handle+
27
+ # ({docs/behavior.md B-34}[link:../../../docs/behavior.md]) — the
28
+ # symmetric counterpart of the guest→host wrap path in the
29
+ # dispatcher (B-14). A +Kobako::Handle+ that arrives **already
30
+ # constructed** in the caller's +args+ / +kwargs+ is rejected at
31
+ # construction (E-29): legitimate Handles only enter Host App code
32
+ # through error fields, so a Handle reaching the call site is by
33
+ # definition smuggled in. The +#encode+ output is the "Run envelope"
34
+ # that ships through the +__kobako_run+ command buffer.
35
+ #
36
+ # Built on the +class X < Data.define(...)+ subclass form (the
37
+ # Steep-friendly shape — see +lib/kobako/outcome/panic.rb+).
38
+ class Run < Data.define(:entrypoint, :args, :kwargs)
39
+ # Ruby constant-name pattern enforced on the +entrypoint+ Symbol
40
+ # ({docs/behavior.md E-25}[link:../../../docs/behavior.md]). Parallel to
41
+ # +Kobako::Catalog::Snippets::NAME_PATTERN+; the two constants name the
42
+ # same regex but cover distinct surfaces (snippet identity vs.
43
+ # entrypoint resolution) so a future divergence stays local.
44
+ NAME_PATTERN = /\A[A-Z]\w*\z/
45
+
46
+ def initialize(entrypoint:, args: [], kwargs: {})
47
+ entrypoint = normalize_entrypoint(entrypoint)
48
+ args = validate_args!(args)
49
+ kwargs = validate_kwargs!(kwargs)
50
+ super
51
+ end
52
+
53
+ # Encode this Run to the msgpack bytes the guest's +__kobako_run+
54
+ # entry point consumes as its command-buffer payload
55
+ # ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md]).
56
+ # Walks +args+ / +kwargs+ through {Codec::Utils.deep_wrap} so any
57
+ # non-wire-representable leaf is allocated into +handler+ and
58
+ # replaced with a +Kobako::Handle+
59
+ # ({docs/behavior.md B-34}[link:../../../docs/behavior.md]); the
60
+ # +handler+ argument is the Sandbox's table, sharing the same
61
+ # allocator the guest→host return path (B-14) uses.
62
+ #
63
+ # Layout: msgpack map with string keys +"entrypoint"+ (Symbol via
64
+ # ext 0x00), +"args"+ (Array), +"kwargs"+ (Map with Symbol keys);
65
+ # any wrapped leaf rides as ext 0x01 in its original position
66
+ # (docs/wire-codec.md § ext 0x01 position rules).
67
+ def encode(handler)
68
+ Codec::Encoder.encode(
69
+ "entrypoint" => entrypoint,
70
+ "args" => Codec::Utils.deep_wrap(args, handler),
71
+ "kwargs" => Codec::Utils.deep_wrap(kwargs, handler)
72
+ )
73
+ end
74
+
75
+ private
76
+
77
+ # E-24: target must be a Symbol or String (TypeError, not
78
+ # ArgumentError — the wrong-type case is a Host App programming
79
+ # error before the run reaches the guest). E-25: after +.to_s+
80
+ # the value must match NAME_PATTERN (ArgumentError), rejecting
81
+ # +::+-segmented names and any non-constant form.
82
+ def normalize_entrypoint(target)
83
+ unless target.is_a?(Symbol) || target.is_a?(String)
84
+ raise TypeError, "entrypoint must be a Symbol or String, got #{target.class}"
85
+ end
86
+
87
+ target_str = target.to_s
88
+ unless NAME_PATTERN.match?(target_str)
89
+ raise ArgumentError,
90
+ "entrypoint must match #{NAME_PATTERN.inspect} (got #{target.inspect})"
91
+ end
92
+
93
+ target_str.to_sym
94
+ end
95
+
96
+ # E-29: +args+ must not contain a +Kobako::Handle+. The Handle
97
+ # allocator lives inside the Host Gem; legitimate paths surface
98
+ # Handle objects only through raised error fields, so a Handle
99
+ # reaching +args+ is a forged or smuggled token. Non-wire-
100
+ # representable arguments that are not Handles are handled by
101
+ # auto-wrap inside +#encode+ (B-34) — the reject path is reserved
102
+ # for Handle objects specifically.
103
+ def validate_args!(args)
104
+ raise ArgumentError, "arguments must be an Array" unless args.is_a?(Array)
105
+ raise ArgumentError, forged_handle_message("arguments") if args.any?(Kobako::Handle)
106
+
107
+ args
108
+ end
109
+
110
+ # E-30 covers the non-Symbol kwargs-key case; E-29 also rejects a
111
+ # +Kobako::Handle+ arriving as a kwargs value (same forged-token
112
+ # principle as the +args+ branch). Both checks live here so the
113
+ # Host App sees the host-side error message before any encode /
114
+ # decode boundary.
115
+ def validate_kwargs!(kwargs)
116
+ raise ArgumentError, "keyword arguments must be a Hash" unless kwargs.is_a?(Hash)
117
+
118
+ bad_keys = kwargs.each_key.grep_v(Symbol)
119
+ unless bad_keys.empty?
120
+ raise ArgumentError,
121
+ "keyword argument keys must be Symbols (got #{bad_keys.inspect})"
122
+ end
123
+ raise ArgumentError, forged_handle_message("keyword argument values") if kwargs.each_value.any?(Kobako::Handle)
124
+
125
+ kwargs
126
+ end
127
+
128
+ # Single source of truth for the E-29 reject message so the args
129
+ # and kwargs branches stay phrased identically. Message stays in
130
+ # caller vocabulary: it names the affected slot and the reason
131
+ # without leaking SPEC anchor identifiers (B-xx / E-xx live in
132
+ # source comments, not user-visible errors) or self-referential
133
+ # architecture terms — the error is raised BY kobako, so saying
134
+ # "allocated by the Host Gem" reads as third-person about self.
135
+ def forged_handle_message(slot)
136
+ "#{slot} must not contain a Kobako::Handle — " \
137
+ "Handles are created internally by the Sandbox and cannot be passed in"
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../codec"
4
+
5
+ module Kobako
6
+ # See lib/kobako/transport.rb for the umbrella module doc; this file
7
+ # owns the +Yield+ envelope value object plus its +#encode+ / +.decode+
8
+ # codec for the +__kobako_yield_to_block+ wire form.
9
+ module Transport
10
+ # First byte of the YieldResponse for the success branch — body is
11
+ # the block's return value encoded as a single msgpack value.
12
+ TAG_OK = 0x01
13
+ # First byte for `break val` — body is the break value.
14
+ TAG_BREAK = 0x02
15
+ # Reserved for future `return val` support; both sides reject this
16
+ # tag as a wire violation (YieldResponse envelope contract).
17
+ TAG_RESERVED = 0x03
18
+ # First byte for an error / fault outcome — body is a
19
+ # +{"class", "message", "backtrace"}+ Hash.
20
+ TAG_ERROR = 0x04
21
+
22
+ # Tags both sides currently accept on the wire.
23
+ LIVE_TAGS = [TAG_OK, TAG_BREAK, TAG_ERROR].freeze
24
+
25
+ # Value object for a single YieldResponse envelope
26
+ # ({docs/wire-codec.md YieldResponse Envelope}[link:../../../docs/wire-codec.md]).
27
+ #
28
+ # The wire form is a one-byte tag followed by an msgpack payload.
29
+ # The three live tags are +0x01+ (ok), +0x02+ (break), and +0x04+
30
+ # (error); +0x03+ is reserved and rejected by both sides.
31
+ #
32
+ # +value+ carries whatever the wire payload decoded to — a plain
33
+ # Ruby value for the +ok+ / +break+ tags, and a +{"class",
34
+ # "message", "backtrace"}+ Hash for the +error+ tag. No further
35
+ # shape constraint is enforced here; the host-side dispatcher
36
+ # decides how to translate each variant into Ruby control flow.
37
+ #
38
+ # Lives alongside the other envelope value objects (+Request+,
39
+ # +Response+) since it is the guest-to-host shape used
40
+ # mid-dispatch-frame to answer a +__kobako_yield_to_block+ re-entry.
41
+ class Yield < Data.define(:tag, :value)
42
+ def initialize(tag:, value:)
43
+ unless Kobako::Transport::LIVE_TAGS.include?(tag)
44
+ raise ArgumentError,
45
+ "Yield tag must be one of #{Kobako::Transport::LIVE_TAGS.inspect}, got #{tag.inspect}"
46
+ end
47
+
48
+ super
49
+ end
50
+
51
+ def ok? = tag == Kobako::Transport::TAG_OK
52
+ def break? = tag == Kobako::Transport::TAG_BREAK
53
+ def error? = tag == Kobako::Transport::TAG_ERROR
54
+
55
+ # Encode this Yield to YieldResponse bytes: one tag byte followed
56
+ # by an msgpack-encoded +value+.
57
+ def encode
58
+ [tag].pack("C") + Codec::Encoder.encode(value)
59
+ end
60
+
61
+ # Decode +bytes+ into a {Yield}. Rejects empty input, the reserved
62
+ # tag 0x03, and any tag outside +LIVE_TAGS+ by raising
63
+ # +Kobako::Codec::InvalidType+ — these are wire violations per the
64
+ # SPEC's YieldResponse envelope contract.
65
+ def self.decode(bytes)
66
+ bytes = bytes.b
67
+ raise Codec::InvalidType, "YieldResponse must carry at least one byte" if bytes.empty?
68
+
69
+ tag = bytes.getbyte(0) # : Integer
70
+ body = bytes.byteslice(1, bytes.bytesize - 1) || +""
71
+
72
+ reject_dead_tag!(tag)
73
+ new(tag: tag, value: Codec::Decoder.decode(body))
74
+ end
75
+
76
+ def self.reject_dead_tag!(tag)
77
+ return if LIVE_TAGS.include?(tag)
78
+
79
+ msg = if tag == TAG_RESERVED
80
+ "YieldResponse tag 0x03 is reserved"
81
+ else
82
+ format(
83
+ "YieldResponse tag 0x%02x is not recognised", tag
84
+ )
85
+ end
86
+ raise Codec::InvalidType, msg
87
+ end
88
+ private_class_method :reject_dead_tag!
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../codec"
4
+ require_relative "yield"
5
+
6
+ module Kobako
7
+ # See lib/kobako/transport.rb for the umbrella module doc; this file
8
+ # owns the host-side object that materialises a guest-supplied block as
9
+ # a Ruby callable the Service method can yield into.
10
+ module Transport
11
+ # Host-side stand-in for a guest-supplied block (B-23).
12
+ #
13
+ # Each guest call that carries +block_given: true+ gets a Yielder
14
+ # that the Dispatcher hands to the Service method as +&block+. The
15
+ # Service method observes it as an ordinary Ruby Proc through
16
+ # {#to_proc}; +yield val+ / +block.call(val)+ invokes {#yield}, which
17
+ # serialises the positional args, re-enters the guest via the injected
18
+ # +yield_to_guest+ lambda
19
+ # ({docs/behavior.md B-24}[link:../../../docs/behavior.md]), and
20
+ # reifies the +YieldResponse+ into Ruby control flow:
21
+ #
22
+ # * +tag 0x01+ ok — return the decoded value to +yield+'s caller
23
+ # * +tag 0x02+ break — +throw break_tag, value+ so the Dispatcher's
24
+ # +catch+ frame unwinds the Service method
25
+ # ({docs/behavior.md B-25}[link:../../../docs/behavior.md])
26
+ # * +tag 0x04+ error — raise the +{class, message}+ payload at the
27
+ # Service's yield site
28
+ #
29
+ # The Dispatcher calls {#invalidate!} from its +ensure+ block once
30
+ # dispatch completes; any later call to a stashed Yielder then raises
31
+ # +LocalJumpError+ — the observable shape of
32
+ # {docs/behavior.md E-23}[link:../../../docs/behavior.md] (escaped
33
+ # Yielder).
34
+ class Yielder
35
+ # +yield_to_guest+ is a +String → String+ callable (typically
36
+ # +Runtime#yield_to_active_invocation+ bound through a lambda) that
37
+ # {#yield} invokes to re-enter the guest; +break_tag+ is the +catch+
38
+ # throw tag the Dispatcher matches against to unwind the Service on
39
+ # +tag 0x02+.
40
+ def initialize(yield_to_guest, break_tag)
41
+ @yield_to_guest = yield_to_guest
42
+ @break_tag = break_tag
43
+ @active = true
44
+ end
45
+
46
+ # Re-enter the guest with +args+ and reify the YieldResponse into
47
+ # Ruby control flow. Raises +LocalJumpError+ if called after
48
+ # {#invalidate!} (E-23).
49
+ def yield(*args)
50
+ raise LocalJumpError, "guest block invoked after host dispatch frame returned" unless @active
51
+
52
+ response = Kobako::Transport::Yield.decode(@yield_to_guest.call(Kobako::Codec::Encoder.encode(args)))
53
+ return response.value if response.ok?
54
+
55
+ throw @break_tag, response.value if response.break?
56
+
57
+ raise yield_failure(response.value, default: "yield error")
58
+ end
59
+
60
+ # The Proc the Dispatcher passes as +&block+, binding {#yield} so a
61
+ # Service method's +yield+ / +block.call+ drives the round-trip.
62
+ def to_proc
63
+ method(:yield).to_proc
64
+ end
65
+
66
+ # Mark this Yielder dead. Called by the Dispatcher's +ensure+ block
67
+ # when the originating dispatch frame returns; any later {#yield}
68
+ # call then raises +LocalJumpError+ (E-23).
69
+ def invalidate!
70
+ @active = false
71
+ end
72
+
73
+ private
74
+
75
+ # Reify a +YieldResponse+ tag 0x04 payload into a +RuntimeError+ the
76
+ # Service method observes at its +yield+ site. The +{class, message,
77
+ # backtrace}+ shape mirrors the +Kobako::Transport::Yield+ tag 0x04
78
+ # payload; +default+ provides a fallback when the payload is not a
79
+ # Hash.
80
+ def yield_failure(payload, default:)
81
+ return RuntimeError.new(default) unless payload.is_a?(Hash)
82
+
83
+ klass = payload["class"] || "RuntimeError"
84
+ message = payload["message"] || default
85
+ RuntimeError.new("#{klass}: #{message}")
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "transport/request"
4
+ require_relative "transport/response"
5
+ require_relative "transport/run"
6
+ require_relative "transport/yield"
7
+ require_relative "transport/yielder"
8
+ require_relative "transport/error"
9
+ require_relative "transport/dispatcher"
10
+
11
+ module Kobako
12
+ # Kobako::Transport — host↔guest message transport namespace.
13
+ # Houses the envelope value objects (Request / Response / Run / Yield),
14
+ # the guest→host +Dispatcher+, and the host→guest +Yielder+.
15
+ # +Sandbox#initialize+ composes them onto the
16
+ # +Runtime+ as a dispatch +Proc+ + +yield_to_guest+ lambda pair
17
+ # ({docs/behavior.md B-12}[link:../../docs/behavior.md]). "RPC" was
18
+ # deliberately not chosen — it implies a cross-process boundary that
19
+ # kobako does not have, since host and guest share one OS thread and
20
+ # one wasm linear memory. See
21
+ # {SPEC.md Refinement → Internal Concepts}[link:../../SPEC.md].
22
+ module Transport
23
+ end
24
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/kobako.rb CHANGED
@@ -3,8 +3,8 @@
3
3
  require_relative "kobako/version"
4
4
  require "kobako/kobako"
5
5
  require_relative "kobako/errors"
6
- require_relative "kobako/rpc"
7
- require_relative "kobako/rpc/wire_error"
8
- require_relative "kobako/rpc/server"
9
- require_relative "kobako/wasm"
6
+ require_relative "kobako/transport"
7
+ require_relative "kobako/catalog"
8
+ require_relative "kobako/runtime"
9
+ require_relative "kobako/snapshot"
10
10
  require_relative "kobako/sandbox"
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
+ "release-type": "ruby",
4
+ "last-release-sha": "5694da60b08931ea260e13025689b8d8c47d767a",
5
+ "packages": {
6
+ ".": {
7
+ "component": "kobako",
8
+ "include-component-in-tag": false,
9
+ "release-type": "ruby"
10
+ }
11
+ },
12
+ "extra-files": [
13
+ {
14
+ "type": "toml",
15
+ "path": "ext/kobako/Cargo.toml",
16
+ "jsonpath": "$.package.version"
17
+ },
18
+ {
19
+ "type": "toml",
20
+ "path": "Cargo.lock",
21
+ "jsonpath": "$.package[?(@.name=='kobako')].version"
22
+ }
23
+ ]
24
+ }
@@ -7,7 +7,5 @@ module Kobako
7
7
  def initialize: (bytes: String, truncated: bool) -> void
8
8
 
9
9
  def truncated?: () -> bool
10
-
11
- def self.from_ext: (String bytes, bool truncated) -> Capture
12
10
  end
13
11
  end
@@ -0,0 +1,19 @@
1
+ module Kobako
2
+ module Catalog
3
+ class Handles
4
+ def initialize: (?next_id: Integer) -> void
5
+
6
+ def alloc: (untyped object) -> Kobako::Handle
7
+
8
+ def fetch: (Integer id) -> untyped
9
+
10
+ def reset!: () -> self
11
+
12
+ def size: () -> Integer
13
+
14
+ private
15
+
16
+ def require_bound!: (Integer id) -> void
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module Kobako
2
+ module Catalog
3
+ class Namespaces
4
+ def initialize: (?handler: Kobako::Catalog::Handles) -> void
5
+
6
+ def define: (Symbol | String name) -> Kobako::Namespace
7
+
8
+ def lookup: (String target) -> untyped
9
+
10
+ def encode: () -> String
11
+
12
+ def seal!: () -> self
13
+
14
+ def sealed?: () -> bool
15
+ end
16
+ end
17
+ end
@@ -1,6 +1,6 @@
1
1
  module Kobako
2
- module Snippet
3
- class Table
2
+ module Catalog
3
+ class Snippets
4
4
  NAME_PATTERN: Regexp
5
5
 
6
6
  type entry = Kobako::Snippet::Source | Kobako::Snippet::Binary
@@ -11,15 +11,6 @@ module Kobako
11
11
 
12
12
  def encode: () -> String
13
13
 
14
- def each: () { (entry) -> void } -> Array[entry]
15
- | () -> Enumerator[entry, Array[entry]]
16
-
17
- def names: () -> Array[Symbol]
18
-
19
- def size: () -> Integer
20
-
21
- def empty?: () -> bool
22
-
23
14
  private
24
15
 
25
16
  def register_source!: (String? code, (Symbol | String)? name) -> Symbol
@@ -1,4 +1,4 @@
1
1
  module Kobako
2
- module RPC
2
+ module Catalog
3
3
  end
4
4
  end
@@ -1,7 +1,8 @@
1
1
  module Kobako
2
2
  module Codec
3
3
  module Decoder
4
- def self.decode: (String bytes) -> untyped
4
+ def self.decode: [T] (String bytes) { (untyped value) -> T } -> T
5
+ | (String bytes) -> untyped
5
6
 
6
7
  private
7
8
 
@@ -24,8 +24,8 @@ module Kobako
24
24
  def register_handle: () -> void
25
25
  def register_fault: () -> void
26
26
  def unpack_handle: (String payload) -> Kobako::Handle
27
- def pack_fault: (Kobako::RPC::Fault fault) -> String
28
- def unpack_fault: (String payload) -> Kobako::RPC::Fault
27
+ def pack_fault: (Kobako::Fault fault) -> String
28
+ def unpack_fault: (String payload) -> Kobako::Fault
29
29
  end
30
30
  end
31
31
  end
@@ -5,15 +5,15 @@ module Kobako
5
5
 
6
6
  def self?.assert_utf8!: (String string, String label) -> void
7
7
 
8
- def self?.wire_boundary: [T] () { () -> T } -> T
8
+ def self?.with_boundary: [T] () { () -> T } -> T
9
9
 
10
- def self?.wire_representable?: (untyped value) -> bool
10
+ def self?.representable?: (untyped value) -> bool
11
11
 
12
- def self?.deep_wrap: (untyped value, Kobako::HandleTable handle_table) -> untyped
12
+ def self?.deep_wrap: (untyped value, Kobako::Catalog::Handles handler) -> untyped
13
13
 
14
- def self?.primitive_wire_type?: (untyped value) -> bool
14
+ def self?.primitive_type?: (untyped value) -> bool
15
15
 
16
- def self?.container_wire_representable?: (untyped value) -> bool
16
+ def self?.container_representable?: (untyped value) -> bool
17
17
  end
18
18
  end
19
19
  end