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

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;
}
};