Rúnar

Language Feature Matrix

Runar supports eight source languages, each with different syntax and maturity levels. This page provides a comprehensive side-by-side comparison to help you choose the right language for your contract and understand the syntactic differences between them.

Language Status

Not all language frontends are at the same level of maturity. Choose accordingly based on your needs:

LanguageFile ExtensionFrontend PackageStatusNotes
TypeScript.runar.tsrunar-langStablePrimary language. Most complete tooling and documentation.
Go.runar.gorunar-goStableFull-featured, conformance-tested against TypeScript.
Rust.runar.rsrunar-rsStableFull-featured, conformance-tested against TypeScript.
Python.runar.pyrunar-pyStableFull-featured, conformance-tested against TypeScript.
Zig.runar.zigrunar-zigExperimentalConformance-tested. Compile-time safety with zero overhead.
Ruby.runar.rbrunar-rbExperimentalConformance-tested. Expressive DSL with familiar Ruby syntax.
Solidity.runar.solrunar-langExperimentalFamiliar syntax for Ethereum developers. Subset of Solidity.
Move.runar.moverunar-langExperimentalResource-oriented syntax. Subset of Move.

Stable means the frontend is conformance-tested, produces identical Bitcoin Script output to TypeScript for the same contract, and is suitable for production use. Experimental means the frontend compiles contracts correctly but cannot be tested natively in its respective language — tests must use the TypeScript test runner. The API could change in minor releases.

Contract Declaration

How each language defines a contract:

FeatureTypeScriptGoRustPythonZigRubySolidityMove
Statelessclass C extends SmartContracttype C struct { runar.SmartContract }#[runar::contract] struct Cclass C(SmartContract)pub const C = struct { pub const Contract = runar.SmartContract; ... }class C < Runar::SmartContractcontract C is SmartContractmodule c { struct C has key }
Statefulclass C extends StatefulSmartContracttype C struct { runar.StatefulSmartContract }#[runar::contract(stateful)] struct Cclass C(StatefulSmartContract)pub const C = struct { pub const Contract = runar.StatefulSmartContract; ... }class C < Runar::StatefulSmartContractcontract C is StatefulSmartContractstruct C has key + &mut self methods

Readonly Fields

How each language marks a field as immutable (baked into the locking script):

LanguageSyntaxExample
TypeScriptreadonly keywordreadonly owner: PubKey;
Gorunar:"readonly" struct tagOwner runar.PubKey \runar:“readonly”“
Rust#[readonly] attribute#[readonly] owner: PubKey,
PythonReadonly[T] type wrapperowner: Readonly[PubKey]
ZigFields without defaults (or runar.Readonly(T))pubKeyHash: runar.Addr,
Rubyprop :name, T, readonly: trueprop :owner, PubKey, readonly: true
Solidityimmutable keywordPubKey immutable owner;
MoveInferred from usageFields never modified in &mut self methods

Mutable State Fields

How each language declares mutable state (only valid in stateful contracts):

LanguageSyntaxExample
TypeScriptPlain field (no readonly)count: bigint;
GoField without struct tagCount int64
RustField without #[readonly]count: i64,
PythonPlain type annotationcount: int = 0
ZigField with default valuecount: i64 = 0,
Rubyprop without readonly: trueprop :count, Bigint
SolidityPlain variableint64 count;
MovePlain struct fieldcount: u64,

Public Methods (Entry Points)

How each language defines a spending condition:

LanguageSyntaxExample
TypeScriptpublic keywordpublic unlock(sig: Sig) { ... }
GoExported method (capitalized)func (c *C) Unlock(sig runar.Sig) { ... }
Rust#[public] attribute#[public] fn unlock(&self, sig: Sig) { ... }
Python@public decorator@public def unlock(self, sig: Sig): ...
Zigpub fnpub fn unlock(self: *const C, sig: runar.Sig) void { ... }
Rubyrunar_public markerrunar_public sig: Sig; def unlock(sig) ... end
Soliditypublic visibilityfunction unlock(Sig sig) public { ... }
Movepublic fun keywordpublic fun unlock(self: &C, sig: Sig) { ... }

Private Methods (Helpers)

How each language defines a private method that gets inlined:

LanguageSyntaxExample
TypeScriptprivate keywordprivate verify(pk: PubKey): boolean { ... }
GoUnexported method (lowercase)func (c *C) verify(pk runar.PubKey) bool { ... }
RustMethod without #[public]fn verify(&self, pk: PubKey) -> bool { ... }
PythonMethod without @publicdef _verify(self, pk: PubKey) -> bool: ...
Zigfn (no pub)fn verify(self: *const C, pk: runar.PubKey) bool { ... }
RubyMethod without runar_publicdef _verify(pk) ... end
Solidityprivate visibilityfunction verify(PubKey pk) private returns (bool) { ... }
Movefun without publicfun verify(self: &C, pk: PubKey): bool { ... }

Assertions

How each language asserts conditions:

LanguageFunctionExample
TypeScriptassert()assert(checkSig(sig, pk));
Gorunar.Assert()runar.Assert(runar.CheckSig(sig, pk))
Rustassert!() macroassert!(check_sig(&sig, &pk));
Pythonassert_()assert_(check_sig(sig, pk))
Zigrunar.assert()runar.assert(runar.checkSig(sig, pk));
Rubyassertassert check_sig(sig, pk)
Solidityrequire() or assert()require(checkSig(sig, pk));
Moveassert!() macroassert!(check_sig(&sig, &pk));

Constructors

How each language initializes contract state:

LanguageSyntax
TypeScriptconstructor(args) { super(args); this.field = arg; }
GoStruct literal at deploy time (no explicit constructor in contract)
RustStruct literal at deploy time (no explicit constructor in contract)
Pythondef __init__(self, args): super().__init__(args); self.field = arg
Zigpub fn init(args) C { return .{ .field = arg }; }
Rubydef initialize(args); super(args); @field = arg; end
Solidityconstructor(args) { field = arg; }
MoveModule-level init function or struct literal at deploy time

Imports

How each language imports Runar types and functions:

LanguageImport Statement
TypeScriptimport { SmartContract, assert, PubKey, Sig } from 'runar-lang';
Goimport "runar"
Rustuse runar::prelude::*;
Pythonfrom runar import SmartContract, assert_, PubKey, Sig
Zigconst runar = @import("runar");
Rubyrequire 'runar'
SolidityImplicit (all types and functions are globally available)
Moveuse runar::*;

Numeric Types

How each language represents integers:

LanguageTypeLiteral SyntaxExample
TypeScriptbigint42nconst x: bigint = 42n;
Goint6442x := int64(42)
Rusti6442let x: i64 = 42;
Pythonint42x: int = 42
Zigi6442const x: i64 = 42;
RubyBigint42prop :x, Bigint, default: 42
Solidityint6442int64 x = 42;
Moveu6442let x: u64 = 42;

Note that Move uses unsigned u64 while Go, Rust, and Solidity use signed int64/i64. TypeScript’s bigint and Python’s int are arbitrary-precision. All compile to the same Bitcoin Script numeric representation.

Fixed Arrays

How each language declares fixed-size arrays:

LanguageSyntaxExample
TypeScriptFixedArray<T, N>signers: FixedArray<PubKey, 3>
Go[N]TSigners [3]runar.PubKey
Rust[T; N]signers: [PubKey; 3]
PythonFixedArray[T, N]signers: FixedArray[PubKey, 3]
ZigFixedArray (resolves to unknown)signers: runar.FixedArray(runar.PubKey, 3)
RubyFixedArray[T, N]prop :signers, FixedArray[PubKey, 3]
SolidityT[N]PubKey[3] signers;
Movevector<T> (fixed)signers: vector<PubKey>

For Loops

How each language writes a compile-time-bounded loop:

LanguageSyntax
TypeScriptfor (let i = 0n; i < 10n; i++) { ... }
Gofor i := int64(0); i < 10; i++ { ... }
Rustfor i in 0..10 { ... }
Pythonfor i in range(10): ...
Zigfor (0..10) |i| { ... }
Rubyfor i in 0...10 ... end
Solidityfor (int64 i = 0; i < 10; i++) { ... }
Movewhile (i < 10) { ...; i = i + 1; }

All loops are unrolled at compile time. The bound (10 in these examples) must be a compile-time constant in every language.

Conditionals

How each language writes conditional logic:

LanguageIf/ElseTernary / Expression
TypeScriptif (cond) { ... } else { ... }cond ? a : b
Goif cond { ... } else { ... }None (use if/else)
Rustif cond { ... } else { ... }if cond { a } else { b } (expression)
Pythonif cond: ... else: ...a if cond else b
Zigif (cond) { ... } else { ... }None (use if/else)
Rubyif cond ... else ... endcond ? a : b
Solidityif (cond) { ... } else { ... }cond ? a : b
Moveif (cond) { ... } else { ... }if (cond) { a } else { b } (expression)

Signature Verification

How each language verifies a signature:

LanguageSingle SigMulti-Sig
TypeScriptcheckSig(sig, pk)checkMultiSig(sigs, pks)
Gorunar.CheckSig(sig, pk)runar.CheckMultiSig(sigs, pks)
Rustcheck_sig(&sig, &pk)check_multi_sig(&sigs, &pks)
Pythoncheck_sig(sig, pk)check_multi_sig(sigs, pks)
Zigrunar.checkSig(sig, pk)runar.checkMultiSig(sigs, pks)
Rubycheck_sig(sig, pk)check_multi_sig(sigs, pks)
SoliditycheckSig(sig, pk)checkMultiSig(sigs, pks)
Movecheck_sig(&sig, &pk)check_multi_sig(&sigs, &pks)

Hashing

How each language calls hash functions:

LanguageSHA-256Hash-160Double SHA-256
TypeScriptsha256(data)hash160(data)hash256(data)
Gorunar.Sha256(data)runar.Hash160(data)runar.Hash256(data)
Rustsha256(&data)hash160(&data)hash256(&data)
Pythonsha256(data)hash160(data)hash256(data)
Zigrunar.sha256(data)runar.hash160(data)runar.hash256(data)
Rubysha256(data)hash160(data)hash256(data)
Soliditysha256(data)hash160(data)hash256(data)
Movesha256(&data)hash160(&data)hash256(&data)

Complete Minimal Example in Each Language

For reference, here is the same simple P2PKH contract written in all eight languages.

TypeScript

import { SmartContract, assert, PubKey, Sig, Addr, hash160, checkSig } from 'runar-lang';

class P2PKH extends SmartContract {
  readonly pubKeyHash: Addr;
  constructor(pubKeyHash: Addr) {
    super(pubKeyHash);
    this.pubKeyHash = pubKeyHash;
  }
  public unlock(sig: Sig, pubKey: PubKey) {
    assert(hash160(pubKey) === this.pubKeyHash);
    assert(checkSig(sig, pubKey));
  }
}

Go

package p2pkh
import "runar"
type P2PKH struct {
    runar.SmartContract
    PubKeyHash runar.Addr `runar:"readonly"`
}
func (p *P2PKH) Unlock(sig runar.Sig, pubKey runar.PubKey) {
    runar.Assert(runar.Hash160(pubKey) == p.PubKeyHash)
    runar.Assert(runar.CheckSig(sig, pubKey))
}

Rust

use runar::prelude::*;
#[runar::contract]
struct P2PKH {
    #[readonly] pub_key_hash: Addr,
}
#[runar::methods]
impl P2PKH {
    #[public]
    fn unlock(&self, sig: Sig, pub_key: PubKey) {
        assert!(hash160(&pub_key) == self.pub_key_hash);
        assert!(check_sig(&sig, &pub_key));
    }
}

Python

from runar import SmartContract, PubKey, Sig, Addr, Readonly, public, assert_, hash160, check_sig
class P2PKH(SmartContract):
    pub_key_hash: Readonly[Addr]
    def __init__(self, pub_key_hash: Addr):
        super().__init__(pub_key_hash)
        self.pub_key_hash = pub_key_hash
    @public
    def unlock(self, sig: Sig, pub_key: PubKey):
        assert_(hash160(pub_key) == self.pub_key_hash)
        assert_(check_sig(sig, pub_key))

Solidity

pragma runar ^0.1.0;
contract P2PKH is SmartContract {
    Addr immutable pubKeyHash;
    constructor(Addr _pubKeyHash) {
        pubKeyHash = _pubKeyHash;
    }
    function unlock(Sig sig, PubKey pubKey) public {
        require(hash160(pubKey) == pubKeyHash);
        require(checkSig(sig, pubKey));
    }
}

Move

module p2pkh {
    use runar::*;
    struct P2PKH has key {
        pub_key_hash: Addr,
    }
    public fun unlock(self: &P2PKH, sig: Sig, pub_key: PubKey) {
        assert!(hash160(&pub_key) == self.pub_key_hash);
        assert!(check_sig(&sig, &pub_key));
    }
}

Zig

const runar = @import("runar");
pub const P2PKH = struct {
    pub const Contract = runar.SmartContract;
    pubKeyHash: runar.Addr,
    pub fn init(pubKeyHash: runar.Addr) P2PKH {
        return .{ .pubKeyHash = pubKeyHash };
    }
    pub fn unlock(self: *const P2PKH, sig: runar.Sig, pubKey: runar.PubKey) void {
        runar.assert(runar.bytesEq(runar.hash160(pubKey), self.pubKeyHash));
        runar.assert(runar.checkSig(sig, pubKey));
    }
};

Ruby

require 'runar'
class P2PKH < Runar::SmartContract
  prop :pub_key_hash, Addr
  def initialize(pub_key_hash)
    super(pub_key_hash)
    @pub_key_hash = pub_key_hash
  end
  runar_public sig: Sig, pub_key: PubKey
  def unlock(sig, pub_key)
    assert hash160(pub_key) == @pub_key_hash
    assert check_sig(sig, pub_key)
  end
end

Choosing a Language

Choose TypeScript if:

  • You want the most mature tooling, documentation, and community support
  • You are new to Runar and want the smoothest learning path
  • Your team primarily works in JavaScript/TypeScript

Choose Go if:

  • You prefer Go’s simplicity and explicit error handling style
  • Your backend services are written in Go
  • You want a statically compiled frontend with fast compilation

Choose Rust if:

  • You want Rust’s ownership model to catch errors at compile time
  • You are already comfortable with Rust’s syntax and borrow checker
  • You value the alignment between Rust’s ownership and UTXO resource semantics

Choose Python if:

  • You want the most concise syntax with minimal boilerplate
  • Your team primarily works in Python
  • You are prototyping contracts quickly

Choose Solidity if:

  • You are coming from Ethereum and want a familiar syntax
  • You are porting existing Solidity contracts to BSV
  • You understand the EVM-to-UTXO conceptual differences

Choose Move if:

  • You are coming from Sui or Aptos and want a familiar syntax
  • You value Move’s resource-oriented safety guarantees
  • You want the closest conceptual alignment between language and UTXO model

Choose Zig if:

  • You want compile-time safety with zero-overhead abstractions
  • Your systems programming uses Zig
  • You value explicit control over memory and resources

Choose Ruby if:

  • You want the most expressive DSL with minimal ceremony
  • Your team primarily works in Ruby
  • You are prototyping contracts quickly and want readable code

Regardless of which language you choose, the compiled Bitcoin Script output is identical. A P2PKH contract compiled from TypeScript produces the exact same script as the same contract compiled from Go, Rust, Python, Zig, Ruby, Solidity, or Move.

Next Steps