# Elci documentation Elci 0.1.0-dev (Julc 0.1.0-pre20-ebaf028-SNAPSHOT). Every page of https://elci-lang.dev, in reading order. --- # Why Elci > What Elci is, who it is for, how it turns Elm-syntax source into Plutus V3 scripts, and what you get from it today. Source: https://elci-lang.dev/start/introduction/ Elci (pronounced "el-see") is a small functional language for writing Cardano smart contracts. You write Elm-syntax `.elm` files, and Elci compiles them to Plutus V3 scripts that run on the Cardano ledger. This page explains what Elci is, who it is for, how the compiler works, and where the project stands today. ## What Elci is A Cardano smart contract is a **validator**: a function that the ledger runs when a transaction tries to spend locked funds, mint tokens, withdraw rewards or take part in governance. It receives the data involved and the whole transaction, and it either approves or rejects. Elci lets you write those functions in the syntax of [Elm](https://elm-lang.org), a language known for readable code and helpful compile errors: ```elm title="src/Treasury.elm" module Treasury exposing (Action(..), Payout, spend) import Julc.Ledger exposing (PubKeyHash, ScriptContext) type alias Payout = { beneficiary : PubKeyHash , amount : Int } type Action = Release | Cancel spend : PubKeyHash -> Payout -> Action -> ScriptContext -> Bool spend admin payout action ctx = case action of Release -> payout.amount > 0 && signedBy payout.beneficiary ctx Cancel -> signedBy admin ctx signedBy : PubKeyHash -> ScriptContext -> Bool signedBy key ctx = List.any (\signer -> signer == key) ctx.txInfo.signatories ``` The `Payout` record is the datum stored with the locked funds, `Action` is the redeemer the spender sends, and `admin` is a deployment parameter. You declare how the function is deployed in `elci.toml`, not in the source: ```toml title="elci.toml" [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" [validators.treasury] boundary = "julc-strict-v1" parameters = ["admin"] spend = "Treasury.spend" ``` A test runs the compiled script against a real transaction context: ```elm title="tests/TreasuryTest.elm" module TreasuryTest exposing (beneficiaryReleases, strangerCannotRelease) import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Scenario exposing (Scenario) import Treasury exposing (Action(..)) admin : PubKeyHash admin = Scenario.key "admin" beneficiary : PubKeyHash beneficiary = Scenario.key "beneficiary" release : Scenario release = Scenario.spend "treasury" (Scenario.outRef "treasury" 0) (Data.encode { beneficiary = beneficiary, amount = 50 }) |> Scenario.parameters [ Data.encode admin ] |> Scenario.redeemer (Data.encode Release) beneficiaryReleases : Scenario beneficiaryReleases = release |> Scenario.signedBy beneficiary strangerCannotRelease : Scenario strangerCannotRelease = release |> Scenario.signedBy (Scenario.key "stranger") |> Scenario.expectFailure ``` ```text $ elci test . PASS TreasuryTest.beneficiaryReleases (cpu 11698810, mem 46679) PASS TreasuryTest.strangerCannotRelease (cpu 12077243, mem 48611) 2 passed, 0 failed $ elci build . validator treasury (spend): checked; 458 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` ## Who it is for - **Developers who like typed functional programming.** If you know Elm, Haskell, F# or OCaml, most of the syntax will be familiar. If you don't, it is a small language, and the Learn pages cover all of it. - **Newcomers to Cardano.** The compiler rejects whole classes of mistakes before anything reaches the chain, and the test runner runs your script in a realistic ledger context. - **Java and Cardano Client Lib teams.** Elci is built on Julc, the Java toolchain for Cardano smart contracts. It writes Julc's artifact format, can call Julc's on-chain Java libraries, and its scripts can be tested from JUnit. ## How it works Elci is a compiler frontend. It owns the language (parsing, modules, types) and hands a typed program to Julc's backend, which produces the script: ```text .elm source ──► parse and resolve modules ──► type inference and checks │ ┌──────────────────────────────────────────────┘ ▼ specialization ──► typed PIR ──► Julc backend ──► Plutus V3 UPLC (wrapper, strict (validator.plutus.json, decoding, optimizer) abi.json, .flat, .plutus) ``` 1. **Source.** Your modules under `src/`, written in Elm 0.19 syntax. 2. **Type inference.** Hindley–Milner style inference checks every expression. It also checks that every `case` covers all possibilities and that `==` is used only on types that support it. 3. **Typed PIR.** Generic functions are specialized for each type they are used at, and the program is lowered to PIR, the typed intermediate language that Julc also uses for its Java validators. 4. **Julc backend.** Julc wraps each validator so that it checks the purpose (spend, mint and so on) and strictly decodes the datum and redeemer before calling your handler. Then it optimizes and serializes the script. 5. **Plutus V3 UPLC.** The result is a Plutus V3 script, plus an `abi.json` that records exactly how your types are encoded as on-chain data. ## What you get - **Elm syntax with type inference.** Records, custom types, pattern matching, pipelines and lambdas. Types are inferred inside functions. Every top-level declaration needs a signature, so its types are written where the reader can see them. - **Exhaustive pattern matching.** A `case` that misses a constructor is a compile error, and so is an unreachable branch. Adding a redeemer action means the compiler shows you every place that must handle it. - **Strict data boundaries.** Datums and redeemers are decoded strictly before your code runs. Wrong constructor tags, wrong field counts and wrong kinds of data fail the script instead of reaching your logic. `elci apply` checks deployment parameters against their types before applying them. - **Named, multi-purpose validators with parameters.** Each `[validators.]` table is one script with one hash. It can combine `spend`, `mint`, `withdraw`, `certify`, `vote` and `propose` handlers, take deployment parameters, and have them applied with `elci apply`. - **Built-in testing.** `elci test` runs unit tests, transaction scenarios against the same script that `elci build` writes, and property-based tests with shrinking. Every test reports its CPU and memory budget. - **Julc libraries and Java interop.** Map a Julc on-chain Java library (math, cryptography, byte strings and more) in `elci.toml` and import it like an Elm module. Elci compiles the library's Java source to on-chain code. It doesn't run that Java on your machine. - **Typed collections, values and bytes.** `Dict k v` is Elm's `Dict`, stored as a sorted Plutus map, `Map k v` holds the ledger's own maps, `Array a` gives constant-time indexed reads, `Value` handles multi-asset values with exact `==` on the protocol's value builtins, and `Bytes` exposes the Plutus byte-string builtins, including hashes and bitwise operations. - **Transaction helpers.** `Tx` answers the questions validators ask about a transaction: who signed, what it spends and pays, which outputs stay at the script, what it mints and which datum an output carries. `Interval` checks deadlines against the validity range. - **A native CLI.** Build `elci` as a GraalVM native executable that starts fast and needs no Java at runtime, or run the JVM version. ## Current status :::caution[Development preview] Elci is a development preview, not a production-ready contract toolchain, and its own test suite is not a security audit. Test your contracts thoroughly, and don't lock real value with them yet. ::: Be aware of these limits before you start: - **You build it from source.** Elci depends on a development build of Julc that you publish to your local Maven repository. [Installation](https://elci-lang.dev/start/installation/) walks you through it. - **It is not browser Elm.** There is no `Html`, no ports, no commands or subscriptions, and no `Float`. A contract is a pure function that returns `Bool`. - **No Elm packages.** Elci has Elm's core modules (`Basics`, `List`, `Maybe`, `Result`, `Tuple`, `Dict` and part of `String`), its own `Map`, `Array`, `Value`, `Interval`, `Tx`, `Bytes` and `Julc.Data` modules, and Julc's Java libraries. `elm.json` and the Elm package registry are not used. - **Some tooling is missing.** There is no language server, and no CIP-57 blueprint generation yet. The build writes Julc's `validator.plutus.json` format and Elci's own `abi.json`. [Coming from Elm](https://elci-lang.dev/learn/coming-from-elm/) lists every difference in detail. ## Next steps - [Installation](https://elci-lang.dev/start/installation/): Build the elci CLI and put it on your PATH. - [Your first validator](https://elci-lang.dev/start/first-validator/): Create, test, build and change a validator in a few minutes. - [Learn the language](https://elci-lang.dev/learn/basics/): Values, functions and types, with examples you can run. - [Coming from Elm](https://elci-lang.dev/learn/coming-from-elm/): What stays the same and what changes on-chain. --- # Installation > Build the elci compiler from source with Java 25, optionally as a native executable, and put it on your PATH. Source: https://elci-lang.dev/start/installation/ On this page you build the `elci` command-line compiler from source and put it on your `PATH`. You need Java 25. GraalVM is only needed if you also want the native executable. :::note[Development preview] Elci depends on a development snapshot of [Julc](https://github.com/bloxbean/julc) that is not on Maven Central. Gradle downloads that exact version from the BloxBean Maven repository (`https://repo.bloxbean.org/maven/snapshots`), so you don't need to build Julc yourself. ::: ## Prerequisites | Tool | Needed for | Notes | | --- | --- | --- | | JDK 25 | Building Elci and running the JVM `elci` | Set `JAVA_HOME` to it | | Git | Cloning Elci | | | GraalVM 25 with `native-image` | Only the native `elci` executable | Also needs your platform's C toolchain | | Node.js and npm | Only formatting with elm-format | Optional | You don't need to install Gradle: the project includes the Gradle wrapper (`./gradlew`). The commands below are for macOS and Linux shells. ## Build from source 1. **Check your Java version.** Gradle builds Elci with Java 25: ```sh java -version ``` The first line of the output should report version 25. If it doesn't, install a JDK 25 and point `JAVA_HOME` at it. 2. **Clone Elci.** ```sh git clone https://github.com/bloxbean/elci-lang.git ``` 3. **Check the pinned Julc version.** Elci's `gradle.properties` names the Julc snapshot it was built and tested with, and the Julc commit it was built from: ```sh grep '^julc' elci-lang/gradle.properties ``` ```text julcVersion=0.1.0-pre20-ebaf028-SNAPSHOT julcRevision=ebaf028c982479906a1f9449b9fef68fa7836301 ``` The first build downloads that version from the BloxBean Maven repository. 4. **Build the Elci CLI.** ```sh cd elci-lang ./gradlew installDist ``` This creates `build/install/elci/bin/elci`, a launcher script that runs the compiler on your Java 25. To run Elci's own test suite first, use `./gradlew test installDist`. 5. **Optional: build the native executable.** Point `GRAALVM_HOME` at a GraalVM 25 installation that contains `bin/native-image`, then run: ```sh export GRAALVM_HOME=/path/to/graalvm ./gradlew nativeCompile ``` This creates `build/native/elci`. It starts fast and doesn't need Java at runtime, but it only runs on the platform you built it on. 6. **Put `elci` on your `PATH`.** From the `elci-lang` directory, add one of the two builds: ```sh # The JVM build export PATH="$PWD/build/install/elci/bin:$PATH" # Or the native build export PATH="$PWD/build/native:$PATH" ``` Add the same line, with the absolute path, to your shell profile (for example `~/.zshrc` or `~/.bashrc`) to keep it in new terminals. 7. **Check the installation.** ```sh elci --version ``` ```text elci 0.1.0-dev / julc 0.1.0-pre20-ebaf028-SNAPSHOT ``` The first part is the Elci version and the second is the Julc version the compiler is pinned to. :::tip[Prebuilt binaries] Elci's release workflow builds native archives for Linux (x64 and arm64) and macOS (Apple Silicon) from version tags. When a release is published for your platform, you can download the archive, check it against `SHA256SUMS`, and put the `elci` executable on your `PATH` without installing Java or Julc. ::: ## The Julc version in your projects Every project's `elci.toml` records the Julc version it expects: ```toml julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" ``` It must equal the version that `elci --version` prints. `elci init` writes the right value for you. If they differ, for example after you upgrade Elci, every command stops with: ```text elci: ELCI4001: Julc version does not match this compiler; expected julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" ``` Update `julc-version` in `elci.toml`, then rebuild and re-run your tests. A new Julc version can change the generated scripts and their hashes. ## Editor setup - **Syntax highlighting:** Elci source files use the `.elm` extension, so any Elm syntax-highlighting extension works. - **Formatting:** Elci code follows [elm-format](https://github.com/avh4/elm-format) 0.8.8 style. To check your files without installing anything globally: ```sh npm exec --yes --package elm-format@0.8.8 -- elm-format src tests --validate ``` - **Diagnostics:** the Elm language server doesn't know Elci's modules (`Julc.Ledger`, `Map`, `Array`, `Bytes`, your Java library imports), so its errors aren't reliable for Elci code. Use `elci check`, which reports every error as `file:line:column: ELCIxxxx: message`, followed by the source line and any hints. With `--report=json` it prints each error as one JSON object on its own line instead, for editor integrations. `elci explain ` explains a code. :::caution elm-format silently changes integer literals larger than about 9.2 × 10¹⁸, because it stores them as 64-bit numbers. See [Coming from Elm](https://elci-lang.dev/learn/coming-from-elm/). ::: ## Troubleshooting - **Could not resolve `org.julclang:...` while building Elci.** Gradle could not download the pinned Julc snapshot. Check that `https://repo.bloxbean.org/maven/snapshots` is reachable from your machine (a proxy or firewall can block it), and that `julcVersion` in `gradle.properties` is unchanged. - **Gradle reports a Java toolchain problem.** Elci and Julc need Java 25. Check `java -version` and `JAVA_HOME`. - **`GRAALVM_HOME must point to a GraalVM installation containing native-image`.** `nativeCompile` needs `GRAALVM_HOME` set to a GraalVM installation that contains `bin/native-image`. - **`elci: command not found`.** The build directory is not on your `PATH`. Run the binary by its full path to confirm the build worked, then fix your `PATH`. Next, create your first project: [Your first validator](https://elci-lang.dev/start/first-validator/). --- # Your first validator > Create a validator with elci init, read every generated file, check, test and build it, then change it to require a second signature. Source: https://elci-lang.dev/start/first-validator/ In this tutorial you create a small validator, read every file that `elci init` generates, and check, test and build it. Then you change it to require a second signature and update its tests. You need `elci` on your `PATH` (see [Installation](https://elci-lang.dev/start/installation/)). ## What a validator does On Cardano, funds can be locked at a script address. Whenever a transaction tries to spend them, the ledger runs the script, called a **validator**, and the spend only happens if the validator approves. A spending validator receives four things: - **Parameters** that you fix when you deploy the script, such as the owner's key. - The **datum**: data stored with the locked output when it was created. - The **redeemer**: data that the spending transaction supplies. - The **script context**: the whole transaction being validated, including who signed it. It returns `True` to approve the transaction. Returning `False`, or failing during evaluation, rejects it. Validators can also control minting, reward withdrawals and governance actions; this tutorial sticks to spending. ## Create the project ```sh elci init my-contract cd my-contract ``` ```text Created Elci project at .../my-contract with initial spend handler in validator main. Run: elci check .../my-contract ``` `elci init` creates this layout: - my-contract/ - elci.toml project configuration - README.md - .gitignore ignores build/ - src/ - Main.elm the validator - tests/ - MainTest.elm two transaction scenarios ## The configuration ```toml title="elci.toml" [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" # One deployed script. Add handler keys (spend, mint, withdraw, certify, vote, # propose) to let this script serve more purposes; add more [validators.] # tables for independent scripts. [validators.main] boundary = "julc-strict-v1" parameters = ["owner"] spend = "Main.spend" ``` - `[project]` says where your modules live, which on-chain target to compile for (Plutus V3), and which Julc version the compiler must match. - `[validators.main]` declares one script called `main`. Its `spend` handler is the function `spend` in module `Main`, and it takes one deployment parameter called `owner`. - `boundary = "julc-strict-v1"` selects strict decoding of the data the script receives. It is currently the only boundary policy. The source code doesn't say how it is deployed. The configuration does, so the same function could be reused in another validator. ## The validator ```elm title="src/Main.elm" module Main exposing (spend) import Julc.Ledger exposing (PubKeyHash, ScriptContext) {-| Accept only transactions signed by the owner, a deployment parameter. -} spend : PubKeyHash -> () -> () -> ScriptContext -> Bool spend owner _ _ ctx = List.any (\signer -> signer == owner) ctx.txInfo.signatories ``` Line by line: - `module Main exposing (spend)` names the module (it must match the file path `src/Main.elm`) and makes `spend` visible to the configuration and to tests. - `import Julc.Ledger exposing (PubKeyHash, ScriptContext)` brings in two Cardano ledger types. - The signature lists the arguments in the order the handler receives them: | Argument | Type | Meaning | | --- | --- | --- | | `owner` | `PubKeyHash` | The `owner` parameter: the hash of the owner's public key | | `_` | `()` | The datum. `()` (unit) means "no information" | | `_` | `()` | The redeemer, also unit | | `ctx` | `ScriptContext` | The transaction being validated | - The body reads `ctx.txInfo.signatories`, the key hashes that signed the transaction, and uses `List.any` to check whether one of them is the owner. `\signer -> signer == owner` is an anonymous function (a lambda). The core `Tx` module has the same check as `Tx.signedBy owner ctx`, after `import Tx`. The `_` pattern ignores an argument. The datum and redeemer are still decoded strictly before `spend` runs, so they must be unit data. ## Check it `elci check` compiles every validator all the way through the backend, without writing any files: ```sh elci check . ``` ```text validator main (spend): checked; 300 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` The line names the validator and its purposes, then gives the size of the compiled script in bytes and the target. Script size matters on Cardano, because the transaction that uses a script pays for its bytes. ## The tests ```elm title="tests/MainTest.elm" module MainTest exposing (othersAreRejected, ownerSigns) import Julc.Data as Data import Julc.Ledger as Ledger exposing (PubKeyHash) import Scenario exposing (Scenario) owner : PubKeyHash owner = Scenario.key "owner" transaction : Scenario transaction = Scenario.spend "main" (Scenario.outRef "main" 0) Scenario.unit |> Scenario.parameters [ Data.encode owner ] ownerSigns : Scenario ownerSigns = transaction |> Scenario.signedBy owner othersAreRejected : Scenario othersAreRejected = transaction |> Scenario.signedBy (Scenario.key "someone else") |> Scenario.expectFailure ``` A `Scenario` describes one transaction against a validator of your project: - `Scenario.key "owner"` derives a deterministic test key hash from a name. - `Scenario.spend "main" (Scenario.outRef "main" 0) Scenario.unit` spends an output locked at validator `main`'s address, with a unit datum. - `Scenario.parameters [ Data.encode owner ]` supplies the deployment parameters, in the order declared in `elci.toml`. `Data.encode` turns an Elci value into on-chain data. - `Scenario.signedBy` adds a signer, and `Scenario.expectFailure` says the validator must reject the transaction. Only the exposed declarations of type `Scenario` are tests. `owner` and `transaction` are helpers. ## Run the tests ```sh elci test . ``` ```text PASS MainTest.ownerSigns (cpu 7675163, mem 29990) PASS MainTest.othersAreRejected (cpu 8053596, mem 31922) 2 passed, 0 failed ``` Each scenario compiles the validator exactly as `elci build` does, applies the parameters, builds a complete ledger context for the transaction and runs the script. `cpu` and `mem` are the execution budget the script used. The ledger charges fees for these units and limits how many a transaction may use. ## Build it ```sh elci build . ``` ```text validator main (spend): checked; 300 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` The build writes one directory per validator: - build/ - validators/ - elci-outputs.json the files Elci owns - main/ - abi.json how your types are encoded - validator.flat the script in FLAT encoding - validator.plutus a Plutus V3 text envelope - validator.plutus.json the Julc artifact for Cardano Client Lib `validator.plutus.json` is the file your off-chain code loads (the `cborHex` value is shortened here): ```json title="build/validators/main/validator.plutus.json" { "type": "PlutusScriptV3", "purpose": "spending", "description": "main", "cborHex": "59012f59012c010100323232322322323232323293370e...", "hash": "", "params": "owner:PubKeyHash", "sizeBytes": 300 } ``` The `hash` is empty because the script still needs its `owner` parameter. The hash of a script changes when you apply parameters to it. ## Make it your own Now change the validator so that spending needs two signatures: the owner's and a guardian's. 1. **Change the handler.** Add a `guardian` parameter, and move the signature check into a helper function so you can use it twice: ```elm title="src/Main.elm" ins={6,8-10,13-15} module Main exposing (spend) import Julc.Ledger exposing (PubKeyHash, ScriptContext) {-| Accept only transactions signed by both the owner and the guardian. -} spend : PubKeyHash -> PubKeyHash -> () -> () -> ScriptContext -> Bool spend owner guardian _ _ ctx = signedBy owner ctx && signedBy guardian ctx signedBy : PubKeyHash -> ScriptContext -> Bool signedBy key ctx = List.any (\signer -> signer == key) ctx.txInfo.signatories ``` `signedBy` isn't exposed, so it stays private to the module. 2. **Check it.** The compiler notices that the handler no longer matches its configuration: ```sh elci check . ``` ```text .../my-contract/src/Main.elm:8:1: ELCI2014: Handler Main.spend does not have the spend signature owner -> datum -> redeemer -> ScriptContext -> Bool that [validators.main] needs (1 parameter); its type is PubKeyHash -> PubKeyHash -> () -> () -> ScriptContext -> Bool; it has `()` where the configuration needs `ScriptContext` 8| spend : PubKeyHash -> PubKeyHash -> () -> () -> ScriptContext -> Bool ^^^^^ In .../my-contract/elci.toml:9: [validators.main] For more on ELCI2014, run `elci explain ELCI2014`. ``` The table still declares one parameter, so the handler must take one parameter, then the datum, the redeemer and the context. `spend` now takes two parameters, so its arguments no longer line up. `ELCI2014` always means that a handler and its `elci.toml` table disagree; `elci explain ELCI2014` says more. 3. **Declare the new parameter.** Parameters are listed in the order the handler receives them: ```toml title="elci.toml" ins={3} [validators.main] boundary = "julc-strict-v1" parameters = ["owner", "guardian"] spend = "Main.spend" ``` ```sh elci check . ``` ```text validator main (spend): checked; 323 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` 4. **Run the old tests.** They still supply only one parameter: ```sh elci test . ``` ```text FAIL MainTest.ownerSigns cannot prepare validator 'main': it takes 2 parameters [owner, guardian], but the scenario gives 1; set them with Scenario.parameters FAIL MainTest.othersAreRejected cannot prepare validator 'main': it takes 2 parameters [owner, guardian], but the scenario gives 1; set them with Scenario.parameters 0 passed, 2 failed ``` Both scenarios fail before the script even runs, and that includes `othersAreRejected`, which expects the validator to reject its transaction. Without this check it could pass for the wrong reason: the script would fail on the missing parameter, not on the missing signature. A scenario with `expectFailure` only proves something when the transaction is otherwise valid, so pair each one with a similar scenario that must succeed. 5. **Update the tests.** Supply both parameters, and cover each way the transaction can be signed: ```elm title="tests/MainTest.elm" module MainTest exposing (bothSign, guardianAloneIsRejected, othersAreRejected, ownerAloneIsRejected) import Julc.Data as Data import Julc.Ledger as Ledger exposing (PubKeyHash) import Scenario exposing (Scenario) owner : PubKeyHash owner = Scenario.key "owner" guardian : PubKeyHash guardian = Scenario.key "guardian" transaction : Scenario transaction = Scenario.spend "main" (Scenario.outRef "main" 0) Scenario.unit |> Scenario.parameters [ Data.encode owner, Data.encode guardian ] bothSign : Scenario bothSign = transaction |> Scenario.signedBy owner |> Scenario.signedBy guardian ownerAloneIsRejected : Scenario ownerAloneIsRejected = transaction |> Scenario.signedBy owner |> Scenario.expectFailure guardianAloneIsRejected : Scenario guardianAloneIsRejected = transaction |> Scenario.signedBy guardian |> Scenario.expectFailure othersAreRejected : Scenario othersAreRejected = transaction |> Scenario.signedBy (Scenario.key "someone else") |> Scenario.expectFailure ``` 6. **Run the tests again.** ```sh elci test . ``` ```text PASS MainTest.bothSign (cpu 11441262, mem 45132) PASS MainTest.ownerAloneIsRejected (cpu 11088065, mem 43199) PASS MainTest.guardianAloneIsRejected (cpu 8425738, mem 34154) PASS MainTest.othersAreRejected (cpu 8425738, mem 34154) 4 passed, 0 failed ``` To run only some tests, pass part of their name with `--match`: ```sh elci test . --match alone ``` ```text PASS MainTest.ownerAloneIsRejected (cpu 11088065, mem 43199) PASS MainTest.guardianAloneIsRejected (cpu 8425738, mem 34154) 2 passed, 0 failed ``` 7. **Build the new script.** ```sh elci build . ``` ```text validator main (spend): checked; 323 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` `validator.plutus.json` now lists both parameters: `"params": "owner:PubKeyHash,guardian:PubKeyHash"`. ## Apply the parameters Before you deploy, you apply the real parameter values. `elci apply` takes each value as Plutus Data in cardano-cli's JSON format. A `PubKeyHash` is 28 bytes, written as hex. These two keys are placeholders: ```sh elci apply . --validator main \ --param 'owner={"bytes": "01000000000000000000000000000000000000000000000000000000"}' \ --param 'guardian={"bytes": "02000000000000000000000000000000000000000000000000000000"}' ``` ```text validator main: applied 2 parameters; hash 7cf11fa222b171e7a3102db8f51aac3f4815ee8256ed47bde39cc6a6; wrote .../my-contract/build/validators/main/applied.plutus.json ``` `apply` first checks each value against the parameter's type. The applied script has its own hash, which determines its address on-chain. [Parameterized validators](https://elci-lang.dev/contracts/parameters/) explains the format. ## What you learned - `elci.toml` decides how functions are deployed: `[validators.]`, handler keys and `parameters`. - A spend handler receives parameters, datum, redeemer and context, and returns `Bool`. - `elci check` catches mistakes, including a handler that doesn't match its configuration. - `elci test` runs transaction scenarios against the exact script that `elci build` writes. ## Next steps - [Anatomy of a project](https://elci-lang.dev/start/project-tour/): Every file and directory in an Elci project. - [Learn the language](https://elci-lang.dev/learn/basics/): Values, functions and types, with runnable examples. - [Validators and purposes](https://elci-lang.dev/contracts/validators/): Handlers for spending, minting, withdrawing and governance. - [Transaction scenarios](https://elci-lang.dev/testing/scenarios/): Everything a scenario can describe. --- # Anatomy of a project > What each file and directory in an Elci project is for, from elci.toml and src/ to tests/ and the build outputs. Source: https://elci-lang.dev/start/project-tour/ This page walks through the files in an Elci project and what each one does, so you know where new code, tests and configuration belong. ## Two kinds of project Every project has an `elci.toml` that makes it one of two kinds: | Kind | Created with | Configured by | Commands | | --- | --- | --- | --- | | Validator project | `elci init ` | One or more `[validators.]` tables | `check`, `test`, `build`, `apply` | | Function project | `elci init --purpose function` | One `[entrypoint]` table | `check`, `test`, `build`, `eval` | A **validator project** holds scripts you deploy. A **function project** names one function or value for `elci eval` to run. It is not a validator, which makes it a good playground for learning the language, and the [Learn](https://elci-lang.dev/learn/basics/) pages use one. A project can't be both: ```text elci: ELCI4001: elci.toml mixes [entrypoint] with [validators.*]; use one form ``` ## The layout Here is a validator project with two validators, a shared helper module and a test module, after `elci build`: - treasury/ - elci.toml configuration - README.md - .gitignore contains build/ - src/ your modules - Auth.elm shared helper - Badge.elm the badge minting policy - Treasury.elm the treasury validator - tests/ test modules, never deployed - TreasuryTest.elm - build/ generated by elci build - validators/ - elci-outputs.json - badge/ - abi.json - validator.flat - validator.plutus - validator.plutus.json - treasury/ - abi.json - validator.flat - validator.plutus - validator.plutus.json ## elci.toml The configuration for that project: ```toml title="elci.toml" [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" # The treasury script: holds payouts until a beneficiary claims them. [validators.treasury] boundary = "julc-strict-v1" parameters = ["admin"] spend = "Treasury.spend" # A separate script: a minting policy for badges. [validators.badge] boundary = "julc-strict-v1" parameters = ["admin"] mint = "Badge.mint" # Julc Java libraries, imported in Elci as `import Julc.Math as Math`. [java-modules] "Julc.Math" = "org.julclang.stdlib.lib.MathLib" [tests] source-directories = ["tests"] ``` | Section | What it configures | | --- | --- | | `[project]` | `source-directories`, the on-chain `target` and the `julc-version` the compiler must match. Optionally `library-jars` for custom Java libraries. | | `[validators.]` | One deployed script with its own hash: `boundary`, handler keys (`spend`, `mint`, `withdraw`, `certify`, `vote`, `propose`), and optionally `parameters`, `spend-datum` and `expected-abi`. | | `[entrypoint]` | A function project: `module`, `function` and `purpose = "function"`. | | `[java-modules]` | Maps an Elm-style module name to a Julc on-chain Java library class. The core modules, such as `Tx` and `Interval`, need no mapping. | | `[tests]` | Where test modules live. The default is `["tests"]`. | Validator names use lowercase letters, digits, `-` and `_`, and start with a letter. The name becomes the directory under `build/validators/`, and tests refer to the validator by that name. The file uses a strict subset of TOML: section headers, `key = "value"` with quoted strings (no escapes), and arrays of quoted strings. Full-line `#` comments are fine, but a comment at the end of a line is not: ```text elci: ELCI4001: Expected a quoted string without escapes: "Main.spend" # the handler ``` Unknown or duplicate keys are errors too, which catches typos: ```text elci: ELCI4001: Unknown configuration key: [validators.main] boundry ``` [elci.toml reference](https://elci-lang.dev/reference/configuration/) lists every key. ## src/: your modules Each module lives at the path that matches its name under a source directory. `Treasury` is `src/Treasury.elm`, and `Market.Pricing` would be `src/Market/Pricing.elm`. Handler keys in `elci.toml` point at exposed functions as `Module.function`. Put code shared by several validators in its own module, such as `Auth.elm` above, and import it where you need it. Each validator only includes the code its handlers use. :::note `elci check` compiles the modules that your handlers (or your entrypoint) import, directly or indirectly. A module that nothing imports isn't compiled, so its errors don't show up until you use it. ::: To share Elci code between projects, keep it in its own directory and add that directory to `source-directories`. See [Modules and imports](https://elci-lang.dev/learn/modules/). ## tests/: test modules Test modules live under `tests/` and are compiled only by `elci test`. They are never part of `check`, `build` or the published scripts. A test is an exposed, zero-argument declaration of type `Bool`, `Expectation`, `Scenario` or `Property` (or an exposed function that returns a `Property`). Other declarations are helpers: ```text PASS TreasuryTest.beneficiaryReleases (cpu 11698810, mem 46679) PASS TreasuryTest.strangerCannotRelease (cpu 12077243, mem 48611) 2 passed, 0 failed ``` See [Unit tests](https://elci-lang.dev/testing/unit-tests/) for the details. ## build/: generated files `elci build` writes everything under `build/`, and `.gitignore` keeps it out of version control. For each validator it writes a directory under `build/validators/`: | File | Contents | | --- | --- | | `validator.plutus.json` | Julc's validator artifact (`type`, `purpose`, `cborHex`, `hash`, `params`, `sizeBytes`), which Cardano Client Lib applications load | | `validator.plutus` | A Plutus V3 text envelope | | `validator.flat` | The script's UPLC in FLAT encoding | | `abi.json` | Parameters, handlers, and how every datum and redeemer type is encoded | | `applied.plutus.json` | Written by `elci apply`: the script with its parameters applied, and its hash | `build/validators/elci-outputs.json` records which files Elci wrote. When you remove a validator from `elci.toml`, the next full build deletes that validator's files. Any other files in those directories are left alone. `elci build . --validator treasury` builds only the named validator and removes nothing else. Every selected validator compiles before anything is written, so a failed build leaves the previous outputs untouched. A function project writes `build/validator.flat`, `build/validator.plutus` and `build/abi.json`. It writes no `validator.plutus.json`, because a function isn't deployed. :::tip To guard against accidental changes to your on-chain data format, keep a copy of a validator's `abi.json` outside `build/` and point `expected-abi` at it. The build then fails if the encoding drifts. See [Build and deploy](https://elci-lang.dev/contracts/build-and-deploy/). ::: ## Where things go | You want to… | Put it in | | --- | --- | | Write a validator | A module in `src/`, plus a handler key in a `[validators.]` table | | Share helpers between validators | Another module in `src/`, imported where needed | | Reuse Elci code across projects | A separate source directory, added to `source-directories` | | Use a Julc Java library | A `[java-modules]` entry, then `import` it | | Use your own Java library | `library-jars` in `[project]`, plus a `[java-modules]` entry | | Test a validator or function | A module under `tests/` | | Pin the on-chain data format | A committed copy of `abi.json`, referenced by `expected-abi` | | Experiment with the language | A separate function project (`elci init playground --purpose function`) | --- # Build with AI > Point Claude Code, Codex, Cursor, Copilot or any coding agent at Elci's AI resources, and let it write, check and test Elci contracts. Source: https://elci-lang.dev/ai/ Elci is new, so AI models have seen little or none of it. Without context, an agent writes code that looks like Elm but doesn't compile in Elci. Typical mistakes are `Float`, `Set`, `Debug.log`, `String.length`, `Html` and helpers Elci doesn't have. The resources on this page give an agent the exact language, APIs and workflow in one read. The `elci` CLI then lets it check its own work. ## The short version Install Elci first ([Installation](https://elci-lang.dev/start/installation/)). Then start your agent with this prompt: ```text wrap You are writing Cardano smart contracts in Elci, an Elm-syntax language that compiles to Plutus V3. Before writing any code, read https://elci-lang.dev/ai/starter-pack.md and follow it strictly. Use https://elci-lang.dev/llms.txt to find other pages when you need them. After every change, run `elci check .` and `elci test .` and fix every ELCI error before continuing. Only use functions and modules that the starter pack or https://elci-lang.dev/ai/catalog.json lists. ``` ## Resources for agents | Resource | What it is | Use it | | --- | --- | --- | | [Starter pack](https://elci-lang.dev/ai/starter-pack.md) | The language subset, encodings, handler signatures, APIs, error fixes and complete examples, written for agents | Read it first, every session | | [Agent instructions](https://elci-lang.dev/ai/agent-instructions.md) | The must-follow rules in about a hundred lines | Save it as `AGENTS.md`, `CLAUDE.md` or a rules file | | [llms.txt](https://elci-lang.dev/llms.txt) | An index of every page, following the [llms.txt](https://llmstxt.org) convention | To find the right page | | [llms-full.txt](https://elci-lang.dev/llms-full.txt) | All documentation in one Markdown file | Attach it for long sessions | | `/ai/pages/…` | Each page as plain Markdown, such as [transaction scenarios](https://elci-lang.dev/ai/pages/testing/scenarios.md) | To read one topic | | [catalog.json](https://elci-lang.dev/ai/catalog.json) | Signatures of the bundled modules, compiler-provided modules and Java libraries | To check that a function exists | | [diagnostics.json](https://elci-lang.dev/ai/diagnostics.json) | Every ELCI error code and its meaning, the error format and the `--report=json` fields | To explain a compiler error | | [examples.json](https://elci-lang.dev/ai/examples.json) | The example projects, with every source file | To copy a working pattern | | [manifest.json](https://elci-lang.dev/ai/manifest.json) | The Elci and Julc versions the docs describe, and checksums of all of the above | To match your installed version | The pages and the catalog are generated from the same sources as this site. Every code sample in them was compiled with the Elci version named in the manifest. Check that `elci --version` reports the same version. ## Set up your agent Save the agent instructions where your tool reads them. They point the agent to the starter pack and the rest of these resources. **Claude Code** Claude Code reads `CLAUDE.md` at the start of every session. An `@path` line in it imports another file, so the whole starter pack is loaded too: ```sh curl -o CLAUDE.md https://elci-lang.dev/ai/agent-instructions.md curl --create-dirs -o docs/elci/starter-pack.md https://elci-lang.dev/ai/starter-pack.md echo '@docs/elci/starter-pack.md' >> CLAUDE.md ``` **Codex and AGENTS.md** Codex and other tools that follow the `AGENTS.md` convention read it from the project root: ```sh curl -o AGENTS.md https://elci-lang.dev/ai/agent-instructions.md ``` **Cursor** Cursor applies the rules in `.cursor/rules/`. This rule is active for Elci sources and `elci.toml`: ```sh mkdir -p .cursor/rules { printf -- '---\ndescription: Elci smart contract rules\nglobs: ["**/*.elm", "elci.toml"]\nalwaysApply: true\n---\n\n' curl -s https://elci-lang.dev/ai/agent-instructions.md; } > .cursor/rules/elci.mdc ``` **GitHub Copilot** Copilot reads repository instructions from `.github/copilot-instructions.md`: ```sh mkdir -p .github curl -o .github/copilot-instructions.md https://elci-lang.dev/ai/agent-instructions.md ``` **Chat on the web** For ChatGPT, Claude or Gemini in the browser, paste [the prompt above](#the-short-version). For a longer project, attach [llms-full.txt](https://elci-lang.dev/llms-full.txt) or the [starter pack](https://elci-lang.dev/ai/starter-pack.md) as a file. A chat can't run the compiler, so paste `elci check` and `elci test` output back into the conversation. ## Let the agent check its own work The CLI is the agent's feedback loop. Every command is non-interactive and exits with status 1 on failure. Errors are printed as `path:line:column: ELCIxxxx: message`, followed by the source line and any hints with the likely fix. A run reports every error it finds, one per top-level declaration, so the agent can fix several at once. With `--report=json`, `check`, `build`, `eval` and `test` print each error as one JSON object on its own line instead. `elci explain ` prints the long-form explanation of a code, with an example and its fix. | Command | What the agent uses it for | | --- | --- | | `elci check .` | Type-check and compile every validator, without writing files | | `elci test .` | Run unit tests, transaction scenarios and property tests. `--match` selects tests, and `--seed` reproduces a property run | | `elci eval .` | Evaluate a function project's value and print it with its cost; handy for checking an expression | | `elci build .` | Write the deployable artifacts once everything passes | | `elci explain ` | Explain an error code, such as `ELCI2002`, with an example and its fix | | `elci --help` | Every command and option | A loop that works well: 1. Write the types (datum, redeemer, parameters) and the handler signature, then run `elci check .`. 2. Write the logic, and run `elci check .` until it passes. 3. Add scenarios for every way the transaction should succeed or fail, plus a property, and run `elci test .`. 4. When a test fails, read the reason and the counterexample, then fix the code rather than the test. 5. Run `elci build .`. The native `elci` binary starts in milliseconds, which suits agents that run the compiler many times. ## Prompts to try ```text wrap Create an Elci validator in this project for a time-locked escrow: the datum holds the beneficiary and a deadline; the beneficiary may claim at or after the deadline; the depositor may reclaim before it. Add scenarios for every case and a property over the validity start. Follow the starter pack, and run elci check and elci test until everything passes. ``` ```text wrap Explain this Elci error and fix the code, using https://elci-lang.dev/ai/diagnostics.json and the starter pack: ``` ```text wrap Add property tests to tests/ for the validator in src/, covering every branch of the redeemer. Run elci test --seed 1 and show me any counterexample you find before changing code. ``` ## What to review yourself Agents write plausible code quickly. On-chain mistakes are costly, so review what matters before deploying: - **Authorization:** every path that releases funds or mints checks the right signatures, inputs or validity range. - **Types at the boundary:** the datum, redeemer and parameter types. `abi.json` records exactly how they are encoded. - **What leaves the script:** outputs, amounts and minted tokens, not only the redeemer's claims. - **Tests that prove something:** every `expectFailure` scenario has a matching success scenario, so a failure isn't caused by something unrelated. Elci is a development preview. Tests are not a security audit. There is no Elci MCP server yet. The CLI is the tool interface, and every agent that can run shell commands can use it. ## Next steps - [AI starter pack](https://elci-lang.dev/ai/starter-pack/): What an agent needs to write correct Elci. - [Agent instructions](https://elci-lang.dev/ai/agent-instructions/): The compact rules file for your project. - [Your first validator](https://elci-lang.dev/start/first-validator/): The hands-on tutorial, for you or your agent. - [Transaction scenarios](https://elci-lang.dev/testing/scenarios/): How to test validators against full transactions. --- # Elci AI starter pack > Everything an AI agent needs to write correct Elci on the first try. Ingest this page before generating Elci code. Source: https://elci-lang.dev/ai/starter-pack/ > **Read this entire page before generating any Elci code.** It states the Elci subset of Elm, the > project format, the on-chain rules and the mistakes agents make most often. Every rule, signature > and message here was checked with the Elci CLI (`elci 0.1.0-dev / julc 0.1.0-pre20-ebaf028-SNAPSHOT`). This page is optimized for AI ingestion, not for humans. For the tutorial, see [your first validator](https://elci-lang.dev/start/first-validator/). Sections are numbered so you can cite them. --- ## 1. What Elci is, and what it is not - **Elci** compiles `.elm` source written in an **Elm 0.19 syntax subset** into **Plutus V3** scripts for Cardano. It lowers to typed PIR and uses Julc's backend (validator wrapper, UPLC optimizer, evaluator). The only target is `plutus-v3-pv11-uplc-1.1.0`. - **On-chain only.** Elci code is validator logic. Building transactions is done off-chain with other tools (for example Cardano Client Lib, reading `validator.plutus.json`). - **Not browser Elm.** There is no `Html`, `Browser`, `Cmd`, `Sub`, `port`, JSON decoders or runtime. - **No Elm packages.** `elm.json` is not used. There is no package manager. - **Elm's core library, partly.** `Basics`, `List`, `Maybe`, `Result`, `Tuple`, `String` and `Debug` are imported by default, as in Elm, with Elm's names and argument orders, but `String` has only five functions and `Debug` only `trace`. `Dict` exists with Elm's API (`import Dict exposing (Dict)`); `Set`, `Char` and `Float` **do not exist** (section 3.3). A `Value` module (`import Value exposing (Value)`) handles the ledger's multi-asset values (section 7). `Tx` and `Interval` answer questions about the transaction and its validity range (section 8); prefer them to hand-written list walks and to Julc's `ContextsLib` and `IntervalLib`. - **Strict, not lazy.** Every `let` value and every function argument is evaluated (section 4). - **Configuration decides deployment.** Handlers are ordinary functions; `elci.toml` says which function serves which ledger purpose (section 2). - **Development preview.** Not production-ready. No CIP-57 blueprints, no language server. ## 2. Project layout and `elci.toml` `elci init my-project` creates a validator project; `elci init my-project --purpose function` creates a function project for `elci eval`. ```text my-project/ elci.toml project configuration (strict TOML subset) src/Main.elm source modules; module A.B lives at src/A/B.elm tests/MainTest.elm test modules (validator projects only) build/ outputs of elci build (git-ignored) ``` A complete `elci.toml` using every validator setting: ```toml title="elci.toml" # Settings for the whole project. [project] source-directories = ["src", "lib"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" library-jars = ["libs/pricing.jar"] # One script with two purposes and a deployment parameter. [validators.treasury] boundary = "julc-strict-v1" parameters = ["admin"] spend = "Treasury.spend" withdraw = "Treasury.withdraw" spend-datum = "optional" expected-abi = "abi/treasury.json" # A second, independent script. [validators.badge] boundary = "julc-strict-v1" parameters = ["admin"] mint = "Badge.mint" [java-modules] "Julc.Math" = "org.julclang.stdlib.lib.MathLib" "Acme.Pricing" = "acme.Pricing" [tests] source-directories = ["spec"] ``` A function project instead has: ```toml title="elci.toml" [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" [entrypoint] module = "Main" function = "main" purpose = "function" ``` ### 2.1 Rules 1. `[project]` requires `source-directories`, `target = "plutus-v3-pv11-uplc-1.1.0"` and `julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT"` (must equal the compiler's pinned version; `elci --version` prints it). Optional: `library-jars`; `expected-abi` only in function projects. 2. A project has **either** one or more `[validators.]` tables **or** one `[entrypoint]`, never both. 3. `[validators.]`: name is 1–64 chars of `a-z`, `0-9`, `-`, `_`, starting with a letter. `boundary = "julc-strict-v1"` is required. At least one handler key: `spend`, `mint`, `withdraw`, `certify`, `vote`, `propose`, each `"Module.function"` naming an **exposed** function. One table = one script = one hash. 4. `parameters = ["a", "b"]`: deployment parameters in application order; names match `[a-z][A-Za-z0-9_]*`. Every handler of the table receives all of them as leading arguments, with the same types. 5. `spend-datum = "required"` (default), `"optional"` or `"absent"`; requires a `spend` handler. 6. `expected-abi = "path"`: `check`, `build` and `apply` fail if the ABI (`abi.json`) differs. 7. `[java-modules]`: `"Module.Name" = "fully.qualified.Class"`. You choose the module name. `Julc.TypedMap` and `Julc.TypedArray` are reserved, and so are the core module names (`Basics`, `List`, `Maybe`, `Result`, `String`, `Tuple`, `Map`, `Dict`, `Array`, `Set`, `Value`, `Interval`, `Tx`, `Elci`) and names starting with `Elci.`. 8. `[tests]`: `source-directories`, default `["tests"]`. 9. TOML subset: one `key = value` per line; values are quoted strings without escapes or single-line arrays of quoted strings; comments are whole lines starting with `#` (no trailing comments); unknown tables and keys are errors. ## 3. The language subset ### 3.1 Allowed - Modules: `module A.B exposing (f, T, U(..))` or `exposing (..)`; `import M as N exposing (x, T(..))`. Imports must be acyclic. Every module has Elm's default imports (section 8). Own declarations shadow imports; a name two imports expose is ambiguous where used, except that a name listed in `exposing` beats a default import. - **Every top-level declaration needs a type signature.** Local (`let`) definitions may have one. - Functions, lambdas `\a b -> ...`, currying, partial application, `|>`, `<|`, `>>`, `<<`. Operators as functions: `(+)`, `(::)`, `(<)`, as in `List.foldl (+) 0 xs`. `^` is integer power. - Rank-1 generics (`a -> a`), specialized per concrete use. Local helpers may be used at several types. Elm's constrained variables: a signature variable compared with `<`/`compare` must be named `comparable…`, one appended with `++` `appendable…`, both `compappend…`. - Types nothing determines need no annotation (`[] == []`, `List.length []`, `Data.encode Nothing`), except where a value is created: `Data.decode` and generic Java functions (`ELCI3001`). - Self-recursion and mutually recursive functions (top-level or local). - Types: `Int` (arbitrary precision), `Bool`, `String`, `()`, `Bytes`, `Data`, `List a`, `Maybe a`, `Result error value`, `Order`, `Dict k v` (comparable keys), `Map k v`, `Array a`, `Value`, tuples of 2 or 3, closed records, custom types (generic, recursive). - `type alias R = { x : Int }` (also a constructor `R 1`); `type T a = A a | B`. - Records: `{ x = 1 }`, `r.x`, `{ r | x = 2 }`, destructuring `{ x, y }` in parameters and `let`. Accessor functions as in Elm: `.x` is `\r -> r.x`, so `List.map .amount payouts` works. A field of a qualified value: `Config.settings.fee`. Write qualified names without spaces. - `case` with nested patterns: constructors, `[]`, `[ a, b ]`, `a :: rest`, tuples, record fields, `as`, `_`, `()`, `True`/`False`, Int and String literals. Exhaustive and no unreachable branches. - `if c then a else b` (else required), `let ... in`, `++` on `String` and `List`, `::`. - `<`, `<=`, `>`, `>=`, `compare`, `min`, `max`, `List.sort` on **comparable** types: `Int`, `String` (UTF-8 byte order), `Bytes`, ledger hashes (`PubKeyHash`, `ScriptHash`, `PolicyId`, `TokenName`, `TxId`, …), and lists and tuples of these. - Comments `--`, `{- nested {- ok -} -}`, doc comments `{-| -}`. ### 3.2 Rejected by the compiler | Construct | Result | | --- | --- | | `Float`, `1.5`, `/` | `ELCI1001`/`ELCI2001`; use scaled `Int` and `//` | | `number` in a signature | `ELCI2003`; write `Int` | | `a > b` or `a ++ b` on a signature variable not named `comparable…`/`appendable…` | `ELCI2003`; rename it | | Extensible records `{ r \| x : Int }` | `ELCI1002` | | Refutable patterns in function or lambda arguments (`f (x :: _) = ...`) | `ELCI1002`; use `case` | | Chained comparisons `a < b < c` | `ELCI1002`; use `a < b && b < c` | | Tuples of 4 or more | `ELCI1002` | | `(/)` | `ELCI2001`; use `(//)` | | Char literals `'a'`, `%`, hex `0xFF`, `1_000` | `ELCI1001` | | `port module`, `import Html` | `ELCI1002` / `ELCI2101` | | Polymorphic recursion, recursive values (`a = b + 1`, `b = a + 1`) | `ELCI2002` / `ELCI2008` | | `==` on functions, `Map`, `Array`, `TxOut`, `TxInInfo`, `TxInfo` (`Data` and `Value` do have `==`) | `ELCI2003` | | `<`, `compare`, `List.sort` on `Bool`, records, custom types, `Maybe`, `Dict`, `Map`, `Value`, `Data`, `TxOutRef` | `ELCI2003`; use `List.sortBy` with a key; compare values with `Value.geq` | | `Dict` keys that are not comparable (`Dict.get` on a `Dict Credential Int`; a `Dict Bool Int` in a datum or `Data.decode`) | `ELCI2003`; use a `Map` | | A Java library generic at a type containing a `Dict` | `ELCI3003`; pass a `Map` or `List`, then `Dict.fromList` | | A Java export whose result can hold a `Value` (`ValuesLib.add`, `ValuesLib.singleton`, a Java record with a `Value` field, …); `ContextsLib` and `OutputLib` are exempt | `ELCI2105`; use the core `Value` module | | A Java library generic at a type holding a `Value` (`Lists.filter values f`) | `ELCI3003`; use `List` and the `Value` module | | `Value` in a datum, redeemer or parameter type | `ELCI2013`; store `List ( PolicyId, TokenName, Int )`, rebuild with `Value.fromList` | | Module named `Basics`, `List`, `Maybe`, `Result`, `String`, `Tuple`, `Map`, `Dict`, `Array`, `Set`, `Value`, `Interval`, `Tx`, `Elci` or `Elci.*`; alias named after a default import (`Basics`, `List`, `Maybe`, `Result`, `String`, `Tuple`, `Debug`) | `ELCI2104` | | Constructors named `Just`, `Nothing`, `True`, `False`; types named `Int`, `Bool`, `String`, `Bytes`, `Data`, `List`, `Maybe`, `Dict`, `Map`, `Array`, `Float` | `ELCI2011` | | `f <\| x \|> g`, `f << g >> h` (opposite associativity mixed) | `ELCI1002`; add parentheses | | `Tuple a b`, `Unit` as type names | `ELCI2012`; write `( a, b )`, `()` | ### 3.3 Elm idioms that do NOT exist in Elci, and what to write instead | Elm | Elci | | --- | --- | | `String.length s` | `Bytes.length (Bytes.fromString s)` (UTF-8 byte count) | | `String.toInt`, `split`, `slice`, `toUpper` and other `String.*` | none; keep numbers as `Int` and bytes as `Bytes`. `String` has only `fromInt`, `isEmpty`, `append`, `concat`, `join` | | `String.fromFloat`, `toFloat`, `round`, `sqrt`, `pi` | none; no `Float`: use scaled `Int` | | `Set` | `Dict k Bool` or a `List` (`Dict k ()` is rejected: `()` has no Data encoding) | | `Char`, `'a'` | none | | `List.map4`, `Maybe.map4`, `Result.map5`, … | nest `map3`, or `case` | | `Debug.log`, `Debug.toString`, `Debug.todo` | `Debug.trace : String -> a -> a`; `String.fromInt` for numbers | | `Bytes.lessThan` (removed) | `a < b` | Everything else in Elm's `Basics`, `List`, `Maybe`, `Result`, `Tuple` exists with Elm's signature (section 8). This program uses them; it compiles and runs: ```elm title="src/Main.elm" module Main exposing (main) type alias Payout = { beneficiary : String , amount : Int } payouts : List Payout payouts = [ Payout "carol" 30, Payout "alice" 50, Payout "bob" 30 ] main : ( ( Int, Maybe Int, List String ), ( String, Int, Result String Int ) ) main = ( ( List.sum (List.map (\p -> p.amount) payouts) , List.maximum (List.map (\p -> p.amount) payouts) , List.map (\p -> p.beneficiary) (List.sortBy (\p -> p.amount) payouts) ) , ( String.join ", " (List.map (\p -> p.beneficiary ++ "=" ++ String.fromInt p.amount) payouts) , Maybe.withDefault 0 (List.head (List.range 5 9)) , Result.fromMaybe "no payouts" (Maybe.map (\p -> p.amount) (List.head payouts)) ) ) ``` ```text $ elci eval ( ( 110, Just 50, ["carol", "bob", "alice"] ), ( "carol=30, alice=50, bob=30", 5, Ok 30 ) ) cpu 65205046, mem 276517 ``` ## 4. Evaluation rules 1. **Strict.** Every `let` binding is evaluated before the body, even if unused. An unused `let bad = 10 // 0` fails the script. 2. **Selective forms only:** `if`, `case`, `&&`, `||` evaluate just the taken branch/operand. Put cheap, likely-false checks first **inline**; a check bound in `let` is always paid for. 3. **Order:** function, then arguments left to right; `let` bindings in dependency order, ties in source order; record fields, list and tuple elements in source order; record update: base, then new fields. Arguments are always evaluated: `Maybe.withDefault expensive m` computes `expensive` even for a `Just`, and `(&&) a b` as a function evaluates `b`. Use `case` or the `&&` operator to skip work. 4. **Top-level values** (zero-argument declarations) reachable from any handler run once per script execution **before** purpose dispatch, so every purpose pays for them. Make expensive ones functions (`limit : () -> Int`). 5. **Negation** follows Elm 0.19: `f -1` passes `-1`; `scale -x 10` passes `-x`; `x - 1` and `x-1` subtract; `x -1` is an application of `x`. 6. **Integers:** `//` truncates towards zero (`-7 // 2` is `-3`). `modBy d v` has the divisor's sign (`modBy 3 -7` is `2`); `remainderBy d v` has the value's sign (`remainderBy 3 -7` is `-1`). Division by zero fails the script. 7. **Failure:** a runtime failure (division by zero, out-of-range byte, failed decode) or a handler result of `False` rejects the transaction. There are no exceptions to catch. 8. **Equality** (`==`, `/=`): Int, Bool, String, `()`, Bytes, ledger hashes of the same type, `Value` (exact), `Data` (exact structure), lists, tuples, records, `Maybe`, `Dict` (same bindings) and custom types of these; imported types with approved equality. Not `Map`, functions, `Array`, `TxOut`, `TxInInfo`, `TxInfo`. Both operands are evaluated once, left to right. 9. **Ordering** (`<`, `<=`, `>`, `>=`, `compare`, `min`, `max`, `List.sort`): Int numerically; String by UTF-8 bytes (`"Zebra" < "apple"`); Bytes and ledger hashes lexicographically by unsigned byte, a proper prefix first (`#ff` > `#0001`); lists lexicographically; tuples by component. Every order agrees with `==`. 10. **Integer power:** `2 ^ 10` is `1024`; `^` binds tighter than `*` and groups right; a negative exponent fails the script. ## 5. Data encoding and `Julc.Data` Datums, redeemers and parameters cross the script boundary as Plutus Data and are **decoded strictly** (tags, arity, field kinds checked; malformed data fails the script, never `Nothing`). | Elci value | Plutus Data | | --- | --- | | `Int` | integer | | `Bytes`, ledger hashes | byte string | | `String` | byte string (UTF-8) | | `Bool` | `False` = constructor 0, `True` = constructor 1, no fields | | `()` | constructor 0, no fields (as datum/redeemer; `Julc.Data.encode ()` is rejected) | | record | constructor 0, fields **sorted by field name** (not declaration order) | | tuple | constructor 0, fields in position order | | custom type | constructor tag = variant's declaration position from 0, fields in order | | `Maybe a` | `Nothing` = constructor 0; `Just x` = constructor 1 `[x]` | | `Result e a` | `Ok x` = constructor 0 `[x]`; `Err e` = constructor 1 `[e]` (the **reverse** of PlutusTx `Either`) | | `Order` | `LT`, `EQ`, `GT` = constructors 0, 1, 2 | | `JulcOptional a` (Java `Optional`) | `Some x` = constructor 0 `[x]`; `None` = constructor 1 | | `List a` / `Map k v` | list / map (in the map's order) | | `Dict k v` | map with **strictly ascending keys**; unsorted or repeated keys fail to decode (datum, redeemer, parameter, `Data.decode`) | | `Array a`, functions | **no encoding**; cannot be stored in data (`ELCI2013`) | ```elm type Action = Buy Int | Cancel type alias Payout = { beneficiary : Int, amount : Int } main : List Data main = [ Data.encode (Just 5) , Data.encode Cancel , Data.encode (Buy 3) , Data.encode { beneficiary = 1, amount = 2 } , Data.encode ( 1, True ) , Data.encode [ 1, 2 ] , Data.encode (Data.toOptional (Just 5)) , Data.encode "hi" ] ``` ```text $ elci eval [Constr 1 [5], Constr 1, Constr 0 [3], Constr 0 [2, 1], Constr 0 [1, Constr 1], [1, 2], Constr 0 [5], #6869] cpu 5878521, mem 25242 ``` `Julc.Data` (`import Julc.Data as Data`): `decode : Data -> a`, `encode : a -> Data`, `fromOptional : JulcOptional a -> Maybe a`, `toOptional : Maybe a -> JulcOptional a`. The decoded type must be known where `decode` is used; annotate otherwise (`ELCI3001: Cannot determine the type converted by Julc.Data.decode; add a type annotation`). `==` on `Data` compares structure exactly, so a datum check needs no decoding: `Tx.inlineDatum out == Just (Data.encode expected)`. Elci's encodings are deterministic, except that a `Map`'s encoding follows its insertion order: `Data.encode` of two maps with the same bindings may differ, so compare their sorted `toList` or use a `Dict`. Tests build datums and redeemers with `Data.encode`. ## 6. Handler signatures `parameters` come first, in the order listed in `elci.toml`. The last argument is always `ScriptContext` (`import Julc.Ledger exposing (ScriptContext)`). The result is `Bool`. Handlers must be exposed and have concrete types. | Handler key | Required type | | --- | --- | | `spend` | `params -> datum -> redeemer -> ScriptContext -> Bool` | | `spend` + `spend-datum = "optional"` | `params -> Maybe datum -> redeemer -> ScriptContext -> Bool` | | `spend` + `spend-datum = "absent"` | `params -> redeemer -> ScriptContext -> Bool` | | `mint`, `withdraw`, `certify`, `vote`, `propose` | `params -> redeemer -> ScriptContext -> Bool` | - Datum, redeemer and parameter types need a strict Data boundary: your records and custom types, `Int`, `Bool`, `String`, `Bytes`, `()`, tuples, lists, `Dict`, `Map`, `Maybe`, and ledger types such as `PubKeyHash`, `TxOutRef`, `Credential`, `PolicyId`, `TokenName`. Not: functions, `Array`, `ScriptContext`, `TxOut`, `TxInfo`, `Value` (for an amount of tokens, store `List ( PolicyId, TokenName, Int )` and call `Value.fromList`). - Use `()` for an unused redeemer or datum. Only the selected handler's datum and redeemer are decoded. - A validator always checks the purpose; purposes without a handler are rejected. - Parameters are decoded but **not validated on-chain**; `elci apply` validates them off-chain. Exception: the key order of a `Dict` (in a datum, redeemer or parameter) is checked on every run. ## 7. The script context and ledger types `import Julc.Ledger exposing (ScriptContext, PubKeyHash)` exposes types. To match or build a sum type's constructors unqualified, expose them with `(..)`: `Credential(..)`, `ScriptInfo(..)`, `OutputDatum(..)`, `IntervalBoundType(..)`. `exposing (..)` exposes types only, not constructors (a `PubKeyCredential` pattern then fails with `ELCI2001`). Fields are read with `.field`. | Expression | Type | | --- | --- | | `ctx.txInfo` | `TxInfo` | | `ctx.scriptInfo` | `ScriptInfo` | | `ctx.redeemer` | `Data` (raw; the handler already gets it decoded) | | `tx.inputs`, `tx.referenceInputs` | `List TxInInfo` | | `tx.outputs` | `List TxOut` | | `tx.fee` | `Int` (lovelace) | | `tx.mint` | `Value` | | `tx.certificates` | `List TxCert` | | `tx.withdrawals` | `Map Credential Int` | | `tx.validRange` | `Interval` (POSIX milliseconds) | | `tx.signatories` | `List PubKeyHash` | | `tx.redeemers` | `Map ScriptPurpose Data` | | `tx.datums` | `Map DatumHash Data` | | `tx.id` | `TxId` | | `tx.proposalProcedures` | `List ProposalProcedure` | | `input.outRef` / `input.resolved` | `TxOutRef` / `TxOut` | | `out.address`, `out.value`, `out.datum`, `out.referenceScript` | `Address`, `Value`, `OutputDatum`, `JulcOptional ScriptHash` | | `ref.txId`, `ref.index` | `TxId`, `Int` | | `addr.credential`, `addr.stakingCredential` | `Credential`, `JulcOptional StakingCredential` | | `range.from`, `range.to` | `IntervalBound` | | `bound.boundType`, `bound.isInclusive` | `IntervalBoundType`, `Bool` | | `pkh.hash`, `txId.hash`, `policy.hash`, `token.hash` (a `TokenName`; it has no `.name`), `scriptHash.hash` | `Bytes` | Sum types: `Credential = PubKeyCredential PubKeyHash | ScriptCredential ScriptHash`; `OutputDatum = NoOutputDatum | OutputDatumHash DatumHash | OutputDatumInline Data`; `IntervalBoundType = NegInf | Finite Int | PosInf`; `ScriptInfo = MintingScript PolicyId | SpendingScript TxOutRef (JulcOptional Data) | RewardingScript Credential | CertifyingScript Int TxCert | VotingScript Voter | ProposingScript Int ProposalProcedure`. ```elm keyOf : Credential -> Bytes keyOf cred = case cred of PubKeyCredential pkh -> pkh.hash ScriptCredential sh -> sh.hash ``` For datums, use `Tx.inlineDatum out` or `Tx.datumOf out ctx` (section 8) rather than matching `OutputDatum` yourself. Rules: - `PubKeyHash`, `ScriptHash`, `PolicyId`, `TokenName`, `TxId`, `DatumHash` are **distinct from `Bytes`**; compare like with like, or use `.hash`. `key == Bytes.fromString "k"` with `key : PubKeyHash` is `` ELCI2002: The two sides of == have different types: the left is `PubKeyHash`, but the right is `Bytes` ``, with a hint to wrap the bytes with the constructor. `token.name` is `ELCI2010: Unknown field 'name' of TokenName`; write `token.hash`. - Signature check: `Tx.signedBy key ctx`. - Ledger hashes order like bytes: `List.sort` sorts a `List PubKeyHash`. `TxOutRef` and other ledger records are not comparable. - Construct ledger values qualified: `import Julc.Ledger as Ledger` then `Ledger.PubKeyHash bytes`, `Ledger.TxOutRef (Ledger.TxId bytes) 0`. - `Value` (`out.value`, `tx.mint`) is the core `Value` type: read it with the `Value` module (section 8), `Value.lovelaceOf value`, `Value.quantityOf policy tokenName value`. Build asset ids with `import Julc.Ledger exposing (PolicyId(..), TokenName(..))`: `PolicyId bytes`, `TokenName (Bytes.fromString "BADGE")`. Ada is `PolicyId Bytes.empty` with `TokenName Bytes.empty`; use `Value.lovelace` and `Value.lovelaceOf`. - Every `Value` is canonical (policies and token names ascending, no zero quantity), so `==` on values is exact. `TxOut`, `TxInInfo` and `TxInfo`, which contain a `Value`, have no equality. A `Value` cannot cross the boundary yet. In `tx.mint`, burns are negative quantities. - `JulcOptional` has no equality; convert with `Data.fromOptional`. ## 8. Core modules and test library (exact signatures) Every source and test module implicitly has, as in Elm: ```elm import Basics exposing (..) import List import Maybe exposing (Maybe(..)) import Result exposing (Result(..)) import String import Tuple import Debug ``` `Bytes`, `Dict`, `Map`, `Array`, `Value`, `Interval`, `Tx` and `Julc.Data` need explicit imports. Using one without its import is `` ELCI2104: Bytes.fromString needs the Bytes module; add `import Bytes` ``. Machine-readable signatures of every module are in `/ai/catalog.json`. **Basics** (unqualified everywhere): ```text negate : Int -> Int abs : Int -> Int min : comparable -> comparable -> comparable max : comparable -> comparable -> comparable clamp : Int -> Int -> Int -> Int -- clamp low high x compare : comparable -> comparable -> Order -- type Order = LT | EQ | GT identity : a -> a always : a -> b -> a not : Bool -> Bool xor : Bool -> Bool -> Bool modBy : Int -> Int -> Int -- modBy divisor value; divisor's sign remainderBy : Int -> Int -> Int -- value's sign (^) : Int -> Int -> Int -- negative exponent fails ``` Built-in constructors: `Just`, `Nothing`, `True`, `False`. Every binary operator is also a function: `(+)`, `(::)`, `(<)`, `(++)`. **List** (element first, accumulator second in folds; `map2`/`map3` stop at the shortest list; sorts are stable): ```text singleton : a -> List a repeat : Int -> a -> List a -- [] when n <= 0 range : Int -> Int -> List Int -- range 1 3 == [ 1, 2, 3 ]; range 5 1 == [] map : (a -> b) -> List a -> List b indexedMap : (Int -> a -> b) -> List a -> List b filter : (a -> Bool) -> List a -> List a filterMap : (a -> Maybe b) -> List a -> List b foldl : (a -> b -> b) -> b -> List a -> b foldr : (a -> b -> b) -> b -> List a -> b concat : List (List a) -> List a concatMap : (a -> List b) -> List a -> List b intersperse : a -> List a -> List a map2 : (a -> b -> result) -> List a -> List b -> List result map3 : (a -> b -> c -> result) -> List a -> List b -> List c -> List result length : List a -> Int reverse : List a -> List a member : a -> List a -> Bool all : (a -> Bool) -> List a -> Bool any : (a -> Bool) -> List a -> Bool maximum : List comparable -> Maybe comparable minimum : List comparable -> Maybe comparable sum : List Int -> Int product : List Int -> Int append : List a -> List a -> List a isEmpty : List a -> Bool head : List a -> Maybe a tail : List a -> Maybe (List a) take : Int -> List a -> List a -- [] when n <= 0 drop : Int -> List a -> List a -- unchanged when n <= 0 partition : (a -> Bool) -> List a -> ( List a, List a ) -- ( passing, failing ) unzip : List ( a, b ) -> ( List a, List b ) sort : List comparable -> List comparable sortBy : (a -> comparable) -> List a -> List a sortWith : (a -> a -> Order) -> List a -> List a ``` **Maybe** (`Maybe`, `Just`, `Nothing` built in): ```text withDefault : a -> Maybe a -> a -- evaluates the default even for Just map : (a -> b) -> Maybe a -> Maybe b map2 : (a -> b -> value) -> Maybe a -> Maybe b -> Maybe value map3 : (a -> b -> c -> value) -> Maybe a -> Maybe b -> Maybe c -> Maybe value andThen : (a -> Maybe b) -> Maybe a -> Maybe b ``` **Result** (`type Result error value = Ok value | Err error`): ```text map : (a -> value) -> Result x a -> Result x value map2 : (a -> b -> value) -> Result x a -> Result x b -> Result x value -- first Err wins map3 : (a -> b -> c -> value) -> Result x a -> Result x b -> Result x c -> Result x value andThen : (a -> Result x b) -> Result x a -> Result x b withDefault : a -> Result x a -> a toMaybe : Result x a -> Maybe a fromMaybe : x -> Maybe a -> Result x a mapError : (x -> y) -> Result x a -> Result y a ``` **Tuple** (pairs only): ```text pair : a -> b -> ( a, b ) first : ( a, b ) -> a second : ( a, b ) -> b mapFirst : (a -> x) -> ( a, b ) -> ( x, b ) mapSecond : (b -> y) -> ( a, b ) -> ( a, y ) mapBoth : (a -> x) -> (b -> y) -> ( a, b ) -> ( x, y ) ``` **String** (UTF-8 text; these five only): ```text fromInt : Int -> String -- fromInt -42 == "-42" isEmpty : String -> Bool append : String -> String -> String concat : List String -> String join : String -> List String -> String -- join ", " [ "a", "b" ] == "a, b" ``` **Dict** (`import Dict exposing (Dict)`; Elm's API; a Plutus map with keys strictly ascending; keys must be comparable; `keys`/`toList`/folds in ascending key order; `get`/`member`/`insert`/`remove` O(n), stopping at the first key not smaller; `fromList` O(n log n), last binding wins; `union` prefers the left; `==` compares bindings): ```text empty : Dict k v singleton : comparable -> v -> Dict comparable v insert : comparable -> v -> Dict comparable v -> Dict comparable v update : comparable -> (Maybe v -> Maybe v) -> Dict comparable v -> Dict comparable v remove : comparable -> Dict comparable v -> Dict comparable v isEmpty : Dict k v -> Bool member : comparable -> Dict comparable v -> Bool get : comparable -> Dict comparable v -> Maybe v size : Dict k v -> Int keys : Dict k v -> List k values : Dict k v -> List v toList : Dict k v -> List ( k, v ) fromList : List ( comparable, v ) -> Dict comparable v map : (k -> a -> b) -> Dict k a -> Dict k b foldl : (k -> v -> b -> b) -> b -> Dict k v -> b -- smallest key first foldr : (k -> v -> b -> b) -> b -> Dict k v -> b -- largest key first filter : (comparable -> v -> Bool) -> Dict comparable v -> Dict comparable v partition : (comparable -> v -> Bool) -> Dict comparable v -> ( Dict comparable v, Dict comparable v ) union : Dict comparable v -> Dict comparable v -> Dict comparable v intersect : Dict comparable v -> Dict comparable v -> Dict comparable v diff : Dict comparable a -> Dict comparable b -> Dict comparable a merge : (comparable -> a -> result -> result) -> (comparable -> a -> b -> result -> result) -> (comparable -> b -> result -> result) -> Dict comparable a -> Dict comparable b -> result -> result ``` **Map** (`import Map` or `import Map exposing (Map)`; the ledger's association list, keys compared by Data encoding, any Data-encodable key; O(n) lookups; `insert` puts the binding first; `fromList` is O(n²); `filter`/`map` are O(n) and keep order; no `==`. A map decoded from Data may repeat a key: `get` returns the first binding, `remove`/`insert` drop every binding of it. Use it for ledger maps such as `txInfo.withdrawals` and for keys that are not comparable; use `Dict` otherwise): ```text empty : Map k v singleton : k -> v -> Map k v get : k -> Map k v -> Maybe v member : k -> Map k v -> Bool insert : k -> v -> Map k v -> Map k v remove : k -> Map k v -> Map k v update : k -> (Maybe v -> Maybe v) -> Map k v -> Map k v size : Map k v -> Int isEmpty : Map k v -> Bool keys : Map k v -> List k values : Map k v -> List v toList : Map k v -> List ( k, v ) fromList : List ( k, v ) -> Map k v foldl : (k -> v -> b -> b) -> b -> Map k v -> b foldr : (k -> v -> b -> b) -> b -> Map k v -> b filter : (k -> v -> Bool) -> Map k v -> Map k v map : (k -> a -> b) -> Map k a -> Map k b ``` **Array** (`import Array`; O(1) `get`/`length`; no Data encoding; no `set`/`push`): ```text empty : Array a fromList : List a -> Array a toList : Array a -> List a toIndexedList : Array a -> List ( Int, a ) length : Array a -> Int isEmpty : Array a -> Bool get : Int -> Array a -> Maybe a initialize : Int -> (Int -> a) -> Array a repeat : Int -> a -> Array a foldl : (a -> b -> b) -> b -> Array a -> b foldr : (a -> b -> b) -> b -> Array a -> b map : (a -> b) -> Array a -> Array b indexedMap : (Int -> a -> b) -> Array a -> Array b filter : (a -> Bool) -> Array a -> Array a slice : Int -> Int -> Array a -> Array a ``` **Value** (`import Value exposing (Value)`; the ledger's multi-asset value, as in `out.value` and `tx.mint`; `PolicyId` and `TokenName` from `Julc.Ledger`; always canonical, so `==` is exact; quantities may be negative, as for burns in a mint; `lovelaceOf` and `isZero` read the Data directly, the filters (`positive`, `negative`, `restrictedTo`, `withoutLovelace`) walk it, the rest convert through the CIP-153 builtins, about 1M CPU for a few assets; a policy id or token name over 32 bytes, or a quantity outside signed 128 bits, fails the script. `Data.encode` of a `Value` is free and `Data.decode` accepts only canonical values, but a `Value` cannot be a datum, redeemer or parameter, and a type that contains one cannot be decoded): ```text zero : Value lovelace : Int -> Value singleton : PolicyId -> TokenName -> Int -> Value insert : PolicyId -> TokenName -> Int -> Value -> Value -- sets the quantity; 0 removes the token fromList : List ( PolicyId, TokenName, Int ) -> Value -- repeated tokens add up lovelaceOf : Value -> Int -- ada's token only; 0 when none, as in a mint quantityOf : PolicyId -> TokenName -> Value -> Int -- 0 when none isZero : Value -> Bool policies : Value -> List PolicyId -- ascending; ada's is the empty policy tokens : PolicyId -> Value -> Dict TokenName Int toList : Value -> List ( PolicyId, TokenName, Int ) -- ascending add : Value -> Value -> Value subtract : Value -> Value -> Value -- first minus second negate : Value -> Value scale : Int -> Value -> Value sum : List Value -> Value geq : Value -> Value -> Bool -- geq a b: a holds at least as much of every token as b; total, also with negatives leq : Value -> Value -> Bool withoutLovelace : Value -> Value restrictedTo : List PolicyId -> Value -> Value positive : Value -> Value -- in a mint: what is minted negative : Value -> Value -- in a mint: what is burned, still negative ``` **Interval** (`import Interval`; the ledger's time range, as in `tx.validRange`; POSIX milliseconds; name the type with `import Interval exposing (Interval)`; build ranges by hand with `Interval`, `IntervalBound`, `Finite`, `NegInf`, `PosInf` from `Julc.Ledger`; about 3M CPU per check): ```text isEntirelyAfter : Int -> Interval -> Bool -- the range has a finite start and every time in it is > t isEntirelyBefore : Int -> Interval -> Bool -- the range has a finite end and every time in it is < t contains : Int -> Interval -> Bool -- honors inclusive and exclusive bounds start : Interval -> Maybe Int -- Nothing for an infinite start; ignores inclusiveness end : Interval -> Maybe Int after : Int -> Interval -- [t, +inf) before : Int -> Interval -- (-inf, t] between : Int -> Int -> Interval -- [low, high] everything : Interval ``` "After the deadline": `Interval.isEntirelyAfter deadline tx.validRange` (strict; a range without a finite start fails, and so does an empty range's `PosInf` start, as in Aiken's stdlib v2). "Before the deadline": `Interval.isEntirelyBefore deadline tx.validRange`. `Scenario.validFrom t` is an inclusive start, so it passes `isEntirelyAfter d` only for `t > d`; `Scenario.validBefore t` is an exclusive end, so it passes `isEntirelyBefore d` for `t <= d`. To accept from the deadline itself, check `isEntirelyAfter (deadline - 1)`. **Tx** (`import Tx`; questions about the transaction; the `ScriptContext` comes last; walks the context's lists, about 0.7M CPU per signatory, 2.5M per input and 2.8M per output passed): ```text signedBy : PubKeyHash -> ScriptContext -> Bool spends : TxOutRef -> ScriptContext -> Bool -- inputs only, not reference inputs findInput : TxOutRef -> ScriptContext -> Maybe TxInInfo ownRef : ScriptContext -> Maybe TxOutRef -- spending only; Nothing for other purposes ownInput : ScriptContext -> Maybe TxInInfo -- spending only ownPolicy : ScriptContext -> Maybe PolicyId -- minting only outputsTo : Credential -> ScriptContext -> List TxOut -- payment credential only; the tx builder picks the staking part valuePaidTo : Credential -> ScriptContext -> Value -- total of outputsTo; untagged, open to double satisfaction continuingOutputs : ScriptContext -> List TxOut -- spending only: outputs to the spent output's whole address valueSpent : ScriptContext -> Value -- inputs, not reference inputs valueProduced : ScriptContext -> Value -- outputs; fee and mint are separate mintedOf : PolicyId -> ScriptContext -> Dict TokenName Int -- burns are negative; empty when nothing minted inlineDatum : TxOut -> Maybe Data -- Nothing for a datum hash or no datum datumOf : TxOut -> ScriptContext -> Maybe Data -- inline, or by hash from tx.datums ``` Decode a datum with an annotation: `orderOf out = Maybe.map Data.decode (Tx.inlineDatum out)` with `orderOf : TxOut -> Maybe Order`; a malformed datum fails the script. Check one without decoding: `Tx.inlineDatum out == Just (Data.encode expected)`. **Bytes** (`import Bytes`; builtins; the byte string is the last argument; compare with `==`, `<`, `compare`): ```text empty : Bytes fromString : String -> Bytes toString : Bytes -> String length : Bytes -> Int append : Bytes -> Bytes -> Bytes slice : Int -> Int -> Bytes -> Bytes -- start, count (not end) take : Int -> Bytes -> Bytes drop : Int -> Bytes -> Bytes at : Int -> Bytes -> Int -- fails out of range cons : Int -> Bytes -> Bytes replicate : Int -> Int -> Bytes -- count, byte fromIntBigEndian : Int -> Int -> Bytes -- width (0 = minimal), value fromIntLittleEndian : Int -> Int -> Bytes toIntBigEndian : Bytes -> Int toIntLittleEndian : Bytes -> Int and : Bytes -> Bytes -> Bytes -- and/or/xor truncate to the shorter or : Bytes -> Bytes -> Bytes xor : Bytes -> Bytes -> Bytes complement : Bytes -> Bytes shift : Int -> Bytes -> Bytes rotate : Int -> Bytes -> Bytes testBit : Int -> Bytes -> Bool -- bit 0 = lowest bit of the last byte countSetBits : Bytes -> Int findFirstSetBit : Bytes -> Int -- -1 when none blake2b224 : Bytes -> Bytes blake2b256 : Bytes -> Bytes sha256 : Bytes -> Bytes sha3_256 : Bytes -> Bytes keccak256 : Bytes -> Bytes ripemd160 : Bytes -> Bytes ``` **Debug** (imported by default): `trace : String -> a -> a`. Both arguments are evaluated first, so the message is not emitted if the value fails. Traces cost budget and bytes. **Expect** (tests only; `import Expect exposing (Expectation)`): ```text pass : Expectation fail : String -> Expectation onFail : String -> Expectation -> Expectation equal : a -> a -> Expectation -- expected, actual notEqual : a -> a -> Expectation atMost : comparable -> comparable -> Expectation -- bound, actual: actual <= bound atLeast : comparable -> comparable -> Expectation -- actual >= bound lessThan : comparable -> comparable -> Expectation -- actual < bound greaterThan : comparable -> comparable -> Expectation -- actual > bound every : List Expectation -> Expectation ``` **Scenario** (tests only; `import Scenario exposing (Scenario)`; the `String` is the validator name from `elci.toml`): ```text spend : String -> TxOutRef -> Data -> Scenario -- spent ref, inline datum spendWithoutDatum : String -> TxOutRef -> Scenario mint : String -> Scenario withdraw : String -> Scenario certify : String -> Int -> TxCert -> Scenario vote : String -> Voter -> Scenario propose : String -> Int -> ProposalProcedure -> Scenario redeemer : Data -> Scenario -> Scenario -- default: the Data of () parameters : List Data -> Scenario -> Scenario -- ALL parameters, in declared order signedBy : PubKeyHash -> Scenario -> Scenario input : TxOutRef -> Output -> Scenario -> Scenario -- at the spent ref: replaces it; same datum as spend referenceInput : TxOutRef -> Output -> Scenario -> Scenario output : Output -> Scenario -> Scenario minting : Bytes -> Int -> Scenario -> Scenario -- token name, quantity, own policy fee : Int -> Scenario -> Scenario validFrom : Int -> Scenario -> Scenario -- inclusive validBefore : Int -> Scenario -> Scenario -- exclusive worth : Int -> Scenario -> Scenario -- lovelace of the spent output expectFailure : Scenario -> Scenario withinBudget : Int -> Int -> Scenario -> Scenario -- cpu, memory toScript : Int -> Output -- the validator's own address toStakedScript : StakingCredential -> Int -> Output -- own payment credential with a staking part toAddress : Address -> Int -> Output toKey : PubKeyHash -> Int -> Output -- the key's address, no staking part withDatum : Data -> Output -> Output -- inline datum withDatumHash : Data -> Output -> Output -- datum by hash, with the datum as a witness withToken : Policy -> Bytes -> Int -> Output -> Output key : String -> PubKeyHash -- deterministic test key outRef : String -> Int -> TxOutRef unit : Data ``` **Property** (tests only; `import Property exposing (Property)`): `holds : Bool -> Property`, `expect : Expectation -> Property`, `scenario : Scenario -> Property`. **Test discovery:** a test is an **exposed, zero-argument, top-level** declaration of type `Bool`, `Expectation`, `Scenario` or `Property`. An **exposed function returning `Property`** is a property test; its arguments (any Data-encodable types, and `Value`, generated canonical) are generated, run 100 times by default, and shrunk on failure. Unexposed declarations are helpers. Test modules live in `tests/`, may import `src` modules and `[java-modules]`, and must not share a name with a source module. **Only test modules may import `Expect`, `Scenario` and `Property`.** ## 9. Java libraries (Julc stdlib) Java libraries fill gaps in the core library; prefer a core function when one exists (`max`, `abs`, `^`, `List.member`). Map a class, then import the module. Nothing is mapped by default. Use explicit exposing lists (`exposing (..)` is rejected for Java modules). Java argument order: the collection or subject comes **first**. ```toml [java-modules] "Julc.Math" = "org.julclang.stdlib.lib.MathLib" "Julc.Output" = "org.julclang.stdlib.lib.OutputLib" ``` | Function | Type | | --- | --- | | `Math.floorDiv`, `floorMod` | `Int -> Int -> Int` (round towards negative infinity; `//` truncates) | | `Math.expMod` | `Int -> Int -> Int -> Int` | | `Math.sign` | `Int -> Int` | | `Output.outputsAt`, `lovelacePaidTo` (`OutputLib`) | `List TxOut -> Address -> List TxOut` / `Int`: whole address, staking part included | | `Address.isPubKeyAddress`, `isScriptAddress` (`AddressLib`) | `Address -> Bool` | | `Crypto.verifyEd25519Signature` (`CryptoLib`) | `Bytes -> Bytes -> Bytes -> Bool` (key, message, signature) | | `ByteString.toHex`, `intToDecimalString` (`ByteStringLib`) | `Bytes -> Bytes`, `Int -> Bytes` | Use the core `Tx` and `Interval` modules instead of `ContextsLib` and `IntervalLib`. `IntervalLib`'s `finiteLowerBound` and `finiteUpperBound` return `-1` for an infinite bound, a sentinel that makes "no end" look like "ends at -1". `Interval` and `Tx` are core module names: a `[java-modules]` key can't be either, and a Java module should not be aliased as either. Unsupported signatures (for example `MathLib.divMod`, all of `BlsLib`) are `ELCI2105`, and so is every export whose result can hold a `Value` (`ValuesLib.add`, `ValuesLib.singleton`), except in `ContextsLib` and `OutputLib`, which pass along the ledger's own values: use the core `Value` module (section 8), not `ValuesLib`. Generic Java methods are specialized per use; type arguments must be Data-encodable and must not contain a `Dict` or a `Value` (`ELCI3003`). ## 10. The CLI loop Run from the project directory (or pass it as the first argument). Exit status is `0` on success and `1` on any error, failing test, failed evaluation or when no tests are found. 1. `elci check`: compile every validator (or the entrypoint); prints one line each: `validator badge (mint): checked; 299 FLAT bytes; plutus-v3-pv11-uplc-1.1.0`. Fix every error first. 2. `elci test`: run tests; prints `PASS`/`FAIL Module.test (cpu N, mem M)` then `N passed, M failed (seed S)`. Options: `--match ` (case-insensitive on `Module.test`), `--seed ` (reproduce), `--fuzz ` (property runs, default 100). 3. `elci eval` (function projects only): prints the value in Elci syntax, then `cpu N, mem M`, then `trace:` lines; `--builtins` lists the last 20 builtin calls; failure prints `evaluation failed: `. 4. `elci build`: writes `build/validators//{validator.flat, validator.plutus, validator.plutus.json, abi.json}`; `--validator ` selects. 5. `elci apply --validator --param name=`: applies parameters, checking them off-chain. JSON is cardano-cli detailed schema: `{"int": 5}`, `{"bytes": "hex"}`, `{"list": [...]}`, `{"map": [{"k": ..., "v": ...}]}`, `{"constructor": 0, "fields": [...]}`; `@file` reads a file. A `Dict` parameter must list its keys strictly ascending (integers numerically, bytes lexicographically). Error format: the first line is `path:line:col: ELCIxxxx: message`. Then come a blank line, the source line as `N| code`, a caret line under the reported token or expression, and zero or more `Hint: ...` lines. For a validator, a blank line and `In .../elci.toml:: [validators.]` follow. The report ends with the command that explains the code: ```text src/Main.elm:6:5: ELCI2005: Unknown name 'lenght' 6| lenght [ 1, 2 ] ^^^^^^ Hint: did you mean `List.length`? For more on ELCI2005, run `elci explain ELCI2005`. ``` Parse the first line, whose form never changes, and apply the hints: they name a close name, a conversion (`String.fromInt`, `Bytes.fromString`, `.hash`, `Maybe.withDefault`, `Value.lovelaceOf`) or an import to add. A run reports every error it finds, in source order and separated by blank lines, up to 20, then `... and N more` and a count such as `3 errors`. Each top-level declaration reports its first error. Type errors appear only once there are no syntax, import or qualified-name errors, so rerun after each round of fixes. A long type mismatch adds `It is:` and `It needs:` lines with the first difference marked. `elci explain ELCI2002` explains a code with an example and its fix. Configuration and command-line errors start with `elci: ELCI4001:`. For tools, `--report=json` on `check`, `build`, `eval` and `test` prints each error on stderr as one JSON object on its own line (JSON Lines), with `code`, `path`, `line`, `column`, `message`, `hints` (an array), `excerpt` and `context` (the validator's `elci.toml` line, or null); prefer it to parsing text. Results and test output stay text. ## 11. Error codes: cause and fix | Code | Typical message | Fix | | --- | --- | --- | | `ELCI1001` | `Float is unsupported; ...`, `Unsupported character '%'` | scaled `Int`; `modBy` | | `ELCI1002` | `Function parameters require irrefutable patterns; use case ...`, `Comparison operators cannot be chained`, `Mixing (<\|) and (\|>) needs parentheses ...` | `case`; `&&`; parentheses | | `ELCI2001` | `Float division is unsupported; use // for integer quotient`, `The constructor Just takes 1 argument, but this pattern gives it 2`, `Unknown constructor Jsut in this pattern` | `//`; fix the pattern | | `ELCI2002` | `` The 1st argument to `String.fromInt` is `String`, but `String.fromInt` needs `Int` ``, `` The annotation of `identity` says `a` can be any type, but its definition needs `Int` ``, `` The 2nd branch of this case is `String`, but the first branch is `Int` ``, `The operator + works on Int values, not String; use ++ to append` | fix the part the message names; apply the hint | | `ELCI2003` | `The operator == cannot compare Map Int Int values: it has no equality`, `The operator < needs comparable values, but Bool, in ( Int, Bool ), is not comparable. ...`, `` The operator > compares values of type `a`, but the annotation of `larger` says `a` can be any type. Rename it `comparable` ... ``, `The operator ++ appends String or List values, not Int` | compare lookups or keys; `List.sortBy`; rename to `comparable`/`appendable` | | `ELCI2004` | `Top-level declarations require a type signature` | add the signature | | `ELCI2005` | `Unknown name 'lenght'` with ``Hint: did you mean `List.length`?``, `Unknown name 'double'` with ``Hint: write `Helper.double`, or expose `double` in its import``, `Unknown name 'Math.maximum'; check that the imported module exports it` | spelling; the import; section 3.3 | | `ELCI2006` | `Unreachable pattern`, `Duplicate pattern variable x` | remove the branch; rename | | `ELCI2007` | `` This case does not cover every possibility; it is missing `Nothing` and `Just (Err _)` `` | add a branch for each listed pattern | | `ELCI2008` | `Cyclic eager values are unsupported; recursion requires functions` | break the cycle | | `ELCI2009` | `Entrypoints require concrete types`, `Handler Main.mint requires a concrete type` | no type variables in handlers | | `ELCI2010` | `Missing field 'price'; available fields: [amount]` | correct field; annotate the record type | | `ELCI2011` | `Duplicate constructor Buy: the type Action has one too`, `Nothing is a constructor of the built-in Maybe type; choose another name`, `Bytes is a built-in type; choose another name` | rename | | `ELCI2012` | `The type List takes 1 argument, but it is given 2`, `Type is not declared or imported in this module: Amount`, `Write tuple types as ( a, b ) or ( a, b, c )` | declare/import the type | | `ELCI2013` | `Functions cannot be serialized as datum or redeemer`, `Arrays have no Data encoding ...`, `A Value cannot be part of a datum, redeemer or parameter yet: ...` | Data-encodable boundary types; `List` in data; `List ( PolicyId, TokenName, Int )` for a value | | `ELCI2014` | `Handler Main.spend does not have the spend signature datum -> redeemer -> ScriptContext -> Bool that [validators.main] needs (0 parameters); its type is ...` | section 6 | | `ELCI2101` | `Missing source module: X; add X.elm under a source directory, or map a Java library to it under [java-modules]`, `Expect is part of the test library; only test modules can import it` | add module / mapping; move to `tests/` | | `ELCI2102` | `max is ambiguous: it could be Basics.max or Utils.max. Write the one you mean, like Basics.max, or stop exposing the other` | qualify, or expose the name explicitly | | `ELCI2103` | `Handler Main.mint must be exposed by its module`, `Entrypoint Main.main must be exposed by its module` | expose it | | `ELCI2104` | ``Bytes.fromString needs the Bytes module; add `import Bytes` ``, `Module Helper does not expose 'doubel'`, ``Module Helper is not imported; add `import Helper` ``, `No module Lsit is imported, and none has that name`, `Elci's List module has no find. It has all, any, ...`, `Elci has no Debug.log; use Debug.trace message value, which returns value`, `Map is imported as M; write M.get`, `The module name Tx is taken by Elci's core library; rename this module`, `Import cycle: Main -> Pricing -> Main`, `Java modules cannot be imported with exposing (..); ...` | add the import; section 3.3; rename; break cycles | | `ELCI2105` | `...MathLib.divMod: Unsupported on-chain source type: Tuple2`, `...ValuesLib.add: returns a Value, which Java libraries do not keep in the ledger's canonical order; use the core Value module` | use another export; the core `Value` module | | `ELCI3001` | `Cannot determine the type converted by Julc.Data.decode; add a type annotation` | annotate (`let xs : List Int`) | | `ELCI3002` | specialization limit (256) | remove polymorphic recursion | | `ELCI3003` | `No encoder for Int -> Int: functions have no Data encoding`, `Type argument T = Unit of firstOr must have a Plutus Data encoding`, `No strict decoder for org.julclang.ledger.TxOut ...`, `No encoder for List [org.julclang.ledger.Value] ...`, `Type argument a = Dict Int Int of filter contains a Dict, which a Java library cannot keep sorted; ...`, `Type argument a = Value of filter contains a Value, which a Java library does not keep in the ledger's canonical order; ...` | Data-encodable type; convert values with `Value.toList`; no `Dict` or `Value` through Java generics | | `ELCI4001` | `unknown validator 'x'`, `src/Main.elm is not valid UTF-8 text; save it as UTF-8`, `eval runs an [entrypoint] function; test validators with elci test`, `ABI drift from ...`, `parameter caps has a Dict whose keys are not strictly ascending; write its keys sorted and without repeats`, `Julc version does not match this compiler; expected julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT"`, `Unknown configuration key: [project] name`, `Unknown option: --frob`, `my-project/elci.toml not found; ...`, `Source directory not found: ...`, `my-project/elci.toml:6: [validators.main]: Handler Main.spent for spend: module Main does not declare spent` | fix config or command; sort `Dict` keys | | `ELCI4002`, `ELCI4003` | backend rejected or lacks a capability | compiler issue; report it | | (none) | `On-chain Java source not found: ...`, `Cannot resolve type: ?`, `Unknown type: T` (a Java library) | fix `[java-modules]` or the library | A failing scenario that was never run prints `cannot prepare validator 'badge': it takes 1 parameter [admin], but the scenario gives 0; set them with Scenario.parameters`. ## 12. Anti-patterns agents get wrong 1. **Using Elm APIs Elci lacks** (`Set`, `Debug.log`, `String.length`, `String.toInt`, `List.map4`). Use section 3.3. The core functions in section 8 do exist; use them rather than writing your own or mapping `Julc.Math`. 2. **Forgetting imports.** `Dict`, `Map`, `Array`, `Value`, `Interval`, `Tx`, `Bytes`, `Julc.Data` must be imported; `Basics`, `List`, `Maybe`, `Result`, `String`, `Tuple` and `Debug` are implicit. `Julc.*` Java modules also need a `[java-modules]` mapping. 3. **Omitting type signatures** on top-level declarations (`ELCI2004`). 4. **Wrong handler shape.** Parameters first, then datum (spend only), redeemer, `ScriptContext`, returning `Bool`. Use `()` for unused datum/redeemer. Expose the handler. 5. **Mixing `Bytes` and `PubKeyHash`.** Keep keys as `PubKeyHash`; use `.hash` only when bytes are needed. 6. **Expensive checks in `let`** or before cheap ones: they always run. Write `cheap && expensive` inline. 7. **Expensive top-level values** in multi-purpose scripts: every purpose pays. Use functions. 8. **Assuming record field order** in Data: records encode fields **sorted by name**. 9. **Assuming `Maybe` = Java `Optional`** tags: convert with `Data.fromOptional` / `Data.toOptional`. 10. **Julc's `IntervalLib` for deadlines**, whose `finiteLowerBound`/`finiteUpperBound` return `-1` for an infinite bound. Use `Interval.isEntirelyAfter`/`isEntirelyBefore`, and mind that they are strict. 11. **Storing `Array` in data**, or building big maps with `Map.fromList` (quadratic; a 100-entry map exceeds a transaction's memory budget). Keep lists in data, convert with `Array.fromList` once; build keyed data with `Dict.fromList` (O(n log n)). 12. **`Data.decode` without a known type** (`ELCI3001`). Annotate the binding. 13. **Missing `Scenario.parameters`**: a scenario must pass every validator parameter, in order. 14. **Importing `Expect`/`Scenario`/`Property` from `src`** (`ELCI2101`). They are for `tests/` only. 15. **`x -1` meaning subtraction.** It is an application; write `x - 1`. 16. **Float, `/`, `%`**: integers only; `//`, `modBy`, `remainderBy`, `^` (non-negative exponents). 17. **`exposing (..)` on Java modules** or relying on Java overloads (rejected). 18. **Trusting parameters on-chain**: they are not validated; validate with `elci apply` before deployment. 19. **Very large integer literals with elm-format**: elm-format 0.8.8 corrupts literals beyond 64 bits. 20. **Plain type variables with `<` or `++`.** Name them `comparable` or `appendable` (`ELCI2003`). 21. **Sorting records with `List.sort`.** Records and custom types are not comparable: use `List.sortBy (\r -> r.key)` or `List.sortWith`. 22. **Reading a PlutusTx `Either` as `Result`.** `Ok` is constructor 0 and `Err` 1, the reverse of `Left`/`Right`. 23. **Expensive `Maybe.withDefault` defaults.** The default is always evaluated; use `case` instead. 24. **`Map` for your own keyed data.** Use `Dict` when keys are comparable: it is sorted, has `==` and builds in O(n log n). Keep `Map` for ledger maps (`txInfo.withdrawals`, `txInfo.redeemers`) and keys that are not comparable. 25. **Unsorted `Dict` data.** A `Dict` in a datum, redeemer or parameter must have strictly ascending keys, or it fails to decode. Build test data with `Data.encode (Dict.fromList ...)`; off-chain, sort keys in Elci's order, not CBOR's canonical order (which puts shorter byte strings first). 26. **Reading values through `ValuesLib`.** Use the core `Value` module; Java exports that return a `Value` are rejected (`ELCI2105`). `==` on values is exact, so use `Value.geq` for "pays at least". `Value.lovelaceOf tx.mint` is 0, and a burn is a negative quantity in `tx.mint`. `import Julc.Ledger exposing (Value(..))` is `ELCI2104`: values come from the `Value` module. 27. **Hand-written context walks.** `List.any (\s -> s == key) tx.signatories`, a `case` on `scriptInfo` to find the own input or policy, or a filter over `tx.outputs` by credential: use `Tx.signedBy`, `Tx.ownInput`, `Tx.ownPolicy` and `Tx.outputsTo`. 28. **Untagged payment checks.** `Value.lovelaceOf (Tx.valuePaidTo (PubKeyCredential seller) ctx) >= price` lets one output pay for two script inputs (double satisfaction). See section 13. 29. **`token.name` on a `TokenName`.** Its bytes are `token.hash`; there is no `.name` field. ## 13. Security checklist for validators The escrow, marketplace and order-swap example projects apply the items that concern spend validators and test each attack (`examples/escrow`, `examples/marketplace`, `examples/order-swap`; their sources are in `/ai/examples.json`). 1. **Tag every payment against double satisfaction.** When two inputs of one script can each require a payment to the same party, one output satisfies both checks. Require the paying output to carry the spent output's reference as its inline datum: ```elm case Tx.ownRef ctx of Just ref -> List.any (\output -> (Value.lovelaceOf output.value >= price) && (Tx.inlineDatum output == Just (Data.encode ref)) ) (Tx.outputsTo (PubKeyCredential seller) ctx) Nothing -> False ``` Never accept an untagged `Tx.valuePaidTo` total. Write a scenario that pays with another input's tag (`Scenario.withDatum (Data.encode (Scenario.outRef "listing" 1))`) and expects failure. 2. **One script input per transaction when outputs are matched by address.** An output returned to the script carries the script's datum, not a tag, so one returned output could stand for two inputs. Count the inputs whose `resolved.address.credential` is the own input's and require exactly one. 3. **Check the whole value that stays.** For an output back to the script, compare its whole value with `Value.geq output.value expected`, where `expected` is everything that must stay (for example `Value.subtract own.resolved.value taken`), not just one token: otherwise the spender keeps the lovelace and the other tokens. To stop padding with junk tokens as well, require `Value.withoutLovelace output.value == Value.withoutLovelace expected` and `Value.lovelaceOf output.value >= Value.lovelaceOf expected`. 4. **Use `Value.geq` or `>=` for "pays at least", never `==`,** which rejects a payment with extra lovelace or tokens. Use `==` when exactness is the rule, as for `Tx.mintedOf policy ctx` in a minting policy. 5. **Check signatures with `Tx.signedBy` for every privileged action** (cancel, refund, update, withdraw), and write a scenario where a stranger signs. 6. **Bound numbers taken from datums and parameters.** Parameters are not validated on-chain: check a fee in basis points is between 0 and 10000, and amounts are positive (`amount > 0`), before using them. 7. **Deadlines use `Interval`.** `isEntirelyAfter`/`isEntirelyBefore` fail for a transaction without a validity range. They are strict; test the boundary with a property over offsets around the deadline. 8. **Payees are matched by payment credential.** `Tx.outputsTo` ignores the staking part: whoever builds the transaction chooses the payee's staking part. To protect a payee's staking rights, store their full `Address` and compare `output.address`. 9. **Minting policies check the whole mint under their policy:** `Dict.toList (Tx.mintedOf policy ctx) == [ ( TokenName name, quantity ) ]` rejects extra token names. ## 14. Canonical examples All three were built and run with the CLI exactly as shown. ### 14.1 Spend validator with typed datum and redeemer, scenarios and a property ```toml title="elci.toml" [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" [validators.escrow] boundary = "julc-strict-v1" spend = "Escrow.spend" ``` ```elm title="src/Escrow.elm" module Escrow exposing (Action(..), Lock, spend) import Interval import Julc.Ledger exposing (PubKeyHash, ScriptContext) import Tx {-| The datum stored with the locked funds. -} type alias Lock = { owner : PubKeyHash , beneficiary : PubKeyHash , deadline : Int } {-| The redeemer: what the spender wants to do. -} type Action = Claim | Refund spend : Lock -> Action -> ScriptContext -> Bool spend lock action ctx = case action of Claim -> Tx.signedBy lock.beneficiary ctx && Interval.isEntirelyAfter lock.deadline ctx.txInfo.validRange Refund -> Tx.signedBy lock.owner ctx ``` ```elm title="tests/EscrowTest.elm" module EscrowTest exposing ( beneficiaryClaimsAfterTheDeadline , claimAtTheDeadlineFails , claimsOpenOnlyAfterTheDeadline , lockRoundTrips , ownerRefunds , strangerCannotRefund ) import Escrow exposing (Action(..), Lock) import Expect exposing (Expectation) import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Property exposing (Property) import Scenario exposing (Scenario) owner : PubKeyHash owner = Scenario.key "owner" beneficiary : PubKeyHash beneficiary = Scenario.key "beneficiary" lock : Lock lock = { owner = owner, beneficiary = beneficiary, deadline = 1000 } {-| Spend the locked output with a redeemer; helpers are not exposed, so they are not tests. -} spendWith : Action -> Scenario spendWith action = Scenario.spend "escrow" (Scenario.outRef "escrow" 0) (Data.encode lock) |> Scenario.redeemer (Data.encode action) beneficiaryClaimsAfterTheDeadline : Scenario beneficiaryClaimsAfterTheDeadline = spendWith Claim |> Scenario.validFrom 1001 |> Scenario.signedBy beneficiary {-| `validFrom` is inclusive: a transaction valid from 1000 could run at the deadline. -} claimAtTheDeadlineFails : Scenario claimAtTheDeadlineFails = spendWith Claim |> Scenario.validFrom 1000 |> Scenario.signedBy beneficiary |> Scenario.expectFailure ownerRefunds : Scenario ownerRefunds = spendWith Refund |> Scenario.signedBy owner strangerCannotRefund : Scenario strangerCannotRefund = spendWith Refund |> Scenario.signedBy (Scenario.key "stranger") |> Scenario.expectFailure lockRoundTrips : Expectation lockRoundTrips = Expect.equal lock (Data.decode (Data.encode lock)) {-| A signed claim succeeds exactly when its validity range starts after the deadline. The generated offset is relative to the deadline, so the boundary is tested early and often. -} claimsOpenOnlyAfterTheDeadline : Int -> Property claimsOpenOnlyAfterTheDeadline offset = let start = lock.deadline + offset claim = spendWith Claim |> Scenario.validFrom start |> Scenario.signedBy beneficiary in if start > lock.deadline then Property.scenario claim else Property.scenario (Scenario.expectFailure claim) ``` ```text $ elci check validator escrow (spend): checked; 599 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 $ elci test --seed 42 PASS EscrowTest.beneficiaryClaimsAfterTheDeadline (cpu 17930587, mem 67036) PASS EscrowTest.claimAtTheDeadlineFails (cpu 17914587, mem 66936) PASS EscrowTest.ownerRefunds (cpu 13130425, mem 52579) PASS EscrowTest.strangerCannotRefund (cpu 13508858, mem 54511) PASS EscrowTest.lockRoundTrips (cpu 8484615, mem 30707) PASS EscrowTest.claimsOpenOnlyAfterTheDeadline (100 runs, max cpu 17930587, mem 67036) 6 passed, 0 failed (seed 42) ``` Changing `Interval.isEntirelyAfter lock.deadline` to `Interval.isEntirelyAfter (lock.deadline - 1)` in `Escrow.spend` makes the property fail on its first run with the counterexample `claimsOpenOnlyAfterTheDeadline 0`. ### 14.2 Parameterized minting policy ```toml title="elci.toml" [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" [validators.badge] boundary = "julc-strict-v1" parameters = ["admin"] mint = "Badge.mint" ``` ```elm title="src/Badge.elm" module Badge exposing (badgeName, mint) import Bytes import Dict import Julc.Ledger exposing (PubKeyHash, ScriptContext, TokenName(..)) import Tx badgeName : Bytes badgeName = Bytes.fromString "BADGE" {-| The admin (a deployment parameter) may mint or burn BADGE tokens. The redeemer is the quantity, which must be exactly what the transaction mints under this policy. -} mint : PubKeyHash -> Int -> ScriptContext -> Bool mint admin quantity ctx = case Tx.ownPolicy ctx of Just policy -> (quantity /= 0) && Tx.signedBy admin ctx && (Dict.toList (Tx.mintedOf policy ctx) == [ ( TokenName badgeName, quantity ) ]) Nothing -> False ``` ```elm title="tests/BadgeTest.elm" module BadgeTest exposing (adminBurns, adminMints, onlyTheDeclaredQuantity, otherTokensAreRejected, strangerCannotMint, zeroIsRejected) import Badge import Bytes import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Property exposing (Property) import Scenario exposing (Scenario) admin : PubKeyHash admin = Scenario.key "admin" {-| Every scenario must supply every parameter of the validator. -} mintAs : PubKeyHash -> Int -> Int -> Scenario mintAs signer declared actual = Scenario.mint "badge" |> Scenario.parameters [ Data.encode admin ] |> Scenario.redeemer (Data.encode declared) |> Scenario.minting Badge.badgeName actual |> Scenario.signedBy signer adminMints : Scenario adminMints = mintAs admin 5 5 adminBurns : Scenario adminBurns = mintAs admin -2 -2 strangerCannotMint : Scenario strangerCannotMint = mintAs (Scenario.key "stranger") 5 5 |> Scenario.expectFailure zeroIsRejected : Scenario zeroIsRejected = mintAs admin 0 0 |> Scenario.expectFailure {-| Nothing but BADGE may be minted under the policy. -} otherTokensAreRejected : Scenario otherTokensAreRejected = mintAs admin 5 5 |> Scenario.minting (Bytes.fromString "OTHER") 1 |> Scenario.expectFailure {-| The admin's transaction succeeds only when the redeemer equals the minted quantity. -} onlyTheDeclaredQuantity : Int -> Int -> Property onlyTheDeclaredQuantity declared actual = let transaction = mintAs admin declared actual in if declared == actual && declared /= 0 then Property.scenario transaction else Property.scenario (Scenario.expectFailure transaction) ``` ```text $ elci check validator badge (mint): checked; 531 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 $ elci test --seed 42 PASS BadgeTest.adminMints (cpu 16338349, mem 56630) PASS BadgeTest.adminBurns (cpu 16338349, mem 56630) PASS BadgeTest.strangerCannotMint (cpu 9466389, mem 36712) PASS BadgeTest.zeroIsRejected (cpu 6425629, mem 25735) PASS BadgeTest.otherTokensAreRejected (cpu 17971962, mem 62350) PASS BadgeTest.onlyTheDeclaredQuantity (100 runs, max cpu 16338349, mem 56630) 6 passed, 0 failed (seed 42) $ elci apply --validator badge --param 'admin={"bytes": "a2c20c77887ace1cd986193e4e75babd8993cfd56995cd5cfce609c2"}' validator badge: applied 1 parameter; hash 8769d0504d637d83ffdc196eb33ab8146710d182338b0608f970c3cb; wrote .../badge/build/validators/badge/applied.plutus.json ``` ### 14.3 Function project for `elci eval` `elci init pricing --purpose function` creates the `[entrypoint]` configuration from section 2. ```elm title="src/Main.elm" module Main exposing (main) import Dict exposing (Dict) type alias Purchase = { item : String , quantity : Int } prices : Dict String Int prices = Dict.fromList [ ( "apple", 3 ), ( "pear", 5 ) ] lineTotal : Purchase -> Maybe Int lineTotal purchase = Maybe.map (\price -> price * purchase.quantity) (Dict.get purchase.item prices) total : List Purchase -> Int total purchases = List.foldl (\purchase sum -> case lineTotal purchase of Just amount -> sum + amount Nothing -> Debug.trace ("no price for " ++ purchase.item) sum ) 0 purchases main : ( Int, Maybe Int ) main = ( total [ { item = "apple", quantity = 2 }, { item = "pear", quantity = 1 }, { item = "kiwi", quantity = 9 } ] , lineTotal { item = "kiwi", quantity = 1 } ) ``` ```text $ elci eval ( 11, Nothing ) cpu 43542823, mem 172473 trace: no price for kiwi ``` ## 15. Checklist before generating Elci code 1. Is every top-level declaration annotated, and every module name equal to its file path? 2. Did I avoid every Elm API in section 3.3 and import `Dict`/`Map`/`Array`/`Value`/`Interval`/`Tx`/`Bytes`/`Julc.Data` where used? 3. Does each handler match section 6, with parameters first, and is it exposed and listed in `elci.toml`? 4. Are datum, redeemer and parameter types Data-encodable (no functions, `Array`, `Value`, `TxOut`)? 5. Do I compare `PubKeyHash` with `PubKeyHash` (or `.hash` with `Bytes`), never mixed? 6. Are cheap checks first and inline, and expensive work kept out of `let` and top-level values? 7. Is every `Julc.*` module I import mapped under `[java-modules]`, with explicit exposing only? 8. Do my scenarios pass all parameters, and do they cover success, each failure path and a property? 9. Did I use `Tx` and `Interval` rather than hand-written context walks, `ContextsLib` or `IntervalLib`? 10. Does the validator pass the security checklist (section 13), with a scenario for each attack? 11. Did I run `elci check`, then `elci test`, and fix every error before `elci build`? More depth: [language reference](https://elci-lang.dev/reference/language/), [core modules](https://elci-lang.dev/reference/core-modules/), [test library](https://elci-lang.dev/reference/test-library/), [diagnostics](https://elci-lang.dev/reference/diagnostics/), [elci.toml](https://elci-lang.dev/reference/configuration/), [CLI](https://elci-lang.dev/reference/cli/). --- # Agent instructions > A compact ruleset for AI coding agents working in an Elci project. Save it as AGENTS.md, CLAUDE.md or a Cursor rule. Source: https://elci-lang.dev/ai/agent-instructions/ Rules for AI coding agents working in an Elci project. Save this page as `AGENTS.md`, `CLAUDE.md` or `.cursor/rules/elci.mdc` at the project root. For depth, read `/ai/starter-pack.md` and `/llms.txt` on the Elci documentation site before writing code. ## What Elci is Elci compiles an Elm 0.19 syntax subset (`.elm` files) to Plutus V3 validators for Cardano. It is not browser Elm: there is no `Html`, `Cmd`, `port`, `elm.json`, Elm package, `Float`, `Char` or `Set`. Elm's core modules `Basics`, `List`, `Maybe`, `Result`, `Tuple`, `String` and `Dict` exist with Elm's names and argument orders, but `String` has only a few functions. A `Value` module handles the ledger's multi-asset values, and the `Tx` and `Interval` modules answer questions about the transaction and its validity range. Evaluation is strict. ## Must-follow rules 1. Give every top-level declaration a type signature. Module `A.B` lives at `src/A/B.elm`. 2. Elm's default imports apply: `Basics exposing (..)`, `List`, `Maybe exposing (Maybe(..))`, `Result exposing (Result(..))`, `String`, `Tuple`, `Debug`. Import `Dict exposing (Dict)`, `Map`, `Array`, `Value exposing (Value)`, `Interval`, `Tx`, `Bytes` and `Julc.Data as Data` when you use them. `Julc.Ledger` provides ledger types; expose constructors with `Type(..)`. Never name a module or alias `Basics`, `List`, `Maybe`, `Result`, `String`, `Tuple`, `Map`, `Dict`, `Array`, `Set`, `Value`, `Interval`, `Tx` or `Elci`, a constructor `Just`, `Nothing`, `True` or `False`, or a type `Int`, `Bool`, `String`, `Bytes`, `Data`, `List`, `Maybe`, `Dict`, `Map`, `Array` or `Float`. 3. Use only the core functions that exist (see `/ai/catalog.json`). Prelude: `min`, `max`, `abs`, `negate`, `clamp`, `compare` (`LT`/`EQ`/`GT`), `identity`, `always`, `not`, `xor`, `modBy`, `remainderBy`, `^`. `Maybe`: `withDefault`, `map`, `map2`, `map3`, `andThen`. `Result`: `map`, `map2`, `map3`, `andThen`, `withDefault`, `toMaybe`, `fromMaybe`, `mapError`. `String`: only `fromInt`, `isEmpty`, `append`, `concat`, `join`. `List` has all of Elm's except `map4`/`map5`, and `Dict` all of Elm's. Replacements: `Set` → `Dict k Bool`; `Debug.log` → `Debug.trace`; `String.length s` → `Bytes.length (Bytes.fromString s)`. 4. No `Float`, `/`, `%`, `number`, char literals or extensible records. Use `Int`, `//`, `modBy` and `remainderBy`. Operators work as functions: `(+)`, `(::)`. Accessor functions work as in Elm: `List.map .amount payouts`. Write qualified names without spaces. Tuples have 2 or 3 elements. 5. `<`, `>`, `<=`, `>=`, `compare`, `max` and `List.sort` work on comparable types: `Int`, `String`, `Bytes`, ledger hashes such as `PubKeyHash`, and lists and tuples of these. Sort records with `List.sortBy`. A signature variable used with `<` must be named `comparable`, with `++` `appendable`. `==` does not work on functions, `Map`, `Array` or `TxOut`; it works on `Dict`, `Value` and `Data` (exact structure: `Tx.inlineDatum out == Just (Data.encode expected)`). Arguments are strict: `Maybe.withDefault` evaluates its default. 6. Negation is Elm's: `f -1` passes `-1`; write `x - 1` to subtract. 7. Configure validators in `elci.toml`, never in source: `[validators.]` with `boundary = "julc-strict-v1"`, handler keys `spend`/`mint`/`withdraw`/`certify`/`vote`/`propose` = `"Module.function"`, optional `parameters = [...]` and `spend-datum = "required" | "optional" | "absent"`. Keep `target = "plutus-v3-pv11-uplc-1.1.0"` and the `julc-version` the compiler prints. 8. Handler types: parameters first, then the datum (spend only; `Maybe datum` when optional), the redeemer, and `ScriptContext`, returning `Bool`. Expose every handler. Use `()` for unused values. 9. Datum, redeemer and parameter types must be Data-encodable: records, custom types, `Int`, `Bool`, `String`, `Bytes`, `()`, tuples, lists, `Dict`, `Map`, `Maybe`, `Result` and ledger types such as `PubKeyHash` and `TxOutRef`. Never functions, `Array`, `Value`, `TxOut` or `ScriptContext`; for an amount of tokens store `List ( PolicyId, TokenName, Int )` and call `Value.fromList`. 10. Records encode with fields sorted by name; custom types by declaration order; `Maybe` is `Nothing` 0, `Just` 1; `Result` is `Ok` 0, `Err` 1 (the reverse of PlutusTx `Either`). Julc optionals differ: convert with `Data.fromOptional`/`Data.toOptional`. A `Dict` is a Plutus map with strictly ascending keys: unsorted or repeated keys fail to decode, so build test data with `Data.encode (Dict.fromList ...)` and give `elci apply` sorted keys. 11. Use `Dict` for data you build or look up by a comparable key, including datums. Use `Map` for ledger maps (`txInfo.withdrawals`, `txInfo.redeemers`) and keys that are not comparable. Both look up in O(n); `Map.fromList` is O(n²), `Dict.fromList` O(n log n). 12. Keep `PubKeyHash` and `Bytes` apart; use `.hash` when raw bytes are needed, including `token.hash` for a `TokenName` (it has no `.name`). Read the transaction with the core `Tx` module: `Tx.signedBy key ctx`, `Tx.ownRef`, `Tx.ownInput`, `Tx.ownPolicy`, `Tx.outputsTo`, `Tx.continuingOutputs`, `Tx.mintedOf`, `Tx.inlineDatum`, `Tx.datumOf`. Don't hand-write these walks. 13. Every `let` value is evaluated even if unused. Put cheap checks first, inline, in `&&` chains. Keep expensive work out of top-level values; they run for every purpose of the script. 14. Check deadlines with the core `Interval` module: `Interval.isEntirelyAfter deadline ctx.txInfo.validRange` and `Interval.isEntirelyBefore deadline ctx.txInfo.validRange`. Both are strict and need a finite bound, so a transaction without a validity range fails them. `Scenario.validFrom` is inclusive and `Scenario.validBefore` exclusive. Don't use Julc's `IntervalLib`, whose bounds use a `-1` sentinel. 15. Read and build ledger values (`out.value`, `ctx.txInfo.mint`) with the core `Value` module: `Value.lovelaceOf v`, `Value.quantityOf policy name v`, `Value.geq paid required`, `==` (exact). Make asset ids with `PolicyId bytes` and `TokenName bytes` from `Julc.Ledger exposing (PolicyId(..), TokenName(..))`. A mint holds burns as negative quantities; split it with `Value.positive` and `Value.negative`. Never read values through `Julc.Values` (`ValuesLib`): Java exports that return a `Value` are rejected (`ELCI2105`), except the ledger accessors of `ContextsLib` and `OutputLib`. 16. Java libraries need a `[java-modules]` mapping, explicit `exposing` lists, and take the subject first (`Output.outputsAt outputs address`). Never pass a type containing a `Dict` or a `Value` to a generic Java function (`ELCI3003`). 17. Annotate a `Data.decode` result when inference cannot see its type (`ELCI3001`). 18. Tests live in `tests/`. Only test modules may import `Expect`, `Scenario` and `Property`. A test is an exposed zero-argument value of type `Bool`, `Expectation`, `Scenario` or `Property`; an exposed function returning `Property` is a property test. Keep helpers unexposed. 19. Every scenario passes all validator parameters with `Scenario.parameters [ Data.encode x, ... ]`. Cover the success path, every failure path (`Scenario.expectFailure`) and at least one property that generates offsets around each boundary. Pay a key with `Scenario.toKey key lovelace`; attach an inline datum with `Scenario.withDatum` or a datum by hash with `Scenario.withDatumHash`. 20. Do not weaken tests or add `expectFailure` to make a failing test pass. Fix the validator. ## Security rules for validators The `escrow`, `marketplace` and `order-swap` example projects follow the rules for spend validators and test each attack. 1. Tag every payment a spend validator requires: the paying output carries the spent output's reference, `Tx.inlineDatum output == Just (Data.encode ref)` with `ref` from `Tx.ownRef ctx`. An untagged `Tx.valuePaidTo` check lets one output pay for two script inputs (double satisfaction). 2. When outputs back to the script are matched by address (`Tx.continuingOutputs`), allow one input of the script per transaction. 3. Check the whole value that stays at the script with `Value.geq`, not one token, so the spender can't keep the lovelace or other tokens. To stop padding with junk tokens too, compare `Value.withoutLovelace` of both values with `==` and their lovelace with `>=`. 4. Use `Value.geq` or `>=` for "pays at least"; `==` on values is exact. 5. Require `Tx.signedBy` for every privileged action, and test it with a stranger's signature. 6. Bound numbers from datums and parameters before using them: a fee in basis points between 0 and 10000, quantities above 0. Parameters are not validated on-chain. 7. `Tx.outputsTo` matches the payment credential; whoever builds the transaction chooses the payee's staking part. Store and compare the full `output.address` when the staking part matters. 8. A minting policy checks the whole mint under its policy: `Dict.toList (Tx.mintedOf policy ctx) == [ ( TokenName name, quantity ) ]`. ## Workflow Run these from the project directory after every change and fix every error before moving on: ```sh elci check # compile; prints FLAT size per validator elci test --seed 42 # run tests; reproduce with the printed seed elci test --match # rerun one test; --fuzz 1000 for deeper property runs elci build # write build/validators// artifacts ``` Use `elci eval` only in a function project (`[entrypoint]`) to evaluate helper logic. Apply parameters with `elci apply --validator --param name='{"bytes": "..."}'`, which validates them off-chain. Every command exits with status 0 on success and 1 on any error or failing test. ## Reading diagnostics A diagnostic's first line is `path:line:col: ELCIxxxx: message`, and its form never changes. Below it come the source line with a caret under the reported token, and zero or more `Hint:` lines: ```text src/Main.elm:8:5: ELCI2005: Unknown name 'double' 8| double 4 ^^^^^^ Hint: write `Helper.double`, or expose `double` in its import ``` - Fix the part the message names: the argument, branch, list element, operator side, annotation or field whose type differs, or each pattern a `case` is missing. - Apply the hint when it fits: a close name (`did you mean`), a conversion such as `String.fromInt`, `Bytes.fromString`, `.hash` or `Maybe.withDefault`, or an import to add. - A run reports every error it finds, in source order, up to 20, with a count such as `3 errors`. Each top-level declaration reports its first error, and type errors appear only once syntax, import and qualified-name errors are fixed. Fix what is reported, then rerun `elci check`. - `elci explain ` explains a code, such as `elci explain ELCI2002`, with a failing example and its fix. The last line of a failed run names the codes to look up. - To parse errors, add `--report=json` to `check`, `build`, `eval` or `test`. Each error is then one JSON object on its own line on stderr (JSON Lines), with `code`, `path`, `line`, `column`, `message`, `hints`, `excerpt` and `context`. Look the code up in the starter pack's error table: `ELCI2002` type mismatch, `ELCI2003` equality, ordering or append on a type without it, `ELCI2004` missing signature, `ELCI2005` unknown name, `ELCI2007` non-exhaustive case, `ELCI2013` type cannot cross the data boundary, `ELCI2014` handler signature does not match `elci.toml`, `ELCI2102` ambiguous name, `ELCI2101`/`ELCI2104` import problems or a function Elci lacks, `ELCI2105` a Java export Elci can't use, such as one that returns a `Value`. ## Done means - `elci check` and `elci test` both exit with status 0. - Every rule of the validator has a passing scenario, and every way to break it has an `expectFailure` scenario or a property, including each attack in the security rules that applies. - elm-format 0.8.8 `--validate` passes. elm-format corrupts integer literals beyond 64 bits, so re-check any large constant after formatting. --- # Values, functions and types > Elci's basic types, operators, functions with type signatures, lambdas, currying and generic functions, with examples you can run. Source: https://elci-lang.dev/learn/basics/ This page covers the building blocks of Elci: basic values and their types, operators, functions and their signatures, lambdas, partial application and generic functions. Every example is a complete module that you can run. ## Set up a playground A function project runs one value with `elci eval`, which makes it the quickest way to try things out: ```sh elci init playground --purpose function cd playground elci eval . ``` ```text 42 cpu 16100, mem 200 ``` The project's `elci.toml` ends with an `[entrypoint]` table, which points `elci eval` at the value `main` in module `Main`: ```toml title="elci.toml" [entrypoint] module = "Main" function = "main" purpose = "function" ``` and `src/Main.elm` is: ```elm title="src/Main.elm" module Main exposing (main) main : Int main = 42 ``` To try an example from these pages, replace `src/Main.elm` with it and run `elci eval .` again. Keep the `module Main exposing (main)` line and a `main` value, because that's what `elci.toml` names. `elci eval` compiles `main` to a Plutus script, runs it on Julc's virtual machine and prints: - the value, written in Elci syntax; - the execution budget the script used, in CPU and memory units. The ledger charges fees for these units; - a `trace:` line for each `Debug.trace` message, if there are any. Add `--builtins` to also list every Plutus builtin function that the script called. ## Basic types | Type | Examples | Notes | | --- | --- | --- | | `Int` | `42`, `-7`, `1000000` | Whole numbers of any size. There is no `Float`. | | `Bool` | `True`, `False` | | | `String` | `"GOLD"`, `"line\n"` | Text. Join with `++`, compare with `==` or `<`. The `String` module has `String.fromInt` and more. | | `Bytes` | `Bytes.fromString "GOLD"` | Raw byte strings, from `import Bytes` | | `()` | `()` | Unit: a type with exactly one value, used for "nothing to say" | Strings support the escapes `\n`, `\r`, `\t`, `\"` and `\\`. There is no `Char` type. Values and functions are both defined with `=`, each with a type signature above it. A function lists its parameters before the `=`: ```elm title="src/Main.elm" module Main exposing (main) lovelacePerAda : Int lovelacePerAda = 1000000 toLovelace : Int -> Int toLovelace ada = ada * lovelacePerAda main : Int main = toLovelace 25 + 500000 ``` ```sh elci eval . --builtins ``` ```text 25500000 cpu 480261, mem 1904 builtin: MultiplyInteger(25, 1000000) → 25000000 builtin: AddInteger(25000000, 500000) → 25500000 ``` The builtin lines show the two operations the script performed. A lovelace is the smallest unit of ada, and on-chain amounts are always whole lovelace, which is one reason Elci doesn't need `Float`. ## Numbers `Int` has arbitrary precision: `+`, `-` and `*` are exact and never overflow. ```elm title="src/Main.elm" module Main exposing (main) main : Int main = 1000000000 * 1000000000 * 1000000000 ``` ```text 1000000000000000000000000000 cpu 16100, mem 200 ``` There are three ways to divide: | Operation | Result | | --- | --- | | `a // b` | Integer division, truncating toward zero: `7 // 2` is `3` and `-7 // 2` is `-3` | | `modBy d n` | Remainder with the sign of the divisor `d`: `modBy 3 -7` is `2` | | `remainderBy d n` | Remainder with the sign of the dividend `n`: `remainderBy 3 -7` is `-1` | ```elm title="src/Main.elm" module Main exposing (main) main : ( Int, Int, Int ) main = ( -7 // 2, modBy 3 -7, remainderBy 3 -7 ) ``` ```text ( -3, 2, -1 ) cpu 1377500, mem 4659 ``` `^` raises an integer to a power, and the prelude, which every module imports, provides the other everyday functions: `negate`, `abs`, `min`, `max` and `clamp`. ```elm title="src/Main.elm" module Main exposing (main) main : List Int main = [ 2 ^ 10, negate 5, abs -4, min 3 7, max 3 7, clamp 0 100 250 ] ``` ```text [1024, -5, 4, 3, 7, 100] cpu 9197162, mem 37659 ``` `clamp low high x` keeps `x` between the bounds. [Core modules](https://elci-lang.dev/reference/core-modules/#basics) lists the whole prelude. Dividing by zero (with `//`, `modBy` or `remainderBy`) doesn't return a value: it fails the script, and a failing validator rejects the transaction. `/` and fractional literals such as `1.5` are rejected by the compiler: ```elm main : Int main = 7 / 2 ``` ```text .../src/Main.elm:6:7: ELCI2001: Float division is unsupported; use // for integer quotient 6| 7 / 2 ^ ``` To work with fractions, scale your integers. For example, store a percentage in basis points (1% is 100). ## Booleans and comparisons | Operator | Meaning | | --- | --- | | `==`, `/=` | Equal, not equal. Works on numbers, strings, bytes, lists, tuples, records, custom types and dictionaries, but not on functions or `Map`s | | `<`, `<=`, `>`, `>=` | Ordering, for comparable types: numbers, strings, bytes, and lists and tuples of them | | `&&`, `\|\|` | And, or. The right side is evaluated only when needed | | `not` | Negation: `not True` is `False` | ```elm title="src/Main.elm" module Main exposing (main) isValidFee : Int -> Bool isValidFee fee = fee >= 155381 && fee <= 2000000 main : Bool main = isValidFee 200000 && not (isValidFee 5) && "ada" < "lovelace" ``` ```text True cpu 1051432, mem 5116 ``` Strings order by their UTF-8 bytes, so `"Zebra" < "apple"`: capital letters come first. `compare a b` returns `LT`, `EQ` or `GT`. Booleans, records and custom types have no order, and `True < False` is a compile error. Comparisons can't be chained. Write `low < x && x < high` instead of `low < x < high`: ```elm main : Bool main = 1 < 2 < 3 ``` ```text .../src/Main.elm:6:11: ELCI1002: Comparison operators cannot be chained; use && or explicit parentheses 6| 1 < 2 < 3 ^ ``` ## Functions and signatures A call lists its arguments after the function name, separated by spaces: `toLovelace 25`, `isValidFee 200000`. Parentheses group expressions, not arguments: `not (isValidFee 5)`. **Every top-level declaration needs a type signature.** In a signature, `->` separates the parameter types from the result type. `Int -> Int -> Int` takes two integers and returns one. Leaving a signature out is an error: ```elm double x = x * 2 ``` ```text .../src/Main.elm:4:1: ELCI2004: Top-level declarations require a type signature 4| double x = ^^^^^^ ``` Inside a function, types are inferred, so local values need no annotations (you may still add them). When types don't fit together, the compiler shows you where and suggests a fix: ```elm main : Int main = 1 + "one" ``` ```text .../src/Main.elm:6:7: ELCI2002: The operator + works on Int values, not String; use ++ to append 6| 1 + "one" ^ ``` The first line gives the file, line and column, and a code you can look up in the [diagnostics](https://elci-lang.dev/reference/diagnostics/) reference. Below it are the line itself, with a caret under the problem, and sometimes a `Hint:` line with the likely fix. ## Lambdas, currying and partial application `\x -> expression` is an anonymous function, or lambda. `\n total -> n + total` takes two arguments. Every function takes its arguments one at a time. `add : Int -> Int -> Int` is a function that takes an `Int` and returns a function `Int -> Int`, so you can supply only the first argument: ```elm title="src/Main.elm" module Main exposing (main) add : Int -> Int -> Int add x y = x + y addFive : Int -> Int addFive = add 5 main : Int main = List.foldl add 0 (List.map addFive [ 1, 2, 3 ]) ``` ```text 21 cpu 6795728, mem 31808 ``` `addFive` is `add` with its first argument already supplied. `List.map addFive` adds 5 to each element (giving `[ 6, 7, 8 ]`), and `List.foldl add 0` sums them. Operators are functions too. Written in parentheses, `(+)` is the function `\a b -> a + b`, so `List.foldl (+) 0` also sums a list, and `(*) 2` doubles a number. Elci is strict: a partial application evaluates the arguments you give it straight away. `add (10 // 0)` fails at that point, not later when the second argument arrives. ## Generic functions A lowercase name in a signature, such as `a`, is a type variable: the function works for any type in that position. ```elm title="src/Main.elm" module Main exposing (main) twice : (a -> a) -> a -> a twice transform value = transform (transform value) main : Int main = if twice (\word -> word ++ "!") "go" == "go!!" then twice (\n -> n * 10) 3 else 0 ``` ```text 300 cpu 1189600, mem 6013 ``` `twice` is used with strings and with integers. The compiler specializes a generic function for each concrete type it's used with. A signature can't promise more than the code delivers. This one claims to work for any `a`, but `x + 0` only works for `Int`: ```elm identity : a -> a identity x = x + 0 ``` ```text .../src/Main.elm:4:1: ELCI2002: The annotation of `identity` says `a` can be any type, but its definition needs `Int` 4| identity : a -> a ^^^^^^^^ Hint: write `Int` in the annotation, or make the definition work for any type ``` To compare values of a type variable with `<`, or to join them with `++`, name the variable `comparable` or `appendable`, as in Elm: `largest : comparable -> comparable -> comparable` works for numbers, strings and bytes. The [language reference](https://elci-lang.dev/reference/language/#constrained-type-variables) covers these names. ## Bytes and unit Byte strings are everywhere on Cardano: key hashes, token names and script hashes. The `Bytes` module wraps the Plutus byte-string builtins: ```elm title="src/Main.elm" module Main exposing (main) import Bytes tokenName : Bytes tokenName = Bytes.fromString "GOLD" main : Int main = Bytes.length tokenName + Bytes.length (Bytes.sha256 tokenName) ``` ```sh elci eval . --builtins ``` ```text 36 cpu 786669, mem 2032 builtin: EncodeUtf8("GOLD") → #474f4c44 builtin: LengthOfByteString(#474f4c44) → 4 builtin: Sha2_256(#474f4c44) → #fb5b8ae0fd37258b... builtin: LengthOfByteString(#fb5b8ae0fd37258b...) → 32 builtin: AddInteger(4, 32) → 36 ``` "GOLD" is the 4 bytes `#474f4c44`, and its SHA-256 hash is 32 bytes. Byte strings are written in hex after a `#`. [Bytes, hashes and bits](https://elci-lang.dev/advanced/bytes-and-crypto/) covers the whole module. The unit value `()` carries no information. You met it in [Your first validator](https://elci-lang.dev/start/first-validator/) as a datum and redeemer that the validator ignores. ## Comments ```elm title="src/Main.elm" module Main exposing (main) answer : Int answer = -- A line comment. 42 {- A block comment, which can span lines. -} {-| A documentation comment, directly above a declaration. -} main : Int main = answer ``` ## Next [Conditionals, let and pipelines](https://elci-lang.dev/learn/control-flow/) shows how to make decisions, name intermediate values and chain function calls. --- # Conditionals, let and pipelines > Make decisions with if and case, name intermediate values with let, chain calls with pipelines, and understand Elci's strict evaluation. Source: https://elci-lang.dev/learn/control-flow/ This page shows how to make decisions with `if` and `case`, name intermediate results with `let`, and chain function calls with pipelines. It ends with how Elci evaluates your code, which matters for both correctness and cost on-chain. ## if … then … else `if` is an expression, so it always has an `else` and both branches have the same type. Chain conditions with `else if`: ```elm title="src/Main.elm" module Main exposing (main) feeFor : Int -> Int feeFor amount = if amount < 1000 then 0 else if amount < 100000 then 10 else amount // 1000 main : Int main = feeFor 500 + feeFor 5000 + feeFor 250000 ``` With `--builtins`, `elci eval` also lists every builtin operation the script performed: ```sh elci eval . --builtins ``` ```text 260 cpu 1489307, mem 5910 builtin: LessThanInteger(500, 1000) → True builtin: LessThanInteger(5000, 1000) → False builtin: LessThanInteger(5000, 100000) → True builtin: AddInteger(0, 10) → 10 builtin: LessThanInteger(250000, 1000) → False builtin: LessThanInteger(250000, 100000) → False builtin: QuotientInteger(250000, 1000) → 250 builtin: AddInteger(10, 250) → 260 ``` Each call only tested the conditions it needed: `feeFor 500` stopped after the first comparison, and only `feeFor 250000` divided. ## case … of `case` compares a value against patterns, top to bottom, and evaluates the first branch that matches. `_` matches anything: ```elm title="src/Main.elm" module Main exposing (main) label : Int -> String label count = case count of 0 -> "none" 1 -> "one" _ -> "many" main : ( String, String, String ) main = ( label 0, label 1, label 7 ) ``` ```text ( "none", "one", "many" ) cpu 1894636, mem 8277 ``` Patterns can be `Int`, `String` or `Bool` literals, names, `_`, tuples, records, lists and constructors of custom types. A `case` must cover every possible value; without the `_` branch above, the compiler rejects it. [Custom types and pattern matching](https://elci-lang.dev/learn/custom-types/) goes further. ## let … in `let` names intermediate values. The names are visible in the rest of the `let` and in the expression after `in`: ```elm title="src/Main.elm" module Main exposing (main) firstShare : Int -> Int -> Int firstShare total shares = let perShare = total // shares leftover = remainderBy shares total in perShare + leftover main : Int main = firstShare 1000 3 ``` ```text 334 cpu 813990, mem 2904 ``` A `let` can also define local functions, optionally with a signature, and destructure tuples and records. You'll see those forms on the next pages. ## Pipelines and composition Nested calls read inside out. The pipe operator `|>` passes a value into the next function, so the steps read top to bottom: ```elm title="src/Main.elm" module Main exposing (main) main : Int main = [ 5, -3, 12, 0, 8 ] |> List.filter (\n -> n > 0) |> List.map (\n -> n * 2) |> List.foldl (\n total -> n + total) 0 ``` ```text 50 cpu 12396014, mem 60117 ``` `x |> f` means `f x`, and `f <| x` also means `f x`, which saves a pair of parentheses. `>>` and `<<` compose two functions into a new one: `(f >> g) x` is `g (f x)`, and `(g << f) x` is the same. ```elm title="src/Main.elm" module Main exposing (main) double : Int -> Int double n = n * 2 isLarge : Int -> Bool isLarge n = n > 10 main : Bool main = let doubledIsLarge = double >> isLarge in doubledIsLarge 6 && (not <| doubledIsLarge 5) ``` ```text True cpu 1197680, mem 5906 ``` `<|` and `|>` have the lowest precedence of all operators, which is why `not <| doubledIsLarge 5` needs parentheses when it's part of a larger expression. ## Strict evaluation Elci evaluates eagerly, like Elm. Knowing exactly what gets evaluated matters on-chain, for two reasons: every step costs budget, and a single failure rejects the whole transaction. **Local values are always evaluated, even when unused.** This program fails, although it never uses `unused`: ```elm title="src/Main.elm" module Main exposing (main) main : Int main = let unused = 1 // 0 in 5 ``` ```text evaluation failed: QuotientInteger: division by zero cpu 276441, mem 1001 ``` `Debug.trace message value` returns `value` and records `message`, and `elci eval` prints each message on a `trace:` line, so you can watch the order. `Debug`, like `List`, is imported into every module. Both values are computed here, in source order, although the `if` only returns one of them: ```elm title="src/Main.elm" module Main exposing (main) main : Int main = let small = Debug.trace "computing small" 1 large = Debug.trace "computing large" 1000 in if True then small else large ``` ```text 1 cpu 471096, mem 2364 trace: computing small trace: computing large ``` **`if`, `case`, `&&` and `||` evaluate only what they need.** Only the chosen branch of an `if` or `case` runs, and `&&` and `||` skip their right side when the left side decides the answer. So put expensive or failing work inside a branch: ```elm title="src/Main.elm" module Main exposing (main) safeDivide : Int -> Int -> Int safeDivide amount parts = if parts == 0 then 0 else amount // parts main : Int main = safeDivide 10 0 + safeDivide 10 3 ``` ```text 3 cpu 962315, mem 4005 ``` The other rules follow the same idea: - A function call evaluates the function, then its arguments from left to right, before the body runs. So `Maybe.withDefault fallback m` evaluates `fallback` even when `m` is a `Just`, and `(&&) a b`, the operator used as a function, evaluates `b` even when `a` is `False`. - A partial application such as `add (10 // 0)` evaluates its argument immediately. - In a validator, top-level values the handlers use are evaluated once per script run, before the handler is chosen. A failing top-level value therefore fails every purpose of the script. :::tip In a validator, cheap checks that often fail belong on the left of `&&`. When the left side is `False`, the expensive right side never runs, which saves budget. ::: `Debug.trace` also costs budget on-chain, so remove traces before you deploy. [Debugging and budgets](https://elci-lang.dev/testing/debugging/) has more. ## Next [Records and tuples](https://elci-lang.dev/learn/records-and-tuples/) groups values into structures. --- # Records and tuples > Group values with records and tuples, update and destructure them, and see how they are encoded as on-chain data. Source: https://elci-lang.dev/learn/records-and-tuples/ Records and tuples group several values into one. This page shows how to create, read, update and destructure them, and how Elci encodes them as on-chain data. That encoding matters as soon as a record becomes a datum or redeemer. ## Records A record has named fields. `type alias` gives a record type a name: ```elm title="src/Main.elm" module Main exposing (main) type alias Listing = { price : Int , active : Bool } discount : Int -> Listing -> Listing discount percent listing = { listing | price = listing.price - listing.price * percent // 100 } main : Int main = let original = { price = 2000, active = True } cheaper = discount 25 original in if cheaper.active then cheaper.price else 0 ``` ```text 1500 cpu 5077108, mem 17933 ``` - `{ price = 2000, active = True }` creates a record. It has type `Listing` because it has exactly those fields. - `listing.price` reads a field. - `{ listing | price = ... }` is a **record update**: a copy of `listing` with new values for the named fields. You can update several fields at once, separated by commas. The record to update must be a plain name. Records are values. An update creates a new record and leaves the original unchanged. ## Record constructors A record alias also defines a constructor function. Its arguments follow the order of the fields in the declaration: ```elm title="src/Main.elm" module Main exposing (main) type alias Listing = { price : Int , active : Bool } main : Listing main = Listing 2000 True ``` ```text { active = True, price = 2000 } cpu 886960, mem 4025 ``` `Listing 2000 True` is the record `{ price = 2000, active = True }`. `elci eval` prints record fields in alphabetical order, which is a hint about how Elci stores them. ## Accessor functions As in Elm, `.amount` is a function that reads the field `amount` of a record. It is short for `\payout -> payout.amount`, and it is handy with list functions: ```elm title="src/Main.elm" module Main exposing (main) type alias Payout = { beneficiary : String , amount : Int } main : ( Int, List String ) main = let payouts = [ Payout "carol" 300, Payout "alice" 150, Payout "bob" 250 ] in ( List.sum (List.map .amount payouts) , payouts |> List.sortBy .amount |> List.map .beneficiary ) ``` ```text ( 700, ["alice", "bob", "carol"] ) cpu 31097157, mem 139215 ``` A record that another module exposes is read the same way. `Config.settings.fee` reads the field `fee` of the value `settings` in module `Config`: ```elm title="src/Config.elm" module Config exposing (Settings, settings) type alias Settings = { fee : Int , owner : String } settings : Settings settings = { fee = 200, owner = "alice" } ``` ```elm title="src/Main.elm" module Main exposing (main) import Config main : Int main = Config.settings.fee * 2 ``` ```text 400 cpu 1310048, mem 4828 ``` A qualified name is written without spaces and ends at its first lowercase part: `Config.settings` is the value, and `.fee` reads its field. In `List.map .amount payouts`, the space before `.amount` makes it an accessor passed to `List.map`. ## How records are stored on-chain Cardano stores datums and redeemers as **Plutus Data**: integers, byte strings, lists, maps and constructors (a tag number with a list of fields). `Julc.Data.encode` turns any value into its Data, so you can see exactly what goes on-chain: ```elm title="src/Main.elm" module Main exposing (main) import Julc.Data as Data type alias Listing = { price : Int , active : Bool } main : Data main = Data.encode (Listing 2000 True) ``` ```text Constr 0 [Constr 1, 2000] cpu 982960, mem 4625 ``` - A record is a constructor with tag 0. - Its fields are sorted **alphabetically by name**, whatever order you declared them in. That is why `active` (`True`, itself encoded as constructor 1 with no fields) comes before `price` (`2000`). This has practical consequences for datums and redeemers: - Reordering the fields of a record declaration doesn't change its encoding. - **Renaming a field can change the encoding**, because it can change the sort order. Data already on-chain would no longer decode. The build's `abi.json` records the exact layout, so you can detect such changes. [Datums and redeemers](https://elci-lang.dev/contracts/datums-and-redeemers/) covers the encoding rules for every type. ## Destructuring Patterns can take a record apart. `{ amount }` binds the field `amount` to a variable of the same name. It works in function and lambda parameters, in `let`, and in `case`: ```elm title="src/Main.elm" module Main exposing (main) type alias Payout = { beneficiary : String , amount : Int } total : List Payout -> Int total payouts = List.foldl (\{ amount } sum -> amount + sum) 0 payouts main : Int main = let payouts = [ Payout "alice" 150, Payout "bob" 250 ] { beneficiary } = Payout "carol" 10 in if beneficiary == "carol" then total payouts else 0 ``` ```text 400 cpu 6314459, mem 26311 ``` In a `case`, a record pattern selects fields and can sit inside other patterns, such as tuples: ```elm title="src/Main.elm" module Main exposing (main) type alias Order = { kind : String , quantity : Int , paid : Bool } priority : Order -> Int priority order = case order of { paid, quantity } -> if paid && quantity > 10 then 2 else if paid then 1 else 0 kindCode : Order -> Int kindCode order = case ( order.kind, order.paid ) of ( "bulk", True ) -> 3 ( "bulk", False ) -> 2 ( _, _ ) -> 1 main : Int main = let order = { kind = "bulk", quantity = 25, paid = True } in priority order * 10 + kindCode order ``` ```text 23 cpu 7369610, mem 25543 ``` ## Generic records A record alias can take type parameters, like a function takes arguments: ```elm title="src/Main.elm" module Main exposing (main) type alias Tagged a = { label : String , value : a } relabel : String -> Tagged a -> Tagged a relabel label tagged = { tagged | label = label } main : Int main = let amount = relabel "fee" (Tagged "draft" 170000) flag = Tagged "enabled" True in if flag.value && amount.label == "fee" then amount.value else 0 ``` ```text 170000 cpu 5150199, mem 18573 ``` `amount` is a `Tagged Int` and `flag` is a `Tagged Bool`. `relabel` works on both. ## Tuples A tuple groups two or three values without naming them: `( 1, "one" )` has type `( Int, String )`. Tuples are handy for returning several results from a function: ```elm title="src/Main.elm" module Main exposing (main) divMod : Int -> Int -> ( Int, Int ) divMod amount parts = ( amount // parts, modBy parts amount ) main : Int main = let ( quotient, remainder ) = divMod 17 5 in quotient * 10 + remainder ``` ```text 32 cpu 3397721, mem 12146 ``` The `Tuple` module, which every module imports, works on pairs: `Tuple.first`, `Tuple.second`, `Tuple.pair`, `Tuple.mapFirst`, `Tuple.mapSecond` and `Tuple.mapBoth`. Tuples of comparable values, such as numbers and strings, compare component by component, so `List.sort` sorts them: ```elm title="src/Main.elm" module Main exposing (main) main : ( List ( Int, String ), List Int ) main = let scores = [ ( 2, "bob" ), ( 1, "carol" ), ( 2, "alice" ) ] in ( List.sort scores, List.map Tuple.first scores ) ``` ```text ( [( 1, "carol" ), ( 2, "alice" ), ( 2, "bob" )], [2, 1, 2] ) cpu 28573982, mem 123050 ``` A tuple is encoded like a record, as a constructor with tag 0, but its fields stay in position order. For anything with more than three parts, or whose parts deserve names, use a record. A four-element tuple is rejected: ```elm let ( a, b, c, d ) = ( 1, 2, 3, 4 ) in ``` ```text .../src/Main.elm:7:24: ELCI1002: Tuple patterns have two or three elements 7| ( a, b, c, d ) = ^ ``` ## Equality `==` compares records and tuples field by field. Field order in the literal doesn't matter: ```elm title="src/Main.elm" module Main exposing (main) main : Bool main = ( 1, "a", True ) == ( 1, "a", True ) && { x = 1, y = 2 } == { y = 2, x = 1 } ``` ```text True cpu 6141566, mem 15472 ``` Records have no order, so `<` and `List.sort` reject them. Sort records by a field with `List.sortBy (\listing -> listing.price)`; see [sorting](https://elci-lang.dev/learn/collections/#sorting). ## Limits Records in Elci are **closed**: a record type lists all of its fields. - Extensible record types such as `{ r | price : Int }` are not supported: ```text .../src/Main.elm:4:13: ELCI1002: Extensible records are unsupported; use a closed record 4| total : { r | price : Int } -> Int ^ ``` - A field access, or an accessor function such as `.price`, needs to know which record type it is reading. Use a named alias or a signature when inference can't tell. Reading a field that doesn't exist is an error: ```text .../src/Main.elm:14:12: ELCI2010: Missing field 'cost'; available fields: [price] 14| listing.cost ^ ``` ## Next [Custom types and pattern matching](https://elci-lang.dev/learn/custom-types/) models values that can take different shapes. --- # Custom types and pattern matching > Define custom types, match on them exhaustively with case, use Maybe and Result, nest patterns, and see how constructors are encoded on-chain. Source: https://elci-lang.dev/learn/custom-types/ A custom type lists the shapes a value can take. Custom types and exhaustive pattern matching are the most useful part of Elci for contracts. A redeemer is usually a custom type with one constructor per action, and the compiler checks that your validator handles every one. ## Defining a custom type Each alternative is a **constructor**, optionally carrying values: ```elm title="src/Main.elm" module Main exposing (main) type Action = Deposit Int | Withdraw Int | Close apply : Action -> Int -> Int apply action balance = case action of Deposit amount -> balance + amount Withdraw amount -> balance - amount Close -> 0 main : Int main = [ Deposit 100, Withdraw 30, Deposit 5 ] |> List.foldl apply 0 ``` ```text 75 cpu 7551636, mem 30690 ``` `Deposit 100` builds a value of type `Action`. In `case`, the pattern `Deposit amount` matches that constructor and binds its value to `amount`. ## Every case must be complete If you add a constructor, every `case` on that type must handle it. Here `Close` is missing: ```elm apply : Action -> Int -> Int apply action balance = case action of Deposit amount -> balance + amount Withdraw amount -> balance - amount ``` ```text .../src/Main.elm:12:5: ELCI2007: This case does not cover every possibility; it is missing `Close` 12| case action of ^^^^ Hint: add a branch for each missing pattern ``` The message lists the patterns the `case` misses: up to three, then how many more there are. Nested patterns are listed too: a `case` on a `Maybe (Result String Int)` with only a `Just (Ok n)` branch is missing `Nothing` and `Just (Err _)`. A branch that can never match is also an error. `Just 0` below is unreachable, because `Just _` already matches every `Just`: ```elm isZero : Maybe Int -> Bool isZero value = case value of Just _ -> False Just 0 -> True Nothing -> False ``` ```text .../src/Main.elm:10:9: ELCI2006: Unreachable pattern 10| Just 0 -> ^^^^ ``` :::tip Prefer listing constructors to a `_` wildcard in validators. With explicit branches, adding a new redeemer action makes the compiler point at every place that must decide what to do with it. ::: ## How constructors are encoded Constructors are numbered in declaration order, starting at 0: `Deposit` is 0, `Withdraw` is 1 and `Close` is 2. On-chain, a value is a Plutus Data constructor with that tag and its values as fields. `Julc.Data.encode` shows the encoding: ```elm title="src/Main.elm" module Main exposing (main) import Julc.Data as Data type Action = Deposit Int | Withdraw Int | Close main : List Data main = [ Data.encode (Deposit 100), Data.encode (Withdraw 30), Data.encode Close ] ``` ```text [Constr 0 [100], Constr 1 [30], Constr 2] cpu 1623933, mem 7648 ``` :::caution Reordering, inserting or removing constructors changes their tags, and with them the encoding of every datum and redeemer of that type. Add new constructors at the end, and pin the encoding with `expected-abi` (see [Build and deploy](https://elci-lang.dev/contracts/build-and-deploy/)). ::: `Bool` is encoded the same way: `False` is constructor 0 and `True` is constructor 1. ## Maybe `Maybe a` is built in. It is either `Just value` or `Nothing`, and it's how Elci represents "there may be no answer". The `Maybe` module, which every module imports, has Elm's helpers: `withDefault`, `map`, `map2`, `map3` and `andThen`. ```elm title="src/Main.elm" module Main exposing (main) findPrice : String -> List ( String, Int ) -> Maybe Int findPrice name prices = case prices of [] -> Nothing ( candidate, price ) :: rest -> if candidate == name then Just price else findPrice name rest main : Int main = let prices = [ ( "apple", 3 ), ( "pear", 5 ) ] in Maybe.withDefault 0 (findPrice "pear" prices) + Maybe.withDefault 0 (findPrice "plum" prices) ``` ```text 5 cpu 11103777, mem 42693 ``` `Nothing` is constructor 0 and `Just` is constructor 1. Julc's Java `Optional` uses different tags, so the two aren't interchangeable. `Julc.Data.fromOptional` and `toOptional` convert between them when a Java library returns an optional value. ## Result `Result error value` is either `Ok value` or `Err error`, as in Elm. Use it for a computation that can fail with a reason. The `Result` module, imported into every module, has `map`, `andThen`, `withDefault`, `toMaybe`, `mapError` and more: ```elm title="src/Main.elm" module Main exposing (main) type alias Payout = { amount : Int , fee : Int } validate : Payout -> Result String Payout validate payout = if payout.amount <= 0 then Err "amount must be positive" else if payout.fee > payout.amount then Err "fee exceeds amount" else Ok payout main : List (Result String Int) main = [ Payout 100 2, Payout 0 1, Payout 5 9 ] |> List.map (\payout -> Result.map (\valid -> valid.amount - valid.fee) (validate payout)) ``` ```text [Ok 98, Err "amount must be positive", Err "fee exceeds amount"] cpu 15781289, mem 59252 ``` On-chain, a validator returns `Bool`, so a `Result` usually ends in a `case` that returns `False` for an `Err`. As Data, `Ok` is constructor 0 and `Err` is constructor 1, the reverse of PlutusTx's `Either`. ## Nested patterns Patterns can nest constructors, tuples, records and lists. A list pattern such as `[ x ]` matches a list of exactly that length, and `first :: rest` matches any non-empty list: ```elm title="src/Main.elm" module Main exposing (main) type Shape = Circle Int | Rectangle Int Int describe : List Shape -> Int describe shapes = case shapes of [] -> 0 [ Circle 0 ] -> 1 [ Rectangle width height ] -> if width == height then 2 else 3 (Circle _) :: rest -> 10 + describe rest (Rectangle _ _) :: rest -> 20 + describe rest main : Int main = describe [ Circle 5, Rectangle 2 3, Rectangle 4 4 ] ``` ```text 32 cpu 12364548, mem 46519 ``` `Circle 5` doesn't match `[ Circle 0 ]` (the list has three elements), so the `(Circle _) :: rest` branch adds 10. The same happens for `Rectangle 2 3`, which adds 20. The last element, `Rectangle 4 4`, is alone in its list and square, which adds 2. ## Constructors as functions, and `as` A constructor is also a function: `Deposit` has type `Int -> Action`, so you can pass it to `List.map`. An `as` pattern names the whole value that a pattern matched: ```elm title="src/Main.elm" module Main exposing (main) type Action = Deposit Int | Withdraw Int | Close total : List Action -> Int total actions = case actions of [] -> 0 ((Deposit amount) as first) :: rest -> if first == Deposit 0 then total rest else amount + total rest _ :: rest -> total rest main : Int main = total (List.map Deposit [ 0, 10, 20 ] ++ [ Close, Withdraw 5 ]) ``` ```text 30 cpu 19075676, mem 69864 ``` Custom types support `==` when all their fields do. ## Recursive and generic types A custom type can refer to itself, and can take type parameters: ```elm title="src/Main.elm" module Main exposing (main) type Tree a = Leaf a | Branch (Tree a) (Tree a) sum : Tree Int -> Int sum tree = case tree of Leaf value -> value Branch left right -> sum left + sum right depth : Tree a -> Int depth tree = case tree of Leaf _ -> 1 Branch left right -> 1 + max (depth left) (depth right) max : Int -> Int -> Int max a b = if a > b then a else b main : Int main = let tree = Branch (Leaf 3) (Branch (Leaf 4) (Leaf 5)) in sum tree * 10 + depth tree ``` ```text 123 cpu 17632608, mem 64972 ``` The sum is 12 and the depth is 3. Elci has no built-in `max`, so this module defines one. Recursion is how you walk any recursive structure, including lists. Every step costs budget, so keep on-chain data shallow. ## Next [Lists, dictionaries, maps and arrays](https://elci-lang.dev/learn/collections/) covers Elci's collection types and what they cost. --- # Lists, dictionaries, maps and arrays > Work with Elci's collections, List, Dict, Map and Array, write your own list functions with recursion, and choose the right one for on-chain cost. Source: https://elci-lang.dev/learn/collections/ Elci has four collection types. `List` is the everyday sequence, `Dict` stores values under comparable keys as Elm's `Dict` does, `Map` is the ledger's own key/value map, and `Array` gives constant-time indexed reads. This page covers all four, and what they cost on-chain. ## Lists A list holds any number of values of one type: `[ 1, 2, 3 ]` is a `List Int`. `::` puts an element in front of a list, `++` joins two lists, and you take lists apart with patterns: ```elm title="src/Main.elm" module Main exposing (main) sum : List Int -> Int sum values = case values of [] -> 0 first :: rest -> first + sum rest main : Int main = let amounts = 0 :: [ 10, 20 ] ++ [ 30 ] in sum amounts + List.length amounts ``` ```text 64 cpu 10418493, mem 50002 ``` `amounts` is `[ 0, 10, 20, 30 ]`, so the result is 60 + 4. `[]` matches the empty list, and `first :: rest` matches a list with a first element and the rest after it. Most list functions are this shape: handle the empty list, then handle one element and recurse on the rest. `List.sum` already does this one; writing it yourself shows the pattern. ### The List module Every module imports `List`, so its functions need no import. These are the ones you'll use most: | Function | Type | Example | Result | | --- | --- | --- | --- | | `map` | `(a -> b) -> List a -> List b` | `List.map (\n -> n * 2) [ 3, 1, 4 ]` | `[ 6, 2, 8 ]` | | `filter` | `(a -> Bool) -> List a -> List a` | `List.filter (\n -> n > 2) [ 3, 1, 4 ]` | `[ 3, 4 ]` | | `foldl` | `(a -> b -> b) -> b -> List a -> b` | `List.foldl (\n acc -> n :: acc) [] [ 3, 1, 4 ]` | `[ 4, 1, 3 ]` | | `foldr` | `(a -> b -> b) -> b -> List a -> b` | `List.foldr (\n acc -> n :: acc) [] [ 3, 1, 4 ]` | `[ 3, 1, 4 ]` | | `any` | `(a -> Bool) -> List a -> Bool` | `List.any (\n -> n == 4) [ 3, 1, 4 ]` | `True` | | `all` | `(a -> Bool) -> List a -> Bool` | `List.all (\n -> n > 1) [ 3, 1, 4 ]` | `False` | | `length` | `List a -> Int` | `List.length [ 3, 1, 4 ]` | `3` | | `reverse` | `List a -> List a` | `List.reverse [ 3, 1, 4 ]` | `[ 4, 1, 3 ]` | | `append` | `List a -> List a -> List a` | `List.append [ 1 ] [ 2 ]` | `[ 1, 2 ]` | | `concatMap` | `(a -> List b) -> List a -> List b` | `List.concatMap (\n -> [ n, n ]) [ 1, 2 ]` | `[ 1, 1, 2, 2 ]` | The folds pass the element first and the accumulator second. `foldl` starts from the front of the list and `foldr` from the back. `List` has the rest of Elm's list functions too, with the same names, argument orders and edge cases: | Function | Example | Result | | --- | --- | --- | | `singleton`, `repeat` | `List.repeat 3 0` | `[ 0, 0, 0 ]` | | `range` | `List.range 1 4` | `[ 1, 2, 3, 4 ]` | | `isEmpty` | `List.isEmpty []` | `True` | | `head` | `List.head [ 3, 1, 4 ]` | `Just 3` | | `tail` | `List.tail [ 3, 1, 4 ]` | `Just [ 1, 4 ]` | | `take` | `List.take 2 [ 3, 1, 4 ]` | `[ 3, 1 ]` | | `drop` | `List.drop 2 [ 3, 1, 4 ]` | `[ 4 ]` | | `member` | `List.member 4 [ 3, 1, 4 ]` | `True` | | `sum`, `product` | `List.sum [ 3, 1, 4 ]` | `8` | | `maximum`, `minimum` | `List.maximum [ 3, 1, 4 ]` | `Just 4` | | `indexedMap` | `List.indexedMap (\i n -> i * n) [ 3, 1, 4 ]` | `[ 0, 1, 8 ]` | | `filterMap` | `List.filterMap List.head [ [ 3 ], [], [ 4 ] ]` | `[ 3, 4 ]` | | `partition` | `List.partition (\n -> n > 2) [ 3, 1, 4 ]` | `( [ 3, 4 ], [ 1 ] )` | | `concat` | `List.concat [ [ 1 ], [ 2, 3 ] ]` | `[ 1, 2, 3 ]` | | `intersperse` | `List.intersperse 0 [ 3, 1, 4 ]` | `[ 3, 0, 1, 0, 4 ]` | | `map2`, `map3` | `List.map2 (+) [ 3, 1, 4 ] [ 10, 20 ]` | `[ 13, 21 ]` | | `unzip` | `List.unzip [ ( 1, "a" ), ( 2, "b" ) ]` | `( [ 1, 2 ], [ "a", "b" ] )` | | `sort` | `List.sort [ 3, 1, 4 ]` | `[ 1, 3, 4 ]` | | `sortBy` | `List.sortBy negate [ 3, 1, 4 ]` | `[ 4, 3, 1 ]` | | `sortWith` | `List.sortWith (\a b -> compare b a) [ 3, 1, 4 ]` | `[ 4, 3, 1 ]` | `map2` stops at the shorter list, as in Elm, so the `4` above is dropped. [Core modules](https://elci-lang.dev/reference/core-modules/#list) has every signature. These functions are ordinary Elci code, compiled like yours. When one is missing, write it in a few lines. There is no function to read the element at an index, for example: ```elm title="src/Main.elm" module Main exposing (main) nth : Int -> List a -> Maybe a nth index items = case items of [] -> Nothing first :: rest -> if index == 0 then Just first else nth (index - 1) rest range : Int -> Int -> List Int range from to = if from > to then [] else from :: range (from + 1) to main : Maybe Int main = nth 90 (List.range 1 100) ``` ```text Just 91 cpu 159618965, mem 723356 ``` The 91st element (index 90) is `Just 91`. Note the budget: building a 100-element list and walking 90 steps into it took about 160 million CPU units. On-chain lists are linked lists, so reaching element `n` takes `n` steps. ### Sorting `List.sort` sorts values of a comparable type: numbers, strings, bytes and ledger hashes, and lists and tuples of them. Records and custom types are not comparable, so sort them by a key with `List.sortBy`, or with a comparison function that returns `LT`, `EQ` or `GT`: ```elm title="src/Main.elm" module Main exposing (main) type alias Bid = { bidder : String , amount : Int } bids : List Bid bids = [ Bid "carol" 30, Bid "alice" 50, Bid "bob" 30 ] main : ( List String, List String, List String ) main = ( List.sort (List.map (\bid -> bid.bidder) bids) , List.map (\bid -> bid.bidder) (List.sortBy (\bid -> bid.amount) bids) , List.map (\bid -> bid.bidder) (List.sortWith (\a b -> compare b.amount a.amount) bids) ) ``` ```text ( ["alice", "bob", "carol"], ["carol", "bob", "alice"], ["alice", "carol", "bob"] ) cpu 67631155, mem 300118 ``` The sorts are stable: `carol` and `bob` bid the same amount and keep their order. Sorting costs budget, so sort on-chain only when the result depends on the order. ## Dictionaries `Dict k v` stores values of type `v` under keys of type `k`, with Elm's `Dict` API. Import it as in Elm, with `import Dict exposing (Dict)`. Keys must be comparable: `Int`, `String`, `Bytes`, ledger hashes such as `PubKeyHash`, and lists and tuples of these. ```elm title="src/Main.elm" module Main exposing (main) import Dict exposing (Dict) type alias Balances = Dict String Int deposit : String -> Int -> Balances -> Balances deposit owner amount balances = Dict.update owner (\current -> case current of Just balance -> Just (balance + amount) Nothing -> Just amount ) balances balanceOf : String -> Balances -> Int balanceOf owner balances = Maybe.withDefault 0 (Dict.get owner balances) main : ( List ( String, Int ), Int ) main = let balances = Dict.empty |> deposit "carol" 70 |> deposit "alice" 100 |> deposit "bob" 40 |> deposit "alice" 25 in ( Dict.toList balances, balanceOf "dave" balances ) ``` ```text ( [( "alice", 125 ), ( "bob", 40 ), ( "carol", 70 )], 0 ) cpu 37768518, mem 158024 ``` Alice has 125, and Dave has no balance, so `balanceOf` gives 0. The bindings come out in key order, not in the order they went in. The module has all of Elm's `Dict` functions, with the same names, argument orders and results: | Group | Functions | | --- | --- | | Build | `empty`, `singleton`, `insert`, `update`, `remove` | | Query | `isEmpty`, `member`, `get`, `size` | | Lists | `keys`, `values`, `toList`, `fromList` | | Transform | `map`, `foldl`, `foldr`, `filter`, `partition` | | Combine | `union`, `intersect`, `diff`, `merge` | [Core modules](https://elci-lang.dev/reference/core-modules/#dict) has every signature. A `Dict` is stored as a Plutus map whose keys are kept in ascending order. What that means for you: - **Key order, always.** `keys`, `values`, `toList` and the folds follow ascending key order, as in Elm. `fromList` keeps the last value of a repeated key, and `union` prefers the left dictionary's value. - **`==` works.** Two dictionaries with the same bindings are equal, however they were built: ```elm title="src/Main.elm" module Main exposing (main) import Dict main : ( List String, Bool ) main = let scores = Dict.fromList [ ( "carol", 7 ), ( "alice", 9 ), ( "bob", 4 ), ( "alice", 5 ) ] in ( Dict.keys scores , scores == Dict.fromList [ ( "bob", 4 ), ( "alice", 5 ), ( "carol", 7 ) ] ) ``` ```text ( ["alice", "bob", "carol"], True ) cpu 73414533, mem 282640 ``` - **Lookups are O(n).** A `Dict` is a list of pairs, not a tree. `get`, `member`, `insert` and `remove` walk from the smallest key and stop at the first key that isn't smaller, at about 1.1 million CPU units per key passed. `fromList` sorts, in O(n log n). - **Datums can hold dictionaries.** A `Dict` encodes as a Plutus map with sorted keys. A datum, redeemer or parameter whose keys are out of order or repeated fails to decode, so an attacker can't hide a key where lookups stop early. See [datums and redeemers](https://elci-lang.dev/contracts/datums-and-redeemers/#dictionaries-in-data). ## Maps `Map k v` is the ledger's own key/value map: an unsorted list of pairs, with keys compared by their Data encoding. The transaction's maps are `Map`s, such as `txInfo.withdrawals` (`Map Credential Int`) and `txInfo.redeemers` (`Map ScriptPurpose Data`). Their keys are not comparable, so they can't be `Dict`s. Use a `Map` for those, and for keys of your own that are not comparable, such as a custom type. Write `import Map` to use its functions; the type name `Map` is always in scope. ```elm title="src/Main.elm" module Main exposing (main) import Map exposing (Map) type Asset = Ada | Token String holdings : Map Asset Int holdings = Map.fromList [ ( Ada, 2000000 ), ( Token "GOLD", 5 ) ] main : ( Maybe Int, Maybe Int, List Asset ) main = ( Map.get (Token "GOLD") holdings , Map.get (Token "SILVER") holdings , Map.keys (Map.insert (Token "SILVER") 1 holdings) ) ``` ```text ( Just 5, Nothing, [Token "SILVER", Token "GOLD", Ada] ) cpu 30055770, mem 101862 ``` `Asset` is a custom type, so it can't be a `Dict` key, but it can be a `Map` key. The new key `Token "SILVER"` comes first. `Asset` only illustrates a key: the ledger's own amounts of ada and tokens, such as an output's value, are `Value`s, with their own [`Value`](https://elci-lang.dev/reference/core-modules/#value) module. | Function | Type | | --- | --- | | `empty` | `Map k v` | | `singleton` | `k -> v -> Map k v` | | `fromList` | `List ( k, v ) -> Map k v` | | `get` | `k -> Map k v -> Maybe v` | | `member` | `k -> Map k v -> Bool` | | `insert` | `k -> v -> Map k v -> Map k v` (replaces an existing key) | | `remove` | `k -> Map k v -> Map k v` | | `update` | `k -> (Maybe v -> Maybe v) -> Map k v -> Map k v` (`Nothing` removes the key) | | `size`, `isEmpty` | `Map k v -> Int`, `Map k v -> Bool` | | `keys`, `values` | `Map k v -> List k`, `Map k v -> List v` | | `toList` | `Map k v -> List ( k, v )` | | `foldl`, `foldr` | `(k -> v -> b -> b) -> b -> Map k v -> b` | | `filter` | `(k -> v -> Bool) -> Map k v -> Map k v` | | `map` | `(k -> a -> b) -> Map k a -> Map k b` | Because a `Map` is the ledger's representation: - **Lookups are O(n).** `get`, `member`, `insert` and `remove` scan the pairs from the front, at about 1.1 million CPU units per key passed. Building a map with `fromList` is O(n²). That's fine for small maps; for your own data with comparable keys, a `Dict` builds faster. - **Maps are not sorted.** `keys`, `values` and `toList` follow the map's own order. `insert` and `fromList` put a new key at the front, and `map` and `filter` keep the existing order. When you need key order, sort the bindings with `List.sortBy Tuple.first`, or use a `Dict`. ```elm title="src/Main.elm" module Main exposing (main) import Map main : List Int main = Map.fromList [ ( 3, "c" ), ( 1, "a" ), ( 2, "b" ) ] |> Map.insert 0 "z" |> Map.keys ``` ```text [0, 2, 1, 3] cpu 26382535, mem 110836 ``` - **Maps can't be compared with `==`:** ```text .../src/Main.elm:8:23: ELCI2003: The operator == cannot compare Map Int Int values: it has no equality ``` Keys and values can be any type that has a Data encoding, including your own records and custom types and ledger types such as `PubKeyHash`. ## Arrays `Array a` is an immutable sequence with **constant-time** `get` and `length`, like Elm's `Array`. It is a Plutus V3 array (CIP-138). As with `Dict` and `Map`, import the module for its functions: ```elm title="src/Main.elm" module Main exposing (main) import Array main : Int main = let tiers = Array.fromList [ 0, 100, 250, 500 ] in case ( Array.get 2 tiers, Array.get 9 tiers ) of ( Just fee, Nothing ) -> fee + Array.length tiers _ -> 0 ``` ```text 254 cpu 6588326, mem 22837 ``` `Array.get` returns `Nothing` for an index outside the array. The module also has `empty`, `isEmpty`, `initialize`, `repeat`, `toList`, `toIndexedList`, `map`, `indexedMap`, `foldl`, `foldr`, `filter` and `slice`. In `slice start end`, a negative index counts from the end, so `Array.slice 0 -1 tiers` drops the last element. Arrays have two important limits: - **No updates.** There is no `set` or `push`. Build a `List`, then convert it once with `Array.fromList`, which is O(n). - **No Data encoding.** An array can't be a datum, redeemer or parameter, and can't be stored inside a record, custom type, tuple, list or map: ```elm main : Int main = List.length [ Array.fromList [ 1 ] ] ``` ```text .../src/Main.elm:8:5: ELCI2013: Arrays have no Data encoding and cannot be stored in data; keep a List and convert it with Array.fromList ``` Keep a `List` in your data, and convert it where you need many indexed reads. ## Choosing a collection Everything a script does costs budget, and traversals are where budgets go. Here are nine lookups into the same 100-element list, done two ways. This version uses an array: ```elm title="src/Main.elm" module Main exposing (main) import Array main : Int main = let values = Array.fromList (List.range 1 100) at index = case Array.get index values of Just value -> value Nothing -> 0 in List.foldl (\index total -> at index + total) 0 [ 10, 20, 30, 40, 50, 60, 70, 80, 90 ] ``` The other version is the same program with `values = List.range 1 100` and `nth index values` in place of `Array.get index values`, using the `nth` function from earlier on this page: | Version | CPU | Memory | | --- | ---: | ---: | | `List` with `nth` | 500,299,665 | 2,264,391 | | `Array` with `Array.get` | 116,118,728 | 493,247 | Both results are 459, and both build the same list. The array version also converts it once, but it wins on the lookups: each `nth` walks up to 90 elements, while each `Array.get` takes a constant number of steps. - Use a **List** for data, and when you process every element once (`map`, `filter`, `foldl`, `any`). - Use a **Dict** for data you build or look up by a comparable key, including datums. - Use a **Map** for the ledger's maps, such as `txInfo.withdrawals`, and for keys that are not comparable. - Use an **Array** when you read many elements by index. Convert once, then read many times. [Costs and performance](https://elci-lang.dev/advanced/costs/) goes deeper into budgets. ## Next [Modules and imports](https://elci-lang.dev/learn/modules/) splits your code across files. --- # Modules and imports > Split code into modules, control what each module exposes, import Elci modules and Julc Java libraries, and share code between projects. Source: https://elci-lang.dev/learn/modules/ As a project grows, you split it into modules. This page shows how modules map to files, how `exposing` keeps helpers private, the forms of `import`, and which modules come with Elci. You also see how to use Julc's Java libraries and how to share Elci code between projects. ## One module per file Every file starts with a module header. The module name must match the file's path under a source directory: `Shop.Pricing` lives in `src/Shop/Pricing.elm`. ```elm title="src/Shop/Pricing.elm" module Shop.Pricing exposing (Item, Tier(..), priceOf) type Tier = Standard | Member type alias Item = { name : String , price : Int } priceOf : Tier -> Item -> Int priceOf tier item = case tier of Standard -> item.price Member -> item.price - memberDiscount item.price memberDiscount : Int -> Int memberDiscount price = price // 10 ``` ```elm title="src/Main.elm" module Main exposing (main) import Shop.Pricing as Pricing exposing (Tier(..)) main : Int main = Pricing.priceOf Member (Pricing.Item "hat" 500) + Pricing.priceOf Standard { name = "scarf", price = 300 } ``` Put both files in a function project (create the `src/Shop` directory) and run it: ```text 750 cpu 4856488, mem 17742 ``` A member pays 450 for the hat, and the scarf costs 300. If the header and the path disagree, the compiler says so: ```text .../src/Shop/Pricing.elm:1:1: ELCI2104: Module header Pricing does not match its source path, which names module Shop.Pricing 1| module Pricing exposing (Item, Tier(..), priceOf) ^^^^^^^ ``` ## What a module exposes The `exposing` list in the header is the module's public interface: | In the header | What other modules can use | | --- | --- | | `priceOf` | The function or value `priceOf` | | `Item` | The type alias `Item`, and for a record alias its constructor `Item` | | `Tier` | The custom type `Tier`, but not its constructors | | `Tier(..)` | The custom type `Tier` and all of its constructors | Everything else is private. `memberDiscount` above isn't exposed, so `Main` can't call it: ```text .../src/Main.elm:9:11: ELCI2104: Module Pricing does not expose 'memberDiscount' 9| - Pricing.memberDiscount 500 ^^^^^^^^^^^^^^^^^^^^^^^ ``` Keeping helpers private lets you change them without breaking other modules. In a validator module, expose the handlers and the datum and redeemer types that tests and off-chain code need. ## Importing | Import | Then you can write | | --- | --- | | `import Shop.Pricing` | `Shop.Pricing.priceOf` | | `import Shop.Pricing as Pricing` | `Pricing.priceOf` | | `import Shop.Pricing exposing (priceOf, Tier(..))` | `priceOf`, `Member` and `Standard` unqualified, and `Shop.Pricing.Item` | | `import Shop.Pricing exposing (..)` | Everything the module exposes, unqualified | A qualified name only works through an import in the current module. After `import Shop.Pricing exposing (Tier(..))` without `as Pricing`, the name `Pricing.priceOf` means nothing: ```text .../src/Main.elm:8:5: ELCI2104: No module Pricing is imported, and none has that name 8| Pricing.priceOf Member { name = "hat", price = 500 } ^^^^^^^^^^^^^^^ ``` Write `Shop.Pricing.priceOf`, or import the module `as Pricing`. Without any import of the module, `Shop.Pricing.priceOf` gives ``Module Shop.Pricing is not imported; add `import Shop.Pricing` ``. A misspelt qualified name gets a suggestion, such as ``did you mean `List.length`?`` for `Lsit.length`. Your own declarations take precedence over imported names. If two imports expose different things under one name, using that name is an error that lists the qualified alternatives; write the one you mean, such as `Pricing.priceOf`. Imports can't form a cycle. If `Shop.Pricing` imported `Main` while `Main` imports `Shop.Pricing`, the build stops with: ```text .../src/Shop/Pricing.elm:3:1: ELCI2104: Import cycle: Main -> Shop.Pricing -> Main 3| import Main ^^^^^^ ``` Move the shared code into a third module that both import. ## Modules that come with Elci As in Elm, every module imports the core modules for you: | Module | Imported as | Contents | | --- | --- | --- | | `Basics` | `import Basics exposing (..)` | The prelude: `min`, `max`, `abs`, `negate`, `clamp`, `compare`, `identity`, `always`, `not`, `xor`, `modBy`, `remainderBy` | | `List` | `import List` | `map`, `filter`, `foldl`, `head`, `range`, `sum`, `sort` and the rest of Elm's list functions | | `Maybe` | `import Maybe exposing (Maybe(..))` | `withDefault`, `map`, `map2`, `map3`, `andThen` | | `Result` | `import Result exposing (Result(..))` | `Ok` and `Err`, with `map`, `andThen`, `withDefault`, `toMaybe` and more | | `Tuple` | `import Tuple` | `pair`, `first`, `second`, `mapFirst`, `mapSecond`, `mapBoth` | | `String` | `import String` | `fromInt`, `isEmpty`, `append`, `concat`, `join` | | `Debug` | `import Debug` | `Debug.trace`, which records a message | The others you import yourself: | Module | Import | Contents | | --- | --- | --- | | `Dict` | `import Dict exposing (Dict)` | Elm's dictionaries, with comparable keys kept in order | | `Map` | `import Map` | The ledger's key/value maps, for ledger data and keys that aren't comparable | | `Array` | `import Array` | Constant-time indexed arrays | | `Value` | `import Value exposing (Value)` | The ledger's multi-asset values: lovelace and native tokens | | `Interval` | `import Interval` | Time ranges such as the transaction's validity range: deadline checks | | `Tx` | `import Tx` | Questions about the transaction: signers, spent outputs, payments, minted tokens, datums | | `Bytes` | `import Bytes` | Byte-string builtins: building, slicing, integers, bits and hashes | | `Julc.Data` | `import Julc.Data as Data` | `encode` and `decode` to and from Plutus Data, and `Maybe` adapters | | `Julc.Ledger` | `import Julc.Ledger exposing (...)` | Ledger types: `ScriptContext`, `PubKeyHash`, `TxOutRef` and more | [Core modules](https://elci-lang.dev/reference/core-modules/) lists every function. Writing a default import yourself, such as `import List exposing (head)`, is allowed and adds the names you expose. Test modules can also import the test library, `Expect`, `Scenario` and `Property` (see [Unit tests](https://elci-lang.dev/testing/unit-tests/)). Source modules can't, which keeps test code out of your scripts. `import Julc.Ledger exposing (..)` exposes the ledger's types but not their constructors. Name the constructors you need, as in `exposing (Credential(..))`. [Reading the transaction](https://elci-lang.dev/contracts/reading-the-transaction/) covers the ledger types. ## Julc's Java libraries Julc ships on-chain libraries written in Java, such as `MathLib`, `CryptoLib`, `AddressLib` and `ByteStringLib`. They fill the gaps in the core modules, such as floor division and signature verification. To use one, give it a module name in `elci.toml`: ```toml title="elci.toml" [java-modules] "Julc.Math" = "org.julclang.stdlib.lib.MathLib" ``` Then import it like any other module: ```elm title="src/Main.elm" module Main exposing (main) import Julc.Math as Math exposing (floorDiv) main : ( Int, Int, Int ) main = ( floorDiv -7 2, Math.floorMod -7 2, Math.expMod 3 4 5 ) ``` ```text ( -4, 1, 1 ) cpu 3991568, mem 16175 ``` `floorDiv` rounds towards negative infinity, where `//` truncates towards zero. The compiler reads the library's Java source and compiles the functions you call into your script. Two rules differ from Elci modules: - The module name is whatever you choose in `elci.toml`. `Julc.Math` is a convention, not a built-in name. A core module's name, such as `Interval` or `Tx`, can't be used. - Expose names explicitly. `import Julc.Math exposing (..)` is rejected: ```text .../src/Main.elm:3:1: ELCI2104: Java modules cannot be imported with exposing (..); list the exported names you use 3| import Julc.Math exposing (..) ^^^^^^ ``` [Standard library](https://elci-lang.dev/advanced/standard-library/) lists what each library provides, and [Java libraries](https://elci-lang.dev/advanced/java-libraries/) covers your own Java code. ## Sharing code between projects There is no package manager. To reuse Elci code, keep it in its own source directory, for example a `lib` directory in a Git repository that several projects check out, and list that directory in each project's `source-directories`: ```toml title="elci.toml" [project] source-directories = ["src", "lib"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" ``` A module in `lib/Finance/Pricing.elm` is then `Finance.Pricing`, imported like any other: ```elm title="lib/Finance/Pricing.elm" module Finance.Pricing exposing (totalWithFee) totalWithFee : Int -> List Int -> Int totalWithFee fee prices = subtotal prices + fee subtotal : List Int -> Int subtotal prices = List.foldl (\price total -> price + total) 0 prices ``` ```elm title="src/Main.elm" module Main exposing (main) import Finance.Pricing exposing (totalWithFee) import Shop.Pricing as Pricing exposing (Tier(..)) main : Int main = totalWithFee 5 [ Pricing.priceOf Member (Pricing.Item "hat" 500) , Pricing.priceOf Standard { name = "scarf", price = 300 } ] ``` ```text 755 cpu 7570256, mem 30966 ``` The library is compiled together with each project that uses it. A module name may appear in only one source directory. The core module names (`Basics`, `List`, `Maybe`, `Result`, `String`, `Tuple`, `Map`, `Dict`, `Array`, `Value`) are taken, and `Set`, `Elci` and names starting with `Elci.` are reserved for the core library. The [elci-library example](https://github.com/bloxbean/elci-lang/tree/main/examples/elci-library) is a complete project. :::note The compiler only compiles modules that your entrypoint or handlers import, directly or indirectly. A module that nothing imports isn't checked, so import a new module before you rely on `elci check` to report its errors. ::: ## Next [Coming from Elm](https://elci-lang.dev/learn/coming-from-elm/) summarizes how Elci differs from Elm. If you're new to Elm too, you can go straight to [Validators and purposes](https://elci-lang.dev/contracts/validators/). --- # Coming from Elm > What carries over from Elm 0.19 to Elci and what changes, from the program model and the core library to Data encoding, Dict, Map and strict decoding. Source: https://elci-lang.dev/learn/coming-from-elm/ If you know Elm 0.19, you already know most of Elci's syntax. This page lists what carries over and every difference that matters, so you can skip the parts of the Learn section you don't need. ## What stays the same - The syntax of Elm 0.19.1: modules and `exposing`, `import … as … exposing`, `type` and `type alias`, `case`, `let`, `if`, lambdas, records, tuples of two or three elements, `|>`, `<|`, `>>` and `<<`. The [grammar](https://elci-lang.dev/reference/grammar/) defines it exactly and lists [the Elm syntax Elci rejects](https://elci-lang.dev/reference/grammar/#elm-syntax-that-elci-does-not-support). - Immutable values and pure functions. Evaluation is strict, as in Elm: arguments and `let` values are evaluated before use, and only the chosen branch of `if` and `case` runs. - Hindley–Milner type inference, generic functions (`a -> a`), and generic type aliases and custom types. - Exhaustive `case` checking. Redundant patterns are errors. - Local type annotations in `let`. - Records: field access, updates, destructuring and accessor functions such as `.price`, as in `List.map .price items`. A field of a value from another module reads as in Elm: `Config.settings.fee`. - The core library's default imports: `Basics` with `exposing (..)`, `List`, `Maybe` with `Maybe(..)`, `Result` with `Result(..)`, `String`, `Tuple` and `Debug`. Their functions have Elm's names, argument orders and edge cases: `max`, `compare`, `identity`, `Maybe.withDefault`, `List.head`, `List.range`, `List.sortBy`, `Result.andThen`, `Tuple.first`, `String.fromInt` and more. - Elm's name rules: your own declarations shadow imports, and two imports exposing one name make it ambiguous where you use it. - No typeclasses, with Elm's constrained type variables `comparable`, `appendable` and `compappend`. - Operators as functions, such as `List.foldl (+) 0`, and `^` for powers. - Types that nothing determines, as in `[] == []` or `Result.withDefault 0 (Ok 5)`, need no annotation. - `//` truncates toward zero, and `modBy` and `remainderBy` follow Elm's sign rules: `modBy` takes the sign of the divisor, `remainderBy` the sign of the dividend. - Negative arguments: `modBy 3 -7` passes `-7`, while `a - 1` and `a-1` subtract. - elm-format 0.8.8 style, apart from one pitfall described below. ## The program model Elm programs run in a browser. Elci programs are scripts that the Cardano ledger runs when it validates a transaction. | Elm | Elci | | --- | --- | | `main : Program flags model msg` | A handler function that returns `Bool`, named in a `[validators.]` table of `elci.toml` | | `Html`, `Browser`, `Cmd`, `Sub`, `Task`, ports | None. A script has no effects: it reads its inputs and approves or rejects | | `elm.json` and the package registry | `elci.toml`. No Elm packages; share code as source directories | | `elm make`, `elm repl`, `elm reactor` | `elci check`, `elci build`, `elci test`, and `elci eval` for a function project | | Runtime exceptions are rare | Failing is normal: any failure rejects the transaction | That last row changes how you think about errors. On-chain, failing is often the right answer: a malformed datum, a missing signature or a division by zero all reject the transaction, and nothing else happens. In Elm, `n // 0` evaluates to `0`. In Elci, it fails the script, as do `modBy 0` and `remainderBy 0`. ## Types and values | Topic | Elm | Elci | | --- | --- | --- | | Top-level signatures | Optional | **Required** on every top-level declaration (`ELCI2004`) | | `Int` | JavaScript number | Arbitrary precision, exact | | `Float`, `/`, `toFloat`, `round` | Available | **Not available.** Use `Int` with an explicit scale, such as lovelace or basis points | | `Char`, `'a'` | Available | Not available | | Hex literals (`0xFF`) | Available | Not available | | String escapes | Including `\u{…}`, and `"""` for multi-line | `\n`, `\r`, `\t`, `\"`, `\\` only | | `String` module | Full module | `fromInt`, `isEmpty`, `append`, `concat` and `join`. Use `Bytes.fromString` and `Bytes.toString` for bytes | | `String` order | By UTF-16 code units | By UTF-8 bytes, which is code point order. They differ only for characters outside the Basic Multilingual Plane | | `comparable`, `appendable`, `compappend` | Constrained type variables | The same. `Bytes` and ledger hashes such as `PubKeyHash` are also comparable | | `number` | `Int` or `Float` | Not available: write `Int` | | `2 ^ -1` | `0.5` | Fails the script | | `==` on functions | Runtime crash | Compile error, as for `Map`. `==` on `Data` compares structure | | Extensible records `{ r \| x : Int }` | Available | Not available. Records are closed | | Let-polymorphism | Values and functions | Only for non-expansive bindings (functions, lambdas, plain values) | | Undetermined type variables | Allowed | Allowed, except where a value of the type is created: `Julc.Data.decode` and generic Java library functions need an annotation | Constrained type variables follow Elm's rule, strictly: a signature variable whose values are compared must be named `comparable…`, and one whose values are appended `appendable…`. Elci reports the missing name: ```elm joinTwo : a -> a -> a joinTwo left right = left ++ right ``` ```text .../src/Main.elm:6:10: ELCI2003: The operator ++ appends values of type `a`, but the annotation of `joinTwo` says `a` can be any type. Rename it `appendable` (as in Elm) to require String or List values 6| left ++ right ^^ ``` With `joinTwo : appendable -> appendable -> appendable`, both `joinTwo "a" "b"` and `joinTwo [ 1 ] [ 2 ]` compile, and `joinTwo 1 2` doesn't. ## The core library Elci's core library is Elm's, imported by default in every module: | Module | What Elci has | | --- | --- | | `Basics` | `negate`, `abs`, `min`, `max`, `clamp`, `compare` and `Order`, `identity`, `always`, `not`, `xor`, `modBy`, `remainderBy`, `^` | | `List` | all of Elm's `List` except `map4` and `map5` | | `Maybe` | `withDefault`, `map`, `map2`, `map3`, `andThen` | | `Result` | `Ok`, `Err`, `map`, `map2`, `map3`, `andThen`, `withDefault`, `toMaybe`, `fromMaybe`, `mapError` | | `Tuple` | all of Elm's `Tuple` | | `String` | `fromInt`, `isEmpty`, `append`, `concat`, `join` | | `Debug` | `trace` | [Core modules](https://elci-lang.dev/reference/core-modules/) has every signature. What is still missing: | Elm | In Elci | | --- | --- | | `toFloat`, `round`, `sqrt`, `pi` and the other `Float` functions | Not available: there is no `Float` | | `map4` and `map5` in `List`, `Maybe` and `Result` | Not available. Nest `map3`, or use `case` | | `String.length`, `toInt`, `split`, `slice`, `fromFloat`… | Not available. `Bytes.length (Bytes.fromString s)` is the byte length | | `Char` | Not available | | `Set` | Not available. Use a `Dict k Bool` or a `List` | | `Debug.log`, `Debug.todo`, `Debug.toString` | `Debug.trace : String -> a -> a`, and `String.fromInt` for numbers | A name Elci lacks is an error that says what to use or lists what the module has: ```text .../src/Main.elm:6:5: ELCI2104: Elci has no Debug.log; use Debug.trace message value, which returns value .../src/Main.elm:6:5: ELCI2104: Elci's String module has no toInt. It has append, concat, fromInt, isEmpty, join ``` One import rule differs from Elm: a name you list explicitly, as in `import Utils exposing (clamp)`, takes precedence over the default import of `Basics.clamp` instead of being ambiguous. Modules outside the default imports, `Bytes`, `Dict`, `Map`, `Array`, `Value` and `Julc.Data`, need their own imports, as `Dict` and `Array` do in Elm. ## Dict, Map and Array `Dict` has all of Elm's `Dict` API, but it is stored differently: | | Elm `Dict` | Elci `Dict` | | --- | --- | --- | | Import | `import Dict exposing (Dict)` | The same; the type `Dict k v` is always in scope | | Keys | `comparable` | `comparable`, which includes `Bytes` and ledger hashes such as `PubKeyHash` | | Structure | Balanced tree | A Plutus map with its keys in ascending order | | `get`, `insert`, `remove` | O(log n) | O(n): they stop at the first key that isn't smaller | | `fromList` | O(n log n) | O(n log n), and O(n) for sorted input | | Order of `keys`, `toList` and the folds | Ascending keys | The same | | `==` | Compares bindings | The same | In a datum, redeemer or parameter, a `Dict` is a Plutus map with sorted keys, and a map whose keys are out of order or repeated fails to decode. Elci also has `Map`, which Elm doesn't. A `Map k v` is the ledger's own map: an unsorted association list whose keys may be any type with a Data encoding, such as `Credential`. The transaction's maps, such as `txInfo.withdrawals` and `txInfo.redeemers`, are `Map`s. A map keeps its own order (`insert` puts a key first), has no `==`, and `Map.fromList` is O(n²). Use `Dict` for keyed data of your own, and `Map` where the ledger or a key that isn't comparable requires it. `Array` resembles Elm's `Array`: `get` returns a `Maybe` and `slice` accepts negative indexes. It has no `set`, `push` or `append`, and an array can't be stored in data (datums, redeemers, records, lists and so on). Build a `List` and convert it once with `Array.fromList`. Elci also has a `Value` module, for the ledger's amounts of ada and native tokens, such as an output's value and the transaction's mint. It has no Elm counterpart; see [`Value`](https://elci-lang.dev/reference/core-modules/#value). ## Your data is Plutus Data In Elm, you convert values to JSON with hand-written encoders and decoders. In Elci, every datum, redeemer and parameter is **Plutus Data**, and the encoding is derived from your types: | Type | Encoding | | --- | --- | | Record | Constructor 0, fields **sorted by field name** | | Custom type | Constructor numbered by declaration order (first is 0), with its values as fields | | Tuple | Constructor 0, fields in position order | | `Bool` | `False` is constructor 0, `True` is constructor 1 | | `Maybe a` | `Nothing` is constructor 0, `Just` is constructor 1 | | `Result e a` | `Ok` is constructor 0, `Err` is constructor 1: the reverse of PlutusTx's `Either` | | `List a` | A Data list | | `Dict k v` | A Data map with its keys in ascending order | | `Map k v` | A Data map, in the map's order | | `Int`, `Bytes`, `String` | An integer, a byte string, and UTF-8 bytes | `Maybe`'s tags are the reverse of those used by Julc's Java `Optional` and the ledger's own optional values, where the present value is constructor 0. They aren't interchangeable: `Julc.Data.fromOptional` and `toOptional` convert between them. You can see Elci's tags by encoding a list of `Maybe` values: ```elm title="src/Main.elm" module Main exposing (main) import Julc.Data as Data main : Data main = Data.encode [ Nothing, Just 7 ] ``` ```text [Constr 0, Constr 1 [7]] cpu 924368, mem 4120 ``` Handlers receive their datum and redeemer already decoded. When you hold raw `Data`, such as an inline datum in a transaction output, convert it explicitly with `Julc.Data`: ```elm title="src/Main.elm" module Main exposing (main) import Julc.Data as Data type alias Payout = { beneficiary : String , amount : Int } main : Int main = let raw = Data.encode (Payout "alice" 50) decoded : Payout decoded = Data.decode raw in decoded.amount ``` ```text 50 cpu 4820844, mem 21097 ``` Unlike `Json.Decode.decodeValue`, `Data.decode` doesn't return a `Result`. Malformed data fails the script, which rejects the transaction. Annotate the target type (`decoded : Payout`) when inference can't tell what to decode. [Datums and redeemers](https://elci-lang.dev/contracts/datums-and-redeemers/) covers the rules. ## Error messages Elci's error messages follow Elm's: they quote the line of source, say what is wrong in terms of your program and suggest a fix. ```elm label : Int -> String label count = case count of 0 -> "none" 1 -> "one" _ -> count ``` ```text .../src/Main.elm:14:13: ELCI2002: The 3rd branch of this case is `Int`, but the branches before it are `String` 14| count ^^^^^ Hint: `String.fromInt` turns an Int into a String ``` As in Elm: - A type mismatch names the argument, branch, list element, operator side, annotation or field whose type differs, and writes types as in source. - A `case` that doesn't cover every value lists the missing patterns, such as `Nothing` and `Just (Err _)`. - A misspelt name gets a suggestion, such as ``did you mean `List.length`?``. - A run reports every error it finds, one per top-level declaration, in source order. Type errors are reported once there are no syntax, import or qualified-name errors. What differs: - The first line is `path:line:column: ELCIxxxx: message`, not a `-- TYPE MISMATCH --` banner. Editors and scripts parse this line, and each code keeps its meaning between releases. - `--report=json` prints each error as one JSON object on its own line, and its fields are Elci's own: `code`, `path`, `line`, `column`, `message`, `hints`, `excerpt` and `context`. See [diagnostics](https://elci-lang.dev/reference/diagnostics/#json-reports-for-tools). - `elci explain ELCI2002` prints a longer explanation of a code, with an example and its fix. ## An elm-format pitfall elm-format 0.8.8 is Elci's formatter, but it stores integer literals as 64-bit numbers. It silently changes a literal larger than about 9.2 × 10¹⁸ into a different number. Elci's `Int` has no limit, so compute such values instead, for example `1000000000 * 1000000000 * 1000000000`. ## Tooling - The repository has a [tree-sitter grammar](https://github.com/bloxbean/elci-lang/tree/main/tree-sitter-elci) for highlighting, folding and indentation in Neovim, Helix and Zed. - There is no Elci language server. The Elm language server doesn't know Elci's modules, so trust `elci check` over its diagnostics. An editor integration can run `elci check --report=json` and read each error's location, message and hints. - `elci eval` runs a function project and shows the result and its cost, which is the closest thing to `elm repl`. [Values, functions and types](https://elci-lang.dev/learn/basics/) shows how to set one up. - `elci test` replaces elm-test, and adds transaction scenarios and property tests. See [Unit tests](https://elci-lang.dev/testing/unit-tests/). --- # Validators and purposes > Declare validators in elci.toml, give each purpose a handler with the right signature, and let one script serve several purposes. Source: https://elci-lang.dev/contracts/validators/ This page shows how a validator is declared, which purposes it can handle, and the exact signature each handler needs. You also build one script that both holds funds and mints tokens. ## Purposes The Cardano ledger runs a script whenever a transaction does something the script guards. That reason is the script's **purpose**. Elci supports all six Plutus V3 purposes, and each has a handler key in `elci.toml`: | Handler key | The ledger runs the script when a transaction... | | --- | --- | | `spend` | spends an output locked at the script's address | | `mint` | mints or burns tokens under the script's policy id | | `withdraw` | withdraws staking rewards from the script's stake credential | | `certify` | publishes a certificate for the script's credential, such as a stake registration | | `vote` | casts a governance vote with the script as the voter | | `propose` | submits a governance proposal that the script guards | A script is identified by its **hash**. The same hash serves as the payment credential of its address, as its minting policy id and as its stake credential, so one script can guard all of these at once. ## Declaring a validator A validator is a `[validators.]` table in `elci.toml`. Each table becomes **one script** with one hash. Its handler keys point at exposed functions, written as `Module.function`: ```toml title="elci.toml" [validators.vault] boundary = "julc-strict-v1" parameters = ["owner"] spend = "Vault.spend" mint = "Vault.mint" ``` | Key | Required | Meaning | | --- | --- | --- | | `boundary` | yes | How data entering the script is decoded. `"julc-strict-v1"` is the only policy: every datum and redeemer is checked strictly before the handler runs. | | `spend`, `mint`, `withdraw`, `certify`, `vote`, `propose` | at least one | The handler for that purpose, as `"Module.function"`. Each key may appear once. | | `parameters` | no | Deployment parameter names, in the order the handlers receive them. See [Parameterized validators](https://elci-lang.dev/contracts/parameters/). | | `spend-datum` | no | `"required"` (the default), `"optional"` or `"absent"`. Only with a `spend` handler. See [Datums and redeemers](https://elci-lang.dev/contracts/datums-and-redeemers/). | | `expected-abi` | no | A committed `abi.json` to compare against before anything is written. See [Build and deploy](https://elci-lang.dev/contracts/build-and-deploy/). | The name after `validators.` is also the output directory, `build/validators//`. It must start with a lowercase letter and use only lowercase letters, digits, `-` and `_`, up to 64 characters. The source code never says how a function is deployed; only the configuration does. ## Handler signatures Each handler is an ordinary function that returns `Bool`. Its arguments come in a fixed order: the deployment parameters, then the datum (spending only), then the redeemer, then the `ScriptContext`. | Handler key | Required type | | --- | --- | | `spend` | `parameters -> datum -> redeemer -> ScriptContext -> Bool` | | `spend` with `spend-datum = "optional"` | `parameters -> Maybe datum -> redeemer -> ScriptContext -> Bool` | | `spend` with `spend-datum = "absent"` | `parameters -> redeemer -> ScriptContext -> Bool` | | `mint`, `withdraw`, `certify`, `vote`, `propose` | `parameters -> redeemer -> ScriptContext -> Bool` | `parameters` stands for zero or more leading arguments, one per name in `parameters = [...]`. You choose the datum and redeemer types; use `()` when you don't need one. Returning `True` approves the transaction. Returning `False`, or failing during evaluation (a failed decode, a division by zero), rejects it. The handler must be exposed by its module, and must be a concrete function: its signature can't keep type variables such as `a`. ## One script, several purposes Here is a vault that holds funds and mints tokens under its own policy id. Both handlers live in one module and share a helper: ```toml title="elci.toml" [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" # One script: it holds funds and mints receipts under its own policy. [validators.vault] boundary = "julc-strict-v1" parameters = ["owner"] spend = "Vault.spend" mint = "Vault.mint" ``` ```elm title="src/Vault.elm" module Vault exposing (mint, spend) import Julc.Ledger exposing (PubKeyHash, ScriptContext) {-| Spending: the owner may take anything held at the vault's address. -} spend : PubKeyHash -> () -> () -> ScriptContext -> Bool spend owner _ _ ctx = signedBy owner ctx {-| Minting: the owner may mint or burn tokens under the vault's policy id. -} mint : PubKeyHash -> () -> ScriptContext -> Bool mint owner _ ctx = signedBy owner ctx signedBy : PubKeyHash -> ScriptContext -> Bool signedBy key ctx = List.any (\signer -> signer == key) ctx.txInfo.signatories ``` The two signatures differ: `spend` takes a datum and a redeemer, `mint` only a redeemer. Both start with the `owner` parameter, because every handler of a validator receives all of its parameters. ```sh elci check . ``` ```text validator vault (mint, spend): checked; 389 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` The check line lists the purposes the script serves. Test each purpose with a [transaction scenario](https://elci-lang.dev/testing/scenarios/), including one the vault doesn't handle: ```elm title="tests/VaultTest.elm" module VaultTest exposing (ownerMints, ownerSpends, strangerCannotSpend, withdrawalsAreNotHandled) import Bytes import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Scenario exposing (Scenario) owner : PubKeyHash owner = Scenario.key "owner" ownerSpends : Scenario ownerSpends = Scenario.spend "vault" (Scenario.outRef "vault" 0) Scenario.unit |> Scenario.parameters [ Data.encode owner ] |> Scenario.signedBy owner strangerCannotSpend : Scenario strangerCannotSpend = Scenario.spend "vault" (Scenario.outRef "vault" 0) Scenario.unit |> Scenario.parameters [ Data.encode owner ] |> Scenario.signedBy (Scenario.key "stranger") |> Scenario.expectFailure ownerMints : Scenario ownerMints = Scenario.mint "vault" |> Scenario.parameters [ Data.encode owner ] |> Scenario.minting (Bytes.fromString "RECEIPT") 1 |> Scenario.signedBy owner {-| The vault has no withdraw handler, so the script rejects every withdrawal. -} withdrawalsAreNotHandled : Scenario withdrawalsAreNotHandled = Scenario.withdraw "vault" |> Scenario.parameters [ Data.encode owner ] |> Scenario.signedBy owner |> Scenario.expectFailure ``` ```sh elci test . ``` ```text PASS VaultTest.ownerSpends (cpu 8015496, mem 31791) PASS VaultTest.strangerCannotSpend (cpu 8393929, mem 33723) PASS VaultTest.ownerMints (cpu 5954941, mem 23732) PASS VaultTest.withdrawalsAreNotHandled (cpu 2341260, mem 9622) 4 passed, 0 failed ``` ### How dispatch works On-chain, a Plutus V3 script receives a single argument: the whole script context. Elci generates the code around your handlers that turns it into the arguments you declared: 1. The deployment parameters are decoded once, when the script is applied to them. 2. The script checks the purpose and selects the matching handler. A purpose without a handler fails immediately, which is why the withdrawal above is rejected even though the owner signed it. 3. It decodes the selected handler's datum and redeemer. Decoding is strict: data of the wrong shape fails the script. 4. It calls the handler and turns `False` into a failure. Because only the selected handler's datum and redeemer are decoded, different handlers can use different redeemer types. The purpose check runs even for a single handler. It costs roughly 1.1M CPU units and 35 bytes of script, and it means a script never runs a handler for a purpose you didn't configure. Adding, removing or changing a handler changes the script, and so its hash, address and policy id. ### Top-level values run for every purpose Top-level values that any handler uses are evaluated once, when the script starts and before dispatch. A top-level value that fails therefore fails **every** purpose. Suppose `mint` used this limit: ```elm mint : PubKeyHash -> () -> ScriptContext -> Bool mint owner _ ctx = (limit > 0) && signedBy owner ctx limit : Int limit = 100 // 0 ``` Spending breaks too, although `spend` never reads `limit`: ```text FAIL VaultTest.ownerSpends (cpu 372441, mem 1601) validator vault rejected the transaction: QuotientInteger: division by zero PASS VaultTest.strangerCannotSpend (cpu 372441, mem 1601) FAIL VaultTest.ownerMints (cpu 372441, mem 1601) validator vault rejected the transaction: QuotientInteger: division by zero PASS VaultTest.withdrawalsAreNotHandled (cpu 372441, mem 1601) 2 passed, 2 failed ``` The two tests that expect a rejection still pass, but only because every transaction now fails. Keep work that belongs to one purpose inside its handler. Top-level values also cost budget in every transaction, so keep them cheap. ## Several validators in one project Add more tables for independent scripts. Each has its own hash, its own outputs under `build/validators//` and its own parameters. Modules can be shared freely; each script includes only the code its handlers reach. ```toml [validators.treasury] boundary = "julc-strict-v1" parameters = ["admin"] spend = "Treasury.spend" withdraw = "Treasury.withdraw" [validators.badge] boundary = "julc-strict-v1" parameters = ["admin"] mint = "Badge.mint" ``` `elci check` and `elci build` process every validator in name order. Select some with `--validator`, which you can repeat: ```sh elci build . --validator badge ``` The [treasury tutorial](https://elci-lang.dev/tutorials/treasury/) builds this project step by step. ## When a handler doesn't fit The compiler checks every handler against its table. The errors name the `elci.toml` line and table, then the source location. If the signature doesn't match the purpose, you get `ELCI2014`. Here `mint` was given a datum argument, which mint handlers don't receive: ```elm mint : PubKeyHash -> () -> () -> ScriptContext -> Bool mint owner _ _ ctx = signedBy owner ctx ``` ```text .../vault/src/Vault.elm:15:1: ELCI2014: Handler Vault.mint does not have the mint signature owner -> redeemer -> ScriptContext -> Bool that [validators.vault] needs (1 parameter); its type is PubKeyHash -> () -> () -> ScriptContext -> Bool; it has `()` where the configuration needs `ScriptContext` 15| mint : PubKeyHash -> () -> () -> ScriptContext -> Bool ^^^^ In .../vault/elci.toml:7: [validators.vault] ``` The message gives the signature the table needs, with its parameter count, and the handler's actual type. A mint handler takes the parameters, a redeemer and the context, so the extra `()` puts a `()` where the `ScriptContext` should be. A handler that its module doesn't expose is `ELCI2103`: ```text .../vault/src/Vault.elm:15:1: ELCI2103: Handler Vault.mint must be exposed by its module 15| mint : PubKeyHash -> () -> ScriptContext -> Bool ^^^^ In .../vault/elci.toml:7: [validators.vault] ``` And a handler key that names a missing function: ```text elci: ELCI4001: .../vault/elci.toml:7: [validators.vault]: Handler Vault.spent for spend: module Vault does not declare spent ``` The [diagnostics reference](https://elci-lang.dev/reference/diagnostics/) lists every error code. ## Starting from a template `elci init` can generate a starter project for any purpose. The template accepts transactions signed by an `owner` parameter and comes with two passing scenarios: ```sh elci init my-policy --purpose mint elci init rewards --validator rewards --purpose withdraw ``` `--purpose` accepts `spend` (the default), `mint`, `withdraw`, `certify`, `vote` and `propose`; `function` creates an `[entrypoint]` project for `elci eval` instead of a validator. `--validator` names the table (default `main`). The purpose only selects the starter code: add handler keys to the table at any time. Next, see what a handler can read from the transaction in [Reading the transaction](https://elci-lang.dev/contracts/reading-the-transaction/). --- # Reading the transaction > What a handler can read from the ScriptContext, the ledger types behind it, and verified recipes with the Tx and Interval modules for signers, validity ranges, inputs, payments, datums and minting, including the defence against double satisfaction. Source: https://elci-lang.dev/contracts/reading-the-transaction/ The last argument of every handler is the `ScriptContext`: the whole transaction being validated, as the ledger describes it. This page lists what you can read from it, explains the ledger types, and gives small recipes for the checks most contracts need, written with the core `Tx` and `Interval` modules. ## The script context `ScriptContext` is a record with three fields: | Field | Type | Contents | | --- | --- | --- | | `ctx.txInfo` | `TxInfo` | The transaction: inputs, outputs, signers, validity range, minted value and more | | `ctx.scriptInfo` | `ScriptInfo` | Why this script is running, with the purpose's details (the spent output reference, the policy id, ...) | | `ctx.redeemer` | `Data` | The raw redeemer. You rarely need it: the handler already receives it decoded | Import the types you mention by name from `Julc.Ledger`: ```elm import Julc.Ledger exposing (Credential(..), PubKeyHash, ScriptContext, TxOut) ``` Exposing a type with `(..)`, as in `Credential(..)`, also exposes its constructors. `import Julc.Ledger exposing (..)` exposes the types only; you then write constructors qualified, such as `Ledger.PubKeyCredential` after `import Julc.Ledger as Ledger`. ## Transaction fields These are the fields of `ctx.txInfo`, with the Elci types the compiler gives them: | Field | Type | Contents | | --- | --- | --- | | `inputs` | `List TxInInfo` | The outputs this transaction spends | | `referenceInputs` | `List TxInInfo` | Outputs it reads without spending | | `outputs` | `List TxOut` | The outputs it creates, in order | | `fee` | `Int` | The fee, in lovelace | | `mint` | `Value` | Tokens minted (positive) and burned (negative) | | `certificates` | `List TxCert` | Certificates it publishes | | `withdrawals` | `Map Credential Int` | Staking rewards withdrawn, in lovelace | | `validRange` | `Interval` | The POSIX time range (milliseconds) in which the transaction is valid | | `signatories` | `List PubKeyHash` | The key hashes of its required signers | | `redeemers` | `Map ScriptPurpose Data` | The redeemer of every script it runs | | `datums` | `Map DatumHash Data` | Datums supplied in the witness set | | `id` | `TxId` | The transaction's id | | `votes` | `Map Voter (Map GovernanceActionId Vote)` | Governance votes | | `proposalProcedures` | `List ProposalProcedure` | Governance proposals | | `currentTreasuryAmount` | `JulcOptional Int` | The treasury amount it states, if any | | `treasuryDonation` | `JulcOptional Int` | Its treasury donation, if any | `JulcOptional` is Julc's optional value. Convert it to a `Maybe` with `Julc.Data.fromOptional` before matching on it: ```elm import Julc.Data as Data donation : ScriptContext -> Maybe Int donation ctx = Data.fromOptional ctx.txInfo.treasuryDonation ``` `Map` fields work with the bundled `Map` module (`import Map`). They are the ledger's own maps: their keys, such as `Credential` and `ScriptPurpose`, are not comparable, so they are not `Dict`s. ## Ledger types The records and custom types you meet while walking the context: | Type | Shape | | --- | --- | | `TxInInfo` | `{ outRef : TxOutRef, resolved : TxOut }`: a spent output and where it came from | | `TxOut` | `{ address : Address, value : Value, datum : OutputDatum, referenceScript : JulcOptional ScriptHash }` | | `TxOutRef` | `{ txId : TxId, index : Int }`: a transaction id and an output index | | `Address` | `{ credential : Credential, stakingCredential : JulcOptional StakingCredential }` | | `Credential` | `PubKeyCredential PubKeyHash` or `ScriptCredential ScriptHash` | | `StakingCredential` | `StakingHash Credential` or `StakingPtr Int Int Int` | | `OutputDatum` | `NoOutputDatum`, `OutputDatumHash DatumHash` or `OutputDatumInline Data` | | `Interval` | `{ from : IntervalBound, to : IntervalBound }` | | `IntervalBound` | `{ boundType : IntervalBoundType, isInclusive : Bool }` | | `IntervalBoundType` | `NegInf`, `Finite Int` or `PosInf` | | `ScriptInfo` | `MintingScript PolicyId`, `SpendingScript TxOutRef (JulcOptional Data)`, `RewardingScript Credential`, `CertifyingScript Int TxCert`, `VotingScript Voter` or `ProposingScript Int ProposalProcedure` | Hashes have their own types: `PubKeyHash`, `ScriptHash`, `ValidatorHash`, `PolicyId`, `TokenName`, `TxId` and `DatumHash`. They are byte strings underneath, but the compiler keeps them apart, so a key hash can't be mistaken for arbitrary bytes. This helper takes the key as `Bytes`: ```elm signedByBytes : Bytes -> ScriptContext -> Bool signedByBytes key ctx = List.any (\signer -> signer == key) ctx.txInfo.signatories ``` ```text .../my-project/src/Main.elm:8:51: ELCI2002: The field `signatories` is `List PubKeyHash`, but it is used here as `List Bytes` 8| List.any (\signer -> signer == key) ctx.txInfo.signatories ^ Hint: the `.hash` field is the Bytes inside a PubKeyHash In .../my-project/elci.toml:9: [validators.main] ``` Take a `PubKeyHash` instead, or use the `.hash` field to get the bytes when you really need them: `policy.hash` for a `PolicyId`, and `token.hash` for a `TokenName`, which has no other field. What you can do with a ledger type is decided per type: - **Fields**: read record fields with `.field`, including nested ones such as `output.address.credential`. Accessor functions work too: `List.map .resolved ctx.txInfo.inputs` is the list of spent outputs. - **Patterns and constructors**: match on custom types in `case`, and build values such as `Ledger.TxOutRef txId 0`. - **Equality**: `==` and `/=` work on hashes, `TxOutRef`, `Credential`, `Address`, `Interval`, `OutputDatum`, `ScriptInfo`, `Value`, `Data` and the other plain ledger types. A `JulcOptional` has no equality: convert it with `Data.fromOptional` and compare the `Maybe`. - **Records that contain a `Value`** (`TxOut`, `TxInInfo`, `TxInfo`) have no equality: compare their fields. Neither they nor a `Value` can be a datum, redeemer or parameter. - **`Value` and `Interval` have their own modules.** Read, build and compare values with the core [`Value`](https://elci-lang.dev/reference/core-modules/#value) module and time ranges with the [`Interval`](https://elci-lang.dev/reference/core-modules/#interval) module, both shown below. ## Recipes The core `Tx` and `Interval` modules answer the questions most contracts ask. They need an import and no configuration: ```elm import Interval import Tx ``` Each recipe is a check to use in a handler, or a small function to put next to it. The [core modules](https://elci-lang.dev/reference/core-modules/#tx) reference lists every function with its cost. Test the rules you build from them with [transaction scenarios](https://elci-lang.dev/testing/scenarios/). ### Who signed `signatories` lists the key hashes that must sign the transaction. `Tx.signedBy` searches it: ```elm Tx.signedBy owner ctx ``` ### When the transaction runs A script can't read the clock. Instead, the transaction declares a validity range and the ledger only accepts it inside that range, so the script reasons about the range with the `Interval` module: | Check | Holds when | | --- | --- | | `Interval.isEntirelyAfter deadline ctx.txInfo.validRange` | the transaction can't run at or before `deadline` | | `Interval.isEntirelyBefore deadline ctx.txInfo.validRange` | the transaction can't run at or after `deadline` | | `Interval.contains time ctx.txInfo.validRange` | `time` lies in the range | The two deadline checks never hold on an infinite bound. A transaction without a validity range could run at any time, so it fails both. "After" and "before" are strict, and the checks honor inclusive and exclusive bounds. A scenario's `Scenario.validFrom t` is inclusive, so `isEntirelyAfter deadline` accepts `validFrom (deadline + 1)` but not `validFrom deadline`; check `isEntirelyAfter (deadline - 1)` to accept a transaction from the deadline itself. `Scenario.validBefore t` is exclusive, so `isEntirelyBefore deadline` accepts `validBefore deadline`. The [vesting tutorial](https://elci-lang.dev/tutorials/vesting/) builds a deadline check step by step. ### Which outputs are spent `inputs` holds every spent output with its reference. `Tx.spends` asks whether the transaction spends a particular output, and `Tx.findInput` returns it as a `Maybe TxInInfo`: ```elm Tx.spends ref ctx ``` Reference inputs are read, not spent, so they don't count; they are in `ctx.txInfo.referenceInputs`. The [one-shot minting policy](https://elci-lang.dev/tutorials/one-shot-policy/) is built on this check. ### The output being spent When spending, `Tx.ownRef ctx` is the reference of the output being validated, and `Tx.ownInput ctx` is that output with its address, value and datum. For other purposes both are `Nothing`: ```elm ownLovelace : ScriptContext -> Int ownLovelace ctx = case Tx.ownInput ctx of Just own -> Value.lovelaceOf own.resolved.value Nothing -> 0 ``` `Tx.continuingOutputs ctx` lists the outputs paid back to the spent output's address, staking part included: the outputs that keep funds at the script. ### Payments `Tx.outputsTo credential ctx` lists the outputs paid to a key or a script, in order, and `Tx.valuePaidTo credential ctx` adds up their value: ```elm Tx.outputsTo (PubKeyCredential seller) ctx ``` Both match the address's payment credential and ignore its staking part: whoever builds the transaction chooses the payee's staking part. So a payment to the seller's key counts whatever staking credential it carries. When the staking part matters, store the payee's full `Address` and compare `output.address`. Before you check a total, read [one payment per input](#one-payment-per-input). ### The script's own policy id When minting, `Tx.ownPolicy ctx` is the policy id, which is the script's own hash, and `Tx.mintedOf policy ctx` holds the tokens minted (positive) and burned (negative) under it, as a `Dict TokenName Int`: ```elm {-| The transaction mints exactly one BADGE under this policy, and nothing else under it. -} mintsOneBadge : ScriptContext -> Bool mintsOneBadge ctx = case Tx.ownPolicy ctx of Just policy -> Dict.toList (Tx.mintedOf policy ctx) == [ ( TokenName (Bytes.fromString "BADGE"), 1 ) ] Nothing -> False ``` Comparing the whole dictionary also rejects a transaction that mints a second token name under the policy. Build a token name from bytes after `import Julc.Ledger exposing (TokenName(..))`. Like the other ledger hashes, a token name exposes its bytes as its `.hash` field. ### Amounts and tokens An output's `value` and the transaction's `mint` are `Value`s: lovelace and quantities of native tokens. Read them with the core `Value` module (`import Value`). A token is named by a `PolicyId` and a `TokenName`. | Function | Returns | | --- | --- | | `Value.lovelaceOf value` | the lovelace in `value` | | `Value.quantityOf policy name value` | the quantity of one token; negative in a mint that burns it, `0` if absent | | `Value.tokens policy value` | the tokens of one policy and their quantities, as a `Dict TokenName Int` | | `Value.geq value required` | whether `value` holds at least as much of every token as `required` | | `Value.isZero value` | whether `value` holds nothing | Values have `==`, and it is exact: two values are equal when they hold the same quantities. So `output.value == Value.lovelace 2000000` checks that an output holds exactly 2 ada and nothing else. `Value.positive` and `Value.negative` split a mint into what it mints and what it burns. The [core modules](https://elci-lang.dev/reference/core-modules/#value) reference lists every function. ### Datums on other outputs An output's datum arrives as `OutputDatum`. `Tx.inlineDatum output` is its inline datum, as `Maybe Data`. `Tx.datumOf output ctx` also finds a datum given by its hash among the datums the transaction supplies. Decode the `Data` into your own type with `Julc.Data.decode`, annotating the result type. Decoding is strict, so a datum of the wrong shape fails the whole script rather than producing `Nothing`: ```elm type alias Order = { owner : PubKeyHash , price : Int } orderOf : TxOut -> Maybe Order orderOf output = Maybe.map Data.decode (Tx.inlineDatum output) ``` To check that an output carries a datum you expect, compare encodings. `==` on `Data` compares structure, and nothing is decoded, so a datum of another shape makes the check `False`: ```elm Tx.inlineDatum output == Just (Data.encode expected) ``` Off-chain code must then build the datum exactly as Elci encodes it, with a record's fields in name order. The validator's `abi.json` records the layout. ### Withdrawals `withdrawals` is a map from credentials to lovelace: ```elm withdrawn : Credential -> ScriptContext -> Int withdrawn credential ctx = case Map.get credential ctx.txInfo.withdrawals of Just amount -> amount Nothing -> 0 ``` ### One payment per input A check such as "some output pays the seller the price" does not say which input the output pays for. When a transaction spends two outputs of your script that are both for the same seller, the script runs once for each, and both runs find the same payment. The seller is paid once for two sales. This is the **double-satisfaction** attack. There are two defences. **Tag each payment** with the output reference of the input it pays for, as its inline datum. An output has one datum, so each input needs its own payment: ```elm paidFor : PubKeyHash -> Int -> ScriptContext -> Bool paidFor seller price ctx = case Tx.ownRef ctx of Just ref -> List.any (\output -> (Value.lovelaceOf output.value >= price) && (Tx.inlineDatum output == Just (Data.encode ref)) ) (Tx.outputsTo (PubKeyCredential seller) ctx) Nothing -> False ``` **Allow one input of the script per transaction** when an output can't carry a tag, for example an output that returns funds to the script under the script's own datum: ```elm onlyScriptInput : TxInInfo -> ScriptContext -> Bool onlyScriptInput own ctx = List.length (List.filter (\input -> input.resolved.address.credential == own.resolved.address.credential) ctx.txInfo.inputs ) == 1 ``` Call it with the input from `Tx.ownInput ctx`. When funds return to the script, check the whole value that stays, not only the token you care about: an output that keeps the tokens but not the lovelace lets the spender take the rest. `Value.geq output.value expected` requires at least `expected`. To also stop a spender from padding the output with other tokens, require the tokens exactly: `Value.withoutLovelace output.value == Value.withoutLovelace expected`, with `Value.lovelaceOf` compared by `>=`. The [escrow, marketplace and order-swap examples](https://elci-lang.dev/reference/examples/) use these defences, and each has a scenario that shows the attack failing. ## A complete example: settling an invoice This validator locks an invoice. Anyone may spend it, but only in a transaction that pays the payee the full amount and that is guaranteed to run before the due date. The payment carries the invoice's output reference, so one payment can't settle two invoices. ```toml title="elci.toml" [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" [validators.invoice] boundary = "julc-strict-v1" spend = "Invoice.spend" ``` ```elm title="src/Invoice.elm" module Invoice exposing (Invoice, spend) import Interval import Julc.Data as Data import Julc.Ledger exposing (Credential(..), PubKeyHash, ScriptContext, TxOut, TxOutRef) import Tx import Value type alias Invoice = { payee : PubKeyHash , amount : Int , due : Int } {-| Anyone may settle an invoice by paying the payee in full before it is due. -} spend : Invoice -> () -> ScriptContext -> Bool spend invoice _ ctx = case Tx.ownRef ctx of Just ref -> Interval.isEntirelyBefore invoice.due ctx.txInfo.validRange && List.any (settles invoice ref) (Tx.outputsTo (PubKeyCredential invoice.payee) ctx) Nothing -> False {-| The output pays the full amount and carries the invoice's output reference. -} settles : Invoice -> TxOutRef -> TxOut -> Bool settles invoice ref output = (Value.lovelaceOf output.value >= invoice.amount) && (Tx.inlineDatum output == Just (Data.encode ref)) ``` - `Tx.ownRef` is the invoice's output reference. It is `Nothing` only when the script runs for another purpose, which this validator doesn't handle. - `Interval.isEntirelyBefore` rejects a transaction without an end to its validity range, which could run at any time. - `Tx.outputsTo` ignores the staking part of the address, so any address of the payee's key counts. The tests pay the payee with `Scenario.toKey`, an output to the key's address without a staking part, and tag the payment with `Scenario.withDatum`: ```elm title="tests/InvoiceTest.elm" module InvoiceTest exposing ( latePaymentsFail , openEndedPaymentsFail , paymentForAnotherInvoiceFails , paymentSettles , shortPaymentsFail , untaggedPaymentsFail ) import Invoice exposing (Invoice) import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Scenario exposing (Output, Scenario) payee : PubKeyHash payee = Scenario.key "payee" invoice : Invoice invoice = { payee = payee, amount = 5000000, due = 10000 } {-| A payment to the payee, tagged with the output reference of invoice `index`. -} payment : Int -> Int -> Output payment index lovelace = Scenario.toKey payee lovelace |> Scenario.withDatum (Data.encode (Scenario.outRef "invoice" index)) settle : Output -> Scenario settle paid = Scenario.spend "invoice" (Scenario.outRef "invoice" 0) (Data.encode invoice) |> Scenario.output paid paymentSettles : Scenario paymentSettles = settle (payment 0 5000000) |> Scenario.validBefore 9000 shortPaymentsFail : Scenario shortPaymentsFail = settle (payment 0 4999999) |> Scenario.validBefore 9000 |> Scenario.expectFailure latePaymentsFail : Scenario latePaymentsFail = settle (payment 0 5000000) |> Scenario.validBefore 10001 |> Scenario.expectFailure {-| Without an end to its validity range, the transaction could run after the due date. -} openEndedPaymentsFail : Scenario openEndedPaymentsFail = settle (payment 0 5000000) |> Scenario.expectFailure untaggedPaymentsFail : Scenario untaggedPaymentsFail = settle (Scenario.toKey payee 5000000) |> Scenario.validBefore 9000 |> Scenario.expectFailure {-| A payment tagged for another invoice can't settle this one too. -} paymentForAnotherInvoiceFails : Scenario paymentForAnotherInvoiceFails = settle (payment 1 5000000) |> Scenario.validBefore 9000 |> Scenario.expectFailure ``` ```sh elci test . ``` ```text PASS InvoiceTest.paymentSettles (cpu 31704477, mem 108622) PASS InvoiceTest.shortPaymentsFail (cpu 28198789, mem 101740) PASS InvoiceTest.latePaymentsFail (cpu 18812622, mem 71468) PASS InvoiceTest.openEndedPaymentsFail (cpu 16656709, mem 65018) PASS InvoiceTest.untaggedPaymentsFail (cpu 31025075, mem 108726) PASS InvoiceTest.paymentForAnotherInvoiceFails (cpu 32082910, mem 110554) 6 passed, 0 failed ``` `validBefore 10000` would also settle: its end is exclusive, so the transaction can't run at the due time. ## Julc's helper libraries The core modules cover what Julc's `ContextsLib`, `IntervalLib` and most of `OutputLib` are used for. Julc's libraries still help in a few cases. Map a class under `[java-modules]` and import it with the alias you chose: | Class under `org.julclang.stdlib.lib` | Still useful for | | --- | --- | | `OutputLib` | matching outputs by whole address, staking part included: `outputsAt`, `lovelacePaidTo`, `paidAtLeast` | | `AddressLib` | classifying an address: `isPubKeyAddress`, `isScriptAddress`, `paymentCredential`, `credentialHash` | Prefer the `Interval` module to `IntervalLib`: `IntervalLib.finiteLowerBound` and `finiteUpperBound` return `-1` for an infinite bound, a sentinel that makes "no end" look like "ends at -1". Don't alias a Java module as `Interval` or `Tx`; those names belong to the core modules. Functions that return a Java `Optional`, such as `ContextsLib.findOwnInput`, give you a `JulcOptional`; convert it with `Julc.Data.fromOptional`. `ContextsLib` and `OutputLib` may return the ledger's own values, as `valuePaid` does, but no other Java library may hand Elci a `Value`: `ValuesLib.add` and the other exports that build one are rejected (`ELCI2105`), because Java libraries don't keep values in the ledger's order. Use the `Value` module for those. The [standard library](https://elci-lang.dev/advanced/standard-library/) page lists every verified export. Next, see how your own datum and redeemer types cross the boundary in [Datums and redeemers](https://elci-lang.dev/contracts/datums-and-redeemers/). --- # Datums and redeemers > Which types can be datums, redeemers and parameters, how strict decoding protects a validator, the three spending-datum profiles, and exactly how Elci values are encoded as Plutus Data. Source: https://elci-lang.dev/contracts/datums-and-redeemers/ A validator receives its datum and redeemer as Plutus **Data**, the ledger's universal format, and Elci decodes them into your types before your handler runs. This page covers which types you can use, how strict decoding behaves, how to accept outputs with or without a datum, and how each Elci type is encoded. ## Datum, redeemer and parameters - The **datum** is stored with an output when it is locked at a script address. It records the state of that output: who may claim it, a price, a deadline. - The **redeemer** is supplied by the transaction that runs the script. It says what the transaction wants to do: claim, cancel, refund. - **Parameters** are fixed once per deployed script. See [Parameterized validators](https://elci-lang.dev/contracts/parameters/). All three use the same types and the same encoding. The difference is when they are checked: the script checks the datum and redeemer on every run, while parameters are checked once, off-chain, when you apply them. The key order of a `Dict` is the exception: the script checks it in parameters too. ## Types you can use | Type | At the boundary | | --- | --- | | `Int`, `Bool`, `String`, `Bytes` | yes | | `()` | yes, as a whole datum or redeemer; not yet inside records, lists or other containers (`ELCI2013`) | | Records and custom types | yes, when their fields are allowed types | | `Maybe a`, tuples, `List a`, `Dict k v`, `Map k v` | yes, when their contents are allowed types | | Ledger types such as `PubKeyHash`, `TxOutRef`, `Credential`, `Address` | yes | | `Data` | yes: any data is accepted unchecked; decode it yourself with `Julc.Data.decode` | | `Value` | not yet (`ELCI2013`): store a `List ( PolicyId, TokenName, Int )` and rebuild the value with `Value.fromList` | | `TxOut`, `TxInInfo`, `TxInfo` | no: the compiler can't check their layout (`ELCI2013`) | | `ScriptContext`, functions | no (`ELCI2013`) | | `Array a` | no: arrays have no Data encoding (`ELCI2013`); keep a `List` and convert it with `Array.fromList` | The compiler checks this when it compiles a handler, so an unsupported type is a compile error, never a runtime surprise. ## Strict decoding Decoding is **strict**. Before your handler runs, the script checks the whole datum and redeemer: every constructor tag, every field count and every field's kind, even the fields your handler never reads. Data of the wrong shape fails the script. This escrow shows the effect. The datum is a record and the redeemer a custom type: ```toml title="elci.toml" [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" [validators.escrow] boundary = "julc-strict-v1" spend = "Escrow.spend" [validators.locker] boundary = "julc-strict-v1" parameters = ["admin"] spend-datum = "optional" spend = "Locker.spend" [validators.donations] boundary = "julc-strict-v1" parameters = ["admin"] spend-datum = "absent" spend = "Donations.spend" [validators.ballot] boundary = "julc-strict-v1" spend = "Ballot.spend" ``` ```elm title="src/Auth.elm" module Auth exposing (signedBy) import Julc.Ledger exposing (PubKeyHash, ScriptContext) signedBy : PubKeyHash -> ScriptContext -> Bool signedBy key ctx = List.any (\signer -> signer == key) ctx.txInfo.signatories ``` ```elm title="src/Escrow.elm" module Escrow exposing (Action(..), Deal, spend) import Auth exposing (signedBy) import Julc.Ledger exposing (PubKeyHash, ScriptContext) type alias Deal = { seller : PubKeyHash , buyer : PubKeyHash , price : Int } type Action = Complete | Refund Int spend : Deal -> Action -> ScriptContext -> Bool spend deal action ctx = case action of Complete -> signedBy deal.buyer ctx Refund amount -> amount <= deal.price && signedBy deal.seller ctx ``` The tests lock outputs with good and bad datums. Scenarios take the datum and redeemer as `Data`, so each value goes through `Data.encode`: ```elm title="tests/EscrowTest.elm" module EscrowTest exposing (buyerCompletes, dealsRoundTrip, incompleteDealsAreRejected, missingDatumsAreRejected, sellerRefunds, wrongRedeemersAreRejected) import Escrow exposing (Action(..), Deal) import Expect exposing (Expectation) import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Property exposing (Property) import Scenario exposing (Scenario) seller : PubKeyHash seller = Scenario.key "seller" buyer : PubKeyHash buyer = Scenario.key "buyer" deal : Deal deal = { seller = seller, buyer = buyer, price = 100 } spendDeal : Data -> Scenario spendDeal datum = Scenario.spend "escrow" (Scenario.outRef "escrow" 0) datum buyerCompletes : Scenario buyerCompletes = spendDeal (Data.encode deal) |> Scenario.redeemer (Data.encode Complete) |> Scenario.signedBy buyer sellerRefunds : Scenario sellerRefunds = spendDeal (Data.encode deal) |> Scenario.redeemer (Data.encode (Refund 30)) |> Scenario.signedBy seller {-| `Complete` never reads `price`, but the whole datum is still decoded. -} incompleteDealsAreRejected : Scenario incompleteDealsAreRejected = spendDeal (Data.encode { seller = seller, buyer = buyer }) |> Scenario.redeemer (Data.encode Complete) |> Scenario.signedBy buyer |> Scenario.expectFailure missingDatumsAreRejected : Scenario missingDatumsAreRejected = Scenario.spendWithoutDatum "escrow" (Scenario.outRef "escrow" 0) |> Scenario.redeemer (Data.encode Complete) |> Scenario.signedBy buyer |> Scenario.expectFailure wrongRedeemersAreRejected : Scenario wrongRedeemersAreRejected = spendDeal (Data.encode deal) |> Scenario.redeemer (Data.encode 0) |> Scenario.signedBy buyer |> Scenario.expectFailure dealsRoundTrip : Deal -> Property dealsRoundTrip generated = Property.expect (Expect.equal generated (Data.decode (Data.encode generated))) ``` ```sh elci test . --match escrow --seed 42 ``` ```text PASS EscrowTest.buyerCompletes (cpu 12087950, mem 48649) PASS EscrowTest.sellerRefunds (cpu 15076849, mem 59803) PASS EscrowTest.incompleteDealsAreRejected (cpu 5999806, mem 24913) PASS EscrowTest.missingDatumsAreRejected (cpu 3001320, mem 10881) PASS EscrowTest.wrongRedeemersAreRejected (cpu 7930169, mem 33878) PASS EscrowTest.dealsRoundTrip (100 runs, max cpu 11543229, mem 46989) 6 passed, 0 failed (seed 42) ``` `incompleteDealsAreRejected` deserves a closer look. The buyer signs and the redeemer is `Complete`, which only checks the buyer's signature, yet the script rejects the transaction. Without `expectFailure` you would see why: ```text FAIL EscrowTest.incompleteDealsAreRejected (cpu 5999806, mem 24913) validator escrow rejected the transaction: UnIData: not IntData ``` The datum has two fields where `Deal` has three, so decoding fails before `spend` is called. Strictness means your handler only ever sees well-formed values, and an attacker can't slip in data of an unexpected shape. :::caution[A bad datum locks funds] Strictness also applies to the outputs you create. If an output is locked at the escrow address with a datum that isn't a valid `Deal`, **no** transaction can ever spend it. Encode datums from the same types the validator uses (`Data.encode` in Elci, or the layout in `abi.json` off-chain), and test them. ::: ### Dictionaries in data A `Dict` crosses the boundary as a Plutus map with its keys in ascending order, and decoding checks that order. The check matters because `Dict.member` and `Dict.get` stop at the first key that isn't smaller than the one they look for: in a map whose keys are out of order, they could stop before a key that is there. This ballot, the `ballot` validator in the `elci.toml` above, lets each key holder vote once: ```elm title="src/Ballot.elm" module Ballot exposing (Ballot, spend) import Dict exposing (Dict) import Julc.Ledger exposing (PubKeyHash, ScriptContext) type alias Ballot = { votes : Dict PubKeyHash Int } {-| The redeemer names the voter, who must sign and must not have voted yet. -} spend : Ballot -> PubKeyHash -> ScriptContext -> Bool spend ballot voter ctx = not (Dict.member voter ballot.votes) && List.member voter ctx.txInfo.signatories ``` The scenarios vote with a fresh key, vote twice, and try to vote twice with a forged datum: ```elm title="tests/BallotTest.elm" module BallotTest exposing (forgedOrderIsRejected, keysAscend, newVotersVote, votersVoteOnce) import Ballot exposing (Ballot) import Dict import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Map exposing (Map) import Scenario exposing (Scenario) alice : PubKeyHash alice = Scenario.key "alice" bob : PubKeyHash bob = Scenario.key "bob" carol : PubKeyHash carol = Scenario.key "carol" keysAscend : Bool keysAscend = Dict.keys (Dict.fromList [ ( carol, 1 ), ( alice, 1 ), ( bob, 1 ) ]) == [ alice, bob, carol ] vote : Data -> PubKeyHash -> Scenario vote ballot voter = Scenario.spend "ballot" (Scenario.outRef "ballot" 0) ballot |> Scenario.redeemer (Data.encode voter) |> Scenario.signedBy voter newVotersVote : Scenario newVotersVote = vote (Data.encode (Ballot (Dict.fromList [ ( alice, 1 ) ]))) bob votersVoteOnce : Scenario votersVoteOnce = vote (Data.encode (Ballot (Dict.fromList [ ( alice, 1 ), ( bob, 1 ) ]))) bob |> Scenario.expectFailure {-| `Ballot`'s layout, but a `Map` keeps its own order: the newest key first. -} type alias Forged = { votes : Map PubKeyHash Int } {-| Bob has voted, but his key comes after Carol's larger one. -} forgedOrderIsRejected : Scenario forgedOrderIsRejected = vote (Data.encode (Forged (Map.fromList [ ( bob, 1 ), ( carol, 1 ) ]))) bob |> Scenario.expectFailure ``` ```sh elci test . --match ballot ``` ```text PASS BallotTest.keysAscend (cpu 35117763, mem 132891) PASS BallotTest.newVotersVote (cpu 15827728, mem 64830) PASS BallotTest.votersVoteOnce (cpu 18258644, mem 76709) PASS BallotTest.forgedOrderIsRejected (cpu 14523407, mem 60626) 4 passed, 0 failed ``` `keysAscend` shows that the test keys order as Alice, Bob, Carol. `Forged` has `Ballot`'s layout, but a `Map` keeps its own order, so the forged datum lists Carol's key before Bob's. Bob has already voted, yet `Dict.member` would stop at Carol's key, the first that isn't smaller than his, and answer `False`. The script never gets that far: a datum whose keys are out of order or repeated fails to decode, whatever the handler would have decided. The check reads each key once, at about 1.1 million CPU units per entry. When you build such a datum off-chain, write the keys in Elci's order: integers numerically, byte strings and strings by their bytes with a proper prefix first, lists and tuples element by element. This is not CBOR's canonical key order, which puts shorter byte strings first and negative integers after positive ones. Build test datums with `Dict.fromList` and `Data.encode`, which always produce the right order. ## Spending-datum profiles By default a spending handler requires a datum, and an output without one can't be spent (`missingDatumsAreRejected` above). Set `spend-datum` in the validator's table to change that. It changes the handler's signature: | `spend-datum` | Handler type | An output without a datum | | --- | --- | --- | | `"required"` (default) | `params -> datum -> redeemer -> ScriptContext -> Bool` | is rejected | | `"optional"` | `params -> Maybe datum -> redeemer -> ScriptContext -> Bool` | arrives as `Nothing` | | `"absent"` | `params -> redeemer -> ScriptContext -> Bool` | is accepted; no datum is ever read | ### Optional With `"optional"`, the handler receives `Just datum` or `Nothing`. A datum that is present is still decoded strictly. This locker lets each output name its owner, and lets the admin recover outputs sent without a datum: ```elm title="src/Locker.elm" module Locker exposing (spend) import Auth exposing (signedBy) import Julc.Ledger exposing (PubKeyHash, ScriptContext) {-| An output's datum names its owner. Outputs sent without a datum can be recovered by the admin. -} spend : PubKeyHash -> Maybe PubKeyHash -> () -> ScriptContext -> Bool spend admin datum _ ctx = case datum of Just owner -> signedBy owner ctx Nothing -> signedBy admin ctx ``` ```elm title="tests/LockerTest.elm" module LockerTest exposing (adminCannotTakeOwnedOutputs, adminRecoversUnlabelled, malformedDatumsAreRejected, ownerSpends) import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash, TxOutRef) import Scenario exposing (Scenario) admin : PubKeyHash admin = Scenario.key "admin" owner : PubKeyHash owner = Scenario.key "owner" ref : TxOutRef ref = Scenario.outRef "locker" 0 ownerSpends : Scenario ownerSpends = Scenario.spend "locker" ref (Data.encode owner) |> Scenario.parameters [ Data.encode admin ] |> Scenario.signedBy owner adminCannotTakeOwnedOutputs : Scenario adminCannotTakeOwnedOutputs = Scenario.spend "locker" ref (Data.encode owner) |> Scenario.parameters [ Data.encode admin ] |> Scenario.signedBy admin |> Scenario.expectFailure adminRecoversUnlabelled : Scenario adminRecoversUnlabelled = Scenario.spendWithoutDatum "locker" ref |> Scenario.parameters [ Data.encode admin ] |> Scenario.signedBy admin malformedDatumsAreRejected : Scenario malformedDatumsAreRejected = Scenario.spend "locker" ref (Data.encode 42) |> Scenario.parameters [ Data.encode admin ] |> Scenario.signedBy admin |> Scenario.expectFailure ``` ```sh elci test . --match locker ``` ```text PASS LockerTest.ownerSpends (cpu 10898424, mem 43407) PASS LockerTest.adminCannotTakeOwnedOutputs (cpu 11276857, mem 45339) PASS LockerTest.adminRecoversUnlabelled (cpu 9203608, mem 36654) PASS LockerTest.malformedDatumsAreRejected (cpu 4502607, mem 19149) 4 passed, 0 failed ``` `malformedDatumsAreRejected` shows that "optional" doesn't mean "anything goes": the integer `42` is not a `PubKeyHash`, so it is not treated as a missing datum. ### Absent With `"absent"`, the handler has no datum argument and the datum is never inspected. Use it when outputs may arrive from wallets that attach no datum, or any datum: ```elm title="src/Donations.elm" module Donations exposing (spend) import Auth exposing (signedBy) import Julc.Ledger exposing (PubKeyHash, ScriptContext) {-| Anyone can donate, with or without a datum; only the admin can spend. -} spend : PubKeyHash -> () -> ScriptContext -> Bool spend admin _ ctx = signedBy admin ctx ``` ```elm title="tests/DonationsTest.elm" module DonationsTest exposing (adminSpendsAnyDatum, adminSpendsWithoutDatum, strangersCannotSpend) import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash, TxOutRef) import Scenario exposing (Scenario) admin : PubKeyHash admin = Scenario.key "admin" ref : TxOutRef ref = Scenario.outRef "donations" 0 adminSpendsWithoutDatum : Scenario adminSpendsWithoutDatum = Scenario.spendWithoutDatum "donations" ref |> Scenario.parameters [ Data.encode admin ] |> Scenario.signedBy admin adminSpendsAnyDatum : Scenario adminSpendsAnyDatum = Scenario.spend "donations" ref (Data.encode "thank you") |> Scenario.parameters [ Data.encode admin ] |> Scenario.signedBy admin strangersCannotSpend : Scenario strangersCannotSpend = Scenario.spendWithoutDatum "donations" ref |> Scenario.parameters [ Data.encode admin ] |> Scenario.signedBy (Scenario.key "stranger") |> Scenario.expectFailure ``` ```sh elci test . --match donations ``` ```text PASS DonationsTest.adminSpendsWithoutDatum (cpu 5906941, mem 23432) PASS DonationsTest.adminSpendsAnyDatum (cpu 5906941, mem 23432) PASS DonationsTest.strangersCannotSpend (cpu 6285374, mem 25364) 3 passed, 0 failed ``` ## How Elci values are encoded Every boundary type has one fixed Data encoding. You need it whenever off-chain code builds a datum, redeemer or parameter, and whenever you decode data that another contract wrote. | Elci type | Plutus Data | | --- | --- | | `Int` | integer | | `Bytes`, and hashes such as `PubKeyHash` | byte string | | `String` | byte string holding the UTF-8 text | | `Bool` | `False` is constructor 0, `True` is constructor 1, both with no fields | | `()` | constructor 0 with no fields | | Record | constructor 0; fields **sorted by name**, not in declaration order | | Custom type | constructor tag = position in the declaration (first is 0); fields in argument order | | `Maybe a` | `Nothing` is constructor 0; `Just x` is constructor 1 with one field | | Tuple | constructor 0; components in order | | `List a` | list | | `Dict k v` | map, with its keys in strictly ascending order | | `Map k v` | map, in the map's order | | `TxOutRef` | constructor 0 with the transaction id bytes and the index | Three rules cause most surprises: - **Record fields are sorted by name.** `Deal` is declared as seller, buyer, price but encoded as buyer, price, seller. Reordering a record's declaration never changes its encoding. - **Constructor order is the encoding.** Reordering the constructors of a custom type changes their tags, and existing on-chain data no longer decodes as intended. Add new constructors at the end. - **`Maybe` is not Julc's optional.** Julc's Java `Optional` uses different tags, so the two are not interchangeable. Convert with `Julc.Data.fromOptional` and `toOptional`. You can see these rules in action by encoding a value and decoding it as a different type with the same layout: ```elm title="tests/EncodingTest.elm" module EncodingTest exposing (boolsAreConstructors, maybeIsNothingThenJust, recordsAreSortedByFieldName) import Escrow exposing (Deal) import Expect exposing (Expectation) import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Scenario type Switch = Off | On type Option = None | Some Int seller : PubKeyHash seller = Scenario.key "seller" buyer : PubKeyHash buyer = Scenario.key "buyer" {-| `Deal` declares seller, buyer, price; it is encoded as buyer, price, seller. -} recordsAreSortedByFieldName : Expectation recordsAreSortedByFieldName = let deal : Deal deal = { seller = seller, buyer = buyer, price = 100 } in Expect.equal ( buyer, 100, seller ) (Data.decode (Data.encode deal)) {-| `False` is constructor 0 and `True` is constructor 1. -} boolsAreConstructors : Expectation boolsAreConstructors = Expect.equal [ Off, On ] (Data.decode (Data.encode [ False, True ])) {-| `Nothing` is constructor 0; `Just x` is constructor 1 with one field. -} maybeIsNothingThenJust : Expectation maybeIsNothingThenJust = Expect.equal [ None, Some 5 ] (Data.decode (Data.encode [ Nothing, Just 5 ])) ``` ```sh elci test . --match encoding ``` ```text PASS EncodingTest.recordsAreSortedByFieldName (cpu 9149529, mem 33661) PASS EncodingTest.boolsAreConstructors (cpu 7559553, mem 29633) PASS EncodingTest.maybeIsNothingThenJust (cpu 9322159, mem 36461) 3 passed, 0 failed ``` The build records every layout in `abi.json`, so off-chain code doesn't have to guess. This is the escrow's datum, extracted with `jq`: ```sh jq '.handlers[0].datum' build/validators/escrow/abi.json ``` ```json { "profile": "required", "schema": { "fields": [ { "name": "buyer", "schema": { "encoding": "B", "kind": "bytes" } }, { "name": "price", "schema": { "encoding": "I", "kind": "integer" } }, { "name": "seller", "schema": { "encoding": "B", "kind": "bytes" } } ], "identity": "elci.Record:buyer,price,seller [org.julclang.ledger.PubKeyHash, Int, org.julclang.ledger.PubKeyHash]", "kind": "record", "tag": 0 }, "sourceType": "Record:buyer,price,seller [org.julclang.ledger.PubKeyHash, Int, org.julclang.ledger.PubKeyHash]" } ``` ## Converting with `Julc.Data` `Julc.Data` is a compiler-provided module of explicit conversions. Import it with `import Julc.Data as Data`. | Function | Type | Behavior | | --- | --- | --- | | `Data.encode` | `a -> Data` | The Data encoding of a value | | `Data.decode` | `Data -> a` | Strict decoding; malformed data fails the script | | `Data.fromOptional` | `JulcOptional a -> Maybe a` | Re-tags a Julc optional (a ledger field or a library result) as `Maybe` | | `Data.toOptional` | `Maybe a -> JulcOptional a` | The reverse | The type `a` must be known where you convert. Usually the context fixes it, as in the tests above; otherwise add a type annotation. Where you use them: - **In tests**, to build the `Data` a scenario needs: `Data.encode deal`, `Data.encode (Refund 30)`. - **In contracts**, to read data the handler doesn't receive directly, such as the inline datum of another output (see [Reading the transaction](https://elci-lang.dev/contracts/reading-the-transaction/)). - **In properties**, to check that a type survives a round trip, like `dealsRoundTrip` above. `decode` never returns a `Maybe` on bad input: a failed decode fails the transaction. In a unit test that decodes a string as an `Int`: ```elm decodingIsStrict : Bool decodingIsStrict = Data.decode (Data.encode "five") == 5 ``` ```text FAIL EncodingTest.decodingIsStrict (cpu 928281, mem 5171) evaluation failed: UnIData: not IntData ``` A `Value` converts on its own: `Data.encode ctx.txInfo.mint` costs nothing, and `Data.decode` accepts only a canonical value (see [`Value`](https://elci-lang.dev/reference/core-modules/#value)). A type that contains a `Value` can't be decoded yet, a list or tuple of values can't be encoded either, and a ledger record without a checkable layout, such as `TxOut`, can't be decoded. Each is rejected at compile time. For `Data.encode [ Value.lovelace 5 ]`: ```text ELCI3003: No encoder for List [org.julclang.ledger.Value]: The library provider cannot supply encode List record Value: [JULC0051] Library request encode List record Value is not supported: org.julclang.ledger.Value does not approve ENCODE ``` Convert the values with `Value.toList` first, and back with `Value.fromList`. --- # Parameterized validators > Declare deployment parameters, test them with scenarios, and apply them to a built script with elci apply and cardano-cli's detailed Data JSON. Source: https://elci-lang.dev/contracts/parameters/ A parameter is a value baked into a script when you deploy it, such as an admin's key or a limit. This page shows how to declare parameters, test them, and apply them to the built script with `elci apply`. ## Why parameters A datum belongs to one output; a parameter belongs to the whole script. Two deployments of the same code with different parameters are different scripts, with different hashes, addresses and policy ids. That is exactly what you want when the value decides who controls the script: someone who deploys your policy with their own admin key gets their own policy id, not yours. ## Declaring parameters List the names under `parameters` in the validator's table. The handlers receive them as leading arguments, in that order. This minting policy lets an admin mint between a minimum and a maximum quantity per transaction: ```toml title="elci.toml" [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" [validators.drop] boundary = "julc-strict-v1" parameters = ["admin", "limits"] mint = "Drop.mint" ``` ```elm title="src/Drop.elm" module Drop exposing (Limits, mint) import Julc.Ledger exposing (PubKeyHash, ScriptContext) type alias Limits = { minimum : Int , maximum : Int } mint : PubKeyHash -> Limits -> Int -> ScriptContext -> Bool mint admin limits quantity ctx = (quantity >= limits.minimum) && (quantity <= limits.maximum) && List.any (\signer -> signer == admin) ctx.txInfo.signatories ``` `admin` and `limits` come first, then the redeemer (here the quantity the transaction claims to mint), then the context. The rules: - **Order matters.** Parameters are applied in the listed order, and every handler of the validator receives all of them, in that order. - **Types must agree.** Each parameter must have the same type in every handler. If `Treasury.spend` takes `admin : PubKeyHash` but `Treasury.withdraw` declares and uses it as an `Int`, the check fails: ```text .../treasury/src/Treasury.elm:11:1: ELCI2014: Handler Treasury.withdraw does not have the withdraw signature admin -> redeemer -> ScriptContext -> Bool that [validators.treasury] needs (1 parameter); its type is Int -> () -> ScriptContext -> Bool; it has `Int` where the configuration needs `PubKeyHash` 11| withdraw : Int -> () -> ScriptContext -> Bool ^^^^^^^^ In .../treasury/elci.toml:6: [validators.treasury] ``` - **Boundary types only.** Parameters follow the same rules as datums and redeemers: records, custom types, lists, dictionaries, maps and ledger types such as `PubKeyHash` or `TxOutRef` are fine; functions, arrays and `Value` are not. To parameterize a script by an amount of tokens, take a `List ( PolicyId, TokenName, Int )` and build the value with `Value.fromList`. See [Datums and redeemers](https://elci-lang.dev/contracts/datums-and-redeemers/). - **Names** start with a lowercase letter and use letters, digits and `_`. Parameters are decoded when the script is applied, but the script doesn't validate them on-chain. `elci apply` checks them off-chain instead, as shown below. The one exception is a `Dict`: the script checks its key order on every run, as it does in a datum. ## Testing with parameters A scenario supplies parameters as `Data`, in the declared order, with `Scenario.parameters`: ```elm title="tests/DropTest.elm" module DropTest exposing (adminMintsWithinLimits, onlyQuantitiesWithinLimits, tooManyAreRejected) import Bytes import Drop exposing (Limits) import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Property exposing (Property) import Scenario exposing (Scenario) admin : PubKeyHash admin = Scenario.key "admin" limits : Limits limits = { minimum = 1, maximum = 10 } drop : Int -> Scenario drop quantity = Scenario.mint "drop" |> Scenario.parameters [ Data.encode admin, Data.encode limits ] |> Scenario.minting (Bytes.fromString "DROP") quantity |> Scenario.redeemer (Data.encode quantity) |> Scenario.signedBy admin adminMintsWithinLimits : Scenario adminMintsWithinLimits = drop 10 tooManyAreRejected : Scenario tooManyAreRejected = drop 11 |> Scenario.expectFailure onlyQuantitiesWithinLimits : Int -> Property onlyQuantitiesWithinLimits quantity = if quantity >= 1 && quantity <= 10 then Property.scenario (drop quantity) else Property.scenario (Scenario.expectFailure (drop quantity)) ``` ```sh elci test . --seed 42 ``` ```text PASS DropTest.adminMintsWithinLimits (cpu 6369489, mem 23925) PASS DropTest.tooManyAreRejected (cpu 3787162, mem 15380) PASS DropTest.onlyQuantitiesWithinLimits (100 runs, max cpu 6369489, mem 23925) 3 passed, 0 failed (seed 42) ``` The test runner applies the parameters to the compiled script before it builds the transaction, so the script's address and policy id in each scenario are those of the applied script. A scenario must supply every parameter. If `drop` passed only `[ Data.encode admin ]`, its scenarios would fail before running: ```text FAIL DropTest.adminMintsWithinLimits cannot prepare validator 'drop': it takes 2 parameters [admin, limits], but the scenario gives 1; set them with Scenario.parameters 0 passed, 1 failed ``` :::note This example trusts the redeemer to tell the truth about the quantity, which keeps it short. A real policy should read the minted quantity from `ctx.txInfo.mint` with `Value.quantityOf`, as the [treasury tutorial](https://elci-lang.dev/tutorials/treasury/) does. ::: ## The built script `elci build` writes the script **before** parameters are applied. Its `validator.plutus.json` lists the parameters and leaves the hash empty, because the hash only exists once the parameters are known: ```json title="build/validators/drop/validator.plutus.json" { "type": "PlutusScriptV3", "purpose": "minting", "description": "drop", "cborHex": "...", "hash": "", "params": "admin:PubKeyHash,limits:Record", "sizeBytes": 218 } ``` `params` shows records as `Record`. The full layout of each parameter is in `abi.json`: ```sh jq '.parameters' build/validators/drop/abi.json ``` ```json [ { "name": "admin", "schema": { "encoding": "B", "kind": "bytes" }, "sourceType": "org.julclang.ledger.PubKeyHash" }, { "name": "limits", "schema": { "fields": [ { "name": "maximum", "schema": { "encoding": "I", "kind": "integer" } }, { "name": "minimum", "schema": { "encoding": "I", "kind": "integer" } } ], "identity": "elci.Record:maximum,minimum [Int, Int]", "kind": "record", "tag": 0 }, "sourceType": "Record:maximum,minimum [Int, Int]" } ] ``` Note the field order: `maximum` before `minimum`, because record fields are encoded sorted by name. ## Applying parameters with `elci apply` `elci apply` compiles the validator, checks each value and writes the applied script. Pass every parameter with `--param name=value`, where the value is Plutus Data in cardano-cli's detailed JSON schema: ```sh elci apply . --validator drop \ --param 'admin={"bytes": "887d8fbb82ce756a9dac9210c3d1235e9d3ff2457a667d311d318142"}' \ --param 'limits={"constructor": 0, "fields": [{"int": 10}, {"int": 1}]}' ``` ```text validator drop: applied 2 parameters; hash 4ba1e0d80a5891c79032b5eccdf0514392f90e90278ce6567a7d3d76; wrote .../token-drop/build/validators/drop/applied.plutus.json ``` The applied artifact has the same format as `validator.plutus.json`, now with a hash and no remaining parameters: ```json title="build/validators/drop/applied.plutus.json" { "type": "PlutusScriptV3", "purpose": "minting", "description": "drop", "cborHex": "59010a590107010100333232323223222323232323293370e...", "hash": "4ba1e0d80a5891c79032b5eccdf0514392f90e90278ce6567a7d3d76", "params": "", "sizeBytes": 263 } ``` For a minting policy, this hash is the policy id. For a spending validator, the script address is derived from it. ### Plutus Data JSON Each value is one JSON object in the detailed schema: | JSON | Data | For Elci types | | --- | --- | --- | | `{"int": 42}` | integer | `Int` | | `{"bytes": "0a1b"}` | byte string, as hex | `Bytes`, hashes, `String` (the UTF-8 bytes) | | `{"list": [ ... ]}` | list | `List a` | | `{"map": [{"k": ..., "v": ...}]}` | map | `Dict k v` (keys in ascending order), `Map k v` | | `{"constructor": 0, "fields": [ ... ]}` | constructor | records, custom types, `Bool`, `Maybe`, tuples, `()`, `TxOutRef` | Build values with the [encoding rules](https://elci-lang.dev/contracts/datums-and-redeemers/): a record is constructor 0 with its fields sorted by name, `True` is `{"constructor": 1, "fields": []}`, and a `TxOutRef` is `{"constructor": 0, "fields": [{"bytes": ""}, {"int": }]}`. The parser accepts integers, hex strings, arrays and objects; strings with escape sequences are not supported. ### What `apply` checks Before applying anything, `apply` runs Julc's strict decoder for each parameter's type against the value you gave. Wrong kinds, constructor tags and field counts are rejected, and nothing is written: ```text elci: ELCI4001: parameter limits is not a well-formed Record:maximum,minimum [Int, Int] value ``` (from `{"constructor": 0, "fields": [{"int": 10}]}`, which has one field too few), and: ```text elci: ELCI4001: parameter admin is not a well-formed org.julclang.ledger.PubKeyHash value ``` (from `{"int": 42}`). A `Dict` must list its keys in ascending order, each once, so `apply` also rejects ```json {"map": [{"k": {"int": 2}, "v": {"int": 10}}, {"k": {"int": 1}, "v": {"int": 5}}]} ``` for a parameter `caps : Dict Int Int`: ```text elci: ELCI4001: parameter caps has a Dict whose keys are not strictly ascending; write its keys sorted and without repeats ``` Other mistakes are reported the same way: | Mistake | Message | | --- | --- | | No `--param limits=...` | `ELCI4001: missing --param limits=` | | `--param owner=...` | `ELCI4001: validator drop has no parameter 'owner'; declared: admin, limits` | | `--param admin=887d8fbb` (bare hex, not JSON) | `ELCI4001: parameter admin is not Plutus Data JSON (for example {"int": 5}, {"bytes": "ab"} or {"constructor": 0, "fields": []}): unexpected trailing content at offset 3` | :::caution[The check is about shape, not meaning] `apply` checks the kind and layout of each value, not its meaning. It accepts `{"constructor": 0, "fields": [{"int": 1}, {"int": 10}]}` for `limits`, although that means `minimum = 10, maximum = 1` and nothing could ever be minted. It also accepts a `PubKeyHash` of the wrong length, because Elci adds no length rule to ledger hashes. Double-check values against `abi.json`, and test the applied parameters in scenarios. ::: ### Values from files Long values are easier to keep in files. Write `@path` instead of the JSON. The path is resolved against the **project directory**, not your current directory: ```json title="params/limits.json" { "constructor": 0, "fields": [ { "int": 10 }, { "int": 1 } ] } ``` ```sh elci apply . --validator drop \ --param 'admin={"bytes": "887d8fbb82ce756a9dac9210c3d1235e9d3ff2457a667d311d318142"}' \ --param limits=@params/limits.json ``` ### Where the result goes By default the result is `build/validators//applied.plutus.json`. It belongs to the build: the next `elci build` of that validator removes it, because it was applied to the previous script. To keep an applied script, write it elsewhere with `--output`, which is resolved against your current directory: ```sh elci apply token-drop --validator drop \ --param 'admin={"bytes": "887d8fbb82ce756a9dac9210c3d1235e9d3ff2457a667d311d318142"}' \ --param limits=@params/limits.json --output drop-preprod.plutus.json ``` ```text validator drop: applied 2 parameters; hash 4ba1e0d80a5891c79032b5eccdf0514392f90e90278ce6567a7d3d76; wrote .../drop-preprod.plutus.json ``` If the validator sets `expected-abi` and the current ABI differs, `apply` refuses to apply parameters. See [Build and deploy](https://elci-lang.dev/contracts/build-and-deploy/). ## Applying parameters from Java Off-chain Java code can apply the same parameters to the unapplied artifact with Julc's Cardano Client Lib adapter. Pass `PlutusData` values in the declared order. This example gives the same hash as `elci apply` above: ```java title="ApplyParams.java (example)" import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; import java.util.HexFormat; import java.util.List; import org.julclang.clientlib.JulcScriptAdapter; import org.julclang.clientlib.ValidatorOutput; import org.julclang.core.PlutusData; var output = ValidatorOutput.fromJson(Files.readString( Path.of("build/validators/drop/validator.plutus.json"))); var admin = PlutusData.bytes(HexFormat.of().parseHex( "887d8fbb82ce756a9dac9210c3d1235e9d3ff2457a667d311d318142")); // Limits { minimum = 1, maximum = 10 }: fields are sorted by name, so maximum comes first. var limits = new PlutusData.ConstrData(BigInteger.ZERO, List.of( PlutusData.integer(BigInteger.TEN), PlutusData.integer(BigInteger.ONE))); var program = JulcScriptAdapter.toProgram(output.cborHex()).applyParams(admin, limits); var script = JulcScriptAdapter.fromProgram(program); // a Cardano Client Lib PlutusV3Script System.out.println(HexFormat.of().formatHex(script.getScriptHash())); ``` ```text 4ba1e0d80a5891c79032b5eccdf0514392f90e90278ce6567a7d3d76 ``` It needs `org.julclang:julc-cardano-client-lib` (at the Julc version your compiler uses) and Cardano Client Lib on the classpath. Unlike `elci apply`, this path doesn't check the values against their types, so validate them yourself. --- # Build and deploy > What elci build writes, how abi.json and expected-abi guard your data layout, where script hashes come from, and how to load an artifact in Java with Cardano Client Lib. Source: https://elci-lang.dev/contracts/build-and-deploy/ This page walks through the files `elci build` produces, how to protect your on-chain data layout from accidental changes, and how off-chain code loads the compiled script. The examples use the vesting contract from the [vesting tutorial](https://elci-lang.dev/tutorials/vesting/). ## Building ```sh elci build . ``` ```text validator vesting (spend): checked; 533 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` Each validator gets its own directory: - build/ - validators/ - elci-outputs.json the files Elci owns, per validator - vesting/ - validator.flat the script, FLAT-encoded UPLC - validator.plutus a Plutus V3 text envelope - validator.plutus.json the Julc artifact for off-chain code - abi.json the data layout of parameters, datums and redeemers | File | Contents | Use it for | | --- | --- | --- | | `validator.flat` | The raw UPLC program in FLAT encoding | Tools that read FLAT directly | | `validator.plutus` | A text envelope: `type`, `description` and `cborHex`, with the script bytes CBOR-wrapped twice | Tools that read Plutus script envelopes | | `validator.plutus.json` | Julc's validator artifact: `cborHex`, `hash`, `params`, `purpose`, `sizeBytes` | Loading the script with Cardano Client Lib | | `abi.json` | Handlers, datum, redeemer and parameter schemas, constructor tags and field order | Building data off-chain; detecting layout changes | Neither `validator.plutus.json` nor `abi.json` is a CIP-57 blueprint; Elci doesn't generate blueprints yet. ### How a build writes files - **All or nothing.** Every selected validator is compiled, and checked against its `expected-abi`, before any file is written. If one fails, nothing is published and the previous outputs stay as they were. - **Selection.** `elci build . --validator vesting` builds only the named validators (repeat the option for more) and leaves the others' outputs alone. - **Cleanup.** A full build removes the outputs of validators you have deleted from `elci.toml`. It only removes files listed in `elci-outputs.json`, so anything else you put in those directories is kept. - **Applied scripts.** An `applied.plutus.json` written by `elci apply` is removed by the next build of its validator, because it was applied to the previous script. ## The Julc artifact `validator.plutus.json` is the file off-chain code loads (`cborHex` shortened): ```json title="build/validators/vesting/validator.plutus.json" { "type": "PlutusScriptV3", "purpose": "spending", "description": "vesting", "cborHex": "590218590215010100323232323232323232322323232323...", "hash": "cdf09c382832689b016adac7ef1470488e9ff58b8a474b9086c7a0c3", "params": "", "sizeBytes": 533 } ``` | Field | Meaning | | --- | --- | | `type` | Always `PlutusScriptV3` | | `purpose` | `spending`, `minting`, `withdraw`, `certifying`, `voting` or `proposing` for one handler; `multi` for several | | `description` | The validator's name | | `cborHex` | The script, CBOR-wrapped twice as Cardano Client Lib expects | | `hash` | The script hash; empty while parameters are still to be applied | | `params` | The parameters still to be applied, as `name:type`, comma-separated; records show as `Record` | | `sizeBytes` | The size of the FLAT script | ## The ABI `abi.json` describes everything off-chain code needs to talk to the script. Its top level: ```sh jq '{formatVersion, validator, purpose, target, julcVersion, compiler, boundary, ledgerArguments, parameters, definitions}' build/validators/vesting/abi.json ``` ```json { "formatVersion": 2, "validator": "vesting", "purpose": "spend", "target": "plutus-v3-pv11-uplc-1.1.0", "julcVersion": "0.1.0-pre20-ebaf028-SNAPSHOT", "compiler": "elci-0.1.0-dev", "boundary": "julc-strict-v1", "ledgerArguments": 1, "parameters": [], "definitions": {} } ``` `handlers` lists one entry per handler, with its function, its purpose and the ledger's tag for that purpose, plus the datum profile and the datum and redeemer schemas. `parameters` holds the parameter schemas in application order, and `definitions` holds shared definitions such as recursive types. Each schema gives the kind of data, the constructor tags and the field order. The vesting datum: ```sh jq '.handlers[0] | {datum, redeemer}' build/validators/vesting/abi.json ``` ```json { "datum": { "profile": "required", "schema": { "fields": [ { "name": "beneficiary", "schema": { "encoding": "B", "kind": "bytes" } }, { "name": "deadline", "schema": { "encoding": "I", "kind": "integer" } } ], "identity": "elci.Record:beneficiary,deadline [org.julclang.ledger.PubKeyHash, Int]", "kind": "record", "tag": 0 }, "sourceType": "Record:beneficiary,deadline [org.julclang.ledger.PubKeyHash, Int]" }, "redeemer": { "schema": { "constructors": [ { "fields": [], "name": "Vesting.Claim", "tag": 0 } ], "identity": "elci.Vesting.VestingAction", "kind": "union" }, "sourceType": "Vesting.VestingAction" } } ``` Keep the ABI with the application code that builds datums and redeemers. The encoding rules behind it are on [Datums and redeemers](https://elci-lang.dev/contracts/datums-and-redeemers/). ## Guarding the ABI with `expected-abi` Once outputs exist on-chain, their datums must keep decoding. A harmless-looking refactor, such as reordering constructors, can silently change the layout. To catch that, commit a copy of the ABI outside `build/` and point the validator at it: ```sh mkdir -p abi cp build/validators/vesting/abi.json abi/vesting.json ``` ```toml title="elci.toml" ins={4} [validators.vesting] boundary = "julc-strict-v1" spend = "Vesting.validate" expected-abi = "abi/vesting.json" ``` The path is relative to the project directory. From now on, `elci check`, `elci build` and `elci apply` compare the current ABI with the committed one before writing anything. Changes that keep the layout pass. Reordering the fields of the datum's declaration, for example, still builds: ```elm type alias VestingDatum = { deadline : Int , beneficiary : PubKeyHash } ``` ```text validator vesting (spend): checked; 533 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` Renaming `deadline` to `unlockAt` does not: ```text elci: ELCI4001: .../vesting/elci.toml:6: [validators.vesting]: ABI drift from abi/vesting.json; compare constructor tags, field order, handler roles and parameters ``` The field names are part of the ABI, because off-chain code uses them. Constructor order, field names and types, handler roles and parameters are all compared. Nothing is written when the check fails, so the old artifacts stay in `build/`. When the change is intended, remove the `expected-abi` line, build, review the difference, then copy the new ABI over the committed one and restore the line: ```sh diff <(jq . abi/vesting.json) <(jq . build/validators/vesting/abi.json) ``` ```text 20c20 < "name": "deadline", --- > "name": "unlockAt", 27c27 < "identity": "elci.Record:beneficiary,deadline [org.julclang.ledger.PubKeyHash, Int]", --- > "identity": "elci.Record:beneficiary,unlockAt [org.julclang.ledger.PubKeyHash, Int]", 31c31 < "sourceType": "Record:beneficiary,deadline [org.julclang.ledger.PubKeyHash, Int]" --- > "sourceType": "Record:beneficiary,unlockAt [org.julclang.ledger.PubKeyHash, Int]" ``` ## Script hashes The script hash identifies a deployed script: - For a **spending** validator, the script address is built from it. - For a **minting** policy, it is the policy id. - For **withdraw** and **certify**, it is the stake credential. A validator without parameters has its hash in `validator.plutus.json`. A parameterized validator has an empty `hash` until you apply its parameters with `elci apply`, which prints the hash and writes it into `applied.plutus.json` (see [Parameterized validators](https://elci-lang.dev/contracts/parameters/)). Any change to the source, the handlers, the parameters or the compiler version can change the hash, so record which build you deployed. ## Loading the script in Java Cardano Client Lib can load the artifact directly with Julc's adapter, `org.julclang:julc-cardano-client-lib`. This example reads `validator.plutus.json` (or an `applied.plutus.json`), checks the hash and derives the script's address on the preprod test network: ```java title="LoadScript.java (example)" import com.bloxbean.cardano.client.address.AddressProvider; import com.bloxbean.cardano.client.common.model.Networks; import com.bloxbean.cardano.client.plutus.spec.PlutusV3Script; import java.nio.file.Files; import java.nio.file.Path; import java.util.HexFormat; import org.julclang.clientlib.ValidatorOutput; public class LoadScript { public static void main(String[] args) throws Exception { var output = ValidatorOutput.fromJson( Files.readString(Path.of(args[0]))); var script = PlutusV3Script.builder().cborHex(output.cborHex()).build(); String hash = HexFormat.of().formatHex(script.getScriptHash()); if (!hash.equals(output.hash())) { throw new IllegalStateException("Script hash mismatch"); } var address = AddressProvider.getEntAddress(script, Networks.preprod()); System.out.println("hash " + hash); System.out.println("address " + address.toBech32()); } } ``` Run with Cardano Client Lib 0.7.2 and the Julc adapter on the classpath, it prints: ```text hash cdf09c382832689b016adac7ef1470488e9ff58b8a474b9086c7a0c3 address addr_test1wrxlp8pc9qex3xcpdtdv0mc5wpyga8l43w9ywjussmr6psceanc6r ``` The `PlutusV3Script` is an ordinary Cardano Client Lib script: attach it to transactions, together with datums and redeemers you build as `PlutusData` following `abi.json`. Parameterized scripts need their parameters first, either with `elci apply` or in Java, as shown on [Parameterized validators](https://elci-lang.dev/contracts/parameters/). To test Elci scripts from JUnit, see [Testing from Java](https://elci-lang.dev/testing/java/). ## Before you deploy Elci is a development preview, and its output has not been audited. Deploy to a test network first, and treat any mainnet use as experimental. Before deploying a script: - Run `elci test`, with scenarios for every purpose and every way a transaction should be rejected. - Check the budgets your scenarios report. The ledger limits every transaction's CPU and memory, and the fee grows with both. - Commit the ABI and set `expected-abi`, so that later changes can't silently break existing datums. - Apply parameters with `elci apply`, which checks their shape, and double-check their meaning against `abi.json`. - Record the script hash you deployed. --- # Unit tests > Write Bool and Expect tests for your Elci code, run them with elci test, read the results and choose which tests run. Source: https://elci-lang.dev/testing/unit-tests/ In this page you write your first tests in Elci, run them with `elci test` and learn to read what it prints. Unit tests are the quickest way to check the pure functions your validators rely on, such as fee calculations or datum helpers. Every test compiles to its own small program and runs on the same Plutus VM that evaluates your contracts. So a test tells you both whether your code is right and what it costs in CPU and memory. ## A function to test Start from a project made by `elci init shop`. It already contains a validator, `src/Main.elm`, and two tests for it in `tests/MainTest.elm`. Add a module that computes a 0.3% trading fee: ```elm title="src/Pricing.elm" module Pricing exposing (Quote, fee, quote) type alias Quote = { amount : Int , fee : Int , total : Int } {-| A 0.3% fee (30 basis points), rounded down. -} fee : Int -> Int fee amount = amount * 30 // 10000 quote : Int -> Quote quote amount = { amount = amount , fee = fee amount , total = amount + fee amount } ``` `//` is integer division that truncates toward zero, so small amounts pay no fee at all. That is exactly the kind of detail worth pinning down with a test. ## Your first tests Tests live in modules under `tests/`. Create `tests/PricingTest.elm`: ```elm title="tests/PricingTest.elm" module PricingTest exposing ( feeOnTenThousand , feesRoundDown , feesStayUnderOnePercent , quotesAddTheFee , smallAmountsAreFree ) import Expect exposing (Expectation) import Pricing smallAmountsAreFree : Bool smallAmountsAreFree = Pricing.fee 300 == 0 feeOnTenThousand : Expectation feeOnTenThousand = Expect.equal 30 (Pricing.fee 10000) quotesAddTheFee : Expectation quotesAddTheFee = Expect.equal { amount = 10000, fee = 30, total = 10030 } (Pricing.quote 10000) feesRoundDown : Expectation feesRoundDown = Expect.every [ Expect.equal 0 (Pricing.fee 333) , Expect.equal 1 (Pricing.fee 334) , Expect.equal 1 (Pricing.fee 666) ] feesStayUnderOnePercent : Expectation feesStayUnderOnePercent = Expect.lessThan 10000 (Pricing.fee 1000000) |> Expect.onFail "a fee must stay under 1% of the amount" ``` There are two kinds of unit test here: - A **`Bool` test** passes when it evaluates to `True`. It is the simplest possible check. - An **`Expectation` test** uses the `Expect` module. When it fails, it tells you what it expected and what it got, which a `Bool` cannot. The project now looks like this: - shop/ - elci.toml - src/ - Main.elm the validator from elci init - Pricing.elm - tests/ - MainTest.elm scenarios from elci init - PricingTest.elm ## Run the tests Run `elci test` from the project directory, or pass the directory as an argument: ```sh elci test ``` ```text PASS MainTest.ownerSigns (cpu 7675163, mem 29990) PASS MainTest.othersAreRejected (cpu 8053596, mem 31922) PASS PricingTest.smallAmountsAreFree (cpu 675727, mem 2604) PASS PricingTest.feeOnTenThousand (cpu 929121, mem 4068) PASS PricingTest.quotesAddTheFee (cpu 4498493, mem 12685) PASS PricingTest.feesRoundDown (cpu 5947066, mem 24719) PASS PricingTest.feesStayUnderOnePercent (cpu 1794288, mem 8097) 7 passed, 0 failed ``` Each line names the test as `Module.name` and shows the CPU steps and memory units its evaluation consumed. Modules run in alphabetical order, and the tests of a module in source order. The two `MainTest` tests are [transaction scenarios](https://elci-lang.dev/testing/scenarios/): for those, the numbers are the cost of the validator itself. `elci test` only reads your project. It never writes `build/` and its tests never become part of `elci check`, `elci build` or the published scripts. ## When a test fails To see failures, break three tests on purpose. Check `Pricing.fee 400 == 0` in the `Bool` test, expect `31` in `feeOnTenThousand`, and expect a `total` of `10031` in `quotesAddTheFee`: ```text FAIL PricingTest.smallAmountsAreFree (cpu 675727, mem 2604) evaluated to False FAIL PricingTest.feeOnTenThousand (cpu 1991208, mem 7866) Expect.equal expected: 31 actual: 30 FAIL PricingTest.quotesAddTheFee (cpu 5657982, mem 17219) Expect.equal expected: Constr 0 [10000, 30, 10031] actual: Constr 0 [10000, 30, 10030] PASS PricingTest.feesRoundDown (cpu 5947066, mem 24719) PASS PricingTest.feesStayUnderOnePercent (cpu 1794288, mem 8097) 2 passed, 3 failed ``` The `Bool` test can only say that it evaluated to `False`. The expectations show both values. Values in a failure are printed as their **Plutus Data encoding**, because that is the form the test program returns from the VM. The encoding follows the rules of your datums and redeemers: | Elci value | Printed as | | --- | --- | | `30`, `-5` | `30`, `-5` | | `Bytes.fromString "GOLD"` and the `String` `"GOLD"` | `#474f4c44` (hex of the UTF-8 bytes) | | `False`, `True` | `Constr 0`, `Constr 1` | | `Nothing`, `Just 5` | `Constr 0`, `Constr 1 [5]` | | `[ 1, 2 ]` | `[1, 2]` | | `( 1, True )` | `Constr 0 [1, Constr 1]` | | a record | `Constr 0 [...]`, with the fields in alphabetical order of their names | | a custom type | `Constr tag [...]`, where the tag is the constructor's position, from 0 | So `Constr 0 [10000, 30, 10031]` is the `Quote` record with fields `amount`, `fee` and `total`. A test can also fail at runtime, for example by dividing by zero. It is then reported with the VM's error, such as `evaluation failed: QuotientInteger: division by zero`. [Debugging and budgets](https://elci-lang.dev/testing/debugging/) covers these messages and how to add traces. ## The Expect module `Expect` has a small set of functions in the style of elm-test. Import it with `import Expect exposing (Expectation)`. | Function | Passes when | | --- | --- | | `Expect.equal expected actual` | the two values are structurally equal | | `Expect.notEqual unexpected actual` | the two values differ | | `Expect.atMost bound actual` | `actual <= bound` | | `Expect.atLeast bound actual` | `actual >= bound` | | `Expect.lessThan bound actual` | `actual < bound` | | `Expect.greaterThan bound actual` | `actual > bound` | | `Expect.pass` | always | | `Expect.fail message` | never; the failure shows `message` | | `Expect.every [ e1, e2 ]` | every expectation passes; the first failure is reported | | `Expect.onFail message expectation` | `expectation` passes; otherwise its message is replaced | The comparisons work on any comparable values, such as integers, strings and bytes. They take the bound first and the actual value last, so they read well in a pipeline. When one fails, the report lists the bound and then the actual value: ```elm feesAreCapped : Expectation feesAreCapped = Expect.atMost 100 (Pricing.fee 50000) ``` ```text FAIL PricingTest.feesAreCapped (cpu 2126712, mem 8766) Expect.atMost values: 100, 150 ``` `Expect.onFail` puts your own explanation in place of the function name: ```elm feesAreCappedExplained : Expectation feesAreCappedExplained = Expect.atMost 100 (Pricing.fee 50000) |> Expect.onFail "fees are capped at 100 lovelace" ``` ```text FAIL PricingTest.feesAreCappedExplained (cpu 5046289, mem 18138) fees are capped at 100 lovelace values: 100, 150 ``` `Expect.every` combines several checks into one test and reports the first that fails. Its argument is a list of evaluated expectations: Elci lists hold data, not functions, so there is no elm-test style `Expect.all` that takes checks as functions. :::note `Expect.equal` and `Expect.notEqual` need the same structural equality as `==`. They accept numbers, bytes, strings, `Data`, lists, tuples, records and custom types, and reject `Map`, functions and imported types without approved equality at compile time. ::: ## How tests are discovered `elci test` treats a declaration in a test module as a test when it is: 1. **exposed** by its module, 2. a **top-level** declaration with **no arguments**, and 3. of type `Bool`, `Expect.Expectation`, `Scenario.Scenario` or `Property.Property`. An exposed **function** whose result is a `Property` is also a test: a [property-based test](https://elci-lang.dev/testing/properties/) that runs many times with generated arguments. Everything else is a helper. That includes functions that return an `Expectation`, a `Scenario` or a `Bool`, such as a function that builds a transaction for a given signer. With an explicit `exposing` list you choose exactly which declarations are tests. With `exposing (..)`, every zero-argument declaration of a test type runs, and declarations of other types, such as a test key of type `PubKeyHash`, are skipped. ## Choose which tests run `--match` runs only the tests whose full name contains a substring. The match ignores case and includes the module name: ```sh elci test --match fee ``` ```text PASS PricingTest.feeOnTenThousand (cpu 929121, mem 4068) PASS PricingTest.quotesAddTheFee (cpu 4498493, mem 12685) PASS PricingTest.feesRoundDown (cpu 5947066, mem 24719) PASS PricingTest.feesStayUnderOnePercent (cpu 1794288, mem 8097) 4 passed, 0 failed ``` `quotesAddTheFee` matches because its name contains `Fee`. Use `--match PricingTest` to run one module. ## Where tests live By default, test modules are the `.elm` files under `tests/`. To use other directories, list them in `elci.toml`: ```toml title="elci.toml" [tests] source-directories = ["tests", "spec"] ``` A few rules apply to test modules: - **Imports.** A test module can import your project's modules, the Java modules configured in `[java-modules]`, the test library (`Expect`, `Scenario` and `Property`) and other test modules. A shared module such as `tests/Fixtures.elm` is a good home for test keys and sample datums. Source modules cannot import the test library: `elci test` rejects such an import with `ELCI2101`. - **Names.** A test module cannot have the same name as a source module, or as `Expect`, `Scenario` or `Property`. `elci test` stops with `ELCI4001: module Pricing is both a source and a test module` if you try. - **Isolation.** Test modules are compiled only by `elci test`. They cannot change what `elci build` produces. ## Use elci test in CI `elci test` exits with status `1` when any test fails, and also when no test is found or none matches `--match`: ```text No tests match 'nothing-here' ``` Otherwise it exits with `0`. You can run it as a step in any CI pipeline without extra configuration. ## Next steps - Test whole transactions against your validators with [transaction scenarios](https://elci-lang.dev/testing/scenarios/). - Check a rule for many generated inputs with [property-based tests](https://elci-lang.dev/testing/properties/). - Look up every function in the [test library reference](https://elci-lang.dev/reference/test-library/). --- # Transaction scenarios > Test validators with whole transactions. Describe a spend, mint or withdrawal in Elci and elci test runs the deployed script in a complete ledger context. Source: https://elci-lang.dev/testing/scenarios/ A scenario describes one transaction against one of your validators, and `elci test` checks that the validator accepts or rejects it. In this page you test a small marketplace: spending with a payment, a validity range and a continuing output, minting, and withdrawing staking rewards. Unit tests check functions. Scenarios check what actually goes on-chain: the compiled script, its purpose dispatch, the strict decoding of the datum and redeemer, and your logic, all against a realistic `ScriptContext`. ## How a scenario runs A `Scenario` is plain data: a record that names a validator and lists the transaction's details. You build it with a pipeline of functions from the `Scenario` module, starting with the purpose. These two come from the test module that `elci init` generates: ```elm transaction : Scenario transaction = Scenario.spend "main" (Scenario.outRef "main" 0) Scenario.unit |> Scenario.parameters [ Data.encode owner ] ownerSigns : Scenario ownerSigns = transaction |> Scenario.signedBy owner ``` `transaction` spends an output of the validator `main`, and `ownerSigns` is the same transaction signed by the owner. Only `ownerSigns` is a test: `transaction` is not exposed. When `elci test` finds an exposed `Scenario` value, it: 1. compiles the named validator exactly as `elci build` does, and applies the scenario's `parameters` in the order the validator declares them; 2. computes the applied script's hash, which gives the validator its own address, minting policy and staking credential; 3. builds the complete ledger context with julc-testkit's `ScriptContextTestBuilder`: purpose, redeemer, signers, inputs, outputs, minted tokens, fee and validity range; 4. runs the **deployed script** against that context, so purpose dispatch and strict datum and redeemer decoding happen exactly as on-chain; 5. compares the outcome with the expectation: success by default, or rejection after `Scenario.expectFailure`, plus an optional budget. Because the script is the one `elci build` publishes, a passing scenario is evidence about your real artifact, not about a simulation of it. ## The example project The marketplace has two validators. `market` holds listings and earns staking rewards: its `spend` handler lets anyone buy a listing before it expires by paying the seller, and lets the seller change the price or delist. Its `withdraw` handler lets only the operator withdraw rewards. `ticket` is a minting policy for event tickets. ```toml title="elci.toml" [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" # The market script: holds listings and earns staking rewards. [validators.market] boundary = "julc-strict-v1" parameters = ["operator"] spend = "Market.spend" withdraw = "Market.withdraw" # The ticket minting policy. [validators.ticket] boundary = "julc-strict-v1" parameters = ["organizer"] mint = "Ticket.mint" ``` ```elm title="src/Market.elm" module Market exposing (Action(..), Listing, spend, withdraw) import Interval import Julc.Data as Data import Julc.Ledger exposing (Credential(..), PubKeyHash, ScriptContext, TxOut) import Tx import Value type alias Listing = { seller : PubKeyHash , price : Int , expires : Int } type Action = Buy | Reprice Int | Delist {-| Anyone may buy before the listing expires by paying the seller the price. The seller may change the price or delist at any time. -} spend : PubKeyHash -> Listing -> Action -> ScriptContext -> Bool spend _ listing action ctx = case action of Buy -> sellerPaid listing ctx && Interval.isEntirelyBefore listing.expires ctx.txInfo.validRange Reprice newPrice -> Tx.signedBy listing.seller ctx && List.any (holds { listing | price = newPrice }) (Tx.continuingOutputs ctx) Delist -> Tx.signedBy listing.seller ctx {-| Only the operator withdraws the market's staking rewards. -} withdraw : PubKeyHash -> () -> ScriptContext -> Bool withdraw operator _ ctx = Tx.signedBy operator ctx {-| An output pays the seller the price and carries this listing's output reference, so that it can't also pay for another listing in the same transaction. -} sellerPaid : Listing -> ScriptContext -> Bool sellerPaid listing ctx = case Tx.ownRef ctx of Just ref -> List.any (\output -> (Value.lovelaceOf output.value >= listing.price) && (Tx.inlineDatum output == Just (Data.encode ref)) ) (Tx.outputsTo (PubKeyCredential listing.seller) ctx) Nothing -> False holds : Listing -> TxOut -> Bool holds expected output = Tx.inlineDatum output == Just (Data.encode expected) ``` ```elm title="src/Ticket.elm" module Ticket exposing (mint) import Bytes import Julc.Ledger exposing (PubKeyHash, ScriptContext, TokenName(..)) import Tx import Value {-| The organizer mints exactly one TICKET per transaction. -} mint : PubKeyHash -> () -> ScriptContext -> Bool mint organizer _ ctx = case Tx.ownPolicy ctx of Just policy -> (Value.quantityOf policy (TokenName (Bytes.fromString "TICKET")) ctx.txInfo.mint == 1) && Tx.signedBy organizer ctx Nothing -> False ``` The validators read the transaction with the core [`Tx` and `Interval`](https://elci-lang.dev/reference/core-modules/#tx) modules. A buyer's payment must carry the listing's output reference as its inline datum, so one payment can't pay for two listings bought in the same transaction; [reading the transaction](https://elci-lang.dev/contracts/reading-the-transaction/#one-payment-per-input) explains why. ## Test keys and a helper Start the test module with the people involved and helpers that build the common parts of every spend. `Scenario.key` derives a deterministic 28-byte key hash from a name, so `Scenario.key "seller"` is the same in every test and every run: ```elm title="tests/MarketTest.elm" module MarketTest exposing ( buyerPaysTheSeller , buyingAfterExpiryFails , buyingWithoutADeadlineFails , operatorWithdrawsRewards , othersCannotWithdraw , paymentForAnotherListingFails , sellerDelists , sellerReprices , strangerCannotDelist , underpayingFails ) import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Market exposing (Action(..), Listing) import Scenario exposing (Output, Scenario) operator : PubKeyHash operator = Scenario.key "operator" seller : PubKeyHash seller = Scenario.key "seller" listing : Listing listing = { seller = seller, price = 50000000, expires = 2000 } {-| Spend the listed output with an action. -} settle : Action -> Scenario settle action = Scenario.spend "market" (Scenario.outRef "listing" 0) (Data.encode listing) |> Scenario.parameters [ Data.encode operator ] |> Scenario.redeemer (Data.encode action) {-| A payment to the seller, tagged with the listing's output reference. -} payment : Int -> Output payment lovelace = Scenario.toKey seller lovelace |> Scenario.withDatum (Data.encode (Scenario.outRef "listing" 0)) ``` `settle` returns a `Scenario` but takes an argument, so it is a helper, not a test. The same goes for `operator`, `seller`, `listing` and `payment`, whose types are not test types. `payment` builds an output with `Scenario.toKey`, which pays a key's address without a staking part, and tags it with the listing's reference. Everything that crosses into the script is `Data`, so datums, redeemers and parameters are encoded with `Data.encode`. `Scenario.parameters` must give every parameter the validator declares, in order; `market` declares one, `operator`. `Scenario.outRef "listing" 0` makes a deterministic output reference from a name and an index; `settle` spends that output, and `payment` tags with the same reference. ## Spending `Scenario.spend validator ref datum` spends `ref`. The runner adds the spent output to the transaction's inputs, at the validator's own address, holding 2 ADA and the datum inline. Then you add what the transaction does. A buyer pays the seller and the transaction must end before the listing expires: ```elm buyerPaysTheSeller : Scenario buyerPaysTheSeller = settle Buy |> Scenario.output (payment 50000000) |> Scenario.validBefore 1500 underpayingFails : Scenario underpayingFails = settle Buy |> Scenario.output (payment 49999999) |> Scenario.validBefore 1500 |> Scenario.expectFailure ``` `Scenario.expectFailure` turns a scenario into a negative test: it passes only when the validator rejects the transaction. Write one next to each rule: a validator that accepts everything passes every positive test. Test the attacks too. A payment tagged with another listing's reference must not pay for this one: ```elm {-| A payment tagged for another listing can't also pay for this one. -} paymentForAnotherListingFails : Scenario paymentForAnotherListingFails = settle Buy |> Scenario.output (Scenario.toKey seller 50000000 |> Scenario.withDatum (Data.encode (Scenario.outRef "listing" 1)) ) |> Scenario.validBefore 1500 |> Scenario.expectFailure ``` Signatures work the same way. `Scenario.signedBy` adds a required signer, and you can call it more than once: ```elm sellerDelists : Scenario sellerDelists = settle Delist |> Scenario.signedBy seller strangerCannotDelist : Scenario strangerCannotDelist = settle Delist |> Scenario.signedBy (Scenario.key "stranger") |> Scenario.expectFailure ``` For a spend without a datum, as with `spend-datum = "optional"` or `"absent"`, use `Scenario.spendWithoutDatum validator ref`. ## Validity ranges A transaction without `validFrom` or `validBefore` is valid at any time: both bounds are infinite. The two functions set the bounds in POSIX time: - `Scenario.validFrom time` sets an **inclusive** lower bound: the transaction is valid from `time`. - `Scenario.validBefore time` sets an **exclusive** upper bound: the transaction is valid before `time`. ```elm buyingAfterExpiryFails : Scenario buyingAfterExpiryFails = settle Buy |> Scenario.output (payment 50000000) |> Scenario.validFrom 1800 |> Scenario.validBefore 2500 |> Scenario.expectFailure buyingWithoutADeadlineFails : Scenario buyingWithoutADeadlineFails = settle Buy |> Scenario.output (payment 50000000) |> Scenario.expectFailure ``` The second test matters: a transaction without an upper bound could run long after the listing expired. `Interval.isEntirelyBefore` needs a finite end, so the validator rejects it. ## Outputs, datums and tokens An output is built with one of four functions and then extended: - `Scenario.toKey key lovelace` pays a key's address, without a staking part. - `Scenario.toAddress address lovelace` pays any address, such as one with a staking part. - `Scenario.toScript lovelace` pays the validator's own script address, which has no staking part. - `Scenario.toStakedScript stake lovelace` pays the validator's own payment credential with a staking part. That is another address: `Tx.continuingOutputs` of an output spent from `toScript`'s address doesn't include it. - `Scenario.withDatum data` attaches an inline datum. - `Scenario.withDatumHash data` holds the datum by hash instead: the output carries the datum's hash, and the transaction carries the datum among its witnesses. `Tx.datumOf` finds it there; `Tx.inlineDatum` returns `Nothing` for such an output. - `Scenario.withToken policy name quantity` adds tokens. The policy is `Scenario.Own`, the validator's own policy, or `Scenario.Policy bytes` for any other. The runner builds every value the ledger's way, as the script expects a [`Value`](https://elci-lang.dev/reference/core-modules/#value) to be: policies and token names in ascending order, quantities of a repeated token added up, and zero quantities left out. So the order of your `withToken` and `Scenario.minting` calls never matters, and `Scenario.minting name 0` mints nothing. The ledger limits policy ids and token names to 32 bytes, and so does the runner: a longer one fails the scenario before the validator runs, even with `expectFailure`, with `cannot build the transaction: A scenario value has a policy id or token name longer than 32 bytes, which the ledger does not allow`. Repricing requires a continuing output, one of `Tx.continuingOutputs`: an output back at the market's address that carries the updated listing: ```elm sellerReprices : Scenario sellerReprices = settle (Reprice 40000000) |> Scenario.signedBy seller |> Scenario.output (Scenario.toScript 2000000 |> Scenario.withDatum (Data.encode { listing | price = 40000000 }) ) ``` To give the spent output tokens or a different value, list it yourself with `Scenario.input` and the same reference. The runner then uses your input instead of the default one. As on the ledger, it must hold the datum that `Scenario.spend` gives, inline or by hash; otherwise the scenario fails before the validator runs with `cannot build the transaction: the spent output ... is listed with a different datum from the one Scenario.spend gives`. This test also needs `import Bytes`: ```elm listingHoldsAnNft : Scenario listingHoldsAnNft = settle Delist |> Scenario.signedBy seller |> Scenario.input (Scenario.outRef "listing" 0) (Scenario.toScript 2000000 |> Scenario.withDatum (Data.encode listing) |> Scenario.withToken (Scenario.Policy (Bytes.replicate 28 7)) (Bytes.fromString "ART") 1 ) ``` `Scenario.referenceInput ref output` adds a reference input, such as an oracle output your validator reads, and `Scenario.worth lovelace` changes the lovelace of the default spent output. ## Minting `Scenario.mint validator` runs the validator as a minting policy. Its policy id is the applied script's hash, so the tokens you add with `Scenario.minting name quantity` are minted under that policy. Token names are bytes, usually written with `Bytes.fromString`: ```elm title="tests/TicketTest.elm" module TicketTest exposing ( burningIsRejected , organizerMintsOneTicket , twoTicketsAreRejected , unsignedMintIsRejected ) import Bytes import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Scenario exposing (Scenario) organizer : PubKeyHash organizer = Scenario.key "organizer" tickets : Int -> Scenario tickets quantity = Scenario.mint "ticket" |> Scenario.parameters [ Data.encode organizer ] |> Scenario.minting (Bytes.fromString "TICKET") quantity organizerMintsOneTicket : Scenario organizerMintsOneTicket = tickets 1 |> Scenario.signedBy organizer twoTicketsAreRejected : Scenario twoTicketsAreRejected = tickets 2 |> Scenario.signedBy organizer |> Scenario.expectFailure burningIsRejected : Scenario burningIsRejected = tickets -1 |> Scenario.signedBy organizer |> Scenario.expectFailure unsignedMintIsRejected : Scenario unsignedMintIsRejected = tickets 1 |> Scenario.expectFailure ``` A negative quantity burns, so `tickets -1` burns one ticket, which this policy rejects. As in Elm, `tickets -1` passes the argument `-1`, while `quantity - 1` subtracts. ```sh elci test --match TicketTest ``` ```text PASS TicketTest.organizerMintsOneTicket (cpu 11788207, mem 41590) PASS TicketTest.twoTicketsAreRejected (cpu 9157880, mem 32745) PASS TicketTest.burningIsRejected (cpu 9157880, mem 32745) PASS TicketTest.unsignedMintIsRejected (cpu 11483010, mem 39957) 4 passed, 0 failed ``` ## Withdrawing rewards `Scenario.withdraw validator` runs the validator for a withdrawal from its own staking credential: ```elm operatorWithdrawsRewards : Scenario operatorWithdrawsRewards = Scenario.withdraw "market" |> Scenario.parameters [ Data.encode operator ] |> Scenario.signedBy operator othersCannotWithdraw : Scenario othersCannotWithdraw = Scenario.withdraw "market" |> Scenario.parameters [ Data.encode operator ] |> Scenario.signedBy seller |> Scenario.expectFailure ``` `market` is one script with two handlers, so these scenarios and the spending ones above run the same compiled script. The purpose in the context selects the handler. ## Certificates, votes and proposals The remaining purposes take what the ledger supplies for them, built with `Julc.Ledger` constructors. These are the transactions that `elci init --purpose certify`, `vote` and `propose` generate, with `import Julc.Data as Data`, `import Julc.Ledger as Ledger` and a test key `owner`: ```elm Scenario.certify "main" 0 (Ledger.RegStaking (Ledger.PubKeyCredential owner) (Data.toOptional Nothing)) Scenario.vote "main" (Ledger.DRepVoter (Ledger.PubKeyCredential owner)) Scenario.propose "main" 0 (Ledger.ProposalProcedure 0 (Ledger.PubKeyCredential owner) Ledger.InfoAction) ``` The integer after the validator name is the index of the certificate or proposal in the transaction. ## Run the scenarios ```sh elci test --match MarketTest ``` ```text PASS MarketTest.buyerPaysTheSeller (cpu 33518015, mem 117583) PASS MarketTest.underpayingFails (cpu 24050900, mem 93018) PASS MarketTest.paymentForAnotherListingFails (cpu 27935021, mem 101832) PASS MarketTest.sellerDelists (cpu 15573596, mem 65977) PASS MarketTest.strangerCannotDelist (cpu 15952029, mem 67909) PASS MarketTest.buyingAfterExpiryFails (cpu 33502015, mem 117483) PASS MarketTest.buyingWithoutADeadlineFails (cpu 31346102, mem 111033) PASS MarketTest.sellerReprices (cpu 36080596, mem 124349) PASS MarketTest.operatorWithdrawsRewards (cpu 7879274, mem 35433) PASS MarketTest.othersCannotWithdraw (cpu 8257707, mem 37365) 10 passed, 0 failed ``` For a scenario, the CPU and memory are what the **validator** consumed for that transaction, including a rejected one. These execution units are what the script adds to your transaction's fee. When a scenario does not get the outcome it expects, the report says why. Here, `strangerDelists` is `strangerCannotDelist` without `expectFailure`, `sellerCannotDelist` is `sellerDelists` with it, `typo` names a validator that does not exist, and `unparameterized` is `operatorWithdrawsRewards` without `Scenario.parameters`: ```text FAIL MarketTest.strangerDelists (cpu 15952029, mem 67909) validator market rejected the transaction: Error term encountered FAIL MarketTest.sellerCannotDelist (cpu 15573596, mem 65977) validator market accepted a transaction expected to fail FAIL MarketTest.typo cannot prepare validator 'markt': no [validators.markt] in elci.toml FAIL MarketTest.unparameterized cannot prepare validator 'market': it takes 1 parameter [operator], but the scenario gives 0; set them with Scenario.parameters ``` The last two fail before the validator runs, so they fail even with `Scenario.expectFailure`: a scenario cannot pass just because it was set up wrongly. A validator that returns `False` fails with `Error term encountered`. [Debugging and budgets](https://elci-lang.dev/testing/debugging/) shows how to find out which check failed. :::caution[Set the redeemer] The default redeemer is `Scenario.unit`, which encodes as `Constr 0 []`. That is also the encoding of the first constructor of a union without fields, such as `Buy`. A scenario that forgets `Scenario.redeemer` can therefore run a different action than you meant. Set the redeemer explicitly. ::: ## All builder functions The complete signatures are in the [test library reference](https://elci-lang.dev/reference/test-library/). **Start a scenario** with the purpose. `validator` is the name of a `[validators.]` table. | Function | Transaction | | --- | --- | | `spend validator ref datum` | spends `ref`, held at the script address with an inline `datum` | | `spendWithoutDatum validator ref` | spends `ref`, held at the script address without a datum | | `mint validator` | mints or burns under the validator's own policy | | `withdraw validator` | withdraws from the validator's own staking credential | | `certify validator index certificate` | publishes a `TxCert` at an index | | `vote validator voter` | votes as `voter` | | `propose validator index procedure` | submits a `ProposalProcedure` at an index | **Describe the transaction.** | Function | Effect | Default | | --- | --- | --- | | `redeemer data` | the redeemer | `Scenario.unit` | | `parameters [ data, ... ]` | deployment parameters, in declared order; required when the validator has parameters | none | | `signedBy key` | adds a required signer | no signers | | `fee lovelace` | the transaction fee | 200000 | | `validFrom time` | inclusive lower bound of the validity range | unbounded | | `validBefore time` | exclusive upper bound of the validity range | unbounded | **Add inputs, outputs and tokens.** | Function | Effect | | --- | --- | | `input ref output` | adds an input; with the spent `ref`, replaces the default spent output, and must hold the spend's datum | | `referenceInput ref output` | adds a reference input | | `output out` | adds an output | | `minting name quantity` | mints (positive) or burns (negative) under the own policy | | `worth lovelace` | lovelace held by the default spent output (default 2000000) | | `toKey key lovelace` | an output to the key's address, without a staking part | | `toScript lovelace` | an output to the validator's own address | | `toStakedScript stake lovelace` | an output to the validator's own payment credential with the staking credential `stake` | | `toAddress address lovelace` | an output to `address` | | `withDatum data` | gives an output an inline datum | | `withDatumHash data` | gives an output the datum's hash, and the transaction the datum as a witness | | `withToken policy name quantity` | adds tokens to an output; `policy` is `Own` or `Policy bytes` | **Set the expectation.** | Function | Effect | | --- | --- | | `expectFailure` | the validator must reject the transaction | | `withinBudget cpu memory` | the execution must cost at most this CPU and memory | **Make test data.** | Function | Result | | --- | --- | | `key name` | a deterministic 28-byte `PubKeyHash` derived from `name` | | `outRef name index` | a deterministic `TxOutRef` derived from `name` and `index` | | `unit` | the `Data` of `()` | ## Limits Scenarios cover single and multiple signatures, validity ranges, lovelace, tokens of the own policy and of other policies, inline datums, datums by hash, reference inputs and every purpose. The validator's own address has no staking part. The following need a [Java test suite](https://elci-lang.dev/testing/java/), where you build the context yourself: - reference scripts; - other staking credentials and additional withdrawals; - more than one script in a transaction. ## Next steps - Run a scenario for many generated inputs with [property-based tests](https://elci-lang.dev/testing/properties/). - Find out why a validator rejects a transaction in [Debugging and budgets](https://elci-lang.dev/testing/debugging/). --- # Property-based tests > State a rule that must hold for every input, let elci test generate hundreds of inputs, and get the smallest counterexample when the rule breaks. Source: https://elci-lang.dev/testing/properties/ A property is a rule that should hold for **every** input, such as "a buyer who pays less than the price is rejected". In this page you write properties for pure functions and for whole transactions, read a shrunk counterexample, and learn to shape generated inputs so they find real bugs. ## Why properties A unit test or a scenario checks one input you thought of. The bugs that hurt in smart contracts usually sit at inputs nobody thought of: a negative amount, an empty byte string, a validity range that ends exactly at the deadline, a 64-bit number. A property describes the rule once. `elci test` then: 1. generates arguments from the property's parameter types; 2. runs the property 100 times, starting with small values; 3. when a run fails, shrinks the arguments to a small counterexample and reports it. ## Your first property Take the `Pricing` module from [Unit tests](https://elci-lang.dev/testing/unit-tests/). Adding the fee to an amount should never give a total below the amount. Write that as a property in `tests/PricingProperties.elm`: ```elm title="tests/PricingProperties.elm" module PricingProperties exposing (quotesNeverLoseValue) import Pricing import Property exposing (Property) quotesNeverLoseValue : Int -> Property quotesNeverLoseValue amount = Property.holds ((Pricing.quote amount).total >= amount) ``` A property is an exposed function whose result type is `Property`. Its parameters are the inputs to generate: here one `Int`. Run it: ```sh elci test --seed 42 --match PricingProperties ``` ```text FAIL PricingProperties.quotesNeverLoseValue (15 runs, cpu 5409589, mem 21823) counterexample after 15 runs, shrunk 4 times: quotesNeverLoseValue (-334) the property does not hold 0 passed, 1 failed (seed 42) ``` The 15th run failed on a negative number, and the runner shrank it four times to `-334`. That is the negative amount closest to zero that breaks the rule: `-334 * 30 // 10000` is `-1`, because `//` truncates toward zero, so the total is `-335`. No amount between `-333` and `0` fails, which is why shrinking stopped there. The counterexample is printed as an Elci call, so you can reproduce it directly in a unit test. ### State the real rule The property found a real question: what should a quote for a negative amount be? A lovelace amount can never be negative, so the honest fix is to state that in the property. Use `modBy` to turn any generated integer into a valid amount: ```elm title="tests/PricingProperties.elm" module PricingProperties exposing (quotesNeverLoseValue) import Pricing import Property exposing (Property) {-| Any amount of lovelace up to the 45 billion ADA supply. -} quotesNeverLoseValue : Int -> Property quotesNeverLoseValue raw = let amount = modBy 45000000000000000 raw in Property.holds ((Pricing.quote amount).total >= amount) ``` ```text PASS PricingProperties.quotesNeverLoseValue (100 runs, max cpu 4641008, mem 18984) 1 passed, 0 failed (seed 42) ``` For a passing property, the budget is the costliest of its runs. For a failing one, it is the counterexample's run. ## The Property module `Property` has one type and three ways to build it: | Function | The property holds when | | --- | --- | | `Property.holds condition` | the `Bool` is `True` | | `Property.expect expectation` | the `Expectation` passes, so failures show `expected` and `actual` | | `Property.scenario transaction` | the `Scenario` has its expected outcome, so a property can run your deployed validator | The result type is what makes a function a property. An exposed function that returns a `Bool`, an `Expectation` or a `Scenario` is a helper, never a test, however many arguments it takes. So you can write helpers freely. The only helpers to keep unexposed are those that themselves return a `Property`, because an exposed function returning `Property` is run as a property. A zero-argument value of type `Property` is allowed too. It runs once, like a unit test. ## Properties over transactions `Property.scenario` runs a transaction against the deployed validator on every run. Here are four properties for the marketplace from [Transaction scenarios](https://elci-lang.dev/testing/scenarios/). The module repeats the scenario helpers; in a larger project, put them in a shared test module. ```elm title="tests/MarketProperties.elm" module MarketProperties exposing ( buyersMustPayThePrice , buyingClosesAtExpiry , listingsRoundTrip , onlyTheSellerDelists ) import Expect import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Market exposing (Action(..), Listing) import Property exposing (Property) import Scenario exposing (Output, Scenario) seller : PubKeyHash seller = Scenario.key "seller" listing : Listing listing = { seller = seller, price = 50000000, expires = 2000 } settle : Action -> Scenario settle action = Scenario.spend "market" (Scenario.outRef "listing" 0) (Data.encode listing) |> Scenario.parameters [ Data.encode (Scenario.key "operator") ] |> Scenario.redeemer (Data.encode action) payment : Int -> Output payment lovelace = Scenario.toKey seller lovelace |> Scenario.withDatum (Data.encode (Scenario.outRef "listing" 0)) {-| Succeeds when `ok`, and must be rejected otherwise. -} expecting : Bool -> Scenario -> Property expecting ok transaction = if ok then Property.scenario transaction else Property.scenario (Scenario.expectFailure transaction) listingsRoundTrip : Listing -> Property listingsRoundTrip generated = Property.expect (Expect.equal generated (Data.decode (Data.encode generated))) {-| Whatever a listing holds, only its seller can delist it. -} onlyTheSellerDelists : Listing -> Bool -> Property onlyTheSellerDelists generated bySeller = let signer = if bySeller then generated.seller else Scenario.key "stranger" in Scenario.spend "market" (Scenario.outRef "listing" 0) (Data.encode generated) |> Scenario.parameters [ Data.encode (Scenario.key "operator") ] |> Scenario.redeemer (Data.encode Delist) |> Scenario.signedBy signer |> expecting bySeller buyersMustPayThePrice : Int -> Property buyersMustPayThePrice shortBy = settle Buy |> Scenario.output (payment (listing.price - shortBy)) |> Scenario.validBefore 1500 |> expecting (shortBy <= 0) buyingClosesAtExpiry : Int -> Property buyingClosesAtExpiry offset = let end = listing.expires + offset in settle Buy |> Scenario.output (payment listing.price) |> Scenario.validBefore end |> expecting (end <= listing.expires) ``` - `listingsRoundTrip` checks that any `Listing` survives encoding and strict decoding, the path every datum takes into your script. - `onlyTheSellerDelists` generates whole listings and signs either with the listing's own seller or with a stranger. The validator must accept exactly the first case. - `buyersMustPayThePrice` pays the seller `shortBy` lovelace less than the price. Any shortfall above zero must be rejected, and paying the price or more must succeed. - `buyingClosesAtExpiry` ends the validity range `offset` milliseconds from the listing's expiry. `expecting` is the helper that turns "should this succeed?" into the right scenario. It returns a `Property`, so it stays unexposed. ```sh elci test --seed 7 --match MarketProperties ``` ```text PASS MarketProperties.listingsRoundTrip (100 runs, max cpu 11509152, mem 46989) PASS MarketProperties.onlyTheSellerDelists (100 runs, max cpu 15953522, mem 67909) PASS MarketProperties.buyersMustPayThePrice (100 runs, max cpu 33518015, mem 117583) PASS MarketProperties.buyingClosesAtExpiry (100 runs, max cpu 33518015, mem 117583) 4 passed, 0 failed (seed 7) ``` ### A property catches a bug Suppose someone changes `sellerPaid` in `src/Market.elm` to accept payments 100 lovelace short of the price, `(listing.price - 100)`. The property finds the boundary on its own: ```text FAIL MarketProperties.buyersMustPayThePrice (1 run, cpu 33683223, mem 117985) counterexample after 1 run, shrunk 5 times: buyersMustPayThePrice 1 validator market accepted a transaction expected to fail 0 passed, 1 failed (seed 7) ``` Paying 1 lovelace short is accepted. The `underpayingFails` scenario catches this particular bug as well, but only because it underpays by exactly 1 lovelace. The property does not depend on you picking the right example. ### A property finds an edge case The first version of `buyingClosesAtExpiry` stated the rule as "succeed when the range ends before the expiry", a negative offset: ```elm buyingClosesAtExpiry : Int -> Property buyingClosesAtExpiry offset = settle Buy |> Scenario.output (payment listing.price) |> Scenario.validBefore (listing.expires + offset) |> expecting (offset < 0) ``` ```text FAIL MarketProperties.buyingClosesAtExpiry (1 run, cpu 33518015, mem 117583) counterexample after 1 run: buyingClosesAtExpiry 0 validator market accepted a transaction expected to fail 0 passed, 1 failed (seed 7) ``` An offset of `0` ends the range at the expiry, and the validator accepts it. The validator is right: `validBefore` is exclusive, so a transaction valid before `2000` can't run at `2000`, and every time it can run at is before the expiry. The final version states the complete rule: `end <= listing.expires`. ## Seeds and runs The summary line of a run with properties shows its seed. Without `--seed`, the seed is random, so each run explores new inputs: ```text 8 passed, 0 failed (seed -2828489748881525870) ``` Two options control properties: - `--seed ` reproduces a run exactly. Each property derives its own seed from `n` and its name, so a property sees the same inputs whatever `--match` selects. Pass the seed from a failing run to reproduce the failure. - `--fuzz ` changes the number of runs per property. The default is 100. ```sh elci test --seed 42 --fuzz 1000 --match PricingProperties ``` ```text PASS PricingProperties.quotesNeverLoseValue (1000 runs, max cpu 4641008, mem 18984) 1 passed, 0 failed (seed 42) ``` ## Generated values Every generated argument is a valid value of its type. It is produced as Plutus Data and decoded by the same strict decoder a validator uses for its datum and redeemer, so a property sees exactly what the ledger could send. | Type | Generated values | Shrinks toward | | --- | --- | --- | | `Int` | mostly small values around 0, the edge cases 0, 1 and -1, and some values up to 64 bits | `0` | | `Bytes`, `String` | up to 64 bytes, sometimes exactly 28 or 32 (hash sizes) | `#(empty)`, `""` | | `PubKeyHash`, `ScriptHash`, `ValidatorHash`, `PolicyId` parameter | exactly 28 bytes | 28 zero bytes | | `TxId`, `DatumHash` parameter | exactly 32 bytes | 32 zero bytes | | `TokenName` parameter | 0 to 32 bytes | `#(empty)` | | `List a`, `Map k v` | grow with the run; map keys are distinct | shorter lists and maps | | `Dict k v` | grow with the run; keys are sorted and distinct, as decoding requires | smaller dictionaries, still sorted | | `Value` | canonical values: often some lovelace, and a few tokens under 28-byte policies; quantities may be negative, never zero | fewer policies, down to the empty value | | records, tuples | built from their fields | each field shrinks | | unions, `Maybe` | every constructor is used | earlier constructors, such as `Nothing` | | recursive unions | always finite; the first runs use the shallowest constructors | their own subterms | The exact lengths apply to a **parameter** of a hash type. A hash inside a record, such as a listing's `seller`, follows its Data representation and can have any length, including zero. Counterexamples are printed in Elci syntax. Byte strings are hex, record fields appear in alphabetical order, and constructors appear without their module name. Some shrunk counterexamples from failing properties: | Property parameters | Counterexample | | --- | --- | | `Listing -> Bool` | `onlyTheSellerDelists { expires = 0, price = 0, seller = #(empty) } False` | | `Action -> Maybe Int -> ( Int, Bool )` | `unions Buy Nothing ( 0, False )` | | `Map Int Bytes`, failing for two or more entries | `maps (Map.fromList [( 0, #(empty) ), ( 1, #(empty) )])` | | `Shape`, a recursive union with constructors `Dot`, `Line Int` and `Pair Shape Shape` | `smallShapes (Pair (Pair Dot Dot) Dot)` | ## Shape the inputs Generated integers cluster around 0, and shrinking moves toward 0. Design your parameters so that 0 sits where the interesting behavior is. **Generate an offset from the boundary.** `buyersMustPayThePrice` takes a shortfall and `buyingClosesAtExpiry` an offset from the expiry, rather than a price or a time. The boundary between success and failure is then at 0, where most generated values fall, and a counterexample shrinks to the smallest step across it, such as `buyersMustPayThePrice 1`. **Bound values with `modBy`.** When the rule only makes sense in a range, such as non-negative amounts, transform the raw argument with `modBy` as in `quotesNeverLoseValue`. `modBy n` always gives a result from 0 to `n - 1`. Prefer offsets when a boundary matters. With `end = modBy 4000 raw` instead of an offset, a validator bug that accepted purchases up to 50 ms after the expiry passed in 9 of 10 seeds: the generated values rarely land in a 50-wide window. The offset version reported `buyingClosesAtExpiry 1` on its first failing run. :::note[Counterexamples show the raw argument] Arguments are shrunk before your transformation runs, and the report shows the generated argument, not the transformed value. For a property that computes `amount = 1000 + modBy 1000000 raw`, a failure is reported as `bigOrdersPayAtLeastFour 0` although the property checked `amount = 1000`. Apply the same transformation to read it. ::: ## Limits A property's parameters must be concrete, Data-encodable types. `elci test` rejects others before running anything. For `applies : (Int -> Int) -> Property` it reports: ```text tests/Lim.elm:6:1: ELCI2009: Property applies needs Data-encodable parameters to generate; functions, arrays and ScriptContext cannot be generated (take a List and use Array.fromList) 6| applies : (Int -> Int) -> Property ^^^^^^^ ``` - **Functions** cannot be generated. - **Arrays** have no Data encoding. Take a `List` and convert it with `Array.fromList` inside the property. - **`ScriptContext`** cannot be generated. Use `Property.scenario` to run the validator in a complete context instead. - **Type variables** must be concrete: `anything : a -> Property` is rejected with `ELCI2009: Property anything needs concrete parameter types; add a type annotation`. Generation and shrinking come from julc-testkit's `DataGenerators` and `PropertyCheck`. They are seeded and need no reflection, so the native `elci` binary runs properties too. For property tests driven from Java with jqwik, see [Testing from Java](https://elci-lang.dev/testing/java/). --- # Testing from Java > Load the scripts elci build publishes into JUnit, build ledger contexts with julc-testkit and run jqwik property tests with datums generated from the validator's ABI. Source: https://elci-lang.dev/testing/java/ `elci test` covers most contracts. When you need full control over the ledger context, or want your contract tests next to the Java code of your off-chain application, you can test the same scripts from JUnit. In this page you set up a Gradle test suite, evaluate a validator with julc-testkit, and write jqwik properties with generated datums and redeemers. Reach for Java when a test needs something [scenarios](https://elci-lang.dev/testing/scenarios/) cannot describe: reference scripts, other staking credentials or several withdrawals, or more than one script in a transaction. ## Set up a test suite Elci is a development preview and is not published to a Maven repository. Put the JARs of your Elci build on the test classpath instead: `./gradlew installDist` in the Elci repository writes them to `build/install/elci/lib`, together with the pinned Julc libraries they need. The jqwik integration, `julc-testkit-jqwik`, comes from the BloxBean Maven repository, which publishes the pinned Julc snapshot. ```groovy title="build.gradle" plugins { id 'java' } repositories { mavenCentral() maven { url = uri('https://repo.bloxbean.org/maven/snapshots') mavenContent { snapshotsOnly() } } } java { toolchain { languageVersion = JavaLanguageVersion.of(25) } } // The Elci installation: the elci-lang JAR and its pinned Julc libraries. def elciLib = '/path/to/elci-lang/build/install/elci/lib' dependencies { testImplementation fileTree(elciLib) { include '*.jar' } testImplementation 'org.julclang:julc-testkit-jqwik:0.1.0-pre20-ebaf028-SNAPSHOT' testImplementation 'net.jqwik:jqwik:1.9.2' testImplementation platform('org.junit:junit-bom:5.11.4') testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } test { useJUnitPlatform() testLogging { events 'passed', 'failed', 'skipped' } } ``` Keep the Elci project in its own directory next to the Java sources. Tests run from the Gradle project directory, so `Path.of("market")` finds it: - my-dapp/ - build.gradle - settings.gradle - market/ the Elci project from Transaction scenarios - elci.toml - src/ - tests/ - src/ - test/ - java/ - market/ - MarketValidatorTest.java - MarketProperties.java ## ElciTestkit `org.elci.testkit.ElciTestkit` compiles Elci code exactly as the CLI does, so your JUnit tests evaluate the scripts you deploy. | Method | Returns | | --- | --- | | `validator(project, name, parameters...)` | the `[validators.]` script as a `Program`, with its deployment parameters applied in declared order | | `abi(project, name)` | the validator's `ValidatorAbi`: its parameter, datum and redeemer types | | `entrypoint(project)` | the `[entrypoint]` function as a `Program`, as `elci eval` runs it | | `value(project, module, name)` | any exposed zero-argument declaration of a source or test module, as a `Program` | `project` is the directory that contains `elci.toml`. All four methods declare `IOException` for a project that cannot be read. `validator` and `abi` throw `IllegalArgumentException` for an unknown validator, and `validator` also when the number of parameters is wrong: ```java @Test void theValidatorNeedsItsParameter() { assertThrows(IllegalArgumentException.class, () -> ElciTestkit.validator(MARKET, "market")); } ``` ## Encode datums and redeemers In Java you pass `PlutusData`, so you encode values by hand with the same rules Elci uses: - A **record** is constructor 0 with its fields in **alphabetical order of their names**. `Listing { seller, price, expires }` is `Constr 0 [expires, price, seller]`. - A **custom type** constructor is numbered by its position in the declaration, from 0: `Buy` is 0, `Reprice` 1 and `Delist` 2. - Ledger types such as `PubKeyHash` and `TxOutRef` have `toPlutusData()`. `PlutusData.UNIT` is the Data of `()`. ```java /** A Listing record encodes its fields in name order: expires, price, seller. */ static PlutusData listing(long price, long expires) { return PlutusData.constr(0, PlutusData.integer(expires), PlutusData.integer(price), SELLER.toPlutusData()); } /** Action constructors are numbered in declaration order: Buy 0, Reprice 1, Delist 2. */ static final PlutusData DELIST = PlutusData.constr(2); ``` ## A JUnit test julc-testkit provides the rest: - `ScriptContextTestBuilder` builds the ledger context. Start with the purpose, `spending(ref, datum)`, `spending(ref)`, `minting(policyId)`, `rewarding(credential)`, `certifying(index, cert)`, `voting(voter)` or `proposing(index, procedure)`, then add `redeemer`, `signer`, `input`, `referenceInput`, `output`, `mint`, `fee`, `validRange`, `withdrawal`, `datum` (a datum hash and its datum) and more. Finish with `buildPlutusData()`. - `ValidatorTest.evaluate(program, context)` runs a script and returns an `EvalResult`. `assertValidates` and `assertRejects` evaluate and assert in one step. - `BudgetAssertions` checks an `EvalResult`: `assertSuccess`, `assertFailure`, `assertBudgetUnder(result, cpu, memory)`, `assertTrace(result, messages...)`, `assertTraceExact` and `assertNoTraces`. Unlike a scenario, the builder fills in nothing about your script. You choose the spent reference, credentials and policy ids yourself, and `spending(ref, datum)` sets the purpose without adding the spent output to the inputs: add it with `input` when your validator looks for its own input, for example through `Tx.ownInput` or `Tx.continuingOutputs`. The fee defaults to 0 and the validity range to always. Here is a complete test class for the marketplace's `market` validator: ```java title="src/test/java/market/MarketValidatorTest.java" package market; import static org.junit.jupiter.api.Assertions.*; import java.io.IOException; import java.math.BigInteger; import java.nio.file.Path; import java.util.Arrays; import org.elci.testkit.ElciTestkit; import org.julclang.core.PlutusData; import org.julclang.core.Program; import org.julclang.ledger.*; import org.julclang.testkit.BudgetAssertions; import org.julclang.testkit.ScriptContextTestBuilder; import org.julclang.testkit.ValidatorTest; import org.junit.jupiter.api.Test; class MarketValidatorTest { static final Path MARKET = Path.of("market"); static final PubKeyHash OPERATOR = new PubKeyHash(new byte[28]); static final PubKeyHash SELLER = new PubKeyHash(filled(28, 1)); static final TxOutRef LISTED = new TxOutRef(new TxId(new byte[32]), BigInteger.ZERO); static byte[] filled(int length, int value) { var bytes = new byte[length]; Arrays.fill(bytes, (byte) value); return bytes; } /** A Listing record encodes its fields in name order: expires, price, seller. */ static PlutusData listing(long price, long expires) { return PlutusData.constr(0, PlutusData.integer(expires), PlutusData.integer(price), SELLER.toPlutusData()); } /** Action constructors are numbered in declaration order: Buy 0, Reprice 1, Delist 2. */ static final PlutusData DELIST = PlutusData.constr(2); @Test void theSellerDelists() throws IOException { Program market = ElciTestkit.validator(MARKET, "market", OPERATOR.toPlutusData()); var ctx = ScriptContextTestBuilder.spending(LISTED, listing(50_000_000, 2000)) .redeemer(DELIST) .signer(SELLER) .buildPlutusData(); ValidatorTest.assertValidates(market, ctx); } @Test void onlyTheOperatorWithdrawsRewards() throws IOException { Program market = ElciTestkit.validator(MARKET, "market", OPERATOR.toPlutusData()); var credential = new Credential.ScriptCredential(new ScriptHash(new byte[28])); var signed = ScriptContextTestBuilder.rewarding(credential) .signer(OPERATOR) .redeemer(PlutusData.UNIT) .buildPlutusData(); var result = ValidatorTest.evaluate(market, signed); BudgetAssertions.assertSuccess(result); BudgetAssertions.assertBudgetUnder(result, 10_000_000, 50_000); var unsigned = ScriptContextTestBuilder.rewarding(credential) .redeemer(PlutusData.UNIT) .buildPlutusData(); ValidatorTest.assertRejects(market, unsigned); } @Test void theValidatorNeedsItsParameter() { assertThrows(IllegalArgumentException.class, () -> ElciTestkit.validator(MARKET, "market")); } } ``` Run it with `./gradlew test`. :::tip `BudgetAssertions.assertTrace` pairs well with `Debug.trace` in your validator: it checks that a rejected transaction was rejected for the reason you expect. See [Debugging and budgets](https://elci-lang.dev/testing/debugging/). ::: ## Properties with jqwik `SchemaArbitraries` from `julc-testkit-jqwik` reads a validator's ABI and generates valid datums, redeemers and parameters for jqwik. jqwik then runs each property many times and shrinks failures with its own strategies. | Method | Generates | | --- | --- | | `SchemaArbitraries.datum(abi)` | datums of the validator's spend handler | | `SchemaArbitraries.redeemer(abi, purpose)` | redeemers of the handler for a `ContractSchema.Purpose`, such as `SPEND` | | `SchemaArbitraries.parameters(abi)` | lists of deployment parameters | | `SchemaArbitraries.forType(type)` | values of any `PirType` | ```java title="src/test/java/market/MarketProperties.java" package market; import static org.junit.jupiter.api.Assertions.*; import java.io.IOException; import java.io.UncheckedIOException; import java.math.BigInteger; import java.nio.file.Path; import net.jqwik.api.*; import org.elci.testkit.ElciTestkit; import org.julclang.compiler.backend.ValidatorAbi; import org.julclang.compiler.schema.ContractSchema; import org.julclang.core.PlutusData; import org.julclang.core.Program; import org.julclang.ledger.PubKeyHash; import org.julclang.ledger.TxId; import org.julclang.ledger.TxOutRef; import org.julclang.testkit.ScriptContextTestBuilder; import org.julclang.testkit.ValidatorTest; import org.julclang.testkit.jqwik.SchemaArbitraries; class MarketProperties { static final Path MARKET = Path.of("market"); static final PubKeyHash OPERATOR = new PubKeyHash(new byte[28]); static final TxOutRef LISTED = new TxOutRef(new TxId(new byte[32]), BigInteger.ZERO); static final ValidatorAbi ABI; static final Program SCRIPT; static { try { ABI = ElciTestkit.abi(MARKET, "market"); SCRIPT = ElciTestkit.validator(MARKET, "market", OPERATOR.toPlutusData()); } catch (IOException e) { throw new UncheckedIOException(e); } } @Provide Arbitrary listings() { return SchemaArbitraries.datum(ABI); } @Provide Arbitrary actions() { return SchemaArbitraries.redeemer(ABI, ContractSchema.Purpose.SPEND); } /** With no outputs and no signature, no action can succeed. */ @Property(tries = 100) void emptyTransactionsAreRejected(@ForAll("listings") PlutusData listing, @ForAll("actions") PlutusData action) { var ctx = ScriptContextTestBuilder.spending(LISTED, listing).redeemer(action).buildPlutusData(); assertFalse(ValidatorTest.evaluate(SCRIPT, ctx).isSuccess(), () -> listing + " " + action); } /** A listing encodes its fields in name order: expires, price, seller. */ @Property(tries = 100) void theSellerCanAlwaysDelist(@ForAll("listings") PlutusData listing) { var fields = ((PlutusData.ConstrData) listing).fields(); var seller = new PubKeyHash(((PlutusData.BytesData) fields.get(2)).value()); var ctx = ScriptContextTestBuilder.spending(LISTED, listing) .redeemer(PlutusData.constr(2)) .signer(seller) .buildPlutusData(); assertTrue(ValidatorTest.evaluate(SCRIPT, ctx).isSuccess(), listing::toString); } } ``` The script and ABI are compiled once, in the static initializer, and shared by every try. `elci test` properties and jqwik properties complement each other. `elci test` needs no Java and generates Elci-typed arguments, including records and unions you define in tests. With jqwik you build and vary the whole ledger context in Java, and its generators compose with the rest of your Java test code. ## Evaluate other declarations `ElciTestkit.value` compiles any exposed zero-argument declaration, including tests and helpers in test modules, and `ElciTestkit.entrypoint` compiles a function project's `[entrypoint]`. Evaluate them with `ValidatorTest.evaluate` and read the result term: ```java static Constant resultOf(EvalResult result) { var success = assertInstanceOf(EvalResult.Success.class, result); return ((Term.Const) success.resultTerm()).value(); } @Test void unitTestsAndEntrypointsEvaluate() throws IOException { var free = ElciTestkit.value(Path.of("shop"), "PricingTest", "smallAmountsAreFree"); assertEquals(new Constant.BoolConst(true), resultOf(ValidatorTest.evaluate(free))); var main = ElciTestkit.entrypoint(Path.of("fn")); assertEquals(new Constant.IntegerConst(BigInteger.valueOf(42)), resultOf(ValidatorTest.evaluate(main))); } ``` Here `shop` is the project from [Unit tests](https://elci-lang.dev/testing/unit-tests/), and `fn` is a project made by `elci init fn --purpose function`, whose `main` is `42`. `Constant` and `Term` are in `org.julclang.core`, and `EvalResult` is in `org.julclang.vm`. --- # Debugging and budgets > Read elci test failures, find out why a validator rejects a transaction with Debug.trace, and keep execution budgets in check with Scenario.withinBudget. Source: https://elci-lang.dev/testing/debugging/ When a test fails, `elci test` tells you what it expected and what happened. In this page you learn to read those reports, to add traces that explain a rejection, and to use the CPU and memory numbers to keep your contracts cheap. ## Read a failure Every failing test prints a `FAIL` line, then indented details, then any traces. The details depend on the kind of test: | Kind of test | Typical detail | | --- | --- | | `Bool` | `evaluated to False` | | `Expect.equal` | `expected:` and `actual:`, as Data | | other `Expect` functions | the function name or your `onFail` message, then `values:` | | runtime error in a test | `evaluation failed:` and the VM's error | | scenario | `validator rejected the transaction: ...` or `validator accepted a transaction expected to fail` | | scenario with `withinBudget` | `Budget exceeded: ...` | | property | `counterexample after N runs, shrunk M times:`, the shrunk call, and the failure of that run | [Unit tests](https://elci-lang.dev/testing/unit-tests/) explains how values are printed as Data, and [Property-based tests](https://elci-lang.dev/testing/properties/) how to read a counterexample. Errors inside a test are reported with the VM's own message. Two examples from a test module `TraceTest` that imports `Julc.Data as Data`: ```elm splitsEvenly : Bool splitsEvenly = 1000 // 0 == 0 decodesAnAmount : Expectation decodesAnAmount = let amount : Int amount = Data.decode (Data.encode "ten") in Expect.equal 10 amount ``` ```text FAIL TraceTest.splitsEvenly (cpu 276441, mem 1001) evaluation failed: QuotientInteger: division by zero FAIL TraceTest.decodesAnAmount (cpu 933360, mem 5469) evaluation failed: UnIData: not IntData ``` A failed `Data.decode` never becomes a `Maybe`: like a malformed datum on-chain, it stops evaluation. ## Why a validator rejected a transaction These are the messages you meet most often in a failing scenario, and what they usually mean: | Message | Usual cause | | --- | --- | | `rejected the transaction: Error term encountered` | a handler returned `False`, or the validator has no handler for the scenario's purpose | | `rejected the transaction: UnConstrData: not ConstrData` | a datum or redeemer that does not match the handler's type, such as `Data.encode 5` for a union | | `rejected the transaction: UnBData: not BytesData` | a value that should be bytes is something else, such as `Data.encode 5` given as a `PubKeyHash` parameter | | `accepted a transaction expected to fail` | a check is missing or too loose | | `cannot prepare validator 'markt': no [validators.markt] in elci.toml` | a misspelled validator name | | `cannot prepare validator 'market': it takes 1 parameter [operator], but the scenario gives 0; set them with Scenario.parameters` | `Scenario.parameters` is missing or incomplete | The first message is the common one, and the least helpful: a validator that returns `False` fails with an error, whichever check was false. Traces tell you which one. ## Debug.trace `Debug.trace : String -> a -> a` returns its second argument unchanged and emits the message as a Plutus trace. As in Elm, every module imports `Debug`, so it needs no import. Put a trace in the branch that rejects, so it only fires when that check fails. For the marketplace from [Transaction scenarios](https://elci-lang.dev/testing/scenarios/), move the deadline check of `Buy` in `src/Market.elm` into a function that traces when it fails: ```elm Buy -> sellerPaid listing ctx && endsBy listing.expires ctx ``` ```elm endsBy : Int -> ScriptContext -> Bool endsBy time ctx = if Interval.isEntirelyBefore time ctx.txInfo.validRange then True else Debug.trace "the validity range does not end by the listing's expiry" False ``` Now remove `Scenario.expectFailure` from `buyingAfterExpiryFails` to see what the validator says about that late purchase: ```sh elci test --match buyingAfter ``` ```text FAIL MarketTest.buyingAfterExpiryFails (cpu 33817513, mem 119115) validator market rejected the transaction: Error term encountered trace: the validity range does not end by the listing's expiry 0 passed, 1 failed ``` Traces are printed for failing tests only. Passing tests, including scenarios that are rejected as expected, stay quiet. `Debug.trace` works in test modules too. There, it helps you see how far a computation got. The expected value below is wrong on purpose, so the test fails and prints its trace: ```elm feeOfTwoThousand : Expectation feeOfTwoThousand = let fee = Debug.trace "computing the fee of 2000" (Pricing.fee 2000) in Expect.equal 7 fee ``` ```text FAIL TraceTest.feeOfTwoThousand (cpu 2178706, mem 8698) Expect.equal expected: 7 actual: 6 trace: computing the fee of 2000 ``` :::caution[Remove traces before you deploy] Traces are part of the compiled script. They make it larger and cost budget whenever they run, and `elci build` does not strip them. The trace in `endsBy` adds 61 bytes to the `market` script: `elci check` reports 1672 FLAT bytes, against 1611 with `False` in its place. Remove your traces before you build for deployment, and rerun your tests. ::: ## Execution budgets Every `PASS` and `FAIL` line shows the CPU steps and memory units of a run. What the numbers measure depends on the test: - **`Bool` and `Expectation` tests:** the whole test program, including computing the expected value. - **Scenarios:** the validator alone, running against the scenario's transaction. This is the cost you pay for on-chain. - **Properties:** the costliest passing run, shown as `max cpu`; for a failure, the counterexample's run. Protocol parameters cap the execution units of a transaction (on mainnet, currently 10 billion CPU steps and 14 million memory units), and the units you use add to the transaction fee. So scenario budgets are worth watching as your contract grows. [Costs and performance](https://elci-lang.dev/advanced/costs/) explains where the cost comes from and how to reduce it. ### Guard a budget with withinBudget `Scenario.withinBudget cpu memory` makes a scenario fail when the validator needs more than the given CPU steps or memory units, whether it accepts or rejects the transaction: ```elm delistingStaysCheap : Scenario delistingStaysCheap = settle Delist |> Scenario.signedBy seller |> Scenario.withinBudget 17000000 70000 ``` ```text PASS MarketTest.delistingStaysCheap (cpu 15573596, mem 65977) ``` Set the limits a little above today's numbers. A change that makes the path noticeably more expensive then fails the test instead of slipping through. With limits of `10000000` and `50000`, the same scenario fails: ```text FAIL MarketTest.delistingStaysCheap (cpu 15573596, mem 65977) Budget exceeded: CPU steps 15573596 exceeds maximum 10000000; Memory units 65977 exceeds maximum 50000 (consumed: ExBudget{cpu=15573596, mem=65977}) ``` The check is julc-testkit's `BudgetAssertions.assertBudgetUnder`, the same assertion you can use in [Java tests](https://elci-lang.dev/testing/java/). ## A debugging routine When a scenario fails and the reason is not obvious: 1. Run only that test with `--match`, so the output is short. 2. Read the reason. An `accepted` failure means a check is missing; a decoding error means the data you built does not match the handler's types; `Error term encountered` means a check returned `False`. 3. Add a `Debug.trace` to each branch that can reject, and run the test again. The trace lines show which check failed. 4. If a helper computes the wrong value, move it into a module you can unit-test, and test it directly with `Expect.equal`. 5. Remove the traces and rerun the whole suite before you build for deployment. --- # Build a vesting contract > Build a vesting validator from an empty directory, with a typed datum and redeemer, a deadline checked against the validity range, a signature check, scenarios and a property test. Source: https://elci-lang.dev/tutorials/vesting/ In this tutorial you build a vesting contract from scratch: funds are locked for a beneficiary and can only be claimed after a deadline. You write a typed datum and redeemer, check signatures and the transaction's validity range, cover the rules with transaction scenarios and a property test, and build the script. You need `elci` on your `PATH` (see [Installation](https://elci-lang.dev/start/installation/)). If you haven't yet, work through [Your first validator](https://elci-lang.dev/start/first-validator/) first. The repository's [vesting example](https://github.com/bloxbean/elci-lang/tree/main/examples/vesting) is the finished contract. ## What you'll build Someone locks funds at the vesting script's address with a datum that names a **beneficiary** and a **deadline**. Later, the beneficiary spends the output with the redeemer `Claim`. The validator accepts the claim only when both hold: 1. the transaction is signed by the beneficiary, and 2. the transaction can only run after the deadline. ## Create the project ```sh elci init vesting --validator vesting cd vesting ``` ```text Created Elci project at .../vesting with initial spend handler in validator vesting. Run: elci check .../vesting ``` `--validator vesting` names the validator, and so its output directory, `vesting` instead of `main`. The starter project already passes its tests: ```sh elci test . ``` ```text PASS MainTest.ownerSigns (cpu 7675163, mem 29990) PASS MainTest.othersAreRejected (cpu 8053596, mem 31922) 2 passed, 0 failed ``` You'll replace the starter validator and its tests with your own, so delete them: ```sh rm src/Main.elm tests/MainTest.elm ``` ## Configure the validator Open `elci.toml` and replace the validator table. The vesting contract has no deployment parameters, and its spend handler will be `validate` in a module called `Vesting`: ```toml title="elci.toml" [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" [validators.vesting] boundary = "julc-strict-v1" spend = "Vesting.validate" ``` ## Write the datum, the redeemer and a first handler Start with the types and the signature rule. Create `src/Vesting.elm`: ```elm title="src/Vesting.elm" module Vesting exposing (VestingAction(..), VestingDatum, validate) import Julc.Ledger exposing (PubKeyHash, ScriptContext) import Tx type alias VestingDatum = { beneficiary : PubKeyHash , deadline : Int } type VestingAction = Claim validate : VestingDatum -> VestingAction -> ScriptContext -> Bool validate datum action ctx = case action of Claim -> Tx.signedBy datum.beneficiary ctx ``` - `VestingDatum` is the datum: a record with the beneficiary's key hash and the deadline. The module exposes the type so tests can build datums. - `VestingAction` is the redeemer. It has a single constructor today, but a custom type leaves room for more actions later. `VestingAction(..)` exposes the constructor too. - `validate` has the spend handler shape: datum, redeemer, script context, `Bool`. There are no parameters, so the datum comes first. - `case action of` handles each redeemer. The compiler insists that every constructor is covered. - `Tx.signedBy` asks whether the key is among the transaction's signatories. `Tx` is a core module that answers common questions about the transaction; it needs the import, but no configuration. ```sh elci check . ``` ```text validator vesting (spend): checked; 393 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` ## Test the signature rule Create `tests/VestingTest.elm` with two scenarios: the beneficiary claims, and a stranger tries to: ```elm title="tests/VestingTest.elm" module VestingTest exposing (beneficiaryClaims, strangersCannotClaim) import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Scenario exposing (Scenario) import Vesting exposing (VestingAction(..), VestingDatum) beneficiary : PubKeyHash beneficiary = Scenario.key "beneficiary" datum : VestingDatum datum = { beneficiary = beneficiary, deadline = 1000 } claim : Scenario claim = Scenario.spend "vesting" (Scenario.outRef "vesting" 0) (Data.encode datum) |> Scenario.redeemer (Data.encode Claim) beneficiaryClaims : Scenario beneficiaryClaims = claim |> Scenario.signedBy beneficiary strangersCannotClaim : Scenario strangersCannotClaim = claim |> Scenario.signedBy (Scenario.key "stranger") |> Scenario.expectFailure ``` `Scenario.spend` locks an output at the `vesting` script's address with the encoded datum, then spends it. `Data.encode` turns your Elci values into the on-chain Data the ledger passes to the script. ```sh elci test . ``` ```text PASS VestingTest.beneficiaryClaims (cpu 10873274, mem 42950) PASS VestingTest.strangersCannotClaim (cpu 11251707, mem 44882) 2 passed, 0 failed ``` ## Add the deadline A script can't read the clock. Instead, every transaction declares a **validity range**, the time interval in which it may be included in a block, and the ledger enforces it. If every time in the range is after the deadline, the transaction can't possibly run at or before the deadline. So the rule is: the range must lie entirely after the deadline. Times are POSIX times in milliseconds. The tests use small numbers such as `1000` for readability; a real deadline looks like `1767225600000` (1 January 2026, UTC). 1. **Check the range.** The core `Interval` module reads validity ranges. Update the handler: ```elm title="src/Vesting.elm" ins={3,22-23} module Vesting exposing (VestingAction(..), VestingDatum, validate) import Interval import Julc.Ledger exposing (PubKeyHash, ScriptContext) import Tx type alias VestingDatum = { beneficiary : PubKeyHash , deadline : Int } type VestingAction = Claim validate : VestingDatum -> VestingAction -> ScriptContext -> Bool validate datum action ctx = case action of Claim -> Interval.isEntirelyAfter datum.deadline ctx.txInfo.validRange && Tx.signedBy datum.beneficiary ctx ``` `Interval.isEntirelyAfter datum.deadline range` holds when the range has a start and every time in it is after the deadline. A range without a start could run at any time, so it never passes. `&&` only evaluates its right side when the left side is `True`, so a claim before the deadline doesn't pay for the signature search. 2. **Run the old tests.** ```sh elci test . ``` ```text FAIL VestingTest.beneficiaryClaims (cpu 11922589, mem 46110) validator vesting rejected the transaction: Error term encountered PASS VestingTest.strangersCannotClaim (cpu 11922589, mem 46110) 1 passed, 1 failed ``` `beneficiaryClaims` fails, as it should: its transaction has no validity range, so it could run before the deadline. Notice that `strangersCannotClaim` now passes for a different reason: it fails the deadline check before the signature is ever looked at. Both scenarios cost exactly the same. A test that expects failure needs a nearly identical test that succeeds, or it proves little. ## Test the deadline Rewrite the tests around a helper that claims from a given time. `Scenario.validFrom` sets the start of the validity range (inclusive) and `Scenario.validBefore` its end (exclusive): ```elm title="tests/VestingTest.elm" module VestingTest exposing (..) import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Scenario exposing (Scenario) import Vesting exposing (VestingAction(..), VestingDatum) beneficiary : PubKeyHash beneficiary = Scenario.key "beneficiary" datum : VestingDatum datum = { beneficiary = beneficiary, deadline = 1000 } claimFrom : Int -> Scenario claimFrom time = Scenario.spend "vesting" (Scenario.outRef "vesting" 0) (Data.encode datum) |> Scenario.redeemer (Data.encode Claim) |> Scenario.validFrom time claimsJustAfterTheDeadline : Scenario claimsJustAfterTheDeadline = claimFrom 1001 |> Scenario.signedBy beneficiary claimsLongAfterTheDeadline : Scenario claimsLongAfterTheDeadline = claimFrom 5000 |> Scenario.validBefore 6000 |> Scenario.signedBy beneficiary claimsAtTheDeadlineFail : Scenario claimsAtTheDeadlineFail = claimFrom 1000 |> Scenario.signedBy beneficiary |> Scenario.expectFailure strangersCannotClaim : Scenario strangersCannotClaim = claimFrom 2000 |> Scenario.signedBy (Scenario.key "stranger") |> Scenario.expectFailure claimsWithoutAStartFail : Scenario claimsWithoutAStartFail = Scenario.spend "vesting" (Scenario.outRef "vesting" 0) (Data.encode datum) |> Scenario.redeemer (Data.encode Claim) |> Scenario.signedBy beneficiary |> Scenario.expectFailure ``` The module now uses `exposing (..)`. Test discovery only runs exposed, zero-argument values of the test types (`Scenario` here), so `beneficiary`, `datum` and the function `claimFrom` stay helpers. A zero-argument `Scenario` helper would run as a test, which is why `claimsWithoutAStartFail` builds its own transaction rather than sharing an exposed base scenario. The tests pin down the edges: one millisecond after the deadline, exactly at it, a stranger after it, and no start at all. A transaction valid from `1000` could run at `1000`, which is not after the deadline, so it is rejected. ```sh elci test . ``` ```text PASS VestingTest.claimsJustAfterTheDeadline (cpu 16763911, mem 61637) PASS VestingTest.claimsLongAfterTheDeadline (cpu 16763911, mem 61637) PASS VestingTest.claimsAtTheDeadlineFail (cpu 13950292, mem 52228) PASS VestingTest.strangersCannotClaim (cpu 17142344, mem 63569) PASS VestingTest.claimsWithoutAStartFail (cpu 11922589, mem 46110) 5 passed, 0 failed ``` ## Add a property test Scenarios check the times you thought of. A **property** states a rule for every time, and `elci test` tries it with many generated values. Add the import and the property to `tests/VestingTest.elm`: ```elm title="tests/VestingTest.elm" ins={5} module VestingTest exposing (..) import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Property exposing (Property) import Scenario exposing (Scenario) import Vesting exposing (VestingAction(..), VestingDatum) ``` ```elm title="tests/VestingTest.elm (at the end)" {-| Whenever a signed claim's validity range starts, it succeeds exactly when that is after the deadline. The start is narrowed to the 20 milliseconds around the deadline. -} claimsSucceedOnlyAfterTheDeadline : Int -> Property claimsSucceedOnlyAfterTheDeadline raw = let time = datum.deadline - 10 + modBy 20 raw signed = claimFrom time |> Scenario.signedBy beneficiary in if time > datum.deadline then Property.scenario signed else Property.scenario (Scenario.expectFailure signed) ``` - An exposed function whose result is a `Property` is a [property test](https://elci-lang.dev/testing/properties/). `elci test` generates its `Int` argument, 100 times by default. - Generated integers are mostly small, so the property narrows them with `modBy` to times between 990 and 1009, where the interesting boundary is. - `Property.scenario` runs the transaction against the real script. The `if` states the expected outcome for each time. ```sh elci test . --seed 42 ``` ```text PASS VestingTest.claimsJustAfterTheDeadline (cpu 16763911, mem 61637) PASS VestingTest.claimsLongAfterTheDeadline (cpu 16763911, mem 61637) PASS VestingTest.claimsAtTheDeadlineFail (cpu 13950292, mem 52228) PASS VestingTest.strangersCannotClaim (cpu 17142344, mem 63569) PASS VestingTest.claimsWithoutAStartFail (cpu 11922589, mem 46110) PASS VestingTest.claimsSucceedOnlyAfterTheDeadline (100 runs, max cpu 16763911, mem 61637) 6 passed, 0 failed (seed 42) ``` The summary reports the seed of the run. Without `--seed`, every run picks a random one; passing a reported seed reproduces that run exactly. ### See it catch a bug To see what the property is for, break the contract on purpose. Change `Interval.isEntirelyAfter datum.deadline` to `Interval.isEntirelyAfter (datum.deadline - 1)` in `src/Vesting.elm`, so a claim valid from the deadline itself is accepted, and run only the property: ```sh elci test . --match OnlyAfter --seed 4 ``` ```text FAIL VestingTest.claimsSucceedOnlyAfterTheDeadline (38 runs, cpu 16929119, mem 62039) counterexample after 38 runs, shrunk 1 time: claimsSucceedOnlyAfterTheDeadline 10 validator vesting accepted a transaction expected to fail 0 passed, 1 failed (seed 4) ``` The counterexample is the generated argument, `raw = 10`, which gives `time = 990 + modBy 20 10 = 1000`: exactly the deadline. The property found the off-by-one without you writing that case. Change the check back before you continue. (`claimsAtTheDeadlineFail` catches this bug too; the property would also catch the ones you didn't think to write a scenario for.) ## Build the script ```sh elci build . ``` ```text validator vesting (spend): checked; 533 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` Your project now looks like this: - vesting/ - elci.toml - README.md - .gitignore - src/ - Vesting.elm - tests/ - VestingTest.elm - build/ - validators/ - elci-outputs.json - vesting/ - abi.json - validator.flat - validator.plutus - validator.plutus.json The validator has no parameters, so its artifact already carries the script hash, from which the script address is derived (`cborHex` shortened): ```json title="build/validators/vesting/validator.plutus.json" { "type": "PlutusScriptV3", "purpose": "spending", "description": "vesting", "cborHex": "590218590215010100323232323232323232322323232323...", "hash": "cdf09c382832689b016adac7ef1470488e9ff58b8a474b9086c7a0c3", "params": "", "sizeBytes": 533 } ``` `abi.json` records how the datum and redeemer are encoded. `VestingDatum` is constructor 0 with `beneficiary` then `deadline` (fields in name order), and `Claim` is constructor 0 with no fields. Off-chain code that locks funds must build the datum in exactly that layout, or the output can never be claimed. [Build and deploy](https://elci-lang.dev/contracts/build-and-deploy/) explains the files and shows how to load the script from Java. ## Recap - The datum and the redeemer are ordinary Elci types. The compiler decodes them strictly before `validate` runs. - Time checks reason about the transaction's validity range, never a clock. "After the deadline" means every time in the range is after it: `Interval.isEntirelyAfter`. - The core `Tx` and `Interval` modules answer the common questions about a transaction without any configuration. - Scenarios pin down the edge cases. A failing scenario needs a passing twin, or it may fail for the wrong reason. - A property checks a rule over many generated values, and shrinks a failure to a small counterexample with a seed that reproduces it. Next, build two cooperating scripts with parameters in [A treasury and its badge policy](https://elci-lang.dev/tutorials/treasury/). --- # A treasury and its badge policy > Build two cooperating scripts from an empty directory, a treasury that spends payouts and withdraws staking rewards and a badge minting policy, both parameterized by an admin key. Source: https://elci-lang.dev/tutorials/treasury/ In this tutorial you build a project with two validators. The first, a treasury, is one script with two purposes: it releases payouts and withdraws its own staking rewards. The second is a separate minting policy for badges. Both take an `admin` key as a deployment parameter. Along the way you share code between scripts, test every purpose, read the minted quantity from the transaction, and apply the parameter for deployment. You should be comfortable with the [vesting tutorial](https://elci-lang.dev/tutorials/vesting/). The finished project is close to the [treasury example](https://github.com/bloxbean/elci-lang/tree/main/examples/treasury) in the repository; the badge policy here also checks the minted tokens. ## What you'll build - **`treasury`**, one script with two handlers: - `spend`: each locked output holds a `Payout` datum naming a beneficiary and an amount. The redeemer `Release` lets the beneficiary take a payout with a positive amount; `Cancel` lets the admin take it back. - `withdraw`: only the admin may withdraw the staking rewards earned by the treasury's stake credential. - **`badge`**, a separate minting policy: only the admin may mint `BADGE` tokens, and only a positive number of them. Both scripts are parameterized by the same `admin` key hash, but they are independent scripts with different hashes. ## Create the project ```sh elci init treasury --validator treasury cd treasury rm src/Main.elm tests/MainTest.elm ``` ```text Created Elci project at .../treasury with initial spend handler in validator treasury. Run: elci check .../treasury ``` ## Share the signature check Both scripts check signatures, so put the check in its own module. Create `src/Auth.elm`: ```elm title="src/Auth.elm" module Auth exposing (signedBy) import Julc.Ledger exposing (PubKeyHash, ScriptContext) signedBy : PubKeyHash -> ScriptContext -> Bool signedBy key ctx = List.any (\signer -> signer == key) ctx.txInfo.signatories ``` A module is plain source. Each script includes only the functions its handlers reach, so sharing a module costs nothing. The core `Tx` module has the same check as `Tx.signedBy`; this project keeps its own to show a shared module. ## Write the spend handler 1. **Configure the treasury.** Replace the validator table in `elci.toml`. The table declares the `admin` parameter and the spend handler: ```toml title="elci.toml" [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" [validators.treasury] boundary = "julc-strict-v1" parameters = ["admin"] spend = "Treasury.spend" ``` 2. **Write the handler.** Create `src/Treasury.elm`: ```elm title="src/Treasury.elm" module Treasury exposing (Action(..), Payout, spend) import Auth exposing (signedBy) import Julc.Ledger exposing (PubKeyHash, ScriptContext) type alias Payout = { beneficiary : PubKeyHash , amount : Int } type Action = Release | Cancel spend : PubKeyHash -> Payout -> Action -> ScriptContext -> Bool spend admin payout action ctx = case action of Release -> payout.amount > 0 && signedBy payout.beneficiary ctx Cancel -> signedBy admin ctx ``` The parameter comes first, then the datum (`Payout`), the redeemer (`Action`) and the context. 3. **Check it.** ```sh elci check . ``` ```text validator treasury (spend): checked; 458 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` 4. **Test it.** Create `tests/TreasuryTest.elm`. The helper `claim` builds a spend of a payout with a given action. Parameters are passed as `Data` in the declared order: ```elm title="tests/TreasuryTest.elm" module TreasuryTest exposing (adminCancels, adminCannotRelease, beneficiaryReleases) import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Scenario exposing (Scenario) import Treasury exposing (Action(..), Payout) admin : PubKeyHash admin = Scenario.key "admin" beneficiary : PubKeyHash beneficiary = Scenario.key "beneficiary" payout : Payout payout = { beneficiary = beneficiary, amount = 50 } claim : Action -> Scenario claim action = Scenario.spend "treasury" (Scenario.outRef "treasury" 0) (Data.encode payout) |> Scenario.parameters [ Data.encode admin ] |> Scenario.redeemer (Data.encode action) beneficiaryReleases : Scenario beneficiaryReleases = claim Release |> Scenario.signedBy beneficiary adminCannotRelease : Scenario adminCannotRelease = claim Release |> Scenario.signedBy admin |> Scenario.expectFailure adminCancels : Scenario adminCancels = claim Cancel |> Scenario.signedBy admin ``` ```sh elci test . ``` ```text PASS TreasuryTest.beneficiaryReleases (cpu 11698810, mem 46679) PASS TreasuryTest.adminCannotRelease (cpu 12077243, mem 48611) PASS TreasuryTest.adminCancels (cpu 11310693, mem 45520) 3 passed, 0 failed ``` ## Add the withdraw handler A script's hash can also serve as a stake credential. If the treasury's address uses the script's hash for its staking part too, the staking rewards of the funds it holds accrue to that credential, and withdrawing them runs the script with the `withdraw` purpose. Adding a handler to the same table keeps it **one script**, so the address that holds the payouts and the credential that earns the rewards share one hash. 1. **Add the handler key.** ```toml title="elci.toml" ins={6,11} [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" # One script: the treasury address spends payouts and withdraws its staking rewards. [validators.treasury] boundary = "julc-strict-v1" parameters = ["admin"] spend = "Treasury.spend" withdraw = "Treasury.withdraw" ``` 2. **Write the handler.** Expose `withdraw` from `src/Treasury.elm` and add it at the end: ```elm title="src/Treasury.elm" module Treasury exposing (Action(..), Payout, spend, withdraw) ``` ```elm title="src/Treasury.elm (at the end)" withdraw : PubKeyHash -> () -> ScriptContext -> Bool withdraw admin _ ctx = signedBy admin ctx ``` A withdraw handler has no datum: parameters, redeemer, context. It receives the same `admin` parameter as `spend`; every handler of a validator receives all of its parameters. 3. **Check it.** The check line now lists both purposes: ```sh elci check . ``` ```text validator treasury (spend, withdraw): checked; 540 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` 4. **Test it.** Add two scenarios to `tests/TreasuryTest.elm`, and to its `exposing` list. `Scenario.withdraw "treasury"` withdraws rewards from the script's own stake credential: ```elm title="tests/TreasuryTest.elm" module TreasuryTest exposing (adminCancels, adminCannotRelease, adminWithdrawsRewards, beneficiaryReleases, rewardsNeedTheAdmin) ``` ```elm title="tests/TreasuryTest.elm (at the end)" adminWithdrawsRewards : Scenario adminWithdrawsRewards = Scenario.withdraw "treasury" |> Scenario.parameters [ Data.encode admin ] |> Scenario.signedBy admin rewardsNeedTheAdmin : Scenario rewardsNeedTheAdmin = Scenario.withdraw "treasury" |> Scenario.parameters [ Data.encode admin ] |> Scenario.signedBy beneficiary |> Scenario.expectFailure ``` ```sh elci test . ``` ```text PASS TreasuryTest.beneficiaryReleases (cpu 11746810, mem 46979) PASS TreasuryTest.adminCannotRelease (cpu 12125243, mem 48911) PASS TreasuryTest.adminCancels (cpu 11358693, mem 45820) PASS TreasuryTest.adminWithdrawsRewards (cpu 6103274, mem 24333) PASS TreasuryTest.rewardsNeedTheAdmin (cpu 6481707, mem 26265) 5 passed, 0 failed ``` The spend scenarios cost a little more than before: the script now chooses between two purposes before it runs a handler. ## Add the badge policy The badge policy is a separate script, so it gets its own table. It reads the minted quantity from the transaction's `mint` field. That field is a multi-asset `Value`, which the core [`Value`](https://elci-lang.dev/reference/core-modules/#value) module reads. :::caution[Don't trust the redeemer for amounts] The redeemer is whatever the transaction's builder puts there. A policy that only checked a quantity passed in the redeemer would approve a transaction whose redeemer says `3` while it mints a thousand tokens. Read what actually happens from `ctx.txInfo`. ::: 1. **Configure it.** Add the `badge` table: ```toml title="elci.toml" ins={13-17} [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" # One script: the treasury address spends payouts and withdraws its staking rewards. [validators.treasury] boundary = "julc-strict-v1" parameters = ["admin"] spend = "Treasury.spend" withdraw = "Treasury.withdraw" # A separate script, and so a separate policy id. [validators.badge] boundary = "julc-strict-v1" parameters = ["admin"] mint = "Badge.mint" ``` 2. **Write the policy.** Create `src/Badge.elm`: ```elm title="src/Badge.elm" module Badge exposing (mint) import Auth import Bytes import Julc.Ledger exposing (PubKeyHash, ScriptContext, ScriptInfo(..), TokenName(..)) import Value {-| The admin may mint badges. The quantity is read from the transaction's mint field, never from the redeemer, which the transaction builder chooses freely. -} mint : PubKeyHash -> () -> ScriptContext -> Bool mint admin _ ctx = case ctx.scriptInfo of MintingScript policy -> (Value.quantityOf policy (TokenName (Bytes.fromString "BADGE")) ctx.txInfo.mint > 0) && Auth.signedBy admin ctx _ -> False ``` - `ctx.scriptInfo` is `MintingScript policy` when the script runs as a minting policy. `policy` is the script's own policy id. - `Value.quantityOf` returns how many tokens of that policy and name the transaction mints (negative for a burn, zero if none). `policy` is already a `PolicyId`, and `TokenName (Bytes.fromString "BADGE")` builds the token name from its bytes. - The `_` branch never runs for a script with only a mint handler, but `case` must cover every constructor. 3. **Check both validators.** ```sh elci check . ``` ```text validator badge (mint): checked; 341 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 validator treasury (spend, withdraw): checked; 540 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` 4. **Test it.** Create `tests/BadgeTest.elm`, with a property that covers every quantity: ```elm title="tests/BadgeTest.elm" module BadgeTest exposing (adminMintsBadges, badgesNeedAPositiveQuantity, strangersCannotMint) import Bytes import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Property exposing (Property) import Scenario exposing (Scenario) admin : PubKeyHash admin = Scenario.key "admin" badges : Int -> Scenario badges quantity = Scenario.mint "badge" |> Scenario.parameters [ Data.encode admin ] |> Scenario.minting (Bytes.fromString "BADGE") quantity adminMintsBadges : Scenario adminMintsBadges = badges 3 |> Scenario.signedBy admin strangersCannotMint : Scenario strangersCannotMint = badges 3 |> Scenario.signedBy (Scenario.key "stranger") |> Scenario.expectFailure badgesNeedAPositiveQuantity : Int -> Property badgesNeedAPositiveQuantity quantity = let signed = badges quantity |> Scenario.signedBy admin in if quantity > 0 then Property.scenario signed else Property.scenario (Scenario.expectFailure signed) ``` `Scenario.minting` mints (or, with a negative quantity, burns) tokens under the validator's own policy. The property runs with generated quantities: positive ones must succeed, and zero and negative ones must be rejected. ## Add a property and a budget to the treasury Two more checks for the treasury tests. Add `import Property exposing (Property)`, expose `onlyTheBeneficiaryReleases`, give `beneficiaryReleases` a budget, and add the property at the end: ```elm title="tests/TreasuryTest.elm" ins={1,5} module TreasuryTest exposing (adminCancels, adminCannotRelease, adminWithdrawsRewards, beneficiaryReleases, onlyTheBeneficiaryReleases, rewardsNeedTheAdmin) import Julc.Data as Data import Julc.Ledger exposing (PubKeyHash) import Property exposing (Property) import Scenario exposing (Scenario) import Treasury exposing (Action(..), Payout) ``` ```elm title="tests/TreasuryTest.elm" ins={5} beneficiaryReleases : Scenario beneficiaryReleases = claim Release |> Scenario.signedBy beneficiary |> Scenario.withinBudget 20000000 100000 ``` ```elm title="tests/TreasuryTest.elm (at the end)" onlyTheBeneficiaryReleases : PubKeyHash -> Property onlyTheBeneficiaryReleases signer = let release = claim Release |> Scenario.signedBy signer in if signer == beneficiary then Property.scenario release else Property.scenario (Scenario.expectFailure release) ``` - `Scenario.withinBudget cpu memory` fails the scenario if the script uses more than that budget, so a change that makes releases much more expensive can't slip through. - The property generates 28-byte key hashes and checks that a release signed by anyone else is rejected. Random keys essentially never equal the beneficiary, so in practice it checks "everyone else is rejected"; `beneficiaryReleases` covers the beneficiary. Run everything: ```sh elci test . --seed 42 ``` ```text PASS BadgeTest.adminMintsBadges (cpu 10442084, mem 36669) PASS BadgeTest.strangersCannotMint (cpu 10820517, mem 38601) PASS BadgeTest.badgesNeedAPositiveQuantity (100 runs, max cpu 10442084, mem 36669) PASS TreasuryTest.beneficiaryReleases (cpu 11746810, mem 46979) PASS TreasuryTest.adminCannotRelease (cpu 12125243, mem 48911) PASS TreasuryTest.adminCancels (cpu 11358693, mem 45820) PASS TreasuryTest.adminWithdrawsRewards (cpu 6103274, mem 24333) PASS TreasuryTest.rewardsNeedTheAdmin (cpu 6481707, mem 26265) PASS TreasuryTest.onlyTheBeneficiaryReleases (100 runs, max cpu 12125243, mem 48911) 9 passed, 0 failed (seed 42) ``` A mint costs about 10 million CPU units, against about 6 million for a withdrawal, which only checks a signature. Most of the difference is `Value.quantityOf` reading the mint value. That's affordable, but the budget is worth watching as a policy grows. ## Build both scripts ```sh elci build . ``` ```text validator badge (mint): checked; 341 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 validator treasury (spend, withdraw): checked; 540 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` - treasury/ - elci.toml - src/ - Auth.elm - Badge.elm - Treasury.elm - tests/ - BadgeTest.elm - TreasuryTest.elm - build/ - validators/ - elci-outputs.json - badge/ - abi.json - validator.flat - validator.plutus - validator.plutus.json - treasury/ - abi.json - validator.flat - validator.plutus - validator.plutus.json To rebuild only one script, select it: `elci build . --validator badge`. The treasury's artifact records its two purposes as `multi` and its pending parameter (`cborHex` shortened): ```json title="build/validators/treasury/validator.plutus.json" { "type": "PlutusScriptV3", "purpose": "multi", "description": "treasury", "cborHex": "...", "hash": "", "params": "admin:PubKeyHash", "sizeBytes": 540 } ``` ## Apply the admin key Both scripts need the admin's key hash before they can be deployed. Keep it in a file, as Plutus Data JSON: ```json title="params/admin.json" {"bytes": "887d8fbb82ce756a9dac9210c3d1235e9d3ff2457a667d311d318142"} ``` Apply it to each script. `@params/admin.json` is read relative to the project directory: ```sh elci apply . --validator treasury --param admin=@params/admin.json elci apply . --validator badge --param admin=@params/admin.json ``` ```text validator treasury: applied 1 parameter; hash 16a075885ca1a8102d56e474815e720bf5d7c342f4bd2e225a87441e; wrote .../treasury/build/validators/treasury/applied.plutus.json validator badge: applied 1 parameter; hash 94e8ac9685e836de2886ca6411c97a1328aea45ac669f3fd05ae4949; wrote .../treasury/build/validators/badge/applied.plutus.json ``` The same admin gives two different hashes, because the scripts are different: - The **treasury** hash is the payment credential of the treasury's address, and the stake credential whose rewards `withdraw` guards. - The **badge** hash is the badge policy id. `elci apply` checked that the value is a well-formed `PubKeyHash` before applying it. Replace the example key with the real admin's key hash, and note that the next `elci build` removes both `applied.plutus.json` files; use `--output` to keep them elsewhere. See [Parameterized validators](https://elci-lang.dev/contracts/parameters/) and [Build and deploy](https://elci-lang.dev/contracts/build-and-deploy/). ## Recap and next steps - One `[validators.]` table is one script. Several handler keys make one script serve several purposes with one hash. - Separate tables make independent scripts. They can share modules and parameters but have their own hashes. - Every handler receives all of its validator's parameters, first and in the declared order. - Read amounts from the transaction, not from the redeemer. The treasury trusts the beneficiary to take the payout: `Release` checks the signature and that the amount is positive, not where the funds go. As an exercise, require that the transaction pays `payout.amount` lovelace to the beneficiary, using the output checks in [Reading the transaction](https://elci-lang.dev/contracts/reading-the-transaction/), and add scenarios with `Scenario.output`. Next, write a minting policy that can only ever run once in [A one-shot minting policy](https://elci-lang.dev/tutorials/one-shot-policy/). --- # A one-shot minting policy > Build a minting policy that can mint only once, by tying it to a seed output that the minting transaction must spend, and prove the accept and reject cases with scenarios and properties. Source: https://elci-lang.dev/tutorials/one-shot-policy/ In this tutorial you build a **one-shot** minting policy: a policy under which one token can be minted once, and never again. It is the standard way to make a true NFT. You take a ledger type as a deployment parameter, check the transaction's inputs, inspect what it mints, and prove every rule with scenarios and properties. You should have finished the [vesting tutorial](https://elci-lang.dev/tutorials/vesting/). [Reading the transaction](https://elci-lang.dev/contracts/reading-the-transaction/) explains the ledger types used here. ## How a one-shot policy works A minting policy's id is its script hash, and a parameter is part of the script. So if the policy takes an output reference, a `TxOutRef`, as a parameter, every choice of that **seed** output gives a different policy id. Now require that minting spends the seed. An output can be spent only once, so only one transaction can ever satisfy the policy. After it, no transaction can mint under this policy id again. The policy you'll build has two parameters, `seed : TxOutRef` and `tokenName : TokenName`, and these rules: 1. Minting succeeds only in a transaction that spends the seed output. 2. That transaction must mint exactly one `tokenName` token under this policy, and nothing else. 3. Burning that one token is always allowed. ## Create the project ```sh elci init one-shot --validator nft --purpose mint cd one-shot rm src/Main.elm tests/MainTest.elm ``` ```text Created Elci project at .../one-shot with initial mint handler in validator nft. Run: elci check .../one-shot ``` `--purpose mint` starts from a minting-policy template, which you've just removed; `--validator nft` names the validator. ## Require the seed 1. **Configure the policy** with a single parameter for now: ```toml title="elci.toml" [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" [validators.nft] boundary = "julc-strict-v1" parameters = ["seed"] mint = "OneShot.mint" ``` 2. **Write the check.** Create `src/OneShot.elm`: ```elm title="src/OneShot.elm" module OneShot exposing (mint) import Julc.Ledger exposing (ScriptContext, TxOutRef) {-| Minting is allowed only in the transaction that spends the seed output. -} mint : TxOutRef -> () -> ScriptContext -> Bool mint seed _ ctx = List.any (\input -> input.outRef == seed) ctx.txInfo.inputs ``` A mint handler has the shape parameters, redeemer, context. `TxOutRef` is a ledger type that can cross the Data boundary, so it can be a parameter; `==` compares two references. Each entry of `ctx.txInfo.inputs` is a `TxInInfo`, whose `outRef` says which output it spends. The core `Tx` module has this check as `Tx.spends seed ctx`; this tutorial writes it out to show what it does. ```sh elci check . ``` ```text validator nft (mint): checked; 208 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` 3. **Test both sides.** A minting scenario doesn't spend anything by default, so add the seed as an input with `Scenario.input`. An input needs an output to spend: here, 5 ADA at a wallet address built from a test key. Create `tests/OneShotTest.elm`: ```elm title="tests/OneShotTest.elm" module OneShotTest exposing (mintingWithTheSeedSucceeds, mintingWithoutTheSeedFails) import Bytes import Julc.Data as Data import Julc.Ledger as Ledger exposing (Address, TxOutRef) import Scenario exposing (Scenario) seed : TxOutRef seed = Scenario.outRef "seed" 0 wallet : Address wallet = Ledger.Address (Ledger.PubKeyCredential (Scenario.key "minter")) (Data.toOptional Nothing) mintOne : Scenario mintOne = Scenario.mint "nft" |> Scenario.parameters [ Data.encode seed ] |> Scenario.minting (Bytes.fromString "TICKET") 1 mintingWithTheSeedSucceeds : Scenario mintingWithTheSeedSucceeds = mintOne |> Scenario.input seed (Scenario.toAddress wallet 5000000) mintingWithoutTheSeedFails : Scenario mintingWithoutTheSeedFails = mintOne |> Scenario.input (Scenario.outRef "other" 0) (Scenario.toAddress wallet 5000000) |> Scenario.expectFailure ``` - `Scenario.outRef "seed" 0` derives a deterministic output reference from a name and an index. - `Ledger.Address` builds an address from a payment credential and an optional staking credential. `Data.toOptional Nothing` gives the empty `JulcOptional` the ledger type expects. - `mintOne` is a helper: it isn't exposed, so it doesn't run as a test. ```sh elci test . ``` ```text PASS OneShotTest.mintingWithTheSeedSucceeds (cpu 6395919, mem 20708) PASS OneShotTest.mintingWithoutTheSeedFails (cpu 6774352, mem 22640) 2 passed, 0 failed ``` The core idea works. But the seed transaction may still mint as many tokens as it likes, under any names. An NFT needs more. ## Mint exactly one token The policy must look at what the transaction mints under its own policy id, and accept exactly one token with the expected name. The mint field is a multi-asset `Value`, which the core [`Value`](https://elci-lang.dev/reference/core-modules/#value) module reads. `Value.tokens policy value` gives the tokens of one policy with their quantities, as a `Dict TokenName Int`. Dictionaries have `==`, so a single comparison can check everything the policy mints. 1. **Add the token name parameter.** ```toml title="elci.toml" ins={8} [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" [validators.nft] boundary = "julc-strict-v1" parameters = ["seed", "tokenName"] mint = "OneShot.mint" ``` 2. **Check what is minted.** Replace `src/OneShot.elm`: ```elm title="src/OneShot.elm" module OneShot exposing (mint) import Dict exposing (Dict) import Julc.Ledger exposing (PolicyId, ScriptContext, ScriptInfo(..), TokenName, TxOutRef) import Value {-| Mint exactly one `tokenName` token, in the transaction that spends the seed output. -} mint : TxOutRef -> TokenName -> () -> ScriptContext -> Bool mint seed tokenName _ ctx = case ctx.scriptInfo of MintingScript policy -> spendsSeed seed ctx && minted policy ctx == Dict.singleton tokenName 1 _ -> False spendsSeed : TxOutRef -> ScriptContext -> Bool spendsSeed seed ctx = List.any (\input -> input.outRef == seed) ctx.txInfo.inputs {-| The token names and quantities this transaction mints under `policy`. -} minted : PolicyId -> ScriptContext -> Dict TokenName Int minted policy ctx = Value.tokens policy ctx.txInfo.mint ``` - `ctx.scriptInfo` is `MintingScript policy` when the script runs as a minting policy, and `policy` is the script's own policy id. Other purposes can't reach this handler, but `case` must be exhaustive. - `minted` keeps only this policy's tokens. A value never holds a zero quantity, so a token the transaction doesn't mint is simply absent. - Comparing the whole dictionary with `Dict.singleton tokenName 1` rejects a second name, a quantity other than one, and a transaction that mints nothing under this policy, all in one test. - `TokenName` is the ledger's type for token names. Its Data is plain bytes, so the parameter is applied as bytes. ```sh elci check . ``` ```text validator nft (mint): checked; 409 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` The script grew from 208 to 409 bytes: it now reads the mint value and compares dictionaries. 3. **Test every way to get it wrong.** Replace `tests/OneShotTest.elm`. `mintSpending` builds a transaction that spends a given output; each test adds what it mints: ```elm title="tests/OneShotTest.elm" module OneShotTest exposing (..) import Bytes import Julc.Data as Data import Julc.Ledger as Ledger exposing (Address, TxOutRef) import Property exposing (Property) import Scenario exposing (Scenario) seed : TxOutRef seed = Scenario.outRef "seed" 0 ticket : Bytes ticket = Bytes.fromString "TICKET" wallet : Address wallet = Ledger.Address (Ledger.PubKeyCredential (Scenario.key "minter")) (Data.toOptional Nothing) {-| A transaction that spends `spent` and mints under the policy. -} mintSpending : TxOutRef -> Scenario mintSpending spent = Scenario.mint "nft" |> Scenario.parameters [ Data.encode seed, Data.encode ticket ] |> Scenario.input spent (Scenario.toAddress wallet 5000000) mintingWithTheSeedSucceeds : Scenario mintingWithTheSeedSucceeds = mintSpending seed |> Scenario.minting ticket 1 mintingWithoutTheSeedFails : Scenario mintingWithoutTheSeedFails = mintSpending (Scenario.outRef "other" 0) |> Scenario.minting ticket 1 |> Scenario.expectFailure mintingTwoFails : Scenario mintingTwoFails = mintSpending seed |> Scenario.minting ticket 2 |> Scenario.expectFailure mintingAnotherNameFails : Scenario mintingAnotherNameFails = mintSpending seed |> Scenario.minting (Bytes.fromString "OTHER") 1 |> Scenario.expectFailure mintingExtraTokensFails : Scenario mintingExtraTokensFails = mintSpending seed |> Scenario.minting ticket 1 |> Scenario.minting (Bytes.fromString "BONUS") 1 |> Scenario.expectFailure {-| Whatever output a transaction spends, it can mint only when that output is the seed. -} onlyTheSeedUnlocksMinting : TxOutRef -> Property onlyTheSeedUnlocksMinting spent = let transaction = mintSpending spent |> Scenario.minting ticket 1 in if spent == seed then Property.scenario transaction else Property.scenario (Scenario.expectFailure transaction) {-| Spending the seed mints exactly one ticket, never more, fewer or a burn. -} exactlyOneTicket : Int -> Property exactlyOneTicket quantity = let transaction = mintSpending seed |> Scenario.minting ticket quantity in if quantity == 1 then Property.scenario transaction else Property.scenario (Scenario.expectFailure transaction) ``` The two properties generalize the scenarios: - `onlyTheSeedUnlocksMinting` generates output references. Random references essentially never equal the seed, so it checks that every other output is rejected; `mintingWithTheSeedSucceeds` covers the seed itself. - `exactlyOneTicket` generates quantities, including zero and negative ones. Only `1` may succeed. ```sh elci test . --seed 42 ``` ```text PASS OneShotTest.mintingWithTheSeedSucceeds (cpu 13722157, mem 43763) PASS OneShotTest.mintingWithoutTheSeedFails (cpu 9127629, mem 31957) PASS OneShotTest.mintingTwoFails (cpu 13706157, mem 43663) PASS OneShotTest.mintingAnotherNameFails (cpu 13706157, mem 43663) PASS OneShotTest.mintingExtraTokensFails (cpu 13706157, mem 43663) PASS OneShotTest.onlyTheSeedUnlocksMinting (100 runs, max cpu 9127629, mem 31957) PASS OneShotTest.exactlyOneTicket (100 runs, max cpu 13722157, mem 43763) 7 passed, 0 failed (seed 42) ``` The budgets tell a story. Without the seed, `&&` stops after `spendsSeed`, at about 9M CPU units. With it, reading the mint value and comparing the dictionaries costs about 5M more. That is well within a transaction's limits, and it only runs once. ## Allow burning As written, the token can never be burned: a burn is a negative quantity, and it can't spend the long-gone seed. Give the redeemer a meaning: `Mint` or `Burn`. 1. **Add the action type.** Replace `src/OneShot.elm`: ```elm title="src/OneShot.elm" module OneShot exposing (Action(..), mint) import Dict exposing (Dict) import Julc.Ledger exposing (PolicyId, ScriptContext, ScriptInfo(..), TokenName, TxOutRef) import Value type Action = Mint | Burn {-| Mint exactly one `tokenName` token, in the transaction that spends the seed output. Burning that token is always allowed. -} mint : TxOutRef -> TokenName -> Action -> ScriptContext -> Bool mint seed tokenName action ctx = case ctx.scriptInfo of MintingScript policy -> case action of Mint -> spendsSeed seed ctx && minted policy ctx == Dict.singleton tokenName 1 Burn -> minted policy ctx == Dict.singleton tokenName -1 _ -> False spendsSeed : TxOutRef -> ScriptContext -> Bool spendsSeed seed ctx = List.any (\input -> input.outRef == seed) ctx.txInfo.inputs {-| The token names and quantities this transaction mints under `policy`. -} minted : PolicyId -> ScriptContext -> Dict TokenName Int minted policy ctx = Value.tokens policy ctx.txInfo.mint ``` `Burn` doesn't need the seed. It accepts only a burn of exactly one `tokenName`, which is all there can be. 2. **Update the tests.** Import the action, send `Mint` as the redeemer in `mintSpending`, and add a burn scenario at the end: ```elm title="tests/OneShotTest.elm" ins={6} module OneShotTest exposing (..) import Bytes import Julc.Data as Data import Julc.Ledger as Ledger exposing (Address, TxOutRef) import OneShot exposing (Action(..)) import Property exposing (Property) import Scenario exposing (Scenario) ``` ```elm title="tests/OneShotTest.elm" ins={6} {-| A transaction that spends `spent` and mints under the policy. -} mintSpending : TxOutRef -> Scenario mintSpending spent = Scenario.mint "nft" |> Scenario.redeemer (Data.encode Mint) |> Scenario.parameters [ Data.encode seed, Data.encode ticket ] |> Scenario.input spent (Scenario.toAddress wallet 5000000) ``` ```elm title="tests/OneShotTest.elm (at the end)" burningIsAlwaysAllowed : Scenario burningIsAlwaysAllowed = Scenario.mint "nft" |> Scenario.parameters [ Data.encode seed, Data.encode ticket ] |> Scenario.redeemer (Data.encode Burn) |> Scenario.minting ticket -1 ``` As in Elm, `-1` directly after a space is a negative argument, so `Scenario.minting ticket -1` burns one token. ```sh elci test . --seed 42 ``` ```text PASS OneShotTest.mintingWithTheSeedSucceeds (cpu 14212973, mem 45528) PASS OneShotTest.mintingWithoutTheSeedFails (cpu 9618445, mem 33722) PASS OneShotTest.mintingTwoFails (cpu 14196973, mem 45428) PASS OneShotTest.mintingAnotherNameFails (cpu 14196973, mem 45428) PASS OneShotTest.mintingExtraTokensFails (cpu 14196973, mem 45428) PASS OneShotTest.onlyTheSeedUnlocksMinting (100 runs, max cpu 9618445, mem 33722) PASS OneShotTest.exactlyOneTicket (100 runs, max cpu 14212973, mem 45528) PASS OneShotTest.burningIsAlwaysAllowed (cpu 11424675, mem 39941) 8 passed, 0 failed (seed 42) ``` `exactlyOneTicket` now also proves that the `Mint` action can't be used to burn: a quantity of `-1` with `Mint` is still rejected. ## Build and apply ```sh elci build . ``` ```text validator nft (mint): checked; 477 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` To deploy, pick the seed: an unspent output in the minter's wallet, identified by its transaction id and output index. Apply it with the token name. A `TxOutRef` is constructor 0 with the transaction id's bytes and the index; the token name is the hex of `TICKET`: ```sh elci apply . --validator nft \ --param 'seed={"constructor": 0, "fields": [{"bytes": "a8a61c7bc19b634cd1ad661a305682b387363841f79194bee1df3b12d7054f8e"}, {"int": 1}]}' \ --param 'tokenName={"bytes": "5449434b4554"}' ``` ```text validator nft: applied 2 parameters; hash 4481b6b376b6479a565cde7c29f6ab8e12019a6091d2140afc94d5b5; wrote .../one-shot/build/validators/nft/applied.plutus.json ``` The hash is the policy id of your NFT. The minting transaction must spend exactly the output you applied (`a8a61c...#1` here), mint one `TICKET` under this policy id with the `Mint` redeemer, and attach `applied.plutus.json`'s script. Once that output is spent, the policy can never mint again. :::caution Apply the seed only after you've chosen an output that really exists and is unspent. If the seed is spent by any other transaction first, nothing can ever be minted under this policy id. ::: ## Recap - A ledger type such as `TxOutRef` can be a deployment parameter, so a script can be tied to one specific output. - `ctx.txInfo.inputs` tells you which outputs a transaction spends; `ctx.scriptInfo` gives a minting policy its own policy id. The core `Tx` module packages these checks as `Tx.spends`, `Tx.ownPolicy` and `Tx.mintedOf`. - `Value.tokens` gives the tokens a transaction mints under one policy as a `Dict`, which `==` compares exactly. - Properties over generated `TxOutRef`s and quantities check the rules far beyond the cases you write by hand. What this tutorial doesn't cover: token metadata (CIP-25 or CIP-68) is off-chain or needs more on-chain rules, and building and submitting the minting transaction happens in off-chain code, for example with Cardano Client Lib as shown in [Build and deploy](https://elci-lang.dev/contracts/build-and-deploy/). --- # Java libraries > Import Julc's on-chain Java libraries and your own library JARs into Elci, and learn how Java types, generic methods and imported records map to Elci. Source: https://elci-lang.dev/advanced/java-libraries/ Elci can call on-chain libraries written in Java for Julc. They complement Elci's [core library](https://elci-lang.dev/reference/core-modules/): reach for them when the core has no equivalent, such as floor division, validity intervals or signature checks. This page shows how to map a Java library to an Elm-style module, how to package your own library as a JAR, how Java types appear in Elci, and which signatures are rejected and why. ## How Java imports work A Julc on-chain library is a Java class annotated with `@OnchainLibrary` whose public static methods use a restricted, on-chain subset of Java. Elci never runs that Java on the JVM. Julc parses the library's **source code**, compiles each method you use to typed PIR (Plutus IR) and hands it to Elci, which type-checks your calls and links the compiled code into your script. Two consequences follow: - A library must be available as **source**. Compiled `.class` files alone are not enough. - A library call is compiled code, not a builtin. It costs whatever its Java implementation costs on-chain. ## Map a library to a module Pick a module name and map it to the Java class in the `[java-modules]` table of `elci.toml`: ```toml title="elci.toml" [java-modules] "Julc.Math" = "org.julclang.stdlib.lib.MathLib" ``` Then import it like any Elci module. This spend validator, configured as `[validators.offer]` with `spend = "Example.Offer.validate"`, accepts an offer of at least the minimum number of whole ada: ```elm title="src/Example/Offer.elm" module Example.Offer exposing (validate) import Julc.Ledger exposing (ScriptContext) import Julc.Math as Math validate : Int -> Int -> ScriptContext -> Bool validate minimumAda offeredLovelace _ = Math.floorDiv offeredLovelace 1000000 >= minimumAda ``` ```text $ elci check validator offer (spend): checked; 264 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` The rules: - **You choose the module name.** `Julc.Math` is a convention, not a built-in name. Nothing is mapped automatically, except `Julc.TypedMap` and `Julc.TypedArray`, which the core `Map` and `Array` modules use and which you may not remap. - **Mappings load on import.** An unused mapping is never read. Importing a module that is neither a source module nor mapped fails with `ELCI2101: Missing source module: Julc.Nope; add Julc/Nope.elm under a source directory, or map a Java library to it under [java-modules]`. - **Expose names explicitly.** `import Julc.Math exposing (floorDiv)` works; `exposing (..)` is rejected for Java modules (`ELCI2104`). Qualified use through the alias always works. A name you expose explicitly takes precedence over a prelude name, so `import Julc.Math exposing (max)` replaces the prelude's `max` in that module. - **Core module names are taken.** A `[java-modules]` key can't be `Basics`, `List`, `Maybe`, `Result`, `String`, `Tuple`, `Map`, `Array`, `Dict`, `Value`, `Interval`, `Tx`, `Set` or `Elci`, or start with `Elci.`. - **Names are checked where you use them.** A misspelled export is reported as `ELCI2005: Unknown name 'Math.maximum'; check that the imported module exports it`. ## Bundled libraries The compiler ships Julc's standard libraries as source, including inside the native executable. Each class is in the `org.julclang.stdlib.lib` package: | Class | Use it for | | --- | --- | | `MathLib` | `expMod`, `sign`, floor division and modulus (`max`, `min`, `abs` and `pow` are in the prelude as `max`, `min`, `abs` and `^`) | | `IntervalLib` | building validity intervals; for deadline checks, use the core [`Interval`](https://elci-lang.dev/reference/core-modules/#interval) module, which has no `-1` sentinel | | `ContextsLib` | reading the script context; the core [`Tx`](https://elci-lang.dev/reference/core-modules/#tx) module covers signers, own input and continuing outputs | | `OutputLib` | finding outputs and amounts paid to an exact address, staking part included | | `ValuesLib` | a few `Value` queries; the core [`Value`](https://elci-lang.dev/reference/core-modules/#value) module covers the rest, and exports that return a `Value` are rejected | | `AddressLib` | address and credential helpers | | `CryptoLib` | hashes and Ed25519/ECDSA/Schnorr signature checks | | `ByteStringLib` | hex and decimal conversion, CBOR serialization | | `BitwiseLib` | bitwise operations on byte strings | | `ListsLib` | list helpers over `List Data`, plus generic `map`, `filter`, `any`, `all`, `find`, `foldl` | | `MapLib` | map helpers over `Map Data Data` | The [standard library](https://elci-lang.dev/advanced/standard-library/) page lists the verified exports of each library with their Elci types. ## Use your own library JAR You can package your own `@OnchainLibrary` classes and use them the same way. The JAR carries the Java source under `META-INF/plutus-sources/`; Elci reads it but never loads or runs its classes. 1. Write the library. Put the source where the JAR expects it: `java/META-INF/plutus-sources/acme/Pricing.java`. ```java title="java/META-INF/plutus-sources/acme/Pricing.java" package acme; import java.math.BigInteger; import org.julclang.core.types.JulcList; import org.julclang.stdlib.annotation.OnchainLibrary; @OnchainLibrary public class Pricing { public record Quote(BigInteger amount, boolean approved) {} public sealed interface Action { record Buy(BigInteger amount) implements Action {} record Cancel() implements Action {} } public static Quote quote(BigInteger amount) { return new Quote(amount, true); } public static BigInteger fee(BigInteger amount, BigInteger basisPoints) { return amount.multiply(basisPoints).divide(BigInteger.valueOf(10000)); } public static T firstOr(JulcList items, T fallback) { if (items.isEmpty()) return fallback; return items.head(); } } ``` 2. Package the source into a JAR: ```sh mkdir -p libs jar --create --file libs/pricing.jar -C java . ``` 3. Add the JAR and the module mapping to `elci.toml`. JAR paths are relative to the project directory: ```toml title="elci.toml" [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" library-jars = ["libs/pricing.jar"] [entrypoint] module = "Main" function = "main" purpose = "function" [java-modules] "Acme.Pricing" = "acme.Pricing" ``` 4. Import the module and use its functions and types: ```elm title="src/Main.elm" module Main exposing (main) import Acme.Pricing as Pricing exposing (Action(..), Quote) type alias Order = { amount : Int, open : Bool } amount : Action -> Int amount action = case action of Buy value -> value Cancel -> 0 firstOrder : Order firstOrder = Pricing.firstOr [ { amount = 7, open = True } ] { amount = 0, open = False } main : Int main = let quote = Pricing.Quote 250 True in Pricing.fee quote.amount 200 + amount (Buy 3) + Pricing.firstOr [ 10, 20 ] 0 + firstOrder.amount ``` 5. Run it: ```text $ elci eval 25 cpu 7620971, mem 29645 ``` The project now looks like this: - elci.toml - java/ - META-INF/ - plutus-sources/ - acme/ - Pricing.java - libs/ - pricing.jar - src/ - Main.elm Rules for library JARs: - **Bundle every source the library needs**, including records and other libraries it calls, even when they live in another JAR. Files are read in a deterministic order. - **No duplicates.** Two JARs providing the same class, or a JAR providing a class the compiler already bundles, is an error: `Custom library conflicts with bundled source: org.julclang.stdlib.lib.MathLib`. - **A JAR must contain sources.** Otherwise the build stops with `No on-chain Java sources in .../libs/pricing.jar; bundle sources under META-INF/plutus-sources/`. - **A normal library JAR works** if it also bundles its sources under `META-INF/plutus-sources/`. - **The native executable supports JARs too**, because it reads them as source files. The runnable version of this pattern is the [custom-library example](https://github.com/bloxbean/elci-lang/tree/main/examples/custom-library). ## Java types in Elci Julc describes each Java parameter and result type, and Elci maps it to an Elci type: | Java type | Elci type | | --- | --- | | `BigInteger`, `Integer`, `Long`, `int`, `long`, `short`, `byte`, `char` | `Int` | | `boolean`, `Boolean` | `Bool` | | `String` | `String` | | `byte[]` | `Bytes` | | `PlutusData` | `Data` | | `List`, `JulcList` | `List t` | | `Map`, `JulcMap` | `Map k v` | | `Optional` | `JulcOptional t` (convert with `Julc.Data.fromOptional`) | | `JulcArray` | `Array t` | | `void` | `()` | | A record or sealed interface in library source | a nominal type, such as `Pricing.Quote` | | A ledger type such as `PubKeyHash` or `TxOut` | the matching `Julc.Ledger` type | | An unbounded type parameter `T` | a type variable, specialized at each use | Imported types stay **nominal**. `Pricing.Quote` is not interchangeable with an Elci record of the same shape, and a `PubKeyHash` is not `Bytes` even though both are byte strings on-chain. Java's `Optional` is not Elci's `Maybe` either: their Data tags differ, so you convert explicitly with `Julc.Data.fromOptional` and `Julc.Data.toOptional` (see [core modules](https://elci-lang.dev/reference/core-modules/)). Records and sealed interfaces declared in the library class, or at the top level of its package, can be named through the module alias (`Pricing.Quote`) or exposed (`exposing (Quote)`). Two exposed types with the same name are rejected as ambiguous; map the other package to a second module name. ## Imported records and sum types Julc approves a set of operations for each imported type it has verified, and Elci enables exactly those: | Operation | What you can write | | --- | --- | | Construct | `Pricing.Quote 250 True`, `Buy 3`. Record fields are in Java declaration order. | | Match | Exhaustive `case` over a sealed interface's records, such as `Buy value` and `Cancel`. | | Project | Field access: `quote.amount`. | | Equality | `==` and `/=`, with Java `equals` semantics. | | Encode and decode | `Julc.Data.encode` and `Julc.Data.decode`. | | Boundary | Use as a validator's datum, redeemer or parameter type. | To expose a sum's constructors unqualified, write `exposing (Action(..))`. Types with special layouts get fewer operations. The ledger's `Value` is the core [`Value`](https://elci-lang.dev/reference/core-modules/#value) type, with its own module and `==`. Apart from Julc's context accessors, no Java library may return one (below). Types that contain it, such as `TxOut`, keep field access and matching but have no equality, decoding or boundary use. Here the Java types form a validator's datum and redeemer: ```elm title="src/Shop.elm" module Shop exposing (spend) import Acme.Pricing as Pricing exposing (Action(..), Quote) import Julc.Ledger exposing (ScriptContext) spend : Int -> Quote -> Action -> ScriptContext -> Bool spend maximum quote action _ = case action of Buy amount -> quote.approved && amount + Pricing.fee amount 200 <= maximum Cancel -> True ``` ```text $ elci test PASS ShopTest.buysWithinTheMaximum (cpu 11503887, mem 45868) PASS ShopTest.overpaymentIsRejected (cpu 11487887, mem 45768) 2 passed, 0 failed ``` The record encodes as a constructor with tag 0 and its fields in declaration order. The sealed interface's records take tags in declaration order: `Buy` is 0 and `Cancel` is 1. The build's `abi.json` records this. ## Generic methods A generic Java method such as `firstOr` is a **template**. Elci imports it as a generic function and asks Julc to compile a separate copy for each concrete type you use it at. In the example above, `Pricing.firstOr [ 10, 20 ] 0` compiles a copy for `Int`, and `firstOrder` compiles another for the Elci record `Order`. Elci's own records, tuples and unions are valid type arguments. Every type parameter must have a Plutus Data encoding: an integer, bytes, string, `Bool`, `Data`, a record or union (Elci or imported), or a list, map or `JulcOptional` of these. A function or `()` is rejected before anything is compiled: ```text src/Main.elm:8:5: ELCI3003: Type argument T = Unit of firstOr must have a Plutus Data encoding (DATA_ENCODABLE); functions, unit and native values do not ``` A type that contains a `Dict` is rejected too. A `Dict` must keep its keys in ascending order, and a library could build one that doesn't, so pass it a `Map` or a `List` and build the dictionary with `Dict.fromList`: ```text src/Main.elm:14:18: ELCI3003: Type argument a = Dict Int Int of filter contains a Dict, which a Java library cannot keep sorted; pass a Map or a List and build the Dict with Dict.fromList ``` A type that holds a `Value` is rejected for the same reason: every value must stay in the ledger's canonical order, and a library could build one that isn't. ```text src/Main.elm:9:5: ELCI3003: Type argument a = Value of filter contains a Value, which a Java library does not keep in the ledger's canonical order; build it with the Value module ``` Calls are strict and curried, in Java argument order: each argument is evaluated once, even under partial application. :::caution[Equality inside a template] A template that compares values of its type parameter uses Data equality, whatever the type. For a type without equality, such as a `Map` or a type containing one, that comparison is sensitive to map order. Avoid instantiating such templates at those types. ::: ## Programmatic list exports `ListsLib` also offers `map`, `filter`, `any`, `all`, `find` and `foldl` as generic functions built directly in PIR. As in Java, the list comes first: ```toml title="elci.toml" [java-modules] "Julc.Lists" = "org.julclang.stdlib.lib.ListsLib" ``` ```elm openTotal : List Order -> Int openTotal items = Lists.filter items (\order -> order.open) |> Lists.foldl (\sum order -> sum + order.amount) 0 ``` The function argument is evaluated once, strictly, even for an empty list. You rarely need these: the built-in `List` module needs no configuration, and the same computation written with `List.filter` and `List.foldl` was cheaper in our measurement (11.4 million CPU and 288 bytes, against 15.0 million CPU and 354 bytes). ## What is rejected, and why Elci never pretends an unsupported Java type is plain `Data`. A signature it cannot represent faithfully is rejected when you use it, with the reason: | Java signature | Result | | --- | --- | | A type Julc cannot map, such as `Tuple2`, `PlutusData.ListData`, `JulcValue` or BLS points | `ELCI2105: org.julclang.stdlib.lib.MathLib.divMod: Unsupported on-chain source type: Tuple2` | | A result that can hold a ledger `Value`, directly or in a record or union field, except in `ContextsLib` and `OutputLib` | `ELCI2105: org.julclang.stdlib.lib.ValuesLib.add: returns a Value, which Java libraries do not keep in the ledger's canonical order; use the core Value module` | | A bounded type parameter, such as `>` | `ELCI2105: acme.Pricing.largest: Type parameter T extends Comparable has a bound; generic library methods support only unbounded type parameters` | | A raw container, such as `List` without a type argument | `ELCI2105: acme.Pricing.count: Raw or invalid container signature: List` | | Overloads with different signatures | `ELCI2105: acme.Pricing.scale: Ambiguous overloaded library export acme.Pricing.scale: overloads with different signatures cannot be told apart on-chain; give them distinct names` | | A function, `()` or a type containing a `Dict` or a `Value` as a generic type argument | `ELCI3003` (above) | | `exposing (..)` on a Java module | `ELCI2104: Java modules cannot be imported with exposing (..); list the exported names you use` | Some library shapes stop the whole library from loading, so no export of it can be used: - A wildcard type such as `JulcList` fails with `Cannot resolve type: ?`. - A parameterized record such as `record Box(T value)` fails with `Unknown type: T`. While a class has an ambiguous overload, Elci reports that overload for every export of the class. Give overloads distinct names. Overloads with identical Elci signatures, such as `ByteStringLib`'s two `integerToByteString` methods, are exported once. ## Linking and cost In a validator, each library function and each generic specialization is linked **once per script** and shared by all handlers. Zero-argument library values are evaluated once when the script starts, like top-level values. In function programs (`elci eval` and each test), a closed copy is inlined at each use. A library import therefore adds its compiled size to your script; see [costs and performance](https://elci-lang.dev/advanced/costs/) for how to measure it. --- # Standard library > What ships with Elci — Elm's core modules, written in Elci, and Julc's verified Java libraries as gap fillers — how to import them, and the most useful functions of each. Source: https://elci-lang.dev/advanced/standard-library/ Elci's standard library has two layers. The first is Elci's core library: Elm's core modules, with Elm's names, argument orders and edge cases, bundled with the compiler, plus `Value`, `Interval` and `Tx` modules for the ledger's multi-asset values, time ranges and transactions. Reach for it first. The second is Julc's on-chain Java libraries, which you map in `elci.toml`. They fill the gaps, such as floor division, signature verification and matching whole addresses. This page shows what each layer offers, how to import it and which functions you will reach for most. Elm packages cannot be installed. ## The core library As in Elm, every module imports these for you: | Module | Imported as | What it gives you | | --- | --- | --- | | `Basics` | `import Basics exposing (..)` | `min`, `max`, `abs`, `negate`, `clamp`, `compare`, `identity`, `always`, `not`, `xor`, `modBy`, `remainderBy`, and the `Order` type | | `List` | `import List` | Elm's list functions: `map`, `foldl`, `head`, `range`, `sum`, `member`, `sort`, `sortBy`, `filterMap` and more | | `Maybe` | `import Maybe exposing (Maybe(..))` | `withDefault`, `map`, `map2`, `map3`, `andThen` | | `Result` | `import Result exposing (Result(..))` | `Ok`, `Err`, `map`, `map2`, `map3`, `andThen`, `withDefault`, `toMaybe`, `fromMaybe`, `mapError` | | `Tuple` | `import Tuple` | `pair`, `first`, `second`, `mapFirst`, `mapSecond`, `mapBoth` | | `String` | `import String` | `fromInt`, `isEmpty`, `append`, `concat`, `join` | | `Debug` | `import Debug` | `trace`, which emits a Plutus trace | These need an import but no configuration: | Module | Import | What it gives you | | --- | --- | --- | | `Dict` | `import Dict exposing (Dict)` | Elm's dictionaries: comparable keys, kept sorted | | `Map` | `import Map` | the ledger's association maps, for ledger data and keys that aren't comparable | | `Array` | `import Array` | arrays with constant-time `get` and `length` | | `Value` | `import Value exposing (Value)` | the ledger's multi-asset values: `lovelaceOf`, `quantityOf`, `add`, `geq` and more | | `Interval` | `import Interval` | time ranges such as the validity range: `isEntirelyAfter`, `isEntirelyBefore`, `contains`, without sentinel values | | `Tx` | `import Tx` | questions about the transaction: `signedBy`, `ownInput`, `outputsTo`, `continuingOutputs`, `mintedOf`, `inlineDatum` and more | | `Bytes` | `import Bytes` | byte-string builtins: slicing, integers, bits, hashes | | `Julc.Data` | `import Julc.Data as Data` | explicit `decode`, `encode` and optional/`Maybe` adapters | | `Julc.Ledger` | `import Julc.Ledger exposing (...)` | ledger types such as `ScriptContext`, `PubKeyHash` and `TxOutRef` | The types `Int`, `Bool`, `String`, `Bytes`, `Data`, `List`, `Maybe` (with `Just` and `Nothing`), `Dict`, `Map` and `Array` are built in. `Basics`, `List`, `Maybe`, `Result`, `Tuple`, `String`, `Dict`, `Map`, `Array`, `Value`, `Interval` and `Tx` are written in Elci itself, and compiled with your code; a function you don't call is never compiled into your script. `Dict`, `Map` and `Array` sit on top of Julc's typed map and array libraries and of list operations the compiler specializes for their key and value types. `Value` sits on the protocol's native value builtins (CIP-153). `Interval` and `Tx` read the ledger's types directly. Every signature is on the [core modules](https://elci-lang.dev/reference/core-modules/) reference page, and [lists, dictionaries, maps and arrays](https://elci-lang.dev/learn/collections/) teaches them. Bytes get their own page: [bytes, hashes and bits](https://elci-lang.dev/advanced/bytes-and-crypto/). ## Julc's Java libraries Julc ships its on-chain standard library as Java source, and the Elci compiler bundles it. To use a library, map a module name to its class, then import that module. Here is every bundled library, with the module names used on this site: ```toml title="elci.toml" [java-modules] "Julc.Address" = "org.julclang.stdlib.lib.AddressLib" "Julc.Bitwise" = "org.julclang.stdlib.lib.BitwiseLib" "Julc.ByteString" = "org.julclang.stdlib.lib.ByteStringLib" "Julc.Contexts" = "org.julclang.stdlib.lib.ContextsLib" "Julc.Crypto" = "org.julclang.stdlib.lib.CryptoLib" "Julc.Interval" = "org.julclang.stdlib.lib.IntervalLib" "Julc.Lists" = "org.julclang.stdlib.lib.ListsLib" "Julc.MapLib" = "org.julclang.stdlib.lib.MapLib" "Julc.Math" = "org.julclang.stdlib.lib.MathLib" "Julc.Output" = "org.julclang.stdlib.lib.OutputLib" "Julc.Values" = "org.julclang.stdlib.lib.ValuesLib" ``` Map only what you import; an unused mapping is never loaded. The module names are yours to choose, except the core modules' names: a `[java-modules]` key can't be `Interval`, `Tx` or another core module. How mapping, types and rejections work in general is covered in [Java libraries](https://elci-lang.dev/advanced/java-libraries/). ### What "verified" means The compiler repository keeps an inventory of every public export. An export is **compile-verified** when an Elci wrapper with its concrete signature type-checks, links the real Java implementation and produces UPLC. That proves the signature maps correctly; it does not prove each function was evaluated with representative inputs through Elci. Julc tests the libraries' behavior separately. The current scan found **145 compile-verified exports**, **41 unsupported signatures**, **6 exports that return a `Value`**, which Elci rejects, and **12 generic templates**: | Library | Verified exports | Gaps | | --- | ---: | --- | | `AddressLib` | 4 | none | | `BitwiseLib` | 9 | `writeBits` (uses `PlutusData.ListData`) | | `BlsLib` | 0 | all 19 exports use BLS point types | | `ByteStringLib` | 24 | none | | `ContextsLib` | 23 | `listIndex` (uses `PlutusData.ListData`) | | `CryptoLib` | 9 | none | | `IntervalLib` | 9 | none | | `ListsLib` | 17 | plus 6 generic programmatic exports | | `MapLib` | 10 | concrete `Data`-based signatures only | | `MathLib` | 8 | `divMod`, `quotRem` (return a Java `Tuple2`) | | `NativeValueLib` | 0 | all 7 exports use `JulcValue` | | `OutputLib` | 14 | none | | `ValuesLib` | 18 | 11 exports use unmapped Data subtypes or `AssetEntry`; 6 return a `Value` | | `TypedMapLib` | 9 generic | used by the core `Map` module | | `TypedArrayLib` | 3 generic | used by the core `Array` module | Using an unsupported export is a compile error (`ELCI2105`) that names the Java type involved. An export whose result can hold a `Value` is rejected the same way, because Java libraries don't keep values in the ledger's order; use the core [`Value`](https://elci-lang.dev/reference/core-modules/#value) module instead. `ContextsLib` and `OutputLib` are exempt: they only pass along the ledger's own values. Some verified exports are public implementation helpers (their names often start with `_` or end in `Step`). They are listed for completeness; do not build on them. ## An example This listing validator uses two Java libraries next to the core `Tx`, `Interval` and `Value` modules. It requires the seller's address to be a key address, and the payment to reach that exact address, staking part included. `Tx.outputsTo` would match the key whatever the staking part, so this is a case for `OutputLib`: ```toml title="elci.toml" [java-modules] "Julc.Address" = "org.julclang.stdlib.lib.AddressLib" "Julc.Output" = "org.julclang.stdlib.lib.OutputLib" ``` ```elm title="src/Market.elm" module Market exposing (Listing, spend) import Interval import Julc.Address as Address import Julc.Data as Data import Julc.Ledger exposing (Address, ScriptContext, TxOut, TxOutRef) import Julc.Output as Output import Tx import Value type alias Listing = { seller : Address, price : Int, deadline : Int } {-| Anyone may buy before the deadline by paying the seller's exact address, with the listing's output reference as the payment's inline datum. -} spend : Listing -> () -> ScriptContext -> Bool spend listing _ ctx = case Tx.ownRef ctx of Just ref -> Address.isPubKeyAddress listing.seller && Interval.isEntirelyBefore listing.deadline ctx.txInfo.validRange && List.any (pays listing.price ref) (Output.outputsAt ctx.txInfo.outputs listing.seller) Nothing -> False pays : Int -> TxOutRef -> TxOut -> Bool pays price ref output = (Value.lovelaceOf output.value >= price) && (Tx.inlineDatum output == Just (Data.encode ref)) ``` ```text $ elci check validator market (spend): checked; 1297 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` Things to notice: - **Java argument order.** Library functions take the collection or subject first: `Output.outputsAt outputs address`. Core functions take it last, as in Elm: `Tx.outputsTo credential ctx`. - **Optional results.** A Java function that returns an `Optional`, such as `ContextsLib.findOwnInput`, gives a Julc optional. Convert it with `Data.fromOptional` before you match on `Just` and `Nothing`. - **Core and Java together.** `Tx`, `Interval` and `Value` are core modules; `Address` and `Output` are Java library calls. The payment carries the listing's reference, so one payment can't buy two listings ([one payment per input](https://elci-lang.dev/contracts/reading-the-transaction/#one-payment-per-input)). - **Size.** Library code is linked into your script: this validator is 1,297 bytes. Check the size after adding a library. ## Most useful functions The tables give each export's Elci type. `Ledger.X` means the type `X` from `Julc.Ledger`. ### `MathLib` | Function | Type | Notes | | --- | --- | --- | | `sign` | `Int -> Int` | `-1`, `0` or `1` | | `expMod` | `Int -> Int -> Int -> Int` | modular exponentiation: `Math.expMod 3 4 5` is `1` | | `floorDiv`, `floorMod` | `Int -> Int -> Int` | round towards negative infinity: `Math.floorDiv -7 2` is `-4` and `Math.floorMod -7 2` is `1` | | `max`, `min` | `Int -> Int -> Int` | the prelude's `max` and `min` do the same, for any comparable type | | `abs` | `Int -> Int` | the prelude has `abs` | | `pow` | `Int -> Int -> Int` | like the `^` operator (`2 ^ 10` is `1024`), except that a negative exponent gives `1`, where `^` fails | Use the prelude's `max`, `min`, `abs` and `^` rather than their `MathLib` versions: they need no mapping. Elci's own `//` truncates towards zero, and `modBy` and `remainderBy` are in the prelude. ### `IntervalLib` | Function | Type | Notes | | --- | --- | --- | | `contains` | `Ledger.Interval -> Int -> Bool` | bounds are inclusive | | `after`, `before` | `Int -> Ledger.Interval` | from a time onwards; up to a time | | `between` | `Int -> Int -> Ledger.Interval` | both ends inclusive | | `always`, `never` | `Ledger.Interval` | | | `isEmpty` | `Ledger.Interval -> Bool` | | | `finiteLowerBound`, `finiteUpperBound` | `Ledger.Interval -> Int` | return `-1` for an infinite bound | The `-1` sentinel is easy to miss: `finiteUpperBound (after 100)` is `-1`, so "no end" looks like "ends at -1". Use the core [`Interval`](https://elci-lang.dev/reference/core-modules/#interval) module instead: its checks never hold on an infinite bound, and its `start` and `end` return `Nothing` for one. Map `IntervalLib` under another name than `Interval`, which is the core module's. ### `ContextsLib` | Function | Type | | --- | --- | | `signedBy` | `Ledger.TxInfo -> Bytes -> Bool` | | `findOwnInput` | `Ledger.ScriptContext -> JulcOptional Ledger.TxInInfo` | | `getContinuingOutputs` | `Ledger.ScriptContext -> List Ledger.TxOut` | | `ownHash` | `Ledger.ScriptContext -> Bytes` | | `scriptOutputsAt` | `Ledger.TxInfo -> Bytes -> List Ledger.TxOut` | | `valuePaid` | `Ledger.TxInfo -> Ledger.Address -> List Ledger.Value` | | `valueSpent` | `Ledger.TxInfo -> List Ledger.Value` | | `findDatum` | `Ledger.TxInfo -> Data -> JulcOptional Data` | | `getSpendingDatum` | `Ledger.ScriptContext -> JulcOptional Data` | | `getRedeemer` | `Ledger.ScriptContext -> Data` | The core [`Tx`](https://elci-lang.dev/reference/core-modules/#tx) module covers these with typed results: `Tx.signedBy`, `Tx.ownInput`, `Tx.continuingOutputs`, `Tx.outputsTo`, `Tx.valueSpent` and `Tx.datumOf`. Prefer it. `ContextsLib.signedBy` takes raw bytes, so it needs a key hash's `.hash`: `Contexts.signedBy ctx.txInfo owner.hash`. The `txInfo...` accessors (`txInfoFee`, `txInfoInputs` and so on) duplicate fields you can read directly, such as `ctx.txInfo.fee`. `valuePaid` and `valueSpent` return the ledger's values, one per output or input. ### `OutputLib` | Function | Type | | --- | --- | | `lovelacePaidTo` | `List Ledger.TxOut -> Ledger.Address -> Int` | | `paidAtLeast` | `List Ledger.TxOut -> Ledger.Address -> Int -> Bool` | | `outputsAt` | `List Ledger.TxOut -> Ledger.Address -> List Ledger.TxOut` | | `countOutputsAt` | `List Ledger.TxOut -> Ledger.Address -> Int` | | `uniqueOutputAt` | `List Ledger.TxOut -> Ledger.Address -> Ledger.TxOut` | | `outputsWithToken` | `List Ledger.TxOut -> Bytes -> Bytes -> List Ledger.TxOut` | | `findOutputWithToken` | `List Ledger.TxOut -> Bytes -> Bytes -> Bytes -> Ledger.TxOut` | | `findInputWithToken` | `List Ledger.TxInInfo -> Bytes -> Bytes -> Bytes -> Ledger.TxInInfo` | | `valueHasToken` | `Ledger.Value -> Bytes -> Bytes -> Bool` | | `getInlineDatum` | `Ledger.TxOut -> Data` | | `txOutAddress`, `txOutValue`, `txOutDatum` | `Ledger.TxOut -> ...` | `OutputLib` matches whole addresses, staking part included, where `Tx.outputsTo` and `Tx.valuePaidTo` match the payment credential only. Use it when the staking part matters. `getInlineDatum` fails the script for an output without an inline datum; `Tx.inlineDatum` returns a `Maybe`. ### `ValuesLib` Use the core [`Value`](https://elci-lang.dev/reference/core-modules/#value) module for values: it reads, builds and compares them, and `==` works on its results. Julc's `ValuesLib` builds values in its own order, so its exports that return a `Value` (`add`, `subtract`, `negate`, `singleton` and two helpers) are rejected with `ELCI2105`. The exports that only read a value still work, and a few have no direct equivalent in the `Value` module: | Function | Type | | --- | --- | | `containsPolicy` | `Ledger.Value -> Bytes -> Bool` | | `countTokensWithQty` | `Ledger.Value -> Bytes -> Int -> Int` | | `findTokenName` | `Ledger.Value -> Bytes -> Int -> Bytes` | | `uniqueTokenName` | `Ledger.TxOutRef -> Bytes` | `Value.policies` and `Value.tokens` answer the same questions with typed results. ### `AddressLib` and `CryptoLib` | Function | Type | | --- | --- | | `Address.isPubKeyAddress`, `Address.isScriptAddress` | `Ledger.Address -> Bool` | | `Address.paymentCredential` | `Ledger.Address -> Ledger.Credential` | | `Address.credentialHash` | `Ledger.Address -> Bytes` | | `Crypto.verifyEd25519Signature` | `Bytes -> Bytes -> Bytes -> Bool` (key, message, signature) | | `Crypto.verifyEcdsaSecp256k1`, `Crypto.verifySchnorrSecp256k1` | `Bytes -> Bytes -> Bytes -> Bool` | | `Crypto.sha2_256`, `sha3_256`, `blake2b_224`, `blake2b_256`, `keccak_256`, `ripemd_160` | `Bytes -> Bytes` | The hashes are also in the built-in `Bytes` module; the signature checks are only here. ### `ByteStringLib` and `BitwiseLib` | Function | Type | Notes | | --- | --- | --- | | `ByteString.toHex` | `Bytes -> Bytes` | lowercase hex digits as UTF-8 bytes | | `ByteString.intToDecimalString` | `Int -> Bytes` | non-negative integers only | | `ByteString.utf8ToInteger` | `Bytes -> Int` | ASCII digits only, no sign | | `ByteString.serialiseData` | `Data -> Bytes` | CBOR encoding | | `ByteString.zeros` | `Int -> Bytes` | | | `Bitwise.andByteString`, `orByteString`, `xorByteString` | `Bool -> Bytes -> Bytes -> Bytes` | `True` pads the shorter operand at the end | | `Bitwise.readBit` | `Bytes -> Int -> Bool` | | The rest of both libraries mirrors builtins that the `Bytes` module already provides with the byte string last. [Bytes, hashes and bits](https://elci-lang.dev/advanced/bytes-and-crypto/) covers both. ### `ListsLib` and `MapLib` Most `ListsLib` and `MapLib` exports work on `List Data` and `Map Data Data`, so you must encode every element with `Julc.Data.encode` first. Examples are `ListsLib.containsInt : List Data -> Int -> Bool`, `ListsLib.hasDuplicateInts : List Data -> Bool` and `MapLib.lookup : Map Data Data -> Data -> JulcOptional Data`. Prefer the typed core `List`, `Dict` and `Map` modules. `ListsLib`'s generic `map`, `filter`, `any`, `all`, `find` and `foldl` are covered in [Java libraries](https://elci-lang.dev/advanced/java-libraries/). ## What is missing - **BLS12-381** (`BlsLib`) and **native values** (`NativeValueLib`) have no usable exports yet. - **`Value` at the boundary.** A `Value` can't be a datum, redeemer or parameter yet. Store a `List ( PolicyId, TokenName, Int )` and rebuild it with `Value.fromList`. - **Parts of Elm's core** are not available yet: `Set` (use a `Dict k Bool`), `Char`, most of `String` (`length`, `toInt`, `split` and so on), and the `Float` functions. Write small helpers yourself or use the Java libraries above. ## Your own libraries You can write reusable libraries in Elci: ordinary modules under any source directory, listed in `source-directories`. There is no package registry yet; share them as source. See [modules and imports](https://elci-lang.dev/learn/modules/). To reuse Java code, package it as a [library JAR](https://elci-lang.dev/advanced/java-libraries/). --- # Bytes, hashes and bits > Build, slice and compare byte strings, convert integers, use bit operations and hashes, verify signatures and print bytes as hex in Elci. Source: https://elci-lang.dev/advanced/bytes-and-crypto/ Byte strings are everywhere on Cardano: key hashes, script hashes, token names and transaction IDs are all bytes. This page walks through the `Bytes` module function by function, then covers hashes, signature checks and the `Julc.ByteString` helpers. Every example is a function project you can run with `elci eval`. ## The `Bytes` type `Bytes` is a built-in type, so you can write it in any signature. The functions live in the compiler-provided `Bytes` module, which you import like any other module: ```elm import Bytes ``` A few things to know before you start: - **There is no byte literal.** You create bytes with `Bytes.fromString` (UTF-8), `Bytes.fromIntBigEndian`, `Bytes.replicate` or `Bytes.empty`. In a real contract most bytes arrive in a datum, redeemer, parameter or the transaction itself. - **Every function is a direct builtin call.** There is no hidden loop, so each call is cheap and predictable. - **The byte string comes last.** Functions take the byte string as their final argument, so they read well in pipelines: `name |> Bytes.take 4`. - **`==` compares bytes, and `<` orders them.** Ledger hashes such as `PubKeyHash` are separate nominal types, even though they are byte strings underneath. They compare and order like bytes, but only with their own type. Use the `.hash` field to get their raw `Bytes`. ## Seeing bytes with `elci eval` `elci eval` prints a byte string as `#` followed by its hex digits, and the empty byte string as `#(empty)`. Here is a small function project that checks a token-name prefix: ```elm title="src/Main.elm" module Main exposing (main) import Bytes hasPrefix : Bytes -> Bytes -> Bool hasPrefix prefix name = Bytes.take (Bytes.length prefix) name == prefix main : Bool main = hasPrefix (Bytes.fromString "GOLD") (Bytes.fromString "GOLD-042") ``` Add `--builtins` to see each builtin call with its arguments and result: ```text $ elci eval --builtins True cpu 714261, mem 3329 builtin: EncodeUtf8("GOLD") → #474f4c44 builtin: EncodeUtf8("GOLD-042") → #474f4c442d303432 builtin: LengthOfByteString(#474f4c44) → 4 builtin: SliceByteString(0, 4, #474f4c442d303432) → #474f4c44 builtin: EqualsByteString(#474f4c44, #474f4c44) → True ``` The builtin log keeps the last 20 calls, which is plenty for small experiments like these. ## Function reference | Group | Function | Type | Builtin | | --- | --- | --- | --- | | Building | `empty` | `Bytes` | constant | | | `fromString` | `String -> Bytes` | `encodeUtf8` | | | `toString` | `Bytes -> String` | `decodeUtf8` | | | `cons` | `Int -> Bytes -> Bytes` | `consByteString` | | | `replicate` | `Int -> Int -> Bytes` | `replicateByte` | | | `append` | `Bytes -> Bytes -> Bytes` | `appendByteString` | | Reading | `length` | `Bytes -> Int` | `lengthOfByteString` | | | `at` | `Int -> Bytes -> Int` | `indexByteString` | | | `slice` | `Int -> Int -> Bytes -> Bytes` | `sliceByteString` | | | `take` | `Int -> Bytes -> Bytes` | `sliceByteString` | | | `drop` | `Int -> Bytes -> Bytes` | `sliceByteString` | | Comparing | `==`, `/=` (operators) | `Bytes -> Bytes -> Bool` | `equalsByteString` | | | `<`, `<=`, `>`, `>=` (operators) | `Bytes -> Bytes -> Bool` | `lessThanByteString`, `lessThanEqualsByteString` | | | `compare`, `min`, `max` (prelude) | `Bytes -> Bytes -> Order`, `Bytes -> Bytes -> Bytes` | `lessThanByteString`, `equalsByteString` | | Integers | `fromIntBigEndian` | `Int -> Int -> Bytes` | `integerToByteString` | | | `fromIntLittleEndian` | `Int -> Int -> Bytes` | `integerToByteString` | | | `toIntBigEndian` | `Bytes -> Int` | `byteStringToInteger` | | | `toIntLittleEndian` | `Bytes -> Int` | `byteStringToInteger` | | Bits | `and`, `or`, `xor` | `Bytes -> Bytes -> Bytes` | `andByteString`, `orByteString`, `xorByteString` | | | `complement` | `Bytes -> Bytes` | `complementByteString` | | | `shift` | `Int -> Bytes -> Bytes` | `shiftByteString` | | | `rotate` | `Int -> Bytes -> Bytes` | `rotateByteString` | | | `testBit` | `Int -> Bytes -> Bool` | `readBit` | | | `countSetBits` | `Bytes -> Int` | `countSetBits` | | | `findFirstSetBit` | `Bytes -> Int` | `findFirstSetBit` | | Hashes | `blake2b224`, `blake2b256` | `Bytes -> Bytes` | `blake2b_224`, `blake2b_256` | | | `sha256`, `sha3_256` | `Bytes -> Bytes` | `sha2_256`, `sha3_256` | | | `keccak256`, `ripemd160` | `Bytes -> Bytes` | `keccak_256`, `ripemd_160` | ## Building and slicing ```elm title="src/Main.elm" module Main exposing (main) import Bytes abcd : Bytes abcd = Bytes.fromString "abcd" main : List Bytes main = [ Bytes.fromString "GOLD" , Bytes.append (Bytes.fromString "a") (Bytes.fromString "b") , Bytes.cons 255 (Bytes.fromString "ab") , Bytes.replicate 3 7 , Bytes.slice 1 2 abcd , Bytes.slice 2 10 abcd , Bytes.take 2 abcd , Bytes.drop 1 abcd , Bytes.drop 10 abcd ] ``` ```text $ elci eval [#474f4c44, #6162, #ff6162, #070707, #6263, #6364, #6162, #626364, #(empty)] cpu 3394517, mem 14178 ``` What this shows: - `append a b` puts `a` first. In a pipeline that reverses the reading order: `name |> Bytes.append prefix` produces `prefix` followed by `name`. - `slice start count bytes` takes a start index and a **length**, not an end index. It clamps to the available bytes instead of failing, and so do `take` and `drop`. - `cons byte bytes` and `replicate count byte` fail for a byte outside 0–255. `replicate` also fails above 8,192 bytes. ## Reading and comparing ```elm title="src/Main.elm" module Main exposing (main) import Bytes abcd : Bytes abcd = Bytes.fromString "abcd" main : ( List Int, List Bool, String ) main = ( [ Bytes.length abcd, Bytes.at 1 abcd ] , [ Bytes.fromString "ab" < Bytes.fromString "abc" , abcd <= abcd , Bytes.take 2 abcd == Bytes.fromString "ab" ] , Bytes.toString (Bytes.drop 2 abcd) ) ``` ```text $ elci eval ( [4, 98], [True, True, True], "cd" ) cpu 3449703, mem 15299 ``` - `at index bytes` returns a byte as an `Int` from 0 to 255. It **fails** outside the byte string. - `<`, `<=`, `>`, `>=` and `compare` order byte strings lexicographically by unsigned byte, so a proper prefix is smaller. Because bytes are comparable, `min`, `max`, `List.sort` and `List.maximum` work on them, and on ledger hashes such as `PubKeyHash`. - `toString` decodes UTF-8 and fails on invalid UTF-8. The order is lexicographic, not numeric: the one byte `#ff` sorts after the two bytes `#0001`. To order byte strings as numbers, compare `Bytes.toIntBigEndian` of each. ```elm title="src/Main.elm" module Main exposing (main) import Bytes main : ( List Bytes, Order ) main = ( List.sort [ Bytes.fromIntBigEndian 1 200, Bytes.fromString "ab", Bytes.fromIntBigEndian 1 7, Bytes.fromString "a" ] , compare (Bytes.fromIntBigEndian 1 255) (Bytes.fromIntBigEndian 2 1) ) ``` ```text $ elci eval ( [#07, #61, #6162, #c8], GT ) cpu 24647226, mem 95818 ``` ## Integer conversions `fromIntBigEndian width n` and `fromIntLittleEndian width n` write a non-negative integer into exactly `width` bytes. A width of `0` means "as few bytes as needed". The `to...` functions read the bytes back as a non-negative integer. ```elm title="src/Main.elm" module Main exposing (main) import Bytes main : ( List Bytes, List Int ) main = ( [ Bytes.fromIntBigEndian 4 1000 , Bytes.fromIntLittleEndian 4 1000 , Bytes.fromIntBigEndian 0 1000 , Bytes.fromIntBigEndian 0 0 ] , [ Bytes.toIntBigEndian (Bytes.fromIntBigEndian 4 1000) , Bytes.toIntLittleEndian (Bytes.fromIntBigEndian 4 1000) , Bytes.toIntBigEndian Bytes.empty ] ) ``` ```text $ elci eval ( [#000003e8, #e8030000, #03e8, #(empty)], [1000, 3892510720, 0] ) cpu 13938958, mem 13313 ``` Converting fails when the integer is negative (`IntegerToByteString: negative integer`), when it does not fit in the width (`IntegerToByteString: integer doesn't fit in 1 bytes`) or when the width exceeds 8,192 bytes. :::tip[A byte-string constant without a literal] Because `Int` is arbitrary precision, `Bytes.fromIntBigEndian 32 n` turns a decimal constant into a fixed-width byte string. Keys and hashes usually arrive as data or deployment parameters, so you rarely need this. If you do, note that elm-format 0.8.8 corrupts integer literals that do not fit in 64 bits, so check large constants after formatting. ::: ## Bit operations Bits are numbered from the **end** of the byte string: bit 0 is the least significant bit of the last byte. Here `f0` is the single byte `0xf0`: ```elm title="src/Main.elm" module Main exposing (main) import Bytes f0 : Bytes f0 = Bytes.fromIntBigEndian 1 240 main : ( List Bytes, List Bool, List Int ) main = ( [ Bytes.and (Bytes.fromIntBigEndian 2 65535) f0 , Bytes.or (Bytes.fromIntBigEndian 1 15) f0 , Bytes.xor (Bytes.fromIntBigEndian 1 255) f0 , Bytes.complement f0 , Bytes.shift 1 f0 , Bytes.shift -1 f0 , Bytes.rotate 4 (Bytes.fromIntBigEndian 2 4660) , Bytes.rotate -4 (Bytes.fromIntBigEndian 2 4660) ] , [ Bytes.testBit 0 (Bytes.fromIntBigEndian 2 1) , Bytes.testBit 8 (Bytes.fromIntBigEndian 2 256) , Bytes.testBit 0 f0 ] , [ Bytes.countSetBits f0 , Bytes.findFirstSetBit f0 , Bytes.findFirstSetBit (Bytes.replicate 2 0) ] ) ``` ```text $ elci eval ( [#f0, #ff, #0f, #0f, #e0, #78, #2341, #4123], [True, True, False], [4, 4, -1] ) cpu 18446645, mem 28868 ``` - `and`, `or` and `xor` **truncate** to the shorter operand: `0xffff` and `0xf0` give one byte. - `shift n` moves bits towards the front for positive `n` and towards the end for negative `n`. Bits shifted out are lost and the length stays the same. `rotate n` wraps them around instead. - `testBit i` reads bit `i`; `findFirstSetBit` returns the lowest set bit's index, or `-1` when no bit is set. ## Hashes The six hash functions return fixed-length digests: `blake2b224` gives 28 bytes (the size of key and script hashes), `ripemd160` gives 20 bytes and the others give 32 bytes. ```elm title="src/Main.elm" module Main exposing (main) import Bytes abc : Bytes abc = Bytes.fromString "abc" main : List Bytes main = [ Bytes.blake2b224 abc , Bytes.blake2b256 abc , Bytes.sha256 abc , Bytes.sha3_256 abc , Bytes.keccak256 abc , Bytes.ripemd160 abc ] ``` ```text $ elci eval [#9bd237b02a29e43bdd6738afa5b53ff0eee178d6210b618e4511aec8, #bddd813c634239723171ef3fee98579b94964e3bb1cb3e427262c8c068d52319, #ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad, #3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532, #4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45, #8eb208f7e05d987a9b044a8e98c6b087f15a0bfc] cpu 8104959, mem 7043 ``` Hashing is cheap: `Bytes.blake2b256` over a five-byte input adds roughly 240,000 CPU units. A common use is checking that a public key belongs to a key hash: ```elm import Bytes import Julc.Ledger exposing (PubKeyHash) ownsKey : PubKeyHash -> Bytes -> Bool ownsKey owner key = Bytes.blake2b224 key == owner.hash ``` ## Verifying signatures Signature checks come from Julc's `CryptoLib`. Map it, then call `verifyEd25519Signature key message signature`: ```toml title="elci.toml" [java-modules] "Julc.Crypto" = "org.julclang.stdlib.lib.CryptoLib" ``` ```elm title="src/Oracle.elm" module Oracle exposing (spend) import Bytes import Julc.Crypto as Crypto import Julc.Ledger exposing (ScriptContext) type alias Report = { price : Int, signature : Bytes } {-| Accept a price report only when the oracle signed its 8-byte big-endian encoding. -} spend : Bytes -> () -> Report -> ScriptContext -> Bool spend oracleKey _ report _ = Crypto.verifyEd25519Signature oracleKey (Bytes.fromIntBigEndian 8 report.price) report.signature ``` Here `oracleKey` is a deployment parameter (see [parameterized validators](https://elci-lang.dev/contracts/parameters/)). The library also has `verifyEcdsaSecp256k1` and `verifySchnorrSecp256k1` with the same `key -> message -> signature -> Bool` shape. Verifying the RFC 8032 Ed25519 test vector, including building the key and signature bytes, cost about 57 million CPU units: far more than a hash. ## `Julc.ByteString` helpers `ByteStringLib` adds composite helpers that the builtins do not cover. Map it first: ```toml title="elci.toml" [java-modules] "Julc.ByteString" = "org.julclang.stdlib.lib.ByteStringLib" ``` They are ordinary Java library functions, so they cost more than builtins, and they take the byte string **first**, in Java argument order: | Function | Type | Result | | --- | --- | --- | | `toHex` | `Bytes -> Bytes` | lowercase hex digits, as UTF-8 bytes | | `intToDecimalString` | `Int -> Bytes` | decimal digits of a **non-negative** integer | | `utf8ToInteger` | `Bytes -> Int` | parses ASCII digits; no sign handling | | `zeros` | `Int -> Bytes` | `n` zero bytes | | `serialiseData` | `Data -> Bytes` | CBOR encoding of a `Data` value | | `integerToByteString` | `Bool -> Int -> Int -> Bytes` | big-endian flag, width, integer | | `byteStringToInteger` | `Bool -> Bytes -> Int` | big-endian flag, bytes | | `slice` | `Bytes -> Int -> Int -> Bytes` | bytes, start, length | | `take`, `drop`, `at` | `Bytes -> Int -> ...` | as in `Bytes`, bytes first | ```elm title="src/Main.elm" module Main exposing (main) import Bytes import Julc.ByteString as ByteString import Julc.Data as Data main : ( List String, List Bytes, List Int ) main = ( [ Bytes.toString (ByteString.toHex (Bytes.fromString "GOLD")) , Bytes.toString (ByteString.intToDecimalString 1234) ] , [ ByteString.zeros 3 , ByteString.slice (Bytes.fromString "abcd") 1 2 , ByteString.serialiseData (Data.encode 42) , ByteString.serialiseData (Data.encode [ 1, 2 ]) , ByteString.integerToByteString True 2 258 ] , [ ByteString.utf8ToInteger (Bytes.fromString "42") , ByteString.byteStringToInteger False (Bytes.fromIntBigEndian 2 258) ] ) ``` ```text $ elci eval ( ["474f4c44", "1234"], [#000000, #6263, #182a, #9f0102ff, #0102], [42, 513] ) cpu 33110364, mem 94296 ``` `toHex` is useful when you need hex as text, for example in a trace message. It is an ordinary recursive function: `Bytes.toString (ByteString.toHex (Bytes.sha256 (Bytes.fromString "abc")))` cost about 73 million CPU units. Keep it out of a validator's hot path. :::caution `intToDecimalString` and `utf8ToInteger` do not validate their input. A negative number or a non-digit byte produces a meaningless result rather than a failure. ::: The full list of `ByteStringLib` exports is on the [standard library](https://elci-lang.dev/advanced/standard-library/) page. Prefer the `Bytes` builtins where both exist. ## When byte operations fail A failing builtin fails the whole script, exactly like a validator returning `False`. There is no `Maybe` result to check. Guard the input first (for example compare the index with `Bytes.length`) when a failure would be wrong: ```elm title="src/Main.elm" module Main exposing (main) import Bytes main : Int main = Bytes.at 5 (Bytes.fromString "ab") ``` ```text $ elci eval --builtins evaluation failed: IndexByteString: index 5 out of bounds for length 2 cpu 206269, mem 1308 builtin: EncodeUtf8("ab") → #6162 builtin: IndexByteString(#6162, 5) → ``` The command exits with status 1. The other failure messages you may meet are `ConsByteString: byte value out of range: 256`, `ReplicateByte: length exceeds 8192`, `DecodeUtf8: invalid UTF-8 byte sequence` and the `IntegerToByteString` errors above. :::tip[Pin facts with a test] Once an experiment works, turn it into a test so it stays true, for example `Expect.equal 4 (Bytes.length abcd)`. See [unit tests](https://elci-lang.dev/testing/unit-tests/). ::: For every signature in one place, see [core modules](https://elci-lang.dev/reference/core-modules/). --- # Costs and performance > Measure execution budgets and script size, understand how strictness and top-level values affect cost, choose between lists, arrays, dictionaries and maps, and know what values cost. Source: https://elci-lang.dev/advanced/costs/ Every validator run is metered, and every script byte is paid for. This page shows how to measure both with the Elci CLI, explains the language rules that most affect cost, and backs each tip with a measurement you can reproduce. ## What the ledger charges A script run consumes two budgets: - **CPU steps**: how much computation the script does. - **Memory units**: how much data the script allocates. The transaction declares the budget of each script it runs and pays fees for it. The ledger also caps the total per transaction. At the time of writing, mainnet allows 14 million memory units and 10 billion CPU steps per transaction, and a transaction of at most 16,384 bytes. Check the current protocol parameters before you rely on these numbers. In practice, memory usually runs out first. **Script size** matters too: a script is carried in the transaction that runs it, or in a reference input, and fees grow with bytes. All budgets on this page come from Julc's evaluator for Elci's target, `plutus-v3-pv11-uplc-1.1.0`. ## Measuring execution `elci eval` runs a function project's `[entrypoint]` and prints its value, then the budget it consumed: ```text $ elci eval 42 cpu 16100, mem 200 ``` Add `--builtins` to list the builtin calls behind the number (the last 20 of them). `elci test` prints the budget of every test. For a transaction scenario that is the cost of the deployed script, run with a full ledger context. For a property it is the largest cost over all runs: ```text $ elci test --seed 42 PASS VestingTest.claimsAfterTheDeadline (cpu 16763911, mem 61637) PASS VestingTest.claimsInAClosedRange (cpu 16763911, mem 61637) PASS VestingTest.claimsAtTheDeadlineFail (cpu 13950292, mem 52228) PASS VestingTest.earlyClaimsFail (cpu 13950292, mem 52228) PASS VestingTest.unsignedClaimsFail (cpu 16458714, mem 60004) PASS VestingTest.claimsSucceedExactlyAfterTheDeadline (100 runs, max cpu 16763911, mem 61637) 6 passed, 0 failed (seed 42) ``` To turn a budget into a requirement, add `Scenario.withinBudget cpu memory` to a scenario. See [debugging and budgets](https://elci-lang.dev/testing/debugging/). As a baseline, a mint handler that ignores everything and returns `True` compiles to 114 bytes and costs 2,712,472 CPU and 11,155 memory in a scenario. That is roughly the fixed cost every script pays before your handler's own logic runs: the wrapper that checks the purpose and decodes the redeemer. ## Measuring script size `elci check` and `elci build` print each validator's size in FLAT bytes, the serialized script: ```text $ cd treasury && elci build validator badge (mint): checked; 198 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 validator treasury (spend, withdraw): checked; 540 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` The same number is `sizeBytes` in `build/validators//validator.plutus.json`. For reference, the example projects build to: | Example | Handlers | FLAT bytes | | --- | --- | ---: | | `threshold` | spend | 149 | | `signed` | spend | 241 | | `vesting` | spend | 533 | | `treasury` (`badge`) | mint | 198 | | `treasury` (`treasury`) | spend, withdraw | 540 | What adds bytes, measured on small mint handlers: | Change | FLAT bytes | | --- | ---: | | Handler using `List.any` at `Int` | 267 | | Same, calling `List.any` twice at `Int` | 280 | | Same, plus `List.any` at `Bool` and `List.map` from `Int` to `Bool` | 404 | | A trivial handler | 114 | | The same handler in a script that also has a `withdraw` handler | 189 | A generic function is compiled once for each concrete type it is used at. Calling it again at the same type reuses that copy; using it at a new type adds a new copy. Library functions are linked once per script. Each `Debug.trace` message is stored in the script: a 19-character message added 26 bytes. ## Strict evaluation Elci is strict. Every `let` value is evaluated when the `let` is entered, even if the body never uses it. Only `if`, `case`, `&&` and `||` skip the branches they do not take. ```elm title="src/Main.elm" module Main exposing (main) main : Int main = let unused = 10 // 0 in 7 ``` ```text $ elci eval evaluation failed: QuotientInteger: division by zero cpu 276441, mem 1001 ``` The same rule has a cost side. Naming a check in a `let` evaluates it even when an earlier check already decided the result: ```elm main : Bool main = let manyItems = List.length (range 1 100) > 50 in amount > 1000 && manyItems ``` | Form | CPU | | --- | ---: | | `amount > 1000 && List.length (range 1 100) > 50` | 560,100 | | `List.length (range 1 100) > 50 && amount > 1000` | 165,862,550 | | `let manyItems = List.length (range 1 100) > 50 in amount > 1000 && manyItems` | 165,910,550 | Here `amount` is `5`, so the first check fails. Written inline and first, it saves the whole traversal. Put cheap, likely-to-fail checks first, and keep expensive checks inline or behind a function call rather than in a `let`. ## Top-level values run at script start A top-level value (a definition with no arguments) that a handler uses is computed once per script run, **before** the ledger purpose is dispatched. In a script with several handlers, every purpose pays for it. This validator has a spend handler that needs an expensive minimum fee and a mint handler that does not: ```elm title="src/Shop.elm" minimumFee : Int minimumFee = List.foldl (\value total -> value + total) 0 (range 1 50) spend : () -> () -> ScriptContext -> Bool spend _ _ ctx = ctx.txInfo.fee >= minimumFee mint : () -> ScriptContext -> Bool mint _ _ = True ``` ```text $ elci test PASS ShopTest.minting (cpu 81348535, mem 374470) PASS ShopTest.spending (cpu 84875573, mem 386950) 2 passed, 0 failed ``` The mint handler pays about 78 million CPU for a value it never reads. Make the value a function, so it is only computed where it is called: ```elm minimumFee : () -> Int minimumFee _ = List.foldl (\value total -> value + total) 0 (range 1 50) spend : () -> () -> ScriptContext -> Bool spend _ _ ctx = ctx.txInfo.fee >= minimumFee () ``` ```text $ elci test PASS ShopTest.minting (cpu 3288472, mem 14755) PASS ShopTest.spending (cpu 84923573, mem 387250) 2 passed, 0 failed ``` A top-level constant such as `limit = 1275` costs almost nothing, so this matters only for values that compute something. Zero-argument library values, such as `Map.empty`, are also evaluated at script start. ## Purpose dispatch A validator always checks the ledger purpose before it calls a handler, even when it has only one, so a purpose without a handler is rejected. The check costs roughly 1.1 million CPU and 35 bytes per script, and it is part of the 2.7 million CPU baseline above. Adding a second handler cost 75 bytes and 48,000 CPU in the measurements above. Only the selected handler's datum and redeemer are decoded. ## Lists and arrays A `List` is a linked list: reaching element `n` walks `n` cells, and `List.length` walks them all. An `Array` (from `import Array`) is a Plutus V3 array with constant-time `get` and `length`, built once from a list. This program reads nine elements of a 100-element list by walking it: ```elm title="src/Main.elm" module Main exposing (main) range : Int -> Int -> List Int range from to = if from > to then [] else from :: range (from + 1) to nth : Int -> List Int -> Int nth index items = case items of [] -> 0 first :: rest -> if index == 0 then first else nth (index - 1) rest main : Int main = let values = range 1 100 in List.foldl (\index total -> total + nth index values) 0 [ 10, 20, 30, 40, 50, 60, 70, 80, 90 ] ``` The array version converts once and reads in constant time: ```elm at : Int -> Array Int -> Int at index values = case Array.get index values of Just value -> value Nothing -> 0 main : Int main = let values = Array.fromList (range 1 100) in List.foldl (\index total -> total + at index values) 0 [ 10, 20, 30, 40, 50, 60, 70, 80, 90 ] ``` | Program (100 elements) | CPU | Memory | | --- | ---: | ---: | | Build the list with `range 1 100` | 78,436,213 | 350,865 | | Build it and take `List.length` | 165,658,713 | 779,865 | | Build it and read 9 elements by walking | 486,410,560 | 2,208,802 | | Build it, `Array.fromList`, read 9 elements with `Array.get` | 114,646,728 | 484,047 | After building the list, nine walking reads cost about 408 million CPU; converting to an array and reading cost about 36 million. The list version's 2.2 million memory units are already about a sixth of a transaction's memory limit. Use a `List` when you traverse once (`List.any`, `List.foldl`, `List.filter`) and an `Array` when you read many positions. An array has no Data encoding, so datums, redeemers and parameters stay lists; call `Array.fromList` inside the handler. ## Sorting and comparing `List.sort`, `List.sortBy` and `List.sortWith` are merge sorts written in Elci, O(n log n). Each comparison is a function call, so sorting is one of the more expensive things a script can do. Each row includes building its input: | Program (20 elements) | CPU | Memory | | --- | ---: | ---: | | `List.range 1 20` | 16,577,300 | 75,673 | | `List.sort (List.range 1 20)` | 133,140,808 | 644,693 | | `List.map String.fromInt (List.range 1 20)` | 61,921,144 | 261,394 | | `List.sort` of those strings | 213,536,764 | 959,078 | | The same 20 values as `Bytes` | 62,881,144 | 267,394 | | `List.sort` of those byte strings | 190,046,812 | 894,614 | Sorting cost about 117 million CPU for 20 integers, 152 million for 20 strings and 127 million for 20 byte strings. Checking that a list is already sorted is a single pass. When a transaction can supply sorted data, have the validator check the order instead of sorting: ```elm title="src/Main.elm" module Main exposing (main) isSorted : List comparable -> Bool isSorted items = case items of first :: second :: rest -> first <= second && isSorted (second :: rest) _ -> True main : Bool main = isSorted (List.range 1 20) ``` ```text True cpu 33372778, mem 162456 ``` That is about 17 million CPU for the check, against 117 million for the sort. The cost of one comparison depends on the type. `Int` and `Bytes` use one builtin call. `String` first encodes both sides to UTF-8, then compares the bytes. In a fold that compares the same two values 100 times, `Bytes` values cost about 10 million CPU more than `Int` values, and `String` values about 24 million more. Keys that are compared many times are cheaper as `Bytes`. Lists and tuples compare element by element: `compare [ 1, 2, 3 ] [ 1, 2, 4 ]` cost about 6 million CPU, against 0.5 million for `compare 3 7`. ## Maps and dictionaries A `Map k v` is the ledger's map representation: a list of key/value pairs, compared by their Data encoding, in no particular order. A `Dict k v` is the same list of pairs with its keys kept in ascending order. Neither hashes its keys or balances a tree, so their costs are those of a list. The figures below use `Int` keys and count the operation alone: the cost of a program that runs it, minus the cost of the same program without it. - **Lookups scan.** `Map.get` walks from the front until it finds the key. `Dict.get` walks from the smallest key and stops at the first key that isn't smaller, so a missing key costs half a scan on average. Both cost about 1.1 million CPU per entry passed: in a 50-entry map or dictionary, the first key cost about 2 million CPU and the last about 55 million. `member`, `insert` and `remove` walk the same way, and `insert` and `remove` also rebuild the pairs in front of the position. - **`size` scans too.** It cost about 9 million CPU at 10 entries and 44 million at 50. - **Building.** `Map.fromList` checks each key against the pairs before it, so it is O(n²). `Dict.fromList` sorts in O(n log n), and takes a single pass when the keys are already in order: | Building from a list of pairs | 10 entries | 50 entries | 100 entries | | --- | ---: | ---: | ---: | | `Map.fromList` | 82 million | 1.47 billion | 5.58 billion | | `Dict.fromList`, keys in order | 57 million | 0.28 billion | 0.55 billion | | `Dict.fromList`, keys shuffled | 111 million | 0.66 billion | 1.44 billion | The pairs were `( i, i )` for `i` from 1 to n; the shuffled keys were `modBy 101 (i * 17)` for 100 entries, with 53 and 11 for the smaller sizes. The 100-entry `Map.fromList` also used 21 million memory units, more than a whole transaction may use. - **Whole-collection operations are single passes.** `map`, `filter`, the folds and `toList`, and a dictionary's `union`, `intersect`, `diff` and `merge`, are O(n). Their cost per entry depends on the function you pass. - **Decoding a `Dict` checks its keys.** When a datum, redeemer, parameter or `Julc.Data.decode` result contains a `Dict`, the script checks that its keys ascend, at about 1.1 million CPU per entry. A `Map` is decoded without this check. Read maps that arrive in data, such as datum fields or the transaction's withdrawals and redeemers, rather than building large ones on-chain. Build keyed data of your own with `Dict.fromList`, with its keys in order when you can. Keys you look up often are cheapest near the front: the newest binding in a `Map`, the smallest keys in a `Dict`. ## Values A [`Value`](https://elci-lang.dev/reference/core-modules/#value) is stored as the ledger's Data map. `lovelaceOf` and `isZero` read the start of that map, so their cost is fixed. `==` compares two maps directly, and `positive`, `negative`, `restrictedTo` and `withoutLovelace` filter one entry by entry. The other functions convert the whole value to the protocol's native value (the CIP-153 builtins) and back. Every cost except the fixed ones grows with the number of assets. The figures below count the operation alone, on values with lovelace and one token under each of the other policies: | Operation | 3 assets | 10 assets | 30 assets | | --- | ---: | ---: | ---: | | `Value.isZero v` | 0.3 million | 0.3 million | 0.3 million | | `Value.lovelaceOf v` | 2.0 million | 2.0 million | 2.0 million | | `Value.quantityOf policy name v` | 3.5 million | 6.2 million | 13.9 million | | `v == w` | 2.8 million | 7.0 million | 19.0 million | | `Value.add v w` | 3.9 million | 12.1 million | 35.3 million | | `Value.geq v w` | 5.4 million | 15.2 million | 43.3 million | | `Value.positive v` | 12.7 million | 40.6 million | 120.4 million | `geq` subtracts, then checks every quantity of the difference, so it costs more again when the difference holds many tokens. `Value.sum` converts each value once: summing five small values cost about 20 million CPU, against 22 million for folding them with `Value.add`. Read ada with `lovelaceOf`, and prefer `quantityOf` or `==` to building a filtered value. ## Traces `Debug.trace` costs budget and bytes even when the script succeeds. One trace of a 19-character message added about 140,000 CPU and 26 bytes. Remove traces from validators you deploy, or keep them only on failure paths. ## Practical tips - Measure before and after a change: `elci test` for validators, `elci eval` for helper functions. - Order `&&` chains from cheap to expensive, and avoid naming expensive checks in a `let` you might not need. - Keep purpose-specific work inside its handler. Turn expensive top-level values into functions. - Traverse lists once. Over 100 elements, `List.filter` followed by `List.foldl` cost about 127 million CPU; one `List.foldl` with an `if` inside cost about 96 million. - Convert to an `Array` once when you read many positions; never rebuild an array to change it. - Avoid sorting on-chain. When the transaction can provide sorted data, check the order in one pass. - Build keyed data with `Dict.fromList` rather than `Map.fromList`, which is quadratic. Keep maps and dictionaries small: every lookup scans. - Read ada with `Value.lovelaceOf`, whose cost is fixed. Other `Value` operations grow with the number of assets in the value. - Use each generic helper at as few types as you can, and drop `Debug.trace` from deployed scripts. - Library functions are compiled Java code, not builtins: `Julc.ByteString.toHex` over 32 bytes cost about 73 million CPU. Check the cost of a library call before using it in a loop. For how strictness and specialization come from the compiler's design, see [how Elci compiles](https://elci-lang.dev/advanced/how-it-works/). --- # How Elci compiles > The path from .elm source to a Plutus V3 script — Elci's frontend stages, the boundary with Julc's backend, strict data boundaries and the native executable. Source: https://elci-lang.dev/advanced/how-it-works/ This page follows your source through the compiler, from `.elm` text to the bytes in `validator.plutus.json`. Knowing the stages helps you read error messages, predict costs and understand what Elci guarantees and what it does not. ## The pipeline Elci is a **frontend** over Julc's backend. It does not translate your code to Java, and it does not have its own UPLC optimizer. It turns source into typed Plutus IR (PIR), and Julc does the rest: ```text .elm sources + elci.toml | v lexer and layout-aware parser ELCI1xxx | v module and name resolution ELCI21xx | v type inference and checks ELCI20xx | v specialization and PIR lowering ELCI3xxx | ======= | ======= Julc backend boundary ========== v validator wrapper, PIR verifier ELCI4002, ELCI4003 | v UPLC optimization and FLAT encoding | v build/validators//validator.plutus.json, abi.json, ... ``` The diagnostic codes on the right show where each class of error comes from; see [diagnostics](https://elci-lang.dev/reference/diagnostics/). ## Frontend stages ### Lexing and parsing The lexer turns text into tokens and records the line and column of each one. It rejects what Elci does not support at the character level: tabs, `Float` literals such as `1.5`, character literals and unknown escapes. The parser is a hand-written, layout-aware recursive-descent parser. Like Elm, it uses indentation: top-level declarations start in column 1, `let` declarations align with each other, and `case` branches align with each other. Operators are parsed by precedence, as listed in the [language reference](https://elci-lang.dev/reference/language/). Features outside Elci's profile, such as extensible records or refutable patterns in function arguments, fail here with a message that names the feature. ### Modules and names Module resolution builds the module graph. Each module `A.B` must live at `A/B.elm` under one source directory, every module has one owner, and import cycles are rejected. The core modules (`Basics`, `List`, `Maybe`, `Result`, `Tuple`, `String`, `Map`, `Dict`, `Array`, `Value`, `Interval` and `Tx`) are ordinary Elci source, bundled with the compiler and compiled with your code. Every project and test module gets Elm's default imports of `Basics`, `List`, `Maybe`, `Result`, `String`, `Tuple` and `Debug`; the core modules themselves import what they need explicitly, as in Elm. `Bytes`, `Debug` and `Julc.Data` are compiler intrinsics, as are `not`, `modBy` and `remainderBy`, which `Basics` exposes. Modules listed in `[java-modules]` are resolved to Julc library classes. Every name is then qualified with the module that owns it. A name is visible only if its module exposes it and your module imports it. A module's own declarations shadow its imports, and two imports never silently shadow each other: a name both expose is an error where it is used. ### Type inference and checks Elci infers types with Hindley-Milner inference restricted to rank-1 types, then checks them against your signatures. Every top-level declaration needs a signature. The checker also: - groups recursive definitions and rejects eager value cycles (recursion must go through functions); - keeps recursive calls monomorphic, so polymorphic recursion is rejected; - allows limited local polymorphism: simple local functions and values may be used at several types; - checks that `==`, ordering and `++` are used only at types that support them, and that a signature names such requirements with Elm's `comparable` and `appendable` type variables; - accepts type variables that nothing determines, as Elm does: no value of such a type can exist, so any representation serves; - proves every `case` exhaustive and every branch reachable; - requires validator handlers and entrypoints to have concrete types that match their configuration. ### Specialization and lowering Plutus has no runtime type information, so generic code cannot run as-is. The lowering stage **specializes** every generic function reachable from your handlers: `List.map` used at `Int` and at `Bool` becomes two separate functions. Each concrete use of a generic Java method is compiled by Julc in the same way. A compiler limit of 256 specializations stops runaway growth. Lowering then produces typed PIR while keeping Elci's evaluation rules: - **Strict calls.** Arguments and captured variables are evaluated once, before use, from left to right. `let` values are evaluated in dependency order, with source order breaking ties, and an unused `let` value is still evaluated. - **Selective branches.** `if`, `case`, `&&` and `||` evaluate only the branch they take. - **Explicit representations.** Lists are Julc's native `List`, with each element encoded and decoded explicitly. Records, tuples and custom types become Plutus Data constructors. - **Ordering.** `Int` and byte strings compare with the Plutus builtins, and `String` compares its UTF-8 bytes. Lists and tuples compare with a function generated once per concrete type, like any other specialization. Nothing is erased to an untyped `Data` value behind your back: a type Elci cannot represent faithfully is a compile error. ## The Julc backend boundary Elci hands Julc one of two kinds of program. **Function programs.** An `[entrypoint]` function, run by `elci eval`, and each `elci test` program is a closed, fully specialized function. Library exports are inlined at each use. Julc checks that the term is closed and valid for the target, then compiles it without a validator wrapper. **Validator programs.** Each `[validators.]` table becomes one validator program with: - **imports:** every library function the handlers need, linked once per script; - **definitions:** the specialized top-level definitions reachable from any handler, with their PIR types; - **handlers:** one reference per configured purpose; - **parameters:** the deployment parameters, in the order listed in `parameters`; - **named types:** your records and custom types. Before compiling, Elci checks that the backend supports every capability the validator needs (`ELCI4003` otherwise). Julc then runs a structural **PIR verifier** over Elci's definitions and handlers, checking bindings, types and data-constructor agreement. A rejection is reported at the Elci declaration it names (`ELCI4002`). If you ever see one, it is a compiler bug worth reporting. Julc wraps the program in its shared multi-validator wrapper. The deployed script: 1. takes the deployment parameters, then **one** argument: the ledger's script context; 2. dispatches on the script purpose, rejecting purposes without a handler; 3. decodes the selected handler's datum and redeemer strictly; 4. calls the handler and turns `True` into success and `False` into a script failure. Elci never builds its own dispatcher. Its one addition is a small adapter for `spend-datum = "optional"`: Julc's optional datum and Elci's `Maybe` use different constructor tags, so the adapter re-tags the value. Finally, Julc's UPLC pipeline optimizes the program and encodes it as FLAT bytes, and the CLI writes the artifacts. ### Library imports Java on-chain libraries reach Elci through a **provider**. The provider parses each library's Java source (bundled with the compiler, or read from your `library-jars`), compiles the exports you use to closed PIR with their transitive dependencies, and describes their types. Generic methods are described as schemes and compiled once per concrete type. Elci never calls library code on the JVM. See [Java libraries](https://elci-lang.dev/advanced/java-libraries/). ## Strict data boundaries Everything that enters a validator from outside is `Data`, and Elci decodes it **strictly** before your code runs. The wrapper checks constructor tags, field counts and nested field kinds of the datum and redeemer, even the parts your handler ignores. Malformed data fails the transaction; it never becomes a `Nothing` or a default value. Only the selected handler's datum and redeemer are decoded, so each handler may use its own redeemer type. Deployment parameters are decoded once when they are applied to the script but, as with Julc's Java validators, not validated on-chain. `elci apply` therefore checks each parameter off-chain with the same strict decoder before applying it. See [parameterized validators](https://elci-lang.dev/contracts/parameters/). The encoding is deterministic: | Elci value | Plutus Data | | --- | --- | | `Int` | integer | | `Bytes`, ledger hashes | byte string | | `String` | byte string holding its UTF-8 encoding | | `Bool` | `False` is constructor 0, `True` is constructor 1, no fields | | `()` | constructor 0, no fields | | record | constructor 0, fields sorted by **name** | | tuple | constructor 0, fields in position order | | custom type | constructor tag = declaration order of the variant, fields in order | | `Maybe a` | `Nothing` is constructor 0, `Just x` is constructor 1 with one field | | `Result e a` | `Ok x` is constructor 0, `Err e` is constructor 1, each with one field | | `List a` | list | | `Map k v` | map | Each build writes these choices to `abi.json`. Pin that file with `expected-abi` and the build fails if a change to your types would change the on-chain format. The ledger's own context is decoded by Julc's existing ledger-context convention, not by a new general decoder. ## What the verifier does and does not prove Elci owns source typing, name hygiene, specialization and data representation. Julc's verifier checks that the PIR it receives is well-formed and well-typed; it is not a proof that your contract is correct. Your tests are. Compilation also has resource guards, such as the specialization limit, that are separate from Cardano's execution budget. ## The native executable The `elci` command is built twice from the same code: as a JVM application and as a GraalVM native executable. The native executable: - bundles the Elci core modules, Julc's standard-library sources and the JavaParser metadata needed to compile Java library source, so it needs no JDK at run time; - reads your library JARs as source files, exactly like the JVM build; - produces byte-identical artifacts. Building the `vesting` example with both gave the same file hashes; - starts fast: that build took about 0.05 seconds natively and about 0.5 seconds on the JVM. The compiler keeps no global mutable state between compilations, and builds are deterministic: building twice gives the same bytes. ## What Elci does not do - It does not run your code on the JVM to compute on-chain results. - It does not generate Java, and it has no second UPLC pipeline. - It does not fall back to untyped `Data` when a type is unsupported; it reports an error. - It does not produce CIP-57 blueprints yet; the ABI manifest is Elci's own format. To see how these stages show up in practice, read [costs and performance](https://elci-lang.dev/advanced/costs/) and the [language reference](https://elci-lang.dev/reference/language/). --- # CLI > Every elci command and option — init, check, build, eval, test, apply and explain — with outputs, exit codes and error formats. Source: https://elci-lang.dev/reference/cli/ This page documents every `elci` command and option, what each one prints and writes, and how it reports errors. The examples use the native executable; the JVM launcher accepts exactly the same arguments. ## Synopsis ```text $ elci --help Elci 0.1.0-dev — Elm-syntax compiler powered by Julc Usage: elci [project-directory] [--validator ]... elci eval [project-directory] [--builtins] elci test [project-directory] [--match ] [--seed ] [--fuzz ] elci apply [project-directory] --validator --param = [--output ] elci init [--validator ] [--purpose spend|mint|withdraw|certify|vote|propose|function] elci explain [code] elci --version check, build, eval and test take --report=json to print each compile error as a JSON object on its own line, and --color=auto|always|never for coloured errors (auto: on a terminal, unless NO_COLOR is set) A build writes build/validators//validator.flat, validator.plutus, validator.plutus.json (Julc format) and abi.json (Elci schema) for each [validators.] group; an [entrypoint] function project writes build/validator.flat, validator.plutus and abi.json. ``` `elci`, `elci help`, `elci --help` and `--help` after any command (such as `elci eval --help`) print this text and exit with status 0. ```text $ elci --version elci 0.1.0-dev / julc 0.1.0-pre20-ebaf028-SNAPSHOT ``` The second version is the Julc build the compiler is pinned to. A project's `julc-version` in [`elci.toml`](https://elci-lang.dev/reference/configuration/) must match it exactly. ## Common behavior **Project directory.** Every command except `init` and `explain` takes an optional project directory: the directory that holds `elci.toml`. It defaults to the current directory. Giving two directories is an error. **Two kinds of project.** A project configures either `[validators.]` tables or one `[entrypoint]` function. `check`, `build` and `test` work with both; `eval` needs an `[entrypoint]`; `apply` and `--validator` need validators. **Output streams.** Results go to standard output, including test results and a failed `eval` run. Compile, configuration and command-line errors go to standard error. **Exit status.** | Status | Meaning | | --- | --- | | `0` | The command succeeded. | | `1` | Any error: invalid arguments or configuration, a compile error, an `eval` that failed at run time, a failing test, or no tests found. | **Error format.** A compile error starts with a line that names the source location and the code. The source line follows, with carets under the reported code, and then any hints. The report ends with the command that explains the code: ```text my-project/src/Main.elm:6:5: ELCI2005: Unknown name 'lenght' 6| lenght [ 1, 2 ] ^^^^^^ Hint: did you mean `List.length`? For more on ELCI2005, run `elci explain ELCI2005`. ``` The first line always has the form `path:line:column: code: message`. A problem inside a bundled module such as `List` is reported where your code uses it, or otherwise as `/List.elm`, without an excerpt. For a validator, the `elci.toml` table follows the diagnostic, on its own line: ```text my-project/src/Main.elm:6:1: ELCI2014: Handler Main.spend does not have the spend signature datum -> redeemer -> ScriptContext -> Bool that [validators.main] needs (0 parameters); its type is () -> ScriptContext -> Bool; it has `Bool` where the configuration needs `ScriptContext -> Bool` 6| spend : () -> ScriptContext -> Bool ^^^^^ In my-project/elci.toml:6: [validators.main] For more on ELCI2014, run `elci explain ELCI2014`. ``` Configuration and command-line errors start with `elci:` and carry the code `ELCI4001`, apart from the few listed under [errors without a code](https://elci-lang.dev/reference/diagnostics/#errors-without-a-code): ```text elci: ELCI4001: unknown validator 'treasury'; configured: main ``` **Several errors.** A run reports every compile error it finds, in source order and separated by blank lines, followed by a count such as `3 errors`. It shows at most 20, then a line such as `... and 3 more`. Each top-level declaration reports its first error, and type errors are reported only when there are no syntax, import or qualified-name errors. Code generation, configuration and command-line errors stop the run at the first. The [diagnostics](https://elci-lang.dev/reference/diagnostics/#several-errors-in-one-run) page explains how to read them and lists every code. **Colour.** On a terminal, compile errors are in colour, unless the `NO_COLOR` environment variable is set. Every command takes `--color=auto`, `--color=always` or `--color=never` to choose; `auto` is the default. Any other value is an `ELCI4001` error. JSON reports are never coloured. **JSON reports.** `check`, `build`, `eval` and `test` take `--report=json`. With it, each error is printed on standard error as one JSON object on its own line (JSON Lines), instead of the text above. A run with one error prints one line; a run with several prints one line for each. Here one is formatted with `jq`: ```text $ elci check my-project --report=json 2>&1 | jq { "code": "ELCI2005", "path": "my-project/src/Main.elm", "line": 6, "column": 5, "message": "Unknown name 'lenght'", "hints": [ "did you mean `List.length`?" ], "excerpt": "6| lenght [ 1, 2 ]\n ^^^^^^", "context": null } ``` | Field | Contents | | --- | --- | | `code` | The `ELCI` code, or null for an error without one | | `path`, `line`, `column` | The location, or null for an error without one | | `message` | The message, without the path and code | | `hints` | The hints, as an array of strings; empty when there are none | | `excerpt` | The source lines and their caret lines, separated by newlines, or null | | `context` | The `elci.toml` line and table of a failing validator, such as `my-project/elci.toml:6: [validators.main]`, or null | The JSON report has no count line and no `elci explain` line. A configuration or command-line error keeps the same fields, with null location fields: ```text {"code":"ELCI4001","path":null,"line":null,"column":null,"message":"unknown validator 'treasury'; configured: main","hints":[],"excerpt":null,"context":null} ``` The exit status doesn't change. Results stay text on standard output: the `check` and `build` lines, the value, budget or `evaluation failed:` line of `eval`, and test results, including failing tests. `init`, `apply` and `explain` reject the option: ```text $ elci apply --report=json {"code":"ELCI4001","path":null,"line":null,"column":null,"message":"--report=json applies to check, build, eval and test","hints":[],"excerpt":null,"context":null} ``` ## `elci init` Creates a new project in an empty or missing directory. ```text elci init [--validator ] [--purpose ] ``` | Option | Default | Meaning | | --- | --- | --- | | `--purpose` | `spend` | The first handler's purpose: `spend`, `mint`, `withdraw`, `certify`, `vote` or `propose`. `function` creates an `[entrypoint]` project for `elci eval` instead. | | `--validator` | `main` | The validator's name: lowercase letters, digits, `-` or `_`, starting with a letter. Not allowed with `--purpose function`. | ```text $ elci init my-project Created Elci project at /path/to/my-project with initial spend handler in validator main. Run: elci check /path/to/my-project ``` A validator project gets `elci.toml`, `src/Main.elm`, `tests/MainTest.elm`, `README.md` and a `.gitignore` that ignores `build/`. The starter handler accepts transactions signed by an `owner` deployment parameter, and the two tests check that the owner passes and anyone else is rejected. A `--purpose function` project gets `elci.toml`, `src/Main.elm` (returning `42`), `README.md` and `.gitignore`, and no tests. Errors are `ELCI4001`, such as `ELCI4001: Project directory is not empty: ...`, `ELCI4001: Unsupported purpose: lend; expected spend, mint, withdraw, certify, vote, propose or function` and `ELCI4001: Invalid validator name '9bad'; use lowercase letters, digits, '-' or '_', starting with a letter`. ## `elci check` Compiles the project and reports each result without writing files. ```text elci check [project-directory] [--validator ]... [--report=json] ``` | Option | Meaning | | --- | --- | | `--validator ` | Check only this validator. Repeat it to select several. Validator projects only. | | `--report=json` | Print each error as one JSON object on its own line on standard error (see [JSON reports](#common-behavior)). | For a validator project, it prints one line per validator, sorted by name, with the purposes it handles and its size in FLAT bytes: ```text $ elci check validator badge (mint): checked; 198 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 validator treasury (spend, withdraw): checked; 540 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` For an `[entrypoint]` project it prints the function instead: ```text $ elci check Main.main: checked; 6 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` `check` compiles the source directories only; test modules are compiled by `elci test`. When `expected-abi` is set, `check` also compares the ABI and fails on drift, exactly like `build`. ## `elci build` Compiles the project and writes its artifacts under `build/`. It prints the same lines as `check`. ```text elci build [project-directory] [--validator ]... [--report=json] ``` It takes the same options as `check`. For each validator it writes `build/validators//`: | File | Contents | | --- | --- | | `validator.flat` | the script's FLAT bytes | | `validator.plutus` | a `PlutusScriptV3` text envelope with the CBOR hex | | `validator.plutus.json` | Julc's artifact: type, purpose, CBOR hex, script hash, parameters and `sizeBytes` | | `abi.json` | Elci's ABI manifest: handlers, datum and redeemer schemas, parameters | `build/validators/elci-outputs.json` records which files Elci owns. The `hash` in `validator.plutus.json` is empty for a validator with parameters, because applying parameters changes the hash. Its `purpose` is `multi` when the validator has several handlers. Publishing is all-or-nothing: every selected validator is compiled and ABI-checked before any file is written, and files are replaced atomically. A build of all validators also removes the output directories of validators you deleted from `elci.toml`. A build with `--validator` touches only the selected validators. Rebuilding a validator removes the `applied.plutus.json` that `elci apply` left in its directory, since it belonged to the previous script. An `[entrypoint]` project writes `build/validator.flat`, `build/validator.plutus` and `build/abi.json`. See [build and deploy](https://elci-lang.dev/contracts/build-and-deploy/) for using these files. ## `elci eval` Runs an `[entrypoint]` project's function on Julc's evaluator and prints the result. ```text elci eval [project-directory] [--builtins] [--report=json] ``` | Option | Meaning | | --- | --- | | `--builtins` | Also print the builtin calls of the run, one per line, with arguments and result. The log keeps the last 20 calls. | | `--report=json` | Print each compile error, or a configuration error, as one JSON object on its own line. A failed evaluation still prints `evaluation failed:` as text. | The first line is the value, written in Elci syntax. The second is the CPU and memory consumed. Each `Debug.trace` message follows on its own line: ```elm title="src/Main.elm" module Main exposing (main) type alias Quote = { price : Int, approved : Bool } main : Quote main = Debug.trace "pricing" { price = 25 * 1000000, approved = True } ``` ```text $ elci eval --builtins { approved = True, price = 25000000 } cpu 1026458, mem 4557 trace: pricing builtin: IfThenElse(True, , ) → builtin: MkNilData(()) → [0 elems] builtin: ConstrData(1, [0 elems]) → builtin: IData(25000000) → builtin: MkNilData(()) → [0 elems] builtin: MkCons(, [0 elems]) → [1 elems] builtin: MkCons(, [1 elems]) → [2 elems] builtin: ConstrData(0, [2 elems]) → builtin: Trace("pricing", ) → ``` Values print as you would write them: `True`, `"text"`, `[1, -2]`, `Just 5`, `( 1, "a" )`, records with their fields in name order, byte strings as `#474f4c44` (and `#(empty)`), maps as `Map.fromList [...]`, dictionaries as `Dict.fromList [...]`, arrays as `Array.fromList [...]` and values as `Value.fromList [( #policy, #token, quantity )]`, where ada's policy and token name are `#(empty)`. `eval` passes no arguments, so an entrypoint of function type prints ``. If evaluation fails, it prints `evaluation failed:` with the reason, then the budget, and exits with status 1: ```text $ elci eval evaluation failed: QuotientInteger: division by zero cpu 276441, mem 1001 ``` `eval` rejects validator projects and the `--validator` option; test validators with `elci test`. ## `elci test` Runs the tests in the project's test directories (`tests/` by default) and reports each with its budget. ```text elci test [project-directory] [--match ] [--seed ] [--fuzz ] [--report=json] ``` | Option | Default | Meaning | | --- | --- | --- | | `--match ` | all tests | Run tests whose full name (`Module.test`) contains the substring, ignoring case. | | `--seed ` | random | Seed for property tests. The seed used is printed, so any run can be repeated. | | `--fuzz ` | `100` | Number of runs for each property test. Must be positive. | | `--report=json` | text | Print each compile error, such as one in a test module, or a configuration error as one JSON object on its own line. Test results stay text. | ```text $ elci test --match deadline --seed 42 PASS VestingTest.claimsAfterTheDeadline (cpu 16763911, mem 61637) PASS VestingTest.claimsAtTheDeadlineFail (cpu 13950292, mem 52228) PASS VestingTest.claimsSucceedExactlyAfterTheDeadline (100 runs, max cpu 16763911, mem 61637) 3 passed, 0 failed (seed 42) ``` A failing test prints its details and traces, indented, and the command exits with status 1: ```text $ elci test FAIL FeeTest.feeIsRounded (cpu 2130706, mem 8398) Expect.equal expected: 30 actual: 31 trace: computing fee PASS FeeTest.halves (cpu 424774, mem 1602) PASS MainTest.ownerSigns (cpu 7675163, mem 29990) PASS MainTest.othersAreRejected (cpu 8053596, mem 31922) 3 passed, 1 failed ``` The `(seed ...)` suffix appears only when a property test ran. If nothing runs, `elci test` prints `No tests found in tests` or `No tests match ''` and exits with status 1, so it is safe as a CI gate. What counts as a test is described in [unit tests](https://elci-lang.dev/testing/unit-tests/). ## `elci apply` Applies deployment parameters to a validator and writes the applied script. ```text elci apply [project-directory] --validator --param = [--param ...] [--output ] ``` | Option | Meaning | | --- | --- | | `--validator ` | Required. Exactly one validator. | | `--param =` | Required once for every name in the validator's `parameters`, and only for those. | | `--output ` | Where to write the applied script. Default: `build/validators//applied.plutus.json`. | Each value is Plutus Data in the detailed JSON schema that cardano-cli uses: | JSON | Data | | --- | --- | | `{"int": 5}` | integer | | `{"bytes": "a2c2..."}` | byte string, in hex | | `{"list": [ ... ]}` | list | | `{"map": [{"k": ..., "v": ...}]}` | map | | `{"constructor": 0, "fields": [ ... ]}` | constructor | Write `@path` instead of JSON to read the value from a file. That path is relative to the project directory, while `--output` is relative to the current directory. JSON strings may not contain escapes. ```text $ elci apply --validator main --param 'owner={"bytes": "a2c20c77887ace1cd986193e4e75babd8993cfd56995cd5cfce609c2"}' validator main: applied 1 parameter; hash f050dee3352fb4863e471b360e9d9a1121d81e6705580d1cbc3b831f; wrote /path/to/my-project/build/validators/main/applied.plutus.json ``` `apply` compiles the validator, checks its ABI when `expected-abi` is set, and checks every value off-chain with the strict decoder for the parameter's type, because the script itself does not validate parameters. A value of the wrong shape is rejected before anything is written: ```text $ elci apply --validator main --param 'owner={"int": 5}' elci: ELCI4001: parameter owner is not a well-formed org.julclang.ledger.PubKeyHash value ``` Other errors name the problem: `missing --param owner=`, `parameter owner is given more than once`, `validator main has no parameter 'nope'; declared: owner`, and `validator main declares no parameters`. See [parameterized validators](https://elci-lang.dev/contracts/parameters/). ## `elci explain` Prints the long-form entry for a diagnostic code. ```text elci explain [code] ``` The code may be written `ELCI2002`, `elci2002` or `2002`. An entry says what the code means and its common causes, shows a failing example with its exact output, and gives the fix and the pages to read. Code blocks are indented by four spaces. Without a code, `explain` lists every code with its title: ```text $ elci explain Run elci explain for one of these: ELCI1001: Invalid character or literal ELCI1002: Malformed or unsupported syntax ELCI2001: Invalid expression or pattern ELCI2002: Type mismatch ``` The list goes on with every other code. An unknown code, or more than one, is an error: ```text $ elci explain ELCI9999 elci: ELCI4001: ELCI9999 is not an Elci error code; run elci explain to list them ``` The entries are part of the compiler, so they match its version. The [diagnostics](https://elci-lang.dev/reference/diagnostics/#elci-explain) page shows a whole entry. --- # elci.toml > Every table and key of elci.toml — project settings, validators, entrypoints, Java modules and tests — with the rules the parser enforces and complete examples. Source: https://elci-lang.dev/reference/configuration/ Every Elci project has an `elci.toml` file at its root. This page lists every table and key, the values each one accepts and the rules the compiler enforces. Unknown tables and keys are errors, so a typo never goes unnoticed. ## Format `elci.toml` uses a small, strict subset of TOML: - A table header is a line like `[project]` or `[validators.treasury]`. Each table may appear once. - Each setting is `key = value` on **one line**. A key may be quoted, as in `"Julc.Math" = "..."`. - A value is either a quoted string, such as `"src"`, or an array of quoted strings on the same line, such as `["src", "lib"]`. - Strings cannot contain escapes (`\`) or embedded quotes. - Comments are whole lines starting with `#`. A comment after a value is an error. - A key may appear only once per table. ## Project kinds A project configures **either** one or more `[validators.]` tables **or** one `[entrypoint]` function, never both: - **Validator projects** produce deployable scripts. Use `elci check`, `build`, `test` and `apply`. - **Function projects** have an `[entrypoint]` for `elci eval`, which runs a function and prints its value and cost. They are for experiments and helper code, not for deployment. ## `[project]` Required in every project. | Key | Required | Value | | --- | --- | --- | | `source-directories` | yes | Array of directories holding source modules, relative to the project. At least one; each must exist. | | `target` | yes | Must be `"plutus-v3-pv11-uplc-1.1.0"`, the only supported target. | | `julc-version` | yes | Must equal the Julc version the compiler is pinned to, currently `"0.1.0-pre20-ebaf028-SNAPSHOT"`. `elci --version` prints it. | | `library-jars` | no | Array of JAR paths, relative to the project, that contain on-chain Java library sources. See [Java libraries](https://elci-lang.dev/advanced/java-libraries/). | | `expected-abi` | no | Path of a committed `abi.json` to compare with the build. **Function projects only**; validators set it in their own table. | A module `A.B` must live at `A/B.elm` inside exactly one source directory. Two files defining the same module are an error. ## `[validators.]` One table per deployed script. The name becomes the output directory `build/validators//` and the name you use in `elci apply --validator`, in tests and in `--validator` options. It must be 1 to 64 characters: lowercase letters, digits, `-` or `_`, starting with a letter. | Key | Required | Value | | --- | --- | --- | | `boundary` | yes | Must be `"julc-strict-v1"`, the strict data boundary. | | `spend` | at least one handler | Handler for spending, as `"Module.function"`. | | `mint` | | Handler for minting. | | `withdraw` | | Handler for reward withdrawals. | | `certify` | | Handler for certificates. | | `vote` | | Handler for governance votes. | | `propose` | | Handler for governance proposals. | | `parameters` | no | Array of deployment parameter names, in application order. | | `spend-datum` | no | `"required"` (default), `"optional"` or `"absent"`. Needs a `spend` handler. | | `expected-abi` | no | Path of a committed `abi.json`; `check`, `build` and `apply` fail if the new ABI differs. | Rules: - **Handlers** name an exposed function as `"Module.function"`, for example `"Treasury.spend"`. Each handler key adds a purpose to the **same** script, so one table is always one script hash. - **Parameter names** start with a lowercase letter, followed by letters, digits or `_`, and must be unique. Every handler receives all parameters as its leading arguments, in this order, so their types must agree across handlers. - **Handler signatures** depend on the purpose and `spend-datum`: | Handler | Required type | | --- | --- | | `spend` | `parameters -> datum -> redeemer -> ScriptContext -> Bool` | | `spend` with `spend-datum = "optional"` | `parameters -> Maybe datum -> redeemer -> ScriptContext -> Bool` | | `spend` with `spend-datum = "absent"` | `parameters -> redeemer -> ScriptContext -> Bool` | | `mint`, `withdraw`, `certify`, `vote`, `propose` | `parameters -> redeemer -> ScriptContext -> Bool` | Adding, removing or changing a handler changes the script and its hash. See [validators and purposes](https://elci-lang.dev/contracts/validators/) and [datums and redeemers](https://elci-lang.dev/contracts/datums-and-redeemers/). ## `[entrypoint]` The single function of a function project. | Key | Required | Value | | --- | --- | --- | | `module` | yes | The module name, such as `"Main"`. | | `function` | yes | An exposed top-level function or value with a concrete type, such as `"main"`. | | `purpose` | no | Only `"function"`, the default. | | `boundary` | no | Only `"julc-strict-v1"`. | Validator purposes in `[entrypoint]` were removed. `purpose = "spend"` fails with a message telling you to move the handler into a `[validators.]` table. ## `[java-modules]` Maps Elm-style module names to on-chain Java library classes: ```toml [java-modules] "Julc.Math" = "org.julclang.stdlib.lib.MathLib" "Acme.Pricing" = "acme.Pricing" ``` The key is the module name you import; the value is the fully qualified class name. A mapping is only read when a module imports it. A module name cannot be both a source module and a Java module. `Julc.TypedMap` and `Julc.TypedArray` are reserved by the core `Map` and `Array` modules and cannot be remapped. The core module names `Basics`, `List`, `Maybe`, `Result`, `String`, `Tuple`, `Map`, `Dict`, `Array`, `Value`, `Interval` and `Tx`, the reserved names `Set` and `Elci`, and names starting with `Elci.` cannot be keys. See [standard library](https://elci-lang.dev/advanced/standard-library/) for the bundled classes. ## `[tests]` | Key | Required | Value | | --- | --- | --- | | `source-directories` | no | Array of directories holding test modules. Default `["tests"]`. | A missing default `tests` directory simply means there are no tests. Test modules are never part of `check`, `build` or the deployed scripts. See [unit tests](https://elci-lang.dev/testing/unit-tests/). ## Complete examples ### A validator project This project uses every validator setting: two scripts, a shared parameter, an optional datum, a pinned ABI, a Java library JAR, a second source directory and a custom test directory. ```toml title="elci.toml" # Settings for the whole project. [project] source-directories = ["src", "lib"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" library-jars = ["libs/pricing.jar"] # One script with two purposes and a deployment parameter. [validators.treasury] boundary = "julc-strict-v1" parameters = ["admin"] spend = "Treasury.spend" withdraw = "Treasury.withdraw" spend-datum = "optional" expected-abi = "abi/treasury.json" # A second, independent script. [validators.badge] boundary = "julc-strict-v1" parameters = ["admin"] mint = "Badge.mint" [java-modules] "Julc.Math" = "org.julclang.stdlib.lib.MathLib" "Acme.Pricing" = "acme.Pricing" [tests] source-directories = ["spec"] ``` ```text $ elci check validator badge (mint): checked; 229 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 validator treasury (spend, withdraw): checked; 603 FLAT bytes; plutus-v3-pv11-uplc-1.1.0 ``` With `spend-datum = "optional"`, the spend handler's datum is a `Maybe`: ```elm title="src/Treasury.elm" spend : PubKeyHash -> Maybe Int -> () -> ScriptContext -> Bool spend admin datum _ ctx = case datum of Just limit -> ctx.txInfo.fee <= Math.max limit (Pricing.fee limit 100) Nothing -> signedBy admin ctx ``` ### A function project This is what `elci init my-project --purpose function` creates: ```toml title="elci.toml" [project] source-directories = ["src"] target = "plutus-v3-pv11-uplc-1.1.0" julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT" [entrypoint] module = "Main" function = "main" purpose = "function" ``` ## Common errors | Mistake | Message | | --- | --- | | Wrong `julc-version` | `ELCI4001: Julc version does not match this compiler; expected julc-version = "0.1.0-pre20-ebaf028-SNAPSHOT"` | | Wrong `target` | `ELCI4001: Unsupported target; expected plutus-v3-pv11-uplc-1.1.0` | | Misspelled key | `ELCI4001: Unknown configuration key: [project] name` | | Unknown table | `ELCI4001: elci.toml:6: unknown configuration section: dependencies` | | Missing `boundary` | `ELCI4001: Missing configuration key: [validators.main] boundary` | | Bad validator name | `ELCI4001: elci.toml:6: invalid validator name 'Main'; use lowercase letters, digits, '-' or '_', starting with a letter (at most 64 characters)` | | No handler | `ELCI4001: elci.toml:6: [validators.main] configures no handler; add one of mint, spend, withdraw, certify, vote, propose` | | Handler without module | `ELCI4001: elci.toml:9: [validators.main] spend = "spend" must name an exposed function as "Module.function"` | | `spend-datum` without `spend` | `ELCI4001: elci.toml:6: [validators.main] spend-datum requires a spend handler` | | Both project kinds | `ELCI4001: elci.toml mixes [entrypoint] with [validators.*]; use one form` | | Neither project kind | `ELCI4001: elci.toml must configure at least one [validators.], or an [entrypoint] function` | | `expected-abi` in `[project]` of a validator project | `ELCI4001: [project] expected-abi applies to [entrypoint] projects; set expected-abi in each [validators.] table` | | Unquoted value | `ELCI4001: Expected a quoted string without escapes: julc-strict-v1` | | Comment after a value | `ELCI4001: Expected a quoted string without escapes: "0.1.0-pre20-ebaf028-SNAPSHOT" # pinned` | | Multi-line array | `ELCI4001: elci.toml:3: expected key = value` | | Missing source directory | `ELCI4001: Source directory not found: my-project/lib` | | Remapping a core library | `ELCI4001: my-project/elci.toml:6: [validators.main]: [java-modules] may not remap Julc.TypedMap, which the Elci core library uses for org.julclang.stdlib.lib.TypedMapLib` | | Mapping a core module name | `ELCI4001: my-project/elci.toml:6: [validators.main]: [java-modules] may not map Value, a module name of Elci's core library` | | ABI changed | `ELCI4001: my-project/elci.toml:9: [validators.main]: ABI drift from abi/main.json; compare constructor tags, field order, handler roles and parameters` | Each message is printed after `elci: ` and the command exits with status 1. --- # Core modules > Exact signatures and semantics of Elci's core library, imported by default as in Elm, and of the Dict, Map, Array, Value, Interval, Tx, Bytes, Julc.Data and Debug modules. Source: https://elci-lang.dev/reference/core-modules/ This page lists every function the compiler provides without configuration: the `Basics` prelude, the `List`, `Maybe`, `Result`, `Tuple` and `String` modules that every module imports, and the `Dict`, `Map`, `Array`, `Value`, `Interval`, `Tx`, `Bytes`, `Julc.Data` and `Debug` modules. Signatures are exact; the notes describe behavior, failure cases and cost. Names, argument orders and edge cases follow Elm 0.19's core library; the few differences are listed [at the end](#differences-from-elm). ## Default imports As in Elm, every source and test module starts with these imports, which you don't write: ```elm import Basics exposing (..) import List import Maybe exposing (Maybe(..)) import Result exposing (Result(..)) import String import Tuple import Debug ``` So `max`, `compare`, `Just` and `Ok` work unqualified, and `List.map`, `String.fromInt` and `Debug.trace` work qualified. Writing one of these imports yourself is allowed; an `exposing` list then adds names, as in `import List exposing (head)`. `Bytes`, `Dict`, `Map`, `Array`, `Value`, `Interval`, `Tx` and `Julc.Data` need an explicit import, as `Dict` and `Array` do in Elm. Using one without it names the import to add: ```text src/Main.elm:6:5: ELCI2104: Bytes.fromString needs the Bytes module; add `import Bytes` ``` **Name precedence** follows Elm, with one addition: - A module's own declarations shadow every import. If your module defines `max`, it uses its own; `Basics.max` still reaches the prelude's. - Two imports that expose different things under one name make that name ambiguous. The error is reported where you use the name, and lists the qualified alternatives. A default import counts, so an `import Utils exposing (..)` whose `max` differs from `Basics.max` gives: ```text src/Main.elm:8:5: ELCI2102: max is ambiguous: it could be Basics.max or Utils.max. Write the one you mean, like Basics.max, or stop exposing the other ``` - *Addition:* a name you list explicitly, as in `import Utils exposing (max)`, takes precedence over a default import. - Qualified names always work: `Basics.max`, `Maybe.withDefault`, `Result.Ok`. - An import can't use a default module's name as its alias. `import Utils as String` gives `ELCI2104: The alias String names a core module that every module imports; choose another alias for Utils`. - Nor can it use the name of any other core module or of a module in the project, since the two modules' names would merge. `import Julc.Interval as Interval` gives `ELCI2104: The alias Interval names Elci's core module Interval; choose another alias for Julc.Interval`. The core modules are compiled with your code. A function you don't use is never compiled into the script, so the default imports cost nothing. ### Built-in types | Type | Values | | --- | --- | | `Int` | arbitrary-precision integers | | `Bool` | `True`, `False` | | `String` | UTF-8 text; `++` appends; functions in [`String`](#string) | | `()` | the unit value `()` | | `Bytes` | byte strings; functions in [`Bytes`](#bytes) | | `Data` | Plutus Data; `==` compares structure; convert with [`Julc.Data`](#julcdata) | | `List a` | linked lists; functions in [`List`](#list) | | `Maybe a` | `Nothing`, `Just a`; functions in [`Maybe`](#maybe) | | `Result error value` | `Ok value`, `Err error`; functions in [`Result`](#result) | | `Order` | `LT`, `EQ`, `GT`, the result of `compare` | | `Dict k v` | dictionaries with comparable keys, kept sorted; functions in [`Dict`](#dict) | | `Map k v` | the ledger's association maps; functions in [`Map`](#map) | | `Array a` | constant-time indexed arrays; functions in [`Array`](#array) | | `Value` | the ledger's multi-asset values: lovelace and native tokens; functions in [`Value`](#value) | | `JulcOptional a` | Julc's optional, returned by some Java library functions | Tuples of two or three elements, closed records and your own custom types complete the picture; see the [language reference](https://elci-lang.dev/reference/language/). `Dict`, `Map` and `Array` are type names you can use without importing their modules. You may also write them as in Elm, with `import Dict exposing (Dict)` or qualified as `Dict.Dict`, and likewise for `Map` and `Array`. The compiler's own modules qualify their types the same way: `Bytes.Bytes` after `import Bytes`, and `Data.Data` after `import Julc.Data as Data`. `Value` is not an Elm type, so its name needs an import: `import Value exposing (Value)`, or `Value.Value` after `import Value`. Reading `out.value` needs no import. The same holds for `Interval`: name it after `import Interval exposing (Interval)`, but read `ctx.txInfo.validRange` without one. `Maybe`, `Just` and `Nothing` are built into the compiler. A module can't declare constructors named `Just`, `Nothing`, `True` or `False` (`ELCI2011: Nothing is a constructor of the built-in Maybe type; choose another name`), or types named `Int`, `Bool`, `String`, `Bytes`, `Data`, `List`, `Maybe`, `Dict`, `Map`, `Array` or `Float` (`ELCI2011: Bytes is a built-in type; choose another name`). ## Comparable types `<`, `<=`, `>`, `>=`, `compare`, `min`, `max`, `List.sort`, `List.sortBy`, `List.maximum` and `List.minimum` work on **comparable** types. Both operands have the same type. | Type | Order | | --- | --- | | `Int` | numeric | | `String` | by its UTF-8 bytes, which is Unicode code point order: `"Zebra" < "apple"` | | `Bytes` | lexicographic by unsigned byte; a proper prefix comes first | | ledger byte strings: `PubKeyHash`, `ScriptHash`, `PolicyId`, `TokenName`, `TxId`, `DatumHash` and the other hashes | as `Bytes` | | lists of a comparable type | lexicographic; a proper prefix comes first | | tuples of comparable types | component by component | `Bool`, `()`, records, custom types, `Maybe`, `Result`, `Dict`, `Map`, `Array`, `Value`, `Data`, functions and ledger records such as `TxOutRef` are not comparable. Sort them by a comparable key with `List.sortBy`, or with your own order and `List.sortWith`. Every order is total and agrees with `==`: `compare x y == EQ` exactly when `x == y`. The arithmetic operators remain `Int`-only. The error names the operation and the part of the type that isn't comparable: ```text src/Main.elm:6:17: ELCI2003: The operator < needs comparable values, but Bool, in ( Int, Bool ), is not comparable. Comparable types are Int, String, Bytes, byte-string ledger types such as PubKeyHash, and lists and tuples of comparable types ``` A generic function that compares its arguments names its type variable `comparable`, as in Elm; see [constrained type variables](https://elci-lang.dev/reference/language/#constrained-type-variables). The keys of a [`Dict`](#dict) are comparable too. ## `Basics` The prelude. Every module imports it with `exposing (..)`, so these names need no qualifier. | Function | Type | Behavior | | --- | --- | --- | | `negate` | `Int -> Int` | `negate 42` is `-42` | | `abs` | `Int -> Int` | `abs -4` is `4` | | `min` | `comparable -> comparable -> comparable` | the smaller: `min 3 7` is `3` | | `max` | `comparable -> comparable -> comparable` | the larger: `max "pear" "apple"` is `"pear"` | | `clamp` | `Int -> Int -> Int -> Int` | `clamp low high x`: `clamp 0 100 150` is `100` | | `compare` | `comparable -> comparable -> Order` | `compare 3 7` is `LT` | | `identity` | `a -> a` | returns its argument | | `always` | `a -> b -> a` | `always x` ignores its argument: `List.map (always 0) [ 1, 2 ]` is `[ 0, 0 ]` | | `xor` | `Bool -> Bool -> Bool` | `True` when exactly one argument is `True` | | `not` | `Bool -> Bool` | Boolean negation | | `modBy` | `Int -> Int -> Int` | `modBy divisor value`: the result has the divisor's sign | | `remainderBy` | `Int -> Int -> Int` | `remainderBy divisor value`: the result has the value's sign | `type Order = LT | EQ | GT` is also part of `Basics`. The operator `^` raises an `Int` to a power: `2 ^ 10` is `1024`. It binds tighter than `*` and groups to the right, so `2 * 3 ^ 2` is `18` and `2 ^ 3 ^ 2` is `512`. A negative exponent fails the script, because there is no `Float` for `2 ^ -1`. | Expression | Result | | --- | ---: | | `modBy 3 7` | `1` | | `modBy 3 -7` | `2` | | `modBy -3 7` | `-2` | | `remainderBy 3 7` | `1` | | `remainderBy 3 -7` | `-1` | | `remainderBy -3 7` | `1` | | `-7 // 2` | `-3` | A zero divisor fails the script. `//` truncates towards zero. There is no `/`, `Float`, `toFloat`, `round` or other floating-point function. ```elm title="src/Main.elm" module Main exposing (main) main : ( List Int, ( String, String ), List Order ) main = ( [ negate 42, abs -4, min 3 7, clamp 0 100 150, 2 ^ 10, always 1 2 ] , ( max "pear" "apple", min "Zebra" "apple" ) , [ compare 3 7, compare [ 1, 2 ] [ 1, 2 ], compare ( 2, "a" ) ( 1, "z" ) ] ) ``` ```text $ elci eval ( [-42, 4, 3, 100, 1024, 1], ( "pear", "Zebra" ), [LT, EQ, GT] ) cpu 23660515, mem 86721 ``` ## `List` Every module imports `List`; call its functions as `List.map` and so on, or expose them with `import List exposing (foldl)`. The module is ordinary Elci source, compiled with your code. | Function | Type | Behavior | | --- | --- | --- | | `singleton` | `a -> List a` | a list of one element | | `repeat` | `Int -> a -> List a` | `n` copies; empty when `n <= 0` | | `range` | `Int -> Int -> List Int` | `range 1 4` is `[ 1, 2, 3, 4 ]`; empty when the first is larger | | `map` | `(a -> b) -> List a -> List b` | | | `indexedMap` | `(Int -> a -> b) -> List a -> List b` | the index counts from 0 | | `filter` | `(a -> Bool) -> List a -> List a` | | | `filterMap` | `(a -> Maybe b) -> List a -> List b` | keeps the `Just` results | | `foldl` | `(a -> b -> b) -> b -> List a -> b` | from the first element | | `foldr` | `(a -> b -> b) -> b -> List a -> b` | from the last element | | `concat` | `List (List a) -> List a` | | | `concatMap` | `(a -> List b) -> List a -> List b` | | | `intersperse` | `a -> List a -> List a` | `intersperse 0 [ 1, 2, 3 ]` is `[ 1, 0, 2, 0, 3 ]` | | `map2` | `(a -> b -> result) -> List a -> List b -> List result` | stops at the shorter list | | `map3` | `(a -> b -> c -> result) -> List a -> List b -> List c -> List result` | stops at the shortest list | | `length` | `List a -> Int` | walks the whole list | | `reverse` | `List a -> List a` | | | `member` | `a -> List a -> Bool` | uses `==` | | `all` | `(a -> Bool) -> List a -> Bool` | `True` for `[]` | | `any` | `(a -> Bool) -> List a -> Bool` | `False` for `[]` | | `maximum` | `List comparable -> Maybe comparable` | `Nothing` for `[]` | | `minimum` | `List comparable -> Maybe comparable` | `Nothing` for `[]` | | `sum` | `List Int -> Int` | `0` for `[]` | | `product` | `List Int -> Int` | `1` for `[]` | | `append` | `List a -> List a -> List a` | `append a b` is `a ++ b` | | `isEmpty` | `List a -> Bool` | | | `head` | `List a -> Maybe a` | `Nothing` for `[]` | | `tail` | `List a -> Maybe (List a)` | `Nothing` for `[]` | | `take` | `Int -> List a -> List a` | the first `n`; empty when `n <= 0` | | `drop` | `Int -> List a -> List a` | all but the first `n`; the whole list when `n <= 0` | | `partition` | `(a -> Bool) -> List a -> ( List a, List a )` | `( passing, failing )`, both in input order | | `unzip` | `List ( a, b ) -> ( List a, List b )` | | | `sort` | `List comparable -> List comparable` | smallest first | | `sortBy` | `(a -> comparable) -> List a -> List a` | by a key | | `sortWith` | `(a -> a -> Order) -> List a -> List a` | by a comparison function | Semantics: - `foldl` combines from the first element and `foldr` from the last. Both callbacks receive the **element first** and the accumulator second: `List.foldl (\v acc -> acc * 10 + v) 0 [ 1, 2, 3 ]` is `123`, and the same with `foldr` is `321`. - `all` stops at the first `False` and `any` at the first `True`. - `map2` and `map3` drop the extra elements of longer lists, as in Elm. On-chain, check lengths first when extras matter: pairing outputs with expected payouts would silently ignore an extra output. - The sorts are stable merge sorts, O(n log n): equal elements keep their order. `sortBy` computes the key at each comparison, so keep keys cheap. - Every other function is a traversal, O(n) in the list's length. ```elm title="src/Main.elm" module Main exposing (main) main : ( List Int, Maybe Int, ( List Int, List Int ) ) main = let amounts = [ 30, 5, 12, 5, 40 ] in ( List.sort amounts , List.maximum amounts , List.partition (\amount -> amount >= 10) amounts ) ``` ```text $ elci eval ( [5, 5, 12, 30, 40], Just 40, ( [30, 12, 40], [5, 5] ) ) cpu 48378577, mem 228938 ``` Records and custom types are not comparable. Sort them by a key, or with a comparison: ```elm title="src/Main.elm" module Main exposing (main) type alias Bid = { bidder : String , amount : Int } bids : List Bid bids = [ Bid "carol" 30, Bid "alice" 50, Bid "bob" 30 ] main : ( List String, List String, String ) main = ( List.map (\bid -> bid.bidder) (List.sortBy (\bid -> bid.amount) bids) , List.map (\bid -> bid.bidder) (List.sortWith (\a b -> compare b.amount a.amount) bids) , String.join ", " (List.map (\bid -> String.fromInt bid.amount) bids) ) ``` ```text $ elci eval ( ["carol", "bob", "alice"], ["alice", "carol", "bob"], "30, 50, 30" ) cpu 60322206, mem 262554 ``` The constructors `[]` and `::`, list literals `[ 1, 2 ]`, `++` and list patterns are part of the language. Sorting on-chain is not cheap; see [costs and performance](https://elci-lang.dev/advanced/costs/#sorting-and-comparing). ## `Maybe` `Maybe a` is built in. Every module imports the `Maybe` module with `exposing (Maybe(..))`, so `Just` and `Nothing` need no qualifier. | Function | Type | Behavior | | --- | --- | --- | | `withDefault` | `a -> Maybe a -> a` | the value inside a `Just`, or the default | | `map` | `(a -> b) -> Maybe a -> Maybe b` | transforms the value inside a `Just` | | `map2` | `(a -> b -> value) -> Maybe a -> Maybe b -> Maybe value` | `Just` when both are present | | `map3` | `(a -> b -> c -> value) -> Maybe a -> Maybe b -> Maybe c -> Maybe value` | `Just` when all are present | | `andThen` | `(a -> Maybe b) -> Maybe a -> Maybe b` | chains computations that may produce nothing | Arguments are evaluated before a call, as in Elm, so `Maybe.withDefault default m` always evaluates `default`, even when `m` is a `Just`. On-chain that costs budget, and a failing default fails the script. Keep an expensive or failing default in a `case` branch. `Nothing` is constructor 0 and `Just` is constructor 1 in Plutus Data. ## `Result` `type Result error value = Ok value | Err error`. Every module imports the `Result` module with `exposing (Result(..))`. | Function | Type | Behavior | | --- | --- | --- | | `map` | `(a -> value) -> Result x a -> Result x value` | transforms an `Ok` value | | `map2` | `(a -> b -> value) -> Result x a -> Result x b -> Result x value` | the first `Err` wins | | `map3` | `(a -> b -> c -> value) -> Result x a -> Result x b -> Result x c -> Result x value` | the first `Err` wins | | `andThen` | `(a -> Result x b) -> Result x a -> Result x b` | chains computations that may fail | | `withDefault` | `a -> Result x a -> a` | the `Ok` value, or the default | | `toMaybe` | `Result x a -> Maybe a` | `Just` the `Ok` value, or `Nothing` | | `fromMaybe` | `x -> Maybe a -> Result x a` | `Ok` with the value of a `Just`, or `Err` with the given error | | `mapError` | `(x -> y) -> Result x a -> Result y a` | transforms an `Err` error | In Plutus Data, `Ok` is constructor 0 and `Err` is constructor 1, following the declaration order. This is the reverse of PlutusTx's `Either` (`Left` 0, `Right` 1): a datum written as an `Either` does not read as a `Result`, because decoding would swap success and failure. ```elm title="src/Main.elm" module Main exposing (main) positive : Int -> Result String Int positive amount = if amount > 0 then Ok amount else Err "not positive" main : ( ( Int, Maybe Int, Maybe Int ), ( Result String Int, Result String Int, Int ) ) main = ( ( Maybe.withDefault 0 (List.head []) , Maybe.map (\n -> n * 2) (List.head [ 4, 5 ]) , Maybe.andThen List.head (List.tail [ 1, 2, 3 ]) ) , ( Result.map2 (+) (positive 5) (positive 7) , Result.map2 (+) (positive -1) (positive 0) , Result.withDefault 0 (Result.andThen positive (Ok 3)) ) ) ``` ```text $ elci eval ( ( 0, Just 8, Just 2 ), ( Ok 12, Err "not positive", 3 ) ) cpu 19297460, mem 75445 ``` ## `Tuple` Every module imports `Tuple`. The functions work on pairs; take a triple apart with a pattern such as `( a, b, c )`. | Function | Type | Behavior | | --- | --- | --- | | `pair` | `a -> b -> ( a, b )` | `pair 1 "one"` is `( 1, "one" )` | | `first` | `( a, b ) -> a` | | | `second` | `( a, b ) -> b` | | | `mapFirst` | `(a -> x) -> ( a, b ) -> ( x, b )` | | | `mapSecond` | `(b -> y) -> ( a, b ) -> ( a, y )` | | | `mapBoth` | `(a -> x) -> (b -> y) -> ( a, b ) -> ( x, y )` | | ## `String` Every module imports `String`. A `String` is Plutus text: UTF-8, compared by its bytes. | Function | Type | Behavior | | --- | --- | --- | | `fromInt` | `Int -> String` | the decimal form: `fromInt -42` is `"-42"` | | `isEmpty` | `String -> Bool` | | | `append` | `String -> String -> String` | the same as `++` | | `concat` | `List String -> String` | `concat [ "ab", "cd" ]` is `"abcd"` | | `join` | `String -> List String -> String` | `join ", " [ "a", "b" ]` is `"a, b"` | There is no `length`, `toInt`, `split`, `slice` or `fromFloat`. `Bytes.fromString` and `Bytes.toString` convert to and from the UTF-8 bytes, and `Bytes.length (Bytes.fromString s)` is the byte length. Asking for a missing function lists what the module has: ```text src/Main.elm:6:5: ELCI2104: Elci's String module has no toInt. It has append, concat, fromInt, isEmpty, join ``` ## `Dict` `import Dict exposing (Dict)`. A `Dict k v` is Elm's dictionary, with Elm's functions, argument orders and results. It is stored as a Plutus map whose keys are strictly ascending in the [comparable order](#comparable-types), so every dictionary has exactly one representation. Keys are comparable types: `Int`, `String`, `Bytes`, ledger hashes such as `PubKeyHash`, and lists and tuples of these. Values may be any type with a Data encoding. | Function | Type | Behavior | | --- | --- | --- | | `empty` | `Dict k v` | | | `singleton` | `comparable -> v -> Dict comparable v` | | | `insert` | `comparable -> v -> Dict comparable v -> Dict comparable v` | replaces an existing binding | | `update` | `comparable -> (Maybe v -> Maybe v) -> Dict comparable v -> Dict comparable v` | `Nothing` removes the key, `Just v` sets it | | `remove` | `comparable -> Dict comparable v -> Dict comparable v` | | | `isEmpty` | `Dict k v -> Bool` | | | `member` | `comparable -> Dict comparable v -> Bool` | | | `get` | `comparable -> Dict comparable v -> Maybe v` | `Nothing` when the key is absent | | `size` | `Dict k v -> Int` | | | `keys` | `Dict k v -> List k` | in ascending order | | `values` | `Dict k v -> List v` | in ascending key order | | `toList` | `Dict k v -> List ( k, v )` | in ascending key order | | `fromList` | `List ( comparable, v ) -> Dict comparable v` | for a repeated key, the last pair wins | | `map` | `(k -> a -> b) -> Dict k a -> Dict k b` | | | `foldl` | `(k -> v -> b -> b) -> b -> Dict k v -> b` | from the smallest key | | `foldr` | `(k -> v -> b -> b) -> b -> Dict k v -> b` | from the largest key | | `filter` | `(comparable -> v -> Bool) -> Dict comparable v -> Dict comparable v` | | | `partition` | `(comparable -> v -> Bool) -> Dict comparable v -> ( Dict comparable v, Dict comparable v )` | `( passing, failing )` | | `union` | `Dict comparable v -> Dict comparable v -> Dict comparable v` | for a key in both, the left value wins | | `intersect` | `Dict comparable v -> Dict comparable v -> Dict comparable v` | the left bindings whose keys the right has | | `diff` | `Dict comparable a -> Dict comparable b -> Dict comparable a` | the left bindings whose keys the right lacks | | `merge` | `(comparable -> a -> result -> result) -> (comparable -> a -> b -> result -> result) -> (comparable -> b -> result -> result) -> Dict comparable a -> Dict comparable b -> result -> result` | see below | ```elm title="src/Main.elm" module Main exposing (main) import Dict exposing (Dict) stock : Dict String Int stock = Dict.fromList [ ( "pear", 5 ), ( "apple", 3 ), ( "fig", 0 ), ( "apple", 4 ) ] main : ( List ( String, Int ), ( Maybe Int, Bool ), List String ) main = ( Dict.toList (Dict.insert "kiwi" 2 stock) , ( Dict.get "apple" stock, Dict.member "plum" stock ) , Dict.keys (Dict.filter (\_ count -> count > 0) stock) ) ``` ```text $ elci eval ( [( "apple", 4 ), ( "fig", 0 ), ( "kiwi", 2 ), ( "pear", 5 )], ( Just 4, False ), ["apple", "pear"] ) cpu 77921806, mem 309819 ``` The bindings come out in key order, whatever order they went in, and the second `"apple"` replaced the first. Combining two dictionaries follows Elm too. `merge` folds over both in ascending key order, with one step for keys only in the left dictionary, one for keys in both and one for keys only in the right: ```elm title="src/Main.elm" module Main exposing (main) import Dict exposing (Dict) left : Dict Int String left = Dict.fromList [ ( 1, "a" ), ( 2, "b" ) ] right : Dict Int String right = Dict.fromList [ ( 2, "x" ), ( 3, "y" ) ] main : ( ( List ( Int, String ), List Int, List Int ), ( List String, Bool ) ) main = ( ( Dict.toList (Dict.union left right) , Dict.keys (Dict.intersect left right) , Dict.keys (Dict.diff left right) ) , ( Dict.merge (\key _ result -> result ++ [ "only left " ++ String.fromInt key ]) (\key _ _ result -> result ++ [ "both " ++ String.fromInt key ]) (\key _ result -> result ++ [ "only right " ++ String.fromInt key ]) left right [] , Dict.insert 2 "b" (Dict.singleton 1 "a") == left ) ) ``` ```text $ elci eval ( ( [( 1, "a" ), ( 2, "b" ), ( 3, "y" )], [2], [1] ), ( ["only left 1", "both 2", "only right 3"], True ) ) cpu 116952501, mem 457650 ``` **Equality.** `==` compares bindings: two dictionaries are equal when they have the same keys with equal values, however they were built. The values need equality themselves, so a `Dict Int (Map Int Int)` can't be compared (`ELCI2003`). Dictionaries are not comparable. **Cost.** A `Dict` is a list of pairs, not a tree as in Elm: - `get`, `member`, `insert`, `remove` and `update` walk from the smallest key and stop at the first key that isn't smaller than the one they look for. They are O(n), at about 1.1 million CPU units per key passed. Small keys are found fastest, and a missing key costs half a scan on average. - `fromList` is a stable merge sort: O(n log n), and O(n) when the pairs are already in key order. For 50 entries it cost about 0.28 billion CPU units with sorted input and 0.66 billion with shuffled input, against 1.5 billion for `Map.fromList`. - `size`, `keys`, `values`, `toList`, the folds, `map`, `filter` and `partition` are single passes. `union`, `intersect`, `diff` and `merge` walk both dictionaries once: O(n + m). **In Data.** A `Dict` encodes as a Plutus map with its keys in ascending order. Decoding checks the order: a map whose keys are not strictly ascending, because they are out of order or repeated, fails to decode. The check applies to datums, redeemers, parameters, `Julc.Data.decode` and property-test arguments, and to every `Dict` inside them: in records, tuples, custom types, `Maybe`, `Result`, Julc optionals, lists and map values. It costs about 1.1 million CPU units per entry. Without it, a datum could place a key after a larger one, where `member` and `get` stop before reaching it; [datums and redeemers](https://elci-lang.dev/contracts/datums-and-redeemers/#dictionaries-in-data) shows the attack. `elci apply` rejects such a parameter: ```text elci: ELCI4001: parameter caps has a Dict whose keys are not strictly ascending; write its keys sorted and without repeats ``` Two rules keep every decoded `Dict` checked. Checking the order needs comparable keys, so a `Dict` with other keys can't be decoded from Data: ```text src/Main.elm:9:5: ELCI2003: A Dict decoded from Data needs comparable keys, to check that they are in order; Bool is not comparable. Use a Map instead ``` And a Java library's generic function can't be used at a type that contains a `Dict`, because the library could build one without the check: ```text src/Main.elm:14:18: ELCI3003: Type argument a = Dict Int Int of filter contains a Dict, which a Java library cannot keep sorted; pass a Map or a List and build the Dict with Dict.fromList ``` **`Dict` or `Map`?** Use a `Dict` for data you build or look up by a comparable key, including datums. Use a [`Map`](#map) for the ledger's own maps, such as `txInfo.withdrawals` and `txInfo.redeemers`, and for keys that are not comparable. ## `Map` `import Map`. A `Map k v` is the ledger's map representation: a list of key/value pairs, compared by the keys' Data encoding. The transaction's maps, such as `txInfo.withdrawals`, `txInfo.redeemers` and `txInfo.datums`, are `Map`s. Keys and values may be any type with a Data encoding, including records, custom types and ledger types; keys need not be comparable. | Function | Type | Behavior | | --- | --- | --- | | `empty` | `Map k v` | | | `singleton` | `k -> v -> Map k v` | | | `get` | `k -> Map k v -> Maybe v` | `Nothing` when the key is absent | | `member` | `k -> Map k v -> Bool` | | | `insert` | `k -> v -> Map k v -> Map k v` | replaces an existing binding | | `remove` | `k -> Map k v -> Map k v` | | | `update` | `k -> (Maybe v -> Maybe v) -> Map k v -> Map k v` | `Nothing` removes the key, `Just v` sets it | | `size` | `Map k v -> Int` | | | `isEmpty` | `Map k v -> Bool` | | | `keys` | `Map k v -> List k` | in map order | | `values` | `Map k v -> List v` | in map order | | `toList` | `Map k v -> List ( k, v )` | in map order | | `fromList` | `List ( k, v ) -> Map k v` | inserts the pairs in list order | | `foldl` | `(k -> v -> b -> b) -> b -> Map k v -> b` | in map order | | `foldr` | `(k -> v -> b -> b) -> b -> Map k v -> b` | in reverse map order | | `filter` | `(k -> v -> Bool) -> Map k v -> Map k v` | keeps map order | | `map` | `(k -> a -> b) -> Map k a -> Map k b` | keeps map order | **Map order.** `insert` puts the binding at the front, and a replaced key moves to the front. So `Map.keys (Map.fromList [ ( 1, 10 ), ( 2, 20 ), ( 3, 30 ) ])` is `[ 3, 2, 1 ]`. `filter` and `map` keep the order of the map they are given: ```elm title="src/Main.elm" module Main exposing (main) import Map exposing (Map) prices : Map String Int prices = Map.fromList [ ( "apple", 3 ), ( "pear", 5 ) ] main : ( Map String Int, Map String Int, Maybe Int ) main = ( Map.insert "fig" 7 prices , Map.filter (\_ price -> price > 3) (Map.insert "fig" 7 prices) , Map.get "pear" prices ) ``` ```text $ elci eval ( Map.fromList [( "fig", 7 ), ( "pear", 5 ), ( "apple", 3 )], Map.fromList [( "fig", 7 ), ( "pear", 5 )], Just 5 ) cpu 36520840, mem 151709 ``` Avoid making results depend on map order. To process the bindings in key order, sort them, `List.sortBy Tuple.first (Map.toList prices)`, or keep them in a [`Dict`](#dict). **No equality.** `==` on maps is rejected (`ELCI2003`): two maps with the same bindings may hold them in different orders. For the same reason `Data.encode` of two such maps may differ. Compare the lookups you care about, or their `toList` after sorting, or use a `Dict`. **Repeated keys.** A map built in Elci never repeats a key, but a map decoded from Data can, because decoding doesn't check a `Map`'s keys. In such a map, `get` returns the first binding of the key, `remove` and `insert` remove every binding of it, and `map` and `filter` keep every binding. **Cost.** `get`, `member`, `insert`, `remove` and `update` scan the pairs from the front: O(n), at about 1.1 million CPU units per key passed. `size`, `keys`, `values`, `toList`, the folds, `filter` and `map` are single passes. `fromList` checks each key against the pairs before it, so it is O(n²): building a 50-entry map cost about 1.5 billion CPU units. With comparable keys, `Dict.fromList` is much cheaper. See [costs and performance](https://elci-lang.dev/advanced/costs/#maps-and-dictionaries). A map or dictionary whose key and value types nothing determines can't be compiled, because the operations are specialized for them. `Map.size Map.empty` on its own fails at the call: ```text src/Main.elm:8:5: ELCI3001: `Map.size` is used here at a type that is not known (Map a b -> Int); add a type annotation, for example to a let binding ``` Give the map a type, for example with a local definition `noPrices : Map String Int`. ## `Array` `import Array`. An `Array a` works like Elm's `Array`. It is a Plutus V3 array: built once from a list, then read in constant time. | Function | Type | Behavior | | --- | --- | --- | | `empty` | `Array a` | | | `fromList` | `List a -> Array a` | O(n) | | `toList` | `Array a -> List a` | O(n) | | `toIndexedList` | `Array a -> List ( Int, a )` | pairs of index and element | | `length` | `Array a -> Int` | O(1) | | `isEmpty` | `Array a -> Bool` | O(1) | | `get` | `Int -> Array a -> Maybe a` | O(1); `Nothing` for an index below 0 or at least `length` | | `initialize` | `Int -> (Int -> a) -> Array a` | `initialize 4 (\i -> i * i)` is `[ 0, 1, 4, 9 ]` | | `repeat` | `Int -> a -> Array a` | `repeat 3 7` is `[ 7, 7, 7 ]` | | `foldl` | `(a -> b -> b) -> b -> Array a -> b` | from the first element | | `foldr` | `(a -> b -> b) -> b -> Array a -> b` | from the last element | | `map` | `(a -> b) -> Array a -> Array b` | O(n) | | `indexedMap` | `(Int -> a -> b) -> Array a -> Array b` | O(n) | | `filter` | `(a -> Bool) -> Array a -> Array a` | O(n) | | `slice` | `Int -> Int -> Array a -> Array a` | from `start` up to, not including, `end` | `slice` counts a negative index from the end and clamps to the array. For `[ 10, 20, 30, 40, 50 ]`, `slice 1 3` is `[ 20, 30 ]`, `slice 0 -1` is `[ 10, 20, 30, 40 ]` and `slice -2 10` is `[ 40, 50 ]`. Arrays are immutable and there is no `set` or `push`. Build a list, then convert it once. **No Data encoding.** An array cannot be a datum, redeemer, parameter or property-test argument, nor a field of a record, custom type, tuple, list or map. Storing one there fails with `ELCI2013: Arrays have no Data encoding and cannot be stored in data; keep a List and convert it with Array.fromList`, and `Julc.Data.encode` on an array is rejected with `ELCI3003`. Keep lists in your data and call `Array.fromList` in the handler. An `[entrypoint]` may return an array on its own; `elci eval` prints it as `Array.fromList [40, 50]`. ## `Value` `import Value exposing (Value)`. A `Value` is the ledger's multi-asset value: an amount of lovelace and quantities of native tokens. `out.value` and `ctx.txInfo.mint` are `Value`s, and so is everything this module builds. A token is named by a policy id and a token name, the ledger types `PolicyId` and `TokenName`; build them from bytes with `import Julc.Ledger exposing (PolicyId(..), TokenName(..))`, as in `PolicyId bytes`. Ada is the empty token name under the empty policy, and `lovelace` and `lovelaceOf` cover it; a token with another name under the empty policy is not lovelace. Functions take the value last, as in Elm. | Function | Type | Behavior | | --- | --- | --- | | `zero` | `Value` | the empty value | | `lovelace` | `Int -> Value` | an amount of lovelace | | `singleton` | `PolicyId -> TokenName -> Int -> Value` | a quantity of one token | | `insert` | `PolicyId -> TokenName -> Int -> Value -> Value` | sets the quantity of one token, replacing what the value held; `0` removes the token | | `fromList` | `List ( PolicyId, TokenName, Int ) -> Value` | the listed quantities; a repeated token adds up | | `lovelaceOf` | `Value -> Int` | the lovelace: the quantity of ada's token; `0` when there is none, as in a minted value | | `quantityOf` | `PolicyId -> TokenName -> Value -> Int` | the quantity of one token; `0` when there is none | | `isZero` | `Value -> Bool` | whether the value holds nothing | | `policies` | `Value -> List PolicyId` | in ascending order; ada's is the empty policy | | `tokens` | `PolicyId -> Value -> Dict TokenName Int` | the tokens of one policy and their quantities | | `toList` | `Value -> List ( PolicyId, TokenName, Int )` | every token, in ascending order | | `add` | `Value -> Value -> Value` | the sum | | `subtract` | `Value -> Value -> Value` | the first value minus the second | | `negate` | `Value -> Value` | every quantity negated | | `scale` | `Int -> Value -> Value` | every quantity multiplied | | `sum` | `List Value -> Value` | the sum of the list | | `geq` | `Value -> Value -> Bool` | `geq a b`: `a` holds at least as much of every token as `b` | | `leq` | `Value -> Value -> Bool` | `leq a b`: `a` holds at most as much of every token as `b` | | `withoutLovelace` | `Value -> Value` | the value without its lovelace | | `restrictedTo` | `List PolicyId -> Value -> Value` | only the tokens of the listed policies | | `positive` | `Value -> Value` | only the positive quantities: in a minted value, what is minted | | `negative` | `Value -> Value` | only the negative quantities, still negative: in a minted value, what is burned | ```elm title="src/Main.elm" module Main exposing (main) import Bytes import Julc.Ledger exposing (PolicyId(..), TokenName(..)) import Value exposing (Value) gold : PolicyId gold = PolicyId (Bytes.fromString "gold") coin : TokenName coin = TokenName (Bytes.fromString "coin") payment : Value payment = Value.add (Value.lovelace 2000000) (Value.fromList [ ( gold, coin, 3 ), ( gold, coin, 2 ) ]) main : ( Value, ( Int, Int ), ( Bool, Bool ) ) main = ( payment , ( Value.lovelaceOf payment, Value.quantityOf gold coin payment ) , ( Value.geq payment (Value.lovelace 1500000) , payment == Value.insert gold coin 5 (Value.lovelace 2000000) ) ) ``` ```text $ elci eval ( Value.fromList [( #(empty), #(empty), 2000000 ), ( #676f6c64, #636f696e, 5 )], ( 2000000, 5 ), ( True, True ) ) cpu 29654521, mem 85992 ``` `elci eval` prints a value as the `Value.fromList` that builds it, one `( policy, token name, quantity )` per token; `#(empty)` is ada's policy and token name. The two `coin` entries added up to 5. **Canonical form and equality.** Every `Value` is canonical: policies ascend by their bytes, compared as unsigned, token names ascend within each policy, and no quantity is zero. The ledger's values are canonical, the functions above keep them so, and decoding a `Value` from Data rejects any other form. So two values are equal exactly when they hold the same quantities, whatever order the tokens were added in: `==` works on values, and on your records, tuples and lists that contain them. Values are not [comparable](#comparable-types); compare them with `geq` and `leq`. **Negative quantities.** A value may hold negative quantities. The transaction's `mint` field holds the tokens it burns as negative quantities. `geq` and `leq` handle them: `geq a b` subtracts `b` from `a` and checks that no quantity is below zero, so a burn counts as less than none. `positive` and `negative` split a mint into what it mints and what it burns: ```elm title="src/Main.elm" module Main exposing (main) import Bytes import Dict import Julc.Ledger exposing (PolicyId(..), TokenName(..)) import Value exposing (Value) gold : PolicyId gold = PolicyId (Bytes.fromString "gold") {-| A mint that creates 3 new coins and burns 2 old ones. -} minted : Value minted = Value.fromList [ ( gold, TokenName (Bytes.fromString "new"), 3 ) , ( gold, TokenName (Bytes.fromString "old"), -2 ) ] main : ( ( Value, Value ), ( Bool, Bool ), List ( TokenName, Int ) ) main = ( ( Value.positive minted, Value.negate (Value.negative minted) ) , ( Value.geq minted Value.zero, Value.geq (Value.positive minted) Value.zero ) , Dict.toList (Value.tokens gold minted) ) ``` ```text $ elci eval ( ( Value.fromList [( #676f6c64, #6e6577, 3 )], Value.fromList [( #676f6c64, #6f6c64, 2 )] ), ( False, True ), [( #6e6577, 3 ), ( #6f6c64, -2 )] ) cpu 45778556, mem 170124 ``` `Value.negate (Value.negative minted)` gives the burned amounts as positive quantities. **Failures.** A policy id or token name longer than 32 bytes, and a quantity outside the signed 128-bit range, fail the script: `evaluation failed: InsertCoin: tokenName too long: 33 bytes (max 32)`. **In Data.** A `Value` is stored as the ledger stores it, a Plutus map from policy ids to maps from token names to quantities, so `Julc.Data.encode` of a value costs nothing. `Julc.Data.decode` accepts only the canonical form. Unsorted or repeated policies or token names fail with `UnValueData: currencies not strictly ordered or duplicate`, a zero quantity with `UnValueData: zero quantity`, and a policy without tokens or an id longer than 32 bytes fail too. Both conversions work on a `Value` itself. A type that contains one can't be decoded yet, and a list or tuple of values can't be encoded either (`ELCI3003`). A `Value` can't be a datum, redeemer or parameter yet, nor part of one: ```text src/Main.elm:7:1: ELCI2013: org.julclang.ledger.Value has no approved strict Data boundary (BOUNDARY); for example records containing the map-encoded Value cannot be checked ``` Store a `List ( PolicyId, TokenName, Int )` instead, and rebuild the value with `Value.fromList`. **Cost.** `lovelaceOf`, `isZero`, `policies`, `tokens` and `toList` read the value's Data directly, and `positive`, `negative`, `restrictedTo` and `withoutLovelace` filter it entry by entry, keeping it canonical. The other functions convert it to the protocol's native value (CIP-153 builtins) and back, which costs about a million CPU units for a value of a few assets; `sum` converts each value once. These small programs, which include building their values, cost: | Program | CPU | | --- | ---: | | `Value.add` of `Value.lovelace` and `Value.singleton` | 3.5 million | | `Value.lovelaceOf (Value.lovelace 5000000)` | 2.7 million | | `Value.quantityOf` of a `Value.singleton` | 2.5 million | | `Value.geq` of two `Value.lovelace` values | 6.3 million | | `Value.sum` of five `Value.lovelace` values | 20 million | Most functions grow with the number of assets; [costs and performance](https://elci-lang.dev/advanced/costs/#values) has figures for larger values. **Java libraries.** A Java library builds values in its own order, as Julc's `ValuesLib` does, so no Java library may hand Elci a `Value`. An export whose result can hold one, directly or in a field of a record or union, is rejected: ```text src/Main.elm:9:5: ELCI2105: org.julclang.stdlib.lib.ValuesLib.add: returns a Value, which Java libraries do not keep in the ledger's canonical order; use the core Value module ``` A generic Java function can't be used at a type that holds a `Value` either: ```text src/Main.elm:9:5: ELCI3003: Type argument a = Value of filter contains a Value, which a Java library does not keep in the ledger's canonical order; build it with the Value module ``` Julc's `ContextsLib` and `OutputLib` are exempt: they only pass along the ledger's own values, as `ContextsLib.valuePaid` does. Exports that only read a value, such as `ValuesLib.assetOf`, still work, but this module covers them. A value has no constructors or patterns of its own: `import Julc.Ledger exposing (Value(..))` gives `ELCI2104: Value has no approved constructors or patterns; its values come from the Value module`. ## `Interval` `import Interval`. An `Interval` is the ledger's time range, such as the transaction's validity range `ctx.txInfo.validRange`. Times are POSIX milliseconds. A range has a start, `range.from`, and an end, `range.to`. Each bound is `NegInf`, `Finite time` or `PosInf`, and is inclusive or exclusive. A script can't read the clock. The ledger accepts a transaction only inside its validity range, so a time check asks where the whole range lies: `isEntirelyAfter deadline ctx.txInfo.validRange` holds only when the transaction cannot run at or before the deadline. | Function | Type | Behavior | | --- | --- | --- | | `isEntirelyAfter` | `Int -> Interval -> Bool` | `isEntirelyAfter t range`: the range has a finite start, and every time in it is after `t` | | `isEntirelyBefore` | `Int -> Interval -> Bool` | `isEntirelyBefore t range`: the range has a finite end, and every time in it is before `t` | | `contains` | `Int -> Interval -> Bool` | `contains t range`: `t` is in the range | | `start` | `Interval -> Maybe Int` | the finite start, or `Nothing` | | `end` | `Interval -> Maybe Int` | the finite end, or `Nothing` | | `after` | `Int -> Interval` | from `t` on, inclusive | | `before` | `Int -> Interval` | up to `t`, inclusive | | `between` | `Int -> Int -> Interval` | `between low high`: from `low` to `high`, both inclusive | | `everything` | `Interval` | all time | "After" and "before" are strict: no time in the range may equal `t`. The checks honor each bound's inclusiveness. A scenario's `Scenario.validFrom t` gives the range an inclusive start, and `Scenario.validBefore t` an exclusive end, so: - `isEntirelyAfter deadline` holds for a range valid from `deadline + 1` on, not from `deadline`. To accept a transaction valid from the deadline itself, check `isEntirelyAfter (deadline - 1)`. - `isEntirelyBefore deadline` holds for a range valid before `deadline`: the exclusive end may equal it. ```elm title="src/Main.elm" module Main exposing (main) import Interval exposing (Interval) import Julc.Ledger exposing (Interval(..), IntervalBound(..), IntervalBoundType(..)) {-| A validity range from 1000, inclusive, to 2000, exclusive. -} window : Interval window = Interval (IntervalBound (Finite 1000) True) (IntervalBound (Finite 2000) False) main : ( ( Bool, Bool, Bool ), ( Bool, Bool, Bool ), ( Bool, Bool, Maybe Int ) ) main = ( ( Interval.isEntirelyAfter 999 window, Interval.isEntirelyAfter 1000 window, Interval.contains 1000 window ) , ( Interval.isEntirelyBefore 2000 window, Interval.isEntirelyBefore 1999 window, Interval.contains 2000 window ) , ( Interval.isEntirelyAfter 0 (Interval.before 5), Interval.isEntirelyBefore 10 (Interval.after 5), Interval.end (Interval.after 5) ) ) ``` ```text $ elci eval ( ( True, False, True ), ( True, False, False ), ( False, False, Nothing ) ) cpu 44424561, mem 167367 ``` The window starts at 1000, inclusive: it is entirely after 999 but not after 1000, and it contains 1000. It ends at 2000, exclusive: it is entirely before 2000 but not before 1999, and it doesn't contain 2000. **Unbounded ranges.** The two deadline checks never hold on an infinite bound. `isEntirelyAfter` needs a finite start and `isEntirelyBefore` a finite end, so a transaction that sets no validity range passes neither check, and `Interval.before 5` is not entirely after any time. A `PosInf` start or a `NegInf` end, which only an empty range has, fails the check that reads it, as in Aiken's stdlib v2; Aiken v3 lets such an empty range pass both checks. `start` and `end` return `Nothing` for an infinite bound, never a sentinel time. They return the bound's time whether or not it is inclusive; read `range.from.isInclusive` when that matters, or use the checks, which account for it. The constructors `Interval`, `IntervalBound`, `Finite`, `NegInf` and `PosInf` come from `Julc.Ledger`, as in the example. The `Interval` type is the ledger's, so `import Julc.Ledger exposing (Interval)` names the same type. **Cost.** Each check reads one or two bounds and compares integers. `isEntirelyAfter` and `isEntirelyBefore` cost about 2.8 million CPU units, `contains` about 5.5 million. ## `Tx` `import Tx`. Questions about the transaction a script validates. Every function takes the `ScriptContext` last, so a check reads `Tx.signedBy owner ctx`. | Function | Type | Behavior | | --- | --- | --- | | `signedBy` | `PubKeyHash -> ScriptContext -> Bool` | the key is among the transaction's signatories | | `spends` | `TxOutRef -> ScriptContext -> Bool` | the transaction spends the output; reference inputs don't count | | `findInput` | `TxOutRef -> ScriptContext -> Maybe TxInInfo` | the spent output with that reference, with its address, value and datum | | `ownRef` | `ScriptContext -> Maybe TxOutRef` | when spending, the reference of the output being validated | | `ownInput` | `ScriptContext -> Maybe TxInInfo` | when spending, the output being validated | | `ownPolicy` | `ScriptContext -> Maybe PolicyId` | when minting, the script's own policy id | | `outputsTo` | `Credential -> ScriptContext -> List TxOut` | the outputs whose payment credential is the credential, in order | | `valuePaidTo` | `Credential -> ScriptContext -> Value` | the total value of those outputs | | `continuingOutputs` | `ScriptContext -> List TxOut` | when spending, the outputs to the spent output's address | | `valueSpent` | `ScriptContext -> Value` | the total value of the spent outputs | | `valueProduced` | `ScriptContext -> Value` | the total value of the outputs | | `mintedOf` | `PolicyId -> ScriptContext -> Dict TokenName Int` | the tokens the policy mints; a burn is a negative quantity | | `inlineDatum` | `TxOut -> Maybe Data` | the output's inline datum | | `datumOf` | `TxOut -> ScriptContext -> Maybe Data` | the output's datum: inline, or by its hash from the datums the transaction supplies | - **Purposes.** `ownRef`, `ownInput` and `continuingOutputs` describe the output a spend handler validates, and `ownPolicy` the policy a mint handler validates. For any other purpose they return `Nothing` or `[]`. Match the `Nothing` case and return `False`. - **Credentials, not addresses.** `outputsTo` and `valuePaidTo` match an output's payment credential (`output.address.credential`) and ignore its staking part. Whoever builds the transaction chooses the payee's staking part: a payment to the seller's key counts whatever staking credential its address carries. When the staking part matters, store the payee's full `Address` and compare `output.address`. `continuingOutputs` compares whole addresses, staking part included. - **Payments must be tagged.** "Some output pays the seller" is true for two script inputs spent in one transaction when one output pays for both. This is the double-satisfaction attack, and an untagged `valuePaidTo` check has it. Require each payment to carry the spent output's reference as its inline datum, as below, or allow one script input per transaction. [Reading the transaction](https://elci-lang.dev/contracts/reading-the-transaction/#one-payment-per-input) explains both defences. - **Datums stay `Data`.** `inlineDatum` returns `Nothing` for an output with a datum hash or no datum. `datumOf` also finds a datum by its hash in `txInfo.datums`, and returns `Nothing` when the transaction doesn't supply it. Decode a datum with an annotated `Data.decode`, which fails the script on a malformed datum, or compare it with an expected encoding: `==` on `Data` compares structure. - **Values.** `valueSpent` sums the spent outputs, not the reference inputs. `valueProduced` sums the outputs; the fee and the minted tokens are separate fields, `txInfo.fee` and `txInfo.mint`. `mintedOf` is `Value.tokens policy ctx.txInfo.mint`: an empty `Dict` when the policy mints nothing. A payment check that pays for this input only: ```elm title="src/Sale.elm" module Sale exposing (paidFor) import Julc.Data as Data import Julc.Ledger exposing (Credential(..), PubKeyHash, ScriptContext) import Tx import Value {-| An output pays the seller at least `price` lovelace and carries this input's reference as its inline datum. -} paidFor : PubKeyHash -> Int -> ScriptContext -> Bool paidFor seller price ctx = case Tx.ownRef ctx of Just ref -> List.any (\output -> (Value.lovelaceOf output.value >= price) && (Tx.inlineDatum output == Just (Data.encode ref)) ) (Tx.outputsTo (PubKeyCredential seller) ctx) Nothing -> False ``` **Cost.** The functions walk the transaction's lists. Measured per element, in CPU units: | Function | Cost | | --- | --- | | `signedBy` | about 0.7 million per signatory before the key | | `spends`, `findInput`, `ownInput` | about 2.5 million per input before the match; `spends` walks every input when the output isn't spent | | `outputsTo` | about 2.8 million per output | | `valuePaidTo` | as `outputsTo`, plus about 2.9 million per matching output to add its value | | `continuingOutputs` | `ownInput`, then about 3.6 million per output | | `valueSpent`, `valueProduced` | about 3.3 million per input and 2.8 million per output | | `ownRef`, `ownPolicy`, `inlineDatum` | a few field reads, whatever the transaction's size | When a branch needs a result twice, bind it once in a `let` inside that branch. ## `Bytes` `import Bytes`. Each function is a direct Plutus builtin call, with the byte string last. [Bytes, hashes and bits](https://elci-lang.dev/advanced/bytes-and-crypto/) explains each group with examples. | Function | Type | Behavior | | --- | --- | --- | | `empty` | `Bytes` | | | `fromString` | `String -> Bytes` | UTF-8 encoding | | `toString` | `Bytes -> String` | UTF-8 decoding; fails on invalid UTF-8 | | `length` | `Bytes -> Int` | | | `append` | `Bytes -> Bytes -> Bytes` | `append a b` is `a` followed by `b` | | `slice` | `Int -> Int -> Bytes -> Bytes` | `slice start count bytes`; clamps | | `take` | `Int -> Bytes -> Bytes` | clamps | | `drop` | `Int -> Bytes -> Bytes` | clamps | | `at` | `Int -> Bytes -> Int` | the byte at an index, 0–255; fails outside the string | | `cons` | `Int -> Bytes -> Bytes` | prepends a byte; fails outside 0–255 | | `replicate` | `Int -> Int -> Bytes` | `replicate count byte`; at most 8,192 bytes | | `fromIntBigEndian` | `Int -> Int -> Bytes` | `fromIntBigEndian width n`; width `0` is minimal; fails for negative `n` or if it does not fit | | `fromIntLittleEndian` | `Int -> Int -> Bytes` | as above, little-endian | | `toIntBigEndian` | `Bytes -> Int` | unsigned | | `toIntLittleEndian` | `Bytes -> Int` | unsigned | | `and` | `Bytes -> Bytes -> Bytes` | truncates to the shorter operand | | `or` | `Bytes -> Bytes -> Bytes` | truncates to the shorter operand | | `xor` | `Bytes -> Bytes -> Bytes` | truncates to the shorter operand | | `complement` | `Bytes -> Bytes` | flips every bit | | `shift` | `Int -> Bytes -> Bytes` | positive shifts towards the front, negative towards the end; same length | | `rotate` | `Int -> Bytes -> Bytes` | like `shift`, wrapping bits around | | `testBit` | `Int -> Bytes -> Bool` | bit 0 is the lowest bit of the last byte | | `countSetBits` | `Bytes -> Int` | | | `findFirstSetBit` | `Bytes -> Int` | lowest set bit, or `-1` if none | | `blake2b224` | `Bytes -> Bytes` | 28-byte digest | | `blake2b256` | `Bytes -> Bytes` | 32-byte digest | | `sha256` | `Bytes -> Bytes` | 32-byte digest | | `sha3_256` | `Bytes -> Bytes` | 32-byte digest | | `keccak256` | `Bytes -> Bytes` | 32-byte digest | | `ripemd160` | `Bytes -> Bytes` | 20-byte digest | `==` and `/=` compare byte strings. `<`, `<=`, `>`, `>=` and `compare` order them lexicographically, a proper prefix first, with the Plutus builtins `lessThanByteString` and `lessThanEqualsByteString`. There is no `Bytes.lessThan`: `ELCI2104: Elci has no Bytes.lessThan; bytes are comparable: write a < b`. Ledger hashes such as `PubKeyHash` are distinct types that compare and order like bytes; read their bytes with `.hash`. ## `Julc.Data` `import Julc.Data as Data`. Explicit conversions between Elci values and Plutus Data. | Function | Type | Behavior | | --- | --- | --- | | `decode` | `Data -> a` | Julc's strict decoder for `a`. Malformed Data fails the script. | | `encode` | `a -> Data` | The Data encoding of `a`. | | `fromOptional` | `JulcOptional a -> Maybe a` | Re-tags a Julc optional as a `Maybe`. | | `toOptional` | `Maybe a -> JulcOptional a` | The reverse. | ```elm import Julc.Data as Data import Julc.Ledger exposing (OutputDatum(..)) inlineAmount : OutputDatum -> Maybe Int inlineAmount datum = case datum of OutputDatumInline raw -> Just (Data.decode raw) _ -> Nothing ``` - **Supported types.** `Int`, `Bool`, `String`, `Bytes`, `Data`, your records and custom types, tuples, `Maybe`, `Result`, `Order`, lists, dictionaries, maps and optionals of these, a [`Value`](#value), and imported types whose provider approves decoding or encoding (for example `PubKeyHash` or `TxOutRef`). The rest are rejected with `ELCI3003`: `()`, functions, `Array`, and ledger types without a checkable layout, such as `TxOut` for `decode`. A type that contains a `Value` can't be decoded yet. - **Known types for decoding.** `decode` must know the type it creates where it is used. If inference cannot tell, annotate the binding (local definitions accept signatures). Otherwise the call fails with `ELCI3001: Cannot determine the type converted by Julc.Data.decode; add a type annotation`. `encode` and the optional adapters don't need to know it: `Data.encode Nothing` is `Constr 0`. - **Strict decoding.** `decode` never returns a `Maybe` for malformed input. A value of the wrong shape, such as an integer where a record is expected, fails with an error like `UnConstrData: not ConstrData`. A `Dict` whose keys are not strictly ascending fails too, and so does a `Value` that is not canonical. - **Optionals.** Julc's optional tags `Some` as 0 and `None` as 1, the reverse of `Maybe`. The adapters swap the tags without decoding the payload. - **Equality.** `==` and `/=` on `Data` compare structure exactly, with the `equalsData` builtin. Elci's encodings are deterministic, so `Data.encode a == Data.encode b` exactly when `a == b` for your records, custom types, lists and dictionaries. That makes a datum check one comparison: `Tx.inlineDatum output == Just (Data.encode expected)`. A `Map` has no `==`: two maps with the same bindings may hold them in different orders, so their encodings may differ too. Compare their `toList` after sorting, or use a `Dict`. `Data` is not comparable: `<` and sorting reject it. ```elm title="src/Main.elm" module Main exposing (main) import Dict import Julc.Data as Data import Map type alias Payout = { beneficiary : Int , amount : Int } main : ( Bool, Bool, ( Bool, Bool ) ) main = ( Data.encode (Payout 1 2) == Data.encode { amount = 2, beneficiary = 1 } , Data.encode (Just 5) == Data.encode 5 , ( Data.encode (Dict.fromList [ ( 1, 10 ), ( 2, 20 ) ]) == Data.encode (Dict.fromList [ ( 2, 20 ), ( 1, 10 ) ]) , Data.encode (Map.fromList [ ( 1, 10 ), ( 2, 20 ) ]) == Data.encode (Map.fromList [ ( 2, 20 ), ( 1, 10 ) ]) ) ) ``` ```text $ elci eval ( True, False, ( True, False ) ) cpu 62954744, mem 240991 ``` The record and the record literal with the same fields encode alike; `Just 5` and `5` do not. The two dictionaries hold the same bindings, so their encodings are equal; the two maps hold them in different orders, so theirs are not. The encoding rules are in the [language reference](https://elci-lang.dev/reference/language/#data-encoding). ## `Debug` Every module imports `Debug`. | Function | Type | Behavior | | --- | --- | --- | | `trace` | `String -> a -> a` | Emits the message as a Plutus trace and returns the second argument. | There is no `Debug.log`, `Debug.todo` or `Debug.toString`; the error says what to use instead, such as `ELCI2104: Elci has no Debug.log; use Debug.trace message value, which returns value`. Both arguments are evaluated first, because Elci is strict. The message is emitted only after the value has been computed, so `Debug.trace "about to divide" (10 // 0)` fails without tracing. To trace before something that may fail, trace in a `let`: ```elm main : Int main = let _ = Debug.trace "about to divide" () in 10 // 0 ``` ```text $ elci eval evaluation failed: QuotientInteger: division by zero cpu 431939, mem 1633 trace: about to divide ``` Traces cost CPU and script bytes in deployed validators. `elci test` prints the traces of failing tests. See [debugging and budgets](https://elci-lang.dev/testing/debugging/). ## `Julc.Ledger` `Julc.Ledger` provides the ledger types (`ScriptContext`, `TxInfo`, `PubKeyHash`, `TxOutRef`, `Credential` and more) rather than functions. See [reading the transaction](https://elci-lang.dev/contracts/reading-the-transaction/). Its `Value` is the type of the [`Value`](#value) module and its `Interval` the type of the [`Interval`](#interval) module, so `import Julc.Ledger exposing (Value)` names the same type. The [`Tx`](#tx) module answers the common questions about a `ScriptContext`. ## Differences from Elm | Elm | Elci | Why | | --- | --- | --- | | `String` ordered by UTF-16 code units | ordered by UTF-8 bytes, which is code point order | Plutus strings are UTF-8. The orders differ only for a character outside the Basic Multilingual Plane against one from U+E000 to U+FFFF | | `2 ^ -1 == 0.5` | fails the script | no `Float` | | `x // 0 == 0`; `modBy 0 x` crashes | both fail the script | Plutus division by zero | | `number`, `Float` | `Int` only | no floating point on-chain | | Bytes are not comparable | `Bytes` and ledger hashes are comparable | they are the natural keys on Cardano | | two imports exposing one name are ambiguous | a name listed in `exposing (...)` takes precedence over a default import | the intent is explicit | | `Dict` is a balanced tree: `get` and `insert` are O(log n) | `Dict` is a Plutus map with sorted keys: `get` and `insert` are O(n), `fromList` O(n log n) | a tree would not encode as a Plutus map, and rebalancing it on-chain costs more than scanning the small maps contracts use | | `Debug.log`, `Debug.todo`, `Debug.toString` | `Debug.trace message value` | traces are the on-chain log | | `Result` has no fixed encoding | `Ok` is constructor 0, `Err` constructor 1 | values cross the script boundary as Data | Elm's `Set` and `Char` modules and most of `String` are not available. [Coming from Elm](https://elci-lang.dev/learn/coming-from-elm/) lists every other difference. --- # Test library > Complete API of the Expect, Scenario and Property modules that elci test provides to test modules. Source: https://elci-lang.dev/reference/test-library/ `elci test` provides three modules, written in Elci, to your test modules: `Expect` for expectations, `Scenario` for transactions against your validators, and `Property` for property-based tests. This page lists every type and function they expose. For a guided introduction, start with [Unit tests](https://elci-lang.dev/testing/unit-tests/). Only test modules can import these modules. An import from a source module is an error at the import, `ELCI2101`: `elci test` reports `Only test modules can import Expect; it is part of the test library, not available to elci build`. A test module cannot be named `Expect`, `Scenario` or `Property`. ## Test discovery A test is an exposed, top-level declaration of a test module that is either: - a zero-argument value of type `Bool`, `Expect.Expectation`, `Scenario.Scenario` or `Property.Property`; or - a function whose result type is `Property.Property`, which runs as a property with generated arguments. Every other declaration is a helper. Modules run in alphabetical order and the tests of a module in source order. | `elci test` option | Effect | | --- | --- | | `--match ` | runs only the tests whose `Module.name` contains the substring, ignoring case | | `--seed ` | runs properties with seed `n`; without it, a random seed is used and printed | | `--fuzz ` | runs each property `runs` times (default 100; must be positive) | The command exits with status 1 when a test fails or when no test is found or matches. See also the [CLI reference](https://elci-lang.dev/reference/cli/). ## Expect ```elm import Expect exposing (Expectation) ``` ### Types ```elm type Expectation = Pass | Fail String (List Data) ``` A failure carries a message and the Data encodings of the values involved, which the test runner prints. The constructors are exposed, so a helper can match on an expectation with `import Expect exposing (Expectation(..))`. ### Functions | Function | Signature | Passes when | | --- | --- | --- | | `pass` | `Expectation` | always | | `fail` | `String -> Expectation` | never | | `equal` | `a -> a -> Expectation` | `expected == actual`, for `equal expected actual` | | `notEqual` | `a -> a -> Expectation` | `unexpected /= actual`, for `notEqual unexpected actual` | | `atMost` | `comparable -> comparable -> Expectation` | `actual <= bound`, for `atMost bound actual` | | `atLeast` | `comparable -> comparable -> Expectation` | `actual >= bound`, for `atLeast bound actual` | | `lessThan` | `comparable -> comparable -> Expectation` | `actual < bound`, for `lessThan bound actual` | | `greaterThan` | `comparable -> comparable -> Expectation` | `actual > bound`, for `greaterThan bound actual` | | `every` | `List Expectation -> Expectation` | every expectation in the list passes; otherwise the first failure is the result | | `onFail` | `String -> Expectation -> Expectation` | the expectation passes; otherwise its message is replaced, keeping its values | `equal` and `notEqual` require the same structural equality as `==`. Types without it, such as `Map`, functions and imported types without approved equality, are rejected at compile time with `ELCI2003`. `Data` has it: `Expect.equal (Data.encode expected) datum` compares structure. `atMost`, `atLeast`, `lessThan` and `greaterThan` take any [comparable type](https://elci-lang.dev/reference/core-modules/#comparable-types), as in elm-test: integers, strings, bytes, ledger hashes, and lists and tuples of these. ### Failure reports Values are printed as their Plutus Data encoding: integers as numbers, bytes and strings as `#hex`, lists as `[...]`, and records, tuples, `Bool`, `Maybe` and custom types as `Constr tag [fields]`. | Expectation | Report | | --- | --- | | `equal` | `Expect.equal`, then `expected:` and `actual:` | | `notEqual` | `Expect.notEqual`, then `values:` with the actual value | | `atMost`, `atLeast`, `lessThan`, `greaterThan` | the function name, then `values:` with the bound and the actual value | | `fail message` | the message | | `onFail message e` | the message, then the values of `e`, if any | ## Scenario ```elm import Scenario exposing (Scenario) ``` A scenario is data describing one transaction against a validator of the project. The runner compiles the named validator as `elci build` does, applies the scenario's parameters, builds the complete ledger context with julc-testkit and runs the deployed script. The validator's own address, minting policy and staking credential are derived from the applied script's hash. See [Transaction scenarios](https://elci-lang.dev/testing/scenarios/). ### Types ```elm type Scenario = Scenario Spec type alias Spec = { validator : String , purpose : Purpose , redeemer : Data , parameters : List Data , signers : List PubKeyHash , inputs : List Input , referenceInputs : List Input , outputs : List Output , minted : List Token , fee : Int , validFrom : Maybe Int , validBefore : Maybe Int , ownLovelace : Int , expectSuccess : Bool , budget : Maybe ( Int, Int ) } type Purpose = Spend TxOutRef (Maybe Data) | Mint | Withdraw | Certify Int TxCert | Vote Voter | Propose Int ProposalProcedure type Policy = Own | Policy Bytes type alias Token = { policy : Policy , name : Bytes , quantity : Int } type Destination = ToScript | ToAddress Address | ToStakedScript StakingCredential type alias Output = { destination : Destination , lovelace : Int , tokens : List Token , datum : OutputDatum , hashedDatum : Maybe Data } type alias Input = { ref : TxOutRef , output : Output } ``` `Policy` is the policy of a token: `Own` is the validator's own script hash, `Policy bytes` any other policy id. `Destination` is where an output is paid: the validator's own address, any address, or the validator's own payment credential with a staking part. An output's `datum` is its inline datum or none; `hashedDatum` holds a datum by hash, which the runner turns into the datum's hash on the output and the datum among the transaction's witnesses. The ledger types `TxOutRef`, `TxCert`, `Voter`, `ProposalProcedure`, `Address`, `StakingCredential`, `OutputDatum` and `PubKeyHash` come from `Julc.Ledger`. Build scenarios with the functions below rather than the `Spec` record. ### Start a scenario The first argument is the name of a `[validators.]` table. | Function | Signature | Transaction | | --- | --- | --- | | `spend` | `String -> TxOutRef -> Data -> Scenario` | spends the output at `ref`, held at the validator's address with an inline datum | | `spendWithoutDatum` | `String -> TxOutRef -> Scenario` | spends the output at `ref`, held at the validator's address without a datum | | `mint` | `String -> Scenario` | mints or burns under the validator's own policy; add tokens with `minting` | | `withdraw` | `String -> Scenario` | withdraws from the validator's own staking credential | | `certify` | `String -> Int -> TxCert -> Scenario` | publishes a certificate at an index | | `vote` | `String -> Voter -> Scenario` | votes as the given voter | | `propose` | `String -> Int -> ProposalProcedure -> Scenario` | submits a proposal at an index | A new scenario has these defaults: | Field | Default | | --- | --- | | redeemer | `unit`, the Data of `()` | | parameters, signers, inputs, reference inputs, outputs, minted tokens | none | | fee | 200000 lovelace | | validity range | unbounded on both sides | | lovelace in the spent output | 2000000 | | expectation | the validator accepts; no budget limit | ### Describe the transaction Each function takes the scenario last, so they chain with `|>`. Functions that set a value replace any earlier value; functions that add to a list append in call order. | Function | Signature | Effect | | --- | --- | --- | | `redeemer` | `Data -> Scenario -> Scenario` | sets the redeemer | | `parameters` | `List Data -> Scenario -> Scenario` | sets the deployment parameters, in the validator's declared order; all of them are required | | `signedBy` | `PubKeyHash -> Scenario -> Scenario` | adds a required signer | | `input` | `TxOutRef -> Output -> Scenario -> Scenario` | adds an input; an input at the spent reference replaces the default spent output, and must hold the datum the spend gives | | `referenceInput` | `TxOutRef -> Output -> Scenario -> Scenario` | adds a reference input | | `output` | `Output -> Scenario -> Scenario` | adds an output | | `minting` | `Bytes -> Int -> Scenario -> Scenario` | mints (positive quantity) or burns (negative) a token under the own policy | | `fee` | `Int -> Scenario -> Scenario` | sets the fee in lovelace | | `validFrom` | `Int -> Scenario -> Scenario` | sets the inclusive lower bound of the validity range, in POSIX time | | `validBefore` | `Int -> Scenario -> Scenario` | sets the exclusive upper bound of the validity range, in POSIX time | | `worth` | `Int -> Scenario -> Scenario` | sets the lovelace held by the default spent output | ### Set the expectation | Function | Signature | Effect | | --- | --- | --- | | `expectFailure` | `Scenario -> Scenario` | the validator must reject the transaction | | `withinBudget` | `Int -> Int -> Scenario -> Scenario` | the execution must cost at most this many CPU steps and memory units, whether it succeeds or fails | ### Build outputs | Function | Signature | Result | | --- | --- | --- | | `toScript` | `Int -> Output` | an output of this many lovelace to the validator's own address | | `toStakedScript` | `StakingCredential -> Int -> Output` | an output of this many lovelace to the validator's own payment credential with a staking part, an address other than `toScript`'s | | `toAddress` | `Address -> Int -> Output` | an output of this many lovelace to an address | | `toKey` | `PubKeyHash -> Int -> Output` | an output of this many lovelace to the key's address, without a staking part | | `withDatum` | `Data -> Output -> Output` | the output with an inline datum; replaces a datum set before | | `withDatumHash` | `Data -> Output -> Output` | the output with the datum's hash, and the datum among the transaction's witnesses, where `Tx.datumOf` finds it; replaces a datum set before | | `withToken` | `Policy -> Bytes -> Int -> Output -> Output` | the output with an added token: policy, name and quantity | ### Test data | Function | Signature | Result | | --- | --- | --- | | `key` | `String -> PubKeyHash` | the BLAKE2b-224 hash of the name's UTF-8 bytes: a deterministic 28-byte test key | | `outRef` | `String -> Int -> TxOutRef` | an output reference whose transaction id is the BLAKE2b-256 hash of the name's UTF-8 bytes | | `unit` | `Data` | the Data of `()`, `Constr 0 []` | ### Scenario failure reports | Report | Meaning | | --- | --- | | `validator rejected the transaction: ` | the validator failed, and the scenario expected success | | `validator accepted a transaction expected to fail` | the validator succeeded after `expectFailure` | | `Budget exceeded: ...` | the execution went over the `withinBudget` limits | | `cannot prepare validator '': ...` | the validator does not exist, or the scenario does not give all of its parameters; the scenario fails before it runs, even with `expectFailure` | | `cannot build the transaction: ...` | the ledger could not produce the scenario's transaction, such as one with a token name longer than 32 bytes, or a listed spent output whose datum differs from the one `spend` gives; the scenario fails before it runs, even with `expectFailure` | ## Property ```elm import Property exposing (Property) ``` ### Types ```elm type Property ``` `Property` is opaque: build it with the functions below. ### Functions | Function | Signature | The property holds when | | --- | --- | --- | | `holds` | `Bool -> Property` | the condition is `True`; otherwise it fails with `the property does not hold` | | `expect` | `Expectation -> Property` | the expectation passes; otherwise it fails with the expectation's report | | `scenario` | `Scenario -> Property` | the scenario has its expected outcome: accepted, or rejected after `Scenario.expectFailure` | ### Property reports A passing property reports its number of runs and the budget of its costliest run, as `(100 runs, max cpu ..., mem ...)`. A failing property reports the runs until the failure and the shrunk counterexample, printed as a call in Elci syntax, followed by the failure of that run: ```text FAIL PricingProperties.quotesNeverLoseValue (15 runs, cpu 5409589, mem 21823) counterexample after 15 runs, shrunk 4 times: quotesNeverLoseValue (-334) the property does not hold ``` A property's parameters must be concrete, Data-encodable types. Functions, arrays, `ScriptContext` and type variables are rejected with `ELCI2009`. [Property-based tests](https://elci-lang.dev/testing/properties/) describes the generated values for each type. --- # Language reference > Elci's feature matrix, syntax, imports and reserved names, operators and precedence, equality and ordering, constrained type variables, evaluation order and Plutus Data encoding rules. Source: https://elci-lang.dev/reference/language/ This page is the compact reference for the Elci language: what is supported, how the syntax looks, how names resolve, how operators bind, which types compare, in which order things are evaluated and how values are encoded as Plutus Data. For a gentle introduction, start with [values, functions and types](https://elci-lang.dev/learn/basics/). Elci uses Elm 0.19 syntax for a functional core, with its own on-chain semantics. It is a development preview: the profile below is implemented and tested, and everything outside it is rejected with an error. The [grammar](https://elci-lang.dev/reference/grammar/) defines the syntax exactly. ## Feature matrix | Feature | Support | | --- | --- | | Modules and imports | Source modules, `as` aliases, explicit `exposing`, `exposing (..)` for source modules, `Type(..)` for constructors, and Elm's default imports. Import cycles are rejected. | | Functions | Top-level and local functions, lambdas with several arguments, curried and partial application, operators as functions such as `(+)`. | | Type signatures | Required on every top-level declaration; allowed on local ones. | | Generics | Rank-1 type variables, specialized to each concrete use. Elm's constrained variables `comparable`, `appendable` and `compappend`. | | Local polymorphism | Simple local functions and values can be used at several types; calls, conditionals and matches stay monomorphic. | | Recursion | Self-recursion and mutually recursive functions, top-level or local. Polymorphic recursion and recursive values are rejected. | | Basic types | `Int` (arbitrary precision), `Bool`, `String`, `()`, `Bytes`, `Data`. | | Core library | The `Basics` prelude and the `List`, `Maybe`, `Result`, `Tuple` and `String` modules, imported by default. See [core modules](https://elci-lang.dev/reference/core-modules/). | | Lists | `List a`, literals, `::`, `++` and list patterns. | | Tuples | Pairs and triples, with patterns and destructuring. | | Records | Closed records, generic aliases with constructors, field access, accessor functions such as `.name`, update, destructuring. | | Custom types | Generic and recursive unions; `Maybe` is built in, and `Result` and `Order` come from the core library. | | Patterns | Nested constructors, lists, cons, tuples, record fields, `as`, `_`, unit, and `Bool`, `Int` and `String` literals. Exhaustiveness and reachability are checked. | | Equality and order | Structural `==` and `/=` for equality-capable types, and `<`, `compare` and sorting for comparable types (below). | | Dictionaries, maps and arrays | `Dict k v` (Elm's `Dict`, stored as a sorted Plutus map), `Map k v` (the ledger's association map) and `Array a` (constant-time `get`). | | Values | `Value`, the ledger's multi-asset value, with the core `Value` module: arithmetic, comparison and exact `==` on the CIP-153 builtins. | | Bytes | The `Bytes` module: every byte-string builtin. | | Ledger access | Typed views of the script context, nominal hash types, approved constructors, patterns and equality. The core `Tx` and `Interval` modules answer common questions about the transaction and its validity range. | | Data conversions | `Julc.Data.decode` and `encode`, optional adapters, and structural `==` on `Data`. | | Java libraries | Julc's on-chain Java libraries and your own library JARs, including generic methods. | | Validators | `[validators.]` scripts with spend, mint, withdraw, certify, vote and propose handlers, parameters and datum profiles. | | Tests | `elci test` with unit tests, transaction scenarios and property tests. | **Not supported:** `Float`, `/` and `number`, typeclasses and user-defined constraints, extensible records and row polymorphism, higher-rank types, polymorphic recursion, ports and effects, Elm's `Set` and `Char` modules and most of `String`, character literals, hexadecimal literals, `%`, tuples of four or more, and Elm packages. ## Syntax summary This module uses most of the syntax: ```elm title="src/Main.elm" module Main exposing (main) {-| A tour of the syntax. Block comments {- can {- nest -} -}. -} type Shape = Circle Int | Rect Int Int type alias Point = { x : Int, y : Int } type alias Pair a = ( a, a ) area : Shape -> Int area shape = case shape of Circle radius -> 3 * radius * radius Rect width height -> width * height describe : List Int -> String describe values = case values of [] -> "empty" [ _ ] -> "one" 0 :: _ -> "starts with zero" first :: _ -> if first < 0 then "starts negative" else "many" norm : Point -> Int norm { x, y } = x * x + y * y swap : Pair a -> Pair a swap ( first, second ) = ( second, first ) main : Int main = let origin = Point 0 0 moved = { origin | x = 3, y = 4 } ( a, b ) = swap ( 1, 2 ) total : Int total = [ Circle 1, Rect 2 3 ] |> List.map area |> List.foldl (\value sum -> value + sum) 0 in if describe [ 0, 1 ] == "starts with zero" && not (a == b) then norm moved + total else -1 ``` | Construct | Syntax | | --- | --- | | Module header | `module A.B exposing (f, T, U(..))` or `exposing (..)` | | Import | `import A.B as B exposing (f, T(..))` | | Signature and definition | `f : Int -> Int` then `f x = x + 1` | | Lambda | `\x y -> x + y`, `\( a, b ) -> a + b` | | Local definitions | `let` ... `in`, with optional signatures and tuple/record destructuring | | Conditional | `if c then a else b` (the `else` is required) | | Match | `case e of` followed by aligned `pattern ->` branches | | Custom type | `type T a = A a \| B` | | Type alias | `type alias R = { x : Int }`; a record alias is also a constructor `R 1` | | Record | `{ x = 1, y = 2 }`, `r.x`, `{ r \| x = 3 }` | | Accessor function | `.x`, which is `\r -> r.x`: `List.map .x points` | | Field of a qualified value | `Config.settings.x` | | Tuple | `( 1, True )`, `( 1, 2, 3 )` | | List | `[ 1, 2 ]`, `1 :: rest`, `xs ++ ys` | | Operator as a function | `(+)`, `(::)`, `(<)`: `List.foldl (+) 0 xs` | | Unit | `()` as a value, type and pattern | Function and lambda parameters, and local destructuring, accept only **irrefutable** patterns: names, `_`, `()`, tuples and record fields. Anything that can fail to match belongs in `case`. ### Layout Indentation is significant, as in Elm. Top-level declarations start in column 1. The declarations of a `let` align with each other, and so do the branches of a `case`. A signature must directly precede its definition. Tabs are rejected. The grammar gives the [exact layout rules](https://elci-lang.dev/reference/grammar/#layout). ### Lexical rules - **Names.** Values and functions start with a lowercase letter; types, constructors and module segments start with an uppercase letter. Letters, digits and `_` may follow. - **Reserved words.** `if`, `then`, `else`, `case`, `of`, `let`, `in`, `module`, `import`, `exposing`, `as`, `type` and `port`. `alias` is only special after `type`. Don't use them as names: the parser doesn't yet reject every such use, as the grammar's [parser details](https://elci-lang.dev/reference/grammar/#parser-details-that-differ-from-elm) describe. - **Integers.** Decimal digits only, arbitrarily large. `1.5` is rejected (`Float is unsupported`), and so are `0xFF` and `1_000`. elm-format 0.8.8 corrupts literals that do not fit in 64 bits, so check large constants after formatting. - **Strings.** Double-quoted, on one line. The escapes are `\n`, `\r`, `\t`, `\"` and `\\`. There are no triple-quoted strings, Unicode escapes or character literals. - **Comments.** `--` to the end of the line, and `{- ... -}` blocks, which nest. `{-| ... -}` doc comments are ordinary block comments. ## Imports and names Every module has Elm's default imports, which you don't write: ```elm import Basics exposing (..) import List import Maybe exposing (Maybe(..)) import Result exposing (Result(..)) import String import Tuple import Debug ``` `Bytes`, `Dict`, `Map`, `Array`, `Value`, `Interval`, `Tx`, `Julc.Data` and Java library modules are imported explicitly. Names resolve as in Elm, with one addition: - A module's own declarations shadow every import. - Two imports that expose different things under one name make it ambiguous where it is used (`ELCI2102`). The message lists the qualified alternatives. - *Addition:* a name listed explicitly, as in `import Utils exposing (clamp)`, takes precedence over a default import. - Qualified names such as `Basics.max` and `Result.Ok` always work. A qualified name is written without spaces and ends at its first lowercase part, so `Config.settings.fee` is the field `fee` of the value `Config.settings`. - A qualified name that doesn't resolve is `ELCI2104`, with one of three messages: the module doesn't expose the name, the module isn't imported (with the import to add), or no imported module has that name. See [diagnostics](https://elci-lang.dev/reference/diagnostics/#elci2101-elci2102-and-elci2104). - An import can't be aliased to the name of another module, core or in the project: `import Utils as String` and `import Julc.Interval as Interval` are `ELCI2104`. [Core modules](https://elci-lang.dev/reference/core-modules/#default-imports) has the details and the messages. ### Reserved names - **Module names.** `Basics`, `List`, `Maybe`, `Result`, `String`, `Tuple`, `Map`, `Dict`, `Array`, `Value`, `Interval` and `Tx` are core modules, and `Set` and `Elci` are reserved for the core library. Neither a project module nor a `[java-modules]` key can use these names, or a name that starts with `Elci.`: ```text src/String.elm:1:1: ELCI2104: The module name String is taken by Elci's core library; rename this module src/Value.elm:1:1: ELCI2104: The module name Value is taken by Elci's core library; rename this module src/Tx.elm:1:1: ELCI2104: The module name Tx is taken by Elci's core library; rename this module src/Set.elm:1:1: ELCI2104: The module name Set is reserved for Elci's core library; rename this module src/Elci/Tools.elm:1:1: ELCI2104: The module name Elci.Tools is reserved for Elci's core library; rename this module elci: ELCI4001: [java-modules] may not map Value, a module name of Elci's core library ``` `Bytes`, `Debug` and `Julc.Data` belong to the compiler as well. - **Constructors and types.** A module can't declare constructors named `Just`, `Nothing`, `True` or `False`, or types named `Int`, `Bool`, `String`, `Bytes`, `Data`, `List`, `Maybe`, `Dict`, `Map`, `Array` or `Float`: ```text src/Main.elm:4:6: ELCI2011: Nothing is a constructor of the built-in Maybe type; choose another name src/Main.elm:4:6: ELCI2011: Bytes is a built-in type; choose another name ``` Other prelude names, such as `Ok`, `Order` or `max`, can be declared; your declaration shadows the prelude's in your module. - **Type names.** Tuples are written `( a, b )` and unit `()`. `Tuple Int Int` gives `ELCI2012: Write tuple types as ( a, b ) or ( a, b, c )`, and `Unit` gives `ELCI2012: Write the unit type as ()`. ## Operators From the tightest binding to the loosest: | Precedence | Operators | Associativity | Meaning | | ---: | --- | --- | --- | | highest | `record.field` | | field access, written without spaces | | 10 | `f x` | left | function application | | 9 | `>>`, `<<` | `>>` right, `<<` left | function composition | | 8 | `^` | right | integer power | | 7 | `*`, `//` | left | multiplication, integer division truncating towards zero | | 6 | `+`, `-` | left | addition, subtraction | | 5 | `::`, `++` | right | list cons; append lists or strings | | 4 | `==`, `/=`, `<`, `>`, `<=`, `>=` | none | comparisons; chaining is an error | | 3 | `&&` | right | and, short-circuit | | 2 | `\|\|` | right | or, short-circuit | | 0 | `\|>`, `<\|` | `\|>` left, `<\|` right | pipelines | Operators of one precedence that group in opposite directions can't be mixed without parentheses, as in Elm: `f <| x |> g` is `ELCI1002: Mixing (<|) and (|>) needs parentheses: they associate in opposite directions, as in Elm`, and the same holds for `>>` with `<<`. - `+`, `-`, `*`, `//` and `^` work on `Int`. `2 ^ 10` is `1024`, `2 * 3 ^ 2` is `18`, and a negative exponent fails the script. - `<`, `>`, `<=` and `>=` compare two values of the same [comparable type](#ordering). - `==` and `/=` work on equality-capable types (below). - `/` is rejected; use `//` for the integer quotient, and `modBy` or `remainderBy` for the rest. `%` does not exist. - `a < b < c` is rejected: write `a < b && b < c`. ### Operators as functions Every binary operator can be used as a function by writing it in parentheses, as in Elm: `(+)`, `(*)`, `(::)`, `(++)`, `(<)`, `(^)`, `(|>)`. `(op)` is the function `\a b -> a op b`, so `(-) 10` subtracts from 10. `(/)` is rejected, like `/`. As functions, `(&&)` and `(||)` evaluate both arguments, like any other call. ```elm title="src/Main.elm" module Main exposing (main) main : ( Int, List Int, List Bool ) main = ( List.foldl (+) 0 [ 1, 2, 3 ] , List.foldr (::) [] [ 4, 5 ] ++ List.map ((-) 10) [ 1, 2 ] , List.map2 (<) [ 1, 5 ] [ 2, 2 ] ) ``` ```text $ elci eval ( 6, [4, 5, 9, 8], [True, False] ) cpu 16223362, mem 77496 ``` ### Unary minus Negation follows Elm 0.19. A `-` written directly before its operand, after a space or an opening bracket, negates it: `f -1` passes `-1` to `f`, and `scale -x 10` passes `-x`. With a space after it, or with no space before it, `-` subtracts: `x - 1` and `x-1` are both subtractions. A leading `-` negates the whole function application that follows, so `-(scale x 2)` and `-f x` are negations of the result. ```elm title="src/Main.elm" module Main exposing (main) scale : Int -> Int -> Int scale value factor = value * factor main : List Int main = let x = 4 in [ scale -x 10 , modBy 3 -7 , x - 1 , -x * 2 , -(scale x 2) , 2 * -3 ] ``` ```text $ elci eval [-40, 2, 3, -8, -8, -6] cpu 2719341, mem 9131 ``` This is also how elm-format writes negative arguments, so formatted code keeps its meaning. As in Elm, `x -1` is an application of `x` to `-1`, not a subtraction. ## Equality `==` and `/=` work on `Int`, `Bool`, `String`, `()`, `Bytes`, ledger hashes of the same type, `Value`, `Data`, and lists, tuples, records, `Maybe`, `Dict` and custom types whose contents are all equality-capable. Two dictionaries are equal when they have the same bindings, and two values when they hold the same quantities: every `Value` is kept in one canonical form, so `==` compares their encodings. `Data` compares structure exactly, so `Tx.inlineDatum output == Just (Data.encode expected)` checks a datum without decoding it. Imported types use the equality their provider approves (Java `equals`). Both operands are evaluated once, left to right. Rejected with `ELCI2003`: functions, `Map`, `Array`, imported types without approved equality, and the ledger records that contain a `Value` (`TxOut`, `TxInInfo`, `TxInfo`). Two maps with the same bindings may hold them in different orders, so `Data.encode` of the two may differ: compare their sorted `toList`, or use a `Dict`. The message names the part that has no equality: ```text src/Main.elm:6:21: ELCI2003: The operator == cannot compare List (Int -> Int) values: Int -> Int is part of them, and functions have no equality ``` ## Ordering `<`, `<=`, `>`, `>=`, `compare`, `min`, `max` and the sorting functions take two values of the same **comparable** type: - `Int`, ordered numerically; - `String`, ordered by its UTF-8 bytes, which is Unicode code point order; - `Bytes` and the ledger's byte-string types (`PubKeyHash`, `ScriptHash`, `PolicyId`, `TokenName`, `TxId` and the other hashes), ordered lexicographically by unsigned byte, a proper prefix first; - lists of a comparable type, lexicographically, and tuples of comparable types, component by component. `Bool`, `()`, records, custom types, `Maybe`, `Dict`, `Map`, `Array`, `Value`, `Data`, functions and ledger records such as `TxOutRef` are not comparable (`ELCI2003`). Every order is total and agrees with `==`. Comparing `Int` values uses the integer builtins; byte strings use `lessThanByteString` and `lessThanEqualsByteString`; `String` encodes both sides to UTF-8 and compares the bytes; lists and tuples use a comparison function generated once per type. ## Constrained type variables As in Elm, a type variable in a signature is unconstrained unless its name says otherwise: | Name | Values must support | Example | | --- | --- | --- | | `comparable`, or a name starting with it (`comparable2`, `comparableKey`) | `<`, `compare` and sorting | `max : comparable -> comparable -> comparable` | | `appendable…` | `++` (strings and lists) | `twice : appendable -> appendable` | | `compappend…` | both | | The name is also a requirement on callers: each use is checked at its concrete type. ```elm title="src/Main.elm" module Main exposing (main) largest : comparable -> List comparable -> comparable largest first rest = List.foldl max first rest twice : appendable -> appendable twice value = value ++ value main : ( String, List Int ) main = ( twice (largest "fig" [ "apple", "pear" ]), twice [ largest 3 [ 9, 4 ] ] ) ``` ```text $ elci eval ( "pearpear", [9, 9] ) cpu 8891872, mem 43250 ``` Comparing or appending values of a plain variable is an error that suggests the rename: ```text src/Main.elm:6:10: ELCI2003: The operator > compares values of type `a`, but the annotation of `larger` says `a` can be any type. Rename it `comparable` (as in Elm) to require comparable values ``` A requirement that no type can meet, such as `x < y` on values of type `Maybe comparable`, is reported at the definition. A caller that passes the wrong type is reported at the call, and Elm's `number` gets its own message, because Elci has one number type: ```text src/Main.elm:11:5: ELCI2003: `twice` appends String or List values, not Int src/Main.elm:4:1: ELCI2003: The annotation of `total` uses `number`, Elm's type variable for Int or Float. Elci has one number type: write Int ``` Equality needs no special name, as in Elm: `List.member : a -> List a -> Bool` uses `==`, and each use is checked at its type. Type and alias parameters are never constrained, and a definition without a signature, such as a local helper, infers its requirements. ## Ambiguous types A type variable that nothing in a program determines, such as the element type of `[] == []` or the error type of `Ok 5`, is *ambiguous*. As in Elm, such programs compile without annotations: no value of that type can exist, so any representation serves. `List.length []` is `0`, `Result.withDefault 0 (Ok 5)` is `5`, and `Julc.Data.encode Nothing` is `Constr 0`. Creating a value of the type still needs to know it. `Julc.Data.decode` and generic Java library functions report `ELCI3001` and ask for an annotation: ```elm import Julc.Data as Data main : Int main = List.length (Data.decode (Data.encode [ 1, 2 ])) ``` ```text src/Main.elm:8:18: ELCI3001: Cannot determine the type converted by Julc.Data.decode; add a type annotation ``` Annotating the decoded value, as in a local `amounts : List Int`, fixes it. ## Evaluation order Elci is strict. These rules are part of the language, because they decide which failures and costs happen: 1. **Application.** The function, then its arguments from left to right. A partial application evaluates the arguments it has immediately. 2. **`let`.** Every binding is evaluated, even if unused, before the body. Bindings run in dependency order; independent ones run in source order. 3. **Literals and constructors.** List elements, tuple components, record fields and constructor arguments are evaluated from left to right, in source order. Record order does not follow the field names. 4. **Record update.** The base record first, then the new field values in source order. 5. **Selective forms.** `if` evaluates the condition and one branch. `case` evaluates the scrutinee once and one branch. `&&` and `||` evaluate the right operand only when needed. 6. **Composition.** `f >> g` evaluates `f` and `g` when the composition is built. 7. **Validators.** Top-level values reachable from any handler are evaluated once per script run, before the purpose is dispatched. This program traces every step: ```elm title="src/Main.elm" module Main exposing (main) type alias Order = { price : Int, amount : Int } add : Int -> Int -> Int add left right = left + right main : Int main = let total = Debug.trace "total" (subtotal + 1) subtotal = Debug.trace "subtotal" 10 unrelated = Debug.trace "unrelated" 0 order = { price = Debug.trace "price" 5, amount = Debug.trace "amount" 2 } sum = add (Debug.trace "left" 1) (Debug.trace "right" 2) same = Debug.trace "eq-left" 1 == Debug.trace "eq-right" 1 in if same then total + order.price + sum else 0 ``` ```text $ elci eval 19 cpu 3661600, mem 13949 trace: subtotal trace: total trace: unrelated trace: price trace: amount trace: left trace: right trace: eq-left trace: eq-right ``` `subtotal` runs before `total` because `total` needs it, and `unrelated` runs although nothing uses it. ## Data encoding Values that cross the script boundary (datums, redeemers, parameters) or pass through `Julc.Data.encode` use a fixed encoding: | Elci value | Plutus Data | | --- | --- | | `Int` | integer | | `Bytes`, ledger hashes | byte string | | `String` | byte string with the UTF-8 encoding | | `Bool` | constructor 0 (`False`) or 1 (`True`), no fields | | `()` | constructor 0, no fields (as a datum or redeemer; `Julc.Data.encode ()` is rejected) | | record | constructor 0, fields **sorted by field name** | | tuple | constructor 0, fields in position order | | custom type | constructor tag = the variant's position in the declaration, from 0; fields in order | | `Maybe a` | `Nothing` is constructor 0; `Just x` is constructor 1 with one field | | `Result e a` | `Ok x` is constructor 0 and `Err e` constructor 1, each with one field | | `Order` | `LT`, `EQ` and `GT` are constructors 0, 1 and 2, no fields | | `JulcOptional a` | `Some x` is constructor 0 with one field; `None` is constructor 1 | | `List a` | list | | `Dict k v` | map with its keys in strictly ascending order | | `Map k v` | map, in the map's order | | `Value` | map from policy ids to maps from token names to quantities, both in ascending byte order, without zero quantities | | imported record or sum | as declared in Java: constructor in declaration order, fields in declaration order | Functions and arrays have no encoding. A `Value` can't be a datum, redeemer or parameter yet, and a type that contains one can't be decoded. You can see the encoding of any value with `Julc.Data.encode` and `elci eval`, which prints Data as `Constr tag [fields]`, lists, `{key: value}` maps, integers and `#bytes`: ```elm title="src/Main.elm" module Main exposing (main) import Julc.Data as Data type Action = Buy Int | Cancel type alias Payout = { beneficiary : Int, amount : Int } main : List Data main = [ Data.encode (Just 5) , Data.encode Cancel , Data.encode (Buy 3) , Data.encode { beneficiary = 1, amount = 2 } , Data.encode ( 1, True ) , Data.encode [ 1, 2 ] , Data.encode (Data.toOptional (Just 5)) , Data.encode "hi" ] ``` ```text $ elci eval [Constr 1 [5], Constr 1, Constr 0 [3], Constr 0 [2, 1], Constr 0 [1, Constr 1], [1, 2], Constr 0 [5], #6869] cpu 5878521, mem 25242 ``` The record `{ beneficiary = 1, amount = 2 }` encodes as `Constr 0 [2, 1]`, because `amount` sorts before `beneficiary`. Decoding is strict: tags, field counts and nested field kinds are checked, and a mismatch fails the script. A `Dict`, at any depth, must have strictly ascending keys: a map whose keys are out of order or repeated fails to decode. A `Value` must be canonical: its policies and token names strictly ascending, and no quantity zero. Each build records the encoding of its boundary types in `abi.json`; see [datums and redeemers](https://elci-lang.dev/contracts/datums-and-redeemers/). ## Compiler limits - A program may create at most 256 specializations of generic functions. - Tuples have two or three elements. - The only target is `plutus-v3-pv11-uplc-1.1.0`. For the error codes these rules produce, see [diagnostics](https://elci-lang.dev/reference/diagnostics/). --- # Grammar > Elci's formal grammar in EBNF — the lexical grammar, the exact layout rules, the syntactic grammar, operator precedence, the adjacency rules for `.` and `-`, and the Elm syntax Elci rejects. Source: https://elci-lang.dev/reference/grammar/ This page is Elci's formal grammar: the lexical grammar, the layout rules and the syntactic grammar. It describes what the compiler's parser accepts, including the edge cases. For a summary of the syntax, see the [language reference](https://elci-lang.dev/reference/language/#syntax-summary); for what programs mean, the rest of that page. The grammar covers syntax only. A program that matches it can still be rejected later, for example for a type error, a missing signature or an unknown name. The same grammar is kept in the repository as [`docs/GRAMMAR.md`](https://github.com/bloxbean/elci-lang/blob/main/docs/GRAMMAR.md). A test parses every complete module it shows, and the syntax corpus has a module for each area of the grammar. The repository also has a [tree-sitter grammar](https://github.com/bloxbean/elci-lang/tree/main/tree-sitter-elci) that follows it, for highlighting and folding in Neovim, Helix and Zed. ## Notation The grammar uses the W3C EBNF notation of the XML 1.0 specification (section 6): | Notation | Meaning | | --- | --- | | `A ::= ...` | defines the symbol `A` | | `"text"` | the literal text | | `[abc]`, `[^abc]` | one character from the set, or one not in it | | `#xN` | the character with hexadecimal code point `N` | | `A B` | `A` followed by `B` | | `A \| B` | `A` or `B` | | `A?`, `A*`, `A+` | `A` optionally, zero or more times, one or more times | | `A - B` | what `A` matches, except what `B` matches | | `( ... )` | grouping | | `/* ... */` | a comment | Elci's layout needs two additions, used only in the syntactic grammar: | Notation | Meaning | | --- | --- | | `X` | the symbol `X` read with the *floor* `f`, a column (see [Layout](#layout)); `X` has no floor | | `CONT` | matches nothing; allowed only when the next token is not a *boundary* for the floor `f` | | `COL` | matches nothing; allowed only when the next token is in column `c` | The lexical grammar is over characters. The syntactic grammar is over tokens: a quoted string there means a token with that text, and the lexer has already removed whitespace and comments. ## Lexical grammar The lexer reads the source from left to right and produces tokens. Between tokens it skips whitespace and comments. At each position it tries the rules in this order: whitespace, line comment, block comment, name, integer, string, symbol. ### Whitespace and positions ```text Space ::= /* a character for which Java's Character.isWhitespace is true, except tab */ Tab ::= #x9 ``` - A tab outside a string or comment is an error: `ELCI1001: Tabs are not permitted; use spaces for Elm layout`. Tabs inside strings and comments are allowed. - Every token has a line and a column, both counted from 1. The column counts characters (Java `char`s) from the start of the line. A newline is `#xA`; `#xD` is whitespace, so CRLF line endings work. ### Comments ```text LineComment ::= "--" [^#xA]* BlockComment ::= "{-" CommentText ( BlockComment CommentText )* "-}" CommentText ::= Char* - ( Char* ( "{-" | "-}" ) Char* ) Char ::= /* any character */ ``` - Block comments nest: `{- a {- b -} c -}` is one comment. - The lexer scans a block comment from left to right: each `{-` opens a nested comment, and each `-}` closes one. So `{- a{-}` is unterminated, since its `{-}` opens a comment (`ELCI1001: Unterminated block comment`). - `{-| ... -}`, Elm's doc comment, is an ordinary block comment. The compiler does not read it. - An unterminated block comment is `ELCI1001: Unterminated block comment`. - Comments are not tokens. They take no part in layout: a token after a comment is placed by its own line and column. ### Names and reserved words ```text Letter ::= /* a character for which Character.isLetter is true */ Digit ::= /* a character for which Character.isDigit is true */ Name ::= ( Letter | "_" ) ( Letter | Digit | "_" )* LowerName ::= Name - Reserved /* whose first character is a lowercase letter */ UpperName ::= Name /* whose first character is an uppercase letter */ Reserved ::= "then" | "else" | "in" | "of" | "module" | "import" | "exposing" | "as" | "type" | "port" ``` - `_` alone is the wildcard pattern. - `if`, `case` and `let` are keywords where an expression starts. `alias` is a keyword only after `type`. `True` and `False` are the Boolean literals. ### Integers ```text Int ::= Digit+ ``` - An integer is decimal and has no size limit. - A `.` followed by a digit right after an integer is an error: `ELCI1001: Float is unsupported; represent fractional amounts using an explicit integer scale`. - `0xFF` and `1e3` are not literals: they read as the integer `0` or `1` followed by a name. ### Strings ```text String ::= '"' ( StringChar | Escape )* '"' StringChar ::= Char - ( '"' | "\" | #xA | #xD ) Escape ::= "\" ( "n" | "r" | "t" | '"' | "\" ) ``` - A string is on one line. A raw line break in it is `ELCI1001: Use an escaped newline in a string`. - Any other escape, such as `\u{0041}`, is `ELCI1001: Unsupported string escape`. - There are no character literals and no triple-quoted strings. ### Symbols ```text Symbol ::= "->" | "//" | "==" | "/=" | "<=" | ">=" | "&&" | "||" | "::" | "++" | "|>" | "<|" | ">>" | "<<" | ".." | "(" | ")" | "[" | "]" | "{" | "}" | "," | ":" | "=" | "|" | "." | "\" | "+" | "-" | "*" | "<" | ">" | "/" | "^" ``` - The two-character symbols are tried first, so `a+++b` reads as `a ++ + b`. - Any other character is `ELCI1001: Unsupported character 'c'`, for example `%`, `'`, `` ` `` and `!`. - `/` is a symbol but not an operator: the parser rejects it (see [Operators](#operators)). ## Layout Indentation is significant. The parser reads it from token columns; it does not insert layout tokens. These are the exact rules. **New line.** A token *starts a new line* when its line is after the line of the token before it. **Floors and boundaries.** Every expression and type is read with a *floor* `f`, a column. A token is a *boundary* for the floor `f` when it is the end of the input, or when it starts a new line in column `f` or less. A boundary ends the expression or type being read. `none` is a floor that no token reaches: only the end of the input is a boundary for it. **Where boundaries are checked.** The parser looks for a boundary only at these points, written `CONT` in the grammar: - in an expression, before a binary operator, before a function argument (including a negative argument such as `-1` in `f -1`) and before a field access `.field`; - in a type, before each argument of a type application; - in a custom type, before each argument type of a constructor. Everything else is read at any column: the first token of an expression; the keywords `then`, `else`, `of` and `in`; the symbols `->`, `=`, `:`, `|`, `,` and the closing brackets; every token of a pattern; and the module header, imports and exposing lists. **Blocks.** Three places start a block whose items align at one column `c`: | Block | Column `c` | Items | The block ends | | --- | --- | --- | --- | | The module | 1 | declarations | at the end of the input | | `let` | the column of the first token after `let` | declarations | at `in`, in any column | | `case` | the column of the first token after `of` | branches | at a token not in column `c`, or at `in`, `else` or the end of the input | - Each item starts in column `c`. After an item, the next token must end the block or be in column `c`. Otherwise it is: - after a value declaration, `ELCI1002: Unexpected token after declaration`; - after a `type`, a `type alias` or a destructuring, `ELCI1002: Declarations must align at column c`. - A declaration's expressions and types have the floor `c`. A branch's body has the floor `c` too. So a line that continues a declaration or branch must be indented past `c`. **Floors elsewhere.** - The parts of `if`, the body of a lambda, the body after `in` and the scrutinee of `case` keep the floor of the expression they are in. - Inside `( )`, `[ ]` and `{ }` in an expression, and inside a record type, the floor is `none`. A `let` or `case` inside brackets still has its own block column. - Inside `( )` in a type, the floor is kept. This module shows the rules: ```elm module Layout exposing (total) total : List Int -> Int total values = let doubled = List.map (\value -> value * 2) values sum = List.foldl (+) 0 doubled in case doubled of [] -> 0 first :: _ -> sum + first ``` `sum` and `doubled` align in column 9. `first :: _` aligns with `[]` in column 9, and `+ first` continues the branch because column 17 is past 9. `in` ends the `let` block at any column. ## Syntactic grammar ### Modules and imports ```text Module ::= Header Import* ( COL<1> TopDecl )* Header ::= "module" ModuleName "exposing" Exposing ModuleName ::= UpperName ( "." UpperName )* Import ::= "import" ModuleName ( "as" UpperName )? ( "exposing" Exposing )? Exposing ::= "(" ( ".." | ( Exposed ( "," Exposed )* )? ) ")" Exposed ::= LowerName | UpperName ( "(" ".." ")" )? ``` - There are no `port module` or `effect module` headers. - An exposing list names values, types, and types with all their constructors (`Shape(..)`). It cannot list constructors one by one or name an operator. - Imports come before the first declaration. ```elm module Example.Shapes exposing (Shape(..), area, unit) import Dict exposing (Dict) import Julc.Data as Data type Shape = Square Int | Rect Int Int area : Shape -> Int area shape = case shape of Square side -> side * side Rect width height -> width * height unit : Data unit = Data.encode (Dict.fromList [ ( 1, area (Square 1) ) ]) ``` ### Declarations ```text TopDecl ::= TypeAlias<1> | CustomType<1> | ValueDecl<1> LetDecl ::= TypeAlias | CustomType | ValueDecl | Destructuring TypeAlias ::= "type" "alias" UpperName LowerName* "=" Type CustomType ::= "type" UpperName LowerName* "=" Constructor ( "|" Constructor )* Constructor ::= UpperName ( CONT TypeAtom )* ValueDecl ::= Signature? LowerName PatternAtom* "=" Expr Signature ::= LowerName ":" Type COL Destructuring ::= ( PatternAtom - LowerName ) "=" Expr ``` - A signature must be followed directly by the definition of the same name, in the same column. When another value declaration follows instead, it is `ELCI1002: A signature must precede its matching definition`. When the input ends, or a `type` or an uppercase name follows, it is `ELCI1002: Expected name` or `Expected lowercase name`. Every top-level value needs a signature, but that is a type-checking rule (`ELCI2004`). - A function's parameters and a destructuring pattern must be irrefutable: `_`, a name, `()`, a tuple of irrefutable patterns, a record pattern, or an irrefutable pattern with `as`. Otherwise: `ELCI1002: Function parameters require irrefutable patterns; use case for refutable matching`, and likewise for lambda parameters, and `ELCI1002: Local destructuring requires an irrefutable tuple or record pattern`. - Destructuring is only for `let`. At the top level it is `ELCI1002: Top-level declarations require a name`. - A record type alias also declares a constructor function, with the fields in declaration order. ```elm module Declarations exposing (Pair, Tree(..), main) type alias Pair a = ( a, a ) type Tree a = Leaf | Node (Tree a) a (Tree a) type alias Account = { owner : String, balance : Int } main : Int main = let ( low, high ) = ( 1, 2 ) { balance } = Account "alice" 5 add : Int -> Int -> Int add a b = a + b in add low high + balance ``` ### Types ```text Type ::= TypeApp ( "->" Type )? TypeApp ::= TypeAtom ( CONT TypeAtom )* TypeAtom ::= QualifiedType | LowerName | "(" ")" | "(" Type ")" | "(" Type "," Type ( "," Type )* ")" | "{" ( FieldType ( "," FieldType )* )? "}" FieldType ::= LowerName ":" Type QualifiedType ::= UpperName ( "." UpperName )* ``` - `->` groups to the right: `a -> b -> c` is `a -> (b -> c)`. - A lowercase name is a type variable. `comparable`, `appendable` and `compappend` (and names that start with them) are constrained, as in Elm. - Tuples are written `( a, b )` or `( a, b, c )`; a longer tuple is `ELCI2012: Tuples require two or three elements`. `()` is the unit type. `Tuple` and `Unit` are not type names: `ELCI2012: Write tuple types as ( a, b ) or ( a, b, c )` and `ELCI2012: Write the unit type as ()`. - Records are closed. `{ r | field : Int }` is `ELCI1002: Extensible records are unsupported; use a closed record`. - `Float` is `ELCI2001: Float is unsupported; use scaled Int`. ### Expressions ```text Expr ::= Operand ( CONT BinaryOperator Operand )* /* grouped by the operator table */ Operand ::= Head ( CONT Suffix )* Head ::= Atom | "-" Operand ( CONT ( ">>" | "<<" ) Operand )* /* negation */ | "if" Expr "then" Expr "else" Expr | "let" LetBlock "in" Expr | "case" Expr "of" CaseBlock | "\" PatternAtom+ "->" Expr Suffix ::= FieldAccess | Argument | NegativeArgument Argument ::= Atom FieldAccess* NegativeArgument ::= "-" Atom FieldAccess* /* see Adjacency rules */ FieldAccess ::= "." LowerName /* the "." touches the token before it */ LetBlock ::= ( COL LetDecl )* /* c: the column of the first token after "let" */ CaseBlock ::= Branch ( COL Branch )* /* c: the column of the first token after "of" */ Branch ::= Pattern "->" Expr Atom ::= QualifiedName | "True" | "False" | Int | String | "(" ")" | "(" Expr ")" | "(" Expr "," Expr ( "," Expr )? ")" | "(" BinaryOperator ")" | "[" ( Expr ( "," Expr )* )? "]" | "{" ( Field ( "," Field )* )? "}" | "{" LowerName "|" Field ( "," Field )* "}" | "." LowerName /* accessor: see Adjacency rules */ Field ::= LowerName "=" Expr QualifiedName ::= LowerName | UpperName ( "." UpperName )* ( "." LowerName )? /* no spaces, see Adjacency rules */ BinaryOperator ::= "|>" | "<|" | "||" | "&&" | "==" | "/=" | "<" | ">" | "<=" | ">=" | "::" | "++" | "+" | "-" | "*" | "//" | "^" | ">>" | "<<" ``` - `if` needs its `else`. - `if`, `let` and a lambda extend as far to the right as they can: `if a then b else c |> f` is `if a then b else (c |> f)`. A `case` ends where its block ends, so an operator on a line left of its branches applies to the whole `case`. - A `-` after an operand is either `BinaryOperator` or the start of a `NegativeArgument`; the characters around it decide (see [Adjacency rules](#adjacency-rules)). - A tuple has two or three elements: `ELCI1002: Tuples have two or three elements`. - `(op)` is the operator as a function of two arguments, for every operator in the table: `(+)`, `(::)`, `(|>)`. `(/)` is rejected, like `/`. - `{ r | x = 1 }` updates the record `r`, which must be a plain lowercase name. - `.field` is the function `\r -> r.field`. - An argument can't start with `if`, `let`, `case`, `\` or `-`; put it in parentheses. A minus written as in `f -1` is the exception: see [Adjacency rules](#adjacency-rules). ```elm module Expressions exposing (main) type alias Point = { x : Int, y : Int } main : ( Int, List Int, Bool ) main = let origin = { x = 0, y = 0 } moved = { origin | x = 3 } points = [ moved, Point 1 2 ] in ( List.foldl (+) 0 (List.map .x points) , List.map (\point -> point.y * -1) points , if moved.x > origin.x then True else False ) ``` ### Patterns ```text Pattern ::= PatternApp ( "::" Pattern )? ( "as" LowerName )? PatternApp ::= QualifiedCtor PatternAtom+ | PatternAtom /* True and False take no arguments */ PatternAtom ::= "_" | LowerName | QualifiedCtor | Int | "-" Int | String | "(" ")" | "(" Pattern ")" | "(" Pattern "," Pattern ( "," Pattern )? ")" | "[" ( Pattern ( "," Pattern )* )? "]" | "{" ( LowerName ( "," LowerName )* )? "}" QualifiedCtor ::= UpperName ( "." UpperName )* ``` - `True` and `False` take no arguments: `True x ->` is `ELCI1002: Expected '->', found 'x'`. - `::` groups to the right: `a :: b :: rest`. - `as` names the pattern just before it. In `first :: rest as all`, `all` names `rest`, not the whole list (Elm names the whole list); write `(first :: rest) as all` for that. - A tuple pattern has two or three elements: `ELCI1002: Tuple patterns have two or three elements`. - `[ a, b ]` is the pattern `a :: b :: []`. - Function and lambda parameters and `let` destructuring take a single `PatternAtom`, which must be irrefutable (see [Declarations](#declarations)). ```elm module Patterns exposing (describe) describe : List (Maybe Int) -> String describe values = case values of [] -> "none" [ Just 0 ] -> "one zero" (Just -1) :: _ -> "starts at minus one" ((Just _) :: _) as all -> if List.length all > 2 then "many" else "some" Nothing :: _ -> "starts empty" ``` ## Operators From the tightest binding to the loosest: | Precedence | Operators | Associativity | | ---: | --- | --- | | | `record.field` | | | 10 | function application `f x` | left | | 9 | `>>` | right | | 9 | `<<` | left | | | negation `-x`, a `-` where an expression starts | | | 8 | `^` | right | | 7 | `*`, `//` | left | | 6 | `+`, `-` | left | | 5 | `::`, `++` | right | | 4 | `==`, `/=`, `<`, `>`, `<=`, `>=` | none | | 3 | `&&` | right | | 2 | `\|\|` | right | | 0 | `\|>` | left | | 0 | `<\|` | right | - The precedences are those of Elm 0.19, and of `Parser.PRECEDENCE`. Negation has no number in the parser: it reads its operand at precedence 9, so it covers applications and `>>`/`<<` chains and stops before `^`. `-x ^ 2` is `(-x) ^ 2`, `-f x` is `-(f x)` and `2 * -3` is `2 * (-3)`. - Comparisons do not chain: `a < b < c` is `ELCI1002: Comparison operators cannot be chained; use && or explicit parentheses`. - Operators of one precedence that associate in opposite directions do not mix: `f <| x |> g` is `ELCI1002: Mixing (<|) and (|>) needs parentheses: they associate in opposite directions, as in Elm`, and the same holds for `>>` and `<<`. - `/` is `ELCI2001: Float division is unsupported; use // for integer quotient`, and `(/)` is `ELCI2001: Float division is unsupported; use (//) for integer quotient`. `%` does not exist: use `modBy` or `remainderBy`. ```elm module Operators exposing (main) main : List Int main = [ 1 + 2 * 3 ^ 2 , 2 ^ 3 ^ 2 , (List.map ((*) 2) >> List.sum) [ 1, 2 ] , [ 1, 2 ] |> List.map negate |> List.sum , List.sum <| 0 :: [ 1 ] ++ [ 2 ] ] ``` ## Adjacency rules Some tokens mean different things depending on whether they *touch*: two tokens touch when no character, space or comment, separates them. **Field access and accessors.** A `.` that touches the token before it reads a field of that value. A `.` that does not touch the token before it, followed by a lowercase name that touches the `.`, is an accessor function. | Source | Meaning | | --- | --- | | `r.field` | the field `field` of `r` | | `r .field` | `r` applied to the accessor `.field` | | `List.map .amount orders` | `.amount` is `\o -> o.amount` | | `(f x).amount`, `"text".length` | a field of a parenthesized expression, or of a literal | | `r{- note -}.field` | `r` applied to `.field`: the comment separates them | **Qualified names.** In an expression, a qualified name is written without spaces and ends at its first lowercase part. Every `.` in it touches the names on both sides. | Source | Meaning | | --- | --- | | `List.map` | the value `map` of the module `List` | | `Maybe.Just` | the constructor `Just` of the module `Maybe` | | `Config.settings.fee` | the field `fee` of the value `Config.settings` | | `List . map` | an error: `List` followed by a stray `.` | In module names, imports, types and patterns, the parser accepts spaces around the `.`, but no formatter writes them. **Negative arguments.** A `-` after an operand is read by the characters around it, as in Elm 0.19: - It negates the operand that follows when the character before it is whitespace (or `(`, `[`, `{`, `,`, or the start of the file) and the character after it is neither whitespace nor `-`. The negated operand is then an argument of the function before it. - Otherwise it subtracts. - A `-` where an expression starts always negates, with or without a space after it. | Source | Meaning | | --- | --- | | `f -1` | `f (-1)` | | `modBy 3 -7` | `modBy 3 (-7)` | | `a - 1`, `a-1`, `a- 1` | subtraction | | `a -b` | `a (-b)`: an application, as in Elm | | `f -x.y` | `f (-(x.y))` | | `(-1)`, `[ -1 ]`, `x = -1` | negation where an expression starts | | `f{- c -}-1` | subtraction: the character before `-` is `}` | ```elm module Negation exposing (main) main : List Int main = let x = 4 scale factor value = factor * value in [ scale -x 10, modBy 3 -7, x - 1, x-1, -x * 2, -(scale x 2), 2 * -3 ] ``` ## Elm syntax that Elci does not support These parts of Elm 0.19's syntax are rejected. The messages are the compiler's: | Elm syntax | Elci's message | | --- | --- | | `port module Main exposing (..)` | `ELCI1002: Expected 'module', found 'port'` | | `effect module ... where { ... }` | `ELCI1002: Expected 'module', found 'effect'` | | `1.5`, `Float` | `ELCI1001: Float is unsupported; represent fractional amounts using an explicit integer scale`; `ELCI2001: Float is unsupported; use scaled Int` | | `x / y` | `ELCI2001: Float division is unsupported; use // for integer quotient` | | `'a'` | `ELCI1001: Unsupported character '''` | | `0xFF`, `1e3` | read as `0 xFF` and `1 e3`: `ELCI2005: Unknown name 'xFF'` | | `"\u{0041}"` | `ELCI1001: Unsupported string escape` | | `"""multi-line"""` | read as `""` applied to `"multi-line"` and `""`: `` ELCI2002: This is `String`, not a function, so it cannot take an argument `` | | `{ r \| x : Int }` | `ELCI1002: Extensible records are unsupported; use a closed record` | | `( 1, 2, 3, 4 )` | `ELCI1002: Tuples have two or three elements` | | `(,)` | `ELCI1002: Expected expression, found ','` | | `exposing (Shape(Circle))` | `ELCI1002: Expected '..', found 'Circle'` | | `exposing ((+))` | `ELCI1002: Expected name` | | `( a, b ) = pair` at the top level | `ELCI1002: Top-level declarations require a name` | | `(+++) : Int -> Int -> Int` | `ELCI1002: Top-level declarations require a name` | | `` a `max` b ``, `a % b` | `` ELCI1001: Unsupported character '`' ``, `ELCI1001: Unsupported character '%'` | | a tab in the indentation | `ELCI1001: Tabs are not permitted; use spaces for Elm layout` | ## Parser details that differ from Elm The parser accepts some programs that Elm rejects, and in one case reads a program differently. These are the details as implemented; formatted code does not rely on them. - `as` in a cons pattern names only the tail: see [Patterns](#patterns). - `-` where an expression starts negates even with a space after it: `- 1` is `-1`. - The first token of an expression, and every token taken by keyword (`then`, `else`, `of`, `in`, `->`, `=`, `:`, `|`), may be in any column. So `main =` followed by `1` in column 1 is accepted, and a signature may continue on a line that starts with `->` in column 1. - A `case` branch whose body starts in the branch column is accepted: the body's first token is not checked. - A field access may have spaces after the dot: `r. field` is `r.field`. - `let` may declare types (`type alias` and `type`). They belong to the module, not the `let`: a top-level signature can use them. A `let` with no declarations (`let in x`) is accepted. - The module header and imports are not checked for layout, and the header may be indented. - An empty exposing list, `exposing ()`, is accepted, and so is `(..)` after a lowercase name, as in `exposing (main(..))`. Neither exposes anything. - `if`, `case` and `let` are not reserved: they can name a declaration or a parameter, which then can't be used. Reserved words can name pattern variables, as in `\of -> 1`. - `_` is accepted as a type parameter (`type alias Box _ = Int`), a record field (`{ _ = 1 }`) and an `as` name. - A name may start with `_`, as in `_count`. It can't be declared (`ELCI1002: Top-level declarations require a name`), and in a pattern it reads as a constructor (`ELCI2001: Unknown constructor _count in this pattern`). - Digits are Unicode decimal digits: `١٢` is `12`. --- # Diagnostics > How to read an Elci error, how a run reports several errors, elci explain, the JSON report for tools, and every ELCI error code with real messages and fixes. Source: https://elci-lang.dev/reference/diagnostics/ Elci reports each error with a stable code and a source location, shows the source code it is about and suggests a fix. This page explains how to read a diagnostic, how a run reports several errors, and how tools can read them. It then lists every code, and shows the most common errors with a small program that triggers each one, the exact output and the fix. ## Reading a diagnostic A compile error starts with one line, `path:line:column: code: message`. A blank line follows, then the source line with carets under what the error is about, then any hints: ```text src/Main.elm:6:5: ELCI2005: Unknown name 'lenght' 6| lenght [ 1, 2 ] ^^^^^^ Hint: did you mean `List.length`? For more on ELCI2005, run `elci explain ELCI2005`. ``` - The **first line** has the same form for every error, so editors and scripts can parse it. The **path** is the file of the module where the problem is, which may be a module imported by the one you compiled. A problem inside a bundled module such as `List` is reported where your code uses it, or otherwise as `/List.elm`. - The **code** names the class of error and never changes between releases. The **message** says what is wrong in terms of your code: which argument, branch, list element, operator side, annotation or field has the wrong type, or which patterns a `case` doesn't cover. - The **excerpt** shows the line with its number, and underlines what the error is about. For an error at the end of the file, it points just past the last code. An error in a bundled module has no excerpt. - Each **hint** suggests a likely fix: a close name, a conversion such as `String.fromInt`, or an import to add. An error has zero or more hints. - The **last line** names the codes of the run and the command that explains them; see [`elci explain`](#elci-explain). The examples in the rest of this page leave it out and show only the diagnostic. Some show only its first line, without the excerpt and hints. Types in messages are written as in source, in backticks: `Result a Int`, `Order`. When two types of one name meet in a message, both are qualified, as in `Main.Value` and `Julc.Ledger.Value`. For a validator, the `elci.toml` table follows the diagnostic, on its own line: ```text my-project/src/Main.elm:6:1: ELCI2013: Functions cannot be serialized as datum or redeemer 6| mint : (Int -> Int) -> ScriptContext -> Bool ^^^^ In my-project/elci.toml:6: [validators.main] ``` Every error makes the command exit with status 1. Codes starting with `ELCI1` come from the lexer and parser, `ELCI20` from type checking, `ELCI21` from modules and imports, `ELCI3` from code generation and `ELCI4` from configuration and the backend. ### What is underlined The carets underline the reported token, such as a name, a qualified name or a string literal with its quotes. When the error is about a whole expression, they underline all of it. This is the case for an argument, a list element, the body of a `case` branch and a field access such as `.price`: ```elm main : String main = String.fromInt (String.append "4" "2") ``` ```text src/Main.elm:6:20: ELCI2002: The 1st argument to `String.fromInt` is `String`, but `String.fromInt` needs `Int` 6| String.fromInt (String.append "4" "2") ^^^^^^^^^^^^^^^^^^^^^^^ ``` An expression over several lines is underlined on each of its lines, up to five: ```elm main : String main = String.fromInt (String.append "4" "2" ) ``` ```text src/Main.elm:7:9: ELCI2002: The 1st argument to `String.fromInt` is `String`, but `String.fromInt` needs `Int` 7| (String.append ^^^^^^^^^^^^^^ 8| "4" ^^^ 9| "2" ^^^ 10| ) ^ ``` The line and column of the first line are where the underline starts. A longer expression shows its first five lines, then a line with `…`. Some expressions are reported at a token inside them: an operation such as `"x" ++ "y"` at its operator, and a call without parentheses such as `String.fromInt 3` at its last argument. The caret then marks that token alone, as an underline from there would cover only part of the expression. ### Long types When the two types of a mismatch are long, `It is:` and `It needs:` lines follow the excerpt. They show the types one above the other, with the first difference marked: ```elm type alias Order = { owner : String, amount : Int, deadline : Int } amountOf : Order -> Int amountOf order = order.amount main : Int main = amountOf { owner = "alice", amount = "10", deadline = 5 } ``` ```text src/Main.elm:15:14: ELCI2002: The 1st argument to `amountOf` is `{ amount : String, deadline : Int, owner : String }`, but `amountOf` needs `{ amount : Int, deadline : Int, owner : String }` 15| amountOf { owner = "alice", amount = "10", deadline = 5 } ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ It is: { amount : String, deadline : Int, owner : String } It needs: { amount : Int, deadline : Int, owner : String } ^^^ ``` The message still names both types, so the first line reads the same as for short types. The lines are added when the two types together are 60 characters or longer. ### Colour On a terminal, the path and code, the carets and `Hint:` are in colour. Output to a file or a pipe is plain, and so is any output when the `NO_COLOR` environment variable is set. `--color` overrides this on any command: | Option | Colour | | --- | --- | | `--color=auto` | On a terminal, unless `NO_COLOR` is set. The default. | | `--color=always` | Always, also in a pipe. | | `--color=never` | Never. | The text is the same, character for character, with and without colour. The [JSON report](#json-reports-for-tools) is never coloured. ## Several errors in one run A run reports every error it finds, not just the first. Compilation has three phases: lexing and parsing, name resolution, and type checking. Each phase reports all of its errors. A later phase runs only when the earlier ones found none, as in Elm, so one mistake isn't reported again as a different error later. ```elm fee : Int fee = "2" total : Int -> Int total amount = amount + lenght [ 1 ] label : Int -> String label amount = String.fromInt "amount" main : Int main = total fee ``` ```text src/Main.elm:4:1: ELCI2002: The annotation of `fee` says it is `Int`, but its definition is `String` 4| fee : Int ^^^ src/Main.elm:11:14: ELCI2005: Unknown name 'lenght' 11| amount + lenght [ 1 ] ^^^^^^ Hint: did you mean `List.length`? src/Main.elm:16:20: ELCI2002: The 1st argument to `String.fromInt` is `String`, but `String.fromInt` needs `Int` 16| String.fromInt "amount" ^^^^^^^^ 3 errors For more on ELCI2002 and ELCI2005, run `elci explain `. ``` - **Order.** Errors are shown in source order: by module, then by line and column. A blank line separates them. - **Count.** When there is more than one error, a line such as `3 errors` follows them. - **Limit.** A run shows at most 20 errors, then a line such as `... and 3 more`. The count still gives the total. - **One per declaration.** Each top-level declaration reports its first error. A declaration whose definition fails still has the type its annotation gives it, so its callers are checked against the annotation and don't report errors of their own. Above, `main` passes `fee` to `total`, which needs an `Int`. The annotation says `fee` is one, so `main` is not reported. - **Once each.** A mistake that fails several checks alike, such as each comparison inside `List.sort`, is reported once. - **Phases.** A module with a syntax error is not type-checked. Nor is a program with an import or qualified-name error (`ELCI21xx`), such as `Lsit.length`. Fix those, and the next run reports the type errors. An unknown unqualified name (`ELCI2005`), as above, is found while type checking and is reported with the type errors. - **One at a time.** Code generation (`ELCI3xxx`), configuration and command-line errors (`ELCI4xxx`) still stop the run at the first error. ## `elci explain` A failed run's text report ends with a line that names its codes and how to read more about them: ```text For more on ELCI2005, run `elci explain ELCI2005`. For more on ELCI2002 and ELCI2005, run `elci explain `. ``` `elci explain` prints the entry for a code: what it means and its common causes, a failing example with its exact output, the fix, and where to read more. `ELCI2005`, `elci2005` and `2005` all name the same code: ```text $ elci explain 2005 ELCI2005: Unknown name A value name is used that nothing in scope defines. Common causes: - A misspelling. The hint suggests up to three close names, local ones first. - A name from another module that is not imported, or that its import doesn't expose. The hint says how to reach it, such as writing `Helper.double`. - A name a Java library module doesn't export. A qualified name such as `List.lenght` is ELCI2104. Example main : Int main = lenght [ 1, 2 ] `elci check` reports: src/Main.elm:6:5: ELCI2005: Unknown name 'lenght' 6| lenght [ 1, 2 ] ^^^^^^ Hint: did you mean `List.length`? Fix Correct the spelling, or import the module that defines the name: main : Int main = List.length [ 1, 2 ] See also - The Diagnostics reference, section ELCI2005. - ELCI2104, for qualified names. ``` `elci explain` without a code lists every code with its title. The entries are part of the compiler and match its version. Each entry's example is tested to give exactly the output it shows, and its fix to compile. An unknown code is an `ELCI4001` error. ## JSON reports for tools `elci check`, `elci build`, `elci eval` and `elci test` take `--report=json`. Each error is then printed on standard error as one JSON object on its own line ([JSON Lines](https://jsonlines.org/)). A run with one error prints one line: ```text $ elci check --report=json {"code":"ELCI2005","path":"src/Main.elm","line":6,"column":5,"message":"Unknown name 'lenght'","hints":["did you mean `List.length`?"],"excerpt":"6| lenght [ 1, 2 ]\n ^^^^^^","context":null} ``` A run with several errors prints one line for each, in the same order and up to the same limit as the text report. Read the output line by line, and parse each line as JSON: ```text $ elci check --report=json {"code":"ELCI2002","path":"src/Main.elm","line":4,"column":1,"message":"The annotation of `fee` says it is `Int`, but its definition is `String`","hints":[],"excerpt":"4| fee : Int\n ^^^","context":null} {"code":"ELCI2005","path":"src/Main.elm","line":11,"column":14,"message":"Unknown name 'lenght'","hints":["did you mean `List.length`?"],"excerpt":"11| amount + lenght [ 1 ]\n ^^^^^^","context":null} {"code":"ELCI2002","path":"src/Main.elm","line":16,"column":20,"message":"The 1st argument to `String.fromInt` is `String`, but `String.fromInt` needs `Int`","hints":[],"excerpt":"16| String.fromInt \"amount\"\n ^^^^^^^^","context":null} ``` The JSON report has no count line, no `... and N more` line and no `elci explain` line. | Field | Type | Contents | | --- | --- | --- | | `code` | string or null | The `ELCI` code; null for a failure that has no code | | `path` | string or null | The file, as on the first line of the text report | | `line`, `column` | number or null | The position, counting from 1 | | `message` | string | The message, without the path and code | | `hints` | array of strings | The hints, without `Hint:`; empty when there are none | | `excerpt` | string or null | The source lines and their caret lines, separated by `\n`; null when there is no source to show | | `context` | string or null | For a failure in a named validator, its `elci.toml` line and table; otherwise null | The `It is:` and `It needs:` lines of a long mismatch are not part of the JSON; the message names both types. A failure in a validator names its table in `context`: ```text {"code":"ELCI2013","path":"my-project/src/Main.elm","line":6,"column":1,"message":"Functions cannot be serialized as datum or redeemer","hints":[],"excerpt":"6| mint : (Int -> Int) -> ScriptContext -> Bool\n ^^^^","context":"my-project/elci.toml:6: [validators.main]"} ``` A failure without a source location keeps the same fields, with null location fields: ```text {"code":"ELCI4001","path":null,"line":null,"column":null,"message":"unknown validator 'treasury'; configured: main","hints":[],"excerpt":null,"context":null} ``` The exit status is the same with or without the option. Results stay text on standard output: the lines of `elci check` and `elci build`, the value and budget of `elci eval` or its `evaluation failed:` line, and test results, including failing tests. The other commands reject the option with `ELCI4001`. ## All codes | Code | Meaning | | --- | --- | | `ELCI1001` | Invalid source character or literal | | `ELCI1002` | Malformed or unsupported syntax | | `ELCI2001` | Invalid pattern or expression form | | `ELCI2002` | Type mismatch, or a type that would have to contain itself | | `ELCI2003` | Unsupported equality, ordering or append type; a constrained type variable that is missing or misnamed; a `Dict` decoded from Data whose keys aren't comparable | | `ELCI2004` | Missing required top-level signature | | `ELCI2005` | Unknown value name | | `ELCI2006` | Duplicate binder or unreachable pattern | | `ELCI2007` | Non-exhaustive `case` expression | | `ELCI2008` | Eager recursive value cycle | | `ELCI2009` | Non-concrete handler, entrypoint or property | | `ELCI2010` | Unknown or unconstrained record field | | `ELCI2011` | Duplicate type or constructor, or a built-in constructor or type name (`Just`, `Nothing`, `True`, `False`; `Int`, `Maybe`, `List` and the other built-in types) | | `ELCI2012` | Invalid type, alias, arity or alias cycle; `Tuple` or `Unit` written as a type name | | `ELCI2013` | Unsupported serialized boundary type, such as a `Value` in a datum, or a value without a Data encoding (a function or an `Array`) stored in data | | `ELCI2014` | Validator handler signature does not match its purpose, parameters or datum profile | | `ELCI2101` | Unknown or missing module or imported export; a source module importing the test library | | `ELCI2102` | Ambiguous name: two imports expose different things under it | | `ELCI2103` | Entrypoint or handler is not exposed | | `ELCI2104` | Module path, ownership or cycle error; a reserved module name or alias; a name a module lacks, an Elm function Elci doesn't have, or a module used without its import | | `ELCI2105` | Unsupported Java export signature, with the provider's reason; a Java export whose result can hold a `Value` | | `ELCI3001` | The inferred program cannot be lowered, such as `Julc.Data.decode` or a Java library generic at a type nothing determines | | `ELCI3002` | Specialization limit or polymorphic recursion | | `ELCI3003` | A generic library type argument or a Data conversion needs a Data encoding; a Java library generic used at a type containing a `Dict` or a `Value` | | `ELCI4001` | Invalid project configuration or command-line request, including a `[java-modules]` key that names a core module and an `elci apply` parameter that doesn't fit its type | | `ELCI4002` | Julc's backend rejected the generated code | | `ELCI4003` | The Julc backend lacks a capability the program needs | ## Syntax errors ### `ELCI1001`: invalid character or literal ```elm main : Int main = 1.5 ``` ```text src/Main.elm:6:6: ELCI1001: Float is unsupported; represent fractional amounts using an explicit integer scale 6| 1.5 ^ ``` **Fix:** use integers with an explicit scale, such as lovelace instead of ada or basis points instead of percentages. The same code reports tabs, unsupported characters such as `%` or `'` and unknown string escapes. ### `ELCI1002`: malformed or unsupported syntax The message says what is wrong. Refutable patterns in function arguments are a common case: ```elm first : List Int -> Int first (x :: _) = x ``` ```text src/Main.elm:5:8: ELCI1002: Function parameters require irrefutable patterns; use case for refutable matching 5| first (x :: _) = ^ ``` **Fix:** match with `case`, and handle every shape: ```elm first : List Int -> Int first values = case values of x :: _ -> x [] -> 0 ``` Other `ELCI1002` messages include `Comparison operators cannot be chained; use && or explicit parentheses` (for `1 < 2 < 3`), `Mixing (<|) and (|>) needs parentheses: they associate in opposite directions, as in Elm` (for `f <| x |> g`), `Extensible records are unsupported; use a closed record` and `Tuples have two or three elements`. ### `ELCI2001`: invalid expression or pattern ```elm main : Int main = 7 / 2 ``` ```text src/Main.elm:6:7: ELCI2001: Float division is unsupported; use // for integer quotient 6| 7 / 2 ^ ``` **Fix:** `7 // 2`, together with `modBy` or `remainderBy` when you need the rest. The function `(/)` is rejected the same way, with a suggestion to use `(//)`. `ELCI2001` also covers patterns that can't match their value's shape. A constructor pattern must give the constructor all of its arguments: ```elm main : Int main = case Just 1 of Just x y -> x Nothing -> 0 ``` ```text src/Main.elm:7:9: ELCI2001: The constructor Just takes 1 argument, but this pattern gives it 2 7| Just x y -> ^^^^ ``` A misspelt constructor in a pattern gets a suggestion: ```text src/Main.elm:7:9: ELCI2001: Unknown constructor Jsut in this pattern 7| Jsut n -> ^^^^ Hint: did you mean `Just`? ``` A record pattern such as `{ index }` can't take apart a ledger record. Read its fields instead: ```text src/Main.elm:10:9: ELCI2001: TxOutRef is a library record, which a pattern cannot take apart; read its fields with `.index` instead ``` ## Type errors ### `ELCI2002`: type mismatch ```elm main : String main = String.fromInt "42" ``` ```text src/Main.elm:6:20: ELCI2002: The 1st argument to `String.fromInt` is `String`, but `String.fromInt` needs `Int` 6| String.fromInt "42" ^^^^ ``` The message names the part of the code whose type differs, and the type it needs: | Where | Example message | | --- | --- | | An argument | `` The 1st argument to `String.fromInt` is `String`, but `String.fromInt` needs `Int` `` | | A value piped in with `\|>` | `` The 1st argument to `double`, the value piped in, is `String`, but `double` needs `Int` `` | | Too many arguments | `` `max` takes 2 arguments, but it is given 3 `` | | A value applied like a function | `` `fee` is `Int`, not a function, so it cannot take an argument `` | | The condition of an `if` | `` The condition of this if is `Int`, but it must be `Bool` `` | | The branches of an `if` | `` The branches of this if have different types: the then branch is `Int`, but the else branch is `String` `` | | A branch of a `case` | `` The 2nd branch of this case is `String`, but the first branch is `Int` `` | | A pattern | `` This pattern matches `Maybe a`, but the value it is matched against is `Int` `` | | A list element | `` The 3rd element of this list is `String`, but the elements before it are `Int` `` | | An operator | `` The left side of :: is `String`, but the list on the right holds `Int` ``, `` The two sides of ++ have different types: the left is `String`, but the right is `Int` `` | | A composition | `` The left side of >> returns `String`, but the right side takes `Int` `` | | An annotation | `` The annotation of `add` says it returns `Int`, but its body is `String` ``, `` The annotation of `add` gives it 1 argument, but its definition has 2 parameters `` | | A record field | `` The field `owner` is `String`, but it is used here as `Int` `` | A too-many-arguments error also hints that a missing comma or parentheses can make values look like extra arguments. A signature that claims more than the body delivers is also a mismatch: ```elm identity : a -> a identity value = value + 1 ``` ```text src/Main.elm:4:1: ELCI2002: The annotation of `identity` says `a` can be any type, but its definition needs `Int` 4| identity : a -> a ^^^^^^^^ Hint: write `Int` in the annotation, or make the definition work for any type ``` **Fix:** make the signature match the body (`increment : Int -> Int`), or the body match the signature. An annotation with two type variables that the definition needs to be the same says `` The annotation of `pick` says `a` and `b` can be different types, but its definition needs them to be the same ``. The annotations of mutually recursive functions are all read before any body is checked, so a mistake is reported in the body that makes it. Arithmetic operators name the types they accept, and suggest the right operator where there is one: ```text src/Main.elm:6:7: ELCI2002: The operator + works on Int values, not String; use ++ to append ``` Comparisons on a type that has no order are `ELCI2003`, below. **Hints.** A mismatch between types that are often confused carries a hint with the conversion: ```elm main : Int main = List.head [ 1, 2 ] + 1 ``` ```text src/Main.elm:6:24: ELCI2002: The operator + works on Int values, not Maybe Int 6| List.head [ 1, 2 ] + 1 ^ Hint: this is a Maybe: handle `Nothing` with a case, or use `Maybe.withDefault` ``` The hints cover `Int` and `String` (`String.fromInt`), `String` and `Bytes` (`Bytes.fromString`, `Bytes.toString`), `Bytes` and a ledger hash such as `PubKeyHash` (the constructor, or the `.hash` field), a `Maybe` or `Result` and a plain value (a `case`, `Maybe.withDefault`, `Result.withDefault` or `Just`), `Value` and `Int` (`Value.lovelaceOf`, `Value.lovelace`), `Data` and a typed value (`Data.decode`, `Data.encode`), and a function where a value is needed (a missing argument). An error shows at most two hints, and only those that fit the types. A pattern of the wrong type gets no hint. A type that would have to contain itself, as when a generic function calls itself with a tuple of its argument, is reported as `` ELCI2002: This needs a type that contains itself: `a` would have to be `( a, a )` ``. ### `ELCI2003`: unsupported equality, ordering or append ```elm double : Int -> Int double x = x * 2 main : Bool main = double == double ``` ```text src/Main.elm:11:12: ELCI2003: The operator == cannot compare Int -> Int values: functions have no equality 11| double == double ^^ ``` Equality is rejected for functions, `Map`, `Array` and imported types without approved equality, such as `TxOut`. A `Dict` has equality when its values do, and a `Value` and `Data` always have it. When the offending type is inside another, the message names it: `The operator == cannot compare List (Int -> Int) values: Int -> Int is part of them, and functions have no equality`. `++` needs a `String` or a `List`: `1 ++ 2` gives `ELCI2003: The operator ++ appends String or List values, not Int`. **Fix:** compare the values you care about, such as a map's lookups or a decoded datum, or keep keyed data in a `Dict`, which has equality. Ordering (`<`, `compare`, `max`, `List.sort` and the rest) needs a [comparable type](https://elci-lang.dev/reference/core-modules/#comparable-types). The message names the operation and the part of the type that isn't comparable: ```elm main : Bool main = ( 1, True ) < ( 2, False ) ``` ```text src/Main.elm:6:17: ELCI2003: The operator < needs comparable values, but Bool, in ( Int, Bool ), is not comparable. Comparable types are Int, String, Bytes, byte-string ledger types such as PubKeyHash, and lists and tuples of comparable types 6| ( 1, True ) < ( 2, False ) ^ ``` For records and custom types, the message goes on to suggest a key: ```text src/Main.elm:10:5: ELCI2003: `List.sort` needs comparable values, but { age : Int, name : String } is not comparable. Comparable types are Int, String, Bytes, byte-string ledger types such as PubKeyHash, and lists and tuples of comparable types. Compare a comparable field instead, or sort with List.sortBy or List.sortWith ``` **Fix:** compare a comparable field, or sort with `List.sortBy (\person -> person.age)` or `List.sortWith`. A signature whose type variable is compared or appended must say so with its name, as in Elm ([constrained type variables](https://elci-lang.dev/reference/language/#constrained-type-variables)): ```elm larger : a -> a -> a larger x y = if x > y then x else y ``` ```text src/Main.elm:6:10: ELCI2003: The operator > compares values of type `a`, but the annotation of `larger` says `a` can be any type. Rename it `comparable` (as in Elm) to require comparable values 6| if x > y then ^ ``` **Fix:** write `larger : comparable -> comparable -> comparable`. For `++`, the message suggests `appendable`. A caller that then passes the wrong type is reported at the call: ``ELCI2003: `twice` appends String or List values, not Int``. Elm's `number` has its own message: ```text src/Main.elm:4:1: ELCI2003: The annotation of `total` uses `number`, Elm's type variable for Int or Float. Elci has one number type: write Int ``` Decoding a `Dict` checks that its keys are in order, so a `Dict` in a datum, a redeemer, a parameter or a `Julc.Data.decode` result needs comparable keys: ```text src/Main.elm:9:5: ELCI2003: A Dict decoded from Data needs comparable keys, to check that they are in order; Bool is not comparable. Use a Map instead ``` **Fix:** use a `Map`, whose keys may be any type with a Data encoding. ### `ELCI2004`: missing signature ```elm double x = x * 2 ``` ```text src/Main.elm:4:1: ELCI2004: Top-level declarations require a type signature 4| double x = ^^^^^^ ``` **Fix:** add `double : Int -> Int` directly above the definition. Local definitions do not need one. ### `ELCI2005`: unknown name ```elm main : Int main = lenght [ 1, 2 ] ``` ```text src/Main.elm:6:5: ELCI2005: Unknown name 'lenght' 6| lenght [ 1, 2 ] ^^^^^^ Hint: did you mean `List.length`? ``` **Fix:** correct the spelling, or import the module that defines the name. The hint lists up to three close names. It looks first at the names the module can write unqualified, local ones included, and then at qualified names. A name is close when it differs only in case, by one edit for a name of three to five characters, or by two edits for a longer name. Swapping two neighbouring letters counts as one edit. A name that an imported module exposes, but that its import doesn't list, gets a hint that says how to reach it. Here the module `Helper` exposes `double`: ```elm title="src/Main.elm" module Main exposing (main) import Helper main : Int main = double 4 ``` ```text src/Main.elm:8:5: ELCI2005: Unknown name 'double' 8| double 4 ^^^^^^ Hint: write `Helper.double`, or expose `double` in its import ``` A name that a Java library module doesn't export is reported with a reminder to check its exports: ```text src/Main.elm:8:5: ELCI2005: Unknown name 'Math.maximum'; check that the imported module exports it ``` A qualified name whose module is missing, not imported or lacks the name is `ELCI2104`; see [module and import errors](#elci2101-elci2102-and-elci2104). ### `ELCI2006`: duplicate binder or unreachable pattern ```elm describe : Maybe Int -> Int describe value = case value of Just n -> n Nothing -> 0 Just _ -> 1 ``` ```text src/Main.elm:13:9: ELCI2006: Unreachable pattern 13| Just _ -> ^^^^ ``` **Fix:** remove the branch that can never match. A pattern that binds the same name twice, such as `( x, x )`, gives `ELCI2006: Duplicate pattern variable x`. ### `ELCI2007`: non-exhaustive `case` ```elm type Action = Buy Int | Sell Int | Cancel amount : Action -> Int amount action = case action of Buy value -> value Sell value -> value ``` ```text src/Main.elm:12:5: ELCI2007: This case does not cover every possibility; it is missing `Cancel` 12| case action of ^^^^ Hint: add a branch for each missing pattern ``` **Fix:** add a `Cancel ->` branch. Prefer naming every constructor over `_ ->`, so that adding a constructor later points you to every `case` to update. The message lists the missing patterns as you would write them, including nested ones: ```elm describe : Maybe (Result String Int) -> String describe value = case value of Just (Ok n) -> String.fromInt n ``` ```text src/Main.elm:6:5: ELCI2007: This case does not cover every possibility; it is missing `Nothing` and `Just (Err _)` 6| case value of ^^^^ Hint: add a branch for each missing pattern ``` The list is complete: a branch for each pattern in it makes the `case` exhaustive. It shows up to three patterns, then how many more there are, as in `` it is missing `Square _`, `Triangle _ _ _`, `Line` and 1 more ``. Lists and literals are covered too: a `case` on a list with branches for `[]` and `[ _ ]` is missing `_ :: _ :: _`, and one on an `Int` or a `String` without a `_` branch is missing `_`. ### `ELCI2008`: eager value cycle ```elm main : Int main = let a = b + 1 b = a + 1 in a ``` ```text src/Main.elm:7:9: ELCI2008: Cyclic eager values are unsupported; recursion requires functions 7| a = ^ ``` **Fix:** values are computed eagerly, so they cannot depend on each other in a loop. Break the cycle, or turn the recursion into a function. ### `ELCI2009`: type is not concrete ```elm main : List a main = [] ``` ```text src/Main.elm:4:1: ELCI2009: Entrypoints require concrete types 4| main : List a ^^^^ ``` A validator handler gets `Handler Main.mint requires a concrete type`, and a property test with generic arguments gets a similar message. **Fix:** replace the type variable with a concrete type, such as `List Int`. ### `ELCI2010`: unknown record field ```elm main : Int main = let order = { amount = 5 } in order.price ``` ```text src/Main.elm:10:10: ELCI2010: Missing field 'price'; available fields: [amount] 10| order.price ^^^^^^ ``` **Fix:** use one of the listed fields. A field name close to one of them, such as `order.amont`, also gets the hint ``did you mean `amount`?``. When Elci cannot tell which record a field access belongs to, add a signature with the record type. ### `ELCI2011` and `ELCI2012`: invalid type declarations Two custom types in one module cannot share a constructor name. No module may declare the built-in constructors `Just`, `Nothing`, `True` or `False`, or a type named `Int`, `Bool`, `String`, `Bytes`, `Data`, `List`, `Maybe`, `Map`, `Array` or `Float`: ```text src/Main.elm:9:6: ELCI2011: Duplicate constructor Buy: the type Action has one too src/Main.elm:4:6: ELCI2011: Nothing is a constructor of the built-in Maybe type; choose another name src/Main.elm:4:6: ELCI2011: Bytes is a built-in type; choose another name ``` A type applied to the wrong number of arguments, and type aliases that refer to each other in a loop, are `ELCI2012`: ```text src/Main.elm:4:1: ELCI2012: The type List takes 1 argument, but it is given 2 src/Main.elm:4:12: ELCI2012: Cyclic type alias: A -> B -> A ``` Tuple and unit types have their own syntax. `Tuple Int Int` and `Unit` are `ELCI2012`: ```text src/Main.elm:4:8: ELCI2012: Write tuple types as ( a, b ) or ( a, b, c ) src/Main.elm:4:10: ELCI2012: Write the unit type as () ``` An unknown type name in a signature is also `ELCI2012`, reported at the declaration. A close type name is suggested: ```elm main : Strin main = "x" ``` ```text src/Main.elm:4:1: ELCI2012: Type is not declared or imported in this module: Strin 4| main : Strin ^^^^ Hint: did you mean `String`? ``` **Fix:** declare or import the type, or correct its spelling. ## Validator errors ### `ELCI2013`: type cannot cross the boundary A datum, redeemer or parameter must have a strict Data encoding: ```elm mint : (Int -> Int) -> ScriptContext -> Bool mint _ _ = True ``` ```text my-project/src/Main.elm:6:1: ELCI2013: Functions cannot be serialized as datum or redeemer 6| mint : (Int -> Int) -> ScriptContext -> Bool ^^^^ In my-project/elci.toml:6: [validators.main] ``` A `Value` is rejected the same way: ```text my-project/src/Main.elm:7:1: ELCI2013: A Value cannot be part of a datum, redeemer or parameter yet: the ledger's strict boundary cannot check its form. Store a List ( PolicyId, TokenName, Int ) and rebuild it with Value.fromList 7| mint : Value -> ScriptContext -> Bool ^^^^ In my-project/elci.toml:6: [validators.main] ``` So is a ledger type that contains one, such as `TxOut`: `ELCI2013: TxOut has no approved strict Data boundary (BOUNDARY); for example records containing a ledger Value (such as TxOut) cannot be checked`. **Fix:** use your own record or custom type, or a ledger type with an approved boundary such as `TxOutRef` or `PubKeyHash`. For an amount of tokens, store a `List ( PolicyId, TokenName, Int )` and rebuild the value with `Value.fromList`. The same code rejects an `Array` stored anywhere in data (a record, list or tuple field, or a datum, redeemer or parameter), in any project: ```elm module Main exposing (main) import Array exposing (Array) type alias Book = { pages : Array Int } main : Int main = let books = [ { pages = Array.fromList [ 1 ] } ] in List.length books ``` ```text src/Main.elm:10:1: ELCI2013: Arrays have no Data encoding and cannot be stored in data; keep a List and convert it with Array.fromList 10| main : Int ^^^^ ``` ### `ELCI2014`: handler signature does not match A spend handler with only a redeemer and context: ```elm spend : () -> ScriptContext -> Bool spend _ _ = True ``` ```text my-project/src/Main.elm:6:1: ELCI2014: Handler Main.spend does not have the spend signature datum -> redeemer -> ScriptContext -> Bool that [validators.main] needs (0 parameters); its type is () -> ScriptContext -> Bool; it has `Bool` where the configuration needs `ScriptContext -> Bool` 6| spend : () -> ScriptContext -> Bool ^^^^^ In my-project/elci.toml:6: [validators.main] ``` The message gives the signature the table needs, how many parameters it declares and the handler's actual type. The part in parentheses names the first pair of types that differ. The same code reports a missing parameter argument (`does not have the mint signature owner -> redeemer -> ScriptContext -> Bool that [validators.main] needs (1 parameter)`) and a datum that should be a `Maybe` (`uses spend-datum = "optional", so its datum must have type Maybe d, not Int`). **Fix:** make the signature match the table in [`elci.toml`](https://elci-lang.dev/reference/configuration/): parameters first, then the datum (for `spend`), the redeemer and the `ScriptContext`. ### `ELCI2103`: not exposed ```text my-project/src/Main.elm:11:1: ELCI2103: Handler Main.mint must be exposed by its module 11| mint : () -> ScriptContext -> Bool ^^^^ In my-project/elci.toml:6: [validators.main] ``` An `[entrypoint]` function that its module doesn't expose gets `ELCI2103: Entrypoint Main.main must be exposed by its module`. **Fix:** add the handler, or the `[entrypoint]` function, to the module's `exposing` list. ## Module and import errors ### `ELCI2101`, `ELCI2102` and `ELCI2104` A module that is neither a source file nor a `[java-modules]` mapping, or an unknown ledger type, is `ELCI2101`, reported at the import: ```elm import Julc.Nope as Nope ``` ```text src/Main.elm:3:1: ELCI2101: Missing source module: Julc.Nope; add Julc/Nope.elm under a source directory, or map a Java library to it under [java-modules] 3| import Julc.Nope as Nope ^^^^^^ ``` ```elm import Julc.Ledger exposing (Hash) ``` ```text src/Main.elm:3:1: ELCI2101: Unknown ledger record type Hash 3| import Julc.Ledger exposing (Hash) ^^^^^^ ``` A misspelt module, ledger type or exposed name in an import gets a suggestion: ```text src/Main.elm:3:1: ELCI2101: Unknown ledger type Credentail 3| import Julc.Ledger exposing (Credentail(..)) ^^^^^^ Hint: did you mean `Credential`? ``` The test library (`Expect`, `Scenario`, `Property`) is only for test modules. A source module that imports it is rejected. `elci test` reports: ```text src/Main.elm:3:1: ELCI2101: Only test modules can import Expect; it is part of the test library, not available to elci build ``` `elci check` and `elci build` report it when a validator reaches that module: ```text src/Main.elm:3:1: ELCI2101: Expect is part of the test library; only test modules can import it ``` Imports of existing modules fail with `ELCI2104` when the name is not exposed: ```elm import Map exposing (lookup) ``` ```text src/Main.elm:3:1: ELCI2104: Map does not expose 'lookup' 3| import Map exposing (lookup) ^^^^^^ ``` A qualified name gives one of three messages, depending on the module. The hint suggests close names: | Situation | Message | | --- | --- | | The module is imported, but doesn't expose the name | `Module Helper does not expose 'doubel'` | | A project or Java module exists, but this module doesn't import it | ``Module Helper is not imported; add `import Helper` `` | | No imported module has that name | `No module Lsit is imported, and none has that name` | ```elm main : Int main = Lsit.length [ 1, 2 ] ``` ```text src/Main.elm:6:5: ELCI2104: No module Lsit is imported, and none has that name 6| Lsit.length [ 1, 2 ] ^^^^^^^^^^^ Hint: did you mean `List.length`? ``` A qualified name must use the name the module is imported under. After `import Shop.Pricing` without `as Pricing`, `Pricing.priceOf` gets the third message; write `Shop.Pricing.priceOf`. A qualified name that a core module lacks lists what the module has. For Elm functions that Elci doesn't provide, the message says what to use instead: ```text src/Main.elm:8:5: ELCI2104: Elci's Map module has no lookup. It has empty, filter, foldl, foldr, fromList, get, insert, isEmpty, keys, map, member, remove, singleton, size, toList, update, values src/Main.elm:6:5: ELCI2104: Elci has no Debug.log; use Debug.trace message value, which returns value src/Main.elm:6:5: ELCI2104: Elci has no String.length; a String is UTF-8 text; Bytes.length (Bytes.fromString s) is its byte length src/Main.elm:6:5: ELCI2104: Elci has no Bytes.lessThan; bytes are comparable: write a < b ``` Using a core module that isn't imported by default, such as `Bytes`, `Dict`, `Map`, `Array` or `Julc.Data`, without its import names the import to add: ```text src/Main.elm:6:5: ELCI2104: Bytes.fromString needs the Bytes module; add `import Bytes` ``` A module imported with an alias is used through the alias: ```text src/Main.elm:8:5: ELCI2104: Map is imported as M; write M.get src/Main.elm:8:5: ELCI2104: Shop.Pricing is imported as P; write P.priceOf ``` `ELCI2104` also covers `exposing (..)` on a Java module, a file whose `module` line does not match its path, and import cycles: ```text src/Main.elm:3:1: ELCI2104: Java modules cannot be imported with exposing (..); list the exported names you use src/Pricing.elm:1:1: ELCI2104: Module header Finance.Pricing does not match its source path, which names module Pricing src/Pricing.elm:3:1: ELCI2104: Import cycle: Main -> Pricing -> Main ``` Core module names are reserved, for modules and for aliases (see [reserved names](https://elci-lang.dev/reference/language/#reserved-names)): ```text src/String.elm:1:1: ELCI2104: The module name String is taken by Elci's core library; rename this module src/Tx.elm:1:1: ELCI2104: The module name Tx is taken by Elci's core library; rename this module src/Set.elm:1:1: ELCI2104: The module name Set is reserved for Elci's core library; rename this module src/Main.elm:3:1: ELCI2104: The alias String names a core module that every module imports; choose another alias for Utils ``` Modules whose names start with `Elci.` belong to the core library too, and the ones it uses internally can't be imported: `ELCI2104: Elci.Assoc is internal to Elci's core library; use Map or Dict`, and `ELCI2104: Elci.Value is internal to Elci's core library; use Value`. A `Value` has no constructors to expose: `import Julc.Ledger exposing (Value(..))` gives `ELCI2104: Value has no approved constructors or patterns; its values come from the Value module`. A name that two imports expose, one of them possibly a default import, is `ELCI2102` where you use it: ```text src/Main.elm:8:5: ELCI2102: max is ambiguous: it could be Basics.max or Utils.max. Write the one you mean, like Basics.max, or stop exposing the other ``` An ambiguous type name is reported at the declaration whose annotation uses it: ```text src/Main.elm:6:1: ELCI2102: The type Order is ambiguous: it could be Basics.Order or Shop.Order. Write the one you mean, like Basics.Order, or stop exposing the other ``` **Fix:** import the module, correct the name (`Map.get`, not `lookup`), rename your module or alias, or qualify one of the ambiguous names. Listing the name explicitly, as in `import Utils exposing (max)`, also resolves a clash with a default import. ### `ELCI2105`: unsupported Java export ```elm main : Int main = case Math.divMod 7 2 of _ -> 0 ``` ```text src/Main.elm:8:10: ELCI2105: org.julclang.stdlib.lib.MathLib.divMod: Unsupported on-chain source type: Tuple2 8| case Math.divMod 7 2 of ^^^^^^^^^^^ ``` **Fix:** use another export, such as `Math.floorDiv` and `Math.floorMod`, or Elci operators. An export whose result can hold a ledger `Value`, directly or in a field of a record or union, is rejected too. Java libraries such as `ValuesLib` build values in their own order, and Elci relies on every value being canonical. Julc's `ContextsLib` and `OutputLib` are exempt, because they only pass along the ledger's own values: ```text src/Main.elm:9:5: ELCI2105: org.julclang.stdlib.lib.ValuesLib.add: returns a Value, which Java libraries do not keep in the ledger's canonical order; use the core Value module ``` **Fix:** use the core [`Value`](https://elci-lang.dev/reference/core-modules/#value) module, here `Value.add`. See [Java libraries](https://elci-lang.dev/advanced/java-libraries/) for every reason an export can be rejected. ## Code generation errors ### `ELCI3001`: type is not known where it is used A type that nothing in the program determines, such as the element type of `[]`, is fine for most code: `List.length []` is `0` and `Julc.Data.encode []` is an empty list, as in Elm. Operations that create a value of the type need to know it. These are `Julc.Data.decode` and generic Java library functions: ```elm import Julc.Data as Data main : Int main = List.length (Data.decode (Data.encode [ 1, 2 ])) ``` ```text src/Main.elm:8:18: ELCI3001: Cannot determine the type converted by Julc.Data.decode; add a type annotation 8| List.length (Data.decode (Data.encode [ 1, 2 ])) ^^^^^^^^^^^ ``` `List.length` works on any list, so nothing tells Elci what the decoded list contains, and it cannot pick a decoder. **Fix:** give the value a type: ```elm main : Int main = let amounts : List Int amounts = Data.decode (Data.encode [ 1, 2 ]) in List.length amounts ``` A generic Java library function needs its type arguments. When a core module such as `Map` uses one at a type nothing in your program determines, the error is reported at your call: ```text src/Main.elm:8:5: ELCI3001: `Map.size` is used here at a type that is not known (Map a b -> Int); add a type annotation, for example to a let binding ``` That one comes from `Map.size Map.empty`; annotate the map (`noPrices : Map String Int`). ### `ELCI3002`: specialization limit A program may create at most 256 specializations of its generic functions. This happens with polymorphic recursion that inference did not already reject. **Fix:** make the recursive function monomorphic. ### `ELCI3003`: no Data encoding Generic Java methods and `Julc.Data` conversions need a type with a Data encoding: ```text src/Main.elm:8:5: ELCI3003: Type argument T = Unit of firstOr must have a Plutus Data encoding (DATA_ENCODABLE); functions, unit and native values do not src/Main.elm:12:13: ELCI3003: No strict decoder for org.julclang.ledger.TxOut: The library provider cannot supply decode_strict record TxOut: [JULC0051] Library request decode_strict record TxOut is not supported: org.julclang.ledger.TxOut does not approve DECODE_STRICT ``` Encoding a function names the reason: ```elm import Julc.Data as Data double : Int -> Int double x = x * 2 main : Int main = List.length [ Data.encode double ] ``` ```text src/Main.elm:13:19: ELCI3003: No encoder for Int -> Int: functions have no Data encoding 13| List.length [ Data.encode double ] ^^^^^^^^^^^ ``` **Fix:** use a Data-encodable type. Functions, `()` and `Array` have no encoding, and `TxOut` can't be decoded. A `Value` converts on its own, but a type that contains one can't be decoded yet, and a list or tuple of values can't be encoded: convert them with `Value.toList` and back with `Value.fromList`. A Java library's generic function can't be used at a type that contains a `Dict`. The library could build the dictionary itself and skip the check that its keys are in order: ```text src/Main.elm:14:18: ELCI3003: Type argument a = Dict Int Int of filter contains a Dict, which a Java library cannot keep sorted; pass a Map or a List and build the Dict with Dict.fromList ``` **Fix:** pass the library a `Map` or a `List`, and build the `Dict` with `Dict.fromList`. A type that holds a `Value` is rejected the same way: ```text src/Main.elm:9:5: ELCI3003: Type argument a = Value of filter contains a Value, which a Java library does not keep in the ledger's canonical order; build it with the Value module ``` Use the core `List` and [`Value`](https://elci-lang.dev/reference/core-modules/#value) modules instead. ## Configuration and backend errors ### `ELCI4001`: configuration or command line ```text elci: ELCI4001: unknown validator 'treasury'; configured: main elci: ELCI4001: eval runs an [entrypoint] function; test validators with elci test elci: ELCI4001: parameter owner is not a well-formed org.julclang.ledger.PubKeyHash value elci: ELCI4001: parameter caps has a Dict whose keys are not strictly ascending; write its keys sorted and without repeats elci: ELCI4001: src/Main.elm is not valid UTF-8 text; save it as UTF-8 ``` The message says what to change. A source file must be UTF-8 text. A `Dict` parameter must list its keys in ascending order, each once; see [parameterized validators](https://elci-lang.dev/contracts/parameters/#what-apply-checks). Mistakes in `elci.toml`, in the arguments of `elci init` and on the command line have this code too: ```text elci: ELCI4001: Unknown configuration key: [project] name elci: ELCI4001: Unsupported purpose: lend; expected spend, mint, withdraw, certify, vote, propose or function elci: ELCI4001: Unknown option: --frob elci: ELCI4001: the [entrypoint] function main is not defined in module Main elci: ELCI4001: my-project/elci.toml not found; a project needs an elci.toml, which elci init creates elci: ELCI4001: Source directory not found: my-project/lib elci: ELCI4001: library-jars file my-project/libs/pricing.jar not found elci: ELCI4001: No source file for module Nope; add Nope.elm under a source directory ``` A problem with one validator's table names the table after the code: ```text elci: ELCI4001: my-project/elci.toml:6: [validators.main]: Handler Main.spent for spend: module Main does not declare spent elci: ELCI4001: my-project/elci.toml:6: [validators.main]: Handler Vault.spend for spend: no source module Vault elci: ELCI4001: my-project/elci.toml:6: [validators.main]: [java-modules] may not map Value, a module name of Elci's core library ``` The [CLI](https://elci-lang.dev/reference/cli/) and [`elci.toml`](https://elci-lang.dev/reference/configuration/) references list the others. A few errors have no code yet; they are listed [below](#errors-without-a-code). ### `ELCI4002` and `ELCI4003`: backend `ELCI4002` means Julc's PIR verifier rejected the code Elci generated. It is reported at the Elci declaration involved and keeps Julc's own `JULC00xx` code in the message. `ELCI4003` means the Julc backend in use lacks a capability the program needs, which happens only with a mismatched Julc build. Both point at a compiler problem rather than a mistake in your code: please report them with the program that triggers them. ## Errors without a code Three errors from reading a Java library are still reported without a code or location. Each is printed after `elci:`, and in the JSON report its `code` is null: | Message | Cause | | --- | --- | | `On-chain Java source not found: org.julclang.stdlib.lib.NopeLib` | An imported `[java-modules]` class that does not exist. | | `Cannot resolve type: ?. Supported types: ...` | A Java library that uses a wildcard type such as `JulcList`. | | `Unknown type: T. Did you mean: Vote?` | A Java library that declares a parameterized record such as `Box`. | --- # Example projects > The example projects in the Elci repository, what each one demonstrates and how to check, build, test or evaluate it. Source: https://elci-lang.dev/reference/examples/ The Elci repository has small example projects under `examples/`. Each one builds with the current compiler, and those with tests pass `elci test`. This page tells you what each example demonstrates and how to run it. The escrow, marketplace and order-swap examples are complete contracts: read their sources and tests as references for your own. Run the commands from the repository root, with `elci` on your `PATH` (see [Installation](https://elci-lang.dev/start/installation/)). `elci build` writes its artifacts to the example's own `build/` directory. | Example | Kind | Demonstrates | | --- | --- | --- | | [threshold](#threshold) | spend validator | the smallest validator, `Int` datum and redeemer, a Julc Java library | | [signed](#signed) | spend validator | reading the fee and signatories, the nominal `PubKeyHash` type | | [vesting](#vesting) | spend validator with tests | a record datum, a validity range, scenarios and a property | | [treasury](#treasury) | two validators with tests | spend, withdraw and mint handlers, parameters, a full test suite | | [escrow](#escrow) | spend validator with tests | `Tx` and `Interval`, a tagged payment against double satisfaction | | [marketplace](#marketplace) | parameterized spend validator with tests | two tagged payments, exact fee arithmetic, a bounded parameter | | [order-swap](#order-swap) | spend validator with tests | partial fills, continuing outputs, whole-value checks, one script input per transaction | | [functional](#functional) | function project | recursive custom types, `List.map`, folds and pipelines | | [core-completion](#core-completion) | function project | generic records and unions, nested patterns, destructuring | | [elci-library](#elci-library) | function project | a reusable library written in Elci | | [custom-library](#custom-library) | function project | a custom Java on-chain library supplied as a JAR | A function project has an `[entrypoint]` function instead of validators. You run it with `elci eval`, which prints the result in Elci syntax and then its CPU and memory cost. ## threshold [examples/threshold](https://github.com/bloxbean/elci-lang/tree/main/examples/threshold) The smallest validator in the repository. `Example.Threshold.validate` takes an `Int` datum, the minimum, and an `Int` redeemer, the offer, and accepts when the offer is at least the minimum or 0, whichever is larger. The comparison uses `Math.max` from Julc's `MathLib`, imported through `[java-modules]` as `Julc.Math`. The module lives at `src/Example/Threshold.elm`, which shows how a dotted module name maps to a path. ```sh elci check examples/threshold elci build examples/threshold ``` The example has no tests, so `elci test examples/threshold` prints `No tests found in tests` and exits with status 1. It makes a good first exercise for [scenarios](https://elci-lang.dev/testing/scenarios/): spending with a datum of 10 and a redeemer of 12 must succeed, and a redeemer of 9 must fail. ## signed [examples/signed](https://github.com/bloxbean/elci-lang/tree/main/examples/signed) A spend validator that reads the transaction. Its datum is the owner's `PubKeyHash` and its redeemer a minimum fee. It accepts when `ctx.txInfo.fee` is at least that minimum and the owner is among `ctx.txInfo.signatories`. The `PubKeyHash` is a nominal ledger type: it compares with other key hashes, not with raw bytes. ```sh elci check examples/signed elci build examples/signed ``` ## vesting [examples/vesting](https://github.com/bloxbean/elci-lang/tree/main/examples/vesting) A vesting contract that locks an output until a deadline. The datum is a record, `VestingDatum { beneficiary, deadline }`, and the redeemer a one-constructor union, `Claim`. A claim succeeds when the transaction's whole validity range is strictly after the deadline, checked with `Interval.isEntirelyAfter`, and the beneficiary signs, checked with `Tx.signedBy`. A range that could run at the deadline itself, or has no start, is rejected. `tests/VestingTest.elm` holds scenarios for claims after the deadline, in a closed range, at it and before it, and without the signature, and a property that a signed claim succeeds exactly when its range starts after the deadline. ```sh elci check examples/vesting elci test examples/vesting elci build examples/vesting ``` ```text PASS VestingTest.claimsAfterTheDeadline (cpu 16763911, mem 61637) PASS VestingTest.claimsInAClosedRange (cpu 16763911, mem 61637) PASS VestingTest.claimsAtTheDeadlineFail (cpu 13950292, mem 52228) PASS VestingTest.earlyClaimsFail (cpu 13950292, mem 52228) PASS VestingTest.unsignedClaimsFail (cpu 16458714, mem 60004) PASS VestingTest.claimsSucceedExactlyAfterTheDeadline (100 runs, max cpu 16763911, mem 61637) 6 passed, 0 failed (seed 42) ``` The output above comes from `elci test examples/vesting --seed 42`. The tutorial [Build a vesting contract](https://elci-lang.dev/tutorials/vesting/) builds the same validator step by step, with its own tests. ## treasury [examples/treasury](https://github.com/bloxbean/elci-lang/tree/main/examples/treasury) Two independent scripts in one project, both parameterized by an `admin` key: - **`treasury`** is one script with two handlers. Its `spend` handler releases a `Payout { beneficiary, amount }` to the beneficiary with `Release`, or lets the admin `Cancel`. Its `withdraw` handler lets the admin withdraw the script's staking rewards. - **`badge`** is a minting policy that accepts a positive quantity signed by the admin. `src/Auth.elm` holds the shared `signedBy` check, and each script links only what its handlers use. `tests/TreasuryTest.elm` is the most complete test suite in the repository: scenarios for every role, a budget limit, a `Bool` and an `Expectation` test, and three properties. ```sh elci check examples/treasury elci test examples/treasury elci build examples/treasury ``` ```text PASS TreasuryTest.beneficiaryReleases (cpu 11746810, mem 46979) PASS TreasuryTest.strangerCannotRelease (cpu 12125243, mem 48911) PASS TreasuryTest.adminCancels (cpu 11358693, mem 45820) PASS TreasuryTest.adminWithdrawsRewards (cpu 6103274, mem 24333) PASS TreasuryTest.rewardsNeedTheAdmin (cpu 6481707, mem 26265) PASS TreasuryTest.adminMintsBadges (cpu 5468168, mem 21200) PASS TreasuryTest.zeroBadgesAreRejected (cpu 2789841, mem 12055) PASS TreasuryTest.keysAreDistinct (cpu 1320080, mem 4623) PASS TreasuryTest.payoutsRoundTripThroughData (cpu 6436315, mem 23234) PASS TreasuryTest.payoutsAlwaysRoundTrip (100 runs, max cpu 9087645, mem 36791) PASS TreasuryTest.onlyTheBeneficiaryReleases (100 runs, max cpu 12125243, mem 48911) PASS TreasuryTest.badgesNeedAPositiveQuantity (100 runs, max cpu 5468168, mem 21200) 12 passed, 0 failed (seed 42) ``` The output above comes from `elci test examples/treasury --seed 42`. `elci build` writes `build/validators/treasury/` and `build/validators/badge/`. The tutorial [A treasury and its badge policy](https://elci-lang.dev/tutorials/treasury/) walks through the design. ## escrow [examples/escrow](https://github.com/bloxbean/elci-lang/tree/main/examples/escrow) A buyer locks a payment for a seller. The buyer releases it by signing a transaction that pays the seller at least the price, or takes it back with a transaction that runs entirely after the deadline. The seller's payment must carry the escrow's output reference as its inline datum, so one payment can't settle two escrows for the same seller. It shows `Tx.signedBy`, `Tx.ownRef`, `Tx.outputsTo`, `Tx.inlineDatum` and `Interval.isEntirelyAfter`. The tests cover each rule and each way to break it: a missing signature, an underpayment, a missing or wrong tag, one payment for two escrows, and a property over the deadline boundary. ```sh elci check examples/escrow elci test examples/escrow elci build examples/escrow ``` ## marketplace [examples/marketplace](https://github.com/bloxbean/elci-lang/tree/main/examples/marketplace) An NFT marketplace. A seller locks an NFT with a listing, a seller and a price in lovelace. Anyone may buy it by paying the seller the price less the marketplace's fee and paying the operator the fee, both tagged with the listing's output reference. The seller may cancel. The marketplace, an operator key and a fee in basis points, is a deployment parameter, so every scenario passes it with `Scenario.parameters`. It shows exact integer arithmetic for the fee, which rounds down; a parameter checked on-chain, since a fee outside 0 to 10000 basis points rejects every purchase; an operator selling their own NFT; and the attacks that the tags stop. A property checks, at any price and fee, that the exact split buys the NFT and one lovelace less to the seller does not. ```sh elci check examples/marketplace elci test examples/marketplace elci build examples/marketplace ``` ## order-swap [examples/order-swap](https://github.com/bloxbean/elci-lang/tree/main/examples/order-swap) A limit order. A maker locks tokens and asks a price, in another token, for each unit. A taker fills all or part of the order by paying the maker; the maker may cancel. A partial fill must return everything else the order holds to the script, under the same order datum. A full fill must pay the maker everything the order held besides the offered token. It shows `Tx.ownInput` and `Tx.continuingOutputs`, and checks the whole value that stays at the script: at least its lovelace, and exactly its tokens, so no one can pad the order with other tokens. It also shows both defences against double satisfaction: the maker's payment is tagged with the order's output reference, and a fill may spend only one input of the script, whatever its staking part, because the returned output carries the order rather than a tag. A property covers every fill size with exact and short payments. ```sh elci check examples/order-swap elci test examples/order-swap elci build examples/order-swap ``` ## functional [examples/functional](https://github.com/bloxbean/elci-lang/tree/main/examples/functional) A function project that sums a list of binary trees. It defines a recursive custom type, `Tree = Leaf Int | Branch Tree Tree`, a recursive function over it, and a pipeline with `List.map` and `List.foldl`. ```sh elci eval examples/functional ``` ```text 12 cpu 11011670, mem 45999 ``` ## core-completion [examples/core-completion](https://github.com/bloxbean/elci-lang/tree/main/examples/core-completion) A tour of the core language in one function project: a generic record alias `Box a` and its constructor, a generic union `Choice a`, nested list patterns with an `as` alias, tuple and record destructuring in function parameters and in `let`, and a generic equality helper. ```sh elci eval examples/core-completion ``` ```text 15 cpu 11311095, mem 41004 ``` ## elci-library [examples/elci-library](https://github.com/bloxbean/elci-lang/tree/main/examples/elci-library) A library written in Elci. `lib/Finance/Pricing.elm` exposes `totalWithFee` and keeps its `subtotal` helper private, and `src/Main.elm` imports it like any other module. Both directories are listed in `source-directories`, so no Java mapping or JAR is needed. To share such a library, share its source directory and add it to the consuming project's `source-directories`. ```sh elci eval examples/elci-library ``` ```text 105 cpu 2943076, mem 14126 ``` ## custom-library [examples/custom-library](https://github.com/bloxbean/elci-lang/tree/main/examples/custom-library) A custom on-chain Java library. `java/META-INF/plutus-sources/example/Pricing.java` is a Julc `@OnchainLibrary` class with a Java record, `Quote`. The project lists the JAR in `library-jars` and maps the class to the Elci module `Custom.Pricing` in `[java-modules]`. Elci reads the Java source from the JAR and Julc compiles it; no JVM code from the JAR runs. `Quote` stays a nominal type: its fields can be read, and values are made with the factory `Pricing.quote`. Package the JAR first, then evaluate: ```sh mkdir -p examples/custom-library/build jar --create --file examples/custom-library/build/pricing.jar -C examples/custom-library/java . elci eval examples/custom-library ``` ```text 42 cpu 1525434, mem 6453 ``` [Java libraries](https://elci-lang.dev/advanced/java-libraries/) covers library JARs in detail.