Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

FOL Language Specification

FOL is intended as a general-purpose and systems-oriented programming language with a compact declaration syntax, a rich type system, and a strong preference for explicit structure.

This book is the specification draft for the language.

Core Shape

Much of the surface syntax follows the same high-level shape:

declaration[options] name: type = { body }

That pattern appears across:

  • bindings
  • routines
  • type declarations
  • module-like declarations
  • standards and implementations

Main Declaration Families

The main declaration families are:

use    // import declarations
def    // named definitions such as modules, blocks, and tests
seg    // segment/module-like declarations
imp    // implementation declarations

var    // mutable bindings
let    // immutable local bindings
con    // constants
lab    // labels and label-like bindings

fun    // functions
pro    // procedures
log    // logical routines

typ    // named types
ali    // aliases
std    // standards: protocol, blueprint, extension

Expression And Control Surface

FOL combines block-oriented control flow with expression forms:

if (condition) { ... }
when (value) { ... }
while (condition) { ... }
loop (condition) { ... }
for (binder in iterable) { ... }
each (binder in iterable) { ... }

Expressions include:

  • literals
  • calls and method calls
  • ranges and container literals
  • access forms
  • pipes
  • rolling expressions
  • anonymous routines and lambdas

How To Use This Book

The book is organized from language foundation to higher-level facilities:

  1. lexical structure
  2. statements and expressions
  3. metaprogramming
  4. types
  5. declarations and items
  6. modules and source layout
  7. errors
  8. sugar and convenience forms
  9. conversions
  10. memory model
  11. concurrency

Read Notation And Conventions before using chapter examples as a normative reference.

For the current user-facing tool workflow, read Frontend Workflow.

Example

use log: std = {"fmt/log"};

var[hid] prefix: str = "arith";

fun[] main(): int = {
    .echo(prefix);
    .echo(add(3, 5));
    return 0;
}

fun[exp] add(a, b: int): int = {
    return a + b;
}

Overview

This opening section explains what FOL is, how this book is organized, and how syntax examples are written.

Use it before diving into the detailed lexical, type, item, and runtime chapters.

It also describes the user-facing workflow tool and the first editor-tooling surface before the language chapters begin.

Notation And Conventions

This book is the language specification for FOL.

It is written as a spec first:

  • examples describe intended language behavior
  • implementation status may lag behind the book
  • when code and prose disagree, the disagreement should be resolved explicitly

Reading Syntax Examples

The examples in this book follow a few conventions.

  • Keywords are written literally:
    • fun
    • pro
    • typ
    • use
  • Punctuation is written literally:
    • (
    • )
    • [
    • ]
    • {
    • }
    • :
    • =
  • Placeholder names are descriptive:
    • name
    • type
    • expr
    • body
    • source

For example:

fun[options] name(params): return_type = { body }

means:

  • fun is a literal keyword
  • options is a placeholder for zero or more routine options
  • name is the declared routine name
  • params is a parameter list
  • return_type is a type reference
  • body is a routine body

Spec Vocabulary

The book uses the following terms consistently:

  • declaration: a top-level or block-level form that introduces a named entity
  • statement: an executable form that appears in a block
  • expression: a form that produces a value
  • type reference: a syntactic reference to a type
  • type definition: a declaration form that creates a new named type
  • source kind: an import/source locator family such as loc, pkg, or std

Examples vs Grammar

Examples are illustrative, not exhaustive.

When a chapter gives one or two examples, that does not imply the syntax is limited to only those exact spellings. The normative rule is the chapter text plus the grammar intent described there.

Terminology Preferences

This book prefers the following terms:

  • routine: umbrella term for fun, pro, and log
  • record: named field-based type
  • entry: named variant-based type
  • standard: protocol/blueprint/extension-style contract surface
  • module: namespace/package surface addressed through use, def, or seg

Status Notes

Some older chapters still use inconsistent wording inherited from earlier drafts.

During cleanup, the following principles apply:

  • keep examples unless they are contradictory
  • prefer clarifying rewrites over removal
  • keep the chapter tree stable where possible
  • make chapter indexes explain scope before detail

Frontend Workflow

FOL now has a dedicated frontend layer above the compiler pipeline.

That layer is the fol tool itself.

The frontend is implemented in fol-frontend, and it is the user-facing workflow shell for:

  • project/workspace setup
  • fetch/update flows
  • build/run/test/emit flows
  • editor tooling dispatch under fol tool

The detailed reference has moved to the Tooling section:

Use this overview page only as the entrypoint pointer.

  • workflow commands
  • direct compile dispatch
  • root help
  • output rendering
  • frontend diagnostics

So the root binary is no longer its own separate CLI implementation.

Current Boundary

The current frontend milestone is about local workflows and the first backend.

It already covers:

  • project and workspace scaffolding
  • documented build.fol package entry files for new projects
  • root discovery
  • package preparation through fol-package
  • git-backed dependency fetching and materialization
  • fol.lock writing, locked fetches, offline warm-cache fetches, and update flows
  • workspace dependency/status reporting
  • full V1 build/run/test orchestration
  • routed workspace build/run/test/check entry through build.fol
  • emitted Rust and lowered IR output
  • editor-tooling entrypoints for parse, highlight, symbols, and LSP startup
  • shell completions
  • safe cleanup of build/cache/git/package-store roots
  • frontend-owned direct compile routing

Future work is still expected around:

  • richer package-store policy beyond the first git/store workflow
  • lockfile/version solving beyond the current pinned git contract
  • additional backend targets

Editor Tooling

FOL now has a dedicated editor-tooling crate:

  • fol-editor

It owns:

  • Tree-sitter assets for syntax/highlighting/query work
  • the language server for compiler-backed editor services
  • build-file affordances for build.fol through the same parse/highlight/symbol and LSP surfaces used for ordinary source files
  • one public editor command surface under fol tool ...

The detailed operational reference now lives in the Tooling section:

Use this page as the overview pointer, not the full reference.

Tooling

FOL now has a real tooling layer above the compiler.

That layer is split into:

  • the fol frontend workflow tool
  • editor-facing tooling under fol tool ...

Use this section for:

  • creating and organizing projects
  • fetching and updating packages
  • building, running, testing, and emitting artifacts
  • understanding the editor stack
  • integrating FOL with Neovim

The overview chapter only introduces the existence of these pieces.

This section is the detailed operational reference.

Frontend Workflow

The public FOL entrypoint is the fol tool.

fol is implemented by the fol-frontend crate. It sits above:

  • fol-package
  • fol-resolver
  • fol-typecheck
  • fol-lower
  • fol-backend

Its job is orchestration, not semantic analysis.

What The Frontend Owns

The frontend owns:

  • CLI parsing with clap
  • grouped command structure
  • package and workspace discovery
  • project scaffolding
  • package preparation and fetch/update workflows
  • build, run, test, and emit orchestration
  • editor command dispatch under fol tool
  • shell completions
  • user-facing summaries and workflow errors

Compiler truth remains in the compiler crates.

Public Command Groups

The root command groups are:

  • fol work
  • fol pack
  • fol code
  • fol tool

Root aliases are single-letter only:

  • fol w
  • fol p
  • fol c
  • fol t

Run:

fol --help
fol <group> --help
fol <group> <command> --help

for the relevant usage surface.

Root Discovery

For workflow commands, the frontend discovers roots upward from the current directory or from an explicit path.

It recognizes:

  • package roots via package.yaml
  • workspace roots via fol.work.yaml

A package root is one buildable package.

A workspace root is a parent root that owns multiple member packages and frontend-managed roots like build/cache/package-store locations.

Direct Package Use

The normal workflow shape is group-first:

fol work init --bin
fol pack fetch
fol code check
fol code build
fol code run

There is still a frontend-owned direct package path for compile-oriented use, but the grouped commands are the public workflow shape.

Output

Frontend summaries support:

  • human
  • plain
  • json

The frontend also owns workflow-level errors such as:

  • missing workspace roots
  • fetch/update/store failures
  • clean/completion/workflow command failures

Compiler diagnostics stay separate and flow through compiler-owned diagnostic models.

Tool Commands

This chapter lists the current frontend surface by workflow area.

Work

Project and workspace commands:

  • fol work init
  • fol work new
  • fol work info
  • fol work list
  • fol work deps
  • fol work status

Examples:

fol work init --bin
fol work init --workspace
fol work new demo --lib
fol work info
fol work deps

Use work for:

  • creating package/workspace roots
  • inspecting workspace structure
  • seeing member and dependency state

Pack

Package acquisition commands:

  • fol pack fetch
  • fol pack update

Examples:

fol pack fetch
fol pack fetch --locked
fol pack fetch --offline
fol pack fetch --refresh
fol pack update

Use pack for:

  • materializing dependencies
  • writing or honoring fol.lock
  • refreshing pinned git dependencies

Code

Build-oriented commands:

  • fol code check
  • fol code build
  • fol code run
  • fol code test
  • fol code emit rust
  • fol code emit lowered

Examples:

fol code check
fol code build --release
fol code run -- --flag value
fol code emit rust
fol code emit lowered

Use code for:

  • driving the compile pipeline
  • building binaries through the current Rust backend
  • running produced binaries
  • emitting backend/debug artifacts

Tool

Tooling commands:

  • fol tool lsp
  • fol tool format <PATH>
  • fol tool parse <PATH>
  • fol tool highlight <PATH>
  • fol tool symbols <PATH>
  • fol tool references <PATH> --line <LINE> --character <CHARACTER>
  • fol tool rename <PATH> --line <LINE> --character <CHARACTER> <NEW_NAME>
  • fol tool semantic-tokens <PATH>
  • fol tool tree generate <PATH>
  • fol tool clean
  • fol tool completion

Examples:

fol tool parse src/main.fol
fol tool format src/main.fol
fol tool highlight src/main.fol
fol tool symbols src/main.fol
fol tool references src/main.fol --line 12 --character 8
fol tool rename src/main.fol --line 12 --character 8 total
fol tool semantic-tokens src/main.fol
fol tool tree generate /tmp/fol
fol tool lsp
fol tool completion bash

Use tool for:

  • editor integration
  • Tree-sitter debugging
  • LSP serving
  • generated tool assets

The public editor surface stays under fol tool .... There is no parallel fol editor ... command group. Future editor features are not exposed as placeholder commands. Only the shipped fol tool subcommands above are public.

Artifact Reporting

Frontend commands report explicit artifact roots when applicable, including:

  • emitted Rust crate roots
  • lowered snapshot roots
  • final binary paths
  • fetch/store/cache roots where relevant

Editor Tooling

FOL editor support lives in one crate:

  • fol-editor

That crate owns two related subsystems:

  • Tree-sitter assets for syntax-oriented editor work
  • the language server for compiler-backed editor services

Public Entry

The public entrypoints are exposed through fol tool:

  • fol tool lsp
  • fol tool format <PATH>
  • fol tool parse <PATH>
  • fol tool highlight <PATH>
  • fol tool symbols <PATH>
  • fol tool references <PATH> --line <LINE> --character <CHARACTER>
  • fol tool rename <PATH> --line <LINE> --character <CHARACTER> <NEW_NAME>
  • fol tool semantic-tokens <PATH>
  • fol tool tree generate <PATH>

This keeps editor workflows under the same fol binary rather than introducing a second public tool.

Split Of Responsibilities

Tree-sitter is the editor syntax layer.

It is responsible for:

  • syntax trees while typing
  • query-driven highlighting
  • locals and symbol-style structure captures
  • editor-facing structural parsing

The language server is the semantic editor layer.

It is responsible for:

  • JSON-RPC/LSP transport
  • open-document state
  • compiler-backed diagnostics
  • hover
  • go-to-definition
  • whole-document formatting
  • code actions for exact compiler suggestions The current shipped inventory is intentionally one diagnostic family: unresolved-name replacements where the compiler attached an exact replacement.
  • signature help for plain and qualified routine calls
  • references
  • rename for same-file local and current-package top-level symbols
  • semantic tokens
  • document symbols
  • workspace symbols for current open workspace members
  • completion

The currently supported v1 LSP surface is:

  • diagnostics
  • hover
  • definition
  • formatting
  • code actions for exact compiler suggestions
  • signature help for plain and qualified routine calls
  • references
  • rename for same-file local and current-package top-level symbols
  • semantic tokens
  • document symbols
  • workspace symbols for current open workspace members
  • completion

The server keeps diagnostics and semantic snapshots separately.

Formatting is intentionally whole-document only right now. textDocument/rangeFormatting stays unsupported until there is a safe structure-preserving boundary instead of a partial line-rewriter.

The current formatter contract is intentionally narrow but explicit:

  • indentation is four spaces per brace depth
  • lines are trimmed before indentation is re-applied
  • leading blank lines are removed and repeated blank lines collapse to one
  • trailing blank lines are removed
  • output always ends with one final newline when the document is non-empty
  • line endings are normalized to \n
  • build.fol follows the same formatter entrypoint and indentation rules as ordinary source files

Diagnostics refresh on didOpen and didChange.

Semantic requests keep one semantic snapshot per open document version and reuse it for hover, definition, signature help, references, rename, semantic tokens, document symbols, workspace symbols, and completion until the document changes or closes.

Compiler Truth

fol-editor does not create a second semantic engine.

Semantic truth still comes from:

  • fol-package
  • fol-resolver
  • fol-typecheck
  • fol-diagnostics

So the model is:

  • Tree-sitter answers “what does this text structurally look like?”
  • compiler crates answer “what does this code mean?”

For the current maintenance contract between compiler crates, LSP behavior, and Tree-sitter assets, see:

Current Practical Workflow

Use:

fol tool lsp
fol tool format path/to/file.fol

as the language server entrypoint.

Launch it from inside a discovered package or workspace root. The frontend looks upward for package.yaml or fol.work.yaml before starting the server.

Use:

fol tool parse path/to/file.fol
fol tool highlight path/to/file.fol
fol tool symbols path/to/file.fol
fol tool format path/to/file.fol
fol tool references path/to/file.fol --line 12 --character 8
fol tool rename path/to/file.fol --line 12 --character 8 total
fol tool semantic-tokens path/to/file.fol

for parser/query debugging and validation.

Compiler Integration

This chapter explains how the compiler and editor tooling are connected today, and what should remain the source of truth as FOL grows.

The Three Layers

FOL editor tooling is split into three layers:

  1. compiler truth
  2. semantic editor services
  3. syntax editor services

Compiler truth lives in crates such as:

  • fol-lexer
  • fol-parser
  • fol-package
  • fol-resolver
  • fol-typecheck
  • fol-intrinsics
  • fol-diagnostics

Semantic editor services live in:

  • fol-editor LSP analysis and semantic code

Syntax editor services live in:

  • the Tree-sitter grammar
  • Tree-sitter query files
  • editor bundle generation

That split matters because not every editor feature should be solved in the same way.

What The LSP Should Trust

For semantic meaning, the LSP should trust the compiler pipeline.

That means:

  • diagnostics come from compiler diagnostics
  • hover should be derived from resolved or typed compiler state
  • definition should be derived from resolved symbol data
  • document symbols should prefer compiler symbol ownership where practical
  • completion should prefer compiler facts over text heuristics

The LSP should not invent a parallel semantic model.

If a feature needs semantic meaning, the first question should be:

  • can this come from fol-parser / fol-resolver / fol-typecheck instead of editor-only code?

What Tree-sitter Should Trust

Tree-sitter is not the compiler parser.

It exists for:

  • syntax trees while typing
  • highlighting
  • locals queries
  • symbol-style structural captures
  • future textobjects and editor movement

So Tree-sitter should stay editor-oriented.

That means:

  • the grammar can remain handwritten
  • query files can remain handwritten
  • but duplicated language facts should not be copied manually forever

Examples of facts that should come from compiler-owned truth:

  • builtin type spellings
  • implemented intrinsic names
  • import source kinds
  • keyword families used by syntax tooling

How Diagnostics Flow

Today the intended direction is:

  1. compiler stage creates a structured fol_diagnostics::Diagnostic
  2. editor tooling adapts that diagnostic for the active editor protocol
  3. the editor displays the adapted result

The important rule is:

  • the compiler diagnostic object is canonical

So when diagnostic wording, labels, helps, or codes change, the editor should adapt that same structure. It should not create its own free-form diagnosis logic.

What Can Be Generated

Generation is useful when the same language fact appears in multiple places.

Good generation targets:

  • keyword manifests
  • builtin type manifests
  • intrinsic name/surface manifests
  • source-kind manifests
  • small generated query fragments or validation snapshots

Bad generation targets:

  • the entire Tree-sitter grammar
  • Tree-sitter precedence/conflict structure
  • most highlight policy
  • LSP transport behavior

The rule of thumb is:

  • generate shared facts
  • handwrite editor behavior

What To Update For A New Feature

When you add a new language feature, ask these questions in order:

  1. Does the lexer need new tokens or token families?
  2. Does the parser need new syntax or AST nodes?
  3. Does resolver or typechecker logic need new meaning?
  4. Does the feature introduce or change diagnostics?
  5. Does the LSP need hover, completion, definition, or symbol updates?
  6. Does Tree-sitter need grammar, query, or corpus updates?
  7. Can any new editor-visible facts be generated from compiler-owned sources?

If the answer to any of those is yes, update that layer in the same change set.

Current Practical Rule

In FOL today:

  • compiler crates own meaning
  • fol-editor owns editor protocol behavior
  • Tree-sitter owns syntax-oriented editor structure

The safer future direction is:

  • move shared contracts closer to compiler-owned crates
  • keep protocol/UI code in tooling crates
  • generate duplicated facts instead of hand-copying them

Tree-sitter Integration

The Tree-sitter side of FOL is the editor-facing syntax layer.

It is not the compiler parser.

What Is In The Repo

The editor crate carries:

  • the grammar source
  • corpus fixtures
  • query files on disk

Canonical query assets live as real files, not just embedded Rust strings:

  • queries/fol/highlights.scm
  • queries/fol/locals.scm
  • queries/fol/symbols.scm

This is intentional because editors such as Neovim expect query files on disk in the standard Tree-sitter layout.

Generated Bundle

To generate a Neovim-consumable bundle, run:

fol tool tree generate /tmp/fol

That writes a bundle containing the grammar and query assets under the target directory.

The intended consumer path is:

  • generate bundle
  • point the editor’s Tree-sitter parser configuration at that bundle
  • let the editor compile/use the parser from there

File Ownership

Intentionally Handwritten

These files are human-authored and should remain so:

FilePurposeOwner
tree-sitter/grammar.jsGrammar rules, precedence, conflictsEditor/syntax maintainer
queries/fol/highlights.scmHighlight capture groups and query patternsEditor/syntax maintainer
queries/fol/locals.scmScope and definition trackingEditor/syntax maintainer
queries/fol/symbols.scmSymbol navigation capturesEditor/syntax maintainer
test/corpus/*.txtCorpus fixtures for grammar validationEditor/syntax maintainer

Do not attempt to generate these from the compiler parser. The parsing models are different and auto-generation creates more fragility than value.

Validated Against Compiler Constants

These facts appear in handwritten files but are validated by integration tests to stay in sync with compiler-owned constants:

FactHandwritten LocationCompiler Source
Builtin type nameshighlights.scm regex ^(int|bol|...)$BuiltinType::ALL_NAMES in fol-typecheck
Dot-call intrinsic nameshighlights.scm regex ^(len|echo|...)$Implemented DotRootCall entries in fol-intrinsics
Container type nameshighlights.scm node labels + grammar.js choiceCONTAINER_TYPE_NAMES in fol-parser
Shell type nameshighlights.scm node labels + grammar.js choiceSHELL_TYPE_NAMES in fol-parser
Source kind nameshighlights.scm node labels + grammar.js choiceSOURCE_KIND_NAMES in fol-parser

The sync tests live in test/run_tests.rs under treesitter_sync. If you add a new builtin type, intrinsic, container, shell, or source kind to the compiler, these tests fail until the tree-sitter files are updated to match.

When To Update Tree-sitter Files

grammar.js

Update when:

  • A new declaration form is added (e.g. a new seg or lab declaration)
  • A new expression or statement form is added
  • A new type syntax is added (e.g. a new container or shell family)
  • A new source kind is added
  • Operator precedence or conflict rules change

Do not update for:

  • New diagnostic codes or error messages
  • New resolver/typecheck rules that don’t change syntax
  • New intrinsics (these use existing dot_intrinsic grammar rule)

highlights.scm

Update when:

  • A new keyword needs highlighting
  • A new builtin type is added to the compiler
  • A new implemented dot-call intrinsic is added
  • A new container or shell type family is added
  • A new source kind is added
  • Highlight group policy changes (e.g. moving a keyword from @keyword to @keyword.function)

locals.scm

Update when:

  • Scope rules change (e.g. new block forms that introduce scopes)
  • Definition capture patterns change

symbols.scm

Update when:

  • New declaration forms should appear in document symbol navigation

Corpus fixtures

Update when:

  • Any grammar rule is added or modified
  • A new syntax family needs parse-tree validation

Corpus Coverage Expectations

Corpus fixtures live in tree-sitter/test/corpus/ and cover syntax families:

Corpus FileCovers
declarations.txtuse, ali, typ, fun, log, var declarations
expressions.txtIntrinsic calls, when/loop control flow, break/return
recoverable.txtError propagation (/), report, pipe-or (||)

When a new syntax family is added, it should have corpus coverage. The expected families that should each have at least one corpus example:

  • Import declarations (use)
  • Type declarations (typ, ali)
  • Routine declarations (fun, log)
  • Variable declarations (var)
  • Control flow (when, loop, case, break, return)
  • Expressions (binary, call, field access, dot intrinsic)
  • Container and shell types
  • Record and entry types
  • Error handling (report, pipe-or, check)

What Tree-sitter Is For

Use the Tree-sitter layer for:

  • highlighting
  • locals and capture queries
  • symbol-style structural views
  • editor textobjects and movement later

Do not use it as a substitute for typechecking or resolution.

Those remain compiler tasks.

When a language feature changes syntax, use the Feature Update Checklist to decide whether the grammar, queries, corpus, or generated language facts also need updates.

Feature Update Checklist

Use this checklist whenever a new language feature, syntax form, intrinsic, or error surface is added.

This chapter is about maintenance discipline, not just implementation order.

Quick Reference

When adding a feature, touch these layers in order:

  1. Lexer — new keywords, operators, tokens
  2. Parser — new AST nodes, syntax rules
  3. Semantics — resolver, typecheck, lowering, intrinsics
  4. Diagnostics — new error/warning cases
  5. LSP — hover, completion, definition, symbols
  6. Tree-sitter — grammar, queries, corpus
  7. Generated facts — compiler-owned constants
  8. Docs — language chapters, tooling pages
  9. Tests — unit, integration, editor, corpus

Automated guards exist for some of these. The treesitter_sync integration tests verify that highlights.scm matches compiler-owned constants for builtin types, intrinsic names, container/shell types, and source kinds. Adding a new constant to the compiler without updating Tree-sitter will fail those tests.

1. Lexical Surface

Check:

  • new keywords
  • new operators or punctuation
  • new literal/token families
  • comment or whitespace effects

Update:

  • fol-lexer
  • lexical docs under 100_lexical
  • any generated keyword/facts manifest if one exists

2. Parser Surface

Check:

  • new declarations
  • new expressions
  • new statement forms
  • new type forms
  • new precedence or ambiguity rules

Update:

  • fol-parser
  • parser tests
  • Tree-sitter grammar if the syntax is editor-visible
  • Tree-sitter corpus fixtures for the new syntax family

3. Semantic Surface

Check:

  • name resolution
  • type checking
  • lowering
  • intrinsic availability
  • runtime/backend impact

Update:

  • fol-resolver
  • fol-typecheck
  • fol-lower
  • fol-intrinsics
  • runtime/backend crates if needed

4. Diagnostics Surface

Check:

  • new error cases
  • new warning/info cases
  • changed wording for existing rules
  • changed labels, notes, helps, or suggestions

Update:

  • compiler producer diagnostics
  • fol-diagnostics contract tests
  • editor/LSP diagnostic adapter tests if the visible shape changes
  • docs under 650_errors if behavior changed materially

5. LSP Surface

Check:

  • hover content
  • go-to-definition behavior
  • document symbols
  • completion
  • open-document analysis behavior under broken code

Update:

  • fol-editor semantic analysis
  • fol-editor semantic display helpers or compiler-owned helpers they consume
  • LSP tests

Important rule:

  • prefer compiler-backed meaning
  • use fallback heuristics only when the compiler cannot supply semantic data yet

6. Tree-sitter Surface

Check:

  • syntax shape visible while typing
  • highlight groups
  • locals captures
  • symbols captures
  • corpus examples

Update:

  • tree-sitter/grammar.js
  • queries/fol/highlights.scm
  • queries/fol/locals.scm
  • queries/fol/symbols.scm
  • tree-sitter/test/corpus/*.txt

Important rule:

  • Tree-sitter is for syntax-facing editor behavior
  • it does not replace compiler semantics

7. Generated Facts

Check whether the feature adds a new fact that should be exported once instead of copied by hand.

Examples:

  • intrinsic names
  • builtin type names
  • source kinds
  • keyword groups
  • shell/container family names

If yes:

  • update the compiler-owned source
  • regenerate editor-facing artifacts from that source
  • do not patch multiple copies manually

8. Documentation

Update:

  • the language chapter for the feature
  • tooling docs if editor behavior changes
  • diagnostics docs if compiler reporting changes
  • examples/fixtures that demonstrate the preferred form

9. Tests

Add or update:

  • compiler unit tests
  • integration tests
  • editor/LSP tests if semantic editor behavior changes
  • Tree-sitter tests/corpus if syntax-facing behavior changes

Keep the feature test in the same change as the feature.

10. Final Review Questions

Before considering the feature complete, answer:

  1. Is compiler meaning implemented?
  2. Are diagnostics correct and structured?
  3. Does the LSP reflect the new meaning where needed?
  4. Does Tree-sitter reflect the new syntax where needed?
  5. Did we generate shared facts instead of duplicating them?
  6. Did docs explain the intended user-facing behavior?

Language Server

The FOL language server is started with:

fol tool lsp

It runs over stdio and is meant to be launched by editors.

Current Feature Set

The current working surface includes:

  • initialize / shutdown / exit
  • document open / change / close
  • diagnostics
  • hover
  • go-to-definition
  • whole-document formatting
  • code actions for compiler-suggested unresolved-name replacements
  • signature help for plain and qualified routine calls
  • references
  • rename for same-file local and current-package top-level symbols
  • semantic tokens
  • document symbols
  • workspace symbols for current open workspace members
  • completion
  • the same compiler-backed behavior for ordinary source files and build.fol

That means build files should not need a separate editor mode. build.fol goes through the same language server entrypoint as the rest of the package.

textDocument/didChange accepts both whole-document replacements and ranged change events. The server applies ranged changes in order against the current in-memory document version.

The server advertises incremental text sync by default. Full-document sync remains an explicit opt-in for clients/configs that need it.

Formatting is intentionally limited to textDocument/formatting. textDocument/rangeFormatting remains unsupported until the formatter can preserve surrounding structure safely instead of guessing at partial blocks.

Current whole-document formatting also normalizes blank-line runs: leading blank lines are removed and repeated blank lines collapse to one.

Analysis Model

The server works like this:

  1. receive editor request/notification
  2. map the open document to a package/workspace context
  3. materialize an analysis overlay for the in-memory document state
  4. run parse/package/resolve/typecheck as needed
  5. convert compiler results into LSP responses

Package/workspace root discovery is cached for the current session once a document directory has been mapped.

That means diagnostics and navigation are compiler-backed, not guessed from Tree-sitter alone.

For open documents, diagnostics and semantic snapshots are cached separately.

didOpen and didChange refresh diagnostics for the current in-memory text.

Hover, definition, signature help, references, rename, semantic tokens, document symbols, workspace symbols, and completion use a semantic snapshot keyed by document version and reuse it until didChange or didClose invalidates it.

Diagnostics

Compiler diagnostics remain canonical.

The server adapts them into LSP diagnostics for the currently open document.

LSP diagnostics include the diagnostic code in the message (e.g. [R1003] could not resolve name 'answer') so editors display the code inline.

The server deduplicates diagnostics by (line, code) before publishing. This means a parse cascade that produces many identical errors on the same line will show at most one diagnostic per line per code in the editor.

Expected Behavior

If the LSP client is attached correctly, you should expect:

  • source-file diagnostics in the open file
  • hover on resolved symbols
  • go-to-definition across current-package and imported symbols where supported
  • whole-document formatting edits from textDocument/formatting
  • code actions only when the compiler attached an exact replacement suggestion The current shipped code-action inventory is intentionally narrow: unresolved-name replacements only.
  • signature help for supported routine call sites
  • references for the supported symbol classes
  • rename for same-file local and current-package top-level symbols only
  • semantic tokens for semantic identifier categories
  • document symbols for the current file
  • workspace symbols across the current open workspace members
  • completion in ordinary source files and build.fol

If the client is attached but you see no diagnostics at all, the likely issue is not Tree-sitter. It is the LSP request/attach path.

For the compiler-owned contracts behind diagnostics and semantic editor behavior, see Compiler Integration.

Neovim Integration

Neovim integration has two separate pieces:

  • Tree-sitter for syntax/highlighting/queries
  • LSP for diagnostics and semantic editor features

They should be configured together, but they do different jobs.

LSP Setup

The language server command is:

vim.lsp.config("fol", {
  cmd = { "fol", "tool", "lsp" },
  filetypes = { "fol" },
  root_markers = { "fol.work.yaml", "package.yaml", ".git" },
})

vim.lsp.enable("fol")

Also ensure Neovim recognizes .fol files:

vim.filetype.add({
  extension = { fol = "fol" },
})

Tree-sitter Setup

First generate a bundle:

fol tool tree generate /tmp/fol

Then point Neovim’s Tree-sitter parser config at that bundle:

local parser_config = require("nvim-treesitter.parsers").get_parser_configs()

parser_config.fol = {
  install_info = {
    url = "/tmp/fol",
    files = { "src/parser.c" },
    requires_generate_from_grammar = false,
  },
  filetype = "fol",
}

The query files are expected at:

  • /tmp/fol/queries/fol/highlights.scm
  • /tmp/fol/queries/fol/locals.scm
  • /tmp/fol/queries/fol/symbols.scm

Neovim also needs that bundle on runtimepath so it can find the queries.

Practical Model

Tree-sitter provides:

  • highlight captures
  • locals captures
  • symbol-style structure queries

LSP provides:

  • diagnostics
  • hover
  • definitions
  • references
  • rename for same-file local and current-package top-level symbols
  • semantic tokens
  • document symbols

So the normal editor shape is:

  1. Neovim opens a .fol file
  2. Tree-sitter handles syntax/highlighting
  3. Neovim launches fol tool lsp
  4. the server provides semantic editor features

Debugging A Setup

Useful checks:

:echo &filetype
:lua print(vim.inspect(vim.lsp.get_clients({ bufnr = 0 })))

And from the shell:

fol tool tree generate /tmp/fol
fol tool lsp

If fol tool lsp prints nothing and waits, that is correct.

It is a stdio server, not an interactive shell command.

If the server refuses to start, check that Neovim opened the file inside a directory tree that contains package.yaml or fol.work.yaml.

Build System

FOL uses a Zig-style build model. The build specification is a normal FOL program called build.fol. It goes through the full compiler pipeline and is executed against a build graph IR instead of emitting backend code.

The build system lives in lang/execution/fol-build. It handles:

  • graph IR construction for artifacts, steps, options, modules, and generated files
  • full control flow in build programs (when, loop, helper routines)
  • -D CLI option passing into the build program
  • named step selection at the command line

Entry Point

Every buildable package must have a build.fol at its root with exactly one canonical entry:

pro[] build(graph: Graph): non = {
    ...
}

The graph parameter is the injection surface. All build operations go through method calls on graph and on the handles it returns.

Minimal Example

pro[] build(graph: Graph): non = {
    var app = graph.add_exe({ name = "app", root = "src/main.fol" });
    graph.install(app);
    graph.add_run(app);
}

This registers an executable, marks it for installation, and binds a default run step.

What fol-build Owns

  • graph.rs — build graph IR (steps, artifacts, modules, options, generated files)
  • api.rs — Rust-level graph mutation interface
  • semantic.rs — method signatures and type info for the resolver and typechecker
  • stdlib.rsBuildStdlibScope: the ambient scope injected into build.fol
  • executor.rs — executes the lowered FOL IR against the build graph
  • eval.rs — evaluate a build.fol from source; entry point for fol-package
  • option.rs — build option kinds, target triples, optimize modes
  • runtime.rs — runtime representation of artifacts, generated files, step bindings
  • step.rs — step planning, ordering, cache keys, execution reports
  • codegen.rs — system tool and codegen request types
  • artifact.rs — artifact pipeline definitions and output types
  • dependency.rs — inter-package dependency surfaces

Use this section for:

  • understanding the shape of build.fol
  • the full graph API reference
  • control flow available inside build.fol
  • build options and -D flags
  • artifact types, modules, and generated files

build.fol

build.fol is a file-bound FOL compilation unit. It is the build specification for a package.

File-Bound vs Folder-Bound

Normal FOL packages are folder-bound: every .fol file in the package folder shares one namespace. build.fol is the one exception.

Rules for build.fol:

  • It is its own compilation unit — it does not see sibling .fol files
  • It has one implicit import: the build stdlib (fol/build), providing Graph and all handle types
  • It can define local helper fun[], pro[], and typ declarations
  • Those local declarations are not exported to the package
  • It must declare exactly one pro[] build(graph: Graph): non entry
  • Additional use imports from the FOL stdlib are allowed

Compilation Pipeline

build.fol goes through the full FOL compiler pipeline:

build.fol
    │
    ▼  stream → lexer → parser
    │
    ▼  fol-resolver   (build stdlib injected as ambient scope)
    │
    ▼  fol-typecheck  (handle types and method signatures validated)
    │
    ▼  fol-lower      (lowered IR produced)
    │
    ▼  fol-build executor  (IR executed against BuildGraph)
    │
    ▼  BuildGraph

The compiler rejects build.fol files that reference sibling source files, use filesystem or network APIs, or contain more than one canonical entry.

Canonical Entry

The entry must match exactly:

pro[] build(graph: Graph): non = {
    ...
}
  • pro[] — procedure with no receivers
  • parameter name graph, type Graph
  • return type non

Missing entry, wrong signature, or duplicate entries are compile errors.

Local Helpers

build.fol can define helper functions visible only within itself:

fun[] make_lib(graph: Graph, name: str, root: str): Artifact = {
    return graph.add_static_lib({ name = name, root = root });
}

pro[] build(graph: Graph): non = {
    var core = make_lib(graph, "core", "src/core/lib.fol");
    var io   = make_lib(graph, "io",   "src/io/lib.fol");
    var app  = graph.add_exe({ name = "app", root = "src/main.fol" });
    app.link(core);
    app.link(io);
    graph.install(app);
}

package.yaml

Every package needs a package.yaml alongside build.fol:

name: my-app
version: 1.0.0

The build system reads name and version from the manifest. Dependencies declared in the manifest are made available to build.fol via graph.dependency(...).

Capability Restrictions

The build executor enforces a capability model. Allowed operations:

  • graph mutation (adding artifacts, steps, options)
  • option reads (graph.standard_target(), graph.standard_optimize(), etc.)
  • path joining and normalization (graph.path_from_root(...))
  • basic string and container operations
  • controlled file generation (graph.write_file(...), graph.copy_file(...))
  • controlled process execution (graph.add_system_tool(...))

Forbidden operations (produce a compile or runtime error):

  • arbitrary filesystem reads or writes
  • network access
  • wall clock access
  • ambient environment variable access
  • uncontrolled process execution

These restrictions ensure build graphs are deterministic and portable.

Graph API

The Graph handle is the sole parameter to pro[] build. All build graph construction goes through method calls on it.

Artifacts

graph.add_exe

Adds an executable artifact.

var app = graph.add_exe({
    name     = "app",
    root     = "src/main.fol",
    target   = target,    // optional
    optimize = optimize,  // optional
});

Returns an Artifact handle.

Required fields: name, root. Optional fields: target, optimize.

root is the path to the entry-point .fol source file relative to the package root.

graph.add_static_lib

Adds a static library artifact.

var core = graph.add_static_lib({ name = "core", root = "src/core/lib.fol" });

Returns an Artifact handle.

graph.add_shared_lib

Adds a shared (dynamic) library artifact.

var sdk = graph.add_shared_lib({ name = "sdk", root = "src/sdk/lib.fol" });

Returns an Artifact handle.

graph.add_test

Adds a test artifact.

var tests = graph.add_test({ name = "tests", root = "src/tests.fol" });

Returns an Artifact handle.

graph.add_module

Adds a standalone module that can be imported by other artifacts.

var utils = graph.add_module({ name = "utils", root = "src/utils.fol" });

Returns a Module handle.

Installation and Runs

graph.install

Marks an artifact for installation.

graph.install(app);

Returns an Install handle.

graph.install_file

Installs a file by path.

graph.install_file("config/defaults.toml");

Returns an Install handle.

graph.install_dir

Installs a directory by path.

graph.install_dir("assets/");

Returns an Install handle.

graph.add_run

Registers an artifact as a run target. Binds the default run step when only one executable exists and no explicit run step has been registered.

var run = graph.add_run(app);

Returns a Run handle. See Handle API for Run methods.

Steps

graph.step

Creates a named custom step.

var docs = graph.step("docs");
var docs = graph.step("docs", "Generate documentation");  // with description

Returns a Step handle. See Handle API for Step methods.

Named steps are selectable on the command line:

fol code build docs
fol code build --step docs

Options

graph.standard_target

Reads the -Dtarget option. Returns a Target handle.

var target = graph.standard_target();

The value is provided at build time via -Dtarget=x86_64-linux-gnu. If no value is provided, target resolves to the host target.

An optional config record is accepted:

var target = graph.standard_target({ default = "x86_64-linux-gnu" });

graph.standard_optimize

Reads the -Doptimize option. Returns an Optimize handle.

var optimize = graph.standard_optimize();

The value is provided via -Doptimize=release-fast. Defaults to debug if not set.

Valid values: debug, release-safe, release-fast, release-small.

graph.option

Declares a named user option readable via -D.

var root_opt = graph.option({
    name    = "root",
    kind    = "path",
    default = "src/main.fol",
});

Returns a UserOption handle.

Required fields: name, kind. Optional field: default.

Option kinds:

KindDescriptionCLI Example
boolBoolean flag-Dstrip=true
intInteger value-Djobs=4
strArbitrary string-Dprefix=/usr/local
enumOne of a fixed set of strings-Dbackend=llvm
pathFile or directory path-Droot=src/main.fol
targetTarget triple-Dtarget=x86_64-linux-gnu
optimizeOptimization mode-Doptimize=release-fast

Generated Files

graph.write_file

Declares a file to be written with static contents at build time.

var header = graph.write_file({
    name     = "version.h",
    path     = "gen/version.h",
    contents = "#define VERSION 1\n",
});

Returns a GeneratedFile handle.

graph.copy_file

Declares a file to be copied from a source path.

var cfg = graph.copy_file({
    name   = "config",
    source = "config/template.toml",
    dest   = "gen/config.toml",
});

Returns a GeneratedFile handle.

graph.add_system_tool

Declares a system tool invocation that produces a file.

var packed = graph.add_system_tool({
    tool   = "wasm-pack",
    args   = ["build", "--target", "web"],
    output = "gen/app.wasm",
});

Returns a GeneratedFile handle.

The generated file is keyed by the output path. Use this handle with step.attach(...) or artifact.add_generated(...).

graph.add_codegen

Declares a FOL codegen step.

var schema = graph.add_codegen({
    kind   = "schema",
    input  = "schema/api.yaml",
    output = "gen/api.fol",
});

Returns a GeneratedFile handle.

Codegen kinds: fol-to-fol, schema, asset-preprocess.

Path Utilities

graph.path_from_root

Returns an absolute path by joining the package root with a relative subpath.

var cfg = graph.path_from_root("config/default.toml");

Useful when passing file paths to add_run args.

graph.build_root

Returns the package root directory as an absolute path string.

var root = graph.build_root();

graph.install_prefix

Returns the installation prefix. Defaults to the workspace install directory.

var prefix = graph.install_prefix();

Dependencies

graph.dependency

Declares a reference to another package in the workspace or as a fetched dependency. Returns a Dependency handle.

var deps = graph.dependency("mylib", "local:../mylib");

See Handle API for querying modules, artifacts, steps, and generated outputs from a Dependency handle.

Handle API

Graph methods return typed handles. Each handle type exposes its own set of methods for configuring relationships and behavior.

Artifact

The Artifact handle is returned by add_exe, add_static_lib, add_shared_lib, and add_test.

Links another artifact as a dependency. The linker will include it.

var core = graph.add_static_lib({ name = "core", root = "src/core/lib.fol" });
var app  = graph.add_exe({ name = "app", root = "src/main.fol" });
app.link(core);

Equivalent to Zig’s artifact.linkLibrary(dep).

artifact.import

Imports a module into this artifact’s compilation scope.

var utils = graph.add_module({ name = "utils", root = "src/utils.fol" });
var app   = graph.add_exe({ name = "app", root = "src/main.fol" });
app.import(utils);

Equivalent to Zig’s artifact.root_module.addImport(name, module).

artifact.add_generated

Declares that this artifact depends on a generated file being produced before it can compile.

var schema = graph.add_codegen({
    kind   = "schema",
    input  = "schema/api.yaml",
    output = "gen/api.fol",
});
var app = graph.add_exe({ name = "app", root = "src/main.fol" });
app.add_generated(schema);

Run

The Run handle is returned by graph.add_run. All Run methods are chainable and return Run.

run.add_arg

Appends a literal string argument to the run command.

var run = graph.add_run(app);
run.add_arg("--config").add_arg("config/default.toml");

run.add_file_arg

Appends a generated file as a path argument.

var cfg = graph.copy_file({
    name   = "config",
    source = "config/defaults.toml",
    dest   = "gen/config.toml",
});
var run = graph.add_run(app);
run.add_file_arg(cfg);

Equivalent to Zig’s run.addFileArg(file).

run.add_dir_arg

Appends a directory path as an argument.

var run = graph.add_run(app);
run.add_dir_arg("assets/");

run.capture_stdout

Captures the standard output of the run command as a generated file.

var run    = graph.add_run(generator_tool);
var output = run.capture_stdout();
app.add_generated(output);

Returns a GeneratedFile handle. Equivalent to Zig’s run.captureStdOut().

run.set_env

Sets an environment variable for the run command.

var run = graph.add_run(app);
run.set_env("LOG_LEVEL", "debug");

run.depend_on

Makes this run step depend on another step completing first.

var codegen = graph.step("codegen");
var run     = graph.add_run(app);
run.depend_on(codegen);

Step

The Step handle is returned by graph.step. All Step methods are chainable and return Step.

step.depend_on

Makes this step depend on another step.

var compile = graph.step("compile");
var bundle  = graph.step("bundle");
bundle.depend_on(compile);

step.attach

Attaches a generated file production to this step. When the step runs, the attached generated file is produced first.

var strip_tool = graph.add_system_tool({
    tool   = "strip",
    output = "gen/app.stripped",
});
var strip_step = graph.step("strip");
strip_step.attach(strip_tool);

Install

The Install handle is returned by graph.install, graph.install_file, and graph.install_dir.

install.depend_on

Makes this install step depend on another step.

var check   = graph.step("check");
var install = graph.install(app);
install.depend_on(check);

Dependency

The Dependency handle is returned by graph.dependency. It exposes the public surface of another package.

dependency.module

Resolves a named module from the dependency.

var dep    = graph.dependency("mylib", "local:../mylib");
var module = dep.module("core");
app.import(module);

dependency.artifact

Resolves a named artifact from the dependency.

var dep = graph.dependency("mylib", "local:../mylib");
var lib = dep.artifact("mylib-static");
app.link(lib);

dependency.step

Resolves a named step from the dependency.

var dep  = graph.dependency("mylib", "local:../mylib");
var step = dep.step("codegen");
app_step.depend_on(step);

dependency.generated

Resolves a named generated output from the dependency.

var dep   = graph.dependency("mylib", "local:../mylib");
var types = dep.generated("types.fol");
app.add_generated(types);

Build Options

Build options are named values passed from the command line into build.fol. They follow Zig’s -D convention.

Syntax

fol code build -Dname=value

Multiple options can be passed in one command:

fol code build -Dtarget=x86_64-linux-gnu -Doptimize=release-fast -Dstrip=true

Standard Options

Two options are pre-defined by the build system: target and optimize. They are read via dedicated graph methods.

Target

var target = graph.standard_target();

CLI: -Dtarget=arch-os-env

Format: arch-os-env triple. Examples:

TripleMeaning
x86_64-linux-gnux86-64 Linux with glibc
x86_64-linux-muslx86-64 Linux with musl libc
aarch64-linux-gnuARM64 Linux with glibc
x86_64-macosx86-64 macOS
x86_64-windows-msvcx86-64 Windows with MSVC

Supported architectures: x86_64, aarch64. Supported operating systems: linux, macos, windows. Supported environments: gnu, musl, msvc.

If not set, the host target is used.

To pass the resolved value to an artifact:

var target = graph.standard_target();
var app = graph.add_exe({
    name   = "app",
    root   = "src/main.fol",
    target = target,
});

Optimize

var optimize = graph.standard_optimize();

CLI: -Doptimize=mode

Valid modes:

ModeMeaning
debugNo optimization, full debug info
release-safeOptimized with safety checks
release-fastMaximum speed, no safety checks
release-smallMinimize binary size

Default: debug.

To pass the resolved value to an artifact:

var optimize = graph.standard_optimize();
var app = graph.add_exe({
    name     = "app",
    root     = "src/main.fol",
    optimize = optimize,
});

User Options

graph.option(...) declares a named option specific to the package.

var strip = graph.option({
    name    = "strip",
    kind    = "bool",
    default = false,
});

CLI: -Dstrip=true

Option Kinds

KindExample CLIDefault type
bool-Dverbose=truefalse
int-Djobs=80
str-Dprefix=/usr""
enum-Dbackend=llvmfirst value
path-Droot=src/main.fol""
target-Dtarget=x86_64-linux-gnuhost target
optimize-Doptimize=release-fastdebug

Using Option Values

Option handle values can be interpolated into strings or compared with ==:

var root_opt = graph.option({ name = "root", kind = "path", default = "src/main.fol" });
var app = graph.add_exe({ name = "app", root = root_opt });

Comparing in a when condition:

var strip = graph.option({ name = "strip", kind = "bool", default = false });
when(strip == true) {
    {
        var strip_step = graph.step("strip");
    }
};

Shorthand vs Long Form

Both of these are equivalent:

fol code build -Dtarget=x86_64-linux-gnu
fol code build --build-option target=x86_64-linux-gnu

-D is the shorthand. -Dtarget= and -Doptimize= route to the dedicated standard option slots. All other -Dname=value pairs route to user-declared options.

Control Flow in build.fol

build.fol is a real FOL program. It supports when, loop, and user-defined helper routines.

when

when conditionally executes build operations based on a boolean expression.

The canonical form for a conditional block with no case arms uses a double-brace default body:

when(optimize == "release-fast") {
    {
        var strip_step = graph.step("strip");
        var packed = graph.add_system_tool({
            tool   = "strip",
            output = "gen/app.stripped",
        });
        strip_step.attach(packed);
    }
};

With explicit case arms:

when(target) {
    case("x86_64-linux-gnu") {
        graph.step("asan", "Enable address sanitizer");
    }
    case("aarch64-linux-gnu") {
        graph.step("tsan", "Enable thread sanitizer");
    }
    * {
        graph.step("default-check");
    }
};

The inner double-brace { { ... } } is required for the default arm when no case clauses are present. This is how the FOL parser distinguishes the default body from raw statements.

Conditional Expressions

Any boolean or option value comparison is valid:

when(optimize == "release-fast") { { ... } };
when(strip == true) { { ... } };
when(target == "x86_64-linux-gnu") { { ... } };

The comparison is resolved at build evaluation time using the values passed via -D flags. If no value was provided, the declared default is used.

loop

loop iterates over a list and runs the body for each element.

loop(name in {"core", "io", "utils"}) {
    graph.add_static_lib({ name = name, root = name });
};

The loop variable (name) is bound in scope for each iteration. It can be used anywhere inside the body as a string value.

Loop over a list with multiple fields:

loop(name in {"core", "io"}) {
    var lib = graph.add_static_lib({ name = name, root = name });
    graph.install(lib);
};

The iterable is a container literal { elem1, elem2, ... }. Currently only string lists are supported as loop iterables in build.fol.

Helper Routines

build.fol can define helper fun[] and pro[] routines. They are visible only within the file — they are not exported to the package.

Helper Function

fun[] make_lib(graph: Graph, name: str, root: str): Artifact = {
    return graph.add_static_lib({ name = name, root = root });
}

pro[] build(graph: Graph): non = {
    var core = make_lib(graph, "core", "src/core/lib.fol");
    var io   = make_lib(graph, "io",   "src/io/lib.fol");
    var app  = graph.add_exe({ name = "app", root = "src/main.fol" });
    app.link(core);
    app.link(io);
    graph.install(app);
    graph.add_run(app);
}

The helper make_lib receives the graph handle as a parameter because graph is not a global — it is a parameter of build.

Helpers Calling Helpers

Helpers can call other helpers:

fun[] lib_root(name: str): str = {
    return "src/" + name + "/lib.fol";
}

fun[] add_lib(graph: Graph, name: str): Artifact = {
    return graph.add_static_lib({ name = name, root = lib_root(name) });
}

pro[] build(graph: Graph): non = {
    var core = add_lib(graph, "core");
    var app  = graph.add_exe({ name = "app", root = "src/main.fol" });
    app.link(core);
    graph.install(app);
}

Combined Example

fun[] make_lib(graph: Graph, name: str): Artifact = {
    return graph.add_static_lib({ name = name, root = name });
}

pro[] build(graph: Graph): non = {
    var target   = graph.standard_target();
    var optimize = graph.standard_optimize();
    var strip    = graph.option({ name = "strip", kind = "bool", default = false });

    loop(name in {"core", "io", "net"}) {
        make_lib(graph, name);
    };

    var app = graph.add_exe({
        name     = "app",
        root     = "src/main.fol",
        target   = target,
        optimize = optimize,
    });
    graph.install(app);
    graph.add_run(app);

    when(strip == true) {
        {
            var strip_step = graph.step("strip");
            var packed = graph.add_system_tool({
                tool   = "strip",
                output = "gen/app.stripped",
            });
            strip_step.attach(packed);
        }
    };
}

Artifacts, Modules, and Generated Files

The build graph tracks three kinds of compilable or producible outputs: artifacts, modules, and generated files.

Artifacts

Artifacts are the primary compiled outputs of a package.

KindMethodOutput
Executablegraph.add_exeBinary
Static librarygraph.add_static_lib.a / .lib
Shared librarygraph.add_shared_lib.so / .dylib
Test bundlegraph.add_testRunnable test binary

All artifact constructors accept the same base config record:

var app = graph.add_exe({
    name     = "app",      // required: output name
    root     = "src/main.fol",  // required: entry-point source file
    target   = target,     // optional: Target handle
    optimize = optimize,   // optional: Optimize handle
});

name must be lowercase. Allowed characters: a-z, 0-9, -, _, ..

Artifact Name Validation

The build system validates artifact names at evaluation time. Invalid names (uppercase, spaces, special characters) produce a build error.

Linking

Static and shared libraries can be linked into executables using artifact.link(dep):

var core = graph.add_static_lib({ name = "core", root = "src/core/lib.fol" });
var app  = graph.add_exe({ name = "app", root = "src/main.fol" });
app.link(core);

Linking is transitive through the graph. If core itself links utils, app will also see utils.

Installation

Any artifact can be marked for installation:

graph.install(app);
graph.install(core);

Files and directories can also be installed directly:

graph.install_file("config/defaults.toml");
graph.install_dir("assets/");

Modules

Modules are named source units that can be shared across artifacts without being standalone binaries.

var utils = graph.add_module({ name = "utils", root = "src/utils.fol" });
var app   = graph.add_exe({ name = "app", root = "src/main.fol" });
app.import(utils);

artifact.import(module) makes the module visible in the importing artifact’s source scope. Equivalent to Zig’s artifact.root_module.addImport(name, dep).

Modules from dependencies are accessed via dep.module(name):

var dep    = graph.dependency("mylib", "local:../mylib");
var logger = dep.module("logger");
app.import(logger);

Generated Files

Generated files are outputs produced before compilation. They must be declared in the graph so the build system knows to produce them and what depends on them.

Kinds

KindMethodDescription
Writegraph.write_fileWritten with literal string contents
Copygraph.copy_fileCopied from a source path
Tool outputgraph.add_system_toolProduced by an external tool
Codegengraph.add_codegenProduced by the FOL codegen pipeline
Captured runrun.capture_stdout()Stdout of a run step

Connecting Generated Files

A generated file must be connected to the graph entity that depends on it.

Attach to a step (the step triggers its production):

var gen  = graph.add_system_tool({ tool = "protoc", output = "gen/types.fol" });
var step = graph.step("proto");
step.attach(gen);

Add to an artifact (artifact cannot compile without it):

var schema = graph.add_codegen({
    kind   = "schema",
    input  = "schema/api.yaml",
    output = "gen/api.fol",
});
var app = graph.add_exe({ name = "app", root = "src/main.fol" });
app.add_generated(schema);

Capture stdout and feed it as an arg to another run:

var gen_tool   = graph.add_exe({ name = "gen", root = "tools/gen.fol" });
var gen_run    = graph.add_run(gen_tool);
var gen_output = gen_run.capture_stdout();

var app_run = graph.add_run(app);
app_run.add_file_arg(gen_output);

Steps

Steps are named build phases. The build system has several implicit steps (build, run, test, install). Custom steps are declared with graph.step.

var docs = graph.step("docs", "Generate documentation");

Steps are executed in dependency order. A step depends on another step via step.depend_on:

var compile = graph.step("compile");
var docs    = graph.step("docs");
docs.depend_on(compile);

Default Step Bindings

When only one executable exists in a package, the build system automatically binds it to the default build and run steps. When multiple executables exist, explicit step bindings are required via graph.add_run(artifact).

Selecting Steps at the Command Line

fol code build         # run the install steps
fol code build docs    # run the "docs" step
fol code run           # run the default run step
fol code run --step serve  # run the "serve" step
fol code test          # run test steps

Graph Validation

After the build program executes, the graph is validated:

  • No step dependency cycles
  • All artifact inputs (modules, generated files) are resolvable
  • Install targets point to declared artifacts

Validation errors are reported as build evaluation errors before any compilation begins.

Cross Compilation

FOL package builds now stay on one backend path:

FOL source
  -> lowered FOL IR
  -> generated Rust crate
  -> rustc
  -> native binary

Normal artifact builds do not call Cargo anymore. Cargo is still useful for fol code emit rust, but fol code build and fol code run compile the generated crate directly with rustc.

Selecting A Target

Use either:

fol code build --target aarch64-unknown-linux-gnu
fol code build --target x86_64-pc-windows-gnu

or inside build.fol:

pro[] build(graph: Graph): non = {
    var target = graph.standard_target();
    var app = graph.add_exe({
        name = "app",
        root = "src/main.fol",
        target = target,
    });
    graph.install(app);
    graph.add_run(app);
};

Target precedence is:

  1. --target
  2. artifact target declared in build.fol
  3. host default

Accepted Target Spellings

The backend accepts both canonical Rust triples and the shorter FOL spellings already used in build code.

Examples:

  • x86_64-linux-gnu -> x86_64-unknown-linux-gnu
  • x86_64-linux-musl -> x86_64-unknown-linux-musl
  • aarch64-linux-gnu -> aarch64-unknown-linux-gnu
  • aarch64-linux-musl -> aarch64-unknown-linux-musl
  • x86_64-windows-gnu -> x86_64-pc-windows-gnu
  • x86_64-windows-msvc -> x86_64-pc-windows-msvc
  • aarch64-windows-msvc -> aarch64-pc-windows-msvc
  • x86_64-macos-gnu -> x86_64-apple-darwin
  • aarch64-macos-gnu -> aarch64-apple-darwin

Unknown spellings are rejected before the backend tries to build.

Build vs Run

Cross-building and cross-running are different operations.

  • fol code build supports host and non-host targets
  • fol code emit rust stays available for source inspection
  • fol code run is host-only
  • fol code test is host-only

If the selected target does not match the current machine, run and test fail early with a diagnostic instead of trying to execute the foreign binary.

Output Layout

Compiled binaries are target-scoped so host and cross builds do not overwrite each other.

Typical layout:

.fol/build/<profile>/bin/<target>/<artifact>
.fol/build/<profile>/fol-backend/runtime/<target>/<profile>/...

That means:

  • host builds and cross builds can coexist
  • runtime artifacts are compiled per target
  • the generated entry crate links against the matching target runtime

emit rust

fol code emit rust still writes a Cargo-compatible crate for debugging and inspection. That command is source emission, not the product binary build path.

The binary build path remains direct rustc.

Lexical Structure

This section defines the smallest syntactic building blocks of FOL source code.

All FOL source is interpreted as Unicode text encoded in UTF-8. The lexer groups raw characters into tokens such as:

  • keywords
  • identifiers
  • literals
  • symbols
  • comments
  • whitespace/newline boundaries

The lexical chapters answer questions such as:

  • which words are reserved
  • how identifiers are formed
  • which literal forms exist
  • how comments and spacing affect parsing

The detailed chapters are:

  • keywords
  • identifiers
  • comments
  • whitespace
  • strings, characters, and booleans
  • numbers
  • symbols

Keywords

Fol has a number of restricted groups of keywords:

BK (build-in keywords)


BK_OR              `or`
BK_XOR             `xor`
BK_AND             `and`

BK_IF              `if`
BK_FOR             `for`
BK_WHEN            `when`
BK_EACH            `each`
BK_LOOP            `loop`

BK_IS              `is`
BK_HAS             `has`
BK_IN              `in`

BK_THIS            `this`
BK_SELF            `self`

BK_BREAK           `break`
BK_RETURN          `return`
BK_YEILD           `yield`
BK_PANIC           `panic`
BK_REPORT          `report`
BK_CHECK           `check`
BK_ASSERT          `assert`
BK_WHERE           `where`

BK_TRUE            `true`
BK_FALSE           `false`

BK_AS              `as`
BK_CAST            `cast`

BK_DO              `do`
BK_GO              `go`

BUILD-IN KEYWORDS - BK:
`(BK_AS|BK_IN|...)`

AK (assignment keywords)


AK_USE             `use`
AK_DEF             `def`
AK_VAR             `var`
AK_FUN             `fun`
AK_PRO             `pro`
AK_LOG             `log`
AK_TYP             `typ`
AK_STD             `std`

ASSIGNMENT KEYWORDS - AK:
`(AK_USE|AK_DEF|...)`

TK (type keywords)

TK_INT             `int`
TK_FLT             `flt`
TK_CHR             `chr`
TK_BOL             `bol`

TK_ARR             `arr`
TTKVEC             `vec`
TK_SEQ             `seq`
TK_MAT             `mat`
TK_SET             `set`
TK_MAP             `map`

TK_STR             `str`
TK_NUM             `num`

TK_OPT             `opt`
TK_MUL             `mul`
TK_ANY             `any`

TK_PTR             `ptr`
TK_ERR             `err`
TK_NON             `non`

TK_REC             `rec`
TK_LST             `lst`
TK_ENM             `enm`
TK_UNI             `uni`
TK_CLS             `cls`

TK_STD             `std`
TK_MOD             `mod`
TK_BLK             `blk`
TYPE KEYWORDS - TK:
`(TK_INT|TK_FLT|...)`

Note that all of the type keywords are of three characters long. It is recomanded that new identifiers not to be of the same number of characters, as one day in the future that same identifier can be used s a keyword in FOL compiler.

OK (option keywords)

OK_EXP              `exp`
OK_HID              `hid`

Older draft material sometimes used pub. The current compiler/book contract uses exp for exported visibility and hid for file-local visibility.

OPTION KEYWORDS - OK:
`((OK_EXP|OK_HID|...),?)*`

Assigning

`(`*WS*`)*(\W)?(`*AK*`)(\[(`*OK*`)?\])?`

`(`*WS*`)*(`*AK*`)`
| `(`*WS*`)*\W(`*AK*`)`
| `(`*WS*`)*(`*AK*`)(\[\])`
| `(`*WS*`)*\W(`*AK*`)(\[\])`
| `(`*WS*`)*(`*AK*`)(\[(`*OK*`)\])`
| `(`*WS*`)*\W(`*AK*`)(\[(`*OK*`)\])`

Identifiers

Identifiers in the current front-end are ASCII names built from letters, digits, and underscores, but they may not start with a digit. Repeated underscore runs __ are not allowed.

IDENTIFIER:

[a-z A-Z _] [a-z A-Z 0-9 _]*

The hardened front-end currently accepts:

  • leading underscores
  • internal underscores
  • non-leading digits

The hardened front-end currently rejects:

  • leading digits
  • repeated underscore runs
  • non-ASCII identifier spellings

_ by itself is still accepted by the current lexer/parser boundary as a dedicated placeholder or binder surface. It should not be treated as an ordinary named identifier for later-phase semantic work.

Identifier equality

Parser-owned duplicate checks currently treat two identifiers as equal if the following algorithm returns true:

pro sameIdentifier(a, b: string): bol = {
    result = a.replace("_", "").toLowerAscii == b.replace("_", "").toLowerAscii
}

That means ASCII letters are compared case-insensitively and underscores are ignored for those parser-owned duplicate checks. The lexer and stream still preserve original identifier spelling; they do not canonicalize token or namespace text up front.

Comments

Backtick-delimited comments are the authoritative comment syntax in FOL.

Normal comments

Single-line and multiline comments use the same backtick-delimited form.

SINGLE_LINE_COMMENT:

`this is a single line comment`

MULTI_LINE_COMMENT:

`this is a
multi
line
comment`

Doc comments

Documentation comments use the [doc] prefix inside the same backtick-delimited comment family.

DOC_COMMENT:

`[doc] this is a documentation comment`

Current front-end compatibility

The hardened front-end still accepts // and /* ... */ comments as frozen compatibility syntax. They are not the authoritative book spelling, but they remain intentionally supported by the current lexer and parser.

The current front-end also preserves comment kind and raw spelling past lexing. In the parser today, standalone root comments and standalone routine-body comments lower to explicit AST comment nodes, and many inline expression-owned comments now survive through AstNode::Commented wrappers around the parsed node they belong to. That gives later doc-comment tooling retained comment content to build on instead of re-scanning raw source text.

Whitespaces

Whitespace is any non-empty string containing only characters that have the below Unicode properties:

  • U+0009 (horizontal tab, ‘\t’)
  • U+000B (vertical tab)
  • U+000C (form feed)
  • U+0020 (space, ’ ’)
  • U+0085 (next line)
  • U+200E (left-to-right mark)
  • U+200F (right-to-left mark)
  • U+2028 (line separator)
  • U+2029 (paragraph separator)

New lines

New line are used as end-of-line separators:

  • U+000A (line feed, ‘\n’)
  • U+000D (carriage return, ‘\r’)

Strings

Characters

A character is a single Unicode element enclosed within quotes U+0022 (") with the exception of U+0022 itself, which must be escaped by a preceding U+005C character (\).

var aCharacter: chr = "z\n"
var anotherOne: str = "語\n"

Raw characters

Raw character literals do not process any escapes. They are enclosed within single-quotes U+0027 (') with the exception of U+0027 itself:

var aCharacter: chr = 'z'

Strings

A string is a single or a sequence of Unicode elements enclosed within quotes U+0022 (") with the exception of U+0022 itself, which must be escaped by a preceding U+005C character (\).

var hiInEnglish: str = "Hello, world!\n"
var hInCantonese: str = "日本語"

Line-breaks are allowed in strings. A line-break is either a newline (U+000A) or a pair of carriage return and newline (U+000D, U+000A). Both byte sequences are normally translated to U+000A, but as a special exception, when an unescaped U+005C character (\ occurs immediately before the line-break, the U+005C character, the line-break, and all whitespace at the beginning of the next line are ignored. Thus a and b are equal:

var a: str = "foobar";
var b: str = "foo\
              bar";

assert(a,b);

Escape sequences

Some additional escapes are available in either character or non-raw string literals.

codedescription
\pplatform specific newline: CRLF on Windows, LF on Unix
\r, \ccarriage return
\n, \lline feed (often called newline)
\fform feed
\ttabulator
\vvertical tabulator
\\backslash
\“quotation mark
\’apostrophe
\ ‘0’..‘9’+character with decimal value d; all decimal digits directly following are used for the character
\aalert
\bbackspace
\eescape [ESC]
\x HHcharacter with hex value HH; exactly two hex digits are allowed
\u HHHHunicode codepoint with hex value HHHH; exactly four hex digits are allowed
\u {H+}unicode codepoint; all hex digits enclosed in {} are used for the codepoint

Raw strings

Just like raw characters, raw string literals do not process any escapes either. They are enclosed within single-quotes U+0027 (') with the exception of U+0027 itself:

var hiInEnglish: str = 'Hello, world!'

Booleans

The two values of the boolean type are written true and false:

var isPresent: bol = false;

Numbers

A number in the current front-end is either an integer or a floating-point literal. Imaginary suffix forms are intentionally outside the hardened lexer/parser contract for this phase.

Intigers

An integer has one of four forms:

  • A decimal literal starts with a decimal digit and continues with decimal digits and optional separating underscores.
  • A hex literal starts with 0x or 0X and then uses hex digits with optional separating underscores.
  • An octal literal starts with 0o or 0O and then uses octal digits with optional separating underscores.
  • A binary literal starts with 0b or 0B and then uses binary digits with optional separating underscores.
var decimal: int = 45;
var hexadec: int = 0x6HF53BD5;
var octal: int = 0o822371;
var binary: int = 0b010010010;

Underscore

Underscore character U+005F (_) is a special character, that does not represent anything withing the number laterals. An integer lateral containing this character is the same as the one without. It is used only as a syntastc sugar:

var aNumber: int = 540_467;
var bNumber: int = 540467;

assert(aNumber, bNumber)

Floating points

A floating-point has one of two forms:

  • A decimal literal followed by a period character U+002E (.). This is optionally followed by another decimal literal.
  • A decimal literal that follows a period character U+002E (.).
  • A decimal literal followed by a period with no fractional digits is also accepted by the current front-end.
var aFloat: flt = 3.4;
var bFloat: flt = .4;
var cFloat: flt = 1.;

Current front-end note

Imaginary literals such as 5i remain a language-design topic in the book, but they are not tokenized or lowered by the current hardened stream/lexer/parser pipeline.

Symbols

Operators

Fol allows user defined operators. An operator is any combination of the following characters:

=     +     -     *     /     >     .
@     $     ~     &     %     <     :
!     ?     ^     #     `     \     _

The grammar uses the terminal OP to refer to operator symbols as defined here.

Brackets

Bracket punctuation is used in various parts of the grammar. An open bracket must always be paired with a close bracket. Here are type of brackets used in FOL:

brackettypepurpose
{ }Curly bracketsCode blocks, Namespaces, Containers
[ ]Square bracketsType options, Container acces, Multithreading
( )Round bracketsCalculations, Comparisons, Argument passing
< >Angle brackets

The grammar uses the terminal BR to refer to operator symbols as defined here.

Statements And Expressions

This section covers executable syntax.

FOL separates executable forms into two broad groups:

  • statements: forms executed for control flow, side effects, or declaration within a block
  • expressions: forms that compute a value

Statements

Statements include:

  • local declarations
  • assignments
  • routine calls used for side effects
  • branching
  • looping
  • nested blocks

Examples:

var x: int = 0;
x = 5;
if (x > 0) { .echo(x) }
for (item in items) { .echo(item) }

Expressions

Expressions include:

  • operator expressions
  • literal expressions
  • range expressions
  • access expressions
  • call expressions

Expressions can be nested freely and may appear in declarations, assignments, return statements, control-flow headers, and other expressions.

Statements

Statements are executable forms that primarily control behavior, perform side effects, or introduce local declarations inside a block.

This chapter family focuses on:

  • declaration statements
  • assignment-like statements
  • control flow
  • block structure

Examples:

var x: int = 0;
x = 1;
if (x > 0) { .echo(x) }
for (item in items) { .echo(item) }

Control

At least two linguistic mechanisms are necessary to make the computations in programs flexible and powerful: some means of selecting among alternative control flow paths (of statement execution) and some means of causing the repeated execution of statements or sequences of statements. Statements that provide these kinds of capabilities are called control statements. A control structure is a control statement and the collection of statements whose execution it controls. This set of statements is in turn generally structured as a block, which in addition to grouping, also defines a lexical scope.

There are two types of control flow mechanisms:

  • choice - when
  • loop - loop

Choice type

when(condition){ case(condition){} case(condition){} * {} };
when(variable){ is (value){}; is (value){}; * {}; };
when(variable){ in (iterator){}; in (iterator){}; * {}; };
when(iterable){ has (member){}; has (member){}; * {}; };
when(generic){ of (type){}; of (type){}; * {}; };
when(type){ on (channel){}; on (channel){}; };

Condition

when(true) {
    case (x == 6){ // implementation }
    case (y.set()){ // implementation } 
    * { // default implementation }
}

Valueation

when(x) {
    is (6){ // implementation }
    is (>7){ // implementation } 
    * { // default implementation }
}

Iteration

when(2*x) {
    in ({0..4}){ // implementation }
    in ({ 5, 6, 7, 8, }){ // implementation } 
    * { // default implementation }
}

Contains

when({4,5,6,7,8,9,0,2,3,1}) {
    has (5){ // implementation }
    has (10){ // implementation } 
    * { // default implementation }
}

Generics

when(T) {
    of (int){ // implementation }
    of (str){ // implementation } 
    * { // default implementation }
}

Channel

when(str) {
    on (channel){ // implementation }
    on (channel){ // implementation } 
    * { // default implementation }
}

Loop type

loop(condition){};
loop(iterable){};

Condition

loop( x == 5 ){
    // implementation
};

Enumeration

loop( x in {..100}){
    // implementation
}

loop( x in {..100}) if ( x % 2 == 0 )){
    // implementation
}

loop( x in {..100} if ( x in somearra ) and ( x in anotherarray )){
    // implementation
}

Iteration

loop( x in array ){
    // implementation
}

Expressions

Expressions are value-producing forms.

An expression may be:

  • a literal
  • a reference
  • an operator application
  • a call
  • an access form
  • a range or container form

The detailed chapters in this section focus on:

  • calculations and operators
  • literals
  • ranges
  • access expressions

Calculations

In fol, every calcultaion, needs to be enclosed in rounded brackets ( //to evaluate ) - except in one line evaluating, the curly brackets are allowed too { // to evaluate }:

fun adder(a, b: int): int = {
    retun a + b                                                 // this will throw an error 
}

fun adder(a, b: int): int = {
    retun (a + b)                                               // this is the right way to enclose 
}

Order of evaluation is strictly left-to-right, inside-out as it is typical for most others imperative programming languages:

.echo((12 / 4 / 8))                                             // 0.375 (12 / 4 = 3.0, then 3 / 8 = 0.375)
.echo((12 / (4 / 8)))                                           // 24 (4 / 8 = 0.5, then 12 / 0.5 = 24)

Calculation expressions include:

  • arithmetics
  • comparison
  • logical
  • compounds

Arithmetics

The behavior of arithmetic operators is only on intiger and floating point primitive types. For other types, there need to be operator overloading implemented.

symboldescription
-substraction
*multiplication
+addition
/division
%reminder
^exponent
assert((3 + 6), 9);
assert((5.5 - 1.25), 4.25);
assert((-5 * 14), -70);
assert((14 / 3), 4);
assert((100 % 7), 2);

Comparisons

Comparison operators are also defined both for primitive types and many type in the standard library. Parentheses are required when chaining comparison operators. For example, the expression a == b == c is invalid and may be written as ((a == b) == c).

SymbolMeaning
==equal
!=not equal
>>greater than
<<Less than
>=greater than or equal to
<=Less than or equal to
assert((123 == 123));
assert((23 != -12));
assert((12.5 >> 12.2));
assert(({1, 2, 3} << {1, 3, 4}));
assert(('A' <= 'B'));
assert(("World" >= "Hello"));

Logical

A branch of algebra in which all operations are either true or false, thus operates only on booleans, and all relationships between the operations can be expressed with logical operators such as:

  • and (conjunction), denoted (x and y), satisfies (x and y) = 1 if x = y = 1, and (x and y) = 0 otherwise.
  • or (disjunction), denoted (x or y), satisfies (x or y) = 0 if x = y = 0, and (x or) = 1 otherwise.
  • not (negation), denoted (not x), satisfies (not x) = 0 if x = 1 and (not x) = 1ifx = 0`.
assert((true and false), (false and true));
assert((true or false), true)
assert((not true), false)

Compounds

There are further assignment operators that can be used to modify the value of an existing variable. These are the compounds or aka compound assignments. A compound assignment operator is used to simplify the coding of some expressions. For example, using the operators described earlier we can increase a variable’s value by ten using the following code:

value = value + 10;

This statement has an equivalent using the compound assignment operator for addition (+=).

value += 10;

There are compound assignment operators for each of the six binary arithmetic operators: +, -, *, /, % and ^. Each is constructed using the arithmetic operator followed by the assignment operator. The following code gives examples for addition +=, subtraction -=, multiplication *=, division /= and modulus %=:


var value: int = 10;
(value += 10);        // value = 20
(value -= 5);         // value = 15
(value *= 10);        // value = 150
(value /= 3);         // value = 50
(value %= 8);         // value = 2

Compound assignment operators provide two benefits. Firstly, they produce more compact code; they are often called shorthand operators for this reason. Secondly, the variable being operated upon, or operand, will only be evaluated once in the compiled application. This can make the code more efficient.

Literals

A literal expression consists of one or more of the numerical/letter forms described earlier. It directly describes a numbers, characters, booleans, containers and constructs.

There are two type of literals:

  • values
  • calls

Value literals

Value literals are the simpliest expressions. They are direct values assigned to variables and are divided into two types:

  • singletons
  • clusters

Singelton literals

Singleton literals represent one sigle values:

4                       // intiger literal
0xA8                    // hex-intiger literal
4.6                     // floating-point literal
"c"                     // character literal
"one"                   // string literal
true                    // boolean literal

The current hardened front-end does not yet implement imaginary literal lowering, so imaginary examples are intentionally omitted from the active literal surface here.

Cluster literals

Cluster literals represent both container types and construct types. Cluster literals are always enclosed within curly brackets { }. The difference between scopes and cluster literals is that cluster literals shoud always have comma , within the initializaion and assignment brackets, e.g { 5, }.

Containers

Some simple container expressions

{ 5, 6, 7, 8, }                     // array, vector, sequences
{ "one":1, "two":2, }               // maps
{ 6, }                              // single element container

A 3x3x3 matrix

{{{1,2,3},{4,5,6},{7,8,9}},{{1,2,3},{4,5,6},{7,8,9}},{{1,2,3},{4,5,6},{7,8,9}}}

Constructs

// constructs 
{ email = "someone@example.com", username = "someusername123", active = true, sign_in_count = 1 }

// nested constructs
{
    FirstName = "Mark",
    LastName =  "Jones",
    Email =     "mark@gmail.com",
    Age =       25,
    MonthlySalary = {
        Basic = 15000.00,
        Bonus = {
            HTA =    2100.00,
            RA =   5000.00,
        },
    },
}

Call literals

Call literals are function calls that resolve to values:

var seven: int = add(2, 5);             // assigning variables "seven" to function call "add"

`typ Vector: rec = { var x: flt var y: flt }

typ Rect: rec = { var pos: Vector var size: Vecotr }

fun make_rect(min, max: Vector): Rect { return [Rect]{{min.x, min.y}, {max.x - max.y, max.y - max.y}} return [Rect]{pos = {min.x, min.y}, size = {max.x - max.y, max.y - max.y}} }

`

Ranges

There are two range expressions:

  • Defined ranges
  • Undefined ranges

Defined ranges

Defined ranges represent a group of values that are generated as a sequence based on some predefined rules. Ranges are represented with two dots .. operator.

{ 1..8 }                // a range from 1 to 8
{ 1,2,3,4,5,6,7,8 }

{ 8..1 }                // a range from 8 to 1
{ 8,7,6,5,4,3,2,1 }

{ 1..8..2 }             // a range from 1 to 8 jumping by 2
{ 1,3,5,7 }

{ 3..-3 }               // a range from 4 to -4
{ 3,2,1,0,-1,-2,-3 }

{ -3..3 }               // a range from -3 to 3
{ -3,-2,-1,0,1,2,3 }

{ ..5 }                 // a range form 0 to 5 
{ 0,1,2,3,4,5 }

{ ..-5 }                // a range from 0 to -5
{ 0,-1,-2,-3,-4,-5 }

{ 5.. }                 // a range from 5 to 0
{ 5,4,3,2,1,0 }

{ -5.. }                // a range from -5 to 0
{ -5,-4,-3,-2,-1,0 }
syntaxmeaning
start..endfrom start to end
..endfrom zero to end
start..from start to zero

Undefined ranges

Undefined ranges represent values that have only one side defined at the definition time, and the compiler defines the other side at compile time. They are represented with three dots ...

{ 2... }                // from 2 to infinity
syntaxmeaning
start...from start to infinite

In most of the cases, they are used for variadic parameters passing:

fun calc(number: ...int): int = { return number[0] + number[1] + number[2] * number[3]}

Access

There are four access expresions:

  • namespace member access
  • routine member access
  • container memeber access
  • field member access

Subprogram access

One access form is the receiver-style routine call, often called a “method-call expression” in other languages. In FOL this is still procedural syntax: the receiver value is the first routine input, and dot-call form is just sugar at the call site.

A method call consists of an expression (the receiver) followed by a single dot ., an expression path segment, and a parenthesized expression-list:

"3.14".cast(float).pow(2);                  // casting a numbered string to float, then rising it to power of 2

Read:

value.method(arg1, arg2)

as the procedural idea:

method(value, arg1, arg2)

The syntax does not introduce classes or object-owned method dispatch by itself.

Namespaces access

Accesing namespaces is done through double colon operator :::

use log: std = {"fmt/log"};                 // using the log namespace of fmt
io::console::write_out.echo();              // echoing out

Container access

Array, Vectors, Sequences, Sets

Containers can be indexed by writing a square-bracket-enclosed expression of type int[arch] (the index) after them.

var collection: int = { 5, 4, 8, 3, 9, 0, 1, 2, 7, 6 }

collection[5]                               // get the 5th element staring from front (this case is 0)
collection[-2]                              // get the 3th element starting from back (this case is 1)

Containers can be accessed with a specified range too, by using colon within a square-bracket-enclosed:

syntaxmeaning
:the whole container
elA:elBfrom element elA to element elB
:elAfrom beginning to element elA
elA:from element elA to end
collection[-0]                              // last item in the array
{ 6 }
collection[-1:]                             // last two items in the array
{ 7, 6 }
collection[:-2]                             // everything except the last two items
{ 5, 4, 8, 3, 9, 0, 1, 2 }

If we use double colon within a square-bracket-enclosed then the collection is inversed:

syntaxmeaning
::the whole container in reverse
elA::elBfrom element elA to element elB in reverse
::elAfrom beginning to element elA in reverse
elA::from element elA to end in reverse
collection[::]                              // all items in the array, reversed
{ 6, 7, 2, 1, 0, 9, 3, 8, 4, 5 }
collection[2::]                             // the first two items, reversed
{ 4, 5 }
collection[-2::]                            // the last two items, reversed
{ 6, 7 }
collection[::-3]                            // everything except the last three items, reversed
{ 2, 1, 0, 9, 3, 8, 4, 5 }

Matrixes

Matrixes are 2D+ arrays, thus they have a bit more complex acces way:

var aMat = mat[int, int] = { {1,2,3}, {4,5,6}, {7,8,9} };

nMat[[1][0]]                                // this will return 4
                                            // first [] accesses the first dimension, then second [] accesses the second

All other operations are the same like arrays.

Maps

Accesing maps is donw by using the key within square-bracket-enclosed:

var someMap: map[str, int] = { {"prolog", 1}, {"lisp", 2}, {"c", 3} }
someMap["lisp"]                             // will return 2

Axioms

Accesing axioms is more or less like accessing maps, but more verbose and matching through backtracing, and the return is always a vector of elements (empty if no elements are found):

var parent: axi[str, str] = { {"albert","bob"}, {"alice","bob"}, {"bob","carl"}, {"bob","tom"} };

parent["albert",*]                          // this will return strng vector: {"bob"}
parent["bob",*]                             // this will return strng vector: {"carl","tom"}

parent[*,_]                                 // this will match to {"albert", "alice", "bob"}

Matching can be with a vector too:

var parent: axi[str, str] = { {"albert","bob"}, {"alice","bob"}, {"bob","carl"}, {"bob","tom"}, {"maggie","bill"} };
var aVec: vec[str] = { "tom", "bob" };

parent[*,aVec]                              // will match all possible values that have "tom" ot "bob" as second element
                                            // in this case will be a strng vector: {"albert", "alice", "bob"}

a more complex matching:

var class: axi;
class.add({"cs340","spring",{"tue","thur"},{12,13},"john","coor_5"})
class.add({"cs340","winter",{"tue","fri"},{12,13},"mike","coor_5"})
class.add({"cs340",winter,{"wed","fri"},{15,16},"bruce","coor_3"})
class.add({"cs101",winter,{"mon","wed"},{10,12},"james","coor_1"})
class.add({"cs101",spring,{"tue","tue"},{16,18},"tom","coor_1"})

var aClass = "cs340"
class[aClass,_,[_,"fri"],_,*,_]             // this will return string vector: {"mike", bruce}
                                            // it matches everything that has aClass ad "fri" within
                                            // and ignore ones with meh symbol

Avaliability

To check if an element exists, we add : before accessing with []. Thus this will return true if element exists.

var val: vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

val:[5]                                     // returns true
val:[15]                                    // returns false


var likes: axi[str, str] = { {"bob","alice"} , {"alice","bob"}, {"dan","sally"} };

likes["bob","alice"]:                       // will return true
likes["sally","dan"]:                       // will return false

In-Place assignment

One of the features that is very important in arrays is that they can assign variables immediately:

var val: vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var even: vec = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20}
val[even => Y]                              // this equals to {2, 4, 6, 8, 10} and same time assign to Y
.echo(Y)                                    // will print {2, 4, 6, 8, 10}

This dows not look very much interesting here, you can just as easy assign the whole filtered array to a variable, but it gets interesting for axioms:

var parent: axi[str, str] = { {"albert","bob"}, {"alice","bob"}, {"bob","carl"}, {"bob","tom"}, {"maggie","bill"} };

parent:[* => Y,"bob"]                       // this one returns true if we find a parent of "bob"
                                            // same time it assigns parent to a string vector `Y`

Field access

Field access expressoin accesses fields inside constructs. Here is a recorcalled user:

var user1: user = {
    email = "someone@example.com",
    username = "someusername123",
    active = true,
    sign_in_count = 1
};

fun (user)getName(): str = { result = self.username; };

There are two types of fields that can be accesed within constructs:

  • methods
  • data

Methods

Methods are accessed with the same dot form as routine member access, but they remain receiver-qualified routines rather than object-owned behavior.

user1.getName()

Data

There are multiple ways to acces data within the construct. The easiest one is by dot operator .:

user1.email                                 // accessing the field through dot-accesed-memeber

Another way is by using square bracket enclosed by name:

user1[email]                                // accessing the field through square-bracket-enclosed by name

And lastly, by square bracket enclosed by index:

user1[0]                                    // accessing the field through square-bracket-enclosed by index

Metaprogramming

This section describes the compile-time extension surface of FOL.

Metaprogramming facilities allow source to be generated, transformed, or specialized before normal execution semantics apply.

The chapter family currently covers:

  • built-ins
  • macros
  • alternatives
  • defaults
  • templates

These features are powerful but high-risk for readability. They should be used to remove repetitive structure, not to obscure ordinary program logic.

Intrinsics

Intrinsics are compiler-owned language operations.

They are not ordinary library functions, and they are not imported through use.

FOL currently keeps three layers separate:

  • intrinsics: compiler-owned operations such as .eq(...), .len(...), check(...), and panic(...)
  • core: ordinary foundational library code that should work across targets
  • std: broader library code such as filesystem, networking, serialization, and other richer services

If an operation can live as an ordinary library API, that is usually the better home for it. Intrinsics are reserved for surfaces the compiler must understand directly.

Surfaces

The current compiler recognizes three intrinsic surfaces.

Dot-root calls

These are written with a leading dot:

.eq(a, b)
.not(flag)
.len(items)
.echo(value)

Dot-root intrinsics are the main current intrinsic family.

Keyword calls

These look like language keywords rather than dot calls:

check(read_code(path))
panic("unreachable state")

The current V1 compiler treats check and panic as intrinsics too, even though they are not written with ..

Operator aliases

Some future intrinsic surfaces are written like operators:

value as target_type
value cast target_type

These are registry-owned now, but they are not implemented in the current V1 compiler.

Current V1 implemented intrinsics

The current compiler implements this subset end to end through type checking and lowering.

For current V1, backend execution of the implemented intrinsic set is expected to go through fol-runtime where policy matters. In practice that means:

  • .len(...) uses the runtime length helper
  • .echo(...) uses the runtime echo hook and formatting contract
  • check(...) uses the runtime recoverable-result inspection contract
  • scalar comparisons and .not(...) may lower to native target operations

Comparison

.eq(left, right)
.nq(left, right)
.lt(left, right)
.gt(left, right)
.ge(left, right)
.le(left, right)

Current V1 rule:

  • .eq(...) and .nq(...) work on comparable scalar pairs
  • .lt(...), .gt(...), .ge(...), and .le(...) work on ordered scalar pairs

If you call them with the wrong number of arguments or with unsupported type families, the compiler reports an intrinsic-specific type error.

Boolean

.not(flag)

Current V1 rule:

  • .not(...) accepts exactly one bol

Query

.len(items)

Current V1 rule:

  • .len(...) accepts exactly one operand
  • the operand must currently be one of:
    • str
    • arr[...]
    • vec[...]
    • seq[...]
    • set[...]
    • map[...]

In the current compiler, .len(...) is the only implemented query intrinsic.

Diagnostic

.echo(value)

Current V1 rule:

  • .echo(...) accepts exactly one argument
  • it emits the value through the fol-runtime debug hook
  • it then forwards the same value unchanged

So this is valid:

fun[] main(flag: bol): bol = {
    return .echo(flag)
}

Recoverable and control intrinsics

check(read_code(path))
panic("fatal")

Current V1 rule:

  • check(expr) asks whether a recoverable routine call failed and returns bol
  • panic(...) aborts control flow immediately

These are described in more detail in the recoverable-error chapter.

Current V1 deferred intrinsics

The registry already reserves more names than the compiler implements.

That does not mean they work today.

Reserved but deferred for likely V1.x

  • as
  • cast
  • assert
  • .cap(...)
  • .is_empty(...)
  • .low(...)
  • .high(...)

These are recognized as registry-owned language surfaces, but the current compiler rejects them with explicit milestone-boundary diagnostics.

Reserved for later V2

  • bitwise helpers such as .bit_and(...), .bit_or(...), .shl(...), .shr(...), .rotl(...), .rotr(...), .pop_count(...), .clz(...), .ctz(...), .byte_swap(...), .bit_reverse(...)
  • overflow-mode helpers such as .checked_add(...), .wrapping_add(...), .saturating_add(...), .overflowing_add(...), and their subtraction forms

These are intentionally reserved now so the language can grow without accidental user-space name collisions, but they are not part of the current V1 compiler.

Reserved for later V3

  • .de_alloc(...)
  • .give_back(...)
  • .address_of(...)
  • .pointer_value(...)
  • .borrow_from(...)

These depend on later ownership, pointer, and low-level systems semantics.

Library-preferred surfaces

Some names are kept in the registry roadmap only as placeholders while the language decides whether they should really stay compiler-owned.

Current examples:

  • .add(...)
  • .sub(...)
  • .mul(...)
  • .div(...)
  • .abs(...)
  • .min(...)
  • .max(...)
  • .clamp(...)
  • .floor(...)
  • .ceil(...)
  • .round(...)
  • .trunc(...)
  • .pow(...)
  • .sqrt(...)

The current direction is that many of these may fit better in core or std instead of becoming permanent compiler intrinsics.

Intrinsics are not shell operations

Do not confuse intrinsics with shell syntax such as nil and postfix unwrap !.

For example:

ali MaybeText: opt[str]
ali Failure: err[str]

fun[] unwrap_optional(value: MaybeText): str = {
    return value!
}

fun[] unwrap_failure(value: Failure): str = {
    return value!
}

That ! surface is part of shell typing, not the intrinsic registry.

Likewise, recoverable routine calls such as:

fun[] read_code(path: str): int / str = { ... }

are handled with:

  • check(expr)
  • expr || fallback

not with shell unwrap.

Current compiler truth

The current compiler has one shared intrinsic registry crate:

fol-intrinsics

That registry is the source of truth for:

  • canonical intrinsic names and aliases
  • milestone availability (V1 / V2 / V3)
  • type-checking selection rules
  • lowering mode
  • backend/runtime role classification

The current runtime companion for implemented V1 intrinsics is:

fol-runtime

  • intrinsic names
  • aliases
  • categories
  • current milestone availability
  • deferred-roadmap classification
  • lowering mode
  • backend-facing role

So the short rule is:

  • parser recognizes intrinsic syntax
  • fol-intrinsics owns intrinsic identity
  • type checking validates intrinsic calls
  • lowering maps them to explicit IR shapes

This page should describe only the subset that is actually implemented, plus clearly marked deferred surfaces.

Macros

Are a very complicated system, and yet can be used as simply as in-place replacement. A lot of build-in macros exist in the language to make the code more easy to type. Below are some system defined macros.

For example, wherever $ is before any variable name, its replaced with .to_string. Or wherever ! is before bol name, its replaced with .not but when the same ! is placed before ptr it is replaced with .delete_pointer.

def '$'(a: any): mac = '.to_string'
def '!'(a: bol): mac = '.not '
def '!'(a: ptr): mac = '.delete_pointer';
def '*'(a: ptr): mac = '.pointer_value';
def '#'(a: any): mac = '.borrow_from';
def '&'(a: any): mac = '.address_of';

Alternatives

Alternatives are used when we want to simplify code. For example, define an alternative, so whenever you write +var it is the same as var[+].

def '+var': alt = 'var[+]'
def '~var': alt = 'var[~]'
def '.pointer_content': alt = '.pointer_value'

Defaults

Defaults are a way to change the default behaviour of options. Example the default behaviour of str when called without options. By defalt str is it is saved on stack, it is a constant and not public, thus has str[pil,imu,nor], and we want to make it mutable and saved on heap by default:

def 'str': def[] = 'str[new,mut,nor]'

Templates

Templates are supposed to be mostly used for operator overloading. They are glorified functions, hence used with pro or fun instead of def.

For example here is how the != is defined:

fun '!='(a, b: int): bol = { return .not(.eq(a, b)) }

.assert( 5 != 4 )

or define $ to return the string version of an object (careful, it is object$ and not $object, the latest is a macro, not a template):

pro (file)'$': str = { return "somestring" }

.echo( file$ )

Type System

This section defines the built-in type families used throughout the language.

Every expression has a type, and every declaration that introduces a value or callable surface interacts with the type system.

The built-in type families are grouped as:

  • ordinal types: integers, floats, booleans, characters
  • container types: arrays, vectors, sequences, matrices, maps, sets
  • complex types: strings, numeric abstractions, pointers, errors
  • special types: optional, union-like/sum-style surfaces, any-like and none-like forms

User-defined type construction is described later in the declarations section under typ, ali, records, entries, and standards.

Ordinal

Ordinal types

Ordinal types have the following characteristics:

  • Ordinal types are countable and ordered. This property allows the operation of functions as inc, ord, dec on ordinal types to be defined.
  • Ordinal values have a smallest possible value. Trying to count further down than the smallest value gives a checked runtime or static error.
  • Ordinal values have a largest possible value. Trying to count further than the largest value gives a checked runtime or static error.

Ordinal types are the most primitive type of data:

  • Intigers: int[options]
  • Floating: flt[options]
  • Characters: chr[options]
  • Booleans: bol

Intiger type

An integer is a number without a fractional component. We used one integer of the u32 type, the type declaration indicates that the value it’s associated with should be an unsigned integer (signed integer types start with i, instead of u) that takes up 32 bits of space:

var aVar: int[u32] = 45;

Each variant can be either signed or unsigned and has an explicit size. Signed and unsigned refer to whether it’s possible for the number to be negative or positive—in other words, whether the number needs to have a sign with it (signed) or whether it will only ever be positive and can therefore be represented without a sign (unsigned). It’s like writing numbers on paper: when the sign matters, a number is shown with a plus sign or a minus sign; however, when it’s safe to assume the number is positive, it’s shown with no sign.

Length  |   Signed  | Unsigned  |
-----------------------------------
8-bit   |   8       |   u8      |
16-bit  |   16      |	u16     |
32-bit	|   32      |	u32     |
64-bit	|   64	    |   u64     |
128-bit	|   128     |	u128    |
arch	|   arch    |	uarch   |

Float type

Fol also has two primitive types for floating-point numbers, which are numbers with decimal points. Fol’s floating-point types are flt[32] and flt[64], which are 32 bits and 64 bits in size, respectively. The default type is flt[64] because on modern CPUs it’s roughly the same speed as flt[32] but is capable of more precision.

Length  |    Type  |
--------------------
32-bit	|   32     |
64-bit	|   64     |
arch	|   arch   |

Floating-point numbers are represented according to the IEEE-754 standard. The flt[32] type is a single-precision float, and flt[f64] has double precision.

pro[] main: int = {
    var aVar: flt = 2.;                         // float 64 bit
    var bVar: flt[64] = .3;                     // float 64 bit
    .echo(.eq(aVar, bVar))                      // compare values with the V1 intrinsic surface

    var bVar: flt[32] = .54;                    // float 32 bit
}

Character type

In The Unicode Standard 8.0, Section 4.5 “General Category” defines a set of character categories. Fol treats all characters in any of the letter as Unicode letters, and those in the Number category as Unicode digits.

chr[utf8,utf16,utf32]
def testChars: tst["some testing on chars"] = {
    var bytes = "hello";
    .echo(.len(bytes));
    .echo(bytes[1]);
    .echo(.eq("e", "\x65"));
}

Current `V1` intrinsic note:

- `.eq(...)` is implemented for scalar equality
- `.len(...)` is implemented for strings and supported containers
- older helper surfaces such as `.assert(...)`, `.typeof(...)`, and related
  compile-time queries are not part of the current implemented intrinsic subset

Boolean type

The boolean type is named bol in Fol and can be one of the two pre-defined values true and false.

bol

Container

Containers are of compound types. They contain other primitive or constructed types. To access the types in container those brackets are used: [], so:

var container: type = { element, element, element }             // declaring a container
var varable: type = container[2]                                // accessing the last element

{{% notice note %}}

Containers are always zero indexed

{{% /notice %}}

Static Arrays

Arrays

arr[type,size]

Arrays are the most simple type of container. They contain homogeneous type, meaning that each element in the array has the same type. Arrays always have a fixed length specified as a constant expression arr[type, size]. They can be indexed by any ordinal type to acces its members.

pro[] main: int = {
    var anArray: arr[int, 5] = { 0, 1, 2, 3, 4 };             // declare an array of intigers of five elements
    var element = anArray[3];                                 // accessing the element
    .echo(element)                                            // prints: 3
}

To allocate memory on heap, the var[new] is used more about memory, ownreship and pointer :

pro[] main: int = {
    var[new] aSequence: arr[str] = { "get", "over", "it" };   // this array is stored in stack
}

Dynamic arrays

Dynamic are similar to arrays but of dynamic length which may change during runtime (like strings). A dynamic array s is always indexed by integers from 0 to .len(s)-1 and its bounds are checked.

Current V1 intrinsic note:

  • .len(...) is implemented
  • .low(...), .high(...), .cap(...), and .is_empty(...) are registry-owned deferred surfaces, not active V1 intrinsics

So the safe current query surface for containers is .len(...).

Current V1 runtime note:

  • arr[...] remains the fixed-size container family
  • vec[...] and seq[...] are lowered onto dedicated runtime container types
  • set[...] and map[...] also use runtime-backed container types
  • runtime-backed set[...] and map[...] preserve deterministic ordering for rendering and backend-visible behavior in the current compiler

{{% notice tip %}}

Dynamic arrays are a dynamically allocated (hence the name), thus if not allocated in heap but in stack, the size will be defined automatically in compile time and will be changed to static array.

{{% /notice %}}

There are two implementations of dynamic arrays:

  • vectors vec[]
  • sequences seq[]

Vecotors

Vectors are dynamic arrays, that resizes itself up or down depending on the number of content.

Advantage:

  • accessing and assignment by index is very fast O(1) process, since internally index access is just [address of first member] + [offset].
  • appending object (inserting at the end of array) is relatively fast amortized O(1). Same performance characteristic as removing objects at the end of the array. Note: appending and removing objects near the end of array is also known as push and pop.

Disadvantage:

  • inserting or removing objects in a random position in a dynamic array is very slow O(n/2), as it must shift (on average) half of the array every time. Especially poor is insertion and removal near the start of the array, as it must copy the whole array.
  • Unpredictable performance when insertion or removal requires resizing
  • There is a bit of unused space, since dynamic array implementation usually allocates more memory than necessary (since resize is a very slow operation)

In FOL vecotrs are represented like this:

vec[type]

Example:

pro[] main: int = {
    var[new] aSequence: seq[str] = { "get", "over", "it" };   // declare an array of intigers of five elements
    var element = aSequence[3];                               // accessing the element
}

Sequences

Sequences are linked list, that have a general structure of [head, [tail]], head is the data, and tail is another Linked List. There are many versions of linked list: singular, double, circular etc…

Advantage:

  • fast O(1) insertion and removal at any position in the list, as insertion in linked list is only breaking the list, inserting, and repairing it back together (no need to copy the tails)
  • linked list is a persistent data structure, rather hard to explain in short sentence, see: wiki-link . This advantage allow tail sharing between two linked list. Tail sharing makes it easy to use linked list as copy-on-write data structure.

Disadvantage:

  • Slow O(n) index access (random access), since accessing linked list by index means you have to recursively loop over the list.
  • poor locality, the memory used for linked list is scattered around in a mess. In contrast with, arrays which uses a contiguous addresses in memory. Arrays (slightly) benefits from processor caching since they are all near each other

In FOL sequneces are represented like this:

seq[type]

Example:

pro[] main: int = {
    var[new] aSequence: seq[str] = { "get", "over", "it" };   // declare an array of intigers of five elements
    var element = aSequence[3];                               // accessing the element
}

SIMD

Matrixes are of type SIMD (single instruction, multiple data )

Matrix

mat[sizex]
mat[sizex,sizey]
mat[sizex,sizey,sizez]

Sets

set[type,type,type..]

A set is a general way of grouping together a number of values with a variety of types into one compound type. Sets have a fixed length: once declared, they cannot grow or shrink in size. In other programming languages they usually are referenced as tuples.

pro[] main: int = {
    var aSet: set[str, flt, arr[int, 2]] = { "go", .3, { 0, 5, 3 } };
    var element = aSet[2][1];                                 // accessing the [1] element of the `arr` in the set
    .echo(element)                                            // prints: 5
}

Maps

map[key,value]

A map is an unordered group of elements of one type, called the element type, indexed by a set of unique keys of another type, called the key type.

pro[] main: int = {
    var aMap: map[str, int] = { {"US",45}, {"DE",82}, {"AL",54} };
    var element = aMap["US"];                                 // accessing the "US" key
    .echo(element)                                            // prints: 45
}

The number of map elements is called its length. For a map aMap, it can be discovered using the intrinsic .len(...) and may change during execution. To add a new element, we use name+[element] or addfunction:

.echo(.len(aMap))           // prints: 3
aMap.add( {"IT",55} )
aMap+[{"RU",24}]
.echo(.len(aMap))           // prints: 4

In current V1, backend execution should treat .len(...) and runtime-visible container rendering as part of the fol-runtime contract rather than re-deriving container policy per backend. The comparison operators == and != must be fully defined for operands of the key type; thus the key type must not be a function, map, or sequence.

{{% notice tip %}}

Maps are a growable containers too, thus if not allocated in heap but in stack, the size will be defined automatically in compile time and will be changet to static containers

{{% /notice %}}

Axiom

axi[typ, typ]

A axiom is a list of facts. A fact is a predicate expression that makes a declarative statement about the problem domain. And whenever a variable occurs in a expression, it is assumed to be universally quantified as silent.

var likes: axi[str, str] = { {"bob","alice"} , {"alice","bob"}, {"dan","sally"} };

{{% notice info %}}

Accesing any container always returns the value, but if we put an : before the access symbol so :[], then it will return true or false if there is data or not on the specified access.

{{% /notice %}}

likes["bob","alice"]                // will return {"bob","alice"}
likes:["bob","alice"]               // will return true
likes["sally","dan"]                // will return {}
likes:["sally","dan"]               // will return false

Axioms are a data types that are meant to be used with logic programming. There are containers where facts are stated, and when we want to acces the data, they are always served as containers.

var parent: axi[str, str] = { {"albert","bob"}, {"alice","bob"}, {"bob","carl"}, {"bob","tom"} };

parent["bob",*]                     // this gets all elements that "bob" relates to
{"carl", "tom"}
parent[*,"bob"]                     // this gets all elements that "bob" relates from
{"albert", "alice"}

Adding new element can be done like in other containers:

var parent: axi[str, str] = { {"albert","bob"}, {"alice","bob"}, {"bob","carl"}, {"bob","tom"} };
parent.add({"albert","betty"})
parent.add({"albert","bill"})
parent.add({"alice","betty"})
parent.add({"alice","bill"})

And they can be nesetd too:

var line: axi[axi[int, int], axi[int, int]] = {{{4,5},{4,8}},{{8,5},{4,5}}}

And we can use the simplified form too, just axi instead of all the type. We let the compiler fill in the for us:

var line: axi = {{{4,5},{4,8}},{{8,5},{4,5}}}

Complex

Strings

Strings are a complex type that are made of array of chars with null terminator ‘\0’, and by default is utf8 encoded:

str[]

Number

Number type is an abstraction of integer and float type in the current hardened front-end. Imaginary-number support is still outside the active stream/lexer/parser contract.

num[]

Pointer

ptr[]

Error

err[]

Special

Optional

Either are empty or have a value

opt[]

Never

nev[]

The never type is a type with no values, representing the result of computations that never complete.

Union

Union is a data type that allows different data types to be stored in the same memory locations. Union provides an efficient way of reusing the memory location, as only one of its members can be accessed at a time. It uses a single memory location to hold more than one variables. However, only one of its members can be accessed at a time and all other members will contain garbage values. The memory required to store a union variable is the memory required for the largest element of the union.

We can use the unions in the following locations.

  • Share a single memory location for a variable and use the same location for another variable of different data type.
  • Use it if you want to use, for example, a long variable as two short type variables.
  • We don’t know what type of data is to be passed to a function, and you pass union which contains all the possible data types.
var aUnion: uni[int[8], int, flt]; 

Any

any[]

Null

nil

In the current V1 compiler milestone, nil is accepted only when type checking already knows it is flowing into an opt[...] or err[...] shell. Plain inference from nil alone is not supported.

Current V1 shell note:

  • opt[...] and err[...] are shell values.
  • postfix unwrap value! applies to those shell values.
  • routine calls declared with ResultType / ErrorType are not err[...] shells.
  • use check(...) or expr || fallback for those routine calls instead of !.
  • use err[...] when you need a storable error value.

Declarations And Items

This section covers the main named program elements of FOL.

The declaration families include:

  • bindings: var, let, con, lab
  • routines: fun, pro, log
  • type and construct declarations: typ, ali
  • contract-like declarations: std

Module-like forms such as use, def, seg, and imp are covered in the modules chapter because they primarily define source layout, namespace, and composition boundaries.

Variables

Here are some of the ways that variables can be defined:

var[mut] counter: int = 98
var[exp] label: str = "this is a string"
var[~] ratio = 192.56
+var short_flag = true
var names: arr[str, 3] = { "one", "two", "three" }
var scores: seq[int] = { 20, 25, 45, 68, 73, 98 }
var pair: set[int, str] = { 12, "word" }
var picked = names[1]

Assignments

Following the general rule of FOL:

declaration[options] name: type[options] = { implementation; };

then declaring a new variable is like this:

var[exp] aVar: int = 64

however, the short version can be used too, and the compiler figures out at compute time the type:

var shortVar = 24;                      // compiler gives this value of `int[arch]`

When new variable is created, and uses an old variable to assign, the resulting binding is a new value binding rather than an alias to the old name:

pro[] main: int = {
    var aVar: int = 55;
    var newVar: int = aVar;
    return newVar;
}

Ownership, borrowing, and pointer-level aliasing are later systems-language work and are described in the memory chapters as future milestones rather than as part of the current V1 compiler contract.

Variables can be assigned to an output of a function:

pro[] main: int = {
    fun addFunc(x, y: int): int = {
        return x + y;
    }
    var aVar: int = addFunc(4, 5);
}

Piping / Ternary

Piping can be used as ternary operator. More about piping can be found here. Here is an example, the code below basically says: if the function internally had an error, don’t exit the program, but assign another value (or default value) to the variable:

pro[] main: int = {
    fun addFunc(x, y: int): int = {
        return x + y;
    }
    var aVar: int = addFunc(4, 5) | result > 8 | return 6;
}

Borrowing

If we want to reference a variable, the easiest way is to borrow the variable, use inside another scope (or the same) and return it back. If the ownership is not returned manually, by the end of the scope, it gets returned automatically.

pro[] main: int = {
    var[~] aVar: int = 55;
    {
        var[bor] newVar: int = aVar         // var[bor] represents borrowing
        .echo(newVar)                       // this return 55
    }
        .echo(aVar)                         // here $aVar it not accesible, as the ownership returns at the end of the scope
        .echo(newVar)                       // we cant access the variable because the scope has ended
}

More on borrowing you can find here

Options

As with all other blocks, var have their options: var[opt]:

Options can be of two types:

  • flags eg. var[mut]
  • values eg. var[pri=2]

Flag options can have symbol aliases eg. var[mut] is the somename as var[~].

|  opt   | s |   type    | description                                       | control       |
----------------------------------------------------------------------------------------------
|  mut   | ~ |   flag    | making a variable mutable                         | mutability    |
|  imu   |   |   flag    | making a variable imutable (default)              |               |
|  sta   | ! |   flag    | making a variable a static                        |               |
|  rac   | ? |   flag    | making a variable reactive                        |               |
----------------------------------------------------------------------------------------------
|  exp   | + |   flag    | making a global variable exported                 | visibility    |
|  nor   |   |   flag    | making a global variable normal (default)         |               |
|  hid   | - |   flag    | making a global variable file-local               |               |

Alternatives

There is a shorter way for variables using alternatives, for example, instead of using var[+], a leaner +var can be used instead.

+var aVar: int = 55
fun[] main(): int = {
    .echo(aVar)
    return aVar
}

However, when we use two option in varable, only one can use the alternative form, so instead of using var[mut,exp], this can be used +var[mut] or +var[~], or vice varsa ~var[exp] or ~var[+]:

+var[mut] aVar: int = 55
fun[] main(): int = {
    .echo(aVar)
    return aVar
}

Types

Immutable types (constants)

By default when a variable is defined without options, it is immutable type, for example here an intiger variable:

pro[] main: int = {
    var aNumber: int = 5;
    aNumber = 54;                       // reassigning varibale $aNumber thorws an error
}

Mutable types

If we want a variable to be mutable, we have to explicitly pass as an option to the variable var[mut] or var[~]:

pro[] main: int = {
    var[mut] aNumber: int = 5
    var[~] anotherNumber: int = 24
    aNumber, anotherNumber = 6          // this is completely fine, we assign two wariables new values
}

Reactive types

Current milestone note: reactive variables are part of a later milestone, not the current V1 compiler contract. The syntax may appear in design examples, but present-day V1 typechecking rejects reactive semantics explicitly.

Reactive types is a types that flows and propagates changes.

For example, in an normal variable setting, var a = b + c would mean that a is being assigned the result of b + c in the instant the expression is evaluated, and later, the values of b and c can be changed with no effect on the value of a. On the other hand, declared as reactive, the value of a is automatically updated whenever the values of b or c change, without the program having to re-execute the statement a = b + c to determine the presently assigned value of a.

pro[] main: int = {
    var[mut] b, c = 5, 4;
    var[rac] a: int = b + c
    .echo(a)                            // prints 9
    c = 10;
    .echo(a)                            // now it prints 10
}

Static types

Current milestone note: static variables are also part of later systems/runtime work. The current V1 compiler keeps them outside the implemented subset.

Is a variable which allows a value to be retained from one call of the function to another, meaning that its lifetime declaration. and can be used as var[sta] or var[!]. This variable is special, because if it is initialized, it is placed in the data segment (aka: initialized data) of the program memory. If the variable is not set, it is places in .bss segmant (aka: uninitialized data)

pro[] main: int = {
    {
        var[!] aNumber: int = 5
    }
    {
        .echo(aNumber)                  // it works as it is a static variable.
    }
}

Scope

As discussed before, files in the same package share one package scope. That means package-level functions and variables may be used across sibling files without importing those sibling files one by one.

However, package-private declarations are still different from exported declarations:

  • default visibility means the declaration is available inside the same package
  • exp / + means the declaration may be used through imports from outside the package
  • hid / - means the declaration is visible only inside its own file

So the visibility model is:

  • package scope by default
  • exported outside the package with exp
  • file-only with hid

In order for a variable to be accessed by the importer, it needs the exp flag option, so var[exp], or var[+].

package shko, file1.fol

fun[exp] add(a, b: int): int = { return a + b }
fun sub(a, b: int): int = { return a - b }

package vij, file1.fol

use shko: loc = {"../folder/shko"}

fun[] main(): int = {
    .echo(add(5, 4))                    // this works, `add` is exported
    .echo(sub(5, 4))                    // this fails, `sub` is not exported
    return add(5, 4)
}

There is even the opposite option too. If we want a function or variable to be used only inside its own file, even though the package is shared, then we use the hid option flag: var[hid] or var[-].

file1.fol

var[-] aVar: str = "yo, sup!"

file2.fol

fun[] main(): int = {
    .echo(aVar)                           // this throws, `aVar` is hidden to its own file
    return 0
}

Multiple

Many to many

Many variables can be assigned at once, This is especially usefull, if variables have same options but different types eg. variable is mutabe and exported:

~var[exp] oneVar: int[32] = 24, twoVar = 13, threeVar: string = "shko";

Or to assign multiple variables of the same type:

~var[exp] oneVar, twoVar: int[32] = 24, 13;

To assign multiple variables of multiple types, the type is omitted, however, this way we can not put options on the type (obviously, the default type is assign by compiler):

~var[exp] oneVar, twoVar, threeVar = 24, 13, "shko";

Another “shameless plagiarism” from golang can be used by using ( ... ) to group variables:

~var[exp] (
    oneVar: int[32] = 13,
    twoVar: int[8] = 13,
    threeVar: str = "shko",
)

Many to one

Many variables of the same type can be assigned to one output too:

var oneVar, twoVar: int[8] = 2;

However, each of them gets a copy of the variable on a new memory address:

.assert(&oneVar == &twoVar)           // this will return false

One to many

And lastly, one variable can be assigned to multiple ones. This by using container types:

oneVar grouppy: seq[int] = { 5, 2, 4, 6 }

Or a more complicated one:

var anothermulti: set[str, seq[num[f32]]] = { "string", {5.5, 4.3, 7, .5, 3.2} }

Or a very simple one:

var simplemulti: any = { 5, 6, {"go", "go", "go"} }

Containers

Containers are of special type, they hold other types within. As described before, there are few of them

Access

To acces container variables, brackets like this [] are use:

var shortvar = anothermulti[1][3]     // compiler will copy the value `anothermulti[1][3]` (which is a float) to a new memory location

Routines

Routines are callable declarations.

FOL has three routine families:

  • pro: procedures, used for effectful work
  • fun: functions, intended for ordinary value-producing computation
  • log: logical routines and relation-like callable forms

Routine declarations support a shared structural pattern:

fun[options] name(params): return_type = { body }
pro[options] name(params): return_type = { body }
log[options] name(params): return_type = { body }

FOL also allows an alternate header style:

fun[options] name: return_type = (params) { body }

This chapter family covers parameters, calls, defaults, variadics, return values, and routine-specific semantics.

Types

There are two main types of routines in fol:

  • Procedurues

    A procedure is a piece of code that is called by name. It can be passed data to operate on (i.e. the parameters) and can optionally return data (the return value). All data that is passed to a procedure is explicitly passed.

  • Functions

    A function is called pure function if it always returns the same result for same argument values and it has no side effects like modifying an argument (or global variable) or outputting to I/O. The only result of calling a pure function is the return value.

Parameters

Formal parameters

Routines typically describe computations. There are two ways that a routine can gain access to the data that it is to process: through direct access to nonlocal variables (declared elsewhere but visible in the routine) or through parameter passing. Data passed through parameters are accessed using names that are local to the routine. Routine create their own unnamed namespace. Every routine has its own Workspace. This means that every variable inside the routine is only usable during the execution of the routine (and then the variables go away).

Parameter passing is more flexible than direct access to nonlocal variables. Prrameters are special variables that are part of a routine’s signature. When a routine has parameters, you can provide it with concrete values for those parameters. The parameters in the routine header are called formal parameters. They are sometimes thought of as dummy variables because they are not variables in the usual sense: In most cases, they are bound to storage only when the routine is called, and that binding is often through some other program variables.

Parameters are declared as a list of identifiers separated by semicolon (or by a colon, but for code cleanness, the semicolon is preferred). A parameter is given a type by : typename. If after the parameter the : is not declared, but , colon to identfy another paremeter, of which both parameters are of the same type if after the second one the : and the type is placed. Then the same type parameters continue to grow with , until : is reached.

fun[] calc(el1, el2, el3: int[64]; changed: bol = true): int[64] = { result = el1 + el2 - el3 }

In routine signatures, you must declare the type of each parameter. Requiring type annotations in routine definitions is obligatory, which means the compiler almost never needs you to use them elsewhere in the code to figure out what you mean. Routine can parameter overloaded too. It makes possible to create multiple routine of the same name with different implementations. Calls to an overloaded routine will run a specific implementation of that routine appropriate to the context of the call, allowing one routine call to perform different tasks depending on context:

fun retBigger(el2, el2: int): int = { return el1 | this > el2 | el2 }
fun retBigger(el2, el2: flt): flt = { return el1 | this > el2 | el2 }

pro main: int = {
    retBigger(4, 5);                                        // calling a routine with intigers
    retBigger(4.5, .3);                                     // calling another routine with same name but floats
}

The overloading resolution algorithm determines which routine is the best match for the arguments. Example:

pro toLower(c: char): char = {                              // toLower for characters
    if (c in {'A' ... 'Z'}){
        result = chr(ord(c) + (ord('a') - ord('A')))
    } else {
        result = c
    }
}

pro toLower(s: str): str = {                                // toLower for strings
    result = newString(.len(s))
    for i in {0 ... len(s) - 1}:
        result[i] = toLower(s[i])                           // calls toLower for characters; no recursion!
}

Actual parameters

routine call statements must include the name of the routine and a list of parameters to be bound to the formal parameters of the routine. These parameters are called actual parameters. They must be distinguished from formal parameters, because the two usually have different restrictions on their forms.

Positional parameters

The correspondence between actual and formal parameters, or the binding of actual parameters to formal parameters - is done by position: The first actual parameter is bound to the first formal parameter and so forth. Such parameters are called positional parameters. This is an effective and safe method of relating actual parameters to their corresponding formal parameters, as long as the parameter lists are relatively short.

fun[] calc(el1, el2, el3: int): int = { result = el1 + el2 - el3 }

pro main: int = {
    calc(3,4,5);                                            // calling routine with positional arguments
}

Keyword parameters

When parameter lists are long, however, it is easy to make mistakes in the order of actual parameters in the list. One solution to this problem is with keyword parameters, in which the name of the formal parameter to which an actual parameter is to be bound is specified with the actual parameter in a call. The advantage of keyword parameters is that they can appear in any order in the actual parameter list.

fun[] calc(el1, el2, el3: int): int = { result = el1 + el2 - el3 }

pro main: int = {
    calc(el3 = 5, el2 = 4, el1 = 3);                        // calling routine with keywords arguments
}

Mixed parameters

Keyword and positional arguments can be used at the same time too. The only restriction with this approach is that after a keyword parameter appears in the list, all remaining parameters must be keyworded. This restriction is necessary because a position may no longer be well defined after a keyword parameter has appeared.

fun[] calc(el1, el2, el3: int, el4, el5: flt): int = { result[0] = ((el1 + el2) * el4 ) - (el3 ** el5);  }

pro main: int = {
    calc(3, 4, el5 = 2, el4 = 5, el3 = 6);                  // element $el3 needs to be keyeorded at the end because 
                                                            // its positional place is taken by keyword argument $el5
}

Default arguments

Formal parameters can have default values too. A default value is used if no actual parameter is passed to the formal parameter. The default parameter is assigned directly after the formal parameter declaration. The compiler converts the list of arguments to an array implicitly. The number of parameters needs to be known at compile time.

fun[] calc(el1, el2, el3: rise: bool = true): int = { result[0] = el1 + el2 * el3 | this | el1 + el2;  }

pro main: int = {
    calc(3,3,2);                                            // this returns 6, last positional parameter is not passed but 
                                                            // the default `true` is used from the routine declaration
    calc(3,3,2,false)                                       // this returns 12
}

Variadic routine

The use of ... as the type of argument at the end of the argument list declares the routine as variadic. This must appear as the last argument of the routine. When variadic routine is used, the default arguments can not be used at the same time.

fun[] calc(rise: bool; ints: ... int): int = { result[0] = ints[0] + ints[1] + ints[2] * ints[3] | this | ints[0] + ints[1];  }

pro main: int = {
    calc(true,3,3,3,2);                                     // this returns 81, four parmeters are passed as variadic arguments
    calc(true,3,3,2)                                        // this returns 0, as the routine multiplies with the forth varadic parameter
                                                            // and we have given only three (thus the forth is initialized as zero)
}

... is called unpack operator - just like in Golang. In the routine above, you see ..., which means pack all incoming arguments into seq[int] after the first argument. The sequence then is turned into a list at compile time.

{{% notice warn %}}

Nested procedures don’t have access to the outer scope, while nested function have but can’t change the state of it.

{{% /notice %}}

Return

The return type of the routine has to always be defined, just after the formal parameter definition. Following the general rule of FOL:

fun[] add(el1, el2: int[64]): int[64] = { result = el1 + el2 }

To make it shorter (so we don’t have to type int[64] two times), we can use a short form by omitting the return type. The compiler then will assign the returntype the same as the functions return value.

fun[] add(el1, el2: int[64]) = { result = el1 + el2 }

{{% notice info %}}

Current V1 routine summary:

  • routines declare a success type after :
  • routines may also declare a recoverable error type after /
  • report expr exits through that declared error path
  • routine call results declared with / ErrorType are not err[...] shell values
  • use check(...) or expr || fallback for those calls
  • ordinary plain-value use of / ErrorType calls is rejected
  • keep postfix ! for opt[...] and err[...] shell values

{{% /notice %}}

The implicitly declared variable result is of the same type of the return type. For it top be implicitly declared, the return type of the function shoud be always declared, and not use the short form. The variable is initialized with zero value, and if not changed during the body implementation, the same value will return (so zero).

pro main(): int = {
    fun[] add(el1, el2: int[64]): int[64] = { result = el1 + el2 }          // using the implicitly declared $result variable
    fun[] sub(el1, el2: int[64]) = { return el1 - el2 }                     // can't access the result variable, thus we use return
}

Recoverable error-aware routines use the current signature form:

fun[] read(path: str): int / str = {
    report "missing path"
}

and are handled at the call site with check(...) or || rather than shell unwrap or plain propagation.

Current intrinsic note:

  • .echo(...) is a dot-root diagnostic intrinsic
  • check(...) is a keyword intrinsic for recoverable-call inspection
  • panic(...) is a keyword intrinsic for immediate abort
  • as and cast are registry-owned but still deferred in current V1

The final expression in the function will be used as return value. For this to be used, the return type of the function needs to be defined (so the function cnat be in the short form)). ver this can be used only in one statement body.

pro main(): int = {
    fun[] add(el1, el2: int[64]): int[64] = { el1 + el2 }                   // This is tha last statement, this will serve as return
    fun[] someting(el1,el2: int): int = {
        if (condition) {

        } else {

        }
        el1 + el2                                                           // this will throw an error, cand be used in kulti statement body
    }
    fun[] add(el1, el2: int[64]) = { el1 + el2 }                            // this will throw an error, we can't use the short form of funciton in this way

Alternatively, return and report can exit a routine early from within control flow. See the recoverable-error chapter for the full current V1 contract and the shell-vs-routine distinction.

Procedures

Procedures are most common type of routines in Fol. When a procedure is “called” the program “leaves” the current section of code and begins to execute the first line inside the procedure. Thus the procedure “flow of control” is:

  • The program comes to a line of code containing a “procedure call”.
  • The program enters the procedure (starts at the first line in the procedure code).
  • All instructions inside of the procedure are executed from top to bottom.
  • The program leaves the procedure and goes back to where it started from.
  • Any data computed and RETURNED by the procedure is used in place of the procedure in the original line of code.

Procedures have side-effects, it can modifies some state variable value(s) outside its local environment, that is to say has an observable effect besides returning a value (the main effect) to the invoker of the operation. State data updated “outside” of the operation may be maintained “inside” a stateful object or a wider stateful system within which the operation is performed.

Current milestone note:

  • ordinary procedure declarations are part of V1
  • recoverable procedure errors (Result / Error) are part of V1
  • ownership-, borrowing-, and heap-move-specific calling conventions are later systems-language work

So this chapter describes the routine surface that exists now, while any pointer/borrowing examples should be read as future design rather than current compiler behavior.

Procedures can also declare a custom recoverable error type with / after the result type:

pro[] write(path: str): int / io_err = {
    report "permission denied";
}

The first : declares the result type, and / declares the routine error type.

Current V1 note:

  • a procedure declared as pro[] write(...): T / E does not produce an err[E] shell value that can be unwrapped with !
  • it produces a recoverable routine result with a success path and an error path
  • use check(...) or expr || fallback at the call site
  • keep postfix ! for opt[...] and err[...] shell values only

Passing values

In the current V1 compiler, procedure parameters are ordinary typed inputs. You pass values to them exactly as you would pass values to any other routine.

The more ambitious ownership- and borrowing-specific parameter rules described in older drafts are not part of the current V1 procedure contract yet.

Simple example:

pro[] write_line(text: str): non = {
    .echo(text)
}

pro[] main(): int = {
    var message: str = "hello"
    write_line(message)
    return 0
}

Ownership and borrowing

Ownership-, borrowing-, and pointer-specific procedure semantics are part of the later V3 systems milestone, not the current V1 compiler surface.

Older drafts used several experimental spellings for those ideas, including:

  • all-caps borrowable parameter names
  • .give_back(...)
  • double-parenthesis routine parameters

Those forms should be read as future design notes, not as current language guarantees.

The current authoritative split is:

  • ordinary procedures and functions are part of V1
  • recoverable routine errors are part of V1
  • ownership/borrowing calling conventions are part of V3

See the memory chapters and VERSIONS.md for the version boundary.

Functions

Functions compared to procedure are pure. A pure function is a function that has the following properties:

  • Its return value is the same for the same arguments (no variation with local static variables, non-local variables, mutable reference arguments or input streams from I/O devices).
  • Its evaluation has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments or I/O streams).

Thus a pure function is a computational analogue of a mathematical function. Pure functions are declared with fun[]

fun[] add(el1, el2: int[64]): int[64] = { result = el1 + el2 }

When a function has a custom recoverable error type, use / after the result type:

fun[] read(path: str): str / io_err = {
    report "file not found";
}

The first : declares the result type, and / declares the routine error type.

Current V1 note:

  • a function declared as fun[] read(...): T / E does not return an err[E] shell value
  • it returns through a recoverable routine error path
  • handle that path immediately with check(...) or expr || fallback
  • plain assignment, return, and ordinary expression use are rejected
  • postfix ! stays reserved for opt[...] and err[...] shell values {{% notice warn %}}

Functions in FOL are lazy-initialized.

{{% /notice %}}

So it is an evaluation strategy which delays the evaluation of the function until its value is needed. You call a function passing it some arguments that were expensive to calculate and then the function don’t need all of them due to some other arguments.

Consider a function that logs a message:

log.debug("Called foo() passing it " + .to_string(argument_a) + " and " + .to_string(argument_b));

The log library has various log levels like “debug”, “warning”, “error” etc. This allows you to control how much is actually logged; the above message will only be visible if the log level is set to the “debug” level. However, even when it is not shown the string will still be constructed and then discarded, which is wasteful.

{{% notice tip %}}

Since Fol supports first class functions, it allows functions to be assigned to variables, passed as arguments to other functions and returned from other functions.

{{% /notice %}}

Anonymous functoins

Anonymous function is a function definition that is not bound to an identifier. These are a form of nested function, in allowing access to variables in the scope of the containing function (non-local functions).

Staring by assigning a anonymous function to a vriable:

var f = fun (a, b: int): int = {                                        // assigning a variable to function
    return a + b
}
.echo(f(5,6))                                                           // prints 11

var f: int = (a, b: int){                                               // this is an short alternative of same variable assignmet to function
    return a + b
}

It is also possible to call a anonymous function without assigning it to a variable.

`version 1`
fun[] (a, b: int) = {                                                   `define anonymous function`
    .echo(a + b)
}(5, 6)                                                                 `calling anonymous function`


`version 2`
(a, b: int){                                                            `define anonymous function`
    .echo(a + b)
}(5, 6)                                                                 `calling anonymous function`

Closures

Functions can appear at the top level in a module as well as inside other scopes, in which case they are called nested functions. A nested function can access local variables from its enclosing scope and if it does so it becomes a closure. Any captured variables are stored in a hidden additional argument to the closure (its environment) and they are accessed by reference by both the closure and its enclosing scope (i.e. any modifications made to them are visible in both places). The closure environment may be allocated on the heap or on the stack if the compiler determines that this would be safe.

There are two types of closures:

  • anonymous
  • named

Anonymus closures automatically capture variables, while named closures need to be specified what to capture. For capture we use the [] just before the type declaration.

fun[] add(n: int): int = {
    fun added(x: int)[n]: int = {                                       // we make a named closure 
        return x + n                                                    // variable $n can be accesed because we have captured ti
    }    
    return adder()
}

var added = add(1)                                                      // assigning closure to variable
added(5)                                                                // this returns 6
fun[] add(n: int): int = {
    return fun(x: int): int = {                                         // we make a anonymous closure 
        return x + n                                                    // variable $n can be accesed from within the nested function
    }
}

Currying

Currying is converting a single function of “n” arguments into “n” functions with a “single” argument each. Given the following function:

fun f(x,y,z) = { z(x(y));}

When curried, becomes:

fun f(x) = { fun(y) = { fun(z) = { z(x(y)); } } }

And calling it woud be like:

f(x)(y)(z)

However, the more iportant thing is taht, currying is a way of constructing functions that allows partial application of a function’s arguments. What this means is that you can pass all of the arguments a function is expecting and get the result, or pass a subset of those arguments and get a function back that’s waiting for the rest of the arguments.

fun calc(x): int = {
   return fun(y): int = {
       return fun (z): int = {
           return x + y + z
       } 
   }
}

var value: int = calc(5)(6)                                             // this is okay, the function is still finished
var another int = value(8)                                              // this completes the function

var allIn: int = calc(5)(6)(8)                                          // or this as alternative

Higer-order functions

A higher-order function is a function that takes a function as an argument. This is commonly used to customize the behavior of a generically defined function, often a looping construct or recursion scheme.

They are functions which do at least one of the following:

  • takes one or more functions as arguments
  • returns a function as its result
//function as parameter
fun[] add1({fun adder(x: int): int}): int = {
    return adder(x + n)
}

//function as return
fun[] add2(): {fun (x: int): int} = {
    var f = fun (a, b: int): int = {
        return a + b
    }    
    return f
}

Generators

A generator is very similar to a function that returns an array, in that a generator has parameters, can be called, and generates a sequence of values. However, instead of building an array containing all the values and returning them all at once, a generator yields the values one at a time, which requires less memory and allows the caller to get started processing the first few values immediately. In short, a generator looks like a function but behaves like an iterator.

For a function to be a generator (thus to make the keyword yield accesable), it needs to return a type of container: arr, vec, seq, mat but not set, any.

fun someIter: vec[int] = {
    var curInt = 0;
    loop(){
        yield curInt.inc(1)
    }
}

Methods

A method is a routine associated with a receiver type. In FOL, methods can be declared as either fun or pro and called with dot syntax (value.method(...)).

Methods in FOL are procedural, not object-oriented. A method call is just sugar for calling a routine whose first explicit input is the receiver value. In other words:

tool.parse_msg(10)

should be read as the procedural call:

parse_msg(tool, 10)

There is no separate object-method runtime model implied by the syntax. typ declares data. Receiver-qualified routines are still routines.

Current parser-supported receiver declaration syntax is:

fun (parser)parse_msg(code: int): str = {
    return "ok";
}

pro (parser)update(code: int): int = {
    return code;
}

The receiver type appears in parentheses right after fun or pro, followed by the method name.

That receiver clause does not move the routine “inside” the type. It only says which type may be used in dot-call form for that routine.

Current parser-supported receiver syntax is intentionally broader than a named-only rule. At parse time, receiver positions accept named, qualified, builtin-scalar, and bracketed/composite type references. This keeps extension-style examples such as typ[ext] int: int; pro (int)print(): non = { ... } and dispatch examples on extended builtin aliases in scope for the front-end.

The dedicated parser-level rejection in this hardening phase is still for special builtin forms such as any, none, and non.

Invalid example:

fun (any)parse_msg(code: int): str = {
    return "ok";
}

This form reports: Method receiver type cannot be any, non, or none.

Method calls use standard dot syntax:

var tool: parser = parser.new()
var msg: str = tool.parse_msg(10)

This is equivalent in meaning to passing tool as the first routine argument. The dot form is only the call-site spelling.

Custom error routines also support reporting method call results when receiver-qualified signatures are available:

fun (parser)parse_err(code: int): str = {
    return "bad-input";
}

fun run(tool: parser, code: int): int / str = {
    report tool.parse_err(code)
    return 0
}

Logicals

Logicals, which are logic routines, and represent logic programming, state the routine as a set of logical relations (e.g., a grandparent is the parent of a parent of someone). Such rutines are similar to the database languages. A program is executed by an “inference engine” that answers a query by searching these relations systematically to make inferences that will answer a query.

{{% notice info %}}

One of the main goals of the development of symbolic logic hasbeen to capture the notion of logical consequence with formal, mechanical, means. If the conditions for a certain class of problems can be formalized within a suitable logic as a set of premises, and if a problem to be solved can bestated as a sentence in the logic, then a solution might be found by constructing a formal proof of the problem statement from the premises

{{% /notice %}}

Declaration

In FOL, logic programming is considered as a first class citzen with axioms (axi) as facts and logicals (log) as rules, thus resembling Prolog language. For example:

Facts

Declaring a list of facts (axioms)

var likes: axi[str, str] = { {"bob","alice"} , {"alice","bob"}, {"dan","sally"} };

Rules

Declaring a rule that states if A likes B and B likes A, they are dating

log dating(a, b: str): bol = {
    likes:[a,b] and
    likes:[b,a]
}

Declaring a rule that states if A likes B and B likes A, they are just friends

log frends(a, b): bol = {
    likes:[a,b] or
    likes:[b,a]
}

{{% notice warn %}}

Rules can have only facts and varibles within

{{% /notice %}}

Return

A logical log can return different values, but they are either of type bol, or of type container (axioms axi or vectors vec):

Lets define a axiom of parents and childrens called parents and another one of parents that can dance called dances:

var parent: axi[str, str] = { {"albert","bob"}, 
                              {"albert","betty"},
                              {"albert","bill"},
                              {"alice","bob"},
                              {"alice","betty"},
                              {"alice","bill"},
                              {"bob","carl"},
                              {"bob","tom"} };
var dances axi[str] = { "albert", "alice", "carl" };

Boolean

Here we return a boolean bol. This rule check if a parent can dance:

log can_parent_dance(a: str): bol = {
    parent:[a,_] and dances:[a]
}

can_parent_dance("albert")          // return true, "albert" is both a parent and can dance
can_parent_dance("bob")             // return false, "bob" is a parent but can't dance
can_parent_dance("carl")            // return false, "carl" is not a parent

Lets examine this: parent:[a,_] and dances:[a] this is a combintion of two facts. Here we say if a is parent of anyone (we dont care whose, that’s why we use meh symbol [a,_]) and if true, then we check if parent a (since he is a parent now, we fact-checked) can dance.

Vector

The same, we can create a vector of elements. For example, if we want to get the list of parents that dance:

log all_parents_that_dance(): vec[str] = {
    parent:[*->X,_] and
    dances:[X->Y]
    Y
}

all_parents_that_dance()            // this will return a string vector {"albert", "alice"}

Now lets analyze the body of the rule:

parent:[*->X,_] and
dances:[X->Y]
Y

Here are a combination of facts and variable assignment through silents. Silents are a single letter identifiers. If a silent constant is not declared, it gets declared and assigned in-place.

Taking a look each line: parent:[X,_] and this gets all parents ([*->X,_]),and assign them to silent X. So, X is a list of all parents. then: dances[X->Y]: this takes the list of parents X and checks each if they can dance, and filter it by assigning it to Y so [X->Y] it will have only the parents that can dance. then: Y this just returns the list Y of parents that can dance.

Relationship

If A is object and objects can be destroyed, then A can be destroyed. As a result axioms can be related or conditioned to other axioms too, much like facts.

For example: if carl is the son of bob and bob is the son of albert then carl must be the grandson of albert:

log grandparent(a: str): vec[str] = {
    parent[*->X,a]: and 
    parent[*->Y,X]:
    Y
}

Or: if bob is the son of albert and betty is the doughter of albert, then bob and betty must be syblings:

log are_syblings(a, b: str): vec[str] = {
    parent[*->X,a]: and
    parent[X->Y,b]:
    Y
}

Same with uncle relationship:

var brothers: axi[str] = { {"bob":"bill"}, {"bill","bob"} };

log has_uncle(a: str): vec[str] = {
    parent[*->Y,a]: and
    brothers[Y,*->Z]:;
    Z
}

Conditional facts

Here an example, the axioms hates will add a memeber romeo only if the relation x is satisfied:

var stabs: axi = {{"tybalt","mercutio","sword"}}
var hates: axi;

log romeHates(X: str): bol = {
    stabs[X,"mercutio",_]:
}

hates+["romeo",X] if (romeHates(X));

Anonymous logicals

Conditional facts can be added with the help of anonymous logicals/rules:

eats+[x,"cheesburger"] if (eats[x,"bread"] and eats[X,"cheese"]);

eats+[x:"cheesburger"] if (log (a: str): bol = {
    eats[a,"bread"]: and
    eats[a,"cheese"]:
}(x));

Nested facts

var line: axi = { {{4,5},{4,8}}, {{8,5},{4,5}} }

log vertical(line: axi): bol = {
    line[*->A,*->B]: and 
    A[*->X,Y*->]: and
    B[X,*->Y2]:
}

log horizontal(line: axi): bol = {
    line[*->A,*->B]: and 
    A[*->X,*->Y]: and
    B[*->X2,Y]:
}

assert(vertical(line.at(0))
assert(horizontal(line.at(1))

Filtering

Another example of filtering a more complex axion:

var class: axi;

class.add({"cs340","spring",{"tue","thur"},{12,13},"john","coor_5"})
class.add({"cs340",winter,{"wed","fri"},{15,16},"bruce","coor_3"})

log instructor(class: str): vec[str] = {
    class[class,_,[_,"fri"],_,*->X,_]
    X
}

Construct Types

This section covers named type construction beyond the built-in type families.

The main construct surfaces are:

  • aliases: ali
  • record-like definitions
  • entry-like definitions

These chapters describe how named data models are introduced, how members are declared, and how construct values are initialized and accessed.

Aliases

An alias declaration binds an identifier to an existing type. All the properties of the existing type are bound to the alias too.

There are two type of aliasing:

  • aliasing
  • extending

Aliasing

typ[ali] I5: arr[int, 5];

So now the in the code, instead of writing arr[int, 5] we could use I5:

~var[exp] fiveIntigers: I5 = { 0, 1, 2, 3, 4, 5 }

Another example is creating a rgb type that can have numbers only form 0 to 255:

typ[ali] rgb: int[8][.range(255)] ;                        // we create a type that holds only number from 0 to 255
typ[ali] rgbSet: set[rgb, rgb, rgb];                       // then we create a type holding the `rgb` type

Alias declaration are created because they can simplify using them multiple times, their identifier (their name) may be expressive in other contexts, and-most importantly-so that you can define receiver-qualified routines on a named type surface. Anonymous types still need a named alias or extension surface first, while built-in or foreign types can be extended explicitly through typ[ext].

Attaching methods does not make a type into an object. It simply gives a routine a receiver-qualified dot-call spelling. You could still think of the operation procedurally as a routine that takes the value as its first input.

Current milestone note:

  • aliasing and extension over current V1 built-in and declared types are part of the present language surface
  • true foreign-type interop remains later work
  • C ABI and Rust interop belong to the planned V4 milestone, not the current compiler contract

Extending

Extensions add new functionality to an existing constructs. This includes the ability to extend types for which you do not have access to the original source code (known as retroactive modeling).

typ[ext] type: type;

For example, adding a print function to the default integer type int:

typ[ext] int: int;

pro (int)print(): non = {
    .echo(self)
}

pro main: int = {
    5.print()                   // method print on int
}

Or turning a string str into a vector of characters:

typ[ext] str: str;

fun (str)to_array(): vec[chr] = {
    loop(x in self){
        yield x; 
    }
}


pro main(): int = {
    var characters: vec[chr] = "a random str".to_array();

    .echo(characters)           // will print: {"a"," ","r","a","n","d","o","m"," ","s","t","r"}
}

Structs

Structs are the way to declare new type of data. A struct binds an identifier, the type name, to a type.

A struct definition creates a new, distinct type and are few of them in FOL:

  • records
  • entries

Definition

Records

A record is an aggregate of data elements in which the individual elements are identified by names and types and accessed through offsets from the beginning of the structure. There is frequently a need in programs to model a collection of data in which the individual elements are not of the same type or size. For example, information about a college student might include name, student number, grade point average, and so forth. A data type for such a collection might use a character string for the name, an integer for the student number, a floating- point for the grade point average, and so forth. Records are designed for this kind of need.

It may appear that records and heterogeneous set are the same, but that is not the case. The elements of a heterogeneous set[] are all references to data values that may reside in scattered locations. The elements of a record are of potentially different sizes and reside in adjacent memory locations. Records are primarily data layouts.

typ user: rec = {
    var username: str;
    var email: str;
    var sign_in_count: int[64];
    var active: bol;
};

Records are data, not classes

typ ...: rec = { ... } declares a data type. FOL does not treat records as classes with hidden object state or class-owned method bodies. If a record has operations associated with it, those operations are still declared as ordinary receiver-qualified routines outside the record body.

typ computer: rec = {
    brand: str;
    memory: int
}

fun (computer)get_type(): str = {
    return self.brand
}

var laptop: computer = { brand = "acme", memory = 16 }
.echo(laptop.get_type())

The call laptop.get_type() is procedural sugar for calling the receiver routine with laptop as its first input.

Current V1 backend/runtime note:

  • backends may emit records and entries as plain target-language structs/enums
  • that does not change the language model: they are still data plus ordinary receiver-qualified routines
  • when runtime-visible formatting is needed, generated backends should preserve the fol-runtime aggregate formatting contract instead of inventing a backend-specific display shape

Entries

Is an a group of constants (identified with ent) consisting of a set of named values called elements.

typ color: ent = {
    var BLUE: str = "#0037cd" 
    var RED str = "#ff0000" 
    var BLACK str = "#000000" 
    var WHITE str = "#ffffff" 
};

if( something == color.BLUE ) { doathing } else { donothing }

Entries as enums

Unums represent enumerated data. An enumeration type (or enum type) is a value type defined by a set of named constants of the underlying integral numeric type.

typ aUnion: ent = {
    var BLUE, RED, BLACK, WHITE: int[8] = {..3}
}

Initializaion

To use a record after we’ve defined it, we create an instance of that record by specifying concrete values for each of the fields. We create an instance by stating the name of the record and then add curly brackets containing key: value pairs, where the keys are the names of the fields and the values are the data we want to store in those fields. We don’t have to specify the fields in the same order in which we declared them in the record. In other words, the record definition is like a general template for the type, and instances fill in that template with particular data to create values of the type.

@var user1: user = {
    email = "someone@example.com",
    username = "someusername123",
    active = true,
    sign_in_count = 1,
};

Named initialization:

@var[mut] user1: user = { email = "someone@example.com", username = "someusername123", active = true, sign_in_count = 1 }

Ordered initialization

@var[mut] user1: user = { "someone@example.com", "someusername123", true, 1 }

Accessing

To get a specific value from a record, we can use dot notation or the access brackets. If we wanted just this user’s email address, we could use user1.email or user1[email] wherever we wanted to use this value. If the instance is mutable, we can change a value by assigning into a particular field. Note that the entire instance must be mutable; FOL doesn’t allow us to mark only certain fields as mutable.

@var[mut] user1: user = {
    email = "someone@example.com",
    username = "someusername123",
    active = true,
    sign_in_count = 1,
};

user1.email = "new.mail@example.com"
user1[username] = "anotherusername"

Returning

As with any expression, we can construct a new instance of the record as the last expression in the function body to implicitly return that new instance. As specified in function return, the final expression in the function will be used as return value. For this to be used, the return type of the function needs to be defined (here is defined as user) and this can be used only in one statement body. Here we have declared only one variable user1 and that itslef spanc into multi rows:

pro buildUser(email, username: str): user = { user1: user = {
    email = "someone@example.com",
    username = "someusername123",
    active = true,
    sign_in_count = 1,
} }

Nesting

Records can be nested by creating a record type using other record types as the type for the fields of record. Nesting one record within another can be a useful way to model more complex structures:

var empl1: employee = {
    FirstName = "Mark",
    LastName =  "Jones",
    Email =     "mark@gmail.com",
    Age =       25,
    MonthlySalary = {
        Basic = 15000.00,
        Bonus = {
            HTA =    2100.00,
            RA =   5000.00,
        },
    },
}

Defauling

Records can have default values in their fields too.

typ user: rec = {
    var username: str;
    var email: str;
    var sign_in_count: int[64] = 1;
    var active: bol = true;
};

This makes possible to enforce some fields (empty ones), and leave the defaults untouched:

@var[mut] user1: user = { email = "someone@example.com", username = "someusername123" }

Limiting

We can also restrict the values (with ranges) assigned to each field:

typ rgb: rec[] = {
    var r: int[8][.range(255)];
    var g: int[8][.range(255)];
    var b: int[8][.range(255)];
}

var mint: rgb = { 153, 255, 187 }

This of course can be achieve just with variable types and aliased types and sets too, but we would need to create two types:

typ rgb: set[int[8][.range(255)], int[8][.range(255)], int[8][.range(255)]];

var mint: rgb = { 153, 255, 187 }

Methods

A record may have receiver-qualified routines associated with it. This does not turn the record into an object-oriented type. It only means a routine may use dot-call syntax when its first input is a value of that record type. To create such a routine for a record, declare the receiver type on the routine itself:

fun (recieverRecord)someFunction(): str = { self.somestring; };

After declaring the record receiver, the routine body may refer to that input through self. A receiver is simply the explicit first input that enables dot-call syntax.

typ user: rec = {
    var username: str;
    var email: str;
    var sign_in_count: int[64];
    var active: bol;
};

fun (user)getName(): str = { result = self.username; };

Receiver-qualified routines have one main ergonomic benefit over plain routine calls: they let the call site read value.method(...). In the same package, multiple routines may still share the same method name if the receiver types are different.

Each record value can therefore use the dot form, but the underlying model remains procedural.

var[mut] user1: user = { email = "someone@example.com", username = "someusername123", active = true, sign_in_count = 1 }

.echo(user1.getName());

Standards

This chapter describes V2 contract/conformance design rather than current V1 compiler behavior.

Current milestone note:

  • standards are not part of the implemented V1 typechecker
  • blueprints and extensions are not part of the implemented V1 typechecker
  • examples here are semantic design examples for a later milestone

Satndard

A standard is an established norm or requirement for a repeatable technical task. It is usually a formal declaration that establishes uniform technical criteria, methods, processes, and practices.

S, what is a to be considered a standard:

  • A standard specification is an explicit set of requirements for an item, object or service. It is often used to formalize the technical aspects of a procurement agreement or contract.
  • A standard test method describes a definitive procedure that produces a test result. It may involve making a careful personal observation or conducting a highly technical measurement.
  • A standard procedure gives a set of instructions for performing operations or functions.
  • A standard guide is general information or options that do not require a specific course of action.
  • A standard definition is formally established terminology.

In FOL, standards are named collections of receiver-qualified routine signatures and/or required data, created with std. They are not class hierarchies. They are procedural/data contracts:

std geometry: pro = {
    fun area(): flt[64];
    fun perim(): flt[64];
};

There are three types of standards,

  • protocol pro[] that enforce just function implementation
  • blueprint blu[] that enforces just data implementation
  • extended ext[], that enforces function and data:
std geometry: pro = {
    fun area(): flt[64];
    fun perim(): flt[64];
};


std geometry: blu = {
    var color: rgb; 
    var size: int;
};

std geometry: ext = {
    fun area(): flt[64];
    fun perim(): flt[64];
    var color: rgb;
    var size: int;
};

Contract

A contract is a legally binding agreement that recognises and governs the rights and duties of the parties to the agreement. A contract is enforceable because it meets the requirements and approval of an higher authority. An agreement typically involves a written declaration given in exchange for something of value that binds the maker to do. Its an specific act which gives to the person to whom the declaration is made the right to expect and enforce performance. In the event of breach of contract, the higher authority will refrain the contract from acting.

In fol contracts are used to bind a type to a standard. If a type declares to use a standard, it is the job of the contract (compiler internally) to see the standard full-filled.

std geo: pro = {
    fun area(): flt[64];
    fun perim(): flt[64];
};

std rect(geo): rec[] = {                                             // this type makes a contract to use the geometry standard
    width: int[64];
    heigh: int[64];
}

Now we can make rect records, but we have to respect the contract. If we don’t implement the required receiver-qualified routines, the compiler should reject uses that require that contract.

var aRectangle: rect = { width = 5, heigh = 6 }                      // this throws an error, we haven't fullfill the ocntract

To do so, we need first to create the required rect receiver-qualified routines, then instantiate a record value:

fun (rect)area(): flt[64] = { result = self.width + self.heigh }
fun (rect)perim(): flt[64] = { result = 2 * self.width + 2 * self.heigh }

var aRectangle: rect = { width = 5, heigh = 6 }                     // this from here on will work

The benefit of standards is that a routine parameter may require a standard contract, and then any type that satisfies that contract may be used there:

std geo: pro = {
    fun area(): flt[64];
    fun perim(): flt[64];
};

typ rect(geo): rec[] = {                                            // this type makes a contract to use the geometry standard
    width: int[64]; 
    heigh: int[64]; 
}
fun (rect)area(): flt[64] = { result = self.width + self.heigh }
fun (rect)perim(): flt[64] = { result = 2 * self.width + 2 * self.heigh }

typ circle(geo): rec[] = {                                          // another type makes a contract to use the geometry standard
    radius: int[64]; 
}
fun (circle)area(): flt[64] = { result = math::const.pi * self.radius ** 2 }
fun (circle)perim(): flt[64] = { result = 2 * math::const.pi * self.radius}

typ square: rec[] = {                                               // this type does not make contract with `geo`
    heigh: int[64] 
}

pro measure( shape: geo) { .echo(shape.area() + "m2") }        // a siple method to print the standard's area

// instantiate two record values
var aRectangle: rect = { width = 5, heigh = 6 }                      // creating a new rectangle
var aCircle: circle = { radius = 5 }                                 // creating a new rectangle
var aSquare: square = { heigh = 6 }                                  // creating a new square


// to call the measure function that rpints the surface
measure(aRectangle)                                                  // this prints: 30m2
measure(aSquare)                                                     // this throws error, square does not satisfy the contract
measure(aCircle)                                                     // this prints: 78m2

Generics

This chapter describes later generic-language design rather than current V1 compiler behavior.

Current milestone note:

  • generic routines are not part of the implemented V1 typechecker
  • generic types are not part of the implemented V1 typechecker
  • examples here should be read as future V2 design

Types

Generic functions - lifting

The generic programming process focuses on finding commonality among similar implementations of the same algorithm, then providing suitable abstractions so that a single, generic algorithm can cover many concrete implementations. This process, called lifting, is repeated until the generic algorithm has reached a suitable level of abstraction, where it provides maximal reusability while still yielding efficient, concrete implementations. The abstractions themselves are expressed as requirements on the parameters to the generic algorithm.

pro max[T: gen](a, b: T): T = {
	result =  a | a < b | b;
};
fun biggerFloat(a, b: flt[32]): flt[32] = { max(a, b) }
fun biggerInteger(a, b: int[64]): int[64] = { max(a, b) }

Generic types - concepts

Once many algorithms within a given problem domain have been lifted, we start to see patterns among the requirements. It is common for the same set of requirements to be required by several different algorithms. When this occurs, each set of requirements is bundled into a concept. A concept contains a set of requirements that describe a family of abstractions, typically data types. Examples of concepts include Input Iterator, Graph, and Equality Comparable. When the generic programming process is carefully followed, the concepts that emerge tend to describe the abstractions within the problem domain in some logical way.

typ container[T: gen, N: int](): obj = {
	var anarray: arr[T,N];
	+fun getsize(): num = { result = N; }
};
var aContainer: container[int, 5] = { anarray = {zero, one, two, three, four}; };

Dispach

Static dispatch (or early binding) happens when compiler knows at compile time which function body will be executed when I call a method. In contrast, dynamic dispatch (or run-time dispatch or virtual method call or late binding) happens when compiler defers that decision to run time. This runtime dispatch requires either an indirect call through a function pointer, or a name-based method lookup.

std foo: pro = { fun bar(); }

typ[ext] int, str: int, str;

fun (int)bar() = {  }
fun (str)bar() = {  }

pro callBar(T: foo)(value: T) = { value.bar() }             // dispatch with generics
pro barCall( value: foo ) = { value.bar() }                 // dispatch with standards

pro main: int = {
    callBar(2);
    callBar("go");

    barCall(2);
    barCall("go")
}

Modules And Source Layout

This section defines how FOL source is organized across files, folders, packages, imports, and named module-like declarations.

It covers:

  • imports through use
  • namespaces and package layout
  • block-like named definitions
  • test-oriented module surfaces

At a high level:

  • files in the same package contribute to the same package surface
  • namespaces are expressed through folder structure and :: access
  • imported sources are classified by source kind such as loc, pkg, and std

Imports

An import declaration states that the source file containing the declaration depends on functionality of the imported package and enables access to exported identifiers of that package.

Syntax to import a library is:

use alias: source_kind = { source }

Current source kinds are:

  • loc for local directory imports
  • std for standard-library directory imports
  • pkg for installed external packages

What use imports

use works against the source-layout model:

  • the folder root is the package
  • subfolders are namespaces inside that package
  • a file is only a source file, not a separate module by itself

So a use target is normally:

  • a whole package
  • or one namespace inside that package

use does not mean “import another file from the same folder”. Files in the same package already share package scope. Imports are for reaching another package or another namespace boundary.

Also note:

  • use brings in exported functionality
  • hid declarations remain file-only even inside the same package
  • importing a package does not erase the original file boundary rules
  • use is for consuming functionality, not for defining package dependencies

Import kinds

loc

loc is the simplest import kind:

  • it points to a local directory
  • that directory is scanned as a FOL package / namespace tree
  • no package.yaml is required
  • no build.fol is required

This makes loc useful for local workspace code, experiments, and monorepo-style sharing.

std

std works like loc, except the directory is resolved from the toolchain’s standard-library root.

So:

  • std imports are directory-backed
  • they are owned by the FOL toolchain
  • they do not need user-managed package metadata in source code

pkg

pkg is for formal external packages.

Unlike loc and std, a pkg import does not just point at an arbitrary source directory. It points at an installed package root that must define its identity and build surface explicitly. The package layer discovers that root first, and ordinary name resolution happens only after the package has been prepared.

For a pkg package root:

  • package.yaml is required
  • build.fol is required
  • package.yaml stores metadata only
  • build.fol declares dependencies and exports

Package Metadata And Build Files

Formal packages use two files at the root:

  • package.yaml
  • build.fol

package.yaml

package.yaml is metadata only. It is intentionally not a normal .fol source file.

Typical metadata belongs here:

  • package name
  • version
  • package kind
  • human-oriented description/license/author data

What does not belong here:

  • use
  • dependency edges
  • export wiring
  • build logic

build.fol

build.fol is the package build entry file.

This file is responsible for:

  • declaring package build logic
  • declaring artifacts, steps, and generated outputs through the build API
  • becoming the canonical package entrypoint for fol code build/run/test/check

build.fol is still an ordinary FOL file. It is parsed with the same front-end as other .fol sources. The difference is that the package layer evaluates one canonical build routine inside it.

Today that means:

  • fol code build/run/test/check starts from build.fol
  • the canonical entry is pro[] build(graph: Graph): non
  • old def root: loc = ... and def build(...) forms are not the build model

So:

  • def is still a general FOL declaration form
  • build.fol is not a separate mini-language
  • build.fol uses an ordinary routine entrypoint, like Zig’s build.zig

That means:

  • ordinary source .fol files use use to consume packages/namespaces
  • build.fol uses pro[] build(graph: Graph): non to mutate the build graph

So use and the build routine serve different jobs:

  • use = consume functionality
  • pro[] build(...) in build.fol = define package/build surface

System libraries

This is how including other libraries works, for example include fmt module from standard library:

use fmt: std = {"fmt"};

pro main: ini = {
    fmt::log.warn("Last warning!...")
}

To use only the log namespace of fmt module:

use log: std = {"fmt/log"};

pro[] main: int = {
    log.warn("Last warning!...")
}

But let’s say you only wanna use ONLY the warn functionality of log namespace from fmt module:

use warn: std = {"fmt/log"};

pro[] main: int = {
    warn("Last warning!...")
}

Local libraries

To include a local package or namespace, point loc at the directory:

use bend: loc = {"../folder/bender"};

Then to acces only a namespace:

use space: loc = {"../folder/bender/space"};

That second form is namespace import, not “single file import”. If space contains multiple .fol files in the same folder, they still belong to the same imported namespace.

loc does not require package.yaml or build.fol. But if the target directory already defines build.fol at its root, that directory is treated as a formal package root and should be imported through pkg, not loc.

External packages

External packages are imported through pkg:

use space: pkg = {"space"};

pkg imports are different from loc and std:

  • the imported root is an installed package root
  • that root must contain package.yaml
  • that root must contain build.fol
  • package.yaml provides metadata only
  • build.fol is the package build entry file and currently defines dependencies, exports, and root declarations that package loading depends on
  • raw transport URLs do not appear in source code; package acquisition and installed-package preparation are separate from ordinary source resolution

Declarations

Each .fol file inside a folder is part of the same package.

The important part is that files are connected, but not merged:

  • every physical .fol file is still its own source file
  • files in the same folder share one package scope
  • declarations are order independent across those files
  • a declaration started in one file can not continue into the next file

There is no need to import sibling files from the same package. They already belong to the same package scope.

Package, Namespace, And File Scope

It helps to think about source layout in three layers:

  • package scope: all .fol files directly inside the package folder
  • namespace scope: declarations inside subfolders of that package
  • file scope: declarations marked hid, which stay visible only inside their own file

In short:

  • same folder = same package
  • subfolder = nested namespace
  • hid = file only
  • files are never imported directly as standalone modules

Package scope

Files that live directly in the package root share one package scope:

root/
    math/
        add.fol
        sub.fol

Both add.fol and sub.fol belong to package math, so declarations from one file may be used by the other without importing the sibling file.

Namespace scope

Subfolders do not create a new package by themselves. They create nested namespaces inside the same package:

root/
    math/
        add.fol
        stats/
            mean.fol

Here:

  • add.fol is in package namespace math
  • mean.fol is in package namespace math::stats

Code may reach namespace members either by:

  • direct use
  • or qualified access with ::

File scope

Sometimes a declaration should stay inside one file even though the package is shared. That is what hid is for.

// file1.fol
var[hid] cache_key: str = "local"
// file2.fol
pro[] main: int = {
    .echo(cache_key)      // error: hidden declarations are file-only
}

Namespaces

A namespace can be defined in a subfolder of the main folder, and namespaces can be nested.

To acces the namespace there are two ways:

  • direct import with use
  • or code access with ::

Direct import

use aNS: loc = { "home/folder/printing/logg" }

pro[] main: int = {
    logg.warn("something")
}

Code access

use aNS: loc = { "home/folder/printing" }

pro[] main: int = {
    printing::logg.warn("something")
}

Mental model

For source layout, the mental model is:

  • one folder root gives one package
  • each file in that folder is a real source file in that package
  • subfolders extend the namespace path
  • use imports packages or namespaces
  • hid keeps a declaration private to one file
  • loc imports a local directory tree without package metadata
  • std imports a toolchain-owned directory tree
  • pkg imports a formal external package defined by package.yaml + build.fol

This means FOL is not “one file = one module”. The package is the folder; the file is a source unit inside that package.

Package Roots

When a directory is treated as a package root, the exact contract depends on the import kind:

  • loc: plain local directory import, no package metadata required
  • std: toolchain standard-library directory import
  • pkg: installed external package import with explicit root files

For pkg, the root is not just “a folder containing .fol files”. It is a formal package root with:

  • package.yaml for metadata
  • build.fol as the package build entry file

This keeps the language model clean:

  • source files use other namespaces/packages
  • package build execution starts from pro[] build(graph: Graph): non in build.fol
  • package metadata lives in package.yaml
  • package loading happens before ordinary name resolution

build.fol itself is still ordinary FOL syntax. It is not a separate mini-language. The package layer simply gives package/build meaning to the canonical build routine there.

Blocks

Block statement is used for scopes where members get destroyed when scope is finished. And there are two ways to define a block:

  • unnamed blocks and
  • named blocks

Unnamed blocks

Are simply scopes, that may or may not return value, and are represented as: { //block }, with . before the brackets for return types and _ for non return types:

pro[] main: int = {
    _{
        .echo("simple type block")
    }
    .echo(.{ return "return type block" })
}

Named blocks

Blocks can be used as labels too, when we want to unconditionally jump to a specific part of the code.

pro[] main: int = {
    def block: blk[] = {            // $block A named block that can be referenced
        // implementation
    }
    def mark: blk[]                 // $mark A named block that can be referenced, usually for "jump" statements
}

Tests

Blocks defined with type tst, have access to the module (or namespace) defined in tst["name", access].

def test1: tst["sometest", shko] = {}
def "some unit testing": tst[shko] = {}

Error Handling

FOL does not center error handling around exceptions.

This section distinguishes two broad error categories:

  • breaking errors: unrecoverable failures, typically associated with panic
  • recoverable errors: errors that can be propagated or handled, typically associated with report

The detailed chapters explain:

  • how each category behaves
  • how routines expose recoverable error types
  • how error-aware forms interact with control flow and pipes
  • how the current compiler reports syntax, package, and resolver failures

Current compiler diagnostics

The current compiler surface guarantees the following reporting behaviors across the active parser/package/resolver/typecheck/lower/backend chain:

  • every diagnostic carries a stable code shown in brackets (e.g. error[R1003]:)
  • all failures keep exact primary file:line:column locations
  • human-readable diagnostics render source snippets and underline the primary span
  • related sites such as duplicate declarations or ambiguity candidates appear as secondary labels
  • JSON diagnostics preserve the same structured information with labels, notes, helps, and stable producer-owned diagnostic codes
  • the parser recovers after failed declarations instead of cascading errors
  • duplicate diagnostics on the same line are suppressed, with a hard cap at 50
  • LSP diagnostics are deduplicated by line and code before reaching the editor

The exact wording of messages is still implementation detail, but the current compiler contract is that locations, codes, and structured diagnostic shape are stable enough to build tests and tooling around them.

For the detailed compiler-facing reporting model, see Compiler Diagnostics.

Breaking errors

panic keyword allows a program to terminate immediately and provide feedback to the caller of the program. It should be used when a program reaches an unrecoverable state. This most commonly occurs when a bug of some kind has been detected and it’s not clear to the programmer how to handle the error.

pro main(): int = {
    panic "Hello";
    .echo("End of main");                                                       //unreachable statement
}

In the above example, the program will terminate immediately when it encounters the panic keyword. Output:

main.fol:3
routine 'main' panicked at 'Hello'
-------

Trying to acces an out of bound element of array:

pro main(): int = {
    var a: arr[int, 3] = [10,20,30];
    a[10];                                                                      //invokes a panic since index 10 cannot be reached
}

Output:

main.fol:4
routine 'main' panicked at 'index out of bounds: the len is 3 but the index is 10'
-------
a[10];
   ^-------- index out of bounds: the len is 3 but the index is 10

A program can invoke panic if business rules are violated, for example: if the value assigned to the variable is odd it throws an error:

pro main(): int = {
   var no = 13; 
   //try with odd and even
   if (no % 2 == 0) {
      .echo("Thank you , number is even");
   } else {
      panic "NOT_AN_EVEN"; 
   }
   .echo("End of main");
}

Output:

main.fol:9
routine 'main' panicked at 'NOT_AN_EVEN'
-------

Recoverable errors

Recoverable errors are part of the current V1 language contract, but they are split into two different surfaces:

  • T / E for routine-call handling
  • err[...] for normal storable values

Those are intentionally not the same thing.

T / E is immediate call-site handling

Use / ErrorType after the success type:

fun read_code(path: str): int / str = {
    when(path == "") {
        case(true) { report "missing path" }
        * { return 7 }
    }
}

This means:

  • success yields int
  • report expr exits through the routine error path with str
  • the call result is not a storable plain value

report expr must match the declared error type:

fun read_code(path: str): int / str = {
    report "missing path"
}

The following is invalid because the reported value is the wrong type:

fun read_code(path: str): int / str = {
    report 1
}

No plain propagation

In current V1, / ErrorType routine results do not flow through ordinary expressions.

These are rejected:

var value = read_code(path)
return read_code(path)
consume(read_code(path))
read_code(path) + 1

/ ErrorType must be handled immediately at the call site.

check(...)

check(expr) asks whether a / ErrorType routine call failed.

It returns bol.

fun main(path: str): bol = {
    return check(read_code(path))
}

check(...) works on recoverable routine calls, not on err[...] shell values.

||

expr || fallback handles a / ErrorType routine call immediately.

Rules:

  • if expr succeeds, use its success value
  • if expr fails, evaluate fallback
  • fallback may:
    • provide a default value
    • report
    • panic

Examples:

fun with_default(path: str): int = {
    return read_code(path) || 0
}

fun with_context(path: str): int / str = {
    return read_code(path) || report "read failed"
}

fun must_succeed(path: str): int = {
    return read_code(path) || panic "read failed"
}

err[...] is the storable error form

err[...] is a normal value type.

You may store it, pass it, return it, and unwrap it later:

ali Failure: err[str]

fun keep(value: Failure): Failure = {
    return value
}

fun unwrap(value: Failure): str = {
    return value!
}

This is different from:

fun read_code(path: str): int / str = { ... }

A call to read_code(...) is not an err[str] value. If you need a storable error container, use err[...]. If you use / ErrorType, handle it with check(...) or ||.

Current V1 boundary

The current compiler supports:

  • declared routine error types with /
  • report expr
  • check(expr)
  • expr || fallback
  • err[...] shell/value behavior

The current compiler rejects:

  • plain assignment of / ErrorType call results
  • direct returns of / ErrorType call results
  • implicit conversion from / ErrorType into err[...]
  • postfix ! on / ErrorType routine calls

For backend work:

  • / ErrorType routine calls lower through the recoverable runtime ABI
  • err[...] remains a separate shell/value runtime type

Those two categories are intentionally not merged.

Compiler Diagnostics

This chapter is about compiler reporting, not about language-level panic or report semantics.

In other words:

  • panic and report describe what your program does
  • diagnostics describe what the compiler tells you when it cannot continue or when it wants to surface important information

Why this chapter exists

FOL now has a real compiler pipeline:

  • fol-stream
  • fol-lexer
  • fol-parser
  • fol-package
  • fol-resolver
  • fol-typecheck
  • fol-lower
  • fol-runtime
  • fol-backend
  • fol-diagnostics

That means errors are no longer just loose strings printed from one place. Compiler failures now move through a shared diagnostics layer with stable structure.

This matters for three reasons:

  • humans need readable compiler output
  • tests need stable enough structure to assert against
  • future tools need a machine-readable format that is not just a copy of the human renderer

What a diagnostic contains

At the current compiler stage, a diagnostic can carry:

  • severity
  • main message
  • a stable diagnostic code (e.g. P1001, R1003, T1003)
  • one primary location
  • zero or more related locations
  • notes
  • helps
  • suggestions

The current compiler mostly emits Error, but the reporting layer also supports Warning and Info.

Diagnostic codes

Every diagnostic carries a stable producer-owned code. The code identifies the error family and specific failure without relying on message text.

Current code families:

PrefixProducerExamples
P1xxxparserP1001 syntax, P1002 file root
K1xxxpackage loadingK1001 metadata, K1002 layout
R1xxxresolverR1003 unresolved, R1005 ambiguous
T1xxxtype checkerT1003 type mismatch
L1xxxloweringL1001 unsupported surface
F1xxxfrontendF1001 invalid input, F1002 workspace not found
K11xxbuild evaluatorK1101 build failure

Codes are structurally assigned. The parser carries an explicit ParseErrorKind field on each error rather than deriving the code from message text. This means message wording can change without breaking code identity.

Human output shows codes in brackets:

error[R1003]: could not resolve name 'answer'

JSON output includes the code as a top-level field:

{ "code": "R1003", "message": "could not resolve name 'answer'" }

Primary location

The most important part of a diagnostic is its primary location.

That location is currently expressed as:

  • file
  • line
  • column
  • optional span length

This is what allows the compiler to point at the exact token or source span that caused the failure.

Every diagnostic now carries a real location. Parser errors that previously lacked locations (safety-bound overflows, constraint violations like duplicate parameter names) now extract file/line/column from the current token position.

Typical examples:

  • a parser error at the token that made a declaration invalid
  • a package-loading error at the control file or package root that failed
  • a resolver error at the unresolved identifier or ambiguous reference
  • a typecheck error at the expression or declaration whose types do not match
  • a lowering error at the typed surface that has no current V1 lowering rule

Some compiler failures are not well described by one location alone.

For example:

  • duplicate declarations
  • ambiguous references
  • duplicate package metadata fields

In those cases the compiler keeps one primary site and can also attach related sites as secondary labels.

That allows the compiler to say things like:

  • this declaration conflicts with an earlier declaration
  • this name could refer to either of these two candidates
  • this metadata field was already defined elsewhere

Notes, helps, and suggestions

FOL diagnostics separate extra guidance into different buckets instead of forcing everything into one long message.

The current contract is:

  • the main message says what went wrong
  • notes add technical context
  • helps add actionable guidance
  • suggestions describe a possible replacement or next step when the producer can express one

This split matters because tooling and tests can preserve structure instead of trying to parse intent back out of prose.

Error recovery

The parser implements error recovery so that a single syntax mistake does not cascade into dozens of unrelated errors.

When a declaration parse fails, the parser calls sync_to_next_declaration to skip forward to the next declaration-start keyword (fun, var, def, typ, pro, log, seg, ali, imp, lab, con, use) or EOF. This means:

  • fun[exp] emit(...) = { ... } produces exactly 1 error, not 20+
  • two broken declarations separated by a good one produce 2 errors, and the good declaration still parses correctly

Cascade suppression

Even with parser recovery, edge cases in any pipeline stage can cascade.

The diagnostic report layer applies two safety nets:

  • same-code, same-line dedup: if the most recently added diagnostic has the same code and same line as a new one, the new one is suppressed
  • hard cap: the report accepts at most 50 diagnostics total and shows “(output truncated)” when the limit is reached

These limits prevent walls of identical errors without hiding genuinely distinct failures.

Human-readable diagnostics

By default the CLI prints human-readable diagnostics.

The current renderer is designed around:

  • a severity prefix with a diagnostic code bracket (e.g. error[R1003]:)
  • an arrow line with file:line:column
  • a source snippet when the file and line can be loaded
  • an underline for the primary span
  • note-style summaries for related labels
  • note/help lines after the main snippet

Illustrative shape:

error[R1003]: could not resolve name 'answer'
  --> app/main.fol:3:12
    |
  3 |     return answer
    |            ^^^^^^ unresolved name
  note: no visible declaration with that name was found in the current scope chain
  help: check imports or declare the name before use

Messages are clean human-readable text. The compiler does not prepend internal kind labels like ResolverUnresolvedName: to messages. The diagnostic code in brackets is the stable identifier.

Source fallbacks

Sometimes the compiler knows the location but cannot render the source line itself.

Examples:

  • the file is no longer readable
  • the file path is missing
  • the requested line is outside the current file contents

In those cases the compiler still keeps the location and falls back cleanly instead of crashing the renderer.

So the priority order is:

  1. exact location
  2. source snippet when available
  3. explicit fallback note when the snippet cannot be shown

JSON diagnostics

When the CLI is invoked with --json, diagnostics are emitted as structured JSON instead of human-readable text.

This output is meant for scripts, tests, editor tooling, and future integration layers.

Important rule:

  • JSON is not a lossy summary of human output

Instead, both human and JSON outputs are generated from the same structured diagnostic model.

The editor/LSP layer should follow that same rule too: editor diagnostics should be adapted from the shared structured diagnostic model rather than rebuilt from free-form strings.

That means JSON can preserve:

  • severity
  • code
  • message
  • primary location
  • related labels
  • notes
  • helps
  • suggestions

Illustrative shape:

{
  "severity": "Error",
  "code": "R1003",
  "message": "could not resolve name 'answer'",
  "location": {
    "file": "app/main.fol",
    "line": 3,
    "column": 12,
    "length": 6
  },
  "labels": [
    {
      "kind": "Primary",
      "message": "unresolved name",
      "location": {
        "file": "app/main.fol",
        "line": 3,
        "column": 12,
        "length": 6
      }
    }
  ],
  "notes": [
    "no visible declaration with that name was found in the current scope chain"
  ],
  "helps": [
    "check imports or declare the name before use"
  ],
  "suggestions": []
}

Again, the exact payload can evolve, but the important guarantee is that the structured fields are first-class rather than reverse-engineered from text.

Which compiler phases currently participate

At head, the main producers that lower into the shared diagnostics layer are:

  • parser
  • package loading
  • resolver
  • type checking
  • lowering
  • build evaluator
  • backend
  • frontend (workspace discovery and input validation)

That means diagnostics are already strong across:

  • syntax errors (with error recovery so cascades are contained)
  • package metadata and package-root errors
  • import-loading failures
  • unresolved names
  • duplicate names
  • ambiguous references
  • type mismatches and unsupported semantic surfaces inside V1
  • unsupported lowered V1 surfaces before target emission
  • backend emission and build failures when lowered V1 workspaces cannot become runnable artifacts
  • build graph evaluation failures

This is the important boundary for the current compiler stage:

  • the compiler can now parse, resolve, type-check, and lower the supported V1 subset
  • diagnostics already cover failures from each of those stages plus backend emission/build failures
  • the project now does promise a finished first backend for the current V1 subset, while later targets, optimizations, C ABI work, and Rust interop work remain outside this chapter

What diagnostics do not guarantee

Diagnostics are strong, but they are not a substitute for the later semantic phases.

Current limits still matter:

  • parser diagnostics do not imply type checking has happened
  • report compatibility still belongs to later semantic work
  • type mismatch, coercion, and conversion diagnostics are future type-checker work
  • ownership and borrowing diagnostics are future semantic work
  • C ABI diagnostics are future V4 package/type/backend work
  • Rust interop diagnostics are future V4 package/type/backend work

So the current guarantee is:

  • if stream, lexer, parser, package loading, resolver, typechecker, or lowering can identify the problem now, diagnostics should be structured and exact

But not:

  • all language-semantic errors already exist today

Practical rule of thumb

When reading compiler output, think in this order:

  1. look at the diagnostic code in brackets to identify the error family
  2. trust the primary location
  3. use related labels to understand competing or earlier sites
  4. read notes for technical context
  5. read helps for the most actionable next step

That mental model matches how the compiler currently structures reporting.

Syntactic Sugar

This section collects forms that are primarily about expressiveness, convenience, or alternate notation.

These chapters should be read after the core statement, expression, and declaration rules, because most sugar expands on top of those more fundamental forms.

The current sugar chapters cover:

  • silent/discard forms
  • pipes
  • mixed/compound convenience forms
  • limits
  • matching
  • rolling
  • unpacking
  • inquiry
  • chaining

Silents

Single letter identifiers (SILENTs) identifiers are a form of languages sugar assignment.

Letter

Lowercase

Many times is needed to use a variable in-place and to decluter the code we use silents:

each(var x: str; x in {..10}){
    // implementation
}

each(x in {..10}){                      // we use the sicale `x` here
    // implementation
}

Uppercase

If a silent is uppercase, then it is a constant, can’t be changed. This is very important when using FOL for logic programming:

log vertical(l: line): bol = {
    l[A:B] and                          // we assign sicales `A` and `B`
    A[X:Y] and                          // we assign sicales `X` and `Y`
    B[X:Y2]                             // here we assign only `Y2` becase `X` exists from before
}

Symbols

Meh

Meh is the _ identifier. The use of the term “meh” shows that the user is apathetic, uninterested, or indifferent to the question or subject at hand. It is occasionally used as an adjective, meaning something is mediocre or unremarkable.

We use meh when we want to discard the variable, or we dont intend to use:

var array: arr[int, 3] = {1, 2, 3};
var a, _, b: int = array;               // we discard, the middle value

Y’all

Y’all is the * identifier. It represents app possible values that can be.

when(true) {
    case (x == 6){ // implementation }
    case (y.set()){ // implementation } 
    * { // default implementation }
}

Pipes

Pipes connect the value on the left to the expression on the right.

The basic idea is still:

left | right

where the right-hand side sees the left-hand side as this.

Ordinary value piping

Use | when you want to continue transforming a normal value:

fun add(x: int, y: int): int = {
    return x + y
}

fun main(): int = {
    return add(4, 5) | when(this > 8) {
        case(true) { 6 }
        * { 0 }
    }
}

This is ordinary value flow. The pipe itself does not create a special error model.

Recoverable calls are separate from ordinary pipes

Routines declared with / ErrorType produce recoverable call results:

fun read_code(path: str): int / str = {
    when(path == "") {
        case(true) { report "missing path" }
        * { return 7 }
    }
}

For these calls, the current V1 compiler does not treat plain | as the main error-handling tool.

Instead, the implemented recoverable-call surfaces are:

  • check(expr)
  • expr || fallback

check and panic are compiler intrinsics in the current V1 compiler. They are not imported library functions.

check(expr)

check(expr) asks whether a recoverable routine call failed.

It returns bol.

fun main(path: str): int / str = {
    when(check(read_code(path))) {
        case(true) { report "read failed" }
        * { return 0 }
    }
}

This is the current V1 inspection surface for recoverable calls.

|| fallback

Double-pipe is the current shorthand for recovery:

fun read_code(path: str): int / str = {
    when(path == "") {
        case(true) { report "missing path" }
        * { return 7 }
    }
}

fun with_default(path: str): int = {
    return read_code(path) || 5
}

Meaning:

  • if the call succeeds, use the success value
  • if the call fails, evaluate the right-hand side

The fallback may be:

  • a default value
  • report ...
  • panic ...

Examples:

fun recover(path: str): int = {
    return read_code(path) || 5
}

fun re_report(path: str): int / str = {
    return read_code(path) || report "read failed"
}

fun must_succeed(path: str): int = {
    return read_code(path) || panic "read failed"
}

What plain | does not mean in current V1

The current compiler does not claim that plain | automatically forwards both the success value and the error to the next stage.

That older description is too broad for the current implementation.

For recoverable calls in V1, use check(expr) or expr || fallback, and treat ordinary | as value piping rather than the main recoverable-error mechanism.

Mixture

Optional

var someMixtureInt: ?int = 45;

Never

var someNverType: !int = panic();

Limits

Limiting is a syntactic way to set boundaries for variables. The way FOL does is by using [] right after the type declaration type[], so: type[options][limits]

Initger limiting

Example, making a intiger variable have only numbers from 0 to 255 that represents an RGB value for a single color:

var rgb: int[][.range(255)];

Character limiting

It works with strings too, say we want a string that can should be of a particular form, for example an email:

var email: str[][.regex('[._a-z0-9]+@[a-z.]+')]

Matching

Variable

As variable assignment:

var checker: str = if(variable) { 
    in {..10} -> "in range of 1-10"; 
    in {11..20} -> "in range of 11-20";
    * -> "out of range";
}

var is_it: int = if(variable) { 
    is "one" -> 1; 
    is "two" -> 2; 
    * -> 0;
}

var has_it: bol = if(variable) { 
    has "o", "k" -> true; 
    * -> false;
}

Function

As function return:

fun someValue(variable: int): str = when(variable) { 
    in {..10} -> "1-10"; 
    in {11..20} -> "11-20"; 
    * -> "0";
}

Rolling

Rolling or list comprehension is a syntactic construct available FOL for creating a list based on existing lists. It follows the form of the mathematical set-builder notation - set comprehension.

Rolling has the same syntactic components to represent generation of a list in order from an input list or iterator:

  • A variable representing members of an input list.
  • An input list (or iterator).
  • An optional predicate expression.
  • And an output expression producing members of the output list from members of the input iterable that satisfy the predicate.

The order of generation of members of the output list is based on the order of items in the input. Syntactically, rolling consist of an iterable containing an expression followed by a for statement. In FOL the syntax follows exacly the Python’s list comprehension syntax:

var aList: vec[] = { x for x in iterable if condition }

Rolling provides an alternative syntax to creating lists and other sequential data types. While other methods of iteration, such as for loops, can also be used to create lists, rolling may be preferred because they can limit the number of lines used in your program.

var aList: vec[] = {..12};

var another: vec[] = { ( x * x ) for ( x in aList ) if ( x % 3 == 0 ) }
var matrix: mat[int, int] = { x * y for ( x in {..5}, y in {..5} ) }

Unpacking

Unpacking—also known as iterable destructuring—is another form of pattern matching used to extract data from collections of data. Take a look at the following example:

var start, *_ = { 1, 4, 3, 8 }
.echo(start)                                // Prints 1
.echo(_)                                    // Prints [4, 3, 8]

In this example, we’re able to extract the first element of the list and ignore the rest. Likewise, we can just as easily extract the last element of the list:

var *_, end = { "red", "blue", "green" }
.echo(end)                                  // Prints "green"

In fact, with pattern matching, we can extract whatever we want from a data set assuming we know it’s structure:

var start, *_, (last_word_first_letter, *_) = { "Hi", "How", "are", "you?" }
.echo(last_word_first_letter)               // Prints "y"
.echo(start)                                // Prints "Hi"

Now, that has to be one of the coolest programming language features. Instead of extracting data by hand using indices, we can just write a pattern to match which values we want to unpack or destructure.

Inquiry

Inquiries are inline unit tests and are a part of the basic syntax sugar. In other words, we don’t have to import any libraries or build up any suites to run tests.

Instead, FOL includes a couple of clauses for testing within the source code:

fun sum(l: ...?int): int = {
    when(l.length()) {
        is 0 => 0;
        is 1 => l[0];
        $ => l[0] + sum(l[1:]);
    }

    where(self) {
        sum() is 0;
        sum(8) is 8;
        sum(1, 2, 3) is 6;
    }
} 

Here, we can see an awesome list sum function. Within the function, there are two basic cases: empty and not empty. In the empty case, the function returns 0. Otherwise, the function performs the sum.

At that point, most languages would be done, and testing would be an afterthought. Well, that’s not true in FOL. To add tests, we just include a where clause. In this case, we test an empty list and a list with an expected sum of 6.

When the code is executed, the tests run. However, the tests are non-blocking, so code will continue to run barring any catastrophic issues.

Chaining

Optional chaining is a process for querying and calling properties, methods, and subscripts on an optional that might currently be nil. If the optional contains a value, the property, method, or subscript call succeeds; if the optional is nil, the property, method, or subscript call returns nil. Multiple queries can be chained together, and the entire chain fails gracefully if any link in the chain is nil.

Before I can really explain optional chaining, we have to get a grasp of what an optional value is. In FOL, variables cannot be empty. In other words, variables cannot store a value of NIL, at least not directly. This is a great feature because we can assume that all variables contain some value. Of course, sometimes variables need to be NIL. Fortunately, FOL provides that through a boxing feature called optionals. Optionals allow a user to wrap a value in a container which can be unwrapped to reveal either a value or NIL:

var printString: ?str;
printString = "Hello, World!"
.echo(printString!)

In this example, we declare an optional string and give it a value of “Hello, World!” Since we know that the variable stores a str, we can unconditionally unwrap the value and echo it. Of course, unconditional unwrapping is typically bad practice, so I’m only showing it for the purposes of showing off optionals.

Current V1 compiler note:

  • nil is type-checked only when the surrounding context already expects an opt[...] or err[...] shell.
  • A standalone nil with no expected shell type is rejected during typechecking.
  • Postfix unwrap value! is currently type-checked for opt[T] and err[T] and yields T.
  • Bare err[] does not currently support postfix unwrap because there is no payload value to recover.
  • This ! surface is shell-only. It does not unwrap routine call results declared with ResultType / ErrorType.

At any rate, optional chaining takes this concept of optionals and applies it to method calls and fields. For instance, imagine we have some long chain of method calls:

important_char = commandline_input.split('-').get(5).charAt(7)

In this example, we take some command line input and split it by hyphen. Then, we grab the fifth token and pull out the seventh character. If at any point, one of these method calls fails, our program will crash.

With optional chaining, we can actually catch the NIL return values at any point in the chain and fail gracefully. Instead of a crash, we get an important_char value of NIL. Now, that’s quite a bit more desirable than dealing with the pyramid of doom.

The current V1 compiler milestone only implements the plain nil/! surfaces described above. Full optional chaining remains later work.

Value Conversion

This section covers conversions between types.

FOL distinguishes:

  • coercion: conversions considered safe and unambiguous
  • casting: conversions that should be explicit in source

Coercion

The book-level idea of coercion is simple:

  • coercion is implicit
  • it should only happen when the conversion is safe and unambiguous

For the current V1 compiler milestone, that contract is intentionally narrow.

V1 coercion currently allows:

  • exact type matches
  • alias-wrapped values whose apparent type matches the expected type
  • never flowing into any expected type
  • the current optional/error shell lifting used by ordinary V1 surfaces

V1 coercion currently does not allow:

  • implicit int -> flt
  • implicit flt -> int
  • implicit width or signedness changes
  • implicit container reshaping
  • implicit string/character/container conversions

So today the rule is:

  • if two values are not already the same semantic family, the compiler should reject the implicit conversion

This is deliberate. The conversion chapter in the language design is broader than the current compiler guarantee, and V1 chooses explicit rejection over silent guessing.

Examples that are accepted in V1:

var count: int = 1
var ratio: flt = 1.5

fun[] take_int(value: int): int = {
    return value;
}

fun[] take_float(value: flt): flt = {
    return value;
}

Examples that are rejected in V1:

var count: int = 1.5
var ratio: flt = 1
fun[] take_float(value: flt): flt = {
    return value;
}

fun[] bad(): flt = {
    return take_float(1);
}

Later versions may widen this contract, but V1 keeps coercion intentionally small so type behavior stays predictable.

Casting

Casting is the explicit side of value conversion.

The long-term language direction is:

  • coercion stays implicit and narrow
  • casting stays explicit and source-visible

For the current V1 compiler milestone, casting syntax is parsed, but casting semantics are not implemented yet.

That means:

  • value as target
  • value cast target

are both valid syntax surfaces, but they are not part of the supported V1 type system.

The current compiler behavior is explicit:

  • it does not silently reinterpret these expressions
  • it does not treat them as ordinary coercions
  • it reports them as unsupported V1 typecheck surfaces

Example:

fun[] bad_as(value: int): int = {
    return value as text;
}

fun[] bad_cast(value: int): int = {
    return value cast target;
}

Both forms currently fail during typechecking.

This boundary is intentional. Before FOL can support casting for real, the compiler needs a stable legality contract answering questions such as:

  • which scalar casts are allowed
  • whether lossy casts are permitted
  • whether container casts exist
  • how aliases interact with explicit conversion
  • how future foreign/ABI types participate in conversion

That last point is deliberately later work:

  • C ABI and Rust interop are planned V4 features
  • casting rules for foreign or ABI-facing types should be specified together with that V4 interop contract, not guessed earlier

Until that contract exists, V1 treats cast syntax as parsed-but-unsupported instead of guessing semantics.

Memory Model

This section explains how FOL talks about storage, ownership, allocation, and pointer-like behavior.

The main topics are:

  • ownership
  • pointers
  • stack vs heap intuition
  • allocation lifetime
  • cross-thread memory concerns

The detailed chapters should be treated as the normative source. This index only frames the area.

Ownership

This chapter describes the planned systems-language ownership model.

Current milestone note:

  • ownership is not part of the current V1 compiler contract
  • borrowing is not part of the current V1 compiler contract
  • these semantics belong to the later V3 systems milestone

So the syntax and examples here should be read as future design, not as current compiler behavior.

Much like C++ and Rust, in Fol every variable declared, by default is created in stack unless explicitly specified othervise. Using option [new] or [@] in a variable, it allocates memory in the heap. The size of the allocation is defined by the type. Internally this creates a pointer to heap address, and dereferences it to the type you are having. Usually those behind the scene pointers here are unique pointers. This means that when the scope ends, the memory that the pointer used is freed.

var[new] intOnHeap: int[64];
@var intOnHeap: int[64];

Assignments

As discussed before, declaring a new variable is like this:

var[exp] aVar: int[32] = 64

{{% notice warn %}}

However, when new variable is created and uses an old variable as value, the value is always cloned for “stack” declared values, but moved for “heap” declared values.

{{% /notice %}}

@var aVar: int[32] = 64
{
    var bVar = aVar                                              // this moves the content from $aVar to $bVar
}
.echo(aVar)                                                      // this will throw n error, since the $aVar is not anymore owner of any value

When the variable is moved, the owner is changed. In example above, the value 64 (saved in stack) is owned my aVar and then the ownership is moved to bVar. Now bVar is the new owner of the variable, making the aVar useless and can’t be refered anymore. Since the bVar now controls the value, it’s lifetime lasts until the end of the scope. When the scope ends, the variable is destroyed with .de_alloc() function. This because when the ovnership is moved, the attributes are moved too, so the @ of aVar is now part of the bVar even if not implicitly specified. To avoid destruction, the bVar needs to return the ownership back to aVar before the scope ends with .give_back(bVar) or !bVar.

@var aVar: int[32] = 64
{
    var bVar = aVar                                              // this moves the content from $aVar to $bVar
    !bvar                                                        // return ownership
}
.echo(aVar)                                                      // this now will print 64

This can be done automatically by using borrowing.

Borrowing

Borrowing does as the name says, it borrows a value from another variable, and at the end of the scope it automatically returns to the owner.

pro[] main: int = {
    var[~] aVar: int = 55;
    {
        var[bor] newVar: int = aVar                              // represents borrowing
        .echo(newVar)                                            // this return 55
    }
    .echo(aVar)                                                  // here $aVar it not accesible, as the ownership returns at the end of the scope
    .echo(newVar)                                                // we cant access the variable because the scope has ended
}

Borrowing uses a predefined option [bor], which is not conventional like other languages that use & or *. This because you can get away just with “borrowing” without using pointers (so, symbols like * and & are strictly related to pointers)

However, while the value is being borrowed, we can’t use the old variable while is being borrowed but we still can lend to another variable:

pro[] main: int = {
    var[~] aVar: int = 55;
    {
        var[bor] newVar = aVar                                   // represents borrowing
        .echo(newVar)                                            // this prints 55
        .echo(aVar)                                              // this throws an error, cos we already have borrowd the value from $aVar
        var[bor] anotherVar = aVar                               // $anotherVar again borrows from a $aVar
    }
}

{{% notice warn %}}

When borrowed, a the value is read-only (it’s immutable). To make it muttable, firtsly, the owner needs to be muttable, secondly the borrower needs to declarare that it intends to change.

{{% /notice %}}

To do so, the borrower uses var[mut, bor]. However, when the value is declared mutable by owner, only one borrower within one scope can declare to modify it:

pro[] main: int = {
    var[~] aVar: int = 55;
    {
        var[mut, bor] newVar = aVar                              // [mut, bor] represents a mutable borrowing
        var[mut, bor] anotherVar = aVar                          // this throws an error, cos we already have borrowed the muttable value before
    }
    {
        var[mut, bor] anotherVar = aVar                          // this is okay, s it is in another scope
    }
}

Pointers

This chapter is future systems-language design.

Current milestone note:

  • pointer semantics are not part of the implemented V1 compiler contract
  • pointer-specific intrinsics such as .pointer_value(...), .address_of(...), and .borrow_from(...) are deferred
  • the material here belongs to the later V3 systems milestone

So the examples in this chapter should be read as planned language direction, not as currently supported V1 code.

The only way to access the same memory with different variable is by using pointers. In example below, we create a pointer, and when we want to dereference it to modify the content of the address that the pointer is pointing to, we use *ptrname or .pointer_value(ptrname).

@var aContainer: arr[int, 5];                                    //allocating memory on the heap
var contPoint: ptr[] = aContainer;
    
*contPoint = { zero, one, two, three, four };                    //dereferencing and then assigning values

Bare in mind, that the pointer (so, the address itself) can’t be changes, unless when created is marked as var[mut]. To see tha address of a pointer we use &ptrname or .address_of(ptrname)

@var aContainer: arr[int, 5];                                    //allocating memory on the heap
var contPoint: ptr[] = aContainer;
    
var anotherPoint: ptr[] = &contPoint;                            //assigning the same adress to another pointer

Unique pointer

Ponter of a pointer is very simimilar to RUST move pointer, it actually, deletes the first pointer and references the new one to the location of deleted one. However this works only when the pointer is unique (all pointers by default all unique). This is like borrowing, but does not invalidate the source variable:

var aContainer: arr[int, 5] = { zero, one, two, three, four };

var contPoint: ptr[] = aContainer;
var anotherPoint: ptr[] = &contPoint;

with borrowing, we use #varname or .borrow_from(varname)

var aContainer: arr[int, 5] = { zero, one, two, three, four };
{
    var borrowVar = #aContainer;                                    //this makes a new var form the old var, but makes the old invalid (until out of scope)
}

Shred pointer

Ponter can be shared too. They can get referenced by another pointer, and they don’t get destroyed until the last reference’s scope is finished. This is exacly like smart shared_ptr in C++. Pointer to this pointer makes a reference not a copy as unique pointers. Dereferencing is a bit complicated here, as when you dereference a pointer pointer you get a pointer, so you need to dereference it too to get the value.

@var aContainer: arr[int, 5] = { zero, one, two, three, four };

var contPoint: ptr[] = aContainer;
var pointerPoint: ptr[shared] = &contPoint;

Dereferencing (step-by-step):

var apointerValue = *pointerPoint
var lastpointerValue = *apointer

Dereferencing (all-in-one):

var lastpointer = *(*pointerPoint)

Raw pointer

Lastly, pointers can be raw too. This is the base of ALL POINTERS AND VARIABLES. Pointers of this type need to MANUALLY GET DELETED. If a pointer gets deleted before the new pointer that points at it, we get can get memory corruptions:

var aContainer: arr[int, 5] = { zero, one, two, three, four };

var contPoint: ptr[raw] = aContainer;
var pointerPoint: ptr[raw] = &contPoint;

Deleting:

!(pointerPoint)
!(contPoint)

Concurrency

This section covers FOL’s concurrency and asynchronous execution model.

The current chapter split is:

  • eventuals
  • coroutines

Together they define the language-level model for deferred work, coordination, and concurrent execution structure.

Eventuals

This chapter is future concurrency/runtime design.

Current milestone note:

  • async/eventual semantics are not part of the implemented V1 compiler
  • this material belongs to the later V3 systems milestone

Eventuals describe an object that acts as a proxy for a result that is initially unknown, usually because the computation of its value is not yet complete.

Async/Await

Async methods are intended to be non-blocking operations. An await expression in an async routine doesn’t block the current thread while the awaited task is running. Instead, the expression signs up the rest of the routine as a continuation and returns control to the caller of the async routine and it means “Once this is done, execute this function”. It’s basically a “when done” hook for your code, and what is happening here is an async routine, when executed, returns a coroutine which can then be awaited. This is done usually in one thread, but can be done in multiple threads too, but thread invocations are invisible to the programmer in this case.

pro main(): int = {
    doItFast() | async                                             // compiler knows that this routine has an await routine, thus continue when await rises
    .echo("dosomething to echo")
                                                                   // the main program does not exit until the await is resolved
}
fun doItFast(): str = {
    result = client.get(address).send() | await                    // this tells the routine that it might take time
    .echo(result)
}

Coroutines

This chapter is future concurrency/runtime design.

Current milestone note:

  • coroutine, channel, and mutex semantics are not part of the implemented V1 compiler
  • this material belongs to the later V3 systems milestone

A coroutine is a task given form the main thread, similar to a routine, that can be in concurrent execution with other tasks of the same program though other routines. A worker takes the task and runs it, concurrently. Each task in a program can be assigned to one or multiple workers.

Three characteristics of coroutine distinguish them from normal routines:

  • First, a task may be implicitly started, whereas a routine must be explicitly called.
  • Second, when a program unit invokes a task, in some cases it need not wait for the task to complete its execution before continuing its own.
  • Third, when the execution of a task is completed, control may or may not return to the unit that started that execution.
  • Fourth and most importantly, the execution of the routine is entirely independent from main thread.

In fol to assign a task to a worker, we use the symbols [>]

Channels

FOL provides asynchronous channels for communication between threads. Channels allow a unidirectional flow of information between two end-points: the Transmitter and the Receiver. It creates a new asynchronous channel, returning the tx/tx halves. All data sent on the Tx (transmitter) will become available on the Rx (receiver) in the same order as it was sent. The data is sent in a sequence of a specifies type seq[type]. tx will not block the calling thread while rx will block until a message is available.

pro main(): int = {
    var channel: chn[str];
    for (0 ... 4) {
        [>]doItFast() | channel[tx]                                             // sending the output of four routines to a channel transmitter
                                                                                // each transmitter at the end sends the close signal
    }
    var fromCh1 = channel[rx][0]                                                // reciveing data from one transmitter, `0`
}

fun doItFast(i: int; found: bol): str = {
    return "hello"
}

If we want to use the channel within the function, we have to clone the channel’s tx and capture with an ananymus routine: Once the channels transmitter goes out of scope, it gets disconnected too.

pro main(): int = {
    var channel: chn[str];                                                      // a channel with four buffer transmitters
    var sequence: seq[str];

    for (0 ... 4) {
        [>]fun()[channel[tx]] = {                                               // capturin gthe pipe tx from four coroutines
            for(0 ... 4){
                "hello" | channel[tx]                                           // the result are sent fom withing the funciton eight times
            }
        }                                                                       // when out of scope a signal to close the `tx` is sent
    }

    select(channel as c){
        sequence.push(channel[rx][c])                                           // select statement will check for errors and check which routine is sending data
    }
}

Locks - Mutex

Mutex is a locking mechanism that makes sure only one task can acquire the mutexed varaible at a time and enter the critical section. This task only releases the mutex when it exits the critical section. It is a mutual exclusion object that synchronizes access to a resource.

In FOL mutexes can be passed only through a routine. When declaring a routine, instead of using the borrow form with ( // borrowing variable ), we use double brackets (( // mutex )). When we expect a mutex, then that variable, in turn has two method more:

  • the lock() which unwraps the variable from mutex and locks it for writing and
  • the unlock() which releases the lock and makes the file avaliable to other tasks

fun loadMesh(path: str, ((meshes)): vec[mesh]) = {                              // declaring a *mutex and *atomic reference counter with double "(( //declaration ))"
    var aMesh: mesh = mesh.loadMesh(path) 
    meshes.lock()
    meshes.push(aMesh)                                                          // there is no need to unlock(), FOL automatically drops at the end of funciton
                                                                                // if the function is longer, then we can unlock to not keep other tasks waiting
}

pro main(): int = {
    ~var meshPath: vec[str];
    ~var meshes: vec[mesh];
    var aFile = file.readfile(filepath) || .panic("cant open the file")

    each( line in aFile.line() ) { meshPath.push(line) };
    for(m in meshPath) { [>]loadMesh(m, meshes) };
}