diff --git a/Makefile b/Makefile index 80d0ffd9..640cef59 100644 --- a/Makefile +++ b/Makefile @@ -6,10 +6,11 @@ TMP=./tmp/dist buildgrammars: mkdir -p gen/parser $(LEZER) src/parser/lang-6502.grammar -o gen/parser/lang-6502.grammar.js + $(LEZER) src/parser/lang-z80.grammar -o gen/parser/lang-z80.grammar.js watchgrammars: while true; do \ - if [ src/parser/lang-6502.grammar -nt gen/parser/lang-6502.grammar.js ]; then \ + if [ src/parser/lang-6502.grammar -nt gen/parser/lang-6502.grammar.js ] || [ src/parser/lang-z80.grammar -nt gen/parser/lang-z80.grammar.js ]; then \ make buildgrammars; \ fi; \ sleep 1; \ diff --git a/src/parser/lang-z80.grammar b/src/parser/lang-z80.grammar new file mode 100644 index 00000000..a4c5f063 --- /dev/null +++ b/src/parser/lang-z80.grammar @@ -0,0 +1,166 @@ +@top Program { Line* } + +@skip { space | Comment } + +Line { + Label? Statement? eol +} + +Statement { + Instruction | + Directive +} + +Label { Identifier ":" | Identifier } + +Instruction { + Opcode Operand? +} + +Directive { + PseudoOp (Expression)* +} + +PseudoOp { + @specialize +} + +Condition { + @specialize +} + +Register { + @specialize +} + +Opcode { + @specialize +} + +Expression { + Expression !logic LogicOp Expression | + Expression !bit BitOp Expression | + Expression !compare (CompareOp | BinaryLt | BinaryGt) Expression | + Expression !term (ArithOp | Plus | Minus | Percent) Expression | + UnaryExpression | + Value | + "(" Expression ")" +} + +UnaryExpression { + (Plus | Minus | Not | Tilde | UnaryLt | UnaryGt) Expression +} + +BinaryLt { lt !bin } +BinaryGt { gt !bin } +UnaryLt { lt !un } +UnaryGt { gt !un } + +Value { + Number | + Identifier | + Register | + Condition | + String | + Char +} + +Operand { + Expression (Comma Expression)* +} + +@tokens { + Identifier { $[a-zA-Z_] $[a-zA-Z0-9_]* } + + Hex { ("0x" | "$") $[0-9a-fA-F]+ | $[0-9] $[0-9a-fA-F]* "h" } + Bin { "%" $[01]+ | $[01]+ "b" } + Oct { "0o" $[0-7]+ | $[0-7]+ "o" } + Dec { $[0-9]+ } + + Number { Hex | Bin | Oct | Dec } + + String { '"' (!["\\\n] | "\\" _)* '"' } + + Char { "'" !['\\\n] "'"? } + + Comment { ";" ![\n]* } + + space { $[ \t]+ } + eol { $[\n\r]+ } + + Comma { "," } + ":" + "#" + "(" ")" + + ArithOp { "*" | "/" } + Percent { "%" } + Plus { "+" } + Minus { "-" } + + BitOp { "&" | "|" | "^" | "<<" | ">>" } + Tilde { "~" } + + LogicOp { "&&" | "||" } + Not { "!" } + + CompareOp { "==" | "!=" | "<=" | ">=" } + lt { "<" } + gt { ">" } + + @precedence { String, Char, Number, Percent, Identifier } +} + +@precedence { + un, + term @left, + compare @left, + bit @left, + logic @left, + bin @left, + PseudoOp, + Opcode, + Label +} + +@detectDelim diff --git a/src/parser/lang-z80.ts b/src/parser/lang-z80.ts index d36fa8ca..ba1cf47c 100644 --- a/src/parser/lang-z80.ts +++ b/src/parser/lang-z80.ts @@ -1,131 +1,44 @@ -// CodeMirror 6 language support for Z80 assembly -// Migrated from CodeMirror 5 mode -// Original copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/5/LICENSE +import { LRLanguage, LanguageSupport } from "@codemirror/language" +import { styleTags, tags as t } from "@lezer/highlight" +import { parser } from "../../gen/parser/lang-z80.grammar.js" -import { StreamLanguage, StreamParser } from "@codemirror/language"; -import { LanguageSupport } from "@codemirror/language"; - -interface Z80State { - context: number; -} - -interface Z80Config { - ez80?: boolean; -} - -// TODO: Migrate to CodeMirror 6 Lezer parser. -function createZ80Parser(config: Z80Config = {}): StreamParser { - const ez80 = config.ez80 || false; - - let keywords1: RegExp, keywords2: RegExp; - if (ez80) { - keywords1 = /^(exx?|(ld|cp)([di]r?)?|[lp]ea|pop|push|ad[cd]|cpl|daa|dec|inc|neg|sbc|sub|and|bit|[cs]cf|x?or|res|set|r[lr]c?a?|r[lr]d|s[lr]a|srl|djnz|nop|[de]i|halt|im|in([di]mr?|ir?|irx|2r?)|ot(dmr?|[id]rx|imr?)|out(0?|[di]r?|[di]2r?)|tst(io)?|slp)(\.([sl]?i)?[sl])?\b/i; - keywords2 = /^(((call|j[pr]|rst|ret[in]?)(\.([sl]?i)?[sl])?)|(rs|st)mix)\b/i; - } else { - keywords1 = /^(exx?|(ld|cp|in)([di]r?)?|pop|push|ad[cd]|cpl|daa|dec|inc|neg|sbc|sub|and|bit|[cs]cf|x?or|res|set|r[lr]c?a?|r[lr]d|s[lr]a|srl|djnz|nop|rst|[de]i|halt|im|ot[di]r|out[di]?)\b/i; - keywords2 = /^(call|j[pr]|ret[in]?|b_?(call|jump))\b/i; +export const LezerZ80: LRLanguage = LRLanguage.define({ + parser: parser.configure({ + props: [ + styleTags({ + Identifier: t.variableName, + PseudoOp: t.definition(t.variableName), + Opcode: t.keyword, + Register: t.typeName, + Condition: t.className, + Label: t.labelName, + String: t.string, + Char: t.number, + Number: t.number, + Comment: t.lineComment, + ArithOp: t.arithmeticOperator, + Plus: t.arithmeticOperator, + Minus: t.arithmeticOperator, + Percent: t.arithmeticOperator, + BitOp: t.bitwiseOperator, + Tilde: t.bitwiseOperator, + LogicOp: t.logicOperator, + Not: t.logicOperator, + CompareOp: t.compareOperator, + BinaryLt: t.compareOperator, + BinaryGt: t.compareOperator, + UnaryLt: t.arithmeticOperator, + UnaryGt: t.arithmeticOperator, + Comma: t.separator, + "( )": t.paren + }) + ] + }), + languageData: { + commentTokens: { line: ";" } } +}) - const variables1 = /^(af?|bc?|c|de?|e|hl?|l|i[xy]?|r|sp)\b/i; - const variables2 = /^(n?[zc]|p[oe]?|m)\b/i; - const errors = /^([hl][xy]|i[xy][hl]|slia|sll)\b/i; - const numbers = /^([\da-f]+h|[0-7]+o|[01]+b|\d+d?)\b/i; - - return { - startState(): Z80State { - return { - context: 0 - }; - }, - - token(stream, state) { - if (!stream.column()) - state.context = 0; - - if (stream.eatSpace()) - return null; - - var w; - - if (stream.eatWhile(/\w/)) { - if (ez80 && stream.eat('.')) { - stream.eatWhile(/\w/); - } - w = stream.current(); - - if (stream.indentation()) { - if ((state.context == 1 || state.context == 4) && variables1.test(w)) { - state.context = 4; - return 'variableName.special'; - } - - if (state.context == 2 && variables2.test(w)) { - state.context = 4; - return 'variableName.constant'; - } - - if (keywords1.test(w)) { - state.context = 1; - return 'keyword'; - } else if (keywords2.test(w)) { - state.context = 2; - return 'keyword'; - } else if (state.context == 4 && numbers.test(w)) { - return 'number'; - } - - if (errors.test(w)) - return 'invalid'; - } else if (stream.match(numbers)) { - return 'number'; - } else { - return null; - } - } else if (stream.eat(';')) { - stream.skipToEnd(); - return 'comment'; - } else if (stream.eat('"')) { - while (w = stream.next()) { - if (w == '"') - break; - - if (w == '\\') - stream.next(); - } - return 'string'; - } else if (stream.eat('\'')) { - if (stream.match(/\\?.'/)) - return 'number'; - } else if (stream.eat('.') || stream.sol() && stream.eat('#')) { - state.context = 5; - - if (stream.eatWhile(/\w/)) - return 'keyword.control'; - } else if (stream.eat('$')) { - if (stream.eatWhile(/[\da-f]/i)) - return 'number'; - } else if (stream.eat('%')) { - if (stream.eatWhile(/[01]/)) - return 'number'; - } else { - stream.next(); - } - return null; - } - }; -} - -/** - * Language support for Z80 assembly language - */ export function asmZ80(): LanguageSupport { - return new LanguageSupport(StreamLanguage.define(createZ80Parser())); -} - -/** - * Language support for eZ80 assembly language - */ -export function asmEZ80(): LanguageSupport { - return new LanguageSupport(StreamLanguage.define(createZ80Parser({ ez80: true }))); + return new LanguageSupport(LezerZ80) } diff --git a/test/parsers/testparserz80.js b/test/parsers/testparserz80.js new file mode 100644 index 00000000..08cd51a9 --- /dev/null +++ b/test/parsers/testparserz80.js @@ -0,0 +1,51 @@ +const assert = require('assert'); +const { EditorState } = require("@codemirror/state"); +const { syntaxTree } = require("@codemirror/language"); +const { asmZ80 } = require("../../gen/parser/lang-z80.js"); + +describe('Z80 Parser', function () { + + it('Should parse basic instructions', function () { + const code = ` + ld a, 0 + ld hl, $1234 + ret + `; + + // Create an editor state with the new parser + const state = EditorState.create({ + doc: code, + extensions: [asmZ80()] + }); + + // Check if the tree is available (basic check that parser didn't crash) + assert.ok(syntaxTree(state), "Syntax tree should be generated"); + }); + + it('Should handle labels', function () { + const code = ` + start: + jp start + `; + const state = EditorState.create({ + doc: code, + extensions: [asmZ80()] + }); + assert.ok(syntaxTree(state), "Syntax tree should be generated"); + }); + + it('Should handle 8080 instructions', function () { + const code = ` + mvi a, 0 + lxi h, $1234 + mov a, b + inx h + jmp start + `; + const state = EditorState.create({ + doc: code, + extensions: [asmZ80()] + }); + assert.ok(syntaxTree(state), "Syntax tree should be generated"); + }); +});