std: map 을 쓴다 -- 바이트 열에서 값으로

컴파일러는 이름을 끊임없이 찾는데 리스트 선형 탐색은 그 모양이 아니다. 키는
맵이 소유하는 한 버퍼에 복사되고 슬롯은 그 안의 어디인지만 적는다 -- R11 이
말하는 아레나와 핸들 모양이고, 그래서 맵을 놓는 것이 엔트리마다 하나가 아니라
두 번의 해제다.

개방 주소법에 선형 탐사. 표는 2의 거듭제곱이라 나눗셈이 아니라 마스크이고,
탐사가 길어지는 것이 표가 차는 것보다 먼저라 3/4 에서 자란다.

길에서 고친 것 넷:

- 인스턴스를 만들 때 선언 유닛으로 전환하지 않아서, 필드 타입 Slot(V) 를
  호출자 유닛에서 찾고 있었다.
- mem.alloc_slice 가 원소 타입으로 단순한 이름만 받았다. Slot(V) 같은
  인스턴스도 받는다.
- 쓸 수 있는지를 바인딩이 아니라 소유된 것이 정한다. let p: ^[]mut T 는 p 를
  고정하고 그것이 소유한 것은 쓸 수 있게 둔다. 슬라이스 인덱스도 마찬가지다.
- 메서드 인자에 자유 함수와 같은 호출 한정 약화가 없었다.

알게 된 것: 대여는 루트 단위라 self 의 한 필드에 쓰는 동안 다른 필드를 읽을 수
없다. 지역으로 빼거나 메서드를 나누면 되지만, 필드 단위 대여가 있으면 훨씬
편할 자리다.

  count 5 / fn 2 let 3 missing 0 / grown 64 / after 40 / balanced

218/218, 28/28.
This commit is contained in:
2026-08-17 12:23:52 +09:00
parent c7cba061b3
commit 8c4e80e246
4 changed files with 265 additions and 9 deletions
+38 -7
View File
@@ -142,9 +142,20 @@ int lvalue_writable(FeCheckerState *s, FeNode *n)
t=n->a ? n->a->sem_type : 0; t=n->a ? n->a->sem_type : 0;
if (t && t->kind==FE_TYPE_REF && n->b && n->b->text && if (t && t->kind==FE_TYPE_REF && n->b && n->b->text &&
strcmp(n->b->text,"^")==0) return t->ref_mut; strcmp(n->b->text,"^")==0) return t->ref_mut;
/* Through an owner, what may be written is decided by what is owned,
not by whether the binding may be pointed somewhere else. `let p:
^[]mut T` fixes p and leaves what it owns writable. */
if (t && t->kind==FE_TYPE_OWNED && n->b && n->b->text &&
strcmp(n->b->text,"^")==0)
return !t->elem || t->elem->kind!=FE_TYPE_SLICE || t->elem->ref_mut;
return lvalue_writable(s,n->a);
}
if (n->kind == FE_N_INDEX) {
/* An index into a slice asks the slice, not the binding. */
t=n->a ? n->a->sem_type : 0;
if (t && t->kind==FE_TYPE_SLICE) return t->ref_mut;
return lvalue_writable(s,n->a); return lvalue_writable(s,n->a);
} }
if (n->kind == FE_N_INDEX) return lvalue_writable(s,n->a);
return 0; return 0;
} }
@@ -440,10 +451,16 @@ FeType *check_expr_core(FeCheckerState *s, FeNode *n)
if (strcmp(n->a->b->text,"alloc_slice")==0) { if (strcmp(n->a->b->text,"alloc_slice")==0) {
FeNode *count=arg ? arg->next : 0; FeNode *count=arg ? arg->next : 0;
FeType *item; FeType *item;
if(!arg || arg->kind!=FE_N_IDENT || !count || count->next) {
err(c,n->loc,"mem.alloc_slice requires a type and length"); /* The element type may be an instance -- `Slot(V)` -- and
item=arg && arg->kind==FE_N_IDENT ? not just a name. */
fe_type_intern(&c->types,arg->text) : unknown(c); int named=0;
item=arg ? type_from_expr(s,arg,&named) : unknown(c);
if(!arg || !named || !count || count->next) {
err(c,n->loc,"mem.alloc_slice requires a type and length");
item=unknown(c);
}
}
b=count ? check_expr(s,count) : unknown(c); b=count ? check_expr(s,count) : unknown(c);
if(known(b) && !fe_type_is_integer(b)) if(known(b) && !fe_type_is_integer(b))
err(c,count->loc,"slice length must be an integer"); err(c,count->loc,"slice length must be an integer");
@@ -578,9 +595,23 @@ FeType *check_expr_core(FeCheckerState *s, FeNode *n)
while(param && arg) { while(param && arg) {
a=check_expr(s,arg); a=check_expr(s,arg);
b=method_type(c,param->a,et); b=method_type(c,param->a,et);
if(!compatible(b,a,arg) && a->kind!=FE_TYPE_UNKNOWN) /* A method argument gets the same call-only weakening a
free function's does: an exclusive view may be handed
over as a shared one for the length of the call, and an
exclusive borrow is lent rather than given. */
if(!compatible(b,a,arg) &&
!(b && a && b->kind==FE_TYPE_SLICE &&
a->kind==FE_TYPE_SLICE && !b->ref_mut && a->ref_mut &&
fe_type_equal(b->elem,a->elem)) &&
!(b && a && b->kind==FE_TYPE_REF && a->kind==FE_TYPE_REF &&
!b->ref_mut && a->ref_mut &&
fe_type_equal(b->elem,a->elem)) &&
a->kind!=FE_TYPE_UNKNOWN)
err(c,arg->loc,"method argument type mismatch"); err(c,arg->loc,"method argument type mismatch");
mark_moved(s,arg,a); if(!call_reborrows(b,a) &&
!(b && a && b->kind==FE_TYPE_SLICE &&
a->kind==FE_TYPE_SLICE && !b->ref_mut && a->ref_mut))
mark_moved(s,arg,a);
param=param->next; param=param->next;
arg=arg->next; arg=arg->next;
} }
+5
View File
@@ -226,9 +226,13 @@ FeType *build_struct_instance(FeCheck *c, FeUnit *home, FeNode *decl,
for (f=decl->children;f;f=f->next) if (f->kind==FE_N_FIELD) ++fields; for (f=decl->children;f;f=f->next) if (f->kind==FE_N_FIELD) ++fields;
t->field_count=fields; t->field_count=fields;
if (fields) { if (fields) {
const char *save_unit=c->types.unit_name;
t->fields=(FeFieldType *)fe_arena_alloc(&c->arena, t->fields=(FeFieldType *)fe_arena_alloc(&c->arena,
fields*sizeof(FeFieldType)); fields*sizeof(FeFieldType));
if (!t->fields) { t->field_count=0; return t; } if (!t->fields) { t->field_count=0; return t; }
/* Field types are written in the unit that declared the struct, not in
whichever unit asked for this instance. */
c->types.unit_name=home->name;
push_instance_bindings(c,&save,t); push_instance_bindings(c,&save,t);
bind_self(c,t); bind_self(c,t);
i=0; i=0;
@@ -240,6 +244,7 @@ FeType *build_struct_instance(FeCheck *c, FeUnit *home, FeNode *decl,
++i; ++i;
} }
pop_bindings(c,&save); pop_bindings(c,&save);
c->types.unit_name=save_unit;
} }
fe_type_layout_all(&c->types); fe_type_layout_all(&c->types);
/* A type that says how to let go of itself needs that method to exist for /* A type that says how to let go of itself needs that method to exist for
+174 -2
View File
@@ -1,4 +1,176 @@
unit map; unit std.map;
pub struct Map(K, V) {
// A table from a run of bytes to a value.
//
// A compiler looks names up constantly and a linear scan over a list is the
// wrong shape for that. Keys are copied into one buffer the map owns and each
// slot records where in it the key sits -- the arena-and-handle shape R11 asks
// for, which also means letting go of the map is two frees and not one per
// entry.
//
// Open addressing with linear probing. The table is a power of two so the
// index is a mask rather than a division, and it grows at three quarters full
// because probing gets long well before the table gets full.
pub struct Slot(V) {
at: usize,
len: usize, len: usize,
used: bool,
value: V,
}
pub struct Map(V) {
slots: ^[]mut Slot(V),
bytes: ^[]mut u8,
used_bytes: usize,
count: usize,
pub fn with_capacity(n: usize) -> !Self {
var room: usize = 8;
while room < n * 2 { room = room * 2; }
let table: ^[]mut Slot(V) = try mem.alloc_slice(Slot(V), room);
let text: ^[]mut u8 = try mem.alloc_slice(u8, 64);
var i: usize = 0;
while i < room {
table.^[i].used = false;
i = i + 1;
}
return Self{ slots: table, bytes: text, used_bytes: 0, count: 0 };
}
pub fn count_of(self: &Self) -> usize { return self.count; }
pub fn room(self: &Self) -> usize { return self.slots.^.n; }
/// Where `key` sits in the table: the slot holding it, or the first free
/// slot it could go in. Probing stops at a free slot, which is why a slot
/// is never cleared -- only ever filled.
fn find(self: &Self, key: []u8) -> usize {
let mask: usize = self.slots.^.n - 1;
var at: usize = hash(key) & mask;
while true {
if not self.slots.^[at].used { return at; }
if self.same(at, key) { return at; }
at = (at + 1) & mask;
}
return 0;
}
fn same(self: &Self, slot: usize, key: []u8) -> bool {
if self.slots.^[slot].len != key.n { return false; }
let from: usize = self.slots.^[slot].at;
var i: usize = 0;
while i < key.n {
if self.bytes.^[from + i] != key[i] { return false; }
i = i + 1;
}
return true;
}
pub fn has(self: &Self, key: []u8) -> bool {
return self.slots.^[self.find(key)].used;
}
pub fn get(self: &Self, key: []u8, missing: V) -> V {
let at: usize = self.find(key);
if self.slots.^[at].used { return self.slots.^[at].value; }
return missing;
}
pub fn put(self: &mut Self, key: []u8, value: V) -> !void {
if self.count * 4 >= self.slots.^.n * 3 { try self.regrow(); }
let at: usize = self.find(key);
if self.slots.^[at].used {
self.slots.^[at].value = value;
return;
}
try self.keep(key, at);
self.slots.^[at].value = value;
self.slots.^[at].used = true;
self.count = self.count + 1;
return;
}
/// Make sure `need` bytes fit, moving to a bigger buffer if they do not.
///
/// Kept apart from `keep` because the borrow that swaps the buffer in is
/// a borrow of the whole of `self` -- borrowing is tracked at the root --
/// and it has to be over before any field is read again.
fn ensure_room(self: &mut Self, need: usize) -> !void {
let have: usize = self.bytes.^.n;
if need <= have { return; }
var room: usize = have;
while room < need { room = room * 2; }
let bigger: ^[]mut u8 = try mem.alloc_slice(u8, room);
var k: usize = 0;
let filled: usize = self.used_bytes;
while k < filled {
bigger.^[k] = self.bytes.^[k];
k = k + 1;
}
let old: ^[]mut u8 = mem.replace(&mut self.bytes, bigger);
mem.destroy(old);
return;
}
/// Copy `key` into the byte buffer and point the slot at it.
fn keep(self: &mut Self, key: []u8, slot: usize) -> !void {
let at: usize = self.used_bytes;
let need: usize = at + key.n;
try self.ensure_room(need);
var i: usize = 0;
while i < key.n {
self.bytes.^[at + i] = key[i];
i = i + 1;
}
self.slots.^[slot].at = at;
self.slots.^[slot].len = key.n;
self.used_bytes = at + key.n;
return;
}
/// Twice the slots, everything placed again. The keys do not move: they
/// live in the byte buffer and the slots only point at them.
fn regrow(self: &mut Self) -> !void {
let bigger: ^[]mut Slot(V) = try mem.alloc_slice(Slot(V),
self.slots.^.n * 2);
let mask: usize = bigger.^.n - 1;
var i: usize = 0;
while i < bigger.^.n {
bigger.^[i].used = false;
i = i + 1;
}
i = 0;
while i < self.slots.^.n {
if self.slots.^[i].used {
let from: usize = self.slots.^[i].at;
let len: usize = self.slots.^[i].len;
var at: usize = hash(self.bytes.^[from..from + len]) & mask;
while bigger.^[at].used { at = (at + 1) & mask; }
bigger.^[at] = self.slots.^[i];
}
i = i + 1;
}
let old: ^[]mut Slot(V) = mem.replace(&mut self.slots, bigger);
mem.destroy(old);
return;
}
pub fn drop(self: &mut Self) -> void {
mem.destroy(self.slots);
mem.destroy(self.bytes);
}
}
/// FNV-1a. Small, fast, and good enough for identifiers; nothing here has to
/// resist an adversary choosing the keys.
pub fn hash(key: []u8) -> usize {
var h: u32 = 2166136261;
var i: usize = 0;
while i < key.n {
h = h ^ (key[i] as u32);
h = h * 16777619;
i = i + 1;
}
return h as usize;
} }
+48
View File
@@ -0,0 +1,48 @@
// EXIT:0
// OUTPUT:count 5
// OUTPUT:fn 2 let 3 missing 0
// OUTPUT:grown 64
// OUTPUT:after 40
// OUTPUT:balanced
unit maps;
import std.io;
import std.map;
import std.fmt;
import std.sys;
fn run() -> !void {
var seen: map.Map(i32) = try map.Map(i32).with_capacity(4);
try seen.put("unit", 1);
try seen.put("fn", 2);
try seen.put("let", 3);
try seen.put("struct", 4);
try seen.put("return", 5);
@print("count {}\n", seen.count_of());
@print("fn {} let {} missing {}\n", seen.get("fn", 0), seen.get("let", 0),
seen.get("nope", 0));
// Enough keys to make it grow more than once.
var buf: [8]u8 = undefined;
var i: usize = 0;
while i < 40 {
let n: usize = fmt.fmt_i32(buf[..], i as i32);
try seen.put(buf[0..n], (i as i32) + 100);
i = i + 1;
}
@print("grown {}\n", seen.room());
var found: usize = 0;
i = 0;
while i < 40 {
let n: usize = fmt.fmt_i32(buf[..], i as i32);
if seen.get(buf[0..n], 0) == (i as i32) + 100 { found = found + 1; }
i = i + 1;
}
@print("after {}\n", found);
return;
}
fn main() -> i32 {
run() catch |e| { @print("failed\n"); return 1; };
if sys.allocs() != sys.frees() { @print("leaked\n"); return 2; }
@print("balanced\n");
return 0;
}