Added keyword REGISTER for local BYTE variables to help a new optimization mode
This commit is contained in:
parent
11cc5f220b
commit
501f58a968
31 changed files with 850 additions and 126 deletions
|
|
@ -5,7 +5,7 @@ for dir in */; do
|
||||||
file="$dir/$name.c65"
|
file="$dir/$name.c65"
|
||||||
if [ -f "$file" ]; then
|
if [ -f "$file" ]; then
|
||||||
echo "=== Building $name ==="
|
echo "=== Building $name ==="
|
||||||
c65gm "$file"
|
c65gm build --opt -i "$file"
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@
|
||||||
# Define filename as variable
|
# Define filename as variable
|
||||||
PROGNAME="for_byte_max_test"
|
PROGNAME="for_byte_max_test"
|
||||||
# Compile and assemble directly
|
# Compile and assemble directly
|
||||||
c65gm ${PROGNAME}.c65
|
c65gm build --opt --keep-asm -i ${PROGNAME}.c65
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@
|
||||||
# Define filename as variable
|
# Define filename as variable
|
||||||
PROGNAME="hires"
|
PROGNAME="hires"
|
||||||
# Compile and assemble directly
|
# Compile and assemble directly
|
||||||
c65gm ${PROGNAME}.c65
|
c65gm build --opt --keep-asm -i ${PROGNAME}.c65
|
||||||
|
|
@ -5,7 +5,7 @@ GOTO start
|
||||||
|
|
||||||
|
|
||||||
FUNC sethires
|
FUNC sethires
|
||||||
BYTE b
|
BYTE REGISTER b
|
||||||
b = PEEK $d011
|
b = PEEK $d011
|
||||||
b = b | 32 //enable bitmap mode
|
b = b | 32 //enable bitmap mode
|
||||||
POKE $d011, b
|
POKE $d011, b
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@
|
||||||
# Define filename as variable
|
# Define filename as variable
|
||||||
PROGNAME="irq_demo"
|
PROGNAME="irq_demo"
|
||||||
# Compile and assemble directly
|
# Compile and assemble directly
|
||||||
c65gm ${PROGNAME}.c65
|
c65gm build --opt --keep-asm -i ${PROGNAME}.c65
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@
|
||||||
# Define filename as variable
|
# Define filename as variable
|
||||||
PROGNAME="load_binary_demo"
|
PROGNAME="load_binary_demo"
|
||||||
# Compile and assemble directly
|
# Compile and assemble directly
|
||||||
c65gm ${PROGNAME}.c65
|
c65gm build --opt --keep-asm -i ${PROGNAME}.c65
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@
|
||||||
# Define filename as variable
|
# Define filename as variable
|
||||||
PROGNAME="memlib_demo"
|
PROGNAME="memlib_demo"
|
||||||
# Compile and assemble directly
|
# Compile and assemble directly
|
||||||
c65gm ${PROGNAME}.c65
|
c65gm build --opt --keep-asm -i ${PROGNAME}.c65
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
PROGNAME="memlib_demo2"
|
PROGNAME="memlib_demo2"
|
||||||
c65gm ${PROGNAME}.c65
|
c65gm build --opt --keep-asm -i ${PROGNAME}.c65
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
PROGNAME="multdiv_demo"
|
PROGNAME="multdiv_demo"
|
||||||
c65gm ${PROGNAME}.c65
|
c65gm build --opt --keep-asm -i ${PROGNAME}.c65
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ GOTO start
|
||||||
|
|
||||||
|
|
||||||
FUNC setmulti
|
FUNC setmulti
|
||||||
BYTE b
|
BYTE REGISTER b
|
||||||
b = PEEK $d011
|
b = PEEK $d011
|
||||||
b = b | 32
|
b = b | 32
|
||||||
POKE $d011, b
|
POKE $d011, b
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ ENDSCRIPT
|
||||||
// VIC-II multi-color bitmap setup (unchanged from v1)
|
// VIC-II multi-color bitmap setup (unchanged from v1)
|
||||||
//-----------------------------------------------------------
|
//-----------------------------------------------------------
|
||||||
FUNC setmulti
|
FUNC setmulti
|
||||||
BYTE b
|
BYTE REGISTER b
|
||||||
b = PEEK $d011
|
b = PEEK $d011
|
||||||
b = b | 32
|
b = b | 32
|
||||||
POKE $d011, b
|
POKE $d011, b
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@
|
||||||
# Define filename as variable
|
# Define filename as variable
|
||||||
PROGNAME="script_library_demo"
|
PROGNAME="script_library_demo"
|
||||||
# Compile and assemble directly
|
# Compile and assemble directly
|
||||||
c65gm ${PROGNAME}.c65
|
c65gm build --opt --keep-asm -i ${PROGNAME}.c65
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@
|
||||||
# Define filename as variable
|
# Define filename as variable
|
||||||
PROGNAME="shift_demo"
|
PROGNAME="shift_demo"
|
||||||
# Compile and assemble directly
|
# Compile and assemble directly
|
||||||
c65gm ${PROGNAME}.c65
|
c65gm build --opt --keep-asm -i ${PROGNAME}.c65
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,8 @@ FEND
|
||||||
//-----------------------------------------------------------
|
//-----------------------------------------------------------
|
||||||
FUNC print_binary({BYTE val})
|
FUNC print_binary({BYTE val})
|
||||||
BYTE i = 7
|
BYTE i = 7
|
||||||
BYTE mask
|
BYTE REGISTER mask
|
||||||
BYTE bit
|
BYTE REGISTER bit
|
||||||
|
|
||||||
lib_cbmio_print(" %")
|
lib_cbmio_print(" %")
|
||||||
|
|
||||||
|
|
@ -139,8 +139,8 @@ FUNC demo_bit_manipulation
|
||||||
// Extract color components from C64 color byte
|
// Extract color components from C64 color byte
|
||||||
// C64 color: bits 7-4 = background, bits 3-0 = foreground
|
// C64 color: bits 7-4 = background, bits 3-0 = foreground
|
||||||
BYTE color = $3E // Background: 3, Foreground: E
|
BYTE color = $3E // Background: 3, Foreground: E
|
||||||
BYTE background
|
BYTE REGISTER background
|
||||||
BYTE foreground
|
BYTE REGISTER foreground
|
||||||
|
|
||||||
background = color >> 4
|
background = color >> 4
|
||||||
foreground = color & $0F
|
foreground = color & $0F
|
||||||
|
|
@ -159,7 +159,7 @@ FUNC demo_bit_manipulation
|
||||||
|
|
||||||
// Create bit masks
|
// Create bit masks
|
||||||
lib_cbmio_printlf("bit masks:")
|
lib_cbmio_printlf("bit masks:")
|
||||||
BYTE mask
|
BYTE REGISTER mask
|
||||||
|
|
||||||
mask = 1 << 0
|
mask = 1 << 0
|
||||||
lib_cbmio_print("1 << 0 = $")
|
lib_cbmio_print("1 << 0 = $")
|
||||||
|
|
@ -287,7 +287,7 @@ FUNC demo_word_operations
|
||||||
lib_cbmio_printlf("")
|
lib_cbmio_printlf("")
|
||||||
|
|
||||||
// Byte to word conversion with shift
|
// Byte to word conversion with shift
|
||||||
BYTE small = $81
|
BYTE REGISTER small = $81
|
||||||
WORD large
|
WORD large
|
||||||
|
|
||||||
large = small << 2 // Zero-extends byte to word, then shifts
|
large = small << 2 // Zero-extends byte to word, then shifts
|
||||||
|
|
@ -323,8 +323,8 @@ FUNC demo_c64_example
|
||||||
lib_cbmio_printlf("")
|
lib_cbmio_printlf("")
|
||||||
|
|
||||||
// Fire button (bit 4)
|
// Fire button (bit 4)
|
||||||
BYTE fire_mask
|
BYTE REGISTER fire_mask
|
||||||
BYTE fire_check
|
BYTE REGISTER fire_check
|
||||||
fire_mask = 1 << 4
|
fire_mask = 1 << 4
|
||||||
fire_check = joystick & fire_mask
|
fire_check = joystick & fire_mask
|
||||||
IF fire_check = 0
|
IF fire_check = 0
|
||||||
|
|
@ -334,8 +334,8 @@ FUNC demo_c64_example
|
||||||
ENDIF
|
ENDIF
|
||||||
|
|
||||||
// Up button (bit 0)
|
// Up button (bit 0)
|
||||||
BYTE up_mask
|
BYTE REGISTER up_mask
|
||||||
BYTE up_check
|
BYTE REGISTER up_check
|
||||||
up_mask = 1 << 0
|
up_mask = 1 << 0
|
||||||
up_check = joystick & up_mask
|
up_check = joystick & up_mask
|
||||||
IF up_check = 0
|
IF up_check = 0
|
||||||
|
|
@ -345,8 +345,8 @@ FUNC demo_c64_example
|
||||||
ENDIF
|
ENDIF
|
||||||
|
|
||||||
// Right button (bit 3)
|
// Right button (bit 3)
|
||||||
BYTE right_mask
|
BYTE REGISTER right_mask
|
||||||
BYTE right_check
|
BYTE REGISTER right_check
|
||||||
right_mask = 1 << 3
|
right_mask = 1 << 3
|
||||||
right_check = joystick & right_mask
|
right_check = joystick & right_mask
|
||||||
IF right_check = 0
|
IF right_check = 0
|
||||||
|
|
@ -357,7 +357,7 @@ FUNC demo_c64_example
|
||||||
lib_cbmio_printlf("")
|
lib_cbmio_printlf("")
|
||||||
|
|
||||||
// Extract direction bits to nibble
|
// Extract direction bits to nibble
|
||||||
BYTE direction
|
BYTE REGISTER direction
|
||||||
direction = joystick & $0F // Mask off fire button
|
direction = joystick & $0F // Mask off fire button
|
||||||
|
|
||||||
lib_cbmio_print("direction bits: $")
|
lib_cbmio_print("direction bits: $")
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@
|
||||||
# Define filename as variable
|
# Define filename as variable
|
||||||
PROGNAME="switch_demo"
|
PROGNAME="switch_demo"
|
||||||
# Compile and assemble directly
|
# Compile and assemble directly
|
||||||
c65gm ${PROGNAME}.c65
|
c65gm build --opt --keep-asm -i ${PROGNAME}.c65
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,8 @@ FEND
|
||||||
// Test 3: Nested SWITCH statements
|
// Test 3: Nested SWITCH statements
|
||||||
//-----------------------------------------------------------
|
//-----------------------------------------------------------
|
||||||
FUNC test_nested_switch
|
FUNC test_nested_switch
|
||||||
BYTE outer
|
BYTE REGISTER outer
|
||||||
BYTE inner
|
BYTE REGISTER inner
|
||||||
|
|
||||||
LET outer = 2
|
LET outer = 2
|
||||||
LET inner = 3
|
LET inner = 3
|
||||||
|
|
@ -180,8 +180,8 @@ FEND
|
||||||
// Test 7: SWITCH with variable cases (not just literals)
|
// Test 7: SWITCH with variable cases (not just literals)
|
||||||
//-----------------------------------------------------------
|
//-----------------------------------------------------------
|
||||||
FUNC test_variables
|
FUNC test_variables
|
||||||
BYTE match_val1
|
BYTE REGISTER match_val1
|
||||||
BYTE match_val2
|
BYTE REGISTER match_val2
|
||||||
WORD match_val3
|
WORD match_val3
|
||||||
|
|
||||||
LET match_val1 = 15
|
LET match_val1 = 15
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ import (
|
||||||
// BYTE varname = value # byte with init value
|
// BYTE varname = value # byte with init value
|
||||||
// BYTE varname @ address # byte at absolute address
|
// BYTE varname @ address # byte at absolute address
|
||||||
// BYTE CONST varname = value # constant byte
|
// BYTE CONST varname = value # constant byte
|
||||||
|
// BYTE REGISTER varname # register-hinted byte (function-local only)
|
||||||
|
// BYTE REGISTER varname = value # register-hinted byte with init value
|
||||||
type ByteCommand struct {
|
type ByteCommand struct {
|
||||||
varName string
|
varName string
|
||||||
value uint16
|
value uint16
|
||||||
|
|
@ -32,7 +34,6 @@ func (c *ByteCommand) WillHandle(line preproc.Line) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ByteCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error {
|
func (c *ByteCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error {
|
||||||
// Clear state
|
|
||||||
c.varName = ""
|
c.varName = ""
|
||||||
c.value = 0
|
c.value = 0
|
||||||
c.isConst = false
|
c.isConst = false
|
||||||
|
|
@ -45,21 +46,32 @@ func (c *ByteCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext
|
||||||
|
|
||||||
paramCount := len(params)
|
paramCount := len(params)
|
||||||
|
|
||||||
// Validate parameter count
|
// Check for REGISTER keyword
|
||||||
if paramCount != 2 && paramCount != 4 && paramCount != 5 {
|
register := false
|
||||||
return fmt.Errorf("BYTE: wrong number of parameters (%d)", paramCount)
|
if paramCount >= 2 && strings.ToUpper(params[1]) == "REGISTER" {
|
||||||
|
register = true
|
||||||
|
params = append(params[:1], params[2:]...)
|
||||||
|
paramCount = len(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
if register {
|
||||||
|
if paramCount != 2 && paramCount != 4 {
|
||||||
|
return fmt.Errorf("BYTE REGISTER: expected 'name' or 'name = value', got %d parameters after REGISTER", paramCount)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if paramCount != 2 && paramCount != 4 && paramCount != 5 {
|
||||||
|
return fmt.Errorf("BYTE: wrong number of parameters (%d)", paramCount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var varName string
|
var varName string
|
||||||
var value int64
|
var value int64
|
||||||
scope := ctx.FunctionHandler.CurrentFunction()
|
scope := ctx.FunctionHandler.CurrentFunction()
|
||||||
|
|
||||||
// Create constant lookup function
|
|
||||||
constLookup := ctx.SymbolTable.ConstantLookupFunc(ctx.CurrentScope())
|
constLookup := ctx.SymbolTable.ConstantLookupFunc(ctx.CurrentScope())
|
||||||
|
|
||||||
switch paramCount {
|
switch paramCount {
|
||||||
case 2:
|
case 2:
|
||||||
// BYTE varname
|
|
||||||
varName = params[1]
|
varName = params[1]
|
||||||
value = 0
|
value = 0
|
||||||
|
|
||||||
|
|
@ -67,10 +79,16 @@ func (c *ByteCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext
|
||||||
return fmt.Errorf("BYTE: invalid identifier %q", varName)
|
return fmt.Errorf("BYTE: invalid identifier %q", varName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if register {
|
||||||
|
if scope == "" {
|
||||||
|
return fmt.Errorf("BYTE REGISTER %q is only valid inside a FUNC block (remove REGISTER or move inside a function)", varName)
|
||||||
|
}
|
||||||
|
err = ctx.SymbolTable.AddRegisterVar(varName, scope, uint16(value), line)
|
||||||
|
} else {
|
||||||
err = ctx.SymbolTable.AddVar(varName, scope, compiler.KindByte, uint16(value), line)
|
err = ctx.SymbolTable.AddVar(varName, scope, compiler.KindByte, uint16(value), line)
|
||||||
|
}
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
// BYTE varname = value OR BYTE varname @ address
|
|
||||||
varName = params[1]
|
varName = params[1]
|
||||||
operator := params[2]
|
operator := params[2]
|
||||||
valueStr := params[3]
|
valueStr := params[3]
|
||||||
|
|
@ -85,14 +103,22 @@ func (c *ByteCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext
|
||||||
}
|
}
|
||||||
|
|
||||||
if operator == "=" {
|
if operator == "=" {
|
||||||
// BYTE varname = value
|
|
||||||
if value < 0 || value > 255 {
|
if value < 0 || value > 255 {
|
||||||
return fmt.Errorf("BYTE: init value %d out of range (0-255)", value)
|
return fmt.Errorf("BYTE: init value %d out of range (0-255)", value)
|
||||||
}
|
}
|
||||||
err = ctx.SymbolTable.AddVar(varName, scope, compiler.KindByte, uint16(value), line)
|
if register {
|
||||||
|
if scope == "" {
|
||||||
|
return fmt.Errorf("BYTE REGISTER: variable %q must be declared in function scope", varName)
|
||||||
|
}
|
||||||
|
err = ctx.SymbolTable.AddRegisterVar(varName, scope, uint16(value), line)
|
||||||
|
} else {
|
||||||
|
err = ctx.SymbolTable.AddVar(varName, scope, compiler.KindByte, uint16(value), line)
|
||||||
|
}
|
||||||
|
|
||||||
} else if operator == "@" {
|
} else if operator == "@" {
|
||||||
// BYTE varname @ address
|
if register {
|
||||||
|
return fmt.Errorf("BYTE REGISTER: @-mapped address is not valid for register variables")
|
||||||
|
}
|
||||||
if value < 0 || value > 0xFFFF {
|
if value < 0 || value > 0xFFFF {
|
||||||
return fmt.Errorf("BYTE: absolute address $%X out of range", value)
|
return fmt.Errorf("BYTE: absolute address $%X out of range", value)
|
||||||
}
|
}
|
||||||
|
|
@ -104,7 +130,9 @@ func (c *ByteCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext
|
||||||
}
|
}
|
||||||
|
|
||||||
case 5:
|
case 5:
|
||||||
// BYTE CONST varname = value
|
if register {
|
||||||
|
return fmt.Errorf("BYTE REGISTER: unexpected additional parameters")
|
||||||
|
}
|
||||||
constKeyword := strings.ToUpper(params[1])
|
constKeyword := strings.ToUpper(params[1])
|
||||||
varName = params[2]
|
varName = params[2]
|
||||||
operator := params[3]
|
operator := params[3]
|
||||||
|
|
|
||||||
|
|
@ -438,3 +438,66 @@ func TestByteCommand_ConstantNotFound(t *testing.T) {
|
||||||
t.Errorf("Error should mention constant not found, got: %v", err)
|
t.Errorf("Error should mention constant not found, got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestByteCommand_RegisterGlobalScope(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
|
||||||
|
cmd := &ByteCommand{}
|
||||||
|
line := preproc.Line{
|
||||||
|
Text: "BYTE REGISTER x",
|
||||||
|
Filename: "test.c65",
|
||||||
|
LineNo: 1,
|
||||||
|
Kind: preproc.Source,
|
||||||
|
PragmaSetIndex: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Interpret(line, ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected error for REGISTER in global scope")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "REGISTER") {
|
||||||
|
t.Errorf("Error should mention REGISTER, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestByteCommand_RegisterAtMapped(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
|
||||||
|
cmd := &ByteCommand{}
|
||||||
|
line := preproc.Line{
|
||||||
|
Text: "BYTE REGISTER x @ $02",
|
||||||
|
Filename: "test.c65",
|
||||||
|
LineNo: 1,
|
||||||
|
Kind: preproc.Source,
|
||||||
|
PragmaSetIndex: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Interpret(line, ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected error for REGISTER with @-mapped address")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "@-mapped") && !strings.Contains(err.Error(), "REGISTER") {
|
||||||
|
t.Errorf("Error should mention @-mapped issue, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestByteCommand_RegisterConst(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
|
||||||
|
cmd := &ByteCommand{}
|
||||||
|
line := preproc.Line{
|
||||||
|
Text: "BYTE REGISTER CONST x = 5",
|
||||||
|
Filename: "test.c65",
|
||||||
|
LineNo: 1,
|
||||||
|
Kind: preproc.Source,
|
||||||
|
PragmaSetIndex: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Interpret(line, ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected error for REGISTER CONST")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,11 @@ func (c *WordCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext
|
||||||
|
|
||||||
paramCount := len(params)
|
paramCount := len(params)
|
||||||
|
|
||||||
|
// Reject WORD REGISTER (6502 has no 16-bit ALU register)
|
||||||
|
if paramCount >= 2 && strings.ToUpper(params[1]) == "REGISTER" {
|
||||||
|
return fmt.Errorf("WORD REGISTER is not supported; only BYTE variables may use the REGISTER hint (the 6502 has no 16-bit ALU register). Use a @-mapped zero-page WORD instead")
|
||||||
|
}
|
||||||
|
|
||||||
// Validate parameter count
|
// Validate parameter count
|
||||||
if paramCount != 2 && paramCount != 4 && paramCount != 5 {
|
if paramCount != 2 && paramCount != 4 && paramCount != 5 {
|
||||||
return fmt.Errorf("WORD: wrong number of parameters (%d)", paramCount)
|
return fmt.Errorf("WORD: wrong number of parameters (%d)", paramCount)
|
||||||
|
|
|
||||||
|
|
@ -636,3 +636,25 @@ func TestWordCommand_MultipleStrings(t *testing.T) {
|
||||||
t.Errorf("Expected at least 9 lines of string declarations, got %d", len(strDecls))
|
t.Errorf("Expected at least 9 lines of string declarations, got %d", len(strDecls))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWordCommand_RegisterError(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
|
||||||
|
cmd := &WordCommand{}
|
||||||
|
line := preproc.Line{
|
||||||
|
Text: "WORD REGISTER x",
|
||||||
|
Filename: "test.c65",
|
||||||
|
LineNo: 1,
|
||||||
|
Kind: preproc.Source,
|
||||||
|
PragmaSetIndex: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Interpret(line, ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected error for WORD REGISTER")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "REGISTER") {
|
||||||
|
t.Errorf("Error should mention REGISTER, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ type Compiler struct {
|
||||||
ctx *CompilerContext
|
ctx *CompilerContext
|
||||||
registry *CommandRegistry
|
registry *CommandRegistry
|
||||||
deferredAsm []string // ASM blocks with _P_ASM_AFTER_VARS pragma
|
deferredAsm []string // ASM blocks with _P_ASM_AFTER_VARS pragma
|
||||||
|
dissolvedVars map[string]bool // REGISTER vars dissolved by optimizer
|
||||||
CmdlineOpt bool // --opt enables all passes
|
CmdlineOpt bool // --opt enables all passes
|
||||||
CmdlineDebug bool // --opt-debug enables debug output
|
CmdlineDebug bool // --opt-debug enables debug output
|
||||||
CmdlineIORegions []optimizer.IORegion // --opt-exclude ranges
|
CmdlineIORegions []optimizer.IORegion // --opt-exclude ranges
|
||||||
|
|
@ -197,6 +198,11 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
varName := codePart[start+1 : end]
|
varName := codePart[start+1 : end]
|
||||||
|
sym := c.ctx.SymbolTable.LookupWithoutUsage(varName, c.ctx.CurrentScope())
|
||||||
|
if sym != nil && sym.IsRegister() {
|
||||||
|
c.printErrorWithContext(lines, i, fmt.Errorf("REGISTER variable %q cannot be referenced from ASM blocks", varName))
|
||||||
|
return nil, fmt.Errorf("compilation failed")
|
||||||
|
}
|
||||||
expandedName := c.ctx.SymbolTable.ExpandName(varName, c.ctx.CurrentScope())
|
expandedName := c.ctx.SymbolTable.ExpandName(varName, c.ctx.CurrentScope())
|
||||||
codePart = codePart[:start] + expandedName + codePart[end+1:]
|
codePart = codePart[:start] + expandedName + codePart[end+1:]
|
||||||
// Continue searching after the replacement
|
// Continue searching after the replacement
|
||||||
|
|
@ -271,7 +277,9 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
|
|
||||||
// Peephole optimization pass
|
// Peephole optimization pass
|
||||||
if cfg := c.getOptimizerConfig(); cfg != nil {
|
if cfg := c.getOptimizerConfig(); cfg != nil {
|
||||||
codeOutput = optimizer.Optimize(codeOutput, cfg)
|
var dissolved map[string]bool
|
||||||
|
codeOutput, dissolved = optimizer.Optimize(codeOutput, cfg)
|
||||||
|
c.dissolvedVars = dissolved
|
||||||
}
|
}
|
||||||
|
|
||||||
// Analyze for overlapping absolute addresses in function call chains
|
// Analyze for overlapping absolute addresses in function call chains
|
||||||
|
|
@ -281,7 +289,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
funcsWithRemovePragma := c.ctx.FunctionHandler.GetFunctionsWithRemovePragma()
|
funcsWithRemovePragma := c.ctx.FunctionHandler.GetFunctionsWithRemovePragma()
|
||||||
|
|
||||||
// Check for unused variables and print warnings (skip variables in functions with remove pragma)
|
// Check for unused variables and print warnings (skip variables in functions with remove pragma)
|
||||||
warnings := c.ctx.SymbolTable.CheckUnused(funcsWithRemovePragma)
|
warnings := c.ctx.SymbolTable.CheckUnused(funcsWithRemovePragma, c.dissolvedVars)
|
||||||
for _, warning := range warnings {
|
for _, warning := range warnings {
|
||||||
_, _ = fmt.Fprintf(os.Stderr, "%s\n", warning)
|
_, _ = fmt.Fprintf(os.Stderr, "%s\n", warning)
|
||||||
}
|
}
|
||||||
|
|
@ -342,11 +350,20 @@ func (c *Compiler) getOptimizerConfig() *optimizer.Config {
|
||||||
cfg.EnableJmp = true
|
cfg.EnableJmp = true
|
||||||
cfg.EnableSelf = true
|
cfg.EnableSelf = true
|
||||||
cfg.EnableStoreLoad = true
|
cfg.EnableStoreLoad = true
|
||||||
|
cfg.EnableRegisterVars = true
|
||||||
}
|
}
|
||||||
if c.CmdlineDebug {
|
if c.CmdlineDebug {
|
||||||
cfg.Debug = true
|
cfg.Debug = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Populate register variable names for the optimizer
|
||||||
|
cfg.RegisterVars = make(map[string]bool)
|
||||||
|
for _, sym := range c.ctx.SymbolTable.Symbols() {
|
||||||
|
if sym.IsRegister() {
|
||||||
|
cfg.RegisterVars[sym.FullName()] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !cfg.Any() {
|
if !cfg.Any() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -724,7 +741,7 @@ func (c *Compiler) assembleOutput(codeLines []string, removedFuncs map[string]bo
|
||||||
output = append(output, "")
|
output = append(output, "")
|
||||||
|
|
||||||
// Variables section
|
// Variables section
|
||||||
if varLines := GenerateVariables(c.ctx.SymbolTable, removedFuncs); len(varLines) > 0 {
|
if varLines := GenerateVariables(c.ctx.SymbolTable, removedFuncs, c.dissolvedVars); len(varLines) > 0 {
|
||||||
output = append(output, varLines...)
|
output = append(output, varLines...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,11 @@ func (fh *FunctionHandler) HandleFuncDecl(line preproc.Line) (string, error) {
|
||||||
return "", fmt.Errorf("%s:%d: FUNC %s: parameter %q cannot be a constant", line.Filename, line.LineNo, funcName, varName)
|
return "", fmt.Errorf("%s:%d: FUNC %s: parameter %q cannot be a constant", line.Filename, line.LineNo, funcName, varName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sym.IsRegister() && direction.Has(DirOut) {
|
||||||
|
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1]
|
||||||
|
return "", fmt.Errorf("%s:%d: FUNC %s: REGISTER parameter %q cannot be out: or io: (REGISTER values do not persist after the function call)", line.Filename, line.LineNo, funcName, varName)
|
||||||
|
}
|
||||||
|
|
||||||
funcParams = append(funcParams, &FuncParam{
|
funcParams = append(funcParams, &FuncParam{
|
||||||
Symbol: sym,
|
Symbol: sym,
|
||||||
Direction: direction,
|
Direction: direction,
|
||||||
|
|
@ -614,15 +619,26 @@ func (fh *FunctionHandler) processConstValue(value uint16, param *FuncParam, fun
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseImplicitDecl parses {BYTE varname} or {WORD varname} or {BYTE varname @ address} and adds to symbol table
|
// parseImplicitDecl parses {BYTE varname} or {WORD varname} or {BYTE REGISTER varname} or {BYTE varname @ address} and adds to symbol table
|
||||||
func (fh *FunctionHandler) parseImplicitDecl(decl string, funcName string, line preproc.Line) error {
|
func (fh *FunctionHandler) parseImplicitDecl(decl string, funcName string, line preproc.Line) error {
|
||||||
parts := strings.Fields(decl)
|
parts := strings.Fields(decl)
|
||||||
if len(parts) != 2 && len(parts) != 4 {
|
if len(parts) < 2 || len(parts) > 4 {
|
||||||
return fmt.Errorf("implicit declaration must be 'TYPE name' or 'TYPE name @ addr', got: %q", decl)
|
return fmt.Errorf("implicit declaration must be 'TYPE name', 'TYPE REGISTER name', or 'TYPE name @ addr', got: %q", decl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typeIdx := 0
|
||||||
typeStr := strings.ToUpper(parts[0])
|
typeStr := strings.ToUpper(parts[0])
|
||||||
varName := parts[1]
|
register := false
|
||||||
|
|
||||||
|
// Check for REGISTER keyword after type
|
||||||
|
if len(parts) >= 3 && strings.ToUpper(parts[1]) == "REGISTER" {
|
||||||
|
register = true
|
||||||
|
typeIdx = 1 // parts[1] consumed as REGISTER
|
||||||
|
}
|
||||||
|
|
||||||
|
if register && typeStr == "WORD" {
|
||||||
|
return fmt.Errorf("WORD REGISTER is not supported; only BYTE variables may use the REGISTER hint")
|
||||||
|
}
|
||||||
|
|
||||||
var kind VarKind
|
var kind VarKind
|
||||||
switch typeStr {
|
switch typeStr {
|
||||||
|
|
@ -634,33 +650,48 @@ func (fh *FunctionHandler) parseImplicitDecl(decl string, funcName string, line
|
||||||
return fmt.Errorf("implicit declaration type must be BYTE or WORD, got: %s", typeStr)
|
return fmt.Errorf("implicit declaration type must be BYTE or WORD, got: %s", typeStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(parts) == 2 {
|
if register && kind != KindByte {
|
||||||
// Simple: BYTE name or WORD name
|
return fmt.Errorf("REGISTER hint is only valid for BYTE variables")
|
||||||
|
}
|
||||||
|
|
||||||
|
varName := parts[1+typeIdx]
|
||||||
|
|
||||||
|
// Simple declaration: TYPE [REGISTER] name
|
||||||
|
if len(parts) == 2+typeIdx {
|
||||||
|
if register {
|
||||||
|
return fh.symTable.AddRegisterVar(varName, funcName, 0, line)
|
||||||
|
}
|
||||||
return fh.symTable.AddVar(varName, funcName, kind, 0, line)
|
return fh.symTable.AddVar(varName, funcName, kind, 0, line)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extended: BYTE name @ address or WORD name @ address
|
// Extended: TYPE [REGISTER] name @ address
|
||||||
operator := parts[2]
|
if len(parts) == 4+typeIdx {
|
||||||
addrStr := parts[3]
|
operator := parts[2+typeIdx]
|
||||||
|
addrStr := parts[3+typeIdx]
|
||||||
|
|
||||||
if operator != "@" {
|
if register {
|
||||||
return fmt.Errorf("expected '@' operator, got: %q", operator)
|
return fmt.Errorf("REGISTER variable cannot be @-mapped; REGISTER is incompatible with fixed address")
|
||||||
|
}
|
||||||
|
|
||||||
|
if operator != "@" {
|
||||||
|
return fmt.Errorf("expected '@' operator, got: %q", operator)
|
||||||
|
}
|
||||||
|
|
||||||
|
constLookup := fh.symTable.ConstantLookupFunc([]string{funcName})
|
||||||
|
|
||||||
|
addr, err := utils.EvaluateExpression(addrStr, constLookup)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid address %q: %w", addrStr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr < 0 || addr > 0xFFFF {
|
||||||
|
return fmt.Errorf("absolute address $%X out of range", addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fh.symTable.AddAbsolute(varName, funcName, kind, uint16(addr), line)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create constant lookup function for address evaluation
|
return fmt.Errorf("invalid implicit declaration format: %q", decl)
|
||||||
constLookup := fh.symTable.ConstantLookupFunc([]string{funcName})
|
|
||||||
|
|
||||||
// Parse address (supports $hex and decimal) using EvaluateExpression
|
|
||||||
addr, err := utils.EvaluateExpression(addrStr, constLookup)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid address %q: %w", addrStr, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if addr < 0 || addr > 0xFFFF {
|
|
||||||
return fmt.Errorf("absolute address $%X out of range", addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fh.symTable.AddAbsolute(varName, funcName, kind, uint16(addr), line)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndFunction pops all functions from the stack (called by FEND)
|
// EndFunction pops all functions from the stack (called by FEND)
|
||||||
|
|
@ -863,7 +894,7 @@ func parseParamSpec(spec string) (ParamDirection, string, bool, string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for implicit declaration {TYPE name}
|
// Check for implicit declaration {TYPE name} or {TYPE REGISTER name}
|
||||||
if strings.HasPrefix(varName, "{") && strings.HasSuffix(varName, "}") {
|
if strings.HasPrefix(varName, "{") && strings.HasSuffix(varName, "}") {
|
||||||
isImplicit = true
|
isImplicit = true
|
||||||
implicitDecl = varName[1 : len(varName)-1] // strip { }
|
implicitDecl = varName[1 : len(varName)-1] // strip { }
|
||||||
|
|
@ -873,7 +904,12 @@ func parseParamSpec(spec string) (ParamDirection, string, bool, string, error) {
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
return 0, "", false, "", fmt.Errorf("invalid implicit declaration: %q", varName)
|
return 0, "", false, "", fmt.Errorf("invalid implicit declaration: %q", varName)
|
||||||
}
|
}
|
||||||
varName = parts[1]
|
// Handle {TYPE REGISTER name} — variable name is parts[2]
|
||||||
|
if len(parts) >= 3 && strings.ToUpper(parts[1]) == "REGISTER" {
|
||||||
|
varName = parts[2]
|
||||||
|
} else {
|
||||||
|
varName = parts[1]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return direction, varName, isImplicit, implicitDecl, nil
|
return direction, varName, isImplicit, implicitDecl, nil
|
||||||
|
|
|
||||||
|
|
@ -1958,3 +1958,91 @@ func TestMultiFuncGroupRemoval(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseImplicitDecl_Register(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
symTable := NewSymbolTable()
|
||||||
|
fh := NewFunctionHandler(symTable, NewLabelStack("L"), nil, pragma)
|
||||||
|
|
||||||
|
err := fh.parseImplicitDecl("BYTE REGISTER temp", "myFunc", preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseImplicitDecl with REGISTER failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sym := symTable.Lookup("temp", []string{"myFunc"})
|
||||||
|
if sym == nil {
|
||||||
|
t.Fatal("expected symbol temp to be found")
|
||||||
|
}
|
||||||
|
if !sym.IsRegister() {
|
||||||
|
t.Error("expected IsRegister() to be true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseImplicitDecl_WordRegisterError(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
symTable := NewSymbolTable()
|
||||||
|
fh := NewFunctionHandler(symTable, NewLabelStack("L"), nil, pragma)
|
||||||
|
|
||||||
|
err := fh.parseImplicitDecl("WORD REGISTER ptr", "myFunc", preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for WORD REGISTER")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "REGISTER") {
|
||||||
|
t.Errorf("error should mention REGISTER, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseImplicitDecl_RegisterAtError(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
symTable := NewSymbolTable()
|
||||||
|
fh := NewFunctionHandler(symTable, NewLabelStack("L"), nil, pragma)
|
||||||
|
|
||||||
|
err := fh.parseImplicitDecl("BYTE REGISTER temp @ $FB", "myFunc", preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for REGISTER with @")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleFuncDecl_OutRegisterError(t *testing.T) {
|
||||||
|
st := NewSymbolTable()
|
||||||
|
ls := NewLabelStack("L")
|
||||||
|
csh := NewConstantStringHandler()
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
fh := NewFunctionHandler(st, ls, csh, pragma)
|
||||||
|
|
||||||
|
_, err := fh.HandleFuncDecl(makeLine("FUNC test_out_reg ( out:{BYTE REGISTER temp} )"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for out: REGISTER parameter")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "REGISTER") && !strings.Contains(err.Error(), "out:") {
|
||||||
|
t.Errorf("error should mention REGISTER and out:, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleFuncDecl_ImplicitRegisterParam(t *testing.T) {
|
||||||
|
st := NewSymbolTable()
|
||||||
|
ls := NewLabelStack("L")
|
||||||
|
csh := NewConstantStringHandler()
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
fh := NewFunctionHandler(st, ls, csh, pragma)
|
||||||
|
|
||||||
|
funcName, err := fh.HandleFuncDecl(makeLine("FUNC test_reg ( {BYTE REGISTER temp} )"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("HandleFuncDecl with REGISTER failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if funcName != "test_reg" {
|
||||||
|
t.Fatalf("expected funcName = \"test_reg\", got %q", funcName)
|
||||||
|
}
|
||||||
|
|
||||||
|
sym := st.Lookup("temp", []string{"test_reg"})
|
||||||
|
if sym == nil {
|
||||||
|
t.Fatal("REGISTER parameter not declared")
|
||||||
|
}
|
||||||
|
if !sym.IsRegister() {
|
||||||
|
t.Error("expected IsRegister() to be true")
|
||||||
|
}
|
||||||
|
if !sym.IsByte() {
|
||||||
|
t.Error("expected IsByte() to be true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,10 @@ func executeScript(scriptLines []preproc.Line, ctx *CompilerContext, isLibrary b
|
||||||
scriptText := strings.Join(texts, "\n")
|
scriptText := strings.Join(texts, "\n")
|
||||||
|
|
||||||
// Expand |varname| -> actual variable names
|
// Expand |varname| -> actual variable names
|
||||||
scriptText = expandVariables(scriptText, ctx)
|
scriptText, err := expandVariables(scriptText, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Determine the source filename for Starlark
|
// Determine the source filename for Starlark
|
||||||
sourceFile := scriptLines[0].Filename
|
sourceFile := scriptLines[0].Filename
|
||||||
|
|
@ -298,7 +301,7 @@ func executeScript(scriptLines []preproc.Line, ctx *CompilerContext, isLibrary b
|
||||||
}
|
}
|
||||||
|
|
||||||
// expandVariables replaces |varname| with expanded variable names from symbol table
|
// expandVariables replaces |varname| with expanded variable names from symbol table
|
||||||
func expandVariables(text string, ctx *CompilerContext) string {
|
func expandVariables(text string, ctx *CompilerContext) (string, error) {
|
||||||
result := text
|
result := text
|
||||||
for {
|
for {
|
||||||
start := strings.IndexByte(result, '|')
|
start := strings.IndexByte(result, '|')
|
||||||
|
|
@ -312,10 +315,14 @@ func expandVariables(text string, ctx *CompilerContext) string {
|
||||||
end += start + 1
|
end += start + 1
|
||||||
|
|
||||||
varName := result[start+1 : end]
|
varName := result[start+1 : end]
|
||||||
|
sym := ctx.SymbolTable.LookupWithoutUsage(varName, ctx.CurrentScope())
|
||||||
|
if sym != nil && sym.IsRegister() {
|
||||||
|
return "", fmt.Errorf("REGISTER variable %q cannot be referenced from SCRIPT/MACRO blocks", varName)
|
||||||
|
}
|
||||||
expandedName := ctx.SymbolTable.ExpandName(varName, ctx.CurrentScope())
|
expandedName := ctx.SymbolTable.ExpandName(varName, ctx.CurrentScope())
|
||||||
result = result[:start] + expandedName + result[end+1:]
|
result = result[:start] + expandedName + result[end+1:]
|
||||||
}
|
}
|
||||||
return result
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateScriptFilePath checks that path is safe and resolves it within the project root.
|
// validateScriptFilePath checks that path is safe and resolves it within the project root.
|
||||||
|
|
@ -547,7 +554,10 @@ func ExecuteMacro(macroName string, args []string, ctx *CompilerContext, pragmaS
|
||||||
|
|
||||||
// Expand |varname| -> actual variable names in the OUTPUT
|
// Expand |varname| -> actual variable names in the OUTPUT
|
||||||
// This happens at call site, so local variables are resolved using caller's scope
|
// This happens at call site, so local variables are resolved using caller's scope
|
||||||
outputStr = expandVariables(outputStr, ctx)
|
outputStr, err = expandVariables(outputStr, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return strings.Split(strings.TrimRight(outputStr, "\n"), "\n"), nil
|
return strings.Split(strings.TrimRight(outputStr, "\n"), "\n"), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ const (
|
||||||
FlagAbsolute
|
FlagAbsolute
|
||||||
FlagZeroPage
|
FlagZeroPage
|
||||||
FlagLabelRef
|
FlagLabelRef
|
||||||
|
FlagRegister
|
||||||
)
|
)
|
||||||
|
|
||||||
// Symbol represents a variable, constant, or label reference
|
// Symbol represents a variable, constant, or label reference
|
||||||
|
|
@ -74,6 +75,7 @@ func (s *Symbol) IsWord() bool { return s.Has(FlagWord) }
|
||||||
func (s *Symbol) IsConst() bool { return s.Has(FlagConst) }
|
func (s *Symbol) IsConst() bool { return s.Has(FlagConst) }
|
||||||
func (s *Symbol) IsAbsolute() bool { return s.Has(FlagAbsolute) }
|
func (s *Symbol) IsAbsolute() bool { return s.Has(FlagAbsolute) }
|
||||||
func (s *Symbol) IsZeroPage() bool { return s.Has(FlagZeroPage) }
|
func (s *Symbol) IsZeroPage() bool { return s.Has(FlagZeroPage) }
|
||||||
|
func (s *Symbol) IsRegister() bool { return s.Has(FlagRegister) }
|
||||||
func (s *Symbol) IsZeroPagePointer() bool { return s.HasAll(FlagAbsolute | FlagZeroPage | FlagWord) }
|
func (s *Symbol) IsZeroPagePointer() bool { return s.HasAll(FlagAbsolute | FlagZeroPage | FlagWord) }
|
||||||
|
|
||||||
// FullName returns the fully qualified name (scope.name or just name)
|
// FullName returns the fully qualified name (scope.name or just name)
|
||||||
|
|
@ -143,6 +145,20 @@ func (st *SymbolTable) AddVar(name, scope string, kind VarKind, initValue uint16
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddRegisterVar adds a REGISTER-hinted byte variable (function-local only, BYTE only)
|
||||||
|
func (st *SymbolTable) AddRegisterVar(name, scope string, initValue uint16, line preproc.Line) error {
|
||||||
|
if scope == "" {
|
||||||
|
return fmt.Errorf("BYTE REGISTER %q is only valid inside a FUNC block (remove REGISTER or move inside a function)", name)
|
||||||
|
}
|
||||||
|
return st.add(&Symbol{
|
||||||
|
Name: name,
|
||||||
|
Scope: scope,
|
||||||
|
Flags: FlagByte | FlagRegister,
|
||||||
|
Value: initValue,
|
||||||
|
Line: line,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// AddConst adds a constant (byte or word)
|
// AddConst adds a constant (byte or word)
|
||||||
func (st *SymbolTable) AddConst(name, scope string, kind VarKind, value uint16, line preproc.Line) error {
|
func (st *SymbolTable) AddConst(name, scope string, kind VarKind, value uint16, line preproc.Line) error {
|
||||||
var flags SymbolFlags
|
var flags SymbolFlags
|
||||||
|
|
@ -340,15 +356,13 @@ func (st *SymbolTable) ConstantLookupFunc(currentScopes []string) func(string) (
|
||||||
// CheckUnused returns warnings for unused variables
|
// CheckUnused returns warnings for unused variables
|
||||||
// Returns slice of warning messages for regular variables (not constants, not absolutes) that were never used
|
// Returns slice of warning messages for regular variables (not constants, not absolutes) that were never used
|
||||||
// excludeFuncs is a map of function names that will be removed (e.g., have _P_REMOVE_UNUSED pragma)
|
// excludeFuncs is a map of function names that will be removed (e.g., have _P_REMOVE_UNUSED pragma)
|
||||||
func (st *SymbolTable) CheckUnused(excludeFuncs map[string]bool) []string {
|
// dissolvedVars is a set of REGISTER variables dissolved by the optimizer (do not warn)
|
||||||
|
func (st *SymbolTable) CheckUnused(excludeFuncs map[string]bool, dissolvedVars map[string]bool) []string {
|
||||||
var warnings []string
|
var warnings []string
|
||||||
for _, sym := range st.symbols {
|
for _, sym := range st.symbols {
|
||||||
// Skip constants and absolute variables (they shouldn't track usage)
|
// Skip constants and absolute variables (they shouldn't track usage)
|
||||||
if sym.IsConst() || sym.IsAbsolute() {
|
if sym.IsConst() || sym.IsAbsolute() {
|
||||||
// Sanity check: constants and absolutes should never be marked as used
|
|
||||||
// If they are, it's a bug in the compiler
|
|
||||||
if sym.IsUsed() {
|
if sym.IsUsed() {
|
||||||
// This would be an internal error, but we'll just skip it
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|
@ -359,6 +373,11 @@ func (st *SymbolTable) CheckUnused(excludeFuncs map[string]bool) []string {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip dissolved REGISTER variables (dissolved is the intended outcome)
|
||||||
|
if sym.IsRegister() && dissolvedVars != nil && dissolvedVars[sym.FullName()] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Check if pragma indicates we should ignore unused warnings for this variable
|
// Check if pragma indicates we should ignore unused warnings for this variable
|
||||||
if st.pragma != nil {
|
if st.pragma != nil {
|
||||||
pragmaSet := st.pragma.GetPragmaSetByIndex(sym.Line.PragmaSetIndex)
|
pragmaSet := st.pragma.GetPragmaSetByIndex(sym.Line.PragmaSetIndex)
|
||||||
|
|
@ -370,7 +389,6 @@ func (st *SymbolTable) CheckUnused(excludeFuncs map[string]bool) []string {
|
||||||
|
|
||||||
// Check if variable was never used
|
// Check if variable was never used
|
||||||
if !sym.IsUsed() {
|
if !sym.IsUsed() {
|
||||||
// Format warning message with file and line info
|
|
||||||
var scopeInfo string
|
var scopeInfo string
|
||||||
if sym.Scope != "" {
|
if sym.Scope != "" {
|
||||||
scopeInfo = fmt.Sprintf(" in function '%s'", sym.Scope)
|
scopeInfo = fmt.Sprintf(" in function '%s'", sym.Scope)
|
||||||
|
|
@ -502,7 +520,7 @@ func GenerateAbsolutes(st *SymbolTable, excludeScopes map[string]bool) []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateVariables generates variable declarations (name !8 $value)
|
// GenerateVariables generates variable declarations (name !8 $value)
|
||||||
func GenerateVariables(st *SymbolTable, excludeScopes map[string]bool) []string {
|
func GenerateVariables(st *SymbolTable, excludeScopes map[string]bool, dissolvedVars map[string]bool) []string {
|
||||||
var lines []string
|
var lines []string
|
||||||
hasVars := false
|
hasVars := false
|
||||||
|
|
||||||
|
|
@ -515,6 +533,10 @@ func GenerateVariables(st *SymbolTable, excludeScopes map[string]bool) []string
|
||||||
if excludeScopes != nil && sym.Scope != "" && excludeScopes[sym.Scope] {
|
if excludeScopes != nil && sym.Scope != "" && excludeScopes[sym.Scope] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Skip dissolved REGISTER variables (optimizer eliminated all references)
|
||||||
|
if sym.IsRegister() && dissolvedVars != nil && dissolvedVars[sym.FullName()] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
hasVars = true
|
hasVars = true
|
||||||
var line string
|
var line string
|
||||||
|
|
|
||||||
|
|
@ -580,7 +580,7 @@ func TestGenerateVariables(t *testing.T) {
|
||||||
// Absolute (should be skipped)
|
// Absolute (should be skipped)
|
||||||
st.AddAbsolute("SKIP2", "", KindByte, 0x80, preproc.Line{Filename: "test.c65", LineNo: 1})
|
st.AddAbsolute("SKIP2", "", KindByte, 0x80, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||||
|
|
||||||
lines := GenerateVariables(st, nil)
|
lines := GenerateVariables(st, nil, nil)
|
||||||
|
|
||||||
if len(lines) == 0 {
|
if len(lines) == 0 {
|
||||||
t.Fatal("expected output lines")
|
t.Fatal("expected output lines")
|
||||||
|
|
@ -630,7 +630,7 @@ func TestGenerateEmpty(t *testing.T) {
|
||||||
if lines := GenerateAbsolutes(st, nil); lines != nil {
|
if lines := GenerateAbsolutes(st, nil); lines != nil {
|
||||||
t.Error("expected nil for empty absolutes")
|
t.Error("expected nil for empty absolutes")
|
||||||
}
|
}
|
||||||
if lines := GenerateVariables(st, nil); lines != nil {
|
if lines := GenerateVariables(st, nil, nil); lines != nil {
|
||||||
t.Error("expected nil for empty variables")
|
t.Error("expected nil for empty variables")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -652,7 +652,7 @@ func TestGenerateScopedVariables(t *testing.T) {
|
||||||
st.AddVar("local", "main", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
st.AddVar("local", "main", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||||
st.AddVar("nested", "main_helper", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
st.AddVar("nested", "main_helper", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||||
|
|
||||||
lines := GenerateVariables(st, nil)
|
lines := GenerateVariables(st, nil, nil)
|
||||||
output := strings.Join(lines, "\n")
|
output := strings.Join(lines, "\n")
|
||||||
|
|
||||||
// Check full names are used
|
// Check full names are used
|
||||||
|
|
@ -676,7 +676,7 @@ func TestGenerateHexLowercase(t *testing.T) {
|
||||||
|
|
||||||
constLines := GenerateConstants(st, nil)
|
constLines := GenerateConstants(st, nil)
|
||||||
absLines := GenerateAbsolutes(st, nil)
|
absLines := GenerateAbsolutes(st, nil)
|
||||||
varLines := GenerateVariables(st, nil)
|
varLines := GenerateVariables(st, nil, nil)
|
||||||
|
|
||||||
output := strings.Join(append(append(constLines, absLines...), varLines...), "\n")
|
output := strings.Join(append(append(constLines, absLines...), varLines...), "\n")
|
||||||
|
|
||||||
|
|
@ -716,7 +716,7 @@ func TestUsageTracking(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that warning is generated
|
// Check that warning is generated
|
||||||
warnings := st.CheckUnused(nil)
|
warnings := st.CheckUnused(nil, nil)
|
||||||
if len(warnings) != 1 {
|
if len(warnings) != 1 {
|
||||||
t.Fatalf("CheckUnused() returned %d warnings, want 1", len(warnings))
|
t.Fatalf("CheckUnused() returned %d warnings, want 1", len(warnings))
|
||||||
}
|
}
|
||||||
|
|
@ -748,7 +748,7 @@ func TestUsageTracking(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that no warning is generated
|
// Check that no warning is generated
|
||||||
warnings := st.CheckUnused(nil)
|
warnings := st.CheckUnused(nil, nil)
|
||||||
if len(warnings) != 0 {
|
if len(warnings) != 0 {
|
||||||
t.Errorf("CheckUnused() returned %d warnings, want 0: %v", len(warnings), warnings)
|
t.Errorf("CheckUnused() returned %d warnings, want 0: %v", len(warnings), warnings)
|
||||||
}
|
}
|
||||||
|
|
@ -764,7 +764,7 @@ func TestUsageTracking(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that no warning is generated
|
// Check that no warning is generated
|
||||||
warnings := st.CheckUnused(nil)
|
warnings := st.CheckUnused(nil, nil)
|
||||||
if len(warnings) != 0 {
|
if len(warnings) != 0 {
|
||||||
t.Errorf("CheckUnused() returned %d warnings for constant, want 0: %v", len(warnings), warnings)
|
t.Errorf("CheckUnused() returned %d warnings for constant, want 0: %v", len(warnings), warnings)
|
||||||
}
|
}
|
||||||
|
|
@ -786,7 +786,7 @@ func TestUsageTracking(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that no warning is generated
|
// Check that no warning is generated
|
||||||
warnings := st.CheckUnused(nil)
|
warnings := st.CheckUnused(nil, nil)
|
||||||
if len(warnings) != 0 {
|
if len(warnings) != 0 {
|
||||||
t.Errorf("CheckUnused() returned %d warnings for absolute variable, want 0: %v", len(warnings), warnings)
|
t.Errorf("CheckUnused() returned %d warnings for absolute variable, want 0: %v", len(warnings), warnings)
|
||||||
}
|
}
|
||||||
|
|
@ -821,7 +821,7 @@ func TestUsageTracking(t *testing.T) {
|
||||||
st.Lookup("local_used", []string{"myFunc"})
|
st.Lookup("local_used", []string{"myFunc"})
|
||||||
|
|
||||||
// Check warnings
|
// Check warnings
|
||||||
warnings := st.CheckUnused(nil)
|
warnings := st.CheckUnused(nil, nil)
|
||||||
if len(warnings) != 2 {
|
if len(warnings) != 2 {
|
||||||
t.Fatalf("CheckUnused() returned %d warnings, want 2: %v", len(warnings), warnings)
|
t.Fatalf("CheckUnused() returned %d warnings, want 2: %v", len(warnings), warnings)
|
||||||
}
|
}
|
||||||
|
|
@ -873,7 +873,7 @@ func TestUsageTracking(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// No warnings should be generated
|
// No warnings should be generated
|
||||||
warnings := st.CheckUnused(nil)
|
warnings := st.CheckUnused(nil, nil)
|
||||||
if len(warnings) != 0 {
|
if len(warnings) != 0 {
|
||||||
t.Errorf("CheckUnused() returned %d warnings for used variable, want 0", len(warnings))
|
t.Errorf("CheckUnused() returned %d warnings for used variable, want 0", len(warnings))
|
||||||
}
|
}
|
||||||
|
|
@ -900,7 +900,7 @@ func TestUsageTracking(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warning should be generated
|
// Warning should be generated
|
||||||
warnings := st.CheckUnused(nil)
|
warnings := st.CheckUnused(nil, nil)
|
||||||
if len(warnings) != 1 {
|
if len(warnings) != 1 {
|
||||||
t.Errorf("CheckUnused() returned %d warnings, want 1", len(warnings))
|
t.Errorf("CheckUnused() returned %d warnings, want 1", len(warnings))
|
||||||
}
|
}
|
||||||
|
|
@ -922,7 +922,7 @@ func TestUsageTracking(t *testing.T) {
|
||||||
st.Lookup("used2", []string{"func1"})
|
st.Lookup("used2", []string{"func1"})
|
||||||
|
|
||||||
// Check warnings
|
// Check warnings
|
||||||
warnings := st.CheckUnused(nil)
|
warnings := st.CheckUnused(nil, nil)
|
||||||
if len(warnings) != 2 {
|
if len(warnings) != 2 {
|
||||||
t.Fatalf("CheckUnused() returned %d warnings, want 2: %v", len(warnings), warnings)
|
t.Fatalf("CheckUnused() returned %d warnings, want 2: %v", len(warnings), warnings)
|
||||||
}
|
}
|
||||||
|
|
@ -970,7 +970,7 @@ func TestUsageTracking(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since we haven't used it, it should generate a warning
|
// Since we haven't used it, it should generate a warning
|
||||||
warnings := st.CheckUnused(nil)
|
warnings := st.CheckUnused(nil, nil)
|
||||||
if len(warnings) != 1 {
|
if len(warnings) != 1 {
|
||||||
t.Errorf("CheckUnused() returned %d warnings for label reference, want 1", len(warnings))
|
t.Errorf("CheckUnused() returned %d warnings for label reference, want 1", len(warnings))
|
||||||
}
|
}
|
||||||
|
|
@ -979,7 +979,7 @@ func TestUsageTracking(t *testing.T) {
|
||||||
st.Lookup("handler", []string{})
|
st.Lookup("handler", []string{})
|
||||||
|
|
||||||
// Now no warning should be generated
|
// Now no warning should be generated
|
||||||
warnings = st.CheckUnused(nil)
|
warnings = st.CheckUnused(nil, nil)
|
||||||
if len(warnings) != 0 {
|
if len(warnings) != 0 {
|
||||||
t.Errorf("CheckUnused() returned %d warnings for used label reference, want 0", len(warnings))
|
t.Errorf("CheckUnused() returned %d warnings for used label reference, want 0", len(warnings))
|
||||||
}
|
}
|
||||||
|
|
@ -1022,7 +1022,7 @@ func TestUsageTracking(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warning should be generated for the variable
|
// Warning should be generated for the variable
|
||||||
warnings := st.CheckUnused(nil)
|
warnings := st.CheckUnused(nil, nil)
|
||||||
if len(warnings) != 1 {
|
if len(warnings) != 1 {
|
||||||
t.Errorf("CheckUnused() returned %d warnings, want 1", len(warnings))
|
t.Errorf("CheckUnused() returned %d warnings, want 1", len(warnings))
|
||||||
}
|
}
|
||||||
|
|
@ -1067,9 +1067,101 @@ func TestUsageTracking(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// No warnings should be generated
|
// No warnings should be generated
|
||||||
warnings := st.CheckUnused(nil)
|
warnings := st.CheckUnused(nil, nil)
|
||||||
if len(warnings) != 0 {
|
if len(warnings) != 0 {
|
||||||
t.Errorf("CheckUnused() returned %d warnings for constants/absolutes, want 0", len(warnings))
|
t.Errorf("CheckUnused() returned %d warnings for constants/absolutes, want 0", len(warnings))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddRegisterVar(t *testing.T) {
|
||||||
|
st := NewSymbolTable()
|
||||||
|
|
||||||
|
err := st.AddRegisterVar("temp", "myFunc", 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AddRegisterVar failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sym := st.Lookup("temp", []string{"myFunc"})
|
||||||
|
if sym == nil {
|
||||||
|
t.Fatal("expected symbol to be found")
|
||||||
|
}
|
||||||
|
if !sym.IsRegister() {
|
||||||
|
t.Error("expected IsRegister() to be true")
|
||||||
|
}
|
||||||
|
if !sym.IsByte() {
|
||||||
|
t.Error("expected IsByte() to be true")
|
||||||
|
}
|
||||||
|
if sym.FullName() != "myFunc_temp" {
|
||||||
|
t.Errorf("expected full name myFunc_temp, got %q", sym.FullName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddRegisterVar_GlobalScopeError(t *testing.T) {
|
||||||
|
st := NewSymbolTable()
|
||||||
|
|
||||||
|
err := st.AddRegisterVar("temp", "", 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for global scope REGISTER")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "REGISTER") {
|
||||||
|
t.Errorf("error should mention REGISTER, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateVariables_DissolvedRegister(t *testing.T) {
|
||||||
|
st := NewSymbolTable()
|
||||||
|
|
||||||
|
st.AddRegisterVar("temp", "myFunc", 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||||
|
st.AddVar("normal", "myFunc", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||||
|
|
||||||
|
dissolved := map[string]bool{"myFunc_temp": true}
|
||||||
|
lines := GenerateVariables(st, nil, dissolved)
|
||||||
|
|
||||||
|
joined := strings.Join(lines, "\n")
|
||||||
|
if strings.Contains(joined, "myFunc_temp") {
|
||||||
|
t.Error("dissolved REGISTER variable should not be emitted")
|
||||||
|
}
|
||||||
|
if !strings.Contains(joined, "myFunc_normal") {
|
||||||
|
t.Error("normal variable should still be emitted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateVariables_NonDissolvedRegister(t *testing.T) {
|
||||||
|
st := NewSymbolTable()
|
||||||
|
|
||||||
|
st.AddRegisterVar("temp", "myFunc", 42, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||||
|
|
||||||
|
lines := GenerateVariables(st, nil, nil)
|
||||||
|
|
||||||
|
joined := strings.Join(lines, "\n")
|
||||||
|
if !strings.Contains(joined, "myFunc_temp") {
|
||||||
|
t.Error("non-dissolved REGISTER variable should be emitted with !8")
|
||||||
|
}
|
||||||
|
if !strings.Contains(joined, "!8 $2a") {
|
||||||
|
t.Error("expected init value 42 ($2a) in output")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckUnused_DissolvedRegisterNoWarning(t *testing.T) {
|
||||||
|
st := NewSymbolTable()
|
||||||
|
|
||||||
|
st.AddRegisterVar("temp", "myFunc", 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||||
|
|
||||||
|
dissolved := map[string]bool{"myFunc_temp": true}
|
||||||
|
warnings := st.CheckUnused(nil, dissolved)
|
||||||
|
if len(warnings) != 0 {
|
||||||
|
t.Errorf("dissolved REGISTER var should not trigger warning, got: %v", warnings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckUnused_NonDissolvedRegisterWarning(t *testing.T) {
|
||||||
|
st := NewSymbolTable()
|
||||||
|
|
||||||
|
st.AddRegisterVar("temp", "myFunc", 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||||
|
|
||||||
|
warnings := st.CheckUnused(nil, nil)
|
||||||
|
if len(warnings) != 1 {
|
||||||
|
t.Errorf("non-dissolved unused REGISTER var should trigger 1 warning, got %d", len(warnings))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,32 +9,35 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
EnableLoad bool
|
EnableLoad bool
|
||||||
EnableImm bool
|
EnableImm bool
|
||||||
EnableJmp bool
|
EnableJmp bool
|
||||||
EnableSelf bool
|
EnableSelf bool
|
||||||
EnableStoreLoad bool
|
EnableStoreLoad bool
|
||||||
Debug bool
|
EnableRegisterVars bool
|
||||||
ShowMarkers bool
|
Debug bool
|
||||||
IOMap [65536]bool
|
ShowMarkers bool
|
||||||
|
IOMap [65536]bool
|
||||||
|
RegisterVars map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig(ps preproc.PragmaSet) *Config {
|
func NewConfig(ps preproc.PragmaSet) *Config {
|
||||||
all := ps.GetPragma("_P_OPT_ALL") != "" && ps.GetPragma("_P_OPT_ALL") != "0"
|
all := ps.GetPragma("_P_OPT_ALL") != "" && ps.GetPragma("_P_OPT_ALL") != "0"
|
||||||
|
|
||||||
return &Config{
|
return &Config{
|
||||||
EnableLoad: all || (ps.GetPragma("_P_OPT_LOAD") != "" && ps.GetPragma("_P_OPT_LOAD") != "0"),
|
EnableLoad: all || (ps.GetPragma("_P_OPT_LOAD") != "" && ps.GetPragma("_P_OPT_LOAD") != "0"),
|
||||||
EnableImm: all || (ps.GetPragma("_P_OPT_IMM") != "" && ps.GetPragma("_P_OPT_IMM") != "0"),
|
EnableImm: all || (ps.GetPragma("_P_OPT_IMM") != "" && ps.GetPragma("_P_OPT_IMM") != "0"),
|
||||||
EnableJmp: all || (ps.GetPragma("_P_OPT_JMP") != "" && ps.GetPragma("_P_OPT_JMP") != "0"),
|
EnableJmp: all || (ps.GetPragma("_P_OPT_JMP") != "" && ps.GetPragma("_P_OPT_JMP") != "0"),
|
||||||
EnableSelf: all || (ps.GetPragma("_P_OPT_SELF") != "" && ps.GetPragma("_P_OPT_SELF") != "0"),
|
EnableSelf: all || (ps.GetPragma("_P_OPT_SELF") != "" && ps.GetPragma("_P_OPT_SELF") != "0"),
|
||||||
EnableStoreLoad: all || (ps.GetPragma("_P_OPT_STLD") != "" && ps.GetPragma("_P_OPT_STLD") != "0"),
|
EnableStoreLoad: all || (ps.GetPragma("_P_OPT_STLD") != "" && ps.GetPragma("_P_OPT_STLD") != "0"),
|
||||||
Debug: (ps.GetPragma("_P_OPT_DEBUG") != "" && ps.GetPragma("_P_OPT_DEBUG") != "0"),
|
EnableRegisterVars: all || (ps.GetPragma("_P_OPT_REGISTER_VARS") != "" && ps.GetPragma("_P_OPT_REGISTER_VARS") != "0"),
|
||||||
ShowMarkers: (ps.GetPragma("_P_OPT_MARKERS") != "" && ps.GetPragma("_P_OPT_MARKERS") != "0"),
|
Debug: (ps.GetPragma("_P_OPT_DEBUG") != "" && ps.GetPragma("_P_OPT_DEBUG") != "0"),
|
||||||
|
ShowMarkers: (ps.GetPragma("_P_OPT_MARKERS") != "" && ps.GetPragma("_P_OPT_MARKERS") != "0"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Any() bool {
|
func (c *Config) Any() bool {
|
||||||
return c.EnableLoad || c.EnableImm || c.EnableJmp || c.EnableSelf || c.EnableStoreLoad
|
return c.EnableLoad || c.EnableImm || c.EnableJmp || c.EnableSelf || c.EnableStoreLoad || c.EnableRegisterVars
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildIOMap scans all pragma sets for _P_OPT_IO and marks I/O regions.
|
// BuildIOMap scans all pragma sets for _P_OPT_IO and marks I/O regions.
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
package optimizer
|
package optimizer
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
// Optimize applies all enabled peephole passes to the generated ASM lines.
|
// Optimize applies all enabled peephole passes to the generated ASM lines.
|
||||||
// Each pass runs sequentially on a parsed representation of the lines.
|
// Each pass runs sequentially on a parsed representation of the lines.
|
||||||
// @@OPT markers are stripped from output.
|
// @@OPT markers are stripped from output.
|
||||||
func Optimize(lines []string, cfg *Config) []string {
|
// Returns optimized lines and the set of dissolved REGISTER variable names
|
||||||
|
// (variables that had all their stores/loads eliminated).
|
||||||
|
func Optimize(lines []string, cfg *Config) ([]string, map[string]bool) {
|
||||||
|
dissolved := map[string]bool{}
|
||||||
|
|
||||||
if cfg == nil || !cfg.Any() {
|
if cfg == nil || !cfg.Any() {
|
||||||
return lines
|
return lines, dissolved
|
||||||
}
|
}
|
||||||
|
|
||||||
parsed := parseLines(lines)
|
parsed := parseLines(lines)
|
||||||
|
|
@ -26,11 +32,41 @@ func Optimize(lines []string, cfg *Config) []string {
|
||||||
if cfg.EnableSelf {
|
if cfg.EnableSelf {
|
||||||
parsed = passSelfAssignment(parsed)
|
parsed = passSelfAssignment(parsed)
|
||||||
}
|
}
|
||||||
|
if cfg.EnableRegisterVars && len(cfg.RegisterVars) > 0 {
|
||||||
|
parsed = passRegDead(parsed, cfg.RegisterVars)
|
||||||
|
}
|
||||||
|
|
||||||
parsed = stripOptMarkers(parsed)
|
parsed = stripOptMarkers(parsed)
|
||||||
if cfg.Debug {
|
if cfg.Debug {
|
||||||
original = stripOptMarkers(original)
|
original = stripOptMarkers(original)
|
||||||
parsed = debugDiff(original, parsed)
|
parsed = debugDiff(original, parsed)
|
||||||
}
|
}
|
||||||
return linesToString(parsed)
|
|
||||||
|
resultLines := linesToString(parsed)
|
||||||
|
|
||||||
|
if len(cfg.RegisterVars) > 0 {
|
||||||
|
dissolved = computeDissolved(resultLines, cfg.RegisterVars)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultLines, dissolved
|
||||||
|
}
|
||||||
|
|
||||||
|
// computeDissolved finds REGISTER variables that have no remaining references
|
||||||
|
// in the output and can have their memory allocation elided.
|
||||||
|
func computeDissolved(outputLines []string, registerVars map[string]bool) map[string]bool {
|
||||||
|
dissolved := map[string]bool{}
|
||||||
|
for name := range registerVars {
|
||||||
|
dissolved[name] = true
|
||||||
|
}
|
||||||
|
for _, line := range outputLines {
|
||||||
|
for name := range dissolved {
|
||||||
|
if strings.Contains(line, name) {
|
||||||
|
delete(dissolved, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(dissolved) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dissolved
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package optimizer
|
package optimizer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -232,7 +233,7 @@ func TestOptimizeIntegration(t *testing.T) {
|
||||||
)
|
)
|
||||||
|
|
||||||
cfg := &Config{EnableLoad: true}
|
cfg := &Config{EnableLoad: true}
|
||||||
output := Optimize(input, cfg)
|
output, _ := Optimize(input, cfg)
|
||||||
|
|
||||||
// 2 source comments + 3 asm lines (lda b removed) = 5
|
// 2 source comments + 3 asm lines (lda b removed) = 5
|
||||||
if len(output) != 5 {
|
if len(output) != 5 {
|
||||||
|
|
@ -249,7 +250,7 @@ func TestOptimizeWithDebug(t *testing.T) {
|
||||||
)
|
)
|
||||||
|
|
||||||
cfg := &Config{EnableLoad: true, Debug: true}
|
cfg := &Config{EnableLoad: true, Debug: true}
|
||||||
output := Optimize(input, cfg)
|
output, _ := Optimize(input, cfg)
|
||||||
|
|
||||||
// Header + 2 kept lines + 1 removed annotation = 4
|
// Header + 2 kept lines + 1 removed annotation = 4
|
||||||
if len(output) != 4 {
|
if len(output) != 4 {
|
||||||
|
|
@ -516,3 +517,218 @@ func TestPassStoreReloadIO(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPassRegDead_DeadStore(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"\tlda #$05",
|
||||||
|
"\tsta myFunc_temp",
|
||||||
|
"\tsta $d020",
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
EnableRegisterVars: true,
|
||||||
|
RegisterVars: map[string]bool{
|
||||||
|
"myFunc_temp": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
output, dissolved := Optimize(input, cfg)
|
||||||
|
|
||||||
|
if !dissolved["myFunc_temp"] {
|
||||||
|
t.Error("expected myFunc_temp to be dissolved")
|
||||||
|
}
|
||||||
|
|
||||||
|
joined := strings.Join(output, "\n")
|
||||||
|
if strings.Contains(joined, "myFunc_temp") {
|
||||||
|
t.Errorf("expected sta myFunc_temp to be removed, got:\n%s", joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassRegDead_KeptStoreWithLoad(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"\tlda #$05",
|
||||||
|
"\tsta myFunc_temp",
|
||||||
|
"\tlda myFunc_temp",
|
||||||
|
"\tsta $d020",
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
EnableRegisterVars: true,
|
||||||
|
RegisterVars: map[string]bool{
|
||||||
|
"myFunc_temp": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
output, dissolved := Optimize(input, cfg)
|
||||||
|
|
||||||
|
if dissolved["myFunc_temp"] {
|
||||||
|
t.Error("expected myFunc_temp NOT to be dissolved")
|
||||||
|
}
|
||||||
|
|
||||||
|
joined := strings.Join(output, "\n")
|
||||||
|
if !strings.Contains(joined, "myFunc_temp") {
|
||||||
|
t.Errorf("expected sta myFunc_temp to be kept, got:\n%s", joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassRegDead_KeptStoreAtLabel(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"\tlda #$05",
|
||||||
|
"\tsta myFunc_temp",
|
||||||
|
"myskip:",
|
||||||
|
"\tnop",
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
EnableRegisterVars: true,
|
||||||
|
RegisterVars: map[string]bool{
|
||||||
|
"myFunc_temp": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
output, _ := Optimize(input, cfg)
|
||||||
|
|
||||||
|
joined := strings.Join(output, "\n")
|
||||||
|
if !strings.Contains(joined, "sta myFunc_temp") {
|
||||||
|
t.Errorf("expected sta myFunc_temp to be kept (label), got:\n%s", joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassRegDead_NonRegisterVarUnchanged(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"\tlda #$05",
|
||||||
|
"\tsta normalVar",
|
||||||
|
"\tsta $d020",
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
EnableRegisterVars: true,
|
||||||
|
RegisterVars: map[string]bool{},
|
||||||
|
}
|
||||||
|
|
||||||
|
output, _ := Optimize(input, cfg)
|
||||||
|
|
||||||
|
joined := strings.Join(output, "\n")
|
||||||
|
if !strings.Contains(joined, "sta normalVar") {
|
||||||
|
t.Errorf("expected sta normalVar to be kept (not a register var), got:\n%s", joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassRegDead_DeadStoreBeforeRts(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"\tlda #$05",
|
||||||
|
"\tsta myFunc_temp",
|
||||||
|
"\tsta $d020",
|
||||||
|
"\trts",
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
EnableRegisterVars: true,
|
||||||
|
RegisterVars: map[string]bool{
|
||||||
|
"myFunc_temp": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
output, dissolved := Optimize(input, cfg)
|
||||||
|
|
||||||
|
if !dissolved["myFunc_temp"] {
|
||||||
|
t.Error("expected myFunc_temp to be dissolved")
|
||||||
|
}
|
||||||
|
|
||||||
|
joined := strings.Join(output, "\n")
|
||||||
|
if strings.Contains(joined, "sta myFunc_temp") {
|
||||||
|
t.Errorf("expected sta myFunc_temp to be removed (dead before rts), got:\n%s", joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassRegDead_StoreNeededAfterAClobber(t *testing.T) {
|
||||||
|
// sta regVar; lda other → A clobbered, but regVar is reloaded later → keep store
|
||||||
|
input := []string{
|
||||||
|
"\tlda 53281",
|
||||||
|
"\tsta spill_me_bg",
|
||||||
|
"\tlda 53282",
|
||||||
|
"\tsta spill_me_mc1",
|
||||||
|
"\tlda spill_me_bg",
|
||||||
|
"\tsta 53282",
|
||||||
|
"\trts",
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
EnableRegisterVars: true,
|
||||||
|
RegisterVars: map[string]bool{
|
||||||
|
"spill_me_bg": true,
|
||||||
|
"spill_me_mc1": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
output, dissolved := Optimize(input, cfg)
|
||||||
|
|
||||||
|
if dissolved["spill_me_bg"] {
|
||||||
|
t.Error("spill_me_bg should NOT be dissolved — its value is reloaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
joined := strings.Join(output, "\n")
|
||||||
|
if !strings.Contains(joined, "sta spill_me_bg") {
|
||||||
|
t.Errorf("expected sta spill_me_bg to be kept (reloaded after A clobber), got:\n%s", joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassRegDead_DeadStoreNeverReloaded(t *testing.T) {
|
||||||
|
// sta → and (A clobbered) → POKE (uses A directly) → rts. Store dead.
|
||||||
|
input := []string{
|
||||||
|
"\tlda 56576",
|
||||||
|
"\tsta dissolve_me_temp",
|
||||||
|
"\tand #$fc",
|
||||||
|
"\tsta dissolve_me_temp",
|
||||||
|
"\tsta 56576",
|
||||||
|
"\trts",
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
EnableRegisterVars: true,
|
||||||
|
RegisterVars: map[string]bool{
|
||||||
|
"dissolve_me_temp": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
output, dissolved := Optimize(input, cfg)
|
||||||
|
|
||||||
|
if !dissolved["dissolve_me_temp"] {
|
||||||
|
t.Error("dissolve_me_temp should be dissolved — never reloaded after either store")
|
||||||
|
}
|
||||||
|
|
||||||
|
joined := strings.Join(output, "\n")
|
||||||
|
if strings.Contains(joined, "sta dissolve_me_temp") {
|
||||||
|
t.Errorf("expected all sta dissolve_me_temp to be removed, got:\n%s", joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPassRegDead_CallSiteKeepsStore(t *testing.T) {
|
||||||
|
// sta regVar; jsr foo; ... no lda regVar ...; rts → store kept (conservative)
|
||||||
|
// The callee may modify A, so the store survives even though nothing
|
||||||
|
// reloads regVar after the call.
|
||||||
|
input := []string{
|
||||||
|
"\tlda 53281",
|
||||||
|
"\tsta call_val",
|
||||||
|
"\tjsr helper",
|
||||||
|
"\trts",
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
EnableRegisterVars: true,
|
||||||
|
RegisterVars: map[string]bool{
|
||||||
|
"call_val": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
output, dissolved := Optimize(input, cfg)
|
||||||
|
|
||||||
|
if dissolved["call_val"] {
|
||||||
|
t.Error("call_val should NOT be dissolved — jsr is conservative")
|
||||||
|
}
|
||||||
|
|
||||||
|
joined := strings.Join(output, "\n")
|
||||||
|
if !strings.Contains(joined, "sta call_val") {
|
||||||
|
t.Errorf("expected sta call_val to be kept (before jsr), got:\n%s", joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
71
internal/optimizer/pass_regdead.go
Normal file
71
internal/optimizer/pass_regdead.go
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
package optimizer
|
||||||
|
|
||||||
|
// passRegDead eliminates dead stores to REGISTER variables.
|
||||||
|
// A store is dead if no matching load (lda regVar) follows before A is
|
||||||
|
// clobbered or a label is reached. This is safe because REGISTER variables
|
||||||
|
// have a contract: nothing external can observe their memory location.
|
||||||
|
func passRegDead(lines []asmLine, registerVars map[string]bool) []asmLine {
|
||||||
|
var result []asmLine
|
||||||
|
|
||||||
|
for i := 0; i < len(lines); i++ {
|
||||||
|
line := lines[i]
|
||||||
|
|
||||||
|
if line.isCode && line.opcode == "sta" && line.operand != "" && registerVars[line.operand] {
|
||||||
|
if isRegStoreDead(lines, i+1, line.operand) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// isRegStoreDead scans forward from start to determine whether a store to a
|
||||||
|
// REGISTER variable operand is dead. Returns true if the store can be removed.
|
||||||
|
// A store is dead only if no matching lda is found anywhere forward before
|
||||||
|
// a label or control-flow-ending instruction.
|
||||||
|
func isRegStoreDead(lines []asmLine, start int, operand string) bool {
|
||||||
|
for i := start; i < len(lines); i++ {
|
||||||
|
l := lines[i]
|
||||||
|
|
||||||
|
if l.optMarker || l.isComment {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.isLabel {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.isCode && isCallSite(l) {
|
||||||
|
return false // callee may modify A; conservatively keep the store
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.isCode && blocksFlow(l) {
|
||||||
|
return true // rts/jmp/brk/rti — execution ends here
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.isCode && l.opcode == "lda" && l.operand == operand {
|
||||||
|
return false // the value IS read from memory later
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true // end of block reached with no matching load
|
||||||
|
}
|
||||||
|
|
||||||
|
// blocksFlow returns true for instructions that unconditionally end the current
|
||||||
|
// execution path: rts, jmp, brk, rti
|
||||||
|
func blocksFlow(line asmLine) bool {
|
||||||
|
switch line.opcode {
|
||||||
|
case "rts", "jmp", "brk", "rti":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isCallSite returns true for instructions that transfer control to a callee
|
||||||
|
// that may modify A, X, Y. The store before a call is conservatively kept.
|
||||||
|
func isCallSite(line asmLine) bool {
|
||||||
|
return line.opcode == "jsr"
|
||||||
|
}
|
||||||
19
main.go
19
main.go
|
|
@ -71,6 +71,10 @@ func main() {
|
||||||
// Default mode: treat as build command with implicit arguments
|
// Default mode: treat as build command with implicit arguments
|
||||||
// Parse arguments flexibly
|
// Parse arguments flexibly
|
||||||
var inputFile, outputFile string
|
var inputFile, outputFile string
|
||||||
|
opt := false
|
||||||
|
optDebug := false
|
||||||
|
optC64 := false
|
||||||
|
var optExcludes []string
|
||||||
args := os.Args[1:]
|
args := os.Args[1:]
|
||||||
|
|
||||||
for i := 0; i < len(args); i++ {
|
for i := 0; i < len(args); i++ {
|
||||||
|
|
@ -94,6 +98,17 @@ func main() {
|
||||||
printUsage()
|
printUsage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
} else if arg == "--opt" || arg == "-O" {
|
||||||
|
opt = true
|
||||||
|
} else if arg == "--opt-debug" {
|
||||||
|
optDebug = true
|
||||||
|
} else if arg == "--opt-exclude-c64-io" {
|
||||||
|
optC64 = true
|
||||||
|
} else if arg == "--opt-exclude" {
|
||||||
|
if i+1 < len(args) {
|
||||||
|
optExcludes = append(optExcludes, args[i+1])
|
||||||
|
i++ // Skip next arg
|
||||||
|
}
|
||||||
} else if !strings.HasPrefix(arg, "-") && inputFile == "" {
|
} else if !strings.HasPrefix(arg, "-") && inputFile == "" {
|
||||||
// First non-flag argument is the input file
|
// First non-flag argument is the input file
|
||||||
inputFile = arg
|
inputFile = arg
|
||||||
|
|
@ -115,13 +130,13 @@ func main() {
|
||||||
// Determine mode by output extension
|
// Determine mode by output extension
|
||||||
if strings.HasSuffix(strings.ToLower(outputFile), ".prg") {
|
if strings.HasSuffix(strings.ToLower(outputFile), ".prg") {
|
||||||
// Build mode (compile + assemble)
|
// Build mode (compile + assemble)
|
||||||
if err := build(inputFile, outputFile, false, false, false, false, false, nil); err != nil {
|
if err := build(inputFile, outputFile, false, false, opt, optDebug, optC64, optExcludes); err != nil {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
}
|
}
|
||||||
fmt.Println("Build successful.")
|
fmt.Println("Build successful.")
|
||||||
} else {
|
} else {
|
||||||
// Compile mode (assembly only)
|
// Compile mode (assembly only)
|
||||||
if err := compileOnly(inputFile, outputFile, false, false, false, nil); err != nil {
|
if err := compileOnly(inputFile, outputFile, opt, optDebug, optC64, optExcludes); err != nil {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
}
|
}
|
||||||
fmt.Println("Compilation successful.")
|
fmt.Println("Compilation successful.")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue