Is anyone interested in a new memory management syntax or design pattern

I designed a memory management syntax that is easy almost like a GC lang, drawing on the principles of C++ RAII and Rust lifetimes. The reason memory management is so complex in C++ and Rust is their lack of a syntax for defining module trees. If so, RAII and lifetimes could have been very simple, no need for borrow checker entirely. Depending on the specific language, the “module tree” could be a package tree, file tree, namespace tree, or class tree—essentially any tree representing scopes. The class syntax in OOP was originally designed to define module trees, but it suffers from significant flaws; I have explained the root causes of these syntactic defects in a YouTube video and proposed a new syntax design to resolve them.

In languages ​​that do not support this syntax, it can also be implemented indirectly using mechanisms such as defer and namespaces, as a design pattern. The underlying principles are the same.

Let me first demonstrate the programming process when using module trees for memory reclamation.

When designing a program, programmers typically visualize its module architecture. Consider the following architecture for an FPS game featuring two modes: 1v1 and 5v5.

Write the program directly based on the module architecture diagram in your mind. See below.

mod FPS(Game1v1, Game5v5) {}

mod Game1v1(Player) {
  run fn end() {}
}
  
mod Game5v5(Player) {
  run fn end() {}
}

mod Player {
  fn fire() {
    this..super.end()
  }
}

The mod elements in the code correspond one-to-one with the modules in the module architecture diagram.

If you need to reclaim the memory used by a game session once it ends, simply define a destructor for that module and link it to an external trigger event. In this example, the game ends in victory when the player fires and defeats all enemis.

Then the memory management is done.

Here, run is the keyword used to define the destructor; the difference from a standard destructor is that the programmer does not need to write the actual cleanup logic. The choice of the keyword run holds no special significance—you could just as easily use destruction or free_this_scope_all.

Let’s explain the underlying principles.

Programs can be categorized into two types: procedural programs and standby programs.

Procedural programs are characterized by continuous execution; once execution finishes, all memory allocated during the process can be reclaimed, with interaction with the outside world limited to input parameters and output return values. This is similar to how the operating system reclaims memory allocated by the main function after it terminates. Programmers do not need to worry about memory reclamation for procedural programs, as all newly allocated memory is automatically freed when the function finishes. You can think of a “process” simply as a function—while they aren’t exactly identical, this is a useful simplification for now.

Standby programs are characterized by their ability to remain in memory without executing any instructions. Their reclamation is always triggered externally; the program itself does not decide when to free the memory—it might be triggered in ten seconds or not for a million years. Consequently, the reclamation process is always tied to an external trigger event. Standby programs typically store only shared state, pool data, or pool-like data.

By structuring the standby program components according to the program’s logical architecture tree, you ensure that reclaiming a parent node forces the reclamation of all its child nodes. Thus, you only need to focus on the specific nodes that require collective reclamation.

This tree effectively represents your program’s resident memory scope tree, logical architecture tree, module architecture tree, and logical memory model. This structure makes the program easiest to read and write, as the code corresponds one-to-one with the logical architecture diagram, allowing for seamless conversion between the two using tools. The convention of this syntax dictates that a variable’s lifetime is tied to the scope in which it is declared—a relationship that is unique and immutable; reclaiming a parent scope forces the reclamation of its child scopes. Modules, packages, files, namespaces, and functions all constitute scopes. Semantically, every memory variable is associated with exactly one designated parent: either a module scope (where reclamation is triggered externally) or a function scope (where reclamation occurs automatically upon the function’s termination).

The syntax prohibits a scenario where a child node remains in memory after its parent node has been reclaimed. Semantically, this is equivalent to the situation where “memory remains unreclaimed yet the pointer is lost”—except that, in this case, what is lost is the semantic attachment point, akin to a handle.

There are two other less commonly used keywords: up and defer.

The up keyword is used to mark a function in order to optimize the timing of memory reclamation within the function’s scope.

fn outer(){
  inner()
}

up fn inner(){
  a []int = [1,2,3]
}

Functions marked with the up keyword are not reclaimed immediately after the inner function finishes; instead, they are reclaimed only when the outer function concludes. Note that although variable a is not reclaimed until after outer finishes, it cannot be accessed within outer. The up keyword is used solely to optimize the timing of resource reclamation; it does not alter the access scope.

The defer keyword is used to reclaim resources requested from external entities, such as database connections or file handles. This is because the memory management mechanism can only automatically reclaim memory belonging to the local program; it cannot reclaim memory held by remote programs (such as a remote database). Reclaiming memory held by a remote program generally requires sending a message to that program, instructing it to perform the reclamation.

fn user(){
  db_conn := libDb_newConn()
  // no need to write any code of releasing db_conn
}

fn libDb_newConn(){
  a := db.sendMessageToDb('Connect with me')
  defer a {
    db.sendMessageToDb('disconnect with me')
  }
  // defer must bind one variable
  return a
}

Programmers do not need to manually manage the release of db_conn; they simply need to properly define its scope, and the defer action will execute automatically once the variable goes out of scope.

2 Likes

I feel like this is the wrong place to upload this.

What is the reason to have “up”?? You’re pretty much accumulating everything, wouldn’t it be better to destroy asap.

I can’t rack my head on the effects of run.

On top of that there are a few holes

  • What if you were to defer an already deferred object?
  • How are you gonna track the defer destructor?
    Dynamically or statically in the type?
  • what if you want to run the destructor early
  • How does the stack behave on up functions?

Do you have an mvp impl or at least formalized it for reasoning?

I feel like this is the wrong place to upload this.

Could you clarify whether I posted in the wrong category or if the content is unsuitable for this forum? Although this specific syntax doesn’t exist in Odin, it can be implemented there as a design pattern.

What is the reason to have “up”?? You’re pretty much accumulating everything, wouldn’t it be better to destroy asap.

It depends on the specific situations. up does not affect stack variables; it only impacts heap memory. It is rarely used, as you noted, reclaiming memory as soon as possible is generally better. However, up can be useful in certain specific situations. Consider a concurrent program that shares a single allocator: if that allocation needs to be freed every time a function completes, it could lead to too many calls to that shared allocator. In this case, one could use up to optimize performance by consuming more memory in exchange for reducing the number of calls to the shared allocator. Such situations are uncommon, so in most cases, you do not need up.

Additionally, functions marked as up might enable a specific optimization: establishing a buffer zone at the nearest enclosing function that is not marked up, allowing the up-marked functions below it to reuse that same buffer zone—though this idea has not yet been fully fleshed out.

What if you were to defer an already deferred object?

I might not fully grasp what you mean. Applying defer again to an object would be similar to the multiple defer statements found in existing languages ​​that support this syntax; the only difference is that the execution of the defer is triggered when the variable goes out of scope.

How are you gonna track the defer destructor? Dynamically or statically in the type?

What exactly do you want to track? When you mention “dynamic” or “static,” are you referring to how the code is traced—such as using defer (which adds a destructor to the type but might reduce readability)—or to tracking whether execution actually occurs at runtime? If the concern is code readability, the use of defer does indeed compromise it; my current idea is to rely on IDE static analysis to provide hints or warnings. It is a problem now.

what if you want to run the destructor early

What exactly do you mean by “earlier”? Are you referring to a scenario where, for instance, no user presses a button? The program would still have something like a timer or an event loop, which could trigger the destructor. If a program lacks any function capable of triggering such an action, it inherently lacks the means (via function calls) to reclaim the shared pool-like memory.

sorry thought that this was about language features and syntax

allocate many times, free once
sounds like an arena allocator.

it is very common, and it is amazing for small frequent non-growing memory

create the arena allocator on the stack,
then just pass an arena allocator.

most call a fixed sized arena allocator a “region allocator”, “zone allocator”, or whatever
it is just a label

i see

i’m talking about implementation details,
how does it do this?

most languages with normal defer runs the defer at the end of the static scope/lexical scope.
so, sorry for yapping, when do you call the destructor, at the end of the lexical variable OR at the end of the lifetime of the value?

readability is not a problem, that is not as important as runtime and compile time

consider this program

main :: proc() {
  // foo is BIG Object
  foo := open_foo(foo_args..);
  defer foo close_foo($);
 
  // reads from foo, but doesn't depend on foo in lifetime
  // bar is smol object
  bar := open_bar(foo);
  defer bar close_bar($);

  // if foo's defer destructor doesn't run early
  // then it is a waste of resources
  foo.~defer(); // eeww, c++'s syntax

  for {
    // long lasting loop, like loooooong
    // e.g game loop
  }
}

not really a complete program or reasonable example, but i hope you can understand my intentions


for some reason i feel like i misunderstood you on some things


edit:
i want to add-on this blog post about arena allocator with non-trivial destructors

sounds like an arena allocator

create the arena allocator on the stack

Yes. If implemented using design patterns in Odin, it should be implemented as you described. In my syntax design, it’s essentially equivalent to forcing the release of a variable using defer after declaration (memory allocation) in a language like Odin. Note that it’s forced; similar to that the compiler automatically inserts defer to release the variable. The up keyword is only needed when the programmer doesn’t want defer to be forcibly inserted. Since the defer to release variables is written manually in Odin, the up keyword is not needed.

It is similar to, the programmer doesn’t need to manually define the allocator; instead, the compiler automatically defines the allocator based on keywords. Of course, in Odin, design patterns and static analysis can be used to check at compile time whether the design pattern is conformed to.

Using this design pattern, to some extent, is less performant than the programmer’s fine-grained control of the allocator, and it increases program complexity, which also reduces compilation performance. I will explain why using this design pattern in another reply later.

i’m talking about implementation details,
how does it do this?

My initial idea was to determine that at compile time through static analysis, but the design may have flaws. In my syntax design, variable ownership can only be transferred in two ways: through return values ​​or by storing it in an object property (a variable under mod). The compiler can determine the variable’s end-of-scope by tracing the return value to the outer function (or passing it to an object property). This is because there are no other ways to modify ownership (unlike in Rust where borrowing is possible). Below is sample code for modifying ownership.

fn outer(){
    a := inner()
    // the end of scope can only be some layer of return function
}

fn inner(){
    b []int = [1,2,3]
    return b
}

However, it’s best to implement the defer keyword only within my syntax. Implementing it through design patterns in Odin would make the code extremely complex, defeating the purpose of my design. In Odin, the simplest and most convenient approach is for programmers to manually defer to release the db_conn where a new db_conn is created.

consider this program

Yes, when programming using the module tree design pattern, the entire program is simply only one single, huge module tree. The code would look something like this:

main{
    root = RootModFoo.init(args..)
    for  {
        // long long game event loop
        root.Mod5v5.init() // This line called by an event in the loop
        root.Mod5v5.end() // This line called by another event in the loop
    }
}

mod RootModFoo(ModMainMenu, lazy Mod5v5) {}

In my syntax, there’s a keyword called lazy, which means that when initializing a parent module, the child module isn’t initialized; the programmer needs to initialize it manually. However, since this isn’t a discussion of my syntax, I haven’t gone into detail. The principle is the same. In other languages, this could be implemented using design patterns, resulting in a huge struct in Golang or Odin where the root isn’t initialized with child nodes; the programmer manually initializes a child node of struct and then attaches it to the main struct tree.

In Odin, I think it might be closer to this, because I’m not familiar with Odin and it might not be implemented this way, but the principle is the same.

You have a huge struct tree, which contains not only pool data but also the context allocator for each module.

main :: proc() {
  // big obj tree, but the submodule was not initialized yet.
  foo := open_foo(foo_args..);

  for {
    // do_some_other_thing()

    foo.x = initMod5v5() // This line called by an event in the loop
    freeAllMod5v5() // This line called by another event in the loop

    // do_some_other_thing()
  }
}

Foo :: struct {
    x : Mod5v5,
    y : ModMainMenu
    z : FooContextType // maybe I wrote this in a wrong way
}

What we’re really working on here isn’t syntax—note, the important thing isn’t my syntax—but the code itself is the module tree, which can also be called modular development. I’ll explain its purpose in a later reply.

This design pattern increases program complexity and may reduce performance. Its main purposes are:

  1. Memory safety (at least partially)

  2. Modularization

Let’s discuss memory safety first.

Memory leaks can be divided into two types:

  1. Memory not reclaimed, pointers lost.

  2. Memory not reclaimed, pointers not lost. Whether this constitutes a memory leak depends on the specific situation.

As long as this design pattern is satisfied, the first type of memory leak is guaranteed to be absent, but the second type is not guaranteed to be absent. Some cases in the second type are not considered memory leaks, so as long as the code satisfies this design pattern through static checking, memory safety can be guaranteed to a certain extent. Even Rust and the GC language cannot guarantee the absence of the second type of memory leak.

The principle is that all function scopes are subject to mandatory garbage collection of function variables, so any memory that might leak can only exist under the module tree (or struct tree). Therefore, for any unreclaimed memory, there will always be a pointer pointing to it. Even if the programmer fails to reclaim it in the program now, leading to a leak, the program can be easily fixed if the leak is discovered in the future, because the pointer to the leaked memory always exists. Since the module tree is reclaimed as a whole from a certain node, as long as a few key nodes are handled properly, memory safety can be guaranteed with a high probability.

The first type of memory leak is relatively difficult to troubleshoot. If your program has many lost pointers but the memory isn’t reclaimed, the leak is hard to understand. For example, in a concurrent program, pointers might not be lost in some cases (e.g., during testing), but lose them in others (e.g., during actual runtime). This kind of probability-based bug is very difficult to reproduce and troubleshoot. Ideally, this type of code should be avoided at compile time. Rust can solve this problem, and this design pattern also addresses it, as its principles originate from Rust.

Then there’s the issue of modular development, where memory leaks are also crucial.

Application development, such as game development, is very different from operating system development.

Operating system development involves programmers working on a shared project. Each module can always find a skilled programmer to develop and maintain it. These programmers are dedicated and understand memory management. Even if a memory leak bug does occur, you can always find the person responsible for that module to fix it.

However, the gaming industry is completely different. First, many “game developers” may not be professional programmers. Their expertise lies in game mechanics design and art; they lack sufficient knowledge of memory management and are unlikely to spend much time mastering it. Their primary job is designing game mechanics. Their development process is more like to purchasing several frameworks from the Unreal Engine Marketplace and extracting sub-modules to assemble a new game. This raises a problem.

For example, an independent game developer might buy two frameworks, framework_A(submod_A1, submod_A2) and framework_B(submod_B1, submod_B2), and create a new game, newGame(submod_A1, submod_B2). Without automatic memory management (or near-automatic garbage collection like in this design pattern), a memory leak might occur, and the developer might not even know how to fix it. They might lack sufficient memory management skills, but even if they did, they might not be able to solve it. This is because a project might actually consist of dozens of sub-modules. Even if the programmer is skilled in memory management, they would need to thoroughly understand the internal implementation of these dozens of sub-modules, which is extremely inefficient. Could consulting the framework seller solve the problem? May not, at least not efficiently. submod_A1 might be garbage collected correctly under framework_A, but when the programmer tries to assemble the sub-modules themselves, a leak might occur.

In absolute numbers, game designers who lack memory management knowledge far outnumber those who do. Godot, Unity C Sharp, and even Unreal Engine C++—they all choose automatic memory management. They choose this inefficient method to bring game designers unfamiliar with memory management into the game development community.

This design pattern provides users unfamiliar with memory management with another option. It allows them to easily manage memory, although the performance might be slightly worse than manual memory management by skilled programmers.

sorry for late reply

so linearity? you might like “austural” (if i remember correctly)

if you don’t know what is austural, it is a toy language that supports two type universes, free and linear

anyways, if you define objects as linear? then why have defer? wouldn’t linearity force defer to only be applied once?