Skip to content

Commit 5d87557

Browse files
Bleak84fubark
authored andcommitted
- Rebased with master. Reduce to MVP.
feat: Add type embedding + compile-time safety Implements object type embedding with the use keyword, enabling types to embed other types and access their fields/methods. Features: - Two-step field access through embedded types - Method embedding support - Conflict resolution (child fields take precedence) - Hidden embedded fields for encapsulation - Circular embedding detection using resolving flag pattern - Compile-time method resolution for static types (direct call emission) - Field access coalescing (hidden local reuse to prevent IR bloat) - Compile-time error for ambiguous field assignment in methods (requires explicit self. for field mutation; e.g. use self.value = new_val) Tests: - All existing tests pass - New tests added: - test/types/type_embedding_base.cy - test/types/type_embedding_basic.cy - test/types/type_embedding_circular_error.cy (intentionally compile-errors) - test/types/type_embedding_performance.cy - test/types/type_embedding_three_objects_test.cy - test/types/test_field_assignment_error.cy - test/types/test_field_assignment_compile_error.cy (intentionally compile-errors)
1 parent b751ca6 commit 5d87557

14 files changed

+256
-22
lines changed

docs/docs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1201,7 +1201,7 @@ type Base:
12011201
a int
12021202
12031203
fn (&Base) double() -> int:
1204-
return a * 2
1204+
return $a * 2
12051205
12061206
type Container:
12071207
b use Base

src/ast.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,7 @@ pub const Field = struct {
715715
typeSpec: ?*Node,
716716
init: ?*Node,
717717
hidden: bool,
718+
embedded: bool,
718719
};
719720

720721
pub const TraitDecl = extern struct {
@@ -748,6 +749,7 @@ pub const StructDecl = extern struct {
748749
attrs: Slice(*Attribute),
749750
impls: Slice(*ImplDecl),
750751
fields: Slice(*Field),
752+
num_embedded_fields: u32,
751753
is_tuple: bool,
752754
pos: u32,
753755
};

src/builtins/cy.cy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,7 @@ type StructDecl:
848848
attrs []^Node
849849
impls []^Node
850850
fields []^Node
851+
num_embedded_fields i32
851852
is_tuple bool
852853
pos i32
853854

src/parser.zig

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -948,7 +948,7 @@ pub const Parser = struct {
948948
});
949949
}
950950

951-
fn parseStructField(self: *Parser) !?*ast.Node {
951+
fn parseStructField(self: *Parser) !?*ast.Field {
952952
var hidden = false;
953953
if (self.peek().tag() == .minus) {
954954
hidden = true;
@@ -959,20 +959,33 @@ pub const Parser = struct {
959959
return null;
960960
};
961961

962+
// Check for 'use' after field name (type embedding)
963+
const is_embedded = if (self.peek().tag() == .use_k) blk: {
964+
self.advance();
965+
break :blk true;
966+
} else false;
967+
962968
const typeSpec = try self.parseOptTypeSpec();
963969

970+
// Validate embedded fields must have a type specifier
971+
if (is_embedded and typeSpec == null) {
972+
return self.reportError("Embedded field must have a type specifier.", &.{});
973+
}
974+
964975
var init_expr: ?*ast.Node = null;
965976
if (self.peek().tag() == .equal) {
966977
self.advance();
967978
init_expr = try self.parseExpr(.{}) orelse {
968979
return self.reportError("Expected default initializer.", &.{});
969980
};
970981
}
971-
return self.ast.newNodeErase(.struct_field, .{
982+
983+
return self.ast.newNode(.struct_field, .{
972984
.name = name,
973985
.typeSpec = typeSpec,
974986
.init = init_expr,
975987
.hidden = hidden,
988+
.embedded = is_embedded,
976989
});
977990
}
978991

@@ -1268,7 +1281,7 @@ pub const Parser = struct {
12681281
}
12691282

12701283
fn newStructDecl(self: *Parser, start: TokenId, node_t: ast.NodeType, name: *ast.Node,
1271-
config: TypeDeclConfig, impls: []*ast.ImplDecl, fields: []*ast.Field,
1284+
config: TypeDeclConfig, impls: []*ast.ImplDecl, fields: []*ast.Field, num_embedded_fields: usize,
12721285
is_tuple: bool) !*ast.StructDecl {
12731286

12741287
const n = try self.ast.newNodeErase(.struct_decl, .{
@@ -1277,6 +1290,7 @@ pub const Parser = struct {
12771290
.impls = .{ .ptr = impls.ptr, .len = impls.len },
12781291
.fields = .{ .ptr = fields.ptr, .len = fields.len },
12791292
.attrs = .{ .ptr = config.attrs.ptr, .len = config.attrs.len },
1293+
.num_embedded_fields = @intCast(num_embedded_fields),
12801294
.is_tuple = is_tuple,
12811295
});
12821296
n.setType(node_t);
@@ -1341,10 +1355,15 @@ pub const Parser = struct {
13411355
return @ptrCast(try self.ast.dupeNodes(self.node_stack.items[field_start..]));
13421356
}
13431357

1344-
fn parseTypeFields(self: *Parser, req_indent: u32) ![]*ast.Field {
1358+
fn parseTypeFields(self: *Parser, req_indent: u32, out_num_embedded_fields: *usize) ![]*ast.Field {
1359+
var num_embedded_fields: usize = 0;
13451360
var field = (try self.parseStructField()) orelse {
1361+
out_num_embedded_fields.* = 0;
13461362
return &.{};
13471363
};
1364+
if (field.embedded) {
1365+
num_embedded_fields += 1;
1366+
}
13481367

13491368
const field_start = self.node_stack.items.len;
13501369
defer self.node_stack.items.len = field_start;
@@ -1359,8 +1378,12 @@ pub const Parser = struct {
13591378
field = (try self.parseStructField()) orelse {
13601379
break;
13611380
};
1381+
if (field.embedded) {
1382+
num_embedded_fields += 1;
1383+
}
13621384
try self.pushNode(@ptrCast(field));
13631385
}
1386+
out_num_embedded_fields.* = num_embedded_fields;
13641387
return @ptrCast(try self.ast.dupeNodes(self.node_stack.items[field_start..]));
13651388
}
13661389

@@ -1476,27 +1499,28 @@ pub const Parser = struct {
14761499
self.advance();
14771500
const fields = try self.parseTupleFields();
14781501
if (self.peek().tag() != .colon) {
1479-
return self.newStructDecl(start, ntype, name, config, &.{}, fields, true);
1502+
return self.newStructDecl(start, ntype, name, config, &.{}, fields, 0, true);
14801503
}
14811504

14821505
self.advance();
14831506
const req_indent = try self.parseFirstChildIndent(self.cur_indent);
14841507
const prev_indent = self.pushIndent(req_indent);
14851508
defer self.cur_indent = prev_indent;
14861509

1487-
return self.newStructDecl(start, ntype, name, config, &.{}, fields, true);
1510+
return self.newStructDecl(start, ntype, name, config, &.{}, fields, 0, true);
14881511
} else {
14891512
// Only declaration. No members.
1490-
return self.newStructDecl(start, ntype, name, config, &.{}, &.{}, false);
1513+
return self.newStructDecl(start, ntype, name, config, &.{}, &.{}, 0, false);
14911514
}
14921515

14931516
const req_indent = try self.parseFirstChildIndent(self.cur_indent);
14941517
const prev_indent = self.pushIndent(req_indent);
14951518
defer self.cur_indent = prev_indent;
14961519

14971520
const impls = try self.parseImplDecls(req_indent);
1498-
const fields = try self.parseTypeFields(req_indent);
1499-
return self.newStructDecl(start, ntype, name, config, impls, fields, false);
1521+
var num_embedded_fields: usize = 0;
1522+
const fields = try self.parseTypeFields(req_indent, &num_embedded_fields);
1523+
return self.newStructDecl(start, ntype, name, config, impls, fields, num_embedded_fields, false);
15001524
}
15011525

15021526
pub fn parse_with_decl(self: *Parser, start: u32, attrs: []*ast.Attribute) !*ast.Node {

src/sema.zig

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1997,6 +1997,16 @@ fn semaAccessFieldName(c: *cy.Chunk, rec_n: *ast.Node, rec: ExprResult, name: []
19971997
const mod = type_.sym().getMod();
19981998

19991999
const sym = mod.getSym(name) orelse {
2000+
if (type_.has_embeddings()) {
2001+
if (try get_embedded_sym(c, type_.cast(.struct_t), name, @ptrCast(field))) |embedded_res| {
2002+
if (embedded_res.sym.type == .field) {
2003+
const embedded_field = embedded_res.sym.cast(.field);
2004+
const final_rec = try semaField(c, rec, @ptrCast(field), embedded_res.field_idx, embedded_res.embedded_t, @ptrCast(field));
2005+
return semaField(c, final_rec, @ptrCast(field), embedded_field.idx, embedded_field.type, @ptrCast(field));
2006+
}
2007+
}
2008+
}
2009+
20002010
// TODO: This depends on @get being known, make sure @get is a nested declaration.
20012011

20022012
if (mod.getSym("@get")) |get_sym| {
@@ -4952,16 +4962,21 @@ pub fn lookupIdent(self: *cy.Chunk, name: []const u8, node: *ast.Node) !LookupId
49524962
return LookupIdentResult{
49534963
.capture = id,
49544964
};
4955-
} else {
4956-
const res = (try lookupStaticIdent(self, name, node)) orelse {
4957-
const resolve_ctx_idx = self.resolve_stack.items.len-1;
4958-
const ctx = self.resolve_stack.items[resolve_ctx_idx];
4959-
const search_c = ctx.chunk;
4960-
_ = search_c;
4961-
return self.reportErrorFmt("Undeclared variable `{}`.", &.{v(name)}, node);
4962-
};
4965+
}
4966+
4967+
if (try lookupStaticIdent(self, name, node)) |res| {
49634968
return res;
49644969
}
4970+
4971+
if (self.cur_sema_proc.isMethodBlock) {
4972+
const base_t = self.cur_sema_proc.func.?.sig.params()[0].get_type().getBaseType();
4973+
if (base_t.sym().getMod().getSym(name)) |sym| {
4974+
if (sym.type == .field) {
4975+
return self.reportErrorFmt("Expected explicit `${}` or `self.{}`.", &.{v(name), v(name)}, node);
4976+
}
4977+
}
4978+
}
4979+
return self.reportErrorFmt("Undeclared variable `{}`.", &.{v(name)}, node);
49654980
}
49664981

49674982
pub fn lookupStaticIdent(c: *cy.Chunk, name: []const u8, node: *ast.Node) !?LookupIdentResult {
@@ -7365,6 +7380,27 @@ pub fn semaExprNoCheck(c: *cy.Chunk, node: *ast.Node, cstr: Cstr) anyerror!ExprR
73657380
}
73667381
}
73677382

7383+
const EmbeddedResult = struct {
7384+
field_idx: usize,
7385+
embedded_t: *cy.Type,
7386+
sym: *cy.Sym,
7387+
};
7388+
7389+
fn get_embedded_sym(c: *cy.Chunk, rec_t: *cy.types.Struct, name: []const u8, node: *ast.Node) !?EmbeddedResult {
7390+
const fields = rec_t.getEmbeddedFields();
7391+
for (fields) |field| {
7392+
if (try c.getResolvedSym(&field.embedded_type.sym().head, name, node)) |embedded_sym| {
7393+
// TODO: Cache result into the receiver type's module as a `EmbeddedField`.
7394+
return .{
7395+
.field_idx = field.field_idx,
7396+
.embedded_t = field.embedded_type,
7397+
.sym = embedded_sym,
7398+
};
7399+
}
7400+
}
7401+
return null;
7402+
}
7403+
73687404
pub fn semaCallExpr(c: *cy.Chunk, node: *ast.Node, opt_target: ?*cy.Type) !ExprResult {
73697405
const call = node.cast(.callExpr);
73707406
if (call.hasNamedArg) {
@@ -7373,7 +7409,7 @@ pub fn semaCallExpr(c: *cy.Chunk, node: *ast.Node, opt_target: ?*cy.Type) !ExprR
73737409

73747410
if (call.callee.type() == .accessExpr) {
73757411
const callee = call.callee.cast(.accessExpr);
7376-
const leftRes = try c.semaExprSkipSym(callee.left, .{});
7412+
var leftRes = try c.semaExprSkipSym(callee.left, .{});
73777413

73787414
const right_name = callee.right.name();
73797415
if (leftRes.resType == .sym) {
@@ -7385,7 +7421,18 @@ pub fn semaCallExpr(c: *cy.Chunk, node: *ast.Node, opt_target: ?*cy.Type) !ExprR
73857421

73867422
if (leftSym.isValue()) {
73877423
// Look for sym under left type's module.
7388-
const rightSym = try c.accessResolvedSymOrFail(leftRes.type, right_name, callee.right);
7424+
const rightSym = (try c.accessResolvedSym(leftRes.type, right_name, callee.right)) orelse b: {
7425+
if (leftRes.type.has_embeddings()) {
7426+
if (try get_embedded_sym(c, leftRes.type.cast(.struct_t), right_name, callee.right)) |embedded_res| {
7427+
leftRes = try semaField(c, leftRes, callee.right, embedded_res.field_idx, embedded_res.embedded_t, callee.right);
7428+
break :b embedded_res.sym;
7429+
}
7430+
}
7431+
const type_name = try c.sema.allocTypeName(leftRes.type);
7432+
defer c.alloc.free(type_name);
7433+
return c.reportErrorFmt("Can not find the symbol `{}` in `{}`.", &.{v(right_name), v(type_name)}, callee.right);
7434+
};
7435+
73897436
if (rightSym.type == .func) {
73907437
// Call method.
73917438
const func_sym = rightSym.cast(.func);
@@ -7408,7 +7455,17 @@ pub fn semaCallExpr(c: *cy.Chunk, node: *ast.Node, opt_target: ?*cy.Type) !ExprR
74087455
}
74097456
} else {
74107457
// Look for sym under left type's module.
7411-
const rightSym = try c.accessResolvedSymOrFail(leftRes.type, right_name, callee.right);
7458+
const rightSym = (try c.accessResolvedSym(leftRes.type, right_name, callee.right)) orelse b: {
7459+
if (leftRes.type.has_embeddings()) {
7460+
if (try get_embedded_sym(c, leftRes.type.cast(.struct_t), right_name, callee.right)) |embedded_res| {
7461+
leftRes = try semaField(c, leftRes, callee.right, embedded_res.field_idx, embedded_res.embedded_t, callee.right);
7462+
break :b embedded_res.sym;
7463+
}
7464+
}
7465+
const type_name = try c.sema.allocTypeName(leftRes.type);
7466+
defer c.alloc.free(type_name);
7467+
return c.reportErrorFmt("Can not find the symbol `{}` in `{}`.", &.{v(right_name), v(type_name)}, callee.right);
7468+
};
74127469

74137470
if (rightSym.type == .func) {
74147471
return c.semaCallFuncSymRec(rightSym.cast(.func), callee.left, leftRes, call.args.slice(), false, node);

src/sema_type.zig

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,11 @@ pub fn resolveStructFields(c: *cy.Chunk, struct_t: *cy.types.Struct, decl: *ast.
823823
const fields = try c.alloc.alloc(cy.types.Field, decl.fields.len);
824824
errdefer c.alloc.free(fields);
825825

826+
// Track embedded fields separately
827+
var embedded_i: usize = 0;
828+
var embedded_fields = try c.alloc.alloc(cy.types.EmbeddedFieldInfo, decl.num_embedded_fields);
829+
errdefer c.alloc.free(embedded_fields);
830+
826831
var field_group_t: ?*cy.Type = null;
827832
var field_group_end: usize = undefined;
828833
for (decl.fields.slice(), 0..) |field, i| {
@@ -866,6 +871,23 @@ pub fn resolveStructFields(c: *cy.Chunk, struct_t: *cy.types.Struct, decl: *ast.
866871
alignment = member_alignment;
867872
}
868873

874+
// Validate embedded type is an object type
875+
if (field.embedded) {
876+
if (field_t.kind() != .struct_t) {
877+
return c.reportErrorFmt(
878+
"Embedded field must be a struct type, got {s}",
879+
&.{v(@tagName(field_t.kind()))},
880+
@ptrCast(field)
881+
);
882+
}
883+
884+
embedded_fields[embedded_i] = .{
885+
.field_idx = @intCast(i),
886+
.embedded_type = field_t,
887+
};
888+
embedded_i += 1;
889+
}
890+
869891
const sym = try c.declareField(@ptrCast(struct_t.base.sym()), fieldName, i, field_t, @ptrCast(field));
870892
fields[i] = .{
871893
.sym = @ptrCast(sym),
@@ -907,6 +929,8 @@ pub fn resolveStructFields(c: *cy.Chunk, struct_t: *cy.types.Struct, decl: *ast.
907929
struct_t.fields_ptr = fields.ptr;
908930
struct_t.fields_len = @intCast(fields.len);
909931
struct_t.field_state_len = field_state_len;
932+
struct_t.embedded_fields_ptr = embedded_fields.ptr;
933+
struct_t.embedded_fields_len = @intCast(embedded_fields.len);
910934
struct_t.resolving_struct = false;
911935

912936
struct_t.base.info.borrow_only = has_borrow_child;

src/types.zig

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ pub const Type = extern struct {
116116
}
117117
}
118118

119+
pub fn has_embeddings(self: *Type) bool {
120+
return self.kind() == .struct_t and self.cast(.struct_t).hasEmbeddings();
121+
}
122+
119123
pub fn isCZeroEligible(self: *Type) bool {
120124
switch (self.id()) {
121125
else => {
@@ -943,6 +947,15 @@ pub const Choice = extern struct {
943947
}
944948
};
945949

950+
/// Tracks embedded fields for automatic member surfacing
951+
pub const EmbeddedFieldInfo = struct {
952+
/// Index into the fields array
953+
field_idx: u32,
954+
955+
/// TypeId of the embedded type
956+
embedded_type: *cy.Type,
957+
};
958+
946959
/// TODO: Hash fields for static casting.
947960
pub const Struct = extern struct {
948961
base: Type = undefined,
@@ -952,6 +965,10 @@ pub const Struct = extern struct {
952965
fields_len: u32 = cy.NullId,
953966
fields_owned: bool = true,
954967

968+
// Embedded field tracking
969+
embedded_fields_ptr: [*]const EmbeddedFieldInfo = undefined,
970+
embedded_fields_len: u32 = 0,
971+
955972
/// Recursive. Field states only includes struct members.
956973
field_state_len: usize = 0,
957974

@@ -984,6 +1001,11 @@ pub const Struct = extern struct {
9841001
impl.deinit(alloc);
9851002
}
9861003
alloc.free(self.impls());
1004+
1005+
if (self.embedded_fields_len > 0) {
1006+
const embedded = self.embedded_fields_ptr[0..self.embedded_fields_len];
1007+
alloc.free(embedded);
1008+
}
9871009
}
9881010
}
9891011

@@ -1003,6 +1025,14 @@ pub const Struct = extern struct {
10031025
}
10041026
return false;
10051027
}
1028+
1029+
pub fn getEmbeddedFields(self: *Struct) []const EmbeddedFieldInfo {
1030+
return self.embedded_fields_ptr[0..self.embedded_fields_len];
1031+
}
1032+
1033+
pub fn hasEmbeddings(self: *Struct) bool {
1034+
return self.embedded_fields_len > 0;
1035+
}
10061036
};
10071037

10081038
pub const Impl = struct {

0 commit comments

Comments
 (0)