Quorlin
Kortana's native contract language: looks like Java, reads like English, publishes an Ethereum ABI.
The shape of a contract
One contract per file, in a `.ql` source file. A contract holds fields, events, an optional constructor and functions. Every function begins with `reads` or `writes`, so you can tell which functions can move money by reading down the left edge of the file.
contract Counter {
number count;
address owner;
event Incremented(address indexed by, number newValue);
constructor {
owner = caller;
count = 0;
}
reads number value() {
return count;
}
writes number increment() {
count = count + 1;
emit Incremented(caller, count);
return count;
}
}Imports
A file can import another. `import "./lib/Token.ql";` makes that file's declarations visible here — its interfaces, its records, and the interface its own contract implies. Every Quorlin function is reachable by selector, so a contract's public functions are its shape, and the compiler reads that off the imported file rather than asking you to retype it as an `interface`.
An import contributes declarations, never code and never storage. The Forwarder below has exactly one state variable; Token's fields are not inherited, because nothing is inherited. There is still one contract per file and one contract per deployed module.
Everything resolves at compile time and flattens into one self-contained module. An import emits byte-for-byte the same bytecode as writing the interface out by hand — `quorlinc --hash` on both returns the same module hash. So nothing is linked at deploy time, nothing is fetched at run time, and no dependency can be swapped underneath a contract that is already deployed.
Paths are relative to the importing file and may not leave the entry file's directory. Cycles are refused and the error names the path. A file that reaches the same unit twice loads it once; two imports contributing the same name is a clash, and you are told which.
Imports are an addition, not a migration: a file that uses none compiles exactly as it did before, byte for byte. All twelve KRS standards are plain files and were not changed.
// lib/Token.ql — an ordinary contract, written once.
contract Token {
map<address, number> balances;
reads number balanceOf(address owner) {
return balances[owner];
}
writes truth transfer(address recipient, number amount) {
require balances[caller] >= amount, "balance exceeded";
balances[caller] = balances[caller] - amount;
balances[recipient] = balances[recipient] + amount;
return yes;
}
}
// Forwarder.ql — another project, calling it.
import "./lib/Token.ql";
contract Forwarder {
address token;
writes truth forward(address recipient, number amount) {
// No hand-written interface anywhere: the shape came from the import.
return Token(token).transfer(recipient, amount);
}
reads number held(address owner) {
return Token(token).balanceOf(owner);
}
}Types
Four scalars, and nothing converts between them by itself.
| Quorlin | Holds | ABI type |
|---|---|---|
| number | A whole number, 0 to 2²⁵⁶−1 | uint256 |
| truth | yes or no | bool |
| address | An account | address |
| text | Up to 128 bytes | string |
| list<T> | Up to 64 scalars, never storage | T[] |
| bytes | Up to 256 opaque bytes | bytes |
| bytes4 | A four-byte tag | bytes4 |
reads and writes
`reads` may look at state and may not change it. That is proved when the contract is compiled, and it compiles to a static call which the VM refuses to let write. A `reads` function calling a `writes` one is a compile error — `reads` is a promise about what calling a function does, not about its own statements.
`writes` may change state, emit events and call other `writes` functions.
What surprises people coming from Solidity
There is no `msg.sender` — use `caller`. There is no hexadecimal literal and no address literal at all: an address comes from a parameter, a field, `caller`, or `nobody`. Booleans are `yes` and `no`. Logic is `and`, `or`, `not` rather than `&&`, `||`, `!`.
There is no `else if` — nest instead. Braces are mandatory on both arms of an `if`, so the dangling-else ambiguity does not exist. `require` is a keyword, not a function, so it takes no wrapping parentheses and cannot be shadowed.
Arithmetic is checked by default: `+ - * / %` stop the call on overflow, underflow or divide by zero. Wrapping is available as `+% -% *%` and has to be written deliberately.
The constructor takes no arguments. To configure at deploy time, use a one-shot initialiser guarded by a flag.
Execution context
These are values, not calls — write `caller`, not `caller()`.
| Built-in | Type | Meaning |
|---|---|---|
| caller | address | Who called this function — may be another contract |
| sender | address | The account that signed the transaction |
| sentAmount | number | DNR sent with this call |
| thisContract | address | This contract's own address |
| blockNumber | number | Current block height |
| blockTime | number | Current block's timestamp |
| clock | number | The dPoH sequence — no EVM equivalent |
| nobody | address | The zero address |
| hashPair(a, b) | number | keccak256(a ‖ b) over two 256-bit words |
| hasCode(a) | truth | Whether address a has contract code |
Access control uses caller, not sender
`caller` is whoever made this call, which may be another contract. `sender` is the account that signed the transaction. A check against `sender` can be passed by tricking a user into calling through a contract you control, so access control should almost always use `caller`.
writes setCap(number newCap) {
require caller == owner, "only the owner";
cap = newCap;
}How Studio compiles Quorlin
Quorlin is compiled by `quorlinc`, the native compiler in the Kortana node repository. Studio invokes that binary directly through its own compiler API, so the diagnostics, the ABI and the KVM bytecode you see are what the compiler produced — not a reimplementation.
Studio finds the binary by looking at `QUORLIN_COMPILER_PATH`, then at `KORTANA_BLOCKCHAIN_PATH` and the conventional build outputs of the node repository. If none of those resolve, Quorlin building reports itself unavailable with the reason, and editing, the outline, hover documentation, go-to-definition, completion and structural diagnostics all keep working.
Each compile runs in its own temporary directory with a time limit and a trimmed environment, and the directory is removed afterwards.
Deploying a Quorlin contract
A Quorlin deployment is an ordinary contract-creation transaction carrying the KVM bytecode, so it goes through `eth_sendRawTransaction` exactly as Solidity does. There is no separate KVM transaction surface, and no `kvm_*` RPC family is required.
The constructor takes no arguments, so deployment carries the bytecode alone. Configure at deploy time with a one-shot initialiser guarded by a flag.