static — README

static

Check Raku types at compile time using the current compilation unit's RakuAST. Requires Rakudo 2026.09 or newer, with the RakuAST frontend enabled (the standard frontend in these releases). No external dependencies.

use static;
CHECK fail-on-mismatch;

sub message() { 'hello' }
my Int $number = message();
# Compilation fails: expected Int, got Str:D

use static only imports the API. Call one checker explicitly in a CHECK phaser in each compilation unit you want to validate. Run raku -Ilib -c program.raku to check a program without running its mainline. Normal Raku compile-time activity, such as the program's own BEGIN blocks, still happens.

Command line

Check existing files without adding use static or a CHECK phaser:

raku bin/static-check.raku program.raku other.raku
cat program.raku | raku bin/static-check.raku
raku bin/static-check.raku --policy=fail-on-unverifiable program.raku
raku bin/static-check.raku --/check-methods program.raku

With no files, or with - as a filename, the command reads stdin. Each file is compiled independently; imported units are not analyzed. Original file paths and line numbers are preserved. All inputs are checked even when an earlier one fails. Exit status is 0 on success, 1 for compilation/checking or input failures, and 2 for invalid command options. The default policy is fail-on-mismatch; --policy accepts any of the four policies below. --/external-signatures and --/check-methods disable the corresponding checks. Module search paths can be provided through RAKULIB as usual. Interpreter -I search paths are also forwarded to the compiler:

raku -Ilib -I../OtherProject/lib bin/static-check.raku ../OtherProject/lib/Example.rakumod

The command itself also accepts repeated -I and -M options, using attached, separate, or = arguments. -M loads a module and imports its exports into each input unit, just like the interpreter option:

bin/static-check.raku -Ilib -I../OtherProject/lib -MExample program.raku
bin/static-check.raku -I lib -M Example -M AnotherModule program.raku

The command uses compile-only mode: mainline code does not run, but normal compile-time activity (BEGIN, CHECK, imports, etc.) still runs.

Policies

FunctionReportsEffect
warn-on-mismatchProven incompatibilitiesWarnings
fail-on-mismatchProven incompatibilitiesCompilation failure
warn-on-unverifiableIncompatibilities and inconclusive checksWarnings
fail-on-unverifiableIncompatibilities and inconclusive checksCompilation failure

All four accept an optional positional RakuAST::CompUnit (default $*CU) and the Boolean options :external-signatures and :check-methods (both default to True):

CHECK warn-on-unverifiable($*CU, :!external-signatures);

With external signatures enabled, the checker reads available contracts of imported routines and CORE routines. It never traverses their bodies or infers their returns from external ASTs. Disabling the option makes those calls inconclusive. Intrinsic CORE operator rules and control-flow rules remain available.

Method existence is checked by default on known exact class objects and instances, including inherited methods. A missing method is a mismatch. Receivers that may be subclasses, FALLBACK, qualified method names, and optional dispatch (.? / .*) remain inconclusive; the checker does not execute dynamic method providers. Disable the existence check with any policy:

CHECK fail-on-mismatch(:!check-methods);

This option only suppresses missing-method diagnostics. Existing method argument checks remain active, and an unknown method return used in a typed assignment can still be inconclusive under a strict policy.

Diagnostics contain [static:mismatch] or [static:unverifiable], source file/line when available, and the relevant types or reason for uncertainty. Warnings continue compilation and can be handled with Raku's usual warning handlers. Failing modes collect selected problems and register a static::CheckFailure compilation error, whose .diagnostics contains the diagnostic objects. The compiler rejects the unit after the CHECK phasers; the checker does not throw from the phaser itself. The error points at the first offending expression, without a CHECK wrapper or internal backtrace. All checkers return the selected diagnostics as a List. The exception can be imported as CheckFailure with use static :DEFAULT, :types.

Inference

The checker tracks lexical declarations by identity, initializations, assignments, branch joins, bounded loop iteration, calls and return paths. For local routines without an explicit return contract, it collects explicit and implicit returns and combines their types with (@types).are:

sub value(Bool $fraction) {
    $fraction ?? 1/2 !! 1
}
# Inferred ancestor: Real

sub identity($value) { $value }
# Inferred return: Any, from the parameter's default type.
# Calling identity(42) does not specialize the routine's return to Int.

Alternatives are retained internally: losing precision through .are does not by itself prove an incompatibility. At call sites of routines without a return contract, mixed compatible and incompatible inferred returns are inconclusive. For a declared return contract, every alternative in the body is checked: a known incompatible return is a mismatch even if another return fits. Forward calls and mutually recursive routines are resolved through bounded fixed-point iteration; unresolved recursion is inconclusive.

Explicit contracts, including returns, take precedence at call sites and are checked against the local routine's body. The pass does not rewrite the AST, add return annotations, or replace Raku's runtime checks.

Coverage and limits

Supported checks include scalar/attribute initialization, assignments and bindings, routine arguments and defaults, positional/named/optional/slurpy parameters, method calls, multi candidates, and declared returns. Typed array and hash initializations and indexed writes are checked, as are recognized array insertion methods. Nominal inheritance, roles, definedness constraints, parameterized contracts, subsets, coercion types and native contracts are considered. Direct ::T captures are bound across arguments and return contracts; nominally more specific multi candidates are preferred when that ordering is statically established. Typed hash keys and lexical aliases are checked too.

The no-argument CORE .pick and .roll on exact built-in lists and arrays retain the possible element types, including Nil when the collection may be empty. For example, ("bla", 42).pick or ("bla", 42).roll can return Str or Int, so each violates a declared --> Int return contract. Selection is not executed by the analyzer. Count arguments and custom implementations use normal method analysis.

Simple local where/subset predicates are analyzed using primitive constants, comparisons, arithmetic, Boolean combinations, intervals, type junctions, *.chars, *.defined, and known collection lengths. For example:

sub positive(Int $x where * > 0) { $x }
positive(-1);                         # proven mismatch
my Int $x where * > 0 = -1;           # proven mismatch
subset Small of Int where 1..10;
my Small $s = 20;                    # proven mismatch

The analyzer interprets only a bounded allowlist of pure primitive operations. It does not invoke the compiled predicate, arbitrary calls, overloaded operators, regexes, or user coercions.

Container and effect checks also cover:

  • Writes to readonly scalar parameters (including aliases, increments and compound assignments), and writable arguments for is rw. is copy permits local writes; is raw needs runtime container information.

  • Finite positional destructuring declarations and assignments, nested routine sub-signatures, slurpy declaration tails, lexical bindings and recognized array/hash element bindings. Dynamic sources, proxies and more complex binding lifetimes can remain inconclusive.

  • given/when/default paths with known type, literal or Boolean conditions; try, CATCH and CONTROL bodies, including explicit returns and loss of value assumptions after exceptional paths. Arbitrary smartmatch predicates and exact exception selection/resumption are not proven.

  • The elements yielded by gather/take, values of start observed through await or .result, and supply/emit values checked against inline tap callback signatures. Mutable captures use declared contracts rather than current values; deferred writes invalidate caller assumptions. This does not prove scheduling, termination, absence of races, or all reactive pipelines.

  • Hyper method calls on known collections and hyper prefix/infix operators on finite compatible shapes. Unequal shapes, recycling and unknown operands remain inconclusive.

This is a conservative analyzer, not a proof system for arbitrary Raku:

  • Predicates outside that allowlist, or predicates depending on unknown values, are inconclusive even when nominal types fit. Coercion execution remains inconclusive when a conversion is needed.

  • Native integer storage follows Raku wrapping semantics: e.g. assigning 300 to int8 yields 44, not a type error. That stored constant is used in later checks. Floating-point narrowing loses constant precision conservatively.

  • Value-dependent dispatch, argument flattening, indirect calls, dynamic invocants, unsupported AST constructs, and external routines without useful return contracts can be inconclusive.

  • Collection reads may be missing. Recognized mutation, captured assignments, and mutable call arguments invalidate constant-content assumptions while retaining declared element/key contracts. Fixed array dimensions, structured initialization and constant index bounds are checked; dynamic dimensions or indices remain inconclusive. Nested generic resolution and complete alias/effect analysis remain inconclusive. Reflective changes are not modeled as exact contents.

  • Runtime subclass dispatch and mutable captured values can require information unavailable during compilation. Strict modes intentionally report uncertainty.

The unverifiable modes include these limitations; the mismatch modes let inconclusive code compile. The analyzer only examines the explicitly supplied unit. A dependency can independently opt into checking its own unit.

Tests

raku -Ilib t/01-basic.rakutest
prove6 -Ilib t

The suite compiles sample programs in subprocesses and checks both diagnostics and exit status, including precompiled dependencies and non-execution of user predicates and program mainlines. The primitive compatibility matrix is checked against Raku's own nominal type checks. COVERAGE.md lists the covered behaviors and the deliberately inconclusive cases; it is not a claim of exhaustive verification of every possible Raku program.

Maintaining documentation

Edit README.md directly. In dist.ini, [ReadmeFromPod] has enabled = false so Mi6 builds and releases preserve this documentation.

Author and license

Fernando Correa de Oliveira [email protected]. Copyright 2026. Artistic License 2.0.

static v0.0.1

Opt-in compile-time type checking for the current RakuAST compilation unit

Authors

  • Fernando Correa de Oliveira

License

Artistic-2.0

Dependencies

Test Dependencies

Provides

  • static
  • static::Analyzer
  • static::CLI
  • static::Predicates
  • static::Types

The Camelia image is copyright 2009 by Larry Wall. "Raku" is a trademark of the Yet Another Society. All rights reserved.

Built with Podlite — the markup and publishing tools behind this site.