Sprint 32 foundation. Replaces the bare ~7-flag parser with the full
sprint-spec surface and threads each flag through to where it has
real effect.
New Options fields cover: --std=fNN, -ffree-form/-ffixed-form,
-fdefault-{integer,real}-8, -fimplicit-none, -frecursive, -fbackslash,
-fmax-stack-var-size=N, -Wall/-Wextra/-Wpedantic/-Wdeprecated/-Werror/
-Wno-name (unknown -Wfoo accepted silently per gfortran convention),
-g (accepted, DWARF deferred to sprint 35), -v / --verbose,
--time-report, --diagnostics-format={text,json},
-fcheck=bounds / =all, -J <dir>, -L <dir>, -l<lib>, -rpath, -shared,
-static, --emit-ast, --emit-tokens, --diagnostics-format=, plus
--version / --help / -dumpversion / -V info actions and @file
response-file expansion.
Wired through:
- --std= → validate_file_with_layouts(.., opts.std, ..)
- -ffree-form / -ffixed-form → source-form override before lex
- -J → .amod write directory (default still parent of -o output)
- -L / -l / -rpath / -shared / -static → push_link_flags() for both
link() and link_multi()
- -v → eprintln'd phase markers
- --time-report → PhaseTimer collects per-phase durations and prints
a Phase/Time(ms)/% table at end of compile()
- -Werror → promotes warning diagnostics to fatal in compile()
The binary alias now lives at src/bin/afs.rs as a one-line
wrapper that calls into a new shared armfortas::cli_entry() (so
cargo doesn't warn about a duplicated bin source path). Both
binaries build from the same logic and pass --version / --help.
tests/cli_driver.rs exercises 14 user-visible behaviours via
subprocess invocation: --version output, --help, -dumpversion,
afs alias, no-arg help-to-stderr, -c, -S, -E (with macro), --std=f95
rejecting ERROR STOP, response file, -J directory, -v phase
streaming, --time-report table, missing-input → exit 3.
Two narrow blockers stood between the existing i128 codegen and the
15 ignored test cases.
The i128_backend_o0_supported gate rejected any Load whose result
type was Ptr(Int(I128)) because type_contains_i128 walks through
Ptr<>. But the loaded value is a normal 64-bit pointer and the
wide-slot machinery is uninvolved — only the value type itself
matters. Add a Load arm that always accepts pointer-typed loads.
The collect_host_references walker built sub_locals from a contained
proc's args + decls but skipped the function's result(r) clause
name. A program with integer(16) :: r at host scope and a contained
function 'function add5(...) result(r)' then captured its OWN result
variable as a host-ref. The lowering appended a trailing ptr<i128>
closure-passing arg, and the function body faithfully wrote its sum
through that pointer instead of into the local result alloca — so
the i128 return value was uninitialised garbage. Seed sub_locals
with the implicit subprogram name and any result-clause name.
After both fixes, all four i128_stack_args tests, all five
i128_formatted_read tests, and all six i128_internal_io tests pass.
The 'backend does not yet support integer(16)' driver gate no longer
trips for any of them.
Harvesting the auditor's amod/derived_fn_noassign.f90 surfaced a new
bug: 'add_t(a, b)%x' inline (i.e. without first assigning the call to
a variable) printed zero. resolve_component_base only handled
Expr::Name and Expr::ComponentAccess bases; a FunctionCall base fell
through and the ComponentAccess arm reached its const_int(0)
fallback. Add a callee_return_derived_type_name helper that walks
the symbol table for the callee's result-variable derived type, and
extend the ComponentAccess arm in lower_expr_full to lower the call
when the base is a function returning a derived type and use its
result pointer as the component base.
Also lift four canonical brutal tests out of the auditor's scratch:
- derived_fn_inline (the inline-component-of-function-result case)
- op_single (cross-module operator(+) on derived type)
- assn_cross_module (cross-module assignment(=))
- final_cross_module (BLOCK-scope finalizer found via cross-module use)
The sprint 31 cluster fix for ALLOCATE-on-component and
pointer-assign-on-component (findings 8-11) short-circuited the
attribute check whenever the target selected into a component,
because FieldLayout didn't carry per-component attributes and the
base variable's attribute was the wrong thing to check. That was a
half-fix: a malformed 'allocate(obj%not_alloc_field)' or
'obj%plain_field => x' would compile silently and either miscompile
or blow up at runtime. Complete it properly.
- FieldLayout gains allocatable / pointer / target bools populated
from the component decl's attrs.
- .amod writer/reader emit '@allocatable' / '@pointer' / '@target'
suffix flags so cross-TU consumers see the same metadata;
backward-compatible with older files lacking the flags.
- validate_file_with_layouts threads the registry into Ctx.
- New leaf_field_layout helper walks a ComponentAccess chain down
to its terminal field and also returns whether any ancestor on
the path carries TARGET / ALLOCATABLE, so F2018 §8.5.14
(subobject of a TARGET is itself a valid target) is honoured for
pointer-assign-source checks.
- validate_allocatable_item and validate_pointer_assignment now
check the leaf's own attributes (with ancestor fallback for the
source-must-be-target rule) instead of skipping outright.
- Two new ERROR_EXPECTED negative tests exercise the ALLOCATE and
pointer-assignment LHS paths so regressions in the per-field
plumbing surface immediately.
F2018 §11.1.4 gives a BLOCK construct its own implicit-typing
environment. An outer scope that is IMPLICIT NONE plus a
block-local 'implicit integer (i-n)' should let i..n be
implicitly typed inside the block, but the AST dropped the
parsed IMPLICIT decls, the IMPLICIT NONE walker treated the
block body as an extension of the outer scope, and the lowerer
never allocated implicitly-typed locals.
- Stmt::Block carries an 'implicit: Vec<SpannedDecl>' field,
populated by the parser instead of being discarded.
- check_implicit_none / walk_stmt_for_undeclared / check_expr_names
thread an 'implicit_letters' set through the walk; the BLOCK arm
layers in the local letters (and an inner IMPLICIT NONE clears
the inherited set per the standard).
- The Stmt::Block lowering pre-walks the body, gathers any names
it references that aren't already declared and whose first
letter falls in a covered range, and synthesises TypeDecls so
alloc_decls / init_decls allocate them with the implicit type.
- New collect_referenced_names visitor walks expressions across
the body's statement forms.
Removes the XFAIL on test_programs/audit31_brutal_implicit_block.f90.
The previous fix used PID in the temp basename, but each
compile_binary call from a parallel test spawns a fresh subprocess
with a different PID, so two back-to-back compiles of the same
output path embedded different /tmp/armfortas_<pid>.o strings into
the linked binary's OSO debug stab — breaking
linked_binary_is_deterministic_for_same_output_path_and_has_no_uuid.
Stripping PID entirely fixed determinism but reintroduced the
parallel-multifile race because two tests compiling files with the
same basename to different unique-dir outputs collided on
/tmp/armfortas_<stem>.s/.o.
Use a stable FNV-1a 64 hash of the full output path: same path →
same temp .o (deterministic across subprocess invocations), and
different paths → different temp .o (parallel-safe).
DefaultHasher's per-process random seed would have defeated (1).
A submodule providing implementations for its parent module's
interface block parsed cleanly but produced nothing in the object
file: lower_unit's ProgramUnit::Submodule arm fell through to the
catch-all and never visited contains. Linker reported 'Undefined
symbols: _compute' for any program that called the function the
submodule was meant to implement.
- lower_unit::ProgramUnit::Submodule now mirrors the Module arm,
using the parent module's name as the host for the contained
procedures.
- Pass 1 (collect_module_globals) installs submodule decls under
the parent module's name so contains procs resolve them through
the same global lookup the parent uses.
- collect_internal_func_names, collect_alloc_return_funcs,
collect_char_len_star_params, collect_optional_params,
collect_descriptor_params, collect_elemental_funcs, and
walk_contained_host_refs_inner all now descend into Submodule
contains.
The macro expander runs per-line and rediscovers in-string state
each call. If the trailing & sat outside any string,
find_code_trailing_ampersand caught it and the preprocessor joined
the lines before expansion; if the & sat inside an unterminated
string, the function returned None, so line N+1 was expanded fresh
and a leading ! was treated as a Fortran comment — chewing through
the closing quote and producing 'unterminated string literal'.
Sprint 31 #470 fixed the single-line counterpart; this finishes the
multi-line case. Extend the scanner to track potential trailing & 's
seen while in_string is still set.
The driver wrote intermediate assembly and object files to
/tmp/armfortas_<pid>.s and .o using just the PID. cargo test runs
multiple test threads inside one binary, so two compilations from the
same process collided on the same temp paths — one thread's link
could pick up another thread's stale .o. Surfaced as an intermittent
"linked binary should be byte-identical" failure on CI under load.
Add a per-process atomic counter to the temp-file basenames.
F2018 §7.5.6.3 / §9.7.3.2: derived-type vars declared in a BLOCK must
have their FINAL procs called, and block-scoped allocatables must be
deallocated, when control leaves the block. The Stmt::Block lowering
just restored shadowed outer locals and returned, so program-scope
finalization worked but block-scope didn't. Gather the keys the block
newly introduced and run insert_implicit_dealloc on them before the
restore step.
Finding 5: function foo() result(r) with 'type(t) :: r' in body left
the result var without a derived_type tag because the lowering only
looked at the header's explicit return type. New
derived_type_name_for_result_var checks both the header and the
body's result-var decl so the result allocates a struct buffer and
component assignments land on it.
Finding 6: lower_expr_full did not consult the descriptor_params
mask (only Stmt::Call did), so FUNCTION calls taking xs(:) got raw
pointers and size(xs) read zero. Thread descriptor_params as an
optional arg through lower_expr_full and emit lower_arg_descriptor
for any callee param flagged as descriptor-backed.
audit31 Finding 4: `inner` inside `outer` inside `program`
lost its write to `host_var` because the closure-passing walker
only collected refs against the IMMEDIATE host. Two missing pieces:
1. walk_contained_host_refs only saw `outer.decls` when analysing
`inner`, never the program's decls. Refactor to thread an
accumulated ancestor chain and try each layer against
collect_host_references — the nested proc's refs list now
includes every host-var it needs, regardless of depth.
2. The intermediate level (`outer`) had no refs of its own, so
at the outer → inner call site `append_host_closure_args` had
nothing to forward and pushed a NULL for host_var. Fold each
proc's nested-contained refs into its own (filtered to names
that live in an ancestor scope, to avoid pulling in
outer-local names). `outer` now carries host_var as a hidden
param, receives it from the program, and forwards it to inner.
3. lower_unit's contains loop now passes `decls + inherited
host_decls` as the child's host_decls so
build_host_ref_params can resolve the host-ref type two
(or more) scopes up.
Reproducer: test_programs/audit31_brutal_nested_host.f90 prints
`final host_var= 20` (was 10). Task #485.
Two audit31 findings, both caught by the tightened IR verifier:
Finding 13 — .and./.or. on a derived-type LOGICAL component
tripped "operands must be Bool (i8 / bool)". The field loads as
i8 (its stored byte), and coerce_to_type's Int→Bool arm was
emitting `int_trunc` which produces a value of type i8, not Bool.
Rework Int→Bool to widen (if needed) and `icmp ne 0` so the
result actually IS a Bool.
Finding 14 — `iand(val_c_long, int(z'400', c_int))` tripped
"bitwise op: operand width mismatch i64 vs i32". F2018
§16.9.104 allows iand/ior/ieor across integer kinds; the verifier
(rightly) doesn't. Add unify_int_widths that sign-extends the
narrower operand to the wider, and route iand/ior/ieor through
it before calling bit_and/or/xor.
Verified: fortsh/src/common/memory_profiler.f90 now compiles.
Tests: audit31_brutal_logical_i8.f90 (CHECK T),
audit31_brutal_mixed_kind_bitwise.f90 (CHECK 768).
Tasks #494, #495.
IntExtend isel unconditionally emitted SXTW Xd, Wn regardless of
source width or dest class. For any instruction whose target type
was i32 or narrower, dest landed in Gp32 (a W register) and the
assembler rightly rejected `sxtw Wd, Wn` — SXTW's dest must be
64-bit. The audit (Finding 12) hit this on fortsh io/suggestions.f90
and parsing/lexer.f90.
Rework the InstKind::IntExtend arm:
- Look up the source IR type via func.value_type to pick
SXTB/SXTH/SXTW (matching source byte width), or MOV for the
same-width and narrowing-disguised-as-extend cases that the
upstream passes sometimes produce.
- Force dest register class to Gp64 when widening to i64 so
SXTW always gets its required X-register dest; otherwise
follow the declared target type.
Add Sxth and Sxtb opcodes to mir + emit. All three sign-extends
take identical two-operand syntax.
Verified: fortsh/src/io/suggestions.f90 now compiles to an object.
Task #493.
Cluster-fix for audit31 findings 8, 9, 10: three validate_*
checks that all ran extract_base_name on their target, then
asserted the BASE carried the pointer/allocatable attribute. For
component-access targets (`pools(i)%tokens(n)`, `ref%data =>`,
`cb => proc`), the real attribute lives on the final component
(or the dummy procedure), not on the base — the validators
reported the wrong entity and rejected valid code.
We don't track per-field attributes in the sema type registry
today, so in the short term the validators skip the base-name
check whenever the expression selects into a component. The
lowering path still requires the leaf to be allocatable/pointer
(bad code surfaces as a verifier or runtime error instead of a
silent miscompile). For the proc-ptr case, allow symbols that
carry attrs.external through (that's how
`procedure(iface) :: proc` parses).
Also drops an unused enumerate() index in install_runtime_dim_bounds
that clippy flagged after the Finding 2 fix.
Reproducers: audit31_brutal_alloc_component, audit31_brutal_ptr_substr,
audit31_brutal_proc_ptr_dummy — all three now compile and print
'ok'. Tasks #489, #490, #491.
`call sub(b=10, a=20)` bound positionally — a=10, b=20 — because
both Stmt::Call and Expr::FunctionCall iterated the actual arg list
verbatim and ignored the `keyword` field. Silent wrong-value bug
affecting any fortsh file that uses keyword args (variables,
readline, printf_builtin).
Add reorder_args_by_keyword: fast path when no keyword is present;
otherwise look up the callee's arg_order from the symbol table and
place each actual into its matching slot — positional actuals fill
0..K, then keyword actuals index by name. Unknown keywords append
to preserve error locality so sema still reports them.
Call the reorderer right before arg lowering at both sites; all
downstream logic (descriptor mask, OPTIONAL pad, hidden character
lengths, host-closure args) then runs against the reordered list
without further changes.
Reproducer: test_programs/audit31_brutal_keyword_args.f90 now
prints a=20 b=10. Task #482.
`character(len=N) :: s = 'hello'` silently left the stack buffer
zero-initialised: init_decls had a catch-all that `continue`d for
any char_kind != None, on the theory that character initializers
were "handled elsewhere". There was no elsewhere for the
fixed-length case. fortsh has ~200 such parameters and every one
was blank at runtime (audit31 Finding 3).
Before the generic non-plain-scalar skip, check for CharKind::Fixed
and emit an afs_assign_char_fixed call that copies the literal
bytes into info.addr with space-padding to the declared length.
Deferred-length and assumed-length cases still take their existing
code paths (afs_assign_char_deferred / hidden-length param).
Reproducer: test_programs/audit31_brutal_char_init.f90 now
prints `hello` and `world` instead of blanks. Task #484.
The auditor reported "F2003 IMPORT statement in interface body
unparsed" as Finding 7, with `import :: c_int` triggering
"parse error: expected expression, got ::". The IMPORT parse
path was actually fine; the real cause is upstream.
parse_function scanned for RESULT first, then BIND. A header
like `function foo(x) bind(C, name="foo") result(r)` left
`result(r)` unconsumed, parse_unit_body entered phase 1 (USE)
with `result` as the first token, skipped through every phase
including Phase 1.5's IMPORT recognition, and landed in Phase 3
parsing `result(r)` as an expression; then the IMPORT line
showed up and `::` blew up the expression parser.
F2008 R1229 allows both orderings (`R BIND` or `BIND R`).
Loop over the two clauses until neither matches, so the header
parser consumes both regardless of order. Same fix unblocks every
fortsh iso_c_binding file whose interfaces used the swapped form.
Reproducer: test_programs/audit31_brutal_import_stmt.f90 now
compiles and prints `ok`. Task #488.
arg_dims_from_decls only handles compile-time bounds; when the
upper bound of an explicit-shape dummy is another dummy argument
(e.g. `subroutine s(xs, n); integer :: xs(n)`), it fell back to
(1, 1) and every bounds check on xs reported "index K outside
[1, 1]". This blocks every BLAS/LAPACK-shaped signature, the
cross-opt stress harness, and roughly 40% of fortsh call patterns.
Add an install_runtime_dim_bounds pass that, right after the
normal param spill slots land, walks every by_ref dummy whose
Explicit-shape bound can't be const-folded AND references an
already-registered dummy. It lowers the bound expression, widens
to i64, and stashes the SSA id on LocalInfo.runtime_dim_upper.
Pass visible_param_consts through so module/host PARAMETERS
stay on the static path (no false positives — realworld_seed_overwrite
uses `xs(n)` where n is a program-scope PARAMETER; arg_dims_from_decls
resolves that fine and we must not overwrite).
compute_flat_elem_offset's static-shape path now carries a
dual static-OR-dynamic cumulative stride. Each dim checks
runtime_dim_upper first: Some → runtime bounds check + dynamic
stride; None → const values as before. Once we hit a dynamic
stride, subsequent dims stay on the dynamic path.
Reproducer: test_programs/audit31_brutal_explicit_shape_bounds.f90
now passes (CHECK: 150).
Harvest: 12 other audit31_brutal_*.f90 reproducers added with
XFAIL annotations keyed to tasks #482-#499 so run_programs keeps
them visible as known-broken, not silently missing. Cross-opt
harness (lib + main) moved to tests/fixtures/audit31_crossopt/
since they need multifile linkage, not single-file run_programs.
Known-deferred i128 footprint: unchanged.
Surfacing CI gaps per the last push brought three categories of
failure to the surface:
1. Clippy: collapsed a nested-if in parser/unit.rs and dropped a
needless borrow in sema/validate.rs so -D warnings passes.
2. i128 tests: 11 test functions across i128_formatted_read.rs,
i128_internal_io.rs, and i128_stack_args.rs exercise code paths
the backend explicitly rejects today ("backend does not yet
support integer(16) / i128 codegen"). The feature is tracked in
.docs/noted_issues.md and task #475. Mark these with
#[ignore = "...task #475"] so they stay visible in test output
(as ignored, not failing) until the backend lands.
3. IPO tests (ipo_const_arg, ipo_dead_arg): both assertions looked
for renamed specialized callees ("@func_") but the current
passes specialize and dead-arg-elim in-place — no cloning, no
rename. Verified the passes produce semantically correct IR and
runtime output (55/57 for ipo_const_arg, 6 for ipo_dead_arg),
then aligned the test with the actual correct-by-design behavior.
The old assertions described a cloning design that was never
implemented; renaming them would be a follow-up, not a bugfix.
Unused _obj1/_obj2 in tests/incremental.rs: underscore-prefix so
the warnings don't mask real failures.
test-armfortas was calling `cargo test -p armfortas --bin armfortas`,
which only tests the binary crate — it has zero tests. The 981
library unit tests (IR, sema, opt, parser, lexer, codegen, runtime
stubs) never ran in CI. Change to --lib --release so regressions
that only surface at -O2 or above don't slip past.
test-end-to-end ran only run_programs, covering the O0..Ofast sweep
across 290 Fortran programs but skipping every other integration
target — audit3_reproducers, multifile, cross_opt_abi_matrix,
module_host_audit, ipo_*, pure_*, licm_lsf_audit_29_11,
sroa_shape_audit_29_11, mem2reg, vectorize_do_loop, incremental,
generated_chain_tests, sprint29_audit_realworld, and so on.
Add test-integration that runs `cargo test -p armfortas --tests`
to cover every tests/*.rs target. Paying the cost of compiling the
compiler once is cheaper than maintaining 20+ per-target jobs, and
keeping them together means a regression in any suite blocks the
merge.
A `use m, only: a => add` statement installs a UseAssociation with
local_name="a" and original_name="add" on the importing scope —
no symbol named "a" is actually defined. Direct-lookup callers
(notably resolve_generic_call, used by every call site) missed the
rename and reported the generic name as unresolved, producing
`linker: _a undefined`.
Add a second-pass scan: after the direct symbol lookup fails, walk
every scope's UseAssociations and follow the first matching rename
back to the original symbol in its source scope. This mirrors
lookup_in's chain-following behaviour.
Three related fixes so chained `a + b + c` on type(vec) operands
works through a user-defined operator(+) interface:
- derived_type_name_for_return extracts the struct name from a
function's return_type. Functions returning a type(T) now allocate
a struct-shaped [i8 x size] buffer and register the result variable
with derived_type = Some(name), so component-access assignments
(`vec_add%x = ...`) land on the buffer instead of silently no-oping.
The return emits a zero-offset GEP of the buffer as a Ptr(i8).
- type_info_to_ir_type returns Ptr(i8) for TypeInfo::Derived instead
of collapsing to an integer of the struct's byte size. Without
this, callee_return_ir_type reported an i32/i64 for a derived-type
result, the caller fed that into iadd as if it were numeric, and
the verifier rightfully rejected pointer-typed operands.
- arg_matches_declared accepts either Ptr(Array(i8, N)) or Ptr(i8)
as a match for a declared Derived(T) argument. Function results
now carry the plain byte-pointer form while local allocas keep
the sized-array form; both encode the same struct reference and
the sema layer already guards cross-type derived-type assignments.
Add audit-level tests:
- audit31_operator_chained.f90 (chained operator + derived result)
- audit31_host_array_in_function.f90 (host-array access through
contained FUNCTION's closure-passing ABI)
- audit31_generic_three_specific.f90 (integer(4)/integer(8) dispatch)
- audit31_implicit_none_in_module.f90 (implicit none inside module)
`procedure(iface), pointer :: f, g` only registered `f`; the parser
treated the entity name as a singleton and the next iteration of
the decls loop consumed the comma as a statement boundary, surfacing
as "expected expression, got ,".
Loop on commas to collect every entity, each with its own optional
`=> null()` initializer, and emit a single TypeDecl with the full
entity list — matching the shape that ordinary type declarations
already use. The interface name and the rest of the procedure-pointer
ABI work remain deferred.
Function::value_type returned None on cache miss, and
check_type_consistency silently skipped every check when either
operand had no entry. Optimiser passes that mutate IR without
calling rebuild_type_cache could ship width-mismatched iadds and
pointee-mismatched stores undetected.
- value_type now falls back to scanning params and instructions
for an authoritative `ty` field; the cache stays as a perf
optimisation but is no longer load-bearing for correctness.
- verify_function gains check 3a: every defined value must have a
cache entry. A miss points at the missing rebuild_type_cache
call rather than the downstream codegen blow-up.
- check_type_consistency reports missing operand types for
integer ops so the cause-and-effect chain stays visible if a
future refactor breaks the fallback.
- New test type_consistency_survives_stale_cache builds a function
by hand (no builder, no cache update) and asserts the verifier
still rejects width-mismatched iadd.