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 }