Skip to content

Commit e201bd1

Browse files
arcjet-reisd2kblaine-arcjetclaude
authored
feat: add a 'records' example, and some required instructions (#171)
* feat: add a 'records' example, and some required instructions Prior to this commit there was ostensibly no support for 'record' WIT types, but there wasn't actually much required for them to work. This commit adds an example for use in the test harness and the required instructions to load/store integer record fields. * Add fallible records test * Define record type inline, not in separate interface * Update CLI tests * fix: make I32FromU32/U32FromI32 direction-aware In the import direction, function params are already uint32, so using api.EncodeU32/api.DecodeU32 is a wasteful roundtrip. Use simple uint32() casts for imports and the API functions for exports where CallWasm operates on uint64 values. * chore: update deps and regenerate snapshots - Update examples/records wit-bindgen to 0.53.1 and wit-component to 0.245.1 to match other examples - Regenerate CLI test snapshots to reflect the EncodeU32/DecodeU32 codegen changes for export functions * fix(codegen): use identity casts for I32FromU32, U32FromI32, and I64FromU64 Replace api.EncodeU32/api.DecodeU32/int64() with simple uint32()/uint64() identity casts. The Encode/Decode helpers round-trip through uint64, which causes Go compile errors when the result is assigned to a uint32 variable (e.g. VariantLower slots) or when int64 is assigned to uint64. The canonical ABI reinterpretation instructions are no-ops at the bit level, so identity casts are correct — wazero only inspects the lower 32 bits for i32 values. Adds regression tests for the variant-payload case that triggered the original compile errors, and TODOs for E2E variant tests once variant type definition generation is implemented. --------- Co-authored-by: Ben Sully <ben.sully@grafana.com> Co-authored-by: Ben Sully <ben.sully88@gmail.com> Co-authored-by: blaine-arcjet <146491715+blaine-arcjet@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2987cfb commit e201bd1

File tree

13 files changed

+1088
-15
lines changed

13 files changed

+1088
-15
lines changed

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/gravity/src/codegen/exports.rs

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,5 +171,207 @@ mod tests {
171171
assert!(generated.contains("results1 := raw1[0]"));
172172
assert!(generated.contains("result2 := uint32(results1)"));
173173
assert!(generated.contains("return result2"));
174+
175+
// I32FromU32 / U32FromI32 are no-op reinterpretations — they must not
176+
// use api.EncodeU32 or api.DecodeU32 (which round-trip through uint64,
177+
// causing type mismatches in VariantLower and needless widening elsewhere).
178+
assert!(
179+
!generated.contains("api.EncodeU32"),
180+
"Export must not use api.EncodeU32 (returns uint64 but downstream expects uint32), got:\n{generated}"
181+
);
182+
assert!(
183+
!generated.contains("api.DecodeU32"),
184+
"Export must not use api.DecodeU32 (needless uint32→uint64→uint32 round-trip), got:\n{generated}"
185+
);
186+
}
187+
188+
/// Regression test: export function with a variant parameter containing
189+
/// a u32 payload must generate Go code where I32FromU32 produces a
190+
/// uint32 value matching the VariantLower variable declaration.
191+
/// Previously I32FromU32 used api.EncodeU32() which returns uint64,
192+
/// causing a Go compile error: cannot use uint64 as uint32.
193+
#[test]
194+
fn test_export_variant_u32_no_encode_u32() {
195+
use wit_bindgen_core::wit_parser::{
196+
Case, TypeDef, TypeDefKind, TypeOwner, Variant,
197+
};
198+
199+
let mut resolve = Resolve::new();
200+
201+
// variant u32-option { some-val(u32), none-val }
202+
let variant_def = TypeDef {
203+
name: Some("u32-option".to_string()),
204+
kind: TypeDefKind::Variant(Variant {
205+
cases: vec![
206+
Case {
207+
name: "some-val".to_string(),
208+
ty: Some(Type::U32),
209+
docs: Default::default(),
210+
span: Default::default(),
211+
},
212+
Case {
213+
name: "none-val".to_string(),
214+
ty: None,
215+
docs: Default::default(),
216+
span: Default::default(),
217+
},
218+
],
219+
}),
220+
owner: TypeOwner::None,
221+
docs: Default::default(),
222+
stability: Default::default(),
223+
span: Default::default(),
224+
};
225+
let variant_id = resolve.types.alloc(variant_def);
226+
227+
let func = Function {
228+
name: "process_u32_option".to_string(),
229+
kind: FunctionKind::Freestanding,
230+
params: vec![Param {
231+
name: "opt".to_string(),
232+
ty: Type::Id(variant_id),
233+
span: Default::default(),
234+
}],
235+
result: Some(Type::U32),
236+
docs: Default::default(),
237+
stability: Default::default(),
238+
span: Default::default(),
239+
};
240+
241+
let world = World {
242+
name: "test-world".to_string(),
243+
imports: [].into(),
244+
exports: [(
245+
WorldKey::Name("process-u32-option".to_string()),
246+
WorldItem::Function(func.clone()),
247+
)]
248+
.into(),
249+
docs: Default::default(),
250+
stability: Default::default(),
251+
includes: Default::default(),
252+
span: Default::default(),
253+
package: None,
254+
};
255+
256+
let mut sizes = SizeAlign::default();
257+
sizes.fill(&resolve);
258+
let instance = GoIdentifier::public("TestInstance");
259+
260+
let config = ExportConfig {
261+
instance: &instance,
262+
world: &world,
263+
resolve: &resolve,
264+
sizes: &sizes,
265+
};
266+
267+
let generator = ExportGenerator::new(config);
268+
let mut tokens = Tokens::new();
269+
generator.generate_function(&func, &mut tokens);
270+
271+
let generated = tokens.to_string().unwrap();
272+
println!("Generated u32-option function:\n{}", generated);
273+
274+
// VariantLower declares `var variant_1 uint32` for the I32 payload slot.
275+
// I32FromU32 must NOT use api.EncodeU32 (returns uint64 → type mismatch)
276+
assert!(
277+
!generated.contains("api.EncodeU32"),
278+
"I32FromU32 must not use api.EncodeU32 in exports (returns uint64, \
279+
but VariantLower variable is uint32), got:\n{generated}"
280+
);
281+
}
282+
283+
/// Regression test: export function with a variant parameter containing
284+
/// a u64 payload must generate Go code where I64FromU64 produces a
285+
/// uint64 value matching the VariantLower variable declaration.
286+
/// Previously I64FromU64 used int64() which returns int64, causing a
287+
/// Go compile error: cannot use int64 as uint64.
288+
#[test]
289+
fn test_export_variant_u64_no_int64_cast() {
290+
use wit_bindgen_core::wit_parser::{
291+
Case, TypeDef, TypeDefKind, TypeOwner, Variant,
292+
};
293+
294+
let mut resolve = Resolve::new();
295+
296+
// variant u64-option { some-val(u64), none-val }
297+
let variant_def = TypeDef {
298+
name: Some("u64-option".to_string()),
299+
kind: TypeDefKind::Variant(Variant {
300+
cases: vec![
301+
Case {
302+
name: "some-val".to_string(),
303+
ty: Some(Type::U64),
304+
docs: Default::default(),
305+
span: Default::default(),
306+
},
307+
Case {
308+
name: "none-val".to_string(),
309+
ty: None,
310+
docs: Default::default(),
311+
span: Default::default(),
312+
},
313+
],
314+
}),
315+
owner: TypeOwner::None,
316+
docs: Default::default(),
317+
stability: Default::default(),
318+
span: Default::default(),
319+
};
320+
let variant_id = resolve.types.alloc(variant_def);
321+
322+
let func = Function {
323+
name: "process_u64_option".to_string(),
324+
kind: FunctionKind::Freestanding,
325+
params: vec![Param {
326+
name: "opt".to_string(),
327+
ty: Type::Id(variant_id),
328+
span: Default::default(),
329+
}],
330+
result: Some(Type::U64),
331+
docs: Default::default(),
332+
stability: Default::default(),
333+
span: Default::default(),
334+
};
335+
336+
let world = World {
337+
name: "test-world".to_string(),
338+
imports: [].into(),
339+
exports: [(
340+
WorldKey::Name("process-u64-option".to_string()),
341+
WorldItem::Function(func.clone()),
342+
)]
343+
.into(),
344+
docs: Default::default(),
345+
stability: Default::default(),
346+
includes: Default::default(),
347+
span: Default::default(),
348+
package: None,
349+
};
350+
351+
let mut sizes = SizeAlign::default();
352+
sizes.fill(&resolve);
353+
let instance = GoIdentifier::public("TestInstance");
354+
355+
let config = ExportConfig {
356+
instance: &instance,
357+
world: &world,
358+
resolve: &resolve,
359+
sizes: &sizes,
360+
};
361+
362+
let generator = ExportGenerator::new(config);
363+
let mut tokens = Tokens::new();
364+
generator.generate_function(&func, &mut tokens);
365+
366+
let generated = tokens.to_string().unwrap();
367+
println!("Generated u64-option function:\n{}", generated);
368+
369+
// VariantLower declares `var variant_1 uint64` for the I64 payload slot.
370+
// I64FromU64 must NOT use int64() (returns int64 → type mismatch)
371+
assert!(
372+
!generated.contains(":= int64("),
373+
"I64FromU64 must not use int64() cast in exports (returns int64, \
374+
but VariantLower variable is uint64), got:\n{generated}"
375+
);
174376
}
175377
}

0 commit comments

Comments
 (0)