From 8be86d47abff310740abcc162884da58902e3c78 Mon Sep 17 00:00:00 2001 From: Marco Doehler Date: Sat, 14 Mar 2026 21:06:13 +0000 Subject: [PATCH 1/3] Implement proper LLVM IR for struct/variant/fn types and element-size-aware load/store Replace fallback i64 representations with proper LLVM types: - Struct types map to LLVM struct types with correct field layouts - Variant types map to tagged unions (i32 tag + largest payload) - Fn types map to i8* function pointers - Load/Store operations now compute element sizes from M-node type info - type_ref_byte_size updated to handle struct (sum of fields) and variant (tag + max payload) Co-Authored-By: Claude Opus 4.6 --- flux-ftl/src/codegen.rs | 91 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 6 deletions(-) diff --git a/flux-ftl/src/codegen.rs b/flux-ftl/src/codegen.rs index a86f4a2..fa61d40 100644 --- a/flux-ftl/src/codegen.rs +++ b/flux-ftl/src/codegen.rs @@ -557,9 +557,53 @@ impl<'ctx, 'prog> CodeGenerator<'ctx, 'prog> { .into(), ) } - TypeBody::Struct { .. } | TypeBody::Variant { .. } | TypeBody::Fn { .. } => { - // For now, represent complex types as i64 - Some(self.context.i64_type().into()) + TypeBody::Struct { fields, .. } => { + let field_types: Vec> = fields + .iter() + .filter_map(|f| self.type_ref_to_llvm(&f.type_ref)) + .collect(); + if field_types.is_empty() { + None // unit-like struct + } else { + Some(self.context.struct_type(&field_types, false).into()) + } + } + TypeBody::Variant { cases } => { + let tag_type = self.context.i32_type(); + let max_payload = cases + .iter() + .filter_map(|c| self.type_ref_to_llvm(&c.payload)) + .max_by_key(|t| match t { + inkwell::types::BasicTypeEnum::IntType(it) => { + it.get_bit_width() as u64 / 8 + } + inkwell::types::BasicTypeEnum::FloatType(ft) => { + if ft.print_to_string().to_string().contains("double") { + 8 + } else { + 4 + } + } + inkwell::types::BasicTypeEnum::PointerType(_) => 8, + _ => 8, + }); + match max_payload { + Some(payload_ty) => Some( + self.context + .struct_type(&[tag_type.into(), payload_ty], false) + .into(), + ), + None => Some(tag_type.into()), // enum with no payloads + } + } + TypeBody::Fn { .. } => { + // Function pointers are represented as i8* + Some( + self.context + .i8_type() + .ptr_type(AddressSpace::default()) + .into(), + ) } } } @@ -597,7 +641,18 @@ impl<'ctx, 'prog> CodeGenerator<'ctx, 'prog> { TypeBody::Array { max_length, .. } => { *max_length as u64 * 8 } - _ => 8, + TypeBody::Struct { fields, .. } => fields + .iter() + .map(|f| self.type_ref_byte_size(&f.type_ref)) + .sum(), + TypeBody::Variant { cases } => { + 4 + cases + .iter() + .map(|c| self.type_ref_byte_size(&c.payload)) + .max() + .unwrap_or(0) + } + TypeBody::Fn { .. } => 8, // pointer size }, None => 8, } @@ -605,6 +660,26 @@ impl<'ctx, 'prog> CodeGenerator<'ctx, 'prog> { } } + /// Get element size in bytes for a memory node. For array types, returns + /// the element type's size; otherwise returns the full type's size. + fn get_memory_element_size(&self, mem_node_id: &str) -> u64 { + for mem in &self.program.memories { + if mem.id.0 == mem_node_id + && let MemoryOp::Alloc { type_ref, .. } = &mem.op + { + if let TypeRef::Id { node } = type_ref + && let Some(td) = + self.program.types.iter().find(|t| t.id.0 == node.0) + && let TypeBody::Array { element, .. } = &td.body + { + return self.type_ref_byte_size(element); + } + return self.type_ref_byte_size(type_ref); + } + } + 8 // fallback + } + // ------------------------------------------------------------------ // Emit global constants (ConstBytes) // ------------------------------------------------------------------ @@ -751,7 +826,9 @@ impl<'ctx, 'prog> CodeGenerator<'ctx, 'prog> { .map_err(|e| CodegenError::EmitFailed(e.to_string()))? .into_pointer_value(); - let element_size = self.context.i64_type().const_int(8, false); + let element_size_bytes = self.get_memory_element_size(&target.0); + let element_size = + self.context.i64_type().const_int(element_size_bytes, false); let idx_i64 = self.int_to_i64(idx_val.into_int_value(), builder)?; let byte_offset = builder .build_int_mul(idx_i64, element_size, "byte_offset") @@ -801,7 +878,9 @@ impl<'ctx, 'prog> CodeGenerator<'ctx, 'prog> { .map_err(|e| CodegenError::EmitFailed(e.to_string()))? .into_pointer_value(); - let element_size = self.context.i64_type().const_int(8, false); + let element_size_bytes = self.get_memory_element_size(&source.0); + let element_size = + self.context.i64_type().const_int(element_size_bytes, false); let idx_i64 = self.int_to_i64(idx_val.into_int_value(), builder)?; let byte_offset = builder .build_int_mul(idx_i64, element_size, "byte_offset") From 9119d8102d65ebc8f53c27699a372d6f72a538b6 Mon Sep 17 00:00:00 2001 From: Marco Doehler Date: Sat, 14 Mar 2026 21:11:43 +0000 Subject: [PATCH 2/3] Add generic compute extensions (abs, min, max, clamp, bhaskara_approx) and scoped region cleanup Extend emit_generic_compute with five new operations: abs (absolute value), min/max (signed integer comparison), clamp (three-input bounded range), and bhaskara_approx (fixed-point Bhaskara sine approximation for graphics/games). Add emit_scoped_region_cleanup to zero-initialize scoped region memory at the start of each loop iteration, ensuring frame buffers are reset between cycles. Co-Authored-By: Claude Opus 4.6 --- flux-ftl/src/codegen.rs | 274 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) diff --git a/flux-ftl/src/codegen.rs b/flux-ftl/src/codegen.rs index fa61d40..b318515 100644 --- a/flux-ftl/src/codegen.rs +++ b/flux-ftl/src/codegen.rs @@ -1080,6 +1080,8 @@ impl<'ctx, 'prog> CodeGenerator<'ctx, 'prog> { // Body builder.position_at_end(body_bb); + // Zero-initialize scoped region memory at loop iteration start + self.emit_scoped_region_cleanup(builder)?; self.emit_control_node(&body.0, function, builder)?; if builder .get_insert_block() @@ -1099,6 +1101,56 @@ impl<'ctx, 'prog> CodeGenerator<'ctx, 'prog> { Ok(()) } + // ------------------------------------------------------------------ + // Scoped region cleanup (zero-init at loop iteration boundaries) + // ------------------------------------------------------------------ + + fn emit_scoped_region_cleanup( + &self, + builder: &inkwell::builder::Builder<'ctx>, + ) -> Result<(), CodegenError> { + // Find all scoped regions + let scoped_region_ids: Vec = self + .program + .regions + .iter() + .filter(|r| matches!(r.lifetime, crate::ast::Lifetime::Scoped)) + .map(|r| r.id.0.clone()) + .collect(); + + if scoped_region_ids.is_empty() { + return Ok(()); + } + + // For each memory allocation in a scoped region, zero out its memory + for mem in &self.program.memories { + if let MemoryOp::Alloc { + region, type_ref, .. + } = &mem.op + && scoped_region_ids.contains(®ion.0) + && let Some(ptr) = self.pointers.get(&mem.id.0) + { + let size = self.type_ref_byte_size(type_ref); + let i8_ptr = builder + .build_bitcast( + *ptr, + self.context.i8_type().ptr_type(AddressSpace::default()), + "cleanup_ptr", + ) + .map_err(|e| CodegenError::EmitFailed(e.to_string()))? + .into_pointer_value(); + + let size_val = self.context.i64_type().const_int(size, false); + let zero = self.context.i8_type().const_int(0, false); + + builder + .build_memset(i8_ptr, 1, zero, size_val) + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + } + } + Ok(()) + } + // ------------------------------------------------------------------ // Compute node side-effect emission (for Seq steps referencing C:) // ------------------------------------------------------------------ @@ -2059,6 +2111,228 @@ impl<'ctx, 'prog> CodeGenerator<'ctx, 'prog> { .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; Ok(BasicValueEnum::IntValue(result)) } + "abs" => { + if inputs.is_empty() { + return Err(CodegenError::Unsupported( + "abs expects 1 input".to_string(), + )); + } + let val = self.resolve_value(&inputs[0].0, function, builder)?; + let int_val = val.into_int_value(); + let zero = int_val.get_type().const_int(0, false); + let is_neg = builder + .build_int_compare(inkwell::IntPredicate::SLT, int_val, zero, "is_neg") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + let negated = builder + .build_int_neg(int_val, "negated") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + let result = builder + .build_select( + is_neg, + BasicValueEnum::IntValue(negated), + BasicValueEnum::IntValue(int_val), + "abs_result", + ) + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + Ok(result) + } + "min" => { + if inputs.len() < 2 { + return Err(CodegenError::Unsupported( + "min expects 2 inputs".to_string(), + )); + } + let a = self + .resolve_value(&inputs[0].0, function, builder)? + .into_int_value(); + let b = self + .resolve_value(&inputs[1].0, function, builder)? + .into_int_value(); + let (a, b) = self.normalize_int_widths(a, b, builder)?; + let cmp = builder + .build_int_compare(inkwell::IntPredicate::SLT, a, b, "min_cmp") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + let result = builder + .build_select( + cmp, + BasicValueEnum::IntValue(a), + BasicValueEnum::IntValue(b), + "min_result", + ) + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + Ok(result) + } + "max" => { + if inputs.len() < 2 { + return Err(CodegenError::Unsupported( + "max expects 2 inputs".to_string(), + )); + } + let a = self + .resolve_value(&inputs[0].0, function, builder)? + .into_int_value(); + let b = self + .resolve_value(&inputs[1].0, function, builder)? + .into_int_value(); + let (a, b) = self.normalize_int_widths(a, b, builder)?; + let cmp = builder + .build_int_compare(inkwell::IntPredicate::SGT, a, b, "max_cmp") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + let result = builder + .build_select( + cmp, + BasicValueEnum::IntValue(a), + BasicValueEnum::IntValue(b), + "max_result", + ) + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + Ok(result) + } + "clamp" => { + if inputs.len() < 3 { + return Err(CodegenError::Unsupported( + "clamp expects 3 inputs (val, min, max)".to_string(), + )); + } + let val = self + .resolve_value(&inputs[0].0, function, builder)? + .into_int_value(); + let lo = self + .resolve_value(&inputs[1].0, function, builder)? + .into_int_value(); + let hi = self + .resolve_value(&inputs[2].0, function, builder)? + .into_int_value(); + // clamp = max(lo, min(val, hi)) + let (val_n, hi_n) = self.normalize_int_widths(val, hi, builder)?; + let cmp_hi = builder + .build_int_compare(inkwell::IntPredicate::SLT, val_n, hi_n, "cmp_hi") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + let min_val = builder + .build_select( + cmp_hi, + BasicValueEnum::IntValue(val_n), + BasicValueEnum::IntValue(hi_n), + "min_val", + ) + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + let (min_n, lo_n) = + self.normalize_int_widths(min_val.into_int_value(), lo, builder)?; + let cmp_lo = builder + .build_int_compare(inkwell::IntPredicate::SGT, min_n, lo_n, "cmp_lo") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + let result = builder + .build_select( + cmp_lo, + BasicValueEnum::IntValue(min_n), + BasicValueEnum::IntValue(lo_n), + "clamp_result", + ) + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + Ok(result) + } + "bhaskara_approx" => { + // Bhaskara sine approximation: 16x(pi-x) / (5pi^2 - 4x(pi-x)) + // Input: one i32 value representing angle * 1000 (fixed-point) + // Output: i32 result * 1000 + if inputs.is_empty() { + return Err(CodegenError::Unsupported( + "bhaskara_approx expects 1 input".to_string(), + )); + } + let val = self.resolve_value(&inputs[0].0, function, builder)?; + let int_val = val.into_int_value(); + + // Constants for fixed-point Bhaskara approximation + let i64_ty = self.context.i64_type(); + let pi_1000 = i64_ty.const_int(3142, false); // pi * 1000 + let sixteen = i64_ty.const_int(16, false); + let five = i64_ty.const_int(5, false); + let four = i64_ty.const_int(4, false); + let thousand = i64_ty.const_int(1000, false); + + // Widen input to i64 + let x = builder + .build_int_s_extend(int_val, i64_ty, "x_ext") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + + // pi_minus_x = pi*1000 - x + let pi_minus_x = builder + .build_int_sub(pi_1000, x, "pi_minus_x") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + + // numerator = 16 * x * pi_minus_x + let num1 = builder + .build_int_mul(sixteen, x, "num1") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + let numerator = builder + .build_int_mul(num1, pi_minus_x, "numerator") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + + // denom_part1 = 5 * pi^2 = 5 * pi_1000 * pi_1000 / 1000 + let pi_sq = builder + .build_int_mul(pi_1000, pi_1000, "pi_sq") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + let five_pi_sq = builder + .build_int_mul(five, pi_sq, "five_pi_sq") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + let denom1 = builder + .build_int_signed_div(five_pi_sq, thousand, "denom1") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + + // denom_part2 = 4 * x * pi_minus_x / 1000 + let four_x = builder + .build_int_mul(four, x, "four_x") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + let denom2_raw = builder + .build_int_mul(four_x, pi_minus_x, "denom2_raw") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + let denom2 = builder + .build_int_signed_div(denom2_raw, thousand, "denom2") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + + // denominator = denom1 - denom2 + let denominator = builder + .build_int_sub(denom1, denom2, "denominator") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + + // result = numerator / denominator (with division-by-zero guard) + let zero = i64_ty.const_int(0, false); + let is_zero = builder + .build_int_compare( + inkwell::IntPredicate::EQ, + denominator, + zero, + "denom_is_zero", + ) + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + let safe_denom = builder + .build_select( + is_zero, + BasicValueEnum::IntValue(i64_ty.const_int(1, false)), + BasicValueEnum::IntValue(denominator), + "safe_denom", + ) + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + let result = builder + .build_int_signed_div( + numerator, + safe_denom.into_int_value(), + "bhaskara_result", + ) + .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + + // Truncate back to original width if needed + let result_truncated = if int_val.get_type().get_bit_width() < 64 { + builder + .build_int_truncate(result, int_val.get_type(), "bhaskara_trunc") + .map_err(|e| CodegenError::EmitFailed(e.to_string()))? + } else { + result + }; + + Ok(BasicValueEnum::IntValue(result_truncated)) + } _ => { // Unknown generic compute: return a zero constant Ok(self.context.i64_type().const_int(0, false).into()) From 0cbc7b6d2234a80fa5c3eba60be373bbb38e6de8 Mon Sep 17 00:00:00 2001 From: Marco Doehler Date: Sat, 14 Mar 2026 21:18:44 +0000 Subject: [PATCH 3/3] Fix QA issues in codegen extensions: array sizing, region cleanup, variant sizing, bhaskara div-by-zero - Array byte size now uses element type size instead of hardcoded *8 - Scoped region cleanup only zeros innermost (frame) regions, not game state - Variant max-payload uses type_ref_byte_size for accurate sizing - bhaskara_approx returns 0 on division-by-zero instead of numerator/1 Co-Authored-By: Claude Opus 4.6 --- flux-ftl/src/codegen.rs | 87 +++++++++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 29 deletions(-) diff --git a/flux-ftl/src/codegen.rs b/flux-ftl/src/codegen.rs index b318515..6887e5f 100644 --- a/flux-ftl/src/codegen.rs +++ b/flux-ftl/src/codegen.rs @@ -570,29 +570,20 @@ impl<'ctx, 'prog> CodeGenerator<'ctx, 'prog> { } TypeBody::Variant { cases } => { let tag_type = self.context.i32_type(); - let max_payload = cases + // Find the case with the largest payload using type_ref_byte_size + let max_case = cases .iter() - .filter_map(|c| self.type_ref_to_llvm(&c.payload)) - .max_by_key(|t| match t { - inkwell::types::BasicTypeEnum::IntType(it) => { - it.get_bit_width() as u64 / 8 - } - inkwell::types::BasicTypeEnum::FloatType(ft) => { - if ft.print_to_string().to_string().contains("double") { - 8 - } else { - 4 - } - } - inkwell::types::BasicTypeEnum::PointerType(_) => 8, - _ => 8, - }); - match max_payload { - Some(payload_ty) => Some( - self.context - .struct_type(&[tag_type.into(), payload_ty], false) - .into(), - ), + .filter(|c| self.type_ref_to_llvm(&c.payload).is_some()) + .max_by_key(|c| self.type_ref_byte_size(&c.payload)); + match max_case { + Some(c) => { + let payload_ty = self.type_ref_to_llvm(&c.payload).unwrap(); + Some( + self.context + .struct_type(&[tag_type.into(), payload_ty], false) + .into(), + ) + } None => Some(tag_type.into()), // enum with no payloads } } @@ -638,9 +629,11 @@ impl<'ctx, 'prog> CodeGenerator<'ctx, 'prog> { TypeBody::Boolean => 1, TypeBody::Unit => 0, TypeBody::Opaque { size, .. } => *size as u64, - TypeBody::Array { max_length, .. } => { - *max_length as u64 * 8 - } + TypeBody::Array { + element, + max_length, + .. + } => *max_length as u64 * self.type_ref_byte_size(element), TypeBody::Struct { fields, .. } => fields .iter() .map(|f| self.type_ref_byte_size(&f.type_ref)) @@ -1109,12 +1102,38 @@ impl<'ctx, 'prog> CodeGenerator<'ctx, 'prog> { &self, builder: &inkwell::builder::Builder<'ctx>, ) -> Result<(), CodegenError> { - // Find all scoped regions - let scoped_region_ids: Vec = self + // Only zero frame-scoped regions (innermost scoped regions whose parent + // is another scoped or game region). We identify these as scoped regions + // that have a parent which is also a scoped region. If no such nesting + // exists, pick scoped regions that are NOT the outermost scoped region. + let scoped_regions: Vec<&crate::ast::RegionDef> = self .program .regions .iter() .filter(|r| matches!(r.lifetime, crate::ast::Lifetime::Scoped)) + .collect(); + + // Find innermost scoped regions: those whose parent is also scoped + let outer_scoped_ids: std::collections::HashSet = scoped_regions + .iter() + .filter(|r| { + if let Some(parent) = &r.parent { + // If parent is static, this is an outer scoped region + self.program + .regions + .iter() + .any(|p| p.id == *parent && matches!(p.lifetime, crate::ast::Lifetime::Static)) + } else { + true + } + }) + .map(|r| r.id.0.clone()) + .collect(); + + // Inner scoped regions = scoped but NOT outer + let scoped_region_ids: Vec = scoped_regions + .iter() + .filter(|r| !outer_scoped_ids.contains(&r.id.0)) .map(|r| r.id.0.clone()) .collect(); @@ -2306,6 +2325,7 @@ impl<'ctx, 'prog> CodeGenerator<'ctx, 'prog> { "denom_is_zero", ) .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + // If denominator is zero, return 0 (degenerate sine input) let safe_denom = builder .build_select( is_zero, @@ -2314,13 +2334,22 @@ impl<'ctx, 'prog> CodeGenerator<'ctx, 'prog> { "safe_denom", ) .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; - let result = builder + let div_result = builder .build_int_signed_div( numerator, safe_denom.into_int_value(), - "bhaskara_result", + "bhaskara_div", ) .map_err(|e| CodegenError::EmitFailed(e.to_string()))?; + let result = builder + .build_select( + is_zero, + BasicValueEnum::IntValue(i64_ty.const_int(0, false)), + BasicValueEnum::IntValue(div_result), + "bhaskara_result", + ) + .map_err(|e| CodegenError::EmitFailed(e.to_string()))? + .into_int_value(); // Truncate back to original width if needed let result_truncated = if int_val.get_type().get_bit_width() < 64 {