From ffe664050347ccd36d6c4f07b65a4ed652ad4ed8 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 17 Mar 2026 01:56:39 +0300 Subject: [PATCH] implement @[soa] struct attribute for Structure of Arrays transformation (#26738) --- vlib/v2/gen/cleanc/soa.v | 139 ++++++++++++++++++++++++++ vlib/v2/gen/cleanc/struct.v | 4 + vlib/v2/gen/cleanc/tests/soa_struct.v | 13 +++ vlib/v2/types/checker.v | 21 ++++ vlib/v2/types/types.v | 1 + 5 files changed, 178 insertions(+) create mode 100644 vlib/v2/gen/cleanc/soa.v create mode 100644 vlib/v2/gen/cleanc/tests/soa_struct.v diff --git a/vlib/v2/gen/cleanc/soa.v b/vlib/v2/gen/cleanc/soa.v new file mode 100644 index 000000000..3217e71f4 --- /dev/null +++ b/vlib/v2/gen/cleanc/soa.v @@ -0,0 +1,139 @@ +// Copyright (c) 2026 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. + +// Structure of Arrays (SoA) code generation. +// +// When a struct is annotated with @[soa], the compiler generates a companion +// SoA container struct that stores each field in a separate contiguous array. +// This layout provides significantly better cache performance for batch +// operations that touch only a subset of fields (common in game math, ECS, +// particle systems, physics simulations, etc.). +// +// Example: +// @[soa] +// struct Particle { +// x f32 +// y f32 +// vx f32 +// vy f32 +// } +// +// Generates a companion type `Particle_SOA` with: +// struct Particle_SOA { +// int len; +// int cap; +// f32* x; +// f32* y; +// f32* vx; +// f32* vy; +// }; +// +// And helper functions: +// Particle_SOA Particle_SOA_new(int len, int cap); +// void Particle_SOA_push(Particle_SOA* soa, Particle val); +// Particle Particle_SOA_get(Particle_SOA soa, int i); +// void Particle_SOA_set(Particle_SOA* soa, int i, Particle val); +// void Particle_SOA_free(Particle_SOA* soa); +module cleanc + +import v2.types + +// gen_soa_companion generates the SoA container struct and helper functions +// for a struct annotated with @[soa]. +fn (mut g Gen) gen_soa_companion(name string, s types.Struct) { + soa_name := '${name}_SOA' + + // --- SoA container struct --- + g.sb.writeln('// SoA (Structure of Arrays) companion for ${name}') + g.sb.writeln('typedef struct {') + g.sb.writeln('\tint len;') + g.sb.writeln('\tint cap;') + for field in s.fields { + c_type := g.types_type_to_c(field.typ) + fname := escape_c_keyword(field.name) + g.sb.writeln('\t${c_type}* ${fname};') + } + g.sb.writeln('} ${soa_name};') + g.sb.writeln('') + + // --- new: allocate SoA container --- + g.sb.writeln('static inline ${soa_name} ${soa_name}_new(int len, int cap) {') + g.sb.writeln('\tif (cap < len) cap = len;') + g.sb.writeln('\t${soa_name} soa;') + g.sb.writeln('\tsoa.len = len;') + g.sb.writeln('\tsoa.cap = cap;') + for field in s.fields { + c_type := g.types_type_to_c(field.typ) + fname := escape_c_keyword(field.name) + g.sb.writeln('\tsoa.${fname} = (${c_type}*)calloc(cap, sizeof(${c_type}));') + } + g.sb.writeln('\treturn soa;') + g.sb.writeln('}') + g.sb.writeln('') + + // --- get: retrieve element at index as original struct --- + g.sb.writeln('static inline ${name} ${soa_name}_get(${soa_name} soa, int i) {') + g.sb.writeln('\treturn (${name}){') + for i, field in s.fields { + comma := if i < s.fields.len - 1 { ',' } else { '' } + fname := escape_c_keyword(field.name) + g.sb.writeln('\t\t.${fname} = soa.${fname}[i]${comma}') + } + g.sb.writeln('\t};') + g.sb.writeln('}') + g.sb.writeln('') + + // --- set: set element at index from original struct --- + g.sb.writeln('static inline void ${soa_name}_set(${soa_name}* soa, int i, ${name} val) {') + for field in s.fields { + fname := escape_c_keyword(field.name) + g.sb.writeln('\tsoa->${fname}[i] = val.${fname};') + } + g.sb.writeln('}') + g.sb.writeln('') + + // --- push: append element, growing capacity if needed --- + g.sb.writeln('static inline void ${soa_name}_push(${soa_name}* soa, ${name} val) {') + g.sb.writeln('\tif (soa->len >= soa->cap) {') + g.sb.writeln('\t\tint new_cap = soa->cap < 8 ? 8 : soa->cap * 2;') + for field in s.fields { + c_type := g.types_type_to_c(field.typ) + fname := escape_c_keyword(field.name) + g.sb.writeln('\t\tsoa->${fname} = (${c_type}*)realloc(soa->${fname}, new_cap * sizeof(${c_type}));') + } + g.sb.writeln('\t\tsoa->cap = new_cap;') + g.sb.writeln('\t}') + for field in s.fields { + fname := escape_c_keyword(field.name) + g.sb.writeln('\tsoa->${fname}[soa->len] = val.${fname};') + } + g.sb.writeln('\tsoa->len++;') + g.sb.writeln('}') + g.sb.writeln('') + + // --- pop: remove and return last element --- + g.sb.writeln('static inline ${name} ${soa_name}_pop(${soa_name}* soa) {') + g.sb.writeln('\tif (soa->len == 0) return (${name}){0};') + g.sb.writeln('\tsoa->len--;') + g.sb.writeln('\treturn (${name}){') + for i, field in s.fields { + comma := if i < s.fields.len - 1 { ',' } else { '' } + fname := escape_c_keyword(field.name) + g.sb.writeln('\t\t.${fname} = soa->${fname}[soa->len]${comma}') + } + g.sb.writeln('\t};') + g.sb.writeln('}') + g.sb.writeln('') + + // --- free: deallocate all field arrays --- + g.sb.writeln('static inline void ${soa_name}_free(${soa_name}* soa) {') + for field in s.fields { + fname := escape_c_keyword(field.name) + g.sb.writeln('\tfree(soa->${fname});') + } + g.sb.writeln('\tsoa->len = 0;') + g.sb.writeln('\tsoa->cap = 0;') + g.sb.writeln('}') + g.sb.writeln('') +} diff --git a/vlib/v2/gen/cleanc/struct.v b/vlib/v2/gen/cleanc/struct.v index c958a1f07..a2349a3a5 100644 --- a/vlib/v2/gen/cleanc/struct.v +++ b/vlib/v2/gen/cleanc/struct.v @@ -339,6 +339,10 @@ fn (mut g Gen) gen_struct_decl(node ast.StructDecl) { g.sb.writeln('#define ${name}_str(v) ${name}__str(v)') } g.sb.writeln('') + // Generate SoA (Structure of Arrays) companion struct and helpers for @[soa] structs + if env_struct.is_soa && env_struct.fields.len > 0 { + g.gen_soa_companion(name, env_struct) + } } fn (mut g Gen) gen_sum_type_decl(node ast.TypeDecl) { diff --git a/vlib/v2/gen/cleanc/tests/soa_struct.v b/vlib/v2/gen/cleanc/tests/soa_struct.v new file mode 100644 index 000000000..5137531e8 --- /dev/null +++ b/vlib/v2/gen/cleanc/tests/soa_struct.v @@ -0,0 +1,13 @@ +module main + +@[soa] +struct Vec2 { + x f32 + y f32 +} + +fn main() { + // Test that the SOA struct definition is generated + // The companion type Vec2_SOA should be available + println('soa_struct test: ok') +} diff --git a/vlib/v2/types/checker.v b/vlib/v2/types/checker.v index 892c6ad23..5e7e688d9 100644 --- a/vlib/v2/types/checker.v +++ b/vlib/v2/types/checker.v @@ -740,6 +740,7 @@ fn (mut c Checker) decl(decl ast.Stmt) { obj := Struct{ name: qualified_name generic_params: generic_params + is_soa: decl.attributes.has('soa') } mut typ := Type(obj) // TODO: proper @@ -2041,12 +2042,32 @@ fn (mut c Checker) process_pending_struct_decls() { pending.decl.pos) } } + // Detect @[soa] attribute + is_soa := pending.decl.attributes.has('soa') + if is_soa { + if pending.decl.is_union { + c.error_with_pos('`@[soa]` attribute cannot be used with unions', pending.decl.pos) + } + if pending.decl.embedded.len > 0 { + c.error_with_pos('`@[soa]` structs cannot have embedded structs', pending.decl.pos) + } + for field in fields { + match field.typ { + Primitive, Char, Rune, ISize, USize {} + else { + c.error_with_pos('`@[soa]` structs can only contain primitive numeric types, not `${field.typ.name()}`', + pending.decl.pos) + } + } + } + } mut update_scope := if pending.decl.language == .c { c.c_scope } else { pending.scope } if mut sd := update_scope.lookup(pending.decl.name) { if mut sd is Type { if mut sd is Struct { sd.fields = fields sd.embedded = embedded + sd.is_soa = is_soa } } } diff --git a/vlib/v2/types/types.v b/vlib/v2/types/types.v index 850b5fc4a..cd0274ad8 100644 --- a/vlib/v2/types/types.v +++ b/vlib/v2/types/types.v @@ -227,6 +227,7 @@ pub mut: fields []Field // fields map[string]Type // methods []Method + is_soa bool // @[soa] - Structure of Arrays layout for better cache performance } // TODO: -- 2.39.5