Dynamic Borrow-Checking in Untyped Languages: A Novel Approach to Memory Safety
Dynamic Borrow-Checking in Untyped Languages: A Novel Approach to Memory Safety
A researcher has unveiled an innovative memory management system that enforces reference borrowing rules at runtime, enabling safer code in dynamically-typed languages without sacrificing performance.
The demonstration showcases a prototype language featuring dynamic typing, stack-allocated values, interior pointers, and single ownership semantics combined with constrained borrowing capabilities. While less expressive than Rust's borrow-checker, this system surpasses traditional second-class reference approaches, enabling constructs like external iterators that were previously impractical.
The Core Challenge
Creating a dynamically-typed system that enforces borrowing rules presents a fundamental problem: without static type information, runtime checks become necessary. The innovation lies in implementing these checks efficiently while providing meaningful error diagnostics. The researcher demonstrates that reference-counting operations incur minimal overhead when carefully designed, with counts stored on the stack to minimize cache impact and never shared across threads to eliminate atomic operations.
Implementation Strategy
The system tracks reference states using a single integer per variable:
- Moved status: indicated by the minimum integer value
- Borrowed state: negative values representing active borrows
- Available state: zero, allowing new borrows or shares
- Shared state: positive values counting active shares
Each borrowed or shared reference maintains metadata identifying its type (owned, borrowed, or shared), the lending variable, and the owning variable. This 42-bit structure fits efficiently into 16-byte references while supporting 8-megabyte stacks with 8-byte alignment.
Safety Rules and Constraints
The system enforces several restrictions to maintain memory safety:
- Boxed references cannot contain borrowed or shared references, ensuring such references remain stack-resident
- Active borrowed references prevent creation of additional borrows from the same value
- Active shared references prevent borrowing, while multiple shared references may coexist
- Values cannot be moved while bearing any active references
- Partially-moved values become entirely inaccessible until fully replaced
- Variables may only hold references to longer-lived values
- Block returns cannot contain references to variables defined within that block
Expressiveness and Capabilities
Despite these constraints, the system enables powerful patterns previously impossible with second-class references. Developers can place references inside tuples, return references from functions, implement both copying and reference-based iterators, and handle complex data structures like linked lists. The borrow-checker correctly manages lifetimes for both external iterators returning shared references and those returning borrowed references.
Error Diagnostics
When violations occur, the runtime performs stack scanning to identify the exact reference causing the problem, delivering precise error messages that pinpoint problematic values and their locations rather than generic constraint failures.
Cross-Stack Transitions
For thread spawning or transitions between dynamically-typed and statically-typed code, the system supports reborrowing references onto new stacks with modified provenance, preventing reference-count interactions across stack boundaries. This maintains thread-safety while enabling integration with statically-compiled code that never directly observes reference counts.
Design Philosophy and Trade-offs
The researcher explored multiple approaches during development, including shadow allocations enabling disjoint borrows and partial moves. However, safe transitions to statically-typed code and practical error-handling requirements led to the current design. The system prioritizes predictable performance and understandable semantics over maximum expressiveness, maintaining the principle that static typing constrains rather than changes behavior.
Compared to reference-counting systems in languages like R and Swift, this approach avoids copy-on-write overhead and integrates naturally with interior pointers and explicit stack allocation. Against static systems like Rust, it trades complexity for flexibility in dynamically-typed contexts, though the explicit annotation requirements may feel cumbersome in practice.
Future Directions
The researcher acknowledges the system feels operationally intricate despite its theoretical soundness. Potential improvements include adopting second-class references with coroutines in the style of the Hylo language, or pursuing complete static typing with improved ergonomics for handling values of unknown types during meta-programming phases.