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 {
|
||||
text := line.Text
|
||||
|
||||
// Check for |@macro()| pattern first
|
||||
if macroStart := strings.Index(text, "|@"); macroStart != -1 {
|
||||
macroEnd := strings.Index(text[macroStart+1:], "|")
|
||||
// Find comment boundary - only process |...| patterns in the code portion
|
||||
commentPos := findAsmCommentStart(text)
|
||||
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 {
|
||||
macroEnd += macroStart + 1
|
||||
invocation := text[macroStart+1 : macroEnd] // @name(args)
|
||||
invocation := codePart[macroStart+1 : macroEnd] // @name(args)
|
||||
|
||||
macroName, args, err := ParseMacroInvocation(invocation)
|
||||
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 {
|
||||
start := strings.IndexByte(text, '|')
|
||||
start := findPipeOutsideStrings(codePart, searchFrom)
|
||||
if start == -1 {
|
||||
break
|
||||
}
|
||||
end := strings.IndexByte(text[start+1:], '|')
|
||||
end := findPipeOutsideStrings(codePart, start+1)
|
||||
if end == -1 {
|
||||
c.printErrorWithContext(lines, i, fmt.Errorf("unclosed | in assembler line"))
|
||||
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())
|
||||
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 {
|
||||
// Collect script lines for execution
|
||||
scriptBuffer = append(scriptBuffer, line.Text)
|
||||
|
|
@ -329,6 +340,103 @@ func parseMacroHeader(header string) (string, []string, error) {
|
|||
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
|
||||
func (c *Compiler) assembleOutput(codeLines []string) []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