Custom programming language built for scripting plugins in my engine.
import std.io;
std.io.print("Hello, World!");
Variables are defined using the let
keyword. An initializer is required, from which the
type of the variable is inferred. Once declared, a variable cannot change type.
let x = 1;
let y = "abc";
There is an exception to this rule. Re-declaring a variable with a new type will shadow the old variable in the current scope.
let x = 1;
let x = "abc"; // x is now a string.
Basic control sequences are supported using if
, while
, for
and else
with similar
syntax to C and C++.
if (condition) {
// statements
} else if (other_condition) {
// other statements
} else {
// final statements
}
while(true) {
// do something
}
for(let i = 0; i < 10; ++i) {
// do something
}
The following builtin types are available:
int
uint
float
bool
str
list
any
void
any
refers to any posible type and is useful for passing generic arguments or
external types around (see later). A string is created by wrapping text inside
quotes ("abc"
). A list can be created by using square braces:
let my_list = [1, 2, 3];
let empty_list = [];
empty_list.append(4);
Additionally, new types can be defined using structs (see later).
A function is declared using the fn
keyword, followed by its name, parameters and a
return type.
fn foo() -> int {
// execute foo()
}
fn bar(a: int, b: any) -> void {
// do something with parameters a and b.
}
A function can then easily be called using standard function call syntax:
bar(a, "xyz"); // since the type of b was 'any', we can pass any type.
A struct allows for creating new types that can be used anywhere after the struct was declared.
A struct declaration starts with the struct
keyword and is followed by a block with member
declarations. Note that like in C and C++, the struct declaration must end with a semicolon.
struct MyStruct {
x: int = 0;
y: str = "abc";
};
Similar to variables, the initializer for struct members is required and must be convertible to the member type. An instance of a struct can be created as follows:
let s = MyStruct { 42, "The meaning of life" };
let s2 = MyStruct { 42 }; // 'y' is initialized to "abc"
The initializers are assigned to the members in order. You can pass fewer initializers to use
the default values for those members. To access a struct member, use the ->
operator.
let s_x = s->x;
Sometimes types do not match exactly. When this happens, the interpreter will try to cast
to the type the called function, variable or struct initializer expects. If this is not
possible, a TypeError
will be reported. You can also cast between compatible types manually
by using the same syntax as struct construction:
let x = float { 5 };
let y = uint { 6 };
Because this syntax for creating unsigned integers is cumbersome, a literal suffix u
is also provided that does the same thing.
let x = 6u;
Importing code from other files (like the standard library) is made possible through
modules. A module can be imported using the import
statement.
import std.io;
This will look in the provided module path (by default only includes the standard
pscript_modules/
folder) for a file std/io.ps
and import it. After the import,
functions, types and variables from the imported file can be accessed by prefixing
their names with the module name.
import std.io;
std.io.print("Hello, World!");
Through the C++ API, external functions and variables can be accessed inside a PScript
program. To do this, the function or variable must be declared using the extern
keyword.
extern fn foo(x: int) -> str;
extern let bar: int;
After that, hook up the names to your C++ objects using the ps::extern_library
class.
ps::string_type foo(int x) {
return ps::string_type { std::to_string(x) };
}
int bar = 42;
ps::context ctx(memsize);
ps::extern_library lib {};
lib.add_function(ctx, "foo", &foo);
lib.add_variable("bar", &bar);
ps::execution_context exec {};
exec.externs = &lib;
ctx.execute(my_script, lib);
These external functions and variables can now be used like normal PScript objects.
Sometimes it is useful to pass objects to functions by reference. This means no copy is
made, and changes to the parameter are also visible to the caller. By default, lists,
strings and structures are always reference types. To create a reference to another
type, prefix its name using an ampersand &
.
fn foo(x: int&, a: int, b: int) -> void {
a = a + 1;
x = a + b;
}
let result = 0;
let a = 10;
let b = 9;
foo(result, a, b); // will set result to 20, but leave a unmodified.
WIP. for now, please refer to the source code in modules/std
.
PScript programs can be executed through the C++ API implemented in this repository.
Everything is available with a single #include <pscript/context.hpp>
. To execute a
script, first load it into a ps::script
object.
ps::script source { "let x = 0; let y = 10;", ctx };
To run scripts, you must create a ps::context
object. This object signifies a single
execution context. Variables created in scripts are created in their context, imported
modules are imported for the entire context, etc. Each context has a memory pool, whose
size must be specified at creation (in bytes).
static constexpr std::size_t mem_size = 1024 * 1024; // 1 MiB of memory.
ps::context ctx { mem_size };
To execute a script on a context, simply call context.execute(script);
. This function
has an additional optional parameter of type ps::execution_context
. This parameter
allows specifying various options to control execution.
- Control standard input and output streams by setting
in
,out
anderr
. - Provide an extern library by building a chain of
ps::extern_library
objects (see also the providedps::extern_library_chain_builder
). This chain structure allows linking multiple extern libraries to the same context. - Provide additional paths to search for modules by adding paths to
module_paths
. Note that this also allows removing the path to the standardpscript_modules/
folder.
Functions can be made to accept any number of arguments by passing in a variadic parameter.
This parameter must always be the last parameter declared. In the function call, the
variable will behave as if it is a list<any>
.
fn print_all(x...) {
for (let i = 0; i < x.size(); ++i) {
std.io.print(x[i]);
}
}
print_all(1, 2.2, 3, "abc");
As you can see, a variadic parameter can store different types. Variadics can be passed around to other functions or constructors by expanding them.
fn printf(fmt: str, args...) {
// pass all elements of args... through to the builtin format() function
std.io.print(fmt.format(args...));
}