Skip to content
Closed
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- **Registry login supports an explicit registry address.** `wfl login
[registry]` scopes a token to that HTTPS origin, mismatched logins are
rejected, and `wfl logout` can recover malformed or incomplete credentials.
- **Cyclic values no longer abort the interpreter during display, diagnostics,
or isolated-module cloning.** List/object formatting now detects cycles and
caps nesting depth, while deep clones preserve cycles and shared references
inside the cloned graph.
- **Subprocess policy is enforced on every process launch** (shell path and
direct-exec / `with arguments` path). Previously, `shell_execution_mode` and
related checks ran only when the engine believed a shell was required, so
Expand Down
41 changes: 41 additions & 0 deletions Dev diary/2026-07-16-cycle-safe-values.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Dev Diary — Cycle-safe values (2026-07-16)

## Context

Lists and objects are reference-counted mutable values, so valid WFL can build
self-referential and mutually recursive graphs. Value equality already handled
those graphs, but `Display`, `Debug`, and the deep clone used for module
isolation still traversed them recursively without cycle detection. Displaying
a cyclic value (or including one in a diagnostic) could therefore exhaust the
native stack, and cloning one could do the same during isolated lookup.

## What changed

- `Display` and `Debug` now carry per-format traversal state. A container that
reappears on the active path renders as `<cycle>`; shared acyclic values still
render normally each time they appear.
- Formatting stops after 64 nested containers and renders `<max-depth>`, keeping
very deep acyclic graphs comfortably below the native stack limit.
- Formatting uses `try_borrow`, so an incidental outstanding mutable borrow is
rendered as a marker instead of causing a `RefCell` panic.
- `Value::deep_clone` now memoizes list, object, and container-instance
placeholders before cloning their contents. Cycles point into the cloned
graph, shared references remain shared within that graph, and the clone stays
isolated from the source.
- Container parent links are cloned through the same memo instead of retaining
a reference into the source graph.

## Compatibility

Acyclic values below the depth limit retain their existing display and debug
forms. Only values that previously recursed indefinitely, exceeded the new
nesting guard, or were formatted while mutably borrowed receive marker text.

## Tests

- Rust-level self-cycle and list/object mutual-cycle formatting regressions.
- Deep-clone assertions for source isolation, back-reference preservation, and
shared identity.
- A depth-bound regression for acyclic nesting.
- An interpreter regression that constructs a self-referential list with WFL's
`push` statement and displays it as `[<cycle>]` without aborting.
25 changes: 25 additions & 0 deletions src/interpreter/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,3 +583,28 @@ async fn test_header_access_case_insensitive_via_request_object() {
"absent header should be nothing, got {result:?}"
);
}

/// A WFL program can insert a list into itself through `push`. Displaying that
/// value used to recurse on the native stack until the whole process aborted.
#[tokio::test]
async fn test_display_self_referential_list_is_cycle_safe() {
let source = r#"
create list items:
end list
push with items and items
display items
"#;
let tokens = lex_wfl_with_positions(source);
let mut parser = Parser::new(&tokens);
let program = parser.parse().expect("parse self-referential list program");
let mut interpreter = Interpreter::new();

let output = std::rc::Rc::new(std::cell::RefCell::new(String::new()));
let result = {
let _capture = super::io_capture::push_capture(std::rc::Rc::clone(&output));
interpreter.interpret(&program).await
};

result.expect("displaying a self-referential list must not abort or error");
assert_eq!(&*output.borrow(), "[<cycle>]\n");
}
Loading
Loading