Element 0 is a small scripting language that can be embedded in Zig applications. It is a new Lisp dialect inspired by Scheme with features like first-class functions, macros, and a simple syntax.
This project provides an implementation of Element 0 (the compiler and virtual machine). The implementation is named Elz (pronounced "el-zee") and can be easily integrated into Zig applications as a scripting engine.
- Small language with a growing standard library (see std.elz)
- Easy to integrate into Zig projects as a lightweight scripting engine
- Easy to extend with Zig functions via the use of FFI or directly writing Element 0 code
- A good tradeoff between performance and simplicity
See the ROADMAP.md for the list of implemented and planned features. The language follows R7RS-small;
the vendored conformance suite (make test-conformance) shows the current coverage.
Important
This project is in early development, so bugs and breaking changes are expected. Please use the issue page to report bugs or request features.
You can download the release binaries for Elz from the release page.
-
Clone the repository
git clone https://github.com/Element0Lang/element-0.git cd element-0 -
Build and run the REPL
zig build && ./zig-out/bin/elz-repl -
Run an Element 0 script file
./zig-out/bin/elz-repl examples/elz/e13-hello-world.elz
Run
./zig-out/bin/elz-repl --helpfor the other flags, and type.helpin the REPL for its commands.
You can add Elz to your project as a dependency and use it as a scripting engine.
Run the following command in the root directory of your project to add Elz as a dependency.
zig fetch --save=elz "https://github.com/Element0Lang/element-0/archive/<branch_or_tag>.tar.gz"Replace <branch_or_tag> with the desired branch or release tag, like main (for the development version) or v0.1.0.
This command will download Elz and add it to Zig's global cache and update your project's build.zig.zon file.
Next, modify your build.zig file. This will make the Elz library available to your application as a module.
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "your-app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
// 1. Get the Elz dependency object from the builder.
const elz_dep = b.dependency("elz", .{});
// 2. Create a module for the Elz library.
const elz_module = elz_dep.module("elz");
// 3. Add the module to your executable so you can @import("elz").
exe.root_module.addImport("elz", elz_module);
// 4. Link system libraries required by Elz.
exe.linkSystemLibrary("c");
b.installArtifact(exe);
}Finally, you can @import("elz") and use the interpreter in your Zig application.
The example below shows how to evaluate a simple script. It also shows how to use the FFI to call a Zig function from Elz.
const std = @import("std");
const elz = @import("elz");
// Define a native Zig function you want to call from Elz.
fn zig_multiply(a: f64, b: f64) f64 {
return a * b;
}
pub fn main() !void {
// 1. Initialize the Elz interpreter (the compiler and VM)
var interpreter = try elz.Interpreter.init(.{});
defer interpreter.deinit();
var buffer: [4096]u8 = undefined;
const stdout_file = std.Io.File.stdout();
var stdout_writer = stdout_file.writerStreaming(interpreter.io, &buffer);
const stdout = &stdout_writer.interface;
// --- Example 1: Evaluate a simple string of Elz code ---
std.debug.print("--- Evaluating simple Elz code ---\n", .{});
const source1 = "(* 10 5)";
var fuel1: u64 = 1000;
const result1 = try interpreter.evalString(source1, &fuel1);
try stdout.print("Result of {s} is: ", .{source1});
try elz.write(result1, stdout);
try stdout.print("\n\n", .{});
try stdout.flush();
// --- Example 2: Expose a Zig function to Elz and call it ---
std.debug.print("--- Calling a Zig function from Elz ---\n", .{});
// 2. Register your Zig function with the interpreter.
// It will be available in Elz under the name "zig-mul".
try elz.define_foreign_func(
interpreter.root_env,
"zig-mul",
zig_multiply,
);
// 3. Write and evaluate Elz code that calls your Zig function.
const source2 = "(zig-mul 7 6)";
var fuel2: u64 = 1000;
const result2 = try interpreter.evalString(source2, &fuel2);
try stdout.print("Result of {s} is: ", .{source2});
try elz.write(result2, stdout);
try stdout.print("\n", .{});
try stdout.flush();
}When you build and run this program, the output will be:
--- Evaluating simple Elz code ---
Result of (* 10 5) is: 50
--- Calling a Zig function from Elz ---
Result of (zig-mul 7 6) is: 42.0
The result is 42.0 rather than 42 because zig_multiply returns an f64, which becomes an inexact number.
Interpreter.init takes a SandboxFlags value that selects which built-in capabilities the script can reach.
Every group is enabled by default.
var interpreter = try elz.Interpreter.init(.{
.enable_filesystem = false, // No file ports, `load`, `include`, or module imports
.enable_process = false, // No `exit` and no environment variables
.time_limit_ms = 100, // Give up after 100 milliseconds
});The project documentation is available here. The Zig API reference is available here.
See the std.elz file for the full list of available items (like functions, variables, etc.) in the standard library.
Check out the examples directory for Element 0 code and Zig FFI examples.
Please see CONTRIBUTING.md for details on how to make a contribution.
Element 0 is licensed under the Apache License, Version 2.0 (see LICENSE).
- The logo is made by Conrad Barski, M.D. with a few changes.
- Bestline is used for the REPL's line editing and history features.
- Chibi-Scheme R7RS test suite is used for conformance testing.
- Chilli is used for the CLI.
- BDWGC is used for the garbage collector.