use brink_format::{ContainerDef, NameId};
use crate::CodegenError;
pub const UNRESOLVED_NAME_ID: u16 = u16::MAX;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NameRef {
Symbol(String),
}
impl NameRef {
#[must_use]
pub fn as_str(&self) -> &str {
match self {
NameRef::Symbol(s) => s,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Relocation {
pub offset: u32,
pub name: NameRef,
}
#[derive(Debug, Clone)]
pub struct ContainerChunk {
pub def: ContainerDef,
pub relocations: Vec<Relocation>,
}
impl ContainerChunk {
pub fn link(
mut self,
resolve: impl Fn(&str) -> Option<NameId>,
) -> Result<ContainerDef, CodegenError> {
for reloc in &self.relocations {
let id = resolve(reloc.name.as_str()).ok_or_else(|| {
CodegenError::new(format!(
"codegen link: unresolved name reference {:?} at byte offset {}",
reloc.name.as_str(),
reloc.offset
))
})?;
let start = reloc.offset as usize;
let len = self.def.bytecode.len();
let slot = self.def.bytecode.get_mut(start..start + 2).ok_or_else(|| {
CodegenError::new(format!(
"codegen link: relocation offset {} out of bounds for chunk bytecode (len {len})",
reloc.offset,
))
})?;
slot.copy_from_slice(&id.0.to_le_bytes());
}
Ok(self.def)
}
}
#[cfg(test)]
mod tests {
use brink_format::{CountingFlags, DefinitionId, DefinitionTag, Opcode};
use super::{ContainerChunk, NameRef, Relocation, UNRESOLVED_NAME_ID};
fn def_id() -> DefinitionId {
DefinitionId::new(DefinitionTag::Address, 1)
}
fn chunk_with(bytecode: Vec<u8>, relocations: Vec<Relocation>) -> ContainerChunk {
ContainerChunk {
def: brink_format::ContainerDef {
id: def_id(),
scope_id: def_id(),
name: None,
bytecode,
counting_flags: CountingFlags::empty(),
path_hash: 0,
param_count: 0,
params: Vec::new(),
local: false,
},
relocations,
}
}
#[test]
fn push_string_relocation_round_trips() {
let mut bytecode = Vec::new();
Opcode::PushString(UNRESOLVED_NAME_ID).encode(&mut bytecode);
let offset = u32::try_from(bytecode.len() - 2).unwrap();
let chunk = chunk_with(
bytecode,
vec![Relocation {
offset,
name: NameRef::Symbol("greeting".to_string()),
}],
);
assert_eq!(
&chunk.def.bytecode[offset as usize..offset as usize + 2],
&[0xFF, 0xFF]
);
let linked = chunk
.link(|s| (s == "greeting").then_some(brink_format::NameId(7)))
.expect("resolvable");
let mut expected = Vec::new();
Opcode::PushString(7).encode(&mut expected);
assert_eq!(linked.bytecode, expected);
}
#[test]
fn multiple_and_empty_relocations() {
let mut bytecode = Vec::new();
Opcode::PushString(UNRESOLVED_NAME_ID).encode(&mut bytecode);
let first = u32::try_from(bytecode.len() - 2).unwrap();
Opcode::PushString(UNRESOLVED_NAME_ID).encode(&mut bytecode);
let second = u32::try_from(bytecode.len() - 2).unwrap();
let chunk = chunk_with(
bytecode,
vec![
Relocation {
offset: first,
name: NameRef::Symbol("a".to_string()),
},
Relocation {
offset: second,
name: NameRef::Symbol("b".to_string()),
},
],
);
let linked = chunk
.link(|s| match s {
"a" => Some(brink_format::NameId(3)),
"b" => Some(brink_format::NameId(9)),
_ => None,
})
.expect("resolvable");
let mut expected = Vec::new();
Opcode::PushString(3).encode(&mut expected);
Opcode::PushString(9).encode(&mut expected);
assert_eq!(linked.bytecode, expected);
let untouched = chunk_with(vec![0xAB, 0xCD], Vec::new());
let out = untouched.link(|_| None).expect("no work");
assert_eq!(out.bytecode, vec![0xAB, 0xCD]);
}
#[test]
fn unresolved_symbol_is_an_error() {
let mut bytecode = Vec::new();
Opcode::PushString(UNRESOLVED_NAME_ID).encode(&mut bytecode);
let offset = u32::try_from(bytecode.len() - 2).unwrap();
let chunk = chunk_with(
bytecode,
vec![Relocation {
offset,
name: NameRef::Symbol("missing".to_string()),
}],
);
let err = chunk.link(|_| None).expect_err("should fail");
assert!(err.to_string().contains("missing"));
}
}