I designed a memory management syntax that is quite simple, almost like a GC lang, but at the cost of forcing programmers to use a special design pattern called “code as module tree”, which can be called scope tree-based memory management.
It follows the principles of C++'s RAII and Rust’s lifetime but implements in very different way.
Let me first demonstrate the process of writing the program.
When designing a program, programmers always first visualize the program’s modular architecture. Suppose the modular architecture is as follows: This is an FPS shooting game with two game modes: 1v1 and 5v5.
Write the program directly based on the module architecture diagram in your mind. See below.
scope FPS(Game1v1, Game5v5) {}
scope Game1v1(Player) {}
scope Game5v5(Player) {}
scope Player {}
If the memory used in each game needs to be reclaimed at the end of each game, then a destructor can be defined for the scope representing that module, and this destructor can then be associated with an external triggering event.
In this example, the player fires, defeats all enemies, and triggers a victory to end the game.
scope FPS(Game1v1, Game5v5) {}
scope Game1v1(Player) {
run fn end(){}
}
scope Game5v5(Player) {
run fn end(){}
}
scope Player {
fn fire(){
this..super.end()
}
}
Then memory management is done.
The run keyword is used to define a destructor. The difference between a destructor and a regular destructor is that the programmer doesn’t need to write the destructor’s code. The keyword name itself doesn’t have any special meaning; you can change it to something else, such as destruction.
Next, we’ll explain the underlying principles.
Programs can be divided into two categories: procedural programs and standby programs.
Procedural programs are characterized by continuous execution. Once execution ends, all newly created memory during the process can be reclaimed. Only input parameters and output return values interact with the outside world. Similar to how the operating system reclaims memory allocated by the main function after it finishes. Programmers don’t need to worry about memory reclamation for procedural programs because all newly allocated memory is automatically reclaimed after the function finishes running. You can simply understand a procedure as a function, although they are not entirely equivalent, but for now, consider it that way.
Standby programs, on the other hand, can remain in memory without executing any instructions. The reclamation of standby programs is always triggered by external events. The reclamation time is not determined by the program itself; it might be triggered after ten seconds or a million years. Therefore, its reclamation is always tied to some external event. Standby programs only store shared state, pool data, or pool-like data.
Write all the standby program parts according to the program’s logical architecture tree. When a parent node is reclaimed, all its child nodes are forcibly reclaimed. Therefore, you only need to focus on a few nodes that need to be reclaimed entirely.
This tree is precisely your program’s resident memory scope tree, logical architecture tree, module architecture tree, and the program’s logical memory model tree. At this point, the program is easiest to read and write because the code corresponds one-to-one with its logical architecture diagram, and the code and logical architecture diagram can be seamlessly converted using tools.
The trade-off is that this approach forces programmers to write programs according to a logical architecture. This syntax exchanges read/write simplicity for a code layout convention. This convention forces programmers to write programs according to a design pattern, where the code itself is the logical architecture.
Taking the game example above, forcibly recycling the player submodule every time the game is terminated is too performance-intensive; the goal is to create a new one and reuse it for each subsequent game. Under this syntax, programmers are forced to change it like this.
Figure 2
In languages like C++ where the lifetime can be changed, it might be changed as follows.
Figure 3
As you can see in Figure 2, just by looking at the diagram, you can understand that the player module can still be used after game1v1 is garbage collected. In Figure 3, it’s much harder to see whether player still exists after game1v1 is garbage collected. If there are four or five such sub-modules in the program, remains in memory after their parent module being garbage collected, it becomes very difficult to understand the garbage collection logic from the code. The reason the garbage collection logic is clear in Figure 2 is because it (is) forced to maintain the principle that “the code is its logical architecture.”
The convention of this syntax is that the lifetime of a variable is equal to the scope in which it is declared; this scope is unique and unchangeable, and the garbage collection of a child scope is forcibly carried out when the parent scope is garbage collected. Namespaces and functions are both scopes. All memory variables semantically have a unique parent association, either a specific scope (triggered by external garbage collection) or a specific function scope (automatically garbage collected when the function ends).
There are two other less commonly used keywords: up and defer.
The up keyword is used to mark functions and optimize the timing of memory reclamation in the function scope.
fn outer(){
inner()
}
up fn inner(){
a []int = [1,2,3]
}
Functions marked with the up keyword are not immediately garbage collected after the function ends; instead, they are collected when the parent function finishes. In the example above, variable a is collected when the outer function ends. However, note that although variable a is collected only after outer ends, it cannot be accessed within outer. The up keyword only optimizes the timing of garbage collection; it cannot change the access scope.
defer is used to reclaim resources requested from external sources, such as database connections and file handles. This is because memory management mechanisms can only automatically reclaim memory allocated to the current program and cannot reclaim memory allocated to remote programs (such as remote databases).
fn user(){
db_conn := libDb_newConn()
}
fn libDb_newConn(){
a := db.sendMessageToDb('Connect with me')
defer a {
db.sendMessageToDb('disconnect with me')
}
// defer must bind one variable
return a
}
db_conn is garbage collected and defer is executed when it goes out of scope.
So the user do not need to write the code of releasing db_conn


