Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Compiler] Compile if-let #3756

Merged
merged 2 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bbq/compiler/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ func (g *InstructionCodeGen) PatchJump(offset int, newTarget uint16) {
ins.Target = newTarget
(*g.target)[offset] = ins

case opcode.InstructionJumpIfNil:
ins.Target = newTarget
(*g.target)[offset] = ins

default:
panic(errors.NewUnreachableError())
}
Expand Down
30 changes: 28 additions & 2 deletions bbq/compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,12 @@ func (c *Compiler[_]) emitUndefinedJumpIfFalse() int {
return offset
}

func (c *Compiler[_]) emitUndefinedJumpIfNil() int {
offset := c.codeGen.Offset()
c.codeGen.Emit(opcode.InstructionJumpIfNil{Target: math.MaxUint16})
return offset
}

func (c *Compiler[_]) patchJump(opcodeOffset int) {
count := c.codeGen.Offset()
if count == 0 {
Expand Down Expand Up @@ -570,14 +576,33 @@ func (c *Compiler[_]) VisitContinueStatement(_ *ast.ContinueStatement) (_ struct

func (c *Compiler[_]) VisitIfStatement(statement *ast.IfStatement) (_ struct{}) {
// TODO: scope
var elseJump int
switch test := statement.Test.(type) {
case ast.Expression:
c.compileExpression(test)
elseJump = c.emitUndefinedJumpIfFalse()

case *ast.VariableDeclaration:
// TODO: second value
c.compileExpression(test.Value)

tempIndex := c.currentFunction.generateLocalIndex()
c.codeGen.Emit(opcode.InstructionSetLocal{LocalIndex: tempIndex})

c.codeGen.Emit(opcode.InstructionGetLocal{LocalIndex: tempIndex})
elseJump = c.emitUndefinedJumpIfNil()

c.codeGen.Emit(opcode.InstructionGetLocal{LocalIndex: tempIndex})
c.codeGen.Emit(opcode.InstructionUnwrap{})
varDeclTypes := c.ExtendedElaboration.VariableDeclarationTypes(test)
c.emitTransfer(varDeclTypes.TargetType)
local := c.currentFunction.declareLocal(test.Identifier.Identifier)
c.codeGen.Emit(opcode.InstructionSetLocal{LocalIndex: local.index})

default:
// TODO:
panic(errors.NewUnreachableError())
}
elseJump := c.emitUndefinedJumpIfFalse()

c.compileBlock(statement.Then)
elseBlock := statement.Else
if elseBlock != nil {
Expand All @@ -588,6 +613,7 @@ func (c *Compiler[_]) VisitIfStatement(statement *ast.IfStatement) (_ struct{})
} else {
c.patchJump(elseJump)
}

return
}

Expand Down
62 changes: 62 additions & 0 deletions bbq/compiler/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package compiler
import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/onflow/cadence/bbq"
Expand Down Expand Up @@ -457,3 +458,64 @@ func TestCompileDictionary(t *testing.T) {
program.Constants,
)
}

func TestCompileIfLet(t *testing.T) {

t.Parallel()

checker, err := ParseAndCheck(t, `
fun test(x: Int?): Int {
if let y = x {
return y
} else {
return 2
}
}
`)
require.NoError(t, err)

compiler := NewInstructionCompiler(checker)
program := compiler.Compile()

require.Len(t, program.Functions, 1)

assert.Equal(t,
[]opcode.Instruction{
// let y = x
opcode.InstructionGetLocal{LocalIndex: 0x0},
opcode.InstructionSetLocal{LocalIndex: 0x1},

// if
opcode.InstructionGetLocal{LocalIndex: 0x1},
opcode.InstructionJumpIfNil{Target: 11},

// let y = x
opcode.InstructionGetLocal{LocalIndex: 0x1},
opcode.InstructionUnwrap{},
opcode.InstructionTransfer{TypeIndex: 0x0},
opcode.InstructionSetLocal{LocalIndex: 0x2},

// then { return y }
opcode.InstructionGetLocal{LocalIndex: 0x2},
opcode.InstructionReturnValue{},
opcode.InstructionJump{Target: 13},

// else { return 2 }
opcode.InstructionGetConstant{ConstantIndex: 0x0},
opcode.InstructionReturnValue{},

opcode.InstructionReturn{},
},
compiler.ExportFunctions()[0].Code,
)

assert.Equal(t,
[]*bbq.Constant{
{
Data: []byte{0x2},
Kind: constantkind.Int,
},
},
program.Constants,
)
}
9 changes: 7 additions & 2 deletions bbq/compiler/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,17 @@ func newFunction[E any](name string, parameterCount uint16, isCompositeFunction
}
}

func (f *function[E]) generateLocalIndex() uint16 {
index := f.localCount
f.localCount++
return index
}

func (f *function[E]) declareLocal(name string) *local {
if f.localCount == math.MaxUint16 {
panic(errors.NewDefaultUserError("invalid local declaration"))
}
index := f.localCount
f.localCount++
index := f.generateLocalIndex()
local := &local{index: index}
f.locals.Set(name, local)
return local
Expand Down
32 changes: 32 additions & 0 deletions bbq/opcode/instructions.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions bbq/opcode/instructions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,22 @@
type: "index"
controlEffects:
- jump: "target"
valueEffects:
pop:
- name: "value"
type: "value"

- name: "jumpIfNil"
description: Jumps to the given instruction, if the top value on the stack is `nil`.
operands:
- name: "target"
type: "index"
controlEffects:
- jump: "target"
valueEffects:
pop:
- name: "value"
type: "value"

- name: "return"
description: Returns from the current function, without a value.
Expand Down
2 changes: 1 addition & 1 deletion bbq/opcode/opcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const (
ReturnValue
Jump
JumpIfFalse
_
JumpIfNil
_
_
_
Expand Down
7 changes: 4 additions & 3 deletions bbq/opcode/opcode_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions bbq/vm/test/vm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3184,3 +3184,51 @@ func TestFunctionPostConditions(t *testing.T) {
assert.Equal(t, []string{"A", "D", "F", "E", "C", "B"}, logs)
})
}

func TestIfLet(t *testing.T) {

t.Parallel()

t.Run("some", func(t *testing.T) {

t.Parallel()

result, err := compileAndInvoke(t, `
fun main(x: Int?): Int {
if let y = x {
return y
} else {
return 2
}
}
`,
"main",
vm.NewSomeValueNonCopying(
vm.NewIntValue(1),
),
)
require.NoError(t, err)
assert.Equal(t, vm.NewIntValue(1), result)
})

t.Run("nil", func(t *testing.T) {

t.Parallel()

result, err := compileAndInvoke(t, `
fun main(x: Int?): Int {
if let y = x {
return y
} else {
return 2
}
}
`,
"main",
vm.NilValue{},
)

require.NoError(t, err)
assert.Equal(t, vm.NewIntValue(2), result)
})
}
9 changes: 9 additions & 0 deletions bbq/vm/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,13 @@ func opJumpIfFalse(vm *VM, ins opcode.InstructionJumpIfFalse) {
}
}

func opJumpIfNil(vm *VM, ins opcode.InstructionJumpIfNil) {
_, ok := vm.pop().(NilValue)
if ok {
vm.ip = ins.Target
}
}

func opBinaryIntAdd(vm *VM) {
left, right := vm.peekPop()
leftNumber := left.(IntValue)
Expand Down Expand Up @@ -698,6 +705,8 @@ func (vm *VM) run() {
opJump(vm, ins)
case opcode.InstructionJumpIfFalse:
opJumpIfFalse(vm, ins)
case opcode.InstructionJumpIfNil:
opJumpIfNil(vm, ins)
case opcode.InstructionIntAdd:
opBinaryIntAdd(vm)
case opcode.InstructionIntSubtract:
Expand Down
Loading