Language reference

This page contains an overview of all the current features in Eclair and their corresponding syntax.

Declarations

A program consists of zero or more top-level declarations. Declarations can be type definitions, atoms, rules or extern definitions. Together, they can be used to describe logical queries that Eclair will search for in a given dataset.

Syntactically, a period (.) is used to mark the end of each declaration.

Type definitions

Before you can use a relation in a atom or rule, you will first need to declare them. A type definition consists of the name of the relation, along with the types of each of their arguments. Note that a relation requires always at least one argument.

Besides the name of the relation and its arguments, you can also specify if the relation is meant to be used as an input, output, as both input and output.

Important: If you do not add any usage qualifier, Eclair will assume the relation is only used internally. If you then try to add data or retrieve results from an internal relation, Eclair will do nothing (because no code for this functionality would have been generated).

Below are some example type definitions:

@def input_example(arg1: u32) input.
@def output_example(arg1: string, arg2: u32) output.
@def input_output(arg1: string) input output.
@def internal_relation(arg1: string).

Atoms

Once a relation is defined, it can be used as an atom. Atoms can appear either directly as a top-level declaration, or inside a rule. A top-level atom is also referred to as fact. When an atom is used inside the body of a rule, they are referred to as a (rule) clause.

An atom consists of the name of the relation, followed by one or more arguments. Note that for top-level atoms, it is only allowed to use constant literals for one of the arguments; variables or wildcards are not allowed. The snippet below shows examples of some facts:

@def person(first_name: string, last_name: string, age: u32).
@def siblings(first_sibling: string, second_sibling: string).

person("John", "Doe", 30).
person("Jane", "Doe", 27).

siblings("John", "Jane").

In the next section about rules, more examples will be given how to use atoms inside rule bodies.

Besides facts defined directly in Eclair code, it is also possible to use the Eclair API to insert input facts or retrieve output facts dynamically.

Rules

Besides atoms, relations can also be used in rules. Rules can be used to describe what patterns to search for in a dataset.

A rule consists of a rule head and a rule body consisting of one or more rule clauses. The syntax for a rule is as follows:

rule_head(arg1, arg2) :-
  clause1(arg1, arg2),
  // ...
  clauseN(arg1).

Eclair will evaluate the above rule as follows:

  1. For each of the rule clauses in the rule body: check if a matching atom can be found,
  2. If this is the case, add the relation with the values in the rule head to the query results.

Multiple clauses are separated by a comma (,). This forms a logical conjunction (AND) of the clauses (all clauses need to be satisfied for the rule to true).

Besides conjunction, it is also possible to create a logical disjunction (OR) with rules. You can do this by writing down two or more rules, each with different rule bodies:

rule_head(arg1, arg2) :-
  // rule body 1...

rule_head(arg1, arg2) :-
  // rule body 2...

In this scenario, the rule head will be added to the query results if one of the two rule bodies is satisfied.

Clauses can be negated (logical NOT) by prepending a exclamation mark (!) to a clause. Eclair will then look if that clause is not satisfied. An example of negation can be found below:

rule_head(arg1, arg2) :-
  clause1(arg1, arg2),
  !clause2(arg2).

Negation has additional constraints to guarantee that the program is well-structured and will terminate. More information regarding logical negation in rules can be found here.

Rules can be recursive (this happens when the relation in the rule head also appears in the rule body). Eclair will use bottom-up evaluation to iteratively look for results until no new information can be deduced. An example of a recursive query can be found below. It makes use of two rules to find all reachable vertices in a graph:

@def edge(from: u32, to: u32) input.
@def reachable(start: u32, end: u32) output.

// 2 points are reachable iff
// there is a direct edge between those points.
reachable(x, y) :-
  edge(x, y).

// 2 points are reachable iff
// there is a direct edge between a point x and a third point z,
// AND point y is reachable from point z
reachable(x, y) :-
  edge(x, z),
  reachable(z, y).

Extern definitions

Eclair has support for linking against functions defined in another language. This is possible via extern definitions (also called user-defined functions). This helps keep the language small while still being extendable.

User-defined functions do not ground any of their arguments, but they do ground their return value if all arguments were also grounded.

Extern definitions are declared in Eclair via the @extern keyword, followed by the name of the function and one or more arguments, possibly followed by a return type. The snippet below shows an example of how to use extern definitions in Eclair:

@extern my_function(u32) u32.
@extern my_constraint(u32).
@def rule(u32).
@def fact(u32).

rule(x) :-
  fact(x),
  y = my_function(x),
  my_constraint(y).

You can find more detailed information about user-defined functions here.

Primitive types

Eclair currently has support for the 32-bit unsigned integers and string primitive types. Other types such as signed integers are planned and will be added in the future.

Integers

Integer literals should be written down in decimal form. Integer arguments in type definitions are specified with the u32 keyword. The following snippet shows the syntax for using integers:

@def coordinate(x: u32, y: u32).

coordinate(3, 7).
coordinate(8, 4).

Integers can be used in both arithmetic expressions or comparisons.

Strings

String literals in Eclair start and end with a double-quote (") and can contain zero or more characters. String arguments in type definitions are specified with the string keyword. The code below is a small example of how you can use string literals:

@def book(title: string).

book("Structure and Interpretation of Computer Programs").
book("7 languages in 7 weeks").

Internally, strings are stored as UTF-8-encoded bytearrays with a known length. They do not have a 0-terminator like in C.

Eclair only provides very minimal functionality for strings in the form of (in-)equality comparisons. This is done to keep the language small. If you do need extra functionality (e.g. regex support), you can add this functionality with user-defined functions.

Variables

Eclair supports the use of variables inside rules. Unlike most languages, variables do not need to be declared up front. Instead you can simply start using them in a rule, and Eclair will generate the necessary code to properly initialize the variables.

If a variable occurs multiple times in a single rule body, an implicit equality check is added to make sure the variables are equal. The code below shows an example where this is used to find 3 consecutive links that form a chain:

chain(first, last) :-
  link(first, second),
  link(second, third),
  link(third, last).

Variables always start with a letter and are followed by zero or more alpha-numeric or underscore characters.

Variables are only allowed in rules (both in the rule body and rule head), not in facts. In order for Eclair to generate valid code, variables need to be grounded.

Wildcards

Wildcards are a special kind of variable that you can use in Eclair when you don’t care about a specific argument value in a rule clause. A wildcard variable unifies with every value. The syntax for a wildcard is a single underscore (_). The code below shows how you could use wildcards to ignore some arguments in a fact:

@def person(name: string, age: u32).
@def person_name(name: string).

person_name(name) :-
  person(name, _).

Contrary to regular variables, when a rule body contains more than two wildcards, they will not unify with each other. In other words, each wildcard is treated as a unique variable. The following example shows how the compiler would rewrite a rule containing multiple wildcards:

rule(x) :-
  fact1(x, _),
  fact2(x, y, _).

// will be rewritten as:

rule(x) :-
  fact1(x, wildcard_var1),
  fact2(x, y, wildcard_var2).

Wildcards are not allowed in rule heads or in arithmetic expressions; they are only allowed as arguments of rule clauses. This restriction is necessary to make sure the program can be properly compiled.

Typed holes

Typed holes (also referred to as just holes) represent a piece of Eclair code that is unfinished. They make it possible to have a valid Eclair program that is not fully thought out yet, but that the compiler can still reason about.

Holes are allowed in any location in a rule body where expressions (literal values or variables) are allowed. The syntax for a typed hole is a single question mark (?).

reachable(x, y) :-
  edge(x, ?),
  reachable(z, y).

fact(?).

rule(x) :-
  x = ?,
  fact(x).

When you use a typed hole, you can ask the Eclair compiler to show you the type-level information that it deduced at that location in the program. This can be useful to help figuring out type errors in your program. For more information about typed holes, check out the typesystem documentation.

Operators

Eclair provides built-in operators for doing arithmetic or comparing values. It is not possible to add custom operators. The following sections provide more information on where these operators are allowed and how they can be used.

Comparisons

Eclair supports built-in operators for comparing 2 values. These can be divided into equality constraints (= and !=), and inequality comparisons (<, <=, > and >=). Equality constraints are supported on all types. The comparison operators are supported only on integer values.

Important: the equality operator has different behavior compared to most other languages! In Eclair, equality is actual mathematical equality; it is not an assignment of a variable. This means that when you write x = 3, you are saying that x equals to 3. (On a side note, this means you can also write 3 = x.) Besides this, the equality operator also plays an important rule in grounding variables.

The code below shows how comparisons can be used in Eclair.

rule(x) :-
  fact(x),
  x < 10,
  x >= 3,
  x != 7.

Comparison operators can only appear in a rule body. Only one comparison is possible per clause, compound in-equality comparisons are not supported.

Arithmetic

Eclair supports arithmetic operators found in most other languages. For now the supported operators are +, -, *, and /. In the future more operators will be added (bitwise operators, logical operators, …).

Operators can be used in any rule clause and even in the rule head, as long as both arguments of the binary operation are grounded.

Expressions with arithmetic operators can be nested, and have the same precedence rules as in math. Parentheses can be used to override the order in which sub-expressions are evaluated.

The snippet below showcases how you can use arithmetic operators:

rule(x + y) :-
  fact1(x),
  fact2(y + 5 * 7),
  y = (1 + 8) / 3 * 4.

Arithmetic operations are only supported on integers. If you want to do e.g. string concatenation (commonly done with + in other languages), then you will need to create a user-defined function that implements this behavior.

Comments

Comments in Eclair have the same syntax as C-style languages. They can appear anywhere in the code. Single-line comments are prefixed with // and continue until the end of the current line. Multi-line comments start with /* and end with */ and can span multiple lines. Below is a snippet of Eclair code with some comments:

// A single line comment

/*
A comment
spanning
multiple lines.
*/
© 2021-2023 Luc Tielen