Improved SSA and added unused value checks
All checks were successful
/ test (push) Successful in 17s

This commit is contained in:
Eduard Urbach 2025-06-23 23:11:05 +02:00
parent cc2e98ca49
commit 2b703e9af2
Signed by: akyoto
GPG key ID: 49226B848C78F6C8
25 changed files with 519 additions and 213 deletions

16
src/core/CheckDeadCode.go Normal file
View file

@ -0,0 +1,16 @@
package core
import (
"git.urbach.dev/cli/q/src/errors"
)
// CheckDeadCode checks for dead values.
func (f *Function) CheckDeadCode() error {
for instr := range f.Values {
if instr.IsConst() && instr.Alive() == 0 {
return errors.New(&UnusedValue{Value: instr.String()}, f.File, instr.Token().Position)
}
}
return nil
}

View file

@ -26,4 +26,6 @@ func (f *Function) Compile() {
return return
} }
} }
f.Err = f.CheckDeadCode()
} }

View file

@ -15,9 +15,10 @@ func (f *Function) CompileReturn(tokens token.List) error {
return err return err
} }
f.Append(ssa.Value{ f.Append(&ssa.Return{
Type: ssa.Return, Arguments: ssa.Arguments{
Args: []*ssa.Value{value}, Args: []ssa.Value{value},
},
}) })
return nil return nil

View file

@ -10,7 +10,7 @@ import (
) )
// Evaluate converts an expression to an SSA value. // Evaluate converts an expression to an SSA value.
func (f *Function) Evaluate(expr *expression.Expression) (*ssa.Value, error) { func (f *Function) Evaluate(expr *expression.Expression) (ssa.Value, error) {
if expr.IsLeaf() { if expr.IsLeaf() {
switch expr.Token.Kind { switch expr.Token.Kind {
case token.Identifier: case token.Identifier:
@ -30,12 +30,16 @@ func (f *Function) Evaluate(expr *expression.Expression) (*ssa.Value, error) {
return nil, err return nil, err
} }
return f.AppendInt(number), nil v := f.AppendInt(number)
v.Source = expr.Token
return v, nil
case token.String: case token.String:
data := expr.Token.Bytes(f.File.Bytes) data := expr.Token.Bytes(f.File.Bytes)
data = Unescape(data) data = Unescape(data)
return f.AppendBytes(data), nil v := f.AppendBytes(data)
v.Source = expr.Token
return v, nil
} }
return nil, errors.New(InvalidExpression, f.File, expr.Token.Position) return nil, errors.New(InvalidExpression, f.File, expr.Token.Position)
@ -44,7 +48,7 @@ func (f *Function) Evaluate(expr *expression.Expression) (*ssa.Value, error) {
switch expr.Token.Kind { switch expr.Token.Kind {
case token.Call: case token.Call:
children := expr.Children children := expr.Children
typ := ssa.Call isSyscall := false
if children[0].Token.Kind == token.Identifier { if children[0].Token.Kind == token.Identifier {
funcName := children[0].String(f.File.Bytes) funcName := children[0].String(f.File.Bytes)
@ -56,11 +60,11 @@ func (f *Function) Evaluate(expr *expression.Expression) (*ssa.Value, error) {
if funcName == "syscall" { if funcName == "syscall" {
children = children[1:] children = children[1:]
typ = ssa.Syscall isSyscall = true
} }
} }
args := make([]*ssa.Value, len(children)) args := make([]ssa.Value, len(children))
for i, child := range children { for i, child := range children {
value, err := f.Evaluate(child) value, err := f.Evaluate(child)
@ -72,12 +76,27 @@ func (f *Function) Evaluate(expr *expression.Expression) (*ssa.Value, error) {
args[i] = value args[i] = value
} }
call := f.Append(ssa.Value{Type: typ, Args: args}) if isSyscall {
return call, nil v := f.Append(&ssa.Syscall{
Arguments: ssa.Arguments{Args: args},
HasToken: ssa.HasToken{Source: expr.Token},
})
return v, nil
} else {
v := f.Append(&ssa.Call{
Arguments: ssa.Arguments{Args: args},
HasToken: ssa.HasToken{Source: expr.Token},
})
return v, nil
}
case token.Dot: case token.Dot:
name := fmt.Sprintf("%s.%s", expr.Children[0].String(f.File.Bytes), expr.Children[1].String(f.File.Bytes)) name := fmt.Sprintf("%s.%s", expr.Children[0].String(f.File.Bytes), expr.Children[1].String(f.File.Bytes))
return f.AppendFunction(name), nil v := f.AppendFunction(name)
v.Source = expr.Children[1].Token
return v, nil
} }
return nil, nil return nil, nil

View file

@ -10,14 +10,14 @@ import (
// Function is the smallest unit of code. // Function is the smallest unit of code.
type Function struct { type Function struct {
ssa.Function ssa.IR
Name string Name string
UniqueName string UniqueName string
File *fs.File File *fs.File
Input []*Parameter Input []*Parameter
Output []*Parameter Output []*Parameter
Body token.List Body token.List
Identifiers map[string]*ssa.Value Identifiers map[string]ssa.Value
Err error Err error
} }
@ -27,9 +27,11 @@ func NewFunction(name string, file *fs.File) *Function {
Name: name, Name: name,
File: file, File: file,
UniqueName: fmt.Sprintf("%s.%s", file.Package, name), UniqueName: fmt.Sprintf("%s.%s", file.Package, name),
Identifiers: make(map[string]*ssa.Value), Identifiers: make(map[string]ssa.Value),
Function: ssa.Function{ IR: ssa.IR{
Blocks: []*ssa.Block{{}}, Blocks: []*ssa.Block{
{Instructions: make([]ssa.Value, 0, 8)},
},
}, },
} }
} }

View file

@ -24,4 +24,13 @@ func (err *UnknownIdentifier) Error() string {
} }
return fmt.Sprintf("Unknown identifier '%s'", err.Name) return fmt.Sprintf("Unknown identifier '%s'", err.Name)
}
// UnusedValue error is created when a value is never used.
type UnusedValue struct {
Value string
}
func (err *UnusedValue) Error() string {
return fmt.Sprintf("Unused value '%s'", err.Value)
} }

View file

@ -13,9 +13,9 @@ type operator struct {
Operands int8 Operands int8
} }
// operators defines the operators used in the language. // Operators defines the Operators used in the language.
// The number corresponds to the operator priority and can not be zero. // The number corresponds to the operator priority and can not be zero.
var operators = [64]operator{ var Operators = [64]operator{
token.Dot: {".", 13, 2}, token.Dot: {".", 13, 2},
token.Call: {"λ", 12, 1}, token.Call: {"λ", 12, 1},
token.Array: {"@", 12, 2}, token.Array: {"@", 12, 2},
@ -59,9 +59,9 @@ var operators = [64]operator{
} }
func numOperands(symbol token.Kind) int { func numOperands(symbol token.Kind) int {
return int(operators[symbol].Operands) return int(Operators[symbol].Operands)
} }
func precedence(symbol token.Kind) int8 { func precedence(symbol token.Kind) int8 {
return operators[symbol].Precedence return Operators[symbol].Precedence
} }

View file

@ -17,9 +17,9 @@ func (expr *Expression) write(builder *strings.Builder, source []byte) {
switch expr.Token.Kind { switch expr.Token.Kind {
case token.Call: case token.Call:
builder.WriteString(operators[token.Call].Symbol) builder.WriteString(Operators[token.Call].Symbol)
case token.Array: case token.Array:
builder.WriteString(operators[token.Array].Symbol) builder.WriteString(Operators[token.Array].Symbol)
default: default:
builder.WriteString(expr.Token.String(source)) builder.WriteString(expr.Token.String(source))
} }

23
src/ssa/Arguments.go Normal file
View file

@ -0,0 +1,23 @@
package ssa
type Arguments struct {
Args []Value
}
func (v *Arguments) Dependencies() []Value {
return v.Args
}
func (a Arguments) Equals(b Arguments) bool {
if len(a.Args) != len(b.Args) {
return false
}
for i := range a.Args {
if !a.Args[i].Equals(b.Args[i]) {
return false
}
}
return true
}

View file

@ -0,0 +1,50 @@
package ssa
import (
"fmt"
"git.urbach.dev/cli/q/src/expression"
"git.urbach.dev/cli/q/src/token"
)
type BinaryOperation struct {
Left Value
Right Value
Op token.Kind
Liveness
HasToken
}
func (v *BinaryOperation) Dependencies() []Value {
return []Value{v.Left, v.Right}
}
func (a *BinaryOperation) Equals(v Value) bool {
b, sameType := v.(*BinaryOperation)
if !sameType {
return false
}
if a.Source.Kind != b.Source.Kind {
return false
}
if !a.Left.Equals(b.Left) {
return false
}
if !a.Right.Equals(b.Right) {
return false
}
return true
}
func (v *BinaryOperation) IsConst() bool {
return true
}
func (v *BinaryOperation) String() string {
return fmt.Sprintf("%s %s %s", v.Left, expression.Operators[v.Op].Symbol, v.Right)
}

View file

@ -6,7 +6,11 @@ type Block struct {
} }
// Append adds a new instruction to the block. // Append adds a new instruction to the block.
func (b *Block) Append(instr Value) *Value { func (block *Block) Append(instr Value) Value {
b.Instructions = append(b.Instructions, instr) for _, dep := range instr.Dependencies() {
return &b.Instructions[len(b.Instructions)-1] dep.AddUse(instr)
}
block.Instructions = append(block.Instructions, instr)
return instr
} }

31
src/ssa/Bytes.go Normal file
View file

@ -0,0 +1,31 @@
package ssa
import "bytes"
type Bytes struct {
Bytes []byte
Liveness
HasToken
}
func (v *Bytes) Dependencies() []Value {
return nil
}
func (a *Bytes) Equals(v Value) bool {
b, sameType := v.(*Bytes)
if !sameType {
return false
}
return bytes.Equal(a.Bytes, b.Bytes)
}
func (v *Bytes) IsConst() bool {
return true
}
func (v *Bytes) String() string {
return string(v.Bytes)
}

27
src/ssa/Call.go Normal file
View file

@ -0,0 +1,27 @@
package ssa
import "fmt"
type Call struct {
Arguments
Liveness
HasToken
}
func (a *Call) Equals(v Value) bool {
b, sameType := v.(*Call)
if !sameType {
return false
}
return a.Arguments.Equals(b.Arguments)
}
func (v *Call) IsConst() bool {
return false
}
func (v *Call) String() string {
return fmt.Sprintf("call%v", v.Args)
}

View file

@ -1,61 +1,29 @@
package ssa package ssa
import (
"git.urbach.dev/cli/q/src/cpu"
)
// Function is a list of basic blocks.
type Function struct { type Function struct {
Blocks []*Block UniqueName string
Liveness
HasToken
} }
// AddBlock adds a new block to the function. func (v *Function) Dependencies() []Value {
func (f *Function) AddBlock() *Block { return nil
block := &Block{}
f.Blocks = append(f.Blocks, block)
return block
} }
// Append adds a new value to the last block. func (a *Function) Equals(v Value) bool {
func (f *Function) Append(instr Value) *Value { b, sameType := v.(*Function)
if len(f.Blocks) == 0 {
f.Blocks = append(f.Blocks, &Block{}) if !sameType {
return false
} }
if instr.IsConst() { return a.UniqueName == b.UniqueName
for _, b := range f.Blocks {
for _, existing := range b.Instructions {
if instr.Equals(existing) {
return &existing
}
}
}
}
return f.Blocks[len(f.Blocks)-1].Append(instr)
} }
// AppendInt adds a new integer value to the last block. func (v *Function) IsConst() bool {
func (f *Function) AppendInt(x int) *Value { return true
return f.Append(Value{Type: Int, Int: x})
} }
// AppendRegister adds a new register value to the last block. func (v *Function) String() string {
func (f *Function) AppendRegister(reg cpu.Register) *Value { return v.UniqueName
return f.Append(Value{Type: Register, Register: reg})
}
// AppendFunction adds a new function value to the last block.
func (f *Function) AppendFunction(name string) *Value {
return f.Append(Value{Type: Func, Text: name})
}
// AppendBytes adds a new byte slice value to the last block.
func (f *Function) AppendBytes(s []byte) *Value {
return f.Append(Value{Type: String, Text: string(s)})
}
// AppendString adds a new string value to the last block.
func (f *Function) AppendString(s string) *Value {
return f.Append(Value{Type: String, Text: s})
} }

11
src/ssa/HasToken.go Normal file
View file

@ -0,0 +1,11 @@
package ssa
import "git.urbach.dev/cli/q/src/token"
type HasToken struct {
Source token.Token
}
func (v *HasToken) Token() token.Token {
return v.Source
}

83
src/ssa/IR.go Normal file
View file

@ -0,0 +1,83 @@
package ssa
import (
"git.urbach.dev/cli/q/src/cpu"
)
// IR is a list of basic blocks.
type IR struct {
Blocks []*Block
}
// AddBlock adds a new block to the function.
func (f *IR) AddBlock() *Block {
block := &Block{
Instructions: make([]Value, 0, 8),
}
f.Blocks = append(f.Blocks, block)
return block
}
// Append adds a new value to the last block.
func (f *IR) Append(instr Value) Value {
if len(f.Blocks) == 0 {
f.AddBlock()
}
if instr.IsConst() {
for existing := range f.Values {
if existing.IsConst() && instr.Equals(existing) {
return existing
}
}
}
return f.Blocks[len(f.Blocks)-1].Append(instr)
}
// AppendInt adds a new integer value to the last block.
func (f *IR) AppendInt(x int) *Int {
v := &Int{Int: x}
f.Append(v)
return v
}
// AppendRegister adds a new register value to the last block.
func (f *IR) AppendRegister(reg cpu.Register) *Register {
v := &Register{Register: reg}
f.Append(v)
return v
}
// AppendFunction adds a new function value to the last block.
func (f *IR) AppendFunction(name string) *Function {
v := &Function{UniqueName: name}
f.Append(v)
return v
}
// AppendBytes adds a new byte slice value to the last block.
func (f *IR) AppendBytes(s []byte) *Bytes {
v := &Bytes{Bytes: s}
f.Append(v)
return v
}
// AppendString adds a new string value to the last block.
func (f *IR) AppendString(s string) *Bytes {
v := &Bytes{Bytes: []byte(s)}
f.Append(v)
return v
}
// Values yields on each value.
func (f *IR) Values(yield func(Value) bool) {
for _, block := range f.Blocks {
for _, instr := range block.Instructions {
if !yield(instr) {
return
}
}
}
}

33
src/ssa/Int.go Normal file
View file

@ -0,0 +1,33 @@
package ssa
import (
"fmt"
)
type Int struct {
Int int
Liveness
HasToken
}
func (v *Int) Dependencies() []Value {
return nil
}
func (a *Int) Equals(v Value) bool {
b, sameType := v.(*Int)
if !sameType {
return false
}
return a.Int == b.Int
}
func (v *Int) IsConst() bool {
return true
}
func (v *Int) String() string {
return fmt.Sprintf("%d", v.Int)
}

13
src/ssa/Liveness.go Normal file
View file

@ -0,0 +1,13 @@
package ssa
type Liveness struct {
alive int
}
func (v *Liveness) AddUse(user Value) {
v.alive++
}
func (v *Liveness) Alive() int {
return v.alive
}

31
src/ssa/Register.go Normal file
View file

@ -0,0 +1,31 @@
package ssa
import "git.urbach.dev/cli/q/src/cpu"
type Register struct {
Register cpu.Register
Liveness
HasToken
}
func (v *Register) Dependencies() []Value {
return nil
}
func (a *Register) Equals(v Value) bool {
b, sameType := v.(*Register)
if !sameType {
return false
}
return a.Register == b.Register
}
func (v *Register) IsConst() bool {
return true
}
func (v *Register) String() string {
return v.Register.String()
}

39
src/ssa/Return.go Normal file
View file

@ -0,0 +1,39 @@
package ssa
import "fmt"
type Return struct {
Arguments
HasToken
}
func (a *Return) AddUse(user Value) { panic("return is not a value") }
func (a *Return) Alive() int { return 0 }
func (a *Return) Equals(v Value) bool {
b, sameType := v.(*Return)
if !sameType {
return false
}
if len(a.Args) != len(b.Args) {
return false
}
for i := range a.Args {
if !a.Args[i].Equals(b.Args[i]) {
return false
}
}
return true
}
func (v *Return) IsConst() bool {
return false
}
func (v *Return) String() string {
return fmt.Sprintf("return %v", v.Args)
}

27
src/ssa/Syscall.go Normal file
View file

@ -0,0 +1,27 @@
package ssa
import "fmt"
type Syscall struct {
Arguments
Liveness
HasToken
}
func (a *Syscall) Equals(v Value) bool {
b, sameType := v.(*Syscall)
if !sameType {
return false
}
return a.Arguments.Equals(b.Arguments)
}
func (v *Syscall) IsConst() bool {
return false
}
func (v *Syscall) String() string {
return fmt.Sprintf("syscall%v", v.Args)
}

View file

@ -1,39 +0,0 @@
package ssa
// Type represents the instruction type.
type Type byte
const (
None Type = iota
// Values
Int
Float
Func
Register
String
// Binary
Add
Sub
Mul
Div
Mod
// Bitwise
And
Or
Xor
Shl
Shr
// Control flow
If
Jump
Call
Return
Syscall
// Special
Phi
)

View file

@ -1,83 +1,13 @@
package ssa package ssa
import ( import "git.urbach.dev/cli/q/src/token"
"fmt"
"git.urbach.dev/cli/q/src/cpu" type Value interface {
) AddUse(Value)
Alive() int
// Value is a single instruction in a basic block. Dependencies() []Value
// It is implemented as a "fat struct" for performance reasons. Equals(Value) bool
// It contains all the fields necessary to represent all instruction types. IsConst() bool
type Value struct { String() string
Args []*Value Token() token.Token
Int int
Text string
Register cpu.Register
Type Type
}
// Equals returns true if the values are equal.
func (a Value) Equals(b Value) bool {
if a.Type != b.Type {
return false
}
if a.Int != b.Int {
return false
}
if a.Text != b.Text {
return false
}
if a.Register != b.Register {
return false
}
if len(a.Args) != len(b.Args) {
return false
}
for i := range a.Args {
if !a.Args[i].Equals(*b.Args[i]) {
return false
}
}
return true
}
// IsConst returns true if the value is constant.
func (i *Value) IsConst() bool {
switch i.Type {
case Func, Int, Register, String:
return true
default:
return false
}
}
// String returns a human-readable representation of the instruction.
func (i *Value) String() string {
switch i.Type {
case Func:
return i.Text
case Int:
return fmt.Sprintf("%d", i.Int)
case Register:
return i.Register.String()
case String:
return fmt.Sprintf("\"%s\"", i.Text)
case Add:
return fmt.Sprintf("%s + %s", i.Args[0], i.Args[1])
case Return:
return fmt.Sprintf("return %s", i.Args[0])
case Call:
return fmt.Sprintf("call%v", i.Args)
case Syscall:
return fmt.Sprintf("syscall%v", i.Args)
default:
return ""
}
} }

View file

@ -5,12 +5,16 @@ import (
"runtime/debug" "runtime/debug"
"testing" "testing"
"git.urbach.dev/cli/q/src/ssa"
"git.urbach.dev/go/assert" "git.urbach.dev/go/assert"
) )
// This benchmark compares the performance of fat structs and interfaces. // This benchmark compares the performance of fat structs and interfaces.
// It allocates `n` objects where `n` must be divisible by 2. // It allocates `actual` objects where `actual` must be divisible by 2.
const n = 100 const (
actual = 64
estimate = 8
)
type FatStruct struct { type FatStruct struct {
Type byte Type byte
@ -45,9 +49,9 @@ func TestMain(m *testing.M) {
func BenchmarkFatStructRaw(b *testing.B) { func BenchmarkFatStructRaw(b *testing.B) {
for b.Loop() { for b.Loop() {
entries := make([]FatStruct, 0, n) entries := make([]FatStruct, 0, estimate)
for i := range n { for i := range actual {
entries = append(entries, FatStruct{ entries = append(entries, FatStruct{
Type: byte(i % 2), Type: byte(i % 2),
A: i, A: i,
@ -65,15 +69,15 @@ func BenchmarkFatStructRaw(b *testing.B) {
} }
} }
assert.Equal(b, count, n/2) assert.Equal(b, count, actual/2)
} }
} }
func BenchmarkFatStructPtr(b *testing.B) { func BenchmarkFatStructPtr(b *testing.B) {
for b.Loop() { for b.Loop() {
entries := make([]*FatStruct, 0, n) entries := make([]*FatStruct, 0, estimate)
for i := range n { for i := range actual {
entries = append(entries, &FatStruct{ entries = append(entries, &FatStruct{
Type: byte(i % 2), Type: byte(i % 2),
A: i, A: i,
@ -91,15 +95,15 @@ func BenchmarkFatStructPtr(b *testing.B) {
} }
} }
assert.Equal(b, count, n/2) assert.Equal(b, count, actual/2)
} }
} }
func BenchmarkInterfaceRaw(b *testing.B) { func BenchmarkInterfaceRaw(b *testing.B) {
for b.Loop() { for b.Loop() {
entries := make([]Instruction, 0, n) entries := make([]Instruction, 0, estimate)
for i := range n { for i := range actual {
if i%2 == 0 { if i%2 == 0 {
entries = append(entries, BinaryInstruction{ entries = append(entries, BinaryInstruction{
A: i, A: i,
@ -123,15 +127,15 @@ func BenchmarkInterfaceRaw(b *testing.B) {
} }
} }
assert.Equal(b, count, n/2) assert.Equal(b, count, actual/2)
} }
} }
func BenchmarkInterfacePtr(b *testing.B) { func BenchmarkInterfacePtr(b *testing.B) {
for b.Loop() { for b.Loop() {
entries := make([]Instruction, 0, n) entries := make([]Instruction, 0, estimate)
for i := range n { for i := range actual {
if i%2 == 0 { if i%2 == 0 {
entries = append(entries, &BinaryInstruction{ entries = append(entries, &BinaryInstruction{
A: i, A: i,
@ -155,6 +159,32 @@ func BenchmarkInterfacePtr(b *testing.B) {
} }
} }
assert.Equal(b, count, n/2) assert.Equal(b, count, actual/2)
}
}
func BenchmarkSSA(b *testing.B) {
for b.Loop() {
f := ssa.IR{}
for i := range actual {
if i%2 == 0 {
f.Append(&ssa.Return{})
} else {
f.Append(&ssa.Call{})
}
}
count := 0
for instr := range f.Values {
switch instr.(type) {
case *ssa.Return:
count++
case *ssa.Call:
}
}
assert.Equal(b, count, actual/2)
} }
} }

View file

@ -4,23 +4,19 @@ import (
"testing" "testing"
"git.urbach.dev/cli/q/src/ssa" "git.urbach.dev/cli/q/src/ssa"
"git.urbach.dev/cli/q/src/token"
"git.urbach.dev/go/assert" "git.urbach.dev/go/assert"
) )
func TestFunction(t *testing.T) { func TestFunction(t *testing.T) {
fn := ssa.Function{} fn := ssa.IR{}
a := fn.AppendInt(1) a := fn.AppendInt(1)
b := fn.AppendInt(2) b := fn.AppendInt(2)
c := fn.Append(ssa.Value{Type: ssa.Add, Args: []*ssa.Value{a, b}}) c := fn.Append(&ssa.BinaryOperation{Op: token.Add, Left: a, Right: b})
fn.AddBlock() fn.AddBlock()
d := fn.AppendInt(3) d := fn.AppendInt(3)
e := fn.AppendInt(4) e := fn.AppendInt(4)
f := fn.Append(ssa.Value{Type: ssa.Add, Args: []*ssa.Value{d, e}}) f := fn.Append(&ssa.BinaryOperation{Op: token.Add, Left: d, Right: e})
assert.Equal(t, c.String(), "1 + 2") assert.Equal(t, c.String(), "1 + 2")
assert.Equal(t, f.String(), "3 + 4") assert.Equal(t, f.String(), "3 + 4")
}
func TestInvalidInstruction(t *testing.T) {
instr := ssa.Value{}
assert.Equal(t, instr.String(), "")
} }