Repository F# setup
open System
open System.IO
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Layers
open Axial.Console
open Axial.FileSystem
open Axial.Hosting
open Axial.Hosting.Browser
open Axial.Hosting.Node
open Axial.PlatformService
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScriptGuardrail diagnostics
Each AXG diagnostic has a narrow contract: the effect or convention it detects, the service or form that replaces
it, and the suppression directive that allows an intentional boundary. The adoption guide covers
install, severity, and suppression choices; this page is the per-diagnostic reference.
AXG001: Use explicit effect services
AXG001 detects direct calls to ambient .NET effects.
| Category | Detects | Use instead |
|---|---|---|
random |
Construction of System.Random |
IRandom, Random.service, or Random.nextDouble |
guid |
Guid.NewGuid() |
IGuid, Guid.service, or Guid.newGuid |
clock |
Ambient date and time properties, Stopwatch, and Task.Delay |
IClock, Clock.service, Clock.utcNow, Clock.timed, Flow.sleep, or Schedule |
environment |
Ambient environment variables, machine or process properties, and TimeZoneInfo.Local |
IEnvironment or a value passed through 'env; an explicit TimeZoneInfo for conversions; Parallelism.ofProcessors for ProcessorCount |
console |
Any System.Console member |
Axial.Console.IConsole |
filesystem |
Any System.IO.File or System.IO.Directory member |
Axial.FileSystem.IFileSystem |
process |
System.Diagnostics.Process.Start |
Axial.Process.IProcess |
sleep |
Thread.Sleep |
Flow.sleep or Schedule |
http |
Construction of HttpClient |
Axial.HttpClient.IHttp, with Http.live over one shared client |
blocking |
Async.RunSynchronously, Task.Wait()/Wait(timeout), task/value-task .Result, and .GetAwaiter().GetResult() outside flows; task .Result and .GetAwaiter().GetResult() inside flows |
Await asynchronously outside flows; let! on the task inside flows |
scope |
A direct Cache.make*, Hub.make/bounded, or Queue.bounded/unbounded returned by a run |
Use the service before the scope closes |
Reads of .Result or .GetAwaiter().GetResult() on a task or value task are exempt when the same value's completion state is tested in the enclosing if branch or when guard, or when the value is the parameter of a ContinueWith callback. Other waits remain findings. Blocking waits outside flows can deadlock UI synchronization contexts. At a genuine synchronous entry point or test boundary, use // axial-allow-effect: blocking.
TimeZoneInfo.ConvertTimeFromUtc(utc, zone) is allowed when zone is supplied explicitly; reading
TimeZoneInfo.Local to supply that argument is not. Pass the desired zone through configuration or 'env.
The analyzer matches resolved System.* symbols, not source text. It does not flag an application type named Random or a local value named now.
Allow an intentional effect boundary
Live service implementations, process entry points, and the scheduler implement explicit effect boundaries.
To allow one call, add a category-specific directive to the flagged line or the line immediately above it:
let live : IClock =
let stopwatch = System.Diagnostics.Stopwatch.StartNew() // axial-allow-effect: clock
{ new IClock with
member _.UtcNow() = DateTimeOffset.UtcNow // axial-allow-effect: clock
member _.Elapsed() = stopwatch.Elapsed
member _.Sleep(delay, token) = Task.Delay(delay, token) }
live: IClockAxial.IClockSupplies wall time, monotonic time, and cancellable delays from one source. Wall time is for timestamps. Use differences between Elapsed readings for durations.
stopwatch: Diagnostics.StopwatchSystemStartNew: unit -> Diagnostics.StopwatchInitializes a new instance, sets the elapsed time property to zero, and starts measuring elapsed time. A that has just begun measuring elapsed time.
DiagnosticsSystem.Diagnostics.StopwatchProvides a set of methods and properties that you can use to accurately measure elapsed time.
_: IClockUtcNow: unit -> DateTimeOffsetSystem.DateTimeOffsetRepresents a point in time, typically expressed as a date and time of day, relative to Coordinated Universal Time (UTC).
UtcNow: DateTimeOffsetGets a object whose date and time are set to the current Coordinated Universal Time (UTC) date and time and whose offset is . An object whose date and time is the current Coordinated Universal Time (UTC) and whose offset is .
Elapsed: unit -> TimeSpanElapsed: TimeSpanGets the total elapsed time measured by the current instance. A read-only representing the total elapsed time measured by the current instance.
Sleep: TimeSpan * CancellationToken -> Taskdelay: TimeSpantoken: CancellationTokenSystem.Threading.Tasks.TaskRepresents an asynchronous operation.
Delay: TimeSpan * CancellationToken -> TaskCreates a cancellable task that completes after a specified time interval. The time span to wait before completing the returned task, or to wait indefinitely. A cancellation token to observe while waiting for the task to complete. A task that represents the time delay. represents a negative time interval other than . -or- The argument's property is greater than . The task has been canceled. The provided has already been disposed.
Requires & types
Requires: open Axial; open System; open System.Threading.Tasks (packages: Axial)val live: IClockTo allow a category throughout a boundary implementation file, place a file directive in the leading comment block immediately before the namespace declaration:
// This file provides the live IConsole implementation.
// axial-allow-effect-file: console
A file directive allows only the named categories. For example, a console directive does not allow System.Random().
Each directive must name a category. There is no directive that allows every effect.
For the exact matching and suppression rules, see EffectCatalog.fs and Suppressions.fs in src/Axial.Guardrails.
AXG002: Remove invalid suppressions
AXG002 validates every axial-allow-effect and axial-allow-effect-file directive.
It reports a directive when the category is unknown or when the directive does not suppress an AXG001 finding. This check catches spelling errors and directives left behind after a refactoring. A directive whose guardrail is turned off suppresses nothing, so it is reported too.
Remove an unused directive. Correct a category only when the associated call is an intentional effect boundary.
AXG003: Return typed failures from workflows
A Flow<'env, 'error, 'value> represents expected failures through its 'error channel. Raising an exception inside flow { } creates an unhandled defect instead, so callers cannot handle it as an expected error.
AXG003 detects direct calls to raise, failwith, failwithf, invalidOp, invalidArg, and reraise inside flow { }. It also checks nested conditionals, matches, bindings, and lambdas.
Use return! Flow.fail error for an expected failure. Use Flow.die when the failure is an unrecoverable defect.
If a nearby try/with catches and translates the exception, add axial-allow-raise to the flagged line or the line immediately above it:
let start (proc: System.Diagnostics.Process) =
if not (proc.Start()) then raise (Exception "Process did not start") // axial-allow-raise
start: Diagnostics.Process -> unitproc: Diagnostics.ProcessSystemSystem.Diagnostics.ProcessProvides access to local and remote processes and enables you to start and stop local system processes.
Diagnostics``not``: bool -> boolNegate a logical value. Not True equals False and not False equals True The value to negate. The result of the negation. not (2 + 2 = 5) // Evaluates to true // not is a function that can be compose with other functions let fileDoesNotExist = System.IO.File.Exists >> not
Start: unit -> boolStarts (or reuses) the process resource that is specified by the property of this component and associates it with the component. if a process resource is started; if no new process resource is started (for example, if an existing process is reused). No file name was specified in the component's . -or- The member of the property is while , , or is . There was an error in opening the associated file. The process object has already been disposed. Method not supported on operating systems without shell support such as Nano Server (.NET Core only).
raise: Exception -> 'TRaises an exception The exception to raise. The result value. open System.IO exception FileNotFoundException of string let readFile (fileName: string) = if not (File.Exists(fileName)) then raise(FileNotFoundException(fileName)) File.ReadAllText(fileName) readFile "/this-file-doest-exist" When executed, raises a FileNotFoundException.
``.ctor``: string -> unitInitializes a new instance of the class with a specified error message. The message that describes the error.
Requires & types
val start: Diagnostics.Process -> unitUse this directive only when the same local boundary catches and translates the exception.
AXG004: Create fresh test fixtures
A module-level let value is initialized once and shared by every test in the module. Mutable or stateful values can therefore make parallel xUnit tests interfere with each other.
AXG004 checks modules that contain an xUnit [<Fact>] or [<Theory>], or an FsCheck [<Property>]. It reports module-level values but not functions such as let fixture () = ....
The analyzer excludes constants and point-free function definitions because they do not create the shared-state risk.
Create fixtures inside each test or expose a function that returns a fresh fixture. For a safe immutable value, add axial-allow-fixture to the flagged line or the line immediately above it:
let private syntheticTime = DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero) // axial-allow-fixture
syntheticTime: DateTimeOffset``.ctor``: int * int * int * int * int * int * TimeSpan -> unitInitializes a new instance of the structure using the specified year, month, day, hour, minute, second, and offset. The year (1 through 9999). The month (1 through 12). The day (1 through the number of days in ). The hours (0 through 23). The minutes (0 through 59). The seconds (0 through 59). The time's offset from Coordinated Universal Time (UTC). does not represent whole minutes. is less than one or greater than 9999. -or- is less than one or greater than 12. -or- is less than one or greater than the number of days in . -or- is less than zero or greater than 23. -or- is less than 0 or greater than 59. -or- is less than 0 or greater than 59. -or- is less than -14 hours or greater than 14 hours. -or- The property is earlier than or later than .
System.TimeSpanRepresents a time interval.
Zero: TimeSpanRepresents the zero value. This field is read-only.
Requires & types
Requires: open Systemval syntheticTime: DateTimeOffsetAXG005: Preserve cancellation tokens
Task adapters receive Flow's cancellation token. Discarding that token prevents cancellation from stopping the underlying operation.
AXG005 checks ColdTask, ColdTask.create, Flow.fromTask, and Flow.fromTaskResult. It reports a single-argument lambda when the argument is a bare discard, such as fun _ -> ....
Use the cancellation-aware overload of the wrapped operation and pass the token to it.
If a legacy API has no cancellation-aware overload, add axial-allow-discarded-cancellation to the flagged line or the line immediately above it:
/// An API with no cancellation support.
let legacyCall () : Task<int> = Task.FromResult 42
let legacy = ColdTask(fun _ -> legacyCall ()) // axial-allow-discarded-cancellation
legacyCall: unit -> Task<int>System.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
intAn abbreviation for the CLI type . Basic Types
System.Threading.Tasks.TaskRepresents an asynchronous operation.
FromResult: 'TResult -> Task<'TResult>Creates a that's completed successfully with the specified result. The result to store into the completed task. The type of the result returned by the task. The successfully completed task.
legacy: ColdTask<int>ColdTaskRequires & types
Requires: open Axial; open System.Threading.Tasks (packages: Axial)val legacyCall: unit -> Task<int>val legacy: ColdTask<int>This check does not run in test projects. Test adapters often wrap completed tasks that have no work to cancel.
AXG006: Format without reflection
F# unions and records get a compiler-generated ToString() that calls sprintf "%+A". %A walks the value with
FSharp.Reflection, and FSharp.Core's option, voption, list, Result, Choice, Map, and Set print their
contents the same way. NativeAOT and full trimming remove the metadata this needs. The call then throws or prints the
wrong text, and the build gives no specific warning because the reflection happens inside FSharp.Core.
AXG006 reports these formatting sites in typed code:
x.ToString(),string x, and interpolation holes such as$"{x}"whenxis an F# union, record, anonymous record, exception, or one of the FSharp.Core types above, and the type has no hand-writtenToStringoverride- the same forms on a type parameter, whose substituted type may be a union or record
%Aon any value other than a primitive, even when its type overridesToString- a boxed value of those types passed to
String.Format,String.Concat,StringBuilder.Append,TextWriter.Write,Console.Write,Trace.WriteLine, orDebug.WriteLine
Render the value explicitly instead. Match on the cases, call a describe function, or give the type a hand-written
override:
type OrderError =
| OutOfStock of sku: string
| InvalidQuantity of int
override this.ToString() =
match this with
| OutOfStock sku -> $"Out of stock: {sku}"
| InvalidQuantity quantity -> $"Invalid quantity: {quantity}"
FsLiveDocsGeneratedPage3_6B7179067CFE.OrderErrorOutOfStocksku: stringstringAn abbreviation for the CLI type . Basic Types
InvalidQuantityintAn abbreviation for the CLI type . Basic Types
this: OrderErrorToString: OrderError -> unit -> stringquantity: intWith that override, $"{error}", string error, and %O are safe. %A is not.
Axial's own public unions render themselves this way, including Exit, Cause, FiberStatus, FiberId, and the
Process, HttpClient, FileSystem, and PlatformService error types. A payload inside Exit or Cause is still
rendered with its own ToString, so give your error types an override too. Cause.prettyPrint also accepts an
explicit error renderer.
If code never runs trimmed, such as a script or a test helper, add axial-allow-reflection-format to the flagged line
or the line immediately above it:
let printDiagnostics (diagnostics: Map<string, int list>) =
printfn "%A" diagnostics // axial-allow-reflection-format
printDiagnostics: Map<string,int list> -> unitdiagnostics: Map<string,int list>Microsoft.FSharp.Collections.FSharpMap`2Immutable maps based on binary trees, where keys are ordered by F# generic comparison. By default comparison is the F# structural comparison function or uses implementations of the IComparable interface on key values. See the module for further operations on maps. All members of this class are thread-safe and may be used concurrently from multiple threads.
stringAn abbreviation for the CLI type . Basic Types
intAn abbreviation for the CLI type . Basic Types
listThe type of immutable singly-linked lists. See the module for further operations related to lists. Use the constructors [] and :: (infix) to create values of this type, or the notation [1; 2; 3]. Use the values in the List module to manipulate values of this type, or pattern match against the values directly. See also F# Language Guide - Lists.
printfn: Printf.TextWriterFormat<'T> -> 'TPrint to stdout using the given format, and add a newline. The formatter. The formatted result. See Printf.printfn (link: ) for examples.
Requires & types
val printDiagnostics: Map<string,int list> -> unitscripts/run-aot-probe.sh publishes a NativeAOT probe that renders these types and dumps a fiber registry, so the
release checks catch a regression that the analyzer cannot see.
AXG007: Keep resources acquired by a fiber
A forked fiber owns the scoped resources it acquires and releases them when it
settles. Fiber.join returns the fiber's value after that, so a resource the fiber returns is already released:
let connection : Resource<ClockEnvironment, Never, MemoryStream> =
Resource.create
(Flow.ok (new MemoryStream()))
(fun stream -> Flow.delay (fun () -> stream.Dispose(); Flow.ok ()))
/// Reported: the connection is released before join returns it.
let releasedBeforeUse =
flow {
let! fiber = Flow.scopeResource connection |> Flow.fork
return! Fiber.join fiber
}
connection: Resource<ClockEnvironment,Never,MemoryStream>Axial.Resource`3Describes acquisition of a value together with registration of its release in the current Flow scope. The release is a flow that cannot fail with a typed error. It runs without interruption, with the environment the value was acquired with; a defect it raises becomes a defect of the flow that closes the scope. The environment required to acquire the value. The typed acquisition failure. The acquired value.
Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
Axial.NeverRepresents an error channel that cannot occur.
System.IO.MemoryStreamCreates a stream whose backing store is memory.
Axial.ResourceModulecreate: Flow<'env,'error,'value> -> ('value -> Flow<'env,Never,unit>) -> Resource<'env,'error,'value>Describes acquisition together with a release flow registered in the current Flow scope. The release runs when the scope closes, without interruption and with the environment captured at acquisition. It cannot fail with a typed error; a defect it raises becomes a defect of the flow that closes the scope, after the scope's other releases have run. Resource.create openConnection (fun connection -> Flow.delay (fun () -> connection.Close(); Flow.succeed ()))
Axial.Flowok: 'value -> Flow<'env,'error,'value>Creates a successful synchronous flow. The value to wrap in a successful flow. A flow that always succeeds with the provided value.
stream: MemoryStreamdelay: (unit -> Flow<'env,'error,'value>) -> Flow<'env,'error,'value>Defers flow construction until execution time. A function that returns the flow to execute. Recursive calls deferred here are consumed by the executor without growing a chain of parent tasks. Work that must run after the recursive call still needs storage until that call finishes. A flow that lazily evaluates the factory when executed. let flow = Flow.delay (fun () -> Flow.succeed 42)
Dispose: unit -> unitReleases all resources used by the .
releasedBeforeUse: Flow<ClockEnvironment,Never,MemoryStream>flow: FlowBuilderThe universal flow { } computation expression.
fiber: Fiber<Never,MemoryStream>scopeResource: Resource<'env,'error,'value> -> Flow<'env,'error,'value>Acquires a described resource and registers its release with the current runtime scope. The resource is a scoped resource: it stays open until the current scope closes. Inside a forked fiber that is the fiber's own scope, which closes when the fiber settles; inside a branch of zipPar, race, sequencePar, or traversePar, the resource moves to the caller's scope if the caller receives the branch's value. Use forkResource to acquire it in the background and keep it. A flow that finishes acquiring after its scope has closed, such as a race loser, releases the resource at once and is interrupted. The acquisition and release description. A flow that succeeds with the acquired value.
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
fork: Flow<'env,'error,'value> -> Flow<'env,'none,Fiber<'error,'value>>Starts a flow in a new fiber without waiting for it to complete. Forking turns a cold flow description into hot child work and returns a handle that can later be joined or interrupted. Prefer zipPar or race when the caller only needs a simple parallel composition. Wait for the handle with Fiber.join or Fiber.await, and stop it with Fiber.interrupt. The fiber runs in its own scope and releases its scoped resources (anything registered with a scope, such as scopeResource, scopeFinalizer, or a hub subscription) when it settles, whether or not anyone joins it. A value the fiber returns therefore outlives the scoped resources the fiber acquired: a resource returned through Fiber.join has already been released. Use forkResource to acquire a resource in the background and keep it, or zipPar and sequencePar to acquire several in parallel. The flow to fork. A flow that produces a handle.
Axial.FiberModuleOperations on a running , the handle returned by Flow.fork. Every operation returns a flow; nothing waits or interrupts until that flow runs. Reading a fiber's outcome through join, await, or interrupt marks it observed, so a defect it died with is not also reported as unobserved.
join: Fiber<'error,'value> -> Flow<'env,'error,'value>Waits for a fiber and returns its value, failing the same way the fiber failed. Joining preserves the child's error channel: a Cause.Fail becomes the same typed error, and interruption and defects remain interruption and defects. Use await to inspect the outcome instead. The fiber to join. A flow that completes with the fiber's value. let loadProfile : Flow<unit, string, string> = Flow.ok "profile" let loadOrders : Flow<unit, string, int list> = Flow.ok [ 1; 2 ] let page = flow { let! fiber = Flow.fork loadProfile let! orders = loadOrders let! profile = Fiber.join fiber return profile, orders }
Requires & types
Requires: open Axial; open System.IO (packages: Axial)val connection: Resource<ClockEnvironment,Never,MemoryStream>val releasedBeforeUse: Flow<ClockEnvironment,Never,MemoryStream>The code compiles and reads like ZIO, where a forked fiber shares its parent's Scope. In Axial the forked
fiber's own scope closes when it settles, so the release runs before Fiber.join returns: the stream is
already disposed, and the first use after the join throws ObjectDisposedException.
AXG007 reports a Flow.fork, forkNamed, forkGraceful, forkDetached, forkReplacing, or forkReplacingKey of
a flow that is a Flow.scopeResource or Flow.scopeAcquireRelease call, or a flow { } that returns what it acquired
with one of them. Acquire the resource with Flow.forkResource to keep it in the caller's scope, or with Flow.zipPar
or Flow.sequencePar to acquire several in parallel:
let keptForUse =
flow {
let! connecting = Flow.forkResource connection
return! Fiber.join connecting
}
keptForUse: Flow<ClockEnvironment,Never,MemoryStream>flow: FlowBuilderThe universal flow { } computation expression.
connecting: Fiber<Never,MemoryStream>Axial.FlowforkResource: Resource<'env,'error,'value> -> Flow<'env,'none,Fiber<'error,'value>>Starts acquiring a resource in a new fiber, keeping the resource in the caller's scope. A fiber started with fork releases its scoped resources when it settles, so a resource it acquires is already released by the time Fiber.join returns it. forkResource runs only the acquisition in the fiber and registers the release with the caller's scope, so Fiber.join returns a live resource that is released when the caller's scope closes, as if the caller had used scopeResource. If the caller's scope closes before the acquisition finishes, the fiber is interrupted, and a resource that was acquired anyway is released straight away. Scoped resources that the acquiring flow itself registers along the way belong to the fiber and are released when it settles; only the described resource moves to the caller. To acquire several resources in parallel and use them together, use zipPar or sequencePar. The acquisition and release description. A flow that produces a fiber whose value is the acquired resource. flow { let! connecting = Flow.forkResource databaseConnection let! config = loadConfig let! connection = Fiber.join connecting // still open return! migrate connection config } |> Flow.scoped // the connection is released here
connection: Resource<ClockEnvironment,Never,MemoryStream>Axial.FiberModuleOperations on a running , the handle returned by Flow.fork. Every operation returns a flow; nothing waits or interrupts until that flow runs. Reading a fiber's outcome through join, await, or interrupt marks it observed, so a defect it died with is not also reported as unobserved.
join: Fiber<'error,'value> -> Flow<'env,'error,'value>Waits for a fiber and returns its value, failing the same way the fiber failed. Joining preserves the child's error channel: a Cause.Fail becomes the same typed error, and interruption and defects remain interruption and defects. Use await to inspect the outcome instead. The fiber to join. A flow that completes with the fiber's value. let loadProfile : Flow<unit, string, string> = Flow.ok "profile" let loadOrders : Flow<unit, string, int list> = Flow.ok [ 1; 2 ] let page = flow { let! fiber = Flow.fork loadProfile let! orders = loadOrders let! profile = Fiber.join fiber return profile, orders }
Requires & types
Requires: open Axial (packages: Axial)val keptForUse: Flow<ClockEnvironment,Never,MemoryStream>Concurrency and scoped resources explains which scope owns a resource in each concurrent combinator.
If the fiber's value is never used after the fiber settles, add axial-allow-resource-from-fiber to the flagged line
or the line immediately above it.
AXG008: Keep scope-owned services inside their run
Flow.run, Flow.runSync, Flow.RunSynchronously, and StartAsTask close their scope after completion. Returning Queue.makeScoped, Hub.makeScoped, or Hub.subscribe directly leaves a shut-down service. Caches made by Cache.make, makeBounded, makeBoundedWithExpiry, or makeRec capture the creating runtime; closing its scope interrupts lookups. Use these services inside the flow or keep their owning scope open. Queue.make (and its bounded, unbounded, dropping, and sliding shorthands) and Hub.make do not register with the scope and may be returned. The syntax check sees direct calls and pipelines ending in a scoped constructor; the result-type check catches composed cache flows but does not flag Queue or Hub by type because it cannot distinguish scoped from unscoped construction. At an intentional boundary, add // axial-allow-effect: scope.

