From a488ee243f861cedcbe309cfdb5b061db20fe3af Mon Sep 17 00:00:00 2001 From: John Ring Date: Sat, 13 Jun 2026 21:13:43 -0400 Subject: [PATCH 1/3] Completed chapter 12 - adding unsigned int/ long --- CLAUDE.md | 4 +- README.md | 32 +- src/codegen.rs | 198 +++++++--- src/emit.rs | 35 +- src/emit_iced.rs | 113 +++--- src/lexer.rs | 21 +- src/main.rs | 2 +- src/parser.rs | 208 ++++++++--- src/pretty.rs | 28 ++ src/tacky.rs | 40 +- src/validate.rs | 349 +++++++++--------- tests/c_programs/expected_results.json | 6 + .../int_wrapping/long_min_literal.c | 14 + .../c_programs/static_vars/mixed_alignment.c | 20 + .../warnings/no_overflow_unsigned.c | 14 + tests/runner.rs | 29 +- 16 files changed, 754 insertions(+), 359 deletions(-) create mode 100644 tests/c_programs/int_wrapping/long_min_literal.c create mode 100644 tests/c_programs/static_vars/mixed_alignment.c create mode 100644 tests/c_programs/warnings/no_overflow_unsigned.c diff --git a/CLAUDE.md b/CLAUDE.md index 3be0b80..54279ce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -151,9 +151,9 @@ Tests validate both successful compilation and error handling: ## Language Implementation Notes -**Type System**: Currently supports `int` (32-bit) and `long` (64-bit). Type conversions follow two's complement truncation. +**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. -**Integer Arithmetic**: Deterministic wrapping behavior (non-standard C extension). Shift amounts are masked to prevent undefined behavior. +**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. **Evaluation Order**: Left-to-right (non-standard, eliminates UB). diff --git a/README.md b/README.md index 6e11243..6117c6b 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` and `long` types, functions, static variables, all control +A substantial subset of C is supported, including `int`, `long`, `unsigned int`, and `unsigned long` 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. @@ -213,7 +213,7 @@ The compiler currently implements a subset of C with the following grammar: ::= { }+ [ "=" ] ";" ::= { }+ "(" ")" ( | ";" ) ::= "void" | { "," } - ::= "int" | "long" + ::= { "int" | "long" | "signed" | "unsigned" }+ ::= | "static" | "extern" ::= "{" { } "}" ::= | @@ -235,7 +235,7 @@ The compiler currently implements a subset of C with the following grammar: | ";" ::= | | | "?" ":" | "++" | "--" - ::= | | | | "++" | "--" + ::= | | | | | | "++" | "--" | "(" ")" | "(" ")" | "(" [ ] ")" ::= { "," } @@ -246,6 +246,8 @@ The compiler currently implements a subset of C with the following grammar: ::= ? An identifier token ? ::= ? An integer constant token ? ::= ? 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') ? ``` ### Supported Features @@ -261,7 +263,7 @@ The compiler supports: functions - **Compound statements (blocks)**: `{ ... }` with proper scoping - **Variable scoping**: Block-local variables with shadowing support -- **Type system**: `int` (32-bit) and `long` (64-bit) with implicit conversions and explicit casts +- **Type system**: `int`/`unsigned int` (32-bit) and `long`/`unsigned long` (64-bit), with the usual arithmetic conversions, implicit conversions, and explicit casts - **Integer arithmetic**: addition, subtraction, multiplication, division, modulo - **Bitwise operations**: AND (`&`), OR (`|`), XOR (`^`), complement (`~`), left/right shift (`<<`, `>>`) - **Logical operations**: AND (`&&`), OR (`||`), NOT (`!`) with short-circuit evaluation @@ -287,13 +289,16 @@ NCC provides several safety features and guarantees to help developers write mor #### Guaranteed Behaviors -- **Deterministic integer overflow**: Integer arithmetic uses two's complement wrapping (`int`: 32-bit, `long`: 64-bit). - For example, `INT_MAX + 1` reliably wraps to `INT_MIN`. +- **Deterministic integer overflow**: Signed integer arithmetic uses two's complement wrapping (`int`: 32-bit, + `long`: 64-bit) instead of being undefined — e.g. `INT_MAX + 1` reliably wraps to `INT_MIN`. Unsigned arithmetic + (`unsigned int`, `unsigned long`) already wraps modulo 2^N per the C standard. - **Left-to-right evaluation**: Binary operations are evaluated left to right, eliminating undefined behavior from evaluation order. -- **Type conversions**: Converting `long` to `int` truncates to the lower 32 bits using two's complement representation, - equivalent to repeatedly subtracting 2^32 until the value fits in an `int` range. - For example, `2147483650L` (INT_MAX + 3) converts to `-2147483646`. +- **Type conversions**: Narrowing (e.g. `long` to `int`) truncates to the lower 32 bits using two's complement + representation, equivalent to repeatedly subtracting 2^32 until the value fits in an `int` range. + For example, `2147483650L` (INT_MAX + 3) converts to `-2147483646`. Widening is value-preserving — signed sources + sign-extend, unsigned sources zero-extend — and same-width signed/unsigned conversions reinterpret the bits + (e.g. `(unsigned)-1` is `UINT_MAX`). - **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. @@ -313,10 +318,11 @@ NCC provides several safety features and guarantees to help developers write mor requires a constant expression (such as a static initializer) is a hard error instead - **Out-of-range shift count** (`-Wshift-count-overflow`, `-Wshift-count-negative`): Warns when a `<<` or `>>` (including `<<=` / `>>=`) has a constant shift count that is negative or `>=` the width of the left operand's type - (32 for `int`, 64 for `long`), e.g. `1 << 32` or `1 << -1` -- **Integer overflow in a constant expression** (`-Woverflow`): Warns when folding a constant expression in a static - initializer or case label leaves the result type (e.g. `int x = 2147483647 + 1;`). NCC wraps deterministically - (two's complement) rather than treating it as undefined, so this flags non-portable code instead of erroring + (32 for 32-bit types, 64 for 64-bit types), e.g. `1 << 32` or `1 << -1` +- **Integer overflow in a constant expression** (`-Woverflow`): Warns when folding a *signed* constant expression in a + static initializer or case label overflows the result type (e.g. `int x = 2147483647 + 1;`). NCC wraps + deterministically (two's complement) rather than treating it as undefined, so this flags non-portable code instead of + erroring. Unsigned wraparound is well-defined and is *not* warned (matching gcc/clang) - **Constant changed by an implicit conversion** (`-Wconstant-conversion`): Warns when a constant initializer is implicitly narrowed to a type that can't hold it (e.g. `int x = 2147483648;`, which truncates to `-2147483648`). An explicit cast (`int x = (int)2147483648;`) silences it diff --git a/src/codegen.rs b/src/codegen.rs index 6d96439..0bcf967 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -27,7 +27,7 @@ //! - Return value in RAX //! - 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 assembly types +//! - Produces a [`BackendSymbolTable`] mapping names to their types (size + signedness) //! //! ## Call Order //! @@ -98,8 +98,8 @@ impl AssemblyType { impl From<&Type> for AssemblyType { fn from(ty: &Type) -> Self { match ty { - Type::Int => AssemblyType::Longword, - Type::Long => AssemblyType::Quadword, + Type::Int | Type::UInt => AssemblyType::Longword, + Type::Long | Type::ULong => AssemblyType::Quadword, Type::FunType { .. } => { panic!("Cannot convert function type to assembly type") } @@ -142,6 +142,8 @@ impl From<&Val> for Operand { 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), } } } @@ -162,6 +164,7 @@ pub enum BinaryOp { BitXOr, BitShl, BitSar, + BitShr, } #[derive(Clone, Debug, PartialEq)] @@ -175,6 +178,10 @@ pub enum Instruction { src: Operand, dst: Operand, }, + MovZeroExtend { + src: Operand, + dst: Operand, + }, Unary { op: UnaryOp, dst: Operand, @@ -192,6 +199,7 @@ pub enum Instruction { size: AssemblyType, }, Idiv(Operand, AssemblyType), + Div(Operand, AssemblyType), Cdq(AssemblyType), Jmp(Identifier), JmpCC { @@ -212,6 +220,10 @@ pub enum Instruction { pub enum CondCode { E, NE, + A, + AE, + B, + BE, G, GE, L, @@ -227,6 +239,10 @@ impl CondCode { CondCode::GE => "ge", CondCode::L => "l", CondCode::LE => "le", + CondCode::B => "b", + CondCode::BE => "be", + CondCode::A => "a", + CondCode::AE => "ae", } } } @@ -244,14 +260,24 @@ pub struct Program { pub static_vars: Vec, } +/// Operand size (`Longword`/`Quadword`) for `val` — constants by their `Const` variant, +/// variables by their type in the backend symbol table. Drives instruction sizing/suffixes. fn get_assembly_type(val: &Val, symbols: &BackendSymbolTable) -> AssemblyType { match val { - Val::Constant(Const::ConstInt(_)) => AssemblyType::Longword, - Val::Constant(Const::ConstLong(_)) => AssemblyType::Quadword, - Val::Var(name) => match symbols.get(name).expect("Variable should be in backend symbol table") { - AsmSymbolEntry::Obj { asm_type, .. } => *asm_type, - AsmSymbolEntry::Fun { .. } => unreachable!("Cannot use function as value"), - }, + Val::Constant(Const::ConstInt(_) | Const::ConstUInt(_)) => AssemblyType::Longword, + Val::Constant(Const::ConstLong(_) | Const::ConstULong(_)) => AssemblyType::Quadword, + Val::Var(name) => symbols.get_var_type(name).into(), + } +} + +/// Whether `val` has a signed type — selects signed vs unsigned instructions (`idiv`/`div`, +/// signed/unsigned condition codes). Constants are classified by their `Const` variant; +/// variables delegate to [`Type::is_signed`] via the backend symbol table. +fn is_signed(val: &Val, symbols: &BackendSymbolTable) -> bool { + match val { + Val::Constant(Const::ConstInt(_) | Const::ConstLong(_)) => true, + Val::Constant(Const::ConstUInt(_) | Const::ConstULong(_)) => false, + Val::Var(name) => symbols.get_var_type(name).is_signed(), } } @@ -322,6 +348,17 @@ fn convert_function_call( instructions } +/// 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 ([`get_assembly_type`]) and signedness ([`is_signed`]): +/// - **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). +/// - **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 { match instruction { tacky::Instruction::Return(x) => { @@ -385,6 +422,10 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol | BinOp::BitwiseXOr | BinOp::BitwiseLeftShift | BinOp::BitwiseRightShift => { + let asm_op = match op { + BinOp::BitwiseRightShift if !is_signed(src1, symbols) => BinaryOp::BitShr, // logical + _ => BinaryOp::from(op), // BitSar / everything else + }; vec![ Instruction::Mov { src: src1.into(), @@ -392,7 +433,7 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol size, }, Instruction::Binary { - op: BinaryOp::from(op), + op: asm_op, src: src2.into(), dst: dst.into(), size, @@ -401,20 +442,29 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol } BinOp::Divide | BinOp::Remainder => { let result_reg = if *op == BinOp::Divide { Reg::AX } else { Reg::DX }; - vec![ - Instruction::Mov { - src: src1.into(), - dst: Operand::Reg(Reg::AX), - size, - }, - Instruction::Cdq(size), - Instruction::Idiv(src2.into(), size), - Instruction::Mov { - src: Operand::Reg(result_reg), - dst: dst.into(), + let signed = is_signed(dst, symbols); + let mut ins = vec![Instruction::Mov { + src: src1.into(), + dst: Operand::Reg(Reg::AX), + size, + }]; + if signed { + ins.push(Instruction::Cdq(size)); + ins.push(Instruction::Idiv(src2.into(), 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::Mov { + src: Operand::Reg(result_reg), + dst: dst.into(), + size, + }); + ins } BinOp::Equal | BinOp::NotEqual @@ -422,13 +472,19 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol | BinOp::LessOrEqual | BinOp::GreaterThan | BinOp::GreaterOrEqual => { - let code = match op { - BinOp::Equal => CondCode::E, - BinOp::NotEqual => CondCode::NE, - BinOp::LessThan => CondCode::L, - BinOp::LessOrEqual => CondCode::LE, - BinOp::GreaterThan => CondCode::G, - BinOp::GreaterOrEqual => CondCode::GE, + // signedness comes from the operands, not dst (a comparison's result is always int) + let signed = is_signed(src1, symbols); + let code = match (op, signed) { + (BinOp::Equal, _) => CondCode::E, + (BinOp::NotEqual, _) => CondCode::NE, + (BinOp::LessThan, true) => CondCode::L, + (BinOp::LessThan, false) => CondCode::B, + (BinOp::LessOrEqual, true) => CondCode::LE, + (BinOp::LessOrEqual, false) => CondCode::BE, + (BinOp::GreaterThan, true) => CondCode::G, + (BinOp::GreaterThan, false) => CondCode::A, + (BinOp::GreaterOrEqual, true) => CondCode::GE, + (BinOp::GreaterOrEqual, false) => CondCode::AE, _ => unreachable!(), }; vec![ @@ -503,9 +559,21 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol size: AssemblyType::Longword, }] } + tacky::Instruction::ZeroExtend { src, dst } => { + vec![Instruction::MovZeroExtend { + src: src.into(), + dst: dst.into(), + }] + } } } +/// Maps an arithmetic/bitwise TACKY `BinOp` to its assembly `BinaryOp`. +/// +/// Division/remainder are `unreachable!` here — they need signedness (`idiv`/`div`) plus a +/// dividend setup, so they're handled directly in [`convert_instruction`]. Right shift maps to +/// the signed `BitSar` by default; `convert_instruction` overrides it to `BitShr` for unsigned +/// operands. Comparisons are likewise handled there (they select condition codes, not a `BinaryOp`). impl From<&BinOp> for BinaryOp { fn from(op: &BinOp) -> Self { match op { @@ -561,7 +629,7 @@ fn convert_function(ast: &tacky::FunctionDefinition, symbols: &BackendSymbolTabl instructions.push(Instruction::Mov { src: Operand::Reg(reg), dst: Operand::Pseudo(param.clone()), - size: *param_ty, + size: param_ty, }); } @@ -571,7 +639,7 @@ fn convert_function(ast: &tacky::FunctionDefinition, symbols: &BackendSymbolTabl instructions.push(Instruction::Mov { src: Operand::Stack(stack_offset), dst: Operand::Pseudo(param.clone()), - size: *param_ty, + size: param_ty, }); } @@ -587,13 +655,14 @@ fn convert_function(ast: &tacky::FunctionDefinition, symbols: &BackendSymbolTabl /// Backend symbol table entry mapping identifiers to their assembly-level properties. /// -/// Tracks whether a symbol is an object (variable) or function, along with -/// the assembly type (Longword/Quadword) needed for instruction sizing. +/// Tracks whether a symbol is an object (variable) or function. For objects it stores the +/// 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. Obj { - asm_type: AssemblyType, + var_type: Type, #[allow(dead_code)] is_static: bool, }, @@ -605,15 +674,22 @@ pub enum AsmSymbolEntry { pub type BackendSymbolTable = HashMap, AsmSymbolEntry>; +/// Operand-type lookups on the backend symbol table: a variable's `Type` (`get_var_type`) +/// and the `AssemblyType` (size) derived from it (`get_obj_type`). pub trait BackendSymbolTableExt { - fn get_obj_type(&self, name: &str) -> &AssemblyType; + fn get_obj_type(&self, name: &str) -> AssemblyType; + fn get_var_type(&self, name: &str) -> &Type; } impl BackendSymbolTableExt for BackendSymbolTable { - fn get_obj_type(&self, name: &str) -> &AssemblyType { - match self.get(name).unwrap() { - AsmSymbolEntry::Obj { asm_type, .. } => asm_type, - AsmSymbolEntry::Fun { .. } => panic!("Expected object type, found function: {}", name), + fn get_obj_type(&self, name: &str) -> AssemblyType { + self.get_var_type(name).into() + } + + fn get_var_type(&self, name: &str) -> &Type { + match self.get(name).expect("Variable not in symbol table") { + AsmSymbolEntry::Obj { var_type, .. } => var_type, + AsmSymbolEntry::Fun { .. } => unreachable!("Expected object type, found function: {}", name), } } } @@ -630,7 +706,7 @@ fn build_backend_symbol_table(ast: &tacky::Program, symbols: &SymbolTable) -> Ba let backend_entry = match &symbol.symbol_type { Type::FunType { defined, .. } => AsmSymbolEntry::Fun { defined: *defined }, ty => AsmSymbolEntry::Obj { - asm_type: ty.into(), + var_type: ty.clone(), //todo any way to consume and avoid clone is_static: static_names.contains(&**name), }, }; @@ -642,7 +718,7 @@ fn build_backend_symbol_table(ast: &tacky::Program, symbols: &SymbolTable) -> Ba backend.insert( temp_name.clone(), AsmSymbolEntry::Obj { - asm_type: temp_type.into(), + var_type: temp_type.clone(), is_static: false, }, ); @@ -652,6 +728,8 @@ fn build_backend_symbol_table(ast: &tacky::Program, symbols: &SymbolTable) -> Ba backend } +/// Converts a TACKY static variable to the codegen [`StaticVariable`], deriving its +/// alignment from the variable's type (4 for int/uint, 8 for long/ulong). fn convert_static_var(static_var: TackyStaticVariable) -> StaticVariable { let TackyStaticVariable { name, @@ -761,11 +839,11 @@ fn replace_pseudo_registers(program: &mut Program, symbols: &BackendSymbolTable) *dst = stack_mapping.replace_pseudo(dst, *size); } Instruction::Unary { op: _, dst, size } => *dst = stack_mapping.replace_pseudo(dst, *size), - Instruction::Movsx { src, dst } => { + Instruction::Movsx { src, dst } | Instruction::MovZeroExtend { src, dst } => { *src = stack_mapping.replace_pseudo(src, AssemblyType::Longword); *dst = stack_mapping.replace_pseudo(dst, AssemblyType::Quadword); } - Instruction::Idiv(src, size) => { + Instruction::Idiv(src, size) | Instruction::Div(src, size) => { *src = stack_mapping.replace_pseudo(src, *size); } Instruction::Cmp { v1, v2, size } => { @@ -774,7 +852,7 @@ fn replace_pseudo_registers(program: &mut Program, symbols: &BackendSymbolTable) } Instruction::SetCC { op, .. } => { if let Operand::Pseudo(name) = op { - let size = *symbols.get_obj_type(name); + let size = symbols.get_obj_type(name); *op = stack_mapping.replace_pseudo(op, size); } } @@ -861,6 +939,26 @@ fn fix_invalid(program: &mut Program, stack_offsets: &HashMap, i32>) { size: AssemblyType::Quadword, }); } + Instruction::MovZeroExtend { ref src, ref dst } => { + if dst.is_memory() { + new_ins.push(Instruction::Mov { + src: src.clone(), + dst: Operand::Reg(Reg::R11), + size: AssemblyType::Longword, + }); + new_ins.push(Instruction::Mov { + src: Operand::Reg(Reg::R11), + dst: dst.clone(), + size: AssemblyType::Quadword, + }); + } else { + new_ins.push(Instruction::Mov { + src: src.clone(), + dst: dst.clone(), + size: AssemblyType::Longword, + }) + } + } Instruction::Idiv(Operand::Imm(c), size) => { new_ins.push(Instruction::Mov { src: Operand::Imm(c), @@ -869,6 +967,14 @@ fn fix_invalid(program: &mut Program, stack_offsets: &HashMap, i32>) { }); new_ins.push(Instruction::Idiv(Operand::Reg(Reg::R10), size)); } + Instruction::Div(Operand::Imm(c), size) => { + new_ins.push(Instruction::Mov { + src: Operand::Imm(c), + dst: Operand::Reg(Reg::R10), + size, + }); + new_ins.push(Instruction::Div(Operand::Reg(Reg::R10), size)) + } Instruction::Binary { op: BinaryOp::Mult, ref src, @@ -923,7 +1029,7 @@ fn fix_invalid(program: &mut Program, stack_offsets: &HashMap, i32>) { }); } Instruction::Binary { - op: op @ (BinaryOp::BitShl | BinaryOp::BitSar), + op: op @ (BinaryOp::BitShl | BinaryOp::BitSar | BinaryOp::BitShr), src, dst, size, diff --git a/src/emit.rs b/src/emit.rs index 2940d08..81fa2f1 100644 --- a/src/emit.rs +++ b/src/emit.rs @@ -102,6 +102,7 @@ fn emit_binaryop(op: &BinaryOp, size: &AssemblyType) -> String { BinaryOp::BitXOr => format!("xor{suffix}"), BinaryOp::BitShl => format!("shl{suffix}"), BinaryOp::BitSar => format!("sar{suffix}"), + BinaryOp::BitShr => format!("shr{suffix}"), } } @@ -164,12 +165,12 @@ fn emit_instruction(ins: &Instruction, fn_name: &str) -> String { output.push_str(&format!("{} {}\n", emit_unaryop(op, size), emit_operand(dst, &width))); } Instruction::Binary { - op: op @ (BinaryOp::BitShl | BinaryOp::BitSar), + op: op @ (BinaryOp::BitShl | BinaryOp::BitSar | BinaryOp::BitShr), src, dst, size, } => { - // shl and sar always use an immediate or cl register as the source operand + // 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!( "{} {}, {}", @@ -192,6 +193,12 @@ fn emit_instruction(ins: &Instruction, fn_name: &str) -> String { 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", @@ -261,7 +268,8 @@ fn emit_function(fun_def: &FunctionDefinition) -> String { /// 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 int, `.quad` for long). +/// 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 { @@ -288,26 +296,23 @@ fn emit_static_variable(sv: &StaticVariable) -> String { } match init_val { - validate::StaticInt::IntInit(0) | validate::StaticInt::LongInit(0) => { + 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")); } - validate::StaticInt::IntInit(val) => { - // Data section for initialized int + 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("\t.align 4\n"); - output.push_str(&format!("{processed_name}:\n")); - output.push_str(&format!("\t.long {val}\n")); - } - validate::StaticInt::LongInit(val) => { - // Data section for initialized long - output.push_str("\t.data\n"); - output.push_str("\t.align 8\n"); + output.push_str(&format!("\t.align {alignment}\n")); output.push_str(&format!("{processed_name}:\n")); - output.push_str(&format!("\t.quad {val}\n")); + output.push_str(&format!("\t{directive} {value}\n")); } } diff --git a/src/emit_iced.rs b/src/emit_iced.rs index a2f8fc9..f5ae97f 100644 --- a/src/emit_iced.rs +++ b/src/emit_iced.rs @@ -359,8 +359,6 @@ fn emit_object_with_labels( let data = obj.section_id(StandardSection::Data); let bss = obj.section_id(StandardSection::UninitializedData); - let mut data_offset: u64 = 0; - let mut bss_offset: u64 = 0; let mut static_var_symbols: HashMap, SymbolId> = HashMap::new(); for StaticVariable { name, @@ -373,24 +371,18 @@ 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) => { - obj.append_section_bss(bss, *alignment, *alignment); - let offset = bss_offset; - bss_offset += alignment; + StaticInt::IntInit(0) + | StaticInt::LongInit(0) + | StaticInt::UIntInit(0) + | StaticInt::ULongInit(0) => { + // append_section_bss returns the actual offset (after alignment padding) + let offset = obj.append_section_bss(bss, *alignment, *alignment); (offset, &bss) } - StaticInt::IntInit(val) => { - let init_bytes = val.to_le_bytes(); - obj.append_section_data(data, &init_bytes, *alignment); - let offset = data_offset; - data_offset += alignment; - (offset, &data) - } - StaticInt::LongInit(val) => { - let init_bytes = val.to_le_bytes(); - obj.append_section_data(data, &init_bytes, *alignment); - let offset = data_offset; - data_offset += alignment; + nonzero => { + let init_bytes = nonzero.to_le_bytes(); + // append_section_data returns the actual offset (after alignment padding) + let offset = obj.append_section_data(data, &init_bytes, *alignment); (offset, &data) } }; @@ -584,8 +576,31 @@ fn make_lbl_ptr(lbl: &CodeLabel, asm_ty: &AssemblyType) -> AsmMemoryOperand { } } +macro_rules! emit_setcc { + ($a:expr, $code:expr, $op:expr) => { + match $code { + CondCode::E => $a.sete($op)?, + CondCode::NE => $a.setne($op)?, + CondCode::G => $a.setg($op)?, + CondCode::GE => $a.setge($op)?, + CondCode::L => $a.setl($op)?, + CondCode::LE => $a.setle($op)?, + CondCode::A => $a.seta($op)?, + CondCode::AE => $a.setae($op)?, + CondCode::B => $a.setb($op)?, + CondCode::BE => $a.setbe($op)?, + } + }; +} + // Data operands (static variables) use RIP-relative addressing with relocations. // +/// Encodes a single assembly instruction into the iced [`CodeAssembler`] `a`. +/// +/// Matches on the operand shapes (register / immediate / stack / RIP-relative data) and size, +/// since iced exposes a distinct typed builder per form — hence the large per-instruction match. +/// Records relocations as it goes: external `Call`s into `external_calls` and RIP-relative data +/// accesses into `data_relocs`, for the linker to patch. // Note: Many Data destination patterns (e.g., `Add(Reg, Data)`, `Neg(Data)`) are currently // unreached because the codegen uses load→op→store sequences for static variables. // These patterns will become reachable after implementing copy propagation and dead store @@ -890,6 +905,25 @@ fn emit_instruction( } _ => unreachable!(), }, + BinaryOp::BitShr => match (src, dst, size) { + (Operand::Imm(v), Operand::Reg(d), AssemblyType::Longword) => a.shr(gpr32(d), *v as i32)?, + (Operand::Imm(v), Operand::Reg(d), AssemblyType::Quadword) => a.shr(gpr64_reg(d), *v as i32)?, + (Operand::Reg(Reg::CX), Operand::Reg(d), AssemblyType::Longword) => a.shr(gpr32(d), gpr8::cl)?, + (Operand::Reg(Reg::CX), Operand::Reg(d), AssemblyType::Quadword) => a.shr(gpr64_reg(d), gpr8::cl)?, + (Operand::Imm(v), Operand::Stack(off), _) => a.shr(mem_rbp(*off, *size), *v as i32)?, + (Operand::Imm(v), Operand::Data(name), _) => { + let lbl = data_labels.get(name).unwrap(); + data_relocs.push((a.instructions().len(), name.clone())); + a.shr(make_lbl_ptr(lbl, size), *v as i32)? + } + (Operand::Reg(Reg::CX), Operand::Stack(off), _) => a.shr(mem_rbp(*off, *size), gpr8::cl)?, + (Operand::Reg(Reg::CX), Operand::Data(name), _) => { + let lbl = data_labels.get(name).unwrap(); + data_relocs.push((a.instructions().len(), name.clone())); + a.shr(make_lbl_ptr(lbl, size), gpr8::cl)? + } + _ => unreachable!(), + }, }, Instruction::Cmp { v1, v2, size } => match (v1, v2, size) { (Operand::Reg(r1), Operand::Reg(r2), AssemblyType::Longword) => a.cmp(gpr32(r2), gpr32(r1))?, @@ -945,6 +979,18 @@ fn emit_instruction( } _ => unreachable!(), }, + Instruction::Div(op, size) => match (op, size) { + (Operand::Reg(r), AssemblyType::Longword) => a.div(gpr32(r))?, + (Operand::Reg(r), AssemblyType::Quadword) => a.div(gpr64_reg(r))?, + (Operand::Stack(off), _) => a.div(mem_rbp(*off, *size))?, + (Operand::Data(name), _) => { + let lbl = data_labels.get(name).unwrap(); + data_relocs.push((a.instructions().len(), name.clone())); + a.div(make_lbl_ptr(lbl, size))? + } + _ => unreachable!("{}", format!("{:?}", op)), + }, + Instruction::MovZeroExtend { .. } => unreachable!("MovZeroExtend in emit"), Instruction::Jmp(label) => { let l = *labels.entry(label.0.clone()).or_insert_with(|| a.create_label()); a.jmp(l)?; @@ -958,36 +1004,19 @@ fn emit_instruction( CondCode::GE => a.jge(l)?, CondCode::L => a.jl(l)?, CondCode::LE => a.jle(l)?, + CondCode::A => a.ja(l)?, + CondCode::AE => a.jae(l)?, + CondCode::B => a.jb(l)?, + CondCode::BE => a.jbe(l)?, }; } Instruction::SetCC { code, op } => match op { - Operand::Reg(_r) => match code { - CondCode::E => a.sete(gpr8::al)?, - CondCode::NE => a.setne(gpr8::al)?, - CondCode::G => a.setg(gpr8::al)?, - CondCode::GE => a.setge(gpr8::al)?, - CondCode::L => a.setl(gpr8::al)?, - CondCode::LE => a.setle(gpr8::al)?, - }, - Operand::Stack(off) => match code { - CondCode::E => a.sete(byte_ptr(gpr64::rbp - (-*off)))?, - CondCode::NE => a.setne(byte_ptr(gpr64::rbp - (-*off)))?, - CondCode::G => a.setg(byte_ptr(gpr64::rbp - (-*off)))?, - CondCode::GE => a.setge(byte_ptr(gpr64::rbp - (-*off)))?, - CondCode::L => a.setl(byte_ptr(gpr64::rbp - (-*off)))?, - CondCode::LE => a.setle(byte_ptr(gpr64::rbp - (-*off)))?, - }, + Operand::Reg(_r) => emit_setcc!(a, code, gpr8::al), + Operand::Stack(off) => emit_setcc!(a, code, byte_ptr(gpr64::rbp - (-*off))), Operand::Data(name) => { let lbl = data_labels.get(name).unwrap(); data_relocs.push((a.instructions().len(), name.clone())); - match code { - CondCode::E => a.sete(byte_ptr(*lbl))?, - CondCode::NE => a.setne(byte_ptr(*lbl))?, - CondCode::G => a.setg(byte_ptr(*lbl))?, - CondCode::GE => a.setge(byte_ptr(*lbl))?, - CondCode::L => a.setl(byte_ptr(*lbl))?, - CondCode::LE => a.setle(byte_ptr(*lbl))?, - } + emit_setcc!(a, code, byte_ptr(*lbl)) } _ => unreachable!(), }, diff --git a/src/lexer.rs b/src/lexer.rs index 1ab1845..950fcc8 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -37,6 +37,8 @@ pub enum Token { Identifier(String), ConstantInt(String), ConstantLong(String), + ConstantUnsignedInt(String), + ConstantUnsignedLong(String), IntKeyword, // int VoidKeyword, // void ReturnKeyword, // return @@ -95,13 +97,20 @@ pub enum Token { StaticKeyword, // static ExternKeyword, // extern LongKeyword, // long + SignedKeyword, // signed + UnsignedKeyword, // unsigned } const TOKEN_PATTERNS: &[(&str, Token)] = &[ // Special handling tokens (handled differently in next_token) (r"^[a-zA-Z_]\w*\b", Token::Identifier(String::new())), (r"^[0-9]+\b", Token::ConstantInt(String::new())), - (r"^[0-9]++[lL]\b", Token::ConstantLong(String::new())), + (r"^[0-9]+[lL]\b", Token::ConstantLong(String::new())), + (r"^[0-9]+[uU]\b", Token::ConstantUnsignedInt(String::new())), + ( + r"^[0-9]++([lL][uU]|[uU][lL])\b", + Token::ConstantUnsignedLong(String::new()), + ), // Keywords (r"^int\b", Token::IntKeyword), (r"^void\b", Token::VoidKeyword), @@ -168,6 +177,8 @@ const TOKEN_PATTERNS: &[(&str, Token)] = &[ (r"^static\b", Token::StaticKeyword), (r"^extern\b", Token::ExternKeyword), (r"^long\b", Token::LongKeyword), + (r"^signed\b", Token::SignedKeyword), + (r"^unsigned\b", Token::UnsignedKeyword), ]; static TOKEN_DEFS: LazyLock, fn() -> Vec> = LazyLock::new(|| { @@ -232,10 +243,18 @@ fn next_token(input: &str, span: Span) -> Result { let token = match variant { Token::Identifier(_) => Token::Identifier(mat.as_str().to_string()), Token::ConstantInt(_) => Token::ConstantInt(mat.as_str().to_string()), + Token::ConstantUnsignedInt(_) => { + let s = mat.as_str(); + Token::ConstantUnsignedInt(s[..s.len() - 1].to_string()) + } Token::ConstantLong(_) => { let s = mat.as_str(); Token::ConstantLong(s[..s.len() - 1].to_string()) } + Token::ConstantUnsignedLong(_) => { + let s = mat.as_str(); + Token::ConstantUnsignedLong(s[..s.len() - 2].to_string()) + } other => other.clone(), }; matches.push(TokenMatch { diff --git a/src/main.rs b/src/main.rs index ae1f394..3969deb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -496,7 +496,7 @@ fn main() { } if args.run { - let run_status = std::process::Command::new(format!("./{}", &out_file)) + let run_status = std::process::Command::new(Path::new(".").join(&out_file)) .status() .expect("Failed to execute compiled binary"); match run_status.code() { diff --git a/src/parser.rs b/src/parser.rs index 6348259..883a0ca 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -30,7 +30,7 @@ //! └─ parse_statement() — if not a declaration //! ├─ parse_exp() — expression statements, conditions //! │ ├─ parse_factor() — literals, unary, casts, postfix, calls -//! │ │ └─ parse_constant() — int/long literal with overflow promotion +//! │ │ └─ parse_constant() — integer literal with suffix-based promotion //! │ └─ parse_exp() (recursive) — binary operators via precedence climbing //! └─ parse_block_item() — compound statements (recursive) //! ``` @@ -110,9 +110,12 @@ pub enum Expr { } #[derive(Clone, Copy, Debug, PartialEq)] +#[allow(clippy::enum_variant_names)] // `Const` prefix disambiguates from Type / SwitchIntType pub enum Const { ConstInt(i32), ConstLong(i64), + ConstUInt(u32), + ConstULong(u64), } #[derive(Clone, Copy, Debug, PartialEq)] @@ -184,6 +187,8 @@ impl BinOp { pub enum SwitchIntType { Int(i32), Long(i64), + UInt(u32), + ULong(u64), Default, } @@ -194,18 +199,19 @@ impl SwitchIntType { /// from the switch expression's type. Used during semantic analysis for /// duplicate case detection. pub fn as_i64(&self, switch_type: &Type) -> Option { - match self { - SwitchIntType::Int(v) => match switch_type { - Type::Int | Type::Long => Some(*v as i64), - Type::FunType { .. } => unreachable!("Cannot switch on function type"), - }, - SwitchIntType::Long(v) => match switch_type { - Type::Int => Some((*v as i32) as i64), // Truncate to int, then extend - Type::Long => Some(*v), - Type::FunType { .. } => unreachable!("Cannot switch on function type"), - }, - SwitchIntType::Default => None, - } + // Value as its exact 64-bit pattern (extended by the *source*'s signedness). + let raw = match self { + SwitchIntType::Int(v) => *v as i64, // sign-extend + SwitchIntType::Long(v) => *v, // identity + SwitchIntType::UInt(v) => *v as i64, // zero-extend + SwitchIntType::ULong(v) => *v as i64, // reinterpret + SwitchIntType::Default => return None, + }; + 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"), + }) } pub fn label_str(&self, switch_num: u64) -> String { @@ -226,6 +232,12 @@ impl SwitchIntType { format!("switch.{switch_num}_case.neg{c_str}L") } } + SwitchIntType::UInt(val) => { + format!("switch.{switch_num}_case.{val}U") + } + SwitchIntType::ULong(val) => { + format!("switch.{switch_num}_case.{val}UL") + } SwitchIntType::Default => { format!("switch.{switch_num}_default") } @@ -238,6 +250,8 @@ impl SwitchIntType { pub enum Type { Int, Long, + UInt, + ULong, FunType { params: Vec, ret: Box, @@ -250,9 +264,41 @@ impl Type { match self { Type::Int => Const::ConstInt(1), Type::Long => Const::ConstLong(1), + Type::UInt => Const::ConstUInt(1), + Type::ULong => Const::ConstULong(1), Type::FunType { .. } => unreachable!("Cannot increment function type"), } } + + pub fn size_bits(&self) -> u32 { + match self { + Type::Int | Type::UInt => 32, + Type::Long | Type::ULong => 64, + Type::FunType { .. } => panic!("Function does not have type size"), + } + } + + pub fn is_signed(&self) -> bool { + match self { + Type::Int | Type::Long => true, + Type::ULong | Type::UInt => false, + Type::FunType { .. } => panic!("Function does not have type size"), + } + } + + /// 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.size_bits() == other.size_bits() { + if self.is_signed() { other.clone() } else { self.clone() } + } else if self.size_bits() > other.size_bits() { + self.clone() + } else { + other.clone() + } + } } #[derive(Debug, Clone)] @@ -425,9 +471,14 @@ fn expect(expected: &Token, tokens: &mut VecDeque) -> Result Result { match &token.token { Token::ConstantInt(value_str) => match value_str.parse::() { @@ -437,10 +488,30 @@ fn parse_constant(token: &SpannedToken) -> Result { span: token.span, }), }, + Token::ConstantUnsignedInt(value_str) => match value_str.parse::() { + Ok(val) => Ok(Expr::Constant(Const::ConstUInt(val))), + Err(_) => parse_constant(&SpannedToken { + token: Token::ConstantUnsignedLong(value_str.clone()), + span: token.span, + }), + }, Token::ConstantLong(value_str) => match value_str.parse::() { Ok(val) => Ok(Expr::Constant(Const::ConstLong(val))), Err(_) => Err(SyntaxError::with_span( - format!("Integer constant '{}' does not fit in 64-bit int", value_str.bold()), + format!( + "Integer constant '{}' does not fit in 64-bit signed int", + value_str.bold() + ), + Some(token.span), + )), + }, + Token::ConstantUnsignedLong(value_str) => match value_str.parse::() { + Ok(val) => Ok(Expr::Constant(Const::ConstULong(val))), + Err(_) => Err(SyntaxError::with_span( + format!( + "Integer constant '{}' does not fit in 64-bit unsigned int", + value_str.bold() + ), Some(token.span), )), }, @@ -448,6 +519,19 @@ fn parse_constant(token: &SpannedToken) -> Result { } } +/// True if `t` is a type-specifier keyword (`int`, `long`, `signed`, `unsigned`). +fn is_type_specifier(t: &Token) -> bool { + matches!( + t, + Token::IntKeyword | Token::LongKeyword | Token::SignedKeyword | Token::UnsignedKeyword + ) +} + +/// True if `t` begins a declaration: a type specifier or a storage-class keyword. +fn is_specifier(t: &Token) -> bool { + is_type_specifier(t) || matches!(t, Token::StaticKeyword | Token::ExternKeyword) +} + /// Parses primary expressions and operators with precedence higher than any binary operator. /// /// Handles: @@ -475,14 +559,19 @@ fn parse_factor(tokens: &mut VecDeque) -> Result match &spanned.token { - Token::ConstantInt(_) | Token::ConstantLong(_) => { + Token::ConstantInt(_) + | Token::ConstantLong(_) + | Token::ConstantUnsignedInt(_) + | Token::ConstantUnsignedLong(_) => { tokens.pop_front(); parse_constant(spanned) } Token::Negation => { - // simply treating negation as an unop cases a panic when unwrapping int min + // Fold a leading `-` into the literal so INT_MIN / LONG_MIN keep the right type: + // their magnitude (2^31 / 2^63) isn't representable in the positive range, so + // parsing the magnitude then negating would overflow-promote and mistype them. if let Some(SpannedToken { - token: Token::ConstantInt(value_str), + token: Token::ConstantInt(value_str) | Token::ConstantLong(value_str), span: _, }) = tokens.get_mut(1) { @@ -513,10 +602,7 @@ fn parse_factor(tokens: &mut VecDeque) -> Result { tokens.pop_front(); let mut type_specifiers = vec![]; - while matches!( - tokens.front().map(|t| &t.token), - Some(Token::LongKeyword | Token::IntKeyword) - ) { + while matches!(tokens.front(), Some(t) if is_type_specifier(&t.token)) { type_specifiers.push(tokens.front().unwrap().token.clone()); tokens.pop_front(); } @@ -633,36 +719,56 @@ fn parse_exp(tokens: &mut VecDeque, min_prec: u64) -> Result, err_span: &Option) -> Result { - match specifier_list.as_slice() { - [Token::IntKeyword] => Ok(Type::Int), - [Token::LongKeyword] | [Token::IntKeyword, Token::LongKeyword] | [Token::LongKeyword, Token::IntKeyword] => { - Ok(Type::Long) + let (mut ints, mut longs, mut signed, mut unsigned) = (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, + _ => return Err(SyntaxError::with_span("Non-type specifier".to_string(), *err_span)), } - [] => Err(SyntaxError::with_span("Missing type specifier".to_string(), *err_span)), - _ => Err(SyntaxError::with_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) { + return Err(SyntaxError::with_span( + "Duplicate type specifier".to_string(), + *err_span, + )); + } + if (signed > 0) && (unsigned > 0) { + return Err(SyntaxError::with_span( "Invalid type specifier combination".to_string(), *err_span, - )), + )); } + Ok(match (unsigned > 0, longs > 0) { + (true, true) => Type::ULong, + (true, false) => Type::UInt, + (false, true) => Type::Long, + (false, false) => Type::Int, + }) } /// Splits a declaration's specifier list into a `Type` and an optional `StorageClass`. /// -/// Separates type specifiers (`int`, `long`) from storage-class specifiers (`static`, `extern`), -/// then delegates to `parse_type` for type resolution. Errors on duplicate storage classes. +/// Separates type specifiers (`int`, `long`, `signed`, `unsigned`) from storage-class specifiers +/// (`static`, `extern`), then delegates to `parse_type` for type resolution. Errors on duplicate +/// storage classes. fn parse_type_and_storage_class( specifier_list: &Vec, ) -> Result<(Type, Option), SyntaxError> { let mut types = Vec::new(); let mut storage_class = None; for token in specifier_list { - match token.token { - Token::IntKeyword | Token::LongKeyword => { - types.push(token.token.clone()); - } + match &token.token { + t if is_type_specifier(t) => types.push(token.token.clone()), Token::StaticKeyword | Token::ExternKeyword => { let sc = if token.token == Token::StaticKeyword { StorageClass::Static @@ -950,10 +1056,7 @@ fn warn_switch_unreachable(stmt: &SpannedStmt) { /// Peeks at the next token and returns an error if it starts a declaration (type or /// storage-class keyword). Used after labels, where C requires a statement, not a declaration. fn declaration_check(tokens: &VecDeque) -> Result<(), SyntaxError> { - if matches!( - tokens.front().map(|t| &t.token), - Some(&Token::IntKeyword | &Token::LongKeyword | &Token::StaticKeyword | &Token::ExternKeyword) - ) { + if tokens.front().is_some_and(|t| is_specifier(&t.token)) { Err(SyntaxError::with_span( "A label can only be part of a statement and a declaration is not a statement. Add a statement or ';' before the declaration.".to_string(), Some(tokens.front().unwrap().span), @@ -984,13 +1087,8 @@ fn parse_declaration( err_span: &Option, ) -> Result, SyntaxError> { let mut specifier_list = Vec::new(); - while let Some(front) = tokens.front() { - match front.token { - Token::IntKeyword | Token::StaticKeyword | Token::ExternKeyword | Token::LongKeyword => { - specifier_list.push(tokens.pop_front().unwrap()); - } - _ => break, - } + while tokens.front().is_some_and(|t| is_specifier(&t.token)) { + specifier_list.push(tokens.pop_front().unwrap()); } if specifier_list.is_empty() { return Ok(None); // no declaration @@ -1076,14 +1174,8 @@ fn parse_function_params( break; } let mut type_tokens = Vec::new(); - while let Some(front) = tokens.front() { - match front.token { - Token::IntKeyword | Token::LongKeyword => { - type_tokens.push(front.token.clone()); - tokens.pop_front(); - } - _ => break, - } + while tokens.front().is_some_and(|t| is_type_specifier(&t.token)) { + type_tokens.push(tokens.pop_front().unwrap().token); } let err_span = tokens.front().map(|t| t.span); if type_tokens.is_empty() { diff --git a/src/pretty.rs b/src/pretty.rs index 0d9aad4..141c228 100644 --- a/src/pretty.rs +++ b/src/pretty.rs @@ -339,6 +339,8 @@ impl ItfDisplay for Type { match self { Type::Int => Node::leaf("Int".truecolor(MUTED_RED.0, MUTED_RED.1, MUTED_RED.2).to_string()), 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::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()])]; @@ -375,6 +377,8 @@ impl ItfDisplay for Expr { let value_str = match c { Const::ConstInt(val) => format!("Int({})", val), Const::ConstLong(val) => format!("Long({})", val), + Const::ConstUInt(val) => format!("UInt({})", val), + Const::ConstULong(val) => format!("ULong({})", val), }; Node::leaf( format!("Constant({})", value_str) @@ -556,6 +560,8 @@ impl ItfDisplay for Stmt { .map(|(c, _span)| match c { SwitchIntType::Int(v) => format!("{v}"), SwitchIntType::Long(v) => format!("{v}L"), + SwitchIntType::UInt(v) => format!("{v}U"), + SwitchIntType::ULong(v) => format!("{v}UL"), SwitchIntType::Default => "default".to_string(), }) .collect::>() @@ -730,6 +736,8 @@ impl ItfDisplay for ValidatedExpr { let value_str = match c { Const::ConstInt(val) => format!("Int({})", val), Const::ConstLong(val) => format!("Long({})", val), + Const::ConstUInt(val) => format!("UInt({})", val), + Const::ConstULong(val) => format!("ULong({})", val), }; Node::leaf( format!("Constant({})", value_str) @@ -911,6 +919,8 @@ impl ItfDisplay for ValidatedStmt { .map(|c| match c { SwitchIntType::Int(v) => format!("{v}"), SwitchIntType::Long(v) => format!("{v}L"), + SwitchIntType::UInt(v) => format!("{v}U"), + SwitchIntType::ULong(v) => format!("{v}UL"), SwitchIntType::Default => "default".to_string(), }) .collect(); @@ -1090,6 +1100,12 @@ impl ItfDisplay for TackyInstruction { val_str(src), val_str(dst) )), + TackyInstruction::ZeroExtend { src, dst } => Node::leaf(format!( + "{} {} -> {}", + "ZeroExtend".truecolor(TEAL.0, TEAL.1, TEAL.2), + val_str(src), + val_str(dst) + )), } } } @@ -1208,6 +1224,12 @@ impl ItfDisplay for CodegenInstruction { operand_str(src), operand_str(dst) )), + CodegenInstruction::MovZeroExtend { src, dst } => Node::leaf(format!( + "{} {} -> {}", + "MovZeroExtend".truecolor(TEAL.0, TEAL.1, TEAL.2), + operand_str(src), + operand_str(dst) + )), CodegenInstruction::Unary { op, dst, size } => Node::leaf(format!( "{}<{}> {:?} {}", "Unary".truecolor(TEAL.0, TEAL.1, TEAL.2), @@ -1236,6 +1258,12 @@ impl ItfDisplay for CodegenInstruction { size_str(size), operand_str(op) )), + CodegenInstruction::Div(op, size) => Node::leaf(format!( + "{}<{}> {}", + "Div".truecolor(TEAL.0, TEAL.1, TEAL.2), + size_str(size), + operand_str(op) + )), CodegenInstruction::Cdq(size) => Node::leaf(format!( "{}<{}>", "Cdq".truecolor(TEAL.0, TEAL.1, TEAL.2), diff --git a/src/tacky.rs b/src/tacky.rs index bdbbd66..0d1d6b0 100644 --- a/src/tacky.rs +++ b/src/tacky.rs @@ -30,7 +30,7 @@ //! │ ├─ 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() — int<->long conversion instructions +//! │ └─ emit_cast() — integer conversions (truncate / sign- / zero-extend) //! └─ convert_symbols_to_tacky() — extract static vars from symbol table //! ``` @@ -131,6 +131,10 @@ pub enum Instruction { src: Val, dst: Val, }, + ZeroExtend { + src: Val, + dst: Val, + }, Unary { op: UnaryOp, src: Val, @@ -197,11 +201,26 @@ pub struct Program { pub static_vars: Vec, } +/// Emits the conversion instruction for casting `src` (`src_type`) into `dst` (`dst_type`). +/// +/// The instruction is chosen by comparing widths, 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`) +/// - **widening from an unsigned source** → `ZeroExtend` (preserves value, e.g. `uint` -> `long`) +/// +/// 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`. fn emit_cast(src: Val, dst: Val, src_type: &Type, dst_type: &Type, instructions: &mut Vec) { - match (src_type, dst_type) { - (Type::Int, Type::Long) => instructions.push(Instruction::SignExtend { src, dst }), - (Type::Long, Type::Int) => instructions.push(Instruction::Truncate { src, dst }), - _ => unreachable!("Unsupported cast: {:?} -> {:?}", src_type, dst_type), + 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 }); } } @@ -626,11 +645,16 @@ fn tackify_stmt( let mut default_label = None; for case in cases { match case { - SwitchIntType::Int(_) | SwitchIntType::Long(_) => { + SwitchIntType::Int(_) + | SwitchIntType::Long(_) + | SwitchIntType::UInt(_) + | SwitchIntType::ULong(_) => { let case_label = Identifier(Rc::from(case.label_str(*switch_num))); let case_const = match case { SwitchIntType::Int(c) => Const::ConstInt(*c), SwitchIntType::Long(c) => Const::ConstLong(*c), + SwitchIntType::UInt(c) => Const::ConstUInt(*c), + SwitchIntType::ULong(c) => Const::ConstULong(*c), SwitchIntType::Default => unreachable!(), }; let cond_name = name_generator.next("case_cond"); @@ -756,7 +780,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::Int | Type::Long | Type::UInt | Type::ULong => { match &entry.val { InitialValue::Initial(static_val) => tacky_defs.push(StaticVariable { name: name.clone(), @@ -768,6 +792,8 @@ fn convert_symbols_to_tacky(symbols: &SymbolTable) -> Vec { 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"), }; tacky_defs.push(StaticVariable { diff --git a/src/validate.rs b/src/validate.rs index 4bf3823..ab50ad7 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -192,9 +192,49 @@ pub enum InitialValue { /// 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 { IntInit(i32), LongInit(i64), + UIntInit(u32), + ULongInit(u64), +} + +macro_rules! checked_op { + // unary: checked_op!(self, overflowing_neg; IntInit, LongInit, UIntInit, ULongInit) + ($self:expr, $method:ident; $($V:ident),+ $(,)?) => {{ + match $self { + $( StaticInt::$V(v) => { let (r, o) = v.$method(); (StaticInt::$V(r), o) } )+ + } + }}; + // binary: checked_op!(self, other, overflowing_add; IntInit, LongInit, UIntInit, ULongInit) + ($self:expr, $other:expr, $method:ident; $($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) } )+ + _ => unreachable!("get_common guarantees matching variants"), + } + }}; +} + +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), )+ + _ => unreachable!("get_common guarantees matching variants"), + } + }}; +} + +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), )+ + _ => unreachable!("get_common guarantees matching variants"), + } + }}; } impl StaticInt { @@ -202,6 +242,8 @@ impl StaticInt { match self { StaticInt::IntInit(_) => Type::Int, StaticInt::LongInit(_) => Type::Long, + StaticInt::UIntInit(_) => Type::UInt, + StaticInt::ULongInit(_) => Type::ULong, } } @@ -209,11 +251,45 @@ impl StaticInt { 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), + } + } + + /// Exact value of this constant as a signedness-split wide integer (see [`Wide`]). + /// INVARIANT: `Wide`'s arms must stay >= the widest StaticInt variant — widen them to + /// 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), + } + } + + 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()), } } pub fn get_common(self, other: Self) -> (Self, Self) { - let common_type = get_common_type(&self.get_type(), &other.get_type()); + let common_type = self.get_type().common_with(&other.get_type()); // Promotion only widens to the common type, so it never truncates — ignore the flag. let (left, _) = convert_to_type(self, &common_type); let (right, _) = convert_to_type(other, &common_type); @@ -221,74 +297,40 @@ impl StaticInt { } fn neg(self) -> (Self, bool) { - match self { - StaticInt::IntInit(v) => { - let (v, o) = v.overflowing_neg(); - (StaticInt::IntInit(v), o) - } - StaticInt::LongInit(v) => { - let (v, o) = v.overflowing_neg(); - (StaticInt::LongInit(v), o) - } - } + checked_op!(self, overflowing_neg; IntInit, LongInit, UIntInit, ULongInit) } fn add(self, other: Self) -> (Self, bool) { - let (left, right) = self.get_common(other); - match (left, right) { - (StaticInt::IntInit(a), StaticInt::IntInit(b)) => { - let (v, o) = a.overflowing_add(b); - (StaticInt::IntInit(v), o) - } - (StaticInt::LongInit(a), StaticInt::LongInit(b)) => { - let (v, o) = a.overflowing_add(b); - (StaticInt::LongInit(v), o) - } - _ => unreachable!(), - } + checked_op!(self, other, overflowing_add; IntInit, LongInit, UIntInit, ULongInit) } fn sub(self, other: Self) -> (Self, bool) { - let (left, right) = self.get_common(other); - match (left, right) { - (StaticInt::IntInit(a), StaticInt::IntInit(b)) => { - let (v, o) = a.overflowing_sub(b); - (StaticInt::IntInit(v), o) - } - (StaticInt::LongInit(a), StaticInt::LongInit(b)) => { - let (v, o) = a.overflowing_sub(b); - (StaticInt::LongInit(v), o) - } - _ => unreachable!(), - } + checked_op!(self, other, overflowing_sub; IntInit, LongInit, UIntInit, ULongInit) } fn mul(self, other: Self) -> (Self, bool) { - let (left, right) = self.get_common(other); - match (left, right) { - (StaticInt::IntInit(a), StaticInt::IntInit(b)) => { - let (v, o) = a.overflowing_mul(b); - (StaticInt::IntInit(v), o) - } - (StaticInt::LongInit(a), StaticInt::LongInit(b)) => { - let (v, o) = a.overflowing_mul(b); - (StaticInt::LongInit(v), o) - } - _ => unreachable!(), - } + checked_op!(self, other, overflowing_mul; IntInit, LongInit, UIntInit, ULongInit) } fn is_zero(&self) -> bool { - match self { - StaticInt::IntInit(v) => *v == 0, - StaticInt::LongInit(v) => *v == 0, - } + 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, + } + } + + 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, } } @@ -296,145 +338,75 @@ impl StaticInt { if other.is_zero() { return Err(ConstEvalError::DivByZero); } - let (left, right) = self.get_common(other); - Ok(match (left, right) { - (StaticInt::IntInit(a), StaticInt::IntInit(b)) => { - let (v, o) = a.overflowing_div(b); - (StaticInt::IntInit(v), o) - } - (StaticInt::LongInit(a), StaticInt::LongInit(b)) => { - let (v, o) = a.overflowing_div(b); - (StaticInt::LongInit(v), o) - } - _ => unreachable!(), - }) + 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); } - let (left, right) = self.get_common(other); - Ok(match (left, right) { - (StaticInt::IntInit(a), StaticInt::IntInit(b)) => { - let (v, o) = a.overflowing_rem(b); - (StaticInt::IntInit(v), o) - } - (StaticInt::LongInit(a), StaticInt::LongInit(b)) => { - let (v, o) = a.overflowing_rem(b); - (StaticInt::LongInit(v), o) - } - _ => unreachable!(), - }) + Ok(checked_op!(self, other, overflowing_rem; IntInit, LongInit, UIntInit, ULongInit)) } fn bitwise_and(self, other: Self) -> Self { - let (left, right) = self.get_common(other); - match (left, right) { - (StaticInt::IntInit(a), StaticInt::IntInit(b)) => StaticInt::IntInit(a & b), - (StaticInt::LongInit(a), StaticInt::LongInit(b)) => StaticInt::LongInit(a & b), - _ => unreachable!(), - } + bitwise_op!(self, other, &; IntInit, LongInit, UIntInit, ULongInit) } - fn bitwise_or(self, other: Self) -> Self { - let (left, right) = self.get_common(other); - match (left, right) { - (StaticInt::IntInit(a), StaticInt::IntInit(b)) => StaticInt::IntInit(a | b), - (StaticInt::LongInit(a), StaticInt::LongInit(b)) => StaticInt::LongInit(a | b), - _ => unreachable!(), - } + bitwise_op!(self, other, |; IntInit, LongInit, UIntInit, ULongInit) } - fn bitwise_xor(self, other: Self) -> Self { - let (left, right) = self.get_common(other); - match (left, right) { - (StaticInt::IntInit(a), StaticInt::IntInit(b)) => StaticInt::IntInit(a ^ b), - (StaticInt::LongInit(a), StaticInt::LongInit(b)) => StaticInt::LongInit(a ^ b), - _ => unreachable!(), - } + bitwise_op!(self, other, ^; IntInit, LongInit, UIntInit, ULongInit) } /// Left shift. Shift amount is masked (& 31 for int, & 63 for long) to prevent /// undefined behavior, matching x86 hardware semantics. fn shl(self, other: Self) -> Self { - let shift_amount = match other { - StaticInt::IntInit(v) => v as u32, - StaticInt::LongInit(v) => v as u32, - }; + 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)), } } - /// Right shift (arithmetic). Shift amount is masked (& 31 for int, & 63 for long) - /// to prevent undefined behavior, matching x86 hardware semantics. + /// Right shift: arithmetic for signed operands, logical for unsigned (Rust's `>>` + /// follows the operand's signedness, matching x86 SAR vs SHR). Shift amount is masked + /// (& 31 for 32-bit, & 63 for 64-bit) to prevent undefined behavior, matching x86 hardware. fn shr(self, other: Self) -> Self { - let shift_amount = match other { - StaticInt::IntInit(v) => v as u32, - StaticInt::LongInit(v) => v as u32, - }; + 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)), } } fn eq(self, other: Self) -> Self { - let (left, right) = self.get_common(other); - match (left, right) { - (StaticInt::IntInit(a), StaticInt::IntInit(b)) => StaticInt::IntInit(if a == b { 1 } else { 0 }), - (StaticInt::LongInit(a), StaticInt::LongInit(b)) => StaticInt::IntInit(if a == b { 1 } else { 0 }), - _ => unreachable!(), - } + compare_op!(self, other, ==; IntInit, LongInit, UIntInit, ULongInit) } fn ne(self, other: Self) -> Self { - let (left, right) = self.get_common(other); - match (left, right) { - (StaticInt::IntInit(a), StaticInt::IntInit(b)) => StaticInt::IntInit(if a == b { 0 } else { 1 }), - (StaticInt::LongInit(a), StaticInt::LongInit(b)) => StaticInt::IntInit(if a == b { 0 } else { 1 }), - _ => unreachable!(), - } + compare_op!(self, other, !=; IntInit, LongInit, UIntInit, ULongInit) } fn lt(self, other: Self) -> Self { - let (left, right) = self.get_common(other); - match (left, right) { - (StaticInt::IntInit(a), StaticInt::IntInit(b)) => StaticInt::IntInit(if a < b { 1 } else { 0 }), - (StaticInt::LongInit(a), StaticInt::LongInit(b)) => StaticInt::IntInit(if a < b { 1 } else { 0 }), - _ => unreachable!(), - } + compare_op!(self, other, <; IntInit, LongInit, UIntInit, ULongInit) } fn le(self, other: Self) -> Self { - let (left, right) = self.get_common(other); - match (left, right) { - (StaticInt::IntInit(a), StaticInt::IntInit(b)) => StaticInt::IntInit(if a <= b { 1 } else { 0 }), - (StaticInt::LongInit(a), StaticInt::LongInit(b)) => StaticInt::IntInit(if a <= b { 1 } else { 0 }), - _ => unreachable!(), - } + compare_op!(self, other, <=; IntInit, LongInit, UIntInit, ULongInit) } fn gt(self, other: Self) -> Self { - let (left, right) = self.get_common(other); - match (left, right) { - (StaticInt::IntInit(a), StaticInt::IntInit(b)) => StaticInt::IntInit(if a > b { 1 } else { 0 }), - (StaticInt::LongInit(a), StaticInt::LongInit(b)) => StaticInt::IntInit(if a > b { 1 } else { 0 }), - _ => unreachable!(), - } + compare_op!(self, other, >; IntInit, LongInit, UIntInit, ULongInit) } fn ge(self, other: Self) -> Self { - let (left, right) = self.get_common(other); - match (left, right) { - (StaticInt::IntInit(a), StaticInt::IntInit(b)) => StaticInt::IntInit(if a >= b { 1 } else { 0 }), - (StaticInt::LongInit(a), StaticInt::LongInit(b)) => StaticInt::IntInit(if a >= b { 1 } else { 0 }), - _ => unreachable!(), - } + compare_op!(self, other, >=; IntInit, LongInit, UIntInit, ULongInit) } fn and(self, other: Self) -> Self { @@ -450,7 +422,7 @@ impl StaticInt { /// /// # Fields /// -/// - `symbol_type`: The type of the symbol (Int, Long, or FunType) +/// - `symbol_type`: The type of the symbol (Int, Long, UInt, ULong, or FunType) /// - For functions, `FunType.defined` tracks if the function has a body /// /// - `global`: Linkage scope @@ -559,6 +531,8 @@ fn typecheck_local_variable_declaration( 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"), }; InitialValue::Initial(zero) @@ -642,7 +616,7 @@ fn typecheck_file_variable_declaration( decl.span, )); } - Type::Int | Type::Long => { + Type::Int | Type::Long | Type::UInt | Type::ULong => { if old_dec.symbol_type != decl.var_type { return Err(SemanticError::with_span( format!( @@ -721,10 +695,6 @@ fn typecheck_file_variable_declaration( }) } -pub(crate) fn get_common_type(t1: &Type, t2: &Type) -> Type { - if t1 == t2 { t1.clone() } else { Type::Long } -} - fn convert_to(exp: TypedExpression, t: &Type) -> TypedExpression { if exp.exp_type == *t { exp @@ -947,6 +917,14 @@ fn typecheck_exp(exp: ParserExpr, symbols: &mut SymbolTable) -> Result Ok(TypedExpression { + exp_type: Type::UInt, + exp: Expr::Const(c), + }), + Const::ConstULong(_) => Ok(TypedExpression { + exp_type: Type::ULong, + exp: Expr::Const(c), + }), }, ParserExpr::Cast(t, inner) => { let typed_inner = typecheck_exp(*inner, symbols)?; @@ -996,7 +974,7 @@ fn typecheck_exp(exp: ParserExpr, symbols: &mut SymbolTable) -> Result Result Result left_type.clone(), - _ => get_common_type(&typed_lhs.exp_type, &typed_rhs.exp_type), + _ => typed_lhs.exp_type.common_with(&typed_rhs.exp_type), }; let compound = Expr::CompoundAssignment(op, Box::new(typed_lhs), Box::new(typed_rhs), op_type); Ok(compound.with_type(left_type)) @@ -1167,7 +1145,7 @@ fn typecheck_function_declaration( global = old_dec.global; defined = defined || *old_defined; } - Type::Int | Type::Long => { + Type::Int | Type::Long | Type::UInt | Type::ULong => { return Err(SemanticError::with_span( format!( "redeclaration of '{}' as a function\n{}: {}: previous declaration was here", @@ -1745,23 +1723,46 @@ fn resolve_statement( } } -/// Converts a constant to `target_type`. Returns `(value, truncated)` where `truncated` is true -/// only for a narrowing (`long`->`int`) that changed the value — the `-Wconstant-conversion` signal. -/// Widening and same-type conversions never truncate. The caller decides whether to warn -/// (implicit conversions do; explicit casts and internal promotion do not). -fn convert_to_type(val: StaticInt, target_type: &Type) -> (StaticInt, bool) { - match (val, target_type) { - (StaticInt::IntInit(v), Type::Int) => (StaticInt::IntInit(v), false), - (StaticInt::LongInit(v), Type::Long) => (StaticInt::LongInit(v), false), +/// 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 +/// invariant on widening this to i128/u128. +#[derive(Clone, Copy)] +enum Wide { + Signed(i64), + Unsigned(u64), +} - (StaticInt::IntInit(v), Type::Long) => (StaticInt::LongInit(v as i64), false), - (StaticInt::LongInit(v), Type::Int) => { - let truncated = v as i32; - (StaticInt::IntInit(truncated), truncated as i64 != v) +/// 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::FunType { .. } => unreachable!("Cannot cast to function type in constant expression"), } + }; +} - (_, Type::FunType { .. }) => unreachable!("Cannot cast to function type in constant expression"), - } +/// Converts a constant to `target_type`. Returns `(value, truncated)` where `truncated` is true +/// only for a *narrowing* that changed the value — the `-Wconstant-conversion`/`-Woverflow` signal. +/// Widening and same-width conversions (including sign reinterprets like `-1` -> unsigned) never set +/// it; those are gcc's separate, off-by-default `-Wsign-conversion`. The caller decides whether to +/// warn (implicit conversions do; explicit casts and internal promotion do not). +// `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) { + let source_bits = val.get_type().size_bits(); + let (result, out_of_range) = match val.wide() { + Wide::Signed(v) => to_target!(v, target_type), + Wide::Unsigned(v) => to_target!(v, target_type), + }; + let truncated = out_of_range && target_type.size_bits() < source_bits; + (result, truncated) } enum ConstEvalError { @@ -1821,6 +1822,8 @@ fn eval_constant_expr(expr: &ParserExpr) -> Result<(StaticInt, bool), ConstEvalE 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::Cast(target, val) => { let (v, o) = eval_constant_expr(val)?; // Explicit cast: suppress the conversion-truncation warning (programmer intent), but @@ -1836,17 +1839,15 @@ fn eval_constant_expr(expr: &ParserExpr) -> Result<(StaticInt, bool), ConstEvalE 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), }, false, ), - UnaryOp::Not => ( - match v { - StaticInt::IntInit(n) => StaticInt::IntInit(if n == 0 { 1 } else { 0 }), - StaticInt::LongInit(n) => StaticInt::IntInit(if n == 0 { 1 } else { 0 }), - }, - false, - ), + UnaryOp::Not => (StaticInt::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(); Ok((r, o | o2)) } ParserExpr::Binary(op, left, right, _) => { @@ -1876,6 +1877,8 @@ fn eval_constant_expr(expr: &ParserExpr) -> Result<(StaticInt, bool), ConstEvalE unreachable!("only used for parsing") } }; + // Unsigned wraparound is well-defined, not overflow — only signed ops warn (-Woverflow). + let op_ovf = op_ovf && v.get_type().is_signed(); Ok((v, base | op_ovf)) } ParserExpr::Conditional(cond, true_expr, false_expr) => { @@ -1946,6 +1949,8 @@ impl LabelTracker { 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), }; // Just collect cases with spans - duplicate checking happens during typecheck self.switch_to_cases.get_mut(label).unwrap().push((case_exp, *span)); diff --git a/tests/c_programs/expected_results.json b/tests/c_programs/expected_results.json index 135013f..3750945 100644 --- a/tests/c_programs/expected_results.json +++ b/tests/c_programs/expected_results.json @@ -53,6 +53,9 @@ "int_wrapping/int_wrapping.c": { "return_code": 0 }, + "int_wrapping/long_min_literal.c": { + "return_code": 2 + }, "int_wrapping/loop_wrapping.c": { "return_code": 2 }, @@ -65,6 +68,9 @@ "static_vars/data_operand_coverage.c": { "return_code": 0 }, + "static_vars/mixed_alignment.c": { + "return_code": 4 + }, "static_vars/variable_shift_data.c": { "return_code": 0 }, diff --git a/tests/c_programs/int_wrapping/long_min_literal.c b/tests/c_programs/int_wrapping/long_min_literal.c new file mode 100644 index 0000000..25a8361 --- /dev/null +++ b/tests/c_programs/int_wrapping/long_min_literal.c @@ -0,0 +1,14 @@ +// Regression: LONG_MIN written with an explicit `L` suffix must parse. +// +// parse_factor folds a leading unary `-` into the literal string only for +// `ConstantInt` tokens. The unsuffixed form `-9223372036854775808` is rescued +// by the int->long overflow-promotion path, but the `L`-suffixed form produced +// a `ConstantLong` token that skipped the fold, so parse_constant tried +// `parse::("9223372036854775808")` (2^63, > i64::MAX) and errored with +// "does not fit in 64-bit int". Both spellings must yield LONG_MIN. +int main(void) { + long suffixed = -9223372036854775808L; + long unsuffixed = -9223372036854775808; // works via int->long promotion + long computed = -9223372036854775807L - 1; // (LONG_MIN + 1) - 1 = LONG_MIN + return (suffixed == unsuffixed) + (suffixed == computed); // 2 when correct +} diff --git a/tests/c_programs/static_vars/mixed_alignment.c b/tests/c_programs/static_vars/mixed_alignment.c new file mode 100644 index 0000000..2864e6e --- /dev/null +++ b/tests/c_programs/static_vars/mixed_alignment.c @@ -0,0 +1,20 @@ +/* Regression: file-scope statics of mixed alignment, smaller-aligned before larger-aligned. + * + * The emitter tracked each static's .data offset manually (`offset += alignment`) instead of + * using the offset that `append_section_data` returns. When a 4-byte static (int/unsigned) + * precedes an 8-byte static (long/unsigned long), the section gets padded to an 8-byte + * boundary, so the real offset jumps past where the manual counter points — and the later + * symbol resolved into the padding, loading garbage. + * + * Trigger is order-dependent: the smaller-aligned static MUST come first (4 bytes, then pad + * to 8). `big`/`big_u` use values that don't fit in 32 bits, so a wrong (truncated/garbage) + * load can't coincidentally compare equal. Returns 4 when every static loads correctly. + */ +int small = 5; +long big = 6442450941L; /* 0x1_8000_003D — bit 33 set, so truncation/garbage would differ */ +unsigned int small_u = 7u; +unsigned long big_u = 12884901890ul; /* 0x3_0000_0002 — bits above 32 set */ + +int main(void) { + return (small == 5) + (big == 6442450941L) + (small_u == 7u) + (big_u == 12884901890ul); +} diff --git a/tests/c_programs/warnings/no_overflow_unsigned.c b/tests/c_programs/warnings/no_overflow_unsigned.c new file mode 100644 index 0000000..91e4330 --- /dev/null +++ b/tests/c_programs/warnings/no_overflow_unsigned.c @@ -0,0 +1,14 @@ +/* -Woverflow must NOT fire on unsigned arithmetic: unsigned wraparound is well-defined + (modular, C 6.2.5p9), unlike signed overflow which is UB. This matches gcc/clang, which + stay silent here. Folded at compile time in static initializers; runs cleanly to 0. + + Each line wraps under two's complement but is defined for unsigned, so the overflowing_* + flag from constant folding must be suppressed when the operand type is unsigned. The + companion file overflow.c covers the signed cases that *should* warn. */ +unsigned int a = 4000000000u + 1000000000u; /* add: 5e9 wraps mod 2^32 */ +unsigned int b = 3000000000u * 3u; /* mul: 9e9 wraps mod 2^32 */ +unsigned int c = -1u; /* neg: -1u -> UINT_MAX */ +unsigned int d = 0u - 1u; /* sub: underflow -> UINT_MAX */ +unsigned long e = 18446744073709551615UL + 1UL; /* add: ULONG_MAX + 1 wraps to 0 */ + +int main(void) { return 0; } \ No newline at end of file diff --git a/tests/runner.rs b/tests/runner.rs index 1fc8ab1..68a2c33 100644 --- a/tests/runner.rs +++ b/tests/runner.rs @@ -3,8 +3,8 @@ use std::collections::HashMap; use std::fs; use std::path::PathBuf; -static CHAPTER_COMPLETED: i32 = 11; -static EXTRA_COMPLETED: i32 = 11; +static CHAPTER_COMPLETED: i32 = 12; +static EXTRA_COMPLETED: i32 = 12; #[derive(Debug, PartialEq, Clone)] enum ProgramOutput { @@ -1085,6 +1085,31 @@ fn test_no_overflow_on_in_range() { println!("✓ No overflow warning on in-range folds test passed"); } +#[test] +fn test_no_overflow_on_unsigned() { + // Unsigned wraparound is well-defined (modular arithmetic), so -Woverflow must NOT fire, + // matching gcc/clang. The overflowing_* flag from constant folding is suppressed when the + // operand type is unsigned; only signed overflow (UB) warns — see test_overflow_warning. + let test_file = "tests/c_programs/warnings/no_overflow_unsigned.c"; + + let ncc_path = get_ncc_binary_path(); + + let output = std::process::Command::new(&ncc_path) + .arg(test_file) + .arg("--validate") + .output() + .expect("Failed to execute ncc"); + + let stderr_output = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr_output.contains("-Woverflow"), + "Unsigned wraparound is defined and must not warn, but stderr was: {}", + stderr_output + ); + + println!("✓ No overflow warning on unsigned wraparound test passed"); +} + #[test] fn test_constant_conversion_warning() { let test_file = "tests/c_programs/warnings/constant_conversion.c"; From 3158caa9a12da8dc88c83ec1cf1b8e195efae4fd Mon Sep 17 00:00:00 2001 From: John Ring Date: Sat, 13 Jun 2026 21:32:57 -0400 Subject: [PATCH 2/3] Additional tests --- tests/c_programs/expected_results.json | 12 ++++++++++++ .../invalid_semantics/duplicate_case_ulong.c | 14 ++++++++++++++ tests/c_programs/unsigned/comparisons.c | 11 +++++++++++ tests/c_programs/unsigned/division.c | 10 ++++++++++ tests/c_programs/unsigned/shift.c | 13 +++++++++++++ tests/c_programs/unsigned/widening_casts.c | 13 +++++++++++++ 6 files changed, 73 insertions(+) create mode 100644 tests/c_programs/switch/invalid_semantics/duplicate_case_ulong.c create mode 100644 tests/c_programs/unsigned/comparisons.c create mode 100644 tests/c_programs/unsigned/division.c create mode 100644 tests/c_programs/unsigned/shift.c create mode 100644 tests/c_programs/unsigned/widening_casts.c diff --git a/tests/c_programs/expected_results.json b/tests/c_programs/expected_results.json index 3750945..855e56a 100644 --- a/tests/c_programs/expected_results.json +++ b/tests/c_programs/expected_results.json @@ -1,4 +1,16 @@ { + "unsigned/widening_casts.c": { + "return_code": 2 + }, + "unsigned/comparisons.c": { + "return_code": 4 + }, + "unsigned/shift.c": { + "return_code": 2 + }, + "unsigned/division.c": { + "return_code": 2 + }, "conditional/valid/assignment.c": { "return_code": 100 }, diff --git a/tests/c_programs/switch/invalid_semantics/duplicate_case_ulong.c b/tests/c_programs/switch/invalid_semantics/duplicate_case_ulong.c new file mode 100644 index 0000000..67c09ad --- /dev/null +++ b/tests/c_programs/switch/invalid_semantics/duplicate_case_ulong.c @@ -0,0 +1,14 @@ +/* Regression: duplicate-case detection at 64-bit width. Case labels are compared after + * conversion to the switch's controlling type. In an `unsigned long` switch, `-1` converts + * to ULONG_MAX, colliding with the explicit ULONG_MAX case -> must be a semantic error. + * (The 32-bit half is covered by Sandler's switch_duplicate_cases; this pins the 64-bit half + * of SwitchIntType::as_i64.) + */ +int main(void) { + unsigned long x = 0ul; + switch (x) { + case -1: return 1; /* converts to 18446744073709551615 */ + case 18446744073709551615UL: return 2; /* same value -> duplicate */ + default: return 0; + } +} \ No newline at end of file diff --git a/tests/c_programs/unsigned/comparisons.c b/tests/c_programs/unsigned/comparisons.c new file mode 100644 index 0000000..d9989eb --- /dev/null +++ b/tests/c_programs/unsigned/comparisons.c @@ -0,0 +1,11 @@ +/* Regression: unsigned comparisons must use unsigned condition codes (setb/seta/jb/ja), + * not signed (setl/setg). Codegen took signedness from the comparison's result (always int) + * instead of its operands. 4294967294u reads as -2 if compared as signed. + * Locals so the compare runs in codegen, not constant folding. + */ +int main(void) { + unsigned u = 4294967294u; /* -2 if misread as signed */ + unsigned hundred = 100u; + /* All true for unsigned; all false if compared as signed -> would return 0 */ + return (u > hundred) + (u >= hundred) + (hundred < u) + (hundred <= u); /* 4 */ +} \ No newline at end of file diff --git a/tests/c_programs/unsigned/division.c b/tests/c_programs/unsigned/division.c new file mode 100644 index 0000000..8ca703a --- /dev/null +++ b/tests/c_programs/unsigned/division.c @@ -0,0 +1,10 @@ +/* Regression: unsigned division uses div (+ zeroed RDX), not idiv (+ cdq); and a constant + * divisor must be materialized into a register (the Div(Imm) fixup, parallel to Idiv(Imm)). + * 4000000000u is negative if misread as signed, so signed division would give wrong results. + */ +int main(void) { + unsigned u = 4000000000u; + unsigned q = u / 7u; /* 571428571 */ + unsigned r = u % 7u; /* 3 */ + return (q == 571428571u) + (r == 3u); /* 2 */ +} \ No newline at end of file diff --git a/tests/c_programs/unsigned/shift.c b/tests/c_programs/unsigned/shift.c new file mode 100644 index 0000000..da8bda6 --- /dev/null +++ b/tests/c_programs/unsigned/shift.c @@ -0,0 +1,13 @@ +/* Regression: right shift selects shr (logical) for unsigned, sar (arithmetic) for signed, + * and a VARIABLE count must be routed through CL. The shift-count fixup arms originally + * omitted BitShr, so a variable-count unsigned right shift hit unreachable code. + * Uses a variable shift count to exercise the CL fixup. + */ +int main(void) { + unsigned u = 4294967294u; /* 0xFFFFFFFE */ + unsigned count = 1u; + int s = -2; + int u_ok = (u >> count) == 2147483647u; /* logical: 0x7FFFFFFF */ + int s_ok = (s >> 1) == -1; /* arithmetic: sign-preserving */ + return u_ok + s_ok; /* 2 */ +} \ No newline at end of file diff --git a/tests/c_programs/unsigned/widening_casts.c b/tests/c_programs/unsigned/widening_casts.c new file mode 100644 index 0000000..f74ae1b --- /dev/null +++ b/tests/c_programs/unsigned/widening_casts.c @@ -0,0 +1,13 @@ +/* Regression: widening casts must extend by the SOURCE's signedness, not the destination's. + * emit_cast keyed on the wrong operand, so mixed-sign widenings were wrong: + * - uint -> long must ZERO-extend (value preserved): (long)UINT_MAX == 4294967295 + * - int -> ulong must SIGN-extend: (unsigned long)(-1) == ULONG_MAX + * Locals (not constants) so the conversion runs in codegen, not constant folding. + */ +int main(void) { + unsigned int a = 4294967295u; /* UINT_MAX */ + int b = -1; + long la = (long)a; /* zero-extend */ + unsigned long lb = (unsigned long)b; /* sign-extend */ + return (la == 4294967295L) + (lb == 18446744073709551615UL); /* 2 when both correct */ +} \ No newline at end of file From d5f2c9c4e82afad63820a6f5c6a0436907875c99 Mon Sep 17 00:00:00 2001 From: John Ring Date: Sun, 14 Jun 2026 08:47:53 -0400 Subject: [PATCH 3/3] Minor codegen refactor --- src/codegen.rs | 116 +++++++++++++++++++++++++++---------------------- 1 file changed, 63 insertions(+), 53 deletions(-) diff --git a/src/codegen.rs b/src/codegen.rs index 0bcf967..9e38532 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -33,7 +33,7 @@ //! //! ```text //! generate() — public entry point, orchestrates all 4 passes -//! ├─ build_backend_symbol_table() — map all vars/fns to assembly types +//! ├─ build_backend_symbol_table() — map all vars/fns to their types //! ├─ convert_function() — per function: lower params + body //! │ └─ convert_instruction() — per instruction: TACKY -> assembly //! │ └─ convert_function_call() — System V ABI argument passing @@ -260,27 +260,6 @@ pub struct Program { pub static_vars: Vec, } -/// Operand size (`Longword`/`Quadword`) for `val` — constants by their `Const` variant, -/// variables by their type in the backend symbol table. Drives instruction sizing/suffixes. -fn get_assembly_type(val: &Val, symbols: &BackendSymbolTable) -> AssemblyType { - match val { - Val::Constant(Const::ConstInt(_) | Const::ConstUInt(_)) => AssemblyType::Longword, - Val::Constant(Const::ConstLong(_) | Const::ConstULong(_)) => AssemblyType::Quadword, - Val::Var(name) => symbols.get_var_type(name).into(), - } -} - -/// Whether `val` has a signed type — selects signed vs unsigned instructions (`idiv`/`div`, -/// signed/unsigned condition codes). Constants are classified by their `Const` variant; -/// variables delegate to [`Type::is_signed`] via the backend symbol table. -fn is_signed(val: &Val, symbols: &BackendSymbolTable) -> bool { - match val { - Val::Constant(Const::ConstInt(_) | Const::ConstLong(_)) => true, - Val::Constant(Const::ConstUInt(_) | Const::ConstULong(_)) => false, - Val::Var(name) => symbols.get_var_type(name).is_signed(), - } -} - /// 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). @@ -311,12 +290,12 @@ fn convert_function_call( instructions.push(Instruction::Mov { src: tacky_arg.into(), dst: Operand::Reg(reg), - size: get_assembly_type(tacky_arg, symbols), + size: symbols.get_assembly_type(tacky_arg), }); } for tacky_arg in stack_args.iter().rev() { let assembly_arg = tacky_arg.into(); - let asm_type = get_assembly_type(tacky_arg, symbols); + 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)); } else { @@ -342,7 +321,7 @@ fn convert_function_call( instructions.push(Instruction::Mov { src: Operand::Reg(Reg::AX), dst: dst.into(), - size: get_assembly_type(dst, symbols), + size: symbols.get_assembly_type(dst), }); instructions @@ -352,7 +331,7 @@ fn convert_function_call( /// (instruction selection, pass 1), still operating on pseudo-registers. /// /// Most ops map straightforwardly; the type-dependent ones consult `symbols` for operand -/// size ([`get_assembly_type`]) and signedness ([`is_signed`]): +/// size ([`BackendSymbolTable::get_assembly_type`]) and signedness ([`BackendSymbolTable::is_signed`]): /// - **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). @@ -362,7 +341,7 @@ fn convert_function_call( fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbolTable) -> Vec { match instruction { tacky::Instruction::Return(x) => { - let size = get_assembly_type(x, symbols); + let size = symbols.get_assembly_type(x); vec![ Instruction::Mov { src: x.into(), @@ -377,7 +356,7 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol src, dst, } => { - let size = get_assembly_type(src, symbols); + let size = symbols.get_assembly_type(src); vec![ Instruction::Cmp { v1: Operand::Imm(0), @@ -396,7 +375,7 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol ] } tacky::Instruction::Unary { op, src, dst } => { - let size = get_assembly_type(src, symbols); + let size = symbols.get_assembly_type(src); let op = convert_unary_op(op); vec![ Instruction::Mov { @@ -412,7 +391,7 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol ] } tacky::Instruction::Binary { op, src1, src2, dst } => { - let size = get_assembly_type(src1, symbols); + let size = symbols.get_assembly_type(src1); match op { BinOp::Add | BinOp::Subtract @@ -423,7 +402,13 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol | BinOp::BitwiseLeftShift | BinOp::BitwiseRightShift => { let asm_op = match op { - BinOp::BitwiseRightShift if !is_signed(src1, symbols) => BinaryOp::BitShr, // logical + BinOp::BitwiseRightShift => { + if symbols.is_signed(src1) { + BinaryOp::BitSar + } else { + BinaryOp::BitShr + } + } _ => BinaryOp::from(op), // BitSar / everything else }; vec![ @@ -442,7 +427,7 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol } BinOp::Divide | BinOp::Remainder => { let result_reg = if *op == BinOp::Divide { Reg::AX } else { Reg::DX }; - let signed = is_signed(dst, symbols); + let signed = symbols.is_signed(dst); let mut ins = vec![Instruction::Mov { src: src1.into(), dst: Operand::Reg(Reg::AX), @@ -473,7 +458,7 @@ 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 = is_signed(src1, symbols); + let signed = symbols.is_signed(src1); let code = match (op, signed) { (BinOp::Equal, _) => CondCode::E, (BinOp::NotEqual, _) => CondCode::NE, @@ -504,7 +489,7 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol } } tacky::Instruction::JumpIfZero { condition, target } => { - let size = get_assembly_type(condition, symbols); + let size = symbols.get_assembly_type(condition); vec![ Instruction::Cmp { v1: Operand::Imm(0), @@ -518,7 +503,7 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol ] } tacky::Instruction::JumpIfNotZero { condition, target } => { - let size = get_assembly_type(condition, symbols); + let size = symbols.get_assembly_type(condition); vec![ Instruction::Cmp { v1: Operand::Imm(0), @@ -538,7 +523,7 @@ fn convert_instruction(instruction: &tacky::Instruction, symbols: &BackendSymbol vec![Instruction::Label(label.clone())] } tacky::Instruction::Copy { src, dst } => { - let size = get_assembly_type(src, symbols); + let size = symbols.get_assembly_type(src); vec![Instruction::Mov { src: src.into(), dst: dst.into(), @@ -587,7 +572,7 @@ impl From<&BinOp> for BinaryOp { BinOp::BitwiseOr => BinaryOp::BitOr, BinOp::BitwiseXOr => BinaryOp::BitXOr, BinOp::BitwiseLeftShift => BinaryOp::BitShl, - BinOp::BitwiseRightShift => BinaryOp::BitSar, + BinOp::BitwiseRightShift => unreachable!("right shift needs signedness; handled in convert_instruction"), BinOp::Equal | BinOp::NotEqual | BinOp::LessThan @@ -611,7 +596,6 @@ fn convert_unary_op(op: &parser::UnaryOp) -> UnaryOp { /// 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. -//todo should this consume FunctionDefination fn convert_function(ast: &tacky::FunctionDefinition, symbols: &BackendSymbolTable) -> FunctionDefinition { let tacky::FunctionDefinition { name, @@ -672,32 +656,58 @@ pub enum AsmSymbolEntry { }, } -pub type BackendSymbolTable = HashMap, AsmSymbolEntry>; - -/// Operand-type lookups on the backend symbol table: a variable's `Type` (`get_var_type`) -/// and the `AssemblyType` (size) derived from it (`get_obj_type`). -pub trait BackendSymbolTableExt { - fn get_obj_type(&self, name: &str) -> AssemblyType; - fn get_var_type(&self, name: &str) -> &Type; -} +/// Backend symbol table: maps each name to its [`AsmSymbolEntry`], with helpers to look up a +/// variable's `Type` and classify an operand's size/signedness for instruction selection. +pub struct BackendSymbolTable(HashMap, AsmSymbolEntry>); -impl BackendSymbolTableExt for BackendSymbolTable { - fn get_obj_type(&self, name: &str) -> AssemblyType { - self.get_var_type(name).into() +impl BackendSymbolTable { + fn new() -> Self { + Self(HashMap::new()) + } + fn insert(&mut self, name: Rc, entry: AsmSymbolEntry) { + self.0.insert(name, entry); } + /// The declared `Type` of variable `name` (panics if absent or a function). fn get_var_type(&self, name: &str) -> &Type { - match self.get(name).expect("Variable not in symbol table") { + match self.0.get(name).expect("Variable not in symbol table") { AsmSymbolEntry::Obj { var_type, .. } => var_type, AsmSymbolEntry::Fun { .. } => unreachable!("Expected object type, found function: {}", name), } } + + /// Operand size (`Longword`/`Quadword`) for `val` — constants by their `Const` variant, + /// variables by their type in the backend symbol table. Drives instruction sizing/suffixes. + fn get_assembly_type(&self, val: &Val) -> AssemblyType { + match val { + 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(), + } + } + + /// Whether `val` has a signed type — selects signed vs unsigned instructions (`idiv`/`div`, + /// signed/unsigned condition codes). Constants are classified by their `Const` variant; + /// variables delegate to [`Type::is_signed`] via the backend symbol table. + fn is_signed(&self, val: &Val) -> bool { + match val { + Val::Constant(Const::ConstInt(_) | Const::ConstLong(_)) => true, + Val::Constant(Const::ConstUInt(_) | Const::ConstULong(_)) => false, + Val::Var(name) => self.get_var_type(name).is_signed(), + } + } + + /// 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 { + self.get_var_type(name).into() + } } /// Builds the backend symbol table from the frontend symbol table and TACKY IR. /// -/// Maps all symbols to their assembly types: frontend symbols (variables and functions) -/// from the validator's symbol table, plus TACKY temporaries from each function definition. +/// Maps all symbols to their types: frontend symbols (variables and functions) from the +/// validator's symbol table, plus TACKY temporaries from each function definition. fn build_backend_symbol_table(ast: &tacky::Program, symbols: &SymbolTable) -> BackendSymbolTable { let mut backend = BackendSymbolTable::new(); let static_names: HashSet<&str> = ast.static_vars.iter().map(|sv| &*sv.name).collect(); @@ -706,7 +716,7 @@ fn build_backend_symbol_table(ast: &tacky::Program, symbols: &SymbolTable) -> Ba let backend_entry = match &symbol.symbol_type { Type::FunType { defined, .. } => AsmSymbolEntry::Fun { defined: *defined }, ty => AsmSymbolEntry::Obj { - var_type: ty.clone(), //todo any way to consume and avoid clone + var_type: ty.clone(), is_static: static_names.contains(&**name), }, };