From 8c4e80e246f2a8b453449855a221e14cbc091a81 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 12:23:52 +0900 Subject: [PATCH] =?UTF-8?q?std:=20map=20=EC=9D=84=20=EC=93=B4=EB=8B=A4=20-?= =?UTF-8?q?-=20=EB=B0=94=EC=9D=B4=ED=8A=B8=20=EC=97=B4=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EA=B0=92=EC=9C=BC=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 컴파일러는 이름을 끊임없이 찾는데 리스트 선형 탐색은 그 모양이 아니다. 키는 맵이 소유하는 한 버퍼에 복사되고 슬롯은 그 안의 어디인지만 적는다 -- 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. --- fec/src/checkexp.c | 45 +++++++++-- fec/src/checkgen.c | 5 ++ fec/std/map.fe | 176 ++++++++++++++++++++++++++++++++++++++++- fec/tests/exec/maps.fe | 48 +++++++++++ 4 files changed, 265 insertions(+), 9 deletions(-) create mode 100644 fec/tests/exec/maps.fe diff --git a/fec/src/checkexp.c b/fec/src/checkexp.c index a8a9af0..a1b3bb5 100644 --- a/fec/src/checkexp.c +++ b/fec/src/checkexp.c @@ -142,9 +142,20 @@ int lvalue_writable(FeCheckerState *s, FeNode *n) t=n->a ? n->a->sem_type : 0; if (t && t->kind==FE_TYPE_REF && n->b && n->b->text && 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); } - if (n->kind == FE_N_INDEX) return lvalue_writable(s,n->a); return 0; } @@ -440,10 +451,16 @@ FeType *check_expr_core(FeCheckerState *s, FeNode *n) if (strcmp(n->a->b->text,"alloc_slice")==0) { FeNode *count=arg ? arg->next : 0; FeType *item; - if(!arg || arg->kind!=FE_N_IDENT || !count || count->next) - err(c,n->loc,"mem.alloc_slice requires a type and length"); - item=arg && arg->kind==FE_N_IDENT ? - fe_type_intern(&c->types,arg->text) : unknown(c); + { + /* The element type may be an instance -- `Slot(V)` -- and + not just a name. */ + 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); if(known(b) && !fe_type_is_integer(b)) 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) { a=check_expr(s,arg); 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"); - 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; arg=arg->next; } diff --git a/fec/src/checkgen.c b/fec/src/checkgen.c index 45c1a31..619213f 100644 --- a/fec/src/checkgen.c +++ b/fec/src/checkgen.c @@ -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; t->field_count=fields; if (fields) { + const char *save_unit=c->types.unit_name; t->fields=(FeFieldType *)fe_arena_alloc(&c->arena, fields*sizeof(FeFieldType)); 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); bind_self(c,t); i=0; @@ -240,6 +244,7 @@ FeType *build_struct_instance(FeCheck *c, FeUnit *home, FeNode *decl, ++i; } pop_bindings(c,&save); + c->types.unit_name=save_unit; } fe_type_layout_all(&c->types); /* A type that says how to let go of itself needs that method to exist for diff --git a/fec/std/map.fe b/fec/std/map.fe index 5e238c6..3427f19 100644 --- a/fec/std/map.fe +++ b/fec/std/map.fe @@ -1,4 +1,176 @@ -unit map; -pub struct Map(K, V) { +unit std.map; + +// 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, + 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; } diff --git a/fec/tests/exec/maps.fe b/fec/tests/exec/maps.fe new file mode 100644 index 0000000..c499f61 --- /dev/null +++ b/fec/tests/exec/maps.fe @@ -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; +}