diff --git a/src/Chip8.Core/Assembly/Parser.cs b/src/Chip8.Core/Assembly/Parser.cs new file mode 100644 index 0000000..d340165 --- /dev/null +++ b/src/Chip8.Core/Assembly/Parser.cs @@ -0,0 +1,266 @@ +using System; +using System.Collections.Generic; + +namespace Chip8.Core.Assembly; + +public sealed class Parser +{ + static readonly HashSet directives = new(StringComparer.OrdinalIgnoreCase) + { + "DB", "DW", "ORG", "ALIGN", + }; + + readonly List tokens; + int index; + + public Parser(List tokens) + { + this.tokens = tokens; + } + + public List parse() + { + var statements = new List(); + + while (current().kind != TokenKind.EndOfFile) + { + if (current().kind == TokenKind.EndOfLine) + { + index++; + continue; + } + + parseLine(statements); + } + + return statements; + } + + void parseLine(List statements) + { + List 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 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 line, int start) + { + List 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> 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 parseOperands(List line, int start) + { + var operands = new List(); + if (start >= line.Count) + { + return operands; + } + + List body = line.GetRange(start, line.Count - start); + foreach (IReadOnlyList group in splitByComma(body, line[start].position)) + { + operands.Add(classifyOperand(group)); + } + + return operands; + } + + static Operand classifyOperand(IReadOnlyList 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(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> splitByComma(List body, SourcePosition position) + { + var groups = new List>(); + var current = new List(); + 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(); + continue; + } + + current.Add(token); + } + + if (current.Count > 0) + { + groups.Add(current); + } + + return groups; + } + + static void requireExpression(List body, SourcePosition position, string directive) + { + if (body.Count == 0) + { + throw new AsmException(directive + " needs an expression", position); + } + } + + List takeLine() + { + var line = new List(); + 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]; +}