fix(ci): Use GitHub ref for setup-java.
Some checks failed
Build and publish / verify (push) Has been cancelled
Build and publish / publish (push) Has been cancelled

This commit is contained in:
Hellings 2026-09-12 08:38:52 -04:00
parent 1ce3286451
commit 4f68bc964b
8 changed files with 250 additions and 26 deletions

View file

@ -11,7 +11,7 @@ jobs:
runs-on: docker runs-on: docker
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-java@v4 - uses: https://github.com/actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: "25" java-version: "25"

View file

@ -58,6 +58,7 @@ enum class OpCode(val code: Byte) {
data class Instruction(val opCode: OpCode, val immediate: ByteArray = byteArrayOf()) { data class Instruction(val opCode: OpCode, val immediate: ByteArray = byteArrayOf()) {
init { init {
require(opCode != OpCode.PUSH32 || immediate.size == 32) require(opCode != OpCode.PUSH32 || immediate.size == 32)
require(opCode == OpCode.PUSH32 || immediate.isEmpty())
} }
} }
@ -78,8 +79,10 @@ object TwistBytecode {
while (input.hasRemaining()) { while (input.hasRemaining()) {
val code = input.get() val code = input.get()
val op = OpCode.entries.firstOrNull { it.code == code } ?: error("Unknown opcode") val op = OpCode.entries.firstOrNull { it.code == code } ?: error("Unknown opcode")
val immediate = val immediate = if (op == OpCode.PUSH32) {
if (op == OpCode.PUSH32) ByteArray(32).also(input::get) else byteArrayOf() require(input.remaining() >= 32) { "Truncated PUSH32 immediate" }
ByteArray(32).also(input::get)
} else byteArrayOf()
output += Instruction(op, immediate) output += Instruction(op, immediate)
} }
return output return output

View file

@ -43,7 +43,49 @@ data class ContractIr(
val functions: List<FunctionIr>, val functions: List<FunctionIr>,
val accessPolicy: IrAccessPolicy = IrAccessPolicy(), val accessPolicy: IrAccessPolicy = IrAccessPolicy(),
) { ) {
fun bytecode(): ByteArray = TwistBytecode.encode(functions.flatMap { it.instructions }) /**
* Encodes a deployed contract. Calldata word zero is the function selector and all
* function arguments begin at word one. A non-matching selector deterministically reverts.
*/
fun bytecode(): ByteArray {
require(functions.map { it.selector }.distinct().size == functions.size) { "Duplicate function selector" }
val dispatchSize = 2 + functions.size * 5 + 1
var nextBody = dispatchSize
val targets = functions.associate { function ->
function.selector to nextBody.also { nextBody += function.instructions.size }
}
val output = mutableListOf(
Instruction(OpCode.PUSH32, Word256.ZERO.toBytes()),
Instruction(OpCode.CALLDATA_LOAD),
)
functions.forEach { function ->
output += Instruction(OpCode.DUP)
output += Instruction(OpCode.PUSH32, Word256.fromLong(function.selector.toLong()).toBytes())
output += Instruction(OpCode.EQ)
output += Instruction(OpCode.PUSH32, Word256.fromLong(targets.getValue(function.selector).toLong()).toBytes())
output += Instruction(OpCode.JUMPI)
}
output += Instruction(OpCode.REVERT)
functions.forEach { function ->
val offset = targets.getValue(function.selector)
output += function.instructions.mapIndexed { index, instruction ->
if (
instruction.opCode == OpCode.PUSH32 &&
function.instructions.getOrNull(index + 1)?.opCode in setOf(OpCode.JUMP, OpCode.JUMPI)
) {
Instruction(
OpCode.PUSH32,
Word256.of(
Word256.fromBytes(instruction.immediate).toBigInteger() +
java.math.BigInteger.valueOf(offset.toLong())
)
.toBytes(),
)
} else instruction
}
}
return TwistBytecode.encode(output)
}
} }
data class FunctionIr(val name: String, val selector: Int, val instructions: List<Instruction>) data class FunctionIr(val name: String, val selector: Int, val instructions: List<Instruction>)

View file

@ -3,6 +3,7 @@ package rip.crit.twist.compiler
import java.math.BigInteger import java.math.BigInteger
import rip.crit.twist.bytecode.Instruction import rip.crit.twist.bytecode.Instruction
import rip.crit.twist.bytecode.OpCode import rip.crit.twist.bytecode.OpCode
import rip.crit.twist.jit.LLVMBuilder
import rip.crit.twist.tvm.Word256 import rip.crit.twist.tvm.Word256
/** /**
@ -23,6 +24,108 @@ class TwistMlCompiler {
FunctionIr(function.name, index + 1, Lowerer(function.parameters).lower(function.body)) FunctionIr(function.name, index + 1, Lowerer(function.parameters).lower(function.body))
}, },
) )
/** Lowers pure arithmetic and explicit storage effects to portable LLVM IR. */
fun lowerToLlvm(source: String, moduleName: String = "twistml"): LLVMBuilder {
val contract = parse(source)
return LLVMBuilder(moduleName, LlvmLowerer(contract).lower())
}
}
private class LlvmLowerer(private val contract: TwistMlContract) {
private var value = 0
private var label = 0
private val lines = mutableListOf<String>()
private var needsLoad = false
private var needsStore = false
private var needsCaller = false
private var needsCallValue = false
fun lower(): String {
contract.functions.forEach { function -> lowerFunction(function) }
return buildString {
append("target triple = \"unknown-unknown-unknown\"\n")
if (needsLoad) append("declare i256 @twist_sload(i256)\n")
if (needsStore) append("declare void @twist_sstore(i256, i256)\n")
if (needsCaller) append("declare i256 @twist_caller()\n")
if (needsCallValue) append("declare i256 @twist_callvalue()\n")
append(lines.joinToString("\n"))
}
}
private fun lowerFunction(function: TwistMlFunction) {
value = 0
label = 0
val arguments = function.parameters.mapIndexed { index, _ -> "i256 %arg$index" }
lines += "define i256 @${function.name}(" + arguments.joinToString(", ") + ") {"
lines += "entry:"
val environment = function.parameters.mapIndexed { index, name -> name to "%arg$index" }.toMap()
val result = emit(function.body, environment)
lines += " ret i256 $result"
lines += "}"
}
private fun emit(expression: TwistMlExpr, environment: Map<String, String>): String = when (expression) {
is TwistMlExpr.Number -> expression.value.toString()
is TwistMlExpr.Variable -> environment[expression.name] ?: error("Unknown variable ${expression.name}")
is TwistMlExpr.Let -> emit(expression.body, environment + (expression.name to emit(expression.value, environment)))
TwistMlExpr.Caller -> {
needsCaller = true
call("i256 @twist_caller()")
}
TwistMlExpr.CallValue -> {
needsCallValue = true
call("i256 @twist_callvalue()")
}
is TwistMlExpr.Load -> {
needsLoad = true
call("i256 @twist_sload(${emit(expression.key, environment)})")
}
is TwistMlExpr.Store -> {
needsStore = true
lines += " call void @twist_sstore(i256 ${emit(expression.key, environment)}, i256 ${emit(expression.value, environment)})"
"0"
}
is TwistMlExpr.Binary -> {
val left = emit(expression.left, environment)
val right = emit(expression.right, environment)
when (expression.operator) {
"+" -> instruction("add i256 $left, $right")
"-" -> instruction("sub i256 $left, $right")
"*" -> instruction("mul i256 $left, $right")
"/" -> instruction("udiv i256 $left, $right")
"%" -> instruction("urem i256 $left, $right")
"&&" -> instruction("and i256 $left, $right")
"||" -> instruction("or i256 $left, $right")
"=", "==", "<", ">" -> {
val predicate = mapOf("=" to "eq", "==" to "eq", "<" to "ult", ">" to "ugt").getValue(expression.operator)
val comparison = instruction("icmp $predicate i256 $left, $right")
instruction("zext i1 $comparison to i256")
}
else -> error("Unsupported operator ${expression.operator}")
}
}
is TwistMlExpr.If -> {
val condition = emit(expression.condition, environment)
val test = instruction("icmp ne i256 $condition, 0")
val id = label++
val thenLabel = "then$id"
val elseLabel = "else$id"
val mergeLabel = "merge$id"
lines += " br i1 $test, label %$thenLabel, label %$elseLabel"
lines += "$thenLabel:"
val thenValue = emit(expression.thenBranch, environment)
lines += " br label %$mergeLabel"
lines += "$elseLabel:"
val elseValue = emit(expression.elseBranch, environment)
lines += " br label %$mergeLabel"
lines += "$mergeLabel:"
instruction("phi i256 [$thenValue, %$thenLabel], [$elseValue, %$elseLabel]")
}
}
private fun call(body: String): String = instruction("call $body")
private fun instruction(body: String): String = "%v${value++}".also { lines += " $it = $body" }
} }
data class TwistMlContract(val name: String, val functions: List<TwistMlFunction>) data class TwistMlContract(val name: String, val functions: List<TwistMlFunction>)
@ -43,7 +146,7 @@ sealed interface TwistMlExpr {
private class Lowerer(parameters: List<String>) { private class Lowerer(parameters: List<String>) {
private val parameterValues = parameters.mapIndexed { index, name -> private val parameterValues = parameters.mapIndexed { index, name ->
name to listOf(Instruction(OpCode.PUSH32, Word256.fromLong((index * 32).toLong()).toBytes()), Instruction(OpCode.CALLDATA_LOAD)) name to listOf(Instruction(OpCode.PUSH32, Word256.fromLong(((index + 1) * 32).toLong()).toBytes()), Instruction(OpCode.CALLDATA_LOAD))
}.toMap() }.toMap()
fun lower(expression: TwistMlExpr): List<Instruction> = expression.lower(parameterValues) + Instruction(OpCode.RETURN) fun lower(expression: TwistMlExpr): List<Instruction> = expression.lower(parameterValues) + Instruction(OpCode.RETURN)

View file

@ -45,6 +45,8 @@ import rip.crit.twist.tvm.Calldata
import rip.crit.twist.tvm.TransientStorage import rip.crit.twist.tvm.TransientStorage
import rip.crit.twist.tvm.Word256 import rip.crit.twist.tvm.Word256
import rip.crit.twist.tvm.BytecodeVirtualMachine import rip.crit.twist.tvm.BytecodeVirtualMachine
import rip.crit.twist.tvm.ExecutionContext
import rip.crit.twist.tvm.StateExecutionHost
import rip.crit.twist.bytecode.Instruction import rip.crit.twist.bytecode.Instruction
import rip.crit.twist.bytecode.OpCode import rip.crit.twist.bytecode.OpCode
import rip.crit.twist.p2p.PeerNetwork import rip.crit.twist.p2p.PeerNetwork
@ -394,12 +396,36 @@ class AppTest {
""".trimIndent() """.trimIndent()
val contract = TwistMlCompiler().compile(source) val contract = TwistMlCompiler().compile(source)
assertEquals(listOf("max", "stored"), contract.functions.map { it.name }) assertEquals(listOf("max", "stored"), contract.functions.map { it.name })
val input = Word256.fromLong(3).toBytes() + Word256.fromLong(7).toBytes() val input =
val result = BytecodeVirtualMachine().execute(contract.functions.first().instructions.let(TwistBytecode::encode), input, Address("ml")) Word256.fromLong(1).toBytes() +
Word256.fromLong(3).toBytes() +
Word256.fromLong(7).toBytes()
val result = BytecodeVirtualMachine().execute(contract.bytecode(), input, Address("ml"))
assertTrue(result.success) assertTrue(result.success)
assertEquals(BigInteger.valueOf(7), Word256.fromBytes(result.output).toBigInteger()) assertEquals(BigInteger.valueOf(7), Word256.fromBytes(result.output).toBigInteger())
} }
@Test
fun twistoTwistMlContractInitializesAndTransfersOnTheVm() {
val contract = TwistMlCompiler().compile(Files.readString(Path.of("protocols/tokens/twisto.twml")))
val code = contract.bytecode()
val state = InMemoryWorldState()
val address = Address("twisto")
val owner = Address("owner")
fun call(caller: Address, selector: Long, vararg arguments: Word256) =
BytecodeVirtualMachine().execute(
code,
Word256.fromLong(selector).toBytes() + arguments.fold(byteArrayOf()) { bytes, word -> bytes + word.toBytes() },
StateExecutionHost(ExecutionContext(address, caller, Amount(BigInteger.ZERO)), state),
)
assertTrue(call(owner, 1, Word256.fromLong(99), Word256.fromLong(10), Word256.fromLong(10)).success)
val recipient = Word256.fromBytes(Sha256.bytes("recipient".encodeToByteArray()))
assertTrue(call(owner, 6, recipient, Word256.fromLong(4)).success)
val ownerKey = Word256.fromBytes(Sha256.bytes(owner.value.encodeToByteArray()))
assertEquals(BigInteger.valueOf(6), Word256.fromBytes(call(owner, 5, ownerKey).output).toBigInteger())
assertEquals(BigInteger.valueOf(4), Word256.fromBytes(call(owner, 5, recipient).output).toBigInteger())
}
@Test @Test
fun didMethodsResolveDocuments() { fun didMethodsResolveDocuments() {
val publicKey = ByteArray(32) { it.toByte() } val publicKey = ByteArray(32) { it.toByte() }

View file

@ -1,31 +1,31 @@
package rip.crit.twist.tool.twisto package rip.crit.twist.tool.twisto
import java.math.BigInteger import java.math.BigInteger
import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import rip.crit.twist.bips.FileStorageContract import rip.crit.twist.bips.FileStorageContract
import rip.crit.twist.core.Address import rip.crit.twist.compiler.TwistMlCompiler
import rip.crit.twist.core.Amount
import rip.crit.twist.store.FileKeyValueStore
import rip.crit.twist.twisto.TwistoMetadata import rip.crit.twist.twisto.TwistoMetadata
import rip.crit.twist.twisto.TwistoToken import rip.crit.twist.tvm.Word256
fun main(args: Array<String>) { fun main(args: Array<String>) {
require(args.size in 1..3) { require(args.size in 1..3) {
"Usage is twisto <data-folder> [owner] [initial-supply]" "Usage is twisto <data-folder> [owner-label] [initial-supply]"
} }
val root = Path.of(args[0]) val root = Path.of(args[0])
val owner = Address(args.getOrNull(1) ?: "twisto-owner") val owner = args.getOrNull(1) ?: "twisto-owner"
val token = val supply = BigInteger(args.getOrNull(2) ?: "1000000")
TwistoToken( val metadata = FileStorageContract(root.resolve("metadata"))
owner, val pointer = metadata.put(TwistoMetadata(description = "Named token on the Twist network").encode())
FileKeyValueStore(root.resolve("state")), val source = checkNotNull(object {}.javaClass.getResource("/twisto.twml")) { "Missing Twisto contract source" }.readText()
FileStorageContract(root.resolve("metadata")), val code = TwistMlCompiler().compile(source).bytecode()
) Files.createDirectories(root)
val pointer = Files.write(root.resolve("twisto.tbc"), code)
token.metadataPointer() val initialization =
?: token.deploy( Word256.fromLong(1).toBytes() +
TwistoMetadata(description = "Named token on the Twist network"), Word256.fromBytes(pointer.value.chunked(2).map { it.toInt(16).toByte() }.toByteArray()).toBytes() +
Amount(BigInteger(args.getOrNull(2) ?: "1000000")), Word256.of(supply).toBytes() +
) Word256.of(supply).toBytes()
println("deployed symbol=${token.symbol} metadata=${pointer.value} owner=${owner.value}") Files.write(root.resolve("initialize.calldata"), initialization)
println("compiled contract=${root.resolve("twisto.tbc")} metadata=${pointer.value} owner-label=$owner")
} }

View file

@ -0,0 +1,10 @@
contract Twisto {
fun initialize(metadata, supply, cap) = if load(0) == 0 then let ignored = store(0, caller) in let ignored = store(1, metadata) in let ignored = store(2, cap) in let ignored = store(3, supply) in store(caller, supply) else 0;
fun metadata() = load(1);
fun total_supply() = load(3);
fun cap() = load(2);
fun balance_of(account) = load(account);
fun transfer(to, amount) = let from = caller in let balance = load(from) in if amount > balance then 0 else let ignored = store(from, balance - amount) in store(to, load(to) + amount);
fun mint(to, amount) = let owner = load(0) in let supply = load(3) in let ceiling = load(2) in if caller == owner then if ceiling < supply + amount then 0 else let ignored = store(3, supply + amount) in store(to, load(to) + amount) else 0;
fun burn(amount) = let account = caller in let balance = load(account) in let supply = load(3) in if amount > balance then 0 else let ignored = store(account, balance - amount) in store(3, supply - amount);
}

View file

@ -0,0 +1,40 @@
contract Twisto {
fun initialize(metadata, supply, cap) =
if load(0) == 0 then
let ignored = store(0, caller) in
let ignored = store(1, metadata) in
let ignored = store(2, cap) in
let ignored = store(3, supply) in
store(caller, supply)
else 0;
fun metadata() = load(1);
fun total_supply() = load(3);
fun cap() = load(2);
fun balance_of(account) = load(account);
fun transfer(to, amount) =
let from = caller in
let balance = load(from) in
if amount > balance then 0 else
let ignored = store(from, balance - amount) in
store(to, load(to) + amount);
fun mint(to, amount) =
let owner = load(0) in
let supply = load(3) in
let ceiling = load(2) in
if caller == owner then
if ceiling < supply + amount then 0 else
let ignored = store(3, supply + amount) in
store(to, load(to) + amount)
else 0;
fun burn(amount) =
let account = caller in
let balance = load(account) in
let supply = load(3) in
if amount > balance then 0 else
let ignored = store(account, balance - amount) in
store(3, supply - amount);
}