Rift is a statically typed language written in C and targeted at the ZX Next retro system. Its designed so that you develop on your pc/laptop and deploy to an emulator or real hardware. A Rift program can also be directly run locally.
Rift provides:
- Scalar types:
int,byte,word,dword,float,boolean,char, andstring - Dynamic and fixed-size arrays
- Records, enums, unions, and modules
- Functions, methods, loops, and
match - C and Z80 assembly embed blocks
- A runtime library for strings, arrays, input, graphics, sound, and ZX Spectrum Next access
- Automatic memory allocation, reclaimed automatically when its final reference dies
For example:
sub main() {
int[] numbers;
for i := 1 to 3 {
append(numbers, i);
}
print(toString(length(numbers)));
}
Fixed arrays grow their logical length through sequential assignment. Writing
at length(array) initializes the next slot, while writing beyond that index
is rejected; skipped capacity is never exposed as initialized elements.
@embed c
int square(int x) { return x * x; }
@end c
sub main() {
print(toString(square(7)));
}
enum Direction { North, South, East, West }
record Point {
int x,
int y
}
sub Point.move(int dx, int dy) returns Point {
return { x := this.x + dx, y := this.y + dy };
}
sub main() {
Point start := { x := 2, y := 3 };
Point end := start.move(1, -1);
}
module Scoreboard;
int score;
sub Scoreboard.add(int points) {
this.score := this.score + points;
}
Use static sub Type.method(...) for behaviour owned by a type rather than an
instance. The declaration receives no implicit this; callers use the type
name directly.
module Sprites;
static sub Sprites.hideall() {
// component-wide work
}
sub main() {
Sprites.hideall();
}
Standard interfaces are preloaded by the compiler, so applications do not
repeat their declarations. Sprite patterns are compiler-only bindings, while
each Sprite value is a one-byte hardware-slot handle:
SpritePattern playerPattern :=
SpritePattern.load("assets/player.spr");
sub main() {
Sprite player := Sprite(1);
player.position(10, 20);
player.frame(playerPattern, 0);
player.show();
player.hide();
Sprite.hideall();
}
SpritePattern.load(path) defaults to 4bpp; pass literal 8 as its second
argument for 8bpp input. This intentionally replaces both the earlier
asset sprite4 declaration and the combined five-argument Sprite.show call.
See the sprite and asset contract for raw formats,
hardware ownership, exact memory costs, and test evidence.
Runtime components are selected from resolved calls through
src/lib/components.manifest; the same dependency closure drives host and ZX
Next builds.
The ZX Next console is length-aware and does not link Z88DK stdio for ordinary
Rift I/O. print and println accept strings, characters, booleans, and all
numeric scalar types; positioned forms use character cells or an exact ULA
top-scanline address:
paper(0);
border(0);
cls();
print(to_byte(10), to_byte(5), "score=");
println(42);
putchar_at(to_byte(31), to_byte(23), '!');
putchar_addr(to_word(16384), 'A');
string line := input(); // echoed, blocking, maximum 255 characters
float value := input(); // inferred checked conversion; invalid input exits
union Token {
// a Token instance is allowed to be *one* of these:
int Number,
string Name,
char Operator,
End
}
Token token := Number(42);
match token {
Number: print("a number");
Name: print("a name");
Operator: print("an operator");
End: print("the end");
}
I'll be reducing the prerequisite requirements in the future (hopefully to nothing!), but for now to compile Rift code you need:
makegcc- Z88DK with
zccavailable on yourPATH
ZX Spectrum Next .nex programs are the default build target and use Z88DK.
Native programs use GCC when selected with --target=gcc.
SpritePattern build inputs are generated directly by the compiler; the build
has no Perl or external asset-packing dependency.
makeThis creates riftc, the Rift-to-C compiler, and rift, the native build
driver.
Create hello.rift:
sub main() {
print("Hello, Rift!\n");
}
.rift is the canonical source extension. The shorter .rft extension is
also accepted by the compiler, driver, and test tooling.
Build it for the default ZX Spectrum Next target:
./rift hello.riftThe rift driver translates the source to C, compiles it with Z88DK, and
creates hello.nex.
To build and run it as a native host program instead:
./rift run --target=gcc hello.riftThis creates and runs hello.exe.
To choose an output name:
./rift hello.rift helloTo retain build intermediates for inspection:
./rift hello.rift --debugNormal builds keep generated C, component sidecars, maps, and target-toolchain
files inside a private /tmp/rift-build-* workspace and remove it after the
final artifact is published. --debug retains that workspace and prints its
location.
Build a native executable with GCC:
./rift hello.rift --target=gccBuild a .nex program with Z88DK:
./rift hello.rift
# or explicitly:
./rift hello.rift --target=zxnRift uses Z88DK’s SDCC backend for this target and publishes the resulting
NEX-format image with its standard .nex extension.
Managed memory is automatic by default. Rift starts its arena after the linked program and grows it on demand toward the protected hardware-stack boundary; unused capacity is not stored in the NEX resident image. The build reports the exact arena bounds and capacity.
Most programs need no memory option. Purpose-level bounds are available when a program has an external memory contract:
./rift --memory-max=16384 --memory-min=8192 hello.rift--memory-max=BYTES limits the total automatic arena, while
--memory-min=BYTES rejects a build whose linked program leaves less than the
required headroom. Managed programs that also coordinate with raw-address or
MMU code can use --memory-reserve=BYTES to leave an additional high-memory
region outside the arena. Explicit memory bounds require a selected runtime
closure with managed allocation. Inputs are decimal byte counts; Rift aligns
the effective target arena bounds and cap internally, without exposing an
allocator split. The compiler automatically omits bump support when the
program has no bump-lifetime allocation path. Freed managed blocks coalesce
immediately or enter small bounded caches; allocation pressure drains those
caches and retries once before reporting a named out-of-memory error.
Run the host test suite:
./run_tests.shThe test harness selects --target=gcc explicitly so it can execute each
compiled program locally.
Run one test while working on a feature:
./run_tests.sh test/array_test.riftTests are Rift programs in test/. Most include test/Assert.rift and print PASS: or FAIL: markers for the test runner.
src/ Compiler: lexer, parser, type checker, and C generator
src/lib/ Runtime library and target support
test/ Rift regression tests
docs/ Language and implementation notes
wikiroot/ Maintainer knowledge wiki
rift Build driver for Rift programs
riftc Generated compiler binary
make
./run_tests.shUse make clean to remove build output and riftc.
The host target has the broadest test coverage. The ZX Spectrum Next target needs Z88DK; compile it separately when a change touches target-specific code. enum_test.rift is currently known to fail on that target because of an SDCC enum syntax incompatibility.
Rift is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-only).
