undar-lang/docs/WIP-Readme

275 lines
11 KiB
Plaintext

A permacomputing & game oriented language.
> Permacomputing is a design practice that encourages the maximization of hardware lifespan, minimization of energy usage and focuses on the use of already available computational resources. It values maintenance and refactoring of systems to keep them efficient, instead of planned obsolescence, permacomputing practices planned longevity. It is about using computation only when it has a strengthening effect on ecosystems.
The language's compiler is written in its own libc. Not all of C stdlib, a "lagom" (just enough) implementation. Undar targets the Uxntal VM Bytecode (16 bit interpreted VM) and the "Reality Engine" VM Bytecode (32 bit VM), both only require a C compiler to compile the VM which makes the language portable to any system with a C compiler.
This is similar to Java's "build once run anywhere" but even more flexable, a Undar program should be able to run on a Linux desktop, a website using Wasm, a embedded device, or even something obscure like a PDP-11.
Memory Management
Memory is handled deterministically by the compiler following RAII style rules. Unless the devloper is doing some fine grained optimization they will need to think about memory as much as any GC language developer would.
Memory is partitioned into a C style "heap" and "stack". the difference is the behavior of the heap matches that of the stack which are laid out as a "region per call frame". A frame is made up of N many "locals" in the stack (bottom up) and a handle to the beginning of that frames allocated memory in the heap (top down).
When a function gets called, it creates a new region in heap memory starting at the end of the previous frame. (or at the beginning of memory if no function has been called). And a new static region for "locals" on the stack.
Variables allocated in the frame's region are owned by default by the frame they are allocated in.
When variables get modified within the current frame they will get updated in that frame in their current location in memory.
All complex types (Plex, arrays, strings) use reference handles. Allocations prepend the length to the allocation, its to make sure that its impossible to have the kind of buffer overflow bugs common in C like languages. It also optimizes copy speed since you do not have to do a O(n) search for the size of the block.
If the size of the variable changes (like adding a value to the end of a array), a new block is allocated at the end of the region of the size of that value + the old value.
When a variable is passed to a child function primitive types are pass by value, complex types (Arrays, Plexes, Strings) are pass by reference.
Variables passed as arguments to the child follow the same rules except when the size of a complex type changes, the new value is allocated at the end of the childs frame and the owner is now the child. This is the same as C++/Rust "move".
When a function "returns" the frame is deallocated and all values allocated by it is freed; thus freeing memory deterministically.
When a variable is returned if it is primitive it is just passed back to the parent. If it is complex the value it is copied back to the parent.
This allows for the low resource usage of a C but the convenience of a garbage collected language like C# or Go but without the GC pauses.
Built in Types
Type Description
byte Character (architecture specific)
u8 exactly 8-bit unsigned int
i8 exactly 8-bit signed int
u16 exactly 16-bit unsigned int
i16 exactly 16-bit signed int
u32 exactly 32-bit unsigned integer
i32 exactly 32-bit signed integer
int 1 word signed number
nat 1 word unsigned number
real Q16.16 fixed-point real number
str string
bool true/false
Standard Library:
String functions
Array functions (sort, filter, etc.)
Fonts
Immediate Mode UI (buttons, forms, etc.) In the style of ImGUI / Raylib.
Trigonometry
Unit of measure
2D Graphics Math/Primitives
2D Collision/Physics
3D Graphics Math/Primitives
3D Collision/Physics
Localization
Example: uPaint (paint.ul)
```ul
/**
* Constants
*/
const byte BLACK = 0;
const byte WHITE = 255;
const byte DARK_GRAY = 73;
const byte GRAY = 146;
const byte LIGHT_GRAY = 182;
byte selected_color = 255;
trait Device {
nat handle;
}
plex Screen implements Device {
nat handle;
nat width;
nat height;
byte[] buffer;
draw() {
write(this, this.buffer, this.buffer.length);
}
}
plex Mouse implements Device {
nat handle;
nat x;
nat y;
bool left;
bool right;
bool middle;
bool btn4;
}
/**
* Main function
*/
function main() {
Screen screen = open("/dev/screen/0", 0);
Mouse mouse = open("/dev/mouse/0", 0);
outline_swatch(screen, BLACK, 1, 1);
outline_swatch(screen, WHITE, 21, 1);
screen.draw();
loop {
mouse.refresh();
if (!mouse.left) continue;
int box_size = 20;
int x = 1;
int y = 1;
byte color = BLACK;
outlined_swatch(screen, color, x, y);
set_color(box_size, x, y, mouse.x, mouse.y, color);
color = WHITE;
x = 21;
outlined_swatch(screen, color, x, y);
set_color(box_size, x, y, mouse.x, mouse.y, color);
screen.draw();
rectangle(screen, selected_color, x, y, 5, 5);
}
exit(0);
}
/**
* Checks if the click is within the bound and update the selected color if so.
*/
function set_color(int box_size, int bx, int by, int mx, int my, byte color) {
int right = bx + box_size;
int bottom = by + box_size;
if (mx < bx) return;
if (mx > right) return;
if (my < by) return;
if (my > bottom) return;
selected_color = color;
}
/**
* Draw a color box with a grey outline, if selected use a darker color
*/
function outline_swatch(Device screen, byte color, int x, int y) {
byte bg_color = GRAY;
if (selected_color == color) {
bg_color = DARK_GRAY;
}
rectangle(screen, bg_color, x, y, 20, 20);
rectangle(screen, color, x + 2, y + 2, 17, 17);
}
/**
* Draw a rectangle
*/
function rectangle(Device screen, byte color, int x, int y, int width, int height) {
int base = y * screen.width + x + screen.buffer.ptr + 4;
for (int i = height; i > 0; i--) {
int row = base + width;
memset(screen.buffer, row, color, width);
base += screen.width;
}
}
```
Plex
"Class" and "Struct" are very loaded terms with a lot of historical baggage. To try and fix this we call our structs "Plexes". A plex is a structure which, stores memory like a struct but can be used syntactically like a class. the naming of "plex" comes from douglas ross's paper "a generalized technique for symbol manipulation and numerical calculation". Unlike classes it should not describe a object in real life, but be a convenitent way to implement compositions + encodings instead of oop/polymorphisms. instances of a plex in memory are called "atoms".
Plexes support permacomputing by allowing the developer to use lower level traits with a friendly syntax.
```ul
plex Player {
str name;
real[3] pos;
init(str name, real[3] pos) {
this.name = name;
this.pos = pos;
}
update() {}
logout() {}
}
```
the language is statically typed and similar to c but with some array semantic ideas from fortran like row major, fortran style replaces need for vec or mat. arrays are first class values, the compiler uses optimized opcodes for array manipulation.
```ul
real[3] pos = [1.0, 2.0, 3.0];
real[3,3] mat = identity(3);
real[3] result = mat * pos; // compiler generates matrix-vector multiply
```
Row-major order
No vec3=/=mat4x4 structs needed
Supports composition, slicing, element-wise ops
it has a abstraction layer for devices that work as system calls that can open, read, write, close, and use ioclt for extra system calls that are beyond ops.
Devices
Core devices include
Window/Screen
Terminal
File I/O
Gamepad/Joystick
Sound
Networking
Cryptography
Random number generator
Date/Time
Mouse
Keyboard
Devices are accessed via a namespace "path/to/device" and are implemented outside of the VM's runtime. This allows for the trait of the system to be the same within the VM but allow for specific variations on a concept depending on the device it is running on.
Immediate Mode GUI
UI elements are draw directly on the canvas and syntactically are spacial to show which elements are child elements of each other. Calling a function is the equivalent of a redraw of that element
3D Modeling
3D Primitives (Similar to .kkrieger) allow for the construction of more complex 3D models from the manipulation of primitives. It also has the ability to create textures dynamically similar to .kkrieger.
Devices: Unified I/O (Plan 9 / 9P-Inspired)
Devices are an abstraction called a device which acts like a device like a screen, mouse, etc. that is inspired by Plan9. Plan 9 was an operating system developed at Bell Labs that treated all resources (including network connections and UI elements) as files in a hierarchical namespace. it allows files, web requests, sockets, etc. to be viewed through a simple unified trait. it is very similar to the 9p protocol where everything is a file in a filesystem, it just might be a file located on a server halfway across the world or on another planet. the only thing that is the difference is that it takes longer to read the file on mars compared to the file on your local system. the way the device system works is that it is written by a developer in another language like c, but from Undâr's pov the trait remains the same, its just a namespaced device like all the other devices, so that it is easy to understand by a new developer. so it is comparable with tcp/udp sockets or something esoteric like a lorawan network, or some new communication method that hasn't been invented yet.
```ul
Client client = Client("tcp://localhost:25565");
if (client.attach(auth)) {
Player[] players = client.read("players");
client.write("me", me.update());
client.close();
}
```
Device Operations
Op Meaning
.attach() Authenticate and open communication
.open() opens a device for doing operations on
.create() creates the plex from the database graph file from file structure
.remove() removes the plex from the database graph file from file structure
.read() reads from a device
.write() writes to a device
.walk() moves around the filesystem or through the graph
.flush() cancels long operation and dumps whatever is in buffer
.close() Close communication [also `clunk` works for historical accuracy]
.stat() returns the status of the file resource
.version() returns the version code for the connected device
Actor System
The "Reality Engine" VM uses a Actor System where multiple actors are assigned to a single core. Actors are used to partition memory efficiently. If we used a async/await system jumping between threads would wreck havock with the fast bump allocator. Actors have fixed sized pools that are decided by the developer at compile time which means the logic can remain the same.
Message passing allows for non-blocking and non-locking communications between actors.
Communication Protocol
Bounded mailbox system
Message flow:
Main thread sends via send_message()
Message copied into actor's mailbox
Actor processes in on_message() during next cycle
Actor sends response via send_response()
Failure Behaviors
Behavior |Action Example| / Use Case
--|--|--
RESTART |Reset region offset to 0 | Network reconnection
EXIT |Free region |Session termination
CRASH |Halt entire VM |Failure of core system
```