diff --git a/src/Chip8.Core/Assembly/MacroExpander.cs b/src/Chip8.Core/Assembly/MacroExpander.cs new file mode 100644 index 0000000..1d936b8 --- /dev/null +++ b/src/Chip8.Core/Assembly/MacroExpander.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; + +namespace Chip8.Core.Assembly; + +public sealed class MacroExpander +{ + const int expansionDepthLimit = 256; + + sealed class MacroDefinition + { + public string name = ""; + public List parameters = new(); + public List> body = new(); + public SourcePosition position; + } + + readonly Dictionary macros = new(StringComparer.OrdinalIgnoreCase); + + public List expand(List tokens) + { + List> lines = splitLines(tokens); + List> withoutDefinitions = collectDefinitions(lines); + var output = new List>(); + emitLines(withoutDefinitions, output, 0); + return joinLines(output); + } + + List> collectDefinitions(List> lines) + { + var remaining = new List>(); + int index = 0; + + while (index < lines.Count) + { + List line = lines[index]; + if (line.Count > 0 && line[0].isIdentifier("MACRO")) + { + index = readDefinition(lines, index); + } + else + { + remaining.Add(line); + index++; + } + } + + return remaining; + } + + int readDefinition(List> lines, int index) + { + List header = lines[index]; + if (header.Count < 2) + { + throw new AsmException("MACRO needs a name", header[0].position); + } + + var definition = new MacroDefinition { name = header[1].text, position = header[0].position }; + for (int p = 2; p < header.Count; p++) + { + if (header[p].kind == TokenKind.Comma) + { + continue; + } + if (header[p].kind != TokenKind.Identifier) + { + throw new AsmException("macro parameter must be a name", header[p].position); + } + definition.parameters.Add(header[p].text); + } + + int cursor = index + 1; + while (cursor < lines.Count) + { + List line = lines[cursor]; + if (line.Count > 0 && (line[0].isIdentifier("ENDM") || line[0].isIdentifier("ENDMACRO"))) + { + macros[definition.name] = definition; + return cursor + 1; + } + definition.body.Add(line); + cursor++; + } + + throw new AsmException("macro '" + definition.name + "' is missing ENDM", definition.position); + } + + void emitLines(List> lines, List> output, int depth) + { + if (depth > expansionDepthLimit) + { + throw new AsmException("macro expansion too deep (recursive macro?)", lines.Count > 0 && lines[0].Count > 0 ? lines[0][0].position : new SourcePosition(0, 0)); + } + + foreach (List original in lines) + { + List line = original; + + if (line.Count >= 2 && line[0].kind == TokenKind.Identifier && line[1].kind == TokenKind.Colon) + { + output.Add(new List { line[0], line[1] }); + line = line.GetRange(2, line.Count - 2); + } + + if (line.Count > 0 && line[0].kind == TokenKind.Identifier && macros.TryGetValue(line[0].text, out MacroDefinition? definition)) + { + List> expanded = instantiate(definition, line); + emitLines(expanded, output, depth + 1); + } + else if (line.Count > 0) + { + output.Add(line); + } + } + } + + static List> instantiate(MacroDefinition definition, List call) + { + List> arguments = splitArguments(call, 1); + if (arguments.Count != definition.parameters.Count) + { + throw new AsmException( + "macro '" + definition.name + "' expects " + definition.parameters.Count + " argument(s), got " + arguments.Count, + call[0].position); + } + + var binding = new Dictionary>(StringComparer.OrdinalIgnoreCase); + for (int p = 0; p < definition.parameters.Count; p++) + { + binding[definition.parameters[p]] = arguments[p]; + } + + var result = new List>(); + foreach (List bodyLine in definition.body) + { + var substituted = new List(); + foreach (Token token in bodyLine) + { + if (token.kind == TokenKind.Identifier && binding.TryGetValue(token.text, out List? replacement)) + { + substituted.AddRange(replacement); + } + else + { + substituted.Add(token); + } + } + result.Add(substituted); + } + + return result; + } + + static List> splitArguments(List line, int start) + { + var arguments = new List>(); + var current = new List(); + int depth = 0; + + for (int index = start; index < line.Count; index++) + { + Token token = line[index]; + if (token.kind == TokenKind.LeftParen || token.kind == TokenKind.LeftBracket) + { + depth++; + } + else if (token.kind == TokenKind.RightParen || token.kind == TokenKind.RightBracket) + { + depth--; + } + + if (token.kind == TokenKind.Comma && depth == 0) + { + arguments.Add(current); + current = new List(); + continue; + } + + current.Add(token); + } + + if (current.Count > 0) + { + arguments.Add(current); + } + + return arguments; + } + + static List> splitLines(List tokens) + { + var lines = new List>(); + var current = new List(); + + foreach (Token token in tokens) + { + if (token.kind == TokenKind.EndOfLine || token.kind == TokenKind.EndOfFile) + { + if (current.Count > 0) + { + lines.Add(current); + current = new List(); + } + continue; + } + current.Add(token); + } + + return lines; + } + + static List joinLines(List> lines) + { + var tokens = new List(); + foreach (List line in lines) + { + tokens.AddRange(line); + SourcePosition position = line.Count > 0 ? line[line.Count - 1].position : new SourcePosition(0, 0); + tokens.Add(new Token(TokenKind.EndOfLine, "\\n", 0, position)); + } + tokens.Add(new Token(TokenKind.EndOfFile, "", 0, new SourcePosition(0, 0))); + return tokens; + } +}