feat(core/asm): add assembler parser
This commit is contained in:
1 parent
40db2b763e
commit
42f330110b
1 file changed
+266
@@ -0,0 +1,266 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Chip8.Core.Assembly;
|
||||||
|
|
||||||
|
public sealed class Parser
|
||||||
|
{
|
||||||
|
static readonly HashSet<string> directives = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"DB", "DW", "ORG", "ALIGN",
|
||||||
|
};
|
||||||
|
|
||||||
|
readonly List<Token> tokens;
|
||||||
|
int index;
|
||||||
|
|
||||||
|
public Parser(List<Token> tokens)
|
||||||
|
{
|
||||||
|
this.tokens = tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Statement> parse()
|
||||||
|
{
|
||||||
|
var statements = new List<Statement>();
|
||||||
|
|
||||||
|
while (current().kind != TokenKind.EndOfFile)
|
||||||
|
{
|
||||||
|
if (current().kind == TokenKind.EndOfLine)
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseLine(statements);
|
||||||
|
}
|
||||||
|
|
||||||
|
return statements;
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseLine(List<Statement> statements)
|
||||||
|
{
|
||||||
|
List<Token> line = takeLine();
|
||||||
|
int cursor = 0;
|
||||||
|
|
||||||
|
if (line.Count >= 2 && line[0].kind == TokenKind.Identifier && line[1].kind == TokenKind.Colon)
|
||||||
|
{
|
||||||
|
statements.Add(new LabelStatement(line[0].text, line[0].position));
|
||||||
|
cursor = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor >= line.Count)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Token head = line[cursor];
|
||||||
|
if (head.kind != TokenKind.Identifier)
|
||||||
|
{
|
||||||
|
throw new AsmException("expected an instruction or directive but found '" + head.text + "'", head.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor + 1 < line.Count && line[cursor + 1].isIdentifier("EQU"))
|
||||||
|
{
|
||||||
|
List<Token> expression = line.GetRange(cursor + 2, line.Count - cursor - 2);
|
||||||
|
if (expression.Count == 0)
|
||||||
|
{
|
||||||
|
throw new AsmException("EQU needs a value", head.position);
|
||||||
|
}
|
||||||
|
statements.Add(new ConstantStatement(head.text, expression, head.position));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directives.Contains(head.text))
|
||||||
|
{
|
||||||
|
statements.Add(parseDirective(head, line, cursor + 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var operands = parseOperands(line, cursor + 1);
|
||||||
|
statements.Add(new InstructionStatement(head.text, operands, head.position));
|
||||||
|
}
|
||||||
|
|
||||||
|
Statement parseDirective(Token head, List<Token> line, int start)
|
||||||
|
{
|
||||||
|
List<Token> body = line.GetRange(start, line.Count - start);
|
||||||
|
|
||||||
|
if (head.isIdentifier("ORG"))
|
||||||
|
{
|
||||||
|
requireExpression(body, head.position, "ORG");
|
||||||
|
return new OrgStatement(body, head.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (head.isIdentifier("ALIGN"))
|
||||||
|
{
|
||||||
|
requireExpression(body, head.position, "ALIGN");
|
||||||
|
return new AlignStatement(body, head.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool word = head.isIdentifier("DW");
|
||||||
|
List<IReadOnlyList<Token>> items = splitByComma(body, head.position);
|
||||||
|
if (items.Count == 0)
|
||||||
|
{
|
||||||
|
throw new AsmException(head.text + " needs at least one value", head.position);
|
||||||
|
}
|
||||||
|
return new DataStatement(word, items, head.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Operand> parseOperands(List<Token> line, int start)
|
||||||
|
{
|
||||||
|
var operands = new List<Operand>();
|
||||||
|
if (start >= line.Count)
|
||||||
|
{
|
||||||
|
return operands;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Token> body = line.GetRange(start, line.Count - start);
|
||||||
|
foreach (IReadOnlyList<Token> group in splitByComma(body, line[start].position))
|
||||||
|
{
|
||||||
|
operands.Add(classifyOperand(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
return operands;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Operand classifyOperand(IReadOnlyList<Token> group)
|
||||||
|
{
|
||||||
|
if (group.Count == 0)
|
||||||
|
{
|
||||||
|
throw new AsmException("empty operand", new SourcePosition(0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
SourcePosition position = group[0].position;
|
||||||
|
|
||||||
|
if (group.Count == 3 && group[0].kind == TokenKind.LeftBracket
|
||||||
|
&& group[1].isIdentifier("I") && group[2].kind == TokenKind.RightBracket)
|
||||||
|
{
|
||||||
|
return Operand.special(OperandKind.IndexIndirect, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.Count == 1 && group[0].kind == TokenKind.Identifier)
|
||||||
|
{
|
||||||
|
int register = registerIndex(group[0].text);
|
||||||
|
if (register >= 0)
|
||||||
|
{
|
||||||
|
return Operand.register(register, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
OperandKind? special = specialRegister(group[0].text);
|
||||||
|
if (special.HasValue)
|
||||||
|
{
|
||||||
|
return Operand.special(special.Value, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.Count == 3 && group[1].kind == TokenKind.Minus
|
||||||
|
&& group[0].kind == TokenKind.Identifier && group[2].kind == TokenKind.Identifier)
|
||||||
|
{
|
||||||
|
int start = registerIndex(group[0].text);
|
||||||
|
int end = registerIndex(group[2].text);
|
||||||
|
if (start >= 0 && end >= 0)
|
||||||
|
{
|
||||||
|
return Operand.registerRange(start, end, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Operand.immediate(new List<Token>(group), position);
|
||||||
|
}
|
||||||
|
|
||||||
|
static OperandKind? specialRegister(string text) => text.ToUpperInvariant() switch
|
||||||
|
{
|
||||||
|
"I" => OperandKind.IndexRegister,
|
||||||
|
"DT" => OperandKind.DelayTimer,
|
||||||
|
"ST" => OperandKind.SoundTimer,
|
||||||
|
"K" => OperandKind.KeyReg,
|
||||||
|
"F" => OperandKind.FontReg,
|
||||||
|
"HF" => OperandKind.BigFontReg,
|
||||||
|
"B" => OperandKind.BcdReg,
|
||||||
|
"R" => OperandKind.RplReg,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
static int registerIndex(string text)
|
||||||
|
{
|
||||||
|
if (text.Length != 2 || (text[0] != 'V' && text[0] != 'v'))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
char digit = text[1];
|
||||||
|
if (digit >= '0' && digit <= '9')
|
||||||
|
{
|
||||||
|
return digit - '0';
|
||||||
|
}
|
||||||
|
if (digit >= 'A' && digit <= 'F')
|
||||||
|
{
|
||||||
|
return digit - 'A' + 10;
|
||||||
|
}
|
||||||
|
if (digit >= 'a' && digit <= 'f')
|
||||||
|
{
|
||||||
|
return digit - 'a' + 10;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<IReadOnlyList<Token>> splitByComma(List<Token> body, SourcePosition position)
|
||||||
|
{
|
||||||
|
var groups = new List<IReadOnlyList<Token>>();
|
||||||
|
var current = new List<Token>();
|
||||||
|
int depth = 0;
|
||||||
|
|
||||||
|
foreach (Token token in body)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (current.Count == 0)
|
||||||
|
{
|
||||||
|
throw new AsmException("empty operand before comma", token.position);
|
||||||
|
}
|
||||||
|
groups.Add(current);
|
||||||
|
current = new List<Token>();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
current.Add(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.Count > 0)
|
||||||
|
{
|
||||||
|
groups.Add(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void requireExpression(List<Token> body, SourcePosition position, string directive)
|
||||||
|
{
|
||||||
|
if (body.Count == 0)
|
||||||
|
{
|
||||||
|
throw new AsmException(directive + " needs an expression", position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Token> takeLine()
|
||||||
|
{
|
||||||
|
var line = new List<Token>();
|
||||||
|
while (current().kind != TokenKind.EndOfLine && current().kind != TokenKind.EndOfFile)
|
||||||
|
{
|
||||||
|
line.Add(current());
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
if (current().kind == TokenKind.EndOfLine)
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
Token current() => tokens[index];
|
||||||
|
}
|
||||||
Reference in new issue
Block a user