From 4624c6d0ec8402e2230ec3fc1670c2a4dd93a01d Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 06:12:44 +0900 Subject: [PATCH] =?UTF-8?q?std:=20=ED=91=9C=EC=A4=80=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=EA=B0=80=20=EC=BB=B4=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=9F=AC=20=EC=98=86=EC=97=90=EC=84=9C=20=ED=95=B4?= =?UTF-8?q?=EC=84=9D=EB=90=98=EA=B3=A0=20=ED=98=B8=EC=B6=9C=EB=90=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit std 는 예약된 이름이고 프로그램이 아니라 컴파일러와 함께 있으므로 자기 루트를 갖는다 (--std=). 본문 없는 선언은 링커가 찾을 것 -- 런타임이나 C 라이브러리 -- 이므로 IR 에 extern 으로 나간다. 런타임에 write/alloc/free/exit 를 넣었다. 이것이 표준 라이브러리가 스스로 말할 수 없는 전부이고 나머지는 Ferro 로 쓴다. @trap @unreachable @size_of @align_of @line 을 내린다. 링크 이름에서 점과 괄호를 걸렀다. 유닛 경로에는 점이 있고 제네릭 인스턴스에는 괄호가 있는데 어셈블러가 받지 않는다. --- fec/rt/start.asm | 79 +++++++++++++++++++++++++++++++++++++++++++++++ fec/src/check.c | 10 ++++++ fec/src/driver.c | 6 ++-- fec/src/lower.c | 49 +++++++++++++++++++++++++++++ fec/src/resolve.c | 19 ++++++++++-- fec/src/resolve.h | 6 +++- fec/std/core.fe | 15 ++++++--- fec/std/sys.fe | 26 ++++++++++++++-- tests/build.py | 4 ++- 9 files changed, 202 insertions(+), 12 deletions(-) diff --git a/fec/rt/start.asm b/fec/rt/start.asm index 9d6860b..340020f 100644 --- a/fec/rt/start.asm +++ b/fec/rt/start.asm @@ -122,6 +122,85 @@ reason_write: call _ExitProcess@4 fe_trap endp +; ---------------------------------------------------------------- primitives +; The standard library is written in Ferro; these are the few things it cannot +; say for itself. All cdecl. + +extern _GetProcessHeap@0 : near +extern _HeapAlloc@12 : near +extern _HeapFree@12 : near + +; fe_rt_write(handle, ptr, len) -> bytes written +public fe_rt_write +fe_rt_write proc near + push ebp + mov ebp, esp + push ebx + mov eax, [ebp+8] ; 1 = stdout, 2 = stderr + cmp eax, 2 + je pick_err + push -11 + jmp pick_done +pick_err: + push -12 +pick_done: + call _GetStdHandle@4 + push 0 + push offset written + push dword ptr [ebp+16] + push dword ptr [ebp+12] + push eax + call _WriteFile@20 + mov eax, [written] + pop ebx + mov esp, ebp + pop ebp + ret +fe_rt_write endp + +; fe_rt_alloc(n) -> pointer, or zero +public fe_rt_alloc +fe_rt_alloc proc near + push ebp + mov ebp, esp + call _GetProcessHeap@0 + push dword ptr [ebp+8] + push 8 ; HEAP_ZERO_MEMORY + push eax + call _HeapAlloc@12 + mov esp, ebp + pop ebp + ret +fe_rt_alloc endp + +; fe_rt_free(p) +public fe_rt_free +fe_rt_free proc near + push ebp + mov ebp, esp + mov eax, [ebp+8] + test eax, eax + je free_done + call _GetProcessHeap@0 + push dword ptr [ebp+8] + push 0 + push eax + call _HeapFree@12 +free_done: + mov esp, ebp + pop ebp + ret +fe_rt_free endp + +; fe_rt_exit(code) -- never returns +public fe_rt_exit +fe_rt_exit proc near + push ebp + mov ebp, esp + push dword ptr [ebp+8] + call _ExitProcess@4 +fe_rt_exit endp + public fe_start_ fe_start_ proc near call fe_main_ diff --git a/fec/src/check.c b/fec/src/check.c index a2d1dbd..134fab5 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -154,11 +154,15 @@ static FeType *node_type(FeCheck *c, FeNode *n) return t; } +/* A link-visible name. A unit path has dots in it and a generic instance has + brackets and commas, none of which an assembler will accept, so everything + outside the portable identifier set becomes an underscore. */ static char *unit_cname(FeCheck *c, const char *name) { char *u; char *p; unsigned long n; + unsigned long i; u = c->ast->root && c->ast->root->text ? c->ast->root->text : "unit"; n = (unsigned long)strlen("fe_") + (unsigned long)strlen(u) + (unsigned long)strlen(name ? name : "name") + 2UL; @@ -168,6 +172,12 @@ static char *unit_cname(FeCheck *c, const char *name) strcat(p, u); strcat(p, "_"); strcat(p, name ? name : "name"); + for (i = 0; p[i]; ++i) { + char ch = p[i]; + if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || ch == '_')) + p[i] = '_'; + } return p; } diff --git a/fec/src/driver.c b/fec/src/driver.c index 7a15a81..2a6ce9b 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -18,7 +18,7 @@ static char *read_file(const char *name, unsigned long *size) static void usage(void) { - puts("usage: fec [--dump-tokens|--dump-ast|--check|--dump-ir|--emit-asm] file.fe [-o out.asm] [--no-checks]"); + puts("usage: fec [--dump-tokens|--dump-ast|--check|--dump-ir|--emit-asm] file.fe [-o out.asm] [--std=dir] [--no-checks]"); } static void dump_tokens(const char *src, unsigned long n, const char *file, @@ -42,6 +42,7 @@ int main(int argc, char **argv) int i,dump=0,dump_tok=0,check_only=0,no_checks=0,dump_ir=0,emit_asm=0; const char *file=0; const char *out_path=0; + const char *std_root=0; unsigned long n; char *src; FeDiags d; @@ -57,6 +58,7 @@ int main(int argc, char **argv) else if(strcmp(argv[i],"--dump-ir")==0) dump_ir=1; else if(strcmp(argv[i],"--emit-asm")==0) emit_asm=1; else if(strcmp(argv[i],"-o")==0 && i+1text; + if (!name || name[0] != '@') return 0; + if (!strcmp(name, "@trap")) { + fe_ir_trap(L->b, FE_TRAP_EXPLICIT, n->loc.line); + L->b = new_block(L); + *out = slot_void(); + return 1; + } + if (!strcmp(name, "@unreachable")) { + fe_ir_trap(L->b, FE_TRAP_UNREACHABLE, n->loc.line); + L->b = new_block(L); + *out = slot_void(); + return 1; + } + if (!strcmp(name, "@size_of") || !strcmp(name, "@align_of")) { + FeNode *arg = n->children; + FeType *t = arg && arg->kind == FE_N_IDENT + ? fe_type_intern(&L->c->types, arg->text) : 0; + long v = !strcmp(name, "@size_of") ? (long)ir_size(t) + : (long)ir_align(t); + *out = slot_value(fe_ir_const(L->m, L->b, FE_IR_I32, v), FE_IR_I32); + return 1; + } + if (!strcmp(name, "@line")) { + *out = slot_value(fe_ir_const(L->m, L->b, FE_IR_I32, + (long)n->loc.line), FE_IR_I32); + return 1; + } + return 0; +} + static Slot lower_call(Lower *L, FeNode *n) { unsigned args[16]; @@ -417,6 +453,10 @@ static Slot lower_call(Lower *L, FeNode *n) const char *callee = n->a && n->a->cname ? n->a->cname : (n->sem_decl && n->sem_decl->cname ? n->sem_decl->cname : 0); + { + Slot built; + if (lower_builtin(L, n, &built)) return built; + } if (!callee) { fail(L, "a call with no target", n); return slot_void(); } /* An aggregate result is written through a hidden first argument. */ if (rt == FE_IR_MEM) { @@ -1169,6 +1209,15 @@ int fe_lower_program(FeCheck *c, FeIrModule *out) for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) if (n->kind == FE_N_GLOBAL || n->kind == FE_N_CONST) lower_global(&L, n); + else if (n->kind == FE_N_FN && !n->c) { + /* A declaration with no body is something the linker will + find: the runtime, or a C library. */ + FeType *ret = n->b ? fe_type_from_ast(&c->types, n->b) : 0; + FeIrFunc *f; + if (!n->cname) continue; + f = fe_ir_func(out, n->cname, ir_type(ret), ir_size(ret)); + if (f) f->is_extern = 1; + } else if (n->kind == FE_N_FN && n->c) { lower_fn(&L, n); /* The entry unit is the one the build was rooted at. */ diff --git a/fec/src/resolve.c b/fec/src/resolve.c index 5de4c13..70baa30 100644 --- a/fec/src/resolve.c +++ b/fec/src/resolve.c @@ -190,7 +190,13 @@ static int load_unit(FeBuild *b, const char *name, FeLoc from, int have_from, unit = &b->units[b->count]; memset(unit, 0, sizeof *unit); strcpy(unit->name, name); - unit_source_path(unit->path, sizeof unit->path, b->root, name); + /* `std` is reserved (SPEC 10) and lives with the compiler, not with the + program, so it is looked up under its own root. */ + unit_source_path(unit->path, sizeof unit->path, + (name[0]=='s' && name[1]=='t' && name[2]=='d' && + (name[3]=='.' || name[3]==0) && b->std_root[0]) + ? b->std_root : b->root, + name); unit->source = read_source(unit->path, &size); if (!unit->source) { if (have_from) @@ -245,7 +251,8 @@ static int check_bindings(FeBuild *b, FeUnit *unit) return ok; } -int fe_build_load(FeBuild *build, const char *entry, FeDiags *diags) +int fe_build_load(FeBuild *build, const char *entry, FeDiags *diags, + const char *std_root) { const char *stack[FE_BUILD_UNIT_MAX]; FeAst probe; @@ -259,6 +266,14 @@ int fe_build_load(FeBuild *build, const char *entry, FeDiags *diags) memset(build, 0, sizeof *build); build->diags = diags; + if (std_root) { + unsigned long k = 0; + while (std_root[k] && k + 1 < sizeof build->std_root) { + build->std_root[k] = std_root[k]; + ++k; + } + build->std_root[k] = 0; + } /* The entry file fixes the import root, so it has to be parsed far enough to know its own name before anything else can be found. */ diff --git a/fec/src/resolve.h b/fec/src/resolve.h index 8116935..e859093 100644 --- a/fec/src/resolve.h +++ b/fec/src/resolve.h @@ -31,6 +31,9 @@ typedef struct FeBuild { FeUnit units[FE_BUILD_UNIT_MAX]; unsigned count; char root[260]; /* import root: where unit paths start */ + /* Where `std.*` is looked for. The standard library is not under the + program's root -- it ships with the compiler. */ + char std_root[260]; FeDiags *diags; } FeBuild; @@ -46,7 +49,8 @@ int fe_resolve_unit_identity(FeAst *ast, FeDiags *diags, const char *source_path `/a/b.fe` fixes ``, so a sibling `import c.d;` is looked for at `/c/d.fe`. Reports missing imports, import cycles, and binding conflicts. Returns non-zero when the whole graph loaded cleanly. */ -int fe_build_load(FeBuild *build, const char *entry, FeDiags *diags); +int fe_build_load(FeBuild *build, const char *entry, FeDiags *diags, + const char *std_root); void fe_build_destroy(FeBuild *build); /* The unit a binding refers to inside `unit`, or null. diff --git a/fec/std/core.fe b/fec/std/core.fe index 38a3c58..ca49229 100644 --- a/fec/std/core.fe +++ b/fec/std/core.fe @@ -1,5 +1,12 @@ -unit core; +unit std.core; -pub error Error { Invalid = 1, Io = 2, } -pub fn panic(msg: str, file: str, line: u32) { } -pub fn assert(ok: bool) { } +// The default error set is open: `error.Name` names a member of it without +// declaring one, and the build assigns the codes (SPEC 4.6). + +pub fn assert(ok: bool) -> void { + if not ok { @trap(); } +} + +pub fn min(a: i32, b: i32) -> i32 { if a < b { return a; } return b; } +pub fn max(a: i32, b: i32) -> i32 { if a > b { return a; } return b; } +pub fn abs(v: i32) -> i32 { if v < 0 { return 0 - v; } return v; } diff --git a/fec/std/sys.fe b/fec/std/sys.fe index d9003a8..18088b8 100644 --- a/fec/std/sys.fe +++ b/fec/std/sys.fe @@ -1,2 +1,24 @@ -unit sys; -pub fn exit(code: u16); +unit std.sys; + +// The few things the language cannot say for itself. The runtime provides +// them; everything else in the standard library is written in Ferro. +extern "c" fn fe_rt_write(handle: i32, bytes: *u8, len: usize) -> i32; +extern "c" fn fe_rt_alloc(n: usize) -> *u8; +extern "c" fn fe_rt_free(p: *u8); +extern "c" fn fe_rt_exit(code: i32); + +pub fn exit(code: i32) -> void { + unsafe { fe_rt_exit(code); } +} + +pub fn raw_write(handle: i32, bytes: *u8, len: usize) -> i32 { + unsafe { return fe_rt_write(handle, bytes, len); } +} + +pub fn raw_alloc(n: usize) -> *u8 { + unsafe { return fe_rt_alloc(n); } +} + +pub fn raw_free(p: *u8) -> void { + unsafe { fe_rt_free(p); } +} diff --git a/tests/build.py b/tests/build.py index 363b349..e1eeb52 100644 --- a/tests/build.py +++ b/tests/build.py @@ -38,7 +38,9 @@ def build(fec: Path, source: Path, out_dir: Path, no_checks: bool = False): asm = out_dir / f"{stem}.asm" log = [] - cmd = [fec, "--emit-asm", source, "-o", asm] + # `std` ships with the compiler, so it is looked for beside it rather + # than beside the program. + cmd = [fec, "--emit-asm", source, "-o", asm, f"--std={ROOT / 'fec'}"] if no_checks: cmd.append("--no-checks") step = _run(cmd, out_dir)