Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/ci-pull.yml
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,25 @@ jobs:
- name: Lint include graph + report dependency count
run: python3 tools/laige-include-lint

determinism-lint:
# M1-DET-01: runs on every PR (no ci:* condition). The sim-source
# determinism scan (the second half of the G-R8 guarantee; the
# first is the compile-time trait checked by the
# trait_compile_* CTest fixtures): forbids raw float/double and
# unordered containers in src/laige-sim/**, with per-line
# LAIGE-DETERM-EXCEPTION markers as the documented false-positive
# policy (docs/concepts/determinism.md). Platform-independent:
# Python 3 stdlib only, no setup step. The CTest suite runs it
# against fixture trees and the real tree in every P0 job as well
# (tests/tools, tests `determinism-lint-*`).
name: Determinism source scan (sim module)
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Scan sim sources for raw FP / unordered containers
run: python3 tools/laige-determinism-lint

api-manifest:
# M0-TOOL-01: runs on every PR (no ci:* condition). The checked-in
# public API manifest (laige-api.json, PRD §9.4, NFR-13.1) must stay
Expand Down
25 changes: 23 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
# ALL ten jobs run (the five P0 OS/variant jobs, the two sanitizer
# lanes (M0-CI-02), and the three platform-independent tooling jobs:
# the include-graph lint with dependency-count metric (M0-CI-03),
# the public API manifest drift check (M0-TOOL-01), and the
# determinism check (M0-TOOL-02)).
# the public API manifest drift check (M0-TOOL-01), the
# determinism source scan (M1-DET-01), and the determinism check
# (M0-TOOL-02)).
#
# Fuzz lane (PRD §14 "every commit (bounded)"; M0-TEST-01): there is no
# separate fuzz job — the `fuzz_json_parse` ctest entry (1000
Expand All @@ -34,6 +35,7 @@
# macos-intel macOS Intel, AppleClang (macos-14)
# include-lint Include-graph lint + dependency count (M0-CI-03)
# api-manifest Public API manifest drift check (M0-TOOL-01)
# det-lint Determinism source scan (M1-DET-01)
# detcheck Determinism check (M0-TOOL-02)
#
# The include-lint job (M0-CI-03; NFR-8.11, NFR-8.13) is platform-
Expand Down Expand Up @@ -259,6 +261,25 @@ jobs:
- name: Lint include graph + report dependency count
run: python3 tools/laige-include-lint

determinism-lint:
# M1-DET-01: platform-independent repository check — the sim-source
# determinism scan (the second half of the G-R8 guarantee; the first
# is the compile-time trait checked by the trait_compile_* CTest
# fixtures). Forbids raw float/double and unordered containers in
# src/laige-sim/**, with per-line LAIGE-DETERM-EXCEPTION markers as
# the documented false-positive policy (docs/concepts/determinism.md).
# Python 3 stdlib only, so one runner image suffices; it runs on
# every PR too (ci-pull.yml, same job definition). The CTest suite
# runs it against fixture trees and the real tree in every P0 job as
# well (tests/tools, tests `determinism-lint-*`).
name: Determinism source scan (sim module)
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Scan sim sources for raw FP / unordered containers
run: python3 tools/laige-determinism-lint

api-manifest:
# M0-TOOL-01 (PRD §9.4, NFR-13.1): the checked-in public API manifest
# (laige-api.json) must stay in sync with the public headers. This
Expand Down
35 changes: 23 additions & 12 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ system scheduler; M1-SYS-03: the per-system timing + budget
enforcement; M1-LOOP-01: the fixed-timestep game loop core;
M1-LOOP-02: the per-tick presentation snapshot + interpolation
state; M1-HEAD-01: the headless engine run — `Engine`
(config → world → systems → loop) and the `laige-run` binary).
(config → world → systems → loop) and the `laige-run` binary;
M1-DET-01: deterministic mode — the SimMath-only sim guarantee
(the G-R8 compile-time trait + the CI source scan), the per-system
PRNG substreams, and the `seed`/`determinism` config keys).
Every section of the AGENTS §13 `docs/` tree exists; each entry below
links what is written and the "not yet written" section marks what is
still to land.
Expand All @@ -29,11 +32,11 @@ still to land.
## Concepts

- [Concepts index](concepts/README.md) — architecture, coordinates
(ARCH-008), lifecycle, threading, and determinism scope. **Not yet
written** (M0 is foundations only); the index names each planned
document and its interim home today (the `Vec2`/`Vec3` comments in
`src/laige-core/include/laige/sim_math.h`, ADR 0002, the per-API
contracts).
(ARCH-008), lifecycle, threading, and determinism scope.
[Determinism](concepts/determinism.md) is written (M1-DET-01: the
same-build scope, the two-layer G-R8 enforcement, the exception
policy, the PRNG substreams); the other topics name their planned
document and interim home.

## API contracts (per public header)

Expand Down Expand Up @@ -89,8 +92,14 @@ still to land.
- [Headless engine run](api/engine.md) — `laige::Engine`
(config → world → systems → loop): `run_headless(maxTicks)` the
bounded + server run forms, the ordered idempotent CONC-006
shutdown, the provisional config surface, and the `laige-run`
CLI (M1-HEAD-01; `laige-sim` + `tools/run`).
shutdown, the provisional config surface (now including the
`seed`/`determinism` keys, M1-DET-01), the backend selection at
init, and the `laige-run` CLI (M1-HEAD-01; `laige-sim` + `tools/run`).
- [Determinism-safe storage](api/determinism.md) — the G-R8
compile-time trait: `SimMathBackend`, `DeterminismConfig`,
`detail::IsDeterminismSafe<T>`, and
`LAIGE_DETERMINISM_SAFE(Type, MemberTypes...)` (M1-DET-01;
`laige-sim`).
- [Result / Status / error codes](api/errors.md) — `laige::Result<T,E>`,
`laige::Status`, the stable `ErrorCode` registry (M0-CORE-01).
- [Structured logging](api/logging.md) — the `laige::log` facade, sinks,
Expand Down Expand Up @@ -160,9 +169,10 @@ still to land.

## Not yet written (honest status)

- `concepts/` — the architecture, coordinates, lifecycle, threading, and
determinism concept documents (the [index](concepts/README.md) names
each and its interim home).
- `concepts/` — the architecture, coordinates, lifecycle, and
threading concept documents (the [index](concepts/README.md) names
each and its interim home). [Determinism](concepts/determinism.md)
is written (M1-DET-01).
- `guides/` — task-oriented usage (first game, profiling, determinism)
— see the [index](guides/README.md).
- `debugging/` — the in-engine debug mode (AGENTS §15; profiling
Expand All @@ -185,7 +195,8 @@ still to land.
[system_timing.md](api/system_timing.md),
[game_loop.md](api/game_loop.md),
[presentation.md](api/presentation.md),
[engine.md](api/engine.md).)
[engine.md](api/engine.md),
[determinism.md](api/determinism.md).)

## Related

Expand Down
171 changes: 171 additions & 0 deletions docs/api/determinism.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Determinism-safe storage (`laige/sim/determinism.h`)

The compile-time half of the G-R8 determinism guarantee (M1-DET-01;
PRD §10.3, AGENTS ARCH-010/S-7/G-R8, ADR 0002). Public header:
`src/laige-sim/include/laige/sim/determinism.h` (header-only; no
implementation file). The concept-level contract — what is
deterministic, at what scope, enforced how — is
[concepts/determinism.md](../concepts/determinism.md); this document
covers the API surface.

## Quick start

```cpp
#include <laige/sim/determinism.h>

struct Health {
std::int32_t current{};
std::int32_t max{};
};
LAIGE_COMPONENT(Health);
// The member list IS the type's storage: verified at this site.
LAIGE_DETERMINISM_SAFE(Health, std::int32_t, std::int32_t);

// Now a system may declare I/O for Health: the G-R8 static_assert in
// World::registerSystem accepts it.
world.registerSystem(MySystem_Def,
laige::Io<Health, laige::Access::ReadWrite>{});
```

## The API

`laige::SimMathBackend` (enum class, `uint8_t`) — the backend ids the
config surface names:

| Value | Config id string | Backend |
|---|---|---|
| `FixedPoint16_16` | `"fixed_point_16_16"` | Q16.16, the default; bit-exact across build/platform/ISA/compiler (ADR 0002). |
| `FloatPinned32` | `"float_pinned_32"` | IEEE `float`, pinned flags; same-build/same-ISA scope (ADR 0002). |

`laige::DeterminismConfig` — the engine's determinism block:

| Member | Default | Contract |
|---|---|---|
| `bool enabled` | `true` | Deterministic mode: SimMath-only sim, per-system PRNG substreams, replay identity (seed + backend + config + inputs). |
| `SimMathBackend math` | `FixedPoint16_16` | The selected SimMath backend (compile-time dispatch at engine init; part of replay identity). |

`laige::detail::IsDeterminismSafe<T>` (trait) — true when `T` is
determinism-safe storage:

| `T` | Safe |
|---|---|
| Any integer type (`int8_t` … `uint64_t`) | yes |
| Any enum type | yes |
| `laige::fpx16_16` | yes |
| `float` | yes — the `fp32_pinned` backend's registered `Scalar` (ADR 0002). `float` is the *backend type*, not "raw float in the sim": using it is legal only through SimMath-registered types (this scalar or a `SimMath<Fp32Pinned>` vector). |
| `SimMath<Fpx16_16>::Vec2/Vec3` | yes |
| `SimMath<Fp32Pinned>::Vec2/Vec3` | yes |
| `double` | **no** — no SimMath backend uses it; never determinism-safe. |
| Any other type (unmarked struct, `std::string`, …) | no (primary template is false). |

`LAIGE_DETERMINISM_SAFE(Type, MemberTypes...)` — the mark:

- Declares that `Type`'s members are **exactly** the listed member types
(every member; order is irrelevant — the list is a set of types).
- Specializes `IsDeterminismSafe<Type>` with the verified member list:
`value = areDeterminismSafeMembers<MemberTypes...>()` (the &&-fold;
an empty list is vacuously safe).
- **Fails at the mark site** (a `static_assert` in the specialization): a
non-safe type in the list — e.g. a `double` member — is a compile
error there, before any system can declare the component in its I/O.
The error names the mark and points here.
- Write it once per type, at namespace scope, next to the type
definition (the `LAIGE_COMPONENT` precedent). A type that is itself an
integer, an enum, or a SimMath-registered scalar/vector needs **no**
mark. A member type that is itself a user struct must be marked in
turn (recursion).

`laige::detail::areDeterminismSafeMembers<Ts...>()` — the &&-fold the
mark expands to (true when every listed type is safe).

`laige::detail::IoComponentSafety<Tag>` — the fold helper
`World::registerSystem` uses: `Io<T, Access>` →
`IsDeterminismSafe<T>::value`; non-`Io` tags are vacuously true (the
`IsIoTag` static_assert fires first, so they never reach this fold).

## Where it is enforced

1. **The mark site** — `LAIGE_DETERMINISM_SAFE` fails fast on a bad
member list (compile error at the declaration).
2. **`World::registerSystem`** (entity.h) — the third `static_assert`
folds `IoComponentSafety` over the system's declared I/O: a component
that is not determinism-safe fails with an actionable message (mark
the component, or change the storage; points here). The check is over
*declared* I/O — a system that writes a `double` through some other
path is caught by the source scan below, not the trait.
3. **`tools/laige-determinism-lint`** — the textual scan of every
`src/laige-sim/**` translation unit (raw `float`/`double` type tokens,
float/double literals, `unordered_*` containers), with same-line
`// LAIGE-DETERM-EXCEPTION: G-R8 <reason>` markers as the documented
false-positive policy. CI job `determinism-lint` (both workflows) +
ctest `determinism-lint-*`.

The two layers are complementary by design: the trait covers *component
storage* (what a system's I/O names); the scan covers *sim translation
units* (what the code does). Neither subsumes the other.

## Determinism scope (ARCH-010)

This header defines *storage safety*, not the determinism scope itself.
The scope statement lives in
[concepts/determinism.md](../concepts/determinism.md): same-build
bit-identity (verified), the per-backend scopes of ADR 0002, and the
cross-target work left to M1-DET-04.

## Performance (DOC-004)

Everything here is `constexpr` template metaprogramming evaluated at
compile time: zero runtime cost, zero allocations, no state. The
`static_assert`s cost compile time only (one fold per
`registerSystem` call site; the mark's check is one &&-fold at the mark
site).

## Misuse warnings

- **An unmarked user struct is never safe** — "all my members are ints"
is not the declaration; the mark is. The trait's primary template is
false on purpose (fail-closed).
- **The member list must be complete.** Listing a subset of the members
claims the type has no other members; a missing non-safe member is a
lie the next `registerSystem` will not catch (the mark already
passed). Keep the list in sync with the struct (same-file, next to
it).
- **`float` in a mark means the fp32_pinned backend's Scalar.** Storing
raw `float` in a component that is meant to run under `fpx16_16` is a
backend mismatch, not a G-R8 violation — use `SimMath<Fpx16_16>`
types for backend-independent sim state.
- **`double` is not "almost safe".** It has no backend; there is no
mode where it is legal in sim storage.

## Verification

- `ctest -R trait_compile` — the compile-check fixtures
(`tests/laige-sim/compile_fail/`): `trait_compile_ok` (a marked safe
component compiles), `trait_compile_reject_double` (a `double` member
fails), `trait_compile_reject_unmarked` (an unmarked struct fails),
`trait_compile_reject_bad_mark` (a `double` in the mark's member list
fails at the mark site).
- `ctest -R determinism_mode` — `determinism_tests.cpp`: the
DeterminismMode / DeterminismEngine / DeterminismConfigParse suites
(same-seed identical 256-tick hash streams, seed divergence, substream
golden cross-check + independence, disabled-mode null rng, backend
selection, the config keys).
- `ctest -R determinism-lint` + CI `determinism-lint` — the source scan
(fixtures + real tree).

## Related

- [concepts/determinism.md](../concepts/determinism.md) — the scope and
the two-layer enforcement.
- [ADR 0002](../decisions/0002-deterministic-math.md) — SimMath, the two
backends, replay identity.
- [api/sim_math.md](sim_math.md) — the SimMath op surface (the only math
allowed in deterministic systems).
- [api/prng.md](prng.md) — `laige::Prng` substream derivation (the
per-system streams `SystemContext.rng` points at).
- [api/engine.md](engine.md) — the `seed`/`determinism` config keys and
the backend selection at init.
- [api/entity.md](entity.md) — `World::Options.seed`/`deterministic` and
the `registerSystem` G-R8 static_assert.
- [api/system_registry.md](system_registry.md) — `SystemContext.rng` and
the per-system substreams.
Loading
Loading