optionals

This commit is contained in:
agra
2026-02-22 22:16:30 +02:00
parent d3e574eae5
commit 1cc67f9b5a
17 changed files with 1952 additions and 32 deletions

View File

@@ -53,6 +53,9 @@ pub const Node = struct {
slice_expr: SliceExpr,
pointer_type_expr: PointerTypeExpr,
many_pointer_type_expr: ManyPointerTypeExpr,
optional_type_expr: OptionalTypeExpr,
force_unwrap: ForceUnwrap,
null_coalesce: NullCoalesce,
deref_expr: DerefExpr,
null_literal: void,
while_expr: WhileExpr,
@@ -192,6 +195,7 @@ pub const Call = struct {
pub const FieldAccess = struct {
object: *Node,
field: []const u8,
is_optional: bool = false,
};
pub const IfExpr = struct {
@@ -199,6 +203,7 @@ pub const IfExpr = struct {
then_branch: *Node,
else_branch: ?*Node,
is_inline: bool, // true for `if cond then a else b`
binding_name: ?[]const u8 = null, // for `if val := expr { ... }` optional binding
};
pub const MatchExpr = struct {
@@ -371,6 +376,19 @@ pub const ManyPointerTypeExpr = struct {
element_type: *Node,
};
pub const OptionalTypeExpr = struct {
inner_type: *Node,
};
pub const ForceUnwrap = struct {
operand: *Node,
};
pub const NullCoalesce = struct {
lhs: *Node,
rhs: *Node,
};
pub const DerefExpr = struct {
operand: *Node,
};
@@ -378,6 +396,7 @@ pub const DerefExpr = struct {
pub const WhileExpr = struct {
condition: *Node,
body: *Node,
binding_name: ?[]const u8 = null, // for `while val := expr { ... }` optional binding
};
pub const ForExpr = struct {

File diff suppressed because it is too large Load Diff

View File

@@ -268,6 +268,9 @@ pub const Instruction = union(enum) {
// Code insertion
eval_insert: InsertInfo, // pop string, parse as code, compile + execute inline
// Optionals
opt_unwrap, // pop value, error if null_val, else push back
// Unions
make_union: UnionMake,
get_union_field: UnionFieldAccess,
@@ -826,18 +829,43 @@ pub const Compiler = struct {
}
},
.if_expr => |ie| {
try self.compileNode(ie.condition);
const jump_false_idx = self.instructions.items.len;
try self.emit(.{ .jump_if_false = 0 }); // placeholder
try self.compileNode(ie.then_branch);
if (ie.else_branch) |eb| {
const jump_end_idx = self.instructions.items.len;
try self.emit(.{ .jump = 0 }); // placeholder
self.patchJumpIfFalse(jump_false_idx);
try self.compileNode(eb);
self.patchJump(jump_end_idx);
if (ie.binding_name) |binding_name| {
// if val := optional_expr { ... } else { ... }
try self.compileNode(ie.condition);
// Dup the optional value, test truthiness
try self.emit(.dup);
const jump_false_idx = self.instructions.items.len;
try self.emit(.{ .jump_if_false = 0 }); // placeholder
// Non-null path: the value is on the stack, bind as local
const slot: u16 = @intCast(self.locals.items.len);
try self.locals.append(self.allocator, .{ .name = binding_name, .depth = self.scope_depth });
try self.emit(.{ .set_local = slot });
try self.compileNode(ie.then_branch);
if (ie.else_branch) |eb| {
const jump_end_idx = self.instructions.items.len;
try self.emit(.{ .jump = 0 }); // placeholder
self.patchJumpIfFalse(jump_false_idx);
try self.emit(.pop); // discard the null value
try self.compileNode(eb);
self.patchJump(jump_end_idx);
} else {
self.patchJumpIfFalse(jump_false_idx);
try self.emit(.pop); // discard the null value
}
} else {
self.patchJumpIfFalse(jump_false_idx);
try self.compileNode(ie.condition);
const jump_false_idx = self.instructions.items.len;
try self.emit(.{ .jump_if_false = 0 }); // placeholder
try self.compileNode(ie.then_branch);
if (ie.else_branch) |eb| {
const jump_end_idx = self.instructions.items.len;
try self.emit(.{ .jump = 0 }); // placeholder
self.patchJumpIfFalse(jump_false_idx);
try self.compileNode(eb);
self.patchJump(jump_end_idx);
} else {
self.patchJumpIfFalse(jump_false_idx);
}
}
},
.call => |call_node| {
@@ -1066,6 +1094,20 @@ pub const Compiler = struct {
.field_names = fnames,
} });
},
.force_unwrap => |fu| {
try self.compileNode(fu.operand);
try self.emit(.opt_unwrap);
},
.null_coalesce => |nc| {
// x ?? y: evaluate x, if non-null keep it, else evaluate y
try self.compileNode(nc.lhs);
try self.emit(.dup);
const jump_idx = self.instructions.items.len;
try self.emit(.{ .jump_if_true = 0 }); // placeholder
try self.emit(.pop); // discard the null
try self.compileNode(nc.rhs);
self.patchJumpIfTrue(jump_idx);
},
.ufcs_alias => {}, // UFCS aliases are resolved at codegen, no-op in comptime
else => {
return error.UnsupportedExpression;
@@ -1574,6 +1616,12 @@ pub const VM = struct {
}
},
.opt_unwrap => {
const val = try self.pop();
if (val == .null_val) return error.NullDereference;
try self.push(val);
},
// Code insertion
.eval_insert => |info| {
// Pop the code string (result of evaluating the inner expression)

View File

@@ -204,6 +204,17 @@ pub const Lexer = struct {
return self.makeToken(.caret, start, self.index);
},
'~' => return self.makeToken(.tilde, start, self.index),
'?' => {
if (self.peek() == '?') {
self.index += 1;
return self.makeToken(.question_question, start, self.index);
}
if (self.peek() == '.') {
self.index += 1;
return self.makeToken(.question_dot, start, self.index);
}
return self.makeToken(.question, start, self.index);
},
'!' => {
if (self.peek() == '=') {
self.index += 1;

View File

@@ -76,6 +76,8 @@ pub const Server = struct {
if (params) |p| self.handleSignatureHelp(id, p) catch |e| self.logError(method, e);
} else if (std.mem.eql(u8, method, "textDocument/semanticTokens/full")) {
if (params) |p| self.handleSemanticTokens(id, p) catch |e| self.logError(method, e);
} else if (std.mem.eql(u8, method, "textDocument/inlayHint")) {
if (params) |p| self.handleInlayHint(id, p) catch |e| self.logError(method, e);
}
return true;
@@ -1015,6 +1017,325 @@ pub const Server = struct {
try self.sendResponse(id_json, result_json);
}
// ---- Inlay hints ----
fn handleInlayHint(self: *Server, id: ?std.json.Value, params: std.json.Value) !void {
const ctx = try self.extractRequest(id, params) orelse return;
const id_json = ctx.id_json;
const file_path = uriToFilePath(ctx.uri) orelse "";
const doc = self.documents.get(file_path) orelse {
return try self.sendResponse(id_json, "[]");
};
const sema = doc.sema orelse doc.last_good_sema orelse {
return try self.sendResponse(id_json, "[]");
};
const root = doc.root orelse {
return try self.sendResponse(id_json, "[]");
};
var hints = std.ArrayList(lsp.InlayHint).empty;
collectInlayHints(self.allocator, root, sema.symbols, doc.source, &hints);
self.collectCallHints(doc, root, &hints);
const result_json = try lsp.inlayHintsJson(self.allocator, hints.items);
try self.sendResponse(id_json, result_json);
}
fn collectInlayHints(
allocator: std.mem.Allocator,
node: *const sx.ast.Node,
symbols: []const sx.sema.Symbol,
source: [:0]const u8,
hints: *std.ArrayList(lsp.InlayHint),
) void {
switch (node.data) {
.root => |r| {
for (r.decls) |decl| collectInlayHints(allocator, decl, symbols, source, hints);
},
.block => |b| {
for (b.stmts) |stmt| collectInlayHints(allocator, stmt, symbols, source, hints);
},
.fn_decl => |fd| {
collectInlayHints(allocator, fd.body, symbols, source, hints);
},
.lambda => |lm| {
collectInlayHints(allocator, lm.body, symbols, source, hints);
},
.if_expr => |ie| {
if (ie.binding_name) |bname| {
addBindingHint(allocator, bname, node.span, symbols, source, hints);
}
collectInlayHints(allocator, ie.then_branch, symbols, source, hints);
if (ie.else_branch) |eb| collectInlayHints(allocator, eb, symbols, source, hints);
},
.while_expr => |we| {
if (we.binding_name) |bname| {
addBindingHint(allocator, bname, node.span, symbols, source, hints);
}
collectInlayHints(allocator, we.body, symbols, source, hints);
},
.for_expr => |fe| {
collectInlayHints(allocator, fe.body, symbols, source, hints);
},
.var_decl => |vd| {
// Only show hint when type is inferred (:= syntax)
if (vd.type_annotation != null) return;
if (vd.value == null) return;
addHintForDecl(allocator, vd.name, node.span, symbols, source, hints, true);
},
.const_decl => |cd| {
// Skip if explicit type annotation
if (cd.type_annotation != null) return;
// Skip functions, types, structs, enums, unions, comptime, foreign, library
switch (cd.value.data) {
.lambda, .fn_decl, .type_expr, .struct_decl, .enum_decl, .union_decl,
.comptime_expr, .foreign_expr, .library_decl,
=> return,
else => {},
}
addHintForDecl(allocator, cd.name, node.span, symbols, source, hints, false);
},
else => {},
}
}
fn addHintForDecl(
allocator: std.mem.Allocator,
name: []const u8,
span: sx.ast.Span,
symbols: []const sx.sema.Symbol,
source: [:0]const u8,
hints: *std.ArrayList(lsp.InlayHint),
is_colon_equal: bool,
) void {
// Find symbol by matching span start
const sym = findSymbolAtSpan(symbols, span.start, name) orelse return;
const ty = sym.ty orelse return;
// Skip void types — not useful to display
if (ty == .void_type) return;
const type_name = ty.displayName(allocator) catch return;
if (is_colon_equal) {
// For `:=` declarations: place hint between `:` and `=`
// Scan from after the name to find `:=`
var pos = span.start + @as(u32, @intCast(name.len));
while (pos + 1 < source.len) : (pos += 1) {
if (source[pos] == ':' and source[pos + 1] == '=') {
// Place hint at the `=` position (between `:` and `=`)
const eq_offset = pos + 1;
const loc = sx.errors.SourceLoc.compute(source, eq_offset);
if (loc.line == 0 or loc.col == 0) return;
hints.append(allocator, .{
.line = loc.line - 1,
.character = loc.col - 1,
.label = type_name,
.padding_left = true,
.padding_right = true,
}) catch {};
return;
}
}
} else {
// For `::` declarations: place hint between first `:` and second `:`
var pos = span.start + @as(u32, @intCast(name.len));
while (pos + 1 < source.len) : (pos += 1) {
if (source[pos] == ':' and source[pos + 1] == ':') {
const second_colon = pos + 1;
const loc = sx.errors.SourceLoc.compute(source, second_colon);
if (loc.line == 0 or loc.col == 0) return;
hints.append(allocator, .{
.line = loc.line - 1,
.character = loc.col - 1,
.label = type_name,
.padding_left = true,
.padding_right = true,
}) catch {};
return;
}
}
}
}
fn addBindingHint(
allocator: std.mem.Allocator,
name: []const u8,
span: sx.ast.Span,
symbols: []const sx.sema.Symbol,
source: [:0]const u8,
hints: *std.ArrayList(lsp.InlayHint),
) void {
// Look up symbol by name + span (sema stores binding with if/while node span)
const sym = findSymbolAtSpan(symbols, span.start, name) orelse return;
const ty = sym.ty orelse return;
if (ty == .void_type) return;
const type_name = ty.displayName(allocator) catch return;
// Scan from span start to find the `:=` used in the binding
var pos = span.start;
while (pos + 1 < source.len) : (pos += 1) {
if (source[pos] == ':' and source[pos + 1] == '=') {
const eq_offset = pos + 1;
const loc = sx.errors.SourceLoc.compute(source, eq_offset);
if (loc.line == 0 or loc.col == 0) return;
hints.append(allocator, .{
.line = loc.line - 1,
.character = loc.col - 1,
.label = type_name,
.padding_left = true,
.padding_right = true,
}) catch {};
return;
}
}
}
fn findSymbolAtSpan(symbols: []const sx.sema.Symbol, span_start: u32, name: []const u8) ?sx.sema.Symbol {
for (symbols) |sym| {
if (sym.def_span.start == span_start and std.mem.eql(u8, sym.name, name)) {
return sym;
}
}
return null;
}
// ---- Parameter name hints at call sites ----
fn collectCallHints(self: *Server, doc: *const Document, node: *const sx.ast.Node, hints: *std.ArrayList(lsp.InlayHint)) void {
switch (node.data) {
.root => |r| {
for (r.decls) |decl| self.collectCallHints(doc, decl, hints);
},
.block => |b| {
for (b.stmts) |stmt| self.collectCallHints(doc, stmt, hints);
},
.fn_decl => |fd| {
self.collectCallHints(doc, fd.body, hints);
},
.lambda => |lm| {
self.collectCallHints(doc, lm.body, hints);
},
.if_expr => |ie| {
self.collectCallHints(doc, ie.condition, hints);
self.collectCallHints(doc, ie.then_branch, hints);
if (ie.else_branch) |eb| self.collectCallHints(doc, eb, hints);
},
.while_expr => |we| {
self.collectCallHints(doc, we.condition, hints);
self.collectCallHints(doc, we.body, hints);
},
.for_expr => |fe| {
self.collectCallHints(doc, fe.iterable, hints);
self.collectCallHints(doc, fe.body, hints);
},
.var_decl => |vd| {
if (vd.value) |val| self.collectCallHints(doc, val, hints);
},
.const_decl => |cd| {
self.collectCallHints(doc, cd.value, hints);
},
.return_stmt => |rs| {
if (rs.value) |val| self.collectCallHints(doc, val, hints);
},
.assignment => |a| {
self.collectCallHints(doc, a.value, hints);
},
.binary_op => |bop| {
self.collectCallHints(doc, bop.lhs, hints);
self.collectCallHints(doc, bop.rhs, hints);
},
.unary_op => |uop| {
self.collectCallHints(doc, uop.operand, hints);
},
.call => |c| {
// Recurse into arguments (they may contain nested calls)
for (c.args) |arg| self.collectCallHints(doc, arg, hints);
// Emit parameter name hints for this call
self.emitCallParamHints(doc, c, hints);
},
.push_stmt => |ps| {
self.collectCallHints(doc, ps.context_expr, hints);
self.collectCallHints(doc, ps.body, hints);
},
.defer_stmt => |ds| {
self.collectCallHints(doc, ds.expr, hints);
},
else => {},
}
}
fn emitCallParamHints(self: *Server, doc: *const Document, call: sx.ast.Call, hints: *std.ArrayList(lsp.InlayHint)) void {
if (call.args.len == 0) return;
// Resolve callee name and find function declaration
var param_offset: usize = 0;
const fd = self.resolveCallTarget(doc, call, &param_offset) orelse return;
// Emit hints for each argument
for (call.args, 0..) |arg, i| {
const param_idx = i + param_offset;
if (param_idx >= fd.params.len) break;
const param = fd.params[param_idx];
// Skip variadic params
if (param.is_variadic) break;
// Skip if arg is an identifier matching the param name
if (arg.data == .identifier) {
if (std.mem.eql(u8, arg.data.identifier.name, param.name)) continue;
}
// Skip _ params
if (std.mem.eql(u8, param.name, "_")) continue;
const loc = sx.errors.SourceLoc.compute(doc.source, arg.span.start);
if (loc.line == 0 or loc.col == 0) continue;
const label = std.fmt.allocPrint(self.allocator, "{s}:", .{param.name}) catch continue;
hints.append(self.allocator, .{
.line = loc.line - 1,
.character = loc.col - 1,
.label = label,
.padding_left = false,
}) catch {};
}
}
fn resolveCallTarget(self: *Server, doc: *const Document, call: sx.ast.Call, param_offset: *usize) ?sx.ast.FnDecl {
param_offset.* = 0;
if (call.callee.data == .identifier) {
const name = call.callee.data.identifier.name;
return self.findFnDeclByName(doc, name);
}
if (call.callee.data == .field_access) {
const fa = call.callee.data.field_access;
// Try namespaced: "ns.func"
if (fa.object.data == .identifier) {
const ns_name = fa.object.data.identifier.name;
const qualified = std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns_name, fa.field }) catch return null;
if (self.findFnDeclByName(doc, qualified)) |fd| {
return fd;
}
}
// Try UFCS: bare function name, skip first param (receiver)
if (self.findFnDeclByName(doc, fa.field)) |fd| {
if (fd.params.len == call.args.len + 1) {
param_offset.* = 1;
}
return fd;
}
}
return null;
}
fn classifyToken(tok: sx.token.Token, sema: SemaResult, source: [:0]const u8) ?u32 {
const ST = lsp.SemanticTokenType;
return switch (tok.tag) {
@@ -1084,6 +1405,9 @@ pub const Server = struct {
.pipe_arrow,
.caret,
.caret_equal,
.question,
.question_question,
.question_dot,
.tilde,
.less_less,
.less_less_equal,

View File

@@ -108,7 +108,8 @@ pub fn initializeResultJson(allocator: std.mem.Allocator) ![]const u8 {
"\"semanticTokensProvider\":{{\"legend\":{{" ++
"\"tokenTypes\":[\"namespace\",\"type\",\"enum\",\"struct\",\"parameter\",\"variable\",\"enumMember\",\"function\",\"keyword\",\"number\",\"string\",\"operator\"]," ++
"\"tokenModifiers\":[\"declaration\",\"readonly\"]" ++
"}},\"full\":true}}}}}}",
"}},\"full\":true}}," ++
"\"inlayHintProvider\":true}}}}",
.{},
);
}
@@ -358,3 +359,29 @@ pub fn publishDiagnosticsJson(allocator: std.mem.Allocator, uri: []const u8, dia
try buf.appendSlice(allocator, "]}");
return buf.items;
}
pub const InlayHint = struct {
line: u32,
character: u32,
label: []const u8,
kind: u32 = 1, // 1 = Type
padding_left: bool = true,
padding_right: bool = false,
};
/// Build inlay hints JSON array response.
pub fn inlayHintsJson(allocator: std.mem.Allocator, hints: []const InlayHint) ![]const u8 {
var buf = std.ArrayList(u8).empty;
try buf.append(allocator, '[');
for (hints, 0..) |hint, idx| {
if (idx > 0) try buf.append(allocator, ',');
const label_escaped = try jsonString(allocator, hint.label);
const json = try std.fmt.allocPrint(allocator,
"{{\"position\":{{\"line\":{d},\"character\":{d}}},\"label\":{s},\"kind\":{d},\"paddingLeft\":{s},\"paddingRight\":{s}}}",
.{ hint.line, hint.character, label_escaped, hint.kind, if (hint.padding_left) "true" else "false", if (hint.padding_right) "true" else "false" },
);
try buf.appendSlice(allocator, json);
}
try buf.append(allocator, ']');
return buf.items;
}

View File

@@ -327,6 +327,13 @@ pub const Parser = struct {
fn parseTypeExpr(self: *Parser) anyerror!*Node {
const start = self.current.loc.start;
// Optional type: ?T
if (self.current.tag == .question) {
self.advance(); // skip '?'
const inner_type = try self.parseTypeExpr();
return try self.createNode(start, .{ .optional_type_expr = .{ .inner_type = inner_type } });
}
// Pointer type: *T
if (self.current.tag == .star) {
self.advance(); // skip '*'
@@ -1128,6 +1135,14 @@ pub const Parser = struct {
continue;
}
// Null coalescing: expr ?? default
if (self.current.tag == .question_question and Prec.null_coalesce >= min_prec) {
self.advance();
const rhs = try self.parseBinary(Prec.null_coalesce + 1);
lhs = try self.createNode(lhs.span.start, .{ .null_coalesce = .{ .lhs = lhs, .rhs = rhs } });
continue;
}
const prec = self.binaryPrec();
if (prec == 0 or prec < min_prec) break;
@@ -1291,6 +1306,20 @@ pub const Parser = struct {
} else {
return self.fail("expected field name or index after '.'");
}
} else if (self.current.tag == .question_dot) {
// Optional chaining: expr?.field
self.advance();
if (self.current.tag == .identifier) {
const field = self.tokenSlice(self.current);
self.advance();
expr = try self.createNode(expr.span.start, .{ .field_access = .{ .object = expr, .field = field, .is_optional = true } });
} else if (self.current.tag == .int_literal) {
const field = self.tokenSlice(self.current);
self.advance();
expr = try self.createNode(expr.span.start, .{ .field_access = .{ .object = expr, .field = field, .is_optional = true } });
} else {
return self.fail("expected field name after '?.'");
}
} else if (self.current.tag == .l_bracket) {
// Index or slice access: expr[expr] or expr[start..end]
self.advance();
@@ -1323,6 +1352,11 @@ pub const Parser = struct {
} });
}
}
} else if (self.current.tag == .bang) {
// Force unwrap: expr!
// Only if it's not != (bang_equal would have been lexed as a single token)
self.advance();
expr = try self.createNode(expr.span.start, .{ .force_unwrap = .{ .operand = expr } });
} else {
break;
}
@@ -1532,6 +1566,32 @@ pub const Parser = struct {
const start = self.current.loc.start;
self.advance(); // skip 'if'
// Optional binding: if val := expr { ... }
// Detect: identifier followed by :=
if (self.current.tag == .identifier and self.peekNext() == .colon_equal) {
const binding_name = self.tokenSlice(self.current);
self.advance(); // skip identifier
self.advance(); // skip :=
const source_expr = try self.parseExpr();
const then_branch = try self.parseBlock();
var else_branch: ?*Node = null;
if (self.current.tag == .kw_else) {
self.advance();
if (self.current.tag == .kw_if) {
else_branch = try self.parseIfExpr();
} else {
else_branch = try self.parseBlock();
}
}
return try self.createNode(start, .{ .if_expr = .{
.condition = source_expr,
.then_branch = then_branch,
.else_branch = else_branch,
.is_inline = false,
.binding_name = binding_name,
} });
}
// Parse condition above comparison level, leaving comparisons
// unconsumed for manual handling with match disambiguation.
var condition = try self.parseBinary(Prec.shift);
@@ -1627,6 +1687,20 @@ pub const Parser = struct {
const start = self.current.loc.start;
self.advance(); // skip 'while'
// Optional binding: while val := expr { ... }
if (self.current.tag == .identifier and self.peekNext() == .colon_equal) {
const binding_name = self.tokenSlice(self.current);
self.advance(); // skip identifier
self.advance(); // skip :=
const source_expr = try self.parseExpr();
const body = try self.parseBlock();
return try self.createNode(start, .{ .while_expr = .{
.condition = source_expr,
.body = body,
.binding_name = binding_name,
} });
}
const condition = try self.parseExpr();
const body = try self.parseBlock();
@@ -1934,15 +2008,16 @@ pub const Parser = struct {
const Prec = struct {
const none: u8 = 0;
const pipe: u8 = 1; // |>
const logical_or: u8 = 2; // or
const logical_and: u8 = 3; // and
const bit_or: u8 = 4; // |
const bit_xor: u8 = 5; // ^
const bit_and: u8 = 6; // &
const comparison: u8 = 7; // == != < <= > >= in
const shift: u8 = 8; // << >>
const additive: u8 = 9; // + -
const multiplicative: u8 = 10; // * / %
const null_coalesce: u8 = 2; // ??
const logical_or: u8 = 3; // or
const logical_and: u8 = 4; // and
const bit_or: u8 = 5; // |
const bit_xor: u8 = 6; // ^
const bit_and: u8 = 7; // &
const comparison: u8 = 8; // == != < <= > >= in
const shift: u8 = 9; // << >>
const additive: u8 = 10; // + -
const multiplicative: u8 = 11; // * / %
};
fn binaryPrec(self: *const Parser) u8 {

View File

@@ -278,6 +278,13 @@ pub const Analyzer = struct {
const elem_name = elem_type.displayName(self.allocator) catch return .void_type;
return .{ .slice_type = .{ .element_name = elem_name } };
}
// Optional type: ?T
if (tn.data == .optional_type_expr) {
const ote = tn.data.optional_type_expr;
const inner_type = self.resolveTypeNode(ote.inner_type);
const inner_name = inner_type.displayName(self.allocator) catch return .void_type;
return .{ .optional_type = .{ .child_name = inner_name } };
}
// Pointer type: *T
if (tn.data == .pointer_type_expr) {
const pte = tn.data.pointer_type_expr;
@@ -456,6 +463,16 @@ pub const Analyzer = struct {
}
return .void_type;
},
.force_unwrap => |fu| {
const opt_ty = self.inferExprType(fu.operand);
if (opt_ty.isOptional()) return Type.fromName(opt_ty.optional_type.child_name) orelse .void_type;
return .void_type;
},
.null_coalesce => |nc| {
const opt_ty = self.inferExprType(nc.lhs);
if (opt_ty.isOptional()) return Type.fromName(opt_ty.optional_type.child_name) orelse .void_type;
return self.inferExprType(nc.rhs);
},
.deref_expr => |de| {
const ptr_ty = self.inferExprType(de.operand);
if (ptr_ty.isPointer()) return ptr_ty.pointerPointeeType() orelse .void_type;
@@ -711,7 +728,20 @@ pub const Analyzer = struct {
},
.if_expr => |ie| {
try self.analyzeNode(ie.condition);
try self.analyzeNode(ie.then_branch);
if (ie.binding_name) |bname| {
// `if val := expr { ... }` — val is the unwrapped optional
const cond_ty = self.inferExprType(ie.condition);
const inner_ty: ?Type = if (cond_ty.isOptional())
Type.fromName(cond_ty.optional_type.child_name)
else
null;
try self.pushScope();
try self.addSymbol(bname, .variable, inner_ty, node.span);
try self.analyzeNode(ie.then_branch);
self.popScope();
} else {
try self.analyzeNode(ie.then_branch);
}
if (ie.else_branch) |eb| {
try self.analyzeNode(eb);
}
@@ -729,7 +759,19 @@ pub const Analyzer = struct {
},
.while_expr => |we| {
try self.analyzeNode(we.condition);
try self.analyzeNode(we.body);
if (we.binding_name) |bname| {
const cond_ty = self.inferExprType(we.condition);
const inner_ty: ?Type = if (cond_ty.isOptional())
Type.fromName(cond_ty.optional_type.child_name)
else
null;
try self.pushScope();
try self.addSymbol(bname, .variable, inner_ty, node.span);
try self.analyzeNode(we.body);
self.popScope();
} else {
try self.analyzeNode(we.body);
}
},
.for_expr => |fe| {
try self.analyzeNode(fe.iterable);
@@ -812,6 +854,7 @@ pub const Analyzer = struct {
.slice_type_expr,
.pointer_type_expr,
.many_pointer_type_expr,
.optional_type_expr,
.null_literal,
.array_literal,
.parameterized_type_expr,
@@ -829,6 +872,13 @@ pub const Analyzer = struct {
try self.analyzeNode(elem.value);
}
},
.force_unwrap => |fu| {
try self.analyzeNode(fu.operand);
},
.null_coalesce => |nc| {
try self.analyzeNode(nc.lhs);
try self.analyzeNode(nc.rhs);
},
.deref_expr => |de| {
try self.analyzeNode(de.operand);
},
@@ -864,6 +914,8 @@ pub const Analyzer = struct {
.index_expr,
.slice_expr,
.deref_expr,
.force_unwrap,
.null_coalesce,
.null_literal,
.type_expr,
.insert_expr,
@@ -905,7 +957,17 @@ pub const Analyzer = struct {
}
}
}
// For compound types (pointers, slices, arrays), resolve inner type refs
// Compound types: ?T, *T, [*]T, []T, [N]T — delegate to resolveTypeNode
switch (tn.data) {
.optional_type_expr, .pointer_type_expr, .many_pointer_type_expr,
.slice_type_expr, .array_type_expr,
=> {
const resolved = self.resolveTypeNode(tn);
if (resolved != .void_type) return resolved;
},
else => {},
}
// For compound types, resolve inner type refs
self.resolveTypeRef(tn);
}
return null;
@@ -950,6 +1012,9 @@ pub const Analyzer = struct {
.array_type_expr => |ate| {
self.resolveTypeRef(ate.element_type);
},
.optional_type_expr => |ote| {
self.resolveTypeRef(ote.inner_type);
},
else => {},
}
}
@@ -1152,6 +1217,7 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node {
.slice_type_expr,
.pointer_type_expr,
.many_pointer_type_expr,
.optional_type_expr,
.null_literal,
.array_literal,
.parameterized_type_expr,
@@ -1165,6 +1231,13 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node {
if (findNodeAtOffset(elem.value, offset)) |found| return found;
}
},
.null_coalesce => |nc| {
if (findNodeAtOffset(nc.lhs, offset)) |found| return found;
if (findNodeAtOffset(nc.rhs, offset)) |found| return found;
},
.force_unwrap => |fu| {
if (findNodeAtOffset(fu.operand, offset)) |found| return found;
},
.deref_expr => |de| {
if (findNodeAtOffset(de.operand, offset)) |found| return found;
},

View File

@@ -70,6 +70,9 @@ pub const Tag = enum {
pipe_arrow, // |>
caret, // ^
caret_equal, // ^=
question, // ?
question_question, // ??
question_dot, // ?.
tilde, // ~
less_less, // <<
less_less_equal, // <<=
@@ -142,6 +145,9 @@ pub const Tag = enum {
.pipe_arrow => "|>",
.caret => "^",
.caret_equal => "^=",
.question => "?",
.question_question => "??",
.question_dot => "?.",
.tilde => "~",
.less_less => "<<",
.less_less_equal => "<<=",

View File

@@ -23,6 +23,7 @@ pub const Type = union(enum) {
vector_type: VectorTypeInfo,
function_type: FunctionTypeInfo,
any_type,
optional_type: OptionalTypeInfo,
meta_type: MetaTypeInfo,
tuple_type: TupleTypeInfo,
@@ -53,6 +54,10 @@ pub const Type = union(enum) {
length: u32,
};
pub const OptionalTypeInfo = struct {
child_name: []const u8,
};
pub const MetaTypeInfo = struct {
name: []const u8,
};
@@ -90,6 +95,7 @@ pub const Type = union(enum) {
}
return info.return_type.eql(o.return_type.*);
},
.optional_type => |info| std.mem.eql(u8, info.child_name, other.optional_type.child_name),
.meta_type => |info| std.mem.eql(u8, info.name, other.meta_type.name),
.tuple_type => |info| {
const o = other.tuple_type;
@@ -141,6 +147,7 @@ pub const Type = union(enum) {
if (std.mem.eql(u8, name, "f64")) return .f64;
return null;
},
'?' => if (name.len >= 2) .{ .optional_type = .{ .child_name = name[1..] } } else null,
'A' => if (std.mem.eql(u8, name, "Any")) .any_type else null,
'v' => if (std.mem.eql(u8, name, "void")) .void_type else null,
'[' => {
@@ -212,6 +219,20 @@ pub const Type = union(enum) {
};
}
pub fn isOptional(self: Type) bool {
return switch (self) {
.optional_type => true,
else => false,
};
}
pub fn optionalChild(self: Type) ?[]const u8 {
return switch (self) {
.optional_type => |info| info.child_name,
else => null,
};
}
pub fn isAny(self: Type) bool {
return switch (self) {
.any_type => true,
@@ -382,6 +403,30 @@ pub const Type = union(enum) {
return true;
}
// T → ?T: any type implicitly wraps into its optional
if (target.isOptional()) {
const child_name = target.optional_type.child_name;
// null → ?T
if (self.isPointer() and std.mem.eql(u8, self.pointer_type.pointee_name, "void")) return true;
// ?T → ?U when T → U
if (self.isOptional()) {
const self_child = fromName(self.optional_type.child_name) orelse return false;
const target_child = fromName(child_name) orelse return false;
return self_child.isImplicitlyConvertibleTo(target_child);
}
// T → ?T: check if self matches the child type
if (fromName(child_name)) |child_type| {
return self.eql(child_type) or self.isImplicitlyConvertibleTo(child_type);
}
// Non-primitive child (struct/enum name): compare by name
return switch (self) {
.struct_type => |n| std.mem.eql(u8, n, child_name),
.enum_type => |n| std.mem.eql(u8, n, child_name),
.union_type => |n| std.mem.eql(u8, n, child_name),
else => false,
};
}
const src_float = self.isFloat();
const dst_float = target.isFloat();
const src_int = self.isInt();
@@ -461,6 +506,7 @@ pub const Type = union(enum) {
}
return try buf.toOwnedSlice(allocator);
},
.optional_type => |info| return fmtAlloc(allocator, "?{s}", .{info.child_name}),
.meta_type => |info| info.name,
.tuple_type => |info| {
var buf = std.ArrayList(u8).empty;
@@ -531,6 +577,9 @@ pub const Type = union(enum) {
return Type.s(capped);
}
// Optional types: widen inner types
if (a.isOptional() and b.isOptional()) return a;
// Pointer types: both are pointers → return first (all are opaque ptr at LLVM level)
if ((a.isPointer() or a.isManyPointer()) and (b.isPointer() or b.isManyPointer())) return a;