A Fortran compiler for ARM64 and x86_64. No borrowed frontends, no LLVM, no GCC. Every stage from lexing to machine code is ours.
fortsh is a ~57,000-line Fortran 2018 shell. When we went to compile it on Apple Silicon we found out that Fortran on ARM64 is, charitably, underserved:
- gfortran has at least seven confirmed critical bugs on macOS ARM64, most of them involving allocatable strings — the exact feature fortsh leans on hardest. The bugs live in GCC's backend and the GCC team's queue for ARM64 Fortran is not short.
- flang-new (LLVM) works around the gfortran bugs but introduces its own, particularly around C interop and derived type layout. It also requires a separate Homebrew installation and the binary is called
flang-newfor reasons. - Both compilers are millions of lines of code we don't own. When something breaks in a corner of AArch64 AAPCS64 that nobody expected a Fortran program to reach, "read the source and fix it" is not a realistic option.
The solution was to write a compiler that runs on the machine we actually have, that we can fix when it breaks.
Active development. The full pipeline — preprocessor through object emission — works on four targets:
arm64-macos(Mach-O, Apple AAPCS64) — the original platformx86_64-linux-gnu,x86_64-linux-musl,x86_64-freebsd(ELF, SysV AMD64)
Pipeline: Source → Preprocessor → Lexer → Parser → AST →
Sema → SSA IR → Optimizations → ARM64 / x86_64 Codegen →
afs-as → .o (Mach-O / ELF) → ld → Binary
afs-as assembles both architectures and emits both object formats in-process; no system as in the default path anywhere. The system linker (ld) is the last delegated component, and afs-ld is replacing it: set AFS_LD=1 (or AFS_LD_PATH) to link ELF and Mach-O binaries with our own linker. Real programs — up to and including fpm building itself to a byte-identical fixed point — compile and run.
git clone --recurse-submodules https://github.com/FortranGoingOnForty/armfortas.git
cd armfortas
cargo build --workspace # compiler + assembler + runtime
cargo test --workspace # full test suite
cargo clippy --workspace # lintOnce built:
target/debug/armfortas hello.f90 -o hello # compile and link
target/debug/armfortas -c module.f90 # compile to object
target/debug/armfortas -S hello.f90 # emit assembly
target/debug/armfortas --emit-ir hello.f90 # emit IR
target/debug/armfortas --target x86_64-linux-gnu -c hello.f90 # cross-compile to objectOn Linux hosts the driver probes the GCC directories for the crt objects
it needs to link (crtbeginS.o etc.). If your distro's GCC lives somewhere
the probe doesn't know (it knows Debian and RedHat layouts), point
AFS_CRT_DIR at the directory containing them, e.g.
AFS_CRT_DIR=/usr/lib/gcc/x86_64-pc-linux-gnu/16 on Arch.
- Free-form and fixed-form source
- All numeric types:
integer,real,double precision,logical,character - Complex arithmetic (storage and arithmetic operations; some intrinsics pending)
- Derived types with component access, type extension (
EXTENDS), and type-bound procedures withPASS/NOPASS FINALproceduresSELECT TYPEwithTYPE ISandCLASS ISguards- Polymorphic dispatch through
CLASSvariables via a single per-type constant vtable: each type with bound procedures emits one_afs_vtable_<module>_<type>table (type tag, parent-vtable pointer, then bindings in declaration order, parent slots first), and a dispatch is load-table → load-slot → indirect-call. Overrides reuse the parent's slot, so dispatch works across translation units that never saw the type's source, including on polymorphic array elements. ALLOCATABLEscalars and arrays, including allocatable character stringsPOINTERandTARGETattributesOPTIONALarguments withPRESENT()intrinsic- Full array sections and whole-array expressions
WHERE/FORALLconstructsDO,DO WHILE,DO CONCURRENTwith locality specsSELECT CASEon integer, character, and logicalASSOCIATEandBLOCKconstructsGOTOand labeled statementsEQUIVALENCEandCOMMONblocksNAMELISTI/OSAVEattribute with correct static storageVALUEattribute for pass-by-value (BIND(C))RECURSIVEfunctions and subroutines- Generic procedures and interfaces
- Operator overloading
- Statement functions
- Arithmetic IF
STOP/ERROR STOPwith stop codes
Full iso_c_binding module: kind parameters (C_INT, C_DOUBLE, C_CHAR, etc.), C_PTR, C_NULL_PTR, C_LOC, C_FUNPTR, BIND(C) procedures with correct ABI including VALUE argument dispatch.
Value-class inquiry and construction (ieee_is_nan/finite/normal, ieee_class, ieee_value, ieee_unordered, ieee_copy_sign, ieee_logb, ieee_rint, ieee_scalb, ieee_next_after) via bit-pattern runtime helpers, so NaN detection survives -Ofast. Rounding-mode and exception-flag get/set reach the hardware control word (FPCR/FPSR on ARM64, MXCSR on x86_64); GVN/CSE will not merge rounding-dependent FP ops across a mode change. The F2023/IEEE 754-2019 ieee_max/ieee_min(_mag) and *_num(_mag) family is implemented. IEEE_SUPPORT_* answers honestly — underflow_control, halting, and standard report false rather than pretend.
PRINTandWRITEwith format strings and list-directed I/O- List-directed integer output uses gfortran-compatible field widths by kind
READfrom stdin and filesOPEN,CLOSE,INQUIRE,REWIND,BACKSPACE,ENDFILE,FLUSH- Unformatted (binary) I/O
- Stream I/O
- Non-advancing I/O
FORMATstatementsNAMELISTgroups
Mathematical: ABS, SQRT, EXP, LOG, LOG10, SIN, COS, TAN, ASIN, ACOS, ATAN, ATAN2, SINH, COSH, TANH, MOD, MODULO, SIGN, DIM, FLOOR, CEILING, NINT, INT, REAL, DBLE, MAX, MIN, MAXVAL, MINVAL, SUM, PRODUCT
Array: SIZE, SHAPE, LBOUND, UBOUND, ALLOCATED, ASSOCIATED, RESHAPE, TRANSPOSE, MATMUL, DOT_PRODUCT, PACK, UNPACK, SPREAD, MERGE, COUNT, ANY, ALL
Character: LEN, LEN_TRIM, TRIM, ADJUSTL, ADJUSTR, INDEX, SCAN, VERIFY, CHAR, ICHAR, ACHAR, IACHAR, REPEAT, NEW_LINE
Bit: IAND, IOR, IEOR, NOT, ISHFT, ISHFTC, IBITS, IBSET, IBCLR, BTEST, POPCNT, POPPAR, LEADZ, TRAILZ
System: SYSTEM_CLOCK, CPU_TIME, DATE_AND_TIME, RANDOM_NUMBER, RANDOM_SEED, COMMAND_ARGUMENT_COUNT, GET_COMMAND_ARGUMENT, GET_COMMAND, GET_ENVIRONMENT_VARIABLE
Inquiry: KIND, SELECTED_INT_KIND, SELECTED_REAL_KIND, HUGE, TINY, EPSILON, PRECISION, RANGE, DIGITS, RADIX, MINEXPONENT, MAXEXPONENT
| Level | Passes |
|---|---|
-O0 |
None (preserve IR exactly) |
-O1 |
mem2reg, constant folding, DCE, basic CSE, copy propagation, small inlining |
-O2 |
-O1 + LICM, strength reduction, DSE, GVN, SROA, loop store forwarding, jump threading, IPO (const-arg, dead-arg, return-prop) |
-Os |
-O2 but prefer code size: no unrolling, less inlining |
-O3 |
-O2 + aggressive inlining, loop unrolling/interchange, vectorization (NEON on arm64, SSE2 on x86_64) |
-Ofast |
-O3 + fast-math (reassociation, multiply-add contraction, no NaN/Inf assumptions, reciprocal) |
The x86_64 vectorizer is capped at SSE2 — the architectural baseline. CI fails on any SSE3+/AVX mnemonic (and on any x87 instruction at any level).
Floating-point multiply-add contraction is disabled at -O0, -O1, -O2,
-Os, and -O3. -Ofast may emit fused instructions on targets that provide
them; baseline x86_64 remains SSE2 and therefore keeps separate operations.
Correctness invariant: every program that produces correct output at -O0
must produce identical output at -O1, -O2, -Os, and -O3. -Ofast may
differ only where its documented fast-math policy applies. This is enforced by
the end-to-end test suite at every level.
iso_c_binding and iso_fortran_env are built-in and always available. Authored modules compile correctly. Multi-file module dependency resolution works: the driver scans MODULE/USE/SUBMODULE statements, topologically orders an unordered file list, and emits/consumes .amod files (-J/-I in the gfortran dialect — fpm drives armfortas as a backend compiler unmodified, and fpm built by armfortas rebuilds itself byte-identically).
Submodules (F2008) work: separate module procedure bodies (both the module procedure NAME and module function/module subroutine prefix forms), nested submodule trees, submodule host association (a submodule sees the parent's PRIVATE entities), and SMPs as type-bound-procedure targets or behind generic interfaces. The multi-source driver topologically orders submodules after their parents, so an unordered file list compiles in one invocation; interface/implementation mismatches and unknown-parent submodules are compile-time errors. Scope note: the dependency scanner is line-based, so a SUBMODULE statement split across continuation lines is not recognized (same limitation as the MODULE/USE scan).
(Refreshed 2026-07-04, l10 — every entry on the previous list had
shipped: complex intrinsics, >32KB frames via heap promotion, the
NEON/SSE vectorizers, inlining, multi-file .amod builds, and the
full optimizer pipeline are all live and CI-gated.)
- Coarray Fortran
- C descriptor (
CFI_cdesc_t/ ISO_Fortran_binding.h) interop; declarations that require descriptors, includingBIND(C) CHARACTER(len=*), are rejected - UCS-4 (
ISO_10646) character data —SELECTED_CHAR_KINDanswers 4, but kind-4 character storage and I/O are not implemented - Internal READ whose unit is a whole character array — rejected loudly; read elements individually (the WRITE side does record-per-element)
- Parameterized derived types beyond what the target projects require
armfortas/
├── afs-as/ Standalone assembler (git submodule)
│ └── src/ ARM64 + x86_64 encoding, .s parsers, Mach-O + ELF emission
├── afs-ld/ Standalone linker (git submodule) — ELF + Mach-O
├── src/
│ ├── preprocess/ Fortran-aware preprocessor (#ifdef, #include, #define)
│ ├── lexer/ Tokenization — free-form + fixed-form
│ ├── parser/ Recursive descent → AST
│ ├── ast/ AST node definitions
│ ├── sema/ Symbol tables, type system, .amod modules, validation
│ ├── ir/ SSA-form IR with block parameters (no phi nodes)
│ ├── opt/ Optimization passes and pass manager
│ ├── target/ Target identity: arch, OS, libc, object format
│ ├── codegen/
│ │ ├── arm64/ ARM64 isel, linear-scan regalloc, peephole, emission
│ │ └── x86/ x86_64 isel, two-address conversion, linear-scan, emission
│ ├── driver/ CLI, compilation orchestration, linking
│ └── runtime/ Runtime interface — I/O, intrinsics, memory management
├── bencch/ Compiler benchmark and test harness (git submodule)
├── test_programs/ ~660 end-to-end test programs with CHECK annotations
└── runtime/ libarmfortas_rt source
No LLVM. gfortran's bugs are in GCC's backend. flang's bugs are in LLVM's frontend lowering. Using either as a backend would mean inheriting the bugs we're trying to escape. We own every pass.
SSA IR with block parameters. Instead of phi nodes, blocks carry typed parameters. Cleaner to construct, easier to verify, simpler to transform. The mem2reg pass promotes stack allocas to SSA values using iterated dominance frontiers (Cytron et al.).
The platform ABI strictly. On ARM64, Apple AAPCS64: 16-byte stack alignment always, x18 reserved, x29/x30 saved in prologue, frame pointer maintained. On x86_64, SysV AMD64: integer/SSE register classes, red zone rules, stack-passed overflow args. We've been bitten by every one of these constraints and handle them correctly.
Host is not target. TargetSpec::host() in src/target.rs is the only code in the workspace allowed to read cfg!(target_*). Everything else takes a TargetSpec value, so --target x86_64-linux-gnu -c works from any host.
Array descriptors. {base_addr, elem_size, rank, flags, dims[15]}. Our ABI — stable across releases.
String descriptors. {data, len, capacity, flags}. Deferred-length assignment always allocates new storage before freeing old. This prevents the use-after-free that causes gfortran's ARM64 allocatable string crashes.
Large arrays on heap. Stack threshold at 64KB. Prevents the stack corruption gfortran exhibits with arrays over ~600KB.
afs-as is the standalone assembler — ARM64 and x86_64 (AT&T subset), Mach-O and ELF. It knows nothing about Fortran — clean API boundary. It runs in-process by default and is validated byte-for-byte against the system assembler over the whole test corpus. afs-ld is the standalone linker on the same terms; the driver uses the system ld by default and switches to afs-ld under AFS_LD=1.
cargo test --workspace # all unit + integration tests
cargo test --test run_programs # end-to-end at -O0
cargo test --test run_programs -- --nocapture # verbose output
cargo run -p afs-tests -- run --suite runtime # bencch runtime suite
cargo run -p afs-tests -- run --suite consistency # reproducibility checksThe root armfortas harness is the fast, armfortas-first runner. It compiles
each .f90 file in test_programs/, runs the binary, and evaluates
source-embedded assertions such as:
! CHECK:for stdout! STDERR_CHECK:for runtime stderr! EXIT_CODE:for exact runtime exit status! XFAIL:for known open bugs! ERROR_EXPECTED:for diagnostics that must be emitted! ERROR_SPAN:for exact diagnostic location! ASM_CHECK:/! ASM_NOT:for assembly shape! FILE_CHECK:/! FILE_NOT:for sandbox file side effects! FILE_EXISTS:/! FILE_MISSING:for explicit sandbox presence or absence! FILE_LINE_COUNT:for structural file-shape assertions! FILE_RERUN_MODE:for explicit overwrite vs append intent across reruns! FILE_SET_EXACT:for exact runtime side-effect file sets! REPRO_CHECK:for per-test asm/object/run reproducibility! OPT_EQ:for explicit cross-opt invariants! PHASE_TRIANGULATE:for same-opt IR/ASM/object availability, compile-cleanliness, and compile-only reproducibility oracles! IR_CHECK:/! IR_NOT:for IR shape
Those source comments are the canonical leaf-assertion language for the project. The root harness is where new annotation ideas should land first.
bencch is the structured matrix/reporting/differential runner around that
same testing language. It is best for:
- opt matrices
- differential/reference runs
- module graphs
- capability-aware execution
- reports and bundles
The two surfaces are meant to converge on syntax and expectations, not drift into separate testing dialects.
All root end-to-end tests run at every optimization level (-O0 through
-Ofast). Programs and imported compatibility fixtures with known bugs carry
! XFAIL: annotations whose reason starts with a stable XFAIL-NNN debt ID
from .docs/audits/xfail-debt.md or an X64-O0-NNN sweep finding. They count
as passing until the bug is fixed, at which point CI catches the unexpected
success.
arm64-macos— Apple Silicon (M1–M4), Mach-O, Apple AAPCS64x86_64-linux-gnu/x86_64-linux-musl— ELF, SysV AMD64x86_64-freebsd— ELF, SysV AMD64- Standard: F77 through F2018, building inward from F2018
- Goal: compile real Fortran projects correctly on the machines we actually use — fortsh (a ~63k-line F2018 shell: builds, 3776/3776 POSIX suite), fpm (self-hosts to a byte-identical fixed point), toml-f, test-drive, and the rest of the campaign ladder
Compiling fortsh is a milestone, not the finish line. A complete compiler handles code fortsh never exercises.
This compiler exists because fortsh exists. Building a non-trivial Fortran program on ARM64 and discovering that neither available compiler handles it reliably was the motivation. The goal is a compiler we can fix when it breaks, running on the hardware we actually use.