use serde::{Deserialize, Serialize};
use super::backend::BackendOutput;
use super::core::{IrModule, IrType};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ArtifactKind {
Executable,
SharedLibrary,
StaticLibrary,
ObjectFile,
WasmModule,
RawBinary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OptimizationLevel {
None,
Less,
Default,
Aggressive,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ComponentImport {
pub name: String,
pub interface: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ComponentExport {
pub name: String,
pub interface: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ComponentCapability {
pub name: String,
pub capability_type: String,
pub args: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ComponentSpec {
pub name: String,
pub target: String,
pub artifact_kind: ArtifactKind,
pub deterministic: bool,
pub checkpoint: String,
pub optimization_level: OptimizationLevel,
pub debug_info: bool,
pub entry_point: Option<String>,
pub imports: Vec<ComponentImport>,
pub exports: Vec<ComponentExport>,
pub capabilities: Vec<ComponentCapability>,
pub capabilities_exported: Vec<ComponentCapability>,
}
impl ComponentSpec {
pub fn host_executable(name: &str, entry_point: Option<&str>) -> Self {
Self {
name: name.to_string(),
target: Self::host_triple(),
artifact_kind: ArtifactKind::Executable,
deterministic: false,
checkpoint: String::new(),
optimization_level: OptimizationLevel::Default,
debug_info: false,
entry_point: entry_point.map(str::to_string),
imports: Vec::new(),
exports: Vec::new(),
capabilities: Vec::new(),
capabilities_exported: Vec::new(),
}
}
pub fn host_triple() -> String {
let os = if cfg!(target_os = "macos") {
"apple-darwin"
} else if cfg!(target_os = "linux") {
"unknown-linux-gnu"
} else if cfg!(target_os = "windows") {
"pc-windows-msvc"
} else {
"unknown-none"
};
let arch = if cfg!(target_arch = "aarch64") {
"aarch64"
} else if cfg!(target_arch = "x86_64") {
"x86_64"
} else if cfg!(target_arch = "arm") {
"armv7"
} else {
"unknown"
};
format!("{arch}-{os}")
}
pub fn object_format(&self) -> &'static str {
if self.target.contains("apple") {
"mach-o"
} else if self.target.contains("-linux-") || self.target.contains("-none") {
"elf"
} else if self.target.contains("-windows-") {
"coff"
} else if self.target.starts_with("wasm32") {
"wasm"
} else {
"raw"
}
}
pub fn is_freestanding(&self) -> bool {
self.target.ends_with("-none") || self.target.ends_with("-none-elf")
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InterfaceMethod {
pub name: String,
pub params: Vec<(String, IrType)>,
pub return_type: IrType,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InterfaceDecl {
pub name: String,
pub methods: Vec<InterfaceMethod>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CodeSection {
pub name: String,
pub offset: u64,
pub size: u64,
pub flags: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DataSection {
pub name: String,
pub offset: u64,
pub size: u64,
pub flags: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ObjectField {
pub name: String,
#[serde(rename = "type")]
pub typ: String,
pub offset: u64,
pub size: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ObjectSchema {
pub name: String,
pub fields: Vec<ObjectField>,
pub size: u64,
pub align: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MemoryRequirements {
pub stack_bytes: u64,
pub heap_bytes: u64,
pub static_bytes: u64,
pub vm_object_pages: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct BuildProvenance {
pub compiler_version: String,
pub source_hash: String,
pub build_timestamp: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ComponentMetadata {
pub name: String,
pub target: String,
pub entry: Option<String>,
pub artifact_kind: ArtifactKind,
pub code_sections: Vec<CodeSection>,
pub data_sections: Vec<DataSection>,
pub imports: Vec<ComponentImport>,
pub exports: Vec<ComponentExport>,
pub capabilities_required: Vec<ComponentCapability>,
pub capabilities_exported: Vec<ComponentCapability>,
pub interfaces: Vec<InterfaceDecl>,
pub object_schemas: Vec<ObjectSchema>,
pub memory: MemoryRequirements,
pub checkpoint: String,
pub deterministic: bool,
pub provenance: BuildProvenance,
}
fn ir_type_name(typ: &IrType) -> String {
match typ {
IrType::I8 | IrType::U8 => "i8".to_string(),
IrType::I16 | IrType::U16 => "i16".to_string(),
IrType::I32 | IrType::U32 | IrType::Int(32) => "i32".to_string(),
IrType::I64 | IrType::U64 | IrType::Int(64) => "i64".to_string(),
IrType::F32 | IrType::Float(32) => "f32".to_string(),
IrType::F64 | IrType::Float(64) => "f64".to_string(),
IrType::Bool => "bool".to_string(),
IrType::Ptr(elem) => format!("*{}", ir_type_name(elem)),
IrType::Array(elem, _) => format!("[{}]", ir_type_name(elem)),
IrType::Slice(elem) => format!("[]{}", ir_type_name(elem)),
IrType::Named(name) => name.clone(),
IrType::Void => "void".to_string(),
IrType::Never => "never".to_string(),
_ => "i64".to_string(),
}
}
impl ComponentMetadata {
pub fn build(
spec: &ComponentSpec,
module: &IrModule,
output: &BackendOutput,
source: &str,
) -> Self {
let source_hash = {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
source.hash(&mut hasher);
format!("{:x}", hasher.finish())
};
let object_schemas: Vec<ObjectSchema> = module
.struct_types
.iter()
.map(|(name, fields)| {
let mut offset = 0u64;
let obj_fields: Vec<ObjectField> = fields
.iter()
.map(|(fn_, typ)| {
let size = match typ {
IrType::I8 | IrType::U8 => 1,
IrType::I16 | IrType::U16 => 2,
IrType::I32 | IrType::U32 | IrType::F32 => 4,
IrType::I64
| IrType::U64
| IrType::F64
| IrType::Bool
| IrType::Ptr(_) => 8,
IrType::Int(bits) => (bits / 8) as u64,
IrType::Float(bits) => (bits / 8) as u64,
IrType::Array(elem, count) => {
let elem_size = match elem.as_ref() {
IrType::I8 | IrType::U8 => 1,
IrType::I16 | IrType::U16 => 2,
IrType::I32 | IrType::U32 | IrType::F32 => 4,
_ => 8,
};
elem_size * (*count as u64)
}
_ => 8,
};
let off = offset;
offset += size;
ObjectField {
name: fn_.clone(),
typ: ir_type_name(typ),
offset: off,
size,
}
})
.collect();
let size = offset.next_multiple_of(8);
ObjectSchema {
name: name.clone(),
fields: obj_fields,
size,
align: 8,
}
})
.collect();
let code_sections = if output.data.is_empty() {
vec![CodeSection {
name: ".text".into(),
offset: 0,
size: output.data.len() as u64,
flags: "rx".into(),
}]
} else {
vec![CodeSection {
name: ".text".into(),
offset: 0,
size: output.data.len() as u64,
flags: "rx".into(),
}]
};
Self {
name: spec.name.clone(),
target: spec.target.clone(),
entry: spec.entry_point.clone(),
artifact_kind: spec.artifact_kind,
code_sections,
data_sections: Vec::new(),
imports: spec.imports.clone(),
exports: spec.exports.clone(),
capabilities_required: spec.capabilities.clone(),
capabilities_exported: spec.capabilities_exported.clone(),
interfaces: Vec::new(),
object_schemas,
memory: MemoryRequirements {
stack_bytes: 0x1000,
heap_bytes: 0,
static_bytes: 0,
vm_object_pages: 0,
},
checkpoint: spec.checkpoint.clone(),
deterministic: spec.deterministic,
provenance: BuildProvenance {
compiler_version: env!("CARGO_PKG_VERSION").to_string(),
source_hash,
build_timestamp: String::new(),
},
}
}
#[deprecated(note = "use build() which accepts backend output and source")]
pub fn from_spec(spec: &ComponentSpec, module: &IrModule) -> Self {
Self::build(
spec,
module,
&BackendOutput {
data: Vec::new(),
extension: "",
entry_offset: None,
symbol_table: vec![],
},
"",
)
}
pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
}
#[cfg(test)]
mod tests {
use super::super::core::{IrModule, IrType};
use super::*;
#[test]
fn spec_host_executable_creates_valid_spec() {
let spec = ComponentSpec::host_executable("test", Some("main"));
assert_eq!(spec.name, "test");
assert_eq!(spec.entry_point, Some("main".to_string()));
assert_eq!(spec.artifact_kind, ArtifactKind::Executable);
assert!(!spec.target.is_empty());
}
#[test]
fn object_format_detection() {
let spec = ComponentSpec {
name: "t".into(),
target: "aarch64-apple-darwin".into(),
artifact_kind: ArtifactKind::Executable,
deterministic: false,
checkpoint: String::new(),
optimization_level: OptimizationLevel::Default,
debug_info: false,
entry_point: None,
imports: vec![],
exports: vec![],
capabilities: vec![],
capabilities_exported: vec![],
};
assert_eq!(spec.object_format(), "mach-o");
let spec2 = ComponentSpec {
target: "x86_64-unknown-none".into(),
..spec
};
assert_eq!(spec2.object_format(), "elf");
assert!(spec2.is_freestanding());
}
#[test]
fn metadata_build_includes_code_sections_and_source_hash() {
let spec = ComponentSpec::host_executable("my-component", Some("start"));
let module = IrModule::new("my-component");
let output = BackendOutput {
data: vec![0x00; 128],
extension: "o",
entry_offset: Some(0),
symbol_table: vec![],
};
let metadata = ComponentMetadata::build(&spec, &module, &output, "fn main() {}");
assert_eq!(metadata.name, "my-component");
assert!(!metadata.code_sections.is_empty());
assert_eq!(metadata.code_sections[0].size, 128);
assert!(!metadata.provenance.source_hash.is_empty());
assert!(metadata.capabilities_exported.is_empty());
let json = metadata.to_json_pretty().unwrap();
assert!(json.contains("code_sections"));
assert!(json.contains("source_hash"));
}
#[test]
fn capabilities_separated() {
let mut spec = ComponentSpec::host_executable("test", None);
spec.capabilities = vec![ComponentCapability {
name: "serial".into(),
capability_type: "DebugConsole".into(),
args: vec!["write".into()],
}];
spec.capabilities_exported = vec![ComponentCapability {
name: "grant".into(),
capability_type: "CapabilityTable".into(),
args: vec!["grant".into(), "attenuate".into()],
}];
let module = IrModule::new("test");
let output = BackendOutput {
data: vec![],
extension: "o",
entry_offset: None,
symbol_table: vec![],
};
let md = ComponentMetadata::build(&spec, &module, &output, "");
assert_eq!(md.capabilities_required.len(), 1);
assert_eq!(md.capabilities_exported.len(), 1);
assert_eq!(md.capabilities_required[0].name, "serial");
assert_eq!(md.capabilities_exported[0].name, "grant");
}
#[test]
fn object_schemas_extracted_from_struct_types() {
let mut module = IrModule::new("objects");
module.struct_types.push((
"User".to_string(),
vec![
("id".to_string(), IrType::I64),
("name".to_string(), IrType::Ptr(Box::new(IrType::I8))),
("active".to_string(), IrType::Bool),
],
));
module.struct_types.push((
"Point".to_string(),
vec![
("x".to_string(), IrType::F64),
("y".to_string(), IrType::F64),
],
));
let spec = ComponentSpec::host_executable("objects-app", None);
let output = BackendOutput {
data: vec![0x00; 64],
extension: "o",
entry_offset: None,
symbol_table: vec![],
};
let md = ComponentMetadata::build(&spec, &module, &output, "fn main() {}");
assert_eq!(md.object_schemas.len(), 2);
let user = &md.object_schemas[0];
assert_eq!(user.name, "User");
assert_eq!(user.fields.len(), 3);
assert_eq!(user.fields[0].name, "id");
assert_eq!(user.fields[0].offset, 0);
assert_eq!(user.fields[0].size, 8);
assert_eq!(user.fields[1].name, "name");
assert_eq!(user.fields[1].offset, 8);
assert_eq!(user.fields[1].size, 8);
assert_eq!(user.fields[2].name, "active");
assert_eq!(user.fields[2].offset, 16);
assert_eq!(user.fields[2].size, 8);
assert_eq!(user.size, 24);
let point = &md.object_schemas[1];
assert_eq!(point.name, "Point");
assert_eq!(point.fields.len(), 2);
assert_eq!(point.fields[0].name, "x");
assert_eq!(point.fields[0].offset, 0);
assert_eq!(point.fields[1].name, "y");
assert_eq!(point.fields[1].offset, 8);
assert_eq!(point.size, 16);
let json = md.to_json_pretty().unwrap();
assert!(json.contains("object_schemas"));
assert!(json.contains("User"));
assert!(json.contains("Point"));
assert!(json.contains("\"offset\": 0"));
}
}