EYG
Guides

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:

EscapeMeaning
\nNewline
\tTab
\rCarriage 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.

UnsupportedUse 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:

SyntaxMeaning
@standardLatest published version (resolved at evaluation time).
@standard:3Exactly 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) }
}
Want to stay up to date?
Join our mailing list.