Skip to content

Commit ffe6640

Browse files
authored
implement @[soa] struct attribute for Structure of Arrays transformation (#26738)
1 parent e21acca commit ffe6640

5 files changed

Lines changed: 178 additions & 0 deletions

File tree

‎vlib/v2/gen/cleanc/soa.v‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
2+
// Use of this source code is governed by an MIT license
3+
// that can be found in the LICENSE file.
4+
5+
// Structure of Arrays (SoA) code generation.
6+
//
7+
// When a struct is annotated with @[soa], the compiler generates a companion
8+
// SoA container struct that stores each field in a separate contiguous array.
9+
// This layout provides significantly better cache performance for batch
10+
// operations that touch only a subset of fields (common in game math, ECS,
11+
// particle systems, physics simulations, etc.).
12+
//
13+
// Example:
14+
// @[soa]
15+
// struct Particle {
16+
// x f32
17+
// y f32
18+
// vx f32
19+
// vy f32
20+
// }
21+
//
22+
// Generates a companion type `Particle_SOA` with:
23+
// struct Particle_SOA {
24+
// int len;
25+
// int cap;
26+
// f32* x;
27+
// f32* y;
28+
// f32* vx;
29+
// f32* vy;
30+
// };
31+
//
32+
// And helper functions:
33+
// Particle_SOA Particle_SOA_new(int len, int cap);
34+
// void Particle_SOA_push(Particle_SOA* soa, Particle val);
35+
// Particle Particle_SOA_get(Particle_SOA soa, int i);
36+
// void Particle_SOA_set(Particle_SOA* soa, int i, Particle val);
37+
// void Particle_SOA_free(Particle_SOA* soa);
38+
module cleanc
39+
40+
import v2.types
41+
42+
// gen_soa_companion generates the SoA container struct and helper functions
43+
// for a struct annotated with @[soa].
44+
fn (mut g Gen) gen_soa_companion(name string, s types.Struct) {
45+
soa_name := '${name}_SOA'
46+
47+
// --- SoA container struct ---
48+
g.sb.writeln('// SoA (Structure of Arrays) companion for ${name}')
49+
g.sb.writeln('typedef struct {')
50+
g.sb.writeln('\tint len;')
51+
g.sb.writeln('\tint cap;')
52+
for field in s.fields {
53+
c_type := g.types_type_to_c(field.typ)
54+
fname := escape_c_keyword(field.name)
55+
g.sb.writeln('\t${c_type}* ${fname};')
56+
}
57+
g.sb.writeln('} ${soa_name};')
58+
g.sb.writeln('')
59+
60+
// --- new: allocate SoA container ---
61+
g.sb.writeln('static inline ${soa_name} ${soa_name}_new(int len, int cap) {')
62+
g.sb.writeln('\tif (cap < len) cap = len;')
63+
g.sb.writeln('\t${soa_name} soa;')
64+
g.sb.writeln('\tsoa.len = len;')
65+
g.sb.writeln('\tsoa.cap = cap;')
66+
for field in s.fields {
67+
c_type := g.types_type_to_c(field.typ)
68+
fname := escape_c_keyword(field.name)
69+
g.sb.writeln('\tsoa.${fname} = (${c_type}*)calloc(cap, sizeof(${c_type}));')
70+
}
71+
g.sb.writeln('\treturn soa;')
72+
g.sb.writeln('}')
73+
g.sb.writeln('')
74+
75+
// --- get: retrieve element at index as original struct ---
76+
g.sb.writeln('static inline ${name} ${soa_name}_get(${soa_name} soa, int i) {')
77+
g.sb.writeln('\treturn (${name}){')
78+
for i, field in s.fields {
79+
comma := if i < s.fields.len - 1 { ',' } else { '' }
80+
fname := escape_c_keyword(field.name)
81+
g.sb.writeln('\t\t.${fname} = soa.${fname}[i]${comma}')
82+
}
83+
g.sb.writeln('\t};')
84+
g.sb.writeln('}')
85+
g.sb.writeln('')
86+
87+
// --- set: set element at index from original struct ---
88+
g.sb.writeln('static inline void ${soa_name}_set(${soa_name}* soa, int i, ${name} val) {')
89+
for field in s.fields {
90+
fname := escape_c_keyword(field.name)
91+
g.sb.writeln('\tsoa->${fname}[i] = val.${fname};')
92+
}
93+
g.sb.writeln('}')
94+
g.sb.writeln('')
95+
96+
// --- push: append element, growing capacity if needed ---
97+
g.sb.writeln('static inline void ${soa_name}_push(${soa_name}* soa, ${name} val) {')
98+
g.sb.writeln('\tif (soa->len >= soa->cap) {')
99+
g.sb.writeln('\t\tint new_cap = soa->cap < 8 ? 8 : soa->cap * 2;')
100+
for field in s.fields {
101+
c_type := g.types_type_to_c(field.typ)
102+
fname := escape_c_keyword(field.name)
103+
g.sb.writeln('\t\tsoa->${fname} = (${c_type}*)realloc(soa->${fname}, new_cap * sizeof(${c_type}));')
104+
}
105+
g.sb.writeln('\t\tsoa->cap = new_cap;')
106+
g.sb.writeln('\t}')
107+
for field in s.fields {
108+
fname := escape_c_keyword(field.name)
109+
g.sb.writeln('\tsoa->${fname}[soa->len] = val.${fname};')
110+
}
111+
g.sb.writeln('\tsoa->len++;')
112+
g.sb.writeln('}')
113+
g.sb.writeln('')
114+
115+
// --- pop: remove and return last element ---
116+
g.sb.writeln('static inline ${name} ${soa_name}_pop(${soa_name}* soa) {')
117+
g.sb.writeln('\tif (soa->len == 0) return (${name}){0};')
118+
g.sb.writeln('\tsoa->len--;')
119+
g.sb.writeln('\treturn (${name}){')
120+
for i, field in s.fields {
121+
comma := if i < s.fields.len - 1 { ',' } else { '' }
122+
fname := escape_c_keyword(field.name)
123+
g.sb.writeln('\t\t.${fname} = soa->${fname}[soa->len]${comma}')
124+
}
125+
g.sb.writeln('\t};')
126+
g.sb.writeln('}')
127+
g.sb.writeln('')
128+
129+
// --- free: deallocate all field arrays ---
130+
g.sb.writeln('static inline void ${soa_name}_free(${soa_name}* soa) {')
131+
for field in s.fields {
132+
fname := escape_c_keyword(field.name)
133+
g.sb.writeln('\tfree(soa->${fname});')
134+
}
135+
g.sb.writeln('\tsoa->len = 0;')
136+
g.sb.writeln('\tsoa->cap = 0;')
137+
g.sb.writeln('}')
138+
g.sb.writeln('')
139+
}

‎vlib/v2/gen/cleanc/struct.v‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,10 @@ fn (mut g Gen) gen_struct_decl(node ast.StructDecl) {
339339
g.sb.writeln('#define ${name}_str(v) ${name}__str(v)')
340340
}
341341
g.sb.writeln('')
342+
// Generate SoA (Structure of Arrays) companion struct and helpers for @[soa] structs
343+
if env_struct.is_soa && env_struct.fields.len > 0 {
344+
g.gen_soa_companion(name, env_struct)
345+
}
342346
}
343347

344348
fn (mut g Gen) gen_sum_type_decl(node ast.TypeDecl) {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module main
2+
3+
@[soa]
4+
struct Vec2 {
5+
x f32
6+
y f32
7+
}
8+
9+
fn main() {
10+
// Test that the SOA struct definition is generated
11+
// The companion type Vec2_SOA should be available
12+
println('soa_struct test: ok')
13+
}

‎vlib/v2/types/checker.v‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,7 @@ fn (mut c Checker) decl(decl ast.Stmt) {
740740
obj := Struct{
741741
name: qualified_name
742742
generic_params: generic_params
743+
is_soa: decl.attributes.has('soa')
743744
}
744745
mut typ := Type(obj)
745746
// TODO: proper
@@ -2041,12 +2042,32 @@ fn (mut c Checker) process_pending_struct_decls() {
20412042
pending.decl.pos)
20422043
}
20432044
}
2045+
// Detect @[soa] attribute
2046+
is_soa := pending.decl.attributes.has('soa')
2047+
if is_soa {
2048+
if pending.decl.is_union {
2049+
c.error_with_pos('`@[soa]` attribute cannot be used with unions', pending.decl.pos)
2050+
}
2051+
if pending.decl.embedded.len > 0 {
2052+
c.error_with_pos('`@[soa]` structs cannot have embedded structs', pending.decl.pos)
2053+
}
2054+
for field in fields {
2055+
match field.typ {
2056+
Primitive, Char, Rune, ISize, USize {}
2057+
else {
2058+
c.error_with_pos('`@[soa]` structs can only contain primitive numeric types, not `${field.typ.name()}`',
2059+
pending.decl.pos)
2060+
}
2061+
}
2062+
}
2063+
}
20442064
mut update_scope := if pending.decl.language == .c { c.c_scope } else { pending.scope }
20452065
if mut sd := update_scope.lookup(pending.decl.name) {
20462066
if mut sd is Type {
20472067
if mut sd is Struct {
20482068
sd.fields = fields
20492069
sd.embedded = embedded
2070+
sd.is_soa = is_soa
20502071
}
20512072
}
20522073
}

‎vlib/v2/types/types.v‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ pub mut:
227227
fields []Field
228228
// fields map[string]Type
229229
// methods []Method
230+
is_soa bool // @[soa] - Structure of Arrays layout for better cache performance
230231
}
231232

232233
// TODO:

0 commit comments

Comments
 (0)