Skip to content

Repository files navigation

Aurora mascot

aurora

A small language for the EVM whose programs answer the same thing on a chain and off it.

Go Reference Last commit Pipeline status Coverage status

Language · Examples · Playground · Roadmap · Changelog

⚠ Alpha. The language moves, and what moved is in the changelog.

What Aurora is

use std/evm/storage as s;

ident deposit = defer { s.set(1, s.get(1) + feed(0)); };
ident balance = defer { s.get(1); };

That is a contract. What follows a name on the command line is the calldata a scope reads with feed, and a chain has two ways in:

aurora tx deposit 10     # a change: feed(0) is the 10, sent, and what it kept stays
aurora call balance      # a question: answered for nothing, and 10 comes back

An untyped, expression-only language. Every value is a tape: a fixed run of bytes, eight wide by default. Numbers, booleans, text and shapes are all the same kind of thing — there is nothing else.

It runs on its own evaluator, and it assembles to EVM bytecode. The point of having both is one sentence:

The same program answers the same thing on a chain and off it.

That is what Aurora is for — an on-chain call you can simulate off-chain, from the same source. It is checked rather than claimed: a differential harness compiles each program, deploys it to an EVM in memory, calls it, and compares the answer against the evaluator — and every time the two have disagreed, the disagreement became a test.

Why "Aurora"

Aurora is the author's daughter, and the language is a tribute to her.

It is also why a new project says Abidu abide — it is what she was saying at one year old, and it seemed like the right first thing for the language to say.

Get started

Install

Platform Command
macOS brew tap guiferpa/tap && brew install guiferpa/tap/aurora
Linux Download from Releases → sudo dpkg -i aurora_*_linux_*.deb
Other Releases — unpack the archive for your OS and arch
From source go install -v github.com/guiferpa/aurora/cmd/aurora@HEAD

Full options, including the macOS Gatekeeper workaround: docs/install.md.

Nothing to install, if you would rather not: the playground runs the language in the browser.

First lines

No project and no files:

aurora repl
>> ident a = 1;
= [0 0 0 0 0 0 0 0]
>> a + 1;
= [0 0 0 0 0 0 0 2]
>> printd a + 1;
2
= [0 0 0 0 0 0 0 2]

A binding answers the neutral tape, because everything is an expression and a binding has nothing of its own to give. A value answers as its bytes; printd reads those bytes as a number, printc reads them as text, and printb shows them as they are.

Ctrl+D exits, Ctrl+C clears the line. ↑/↓ walk the history, which is shared by every project in ~/.aurora/history.

The language itself, in order of how much you want: the commented examples/, which the test suite runs and compares against the output each one declares · the reference, docs/language-design.md · the grammar, docs/grammar.md.

A project

mkdir my-project && cd my-project
aurora init
✨ dawn has broken on your project.
   aurora.toml
   src/main.ar
   src/main.test.ar

Run it with 'aurora run', test it with 'aurora test'.

src/main.ar is a program with one scope in it, and src/main.test.ar names that file and checks what the scope says. Both are commented, and reading them is the shortest tour of the language there is.

$ aurora run
Abidu abide

$ aurora test
src/main.test.ar
  ok    greet says its piece

1 passed, 0 failed in 1 file

Profiles

aurora.toml names profiles, so paths are written once:

[project]
name = "my-project"
version = "0.1.0"
tape_size = 16

[profiles.main]
source = "src/main.ar"
binary = "bin/main"

run and build take a profile name, a path ending in .ar, or nothing at all — nothing at all is the main profile, and a path never needs a manifest:

aurora run              # the main profile
aurora run dev          # another profile
aurora run one.ar       # that file, no manifest involved

A second profile is how one project reaches two chains, or two tape widths. deploy, call and tx always need one, since they read rpc and privkey from it — and a key belongs in the environment rather than in the file:

[profiles.sepolia]
source  = "src/main.ar"
binary  = "bin/main"
rpc     = "${{ AURORA_RPC }}"
privkey = "${{ AURORA_PRIVKEY }}"

Read from the project's .env first, then from the environment the command runs in. A name nothing sets is refused rather than read as empty.

Manifest reference: docs/manifest.md · editor support: docs/lsp.md.

On a chain

aurora build            # src/main.ar → bin/main
aurora deploy -p sepolia

build says what it could not carry, at the line that used it:

src/main.ar:14:1: warning: printc is ignored in compiled code, by decision: a chain has
nowhere to put a log, and the value it was given carries on

That is the shape of the whole promise: a binary that does less than the source said is announced rather than silent. Everything a program can write reaches the bytecode today — the prints and assert are the two exceptions, and both are decisions rather than gaps. docs/roadmap.md lists what a contract still cannot do.

Asking and changing

A chain has two ways in, and Aurora has one command for each:

aurora call balance     # a question: answered against the state as it is, and costs nothing
aurora tx deposit       # a change: sent, paid for, mined, and what it changed stays

Nothing guesses between them. What the compiler knows is used to refuse the wrong one, because a scope that keeps something, asked as a question, answers and leaves the chain exactly as it was:

$ aurora call deposit
deposit changes what the chain keeps, and a call is a question: it would answer, cost
nothing, and leave the chain exactly as it was — send it with 'aurora tx deposit' instead

A question answers the bytes that came back, on their own, so it pipes:

aurora call balance | xxd
aurora call balance --hex     # 0x000000000000002a, for reading and for an explorer

Passing arguments

Both take arguments after the name, and both encode them the same way — a scope has no signature, so feed(0) reads the first, feed(1) the second, and a position nothing was applied to reads as zeros:

aurora call sum 30 20
aurora tx deposit 5

Add --pretend to either and it shows what would be sent without sending it, which is the honest way to see the calldata:

$ aurora call sum 30 20 --pretend
Contract:   0x7e769d0a39ae98fb7b363c41466951e481323a7d (20 bytes)
Function:   0x0fdbd160736e9b9b51ea9a79a8ed86f427a62e0e377d60335d2ec895c27025bb (32 bytes)
Arguments:  0x00…001e00…0014 (64 bytes)

The selector is the whole hash of the name rather than the four bytes an ABI uses, and each value takes a word after it. Neither of those is a choice a program makes — they are what a scope with no signature looks like from outside.

Testing

aurora test

A test file is a file like any other. It imports what it checks and says what should hold:

#- src/calc.ar
ident double = defer { feed(0) * 2; };
#- src/calc.test.ar
use calc as c;

assert(c.double(21) equals 42, "double doubles");

There is no node to start, no second language, and no fixture: it is the same language, run by the same evaluator, in the time a process takes to open. That is where it differs from the usual way of testing for the EVM, where a test is written in another language and run against a simulated chain you have to keep.

And it is worth something because of the sentence at the top. A test that passes off a chain is evidence about a chain, since the compiler's own suite compiles each program, deploys it to an EVM in memory, calls it, and compares the answer against the evaluator. Where the two disagree, that is a bug in the compiler rather than a surprise in your contract.

Tests and assert: docs/testing.md.

Contributing

Build, tests, coverage, lint and CI: docs/development.md. How the compiler is put together: docs/contributing/architecture.md. What the IR is and how to run it, for anyone writing a second backend: docs/contributing/ir.md — and why it is blocks rather than a list: docs/contributing/why-blocks.md.

go run ./cmd/aurora repl   # fast loop, no build needed
make build                 # the binaries, into target/bin
make check                 # build, wasm, test, lint — what CI runs
make install-std           # the standard library, into $AURORA_ROOT/lib

Releases are built with GoReleaser on tag push: docs/releasing.md.

About

🌌 An unconventional programming language that compiles to EVM bytecode.

Topics

Resources

Stars

14 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages