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

Feat: Implement Casm Assembler #44

Merged
merged 10 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 9 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
3 changes: 3 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
run:
skip-files:
- pkg/assembler/grammar.go
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/NethermindEth/cairo-vm-go

go 1.20
go 1.21

require (
github.com/bits-and-blooms/bitset v1.8.0 // indirect
Expand All @@ -15,6 +15,7 @@ require (
)

require (
github.com/alecthomas/participle/v2 v2.0.0
github.com/consensys/gnark-crypto v0.11.1
github.com/go-playground/validator/v10 v10.4.1
github.com/stretchr/testify v1.8.4
Expand Down
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=
github.com/alecthomas/participle/v2 v2.0.0/go.mod h1:rAKZdJldHu8084ojcWevWAL8KmEU+AT+Olodb+WoN2Y=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5MS5JVb4c=
github.com/bits-and-blooms/bitset v1.8.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/consensys/gnark-crypto v0.11.1 h1:pt2nLbntYZA5IXnSw21vcQgoUCRPn6J/xylWQpK8gtM=
Expand All @@ -13,7 +19,10 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c=
github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
274 changes: 274 additions & 0 deletions pkg/assembler/assembler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
package assembler

import (
"github.com/alecthomas/participle/v2"
f "github.com/consensys/gnark-crypto/ecc/stark-curve/fp"
)

var parser *participle.Parser[CasmProgram] = participle.MustBuild[CasmProgram](
// mandatory lookahead to disambiguate between productions:
// expr -> [reg + n] + [reg + m] and
// expr -> [reg + n]
participle.UseLookahead(5),
)

func CasmToBytecode(code string) ([]*f.Element, error) {
casmAst, err := parser.ParseString("", code)
if err != nil {
return nil, err
}
return encodeCasmProgram(*casmAst)
}

//
// Functions that visit the AST in order to encode the instructions
//

const (
// offsets
op0Offset = 16
op1Offset = 32

// flag values
dstRegBit = 48
op0RegBit = 49
op1ImmBit = 50
op1FpBit = 51
op1ApBit = 52
resAddBit = 53
resMulBit = 54
pcJumpAbsBit = 55
pcJumpRelBit = 56
pcJnzBit = 57
apAddBit = 58
apAdd1Bit = 59
opcodeCallBit = 60
opcodeRetBit = 61
opcodeAssertEqBit = 62

// default values
biasedZero uint16 = 0x8000
biasedPlusOne uint16 = 0x8001
biasedMinusOne uint16 = 0x7FFF
biasedMinusTwo uint16 = 0x7FFE
)

func encodeCasmProgram(casmAst CasmProgram) ([]*f.Element, error) {
joshklop marked this conversation as resolved.
Show resolved Hide resolved
n := len(casmAst.Instructions)
bytecode := make([]*f.Element, 0, n+(n/2)+1)
var err error
for i := range casmAst.Instructions {
bytecode, err = encodeInstruction(bytecode, casmAst.Instructions[i])
if err != nil {
return nil, err
}
}
return bytecode, nil
}

func encodeInstruction(bytecode []*f.Element, instruction Instruction) ([]*f.Element, error) {
var encode uint64 = 0
expression := instruction.Unwrap().Expression()

encode, err := encodeDstReg(&instruction, encode)
if err != nil {
return nil, err
}

encode, err = encodeOp0Reg(&instruction, expression, encode)
if err != nil {
return nil, err
}

encode, imm, err := encodeOp1Source(&instruction, expression, encode)
if err != nil {
return nil, err
}

encode = encodeResLogic(expression, encode) |
encodePcUpdate(instruction, encode) |
encodeApUpdate(instruction, encode) |
encodeOpCode(instruction, encode)

encodeAsFelt := new(f.Element).SetUint64(encode)

bytecode = append(bytecode, encodeAsFelt)
if imm != nil {
bytecode = append(bytecode, imm)
}

return bytecode, nil
}

func encodeDstReg(instr *Instruction, encode uint64) (uint64, error) {
if instr.ApPlus != nil || instr.Core.Jump != nil {
// dstOffset is not involved so it is set to fp - 1 as default value
encode |= 1 << dstRegBit
encode |= uint64(biasedMinusOne)
return encode, nil
}
if instr.Core.Call != nil {
// dstOffset is set to ap + 0
encode |= uint64(biasedZero)
return encode, nil
}
if instr.Core.Ret != nil {
// dstOffset is set as fp - 2
encode |= 1 << dstRegBit
encode |= uint64(biasedMinusTwo)
return encode, nil
}

var deref *Deref
if instr.Core.AssertEq != nil {
deref = instr.Core.AssertEq.Dst
} else if instr.Core.Jnz != nil {
deref = instr.Core.Jnz.Condition
}

biasedOffset, err := deref.BiasedOffset()
if err != nil {
return 0, err
}
encode |= uint64(biasedOffset)
if deref.IsFp() {
encode |= 1 << dstRegBit
}

return encode, nil

}

func encodeOp0Reg(instr *Instruction, expr Expressioner, encode uint64) (uint64, error) {
if instr.Core != nil && instr.Core.Call != nil {
// op0 is set as [ap + 1] to store current pc
encode |= uint64(biasedPlusOne) << op0Offset
return encode, nil
}
if (instr.Core != nil && (instr.Core.Jnz != nil || instr.Core.Ret != nil)) ||
(expr.AsDeref() != nil || expr.AsImmediate() != nil) {
// op0 is not involved, it is set as fp - 1 as default value
encode |= 1 << op0RegBit
encode |= uint64(biasedMinusOne) << op0Offset
return encode, nil
}

var deref *Deref
if expr.AsDoubleDeref() != nil {
deref = expr.AsDoubleDeref().Deref
} else {
deref = expr.AsMathOperation().Lhs
}

biasedOffset, err := deref.BiasedOffset()
if err != nil {
return 0, err
}
encode |= uint64(biasedOffset) << op0Offset
if deref.IsFp() {
encode |= 1 << op0RegBit
}

return encode, nil
}

// Given the expression and the current encode returns an updated encode with the corresponding bit
// and offset of op1, an immeadiate if exists, and a possible error
func encodeOp1Source(inst *Instruction, expr Expressioner, encode uint64) (uint64, *f.Element, error) {
if inst.Core != nil && inst.Core.Ret != nil {
// op1 is set as [fp - 1], where we read the previous pc
encode |= uint64(biasedMinusOne) << op1Offset
encode |= 1 << op1FpBit
return encode, nil, nil
}

if expr.AsDeref() != nil {
biasedOffset, err := expr.AsDeref().BiasedOffset()
if err != nil {
return 0, nil, err
}
encode |= uint64(biasedOffset) << op1Offset
if expr.AsDeref().IsFp() {
encode |= 1 << op1FpBit
} else {
encode |= 1 << op1ApBit
}
return encode, nil, nil
} else if expr.AsDoubleDeref() != nil {
biasedOffset, err := expr.AsDoubleDeref().BiasedOffset()
if err != nil {
return 0, nil, err
}
encode |= uint64(biasedOffset) << op1Offset
return encode, nil, nil
} else if expr.AsImmediate() != nil {
imm, err := new(f.Element).SetString(*expr.AsImmediate())
if err != nil {
return 0, nil, err
}
encode |= uint64(biasedPlusOne) << op1Offset
return encode | 1<<op1ImmBit, imm, nil
} else {
// if it is a math operation, the op1 source is set by the right hand side
return encodeOp1Source(inst, expr.AsMathOperation().Rhs, encode)
}
}

func encodeResLogic(expression Expressioner, encode uint64) uint64 {
if expression != nil && expression.AsMathOperation() != nil {
if expression.AsMathOperation().Operator == "+" {
encode |= 1 << resAddBit
} else {
encode |= 1 << resMulBit
}
}
return encode
}

func encodePcUpdate(instruction Instruction, encode uint64) uint64 {
if instruction.Core == nil {
return encode
}

if instruction.Core.Jump != nil || instruction.Core.Call != nil {
var isAbs bool
if instruction.Core.Jump != nil {
isAbs = instruction.Core.Jump.JumpType == "abs"
} else {
isAbs = instruction.Core.Call.CallType == "abs"
}
if isAbs {
encode |= 1 << pcJumpAbsBit
} else {
encode |= 1 << pcJumpRelBit
}
} else if instruction.Core.Jnz != nil {
encode |= 1 << pcJnzBit
} else if instruction.Core.Ret != nil {
encode |= 1 << pcJumpAbsBit
}

return encode
}

func encodeApUpdate(instruction Instruction, encode uint64) uint64 {
if instruction.ApPlus != nil {
encode |= 1 << apAddBit
} else if instruction.ApPlusOne {
encode |= 1 << apAdd1Bit
}
return encode
}

func encodeOpCode(instruction Instruction, encode uint64) uint64 {
if instruction.Core != nil {
if instruction.Core.Call != nil {
encode |= 1 << opcodeCallBit
} else if instruction.Core.Ret != nil {
encode |= 1 << opcodeRetBit
} else if instruction.Core.AssertEq != nil {
encode |= 1 << opcodeAssertEqBit
}
}
return encode
}
Loading