Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cmd/gravity/src/codegen/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,8 @@ impl Bindgen for Func<'_> {
results.push(Operand::SingleValue(var.into()))
}
}
Instruction::RecordLift { record, name, .. } => {
Instruction::RecordLift { record, ty, .. } => {
let name = crate::qualified_type_name(*ty, resolve);
let tmp = self.tmp();
let value = &format!("value{tmp}");
let fields = record
Expand All @@ -860,7 +861,7 @@ impl Bindgen for Func<'_> {

quote_in! {self.body =>
$['\r']
$value := $(GoIdentifier::public(*name)){
$value := $(GoIdentifier::public(&name)){
$(for (name, op) in fields join ($['\r']) => $name: $op,)
}
};
Expand Down
46 changes: 46 additions & 0 deletions cmd/gravity/src/codegen/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1015,4 +1015,50 @@ mod tests {
fn test_result_as_function_result_still_resolves() {
resolve_export_result("result<u32, string>");
}

/// When two interfaces declare a record with the same name, both Go types
/// carry their interface's name, and the host function that lifts the
/// record must construct the qualified type.
#[test]
fn test_host_function_lifts_record_by_its_qualified_name() {
let fixture = Fixture::parse(
"package test:fixture;
interface host {
record job { a: u32 }
ping: func(j: job) -> u32;
}
interface other {
record job { b: u32 }
}
world test-world {
import host;
import other;
}",
);
let ping = fixture.function("host", "ping");
let method = InterfaceMethod {
name: "ping".to_string(),
go_method_name: GoIdentifier::public("Ping"),
parameters: vec![Parameter {
name: GoIdentifier::private("j"),
go_type: GoType::UserDefined("HostJob".to_string()),
wit_type: ping.params[0].ty,
}],
return_type: Some(WitReturn {
go_type: GoType::Uint32,
wit_type: Type::U32,
}),
wit_function: ping.clone(),
};

let code_str = host_function(&fixture, &method);
assert!(
code_str.contains(":= HostJob{"),
"expected the lift to construct HostJob:\n{code_str}"
);
assert!(
!code_str.contains(":= Job{"),
"the lift constructs the unqualified Job:\n{code_str}"
);
}
}
95 changes: 95 additions & 0 deletions cmd/gravity/tests/cmd/regressions.stdout
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,28 @@ type IRegressionsIpSource interface {
) *string
}

type IRegressionsScheduler interface {
Submit(
ctx context.Context,
j SchedulerJob,
) uint32
}

type SchedulerJob struct {
Id uint32
}

type IRegressionsArchiver interface {
Store(
ctx context.Context,
j ArchiverJob,
) uint32
}

type ArchiverJob struct {
Name string
}

type RegressionsFactory struct {
runtime wazero.Runtime
module wazero.CompiledModule
Expand All @@ -112,9 +134,36 @@ func NewRegressionsFactory(
emailChecker IRegressionsEmailChecker,
botVerifier IRegressionsBotVerifier,
ipSource IRegressionsIpSource,
scheduler IRegressionsScheduler,
archiver IRegressionsArchiver,
) (*RegressionsFactory, error) {
wazeroRuntime := wazero.NewRuntime(ctx)

_, err7 := wazeroRuntime.NewHostModuleBuilder("gravity:regressions/archiver").
NewFunctionBuilder().
WithFunc(func(
ctx context.Context,
mod api.Module,
arg0 uint32,
arg1 uint32,
) uint32{
buf0, ok0 := mod.Memory().Read(arg0, arg1)
if !ok0 {
panic(errors.New("failed to read bytes from memory"))
}
str0 := string(buf0)
value1 := ArchiverJob{
Name: str0,
}
value2 := archiver.Store(ctx, value1)
result3 := uint32(value2)
return result3
}).
Export("store").
Instantiate(ctx)
if err7 != nil {
return nil, err7
}
_, err4 := wazeroRuntime.NewHostModuleBuilder("gravity:regressions/bot-verifier").
NewFunctionBuilder().
WithFunc(func(
Expand Down Expand Up @@ -305,6 +354,26 @@ func NewRegressionsFactory(
if err1 != nil {
return nil, err1
}
_, err6 := wazeroRuntime.NewHostModuleBuilder("gravity:regressions/scheduler").
NewFunctionBuilder().
WithFunc(func(
ctx context.Context,
mod api.Module,
arg0 uint32,
) uint32{
result0 := uint32(arg0)
value1 := SchedulerJob{
Id: result0,
}
value2 := scheduler.Submit(ctx, value1)
result3 := uint32(value2)
return result3
}).
Export("submit").
Instantiate(ctx)
if err6 != nil {
return nil, err6
}

// Compiling the module takes a LONG time, so we want to do it once and hold
// onto it with the Runtime
Expand Down Expand Up @@ -541,3 +610,29 @@ func (i *RegressionsInstance) RunIpLookup(
return str4
}

func (i *RegressionsInstance) RunJobs(
ctx context.Context,
id uint32,
name string,
) uint32 {
arg0 := id
arg1 := name
result0 := uint32(arg0)
memory1 := i.module.Memory()
realloc1 := i.module.ExportedFunction("cabi_realloc")
ptr1, len1, err1 := writeString(ctx, arg1, memory1, realloc1)
// The return type doesn't contain an error so we panic if one is encountered
if err1 != nil {
panic(err1)
}
raw2, err2 := i.module.ExportedFunction("run-jobs").Call(ctx, uint64(result0), uint64(ptr1), uint64(len1))
// The return type doesn't contain an error so we panic if one is encountered
if err2 != nil {
panic(err2)
}

results2 := raw2[0]
result3 := uint32(results2)
return result3
}

27 changes: 27 additions & 0 deletions examples/regressions/regressions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ func (IpSource) Lookup(_ context.Context, ip string) *string {
return nil
}

// Scheduler and Archiver — regression 6 (cross-interface record collision).
// Each receives its own interface's `job` type.
type Scheduler struct{}

func (Scheduler) Submit(_ context.Context, j SchedulerJob) uint32 {
return j.Id
}

type Archiver struct{}

func (Archiver) Store(_ context.Context, j ArchiverJob) uint32 {
return uint32(len(j.Name))
}

func newInstance(t *testing.T) *RegressionsInstance {
t.Helper()
fac, err := NewRegressionsFactory(
Expand All @@ -106,6 +120,8 @@ func newInstance(t *testing.T) *RegressionsInstance {
EmailChecker{},
BotVerifier{},
IpSource{},
Scheduler{},
Archiver{},
)
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -260,3 +276,14 @@ func TestImportCallbackOptionString(t *testing.T) {
t.Errorf("RunIpLookup(\"0.0.0.0\") = %q, want \"absent\"", got)
}
}

// TestCrossInterfaceRecordCollision tests regression 6: two imported
// interfaces both declare `record job`. The guest passes one to each host,
// and each host reads a field only its own interface's record has.
func TestCrossInterfaceRecordCollision(t *testing.T) {
ins := newInstance(t)

if got := ins.RunJobs(t.Context(), 40, "ab"); got != 42 {
t.Errorf("RunJobs(40, \"ab\") = %d, want 42", got)
}
}
8 changes: 7 additions & 1 deletion examples/regressions/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use gravity::regressions::{bot_verifier, checker, email_checker, ip_source, pinger, processor};
use gravity::regressions::{
archiver, bot_verifier, checker, email_checker, ip_source, pinger, processor, scheduler,
};

wit_bindgen::generate!({
world: "regressions",
Expand Down Expand Up @@ -48,4 +50,8 @@ impl Guest for RegressionsWorld {
fn run_ip_lookup(ip: String) -> String {
ip_source::lookup(&ip).unwrap_or_else(|| "absent".to_string())
}

fn run_jobs(id: u32, name: String) -> u32 {
scheduler::submit(scheduler::Job { id }) + archiver::store(&archiver::Job { name })
}
}
23 changes: 23 additions & 0 deletions examples/regressions/wit/regressions.wit
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,32 @@ interface ip-source {
lookup: func(ip: string) -> option<string>;
}

// Regression 6: two interfaces in the same world both define a record named
// `job`, so each Go type carries its interface's name. The host functions
// lift each record into its own interface's type.
interface scheduler {
record job {
id: u32,
}
submit: func(j: job) -> u32;
}

interface archiver {
record job {
name: string,
}
store: func(j: job) -> u32;
}

world regressions {
import checker;
import processor;
import pinger;
import email-checker;
import bot-verifier;
import ip-source;
import scheduler;
import archiver;

export check-enabled: func(key: string) -> bool;
export check-status: func(key: string) -> u32;
Expand All @@ -76,4 +95,8 @@ world regressions {
// Round-trip the option<string> callback. Returns "absent" when the
// host returned `none`.
export run-ip-lookup: func(ip: string) -> string;

// Passes one `job` record to each interface and returns the sum of what
// the two hosts answered.
export run-jobs: func(id: u32, name: string) -> u32;
}
Loading