1const std = @import("std");
2
3const aro = @import("aro");
4
5const ast = @import("ast.zig");
6const Translator = @import("Translator.zig");
7
8const Scope = @This();
9
10pub const SymbolTable = std.array_hash_map.String(ast.Node);
11pub const AliasList = std.ArrayList(struct {
12 alias: []const u8,
13 name: []const u8,
14});
15
16/// Associates a container (structure or union) with its relevant member functions.
17pub const ContainerMemberFns = struct {
18 container_decl_ptr: *ast.Node,
19 member_fns: std.ArrayList(*ast.Payload.Func) = .empty,
20};
21pub const ContainerMemberFnsHashMap = std.array_hash_map.Custom(
22 aro.QualType,
23 ContainerMemberFns,
24 struct {
25 pub fn hash(self: @This(), key: aro.QualType) u32 {
26 const auto_hash = std.array_hash_map.getAutoHashFn(aro.QualType, @This());
27 return auto_hash(self, key.unqualified());
28 }
29
30 pub fn eql(self: @This(), a: aro.QualType, b: aro.QualType, b_index: usize) bool {
31 const auto_eql = std.array_hash_map.getAutoEqlFn(aro.QualType, @This());
32 return auto_eql(self, a.unqualified(), b.unqualified(), b_index);
33 }
34 },
35 false,
36);
37
38id: Id,
39parent: ?*Scope,
40
41pub const Id = enum {
42 block,
43 root,
44 condition,
45 loop,
46 do_loop,
47};
48
49/// Used for the scope of condition expressions, for example `if (cond)`.
50/// The block is lazily initialized because it is only needed for rare
51/// cases of comma operators being used.
52pub const Condition = struct {
53 base: Scope,
54 block: ?Block = null,
55
56 fn getBlockScope(cond: *Condition, t: *Translator) !*Block {
57 if (cond.block) |*b| return b;
58 cond.block = try Block.init(t, &cond.base, true);
59 return &cond.block.?;
60 }
61
62 pub fn deinit(cond: *Condition) void {
63 if (cond.block) |*b| b.deinit();
64 }
65};
66
67/// Represents an in-progress Node.Block. This struct is stack-allocated.
68/// When it is deinitialized, it produces an Node.Block which is allocated
69/// into the main arena.
70pub const Block = struct {
71 base: Scope,
72 translator: *Translator,
73 statements: std.ArrayList(ast.Node),
74 variables: AliasList,
75 mangle_count: u32 = 0,
76 label: ?[]const u8 = null,
77
78 /// By default all variables are discarded, since we do not know in advance if they
79 /// will be used. This maps the variable's name to the Discard payload, so that if
80 /// the variable is subsequently referenced we can indicate that the discard should
81 /// be skipped during the intermediate AST -> Zig AST render step.
82 variable_discards: std.array_hash_map.String(*ast.Payload.Discard),
83
84 /// When the block corresponds to a function, keep track of the return type
85 /// so that the return expression can be cast, if necessary
86 return_type: ?aro.QualType = null,
87
88 /// C static local variables are wrapped in a block-local struct. The struct
89 /// is named `mangle(static_local_ + name)` and the Zig variable within the
90 /// struct keeps the name of the C variable.
91 pub const static_local_prefix = "static_local";
92
93 /// C extern local variables are wrapped in a block-local struct. The struct
94 /// is named `mangle(extern_local + name)` and the Zig variable within the
95 /// struct keeps the name of the C variable.
96 pub const extern_local_prefix = "extern_local";
97
98 pub fn init(t: *Translator, parent: *Scope, labeled: bool) !Block {
99 var blk: Block = .{
100 .base = .{
101 .id = .block,
102 .parent = parent,
103 },
104 .translator = t,
105 .statements = .empty,
106 .variables = .empty,
107 .variable_discards = .empty,
108 };
109 if (labeled) {
110 blk.label = try blk.makeMangledName("blk");
111 }
112 return blk;
113 }
114
115 pub fn deinit(block: *Block) void {
116 block.statements.deinit(block.translator.gpa);
117 block.variables.deinit(block.translator.gpa);
118 block.variable_discards.deinit(block.translator.gpa);
119 block.* = undefined;
120 }
121
122 pub fn complete(block: *Block) !ast.Node {
123 const arena = block.translator.arena;
124 if (block.base.parent.?.id == .do_loop) {
125 // We reserve 1 extra statement if the parent is a do_loop. This is in case of
126 // do while, we want to put `if (cond) break;` at the end.
127 const alloc_len = block.statements.items.len + @intFromBool(block.base.parent.?.id == .do_loop);
128 var stmts = try arena.alloc(ast.Node, alloc_len);
129 stmts.len = block.statements.items.len;
130 @memcpy(stmts[0..block.statements.items.len], block.statements.items);
131 return ast.Node.Tag.block.create(arena, .{
132 .label = block.label,
133 .stmts = stmts,
134 });
135 }
136 if (block.statements.items.len == 0) return ast.Node.Tag.empty_block.init();
137 return ast.Node.Tag.block.create(arena, .{
138 .label = block.label,
139 .stmts = try arena.dupe(ast.Node, block.statements.items),
140 });
141 }
142
143 /// Given the desired name, return a name that does not shadow anything from outer scopes.
144 /// Inserts the returned name into the scope.
145 /// The name will not be visible to callers of getAlias.
146 pub fn reserveMangledName(block: *Block, name: []const u8) ![]const u8 {
147 return block.createMangledName(name, true, null);
148 }
149
150 /// Same as reserveMangledName, but enables the alias immediately.
151 pub fn makeMangledName(block: *Block, name: []const u8) ![]const u8 {
152 return block.createMangledName(name, false, null);
153 }
154
155 pub fn createMangledName(block: *Block, name: []const u8, reservation: bool, prefix_opt: ?[]const u8) ![]const u8 {
156 const arena = block.translator.arena;
157 const name_copy = try arena.dupe(u8, name);
158 const alias_base = if (prefix_opt) |prefix|
159 try std.fmt.allocPrint(arena, "{s}_{s}", .{ prefix, name })
160 else
161 name;
162 var proposed_name = alias_base;
163 while (block.contains(proposed_name)) {
164 block.mangle_count += 1;
165 proposed_name = try std.fmt.allocPrint(arena, "{s}_{d}", .{ alias_base, block.mangle_count });
166 }
167 const new_mangle = try block.variables.addOne(block.translator.gpa);
168 if (reservation) {
169 new_mangle.* = .{ .name = name_copy, .alias = name_copy };
170 } else {
171 new_mangle.* = .{ .name = name_copy, .alias = proposed_name };
172 }
173 return proposed_name;
174 }
175
176 fn getAlias(block: *Block, name: []const u8) ?[]const u8 {
177 for (block.variables.items) |p| {
178 if (std.mem.eql(u8, p.name, name))
179 return p.alias;
180 }
181 return block.base.parent.?.getAlias(name);
182 }
183
184 fn localContains(block: *Block, name: []const u8) bool {
185 for (block.variables.items) |p| {
186 if (std.mem.eql(u8, p.alias, name))
187 return true;
188 }
189 return false;
190 }
191
192 fn contains(block: *Block, name: []const u8) bool {
193 if (block.localContains(name))
194 return true;
195 return block.base.parent.?.contains(name);
196 }
197
198 pub fn discardVariable(block: *Block, name: []const u8) Translator.Error!void {
199 const gpa = block.translator.gpa;
200 const arena = block.translator.arena;
201 const name_node = try ast.Node.Tag.identifier.create(arena, name);
202 const discard = try ast.Node.Tag.discard.create(arena, .{ .should_skip = false, .value = name_node });
203 try block.statements.append(gpa, discard);
204 try block.variable_discards.putNoClobber(gpa, name, discard.castTag(.discard).?);
205 }
206};
207
208pub const Root = struct {
209 base: Scope,
210 translator: *Translator,
211 sym_table: SymbolTable,
212 blank_macros: std.array_hash_map.String(void),
213 nodes: std.ArrayList(ast.Node),
214 container_member_fns_map: ContainerMemberFnsHashMap,
215
216 pub fn init(t: *Translator) Root {
217 return .{
218 .base = .{
219 .id = .root,
220 .parent = null,
221 },
222 .translator = t,
223 .sym_table = .empty,
224 .blank_macros = .empty,
225 .nodes = .empty,
226 .container_member_fns_map = .empty,
227 };
228 }
229
230 pub fn deinit(root: *Root) void {
231 root.sym_table.deinit(root.translator.gpa);
232 root.blank_macros.deinit(root.translator.gpa);
233 root.nodes.deinit(root.translator.gpa);
234 for (root.container_member_fns_map.values()) |*members| {
235 members.member_fns.deinit(root.translator.gpa);
236 }
237 root.container_member_fns_map.deinit(root.translator.gpa);
238 }
239
240 /// Check if the global scope contains this name, without looking into the "future", e.g.
241 /// ignore the preprocessed decl and macro names.
242 pub fn containsNow(root: *Root, name: []const u8) bool {
243 return root.sym_table.contains(name);
244 }
245
246 /// Check if the global scope contains the name, includes all decls that haven't been translated yet.
247 pub fn contains(root: *Root, name: []const u8) bool {
248 return root.containsNow(name) or root.translator.global_names.contains(name) or root.translator.weak_global_names.contains(name);
249 }
250
251 pub fn addMemberFunction(root: *Root, func_ty: aro.Type.Func, func: *ast.Payload.Func) !void {
252 std.debug.assert(func.data.name != null);
253 if (func_ty.params.len == 0) return;
254
255 const param1_base = func_ty.params[0].qt.base(root.translator.comp);
256 const container_qt = if (param1_base.type == .pointer)
257 param1_base.type.pointer.child.base(root.translator.comp).qt
258 else
259 param1_base.qt;
260
261 if (root.container_member_fns_map.getPtr(container_qt)) |members| {
262 try members.member_fns.append(root.translator.gpa, func);
263 }
264 }
265
266 pub fn processContainerMemberFns(root: *Root) !void {
267 const gpa = root.translator.gpa;
268 const arena = root.translator.arena;
269
270 var member_names: std.array_hash_map.String(void) = .empty;
271 defer member_names.deinit(gpa);
272 for (root.container_member_fns_map.keys(), root.container_member_fns_map.values()) |container_qt, members| {
273 // Get the container name
274 const container_name = root.translator.unnamed_typedefs.get(container_qt) orelse
275 container_qt.getRecord(root.translator.comp).?.name.lookup(root.translator.comp);
276 std.debug.assert(container_name.len > 0);
277
278 member_names.clearRetainingCapacity();
279 const decls_ptr = switch (members.container_decl_ptr.tag()) {
280 .@"struct", .@"union" => blk_record: {
281 const payload: *ast.Payload.Container = @alignCast(@fieldParentPtr("base", members.container_decl_ptr.ptr_otherwise));
282 // Avoid duplication with field names
283 for (payload.data.fields) |field| {
284 try member_names.put(gpa, field.name, {});
285 }
286 break :blk_record &payload.data.decls;
287 },
288 .opaque_literal => blk_opaque: {
289 const container_decl = try ast.Node.Tag.@"opaque".create(arena, .{
290 .layout = .none,
291 .fields = &.{},
292 .decls = &.{},
293 });
294 members.container_decl_ptr.* = container_decl;
295 break :blk_opaque &container_decl.castTag(.@"opaque").?.data.decls;
296 },
297 else => continue,
298 };
299
300 const old_decls = decls_ptr.*;
301 const new_decls = try arena.alloc(ast.Node, old_decls.len + members.member_fns.items.len * 2);
302 @memcpy(new_decls[0..old_decls.len], old_decls);
303 // Assume the allocator of payload.data.decls is arena,
304 // so don't add arena.free(old_variables).
305 const func_ref_vars = new_decls[old_decls.len..];
306 var count: u32 = 0;
307
308 // Add members without mangling them - only fields may cause name conflicts
309 for (members.member_fns.items) |func| {
310 const func_name = func.data.name.?;
311 const member_name_slot = try member_names.getOrPutValue(gpa, func_name, {});
312 if (member_name_slot.found_existing) continue;
313 func_ref_vars[count] = try ast.Node.Tag.pub_var_simple.create(arena, .{
314 .name = func_name,
315 .init = try ast.Node.Tag.root_ref.create(arena, func_name),
316 });
317 count += 1;
318 }
319
320 for (members.member_fns.items) |func| {
321 const func_name = func.data.name.?;
322 const func_name_alias = blk: {
323 // Try multiple candidate prefixes to extract the alias
324 // 1. typedef struct { ... } foo; -> foo_get_bar() extracts "get_bar"
325 // 2. typedef struct _foo foo; -> foo_get_bar() extracts "get_bar"
326 const container_name_trimmed = std.mem.trimStart(u8, container_name, "_");
327 const suffix = std.mem.cutPrefix(u8, func_name, container_name_trimmed);
328 // Check suffix starts with '_' to avoid invalid aliases like "1_get_bar" from foo1_get_bar()
329 if (suffix) |alias| if (alias.len > 0 and alias[0] == '_') {
330 const alias_trimmed = std.mem.trimStart(u8, alias, "_");
331 if (alias_trimmed.len > 0) break :blk alias_trimmed;
332 };
333
334 // Doesn't match any prefix - fallback to trimming trailing underscores and using last segment
335 const func_name_trimmed = std.mem.trimEnd(u8, func_name, "_");
336 const last_idx = std.mem.findLast(u8, func_name_trimmed, "_") orelse continue;
337 break :blk func_name[last_idx + 1 ..];
338 };
339
340 // Skip if the alias conflicts with an existing type
341 if (root.contains(func_name_alias)) continue;
342 const member_name_slot = try member_names.getOrPutValue(gpa, func_name_alias, {});
343 if (member_name_slot.found_existing) continue;
344 func_ref_vars[count] = try ast.Node.Tag.pub_var_simple.create(arena, .{
345 .name = func_name_alias,
346 .init = try ast.Node.Tag.root_ref.create(arena, func_name),
347 });
348 count += 1;
349 }
350
351 decls_ptr.* = new_decls[0 .. old_decls.len + count];
352 }
353 }
354};
355
356pub fn findBlockScope(inner: *Scope, t: *Translator) !*Block {
357 var scope = inner;
358 while (true) {
359 switch (scope.id) {
360 .root => unreachable,
361 .block => return @fieldParentPtr("base", scope),
362 .condition => return @as(*Condition, @fieldParentPtr("base", scope)).getBlockScope(t),
363 else => scope = scope.parent.?,
364 }
365 }
366}
367
368pub fn findBlockReturnType(inner: *Scope) aro.QualType {
369 var scope = inner;
370 while (true) {
371 switch (scope.id) {
372 .root => unreachable,
373 .block => {
374 const block: *Block = @fieldParentPtr("base", scope);
375 if (block.return_type) |qt| return qt;
376 scope = scope.parent.?;
377 },
378 else => scope = scope.parent.?,
379 }
380 }
381}
382
383pub fn getAlias(scope: *Scope, name: []const u8) ?[]const u8 {
384 return switch (scope.id) {
385 .root => null,
386 .block => @as(*Block, @fieldParentPtr("base", scope)).getAlias(name),
387 .loop, .do_loop, .condition => scope.parent.?.getAlias(name),
388 };
389}
390
391fn contains(scope: *Scope, name: []const u8) bool {
392 return switch (scope.id) {
393 .root => @as(*Root, @fieldParentPtr("base", scope)).contains(name),
394 .block => @as(*Block, @fieldParentPtr("base", scope)).contains(name),
395 .loop, .do_loop, .condition => scope.parent.?.contains(name),
396 };
397}
398
399/// Appends a node to the first block scope if inside a function, or to the root tree if not.
400pub fn appendNode(inner: *Scope, node: ast.Node) !void {
401 var scope = inner;
402 while (true) {
403 switch (scope.id) {
404 .root => {
405 const root: *Root = @fieldParentPtr("base", scope);
406 return root.nodes.append(root.translator.gpa, node);
407 },
408 .block => {
409 const block: *Block = @fieldParentPtr("base", scope);
410 return block.statements.append(block.translator.gpa, node);
411 },
412 else => scope = scope.parent.?,
413 }
414 }
415}
416
417pub fn skipVariableDiscard(inner: *Scope, name: []const u8) void {
418 if (true) {
419 // TODO: due to 'local variable is never mutated' errors, we can
420 // only skip discards if a variable is used as an lvalue, which
421 // we don't currently have detection for in translate-c.
422 // Once #17584 is completed, perhaps we can do away with this
423 // logic entirely, and instead rely on render to fixup code.
424 return;
425 }
426 var scope = inner;
427 while (true) {
428 switch (scope.id) {
429 .root => return,
430 .block => {
431 const block: *Block = @fieldParentPtr("base", scope);
432 if (block.variable_discards.get(name)) |discard| {
433 discard.data.should_skip = true;
434 return;
435 }
436 },
437 else => {},
438 }
439 scope = scope.parent.?;
440 }
441}