use crate::line::{Error, Function};
use crate::shape::{
Constant, Encoding, Global, Held, Local, Member, Place, Qualifier, Reach, Scope, Shape, Sig,
Spot,
};
use gimli::write::{AttributeValue, FileId, UnitEntryId};
pub(crate) fn describe(
dwarf: &mut gimli::write::DwarfUnit,
shapes: &[Shape],
files: &[FileId],
funcs: &[Function],
globals: &[Global],
frames: bool,
) -> Result<(), Error> {
let ids = kinds(dwarf, shapes);
for (shape, &id) in shapes.iter().zip(&ids) {
fill(dwarf, shape, id, &ids)?;
}
for (index, func) in funcs.iter().enumerate() {
let Some(sig) = &func.sig else { continue };
defined(dwarf, func, sig, index, files, &ids, frames)?;
}
for (index, global) in globals.iter().enumerate() {
held_at(dwarf, global, funcs.len() + index, files, &ids)?;
}
Ok(())
}
fn kinds(dwarf: &mut gimli::write::DwarfUnit, shapes: &[Shape]) -> Vec<UnitEntryId> {
let root = dwarf.unit.root();
shapes.iter().map(|shape| dwarf.unit.add(root, tag(shape))).collect()
}
fn tag(shape: &Shape) -> gimli::DwTag {
match shape {
Shape::Base { .. } => gimli::DW_TAG_base_type,
Shape::Pointer { .. } => gimli::DW_TAG_pointer_type,
Shape::Array { .. } => gimli::DW_TAG_array_type,
Shape::Record { union: false, .. } => gimli::DW_TAG_structure_type,
Shape::Record { union: true, .. } => gimli::DW_TAG_union_type,
Shape::Enumeration { .. } => gimli::DW_TAG_enumeration_type,
Shape::Alias { .. } => gimli::DW_TAG_typedef,
Shape::Qualified { which: Qualifier::Const, .. } => gimli::DW_TAG_const_type,
Shape::Qualified { which: Qualifier::Volatile, .. } => gimli::DW_TAG_volatile_type,
Shape::Qualified { which: Qualifier::Restrict, .. } => gimli::DW_TAG_restrict_type,
Shape::Qualified { which: Qualifier::Atomic, .. } => gimli::DW_TAG_atomic_type,
Shape::Subroutine(_) => gimli::DW_TAG_subroutine_type,
}
}
fn fill(
dwarf: &mut gimli::write::DwarfUnit,
shape: &Shape,
at: UnitEntryId,
ids: &[UnitEntryId],
) -> Result<(), Error> {
match shape {
Shape::Base { name, encoding, size } => {
title(dwarf, at, name);
let read = AttributeValue::Encoding(reading(*encoding));
dwarf.unit.get_mut(at).set(gimli::DW_AT_encoding, read);
bytes(dwarf, at, *size);
}
Shape::Pointer { to, size } => {
bytes(dwarf, at, *size);
points(dwarf, at, *to, ids)?;
}
Shape::Array { of, count } => elements(dwarf, at, *of, *count, ids)?,
Shape::Record { name, size, members, .. } => {
if let Some(name) = name {
title(dwarf, at, name);
}
if let Some(size) = size {
bytes(dwarf, at, *size);
}
match members {
None => flag(dwarf, at, gimli::DW_AT_declaration),
Some(members) => {
for member in members {
held(dwarf, at, member, ids)?;
}
}
}
}
Shape::Enumeration { name, of, size, values } => {
if let Some(name) = name {
title(dwarf, at, name);
}
bytes(dwarf, at, *size);
points(dwarf, at, Some(*of), ids)?;
for value in values {
counted(dwarf, at, value);
}
}
Shape::Alias { name, of } => {
title(dwarf, at, name);
points(dwarf, at, *of, ids)?;
}
Shape::Qualified { of, .. } => points(dwarf, at, *of, ids)?,
Shape::Subroutine(sig) => takes(dwarf, at, sig, ids, None, false)?,
}
Ok(())
}
fn defined(
dwarf: &mut gimli::write::DwarfUnit,
func: &Function,
sig: &Sig,
index: usize,
files: &[FileId],
ids: &[UnitEntryId],
frames: bool,
) -> Result<(), Error> {
let root = dwarf.unit.root();
let at = dwarf.unit.add(root, gimli::DW_TAG_subprogram);
title(dwarf, at, &func.name);
if func.external {
flag(dwarf, at, gimli::DW_AT_external);
}
came_from(dwarf, at, &func.name, func.decl, files)?;
let entry = dwarf.unit.get_mut(at);
let start = gimli::write::Address::Symbol { symbol: index, addend: 0 };
entry.set(gimli::DW_AT_low_pc, AttributeValue::Address(start));
entry.set(gimli::DW_AT_high_pc, AttributeValue::Udata(func.len));
if frames {
let mut expr = gimli::write::Expression::new();
expr.op(gimli::DW_OP_call_frame_cfa);
entry.set(gimli::DW_AT_frame_base, AttributeValue::Exprloc(expr));
}
takes(dwarf, at, sig, ids, Some(index), frames)?;
let nests = nested(dwarf, func, at, index, frames)?;
for local in &func.locals {
let under = local.scope.and_then(|scope| nests.get(scope).copied().flatten()).unwrap_or(at);
kept(dwarf, under, local, files, ids, index, frames)?;
}
Ok(())
}
fn nested(
dwarf: &mut gimli::write::DwarfUnit,
func: &Function,
at: UnitEntryId,
which: usize,
frames: bool,
) -> Result<Vec<Option<UnitEntryId>>, Error> {
let mut wanted = vec![false; func.scopes.len()];
for local in &func.locals {
let Some(scope) = local.scope else { continue };
if sayable(&local.spot, frames) {
if let Some(seen) = wanted.get_mut(scope) {
*seen = true;
}
}
}
for index in (0..wanted.len()).rev() {
if let (true, Some(parent)) = (wanted[index], func.scopes[index].parent) {
if let Some(seen) = wanted.get_mut(parent) {
*seen = true;
}
}
}
let mut nests: Vec<Option<UnitEntryId>> = vec![None; func.scopes.len()];
for (index, scope) in func.scopes.iter().enumerate() {
if !wanted[index] {
continue;
}
let under = scope.parent.and_then(|parent| nests[parent]).unwrap_or(at);
let nest = dwarf.unit.add(under, gimli::DW_TAG_lexical_block);
covers(dwarf, nest, scope, which)?;
nests[index] = Some(nest);
}
Ok(nests)
}
fn covers(
dwarf: &mut gimli::write::DwarfUnit,
at: UnitEntryId,
scope: &Scope,
which: usize,
) -> Result<(), Error> {
let mut list = Vec::with_capacity(scope.over.len());
for reach in &scope.over {
list.push(gimli::write::Range::StartLength {
begin: where_it_starts(reach, which)?,
length: reach.len,
});
}
match list.as_slice() {
[] => {}
&[gimli::write::Range::StartLength { begin, length }] => {
let entry = dwarf.unit.get_mut(at);
entry.set(gimli::DW_AT_low_pc, AttributeValue::Address(begin));
entry.set(gimli::DW_AT_high_pc, AttributeValue::Udata(length));
}
_ => {
let id = dwarf.unit.ranges.add(gimli::write::RangeList(list));
dwarf.unit.get_mut(at).set(gimli::DW_AT_ranges, AttributeValue::RangeListRef(id));
}
}
Ok(())
}
fn where_it_starts(reach: &Reach, which: usize) -> Result<gimli::write::Address, Error> {
if reach.len == 0 {
let why = "a scope covers no addresses at all".to_owned();
return Err(Error::Refused { why });
}
let Ok(addend) = i64::try_from(reach.from) else {
let why = format!("a scope starts {} bytes into its function", reach.from);
return Err(Error::Refused { why });
};
Ok(gimli::write::Address::Symbol { symbol: which, addend })
}
fn kept(
dwarf: &mut gimli::write::DwarfUnit,
at: UnitEntryId,
local: &Local,
files: &[FileId],
ids: &[UnitEntryId],
which: usize,
frames: bool,
) -> Result<(), Error> {
if !sayable(&local.spot, frames) {
return Ok(());
}
let child = dwarf.unit.add(at, gimli::DW_TAG_variable);
title(dwarf, child, &local.name);
came_from(dwarf, child, &local.name, local.decl, files)?;
points(dwarf, child, local.ty, ids)?;
somewhere(dwarf, child, &local.name, &local.spot, which, frames)
}
fn sayable(spot: &Spot, frames: bool) -> bool {
if frames {
return true;
}
match spot {
Spot::Always(held) => matches!(held, Held::Reg(_)),
Spot::Over(spans) => spans.iter().any(|span| matches!(span.held, Held::Reg(_))),
}
}
fn somewhere(
dwarf: &mut gimli::write::DwarfUnit,
at: UnitEntryId,
name: &str,
spot: &Spot,
which: usize,
frames: bool,
) -> Result<(), Error> {
let value = match spot {
Spot::Always(held) if !frames && matches!(held, Held::Frame(_)) => return Ok(()),
Spot::Always(held) => AttributeValue::Exprloc(saying(*held)),
Spot::Over(spans) if spans.is_empty() => return Ok(()),
Spot::Over(spans) => {
let mut list = Vec::with_capacity(spans.len());
for span in spans {
if !frames && matches!(span.held, Held::Frame(_)) {
continue;
}
let Ok(addend) = i64::try_from(span.from) else {
let why = format!("{name} is somewhere {} bytes into its function", span.from);
return Err(Error::Refused { why });
};
if span.len == 0 {
let why = format!("{name} is somewhere over no addresses at all");
return Err(Error::Refused { why });
}
list.push(gimli::write::Location::StartLength {
begin: gimli::write::Address::Symbol { symbol: which, addend },
length: span.len,
data: saying(span.held),
});
}
if list.is_empty() {
return Ok(());
}
let id = dwarf.unit.locations.add(gimli::write::LocationList(list));
AttributeValue::LocationListRef(id)
}
};
dwarf.unit.get_mut(at).set(gimli::DW_AT_location, value);
Ok(())
}
fn saying(held: Held) -> gimli::write::Expression {
let mut expr = gimli::write::Expression::new();
match held {
Held::Frame(at) => expr.op_fbreg(at),
Held::Reg(number) => expr.op_reg(gimli::Register(number)),
}
expr
}
fn held_at(
dwarf: &mut gimli::write::DwarfUnit,
global: &Global,
symbol: usize,
files: &[FileId],
ids: &[UnitEntryId],
) -> Result<(), Error> {
let root = dwarf.unit.root();
let at = dwarf.unit.add(root, gimli::DW_TAG_variable);
title(dwarf, at, &global.name);
if global.external {
flag(dwarf, at, gimli::DW_AT_external);
}
came_from(dwarf, at, &global.name, global.decl, files)?;
points(dwarf, at, global.ty, ids)?;
let mut expr = gimli::write::Expression::new();
expr.op_addr(gimli::write::Address::Symbol { symbol, addend: 0 });
dwarf.unit.get_mut(at).set(gimli::DW_AT_location, AttributeValue::Exprloc(expr));
Ok(())
}
fn came_from(
dwarf: &mut gimli::write::DwarfUnit,
at: UnitEntryId,
name: &str,
place: Option<Place>,
files: &[FileId],
) -> Result<(), Error> {
let Some(place) = place else { return Ok(()) };
let Some(&file) = files.get(place.file) else {
let why = format!("{name} names file {}, which is not one", place.file);
return Err(Error::Refused { why });
};
let entry = dwarf.unit.get_mut(at);
entry.set(gimli::DW_AT_decl_file, AttributeValue::FileIndex(Some(file)));
entry.set(gimli::DW_AT_decl_line, AttributeValue::Udata(u64::from(place.line)));
Ok(())
}
fn takes(
dwarf: &mut gimli::write::DwarfUnit,
at: UnitEntryId,
sig: &Sig,
ids: &[UnitEntryId],
which: Option<usize>,
frames: bool,
) -> Result<(), Error> {
if sig.prototyped {
flag(dwarf, at, gimli::DW_AT_prototyped);
}
points(dwarf, at, sig.returns, ids)?;
for param in &sig.params {
let child = dwarf.unit.add(at, gimli::DW_TAG_formal_parameter);
let name = param.name.clone().unwrap_or_default();
if let Some(name) = ¶m.name {
title(dwarf, child, name);
}
points(dwarf, child, Some(param.ty), ids)?;
if let (Some(spot), Some(which)) = (param.spot.as_ref(), which) {
somewhere(dwarf, child, &name, spot, which, frames)?;
}
}
if sig.variadic {
dwarf.unit.add(at, gimli::DW_TAG_unspecified_parameters);
}
Ok(())
}
fn held(
dwarf: &mut gimli::write::DwarfUnit,
at: UnitEntryId,
member: &Member,
ids: &[UnitEntryId],
) -> Result<(), Error> {
let child = dwarf.unit.add(at, gimli::DW_TAG_member);
if let Some(name) = &member.name {
title(dwarf, child, name);
}
points(dwarf, child, Some(member.ty), ids)?;
let entry = dwarf.unit.get_mut(child);
match member.bits {
Some(bits) => {
entry.set(gimli::DW_AT_data_bit_offset, AttributeValue::Udata(bits.at));
entry.set(gimli::DW_AT_bit_size, AttributeValue::Udata(bits.width));
}
None => entry.set(gimli::DW_AT_data_member_location, AttributeValue::Udata(member.at)),
}
Ok(())
}
fn counted(dwarf: &mut gimli::write::DwarfUnit, at: UnitEntryId, value: &Constant) {
let held = match value.value {
held if held < 0 => match i64::try_from(held) {
Ok(held) => AttributeValue::Sdata(held),
Err(_) => return,
},
held => match u64::try_from(held) {
Ok(held) => AttributeValue::Udata(held),
Err(_) => return,
},
};
let child = dwarf.unit.add(at, gimli::DW_TAG_enumerator);
title(dwarf, child, &value.name);
dwarf.unit.get_mut(child).set(gimli::DW_AT_const_value, held);
}
fn elements(
dwarf: &mut gimli::write::DwarfUnit,
at: UnitEntryId,
of: usize,
count: Option<u64>,
ids: &[UnitEntryId],
) -> Result<(), Error> {
points(dwarf, at, Some(of), ids)?;
let child = dwarf.unit.add(at, gimli::DW_TAG_subrange_type);
if let Some(count) = count.filter(|&count| count > 0) {
let last = AttributeValue::Udata(count - 1);
dwarf.unit.get_mut(child).set(gimli::DW_AT_upper_bound, last);
}
Ok(())
}
fn points(
dwarf: &mut gimli::write::DwarfUnit,
at: UnitEntryId,
of: Option<usize>,
ids: &[UnitEntryId],
) -> Result<(), Error> {
let Some(of) = of else { return Ok(()) };
let Some(&target) = ids.get(of) else {
let why = format!("an entry names type {of}, which is not one");
return Err(Error::Refused { why });
};
dwarf.unit.get_mut(at).set(gimli::DW_AT_type, AttributeValue::UnitRef(target));
Ok(())
}
fn title(dwarf: &mut gimli::write::DwarfUnit, at: UnitEntryId, name: &str) {
let id = dwarf.strings.add(name.to_owned());
dwarf.unit.get_mut(at).set(gimli::DW_AT_name, AttributeValue::StringRef(id));
}
fn bytes(dwarf: &mut gimli::write::DwarfUnit, at: UnitEntryId, size: u64) {
dwarf.unit.get_mut(at).set(gimli::DW_AT_byte_size, AttributeValue::Udata(size));
}
fn flag(dwarf: &mut gimli::write::DwarfUnit, at: UnitEntryId, which: gimli::DwAt) {
dwarf.unit.get_mut(at).set(which, AttributeValue::FlagPresent);
}
fn reading(encoding: Encoding) -> gimli::DwAte {
match encoding {
Encoding::Boolean => gimli::DW_ATE_boolean,
Encoding::Signed => gimli::DW_ATE_signed,
Encoding::Unsigned => gimli::DW_ATE_unsigned,
Encoding::SignedChar => gimli::DW_ATE_signed_char,
Encoding::UnsignedChar => gimli::DW_ATE_unsigned_char,
Encoding::Float => gimli::DW_ATE_float,
Encoding::Complex => gimli::DW_ATE_complex_float,
}
}
#[cfg(test)]
mod tests {
use crate::line::{Row, Unit, write};
use crate::shape::{Held, Local, Member, Param, Place, Shape, Sig, Span, Spot};
use rucc_object::{Info, Reference};
use super::*;
fn one() -> Unit {
Unit {
name: "a.c".to_owned(),
dir: "/tmp".to_owned(),
producer: "rucc".to_owned(),
files: vec!["a.c".to_owned()],
types: vec![Shape::Base {
name: "int".to_owned(),
encoding: Encoding::Signed,
size: 4,
}],
funcs: vec![Function {
name: "f".to_owned(),
len: 16,
rows: vec![Row { at: 0, file: 0, line: 3, column: 1 }],
decl: Some(Place { file: 0, line: 3 }),
sig: Some(Sig {
returns: Some(0),
params: vec![Param { name: Some("n".to_owned()), ty: 0, spot: None }],
variadic: false,
prototyped: true,
}),
external: true,
locals: Vec::new(),
scopes: Vec::new(),
}],
globals: Vec::new(),
pointer: 8,
frames: true,
}
}
fn named(info: &Info) -> Vec<String> {
let Some(chunk) = info.chunks.iter().find(|chunk| chunk.name == ".debug_str") else {
return Vec::new();
};
chunk
.bytes
.split(|&byte| byte == 0)
.filter(|part| !part.is_empty())
.map(|part| String::from_utf8_lossy(part).into_owned())
.collect()
}
#[test]
fn a_function_with_a_signature_gets_an_entry_the_linker_fills_in() {
let info = write(&one()).expect("sections");
let unit = info.chunks.iter().find(|chunk| chunk.name == ".debug_info").expect("a unit");
let at = unit.relocs.iter().find(|reloc| reloc.symbol == "f").expect("an address");
assert_eq!(at.kind, Reference::Address { bytes: 8 });
assert_eq!(at.addend, 0);
let names = named(&info);
assert!(names.contains(&"f".to_owned()), "the function, {names:?}");
assert!(names.contains(&"n".to_owned()), "its parameter, {names:?}");
assert!(names.contains(&"int".to_owned()), "the type, {names:?}");
}
#[test]
fn a_function_with_no_signature_gets_no_entry() {
let mut unit = one();
unit.types.clear();
unit.funcs[0].sig = None;
unit.funcs[0].decl = None;
let info = write(&unit).expect("sections");
let held = info.chunks.iter().find(|chunk| chunk.name == ".debug_info").expect("a unit");
assert!(held.relocs.iter().all(|reloc| reloc.symbol != "f"));
assert!(named(&info).is_empty());
}
fn holds(info: &Info, name: &str, want: &[u8]) -> bool {
let Some(chunk) = info.chunks.iter().find(|chunk| chunk.name == name) else {
return false;
};
chunk.bytes.windows(want.len()).any(|seen| seen == want)
}
fn base() -> [u8; 2] {
[
u8::try_from(gimli::DW_AT_frame_base.0).expect("a one byte attribute"),
u8::try_from(gimli::DW_FORM_exprloc.0).expect("a one byte form"),
]
}
#[test]
fn a_function_says_its_frame_base_is_the_call_frame_address() {
let info = write(&one()).expect("sections");
assert!(holds(&info, ".debug_abbrev", &base()), "no frame base on the subprogram");
let expr = [1, gimli::DW_OP_call_frame_cfa.0];
assert!(holds(&info, ".debug_info", &expr), "the frame base is not the call frame address");
}
#[test]
fn a_build_that_writes_no_unwind_table_gets_no_frame_base() {
let mut unit = one();
unit.frames = false;
let info = write(&unit).expect("sections");
assert!(!holds(&info, ".debug_abbrev", &base()), "a frame base nothing answers");
}
fn spot() -> [u8; 2] {
[
u8::try_from(gimli::DW_AT_location.0).expect("a one byte attribute"),
u8::try_from(gimli::DW_FORM_exprloc.0).expect("a one byte form"),
]
}
fn away(offset: u8) -> [u8; 3] {
[2, gimli::DW_OP_fbreg.0, offset]
}
fn fixed(offset: i64) -> Spot {
Spot::Always(Held::Frame(offset))
}
fn slot(offset: i64) -> Option<Spot> {
Some(fixed(offset))
}
#[test]
fn a_local_with_a_slot_says_how_far_below_the_frame_base_it_is() {
let mut unit = one();
unit.funcs[0].locals = vec![Local {
name: "total".to_owned(),
ty: Some(0),
decl: Some(Place { file: 0, line: 4 }),
spot: Spot::Always(Held::Frame(-16)),
scope: None,
}];
let info = write(&unit).expect("sections");
assert!(holds(&info, ".debug_abbrev", &spot()), "no location on the local");
assert!(holds(&info, ".debug_info", &away(0x70)), "the local is not 16 below the base");
assert!(named(&info).contains(&"total".to_owned()), "the local is not named");
}
#[test]
fn a_parameter_with_a_slot_gets_its_location_and_not_a_second_entry() {
let mut unit = one();
unit.funcs[0].sig.as_mut().expect("a signature").params[0].spot = slot(-8);
let info = write(&unit).expect("sections");
assert!(holds(&info, ".debug_abbrev", &spot()), "no location on the parameter");
assert!(holds(&info, ".debug_info", &away(0x78)), "the parameter is not 8 below the base");
let names = named(&info);
assert_eq!(names.iter().filter(|name| *name == "n").count(), 1, "twice over, {names:?}");
}
fn listed() -> [u8; 2] {
[
u8::try_from(gimli::DW_AT_location.0).expect("a one byte attribute"),
u8::try_from(gimli::DW_FORM_sec_offset.0).expect("a one byte form"),
]
}
#[test]
fn a_local_that_moves_says_where_it_is_over_each_stretch_of_its_function() {
let mut unit = one();
unit.funcs[0].locals = vec![Local {
name: "total".to_owned(),
ty: Some(0),
decl: None,
spot: Spot::Over(vec![
Span { from: 0, len: 8, held: Held::Reg(3) },
Span { from: 8, len: 8, held: Held::Frame(-16) },
]),
scope: None,
}];
let info = write(&unit).expect("sections");
assert!(holds(&info, ".debug_abbrev", &listed()), "the location is not a list");
let start = gimli::DW_LLE_start_length.0;
let reg = [start, 0, 0, 0, 0, 0, 0, 0, 0, 8, 1, gimli::DW_OP_reg3.0];
assert!(holds(&info, ".debug_loclists", ®), "the first stretch is not in a register");
let mem = [start, 0, 0, 0, 0, 0, 0, 0, 0, 8, 2, gimli::DW_OP_fbreg.0, 0x70];
assert!(holds(&info, ".debug_loclists", &mem), "the second stretch is not in the frame");
}
#[test]
fn a_stretch_names_the_function_it_is_measured_into() {
let mut unit = one();
unit.funcs[0].locals = vec![Local {
name: "total".to_owned(),
ty: Some(0),
decl: None,
spot: Spot::Over(vec![
Span { from: 0, len: 8, held: Held::Reg(3) },
Span { from: 8, len: 8, held: Held::Reg(4) },
]),
scope: None,
}];
let info = write(&unit).expect("sections");
let list = info.chunks.iter().find(|chunk| chunk.name == ".debug_loclists");
let list = list.expect("a location list");
let asked: Vec<i64> = list
.relocs
.iter()
.filter(|reloc| reloc.symbol == "f")
.map(|reloc| reloc.addend)
.collect();
assert_eq!(asked, [0, 8], "the stretches do not start where they were said to");
}
#[test]
fn a_local_that_is_nowhere_at_all_gets_no_location_and_keeps_its_name() {
let mut unit = one();
unit.funcs[0].locals = vec![Local {
name: "total".to_owned(),
ty: Some(0),
decl: None,
spot: Spot::Over(Vec::new()),
scope: None,
}];
let info = write(&unit).expect("sections");
assert!(!holds(&info, ".debug_abbrev", &listed()), "a list of nothing");
assert!(!holds(&info, ".debug_abbrev", &spot()), "an expression out of nothing");
assert!(
info.chunks.iter().all(|chunk| chunk.name != ".debug_loclists"),
"an empty section"
);
assert!(named(&info).contains(&"total".to_owned()), "the local lost its name too");
}
#[test]
fn a_stretch_that_covers_no_addresses_is_refused() {
let mut unit = one();
unit.funcs[0].locals = vec![Local {
name: "total".to_owned(),
ty: Some(0),
decl: None,
spot: Spot::Over(vec![Span { from: 0, len: 0, held: Held::Reg(3) }]),
scope: None,
}];
assert!(write(&unit).is_err());
}
#[test]
fn a_build_that_writes_no_unwind_table_says_nothing_about_where_a_local_is() {
let mut unit = one();
unit.frames = false;
unit.funcs[0].sig.as_mut().expect("a signature").params[0].spot = slot(-8);
unit.funcs[0].locals = vec![Local {
name: "total".to_owned(),
ty: Some(0),
decl: None,
spot: fixed(-16),
scope: None,
}];
let info = write(&unit).expect("sections");
assert!(!holds(&info, ".debug_abbrev", &spot()), "a location nothing can resolve");
assert!(!named(&info).contains(&"total".to_owned()), "a name with nowhere to be");
}
#[test]
fn a_build_with_no_frame_base_still_says_which_register_a_local_is_in() {
let mut unit = one();
unit.frames = false;
unit.funcs[0].locals = vec![Local {
name: "total".to_owned(),
ty: Some(0),
decl: None,
spot: Spot::Over(vec![Span { from: 0, len: 8, held: Held::Reg(3) }]),
scope: None,
}];
let info = write(&unit).expect("sections");
assert!(holds(&info, ".debug_abbrev", &listed()), "the register went with the frame base");
assert!(named(&info).contains(&"total".to_owned()), "the local lost its name");
}
#[test]
fn a_build_with_no_frame_base_keeps_the_stretches_that_do_not_need_one() {
let mut unit = one();
unit.frames = false;
unit.funcs[0].locals = vec![Local {
name: "total".to_owned(),
ty: Some(0),
decl: None,
spot: Spot::Over(vec![
Span { from: 0, len: 8, held: Held::Reg(3) },
Span { from: 8, len: 8, held: Held::Frame(-16) },
]),
scope: None,
}];
let info = write(&unit).expect("sections");
let start = gimli::DW_LLE_start_length.0;
let reg = [start, 0, 0, 0, 0, 0, 0, 0, 0, 8, 1, gimli::DW_OP_reg3.0];
assert!(holds(&info, ".debug_loclists", ®), "the register stretch went too");
let mem = [start, 0, 0, 0, 0, 0, 0, 0, 0, 8, 2, gimli::DW_OP_fbreg.0, 0x70];
assert!(!holds(&info, ".debug_loclists", &mem), "an offset from nothing");
}
#[test]
fn a_build_with_no_frame_base_drops_a_local_that_is_only_ever_in_the_frame() {
let mut unit = one();
unit.frames = false;
unit.funcs[0].locals = vec![Local {
name: "total".to_owned(),
ty: Some(0),
decl: None,
spot: Spot::Over(vec![Span { from: 0, len: 8, held: Held::Frame(-16) }]),
scope: None,
}];
let info = write(&unit).expect("sections");
assert!(!named(&info).contains(&"total".to_owned()), "a name with nowhere to be");
}
#[test]
fn a_record_that_holds_a_pointer_to_itself_is_written_once() {
let mut unit = one();
unit.types = vec![
Shape::Record {
union: false,
name: Some("node".to_owned()),
size: Some(8),
members: Some(vec![Member {
name: Some("next".to_owned()),
ty: 1,
at: 0,
bits: None,
}]),
},
Shape::Pointer { to: Some(0), size: 8 },
];
unit.funcs[0].sig =
Some(Sig { returns: Some(1), params: Vec::new(), variadic: false, prototyped: true });
let names = named(&write(&unit).expect("sections"));
assert_eq!(names.iter().filter(|name| *name == "node").count(), 1, "{names:?}");
assert!(names.contains(&"next".to_owned()), "{names:?}");
}
#[test]
fn an_entry_naming_a_type_that_is_not_there_is_refused() {
let mut unit = one();
unit.types = vec![Shape::Pointer { to: Some(9), size: 8 }];
assert!(write(&unit).is_err());
}
#[test]
fn a_declaration_naming_a_file_that_is_not_there_is_refused() {
let mut unit = one();
unit.funcs[0].decl = Some(Place { file: 4, line: 3 });
assert!(write(&unit).is_err());
}
#[test]
fn a_file_scope_variable_gets_an_entry_the_linker_fills_in() {
let mut unit = one();
unit.globals = vec![Global {
name: "counter".to_owned(),
ty: Some(0),
decl: Some(Place { file: 0, line: 1 }),
external: true,
}];
let info = write(&unit).expect("sections");
let held = info.chunks.iter().find(|chunk| chunk.name == ".debug_info").expect("a unit");
let at = held.relocs.iter().find(|reloc| reloc.symbol == "counter").expect("an address");
assert_eq!(at.kind, Reference::Address { bytes: 8 });
assert_eq!(at.addend, 0);
assert!(held.relocs.iter().any(|reloc| reloc.symbol == "f"), "and the function still");
assert!(named(&info).contains(&"counter".to_owned()));
}
#[test]
fn a_variable_with_no_type_still_gets_an_entry() {
let mut unit = one();
unit.globals =
vec![Global { name: "opaque".to_owned(), ty: None, decl: None, external: false }];
let info = write(&unit).expect("sections");
let held = info.chunks.iter().find(|chunk| chunk.name == ".debug_info").expect("a unit");
assert!(held.relocs.iter().any(|reloc| reloc.symbol == "opaque"));
assert!(named(&info).contains(&"opaque".to_owned()));
}
#[test]
fn an_enumeration_names_its_enumerators() {
let mut unit = one();
unit.types.push(Shape::Enumeration {
name: Some("color".to_owned()),
of: 0,
size: 4,
values: vec![
Constant { name: "red".to_owned(), value: 0 },
Constant { name: "green".to_owned(), value: -1 },
],
});
let names = named(&write(&unit).expect("sections"));
assert!(names.contains(&"color".to_owned()), "{names:?}");
assert!(names.contains(&"red".to_owned()), "{names:?}");
assert!(names.contains(&"green".to_owned()), "{names:?}");
}
#[test]
fn an_enumerator_too_wide_for_a_form_is_left_out() {
let mut unit = one();
unit.types.push(Shape::Enumeration {
name: Some("wide".to_owned()),
of: 0,
size: 16,
values: vec![
Constant { name: "small".to_owned(), value: 1 },
Constant { name: "huge".to_owned(), value: i128::from(u64::MAX) + 1 },
],
});
let names = named(&write(&unit).expect("sections"));
assert!(names.contains(&"small".to_owned()), "{names:?}");
assert!(!names.contains(&"huge".to_owned()), "{names:?}");
}
fn asked(info: &Info, section: &str) -> Vec<i64> {
let Some(chunk) = info.chunks.iter().find(|chunk| chunk.name == section) else {
return Vec::new();
};
chunk.relocs.iter().filter(|reloc| reloc.symbol == "f").map(|reloc| reloc.addend).collect()
}
fn inside(over: Vec<Reach>) -> Unit {
let mut unit = one();
unit.funcs[0].scopes = vec![Scope { parent: None, over }];
unit.funcs[0].locals = vec![Local {
name: "inner".to_owned(),
ty: Some(0),
decl: Some(Place { file: 0, line: 5 }),
spot: fixed(-16),
scope: Some(0),
}];
unit
}
#[test]
fn a_local_declared_in_an_inner_scope_gets_a_block_around_it() {
let info = write(&inside(vec![Reach { from: 4, len: 8 }])).expect("sections");
assert_eq!(asked(&info, ".debug_info"), vec![0, 4]);
assert!(named(&info).contains(&"inner".to_owned()), "the local is not named");
}
#[test]
fn a_scope_laid_out_in_two_pieces_gets_a_list_of_them() {
let over = vec![Reach { from: 4, len: 8 }, Reach { from: 24, len: 4 }];
let info = write(&inside(over)).expect("sections");
assert_eq!(asked(&info, ".debug_info"), vec![0]);
assert_eq!(asked(&info, ".debug_rnglists"), vec![0, 4, 24]);
}
#[test]
fn a_scope_with_no_addresses_left_still_holds_its_names() {
let info = write(&inside(Vec::new())).expect("sections");
assert_eq!(asked(&info, ".debug_info"), vec![0], "a block that says where it is not");
assert!(named(&info).contains(&"inner".to_owned()), "the local went with it");
}
#[test]
fn a_scope_with_nothing_declared_in_it_gets_no_block() {
let mut unit = inside(vec![Reach { from: 4, len: 8 }]);
unit.funcs[0].locals[0].scope = None;
let info = write(&unit).expect("sections");
assert_eq!(asked(&info, ".debug_info"), vec![0], "an empty nest");
}
#[test]
fn a_scope_whose_only_child_is_a_scope_with_a_local_is_written() {
let mut unit = inside(vec![Reach { from: 4, len: 20 }]);
unit.funcs[0].scopes.push(Scope { parent: Some(0), over: vec![Reach { from: 8, len: 8 }] });
unit.funcs[0].locals[0].scope = Some(1);
let info = write(&unit).expect("sections");
assert_eq!(asked(&info, ".debug_info"), vec![0, 4, 8], "the outer nest is missing");
}
#[test]
fn a_scope_over_a_stretch_of_no_length_is_refused() {
let over = vec![Reach { from: 4, len: 0 }];
assert!(matches!(write(&inside(over)), Err(Error::Refused { .. })));
}
}