Files
doslang-mirror/fec/std/intern.fe
T
coolguy b7ce16f65e GOAL P3-4: StringInterner, 그리고 그것이 드러낸 배치 버그
이름 하나당 사본 하나와 그것을 대신하는 숫자. 컴파일러는 이름을 끊임없이
비교하고 사방에 저장하는데, StrId 둘을 비교하는 것은 정수 둘을 비교하는
것이고 하나를 저장하는 것은 4바이트에 소유권 없음이다.

str 을 꺼내는 API 를 일부러 두지 않았다. 텍스트를 빌리는 것은 interner 를
빌리는 것인데, interner 는 이름을 든 채로 계속 더 넣고 싶은 바로 그 물건이다
-- 파서는 식별자를 읽으면서 같은 숨에 다음 것을 등록한다. 텍스트를 열어서
하려던 일은 전부 여기 있다: eq, len_of, hash_of, find, copy_into.

그리고 이것이 제네릭 인스턴스를 필드로 담는 구조체를 통째로 깨뜨리던 버그를
드러냈다.

  Holder{ bytes: ^[]mut u8, used: usize, seen: map.Map(u32) }
  36 바이트여야 하는데 12 로 잡혔다.

Map(u32) 를 짓는 중에 그 안의 Slot(u32) 를 인스턴스화하면 거기서 배치 패스가
다시 돈다. 그때 Map(u32) 는 field_count 는 4 인데 필드 배열이 아직 아무것도
말하지 않는 상태라, 크기 0 으로 확정되고 굳었다. 이미 크기가 있는 타입은
아무도 다시 계산하지 않으니 Holder 는 그 0 을 읽었다.

셋을 고쳤다: 짓는 중인 인스턴스는 building 을 세워 배치를 거절하고, 멤버가
아직 자리를 못 잡은 집합 타입은 틀린 답으로 굳느니 물러나며, 배치 패스는
움직임이 없을 때까지 돈다.

245/245, 38/38.
2026-08-17 16:27:47 +09:00

132 lines
4.5 KiB
Plaintext

unit std.intern;
import std.map;
import std.mem;
// One copy of every distinct name, and a number that stands for it.
//
// A compiler compares names constantly and stores them everywhere. Comparing
// two `StrId` is comparing two integers; storing one costs four bytes and no
// ownership. That is the whole point.
//
// There is deliberately no way to get a `str` back out. A borrow of the text
// would be a borrow of the interner, and the interner is exactly the thing you
// want to keep adding to while holding names -- the parser reads an identifier
// and registers the next one in the same breath. Everything you would open the
// text for is here instead: compare it, measure it, hash it, write it.
/// A name, as a number. Copy, four bytes, and meaningless to any other
/// interner -- which is fine, because a program has one.
pub struct StrId {
pub raw: u32,
pub fn same(self: &Self, other: StrId) -> bool {
return self.raw == other.raw;
}
}
/// No name.
pub const NONE: u32 = 4294967295;
struct Entry {
at: usize,
len: usize,
}
pub struct Interner {
/// Every name end to end. Nothing is ever removed, so an offset stays
/// good for as long as the interner does.
bytes: ^[]mut u8,
used: usize,
names: ^[]mut Entry,
count: usize,
/// Text to id, so interning the same name twice gives the same number.
seen: map.Map(u32),
pub fn with_capacity(n: usize) -> !Self {
let text: ^[]mut u8 = try mem.alloc_slice(u8, n * 8);
let table: ^[]mut Entry = try mem.alloc_slice(Entry, n);
let index: map.Map(u32) = try map.Map(u32).with_capacity(n);
return Self{ bytes: text, used: 0, names: table, count: 0,
seen: index };
}
pub fn count_of(self: &Self) -> usize { return self.count; }
/// The number for this name, making one if it is new.
pub fn intern(self: &mut Self, text: []u8) -> !StrId {
let found: u32 = self.seen.get(text, NONE);
if found != NONE { return StrId{ raw: found }; }
if self.count == self.names.^.n { return error.OutOfMemory; }
let at: usize = self.used;
if at + text.n > self.bytes.^.n { return error.OutOfMemory; }
var i: usize = 0;
while i < text.n {
self.bytes.^[at + i] = text[i];
i = i + 1;
}
let id: usize = self.count;
self.names.^[id].at = at;
self.names.^[id].len = text.n;
self.used = at + text.n;
self.count = id + 1;
try self.seen.put(text, id as u32);
return StrId{ raw: id as u32 };
}
/// Is this a name it has seen? `NONE` when not.
pub fn find(self: &Self, text: []u8) -> u32 {
return self.seen.get(text, NONE);
}
pub fn len_of(self: &Self, id: StrId) -> usize {
if (id.raw as usize) >= self.count { return 0; }
return self.names.^[id.raw as usize].len;
}
/// Does this id spell this text? The comparison every `if name == "fn"`
/// in a parser turns into.
pub fn eq(self: &Self, id: StrId, text: []u8) -> bool {
if (id.raw as usize) >= self.count { return false; }
let e: usize = id.raw as usize;
if self.names.^[e].len != text.n { return false; }
var i: usize = 0;
while i < text.n {
if self.bytes.^[self.names.^[e].at + i] != text[i] { return false; }
i = i + 1;
}
return true;
}
/// FNV-1a over the stored bytes, for anything that wants to bucket names
/// without opening them.
pub fn hash_of(self: &Self, id: StrId) -> u32 {
if (id.raw as usize) >= self.count { return 0; }
let e: usize = id.raw as usize;
var h: u32 = 2166136261;
var i: usize = 0;
while i < self.names.^[e].len {
h = h ^ (self.bytes.^[self.names.^[e].at + i] as u32);
h = h * 16777619;
i = i + 1;
}
return h;
}
/// Copy the name into `out` and say how many bytes it took. This is how a
/// name reaches a diagnostic without the interner being borrowed past the
/// statement.
pub fn copy_into(self: &Self, id: StrId, out: []mut u8) -> usize {
if (id.raw as usize) >= self.count { return 0; }
let e: usize = id.raw as usize;
var n: usize = self.names.^[e].len;
if n > out.n { n = out.n; }
var i: usize = 0;
while i < n {
out[i] = self.bytes.^[self.names.^[e].at + i];
i = i + 1;
}
return n;
}
}