std: io 와 fmt 를 Ferro 로 쓰고, 컴파일된 프로그램이 출력한다

io.Writer 는 핸들 하나짜리 enum 이다. 참조도 컨텍스트 포인터도 담지 않으므로
Copy 이고 자유롭게 오간다 (SPEC 5 R8). fmt 는 sink 를 소유하지 않는다 --
호출자가 버퍼를 주고 앞에서 몇 바이트가 쓰였는지 돌려받는다.

lowering 에 추가: enum 변이 상수, match, 정수 캐스트, 문자열 이스케이프.

프론트엔드 정밀도 하나: 항상 빠져나가는 분기의 상태를 병합하지 않는다. 그
분기가 소비한 값이 그 분기를 지나지 않은 경로에서도 소비된 것처럼 보였다.
fmt_i32 가 이것 때문에 못 쓰였다.

extern "c" 이름은 유닛 접두사를 붙이지 않는다. 링커가 이미 아는 이름이라는
것이 그 선언의 요점이다.

run.py 199/199, exec.py 11/11.
This commit is contained in:
2026-08-17 06:18:16 +09:00
parent 4624c6d0ec
commit 0de10f37b5
8 changed files with 184 additions and 22 deletions
+1
View File
@@ -43,6 +43,7 @@ struct FeNode {
#define FE_NODE_SHARED 0x4U #define FE_NODE_SHARED 0x4U
#define FE_NODE_PUB 0x8U #define FE_NODE_PUB 0x8U
#define FE_NODE_COMPTIME 0x10U #define FE_NODE_COMPTIME 0x10U
#define FE_NODE_EXTERN 0x20U
typedef struct FeAst { typedef struct FeAst {
FeArena arena; FeArena arena;
+20
View File
@@ -3515,6 +3515,16 @@ static void check_stmt(FeCheckerState *s, FeNode *n)
borrow_left=flow_borrow_new(s,count); borrow_left=flow_borrow_new(s,count);
flow_own_capture(left,own_left,count); flow_own_capture(left,own_left,count);
flow_borrow_capture(left,borrow_left,count); flow_borrow_capture(left,borrow_left,count);
/* A branch that always leaves contributes nothing to what follows.
Merging its state would make a value it consumed look consumed
afterwards, on a path that never ran it. */
if (m7_stmt_definitely_exits(n->b)) {
for (i=0;i<count;++i) left[i]=base[i];
if (own_left && own_base)
for (i=0;i<count;++i) own_left[i]=own_base[i];
if (borrow_left && borrow_base)
for (i=0;i<count;++i) borrow_left[i]=borrow_base[i];
}
m7_restore_flow(base,own_base,borrow_base,count); m7_restore_flow(base,own_base,borrow_base,count);
if (n->c) check_stmt(s,n->c); if (n->c) check_stmt(s,n->c);
if (n->c) { if (n->c) {
@@ -3523,6 +3533,13 @@ static void check_stmt(FeCheckerState *s, FeNode *n)
borrow_right=flow_borrow_new(s,count); borrow_right=flow_borrow_new(s,count);
flow_own_capture(right,own_right,count); flow_own_capture(right,own_right,count);
flow_borrow_capture(right,borrow_right,count); flow_borrow_capture(right,borrow_right,count);
if (m7_stmt_definitely_exits(n->c)) {
for (i=0;i<count;++i) right[i]=base[i];
if (own_right && own_base)
for (i=0;i<count;++i) own_right[i]=own_base[i];
if (borrow_right && borrow_base)
for (i=0;i<count;++i) borrow_right[i]=borrow_base[i];
}
} else { } else {
own_right=flow_own_new(s,count); own_right=flow_own_new(s,count);
borrow_right=flow_borrow_new(s,count); borrow_right=flow_borrow_new(s,count);
@@ -3698,7 +3715,10 @@ static FeScope *declare_unit_scope(FeCheck *c, FeCheckerState *s)
for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next)
if (n->kind==FE_N_FN) { if (n->kind==FE_N_FN) {
t=fe_type_intern(&c->types,"<fn>"); t=fe_type_intern(&c->types,"<fn>");
/* `extern "c"` means the linker already knows this name, so it is
not decorated with the unit it was declared in. */
add_symbol(s,globals,n->text,t,n,0,1, add_symbol(s,globals,n->text,t,n,0,1,
(n->flags & FE_NODE_EXTERN) && n->text ? n->text :
unit_cname(c,n->text ? n->text : "fn"),n); unit_cname(c,n->text ? n->text : "fn"),n);
} }
return globals; return globals;
+94 -4
View File
@@ -62,6 +62,8 @@ static void lower_stmt(Lower *L, FeNode *n);
static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n,
unsigned long size); unsigned long size);
static void lower_for(Lower *L, FeNode *n); static void lower_for(Lower *L, FeNode *n);
static void lower_match(Lower *L, FeNode *n);
static int enum_has_payload(const FeType *t);
static void lower_global(Lower *L, FeNode *n); static void lower_global(Lower *L, FeNode *n);
static long literal_value(FeNode *n); static long literal_value(FeNode *n);
static Slot wrap_context(Lower *L, Slot v, FeNode *n); static Slot wrap_context(Lower *L, Slot v, FeNode *n);
@@ -505,14 +507,33 @@ static Slot lower_expr_core(Lower *L, FeNode *n)
case FE_N_LITERAL: case FE_N_LITERAL:
if (n->text && n->text[0] == '"') { if (n->text && n->text[0] == '"') {
/* The bytes live in the image; the value is a pointer to them and /* The bytes live in the image; the value is a pointer to them and
how many there are. */ how many there are. The lexer keeps the quotes and the escapes,
unsigned long len = strlen(n->text); so this is where `
` becomes one byte. */
char text[1024];
unsigned long raw = strlen(n->text);
unsigned long len = 0;
unsigned long i;
const char *label; const char *label;
unsigned local; unsigned local;
unsigned p; unsigned p;
unsigned c; unsigned c;
if (len >= 2) len -= 2; if (raw >= 2) raw -= 2;
label = fe_ir_string(L->m, n->text + 1, len); for (i = 0; i < raw && len + 1 < sizeof text; ++i) {
char ch = n->text[1 + i];
if (ch == 92 && i + 1 < raw) { /* a backslash */
++i;
switch (n->text[1 + i]) {
case 'n': ch = 10; break;
case 't': ch = 9; break;
case 'r': ch = 13; break;
case '0': ch = 0; break;
default: ch = n->text[1 + i]; break;
}
}
text[len++] = ch;
}
label = fe_ir_string(L->m, text, len);
if (!label) { fail(L, "a string literal", n); return slot_void(); } if (!label) { fail(L, "a string literal", n); return slot_void(); }
local = scratch(L, t, "text"); local = scratch(L, t, "text");
p = fe_ir_addr(L->m, L->b, fe_ir_at_global(label, 0)); p = fe_ir_addr(L->m, L->b, fe_ir_at_global(label, 0));
@@ -583,6 +604,14 @@ static Slot lower_expr_core(Lower *L, FeNode *n)
fail(L, "this unary operator", n); fail(L, "this unary operator", n);
return slot_void(); return slot_void();
case FE_N_MEMBER: case FE_N_MEMBER:
/* A payload-free variant used as a value is just its tag. */
if (t && t->kind == FE_TYPE_ENUM && !enum_has_payload(t) &&
n->b && n->b->text) {
FeVariantType *v = fe_type_variant(t, n->b->text);
if (v)
return slot_value(fe_ir_const(L->m, L->b, ir_type(t),
(long)v->tag), ir_type(t));
}
/* `error.Name` is a member of the open default set: a code, and /* `error.Name` is a member of the open default set: a code, and
nothing to look up. */ nothing to look up. */
if (n->a && n->a->kind == FE_N_IDENT && n->a->text && if (n->a && n->a->kind == FE_N_IDENT && n->a->text &&
@@ -688,6 +717,19 @@ static Slot lower_expr_core(Lower *L, FeNode *n)
} }
case FE_N_CALL: case FE_N_CALL:
return lower_call(L, n); return lower_call(L, n);
case FE_N_TYPE:
/* `x as T`: the operand is `a` and the target type is the node's own.
Between integers this only changes how wide the value is and whether
the top bits repeat the sign. */
if (n->a) {
FeType *from = n->a->sem_type;
unsigned v = as_value(L, lower_expr(L, n->a), n->a);
if (ir_type(from) == it) return slot_value(v, it);
return slot_value(fe_ir_cast(L->m, L->b, ir_type(from), it, v,
type_is_unsigned(from)), it);
}
fail(L, "this type expression", n);
return slot_void();
case FE_N_EXPR: case FE_N_EXPR:
return lower_expr(L, n->a); return lower_expr(L, n->a);
default: default:
@@ -1064,6 +1106,51 @@ static void lower_for(Lower *L, FeNode *n)
L->b = done; L->b = done;
} }
/* `match` over a payload-free enum or an integer: compare the tag against each
arm's pattern in turn. The checker already proved the arms cover everything,
so falling off the end cannot happen in a program that compiled -- but the
generated code has to go somewhere, and going to the join is right. */
static void lower_match(Lower *L, FeNode *n)
{
FeType *t = n->a ? n->a->sem_type : 0;
FeIrType it = ir_type(t);
Slot subject = lower_expr(L, n->a);
unsigned value;
FeIrBlock *join;
FeNode *arm;
if (it == FE_IR_MEM) { fail(L, "a match over a payload", n); return; }
value = as_value(L, subject, n->a);
join = new_block(L);
for (arm = n->children; arm; arm = arm->next) {
FeIrBlock *body;
FeIrBlock *next;
unsigned want;
unsigned same;
FeVariantType *v;
if (arm->kind != FE_N_ARM) continue;
if (arm->text && !strcmp(arm->text, "_")) {
lower_stmt(L, arm->a);
fe_ir_jmp(L->b, join->id);
L->b = join;
return;
}
v = t && t->kind == FE_TYPE_ENUM && arm->text
? fe_type_variant(t, arm->text) : 0;
want = fe_ir_const(L->m, L->b, it,
v ? (long)v->tag : literal_value(arm));
same = fe_ir_binary(L->m, L->b, FE_IR_EQ, it, value, want, 1);
body = new_block(L);
next = new_block(L);
fe_ir_br(L->b, same, body->id, next->id);
L->b = body;
lower_stmt(L, arm->a);
fe_ir_jmp(L->b, join->id);
L->b = next;
}
fe_ir_jmp(L->b, join->id);
L->b = join;
}
static void lower_stmt(Lower *L, FeNode *n) static void lower_stmt(Lower *L, FeNode *n)
{ {
FeNode *x; FeNode *x;
@@ -1123,6 +1210,9 @@ static void lower_stmt(Lower *L, FeNode *n)
case FE_N_FOR: case FE_N_FOR:
lower_for(L, n); lower_for(L, n);
return; return;
case FE_N_MATCH:
lower_match(L, n);
return;
default: default:
fail(L, "this statement", n); fail(L, "this statement", n);
return; return;
+2 -2
View File
@@ -191,9 +191,9 @@ static FeNode *params(FeParser *p)
static FeNode *fn_decl(FeParser *p, int pub, int external, int interrupt, int interrupt_safe) static FeNode *fn_decl(FeParser *p, int pub, int external, int interrupt, int interrupt_safe)
{ {
FeToken t=p->current, name; FeNode *n; FeToken t=p->current, name; FeNode *n;
(void)external; (void)interrupt; (void)interrupt_safe; (void)interrupt; (void)interrupt_safe;
want(p,FE_TOK_FN,"expected 'fn'"); if(!is_name(p)){error(p,"expected function name");return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"fn",2);} want(p,FE_TOK_FN,"expected 'fn'"); if(!is_name(p)){error(p,"expected function name");return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"fn",2);}
name=p->current; n=toknode(p,FE_N_FN,t); if(pub) n->flags|=FE_NODE_PUB; n->text=fe_arena_strdup(&p->ast->arena,name.begin,name.length); next(p); n->a=params(p); if(eat(p,FE_TOK_ARROW)) n->b=type(p); if(eat(p,FE_TOK_SEMI)) return n; n->c=block(p); return n; name=p->current; n=toknode(p,FE_N_FN,t); if(pub) n->flags|=FE_NODE_PUB; if(external) n->flags|=FE_NODE_EXTERN; n->text=fe_arena_strdup(&p->ast->arena,name.begin,name.length); next(p); n->a=params(p); if(eat(p,FE_TOK_ARROW)) n->b=type(p); if(eat(p,FE_TOK_SEMI)) return n; n->c=block(p); return n;
} }
static FeNode *field(FeParser *p, int pub) static FeNode *field(FeParser *p, int pub)
{ {
+32 -6
View File
@@ -1,6 +1,32 @@
unit fmt; unit std.fmt;
pub fn fmt_int_i32(buf: []mut u8, v: i32) -> str;
pub fn fmt_hex_i32(buf: []mut u8, v: i32) -> str; // Pure conversion. Nothing here owns a sink: the caller supplies the buffer
pub fn fmt_char(buf: []mut u8, v: char) -> str; // and is told how many bytes at the front of it were written (SPEC 10).
pub fn fmt_bool(buf: []mut u8, v: bool) -> str;
pub fn fmt_error(buf: []mut u8, v: core.Error) -> str; pub fn fmt_u32(buf: []mut u8, v: u32) -> usize {
var tmp: [10]u8 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
var value: u32 = v;
var count: usize = 0;
while true {
tmp[count] = ((value % 10) as u8) + ('0' as u8);
count = count + 1;
value = value / 10;
if value == 0 { break; }
if count == 10 { break; }
}
var i: usize = 0;
while i < count {
if i < buf.n { buf[i] = tmp[count - 1 - i]; }
i = i + 1;
}
return count;
}
pub fn fmt_i32(buf: []mut u8, v: i32) -> usize {
if v >= 0 { return fmt_u32(buf, v as u32); }
if buf.n == 0 { return 0; }
buf[0] = '-' as u8;
var rest: []mut u8 = buf[1..buf.n];
let digits: usize = fmt_u32(rest, (0 - v) as u32);
return digits + 1;
}
+24 -9
View File
@@ -1,10 +1,25 @@
unit io; unit std.io;
pub enum Writer { Stdout, Stderr, File(u16), Null } import std.sys;
pub enum Reader { Stdin, File(u16) }
pub fn write(w: Writer, bytes: []u8) -> !usize; // A writer is a handle and nothing else: an integer the runtime understands.
pub fn read(r: Reader, bytes: []mut u8) -> !usize; // It stores no reference and no context pointer, so it is Copy and can be
pub struct File { // passed and returned freely (SPEC 5 R8).
handle: u16, pub enum Writer { Null, Stdout, Stderr }
pub fn close(self: Self) -> !void;
pub fn drop(self: &mut Self) { } pub fn write(w: Writer, bytes: []u8) -> usize {
if w == Writer.Null { return bytes.n; }
var handle: i32 = 1;
if w == Writer.Stderr { handle = 2; }
let done: i32 = sys.raw_write(handle, &bytes[0], bytes.n);
if done < 0 { return 0; }
return done as usize;
}
pub fn print(bytes: []u8) -> usize {
return write(Writer.Stdout, bytes);
}
pub fn println(bytes: []u8) -> usize {
let n: usize = write(Writer.Stdout, bytes);
return n + write(Writer.Stdout, "\n");
} }
+9
View File
@@ -0,0 +1,9 @@
// EXIT:0
// OUTPUT:hello from ferro
unit hello;
import std.io;
fn main() -> i32 {
io.println("hello from ferro");
return 0;
}
+2 -1
View File
@@ -80,7 +80,8 @@ def run_case(fec: Path, path: Path) -> tuple[bool, str]:
want = expectation(path) want = expectation(path)
# The grammar fixtures are not all well-typed; stop after parsing. # The grammar fixtures are not all well-typed; stop after parsing.
mode = "--dump-ast" if path.parent.name == "parse" else "--check" mode = "--dump-ast" if path.parent.name == "parse" else "--check"
done = subprocess.run([str(fec), mode, str(path)], done = subprocess.run([str(fec), mode, str(path),
f"--std={ROOT / 'fec'}"],
capture_output=True, text=True, timeout=30) capture_output=True, text=True, timeout=30)
output = (done.stdout + done.stderr).strip() output = (done.stdout + done.stderr).strip()
rejected = done.returncode != 0 rejected = done.returncode != 0