Local variable expansion and macro expansion in ASM block now takes into account comments and ommits those.
This commit is contained in:
parent
8e66e95c90
commit
4acd8f2e87
2 changed files with 400 additions and 12 deletions
|
|
@ -109,12 +109,21 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
if line.Kind == preproc.Assembler {
|
if line.Kind == preproc.Assembler {
|
||||||
text := line.Text
|
text := line.Text
|
||||||
|
|
||||||
// Check for |@macro()| pattern first
|
// Find comment boundary - only process |...| patterns in the code portion
|
||||||
if macroStart := strings.Index(text, "|@"); macroStart != -1 {
|
commentPos := findAsmCommentStart(text)
|
||||||
macroEnd := strings.Index(text[macroStart+1:], "|")
|
codePart := text
|
||||||
|
commentPart := ""
|
||||||
|
if commentPos != -1 {
|
||||||
|
codePart = text[:commentPos]
|
||||||
|
commentPart = text[commentPos:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for |@macro()| pattern first (only in code portion, outside strings)
|
||||||
|
macroStart := findPipeOutsideStrings(codePart, 0)
|
||||||
|
if macroStart != -1 && macroStart+1 < len(codePart) && codePart[macroStart+1] == '@' {
|
||||||
|
macroEnd := findPipeOutsideStrings(codePart, macroStart+1)
|
||||||
if macroEnd != -1 {
|
if macroEnd != -1 {
|
||||||
macroEnd += macroStart + 1
|
invocation := codePart[macroStart+1 : macroEnd] // @name(args)
|
||||||
invocation := text[macroStart+1 : macroEnd] // @name(args)
|
|
||||||
|
|
||||||
macroName, args, err := ParseMacroInvocation(invocation)
|
macroName, args, err := ParseMacroInvocation(invocation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -136,24 +145,26 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expand |varname| -> scoped_varname for local variables in ASM blocks
|
// Expand |varname| -> scoped_varname for local variables in ASM blocks (only in code portion, outside strings)
|
||||||
|
searchFrom := 0
|
||||||
for {
|
for {
|
||||||
start := strings.IndexByte(text, '|')
|
start := findPipeOutsideStrings(codePart, searchFrom)
|
||||||
if start == -1 {
|
if start == -1 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
end := strings.IndexByte(text[start+1:], '|')
|
end := findPipeOutsideStrings(codePart, start+1)
|
||||||
if end == -1 {
|
if end == -1 {
|
||||||
c.printErrorWithContext(lines, i, fmt.Errorf("unclosed | in assembler line"))
|
c.printErrorWithContext(lines, i, fmt.Errorf("unclosed | in assembler line"))
|
||||||
return nil, fmt.Errorf("compilation failed")
|
return nil, fmt.Errorf("compilation failed")
|
||||||
}
|
}
|
||||||
end += start + 1
|
|
||||||
|
|
||||||
varName := text[start+1 : end]
|
varName := codePart[start+1 : end]
|
||||||
expandedName := c.ctx.SymbolTable.ExpandName(varName, c.ctx.CurrentScope())
|
expandedName := c.ctx.SymbolTable.ExpandName(varName, c.ctx.CurrentScope())
|
||||||
text = text[:start] + expandedName + text[end+1:]
|
codePart = codePart[:start] + expandedName + codePart[end+1:]
|
||||||
|
// Continue searching after the replacement
|
||||||
|
searchFrom = start + len(expandedName)
|
||||||
}
|
}
|
||||||
codeOutput = append(codeOutput, text)
|
codeOutput = append(codeOutput, codePart+commentPart)
|
||||||
} else if line.Kind == preproc.Script || line.Kind == preproc.ScriptLibrary {
|
} else if line.Kind == preproc.Script || line.Kind == preproc.ScriptLibrary {
|
||||||
// Collect script lines for execution
|
// Collect script lines for execution
|
||||||
scriptBuffer = append(scriptBuffer, line.Text)
|
scriptBuffer = append(scriptBuffer, line.Text)
|
||||||
|
|
@ -329,6 +340,103 @@ func parseMacroHeader(header string) (string, []string, error) {
|
||||||
return name, params, nil
|
return name, params, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findAsmCommentStart finds the position of ';' that starts a comment in an ASM line,
|
||||||
|
// taking into account that ';' inside strings (single or double quoted) should be ignored.
|
||||||
|
// Supports ACME-style escape sequences (backslash escapes) inside strings.
|
||||||
|
// Returns -1 if no comment is found.
|
||||||
|
func findAsmCommentStart(line string) int {
|
||||||
|
inString := false
|
||||||
|
escaped := false
|
||||||
|
var quoteChar rune
|
||||||
|
|
||||||
|
for i, ch := range line {
|
||||||
|
if escaped {
|
||||||
|
escaped = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inString {
|
||||||
|
if ch == '\\' {
|
||||||
|
escaped = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == quoteChar {
|
||||||
|
inString = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Not in string
|
||||||
|
if ch == '"' || ch == '\'' {
|
||||||
|
inString = true
|
||||||
|
quoteChar = ch
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == ';' {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// findPipeOutsideStrings finds the position of '|' that is not inside a string.
|
||||||
|
// Supports ACME-style escape sequences (backslash escapes) inside strings.
|
||||||
|
// Starts searching from 'startFrom' position.
|
||||||
|
// Returns -1 if no such '|' is found.
|
||||||
|
func findPipeOutsideStrings(line string, startFrom int) int {
|
||||||
|
inString := false
|
||||||
|
escaped := false
|
||||||
|
var quoteChar rune
|
||||||
|
|
||||||
|
for i, ch := range line {
|
||||||
|
if i < startFrom {
|
||||||
|
// Track string state even before startFrom
|
||||||
|
if escaped {
|
||||||
|
escaped = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inString {
|
||||||
|
if ch == '\\' {
|
||||||
|
escaped = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == quoteChar {
|
||||||
|
inString = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '"' || ch == '\'' {
|
||||||
|
inString = true
|
||||||
|
quoteChar = ch
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if escaped {
|
||||||
|
escaped = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inString {
|
||||||
|
if ch == '\\' {
|
||||||
|
escaped = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == quoteChar {
|
||||||
|
inString = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Not in string
|
||||||
|
if ch == '"' || ch == '\'' {
|
||||||
|
inString = true
|
||||||
|
quoteChar = ch
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '|' {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
// assembleOutput combines all generated sections into final assembly
|
// assembleOutput combines all generated sections into final assembly
|
||||||
func (c *Compiler) assembleOutput(codeLines []string) []string {
|
func (c *Compiler) assembleOutput(codeLines []string) []string {
|
||||||
var output []string
|
var output []string
|
||||||
|
|
|
||||||
|
|
@ -459,3 +459,283 @@ func TestParseMacroInvocation(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFindAsmCommentStart(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
// Basic cases
|
||||||
|
{"lda #$00", -1}, // no comment
|
||||||
|
{"; comment", 0}, // comment at start
|
||||||
|
{"lda #$00 ; comment", 9}, // comment after code
|
||||||
|
{" lda #$00 ; comment", 11}, // with leading whitespace
|
||||||
|
|
||||||
|
// Semicolon in double-quoted string
|
||||||
|
{`!text "hello; world"`, -1}, // no comment, ; inside string
|
||||||
|
{`!text "hello; world" ; comment`, 21}, // comment after string with ;
|
||||||
|
{`!text "a;b;c"`, -1}, // multiple ; in string
|
||||||
|
|
||||||
|
// Semicolon in single-quoted string
|
||||||
|
{`!byte ';'`, -1}, // ; as character literal
|
||||||
|
{`!byte ';' ; comment`, 10}, // comment after ; char literal
|
||||||
|
|
||||||
|
// Escape sequences in strings
|
||||||
|
{`!text "hello\"world"`, -1}, // escaped quote, no comment
|
||||||
|
{`!text "hello\"world" ; comment`, 21}, // comment after string with escaped quote
|
||||||
|
{`!text "path\\file"`, -1}, // escaped backslash
|
||||||
|
{`!text "a\\;b"`, -1}, // escaped backslash before ;
|
||||||
|
{`!text "a\;b"`, -1}, // escaped ; in string (stays in string)
|
||||||
|
|
||||||
|
// Mixed quotes
|
||||||
|
{`!text "it's"`, -1}, // single quote inside double
|
||||||
|
{`!byte '"'`, -1}, // double quote as char literal
|
||||||
|
{`!text "say \"hi\""`, -1}, // escaped quotes in string
|
||||||
|
|
||||||
|
// Edge cases
|
||||||
|
{"", -1}, // empty line
|
||||||
|
{`""`, -1}, // empty string
|
||||||
|
{`"" ; comment`, 3}, // empty string then comment
|
||||||
|
{`!text "unterminated`, -1}, // unterminated string (no ; found)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := findAsmCommentStart(tt.input)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("findAsmCommentStart(%q) = %d, want %d", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAsmBlock_VariableExpansion_IgnoresComments(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
comp := NewCompiler(pragma)
|
||||||
|
|
||||||
|
// Add a variable to the symbol table so expansion can work
|
||||||
|
comp.Context().SymbolTable.AddVar("myvar", "", KindByte, 0)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "variable in code expands",
|
||||||
|
input: " lda |myvar|",
|
||||||
|
expected: " lda myvar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "variable in comment stays unexpanded",
|
||||||
|
input: "; use |myvar| here",
|
||||||
|
expected: "; use |myvar| here",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "variable in code, different in comment",
|
||||||
|
input: " lda |myvar| ; load |myvar|",
|
||||||
|
expected: " lda myvar ; load |myvar|",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only comment with variable",
|
||||||
|
input: " nop ; |myvar|",
|
||||||
|
expected: " nop ; |myvar|",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "variable in string not affected",
|
||||||
|
input: ` !text "|myvar|"`,
|
||||||
|
expected: ` !text "|myvar|"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "variable after string with semicolon",
|
||||||
|
input: ` !text "a;b" ; |myvar|`,
|
||||||
|
expected: ` !text "a;b" ; |myvar|`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create fresh compiler for each test
|
||||||
|
comp := NewCompiler(pragma)
|
||||||
|
comp.Context().SymbolTable.AddVar("myvar", "", KindByte, 0)
|
||||||
|
|
||||||
|
lines := []preproc.Line{
|
||||||
|
{
|
||||||
|
Text: tt.input,
|
||||||
|
Filename: "test.asm",
|
||||||
|
LineNo: 1,
|
||||||
|
Kind: preproc.Assembler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Empty source line to close the ASM block
|
||||||
|
Text: "",
|
||||||
|
Filename: "test.asm",
|
||||||
|
LineNo: 2,
|
||||||
|
Kind: preproc.Source,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := comp.Compile(lines)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Compile failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the ASM output line (between ; ASM and ; ENDASM markers)
|
||||||
|
var resultLine string
|
||||||
|
inAsmBlock := false
|
||||||
|
for _, line := range output {
|
||||||
|
if line == "; ASM" {
|
||||||
|
inAsmBlock = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if line == "; ENDASM" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if inAsmBlock {
|
||||||
|
resultLine = line
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if resultLine != tt.expected {
|
||||||
|
t.Errorf("got %q, want %q\nfull output: %v", resultLine, tt.expected, output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindPipeOutsideStrings(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
startFrom int
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
// Basic cases
|
||||||
|
{"lda |var|", 0, 4},
|
||||||
|
{"lda |var|", 5, 8},
|
||||||
|
{"no pipes", 0, -1},
|
||||||
|
|
||||||
|
// Pipe in string should be skipped
|
||||||
|
{`"a|b"`, 0, -1},
|
||||||
|
{`"a|b" |var|`, 0, 6},
|
||||||
|
{`'|' |x|`, 0, 4},
|
||||||
|
|
||||||
|
// Escape sequences
|
||||||
|
{`"a\"|b"`, 0, -1}, // escaped quote, pipe still in string
|
||||||
|
{`"a\\"|b|`, 0, 5}, // escaped backslash, pipe outside
|
||||||
|
{`"a\|b"`, 0, -1}, // escaped pipe stays in string
|
||||||
|
|
||||||
|
// Start from different positions
|
||||||
|
{"|a| |b|", 0, 0},
|
||||||
|
{"|a| |b|", 1, 2},
|
||||||
|
{"|a| |b|", 3, 4},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := findPipeOutsideStrings(tt.input, tt.startFrom)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("findPipeOutsideStrings(%q, %d) = %d, want %d", tt.input, tt.startFrom, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAsmBlock_MacroExpansion_IgnoresComments(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
comp := NewCompiler(pragma)
|
||||||
|
|
||||||
|
// Register a test macro
|
||||||
|
comp.Context().ScriptMacros["delay"] = &ScriptMacro{
|
||||||
|
Name: "delay",
|
||||||
|
Params: []string{"cycles"},
|
||||||
|
Body: []string{
|
||||||
|
"for i in range(cycles):",
|
||||||
|
" print(' nop')",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expectExpanded bool
|
||||||
|
expectedLines int // number of output lines (excluding comment wrappers)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "macro in code expands",
|
||||||
|
input: " |@delay(3)|",
|
||||||
|
expectExpanded: true,
|
||||||
|
expectedLines: 3, // 3 nops
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "macro in comment does not expand",
|
||||||
|
input: "; |@delay(3)|",
|
||||||
|
expectExpanded: false,
|
||||||
|
expectedLines: 1, // just the original line
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "macro after semicolon comment does not expand",
|
||||||
|
input: " nop ; |@delay(3)|",
|
||||||
|
expectExpanded: false,
|
||||||
|
expectedLines: 1, // just the original line with nop
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "macro in string does not expand",
|
||||||
|
input: ` !text "|@delay(3)|"`,
|
||||||
|
expectExpanded: false,
|
||||||
|
expectedLines: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create fresh compiler for each test to avoid state issues
|
||||||
|
comp := NewCompiler(pragma)
|
||||||
|
comp.Context().ScriptMacros["delay"] = &ScriptMacro{
|
||||||
|
Name: "delay",
|
||||||
|
Params: []string{"cycles"},
|
||||||
|
Body: []string{
|
||||||
|
"for i in range(cycles):",
|
||||||
|
" print(' nop')",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := []preproc.Line{
|
||||||
|
{
|
||||||
|
Text: tt.input,
|
||||||
|
Filename: "test.asm",
|
||||||
|
LineNo: 1,
|
||||||
|
Kind: preproc.Assembler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Empty source line to close the ASM block
|
||||||
|
Text: "",
|
||||||
|
Filename: "test.asm",
|
||||||
|
LineNo: 2,
|
||||||
|
Kind: preproc.Source,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := comp.Compile(lines)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Compile failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count nop lines to determine if macro expanded
|
||||||
|
nopCount := 0
|
||||||
|
for _, line := range output {
|
||||||
|
if strings.TrimSpace(line) == "nop" {
|
||||||
|
nopCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.expectExpanded {
|
||||||
|
if nopCount != tt.expectedLines {
|
||||||
|
t.Errorf("expected %d nop lines (macro expanded), got %d\nfull output: %v",
|
||||||
|
tt.expectedLines, nopCount, output)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if nopCount > 0 {
|
||||||
|
t.Errorf("expected no nop lines (macro should not expand in comment), got %d\nfull output: %v",
|
||||||
|
nopCount, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue