Skip to content
This repository was archived by the owner on Jun 19, 2026. It is now read-only.

Commit daa4f5c

Browse files
feat: Bring Swift codegen up to par with Rust/TypeScript
- Rewrite Swift codegen (~220 → ~1170 lines) with: - Streaming type inversion per r[streaming.caller-pov] - Named type generation (structs/enums with Codable) - Client implementation class - Proper encode/decode expression generation - Streaming dispatch support - Add Swift runtime primitives: - Full postcard encoding/decoding (Bool, U8-U128, I8-I128, F32/F64) - Push<T>/Pull<T> types and StreamRegistry - RoamError.userError case - Fix Swift subject compliance: - Handle stream_id=0 and unknown stream IDs per spec - Fix TypeScript subject for local dev: - Add subject-ts.sh wrapper with nvm for Node 25 - Use --experimental-transform-types All three languages now pass 11/11 compliance tests.
1 parent dad8f42 commit daa4f5c

8 files changed

Lines changed: 1422 additions & 48 deletions

File tree

Justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ ts-codegen:
1616
ts *args:
1717
just ts-typecheck
1818
just ts-codegen
19-
SUBJECT_CMD="node --experimental-strip-types typescript/subject/subject.ts" cargo nextest run -p spec-tests {{ quote(args) }}
19+
SUBJECT_CMD="sh typescript/subject/subject-ts.sh" cargo nextest run -p spec-tests {{ quote(args) }}
2020

2121
swift *args:
2222
swift build --package-path swift/subject

rust/roam-codegen/src/targets/swift.rs

Lines changed: 991 additions & 42 deletions
Large diffs are not rendered by default.

rust/roam-codegen/tests/method_ids.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,8 @@ fn swift_service_generation() {
157157

158158
// Should contain handler
159159
assert!(out.contains("protocol EchoHandler"));
160-
assert!(out.contains("createEchoDispatcher"));
160+
// Should contain dispatcher class
161+
assert!(out.contains("class EchoDispatcher"));
161162

162163
// Print for inspection
163164
println!("{}", out);

swift/roam-runtime/Sources/RoamRuntime/Errors.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public enum RoamError: Error {
88
case unknownMethod
99
case invalidPayload
1010
case cancelled
11+
case userError // r[impl unary.error.user] - Application returned an error
1112
}
1213

1314
/// Result type for RPC calls: Result<T, RoamError<E>>

swift/roam-runtime/Sources/RoamRuntime/Postcard.swift

Lines changed: 266 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,90 @@
11
import Foundation
22

3-
// MARK: - Postcard encoding helpers (subset for RPC)
3+
// MARK: - Primitive Encoding
4+
5+
/// Encode a boolean as postcard format (1 byte: 0 or 1)
6+
public func encodeBool(_ value: Bool) -> [UInt8] {
7+
return [value ? 1 : 0]
8+
}
9+
10+
/// Encode an unsigned 8-bit integer (1 byte, no varint)
11+
public func encodeU8(_ value: UInt8) -> [UInt8] {
12+
return [value]
13+
}
14+
15+
/// Encode a signed 8-bit integer (1 byte)
16+
public func encodeI8(_ value: Int8) -> [UInt8] {
17+
return [UInt8(bitPattern: value)]
18+
}
19+
20+
/// Encode an unsigned 16-bit integer as little-endian
21+
public func encodeU16(_ value: UInt16) -> [UInt8] {
22+
return [UInt8(value & 0xFF), UInt8((value >> 8) & 0xFF)]
23+
}
24+
25+
/// Encode a signed 16-bit integer as little-endian
26+
public func encodeI16(_ value: Int16) -> [UInt8] {
27+
return encodeU16(UInt16(bitPattern: value))
28+
}
29+
30+
/// Encode an unsigned 32-bit integer as varint
31+
public func encodeU32(_ value: UInt32) -> [UInt8] {
32+
return encodeVarint(UInt64(value))
33+
}
34+
35+
/// Encode a signed 32-bit integer as zigzag varint
36+
public func encodeI32(_ value: Int32) -> [UInt8] {
37+
// Zigzag encoding: (n << 1) ^ (n >> 31)
38+
let zigzag = UInt32(bitPattern: (value << 1) ^ (value >> 31))
39+
return encodeVarint(UInt64(zigzag))
40+
}
41+
42+
/// Encode a signed 64-bit integer as zigzag varint
43+
public func encodeI64(_ value: Int64) -> [UInt8] {
44+
// Zigzag encoding: (n << 1) ^ (n >> 63)
45+
let zigzag = UInt64(bitPattern: (value << 1) ^ (value >> 63))
46+
return encodeVarint(zigzag)
47+
}
48+
49+
/// Encode an unsigned 128-bit integer (16 bytes, little-endian)
50+
/// Note: Swift UInt128 is available from macOS 15.0+
51+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
52+
public func encodeU128(_ value: UInt128) -> [UInt8] {
53+
var result = [UInt8](repeating: 0, count: 16)
54+
var v = value
55+
for i in 0..<16 {
56+
result[i] = UInt8(truncatingIfNeeded: v)
57+
v >>= 8
58+
}
59+
return result
60+
}
61+
62+
/// Encode a signed 128-bit integer (16 bytes, little-endian)
63+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
64+
public func encodeI128(_ value: Int128) -> [UInt8] {
65+
return encodeU128(UInt128(bitPattern: value))
66+
}
67+
68+
/// Encode a 32-bit float (4 bytes, little-endian)
69+
public func encodeF32(_ value: Float) -> [UInt8] {
70+
let bits = value.bitPattern
71+
return [
72+
UInt8(bits & 0xFF),
73+
UInt8((bits >> 8) & 0xFF),
74+
UInt8((bits >> 16) & 0xFF),
75+
UInt8((bits >> 24) & 0xFF),
76+
]
77+
}
78+
79+
/// Encode a 64-bit float (8 bytes, little-endian)
80+
public func encodeF64(_ value: Double) -> [UInt8] {
81+
let bits = value.bitPattern
82+
var result = [UInt8](repeating: 0, count: 8)
83+
for i in 0..<8 {
84+
result[i] = UInt8((bits >> (i * 8)) & 0xFF)
85+
}
86+
return result
87+
}
488

589
/// Encode a string as postcard format (length-prefixed UTF-8)
690
public func encodeString(_ s: String) -> [UInt8] {
@@ -13,6 +97,144 @@ public func encodeBytes(_ bytes: [UInt8]) -> [UInt8] {
1397
return encodeVarint(UInt64(bytes.count)) + bytes
1498
}
1599

100+
// MARK: - Container Encoding
101+
102+
/// Encode a vector/array with a length prefix
103+
public func encodeVec<T>(_ items: [T], encoder: (T) -> [UInt8]) -> [UInt8] {
104+
var result = encodeVarint(UInt64(items.count))
105+
for item in items {
106+
result.append(contentsOf: encoder(item))
107+
}
108+
return result
109+
}
110+
111+
/// Encode an optional value (0 for None, 1 + value for Some)
112+
public func encodeOption<T>(_ value: T?, encoder: (T) -> [UInt8]) -> [UInt8] {
113+
if let v = value {
114+
return [1] + encoder(v)
115+
} else {
116+
return [0]
117+
}
118+
}
119+
120+
// MARK: - Primitive Decoding
121+
122+
/// Decode a boolean from postcard format
123+
public func decodeBool(from data: Data, offset: inout Int) throws -> Bool {
124+
guard offset < data.count else {
125+
throw RoamError.decodeError("bool: unexpected EOF")
126+
}
127+
let byte = data[offset]
128+
offset += 1
129+
switch byte {
130+
case 0: return false
131+
case 1: return true
132+
default: throw RoamError.decodeError("bool: invalid value \(byte)")
133+
}
134+
}
135+
136+
/// Decode an unsigned 8-bit integer
137+
public func decodeU8(from data: Data, offset: inout Int) throws -> UInt8 {
138+
guard offset < data.count else {
139+
throw RoamError.decodeError("u8: unexpected EOF")
140+
}
141+
let byte = data[offset]
142+
offset += 1
143+
return byte
144+
}
145+
146+
/// Decode a signed 8-bit integer
147+
public func decodeI8(from data: Data, offset: inout Int) throws -> Int8 {
148+
let u = try decodeU8(from: data, offset: &offset)
149+
return Int8(bitPattern: u)
150+
}
151+
152+
/// Decode an unsigned 16-bit integer (little-endian)
153+
public func decodeU16(from data: Data, offset: inout Int) throws -> UInt16 {
154+
guard offset + 2 <= data.count else {
155+
throw RoamError.decodeError("u16: unexpected EOF")
156+
}
157+
let result = UInt16(data[offset]) | (UInt16(data[offset + 1]) << 8)
158+
offset += 2
159+
return result
160+
}
161+
162+
/// Decode a signed 16-bit integer (little-endian)
163+
public func decodeI16(from data: Data, offset: inout Int) throws -> Int16 {
164+
let u = try decodeU16(from: data, offset: &offset)
165+
return Int16(bitPattern: u)
166+
}
167+
168+
/// Decode an unsigned 32-bit integer (varint)
169+
public func decodeU32(from data: Data, offset: inout Int) throws -> UInt32 {
170+
let v = try decodeVarint(from: data, offset: &offset)
171+
guard v <= UInt64(UInt32.max) else {
172+
throw RoamError.decodeError("u32: overflow")
173+
}
174+
return UInt32(v)
175+
}
176+
177+
/// Decode a signed 32-bit integer (zigzag varint)
178+
public func decodeI32(from data: Data, offset: inout Int) throws -> Int32 {
179+
let v = try decodeU32(from: data, offset: &offset)
180+
// Zigzag decode: (n >> 1) ^ -(n & 1)
181+
return Int32(bitPattern: (v >> 1) ^ (0 &- (v & 1)))
182+
}
183+
184+
/// Decode a signed 64-bit integer (zigzag varint)
185+
public func decodeI64(from data: Data, offset: inout Int) throws -> Int64 {
186+
let v = try decodeVarint(from: data, offset: &offset)
187+
// Zigzag decode: (n >> 1) ^ -(n & 1)
188+
return Int64(bitPattern: (v >> 1) ^ (0 &- (v & 1)))
189+
}
190+
191+
/// Decode an unsigned 128-bit integer (16 bytes, little-endian)
192+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
193+
public func decodeU128(from data: Data, offset: inout Int) throws -> UInt128 {
194+
guard offset + 16 <= data.count else {
195+
throw RoamError.decodeError("u128: unexpected EOF")
196+
}
197+
var result: UInt128 = 0
198+
for i in 0..<16 {
199+
result |= UInt128(data[offset + i]) << (i * 8)
200+
}
201+
offset += 16
202+
return result
203+
}
204+
205+
/// Decode a signed 128-bit integer (16 bytes, little-endian)
206+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
207+
public func decodeI128(from data: Data, offset: inout Int) throws -> Int128 {
208+
let u = try decodeU128(from: data, offset: &offset)
209+
return Int128(bitPattern: u)
210+
}
211+
212+
/// Decode a 32-bit float (4 bytes, little-endian)
213+
public func decodeF32(from data: Data, offset: inout Int) throws -> Float {
214+
guard offset + 4 <= data.count else {
215+
throw RoamError.decodeError("f32: unexpected EOF")
216+
}
217+
var bits: UInt32 = 0
218+
for i in 0..<4 {
219+
bits |= UInt32(data[offset + i]) << (i * 8)
220+
}
221+
offset += 4
222+
return Float(bitPattern: bits)
223+
}
224+
225+
/// Decode a 64-bit float (8 bytes, little-endian)
226+
public func decodeF64(from data: Data, offset: inout Int) throws -> Double {
227+
guard offset + 8 <= data.count else {
228+
throw RoamError.decodeError("f64: unexpected EOF")
229+
}
230+
var bits: UInt64 = 0
231+
for i in 0..<8 {
232+
bits |= UInt64(data[offset + i]) << (i * 8)
233+
}
234+
offset += 8
235+
return Double(bitPattern: bits)
236+
}
237+
16238
/// Decode a string from postcard format
17239
public func decodeString(from data: Data, offset: inout Int) throws -> String {
18240
let length = try decodeVarint(from: data, offset: &offset)
@@ -38,6 +260,49 @@ public func decodeBytes(from data: Data, offset: inout Int) throws -> Data {
38260
return bytes
39261
}
40262

263+
// MARK: - Container Decoding
264+
265+
/// Decode a vector/array
266+
public func decodeVec<T>(
267+
from data: Data,
268+
offset: inout Int,
269+
decoder: (Data, inout Int) throws -> T
270+
) throws -> [T] {
271+
let length = try decodeVarint(from: data, offset: &offset)
272+
var result: [T] = []
273+
result.reserveCapacity(Int(length))
274+
for _ in 0..<length {
275+
result.append(try decoder(data, &offset))
276+
}
277+
return result
278+
}
279+
280+
/// Decode an optional value
281+
public func decodeOption<T>(
282+
from data: Data,
283+
offset: inout Int,
284+
decoder: (Data, inout Int) throws -> T
285+
) throws -> T? {
286+
let tag = try decodeU8(from: data, offset: &offset)
287+
switch tag {
288+
case 0: return nil
289+
case 1: return try decoder(data, &offset)
290+
default: throw RoamError.decodeError("option: invalid tag \(tag)")
291+
}
292+
}
293+
294+
/// Decode a 2-tuple
295+
public func decodeTuple2<A, B>(
296+
from data: Data,
297+
offset: inout Int,
298+
decoderA: (Data, inout Int) throws -> A,
299+
decoderB: (Data, inout Int) throws -> B
300+
) throws -> (A, B) {
301+
let a = try decoderA(data, &offset)
302+
let b = try decoderB(data, &offset)
303+
return (a, b)
304+
}
305+
41306
// MARK: - Result encoding
42307

43308
/// Encode Result::Ok variant

0 commit comments

Comments
 (0)