diff --git a/examples/28-sdl-graphics.sx b/examples/28-sdl-graphics.sx index 2c44ab2..fd32f7a 100644 --- a/examples/28-sdl-graphics.sx +++ b/examples/28-sdl-graphics.sx @@ -277,12 +277,15 @@ GLSL; if e.key == { case .escape: running = false; } + } + case .window_exposed: (e) { + } case .key_down: (e) { k : u32 = xx e.key; print("ts={} wid={} sc={} key={}\n", e.timestamp, e.window_id, e.scancode, k); - } + } } } @@ -290,7 +293,7 @@ GLSL; ticks := SDL_GetTicks(); ms : f32 = xx ticks; angle := ms * 0.001; - + // Build matrices proj : Matrix44 = ---; mat4_perspective(proj, PI / 4.0, 800.0 / 600.0, 0.1, 100.0); diff --git a/examples/modules/std.sx b/examples/modules/std.sx index 53f7491..7927fd3 100644 --- a/examples/modules/std.sx +++ b/examples/modules/std.sx @@ -15,6 +15,7 @@ field_name :: ($T: Type, idx: s64) -> string #builtin; field_value :: (s: $T, idx: s64) -> Any #builtin; is_flags :: ($T: Type) -> bool #builtin; field_value_int :: ($T: Type, idx: s64) -> s64 #builtin; +field_index :: ($T: Type, val: T) -> s64 #builtin; string :: []u8 #builtin; int_to_string :: (n: s64) -> string { @@ -221,9 +222,9 @@ flags_to_string :: (val: $T) -> string { enum_to_string :: (u: $T) -> string { if is_flags(T) { return flags_to_string(u); } - tag := cast(s64) u; - result := concat(".", field_name(T, tag)); - payload := field_value(u, tag); + idx := field_index(T, u); + result := concat(".", field_name(T, idx)); + payload := field_value(u, idx); pstr := any_to_string(payload); if pstr.len > 0 { result = concat(result, concat("(", concat(pstr, ")"))); diff --git a/specs.md b/specs.md index 0d0ea98..efeebca 100644 --- a/specs.md +++ b/specs.md @@ -148,6 +148,18 @@ if s == { } ``` +#### Payload Capture +Match arms can capture the variant's payload into a local variable: +```sx +if s == { + case .circle: (radius) { print("radius: {}\n", radius); } + case .rect: (size) => print("size: {}\n", size); +} +``` +The `(name)` after the colon binds the payload. Two forms: +- Block: `case .variant: (name) { body }` +- Short: `case .variant: (name) => expr;` + #### Enum Interpolation Payload-less enums print as `.variant`. Enums with payloads print as `.variant(value)` or ``: ```sx @@ -568,6 +580,33 @@ Syntax: `Name :: enum [flags] [type] { ... }` The backing type must be an integer type (`u8`, `u16`, `u32`, `s8`, `s16`, `s32`, `s64`, etc.). When omitted, the default is `s64`. This is useful for C interop (matching C enum sizes) and memory efficiency. +### Enum Layout Struct + +For C interop with tagged unions (e.g. SDL_Event), a struct can be used as the backing type to specify the exact memory layout: + +```sx +// Inline layout +SDL_Event :: enum struct { tag: u32; _: u32; payload: [30]u32; } { + quit :: 0x100; + key_down :: 0x300: SDL_KeyData; + key_up :: 0x301: SDL_KeyData; +} + +// Named layout +EventLayout :: struct { tag: u32; _: u32; payload: [30]u32; } +SDL_Event :: enum EventLayout { + quit :: 0x100; + key_down :: 0x300: SDL_KeyData; +} +``` + +The layout struct must have: +- A field named `tag` — integer type, the discriminant. Its type becomes the enum's backing type. +- A field named `payload` — array type, the variant data area. Its size determines the maximum payload capacity. +- Any other fields are treated as padding/reserved and positioned by the struct layout. + +This gives explicit control over the memory layout instead of relying on automatic alignment. The total size equals the struct size. Without a layout struct, tagged enums use `{ tag, [max_payload_size x i8] }` with no padding. + ### Enum Flags ```sx @@ -882,6 +921,7 @@ Built-in functions are declared in `std.sx` with the `#builtin` suffix, which te - `field_count($T: Type) -> s64` — returns the number of fields (struct), variants (enum), or elements (vector) in type `T` - `field_name($T: Type, idx: s64) -> string` — returns the name of the `idx`-th field (struct) or variant (enum) of type `T` - `field_value(s: $T, idx: s64) -> Any` — returns the `idx`-th field (struct) or element (vector) of `s`, boxed as `Any` +- `field_index($T: Type, val: T) -> s64` — returns the sequential variant index for an explicit enum value (reverse of `field_value_int`). Returns `-1` if no variant matches. ### Type Conversion - `cast(Type) expr` — prefix operator that converts `expr` to `Type`. Examples: `cast(s32) 3.14`, `cast(f64) n`. When `Type` is a runtime `Type` value inside a type-category match arm, the compiler generates a dispatch switch over all types in the category, monomorphizing the callee for each concrete type. diff --git a/src/lsp/document.zig b/src/lsp/document.zig index 73e3f42..607333b 100644 --- a/src/lsp/document.zig +++ b/src/lsp/document.zig @@ -24,6 +24,8 @@ pub const Document = struct { root: ?*sx.ast.Node, /// Sema results for this file (references are relative to this source). sema: ?sx.sema.SemaResult, + /// Last successful sema (preserved across parse failures for completions). + last_good_sema: ?sx.sema.SemaResult = null, /// Import declarations parsed from this file. imports: []const Import, @@ -199,6 +201,9 @@ pub const DocumentStore = struct { // Run sema on this file's own AST doc.sema = analyzer.analyze(root) catch null; + if (doc.sema != null) { + doc.last_good_sema = doc.sema; + } } pub fn get(self: *const DocumentStore, path: []const u8) ?*Document { diff --git a/src/lsp/server.zig b/src/lsp/server.zig index 79d37af..9dadc12 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -462,36 +462,32 @@ pub const Server = struct { var items = std.ArrayList(lsp.CompletionItem).empty; if (extractDotPrefix(doc.source, cursor_offset)) |prefix| { - const sema = doc.sema orelse { - const items_json = try lsp.completionListJson(self.allocator, items.items); - return try self.sendResponse(id_json, items_json); - }; + if (doc.sema) |sema| { + // Check if prefix is a namespace — offer imported doc's declarations + if (self.findImportByNs(doc, prefix)) |imp| { + if (self.documents.get(imp.path)) |imp_doc| { + if (imp_doc.root) |imp_root| { + if (imp_root.data == .root) { + try collectDeclCompletions(self.allocator, &items, imp_root.data.root.decls); + } + } + } + } else if (doc.root) |root| { + // Try as type name directly (e.g. Vec2., Color.) + try self.collectMemberCompletions(&items, sema, root, prefix); - // Check if prefix is a namespace — offer imported doc's declarations - if (self.findImportByNs(doc, prefix)) |imp| { - if (self.documents.get(imp.path)) |imp_doc| { - if (imp_doc.root) |imp_root| { - if (imp_root.data == .root) { - try collectDeclCompletions(self.allocator, &items, imp_root.data.root.decls); + // Try as variable name — resolve to type and offer fields + UFCS methods + if (items.items.len == 0) { + if (resolveVariableType(sema, prefix)) |type_name| { + try self.collectMemberCompletions(&items, sema, root, type_name); + try self.collectUfcsCompletions(&items, root, type_name); } } } - } else { - const root = doc.root orelse { - const items_json = try lsp.completionListJson(self.allocator, items.items); - return try self.sendResponse(id_json, items_json); - }; - - // Try as type name directly (e.g. Vec2., Color.) - try self.collectMemberCompletions(&items, sema, root, prefix); - - // Try as variable name — resolve to type and offer fields + UFCS methods - if (items.items.len == 0) { - if (resolveVariableType(sema, prefix)) |type_name| { - try self.collectMemberCompletions(&items, sema, root, type_name); - try self.collectUfcsCompletions(&items, root, type_name); - } - } + } + // Fallback: try resolving as a match arm capture variable (works without current parse) + if (items.items.len == 0) { + try self.collectCaptureCompletions(&items, doc, cursor_offset, prefix); } } else { // Bare dot (no prefix) — check if we're inside a match expression @@ -504,62 +500,235 @@ pub const Server = struct { } fn collectMatchEnumCompletions(self: *Server, items: *std.ArrayList(lsp.CompletionItem), doc: *const Document, cursor_offset: u32) !void { - const root = doc.root orelse return; - const sema = doc.sema orelse return; + // Text-based approach: scan backward from cursor to find enclosing "SUBJECT == {" + // This works even when the file has parse errors (user is mid-typing) + const subject_name = findMatchSubjectText(doc.source, cursor_offset) orelse return; - // Find enclosing match expression's subject - const subject = sx.sema.findEnclosingMatchSubject(root, cursor_offset) orelse return; + // Use sema (current or last successful) to resolve the subject's type + const sema = doc.sema orelse doc.last_good_sema orelse return; - // Resolve the subject to an enum type name - const enum_name: ?[]const u8 = switch (subject.data) { - .identifier => |id| blk: { - // Look up variable type, then check if it's an enum - for (sema.symbols) |sym| { - if (!std.mem.eql(u8, sym.name, id.name)) continue; - if (sym.kind != .variable and sym.kind != .param) continue; - const ty = sym.ty orelse break; - break :blk switch (ty) { - .enum_type => |n| n, - .union_type => |n| n, - else => null, - }; + // Resolve subject name to an enum type + const enum_name = self.resolveExprEnumType(sema, subject_name) orelse return; + + // Use enum variant names directly from sema symbols (works even without doc.root) + for (sema.symbols) |sym| { + if (!std.mem.eql(u8, sym.name, enum_name)) continue; + if (sym.kind != .enum_type) continue; + + // Find the enum_decl node in the origin doc (or main doc) + const lookup_doc = if (sym.origin) |origin_path| + self.documents.get(origin_path) + else + null; + const lookup_root = if (lookup_doc) |ld| ld.root orelse doc.root else doc.root; + const root = lookup_root orelse continue; + + if (sx.sema.findNodeAtOffset(root, sym.def_span.start)) |node| { + if (node.data == .enum_decl) { + for (node.data.enum_decl.variant_names) |variant| { + try items.append(self.allocator, .{ + .label = variant, + .kind = @intFromEnum(lsp.CompletionItemKind.EnumMember), + }); + } } - break :blk null; - }, - .field_access => |fa| blk: { - // e.g. e.key — resolve the field's type - if (fa.object.data == .identifier) { - const var_name = fa.object.data.identifier.name; - // Find variable's struct type, then look up the field type - for (sema.symbols) |sym| { - if (!std.mem.eql(u8, sym.name, var_name)) continue; - const ty = sym.ty orelse break; - const struct_name = switch (ty) { - .struct_type => |n| n, - else => break, - }; - // Look up the struct's field type - if (sema.struct_types.get(struct_name)) |info| { - for (info.field_names, 0..) |fname, fi| { - if (std.mem.eql(u8, fname, fa.field) and fi < info.field_types.len) { - break :blk switch (info.field_types[fi]) { - .enum_type => |n| n, - .union_type => |n| n, - else => null, - }; + } + break; + } + } + + /// Scan backward through source text to find the subject of an enclosing match expression. + /// Looks for the pattern: SUBJECT == { ... case . + /// Returns the subject text (e.g. "event", "e.key") or null. + fn findMatchSubjectText(source: []const u8, cursor_offset: u32) ?[]const u8 { + var pos: u32 = cursor_offset; + var brace_depth: u32 = 0; + + // Walk backward, tracking brace depth, looking for "== {" + while (pos > 2) { + pos -= 1; + const ch = source[pos]; + if (ch == '}') { + brace_depth += 1; + } else if (ch == '{') { + if (brace_depth > 0) { + brace_depth -= 1; + } else { + // Found an unmatched '{' — check if preceded by "==" + var scan = pos; + while (scan > 0 and std.ascii.isWhitespace(source[scan - 1])) scan -= 1; + if (scan >= 2 and source[scan - 1] == '=' and source[scan - 2] == '=') { + // Found "== {" — extract the subject before "==" + var end = scan - 2; + while (end > 0 and std.ascii.isWhitespace(source[end - 1])) end -= 1; + if (end == 0) return null; + var start = end; + while (start > 0 and (isIdentChar(source[start - 1]) or source[start - 1] == '.')) { + start -= 1; + } + if (start < end) return source[start..end]; + } + // Not a match expr — but keep scanning (might be a nested block) + } + } + } + return null; + } + + /// Resolve a capture variable's struct type by scanning backward for `case .VARIANT: (name)` + /// and looking up the variant's payload type in the enum declaration. + fn collectCaptureCompletions(self: *Server, items: *std.ArrayList(lsp.CompletionItem), doc: *const Document, cursor_offset: u32, var_name: []const u8) !void { + const sema = doc.sema orelse doc.last_good_sema orelse return; + + // Scan backward from cursor to find: case .VARIANT: (var_name) + const variant_name = findCaptureVariant(doc.source, cursor_offset, var_name) orelse return; + + // Find the enclosing match subject + const subject_name = findMatchSubjectText(doc.source, cursor_offset) orelse return; + + // Resolve subject to an enum type + const enum_name = self.resolveExprEnumType(sema, subject_name) orelse return; + + // Find the enum_decl and look up the variant's payload type + for (sema.symbols) |sym| { + if (!std.mem.eql(u8, sym.name, enum_name)) continue; + if (sym.kind != .enum_type) continue; + + const lookup_doc = if (sym.origin) |origin_path| + self.documents.get(origin_path) + else + null; + const lookup_root = if (lookup_doc) |ld| ld.root orelse doc.root else doc.root; + const root = lookup_root orelse continue; + + if (sx.sema.findNodeAtOffset(root, sym.def_span.start)) |node| { + if (node.data == .enum_decl) { + const ed = node.data.enum_decl; + // Find the matching variant and its payload type + for (ed.variant_names, 0..) |vn, vi| { + if (!std.mem.eql(u8, vn, variant_name)) continue; + if (vi >= ed.variant_types.len) break; + const vt = ed.variant_types[vi] orelse break; + // The payload type should be a struct — get its name + const payload_type_name = if (vt.data == .type_expr) vt.data.type_expr.name else break; + // Now offer that struct's fields + const payload_sema = if (lookup_doc) |ld| ld.sema orelse sema else sema; + try self.collectMemberCompletions(items, payload_sema, root, payload_type_name); + return; + } + } + } + break; + } + } + + /// Scan backward from cursor to find `case .VARIANT: (var_name)` and return VARIANT. + fn findCaptureVariant(source: []const u8, cursor_offset: u32, var_name: []const u8) ?[]const u8 { + // Look backward for pattern: case .VARIANT: (var_name) + // We search for "(var_name)" first, then look for "case .VARIANT:" before it + var pos: u32 = cursor_offset; + while (pos > var_name.len + 10) { // need room for "case .X: (name)" + pos -= 1; + // Look for the closing ) of a capture + if (source[pos] != ')') continue; + // Check if the capture name matches + if (pos < var_name.len + 1) continue; + const name_end = pos; + const name_start = pos - @as(u32, @intCast(var_name.len)); + if (!std.mem.eql(u8, source[name_start..name_end], var_name)) continue; + // Check for ( before the name + if (name_start < 1 or source[name_start - 1] != '(') continue; + + // Now scan backward from '(' to find ": " then ".VARIANT" then "case " + var scan = name_start - 1; + // Skip whitespace + while (scan > 0 and std.ascii.isWhitespace(source[scan - 1])) scan -= 1; + // Expect ':' + if (scan < 1 or source[scan - 1] != ':') continue; + scan -= 1; + // Now extract the variant name: scan backward for ".VARIANT" + // Skip whitespace before ':' + // The variant is an enum literal like .key_down + // Actually the ':' comes right after the variant value (explicit) or variant name + // Pattern: case .VARIANT: (name) OR case .VARIANT :: 0x300: PayloadType;... nah + // In the match arm: case .key_down: (e) { + // So before ':' we have the enum literal .key_down + while (scan > 0 and std.ascii.isWhitespace(source[scan - 1])) scan -= 1; + // Extract identifier (variant name) + const vend = scan; + while (scan > 0 and (isIdentChar(source[scan - 1]))) scan -= 1; + if (scan >= vend) continue; + const variant = source[scan..vend]; + // Check for '.' before variant name + if (scan < 1 or source[scan - 1] != '.') continue; + + return variant; + } + return null; + } + + /// Resolve a text expression like "event" or "e.key" to an enum/union type name. + fn resolveExprEnumType(self: *Server, sema: SemaResult, expr: []const u8) ?[]const u8 { + // Simple identifier: look up variable type + if (std.mem.indexOfScalar(u8, expr, '.') == null) { + for (sema.symbols) |sym| { + if (!std.mem.eql(u8, sym.name, expr)) continue; + if (sym.kind != .variable and sym.kind != .param) continue; + const ty = sym.ty orelse return null; + return switch (ty) { + .enum_type => |n| n, + .union_type => |n| n, + else => null, + }; + } + return null; + } + + // Field access: "var.field" — resolve var's struct type, then look up field + if (std.mem.indexOfScalar(u8, expr, '.')) |dot| { + const var_name = expr[0..dot]; + const field_name = expr[dot + 1 ..]; + for (sema.symbols) |sym| { + if (!std.mem.eql(u8, sym.name, var_name)) continue; + const ty = sym.ty orelse return null; + const struct_name = switch (ty) { + .struct_type => |n| n, + else => return null, + }; + if (sema.struct_types.get(struct_name)) |info| { + for (info.field_names, 0..) |fname, fi| { + if (std.mem.eql(u8, fname, field_name) and fi < info.field_types.len) { + return switch (info.field_types[fi]) { + .enum_type => |n| n, + .union_type => |n| n, + else => null, + }; + } + } + } + // Also check imported docs for struct info + if (sym.origin) |origin_path| { + if (self.documents.get(origin_path)) |od| { + if (od.sema) |imp_sema| { + if (imp_sema.struct_types.get(struct_name)) |info| { + for (info.field_names, 0..) |fname, fi| { + if (std.mem.eql(u8, fname, field_name) and fi < info.field_types.len) { + return switch (info.field_types[fi]) { + .enum_type => |n| n, + .union_type => |n| n, + else => null, + }; + } } } } - break; } } - break :blk null; - }, - else => null, - }; + return null; + } + } - const name = enum_name orelse return; - try self.collectMemberCompletions(items, sema, root, name); + return null; } fn collectDeclCompletions(allocator: std.mem.Allocator, items: *std.ArrayList(lsp.CompletionItem), decls: []const *sx.ast.Node) !void { @@ -1896,3 +2065,45 @@ pub const Server = struct { return buf.items; } }; + +test "findMatchSubjectText: simple identifier" { + const source = "if event == {\n case ."; + const result = Server.findMatchSubjectText(source, @intCast(source.len)); + try std.testing.expectEqualStrings("event", result.?); +} + +test "findMatchSubjectText: field access" { + const source = "if e.key == {\n case ."; + const result = Server.findMatchSubjectText(source, @intCast(source.len)); + try std.testing.expectEqualStrings("e.key", result.?); +} + +test "findMatchSubjectText: nested braces" { + const source = "if event == {\n case .quit: { do_something(); }\n case ."; + const result = Server.findMatchSubjectText(source, @intCast(source.len)); + try std.testing.expectEqualStrings("event", result.?); +} + +test "findMatchSubjectText: no match context" { + const source = "while true {\n case ."; + const result = Server.findMatchSubjectText(source, @intCast(source.len)); + try std.testing.expect(result == null); +} + +test "findCaptureVariant: simple capture" { + const source = "case .key_down: (e) {\n e."; + const result = Server.findCaptureVariant(source, @intCast(source.len), "e"); + try std.testing.expectEqualStrings("key_down", result.?); +} + +test "findCaptureVariant: different name" { + const source = "case .mouse_motion: (evt) {\n evt."; + const result = Server.findCaptureVariant(source, @intCast(source.len), "evt"); + try std.testing.expectEqualStrings("mouse_motion", result.?); +} + +test "findCaptureVariant: no capture" { + const source = "case .quit: running = false;\n e."; + const result = Server.findCaptureVariant(source, @intCast(source.len), "e"); + try std.testing.expect(result == null); +}