From 990068f4245cee73280375cf75b564bd3726a676 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 06:44:39 +0900 Subject: [PATCH] docs: resolve language audit for Ferro v0.1.2 --- SPEC.md | 113 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 81 insertions(+), 32 deletions(-) diff --git a/SPEC.md b/SPEC.md index ecc9e02..b4d387e 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# Ferro 언어 명세 v0.1 +# Ferro 언어 명세 v0.1.2 DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. 파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. @@ -28,7 +28,7 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성 | 메모리 모델 | small, large | flat | | 시스템 호출 | INT 21h 직접 | DPMI 서비스 + 실모드 콜백 | -- CLI: `fec main.fe --target=bits16|bits32 [--model=small|large]` +- CLI: `fec main.fe --target=bits16|bits32 [--model=small|large] [--strip-error-names]` - 소스 분기: `comptime if @bits == 16 { ... } else { ... }` - `bits32`에서 `far` 키워드를 쓰면 컴파일 에러. - 표준 라이브러리는 코어 공용, `sys` 유닛만 타깃별 구현. @@ -47,10 +47,11 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성 **예약어:** ``` -unit import pub fn struct enum error const static var let mut +unit import pub fn struct packed enum error const static var let if else while for in match return break continue defer -unsafe comptime asm try catch as extern interrupt far -true false null self Self type +unsafe critical shared atomic comptime asm try catch as extern interrupt interrupt_safe far +true false null undefined self Self type +and or not orelse ``` --- @@ -164,6 +165,21 @@ fn read_all(path: str) -> IoError!^[]u8 { - `e catch default_value`: 짧은 형태. - 서로 다른 error 타입 간 자동 변환 없음. `!T`(기본 에러 집합 `core.Error`)로 통일하거나 명시 매핑. - 에러는 값이다. 언와인딩, 스택 추적, 소멸자 이외의 자동 정리 없음. +- 실패를 복구하지 않고 트랩으로 바꾸려면 `expr catch @trap()`을 쓴다. v0.1.2에는 별도 `must` 키워드를 두지 않는다. + +`error.Name`은 선언된 error 타입을 만들지 않고 기본 `core.Error`의 이름 있는 +멤버를 참조하는 익명 에러 값이다. 각 유닛은 사용한 이름을 `.fei`에 기록한다. +최종 빌드에서 모든 유닛의 이름을 합치고 중복을 제거한 뒤 이름의 바이트순으로 +정렬하여 1부터 안정적인 `u16` 코드를 부여한다. 따라서 서로 다른 유닛의 +`error.Name`은 같은 값이고 빌드 순서와 병렬 컴파일에도 결과가 결정적이다. +코드는 최종 링크용 생성 헤더의 심볼로 참조하므로 분리 컴파일에서도 일관된다. +이름이 65,535개를 넘으면 컴파일 에러다. 명시적인 `error` 선언은 여전히 nominal +타입이며, 같은 멤버 이름이나 숫자 코드를 가진 다른 선언 및 `core.Error`와 자동 +변환되지 않는다. `error.Name`의 타입은 `core.Error`이며 `core.Error!T` 또는 +축약형 `!T`를 반환하는 함수에서만 직접 반환할 수 있다. +`--strip-error-names`를 사용하면 실행 파일과 런타임 오류 문자열에서 이름을 +제거하지만 숫자 코드와 `.fei`의 타입/코드 일관성 정보는 유지한다. +`fmt.write_error`는 이 정책에 따라 `core.Error` 값을 이름 또는 코드로 출력한다. ### 4.7 타입 동등성 @@ -179,7 +195,7 @@ fn read_all(path: str) -> IoError!^[]u8 { **R2 (Copy 타입).** 다음은 이동 대신 복사된다: 정수, `bool`, `char`, raw 포인터 `*T`, 참조 `&T`, 함수 포인터, 그리고 모든 필드가 Copy이면서 `drop`이 없는 struct/enum/배열. `^T`와 `&mut T`는 Copy가 아니다. -**R3 (소멸자, RAII).** `^T`는 소유자 스코프 종료 또는 재대입 시 `drop` 호출 후 해제. struct에 `fn drop(self: &mut Self)`가 있으면 그 값의 스코프 종료 시 자동 호출되며, 이어서 필드들의 drop이 선언 역순으로 호출된다. `drop`을 직접 호출하는 것은 컴파일 에러(`mem.destroy(x)` 사용). +**R3 (소멸자, RAII).** `^T`는 소유자 스코프 종료 또는 재대입 시 `drop` 호출 후 해제. struct에 `fn drop(self: &mut Self)`가 있으면 그 값의 스코프 종료 시 자동 호출되며, 이어서 필드들의 drop이 선언 역순으로 호출된다. `drop`을 직접 호출하는 것은 컴파일 에러(`mem.destroy(x)` 사용). `^Self` 또는 `?^Self`를 재귀적으로 포함한 타입은 기본 필드 drop이 스택 깊이에 비례할 수 있으므로 컴파일러가 경고한다. 이런 연결 구조는 `mem.replace(&mut link, null)`로 소유 링크를 하나씩 꺼내 반복 해제하고 필드를 빈 값으로 남기는 사용자 `drop`을 정의해야 하며, `--deny-recursive-drop`으로 경고를 에러로 바꿀 수 있다. **R4 (참조는 2급 값).** `&T`, `&mut T`, `[]T`는 다음 위치에만 존재할 수 있다: - 함수 파라미터 @@ -209,11 +225,16 @@ let r = list.at(0); // 에러: 반환 참조를 바인딩 불가 ``` 장수하는 접근이 필요하면 인덱스(`usize`)나 핸들을 쓴다. -**R9 (unsafe).** `unsafe {}` 안에서만 허용: raw 포인터 역참조, `*T` ↔ `^T`/`&T` 변환, `@ptr_cast`, `@seg_ptr`, `@volatile_*`, `@port_*`, `asm`, `*_unchecked` 함수, 전역 `var` 접근. R1~R8은 `unsafe` 안에서도 유지되며, raw 포인터를 경유해야만 우회된다. +**R9 (unsafe).** `unsafe {}` 안에서만 허용: raw 포인터 역참조, `*T` ↔ `^T`/`&T` 변환, `@ptr_cast`, `@seg_ptr`, `@volatile_*`, `@port_*`, `@as_far_fn`, `@call_far`, `asm`, `*_unchecked` 함수. R1~R8은 `unsafe` 안에서도 그대로 유지된다. 특히 `unsafe`가 참조 반환·저장이나 대여 검사를 끄지 않으며, 프로그래머가 명시적으로 raw 포인터를 경유한 부분만 컴파일러의 메모리 안전 보장 밖에 놓인다. -**R10 (전역).** `static`은 불변이며 컴파일타임 상수 초기화만 가능. `var` 전역은 허용되나 접근이 `unsafe`. 전역에 `^T`나 `drop` 있는 타입 금지. +**R10 (전역과 인터럽트 공유).** `static`은 불변이며 컴파일타임 상수 초기화만 가능하다. 일반 전역 `var`의 읽기와 쓰기는 안전하며 `unsafe`가 필요 없다. 인터럽트 핸들러와 메인 흐름이 함께 접근하는 값은 반드시 `shared var`로 선언하고 다음 규칙을 적용한다. +- 메인 흐름은 `critical {}` 안에서만 `shared var`에 접근할 수 있다. 진입 시 플래그를 저장하고 인터럽트를 막으며, 정상 종료·`return`·`break`·`continue`·에러 전파를 포함한 모든 이탈 경로에서 원래 플래그를 복원한다. +- `interrupt fn` 안에서는 `shared var`에 직접 접근할 수 있다. 일반 함수와 `interrupt_safe fn`은 호출 문맥을 알 수 없으므로 명시적 `critical` 밖의 비원자 공유 접근이 금지된다. +- `shared atomic var`는 타깃에서 한 명령으로 읽고 쓸 수 있는 정수/불린/포인터 스칼라에만 허용한다. 메인 흐름의 단일 읽기·쓰기에는 컴파일러가 필요한 최소 임계 구역을 생성한다. 복합 read-modify-write는 여전히 명시적 `critical {}`이 필요하다. 지원되지 않는 크기나 타입은 컴파일 에러다. +- `interrupt fn`은 `interrupt_safe fn`만 호출할 수 있다. `interrupt_safe fn`은 힙 할당, DOS/DPMI 서비스, 블로킹 I/O, 부동소수점, `critical` 및 안전하지 않은 함수 호출을 사용할 수 없으며 컴파일러가 함수 본문만 보고 검증한다. 이 효과는 `.fei` 시그니처에 기록한다. +- 전역에는 `^T`나 `drop` 있는 타입을 둘 수 없다. `shared`, `atomic`, `critical`, `interrupt fn`은 v0.1.2에서 `bits16` 전용이며 `bits32`에서 사용하면 컴파일 에러다. -**R11 (그래프 구조).** 참조 순환이 필요한 자료구조는 아레나 + 인덱스 핸들로 표현한다. 표준 라이브러리 `mem.Arena`와 `u16`/`u32` 인덱스를 쓴다. +**R11 (재귀·그래프 구조).** `^T`는 R4의 2급 참조가 아니므로 소유가 한 방향인 단방향 리스트와 트리는 필드에 저장할 수 있다. 반면 양방향 리스트·순환·일반 그래프는 역방향 필드에 `^T`를 두면 R1의 단일 소유권을 위반하고 `&T`를 두면 R4를 위반한다. 이런 구조는 아레나/배열이 값을 소유하고 `u16`/`u32` 인덱스 핸들이 간선을 나타내도록 구현한다. 표준 라이브러리 `mem.Arena`를 사용할 수 있으며, 핸들 역참조 때 세대 번호 또는 경계 검사를 사용해 해제된 항목 접근을 막아야 한다. --- @@ -228,7 +249,7 @@ import := 'import' ident ';' decl := ['pub'] (fn_decl | struct_decl | enum_decl | error_decl | const_decl | global_decl) -fn_decl := ['extern' string] ['interrupt'] 'fn' ident +fn_decl := ['extern' string] [('interrupt' | 'interrupt_safe')] 'fn' ident '(' [param (',' param)*] ')' ['->' type] (block | ';') param := ['comptime'] ident ':' type struct_decl := ['packed'] 'struct' ident '{' field* fn_decl* '}' @@ -237,11 +258,13 @@ enum_decl := 'enum' ident '{' variant (',' variant)* [','] '}' variant := ident | ident '(' type ')' | ident '{' field* '}' error_decl := 'error' ident '{' (ident '=' int ',')* '}' const_decl := 'const' ident [':' type] '=' expr ';' -global_decl := ('static' | 'var') ident ':' type '=' expr ';' +global_decl := 'static' ident ':' type '=' expr ';' + | 'var' ident ':' type '=' expr ';' + | 'shared' ['atomic'] 'var' ident ':' type '=' expr ';' block := '{' stmt* '}' -stmt := 'let' ['mut'] ident [':' type] '=' expr ';' - | 'var' ident ':' type ['=' expr] ';' +stmt := 'let' ident [':' type] '=' expr ';' + | 'var' ident [':' type] ['=' expr] ';' | 'const' ident [':' type] '=' expr ';' | lvalue ('=' | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<=' | '>>=') expr ';' @@ -249,6 +272,7 @@ stmt := 'let' ['mut'] ident [':' type] '=' expr ';' | 'return' [expr] ';' | 'break' ';' | 'continue' ';' | 'defer' block | 'unsafe' block + | 'critical' block | 'comptime' 'if' expr block ['else' (block | 'if' ...)] | 'asm' '{' asm_body '}' | expr ';' @@ -269,6 +293,7 @@ type := ident ['.' ident] | '?' type | '!' type | ident '!' type | '^' type | '&' ['mut'] type | '*' type | 'far' ('^' | '*' | '&' ['mut']) type + | 'far' 'fn' '(' [type (',' type)*] ')' ['->' type] | '[' expr ']' type | '[' ']' type | 'fn' '(' [type (',' type)*] ')' ['->' type] | ident '(' type (',' type)* ')' // 제네릭 인스턴스 @@ -278,20 +303,21 @@ type := ident ['.' ident] ``` 1 orelse, catch -2 || -3 && +2 or +3 and 4 == != < <= > >= 5 | ^ 6 & 7 << >> 8 + - +% -% 9 * / % *% -10 단항: - ! ~ & &mut ^(주소아님) try +10 단항: - not ~ & &mut ^(주소아님) try 11 후위: .field .? .^ [i] [a..b] (args) as T 12 기본: literal, ident, '(' expr ')', struct_literal, @builtin(...) ``` -- `&&`, `||`는 단축 평가. +- `and`, `or`는 단축 평가한다. 호스트 C 방출에서는 각각 `&&`, `||`로 + 매핑하며, 평가 순서와 단락 규칙은 Ferro 의미론을 그대로 유지한다. - `as`는 후위 우선순위(단항보다 강함): `-x as i32`는 `-(x as i32)`. - 비교 연산 체이닝 금지(`a < b < c`는 에러). @@ -307,9 +333,12 @@ type := ident ['.' ident] @trap() -> never @unreachable() -> never (unsafe) @line() @file() // 진단용 -@print(fmt, ...) // stdout -@fprint(w, fmt, ...) // 임의 Writer +@print(fmt, ...) -> void // stdout, 쓰기 오류 무시 +@fprint(w, fmt, ...) -> !void // 임의 Writer @sprint(buf: []u8, fmt, ...) -> usize // 버퍼에 기록, 쓴 바이트 수 반환 +@compile_error(msg) // comptime에서 항상 컴파일 에러 +@as_far_fn(f) -> far fn() // bits16 전용 함수 포인터 변환 +@call_far(p: far fn()) // bits16/unsafe 전용 호출 ``` ### 6.3.1 포매팅 빌트인 @@ -332,9 +361,21 @@ fmt.write_str(out, "\n"); - verb: `{}` 기본(정수/bool/char/str 자동), `{x}` 16진, `{c}` 문자, `{s}` 문자열/슬라이스, `{b}` 불린. `{{`는 `{` 이스케이프. - `{}` 개수와 인자 개수 불일치 → 컴파일 에러. - 인자 타입에 대응하는 `fmt.write_*` 함수가 없으면 컴파일 에러(메시지에 타입명 표시). -- 자릿수/폭/정렬 지정자는 v0.1에 없음. 필요하면 `fmt.write_int_pad`를 직접 호출. +- 자릿수/폭/정렬 지정자는 v0.1.1에 없음. 필요하면 `fmt.write_int_pad`를 직접 호출. - `@fprint`의 첫 인자는 `&mut io.Writer`(§10의 함수 포인터 struct). -- 전개 결과의 모든 호출은 `!void`를 반환하므로 `@print`도 `!void`. 무시하려면 `_ = @print(...)`. +- `@print`는 stdout에 기록하며 저수준 writer 오류를 삼키고 `void`를 반환한다. + 따라서 `try @print(...)`는 컴파일 에러다. +- `@fprint`는 writer 오류를 전파하여 `!void`를 반환한다. +- `@sprint`는 버퍼가 찬 뒤의 출력이 잘리더라도 트랩하지 않고 기록된 바이트 수를 + `usize`로 반환한다. +- 전개된 `fmt.write_*` 호출은 위 반환 규칙에 맞게 lower 단계에서 오류를 + 전파하거나 무시한다. `fmt.write_error`는 `core.Error`의 이름/코드를 출력한다. + +`@compile_error(msg)`의 `msg`는 comptime 문자열이어야 하며, 평가되는 분기에서 +항상 진단을 발생시킨다. `comptime if`의 제거되는 분기에서는 진단하지 않는다. +`@as_far_fn(f)`와 `@call_far`는 `bits16`에서만 허용된다. 전자는 함수 포인터를 +`far fn()`으로 변환하고 후자는 `far fn()`을 호출한다. 둘 다 `unsafe { }` 안에서만 +사용할 수 있으며, `bits32`에서는 컴파일 에러다. ### 6.4 예제 @@ -396,7 +437,7 @@ pub fn main() -> !void { ### 7.1 변수와 초기화 -- `let`은 불변, `let mut`은 가변, `var`는 타입 명시 필수인 가변 선언. +- `let`은 불변, `var`는 가변 선언이다. 두 형태 모두 초기값이 있으면 타입을 추론할 수 있다. `var x: T;`와 `var x: T = undefined;`처럼 초기값이 없거나 `undefined`이면 타입 명시가 필수다. - 모든 변수는 사용 전 초기화 필수(정적 검사). 명시적 미초기화는 `= undefined`(unsafe 아님, 단 읽기 전 쓰기 필수는 여전히 검사). - 섀도잉 허용(같은 스코프에서 `let` 재선언). @@ -415,7 +456,8 @@ pub fn main() -> !void { - 기본: `bits32`는 cdecl, `bits16`은 타깃 C 컴파일러 기본. - `extern "c" fn name(...) -> T;` — 본문 없이 선언, C 심볼과 링크. 이름 맹글링 없음. 인자/반환에 `^T`, 슬라이스, 에러 유니온 사용 불가(`*T`, `usize`만). -- `interrupt fn name()` — 모든 레지스터 보존 + `iret`. 파라미터/반환 없음. 주소는 `@as_far_fn(name)`으로 획득. +- `interrupt fn name()` — 모든 레지스터 보존 + `iret`. 파라미터/반환 없음. 주소는 `@as_far_fn(name)`으로 획득. 호출 제한과 공유 상태 규칙은 R10을 따른다. +- `interrupt_safe fn name(...)` — 인터럽트 문맥에서 호출 가능한 함수. ABI는 일반 함수와 같고 R10의 제한을 본문 검사로 만족해야 한다. - 큰 struct(> 4바이트)는 숨은 포인터로 반환(C ABI 따름). ### 7.4 검사와 트랩 @@ -488,13 +530,15 @@ var xs: List(u8) = List(u8).new(); ## 10. 표준 라이브러리 (최소 집합) - **core**: `panic`, `set_panic_handler`, `Error`(기본 에러 집합), `assert`. -- **mem**: `create(T) -> !^T`, `destroy(p)`, `alloc_slice(T, n) -> !^[]T`, `copy(dst, src)`, `set(dst, v)`, `Arena{ init, alloc, reset, drop }`. +- **mem**: `create(T) -> !^T`, `destroy(p)`, `alloc_slice(T, n) -> !^[]T`, `replace(dst: &mut T, value: T) -> T`, `copy(dst, src)`, `set(dst, v)`, `Arena{ init, alloc, reset, drop }`. `replace`는 이전 값을 이동해 반환하고 새 값으로 자리를 초기화하며 재귀 구조의 반복 drop에도 사용한다. - **str**: `eq`, `find`, `starts_with`, `split_at`, `parse_int`, `trim`, `to_cstr(buf, s)`, `from_cstr(p)`. - **list**: `List(T)`. - **map**: `Map(K, V)` (오픈 어드레싱, `K`는 정수/str). - **fmt**: `@print` 계열이 전개해 호출하는 저수준 함수 모음. `write_str(w, s)`, `write_int_i8/i16/i32/u8/u16/u32(w, v)`, `write_hex_u8/u16/u32(w, v)`, - `write_char(w, c)`, `write_bool(w, b)`, `write_int_pad(w, v, width, pad)`. + `write_char(w, c)`, `write_bool(w, b)`, `write_error(w, e)`, + `write_int_pad(w, v, width, pad)`. `write_error`는 `--strip-error-names` 설정을 + 따르며, 모든 함수는 `!void`를 반환한다. 전부 `fn(w: &mut io.Writer, ...) -> !void` 시그니처. 사용자가 직접 호출해도 된다. - **io**: - `File{ open, create, read, write, seek, size, close(=drop) }`, `stdin`, `stdout`, `stderr`. @@ -589,7 +633,9 @@ fec/ - **조건부 이동**: 이동 여부가 분기에 따라 다르면 `unsigned char fe_live_ = 1;` 플래그 삽입, drop 전에 검사. - **`match`**: `switch (x.tag)`. 페이로드 바인딩은 지역 변수로 복사 또는 포인터. - **`asm`**: Intel 문법으로 고정 저장. Watcom/Borland는 그대로, gcc는 `__asm__(".intel_syntax noprefix\n" ...)`로 감싼다. -- **`@print` 계열**: emit 단계에는 도달하지 않는다. lower 단계에서 이미 `fmt.write_*` 호출 나열로 전개되므로 emit_c는 일반 함수 호출로만 본다. 포맷 문자열 조각은 각각 static const 문자열 리터럴로 방출하고 동일 문자열은 중복 제거. +- **`@print` 계열**: emit 단계에는 도달하지 않는다. lower 단계에서 이미 `fmt.write_*` 호출 나열로 전개되므로 emit_c는 일반 함수 호출로만 본다. `@print`는 각 호출의 오류 코드를 명시적으로 버리고 `void`가 되며, `@fprint`는 첫 오류를 전파하고, `@sprint`는 남은 버퍼 길이를 추적해 잘라 쓴 뒤 실제 길이를 반환한다. 포맷 문자열 조각은 각각 static const 문자열 리터럴로 방출하고 동일 문자열은 중복 제거. +- **논리 연산**: `and`, `or`, `not`은 각각 C의 `&&`, `||`, `!`로 방출한다. `and`와 `or`는 C의 시퀀스 포인트와 단축 평가를 그대로 사용한다. +- **임계 구역**: bits16의 `critical`은 진입 시 FLAGS를 저장한 뒤 `cli`하고 모든 이탈 경로에서 저장한 FLAGS를 복원한다. `shared atomic var`의 단일 접근도 같은 보존형 시퀀스를 사용하며 무조건 `sti`하지 않는다. - **방출 순서**: typedef 전방선언 → struct 정의(의존 위상 정렬) → 전역 → 함수 프로토타입 → 함수 본문. - 유닛 하나당 `.c` 하나, `.fei`에서 필요한 부분은 `.h`로 생성. @@ -619,13 +665,13 @@ Uninit | Owned | Moved | MaybeMoved | Shared(n) | Exclusive | M1 | lexer, parser, AST 덤프 | `--dump-ast`가 std 소스 전체를 파싱 | | M2 | 타입 검사 + C 방출: 정수, 함수, if, while | bits32 hello world 실행 | | M3 | struct, enum, match, 배열, 슬라이스, 경계 검사, str | 문자열 처리 예제 통과 | -| M4 | **`@print`/`@fprint`/`@sprint` 빌트인** (§6.3.1), `io.Writer` 함수 포인터 struct | 포맷 출력 동작, 인자 개수·타입 불일치가 컴파일 에러 | +| M4 | **`@print`/`@fprint`/`@sprint` 빌트인** (§6.3.1), `io.Writer` 함수 포인터 struct | `@print`의 `void` 오류 삼킴, `@fprint`의 `!void` 전파, `@sprint`의 잘림/길이 동작과 인자 개수·타입 불일치가 컴파일 에러 | | M5 | `^T`, drop, defer, 이동 검사 | 누수/이중해제 테스트 통과 | | M6 | `&`, `&mut`, 배타성 검사 (own.c 전체) | R1~R8 실패 테스트 통과 | | M7 | `?T`, `E!T`, try/catch | io 유닛 동작 | | M8 | 유닛/import/.fei, 분리 컴파일, std 초안 | 다중 유닛 프로그램 빌드 | | M9 | **제네릭** (모노모피제이션) | `List(T)`, `Map(K,V)`를 Ferro로 재작성 | -| M10 | bits16 타깃: far, `@seg_ptr`, 메모리 모델, asm, interrupt fn | DOSBox에서 VGA 데모 실행 | +| M10 | bits16 타깃: far, `@seg_ptr`, 메모리 모델, asm, interrupt fn, `shared`/`atomic`/`critical`/`interrupt_safe` | QEMU FreeDOS에서 자동화된 far 포인터·인터럽트 공유 상태 테스트 통과(VGA 데모는 수동/멀티모달 검증 대상이라 완료 게이트에서 제외) | | M11 | 컴파일러 B를 Ferro로 작성, A로 빌드 | B가 M1~M10 테스트 통과 | | M12 | 셀프호스팅 fixpoint | B(B(B)) == B(B) 바이트 동일, A 폐기 | | M13 | 386 네이티브 백엔드 | gcc 없이 빌드, 컴파일 속도 10배 | @@ -644,13 +690,15 @@ Uninit | Owned | Moved | MaybeMoved | Shared(n) | Exclusive tests/ pass/*.fe + *.expected 컴파일→실행→stdout 비교 fail/*.fe 첫 줄 "// ERROR::<메시지 일부>" - run16/*.fe bits16 빌드 후 DOSBox 실행, 출력 파일 비교 + run16/*.fe bits16 빌드 후 QEMU FreeDOS 실행, 출력 파일 비교 boot/ A/B 출력 비교, fixpoint 검증 ``` - `fail/`은 규칙별 최소 3개: R1(이동 후 사용), R3(직접 drop 호출), R4(필드에 참조), R5(스코프 초과), R6(배타성 위반), R7(무효화), R8(참조 반환 바인딩), R9(unsafe 밖 raw 역참조), match 완전성, 암묵 변환, 타입 불일치. -- 포매팅(§6.3.1) 전용 `fail/` 케이스: `{}` 개수 > 인자 개수, 인자 개수 > `{}` 개수, 미지원 verb(`{q}`), 런타임 값 포맷 문자열, 대응 `write_*` 없는 타입(예: struct), 닫히지 않은 `{`. -- 포매팅 `pass/` 케이스: 각 verb 1개 이상, `{{` 이스케이프, 인자 0개, `@sprint` 반환 길이 검증, `@fprint`를 `io.buf_writer`로 호출. +- 포매팅(§6.3.1) 전용 `fail/` 케이스: `{}` 개수 > 인자 개수, 인자 개수 > `{}` 개수, 미지원 verb(`{q}`), 런타임 값 포맷 문자열, 대응 `write_*` 없는 타입(예: struct), 닫히지 않은 `{`, `try @print(...)`(void). +- 포매팅 `pass/` 케이스: 각 verb 1개 이상, `{{` 이스케이프, 인자 0개, `@print`의 오류 삼킴, `@sprint` 반환 길이/잘림 검증, `@fprint`를 `io.buf_writer`로 호출. +- `@compile_error`, `@as_far_fn`, `@call_far`의 comptime/타깃/unsafe 제약과 `error.Name`의 + `core.Error` 등록, 결정적 코드, `--strip-error-names`, `fmt.write_error`를 각각 pass/fail로 검증한다. - 각 마일스톤은 해당 기능의 pass/fail 테스트와 함께 완료한다. - 회귀 실행: `make test` — 전 타깃 전 테스트. @@ -673,8 +721,9 @@ tests/ | 레이블 있는 break | 편의 | — | 플래그 변수 | | 슬라이스 패턴 매칭 | 편의 | — | 인덱스 비교 | | `inline fn` | 편의 | — | C 방출 시 `static inline` | +| `must` 키워드 | 편의 | 실패를 트랩으로 바꾸는 문법 설탕일 뿐 핵심 의미론이 아님 | `expr catch @trap()` | | 라이프타임 표기 (`'a`) | **구조적 불가** | 전역 분석 필요, R4를 풀어야 함 | R4 (2급 참조), 인덱스 핸들 | -| 스레드 | 구조적 불가 | DOS에 없음 | — | +| 선점형 스레드 | 구조적 불가 | DOS 기본 실행 모델에 없고 함수 단위 소유권 모델을 넘어서는 동기화가 필요 | — | | 매크로 / 전처리기 | **영구** | 도구 지원과 컴파일 속도 파괴 | `const`, `comptime if`, 제네릭, `@print` | | 예외 | 영구 | 언와인딩 기반 시설 없음, 숨은 비용 | 에러 유니온 | | GC | 영구 | 결정적 비용 원칙 위반 | 소유권 + RAII + 아레나 |