@@ -3594,13 +3594,13 @@ fn capitalize(text: &str) -> String {
35943594}
35953595
35963596fn value_fits_reg_width ( value : i64 , width : RegWidth ) -> bool {
3597- if value < 0 {
3598- return false ;
3599- }
3600- match width {
3601- RegWidth :: W8 => value <= i64 :: from ( u8 :: MAX ) ,
3602- RegWidth :: W16 => value <= i64 :: from ( u16 :: MAX ) ,
3603- }
3597+ let bits = match width {
3598+ RegWidth :: W8 => 8 ,
3599+ RegWidth :: W16 => 16 ,
3600+ } ;
3601+ let min = - ( 1i64 << ( bits - 1 ) ) ;
3602+ let max = ( 1i64 << bits ) - 1 ;
3603+ ( min..=max ) . contains ( & value )
36043604}
36053605
36063606fn immediate_width_error ( span : Span , value : i64 , width : RegWidth , mnemonic : & str ) -> Diagnostic {
@@ -4185,6 +4185,10 @@ fn eval_to_number_strict(
41854185 ExprUnaryOp :: HighByte => Some ( ( value >> 8 ) & 0xFF ) ,
41864186 ExprUnaryOp :: WordLittleEndian | ExprUnaryOp :: FarLittleEndian => Some ( value) ,
41874187 ExprUnaryOp :: EvalBracketed => Some ( value) ,
4188+ ExprUnaryOp :: Negate => value. checked_neg ( ) . or_else ( || {
4189+ diagnostics. push ( Diagnostic :: error ( span, "arithmetic overflow" ) ) ;
4190+ None
4191+ } ) ,
41884192 }
41894193 }
41904194 Expr :: TypedView { expr, .. } | Expr :: AddressHint { expr, .. } => {
@@ -4364,7 +4368,8 @@ fn resolve_symbolic_byte_relocation(
43644368 ExprUnaryOp :: HighByte => ByteRelocationKind :: HighByte ,
43654369 ExprUnaryOp :: WordLittleEndian
43664370 | ExprUnaryOp :: FarLittleEndian
4367- | ExprUnaryOp :: EvalBracketed => return None ,
4371+ | ExprUnaryOp :: EvalBracketed
4372+ | ExprUnaryOp :: Negate => return None ,
43684373 } ;
43694374
43704375 let symbol = expr_ident_name ( expr. as_ref ( ) ) ?;
@@ -5196,6 +5201,10 @@ fn eval_to_number(
51965201 ExprUnaryOp :: HighByte => Some ( ( value >> 8 ) & 0xFF ) ,
51975202 ExprUnaryOp :: WordLittleEndian | ExprUnaryOp :: FarLittleEndian => Some ( value) ,
51985203 ExprUnaryOp :: EvalBracketed => Some ( value) ,
5204+ ExprUnaryOp :: Negate => value. checked_neg ( ) . or_else ( || {
5205+ diagnostics. push ( Diagnostic :: error ( span, "arithmetic overflow" ) ) ;
5206+ None
5207+ } ) ,
51995208 }
52005209 }
52015210 Expr :: TypedView { expr, .. } | Expr :: AddressHint { expr, .. } => {
@@ -5599,6 +5608,70 @@ mod tests {
55995608 assert ! ( matches!( operand, OperandOp :: Immediate ( 0x34 ) ) ) ;
56005609 }
56015610
5611+ #[ test]
5612+ fn resolves_negative_const_operand_to_signed_immediate ( ) {
5613+ let source = "const A = -1\n func main @a8 {\n lda #A\n }\n " ;
5614+ let file = parse ( SourceId ( 0 ) , source) . expect ( "parse" ) ;
5615+ let sema = analyze ( & file) . expect ( "analyze" ) ;
5616+ let fs = StdAssetFS ;
5617+ let program = lower ( & file, & sema, & fs, None ) . expect ( "lower" ) ;
5618+
5619+ let operand = program
5620+ . ops
5621+ . iter ( )
5622+ . find_map ( |op| match & op. node {
5623+ Op :: Instruction ( instruction) if instruction. mnemonic == "lda" => {
5624+ instruction. operand . as_ref ( )
5625+ }
5626+ _ => None ,
5627+ } )
5628+ . expect ( "lda operand" ) ;
5629+
5630+ assert ! ( matches!( operand, OperandOp :: Immediate ( -1 ) ) ) ;
5631+ }
5632+
5633+ #[ test]
5634+ fn accepts_immediate_at_signed_lower_bound_a8 ( ) {
5635+ // -128 fits the signed range of 8-bit accumulator immediates.
5636+ let source = "func main @a8 {\n lda #-128\n }\n " ;
5637+ let file = parse ( SourceId ( 0 ) , source) . expect ( "parse" ) ;
5638+ let sema = analyze ( & file) . expect ( "analyze" ) ;
5639+ let fs = StdAssetFS ;
5640+ lower ( & file, & sema, & fs, None ) . expect ( "lower" ) ;
5641+ }
5642+
5643+ #[ test]
5644+ fn rejects_immediate_below_signed_lower_bound_a8 ( ) {
5645+ // -129 falls outside both i8 and u8 — error.
5646+ let source = "func main @a8 {\n lda #-129\n }\n " ;
5647+ let file = parse ( SourceId ( 0 ) , source) . expect ( "parse" ) ;
5648+ let sema = analyze ( & file) . expect ( "analyze" ) ;
5649+ let fs = StdAssetFS ;
5650+ let errors = lower ( & file, & sema, & fs, None ) . expect_err ( "must fail" ) ;
5651+ assert ! (
5652+ errors
5653+ . iter( )
5654+ . any( |e| e. message. contains( "does not fit in @a8" ) ) ,
5655+ "expected @a8 width error, got {errors:?}"
5656+ ) ;
5657+ }
5658+
5659+ #[ test]
5660+ fn rejects_immediate_above_unsigned_upper_bound_a8 ( ) {
5661+ // 256 fits neither i8 nor u8 — error.
5662+ let source = "func main @a8 {\n lda #256\n }\n " ;
5663+ let file = parse ( SourceId ( 0 ) , source) . expect ( "parse" ) ;
5664+ let sema = analyze ( & file) . expect ( "analyze" ) ;
5665+ let fs = StdAssetFS ;
5666+ let errors = lower ( & file, & sema, & fs, None ) . expect_err ( "must fail" ) ;
5667+ assert ! (
5668+ errors
5669+ . iter( )
5670+ . any( |e| e. message. contains( "does not fit in @a8" ) ) ,
5671+ "expected @a8 width error, got {errors:?}"
5672+ ) ;
5673+ }
5674+
56025675 #[ test]
56035676 fn resolves_bare_const_operand_to_immediate_value ( ) {
56045677 let source = "const LIMIT = 0x34\n func main {\n lda LIMIT\n }\n " ;
0 commit comments