Simplified file structure

This commit is contained in:
2024-08-07 19:39:10 +02:00
parent 1b13539b22
commit 66569446b1
219 changed files with 453 additions and 457 deletions

13
src/core/AddBytes.go Normal file
View File

@ -0,0 +1,13 @@
package core
import (
"fmt"
)
// AddBytes adds a sequence of bytes and returns its address as a label.
func (f *Function) AddBytes(value []byte) string {
f.count.data++
label := fmt.Sprintf("data_%s_%d", f.UniqueName, f.count.data)
f.Assembler.SetData(label, value)
return label
}

46
src/core/Compare.go Normal file
View File

@ -0,0 +1,46 @@
package core
import (
"git.akyoto.dev/cli/q/src/ast"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/expression"
"git.akyoto.dev/cli/q/src/token"
)
// Compare evaluates a boolean expression.
func (f *Function) Compare(comparison *expression.Expression) error {
left := comparison.Children[0]
right := comparison.Children[1]
if left.IsLeaf() && left.Token.Kind == token.Identifier {
name := left.Token.Text(f.File.Bytes)
variable := f.VariableByName(name)
if variable == nil {
return errors.New(&errors.UnknownIdentifier{Name: name}, f.File, left.Token.Position)
}
defer f.UseVariable(variable)
return f.Execute(comparison.Token, variable.Register, right)
}
if ast.IsFunctionCall(left) && right.IsLeaf() {
_, err := f.CompileCall(left)
if err != nil {
return err
}
return f.ExecuteLeaf(comparison.Token, f.CPU.Output[0], right.Token)
}
tmp := f.NewRegister()
_, err := f.ExpressionToRegister(left, tmp)
if err != nil {
return err
}
defer f.FreeRegister(tmp)
return f.Execute(comparison.Token, tmp, right)
}

12
src/core/Compile.go Normal file
View File

@ -0,0 +1,12 @@
package core
// Compile turns a function into machine code.
func (f *Function) Compile() {
f.AddLabel(f.UniqueName)
f.Err = f.CompileTokens(f.Body)
f.Return()
for _, call := range f.deferred {
call()
}
}

18
src/core/CompileAST.go Normal file
View File

@ -0,0 +1,18 @@
package core
import (
"git.akyoto.dev/cli/q/src/ast"
)
// CompileAST compiles an abstract syntax tree.
func (f *Function) CompileAST(tree ast.AST) error {
for _, node := range tree {
err := f.CompileASTNode(node)
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,51 @@
package core
import (
"git.akyoto.dev/cli/q/src/ast"
)
// CompileASTNode compiles a node in the AST.
func (f *Function) CompileASTNode(node ast.Node) error {
switch node := node.(type) {
case *ast.Assert:
f.Fold(node.Condition)
return f.CompileAssert(node)
case *ast.Assign:
f.Fold(node.Expression)
return f.CompileAssign(node)
case *ast.Call:
f.Fold(node.Expression)
_, err := f.CompileCall(node.Expression)
return err
case *ast.Define:
f.Fold(node.Expression)
return f.CompileDefinition(node)
case *ast.If:
f.Fold(node.Condition)
return f.CompileIf(node)
case *ast.Loop:
return f.CompileLoop(node)
case *ast.Return:
for _, value := range node.Values {
f.Fold(value)
}
return f.CompileReturn(node)
case *ast.Switch:
for _, c := range node.Cases {
f.Fold(c.Condition)
}
return f.CompileSwitch(node)
default:
panic("unknown AST type")
}
}

29
src/core/CompileAssert.go Normal file
View File

@ -0,0 +1,29 @@
package core
import (
"fmt"
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/ast"
)
// CompileAssert compiles an assertion.
func (f *Function) CompileAssert(assert *ast.Assert) error {
f.count.assert++
success := fmt.Sprintf("%s_assert_%d_true", f.UniqueName, f.count.assert)
fail := fmt.Sprintf("%s_assert_%d_false", f.UniqueName, f.count.assert)
err := f.CompileCondition(assert.Condition, success, fail)
if err != nil {
return err
}
f.AddLabel(success)
f.Defer(func() {
f.AddLabel(fail)
f.Jump(asm.JUMP, "_crash")
})
return err
}

36
src/core/CompileAssign.go Normal file
View File

@ -0,0 +1,36 @@
package core
import (
"git.akyoto.dev/cli/q/src/ast"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/token"
)
// CompileAssign compiles an assign statement.
func (f *Function) CompileAssign(node *ast.Assign) error {
operator := node.Expression.Token
left := node.Expression.Children[0]
right := node.Expression.Children[1]
if left.IsLeaf() {
name := left.Token.Text(f.File.Bytes)
variable := f.VariableByName(name)
if variable == nil {
return errors.New(&errors.UnknownIdentifier{Name: name}, f.File, left.Token.Position)
}
defer f.UseVariable(variable)
return f.Execute(operator, variable.Register, right)
}
if left.Token.Kind == token.Array {
return f.CompileAssignArray(node)
}
if left.Token.Kind == token.Separator && right.Token.Kind == token.Div {
return f.CompileAssignDivision(node)
}
return errors.New(errors.NotImplemented, f.File, left.Token.Position)
}

View File

@ -0,0 +1,38 @@
package core
import (
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/ast"
"git.akyoto.dev/cli/q/src/errors"
)
// CompileAssignArray compiles an assign statement for array elements.
func (f *Function) CompileAssignArray(node *ast.Assign) error {
left := node.Expression.Children[0]
right := node.Expression.Children[1]
name := left.Children[0].Token.Text(f.File.Bytes)
variable := f.VariableByName(name)
if variable == nil {
return errors.New(&errors.UnknownIdentifier{Name: name}, f.File, left.Children[0].Token.Position)
}
defer f.UseVariable(variable)
index := left.Children[1]
offset, err := f.Number(index.Token)
if err != nil {
return err
}
memory := asm.Memory{
Base: variable.Register,
Offset: byte(offset),
Length: byte(1),
}
_, err = f.ExpressionToMemory(right, memory)
return err
}

View File

@ -0,0 +1,43 @@
package core
import (
"git.akyoto.dev/cli/q/src/arch/x64"
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/ast"
"git.akyoto.dev/cli/q/src/errors"
)
// CompileAssignDivision compiles an assign statement that has quotient and remainder on the left side and division on the right.
func (f *Function) CompileAssignDivision(node *ast.Assign) error {
left := node.Expression.Children[0]
right := node.Expression.Children[1]
quotient := left.Children[0]
name := quotient.Token.Text(f.File.Bytes)
quotientVariable := f.VariableByName(name)
if quotientVariable == nil {
return errors.New(&errors.UnknownIdentifier{Name: name}, f.File, quotient.Token.Position)
}
remainder := left.Children[1]
name = remainder.Token.Text(f.File.Bytes)
remainderVariable := f.VariableByName(name)
if remainderVariable == nil {
return errors.New(&errors.UnknownIdentifier{Name: name}, f.File, remainder.Token.Position)
}
dividend := right.Children[0]
_, dividendRegister, err := f.Evaluate(dividend)
if err != nil {
return err
}
divisor := right.Children[1]
err = f.Execute(right.Token, dividendRegister, divisor)
f.RegisterRegister(asm.MOVE, quotientVariable.Register, x64.RAX)
f.RegisterRegister(asm.MOVE, remainderVariable.Register, x64.RDX)
return err
}

109
src/core/CompileCall.go Normal file
View File

@ -0,0 +1,109 @@
package core
import (
"strings"
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/expression"
)
// CompileCall executes a function call.
// All call registers must hold the correct parameter values before the function invocation.
// Registers that are in use must be saved if they are modified by the function.
// After the function call, they must be restored in reverse order.
func (f *Function) CompileCall(root *expression.Expression) (*Function, error) {
var (
pkg = f.Package
nameNode = root.Children[0]
fn *Function
name string
fullName string
exists bool
)
if nameNode.IsLeaf() {
name = nameNode.Token.Text(f.File.Bytes)
if name == "syscall" {
return nil, f.CompileSyscall(root)
}
} else {
pkg = nameNode.Children[0].Token.Text(f.File.Bytes)
name = nameNode.Children[1].Token.Text(f.File.Bytes)
}
if pkg != f.File.Package {
if f.File.Imports == nil {
return nil, errors.New(&errors.UnknownPackage{Name: pkg}, f.File, nameNode.Token.Position)
}
imp, exists := f.File.Imports[pkg]
if !exists {
return nil, errors.New(&errors.UnknownPackage{Name: pkg}, f.File, nameNode.Token.Position)
}
imp.Used = true
}
tmp := strings.Builder{}
tmp.WriteString(pkg)
tmp.WriteString(".")
tmp.WriteString(name)
fullName = tmp.String()
fn, exists = f.Functions[fullName]
if !exists {
return nil, errors.New(&errors.UnknownFunction{Name: name}, f.File, nameNode.Token.Position)
}
parameters := root.Children[1:]
registers := f.CPU.Input[:len(parameters)]
for i := len(parameters) - 1; i >= 0; i-- {
typ, err := f.ExpressionToRegister(parameters[i], registers[i])
if err != nil {
return nil, err
}
if typ != fn.Parameters[i].Type {
return nil, errors.New(&errors.TypeMismatch{
Encountered: string(typ),
Expected: string(fn.Parameters[i].Type),
ParameterName: fn.Parameters[i].Name,
}, f.File, parameters[i].Token.Position)
}
}
for _, register := range f.CPU.Output[:len(fn.ReturnTypes)] {
f.SaveRegister(register)
}
for _, register := range f.CPU.General {
if f.RegisterIsUsed(register) {
f.Register(asm.PUSH, register)
}
}
f.Call(fullName)
for _, register := range registers {
if register == f.CPU.Output[0] && root.Parent != nil {
continue
}
f.FreeRegister(register)
}
for i := len(f.CPU.General) - 1; i >= 0; i-- {
register := f.CPU.General[i]
if f.RegisterIsUsed(register) {
f.Register(asm.POP, register)
}
}
return fn, nil
}

View File

@ -0,0 +1,76 @@
package core
import (
"fmt"
"git.akyoto.dev/cli/q/src/expression"
"git.akyoto.dev/cli/q/src/token"
)
// CompileCondition inserts code to jump to the start label or end label depending on the truth of the condition.
func (f *Function) CompileCondition(condition *expression.Expression, successLabel string, failLabel string) error {
switch condition.Token.Kind {
case token.LogicalOr:
f.count.subBranch++
leftFailLabel := fmt.Sprintf("%s_false_%d", f.UniqueName, f.count.subBranch)
// Left
left := condition.Children[0]
err := f.CompileCondition(left, successLabel, leftFailLabel)
if err != nil {
return err
}
f.JumpIfTrue(left.Token.Kind, successLabel)
// Right
f.AddLabel(leftFailLabel)
right := condition.Children[1]
err = f.CompileCondition(right, successLabel, failLabel)
if condition.Parent != nil && condition.Parent.Token.Kind == token.LogicalOr && condition != condition.Parent.LastChild() {
f.JumpIfTrue(right.Token.Kind, successLabel)
} else {
f.JumpIfFalse(right.Token.Kind, failLabel)
}
return err
case token.LogicalAnd:
f.count.subBranch++
leftSuccessLabel := fmt.Sprintf("%s_true_%d", f.UniqueName, f.count.subBranch)
// Left
left := condition.Children[0]
err := f.CompileCondition(left, leftSuccessLabel, failLabel)
if err != nil {
return err
}
f.JumpIfFalse(left.Token.Kind, failLabel)
// Right
f.AddLabel(leftSuccessLabel)
right := condition.Children[1]
err = f.CompileCondition(right, successLabel, failLabel)
if condition.Parent != nil && condition.Parent.Token.Kind == token.LogicalOr && condition != condition.Parent.LastChild() {
f.JumpIfTrue(right.Token.Kind, successLabel)
} else {
f.JumpIfFalse(right.Token.Kind, failLabel)
}
return err
default:
err := f.Compare(condition)
if condition.Parent == nil {
f.JumpIfFalse(condition.Token.Kind, failLabel)
}
return err
}
}

View File

@ -0,0 +1,66 @@
package core
import (
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/ast"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/expression"
"git.akyoto.dev/cli/q/src/types"
)
// CompileDefinition compiles a variable definition.
func (f *Function) CompileDefinition(node *ast.Define) error {
left := node.Expression.Children[0]
right := node.Expression.Children[1]
if left.IsLeaf() {
variable, err := f.Define(left)
if err != nil {
return err
}
typ, err := f.ExpressionToRegister(right, variable.Register)
if err != nil {
return err
}
variable.Type = typ
if variable.Type == types.Invalid {
return errors.New(errors.UnknownType, f.File, node.Expression.Token.End())
}
f.AddVariable(variable)
return nil
}
if !ast.IsFunctionCall(right) {
return errors.New(errors.NotImplemented, f.File, node.Expression.Token.Position)
}
count := 0
called, err := f.CompileCall(right)
if err != nil {
return err
}
return left.EachLeaf(func(leaf *expression.Expression) error {
variable, err := f.Define(leaf)
if err != nil {
return err
}
if called != nil {
variable.Type = called.ReturnTypes[count]
}
f.RegisterRegister(asm.MOVE, variable.Register, f.CPU.Output[count])
f.AddVariable(variable)
count++
return nil
})
}

55
src/core/CompileIf.go Normal file
View File

@ -0,0 +1,55 @@
package core
import (
"fmt"
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/ast"
)
// CompileIf compiles a branch instruction.
func (f *Function) CompileIf(branch *ast.If) error {
f.count.branch++
var (
end string
success = fmt.Sprintf("%s_if_%d_true", f.UniqueName, f.count.branch)
fail = fmt.Sprintf("%s_if_%d_false", f.UniqueName, f.count.branch)
err = f.CompileCondition(branch.Condition, success, fail)
)
if err != nil {
return err
}
f.AddLabel(success)
f.PushScope(branch.Body, f.File.Bytes)
err = f.CompileAST(branch.Body)
if err != nil {
return err
}
if branch.Else != nil {
end = fmt.Sprintf("%s_if_%d_end", f.UniqueName, f.count.branch)
f.Jump(asm.JUMP, end)
}
f.PopScope()
f.AddLabel(fail)
if branch.Else != nil {
f.PushScope(branch.Else, f.File.Bytes)
err = f.CompileAST(branch.Else)
if err != nil {
return err
}
f.PopScope()
f.AddLabel(end)
return nil
}
return nil
}

25
src/core/CompileLoop.go Normal file
View File

@ -0,0 +1,25 @@
package core
import (
"fmt"
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/ast"
)
// CompileLoop compiles a loop instruction.
func (f *Function) CompileLoop(loop *ast.Loop) error {
for _, register := range f.CPU.Input {
f.SaveRegister(register)
}
f.count.loop++
label := fmt.Sprintf("%s_loop_%d", f.UniqueName, f.count.loop)
f.AddLabel(label)
scope := f.PushScope(loop.Body, f.File.Bytes)
scope.InLoop = true
err := f.CompileAST(loop.Body)
f.Jump(asm.JUMP, label)
f.PopScope()
return err
}

35
src/core/CompileReturn.go Normal file
View File

@ -0,0 +1,35 @@
package core
import (
"git.akyoto.dev/cli/q/src/ast"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/types"
)
// CompileReturn compiles a return instruction.
func (f *Function) CompileReturn(node *ast.Return) error {
defer f.Return()
if len(node.Values) == 0 {
return nil
}
for i := len(node.Values) - 1; i >= 0; i-- {
typ, err := f.ExpressionToRegister(node.Values[i], f.CPU.Output[i])
if err != nil {
return err
}
if typ != types.Any && typ != f.ReturnTypes[i] {
return errors.New(&errors.TypeMismatch{
Encountered: string(typ),
Expected: string(f.ReturnTypes[i]),
ParameterName: "",
IsReturn: true,
}, f.File, node.Values[i].Token.Position)
}
}
return nil
}

55
src/core/CompileSwitch.go Normal file
View File

@ -0,0 +1,55 @@
package core
import (
"fmt"
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/ast"
)
// CompileSwitch compiles a multi-branch instruction.
func (f *Function) CompileSwitch(s *ast.Switch) error {
f.count.multiBranch++
end := fmt.Sprintf("%s_switch_%d_end", f.UniqueName, f.count.multiBranch)
for _, branch := range s.Cases {
if branch.Condition == nil {
f.PushScope(branch.Body, f.File.Bytes)
err := f.CompileAST(branch.Body)
if err != nil {
return err
}
f.PopScope()
break
}
f.count.branch++
var (
success = fmt.Sprintf("%s_case_%d_true", f.UniqueName, f.count.branch)
fail = fmt.Sprintf("%s_case_%d_false", f.UniqueName, f.count.branch)
err = f.CompileCondition(branch.Condition, success, fail)
)
if err != nil {
return err
}
f.AddLabel(success)
f.PushScope(branch.Body, f.File.Bytes)
err = f.CompileAST(branch.Body)
if err != nil {
return err
}
f.Jump(asm.JUMP, end)
f.PopScope()
f.AddLabel(fail)
}
f.AddLabel(end)
return nil
}

View File

@ -0,0 +1,32 @@
package core
import (
"git.akyoto.dev/cli/q/src/expression"
)
// CompileSyscall executes a kernel syscall.
func (f *Function) CompileSyscall(root *expression.Expression) error {
parameters := root.Children[1:]
registers := f.CPU.SyscallInput[:len(parameters)]
err := f.ExpressionsToRegisters(parameters, registers)
if err != nil {
return err
}
for _, register := range f.CPU.SyscallOutput {
f.SaveRegister(register)
}
f.Syscall()
for _, register := range registers {
if register == f.CPU.SyscallOutput[0] && root.Parent != nil {
continue
}
f.FreeRegister(register)
}
return nil
}

19
src/core/CompileTokens.go Normal file
View File

@ -0,0 +1,19 @@
package core
import (
"git.akyoto.dev/cli/q/src/ast"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/token"
)
// CompileTokens compiles a token list.
func (f *Function) CompileTokens(tokens []token.Token) error {
body, err := ast.Parse(tokens, f.File.Bytes)
if err != nil {
err.(*errors.Error).File = f.File
return err
}
return f.CompileAST(body)
}

6
src/core/Defer.go Normal file
View File

@ -0,0 +1,6 @@
package core
// Defer executes the callback at the end of function compilation.
func (f *Function) Defer(call func()) {
f.deferred = append(f.deferred, call)
}

31
src/core/Define.go Normal file
View File

@ -0,0 +1,31 @@
package core
import (
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/expression"
"git.akyoto.dev/cli/q/src/scope"
"git.akyoto.dev/cli/q/src/token"
)
// Define defines a new variable.
func (f *Function) Define(leaf *expression.Expression) (*scope.Variable, error) {
name := leaf.Token.Text(f.File.Bytes)
if f.IdentifierExists(name) {
return nil, errors.New(&errors.VariableAlreadyExists{Name: name}, f.File, leaf.Token.Position)
}
uses := token.Count(f.Body, f.File.Bytes, token.Identifier, name) - 1
if uses == 0 {
return nil, errors.New(&errors.UnusedVariable{Name: name}, f.File, leaf.Token.Position)
}
variable := &scope.Variable{
Name: name,
Register: f.NewRegister(),
Alive: uses,
}
return variable, nil
}

25
src/core/Evaluate.go Normal file
View File

@ -0,0 +1,25 @@
package core
import (
"git.akyoto.dev/cli/q/src/cpu"
"git.akyoto.dev/cli/q/src/expression"
"git.akyoto.dev/cli/q/src/token"
"git.akyoto.dev/cli/q/src/types"
)
// Evaluate evaluates an expression and returns a register that contains the value of the expression.
func (f *Function) Evaluate(expr *expression.Expression) (types.Type, cpu.Register, error) {
if expr.Token.Kind == token.Identifier {
name := expr.Token.Text(f.File.Bytes)
variable := f.VariableByName(name)
if variable.Alive == 1 {
f.UseVariable(variable)
return variable.Type, variable.Register, nil
}
}
tmp := f.NewRegister()
typ, err := f.ExpressionToRegister(expr, tmp)
return typ, tmp, err
}

36
src/core/Execute.go Normal file
View File

@ -0,0 +1,36 @@
package core
import (
"git.akyoto.dev/cli/q/src/ast"
"git.akyoto.dev/cli/q/src/cpu"
"git.akyoto.dev/cli/q/src/expression"
"git.akyoto.dev/cli/q/src/token"
)
// Execute executes an operation on a register with a value operand.
func (f *Function) Execute(operation token.Token, register cpu.Register, value *expression.Expression) error {
if value.IsLeaf() {
return f.ExecuteLeaf(operation, register, value.Token)
}
if ast.IsFunctionCall(value) {
_, err := f.CompileCall(value)
if err != nil {
return err
}
return f.ExecuteRegisterRegister(operation, register, f.CPU.Output[0])
}
tmp := f.NewRegister()
defer f.FreeRegister(tmp)
_, err := f.ExpressionToRegister(value, tmp)
if err != nil {
return err
}
return f.ExecuteRegisterRegister(operation, register, tmp)
}

40
src/core/ExecuteLeaf.go Normal file
View File

@ -0,0 +1,40 @@
package core
import (
"git.akyoto.dev/cli/q/src/cpu"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/token"
)
// ExecuteLeaf performs an operation on a register with the given leaf operand.
func (f *Function) ExecuteLeaf(operation token.Token, register cpu.Register, operand token.Token) error {
switch operand.Kind {
case token.Identifier:
name := operand.Text(f.File.Bytes)
variable := f.VariableByName(name)
if variable == nil {
return errors.New(&errors.UnknownIdentifier{Name: name}, f.File, operand.Position)
}
defer f.UseVariable(variable)
return f.ExecuteRegisterRegister(operation, register, variable.Register)
case token.Number, token.Rune:
number, err := f.Number(operand)
if err != nil {
return err
}
return f.ExecuteRegisterNumber(operation, register, number)
case token.String:
if operation.Kind == token.Assign {
_, err := f.TokenToRegister(operand, register)
return err
}
}
return errors.New(errors.NotImplemented, f.File, operation.Position)
}

View File

@ -0,0 +1,21 @@
package core
import (
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/cpu"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/token"
)
// ExecuteRegister performs an operation on a single register.
func (f *Function) ExecuteRegister(operation token.Token, register cpu.Register) error {
switch operation.Kind {
case token.Negate:
f.Register(asm.NEGATE, register)
default:
return errors.New(&errors.InvalidOperator{Operator: operation.Text(f.File.Bytes)}, f.File, operation.Position)
}
return nil
}

View File

@ -0,0 +1,69 @@
package core
import (
"git.akyoto.dev/cli/q/src/arch/x64"
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/cpu"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/token"
)
// ExecuteRegisterNumber performs an operation on a register and a number.
func (f *Function) ExecuteRegisterNumber(operation token.Token, register cpu.Register, number int) error {
if !operation.IsAssignment() && !operation.IsComparison() {
f.SaveRegister(register)
}
switch operation.Kind {
case token.Add, token.AddAssign:
f.RegisterNumber(asm.ADD, register, number)
case token.Sub, token.SubAssign:
f.RegisterNumber(asm.SUB, register, number)
case token.Mul, token.MulAssign:
f.RegisterNumber(asm.MUL, register, number)
case token.Div, token.DivAssign:
f.SaveRegister(x64.RAX)
f.SaveRegister(x64.RDX)
tmp := f.NewRegister()
f.RegisterNumber(asm.MOVE, tmp, number)
f.RegisterRegister(asm.DIV, register, tmp)
f.FreeRegister(tmp)
case token.Mod, token.ModAssign:
f.SaveRegister(x64.RAX)
f.SaveRegister(x64.RDX)
tmp := f.NewRegister()
f.RegisterNumber(asm.MOVE, tmp, number)
f.RegisterRegister(asm.MODULO, register, tmp)
f.FreeRegister(tmp)
case token.And, token.AndAssign:
f.RegisterNumber(asm.AND, register, number)
case token.Or, token.OrAssign:
f.RegisterNumber(asm.OR, register, number)
case token.Xor, token.XorAssign:
f.RegisterNumber(asm.XOR, register, number)
case token.Shl, token.ShlAssign:
f.RegisterNumber(asm.SHIFTL, register, number)
case token.Shr, token.ShrAssign:
f.RegisterNumber(asm.SHIFTRS, register, number)
case token.Assign:
f.RegisterNumber(asm.MOVE, register, number)
case token.Equal, token.NotEqual, token.Less, token.LessEqual, token.Greater, token.GreaterEqual:
f.RegisterNumber(asm.COMPARE, register, number)
default:
return errors.New(&errors.InvalidOperator{Operator: operation.Text(f.File.Bytes)}, f.File, operation.Position)
}
return nil
}

View File

@ -0,0 +1,57 @@
package core
import (
"git.akyoto.dev/cli/q/src/arch/x64"
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/cpu"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/token"
)
// ExecuteRegisterRegister performs an operation on two registers.
func (f *Function) ExecuteRegisterRegister(operation token.Token, register cpu.Register, operand cpu.Register) error {
if !operation.IsAssignment() && !operation.IsComparison() {
f.SaveRegister(register)
}
switch operation.Kind {
case token.Add, token.AddAssign:
f.RegisterRegister(asm.ADD, register, operand)
case token.Sub, token.SubAssign:
f.RegisterRegister(asm.SUB, register, operand)
case token.Mul, token.MulAssign:
f.RegisterRegister(asm.MUL, register, operand)
case token.Div, token.DivAssign:
f.SaveRegister(x64.RAX)
f.SaveRegister(x64.RDX)
f.RegisterRegister(asm.DIV, register, operand)
case token.Mod, token.ModAssign:
f.SaveRegister(x64.RAX)
f.SaveRegister(x64.RDX)
f.RegisterRegister(asm.MODULO, register, operand)
case token.And, token.AndAssign:
f.RegisterRegister(asm.AND, register, operand)
case token.Or, token.OrAssign:
f.RegisterRegister(asm.OR, register, operand)
case token.Xor, token.XorAssign:
f.RegisterRegister(asm.XOR, register, operand)
case token.Assign:
f.RegisterRegister(asm.MOVE, register, operand)
case token.Equal, token.NotEqual, token.Less, token.LessEqual, token.Greater, token.GreaterEqual:
f.RegisterRegister(asm.COMPARE, register, operand)
default:
return errors.New(&errors.InvalidOperator{Operator: operation.Text(f.File.Bytes)}, f.File, operation.Position)
}
return nil
}

View File

@ -0,0 +1,35 @@
package core
import (
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/expression"
"git.akyoto.dev/cli/q/src/sizeof"
"git.akyoto.dev/cli/q/src/types"
)
// ExpressionToMemory puts the result of an expression into the specified memory address.
func (f *Function) ExpressionToMemory(node *expression.Expression, memory asm.Memory) (types.Type, error) {
if node.IsLeaf() && node.Token.IsNumeric() {
number, err := f.Number(node.Token)
if err != nil {
return types.Invalid, err
}
size := byte(sizeof.Signed(int64(number)))
if size != memory.Length {
return types.Invalid, errors.New(&errors.NumberExceedsBounds{Number: number, ExpectedSize: memory.Length, Size: size}, f.File, node.Token.Position)
}
f.MemoryNumber(asm.STORE, memory, number)
return types.Int, nil
}
tmp := f.NewRegister()
defer f.FreeRegister(tmp)
typ, err := f.ExpressionToRegister(node, tmp)
f.MemoryRegister(asm.STORE, memory, tmp)
return typ, err
}

View File

@ -0,0 +1,87 @@
package core
import (
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/ast"
"git.akyoto.dev/cli/q/src/cpu"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/expression"
"git.akyoto.dev/cli/q/src/token"
"git.akyoto.dev/cli/q/src/types"
)
// ExpressionToRegister puts the result of an expression into the specified register.
func (f *Function) ExpressionToRegister(node *expression.Expression, register cpu.Register) (types.Type, error) {
f.SaveRegister(register)
if node.IsFolded {
f.RegisterNumber(asm.MOVE, register, node.Value)
return types.Int, nil
}
if node.IsLeaf() {
return f.TokenToRegister(node.Token, register)
}
if ast.IsFunctionCall(node) {
fn, err := f.CompileCall(node)
if register != f.CPU.Output[0] {
f.RegisterRegister(asm.MOVE, register, f.CPU.Output[0])
}
if fn == nil || len(fn.ReturnTypes) == 0 {
return types.Any, err
}
return fn.ReturnTypes[0], err
}
if node.Token.Kind == token.Array {
array := f.VariableByName(node.Children[0].Token.Text(f.File.Bytes))
offset, err := f.Number(node.Children[1].Token)
f.MemoryRegister(asm.LOAD, asm.Memory{Base: array.Register, Offset: byte(offset), Length: 1}, register)
return types.Int, err
}
if len(node.Children) == 1 {
if !node.Token.IsUnaryOperator() {
return types.Invalid, errors.New(errors.MissingOperand, f.File, node.Token.End())
}
typ, err := f.ExpressionToRegister(node.Children[0], register)
if err != nil {
return typ, err
}
return typ, f.ExecuteRegister(node.Token, register)
}
left := node.Children[0]
right := node.Children[1]
final := register
if f.UsesRegister(right, register) {
register = f.NewRegister()
}
typ, err := f.ExpressionToRegister(left, register)
if err != nil {
return types.Invalid, err
}
if typ == types.Pointer && (node.Token.Kind == token.Add || node.Token.Kind == token.Sub) && right.Token.Kind == token.Identifier && f.VariableByName(right.Token.Text(f.File.Bytes)).Type == types.Pointer {
typ = types.Int
}
err = f.Execute(node.Token, register, right)
if register != final {
f.RegisterRegister(asm.MOVE, final, register)
f.FreeRegister(register)
}
return typ, err
}

View File

@ -0,0 +1,19 @@
package core
import (
"git.akyoto.dev/cli/q/src/cpu"
"git.akyoto.dev/cli/q/src/expression"
)
// ExpressionsToRegisters moves multiple expressions into the specified registers.
func (f *Function) ExpressionsToRegisters(expressions []*expression.Expression, registers []cpu.Register) error {
for i := len(expressions) - 1; i >= 0; i-- {
_, err := f.ExpressionToRegister(expressions[i], registers[i])
if err != nil {
return err
}
}
return nil
}

74
src/core/Fold.go Normal file
View File

@ -0,0 +1,74 @@
package core
import (
"git.akyoto.dev/cli/q/src/config"
"git.akyoto.dev/cli/q/src/expression"
"git.akyoto.dev/cli/q/src/token"
)
// Fold will try to precalculate the results of operations with constants.
func (f *Function) Fold(expr *expression.Expression) error {
if !config.ConstantFold {
return nil
}
if expr == nil {
return nil
}
if expr.IsLeaf() {
if expr.Token.IsNumeric() {
value, err := f.Number(expr.Token)
expr.Value = value
expr.IsFolded = true
return err
}
return nil
}
canFold := true
for _, child := range expr.Children {
f.Fold(child)
if !child.IsFolded {
canFold = false
}
}
if !canFold || len(expr.Children) != 2 {
return nil
}
a := expr.Children[0].Value
b := expr.Children[1].Value
switch expr.Token.Kind {
case token.Or:
expr.Value = a | b
case token.And:
expr.Value = a & b
case token.Xor:
expr.Value = a ^ b
case token.Add:
expr.Value = a + b
case token.Sub:
expr.Value = a - b
case token.Mul:
expr.Value = a * b
case token.Div:
expr.Value = a / b
case token.Mod:
expr.Value = a % b
case token.Shl:
expr.Value = a << b
case token.Shr:
expr.Value = a >> b
default:
return nil
}
expr.IsFolded = true
return nil
}

35
src/core/Function.go Normal file
View File

@ -0,0 +1,35 @@
package core
import (
"git.akyoto.dev/cli/q/src/fs"
"git.akyoto.dev/cli/q/src/register"
"git.akyoto.dev/cli/q/src/scope"
"git.akyoto.dev/cli/q/src/token"
"git.akyoto.dev/cli/q/src/types"
)
// Function represents the smallest unit of code.
type Function struct {
register.Machine
Package string
Name string
UniqueName string
File *fs.File
Body token.List
Parameters []*scope.Variable
ReturnTypes []types.Type
Functions map[string]*Function
Err error
deferred []func()
count counter
}
// counter stores how often a certain statement appeared so we can generate a unique label from it.
type counter struct {
assert int
branch int
multiBranch int
data int
loop int
subBranch int
}

View File

@ -0,0 +1,13 @@
package core
// IdentifierExists returns true if the identifier has been defined.
func (f *Function) IdentifierExists(name string) bool {
variable := f.VariableByName(name)
if variable != nil {
return true
}
_, exists := f.Functions[name]
return exists
}

24
src/core/JumpIfFalse.go Normal file
View File

@ -0,0 +1,24 @@
package core
import (
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/token"
)
// JumpIfFalse jumps to the label if the previous comparison was false.
func (f *Function) JumpIfFalse(operator token.Kind, label string) {
switch operator {
case token.Equal:
f.Jump(asm.JNE, label)
case token.NotEqual:
f.Jump(asm.JE, label)
case token.Greater:
f.Jump(asm.JLE, label)
case token.Less:
f.Jump(asm.JGE, label)
case token.GreaterEqual:
f.Jump(asm.JL, label)
case token.LessEqual:
f.Jump(asm.JG, label)
}
}

24
src/core/JumpIfTrue.go Normal file
View File

@ -0,0 +1,24 @@
package core
import (
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/token"
)
// JumpIfTrue jumps to the label if the previous comparison was true.
func (f *Function) JumpIfTrue(operator token.Kind, label string) {
switch operator {
case token.Equal:
f.Jump(asm.JE, label)
case token.NotEqual:
f.Jump(asm.JNE, label)
case token.Greater:
f.Jump(asm.JG, label)
case token.Less:
f.Jump(asm.JL, label)
case token.GreaterEqual:
f.Jump(asm.JGE, label)
case token.LessEqual:
f.Jump(asm.JLE, label)
}
}

38
src/core/NewFunction.go Normal file
View File

@ -0,0 +1,38 @@
package core
import (
"git.akyoto.dev/cli/q/src/arch/x64"
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/cpu"
"git.akyoto.dev/cli/q/src/fs"
"git.akyoto.dev/cli/q/src/register"
"git.akyoto.dev/cli/q/src/scope"
"git.akyoto.dev/cli/q/src/token"
)
// NewFunction creates a new function.
func NewFunction(pkg string, name string, file *fs.File, body []token.Token) *Function {
return &Function{
Package: pkg,
Name: name,
UniqueName: pkg + "." + name,
File: file,
Body: body,
Machine: register.Machine{
Assembler: asm.Assembler{
Instructions: make([]asm.Instruction, 0, 8),
},
Stack: scope.Stack{
Scopes: []*scope.Scope{{}},
},
CPU: cpu.CPU{
All: x64.AllRegisters,
General: x64.GeneralRegisters,
Input: x64.InputRegisters,
Output: x64.OutputRegisters,
SyscallInput: x64.SyscallInputRegisters,
SyscallOutput: x64.SyscallOutputRegisters,
},
},
}
}

54
src/core/Number.go Normal file
View File

@ -0,0 +1,54 @@
package core
import (
"strconv"
"strings"
"unicode/utf8"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/token"
)
// Number tries to convert the token into a numeric value.
func (f *Function) Number(t token.Token) (int, error) {
switch t.Kind {
case token.Number:
digits := t.Text(f.File.Bytes)
if strings.HasPrefix(digits, "0x") {
number, err := strconv.ParseInt(digits[2:], 16, 64)
return int(number), err
}
if strings.HasPrefix(digits, "0o") {
number, err := strconv.ParseInt(digits[2:], 8, 64)
return int(number), err
}
if strings.HasPrefix(digits, "0b") {
number, err := strconv.ParseInt(digits[2:], 2, 64)
return int(number), err
}
number, err := strconv.Atoi(digits)
return number, err
case token.Rune:
r := t.Bytes(f.File.Bytes)
r = String(r)
if len(r) == 0 {
return 0, errors.New(errors.InvalidRune, f.File, t.Position+1)
}
number, size := utf8.DecodeRune(r)
if len(r) > size {
return 0, errors.New(errors.InvalidRune, f.File, t.Position+1)
}
return int(number), nil
}
return 0, errors.New(errors.InvalidNumber, f.File, t.Position)
}

View File

@ -0,0 +1,51 @@
package core
import (
"bytes"
"fmt"
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/go/color/ansi"
)
// PrintInstructions shows the assembly instructions.
func (f *Function) PrintInstructions() {
ansi.Dim.Println("╭──────────────────────────────────────────────────────────────────────────────╮")
for i, x := range f.Assembler.Instructions {
ansi.Dim.Print("│ ")
switch x.Mnemonic {
case asm.LABEL:
ansi.Yellow.Printf("%-44s", x.Data.String()+":")
case asm.COMMENT:
ansi.Dim.Printf("%-44s", x.Data.String())
default:
ansi.Green.Printf("%-12s", x.Mnemonic.String())
if x.Data != nil {
fmt.Printf("%-32s", x.Data.String())
} else {
fmt.Printf("%-32s", "")
}
}
registers := bytes.Buffer{}
used := f.RegisterHistory[i]
for _, reg := range f.CPU.All {
if used&(1<<reg) != 0 {
registers.WriteString("⬤ ")
} else {
registers.WriteString("◯ ")
}
}
ansi.Dim.Print(registers.String())
ansi.Dim.Print(" │\n")
}
ansi.Dim.Println("╰──────────────────────────────────────────────────────────────────────────────╯")
}

43
src/core/String.go Normal file
View File

@ -0,0 +1,43 @@
package core
import "bytes"
// String replaces the escape sequences in the contents of a string token with the respective characters.
func String(data []byte) []byte {
data = data[1 : len(data)-1]
escape := bytes.IndexByte(data, '\\')
if escape == -1 {
return data
}
tmp := make([]byte, 0, len(data))
for {
tmp = append(tmp, data[:escape]...)
switch data[escape+1] {
case '0':
tmp = append(tmp, '\000')
case 't':
tmp = append(tmp, '\t')
case 'n':
tmp = append(tmp, '\n')
case 'r':
tmp = append(tmp, '\r')
case '"':
tmp = append(tmp, '"')
case '\'':
tmp = append(tmp, '\'')
case '\\':
tmp = append(tmp, '\\')
}
data = data[escape+2:]
escape = bytes.IndexByte(data, '\\')
if escape == -1 {
return tmp
}
}
}

View File

@ -0,0 +1,50 @@
package core
import (
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/cpu"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/token"
"git.akyoto.dev/cli/q/src/types"
)
// TokenToRegister moves a token into a register.
// It only works with identifiers, numbers and strings.
func (f *Function) TokenToRegister(t token.Token, register cpu.Register) (types.Type, error) {
switch t.Kind {
case token.Identifier:
name := t.Text(f.File.Bytes)
variable := f.VariableByName(name)
if variable == nil {
return types.Invalid, errors.New(&errors.UnknownIdentifier{Name: name}, f.File, t.Position)
}
f.UseVariable(variable)
f.SaveRegister(register)
f.RegisterRegister(asm.MOVE, register, variable.Register)
return variable.Type, nil
case token.Number, token.Rune:
number, err := f.Number(t)
if err != nil {
return types.Invalid, err
}
f.SaveRegister(register)
f.RegisterNumber(asm.MOVE, register, number)
return types.Int, nil
case token.String:
data := t.Bytes(f.File.Bytes)
data = String(data)
label := f.AddBytes(data)
f.SaveRegister(register)
f.RegisterLabel(asm.MOVE, register, label)
return types.Pointer, nil
default:
return types.Invalid, errors.New(errors.InvalidExpression, f.File, t.Position)
}
}

40
src/core/UsesRegister.go Normal file
View File

@ -0,0 +1,40 @@
package core
import (
"git.akyoto.dev/cli/q/src/ast"
"git.akyoto.dev/cli/q/src/cpu"
"git.akyoto.dev/cli/q/src/expression"
)
// UsesRegister returns true if evaluating the expression would write or read the given register.
func (f *Function) UsesRegister(expr *expression.Expression, register cpu.Register) bool {
if expr.IsLeaf() {
return false
}
if ast.IsFunctionCall(expr) {
if register == f.CPU.Output[0] {
return true
}
for i, parameter := range expr.Children[1:] {
if register == f.CPU.Input[i] {
return true
}
if f.UsesRegister(parameter, register) {
return true
}
}
return false
}
for _, child := range expr.Children {
if f.UsesRegister(child, register) {
return true
}
}
return false
}