ASR text format¶
ASR text is a lossless representation of an ASR translation unit. It is intended for compiler tests, generated-input reduction, and direct experimentation with ASR without going through a language frontend.
The format is an EDN data subset, defined by the grammar below. A standard EDN
reader can read it once handlers for the namespaced #asr/* tags are
registered; tests/asr/check_edn.py enforces that on every build by reading
printed ASR back with a third-party EDN implementation. LFortran reads the
format as data only; it does not evaluate Clojure code.
A document is a single ASR constructor, normally a TranslationUnit. There is
no envelope and no version field: the format is defined by this document, and
a future incompatible encoding would announce itself, with its absence meaning
the encoding described here.
Printing ASR text¶
The named form is the default for interactive dumps:
lfortran input.f90 --show-asr --clojure
It prints every non-location field using the exact field names in
src/libasr/ASR.asdl:
(Var
:v (SymbolRef 1 "x")
)
The positional form omits field names but preserves ASDL declaration order.
asr_clojure reference tests use this form so the stored dumps stay small:
lfortran input.f90 --show-asr --clojure --no-member-names
(Var (SymbolRef 1 "x"))
--no-indent emits either form on one line. Printed ASR text never contains
color escape sequences, elides intrinsic modules, or truncates constant data.
The legacy output of --show-asr without --clojure remains available during
the format migration.
Reading ASR text¶
Files ending in .asr are read as ASR text:
lfortran program.asr
--from-asr selects ASR text explicitly when a different extension is used.
The first implementation supports one ASR translation unit with the default
LLVM backend, including --show-asr, --show-llvm, --pass, --skip-pass,
-S, -c, and executable linking. When linking an executable, the ASR
translation unit itself must contain exactly one main program; additional
object files and libraries provide dependencies rather than the entry point.
The parser accepts named and positional constructors. The two forms can appear
in one file, but a single constructor cannot mix positional members with
keyword/member pairs. Committed .asr fixtures use the positional form,
the same form as asr_clojure reference output: a corpus is expected to
grow large, and a fixture written positionally is a fraction of the size.
A valid fixture can therefore be regenerated with
--show-asr --clojure --no-member-names.
--verify-asr parses a standalone ASR file and runs only its initial verifier.
It does not execute ASR passes or code generation, so corpus runners can
distinguish an accepted initial verifier rejection from any later compiler
failure.
Grammar¶
The reader accepts exactly the following. It is a subset of EDN: everything here reads identically under a conforming EDN reader, but the reverse does not hold, since EDN forms with no meaning in ASR text (sets, characters, ratios, namespaced keywords, metadata) are rejected.
document = value ;
value = list | vector | map | tagged
| keyword | symbol | string | integer | float
| "nil" | "true" | "false" ;
list = "(" , { value } , ")" ; (* constructors and products *)
vector = "[" , { value } , "]" ; (* ASDL sequences *)
map = "{" , { value , value } , "}" ; (* symbol table entries *)
tagged = "#" , tag , value ;
tag = "asr/bytes" | "asr/float64" | "asr/real128" | "asr/loc" ;
keyword = ":" , name ; (* member names, enum values *)
symbol = name ; (* constructor names *)
name = ( letter | "_" ) , { letter | digit | "_" } ;
string = '"' , { char | escape } , '"' ;
escape = "\\t" | "\\r" | "\\n" | '\\"' | "\\\\" | "\\u" , 4 * hex ;
integer = [ "-" ] , digit , { digit } ;
float = [ "-" ] , digit , { digit } , [ "." , { digit } ]
, [ ( "e" | "E" ) , [ "+" | "-" ] , digit , { digit } ] ;
hex = digit | "a".."f" | "A".."F" ;
Whitespace, commas and ; line comments separate values and are otherwise
insignificant. A constructor is a list whose first element is a symbol naming
an ASR constructor; its remaining elements are either all positional values in
ASR.asdl declaration order, or :member value pairs. The two forms may
appear in the same document but not within one constructor.
Encoding and text semantics¶
A document is UTF-8. This is the only encoding the format is defined in, and it is what makes the EDN claim meaningful.
Fortran character values are byte arrays, not text. A character constant can hold bytes that are not valid UTF-8, such as
achar(200). Those have no string spelling, so they are written as#asr/bytesinstead. Writing them raw would produce a document no reader can decode, and escaping them as\u00XXwould silently turn one byte into a two-byte character. Wherever a string may appear,#asr/bytesis therefore also accepted.Only portable escapes are emitted.
\t,\r,\n,\",\\and\uNNNN. Other control characters, including backspace and form feed, are written as\uNNNNrather than\band\f, which are not part of EDN.Round-tripping is byte-exact. Printing ASR, reading it back and printing it again yields identical bytes, including for values that take the
#asr/bytespath.Floating point is exact. Finite values print with enough digits to round-trip; infinities and NaNs print as
#asr/float64bit patterns, since EDN has no literal for them.real(16)uses#asr/real128.
Data forms¶
ASR text uses:
lists for constructors and ASDL products;
keywords for member names and enum values;
vectors for sequences;
maps for symbol table entries;
strings for identifiers and string values;
nilfor absent optional fields;trueandfalsefor logical fields;EDN integers and finite floating-point values;
semicolon comments and optional commas.
All non-location fields are explicit in canonical named output, including absent optional fields, empty sequences, and false logical values.
SymbolRef is a reserved text-format form that identifies a symbol by its
document-local symbol table ID and exact symbol-table key:
(SymbolRef 1 "x")
Other ASR-specific values use namespaced tagged elements:
#asr/bytes "..."stores an exact raw constant payload;#asr/float64 "..."and#asr/real128 "..."preserve values that cannot round-trip through an EDN decimal;#asr/loc [[first last] value]overrides a generated text location with inclusive byte offsets in focused tests.
The compact spelling @x is not used because a Clojure reader interprets @
as the dereference reader macro.
Graph references¶
ASR contains a graph rather than a pure tree. Symbol tables own symbol
definitions, while expressions, types, and other symbols refer back to those
definitions. Canonical text assigns deterministic integer IDs such as 0 and
1 to owning symbol tables before printing any references.
The decoder first creates all symbol tables and typed symbol shells, then fills definitions and resolves references. This permits forward references and cycles while preserving pointer identity.
Locations and diagnostics¶
Original Fortran byte offsets are not included by default. Each parsed ASR node
is assigned the location of its constructor token in the .asr file. ASR
parser and verifier diagnostics therefore highlight the direct ASR input:
ASR verify pass error: ...
--> example.asr:12:4
Explicit location tags are reserved for tests that need a specific span.
Parser and verifier boundary¶
The text parser rejects malformed EDN, unknown constructors or fields, missing required fields, wrong field categories, duplicate definitions, and unresolved textual references.
It does not enforce semantic ASR invariants. Once decoding succeeds,
asr_verify is responsible for type, rank, symbol, ownership, and other ASR
requirements. Consequently:
malformed text produces an ASR syntax diagnostic;
structurally decoded but invalid ASR produces an ASR verifier diagnostic;
verifier-valid standalone ASR proceeds through the normal pass and LLVM pipeline.
Regression corpus¶
A minimized ASR graph is checked in under tests/asr/ and registered in
tests/tests.toml like any other reference test, so the ordinary reference
suite runs it:
tests/asr/compile/holds graphs the verifier accepts, registered withllvm = trueso they must lower to LLVM IR. A registered CTest also links every one of them into an executable, which is the property the fuzzer’s contract turns on. They are not run: a generated program may fault at runtime for reasons no ASR verifier could predict;tests/asr/verify/holds graphs the verifier rejects, registered withasr = trueso the stored reference captures the exact diagnostic, its stable code, and the span it points at.
Because the diagnostic code is part of the rendered message, a fixture pins the specific verifier rule it exercises rather than the wording alone.
Fixtures are stored in the printer’s indented form, so a diagnostic points at
a single short line rather than at one long one, and the caret identifies the
offending constructor. Regenerate a valid fixture with
--show-asr --clojure --no-member-names.
--verify-all-passes runs the verifier after every ASR pass regardless of
the build type, so a pass that corrupts a previously valid graph is reported
against that pass instead of surfacing later as an unrelated failure.
The reusable APIs in src/lfortran/pipeline.h own Fortran-or-ASR loading and
the phase-aware ASR-to-default-passes-to-LLVM-to-object path, so the CLI and
the direct-ASR tools share one implementation.
Deterministic mutation fuzzing¶
tests/asr/fuzz.py dynamically prints the verified pre-pass ASR produced from
registered integration-test seeds, applies one field-aware mutation, and runs
the two-outcome oracle in isolated compiler subprocesses.
It also has deterministic schema-generated modes that construct small
verifier-valid integer programs or intentionally invalid ASR directly, without
using the Fortran frontend.
The invalid generators cover three families, chosen because they are the two things a frontend most often gets wrong and the one thing that makes separate compilation worth having:
references that do not resolve: a name read from a scope that does not contain it, a call to something that is not a procedure, a type-bound procedure that names a variable, an import from a module that is not there. This is the ASR a frontend produces when a symbol was never imported;
calls that disagree with the procedure they call: actual arguments whose type, kind, rank or count the callee never declared, including through a type-bound call, and a call statement naming a function;
type-bound procedure overrides that do not conform: an extending type whose binding takes different arguments, or returns differently, from the binding it overrides.
python tests/asr/fuzz.py \
--lfortran src/bin/lfortran \
--seed 1234 \
--cases 1000 \
--generator all
Each case first runs initial verification. A verifier rejection is accepted;
verifier-valid ASR must then emit both an object and an executable with
post-pass verification enabled. Timeouts, signals, parser failures, pass
verification failures, LLVM failures, object failures, and link failures are
persisted under asr-fuzz-artifacts/ as the exact ASR input plus JSON metadata.
The metadata records the source integration test, random seed, case index,
mutation, input hashes, failing phase, commands, and output.
Each campaign also writes coverage.json containing the ASR constructors,
enum values, verifier rules, passes, mutation classes, seed sources, outcomes,
and failure phases observed during the run.
Persisted failures can be replayed without regenerating or mutating the seed:
python tests/asr/fuzz.py \
--lfortran src/bin/lfortran \
--replay asr-fuzz-artifacts/failure-000000-....json
The structural reducer greedily removes vector elements and symbol-table
entries, replaces optional fields with nil, and shrinks numeric values while
requiring the same initial-verification status and normalized failure phase:
python tests/asr/reduce.py \
--lfortran src/bin/lfortran \
--metadata asr-fuzz-artifacts/failure-000000-....json
A verifier rejection is an accepted outcome for the fuzzer, but it is worth
reducing too, since that is what a tests/asr/verify/ fixture pins. Pass
--input a .asr document instead of a fuzzer artifact and the reducer keeps
the rejection’s diagnostic code fixed rather than a failure phase:
python tests/asr/reduce.py \
--lfortran src/bin/lfortran \
--input rejected.asr
It writes a canonical .min.asr file and reduction journal without modifying
the original failure artifact. The reduced file is written in the indented
positional form the committed fixtures use, so it can be added to
tests/asr/verify/ directly.
Running a campaign locally¶
The registered CTest cases are smoke tests: a handful of cases, so the harness cannot rot as the passes change. A real campaign is run by hand, and is worth running against a sanitizer build, where a mutation that corrupts memory is reported at the point of corruption rather than as a later crash:
cmake -S . -B build-asan -G Ninja -DCMAKE_BUILD_TYPE=Debug -DWITH_LLVM=ON \
-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined"
cmake --build build-asan -j
python tests/asr/fuzz.py \
--lfortran build-asan/src/bin/lfortran \
--seed 0 \
--cases 1000 \
--generator all \
--strategy mixed
Campaigns are deliberately not scheduled in CI: a finding needs a person to triage it, and a scheduled job with no owner turns red and stops meaning anything. Run one when the ASR, the passes or the verifier change in a way worth stress testing.
tests/asr/check_llvm_coverage.py compares every constructor generated from
ASR.asdl with the LLVM visitor and
tests/asr/llvm_constructor_coverage.toml. A constructor must either have a
direct LLVM visitor or an explicit classification naming its lowering pass,
helper path, metadata role, or non-executable status. New unclassified
constructors fail the registered CTest.