ObSL is a small interpreted, thread safe, dynamically typed programming language designed for game logic inside
the
Obliberry Game
Engine, made to be easily extendable.
- Documentation
- Features
- Building from Source
- Quick Start
- Language Basics
- Printing
- Operators
- Control Flow
- Loops
- Arrays
- Functions
- Structs
- Objects
- Modules
- Error Handling
- Built-in Library
- Reflection
- Garbage Collection
- Examples
- Todo
- Resources & Acknowledgments
- License
- Dynamically typed variables
- Functions with default parameters
- Closures and first-class functions
- Structs with custom constructors
- Dynamic objects
- Dynamic arrays with negative indexing
- Module importing
- Exception handling
- Runtime reflection
- Built-in regex support
- Automatic garbage collection with cyclic reference handling
- C++20 compatible compiler (GCC, Clang, MSVC)
- CMake
- Git
git clone https://github.com/torkelicious/ObSL.git
cd ObSLmkdir build
cd build
cmake ..
cmake --build .| Option | Default | Description |
|---|---|---|
OBSL_BUILD_RUNTIME |
ON |
Builds the obsl_runtime CLI executable. Set to OFF to build only the obsl static library useful when embedding ObSL into another project (e.g. the Obliberry Game Engine) that doesn't need the standalone CLI/REPL. |
OBSL_BUILD_FORMATTER |
ON |
Builds the obsl_fmt CLI executable. Set to OFF if you don't intend to use the formatter. |
cmake .. -DOBSL_BUILD_RUNTIME=OFF./obsl_runtimeRunning without arguments starts the REPL. Pass a script path to execute a file, or use --lint to syntax-check a file
and emit a JSON report:
./obsl_runtime --lint path/to/script.obslOnly available when
OBSL_BUILD_RUNTIMEisON(the default).
println "Hello, World!";
Variables are dynamically typed:
var score = 100;
var name = "Player";
var alive = true;
var nothing = null;
Variables are declared using the var keyword:
var number = 100;
var text = "hello";
var enabled = true;
Variables can change type:
var value = 10;
value = "Now a string";
ObSL provides print and println.
print "Hello ";
print "World";
Output:
Hello World
println "Hello";
println "World";
Output:
Hello
World
var a = 10 + 5;
var b = 10 - 5;
var c = 5 * 2;
var d = 10 / 2;
var remainder = 10 % 3;
a == b;
a != b;
a > b;
a >= b;
a < b;
a <= b;
true and false;
true or false;
!true;
// Alternative syntax
true && false;
true || false;
a = 10;
a += 5;
a -= 2;
a *= 3;
a /= 2;
a++;
a--;
var bitAnd = 5 & 3;
var bitOr = 5 | 3;
var bitXor = 5 ^ 3;
var bitNot = ~0;
var left = 2 << 3;
var right = 16 >> 2;
var score = 100;
if (score > 50) {
println "Passed!";
} else {
println "Failed!";
}
var value = 2;
switch (value) {
case 1:
println "One";
case 2:
println "Two";
default:
println "Unknown";
}
The is operator checks runtime type:
var value = 10;
if (value is number) {
println "Number";
}
number
string
boolean / bool
null / nil
function / fn
array
object
Note: These are the type names accepted by
is, which are distinct from the strings returned by the reflection functiontype_of()most notably,isusesfunction/fnwheretype_of()returns"callable". See the Standard Library Reference fortype_of()'s exact return values.
var result = 5 + 3;
if (result is number) {
println "Math result";
}
is binds tighter than equality checks:
(x == y) is boolean;
var value = null;
if (value is null) {
println "Empty";
}
if (value is nil) {
println "Also empty";
}
var i = 0;
while (i < 5) {
println i;
i++;
}
var total = 0;
for (var i = 0; i < 10; i++) {
total += i;
}
var numbers = [10, 20, 30];
var sum = 0;
foreach (var number in numbers) {
sum += number;
}
for (var i = 0; i < 10; i++) {
if (i == 5) {
break;
}
}
Arrays are dynamic and support negative indexing:
var arr = [10, 20, 30];
println arr[0];
arr[-1] = 99;
arr[-1] // last element
arr[-2] // second last element
println arr.len;
arr.push(100);
var item = arr.pop();
arr.clear();
fn sayHello(name) {
println "Hello " + name;
}
sayHello("Player");
fn add(a, b) {
return a + b;
}
var result = add(5, 10);
fn damage(amount = 10) {
return amount;
}
println damage();
fn createAdder(amount) {
fn add(value) {
return value + amount;
}
return add;
}
var addFive = createAdder(5);
println addFive(10);
struct Vector3 {
x;
y;
z;
}
struct Player {
health = 100;
name = "Unknown";
}
Example:
var player = Player();
println player.health;
var obj = Object();
obj.value = 42;
obj.printValue = printValue;
Modules are imported using the using statement. Each import creates a module object containing the module's exported variables, which can be accessed through that object.
using "assets/scripts/math.obsl";
var result = math.add(5, 10);
By default, the module name is derived from the filename without its extension. In this example, math.obsl becomes math.
Use as to specify a custom name for an imported module:
using "assets/scripts/math.obsl" as Math;
var result = Math.add(5, 10);
The alias replaces the default filename-based name for that import. Here, the module is available as Math, not math.
Aliases are useful when multiple modules have the same filename or when a different name makes the code easier to understand.
- Path resolution: Module paths are resolved relative to the script root, which defaults to the current working directory.
- Caching: Modules are cached by their canonical file path. Importing the same module under different aliases reuses the same cached module object.
- Isolation: Module variables are accessed through the module object rather than being imported individually into the importing scope.
- Circular imports: Circular imports are detected and reported as errors.
- Custom loading: Embedding applications can provide a custom
ModuleLoaderto load modules from virtual filesystems or archives.
try {
var result = sqrt("invalid");
} catch (err) {
println err;
}
assert(player != null, "Player missing");
throw("Something failed!");
to_string(value);
to_fixed(value, decimals);
to_num(string);
sqrt(x);
pow(a, b);
abs(x);
min(a, b);
max(a, b);
floor(x);
ceil(x);
round(x);
random();
clamp(value, min, max);
sin(x);
cos(x);
tan(x);
atan2(y, x);
pi();
rad(degrees);
deg(radians);
lerp(start, end, t);
map_value(value, inMin, inMax, outMin, outMax);
len(text);
to_upper(text);
to_lower(text);
trim(text);
contains(text, value);
starts_with(text, value);
substring(text, start, length);
replace(text, old, new);
regex_match(text, pattern);
regex_search(text, pattern);
regex_replace(text, pattern, replacement);
regex_find_all(text, pattern);
file_exists(path);
read_file(path);
write_file(path, data);
get_file_ext(path);
read();
readln();
clock();
sleep_thread(seconds);
get_env(name);
exit(code);
type_of(value);
Returns:
null
bool
number
string
array
object
callable
get_arity(function);
has_field(object, "name");
get_fields(object);
ObSL includes automatic garbage collection with cyclic reference handling.
See the Examples/ directory for additional runnable usage examples.
- Proper Linter
- LSP?
This is still very much a toy language :)
ObSL's design and implementation were informed by:
- Crafting Interpreters by Robert Nystrom
- A Quick Guide to Interpreter Design in Modern C++ by Ayman Alheraki
| Library | License | Purpose | More |
|---|---|---|---|
| nlohmann/json v3.12.0 | MIT | JSON output for --lint mode |
docs/THIRD_PARTY_LICENSES.md |
ObSL is licensed under the MIT License. See third-party dependency licenses in THIRD_PARTY_LICENSES.md.