Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion cc/arch/aarch64/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1397,6 +1397,7 @@ impl Aarch64CodeGen {
}

pub(super) fn emit_move(&mut self, src: PseudoId, dst: Reg, size: u32, frame_size: i32) {
let actual_size = size; // Keep original size for sub-32-bit stack loads
let size = size.max(32);
let loc = self.get_location(src);
let op_size = OperandSize::from_bits(size);
Expand All @@ -1413,9 +1414,12 @@ impl Aarch64CodeGen {
}
Loc::Stack(offset) => {
let actual_offset = self.stack_offset(frame_size, offset);
// For sub-32-bit values, use sized load (ldrb/ldrh) which zero-extends.
// This avoids reading garbage from adjacent stack bytes.
let load_size = OperandSize::from_bits(actual_size.max(8));
// LIR: load from stack (FP-relative for alloca safety)
self.push_lir(Aarch64Inst::Ldr {
size: op_size,
size: load_size,
addr: MemAddr::BaseOffset {
base: Reg::X29,
offset: actual_offset,
Expand Down
116 changes: 87 additions & 29 deletions cc/arch/x86_64/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,7 @@ impl X86_64CodeGen {
}

pub(super) fn emit_move(&mut self, src: PseudoId, dst: Reg, size: u32) {
let actual_size = size; // Keep original size for sub-32-bit handling
let size = size.max(32);
let op_size = OperandSize::from_bits(size);
let loc = self.get_location(src);
Expand All @@ -1011,26 +1012,54 @@ impl X86_64CodeGen {
}
Loc::Stack(offset) => {
let adjusted = offset + self.callee_saved_offset;
// LIR: memory-to-register move
self.push_lir(X86Inst::Mov {
size: op_size,
src: GpOperand::Mem(MemAddr::BaseOffset {
base: Reg::Rbp,
offset: -adjusted,
}),
dst: GpOperand::Reg(dst),
});
// For sub-32-bit values, use zero-extending load to avoid garbage in upper bits
if actual_size < 32 {
// LIR: zero-extending memory-to-register move
self.push_lir(X86Inst::Movzx {
src_size: OperandSize::from_bits(actual_size),
dst_size: OperandSize::B32,
src: GpOperand::Mem(MemAddr::BaseOffset {
base: Reg::Rbp,
offset: -adjusted,
}),
dst,
});
} else {
// LIR: memory-to-register move
self.push_lir(X86Inst::Mov {
size: op_size,
src: GpOperand::Mem(MemAddr::BaseOffset {
base: Reg::Rbp,
offset: -adjusted,
}),
dst: GpOperand::Reg(dst),
});
}
}
Loc::IncomingArg(offset) => {
// LIR: memory-to-register move from incoming stack arg
self.push_lir(X86Inst::Mov {
size: op_size,
src: GpOperand::Mem(MemAddr::BaseOffset {
base: Reg::Rbp,
offset,
}),
dst: GpOperand::Reg(dst),
});
// For sub-32-bit values, use zero-extending load
if actual_size < 32 {
// LIR: zero-extending memory-to-register move from incoming stack arg
self.push_lir(X86Inst::Movzx {
src_size: OperandSize::from_bits(actual_size),
dst_size: OperandSize::B32,
src: GpOperand::Mem(MemAddr::BaseOffset {
base: Reg::Rbp,
offset,
}),
dst,
});
} else {
// LIR: memory-to-register move from incoming stack arg
self.push_lir(X86Inst::Mov {
size: op_size,
src: GpOperand::Mem(MemAddr::BaseOffset {
base: Reg::Rbp,
offset,
}),
dst: GpOperand::Reg(dst),
});
}
}
Loc::Imm(v) => {
// x86-64: movl sign-extends to 64-bit, movq only works with 32-bit signed immediates
Expand Down Expand Up @@ -1114,18 +1143,47 @@ impl X86_64CodeGen {
});
}
Loc::Stack(offset) => {
// Use actual size for memory stores (8, 16, 32, 64 bits)
let adjusted = offset + self.callee_saved_offset;
let op_size = OperandSize::from_bits(size);
// LIR: register-to-memory move
self.push_lir(X86Inst::Mov {
size: op_size,
src: GpOperand::Reg(src),
dst: GpOperand::Mem(MemAddr::BaseOffset {
base: Reg::Rbp,
offset: -adjusted,
}),
});
// For sub-32-bit values, first zero-extend to 32 bits then store 32 bits.
// This ensures that subsequent 32-bit loads get correct values.
if size < 32 {
// Use scratch register to avoid modifying source
let scratch = if src == Reg::R10 { Reg::R11 } else { Reg::R10 };
// Copy to scratch
self.push_lir(X86Inst::Mov {
size: OperandSize::B32,
src: GpOperand::Reg(src),
dst: GpOperand::Reg(scratch),
});
// Zero-extend by masking to the appropriate size
let mask = (1i64 << size) - 1;
self.push_lir(X86Inst::And {
size: OperandSize::B32,
src: GpOperand::Imm(mask),
dst: scratch,
});
// Store 32 bits
self.push_lir(X86Inst::Mov {
size: OperandSize::B32,
src: GpOperand::Reg(scratch),
dst: GpOperand::Mem(MemAddr::BaseOffset {
base: Reg::Rbp,
offset: -adjusted,
}),
});
} else {
// Store with actual size
let op_size = OperandSize::from_bits(size);
// LIR: register-to-memory move
self.push_lir(X86Inst::Mov {
size: op_size,
src: GpOperand::Reg(src),
dst: GpOperand::Mem(MemAddr::BaseOffset {
base: Reg::Rbp,
offset: -adjusted,
}),
});
}
}
_ => {}
}
Expand Down
15 changes: 12 additions & 3 deletions cc/ir/linearize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -725,7 +725,12 @@ impl<'a> Linearizer<'a> {
}

TypeKind::Struct | TypeKind::Union => {
if let Some(composite) = self.types.get(typ).composite.as_ref() {
// Resolve incomplete struct types to their complete definitions
// This is needed when a typedef to an incomplete struct is used
// before the struct is fully defined (forward declaration pattern)
let resolved_typ = self.resolve_struct_type(typ);
let resolved_size = (self.types.size_bits(resolved_typ) / 8) as usize;
if let Some(composite) = self.types.get(resolved_typ).composite.as_ref() {
let members = &composite.members;
let mut init_fields = Vec::new();
let mut current_field_idx = 0;
Expand Down Expand Up @@ -765,7 +770,7 @@ impl<'a> Linearizer<'a> {
init_fields.sort_by_key(|(offset, _, _)| *offset);

Initializer::Struct {
total_size,
total_size: resolved_size,
fields: init_fields,
}
} else {
Expand Down Expand Up @@ -1691,8 +1696,12 @@ impl<'a> Linearizer<'a> {
}
}
TypeKind::Struct | TypeKind::Union => {
// Resolve incomplete struct types to their complete definitions
// This is needed when a typedef to an incomplete struct is used
// before the struct is fully defined (forward declaration pattern)
let resolved_typ = self.resolve_struct_type(typ);
// Get struct fields from the type's composite data
if let Some(composite) = self.types.get(typ).composite.as_ref() {
if let Some(composite) = self.types.get(resolved_typ).composite.as_ref() {
// Clone members to avoid borrow issues
let members: Vec<_> = composite.members.clone();

Expand Down
160 changes: 158 additions & 2 deletions cc/ir/test_linearize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
use super::*;
use crate::parse::ast::{
AssignOp, BlockItem, Declaration, ExprKind, ExternalDecl, FunctionDef, InitDeclarator,
Parameter, UnaryOp,
InitElement, Parameter, UnaryOp,
};
use crate::strings::StringTable;
use crate::types::Type;
use crate::symbol::Symbol;
use crate::types::{CompositeType, StructMember, Type};

/// Create a default position for test code
fn test_pos() -> Position {
Expand Down Expand Up @@ -2533,3 +2534,158 @@ fn test_string_literal_char_pointer_init() {
ir
);
}

// ============================================================================
// Incomplete struct type resolution test (regression test for forward declarations)
// Tests the fix in resolve_struct_type() that resolves incomplete struct types
// to their complete definitions when processing initializers.
// ============================================================================

/// Helper to linearize with a custom symbol table (for testing struct resolution)
fn test_linearize_with_symbols(
tu: &TranslationUnit,
symbols: &SymbolTable,
types: &TypeTable,
strings: &StringTable,
) -> Module {
let target = Target::host();
linearize(tu, symbols, types, strings, &target)
}

#[test]
fn test_incomplete_struct_type_resolution() {
// This test verifies that when a typedef refers to an incomplete struct,
// the linearizer correctly resolves it to the complete struct definition
// when processing struct initializers.
//
// Pattern being tested:
// typedef struct foo foo_t; // incomplete at this point
// struct foo { int x; int y; }; // complete definition
// foo_t f = {1, 2}; // should use complete struct's size (8 bytes), not 0
//
let mut strings = StringTable::new();
let mut types = TypeTable::new(64);
let mut symbols = SymbolTable::new();
let test_id = strings.intern("test");
let foo_tag = strings.intern("foo");
let f_id = strings.intern("f");
let x_id = strings.intern("x");
let y_id = strings.intern("y");
let int_type = types.int_id;

// Create the complete struct type: struct foo { int x; int y; }
let complete_composite = CompositeType {
tag: Some(foo_tag),
members: vec![
StructMember {
name: x_id,
typ: int_type,
offset: 0,
bit_offset: None,
bit_width: None,
storage_unit_size: None,
},
StructMember {
name: y_id,
typ: int_type,
offset: 4, // Second int at offset 4 bytes
bit_offset: None,
bit_width: None,
storage_unit_size: None,
},
],
enum_constants: vec![],
size: 8, // 2 ints = 8 bytes
align: 4, // int alignment
is_complete: true,
};
let complete_struct_type = types.intern(Type::struct_type(complete_composite));

// Register the complete struct in the symbol table as a tag
symbols
.declare(Symbol::tag(foo_tag, complete_struct_type, 0))
.expect("Failed to declare tag");

// Verify the symbol table is correctly set up
let looked_up = symbols.lookup_tag(foo_tag);
assert!(
looked_up.is_some(),
"Symbol table should contain tag for 'foo'"
);
assert_eq!(
looked_up.unwrap().typ,
complete_struct_type,
"Tag should point to complete struct type"
);

// Create an incomplete struct type with the same tag
// This simulates what happens with: typedef struct foo foo_t; (before struct foo is defined)
let incomplete_composite = CompositeType::incomplete(Some(foo_tag));
let incomplete_struct_type = types.intern(Type::struct_type(incomplete_composite));

// Verify the incomplete type has size 0 before resolution
assert_eq!(
types.size_bytes(incomplete_struct_type),
0,
"Incomplete struct should have size 0"
);

// Create an initializer list: {1, 2}
let init_list = Expr::typed_unpositioned(
ExprKind::InitList {
elements: vec![
InitElement {
designators: vec![],
value: Box::new(Expr::int(1, &types)),
},
InitElement {
designators: vec![],
value: Box::new(Expr::int(2, &types)),
},
],
},
incomplete_struct_type,
);

// Create function: void test() { foo_t f = {1, 2}; }
// Using the incomplete struct type for the declaration
let func = FunctionDef {
return_type: types.void_id,
name: test_id,
params: vec![],
body: Stmt::Block(vec![BlockItem::Declaration(Declaration {
declarators: vec![InitDeclarator {
name: f_id,
typ: incomplete_struct_type,
init: Some(init_list),
vla_sizes: vec![],
}],
})]),
pos: test_pos(),
};
let tu = TranslationUnit {
items: vec![ExternalDecl::FunctionDef(func)],
};

// Linearize with the symbol table that has the complete struct registered
let module = test_linearize_with_symbols(&tu, &symbols, &types, &strings);
let ir = format!("{}", module);

// The IR should show stores to the struct fields at proper offsets
// Without the fix, the initializer would have total_size=0 and generate no stores
assert!(
ir.contains("store"),
"Struct initializer should generate store instructions. \
This would fail if incomplete struct type was not resolved. IR:\n{}",
ir
);

// Should have at least 2 stores (one for each field: x and y)
let store_count = ir.matches("store").count();
assert!(
store_count >= 2,
"Should have at least 2 stores for struct fields (x, y), got {}: {}",
store_count,
ir
);
}
Loading