Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,4 @@ docs/api/
latest
history.txt
.history
.claude/
142 changes: 142 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# AGENTS.md

This file provides guidance to coding agents collaborating on this repository.

## Mission

Element 0 is a small, embeddable Lisp dialect inspired by Scheme, implemented in Zig.
The interpreter (Elz) is designed to integrate into Zig applications as a scripting engine.
Priorities, in order:

1. Correctness and R5RS compliance where applicable.
2. Clean, minimal public API for embedding into Zig projects.
3. Maintainable and well-tested code.
4. Cross-platform support (Linux, macOS, and Windows).

## Core Rules

- Use English for code, comments, docs, and tests.
- Prefer small, focused changes over large refactoring.
- Add comments only when they clarify non-obvious behavior.
- Do not add features, error handling, or abstractions beyond what is needed for the current task.

## Repository Layout

- `src/lib.zig`: Main public API export module for embedding Elz as a library.
- `src/main.zig`: REPL entry point (`elz-repl` binary).
- `src/elz/core.zig`: Core value types, Environment, and Module definitions.
- `src/elz/interpreter.zig`: Main `Interpreter` struct.
- `src/elz/eval.zig`: Evaluation engine.
- `src/elz/parser.zig`: S-expression parser.
- `src/elz/env_setup.zig`: Environment initialization and FFI setup.
- `src/elz/ffi.zig`: Foreign function interface for calling Zig functions from Element 0.
- `src/elz/gc.zig`: Garbage collection wrapper (uses Boehm-Demers-Weiser GC).
- `src/elz/errors.zig`: Error types.
- `src/elz/writer.zig`: Value serialization and display.
- `src/elz/api_helpers.zig`: Public API helper functions.
- `src/elz/primitives/`: Built-in functions grouped by category (math, lists, strings, control, predicates, vectors, hashmaps, io, ports, datetime, os, modules, and process).
- `src/stdlib/std.elz`: Standard library written in Element 0 itself.
- `examples/zig/`: FFI examples showing how to call Zig functions from Element 0.
- `examples/elz/`: Element 0 script examples.
- `tests/`: Element 0 language-level tests (`test_stdlib.elz`, `test_advanced.elz`, `test_edge_cases.elz`, `test_regression.elz`, `test_module_lib.elz`).
- `.github/workflows/`: CI workflows (tests, lints, docs, and releases).
- `build.zig` / `build.zig.zon`: Zig build configuration and dependencies.
- `Makefile`: GNU Make wrapper around `zig build`.

## Architecture

### Interpreter Pipeline

Source code flows through: Parser (`parser.zig`) -> Evaluator (`eval.zig`) -> Writer (`writer.zig`).
The `Interpreter` struct in `interpreter.zig` ties these together and manages the root environment.

### Core / Primitives Split

- `src/elz/core.zig` defines the value types and environment model.
- `src/elz/primitives/` contains all built-in functions, each in a category-specific module.
- New built-in functions should be added to the appropriate primitives' module.

### FFI

Zig functions can be registered with the interpreter via `env_setup.define_foreign_func()`.
This is the primary extension mechanism for embedding use cases.

### Garbage Collection

Memory is managed by the Boehm-Demers-Weiser GC (`bdwgc`), wrapped in `gc.zig`. The GC is linked as a C library dependency.

### Dependencies

Managed via Zig's package manager (`build.zig.zon`):

- Chilli: CLI framework for the REPL.
- BDWGC (v8.2.12): Garbage collector.
- Linenoise (v2.0): Line editing for the REPL (POSIX only).
- Minish: Property-based testing framework.

## Zig Conventions

- Zig version: 0.15.2.
- Formatting is enforced by `zig fmt`. Run `make format` before committing.
- Naming: `snake_case` for functions and variables, `PascalCase` for types and structs.
- Element 0 symbols use `kebab-case` (e.g., `zig-mul`, `string-length`).

## Required Validation

Run all test suites for any change:

| Target | Command | What It Runs |
|---------------------|--------------------|--------------------------------------------------------------|
| Zig unit tests | `make test` | Inline `test` blocks in `src/**/*.zig` |
| Property tests | `make test-prop` | Property-based tests in `tests/*_prop_test.zig` (Minish) |
| Integration tests | `make test-integ` | Integration tests in `tests/*_integ_test.zig` |
| Language tests | `make test-elz` | Element 0 test files in `tests/test_*.elz` |
| All tests | `make test-all` | Runs all of the above |
| Lint | `make lint` | Checks Zig formatting with `zig fmt --check` |

For interactive exploration: `make repl`.

## First Contribution Flow

1. Read the relevant source module under `src/elz/`.
2. Implement the smallest possible change.
3. Add or update inline `test` blocks in the changed Zig module. Add Element 0 tests in `tests/` if language behavior changed.
4. Run `make test-all`.
5. Verify interactively with `make repl` if needed.

Good first tasks:

- Add a new built-in function in the appropriate `src/elz/primitives/` module.
- Add a new standard library function in `src/stdlib/std.elz`.
- Fix an edge case identified in `tests/test_edge_cases.elz`.
- Add a new FFI example in `examples/zig/`.

## Testing Expectations

- Unit and regression tests live as inline `test` blocks in the module they cover (`src/elz/*.zig` and `src/elz/primitives/*.zig`).
- Property-based tests live in `tests/*_prop_test.zig` and use the Minish framework. They test invariants like commutativity, roundtrip properties, and crash resistance.
- Integration tests live in `tests/*_integ_test.zig` and test the public embedding API (init, evalString, FFI, error propagation, sandboxing).
- Language-level tests live in `tests/test_*.elz` and are run by the interpreter itself via `make test-elz`.
- No language-facing change is complete without an Element 0 test.

## Change Design Checklist

Before coding:

1. Identify which module(s) the change touches (core, primitives, parser, eval, etc.).
2. Consider whether the change requires updates to the standard library (`std.elz`).
3. Check cross-platform implications if the change touches OS or I/O primitives.

Before submitting:

1. `make test && make test-elz` passes.
2. `make lint` passes.
3. Docs updated if the public API surface changed.

## Commit and PR Hygiene

- Keep commits scoped to one logical change.
- PR descriptions should include:
1. Behavioral change summary.
2. Tests added or updated.
3. Interactive verification done (yes/no).
20 changes: 16 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ SHELL := /usr/bin/env bash
# Targets
################################################################################

.PHONY: all help build rebuild run run-elz test test-elz release clean lint format docs serve-docs install-deps setup-hooks test-hooks
.PHONY: all help build rebuild run run-elz test test-elz test-prop test-integ test-all release clean lint format docs serve-docs install-deps setup-hooks test-hooks
.DEFAULT_GOAL := help

help: ## Show the help messages for all targets
Expand All @@ -43,13 +43,13 @@ init: ## Initialize a new Zig project
@echo "Initializing a new Zig project..."
@$(ZIG) init

build: ## Build project (e.g. 'make build BUILD_TYPE=ReleaseSafe')
build: ## Build project (like 'make build BUILD_TYPE=ReleaseSafe')
@echo "Building project in $(BUILD_TYPE) mode with $(JOBS) concurrent jobs..."
@$(ZIG) build $(BUILD_OPTS) -j$(JOBS)

rebuild: clean build ## clean and build

run: ## Run a Zig example (e.g. 'make run EXAMPLE=e1_ffi_1')
run: ## Run a Zig example (like 'make run EXAMPLE=e1_ffi_1')
@if [ "$(EXAMPLE)" = "all" ]; then \
echo "--> Running all Zig examples..."; \
fail=0; \
Expand All @@ -64,7 +64,7 @@ run: ## Run a Zig example (e.g. 'make run EXAMPLE=e1_ffi_1')
$(ZIG) build run-$(EXAMPLE) $(BUILD_OPTS); \
fi

run-elz: build ## Run a Lisp example (e.g. 'make run-elz ELZ_EXAMPLE=e1-cons-car-cdr')
run-elz: build ## Run a Lisp example (like 'make run-elz ELZ_EXAMPLE=e1-cons-car-cdr')
@if [ "$(ELZ_EXAMPLE)" = "all" ]; then \
echo "--> Running all Lisp examples..."; \
fail=0; \
Expand All @@ -91,6 +91,18 @@ test-elz: ## Run Element 0 standard library tests
@echo "Running Element 0 standard library tests..."
@$(ZIG) build test-elz $(BUILD_OPTS) -j$(JOBS) $(TEST_FLAGS)

test-prop: ## Run property-based tests
@echo "Running property-based tests..."
@$(ZIG) build test-prop $(BUILD_OPTS) -j$(JOBS) $(TEST_FLAGS)

test-integ: ## Run integration tests
@echo "Running integration tests..."
@$(ZIG) build test-integ $(BUILD_OPTS) -j$(JOBS) $(TEST_FLAGS)

test-all: ## Run all tests (unit, property, integration, and elz)
@echo "Running all tests..."
@$(ZIG) build test-all $(BUILD_OPTS) -j$(JOBS) $(TEST_FLAGS)

release: ## Build in Release mode
@echo "Building the project in Release mode..."
@$(MAKE) build BUILD_TYPE=$(RELEASE_MODE)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ Result of (zig-mul 7 6) is: 42

### Documentation

You can find the full API documentation for the latest release of Elz [here](https://Element0Lang.github.io/element-0/).
You can find the full API documentation for the latest release of Elz [here](https://element0lang.github.io/element-0/).

Alternatively, you can use the `make docs` command to generate the API documentation for the current version of
Elz from the source code.
Expand Down
19 changes: 10 additions & 9 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ It outlines the features to be implemented and their current status.
* [x] **Math Library**: More common mathematical functions (like trigonometric and logarithmic functions).
* [x] **List Utilities**: `filter`, `fold-left`, `fold-right`, and other common list processing functions.
* [x] **String Utilities**: `string-append`, `string-ref`, `substring`, `string-split`, `number->string`, `string->number`, `make-string`, `string=?`, `string<?`, `string>?`, `string<=?`, `string>=?`, `gensym` implemented.
* [ ] **Regular Expressions**: A library for advanced text pattern matching.
* [x] **Regular Expressions**: `regex-match?`, `regex-search`, `regex-replace`, `regex-split` implemented with NFA-based engine supporting literals, `.`, `*`, `+`, `?`, character classes, and anchors.
* [x] **OS and Filesystem**: `getenv`, `file-exists?`, `delete-file`, `current-directory`, `directory-list`, `rename-file` implemented.
* [ ] **Advanced I/O**: A `format` procedure and a more comprehensive port system.
* [x] **Advanced I/O**: `format` procedure with `~a`, `~s`, `~%`, `~~` directives, and `value->string` implemented.
* [x] **Date and Time**: `current-time`, `current-time-ms`, `time->components`, `sleep-ms` implemented.

### 4. Advanced Language Features (Post-R5RS)
Expand All @@ -114,16 +114,17 @@ It outlines the features to be implemented and their current status.
* [x] **Module System**: A system for organizing code into reusable and encapsulated modules.
* [x] `define-macro` (simple procedural macros)
* [ ] `syntax-rules` (hygienic macros) or similar system for compile-time metaprogramming.
* [ ] `call-with-current-continuation` (`call/cc`): Support for first-class continuations.
* [x] `call-with-escape-continuation` (`call/ec`): Escape-only continuations for early returns. Full `call/cc` deferred pending CPS rewrite.

### 5. Better Host Integration and Embeddability

* [ ] **Advanced FFI**
* **Advanced FFI**
* [ ] Support for passing complex Zig structs.
* [ ] Ability to pass Elz closures to Zig as callbacks.
* [ ] Automatic type conversions for more data types.
* [ ] **Sandboxing and Security**
* [x] Automatic type conversions for `bool`, `[]const u8`, and `?T` (optional) types.
* **Sandboxing and Security**
* [x] A sandboxed mode to restrict access to I/O and other sensitive operations.
* [ ] Host-level controls for memory and execution time limits.
* [ ] **Serialization**
* [ ] Built-in procedures to serialize and deserialize Elz objects (for example, to JSON or S-expressions).
* [x] Host-level controls for execution time limits (`time_limit_ms` in `SandboxFlags`).
* **Serialization**
* [x] `json-serialize` and `json-deserialize` for JSON round-tripping.
* [x] `value->string` for S-expression serialization.
87 changes: 81 additions & 6 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ pub fn build(b: *std.Build) void {
});
docs_step.dependOn(&gen_docs_cmd.step);

// --- Test Setup ---
// --- Unit Test Setup ---
const test_module = b.createModule(.{
.root_source_file = lib_source,
.target = target,
Expand All @@ -162,6 +162,57 @@ pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step);

// --- Property-Based and Integration Test Setup ---
const test_prop_step = b.step("test-prop", "Run property-based tests");
const test_integ_step = b.step("test-integ", "Run integration tests");
const minish_dep = b.dependency("minish", .{});

{
const tests_path = "tests";
var tests_dir = fs.cwd().openDir(tests_path, .{ .iterate = true }) catch |err| {
if (err == error.FileNotFound) {
@panic("Can't open 'tests' directory");
}
@panic("Can't open 'tests' directory");
};
defer tests_dir.close();

var test_iter = tests_dir.iterate();
while (test_iter.next() catch @panic("Failed to iterate tests")) |entry| {
if (!std.mem.endsWith(u8, entry.name, ".zig")) continue;

const is_prop_test = std.mem.endsWith(u8, entry.name, "_prop_test.zig");
const is_integ_test = std.mem.endsWith(u8, entry.name, "_integ_test.zig");

if (!is_prop_test and !is_integ_test) continue;

const test_path = b.fmt("{s}/{s}", .{ tests_path, entry.name });

const t_module = b.createModule(.{
.root_source_file = b.path(test_path),
.target = target,
.optimize = optimize,
});
t_module.addImport("elz", lib_module);

if (is_prop_test) {
t_module.addImport("minish", minish_dep.module("minish"));
}

const t = b.addTest(.{ .root_module = t_module });
const bdwgc_dep_t = b.dependency("bdwgc", .{});
t.addIncludePath(bdwgc_dep_t.path("include"));
t.linkLibrary(gc);

const run_t = b.addRunArtifact(t);
if (is_prop_test) {
test_prop_step.dependOn(&run_t.step);
} else {
test_integ_step.dependOn(&run_t.step);
}
}
}

// --- Example Setup ---
const examples_path = "examples/zig";
var examples_dir = fs.cwd().openDir(examples_path, .{ .iterate = true }) catch |err| {
Expand Down Expand Up @@ -198,10 +249,34 @@ pub fn build(b: *std.Build) void {
run_step.dependOn(&run_cmd.step);
}

// --- Run Element 0 Standard Library Tests ---
// --- Run Element 0 Language Tests ---
const test_elz_step = b.step("test-elz", "Run the Element 0 language tests");
const run_elz_tests_cmd = b.addRunArtifact(repl_exe);
run_elz_tests_cmd.addArg("--file");
run_elz_tests_cmd.addArg("tests/test_stdlib.elz");
test_elz_step.dependOn(&run_elz_tests_cmd.step);
{
const tests_path = "tests";
var tests_dir = fs.cwd().openDir(tests_path, .{ .iterate = true }) catch |err| {
if (err == error.FileNotFound) {
@panic("Can't open 'tests' directory");
}
@panic("Can't open 'tests' directory");
};
defer tests_dir.close();

var test_iter = tests_dir.iterate();
while (test_iter.next() catch @panic("Failed to iterate tests")) |entry| {
if (!std.mem.startsWith(u8, entry.name, "test_")) continue;
if (!std.mem.endsWith(u8, entry.name, ".elz")) continue;

const run_elz_test_cmd = b.addRunArtifact(repl_exe);
run_elz_test_cmd.addArg("--file");
run_elz_test_cmd.addArg(b.fmt("{s}/{s}", .{ tests_path, entry.name }));
test_elz_step.dependOn(&run_elz_test_cmd.step);
}
}

// --- Run All Tests ---
const test_all_step = b.step("test-all", "Run all tests");
test_all_step.dependOn(&run_lib_unit_tests.step);
test_all_step.dependOn(test_prop_step);
test_all_step.dependOn(test_integ_step);
test_all_step.dependOn(test_elz_step);
}
4 changes: 4 additions & 0 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
.url = "https://github.com/antirez/linenoise/archive/refs/tags/2.0.tar.gz",
.hash = "N-V-__8AAJ4HAgCX79UDBfNwhqAqUVoGC44ib6UYa18q6oa_",
},
.minish = .{
.url = "https://github.com/CogitatorTech/minish/archive/refs/tags/v0.1.0.tar.gz",
.hash = "minish-0.1.0-SQtSTWHkAQACfz3xGuWHU8zbx320vK_47r2yto3Pq0Rf",
},
},
.paths = .{ "build.zig", "build.zig.zon", "src", "LICENSE", "README.md" },
}
Loading
Loading