This document provides a quick overview of the Plush programming language, its syntax, features, and built-in capabilities. It is intended for developers who want to get started with Plush and learn the basics of the language.
Plush is an experimental toy programming language and virtual machine inspired by JavaScript, Lox, Lua, and Python. It features a simple, minimalistic design with a stack-based bytecode interpreter, actor-based parallelism, and an easily extensible set of host functions.
The Plush language has:
- Dynamic typing
- Classes, objects, inheritance
- Closures/lambdas
- Dynamic arrays aka vectors/lists
- Dictionaries with JSON-like syntax
- A native byte array type
- A simple frame buffer API for graphics and animations
- A simple audio output API
- Memory safe, actor-based parallelism
- A copying garbage-collector that runs independently for each actor
- No global VM lock
Caveats and limitations:
- Error messages and error handling need improvement
- You may have to look at the source code to figure things out
- We could use your help in improving error messages
- May be missing functionality you're used to in other programming languages
- For example, not as many
StringandArraymethods as Python or JS - This is an area where you can potentially contribute
- For example, not as many
To run a Plush script, you can use the cargo run command, followed by the path to the script. For example:
cargo run examples/amigaball.pshThis will execute the helloworld.psh script and print "Hello, World!" to the console. More example programs
can be found in the examples/ directory. These examples are available under the
CC0 license (public domain).
Plush is a dynamically typed language and supports the following data types:
- Int64: 64-bit signed integers (e.g.,
10,-5). - Float64: 64-bit floating-point numbers (e.g.,
3.14,-0.5). - String: Immutable UTF-8 encoded strings (e.g.,
"hello",'world'). - Bool: The constants
trueorfalse. - Nil: The constant
nilrepresents the absence of a value. - Array: Ordered collections of values (e.g.,
[1, 2, 3]). - ByteArray: Raw, mutable byte buffers.
- Object: Instances of classes.
- Dictionaries: Hash maps with string keys, like JS/Python/JSON (e.g.,
{a:1, b: 2})
Variables are declared using the let keyword. By default, variables are immutable. To declare a mutable variable, use let var.
let x = 10; // Immutable variable
let var y = 20; // Mutable variable
y = 30; // Reassigning a mutable variable
Loop counters, which are mutable, must be declared with let var, e.g.
for (let var i = 0; i < 10; ++i)
$println(i);
Plush supports a range of arithmetic, comparison, and logical operators:
- Arithmetic:
+,-,*,/,_/,% - Comparison:
==,!=,<,>,<=,>=. - Logical:
&&,||,!
The _/ operator performs integer division, that is, truncated division which only accepts integer inputs and yields
an integer output, whereas the division operator / yields a floating-point value as output.
Note that unlike in JavaScript, the == operator performs reference equality for objects and arrays, not structural equality.
Plush provides if/else statements for conditional execution and for and while loops for iteration.
if (x > 5) {
$println("x is greater than 5");
} else {
$println("x is not greater than 5");
}
for (let var i = 0; i < 10; ++i) {
$println(i);
}
let var i = 0;
while (i < 10) {
$println(i);
i = i + 1;
}
Functions are defined using the fun keyword. They can take arguments and return values.
fun add(a, b) {
return a + b;
}
let result = add(5, 10);
$println(result); // 15
The syntax for array literals is similar to that of JavaScript, e.g.
let a = [0, 1, 2, 3, 4];
Array elements an be accessed using the indexing operator with square brackets, e.g. a[0] = 1.
ByteArrays can also be indexed using square brackets to read and write individual bytes.
The length of arrays and ByteArrays is accessed via the .len field.
Plush supports object-oriented programming with classes. Classes are defined using the class keyword, and instances are created by calling the class name as a function. Note that the first argument to a method, including init, is the explicit self argument representing the current object. This argument can have any name, which avoids the JavaScript issue with closures shadowing an implicit this argument.
class Point {
init(self, x, y) {
self.x = x;
self.y = y;
}
to_s(self) {
return "(" + self.x.to_s() + ", " + self.y.to_s() + ")";
}
}
let p = Point(10, 20);
$println(p.to_s()); // (10, 20)
Plush supports an import directive with a syntax similar to Python's. This makes it possible to import code from other files.
There are a few restrictions. For now, only relative local imports are supported. That is, you can import from other source files,
but Plush does not yet have a standard library to speak of. You also need to place all import directives at the beginning of the file.
Lastly, you can import functions and classes, but you can't import global variables from other units.
Example usage:
from ./datetime import DateTime;
from ./font import Font, render_text;
let date = DateTime();
render_text("Hello world");
-
Int64
abs(): Get the absolute value of this number.min(other): Returns the minimum of this number andother.max(other): Returns the maximum of this number andother.to_f(): Converts the integer to a 64-bit float.to_s(): Converts the integer to a string.to_hex(digits): Get a zero-padded and capitalized hexadecimal string representation of this integer.`
-
Float64
abs(): Get the absolute value of this number.ceil(): Returns the smallest integer greater than or equal to the float.floor(): Returns the largest integer less than or equal to the float.trunc(): Truncate the float and produce an integer value.sin(): Returns the sine of the float.cos(): Returns the cosine of the float.tan(): Returns the tangent of the float.atan(): Returns the arctangent of the float.sqrt(): Returns the square root of the float.pow(exp): Raise the value to the given power.exp(): Returns e raised to the power of the number.ln(): Returns the natural logarithm of the number.min(other): Returns the minimum of this number andother.max(other): Returns the maximum of this number andother.clip(min, max): Restrict the value of if it's outside the range defined byminandmax.to_s(): Returns a string representation of the float.format_decimals(n): Produce a string representation with a given number of decimals.
-
String
from_codepoint(int_val): Get a single-character string representing the given unicode codepoint value.byte_at(byte_idx): Get the UTF-8 byte at the given byte index.char_at(byte_idx): Get a string for the single character at the given byte index. Returnsnilif invalid.parse_int(radix): Try to parse the entire string as an integer of the givenradix. Returnsnilon failure.parse_float(): Try to parse the entire string as a float. Returnsnilon failure.trim(): Produce a new string without whitespace at the beginning or end.upper(): Produce a new string as the uppercase version of the string.lower(): Produce a new string as the lowercase version of the string.split(sep): Given a separator string, split a string into an array of parts.
-
Array
with_size(size, value): Creates a new array of the given size, filled with the given value.push(value): Adds a value to the end of the array.pop(): Removes and returns the last value from the array.remove(idx): Removes and returns the element at the given index.insert(idx, val): Insert a new element at the given index, shifting elements from that index to the right.append(other_array): Appends all elements fromother_arrayto the end of this array.
-
ByteArray
with_size(size): Creates a newByteArrayof the given size.resize(new_size): Resizes theByteArray. If the new size is larger, the new bytes are filled with zeros.load_u32(byte_idx): Reads a 32-bit unsigned integer from theByteArrayat the given byte index.store_u32(byte_idx, value): Writes a 32-bit unsigned integer to theByteArrayat the given byte index.load_u16(byte_idx): Reads a 16-bit unsigned integer from theByteArrayat the given byte index.store_u16(byte_idx, value): Writes a 16-bit unsigned integer to theByteArrayat the given byte index.load_f32(byte_idx): Reads a 32-bit float from theByteArrayat the given byte index.store_f32(byte_idx, value): Writes a 32-bit float to theByteArrayat the given byte index.get_u32(index): Treat the byte array as an array of u32 values and read the element at the given index.set_u32(index, value): Treat the byte array as an array of u32 values and write the element at the given index.get_f32(index): Treat the byte array as an array of f32 values and read the element at the given index.set_f32(index, value): Treat the byte array as an array of f32 values and write the element at the given index.num_u32(): How manyu32values can fit in thisByteArray. Size must be divisible by 4.num_f32(): How manyf32values can fit in thisByteArray. Size must be divisible by 4.memcpy(dst_idx, src_bytes, src_idx, len): Copies a block of memory from a sourceByteArrayto this one.zero_fill(): Overwrite the contents of theByteArraywith zeros.fill_u32(start_index, count, value): Fills a portion of theByteArraywith a repeated 32-bit unsigned integer value.blit_bgra32(dst_width, dst_height, src, src_width, src_height, dst_x, dst_y): Copies a rectangular region from a sourceByteArrayinto thisByteArrayat a specified position, with alpha blending. This method assumes that both the source and destination buffers contain pixel data in the BGRA32 format.
-
Dict
has(key): Check if the dictionary contains this key.
Plush provides a set of built-in host functions that can be accessed from your code. Host
functions are prefixed with a $ sign and serve to expose functionality from the host system,
including various input/output functions, as well as the ability to create new actors and
communicate with them.
These host functions are defined in src/host.rs:
$time_current_ms(): Returns the current time in milliseconds since the Unix epoch.$cmd_num_args(): Get the number of command-line arguments available to the program.$cmd_get_arg(idx): Get the command-line argument at the given index. Returnsnilif absent.$cmd_get_arg_or(idx, default): Get the command-line argument at the given index. Returnsdefaultif absent.$print(value): Prints a value to the console.$println(value): Prints a value to the console, followed by a newline.$readln(): Read one line of input into a string.$read_file(file_path): Read an entire file into a newByteArray.$read_file_utf8(file_path): Read an entire file encoded as valid UTF-8 into aString.$write_file(file_path, bytes): Writes aByteArrayto a file.$actor_id(): Returns the ID of the current actor.$actor_parent(): Returns the ID of the parent actor.$actor_sleep(msecs): Pauses the current actor for the specified number of milliseconds.$actor_spawn(function): Spawns a new actor that executes the given function.$actor_join(actor_id): Waits for an actor to finish and returns its result.$actor_send(actor_id, message): Sends a message to the specified actor.$actor_recv(): Receives a message from the current actor's mailbox, blocking until a message is available.$actor_poll(): Polls the actor's mailbox for a message, returningnilif empty.$window_create(width, height, title, flags): Creates a new window.$window_draw_frame(window_id, frame_buffer): Draws a frame buffer to the specified window.$audio_open_output(sample_rate, num_channels): Opens an audio output device with the specified sample rate and number of channels. Returns a device ID.$audio_write_samples(device_id, samples): Writes aByteArrayoffloat32audio samples to the specified audio device.$audio_open_input(sample_rate, num_channels): Opens an audio input device with the specified sample rate and number of channels. Returns a device ID .$audio_read_samples(device_id, num_samples, dst_ba, index): Readsnum_samplesoffloat32audio samples from the specified audio device into a destinationByteArraystarting atindex. This function blocks until enough samples are available.$exit(code): End program execution and produce the given exit code.
Plush supports actor-based concurrency, which allows you to write parallel programs that are safe and easy to reason about. Actors are independent processes that communicate by sending and receiving messages. A parent actor can spawn a child actor using the $actor_spawn(f) host function. This function takes a function as argument to be executed in the new actor. Actors work like isolated process and can only communicate through message passing. When an actor spawns a child, the child gets a copy of the parent's global variables.
fun worker() {
let msg = $actor_recv();
$println("Received: " + msg);
}
let worker_id = $actor_spawn(worker);
$actor_send(worker_id, "Hello from the main actor!");
$actor_join(worker_id);
This example spawns a new worker actor, sends it a message, and then waits for it to complete. The worker receives the message and prints it to the console.
At the moment there is no debugger and you may find that error messages are lackluster. Unsupported behaviors can result in Rust panics, sometimes without helpful messages. I've been working on gradually improving the error messages to make Plush more user-friendly, but PRs to improve this are welcome.
To help in debugging, you can print values with $println() and you can use the built in assert() statement to
validate your assumptions. If you run into a Rust panic with not enough context, you can also run Plush with
RUST_BACKTRACE=1 to produce a backtrace,
e.g.
RUST_BACKTRACE=1 cargo run my_program.psh
In Plush, graphical applications often handle image data directly in memory. This is typically done using ByteArray objects, which represent raw, mutable buffers. This approach provides a high degree of control and performance for graphics-intensive tasks.
The pixel data for frame buffers is stored in the BGRA32 format. This means that each pixel occupies 4 bytes in memory, with the
blue component at the lowest memory address, and the alpha component at the highest address.
When working with 32-bit integer values to represent colors, this corresponds to a little-endian 0xAARRGGBB format. Most examples in this project use a helper function to construct color values:
// Helper function to convert RGB values to a 32-bit color
fun rgb32(r, g, b) {
// The alpha channel is set to 0xFF (fully opaque)
return 0xFF000000 | (r << 16) | (g << 8) | b;
}
let red = rgb32(255, 0, 0);
Here are some of the common operations used to manipulate image data stored in a ByteArray:
- Creating a framebuffer: A
ByteArrayis created to hold the pixel data for a window or image.let frame_buffer = ByteArray.with_size(width * height * 4); - Setting a single pixel: The
store_u32method can be used to set the color of a single pixel at a given(x, y)coordinate.let index = y * width + x; frame_buffer.store_u32(index, color); - Filling a rectangle: The
fill_u32method is an efficient way to fill a rectangular area with a single color.// Fills a width x height rectangle at (x, y) for (let var j = y; j < y + height; ++j) { let start_index = j * width + x; frame_buffer.fill_u32(start_index, width, color); } - Clearing the buffer: The
zero_fillmethod can be used to quickly clear the entire buffer to black.frame_buffer.zero_fill();
By manipulating the ByteArray directly, you can implement a wide range of graphics effects and rendering techniques.