OSDN Git Service

cp code from bytom repo
authorpaladz <453256728@qq.com>
Mon, 20 Aug 2018 08:43:12 +0000 (16:43 +0800)
committerpaladz <453256728@qq.com>
Mon, 20 Aug 2018 08:43:12 +0000 (16:43 +0800)
16 files changed:
compiler/ast.go [new file with mode: 0644]
compiler/builder.go [new file with mode: 0644]
compiler/builder_test.go [new file with mode: 0644]
compiler/builtins.go [new file with mode: 0644]
compiler/checks.go [new file with mode: 0644]
compiler/checks_test.go [new file with mode: 0644]
compiler/cmd/equitycmd/equitycmd.go [new file with mode: 0644]
compiler/compile.go [new file with mode: 0644]
compiler/compile_test.go [new file with mode: 0644]
compiler/doc.go [new file with mode: 0644]
compiler/environ.go [new file with mode: 0644]
compiler/equitytest/equitytest.go [new file with mode: 0644]
compiler/optimize.go [new file with mode: 0644]
compiler/parse.go [new file with mode: 0644]
compiler/stack.go [new file with mode: 0644]
compiler/types.go [new file with mode: 0644]

diff --git a/compiler/ast.go b/compiler/ast.go
new file mode 100644 (file)
index 0000000..8eea5a0
--- /dev/null
@@ -0,0 +1,314 @@
+package compiler
+
+import (
+       "encoding/hex"
+       "fmt"
+       "strconv"
+       "strings"
+
+       chainjson "github.com/bytom/encoding/json"
+)
+
+// Contract is a compiled Equity contract.
+type Contract struct {
+       // Name is the contract name.
+       Name string `json:"name"`
+
+       // Params is the list of contract parameters.
+       Params []*Param `json:"params,omitempty"`
+
+       // Clauses is the list of contract clauses.
+       Clauses []*Clause `json:"clauses"`
+
+       // Value is the name of the value locked by the contract.
+       Value string `json:"value"`
+
+       // Body is the optimized bytecode of the contract body. This is not
+       // a complete program!  Use instantiate to turn this (plus some
+       // arguments) into a program.
+       Body chainjson.HexBytes `json:"body_bytecode"`
+
+       // Opcodes is the human-readable string of opcodes corresponding to
+       // Body.
+       Opcodes string `json:"body_opcodes,omitempty"`
+
+       // Recursive tells whether this contract calls itself.  (This is
+       // used to select between two possible instantiation options.)
+       Recursive bool `json:"recursive"`
+
+       // Pre-optimized list of instruction steps, with stack snapshots.
+       Steps []Step `json:"-"`
+}
+
+// Param is a contract or clause parameter.
+type Param struct {
+       // Name is the parameter name.
+       Name string `json:"name"`
+
+       // Type is the declared parameter type.
+       Type typeDesc `json:"type"`
+
+       // InferredType, if available, is a more-specific type than Type,
+       // inferred from the logic of the contract.
+       InferredType typeDesc `json:"inferred_type,omitempty"`
+}
+
+// Clause is a compiled contract clause.
+type Clause struct {
+       // Name is the clause name.
+       Name string `json:"name"`
+
+       // Params is the list of clause parameters.
+       Params []*Param `json:"params,omitempty"`
+
+       // Reqs is the list of requirements (from the clause's "requires"
+       // section).
+       Reqs []*ClauseReq `json:"reqs,omitempty"`
+
+       statements []statement
+
+       // BlockHeight is the list of expressions passed to greater()/less() in this
+       // clause.
+       BlockHeight []string `json:"blockheight,omitempty"`
+
+       // HashCalls is the list of hash functions and their arguments used
+       // in this clause.
+       HashCalls []HashCall `json:"hash_calls,omitempty"`
+
+       // Values is the list of values unlocked or relocked in this clause.
+       Values []ValueInfo `json:"values"`
+
+       // Contracts is the list of contracts called by this clause.
+       Contracts []string `json:"contracts,omitempty"`
+}
+
+// HashCall describes a call to a hash function.
+type HashCall struct {
+       // HashType is "sha3" or "sha256".
+       HashType string `json:"hash_type"`
+
+       // Arg is the expression passed to the hash function.
+       Arg string `json:"arg"`
+
+       // ArgType is the type of Arg.
+       ArgType string `json:"arg_type"`
+}
+
+// ClauseReq describes a payment requirement of a clause (one of the
+// things after the "requires" keyword).
+type ClauseReq struct {
+       Name string `json:"name"`
+
+       assetExpr, amountExpr expression
+
+       // Asset is the expression describing the required asset.
+       Asset string `json:"asset"`
+
+       // Amount is the expression describing the required amount.
+       Amount string `json:"amount"`
+}
+
+type statement interface {
+       countVarRefs(map[string]int)
+}
+
+type verifyStatement struct {
+       expr expression
+}
+
+func (s verifyStatement) countVarRefs(counts map[string]int) {
+       s.expr.countVarRefs(counts)
+}
+
+type lockStatement struct {
+       locked  expression
+       program expression
+
+       // Added as a decoration, used by CHECKOUTPUT
+       index int64
+}
+
+func (s lockStatement) countVarRefs(counts map[string]int) {
+       s.locked.countVarRefs(counts)
+       s.program.countVarRefs(counts)
+}
+
+type unlockStatement struct {
+       expr expression
+}
+
+func (s unlockStatement) countVarRefs(counts map[string]int) {
+       s.expr.countVarRefs(counts)
+}
+
+type expression interface {
+       String() string
+       typ(*environ) typeDesc
+       countVarRefs(map[string]int)
+}
+
+type binaryExpr struct {
+       left, right expression
+       op          *binaryOp
+}
+
+func (e binaryExpr) String() string {
+       return fmt.Sprintf("(%s %s %s)", e.left, e.op.op, e.right)
+}
+
+func (e binaryExpr) typ(*environ) typeDesc {
+       return e.op.result
+}
+
+func (e binaryExpr) countVarRefs(counts map[string]int) {
+       e.left.countVarRefs(counts)
+       e.right.countVarRefs(counts)
+}
+
+type unaryExpr struct {
+       op   *unaryOp
+       expr expression
+}
+
+func (e unaryExpr) String() string {
+       return fmt.Sprintf("%s%s", e.op.op, e.expr)
+}
+
+func (e unaryExpr) typ(*environ) typeDesc {
+       return e.op.result
+}
+
+func (e unaryExpr) countVarRefs(counts map[string]int) {
+       e.expr.countVarRefs(counts)
+}
+
+type callExpr struct {
+       fn   expression
+       args []expression
+}
+
+func (e callExpr) String() string {
+       var argStrs []string
+       for _, a := range e.args {
+               argStrs = append(argStrs, a.String())
+       }
+       return fmt.Sprintf("%s(%s)", e.fn, strings.Join(argStrs, ", "))
+}
+
+func (e callExpr) typ(env *environ) typeDesc {
+       if b := referencedBuiltin(e.fn); b != nil {
+               switch b.name {
+               case "sha3":
+                       if len(e.args) == 1 {
+                               switch e.args[0].typ(env) {
+                               case strType:
+                                       return sha3StrType
+                               case pubkeyType:
+                                       return sha3PubkeyType
+                               }
+                       }
+
+               case "sha256":
+                       if len(e.args) == 1 {
+                               switch e.args[0].typ(env) {
+                               case strType:
+                                       return sha256StrType
+                               case pubkeyType:
+                                       return sha256PubkeyType
+                               }
+                       }
+               }
+
+               return b.result
+       }
+       if e.fn.typ(env) == predType {
+               return boolType
+       }
+       if e.fn.typ(env) == contractType {
+               return progType
+       }
+       return nilType
+}
+
+func (e callExpr) countVarRefs(counts map[string]int) {
+       e.fn.countVarRefs(counts)
+       for _, a := range e.args {
+               a.countVarRefs(counts)
+       }
+}
+
+type varRef string
+
+func (v varRef) String() string {
+       return string(v)
+}
+
+func (e varRef) typ(env *environ) typeDesc {
+       if entry := env.lookup(string(e)); entry != nil {
+               return entry.t
+       }
+       return nilType
+}
+
+func (e varRef) countVarRefs(counts map[string]int) {
+       counts[string(e)]++
+}
+
+type bytesLiteral []byte
+
+func (e bytesLiteral) String() string {
+       return "0x" + hex.EncodeToString([]byte(e))
+}
+
+func (bytesLiteral) typ(*environ) typeDesc {
+       return "String"
+}
+
+func (bytesLiteral) countVarRefs(map[string]int) {}
+
+type integerLiteral int64
+
+func (e integerLiteral) String() string {
+       return strconv.FormatInt(int64(e), 10)
+}
+
+func (integerLiteral) typ(*environ) typeDesc {
+       return "Integer"
+}
+
+func (integerLiteral) countVarRefs(map[string]int) {}
+
+type booleanLiteral bool
+
+func (e booleanLiteral) String() string {
+       if e {
+               return "true"
+       }
+       return "false"
+}
+
+func (booleanLiteral) typ(*environ) typeDesc {
+       return "Boolean"
+}
+
+func (booleanLiteral) countVarRefs(map[string]int) {}
+
+type listExpr []expression
+
+func (e listExpr) String() string {
+       var elts []string
+       for _, elt := range e {
+               elts = append(elts, elt.String())
+       }
+       return fmt.Sprintf("[%s]", strings.Join(elts, ", "))
+}
+
+func (listExpr) typ(*environ) typeDesc {
+       return "List"
+}
+
+func (e listExpr) countVarRefs(counts map[string]int) {
+       for _, elt := range e {
+               elt.countVarRefs(counts)
+       }
+}
diff --git a/compiler/builder.go b/compiler/builder.go
new file mode 100644 (file)
index 0000000..757f9b6
--- /dev/null
@@ -0,0 +1,210 @@
+package compiler
+
+import (
+       "fmt"
+       "strconv"
+       "strings"
+)
+
+type builder struct {
+       items         []*builderItem
+       pendingVerify *builderItem
+}
+
+type builderItem struct {
+       opcodes string
+       stk     stack
+}
+
+func (b *builder) add(opcodes string, newstack stack) stack {
+       if b.pendingVerify != nil {
+               b.items = append(b.items, b.pendingVerify)
+               b.pendingVerify = nil
+       }
+       item := &builderItem{opcodes: opcodes, stk: newstack}
+       if opcodes == "VERIFY" {
+               b.pendingVerify = item
+       } else {
+               b.items = append(b.items, item)
+       }
+       return newstack
+}
+
+func (b *builder) addRoll(stk stack, n int) stack {
+       b.addInt64(stk, int64(n))
+       return b.add("ROLL", stk.roll(n))
+}
+
+func (b *builder) addDup(stk stack) stack {
+       return b.add("DUP", stk.dup())
+}
+
+func (b *builder) addInt64(stk stack, n int64) stack {
+       s := strconv.FormatInt(n, 10)
+       return b.add(s, stk.add(s))
+}
+
+func (b *builder) addNumEqual(stk stack, desc string) stack {
+       return b.add("NUMEQUAL", stk.dropN(2).add(desc))
+}
+
+func (b *builder) addJumpIf(stk stack, label string) stack {
+       return b.add(fmt.Sprintf("JUMPIF:$%s", label), stk.drop())
+}
+
+func (b *builder) addJumpTarget(stk stack, label string) stack {
+       return b.add("$"+label, stk)
+}
+
+func (b *builder) addDrop(stk stack) stack {
+       return b.add("DROP", stk.drop())
+}
+
+func (b *builder) forgetPendingVerify() {
+       b.pendingVerify = nil
+}
+
+func (b *builder) addJump(stk stack, label string) stack {
+       return b.add(fmt.Sprintf("JUMP:$%s", label), stk)
+}
+
+func (b *builder) addVerify(stk stack) stack {
+       return b.add("VERIFY", stk.drop())
+}
+
+func (b *builder) addData(stk stack, data []byte) stack {
+       var s string
+       switch len(data) {
+       case 0:
+               s = "0"
+       case 1:
+               s = strconv.FormatInt(int64(data[0]), 10)
+       default:
+               s = fmt.Sprintf("0x%x", data)
+       }
+       return b.add(s, stk.add(s))
+}
+
+func (b *builder) addAmount(stk stack) stack {
+       return b.add("AMOUNT", stk.add("<amount>"))
+}
+
+func (b *builder) addAsset(stk stack) stack {
+       return b.add("ASSET", stk.add("<asset>"))
+}
+
+func (b *builder) addCheckOutput(stk stack, desc string) stack {
+       return b.add("CHECKOUTPUT", stk.dropN(5).add(desc))
+}
+
+func (b *builder) addBoolean(stk stack, val bool) stack {
+       if val {
+               return b.add("TRUE", stk.add("true"))
+       }
+       return b.add("FALSE", stk.add("false"))
+}
+
+func (b *builder) addOps(stk stack, ops string, desc string) stack {
+       return b.add(ops, stk.add(desc))
+}
+
+func (b *builder) addToAltStack(stk stack) (stack, string) {
+       t := stk.top()
+       return b.add("TOALTSTACK", stk.drop()), t
+}
+
+func (b *builder) addTxSigHash(stk stack) stack {
+       return b.add("TXSIGHASH", stk.add("<txsighash>"))
+}
+
+func (b *builder) addFromAltStack(stk stack, alt string) stack {
+       return b.add("FROMALTSTACK", stk.add(alt))
+}
+
+func (b *builder) addSwap(stk stack) stack {
+       return b.add("SWAP", stk.swap())
+}
+
+func (b *builder) addCheckMultisig(stk stack, n int, desc string) stack {
+       return b.add("CHECKMULTISIG", stk.dropN(n).add(desc))
+}
+
+func (b *builder) addOver(stk stack) stack {
+       return b.add("OVER", stk.over())
+}
+
+func (b *builder) addPick(stk stack, n int) stack {
+       b.addInt64(stk, int64(n))
+       return b.add("PICK", stk.pick(n))
+}
+
+func (b *builder) addCatPushdata(stk stack, desc string) stack {
+       return b.add("CATPUSHDATA", stk.dropN(2).add(desc))
+}
+
+func (b *builder) addCat(stk stack, desc string) stack {
+       return b.add("CAT", stk.dropN(2).add(desc))
+}
+
+func (b *builder) opcodes() string {
+       var ops []string
+       for _, item := range b.items {
+               ops = append(ops, item.opcodes)
+       }
+       return strings.Join(ops, " ")
+}
+
+// This is for producing listings like:
+// 5                 |  [... <clause selector> borrower lender deadline balanceAmount balanceAsset 5]
+// ROLL              |  [... borrower lender deadline balanceAmount balanceAsset <clause selector>]
+// JUMPIF:$default   |  [... borrower lender deadline balanceAmount balanceAsset]
+// $repay            |  [... borrower lender deadline balanceAmount balanceAsset]
+// 0                 |  [... borrower lender deadline balanceAmount balanceAsset 0]
+// 0                 |  [... borrower lender deadline balanceAmount balanceAsset 0 0]
+// 3                 |  [... borrower lender deadline balanceAmount balanceAsset 0 0 3]
+// ROLL              |  [... borrower lender deadline balanceAsset 0 0 balanceAmount]
+// 3                 |  [... borrower lender deadline balanceAsset 0 0 balanceAmount 3]
+// ROLL              |  [... borrower lender deadline 0 0 balanceAmount balanceAsset]
+// 1                 |  [... borrower lender deadline 0 0 balanceAmount balanceAsset 1]
+// 6                 |  [... borrower lender deadline 0 0 balanceAmount balanceAsset 1 6]
+// ROLL              |  [... borrower deadline 0 0 balanceAmount balanceAsset 1 lender]
+// CHECKOUTPUT       |  [... borrower deadline checkOutput(payment, lender)]
+// VERIFY            |  [... borrower deadline]
+// 1                 |  [... borrower deadline 1]
+// 0                 |  [... borrower deadline 1 0]
+// AMOUNT            |  [... borrower deadline 1 0 <amount>]
+// ASSET             |  [... borrower deadline 1 0 <amount> <asset>]
+// 1                 |  [... borrower deadline 1 0 <amount> <asset> 1]
+// 6                 |  [... borrower deadline 1 0 <amount> <asset> 1 6]
+// ROLL              |  [... deadline 1 0 <amount> <asset> 1 borrower]
+// CHECKOUTPUT       |  [... deadline checkOutput(collateral, borrower)]
+// JUMP:$_end        |  [... borrower lender deadline balanceAmount balanceAsset]
+// $default          |  [... borrower lender deadline balanceAmount balanceAsset]
+// 2                 |  [... borrower lender deadline balanceAmount balanceAsset 2]
+// ROLL              |  [... borrower lender balanceAmount balanceAsset deadline]
+// MINTIME LESSTHAN  |  [... borrower lender balanceAmount balanceAsset after(deadline)]
+// VERIFY            |  [... borrower lender balanceAmount balanceAsset]
+// 0                 |  [... borrower lender balanceAmount balanceAsset 0]
+// 0                 |  [... borrower lender balanceAmount balanceAsset 0 0]
+// AMOUNT            |  [... borrower lender balanceAmount balanceAsset 0 0 <amount>]
+// ASSET             |  [... borrower lender balanceAmount balanceAsset 0 0 <amount> <asset>]
+// 1                 |  [... borrower lender balanceAmount balanceAsset 0 0 <amount> <asset> 1]
+// 7                 |  [... borrower lender balanceAmount balanceAsset 0 0 <amount> <asset> 1 7]
+// ROLL              |  [... borrower balanceAmount balanceAsset 0 0 <amount> <asset> 1 lender]
+// CHECKOUTPUT       |  [... borrower balanceAmount balanceAsset checkOutput(collateral, lender)]
+// $_end             |  [... borrower lender deadline balanceAmount balanceAsset]
+
+type (
+       Step struct {
+               Opcodes string `json:"opcodes"`
+               Stack   string `json:"stack"`
+       }
+)
+
+func (b *builder) steps() []Step {
+       var result []Step
+       for _, item := range b.items {
+               result = append(result, Step{item.opcodes, item.stk.String()})
+       }
+       return result
+}
diff --git a/compiler/builder_test.go b/compiler/builder_test.go
new file mode 100644 (file)
index 0000000..63ec6b1
--- /dev/null
@@ -0,0 +1,51 @@
+package compiler
+
+// func TestBuilder(t *testing.T) {
+//     cases := []struct {
+//             name    string
+//             f       func(*builder)
+//             wantHex string
+//     }{
+//             {
+//                     "single pushdata",
+//                     func(b *builder) {
+//                             b.addInt64(1)
+//                     },
+//                     "51",
+//             },
+//             {
+//                     "pushdata and verify",
+//                     func(b *builder) {
+//                             b.addInt64(1)
+//                             b.addOp(vm.OP_VERIFY)
+//                     },
+//                     "51",
+//             },
+//             {
+//                     "pushdata, verify, second pushdata",
+//                     func(b *builder) {
+//                             b.addInt64(1)
+//                             b.addOp(vm.OP_VERIFY)
+//                             b.addInt64(2)
+//                     },
+//                     "516952",
+//             },
+//     }
+//     for _, c := range cases {
+//             t.Run(c.name, func(t *testing.T) {
+//                     b := newBuilder()
+//                     c.f(b)
+//                     got, err := b.build()
+//                     if err != nil {
+//                             t.Fatal(err)
+//                     }
+//                     want, err := hex.DecodeString(c.wantHex)
+//                     if err != nil {
+//                             t.Fatal(err)
+//                     }
+//                     if !bytes.Equal(got, want) {
+//                             t.Errorf("got %x, want %x", got, want)
+//                     }
+//             })
+//     }
+// }
diff --git a/compiler/builtins.go b/compiler/builtins.go
new file mode 100644 (file)
index 0000000..cdf4530
--- /dev/null
@@ -0,0 +1,79 @@
+package compiler
+
+type builtin struct {
+       name    string
+       opcodes string
+       args    []typeDesc
+       result  typeDesc
+}
+
+var builtins = []builtin{
+       {"sha3", "SHA3", []typeDesc{nilType}, hashType},
+       {"sha256", "SHA256", []typeDesc{nilType}, hashType},
+       {"size", "SIZE SWAP DROP", []typeDesc{nilType}, intType},
+       {"abs", "ABS", []typeDesc{intType}, intType},
+       {"min", "MIN", []typeDesc{intType, intType}, intType},
+       {"max", "MAX", []typeDesc{intType, intType}, intType},
+       {"checkTxSig", "TXSIGHASH SWAP CHECKSIG", []typeDesc{pubkeyType, sigType}, boolType},
+       {"concat", "CAT", []typeDesc{nilType, nilType}, strType},
+       {"concatpush", "CATPUSHDATA", []typeDesc{nilType, nilType}, strType},
+       {"below", "BLOCKHEIGHT GREATERTHAN", []typeDesc{intType}, boolType},
+       {"above", "BLOCKHEIGHT LESSTHAN", []typeDesc{intType}, boolType},
+       {"checkTxMultiSig", "", []typeDesc{listType, listType}, boolType}, // WARNING WARNING WOOP WOOP special case
+}
+
+type binaryOp struct {
+       op         string
+       precedence int
+       opcodes    string
+
+       left, right, result typeDesc
+}
+
+var binaryOps = []binaryOp{
+       // disjunctions disallowed (for now?)
+       // {"||", 1, "BOOLOR", "Boolean", "Boolean", "Boolean"},
+
+       // and disallow this too
+       // {"&&", 2, "BOOLAND", "Boolean", "Boolean", "Boolean"},
+
+       {">", 3, "GREATERTHAN", "Integer", "Integer", "Boolean"},
+       {"<", 3, "LESSTHAN", "Integer", "Integer", "Boolean"},
+       {">=", 3, "GREATERTHANOREQUAL", "Integer", "Integer", "Boolean"},
+       {"<=", 3, "LESSTHANOREQUAL", "Integer", "Integer", "Boolean"},
+
+       {"==", 3, "EQUAL", "", "", "Boolean"},
+       {"!=", 3, "EQUAL NOT", "", "", "Boolean"},
+
+       {"^", 4, "XOR", "", "", ""},
+       {"|", 4, "OR", "", "", ""},
+
+       {"+", 4, "ADD", "Integer", "Integer", "Integer"},
+       {"-", 4, "SUB", "Integer", "Integer", "Integer"},
+
+       // {"&^", 5, "INVERT AND", "", "", ""},
+       {"&", 5, "AND", "", "", ""},
+
+       {"<<", 5, "LSHIFT", "Integer", "Integer", "Integer"},
+       {">>", 5, "RSHIFT", "Integer", "Integer", "Integer"},
+
+       {"%", 5, "MOD", "Integer", "Integer", "Integer"},
+       {"*", 5, "MUL", "Integer", "Integer", "Integer"},
+       {"/", 5, "DIV", "Integer", "Integer", "Integer"},
+}
+
+type unaryOp struct {
+       op      string
+       opcodes string
+
+       operand, result typeDesc
+}
+
+var unaryOps = []unaryOp{
+       {"-", "NEGATE", "Integer", "Integer"},
+
+       // not not allowed (for now?)
+       // {"!", "NOT", "Boolean", "Boolean"},
+
+       {"~", "INVERT", "", ""},
+}
diff --git a/compiler/checks.go b/compiler/checks.go
new file mode 100644 (file)
index 0000000..d1a1c1a
--- /dev/null
@@ -0,0 +1,211 @@
+package compiler
+
+import "fmt"
+
+func checkRecursive(contract *Contract) bool {
+       for _, clause := range contract.Clauses {
+               for _, stmt := range clause.statements {
+                       if l, ok := stmt.(*lockStatement); ok {
+                               if c, ok := l.program.(*callExpr); ok {
+                                       if references(c.fn, contract.Name) {
+                                               return true
+                                       }
+                               }
+                       }
+               }
+       }
+       return false
+}
+
+func prohibitSigParams(contract *Contract) error {
+       for _, p := range contract.Params {
+               if p.Type == sigType {
+                       return fmt.Errorf("contract parameter \"%s\" has type Signature, but contract parameters cannot have type Signature", p.Name)
+               }
+       }
+       return nil
+}
+
+func prohibitValueParams(contract *Contract) error {
+       for _, p := range contract.Params {
+               if p.Type == valueType {
+                       return fmt.Errorf("Value-typed contract parameter \"%s\" must appear in a \"locks\" clause", p.Name)
+               }
+       }
+       for _, c := range contract.Clauses {
+               for _, p := range c.Params {
+                       if p.Type == valueType {
+                               return fmt.Errorf("Value-typed parameter \"%s\" of clause \"%s\" must appear in a \"requires\" clause", p.Name, c.Name)
+                       }
+               }
+       }
+       return nil
+}
+
+func requireAllParamsUsedInClauses(params []*Param, clauses []*Clause) error {
+       for _, p := range params {
+               used := false
+               for _, c := range clauses {
+                       err := requireAllParamsUsedInClause([]*Param{p}, c)
+                       if err == nil {
+                               used = true
+                               break
+                       }
+               }
+               if !used {
+                       return fmt.Errorf("parameter \"%s\" is unused", p.Name)
+               }
+       }
+       return nil
+}
+
+func requireAllParamsUsedInClause(params []*Param, clause *Clause) error {
+       for _, p := range params {
+               used := false
+               for _, stmt := range clause.statements {
+                       switch s := stmt.(type) {
+                       case *verifyStatement:
+                               used = references(s.expr, p.Name)
+                       case *lockStatement:
+                               used = references(s.locked, p.Name) || references(s.program, p.Name)
+                       case *unlockStatement:
+                               used = references(s.expr, p.Name)
+                       }
+                       if used {
+                               break
+                       }
+               }
+               if !used {
+                       for _, r := range clause.Reqs {
+                               if references(r.amountExpr, p.Name) || references(r.assetExpr, p.Name) {
+                                       used = true
+                                       break
+                               }
+                       }
+               }
+               if !used {
+                       return fmt.Errorf("parameter \"%s\" is unused in clause \"%s\"", p.Name, clause.Name)
+               }
+       }
+       return nil
+}
+
+func references(expr expression, name string) bool {
+       switch e := expr.(type) {
+       case *binaryExpr:
+               return references(e.left, name) || references(e.right, name)
+       case *unaryExpr:
+               return references(e.expr, name)
+       case *callExpr:
+               if references(e.fn, name) {
+                       return true
+               }
+               for _, a := range e.args {
+                       if references(a, name) {
+                               return true
+                       }
+               }
+               return false
+       case varRef:
+               return string(e) == name
+       case listExpr:
+               for _, elt := range []expression(e) {
+                       if references(elt, name) {
+                               return true
+                       }
+               }
+               return false
+       }
+       return false
+}
+
+func requireAllValuesDisposedOnce(contract *Contract, clause *Clause) error {
+       err := valueDisposedOnce(contract.Value, clause)
+       if err != nil {
+               return err
+       }
+       for _, req := range clause.Reqs {
+               err = valueDisposedOnce(req.Name, clause)
+               if err != nil {
+                       return err
+               }
+       }
+       return nil
+}
+
+func valueDisposedOnce(name string, clause *Clause) error {
+       var count int
+       for _, s := range clause.statements {
+               switch stmt := s.(type) {
+               case *unlockStatement:
+                       if references(stmt.expr, name) {
+                               count++
+                       }
+               case *lockStatement:
+                       if references(stmt.locked, name) {
+                               count++
+                       }
+               }
+       }
+       switch count {
+       case 0:
+               return fmt.Errorf("value \"%s\" not disposed in clause \"%s\"", name, clause.Name)
+       case 1:
+               return nil
+       default:
+               return fmt.Errorf("value \"%s\" disposed multiple times in clause \"%s\"", name, clause.Name)
+       }
+}
+
+func referencedBuiltin(expr expression) *builtin {
+       if v, ok := expr.(varRef); ok {
+               for _, b := range builtins {
+                       if string(v) == b.name {
+                               return &b
+                       }
+               }
+       }
+       return nil
+}
+
+func assignIndexes(clause *Clause) {
+       var nextIndex int64
+       for _, s := range clause.statements {
+               switch stmt := s.(type) {
+               case *lockStatement:
+                       stmt.index = nextIndex
+                       nextIndex++
+
+               case *unlockStatement:
+                       nextIndex++
+               }
+       }
+}
+
+func typeCheckClause(contract *Contract, clause *Clause, env *environ) error {
+       for _, s := range clause.statements {
+               switch stmt := s.(type) {
+               case *verifyStatement:
+                       if t := stmt.expr.typ(env); t != boolType {
+                               return fmt.Errorf("expression in verify statement in clause \"%s\" has type \"%s\", must be Boolean", clause.Name, t)
+                       }
+
+               case *lockStatement:
+                       if t := stmt.locked.typ(env); t != valueType {
+                               return fmt.Errorf("expression in lock statement in clause \"%s\" has type \"%s\", must be Value", clause.Name, t)
+                       }
+                       if t := stmt.program.typ(env); t != progType {
+                               return fmt.Errorf("program in lock statement in clause \"%s\" has type \"%s\", must be Program", clause.Name, t)
+                       }
+
+               case *unlockStatement:
+                       if t := stmt.expr.typ(env); t != valueType {
+                               return fmt.Errorf("expression \"%s\" in unlock statement of clause \"%s\" has type \"%s\", must be Value", stmt.expr, clause.Name, t)
+                       }
+                       if stmt.expr.String() != contract.Value {
+                               return fmt.Errorf("expression in unlock statement of clause \"%s\" must be the contract value", clause.Name)
+                       }
+               }
+       }
+       return nil
+}
diff --git a/compiler/checks_test.go b/compiler/checks_test.go
new file mode 100644 (file)
index 0000000..79e9282
--- /dev/null
@@ -0,0 +1,77 @@
+package compiler
+
+import "testing"
+
+func TestRequireAllParamsUsedInClauses(t *testing.T) {
+       clauses := []*Clause{
+               &Clause{
+                       statements: []statement{
+                               &verifyStatement{expr: varRef("foo")},
+                               &verifyStatement{
+                                       expr: &binaryExpr{
+                                               left:  varRef("foo"),
+                                               right: varRef("bar"),
+                                       },
+                               },
+                               &lockStatement{
+                                       locked:  varRef("baz"),
+                                       program: varRef("foo"),
+                               },
+                       },
+               },
+               &Clause{
+                       statements: []statement{
+                               &verifyStatement{expr: varRef("foo")},
+                               &verifyStatement{
+                                       expr: &binaryExpr{
+                                               left:  varRef("foo"),
+                                               right: varRef("plugh"),
+                                       },
+                               },
+                               &lockStatement{
+                                       locked:  varRef("xyzzy"),
+                                       program: varRef("foo"),
+                               },
+                       },
+               },
+       }
+
+       cases := []struct {
+               name   string
+               params []string
+               want   string
+       }{
+               {
+                       name:   "contract param used in both clauses",
+                       params: []string{"foo"},
+               },
+               {
+                       name:   "contract param used in one clause",
+                       params: []string{"bar"},
+               },
+               {
+                       name:   "contract param used in no clauses",
+                       params: []string{"y2"},
+                       want:   "parameter \"y2\" is unused",
+               },
+       }
+       for _, c := range cases {
+               t.Run(c.name, func(t *testing.T) {
+                       var params []*Param
+                       for _, p := range c.params {
+                               params = append(params, &Param{Name: p})
+                       }
+                       err := requireAllParamsUsedInClauses(params, clauses)
+                       if err == nil && c.want == "" {
+                               return
+                       }
+                       if err == nil {
+                               t.Errorf("got err==nil, want %s", c.want)
+                               return
+                       }
+                       if err.Error() != c.want {
+                               t.Errorf("got %s, want %s", err, c.want)
+                       }
+               })
+       }
+}
diff --git a/compiler/cmd/equitycmd/equitycmd.go b/compiler/cmd/equitycmd/equitycmd.go
new file mode 100644 (file)
index 0000000..3b16942
--- /dev/null
@@ -0,0 +1,210 @@
+package main
+
+import (
+       "bytes"
+       "flag"
+       "fmt"
+       "log"
+       "os"
+       "strings"
+
+       "github.com/equity/compiler"
+)
+
+func main() {
+       packageName := flag.String("package", "main", "Go package name for generated file")
+       flag.Parse()
+
+       contracts, err := compiler.Compile(os.Stdin)
+       if err != nil {
+               log.Fatal(err)
+       }
+
+       fmt.Printf("package %s\n\n", *packageName)
+
+       imports := map[string]bool{
+               "bytes":        true,
+               "encoding/hex": true,
+               "fmt":          true,
+               "github.com/bytom/equity/compiler": true,
+               "github.com/bytom/protocol/vm":     true,
+       }
+
+       buf := new(bytes.Buffer)
+
+       if len(contracts) == 1 {
+               fmt.Fprintf(buf, "var %s_body_bytes []byte\n\n", contracts[0].Name)
+       } else {
+               fmt.Fprintf(buf, "var (\n")
+               for _, contract := range contracts {
+                       fmt.Fprintf(buf, "\t%s_body_bytes []byte\n", contract.Name)
+               }
+               fmt.Fprintf(buf, ")\n\n")
+       }
+
+       fmt.Fprintf(buf, "func init() {\n")
+       for _, contract := range contracts {
+               fmt.Fprintf(buf, "\t%s_body_bytes, _ = hex.DecodeString(\"%x\")\n", contract.Name, contract.Body)
+       }
+       fmt.Fprintf(buf, "}\n\n")
+
+       for _, contract := range contracts {
+               fmt.Fprintf(buf, "// contract %s(%s) locks %s\n", contract.Name, paramsStr(contract.Params), contract.Value)
+               fmt.Fprintf(buf, "//\n")
+               maxWidth := 0
+               for _, step := range contract.Steps {
+                       if len(step.Opcodes) > maxWidth {
+                               maxWidth = len(step.Opcodes)
+                       }
+               }
+               format := fmt.Sprintf("// %%-%d.%ds  %%s\n", maxWidth, maxWidth)
+               for _, step := range contract.Steps {
+                       fmt.Fprintf(buf, format, step.Opcodes, step.Stack)
+               }
+               fmt.Fprintf(buf, "\n")
+
+               fmt.Fprintf(buf, "// PayTo%s instantiates contract %s as a program with specific arguments.\n", contract.Name, contract.Name)
+               goParams, newImports := asGoParams(contract.Params)
+               for _, imp := range newImports {
+                       imports[imp] = true
+               }
+               fmt.Fprintf(buf, "func PayTo%s(%s) ([]byte, error) {\n", contract.Name, goParams)
+               fmt.Fprintf(buf, "\t_contractParams := []compiler.Param{\n")
+               for _, param := range contract.Params {
+                       fmt.Fprintf(buf, "\t\t{Name: \"%s\", Type: \"%s\"},\n", param.Name, param.Type)
+               }
+               fmt.Fprintf(buf, "\t}\n")
+               fmt.Fprintf(buf, "\tvar _contractArgs []compiler.ContractArg\n")
+               for _, param := range contract.Params {
+                       switch param.Type {
+                       case "Amount":
+                               fmt.Fprintf(buf, "\t_%s := int64(%s)\n", param.Name, param.Name)
+                               fmt.Fprintf(buf, "\t_contractArgs = append(_contractArgs, compiler.ContractArg{I: &_%s})\n", param.Name)
+                       case "Asset":
+                               fmt.Fprintf(buf, "\t_%s := %s[:]\n", param.Name, param.Name)
+                               fmt.Fprintf(buf, "\t_contractArgs = append(_contractArgs, compiler.ContractArg{S: &_%s})\n", param.Name)
+                       case "Boolean", "Hash", "Program", "PublicKey", "Signature", "String":
+                               fmt.Fprintf(buf, "\t_contractArgs = append(_contractArgs, compiler.ContractArg{S: &%s})\n", param.Name)
+                       case "Integer":
+                               fmt.Fprintf(buf, "\t_contractArgs = append(_contractArgs, compiler.ContractArg{I: &%s})\n", param.Name)
+                       }
+               }
+               fmt.Fprintf(buf, "\treturn compiler.Instantiate(_contractParams, %s_body_bytes, %v, _contractArgs)\n", contract.Name, contract.Recursive)
+               fmt.Fprintf(buf, "}\n\n")
+
+               fmt.Fprintf(buf, "// ParsePayTo%s parses the arguments out of an instantiation of contract %s.\n", contract.Name, contract.Name)
+               fmt.Fprintf(buf, "// If the input is not an instantiation of %s, returns an error.\n", contract.Name)
+               fmt.Fprintf(buf, "func ParsePayTo%s(prog []byte) ([][]byte, error) {\n", contract.Name)
+               fmt.Fprintf(buf, "\tvar result [][]byte\n")
+               fmt.Fprintf(buf, "\tinsts, err := vm.ParseProgram(prog)\n")
+               fmt.Fprintf(buf, "\tif err != nil {\n")
+               fmt.Fprintf(buf, "\t\treturn nil, err\n")
+               fmt.Fprintf(buf, "\t}\n")
+               fmt.Fprintf(buf, "\tfor i := 0; i < %d; i++ {\n", len(contract.Params))
+               fmt.Fprintf(buf, "\t\tif len(insts) == 0 {\n")
+               fmt.Fprintf(buf, "\t\t\treturn nil, fmt.Errorf(\"program too short\")\n")
+               fmt.Fprintf(buf, "\t\t}\n")
+               fmt.Fprintf(buf, "\t\tif !insts[0].IsPushdata() {\n")
+               fmt.Fprintf(buf, "\t\t\treturn nil, fmt.Errorf(\"too few arguments\")\n")
+               fmt.Fprintf(buf, "\t\t}\n")
+               fmt.Fprintf(buf, "\t\tresult = append(result, insts[0].Data)\n")
+               fmt.Fprintf(buf, "\t\tinsts = insts[1:]\n")
+               fmt.Fprintf(buf, "\t}\n")
+               if contract.Recursive {
+                       // args... body DEPTH OVER 0 CHECKPREDICATE
+                       fmt.Fprintf(buf, "\tif len(insts) == 0 {\n")
+                       fmt.Fprintf(buf, "\t\treturn nil, fmt.Errorf(\"program too short\")\n")
+                       fmt.Fprintf(buf, "\t}\n")
+                       fmt.Fprintf(buf, "\tif !insts[0].IsPushdata() {\n")
+                       fmt.Fprintf(buf, "\t\treturn nil, fmt.Errorf(\"too few arguments\")\n")
+                       fmt.Fprintf(buf, "\t}\n")
+                       fmt.Fprintf(buf, "\tif !bytes.Equal(%s_body_bytes, insts[0].Data) {\n", contract.Name)
+                       fmt.Fprintf(buf, "\t\treturn nil, fmt.Errorf(\"body bytes do not match %s\")\n", contract.Name)
+                       fmt.Fprintf(buf, "\t}\n")
+                       fmt.Fprintf(buf, "\tinsts = insts[1:]\n")
+               } // else args ... DEPTH body 0 CHECKPREDICATE
+               fmt.Fprintf(buf, "\tif len(insts) != 4 {\n")
+               fmt.Fprintf(buf, "\t\treturn nil, fmt.Errorf(\"program too short\")\n")
+               fmt.Fprintf(buf, "\t}\n")
+               fmt.Fprintf(buf, "\tif insts[0].Op != vm.OP_DEPTH {\n")
+               fmt.Fprintf(buf, "\t\treturn nil, fmt.Errorf(\"wrong program format\")\n")
+               fmt.Fprintf(buf, "\t}\n")
+               if contract.Recursive {
+                       fmt.Fprintf(buf, "\tif insts[1].Op != vm.OP_OVER {\n")
+                       fmt.Fprintf(buf, "\t\treturn nil, fmt.Errorf(\"wrong program format\")\n")
+                       fmt.Fprintf(buf, "\t}\n")
+               } else {
+                       fmt.Fprintf(buf, "\tif !insts[1].IsPushdata() {\n")
+                       fmt.Fprintf(buf, "\t\treturn nil, fmt.Errorf(\"wrong program format\")\n")
+                       fmt.Fprintf(buf, "\t}\n")
+                       fmt.Fprintf(buf, "\tif !bytes.Equal(%s_body_bytes, insts[1].Data) {\n", contract.Name)
+                       fmt.Fprintf(buf, "\t\treturn nil, fmt.Errorf(\"body bytes do not match %s\")\n", contract.Name)
+                       fmt.Fprintf(buf, "\t}\n")
+               }
+               fmt.Fprintf(buf, "\tif !insts[2].IsPushdata() {\n")
+               fmt.Fprintf(buf, "\t\treturn nil, fmt.Errorf(\"wrong program format\")\n")
+               fmt.Fprintf(buf, "\t}\n")
+               fmt.Fprintf(buf, "\tv, err := vm.AsInt64(insts[2].Data)\n")
+               fmt.Fprintf(buf, "\tif err != nil {\n")
+               fmt.Fprintf(buf, "\t\treturn nil, err\n")
+               fmt.Fprintf(buf, "\t}\n")
+               fmt.Fprintf(buf, "\tif v != 0 {\n")
+               fmt.Fprintf(buf, "\t\treturn nil, fmt.Errorf(\"wrong program format\")\n")
+               fmt.Fprintf(buf, "\t}\n")
+               fmt.Fprintf(buf, "\tif insts[3].Op != vm.OP_CHECKPREDICATE {\n")
+               fmt.Fprintf(buf, "\t\treturn nil, fmt.Errorf(\"wrong program format\")\n")
+               fmt.Fprintf(buf, "\t}\n")
+               fmt.Fprintf(buf, "\treturn result, nil\n")
+               fmt.Fprintf(buf, "}\n\n")
+
+               // TODO(bobg): RedeemFoo_Bar functions for marshaling the args to
+               // the Bar clause of contract Foo.
+       }
+
+       fmt.Printf("import (\n")
+       for imp := range imports {
+               fmt.Printf("\t\"%s\"\n", imp)
+       }
+       fmt.Printf(")\n\n")
+
+       os.Stdout.Write(buf.Bytes())
+}
+
+func paramsStr(params []*compiler.Param) string {
+       var strs []string
+       for _, p := range params {
+               strs = append(strs, fmt.Sprintf("%s: %s", p.Name, p.Type))
+       }
+       return strings.Join(strs, ", ")
+}
+
+func asGoParams(params []*compiler.Param) (goParams string, imports []string) {
+       var strs []string
+       for _, p := range params {
+               var typ string
+               switch p.Type {
+               case "Amount":
+                       typ = "uint64"
+               case "Asset":
+                       typ = "bc.AssetId"
+                       imports = append(imports, "github.com/bytom/protocol/bc")
+               case "Boolean":
+                       typ = "bool"
+               case "Hash":
+                       typ = "[]byte"
+               case "Integer":
+                       typ = "int64"
+               case "Program":
+                       typ = "[]byte"
+               case "PublicKey":
+                       typ = "ed25519.PublicKey"
+                       imports = append(imports, "github.com/bytom/crypto/ed25519")
+               case "Signature":
+                       typ = "[]byte"
+               case "String":
+                       typ = "[]byte"
+               }
+               strs = append(strs, fmt.Sprintf("%s %s", p.Name, typ))
+       }
+       return strings.Join(strs, ", "), imports
+}
diff --git a/compiler/compile.go b/compiler/compile.go
new file mode 100644 (file)
index 0000000..99fe605
--- /dev/null
@@ -0,0 +1,753 @@
+package compiler
+
+import (
+       "encoding/json"
+       "fmt"
+       "io"
+       "io/ioutil"
+
+       chainjson "github.com/bytom/encoding/json"
+       "github.com/bytom/errors"
+       "github.com/bytom/protocol/vm"
+       "github.com/bytom/protocol/vm/vmutil"
+)
+
+// ValueInfo describes how a blockchain value is used in a contract
+// clause.
+type ValueInfo struct {
+       // Name is the clause's name for this value.
+       Name string `json:"name"`
+
+       // Program is the program expression used to the lock the value, if
+       // the value is locked with "lock." If it's unlocked with "unlock"
+       // instead, this is empty.
+       Program string `json:"program,omitempty"`
+
+       // Asset is the expression describing the asset type the value must
+       // have, as it appears in a clause's "requires" section. If this is
+       // the contract value instead, this is empty.
+       Asset string `json:"asset,omitempty"`
+
+       // Amount is the expression describing the amount the value must
+       // have, as it appears in a clause's "requires" section. If this is
+       // the contract value instead, this is empty.
+       Amount string `json:"amount,omitempty"`
+}
+
+// ContractArg is an argument with which to instantiate a contract as
+// a program. Exactly one of B, I, and S should be supplied.
+type ContractArg struct {
+       B *bool               `json:"boolean,omitempty"`
+       I *int64              `json:"integer,omitempty"`
+       S *chainjson.HexBytes `json:"string,omitempty"`
+}
+
+// Compile parses a sequence of Equity contracts from the supplied reader
+// and produces Contract objects containing the compiled bytecode and
+// other analysis. If argMap is non-nil, it maps contract names to
+// lists of arguments with which to instantiate them as programs, with
+// the results placed in the contract's Program field. A contract
+// named in argMap but not found in the input is silently ignored.
+func Compile(r io.Reader) ([]*Contract, error) {
+       inp, err := ioutil.ReadAll(r)
+       if err != nil {
+               return nil, errors.Wrap(err, "reading input")
+       }
+       contracts, err := parse(inp)
+       if err != nil {
+               return nil, errors.Wrap(err, "parse error")
+       }
+
+       globalEnv := newEnviron(nil)
+       for _, k := range keywords {
+               globalEnv.add(k, nilType, roleKeyword)
+       }
+       for _, b := range builtins {
+               globalEnv.add(b.name, nilType, roleBuiltin)
+       }
+
+       // All contracts must be checked for recursiveness before any are
+       // compiled.
+       for _, contract := range contracts {
+               contract.Recursive = checkRecursive(contract)
+       }
+
+       for _, contract := range contracts {
+               err = globalEnv.addContract(contract)
+               if err != nil {
+                       return nil, err
+               }
+       }
+
+       for _, contract := range contracts {
+               err = compileContract(contract, globalEnv)
+               if err != nil {
+                       return nil, errors.Wrap(err, "compiling contract")
+               }
+               for _, clause := range contract.Clauses {
+                       for _, stmt := range clause.statements {
+                               switch s := stmt.(type) {
+                               case *lockStatement:
+                                       valueInfo := ValueInfo{
+                                               Name:    s.locked.String(),
+                                               Program: s.program.String(),
+                                       }
+                                       if s.locked.String() != contract.Value {
+                                               for _, r := range clause.Reqs {
+                                                       if s.locked.String() == r.Name {
+                                                               valueInfo.Asset = r.assetExpr.String()
+                                                               valueInfo.Amount = r.amountExpr.String()
+                                                               break
+                                                       }
+                                               }
+                                       }
+                                       clause.Values = append(clause.Values, valueInfo)
+                               case *unlockStatement:
+                                       valueInfo := ValueInfo{Name: contract.Value}
+                                       clause.Values = append(clause.Values, valueInfo)
+                               }
+                       }
+               }
+       }
+
+       return contracts, nil
+}
+
+func Instantiate(body []byte, params []*Param, recursive bool, args []ContractArg) ([]byte, error) {
+       if len(args) != len(params) {
+               return nil, fmt.Errorf("got %d argument(s), want %d", len(args), len(params))
+       }
+
+       // typecheck args against param types
+       for i, param := range params {
+               arg := args[i]
+               switch param.Type {
+               case amountType, intType:
+                       if arg.I == nil {
+                               return nil, fmt.Errorf("type mismatch in arg %d (want integer)", i)
+                       }
+               case assetType, hashType, progType, pubkeyType, sigType, strType:
+                       if arg.S == nil {
+                               return nil, fmt.Errorf("type mismatch in arg %d (want string)", i)
+                       }
+               case boolType:
+                       if arg.B == nil {
+                               return nil, fmt.Errorf("type mismatch in arg %d (want boolean)", i)
+                       }
+               }
+       }
+
+       b := vmutil.NewBuilder()
+
+       for i := len(args) - 1; i >= 0; i-- {
+               a := args[i]
+               switch {
+               case a.B != nil:
+                       var n int64
+                       if *a.B {
+                               n = 1
+                       }
+                       b.AddInt64(n)
+               case a.I != nil:
+                       b.AddInt64(*a.I)
+               case a.S != nil:
+                       b.AddData(*a.S)
+               }
+       }
+
+       if recursive {
+               // <argN> <argN-1> ... <arg1> <body> DEPTH OVER 0 CHECKPREDICATE
+               b.AddData(body)
+               b.AddOp(vm.OP_DEPTH).AddOp(vm.OP_OVER)
+       } else {
+               // <argN> <argN-1> ... <arg1> DEPTH <body> 0 CHECKPREDICATE
+               b.AddOp(vm.OP_DEPTH)
+               b.AddData(body)
+       }
+       b.AddInt64(0)
+       b.AddOp(vm.OP_CHECKPREDICATE)
+       return b.Build()
+}
+
+func compileContract(contract *Contract, globalEnv *environ) error {
+       var err error
+
+       if len(contract.Clauses) == 0 {
+               return fmt.Errorf("empty contract")
+       }
+       env := newEnviron(globalEnv)
+       for _, p := range contract.Params {
+               err = env.add(p.Name, p.Type, roleContractParam)
+               if err != nil {
+                       return err
+               }
+       }
+       err = env.add(contract.Value, valueType, roleContractValue)
+       if err != nil {
+               return err
+       }
+       for _, c := range contract.Clauses {
+               err = env.add(c.Name, nilType, roleClause)
+               if err != nil {
+                       return err
+               }
+       }
+
+       err = prohibitValueParams(contract)
+       if err != nil {
+               return err
+       }
+       err = prohibitSigParams(contract)
+       if err != nil {
+               return err
+       }
+       err = requireAllParamsUsedInClauses(contract.Params, contract.Clauses)
+       if err != nil {
+               return err
+       }
+
+       var stk stack
+
+       if len(contract.Clauses) > 1 {
+               stk = stk.add("<clause selector>")
+       }
+
+       for i := len(contract.Params) - 1; i >= 0; i-- {
+               p := contract.Params[i]
+               stk = stk.add(p.Name)
+       }
+
+       if contract.Recursive {
+               stk = stk.add(contract.Name)
+       }
+
+       b := &builder{}
+
+       if len(contract.Clauses) == 1 {
+               err = compileClause(b, stk, contract, env, contract.Clauses[0])
+               if err != nil {
+                       return err
+               }
+       } else {
+               if len(contract.Params) > 0 {
+                       // A clause selector is at the bottom of the stack. Roll it to the
+                       // top.
+                       n := len(contract.Params)
+                       if contract.Recursive {
+                               n++
+                       }
+                       stk = b.addRoll(stk, n) // stack: [<clause params> <contract params> [<maybe contract body>] <clause selector>]
+               }
+
+               var stk2 stack
+
+               // clauses 2..N-1
+               for i := len(contract.Clauses) - 1; i >= 2; i-- {
+                       stk = b.addDup(stk)                                                   // stack: [... <clause selector> <clause selector>]
+                       stk = b.addInt64(stk, int64(i))                                       // stack: [... <clause selector> <clause selector> <i>]
+                       stk = b.addNumEqual(stk, fmt.Sprintf("(<clause selector> == %d)", i)) // stack: [... <clause selector> <i == clause selector>]
+                       stk = b.addJumpIf(stk, contract.Clauses[i].Name)                      // stack: [... <clause selector>]
+                       stk2 = stk                                                            // stack starts here for clauses 2 through N-1
+               }
+
+               // clause 1
+               stk = b.addJumpIf(stk, contract.Clauses[1].Name) // consumes the clause selector
+
+               // no jump needed for clause 0
+
+               for i, clause := range contract.Clauses {
+                       if i > 1 {
+                               // Clauses 0 and 1 have no clause selector on top of the
+                               // stack. Clauses 2 and later do.
+                               stk = stk2
+                       }
+
+                       b.addJumpTarget(stk, clause.Name)
+
+                       if i > 1 {
+                               stk = b.addDrop(stk)
+                       }
+
+                       err = compileClause(b, stk, contract, env, clause)
+                       if err != nil {
+                               return errors.Wrapf(err, "compiling clause \"%s\"", clause.Name)
+                       }
+                       b.forgetPendingVerify()
+                       if i < len(contract.Clauses)-1 {
+                               b.addJump(stk, "_end")
+                       }
+               }
+               b.addJumpTarget(stk, "_end")
+       }
+
+       opcodes := optimize(b.opcodes())
+       prog, err := vm.Assemble(opcodes)
+       if err != nil {
+               return err
+       }
+
+       contract.Body = prog
+       contract.Opcodes = opcodes
+
+       contract.Steps = b.steps()
+
+       return nil
+}
+
+func compileClause(b *builder, contractStk stack, contract *Contract, env *environ, clause *Clause) error {
+       var err error
+
+       // copy env to leave outerEnv unchanged
+       env = newEnviron(env)
+       for _, p := range clause.Params {
+               err = env.add(p.Name, p.Type, roleClauseParam)
+               if err != nil {
+                       return err
+               }
+       }
+       for _, req := range clause.Reqs {
+               err = env.add(req.Name, valueType, roleClauseValue)
+               if err != nil {
+                       return err
+               }
+               req.Asset = req.assetExpr.String()
+               req.Amount = req.amountExpr.String()
+       }
+
+       assignIndexes(clause)
+
+       var stk stack
+       for _, p := range clause.Params {
+               // NOTE: the order of clause params is not reversed, unlike
+               // contract params (and also unlike the arguments to Equity
+               // function-calls).
+               stk = stk.add(p.Name)
+       }
+       stk = stk.addFromStack(contractStk)
+
+       // a count of the number of times each variable is referenced
+       counts := make(map[string]int)
+       for _, req := range clause.Reqs {
+               req.assetExpr.countVarRefs(counts)
+               req.amountExpr.countVarRefs(counts)
+       }
+       for _, s := range clause.statements {
+               s.countVarRefs(counts)
+       }
+
+       for _, s := range clause.statements {
+               switch stmt := s.(type) {
+               case *verifyStatement:
+                       stk, err = compileExpr(b, stk, contract, clause, env, counts, stmt.expr)
+                       if err != nil {
+                               return errors.Wrapf(err, "in verify statement in clause \"%s\"", clause.Name)
+                       }
+                       stk = b.addVerify(stk)
+
+                       // special-case reporting of certain function calls
+                       if c, ok := stmt.expr.(*callExpr); ok && len(c.args) == 1 {
+                               if b := referencedBuiltin(c.fn); b != nil {
+                                       switch b.name {
+                                       case "below":
+                                               clause.BlockHeight = append(clause.BlockHeight, c.args[0].String())
+                                       case "above":
+                                               clause.BlockHeight = append(clause.BlockHeight, c.args[0].String())
+                                       }
+                               }
+                       }
+
+               case *lockStatement:
+                       // index
+                       stk = b.addInt64(stk, stmt.index)
+
+                       // TODO: permit more complex expressions for locked,
+                       // like "lock x+y with foo" (?)
+
+                       if stmt.locked.String() == contract.Value {
+                               stk = b.addAmount(stk)
+                               stk = b.addAsset(stk)
+                       } else {
+                               var req *ClauseReq
+                               for _, r := range clause.Reqs {
+                                       if stmt.locked.String() == r.Name {
+                                               req = r
+                                               break
+                                       }
+                               }
+                               if req == nil {
+                                       return fmt.Errorf("unknown value \"%s\" in lock statement in clause \"%s\"", stmt.locked, clause.Name)
+                               }
+
+                               // amount
+                               stk, err = compileExpr(b, stk, contract, clause, env, counts, req.amountExpr)
+                               if err != nil {
+                                       return errors.Wrapf(err, "in lock statement in clause \"%s\"", clause.Name)
+                               }
+
+                               // asset
+                               stk, err = compileExpr(b, stk, contract, clause, env, counts, req.assetExpr)
+                               if err != nil {
+                                       return errors.Wrapf(err, "in lock statement in clause \"%s\"", clause.Name)
+                               }
+                       }
+
+                       // version
+                       stk = b.addInt64(stk, 1)
+
+                       // prog
+                       stk, err = compileExpr(b, stk, contract, clause, env, counts, stmt.program)
+                       if err != nil {
+                               return errors.Wrapf(err, "in lock statement in clause \"%s\"", clause.Name)
+                       }
+
+                       stk = b.addCheckOutput(stk, fmt.Sprintf("checkOutput(%s, %s)", stmt.locked, stmt.program))
+                       stk = b.addVerify(stk)
+
+               case *unlockStatement:
+                       if len(clause.statements) == 1 {
+                               // This is the only statement in the clause, make sure TRUE is
+                               // on the stack.
+                               stk = b.addBoolean(stk, true)
+                       }
+               }
+       }
+
+       err = requireAllValuesDisposedOnce(contract, clause)
+       if err != nil {
+               return err
+       }
+       err = typeCheckClause(contract, clause, env)
+       if err != nil {
+               return err
+       }
+       err = requireAllParamsUsedInClause(clause.Params, clause)
+       if err != nil {
+               return err
+       }
+
+       return nil
+}
+
+func compileExpr(b *builder, stk stack, contract *Contract, clause *Clause, env *environ, counts map[string]int, expr expression) (stack, error) {
+       var err error
+
+       switch e := expr.(type) {
+       case *binaryExpr:
+               // Do typechecking after compiling subexpressions (because other
+               // compilation errors are more interesting than type mismatch
+               // errors).
+
+               stk, err = compileExpr(b, stk, contract, clause, env, counts, e.left)
+               if err != nil {
+                       return stk, errors.Wrapf(err, "in left operand of \"%s\" expression", e.op.op)
+               }
+               stk, err = compileExpr(b, stk, contract, clause, env, counts, e.right)
+               if err != nil {
+                       return stk, errors.Wrapf(err, "in right operand of \"%s\" expression", e.op.op)
+               }
+
+               lType := e.left.typ(env)
+               if e.op.left != "" && lType != e.op.left {
+                       return stk, fmt.Errorf("in \"%s\", left operand has type \"%s\", must be \"%s\"", e, lType, e.op.left)
+               }
+
+               rType := e.right.typ(env)
+               if e.op.right != "" && rType != e.op.right {
+                       return stk, fmt.Errorf("in \"%s\", right operand has type \"%s\", must be \"%s\"", e, rType, e.op.right)
+               }
+
+               switch e.op.op {
+               case "==", "!=":
+                       if lType != rType {
+                               // Maybe one is Hash and the other is (more-specific-Hash subtype).
+                               // TODO(bobg): generalize this mechanism
+                               if lType == hashType && isHashSubtype(rType) {
+                                       propagateType(contract, clause, env, rType, e.left)
+                               } else if rType == hashType && isHashSubtype(lType) {
+                                       propagateType(contract, clause, env, lType, e.right)
+                               } else {
+                                       return stk, fmt.Errorf("type mismatch in \"%s\": left operand has type \"%s\", right operand has type \"%s\"", e, lType, rType)
+                               }
+                       }
+                       if lType == "Boolean" {
+                               return stk, fmt.Errorf("in \"%s\": using \"%s\" on Boolean values not allowed", e, e.op.op)
+                       }
+               }
+
+               stk = b.addOps(stk.dropN(2), e.op.opcodes, e.String())
+
+       case *unaryExpr:
+               // Do typechecking after compiling subexpression (because other
+               // compilation errors are more interesting than type mismatch
+               // errors).
+
+               var err error
+               stk, err = compileExpr(b, stk, contract, clause, env, counts, e.expr)
+               if err != nil {
+                       return stk, errors.Wrapf(err, "in \"%s\" expression", e.op.op)
+               }
+
+               if e.op.operand != "" && e.expr.typ(env) != e.op.operand {
+                       return stk, fmt.Errorf("in \"%s\", operand has type \"%s\", must be \"%s\"", e, e.expr.typ(env), e.op.operand)
+               }
+               b.addOps(stk.drop(), e.op.opcodes, e.String())
+
+       case *callExpr:
+               bi := referencedBuiltin(e.fn)
+               if bi == nil {
+                       if v, ok := e.fn.(varRef); ok {
+                               if entry := env.lookup(string(v)); entry != nil && entry.t == contractType {
+                                       clause.Contracts = append(clause.Contracts, entry.c.Name)
+
+                                       partialName := fmt.Sprintf("%s(...)", v)
+                                       stk = b.addData(stk, nil)
+
+                                       if len(e.args) != len(entry.c.Params) {
+                                               return stk, fmt.Errorf("contract \"%s\" expects %d argument(s), got %d", entry.c.Name, len(entry.c.Params), len(e.args))
+                                       }
+
+                                       for i := len(e.args) - 1; i >= 0; i-- {
+                                               arg := e.args[i]
+                                               if entry.c.Params[i].Type != "" && arg.typ(env) != entry.c.Params[i].Type {
+                                                       return stk, fmt.Errorf("argument %d to contract \"%s\" has type \"%s\", must be \"%s\"", i, entry.c.Name, arg.typ(env), entry.c.Params[i].Type)
+                                               }
+                                               stk, err = compileExpr(b, stk, contract, clause, env, counts, arg)
+                                               if err != nil {
+                                                       return stk, err
+                                               }
+                                               stk = b.addCatPushdata(stk, partialName)
+                                       }
+
+                                       switch {
+                                       case entry.c == contract:
+                                               // Recursive call - cannot use entry.c.Body
+                                               // <argN> <argN-1> ... <arg1> <body> DEPTH OVER 0 CHECKPREDICATE
+                                               stk, err = compileRef(b, stk, counts, varRef(contract.Name))
+                                               if err != nil {
+                                                       return stk, errors.Wrap(err, "compiling contract call")
+                                               }
+                                               stk = b.addCatPushdata(stk, partialName)
+                                               stk = b.addData(stk, []byte{byte(vm.OP_DEPTH), byte(vm.OP_OVER)})
+                                               stk = b.addCat(stk, partialName)
+
+                                       case entry.c.Recursive:
+                                               // Non-recursive call to a (different) recursive contract
+                                               // <argN> <argN-1> ... <arg1> <body> DEPTH OVER 0 CHECKPREDICATE
+                                               if len(entry.c.Body) == 0 {
+                                                       // TODO(bobg): sort input contracts topologically to permit forward calling
+                                                       return stk, fmt.Errorf("contract \"%s\" not defined", entry.c.Name)
+                                               }
+                                               stk = b.addData(stk, entry.c.Body)
+                                               stk = b.addCatPushdata(stk, partialName)
+                                               stk = b.addData(stk, []byte{byte(vm.OP_DEPTH), byte(vm.OP_OVER)})
+                                               stk = b.addCat(stk, partialName)
+
+                                       default:
+                                               // Non-recursive call to non-recursive contract
+                                               // <argN> <argN-1> ... <arg1> DEPTH <body> 0 CHECKPREDICATE
+                                               stk = b.addData(stk, []byte{byte(vm.OP_DEPTH)})
+                                               stk = b.addCat(stk, partialName)
+                                               if len(entry.c.Body) == 0 {
+                                                       // TODO(bobg): sort input contracts topologically to permit forward calling
+                                                       return stk, fmt.Errorf("contract \"%s\" not defined", entry.c.Name)
+                                               }
+                                               stk = b.addData(stk, entry.c.Body)
+                                               stk = b.addCatPushdata(stk, partialName)
+                                       }
+                                       stk = b.addData(stk, vm.Int64Bytes(0))
+                                       stk = b.addCatPushdata(stk, partialName)
+                                       stk = b.addData(stk, []byte{byte(vm.OP_CHECKPREDICATE)})
+                                       stk = b.addCat(stk, e.String())
+
+                                       return stk, nil
+                               }
+                       }
+                       return stk, fmt.Errorf("unknown function \"%s\"", e.fn)
+               }
+
+               if len(e.args) != len(bi.args) {
+                       return stk, fmt.Errorf("wrong number of args for \"%s\": have %d, want %d", bi.name, len(e.args), len(bi.args))
+               }
+
+               // WARNING WARNING WOOP WOOP
+               // special-case hack
+               // WARNING WARNING WOOP WOOP
+               if bi.name == "checkTxMultiSig" {
+                       if _, ok := e.args[0].(listExpr); !ok {
+                               return stk, fmt.Errorf("checkTxMultiSig expects list literals, got %T for argument 0", e.args[0])
+                       }
+                       if _, ok := e.args[1].(listExpr); !ok {
+                               return stk, fmt.Errorf("checkTxMultiSig expects list literals, got %T for argument 1", e.args[1])
+                       }
+
+                       var k1, k2 int
+
+                       stk, k1, err = compileArg(b, stk, contract, clause, env, counts, e.args[1])
+                       if err != nil {
+                               return stk, err
+                       }
+
+                       // stack: [... sigM ... sig1 M]
+
+                       var altEntry string
+                       stk, altEntry = b.addToAltStack(stk) // stack: [... sigM ... sig1]
+                       stk = b.addTxSigHash(stk)            // stack: [... sigM ... sig1 txsighash]
+
+                       stk, k2, err = compileArg(b, stk, contract, clause, env, counts, e.args[0])
+                       if err != nil {
+                               return stk, err
+                       }
+
+                       // stack: [... sigM ... sig1 txsighash pubkeyN ... pubkey1 N]
+
+                       stk = b.addFromAltStack(stk, altEntry) // stack: [... sigM ... sig1 txsighash pubkeyN ... pubkey1 N M]
+                       stk = b.addSwap(stk)                   // stack: [... sigM ... sig1 txsighash pubkeyN ... pubkey1 M N]
+                       stk = b.addCheckMultisig(stk, k1+k2, e.String())
+
+                       return stk, nil
+               }
+
+               var k int
+
+               for i := len(e.args) - 1; i >= 0; i-- {
+                       a := e.args[i]
+                       var k2 int
+                       var err error
+                       stk, k2, err = compileArg(b, stk, contract, clause, env, counts, a)
+                       if err != nil {
+                               return stk, errors.Wrapf(err, "compiling argument %d in call expression", i)
+                       }
+                       k += k2
+               }
+
+               // Do typechecking after compiling subexpressions (because other
+               // compilation errors are more interesting than type mismatch
+               // errors).
+               for i, actual := range e.args {
+                       if bi.args[i] != "" && actual.typ(env) != bi.args[i] {
+                               return stk, fmt.Errorf("argument %d to \"%s\" has type \"%s\", must be \"%s\"", i, bi.name, actual.typ(env), bi.args[i])
+                       }
+               }
+
+               stk = b.addOps(stk.dropN(k), bi.opcodes, e.String())
+
+               // special-case reporting
+               switch bi.name {
+               case "sha3", "sha256":
+                       clause.HashCalls = append(clause.HashCalls, HashCall{bi.name, e.args[0].String(), string(e.args[0].typ(env))})
+               }
+
+       case varRef:
+               return compileRef(b, stk, counts, e)
+
+       case integerLiteral:
+               stk = b.addInt64(stk, int64(e))
+
+       case bytesLiteral:
+               stk = b.addData(stk, []byte(e))
+
+       case booleanLiteral:
+               stk = b.addBoolean(stk, bool(e))
+
+       case listExpr:
+               // Lists are excluded here because they disobey the invariant of
+               // this function: namely, that it increases the stack size by
+               // exactly one. (A list pushes its items and its length on the
+               // stack.) But they're OK as function-call arguments because the
+               // function (presumably) consumes all the stack items added.
+               return stk, fmt.Errorf("encountered list outside of function-call context")
+       }
+       return stk, nil
+}
+
+func compileArg(b *builder, stk stack, contract *Contract, clause *Clause, env *environ, counts map[string]int, expr expression) (stack, int, error) {
+       var n int
+       if list, ok := expr.(listExpr); ok {
+               for i := 0; i < len(list); i++ {
+                       elt := list[len(list)-i-1]
+                       var err error
+                       stk, err = compileExpr(b, stk, contract, clause, env, counts, elt)
+                       if err != nil {
+                               return stk, 0, err
+                       }
+                       n++
+               }
+               stk = b.addInt64(stk, int64(len(list)))
+               n++
+               return stk, n, nil
+       }
+       var err error
+       stk, err = compileExpr(b, stk, contract, clause, env, counts, expr)
+       return stk, 1, err
+}
+
+func compileRef(b *builder, stk stack, counts map[string]int, ref varRef) (stack, error) {
+       depth := stk.find(string(ref))
+       if depth < 0 {
+               return stk, fmt.Errorf("undefined reference: \"%s\"", ref)
+       }
+
+       var isFinal bool
+       if count, ok := counts[string(ref)]; ok && count > 0 {
+               count--
+               counts[string(ref)] = count
+               isFinal = count == 0
+       }
+
+       switch depth {
+       case 0:
+               if !isFinal {
+                       stk = b.addDup(stk)
+               }
+       case 1:
+               if isFinal {
+                       stk = b.addSwap(stk)
+               } else {
+                       stk = b.addOver(stk)
+               }
+       default:
+               if isFinal {
+                       stk = b.addRoll(stk, depth)
+               } else {
+                       stk = b.addPick(stk, depth)
+               }
+       }
+       return stk, nil
+}
+
+func (a *ContractArg) UnmarshalJSON(b []byte) error {
+       var m map[string]json.RawMessage
+       err := json.Unmarshal(b, &m)
+       if err != nil {
+               return err
+       }
+       if r, ok := m["boolean"]; ok {
+               var bval bool
+               err = json.Unmarshal(r, &bval)
+               if err != nil {
+                       return err
+               }
+               a.B = &bval
+               return nil
+       }
+       if r, ok := m["integer"]; ok {
+               var ival int64
+               err = json.Unmarshal(r, &ival)
+               if err != nil {
+                       return err
+               }
+               a.I = &ival
+               return nil
+       }
+       r, ok := m["string"]
+       if !ok {
+               return fmt.Errorf("contract arg must define one of boolean, integer, string")
+       }
+       var sval chainjson.HexBytes
+       err = json.Unmarshal(r, &sval)
+       if err != nil {
+               return err
+       }
+       a.S = &sval
+       return nil
+}
diff --git a/compiler/compile_test.go b/compiler/compile_test.go
new file mode 100644 (file)
index 0000000..ea3902b
--- /dev/null
@@ -0,0 +1,104 @@
+package compiler
+
+import (
+       "encoding/hex"
+       "encoding/json"
+       "strings"
+       "testing"
+
+       "github.com/bytom/equity/compiler/equitytest"
+)
+
+func TestCompile(t *testing.T) {
+       cases := []struct {
+               name     string
+               contract string
+               wantJSON string
+       }{
+               {
+                       "TrivialLock",
+                       equitytest.TrivialLock,
+                       `[{"name":"TrivialLock","clauses":[{"name":"trivialUnlock","values":[{"name":"locked"}]}],"value":"locked","body_bytecode":"51","body_opcodes":"TRUE","recursive":false}]`,
+               },
+               {
+                       "LockWithPublicKey",
+                       equitytest.LockWithPublicKey,
+                       `[{"name":"LockWithPublicKey","params":[{"name":"publicKey","type":"PublicKey"}],"clauses":[{"name":"unlockWithSig","params":[{"name":"sig","type":"Signature"}],"values":[{"name":"locked"}]}],"value":"locked","body_bytecode":"ae7cac","body_opcodes":"TXSIGHASH SWAP CHECKSIG","recursive":false}]`,
+               },
+               {
+                       "LockWithPublicKeyHash",
+                       equitytest.LockWithPKHash,
+                       `[{"name":"LockWithPublicKeyHash","params":[{"name":"pubKeyHash","type":"Hash","inferred_type":"Sha3(PublicKey)"}],"clauses":[{"name":"spend","params":[{"name":"pubKey","type":"PublicKey"},{"name":"sig","type":"Signature"}],"hash_calls":[{"hash_type":"sha3","arg":"pubKey","arg_type":"PublicKey"}],"values":[{"name":"value"}]}],"value":"value","body_bytecode":"5279aa887cae7cac","body_opcodes":"2 PICK SHA3 EQUALVERIFY SWAP TXSIGHASH SWAP CHECKSIG","recursive":false}]`,
+               },
+               {
+                       "LockWith2of3Keys",
+                       equitytest.LockWith2of3Keys,
+                       `[{"name":"LockWith3Keys","params":[{"name":"pubkey1","type":"PublicKey"},{"name":"pubkey2","type":"PublicKey"},{"name":"pubkey3","type":"PublicKey"}],"clauses":[{"name":"unlockWith2Sigs","params":[{"name":"sig1","type":"Signature"},{"name":"sig2","type":"Signature"}],"values":[{"name":"locked"}]}],"value":"locked","body_bytecode":"537a547a526bae71557a536c7cad","body_opcodes":"3 ROLL 4 ROLL 2 TOALTSTACK TXSIGHASH 2ROT 5 ROLL 3 FROMALTSTACK SWAP CHECKMULTISIG","recursive":false}]`,
+               },
+               {
+                       "LockToOutput",
+                       equitytest.LockToOutput,
+                       `[{"name":"LockToOutput","params":[{"name":"address","type":"Program"}],"clauses":[{"name":"relock","values":[{"name":"locked","program":"address"}]}],"value":"locked","body_bytecode":"00c3c251547ac1","body_opcodes":"0 AMOUNT ASSET 1 4 ROLL CHECKOUTPUT","recursive":false}]`,
+               },
+               {
+                       "TradeOffer",
+                       equitytest.TradeOffer,
+                       `[{"name":"TradeOffer","params":[{"name":"requestedAsset","type":"Asset"},{"name":"requestedAmount","type":"Amount"},{"name":"sellerProgram","type":"Program"},{"name":"sellerKey","type":"PublicKey"}],"clauses":[{"name":"trade","reqs":[{"name":"payment","asset":"requestedAsset","amount":"requestedAmount"}],"values":[{"name":"payment","program":"sellerProgram","asset":"requestedAsset","amount":"requestedAmount"},{"name":"offered"}]},{"name":"cancel","params":[{"name":"sellerSig","type":"Signature"}],"values":[{"name":"offered","program":"sellerProgram"}]}],"value":"offered","body_bytecode":"547a6413000000007b7b51547ac16322000000547a547aae7cac6900c3c251567ac1","body_opcodes":"4 ROLL JUMPIF:$cancel $trade 0 ROT ROT 1 4 ROLL CHECKOUTPUT JUMP:$_end $cancel 4 ROLL 4 ROLL TXSIGHASH SWAP CHECKSIG VERIFY 0 AMOUNT ASSET 1 6 ROLL CHECKOUTPUT $_end","recursive":false}]`,
+               },
+               {
+                       "EscrowedTransfer",
+                       equitytest.EscrowedTransfer,
+                       `[{"name":"EscrowedTransfer","params":[{"name":"agent","type":"PublicKey"},{"name":"sender","type":"Program"},{"name":"recipient","type":"Program"}],"clauses":[{"name":"approve","params":[{"name":"sig","type":"Signature"}],"values":[{"name":"value","program":"recipient"}]},{"name":"reject","params":[{"name":"sig","type":"Signature"}],"values":[{"name":"value","program":"sender"}]}],"value":"value","body_bytecode":"537a641a000000537a7cae7cac6900c3c251557ac16328000000537a7cae7cac6900c3c251547ac1","body_opcodes":"3 ROLL JUMPIF:$reject $approve 3 ROLL SWAP TXSIGHASH SWAP CHECKSIG VERIFY 0 AMOUNT ASSET 1 5 ROLL CHECKOUTPUT JUMP:$_end $reject 3 ROLL SWAP TXSIGHASH SWAP CHECKSIG VERIFY 0 AMOUNT ASSET 1 4 ROLL CHECKOUTPUT $_end","recursive":false}]`,
+               },
+               {
+                       "CollateralizedLoan",
+                       equitytest.CollateralizedLoan,
+                       `[{"name":"CollateralizedLoan","params":[{"name":"balanceAsset","type":"Asset"},{"name":"balanceAmount","type":"Amount"},{"name":"finalHeight","type":"Integer"},{"name":"lender","type":"Program"},{"name":"borrower","type":"Program"}],"clauses":[{"name":"repay","reqs":[{"name":"payment","asset":"balanceAsset","amount":"balanceAmount"}],"values":[{"name":"payment","program":"lender","asset":"balanceAsset","amount":"balanceAmount"},{"name":"collateral","program":"borrower"}]},{"name":"default","blockheight":["finalHeight"],"values":[{"name":"collateral","program":"lender"}]}],"value":"collateral","body_bytecode":"557a641b000000007b7b51557ac16951c3c251557ac163260000007bcd9f6900c3c251567ac1","body_opcodes":"5 ROLL JUMPIF:$default $repay 0 ROT ROT 1 5 ROLL CHECKOUTPUT VERIFY 1 AMOUNT ASSET 1 5 ROLL CHECKOUTPUT JUMP:$_end $default ROT BLOCKHEIGHT LESSTHAN VERIFY 0 AMOUNT ASSET 1 6 ROLL CHECKOUTPUT $_end","recursive":false}]`,
+               },
+               {
+                       "RevealPreimage",
+                       equitytest.RevealPreimage,
+                       `[{"name":"RevealPreimage","params":[{"name":"hash","type":"Hash","inferred_type":"Sha3(String)"}],"clauses":[{"name":"reveal","params":[{"name":"string","type":"String"}],"hash_calls":[{"hash_type":"sha3","arg":"string","arg_type":"String"}],"values":[{"name":"value"}]}],"value":"value","body_bytecode":"7caa87","body_opcodes":"SWAP SHA3 EQUAL","recursive":false}]`,
+               },
+               {
+                       "CallOptionWithSettlement",
+                       equitytest.CallOptionWithSettlement,
+                       `[{"name":"CallOptionWithSettlement","params":[{"name":"strikePrice","type":"Amount"},{"name":"strikeCurrency","type":"Asset"},{"name":"sellerProgram","type":"Program"},{"name":"sellerKey","type":"PublicKey"},{"name":"buyerKey","type":"PublicKey"},{"name":"finalHeight","type":"Integer"}],"clauses":[{"name":"exercise","params":[{"name":"buyerSig","type":"Signature"}],"reqs":[{"name":"payment","asset":"strikeCurrency","amount":"strikePrice"}],"blockheight":["finalHeight"],"values":[{"name":"payment","program":"sellerProgram","asset":"strikeCurrency","amount":"strikePrice"},{"name":"underlying"}]},{"name":"expire","blockheight":["finalHeight"],"values":[{"name":"underlying","program":"sellerProgram"}]},{"name":"settle","params":[{"name":"sellerSig","type":"Signature"},{"name":"buyerSig","type":"Signature"}],"values":[{"name":"underlying"}]}],"value":"underlying","body_bytecode":"567a76529c64360000006425000000557acda06971ae7cac69007c7b51547ac16346000000557acd9f6900c3c251567ac1634600000075577a547aae7cac69557a547aae7cac","body_opcodes":"6 ROLL DUP 2 NUMEQUAL JUMPIF:$settle JUMPIF:$expire $exercise 5 ROLL BLOCKHEIGHT GREATERTHAN VERIFY 2ROT TXSIGHASH SWAP CHECKSIG VERIFY 0 SWAP ROT 1 4 ROLL CHECKOUTPUT JUMP:$_end $expire 5 ROLL BLOCKHEIGHT LESSTHAN VERIFY 0 AMOUNT ASSET 1 6 ROLL CHECKOUTPUT JUMP:$_end $settle DROP 7 ROLL 4 ROLL TXSIGHASH SWAP CHECKSIG VERIFY 5 ROLL 4 ROLL TXSIGHASH SWAP CHECKSIG $_end","recursive":false}]`,
+               },
+               {
+                       "PriceChanger",
+                       equitytest.PriceChanger,
+                       `[{"name":"PriceChanger","params":[{"name":"askAmount","type":"Amount"},{"name":"askAsset","type":"Asset"},{"name":"sellerKey","type":"PublicKey"},{"name":"sellerProg","type":"Program"}],"clauses":[{"name":"changePrice","params":[{"name":"newAmount","type":"Amount"},{"name":"newAsset","type":"Asset"},{"name":"sig","type":"Signature"}],"values":[{"name":"offered","program":"PriceChanger(newAmount, newAsset, sellerKey, sellerProg)"}],"contracts":["PriceChanger"]},{"name":"redeem","reqs":[{"name":"payment","asset":"askAsset","amount":"askAmount"}],"values":[{"name":"payment","program":"sellerProg","asset":"askAsset","amount":"askAmount"},{"name":"offered"}]}],"value":"offered","body_bytecode":"557a6432000000557a5479ae7cac6900c3c25100597a89587a89587a89587a89557a890274787e008901c07ec1633a000000007b537a51567ac1","body_opcodes":"5 ROLL JUMPIF:$redeem $changePrice 5 ROLL 4 PICK TXSIGHASH SWAP CHECKSIG VERIFY 0 AMOUNT ASSET 1 0 9 ROLL CATPUSHDATA 8 ROLL CATPUSHDATA 8 ROLL CATPUSHDATA 8 ROLL CATPUSHDATA 5 ROLL CATPUSHDATA 0x7478 CAT 0 CATPUSHDATA 192 CAT CHECKOUTPUT JUMP:$_end $redeem 0 ROT 3 ROLL 1 6 ROLL CHECKOUTPUT $_end","recursive":true}]`,
+               },
+               {
+                       "OneTwo",
+                       equitytest.OneTwo,
+                       `[{"name":"Two","params":[{"name":"b","type":"Program"},{"name":"c","type":"Program"},{"name":"expirationHeight","type":"Integer"}],"clauses":[{"name":"redeem","blockheight":["expirationHeight"],"values":[{"name":"value","program":"b"}]},{"name":"default","blockheight":["expirationHeight"],"values":[{"name":"value","program":"c"}]}],"value":"value","body_bytecode":"537a64170000007bcda06900c3c251547ac163220000007bcd9f6900c3c251557ac1","body_opcodes":"3 ROLL JUMPIF:$default $redeem ROT BLOCKHEIGHT GREATERTHAN VERIFY 0 AMOUNT ASSET 1 4 ROLL CHECKOUTPUT JUMP:$_end $default ROT BLOCKHEIGHT LESSTHAN VERIFY 0 AMOUNT ASSET 1 5 ROLL CHECKOUTPUT $_end","recursive":false},{"name":"One","params":[{"name":"a","type":"Program"},{"name":"b","type":"Program"},{"name":"c","type":"Program"},{"name":"switchHeight","type":"Integer"},{"name":"blockHeight","type":"Integer"}],"clauses":[{"name":"redeem","blockheight":["switchHeight"],"values":[{"name":"value","program":"a"}]},{"name":"switch","blockheight":["switchHeight"],"values":[{"name":"value","program":"Two(b, c, blockHeight)"}],"contracts":["Two"]}],"value":"value","body_bytecode":"557a6418000000537acda06900c3c251547ac16358000000537acd9f6900c3c25100587a89577a89567a8901747e22537a64170000007bcda06900c3c251547ac163220000007bcd9f6900c3c251557ac189008901c07ec1","body_opcodes":"5 ROLL JUMPIF:$switch $redeem 3 ROLL BLOCKHEIGHT GREATERTHAN VERIFY 0 AMOUNT ASSET 1 4 ROLL CHECKOUTPUT JUMP:$_end $switch 3 ROLL BLOCKHEIGHT LESSTHAN VERIFY 0 AMOUNT ASSET 1 0 8 ROLL CATPUSHDATA 7 ROLL CATPUSHDATA 6 ROLL CATPUSHDATA 116 CAT 0x537a64170000007bcda06900c3c251547ac163220000007bcd9f6900c3c251557ac1 CATPUSHDATA 0 CATPUSHDATA 192 CAT CHECKOUTPUT $_end","recursive":false}]`,
+               },
+       }
+       for _, c := range cases {
+               t.Run(c.name, func(t *testing.T) {
+                       r := strings.NewReader(c.contract)
+                       got, err := Compile(r)
+                       if err != nil {
+                               t.Fatal(err)
+                       }
+                       gotJSON, _ := json.Marshal(got)
+                       if string(gotJSON) != c.wantJSON {
+                               t.Errorf("\ngot  %s\nwant %s", string(gotJSON), c.wantJSON)
+                       } else {
+                               for _, contract := range got {
+                                       t.Log(contract.Opcodes)
+                               }
+                       }
+               })
+       }
+}
+
+func mustDecodeHex(h string) []byte {
+       bits, err := hex.DecodeString(h)
+       if err != nil {
+               panic(err)
+       }
+       return bits
+}
diff --git a/compiler/doc.go b/compiler/doc.go
new file mode 100644 (file)
index 0000000..84de0f3
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+Package equity provides a compiler for Chain's Equity contract language.
+
+A contract is a means to lock some payment in the output of a
+transaction. It contains a number of clauses, each describing a way to
+unlock, or redeem, the payment in a subsequent transaction.  By
+executing the statements in a clause, using contract arguments
+supplied by the payer and clause arguments supplied by the redeemer,
+nodes in a Chain network can determine whether a proposed spend is
+valid.
+
+The language definition is in flux, but here's what's implemented as
+of late May 2017.
+
+  program = contract*
+
+  contract = "contract" identifier "(" [params] ")" "locks" identifier "{" clause+ "}"
+
+    The identifier after "locks" is a name for the value locked by
+    the contract. It must be unlocked or re-locked (with "unlock"
+    or "lock") in every clause.
+
+  clause = "clause" identifier "(" [params] ")" ["requires" requirements] "{" statement+ "}"
+
+    The requirements are blockchain values that must be present in
+    the spending transaction in order to spend the value locked by
+    the earlier transaction. Each such value must be re-locked
+    (with "lock") in its clause.
+
+  statement = verify | unlock | lock
+
+  verify = "verify" expr
+
+    Verifies that boolean expression expr produces a true result.
+
+  unlock = "unlock" expr
+
+    Expr must evaluate to the contract value. This unlocks that
+    value for any use.
+
+  lock = "lock" expr "with" expr
+
+    The first expr must be a blockchain value (i.e., one named
+    with "locks" or "requires"). The second expr must be a
+    program. This unlocks expr and re-locks it with the new
+    program.
+
+  requirements = requirement | requirements "," requirement
+
+  requirement = identifier ":" expr "of" expr
+
+    The first expr must be an amount, the second must be an
+    asset. This denotes that the named value must have the given
+    quantity and asset type.
+
+  params = param | params "," param
+
+  param = idlist ":" identifier
+
+    The identifiers in idlist are individual parameter names. The
+    identifier after the colon is their type. Available types are:
+
+      Amount; Asset; Boolean; Hash; Integer; Program; PublicKey;
+      Signature; String; Time
+
+  idlist = identifier | idlist "," identifier
+
+  expr = unary_expr | binary_expr | call_expr | identifier | "(" expr ")" | literal
+
+  unary_expr = unary_op expr
+
+  binary_expr = expr binary_op expr
+
+  call_expr = expr "(" [args] ")"
+
+    If expr is the name of an Equity contract, then calling it (with
+    the appropriate arguments) produces a program suitable for use
+    in "lock" statements.
+
+    Otherwise, expr should be one of these builtin functions:
+
+      sha3(x)
+        SHA3-256 hash of x.
+      sha256(x)
+        SHA-256 hash of x.
+      size(x)
+        Size in bytes of x.
+      abs(x)
+        Absolute value of x.
+      min(x, y)
+        The lesser of x and y.
+      max(x, y)
+        The greater of x and y.
+      checkTxSig(pubkey, signature)
+        Whether signature matches both the spending
+        transaction and pubkey.
+      concat(x, y)
+        The concatenation of x and y.
+      concatpush(x, y)
+        The concatenation of x with the bytecode sequence
+        needed to push y on the ChainVM stack.
+      before(x)
+        Whether the spending transaction is happening before
+        time x.
+      after(x)
+        Whether the spending transaction is happening after
+        time x.
+      checkTxMultiSig([pubkey1, pubkey2, ...], [sig1, sig2, ...])
+        Like checkTxSig, but for M-of-N signature checks.
+        Every sig must match both the spending transaction and
+        one of the pubkeys. There may be more pubkeys than
+        sigs, but they are only checked left-to-right so must
+        be supplied in the same order as the sigs. The square
+        brackets here are literal and must appear as shown.
+
+  unary_op = "-" | "~"
+
+  binary_op = ">" | "<" | ">=" | "<=" | "==" | "!=" | "^" | "|" |
+        "+" | "-" | "&" | "<<" | ">>" | "%" | "*" | "/"
+
+  args = expr | args "," expr
+
+  literal = int_literal | str_literal | hex_literal
+
+*/
+package compiler
diff --git a/compiler/environ.go b/compiler/environ.go
new file mode 100644 (file)
index 0000000..07c4645
--- /dev/null
@@ -0,0 +1,72 @@
+package compiler
+
+import "fmt"
+
+// name-binding environment
+type environ struct {
+       entries map[string]*envEntry
+       parent  *environ
+}
+
+type envEntry struct {
+       t typeDesc
+       r role
+       c *Contract // if t == contractType
+}
+
+type role int
+
+const (
+       roleKeyword role = 1 + iota
+       roleBuiltin
+       roleContract
+       roleContractParam
+       roleContractValue
+       roleClause
+       roleClauseParam
+       roleClauseValue
+)
+
+var roleDesc = map[role]string{
+       roleKeyword:       "keyword",
+       roleBuiltin:       "built-in function",
+       roleContract:      "contract",
+       roleContractParam: "contract parameter",
+       roleContractValue: "contract value",
+       roleClause:        "clause",
+       roleClauseParam:   "clause parameter",
+       roleClauseValue:   "clause value",
+}
+
+func newEnviron(parent *environ) *environ {
+       return &environ{
+               entries: make(map[string]*envEntry),
+               parent:  parent,
+       }
+}
+
+func (e *environ) add(name string, t typeDesc, r role) error {
+       if entry := e.lookup(name); entry != nil {
+               return fmt.Errorf("%s \"%s\" conflicts with %s", roleDesc[r], name, roleDesc[entry.r])
+       }
+       e.entries[name] = &envEntry{t: t, r: r}
+       return nil
+}
+
+func (e *environ) addContract(contract *Contract) error {
+       if entry := e.lookup(contract.Name); entry != nil {
+               return fmt.Errorf("%s \"%s\" conflicts with %s", roleDesc[roleContract], contract.Name, roleDesc[entry.r])
+       }
+       e.entries[contract.Name] = &envEntry{t: contractType, r: roleContract, c: contract}
+       return nil
+}
+
+func (e environ) lookup(name string) *envEntry {
+       if res, ok := e.entries[name]; ok {
+               return res
+       }
+       if e.parent != nil {
+               return e.parent.lookup(name)
+       }
+       return nil
+}
diff --git a/compiler/equitytest/equitytest.go b/compiler/equitytest/equitytest.go
new file mode 100644 (file)
index 0000000..da6a7a0
--- /dev/null
@@ -0,0 +1,155 @@
+package equitytest
+
+const TrivialLock = `
+contract TrivialLock() locks locked {
+  clause trivialUnlock() {
+    unlock locked
+  }
+}
+`
+
+const LockWithPublicKey = `
+contract LockWithPublicKey(publicKey: PublicKey) locks locked {
+  clause unlockWithSig(sig: Signature) {
+    verify checkTxSig(publicKey, sig)
+    unlock locked
+  }
+}
+`
+
+const LockWithPKHash = `
+contract LockWithPublicKeyHash(pubKeyHash: Hash) locks value {
+  clause spend(pubKey: PublicKey, sig: Signature) {
+    verify sha3(pubKey) == pubKeyHash
+    verify checkTxSig(pubKey, sig)
+    unlock value
+  }
+}
+`
+
+const LockWith2of3Keys = `
+contract LockWith3Keys(pubkey1, pubkey2, pubkey3: PublicKey) locks locked {
+  clause unlockWith2Sigs(sig1, sig2: Signature) {
+    verify checkTxMultiSig([pubkey1, pubkey2, pubkey3], [sig1, sig2])
+    unlock locked
+  }
+}
+`
+
+const LockToOutput = `
+contract LockToOutput(address: Program) locks locked {
+  clause relock() {
+    lock locked with address
+  }
+}
+`
+
+const TradeOffer = `
+contract TradeOffer(requestedAsset: Asset, requestedAmount: Amount, sellerProgram: Program, sellerKey: PublicKey) locks offered {
+  clause trade() requires payment: requestedAmount of requestedAsset {
+    lock payment with sellerProgram
+    unlock offered
+  }
+  clause cancel(sellerSig: Signature) {
+    verify checkTxSig(sellerKey, sellerSig)
+    lock offered with sellerProgram
+  }
+}
+`
+
+const EscrowedTransfer = `
+contract EscrowedTransfer(agent: PublicKey, sender: Program, recipient: Program) locks value {
+  clause approve(sig: Signature) {
+    verify checkTxSig(agent, sig)
+    lock value with recipient
+  }
+  clause reject(sig: Signature) {
+    verify checkTxSig(agent, sig)
+    lock value with sender
+  }
+}
+`
+
+const CollateralizedLoan = `
+contract CollateralizedLoan(balanceAsset: Asset, balanceAmount: Amount, finalHeight: Integer, lender: Program, borrower: Program) locks collateral {
+  clause repay() requires payment: balanceAmount of balanceAsset {
+    lock payment with lender
+    lock collateral with borrower
+  }
+  clause default() {
+    verify above(finalHeight)
+    lock collateral with lender
+  }
+}
+`
+
+const RevealPreimage = `
+contract RevealPreimage(hash: Hash) locks value {
+  clause reveal(string: String) {
+    verify sha3(string) == hash
+    unlock value
+  }
+}
+`
+
+const PriceChanger = `
+contract PriceChanger(askAmount: Amount, askAsset: Asset, sellerKey: PublicKey, sellerProg: Program) locks offered {
+  clause changePrice(newAmount: Amount, newAsset: Asset, sig: Signature) {
+    verify checkTxSig(sellerKey, sig)
+    lock offered with PriceChanger(newAmount, newAsset, sellerKey, sellerProg)
+  }
+  clause redeem() requires payment: askAmount of askAsset {
+    lock payment with sellerProg
+    unlock offered
+  }
+}
+`
+
+const CallOptionWithSettlement = `
+contract CallOptionWithSettlement(strikePrice: Amount,
+                    strikeCurrency: Asset,
+                    sellerProgram: Program,
+                    sellerKey: PublicKey,
+                    buyerKey: PublicKey,
+                    finalHeight: Integer) locks underlying {
+  clause exercise(buyerSig: Signature)
+                 requires payment: strikePrice of strikeCurrency {
+    verify below(finalHeight)
+    verify checkTxSig(buyerKey, buyerSig)
+    lock payment with sellerProgram
+    unlock underlying
+  }
+  clause expire() {
+    verify above(finalHeight)
+    lock underlying with sellerProgram
+  }
+  clause settle(sellerSig: Signature, buyerSig: Signature) {
+    verify checkTxSig(sellerKey, sellerSig)
+    verify checkTxSig(buyerKey, buyerSig)
+    unlock underlying
+  }
+}
+`
+
+const OneTwo = `
+contract Two(b, c: Program, expirationHeight: Integer) locks value {
+  clause redeem() {
+    verify below(expirationHeight)
+    lock value with b
+  }
+  clause default() {
+    verify above(expirationHeight)
+    lock value with c
+  }
+}
+contract One(a, b, c: Program, switchHeight, blockHeight: Integer) locks value {
+  clause redeem() {
+    verify below(switchHeight)
+    lock value with a
+  }
+  clause switch() {
+    verify above(switchHeight)
+    lock value with Two(b, c, blockHeight)
+  }
+}
+`
diff --git a/compiler/optimize.go b/compiler/optimize.go
new file mode 100644 (file)
index 0000000..f05cac4
--- /dev/null
@@ -0,0 +1,64 @@
+package compiler
+
+import "strings"
+
+var optimizations = []struct {
+       before, after string
+}{
+       {"0 ROLL", ""},
+       {"0 PICK", "DUP"},
+       {"1 ROLL", "SWAP"},
+       {"1 PICK", "OVER"},
+       {"2 ROLL", "ROT"},
+       {"TRUE VERIFY", ""},
+       {"SWAP SWAP", ""},
+       {"OVER OVER", "2DUP"},
+       {"SWAP OVER", "TUCK"},
+       {"DROP DROP", "2DROP"},
+       {"SWAP DROP", "NIP"},
+       {"5 ROLL 5 ROLL", "2ROT"},
+       {"3 PICK 3 PICK", "2OVER"},
+       {"3 ROLL 3 ROLL", "2SWAP"},
+       {"2 PICK 2 PICK 2 PICK", "3DUP"},
+       {"1 ADD", "1ADD"},
+       {"1 SUB", "1SUB"},
+       {"EQUAL VERIFY", "EQUALVERIFY"},
+       {"SWAP TXSIGHASH ROT", "TXSIGHASH SWAP"},
+       {"SWAP EQUAL", "EQUAL"},
+       {"SWAP EQUALVERIFY", "EQUALVERIFY"},
+       {"SWAP ADD", "ADD"},
+       {"SWAP BOOLAND", "BOOLAND"},
+       {"SWAP BOOLOR", "BOOLOR"},
+       {"SWAP MIN", "MIN"},
+       {"SWAP MAX", "MAX"},
+       {"DUP 2 PICK EQUAL", "2DUP EQUAL"},
+       {"DUP 2 PICK EQUALVERIFY", "2DUP EQUALVERIFY"},
+       {"DUP 2 PICK ADD", "2DUP ADD"},
+       {"DUP 2 PICK BOOLAND", "2DUP BOOLAND"},
+       {"DUP 2 PICK BOOLOR", "2DUP BOOLOR"},
+       {"DUP 2 PICK MIN", "2DUP MIN"},
+       {"DUP 2 PICK MAX", "2DUP MAX"},
+}
+
+func optimize(opcodes string) string {
+       opcodes = " " + opcodes + " "
+       looping := true
+       for looping {
+               looping = false
+               for _, o := range optimizations {
+                       before := " " + o.before + " "
+                       var after string
+                       if o.after == "" {
+                               after = " "
+                       } else {
+                               after = " " + o.after + " "
+                       }
+                       newOpcodes := strings.Replace(opcodes, before, after, -1)
+                       if newOpcodes != opcodes {
+                               looping = true
+                               opcodes = newOpcodes
+                       }
+               }
+       }
+       return strings.TrimSpace(opcodes)
+}
diff --git a/compiler/parse.go b/compiler/parse.go
new file mode 100644 (file)
index 0000000..61a4765
--- /dev/null
@@ -0,0 +1,563 @@
+package compiler
+
+import (
+       "bytes"
+       "encoding/hex"
+       "fmt"
+       "strconv"
+       "unicode"
+)
+
+// We have some function naming conventions.
+//
+// For terminals:
+//   scanX     takes buf and position, returns new position (and maybe a value)
+//   peekX     takes *parser, returns bool or string
+//   consumeX  takes *parser and maybe a required literal, maybe returns value
+//             also updates the parser position
+//
+// For nonterminals:
+//   parseX    takes *parser, returns AST node, updates parser position
+
+type parser struct {
+       buf []byte
+       pos int
+}
+
+func (p *parser) errorf(format string, args ...interface{}) {
+       panic(parserErr{buf: p.buf, offset: p.pos, format: format, args: args})
+}
+
+// parse is the main entry point to the parser
+func parse(buf []byte) (contracts []*Contract, err error) {
+       defer func() {
+               if val := recover(); val != nil {
+                       if e, ok := val.(parserErr); ok {
+                               err = e
+                       } else {
+                               panic(val)
+                       }
+               }
+       }()
+       p := &parser{buf: buf}
+       contracts = parseContracts(p)
+       return
+}
+
+// parse functions
+
+func parseContracts(p *parser) []*Contract {
+       var result []*Contract
+       for peekKeyword(p) == "contract" {
+               contract := parseContract(p)
+               result = append(result, contract)
+       }
+       return result
+}
+
+// contract name(p1, p2: t1, p3: t2) locks value { ... }
+func parseContract(p *parser) *Contract {
+       consumeKeyword(p, "contract")
+       name := consumeIdentifier(p)
+       params := parseParams(p)
+       consumeKeyword(p, "locks")
+       value := consumeIdentifier(p)
+       consumeTok(p, "{")
+       clauses := parseClauses(p)
+       consumeTok(p, "}")
+       return &Contract{Name: name, Params: params, Clauses: clauses, Value: value}
+}
+
+// (p1, p2: t1, p3: t2)
+func parseParams(p *parser) []*Param {
+       var params []*Param
+       consumeTok(p, "(")
+       first := true
+       for !peekTok(p, ")") {
+               if first {
+                       first = false
+               } else {
+                       consumeTok(p, ",")
+               }
+               pt := parseParamsType(p)
+               params = append(params, pt...)
+       }
+       consumeTok(p, ")")
+       return params
+}
+
+func parseClauses(p *parser) []*Clause {
+       var clauses []*Clause
+       for !peekTok(p, "}") {
+               c := parseClause(p)
+               clauses = append(clauses, c)
+       }
+       return clauses
+}
+
+func parseParamsType(p *parser) []*Param {
+       firstName := consumeIdentifier(p)
+       params := []*Param{&Param{Name: firstName}}
+       for peekTok(p, ",") {
+               consumeTok(p, ",")
+               name := consumeIdentifier(p)
+               params = append(params, &Param{Name: name})
+       }
+       consumeTok(p, ":")
+       typ := consumeIdentifier(p)
+       for _, parm := range params {
+               if tdesc, ok := types[typ]; ok {
+                       parm.Type = tdesc
+               } else {
+                       p.errorf("unknown type %s", typ)
+               }
+       }
+       return params
+}
+
+func parseClause(p *parser) *Clause {
+       var c Clause
+       consumeKeyword(p, "clause")
+       c.Name = consumeIdentifier(p)
+       c.Params = parseParams(p)
+       if peekKeyword(p) == "requires" {
+               consumeKeyword(p, "requires")
+               c.Reqs = parseClauseRequirements(p)
+       }
+       consumeTok(p, "{")
+       c.statements = parseStatements(p)
+       consumeTok(p, "}")
+       return &c
+}
+
+func parseClauseRequirements(p *parser) []*ClauseReq {
+       var result []*ClauseReq
+       first := true
+       for {
+               switch {
+               case first:
+                       first = false
+               case peekTok(p, ","):
+                       consumeTok(p, ",")
+               default:
+                       return result
+               }
+               var req ClauseReq
+               req.Name = consumeIdentifier(p)
+               consumeTok(p, ":")
+               req.amountExpr = parseExpr(p)
+               consumeKeyword(p, "of")
+               req.assetExpr = parseExpr(p)
+               result = append(result, &req)
+       }
+}
+
+func parseStatements(p *parser) []statement {
+       var statements []statement
+       for !peekTok(p, "}") {
+               s := parseStatement(p)
+               statements = append(statements, s)
+       }
+       return statements
+}
+
+func parseStatement(p *parser) statement {
+       switch peekKeyword(p) {
+       case "verify":
+               return parseVerifyStmt(p)
+       case "lock":
+               return parseLockStmt(p)
+       case "unlock":
+               return parseUnlockStmt(p)
+       }
+       panic(parseErr(p.buf, p.pos, "unknown keyword \"%s\"", peekKeyword(p)))
+}
+
+func parseVerifyStmt(p *parser) *verifyStatement {
+       consumeKeyword(p, "verify")
+       expr := parseExpr(p)
+       return &verifyStatement{expr: expr}
+}
+
+func parseLockStmt(p *parser) *lockStatement {
+       consumeKeyword(p, "lock")
+       locked := parseExpr(p)
+       consumeKeyword(p, "with")
+       program := parseExpr(p)
+       return &lockStatement{locked: locked, program: program}
+}
+
+func parseUnlockStmt(p *parser) *unlockStatement {
+       consumeKeyword(p, "unlock")
+       expr := parseExpr(p)
+       return &unlockStatement{expr}
+}
+
+func parseExpr(p *parser) expression {
+       // Uses the precedence-climbing algorithm
+       // <https://en.wikipedia.org/wiki/Operator-precedence_parser#Precedence_climbing_method>
+       expr := parseUnaryExpr(p)
+       expr2, pos := parseExprCont(p, expr, 0)
+       if pos < 0 {
+               p.errorf("expected expression")
+       }
+       p.pos = pos
+       return expr2
+}
+
+func parseUnaryExpr(p *parser) expression {
+       op, pos := scanUnaryOp(p.buf, p.pos)
+       if pos < 0 {
+               return parseExpr2(p)
+       }
+       p.pos = pos
+       expr := parseUnaryExpr(p)
+       return &unaryExpr{op: op, expr: expr}
+}
+
+func parseExprCont(p *parser, lhs expression, minPrecedence int) (expression, int) {
+       for {
+               op, pos := scanBinaryOp(p.buf, p.pos)
+               if pos < 0 || op.precedence < minPrecedence {
+                       break
+               }
+               p.pos = pos
+
+               rhs := parseUnaryExpr(p)
+
+               for {
+                       op2, pos2 := scanBinaryOp(p.buf, p.pos)
+                       if pos2 < 0 || op2.precedence <= op.precedence {
+                               break
+                       }
+                       rhs, p.pos = parseExprCont(p, rhs, op2.precedence)
+                       if p.pos < 0 {
+                               return nil, -1 // or is this an error?
+                       }
+               }
+               lhs = &binaryExpr{left: lhs, right: rhs, op: op}
+       }
+       return lhs, p.pos
+}
+
+func parseExpr2(p *parser) expression {
+       if expr, pos := scanLiteralExpr(p.buf, p.pos); pos >= 0 {
+               p.pos = pos
+               return expr
+       }
+       return parseExpr3(p)
+}
+
+func parseExpr3(p *parser) expression {
+       e := parseExpr4(p)
+       if peekTok(p, "(") {
+               args := parseArgs(p)
+               return &callExpr{fn: e, args: args}
+       }
+       return e
+}
+
+func parseExpr4(p *parser) expression {
+       if peekTok(p, "(") {
+               consumeTok(p, "(")
+               e := parseExpr(p)
+               consumeTok(p, ")")
+               return e
+       }
+       if peekTok(p, "[") {
+               var elts []expression
+               consumeTok(p, "[")
+               first := true
+               for !peekTok(p, "]") {
+                       if first {
+                               first = false
+                       } else {
+                               consumeTok(p, ",")
+                       }
+                       e := parseExpr(p)
+                       elts = append(elts, e)
+               }
+               consumeTok(p, "]")
+               return listExpr(elts)
+       }
+       name := consumeIdentifier(p)
+       return varRef(name)
+}
+
+func parseArgs(p *parser) []expression {
+       var exprs []expression
+       consumeTok(p, "(")
+       first := true
+       for !peekTok(p, ")") {
+               if first {
+                       first = false
+               } else {
+                       consumeTok(p, ",")
+               }
+               e := parseExpr(p)
+               exprs = append(exprs, e)
+       }
+       consumeTok(p, ")")
+       return exprs
+}
+
+// peek functions
+
+func peekKeyword(p *parser) string {
+       name, _ := scanIdentifier(p.buf, p.pos)
+       return name
+}
+
+func peekTok(p *parser, token string) bool {
+       pos := scanTok(p.buf, p.pos, token)
+       return pos >= 0
+}
+
+// consume functions
+
+var keywords = []string{
+       "contract", "clause", "verify", "output", "return",
+       "locks", "requires", "of", "lock", "with", "unlock",
+}
+
+func consumeKeyword(p *parser, keyword string) {
+       pos := scanKeyword(p.buf, p.pos, keyword)
+       if pos < 0 {
+               p.errorf("expected keyword %s", keyword)
+       }
+       p.pos = pos
+}
+
+func consumeIdentifier(p *parser) string {
+       name, pos := scanIdentifier(p.buf, p.pos)
+       if pos < 0 {
+               p.errorf("expected identifier")
+       }
+       p.pos = pos
+       return name
+}
+
+func consumeTok(p *parser, token string) {
+       pos := scanTok(p.buf, p.pos, token)
+       if pos < 0 {
+               p.errorf("expected %s token", token)
+       }
+       p.pos = pos
+}
+
+// scan functions
+
+func scanUnaryOp(buf []byte, offset int) (*unaryOp, int) {
+       // Maximum munch. Make sure "-3" scans as ("-3"), not ("-", "3").
+       if _, pos := scanIntLiteral(buf, offset); pos >= 0 {
+               return nil, -1
+       }
+       for _, op := range unaryOps {
+               newOffset := scanTok(buf, offset, op.op)
+               if newOffset >= 0 {
+                       return &op, newOffset
+               }
+       }
+       return nil, -1
+}
+
+func scanBinaryOp(buf []byte, offset int) (*binaryOp, int) {
+       offset = skipWsAndComments(buf, offset)
+       var (
+               found     *binaryOp
+               newOffset = -1
+       )
+       for i, op := range binaryOps {
+               offset2 := scanTok(buf, offset, op.op)
+               if offset2 >= 0 {
+                       if found == nil || len(op.op) > len(found.op) {
+                               found = &binaryOps[i]
+                               newOffset = offset2
+                       }
+               }
+       }
+       return found, newOffset
+}
+
+// TODO(bobg): boolean literals?
+func scanLiteralExpr(buf []byte, offset int) (expression, int) {
+       offset = skipWsAndComments(buf, offset)
+       intliteral, newOffset := scanIntLiteral(buf, offset)
+       if newOffset >= 0 {
+               return intliteral, newOffset
+       }
+       strliteral, newOffset := scanStrLiteral(buf, offset)
+       if newOffset >= 0 {
+               return strliteral, newOffset
+       }
+       bytesliteral, newOffset := scanBytesLiteral(buf, offset) // 0x6c249a...
+       if newOffset >= 0 {
+               return bytesliteral, newOffset
+       }
+       return nil, -1
+}
+
+func scanIdentifier(buf []byte, offset int) (string, int) {
+       offset = skipWsAndComments(buf, offset)
+       i := offset
+       for ; i < len(buf) && isIDChar(buf[i], i == offset); i++ {
+       }
+       if i == offset {
+               return "", -1
+       }
+       return string(buf[offset:i]), i
+}
+
+func scanTok(buf []byte, offset int, s string) int {
+       offset = skipWsAndComments(buf, offset)
+       prefix := []byte(s)
+       if bytes.HasPrefix(buf[offset:], prefix) {
+               return offset + len(prefix)
+       }
+       return -1
+}
+
+func scanKeyword(buf []byte, offset int, keyword string) int {
+       id, newOffset := scanIdentifier(buf, offset)
+       if newOffset < 0 {
+               return -1
+       }
+       if id != keyword {
+               return -1
+       }
+       return newOffset
+}
+
+func scanIntLiteral(buf []byte, offset int) (integerLiteral, int) {
+       offset = skipWsAndComments(buf, offset)
+       start := offset
+       if offset < len(buf) && buf[offset] == '-' {
+               offset++
+       }
+       i := offset
+       for ; i < len(buf) && unicode.IsDigit(rune(buf[i])); i++ {
+       }
+       if i > offset {
+               n, err := strconv.ParseInt(string(buf[start:i]), 10, 64)
+               if err != nil {
+                       return 0, -1
+               }
+               return integerLiteral(n), i
+       }
+       return 0, -1
+}
+
+func scanStrLiteral(buf []byte, offset int) (bytesLiteral, int) {
+       offset = skipWsAndComments(buf, offset)
+       if offset >= len(buf) || buf[offset] != '\'' {
+               return bytesLiteral{}, -1
+       }
+       for i := offset + 1; i < len(buf); i++ {
+               if buf[i] == '\'' {
+                       return bytesLiteral(buf[offset : i+1]), i + 1
+               }
+               if buf[i] == '\\' {
+                       i++
+               }
+       }
+       panic(parseErr(buf, offset, "unterminated string literal"))
+}
+
+func scanBytesLiteral(buf []byte, offset int) (bytesLiteral, int) {
+       offset = skipWsAndComments(buf, offset)
+       if offset+4 >= len(buf) {
+               return nil, -1
+       }
+       if buf[offset] != '0' || (buf[offset+1] != 'x' && buf[offset+1] != 'X') {
+               return nil, -1
+       }
+       if !isHexDigit(buf[offset+2]) || !isHexDigit(buf[offset+3]) {
+               return nil, -1
+       }
+       i := offset + 4
+       for ; i < len(buf); i += 2 {
+               if i == len(buf)-1 {
+                       panic(parseErr(buf, offset, "odd number of digits in hex literal"))
+               }
+               if !isHexDigit(buf[i]) {
+                       break
+               }
+               if !isHexDigit(buf[i+1]) {
+                       panic(parseErr(buf, offset, "odd number of digits in hex literal"))
+               }
+       }
+       decoded := make([]byte, hex.DecodedLen(i-(offset+2)))
+       _, err := hex.Decode(decoded, buf[offset+2:i])
+       if err != nil {
+               return bytesLiteral{}, -1
+       }
+       return bytesLiteral(decoded), i
+}
+
+func skipWsAndComments(buf []byte, offset int) int {
+       var inComment bool
+       for ; offset < len(buf); offset++ {
+               c := buf[offset]
+               if inComment {
+                       if c == '\n' {
+                               inComment = false
+                       }
+               } else {
+                       if c == '/' && offset < len(buf)-1 && buf[offset+1] == '/' {
+                               inComment = true
+                               offset++ // skip two chars instead of one
+                       } else if !unicode.IsSpace(rune(c)) {
+                               break
+                       }
+               }
+       }
+       return offset
+}
+
+func isHexDigit(b byte) bool {
+       return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'f') || (b >= 'A' && b <= 'F')
+}
+
+func isIDChar(c byte, initial bool) bool {
+       if c >= 'a' && c <= 'z' {
+               return true
+       }
+       if c >= 'A' && c <= 'Z' {
+               return true
+       }
+       if c == '_' {
+               return true
+       }
+       if initial {
+               return false
+       }
+       return unicode.IsDigit(rune(c))
+}
+
+type parserErr struct {
+       buf    []byte
+       offset int
+       format string
+       args   []interface{}
+}
+
+func parseErr(buf []byte, offset int, format string, args ...interface{}) error {
+       return parserErr{buf: buf, offset: offset, format: format, args: args}
+}
+
+func (p parserErr) Error() string {
+       // Lines start at 1, columns start at 0, like nature intended.
+       line := 1
+       col := 0
+       for i := 0; i < p.offset; i++ {
+               if p.buf[i] == '\n' {
+                       line++
+                       col = 0
+               } else {
+                       col++
+               }
+       }
+       args := []interface{}{line, col}
+       args = append(args, p.args...)
+       return fmt.Sprintf("line %d, col %d: "+p.format, args...)
+}
diff --git a/compiler/stack.go b/compiler/stack.go
new file mode 100644 (file)
index 0000000..1459f02
--- /dev/null
@@ -0,0 +1,116 @@
+package compiler
+
+type (
+       stack struct {
+               *stackEntry
+       }
+       stackEntry struct {
+               str  string
+               prev *stackEntry
+       }
+)
+
+func (stk stack) isEmpty() bool {
+       return stk.stackEntry == nil
+}
+
+func (stk stack) top() string {
+       if stk.isEmpty() {
+               return ""
+       }
+       return stk.str
+}
+
+func (stk stack) add(str string) stack {
+       e := &stackEntry{
+               str:  str,
+               prev: stk.stackEntry,
+       }
+       return stack{e}
+}
+
+func (stk stack) addFromStack(other stack) stack {
+       if other.isEmpty() {
+               return stk
+       }
+       res := stk.addFromStack(other.drop())
+       return res.add(other.top())
+}
+
+func (stk stack) drop() stack {
+       if !stk.isEmpty() {
+               stk = stack{stk.prev}
+       }
+       return stk
+}
+
+func (stk stack) dropN(n int) stack {
+       for n > 0 {
+               stk = stk.drop()
+               n--
+       }
+       return stk
+}
+
+func (stk stack) find(str string) int {
+       if stk.isEmpty() {
+               return -1
+       }
+       if stk.str == str {
+               return 0
+       }
+       res := stk.drop().find(str)
+       if res < 0 {
+               return res
+       }
+       return res + 1
+}
+
+func (stk stack) roll(n int) stack {
+       var x func(stack, int) (stack, string)
+       x = func(stk stack, n int) (stack, string) {
+               if n == 0 {
+                       return stk.drop(), stk.top()
+               }
+               stk2, entry := x(stk.drop(), n-1)
+               return stk2.add(stk.top()), entry
+       }
+       stk, entry := x(stk, n)
+       return stk.add(entry)
+}
+
+func (stk stack) swap() stack {
+       a := stk.top()
+       stk = stk.drop()
+       b := stk.top()
+       stk = stk.drop()
+       return stk.add(a).add(b)
+}
+
+func (stk stack) dup() stack {
+       return stk.add(stk.top())
+}
+
+func (stk stack) over() stack {
+       t := stk.drop().top()
+       return stk.add(t)
+}
+
+func (stk stack) pick(n int) stack {
+       t := stk.dropN(n).top()
+       return stk.add(t)
+}
+
+func (stk stack) String() string {
+       if stk.stackEntry == nil {
+               return "[]"
+       }
+       var x func(stk stack) string
+       x = func(stk stack) string {
+               if stk.stackEntry == nil {
+                       return ""
+               }
+               return x(stk.drop()) + " " + stk.stackEntry.str
+       }
+       return "[..." + x(stk) + "]"
+}
diff --git a/compiler/types.go b/compiler/types.go
new file mode 100644 (file)
index 0000000..d8b5b2c
--- /dev/null
@@ -0,0 +1,76 @@
+package compiler
+
+type typeDesc string
+
+var (
+       amountType   = typeDesc("Amount")
+       assetType    = typeDesc("Asset")
+       boolType     = typeDesc("Boolean")
+       contractType = typeDesc("Contract")
+       hashType     = typeDesc("Hash")
+       intType      = typeDesc("Integer")
+       listType     = typeDesc("List")
+       nilType      = typeDesc("")
+       predType     = typeDesc("Predicate")
+       progType     = typeDesc("Program")
+       pubkeyType   = typeDesc("PublicKey")
+       sigType      = typeDesc("Signature")
+       strType      = typeDesc("String")
+       valueType    = typeDesc("Value")
+
+       sha3StrType      = typeDesc("Sha3(String)")
+       sha3PubkeyType   = typeDesc("Sha3(PublicKey)")
+       sha256StrType    = typeDesc("Sha256(String)")
+       sha256PubkeyType = typeDesc("Sha256(PublicKey)")
+)
+
+var types = map[string]typeDesc{
+       string(amountType): amountType,
+       string(assetType):  assetType,
+       string(boolType):   boolType,
+       string(hashType):   hashType,
+       string(intType):    intType,
+       string(listType):   listType,
+       string(nilType):    nilType,
+       string(predType):   predType,
+       string(progType):   progType,
+       string(pubkeyType): pubkeyType,
+       string(sigType):    sigType,
+       string(strType):    strType,
+       string(valueType):  valueType,
+
+       string(sha3StrType):      sha3StrType,
+       string(sha3PubkeyType):   sha3PubkeyType,
+       string(sha256StrType):    sha256StrType,
+       string(sha256PubkeyType): sha256PubkeyType,
+}
+
+func isHashSubtype(t typeDesc) bool {
+       switch t {
+       case sha3StrType, sha3PubkeyType, sha256StrType, sha256PubkeyType:
+               return true
+       }
+       return false
+}
+
+func propagateType(contract *Contract, clause *Clause, env *environ, t typeDesc, e expression) {
+       v, ok := e.(varRef)
+       if !ok {
+               return
+       }
+       if entry := env.lookup(string(v)); entry != nil {
+               entry.t = t
+               for _, p := range contract.Params {
+                       if p.Name == string(v) {
+                               p.InferredType = t
+                               return
+                       }
+               }
+               for _, p := range clause.Params {
+                       if p.Name == string(v) {
+                               p.InferredType = t
+                               return
+                       }
+               }
+       }
+}