The FFL compiler transforms FFL (Facetwork Flow Language) source code into a JSON workflow definition for execution by the Facetwork runtime.
FFL Source → Lark Parser → Parse Tree → Transformer → AST → Emitter → JSON
The input to the compiler takes two lists of source files:
- Primary sources: The main FFL source files for this agent
- Library sources: Dependencies referenced by the primary sources
Each entry contains the source text plus provenance metadata indicating where the source was obtained:
| Origin Type | Provenance Data |
|---|---|
| File | File path |
| MongoDB | Collection ID + display name |
| Maven | Group ID, artifact ID, version, optional classifier |
The CompilerInput structure holds source entries:
from afl import CompilerInput, SourceEntry, FileOrigin, SourceLoader
# Load from files
entry1 = SourceLoader.load_file("main.ffl")
entry2 = SourceLoader.load_file("lib.ffl", is_library=True)
# Build compiler input
compiler_input = CompilerInput(
primary_sources=[entry1],
library_sources=[entry2]
)
# Parse with provenance tracking
parser = AFLParser()
ast, registry = parser.parse_sources(compiler_input)The SourceRegistry maps source IDs to their origins for provenance lookup.
- Input: FFL source code (string)
- Tool: Lark with LALR parser
- Output: Lark parse tree
- Errors:
ParseErrorwith line/column
- Input: Lark parse tree
- Tool:
AFLTransformer(extendslark.Transformer) - Output:
ProgramAST root node - Features: Source location tracking via
propagate_positions=True
The transformer uses internal helper methods to extract typed items from heterogeneous child lists produced by Lark:
| Helper | Purpose |
|---|---|
_find_one(items, cls) |
Extract the first item of a given type, or None |
_find_all(items, cls) |
Extract all items of a given type |
_left_assoc_fixed_op(meta, items, operator) |
Build a left-associative BinaryExpr chain for a single operator (e.g. ` |
_left_assoc_interleaved(meta, items) |
Build a left-associative BinaryExpr chain where operator tokens are interleaved with operands (e.g. add_expr, mul_expr) |
The CATCH_KW terminal handler discards the catch keyword token, preventing it
from appearing as a raw string in child item lists. The prompt_block rule uses
dict-based dispatch to map prompt directive names (system, template, model,
max_tokens, stop_sequences) to PromptBlock fields.
- Input:
ProgramAST - Tool:
JSONEmitter - Output: JSON string or dictionary
- Options: Include/exclude source locations, indentation
The FFL compiler uses Lark with the following configuration:
Lark(
grammar,
parser="lalr",
propagate_positions=True,
maybe_placeholders=False,
)| File | Purpose |
|---|---|
afl/grammar/afl.lark |
Lark EBNF grammar (87 lines) |
afl/parser.py |
Parser wrapper with error handling |
afl/transformer.py |
Parse tree to AST conversion |
All syntax errors include:
- Error message describing the issue
- Line number (1-indexed)
- Column number (1-indexed)
- Expected tokens (when applicable)
The emitter converts AST nodes to JSON with consistent structure.
| Option | Default | Description |
|---|---|---|
include_locations |
True |
Include source locations |
include_provenance |
False |
Include source provenance in locations |
source_registry |
None |
Registry for provenance lookup |
indent |
2 |
JSON indentation (None for compact) |
When include_provenance=True, locations include sourceId and provenance:
{
"location": {
"line": 1,
"column": 1,
"sourceId": "file:///path/to/file.ffl",
"provenance": {
"type": "file",
"path": "/path/to/file.ffl"
}
}
}As of v0.12.52, the emitter produces a declarations-only JSON format:
- The
Programnode contains a singledeclarationslist — there are no separatenamespaces,facets,eventFacets,workflows,implicits, orschemaskeys Namespacenodes also use adeclarationslist internally- All declaration types (
FacetDecl,EventFacetDecl,WorkflowDecl,ImplicitDecl,SchemaDecl,Namespace) appear in the unifieddeclarationslist
For backward compatibility with legacy/external JSON that uses categorized keys, normalize_program_ast() in afl/ast_utils.py converts categorized-key JSON into declarations-only format.
Example:
{
"type": "Program",
"declarations": [
{"type": "Namespace", "name": "ns", "declarations": [
{"type": "FacetDecl", "name": "MyFacet", ...},
{"type": "ImplicitDecl", "name": "defaults", ...}
]},
{"type": "WorkflowDecl", "name": "Main", ...}
]
}| AST Node | JSON type field |
|---|---|
Program |
"Program" |
FacetDecl |
"FacetDecl" |
EventFacetDecl |
"EventFacetDecl" |
WorkflowDecl |
"WorkflowDecl" |
Namespace |
"Namespace" |
ImplicitDecl |
"ImplicitDecl" |
AndThenBlock |
"AndThenBlock" |
StepStmt |
"StepStmt" |
YieldStmt |
"YieldStmt" |
CallExpr |
"CallExpr" |
Reference (input) |
"InputRef" |
Reference (step) |
"StepRef" |
Literal (string) |
"String" |
Literal (int) |
"Int" |
Literal (bool) |
"Boolean" |
Literal (null) |
"Null" |
afl [options] [input_file]| Flag | Description |
|---|---|
-o, --output FILE |
Output file (default: stdout) |
--primary FILE |
Primary source file (repeatable) |
--library FILE |
Library source file (repeatable) |
--mongo ID:NAME |
MongoDB source |
--maven G:A:V[:CLASSIFIER] |
Maven artifact |
--no-locations |
Exclude source locations |
--include-provenance |
Include source provenance in locations |
--compact |
Compact JSON (no indentation) |
--check |
Syntax check only, no output |
--no-validate |
Skip semantic validation |
# Parse file to stdout (legacy single-file input)
afl input.ffl
# Parse to file
afl input.ffl -o output.json
# Multi-source input
afl --primary main.ffl --primary util.ffl --library lib.ffl
# Include provenance in output
afl --primary main.ffl --include-provenance
# Compact output
afl input.ffl --compact --no-locations
# Syntax check
afl input.ffl --check
# From stdin
echo 'facet Test()' | aflfrom afl import (
parse, emit_json, emit_dict, AFLParser, ParseError,
CompilerInput, SourceEntry, SourceRegistry, SourceLoader,
FileOrigin, JSONEmitter,
)
# Simple single-source parsing (legacy API)
ast = parse("facet User(name: String)")
json_str = emit_json(ast)
# Multi-source parsing with provenance
entry1 = SourceLoader.load_file("main.ffl")
entry2 = SourceLoader.load_file("lib.ffl", is_library=True)
compiler_input = CompilerInput(
primary_sources=[entry1],
library_sources=[entry2]
)
parser = AFLParser()
ast, registry = parser.parse_sources(compiler_input)
# Emit with provenance
emitter = JSONEmitter(
include_provenance=True,
source_registry=registry,
)
json_str = emitter.emit(ast)
# Error handling
try:
ast = parse("invalid (")
except ParseError as e:
print(f"Error at line {e.line}, column {e.column}: {e}")Location: afl/grammar/afl.lark
- Program structure (start, namespace, declarations)
- Facet signatures (params, returns, mixins)
- AndThen blocks (foreach, steps, yield)
- Expressions (calls, references, literals)
- Terminals (identifiers, types, literals)
- Whitespace and comments
Parameters support optional default values:
param: IDENT ":" type ("=" expr)?
When a default is present, the emitter adds a "default" key to the parameter JSON:
{
"name": "input",
"type": "String",
"default": {"type": "String", "value": "hello"}
}Without a default, the key is omitted:
{"name": "input", "type": "String"}BOOLEAN,NULL,TYPE_BUILTINhave priority.2IDENT,QNAMEhave default priority- This ensures
true/falseparse as booleans, not identifiers