Skip to content

Commit 60f0acd

Browse files
authored
Merge pull request #11706 from ziglang/string-literal-interning
stage2: string literal interning
2 parents 8171972 + 953e277 commit 60f0acd

File tree

8 files changed

+244
-26
lines changed

8 files changed

+244
-26
lines changed

src/Module.zig

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,17 @@ import_table: std.StringArrayHashMapUnmanaged(*File) = .{},
7070
/// Keys are fully resolved file paths. This table owns the keys and values.
7171
embed_table: std.StringHashMapUnmanaged(*EmbedFile) = .{},
7272

73+
/// This is a temporary addition to stage2 in order to match stage1 behavior,
74+
/// however the end-game once the lang spec is settled will be to use a global
75+
/// InternPool for comptime memoized objects, making this behavior consistent across all types,
76+
/// not only string literals. Or, we might decide to not guarantee string literals
77+
/// to have equal comptime pointers, in which case this field can be deleted (perhaps
78+
/// the commit that introduced it can simply be reverted).
79+
/// This table uses an optional index so that when a Decl is destroyed, the string literal
80+
/// is still reclaimable by a future Decl.
81+
string_literal_table: std.HashMapUnmanaged(StringLiteralContext.Key, Decl.OptionalIndex, StringLiteralContext, std.hash_map.default_max_load_percentage) = .{},
82+
string_literal_bytes: std.ArrayListUnmanaged(u8) = .{},
83+
7384
/// The set of all the generic function instantiations. This is used so that when a generic
7485
/// function is called twice with the same comptime parameter arguments, both calls dispatch
7586
/// to the same function.
@@ -157,6 +168,39 @@ decls_free_list: std.ArrayListUnmanaged(Decl.Index) = .{},
157168

158169
global_assembly: std.AutoHashMapUnmanaged(Decl.Index, []u8) = .{},
159170

171+
pub const StringLiteralContext = struct {
172+
bytes: *std.ArrayListUnmanaged(u8),
173+
174+
pub const Key = struct {
175+
index: u32,
176+
len: u32,
177+
};
178+
179+
pub fn eql(self: @This(), a: Key, b: Key) bool {
180+
_ = self;
181+
return a.index == b.index and a.len == b.len;
182+
}
183+
184+
pub fn hash(self: @This(), x: Key) u64 {
185+
const x_slice = self.bytes.items[x.index..][0..x.len];
186+
return std.hash_map.hashString(x_slice);
187+
}
188+
};
189+
190+
pub const StringLiteralAdapter = struct {
191+
bytes: *std.ArrayListUnmanaged(u8),
192+
193+
pub fn eql(self: @This(), a_slice: []const u8, b: StringLiteralContext.Key) bool {
194+
const b_slice = self.bytes.items[b.index..][0..b.len];
195+
return mem.eql(u8, a_slice, b_slice);
196+
}
197+
198+
pub fn hash(self: @This(), adapted_key: []const u8) u64 {
199+
_ = self;
200+
return std.hash_map.hashString(adapted_key);
201+
}
202+
};
203+
160204
const MonomorphedFuncsSet = std.HashMapUnmanaged(
161205
*Fn,
162206
void,
@@ -507,7 +551,8 @@ pub const Decl = struct {
507551
decl.name = undefined;
508552
}
509553

510-
pub fn clearValues(decl: *Decl, gpa: Allocator) void {
554+
pub fn clearValues(decl: *Decl, mod: *Module) void {
555+
const gpa = mod.gpa;
511556
if (decl.getExternFn()) |extern_fn| {
512557
extern_fn.deinit(gpa);
513558
gpa.destroy(extern_fn);
@@ -521,6 +566,13 @@ pub const Decl = struct {
521566
gpa.destroy(variable);
522567
}
523568
if (decl.value_arena) |arena_state| {
569+
if (decl.owns_tv) {
570+
if (decl.val.castTag(.str_lit)) |str_lit| {
571+
mod.string_literal_table.getPtrContext(str_lit.data, .{
572+
.bytes = &mod.string_literal_bytes,
573+
}).?.* = .none;
574+
}
575+
}
524576
arena_state.promote(gpa).deinit();
525577
decl.value_arena = null;
526578
decl.has_tv = false;
@@ -2839,6 +2891,9 @@ pub fn deinit(mod: *Module) void {
28392891
mod.decls_free_list.deinit(gpa);
28402892
mod.allocated_decls.deinit(gpa);
28412893
mod.global_assembly.deinit(gpa);
2894+
2895+
mod.string_literal_table.deinit(gpa);
2896+
mod.string_literal_bytes.deinit(gpa);
28422897
}
28432898

28442899
pub fn destroyDecl(mod: *Module, decl_index: Decl.Index) void {
@@ -2857,7 +2912,7 @@ pub fn destroyDecl(mod: *Module, decl_index: Decl.Index) void {
28572912
if (decl.getInnerNamespace()) |namespace| {
28582913
namespace.destroyDecls(mod);
28592914
}
2860-
decl.clearValues(gpa);
2915+
decl.clearValues(mod);
28612916
}
28622917
decl.dependants.deinit(gpa);
28632918
decl.dependencies.deinit(gpa);
@@ -4034,7 +4089,7 @@ fn semaDecl(mod: *Module, decl_index: Decl.Index) !bool {
40344089
if (decl.getFunction()) |prev_func| {
40354090
prev_is_inline = prev_func.state == .inline_only;
40364091
}
4037-
decl.clearValues(gpa);
4092+
decl.clearValues(mod);
40384093
}
40394094

40404095
decl.ty = try decl_tv.ty.copy(decl_arena_allocator);
@@ -4080,7 +4135,7 @@ fn semaDecl(mod: *Module, decl_index: Decl.Index) !bool {
40804135
var type_changed = true;
40814136
if (decl.has_tv) {
40824137
type_changed = !decl.ty.eql(decl_tv.ty, mod);
4083-
decl.clearValues(gpa);
4138+
decl.clearValues(mod);
40844139
}
40854140

40864141
decl.owns_tv = false;
@@ -4694,7 +4749,7 @@ pub fn clearDecl(
46944749
if (decl.getInnerNamespace()) |namespace| {
46954750
try namespace.deleteAllDecls(mod, outdated_decls);
46964751
}
4697-
decl.clearValues(gpa);
4752+
decl.clearValues(mod);
46984753
}
46994754

47004755
if (decl.deletion_flag) {
@@ -5623,7 +5678,7 @@ pub fn populateTestFunctions(mod: *Module) !void {
56235678

56245679
// Since we are replacing the Decl's value we must perform cleanup on the
56255680
// previous value.
5626-
decl.clearValues(gpa);
5681+
decl.clearValues(mod);
56275682
decl.ty = new_ty;
56285683
decl.val = new_val;
56295684
decl.has_tv = true;

src/Sema.zig

Lines changed: 79 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3842,20 +3842,44 @@ fn zirStr(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Ins
38423842
fn addStrLit(sema: *Sema, block: *Block, zir_bytes: []const u8) CompileError!Air.Inst.Ref {
38433843
// `zir_bytes` references memory inside the ZIR module, which can get deallocated
38443844
// after semantic analysis is complete, for example in the case of the initialization
3845-
// expression of a variable declaration. We need the memory to be in the new
3846-
// anonymous Decl's arena.
3847-
var anon_decl = try block.startAnonDecl(LazySrcLoc.unneeded);
3848-
defer anon_decl.deinit();
3845+
// expression of a variable declaration.
3846+
const mod = sema.mod;
3847+
const gpa = sema.gpa;
3848+
const string_bytes = &mod.string_literal_bytes;
3849+
const StringLiteralAdapter = Module.StringLiteralAdapter;
3850+
const StringLiteralContext = Module.StringLiteralContext;
3851+
try string_bytes.ensureUnusedCapacity(gpa, zir_bytes.len);
3852+
const gop = try mod.string_literal_table.getOrPutContextAdapted(gpa, zir_bytes, StringLiteralAdapter{
3853+
.bytes = string_bytes,
3854+
}, StringLiteralContext{
3855+
.bytes = string_bytes,
3856+
});
3857+
if (!gop.found_existing) {
3858+
gop.key_ptr.* = .{
3859+
.index = @intCast(u32, string_bytes.items.len),
3860+
.len = @intCast(u32, zir_bytes.len),
3861+
};
3862+
string_bytes.appendSliceAssumeCapacity(zir_bytes);
3863+
gop.value_ptr.* = .none;
3864+
}
3865+
const decl_index = gop.value_ptr.unwrap() orelse di: {
3866+
var anon_decl = try block.startAnonDecl(LazySrcLoc.unneeded);
3867+
defer anon_decl.deinit();
38493868

3850-
const bytes = try anon_decl.arena().dupeZ(u8, zir_bytes);
3869+
const decl_index = try anon_decl.finish(
3870+
try Type.Tag.array_u8_sentinel_0.create(anon_decl.arena(), gop.key_ptr.len),
3871+
try Value.Tag.str_lit.create(anon_decl.arena(), gop.key_ptr.*),
3872+
0, // default alignment
3873+
);
38513874

3852-
const new_decl = try anon_decl.finish(
3853-
try Type.Tag.array_u8_sentinel_0.create(anon_decl.arena(), bytes.len),
3854-
try Value.Tag.bytes.create(anon_decl.arena(), bytes[0 .. bytes.len + 1]),
3855-
0, // default alignment
3856-
);
3875+
// Needed so that `Decl.clearValues` will additionally set the corresponding
3876+
// string literal table value back to `Decl.OptionalIndex.none`.
3877+
mod.declPtr(decl_index).owns_tv = true;
38573878

3858-
return sema.analyzeDeclRef(new_decl);
3879+
gop.value_ptr.* = decl_index.toOptional();
3880+
break :di decl_index;
3881+
};
3882+
return sema.analyzeDeclRef(decl_index);
38593883
}
38603884

38613885
fn zirInt(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
@@ -19762,6 +19786,35 @@ fn beginComptimePtrMutation(
1976219786
.ty = elem_ty,
1976319787
};
1976419788
},
19789+
.str_lit => {
19790+
// An array is memory-optimized to store a slice of bytes, but we are about
19791+
// to modify an individual field and the representation has to change.
19792+
// If we wanted to avoid this, there would need to be special detection
19793+
// elsewhere to identify when writing a value to an array element that is stored
19794+
// using the `str_lit` tag, and handle it without making a call to this function.
19795+
const arena = parent.beginArena(sema.mod);
19796+
defer parent.finishArena(sema.mod);
19797+
19798+
const str_lit = parent.val.castTag(.str_lit).?.data;
19799+
const dest_len = parent.ty.arrayLenIncludingSentinel();
19800+
const bytes = sema.mod.string_literal_bytes.items[str_lit.index..][0..str_lit.len];
19801+
const elems = try arena.alloc(Value, @intCast(usize, dest_len));
19802+
for (bytes) |byte, i| {
19803+
elems[i] = try Value.Tag.int_u64.create(arena, byte);
19804+
}
19805+
if (parent.ty.sentinel()) |sent_val| {
19806+
assert(elems.len == bytes.len + 1);
19807+
elems[bytes.len] = sent_val;
19808+
}
19809+
19810+
parent.val.* = try Value.Tag.aggregate.create(arena, elems);
19811+
19812+
return ComptimePtrMutationKit{
19813+
.decl_ref_mut = parent.decl_ref_mut,
19814+
.val = &elems[elem_ptr.index],
19815+
.ty = elem_ty,
19816+
};
19817+
},
1976519818
.repeated => {
1976619819
// An array is memory-optimized to store only a single element value, and
1976719820
// that value is understood to be the same for the entire length of the array.
@@ -20097,10 +20150,23 @@ fn beginComptimePtrLoad(
2009720150
}
2009820151
}
2009920152

20100-
deref.pointee = if (elem_ptr.index < check_len) TypedValue{
20153+
if (elem_ptr.index >= check_len) {
20154+
deref.pointee = null;
20155+
break :blk deref;
20156+
}
20157+
if (elem_ptr.index == check_len - 1) {
20158+
if (array_tv.ty.sentinel()) |sent| {
20159+
deref.pointee = TypedValue{
20160+
.ty = elem_ty,
20161+
.val = sent,
20162+
};
20163+
break :blk deref;
20164+
}
20165+
}
20166+
deref.pointee = TypedValue{
2010120167
.ty = elem_ty,
2010220168
.val = try array_tv.val.elemValue(sema.mod, sema.arena, elem_ptr.index),
20103-
} else null;
20169+
};
2010420170
break :blk deref;
2010520171
},
2010620172

src/TypedValue.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,11 @@ pub fn print(
295295
return writer.print(".{s}", .{ty.enumFieldName(val.castTag(.enum_field_index).?.data)});
296296
},
297297
.bytes => return writer.print("\"{}\"", .{std.zig.fmtEscapes(val.castTag(.bytes).?.data)}),
298+
.str_lit => {
299+
const str_lit = val.castTag(.str_lit).?.data;
300+
const bytes = mod.string_literal_bytes.items[str_lit.index..][0..str_lit.len];
301+
return writer.print("\"{}\"", .{std.zig.fmtEscapes(bytes)});
302+
},
298303
.repeated => {
299304
if (level == 0) {
300305
return writer.writeAll(".{ ... }");

src/codegen.zig

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,23 @@ pub fn generateSymbol(
203203
},
204204
.Array => switch (typed_value.val.tag()) {
205205
.bytes => {
206-
const payload = typed_value.val.castTag(.bytes).?;
206+
const bytes = typed_value.val.castTag(.bytes).?.data;
207207
const len = @intCast(usize, typed_value.ty.arrayLenIncludingSentinel());
208208
// The bytes payload already includes the sentinel, if any
209209
try code.ensureUnusedCapacity(len);
210-
code.appendSliceAssumeCapacity(payload.data[0..len]);
210+
code.appendSliceAssumeCapacity(bytes[0..len]);
211+
return Result{ .appended = {} };
212+
},
213+
.str_lit => {
214+
const str_lit = typed_value.val.castTag(.str_lit).?.data;
215+
const mod = bin_file.options.module.?;
216+
const bytes = mod.string_literal_bytes.items[str_lit.index..][0..str_lit.len];
217+
try code.ensureUnusedCapacity(bytes.len + 1);
218+
code.appendSliceAssumeCapacity(bytes);
219+
if (typed_value.ty.sentinel()) |sent_val| {
220+
const byte = @intCast(u8, sent_val.toUnsignedInt(target));
221+
code.appendAssumeCapacity(byte);
222+
}
211223
return Result{ .appended = {} };
212224
},
213225
.aggregate => {

src/codegen/llvm.zig

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2936,9 +2936,39 @@ pub const DeclGen = struct {
29362936
return dg.context.constString(
29372937
bytes.ptr,
29382938
@intCast(c_uint, tv.ty.arrayLenIncludingSentinel()),
2939-
.True, // don't null terminate. bytes has the sentinel, if any.
2939+
.True, // Don't null terminate. Bytes has the sentinel, if any.
29402940
);
29412941
},
2942+
.str_lit => {
2943+
const str_lit = tv.val.castTag(.str_lit).?.data;
2944+
const bytes = dg.module.string_literal_bytes.items[str_lit.index..][0..str_lit.len];
2945+
if (tv.ty.sentinel()) |sent_val| {
2946+
const byte = @intCast(u8, sent_val.toUnsignedInt(target));
2947+
if (byte == 0 and bytes.len > 0) {
2948+
return dg.context.constString(
2949+
bytes.ptr,
2950+
@intCast(c_uint, bytes.len),
2951+
.False, // Yes, null terminate.
2952+
);
2953+
}
2954+
var array = std.ArrayList(u8).init(dg.gpa);
2955+
defer array.deinit();
2956+
try array.ensureUnusedCapacity(bytes.len + 1);
2957+
array.appendSliceAssumeCapacity(bytes);
2958+
array.appendAssumeCapacity(byte);
2959+
return dg.context.constString(
2960+
array.items.ptr,
2961+
@intCast(c_uint, array.items.len),
2962+
.True, // Don't null terminate.
2963+
);
2964+
} else {
2965+
return dg.context.constString(
2966+
bytes.ptr,
2967+
@intCast(c_uint, bytes.len),
2968+
.True, // Don't null terminate. `bytes` has the sentinel, if any.
2969+
);
2970+
}
2971+
},
29422972
.aggregate => {
29432973
const elem_vals = tv.val.castTag(.aggregate).?.data;
29442974
const elem_ty = tv.ty.elemType();

0 commit comments

Comments
 (0)