lib/choir/src/composition/loaded/composition.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const composition = @import("../root.zig");
3 const loaded = @import("root.zig");
4
5 const Allocator = std.mem.Allocator;
6 const abi = composition.abi;
7 const model = composition.module;
8 const source = composition.source;
9
10 pub const ValidationError = model.VerificationError || error{
11 DuplicateFragmentMaterializer,
12 DuplicateLoadedState,
13 MissingFragmentMaterializer,
14 MissingTargetVariant,
15 };
16
17 pub const EntryError = error{
18 AbiMismatch,
19 MissingEntry,
20 MissingFragment,
21 FragmentOutsideVariant,
22 UndeclaredExport,
23 };
24
25 /// Owns one verified target variant and all process-local fragment state.
26 /// Every `Invocation` created from this owner must be deinitialized first;
27 /// violating that `deinit` precondition panics. Destruction releases fragments
28 /// before durable module storage.
29 pub const LoadedComposition = struct {
30 allocator: Allocator,
31 module: model.CompositionModule,
32 selected_variant: model.CompositionVariantId,
33 fragments: []loaded.LoadedFragment,
34 context: abi.Context,
35 invocations: loaded.gate.Gate = .{},
36 lifecycle: loaded.lifecycle.Recorder,
37
38 pub const initOwned = initOwnedImpl;
39 pub const deinit = deinitImpl;
40 pub const contextPointer = contextPointerImpl;
41 pub const loadedFragment = loadedFragmentImpl;
42 /// Resolves a declared export to its nonzero process-local raw address.
43 /// The address is valid only while this composition remains loaded.
44 pub const entryAddress = entryAddressImpl;
45 pub const reserveInvocation = reserveInvocationImpl;
46 pub const releaseInvocation = releaseInvocationImpl;
47 pub const activeInvocationCount = activeInvocationCountImpl;
48 };
49
50 fn initOwnedImpl(
51 allocator: Allocator,
52 module: model.CompositionModule,
53 choice: model.TargetChoiceSpec,
54 materializers: []const loaded.FragmentMaterializer,
55 observer: ?loaded.LifecycleObserver,
56 ) anyerror!*LoadedComposition {
57 var owned_module = module;
58 var module_transferred = false;
59 errdefer if (!module_transferred) owned_module.deinit();
60 try owned_module.verify();
61 const selected_variant = owned_module.selectVariant(choice) orelse return error.MissingTargetVariant;
62 try validateMaterializers(&owned_module, selected_variant, materializers);
63
64 const owned_fragments = try allocator.alloc(loaded.LoadedFragment, materializers.len);
65 var fragments_transferred = false;
66 errdefer if (!fragments_transferred) allocator.free(owned_fragments);
67 const owner = try allocator.create(LoadedComposition);
68 var owner_initialized = false;
69 errdefer if (!owner_initialized) allocator.destroy(owner);
70 const cookie = nonzeroCookie(@intFromPtr(owner));
71 owner.* = .{
72 .allocator = allocator,
73 .module = owned_module,
74 .selected_variant = selected_variant.id,
75 .fragments = owned_fragments,
76 .context = abi.Context.init(@intFromPtr(owner), cookie),
77 .lifecycle = loaded.lifecycle.Recorder.init(observer),
78 };
79 module_transferred = true;
80 fragments_transferred = true;
81 owner_initialized = true;
82
83 var materialized_count: usize = 0;
84 errdefer {
85 loaded.fragment.deinitUnique(allocator, owned_fragments[0..materialized_count], owner.lifecycle.serialized());
86 allocator.free(owned_fragments);
87 owner.module.deinit();
88 allocator.destroy(owner);
89 }
90 const durable_variant = owner.module.variant(owner.selected_variant).?;
91 for (materializers) |materializer| {
92 std.debug.assert(durable_variant.containsFragment(materializer.id));
93 const durable_fragment = owner.module.fragment(materializer.id).?;
94 const result = try materializer.materialize(materializer.context, allocator, &owner.module, durable_fragment);
95 owned_fragments[materialized_count] = .{
96 .id = materializer.id,
97 .state = result.state,
98 .vtable = result.vtable,
99 };
100 materialized_count += 1;
101 if (hasAliasedState(owned_fragments[0..materialized_count])) return error.DuplicateLoadedState;
102 }
103
104 return owner;
105 }
106
107 fn deinitImpl(self: *LoadedComposition) void {
108 self.invocations.close();
109 const allocator = self.allocator;
110 loaded.fragment.deinitUnique(allocator, self.fragments, self.lifecycle.serialized());
111 allocator.free(self.fragments);
112 self.module.deinit();
113 self.lifecycle.record(.composition_destroyed);
114 self.* = undefined;
115 allocator.destroy(self);
116 }
117
118 fn contextPointerImpl(self: *LoadedComposition) *abi.Context {
119 return &self.context;
120 }
121
122 fn loadedFragmentImpl(self: *const LoadedComposition, id: source.FragmentId) ?loaded.LoadedFragment {
123 for (self.fragments) |fragment_value| {
124 if (fragment_value.id.value == id.value) return fragment_value;
125 }
126 return null;
127 }
128
129 fn entryAddressImpl(self: *const LoadedComposition, id: source.FragmentId, export_name: []const u8) EntryError!usize {
130 const selected_variant = self.module.variant(self.selected_variant) orelse return error.MissingFragment;
131 if (!selected_variant.containsFragment(id)) return error.FragmentOutsideVariant;
132 const durable_fragment = self.module.fragment(id) orelse return error.MissingFragment;
133 const declared_export = durable_fragment.exportByName(export_name) orelse return error.UndeclaredExport;
134 if (declared_export.abi_version != abi.version) return error.AbiMismatch;
135 const runtime_fragment = self.loadedFragment(id) orelse return error.MissingFragment;
136 const lookup = runtime_fragment.vtable.lookup_export orelse return error.MissingEntry;
137 const address = lookup(runtime_fragment.state, declared_export.symbol) orelse return error.MissingEntry;
138 if (address == 0) return error.MissingEntry;
139 return address;
140 }
141
142 fn reserveInvocationImpl(self: *LoadedComposition) ?u64 {
143 return self.invocations.reserve();
144 }
145
146 fn releaseInvocationImpl(self: *LoadedComposition, id: u64) void {
147 self.lifecycle.record(.{ .invocation_destroyed = id });
148 self.invocations.release();
149 }
150
151 fn activeInvocationCountImpl(self: *const LoadedComposition) u64 {
152 return self.invocations.activeCount();
153 }
154
155 fn validateMaterializers(
156 module: *const model.CompositionModule,
157 variant: *const model.CompositionVariant,
158 materializers: []const loaded.FragmentMaterializer,
159 ) ValidationError!void {
160 if (materializers.len != variant.fragments.len) return error.MissingFragmentMaterializer;
161 for (materializers, 0..) |materializer, index| {
162 if (!variant.containsFragment(materializer.id) or module.fragment(materializer.id) == null) return error.MissingFragmentMaterializer;
163 for (materializers[0..index]) |previous| {
164 if (previous.id.value == materializer.id.value) return error.DuplicateFragmentMaterializer;
165 }
166 }
167 }
168
169 fn hasAliasedState(fragments: []const loaded.LoadedFragment) bool {
170 if (fragments.len < 2) return false;
171 const newest = fragments[fragments.len - 1];
172 for (fragments[0 .. fragments.len - 1]) |previous| {
173 if (previous.state == newest.state) return true;
174 }
175 return false;
176 }
177
178 fn nonzeroCookie(address: usize) u64 {
179 const value: u64 = @intCast(address);
180 const mixed = value ^ 0x9e3779b97f4a7c15;
181 return if (mixed == 0) 1 else mixed;
182 }