283 lines
7.1 KiB
Go
283 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
const (
|
|
errTmplt string = "line %d: %s: \033[1m%s\033[0m\n"
|
|
)
|
|
|
|
type token struct {
|
|
val string
|
|
line uint16
|
|
}
|
|
|
|
type label struct {
|
|
ref uint16
|
|
mem bool
|
|
}
|
|
|
|
type prog struct {
|
|
labels map[string]label
|
|
macros map[string][]token
|
|
source string
|
|
mp uint16
|
|
tokens []token
|
|
errors []string
|
|
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),
|
|
make(map[string]uint16),
|
|
}
|
|
}
|
|
|
|
func (p *prog) Compile() {
|
|
// 1. Tokenize the source, stripping comments
|
|
p.Tokenize(p.source)
|
|
|
|
// 2. Expand macros
|
|
if verbose {
|
|
fmt.Fprintf(os.Stderr, "+ \033[1mExpanding macros\033[0m\n\tFound %d macros.\n", len(p.macros))
|
|
}
|
|
if len(p.macros) == 0 {
|
|
if verbose {
|
|
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
|
|
ref := v.ref
|
|
if v.mem {
|
|
ref+=uint16(len(t))
|
|
}
|
|
b = append(b, byte(ref >> 8))
|
|
i++
|
|
b = append(b, byte(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
|
|
}
|
|
if verbose {
|
|
fmt.Fprint(os.Stderr, "+ \033[1mWriting to file\033[0m\n")
|
|
}
|
|
|
|
// 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.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
|
|
}
|
|
|
|
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
|
|
if l.val[0] == '+' {
|
|
la.ref = p.mp
|
|
la.mem = true
|
|
} else {
|
|
la.ref = uint16(len(tokens))+2
|
|
}
|
|
p.labels[l.val[1:]] = la
|
|
case '$':
|
|
// pad memory
|
|
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)
|
|
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 strDelim:
|
|
tokens = append(tokens, lexString(reader)...)
|
|
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
|
|
}
|
|
|