Skip to content

Memory 2: cycle detection investigation + Type::can_form_cycle foundation#402

Merged
danieljohnmorris merged 2 commits into
mainfrom
feature/mem-2-cycle-detection
May 18, 2026
Merged

Memory 2: cycle detection investigation + Type::can_form_cycle foundation#402
danieljohnmorris merged 2 commits into
mainfrom
feature/mem-2-cycle-detection

Conversation

@danieljohnmorris
Copy link
Copy Markdown
Collaborator

Summary

Investigation pass on the memory-2 brief (zero-gap-specs/briefs/memory/2-cycle-detection-brief.md). The brief asks for two optimisations to ilo's runtime cycle detector: incremental detection and type-based pruning of immutable types.

ilo does not currently have a cycle detector, and the surface language is structurally cycle-free. Records are immutable after construction. List/map "mutation" goes through Arc::make_mut (interpreter) or fresh-allocation copy-on-share (OP_RECCOPY / OP_RECNEW_EMPTY in the VM). Closures capture by value. There is no field-assignment expression. Refcounts always drain through normal drop chains.

That makes the brief's two deliverables non-applicable as written: there is no detector to make incremental, and no scanning to prune. The brief's underlying intent (foundation for the day ilo grows a mutation primitive that closes the cycle gap) lands here as a static classifier with a regression-surface test suite.

Time-box: 3 hours against a 1-2 month brief, as requested.

What's in the diff

Commit 1: add Type::can_form_cycle classifier (src/ast/mod.rs)

  • impl Type { pub fn can_form_cycle<F>(&self, resolve_record: &F) -> bool ... }
  • Returns true if a runtime value of the given static type could possibly participate in a reference cycle.
  • Defaults to true (cycle-capable) whenever information is missing (Any, Fn, unknown Named). Sound bias: marking a clean type capable only costs unnecessary scanning if and when a collector is added; the reverse would leak.
  • 12 tests cover primitives, list/map/optional/result of primitives, Any, Fn, unknown Named, primitive records, records with primitive collections, records with Any or Fn fields, self-referential records, mutually recursive records, and lists of records.
  • Today every well-typed value classifies cycle-incapable. If a future PR adds a mutation primitive that closes the cycle gap, the relevant classifier output flips and the test suite says so.

Commit 2: docs: memory model notes and cycle freedom (MEMORY-MODEL.md)

  • New top-level reference doc covering the RC model in both engines, why cycles are unreachable, what surface-language changes would flip that, and where the classifier hooks in.

What landed vs what is deferred

Brief acceptance criterion Status
Incremental cycle detection implemented and active by default Not applicable today (no detector)
Type-based pruning implemented; types correctly classified Classifier implemented + tested. Pruning hook is a no-op today (no scanner to skip from)
Existing test suite passes Yes. 3192 lib + full release suite with --features cranelift green
New tests cover: cycles between records, closure captures, multi-hop Covered at the classifier level; no runtime cycle tests because cycles are unreachable from the surface language
Stress test added: program that creates and breaks many cycles Not added. Cannot construct one from ilo source
Benchmark suite: cycle-heavy improvement, cycle-light neutral Not run. Nothing to benchmark
Memory leak detector clean Not run. No code path changes that could introduce leaks
CHANGELOG.md entry Repo has no CHANGELOG.md; skipped

Suggested follow-up: retire or rewrite the memory-2 brief to reflect the actual memory model. The investigation notes in MEMORY-MODEL.md are written so that work can drop straight in.

Test plan

  • cargo fmt --check clean
  • cargo clippy --lib --tests -- -D warnings clean
  • cargo test --release --features cranelift green (3192 lib + full suite)
  • All 12 new can_form_cycle tests pass

Doc sync

No user-visible surface change: no new builtin, no new flag, no semantic change. SPEC.md / ai.txt / SKILL.md / site untouched per the workflow's "internal refactors / perf with no surface-area change" carve-out. The classifier is a future-collector hook, no current call sites.

Follow-ups

  • Retire or rewrite the memory-2 brief (out of scope here).
  • Stress-test harness that tries to construct a cycle from every plausible surface-language angle and confirms drops are clean. Tripwire for any future change that adds mutation. Parking.
  • When a future PR proposes mutation, wire Type::can_form_cycle into the allocator and add the Bacon-Rajan-style detector behind it.

A foundation primitive for any future cycle collector. Returns true if a
runtime value of the given static type could possibly participate in a
reference cycle under ilo's current memory model.

Today every well-typed value classifies as cycle-incapable: records are
immutable after construction, lists and maps mutate via copy-on-share,
closures capture by value. The classifier exists as a regression surface
so that if a future PR adds a cycle-forming construct (field assignment,
ref cells, by-reference captures) the answer for the relevant type will
flip and the test suite will say so.

Defaults to true wherever information is missing (Any, Fn, unknown Named):
marking a cycle-capable type clean would leak; marking a clean type
capable only costs unnecessary scanning if and when a collector ever
exists.

12 tests cover primitives, nested wrappers, records of primitives,
records with Any / Fn fields, self-referential records, mutually
recursive records, and lists of records.
Reference doc covering ilo's reference-counting model and the
structural reasons cycles are unreachable today. Records are
immutable after construction, container mutation goes through
copy-on-share (Arc::make_mut, OP_RECCOPY), closures capture by value,
and there is no field-assignment expression. Lists the surface
changes that would close the cycle gap, and points at
Type::can_form_cycle as the foundation a future cycle collector would
plug into.

Context: the memory-2 brief asks for incremental cycle detection and
type-based pruning on a detector that does not exist. These notes
keep that finding alongside the model itself, so anyone picking the
work up later starts from accurate priors.
@danieljohnmorris danieljohnmorris merged commit 0291282 into main May 18, 2026
4 checks passed
@danieljohnmorris danieljohnmorris deleted the feature/mem-2-cycle-detection branch May 18, 2026 20:08
@codecov
Copy link
Copy Markdown

codecov Bot commented May 18, 2026

Codecov Report

❌ Patch coverage is 93.27731% with 8 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/ast/mod.rs 93.27% 8 Missing ⚠️

📢 Thoughts on this report? Let us know!

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