This document is the canonical runtime-tier matrix for FOL.
fol_model is selected per artifact in build.fol.
It is not:
- a source-file pragma
- an import convention
- a compatibility mode
It is:
- a semantic capability boundary
- a backend/runtime linkage boundary
Import reminder:
- only
stdis an importable source-level library namespace coreandmemoare compiler/runtime capability choices, notusetargets
Everything in this document reduces to three separate ideas. Keeping them apart is what makes the model coherent:
- Declared capability mode — the
fol_modelstring on each artifact inbuild.fol. There are exactly two:coreandmemo. - Bundled
stddependency — an explicitbuild.add_dep({ alias = "std", source = "internal", target = "standard" })declaration. It is a package dependency, never a thirdfol_model. - Effective runtime tier — what the artifact actually typechecks and links against. Derived, never written by hand:
| Declared mode | Package declares bundled std? |
Effective API tier | .echo substrate |
|---|---|---|---|
core |
either; unavailable to this artifact | core |
forbidden |
memo |
no | memo |
forbidden |
memo |
yes | hosted APIs (std) |
legal |
The promotion in the last row is the one rule people trip over: declaring the
bundled internal standard dependency at package level upgrades a memo
artifact's effective API capability to hosted. A core artifact in the same
package remains core; it cannot import or use bundled std. The promotion is
what lets the bundled std wrappers (which are built on the hosted .echo(...)
primitive) typecheck — and it also legalizes direct .echo(...) in your own
hosted source as the raw substrate
(examples/std_substrate_echo pins exactly this). Style still says ordinary
hosted code goes through std.io, but legality is decided by the dependency
declaration, not the spelling.
The build output reports both halves explicitly, for example:
capability_mode=memo, bundled_std=1/1.
The closest Rust analogy is:
| FOL selection | Rust mental model | FOL source can use |
|---|---|---|
fol_model = "core" |
#![no_std] with core |
fixed-shape, no-heap language facilities |
fol_model = "memo" |
#![no_std] with core plus alloc |
core plus FOL heap-backed values |
fol_model = "memo" plus bundled standard |
hosted Rust with std |
memo plus shipped hosted FOL APIs |
This is a capability analogy, not an assertion that the current backend emits
a freestanding Rust binary. core means that the FOL program has no
source-visible heap surface. The compiler, linker, frontend, generated process
entry adapter, and current Rust backend may still use build-host facilities as
implementation substrate. That substrate is not made available to FOL source.
Recommended style:
- spell
fol_modelexplicitly when the artifact iscore - omit
fol_modelwhen the artifact is meant to take the defaultmemo - treat
coreandmemoas capability choices - treat bundled
stdas a declared internal dependency, not as a third model graph.add_run(...),fol code run, andfol code testwork for host-compatiblecoreandmemoartifacts without bundledstd- treat frontend process launching as build-host behavior, not as a source-language hosted capability
fol_model answers which operations source code may express. The selected
machine target answers whether the frontend can launch the emitted artifact.
Those are independent checks:
| Artifact contract | Build | Run/test on a compatible host | Run/test on an incompatible host |
|---|---|---|---|
core, no bundled std |
yes | yes | rejected without an appropriate runner |
memo, no bundled std |
yes | yes | rejected without an appropriate runner |
memo, bundled std |
yes | yes | rejected without an appropriate runner |
The current frontend has no cross-target runner hook. It therefore rejects
fol code run and fol code test when the selected target cannot execute on
the host. Build the artifact normally and use an appropriate external runner
for that target. Declaring bundled std never fixes a target mismatch.
An artifact's root selects both its entry file and its compilation scope.
For an artifact-targeted build, the frontend:
- resolves and canonicalizes
rootrelative to the package root - requires the resolved root to be a source file inside that package
- recursively parses the directory containing that root
- checks that whole source directory under the artifact's
fol_model
The containment check follows the resolved path, so .. paths and symlinks
cannot be used to make an artifact root escape its declaring package.
This directory-scoped compilation is also the mixed-model isolation boundary.
Artifacts declared with different public models (core versus memo) must
use disjoint source directories. Two scopes overlap when they are the same
directory or when either directory contains the other; the frontend rejects
that graph instead of silently checking core source as memo. Sibling
directories such as core/, memo/, and app/ are the intended layout.
Commands that need to compile an entire package under one model still reject a mixed-model package. Select an exact artifact or its named build step, or split the artifacts into separate package members.
Meaning:
- no heap
- no source-level hosted OS/runtime APIs
- executable artifacts are allowed
Allowed language surface:
- scalars:
int,flt,bol,chr - arrays:
arr[...] - records, entries, aliases
- routines and method sugar
- control flow
- compile-time ownership and move checking
- lexical borrowing (
var[bor],#owner,!borrow) dfrand error-onlyedf- typed
ptr[...]declarations for analysis (but not allocating&value) opt[...],err[...]- array
.len(...) panic(...)
Forbidden surface:
strvec[...]seq[...]set[...]map[...].echo(...)- processor and other hosted language APIs
Choose core when:
- the FOL source/API contract must avoid heap-backed values and operations
- the artifact should be valid for embedded-first targets
- arrays and plain records are enough
- source-visible console and OS APIs are not part of the contract
Allowed example:
fun[] checksum(values: arr[int, 3]): int = {
return .len(values) + values[0];
};
Forbidden example:
fun[] label(): str = {
return "core-nope";
};
Meaning:
- heap-backed runtime facilities
- still no source-level hosted OS/runtime APIs
- executable artifacts are allowed
Adds:
strvec[...]seq[...]set[...]map[...]@var/var[new]owned heap allocation- unique/shared pointer construction with
&value - dynamic/string
.len(...)
Still forbidden unless the bundled internal standard dependency is
declared (which upgrades the effective API tier to hosted):
.echo(...)[>]spawn,chn[...],select, and[mux]routine parameters| asyncand| await- process/console/filesystem/network services
Choose memo when:
- the artifact needs strings or dynamic containers
- the artifact still should not depend on source-visible hosted OS/runtime APIs
- you want to keep heap usage explicit in
build.fol
Allowed example:
fun[] label(prefix: str, extras: ... str): str = {
return prefix + extras[0];
};
Forbidden example (a memo artifact with no bundled std dependency):
fun[] main(): int = {
.echo("memo-nope");
return 0;
};
std is not a third fol_model.
It is the bundled standard-library package shipped with FOL.
Projects opt into it explicitly in build.fol:
build.add_dep({
alias = "std",
source = "internal",
target = "standard",
});
Then source code imports it through the declared dependency alias:
use std: pkg = {"std"};
- pick
corefirst if the artifact can stay array-only and no-heap - move to
memowhen you actually needstror dynamic containers - add bundled
stdonly when the package genuinely needs shipped hosted-library wrappers or V3 processor facilities
The intent is to keep capability growth and dependency growth explicit.
Executable-artifact examples that build and run without bundled std:
examples/core_run_minexamples/memo_run_min
These examples declare graph run targets and execute through the ordinary
frontend path. Their core or memo tier constrains which APIs the source may
use; it does not prevent the toolchain from launching the resulting
host-compatible executable.
Hosted std examples with explicit bundled dependency:
examples/std_bundled_ioexamples/std_substrate_echo
Use core when an executable can stay fixed-shape and no-heap. Declaring a
run step does not widen its capability:
pro[] build(): non = {
var build = .build();
build.meta({ name = "core_app", version = "0.1.0" });
var graph = build.graph();
var app = graph.add_exe({
name = "core-app",
root = "src/main.fol",
fol_model = "core",
});
graph.install(app);
graph.add_run(app);
return;
};
Move to memo when the executable itself genuinely needs heap-backed strings
or dynamic containers. memo is the default, so omitting fol_model is
equivalent to spelling fol_model = "memo":
pro[] build(): non = {
var build = .build();
build.meta({ name = "memo_app", version = "0.1.0" });
var graph = build.graph();
var app = graph.add_exe({
name = "memo-app",
root = "src/main.fol",
});
graph.install(app);
graph.add_run(app);
return;
};
Add bundled std only when source needs shipped hosted-library wrappers or
processor facilities. The dependency is declared on build before taking the
graph; the artifact remains a memo artifact:
pro[] build(): non = {
var build = .build();
build.meta({ name = "hosted_app", version = "0.1.0" });
build.add_dep({
alias = "std",
source = "internal",
target = "standard",
});
var graph = build.graph();
var app = graph.add_exe({
name = "hosted-app",
root = "src/main.fol",
fol_model = "memo",
});
graph.install(app);
graph.add_run(app);
return;
};
The hosted source then imports the declared alias with
use std: pkg = {"std"};. std is not an accepted fol_model value.
Direct boundary reminder:
- a
coreartifact must not declarestr,seq,vec,set, ormap - a
memoartifact without the bundledstandarddependency must not call.echo(...) - a
coreormemoartifact may declare an executable, a graph run target, and test artifacts without bundledstandard fol code runandfol code testmay launch those host-compatible artifacts; target compatibility is independent of bundled-std API legality
Transitive boundary reminder:
- a
coreartifact still cannot consume heap-backed API from amemodependency - a
coreartifact cannot consume bundledstdAPIs, even when a siblingmemoartifact caused the package to declare the dependency - a
memoartifact cannot consume bundledstdAPIs unless the bundled internalstandarddependency was declared - a
memoartifact with bundledstdmay consume bothcoreandmemodependencies in one graph
Capability legality is checked at the consuming artifact boundary, not only at the dependency's own artifact boundary.
That means:
- a
coreartifact cannot consume heap-backed API from amemopackage just because the dependency itself was declared withfol_model = "memo" - a
coreartifact cannot reach.echo(...)through bundledstd, regardless of other package artifacts - a
memoartifact cannot reach.echo(...)indirectly through bundledstdunless the package declared internalstandard - a
memoartifact with bundledstdmay consumecoreandmemopackages in the same graph
The consuming artifact model always wins.
| Declared mode | Heap | Host-compatible execution | Hosted language APIs | Typical artifact shape |
|---|---|---|---|---|
core |
no | yes | no | embedded logic, fixed-shape libs, no-heap executables |
memo |
yes | yes | only with explicit bundled std |
heap utilities, alloc-like executables, bundled-std consumers |
Implemented today:
.echo(...)requires hosted std support; declaring the bundled internalstandarddependency on amemoartifact provides it (the whole consuming workspace typechecks at the hosted API capability)str,vec,seq,set, andmapare rejected incore- array
.len(...)stays valid incore - dynamic/string
.len(...)requiresmemo - ownership and lexical borrowing are checked in every model
- pointer type declarations are analyzable in
core, while&value, owned allocation, and pointer construction require the publicmemomode; a bundled std dependency may further raise that artifact's effective API tier, but it is not an alternative model and never makes those forms legal incore - all processor surfaces (
[>], channels,select, mutex parameters,async, andawait) require hostedstd - routed
run/testaccept host-compatiblecoreandmemoartifacts without bundledstd; the dependency is required only when source uses its hosted APIs - emitted Rust imports the matching internal runtime module
- public
fol_model = "memo"currently maps to the internal heap runtime modulefol_runtime::memo - packages import bundled
stdonly through explicit internal dependency declaration
The backend should treat the three runtime modules as intentionally different public surfaces.
Executable entry and recoverable process-outcome adaptation are backend-only
substrate shared by every tier. The generated wrapper calls the separate
fol_runtime::process adapter directly; core, memo, and std do not
re-export it as source-language capability.
fol_runtime::core- no heap-backed types
- no source-visible hosted hooks like
.echo(...) - executable wrappers may pair it with
fol_runtime::process
- internal heap runtime module
- heap-backed strings and dynamic containers
- still no source-visible hosted hooks like
.echo(...) - executable wrappers may pair it with
fol_runtime::process
fol_runtime::std- hosted hooks such as
.echo(...) - OS-thread spawn, channels, selection, mutex, and eventual support
- memo-tier heap types re-exported for host artifacts
- hosted hooks such as
Backend authors should not import a wider tier than the lowered artifact
actually requires. core emission should stay core-only. memo emission
currently routes through the internal heap runtime module and must not silently
widen to std. std is the only tier whose source may reach hosted language
APIs. Backend process entry and frontend host-tool launching are orthogonal to
that source-language gate.
The editor should follow the same model split.
The intended contract is:
- LSP semantic diagnostics should come from the real compiler pipeline
fol_modelshould affect editor diagnostics and completion the same way it affectsfol code build- tree-sitter grammar and structural capture layout stay hand-authored
- repetitive editor name lists are compiler-derived, not manually copied
This means adding a language feature should not require a second semantic
implementation in fol-editor. Only syntax-structure changes should normally
need targeted tree-sitter or editor UX updates.
This is the checked-in shape from examples/mixed_models_workspace. The
package-level bundled dependency raises the effective API tier of its memo
artifacts, while corelib remains core. graph.add_run(tool) only selects a
runnable graph artifact:
pro[] build(): non = {
var build = .build();
build.meta({ name = "mixed_models_workspace", version = "0.1.0" });
build.add_dep({ alias = "std", source = "internal", target = "standard" });
var graph = build.graph();
var corelib = graph.add_static_lib({
name = "corelib",
root = "core/lib.fol",
fol_model = "core",
});
var memolib = graph.add_static_lib({
name = "memolib",
root = "memo/lib.fol",
fol_model = "memo",
});
var tool = graph.add_exe({
name = "tool",
root = "app/main.fol",
fol_model = "memo",
});
graph.install(corelib);
graph.install(memolib);
graph.install(tool);
graph.add_run(tool);
return;
};
examples/core_blink_shapeexamples/core_dfrexamples/core_recordsexamples/core_surface_showcaseexamples/core_run_minexamples/memo_defaultsexamples/memo_containersexamples/memo_collectionsexamples/memo_surface_showcaseexamples/memo_run_minexamples/std_cliexamples/std_bundled_fmtexamples/std_bundled_ioexamples/std_explicit_pkgexamples/std_alias_pkgexamples/std_echo_minexamples/std_logtiny_gitexamples/std_named_callsexamples/std_surface_showcaseexamples/std_terminal_hooksexamples/mixed_models_workspace
Negative example packages:
examples/fail_core_heap_rejectexamples/fail_memo_echoexamples/fail_core_alloc_boundaryexamples/fail_core_std_importexamples/fail_memo_std_missing_dep