diff --git a/.travis.yml b/.travis.yml index 2e6f8000..87456312 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,12 +3,17 @@ language: scala scala: - 2.13.5 -node_js: - - 15.11.0 - jdk: - oraclejdk12 # as of 10 june 2021, up to jdk12 works but 13+ fail +before_install: + - nvm install 16 # as of 19 nov 2021, this version of node works with ganache and 17 does not + - npm install -g npm # update npm + - npm install -g ganache-cli # install ganache + - sudo apt update + - sudo apt install libcryptx-perl # crypto library needed for hashing in ganache interactions + - sudo snap install jq # JSON tool needed for ganache interactions + install: - gradle publish -b Obsidian_Runtime/build.gradle @@ -35,5 +40,4 @@ before_cache: - find $HOME/.sbt -name "*.lock" -print -delete addons: - hosts: - - localhost + hostname: localhost diff --git a/resources/tests/GanacheTests/SetGetConstructorArgs.json b/resources/tests/GanacheTests/SetGetConstructorArgs.json new file mode 100644 index 00000000..7e5a461e --- /dev/null +++ b/resources/tests/GanacheTests/SetGetConstructorArgs.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "main()", + "expected" : "12" +} diff --git a/resources/tests/GanacheTests/SetGetConstructorArgs.obs b/resources/tests/GanacheTests/SetGetConstructorArgs.obs new file mode 100644 index 00000000..6f84a182 --- /dev/null +++ b/resources/tests/GanacheTests/SetGetConstructorArgs.obs @@ -0,0 +1,22 @@ +contract IntContainer{ + int x; + + IntContainer@Owned(int init) { + x = init; + } + + transaction set(int value) { + x = value; + } + transaction get() returns int{ + return x; + } +} + +main contract SetGetConstructorArgs{ + transaction main() returns int{ + IntContainer ic = new IntContainer(5); + ic.set(12); + return (ic.get()); + } +} diff --git a/resources/tests/GanacheTests/SetGetConstructorNoArgs.json b/resources/tests/GanacheTests/SetGetConstructorNoArgs.json new file mode 100644 index 00000000..7e5a461e --- /dev/null +++ b/resources/tests/GanacheTests/SetGetConstructorNoArgs.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "main()", + "expected" : "12" +} diff --git a/resources/tests/GanacheTests/SetGetConstructorNoArgs.obs b/resources/tests/GanacheTests/SetGetConstructorNoArgs.obs index 23a05d2d..deb38349 100644 --- a/resources/tests/GanacheTests/SetGetConstructorNoArgs.obs +++ b/resources/tests/GanacheTests/SetGetConstructorNoArgs.obs @@ -1,22 +1,22 @@ contract IntContainer{ int x; - IntContainer() { + IntContainer@Owned() { x = 0; } - transaction set() { // take no parameters - x = 5; + transaction set(int value) { + x = value; } transaction get() returns int{ return x; } } -main contract SetGetNoArgs{ +main contract SetGetConstructorNoArgs{ transaction main() returns int{ IntContainer ic = new IntContainer(); - ic.set(); + ic.set(12); return (ic.get()); } } diff --git a/resources/tests/GanacheTests/SetGetConstructorNoArgsNoSet.json b/resources/tests/GanacheTests/SetGetConstructorNoArgsNoSet.json new file mode 100644 index 00000000..3c30ec3a --- /dev/null +++ b/resources/tests/GanacheTests/SetGetConstructorNoArgsNoSet.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "main()", + "expected" : "0" +} diff --git a/resources/tests/GanacheTests/SetGetConstructorNoArgsNoSet.obs b/resources/tests/GanacheTests/SetGetConstructorNoArgsNoSet.obs new file mode 100644 index 00000000..ff67ba55 --- /dev/null +++ b/resources/tests/GanacheTests/SetGetConstructorNoArgsNoSet.obs @@ -0,0 +1,21 @@ +contract IntContainer{ + int x; + + IntContainer@Owned() { + x = 0; + } + + transaction set(int value) { + x = value; + } + transaction get() returns int{ + return x; + } +} + +main contract SetGetConstructorNoArgsNoSet{ + transaction main() returns int{ + IntContainer ic = new IntContainer(); + return (ic.get()); + } +} diff --git a/resources/tests/GanacheTests/SetGetTwoConstructors.json b/resources/tests/GanacheTests/SetGetTwoConstructors.json new file mode 100644 index 00000000..800800d9 --- /dev/null +++ b/resources/tests/GanacheTests/SetGetTwoConstructors.json @@ -0,0 +1,8 @@ +{ + "gas" : 30000000, + "gasprice" : "0x9184e72a000", + "startingeth" : 5000000, + "numaccts" : 1, + "testexp" : "main()", + "expected" : "15" +} diff --git a/resources/tests/GanacheTests/SetGetTwoConstructors.obs b/resources/tests/GanacheTests/SetGetTwoConstructors.obs new file mode 100644 index 00000000..64b813de --- /dev/null +++ b/resources/tests/GanacheTests/SetGetTwoConstructors.obs @@ -0,0 +1,27 @@ +contract IntContainer{ + int x; + + IntContainer@Owned(int init) { + x = init; + } + + IntContainer@Owned(int a, int b) { + x = a + b; + } + + transaction set(int value) { + x = value; + } + transaction get() returns int{ + return x; + } +} + +main contract SetGetTwoConstructors{ + transaction main() returns int{ + IntContainer ic = new IntContainer(5,5); + int x = ic.get(); + ic.set(x+5); + return (ic.get()); + } +} diff --git a/src/main/scala/edu/cmu/cs/obsidian/codegen/CodeGenYul.scala b/src/main/scala/edu/cmu/cs/obsidian/codegen/CodeGenYul.scala index 0f9615bb..46700846 100644 --- a/src/main/scala/edu/cmu/cs/obsidian/codegen/CodeGenYul.scala +++ b/src/main/scala/edu/cmu/cs/obsidian/codegen/CodeGenYul.scala @@ -1,7 +1,7 @@ package edu.cmu.cs.obsidian.codegen import edu.cmu.cs.obsidian.CompilerOptions -import edu.cmu.cs.obsidian.typecheck.ObsidianType +import edu.cmu.cs.obsidian.typecheck.{NonPrimitiveType, ObsidianType, UnitType} import java.io.{File, FileWriter} import java.nio.file.{Files, Path, Paths} @@ -117,13 +117,13 @@ object CodeGenYul extends CodeGenerator { * @return the yul statements corresponding to the declaration */ def translateDeclaration(declaration: Declaration, contractName: String, checkedTable: SymbolTable, inMain: Boolean): Seq[YulStatement] = { + // nb there is a .asInstanceOf in the mustache glue code that only works if this really + // returns a sequence of FunctionDeclaration objects. that's OK for now because it's true, + // but as the cases below here get filled in that may not be true and we'll have to fix it. + declaration match { case _: Field => Seq() // fields are translated as they are encountered case t: Transaction => Seq(translateTransaction(t, contractName, checkedTable, inMain)) - - // nb there is a .asInstanceOf in the mustache glue code that only works if this really - // returns a sequence of FunctionDeclaration objects. that's OK for now because it's true, - // but as the cases below here get filled in that may not be true and we'll have to fix it. case _: State => assert(assertion = false, "TODO") Seq() @@ -133,9 +133,35 @@ object CodeGenYul extends CodeGenerator { case _: JavaFFIContractImpl => assert(assertion = false, "Java contracts not supported in Yul translation") Seq() - case _: Constructor => - assert(assertion = false, "constructors not supported in Yul translation") - Seq() + case c: Constructor => + // given an obsidian type, pull out the non-primitive type or raise an exception + def nonprim(t: ObsidianType): NonPrimitiveType = { + t match { + case npt: NonPrimitiveType => npt + case _ => throw new RuntimeException("needed a non-primitive type") + } + } + + // constructors turn into transactions with a special name and the same body + Seq(translateTransaction( + Transaction( + //to support multiple constructors, constructors get the hash of their + // argument types added to their name + name = c.name + hashOfFunctionName(c.name, c.args.map(v => v.typIn.toString)), + // we omit generic type information because we don't have it and would need + // to reconstruct it, and we don't use it to translate to yul anyway + params = Seq(), + args = c.args, + retType = c.retType, + // we omit any ensures because that feature is largely deprecated + ensures = Seq(), + body = c.body, + isStatic = false, + isPrivate = false, + thisType = nonprim(c.thisType), + thisFinalType = nonprim(c.thisFinalType) + ), + contractName, checkedTable, inMain)) case _: TypeDecl => assert(assertion = false, "TODO") Seq() @@ -163,10 +189,14 @@ object CodeGenYul extends CodeGenerator { // return the function definition formed from the above parts, with an added special argument called `this` for the address // of the allocated instance on which it should act addThisArgument( - FunctionDefinition(name = if (inMain) { transaction.name } else { transactionNameMapping(contractName, transaction.name) }, - parameters = transaction.args.map(v => TypedName(v.varName, v.typIn)), - ret, - body = Block(body))) + FunctionDefinition(name = if (inMain) { + transaction.name + } else { + transactionNameMapping(contractName, transaction.name) + }, + parameters = transaction.args.map(v => TypedName(v.varName, v.typIn)), + ret, + body = Block(body))) } /** @@ -320,22 +350,22 @@ object CodeGenYul extends CodeGenerator { /** This encapsulates a general pattern of translation shared between both local and general * invocations, as called below in the two relevant cases of translate expression. * - * @param name the name of the thing being invoked - * @param args the arguments to the invokee - * @param obstype the type at the invocation site - * @param thisID where to look in memory for the relevant fields - * @param retvar the tempory variable to store the return + * @param name the name of the thing being invoked + * @param args the arguments to the invokee + * @param obstype the type at the invocation site + * @param thisID where to look in memory for the relevant fields + * @param retvar the tempory variable to store the return * @param contractName the overall name of the contract being translated * @param checkedTable the checked tabled for the overall contract - * @param inMain whether or not this is being elborated in main + * @param inMain whether or not this is being elborated in main * @return the sequence of yul statements that are the translation of the invocation so described */ - def translateInvocation(name : String, + def translateInvocation(name: String, args: Seq[Expression], obstype: Option[ObsidianType], thisID: Identifier, retvar: Identifier, contractName: String, checkedTable: SymbolTable, inMain: Boolean - ): Seq[YulStatement] ={ + ): Seq[YulStatement] = { // look up the name of the function in the table, get its return type, and then compute // how wide of a tuple that return type is. right now that's either 1 (if the // transaction returns) or 0 (because it's void) @@ -456,27 +486,50 @@ object CodeGenYul extends CodeGenerator { val recipient_yul = translateExpr(id_recipient, recipient, contractName, checkedTable, inMain) (decl_0exp(id_recipient) +: recipient_yul) ++ - // todo: this may be the cause of a bug in the future. this is how non-main functions get their names translated before calling, but i'm not sure that's right at all. + // todo: this may be the cause of a bug in the future. this is how non-main + // functions get their names translated before calling, but that might not + // work with multiple contracts and private transactions. i'm not sure. translateInvocation(transactionNameMapping(getContractName(recipient), name), args, obstype, id_recipient, - retvar, contractName, checkedTable, inMain - ) + retvar, contractName, checkedTable, inMain) case Construction(contractType, args, isFFIInvocation, obstype) => - // todo: currently we ignore the arguments to the constructor - assert(args.isEmpty, "contracts that take arguments are not yet supported") - + // grab an identifier to store memory val id_memaddr = nextTemp() - Seq( - // grab the appropriate amount of space of memory sequentially, off the free memory pointer + // store the names of the types of the arguments + val typeNames = args.map(e => e.obstype.get.toString) + + // given a declaration, test if it's a constructor with type arguments that match the usage here + def isMatchingConstructor(d: Declaration): Boolean = + d match { + case c: Constructor => typeNames == c.args.map(v => v.typIn.toString) + case _ => false + } + + // check to to see if there is a constructor to call, and if so translate the + // arguments and invoke the constructor as normal transaction with the hash appended + // to the name to call the right one + val conCall = + if (checkedTable.contract(contractType.contractName).get.contract.declarations.exists(d => isMatchingConstructor(d))) { + translateInvocation(name = transactionNameMapping(contractType.contractName, contractType.contractName) + hashOfFunctionName(contractType.contractName, typeNames), + args = args, + obstype = Some(UnitType()), + thisID = id_memaddr, + retvar = retvar, contractName = contractName, checkedTable = checkedTable, inMain = inMain) + } else { + Seq() + } + + Seq(// grab the appropriate amount of space of memory sequentially, off the free memory pointer decl_1exp(id_memaddr, apply("allocate_memory", intlit(sizeOfContractST(contractType.contractName, checkedTable)))), // return the address that the space starts at - assign1(retvar, id_memaddr) - ) + assign1(retvar, id_memaddr)) ++ conCall + + case StateInitializer(stateName, fieldName, obstype) => assert(assertion = false, "TODO: translation of " + e.toString + " is not implemented") Seq() diff --git a/src/main/scala/edu/cmu/cs/obsidian/codegen/Util.scala b/src/main/scala/edu/cmu/cs/obsidian/codegen/Util.scala index 0779d6f7..2cdcd553 100644 --- a/src/main/scala/edu/cmu/cs/obsidian/codegen/Util.scala +++ b/src/main/scala/edu/cmu/cs/obsidian/codegen/Util.scala @@ -212,7 +212,7 @@ object Util { case Int256Type() => 1 case UnitType() => 0 } - case _: NonPrimitiveType => assert(assertion = false, "width not implemented for non-primitive types!"); -1 + case _: NonPrimitiveType => 1 case BottomType() => assert(assertion = false, "width not implemented for the bottom type!"); -1 } } diff --git a/travis_specific/ganache_tests.sh b/travis_specific/ganache_tests.sh index 428dd1a8..32277255 100755 --- a/travis_specific/ganache_tests.sh +++ b/travis_specific/ganache_tests.sh @@ -1,11 +1,5 @@ #!/bin/bash -# travis makes this env var available to all builds, so this stops us from installing things locally -if [[ $CI == "true" ]] -then - ./travis_specific/install_ganache.sh -fi - # note: this won't be set locally so either set it on your machine to make # sense or run this only via travis. cd "$TRAVIS_BUILD_DIR" || exit 1 @@ -19,6 +13,15 @@ then exit 1 fi + +# print version info +echo "-----------------------------" +echo "VERSIONS:" +echo "npm " "$(npm --version)" +echo "node " "$(node --version)" +echo "ganache " "$(ganache-cli --version)" +echo "-----------------------------" + # either test only the directories named as arguments or test everything if nothing is specified. # since the travis.yml file doesn't give any argument here, CI will test everything, but this makes # local testing easier. @@ -43,7 +46,7 @@ fi failed=() # build a jar of Obsidian but removing all the tests; that happens in the other Travis matrix job, so we can assume it works here. -sbt 'set assembly / test := {}' ++$TRAVIS_SCALA_VERSION assembly +sbt 'set assembly / test := {}' ++"$TRAVIS_SCALA_VERSION" assembly # check that the jar file for obsidian exists; `sbt assembly` ought to have been run before this script gets run obsidian_jar="$(find target/scala* -name obsidianc.jar | head -n1)" @@ -53,6 +56,8 @@ then exit 1 fi +ganache_host="http://localhost:8545" + for test in "${tests[@]}" do echo "---------------------------------------------------------------" @@ -85,7 +90,7 @@ do fi # compile the contract to yul, also creating the directory to work in, failing otherwise - if ! $(java -jar $obsidian_jar --yul "resources/tests/GanacheTests/$NAME.obs") + if ! $(java -jar "$obsidian_jar" --yul "resources/tests/GanacheTests/$NAME.obs") then echo "$NAME test failed: cannot compile obs to yul" failed+=("$test [compile obs to yul]") @@ -118,7 +123,7 @@ do # start up ganache echo "starting ganache-cli" - ganache-cli --gasLimit "$GAS" --accounts="$NUM_ACCT" --defaultBalanceEther="$START_ETH" &> /dev/null & + ganache-cli --host localhost --gasLimit "$GAS" --accounts="$NUM_ACCT" --defaultBalanceEther="$START_ETH" &> /dev/null & # form the JSON object to ask for the list of accounts ACCT_DATA=$( jq -ncM \ @@ -137,7 +142,7 @@ do ACCTS="" until [ "$KEEPGOING" -eq 0 ] ; do - ACCTS=$(curl --silent -X POST --data "$ACCT_DATA" http://localhost:8545) + ACCTS=$(curl --silent -X POST --data "$ACCT_DATA" http://localhost:8545) # debug KEEPGOING=$? sleep 1 done @@ -173,7 +178,7 @@ do echo "$SEND_DATA" | jq echo - RESP=$(curl -s -X POST --data "$SEND_DATA" http://localhost:8545) + RESP=$(curl -s -X POST --data "$SEND_DATA" "$ganache_host") echo "response from ganache is: " #$RESP echo "$RESP" | jq @@ -215,7 +220,7 @@ do echo "$SEND_DATA" | jq echo - RESP=$(curl -s -X POST --data "$SEND_DATA" http://localhost:8545) + RESP=$(curl -s -X POST --data "$SEND_DATA" "$ganache_host") echo "response from ganache is: " echo "$RESP" | jq @@ -295,7 +300,7 @@ do echo "$SEND_DATA" | jq echo - RESP=$(curl -s -X POST --data "$SEND_DATA" http://localhost:8545) + RESP=$(curl -s -X POST --data "$SEND_DATA" "$ganache_host") echo "response from ganache is: " echo "$RESP" | jq diff --git a/travis_specific/install_ganache.sh b/travis_specific/install_ganache.sh deleted file mode 100755 index 9c8b6c04..00000000 --- a/travis_specific/install_ganache.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -npm install -g npm -npm install -g ganache-cli - -sudo snap install jq # a commandline JSON tool - -sudo apt update -sudo apt install libcryptx-perl # perl crypto library that implements Keccak256