1#![forbid(unsafe_code)]
15
16mod artifact;
17mod candidate;
18pub mod cost;
20mod envelope;
21mod facts;
22pub mod legality;
24mod normalize;
25mod search;
26mod select;
27pub mod target;
29
30pub use envelope::{
31 ArtifactEnvelope, TargetEntryPoint, TargetPayload, TargetPayloadFormat, TargetProfile,
32 TargetResourceAccess, TargetResourceBinding, TargetResourceMemory,
33 ARTIFACT_ENVELOPE_SCHEMA_VERSION, TARGET_PAYLOAD_SCHEMA_VERSION,
34};
35pub use target::{
36 attach_target, compile_selected_modules, EmittedTargetModule, SelectedLowering,
37 TargetCompileError, TargetCompiler, TargetModuleBundle, TargetModuleImage,
38 TARGET_MODULE_BUNDLE_SCHEMA_VERSION,
39};
40
41use std::collections::{BTreeMap, BTreeSet};
42
43use serde::{Deserialize, Serialize};
44use thiserror::Error;
45pub use vyre_foundation::diagnostics::Diagnostic;
46use vyre_foundation::diagnostics::{DiagnosticStage, OpLocation, RetryClass};
47use vyre_foundation::ir::{
48 BufferAccess, DataType, GraphValueId, ProgramGraph, ShapeDim, ValueLifetime,
49};
50use vyre_foundation::validate::{validate_with_options, BackendCapabilities, ValidationOptions};
51
52pub const ARTIFACT_SCHEMA_VERSION: u16 = 4;
54const ARTIFACT_MAGIC: &[u8; 4] = b"VMK0";
55const ARTIFACT_HEADER_BYTES: usize = 10;
56const ARTIFACT_DIGEST_BYTES: usize = 32;
57const ARTIFACT_DIGEST_DOMAIN: &[u8] = b"vyre-megakernel-artifact-v4\0";
58const SOURCE_DIGEST_DOMAIN: &[u8] = b"vyre-megakernel-source-v2\0";
59const REQUEST_DIGEST_DOMAIN: &[u8] = b"vyre-megakernel-request-v2\0";
60const COMPILER_IR_CAPABILITIES: BackendCapabilities = BackendCapabilities {
61 supports_subgroup_ops: true,
62 supports_indirect_dispatch: true,
63 supports_specialization_constants: true,
64 supports_distributed_collectives: true,
65 has_mul_high: true,
66 has_dual_issue_fp32_int32: true,
67 has_tensor_core_int: true,
68 has_native_f16: true,
69 has_warp_shuffle: true,
70 has_shared_memory: true,
71 has_transcendental_polynomial_emit: true,
72 max_native_int_width: u32::MAX,
73};
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
77pub struct Digest(pub [u8; 32]);
78
79impl Digest {
80 #[must_use]
82 pub const fn as_bytes(&self) -> &[u8; 32] {
83 &self.0
84 }
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
89pub struct ArtifactNodeId(pub u32);
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
93pub struct ArtifactValueId(pub u32);
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
97pub struct FusionGroupId(pub u32);
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
101#[serde(rename_all = "snake_case")]
102pub enum DependencyEndpoint {
103 Node(ArtifactNodeId),
105 Value(ArtifactValueId),
107}
108
109#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
111#[serde(rename_all = "snake_case")]
112pub enum DependencyKind {
113 Data,
115 Retained,
117 Materialization,
119}
120
121#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
123pub struct DependencyEdge {
124 pub from: DependencyEndpoint,
126 pub to: DependencyEndpoint,
128 pub kind: DependencyKind,
130 pub value: Option<ArtifactValueId>,
132}
133
134#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
136pub struct SearchBudget {
137 pub max_candidates: u32,
139 pub max_cpu_work: u64,
141 pub max_target_compilations: u32,
143 pub max_measurements: u32,
145 pub max_elapsed_ns: u64,
147}
148
149impl SearchBudget {
150 #[must_use]
152 pub const fn new(
153 max_candidates: u32,
154 max_cpu_work: u64,
155 max_target_compilations: u32,
156 max_measurements: u32,
157 max_elapsed_ns: u64,
158 ) -> Self {
159 Self {
160 max_candidates,
161 max_cpu_work,
162 max_target_compilations,
163 max_measurements,
164 max_elapsed_ns,
165 }
166 }
167}
168#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
170pub struct SearchWork {
171 pub candidates_explored: u32,
173 pub cpu_work: u64,
175 pub target_compilations: u32,
177 pub measurements: u32,
179 pub elapsed_ns: u64,
181}
182
183#[derive(Clone, Debug, PartialEq, Eq)]
185pub struct ExternalFacts {
186 pub configuration_digest: Digest,
188 pub symbolic_bindings: BTreeMap<String, u64>,
190 pub constant_identities: BTreeMap<GraphValueId, Digest>,
192}
193
194impl ExternalFacts {
195 #[must_use]
197 pub fn new(configuration_digest: Digest, symbolic_bindings: BTreeMap<String, u64>) -> Self {
198 Self {
199 configuration_digest,
200 symbolic_bindings,
201 constant_identities: BTreeMap::new(),
202 }
203 }
204}
205
206pub struct CompileRequest {
208 graph: ProgramGraph,
209 facts: ExternalFacts,
210 search_budget: SearchBudget,
211 max_artifact_bytes: u64,
212}
213
214impl CompileRequest {
215 #[must_use]
217 pub const fn new(
218 graph: ProgramGraph,
219 facts: ExternalFacts,
220 search_budget: SearchBudget,
221 max_artifact_bytes: u64,
222 ) -> Self {
223 Self {
224 graph,
225 facts,
226 search_budget,
227 max_artifact_bytes,
228 }
229 }
230
231 pub fn validate(self) -> Result<ValidatedCompileRequest, CompileError> {
233 if self.max_artifact_bytes == 0 {
234 return Err(failure(
235 CompilerFailureKind::ArtifactLimit,
236 "request.max_artifact_bytes",
237 "artifact byte limit must be greater than zero",
238 "supply a positive bounded artifact byte limit",
239 ));
240 }
241 if self.search_budget.max_candidates == 0
242 || self.search_budget.max_cpu_work == 0
243 || self.search_budget.max_elapsed_ns == 0
244 {
245 return Err(failure(
246 CompilerFailureKind::InvalidSearchBudget,
247 "request.search_budget",
248 "candidate, CPU-work, and elapsed-work bounds must be positive",
249 "supply explicit positive bounds for every mandatory search dimension",
250 ));
251 }
252 self.graph.analyze().map_err(|error| {
253 failure(
254 CompilerFailureKind::InvalidProgram,
255 "request.graph",
256 error.to_string(),
257 "supply a structurally valid acyclic ProgramGraph",
258 )
259 })?;
260 for node in self.graph.nodes() {
261 let report = validate_with_options(
262 &node.program,
263 ValidationOptions::universal().with_backend_capabilities(COMPILER_IR_CAPABILITIES),
264 );
265 if let Some(issue) = report.errors.into_iter().next() {
266 let path = format!("request.graph.nodes[{}].program", node.id.0);
267 let mut diagnostic = issue.diagnostic();
268 if let Some(location) = diagnostic.location.as_mut() {
269 location.path = Some(path);
270 location.graph_node = Some(node.id.0);
271 }
272 return Err(CompileError { diagnostic });
273 }
274 }
275 validate_bindings(&self.graph, &self.facts.symbolic_bindings)?;
276 validate_constant_identities(&self.graph, &self.facts.constant_identities)?;
277 Ok(ValidatedCompileRequest {
278 graph: self.graph,
279 facts: self.facts,
280 search_budget: self.search_budget,
281 max_artifact_bytes: self.max_artifact_bytes,
282 })
283 }
284}
285
286pub struct ValidatedCompileRequest {
288 graph: ProgramGraph,
289 facts: ExternalFacts,
290 search_budget: SearchBudget,
291 max_artifact_bytes: u64,
292}
293
294impl ValidatedCompileRequest {
295 #[must_use]
297 pub const fn graph(&self) -> &ProgramGraph {
298 &self.graph
299 }
300
301 #[must_use]
303 pub const fn facts(&self) -> &ExternalFacts {
304 &self.facts
305 }
306
307 #[must_use]
309 pub const fn search_budget(&self) -> SearchBudget {
310 self.search_budget
311 }
312
313 #[must_use]
315 pub const fn max_artifact_bytes(&self) -> u64 {
316 self.max_artifact_bytes
317 }
318}
319
320#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
322pub(crate) enum CompilerFailureKind {
323 InvalidProgram,
325 MissingSymbol,
327 UnknownSymbol,
329 DependencyCycle,
331 ResourceOverflow,
333 UnsizedResource,
335 ArtifactLimit,
337 MalformedArtifact,
339 VersionSkew,
341 DigestMismatch,
343 MalformedTargetPayload,
345 TargetPayloadVersionSkew,
347 TargetPayloadDigestMismatch,
349 TargetPayloadAssociationMismatch,
351 IncompatibleTargetPayload,
353 InvalidSearchBudget,
355 MissingConstantIdentity,
357 UnknownConstantIdentity,
359}
360
361impl CompilerFailureKind {
362 #[must_use]
364 const fn as_str(self) -> &'static str {
365 match self {
366 Self::InvalidProgram => "MKC001_INVALID_PROGRAM",
367 Self::MissingSymbol => "MKC002_MISSING_SYMBOL",
368 Self::UnknownSymbol => "MKC003_UNKNOWN_SYMBOL",
369 Self::DependencyCycle => "MKC010_DEPENDENCY_CYCLE",
370 Self::ResourceOverflow => "MKC011_RESOURCE_OVERFLOW",
371 Self::UnsizedResource => "MKC012_UNSIZED_RESOURCE",
372 Self::ArtifactLimit => "MKC013_ARTIFACT_LIMIT",
373 Self::MalformedArtifact => "MKC014_MALFORMED_ARTIFACT",
374 Self::VersionSkew => "MKC015_VERSION_SKEW",
375 Self::DigestMismatch => "MKC016_DIGEST_MISMATCH",
376 Self::MalformedTargetPayload => "MKC017_MALFORMED_TARGET_PAYLOAD",
377 Self::TargetPayloadVersionSkew => "MKC018_TARGET_PAYLOAD_VERSION_SKEW",
378 Self::TargetPayloadDigestMismatch => "MKC019_TARGET_PAYLOAD_DIGEST_MISMATCH",
379 Self::TargetPayloadAssociationMismatch => "MKC020_TARGET_PAYLOAD_ASSOCIATION_MISMATCH",
380 Self::IncompatibleTargetPayload => "MKC021_INCOMPATIBLE_TARGET_PAYLOAD",
381 Self::InvalidSearchBudget => "MKC022_INVALID_SEARCH_BUDGET",
382 Self::MissingConstantIdentity => "MKC023_MISSING_CONSTANT_IDENTITY",
383 Self::UnknownConstantIdentity => "MKC024_UNKNOWN_CONSTANT_IDENTITY",
384 }
385 }
386}
387
388const fn diagnostic_stage(code: CompilerFailureKind) -> DiagnosticStage {
389 match code {
390 CompilerFailureKind::InvalidProgram
391 | CompilerFailureKind::MissingSymbol
392 | CompilerFailureKind::UnknownSymbol
393 | CompilerFailureKind::InvalidSearchBudget
394 | CompilerFailureKind::MissingConstantIdentity
395 | CompilerFailureKind::UnknownConstantIdentity => DiagnosticStage::Validate,
396 CompilerFailureKind::DependencyCycle => DiagnosticStage::Plan,
397 CompilerFailureKind::ResourceOverflow | CompilerFailureKind::UnsizedResource => {
398 DiagnosticStage::Lower
399 }
400 CompilerFailureKind::ArtifactLimit => DiagnosticStage::Emit,
401 CompilerFailureKind::MalformedArtifact
402 | CompilerFailureKind::VersionSkew
403 | CompilerFailureKind::DigestMismatch
404 | CompilerFailureKind::MalformedTargetPayload
405 | CompilerFailureKind::TargetPayloadVersionSkew
406 | CompilerFailureKind::TargetPayloadDigestMismatch
407 | CompilerFailureKind::TargetPayloadAssociationMismatch
408 | CompilerFailureKind::IncompatibleTargetPayload => DiagnosticStage::Admit,
409 }
410}
411
412const fn diagnostic_retry(code: CompilerFailureKind) -> RetryClass {
413 match code {
414 CompilerFailureKind::VersionSkew
415 | CompilerFailureKind::TargetPayloadVersionSkew
416 | CompilerFailureKind::IncompatibleTargetPayload => RetryClass::RecompileSource,
417 _ => RetryClass::Never,
418 }
419}
420
421#[derive(Clone, Debug, PartialEq, Eq, Error)]
423#[error("{diagnostic}")]
424pub struct CompileError {
425 pub diagnostic: Diagnostic,
427}
428
429#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
431pub struct NodeRecord {
432 pub id: ArtifactNodeId,
434 pub name: String,
436 pub program: Vec<u8>,
438}
439
440#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
442pub struct GeometryRecord {
443 pub node: ArtifactNodeId,
445 pub workgroup_size: [u32; 3],
447}
448
449#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
451#[serde(rename_all = "snake_case")]
452pub enum ResourceLifetime {
453 Constant,
455 Invocation,
457 Retained,
459 Output,
461}
462
463#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
465pub struct ResourceRecord {
466 pub value: ArtifactValueId,
468 pub name: String,
470 pub element_count: u64,
472 pub byte_count: u64,
474 pub lifetime: ResourceLifetime,
476 pub first_stage: u32,
478 pub last_stage: u32,
480}
481
482#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
484pub struct ResourceEnvelope {
485 pub total_bytes: u64,
487 pub peak_live_bytes: u64,
489}
490
491#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
493#[serde(rename_all = "snake_case")]
494pub enum AbiAccess {
495 ReadOnly,
497 WriteOnly,
499 ReadWrite,
501 Uniform,
503}
504
505#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
507pub struct ResourceAbiRecord {
508 pub slot: u32,
510 pub value: ArtifactValueId,
512 pub dtype: DataType,
514 pub access: AbiAccess,
516}
517
518#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
520pub struct EntryAbiRecord {
521 pub node: ArtifactNodeId,
523 pub inputs: Vec<ArtifactValueId>,
525 pub outputs: Vec<ArtifactValueId>,
527}
528
529#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
531pub struct ArtifactAbi {
532 pub resources: Vec<ResourceAbiRecord>,
534 pub entries: Vec<EntryAbiRecord>,
536}
537
538#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
540pub struct FusionRecord {
541 pub id: FusionGroupId,
543 pub members: Vec<ArtifactNodeId>,
545 pub stage: u32,
547 pub legality: Vec<Digest>,
549}
550#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
552pub struct FusionRejection {
553 pub from: ArtifactNodeId,
555 pub to: ArtifactNodeId,
557 pub value: ArtifactValueId,
559 pub reason: legality::FusionRejectionReason,
561}
562
563#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
565pub struct BarrierRecord {
566 pub before_stage: u32,
568 pub after_stage: u32,
570 pub dependencies: Vec<u32>,
572}
573
574#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
576#[serde(rename_all = "snake_case")]
577pub enum MaterializationReason {
578 CrossGroupUse,
580 Output,
582 Retained,
584}
585
586#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
588pub struct MaterializationRecord {
589 pub value: ArtifactValueId,
591 pub producer: FusionGroupId,
593 pub stage: u32,
595 pub reason: MaterializationReason,
597}
598
599#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
601pub struct SelectedPlan {
602 pub fusion: Vec<FusionRecord>,
604 pub barriers: Vec<BarrierRecord>,
606 pub materializations: Vec<MaterializationRecord>,
608 pub candidates_explored: u32,
610 pub search_budget: SearchBudget,
612 pub search_work: SearchWork,
614 pub selection_cost: cost::CostBreakdown,
616 pub pruned_fusions: Vec<FusionRejection>,
618}
619
620#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
622pub struct Provenance {
623 pub source_graph: Digest,
625 pub request: Digest,
627 pub compiler_version: String,
629}
630
631#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
632#[serde(deny_unknown_fields)]
633struct ArtifactPayload {
634 schema_version: u16,
635 nodes: Vec<NodeRecord>,
636 dependencies: Vec<DependencyEdge>,
637 selected_plan: SelectedPlan,
638 abi: ArtifactAbi,
639 resources: Vec<ResourceRecord>,
640 resource_envelope: ResourceEnvelope,
641 geometry: Vec<GeometryRecord>,
642 provenance: Provenance,
643}
644
645#[derive(Clone, Debug, PartialEq, Eq)]
647pub struct Artifact {
648 payload: ArtifactPayload,
649 digest: Digest,
650}
651
652impl Artifact {
653 #[must_use]
655 pub const fn schema_version(&self) -> u16 {
656 self.payload.schema_version
657 }
658
659 #[must_use]
661 pub fn nodes(&self) -> &[NodeRecord] {
662 &self.payload.nodes
663 }
664
665 #[must_use]
667 pub fn dependencies(&self) -> &[DependencyEdge] {
668 &self.payload.dependencies
669 }
670
671 #[must_use]
673 pub fn fusion(&self) -> &[FusionRecord] {
674 &self.payload.selected_plan.fusion
675 }
676
677 #[must_use]
679 pub fn barriers(&self) -> &[BarrierRecord] {
680 &self.payload.selected_plan.barriers
681 }
682
683 #[must_use]
685 pub fn resources(&self) -> &[ResourceRecord] {
686 &self.payload.resources
687 }
688
689 #[must_use]
691 pub const fn resource_envelope(&self) -> ResourceEnvelope {
692 self.payload.resource_envelope
693 }
694
695 #[must_use]
697 pub fn geometry(&self) -> &[GeometryRecord] {
698 &self.payload.geometry
699 }
700
701 #[must_use]
703 pub fn materializations(&self) -> &[MaterializationRecord] {
704 &self.payload.selected_plan.materializations
705 }
706
707 #[must_use]
709 pub const fn selected_plan(&self) -> &SelectedPlan {
710 &self.payload.selected_plan
711 }
712
713 #[must_use]
715 pub const fn abi(&self) -> &ArtifactAbi {
716 &self.payload.abi
717 }
718
719 #[must_use]
721 pub const fn provenance(&self) -> &Provenance {
722 &self.payload.provenance
723 }
724
725 #[must_use]
727 pub const fn digest(&self) -> Digest {
728 self.digest
729 }
730
731 pub fn to_bytes(&self) -> Result<Vec<u8>, CompileError> {
733 encode_payload(&self.payload)
734 }
735
736 pub fn from_bytes(bytes: &[u8]) -> Result<Self, CompileError> {
738 if bytes.len() < ARTIFACT_HEADER_BYTES + ARTIFACT_DIGEST_BYTES {
739 return Err(failure(
740 CompilerFailureKind::MalformedArtifact,
741 "artifact.header",
742 "artifact is shorter than its fixed framing",
743 "supply complete VMK0 bytes",
744 ));
745 }
746 if &bytes[..4] != ARTIFACT_MAGIC {
747 return Err(failure(
748 CompilerFailureKind::MalformedArtifact,
749 "artifact.magic",
750 "artifact magic is not VMK0",
751 "supply canonical megakernel artifact bytes",
752 ));
753 }
754 let version = u16::from_le_bytes([bytes[4], bytes[5]]);
755 if version != ARTIFACT_SCHEMA_VERSION {
756 return Err(failure(
757 CompilerFailureKind::VersionSkew,
758 "artifact.schema_version",
759 format!("schema {version} is unsupported; expected {ARTIFACT_SCHEMA_VERSION}"),
760 "recompile the source graph with this compiler version",
761 ));
762 }
763 let body_len = u32::from_le_bytes(bytes[6..10].try_into().expect("fixed slice")) as usize;
764 let expected_len = ARTIFACT_HEADER_BYTES
765 .checked_add(body_len)
766 .and_then(|len| len.checked_add(ARTIFACT_DIGEST_BYTES))
767 .ok_or_else(|| {
768 failure(
769 CompilerFailureKind::MalformedArtifact,
770 "artifact.body_length",
771 "framed body length overflowed addressable memory",
772 "supply bounded canonical artifact bytes",
773 )
774 })?;
775 if bytes.len() != expected_len {
776 return Err(failure(
777 CompilerFailureKind::MalformedArtifact,
778 "artifact.body_length",
779 format!(
780 "framing declares {expected_len} bytes but received {}",
781 bytes.len()
782 ),
783 "supply exactly one complete canonical artifact",
784 ));
785 }
786 let body = &bytes[ARTIFACT_HEADER_BYTES..ARTIFACT_HEADER_BYTES + body_len];
787 let expected_digest = artifact_digest(version, body);
788 let encoded_digest: [u8; 32] = bytes[ARTIFACT_HEADER_BYTES + body_len..]
789 .try_into()
790 .expect("validated digest length");
791 if expected_digest.0 != encoded_digest {
792 return Err(failure(
793 CompilerFailureKind::DigestMismatch,
794 "artifact.digest",
795 "artifact body does not match its content identity",
796 "discard the corrupted artifact and recompile",
797 ));
798 }
799 let payload: ArtifactPayload = serde_json::from_slice(body).map_err(|error| {
800 failure(
801 CompilerFailureKind::MalformedArtifact,
802 "artifact.body",
803 error.to_string(),
804 "supply a canonical body emitted by this crate",
805 )
806 })?;
807 if payload.schema_version != version {
808 return Err(failure(
809 CompilerFailureKind::VersionSkew,
810 "artifact.body.schema_version",
811 "body schema disagrees with framing schema",
812 "recompile instead of rewriting artifact framing",
813 ));
814 }
815 let canonical = serde_json::to_vec(&payload).map_err(serialization_failure)?;
816 if canonical != body {
817 return Err(failure(
818 CompilerFailureKind::MalformedArtifact,
819 "artifact.body",
820 "artifact body is valid JSON but not canonical JSON",
821 "use the canonical bytes emitted by Artifact::to_bytes",
822 ));
823 }
824 Ok(Self {
825 payload,
826 digest: expected_digest,
827 })
828 }
829}
830
831pub fn compile(request: &ValidatedCompileRequest) -> Result<Artifact, CompileError> {
833 let canonical_wire = request.graph.to_wire().map_err(|error| {
834 failure(
835 CompilerFailureKind::InvalidProgram,
836 "request.graph",
837 error.to_string(),
838 "supply a graph representable by the canonical foundation wire format",
839 )
840 })?;
841 let source_graph = domain_digest(SOURCE_DIGEST_DOMAIN, &canonical_wire);
842
843 let nodes = request
844 .graph
845 .nodes()
846 .iter()
847 .map(|node| {
848 let program = node.program.canonical_wire_bytes().map_err(|error| {
849 failure(
850 CompilerFailureKind::InvalidProgram,
851 format!("request.graph.nodes[{}].program", node.id.0),
852 error.to_string(),
853 "supply canonical-wire-compatible typed IR",
854 )
855 })?;
856 Ok(NodeRecord {
857 id: ArtifactNodeId(node.id.0),
858 name: node.name.clone(),
859 program,
860 })
861 })
862 .collect::<Result<Vec<_>, CompileError>>()?;
863 let geometry = request
864 .graph
865 .nodes()
866 .iter()
867 .map(|node| GeometryRecord {
868 node: ArtifactNodeId(node.id.0),
869 workgroup_size: node.program.workgroup_size,
870 })
871 .collect::<Vec<_>>();
872
873 let normalized = normalize::normalize(&request.graph)?;
874 let dependencies = normalized.dependencies;
875 let artifact::ArtifactPlan {
876 node_groups,
877 stages,
878 selected_plan,
879 } = artifact::plan(&request.graph, &dependencies, request.search_budget)?;
880 let (resources, resource_envelope) = build_resources(
881 &request.graph,
882 &request.facts.symbolic_bindings,
883 &node_groups,
884 &stages,
885 )?;
886 let abi = build_abi(&request.graph)?;
887 let request_bytes =
888 serde_json::to_vec(&RequestIdentity::from(request)).map_err(serialization_failure)?;
889 let provenance = Provenance {
890 source_graph,
891 request: domain_digest(REQUEST_DIGEST_DOMAIN, &request_bytes),
892 compiler_version: env!("CARGO_PKG_VERSION").to_string(),
893 };
894 let payload = ArtifactPayload {
895 schema_version: ARTIFACT_SCHEMA_VERSION,
896 nodes,
897 dependencies,
898 selected_plan,
899 abi,
900 resources,
901 resource_envelope,
902 geometry,
903 provenance,
904 };
905 let bytes = encode_payload(&payload)?;
906 let byte_len = u64::try_from(bytes.len())
907 .map_err(|_| overflow("artifact", "artifact length exceeds u64"))?;
908 if byte_len > request.max_artifact_bytes {
909 return Err(failure(
910 CompilerFailureKind::ArtifactLimit,
911 "artifact",
912 format!(
913 "canonical artifact is {byte_len} bytes; limit is {}",
914 request.max_artifact_bytes
915 ),
916 "raise the explicit artifact bound or reduce the source graph",
917 ));
918 }
919 let digest: [u8; 32] = bytes[bytes.len() - ARTIFACT_DIGEST_BYTES..]
920 .try_into()
921 .expect("encoded digest length");
922 Ok(Artifact {
923 payload,
924 digest: Digest(digest),
925 })
926}
927
928#[derive(Serialize)]
929struct RequestIdentity<'a> {
930 configuration_digest: Digest,
931 symbolic_bindings: &'a BTreeMap<String, u64>,
932 constant_identities: Vec<(u32, Digest)>,
933 search_budget: SearchBudget,
934}
935
936impl<'a> From<&'a ValidatedCompileRequest> for RequestIdentity<'a> {
937 fn from(request: &'a ValidatedCompileRequest) -> Self {
938 Self {
939 configuration_digest: request.facts.configuration_digest,
940 symbolic_bindings: &request.facts.symbolic_bindings,
941 constant_identities: request
942 .facts
943 .constant_identities
944 .iter()
945 .map(|(id, digest)| (id.0, *digest))
946 .collect(),
947 search_budget: request.search_budget,
948 }
949 }
950}
951
952fn validate_bindings(
953 graph: &ProgramGraph,
954 bindings: &BTreeMap<String, u64>,
955) -> Result<(), CompileError> {
956 let symbols: BTreeSet<&str> = graph
957 .values()
958 .iter()
959 .flat_map(|value| &value.contract.shape)
960 .filter_map(|dim| match dim {
961 ShapeDim::Known(_) => None,
962 ShapeDim::Symbol(symbol) => Some(symbol.as_str()),
963 })
964 .collect();
965 if let Some(symbol) = symbols
966 .iter()
967 .find(|symbol| !bindings.contains_key(**symbol))
968 {
969 return Err(failure(
970 CompilerFailureKind::MissingSymbol,
971 format!("request.facts.symbolic_bindings.{symbol}"),
972 "graph symbol has no exact extent",
973 "bind every symbolic graph dimension before compilation",
974 ));
975 }
976 if let Some(symbol) = bindings
977 .keys()
978 .find(|symbol| !symbols.contains(symbol.as_str()))
979 {
980 return Err(failure(
981 CompilerFailureKind::UnknownSymbol,
982 format!("request.facts.symbolic_bindings.{symbol}"),
983 "binding does not occur in the graph",
984 "remove stale bindings or use the graph's exact symbol name",
985 ));
986 }
987 Ok(())
988}
989
990fn validate_constant_identities(
991 graph: &ProgramGraph,
992 identities: &BTreeMap<GraphValueId, Digest>,
993) -> Result<(), CompileError> {
994 let constants = graph
995 .values()
996 .iter()
997 .filter(|value| value.contract.lifetime == ValueLifetime::Constant)
998 .map(|value| value.id)
999 .collect::<BTreeSet<_>>();
1000 if let Some(id) = constants.iter().find(|id| !identities.contains_key(*id)) {
1001 return Err(failure(
1002 CompilerFailureKind::MissingConstantIdentity,
1003 format!("request.facts.constant_identities.{}", id.0),
1004 "constant graph value has no verified content identity",
1005 "supply one digest keyed by the constant GraphValueId",
1006 ));
1007 }
1008 if let Some(id) = identities.keys().find(|id| !constants.contains(id)) {
1009 return Err(failure(
1010 CompilerFailureKind::UnknownConstantIdentity,
1011 format!("request.facts.constant_identities.{}", id.0),
1012 "constant identity names a non-constant or missing graph value",
1013 "remove stale identities and key constant content by GraphValueId",
1014 ));
1015 }
1016 Ok(())
1017}
1018
1019fn build_abi(graph: &ProgramGraph) -> Result<ArtifactAbi, CompileError> {
1020 let resources = graph
1021 .values()
1022 .iter()
1023 .map(|value| {
1024 let access = match value.contract.access.clone() {
1025 BufferAccess::ReadOnly => AbiAccess::ReadOnly,
1026 BufferAccess::WriteOnly => AbiAccess::WriteOnly,
1027 BufferAccess::ReadWrite => AbiAccess::ReadWrite,
1028 BufferAccess::Uniform => AbiAccess::Uniform,
1029 unsupported => {
1030 return Err(failure(
1031 CompilerFailureKind::InvalidProgram,
1032 format!("request.graph.values[{}].contract.access", value.id.0),
1033 format!("access {unsupported:?} has no artifact ABI representation"),
1034 "lower workgroup/private resources inside the node Program",
1035 ))
1036 }
1037 };
1038 Ok(ResourceAbiRecord {
1039 slot: value.id.0,
1040 value: ArtifactValueId(value.id.0),
1041 dtype: value.contract.dtype.clone(),
1042 access,
1043 })
1044 })
1045 .collect::<Result<Vec<_>, CompileError>>()?;
1046 let entries = graph
1047 .nodes()
1048 .iter()
1049 .map(|node| EntryAbiRecord {
1050 node: ArtifactNodeId(node.id.0),
1051 inputs: node
1052 .inputs
1053 .iter()
1054 .map(|input| ArtifactValueId(input.value.0))
1055 .collect(),
1056 outputs: node
1057 .outputs
1058 .iter()
1059 .map(|output| ArtifactValueId(output.0))
1060 .collect(),
1061 })
1062 .collect();
1063 Ok(ArtifactAbi { resources, entries })
1064}
1065
1066fn ensure_node_dag(
1067 count: usize,
1068 dependencies: &[DependencyEdge],
1069 code: CompilerFailureKind,
1070) -> Result<(), CompileError> {
1071 let groups: Vec<_> = (0..count).map(|id| FusionGroupId(id as u32)).collect();
1072 ensure_group_dag(count, dependencies, &groups, code)
1073}
1074
1075fn ensure_group_dag(
1076 count: usize,
1077 dependencies: &[DependencyEdge],
1078 node_groups: &[FusionGroupId],
1079 code: CompilerFailureKind,
1080) -> Result<(), CompileError> {
1081 group_stages_inner(count, dependencies, node_groups)
1082 .map(|_| ())
1083 .map_err(|_| {
1084 failure(
1085 code,
1086 "artifact.dependencies",
1087 "dependency graph contains a cycle",
1088 "remove the cyclic semantic dependency",
1089 )
1090 })
1091}
1092
1093fn group_stages(
1094 count: usize,
1095 dependencies: &[DependencyEdge],
1096 node_groups: &[FusionGroupId],
1097) -> Result<Vec<u32>, CompileError> {
1098 group_stages_inner(count, dependencies, node_groups).map_err(|_| {
1099 failure(
1100 CompilerFailureKind::DependencyCycle,
1101 "artifact.dependencies",
1102 "selected-plan dependency graph contains a cycle",
1103 "fix compiler legality before plan selection",
1104 )
1105 })
1106}
1107
1108fn group_stages_inner(
1109 count: usize,
1110 dependencies: &[DependencyEdge],
1111 node_groups: &[FusionGroupId],
1112) -> Result<Vec<u32>, ()> {
1113 let mut outgoing = vec![BTreeSet::<usize>::new(); count];
1114 let mut indegree = vec![0usize; count];
1115 for edge in dependencies {
1116 let (DependencyEndpoint::Node(from), DependencyEndpoint::Node(to)) = (edge.from, edge.to)
1117 else {
1118 continue;
1119 };
1120 let from = node_groups[from.0 as usize].0 as usize;
1121 let to = node_groups[to.0 as usize].0 as usize;
1122 if from != to && outgoing[from].insert(to) {
1123 indegree[to] += 1;
1124 }
1125 }
1126 let mut ready: BTreeSet<usize> = indegree
1127 .iter()
1128 .enumerate()
1129 .filter_map(|(index, degree)| (*degree == 0).then_some(index))
1130 .collect();
1131 let mut stage = vec![0u32; count];
1132 let mut visited = 0usize;
1133 while let Some(next) = ready.pop_first() {
1134 visited += 1;
1135 for successor in outgoing[next].iter().copied() {
1136 stage[successor] = stage[successor].max(stage[next].checked_add(1).ok_or(())?);
1137 indegree[successor] -= 1;
1138 if indegree[successor] == 0 {
1139 ready.insert(successor);
1140 }
1141 }
1142 }
1143 (visited == count).then_some(stage).ok_or(())
1144}
1145
1146fn build_barriers(
1147 dependencies: &[DependencyEdge],
1148 node_groups: &[FusionGroupId],
1149 stages: &[u32],
1150) -> Result<Vec<BarrierRecord>, CompileError> {
1151 let max_stage = stages.iter().copied().max().unwrap_or(0);
1152 let mut barriers = Vec::new();
1153 for after_stage in 1..=max_stage {
1154 let mut edge_ids = Vec::new();
1155 for (index, edge) in dependencies.iter().enumerate() {
1156 let (DependencyEndpoint::Node(from), DependencyEndpoint::Node(to)) =
1157 (edge.from, edge.to)
1158 else {
1159 continue;
1160 };
1161 let from_stage = stages[node_groups[from.0 as usize].0 as usize];
1162 let to_stage = stages[node_groups[to.0 as usize].0 as usize];
1163 if from_stage < after_stage && to_stage == after_stage {
1164 edge_ids.push(
1165 u32::try_from(index).map_err(|_| {
1166 overflow("artifact.dependencies", "edge identity exceeds u32")
1167 })?,
1168 );
1169 }
1170 }
1171 barriers.push(BarrierRecord {
1172 before_stage: after_stage - 1,
1173 after_stage,
1174 dependencies: edge_ids,
1175 });
1176 }
1177 Ok(barriers)
1178}
1179
1180fn build_materializations(
1181 graph: &ProgramGraph,
1182 node_groups: &[FusionGroupId],
1183 stages: &[u32],
1184) -> Vec<MaterializationRecord> {
1185 let mut records = Vec::new();
1186 for value in graph.values() {
1187 let Some(producer) = value.producer else {
1188 continue;
1189 };
1190 let producer_node = ArtifactNodeId(producer.0);
1191 let producer_group = node_groups[producer_node.0 as usize];
1192 let producer_stage = stages[producer_group.0 as usize];
1193 let cross_group = value.consumers.iter().any(|consumer| {
1194 let consumer_node = ArtifactNodeId(consumer.0);
1195 node_groups[consumer_node.0 as usize] != producer_group
1196 });
1197 let reason = match value.contract.lifetime {
1198 ValueLifetime::Output => Some(MaterializationReason::Output),
1199 ValueLifetime::Retained => Some(MaterializationReason::Retained),
1200 _ if cross_group => Some(MaterializationReason::CrossGroupUse),
1201 _ => None,
1202 };
1203 if let Some(reason) = reason {
1204 records.push(MaterializationRecord {
1205 value: ArtifactValueId(value.id.0),
1206 producer: producer_group,
1207 stage: producer_stage,
1208 reason,
1209 });
1210 }
1211 }
1212 records.sort_by_key(|record| (record.value, record.reason as u8));
1213 records
1214}
1215
1216fn build_resources(
1217 graph: &ProgramGraph,
1218 bindings: &BTreeMap<String, u64>,
1219 node_groups: &[FusionGroupId],
1220 stages: &[u32],
1221) -> Result<(Vec<ResourceRecord>, ResourceEnvelope), CompileError> {
1222 let final_stage = stages.iter().copied().max().unwrap_or(0);
1223 let mut resources = Vec::with_capacity(graph.values().len());
1224 for value in graph.values() {
1225 let mut element_count = 1u64;
1226 for dim in &value.contract.shape {
1227 let extent = match dim {
1228 ShapeDim::Known(extent) => *extent,
1229 ShapeDim::Symbol(symbol) => bindings[symbol],
1230 };
1231 element_count = element_count.checked_mul(extent).ok_or_else(|| {
1232 overflow(
1233 format!("graph.values[{}].shape", value.name),
1234 "shape element count exceeds u64",
1235 )
1236 })?;
1237 }
1238 let host_count = usize::try_from(element_count).map_err(|_| {
1239 overflow(
1240 format!("graph.values[{}].shape", value.name),
1241 "shape element count exceeds addressable packed-size input",
1242 )
1243 })?;
1244 let byte_count = value
1245 .contract
1246 .dtype
1247 .packed_size_bytes(host_count)
1248 .map_err(|message| overflow(format!("graph.values[{}].dtype", value.name), message))?
1249 .ok_or_else(|| {
1250 failure(
1251 CompilerFailureKind::UnsizedResource,
1252 format!("graph.values[{}].dtype", value.name),
1253 "value representation has no fixed packed byte size",
1254 "resolve the representation to a fixed-width typed value before compilation",
1255 )
1256 })?;
1257 let byte_count = u64::try_from(byte_count).map_err(|_| {
1258 overflow(
1259 format!("graph.values[{}]", value.name),
1260 "packed byte count exceeds u64",
1261 )
1262 })?;
1263 let producer_stage = value.producer.map_or(0, |producer| {
1264 stages[node_groups[producer.0 as usize].0 as usize]
1265 });
1266 let mut last_stage = value
1267 .consumers
1268 .iter()
1269 .map(|consumer| stages[node_groups[consumer.0 as usize].0 as usize])
1270 .max()
1271 .unwrap_or(producer_stage);
1272 if matches!(
1273 value.contract.lifetime,
1274 ValueLifetime::Output | ValueLifetime::Retained
1275 ) {
1276 last_stage = last_stage.max(final_stage);
1277 }
1278 resources.push(ResourceRecord {
1279 value: ArtifactValueId(value.id.0),
1280 name: value.name.clone(),
1281 element_count,
1282 byte_count,
1283 lifetime: match value.contract.lifetime {
1284 ValueLifetime::Constant => ResourceLifetime::Constant,
1285 ValueLifetime::Invocation => ResourceLifetime::Invocation,
1286 ValueLifetime::Retained => ResourceLifetime::Retained,
1287 ValueLifetime::Output => ResourceLifetime::Output,
1288 },
1289 first_stage: producer_stage,
1290 last_stage,
1291 });
1292 }
1293 resources.sort_by_key(|resource| resource.value);
1294 let total_bytes = resources.iter().try_fold(0u64, |total, resource| {
1295 total.checked_add(resource.byte_count).ok_or_else(|| {
1296 overflow(
1297 "artifact.resource_envelope.total_bytes",
1298 "resource sum exceeds u64",
1299 )
1300 })
1301 })?;
1302 let mut peak_live_bytes = 0u64;
1303 for stage in 0..=final_stage {
1304 let live = resources
1305 .iter()
1306 .filter(|resource| resource.first_stage <= stage && stage <= resource.last_stage)
1307 .try_fold(0u64, |total, resource| {
1308 total.checked_add(resource.byte_count).ok_or_else(|| {
1309 overflow(
1310 "artifact.resource_envelope.peak_live_bytes",
1311 "live resource sum exceeds u64",
1312 )
1313 })
1314 })?;
1315 peak_live_bytes = peak_live_bytes.max(live);
1316 }
1317 Ok((
1318 resources,
1319 ResourceEnvelope {
1320 total_bytes,
1321 peak_live_bytes,
1322 },
1323 ))
1324}
1325
1326fn encode_payload(payload: &ArtifactPayload) -> Result<Vec<u8>, CompileError> {
1327 let body = serde_json::to_vec(payload).map_err(serialization_failure)?;
1328 let body_len = u32::try_from(body.len()).map_err(|_| {
1329 overflow(
1330 "artifact.body",
1331 "canonical body exceeds the u32 framing limit",
1332 )
1333 })?;
1334 let digest = artifact_digest(payload.schema_version, &body);
1335 let capacity = ARTIFACT_HEADER_BYTES
1336 .checked_add(body.len())
1337 .and_then(|len| len.checked_add(ARTIFACT_DIGEST_BYTES))
1338 .ok_or_else(|| overflow("artifact", "encoded artifact length overflowed usize"))?;
1339 let mut bytes = Vec::with_capacity(capacity);
1340 bytes.extend_from_slice(ARTIFACT_MAGIC);
1341 bytes.extend_from_slice(&payload.schema_version.to_le_bytes());
1342 bytes.extend_from_slice(&body_len.to_le_bytes());
1343 bytes.extend_from_slice(&body);
1344 bytes.extend_from_slice(&digest.0);
1345 Ok(bytes)
1346}
1347
1348fn artifact_digest(version: u16, body: &[u8]) -> Digest {
1349 let mut hasher = blake3::Hasher::new();
1350 hasher.update(ARTIFACT_DIGEST_DOMAIN);
1351 hasher.update(&version.to_le_bytes());
1352 hasher.update(&(body.len() as u64).to_le_bytes());
1353 hasher.update(body);
1354 Digest(*hasher.finalize().as_bytes())
1355}
1356
1357fn domain_digest(domain: &[u8], bytes: &[u8]) -> Digest {
1358 let mut hasher = blake3::Hasher::new();
1359 hasher.update(domain);
1360 hasher.update(&(bytes.len() as u64).to_le_bytes());
1361 hasher.update(bytes);
1362 Digest(*hasher.finalize().as_bytes())
1363}
1364
1365fn serialization_failure(error: serde_json::Error) -> CompileError {
1366 failure(
1367 CompilerFailureKind::MalformedArtifact,
1368 "artifact.body",
1369 error.to_string(),
1370 "use values representable by the canonical artifact schema",
1371 )
1372}
1373
1374fn overflow(path: impl Into<String>, message: impl Into<String>) -> CompileError {
1375 failure(
1376 CompilerFailureKind::ResourceOverflow,
1377 path,
1378 message,
1379 "reduce resolved extents or split the graph before compilation",
1380 )
1381}
1382
1383fn failure(
1384 code: CompilerFailureKind,
1385 path: impl Into<String>,
1386 message: impl Into<String>,
1387 fix: impl Into<String>,
1388) -> CompileError {
1389 let stage = diagnostic_stage(code);
1390 let retry = diagnostic_retry(code);
1391 CompileError {
1392 diagnostic: Diagnostic::error(code.as_str(), message.into())
1393 .with_stage(stage)
1394 .with_location(OpLocation::op("vyre-megakernel").with_path(path))
1395 .with_fix(fix.into())
1396 .with_retry(retry),
1397 }
1398}