feat(core/asm): add macro expansion
This commit is contained in:
1 parent
42f330110b
commit
2004647201
1 file changed
+225
@@ -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<string> parameters = new();
|
||||
public List<List<Token>> body = new();
|
||||
public SourcePosition position;
|
||||
}
|
||||
|
||||
readonly Dictionary<string, MacroDefinition> macros = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public List<Token> expand(List<Token> tokens)
|
||||
{
|
||||
List<List<Token>> lines = splitLines(tokens);
|
||||
List<List<Token>> withoutDefinitions = collectDefinitions(lines);
|
||||
var output = new List<List<Token>>();
|
||||
emitLines(withoutDefinitions, output, 0);
|
||||
return joinLines(output);
|
||||
}
|
||||
|
||||
List<List<Token>> collectDefinitions(List<List<Token>> lines)
|
||||
{
|
||||
var remaining = new List<List<Token>>();
|
||||
int index = 0;
|
||||
|
||||
while (index < lines.Count)
|
||||
{
|
||||
List<Token> 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<List<Token>> lines, int index)
|
||||
{
|
||||
List<Token> 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<Token> 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<List<Token>> lines, List<List<Token>> 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<Token> original in lines)
|
||||
{
|
||||
List<Token> line = original;
|
||||
|
||||
if (line.Count >= 2 && line[0].kind == TokenKind.Identifier && line[1].kind == TokenKind.Colon)
|
||||
{
|
||||
output.Add(new List<Token> { 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<List<Token>> expanded = instantiate(definition, line);
|
||||
emitLines(expanded, output, depth + 1);
|
||||
}
|
||||
else if (line.Count > 0)
|
||||
{
|
||||
output.Add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static List<List<Token>> instantiate(MacroDefinition definition, List<Token> call)
|
||||
{
|
||||
List<List<Token>> 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<string, List<Token>>(StringComparer.OrdinalIgnoreCase);
|
||||
for (int p = 0; p < definition.parameters.Count; p++)
|
||||
{
|
||||
binding[definition.parameters[p]] = arguments[p];
|
||||
}
|
||||
|
||||
var result = new List<List<Token>>();
|
||||
foreach (List<Token> bodyLine in definition.body)
|
||||
{
|
||||
var substituted = new List<Token>();
|
||||
foreach (Token token in bodyLine)
|
||||
{
|
||||
if (token.kind == TokenKind.Identifier && binding.TryGetValue(token.text, out List<Token>? replacement))
|
||||
{
|
||||
substituted.AddRange(replacement);
|
||||
}
|
||||
else
|
||||
{
|
||||
substituted.Add(token);
|
||||
}
|
||||
}
|
||||
result.Add(substituted);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static List<List<Token>> splitArguments(List<Token> line, int start)
|
||||
{
|
||||
var arguments = new List<List<Token>>();
|
||||
var current = new List<Token>();
|
||||
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<Token>();
|
||||
continue;
|
||||
}
|
||||
|
||||
current.Add(token);
|
||||
}
|
||||
|
||||
if (current.Count > 0)
|
||||
{
|
||||
arguments.Add(current);
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
|
||||
static List<List<Token>> splitLines(List<Token> tokens)
|
||||
{
|
||||
var lines = new List<List<Token>>();
|
||||
var current = new List<Token>();
|
||||
|
||||
foreach (Token token in tokens)
|
||||
{
|
||||
if (token.kind == TokenKind.EndOfLine || token.kind == TokenKind.EndOfFile)
|
||||
{
|
||||
if (current.Count > 0)
|
||||
{
|
||||
lines.Add(current);
|
||||
current = new List<Token>();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
current.Add(token);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
static List<Token> joinLines(List<List<Token>> lines)
|
||||
{
|
||||
var tokens = new List<Token>();
|
||||
foreach (List<Token> 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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user