diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 9e2c1fa..eeec9e2 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -35,7 +35,7 @@ jobs: - name: Run Claude Code Review id: claude-review - uses: anthropics/claude-code-action@beta + uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} @@ -43,7 +43,7 @@ jobs: # model: "claude-opus-4-20250514" # Direct prompt for automated review (no @claude mention needed) - direct_prompt: | + prompt: | Please review this pull request and provide feedback on: - Code quality and best practices - Potential bugs or issues diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 3aa5a2b..3cef8c9 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -29,10 +29,11 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 1 + submodules: recursive - name: Run Claude Code id: claude - uses: anthropics/claude-code-action@beta + uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/CLAUDE.md b/CLAUDE.md index 54279ce..4aa6145 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,7 +80,6 @@ cargo run -- file.c -c # Emit object file only cargo run -- file.c -o binary # Custom output name cargo run -- file.c --static # Static linking (Linux only) cargo run -- file.c --external-linker # Use system ld instead of libwild -cargo run -- file.c --no-iced # Use deprecated text-based assembler ``` ## Compiler Architecture @@ -110,9 +109,7 @@ Returns `(NameGenerator, SymbolTable)` needed by subsequent passes. Exit code 30 **Codegen** (`codegen.rs`): Lowers TACKY to x86-64 assembly AST. Assigns pseudo-registers to stack slots, fixes invalid instruction operands (x86 restrictions), implements System V AMD64 calling convention (arguments in RDI, RSI, RDX, RCX, R8, R9, then stack). -**Emitter** (`emit_iced.rs`): Primary emitter using [iced-x86](https://github.com/icedland/iced) to encode instructions to machine code and [object](https://github.com/gimli-rs/object) crate to write ELF (Linux) or Mach-O (macOS) object files. No external assembler needed. - -Alternative: `emit.rs` (deprecated `--no-iced` flag) generates text assembly for `as`. +**Emitter** (`emit_iced.rs`): Encodes instructions to machine code using [iced-x86](https://github.com/icedland/iced) and writes ELF (Linux) or Mach-O (macOS) object files via the [object](https://github.com/gimli-rs/object) crate. No external assembler needed. **Linker** (`main.rs`): On Linux, uses [wild](https://github.com/wild-linker/wild) for in-process linking. On macOS, shells out to system `ld`. Locates CRT files and libc via the `cc` compiler. @@ -151,10 +148,12 @@ Tests validate both successful compilation and error handling: ## Language Implementation Notes -**Type System**: Currently supports `int`/`unsigned int` (32-bit) and `long`/`unsigned long` (64-bit), with the usual arithmetic conversions. Narrowing truncates (two's complement); widening sign-extends signed sources and zero-extends unsigned ones; same-width signed/unsigned conversions reinterpret the bits. +**Type System**: Currently supports `int`/`unsigned int` (32-bit), `long`/`unsigned long` (64-bit), and `double` (64-bit IEEE-754), with the usual arithmetic conversions. Narrowing truncates (two's complement); widening sign-extends signed sources and zero-extends unsigned ones; same-width signed/unsigned conversions reinterpret the bits. **Integer Arithmetic**: Signed overflow wraps deterministically (non-standard C extension; standard C makes it UB); unsigned wraps mod 2^N (standard). Shift amounts are masked to prevent undefined behavior. The `-Woverflow` warning fires only for signed overflow. +**Floating Point**: `double` arithmetic/comparisons use SSE2 (`addsd`/`comisd`/etc.); constants live in a `.rodata` pool. Comparisons follow IEEE-754 ordering — a `NaN` operand is unordered (relationals and `==` are false, `!=` true, `NaN` is truthy in conditions), implemented via the parity flag. `double`→integer truncates toward zero; out-of-range or `NaN` yields the x86 `cvttsd2si` "integer indefinite" value (target MIN). Unsigned↔`double` conversions use SSE2 workarounds (no native unsigned convert pre-AVX-512). An out-of-range floating constant rounds to ±infinity or zero (`-Woverflow`). + **Evaluation Order**: Left-to-right (non-standard, eliminates UB). **Constant Expressions**: Evaluated at compile time using the same rules as runtime expressions. Used for static initializers and case labels. diff --git a/README.md b/README.md index 6117c6b..4c7767c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A (**N**ot **C**ompletely) **C** compiler written in Rust, inspired by Sandler's NCC is a full pipeline compiler, going from lexing all the way down to x86-64 machine code emission and linking. Machine code is encoded directly using [iced-x86](https://github.com/icedland/iced) and emitted to ELF/Mach-O object files via the [object](https://github.com/gimli-rs/object) crate—no external assembler required. -A substantial subset of C is supported, including `int`, `long`, `unsigned int`, and `unsigned long` types, functions, static variables, all control +A substantial subset of C is supported, including `int`, `long`, `unsigned int`, `unsigned long`, and `double` types, functions, static variables, all control flow statements, and bitwise operations. Additionally, NCC supports developer-friendly warnings and pretty-printing of each compiler pass. Runs on Linux and macOS. @@ -145,7 +145,8 @@ ncc [OPTIONS] ... ### Arguments -`...` Input files (required). Supports multiple C and assembly files. +`...` Input files (required). Supports multiple C (`.c`), assembly (`.s`), and +pre-built object (`.o`) files; objects are passed straight through to the linker. ### Options @@ -161,7 +162,7 @@ ncc [OPTIONS] ... | `-c` | Emit object file only (no linking) | | `--external-linker` | Use system linker (`ld`) instead of built-in libwild | | `--static` | Link statically (no runtime dependencies) - Linux only | -| `--no-iced` | Use text-based asm building instead of iced (deprecated) | +| `-l ` | Link against a library, e.g. `-lm` (forwarded to linker) | | `-o`, `--output ` | Override output file location | | `-h`, `--help` | Print help | @@ -213,7 +214,7 @@ The compiler currently implements a subset of C with the following grammar: ::= { }+ [ "=" ] ";" ::= { }+ "(" ")" ( | ";" ) ::= "void" | { "," } - ::= { "int" | "long" | "signed" | "unsigned" }+ + ::= { "int" | "long" | "signed" | "unsigned" }+ | "double" ::= | "static" | "extern" ::= "{" { } "}" ::= | @@ -235,7 +236,7 @@ The compiler currently implements a subset of C with the following grammar: | ";" ::= | | | "?" ":" | "++" | "--" - ::= | | | | | | "++" | "--" + ::= | | | | | | | "++" | "--" | "(" ")" | "(" ")" | "(" [ ] ")" ::= { "," } @@ -248,8 +249,22 @@ The compiler currently implements a subset of C with the following grammar: ::= ? A long integer constant token (suffix 'l' or 'L') ? ::= ? An unsigned int constant token (suffix 'u' or 'U') ? ::= ? An unsigned long constant token (suffix combining 'u'/'U' and 'l'/'L') ? + ::= ? A floating-point constant token (decimal point and/or exponent) ? ``` +### Data Types + +| Type | Size | Representation | Notes | +|-----------------|--------|--------------------------|-----------------------------------| +| `int` | 32-bit | two's complement signed | | +| `unsigned int` | 32-bit | unsigned | wraps mod 2³² | +| `long` | 64-bit | two's complement signed | LP64 — 64-bit, per System V AMD64 | +| `unsigned long` | 64-bit | unsigned | LP64; wraps mod 2⁶⁴ | +| `double` | 64-bit | IEEE-754 binary64 | | + +Not yet supported: `char`, `short`, `float`, pointers, arrays, structs. See [Safer C](#safer-c) for +arithmetic, conversion, and overflow semantics. + ### Supported Features The compiler supports: @@ -263,8 +278,9 @@ The compiler supports: functions - **Compound statements (blocks)**: `{ ... }` with proper scoping - **Variable scoping**: Block-local variables with shadowing support -- **Type system**: `int`/`unsigned int` (32-bit) and `long`/`unsigned long` (64-bit), with the usual arithmetic conversions, implicit conversions, and explicit casts +- **Type system**: the integer and floating-point types above (see [Data Types](#data-types)), with the usual arithmetic conversions, implicit conversions, and explicit casts - **Integer arithmetic**: addition, subtraction, multiplication, division, modulo +- **Floating-point arithmetic**: `double` addition, subtraction, multiplication, division, negation, and comparisons (SSE2), with conversions to and from every integer type; comparisons follow IEEE-754 ordering, so a `NaN` operand compares unordered (every relational and `==` is false, `!=` is true, and `NaN` is truthy in a condition) - **Bitwise operations**: AND (`&`), OR (`|`), XOR (`^`), complement (`~`), left/right shift (`<<`, `>>`) - **Logical operations**: AND (`&&`), OR (`||`), NOT (`!`) with short-circuit evaluation - **Comparison operators**: `==`, `!=`, `<`, `>`, `<=`, `>=` @@ -302,8 +318,17 @@ NCC provides several safety features and guarantees to help developers write mor - **Shift masking**: Left and right shifts mask the shift amount to prevent undefined behavior. For `int` types, the shift amount is masked with `& 31` (modulo 32); for `long` types, masked with `& 63` (modulo 64). For example, `1 << 32` evaluates to `1 << 0 = 1`, matching x86 hardware behavior. -- **Consistent compile-time and runtime behavior**: Constant expressions (static initializers, case labels) follow - the same arithmetic and type conversion rules as runtime expressions, ensuring predictable behavior. +- **Deterministic floating-point edges**: Cases C leaves undefined are given defined results. A floating-point + constant too large for `double` rounds to ±infinity and one too small rounds to zero (both flagged by + `-Woverflow`), where C §6.4.4.2 leaves an out-of-range floating constant undefined. Converting a `double` to an + integer truncates toward zero; out-of-range or NaN values produce the x86 `cvttsd2si` "integer indefinite" result + (the target type's minimum, e.g. `INT_MIN`), where C §6.3.1.4 leaves the conversion undefined. +- **Consistent compile-time and runtime behavior**: Constant expressions (static initializers, case labels) are + folded with the *same* arithmetic and type-conversion rules as runtime expressions — including all of the + deterministic resolutions above. Where standard C would make an overflowing or out-of-range constant expression a + constraint violation (a required diagnostic), NCC instead folds it to the value equivalent runtime code would + produce: `static int x = 2147483647 + 1;` wraps to `INT_MIN`, and a constant `double`→`int` cast yields the same + `cvttsd2si` result the runtime conversion would. #### Compile-Time Warnings diff --git a/src/codegen.rs b/src/codegen.rs index 9e38532..80f4a1f 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -13,8 +13,9 @@ //! slots (negative RBP offsets) to locals, and RIP-relative [`Operand::Data`] //! references to static/extern variables //! 3. **Instruction fix-up** ([`fix_invalid`]) — rewrites operand combinations that -//! violate x86-64 encoding rules (e.g. memory-to-memory moves) using scratch -//! registers R10/R11/CX, and inserts the stack allocation prologue +//! violate x86-64 encoding rules (e.g. memory-to-memory moves) using GP scratch +//! registers R10/R11/CX (and XMM14/XMM15 for SSE/`double` cases), and inserts the +//! stack allocation prologue //! 4. **Label coalescing** ([`coalesce_labels`]) — merges consecutive labels to reduce //! redundant jump targets //! @@ -22,12 +23,14 @@ //! //! - Translates all TACKY operations to concrete x86-64 instructions //! - Implements the System V AMD64 calling convention: -//! - Arguments 1-6 in RDI, RSI, RDX, RCX, R8, R9; remainder on stack +//! - Integer args in RDI, RSI, RDX, RCX, R8, R9 (up to 6); `double` args in XMM0–XMM7 (up to 8); +//! remainder on the stack //! - 16-byte stack alignment before `call` -//! - Return value in RAX +//! - Return value in RAX (integer) or XMM0 (`double`) //! - Allocates stack frames: locals grow downward from RBP, 16-byte aligned -//! - Produces a [`Program`] of [`FunctionDefinition`]s and [`StaticVariable`]s -//! - Produces a [`BackendSymbolTable`] mapping names to their types (size + signedness) +//! - Produces a [`Program`] of [`FunctionDefinition`]s, [`StaticVariable`]s, and +//! [`StaticConstant`]s (the `.rodata` `double` pool) +//! - Produces a [`BackendSymbolTable`] mapping names to their types (size, signedness, `double`) //! //! ## Call Order //! @@ -62,8 +65,9 @@ use crate::parser; use crate::parser::{Const, Identifier, Type}; use crate::tacky; use crate::tacky::{BinOp, StaticVariable as TackyStaticVariable, Val, VarInit}; -use crate::validate::SymbolTable; +use crate::validate::{NameGenerator, StaticInit, SymbolTable}; use std::collections::{HashMap, HashSet}; +use std::mem; use std::rc::Rc; #[derive(Clone, Copy, Debug, PartialEq)] @@ -78,19 +82,30 @@ pub enum Reg { R10, R11, SP, + XMM0, + XMM1, + XMM2, + XMM3, + XMM4, + XMM5, + XMM6, + XMM7, + XMM14, + XMM15, } #[derive(Clone, Copy, Debug, PartialEq)] pub enum AssemblyType { Longword, Quadword, + Double, } impl AssemblyType { pub fn size(&self) -> u64 { match self { AssemblyType::Longword => 4, - AssemblyType::Quadword => 8, + AssemblyType::Quadword | AssemblyType::Double => 8, } } } @@ -100,6 +115,7 @@ impl From<&Type> for AssemblyType { match ty { Type::Int | Type::UInt => AssemblyType::Longword, Type::Long | Type::ULong => AssemblyType::Quadword, + Type::Double => AssemblyType::Double, Type::FunType { .. } => { panic!("Cannot convert function type to assembly type") } @@ -120,6 +136,12 @@ pub struct StaticVariable { pub alignment: u64, pub init: VarInit, } +#[derive(Debug)] +pub(crate) struct StaticConstant { + pub name: Rc, + pub alignment: u64, + pub init: StaticInit, +} #[derive(Clone, Debug, PartialEq)] pub enum Operand { @@ -136,22 +158,11 @@ impl Operand { } } -impl From<&Val> for Operand { - fn from(val: &Val) -> Self { - match val { - Val::Constant(Const::ConstInt(i)) => Operand::Imm(*i as i64), - Val::Constant(Const::ConstLong(l)) => Operand::Imm(*l), - Val::Var(s) => Operand::Pseudo(s.clone()), - Val::Constant(Const::ConstUInt(i)) => Operand::Imm(*i as i64), - Val::Constant(Const::ConstULong(l)) => Operand::Imm(*l as i64), - } - } -} - #[derive(Clone, Copy, Debug, PartialEq)] pub enum UnaryOp { Neg, Not, + Shr, } #[derive(Clone, Copy, Debug, PartialEq)] @@ -159,6 +170,7 @@ pub enum BinaryOp { Add, Sub, Mult, + DivDouble, BitAnd, BitOr, BitXOr, @@ -182,6 +194,16 @@ pub enum Instruction { src: Operand, dst: Operand, }, + Cvttsd2si { + src: Operand, + dst: Operand, + size: AssemblyType, + }, + Cvtsi2sd { + src: Operand, + dst: Operand, + size: AssemblyType, + }, Unary { op: UnaryOp, dst: Operand, @@ -216,35 +238,38 @@ pub enum Instruction { Ret, } +/// x86 condition codes (the `cc` in `setcc`/`jcc`), named after the flag tests they encode. +/// +/// `E`/`NE` are signedness-agnostic. The signed set (`G`/`GE`/`L`/`LE`) reads SF/OF/ZF; the +/// unsigned set (`A`/`AE`/`B`/`BE`) reads CF/ZF. `double` comparisons (`comisd`) set CF/ZF/PF +/// like an *unsigned* compare, so they reuse `A`/`AE`/`B`/`BE` — plus `P`/`NP` to detect the +/// unordered (NaN) case, where `comisd` sets PF. #[derive(Clone, Copy, Debug, PartialEq)] pub enum CondCode { + /// equal (`ZF=1`) E, + /// not equal (`ZF=0`) NE, + /// above — unsigned `>` (`CF=0 and ZF=0`) A, + /// above or equal — unsigned `>=` (`CF=0`) AE, + /// below — unsigned `<` (`CF=1`) B, + /// below or equal — unsigned `<=` (`CF=1 or ZF=1`) BE, + /// greater — signed `>` (`ZF=0 and SF=OF`) G, + /// greater or equal — signed `>=` (`SF=OF`) GE, + /// less — signed `<` (`SF≠OF`) L, + /// less or equal — signed `<=` (`ZF=1 or SF≠OF`) LE, -} - -impl CondCode { - pub fn ins_suffix(&self) -> &'static str { - match self { - CondCode::E => "e", - CondCode::NE => "ne", - CondCode::G => "g", - CondCode::GE => "ge", - CondCode::L => "l", - CondCode::LE => "le", - CondCode::B => "b", - CondCode::BE => "be", - CondCode::A => "a", - CondCode::AE => "ae", - } - } + /// parity (`PF=1`) — unordered: a NaN operand in a floating-point compare + P, + /// not parity (`PF=0`) — ordered: no NaN operand + NP, } #[derive(Debug)] @@ -255,26 +280,47 @@ pub struct FunctionDefinition { } #[derive(Debug)] -pub struct Program { +pub(crate) struct Program { pub functions: Vec, pub static_vars: Vec, + pub static_constants: Vec, } /// Emits instructions for a function call following the System V AMD64 ABI. /// -/// First 6 integer arguments go in registers (RDI, RSI, RDX, RCX, R8, R9). -/// Remaining arguments are pushed onto the stack in reverse order. -/// Stack is padded to maintain 16-byte alignment before the call. -/// The return value is moved from RAX to the destination. +/// Arguments are classified by [`classify_operands`]: integer args fill RDI/RSI/RDX/RCX/R8/R9 +/// (up to 6), `double` args fill XMM0–XMM7 (up to 8), and the overflow of either kind is pushed +/// onto the stack in reverse order. The stack is padded with an extra 8 bytes when there is an odd +/// number of stack args, keeping it 16-byte aligned at the `call`; the padding plus the pushed +/// args are reclaimed afterward. The return value is read from XMM0 for a `double` result and from +/// RAX otherwise. fn convert_function_call( fun_name: &Identifier, args: &[Val], dst: &Val, symbols: &BackendSymbolTable, + constants: &mut ConstantPool, ) -> Vec { - let arg_registers = [Reg::DI, Reg::SI, Reg::DX, Reg::CX, Reg::R8, Reg::R9]; - let (register_args, stack_args) = args.split_at(args.len().min(6)); - let mut instructions = Vec::new(); + let int_regs = [Reg::DI, Reg::SI, Reg::DX, Reg::CX, Reg::R8, Reg::R9]; + let double_regs = [ + Reg::XMM0, + Reg::XMM1, + Reg::XMM2, + Reg::XMM3, + Reg::XMM4, + Reg::XMM5, + Reg::XMM6, + Reg::XMM7, + ]; + let operands: Vec<(AssemblyType, Operand)> = args + .iter() + .map(|v| (symbols.get_assembly_type(v), val_operand(v, constants, 8))) + .collect(); + + let (int_reg_args, double_reg_args, stack_args) = classify_operands(&operands); + + let mut instructions = vec![]; + let stack_padding = if stack_args.len() % 2 != 0 { instructions.push(Instruction::Binary { op: BinaryOp::Sub, @@ -286,27 +332,36 @@ fn convert_function_call( } else { 0 }; - for (tacky_arg, reg) in register_args.iter().zip(arg_registers) { + + for ((param_ty, dst), reg) in int_reg_args + .iter() + .zip(int_regs) + .chain(double_reg_args.iter().zip(double_regs)) + { instructions.push(Instruction::Mov { - src: tacky_arg.into(), + src: dst.clone(), dst: Operand::Reg(reg), - size: symbols.get_assembly_type(tacky_arg), + size: *param_ty, }); } - for tacky_arg in stack_args.iter().rev() { - let assembly_arg = tacky_arg.into(); - let asm_type = symbols.get_assembly_type(tacky_arg); - if asm_type == AssemblyType::Quadword || matches!(assembly_arg, Operand::Imm(_) | Operand::Reg(_)) { - instructions.push(Instruction::Push(assembly_arg)); + + for (param_ty, dst) in stack_args.iter().rev() { + // 8-byte operands (Quadword/Double) and Reg/Imm operands can be pushed directly; a + // narrower memory operand is first widened through AX so the full 8-byte slot is defined. + if matches!(dst, Operand::Reg(_) | Operand::Imm(_)) + || matches!(param_ty, AssemblyType::Quadword | AssemblyType::Double) + { + instructions.push(Instruction::Push(dst.clone())) } else { instructions.push(Instruction::Mov { - src: assembly_arg, + src: dst.clone(), dst: Operand::Reg(Reg::AX), - size: AssemblyType::Longword, + size: *param_ty, }); instructions.push(Instruction::Push(Operand::Reg(Reg::AX))) } } + instructions.push(Instruction::Call(fun_name.clone())); let bytes_to_remove = 8 * stack_args.len() as i64 + stack_padding; @@ -318,39 +373,177 @@ fn convert_function_call( size: AssemblyType::Quadword, }); } - instructions.push(Instruction::Mov { - src: Operand::Reg(Reg::AX), - dst: dst.into(), - size: symbols.get_assembly_type(dst), - }); + + let assembly_dst = val_operand(dst, constants, 8); + let return_ty = symbols.get_assembly_type(dst); + + if matches!(return_ty, AssemblyType::Double) { + instructions.push(Instruction::Mov { + src: Operand::Reg(Reg::XMM0), + dst: assembly_dst, + size: AssemblyType::Double, + }) + } else { + instructions.push(Instruction::Mov { + src: Operand::Reg(Reg::AX), + dst: assembly_dst, + size: return_ty, + }) + } instructions } +struct ConstantPool { + map: HashMap, u64)>, // f64 bits -> (.rodata label, alignment) +} + +impl ConstantPool { + fn intern(&mut self, value: f64, alignment: u64) -> Rc { + let next = self.map.len(); + let entry = self + .map + .entry(value.to_bits()) + .or_insert_with(|| (Rc::from(format!("double.{next}")), alignment)); + entry.1 = entry.1.max(alignment); + entry.0.clone() + } + + fn new() -> ConstantPool { + ConstantPool { map: HashMap::new() } + } + + /// Drains the interned constants into `StaticConstant`s for `.rodata` emission. + /// + /// Sorted by bit pattern so the emitted constant section is deterministic (`HashMap` iteration + /// order is randomized). Each constant carries the strictest alignment any use requested: 8 for a + /// plain `movsd` load, 16 for the `xorps` sign-flip mask (see [`ConstantPool::intern`]). + fn into_static_constants(self) -> Vec { + let mut entries: Vec<(u64, (Rc, u64))> = self.map.into_iter().collect(); + entries.sort_by_key(|(bits, _)| *bits); + entries + .into_iter() + .map(|(bits, (name, alignment))| StaticConstant { + name, + alignment, + init: StaticInit::DoubleInit(f64::from_bits(bits)), + }) + .collect() + } +} + +/// Converts a TACKY [`Val`] to an assembly [`Operand`]. +/// +/// Integer constants become `Imm`; variables become `Pseudo` (resolved to a stack slot or +/// `Data` reference later). A double constant has no immediate form on x86-64, so it is interned +/// into `constants` and referenced as `Operand::Data` in `.rodata`. +/// +/// `alignment` is forwarded to [`ConstantPool::intern`] and applies *only* to double constants +/// (it's ignored for every other variant). Pass `8` for an ordinary `movsd` load; pass `16` for +/// the `xorps` sign-flip mask, whose 128-bit memory operand requires 16-byte alignment. +fn val_operand(val: &Val, constants: &mut ConstantPool, alignment: u64) -> Operand { + match val { + Val::Constant(Const::ConstDouble(d)) => Operand::Data(constants.intern(*d, alignment)), + Val::Constant(Const::ConstInt(i)) => Operand::Imm(*i as i64), + Val::Constant(Const::ConstLong(l)) => Operand::Imm(*l), + Val::Var(s) => Operand::Pseudo(s.clone()), + Val::Constant(Const::ConstUInt(i)) => Operand::Imm(*i as i64), + Val::Constant(Const::ConstULong(l)) => Operand::Imm(*l as i64), + } +} + /// Lowers a single TACKY instruction to one or more x86-64 assembly instructions /// (instruction selection, pass 1), still operating on pseudo-registers. /// /// Most ops map straightforwardly; the type-dependent ones consult `symbols` for operand -/// size ([`BackendSymbolTable::get_assembly_type`]) and signedness ([`BackendSymbolTable::is_signed`]): +/// size ([`BackendSymbolTable::get_assembly_type`]), signedness ([`BackendSymbolTable::is_signed`]), +/// and whether the operand is a `double` ([`BackendSymbolTable::is_double`]): +/// - **Return** — value in RAX, or XMM0 for a `double`. /// - **Divide / Remainder** — `idiv` (signed) vs `div` (unsigned); the dividend is set up with /// `cdq`/`cqo` (signed) or a zeroed RDX (unsigned). Result taken from RAX (quotient) or RDX -/// (remainder). +/// (remainder). `double` division is the unrelated SSE `divsd` (`BinaryOp::DivDouble`). /// - **Right shift** — `sar` (signed, arithmetic) vs `shr` (unsigned, logical). /// - **Comparisons** — signed (`L`/`G`/…) vs unsigned (`B`/`A`/…) condition codes via `SetCC`. -/// - **Conversions** — `SignExtend` → `Movsx`, `ZeroExtend` → `MovZeroExtend`, `Truncate` → `Mov`. -fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbolTable) -> Vec { +/// `double` comparisons use `comisd` and the *unsigned* codes (it sets ZF/CF/PF like an unsigned +/// compare). NaN (unordered) is handled per IEEE-754: `<`/`<=` swap operands so they use the +/// carry-clear `A`/`AE` (false on unordered); `==`/`!=` combine the parity flag (`setnp`/`setp`) +/// so `NaN == x` is false and `NaN != x` true. +/// - **`double` arithmetic** — `addsd`/`subsd`/`mulsd`/`divsd` on XMM registers (same `BinaryOp`s +/// as the integer forms, selected by `AssemblyType::Double`); negation flips the sign bit with +/// `xorps` against a 16-byte-aligned `-0.0` mask (`xorps`, the single-precision form, is a byte +/// shorter than `xorpd` and bit-identical for a pure XOR); logical `!`/zero-tests compare against +/// a zeroed XMM via `comisd`. +/// - **Integer conversions** — `SignExtend` → `Movsx`, `ZeroExtend` → `MovZeroExtend`, +/// `Truncate` → `Mov`. +/// - **`double` ↔ integer conversions** — `cvtsi2sd`/`cvttsd2si` for the signed cases; the +/// unsigned cases need SSE2 workarounds (no native unsigned convert before AVX-512). See the +/// per-arm comments on `DoubleToUInt` / `UIntToDouble` for the algorithms. +fn convert_instruction( + instruction: &tacky::Instruction, + symbols: &BackendSymbolTable, + constants: &mut ConstantPool, + name_gen: &mut NameGenerator, +) -> Vec { match instruction { tacky::Instruction::Return(x) => { let size = symbols.get_assembly_type(x); + let ret_reg = if matches!(size, AssemblyType::Double) { + Reg::XMM0 + } else { + Reg::AX + }; vec![ Instruction::Mov { - src: x.into(), - dst: Operand::Reg(Reg::AX), + src: val_operand(x, constants, 8), + dst: Operand::Reg(ret_reg), size, }, Instruction::Ret, ] } + tacky::Instruction::Unary { + op: parser::UnaryOp::Not, + src, + dst, + } if symbols.is_double(src) => { + vec![ + Instruction::Binary { + op: BinaryOp::BitXOr, + src: Operand::Reg(Reg::XMM0), + dst: Operand::Reg(Reg::XMM0), + size: AssemblyType::Double, + }, + Instruction::Cmp { + v1: val_operand(src, constants, 8), + v2: Operand::Reg(Reg::XMM0), + size: AssemblyType::Double, + }, + Instruction::Mov { + src: Operand::Imm(0), + dst: val_operand(dst, constants, 8), + size: symbols.get_assembly_type(dst), + }, + Instruction::SetCC { + code: CondCode::E, + op: val_operand(dst, constants, 8), + }, + Instruction::Mov { + src: Operand::Imm(0), + dst: Operand::Reg(Reg::AX), + size: AssemblyType::Longword, + }, + Instruction::SetCC { + code: CondCode::NP, + op: Operand::Reg(Reg::AX), + }, + Instruction::Binary { + op: BinaryOp::BitAnd, + src: Operand::Reg(Reg::AX), + dst: val_operand(dst, constants, 8), + size: AssemblyType::Longword, + }, + ] + } tacky::Instruction::Unary { op: parser::UnaryOp::Not, src, @@ -360,17 +553,37 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol vec![ Instruction::Cmp { v1: Operand::Imm(0), - v2: src.into(), + v2: val_operand(src, constants, 8), size, }, Instruction::Mov { src: Operand::Imm(0), - dst: dst.into(), + dst: val_operand(dst, constants, 8), size, }, Instruction::SetCC { code: CondCode::E, - op: dst.into(), + op: val_operand(dst, constants, 8), + }, + ] + } + tacky::Instruction::Unary { + op: parser::UnaryOp::Negate, + src, + dst, + } if symbols.is_double(src) => { + let sign_mask = Val::Constant(Const::ConstDouble(-0.0)); + vec![ + Instruction::Mov { + src: val_operand(src, constants, 8), + dst: val_operand(dst, constants, 8), + size: AssemblyType::Double, + }, + Instruction::Binary { + op: BinaryOp::BitXOr, + src: val_operand(&sign_mask, constants, 16), + dst: val_operand(dst, constants, 8), + size: AssemblyType::Double, }, ] } @@ -379,13 +592,13 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol let op = convert_unary_op(op); vec![ Instruction::Mov { - src: src.into(), - dst: dst.into(), + src: val_operand(src, constants, 8), + dst: val_operand(dst, constants, 8), size, }, Instruction::Unary { op, - dst: dst.into(), + dst: val_operand(dst, constants, 8), size, }, ] @@ -413,14 +626,29 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol }; vec![ Instruction::Mov { - src: src1.into(), - dst: dst.into(), + src: val_operand(src1, constants, 8), + dst: val_operand(dst, constants, 8), size, }, Instruction::Binary { op: asm_op, - src: src2.into(), - dst: dst.into(), + src: val_operand(src2, constants, 8), + dst: val_operand(dst, constants, 8), + size, + }, + ] + } + BinOp::Divide if symbols.is_double(src1) => { + vec![ + Instruction::Mov { + src: val_operand(src1, constants, 8), + dst: val_operand(dst, constants, 8), + size, + }, + Instruction::Binary { + op: BinaryOp::DivDouble, + src: val_operand(src2, constants, 8), + dst: val_operand(dst, constants, 8), size, }, ] @@ -429,24 +657,24 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol let result_reg = if *op == BinOp::Divide { Reg::AX } else { Reg::DX }; let signed = symbols.is_signed(dst); let mut ins = vec![Instruction::Mov { - src: src1.into(), + src: val_operand(src1, constants, 8), dst: Operand::Reg(Reg::AX), size, }]; if signed { ins.push(Instruction::Cdq(size)); - ins.push(Instruction::Idiv(src2.into(), size)); + ins.push(Instruction::Idiv(val_operand(src2, constants, 8), size)); } else { ins.push(Instruction::Mov { src: Operand::Imm(0), dst: Operand::Reg(Reg::DX), size, }); // zero-extend - ins.push(Instruction::Div(src2.into(), size)); + ins.push(Instruction::Div(val_operand(src2, constants, 8), size)); } ins.push(Instruction::Mov { src: Operand::Reg(result_reg), - dst: dst.into(), + dst: val_operand(dst, constants, 8), size, }); ins @@ -458,13 +686,26 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol | BinOp::GreaterThan | BinOp::GreaterOrEqual => { // signedness comes from the operands, not dst (a comparison's result is always int) - let signed = symbols.is_signed(src1); + let is_double = symbols.is_double(src1); + let signed = !is_double && symbols.is_signed(src1); + let mut v1 = val_operand(src2, constants, 8); + let mut v2 = val_operand(src1, constants, 8); let code = match (op, signed) { (BinOp::Equal, _) => CondCode::E, (BinOp::NotEqual, _) => CondCode::NE, (BinOp::LessThan, true) => CondCode::L, + // double: rewrite `a < b` as `b > a` (swap) so it uses the carry-clear `A`, + // which is false on unordered (NaN); plain unsigned `<` is `B`. + (BinOp::LessThan, false) if is_double => { + mem::swap(&mut v1, &mut v2); + CondCode::A + } (BinOp::LessThan, false) => CondCode::B, (BinOp::LessOrEqual, true) => CondCode::LE, + (BinOp::LessOrEqual, false) if is_double => { + mem::swap(&mut v1, &mut v2); + CondCode::AE + } (BinOp::LessOrEqual, false) => CondCode::BE, (BinOp::GreaterThan, true) => CondCode::G, (BinOp::GreaterThan, false) => CondCode::A, @@ -472,46 +713,96 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol (BinOp::GreaterOrEqual, false) => CondCode::AE, _ => unreachable!(), }; - vec![ - Instruction::Cmp { - v1: src2.into(), - v2: src1.into(), - size, - }, + let mut ins = vec![ + Instruction::Cmp { v1, v2, size }, Instruction::Mov { src: Operand::Imm(0), - dst: dst.into(), + dst: val_operand(dst, constants, 8), size, }, - Instruction::SetCC { code, op: dst.into() }, - ] + Instruction::SetCC { + code, + op: val_operand(dst, constants, 8), + }, + ]; + if is_double && matches!(op, BinOp::Equal | BinOp::NotEqual) { + let (nan_op, code) = if matches!(op, BinOp::Equal) { + (BinaryOp::BitAnd, CondCode::NP) + } else { + (BinaryOp::BitOr, CondCode::P) + }; + ins.push(Instruction::Mov { + src: Operand::Imm(0), + dst: Operand::Reg(Reg::AX), + size: AssemblyType::Longword, + }); + ins.push(Instruction::SetCC { + code, + op: Operand::Reg(Reg::AX), + }); + ins.push(Instruction::Binary { + op: nan_op, + src: Operand::Reg(Reg::AX), + dst: val_operand(dst, constants, 8), + size: AssemblyType::Longword, + }) + } + ins } } } - tacky::Instruction::JumpIfZero { condition, target } => { - let size = symbols.get_assembly_type(condition); - vec![ + tacky::Instruction::JumpIfZero { condition, target } + | tacky::Instruction::JumpIfNotZero { condition, target } + if symbols.is_double(condition) => + { + let (code, nan_target) = if matches!(instruction, tacky::Instruction::JumpIfZero { .. }) { + let nan_skip = Identifier(name_gen.next("nan_skip")); + (CondCode::E, nan_skip.clone()) + } else { + (CondCode::NE, target.clone()) + }; + let mut cmp_ins = vec![ + Instruction::Binary { + op: BinaryOp::BitXOr, + src: Operand::Reg(Reg::XMM0), + dst: Operand::Reg(Reg::XMM0), + size: AssemblyType::Double, + }, Instruction::Cmp { - v1: Operand::Imm(0), - v2: condition.into(), - size, + v1: val_operand(condition, constants, 8), + v2: Operand::Reg(Reg::XMM0), + size: AssemblyType::Double, }, Instruction::JmpCC { - code: CondCode::E, + code: CondCode::P, + label: nan_target.clone(), + }, + Instruction::JmpCC { + code, label: target.clone(), }, - ] + ]; + if matches!(instruction, tacky::Instruction::JumpIfZero { .. }) { + cmp_ins.push(Instruction::Label(nan_target)); + } + cmp_ins } - tacky::Instruction::JumpIfNotZero { condition, target } => { + tacky::Instruction::JumpIfZero { condition, target } + | tacky::Instruction::JumpIfNotZero { condition, target } => { + let code = if matches!(instruction, tacky::Instruction::JumpIfZero { .. }) { + CondCode::E + } else { + CondCode::NE + }; let size = symbols.get_assembly_type(condition); vec![ Instruction::Cmp { v1: Operand::Imm(0), - v2: condition.into(), + v2: val_operand(condition, constants, 8), size, }, Instruction::JmpCC { - code: CondCode::NE, + code, label: target.clone(), }, ] @@ -525,31 +816,126 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol tacky::Instruction::Copy { src, dst } => { let size = symbols.get_assembly_type(src); vec![Instruction::Mov { - src: src.into(), - dst: dst.into(), + src: val_operand(src, constants, 8), + dst: val_operand(dst, constants, 8), size, }] } - tacky::Instruction::FunCall { fun_name, args, dst } => convert_function_call(fun_name, args, dst, symbols), + tacky::Instruction::FunCall { fun_name, args, dst } => { + convert_function_call(fun_name, args, dst, symbols, constants) + } tacky::Instruction::SignExtend { src, dst } => { vec![Instruction::Movsx { - src: src.into(), - dst: dst.into(), + src: val_operand(src, constants, 8), + dst: val_operand(dst, constants, 8), }] } tacky::Instruction::Truncate { src, dst } => { vec![Instruction::Mov { - src: src.into(), - dst: dst.into(), + src: val_operand(src, constants, 8), + dst: val_operand(dst, constants, 8), size: AssemblyType::Longword, }] } tacky::Instruction::ZeroExtend { src, dst } => { vec![Instruction::MovZeroExtend { - src: src.into(), - dst: dst.into(), + src: val_operand(src, constants, 8), + dst: val_operand(dst, constants, 8), }] } + // Signed int -> double: `cvtsi2sd` is native and exact (no rounding mode needed). The + // `size` is the *source* integer width (32 vs 64-bit), since the double dest is implicit. + tacky::Instruction::IntToDouble { src, dst } => { + vec![Instruction::Cvtsi2sd { + src: val_operand(src, constants, 8), + dst: val_operand(dst, constants, 8), + size: symbols.get_assembly_type(src), + }] + } + // double -> signed int: `cvttsd2si` truncates toward zero. The `size` is the *destination* + // integer width. Out-of-range/NaN yield the "integer indefinite" value (target MIN) — a + // deliberate, deterministic edge (see the module/README notes on float-cast behavior). + tacky::Instruction::DoubleToInt { src, dst } => { + vec![Instruction::Cvttsd2si { + src: val_operand(src, constants, 8), + dst: val_operand(dst, constants, 8), + size: symbols.get_assembly_type(dst), + }] + } + // double -> unsigned int. There is no native unsigned conversion before AVX-512, so we + // route through the signed `cvttsd2si` and split on the destination width. + #[rustfmt::skip] + tacky::Instruction::DoubleToUInt { src: src_raw, dst: dst_raw } => { + let src = val_operand(src_raw, constants, 8); + let dst = val_operand(dst_raw, constants, 8); + if matches!(symbols.get_assembly_type(src_raw), AssemblyType::Longword) { + // u32: every value in range fits the positive i64 range, so convert to a full + // 64-bit signed int and keep the low 32 bits. + vec![ + Instruction::Cvttsd2si { src, dst: Operand::Reg(Reg::AX), size: AssemblyType::Quadword }, + Instruction::Mov { src: Operand::Reg(Reg::AX), dst, size: AssemblyType::Longword } + ] + } else { + // u64: `cvttsd2si` only does signed i64, so values in [2^63, 2^64) overflow it. + // If src >= 2^63, subtract 2^63, convert the now-in-range value, then add 2^63 + // back (as the bit pattern 0x8000_0000_0000_0000 = i64::MIN); otherwise convert + // directly. + let long_max_plus_1 = val_operand(&Val::Constant(Const::ConstDouble(9223372036854775808.0)), constants, 8); + let oor = Identifier(name_gen.next("d2u_oor")); + let end = Identifier(name_gen.next("d2u_end")); + vec![ + Instruction::Cmp { v1: long_max_plus_1.clone(), v2: src.clone(), size: AssemblyType::Double }, // src vs 2^63 + Instruction::JmpCC { code: CondCode::AE, label: oor.clone() }, // src >= 2^63 -> fixup + Instruction::Cvttsd2si { src: src.clone(), dst: dst.clone(), size: AssemblyType::Quadword }, // in range: direct + Instruction::Jmp(end.clone()), + Instruction::Label(oor), // src >= 2^63 + Instruction::Mov { src: src.clone(), dst: Operand::Reg(Reg::XMM0), size: AssemblyType::Double }, + Instruction::Binary { op: BinaryOp::Sub, src: long_max_plus_1, dst: Operand::Reg(Reg::XMM0), size: AssemblyType::Double }, // src - 2^63 + Instruction::Cvttsd2si { src: Operand::Reg(Reg::XMM0), dst: dst.clone(), size: AssemblyType::Quadword }, // now in i64 range + Instruction::Mov {src: Operand::Imm(i64::MIN), dst: Operand::Reg(Reg::AX), size: AssemblyType::Quadword }, // 2^63 bit pattern + Instruction::Binary { op: BinaryOp::Add, src: Operand::Reg(Reg::AX), dst, size: AssemblyType::Quadword }, // add 2^63 back + Instruction::Label(end) + ] + } + } + // unsigned int -> double. `cvtsi2sd` only reads signed integers, so we split on the + // source width. + #[rustfmt::skip] + tacky::Instruction::UIntToDouble { src: src_raw, dst } => { + let src = val_operand(src_raw, constants, 8); + if matches!(symbols.get_assembly_type(src_raw), AssemblyType::Longword) { + // u32: zero-extend to 64 bits (always positive as signed i64), then convert. + // MovZeroExtend rather than Mov is deliberate — it matters for register allocation in Part III. + vec![ + Instruction::MovZeroExtend { src, dst: Operand::Reg(Reg::AX) }, + Instruction::Cvtsi2sd { src: Operand::Reg(Reg::AX), dst: Operand::Reg(Reg::XMM0), size: AssemblyType::Quadword }, + Instruction::Mov { src: Operand::Reg(Reg::XMM0), dst: val_operand(dst, constants, 8), size: AssemblyType::Double } + ] + } else { + // u64: if the top bit is set the value reads as negative to `cvtsi2sd`. Halve it + // (so it fits the i64 range), convert, then double the result. The halving rounds + // to odd (`>>1` then OR in the low bit) so the single rounding in `cvtsi2sd` + // matches what a correct u64->double would produce. Top bit clear -> convert directly. + let oor = Identifier(name_gen.next("u2d_oor")); + let end = Identifier(name_gen.next("u2d_end")); + vec![ + Instruction::Cmp { v1: Operand::Imm(0), v2: src.clone(), size: AssemblyType::Quadword }, // src vs 0 (signed) + Instruction::JmpCC { code: CondCode::L, label: oor.clone() }, // top bit set -> halve + Instruction::Cvtsi2sd { src: src.clone(), dst: Operand::Reg(Reg::XMM0), size: AssemblyType::Quadword }, // direct + Instruction::Jmp(end.clone()), + Instruction::Label(oor), // top bit set + Instruction::Mov { src, dst: Operand::Reg(Reg::AX), size: AssemblyType::Quadword }, + Instruction::Mov { src: Operand::Reg(Reg::AX), dst: Operand::Reg(Reg::DX), size: AssemblyType::Quadword }, + Instruction::Unary { op: UnaryOp::Shr, dst: Operand::Reg(Reg::DX), size: AssemblyType::Quadword }, // DX = src >> 1 + Instruction::Binary { op: BinaryOp::BitAnd, src: Operand::Imm(1), dst: Operand::Reg(Reg::AX), size: AssemblyType::Quadword }, // AX = src & 1 + Instruction::Binary { op: BinaryOp::BitOr, src: Operand::Reg(Reg::AX), dst: Operand::Reg(Reg::DX), size: AssemblyType::Quadword }, // round to odd + Instruction::Cvtsi2sd { src: Operand::Reg(Reg::DX), dst: Operand::Reg(Reg::XMM0), size: AssemblyType::Quadword }, + Instruction::Binary { op: BinaryOp::Add, src: Operand::Reg(Reg::XMM0), dst: Operand::Reg(Reg::XMM0), size: AssemblyType::Double }, // *2 + Instruction::Label(end), + Instruction::Mov { src: Operand::Reg(Reg::XMM0), dst: val_operand(dst, constants, 8), size: AssemblyType::Double } + ] + } + } } } @@ -591,49 +977,112 @@ fn convert_unary_op(op: &parser::UnaryOp) -> UnaryOp { } } -/// Converts a TACKY function definition to x86-64 assembly instructions. +/// Splits typed operands into the three System V AMD64 argument-passing classes: +/// - **integer register** — `Longword`/`Quadword` operands, first 6 (→ RDI, RSI, RDX, RCX, R8, R9) +/// - **SSE register** — `Double` operands, first 8 (→ XMM0–XMM7) +/// - **stack** — everything that overflows either register limit, in operand order /// -/// Emits parameter moves (from registers/stack to pseudo-registers) followed -/// by the converted body instructions. First 6 params come from registers, -/// the rest from stack positions above the saved RBP and return address. -fn convert_function(ast: &tacky::FunctionDefinition, symbols: &BackendSymbolTable) -> FunctionDefinition { - let tacky::FunctionDefinition { - name, - params, - body, - global, - temp_types: _, - } = ast; - let arg_registers = [Reg::DI, Reg::SI, Reg::DX, Reg::CX, Reg::R8, Reg::R9]; - let mut instructions = vec![]; +/// The two register limits are independent (6 GP and 8 XMM), matching the ABI. Shared by the +/// callee prologue ([`set_up_parameters`]) and the caller argument setup +/// ([`convert_function_call`]) so both sides necessarily agree on register assignment — the calling +/// convention requires it, and a single classifier makes disagreement unrepresentable. Each side +/// builds the `(AssemblyType, Operand)` pairs from its own source (param pseudos vs argument +/// operands) and emits its own direction of moves. +type ArgClass = Vec<(AssemblyType, Operand)>; - for (Identifier(param), reg) in params.iter().zip(arg_registers) { - let param_ty = symbols.get_obj_type(param); +fn classify_operands(operands: &[(AssemblyType, Operand)]) -> (ArgClass, ArgClass, ArgClass) { + let mut int_reg = vec![]; + let mut double_reg = vec![]; + let mut stack = vec![]; + + for (ty, operand) in operands { + let bucket = match ty { + AssemblyType::Double if double_reg.len() < 8 => &mut double_reg, + AssemblyType::Longword | AssemblyType::Quadword if int_reg.len() < 6 => &mut int_reg, + _ => &mut stack, + }; + bucket.push((*ty, operand.clone())); + } + (int_reg, double_reg, stack) +} +/// Emits the function prologue moves that copy incoming parameters into their pseudo-registers. +/// +/// The mirror of [`convert_function_call`]'s argument setup: parameters are classified by +/// [`classify_operands`] into the same register/stack classes, then each is moved *from* its +/// incoming location *into* its pseudo. Integer-register and SSE-register params come from +/// RDI/…/R9 and XMM0–XMM7; stack params are read from positive RBP offsets (`+16` for the first, +/// then `+8` each — above the saved RBP and return address). Both sides share `classify_operands`, +/// so the callee and caller necessarily agree on where each argument lives. +fn set_up_parameters(params: &[Identifier], symbols: &BackendSymbolTable) -> Vec { + let operands: Vec<(AssemblyType, Operand)> = params + .iter() + .map(|Identifier(p)| (symbols.get_obj_type(p), Operand::Pseudo(p.clone()))) + .collect(); + let (int_reg_args, double_reg_args, stack_args) = classify_operands(&operands); + let int_regs = [Reg::DI, Reg::SI, Reg::DX, Reg::CX, Reg::R8, Reg::R9]; + let double_regs = [ + Reg::XMM0, + Reg::XMM1, + Reg::XMM2, + Reg::XMM3, + Reg::XMM4, + Reg::XMM5, + Reg::XMM6, + Reg::XMM7, + ]; + let mut instructions: Vec = Vec::with_capacity(params.len()); + + for ((param_ty, dst), reg) in int_reg_args + .iter() + .zip(int_regs) + .chain(double_reg_args.iter().zip(double_regs)) + { instructions.push(Instruction::Mov { src: Operand::Reg(reg), - dst: Operand::Pseudo(param.clone()), - size: param_ty, + dst: dst.clone(), + size: *param_ty, }); } - for (i, Identifier(param)) in params.iter().skip(6).enumerate() { + for (i, (param_ty, dst)) in stack_args.iter().enumerate() { let stack_offset = 16 + (i as i32 * 8); // +16 for saved RBP and return address - let param_ty = symbols.get_obj_type(param); instructions.push(Instruction::Mov { src: Operand::Stack(stack_offset), - dst: Operand::Pseudo(param.clone()), - size: param_ty, + dst: dst.clone(), + size: *param_ty, }); } + instructions +} - instructions.extend(body.iter().flat_map(|ins| convert_instruction(ins, symbols))); - { - FunctionDefinition { - name: name.clone(), - body: instructions, - global: *global, - } +/// Converts a TACKY function definition to x86-64 assembly instructions. +/// +/// Emits the parameter prologue ([`set_up_parameters`], which moves incoming arguments from their +/// ABI register/stack locations into pseudo-registers) followed by the converted body instructions. +fn convert_function( + ast: &tacky::FunctionDefinition, + symbols: &BackendSymbolTable, + constants: &mut ConstantPool, + name_gen: &mut NameGenerator, +) -> FunctionDefinition { + let tacky::FunctionDefinition { + name, + params, + body, + global, + temp_types: _, + } = ast; + let mut instructions = set_up_parameters(params, symbols); + + instructions.extend( + body.iter() + .flat_map(|ins| convert_instruction(ins, symbols, constants, name_gen)), + ); + FunctionDefinition { + name: name.clone(), + body: instructions, + global: *global, } } @@ -643,8 +1092,9 @@ fn convert_function(ast: &tacky::FunctionDefinition, symbols: &BackendSymbolTabl /// variable's `Type`, from which both the assembly type (size — Longword/Quadword) and /// signedness (for `idiv`/`div`, `sar`/`shr`, signed/unsigned condition codes) are derived. pub enum AsmSymbolEntry { - // `is_static` and `defined` are tracked for future use (e.g. RIP-relative - // addressing decisions, link-time checks) but not yet read. + // `is_static`/`defined` will pick direct RIP-relative vs GOT-indirect (@GOTPCREL) + // addressing for a symbol (and enable link-time checks); NCC currently emits direct + // addressing for all data, so these aren't read yet. Obj { var_type: Type, #[allow(dead_code)] @@ -683,6 +1133,7 @@ impl BackendSymbolTable { Val::Constant(Const::ConstInt(_) | Const::ConstUInt(_)) => AssemblyType::Longword, Val::Constant(Const::ConstLong(_) | Const::ConstULong(_)) => AssemblyType::Quadword, Val::Var(name) => self.get_var_type(name).into(), + Val::Constant(Const::ConstDouble(_)) => AssemblyType::Double, } } @@ -694,9 +1145,14 @@ impl BackendSymbolTable { Val::Constant(Const::ConstInt(_) | Const::ConstLong(_)) => true, Val::Constant(Const::ConstUInt(_) | Const::ConstULong(_)) => false, Val::Var(name) => self.get_var_type(name).is_signed(), + Val::Constant(Const::ConstDouble(_)) => unreachable!("helper for integer types"), } } + fn is_double(&self, val: &Val) -> bool { + matches!(self.get_assembly_type(val), AssemblyType::Double) + } + /// Assembly type (size) of the named object — `get_var_type` reduced to its `AssemblyType`. /// Use when you hold a name (param/variable); use `get_assembly_type` for a [`Val`]. fn get_obj_type(&self, name: &str) -> AssemblyType { @@ -762,16 +1218,25 @@ fn convert_static_var(static_var: TackyStaticVariable) -> StaticVariable { /// 2. Pseudo-register replacement: assigns stack slots to locals, Data operands to statics /// 3. Fix-up: rewrites invalid x86-64 operand combinations (e.g., memory-to-memory) /// 4. Label coalescing: merges consecutive labels to reduce jump targets -pub fn generate(ast: tacky::Program, symbols: &SymbolTable) -> (Program, BackendSymbolTable) { +pub fn generate( + ast: tacky::Program, + symbols: &SymbolTable, + name_gen: &mut NameGenerator, +) -> (Program, BackendSymbolTable) { + let mut constants = ConstantPool::new(); let backend_symbol_table = build_backend_symbol_table(&ast, symbols); let functions = ast .function_defs .iter() - .map(|f| convert_function(f, &backend_symbol_table)) + .map(|f| convert_function(f, &backend_symbol_table, &mut constants, name_gen)) .collect(); let static_vars = ast.static_vars.into_iter().map(convert_static_var).collect(); - let mut p = Program { functions, static_vars }; + let mut p = Program { + functions, + static_vars, + static_constants: constants.into_static_constants(), + }; let stack_offsets = replace_pseudo_registers(&mut p, &backend_symbol_table); fix_invalid(&mut p, &stack_offsets); coalesce_labels(&mut p); @@ -848,6 +1313,14 @@ fn replace_pseudo_registers(program: &mut Program, symbols: &BackendSymbolTable) *src = stack_mapping.replace_pseudo(src, *size); *dst = stack_mapping.replace_pseudo(dst, *size); } + Instruction::Cvttsd2si { src, dst, size } => { + *src = stack_mapping.replace_pseudo(src, AssemblyType::Double); // src is double + *dst = stack_mapping.replace_pseudo(dst, *size); // dst is integer + } + Instruction::Cvtsi2sd { src, dst, size } => { + *src = stack_mapping.replace_pseudo(src, *size); // src is integer + *dst = stack_mapping.replace_pseudo(dst, AssemblyType::Double); // dst is double + } Instruction::Unary { op: _, dst, size } => *dst = stack_mapping.replace_pseudo(dst, *size), Instruction::Movsx { src, dst } | Instruction::MovZeroExtend { src, dst } => { *src = stack_mapping.replace_pseudo(src, AssemblyType::Longword); @@ -867,7 +1340,12 @@ fn replace_pseudo_registers(program: &mut Program, symbols: &BackendSymbolTable) } } Instruction::Push(op) => *op = stack_mapping.replace_pseudo(op, AssemblyType::Quadword), - _ => {} + Instruction::Ret + | Instruction::Call(_) + | Instruction::Label(_) + | Instruction::JmpCC { .. } + | Instruction::Jmp(_) + | Instruction::Cdq(_) => {} } } offsets.insert(name.clone(), stack_mapping.offset); @@ -877,13 +1355,22 @@ fn replace_pseudo_registers(program: &mut Program, symbols: &BackendSymbolTable) /// Fixes invalid x86-64 instruction operand combinations. /// -/// Rewrites instructions that violate x86-64 encoding rules: -/// - Memory-to-memory moves (uses R10 as intermediate) +/// Rewrites instructions that violate x86-64 encoding rules. Integer/general cases use the GP +/// scratch registers R10/R11 (and CX for shift counts); SSE cases use the reserved XMM scratch +/// registers XMM14/XMM15: +/// - Memory-to-memory moves (intermediate: R10, or XMM14 for a `double` `movsd`) /// - Immediate operand to idiv (moves to R10 first) /// - imul with memory destination (uses R11 as intermediate) -/// - Binary ops with both operands in memory (uses R10) +/// - Integer binary ops with both operands in memory (uses R10) +/// - `double` binary ops (`addsd`/`subsd`/`mulsd`/`divsd`/`xorps`) with a non-register +/// destination — SSE requires an XMM-register destination, so the value is routed through XMM15 /// - Shift with memory source (moves count to CX) -/// - Compare with immediate as destination operand (v2), or both operands in memory, or large immediates +/// - Compare with immediate as destination operand (v2), or both operands in memory, or large +/// immediates; `double` compares (`comisd`) additionally require an XMM-register second operand, +/// routed through XMM15 +/// - `cvttsd2si` with a non-register (memory) destination — converts into R11, then stores +/// - `cvtsi2sd` with an immediate source (moved through R10) and/or a non-register destination +/// (converts into XMM15, then stores the `double` back) /// - Large immediates that don't fit in i32 for quadword operations /// - Movsx with immediate source or pseudo-register destination /// @@ -907,13 +1394,18 @@ fn fix_invalid(program: &mut Program, stack_offsets: &HashMap, i32>) { for ins in body.drain(..) { match ins { Instruction::Mov { ref src, ref dst, size } if src.is_memory() && dst.is_memory() => { + let scratch = if matches!(size, AssemblyType::Double) { + Operand::Reg(Reg::XMM14) + } else { + Operand::Reg(Reg::R10) + }; new_ins.push(Instruction::Mov { src: src.clone(), - dst: Operand::Reg(Reg::R10), + dst: scratch.clone(), size, }); new_ins.push(Instruction::Mov { - src: Operand::Reg(Reg::R10), + src: scratch, dst: dst.clone(), size, }); @@ -985,6 +1477,29 @@ fn fix_invalid(program: &mut Program, stack_offsets: &HashMap, i32>) { }); new_ins.push(Instruction::Div(Operand::Reg(Reg::R10), size)) } + Instruction::Binary { + op, + ref src, + ref dst, + size: AssemblyType::Double, + } if !matches!(dst, Operand::Reg(_)) => { + new_ins.push(Instruction::Mov { + src: dst.clone(), + dst: Operand::Reg(Reg::XMM15), + size: AssemblyType::Double, + }); + new_ins.push(Instruction::Binary { + op, + src: src.clone(), + dst: Operand::Reg(Reg::XMM15), + size: AssemblyType::Double, + }); + new_ins.push(Instruction::Mov { + src: Operand::Reg(Reg::XMM15), + dst: dst.clone(), + size: AssemblyType::Double, + }); + } Instruction::Binary { op: BinaryOp::Mult, ref src, @@ -1083,6 +1598,26 @@ fn fix_invalid(program: &mut Program, stack_offsets: &HashMap, i32>) { size: AssemblyType::Quadword, }) } + Instruction::Cmp { + ref v1, + ref v2, + size: AssemblyType::Double, + } => { + if let Operand::Reg(_) = v2 { + new_ins.push(ins.clone()) + } else { + new_ins.push(Instruction::Mov { + src: v2.clone(), + dst: Operand::Reg(Reg::XMM15), + size: AssemblyType::Double, + }); + new_ins.push(Instruction::Cmp { + v1: v1.clone(), + v2: Operand::Reg(Reg::XMM15), + size: AssemblyType::Double, + }) + } + } Instruction::Cmp { ref v1, ref v2, size } => { let new_v1 = if (v1.is_memory() && v2.is_memory()) || matches!((v1, size), (Operand::Imm(c), AssemblyType::Quadword) if *c < i32::MIN as i64 || *c > i32::MAX as i64) @@ -1122,6 +1657,49 @@ fn fix_invalid(program: &mut Program, stack_offsets: &HashMap, i32>) { }); new_ins.push(Instruction::Push(Operand::Reg(Reg::R10))); } + Instruction::Cvttsd2si { src, dst, size } if !matches!(dst, Operand::Reg(_)) => { + new_ins.push(Instruction::Cvttsd2si { + src, + dst: Operand::Reg(Reg::R11), + size, + }); + new_ins.push(Instruction::Mov { + src: Operand::Reg(Reg::R11), + dst, + size, + }) + } + Instruction::Cvtsi2sd { src, dst, size } => { + let new_src = if let Operand::Imm(_) = src { + new_ins.push(Instruction::Mov { + src, + dst: Operand::Reg(Reg::R10), + size, + }); + Operand::Reg(Reg::R10) + } else { + src + }; + + if !matches!(dst, Operand::Reg(_)) { + new_ins.push(Instruction::Cvtsi2sd { + src: new_src, + dst: Operand::Reg(Reg::XMM15), + size, + }); + new_ins.push(Instruction::Mov { + src: Operand::Reg(Reg::XMM15), + dst, + size: AssemblyType::Double, + }) + } else { + new_ins.push(Instruction::Cvtsi2sd { + src: new_src, + dst, + size, + }) + } + } Instruction::Movsx { ref src, ref dst } => { //extends longword src to quadword dst if let &Operand::Imm(val) = src { diff --git a/src/emit.rs b/src/emit.rs deleted file mode 100644 index 81fa2f1..0000000 --- a/src/emit.rs +++ /dev/null @@ -1,342 +0,0 @@ -//! # Emitter (text) — Assembly AST to AT&T-Syntax Text Assembly -//! -//! Deprecated text-based emitter (enabled via `--no-iced`). Generates AT&T-syntax -//! x86-64 assembly as a [`String`], intended to be written to a `.s` file and -//! assembled by the system `as` assembler. -//! -//! ## Call Order -//! -//! ```text -//! emit_program() — public entry point, returns full assembly text -//! ├─ emit_function() — .globl directive, prologue, instruction loop -//! │ └─ emit_instruction() — single instruction to AT&T syntax -//! │ └─ emit_operand() — register/immediate/stack/data operand formatting -//! └─ emit_static_variable() — .data/.bss directives for static vars -//! ``` - -use crate::codegen::{ - AssemblyType, BinaryOp, FunctionDefinition, Instruction, Operand, Program, Reg, StaticVariable, UnaryOp, -}; -use crate::tacky::VarInit; -use crate::validate; - -enum RegWidth { - Byte, - DWord, - QWord, -} - -impl RegWidth { - fn from_size(size: &AssemblyType) -> Self { - match size { - AssemblyType::Longword => RegWidth::DWord, - AssemblyType::Quadword => RegWidth::QWord, - } - } -} - -fn size_suffix(size: &AssemblyType) -> &'static str { - match size { - AssemblyType::Longword => "l", - AssemblyType::Quadword => "q", - } -} - -fn emit_reg(reg: &Reg, reg_width: &RegWidth) -> &'static str { - match reg_width { - RegWidth::Byte => match reg { - Reg::AX => "al", - Reg::DX => "dl", - Reg::CX => "cl", - Reg::DI => "dil", - Reg::SI => "sil", - Reg::R8 => "r8b", - Reg::R9 => "r9b", - Reg::R10 => "r10b", - Reg::R11 => "r11b", - Reg::SP => "spl", - }, - RegWidth::DWord => match reg { - Reg::AX => "eax", - Reg::DX => "edx", - Reg::CX => "ecx", - Reg::DI => "edi", - Reg::SI => "esi", - Reg::R8 => "r8d", - Reg::R9 => "r9d", - Reg::R10 => "r10d", - Reg::R11 => "r11d", - Reg::SP => "esp", - }, - RegWidth::QWord => match reg { - Reg::AX => "rax", - Reg::DX => "rdx", - Reg::CX => "rcx", - Reg::DI => "rdi", - Reg::SI => "rsi", - Reg::R8 => "r8", - Reg::R9 => "r9", - Reg::R10 => "r10", - Reg::R11 => "r11", - Reg::SP => "rsp", - }, - } -} - -fn emit_unaryop(op: &UnaryOp, size: &AssemblyType) -> String { - let suffix = size_suffix(size); - match op { - UnaryOp::Neg => format!("neg{suffix}"), - UnaryOp::Not => format!("not{suffix}"), - } -} - -fn emit_binaryop(op: &BinaryOp, size: &AssemblyType) -> String { - let suffix = size_suffix(size); - match op { - BinaryOp::Add => format!("add{suffix}"), - BinaryOp::Sub => format!("sub{suffix}"), - BinaryOp::Mult => format!("imul{suffix}"), - BinaryOp::BitAnd => format!("and{suffix}"), - BinaryOp::BitOr => format!("or{suffix}"), - BinaryOp::BitXOr => format!("xor{suffix}"), - BinaryOp::BitShl => format!("shl{suffix}"), - BinaryOp::BitSar => format!("sar{suffix}"), - BinaryOp::BitShr => format!("shr{suffix}"), - } -} - -fn emit_operand(operand: &Operand, reg_width: &RegWidth) -> String { - let mut output = String::new(); - match operand { - Operand::Imm(value) => { - output.push_str(&format!("${value}")); - } - Operand::Reg(reg) => { - output.push_str(&format!("%{}", emit_reg(reg, reg_width))); - } - Operand::Stack(offset) => { - output.push_str(&format!("{offset}(%rbp)")); - } - &Operand::Pseudo(_) => unreachable!("Must be eliminated before emission"), - Operand::Data(name) => { - // RIP-relative addressing for static/extern variables - if cfg!(target_os = "macos") { - output.push_str(&format!("_{name}(%rip)")); - } else { - output.push_str(&format!("{name}(%rip)")); - } - } - } - output -} - -/// Emits AT&T-syntax x86-64 assembly text for a single instruction. -/// -/// `fn_name` is used to prefix labels and jump targets, ensuring they are scoped -/// to the enclosing function (e.g., `main.label0`). -fn emit_instruction(ins: &Instruction, fn_name: &str) -> String { - let mut output = String::new(); - match ins { - Instruction::Mov { src, dst, size } => { - let suffix = size_suffix(size); - let width = RegWidth::from_size(size); - output.push_str(&format!( - "mov{suffix} {}, {}", - emit_operand(src, &width), - emit_operand(dst, &width) - )); - } - Instruction::Movsx { src, dst } => { - // movslq - sign-extend 32-bit to 64-bit - output.push_str(&format!( - "movslq {}, {}", - emit_operand(src, &RegWidth::DWord), - emit_operand(dst, &RegWidth::QWord) - )); - } - Instruction::Ret => { - output.push_str("movq %rbp, %rsp\n"); - output.push_str("\tpopq %rbp\n"); - output.push_str("\tret"); - } - Instruction::Unary { op, dst, size } => { - let width = RegWidth::from_size(size); - output.push_str(&format!("{} {}\n", emit_unaryop(op, size), emit_operand(dst, &width))); - } - Instruction::Binary { - op: op @ (BinaryOp::BitShl | BinaryOp::BitSar | BinaryOp::BitShr), - src, - dst, - size, - } => { - // shl, sar, and shr always use an immediate or cl register as the source operand - let width = RegWidth::from_size(size); - output.push_str(&format!( - "{} {}, {}", - emit_binaryop(op, size), - emit_operand(src, &RegWidth::Byte), - emit_operand(dst, &width) - )); - } - Instruction::Binary { op, src, dst, size } => { - let width = RegWidth::from_size(size); - output.push_str(&format!( - "{} {}, {}", - emit_binaryop(op, size), - emit_operand(src, &width), - emit_operand(dst, &width) - )); - } - Instruction::Idiv(op, size) => { - let suffix = size_suffix(size); - let width = RegWidth::from_size(size); - output.push_str(&format!("idiv{suffix} {} ", emit_operand(op, &width))); - } - Instruction::Div(op, size) => { - let suffix = size_suffix(size); - let width = RegWidth::from_size(size); - output.push_str(&format!("div{suffix} {} ", emit_operand(op, &width))); - } - Instruction::MovZeroExtend { .. } => unreachable!("MovZeroExtend in emit"), - Instruction::Cdq(size) => { - let ins = match size { - AssemblyType::Longword => "cdq", - AssemblyType::Quadword => "cqo", - }; - output.push_str(ins); - } - Instruction::Cmp { v1, v2, size } => { - let suffix = size_suffix(size); - let width = RegWidth::from_size(size); - output.push_str(&format!( - "cmp{suffix} {}, {}", - emit_operand(v1, &width), - emit_operand(v2, &width) - )); - } - Instruction::Jmp(target) => { - output.push_str(&format!("jmp {}.{}", fn_name, target.0)); - } - Instruction::JmpCC { code, label } => { - output.push_str(&format!("j{} {}.{}", code.ins_suffix(), fn_name, label.0)); - } - Instruction::Label(label) => { - output.push_str(&format!("{}.{}:", fn_name, label.0)); - } - Instruction::SetCC { code, op } => { - output.push_str(&format!( - "set{} {}", - code.ins_suffix(), - emit_operand(op, &RegWidth::Byte) - )); - } - Instruction::Push(op) => { - output.push_str(&format!("pushq {}", emit_operand(op, &RegWidth::QWord))); - } - Instruction::Call(name) => { - if cfg!(target_os = "macos") { - output.push_str(&format!("call _{}", name.0)); - } else { - output.push_str(&format!("call {}@PLT", name.0)); - } - } - } - output -} - -fn emit_function(fun_def: &FunctionDefinition) -> String { - let mut output = String::new(); - let FunctionDefinition { name, body, global } = fun_def; - let processed_name = if cfg!(target_os = "macos") { - format!("_{name}") - } else { - name.to_string() - }; - if *global { - output.push_str(&format!("\t.globl {processed_name}\n")); - } - output.push_str(&format!("{processed_name}:\n")); - output.push_str("\tpushq %rbp\n"); - output.push_str("\tmovq %rsp, %rbp\n"); - for ins in body.iter() { - output.push_str(&format!("\t{}\n", emit_instruction(ins, name))); - } - output -} - -/// Emits a static variable as AT&T-syntax assembly directives. -/// -/// Places zero-initialized variables in `.bss` and non-zero variables in `.data`, -/// with appropriate alignment and size directives (`.long` for 32-bit int/uint, `.quad` -/// for 64-bit long/ulong). -/// Extern variables (unresolved by the linker) produce no output. -/// On macOS, symbol names are prefixed with `_`. -fn emit_static_variable(sv: &StaticVariable) -> String { - let mut output = String::new(); - let StaticVariable { - name, - global, - init, - alignment, - } = sv; - let processed_name = if cfg!(target_os = "macos") { - format!("_{name}") - } else { - name.to_string() - }; - - // Extern variables don't need any output - they're resolved by the linker - let VarInit::Defined(init_val) = init else { - return output; - }; - - if *global { - output.push_str(&format!("\t.globl {processed_name}\n")); - } - - match init_val { - validate::StaticInt::IntInit(0) - | validate::StaticInt::LongInit(0) - | validate::StaticInt::UIntInit(0) - | validate::StaticInt::ULongInit(0) => { - // BSS section for zero-initialized data - output.push_str("\t.bss\n"); - output.push_str(&format!("\t.align {alignment}\n")); - output.push_str(&format!("{processed_name}:\n")); - output.push_str(&format!("\t.zero {alignment}\n")); - } - nonzero => { - // Data section: directive (.long/.quad) and value follow the type - let (directive, value) = nonzero.data_directive(); - output.push_str("\t.data\n"); - output.push_str(&format!("\t.align {alignment}\n")); - output.push_str(&format!("{processed_name}:\n")); - output.push_str(&format!("\t{directive} {value}\n")); - } - } - - output -} - -pub fn emit_program(program: &Program) -> String { - let mut output = String::new(); - - // Emit functions in text section - if !program.functions.is_empty() { - output.push_str("\t.text\n"); - for function in &program.functions { - output.push_str(&emit_function(function)); - } - } - - // Emit static variables - for sv in &program.static_vars { - output.push_str(&emit_static_variable(sv)); - } - - if cfg!(target_os = "linux") { - output.push_str("\n.section .note.GNU-stack,\"\",@progbits\n"); - } - output -} diff --git a/src/emit_iced.rs b/src/emit_iced.rs index f5ae97f..505ad1b 100644 --- a/src/emit_iced.rs +++ b/src/emit_iced.rs @@ -29,7 +29,7 @@ //! └─ emit_object_with_labels() — core: assemble + build object file //! └─ emit_function_body() — prologue + instruction loop //! └─ emit_instruction() — encode one instruction, track relocations -//! ├─ gpr32() / gpr64_reg() — register mapping helpers +//! ├─ gpr32() / gpr64_reg() / xmm_reg() — register mapping helpers //! ├─ mem_rbp() — [rbp+offset] memory operands //! └─ make_lbl_ptr() — [label] RIP-relative operands //! @@ -39,7 +39,7 @@ use crate::codegen::{self, AssemblyType, BinaryOp, CondCode, Instruction, Operand, Reg, StaticVariable, UnaryOp}; use crate::tacky::VarInit; -use crate::validate::StaticInt; +use crate::validate::StaticInit; use iced_x86::{BlockEncoderOptions, IcedError, SymbolResolver, SymbolResult, code_asm::*}; use object::write::{ Object, Relocation, RelocationFlags, StandardSection, StandardSegment, Symbol, SymbolFlags, SymbolId, SymbolKind, @@ -87,19 +87,27 @@ impl SymbolResolver for MySymbolResolver { &'_ mut self, instruction: &iced_x86::Instruction, _operand: u32, - _instruction_operand: Option, + instruction_operand: Option, address: u64, _address_size: u32, ) -> Option> { - // First check direct address mapping (functions, internal labels) - if let Some(name) = self.symbols.get(&address) { - return Some(SymbolResult::with_str(address, name)); + use iced_x86::OpKind; + // Only symbolize branch targets and memory references. Immediates must NOT be resolved: + // an immediate's *value* can collide with a code/data address (e.g. `$0` aliasing the + // function at offset 0), which would mis-render `mov $0, ...` as `mov $func, ...`. + match instruction_operand.map(|i| instruction.op_kind(i)) { + // jump/call targets: resolve by target address (functions, internal labels) + Some(OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64) => self + .symbols + .get(&address) + .map(|name| SymbolResult::with_str(address, name)), + // RIP-relative data: resolve by the referencing instruction's IP (static vars / constants) + Some(OpKind::Memory) => self + .reloc_symbols + .get(&instruction.ip()) + .map(|name| SymbolResult::with_str(address, name)), + _ => None, } - // Then check relocation-based symbols (data references) - if let Some(name) = self.reloc_symbols.get(&instruction.ip()) { - return Some(SymbolResult::with_str(address, name)); - } - None } } @@ -237,9 +245,16 @@ fn emit_object_with_labels( } // Create labels for static variables (including extern) to get RIP-relative addressing + // RIP-relative labels for everything referenced via Operand::Data: static variables and + // the double constant pool. let mut data_labels: HashMap, CodeLabel> = HashMap::new(); - for sv in &program.static_vars { - data_labels.insert(sv.name.clone(), a.create_label()); + let data_names = program + .static_vars + .iter() + .map(|sv| &sv.name) + .chain(program.static_constants.iter().map(|sc| &sc.name)); + for name in data_names { + data_labels.insert(name.clone(), a.create_label()); } // Track external function calls (instruction index, function name) @@ -371,10 +386,10 @@ fn emit_object_with_labels( // Defined variable - allocate storage in .data or .bss VarInit::Defined(init_val) => { let (offset, section) = match init_val { - StaticInt::IntInit(0) - | StaticInt::LongInit(0) - | StaticInt::UIntInit(0) - | StaticInt::ULongInit(0) => { + StaticInit::IntInit(0) + | StaticInit::LongInit(0) + | StaticInit::UIntInit(0) + | StaticInit::ULongInit(0) => { // append_section_bss returns the actual offset (after alignment padding) let offset = obj.append_section_bss(bss, *alignment, *alignment); (offset, &bss) @@ -408,6 +423,39 @@ fn emit_object_with_labels( static_var_symbols.insert(name.clone(), sym_id); } + // The `double` constant pool goes in plain read-only data (`.rodata` on ELF, + // `__TEXT,__const` on Mach-O via StandardSection::ReadOnlyData). We already dedup identical + // values within an object file (the codegen ConstantPool keys on `f64::to_bits`). + // + // Possible future optimization: emit into the *mergeable* literal sections so the linker can + // also coalesce identical literals ACROSS object files — `.rodata.cst8`/`.rodata.cst16` + // (SHF_MERGE + sh_entsize 8/16) on ELF, `.literal8`/`.literal16` on Mach-O. + // + // The `object` write API can't express this (checked through 0.39.1 / current `master`): + // `StandardSection` has no mergeable variant, and while `Section.flags` lets us OR in + // SHF_MERGE, there is NO way to set `sh_entsize` — the ELF writer hardcodes it to 0 for all + // non-string sections (see object's write/elf/object.rs, still carrying its own "TODO: maybe + // user should determine this"). SHF_MERGE with entsize 0 is invalid, so a valid `.rodata.cst8` + // is unreachable without forking `object`, post-processing the object, or moving to the + // text-assembler path. Not worth it: the only saving is literals duplicated across separately + // compiled objects, which a whole-program compiler essentially never produces. + let rodata = obj.section_id(StandardSection::ReadOnlyData); + for codegen::StaticConstant { name, init, alignment } in &program.static_constants { + let bytes = init.to_le_bytes(); + let offset = obj.append_section_data(rodata, &bytes, *alignment); + let sym_id = obj.add_symbol(Symbol { + name: name.as_bytes().to_vec(), + value: offset, + size: 8, + kind: SymbolKind::Data, + scope: SymbolScope::Compilation, + weak: false, + section: SymbolSection::Section(rodata), + flags: SymbolFlags::None, + }); + static_var_symbols.insert(name.clone(), sym_id); + } + // Add external function symbols and relocations let mut external_symbols: HashMap, SymbolId> = HashMap::new(); for (_ins_idx, func_name) in &external_calls { @@ -534,6 +582,7 @@ fn gpr32(reg: &Reg) -> AsmRegister32 { R8 => registers::gpr32::r8d, R9 => registers::gpr32::r9d, SP => registers::gpr32::esp, + _ => unreachable!("Only for GP reg"), } } @@ -550,6 +599,24 @@ fn gpr64_reg(reg: &Reg) -> AsmRegister64 { R8 => gpr64::r8, R9 => gpr64::r9, SP => gpr64::rsp, + _ => unreachable!("Only for GP reg"), + } +} + +fn xmm_reg(reg: &Reg) -> AsmRegisterXmm { + use codegen::Reg::*; + match reg { + XMM0 => registers::xmm::xmm0, + XMM1 => registers::xmm::xmm1, + XMM2 => registers::xmm::xmm2, + XMM3 => registers::xmm::xmm3, + XMM4 => registers::xmm::xmm4, + XMM5 => registers::xmm::xmm5, + XMM6 => registers::xmm::xmm6, + XMM7 => registers::xmm::xmm7, + XMM14 => registers::xmm::xmm14, + XMM15 => registers::xmm::xmm15, + _ => unreachable!("Only for XMM reg"), } } @@ -565,14 +632,14 @@ fn mem_rbp(offset: i32, asm_ty: AssemblyType) -> AsmMemoryOperand { }; match asm_ty { AssemblyType::Longword => dword_ptr(pos), - AssemblyType::Quadword => qword_ptr(pos), + AssemblyType::Quadword | AssemblyType::Double => qword_ptr(pos), // movsd reads/writes 8 bytes } } fn make_lbl_ptr(lbl: &CodeLabel, asm_ty: &AssemblyType) -> AsmMemoryOperand { match asm_ty { AssemblyType::Longword => dword_ptr(*lbl), - AssemblyType::Quadword => qword_ptr(*lbl), + AssemblyType::Quadword | AssemblyType::Double => qword_ptr(*lbl), // movsd reads/writes 8 bytes } } @@ -589,6 +656,8 @@ macro_rules! emit_setcc { CondCode::AE => $a.setae($op)?, CondCode::B => $a.setb($op)?, CondCode::BE => $a.setbe($op)?, + CondCode::P => $a.setp($op)?, + CondCode::NP => $a.setnp($op)?, } }; } @@ -623,22 +692,30 @@ fn emit_instruction( (Operand::Reg(s), Operand::Reg(d), AssemblyType::Longword) => a.mov(gpr32(d), gpr32(s))?, (Operand::Reg(s), Operand::Reg(d), AssemblyType::Quadword) => a.mov(gpr64_reg(d), gpr64_reg(s))?, + (Operand::Reg(s), Operand::Reg(d), AssemblyType::Double) => a.movsd_2(xmm_reg(d), xmm_reg(s))?, (Operand::Reg(s), Operand::Stack(off), AssemblyType::Longword) => a.mov(mem_rbp(*off, *size), gpr32(s))?, (Operand::Reg(s), Operand::Stack(off), AssemblyType::Quadword) => { a.mov(mem_rbp(*off, *size), gpr64_reg(s))? } + (Operand::Reg(s), Operand::Stack(off), AssemblyType::Double) => { + a.movsd_2(mem_rbp(*off, *size), xmm_reg(s))? + } (Operand::Stack(off), Operand::Reg(d), AssemblyType::Longword) => a.mov(gpr32(d), mem_rbp(*off, *size))?, (Operand::Stack(off), Operand::Reg(d), AssemblyType::Quadword) => { a.mov(gpr64_reg(d), mem_rbp(*off, *size))? } + (Operand::Stack(off), Operand::Reg(d), AssemblyType::Double) => { + a.movsd_2(xmm_reg(d), mem_rbp(*off, *size))? + } (Operand::Data(name), Operand::Reg(d), _) => { let lbl = data_labels.get(name).unwrap(); data_relocs.push((a.instructions().len(), name.clone())); match size { AssemblyType::Longword => a.mov(gpr32(d), dword_ptr(*lbl))?, AssemblyType::Quadword => a.mov(gpr64_reg(d), qword_ptr(*lbl))?, + AssemblyType::Double => a.movsd_2(xmm_reg(d), qword_ptr(*lbl))?, } } @@ -656,6 +733,7 @@ fn emit_instruction( match size { AssemblyType::Longword => a.mov(dword_ptr(*lbl), gpr32(s))?, AssemblyType::Quadword => a.mov(qword_ptr(*lbl), gpr64_reg(s))?, + AssemblyType::Double => a.movsd_2(qword_ptr(*lbl), xmm_reg(s))?, } } _ => unreachable!("unsupported mov combination: {:?}", ins), @@ -693,11 +771,26 @@ fn emit_instruction( } _ => unreachable!(), }, + UnaryOp::Shr => match (dst, size) { + // shift-by-1 (the round-to-odd step in u64 -> double); only emitted on a GP register + (Operand::Reg(r), AssemblyType::Longword) => a.shr(gpr32(r), 1i32)?, + (Operand::Reg(r), AssemblyType::Quadword) => a.shr(gpr64_reg(r), 1i32)?, + _ => unreachable!(), + }, }, Instruction::Binary { op, src, dst, size } => match op { BinaryOp::Add => match (src, dst, size) { (Operand::Reg(s), Operand::Reg(d), AssemblyType::Longword) => a.add(gpr32(d), gpr32(s))?, (Operand::Reg(s), Operand::Reg(d), AssemblyType::Quadword) => a.add(gpr64_reg(d), gpr64_reg(s))?, + (Operand::Reg(s), Operand::Reg(d), AssemblyType::Double) => a.addsd(xmm_reg(d), xmm_reg(s))?, + (Operand::Stack(off), Operand::Reg(d), AssemblyType::Double) => { + a.addsd(xmm_reg(d), mem_rbp(*off, *size))? + } + (Operand::Data(name), Operand::Reg(d), AssemblyType::Double) => { + let lbl = data_labels.get(name).unwrap(); + data_relocs.push((a.instructions().len(), name.clone())); + a.addsd(xmm_reg(d), qword_ptr(*lbl))? + } (Operand::Imm(v), Operand::Reg(d), AssemblyType::Longword) => a.add(gpr32(d), *v as i32)?, (Operand::Reg(s), Operand::Stack(off), AssemblyType::Longword) => { a.add(mem_rbp(*off, *size), gpr32(s))? @@ -711,6 +804,7 @@ fn emit_instruction( match size { AssemblyType::Longword => a.add(dword_ptr(*lbl), gpr32(s))?, AssemblyType::Quadword => a.add(qword_ptr(*lbl), gpr64_reg(s))?, + AssemblyType::Double => unreachable!(), } } @@ -725,6 +819,15 @@ fn emit_instruction( _ => unreachable!(), }, BinaryOp::Sub => match (src, dst, size) { + (Operand::Reg(s), Operand::Reg(d), AssemblyType::Double) => a.subsd(xmm_reg(d), xmm_reg(s))?, + (Operand::Stack(off), Operand::Reg(d), AssemblyType::Double) => { + a.subsd(xmm_reg(d), mem_rbp(*off, *size))? + } + (Operand::Data(name), Operand::Reg(d), AssemblyType::Double) => { + let lbl = data_labels.get(name).unwrap(); + data_relocs.push((a.instructions().len(), name.clone())); + a.subsd(xmm_reg(d), qword_ptr(*lbl))? + } (Operand::Reg(s), Operand::Reg(d), AssemblyType::Longword) => a.sub(gpr32(d), gpr32(s))?, (Operand::Reg(s), Operand::Reg(d), AssemblyType::Quadword) => a.sub(gpr64_reg(d), gpr64_reg(s))?, (Operand::Imm(v), Operand::Reg(d), AssemblyType::Longword) => a.sub(gpr32(d), *v as i32)?, @@ -740,6 +843,9 @@ fn emit_instruction( match size { AssemblyType::Longword => a.sub(dword_ptr(*lbl), gpr32(s))?, AssemblyType::Quadword => a.sub(qword_ptr(*lbl), gpr64_reg(s))?, + AssemblyType::Double => { + unreachable!("double binary dst is always a register after fix_invalid") + } } } @@ -754,6 +860,15 @@ fn emit_instruction( _ => unreachable!(), }, BinaryOp::Mult => match (src, dst, size) { + (Operand::Reg(s), Operand::Reg(d), AssemblyType::Double) => a.mulsd(xmm_reg(d), xmm_reg(s))?, + (Operand::Stack(off), Operand::Reg(d), AssemblyType::Double) => { + a.mulsd(xmm_reg(d), mem_rbp(*off, *size))? + } + (Operand::Data(name), Operand::Reg(d), AssemblyType::Double) => { + let lbl = data_labels.get(name).unwrap(); + data_relocs.push((a.instructions().len(), name.clone())); + a.mulsd(xmm_reg(d), qword_ptr(*lbl))? + } (Operand::Reg(s), Operand::Reg(d), AssemblyType::Longword) => a.imul_2(gpr32(d), gpr32(s))?, (Operand::Reg(s), Operand::Reg(d), AssemblyType::Quadword) => a.imul_2(gpr64_reg(d), gpr64_reg(s))?, (Operand::Stack(off), Operand::Reg(d), AssemblyType::Longword) => { @@ -768,6 +883,9 @@ fn emit_instruction( match size { AssemblyType::Longword => a.imul_2(gpr32(d), dword_ptr(*lbl))?, AssemblyType::Quadword => a.imul_2(gpr64_reg(d), qword_ptr(*lbl))?, + AssemblyType::Double => { + unreachable!("double binary dst is always a register after fix_invalid") + } } } @@ -780,6 +898,17 @@ fn emit_instruction( } _ => unreachable!("Mult {:?}, {:?}", src, dst), }, + // SSE scalar divide — only ever a double; dst is always a register (fix_invalid) + BinaryOp::DivDouble => match (src, dst) { + (Operand::Reg(s), Operand::Reg(d)) => a.divsd(xmm_reg(d), xmm_reg(s))?, + (Operand::Stack(off), Operand::Reg(d)) => a.divsd(xmm_reg(d), mem_rbp(*off, AssemblyType::Double))?, + (Operand::Data(name), Operand::Reg(d)) => { + let lbl = data_labels.get(name).unwrap(); + data_relocs.push((a.instructions().len(), name.clone())); + a.divsd(xmm_reg(d), qword_ptr(*lbl))? + } + _ => unreachable!("DivDouble {:?}, {:?}", src, dst), + }, BinaryOp::BitAnd => match (src, dst, size) { (Operand::Reg(s), Operand::Reg(d), AssemblyType::Longword) => a.and(gpr32(d), gpr32(s))?, (Operand::Reg(s), Operand::Reg(d), AssemblyType::Quadword) => a.and(gpr64_reg(d), gpr64_reg(s))?, @@ -797,6 +926,9 @@ fn emit_instruction( match size { AssemblyType::Longword => a.and(dword_ptr(*lbl), gpr32(s))?, AssemblyType::Quadword => a.and(qword_ptr(*lbl), gpr64_reg(s))?, + AssemblyType::Double => { + unreachable!("double binary dst is always a register after fix_invalid") + } } } @@ -826,6 +958,9 @@ fn emit_instruction( match size { AssemblyType::Longword => a.or(dword_ptr(*lbl), gpr32(s))?, AssemblyType::Quadword => a.or(qword_ptr(*lbl), gpr64_reg(s))?, + AssemblyType::Double => { + unreachable!("double binary dst is always a register after fix_invalid") + } } } @@ -839,6 +974,15 @@ fn emit_instruction( _ => unreachable!(), }, BinaryOp::BitXOr => match (src, dst, size) { + (Operand::Reg(s), Operand::Reg(d), AssemblyType::Double) => a.xorps(xmm_reg(d), xmm_reg(s))?, + (Operand::Stack(off), Operand::Reg(d), AssemblyType::Double) => { + a.xorps(xmm_reg(d), mem_rbp(*off, *size))? + } + (Operand::Data(name), Operand::Reg(d), AssemblyType::Double) => { + let lbl = data_labels.get(name).unwrap(); + data_relocs.push((a.instructions().len(), name.clone())); + a.xorps(xmm_reg(d), qword_ptr(*lbl))? + } (Operand::Reg(s), Operand::Reg(d), AssemblyType::Longword) => a.xor(gpr32(d), gpr32(s))?, (Operand::Reg(s), Operand::Reg(d), AssemblyType::Quadword) => a.xor(gpr64_reg(d), gpr64_reg(s))?, (Operand::Imm(v), Operand::Reg(d), AssemblyType::Longword) => a.xor(gpr32(d), *v as i32)?, @@ -855,6 +999,9 @@ fn emit_instruction( match size { AssemblyType::Longword => a.xor(dword_ptr(*lbl), gpr32(s))?, AssemblyType::Quadword => a.xor(qword_ptr(*lbl), gpr64_reg(s))?, + AssemblyType::Double => { + unreachable!("double binary dst is always a register after fix_invalid") + } } } @@ -928,6 +1075,8 @@ fn emit_instruction( Instruction::Cmp { v1, v2, size } => match (v1, v2, size) { (Operand::Reg(r1), Operand::Reg(r2), AssemblyType::Longword) => a.cmp(gpr32(r2), gpr32(r1))?, (Operand::Reg(r1), Operand::Reg(r2), AssemblyType::Quadword) => a.cmp(gpr64_reg(r2), gpr64_reg(r1))?, + // comisd: v2 is the xmm register (first operand), v1 the source (fix_invalid forces v2 to a reg) + (Operand::Reg(r1), Operand::Reg(r2), AssemblyType::Double) => a.comisd(xmm_reg(r2), xmm_reg(r1))?, (Operand::Reg(r1), Operand::Stack(off), AssemblyType::Longword) => { a.cmp(mem_rbp(*off, *size), gpr32(r1))? } @@ -940,18 +1089,24 @@ fn emit_instruction( match size { AssemblyType::Longword => a.cmp(dword_ptr(*lbl), gpr32(r1))?, AssemblyType::Quadword => a.cmp(qword_ptr(*lbl), gpr64_reg(r1))?, + // v2 (dst position) is a Data operand — impossible for a double (fix_invalid forces v2 to a reg) + AssemblyType::Double => unreachable!("double comisd second operand is always a register"), } } (Operand::Stack(off), Operand::Reg(r), AssemblyType::Longword) => a.cmp(gpr32(r), mem_rbp(*off, *size))?, (Operand::Stack(off), Operand::Reg(r), AssemblyType::Quadword) => { a.cmp(gpr64_reg(r), mem_rbp(*off, *size))? } + (Operand::Stack(off), Operand::Reg(r), AssemblyType::Double) => { + a.comisd(xmm_reg(r), mem_rbp(*off, *size))? + } (Operand::Data(name), Operand::Reg(r), _) => { let lbl = data_labels.get(name).unwrap(); data_relocs.push((a.instructions().len(), name.clone())); match size { AssemblyType::Longword => a.cmp(gpr32(r), dword_ptr(*lbl))?, AssemblyType::Quadword => a.cmp(gpr64_reg(r), qword_ptr(*lbl))?, + AssemblyType::Double => a.comisd(xmm_reg(r), qword_ptr(*lbl))?, } } (Operand::Imm(v), Operand::Reg(r), AssemblyType::Longword) => a.cmp(gpr32(r), *v as i32)?, @@ -967,6 +1122,7 @@ fn emit_instruction( Instruction::Cdq(size) => match size { AssemblyType::Longword => a.cdq()?, AssemblyType::Quadword => a.cqo()?, + AssemblyType::Double => unreachable!("cdq/cqo are integer-only"), }, Instruction::Idiv(op, size) => match (op, size) { (Operand::Reg(r), AssemblyType::Longword) => a.idiv(gpr32(r))?, @@ -1008,6 +1164,8 @@ fn emit_instruction( CondCode::AE => a.jae(l)?, CondCode::B => a.jb(l)?, CondCode::BE => a.jbe(l)?, + CondCode::P => a.jp(l)?, + CondCode::NP => a.jnp(l)?, }; } Instruction::SetCC { code, op } => match op { @@ -1034,7 +1192,13 @@ fn emit_instruction( Operand::Imm(c) => a.push(*c as i32)?, // i64 handled in fix_invalid Operand::Reg(reg) => a.push(gpr64_reg(reg))?, Operand::Stack(off) => a.push(qword_ptr(gpr64::rbp - (-*off)))?, - Operand::Pseudo(_) | Operand::Data(_) => unreachable!(), + // 8-byte stack arg sourced from .rodata/.data (e.g. a double constant or static var) + Operand::Data(name) => { + let lbl = data_labels.get(name).unwrap(); + data_relocs.push((a.instructions().len(), name.clone())); + a.push(qword_ptr(*lbl))? + } + Operand::Pseudo(_) => unreachable!("pseudo eliminated before emission"), }, Instruction::Call(name) => { if let Some(&lbl) = fn_labels.get(&name.0) { @@ -1052,6 +1216,44 @@ fn emit_instruction( a.db(&[0xE8, 0x00, 0x00, 0x00, 0x00])?; } } + // double -> signed integer. dst is a GP register (width = size, the integer dest type); + // src is the double (xmm or 8-byte memory). dst is always a register after fix_invalid. + Instruction::Cvttsd2si { src, dst, size } => match (src, dst, size) { + (Operand::Reg(s), Operand::Reg(d), AssemblyType::Longword) => a.cvttsd2si(gpr32(d), xmm_reg(s))?, + (Operand::Reg(s), Operand::Reg(d), AssemblyType::Quadword) => a.cvttsd2si(gpr64_reg(d), xmm_reg(s))?, + (Operand::Stack(off), Operand::Reg(d), AssemblyType::Longword) => { + a.cvttsd2si(gpr32(d), mem_rbp(*off, AssemblyType::Double))? + } + (Operand::Stack(off), Operand::Reg(d), AssemblyType::Quadword) => { + a.cvttsd2si(gpr64_reg(d), mem_rbp(*off, AssemblyType::Double))? + } + (Operand::Data(name), Operand::Reg(d), _) => { + let lbl = data_labels.get(name).unwrap(); + data_relocs.push((a.instructions().len(), name.clone())); + match size { + AssemblyType::Longword => a.cvttsd2si(gpr32(d), qword_ptr(*lbl))?, + AssemblyType::Quadword => a.cvttsd2si(gpr64_reg(d), qword_ptr(*lbl))?, + AssemblyType::Double => unreachable!("cvttsd2si destination width is integer, never Double"), + } + } + _ => unreachable!("Cvttsd2si {:?}, {:?}, {:?}", src, dst, size), + }, + // integer -> double. dst is an XMM register; src is the integer (GP reg or memory, width = size). + Instruction::Cvtsi2sd { src, dst, size } => match (src, dst, size) { + (Operand::Reg(s), Operand::Reg(d), AssemblyType::Longword) => a.cvtsi2sd(xmm_reg(d), gpr32(s))?, + (Operand::Reg(s), Operand::Reg(d), AssemblyType::Quadword) => a.cvtsi2sd(xmm_reg(d), gpr64_reg(s))?, + (Operand::Stack(off), Operand::Reg(d), _) => a.cvtsi2sd(xmm_reg(d), mem_rbp(*off, *size))?, + (Operand::Data(name), Operand::Reg(d), _) => { + let lbl = data_labels.get(name).unwrap(); + data_relocs.push((a.instructions().len(), name.clone())); + match size { + AssemblyType::Longword => a.cvtsi2sd(xmm_reg(d), dword_ptr(*lbl))?, + AssemblyType::Quadword => a.cvtsi2sd(xmm_reg(d), qword_ptr(*lbl))?, + AssemblyType::Double => unreachable!("cvtsi2sd source width is integer, never Double"), + } + } + _ => unreachable!("Cvtsi2sd {:?}, {:?}, {:?}", src, dst, size), + }, } Ok(()) } diff --git a/src/lexer.rs b/src/lexer.rs index 950fcc8..6101582 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -17,6 +17,7 @@ //! - Produces a [`VecDeque`] carrying line/column [`Span`]s for error reporting //! - Detects lexer errors (unexpected characters, unterminated comments) and exits with code 10 //! - Promotes integer literals with an `L`/`l` suffix to [`Token::ConstantLong`] +//! - Recognizes floating-point literals (fraction and/or exponent) as [`Token::ConstantDouble`] //! //! ## Call Order //! @@ -39,6 +40,7 @@ pub enum Token { ConstantLong(String), ConstantUnsignedInt(String), ConstantUnsignedLong(String), + ConstantDouble(String), IntKeyword, // int VoidKeyword, // void ReturnKeyword, // return @@ -99,6 +101,7 @@ pub enum Token { LongKeyword, // long SignedKeyword, // signed UnsignedKeyword, // unsigned + DoubleKeyword, // double } const TOKEN_PATTERNS: &[(&str, Token)] = &[ @@ -111,6 +114,10 @@ const TOKEN_PATTERNS: &[(&str, Token)] = &[ r"^[0-9]++([lL][uU]|[uU][lL])\b", Token::ConstantUnsignedLong(String::new()), ), + ( + r"^((?:[0-9]*\.[0-9]+|[0-9]+\.?)[Ee][+-]?[0-9]+|[0-9]*\.[0-9]+|[0-9]+\.)(?:[^\w.]|$)", + Token::ConstantDouble(String::new()), + ), // Keywords (r"^int\b", Token::IntKeyword), (r"^void\b", Token::VoidKeyword), @@ -179,6 +186,7 @@ const TOKEN_PATTERNS: &[(&str, Token)] = &[ (r"^long\b", Token::LongKeyword), (r"^signed\b", Token::SignedKeyword), (r"^unsigned\b", Token::UnsignedKeyword), + (r"^double\b", Token::DoubleKeyword), ]; static TOKEN_DEFS: LazyLock, fn() -> Vec> = LazyLock::new(|| { @@ -239,7 +247,8 @@ pub struct SpannedToken { fn next_token(input: &str, span: Span) -> Result { let mut matches = vec![]; for TokenDef { regex, variant } in TOKEN_DEFS.iter() { - if let Some(mat) = regex.find(input) { + if let Some(caps) = regex.captures(input) { + let mat = caps.get(0).unwrap(); let token = match variant { Token::Identifier(_) => Token::Identifier(mat.as_str().to_string()), Token::ConstantInt(_) => Token::ConstantInt(mat.as_str().to_string()), @@ -255,12 +264,17 @@ fn next_token(input: &str, span: Span) -> Result { let s = mat.as_str(); Token::ConstantUnsignedLong(s[..s.len() - 2].to_string()) } + Token::ConstantDouble(_) => { + let value = caps.get(1).unwrap(); + Token::ConstantDouble(value.as_str().to_string()) + } other => other.clone(), }; - matches.push(TokenMatch { - token, - length: mat.end(), - }); + let length = match variant { + Token::ConstantDouble(_) => caps.get(1).unwrap().end(), + _ => mat.end(), + }; + matches.push(TokenMatch { token, length }); } } if let Some(best_match) = matches.iter().max_by_key(|m| m.length) { diff --git a/src/main.rs b/src/main.rs index 3969deb..2b8fc89 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ mod codegen; -mod emit; mod emit_iced; mod lexer; mod parser; @@ -65,13 +64,13 @@ struct Args { #[arg(long = "static")] static_link: bool, - /// Use text based asm building instead of iced (Deprecated) - #[arg(long)] - no_iced: bool, - #[arg(short = 'o', long)] output: Option, + /// Link against a library, e.g. `-lm` for libm. Forwarded to the linker. + #[arg(short = 'l')] + link_libs: Vec, + /// Input files (required) #[arg(required = true)] filenames: Vec, @@ -139,7 +138,12 @@ fn get_cc_library_paths() -> Vec { /// Build linker arguments for Linux (shared between ld and libwild) #[cfg(target_os = "linux")] -fn build_linux_linker_args(obj_files: &[String], out_file: &str, static_link: bool) -> Vec { +fn build_linux_linker_args( + obj_files: &[String], + out_file: &str, + static_link: bool, + link_libs: &[String], +) -> Vec { let mut args = vec!["-o".to_string(), out_file.to_string()]; if static_link { @@ -161,6 +165,10 @@ fn build_linux_linker_args(obj_files: &[String], out_file: &str, static_link: bo } args.extend(obj_files.iter().cloned()); + // user libraries (-lm, …) before -lc: they may depend on libc, which must follow them + for lib in link_libs { + args.push(format!("-l{lib}")); + } args.push("-lc".to_string()); if static_link { @@ -177,7 +185,7 @@ fn build_linux_linker_args(obj_files: &[String], out_file: &str, static_link: bo /// Link object files using the system linker (ld) #[allow(unused_variables)] -fn link_with_ld(obj_files: &[String], out_file: &str, static_link: bool) { +fn link_with_ld(obj_files: &[String], out_file: &str, static_link: bool, link_libs: &[String]) { let mut cmd = std::process::Command::new("ld"); #[cfg(target_os = "macos")] @@ -201,6 +209,9 @@ fn link_with_ld(obj_files: &[String], out_file: &str, static_link: bool) { } cmd.arg("-lSystem"); + for lib in link_libs { + cmd.arg(format!("-l{lib}")); + } for obj in obj_files { cmd.arg(obj); } @@ -209,7 +220,7 @@ fn link_with_ld(obj_files: &[String], out_file: &str, static_link: bool) { #[cfg(target_os = "linux")] { - for arg in build_linux_linker_args(obj_files, out_file, static_link) { + for arg in build_linux_linker_args(obj_files, out_file, static_link, link_libs) { cmd.arg(arg); } } @@ -223,8 +234,8 @@ fn link_with_ld(obj_files: &[String], out_file: &str, static_link: bool) { /// Link object files using libwild (Linux only, in-process linker) #[cfg(target_os = "linux")] -fn link_with_libwild(obj_files: &[String], out_file: &str, static_link: bool) { - let args_vec = build_linux_linker_args(obj_files, out_file, static_link); +fn link_with_libwild(obj_files: &[String], out_file: &str, static_link: bool, link_libs: &[String]) { + let args_vec = build_linux_linker_args(obj_files, out_file, static_link, link_libs); let args_refs: Vec<&str> = args_vec.iter().map(|s| s.as_str()).collect(); let linker = Linker::new(); @@ -252,8 +263,10 @@ fn main() { // On macOS, always use external linker since libwild doesn't support it let use_external_linker = args.external_linker || cfg!(target_os = "macos"); - // Separate C files from assembly files - let (c_files, asm_files): (Vec<_>, Vec<_>) = args.filenames.iter().partition(|f| f.ends_with(".c")); + // Separate inputs: C sources (compiled), then split the rest into pre-built objects + // (linked directly) and assembly files (assembled with `as`). + let (c_files, rest): (Vec<_>, Vec<_>) = args.filenames.iter().partition(|f| f.ends_with(".c")); + let (obj_inputs, asm_files): (Vec<_>, Vec<_>) = rest.iter().partition(|f| f.ends_with(".o")); // For single-file debug modes (lex, parse, validate, tacky, codegen, -S), // only process the first C file @@ -307,7 +320,7 @@ fn main() { std::process::exit(0); } - let (code_ast, _backend_symbols) = codegen::generate(tacky_ast, &symbols); + let (code_ast, _backend_symbols) = codegen::generate(tacky_ast, &symbols, &mut name_gen); if args.codegen { println!("{}", code_ast.itf_string()); std::process::exit(0); @@ -361,7 +374,7 @@ fn main() { for (sv, init_val) in defined_vars { let is_zero = matches!( init_val, - validate::StaticInt::IntInit(0) | validate::StaticInt::LongInit(0) + validate::StaticInit::IntInit(0) | validate::StaticInit::LongInit(0) ); let section = if is_zero { ".bss" } else { ".data" }; println!("{}", section.cyan()); @@ -377,6 +390,26 @@ fn main() { } } + // Print the read-only `double` constant pool. The stored bytes are the raw IEEE-754 + // bit pattern, so emit `.quad 0x...` (bit-exact) with the decimal value as a comment. + if !code_ast.static_constants.is_empty() { + println!(); + println!("{}", ".section .rodata".cyan()); + for sc in &code_ast.static_constants { + let validate::StaticInit::DoubleInit(d) = sc.init else { + continue; // only doubles live in the constant pool + }; + println!(" {}", format!(".align {}", sc.alignment).dimmed()); + println!("{}:", sc.name.green()); + println!( + " {} {} {}", + ".quad".yellow(), + format!("0x{:016x}", d.to_bits()).cyan(), + format!("# {d}").dimmed() + ); + } + } + std::process::exit(0); } } @@ -408,36 +441,13 @@ fn main() { let (mut name_gen, typed_program, symbols) = validated_result; let tacky_ast = tacky::tackify_program(typed_program, &mut name_gen, &symbols); - let (code_ast, _backend_symbols) = codegen::generate(tacky_ast, &symbols); + let (code_ast, _backend_symbols) = codegen::generate(tacky_ast, &symbols, &mut name_gen); let path = Path::new(c_file); let obj_file = path.with_extension("o").to_string_lossy().to_string(); - if args.no_iced { - let asm = emit::emit_program(&code_ast); - let asm_file = path.with_extension("s").to_string_lossy().to_string(); - fs::write(&asm_file, asm).expect("Failed to write assembly file"); - - let mut cmd = std::process::Command::new("as"); - // On arm64 hosts the system assembler must be told to target x86_64. - #[cfg(target_arch = "aarch64")] - cmd.args(["-arch", "x86_64"]); - let status = cmd - .arg(&asm_file) - .arg("-o") - .arg(&obj_file) - .status() - .expect("Failed to execute as"); - - if !status.success() { - eprintln!("Assembly failed with status: {status}"); - std::process::exit(1); - } - fs::remove_file(&asm_file).ok(); - } else { - let obj = emit_iced::emit_object(&code_ast).expect("iced obj"); - fs::write(&obj_file, &obj).expect("Failed to write object file"); - } + let obj = emit_iced::emit_object(&code_ast).expect("iced obj"); + fs::write(&obj_file, &obj).expect("Failed to write object file"); obj_files.push(obj_file); } @@ -482,15 +492,19 @@ fn main() { std::process::exit(0); } - // Link all object files together + // Link object files together. Pre-built `.o` inputs (e.g. a gcc-built helper) join the link + // set but are NOT cleaned up below — they're caller-owned inputs, not files we produced. + let mut link_objs = obj_files.clone(); + link_objs.extend(obj_inputs.iter().map(|s| s.to_string())); + if use_external_linker { - link_with_ld(&obj_files, &out_file, args.static_link); + link_with_ld(&link_objs, &out_file, args.static_link, &args.link_libs); } else { #[cfg(target_os = "linux")] - link_with_libwild(&obj_files, &out_file, args.static_link); + link_with_libwild(&link_objs, &out_file, args.static_link, &args.link_libs); } - // Clean up object files + // Clean up only the object files we produced (never the caller's .o inputs) for obj in &obj_files { fs::remove_file(obj).ok(); } diff --git a/src/parser.rs b/src/parser.rs index 883a0ca..9fd421a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -45,10 +45,16 @@ use std::rc::Rc; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Identifier(pub Rc); +/// The assembler's local-label prefix for the target platform: `.L` on ELF/Linux, `L` on +/// Mach-O/macOS. Use it for compiler-generated labels (jump targets, `.rodata` constants) so they +/// follow the platform's internal-symbol convention consistently. +pub fn local_label_prefix() -> &'static str { + if cfg!(target_os = "macos") { "L" } else { ".L" } +} + impl fmt::Display for Identifier { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let prefix = if cfg!(target_os = "macos") { "L" } else { ".L" }; - write!(f, "{}{}", prefix, self.0) + write!(f, "{}{}", local_label_prefix(), self.0) } } @@ -99,7 +105,7 @@ pub enum Expr { Constant(Const), Var(Identifier, Span), Cast(Type, Box), - Unary(UnaryOp, Box), + Unary(UnaryOp, Box, Span), Binary(BinOp, Box, Box, Span), Assignment(Box, Box, Span), CompoundAssignment(AssignOp, Box, Box, Span), @@ -116,6 +122,7 @@ pub enum Const { ConstLong(i64), ConstUInt(u32), ConstULong(u64), + ConstDouble(f64), } #[derive(Clone, Copy, Debug, PartialEq)] @@ -210,7 +217,7 @@ impl SwitchIntType { Some(match switch_type { Type::Int | Type::UInt => (raw as i32) as i64, // 32-bit: truncate to low bits, sign-extend Type::Long | Type::ULong => raw, // 64-bit: full pattern - Type::FunType { .. } => unreachable!("Cannot switch on function type"), + Type::FunType { .. } | Type::Double => unreachable!("Cannot switch on non-integer type"), }) } @@ -252,6 +259,7 @@ pub enum Type { Long, UInt, ULong, + Double, FunType { params: Vec, ret: Box, @@ -266,6 +274,7 @@ impl Type { Type::Long => Const::ConstLong(1), Type::UInt => Const::ConstUInt(1), Type::ULong => Const::ConstULong(1), + Type::Double => Const::ConstDouble(1.0), Type::FunType { .. } => unreachable!("Cannot increment function type"), } } @@ -273,7 +282,7 @@ impl Type { pub fn size_bits(&self) -> u32 { match self { Type::Int | Type::UInt => 32, - Type::Long | Type::ULong => 64, + Type::Long | Type::ULong | Type::Double => 64, Type::FunType { .. } => panic!("Function does not have type size"), } } @@ -282,15 +291,21 @@ impl Type { match self { Type::Int | Type::Long => true, Type::ULong | Type::UInt => false, - Type::FunType { .. } => panic!("Function does not have type size"), + Type::FunType { .. } | Type::Double => panic!("called on non-int"), } } + pub fn is_integer(&self) -> bool { + matches!(self, Type::Int | Type::Long | Type::UInt | Type::ULong) + } + /// C usual arithmetic conversions: the common type two operands convert to. /// Same width -> the unsigned type wins; otherwise the wider type wins. pub fn common_with(&self, other: &Type) -> Type { if self == other { self.clone() + } else if *self == Type::Double || *other == Type::Double { + Type::Double } else if self.size_bits() == other.size_bits() { if self.is_signed() { other.clone() } else { self.clone() } } else if self.size_bits() > other.size_bits() { @@ -469,16 +484,20 @@ fn expect(expected: &Token, tokens: &mut VecDeque) -> Result Result { match &token.token { Token::ConstantInt(value_str) => match value_str.parse::() { @@ -515,15 +534,22 @@ fn parse_constant(token: &SpannedToken) -> Result { Some(token.span), )), }, + Token::ConstantDouble(value_str) => { + let val = value_str + .parse::() + .expect("lexer regex guarantees a valid float literal"); + warn_overflow(val, value_str, token.span); + Ok(Expr::Constant(Const::ConstDouble(val))) + } _ => unreachable!(), } } -/// True if `t` is a type-specifier keyword (`int`, `long`, `signed`, `unsigned`). +/// True if `t` is a type-specifier keyword (`int`, `long`, `signed`, `unsigned`, `double`). fn is_type_specifier(t: &Token) -> bool { matches!( t, - Token::IntKeyword | Token::LongKeyword | Token::SignedKeyword | Token::UnsignedKeyword + Token::IntKeyword | Token::LongKeyword | Token::SignedKeyword | Token::UnsignedKeyword | Token::DoubleKeyword ) } @@ -562,7 +588,8 @@ fn parse_factor(tokens: &mut VecDeque) -> Result { + | Token::ConstantUnsignedLong(_) + | Token::ConstantDouble(_) => { tokens.pop_front(); parse_constant(spanned) } @@ -581,13 +608,13 @@ fn parse_factor(tokens: &mut VecDeque) -> Result { let operator = parse_unop(tokens)?; let inner_exp = parse_factor(tokens)?; - Ok(Expr::Unary(operator, Box::from(inner_exp))) + Ok(Expr::Unary(operator, Box::from(inner_exp), spanned.span)) } Token::Increment | Token::Decrement => { tokens.pop_front(); @@ -719,30 +746,35 @@ fn parse_exp(tokens: &mut VecDeque, min_prec: u64) -> Result, err_span: &Option) -> Result { - let (mut ints, mut longs, mut signed, mut unsigned) = (0u32, 0u32, 0u32, 0u32); + if *specifier_list == [Token::DoubleKeyword] { + return Ok(Type::Double); + } + let (mut ints, mut longs, mut signed, mut unsigned, mut double) = (0u32, 0u32, 0u32, 0u32, 0u32); for t in specifier_list { match t { Token::IntKeyword => ints += 1, Token::LongKeyword => longs += 1, Token::SignedKeyword => signed += 1, Token::UnsignedKeyword => unsigned += 1, + Token::DoubleKeyword => double += 1, _ => return Err(SyntaxError::with_span("Non-type specifier".to_string(), *err_span)), } } if specifier_list.is_empty() { return Err(SyntaxError::with_span("Missing type specifier".to_string(), *err_span)); } - if [ints, longs, signed, unsigned].iter().any(|&n| n > 1) { + if [ints, longs, signed, unsigned, double].iter().any(|&n| n > 1) { return Err(SyntaxError::with_span( "Duplicate type specifier".to_string(), *err_span, )); } - if (signed > 0) && (unsigned > 0) { + if ((signed > 0) && (unsigned > 0)) || ((double == 1) && (specifier_list.len() > 1)) { return Err(SyntaxError::with_span( "Invalid type specifier combination".to_string(), *err_span, @@ -758,7 +790,7 @@ fn parse_type(specifier_list: &Vec, err_span: &Option) -> Result infinity, too small -> zero) rather than rejecting it, so this flags the silent +/// value change. Emitted at parse time, where the lexeme is still available — a literal that rounds +/// to zero is indistinguishable from `0.0` once parsed, so it can only be caught here. +fn warn_overflow(value: f64, value_str: &str, span: Span) { + if value.is_infinite() { + eprintln!( + "{}: {}: floating constant {} exceeds range of 'double' (rounded to infinity) {}", + span, + "warning".purple(), + value_str.bold(), + "[-Woverflow]".purple() + ); + } else if value == 0.0 + // Underflow only — a literal whose *significand* has a nonzero digit yet rounds to zero. + // Scan before the exponent so a true zero like `0e10` (nonzero digit only in the exponent) + // isn't mistaken for an underflow. + && value_str + .split(['e', 'E']) + .next() + .is_some_and(|significand| significand.bytes().any(|b| b.is_ascii_digit() && b != b'0')) + { + eprintln!( + "{}: {}: floating constant {} is too small for 'double' (truncated to zero) {}", + span, + "warning".purple(), + value_str.bold(), + "[-Woverflow]".purple() + ); + } +} + /// Guards against declarations appearing where only statements are allowed. /// /// Peeks at the next token and returns an error if it starts a declaration (type or diff --git a/src/pretty.rs b/src/pretty.rs index 141c228..b54608a 100644 --- a/src/pretty.rs +++ b/src/pretty.rs @@ -341,6 +341,7 @@ impl ItfDisplay for Type { Type::Long => Node::leaf("Long".truecolor(MUTED_RED.0, MUTED_RED.1, MUTED_RED.2).to_string()), Type::UInt => Node::leaf("UInt".truecolor(MUTED_RED.0, MUTED_RED.1, MUTED_RED.2).to_string()), Type::ULong => Node::leaf("ULong".truecolor(MUTED_RED.0, MUTED_RED.1, MUTED_RED.2).to_string()), + Type::Double => Node::leaf("Double".truecolor(MUTED_RED.0, MUTED_RED.1, MUTED_RED.2).to_string()), Type::FunType { params, ret, .. } => { let param_types: Vec = params.iter().map(|t| t.itf_node()).collect(); let mut children = vec![Node::branch("return:", vec![ret.itf_node()])]; @@ -379,6 +380,7 @@ impl ItfDisplay for Expr { Const::ConstLong(val) => format!("Long({})", val), Const::ConstUInt(val) => format!("UInt({})", val), Const::ConstULong(val) => format!("ULong({})", val), + Const::ConstDouble(val) => format!("Double({})", val), }; Node::leaf( format!("Constant({})", value_str) @@ -394,7 +396,7 @@ impl ItfDisplay for Expr { Node::branch("expr:", vec![e.itf_node()]), ], ), - Expr::Unary(op, e) => Node::branch( + Expr::Unary(op, e, _span) => Node::branch( format!("Unary ({op:?})").truecolor(TEAL.0, TEAL.1, TEAL.2).to_string(), vec![e.itf_node()], ), @@ -738,6 +740,7 @@ impl ItfDisplay for ValidatedExpr { Const::ConstLong(val) => format!("Long({})", val), Const::ConstUInt(val) => format!("UInt({})", val), Const::ConstULong(val) => format!("ULong({})", val), + Const::ConstDouble(val) => format!("Double({})", val), }; Node::leaf( format!("Constant({})", value_str) @@ -1106,6 +1109,30 @@ impl ItfDisplay for TackyInstruction { val_str(src), val_str(dst) )), + TackyInstruction::DoubleToInt { src, dst } => Node::leaf(format!( + "{} {} -> {}", + "DoubleToInt".truecolor(TEAL.0, TEAL.1, TEAL.2), + val_str(src), + val_str(dst) + )), + TackyInstruction::DoubleToUInt { src, dst } => Node::leaf(format!( + "{} {} -> {}", + "DoubleToUInt".truecolor(TEAL.0, TEAL.1, TEAL.2), + val_str(src), + val_str(dst) + )), + TackyInstruction::IntToDouble { src, dst } => Node::leaf(format!( + "{} {} -> {}", + "IntToDouble".truecolor(TEAL.0, TEAL.1, TEAL.2), + val_str(src), + val_str(dst) + )), + TackyInstruction::UIntToDouble { src, dst } => Node::leaf(format!( + "{} {} -> {}", + "UIntToDouble".truecolor(TEAL.0, TEAL.1, TEAL.2), + val_str(src), + val_str(dst) + )), } } } @@ -1170,6 +1197,7 @@ fn size_str(size: &AssemblyType) -> String { match size { AssemblyType::Longword => "L".truecolor(MUTED_RED.0, MUTED_RED.1, MUTED_RED.2).to_string(), AssemblyType::Quadword => "Q".truecolor(MUTED_RED.0, MUTED_RED.1, MUTED_RED.2).to_string(), + AssemblyType::Double => "D".truecolor(MUTED_RED.0, MUTED_RED.1, MUTED_RED.2).to_string(), } } @@ -1230,6 +1258,20 @@ impl ItfDisplay for CodegenInstruction { operand_str(src), operand_str(dst) )), + CodegenInstruction::Cvttsd2si { src, dst, size } => Node::leaf(format!( + "{}<{}> {} -> {}", + "Cvttsd2si".truecolor(TEAL.0, TEAL.1, TEAL.2), + size_str(size), + operand_str(src), + operand_str(dst) + )), + CodegenInstruction::Cvtsi2sd { src, dst, size } => Node::leaf(format!( + "{}<{}> {} -> {}", + "Cvtsi2sd".truecolor(TEAL.0, TEAL.1, TEAL.2), + size_str(size), + operand_str(src), + operand_str(dst) + )), CodegenInstruction::Unary { op, dst, size } => Node::leaf(format!( "{}<{}> {:?} {}", "Unary".truecolor(TEAL.0, TEAL.1, TEAL.2), diff --git a/src/tacky.rs b/src/tacky.rs index 0d1d6b0..43cc91b 100644 --- a/src/tacky.rs +++ b/src/tacky.rs @@ -30,14 +30,13 @@ //! │ ├─ tackify_var_declaration() — emit local var initializers (skip static/extern) //! │ └─ tackify_stmt() — lower statements to jumps/labels //! │ └─ tackify_expr() — core: flatten expressions to instructions -//! │ └─ emit_cast() — integer conversions (truncate / sign- / zero-extend) +//! │ └─ emit_cast() — scalar conversions (truncate / sign- / zero-extend, int<->double) //! └─ convert_symbols_to_tacky() — extract static vars from symbol table //! ``` -use crate::parser; -use crate::parser::{Const, Identifier, IncDec, SwitchIntType, Type, UnaryOp}; +use crate::parser::{AssignOp, BinOp as ParserBinOp, Const, Identifier, IncDec, SwitchIntType, Type, UnaryOp}; use crate::validate::{ - Block, BlockItem, Expr, ForInit, InitialValue, NameGenerator, StaticInt, Stmt, SymbolTable, TypedDeclaration, + Block, BlockItem, Expr, ForInit, InitialValue, NameGenerator, StaticInit, Stmt, SymbolTable, TypedDeclaration, TypedExpression, TypedFunction, TypedProgram, TypedVarDeclaration, }; use std::cmp::PartialEq; @@ -70,25 +69,25 @@ pub enum BinOp { GreaterOrEqual, } -impl From<&parser::BinOp> for BinOp { - fn from(op: &parser::BinOp) -> Self { +impl From<&ParserBinOp> for BinOp { + fn from(op: &ParserBinOp) -> Self { match op { - parser::BinOp::Add => BinOp::Add, - parser::BinOp::Subtract => BinOp::Subtract, - parser::BinOp::Multiply => BinOp::Multiply, - parser::BinOp::Divide => BinOp::Divide, - parser::BinOp::Remainder => BinOp::Remainder, - parser::BinOp::BitwiseAnd => BinOp::BitwiseAnd, - parser::BinOp::BitwiseOr => BinOp::BitwiseOr, - parser::BinOp::BitwiseXOr => BinOp::BitwiseXOr, - parser::BinOp::BitwiseLeftShift => BinOp::BitwiseLeftShift, - parser::BinOp::BitwiseRightShift => BinOp::BitwiseRightShift, - parser::BinOp::Equal => BinOp::Equal, - parser::BinOp::NotEqual => BinOp::NotEqual, - parser::BinOp::LessThan => BinOp::LessThan, - parser::BinOp::LessOrEqual => BinOp::LessOrEqual, - parser::BinOp::GreaterThan => BinOp::GreaterThan, - parser::BinOp::GreaterOrEqual => BinOp::GreaterOrEqual, + ParserBinOp::Add => BinOp::Add, + ParserBinOp::Subtract => BinOp::Subtract, + ParserBinOp::Multiply => BinOp::Multiply, + ParserBinOp::Divide => BinOp::Divide, + ParserBinOp::Remainder => BinOp::Remainder, + ParserBinOp::BitwiseAnd => BinOp::BitwiseAnd, + ParserBinOp::BitwiseOr => BinOp::BitwiseOr, + ParserBinOp::BitwiseXOr => BinOp::BitwiseXOr, + ParserBinOp::BitwiseLeftShift => BinOp::BitwiseLeftShift, + ParserBinOp::BitwiseRightShift => BinOp::BitwiseRightShift, + ParserBinOp::Equal => BinOp::Equal, + ParserBinOp::NotEqual => BinOp::NotEqual, + ParserBinOp::LessThan => BinOp::LessThan, + ParserBinOp::LessOrEqual => BinOp::LessOrEqual, + ParserBinOp::GreaterThan => BinOp::GreaterThan, + ParserBinOp::GreaterOrEqual => BinOp::GreaterOrEqual, _ => unreachable!("Unsupported binary operator {:#?} in tacky conversion", op), } } @@ -103,35 +102,86 @@ impl From<&IncDec> for BinOp { } } -impl From<&parser::AssignOp> for BinOp { - fn from(op: &parser::AssignOp) -> Self { +impl From<&AssignOp> for BinOp { + fn from(op: &AssignOp) -> Self { match op { - parser::AssignOp::Add => BinOp::Add, - parser::AssignOp::Subtract => BinOp::Subtract, - parser::AssignOp::Multiply => BinOp::Multiply, - parser::AssignOp::Divide => BinOp::Divide, - parser::AssignOp::Remainder => BinOp::Remainder, - parser::AssignOp::BitwiseAnd => BinOp::BitwiseAnd, - parser::AssignOp::BitwiseOr => BinOp::BitwiseOr, - parser::AssignOp::BitwiseXOr => BinOp::BitwiseXOr, - parser::AssignOp::BitwiseLeftShift => BinOp::BitwiseLeftShift, - parser::AssignOp::BitwiseRightShift => BinOp::BitwiseRightShift, + AssignOp::Add => BinOp::Add, + AssignOp::Subtract => BinOp::Subtract, + AssignOp::Multiply => BinOp::Multiply, + AssignOp::Divide => BinOp::Divide, + AssignOp::Remainder => BinOp::Remainder, + AssignOp::BitwiseAnd => BinOp::BitwiseAnd, + AssignOp::BitwiseOr => BinOp::BitwiseOr, + AssignOp::BitwiseXOr => BinOp::BitwiseXOr, + AssignOp::BitwiseLeftShift => BinOp::BitwiseLeftShift, + AssignOp::BitwiseRightShift => BinOp::BitwiseRightShift, } } } +/// A TACKY three-address instruction. +/// +/// TACKY is roughly **LLVM IR without SSA** — a generic three-address code, not a C-specific format +/// (which is why it's the reuse seam for other frontends). The correspondence to LLVM IR: +/// +/// | TACKY | LLVM | +/// |-------|------| +/// | `Return` | `ret` | +/// | `SignExtend` / `ZeroExtend` / `Truncate` | `sext` / `zext` / `trunc` | +/// | `IntToDouble` / `UIntToDouble` | `sitofp` / `uitofp` | +/// | `DoubleToInt` / `DoubleToUInt` | `fptosi` / `fptoui` (saturating variant = `llvm.fptosi.sat` / `.fptoui.sat`) | +/// | `Binary` (arithmetic/bitwise/shift) | `add`/`sub`/`mul`/`sdiv`/`udiv`/`srem`/`urem`/`and`/`or`/`xor`/`shl`/`ashr`/`lshr` (or `fadd`/… for `double`) | +/// | `Binary` (comparison) | `icmp`/`fcmp` **+ `zext i1`** (TACKY yields a full int, not `i1`) | +/// | `Unary::Negate` | `sub 0, x` (int) / `fneg` (double) — LLVM has no integer `neg` | +/// | `Unary::BitwiseComplement` | `xor x, -1` — LLVM has no `not` | +/// | `Unary::Not` | `icmp eq x, 0` **+ `zext i1`** | +/// | `Jump` | unconditional `br` | +/// | `JumpIfZero` / `JumpIfNotZero` | `icmp eq/ne x, 0` **+** conditional `br` | +/// | `Label` | a **basic-block boundary** (LLVM has no label *instruction*; blocks are labels) | +/// | `FunCall` | `call` | +/// | `Copy` | **no equivalent** — an artifact of TACKY's mutable temporaries (see SSA note) | +/// +/// Two structural differences from LLVM IR, both deliberate (TACKY sits *before* SSA): +/// 1. **Not SSA.** Temporaries are reassignable, which is why `Copy` exists at all — in SSA there is +/// nothing to copy, you reference the value. A `Var` here is closer to an LLVM `alloca` slot. +/// A `TACKY → SSA → TACKY` pass would introduce `phi`s and match LLVM's middle-end form. +/// 2. **Flat list + labels, not explicit basic blocks.** `Label`/`Jump` describe the CFG implicitly; +/// LLVM makes basic blocks first-class (each ends in exactly one terminator). #[derive(Clone, Debug)] pub enum Instruction { Return(Val), SignExtend { + // LLVM: sext src: Val, dst: Val, }, Truncate { + // LLVM: trunc src: Val, dst: Val, }, ZeroExtend { + // LLVM: zext + src: Val, + dst: Val, + }, + DoubleToInt { + // LLVM: fptosi (saturating variant = llvm.fptosi.sat) + src: Val, + dst: Val, + }, + DoubleToUInt { + // LLVM: fptoui (saturating variant = llvm.fptoui.sat) + src: Val, + dst: Val, + }, + IntToDouble { + // LLVM: sitofp + src: Val, + dst: Val, + }, + UIntToDouble { + // LLVM: uitofp src: Val, dst: Val, }, @@ -182,7 +232,7 @@ pub struct FunctionDefinition { #[derive(Debug, Clone)] pub enum VarInit { /// Variable defined here with initial value (0 = .bss, non-zero = .data) - Defined(StaticInt), + Defined(StaticInit), /// Extern variable - no storage allocated, resolved by linker Extern, } @@ -201,10 +251,11 @@ pub struct Program { pub static_vars: Vec, } -/// Emits the conversion instruction for casting `src` (`src_type`) into `dst` (`dst_type`). +/// Emits the conversion instruction for casting `src` (`src_type`) into `dst` (`dst_type`), +/// dispatching on whether each side is an integer or `double`. /// -/// The instruction is chosen by comparing widths, with extension keyed on the **source**'s -/// signedness (the value being widened), not the destination's: +/// **Integer → integer** — chosen by width, with extension keyed on the **source**'s signedness +/// (the value being widened), not the destination's: /// - **same width** → `Copy` (a reinterpret, e.g. `int`<->`uint`; no bits change) /// - **narrowing** (dst smaller) → `Truncate` (keep the low bits; signedness-independent) /// - **widening from a signed source** → `SignExtend` (preserves value, e.g. `int -1` -> `long -1`) @@ -212,15 +263,40 @@ pub struct Program { /// /// Keying on the source is what makes mixed-sign widenings correct: `(unsigned long)(-1)` /// sign-extends to `ULONG_MAX`, while `(long)4294967295u` zero-extends to `4294967295`. +/// +/// **`double` → integer** → `DoubleToInt` (signed target) / `DoubleToUInt` (unsigned target). +/// **Integer → `double`** → `IntToDouble` (signed source) / `UIntToDouble` (unsigned source). +/// +/// `double` → `double` cannot reach here: a same-type cast is short-circuited by the caller (the +/// `Expr::Cast` arm of `tackify_expr`) before `emit_cast` runs, so that combination is `unreachable!`. fn emit_cast(src: Val, dst: Val, src_type: &Type, dst_type: &Type, instructions: &mut Vec) { - if src_type.size_bits() == dst_type.size_bits() { - instructions.push(Instruction::Copy { src, dst }); - } else if dst_type.size_bits() < src_type.size_bits() { - instructions.push(Instruction::Truncate { src, dst }); - } else if src_type.is_signed() { - instructions.push(Instruction::SignExtend { src, dst }); - } else { - instructions.push(Instruction::ZeroExtend { src, dst }); + match (src_type.is_integer(), dst_type.is_integer()) { + (true, true) => { + if src_type.size_bits() == dst_type.size_bits() { + instructions.push(Instruction::Copy { src, dst }); + } else if dst_type.size_bits() < src_type.size_bits() { + instructions.push(Instruction::Truncate { src, dst }); + } else if src_type.is_signed() { + instructions.push(Instruction::SignExtend { src, dst }); + } else { + instructions.push(Instruction::ZeroExtend { src, dst }); + } + } + (false, true) => { + if dst_type.is_signed() { + instructions.push(Instruction::DoubleToInt { src, dst }) + } else { + instructions.push(Instruction::DoubleToUInt { src, dst }) + } + } + (true, false) => { + if src_type.is_signed() { + instructions.push(Instruction::IntToDouble { src, dst }) + } else { + instructions.push(Instruction::UIntToDouble { src, dst }) + } + } + _ => unreachable!("Double to double no cast"), } } @@ -270,7 +346,7 @@ fn tackify_expr( }); dst } - Expr::Binary(parser::BinOp::And, e1, e2) => { + Expr::Binary(ParserBinOp::And, e1, e2) => { let src1 = tackify_expr(e1, instructions, name_generator, temp_types); let false_label = Identifier(name_generator.next("and_false")); instructions.push(Instruction::JumpIfZero { @@ -301,7 +377,7 @@ fn tackify_expr( instructions.push(Instruction::Label(end_label)); result } - Expr::Binary(parser::BinOp::Or, e1, e2) => { + Expr::Binary(ParserBinOp::Or, e1, e2) => { let src1 = tackify_expr(e1, instructions, name_generator, temp_types); let false_label = Identifier(name_generator.next("or_false")); instructions.push(Instruction::JumpIfNotZero { @@ -737,6 +813,20 @@ fn tackify_var_declaration( } } +/// Lowers a typed function into its TACKY [`FunctionDefinition`]: tackifies the body into a flat +/// instruction list and collects the types of the temporaries generated along the way. +/// +/// # Synthetic return +/// +/// If the body doesn't already end in a `Return`, one is appended — for **every** function, not just +/// `main`. Codegen emits the terminating `ret` only when it lowers an [`Instruction::Return`], so a +/// function with no trailing return would otherwise fall through into the next function's code. +/// +/// The return *value* is only meaningful for `main`, where C99 §5.1.2.2.3 mandates an implicit +/// `return 0`. For any other function, reaching `}` and using the result is undefined behavior +/// (§6.9.1p12): the standard defines no default, and gcc/clang leave the return register as-is +/// (garbage). NCC instead returns a deterministic `0`/`0.0` of the return type, consistent with its +/// policy of making UB deterministic; the value is unobservable in defined programs. fn tackify_function(func: &TypedFunction, name_generator: &mut NameGenerator) -> FunctionDefinition { let mut instructions = Vec::new(); let mut temp_types = HashMap::new(); @@ -754,7 +844,10 @@ fn tackify_function(func: &TypedFunction, name_generator: &mut NameGenerator) -> let default_return = match ret_type { Type::Int => Val::Constant(Const::ConstInt(0)), Type::Long => Val::Constant(Const::ConstLong(0)), - _ => unreachable!("Function return type must be Int or Long"), + Type::ULong => Val::Constant(Const::ConstULong(0)), + Type::UInt => Val::Constant(Const::ConstUInt(0)), + Type::Double => Val::Constant(Const::ConstDouble(0.0)), + Type::FunType { .. } => unreachable!("function return type cannot be a function type"), }; instructions.push(Instruction::Return(default_return)); } @@ -780,7 +873,7 @@ fn convert_symbols_to_tacky(symbols: &SymbolTable) -> Vec { let mut tacky_defs = vec![]; for (name, entry) in symbols.iter() { match &entry.symbol_type { - Type::Int | Type::Long | Type::UInt | Type::ULong => { + Type::Int | Type::Long | Type::UInt | Type::ULong | Type::Double => { match &entry.val { InitialValue::Initial(static_val) => tacky_defs.push(StaticVariable { name: name.clone(), @@ -790,11 +883,12 @@ fn convert_symbols_to_tacky(symbols: &SymbolTable) -> Vec { }), InitialValue::Tentative => { let zero = match &entry.symbol_type { - Type::Int => StaticInt::IntInit(0), - Type::Long => StaticInt::LongInit(0), - Type::UInt => StaticInt::UIntInit(0), - Type::ULong => StaticInt::ULongInit(0), - Type::FunType { .. } => unreachable!("Tentative must be Int or Long"), + Type::Int => StaticInit::IntInit(0), + Type::Long => StaticInit::LongInit(0), + Type::UInt => StaticInit::UIntInit(0), + Type::ULong => StaticInit::ULongInit(0), + Type::Double => StaticInit::DoubleInit(0.0), + Type::FunType { .. } => unreachable!("a function type cannot have a tentative definition"), }; tacky_defs.push(StaticVariable { name: name.clone(), diff --git a/src/validate.rs b/src/validate.rs index ab50ad7..c31eef2 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -19,7 +19,8 @@ //! - Builds the [`SymbolTable`] mapping identifiers to types, linkage, and initial values //! - Inserts implicit casts via common-type promotion (`int` + `long` -> `long`) //! - Validates function call arity and argument types -//! - Evaluates constant expressions for static initializers and case labels +//! - Evaluates constant expressions for static initializers and case labels (double->int folding +//! mirrors x86 `cvttsd2si`, not Rust's saturating `as` — see [`double_to_i32`]) //! - Emits `-Wdiv-by-zero` for `/` or `%` with a constant zero divisor //! - Emits `-Wshift-count-overflow` / `-Wshift-count-negative` for an out-of-range constant shift count //! - Emits `-Woverflow` when a constant fold leaves the result type (in static initializers / case labels) @@ -183,35 +184,50 @@ impl fmt::Debug for SemanticError { #[derive(Clone, Copy)] pub enum InitialValue { Tentative, - Initial(StaticInt), + Initial(StaticInit), NoInitializer, } -/// A compile-time integer value for static variable initializers. +/// A compile-time constant value (integer or `double`) for static variable initializers. /// /// Carries both the value and its type so that zero-initialized `.bss` vs /// initialized `.data` placement can be decided later in emission. #[derive(Clone, Copy, Debug, PartialEq)] #[allow(clippy::enum_variant_names)] // `Init` suffix = static-initializer value -pub enum StaticInt { +pub enum StaticInit { IntInit(i32), LongInit(i64), UIntInit(u32), ULongInit(u64), + DoubleInit(f64), +} + +impl std::fmt::Display for StaticInit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StaticInit::IntInit(n) => write!(f, "{n}"), + StaticInit::LongInit(n) => write!(f, "{n}"), + StaticInit::UIntInit(n) => write!(f, "{n}"), + StaticInit::ULongInit(n) => write!(f, "{n}"), + StaticInit::DoubleInit(n) => write!(f, "{n}"), + } + } } macro_rules! checked_op { - // unary: checked_op!(self, overflowing_neg; IntInit, LongInit, UIntInit, ULongInit) - ($self:expr, $method:ident; $($V:ident),+ $(,)?) => {{ + // unary: checked_op!(self, overflowing_neg, -; IntInit, LongInit, UIntInit, ULongInit) + ($self:expr, $method:ident; $op:tt; $($V:ident),+ $(,)?) => {{ match $self { - $( StaticInt::$V(v) => { let (r, o) = v.$method(); (StaticInt::$V(r), o) } )+ + StaticInit::DoubleInit(v) => (StaticInit::DoubleInit($op v), false), + $( StaticInit::$V(v) => { let (r, o) = v.$method(); (StaticInit::$V(r), o) } )+ } }}; - // binary: checked_op!(self, other, overflowing_add; IntInit, LongInit, UIntInit, ULongInit) - ($self:expr, $other:expr, $method:ident; $($V:ident),+ $(,)?) => {{ + // binary: checked_op!(self, other, overflowing_add, +; IntInit, LongInit, UIntInit, ULongInit) + ($self:expr, $other:expr, $method:ident; $op:tt; $($V:ident),+ $(,)?) => {{ let (left, right) = $self.get_common($other); match (left, right) { - $( (StaticInt::$V(a), StaticInt::$V(b)) => { let (v, o) = a.$method(b); (StaticInt::$V(v), o) } )+ + (StaticInit::DoubleInit(a), StaticInit::DoubleInit(b)) => (StaticInit::DoubleInit(a $op b), false), + $( (StaticInit::$V(a), StaticInit::$V(b)) => { let (v, o) = a.$method(b); (StaticInit::$V(v), o) } )+ _ => unreachable!("get_common guarantees matching variants"), } }}; @@ -221,7 +237,7 @@ macro_rules! bitwise_op { ($self:expr, $other:expr, $op:tt; $($V:ident),+ $(,)?) => {{ let (left, right) = $self.get_common($other); match (left, right) { - $( (StaticInt::$V(a), StaticInt::$V(b)) => StaticInt::$V(a $op b), )+ + $( (StaticInit::$V(a), StaticInit::$V(b)) => StaticInit::$V(a $op b), )+ _ => unreachable!("get_common guarantees matching variants"), } }}; @@ -231,28 +247,31 @@ macro_rules! compare_op { ($self:expr, $other:expr, $op:tt; $($V:ident),+ $(,)?) => {{ let (left, right) = $self.get_common($other); match (left, right) { - $( (StaticInt::$V(a), StaticInt::$V(b)) => StaticInt::IntInit((a $op b) as i32), )+ + (StaticInit::DoubleInit(a), StaticInit::DoubleInit(b)) => StaticInit::IntInit((a $op b) as i32), + $( (StaticInit::$V(a), StaticInit::$V(b)) => StaticInit::IntInit((a $op b) as i32), )+ _ => unreachable!("get_common guarantees matching variants"), } }}; } -impl StaticInt { +impl StaticInit { fn get_type(&self) -> Type { match self { - StaticInt::IntInit(_) => Type::Int, - StaticInt::LongInit(_) => Type::Long, - StaticInt::UIntInit(_) => Type::UInt, - StaticInt::ULongInit(_) => Type::ULong, + StaticInit::IntInit(_) => Type::Int, + StaticInit::LongInit(_) => Type::Long, + StaticInit::UIntInit(_) => Type::UInt, + StaticInit::ULongInit(_) => Type::ULong, + StaticInit::DoubleInit(_) => Type::Double, } } fn to_const(self) -> Const { match self { - StaticInt::IntInit(i) => Const::ConstInt(i), - StaticInt::LongInit(l) => Const::ConstLong(l), - StaticInt::UIntInit(i) => Const::ConstUInt(i), - StaticInt::ULongInit(l) => Const::ConstULong(l), + StaticInit::IntInit(i) => Const::ConstInt(i), + StaticInit::LongInit(l) => Const::ConstLong(l), + StaticInit::UIntInit(i) => Const::ConstUInt(i), + StaticInit::ULongInit(l) => Const::ConstULong(l), + StaticInit::DoubleInit(d) => Const::ConstDouble(d), } } @@ -261,30 +280,21 @@ impl StaticInt { /// i128/u128 in the same change that adds a 128-bit type, or constants silently truncate. fn wide(self) -> Wide { match self { - StaticInt::IntInit(v) => Wide::Signed(v as i64), - StaticInt::LongInit(v) => Wide::Signed(v), - StaticInt::UIntInit(v) => Wide::Unsigned(v as u64), - StaticInt::ULongInit(v) => Wide::Unsigned(v), + StaticInit::IntInit(v) => Wide::Signed(v as i64), + StaticInit::LongInit(v) => Wide::Signed(v), + StaticInit::UIntInit(v) => Wide::Unsigned(v as u64), + StaticInit::ULongInit(v) => Wide::Unsigned(v), + StaticInit::DoubleInit(_) => unreachable!("only used on integer types"), } } pub(crate) fn to_le_bytes(self) -> Vec { match self { - StaticInt::UIntInit(v) => v.to_le_bytes().to_vec(), - StaticInt::IntInit(v) => v.to_le_bytes().to_vec(), - StaticInt::LongInit(v) => v.to_le_bytes().to_vec(), - StaticInt::ULongInit(v) => v.to_le_bytes().to_vec(), - } - } - - /// Assembler directive (`.long`/`.quad`) and decimal value for a `.data` initializer - /// (text emitter). Width follows the type; the value uses the variant's signedness. - pub(crate) fn data_directive(&self) -> (&'static str, String) { - match self { - StaticInt::IntInit(v) => (".long", v.to_string()), - StaticInt::UIntInit(v) => (".long", v.to_string()), - StaticInt::LongInit(v) => (".quad", v.to_string()), - StaticInt::ULongInit(v) => (".quad", v.to_string()), + StaticInit::UIntInit(v) => v.to_le_bytes().to_vec(), + StaticInit::IntInit(v) => v.to_le_bytes().to_vec(), + StaticInit::LongInit(v) => v.to_le_bytes().to_vec(), + StaticInit::ULongInit(v) => v.to_le_bytes().to_vec(), + StaticInit::DoubleInit(v) => v.to_le_bytes().to_vec(), } } @@ -297,55 +307,62 @@ impl StaticInt { } fn neg(self) -> (Self, bool) { - checked_op!(self, overflowing_neg; IntInit, LongInit, UIntInit, ULongInit) + checked_op!(self, overflowing_neg; -; IntInit, LongInit, UIntInit, ULongInit) } fn add(self, other: Self) -> (Self, bool) { - checked_op!(self, other, overflowing_add; IntInit, LongInit, UIntInit, ULongInit) + checked_op!(self, other, overflowing_add; +; IntInit, LongInit, UIntInit, ULongInit) } fn sub(self, other: Self) -> (Self, bool) { - checked_op!(self, other, overflowing_sub; IntInit, LongInit, UIntInit, ULongInit) + checked_op!(self, other, overflowing_sub; -; IntInit, LongInit, UIntInit, ULongInit) } fn mul(self, other: Self) -> (Self, bool) { - checked_op!(self, other, overflowing_mul; IntInit, LongInit, UIntInit, ULongInit) + checked_op!(self, other, overflowing_mul; *; IntInit, LongInit, UIntInit, ULongInit) } fn is_zero(&self) -> bool { - self.as_i64() == 0 + if let StaticInit::DoubleInit(val) = *self { + val == 0.0 + } else { + self.as_i64() == 0 + } } fn as_i64(&self) -> i64 { match *self { - StaticInt::IntInit(v) => v as i64, - StaticInt::LongInit(v) => v, - StaticInt::UIntInit(v) => v as i64, - StaticInt::ULongInit(v) => v as i64, + StaticInit::IntInit(v) => v as i64, + StaticInit::LongInit(v) => v, + StaticInit::UIntInit(v) => v as i64, + StaticInit::ULongInit(v) => v as i64, + StaticInit::DoubleInit(_) => unreachable!("Integer only helper"), } } fn as_u32(&self) -> u32 { match *self { - StaticInt::IntInit(v) => v as u32, - StaticInt::LongInit(v) => v as u32, - StaticInt::UIntInit(v) => v, - StaticInt::ULongInit(v) => v as u32, + StaticInit::IntInit(v) => v as u32, + StaticInit::LongInit(v) => v as u32, + StaticInit::UIntInit(v) => v, + StaticInit::ULongInit(v) => v as u32, + StaticInit::DoubleInit(_) => unreachable!("Integer only helper"), } } fn div(self, other: Self) -> Result<(Self, bool), ConstEvalError> { - if other.is_zero() { + if !matches!(other, StaticInit::DoubleInit(_)) && other.is_zero() { return Err(ConstEvalError::DivByZero); } - Ok(checked_op!(self, other, overflowing_div; IntInit, LongInit, UIntInit, ULongInit)) + Ok(checked_op!(self, other, overflowing_div; /; IntInit, LongInit, UIntInit, ULongInit)) } fn rem(self, other: Self) -> Result<(Self, bool), ConstEvalError> { if other.is_zero() { return Err(ConstEvalError::DivByZero); } - Ok(checked_op!(self, other, overflowing_rem; IntInit, LongInit, UIntInit, ULongInit)) + // invalid for floating point operations put passing so I can reuse the helper + Ok(checked_op!(self, other, overflowing_rem; /; IntInit, LongInit, UIntInit, ULongInit)) } fn bitwise_and(self, other: Self) -> Self { @@ -364,10 +381,11 @@ impl StaticInt { let shift_amount = other.as_u32(); match self { - StaticInt::IntInit(a) => StaticInt::IntInit(a << (shift_amount & 31)), - StaticInt::LongInit(a) => StaticInt::LongInit(a << (shift_amount & 63)), - StaticInt::UIntInit(a) => StaticInt::UIntInit(a << (shift_amount & 31)), - StaticInt::ULongInit(a) => StaticInt::ULongInit(a << (shift_amount & 63)), + StaticInit::IntInit(a) => StaticInit::IntInit(a << (shift_amount & 31)), + StaticInit::LongInit(a) => StaticInit::LongInit(a << (shift_amount & 63)), + StaticInit::UIntInit(a) => StaticInit::UIntInit(a << (shift_amount & 31)), + StaticInit::ULongInit(a) => StaticInit::ULongInit(a << (shift_amount & 63)), + StaticInit::DoubleInit(_) => unreachable!("Integer only operation"), } } @@ -378,10 +396,11 @@ impl StaticInt { let shift_amount = other.as_u32(); match self { - StaticInt::IntInit(a) => StaticInt::IntInit(a >> (shift_amount & 31)), - StaticInt::LongInit(a) => StaticInt::LongInit(a >> (shift_amount & 63)), - StaticInt::UIntInit(a) => StaticInt::UIntInit(a >> (shift_amount & 31)), - StaticInt::ULongInit(a) => StaticInt::ULongInit(a >> (shift_amount & 63)), + StaticInit::IntInit(a) => StaticInit::IntInit(a >> (shift_amount & 31)), + StaticInit::LongInit(a) => StaticInit::LongInit(a >> (shift_amount & 63)), + StaticInit::UIntInit(a) => StaticInit::UIntInit(a >> (shift_amount & 31)), + StaticInit::ULongInit(a) => StaticInit::ULongInit(a >> (shift_amount & 63)), + StaticInit::DoubleInit(_) => unreachable!("Integer only operation"), } } @@ -410,11 +429,11 @@ impl StaticInt { } fn and(self, other: Self) -> Self { - StaticInt::IntInit(if !self.is_zero() && !other.is_zero() { 1 } else { 0 }) + StaticInit::IntInit(if !self.is_zero() && !other.is_zero() { 1 } else { 0 }) } fn or(self, other: Self) -> Self { - StaticInt::IntInit(if !self.is_zero() || !other.is_zero() { 1 } else { 0 }) + StaticInit::IntInit(if !self.is_zero() || !other.is_zero() { 1 } else { 0 }) } } @@ -529,11 +548,12 @@ fn typecheck_local_variable_declaration( eval_static_initializer(expr, &decl.var_type, &decl.name, decl.span)? } else { let zero = match decl.var_type { - Type::Int => StaticInt::IntInit(0), - Type::Long => StaticInt::LongInit(0), - Type::UInt => StaticInt::UIntInit(0), - Type::ULong => StaticInt::ULongInit(0), - _ => unreachable!("static variable must be int or long"), + Type::Int => StaticInit::IntInit(0), + Type::Long => StaticInit::LongInit(0), + Type::UInt => StaticInit::UIntInit(0), + Type::ULong => StaticInit::ULongInit(0), + Type::Double => StaticInit::DoubleInit(0.0), + _ => unreachable!("static variable must be numeric"), }; InitialValue::Initial(zero) }; @@ -616,7 +636,7 @@ fn typecheck_file_variable_declaration( decl.span, )); } - Type::Int | Type::Long | Type::UInt | Type::ULong => { + Type::Int | Type::Long | Type::UInt | Type::ULong | Type::Double => { if old_dec.symbol_type != decl.var_type { return Err(SemanticError::with_span( format!( @@ -737,7 +757,7 @@ fn walk_region(expr: &ParserExpr, region: &mut HashMap, Span>) { walk_region(rhs, region); } } - ParserExpr::Unary(_, e) | ParserExpr::Cast(_, e) => walk_region(e, region), + ParserExpr::Unary(_, e, _) | ParserExpr::Cast(_, e) => walk_region(e, region), ParserExpr::Conditional(c, t, f) => { check_sequence_points(c); check_sequence_points(t); @@ -774,7 +794,12 @@ fn warn_sequence_point(target: &ParserExpr, span: Span, region: &mut HashMap=` the width of the left operand's type (`int` -> 32, `long` -> 64). fn warn_shift_count(left_type: &Type, rhs: &ParserExpr, span: Span) { - if let Ok((v, _)) = eval_constant_expr(rhs) { + // The double exclusion lives here for two reasons: this runs *before* the rhs is typechecked + // (so the "shift count must be integer" type error hasn't fired yet — see the caller), and + // `as_i64()` just below panics on a `DoubleInit`. A double shift count is rejected as a type + // error elsewhere; here we only need to avoid the meaningless warning and the panic. + if let Ok((v, _)) = eval_constant_expr(rhs) + && !matches!(v, StaticInit::DoubleInit(_)) + { let width: i64 = match left_type { Type::Long => 64, _ => 32, @@ -832,12 +863,17 @@ fn warn_overflow(overflowed: bool, span: Span) { /// `-Wconstant-conversion`: an implicit narrowing conversion of a constant that changed its value /// (e.g. `int x = 0x1FFFFFFFF;`). `changed` is the truncation flag from `convert_to_type`. Explicit /// casts suppress this — the caller only invokes it for implicit conversions. -fn warn_constant_conversion(changed: bool, from: i64, to: i64, span: Span) { +/// +/// Conversions involving `double` never warn here (float↔int narrowing is gcc's separate, deferred +/// `-Wfloat-conversion`); the guard also keeps `as_i64` — an integer-only helper — off double values. +fn warn_constant_conversion(changed: bool, from: &StaticInit, to: &StaticInit, span: Span) { if changed { eprintln!( - "{}: {}: implicit conversion changes constant value from {} to {} {}", + "{}: {}: implicit conversion from {:?} to {:?} changes constant value from {} to {} {}", span, "warning".purple(), + from.get_type(), + to.get_type(), from, to, "[-Wconstant-conversion]".purple() @@ -925,18 +961,26 @@ fn typecheck_exp(exp: ParserExpr, symbols: &mut SymbolTable) -> Result Ok(TypedExpression { + exp_type: Type::Double, + exp: Expr::Const(c), + }), }, ParserExpr::Cast(t, inner) => { let typed_inner = typecheck_exp(*inner, symbols)?; let cast_exp = Expr::Cast(t.clone(), Box::new(typed_inner)); Ok(cast_exp.with_type(t)) } - ParserExpr::Unary(op, inner) => { + ParserExpr::Unary(op, inner, span) => { let typed_inner = typecheck_exp(*inner, symbols)?; let inner_type = typed_inner.exp_type.clone(); let unary_exp = Expr::Unary(op, Box::new(typed_inner)); match op { UnaryOp::Not => Ok(unary_exp.with_type(Type::Int)), + UnaryOp::BitwiseComplement if !inner_type.is_integer() => Err(SemanticError::with_span( + "bitwise complement '~' requires an integer operand".to_string(), + span, + )), _ => Ok(unary_exp.with_type(inner_type)), } } @@ -962,6 +1006,22 @@ fn typecheck_exp(exp: ParserExpr, symbols: &mut SymbolTable) -> Result Result left_type.clone(), @@ -1145,7 +1221,7 @@ fn typecheck_function_declaration( global = old_dec.global; defined = defined || *old_defined; } - Type::Int | Type::Long | Type::UInt | Type::ULong => { + Type::Int | Type::Long | Type::UInt | Type::ULong | Type::Double => { return Err(SemanticError::with_span( format!( "redeclaration of '{}' as a function\n{}: {}: previous declaration was here", @@ -1282,6 +1358,15 @@ fn typecheck_stmt(stmt: ParserStmt, symbols: &mut SymbolTable, ret_type: &Type) let typed_e = typecheck_exp(e, symbols)?; let switch_type = &typed_e.exp_type; + // The controlling expression must have integer type (C §6.8.4.2). Reject `double` here, + // before `as_i64` normalizes case values against it (that would otherwise panic). + if !switch_type.is_integer() { + return Err(SemanticError { + message: "switch controlling expression must have integer type".to_string(), + span: None, + }); + } + // Normalize cases and check for duplicates let mut normalized: HashMap, Span> = HashMap::new(); for (case, case_span) in cases.iter() { @@ -1380,7 +1465,7 @@ fn resolve_exp( *span, )), }, - ParserExpr::Unary(_, e) => resolve_exp(e, variable_map, used_vars), + ParserExpr::Unary(_, e, _) => resolve_exp(e, variable_map, used_vars), ParserExpr::Binary(_, left, right, _) => { resolve_exp(left, variable_map, used_vars)?; resolve_exp(right, variable_map, used_vars) @@ -1725,7 +1810,7 @@ fn resolve_statement( /// Wide-enough exact value of any integer constant, split by signedness so each half maps to a /// lossless std primitive (no single primitive holds both `u128::MAX` and negatives). Today the -/// widest StaticInt variants are i64/u64, so those suffice — see [`StaticInt::wide`] for the +/// widest StaticInt variants are i64/u64, so those suffice — see [`StaticInit::wide`] for the /// invariant on widening this to i128/u128. #[derive(Clone, Copy)] enum Wide { @@ -1733,15 +1818,70 @@ enum Wide { Unsigned(u64), } +// Double -> integer conversion, hand-rolled to match x86 `cvttsd2si` — the instruction codegen +// emits, so a compile-time fold of e.g. `(int)1e20` must agree with the same cast at runtime. +// +// Rust's standard library can't do this for us: +// 1. There is no fallible float->int conversion — `TryFrom for i32` (etc.) doesn't exist — +// so we can't ask "does this double fit?"; we range-check by hand. +// 2. `f64::to_int_unchecked` exists but is undefined behavior out of range, so it can't be turned +// loose on arbitrary constants during folding. +// +// That leaves the `as` cast — but `as` does NOT match the hardware. Rust's float->int `as` +// SATURATES: an out-of-range value clamps to the target's MIN/MAX and NaN becomes 0. `cvttsd2si` +// instead yields the "integer indefinite" value — the target's MIN bit pattern (e.g. INT_MIN) — for +// *every* invalid input: positive overflow, ±infinity, and NaN alike. They agree only when the +// truncated value is already in range. So: explicit range/NaN check returning the indefinite value +// on failure, and plain `as` (which truncates toward zero, matching cvttsd2si) on success. +// +// `double_to_u32`/`u64` build on the signed path because `cvttsd2si` is signed-only (see each fn). +fn double_to_i32(d: f64) -> i32 { + // cvttsd2si: valid iff trunc(d) fits in i32; NaN/inf/overflow -> indefinite (INT_MIN) + if d.is_nan() || d < i32::MIN as f64 || d >= 2147483648.0 + /* 2^31 */ + { + i32::MIN + } else { + d as i32 // in range: truncates toward zero, matches cvttsd2si + } +} + +fn double_to_i64(d: f64) -> i64 { + // cvttsd2si: valid iff trunc(d) fits in i64; NaN/inf/overflow -> indefinite (INT_MIN) + if d.is_nan() || d < i64::MIN as f64 || d >= 9223372036854775808.0 + /* 2^63 */ + { + i64::MIN + } else { + d as i64 // in range: truncates toward zero, matches cvttsd2si + } +} + +fn double_to_u64(d: f64) -> u64 { + const TWO_POW_63: f64 = 9223372036854775808.0; // 2^63 + if d < TWO_POW_63 { + double_to_i64(d) as u64 // fits signed range + } else { + // subtract 2^63, convert the (now in-range) remainder, restore the top bit + (double_to_i64(d - TWO_POW_63) as u64).wrapping_add(1u64 << 63) + } +} + +fn double_to_u32(d: f64) -> u32 { + // u32's full range fits in i64, so convert via the signed 64-bit path and truncate. + double_to_i64(d) as u32 +} + /// Casts a normalized value (`i64`/`u64`) to each target type, yielding `(result, out_of_range)` /// where `out_of_range` means the exact value can't be represented in the target's range. macro_rules! to_target { ($v:expr, $target:expr) => { match $target { - Type::Int => (StaticInt::IntInit($v as i32), i32::try_from($v).is_err()), - Type::UInt => (StaticInt::UIntInit($v as u32), u32::try_from($v).is_err()), - Type::Long => (StaticInt::LongInit($v as i64), i64::try_from($v).is_err()), - Type::ULong => (StaticInt::ULongInit($v as u64), u64::try_from($v).is_err()), + Type::Int => (StaticInit::IntInit($v as i32), i32::try_from($v).is_err()), + Type::UInt => (StaticInit::UIntInit($v as u32), u32::try_from($v).is_err()), + Type::Long => (StaticInit::LongInit($v as i64), i64::try_from($v).is_err()), + Type::ULong => (StaticInit::ULongInit($v as u64), u64::try_from($v).is_err()), + Type::Double => (StaticInit::DoubleInit($v as f64), false), Type::FunType { .. } => unreachable!("Cannot cast to function type in constant expression"), } }; @@ -1755,7 +1895,17 @@ macro_rules! to_target { // `unnecessary_cast`/`useless_conversion`: the i64/u64 normalizer coincides with the Long/ULong // targets today, so those arms are identity casts; they become genuine narrowings once Wide is i128/u128. #[allow(clippy::unnecessary_cast, clippy::useless_conversion)] -fn convert_to_type(val: StaticInt, target_type: &Type) -> (StaticInt, bool) { +fn convert_to_type(val: StaticInit, target_type: &Type) -> (StaticInit, bool) { + if let StaticInit::DoubleInit(v) = val { + return match target_type { + Type::Int => (StaticInit::IntInit(double_to_i32(v)), false), + Type::Long => (StaticInit::LongInit(double_to_i64(v)), false), + Type::UInt => (StaticInit::UIntInit(double_to_u32(v)), false), + Type::ULong => (StaticInit::ULongInit(double_to_u64(v)), false), + Type::Double => (val, false), + Type::FunType { .. } => unreachable!(), + }; + } let source_bits = val.get_type().size_bits(); let (result, out_of_range) = match val.wide() { Wide::Signed(v) => to_target!(v, target_type), @@ -1768,11 +1918,13 @@ fn convert_to_type(val: StaticInt, target_type: &Type) -> (StaticInt, bool) { enum ConstEvalError { NotConstant, DivByZero, + InvalidType, } -/// Evaluate a static / file-scope initializer to its `InitialValue`, mapping the two -/// constant-eval failures to located diagnostics. Shared by the local-`static` and -/// file-scope declaration paths so their error messages stay in sync. +/// Evaluate a static / file-scope initializer to its `InitialValue`, mapping the +/// constant-eval failures (not-constant, division by zero, invalid-type op like `~` on a +/// `double`) to located diagnostics. Shared by the local-`static` and file-scope declaration +/// paths so their error messages stay in sync. fn eval_static_initializer( expr: &ParserExpr, var_type: &Type, @@ -1784,7 +1936,7 @@ fn eval_static_initializer( warn_overflow(overflowed, span); // Implicit narrowing conversion to the declared type: -Wconstant-conversion. let (converted, truncated) = convert_to_type(c, var_type); - warn_constant_conversion(truncated, c.as_i64(), converted.as_i64(), span); + warn_constant_conversion(truncated, &c, &converted, span); Ok(InitialValue::Initial(converted)) } Err(ConstEvalError::DivByZero) => Err(SemanticError::with_span( @@ -1798,6 +1950,10 @@ fn eval_static_initializer( ), span, )), + Err(ConstEvalError::InvalidType) => Err(SemanticError::with_span( + format!("cannot take bitwise complement of a {} value", "'double'".bold()), + span, + )), } } @@ -1818,12 +1974,13 @@ fn eval_static_initializer( /// Returns `Err(ConstEvalError::NotConstant)` if the expression contains non-constant /// elements (variables, function calls, assignments, etc.), or /// `Err(ConstEvalError::DivByZero)` if a `/` or `%` has a zero divisor. -fn eval_constant_expr(expr: &ParserExpr) -> Result<(StaticInt, bool), ConstEvalError> { +fn eval_constant_expr(expr: &ParserExpr) -> Result<(StaticInit, bool), ConstEvalError> { match expr { - ParserExpr::Constant(Const::ConstInt(val)) => Ok((StaticInt::IntInit(*val), false)), - ParserExpr::Constant(Const::ConstLong(val)) => Ok((StaticInt::LongInit(*val), false)), - ParserExpr::Constant(Const::ConstUInt(val)) => Ok((StaticInt::UIntInit(*val), false)), - ParserExpr::Constant(Const::ConstULong(val)) => Ok((StaticInt::ULongInit(*val), false)), + ParserExpr::Constant(Const::ConstInt(val)) => Ok((StaticInit::IntInit(*val), false)), + ParserExpr::Constant(Const::ConstLong(val)) => Ok((StaticInit::LongInit(*val), false)), + ParserExpr::Constant(Const::ConstUInt(val)) => Ok((StaticInit::UIntInit(*val), false)), + ParserExpr::Constant(Const::ConstULong(val)) => Ok((StaticInit::ULongInit(*val), false)), + ParserExpr::Constant(Const::ConstDouble(val)) => Ok((StaticInit::DoubleInit(*val), false)), ParserExpr::Cast(target, val) => { let (v, o) = eval_constant_expr(val)?; // Explicit cast: suppress the conversion-truncation warning (programmer intent), but @@ -1831,20 +1988,21 @@ fn eval_constant_expr(expr: &ParserExpr) -> Result<(StaticInt, bool), ConstEvalE let (cv, _truncated) = convert_to_type(v, target); Ok((cv, o)) } - ParserExpr::Unary(op, inner) => { + ParserExpr::Unary(op, inner, _) => { let (v, o) = eval_constant_expr(inner)?; let (r, o2) = match op { UnaryOp::Negate => v.neg(), UnaryOp::BitwiseComplement => ( match v { - StaticInt::IntInit(n) => StaticInt::IntInit(!n), - StaticInt::LongInit(n) => StaticInt::LongInit(!n), - StaticInt::ULongInit(n) => StaticInt::ULongInit(!n), - StaticInt::UIntInit(n) => StaticInt::UIntInit(!n), + StaticInit::IntInit(n) => StaticInit::IntInit(!n), + StaticInit::LongInit(n) => StaticInit::LongInit(!n), + StaticInit::ULongInit(n) => StaticInit::ULongInit(!n), + StaticInit::UIntInit(n) => StaticInit::UIntInit(!n), + StaticInit::DoubleInit(_) => return Err(ConstEvalError::InvalidType), }, false, ), - UnaryOp::Not => (StaticInt::IntInit(v.is_zero() as i32), false), + UnaryOp::Not => (StaticInit::IntInit(v.is_zero() as i32), false), }; // Unsigned wraparound is well-defined, not overflow — only signed ops warn (-Woverflow). let o2 = o2 && r.get_type().is_signed(); @@ -1853,6 +2011,19 @@ fn eval_constant_expr(expr: &ParserExpr) -> Result<(StaticInt, bool), ConstEvalE ParserExpr::Binary(op, left, right, _) => { let (l, lo) = eval_constant_expr(left)?; let (r, ro) = eval_constant_expr(right)?; + if (matches!(l, StaticInit::DoubleInit(_)) || matches!(r, StaticInit::DoubleInit(_))) + && matches!( + op, + BinOp::Remainder + | BinOp::BitwiseAnd + | BinOp::BitwiseOr + | BinOp::BitwiseXOr + | BinOp::BitwiseLeftShift + | BinOp::BitwiseRightShift + ) + { + return Err(ConstEvalError::InvalidType); + } let base = lo | ro; let (v, op_ovf) = match op { BinOp::Add => l.add(r), @@ -1940,17 +2111,25 @@ impl LabelTracker { } } - fn get_switch_case(&mut self, c: StaticInt, span: &Span) -> Result, SemanticError> { + fn get_switch_case(&mut self, c: StaticInit, span: &Span) -> Result, SemanticError> { // get the active switch id if let Some(LabelTag::Switch(label)) = self.cur_label.iter().rev().find(|x| match x { LabelTag::Switch(..) => true, LabelTag::Loop(..) => false, }) { let case_exp = match c { - StaticInt::IntInit(v) => SwitchIntType::Int(v), - StaticInt::LongInit(v) => SwitchIntType::Long(v), - StaticInt::UIntInit(v) => SwitchIntType::UInt(v), - StaticInt::ULongInit(v) => SwitchIntType::ULong(v), + StaticInit::IntInit(v) => SwitchIntType::Int(v), + StaticInit::LongInit(v) => SwitchIntType::Long(v), + StaticInit::UIntInit(v) => SwitchIntType::UInt(v), + StaticInit::ULongInit(v) => SwitchIntType::ULong(v), + // Case labels are folded here in Pass 1, before typecheck runs, so a double case + // (`case 1.0:`) must be rejected here — not left to Pass 2 — or it would panic. + StaticInit::DoubleInit(_) => { + return Err(SemanticError::with_span( + "case label must be an integer constant, not a double".to_string(), + *span, + )); + } }; // Just collect cases with spans - duplicate checking happens during typecheck self.switch_to_cases.get_mut(label).unwrap().push((case_exp, *span)); @@ -2077,6 +2256,12 @@ fn label_statement(stmt: &mut SpannedStmt, label_tracker: &mut LabelTracker) -> stmt.span, )); } + Err(ConstEvalError::InvalidType) => { + return Err(SemanticError::with_span( + format!("cannot take bitwise complement of a {} value", "'double'".bold()), + stmt.span, + )); + } }; *label = label_tracker.get_switch_case(value, &stmt.span)?; label_statement(s, label_tracker) diff --git a/tests/c_programs/conversions/double_to_int_edges.c b/tests/c_programs/conversions/double_to_int_edges.c new file mode 100644 index 0000000..66addc2 --- /dev/null +++ b/tests/c_programs/conversions/double_to_int_edges.c @@ -0,0 +1,38 @@ +/* NCC-specific deterministic edge: converting an out-of-range, +infinity, or NaN `double` to a + signed integer is undefined behavior in standard C (so the book's suite can't test it). NCC + defines it as the x86 `cvttsd2si` "integer indefinite" = the target type's minimum value. + + Checks use discriminating comparisons (not bare casts returned directly) because the process + exit code is only 8 bits — INT_MIN/LONG_MIN have a low byte of 0, which would otherwise be + indistinguishable from a (wrong) result of 0. Returns the number of checks that held; all + conversions below must yield the type minimum, so the expected result is 7. */ + +int int_min(void) { + return -2147483647 - 1; // INT_MIN, built without an out-of-range literal +} + +long long_min(void) { + return -9223372036854775807L - 1L; // LONG_MIN +} + +int main(void) { + int passed = 0; + + double big = 1e30; // far above INT/LONG range + double neg_big = -1e30; // far below + double inf = 2e308; // rounds to +infinity (also fires -Woverflow at compile time) + double nan = 0.0 / 0.0; // NaN + + // double -> int : every out-of-range / inf / NaN value is the "indefinite" INT_MIN + if ((int) big == int_min()) passed = passed + 1; + if ((int) neg_big == int_min()) passed = passed + 1; + if ((int) inf == int_min()) passed = passed + 1; + if ((int) nan == int_min()) passed = passed + 1; + + // double -> long : likewise LONG_MIN + if ((long) big == long_min()) passed = passed + 1; + if ((long) inf == long_min()) passed = passed + 1; + if ((long) nan == long_min()) passed = passed + 1; + + return passed; // expect 7 +} diff --git a/tests/c_programs/expected_results.json b/tests/c_programs/expected_results.json index 855e56a..9329b9a 100644 --- a/tests/c_programs/expected_results.json +++ b/tests/c_programs/expected_results.json @@ -1,4 +1,7 @@ { + "unsigned/implicit_return_falloff.c": { + "return_code": 2 + }, "unsigned/widening_casts.c": { "return_code": 2 }, @@ -110,5 +113,8 @@ }, "types/compound_assign_mixed_types.c": { "return_code": 30 + }, + "conversions/double_to_int_edges.c": { + "return_code": 7 } } \ No newline at end of file diff --git a/tests/c_programs/unsigned/implicit_return_falloff.c b/tests/c_programs/unsigned/implicit_return_falloff.c new file mode 100644 index 0000000..a2ca637 --- /dev/null +++ b/tests/c_programs/unsigned/implicit_return_falloff.c @@ -0,0 +1,41 @@ +/* Regression (pre-existing since ch12 unsigned): a non-main function with an unsigned return type + that falls off the end used to PANIC the tackifier — the synthetic-return match only handled + int/long, so unsigned int / unsigned long hit `unreachable!`. + + This must compile, and the synthetic return must emit a real `ret` so control returns to the + caller instead of falling through into the next function. Per C §6.9.1p12 the fall-off return + *value* is undefined, so this test never observes it: the fall-off calls are made for their + control-flow effect and their results are ignored, and the final answer comes only from the + normal (explicit-return) paths. */ +unsigned int counter = 0; + +unsigned int bump(int run) { + if (run) { + counter = counter + 1u; + return counter; + } + /* run == 0: reaches the closing brace with no return */ +} + +unsigned long bump_long(int run) { + if (run) { + return 7ul; + } + /* run == 0: falls off the end */ +} + +double bump_double(int run) { + if (run) { + return 3.5; + } + /* run == 0: falls off the end — synthetic return must target XMM0, not RAX */ +} + +int main(void) { + bump(0); /* fall-off path; result ignored (would be UB to use) */ + bump_long(0); /* fall-off path; result ignored */ + bump_double(0); /* fall-off path on a double-returning fn; result ignored */ + bump(1); /* normal path: counter -> 1 */ + bump(1); /* normal path: counter -> 2 */ + return (int)counter; /* defined: 2 */ +} diff --git a/tests/c_programs/warnings/float_overflow.c b/tests/c_programs/warnings/float_overflow.c new file mode 100644 index 0000000..63fea26 --- /dev/null +++ b/tests/c_programs/warnings/float_overflow.c @@ -0,0 +1,12 @@ +/* -Woverflow: floating-point literals that can't be represented in `double`. Unlike integer + constants (which error when they don't fit even a 64-bit type), an out-of-range floating + literal is never an error — NCC rounds it like every real implementation and warns instead. + The program still compiles and runs cleanly to 0. + + gcc rolls both directions into -Woverflow: + 1e400 -> "floating constant exceeds range of 'double'" (rounds to +infinity) + 1e-400 -> "floating constant truncated to zero" (underflows to 0.0) */ +double huge = 1e400; /* overflow -> +infinity */ +double tiny = 1e-400; /* underflow -> 0.0 */ + +int main(void) { return 0; } diff --git a/tests/c_programs/warnings/no_float_overflow.c b/tests/c_programs/warnings/no_float_overflow.c new file mode 100644 index 0000000..31edd0a --- /dev/null +++ b/tests/c_programs/warnings/no_float_overflow.c @@ -0,0 +1,10 @@ +/* In-range floating literals must NOT trigger -Woverflow. This guards both edges of the check: + values at the extremes of `double` round exactly to themselves, and a literal that legitimately + equals zero must stay quiet — the underflow warning only fires when the lexeme has a nonzero + digit, so a true zero (`0.0`, `0e10`) is not mistaken for an underflow. Runs cleanly to 0. */ +double max = 1.7976931348623157e308; /* ~DBL_MAX — fits */ +double min_normal = 2.2250738585072014e-308; /* smallest normal double — fits */ +double zero = 0.0; /* genuine zero — must not warn */ +double zero_exp = 0e10; /* genuine zero with exponent — must not warn */ + +int main(void) { return 0; } diff --git a/tests/runner.rs b/tests/runner.rs index 68a2c33..1cfc15c 100644 --- a/tests/runner.rs +++ b/tests/runner.rs @@ -1,10 +1,10 @@ use glob::glob; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fs; use std::path::PathBuf; -static CHAPTER_COMPLETED: i32 = 12; -static EXTRA_COMPLETED: i32 = 12; +static CHAPTER_COMPLETED: i32 = 13; +static EXTRA_COMPLETED: i32 = 13; #[derive(Debug, PartialEq, Clone)] enum ProgramOutput { @@ -21,6 +21,8 @@ struct TestCase { c_file: String, extra_files: Vec, // Library files or assembly files output: ProgramOutput, + requires_mathlib: bool, // needs `-lm` at link time (libm) + lib_deps: Vec, // helper-lib .c files (gcc-built) to link in (from the `libs` mapping) } /// Accumulated pass/fail tallies for a single case (a case may run several sub-tests). @@ -68,6 +70,41 @@ fn load_assembly_libs() -> HashMap> { result } +/// Load `requires_mathlib` (relative paths of tests that must link libm) from test_properties.json. +/// Mirrors the book framework's REQUIRES_MATHLIB; keyed the same way (the `_client` suffix stripped). +fn load_requires_mathlib() -> HashSet { + let json_content = fs::read_to_string("writing-a-c-compiler-tests/test_properties.json").unwrap_or_default(); + let parsed: serde_json::Value = serde_json::from_str(&json_content).unwrap_or_default(); + parsed + .get("requires_mathlib") + .and_then(|v| v.as_array()) + .map(|arr| arr.iter().filter_map(|v| v.as_str()).map(String::from).collect()) + .unwrap_or_default() +} + +/// Load the `libs` mapping from test_properties.json: test path -> helper-lib `.c` dependencies +/// (e.g. `helper_libs/nan.c`). These are gcc-built and linked into the test, since they `#include` +/// system headers NCC can't preprocess. Values are resolved to full paths under the tests dir. +fn load_libs() -> HashMap> { + let json_content = fs::read_to_string("writing-a-c-compiler-tests/test_properties.json").unwrap_or_default(); + let parsed: serde_json::Value = serde_json::from_str(&json_content).unwrap_or_default(); + + let mut result = HashMap::new(); + if let Some(libs) = parsed.get("libs").and_then(|v| v.as_object()) { + for (test_path, deps) in libs { + if let Some(deps_arr) = deps.as_array() { + let dep_paths: Vec = deps_arr + .iter() + .filter_map(|v| v.as_str()) + .map(|s| format!("writing-a-c-compiler-tests/tests/{}", s)) + .collect(); + result.insert(test_path.clone(), dep_paths); + } + } + } + result +} + // we want a mapping of test path to program output fn get_sandler_cases() -> Vec { // Load expected results for valid tests @@ -77,6 +114,12 @@ fn get_sandler_cases() -> Vec { // Load assembly libs configuration let assembly_libs = load_assembly_libs(); + // Load the set of tests that must link libm + let requires_mathlib = load_requires_mathlib(); + + // Load helper-lib dependencies (test -> [helper .c files], gcc-built and linked in) + let libs = load_libs(); + let mut cases = vec![]; for entry in glob("writing-a-c-compiler-tests/tests/**/*.c") @@ -98,6 +141,13 @@ fn get_sandler_cases() -> Vec { continue; } + // Skip helper libraries: these are dependencies linked into other tests (via the `libs` + // mapping), not standalone tests. They `#include` system headers NCC can't preprocess and + // are always built by gcc, then linked into the test that depends on them. + if path_str.contains("/helper_libs/") { + continue; + } + // Determine the ProgramOutput based on the test type let output = if path_str.contains("invalid_lex") { ProgramOutput::Error(10) @@ -148,11 +198,19 @@ fn get_sandler_cases() -> Vec { extra_files.extend(asm_files.clone()); } + // mathlib and lib deps are keyed like the book's props: a `_client.c` test maps to its + // `.c` library name + let props_key = relative_path.replace("_client.c", ".c"); + let needs_mathlib = requires_mathlib.contains(&props_key); + let lib_deps = libs.get(&props_key).cloned().unwrap_or_default(); + if chapter <= CHAPTER_COMPLETED && (!extra_credit || (chapter <= EXTRA_COMPLETED)) { cases.push(TestCase { c_file: path_str.to_string(), extra_files, output, + requires_mathlib: needs_mathlib, + lib_deps, }) } } @@ -189,6 +247,8 @@ fn get_custom_cases() -> Vec { c_file: path_str.to_string(), extra_files: vec![], output, + requires_mathlib: false, + lib_deps: vec![], }) } cases @@ -278,6 +338,12 @@ fn run_test(case: &TestCase, result: &mut CaseResult, extra_args: &[String], tes cmd.arg("-o").arg(binary_path_str); + // libm is needed only on Linux; macOS provides the math symbols via libSystem (matches the + // book framework, which skips -lm on OSX). + if case.requires_mathlib && !cfg!(target_os = "macos") { + cmd.arg("-lm"); + } + for arg in extra_args { cmd.arg(arg); } @@ -301,6 +367,11 @@ fn run_test(case: &TestCase, result: &mut CaseResult, extra_args: &[String], tes ProgramOutput::Error(compile_output.status.code().unwrap_or(-1)) }; + record(case, actual, result, test_label); +} + +/// Compares a test's actual outcome against its expected `output` and records pass/fail. +fn record(case: &TestCase, actual: ProgramOutput, result: &mut CaseResult, test_label: &str) { let passed = match (&actual, &case.output) { (ProgramOutput::Error(a), ProgramOutput::Error(b)) => a == b, ( @@ -347,6 +418,74 @@ fn run_test(case: &TestCase, result: &mut CaseResult, extra_args: &[String], tes } } +/// Runs a test that depends on helper libraries (the `libs` mapping). Each helper `.c` is compiled +/// by gcc (it `#include`s system headers NCC can't preprocess); then NCC compiles the test and +/// **links** it against those helper objects (exercising NCC's own linker with external objects). +/// Helper objects are named per-test so parallel cases sharing a helper (e.g. several NaN tests) +/// don't collide, and NCC leaves them in place (caller-owned `.o` inputs) for us to clean up. +fn run_test_with_libs(case: &TestCase, result: &mut CaseResult) { + let ncc_path = get_ncc_binary_path(); + let test_path = std::path::Path::new(&case.c_file); + let binary_path = test_path.with_extension(""); + let binary_str = binary_path.to_str().unwrap(); + let test_stem = test_path.file_stem().unwrap().to_string_lossy().to_string(); + + // gcc compiles each helper to a per-test object (avoids collisions across parallel cases) + let mut helper_objs: Vec = Vec::new(); + for lib in &case.lib_deps { + let lib_stem = std::path::Path::new(lib).file_stem().unwrap().to_string_lossy(); + let lib_obj = test_path.with_file_name(format!("{test_stem}__{lib_stem}.o")); + let mut cmd = std::process::Command::new("gcc"); + // On arm64 hosts gcc must cross-target x86_64 to match ncc's output. + #[cfg(target_arch = "aarch64")] + cmd.args(["-arch", "x86_64"]); + let st = cmd.arg("-c").arg(lib).arg("-o").arg(&lib_obj).status(); + if st.is_err() || !st.unwrap().success() { + for o in &helper_objs { + std::fs::remove_file(o).ok(); + } + result.failed += 1; + result + .failures + .push(format!("{} [with-libs] (helper {} compile failed)", case.c_file, lib)); + return; + } + helper_objs.push(lib_obj); + } + + // ncc compiles the test and links it with the helper objects (+ -lm on Linux when needed). + let mut cmd = std::process::Command::new(&ncc_path); + cmd.arg(&case.c_file); + for o in &helper_objs { + cmd.arg(o); + } + cmd.arg("-o").arg(binary_str); + if case.requires_mathlib && !cfg!(target_os = "macos") { + cmd.arg("-lm"); + } + let compile_output = cmd.output().unwrap(); + + // helper objects are ours (ncc leaves caller-supplied .o inputs in place) + for o in &helper_objs { + std::fs::remove_file(o).ok(); + } + + let actual = if compile_output.status.success() { + let run_output = std::process::Command::new(binary_str).output().unwrap(); + std::fs::remove_file(&binary_path).ok(); + let stdout = String::from_utf8_lossy(&run_output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&run_output.stderr).to_string(); + ProgramOutput::Result { + code: run_output.status.code().unwrap_or(-1), + stdout: if stdout.is_empty() { None } else { Some(stdout) }, + stderr: if stderr.is_empty() { None } else { Some(stderr) }, + } + } else { + ProgramOutput::Error(compile_output.status.code().unwrap_or(-1)) + }; + record(case, actual, result, "with-libs"); +} + /// Runs a cross-compilation test for library files /// Compiles client with one compiler and library with the other, then links fn run_library_cross_test( @@ -355,6 +494,7 @@ fn run_library_cross_test( expected: &ProgramOutput, result: &mut CaseResult, ncc_compiles_client: bool, + requires_mathlib: bool, ) { let ncc_path = get_ncc_binary_path(); let path = std::path::Path::new(client_file); @@ -425,12 +565,15 @@ fn run_library_cross_test( // On arm64 hosts gcc must cross-target x86_64 to match ncc's output. #[cfg(target_arch = "aarch64")] link_cmd.args(["-arch", "x86_64"]); - let link_status = link_cmd + link_cmd .arg(&client_obj) .arg(&library_obj) .arg("-o") - .arg(binary_path_str) - .status(); + .arg(binary_path_str); + if requires_mathlib && !cfg!(target_os = "macos") { + link_cmd.arg("-lm"); + } + let link_status = link_cmd.status(); std::fs::remove_file(&client_obj).ok(); std::fs::remove_file(&library_obj).ok(); @@ -509,23 +652,33 @@ fn run_one_case(case: &TestCase) -> CaseResult { run_test(case, &mut result, &[], ""); } ProgramOutput::Result { .. } => { - // For library tests, use cross-compilation to validate ABI compliance - if is_library_test { + // Tests with helper-lib dependencies: ncc compiles the test, gcc builds the helpers, link. + if !case.lib_deps.is_empty() { + run_test_with_libs(case, &mut result); + } else if is_library_test { if let Some(library_file) = case.extra_files.first() { // Test 1: ncc compiles client, gcc compiles library (validates ncc as caller) - run_library_cross_test(&case.c_file, library_file, &case.output, &mut result, true); + run_library_cross_test( + &case.c_file, + library_file, + &case.output, + &mut result, + true, + case.requires_mathlib, + ); // Test 2: gcc compiles client, ncc compiles library (validates ncc as callee) - run_library_cross_test(&case.c_file, library_file, &case.output, &mut result, false); + run_library_cross_test( + &case.c_file, + library_file, + &case.output, + &mut result, + false, + case.requires_mathlib, + ); } } else { // Standard test: compile everything with ncc run_test(case, &mut result, &[], ""); - // The --no-iced text emitter forks an extra `as` per test, doubling - // process count. Codecov unions coverage across the CI matrix, so emit.rs - // stays fully covered by the Linux job; only run it there. (A single - // non-linux --no-iced smoke test below keeps the arm64 `as` shim covered.) - #[cfg(target_os = "linux")] - run_test(case, &mut result, &["--no-iced".to_string()], "no-iced"); } } } @@ -556,20 +709,9 @@ fn run_cases(cases: Vec) { "external-linker", ); - // On non-Linux hosts the per-test --no-iced runs are skipped above to avoid forking an - // extra `as` per test. Run exactly ONE here so the arm64 `as --arch x86_64` cross-assembly - // shim still gets coverage. Pick the first case whose output is a Result and isn't a - // _client.c library test (those go through the cross-compilation path, not run_test). - #[cfg(not(target_os = "linux"))] - if let Some(case) = cases.iter().find(|c| { - matches!(c.output, ProgramOutput::Result { .. }) - && !(c.c_file.contains("/libraries/") && c.c_file.ends_with("_client.c")) - }) { - run_test(case, &mut tally, &["--no-iced".to_string()], "no-iced-smoke"); - } - - assert!( - tally.failed == 0, + assert_eq!( + tally.failed, + 0, "{} of {} sub-tests failed:\n{}", tally.failed, tally.passed + tally.failed,