tt/build/task.go

349 lines
7.6 KiB
Go

package build
import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"runtime/debug"
"slices"
"strings"
"robaertschi.xyz/robaertschi/tt/asm"
"robaertschi.xyz/robaertschi/tt/asm/amd64"
"robaertschi.xyz/robaertschi/tt/asm/qbe"
"robaertschi.xyz/robaertschi/tt/lexer"
"robaertschi.xyz/robaertschi/tt/parser"
"robaertschi.xyz/robaertschi/tt/token"
"robaertschi.xyz/robaertschi/tt/ttir"
"robaertschi.xyz/robaertschi/tt/typechecker"
"robaertschi.xyz/robaertschi/tt/utils"
)
type task interface {
Run(id int, output io.Writer, doneChan chan taskResult)
Name() string
WithName(string)
}
type processTask struct {
taskName string
name string
args []string
}
func NewProcessTask(name string, args ...string) task {
return &processTask{
name: name,
taskName: fmt.Sprintf("starting %q %v\n", name, args),
args: args,
}
}
func (pt *processTask) WithName(name string) {
pt.taskName = name
}
func (pt *processTask) Run(id int, output io.Writer, doneChan chan taskResult) {
cmd := exec.Command(pt.name, pt.args...)
cmd.Stdout = utils.NewPrefixWriterString(output, pt.name+" output: ")
cmd.Stderr = cmd.Stdout
err := cmd.Run()
var exitError error
if cmd.ProcessState.ExitCode() != 0 {
exitError = fmt.Errorf("command %q failed with exit code %d", pt.name, cmd.ProcessState.ExitCode())
}
doneChan <- taskResult{
Id: id,
Err: errors.Join(err, exitError),
}
}
func (pt *processTask) Name() string {
return pt.taskName
}
type removeFileTask struct {
file string
name string
}
func NewRemoveFileTask(file string) task {
return &removeFileTask{file: file, name: fmt.Sprintf("removing file %q", file)}
}
func (rft *removeFileTask) Run(id int, output io.Writer, doneChan chan taskResult) {
err := os.Remove(rft.file)
doneChan <- taskResult{
Id: id,
Err: err,
}
}
func (rft *removeFileTask) Name() string { return rft.name }
func (rft *removeFileTask) WithName(name string) { rft.name = name }
type createFileTask struct {
file string
content string
name string
}
func NewCreateFileTask(file string, content string) task {
return &createFileTask{
file: file,
content: content,
name: fmt.Sprintf("writing file %q", file),
}
}
func (cft *createFileTask) Run(id int, output io.Writer, doneChan chan taskResult) {
file, err := os.Create(cft.file)
if err != nil {
doneChan <- taskResult{
Id: id,
Err: err,
}
return
}
_, err = file.WriteString(cft.content)
doneChan <- taskResult{
Id: id,
Err: err,
}
return
}
func (cft *createFileTask) Name() string { return cft.name }
func (cft *createFileTask) WithName(name string) { cft.name = name }
type funcTask struct {
f func(io.Writer) error
name string
}
func NewFuncTask(taskName string, f func(io.Writer) error) task {
return &funcTask{f: f, name: taskName}
}
func (rft *funcTask) Run(id int, output io.Writer, doneChan chan taskResult) {
doneChan <- taskResult{
Id: id,
Err: rft.f(output),
}
}
func (rft *funcTask) Name() string {
return rft.name
}
func (rft *funcTask) WithName(name string) {
rft.name = name
}
func build(outputWriter io.Writer, input string, output string, toPrint ToPrintFlags, backend asm.Backend) (err error) {
defer func() {
if panicErr := recover(); panicErr != nil {
err = fmt.Errorf("panic in build: %#v\n%s", panicErr, debug.Stack())
}
}()
file, err := os.Open(input)
if err != nil {
return fmt.Errorf("could not open file %q because: %v", input, err)
}
defer file.Close()
inputText, err := io.ReadAll(file)
if err != nil {
return fmt.Errorf("Could not read file %q because: %v", input, err)
}
l, err := lexer.New(string(inputText), input)
if err != nil {
return fmt.Errorf("error while creating lexer: %v", err)
}
l.WithErrorCallback(func(l token.Loc, s string, a ...any) {
fmt.Printf("%s:%d:%d: %s\n", l.File, l.Line, l.Col, fmt.Sprintf(s, a...))
})
p := parser.New(l)
p.WithErrorCallback(func(t token.Token, s string, a ...any) {
loc := t.Loc
fmt.Printf("%s:%d:%d: %s\n", loc.File, loc.Line, loc.Col, fmt.Sprintf(s, a...))
})
program := p.ParseProgram()
if p.Errors() > 0 {
return fmt.Errorf("parser encountered 1 or more errors")
}
if (toPrint & PrintAst) != 0 {
io.WriteString(outputWriter,
fmt.Sprintf("AST:\n%s\n%+#v\n", program.String(), program))
}
tprogram, err := typechecker.New().CheckProgram(program)
if err != nil {
return err
}
if (toPrint & PrintTAst) != 0 {
io.WriteString(outputWriter,
fmt.Sprintf("TAST:\n%s\n%+#v\n", tprogram.String(), tprogram))
}
ir := ttir.EmitProgram(tprogram)
if (toPrint & PrintIr) != 0 {
io.WriteString(outputWriter,
fmt.Sprintf("TTIR:\n%s\n%+#v\n", ir.String(), ir))
}
asmOutputFile, err := os.Create(output)
if err != nil {
return fmt.Errorf("failed to create/truncate asm file %q because: %v\n", output, err)
}
defer asmOutputFile.Close()
if backend == asm.Fasm {
asm := amd64.CgProgram(ir)
asmOutput := asm.Emit()
_, err = asmOutputFile.WriteString(asmOutput)
if err != nil {
return fmt.Errorf("failed to write to file %q because: %v\n", output, err)
}
} else if backend == asm.Qbe {
err := qbe.Emit(asmOutputFile, ir)
if err != nil {
return fmt.Errorf("failed to write to file %q because: %v\n", output, err)
}
}
return
}
type taskResult struct {
Id int
Err error
}
type node struct {
next []int
previous []int
task task
}
type executionState int
const (
notStarted executionState = iota
executing
finished
failed
)
func runTasks(nodes map[int]*node, rootNodes []int, l *utils.Logger) error {
done := make(map[int]executionState)
output := make(map[int]*strings.Builder)
running := []int{}
doneChan := make(chan taskResult)
errs := []error{}
startTask := func(id int) {
if done[id] != notStarted {
panic(fmt.Sprintf("tried starting task %d twice", id))
}
node := nodes[id]
l.Debugf("executing task %d %q", id, node.task.Name())
output[id] = &strings.Builder{}
go node.task.Run(id, output[id], doneChan)
running = append(running, id)
done[id] = executing
}
for id, node := range nodes {
// Initalize map
done[id] = notStarted
// because we are already going trough the whole map, we might as well
// check the relations
for _, next := range node.next {
if node.next != nil {
if _, ok := nodes[next]; !ok {
panic(fmt.Sprintf("task with id %d has a invalid next node", id))
}
}
}
for _, prev := range node.previous {
if _, ok := nodes[prev]; !ok {
panic(fmt.Sprintf("task with id %d has a invalid prev node", id))
}
}
}
l.Debugf("starting rootNodes %v", rootNodes)
for _, rootNode := range rootNodes {
startTask(rootNode)
}
allFinished := false
for !allFinished {
select {
case result := <-doneChan:
l.Debugf("task %d %q is done with err: %v", result.Id, nodes[result.Id].task.Name(), result.Err)
for i, id := range running {
if id == result.Id {
running = slices.Delete(running, i, i+1)
break
}
}
if result.Err != nil {
done[result.Id] = failed
errs = append(errs, result.Err)
break
} else {
done[result.Id] = finished
}
for _, next := range nodes[result.Id].next {
nextNode := nodes[next]
allDone := true
for _, prev := range nextNode.previous {
if done[prev] != finished {
allDone = false
break
}
}
if allDone {
startTask(next)
}
}
default:
if len(running) <= 0 {
allFinished = true
}
}
}
for id, node := range nodes {
if output[id] == nil {
l.Warnf("output of task %q is nil", nodes[id].task.Name())
} else if output[id].Len() > 0 {
l.Infof("task %q output: %s", node.task.Name(), output[id])
}
}
return errors.Join(errs...)
}