Language reference
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.
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 defines the syntax exactly.
Feature matrix
Section titled “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. |
| 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.<name>] 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
Section titled “Syntax summary”This module uses most of the syntax:
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 -> Intarea shape = case shape of Circle radius -> 3 * radius * radius
Rect width height -> width * height
describe : List Int -> Stringdescribe values = case values of [] -> "empty"
[ _ ] -> "one"
0 :: _ -> "starts with zero"
first :: _ -> if first < 0 then "starts negative"
else "many"
norm : Point -> Intnorm { x, y } = x * x + y * y
swap : Pair a -> Pair aswap ( first, second ) = ( second, first )
main : Intmain = 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
Section titled “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.
Lexical rules
Section titled “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,typeandport.aliasis only special aftertype. Don’t use them as names: the parser doesn’t yet reject every such use, as the grammar’s parser details describe. - Integers. Decimal digits only, arbitrarily large.
1.5is rejected (Float is unsupported), and so are0xFFand1_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
Section titled “Imports and names”Every module has Elm’s default imports, which you don’t write:
import Basics exposing (..)import Listimport Maybe exposing (Maybe(..))import Result exposing (Result(..))import Stringimport Tupleimport DebugBytes, 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.maxandResult.Okalways work. A qualified name is written without spaces and ends at its first lowercase part, soConfig.settings.feeis the fieldfeeof the valueConfig.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. - An import can’t be aliased to the name of another module, core or in the project:
import Utils as Stringandimport Julc.Interval as IntervalareELCI2104.
Core modules has the details and the messages.
Reserved names
Section titled “Reserved names”-
Module names.
Basics,List,Maybe,Result,String,Tuple,Map,Dict,Array,Value,IntervalandTxare core modules, andSetandElciare reserved for the core library. Neither a project module nor a[java-modules]key can use these names, or a name that starts withElci.:src/String.elm:1:1: ELCI2104: The module name String is taken by Elci's core library; rename this modulesrc/Value.elm:1:1: ELCI2104: The module name Value is taken by Elci's core library; rename this modulesrc/Tx.elm:1:1: ELCI2104: The module name Tx is taken by Elci's core library; rename this modulesrc/Set.elm:1:1: ELCI2104: The module name Set is reserved for Elci's core library; rename this modulesrc/Elci/Tools.elm:1:1: ELCI2104: The module name Elci.Tools is reserved for Elci's core library; rename this moduleelci: ELCI4001: [java-modules] may not map Value, a module name of Elci's core libraryBytes,DebugandJulc.Databelong to the compiler as well. -
Constructors and types. A module can’t declare constructors named
Just,Nothing,TrueorFalse, or types namedInt,Bool,String,Bytes,Data,List,Maybe,Dict,Map,ArrayorFloat:src/Main.elm:4:6: ELCI2011: Nothing is a constructor of the built-in Maybe type; choose another namesrc/Main.elm:4:6: ELCI2011: Bytes is a built-in type; choose another nameOther prelude names, such as
Ok,Orderormax, can be declared; your declaration shadows the prelude’s in your module. -
Type names. Tuples are written
( a, b )and unit().Tuple Int IntgivesELCI2012: Write tuple types as ( a, b ) or ( a, b, c ), andUnitgivesELCI2012: Write the unit type as ().
Operators
Section titled “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 onInt.2 ^ 10is1024,2 * 3 ^ 2is18, and a negative exponent fails the script.<,>,<=and>=compare two values of the same comparable type.==and/=work on equality-capable types (below)./is rejected; use//for the integer quotient, andmodByorremainderByfor the rest.%does not exist.a < b < cis rejected: writea < b && b < c.
Operators as functions
Section titled “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.
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 ] )$ elci eval( 6, [4, 5, 9, 8], [True, False] )cpu 16223362, mem 77496Unary minus
Section titled “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.
module Main exposing (main)
scale : Int -> Int -> Intscale value factor = value * factor
main : List Intmain = let x = 4 in [ scale -x 10 , modBy 3 -7 , x - 1 , -x * 2 , -(scale x 2) , 2 * -3 ]$ elci eval[-40, 2, 3, -8, -8, -6]cpu 2719341, mem 9131This 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
Section titled “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:
src/Main.elm:6:21: ELCI2003: The operator == cannot compare List (Int -> Int) values: Int -> Int is part of them, and functions have no equalityOrdering
Section titled “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;Bytesand the ledger’s byte-string types (PubKeyHash,ScriptHash,PolicyId,TokenName,TxIdand 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
Section titled “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.
module Main exposing (main)
largest : comparable -> List comparable -> comparablelargest first rest = List.foldl max first rest
twice : appendable -> appendabletwice value = value ++ value
main : ( String, List Int )main = ( twice (largest "fig" [ "apple", "pear" ]), twice [ largest 3 [ 9, 4 ] ] )$ elci eval( "pearpear", [9, 9] )cpu 8891872, mem 43250Comparing or appending values of a plain variable is an error that suggests the rename:
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 valuesA 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:
src/Main.elm:11:5: ELCI2003: `twice` appends String or List values, not Intsrc/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 IntEquality 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
Section titled “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:
import Julc.Data as Data
main : Intmain = List.length (Data.decode (Data.encode [ 1, 2 ]))src/Main.elm:8:18: ELCI3001: Cannot determine the type converted by Julc.Data.decode; add a type annotationAnnotating the decoded value, as in a local amounts : List Int, fixes it.
Evaluation order
Section titled “Evaluation order”Elci is strict. These rules are part of the language, because they decide which failures and costs happen:
- Application. The function, then its arguments from left to right. A partial application evaluates the arguments it has immediately.
let. Every binding is evaluated, even if unused, before the body. Bindings run in dependency order; independent ones run in source order.- 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.
- Record update. The base record first, then the new field values in source order.
- Selective forms.
ifevaluates the condition and one branch.caseevaluates the scrutinee once and one branch.&&and||evaluate the right operand only when needed. - Composition.
f >> gevaluatesfandgwhen the composition is built. - Validators. Top-level values reachable from any handler are evaluated once per script run, before the purpose is dispatched.
This program traces every step:
module Main exposing (main)
type alias Order = { price : Int, amount : Int }
add : Int -> Int -> Intadd left right = left + right
main : Intmain = 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$ elci eval19cpu 3661600, mem 13949trace: subtotaltrace: totaltrace: unrelatedtrace: pricetrace: amounttrace: lefttrace: righttrace: eq-lefttrace: eq-rightsubtotal runs before total because total needs it, and unrelated runs although nothing uses it.
Data encoding
Section titled “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:
module Main exposing (main)
import Julc.Data as Data
type Action = Buy Int | Cancel
type alias Payout = { beneficiary : Int, amount : Int }
main : List Datamain = [ 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" ]$ 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 25242The 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.
Compiler limits
Section titled “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.