EYG Syntax Guide
EYG is a type-safe scripting language with managed effects. It has a minimal surface area: everything is an expression, there are no statements, and effects are explicit.
Comments
Lines beginning with // are comments. They are ignored by the parser.
// This is a comment
let x = 5
// x is now 5
x
Blocks
Multiple let statements form a block.
EYG is expression based and bare statements (without a let) are not support.
Literals
Integers
Integers are written as sequences of digits. Negative integers use a leading -.
let x = 42
let y = -7
0
Strings
Strings are enclosed in double quotes. Supported escape sequences:
| Escape | Meaning |
|---|---|
\n | Newline |
\t | Tab |
\r | Carriage return |
\" | Double quote |
\\ | Backslash |
let a = "hello, world"
let b = "line one\nline two"
"she said \"hi\""
Variables
A variable is any lowercase identifier (letters, digits, and underscores, starting with a letter or underscore).
let _ = x
let _ = my_value
counter1
Let Bindings
let binds a value to a name. In a block (at the top level or inside a function body), multiple let bindings are written on successive lines and their scope extends to the end of the block.
let x = 5
let y = 10
x
In expression position (nested), let takes two expressions: the value and the continuation.
let x = 5 x
Destructuring Records
let can destructure a record into its fields. Each field is extracted by name.
let {name: first, age: n} = {name: "Bob", age: 25, admin: True({})}
first
Any unlisted fields in the pattern are ignored, there is no rest or spread syntax.
If the variable name matches the field name, the : variable part can be omitted:
let {name, age} = person
name
An empty destructuring pattern {} is valid and binds nothing:
let {} = record
result
Functions (Lambdas)
Functions are written with a parameter list in parentheses, ->, an opening {, a body expression, and a closing }.
(x) -> { x }
Multiple parameters are separated by commas. Multiple-argument functions are curried — each parameter produces a nested single-argument function.
(x, y) -> { x }
Destructuring Parameters
Function parameters can be record patterns:
({name, age}) -> { name }
Calling Functions
Apply a function by appending a parenthesised argument list:
let double = (x) -> { !int_multiply(x, 2) }
double(5)
Multiple arguments are written separated by commas. Since functions are curried, f(a, b) is syntactic sugar for f(a)(b).
let add = (x, y) -> { !int_add(x, y) }
add(3, 4)
*EYG has no loop or each construct, instead use !fix, !list_fold or libraries.*
Recursion
To write a recursive function use the !fix builtin.
!fix passes the function itself as the first argument to the function.
let factorial = !fix((self, n) -> {
match !int_compare(n, 0) {
Eq(_) -> { 1 }
| (_) -> { !int_multiply(n, self(!int_subtract(n, 1))) }
}
})
factorial(5)
Fold
To iterate the items of a list use !list_fold.
All iteration patterns can be built on !list_fold, use an effect for early return.
Function Application (Calling)
Any expression followed by (args) applies it as a function. Chained calls are left-associative.
let _ = f(x)
let _ = f(x)(y)
outer(inner(value))
Records
Records are ordered collections of named fields, written with {}.
let _ = {}
{name: "Alice", age: 30}
Field Access
Fields are accessed with .:
person.name
Record Update (Overwrite)
{..record} passes a record through unchanged. Prepending field assignments with ..record overwrites those fields in the existing record:
{age: 31, ..person}
Record Shorthand
When the field name and variable have the same name, you can omit the : value:
let name = "Alice"
{name}
// equivalent to {name: name}
Lists
Lists are written with []. Items are separated by commas. A trailing comma is allowed.
let l1 = []
let l2 = [1, 2, 3]
["a", "b",]
Spread
..expr spreads an existing list as the tail:
let list = [1, ..rest]
Tags (Variants)
Tags create tagged values (like union variants). A tag name starts with an uppercase letter.
let _ = Ok
let _ = Error
let _ = True
False
Tags are applied as functions:
let _ = Ok(value)
Error("something went wrong")
Match
match deconstructs tagged values. Cases are written as TagName variable -> { expression }.
match result {
Ok(value) -> { value }
Error(msg) -> { 0 }
}
The match expression can be written inline (with the subject before the {}):
match Ok(5) {
Ok(n) -> { n }
Error(_) -> { 0 }
}
*_ is the conventional placeholder when a value is discarded.*
Or in "function" form (without a subject), producing a function that takes the value to match:
let handle_result = match {
Ok(value) -> { value }
Error(_) -> { -1 }
}
handle_result(Ok(42))
Else Branch
An else branch catches any tag not handled by previous cases. It is written with |:
match x {
Ok(value) -> { value }
| (other) -> { -1 }
}
What match does NOT support
match only deconstructs tagged values.
| Unsupported | Use instead | ||
|---|---|---|---|
match n { 2 -> { ... } } | Dispatch on a tag via !int_compare returning Lt({}) \ | Eq({}) \ | Gt({}). |
match x { _ -> { ... } } | An else branch: \ | (_) -> { … }. | |
match xs { [] -> { 0 } } | !list_pop(xs) returns Ok({head, tail}) \ | Error({}); match on that tag. | |
Ok(Some(v)) -> ... | Match outer tag, then match on the inner value inside the branch. |
If a branch ignores the payload entirely, the conventional placeholder
is _:
Effects
EYG separates the description of an effect from its implementation. Scripts declare effects they need; the runner decides what to do with them.
Perform
perform EffectName sends an effect. The effect name must start with an uppercase letter.
perform Log("hello")
Handle
handle EffectName returns a handler function for the named effect:
handle Log
handle is used to install custom effect handlers in advanced embedding scenarios.
Builtins
Builtins call built-in runtime operations. They start with ! followed by a lowercase identifier.
let seven = !int_add(3, 4)
let product = !int_multiply(x, y)
!string_append("hello", " world")
See builtins_reference.md for the full list.
References
A reference is an immutable content-addressed module identifier. It starts with # followed by a valid base32-encoded CID.
#bafyreigdmqpykrgxyahdnfmfzmc5j4bkwci6wf6fkdbapq7hfpmg2j3yqy
Packages
A package reference starts with @. Three forms are accepted:
| Syntax | Meaning |
|---|---|
@standard | Latest published version (resolved at evaluation time). |
@standard:3 | Exactly version 3. The hash is whatever the registry has for it. |
@standard:3:bafyrei… | Exactly version 3, pinned to a specific module CID. |
The version is a positive integer. The pinned hash, when given, must be a valid base32-encoded CID.
A bare @standard references the latest pulled release of that package.
Pin a version (@standard:3) for reproducible scripts.
Provide a hash (@standard:3:…) to not require trust in the package hub.
Imports
import loads a module by path.
The path must be a string literal.
Relative paths are resolved from the directory of the source file that contains the import.
An import expression cannot be in a shared or published module.
let fs = import "../eyg_packages/fs/index.eyg"
let {list} = import "../eyg_packages/standard/index.eyg.json"
The imported value is the module's exported value (usually a record of functions).
Vacant
vacant (shown as ? in the structural editor) is a placeholder for an expression that has not yet been written. It has no textual syntax but appears in the IR. The parser produces Vacant when a let binding is the last line of a block with no following expression.
Complete Example: Filtering Files
let fs = import "../eyg_packages/fs/index.eyg"
let {list} = import "../eyg_packages/standard/index.eyg.json"
let files = fs.list_files({root: ".", ignore: [".git"]})
let gleam_files = list.filter(
(path) -> { !string_ends_with(path, ".gleam") },
files
)
gleam_files
Complete Example: Pattern Matching
let parse_age = (s) -> {
match !int_parse(s) {
Ok(n) -> { Ok(n) }
Error(_) -> { Error("not a number") }
}
}
match parse_age("42") {
Ok(age) -> { perform Print(age) }
Error(msg) -> { perform Print(msg) }
}