Getting Started with Overlay Language¶
The Step 4 Python code works, but it has a structural problem: business logic
and I/O are tangled together. Consider user_id in HttpHandlers:
def user_id(request: BaseHTTPRequestHandler) -> int:
return int(request.path.split("/")[-1])
This one function mixes three concerns: reading request.path (I/O), splitting
on "/" (a business decision about URL format), and parsing the last segment as
an integer (another business decision). The path separator, SQL queries, and
format templates are all hardcoded in Python — changing any of them means
changing Python code.
The Overlay language solves this by separating the application into three layers:
Python FFI wraps individual stdlib calls in
@scopeadapters — one class per operation (sqlite3.connect,str.split,wfile.write). Each adapter declares its inputs as@externand exposes a single@public @resourceoutput. The adapter contains zero business logic.``.oyaml`` files contain all application logic. Overlay is not just a configuration format — it is a complete language with lexical scoping, nested scopes, deep-merge composition, and lazy evaluation. These features make it more natural than Python for expressing business logic, which is inherently declarative (“the user ID is the last URL segment”, “the response format is
total={total} current={current}”).Configuration values (SQL queries, format strings, host/port) are pure YAML scalars, gathered in one place.
Business logic written in .oyaml is portable: it is decoupled from the
Python FFI layer. Swap the FFI adapters and the same .oyaml logic runs against
a different runtime — mock adapters for unit testing, a different language’s
stdlib for cross-platform deployment, or instrumented adapters for profiling.
With Python @scope decorators, business logic is locked to the Python runtime
and cannot be extracted or retargeted.
Below is the same Step 4 web application rewritten in this style.
Python FFI adapters¶
Each @scope class wraps exactly one stdlib operation. Below are three
representative adapters; the full module
(tests/fixtures/app_oyaml/stdlib_ffi/FFI.py)
contains 10 more following the same pattern.
SqliteConnectAndExecuteScript — multiple inputs, single output:
"""sqlite3.connect(database_path) + executescript(setup_sql) -> connection"""
import sqlite3
from overlay.language import extern, public, resource
@extern
def database_path() -> str: ...
@extern
def setup_sql() -> str: ...
@public
@resource
def connection(database_path: str, setup_sql: str) -> sqlite3.Connection:
conn = sqlite3.connect(database_path, check_same_thread=False)
conn.executescript(setup_sql)
return conn
GetItem — generic sequence[index] operation:
"""sequence[index] -> element"""
from overlay.language import extern, public, resource
@extern
def sequence() -> object: ...
@extern
def index() -> int: ...
@public
@resource
def element(sequence: object, index: int) -> object:
return sequence[index] # type: ignore[index]
HttpSendResponse — chained I/O, send status + headers + body:
"""send_response(status_code) + end_headers() + wfile.write(body) -> written"""
from http.server import BaseHTTPRequestHandler
from overlay.language import extern, public, resource
@extern
def request() -> BaseHTTPRequestHandler: ...
@extern
def status_code() -> int: ...
@extern
def body() -> bytes: ...
@public
@resource
def written(
request: BaseHTTPRequestHandler, status_code: int, body: bytes
) -> BaseHTTPRequestHandler:
request.send_response(status_code)
request.end_headers()
request.wfile.write(body)
return request
Notice what is not here: no SQL queries, no "/" separator, no format string,
no :memory: path, no port number. Those are all business decisions that live
in the .oyaml.
.oyaml composition¶
An .oyaml file describes a dependency graph, not an execution sequence.
There is no top-to-bottom control flow — the runtime evaluates resources lazily,
on demand. Think spreadsheet cells, not shell scripts.
The business logic lives in Library.oyaml, which references FFI adapters
through abstract declarations (FFI: scope with [] slots). A concrete FFI
module (stdlib_ffi/FFI.py) overrides these slots at composition time. This
separation means the business logic is portable — swap stdlib_ffi for a
different FFI implementation and the .oyaml files need no changes.
The following sections walk through Library.oyaml one scope at a time,
introducing new language concepts as they appear.
SQLiteDatabase — extern, inheritance, wiring, projection¶
SQLiteDatabase:
database_path: [] # extern: caller must provide this
setup_sql: [] # extern: caller must provide this
_db:
- [FFI, SqliteConnectAndExecuteScript] # inherit the FFI adapter
- database_path: [database_path] # wire extern → adapter input
setup_sql: [setup_sql]
connection: [_db, connection] # projection: expose _db.connection
Four new concepts:
``field: []`` — an extern declaration, the
.oyamlequivalent of@extern. The value must come from a parent scope or the caller.``- [FFI, SqliteConnectAndExecuteScript]`` — inheritance.
_dbinherits the FFI adapter, gaining all of its resources (connection).``database_path: [database_path]`` — wiring. The reference
[database_path]is a lexical lookup: search outward through enclosing scopes until a field nameddatabase_pathis found.``connection: [_db, connection]`` — path navigation. Access the
connectionresource on the child scope_db. The leading underscore on_dbmakes it private;connectionis the public-facing projection.
UserRepository (app-scoped part) — nested scope, scope-as-dataclass¶
UserRepository:
connection: [] # extern: from SQLiteDatabase
user_count_sql: [] # extern: provided by app
User: # scope-as-dataclass
user_id: []
name: []
_count:
- [FFI, SqliteScalarQuery]
- connection: [connection]
sql: [user_count_sql]
user_count: [_count, scalar]
``User:`` is a nested scope with two extern fields — the
.oyamlequivalent of@scope class Userwith@public @externfields. It acts as a dataclass constructor:current_user(below) will supply values foruser_idandname.``connection: []`` declares that
UserRepositoryexpects aconnectionfrom outside. When composed withSQLiteDatabaseinsideapp, this extern is satisfied bySQLiteDatabase.connection— resolved by name through lexical scoping.
UserRepository.RequestScope — ANF style, cross-scope references¶
RequestScope:
user_id: [] # extern: from HttpHandlers
user_query_sql: [] # extern: from app.RequestScope
_params:
- [FFI, TupleWrap]
- element: [user_id]
_row:
- [FFI, SqliteRowQuery]
- connection: [connection] # lexical: finds UserRepository.connection
sql: [user_query_sql]
parameters: [_params, wrapped]
_identifier:
- [FFI, GetItem]
- sequence: [_row, row]
index: 0
_name:
- [FFI, GetItem]
- sequence: [_row, row]
index: 1
current_user:
user_id: [_identifier, element]
name: [_name, element]
current_user_name: [current_user, name]
This section demonstrates A-Normal Form (ANF): every intermediate result
must be bound to a named field. You cannot write
GetItem(sequence=SqliteRowQuery(...).row, index=0) — instead, _row holds
the query result, and _identifier extracts column 0 from _row.row. The
cost is verbosity; the benefit is that every intermediate value is inspectable
and independently composable.
Cross-scope lexical reference: connection: [connection] inside
RequestScope finds UserRepository.connection — the lexical scope chain
searches outward through parent, grandparent, etc. No import statement is
needed; the scope hierarchy is the namespace.
Constructing ``current_user``: Instead of calling User(user_id=..., name=...),
the .oyaml directly defines current_user as a scope with two fields. The
User scope-as-dataclass above establishes the field names; here those same
names are filled with concrete values.
HttpHandlers — flat inheritance, qualified this¶
HttpHandlers:
user_count: [] # extern: from UserRepository
RequestScope:
- [FFI, ExtractUserId] # provides: user_id
- [FFI, HttpSendResponse] # provides: written
- request: [] # extern: injected per-request
path_separator: [] # extern: from app.RequestScope
response_template: [] # extern: from app.RequestScope
status_code: 200 # inline scalar
current_user_name: [] # extern: from UserRepository.RequestScope
_format:
- [FFI, FormatResponse]
- response_template: [response_template]
user_count: [user_count]
current_user_name: [current_user_name]
response_body: [_format, response_body]
body: [response_body]
# Qualified this: fails loudly if HttpSendResponse is not composed,
# unlike `written: []` which silently creates an empty scope.
response: [RequestScope, ~, written]
Flat inheritance: RequestScope inherits two FFI adapters in its
inheritance list (- [FFI, ExtractUserId], - [FFI, HttpSendResponse]). Their
@extern and @resource fields all merge into RequestScope’s own field
namespace. The last list item (the mapping starting with request: []) defines
RequestScope’s own fields.
Lexical scoping across scope boundaries: [user_count] inside
RequestScope searches outward and finds HttpHandlers.user_count. At this
point user_count is just an extern [] — its actual value comes from
UserRepository after deep merge (explained below).
Qualified this: ``[RequestScope, ~, written]`` — instead of declaring
written: [] and writing response: [written], this navigates the runtime
composition graph to access the written property inherited from
HttpSendResponse. The advantage: if HttpSendResponse is accidentally not
composed, this fails with an error instead of silently creating an empty scope.
NetworkServer — deep merge, config scoping¶
NetworkServer:
- [FFI, HttpServerCreate]
- host: []
port: []
RequestScope: []
_handler:
- [FFI, HttpHandlerClass]
- RequestScope: [RequestScope]
handler_class: [_handler, handler_class]
All the scopes above live in Library.oyaml. They reference [FFI, Xxx] which
resolves to abstract declarations at the top of the file — no concrete Python
code is involved yet.
Apps.oyaml — integration entry point¶
Apps.oyaml is a separate file that inherits the real FFI implementation and
the Library, then defines concrete application entries:
# Apps.oyaml — integration entry points: Library + real FFI + configuration values.
# Named "Apps" (plural) because multiple entry points can coexist here.
- [stdlib_ffi] # inherit real Python FFI adapters
- [Library] # inherit business logic
- memory_app:
- [Apps, ~, SQLiteDatabase] # qualified this: inherited from Library
- [Apps, ~, UserRepository]
- [Apps, ~, HttpHandlers]
- [Apps, ~, NetworkServer]
- database_path: ":memory:"
setup_sql: |
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO users VALUES (1, 'alice');
INSERT INTO users VALUES (2, 'bob');
user_count_sql: "SELECT COUNT(*) FROM users"
host: "127.0.0.1"
port: 0
RequestScope:
user_query_sql: "SELECT id, name FROM users WHERE id = ?"
path_separator: "/"
response_template: "total={total} current={current}"
Library/FFI separation: - [stdlib_ffi] makes the real FFI module
(Python @scope classes) visible. - [Library] makes the business logic
visible. When composed, stdlib_ffi.FFI (real implementations) deep-merges
with Library.FFI (abstract declarations), and the real @resource methods
override the [] slots. The business logic never imports Python directly.
Portability: To run the same business logic on a different runtime, replace
- [stdlib_ffi] with a different FFI package — the Library.oyaml file needs
no changes. To test with mocks, provide a mock FFI module instead of
stdlib_ffi.
“What Color Is Your Function?” (blog post):
The same Library.oyaml runs unchanged on both synchronous and asynchronous
runtimes. Replacing - [stdlib_ffi] with - [async_ffi] swaps the FFI layer
to one built on aiosqlite + starlette
(implementation) — the business
logic never knows whether it is sync or async. Function colour is confined
entirely to the FFI boundary; Library.oyaml itself is colourless.
Composition via inheritance: memory_app inherits four scopes via
qualified this ([Apps, ~, SQLiteDatabase] etc.) because these scopes are
inherited properties, not own properties of Apps.oyaml. This is not four
separate instances — it is a single scope with all four merged together. The
last list item supplies concrete values for every [] extern.
Deep merge: Both UserRepository and HttpHandlers define a
RequestScope. When composed inside memory_app, these merge by name into a
single RequestScope. After merging:
user_id(fromHttpHandlers.RequestScopeviaExtractUserId) becomes visible toUserRepository.RequestScope, which uses it to look upcurrent_usercurrent_user_name(fromUserRepository.RequestScope) becomes visible toHttpHandlers.RequestScope, which uses it in_format
Neither scope imports or references the other — deep merge makes their fields mutual siblings automatically. This is the most powerful feature of the Overlay language: cross-cutting concerns compose without glue code.
Config value scoping: App-lifetime values (database_path, host, port)
live directly in memory_app. Request-lifetime values (user_query_sql,
path_separator, response_template) live in memory_app.RequestScope — they
are only needed during request handling.
Syntax quick reference¶
Syntax |
Meaning |
|---|---|
|
Extern — value must be provided by a parent scope or caller |
|
Lexical reference — look up |
|
Path navigation — access |
|
Qualified this — access inherited |
|
Scalar value — string, number, etc. |
|
Private — not visible to external callers (leading underscore) |
|
Nested scope — a child scope with its own fields |
|
Inheritance — |
Python vs Overlay language¶
Aspect |
Python |
|
|---|---|---|
Composition |
Manual |
Inheritance list: |
Dependency injection |
|
|
Expression style |
Nested function calls |
ANF: every intermediate has a name |
Cross-cutting concerns |
Explicit adapter / glue code |
Deep merge: same-named scopes auto-merge |
Accessing inherited members |
|
Qualified this: |
Business logic location |
Mixed with I/O in Python |
Separate |
Configuration |
Kwargs at call site |
Scalar values in |
Evaluation¶
import tests.fixtures.app_oyaml as app_oyaml
from overlay.language import evaluate
# evaluate() auto-discovers stdlib_ffi/, Library.oyaml, and Apps.oyaml.
root = evaluate(app_oyaml, modules_public=True)
# Access the composed app — Apps is the .oyaml file name.
composed_app = root.Apps.memory_app
composed_app.server # HTTPServer on 127.0.0.1:<assigned port>
composed_app.connection # sqlite3.Connection to :memory:
composed_app.user_count # 2
# Create a fresh request scope (per-request resources):
scope = composed_app.RequestScope(request=fake_request)
scope.current_user.name # "alice"
scope.response # sends HTTP response as side effect
Swapping configuration is just a different entry in Apps.oyaml — the Python
FFI adapters and Library.oyaml never change. Swapping the FFI layer is just a
different @scope module — the .oyaml business logic never changes.
Runnable tests for this example are in tests/test_readme_package_examples.py, using the fixture package at tests/fixtures/app_oyaml/.
The full language specification is in Overlay Language Specification.
The semantics of the Overlay language are grounded in the Overlay-Calculus, a formal calculus of overlays.