so... jai :D
This commit is contained in:
326
src/ast.zig
Normal file
326
src/ast.zig
Normal 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
25
src/builtins.zig
Normal 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
4999
src/codegen.zig
Normal file
File diff suppressed because it is too large
Load Diff
1753
src/comptime.zig
Normal file
1753
src/comptime.zig
Normal file
File diff suppressed because it is too large
Load Diff
110
src/core.zig
Normal file
110
src/core.zig
Normal 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
96
src/errors.zig
Normal 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
150
src/imports.zig
Normal 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
403
src/lexer.zig
Normal 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
54
src/llvm_api.zig
Normal 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
48
src/lsp/document.zig
Normal 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
1776
src/lsp/server.zig
Normal file
File diff suppressed because it is too large
Load Diff
75
src/lsp/transport.zig
Normal file
75
src/lsp/transport.zig
Normal 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
331
src/lsp/types.zig
Normal 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
158
src/main.zig
Normal 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
1573
src/parser.zig
Normal file
File diff suppressed because it is too large
Load Diff
19
src/root.zig
Normal file
19
src/root.zig
Normal 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
1006
src/sema.zig
Normal file
File diff suppressed because it is too large
Load Diff
175
src/token.zig
Normal file
175
src/token.zig
Normal 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
323
src/types.zig
Normal 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 (1–64 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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user