std: 표준 라이브러리가 컴파일러 옆에서 해석되고 호출된다

std 는 예약된 이름이고 프로그램이 아니라 컴파일러와 함께 있으므로 자기 루트를
갖는다 (--std=). 본문 없는 선언은 링커가 찾을 것 -- 런타임이나 C 라이브러리 --
이므로 IR 에 extern 으로 나간다.

런타임에 write/alloc/free/exit 를 넣었다. 이것이 표준 라이브러리가 스스로
말할 수 없는 전부이고 나머지는 Ferro 로 쓴다.

@trap @unreachable @size_of @align_of @line 을 내린다.

링크 이름에서 점과 괄호를 걸렀다. 유닛 경로에는 점이 있고 제네릭 인스턴스에는
괄호가 있는데 어셈블러가 받지 않는다.
This commit is contained in:
2026-08-17 06:12:44 +09:00
parent 174e6c569d
commit 4624c6d0ec
9 changed files with 202 additions and 12 deletions
+79
View File
@@ -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_
+10
View File
@@ -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;
}
+4 -2
View File
@@ -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+1<argc) out_path=argv[++i];
else if(strncmp(argv[i],"--std=",6)==0) std_root=argv[i]+6;
else if(strcmp(argv[i],"--no-checks")==0) no_checks=1;
else if(strncmp(argv[i],"--target=",9)==0 || strncmp(argv[i],"--model=",8)==0 || strcmp(argv[i],"--strip-error-names")==0) { }
else if(argv[i][0]!='-') file=argv[i];
@@ -94,7 +96,7 @@ int main(int argc, char **argv)
every unit's AST. */
{
FeBuild build;
int ok=fe_build_load(&build,file,&d);
int ok=fe_build_load(&build,file,&d,std_root);
if(ok){
fe_check_init(&check,&build,&d,pointer_bits,no_checks);
if(!fe_check_program(&check)) ok=0;
+49
View File
@@ -406,6 +406,42 @@ static Slot lower_logical(Lower *L, FeNode *n, int is_and)
return slot_place(fe_ir_at_local(result, 0), FE_IR_I8, 1);
}
/* The builtins that are not calls at all: they are a constant, or they stop
the program. `@print` is expanded separately because it becomes several
calls rather than one thing. */
static int lower_builtin(Lower *L, FeNode *n, Slot *out)
{
const char *name = n->text;
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. */
+17 -2
View File
@@ -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. */
+5 -1
View File
@@ -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
`<root>/a/b.fe` fixes `<root>`, so a sibling `import c.d;` is looked for at
`<root>/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.
+11 -4
View File
@@ -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; }
+24 -2
View File
@@ -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); }
}
+3 -1
View File
@@ -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)