Skip to content

[M1-DET-01] Deterministic mode + sim math rules (G-R8 trait, substreams, sim-source scan) - #34

Merged
offdev merged 2 commits into
masterfrom
m1-det-01-determinism-mode
Sep 15, 2026
Merged

offdev merged 2 commits into
masterfrom
m1-det-01-determinism-mode

Conversation

@offdev

@offdev offdev commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Deterministic mode + sim math rules (FR-1.4, S-7, PRD §10.3; ARCH-010; M1-DET-01 scope, nothing else).

What this lands

  • G-R8 compile-time trait — new public header src/laige-sim/include/laige/sim/determinism.h: SimMathBackend (FixedPoint16_16 default / FloatPinned32), DeterminismConfig, detail::IsDeterminismSafe<T> (integers, enums, fpx16_16, float as the fp32_pinned Scalar, the four SimMath<B>::Vec2/Vec3; double never safe), and LAIGE_DETERMINISM_SAFE(Type, Members...) with a static_assert at the mark site. Enforced by a third static_assert in World::registerSystem over the declared I/O, with an actionable message.
  • PRNG substreams — World::Options.seed/deterministic; registerSystem derives Prng::deriveSubstream(seed, systemId) per system (id 0 = master, never assigned); runSystems hands the stream to SystemContext.rng (advanced in place — the stream state IS the replay state).
  • Config surface + backend selection — EngineConfig appends seed (u64) + DeterminismConfig (existing 3-member aggregate inits keep compiling); parseEngineConfig gains seed (0..2^53 exact-double bound; 2^53+2 is the smallest rejectable) + determinism (4 new rejection events, unknown nested key → warn + ignore, first failure wins); Engine::create registers the backend-matching built-in first and builds the matching PresentationSnapshot (zero added allocations — headless setup stays exactly 3); run_started gains seed/determinism/math (the laige-run CLI summary line is unchanged; --replay remains the M1-DET-02 stub).
  • Sim-source determinism scan — tools/laige-determinism-lint (Python 3 stdlib): D1a/D1b/D1c raw float/double, D2 unordered_*, D3 malformed markers, over src/laige-sim/** with comments/literals/raw strings stripped; same-line // LAIGE-DETERM-EXCEPTION: G-R8 <reason> markers are the documented false-positive policy, every suppressed line counted + printed (EXC-006). New determinism-lint CI job in both workflows + 3 CTest fixture tests in tests/tools.
  • Tests — tests/laige-sim/determinism_tests.cpp (14 cases; ctest -R determinism_mode): a trivial moving-entity sim produces bit-identical FNV-1a per-tick state hashes over 256 ticks in two consecutive runs (same build, same seed); different seed diverges; substreams match Prng::deriveSubstream exactly and are independent; deterministic == false → ctx.rng == nullptr; backend selection both ways; the full config-key table. tests/laige-sim/compile_fail/ (4 fixtures; ctest -R trait_compile): 1 positive + 3 negative, each asserting exit code and the G-R8 stderr fragment.
  • Docs (same change) — new docs/concepts/determinism.md (the ARCH-010 scope statement) + docs/api/determinism.md; updates to engine.md, entity.md, system_registry.md, sim_math.md, prng.md, testing.md, building.md, module READMEs, index docs. laige-api.json regenerated (573 → 588 symbols). Roadmap checkbox + change-log line + progress board.

Verify (all green)

Command Result
ctest --test-dir build (Debug GCC 16.2.1) 55/55
ctest --test-dir build-asan (ASan+UBSan) 55/55, leak-free
ctest --test-dir build-tsan (halt_on_error=1) 55/55
ctest -R determinism_mode 1/1 (14 gtest cases)
ctest -R trait_compile 4/4
ctest -R determinism-lint 3/3
python3 tools/laige-include-lint OK (1/10 deps)
python3 tools/laige-determinism-lint OK — 17 files, 0 violations, 15 marked exceptions
./build/bin/laige-api-scanner --root . --check laige-api.json OK — 588 symbols

Zero new warnings under NFR-8.10. CI YAML validated (job present in both workflows).

Notes / limitations

  • Deviation (surfaced, not silent): declared dependency M1-CFG-01 has NOT landed — the seed/determinism keys sit on the provisional parseEngineConfig surface (documented in engine.md, the header preamble, and the change-log line); M1-CFG-01 owns the final versioned schema.
  • Scope is same-build bit-identity (ARCH-010): cross-target proof lands with M1-DET-04, replay execution with M1-DET-02, stream-state introspection in the state hash with M1-DET-03.
  • Windows/macOS/MSVC verification is delegated to the P0 CI jobs (full suite + both lints run there).
  • 15 exception markers in-tree (EXC-006), all listed in the CI output on every run: engine.cpp (ADR 0003 JSON number policy), determinism.h (the trait's own float registration), entity.h/system.h/system_timing.cpp (wall-clock diagnostics, ARCH-009), presentation.h (wall-clock alpha).

…ms, sim-source scan)

Deterministic mode + sim math rules (FR-1.4, S-7, PRD 10.3; ARCH-010;
M1-DET-01 scope, nothing else):

- determinism.h (new public header, src/laige-sim/include/laige/sim/):
  SimMathBackend (FixedPoint16_16 default / FloatPinned32),
  DeterminismConfig {enabled, math}, the G-R8 trait
  detail::IsDeterminismSafe<T> (integers, enums, fpx16_16, float as the
  fp32_pinned Scalar, the four SimMath<B>::Vec2/Vec3; double never safe),
  detail::areDeterminismSafeMembers, and LAIGE_DETERMINISM_SAFE(Type,
  Members...) with a static_assert at the mark site. Enforced by a
  third static_assert in World::registerSystem folding
  detail::IoComponentSafety over the declared I/O (actionable message).
- PRNG substreams: World::Options gains seed/deterministic;
  registerSystem derives Prng::deriveSubstream(seed, systemId) per
  system (id 0 = master, never assigned) into the SystemRecord;
  runSystems hands the stream to SystemContext.rng (Prng*, NSDMI) and
  advances it in place - the stream state IS the replay state.
  SystemContext stays single-arg-initializable.
- EngineConfig appends seed (u64, kDefaultSimulationSeed = 0) +
  DeterminismConfig (existing 3-member aggregate inits keep
  compiling). parseEngineConfig gains seed (0..2^53 - the ADR 0003
  exact-double bound; 2^53+1 is indistinguishable from 2^53 and
  accepted; 2^53+2 is the smallest rejectable) + the determinism
  object (rejections config/seed_invalid, config/determinism_invalid,
  config/determinism_enabled_invalid, config/determinism_math_invalid;
  unknown nested key -> config/unknown_key warn, ignored; first
  failure wins). Engine::create registers the backend-matching
  built-in FIRST and builds the matching PresentationSnapshot
  (type-erased detail::PresentationHandle - zero added allocations;
  headless setup stays exactly 3). engine/run_started gains
  seed/determinism/math fields (the laige-run CLI summary line is
  unchanged; --replay remains the M1-DET-02 stub).
- tools/laige-determinism-lint (new; Python 3 stdlib): the sim-source
  scan over src/laige-sim/** - D1a float/double type tokens, D1b float
  literals, D1c double literals, D2 unordered_* containers, D3
  malformed exception markers. A char scanner strips //, /* */ comments,
  string/char literals, and raw strings before matching (word-bounded,
  case-sensitive). Same-line '// LAIGE-DETERM-EXCEPTION: G-R8
  <reason>' markers are the documented false-positive policy; every
  suppressed line is counted and printed (EXC-006). Exit 0/1/2.
- CI: new determinism-lint job in BOTH ci-pull.yml and ci.yml
  (ubuntu-24.04, python3 tools/laige-determinism-lint).
- Tests: tests/laige-sim/determinism_tests.cpp (suites DeterminismMode/
  DeterminismEngine/DeterminismConfigParse; ctest -R determinism_mode) -
  a trivial moving-entity sim produces bit-identical FNV-1a per-tick
  state hashes over 256 ticks in two consecutive runs (same build,
  same seed); a different seed diverges; substreams match
  Prng::deriveSubstream exactly and are independent; deterministic ==
  false -> ctx.rng == nullptr; backend selection both ways; the full
  config-key table. tests/laige-sim/compile_fail/ (4 fixtures +
  expect-compile-result.cmake.in; ctest -R trait_compile): the positive
  fixture compiles; the three negatives (a double member, an unmarked
  user struct, a double in the mark's member list) each fail to
  compile with the G-R8 message (exit-code + stderr-fragment
  assertions). tests/tools gains the determinism-lint-* fixture tests
  (clean tree with one marked exception -> 0; one violation per rule
  -> 1; real tree -> 0).
- Docs (same change, DOC-006/007): NEW docs/concepts/determinism.md
  (the ARCH-010 scope statement) + NEW docs/api/determinism.md;
  updates to docs/api/{engine,entity,system_registry,sim_math,
  prng}.md, docs/testing.md, docs/getting-started/building.md,
  docs/concepts/README.md, docs/README.md, src/laige-sim/README.md,
  tools/README.md. laige-api.json regenerated (573 -> 588 symbols).
- Roadmap: M1-DET-01 checkbox, progress board (14/25, 34/193),
  change-log line.

Verified: ctest 55/55 on build (Debug GCC 16.2.1), 55/55 on
build-asan (ASan+UBSan leak-free), 55/55 on build-tsan
(halt_on_error=1); ctest -R determinism_mode 1/1 (14 cases);
ctest -R trait_compile 4/4; ctest -R determinism-lint 3/3;
python3 tools/laige-include-lint OK; python3
tools/laige-determinism-lint OK (17 files, 0 violations, 15 marked
exceptions); laige-api-scanner --check OK (588 symbols); CI YAML valid
(determinism-lint job present in both workflows). Zero new warnings
under NFR-8.10.

Deviation (surfaced, not silent): declared dependency M1-CFG-01 has
NOT landed - the seed/determinism keys sit on the PROVISIONAL
parseEngineConfig surface (documented as provisional in engine.md, the
header preamble, and the change-log line); M1-CFG-01 owns the final
versioned schema.
The M1-DET-01 change-log line now carries 56f2835 / PR #34 (the
reference was pending until this step landed as a pull request).
@offdev
offdev merged commit 07fc79e into master Sep 15, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant