so... jai :D

This commit is contained in:
agra
2026-02-04 01:34:30 +02:00
commit 55fc5790e4
60 changed files with 15876 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.zig-cache
zig-out
.DS_Store
.vscode/

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 agra
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

90
build.zig Normal file
View File

@@ -0,0 +1,90 @@
const std = @import("std");
const math = @import("math");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const static_llvm = b.option(bool, "static-llvm", "Statically link LLVM (self-contained binary, no LLVM needed at runtime)") orelse false;
const llvm_prefix = b.option([]const u8, "llvm-prefix", "Path to LLVM installation") orelse "/opt/homebrew/opt/llvm@18";
const include_dir = b.fmt("{s}/include", .{llvm_prefix});
const lib_dir = b.fmt("{s}/lib", .{llvm_prefix});
const llvm_config = b.fmt("{s}/bin/llvm-config", .{llvm_prefix});
const mod = b.addModule("sx", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
mod.addSystemIncludePath(.{ .cwd_relative = include_dir });
mod.addLibraryPath(.{ .cwd_relative = lib_dir });
mod.link_libc = true;
mod.addCSourceFile(.{
.file = b.path("llvm_shim.c"),
.flags = &.{b.fmt("-I{s}", .{include_dir})},
});
if (static_llvm) {
// Query llvm-config for the static libraries needed
const libs_raw = std.mem.trim(u8, b.run(&.{ llvm_config, "--libs", "--link-static" }), " \t\n\r");
var libs_it = std.mem.tokenizeAny(u8, libs_raw, " \t\n\r");
while (libs_it.next()) |flag| {
if (flag.len > 2 and std.mem.startsWith(u8, flag, "-l")) {
mod.linkSystemLibrary(flag[2..], .{ .preferred_link_mode = .static });
}
}
// System libraries LLVM depends on (zlib, zstd, curses, etc.)
const syslibs_raw = std.mem.trim(u8, b.run(&.{ llvm_config, "--system-libs", "--link-static" }), " \t\n\r");
var syslibs_it = std.mem.tokenizeAny(u8, syslibs_raw, " \t\n\r");
while (syslibs_it.next()) |flag| {
if (flag.len > 2 and std.mem.startsWith(u8, flag, "-l")) {
mod.linkSystemLibrary(flag[2..], .{});
}
}
// LLVM is C++ — link the C++ standard library
mod.link_libcpp = true;
} else {
mod.linkSystemLibrary("LLVM-18", .{});
}
const exe = b.addExecutable(.{
.name = "sx",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "sx", .module = mod },
},
}),
});
b.installArtifact(exe);
const run_step = b.step("run", "Run the app");
const run_cmd = b.addRunArtifact(exe);
run_step.dependOn(&run_cmd.step);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const mod_tests = b.addTest(.{
.root_module = mod,
});
const run_mod_tests = b.addRunArtifact(mod_tests);
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
test_step.dependOn(&run_exe_tests.step);
}

81
build.zig.zon Normal file
View File

@@ -0,0 +1,81 @@
.{
// This is the default name used by packages depending on this one. For
// example, when a user runs `zig fetch --save <url>`, this field is used
// as the key in the `dependencies` table. Although the user can choose a
// different name, most users will stick with this provided value.
//
// It is redundant to include "zig" in this name because it is already
// within the Zig package namespace.
.name = .so,
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.0.0",
// Together with name, this represents a globally unique package
// identifier. This field is generated by the Zig toolchain when the
// package is first created, and then *never changes*. This allows
// unambiguous detection of one package being an updated version of
// another.
//
// When forking a Zig project, this id should be regenerated (delete the
// field and run `zig build`) if the upstream project is still maintained.
// Otherwise, the fork is *hostile*, attempting to take control over the
// original project's identity. Thus it is recommended to leave the comment
// on the following line intact, so that it shows up in code reviews that
// modify the field.
.fingerprint = 0x98c64403d9494683, // Changing this has security and trust implications.
// Tracks the earliest Zig version that the package considers to be a
// supported use case.
.minimum_zig_version = "0.16.0-dev.2290+200fb7c2a",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
//.example = .{
// // When updating this field to a new URL, be sure to delete the corresponding
// // `hash`, otherwise you are communicating that you expect to find the old hash at
// // the new URL. If the contents of a URL change this will result in a hash mismatch
// // which will prevent zig from using it.
// .url = "https://example.com/foo.tar.gz",
//
// // This is computed from the file contents of the directory of files that is
// // obtained after fetching `url` and applying the inclusion rules given by
// // `paths`.
// //
// // This field is the source of truth; packages do not come from a `url`; they
// // come from a `hash`. `url` is just one of many possible mirrors for how to
// // obtain a package matching this `hash`.
// //
// // Uses the [multihash](https://multiformats.io/multihash/) format.
// .hash = "...",
//
// // When this is provided, the package is found in a directory relative to the
// // build root. In this case the package's hash is irrelevant and therefore not
// // computed. This field and `url` are mutually exclusive.
// .path = "foo",
//
// // When this is set to `true`, a package is declared to be lazily
// // fetched. This makes the dependency only get fetched if it is
// // actually used.
// .lazy = false,
//},
},
// Specifies the set of files and directories that are included in this package.
// Only files and directories listed here are included in the `hash` that
// is computed for this package. Only files listed here will remain on disk
// when using the zig package manager. As a rule of thumb, one should list
// files required for compilation plus any license(s).
// Paths are relative to the build root. Use the empty string (`""`) to refer to
// the build root itself.
// A directory listed here means that all files within, recursively, are included.
.paths = .{
"build.zig",
"build.zig.zon",
"src",
// For example...
//"LICENSE",
//"README.md",
},
}

2
editors/vscode/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
out/

View File

@@ -0,0 +1,3 @@
src/
tsconfig.json
.gitignore

View File

@@ -0,0 +1,22 @@
{
"comments": {
"lineComment": "//"
},
"brackets": [
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "(", "close": ")" },
{ "open": "\"", "close": "\"", "notIn": ["string"] }
],
"surroundingPairs": [
{ "open": "{", "close": "}" },
{ "open": "(", "close": ")" },
{ "open": "\"", "close": "\"" }
],
"indentationRules": {
"increaseIndentPattern": "\\{\\s*$",
"decreaseIndentPattern": "^\\s*\\}"
}
}

121
editors/vscode/package-lock.json generated Normal file
View File

@@ -0,0 +1,121 @@
{
"name": "sx-lang",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sx-lang",
"version": "0.0.1",
"dependencies": {
"vscode-languageclient": "^9.0.1"
},
"devDependencies": {
"@types/vscode": "^1.75.0",
"typescript": "^5.0.0"
},
"engines": {
"vscode": "^1.75.0"
}
},
"node_modules/@types/vscode": {
"version": "1.108.1",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.108.1.tgz",
"integrity": "sha512-DerV0BbSzt87TbrqmZ7lRDIYaMiqvP8tmJTzW2p49ZBVtGUnGAu2RGQd1Wv4XMzEVUpaHbsemVM5nfuQJj7H6w==",
"dev": true,
"license": "MIT"
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/vscode-jsonrpc": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/vscode-languageclient": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz",
"integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==",
"license": "MIT",
"dependencies": {
"minimatch": "^5.1.0",
"semver": "^7.3.7",
"vscode-languageserver-protocol": "3.17.5"
},
"engines": {
"vscode": "^1.82.0"
}
},
"node_modules/vscode-languageserver-protocol": {
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
"license": "MIT",
"dependencies": {
"vscode-jsonrpc": "8.2.0",
"vscode-languageserver-types": "3.17.5"
}
},
"node_modules/vscode-languageserver-types": {
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
"license": "MIT"
}
}
}

View File

@@ -0,0 +1,68 @@
{
"name": "sx-lang",
"displayName": "sx",
"description": "Language support for the sx programming language",
"version": "0.0.1",
"publisher": "swipelab",
"engines": {
"vscode": "^1.75.0"
},
"categories": [
"Programming Languages"
],
"main": "./out/extension.js",
"contributes": {
"languages": [
{
"id": "sx",
"aliases": [
"sx"
],
"extensions": [
".sx"
],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "sx",
"scopeName": "source.sx",
"path": "./syntaxes/sx.tmLanguage.json"
}
],
"configuration": {
"title": "sx",
"properties": {
"sx.lspPath": {
"type": "string",
"default": "sx-lsp",
"description": "Path to the sx-lsp binary"
}
}
},
"configurationDefaults": {
"editor.tokenColorCustomizations": {
"textMateRules": [
{
"scope": "punctuation.definition.template-expression",
"settings": {
"foreground": "#E5C07B"
}
}
]
}
}
},
"scripts": {
"build": "tsc -p .",
"watch": "tsc -watch -p ."
},
"dependencies": {
"vscode-languageclient": "^9.0.1"
},
"devDependencies": {
"@types/vscode": "^1.75.0",
"typescript": "^5.0.0"
}
}

View File

@@ -0,0 +1,41 @@
import {
workspace,
ExtensionContext,
} from "vscode";
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
} from "vscode-languageclient/node";
let client: LanguageClient;
export function activate(context: ExtensionContext) {
const config = workspace.getConfiguration("sx");
const lspPath = config.get<string>("lspPath", "sx-lsp");
const serverOptions: ServerOptions = {
command: lspPath,
args: ["lsp"],
};
const clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: "file", language: "sx" }],
};
client = new LanguageClient(
"sx-lsp",
"sx Language Server",
serverOptions,
clientOptions
);
client.start();
}
export function deactivate(): Thenable<void> | undefined {
if (!client) {
return undefined;
}
return client.stop();
}

Binary file not shown.

View File

@@ -0,0 +1,208 @@
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "sx",
"scopeName": "source.sx",
"patterns": [
{ "include": "#comments" },
{ "include": "#strings" },
{ "include": "#directives" },
{ "include": "#keywords" },
{ "include": "#types" },
{ "include": "#type-declaration" },
{ "include": "#type-annotation" },
{ "include": "#constants" },
{ "include": "#numbers" },
{ "include": "#operators" },
{ "include": "#function-declaration" },
{ "include": "#enum-literal" },
{ "include": "#identifiers" }
],
"repository": {
"comments": {
"patterns": [
{
"name": "comment.line.double-slash.sx",
"match": "//.*$"
}
]
},
"strings": {
"patterns": [
{
"begin": "\"",
"end": "\"",
"beginCaptures": {
"0": { "name": "punctuation.definition.string.begin.sx" }
},
"endCaptures": {
"0": { "name": "punctuation.definition.string.end.sx" }
},
"patterns": [
{
"name": "constant.character.escape.sx",
"match": "\\\\[ntr\"\\\\{}]"
},
{
"begin": "\\{",
"end": "\\}",
"beginCaptures": {
"0": { "name": "punctuation.definition.template-expression.begin.sx" }
},
"endCaptures": {
"0": { "name": "punctuation.definition.template-expression.end.sx" }
},
"patterns": [
{ "include": "$self" }
]
},
{
"name": "string.quoted.double.sx",
"match": "[^\"\\\\{}]+"
}
]
}
]
},
"directives": {
"patterns": [
{
"name": "keyword.other.directive.sx",
"match": "#run"
}
]
},
"keywords": {
"patterns": [
{
"name": "keyword.control.sx",
"match": "\\b(if|else|then|return|case|break|defer)\\b"
},
{
"name": "keyword.other.sx",
"match": "\\b(enum|struct)\\b"
},
{
"name": "keyword.operator.cast.sx",
"match": "\\bxx\\b"
}
]
},
"types": {
"patterns": [
{
"name": "storage.type.sx",
"match": "\\b(s[0-9]+|u[0-9]+|f32|f64|bool|string)\\b"
}
]
},
"type-declaration": {
"patterns": [
{
"match": "([A-Z][a-zA-Z0-9_]*)\\s*(::)\\s*(?=struct\\b|enum\\b)",
"captures": {
"1": { "name": "entity.name.type.sx" },
"2": { "name": "keyword.operator.declaration.sx" }
}
}
]
},
"type-annotation": {
"patterns": [
{
"match": "(?<![:]):\\s*(?![=:])([A-Z][a-zA-Z0-9_]*)\\b",
"captures": {
"1": { "name": "entity.name.type.sx" }
}
}
]
},
"constants": {
"patterns": [
{
"name": "constant.language.sx",
"match": "\\b(true|false)\\b"
},
{
"name": "constant.language.undefined.sx",
"match": "---"
}
]
},
"numbers": {
"patterns": [
{
"name": "constant.numeric.float.sx",
"match": "\\b[0-9]+\\.[0-9]+\\b"
},
{
"name": "constant.numeric.integer.sx",
"match": "\\b[0-9]+\\b"
}
]
},
"operators": {
"patterns": [
{
"name": "keyword.operator.declaration.sx",
"match": "::"
},
{
"name": "keyword.operator.walrus.sx",
"match": ":="
},
{
"name": "keyword.operator.arrow.sx",
"match": "->|=>"
},
{
"name": "keyword.operator.comparison.sx",
"match": "==|!=|<=|>="
},
{
"name": "keyword.operator.assignment.sx",
"match": "[+\\-*/]="
},
{
"name": "keyword.operator.sx",
"match": "[+\\-*/=<>!]"
}
]
},
"function-declaration": {
"patterns": [
{
"match": "([a-zA-Z_][a-zA-Z0-9_]*)\\s*(::)\\s*(?=\\(|\\{)",
"captures": {
"1": { "name": "entity.name.function.sx" },
"2": { "name": "keyword.operator.declaration.sx" }
}
}
]
},
"enum-literal": {
"patterns": [
{
"match": "\\.([a-zA-Z_][a-zA-Z0-9_]*)",
"captures": {
"1": { "name": "variable.other.enummember.sx" }
}
}
]
},
"identifiers": {
"patterns": [
{
"name": "variable.other.generic-type.sx",
"match": "\\$([a-zA-Z_][a-zA-Z0-9_]*)",
"captures": {
"1": { "name": "entity.name.type.parameter.sx" }
}
},
{
"match": "\\b(io)\\b",
"name": "support.module.sx"
}
]
}
}
}

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"outDir": "out",
"rootDir": "src",
"lib": ["ES2020"],
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"],
"exclude": ["node_modules"]
}

5
examples/01-basic.sx Normal file
View File

@@ -0,0 +1,5 @@
#import "modules/std.sx";
main :: () -> s32 {
if false then 40 else 42;
}

4
examples/02-stdout.sx Normal file
View File

@@ -0,0 +1,4 @@
#import "modules/std.sx";
main :: () {
print("Hello\n");
}

22
examples/03-structs.sx Normal file
View File

@@ -0,0 +1,22 @@
#import "modules/std.sx";
Vec4 :: struct {
x, y, z, w: f32;
}
main :: () {
v1 : Vec4 = .{ 1, 2, 3, 0};
v2 := Vec4.{ 4, 1, 1, 3};
v3 := Vec4.{ w=0, x=2, y=3, z=4};
z := 5.0; // z is f32
w := 6.0; // w is f32
v4 := Vec4.{ y=3, x=9, w, z};
v4.y = 0;
print("v1: {}\nv2: {}\nv3: {}\nv4: {}\n", v1, v2, v3, v4);
}
// ** stdout **
//v1: Vec4{x:1.0, y:2.0, z:3.0, w:0.0}
//v2: Vec4{x:4.0, y:1.0, z:1.0, w:3.0}
//v3: Vec4{x:2.0, y:3.0, z:4.0, w:0.0}
//v4: Vec4{x:9.0, y:3.0, z:5.0, w:6.0}

20
examples/04-shadow.sx Normal file
View File

@@ -0,0 +1,20 @@
#import "modules/std.sx";
main :: () -> s32 {
x := 42;
{
print("scope opened\n");
defer print("scope closed\n");
// define a inner variable x shadowing the one define in the outer scope(s)
x:= 6;
print("scoped x: {}\n", x); //expect 6
}
print("main x: {}\n", x); //expect 42
}
// ** stdout **
// scope opened
// scoped x: 6
// scope closed
// main x: 42
//

20
examples/05-run.sx Normal file
View File

@@ -0,0 +1,20 @@
#import "modules/std.sx";
// this will bake x to be 7 as a global constant
x :: #run compute(5);
compute :: (v: s32) -> s32 => v + 2;
main :: () {
//test
y :: #run compute(7);
c :: 2;
print("hello {}\n", x + y * c);
}
#run main();
// ** stdout after build **
// hello 25
// ** stdout after run **
// hello 25

17
examples/06-generic.sx Normal file
View File

@@ -0,0 +1,17 @@
#import "modules/std.sx";
sum :: (a:$T, b:T) -> T {
return a + b;
}
main :: () {
x:=sum(2,3);
print("sum: {}\n", x);
print("sum: {}\n", sum(40,2));
print("sum: {}\n", sum(40,2.5));
}
// ** stdout **
// sum: 42
// sum: 42.500000
//

11
examples/07-defer.sx Normal file
View File

@@ -0,0 +1,11 @@
#import "modules/std.sx";
main :: () -> s32 {
defer print("still here\n");
return 42;
}
// ** exit code **
// 42
// ** stdout **
// still here
//

43
examples/08-types.sx Normal file
View File

@@ -0,0 +1,43 @@
#import "modules/std.sx";
SPECIAL_VALUE :u8: 42;
resolve :: (x: u8) -> s32 {
return 12 + x;
}
Foo :: struct {
a : u2; // this will have 0 as default
b : u8 = SPECIAL_VALUE;
c : u8 = ---; // default for c is undefined
d : u8 = #run xx resolve(5); // converts s32 to u8
}
main :: () {
a : Foo; // default value of 0
print("a 0 : {}\n", a);
a.a = 1;
// a.c is still undefined at this point
a.c = 8;
print("a 1 : {}\n", a);
large: f64 = 5989.5;
b : Foo = ---; // undefined
b.a = 1;
b.c = xx large; // converts f64 to u8
// expect stdout : "b: Foo{a:1, b: 42, c: 7, d: 12}"
print("b: {}", b);
print("\n");
f := Pack.{1,0,3,5,9,100,3.5};
print("{}\n", f);
}
Pack :: struct {
a: u1;
b: u2;
c: u8;
d: u32;
f: u64;
v: s32;
x: f32;
}

16
examples/09-import.sx Normal file
View File

@@ -0,0 +1,16 @@
std :: #import "modules/std.sx";
//flat
#import "modules/math.sx";
main :: () -> s32 {
{
defer std.print("after hello");
//expect stdout : hello there
std.print("hello there");
}
v:= std.Vector(3,f32).[1,2,3];
std.print("\n{}\n", v);
}

View File

@@ -0,0 +1,91 @@
#import "modules/std.sx";
Vec :: struct($N: u32, $T:Type) {
// <N x T> (LLVM Vector)
// Vector is a Builtin Type
data: Vector(N,T);
}
Complex :: ($T:Type) -> Type {
return struct {
value: T;
//..inject
count: u32;
};
}
Vec3 :: Vec(3, f32);
vec3 :: (x:f32, y:f32, z:f32) -> Vector(3,f32) {
.[x, y, z];
}
Foo :: Complex(u32);
main :: () {
v1 := Vec3.{data = .[1,3,2]};
print("v1: {}\n", v1);
//stdout: Vec(3,f32){data: [1.0, 3.0, 2.0]}
//
v2 := vec3(1,3,2);
print("v2: {}\n", v2);
//stdout: [1.0, 3.0, 2.0]
//
// [N x T] (LLVM Array)
buffer : [5]f32 = .[0, 2, 3.5, 4, 0];
print("buff: {}\n", buffer);
//stdout: [0.0, 2.0, 3.5, 4.0, 0.0]
//
comp : Foo = .{value = 42, count = 1};
print("comp: {}\n", comp);
//stdout: Foo{value: 42, count: 1}
//
// Vector arithmetic
v3 := vec3(3,2,1);
add := v2 + v3;
print("add: {}\n", add);
// Element access
v2x := v2.x;
print("v2.x: {}\n", v2x);
// Index access
v2i := v2[1];
print("v2[1]: {}\n", v2i);
// Scalar broadcast
scaled := v2 * 2.0;
print("scaled: {}\n", scaled);
// Negation
neg := -v2;
print("neg: {}\n", neg);
// sqrt
s := sqrt(9.0);
print("sqrt(9): {}\n", s);
// inline generic type
Sx :: (user: $T) -> Type {
return union {
counter: s32;
user: T;
};
}
sx := Sx(f32).user(0.5);
print("{}\n", sx);
print("{}\n", size_of(f32));
print("{}\n", size_of(Sx(f32)));
print("{}\n", size_of(Foo));
print("{}\n", size_of(Complex));
size:= size_of(Sx);
print("{}\n", size);
}

View File

@@ -0,0 +1,28 @@
#import "modules/std.sx";
math :: #import "modules/std/math.sx";
vec3 :: (x:f32, y:f32, z:f32) -> Vector(3,f32) {
.[x, y, z];
}
main :: () {
a := vec3(1, 0, 0);
b := vec3(0, 1, 0);
// dot product
d := math.dot(a, b);
print("dot: {}\n", d);
// cross product
cr := math.cross(a, b);
print("cross: {}\n", cr);
// length
v := vec3(3, 4, 0);
len := math.length(v);
print("length: {}\n", len);
// normalize
n := math.normalize(v);
print("norm: {}\n", n);
}

12
examples/12-meta.sx Normal file
View File

@@ -0,0 +1,12 @@
#import "modules/std.sx";
#import "modules/math.sx";
main :: () {
x:Type = f64;
v:f64 = 3.2;
print("{}\n", x);
print("{}\n", v);
x= Vec4;
print("{}\n", x);
}

9
examples/13-code.sx Normal file
View File

@@ -0,0 +1,9 @@
#import "modules/std.sx";
generate::() -> string {
return "print(\"hello from the other side\n\");";
}
main :: () {
#insert #run generate();
}

15
examples/14-demo.sx Normal file
View File

@@ -0,0 +1,15 @@
std :: #import "modules/std.sx";
vec3 :: (x:f32, y:f32, z:f32) -> std.Vector(3, f32) {
.[x,y,z];
}
main :: () {
v1 := vec3(1,0,0);
v2 := vec3(0,0,1);
s := 0.5;
sum := (v1 - v2);// math.cross(v1, v2);
std.print("{}\n", sum);
}

50
examples/15-while.sx Normal file
View File

@@ -0,0 +1,50 @@
#import "modules/std.sx";
sumOf10 :: () -> s32 {
i:= 1;
s:=0;
while i <= 10 {
s+=i;
i+=1;
}
s;
}
someSum :: #run sumOf10();
main :: {
// Basic while loop: count to 5
i := 0;
while i < 5 {
i += 1;
}
print("count: {}\n", i);
// While with break
x := 1;
while x < 100 {
if x == 12 {
break;
}
x += 1;
}
print("break at: {}\n", x);
// While with continue: sum odd numbers 1-9
sum := 0;
j := 0;
while j < 10 {
j += 1;
// Skip even numbers
if j == 2 { continue; }
if j == 4 { continue; }
if j == 6 { continue; }
if j == 8 { continue; }
if j == 10 { continue; }
sum += j;
}
print("sum of odd 1-9: {}\n", sum);
print("sum {}", someSum);
}

48
examples/16-union.sx Normal file
View File

@@ -0,0 +1,48 @@
#import "modules/std.sx";
Shape :: union {
circle: f32;
rect: s32;
none;
}
main :: () {
// Construction with .variant(payload)
s :Shape = .circle(3.14);
print("circle: {}\n", s);
// Payload access
r := s.circle;
print("radius: {}\n", r);
// Void variant via enum literal
s = .none;
print("none: {}\n", s);
// Reassign with payload
s = .rect(42);
print("rect: {}\n", s);
// Explicit prefix construction
sh :Shape = Shape.circle(2.71);
print("sh: {}\n", sh);
// Field access on second union variable
sh2 :Shape = .rect(10);
val := sh2.rect;
print("rect val: {}\n", val);
// Match on union
if sh2 == {
case .circle: print("matched circle\n");
case .rect: print("matched rect\n");
case .none: print("matched none\n");
}
cs := if sh2 == {
case .circle: 1;
case .rect: 2;
case .none: 3;
}
print("case : {}", cs);
}

9
examples/17-lambda.sx Normal file
View File

@@ -0,0 +1,9 @@
#import "modules/std.sx";
main :: () {
fx :: (s:s3) -> s3 {
s;
}
print("{}\n", fx(133));
}

23
examples/18-conditions.sx Normal file
View File

@@ -0,0 +1,23 @@
#import "modules/std.sx";
main :: () {
x:= 32;
y:= 40;
if 0 <= x <= 100 and 0 <= y <= 100 {
print("contained");
}
if 0 <= x <= 100 and 0 <= y <= 100 {
print("contained");
}
if 1000 > x > -100 and 0 <= y <= 100 {
print("contained");
}
if 1000 > x >= -100 and 0 <= y <= 100 {
print("contained");
}
}

35
examples/19-varargs.sx Normal file
View File

@@ -0,0 +1,35 @@
#import "modules/std.sx";
sum :: (args: ..s32) -> s32 {
result := 0;
for args {
result = result + it;
}
result;
}
print_all :: (args: ..s32) {
for args {
write(int_to_string(it));
write(" ");
}
write("\n");
}
main :: () -> s32 {
write(int_to_string(sum(10, 20, 30)));
write("\n");
print_all(1, 2, 3, 4, 5);
arr : [3]s32 = .[10, 20, 30];
write(int_to_string(sum(..arr)));
write("\n");
for arr {
write(int_to_string(it));
write(" ");
}
write("\n");
0;
}

View File

@@ -0,0 +1,47 @@
#import "modules/std.sx";
Point :: struct {
x: s32;
y: s32;
}
// Print all arguments — accepts any type, dispatches via type-switch
print_any :: (args: ..Any) {
for args {
type := type_of(it);
if type == {
case int: write(int_to_string(cast(s32) it));
case string: write(cast(string) it);
case bool: write(bool_to_string(cast(bool) it));
case float: write(float_to_string(cast(f64) it));
case Point: {
p := cast(Point) it;
write("(");
write(int_to_string(p.x));
write(",");
write(int_to_string(p.y));
write(")");
}
}
write(" ");
}
write("\n");
}
count :: (args: ..Any) -> s32 {
args.len;
}
main :: () -> s32 {
print_any(42, "hello", true, 3.14);
// Test with struct
p := Point.{ x=10, y=20 };
print_any("point:", p, 99);
// Test count
write(int_to_string(count(1, 2, 3)));
write("\n");
0;
}

19
examples/21-categories.sx Normal file
View File

@@ -0,0 +1,19 @@
#import "modules/std.sx";
Point :: struct {
x, y: s32;
}
Color :: struct {
r, g, b: s32;
}
main :: () {
p := Point.{10, 20};
c := Color.{255, 128, 0};
print("p: {}\n", p);
print("c: {}\n", c);
print("n: {}\n", 42);
print("s: {}\n", "hello");
print("b: {}\n", true);
}

11
examples/22-anytype.sx Normal file
View File

@@ -0,0 +1,11 @@
#import "modules/std.sx";
main :: {
i := 0;
while i < 10 {
i+=1;
if i == 2 then continue;
if i == 5 then break;
}
print("{}\n", i);
}

17
examples/modules/math.sx Normal file
View File

@@ -0,0 +1,17 @@
#import "std.sx";
dot :: (a: Vector(3,f32), b: Vector(3,f32)) -> f32 {
return a.x*b.x + a.y*b.y + a.z*b.z;
}
cross :: (a: Vector(3,f32), b: Vector(3,f32)) -> Vector(3,f32) {
.[a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x];
}
length :: (v: Vector(3,f32)) -> f32 {
return sqrt(dot(v, v));
}
normalize :: (v: Vector(3,f32)) -> Vector(3,f32) {
return v / length(v);
}

220
examples/modules/std.sx Normal file
View File

@@ -0,0 +1,220 @@
Vector :: ($N: int, $T: Type) -> Type #builtin;
write :: (str: string) -> void #builtin;
sqrt :: (x: $T) -> T #builtin;
size_of :: ($T: Type) -> s32 #builtin;
alloc :: (size: s32) -> string #builtin;
type_of :: (val: $T) -> Type #builtin;
type_name :: ($T: Type) -> string #builtin;
field_count :: ($T: Type) -> s32 #builtin;
field_name :: ($T: Type, idx: s32) -> string #builtin;
field_value :: (s: $T, idx: s32) -> Any #builtin;
int_to_string :: (n: s32) -> string {
if n == 0 { return "0"; }
neg := n < 0;
v := if neg then 0 - n else n;
tmp := v;
len := 0;
while tmp > 0 { len += 1; tmp = tmp / 10; }
total := if neg then len + 1 else len;
buf := alloc(total);
i := total - 1;
while v > 0 {
buf[i] = (v % 10) + 48;
v = v / 10;
i -= 1;
}
if neg { buf[0] = 45; }
buf;
}
bool_to_string :: (b: bool) -> string {
if b then "true" else "false";
}
float_to_string :: (f: f64) -> string {
neg := f < 0.0;
v := if neg then 0.0 - f else f;
int_part := cast(s32) v;
frac := cast(s32) ((v - cast(f64) int_part) * 1000000.0);
if frac < 0 { frac = 0 - frac; }
istr := int_to_string(int_part);
fstr := int_to_string(frac);
il := istr.len;
fl := fstr.len;
prefix := if neg then 1 else 0;
total := prefix + il + 1 + 6;
buf := alloc(total);
pos := 0;
if neg { buf[0] = 45; pos = 1; }
i := 0;
while i < il { buf[pos] = istr[i]; pos += 1; i += 1; }
buf[pos] = 46;
pos += 1;
pad := 6 - fl;
j := 0;
while j < pad { buf[pos] = 48; pos += 1; j += 1; }
k := 0;
while k < fl { buf[pos] = fstr[k]; pos += 1; k += 1; }
buf;
}
concat :: (a: string, b: string) -> string {
al := a.len;
bl := b.len;
buf := alloc(al + bl);
i := 0;
while i < al { buf[i] = a[i]; i += 1; }
j := 0;
while j < bl { buf[al + j] = b[j]; j += 1; }
buf;
}
substr :: (s: string, start: s32, len: s32) -> string {
buf := alloc(len);
i := 0;
while i < len {
buf[i] = s[start + i];
i += 1;
}
buf;
}
struct_to_string :: (s: $T) -> string {
result := concat(type_name(T), "{");
i := 0;
while i < field_count(T) {
if i > 0 { result = concat(result, ", "); }
result = concat(result, field_name(T, i));
result = concat(result, ": ");
result = concat(result, any_to_string(field_value(s, i)));
i += 1;
}
concat(result, "}");
}
enum_to_string :: (e: $T) -> string {
concat(".", field_name(T, cast(s32) e));
}
vector_to_string :: (v: $T) -> string {
result := "[";
i := 0;
while i < field_count(T) {
if i > 0 { result = concat(result, ", "); }
result = concat(result, any_to_string(field_value(v, i)));
i += 1;
}
concat(result, "]");
}
array_to_string :: (a: $T) -> string {
result := "[";
i := 0;
while i < field_count(T) {
if i > 0 { result = concat(result, ", "); }
result = concat(result, any_to_string(field_value(a, i)));
i += 1;
}
concat(result, "]");
}
union_to_string :: (u: $T) -> string {
tag := cast(s32) u;
result := concat(".", field_name(T, tag));
payload := field_value(u, tag);
pstr := any_to_string(payload);
if pstr.len > 0 {
result = concat(result, concat("(", concat(pstr, ")")));
}
result;
}
any_to_string :: (val: Any) -> string {
result := "<?>";
type := type_of(val);
if type == {
case void: result = "";
case int: result = int_to_string(xx val);
case string: { s : string = xx val; result = s; }
case bool: result = bool_to_string(xx val);
case float: result = float_to_string(xx val);
case struct: result = struct_to_string(cast(type) val);
case enum: result = enum_to_string(cast(type) val);
case vector: result = vector_to_string(cast(type) val);
case array: result = array_to_string(cast(type) val);
case union: result = union_to_string(cast(type) val);
}
result;
}
build_print :: (fmt: string) -> string {
code := "result := \"\"; ";
seg_start := 0;
i := 0;
arg_idx := 0;
while i < fmt.len {
if fmt[i] == 123 {
if i + 1 < fmt.len {
if fmt[i + 1] == 125 {
if i > seg_start {
code = concat(code, "result = concat(result, substr(fmt, ");
code = concat(code, int_to_string(seg_start));
code = concat(code, ", ");
code = concat(code, int_to_string(i - seg_start));
code = concat(code, ")); ");
}
code = concat(code, "result = concat(result, any_to_string(args[");
code = concat(code, int_to_string(arg_idx));
code = concat(code, "])); ");
arg_idx += 1;
i += 2;
seg_start = i;
} else if fmt[i + 1] == 123 {
code = concat(code, "result = concat(result, substr(fmt, ");
code = concat(code, int_to_string(seg_start));
code = concat(code, ", ");
code = concat(code, int_to_string(i - seg_start + 1));
code = concat(code, ")); ");
i += 2;
seg_start = i;
} else {
i += 1;
}
} else {
i += 1;
}
} else if fmt[i] == 125 {
if i + 1 < fmt.len {
if fmt[i + 1] == 125 {
code = concat(code, "result = concat(result, substr(fmt, ");
code = concat(code, int_to_string(seg_start));
code = concat(code, ", ");
code = concat(code, int_to_string(i - seg_start + 1));
code = concat(code, ")); ");
i += 2;
seg_start = i;
} else {
i += 1;
}
} else {
i += 1;
}
} else {
i += 1;
}
}
if seg_start < fmt.len {
code = concat(code, "result = concat(result, substr(fmt, ");
code = concat(code, int_to_string(seg_start));
code = concat(code, ", ");
code = concat(code, int_to_string(fmt.len - seg_start));
code = concat(code, ")); ");
}
code = concat(code, "write(result);");
code;
}
print :: ($fmt: string, args: ..Any) {
#insert build_print(fmt);
}

98
examples/vision.sx Normal file
View File

@@ -0,0 +1,98 @@
main :: {
// imagine a game loop
while(running) {
render_ui(build_menu);
}
}
build_menu :: (ctx: ViewContext) -> View {
// use ctx to allocate some state at the for Menu ViewContext
state : MenuState = ctx.state(MenuState);
// named args
HStack(ctx,
children = .[
Button(ctx,
label = "Up",
onTap = ctx.callback(goUp, state),
),
ScrollView(ctx,
LazyVStack(ctx,
builder = ctx.callback(build_menu_entry, state),
),
)
],
);
}
build_menu_entry :: (ctx: *ViewContext, index: s32, state: *MenuState) -> View {
entry := state.entries[index];
is_selected := index == state.selected_index;
icon := if entry.is_dir then "[D]" else " ";
Button(ctx,
label = concat(icon, " ", entry.name),
on_tap = ctx.callback(menu_go, state, index),
);
}
ViewContext :: struct {
//TBD
}
MenuState :: struct {
current_path: string;
entries: List(MenuEntry);
error_message: string;
}
MenuEntry :: struct {
name: string;
is_dir: bool;
}
menu_go_up :: (state: *MenuState) {
parent := fs.path.dirname(state.current_path) else return;
// this frees the current path & copies parent to be owned by MenuState
state.current_path = parent;
menu_refresh(state);
}
menu_go :: (state: *MenuState, s32 index) {
entry := state.entries[index] else return;
state.current_path := concat(state.current_path, "/", entry.name);
menu_refresh(state);
}
menu_refresh :: (state: *MenuState) {
// this could retain the capacity
state.entries.clear();
// this would basically create a copy of the empty string :(
state.error_message = "";
// ... multi return params vs Generic Result to deal with exceptions
// ... nullable
dir := io.Dir.open(state.current_path);
if !dir {
state.error_message = "failed to open";
return;
};
defer dir.close();
for iter.iterate() {
entries.append(.{it.name, it.kind == .Directory});
}
}
HStackState :: struct {
spacing: f32 = 8;
alignment: VerticalAlignment = .center;
padding: f32 = 0;
background: ?Color;
corner_radius: f32 = 0,
}
HStack :: (ctx: ViewContext, children: []View) -> View {
data := ctx.alloc(HStackState);
data.* = .{};
}

17
llvm_shim.c Normal file
View File

@@ -0,0 +1,17 @@
#include <llvm-c/Core.h>
#include <llvm-c/Target.h>
#include <llvm-c/TargetMachine.h>
#include <llvm-c/Analysis.h>
void sx_llvm_init_all_targets(void) {
LLVMInitializeAllTargetInfos();
LLVMInitializeAllTargets();
LLVMInitializeAllTargetMCs();
LLVMInitializeAllAsmPrinters();
LLVMInitializeAllAsmParsers();
}
void sx_llvm_init_native_target(void) {
LLVMInitializeNativeTarget();
LLVMInitializeNativeAsmPrinter();
}

40
readme.md Normal file
View File

@@ -0,0 +1,40 @@
# sx
*** HIGHLY EXPERIMENTAL *** DON'T USE ***
This experiment is trying to answer a few questions:
Q: Can we have an system language to build declarative ui ?
NOTE:
> i hope you have memory... currently it doesn't free anything :D
## Building
Requires **Zig 0.16+** and **LLVM 19**.
```sh
zig build
```
## Usage
```sh
# compile to binary
sx build examples/06-generic.sx
# compile and run
sx run examples/06-generic.sx
# emit LLVM IR
sx ir examples/06-generic.sx
# start the language server
sx lsp
```
## Acknowledgments
- [Jonathan Blow](https://en.wikipedia.org/wiki/Jonathan_Blow) — for Jai, the language that inspired this one
- [Andrew Kelley](https://andrewkelley.me) — for Zig, which made this compiler a joy to write

853
specs.md Normal file
View File

@@ -0,0 +1,853 @@
# sx language specification
## 1. Lexical Structure
### Comments
Line comments start with `//` and extend to end of line.
```sx
// this is a comment
```
### Identifiers
- Lowercase or mixed-case for variables, functions: `x`, `compute`, `main`
- UPPER_SNAKE_CASE for constants: `SOME_INT`, `SOME_STR`
- PascalCase for types: `Foo`
### Literals
| Kind | Examples | Type |
|-----------|---------------------|---------|
| Integer | `0`, `42`, `0xFF`, `0b1010` | `s32` |
| Float | `0.3`, `0.9` | `f32` |
| String | `"Hello"`, `"z: {z}"` | `string` |
| Boolean | `true`, `false` | `bool` |
| Enum | `.variant1` | inferred from context |
| Undefined | `---` | context-dependent |
### Keywords
`if`, `else`, `then`, `while`, `break`, `continue`, `true`, `false`, `enum`, `struct`, `union`, `case`, `return`, `defer`, `xx`, `and`, `or`
### Operators
| Operator | Meaning |
|----------|------------------|
| `+` | addition |
| `-` | subtraction / negation |
| `*` | multiplication |
| `/` | division |
| `==` | equality |
| `!=` | inequality |
| `<` | less than |
| `>` | greater than |
| `<=` | less or equal |
| `>=` | greater or equal |
| `and` | logical AND (short-circuit) |
| `or` | logical OR (short-circuit) |
| `+=` | add-assign |
| `-=` | sub-assign |
| `*=` | mul-assign |
| `/=` | div-assign |
### Delimiters and Punctuation
| Token | Meaning |
|--------|--------------------------------------|
| `::` | constant binding / definition |
| `:=` | variable binding (mutable, inferred) |
| `:` | type annotation |
| `=` | assignment (in typed var decl) |
| `;` | statement terminator |
| `,` | separator |
| `.` | field access / enum literal prefix |
| `->` | return type annotation |
| `=>` | lambda arrow |
| `$` | generic type parameter introduction |
| `---` | undefined value |
| `()` | grouping / params |
| `{}` | blocks / bodies |
---
## 2. Type System
### Primitive Types
- `s1`..`s64` — signed integers (1 to 64 bits). `s32` is the default for integer literals.
- `u1`..`u64` — unsigned integers (1 to 64 bits).
- `f32` — 32-bit floating point
- `f64` — 64-bit floating point
- `bool` — boolean (`true` / `false`)
- `string` — string of characters
- `Any` — type-erased value, represented as `{ i32, i64 }` (type tag + payload). Used for variadic arguments and runtime type dispatch.
- `Type` — compile-time type value. At runtime, represented as an `i32` type tag (same tag space as `Any`).
### Enum Types
User-defined sum types with named variants.
```sx
Foo :: enum {
variant1;
variant2;
}
```
Variants are referenced with dot-prefix syntax: `.variant1`
### Struct Types
User-defined product types with named fields.
```sx
Vec4 :: struct {
x, y, z, w: f32;
}
```
Fields are declared as `name1, name2: type;` (comma-separated names sharing a type, semicolon-terminated).
#### Field Defaults
Fields may have default values. Fields without an explicit default have a zero-value default. `---` marks a field as explicitly undefined.
```sx
Foo :: struct {
a : u2; // default is 0
b : u8 = 42; // default is 42
c : u8 = ---; // default is undefined
}
```
#### Struct Literals
```sx
// Positional (with type annotation — type inferred from annotation)
v1 : Vec4 = .{ 1, 2, 3, 0 };
// Positional (with type prefix)
v2 := Vec4.{ 4, 1, 1, 3 };
// Named fields (any order)
v3 := Vec4.{ w=0, x=2, y=3, z=4 };
// Mixed named + shorthand (bare identifier = field name matches variable name)
z := 5.0;
w := 6.0;
v4 := Vec4.{ y=3, x=9, w, z };
```
#### Field Access and Assignment
```sx
v1.x // read field x of struct v1
v1.x = 3.0; // assign to field x of struct v1
```
#### Struct Interpolation
Struct values in string interpolation print as `TypeName{field:value, ...}`:
```sx
print("{v1}"); // Vec4{x:1.0, y:2.0, z:3.0, w:0.0}
```
### Union Types (Tagged Unions)
Sum types where each variant can carry typed data or be void. Internally represented as `{ i32, [max_payload_size x i8] }`.
#### Declaration
```sx
Shape :: union {
circle: f32; // typed variant
rect: s32; // typed variant
none; // void variant
}
```
#### Construction
```sx
s :Shape = .circle(3.14); // inferred from context
s = .none; // void variant (enum literal syntax)
s = Shape.rect(42); // explicit prefix
```
#### Payload Access
```sx
r := s.circle; // load payload as f32 (undefined behavior if wrong variant active)
```
#### Pattern Matching
```sx
if s == {
case .circle: print("circle\n");
case .rect: print("rect\n");
case .none: print("none\n");
}
```
#### Union Interpolation
Union values in string interpolation print as `<TypeName tag=N>`:
```sx
print("{s}"); // <Shape tag=0>
```
### Array Types
Fixed-size arrays with element type and length.
```sx
buffer : [5]f32 = .[0, 2, 3.5, 4, 0];
val := buffer[2]; // 3.5
```
### Vector Types (SIMD)
LLVM SIMD vectors, parameterized by length and element type.
```sx
v := vec3(1, 3, 2); // Vector(3, f32)
```
**Arithmetic**: Element-wise `+`, `-`, `*`, `/` on vectors of same dimensions.
```sx
add := v1 + v2; // element-wise addition
```
**Scalar broadcast**: Scalar operands are broadcast to match the vector.
```sx
scaled := v * 2.0; // [2.0, 6.0, 4.0]
```
**Negation**: Unary `-` negates each element.
```sx
neg := -v; // [-1.0, -3.0, -2.0]
```
**Element access**: `.x`, `.y`, `.z`, `.w` (aliases `.r`, `.g`, `.b`, `.a`) extract single components.
```sx
v.x // first element
v.z // third element
```
**Index access**: `v[i]` extracts by index.
```sx
v[0] // first element
```
**Built-in `sqrt`**: Calls LLVM `llvm.sqrt.f32`/`.f64` intrinsic.
```sx
s := sqrt(9.0); // 3.0
```
### Function Types
Expressed as `(param_types) -> return_type`.
A function with no return type annotation returns void.
```sx
// type is (s32) -> s32
compute :: (x: s32) -> s32 { x * x; }
// type is () -> void
main :: () { }
```
### Type Aliases
A name bound to an existing type.
```sx
SOME_TYPE :: f64;
```
### Generic Functions (Monomorphization)
Functions can be parameterized over types using `$T` syntax. The `$` prefix introduces a type parameter; subsequent uses of the name reference it.
```sx
sum :: (a: $T, b: T) -> T {
return a + b;
}
```
- `$T` in a parameter type **introduces** type parameter `T`
- Bare `T` (without `$`) **references** the introduced type parameter
- At call sites, type arguments are **inferred** from actual argument types:
```sx
sum(40, 2) // T = s32
sum(1.5, 2.5) // T = f32
```
- Each unique set of concrete types produces a **separate specialized function** (monomorphization)
- Multiple type parameters are supported: `(a: $T, b: $U) -> T`
### Variadic Functions
Functions can accept a variable number of arguments using `..Type` syntax:
```sx
print :: (fmt: string, args: ..Any) { ... }
```
- `..Any` means zero or more arguments, each boxed into `Any` (type tag + payload)
- The variadic parameter must be the last parameter
- At call sites, variadic arguments are automatically boxed: `print("x={}, y={}\n", x, y)`
- Inside the function body, `args` is accessed as a slice-like sequence
### Type Inference
- `::` bindings infer type from the right-hand side
- `:=` bindings infer type from the right-hand side
- Explicit annotation overrides inference: `NAME : f64 : 0.9;`
- Integer literals default to `s32`
- Float literals default to `f32`
- Enum literals (`.variant`) infer their enum type from context (expected type)
### Type Conversions
**Implicit (widening)** — allowed without annotation:
- Integer to wider integer of same signedness (`u8` → `u16`, `s8` → `s32`)
- Unsigned to strictly wider signed (`u8` → `s16`)
- Any integer to any float (`u8` → `f32`, `s32` → `f64`)
- Float to wider float (`f32` → `f64`)
- Integer and float literals can convert to any numeric type implicitly
**Explicit (narrowing)** — requires `xx` prefix:
- Integer to narrower integer (`s32` → `u8`)
- Signed to unsigned (`s32` → `u32`)
- Float to narrower float (`f64` → `f32`)
- Float to any integer (`f64` → `u16`)
- Unsigned to signed of same or narrower width (`u8` → `s8`)
The `xx` prefix operator marks an expression for auto-conversion to the expected type from context (assignment, declaration, argument, return):
```sx
large: f64 = 5999.5;
x : u16 = xx large; // f64 → u16
d : u8 = #run xx resolve(5); // s32 → u8 at compile time
```
Using `xx` outside a typed context (where the target type is known) is a compile error.
---
## 3. Declarations
### Constant Binding (immutable)
```sx
// inferred type
NAME :: value;
// explicit type
NAME : type : value;
```
The `::` operator creates an immutable binding. The value is evaluated at compile time when possible.
Examples:
```sx
SOME_INT :: 0; // s32
SOME_STR :: "Hello"; // string
SOME_FLOAT :: 0.3; // f32
SOME_DOUBLE : f64 : 0.9; // f64 (explicit)
SOME_FUNC :: () => 42; // () -> s32
SOME_TYPE :: f64; // type alias
```
### Variable Binding (mutable)
```sx
// inferred type
name := value;
// explicit type
name : type = value;
// default-initialized (type required)
name : type;
// undefined (type required)
name : type = ---;
```
The `:=` operator creates a mutable binding. The type is inferred unless explicitly annotated.
`name : type;` initializes using the type's defaults: zero for primitives, per-field defaults for structs (see Field Defaults).
`name : type = ---;` leaves the value undefined (uninitialized memory). Reading before writing is undefined behavior.
Examples:
```sx
x := 42; // s32, mutable
x := if true then 1 else 2;
z : Foo = .variant2; // Foo, mutable, explicit type
a : Foo; // Foo, default-initialized (a=0, b=42, c=undef)
b : Foo = ---; // Foo, entirely undefined
```
### Function Definition
```sx
name :: (params) -> return_type {
body
}
```
- Parameters: `name: type` separated by commas
- Return type: `-> type` (omit for void)
- Body: block of statements; last expression is the implicit return value
- No `return` keyword needed (last expression = return value)
Examples:
```sx
compute :: (x: s32) -> s32 {
x * x;
}
main :: () {
// void return, no -> annotation
}
// Bare-block shorthand (equivalent to no-arg void function):
main :: {
// same as main :: () { ... }
}
```
### Enum Definition
```sx
Name :: enum {
variant1;
variant2;
}
```
Defines a new enum type with the given variants. Trailing comma is allowed.
---
## 4. Expressions
Everything in `sx` is expression-oriented where possible.
### Operator Precedence
| Prec | Operators | Notes |
|------|-----------|-------|
| 6 (highest) | `*`, `/` | multiplication, division |
| 5 | `+`, `-` | addition, subtraction |
| 4 | `<`, `<=`, `>`, `>=`, `==`, `!=` | comparisons (chainable) |
| 2 | `and` | logical AND (short-circuit) |
| 1 (lowest) | `or` | logical OR (short-circuit) |
### Arithmetic
Standard infix: `+`, `-`, `*`, `/` with usual precedence (`*`/`/` before `+`/`-`).
```sx
x * x
x + 2
```
### Chained Comparisons
Comparison operators can be chained. Each operand is evaluated exactly once.
```sx
0 <= x <= 100 // equivalent to: 0 <= x and x <= 100
1000 > x >= -100 // equivalent to: 1000 > x and x >= -100
a == b == c // equivalent to: a == b and b == c
```
Mixed operators are allowed: `a < b <= c > d` means `a < b and b <= c and c > d`.
### Logical Operators
`and` and `or` are short-circuit boolean operators. The right operand is not evaluated if the left operand determines the result.
```sx
if 0 <= x <= 100 and 0 <= y <= 100 {
print("contained");
}
```
### If Expression (inline form)
```sx
if condition then consequent else alternate
```
Both branches are single expressions. The whole form produces a value.
```sx
x := if true then 1 else 2;
```
The `else` branch is optional. Without it, the form is a statement (no value):
```sx
if i == 2 then continue;
if done then break;
if err then return;
```
### If Expression (block form)
```sx
if condition {
stmts
} else {
stmts
}
```
Each branch is a block. The last expression in each block is the branch's value. Can be used inline within other expressions:
```sx
y := x + if false {
7;
} else {
12;
};
```
### Pattern Matching
```sx
if subject == {
case pattern: body
case pattern: body
else: body // optional default arm
}
```
Matches `subject` against each `case`. Patterns can be:
- **Enum literals**: `.variant` — matches a specific enum variant.
- **Integer/bool literals**: `42`, `true` — matches a specific value.
- **Type categories**: `struct`, `enum`, `union` — matches all types in that category (used with `type_of` values).
`break` exits a case arm without producing a value. The optional `else:` arm matches when no `case` pattern matches.
```sx
if z == {
case .variant1: break;
case .variant2:
print("z: {z}");
else:
print("unknown");
}
```
#### Type Category Matching
When switching on a `Type` value (from `type_of`), category keywords match all registered types of that category:
```sx
type := type_of(val);
if type == {
case int: result = int_to_string(xx val);
case struct: result = struct_to_string(cast(type) val);
case enum: result = enum_to_string(cast(type) val);
}
```
Available categories: `int`, `float`, `bool`, `string`, `struct`, `enum`, `union`.
Inside a category arm, `cast(type) val` performs **runtime generic dispatch**: the compiler generates a switch over all types in the category, monomorphizing the callee for each concrete type.
### While Loop
```sx
while condition {
body
}
```
Repeats `body` as long as `condition` is true. `break;` exits the loop. `continue;` skips to the next iteration.
```sx
i := 0;
while i < 10 {
i += 1;
if i == 5 { continue; }
if i == 8 { break; }
print("{i}\n");
}
```
### For Loop
```sx
for iterable {
// `it` is the current element
// `it_index` is the current index (s32)
print("{it}\n");
}
```
Iterates over arrays and slices. The loop body has two implicit variables:
- `it` — the current element value
- `it_index` — the current index (s32, starting at 0)
`break;` exits the loop. `continue;` skips to the next iteration.
```sx
arr : [5]s32 = .[1, 2, 3, 4, 5];
for arr {
if it_index == 2 { continue; }
print("{it}\n");
}
```
### Lambda
```sx
(params) => expr
(params) -> return_type => expr
```
Anonymous function. Produces a function value. Supports the same parameter features as named functions: `$` generic type params, `..` variadic params, and optional return type annotation.
```sx
SOME_FUNC :: () => 42; // () -> s32
double :: (x: $T) -> T => x + x; // generic lambda with return type
```
### Function Call
```sx
callee(args)
```
```sx
compute(6)
print("hello")
```
### Field Access
```sx
object.field
```
Used for module access (`std.print`) and struct member access.
### Enum Literal
```sx
.variant_name
```
The enum type is inferred from context (expected type from declaration or parameter).
### String Interpolation
Curly braces inside string literals interpolate expressions:
```sx
"z: {z}"
```
The expression inside `{}` is evaluated and formatted according to its type:
- `s32` — decimal integer
- `f64` — decimal float
- `string` — as-is
---
## 5. Statements
Statements are terminated by `;`.
- **Declaration**: `name :: value;` / `name := value;`
- **Assignment**: `name = value;` / `name += value;` (and other compound assignments). Also supports field targets: `obj.field = value;`
- **Expression statement**: `expr;` — evaluates the expression (last in a block = return value)
- **Return**: `return expr;` — returns from the enclosing function with the given value. `return;` returns void.
- **Break**: `break;` — exits a match arm or while loop
- **Continue**: `continue;` — skips to the next iteration of a while loop
- **Defer**: `defer expr;` — defers execution of `expr` until the enclosing block exits (LIFO order)
---
## 6. Blocks, Scoping, and Implicit Returns
A block `{ ... }` contains zero or more statements. The last expression in a block is its value (implicit return).
In function bodies, the last expression becomes the return value:
```sx
compute :: (x: s32) -> s32 {
x * x; // this is returned
}
```
### Scope Blocks
Bare blocks can be used as statements to introduce a new lexical scope. Variables declared inside a scope block are local to that block. No trailing `;` is required.
```sx
main :: {
x := 42;
{
x := 6; // shadows outer x
print("inner: {x}"); // prints 6
}
print("outer: {x}"); // prints 42
}
```
### Variable Shadowing
A variable declaration (`name :=`) inside an inner scope shadows any variable with the same name from outer scopes. The outer variable is restored when the inner scope exits.
### Defer
`defer expr;` schedules `expr` to execute when the enclosing scope block exits. Multiple defers in the same scope execute in reverse order (LIFO).
```sx
{
defer print("second");
defer print("first");
}
// prints: first, then second
```
---
## 7. Built-in Functions
Built-in functions are declared in `std.sx` with the `#builtin` suffix, which tells the compiler to generate the implementation internally rather than looking for a function body.
### I/O
- `write(str: string) -> void` — write a string to standard output
- `print(fmt: string, args: ..Any)` — formatted print. Parses `{}` placeholders in the format string and substitutes arguments. When all argument types are statically known, the compiler specializes the call at compile time (no `Any` boxing).
### Math
- `sqrt(x: $T) -> T` — square root (maps to LLVM intrinsic)
### Memory
- `alloc(size: s32) -> string` — allocate `size` bytes of memory, returned as a string slice
- `size_of($T: Type) -> s32` — size of type `T` in bytes
### Type Introspection
- `type_of(val: $T) -> Type` — returns the runtime type tag of a value
- `type_name($T: Type) -> string` — returns the name of type `T` as a string (e.g., `"Point"`)
- `field_count($T: Type) -> s32` — returns the number of fields (struct), variants (enum), or elements (vector) in type `T`
- `field_name($T: Type, idx: s32) -> string` — returns the name of the `idx`-th field (struct) or variant (enum) of type `T`
- `field_value(s: $T, idx: s32) -> Any` — returns the `idx`-th field (struct) or element (vector) of `s`, boxed as `Any`
### Type Conversion
- `cast(Type) expr` — prefix operator that converts `expr` to `Type`. Examples: `cast(s32) 3.14`, `cast(f64) n`. When `Type` is a runtime `Type` value inside a type-category match arm, the compiler generates a dispatch switch over all types in the category, monomorphizing the callee for each concrete type.
### Vectors
- `Vector($N: int, $T: Type) -> Type` — returns an LLVM vector type of `N` elements of type `T`
---
## 8. Compile-time Evaluation
### `#run` Directive
`#run expr` evaluates `expr` at compile time using lazy JIT execution. It can appear in two contexts:
**Compile-time constants** — bind a compile-time value to a name:
```sx
compute :: (x: s32) -> s32 { x * x; }
x :: #run compute(5); // x = 25, evaluated at compile time
```
Comptime globals are resolved lazily: the JIT executes only when the value is first referenced during code generation. Chained dependencies are resolved automatically.
**Side effects** — execute code at compile time for its side effects:
```sx
#run print("compiling...");
```
### `#insert` Directive
`#insert expr;` evaluates `expr` at compile time to obtain a string, then parses and compiles that string as inline code at the insertion point.
```sx
generate :: () -> string {
return "print(\"hello from the other side\");";
}
main :: () {
#insert #run generate();
// equivalent to: print("hello from the other side");
}
```
The inserted string must contain valid `sx` statements (including semicolons). The statements are parsed and compiled in the same scope as the `#insert` site.
---
## 9. Modules / Imports
### `#import` Directive
The `#import` directive brings declarations from another `.sx` file into the current file. Paths are resolved relative to the importing file's directory.
**Flat import** — splices all declarations from the imported file into the current scope:
```sx
#import "modules/std/math.sx";
```
**Namespaced import** — wraps all declarations under a namespace name:
```sx
std :: #import "modules/std.sx";
```
Namespaced declarations are accessed with dot notation:
```sx
std.print("hello");
```
### Import Resolution
- Imports are resolved after parsing and before code generation.
- Paths are relative to the directory of the file containing the `#import`.
- Nested imports are supported (imported files may themselves contain `#import`).
- Circular imports are detected and silently skipped (each file is imported at most once).
- Generic functions in namespaced imports are supported (e.g., `std.mul(5, 2)` where `mul` is generic).
### Intra-module References
Functions within a namespaced import can call each other without the namespace prefix. When generating code for a namespaced module, unresolved function names are automatically tried with the namespace prefix.
### Example
```sx
// modules/std/math.sx
mul :: (base: $T, exp: T) -> T { base * exp; }
// modules/std/std.sx
print :: (str: string) -> void #builtin;
// main.sx
std :: #import "modules/std.sx";
#import "modules/std/math.sx";
main :: () -> s32 {
std.print("hello there");
mul(5, 2);
}
```
---
## 10. Program Structure
A program is a sequence of top-level declarations and `#import` directives. Execution begins at `main`.
```sx
main :: () {
// entry point
}
```
`main` takes no arguments and returns void. The process exit code is 0 unless otherwise specified.
---
## 11. Grammar (informal)
```
program = top_level*
top_level = decl | import_decl
import_decl = '#import' STRING ';'
| IDENT '::' '#import' STRING ';'
decl = const_decl | var_decl | fn_decl | enum_decl | struct_decl
const_decl = IDENT '::' expr ';'
| IDENT ':' type ':' expr ';'
var_decl = IDENT ':=' expr ';'
| IDENT ':' type '=' expr ';'
| IDENT ':' type ';'
fn_decl = IDENT '::' '(' params? ')' ('->' type)? block
| IDENT '::' block
enum_decl = IDENT '::' 'enum' '{' (IDENT ';')* '}'
struct_decl = IDENT '::' 'struct' '{' field_group* '}'
field_group = IDENT (',' IDENT)* ':' type ('=' expr)? ';'
params = param (',' param)*
param = IDENT ':' type
block = '{' stmt* '}'
stmt = decl | assignment ';' | return_stmt | defer_stmt | insert_stmt
| break_stmt | continue_stmt | expr ';'
return_stmt = 'return' expr? ';'
break_stmt = 'break' ';'
continue_stmt = 'continue' ';'
defer_stmt = 'defer' expr ';'
insert_stmt = '#insert' expr ';'
assignment = lvalue ('=' | '+=' | '-=' | '*=' | '/=') expr
lvalue = IDENT | postfix '.' IDENT
expr = if_expr | match_expr | while_expr | for_expr | lambda | binary
while_expr = 'while' expr block
for_expr = 'for' expr block
binary = unary (binop unary)*
unary = ('-' | '!' | 'xx' | 'cast' '(' type ')') postfix
| postfix
postfix = primary ('(' args? ')' | '.' IDENT | '.{' field_init_list '}')*
primary = INT | HEX_INT | BIN_INT | FLOAT | STRING | BOOL | IDENT | '---'
| '.' IDENT | '.' '{' field_init_list '}'
| '(' expr ')' | block | '#run' expr
field_init_list = field_init (',' field_init)*
field_init = IDENT '=' expr | IDENT | expr
if_expr = 'if' expr 'then' expr ('else' expr)?
| 'if' expr block ('else' block)?
match_expr = 'if' expr '==' '{' case_arm* else_arm? '}'
case_arm = 'case' pattern ':' (stmt* | 'break' ';')
else_arm = 'else' ':' stmt*
pattern = '.' IDENT | INT | BOOL | IDENT
lambda = '(' params? ')' ('->' type)? '=>' expr
args = expr (',' expr)*
type = '$' IDENT | 's32' | 'f32' | 'f64' | 'bool' | 'string'
| 'Any' | 'Type' | '..' type | '[' expr ']' type | IDENT
```
---
## 12. Open Questions
These are inferred gaps — things not shown in the readme that need decisions:
- **`return`**: Both `return expr;` and implicit return (last expression) are supported.
- **Else in match**: Is there a default/else arm in pattern matching?
- **Nested functions**: Can functions be defined inside other functions?
- **Mutability of params**: Are function parameters immutable by default?
- **Array/list types**: Not shown — deferred.
- **Struct types**: Implemented — named struct types with positional/named/shorthand literals.
- **Imports/modules**: `#import` directive supports flat and namespaced imports (see Section 8).
- **Operator overloading**: Not shown — presumably no.
- **Semicolons**: Required on all statements? What about the last expression in a block?
- **Top-level expressions**: Are bare expressions allowed at the top level or only declarations?

326
src/ast.zig Normal file
View File

@@ -0,0 +1,326 @@
const std = @import("std");
pub const Span = struct {
start: u32,
end: u32,
};
pub const Node = struct {
span: Span,
data: Data,
pub const Data = union(enum) {
root: Root,
fn_decl: FnDecl,
block: Block,
int_literal: IntLiteral,
float_literal: FloatLiteral,
bool_literal: BoolLiteral,
string_literal: StringLiteral,
identifier: Identifier,
enum_literal: EnumLiteral,
binary_op: BinaryOp,
chained_comparison: ChainedComparison,
unary_op: UnaryOp,
call: Call,
field_access: FieldAccess,
if_expr: IfExpr,
match_expr: MatchExpr,
match_arm: MatchArm,
const_decl: ConstDecl,
var_decl: VarDecl,
assignment: Assignment,
enum_decl: EnumDecl,
struct_decl: StructDecl,
struct_literal: StructLiteral,
union_decl: UnionDecl,
union_literal: UnionLiteral,
lambda: Lambda,
type_expr: TypeExpr,
param: Param,
defer_stmt: DeferStmt,
comptime_expr: ComptimeExpr,
insert_expr: InsertExpr,
return_stmt: ReturnStmt,
import_decl: ImportDecl,
namespace_decl: NamespaceDecl,
array_type_expr: ArrayTypeExpr,
array_literal: ArrayLiteral,
parameterized_type_expr: ParameterizedTypeExpr,
index_expr: IndexExpr,
while_expr: WhileExpr,
for_expr: ForExpr,
spread_expr: SpreadExpr,
break_expr: void,
continue_expr: void,
undef_literal: void,
builtin_expr: void,
pub fn declName(self: Data) ?[]const u8 {
return switch (self) {
.fn_decl => |d| d.name,
.const_decl => |d| d.name,
.var_decl => |d| d.name,
.enum_decl => |d| d.name,
.struct_decl => |d| d.name,
.union_decl => |d| d.name,
.namespace_decl => |d| d.name,
else => null,
};
}
};
};
pub const Root = struct {
decls: []const *Node,
};
pub const FnDecl = struct {
name: []const u8,
params: []const Param,
return_type: ?*Node,
body: *Node,
type_params: []const StructTypeParam = &.{},
};
pub const Param = struct {
name: []const u8,
name_span: Span,
type_expr: *Node,
is_variadic: bool = false,
is_comptime: bool = false,
};
pub const Block = struct {
stmts: []const *Node,
};
pub const IntLiteral = struct {
value: i64,
};
pub const FloatLiteral = struct {
value: f64,
};
pub const BoolLiteral = struct {
value: bool,
};
pub const StringLiteral = struct {
raw: []const u8,
};
pub const Identifier = struct {
name: []const u8,
};
pub const EnumLiteral = struct {
name: []const u8, // without the leading dot
};
pub const BinaryOp = struct {
op: Op,
lhs: *Node,
rhs: *Node,
pub const Op = enum {
add,
sub,
mul,
div,
mod,
eq,
neq,
lt,
lte,
gt,
gte,
and_op,
or_op,
};
};
pub const ChainedComparison = struct {
operands: []const *Node,
ops: []const BinaryOp.Op,
};
pub const UnaryOp = struct {
op: Op,
operand: *Node,
pub const Op = enum {
negate,
not,
xx,
};
};
pub const Call = struct {
callee: *Node,
args: []const *Node,
};
pub const FieldAccess = struct {
object: *Node,
field: []const u8,
};
pub const IfExpr = struct {
condition: *Node,
then_branch: *Node,
else_branch: ?*Node,
is_inline: bool, // true for `if cond then a else b`
};
pub const MatchExpr = struct {
subject: *Node,
arms: []const MatchArm,
};
pub const MatchArm = struct {
pattern: ?*Node, // null = else (default) arm
body: *Node,
is_break: bool,
};
pub const ConstDecl = struct {
name: []const u8,
type_annotation: ?*Node,
value: *Node,
};
pub const VarDecl = struct {
name: []const u8,
type_annotation: ?*Node,
value: ?*Node,
};
pub const Assignment = struct {
target: *Node,
op: Op,
value: *Node,
pub const Op = enum {
assign,
add_assign,
sub_assign,
mul_assign,
div_assign,
mod_assign,
};
};
pub const EnumDecl = struct {
name: []const u8,
variants: []const []const u8,
};
pub const StructTypeParam = struct {
name: []const u8, // e.g. "N" or "T" (without $)
constraint: *Node, // type_expr: "u32" for value param, "Type" for type param
};
pub const StructDecl = struct {
name: []const u8,
field_names: []const []const u8,
field_types: []const *Node, // type_expr nodes
field_defaults: []const ?*Node, // default value per field, null if none
type_params: []const StructTypeParam = &.{},
};
pub const StructFieldInit = struct {
name: ?[]const u8, // null for positional, non-null for named/shorthand
value: *Node,
};
pub const StructLiteral = struct {
struct_name: ?[]const u8, // null for anonymous `.{ ... }`
type_expr: ?*Node = null, // for GenericType(args).{ ... }
field_inits: []const StructFieldInit,
};
pub const Lambda = struct {
params: []const Param,
return_type: ?*Node,
body: *Node,
type_params: []const StructTypeParam = &.{},
};
pub const TypeExpr = struct {
name: []const u8,
is_generic: bool = false,
};
pub const DeferStmt = struct {
expr: *Node,
};
pub const ComptimeExpr = struct {
expr: *Node,
};
pub const InsertExpr = struct {
expr: *Node,
};
pub const ReturnStmt = struct {
value: ?*Node,
};
pub const ImportDecl = struct {
path: []const u8,
name: ?[]const u8,
};
pub const ArrayTypeExpr = struct {
length: *Node, // int_literal for the size
element_type: *Node, // type_expr for the element type
};
pub const ArrayLiteral = struct {
elements: []const *Node,
type_expr: ?*Node = null,
};
pub const ParameterizedTypeExpr = struct {
name: []const u8, // e.g. "Vector", or later generic struct names
args: []const *Node, // e.g. [int_literal(3), type_expr("f32")]
};
pub const IndexExpr = struct {
object: *Node,
index: *Node,
};
pub const WhileExpr = struct {
condition: *Node,
body: *Node,
};
pub const ForExpr = struct {
iterable: *Node,
body: *Node,
};
pub const SpreadExpr = struct {
operand: *Node,
};
pub const UnionDecl = struct {
name: []const u8,
variant_names: []const []const u8,
variant_types: []const ?*Node, // null for void variants
};
pub const UnionLiteral = struct {
union_name: ?[]const u8, // null for anonymous `.variant(expr)`
variant_name: []const u8,
payload: ?*Node, // null for void variants
};
pub const NamespaceDecl = struct {
name: []const u8,
decls: []const *Node,
};

25
src/builtins.zig Normal file
View File

@@ -0,0 +1,25 @@
const llvm = @import("llvm_api.zig");
const c = llvm.c;
pub const Builtins = struct {
printf_fn: c.LLVMValueRef,
calloc_fn: c.LLVMValueRef,
pub fn init(module: c.LLVMModuleRef, ctx: c.LLVMContextRef) Builtins {
const ptr_type = c.LLVMPointerTypeInContext(ctx, 0);
const i64_type = c.LLVMInt64TypeInContext(ctx);
const i32_type = c.LLVMInt32TypeInContext(ctx);
// Declare: int printf(const char*, ...)
var printf_params = [_]c.LLVMTypeRef{ptr_type};
const printf_type = c.LLVMFunctionType(i32_type, &printf_params, 1, 1);
const printf_fn = c.LLVMAddFunction(module, "printf", printf_type);
// Declare: void* calloc(size_t count, size_t size)
var calloc_params = [_]c.LLVMTypeRef{ i64_type, i64_type };
const calloc_type = c.LLVMFunctionType(ptr_type, &calloc_params, 2, 0);
const calloc_fn = c.LLVMAddFunction(module, "calloc", calloc_type);
return .{ .printf_fn = printf_fn, .calloc_fn = calloc_fn };
}
};

4999
src/codegen.zig Normal file

File diff suppressed because it is too large Load Diff

1753
src/comptime.zig Normal file

File diff suppressed because it is too large Load Diff

110
src/core.zig Normal file
View File

@@ -0,0 +1,110 @@
const std = @import("std");
const ast = @import("ast.zig");
const parser = @import("parser.zig");
const imports = @import("imports.zig");
const sema = @import("sema.zig");
const codegen = @import("codegen.zig");
const errors = @import("errors.zig");
const Node = ast.Node;
pub const Compilation = struct {
allocator: std.mem.Allocator,
io: std.Io,
file_path: []const u8,
source: [:0]const u8,
diagnostics: errors.DiagnosticList,
// Pipeline results
root: ?*Node = null,
resolved_root: ?*Node = null,
import_sources: std.StringHashMap([:0]const u8),
sema_result: ?sema.SemaResult = null,
cg: ?codegen.CodeGen = null,
pub fn init(allocator: std.mem.Allocator, io: std.Io, file_path: []const u8, source: [:0]const u8) Compilation {
return .{
.allocator = allocator,
.io = io,
.file_path = file_path,
.source = source,
.diagnostics = errors.DiagnosticList.init(allocator, source, file_path),
.import_sources = std.StringHashMap([:0]const u8).init(allocator),
};
}
pub fn deinit(self: *Compilation) void {
if (self.cg) |*cg| cg.deinit();
self.diagnostics.deinit();
}
pub fn parse(self: *Compilation) !void {
var p = parser.Parser.init(self.allocator, self.source);
p.diagnostics = &self.diagnostics;
self.root = p.parse() catch return error.CompileError;
}
pub fn resolveImports(self: *Compilation) !void {
const root = self.root orelse return error.CompileError;
var chain = std.StringHashMap(void).init(self.allocator);
var cache = imports.ModuleCache.init(self.allocator);
const base_dir = imports.dirName(self.file_path);
const mod = imports.resolveImports(
self.allocator,
self.io,
root,
base_dir,
self.file_path,
&chain,
&cache,
&self.import_sources,
&self.diagnostics,
) catch return error.CompileError;
// Build a root node from the resolved module's decls
const new_root = try self.allocator.create(Node);
new_root.* = .{
.span = root.span,
.data = .{ .root = .{ .decls = mod.decls } },
};
self.resolved_root = new_root;
}
pub fn analyze(self: *Compilation) !void {
const root = self.resolved_root orelse self.root orelse return error.CompileError;
var analyzer = sema.Analyzer.init(self.allocator);
self.sema_result = analyzer.analyze(root) catch return error.CompileError;
// Merge sema diagnostics into our list
if (self.sema_result) |sr| {
for (sr.diagnostics) |d| {
self.diagnostics.add(d.level, d.message, d.span);
}
}
}
pub fn generateCode(self: *Compilation) !void {
const root = self.resolved_root orelse self.root orelse return error.CompileError;
var cg = codegen.CodeGen.init(self.allocator, "sx_module");
cg.diagnostics = &self.diagnostics;
if (self.sema_result) |*sr| {
cg.sema_result = sr;
}
cg.generate(root) catch return error.CompileError;
self.cg = cg;
}
pub fn renderErrors(self: *const Compilation) void {
for (self.diagnostics.items.items) |d| {
const level_str = switch (d.level) {
.err => "error",
.warn => "warning",
.note => "note",
};
if (d.span) |span| {
const loc = errors.SourceLoc.compute(self.source, span.start);
std.debug.print("{s}:{d}:{d}: {s}: {s}\n", .{ self.file_path, loc.line, loc.col, level_str, d.message });
} else {
std.debug.print("{s}: {s}: {s}\n", .{ self.file_path, level_str, d.message });
}
}
}
};

96
src/errors.zig Normal file
View File

@@ -0,0 +1,96 @@
const std = @import("std");
const Span = @import("ast.zig").Span;
pub const Level = enum {
err,
warn,
note,
};
pub const SourceLoc = struct {
line: u32,
col: u32,
pub fn compute(source: []const u8, byte_offset: u32) SourceLoc {
var line: u32 = 1;
var col: u32 = 1;
for (source[0..byte_offset]) |c| {
if (c == '\n') {
line += 1;
col = 1;
} else {
col += 1;
}
}
return .{ .line = line, .col = col };
}
};
pub const Diagnostic = struct {
level: Level,
message: []const u8,
span: ?Span,
};
pub const DiagnosticList = struct {
items: std.ArrayList(Diagnostic) = .empty,
allocator: std.mem.Allocator,
source: []const u8,
file_name: []const u8,
pub fn init(allocator: std.mem.Allocator, source: []const u8, file_name: []const u8) DiagnosticList {
return .{
.allocator = allocator,
.source = source,
.file_name = file_name,
};
}
pub fn deinit(self: *DiagnosticList) void {
self.items.deinit(self.allocator);
}
pub fn add(self: *DiagnosticList, level: Level, message: []const u8, span: ?Span) void {
// Deduplicate: skip if same level+span+message already exists
for (self.items.items) |d| {
if (d.level == level and std.mem.eql(u8, d.message, message)) {
const a = d.span orelse continue;
const b = span orelse continue;
if (a.start == b.start and a.end == b.end) return;
}
}
self.items.append(self.allocator, .{
.level = level,
.message = message,
.span = span,
}) catch {};
}
pub fn addFmt(self: *DiagnosticList, level: Level, span: ?Span, comptime fmt: []const u8, args: anytype) void {
const message = std.fmt.allocPrint(self.allocator, fmt, args) catch "diagnostic format error";
self.add(level, message, span);
}
pub fn hasErrors(self: *const DiagnosticList) bool {
for (self.items.items) |d| {
if (d.level == .err) return true;
}
return false;
}
pub fn render(self: *const DiagnosticList, writer: anytype) !void {
for (self.items.items) |d| {
const level_str = switch (d.level) {
.err => "error",
.warn => "warning",
.note => "note",
};
if (d.span) |span| {
const loc = SourceLoc.compute(self.source, span.start);
try writer.print("{s}:{d}:{d}: {s}: {s}\n", .{ self.file_name, loc.line, loc.col, level_str, d.message });
} else {
try writer.print("{s}: {s}: {s}\n", .{ self.file_name, level_str, d.message });
}
}
}
};

150
src/imports.zig Normal file
View File

@@ -0,0 +1,150 @@
const std = @import("std");
const ast = @import("ast.zig");
const parser = @import("parser.zig");
const errors = @import("errors.zig");
const Node = ast.Node;
pub fn dirName(path: []const u8) []const u8 {
var last_sep: usize = 0;
var found = false;
for (path, 0..) |ch, i| {
if (ch == '/') {
last_sep = i;
found = true;
}
}
return if (found) path[0..last_sep] else ".";
}
/// A resolved module: the fully-resolved declarations of a single .sx file,
/// with its own scope tracking which names are defined.
pub const ResolvedModule = struct {
path: []const u8,
decls: []const *Node,
scope: std.StringHashMap(void),
/// Try to add a declaration. Returns true if added, false if name already in scope.
pub fn addDecl(self: *ResolvedModule, allocator: std.mem.Allocator, list: *std.ArrayList(*Node), decl: *Node) !bool {
if (decl.data.declName()) |name| {
if (self.scope.contains(name)) return false;
try self.scope.put(name, {});
}
try list.append(allocator, decl);
return true;
}
/// Merge another module's decls as flat imports (skipping duplicates).
pub fn mergeFlat(self: *ResolvedModule, allocator: std.mem.Allocator, list: *std.ArrayList(*Node), other: ResolvedModule) !void {
for (other.decls) |decl| {
_ = try self.addDecl(allocator, list, decl);
}
}
/// Add another module as a namespaced import.
pub fn addNamespace(self: *ResolvedModule, allocator: std.mem.Allocator, list: *std.ArrayList(*Node), name: []const u8, other: ResolvedModule, span: ast.Span) !void {
const ns_node = try allocator.create(Node);
ns_node.* = .{
.span = span,
.data = .{ .namespace_decl = .{
.name = name,
.decls = other.decls,
} },
};
try self.scope.put(name, {});
try list.append(allocator, ns_node);
}
pub fn finalize(self: *ResolvedModule, allocator: std.mem.Allocator, list: *std.ArrayList(*Node)) !void {
self.decls = try list.toOwnedSlice(allocator);
}
};
/// Module cache: maps resolved file paths to their ResolvedModules.
pub const ModuleCache = std.StringHashMap(ResolvedModule);
pub fn resolveImports(
allocator: std.mem.Allocator,
io: std.Io,
root: *Node,
base_dir: []const u8,
file_path: []const u8,
chain: *std.StringHashMap(void),
cache: *ModuleCache,
source_map: ?*std.StringHashMap([:0]const u8),
diagnostics: ?*errors.DiagnosticList,
) !ResolvedModule {
var mod = ResolvedModule{
.path = file_path,
.decls = &.{},
.scope = std.StringHashMap(void).init(allocator),
};
if (root.data != .root) {
mod.decls = &.{};
return mod;
}
var decl_list = std.ArrayList(*Node).empty;
for (root.data.root.decls) |decl| {
if (decl.data != .import_decl) {
_ = try mod.addDecl(allocator, &decl_list, decl);
continue;
}
const imp = decl.data.import_decl;
// Resolve path relative to base_dir
const resolved_path = if (std.mem.eql(u8, base_dir, "."))
imp.path
else
try std.fmt.allocPrint(allocator, "{s}/{s}", .{ base_dir, imp.path });
// Circular import check — only along the current chain
if (chain.contains(resolved_path)) continue;
// Resolve or retrieve the imported module
const imported_mod = if (cache.get(resolved_path)) |cached|
cached
else blk: {
// Read imported file
const imp_bytes = std.Io.Dir.readFileAlloc(.cwd(), io, resolved_path, allocator, .limited(10 * 1024 * 1024)) catch {
if (diagnostics) |diags| {
diags.addFmt(.err, decl.span, "cannot read import '{s}'", .{resolved_path});
}
return error.ImportError;
};
const imp_source = try allocator.dupeZ(u8, imp_bytes);
if (source_map) |sm| {
sm.put(resolved_path, imp_source) catch {};
}
var p = parser.Parser.init(allocator, imp_source);
const imp_root = p.parse() catch {
if (diagnostics) |diags| {
diags.addFmt(.err, decl.span, "parse error in '{s}': {s}", .{ resolved_path, p.err_msg orelse "unknown" });
}
return error.ImportError;
};
// Push onto chain before recursing, pop after
try chain.put(resolved_path, {});
const imp_dir = dirName(resolved_path);
const result = try resolveImports(allocator, io, imp_root, imp_dir, resolved_path, chain, cache, source_map, diagnostics);
_ = chain.remove(resolved_path);
// Cache
try cache.put(resolved_path, result);
break :blk result;
};
if (imp.name) |ns_name| {
try mod.addNamespace(allocator, &decl_list, ns_name, imported_mod, decl.span);
} else {
try mod.mergeFlat(allocator, &decl_list, imported_mod);
}
}
try mod.finalize(allocator, &decl_list);
return mod;
}

403
src/lexer.zig Normal file
View File

@@ -0,0 +1,403 @@
const std = @import("std");
const Token = @import("token.zig").Token;
const Tag = @import("token.zig").Tag;
const getKeyword = @import("token.zig").getKeyword;
pub const Lexer = struct {
source: [:0]const u8,
index: u32,
pub fn init(source: [:0]const u8) Lexer {
return .{ .source = source, .index = 0 };
}
pub fn next(self: *Lexer) Token {
// Skip whitespace and comments
while (true) {
if (self.index >= self.source.len) {
return self.makeToken(.eof, self.index, self.index);
}
const c = self.source[self.index];
if (c == ' ' or c == '\t' or c == '\n' or c == '\r') {
self.index += 1;
continue;
}
// Line comments
if (c == '/' and self.index + 1 < self.source.len and self.source[self.index + 1] == '/') {
while (self.index < self.source.len and self.source[self.index] != '\n') {
self.index += 1;
}
continue;
}
break;
}
const start = self.index;
const c = self.source[start];
// Integer / float literals
if (isDigit(c)) {
return self.lexNumber(start);
}
// Identifiers and keywords
if (isIdentStart(c)) {
return self.lexIdentifier(start);
}
// String literals
if (c == '"') {
return self.lexString(start);
}
// Directives: #import, #insert, #run
if (c == '#') {
if (self.source.len >= start + 7 and std.mem.eql(u8, self.source[start .. start + 7], "#import") and
(start + 7 >= self.source.len or !isIdentContinue(self.source[start + 7])))
{
self.index = start + 7;
return self.makeToken(.hash_import, start, self.index);
}
if (self.source.len >= start + 7 and std.mem.eql(u8, self.source[start .. start + 7], "#insert") and
(start + 7 >= self.source.len or !isIdentContinue(self.source[start + 7])))
{
self.index = start + 7;
return self.makeToken(.hash_insert, start, self.index);
}
if (self.source.len >= start + 4 and std.mem.eql(u8, self.source[start .. start + 4], "#run") and
(start + 4 >= self.source.len or !isIdentContinue(self.source[start + 4])))
{
self.index = start + 4;
return self.makeToken(.hash_run, start, self.index);
}
if (self.source.len >= start + 8 and std.mem.eql(u8, self.source[start .. start + 8], "#builtin") and
(start + 8 >= self.source.len or !isIdentContinue(self.source[start + 8])))
{
self.index = start + 8;
return self.makeToken(.hash_builtin, start, self.index);
}
self.index += 1;
return self.makeToken(.invalid, start, self.index);
}
// Punctuation and operators
self.index += 1;
switch (c) {
';' => return self.makeToken(.semicolon, start, self.index),
',' => return self.makeToken(.comma, start, self.index),
'(' => return self.makeToken(.l_paren, start, self.index),
')' => return self.makeToken(.r_paren, start, self.index),
'{' => return self.makeToken(.l_brace, start, self.index),
'}' => return self.makeToken(.r_brace, start, self.index),
'[' => return self.makeToken(.l_bracket, start, self.index),
']' => return self.makeToken(.r_bracket, start, self.index),
'.' => {
if (self.peek() == '.') {
self.index += 1;
return self.makeToken(.dot_dot, start, self.index);
}
return self.makeToken(.dot, start, self.index);
},
'$' => return self.makeToken(.dollar, start, self.index),
':' => {
if (self.peek() == ':') {
self.index += 1;
return self.makeToken(.colon_colon, start, self.index);
}
if (self.peek() == '=') {
self.index += 1;
return self.makeToken(.colon_equal, start, self.index);
}
return self.makeToken(.colon, start, self.index);
},
'=' => {
if (self.peek() == '=') {
self.index += 1;
return self.makeToken(.equal_equal, start, self.index);
}
if (self.peek() == '>') {
self.index += 1;
return self.makeToken(.fat_arrow, start, self.index);
}
return self.makeToken(.equal, start, self.index);
},
'+' => {
if (self.peek() == '=') {
self.index += 1;
return self.makeToken(.plus_equal, start, self.index);
}
return self.makeToken(.plus, start, self.index);
},
'-' => {
if (self.peek() == '-' and (self.index + 1) < self.source.len and self.source[self.index + 1] == '-') {
self.index += 2;
return self.makeToken(.triple_minus, start, self.index);
}
if (self.peek() == '>') {
self.index += 1;
return self.makeToken(.arrow, start, self.index);
}
if (self.peek() == '=') {
self.index += 1;
return self.makeToken(.minus_equal, start, self.index);
}
return self.makeToken(.minus, start, self.index);
},
'*' => {
if (self.peek() == '=') {
self.index += 1;
return self.makeToken(.star_equal, start, self.index);
}
return self.makeToken(.star, start, self.index);
},
'/' => {
if (self.peek() == '=') {
self.index += 1;
return self.makeToken(.slash_equal, start, self.index);
}
return self.makeToken(.slash, start, self.index);
},
'%' => {
if (self.peek() == '=') {
self.index += 1;
return self.makeToken(.percent_equal, start, self.index);
}
return self.makeToken(.percent, start, self.index);
},
'!' => {
if (self.peek() == '=') {
self.index += 1;
return self.makeToken(.bang_equal, start, self.index);
}
return self.makeToken(.bang, start, self.index);
},
'<' => {
if (self.peek() == '=') {
self.index += 1;
return self.makeToken(.less_equal, start, self.index);
}
return self.makeToken(.less, start, self.index);
},
'>' => {
if (self.peek() == '=') {
self.index += 1;
return self.makeToken(.greater_equal, start, self.index);
}
return self.makeToken(.greater, start, self.index);
},
else => return self.makeToken(.invalid, start, self.index),
}
}
fn lexNumber(self: *Lexer, start: u32) Token {
// Advance past the initial digit that was already matched
self.index += 1;
// Check for hex (0x/0X) or binary (0b/0B) prefix
if (self.source[start] == '0' and self.index < self.source.len) {
const prefix = self.source[self.index];
if (prefix == 'x' or prefix == 'X') {
self.index += 1; // skip 'x'/'X'
while (self.index < self.source.len and isHexDigit(self.source[self.index])) {
self.index += 1;
}
return self.makeToken(.int_literal, start, self.index);
}
if (prefix == 'b' or prefix == 'B') {
self.index += 1; // skip 'b'/'B'
while (self.index < self.source.len and (self.source[self.index] == '0' or self.source[self.index] == '1')) {
self.index += 1;
}
return self.makeToken(.int_literal, start, self.index);
}
}
while (self.index < self.source.len and isDigit(self.source[self.index])) {
self.index += 1;
}
// Check for float
if (self.index < self.source.len and self.source[self.index] == '.') {
// Look ahead: must be followed by a digit (not `.identifier`)
if (self.index + 1 < self.source.len and isDigit(self.source[self.index + 1])) {
self.index += 1; // skip '.'
while (self.index < self.source.len and isDigit(self.source[self.index])) {
self.index += 1;
}
return self.makeToken(.float_literal, start, self.index);
}
}
return self.makeToken(.int_literal, start, self.index);
}
fn lexIdentifier(self: *Lexer, start: u32) Token {
while (self.index < self.source.len and isIdentContinue(self.source[self.index])) {
self.index += 1;
}
const text = self.source[start..self.index];
if (getKeyword(text)) |kw| {
return self.makeToken(kw, start, self.index);
}
return self.makeToken(.identifier, start, self.index);
}
fn lexString(self: *Lexer, start: u32) Token {
self.index += 1; // skip opening "
while (self.index < self.source.len) {
const ch = self.source[self.index];
if (ch == '"') {
self.index += 1;
return self.makeToken(.string_literal, start, self.index);
}
if (ch == '\\') {
self.index += 1; // skip escape
}
self.index += 1;
}
// Unterminated string
return self.makeToken(.invalid, start, self.index);
}
fn peek(self: *const Lexer) u8 {
if (self.index < self.source.len) {
return self.source[self.index];
}
return 0;
}
fn makeToken(_: *const Lexer, tag: Tag, start: u32, end: u32) Token {
return .{ .tag = tag, .loc = .{ .start = start, .end = end } };
}
fn isDigit(c: u8) bool {
return c >= '0' and c <= '9';
}
fn isIdentStart(c: u8) bool {
return (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or c == '_';
}
fn isHexDigit(c: u8) bool {
return isDigit(c) or (c >= 'a' and c <= 'f') or (c >= 'A' and c <= 'F');
}
fn isIdentContinue(c: u8) bool {
return isIdentStart(c) or isDigit(c);
}
};
test "lex minimal main" {
var lex = Lexer.init("main :: () { 42; }");
const expected = [_]Tag{ .identifier, .colon_colon, .l_paren, .r_paren, .l_brace, .int_literal, .semicolon, .r_brace, .eof };
for (expected) |exp| {
const tok = lex.next();
try std.testing.expectEqual(exp, tok.tag);
}
}
test "lex with comments" {
var lex = Lexer.init("// comment\nmain :: () { 0; }");
try std.testing.expectEqual(Tag.identifier, lex.next().tag);
try std.testing.expectEqual(Tag.colon_colon, lex.next().tag);
}
test "lex operators" {
var lex = Lexer.init(":= : :: += -= *= /= -> => == != <= >=");
const expected = [_]Tag{
.colon_equal, .colon, .colon_colon, .plus_equal, .minus_equal,
.star_equal, .slash_equal, .arrow, .fat_arrow, .equal_equal,
.bang_equal, .less_equal, .greater_equal,
};
for (expected) |exp| {
try std.testing.expectEqual(exp, lex.next().tag);
}
}
test "lex float" {
var lex = Lexer.init("0.3 42 0.9");
try std.testing.expectEqual(Tag.float_literal, lex.next().tag);
try std.testing.expectEqual(Tag.int_literal, lex.next().tag);
try std.testing.expectEqual(Tag.float_literal, lex.next().tag);
}
test "lex keywords" {
var lex = Lexer.init("if else then true false enum case break return f32 f64 struct");
const expected = [_]Tag{
.kw_if, .kw_else, .kw_then, .kw_true, .kw_false,
.kw_enum, .kw_case, .kw_break, .kw_return, .kw_f32, .kw_f64, .kw_struct,
};
for (expected) |exp| {
try std.testing.expectEqual(exp, lex.next().tag);
}
}
test "lex type-like identifiers" {
// s32, u8, bool, string are identifiers, not keywords
var lex = Lexer.init("s32 u8 bool string");
for (0..4) |_| {
try std.testing.expectEqual(Tag.identifier, lex.next().tag);
}
}
test "lex hash_run" {
var lex = Lexer.init("#run");
try std.testing.expectEqual(Tag.hash_run, lex.next().tag);
try std.testing.expectEqual(Tag.eof, lex.next().tag);
// #run followed by identifier
var lex2 = Lexer.init("#run compute(5)");
try std.testing.expectEqual(Tag.hash_run, lex2.next().tag);
try std.testing.expectEqual(Tag.identifier, lex2.next().tag);
// #running should not match (identContinue after "run")
var lex3 = Lexer.init("#running");
try std.testing.expectEqual(Tag.invalid, lex3.next().tag);
}
test "lex hash_import" {
var lex = Lexer.init("#import \"foo.sx\"");
try std.testing.expectEqual(Tag.hash_import, lex.next().tag);
try std.testing.expectEqual(Tag.string_literal, lex.next().tag);
try std.testing.expectEqual(Tag.eof, lex.next().tag);
// #importing should not match
var lex2 = Lexer.init("#importing");
try std.testing.expectEqual(Tag.invalid, lex2.next().tag);
}
test "lex hash_insert" {
var lex = Lexer.init("#insert #run generate()");
try std.testing.expectEqual(Tag.hash_insert, lex.next().tag);
try std.testing.expectEqual(Tag.hash_run, lex.next().tag);
try std.testing.expectEqual(Tag.identifier, lex.next().tag);
// #inserting should not match
var lex2 = Lexer.init("#inserting");
try std.testing.expectEqual(Tag.invalid, lex2.next().tag);
}
test "lex string" {
var lex = Lexer.init("\"Hello\"");
const tok = lex.next();
try std.testing.expectEqual(Tag.string_literal, tok.tag);
try std.testing.expectEqualStrings("\"Hello\"", tok.slice("\"Hello\""));
}
test "lex hex literal" {
var lex = Lexer.init("0xFF 0X1A");
const tok1 = lex.next();
try std.testing.expectEqual(Tag.int_literal, tok1.tag);
try std.testing.expectEqualStrings("0xFF", tok1.slice("0xFF 0X1A"));
const tok2 = lex.next();
try std.testing.expectEqual(Tag.int_literal, tok2.tag);
try std.testing.expectEqualStrings("0X1A", tok2.slice("0xFF 0X1A"));
}
test "lex binary literal" {
var lex = Lexer.init("0b1010 0B110");
const tok1 = lex.next();
try std.testing.expectEqual(Tag.int_literal, tok1.tag);
try std.testing.expectEqualStrings("0b1010", tok1.slice("0b1010 0B110"));
const tok2 = lex.next();
try std.testing.expectEqual(Tag.int_literal, tok2.tag);
try std.testing.expectEqualStrings("0B110", tok2.slice("0b1010 0B110"));
}

54
src/llvm_api.zig Normal file
View File

@@ -0,0 +1,54 @@
pub const c = @cImport({
@cInclude("llvm-c/Core.h");
@cInclude("llvm-c/Analysis.h");
@cInclude("llvm-c/BitWriter.h");
@cInclude("llvm-c/Target.h");
@cInclude("llvm-c/TargetMachine.h");
@cInclude("llvm-c/LLJIT.h");
@cInclude("llvm-c/Orc.h");
@cInclude("llvm-c/Error.h");
});
extern fn sx_llvm_init_all_targets() void;
extern fn sx_llvm_init_native_target() void;
pub fn initAllTargets() void {
sx_llvm_init_all_targets();
}
pub fn initNativeTarget() void {
sx_llvm_init_native_target();
}
// Type aliases for ergonomics
pub const Context = c.LLVMContextRef;
pub const Module = c.LLVMModuleRef;
pub const Builder = c.LLVMBuilderRef;
pub const Value = c.LLVMValueRef;
pub const Type = c.LLVMTypeRef;
pub const BasicBlock = c.LLVMBasicBlockRef;
pub const TargetMachine = c.LLVMTargetMachineRef;
pub fn createContext() Context {
return c.LLVMContextCreate();
}
pub fn disposeContext(ctx: Context) void {
c.LLVMContextDispose(ctx);
}
pub fn moduleCreateWithName(name: [*:0]const u8) Module {
return c.LLVMModuleCreateWithNameInContext(name, c.LLVMGetGlobalContext());
}
pub fn disposeModule(module: Module) void {
c.LLVMDisposeModule(module);
}
pub fn createBuilderInContext(ctx: Context) Builder {
return c.LLVMCreateBuilderInContext(ctx);
}
pub fn disposeBuilder(builder: Builder) void {
c.LLVMDisposeBuilder(builder);
}

48
src/lsp/document.zig Normal file
View File

@@ -0,0 +1,48 @@
const std = @import("std");
pub const DocumentStore = struct {
documents: std.StringHashMap(Document),
allocator: std.mem.Allocator,
pub const Document = struct {
uri: []const u8,
text: []const u8,
version: i64,
};
pub fn init(allocator: std.mem.Allocator) DocumentStore {
return .{
.documents = std.StringHashMap(Document).init(allocator),
.allocator = allocator,
};
}
pub fn open(self: *DocumentStore, uri: []const u8, text: []const u8, version: i64) !void {
const uri_copy = try self.allocator.dupe(u8, uri);
const text_copy = try self.allocator.dupe(u8, text);
try self.documents.put(uri_copy, .{
.uri = uri_copy,
.text = text_copy,
.version = version,
});
}
pub fn update(self: *DocumentStore, uri: []const u8, text: []const u8, version: i64) !void {
if (self.documents.getPtr(uri)) |doc| {
self.allocator.free(doc.text);
doc.text = try self.allocator.dupe(u8, text);
doc.version = version;
}
}
pub fn close(self: *DocumentStore, uri: []const u8) void {
if (self.documents.fetchRemove(uri)) |kv| {
self.allocator.free(kv.value.text);
self.allocator.free(kv.key);
}
}
pub fn get(self: *const DocumentStore, uri: []const u8) ?*const Document {
return self.documents.getPtr(uri);
}
};

1776
src/lsp/server.zig Normal file

File diff suppressed because it is too large Load Diff

75
src/lsp/transport.zig Normal file
View File

@@ -0,0 +1,75 @@
const std = @import("std");
pub const Transport = struct {
in: *std.Io.Reader,
out_file: std.Io.File,
io: std.Io,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator, io: std.Io, in: *std.Io.Reader, out_file: std.Io.File) Transport {
return .{
.in = in,
.out_file = out_file,
.io = io,
.allocator = allocator,
};
}
/// Read one LSP message: parse Content-Length header, read body.
pub fn readMessage(self: *Transport) ![]const u8 {
var content_length: ?usize = null;
// Parse headers (terminated by \r\n\r\n)
while (true) {
const line = try self.readLine();
if (line.len == 0) break; // empty line = end of headers
if (std.mem.startsWith(u8, line, "Content-Length: ")) {
content_length = std.fmt.parseInt(usize, line["Content-Length: ".len..], 10) catch
return error.InvalidContentLength;
}
}
const len = content_length orelse return error.MissingContentLength;
const body = try self.allocator.alloc(u8, len);
try self.in.readSliceAll(body);
return body;
}
/// Write one LSP message: Content-Length header + body.
pub fn writeMessage(self: *Transport, body: []const u8) !void {
var buf: [32]u8 = undefined;
const len_str = std.fmt.bufPrint(&buf, "{d}", .{body.len}) catch unreachable;
self.out_file.writeStreamingAll(self.io, "Content-Length: ") catch return error.WriteFailed;
self.out_file.writeStreamingAll(self.io, len_str) catch return error.WriteFailed;
self.out_file.writeStreamingAll(self.io, "\r\n\r\n") catch return error.WriteFailed;
self.out_file.writeStreamingAll(self.io, body) catch return error.WriteFailed;
}
/// Read a single line terminated by \r\n. Returns content without \r\n.
fn readLine(self: *Transport) ![]const u8 {
var buf = std.ArrayList(u8).empty;
while (true) {
const byte = self.in.takeByte() catch |err| switch (err) {
error.EndOfStream => {
if (buf.items.len == 0) return error.EndOfStream;
return buf.items;
},
else => return error.ReadFailed,
};
if (byte == '\n') {
const line = buf.items;
if (line.len > 0 and line[line.len - 1] == '\r') {
return line[0 .. line.len - 1];
}
return line;
}
try buf.append(self.allocator, byte);
}
}
};

331
src/lsp/types.zig Normal file
View File

@@ -0,0 +1,331 @@
const std = @import("std");
pub const Position = struct {
line: u32,
character: u32,
};
pub const Range = struct {
start: Position,
end: Position,
};
pub const Location = struct {
uri: []const u8,
range: Range,
};
pub const Diagnostic = struct {
range: Range,
severity: u32,
message: []const u8,
source: []const u8 = "sx",
};
/// Build a JSON-RPC response with a pre-built result JSON string.
pub fn jsonRpcResponse(allocator: std.mem.Allocator, id_json: []const u8, result_json: []const u8) ![]const u8 {
return std.fmt.allocPrint(allocator, "{{\"jsonrpc\":\"2.0\",\"id\":{s},\"result\":{s}}}", .{ id_json, result_json });
}
/// Build a JSON-RPC notification.
pub fn jsonRpcNotification(allocator: std.mem.Allocator, method: []const u8, params_json: []const u8) ![]const u8 {
return std.fmt.allocPrint(allocator, "{{\"jsonrpc\":\"2.0\",\"method\":\"{s}\",\"params\":{s}}}", .{ method, params_json });
}
/// Serialize a JSON Value to string.
pub fn valueToJson(allocator: std.mem.Allocator, value: std.json.Value) ![]const u8 {
var buf = std.ArrayList(u8).empty;
try writeJsonValue(&buf, allocator, value);
return buf.items;
}
/// Escape a string for JSON.
pub fn jsonString(allocator: std.mem.Allocator, s: []const u8) ![]const u8 {
var buf = std.ArrayList(u8).empty;
try buf.append(allocator, '"');
for (s) |ch| {
switch (ch) {
'"' => try buf.appendSlice(allocator, "\\\""),
'\\' => try buf.appendSlice(allocator, "\\\\"),
'\n' => try buf.appendSlice(allocator, "\\n"),
'\r' => try buf.appendSlice(allocator, "\\r"),
'\t' => try buf.appendSlice(allocator, "\\t"),
else => try buf.append(allocator, ch),
}
}
try buf.append(allocator, '"');
return buf.items;
}
fn writeJsonValue(buf: *std.ArrayList(u8), allocator: std.mem.Allocator, value: std.json.Value) !void {
switch (value) {
.null => try buf.appendSlice(allocator, "null"),
.bool => |b| try buf.appendSlice(allocator, if (b) "true" else "false"),
.integer => |i| {
const s = try std.fmt.allocPrint(allocator, "{d}", .{i});
try buf.appendSlice(allocator, s);
},
.float => |f| {
const s = try std.fmt.allocPrint(allocator, "{d}", .{f});
try buf.appendSlice(allocator, s);
},
.string => |s| {
const escaped = try jsonString(allocator, s);
try buf.appendSlice(allocator, escaped);
},
.array => |arr| {
try buf.append(allocator, '[');
for (arr.items, 0..) |item, idx| {
if (idx > 0) try buf.append(allocator, ',');
try writeJsonValue(buf, allocator, item);
}
try buf.append(allocator, ']');
},
.object => |obj| {
try buf.append(allocator, '{');
var first = true;
var it = obj.iterator();
while (it.next()) |entry| {
if (!first) try buf.append(allocator, ',');
first = false;
const key = try jsonString(allocator, entry.key_ptr.*);
try buf.appendSlice(allocator, key);
try buf.append(allocator, ':');
try writeJsonValue(buf, allocator, entry.value_ptr.*);
}
try buf.append(allocator, '}');
},
.number_string => |s| try buf.appendSlice(allocator, s),
}
}
/// Build the initialize result JSON.
pub fn initializeResultJson(allocator: std.mem.Allocator) ![]const u8 {
return std.fmt.allocPrint(allocator,
"{{\"capabilities\":{{\"textDocumentSync\":1,\"definitionProvider\":true,\"hoverProvider\":true,\"documentSymbolProvider\":true," ++
"\"completionProvider\":{{\"triggerCharacters\":[\".\"]}}," ++
"\"signatureHelpProvider\":{{\"triggerCharacters\":[\"(\",\",\"]}}," ++
"\"semanticTokensProvider\":{{\"legend\":{{" ++
"\"tokenTypes\":[\"namespace\",\"type\",\"enum\",\"struct\",\"parameter\",\"variable\",\"enumMember\",\"function\",\"keyword\",\"number\",\"string\",\"operator\"]," ++
"\"tokenModifiers\":[\"declaration\",\"readonly\"]" ++
"}},\"full\":true}}}}}}",
.{},
);
}
/// LSP SymbolKind enum values.
pub const SymbolKindLsp = enum(u32) {
File = 1,
Module = 2,
Namespace = 3,
Package = 4,
Class = 5,
Method = 6,
Property = 7,
Field = 8,
Constructor = 9,
Enum = 10,
Interface = 11,
Function = 12,
Variable = 13,
Constant = 14,
String = 15,
Number = 16,
Boolean = 17,
Array = 18,
Object = 19,
Key = 20,
Null = 21,
EnumMember = 22,
Struct = 23,
Event = 24,
Operator = 25,
TypeParameter = 26,
};
/// LSP CompletionItemKind enum values.
pub const CompletionItemKind = enum(u32) {
Text = 1,
Method = 2,
Function = 3,
Constructor = 4,
Field = 5,
Variable = 6,
Class = 7,
Interface = 8,
Module = 9,
Property = 10,
Unit = 11,
Value = 12,
Enum = 13,
Keyword = 14,
Snippet = 15,
Color = 16,
File = 17,
Reference = 18,
Folder = 19,
EnumMember = 20,
Constant = 21,
Struct = 22,
Event = 23,
Operator = 24,
TypeParameter = 25,
};
/// Build document symbols JSON array.
pub fn documentSymbolsJson(allocator: std.mem.Allocator, symbols: []const DocumentSymbol) ![]const u8 {
var buf = std.ArrayList(u8).empty;
try buf.append(allocator, '[');
for (symbols, 0..) |sym, idx| {
if (idx > 0) try buf.append(allocator, ',');
const name_escaped = try jsonString(allocator, sym.name);
const item = try std.fmt.allocPrint(allocator,
"{{\"name\":{s},\"kind\":{d},\"range\":{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}},\"selectionRange\":{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}}}}",
.{
name_escaped, sym.kind,
sym.range.start.line, sym.range.start.character,
sym.range.end.line, sym.range.end.character,
sym.selection_range.start.line, sym.selection_range.start.character,
sym.selection_range.end.line, sym.selection_range.end.character,
},
);
try buf.appendSlice(allocator, item);
}
try buf.append(allocator, ']');
return buf.items;
}
pub const DocumentSymbol = struct {
name: []const u8,
kind: u32,
range: Range,
selection_range: Range,
};
/// Build completion items JSON array.
pub fn completionItemsJson(allocator: std.mem.Allocator, items: []const CompletionItem) ![]const u8 {
var buf = std.ArrayList(u8).empty;
try buf.append(allocator, '[');
for (items, 0..) |item, idx| {
if (idx > 0) try buf.append(allocator, ',');
const label_escaped = try jsonString(allocator, item.label);
const detail_escaped = if (item.detail) |d| try jsonString(allocator, d) else null;
if (detail_escaped) |de| {
const json = try std.fmt.allocPrint(allocator,
"{{\"label\":{s},\"kind\":{d},\"detail\":{s}}}",
.{ label_escaped, item.kind, de },
);
try buf.appendSlice(allocator, json);
} else {
const json = try std.fmt.allocPrint(allocator,
"{{\"label\":{s},\"kind\":{d}}}",
.{ label_escaped, item.kind },
);
try buf.appendSlice(allocator, json);
}
}
try buf.append(allocator, ']');
return buf.items;
}
pub const CompletionItem = struct {
label: []const u8,
kind: u32,
detail: ?[]const u8 = null,
};
/// Build a Location JSON response (for go-to-definition).
pub fn locationJson(allocator: std.mem.Allocator, uri: []const u8, range: Range) ![]const u8 {
const uri_escaped = try jsonString(allocator, uri);
return std.fmt.allocPrint(allocator,
"{{\"uri\":{s},\"range\":{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}}}}",
.{ uri_escaped, range.start.line, range.start.character, range.end.line, range.end.character },
);
}
/// Build a Hover JSON response.
pub fn hoverJson(allocator: std.mem.Allocator, contents: []const u8) ![]const u8 {
const escaped = try jsonString(allocator, contents);
return std.fmt.allocPrint(allocator,
"{{\"contents\":{{\"kind\":\"markdown\",\"value\":{s}}}}}",
.{escaped},
);
}
/// Build a SignatureHelp JSON response.
pub fn signatureHelpJson(allocator: std.mem.Allocator, label: []const u8, param_labels: []const []const u8, active_param: u32) ![]const u8 {
var buf = std.ArrayList(u8).empty;
const label_escaped = try jsonString(allocator, label);
try buf.appendSlice(allocator, "{\"signatures\":[{\"label\":");
try buf.appendSlice(allocator, label_escaped);
try buf.appendSlice(allocator, ",\"parameters\":[");
for (param_labels, 0..) |pl, idx| {
if (idx > 0) try buf.append(allocator, ',');
const pl_escaped = try jsonString(allocator, pl);
try buf.appendSlice(allocator, "{\"label\":");
try buf.appendSlice(allocator, pl_escaped);
try buf.append(allocator, '}');
}
const ap_str = try std.fmt.allocPrint(allocator, "{d}", .{active_param});
try buf.appendSlice(allocator, "]}],\"activeSignature\":0,\"activeParameter\":");
try buf.appendSlice(allocator, ap_str);
try buf.append(allocator, '}');
return buf.items;
}
/// Semantic token type indices (must match legend in initializeResultJson).
pub const SemanticTokenType = struct {
pub const namespace: u32 = 0;
pub const type_: u32 = 1;
pub const enum_: u32 = 2;
pub const struct_: u32 = 3;
pub const parameter: u32 = 4;
pub const variable: u32 = 5;
pub const enum_member: u32 = 6;
pub const function: u32 = 7;
pub const keyword: u32 = 8;
pub const number: u32 = 9;
pub const string_: u32 = 10;
pub const operator_: u32 = 11;
};
/// Build a SemanticTokens JSON response.
pub fn semanticTokensJson(allocator: std.mem.Allocator, data: []const u32) ![]const u8 {
var buf = std.ArrayList(u8).empty;
try buf.appendSlice(allocator, "{\"data\":[");
for (data, 0..) |val, idx| {
if (idx > 0) try buf.append(allocator, ',');
const s = try std.fmt.allocPrint(allocator, "{d}", .{val});
try buf.appendSlice(allocator, s);
}
try buf.appendSlice(allocator, "]}");
return buf.items;
}
/// Build publishDiagnostics params JSON.
pub fn publishDiagnosticsJson(allocator: std.mem.Allocator, uri: []const u8, diagnostics: []const Diagnostic) ![]const u8 {
var buf = std.ArrayList(u8).empty;
const uri_escaped = try jsonString(allocator, uri);
try buf.appendSlice(allocator, "{\"uri\":");
try buf.appendSlice(allocator, uri_escaped);
try buf.appendSlice(allocator, ",\"diagnostics\":[");
for (diagnostics, 0..) |d, idx| {
if (idx > 0) try buf.append(allocator, ',');
const msg_escaped = try jsonString(allocator, d.message);
const src_escaped = try jsonString(allocator, d.source);
const diag_json = try std.fmt.allocPrint(allocator,
"{{\"range\":{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}},\"severity\":{d},\"message\":{s},\"source\":{s}}}",
.{ d.range.start.line, d.range.start.character, d.range.end.line, d.range.end.character, d.severity, msg_escaped, src_escaped },
);
try buf.appendSlice(allocator, diag_json);
}
try buf.appendSlice(allocator, "]}");
return buf.items;
}

158
src/main.zig Normal file
View File

@@ -0,0 +1,158 @@
const std = @import("std");
const sx = @import("sx");
pub fn main(init: std.process.Init) !void {
const allocator = init.arena.allocator();
const io = init.io;
const args = try init.minimal.args.toSlice(allocator);
if (args.len < 2) {
printUsage();
return;
}
const command = args[1];
// LSP subcommand doesn't need a file argument
if (std.mem.eql(u8, command, "lsp")) {
runLsp(allocator, io);
return;
}
if (args.len < 3) {
printUsage();
return;
}
const input_path = args[2];
if (std.mem.eql(u8, command, "build")) {
const output_name = deriveOutputName(input_path);
compile(allocator, io, input_path, output_name) catch return;
std.debug.print("compiled: {s}\n", .{output_name});
} else if (std.mem.eql(u8, command, "ir")) {
emitIR(allocator, io, input_path) catch return;
} else if (std.mem.eql(u8, command, "run")) {
const tmp_bin = "/tmp/sx_run_tmp";
compile(allocator, io, input_path, tmp_bin) catch return;
defer {
std.Io.Dir.deleteFile(.cwd(), io, tmp_bin) catch {};
}
var child = std.process.spawn(io, .{
.argv = &.{tmp_bin},
}) catch {
std.debug.print("error: failed to run program\n", .{});
return;
};
_ = child.wait(io) catch {
std.debug.print("error: program execution failed\n", .{});
return;
};
} else {
printUsage();
}
}
fn printUsage() void {
std.debug.print(
\\Usage: sx <command> [file.sx]
\\
\\Commands:
\\ run Build and run immediately
\\ build Build binary in current directory
\\ ir Print LLVM IR to stdout
\\ lsp Start language server (LSP)
\\
, .{});
}
fn runLsp(allocator: std.mem.Allocator, io: std.Io) void {
const Transport = sx.lsp.transport.Transport;
const Server = sx.lsp.server.Server;
const stdin_file = std.Io.File.stdin();
const stdout_file = std.Io.File.stdout();
var read_buf: [4096]u8 = undefined;
var stdin_reader = stdin_file.readerStreaming(io, &read_buf);
var transport = Transport.init(allocator, io, &stdin_reader.interface, stdout_file);
var server = Server.init(allocator, &transport, io);
while (true) {
const msg = transport.readMessage() catch |err| {
if (err == error.EndOfStream) break;
std.debug.print("lsp: read error: {}\n", .{err});
break;
};
const keep_going = server.handleMessage(msg);
if (!keep_going) break;
}
}
fn deriveOutputName(input_path: []const u8) []const u8 {
// Get basename (strip directory)
var start: usize = 0;
for (input_path, 0..) |ch, i| {
if (ch == '/') start = i + 1;
}
const basename = input_path[start..];
// Strip .sx extension
if (std.mem.endsWith(u8, basename, ".sx")) {
return basename[0 .. basename.len - 3];
}
return basename;
}
fn readSource(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8) ![:0]const u8 {
const source_bytes = std.Io.Dir.readFileAlloc(.cwd(), io, input_path, allocator, .limited(10 * 1024 * 1024)) catch |err| {
std.debug.print("error: cannot read '{s}': {}\n", .{ input_path, err });
return error.CompileError;
};
return try allocator.dupeZ(u8, source_bytes);
}
fn emitIR(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8) !void {
const source = try readSource(allocator, io, input_path);
var comp = sx.core.Compilation.init(allocator, io, input_path, source);
defer comp.deinit();
comp.parse() catch { comp.renderErrors(); return error.CompileError; };
comp.resolveImports() catch { comp.renderErrors(); return error.CompileError; };
comp.generateCode() catch { comp.renderErrors(); return error.CompileError; };
var cg = &comp.cg.?;
cg.verify() catch { comp.renderErrors(); return error.CompileError; };
cg.printIR();
}
fn compile(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8) !void {
const source = try readSource(allocator, io, input_path);
var comp = sx.core.Compilation.init(allocator, io, input_path, source);
defer comp.deinit();
comp.parse() catch { comp.renderErrors(); return error.CompileError; };
comp.resolveImports() catch { comp.renderErrors(); return error.CompileError; };
comp.generateCode() catch { comp.renderErrors(); return error.CompileError; };
var cg = &comp.cg.?;
cg.verify() catch { comp.renderErrors(); return error.CompileError; };
// Emit object file
const obj_path = try std.fmt.allocPrintSentinel(allocator, "{s}.o", .{output_path}, 0);
cg.emitObject(obj_path.ptr) catch { comp.renderErrors(); return error.CompileError; };
// Link
sx.codegen.CodeGen.link(io, obj_path, output_path) catch {
std.debug.print("error: linking failed\n", .{});
return error.CompileError;
};
// Clean up object file
std.Io.Dir.deleteFile(.cwd(), io, obj_path) catch {};
}

1573
src/parser.zig Normal file

File diff suppressed because it is too large Load Diff

19
src/root.zig Normal file
View File

@@ -0,0 +1,19 @@
pub const llvm_api = @import("llvm_api.zig");
pub const token = @import("token.zig");
pub const lexer = @import("lexer.zig");
pub const ast = @import("ast.zig");
pub const parser = @import("parser.zig");
pub const types = @import("types.zig");
pub const codegen = @import("codegen.zig");
pub const builtins = @import("builtins.zig");
pub const errors = @import("errors.zig");
pub const sema = @import("sema.zig");
pub const imports = @import("imports.zig");
pub const core = @import("core.zig");
pub const lsp = struct {
pub const server = @import("lsp/server.zig");
pub const transport = @import("lsp/transport.zig");
pub const types = @import("lsp/types.zig");
pub const document = @import("lsp/document.zig");
};

1006
src/sema.zig Normal file

File diff suppressed because it is too large Load Diff

175
src/token.zig Normal file
View File

@@ -0,0 +1,175 @@
pub const Tag = enum {
// Literals
int_literal,
float_literal,
string_literal,
// Identifiers and keywords
identifier,
kw_if,
kw_else,
kw_then,
kw_true,
kw_false,
kw_enum,
kw_case,
kw_break,
kw_continue,
kw_while,
kw_for,
kw_return,
kw_defer,
kw_f32,
kw_f64,
kw_struct,
kw_union,
kw_xx,
kw_and,
kw_or,
kw_Type, // Type (metatype keyword)
// Symbols
colon, // :
colon_colon, // ::
colon_equal, // :=
semicolon, // ;
comma, // ,
dot, // .
dot_dot, // ..
dollar, // $
// Operators
plus, // +
minus, // -
star, // *
slash, // /
equal, // =
equal_equal, // ==
bang, // !
bang_equal, // !=
less, // <
less_equal, // <=
greater, // >
greater_equal, // >=
plus_equal, // +=
minus_equal, // -=
star_equal, // *=
slash_equal, // /=
percent, // %
percent_equal, // %=
// Delimiters
l_paren, // (
r_paren, // )
l_brace, // {
r_brace, // }
l_bracket, // [
r_bracket, // ]
// Arrows
arrow, // ->
fat_arrow, // =>
// Directives
hash_run, // #run
hash_import, // #import
hash_insert, // #insert
hash_builtin, // #builtin
triple_minus, // ---
// Special
eof,
invalid,
pub fn lexeme(tag: Tag) ?[]const u8 {
return switch (tag) {
.colon => ":",
.colon_colon => "::",
.colon_equal => ":=",
.semicolon => ";",
.comma => ",",
.dot => ".",
.dot_dot => "..",
.dollar => "$",
.plus => "+",
.minus => "-",
.star => "*",
.slash => "/",
.equal => "=",
.equal_equal => "==",
.bang => "!",
.bang_equal => "!=",
.less => "<",
.less_equal => "<=",
.greater => ">",
.greater_equal => ">=",
.plus_equal => "+=",
.minus_equal => "-=",
.star_equal => "*=",
.slash_equal => "/=",
.percent => "%",
.percent_equal => "%=",
.l_paren => "(",
.r_paren => ")",
.l_brace => "{",
.r_brace => "}",
.l_bracket => "[",
.r_bracket => "]",
.arrow => "->",
.fat_arrow => "=>",
.triple_minus => "---",
else => null,
};
}
pub fn isTypeKeyword(tag: Tag) bool {
return switch (tag) {
.kw_f32, .kw_f64, .kw_Type => true,
else => false,
};
}
};
pub const Token = struct {
tag: Tag,
loc: Loc,
pub const Loc = struct {
start: u32,
end: u32,
};
pub fn slice(self: Token, source: []const u8) []const u8 {
return source[self.loc.start..self.loc.end];
}
};
pub const keywords = std.StaticStringMap(Tag).initComptime(.{
.{ "if", .kw_if },
.{ "else", .kw_else },
.{ "then", .kw_then },
.{ "true", .kw_true },
.{ "false", .kw_false },
.{ "enum", .kw_enum },
.{ "case", .kw_case },
.{ "break", .kw_break },
.{ "continue", .kw_continue },
.{ "while", .kw_while },
.{ "for", .kw_for },
.{ "return", .kw_return },
.{ "defer", .kw_defer },
.{ "f32", .kw_f32 },
.{ "f64", .kw_f64 },
.{ "struct", .kw_struct },
.{ "union", .kw_union },
.{ "xx", .kw_xx },
.{ "and", .kw_and },
.{ "or", .kw_or },
.{ "Type", .kw_Type },
});
pub fn getKeyword(bytes: []const u8) ?Tag {
return keywords.get(bytes);
}
const std = @import("std");

323
src/types.zig Normal file
View File

@@ -0,0 +1,323 @@
const std = @import("std");
const ast = @import("ast.zig");
const Node = ast.Node;
pub const Type = union(enum) {
// Variable-width integers (164 bits)
signed: u8,
unsigned: u8,
// Fixed-width floats
f32,
f64,
// Other
void_type,
boolean,
string_type,
enum_type: []const u8,
struct_type: []const u8,
union_type: []const u8,
array_type: ArrayTypeInfo,
slice_type: SliceTypeInfo,
vector_type: VectorTypeInfo,
any_type,
meta_type: MetaTypeInfo,
pub const SliceTypeInfo = struct {
element_name: []const u8,
};
pub const ArrayTypeInfo = struct {
element_name: []const u8,
length: u32,
};
pub const VectorTypeInfo = struct {
element_name: []const u8,
length: u32,
};
pub const MetaTypeInfo = struct {
name: []const u8,
};
// Convenience constructors
pub fn s(width: u8) Type {
return .{ .signed = width };
}
pub fn u(width: u8) Type {
return .{ .unsigned = width };
}
pub fn fromName(name: []const u8) ?Type {
// Named types (check before variable-width integers since "string" starts with 's')
if (std.mem.eql(u8, name, "string")) return .string_type;
if (std.mem.eql(u8, name, "bool")) return .boolean;
if (std.mem.eql(u8, name, "f32")) return .f32;
if (std.mem.eql(u8, name, "f64")) return .f64;
if (std.mem.eql(u8, name, "Any")) return .any_type;
// Variable-width integers: s1..s64, u1..u64
if (name.len >= 2 and (name[0] == 's' or name[0] == 'u')) {
const width = std.fmt.parseInt(u8, name[1..], 10) catch return null;
if (width < 1 or width > 64) return null;
return if (name[0] == 's') Type.s(width) else Type.u(width);
}
return null;
}
pub fn fromTypeExpr(node: *Node) ?Type {
if (node.data != .type_expr) return null;
return fromName(node.data.type_expr.name);
}
pub fn isEnum(self: Type) bool {
return switch (self) {
.enum_type => true,
else => false,
};
}
pub fn isStruct(self: Type) bool {
return switch (self) {
.struct_type => true,
else => false,
};
}
pub fn isUnion(self: Type) bool {
return switch (self) {
.union_type => true,
else => false,
};
}
pub fn isAny(self: Type) bool {
return switch (self) {
.any_type => true,
else => false,
};
}
pub fn isSlice(self: Type) bool {
return switch (self) {
.slice_type => true,
else => false,
};
}
pub fn sliceElementType(self: Type) ?Type {
return switch (self) {
.slice_type => |info| fromName(info.element_name),
else => null,
};
}
pub fn isArray(self: Type) bool {
return switch (self) {
.array_type => true,
else => false,
};
}
pub fn isVector(self: Type) bool {
return switch (self) {
.vector_type => true,
else => false,
};
}
pub fn vectorElementType(self: Type) ?Type {
return switch (self) {
.vector_type => |info| fromName(info.element_name),
else => null,
};
}
pub fn isFloat(self: Type) bool {
return switch (self) {
.f32, .f64 => true,
else => false,
};
}
pub fn isInt(self: Type) bool {
return self.isSigned() or self.isUnsigned();
}
pub fn isSigned(self: Type) bool {
return switch (self) {
.signed => true,
else => false,
};
}
pub fn isUnsigned(self: Type) bool {
return switch (self) {
.unsigned => true,
else => false,
};
}
pub fn bitWidth(self: Type) u32 {
return switch (self) {
.signed => |w| w,
.unsigned => |w| w,
.f32 => 32,
.f64 => 64,
.boolean => 1,
else => 0,
};
}
/// Check if this type can be implicitly converted to `target` without `xx`.
/// Safe (implicit) conversions:
/// - Same type
/// - Both unsigned int, target width >= source width
/// - Both signed int, target width >= source width
/// - Unsigned to signed, target width strictly > source width
/// - Any int to any float
/// - Float to wider float (f32 → f64)
/// Everything else requires `xx`.
pub fn isImplicitlyConvertibleTo(self: Type, target: Type) bool {
if (std.meta.eql(self, target)) return true;
const src_float = self.isFloat();
const dst_float = target.isFloat();
const src_int = self.isInt();
// Float → wider float
if (src_float and dst_float) {
return target.bitWidth() >= self.bitWidth();
}
// Int → float (always safe)
if (src_int and dst_float) return true;
// Both unsigned → target width >= source width
if (self.isUnsigned() and target.isUnsigned()) {
return target.bitWidth() >= self.bitWidth();
}
// Both signed → target width >= source width
if (self.isSigned() and target.isSigned()) {
return target.bitWidth() >= self.bitWidth();
}
// Unsigned → signed: target must be strictly wider
if (self.isUnsigned() and target.isSigned()) {
return target.bitWidth() > self.bitWidth();
}
// Everything else requires xx
return false;
}
/// Format type name for mangling and display (e.g. "s32", "u8", "f64")
pub fn displayName(self: Type, allocator: std.mem.Allocator) ![]const u8 {
return switch (self) {
.signed => |w| {
var buf = std.ArrayList(u8).empty;
try buf.append(allocator, 's');
var tmp: [4]u8 = undefined;
const width_str = std.fmt.bufPrint(&tmp, "{d}", .{w}) catch unreachable;
try buf.appendSlice(allocator, width_str);
return try buf.toOwnedSlice(allocator);
},
.unsigned => |w| {
var buf = std.ArrayList(u8).empty;
try buf.append(allocator, 'u');
var tmp: [4]u8 = undefined;
const width_str = std.fmt.bufPrint(&tmp, "{d}", .{w}) catch unreachable;
try buf.appendSlice(allocator, width_str);
return try buf.toOwnedSlice(allocator);
},
.f32 => "f32",
.f64 => "f64",
.boolean => "bool",
.string_type => "string",
.void_type => "void",
.any_type => "Any",
.enum_type => |name| name,
.struct_type => |name| name,
.union_type => |name| name,
.slice_type => |info| {
var buf = std.ArrayList(u8).empty;
try buf.appendSlice(allocator, "[]");
try buf.appendSlice(allocator, info.element_name);
return try buf.toOwnedSlice(allocator);
},
.array_type => |info| {
var buf = std.ArrayList(u8).empty;
try buf.append(allocator, '[');
var tmp: [10]u8 = undefined;
const len_str = std.fmt.bufPrint(&tmp, "{d}", .{info.length}) catch unreachable;
try buf.appendSlice(allocator, len_str);
try buf.append(allocator, ']');
try buf.appendSlice(allocator, info.element_name);
return try buf.toOwnedSlice(allocator);
},
.vector_type => |info| {
var buf = std.ArrayList(u8).empty;
try buf.appendSlice(allocator, "Vector(");
var tmp: [10]u8 = undefined;
const len_str = std.fmt.bufPrint(&tmp, "{d}", .{info.length}) catch unreachable;
try buf.appendSlice(allocator, len_str);
try buf.appendSlice(allocator, ",");
try buf.appendSlice(allocator, info.element_name);
try buf.append(allocator, ')');
return try buf.toOwnedSlice(allocator);
},
.meta_type => |info| info.name,
};
}
/// Widen two types to a common type for binary operations.
/// Used for arithmetic type promotion (e.g., s16 + s32 → s32, int + float → float).
pub fn widen(a: Type, b: Type) Type {
// Same type → return it
if (std.meta.eql(a, b)) return a;
// Vector + vector of same dimensions → return a
if (a.isVector() and b.isVector()) return a;
// Vector + scalar → return vector (scalar will be broadcast)
if (a.isVector() and !b.isVector()) return a;
if (b.isVector() and !a.isVector()) return b;
const a_float = a.isFloat();
const b_float = b.isFloat();
const a_int = a.isInt();
const b_int = b.isInt();
// Both float → wider float
if (a_float and b_float) {
return if (a.bitWidth() >= b.bitWidth()) a else b;
}
// int + float → float
if (a_int and b_float) return b;
if (b_int and a_float) return a;
// Both signed → wider signed
if (a.isSigned() and b.isSigned()) {
return Type.s(@intCast(@max(a.bitWidth(), b.bitWidth())));
}
// Both unsigned → wider unsigned
if (a.isUnsigned() and b.isUnsigned()) {
return Type.u(@intCast(@max(a.bitWidth(), b.bitWidth())));
}
// signed + unsigned (mixed)
if (a_int and b_int) {
const aw = a.bitWidth();
const bw = b.bitWidth();
const max_w = @max(aw, bw);
// If same width, need one extra bit for sign; otherwise max is enough
const need: u32 = if (aw == bw) max_w + 1 else max_w;
const capped: u8 = @intCast(@min(need, 128));
return Type.s(capped);
}
return a;
}
};