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

add cmd/casm-inspect disasm utility #183

Merged
merged 2 commits into from
Feb 23, 2024
Merged
Changes from 1 commit
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
Next Next commit
add cmd/casm-inspect disasm utility
This is a tool I made while studying the casm bytecode format.
Since making thoth disassembler work requires some extra steps,
I figured it would be handy to have a version that relies on our
assembler package and supports the exact versions of input
files that are relevant to this project.

I tried to produce the correct Cairo0 program when disassembling.
If done carefully (and with metadata provided from the compiled json file),
we can use it to test the assembler in an encode-decode style
(basically we can use the disassembler output as an assembler parser input).

Given this cairo0 source file:

```cairo
%builtins output

from starkware.cairo.common.serialize import serialize_word

func div2(x: felt) -> felt {
    return x / 2;
}

func main{output_ptr: felt*}() {
    alloc_locals;
    local x = 42;
    local y = x + 1;
    local z = div2(x);
    if (y == 0) {
      serialize_word(z);
    } else {
      serialize_word(y);
    }
    ret;
}
```

And a compiled casm bytecode produced from it (output.json), we can disassemble it into the following:

```casm
// func entry pc=0
// [fp-3] => word: felt
// [fp-4] => output_ptr: felt* (implicit arg)
func starkware.cairo.common.serialize.serialize_word{output_ptr: felt*}(word: felt) {
    assert [fp-3] = [[fp-4]];
    assert [ap] = [fp-4] + 1, ap++;
    ret;
}
// func entry pc=4
// [fp-3] => x: felt
func div2(x: felt) -> felt {
    assert [ap] = [fp-3] * 1809251394333065606848661391547535052811553607665798349986546028067936010241, ap++; // div 2
    ret;
}
// func entry pc=7
// [fp-3] => output_ptr: felt* (implicit arg)
func main{output_ptr: felt*}() {
    nop; // alloc_locals; ap += 3
    assert [fp] = 42;
    assert [fp+1] = [fp] + 1;
    assert [ap] = [fp], ap++;
    call rel -10; // func div2; ap += 2
    assert [fp+2] = [ap-1];
    jmp rel 8 if [fp+1] != 0; // targets L1
    assert [ap] = [fp-3], ap++;
    assert [ap] = [fp+2], ap++;
    call rel -21; // func starkware.cairo.common.serialize.serialize_word; ap += 2
    jmp rel 6; // targets L3
  L1:
    assert [ap] = [fp-3], ap++;
    assert [ap] = [fp+1], ap++;
    call rel -27; // func starkware.cairo.common.serialize.serialize_word; ap += 2
  L3:
    ret;
}
```

This disassembler annotates some lines with recognized patterns like division operations.
It does not include any hints-related information (yet?)
quasilyte committed Feb 23, 2024
commit 50f6e299561eba2ec39a2e997c59049caffeb135
122 changes: 122 additions & 0 deletions cmd/casm-inspect/disasm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package main

import (
"encoding/json"
"fmt"
"os"
"strings"

"github.com/NethermindEth/cairo-vm-go/pkg/disasm"
f "github.com/consensys/gnark-crypto/ecc/stark-curve/fp"

"github.com/urfave/cli/v2"
)

// disasmProgram implements a "disasm" subcommand.
type disasmProgram struct {
pathToFile string
bytecodeKey string

rawCasm map[string]any

bytecode []*f.Element

disassembled *disasm.Program
}

func (p *disasmProgram) Action(ctx *cli.Context) error {
p.pathToFile = ctx.Args().Get(0)
if p.pathToFile == "" {
return fmt.Errorf("path to casm file not set")
}

type step struct {
name string
fn func() error
}
steps := []step{
{"unmarshal casm file", p.unmarshalCasmFileStep},
{"load bytecode", p.loadBytecodeStep},
{"disassemble", p.disassembleStep},
{"print", p.printStep},
}
for _, s := range steps {
if err := s.fn(); err != nil {
return fmt.Errorf("%s: %w", s.name, err)
}
}

return nil
}

func (p *disasmProgram) unmarshalCasmFileStep() error {
data, err := os.ReadFile(p.pathToFile)
if err != nil {
return err
}
if err := json.Unmarshal(data, &p.rawCasm); err != nil {
return err
}
return nil
}

func (p *disasmProgram) loadBytecodeStep() error {
// Since different versions of CASM files may store bytecode at different places
// (e.g. "data" in Cairo0 and "bytecode" in Cairo1),
// we allow the user to specify the bytecode array location.
// By default, this value will be equal to the default supported version location
// (Cairo0 for now and Cairo1 in the future).
keys := strings.Split(p.bytecodeKey, ".")

v := lookupKeys(p.rawCasm, keys...)
if v == nil {
return fmt.Errorf("key %q doesn't lead to a bytecode", p.bytecodeKey)
}

slice, ok := v.([]any)
if !ok {
return fmt.Errorf("%q: expected a slice of strings", p.bytecodeKey)
}

p.bytecode = make([]*f.Element, 0, len(slice))
for i, s := range slice {
s, ok := s.(string)
if !ok {
return fmt.Errorf("%q: expected a slice of strings, found %T", p.bytecodeKey, slice[i])
}
felt, err := new(f.Element).SetString(s)
if err != nil {
return fmt.Errorf("%q[%d]: parse %q: %w", p.bytecodeKey, i, s, err)
}
p.bytecode = append(p.bytecode, felt)
}

return nil
}

func (p *disasmProgram) disassembleStep() error {
prog, err := disasm.FromBytecode(disasm.Config{
Bytecode: p.bytecode,
Indent: 4,
})
if err != nil {
return err
}
p.disassembled = prog
return nil
}

func (p *disasmProgram) printStep() error {
for _, l := range p.disassembled.Lines {
if len(l.Text) == 0 {
fmt.Printf("// %s\n", strings.Join(l.Comments, "; "))
continue
}
if len(l.Comments) == 0 {
fmt.Printf("%s;\n", l.Text)
} else {
fmt.Printf("%s; // %s\n", l.Text, strings.Join(l.Comments, "; "))
}
}
return nil
}
77 changes: 77 additions & 0 deletions cmd/casm-inspect/inst_fields.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package main

import (
"errors"
"fmt"
"strings"

f "github.com/consensys/gnark-crypto/ecc/stark-curve/fp"
"github.com/urfave/cli/v2"
)

// instFieldsProgram implements an "inst-fields" subcommand.
type instFieldsProgram struct{}

func (p *instFieldsProgram) Action(ctx *cli.Context) error {
s := ctx.Args().Get(0)
if s == "" {
return errors.New("expected 1 non-empty positional argument")
}

felt, err := new(f.Element).SetString(s)
if err != nil {
return fmt.Errorf("parsing %q argument: %w", s, err)
}
if !felt.IsUint64() {
return errors.New("instruction bytes overflow uint64")
}

u64 := felt.Uint64()

fmt.Printf("uint64 value: %v\n", u64)

// We don't use the assembler's package code here to make it possible
// to use this dumper tool even if assembler package can't validate
// the input. Unlike the assembler package, this tool doesn't care
// if the provided bits are valid or not.
// It will split them into "fields" expected by the CASM instruction encoding.

type instField struct {
name string
width int // in bits
signed bool
}
encodingList := []instField{
{"off_dst", 16, true},
{"off_op0", 16, true},
{"off_op1", 16, true},
{"dst_reg", 1, false},
{"op0_reg", 1, false},
{"op1_src", 3, false},
{"res_logic", 2, false},
{"pc_update", 3, false},
{"ap_update", 2, false},
{"opcode", 3, false},
}

const onesMask = ^uint64(0)

var chunks []string

offset := int(0)
for _, field := range encodingList {
mask := onesMask >> (64 - field.width)
fieldBits := (u64 >> offset) & mask
if field.signed {
fmt.Printf("%s: %v (%b)\n", field.name, int16(fieldBits), fieldBits)
} else {
fmt.Printf("%s: %v (%b)\n", field.name, fieldBits, fieldBits)
}
chunks = append(chunks, fmt.Sprintf("%b", fieldBits))
offset += field.width
}

fmt.Printf("bits: %s\n", strings.Join(chunks, " "))

return nil
}
50 changes: 50 additions & 0 deletions cmd/casm-inspect/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"fmt"
"os"

"github.com/urfave/cli/v2"
)

func main() {
disasm := &disasmProgram{}
instFields := &instFieldsProgram{}

app := &cli.App{
Name: "casm-inspect",
Usage: "casm-inspect <subcmd> [args...]",
Description: "A cairo zero file inspector",
EnableBashCompletion: true,
Suggest: true,
DefaultCommand: "help",
Commands: []*cli.Command{
{
Name: "inst-fields",
Usage: "inst-fields 0xa0680017fff8000",
Description: "print CASM instruction fields",
Action: instFields.Action,
},
{
Name: "disasm",
Usage: "disasm compiled_cairo0.json",
Description: "disassemble the casm from the compiled cairo program",
Action: disasm.Action,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "bytecode",
Usage: "a JSON key containing CASM bytecode (period-separated for multi-keys)",
Required: false,
Value: "data",
Destination: &disasm.bytecodeKey,
},
},
},
},
}

if err := app.Run(os.Args); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
16 changes: 16 additions & 0 deletions cmd/casm-inspect/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package main

// lookupKeys performs a multi-level map search given a list of keys to query.
// Given a map like {"a": {"b": {"c": 10}}} and keys ["a", "b", "c"] this
// function will return 10 (a value of the deepest lookup).
func lookupKeys(m map[string]any, keys ...string) any {
var current any = m
for _, k := range keys {
asMap, ok := current.(map[string]any)
if !ok {
return nil
}
current = asMap[k]
}
return current
}
36 changes: 36 additions & 0 deletions pkg/disasm/casm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package disasm

import (
"github.com/NethermindEth/cairo-vm-go/pkg/assembler"
f "github.com/consensys/gnark-crypto/ecc/stark-curve/fp"
)

type casmInstruction struct {
*assembler.Instruction

arg *f.Element
bytecodeOffset int64
}

func (inst *casmInstruction) JumpTarget() (int64, bool) {
if inst.Opcode == assembler.OpCodeRet {
return 0, false
}
if inst.PcUpdate == assembler.PcUpdateNextInstr {
return 0, false
}

offset := feltToInt64(inst.arg)
if inst.PcUpdate == assembler.PcUpdateJump {
return offset, true
}
return inst.bytecodeOffset + offset, true
}

func (inst *casmInstruction) Size() int64 {
// Note: OpCodeCall also has an immediate (call target).
if inst.Op1Source == assembler.Imm {
return 2
}
return 1
}
127 changes: 127 additions & 0 deletions pkg/disasm/casm_formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package disasm

import (
"bytes"
"fmt"
"io"
"strings"

"github.com/NethermindEth/cairo-vm-go/pkg/assembler"
)

type casmFormatter struct {
labels map[int64]int
funcLabels map[int64]int
}

func (cf *casmFormatter) printInstruction(w io.Writer, inst casmInstruction) error {
var buf bytes.Buffer

switch inst.Opcode {
case assembler.OpCodeRet:
buf.WriteString("ret")

case assembler.OpCodeCall:
var callSuffix string
switch inst.PcUpdate {
case assembler.PcUpdateJump:
callSuffix = "abs"
case assembler.PcUpdateJumpRel:
callSuffix = "rel"
}
fmt.Fprintf(&buf, "call %s %+d", callSuffix, feltToInt64(inst.arg))

case assembler.OpCodeAssertEq:
buf.WriteString(cf.formatMemoryOperand(inst.DstRegister, int(inst.OffDest)))
buf.WriteString(" = ")

if inst.Res != assembler.Op1 {
buf.WriteString(cf.formatMemoryOperand(inst.Op0Register, int(inst.OffOp0)))
}
switch inst.Res {
case assembler.AddOperands:
buf.WriteString(" + ")
case assembler.MulOperands:
buf.WriteString(" * ")
}

buf.WriteString(cf.formatOperand1(inst))

case assembler.OpCodeNop:
// Jumps use the same opcode=0.
switch inst.PcUpdate {
case assembler.PcUpdateJump:
buf.WriteString("jmp abs " + cf.formatOperand1(inst))
case assembler.PcUpdateJumpRel:
buf.WriteString("jmp rel " + cf.formatOperand1(inst))
case assembler.PcUpdateJnz:
fmt.Fprintf(&buf, "jmp rel %s if %s != 0",
cf.formatOperand1(inst),
cf.formatMemoryOperand(inst.DstRegister, int(inst.OffDest)))
}
if inst.ApUpdate == assembler.AddRes && inst.Op1Source == assembler.Imm {
// This is an "ap_add" pseudo-instruction encoding.
//
// See Display impl for AddApInstruction type in instruction.rs;
// https://github.com/starkware-libs/cairo/blob/797781d8365445ad4a1ae8202881a61b6656bb98/crates/cairo-lang-casm/src/instructions.rs#L198
fmt.Fprintf(&buf, "ap += %d", feltToInt64(inst.arg))
}

default:
return fmt.Errorf("unexpected opcode: %v", inst.Opcode)
}

switch inst.ApUpdate {
case assembler.Add1:
buf.WriteString(", ap++")
}

_, err := w.Write(buf.Bytes())
return err
}

func (cf *casmFormatter) formatOperand1(inst casmInstruction) string {
var buf strings.Builder

switch inst.Op1Source {
case assembler.ApPlusOffOp1:
buf.WriteString(cf.formatMemoryOperand(assembler.Ap, int(inst.OffOp1)))
case assembler.FpPlusOffOp1:
buf.WriteString(cf.formatMemoryOperand(assembler.Fp, int(inst.OffOp1)))
case assembler.Imm:
buf.WriteString(inst.arg.String())
case assembler.Op0:
// Things like [[fp+10]+20].
buf.WriteString(cf.formatMemoryOperand2(inst.Op0Register, int(inst.OffOp0), int(inst.OffOp1)))
}

return buf.String()
}

func (cf *casmFormatter) formatMemoryOperand(reg assembler.Register, offset int) string {
var buf strings.Builder
buf.WriteString("[")
buf.WriteString(strings.ToLower(reg.String()))
if offset != 0 {
fmt.Fprintf(&buf, "%+d", offset)
}
buf.WriteString("]")
return buf.String()
}

func (cf *casmFormatter) formatMemoryOperand2(reg assembler.Register, offset, offset2 int) string {
var buf strings.Builder

buf.WriteString("[[")
buf.WriteString(strings.ToLower(reg.String()))
if offset != 0 {
fmt.Fprintf(&buf, "%+d", offset)
}
buf.WriteString("]")
if offset2 != 0 {
fmt.Fprintf(&buf, "%+d", offset2)
}
buf.WriteString("]")

return buf.String()
}
28 changes: 28 additions & 0 deletions pkg/disasm/disasm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package disasm

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

type Program struct {
Lines []Line
}

type Line struct {
Text string
Comments []string
}

type Config struct {
Bytecode []*f.Element

Indent int
}

func FromBytecode(config Config) (*Program, error) {
d := &disassembler{
bytecode: config.Bytecode,
config: config,
}
return d.Disassemble()
}
89 changes: 89 additions & 0 deletions pkg/disasm/disasm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package disasm_test

import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"

"github.com/NethermindEth/cairo-vm-go/pkg/assembler"
"github.com/NethermindEth/cairo-vm-go/pkg/disasm"
"github.com/stretchr/testify/assert"
)

func TestDisasm(t *testing.T) {
tests, err := os.ReadDir("testdata")
if err != nil {
t.Fatal(err)
}

// This code tests both CASM parser and disassembler.
// * We take some pre-existing CASM file from the test suite
// * Then it's parsed by our assembler package (instList)
// * The result of our parser is used as a disasm input
// * The disassembly is then stringified to get a new CASM input file
// * This new CASM file is given to our assembler package (instList2)
// * As a final step, instList and instList2 slices are compared
//
// If they're equal, it's a good sign and we have a high chance
// of asm+disasm not losing important information.
//
// We could also check for testCode and testCode2 being identical,
// but apart from the formatting issues (easy to solve) there are
// equivalent ways to write some expressions, e.g.:
// > [fp + -3] vs [fp - 3]
// > [ap+0] vs [ap]
// Both of them encode the same dereference expr, just spelled differently.
// But anyway, with an extra flag we can mark some of the tests as
// "ok to check for textual equallity" (see checkTexts map below).

checkTexts := map[string]struct{}{
"simple.casm": {},
"hash_chain_pretty.casm": {},
}

disasmProgToString := func(p *disasm.Program) string {
// Ignore the comments.
var buf strings.Builder
for _, l := range p.Lines {
if len(l.Text) == 0 {
// This is a comment-only line.
continue
}
fmt.Fprintf(&buf, "%s;\n", l.Text)
}
return buf.String()
}

for _, test := range tests {
testName := filepath.Base(test.Name())
testFile := filepath.Join("testdata", test.Name())
t.Run(testName, func(t *testing.T) {
testCode, err := os.ReadFile(testFile)
if err != nil {
t.Fatal(err)
}
instList, err := assembler.CasmToBytecode(string(testCode))
if err != nil {
t.Fatal(err)
}
disassembled, err := disasm.FromBytecode(disasm.Config{
Bytecode: instList,
Indent: 0,
})
if err != nil {
t.Fatal(err)
}
testCode2 := disasmProgToString(disassembled)
instList2, err := assembler.CasmToBytecode(testCode2)
if err != nil {
t.Fatalf("generated casm parse error: %v\nprog:\n%s", err, testCode2)
}
assert.Equal(t, instList, instList2)
if _, ok := checkTexts[testName]; ok {
assert.Equal(t, string(testCode), testCode2)
}
})
}
}
209 changes: 209 additions & 0 deletions pkg/disasm/disassembler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package disasm

import (
"fmt"
"strings"

"github.com/NethermindEth/cairo-vm-go/pkg/assembler"
f "github.com/consensys/gnark-crypto/ecc/stark-curve/fp"
)

type disassembler struct {
bytecode []*f.Element

config Config

instructions []casmInstruction

labels map[int64]int
funcLabels map[int64]int

formatter *casmFormatter

prog Program
}

func (d *disassembler) Disassemble() (*Program, error) {
type step struct {
name string
fn func() error
}
steps := []step{
{"decode instructions", d.decodeInstructionsStep},
{"collect labels", d.collectLabelsStep},
{"construct program", d.constructProgramStep},
}
for _, s := range steps {
if err := s.fn(); err != nil {
return nil, fmt.Errorf("%s: %w", s.name, err)
}
}

return &d.prog, nil
}

func (d *disassembler) decodeInstructionsStep() error {
offset := int64(0)

for offset < int64(len(d.bytecode)) {
b := d.bytecode[offset]
decoded, err := assembler.DecodeInstruction(b)
if err != nil {
return fmt.Errorf("offset %d (%q): %w", offset, b.String(), err)
}
inst := casmInstruction{
Instruction: decoded,
bytecodeOffset: offset,
}
if inst.Size() == 2 {
inst.arg = d.bytecode[offset+1]
}
d.instructions = append(d.instructions, inst)
offset += inst.Size()
}

return nil
}

func (d *disassembler) collectLabelsStep() error {
d.labels = make(map[int64]int)
d.funcLabels = make(map[int64]int)

for _, inst := range d.instructions {
jumpTarget, ok := inst.JumpTarget()
if !ok {
continue
}
labels := d.labels
if inst.Opcode == assembler.OpCodeCall {
labels = d.funcLabels
}
if _, ok := labels[jumpTarget]; ok {
continue
}
id := len(labels)
labels[jumpTarget] = id
}

return nil
}

func (d *disassembler) constructProgramStep() error {
d.formatter = &casmFormatter{
labels: d.labels,
funcLabels: d.funcLabels,
}

for _, inst := range d.instructions {
funcLabelID, ok := d.funcLabels[inst.bytecodeOffset]
if ok {
d.pushCommentLine("<F%d> (pc=%d)", funcLabelID, inst.bytecodeOffset)
}
labelID, ok := d.labels[inst.bytecodeOffset]
if ok {
d.pushCommentLine("<L%d> (pc=%d)", labelID, inst.bytecodeOffset)
}
if err := d.pushDisasmLine(inst); err != nil {
return err
}
}

return nil
}

func (d *disassembler) pushDisasmLine(inst casmInstruction) error {
var buf strings.Builder

buf.WriteString(strings.Repeat(" ", d.config.Indent))

if err := d.formatter.printInstruction(&buf, inst); err != nil {
return err
}

comments, err := d.collectInstComments(inst)
if err != nil {
return err
}

d.prog.Lines = append(d.prog.Lines, Line{
Text: buf.String(),
Comments: comments,
})

return nil
}

func (d *disassembler) pushCommentLine(format string, args ...any) {
s := fmt.Sprintf(format, args...)
d.prog.Lines = append(d.prog.Lines, Line{
Comments: []string{s},
})
}

func (d *disassembler) collectInstComments(inst casmInstruction) ([]string, error) {
var comments []string

isAddApPseudo := false

switch inst.Opcode {
case assembler.OpCodeCall:
jumpTarget, _ := inst.JumpTarget()
if funcLabelID, ok := d.funcLabels[jumpTarget]; ok {
comments = append(comments, fmt.Sprintf("calls F%d", funcLabelID))
}

case assembler.OpCodeAssertEq:
if inst.Op1Source == assembler.Imm {
// Try to recognize the division.
// So, instead of just this:
// > assert [fp+1] = [fp] * 2894802230932904970957858226476056084498485772265277359978473644908697616385
// ...the user sees this (note the comment):
// > assert [fp+1] = [fp] * 2894802230932904970957858226476056084498485772265277359978473644908697616385 // div 5
imm := inst.arg
if inst.Res == assembler.MulOperands && !imm.IsUint64() {
dividend := f.NewElement(0)
dividend.Inverse(imm)
// If divident is a very large number, then we could got it wrong.
if dividend.IsUint64() {
comments = append(comments, "div "+dividend.String())
}
}
}

case assembler.OpCodeNop:
jumpTarget, ok := inst.JumpTarget()
if ok {
if labelID, ok := d.labels[jumpTarget]; ok {
comments = append(comments, fmt.Sprintf("targets L%d", labelID))
}
}
if inst.ApUpdate == assembler.AddRes && inst.Op1Source == assembler.Imm {
isAddApPseudo = true
}

case assembler.OpCodeRet:
// Nothing to do.

default:
return nil, fmt.Errorf("unexpected opcode: %v", inst.Opcode)
}

switch inst.ApUpdate {
case assembler.Add2:
comments = append(comments, "ap += 2")
case assembler.AddRes:
if !isAddApPseudo {
if inst.Op1Source == assembler.Imm {
comments = append(comments, fmt.Sprintf("ap += %d", feltToInt64(inst.arg)))
} else {
comments = append(comments, "ap += $result")
}
}
case assembler.SameAp, assembler.Add1:
// Nothing to do.
default:
return nil, fmt.Errorf("unexpected ap update: %v", inst.ApUpdate)
}

return comments, nil
}
8 changes: 8 additions & 0 deletions pkg/disasm/testdata/fib.casm
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
jmp rel 4 if [fp + -3] != 0;
[ap + 0] = [fp + -5], ap++;
ret;
[ap + 0] = [fp + -4], ap++;
[ap + 0] = [fp + -5] + [fp + -4], ap++;
[fp + -3] = [ap + 0] + 1, ap++;
call rel -8;
ret;
12 changes: 12 additions & 0 deletions pkg/disasm/testdata/hash_chain.casm
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
jmp rel 6 if [fp + -3] != 0;
[ap + 0] = [fp + -4], ap++;
[ap + 0] = 0, ap++;
ret;
[ap + 0] = [fp + -4], ap++;
[fp + -3] = [ap + 0] + 1, ap++;
call rel -9;
[ap + -1] = [[ap + -2] + 0];
[fp + -3] = [[ap + -2] + 1];
[ap + 0] = [ap + -2] + 3, ap++;
[ap + 0] = [[ap + -3] + 2], ap++;
ret;
12 changes: 12 additions & 0 deletions pkg/disasm/testdata/hash_chain_pretty.casm
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
jmp rel 6 if [fp-3] != 0;
[ap] = [fp-4], ap++;
[ap] = 0, ap++;
ret;
[ap] = [fp-4], ap++;
[fp-3] = [ap] + 1, ap++;
call rel -9;
[ap-1] = [[ap-2]];
[fp-3] = [[ap-2]+1];
[ap] = [ap-2] + 3, ap++;
[ap] = [[ap-3]+2], ap++;
ret;
1 change: 1 addition & 0 deletions pkg/disasm/testdata/simple.casm
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ret;
24 changes: 24 additions & 0 deletions pkg/disasm/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package disasm

import (
"strconv"

f "github.com/consensys/gnark-crypto/ecc/stark-curve/fp"
)

func feltToInt64(felt *f.Element) int64 {
// This would not be correct: int64(felt.Uint64)
// since signed values will reside in more than one 64-bit word.
//
// BigInt().Int64() would not work neither.
//
// String() handles signed values pretty well for our use-case.
// Maybe there is another way to avoid the redundant String()+Parsing?

s := felt.String()
v, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0
}
return v
}