Working: labels, label references, macros, ops, error messaging, dry run, verbose

This commit is contained in:
sloum 2024-01-02 15:06:57 -08:00
parent a71e8032ef
commit 17595fd409
5 changed files with 529 additions and 77 deletions

3
.gitignore vendored
View File

@ -1 +1,4 @@
bird-asm
*.rom
*.asm
*.brd

View File

@ -4,6 +4,16 @@ import (
"flag"
"fmt"
"os"
"os/user"
"path/filepath"
"strings"
)
const (
argsError = 1
fileError = iota
lexError
compileError
)
func fileExists(p string) (bool, error) {
@ -16,20 +26,20 @@ func fileExists(p string) (bool, error) {
func setIn() {
args := flag.Args()
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "no input file\n")
os.Exit(1)
fmt.Fprintf(os.Stderr, "No input file\n")
os.Exit(argsError)
} else if len(args) > 1 {
fmt.Fprintf(os.Stderr, "too many input files (%d), needed 1\n", len(args))
os.Exit(2)
fmt.Fprintf(os.Stderr, "Too many input files (%d), needed 1\n", len(args))
os.Exit(argsError)
}
in = args[0]
in = absFilepath(args[0])
exists, err := fileExists(in)
if err != nil {
fmt.Fprintf(os.Stderr, "could not open file\n\t%s", err.Error())
os.Exit(3)
fmt.Fprintf(os.Stderr, "Could not open file\n\t%s", err.Error())
os.Exit(fileError)
} else if !exists {
fmt.Fprintf(os.Stderr, "%q does not exist\n", in)
os.Exit(4)
os.Exit(fileError)
}
}
@ -41,4 +51,67 @@ func setOut(o string) {
}
}
func isHexValue(v byte) bool {
if (v >= 0x30 && v <= 0x39) || (v >= 0x41 && v <= 0x46) || (v >= 0x61 && v <= 0x66) {
return true
}
return false
}
func isHex(s string) bool {
if len(s) != 2 {
return false
}
for _, b := range []byte(s) {
if !isHexValue(b) {
return false
}
}
return true
}
func loadFile(p string) string {
b, err := os.ReadFile(p)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(fileError)
}
return string(b)
}
func absFilepath(p string) string {
if strings.HasPrefix(p, "~") {
if p == "~" || strings.HasPrefix(p, "~/") {
homedir, _ := os.UserHomeDir()
if len(p) <= 2 {
p = homedir
} else if len(p) > 2 {
p = filepath.Join(homedir, p[2:])
}
} else {
i := strings.IndexRune(p, '/')
var u string
var remainder string
if i < 0 {
u = p[1:]
remainder = ""
} else {
u = p[1:i]
remainder = p[i:]
}
usr, err := user.Lookup(u)
if err != nil {
p = filepath.Join("/home", u, remainder)
} else {
p = filepath.Join(usr.HomeDir, remainder)
}
}
} else if !strings.HasPrefix(p, "/") {
wd, _ := os.Getwd()
p = filepath.Join(wd, p)
}
path, _ := filepath.Abs(p)
return path
}

155
lex.go Normal file
View File

@ -0,0 +1,155 @@
package main
import (
"fmt"
"os"
"strings"
"unicode"
)
var lexLine uint16
func eatMacro(r *strings.Reader) (string, []token) {
startLine := lexLine
name := eatText(r)
data := make([]token, 0)
for r.Len() > 0 {
eatWhiteSpace(r)
c, _, err := r.ReadRune()
if err != nil {
break
}
switch c {
case '[', '{', ']', '}':
// Allow these chars, but assign no meaning to them
continue
case '/':
if c == '/' {
eatSingleLineComment(r)
} else if c == '*' {
eatMultiLineComment(r)
} else {
fmt.Fprintf(os.Stderr, "Illegal char found following '/' on line %d,\nexpected '/' or '*'\n", lexLine)
os.Exit(lexError)
}
case '\'':
c, _, err := r.ReadRune()
if err != nil {
fmt.Fprintf(os.Stderr, "Incomplete char literal on line %d\n", lexLine)
os.Exit(lexError)
}
data = append(data, []token{token{"PSH", lexLine}, token{fmt.Sprintf("%02x", c), lexLine}}...)
case '#':
data = append(data, eatLiteral(r)...)
case '%':
fmt.Fprintf(os.Stderr, "Macro definition inside of macro definition starting on line %d\n", startLine)
os.Exit(lexError)
default:
r.UnreadRune()
dataWord := eatText(r)
if dataWord == ";" {
return name, data
} else {
if dataWord == name {
fmt.Fprintf(os.Stderr, "Macro recursively referencing itself on line %d\n", lexLine)
os.Exit(lexError)
}
data = append(data, token{dataWord, lexLine})
}
}
}
fmt.Fprintf(os.Stderr, "Unclosed macro starting on line %d\n", startLine)
os.Exit(lexError)
return name, data
}
func eatLiteral(r *strings.Reader) []token {
num := eatText(r)
for _, v := range []byte(num) {
if !isHexValue(v) {
fmt.Fprintf(os.Stderr, "Invalid literal '#' value on line %d\n", lexLine)
os.Exit(lexError)
}
}
if len(num) == 2 {
return []token{token{"PSH", lexLine}, token{num, lexLine}}
} else if len(num) == 4 {
return []token{token{"PSH2", lexLine}, token{num[:2], lexLine}, token{num[2:], lexLine}}
} else {
fmt.Fprintf(os.Stderr, "Invalid PSH/# value on line %d\n", lexLine)
os.Exit(lexError)
}
return []token{} // will never happen, but makes the compiler happy
}
func eatText(r *strings.Reader) string {
var buf strings.Builder
for r.Len() > 0 {
c, _, err := r.ReadRune()
if err != nil || unicode.IsSpace(c) || c == '[' || c == ']' || c == '{' || c == '}' {
break
}
buf.WriteRune(c)
}
r.UnreadRune()
return buf.String()
}
func eatWhiteSpace(r *strings.Reader) {
for r.Len() > 0 {
c, _, err := r.ReadRune()
if err != nil {
return
}
if c == '\n' {
lexLine++
}
if !unicode.IsSpace(c) {
r.UnreadRune()
break
}
}
}
func eatSingleLineComment(r *strings.Reader) {
for r.Len() > 0 {
c, _, err := r.ReadRune()
if err != nil {
return
}
if c == '\n' {
lexLine++
break
}
}
}
func eatMultiLineComment(r *strings.Reader) {
startLine := lexLine
var asterisk bool
for r.Len() > 0 {
c, _, err := r.ReadRune()
if err != nil {
break
}
switch c {
case '*':
asterisk = true
continue
case '/':
if asterisk {
return
}
case '\n':
lexLine++
default:
asterisk = false
}
}
fmt.Fprintf(os.Stderr, "Unclosed multiline comment starting on line %d\n", startLine)
os.Exit(lexError)
}

29
main.go
View File

@ -13,6 +13,7 @@ var (
returnMask byte = 1 << 6
verbose bool
dryRun bool
expansions uint16
ops = map[string]uint8{
"NOP": 0x00, "SET": 0x01, "GET": 0x02, "SEI": 0x03, "GEI": 0x04,
"PSH": 0x07, "POP": 0x08, "OVR": 0x09, "SWP": 0x0A, "ROT": 0x0B, "DUP": 0x0C, "SSW": 0x0D,
@ -20,7 +21,31 @@ var (
"AND": 0x15, "BOR": 0x16, "XOR": 0x17, "SHR": 0x18, "SHL": 0x19,
"JMP": 0x1C, "CAL": 0x1D, "JCD": 0x1E, "RET": 0x1F,
"GCH": 0x23, "GST": 0x24, "PCH": 0x25, "PST": 0x26, "HLT": 0x27, "PHX": 0x28,
"GRT": 0x2A, "LST": 0x2B, "GTE": 0x2C, "LTE": 0x2D, "EQL": 0x2E,
"GRT": 0x2A, "LST": 0x2B, "GTE": 0x2C, "LTE": 0x2D, "EQL": 0x2E, "NEQ": 0x2F,
"NOPr": 0x40, "SETr": 0x41, "GETr": 0x42, "SEIr": 0x43, "GEIr": 0x44,
"PSHr": 0x47, "POPr": 0x48, "OVRr": 0x49, "SWPr": 0x4A, "ROTr": 0x4B, "DUPr": 0x4C, "SSWr": 0x4D,
"ADDr": 0x4E, "SUBr": 0x4F, "MULr": 0x50, "DIVr": 0x51, "INCr": 0x52, "DECr": 0x53,
"ANDr": 0x55, "BORr": 0x56, "XORr": 0x57, "SHRr": 0x58, "SHLr": 0x59,
"JMPr": 0x5C, "CALr": 0x5D, "JCDr": 0x5E, "RETr": 0x5F,
"GCHr": 0x63, "GSTr": 0x64, "PCHr": 0x65, "PSTr": 0x66, "HLTr": 0x67, "PHXr": 0x68,
"GRTr": 0x6A, "LSTr": 0x6B, "GTEr": 0x6C, "LTEr": 0x6D, "EQLr": 0x6E, "NEQr": 0x6F,
"NOP2": 0x80, "SET2": 0x81, "GET2": 0x82, "SEI2": 0x83, "GEI2": 0x84,
"PSH2": 0x87, "POP2": 0x88, "OVR2": 0x89, "SWP2": 0x8A, "ROT2": 0x8B, "DUP2": 0x8C, "SSW2": 0x8D,
"ADD2": 0x8E, "SUB2": 0x8F, "MUL2": 0x90, "DIV2": 0x91, "INC2": 0x92, "DEC2": 0x93,
"AND2": 0x95, "BOR2": 0x96, "XOR2": 0x97, "SHR2": 0x98, "SHL2": 0x99,
"JMP2": 0x9C, "CAL2": 0x9D, "JCD2": 0x9E, "RET2": 0x9F,
"GCH2": 0xA3, "GST2": 0xA4, "PCH2": 0xA5, "PST2": 0xA6, "HLT2": 0xA7, "PHX2": 0xA8,
"GRT2": 0xAA, "LST2": 0xAB, "GTE2": 0xAC, "LTE2": 0xAD, "EQL2": 0xAE, "NEQ2": 0xAF,
"NOP2r": 0xC0, "SET2r": 0xC1, "GET2r": 0xC2, "SEI2r": 0xC3, "GEI2r": 0xC4,
"PSH2r": 0xC7, "POP2r": 0xC8, "OVR2r": 0xC9, "SWP2r": 0xCA, "ROT2r": 0xCB, "DUP2r": 0xCC, "SSW2r": 0xCD,
"ADD2r": 0xCE, "SUB2r": 0xCF, "MUL2r": 0xD0, "DIV2r": 0xD1, "INC2r": 0xD2, "DEC2r": 0xD3,
"AND2r": 0xD5, "BOR2r": 0xD6, "XOR2r": 0xD7, "SHR2r": 0xD8, "SHL2r": 0xD9,
"JMP2r": 0xDC, "CAL2r": 0xDD, "JCD2r": 0xDE, "RET2r": 0xDF,
"GCH2r": 0xE3, "GST2r": 0xE4, "PCH2r": 0xE5, "PST2r": 0xE6, "HLT2r": 0xE7, "PHX2r": 0xE8,
"GRT2r": 0xEA, "LST2r": 0xEB, "GTE2r": 0xEC, "LTE2r": 0xED, "EQL2r": 0xEE, "NEQ2r": 0xEF,
}
)
@ -39,4 +64,6 @@ func main() {
setIn()
setOut(*outFile)
program := newProg()
program.Compile()
}

330
types.go
View File

@ -3,102 +3,296 @@ package main
import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
"unicode"
)
const (
errTmplt string = "line %d: %s: \033[1m%s\033[0m"
)
type token struct {
val string
line uint16
}
type label struct {
ref uint16
count uint16
parent string
isSublabel bool
}
func (l *label) Inc() {
l.count++
memContext bool
}
type prog struct {
labels map[string]label
sublabels map[string]label
macros map[string]string
macros map[string][]token
source string
mp uint16
cp uint16
unknownWords []string
tokens []token
errors []string
memContext bool
labelCounts map[string]uint16
}
func newProg() prog {
return prog{
make(map[string]label),
make(map[string][]token),
loadFile(in),
0,
make([]token, 0, 0),
make([]string, 0, 5),
false,
make(map[string]uint16),
}
}
func (p *prog) Compile() {
// 1. Strip comments from source file
p.stripComments()
// 1. Tokenize the source, stripping comments
p.Tokenize(p.source)
// 2. Expand macros
p.expandMacros()
}
func (p *prog) stripComments() {
if verbose {
fmt.Fprint(os.Stderr, "Stripping comments\n")
fmt.Fprintf(os.Stderr, "+ \033[1mExpanding macros\033[0m\n\tFound %d macros.\n", len(p.macros))
}
slc := regexp.MustCompile(`\/\/.*`)
mlc := regexp.MustCompile(`(?s)\/\*.*\*\/`)
p.source = slc.ReplaceAllString(p.source, "")
p.source = mlc.ReplaceAllString(p.source, "")
}
func (p *prog) expandMacros() {
if verbose {
fmt.Fprint(os.Stderr, "+ Expanding macros\n")
}
// Found all macro declarations
re := regexp.MustCompile(`%(\w+)\s+([^;]*)\s+\;`)
macrosFound := re.FindAllStringSubmatch(p.source, -1)
// If there were none, return
if macrosFound == nil {
if len(p.macros) == 0 {
if verbose {
fmt.Fprint(os.Stderr, "\t└ No macros were found\n")
fmt.Fprint(os.Stderr, "\tSkipping.\n")
}
} else {
p.tokens = p.expandMacros(p.tokens)
if verbose {
fmt.Fprintf(os.Stderr, "\tExpanded %d macro references.\n\tDone.\n", expansions)
}
}
// 3. Build labels and other memory constructs
p.tokens = p.buildLabels(p.tokens)
if verbose {
fmt.Fprintf(os.Stderr, "\t%d label definitions found\n\tDone.\n", len(p.labels))
}
// 4. Add the end of defined memory to the beginning of the token stream
// this will allow dynamic memory allocations without overwriting existing
// data. It will also make the offset change for the
heapStart := fmt.Sprintf("%04x", len(p.tokens)+2+int(p.mp))
t := []token{token{heapStart[:2],0}, token{heapStart[2:],0}}
t = append(t, p.tokens...)
// 5. Build the byte slice that is the final compiled file
if verbose {
fmt.Fprint(os.Stderr, "+ \033[1mGenerating bytes from token stream\033[0m\n")
}
b := make([]byte, 0, len(t))
for i := 0; i < len(t); i++ {
if isHex(t[i].val) {
// Write a hex value
n, err := strconv.ParseUint(t[i].val, 16, 8)
if err != nil {
p.errors = append(p.errors, fmt.Sprintf(errTmplt, t[i].line, "byte out or range", t[i].val))
continue
}
b = append(b, byte(n))
} else if v, ok := ops[t[i].val]; ok {
// Write the op value
b = append(b, v)
} else if t[i].val[0] == ':' {
// Fill in the label reference
name := t[i].val[1:]
if v, ok := p.labels[name]; ok {
b = append(b, ops["PSH2"])
i++
// Make the reference apply to the memory area
if v.memContext {
v.ref+=uint16(len(t))
}
b = append(b, byte(v.ref >> 8))
i++
b = append(b, byte(v.ref & uint16(0xFF)))
} else {
p.errors = append(p.errors, fmt.Sprintf(errTmplt, t[i].line, "Undefined label", t[i].val))
continue
}
} else {
p.errors = append(p.errors, fmt.Sprintf(errTmplt, t[i].line, "Unknown token", t[i].val))
}
}
if verbose {
fmt.Fprint(os.Stderr, "\tDone.\n+ \033[1mChecking for errors\033[0m\n")
}
// 6. If there are errors, bail out now and show them
if len(p.errors) > 0 {
fmt.Fprintf(os.Stderr, "%s\n", strings.Join(p.errors, "\n"))
os.Exit(compileError)
}
if verbose {
fmt.Fprint(os.Stderr, "\tDone.\n")
}
if dryRun {
return
}
// Load the macros into our map
for i := range macrosFound {
p.macros[macrosFound[i][1]] = macrosFound[i][2]
if verbose {
fmt.Fprint(os.Stderr, "+ \033[1mWriting to file\033[0m\n")
}
// Remove the declarations
p.source = re.ReplaceAllString(p.source, "")
var twice bool
Twice:
// Expand the macros in the raw source
for k, v := range p.macros {
reM := regexp.MustCompile(`(?m)(\s|^)(?:`+k+`)(\s|$)`)
p.source = reM.ReplaceAllString(p.source, `${1}`+v+`${2}`)
}
if !twice {
twice = true
goto Twice
// 7. write it all to file. Done.
err := os.WriteFile(out, b, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "Error writing to %s:\n %s", out, err.Error())
return
}
if verbose {
fmt.Fprintf(os.Stderr, "\t└ %d macros declarations were found and references were expanded\n", len(p.macros))
fmt.Fprint(os.Stderr, "\tDone.\n\n")
}
fmt.Fprintf(os.Stdout, "Wrote %d bytes\n", len(b))
}
func (p *prog) expandMacros(t []token) []token {
tokens := make([]token, 0, len(t))
for _, v := range t {
if macroData, ok := p.macros[v.val]; ok {
// Macro, expand it (recursively)
expansions++
tokens = append(tokens, p.expandMacros(macroData)...)
} else {
// Not a macro, just add it
tokens = append(tokens, v)
}
}
return tokens
}
/* 1. Strip comments (using regex for dev speed at the moment)
* 2. Do a pass for expanding macros
* 3. Do a pass for finding labels
* 4.
*
* :Label1 (refers to program counter)
* @var1 (var is a pointer to memory)
* +subvar (creates a subvar at current mem pointer)
* $2 (pads the mem pointer to create sizes of vars)
* %macro1 #2c pch .
* Err should be thrown if an attempt to jump tp a var/subvar
* is made.
*/
func (p *prog) buildLabels(t []token) []token {
if verbose {
fmt.Fprint(os.Stderr, "+ \033[1mBuilding label definitions\033[0m\n")
}
tokens := make([]token, 0, len(t))
for _, l := range t {
switch l.val[0] {
case '@':
// parse label definition
if _, ok := p.labels[l.val[1:]]; ok {
p.errors = append(p.errors, fmt.Sprintf(errTmplt, l.line, "Cannot redefine", l.val))
continue
}
var la label
// TODO
// This is wrong. It cannot be known yet.
// Need to add an update step for address (unless it is memContext true)
if p.memContext {
la.ref = p.mp
la.memContext = true
} else {
la.ref = uint16(len(tokens))+2
}
p.labels[l.val[1:]] = la
case '$':
// pad memory
if !p.memContext {
p.errors = append(p.errors, fmt.Sprintf(errTmplt, l.line, "Memory padd in invalid context", "!program"))
continue
}
num := l.val[1:]
s, err := strconv.ParseUint(num, 16, 8)
if err != nil {
p.errors = append(p.errors, fmt.Sprintf(errTmplt, l.line, "Invalid pad value", l.val))
continue
}
p.mp += uint16(s)
case '!':
// Memory context change
if l.val[1:] == "memory" {
p.memContext = true
} else if l.val[1:] == "program" {
p.memContext = false
} else {
p.errors = append(p.errors, fmt.Sprintf(errTmplt, l.line, "Undefined", l.val))
continue
}
default:
tokens = append(tokens, l)
}
}
// TODO
// Add verbose output
return tokens
}
func (p *prog) Tokenize(s string) {
lexLine = 1
reader := strings.NewReader(s)
tokens := make([]token, 0)
var currentString string
if verbose {
fmt.Fprint(os.Stderr, "+ \033[1mLexing input file\033[0m\n")
}
TokenizationLoop:
for reader.Len() > 0 {
c, _, err := reader.ReadRune()
if err != nil {
break TokenizationLoop
}
if unicode.IsSpace(c) {
if c == '\n' {
lexLine++
}
eatWhiteSpace(reader)
continue
}
switch c {
case '[', '{', ']', '}':
// Allow these chars, but assign no meaning to them
continue
case '/':
c, _, err := reader.ReadRune()
if err != nil {
fmt.Fprintf(os.Stderr, "Incomplete comment started on line %d\n", lexLine)
os.Exit(lexError)
}
if c == '/' {
eatSingleLineComment(reader)
} else if c == '*' {
eatMultiLineComment(reader)
} else {
fmt.Fprintf(os.Stderr, "Illegal char found following '/' on line %d,\nexpected '/' or '*'", lexLine)
os.Exit(lexError)
}
case '\'':
c, _, err := reader.ReadRune()
if err != nil {
fmt.Fprintf(os.Stderr, "Incomplete char literal on line %d\n", lexLine)
os.Exit(lexError)
}
tokens = append(tokens, []token{token{"PSH", lexLine}, token{fmt.Sprintf("%02x", c), lexLine}}...)
case '#':
tokens = append(tokens, eatLiteral(reader)...)
case '%':
name, data := eatMacro(reader)
if _, ok := p.macros[name]; ok {
fmt.Fprintf(os.Stderr, "Macro %q was re-declared on line %d", name, lexLine)
os.Exit(lexError)
}
p.macros[name] = data
case ':':
reader.UnreadRune()
currentString = eatText(reader)
if currentString != "" {
tokens = append(tokens, []token{token{currentString, lexLine},token{"NOP", lexLine}, token{"NOP", lexLine}}...)
}
default:
reader.UnreadRune()
currentString = eatText(reader)
if currentString != "" {
tokens = append(tokens, token{currentString, lexLine})
}
}
}
if verbose {
fmt.Fprintf(os.Stderr, "\tDone.\n")
}
p.tokens = tokens
}