0.5.0 -- tuples and more optimizations #12

Merged
dbidwell merged 34 commits from 43-tuple-return into master 2025-12-31 17:03:51 -07:00
2 changed files with 183 additions and 154 deletions
Showing only changes of commit 3092e97d41 - Show all commits

View File

@@ -947,4 +947,63 @@ mod test {
Ok(()) Ok(())
} }
#[test]
fn test_tuple_with_stack_spillover() -> anyhow::Result<()> {
let compiled = compile!(
debug
r#"
fn get8() {
return (1, 2, 3, 4, 5, 6, 7, 8);
}
let (a, b, c, d, e, f, g, h) = get8();
let sum = a + h;
"#
);
assert_eq!(
compiled,
indoc! {
"
j main
get8:
move r15 sp
push ra
push 1
push 2
push 3
push 4
push 5
push 6
push 7
push 8
move r15 1
sub r0 sp 9
get ra db r0
j ra
main:
jal get8
pop r0
sub r0 sp 0
put db r0 r0
pop r14
pop r13
pop r12
pop r11
pop r10
pop r9
pop r8
move sp r15
sub r0 sp 1
get r1 db r0
add r2 r8 r1
push r2
sub sp sp 2
"
}
);
Ok(())
}
} }

View File

@@ -1104,6 +1104,79 @@ impl<'a> Compiler<'a> {
Ok(()) Ok(())
} }
/// Helper: Validate tuple size from function return
fn validate_tuple_function_size(
&mut self,
func_name: &Cow<'a, str>,
expected_count: usize,
span: Span,
) {
if let Some(&actual_size) = self.function_tuple_return_sizes.get(func_name) {
if actual_size != expected_count {
self.errors
.push(Error::TupleSizeMismatch(actual_size, expected_count, span));
}
}
}
/// Helper: Pop tuple values from stack into variables (for function returns)
/// Variables are popped in reverse order (LIFO)
fn pop_tuple_values(
&mut self,
var_locations: Vec<(Option<VariableLocation>, Span)>,
) -> Result<(), Error<'a>> {
for (var_loc_opt, span) in var_locations.into_iter().rev() {
if let Some(var_location) = var_loc_opt {
match var_location {
VariableLocation::Temporary(reg) | VariableLocation::Persistant(reg) => {
self.write_instruction(
Instruction::Pop(Operand::Register(reg)),
Some(span),
)?;
}
VariableLocation::Stack(offset) => {
// Pop into temp register, then write to stack
self.write_instruction(
Instruction::Pop(Operand::Register(VariableScope::TEMP_STACK_REGISTER)),
Some(span),
)?;
self.write_instruction(
Instruction::Sub(
Operand::Register(0),
Operand::StackPointer,
Operand::Number(offset.into()),
),
Some(span),
)?;
self.write_instruction(
Instruction::Put(
Operand::Device(Cow::from("db")),
Operand::Register(0),
Operand::Register(VariableScope::TEMP_STACK_REGISTER),
),
Some(span),
)?;
}
VariableLocation::Constant(_) => {
return Err(Error::ConstAssignment(Cow::from("tuple element"), span));
}
VariableLocation::Device(_) => {
return Err(Error::DeviceAssignment(Cow::from("tuple element"), span));
}
}
} else {
// Underscore: pop into temp register to discard
self.write_instruction(
Instruction::Pop(Operand::Register(VariableScope::TEMP_STACK_REGISTER)),
Some(span),
)?;
}
}
Ok(())
}
fn expression_tuple_declaration( fn expression_tuple_declaration(
&mut self, &mut self,
tuple_decl: TupleDeclarationExpression<'a>, tuple_decl: TupleDeclarationExpression<'a>,
@@ -1111,74 +1184,37 @@ impl<'a> Compiler<'a> {
) -> Result<(), Error<'a>> { ) -> Result<(), Error<'a>> {
let TupleDeclarationExpression { names, value } = tuple_decl; let TupleDeclarationExpression { names, value } = tuple_decl;
// Compile the right-hand side expression
// For function calls returning tuples:
// r15 = pointer to beginning of tuple on stack
// r14, r13, ... contain the tuple elements, or they're on the stack
match value.node { match value.node {
Expression::Invocation(invoke_expr) => { Expression::Invocation(invoke_expr) => {
// Execute the function call // Execute the function call - tuple values will be on the stack
// Tuple values are on the stack, sp points after the last pushed value
// Pop them in reverse order (from end to beginning)
// We don't need to backup registers for tuple returns
self.expression_function_invocation_with_invocation(&invoke_expr, scope, false)?; self.expression_function_invocation_with_invocation(&invoke_expr, scope, false)?;
// Validate tuple return size matches the declaration // Validate tuple return size matches the declaration
let func_name = &invoke_expr.node.name.node; self.validate_tuple_function_size(
if let Some(&expected_size) = self.function_tuple_return_sizes.get(func_name) { &invoke_expr.node.name.node,
if names.len() != expected_size {
self.errors.push(Error::TupleSizeMismatch(
expected_size,
names.len(), names.len(),
value.span, value.span,
)); );
}
}
// First pass: allocate variables in order // Allocate variables and collect their locations
let mut var_locations = Vec::new(); let var_locations: Vec<_> = names
for name_spanned in names.iter() { .iter()
// Skip underscores .map(|name_spanned| {
if name_spanned.node.as_ref() == "_" { if name_spanned.node.as_ref() == "_" {
var_locations.push(None); Ok((None, name_spanned.span))
continue; } else {
}
// Add variable to scope
let var_location = scope.add_variable( let var_location = scope.add_variable(
name_spanned.node.clone(), name_spanned.node.clone(),
LocationRequest::Persist, LocationRequest::Persist,
Some(name_spanned.span), Some(name_spanned.span),
)?; )?;
var_locations.push(Some(var_location)); Ok((Some(var_location), name_spanned.span))
} }
})
.collect::<Result<_, Error<'a>>>()?;
// Second pass: pop in reverse order through the list (since stack is LIFO) // Pop tuple values from stack into variables
// var_locations[0] is the first element (bottom of stack) self.pop_tuple_values(var_locations)?;
// var_locations[n-1] is the last element (top of stack)
// We pop from the top, so we iterate in reverse through var_locations
for (idx, var_loc_opt) in var_locations.iter().enumerate().rev() {
match var_loc_opt {
Some(var_location) => {
let var_reg = self.resolve_register(&var_location)?;
// Pop from stack into the variable's register
self.write_instruction(
Instruction::Pop(Operand::Register(var_reg)),
Some(names[idx].span),
)?;
}
None => {
// Underscore: pop into temp register to discard
self.write_instruction(
Instruction::Pop(Operand::Register(
VariableScope::TEMP_STACK_REGISTER,
)),
Some(names[idx].span),
)?;
}
}
}
// Restore stack pointer to value saved at function entry // Restore stack pointer to value saved at function entry
self.write_instruction( self.write_instruction(
@@ -1245,107 +1281,41 @@ impl<'a> Compiler<'a> {
) -> Result<(), Error<'a>> { ) -> Result<(), Error<'a>> {
let TupleAssignmentExpression { names, value } = tuple_assign; let TupleAssignmentExpression { names, value } = tuple_assign;
// Similar to tuple declaration, but variables must already exist
match value.node { match value.node {
Expression::Invocation(invoke_expr) => { Expression::Invocation(invoke_expr) => {
// Execute the function call // Execute the function call - tuple values will be on the stack
// Tuple values are on the stack, sp points after the last pushed value
// Pop them in reverse order (from end to beginning)
// We don't need to backup registers for tuple returns
self.expression_function_invocation_with_invocation(&invoke_expr, scope, false)?; self.expression_function_invocation_with_invocation(&invoke_expr, scope, false)?;
// Validate tuple return size matches the assignment // Validate tuple return size matches the assignment
let func_name = &invoke_expr.node.name.node; self.validate_tuple_function_size(
if let Some(&expected_size) = self.function_tuple_return_sizes.get(func_name) { &invoke_expr.node.name.node,
if names.len() != expected_size {
self.errors.push(Error::TupleSizeMismatch(
expected_size,
names.len(), names.len(),
value.span, value.span,
)); );
}
}
// First pass: look up variable locations // Look up existing variable locations
let mut var_locs = Vec::new(); let var_locations: Vec<_> = names
for name_spanned in names.iter() { .iter()
// Skip underscores .map(|name_spanned| {
if name_spanned.node.as_ref() == "_" { if name_spanned.node.as_ref() == "_" {
var_locs.push(None); Ok((None, name_spanned.span))
continue; } else {
} let var_location = scope
.get_location_of(&name_spanned.node, Some(name_spanned.span))
// Get the existing variable location .unwrap_or_else(|_| {
let var_location =
match scope.get_location_of(&name_spanned.node, Some(name_spanned.span)) {
Ok(l) => l,
Err(_) => {
self.errors.push(Error::UnknownIdentifier( self.errors.push(Error::UnknownIdentifier(
name_spanned.node.clone(), name_spanned.node.clone(),
name_spanned.span, name_spanned.span,
)); ));
VariableLocation::Temporary(0) VariableLocation::Temporary(0)
});
Ok((Some(var_location), name_spanned.span))
} }
}; })
var_locs.push(Some(var_location)); .collect::<Result<_, Error<'a>>>()?;
}
// Second pass: pop in reverse order and assign // Pop tuple values from stack into variables
for (idx, var_loc_opt) in var_locs.iter().enumerate().rev() { self.pop_tuple_values(var_locations)?;
if let Some(var_location) = var_loc_opt {
// Pop from stack and assign to variable
match var_location {
VariableLocation::Temporary(reg)
| VariableLocation::Persistant(reg) => {
// Pop directly into the variable's register
self.write_instruction(
Instruction::Pop(Operand::Register(*reg)),
Some(names[idx].span),
)?;
}
VariableLocation::Stack(offset) => {
// Pop into temp register, then write to variable stack
self.write_instruction(
Instruction::Pop(Operand::Register(
VariableScope::TEMP_STACK_REGISTER,
)),
Some(names[idx].span),
)?;
// Write to variable stack location
self.write_instruction(
Instruction::Sub(
Operand::Register(0),
Operand::StackPointer,
Operand::Number((*offset).into()),
),
Some(names[idx].span),
)?;
self.write_instruction(
Instruction::Put(
Operand::Device(Cow::from("db")),
Operand::Register(0),
Operand::Register(VariableScope::TEMP_STACK_REGISTER),
),
Some(names[idx].span),
)?;
}
VariableLocation::Constant(_) => {
return Err(Error::ConstAssignment(
names[idx].node.clone(),
names[idx].span,
));
}
VariableLocation::Device(_) => {
return Err(Error::DeviceAssignment(
names[idx].node.clone(),
names[idx].span,
));
}
}
}
}
// Restore stack pointer to value saved at function entry // Restore stack pointer to value saved at function entry
self.write_instruction( self.write_instruction(