Elfie

Documentation / Context layer

Context layer

Every entity in Elfie has a context layer: its name, what it means, what it must do, and how it is tested. The compiler reads this layer to write your code, and the verifier reads it to check that code.

Three layers

Every entity has three layers, and each one has its own accessor.

LayerAccessorHoldsExample
Value.What the entity is at run time.order.status
Context@What the compiler knows about it: description, type, criteria, tests.Order@acceptanceCriteria
Scope$The members it declares, as entities of their own.Order$status

The value layer works like it does in other languages. The scope layer is covered in Scope management. This page is about the context layer, the one most languages don't have.

Take const x = 2;. The value of x is 2, and x@type is number. In a fn, the context layer holds nearly everything: a fn has no body for you to write, only context for the compiler to satisfy.

Descriptions

A description in backticks after a colon becomes the entity's @definition. You can describe data, functions, traits, types, enums, members, and variables.

def/library.lfy
d Book: `A book in the library` {
  $title: `The title on the cover` = string;
  $pages: `How many pages it has` = number;
}

Write descriptions as plain facts that someone could check: what a thing is, what it holds, and what it returns. The compiler treats them as part of the specification.

Reading context

Name@member reads one part of an entity's context. Inside a declaration's body, @ on its own is the entity being declared.

def/library.lfy
/// book: the book to summarize
fn summary(book: Book): `A one line summary of [[book]]` => string {
  @acceptanceCriteria
    .add({ behavior = `Begins with [[Book.title]]` })
    .add({ behavior = `Ends with the page count, such as 312 pages` });
  @test({ input = [{ title = "Dune", pages = 412 }], expect = "Dune, 412 pages" });
}

const bookName = Book@identifier;
const titleType = Book$title@type;
const summaryInputs = summary@parameters;
const generated = summary@agentic;
MemberWhat it holds
@identifierThe declared name.
@definitionThe description.
@typeThe declared or inferred type.
@acceptanceCriteriaIts own criteria, then those of each trait, in order.
@testsEvery test added.
@traitsEvery trait applied to it, directly or inherited.
@referencesEvery entity its description, documentation, or value refers to.
@knowledgeWhat the compiler is given to read for it.
@commandsShell commands for operations such as build and test.
@targetsThe targets it is built for.

Functions add @parameters, @output, and @agentic. Traits add @entities and @extenders. The Context API lists every member for every kind of entity.

Writing context

You add to the context layer with calls that read like method calls but run while the program is compiled.

CallAdds
@acceptanceCriteria.add({ ... })One acceptance criterion.
@test({ input, expect }, ...)One or more test cases.
@knowledge.add({ topic, kind, source })Reading material for the compiler.
@commands.add({ operation, line })A shell command for one operation.
@targets.add(target)A target to build the entity for.

Each add returns the list, so calls chain. To add context to an entity from outside its body, start with its name:

def/targets.lfy
mainApp
  @acceptanceCriteria
  .add({ behavior = `Favicon is [[favicon]]` });

References

A template, a description, or a documentation line can refer to anything in scope with [[...]].

ReferenceRefers to
[[Order]]A declared entity.
[[Order.status]]A member of an entity.
[[items]]A parameter of the current fn or trait.
[[.]]The current entity, such as the entity a trait is applied to.
[[&owner]]The entity a parameter is bound to, rather than its value.

References are checked: elfie check reports one that resolves to nothing. They also connect your definitions, so @references, find references in your editor, and the planner all see them. To write double brackets as plain text, escape them: \[[.

Executions

{{expression}} evaluates an expression while the program is compiled and writes its value into the text.

def/traits.lfy
trait audited: `Every change to it is recorded` {
  where (`It changes`) -> `One audit record names {{@identifier}} and what changed`;
}

In a trait, {{@identifier}} is the name of the entity the trait is applied to. Parameters work too: a trait endpoint(method, path) can say It answers {{method}} {{path}}. See At Compile Execution (ace).

Values only the compiler chooses

Type@like(prompt) stands for any value of a type that a prompt describes. You use it in tests, where writing out a full value would be noise.

Elfie
@test({ input = [string@like(`text with no number in it`)], expect = undefined });

The compiler chooses the value. Generated code never calls like. See Tests & documentation.


Was this guide clear?

Share a suggestion