diff --git a/examples/37-dot-shorthand.sx b/examples/37-dot-shorthand.sx new file mode 100644 index 0000000..2b666cd --- /dev/null +++ b/examples/37-dot-shorthand.sx @@ -0,0 +1,295 @@ +#import "modules/std.sx"; + +// ============================================================ +// Dot-shorthand tests: .identifier(args) unification +// Tests both tagged enum backward compat and struct static methods +// ============================================================ + +// --- Type declarations --- + +Color :: enum { red; green; blue; } + +Shape :: enum { + circle: f32; + rect: struct { w, h: f32; }; + none; +} + +Vec2 :: struct { + x: f32; + y: f32; + + create :: (x: f32, y: f32) -> Vec2 { Vec2.{ x = x, y = y }; } + zero :: () -> Vec2 { Vec2.{ x = 0.0, y = 0.0 }; } + unit_x :: () -> Vec2 { Vec2.{ x = 1.0, y = 0.0 }; } + add :: (a: Vec2, b: Vec2) -> Vec2 { Vec2.{ x = a.x + b.x, y = a.y + b.y }; } + scale :: (v: Vec2, s: f32) -> Vec2 { Vec2.{ x = v.x * s, y = v.y * s }; } + len :: (v: Vec2) -> s32 { xx (v.x + v.y); } +} + +EdgeInsets :: struct { + top: f32; + right: f32; + bottom: f32; + left: f32; + + all :: (v: f32) -> EdgeInsets { EdgeInsets.{ top = v, right = v, bottom = v, left = v }; } + symmetric :: (h: f32, v: f32) -> EdgeInsets { EdgeInsets.{ top = v, right = h, bottom = v, left = h }; } + horizontal :: (h: f32) -> EdgeInsets { EdgeInsets.{ top = 0.0, right = h, bottom = 0.0, left = h }; } +} + +Trio :: struct { + a: s32; + b: s32; + c: s32; + + make :: (a: s32, b: s32, c: s32) -> Trio { Trio.{ a = a, b = b, c = c }; } + sum :: (t: Trio) -> s32 { t.a + t.b + t.c; } +} + +Result :: enum { + ok: s32; + err: string; +} + +main :: () { + // ============================================================ + // SECTION 1: Tagged enum backward compatibility + // ============================================================ + print("--- tagged enum compat ---\n"); + + // T1: .variant(payload) in typed variable declaration + { + sh : Shape = .circle(3.14); + print("T1: {}\n", sh.circle); + } + + // T2: Bare .variant (no payload) in typed variable + { + sh : Shape = .none; + ms := if sh == { + case .circle: 1; + case .rect: 2; + case .none: 3; + }; + print("T2: {}\n", ms); + } + + // T3: .variant with struct payload + { + sh : Shape = .rect(.{ 5.0, 3.0 }); + print("T3: {} {}\n", sh.rect.w, sh.rect.h); + } + + // T4: Qualified Type.variant(payload) still works + { + sh := Shape.circle(2.71); + print("T4: {}\n", sh.circle); + } + + // T5: Match with payload capture + { + sh : Shape = .circle(9.5); + if sh == { + case .circle: (r) { print("T5: {}\n", r); } + case .rect: (sz) { print("T5: rect\n"); } + case .none: print("T5: none\n"); + } + } + + // T6: Return .variant(payload) from function + { + make_shape :: (r: f32) -> Shape { .circle(r); } + sh := make_shape(4.2); + print("T6: {}\n", sh.circle); + } + + // T7: Reassignment with .variant(payload) and bare .variant + { + sh : Shape = .circle(1.0); + print("T7a: {}\n", sh.circle); + sh = .rect(.{ 2.0, 3.0 }); + print("T7b: {} {}\n", sh.rect.w, sh.rect.h); + sh = .none; + ms := if sh == { + case .circle: 1; + case .rect: 2; + case .none: 3; + }; + print("T7c: {}\n", ms); + } + + // T8: .variant(payload) as function argument (match-as-expression) + { + describe :: (sh: Shape) -> s32 { + if sh == { + case .circle: 10; + case .rect: 20; + case .none: 30; + }; + } + print("T8a: {}\n", describe(.circle(7.0))); + print("T8b: {}\n", describe(.rect(.{ 3.0, 4.0 }))); + print("T8c: {}\n", describe(.none)); + } + + // T9: Tagged enum with string payload + { + r : Result = .ok(42); + ms := if r == { + case .ok: (v) { v; } + case .err: (e) { -1; } + }; + print("T9: {}\n", ms); + } + + // T10: Match as expression returning tagged enum + { + select :: (n: s32) -> Shape { + if n == { + case 0: .none; + case 1: .circle(1.0); + else: .rect(.{ 9.0, 9.0 }); + }; + } + print("T10a: {}\n", if select(0) == { case .none: 1; else: 0; }); + print("T10b: {}\n", select(1).circle); + print("T10c: {}\n", select(2).rect.w); + } + + // ============================================================ + // SECTION 2: Struct static method shorthand + // ============================================================ + print("--- struct static shorthand ---\n"); + + // S1: .method(args) as function argument (the motivating use case) + { + print_vec :: (v: Vec2) { print("S1: {} {}\n", v.x, v.y); } + print_vec(.create(3.0, 4.0)); + } + + // S2: .method(args) in typed variable declaration + { + v : Vec2 = .create(5.0, 6.0); + print("S2: {} {}\n", v.x, v.y); + } + + // S3: Return .method(args) from function with return type + { + make_vec :: () -> Vec2 { .create(7.0, 8.0); } + v := make_vec(); + print("S3: {} {}\n", v.x, v.y); + } + + // S4: Zero-arg static method (factory) + { + print_vec :: (v: Vec2) { print("S4: {} {}\n", v.x, v.y); } + print_vec(.zero()); + print_vec(.unit_x()); + } + + // S5: Three-arg static method (proves multi-arg works) + { + print_trio :: (t: Trio) { print("S5: {}\n", t.a + t.b + t.c); } + print_trio(.make(10, 20, 30)); + } + + // S6: Two-arg shorthand matching the EdgeInsets use case + { + apply_insets :: (ei: EdgeInsets) { print("S6: {} {} {} {}\n", ei.top, ei.right, ei.bottom, ei.left); } + apply_insets(.all(8.0)); + apply_insets(.symmetric(16.0, 8.0)); + apply_insets(.horizontal(12.0)); + } + + // S7: Result of .method() used in further computation + { + v : Vec2 = .create(3.0, 4.0); + print("S7: {}\n", Vec2.len(v)); + } + + // S8: Chained qualified + shorthand — ensure both work together + { + v1 := Vec2.create(1.0, 2.0); + print_vec :: (v: Vec2) { print("S8: {} {}\n", v.x, v.y); } + print_vec(.create(3.0, 4.0)); + print("S8q: {} {}\n", v1.x, v1.y); + } + + // S9: Static method taking struct of same type as args + { + v : Vec2 = .add(.create(1.0, 2.0), .create(3.0, 4.0)); + print("S9: {} {}\n", v.x, v.y); + } + + // S10: Static method + piped result + { + v := Vec2.create(2.0, 3.0) |> Vec2.scale(2.0); + print("S10: {} {}\n", v.x, v.y); + } + + // ============================================================ + // SECTION 3: Edge cases mixing both + // ============================================================ + print("--- edge cases ---\n"); + + // E1: Both tagged enum and struct shorthand in same scope + { + sh : Shape = .circle(5.0); + v : Vec2 = .create(1.0, 2.0); + print("E1: {} {} {}\n", sh.circle, v.x, v.y); + } + + // E2: Function taking both types — each resolves correctly + { + use_both :: (sh: Shape, v: Vec2) { + ms : s32 = 0; + if sh == { case .circle: (r) { ms = xx r; } else: {} } + print("E2: {} {} {}\n", ms, v.x, v.y); + } + use_both(.circle(9.0), .create(1.0, 2.0)); + } + + // E3: Bare .variant (no parens) as function arg + { + check_none :: (sh: Shape) -> s32 { + if sh == { case .none: 1; else: 0; }; + } + print("E3: {}\n", check_none(.none)); + } + + // E4: Nested shorthand — .method takes a struct param created with shorthand + // (inner .create must resolve via the method's parameter type, not the outer type) + { + v : Vec2 = .add(Vec2.create(1.0, 2.0), Vec2.create(3.0, 4.0)); + print("E4: {} {}\n", v.x, v.y); + } + + // E5: Tagged enum .variant(payload) in match-as-expression + { + sh : Shape = .circle(42.0); + r : s32 = 0; + if sh == { + case .circle: (v) { r = xx v; } + case .rect: (sz) { r = xx sz.w; } + case .none: r = xx -1; + } + print("E5: {}\n", r); + } + + // E6: Color enum (plain, not tagged) still works with bare .variant + { + c : Color = .green; + ci : s32 = xx c; + print("E6: {}\n", ci); + } + + // E7: Struct shorthand in typed variable, then pass to function + { + ei : EdgeInsets = .symmetric(10.0, 20.0); + show :: (e: EdgeInsets) { print("E7: {} {}\n", e.top, e.left); } + show(ei); + } + + print("=== DONE ===\n"); +} diff --git a/examples/50-smoke.sx b/examples/50-smoke.sx index 889d661..a6ea6ff 100644 --- a/examples/50-smoke.sx +++ b/examples/50-smoke.sx @@ -277,6 +277,13 @@ SumBox :: struct ($T: Type/Summable) { } // ============================================================ +// Struct constants test +Phys :: struct { + x, y: f32; + GRAVITY :f32: 9.81; + MAX_SPEED :: 100; +} + // Init block test struct Builder :: struct { total: s32; @@ -1702,6 +1709,15 @@ END; b := y ?? 99; print("coalesce a: {}\n", a); // coalesce a: 42 print("coalesce b: {}\n", b); // coalesce b: 99 + + // Chained ?? (right-associative): a ?? b ?? c + z: ?s32 = null; + c := x ?? y ?? 0; + d := z ?? y ?? 99; + e := z ?? z ?? 0; + print("chained ?? c: {}\n", c); // chained ?? c: 42 + print("chained ?? d: {}\n", d); // chained ?? d: 99 + print("chained ?? e: {}\n", e); // chained ?? e: 0 } // If-binding (safe unwrap) @@ -2902,6 +2918,15 @@ END; print("AE5: {}\n", acc.total); } + // --- Struct Constants --- + print("=== Struct Constants ===\n"); + { + print("gravity: {}\n", Phys.GRAVITY); // gravity: 9.810000 + print("max speed: {}\n", Phys.MAX_SPEED); // max speed: 100 + p := Phys.{ x = 0.0, y = Phys.GRAVITY }; + print("p.y: {}\n", p.y); // p.y: 9.810000 + } + // --- Init Blocks (IB) --- print("=== Init Blocks ===\n"); @@ -2954,5 +2979,43 @@ END; print("IB5: {}\n", result); } + // ============================================================ + // SECTION: Struct static method shorthand (.method(args) syntax) + // ============================================================ + print("--- struct static method shorthand ---\n"); + + // SM1: Basic shorthand — .create(args) resolves to Dims.create(args) + { + Dims :: struct { + w: f32; + h: f32; + + create :: (w: f32, h: f32) -> Dims { + Dims.{ w = w, h = h }; + } + + square :: (size: f32) -> Dims { + Dims.{ w = size, h = size }; + } + } + use_dims :: (d: Dims) { print("SM1: {} {}\n", d.w, d.h); } + use_dims(.create(16.0, 8.0)); + use_dims(.square(5.0)); + } + + // SM2: Shorthand in variable declaration with explicit type + { + Pair :: struct { + a: s64; + b: s64; + + make :: (a: s64, b: s64) -> Pair { + Pair.{ a = a, b = b }; + } + } + p : Pair = .make(10, 20); + print("SM2: {} {}\n", p.a, p.b); + } + print("=== DONE ===\n"); } diff --git a/src/ast.zig b/src/ast.zig index 5b4df95..26ef935 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -144,7 +144,6 @@ pub const Identifier = struct { pub const EnumLiteral = struct { name: []const u8, // without the leading dot - payload: ?*Node = null, // non-null for enum variants with payloads (tagged unions) }; pub const BinaryOp = struct { @@ -295,6 +294,7 @@ pub const StructDecl = struct { type_params: []const StructTypeParam = &.{}, using_entries: []const UsingEntry = &.{}, methods: []const *Node = &.{}, // fn_decl nodes for struct methods + constants: []const *Node = &.{}, // const_decl nodes for struct-level constants }; pub const StructFieldInit = struct { diff --git a/src/codegen.zig b/src/codegen.zig index ce6ee00..f23e24d 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -1398,7 +1398,10 @@ pub const CodeGen = struct { _ = try self.registerFnDecl(fd, fd.name); } }, - .struct_decl => |sd| try self.registerStructMethods(sd), + .struct_decl => |sd| { + try self.registerStructMethods(sd); + try self.registerStructConstants(sd); + }, .const_decl => |cd| { if (cd.value.data == .builtin_expr or cd.value.data == .type_expr) { // already handled @@ -2501,6 +2504,7 @@ pub const CodeGen = struct { }, .struct_decl => |sd| { try self.registerStructMethods(sd); + try self.registerStructConstants(sd); }, .const_decl => |cd| { if (cd.value.data == .builtin_expr) { @@ -3141,6 +3145,16 @@ pub const CodeGen = struct { }, .struct_decl => |sd| { try self.registerStructType(sd); + try self.registerStructMethods(sd); + // Method bodies are deferred — they'll be generated via genStructMethodBodies + // in registerStructMethods (non-generic methods are registered with registerFnDecl, + // and their bodies are deferred via shouldDeferFnBody or generated inline) + for (sd.methods) |method_node| { + const fd = method_node.data.fn_decl; + if (fd.type_params.len > 0) continue; + const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ sd.name, fd.name }); + try self.deferred_fn_bodies.append(self.allocator, .{ .fd = fd, .name = qualified, .namespace = sd.name, .source_file = self.current_source_file }); + } return null; }, .union_decl => { @@ -3344,6 +3358,13 @@ pub const CodeGen = struct { const lit_alloca = try self.genStructLiteral(vd.value.?.data.struct_literal, sname); try self.registerVariable(vd.name, lit_alloca, sx_ty); return null; + } else if (vd.value.?.data == .call and vd.value.?.data.call.callee.data == .enum_literal) { + // .method(args) — struct static method shorthand with inferred type + const cn = vd.value.?.data.call; + const method_name = cn.callee.data.enum_literal.name; + const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ sname, method_name }); + const val = try self.genCallByName(qualified, cn); + _ = c.LLVMBuildStore(self.builder, val, alloca); } else if (vd.value.?.data == .call) { // Function call returning a struct — result is a value, store to alloca const val = try self.genExpr(vd.value.?); @@ -3425,14 +3446,18 @@ pub const CodeGen = struct { } else if (vd.value.?.data == .undef_literal) { self.storeUndef(info.llvm_type, alloca); } else if (vd.value.?.data == .enum_literal) { - const el = vd.value.?.data.enum_literal; - const lit_alloca = try self.genTaggedEnumLiteral(el, uname); - try self.registerVariable(vd.name, lit_alloca, sx_ty); - return null; + const val = try self.genTaggedEnumLiteral(vd.value.?.data.enum_literal.name, null, uname); + _ = c.LLVMBuildStore(self.builder, val, alloca); + } else if (vd.value.?.data == .call and vd.value.?.data.call.callee.data == .enum_literal) { + // .variant(payload) — tagged enum construction with inferred type + const cn = vd.value.?.data.call; + const payload_node: ?*Node = if (cn.args.len > 0) cn.args[0] else null; + const val = try self.genTaggedEnumLiteral(cn.callee.data.enum_literal.name, payload_node, uname); + _ = c.LLVMBuildStore(self.builder, val, alloca); } else if (vd.value.?.data == .call) { - // Call returning a union — could be enum construction (alloca) or function call (value) + // Call returning a union — function call (value) const result = try self.genExpr(vd.value.?); - _ = c.LLVMBuildStore(self.builder, self.loadIfPointer(result, info.llvm_type, "union_load"), alloca); + _ = c.LLVMBuildStore(self.builder, result, alloca); } else { // Other expression — try genExprAsType const result = try self.genExprAsType(vd.value.?, sx_ty); @@ -4349,7 +4374,7 @@ pub const CodeGen = struct { }, .enum_literal => |el| { if (self.current_return_type.isUnion()) { - return self.genTaggedEnumLiteral(el, self.current_return_type.union_type); + return self.genTaggedEnumLiteral(el.name, null, self.current_return_type.union_type); } if (self.current_return_type.isEnum()) { return self.genEnumLiteral(el.name, self.current_return_type.enum_type); @@ -4932,6 +4957,38 @@ pub const CodeGen = struct { } } + fn registerStructConstants(self: *CodeGen, sd: ast.StructDecl) !void { + if (sd.constants.len == 0) return; + + try self.namespaces.put(sd.name, {}); + + for (sd.constants) |const_node| { + const cd = const_node.data.const_decl; + const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ sd.name, cd.name }); + + // Reuse the same registration logic as top-level constants + const sx_ty = if (cd.type_annotation) |ta| + self.resolveType(ta) + else + self.inferType(cd.value); + if (sx_ty == .void_type) continue; + + const const_val = self.evalConstant(cd.value, sx_ty) orelse continue; + + const name_z = try self.allocator.dupeZ(u8, qualified); + const global = c.LLVMAddGlobal(self.module, self.typeToLLVM(sx_ty), name_z.ptr); + c.LLVMSetInitializer(global, const_val); + c.LLVMSetGlobalConstant(global, 1); + + try self.comptime_globals.put(qualified, .{ + .global = global, + .ty = sx_ty, + .expr = cd.value, + .is_resolved = true, + }); + } + } + /// Register a protocol declaration. For #inline protocols, this generates /// a struct type with ctx + fn-ptr fields and wrapper methods. fn registerProtocolDecl(self: *CodeGen, pd: ast.ProtocolDecl) !void { @@ -5843,7 +5900,7 @@ pub const CodeGen = struct { try self.type_registry.put(ud.name, .{ .union_info = uinfo }); } - fn genTaggedEnumLiteral(self: *CodeGen, el: ast.EnumLiteral, expected_union_name: ?[]const u8) !c.LLVMValueRef { + fn genTaggedEnumLiteral(self: *CodeGen, variant_name: []const u8, payload_node: ?*Node, expected_union_name: ?[]const u8) !c.LLVMValueRef { const uname = expected_union_name orelse (if (self.current_return_type.isUnion()) self.current_return_type.union_type else null) orelse return self.emitError("cannot infer enum type for literal"); @@ -5853,12 +5910,12 @@ pub const CodeGen = struct { // Find variant index var variant_idx: ?u32 = null; for (info.variant_names, 0..) |vn, i| { - if (std.mem.eql(u8, vn, el.name)) { + if (std.mem.eql(u8, vn, variant_name)) { variant_idx = @intCast(i); break; } } - const idx = variant_idx orelse return self.emitErrorFmt("no variant '{s}' in enum '{s}'", .{ el.name, resolved_name }); + const idx = variant_idx orelse return self.emitErrorFmt("no variant '{s}' in enum '{s}'", .{ variant_name, resolved_name }); // Alloca union const alloca = self.buildEntryBlockAlloca(info.llvm_type, "union_tmp"); @@ -5870,15 +5927,15 @@ pub const CodeGen = struct { _ = c.LLVMBuildStore(self.builder, c.LLVMConstInt(tag_ty, tag_val, 0), tag_gep); // Store payload (field 1) if not void - if (el.payload) |payload_node| { + if (payload_node) |pnode| { const variant_ty = info.variant_types[idx]; if (variant_ty != .void_type) { - const payload_val = try self.genExprAsType(payload_node, variant_ty); + const payload_val = try self.genExprAsType(pnode, variant_ty); self.storeStructField(info.llvm_type, alloca, info.payload_field_index, payload_val); } } - return alloca; + return c.LLVMBuildLoad2(self.builder, info.llvm_type, alloca, "union_val"); } fn genStructLiteral(self: *CodeGen, sl: ast.StructLiteral, expected_struct_name: ?[]const u8) anyerror!c.LLVMValueRef { @@ -6176,8 +6233,24 @@ pub const CodeGen = struct { // Enum/union literal assigned to union type: construct tagged enum if (node.data == .enum_literal and target_ty.isUnion()) { - const el = node.data.enum_literal; - return self.genTaggedEnumLiteral(el, target_ty.union_type); + return self.genTaggedEnumLiteral(node.data.enum_literal.name, null, target_ty.union_type); + } + + // Call with enum_literal callee: .variant(payload) or .method(args) with known target type + if (node.data == .call and node.data.call.callee.data == .enum_literal) { + const call_node = node.data.call; + const el_name = call_node.callee.data.enum_literal.name; + + if (target_ty.isUnion()) { + const payload_node: ?*Node = if (call_node.args.len > 0) call_node.args[0] else null; + return self.genTaggedEnumLiteral(el_name, payload_node, target_ty.union_type); + } + + if (target_ty.isStruct()) { + const struct_name = self.resolveAlias(target_ty.struct_type); + const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ struct_name, el_name }); + return self.genCallByName(qualified, call_node); + } } // Struct literal targeting union type: .Variant.{fields} pattern @@ -7393,6 +7466,15 @@ pub const CodeGen = struct { return self.emitErrorFmt("no field '{s}' on Any (available: .tag, .value)", .{fa.field}); } } + // Namespace constant: TypeName.CONSTANT + const ns_name = fa.object.data.identifier.name; + if (self.namespaces.contains(ns_name) or self.type_registry.contains(ns_name)) { + const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns_name, fa.field }); + if (self.comptime_globals.getPtr(qualified)) |ct| { + if (!ct.is_resolved) try self.resolveComptimeGlobal(ct); + return c.LLVMBuildLoad2(self.builder, self.typeToLLVM(ct.ty), ct.global, "struct_const"); + } + } } // Non-identifier object: evaluate expression and check type const obj_val = try self.genExpr(fa.object); @@ -7966,6 +8048,24 @@ pub const CodeGen = struct { } fn genCall(self: *CodeGen, call_node: ast.Call) !c.LLVMValueRef { + // Dot-shorthand call: .variant(payload) or .method(args) with type inferred from context + if (call_node.callee.data == .enum_literal) { + const el_name = call_node.callee.data.enum_literal.name; + + if (self.current_return_type.isUnion()) { + const payload_node: ?*Node = if (call_node.args.len > 0) call_node.args[0] else null; + return self.genTaggedEnumLiteral(el_name, payload_node, self.current_return_type.union_type); + } + + if (self.current_return_type.isStruct()) { + const struct_name = self.resolveAlias(self.current_return_type.struct_type); + const qualified = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ struct_name, el_name }); + return self.genCallByName(qualified, call_node); + } + + return self.emitErrorFmt("cannot infer type for '.{s}(...)' call", .{el_name}); + } + if (call_node.callee.data == .field_access) { const fa = call_node.callee.data.field_access; @@ -7988,10 +8088,7 @@ pub const CodeGen = struct { if (rty.isUnion()) { const type_name = rty.union_type; const payload_node: ?*Node = if (call_node.args.len > 0) call_node.args[0] else null; - return self.genTaggedEnumLiteral(.{ - .name = fa.field, - .payload = payload_node, - }, type_name); + return self.genTaggedEnumLiteral(fa.field, payload_node, type_name); } } @@ -11158,6 +11255,14 @@ pub const CodeGen = struct { return .void_type; }, .call => |call_node| { + // Dot-shorthand call: .variant(payload) — type from context + if (call_node.callee.data == .enum_literal) { + if (self.current_return_type.isEnum()) return self.current_return_type; + if (self.current_return_type.isUnion()) return self.current_return_type; + if (self.current_return_type.isStruct()) return self.current_return_type; + return .{ .enum_type = "" }; + } + // Check for union literal pattern: Type.variant(payload) if (call_node.callee.data == .field_access) { const fa = call_node.callee.data.field_access; @@ -11438,6 +11543,16 @@ pub const CodeGen = struct { } } } + // Namespace constant: TypeName.CONSTANT + if (fa.object.data == .identifier) { + const ns_name = fa.object.data.identifier.name; + if (self.namespaces.contains(ns_name) or self.type_registry.contains(ns_name)) { + const qualified = std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns_name, fa.field }) catch return Type.s(64); + if (self.comptime_globals.getPtr(qualified)) |ct| { + return ct.ty; + } + } + } return Type.s(64); }, .index_expr => |ie| { diff --git a/src/parser.zig b/src/parser.zig index 312c95e..8d8a3c8 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -742,6 +742,7 @@ pub const Parser = struct { var field_defaults = std.ArrayList(?*Node).empty; var using_entries = std.ArrayList(ast.UsingEntry).empty; var methods = std.ArrayList(*Node).empty; + var constants = std.ArrayList(*Node).empty; while (self.current.tag != .r_brace and self.current.tag != .eof) { // Check for #using directive @@ -769,17 +770,26 @@ pub const Parser = struct { if (self.current.tag == .l_paren and self.isFunctionDef()) { try methods.append(self.allocator, try self.parseFnDecl(method_name, method_start)); } else { - return self.fail("only function declarations are allowed inside struct bodies"); + // Non-function constant: name :: value; + const value = try self.parseExpr(); + if (self.current.tag == .semicolon) self.advance(); + try constants.append(self.allocator, try self.createNode(method_start, .{ .const_decl = .{ + .name = method_name, + .type_annotation = null, + .value = value, + } })); } continue; } // Parse field group: name1, name2, ...: type (= default)?; + // Or typed constant: name :Type: value; var group_names = std.ArrayList([]const u8).empty; if (self.current.tag != .identifier) { return self.fail("expected field name in struct"); } + const field_start = self.current.loc.start; try group_names.append(self.allocator, self.tokenSlice(self.current)); self.advance(); @@ -795,6 +805,19 @@ pub const Parser = struct { try self.expect(.colon); const field_type = try self.parseTypeExpr(); + // Typed constant: name :Type: value; (second colon after type) + if (self.current.tag == .colon and group_names.items.len == 1) { + self.advance(); // skip second ':' + const value = try self.parseExpr(); + if (self.current.tag == .semicolon) self.advance(); + try constants.append(self.allocator, try self.createNode(field_start, .{ .const_decl = .{ + .name = group_names.items[0], + .type_annotation = field_type, + .value = value, + } })); + continue; + } + // Check for default value: = expr var default_val: ?*Node = null; if (self.current.tag == .equal) { @@ -828,6 +851,7 @@ pub const Parser = struct { .type_params = try type_params.toOwnedSlice(self.allocator), .using_entries = try using_entries.toOwnedSlice(self.allocator), .methods = try methods.toOwnedSlice(self.allocator), + .constants = try constants.toOwnedSlice(self.allocator), } }); } @@ -1441,7 +1465,7 @@ pub const Parser = struct { // 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); + const rhs = try self.parseBinary(Prec.null_coalesce); lhs = try self.createNode(lhs.span.start, .{ .null_coalesce = .{ .lhs = lhs, .rhs = rhs } }); continue; } @@ -1760,16 +1784,7 @@ pub const Parser = struct { } const name = self.tokenSlice(self.current); self.advance(); - // Enum literal with payload: .variant(payload) — tagged enum (formerly union literal) - if (self.current.tag == .l_paren) { - self.advance(); // skip '(' - const payload = try self.parseExpr(); - try self.expect(.r_paren); - return try self.createNode(start, .{ .enum_literal = .{ - .name = name, - .payload = payload, - } }); - } + // Enum literal: .variant_name — parsePostfix handles optional (...) as a call return try self.createNode(start, .{ .enum_literal = .{ .name = name } }); }, .l_paren => { diff --git a/src/sema.zig b/src/sema.zig index 6f1a928..28b8252 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -850,12 +850,8 @@ pub const Analyzer = struct { .union_decl => |ud| { try self.addSymbol(ud.name, .enum_type, .{ .union_type = ud.name }, node.span); }, - .enum_literal => |el| { - if (el.payload) |p| { - try self.analyzeNode(p); - } - }, // Leaf nodes — nothing to recurse into + .enum_literal, .int_literal, .float_literal, .bool_literal, @@ -1255,12 +1251,8 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node { if (findNodeAtOffset(fi.value, offset)) |found| return found; } }, - .enum_literal => |el| { - if (el.payload) |p| { - if (findNodeAtOffset(p, offset)) |found| return found; - } - }, // Leaf nodes + .enum_literal, .identifier, .int_literal, .float_literal, diff --git a/tests/expected/37-dot-shorthand.exit b/tests/expected/37-dot-shorthand.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/37-dot-shorthand.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/37-dot-shorthand.txt b/tests/expected/37-dot-shorthand.txt new file mode 100644 index 0000000..882fe17 --- /dev/null +++ b/tests/expected/37-dot-shorthand.txt @@ -0,0 +1,41 @@ +--- tagged enum compat --- +T1: 3.140000 +T2: 3 +T3: 5.000000 3.000000 +T4: 2.710000 +T5: 9.500000 +T6: 4.199999 +T7a: 1.000000 +T7b: 2.000000 3.000000 +T7c: 3 +T8a: 10 +T8b: 20 +T8c: 30 +T9: 42 +T10a: 1 +T10b: 1.000000 +T10c: 9.000000 +--- struct static shorthand --- +S1: 3.000000 4.000000 +S2: 5.000000 6.000000 +S3: 7.000000 8.000000 +S4: 0.000000 0.000000 +S4: 1.000000 0.000000 +S5: 60 +S6: 8.000000 8.000000 8.000000 8.000000 +S6: 8.000000 16.000000 8.000000 16.000000 +S6: 0.000000 12.000000 0.000000 12.000000 +S7: 7 +S8: 3.000000 4.000000 +S8q: 1.000000 2.000000 +S9: 4.000000 6.000000 +S10: 4.000000 6.000000 +--- edge cases --- +E1: 5.000000 1.000000 2.000000 +E2: 9 1.000000 2.000000 +E3: 1 +E4: 4.000000 6.000000 +E5: 42 +E6: 1 +E7: 20.000000 10.000000 +=== DONE === diff --git a/tests/expected/50-smoke.txt b/tests/expected/50-smoke.txt index e50613a..ee9c1ea 100644 --- a/tests/expected/50-smoke.txt +++ b/tests/expected/50-smoke.txt @@ -396,6 +396,9 @@ opt y: null unwrap: 10 coalesce a: 42 coalesce b: 99 +chained ?? c: 42 +chained ?? d: 99 +chained ?? e: 0 if-bind x: 7 if-bind y: none match some: 55 @@ -569,10 +572,18 @@ AE2: 8 AE3: 102 AE4: 51 AE5: 15 +=== Struct Constants === +gravity: 9.810000 +max speed: 100 +p.y: 9.810000 === Init Blocks === IB1: 60 3 IB2: 142 2 IB3: 5 1 IB4: 100 IB5: 52 +--- struct static method shorthand --- +SM1: 16.000000 8.000000 +SM1: 5.000000 5.000000 +SM2: 10 20 === DONE ===