RFC 018: language primitives for testing¶
- Status: Implemented
- Created: 2026-01-14
- Author(s): Danny Meijer (@dannymeijer)
- Related: RFC 019 (test runner and CLI), RFC 001 (language portions; superseded), RFC 002 (language portions; superseded)
- Issue: https://github.com/dannys-code-corner/incan/issues/76
- RFC PR: —
- Implementation PRs: https://github.com/dannys-code-corner/incan/pull/410, https://github.com/dannys-code-corner/incan/pull/435
- Written against: v0.1
- Shipped in: v0.3
Summary¶
Define the language-level testing primitives for Incan:
- the
assertkeyword (always-on), including message propagation and pattern-binding forms assert ... raises ErrorType(sync only)- a reserved
module tests:block for inline test-only code std.testing-gated resolution rules for test-only decorators/helpers (no magic global names)- build/check stripping semantics for inline tests
Testing decorators/markers are gated behind the std.testing standard library module (no magic global names).
This RFC is jointly normative with RFC 019, which defines runner and CLI behavior (discovery, fixtures, parametrization, markers, parallelism, timeouts, reporting).
Motivation¶
Testing spans both language concerns (syntax, scoping, desugaring) and runner concerns (discovery, fixtures, selection, reporting). Keeping those concerns in separate RFCs lets the compiler and the test runner evolve independently while still sharing precise invariants.
This RFC defines only the language primitives; RFC 019 specifies the runner and CLI semantics that consume them.
Goals¶
- Define the language-owned testing primitives independently from runner and CLI behavior.
- Standardize
assert,assert ... raises ErrorType, and inlinemodule tests:blocks. - Require
std.testing-gated name resolution for test-only decorators and helpers. - Preserve a clean split between compiler semantics and test-runner semantics across RFC 018 and RFC 019.
Non-Goals¶
- Defining test discovery, fixtures, parametrization, markers, parallelism, or reporting semantics in this RFC.
- Introducing magic global test names outside the
std.testingmodule gate. - Changing production-code semantics beyond the explicitly defined testing primitives in this document.
Guide-level explanation (how users think about it)¶
How to read this RFC¶
This RFC covers language-level testing primitives only:
assert(including messages and pattern binding)assert ... raises ErrorTypemodule tests:inline test blocksstd.testing-gated resolution rules for test-only constructs
Runner/CLI behavior (discovery, fixtures, parametrization, markers, parallelism, timeouts, reporting) is defined in RFC 019.
If you are implementing this RFC, start with the conformance checklist near the end and the reference-level rules above, then implement in dependency order.
The std.testing module¶
Testing utilities are normal functions/decorators imported from the std.testing module:
from std.testing import assert_eq, assert_true, assert_false, fail
This RFC only specifies how these names are resolved (gated behind std.testing) and how assert maps to the std.testing.assert_* surface. Execution semantics for fixtures, parametrization, markers, and CLI options are defined in
RFC 019.
Assertions: assert ... (keyword)¶
This RFC introduces an assert keyword that can be used anywhere (tests or production code) without importing anything:
assert 1 + 2 == 3
assert 2 < 3
assert user is Some(_), "user must be present"
It is syntax sugar for a language-level assertion primitive with the same user-facing behavior as the std.testing module's assertion functions. See the reference-level table below for the full, exhaustive mapping.
Design note (important semantic commitment):
assertmust be valid in production code without pulling in the test runner or requiringincan test.- Implementations may lower
assertto a compiler intrinsic / core runtime primitive. Thestd.testing.assert_*helpers are the test-oriented, explicit API surface and must remain consistent withassertbehavior (messages, formatting, etc.). - This RFC defines
assertas always-on runtime checking (like Python). A compile-out variant (e.g.debug_assert) may be introduced in a future RFC, but is out of scope here.
Rationale (brief):
assertis used both in tests and in production code for invariants ("this should never happen").- Always-on semantics avoid "works in tests/debug, breaks in release" surprises.
- Mitigation: avoid asserts in hot paths and prefer explicit
Result/Option-based error handling for recoverable conditions. A futuredebug_assertcan address performance-sensitive checks.
The std.testing module remains available for explicit imports, and richer APIs.
Note: this RFC specifies assertion messages (e.g. assert x > 0, "x must be positive") and requires the underlying std.testing.assert_* helpers to accept an optional msg. This is not fully supported in current Incan; it is a required
part of implementing this RFC.
Common assert patterns (guide-level, Python-inspired)¶
assert accepts any expression that type-checks to bool. In practice, users will write assertions across a handful of
common shapes.
Equality / inequality (special-cased to assert_eq / assert_ne):
assert a == b
assert actual != unexpected
assert user.name == "alice"
assert len(items) == 3
# Optional message (shown on failure)
assert user.name == "alice", "expected alice user"
Expected failure output (illustrative)¶
Exact formatting is implementation-defined, but failures should be understandable.
At minimum, failed assert statements should be reported as an AssertionError (Python-inspired), with the optional message rendered as AssertionError: <msg>.
Minimum examples:
# assert x > 0, "x must be positive"
FAILED: AssertionError: x must be positive
assertion failed: x > 0
x = -1
# assert x > 0
FAILED: AssertionError
assertion failed: x > 0
x = -1
# assert a == b, "values must match"
FAILED: AssertionError: values must match
assertion failed: left != right
left: 2
right: 3
Ordering comparisons:
assert x > 0
assert x >= 0
assert start <= end
# Incan does not support Python-style chained comparisons like `0 < x < 10` (at least not as part of this RFC).
# Use `and` explicitly:
assert 0 < x and x < 10
Boolean logic:
assert True
assert not True
assert a and b
assert a or b
assert (a and b) or c
Option / Result checks (recommended to be explicit; Option/Result are not implicitly truthy):
assert user.is_some()
assert user.is_none()
assert result.is_ok()
assert result.is_err()
Pattern matching with is (note: is is pattern matching, not identity):
assert user is Some(_)
assert result is Ok(_)
assert result is Err(_)
Binding notes (important semantic commitment):
assert x is Some(v)may introduce a binding (here:v) for the remainder of the current block scope, as if the compiler had emittedlet v = ...at that point.- In RFC 018, only the following binding patterns are supported in
assert:Some(name)andOk(name)/Err(name)wherenameis a single identifier, OR- the wildcard
_(no binding).
- Nested patterns, multiple bindings, and guards are out of scope for this RFC (they may be added later if/when the general pattern-matching system is specified).
"Contains" / membership style checks:
assert name != ""
# If the type exposes an API (e.g. `contains(...)`) this works naturally:
assert tags.contains("release")
Identity checks (Python's is) are intentionally not part of assert in this RFC, because is already has a different meaning (pattern matching). If/when Incan adds a reference-identity operation, it should be spelled explicitly (e.g. ref_eq(a, b)), not overloaded onto is.
Inline test-only module blocks¶
Inline tests live next to production code, but inside a test-only module block:
def add(a: int, b: int) -> int:
return a + b
module tests:
from std.testing import assert_eq, test
def test_addition() -> None:
assert add(2, 3) == 5
@test # explicitly marked as a test (name doesn't need to start with test_)
def explicit_test() -> None:
assert add(2, 3) == 5
This keeps helpers/fixtures/test imports scoped to the test module and allows the compiler to strip all test-only code from incan build and incan run.
Rule of thumb:
- In inline tests (
module tests:inside a production file), putfrom std.testing import ...inside themodule tests:block so the production module namespace stays clean.
Test file discovery and runner behavior are defined in RFC 019.
Reference-level explanation (precise rules)¶
Core principle: testing is gated behind std.testing¶
Test tooling must only recognize testing constructs when they resolve to the std.testing module. The compiler must resolve imports/aliases consistently so runner semantics (defined in RFC 019) can rely on these identities.
Runner-recognized constructs (see RFC 019) include:
@test=std.testing.test@fixture=std.testing.fixture@parametrize=std.testing.parametrize@skip,@xfail,@slow= correspondingstd.testing.*markers@serial/@resource(...)= correspondingstd.testing.*scheduling decorators
This avoids "magic names" (e.g. a random user-defined @fixture decorator should not be treated as a test fixture).
Resolution rule (minimal)¶
For a decorator @X to be treated as std.testing.<name>, one of the following must hold in the file:
Xisstd.testing::<name>/std.testing.<name>(fully-qualified reference), ORXis an imported alias ofstd.testing.<name>(e.g.from std.testing import fixture as X), ORXis imported from thestd.testingmodule without alias (e.g.from std.testing import fixture)
(Exact import-resolution machinery is an implementation detail, but the behavior must match these semantics.)
Rationale:
- Unlike ordinary modules (e.g.
web),std.testingis a gateway for discovery and special semantics. Resolution must be explicit and auditable, so only symbols that resolve tostd.testingare treated as test constructs.
Module aliasing:
- A decorator expression of the form
@M.<name>is treated asstd.testing.<name>only ifMresolves to thestd.testingmodule (e.g.import std.testing as t; @t.fixture).
Re-exports:
- If a decorator name resolves to a symbol re-exported from another module, it is treated as
std.testing.<name>only if the resolver can prove the symbol originates fromstd.testing. Otherwise it is treated as a normal decorator.
Star imports are disallowed for the std.testing module:
from std.testing import *MUST be a compile-time error in any context.- Rationale: explicit imports keep the testing gate analyzable and avoid accidental collisions with user-defined names.
The assert keyword (reference semantics)¶
assert is a language-level statement that is valid in any file (not only in test contexts).
Form:
assert <expr>where<expr>type-checks asboolassert <expr>, <msg>where<msg>type-checks asstr(optional failure message)
The optional message is passed through to the underlying std.testing.assert_* helper and should be displayed as part of the assertion failure output.
Message presence:
- If no message is provided, output should not render a message line.
- An empty string is treated as "no message" for formatting purposes.
Minimum diagnostics guarantee:
- On failure, assertion output must include the optional message (if provided) and enough detail to diagnose the failing condition.
- For equality/inequality assertions (
assert a == b/assert a != b), the minimum guarantee is that the output identifies the failed comparison kind and includes the optional message when one is provided.
Runtime error model:
- A failed
assertraises a built-in runtime error type namedAssertionError. - "Runtime error" refers to a panic-style failure (not a
Result-returning error). It aborts the current test case. ErrorTypeinassert ... raises ErrorTypemust denote a runtime error type.
Runtime error types (normative; scope for this RFC)¶
This RFC uses the term runtime error to mean a panic-style failure that aborts execution (in contrast to
Result-returning errors that are explicitly handled with ? and pattern matching).
Rules:
- This RFC requires at minimum the built-in runtime error type
AssertionError. - This RFC does not define a general, user-extensible runtime error hierarchy.
- User-defined errors should use
Result/Option(Incan's primary error-handling model).
- User-defined errors should use
- Subtyping among runtime error types is optional:
- If the implementation supports runtime error subtyping,
assert ... raises BaseTypemust match subtypes. - If not, implementations MUST - at minimum - match the exact runtime error type named by
ErrorType(as already specified in the raises rules below).
- If the implementation supports runtime error subtyping,
Exhaustive mapping to std.testing.assert_* helpers (required behavior)¶
std.testing.* helper |
assert ... surface form |
Lowers to |
|---|---|---|
assert |
assert <bool-expr>[, msg] |
std.testing.assert(<bool-expr>, msg?) |
assert_true |
assert <bool-expr>[, msg] |
std.testing.assert(<bool-expr>, msg?) |
assert_false |
assert not <bool-expr>[, msg] |
std.testing.assert(not <bool-expr>, msg?) |
assert_eq |
assert a == b[, msg] |
std.testing.assert_eq(a, b, msg?) |
assert_ne |
assert a != b[, msg] |
std.testing.assert_ne(a, b, msg?) |
assert_is_some |
assert opt is Some(v)[, msg] |
let v = std.testing.assert_is_some(opt, msg?) |
assert_is_none |
assert opt is None[, msg] |
std.testing.assert_is_none(opt, msg?) |
assert_is_ok |
assert res is Ok(v)[, msg] |
let v = std.testing.assert_is_ok(res, msg?) |
assert_is_err |
assert res is Err(e)[, msg] |
let e = std.testing.assert_is_err(res, msg?) |
assert_raises |
assert call() raises ErrorType[, msg] |
std.testing.assert_raises[ErrorType](..., msg?) |
Full signatures (required std.testing API surface for this RFC):
assert(condition: bool, msg: str = "assertion failed") -> None
assert_true(condition: bool, msg: str = "assertion failed: expected true") -> None
assert_false(condition: bool, msg: str = "assertion failed: expected false") -> None
assert_eq[T](left: T, right: T, msg: str = "assertion failed: left != right") -> None
assert_ne[T](left: T, right: T, msg: str = "assertion failed: left == right") -> None
assert_is_some[T](option: Option[T], msg: str = "assertion failed: expected Some, got None") -> T
assert_is_none[T](option: Option[T], msg: str = "assertion failed: expected None, got Some") -> None
assert_is_ok[T, E](result: Result[T, E], msg: str = "assertion failed: expected Ok, got Err") -> T
assert_is_err[T, E](result: Result[T, E], msg: str = "assertion failed: expected Err, got Ok") -> E
assert_raises[E](block: () -> None, msg: str = "") -> None
Trait bounds:
assert_eqandassert_nerequire the comparison operation to typecheck forT.- Assertion failure messages report the assertion kind and optional custom message. They do not require
Debugformatting of compared values. - Other assertion helpers do not require extra trait bounds on their type parameters.
Desugaring rule used by the compiler:
Let the optional message be msg when present (i.e. assert <expr>, msg).
- If
<expr>is syntacticallya == b, lower tostd.testing.assert_eq(a, b, msg?) - If
<expr>is syntacticallya != b, lower tostd.testing.assert_ne(a, b, msg?) - If the assert statement is of the form
assert opt is Some(v), lower tolet v = std.testing.assert_is_some(opt, msg?) - If the assert statement is of the form
assert opt is None, lower tostd.testing.assert_is_none(opt, msg?) - If the assert statement is of the form
assert res is Ok(v), lower tolet v = std.testing.assert_is_ok(res, msg?) - If the assert statement is of the form
assert res is Err(e), lower tolet e = std.testing.assert_is_err(res, msg?) - If the assert statement is of the form
assert call() raises ErrorType, lower tostd.testing.assert_raises[ErrorType](lambda: call(), msg?). - Otherwise, lower to
std.testing.assert(<expr>, msg?)
Note: the "lowers to" wording describes the required behavior and message propagation. Implementations may choose to lower
assert to a compiler intrinsic and have std.testing.assert_* call into that intrinsic, as long as the user-visible
semantics match this mapping.
The std.testing module is not required at runtime for assert; the mapping is semantic, and std.testing.assert_* must mirror the intrinsic behavior.
The assert_true / assert_false helpers are aliases/conveniences in the std.testing API; the compiler does not need to emit them directly.
On failure, assertions must produce the same failure semantics and (as much as possible) the same message formatting as the underlying std.testing assertion functions.
Pattern-binding scope and allowed patterns (reference rules)¶
assert supports a limited form of pattern binding via is (leveraging Incan's existing pattern-matching semantics).
Allowed Option patterns:
assert opt is Some(_)[, msg]assert opt is Some(<ident>)[, msg](binds<ident>)assert opt is None[, msg]
Allowed Result patterns:
assert res is Ok(_)[, msg]assert res is Ok(<ident>)[, msg](binds<ident>)assert res is Err(_)[, msg]assert res is Err(<ident>)[, msg](binds<ident>)
Restrictions:
- No nested patterns (e.g.
Some(Ok(x))) and no multiple bindings in a singleassert. - The bound name is introduced in the current scope exactly as if the compiler had emitted
let <ident> = ...at the assertion site; it is in scope for subsequent statements in the same block. - The bound name has the inner type of the matched value (e.g.
TforOption[T],T/EforResult[T, E]), and the assertion does not otherwise narrow the type of the tested expression. - Shadowing: if the bound identifier already exists in the current lexical block, the assertion is a compile-time error. Users should pick a new name or bind in an inner block to avoid ambiguity.
Guidance (non-normative): avoid using assert as control flow in production code. Prefer explicit pattern matching or
assert_is_* helpers when unwrapping Option/Result values.
Raises semantics (reference rules)¶
Syntax: assert <call-expr> raises <ErrorType>[, msg] where <call-expr> is a call expression.
This is a convenience for asserting that a call fails by raising a runtime error.
- "Raises" refers to a runtime error/panic-style failure (the same category of failure used for failed assertions).
- It does not refer to
Result-returning APIs; for results, useassert res is Err(e)/assert_is_err. - Block-style "raises" assertions are out of scope; use
std.testing.assert_raisesfor multi-statement checks. - Matching:
ErrorTypematches that exact type or any of its subtypes. If an implementation lacks subtype information, it MUST at minimum match the exact type. - Async "raises" is out of scope for this RFC.
Inline test module context (reference rules)¶
module tests: introduces a test-only scope inside a production source file. The compiler must treat this block as
strip-able in non-test compilation modes.
Rules:
- A file may contain at most one
module tests:block. Additionalmodule tests:blocks are a compile error. - A file that is a test file context (as defined in RFC 019) must not also contain
module tests:. - Names declared inside
module tests:do not leak into the surrounding module scope.
Test file discovery and how tests/fixtures are collected are defined in RFC 019.
Build and check behavior¶
| Command | Test contexts (module tests:) |
|---|---|
incan build |
stripped (not emitted) |
incan run |
stripped (not included) |
incan test |
included and executed |
incan --check |
type-checked but not emitted |
Test files are only relevant to incan test (they are not part of production builds).
Note: incan --check type-checks inline module tests: blocks in source files, but does not include tests/ unless explicitly passed as a path argument.
Design details¶
Inline test module scoping¶
The inline test module:
- may access names from the surrounding file (like Rust's
use super::*unit-test pattern) - introduces a scope boundary so test-only helpers/imports do not pollute the production namespace
Visibility rules (normative):
- Names declared in the enclosing module (including private names not marked
pub) are visible insidemodule tests:. - This is lexical visibility, not an implicit import: the test block can reference any name that is in scope at the file level, as if the test block were nested code in the same file.
- Names declared inside
module tests:(functions, imports, bindings) are not visible outside the test block. - The test block does not introduce a separate module namespace for the purpose of
pubvisibility; it is purely a scoped block that can be stripped.
This RFC does not define a general-purpose module system beyond existing file/module semantics; module tests: inside a file is specifically a scoped block that can be stripped in non-test compilation modes.
Compatibility / migration¶
- Existing code that uses
std.testing.assert_*continues to work;assertis additive syntax sugar. - Adding a
module tests:block enables inline tests without changing production code layout. - Runner discovery and CLI compatibility are specified in RFC 019.
Alternatives considered¶
- Top-level
@testnext to production functions: rejected; it pollutes the production namespace and makes it hard to keep test-only imports/helpers contained. - Magic language keywords for tests/fixtures: rejected; harms tooling and contradicts the "stdlib-gated" principle.
- Compile-time-only assertions: rejected;
assertis intended for always-on runtime invariants.
Out of scope (for now):
debug_assertor build-mode controlled assertion stripping- richer pattern matching in
assert(nested patterns, guards)
Appendix: testing surface inventory (informative)¶
This appendix is a contributor-oriented inventory of the testing surface after this RFC is implemented, with an informative snapshot of what exists today (at time of writing). It is not normative; the spec sections above are authoritative.
Legend:
- Today: implementation status in the current repository at RFC creation time
- Yes: implemented
- Partial: some pieces exist, but not the full RFC behavior
- No: not implemented
- After RFC 018: whether this RFC introduces it (New), modifies semantics (Changed), or leaves it (Unchanged)
Note: this table should be used as a checkmark toward implementation completeness when this RFC is implemented.
Language + assertion API surface¶
| Item | Today | After RFC 018 | Notes | Implemented |
|---|---|---|---|---|
assert <expr> keyword |
No | New | Lowers to std.testing.assert_* helpers |
|
assert <expr>, <msg> |
No | New | Python-style message; passed through to helpers | |
module tests: inline tests |
No | New | Reserved scope; stripped outside incan test |
|
std.testing.assert*(..., msg="") |
Partial | Changed | RFC requires optional msg on core asserts |
|
std.testing.assert_is_* helpers |
Partial | Changed | RFC pins behavior + msg propagation | |
std.testing.assert_raises (+ assert ... raises) |
Partial | Changed | RFC pins desugaring + optional msg | |
std.testing.fail(msg) |
Yes | Unchanged | Explicit failure |
Layers affected¶
- Lexer —
assertmust be treated as a soft keyword in statement position, not as a hard reserved word. The lexer emitsIdent("assert"); the parser promotes it to the assertion statement form without requiring astd.testingimport. - Parser — must parse the
assert <expr>form, theassert <expr>, <msg>form, and theassert <expr> raises <Type>form as distinct AST nodes; must parse and validatemodule tests:as a reserved inline block producing aTestModuleAST node; must enforce thatmodule tests:appears at most once per file at module scope. - Typechecker — must gate resolution of test-only decorators and helpers behind
std.testing-activated import context; must validate that names insidemodule tests:are not visible outside and that the block has read access to private module members. - Lowering — must lower
assert <expr>to the appropriatestd.testing.assert_*call based on expression shape (equality, inequality, option/result, pattern binding); must lowerassert <expr> raises <Type>tostd.testing.assert_raises; must stripmodule tests:bodies from non-test compilation modes. - Stdlib (
std.testing) —assert_eq,assert_ne,assert_is_some,assert_is_none,assert_raises,assert, and their message-accepting overloads must conform to the desugaring rules specified in this RFC. - CLI —
incan buildandincan runmust stripmodule tests:bodies;incan --checkmust typecheck them;incan testmust include them in the compilation unit.
Implementation Plan¶
Phase 1: Parser, AST, and formatting¶
- Parse
assert, optional assertion messages,assert ... raises, and the supportedis Some/None/Ok/Errpattern-binding forms without requiring astd.testingimport. - Represent inline
module tests:blocks in the AST with stable spans and enforce the file-level placement/cardinality rules. - Keep formatter and LSP syntax behavior aligned with the parsed surface.
Phase 2: Typechecking and scope rules¶
- Typecheck assertion expressions, assertion messages, raises targets, and limited pattern bindings with span-precise diagnostics for unsupported forms.
- Enforce inline test-module scoping: enclosing names are visible inside
module tests:, and names/imports declared inside do not leak out. - Preserve
std.testing-gated recognition for runner-facing decorators and helpers while keeping the languageassertprimitive always-on.
Phase 3: Lowering, emission, and runtime behavior¶
- Lower each assertion form to the corresponding
std.testinghelper or intrinsic-compatible runtime behavior, including message propagation and equality/inequality value formatting. - Strip inline
module tests:bodies from build/run emission while keeping them available to check/test compilation modes. - Ensure generated Rust compiles for production assertions and inline test modules.
Phase 4: Stdlib, docs, and release readiness¶
- Align
std.testinghelper signatures and Rust backing functions with the RFC surface. - Add parser, typechecker, codegen, integration, and docs coverage for the accepted language behavior.
- Update user-facing docs, release notes, RFC progress state, and the active development version.
Implementation log¶
Spec / RFC lifecycle¶
- Review RFC 018 for lifecycle readiness and resolve stale wording that conflicted with always-on
assertsemantics. - Move RFC 018 from
PlannedtoIn Progressbecause implementation work has been picked up. - Keep RFC 018 checklist current as implementation phases land.
Parser / AST / formatter¶
- Parser: parse
assert <expr>without requiringstd.testing. - Parser: parse optional assertion messages.
- Parser: parse and validate
assert <call-expr> raises <ErrorType>. - Parser/AST: represent supported assertion pattern-binding forms.
- Parser/AST: represent inline
module tests:blocks and enforce cardinality/placement rules. - Formatter/LSP: preserve syntax and diagnostics for the new statement forms.
Typechecker / scope¶
- Typechecker: require assert conditions to typecheck as
bool. - Typechecker: require assertion messages to typecheck as
str. - Typechecker: validate raises target/runtime-error requirements or reject unsupported targets explicitly.
- Typechecker: introduce allowed
Some/Ok/Errassertion bindings into the current lexical scope. - Typechecker: reject unsupported nested/multi-binding assertion patterns.
- Typechecker: enforce inline test-module scope visibility and non-leakage.
- Typechecker: keep
std.testingdecorator/helper recognition gated by resolvedstd.testingimports.
Lowering / emission / runtime¶
- Lowering: map boolean, equality, inequality, option/result, and raises assertions to the RFC-defined helper behavior.
- Emission: generate compiling Rust for assertion helper calls and assertion failure paths.
- Emission/CLI: strip inline
module tests:bodies from build/run outputs. - Check/test modes: typecheck inline
module tests:blocks in the appropriate contexts. - Runtime/std.testing: align helper message defaults, panic behavior, and value formatting with RFC 018.
Tests¶
- Parser tests cover accepted and rejected assertion syntax.
- Parser tests cover inline
module tests:placement/cardinality. - Typechecker tests cover assertion typing, message typing, binding scope, and unsupported patterns.
- Codegen snapshots cover the primary assertion lowering forms.
- Integration tests cover runtime assertion failures, optional messages, inline
module tests:execution, and inline test stripping behavior. - Existing
std.testingmarker/runner tests continue to pass.
Docs / release¶
- Update language tutorial/reference docs for always-on
assertand inlinemodule tests:. - Update
std.testingdocs to separate language assertions from helper APIs and runner-facing decorators. - Update current release notes for RFC 018.
- Bump the active
0.3.0-dev.Nversion by one before closeout.
Design Decisions¶
assertis an always-on language primitive and is not compiled out in release builds by this RFC.- Testing-specific decorators and helpers are gated behind the
std.testingmodule rather than treated as ambient global names. module tests:is the reserved inline scope for test-only code in production modules.- Runner and CLI semantics remain split into RFC 019 rather than being folded into this language-level RFC.
References¶
- RFC 019: Test Runner, CLI, and Ecosystem
- Python
assertstatement:https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement