docs: reduce SPEC.md to a language-only specification

Abandon the milestone-driven organisation of SPEC.md. The document now
contains only design philosophy (§1) and the language specification
proper (lexical structure, types, ownership/borrow rules, grammar,
semantics, module/unit semantics, and the minimal stdlib surface the
language itself depends on).

Removed:
- All milestone content (M1-M12: descriptions, completion criteria,
  ordering) and the roadmap/schedule framing.
- Compiler implementation directives: bootstrap strategy, pipeline,
  directory layout, C emission rules, own.c algorithm (§11 in full).
- Test/fixture plans and pass/fail/run16/boot fixture listings (§12
  in full).
- Build-driver/tooling detail that isn't part of the language itself:
  import-root search and ambiguity resolution, .fei cache/hash format,
  the full CLI flag reference table, C-backend evaluation-order
  lowering notes, and the generic-instance C emission ordering.
- SPEC.AUDIT.md entirely (git rm) — the accumulated change log for the
  old milestone-driven spec no longer applies.

Kept and reorganised: §2-§9 (targets, lexical structure, type system,
ownership/borrow rules R1-R11, grammar, statement/expression
semantics, unit/import/visibility semantics, generics) are otherwise
unchanged in wording. §10 (stdlib) is now a short placeholder noting
the stdlib spec is pending, while retaining the minimal surface the
language rules and builtins directly reference (core.Error, str alias
methods, mem.replace/create/destroy/alloc_slice, io.Writer/Reader,
fmt.fmt_*, sys.exit/on_exit). §13 (excluded features) is renumbered to
§11 and kept as-is since it documents language-design decisions, not
implementation.

AGENTS.md's document map is updated to drop the SPEC.AUDIT.md row and
reflect that SPEC.md is now language-only.

Implementation and stdlib specs are intended to be written as separate
documents going forward.
This commit is contained in:
2026-08-17 02:42:12 +09:00
parent d5ba699744
commit 2696dd2abc
3 changed files with 38 additions and 950 deletions
+1 -2
View File
@@ -7,8 +7,7 @@ DOS용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. 규범
| 파일 | 역할 | | 파일 | 역할 |
|---|---| |---|---|
| `SPEC.md` | 언어 명세 + 구현 지시서. 유일한 규범 문서 | | `SPEC.md` | 언어 명세. 유일한 규범 문서. 구현 지시서와 표준 라이브러리 명세는 별도 문서 |
| `SPEC.AUDIT.md` | 명세 변경의 문제·결정·근거·구현 영향 누적 로그 |
| `tools/README.md` | 호스트 요구사항, 최초 셋업, 자동화 구조 | | `tools/README.md` | 호스트 요구사항, 최초 셋업, 자동화 구조 |
개발 환경과 테스트 명령 목록·플래그는 CLI로 확인한다. 개발 환경과 테스트 명령 목록·플래그는 CLI로 확인한다.
-575
View File
@@ -1,575 +0,0 @@
# Ferro specification audit log
`SPEC.md`를 항상 최신 규범 문서로 유지하고, 최초 `AUDIT.md` 반영 이후 설계 판단으로
바뀐 사항은 이 파일에 누적한다.
## 2026-08-16 — v0.1.3
### `char`와 `u8` 사이의 변환
- 문제: §4.1은 `char``u8`과 별개 타입으로 규정하고 암묵 변환을 금지하지만,
§6.4의 줄 수 계산 예제는 `[]u8`에서 얻은 값을 문자 리터럴과 직접 비교했다.
- 결정: 별개 타입과 암묵 변환 금지 원칙을 유지한다. 저장, 대입, 비교 모두 명시적인
`as`가 필요하며 문자 리터럴도 문맥에 따라 자동으로 `u8`이 되지 않는다.
- 명세 반영: 예제의 비교를 `c.^ == '\n' as u8`로 수정하고 §4.1에 규칙을 명시했다.
- 구현 영향: 타입 검사기는 `char`를 독립 기본 타입으로 취급해야 하고, C 방출 시 같은
크기의 정수 표현을 사용할 수 있어도 Ferro 단계에서는 `char`/`u8` 혼용을 거부해야 한다.
## 2026-08-16 — v0.1.4
### `match` scrutinee와 구조체 초기화의 중괄호 모호성
- 문제: `match expr { arms }``Type{ fields }`가 모두 식별자 뒤에 `{`를 사용하므로
`match value { ... }`의 arm 블록을 구조체 초기화로 잘못 소비할 수 있었다.
- 결정: `match` scrutinee 바로 뒤의 `{`는 항상 arm 블록으로 해석한다. 구조체 초기화식
자체를 scrutinee로 쓸 때는 `match (Type{ ... }) { ... }`처럼 괄호가 필수다.
- 구현 영향: match 문맥의 식 파서는 최상위 `{` 앞에서 scrutinee 파싱을 멈춰야 하며,
괄호 안에서는 일반 구조체 초기화 규칙을 그대로 적용한다. 오류 복구는 모든 반복에서
적어도 한 토큰을 소비해 같은 진단을 무한 반복하지 않아야 한다.
## 2026-08-16 — v0.1.5
### 제어 흐름 헤더와 구조체 초기화의 중괄호 모호성 일반화
- 문제: v0.1.4의 모호성은 `match`뿐 아니라 `if flag {}`, `while flag {}`
`for x in values {}`처럼 식 직후 본문이 시작되는 모든 제어 흐름에 동일하게 발생한다.
- 결정: `if`, `while`, `for`, `match`, `comptime if` 헤더 바로 뒤의 최상위 `{`는 항상
제어 흐름 블록을 시작한다. 헤더 최상위에 구조체 초기화식을 쓰려면 괄호가 필수다.
- 구현 영향: 제어 흐름 헤더의 최상위 식 파싱에서 구조체 초기화를 금지하되 괄호 안에서는
일반 식 파싱 상태를 복원한다. 단순 식별자 조건과 배열·슬라이스 반복은 본문 `{` 앞에서
정상적으로 종료되어야 한다.
## 2026-08-16 — v0.1.6
M6(borrow checker) 착수 전에 확정해야 하는 소유권·참조 규칙 결정과, M5까지 누적된
문법·일관성 결함 정리를 함께 반영했다.
### R8 — 파생 반환 규칙으로 전면 교체
- 문제: R4가 `[]T`를 함수 반환 타입에서 금지하고 기존 R8의 예외는 `&T`/`&mut T`
다뤘다. 그 결과 §10이 요구하는 `str.trim`, `str.split_at`, `str.find`를 표현할
방법이 없었다. 기존 R8의 "결과를 지역 변수에 바인딩 불가" 제약도 `trim` 계열을
무의미하게 만들었다.
- 결정: R8을 파생 반환 규칙으로 교체한다. (a) 참조성 파라미터가 정확히 하나이고
반환값이 그것에서 파생될 때, (b) 문자열 리터럴이나 `static`에서 파생될 때
`&T`/`&mut T`/`[]T`/`str`을 반환할 수 있다. 호출 지점에서 (a)의 결과는 그 인자를
대여한 것으로 취급하며, 바인딩 금지 제약은 삭제한다.
- 근거: 표기 없는 lifetime elision이며 정의와 호출 양쪽 모두 함수 하나만 보고
검증되므로 §1.2를 깨지 않는다. 바인딩 금지는 대여 추적을 피하려던 제약인데,
own.c가 R6를 위해 같은 상태 기계를 이미 돌리므로 추가 비용이 거의 없다.
- 구현 영향: own.c는 호출 결과에 "인자로부터의 대여" 상태를 전파해야 한다.
check.c는 시그니처만 보고 참조성 파라미터 개수와 가변성 관계를 검증한다.
참조성 파라미터가 둘 이상이면 참조성 반환을 거부한다.
### R6 — 대여 구간을 마지막 사용 지점까지로 축소
- 문제: 대여가 참조 변수의 스코프 끝까지 유지되고 블록 표현식도 없어서
`let r = &mut x; r.^ = 1; x += 1;`이 에러였다. 회피 수단은 명시적 `{ }`뿐이며,
M11에서 컴파일러 B를 이 언어로 작성할 때 마찰이 누적된다.
- 결정: 대여 구간을 참조 변수의 마지막 사용 지점까지로 한다. 조건부 흐름에서는
모든 경로의 마지막 사용 중 가장 나중 지점을 취한다. 임시 참조는 문장 끝까지로
유지한다.
- 근거: 함수 지역 liveness 분석이므로 §1.2를 위반하지 않는다.
- 시점: M6 착수 전에 결정해야 한다. 나중에 좁히면 진단 메시지와 `fail/` 기대값을
전부 다시 써야 한다.
- 구현 영향: own.c에 역방향 liveness 스캔 한 번을 추가한다.
### R10 — 전역에 대한 대여 금지
- 문제: R5가 참조 대상으로 전역을 허용하므로 `fn f(r: &mut i32) { G = 5; }`
`f(&mut G)`로 호출하면 R6의 배타성이 호출 경계에서 깨진다. 호출자는 `&mut G`
배타적이라고 보고, 피호출자는 자기 파라미터가 `G`를 가리키는지 알 수 없다.
함수 단위 지역 검사로는 원리적으로 검출 불가능하다.
- 결정: `static`(불변)만 `&`로 대여할 수 있다. 일반 전역 `var``&`·`&mut` 모두
대여 불가이며 직접 읽기/쓰기만 허용한다. `shared var``critical` 안의 직접
접근만 허용한다. 전역을 참조로 넘겨야 하면 지역 변수로 복사한다.
- 구현 영향: R5의 대여 대상에서 가변 전역을 제외한다. §11.4에 방출 단계가
aliasing을 가정하지 않는다는 규정을 추가했다. 이 규칙이 없으면 `&mut T`
`restrict`를 붙이거나 M13/M14 네이티브 백엔드에서 noalias를 가정하는 순간
불건전해진다.
### `error.Name` 코드 부여 시점
- 문제: 코드를 "최종 링크용 생성 헤더의 심볼"로 참조하도록 규정했는데, 그러면 C에서
상수식이 아니므로 §11.4의 `switch` 방출을 쓸 수 없고 에러 `match`가 if-else
체인으로 떨어진다.
- 검토 후 기각한 대안: 이름 문자열의 u16 해시. 유닛별 독립 계산과 캐시 유지라는
장점이 있으나 생일 문제로 이름 약 300개에서 충돌 확률이 50%에 달해 컴파일러 B의
에러 이름 규모를 감당하지 못한다.
- 결정: 정렬 기반 번호 부여는 유지하되, 부여 시점을 링크가 아니라 **드라이버의 emit
이전 단계**로 옮긴다. 코드는 방출 C에서 컴파일타임 정수 상수가 된다.
- 대가: 이름 집합이 바뀌면 방출 `.c`와 오브젝트 캐시가 전부 무효화된다. `.fei`
이름만 기록하므로 무효화되지 않는다. 유닛 단위 `--emit-c``--error-table`
확정 표를 받아야 한다.
- 구현 영향: driver.c가 전체 `.fei`에서 이름을 수집해 코드를 확정한 뒤 emit을
시작한다.
### `str`을 `[]u8`과 별개 타입으로
- 문제: §4.2는 `str`을 "`[]u8` 불변 별칭"이라 하고 §4.7은 별칭을 완전 동일 취급이라
규정했다. 완전 동일이면 `str`을 통해 쓸 수 있는데, 문자열 리터럴은 읽기 전용
저장 영역에 놓이므로 안전성 구멍이다.
- 결정: `str`을 별개의 내장 타입으로 한다. `[]u8``str``as str`로 변환 가능
(가변성 약화이므로 안전), 역방향은 금지. `str` 원소 쓰기는 컴파일 에러.
- 구현 영향: §11.4에 `fe_str`(`const uint8_t*`) 방출 행을 추가했다. types.c는
`str``[]u8`과 다른 인터닝 엔트리로 다뤄야 한다.
### `catch` 블록은 값을 만들지 않는다
- 문제: §4.6은 catch 블록이 값을 만들 수 있다고 했으나 문법에 블록 표현식이 없다.
- 결정: 블록 표현식을 도입하는 대신 catch 블록에서 값 생성을 금지한다. 블록은
`return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝난다. 값이 필요하면
짧은 형태 `expr catch <식>`을 쓴다.
- 근거: §1.5. 블록 표현식은 §13에 `편의` 등급으로 등재했다.
### 문법 결함 정리 (§6.1)
- `struct_decl``field* fn_decl*`이라 §4.3 예제의 `pub fn new`가 문법 위반이었고
§8이 요구하는 필드별 `pub`도 표현 불가였다. `member := ['pub'] (field | fn_decl)`
으로 교체하면서 필드와 메서드의 순서 강제도 함께 풀었다.
- enum 배리언트 필드가 struct `field`를 재사용해 `pub`을 받을 수 있었다. `vfield`
분리했다.
- `catch`/`orelse`는 §6.2 우선순위 표에 이름만 있고 프로덕션이 없었다. 추가했다.
- `error_decl`이 빈 에러 집합을 허용하고 마지막 쉼표를 강제했다. 최소 1개 + 선택적
후행 쉼표로 수정했다.
- §6.2에 `expr` 프로덕션이 없어 `try`, `as`, `@builtin`, 구조체 초기화가 EBNF
어디에도 나오지 않았다. 우선순위 표가 표현식 문법의 규범임을 명시했다.
- `Some`/`None`이 패턴에 하드코딩돼 있으나 §3 예약어가 아니었다. 패턴 위치 전용
문맥 키워드임을 §4.5에 명시했다.
### 일관성과 문서 정합
- §4.4의 `u16` 태그 승격 규정이 §11.4 방출 표의 `uint8_t` 고정과 어긋났다. 표를
수정했다.
- CLI 플래그가 §2, §7.4, §8, R3에 흩어져 있었다. §8.1로 통합했다.
- §11.3의 목표 디렉터리에 `own.c`/`lower.c`/`resolve.c`/`generic.c`/`rt/`가 있으나
실제로는 없다. 목표 구조는 유지하고, M5까지 `check.c`/`emit_c.c` 통합 상태이며
M6 착수 시 `own.c/h`를 분리한다는 단서를 달았다.
- §12의 테스트 구조가 실제 `tests/m<N>/`와 달랐다. 두 축을 모두 인정하도록 했다.
R8 변경에 따라 `fail/` 목록의 "참조 반환 바인딩" 항목을 교체하고 R10 전역 대여,
`str` 변환 케이스를 추가했으며 R6·R8 `pass/` 케이스를 신설했다.
### 삭제
- `HANDOFF.md`를 제거했다. 여기에만 있던 빌드 함정(컴파일러 A는 16비트 large model,
링크는 `*.obj`, M4 Watcom 테스트는 `-wx -wcd=202`, 8.3 파일명 제약, `D:`에서 빌드
금지)은 별도 문서로 옮겨야 한다.
## 2026-08-16 — v0.1.7
외부 전면 audit에서 발견된 안전성 모순과 M6~M10 구현 전 미결정 사항을 통합했다.
이 절의 결정은 v0.1.6의 `str` nominal 타입, 함수 포인터 Writer, R8 단일 파라미터
규칙 및 own.c 스코프 끝 해제 결정을 명시적으로 대체한다.
### 슬라이스·문자열·소유 버퍼
- 문제: R4는 일반 `^T`의 대상에 `[]T`를 금지하면서 `^[]T`, `List.items`,
`mem.alloc_slice`를 요구했다. 또한 하나뿐인 `[]T`가 읽기/쓰기를 모두 나타내어
R6의 공유·배타 대여를 표현할 수 없었다.
- 결정: `[]T`는 공유·읽기 전용, `[]mut T`는 배타·쓰기 가능 slice다. var place만
mutable slice를 만들 수 있다. 호출 인자 위치의 `[]mut T → []T`, `&mut T → &T`
원래 Exclusive 상태를 유지하는 암묵 재대여로 한정한다.
- 결정: `^[]T`는 일반 포인터 합성이 아닌 `(ptr,len)` 독립 소유 타입이다.
`^[]T`/`?^[]T`만 R4의 대상 제한에서 예외이고 `*[]T`/`*[]mut T`는 금지한다.
- 재검토: v0.1.6은 문자열 리터럴의 불변성을 위해 `str`을 nominal 타입으로 만들었지만,
`[]T` 자체가 불변이 되면서 근거가 사라졌다. `str`을 미리 선언된 `[]u8` 완전 동일
alias로 내렸다. UTF-8 검증은 없으며 별도 cast·C 표현·쓰기 금지 규칙이 필요 없다.
- 결과: `str`도 R4를 그대로 적용받아 field/element에 저장할 수 없다. 문자열을
소유하려면 `String{ bytes: ^[]u8 }`를 쓰며 `as_str()`은 파생 shared slice를 반환한다.
### 안전한 Writer/Reader와 포매팅 분리
- 문제: 안전한 `File.writer() -> Writer`가 대여 대상을 `*void`에 숨겨 반환하여
`make() -> Writer`만으로 safe-code dangling을 만들 수 있었다. 문서의 "사용자 책임"은
§1의 memory-safety 보장과 충돌하며 Reader도 동일하게 불건전했다.
- 원칙: 안전한 표준 라이브러리 API는 대여 대상을 가리키는 raw pointer를 값에 숨겨
반환할 수 없다. R9 내부에서 unsafe 변환을 한 번 감쌌다는 사실은 safe API를
건전하게 만들지 않는다.
- 결정: v0.1.2 Writer/Reader는 함수 포인터 struct 대신 정수 payload만 가진 Copy handle
enum이다. `Writer{Stdout,Stderr,File(u16),Null}`, `Reader{Stdin,File(u16)}`
`io.write(Writer, []u8)`, `io.read(Reader, []mut u8)`를 쓴다. fd 재사용은 논리적 I/O
오류일 수 있으나 memory dangling은 아니다. buffer Writer는 두지 않는다.
- 결정: fmt는 sink를 모른다. `fmt.fmt_int_i32(tmp: []mut u8, v) -> str`처럼 임시
buffer에 쓰고 R8(a) 파생 slice를 반환하는 순수 함수 한 벌만 둔다. `@print`/`@fprint`
결과를 `io.write`, `@sprint``mem.copy`로 이어 붙인다. v0.2의 `dyn Writer` 전환은
안전성 수정이 아닌 기능 확장이다.
### 소유권·R4·R8
- `str`/`[]T`/조건부·에러 union/배열의 재귀 Copy 규칙을 완성하고 `[]mut T`
`^[]T`는 non-Copy로 정했다.
- field/index/optional projection에서 non-Copy 값을 부분 이동하는 것을 금지했다.
own.c는 변수 단위 상태를 유지하며 `mem.replace(&mut place, replacement)`만 추출을
허용한다. `.?`/field/index는 값을 즉시 꺼내는 연산이 아니라 place projection이다.
- mutable borrow·slice와 `&mut Self` 호출은 var place에서만 허용한다. consuming
`self: Self`는 메서드 내부에서 invalid sentinel을 남길 수 있는 local owner다.
- R8 메서드는 파생 원본을 self로 고정한다. 추가 참조 인자는 받을 수 있지만 반환이
그 인자에서 파생될 수 없다. 자유 함수만 참조성 파라미터 정확히 하나를 요구한다.
`?&T`, `?&mut T`, `?[]T`, `?[]mut T` 반환을 포함한다.
- own.c의 오래된 "스코프 끝 해제"와 "R8 결과 바인딩 거부"를 삭제했다. 역방향
liveness pass로 마지막 사용을 계산하고 defer 사용은 스코프 끝까지 연장한다.
### drop, File, heap 초기화
- `File.close(=drop)` 모순을 제거했다. `close(self: Self) -> !void`는 소비하는 일반
메서드이며 내부 handle을 invalid로 만든 후 오류를 반환한다. 자동 drop은 열린 handle만
조용히 닫는다. `drop` 직접 호출 금지는 유지한다.
- `mem.create(T) -> !^T`는 초기화되지 않은 안전 힙을 반환하므로 삭제했다.
`mem.create(value: T) -> !^T`로 바꾸고 T는 값에서 추론한다.
### error와 결정적 build
- `try`는 operand와 현재 함수의 nominal error 타입이 같을 때만 허용한다. 다른 타입은
catch에서 명시 매핑한다. `catch`는 error-return 함수 밖에서도 허용하며 void 결과의
handler block은 정상 fallthrough할 수 있다.
- 정렬 기반 error code 표는 결정성과 fixpoint를 위해 유지한다. build-directory 이력에
의존하는 append-only 표는 기각했다.
- 드라이버는 단일 `fe_errors.h`에 정렬된 `#define`을 생성한다. 이름 집합 변경 시 유닛
C를 재방출하지 않고 header 의존 object만 다시 컴파일한다. switch 상수 요건도 유지한다.
### 제네릭·증분 build
- 제네릭 이름 해석은 사용 유닛이 아니라 정의 유닛 scope에서 한다. `.fei`는 본문 token과
generic 전용 private signature를 함께 기록한다.
- driver가 전체 인스턴스 요청을 합쳐 단일 `fe_generics.c`에 중복 없이 방출한다.
사용 유닛별 external 중복 심볼과 static 코드 복제를 모두 피한다.
- comptime type 비교와 최소 introspection `@is_int`, `@is_ptr`를 추가했다.
- `.fei` cache key에 source hash뿐 아니라 dependency `.fei` hash를 포함한다.
### interrupt/shared와 panic
- shared C 방출을 `volatile`로 정하고 critical 진입·이탈에 compiler barrier를 둔다.
bits16의 한 명령 크기 8/16비트 atomic load/store는 interrupt 경계에서 원자적이므로
자동 critical 없이 volatile 한 명령만 방출한다. far pointer와 RMW는 explicit critical이다.
- `interrupt_safe`에서 critical, port/volatile builtin, asm과 필요한 unsafe를 허용한다.
금지 목록은 heap, DOS/DPMI, blocking I/O, FPU, non-interrupt-safe 호출로 한정했다.
- panic은 일반 defer unwind를 하지 않지만 interrupt vector 복원용 고정 크기
`sys.on_exit` callback을 실행한다. bits32 interrupt/shared/critical은 v0.2로 명시했다.
### 문법·표기 정리
- generic struct/enum parameter, declaration-level comptime if, for 전용 range,
bool/char pattern, `[]mut T`를 EBNF에 추가했다.
- 정의되지 않은 단항 `^`를 삭제했다. field/index/slice/method의 `&`/`&mut`/`^`
projection과 raw/optional의 비자동 역참조를 명문화했다.
- method가 function-pointer field보다 우선하며 field 호출은 `(x.f)(...)`로 고정했다.
- `@seg_ptr(T, seg, off)`로 타입 인자를 명시하고 type-valued const alias를 허용했다.
- 단항 직후 cast는 `(-x) as T` 또는 `-(x as T)`처럼 괄호를 강제한다.
### 구현 및 milestone 영향
- M3: shared/mutable slice와 str alias를 재검증한다.
- M4: 함수 포인터 Writer를 handle enum + 순수 fmt 함수로 교체한다.
- M5: `^[]T`, consuming close, projection 부분 이동, 초기화된 create를 반영한다.
- M6: 역방향 liveness와 self-source R8을 구현한다.
- M7: try nominal error 일치와 일반 catch를 구현한다.
- M8/M9: `fe_errors.h`, dependency hash, `fe_generics.c`를 구현한다.
- M10: volatile/barrier, interrupt-safe 허용 목록, on_exit 복원을 검증한다.
## 2026-08-16 — v0.1.8
M6~M9 구현 전에 함수-local 소유권 분석, optional/error 의미, 계층형 unit/import,
`.fei` cache와 제네릭 모노모피제이션을 동결했다. compiler A/B가 같은 작은 상태 기계를
구현하고 DOS와 host에서 같은 source graph를 선택하며 M12 fixpoint에서 byte-identical
출력을 만들 수 있는지가 공통 판단 기준이다.
### Deterministic expression evaluation order
- 문제: C는 일반 호출 인자와 많은 operand의 평가 순서를 보장하지 않는다. Ferro가 이를
그대로 상속하면 side effect뿐 아니라 move, borrow, `try`, defer/drop cleanup 결과가 C
compiler와 최적화에 따라 달라진다.
- 결정: Ferro 일반 표현식은 left-to-right다. 호출은 callee 먼저, 이어서 source 순서의
인자, 이항식은 왼쪽 operand 먼저다. `and`/`or`, `orelse`, `catch`는 필요한 우변만
평가하는 lazy 연산이다.
- 근거: source만으로 동작과 cleanup 순서를 예측할 수 있고 compiler A/B의 lower 결과가
동일해진다. 함수-local 분석 원칙도 그대로 유지한다.
- 기각한 대안: target C의 평가 순서에 맡기기. host compiler와 build option에 따라 의미가
변해 M12 결정성을 깨므로 기각했다.
- 구현 영향: lower는 C에서 순서가 보장되지 않는 식을 ordered temporary statement로
분해하고 own.c도 같은 순서로 place effect를 처리한다.
### M6 root-granularity borrow tracking
- 문제: field/index별 독립 대여를 허용하려면 projection overlap, 동적 index 동등성,
union/alias까지 다루는 별도 alias analysis가 필요하다.
- 결정: v0.1 대여 상태는 root local/parameter 단위다. `&mut p.a``p` 전체를 잠그고
`xs[0]``xs[1]`도 같은 root의 충돌 대여다. projection은 root를 찾는 데만 쓴다.
- 근거: R1~R8을 함수 하나의 작은 상태 기계로 검사할 수 있어 compiler A와 M11의 B가
단순해진다. 보수적 거부일 뿐 memory safety나 표현 결정성은 약화하지 않는다.
- 기각한 대안: field-sensitive/index-sensitive borrow checking. 편의는 늘지만 compiler A의
구현량과 진단 상태가 크게 증가하고 동적 index에는 결국 보수성이 남아 기각했다.
- 구현 영향: own.c의 borrow key는 projection이 아니라 root symbol이다. M6에는 disjoint
field/index도 충돌하는 pass/fail 경계를 고정한다.
### M6 reborrow/coercion 제한
- 문제: `&mut → &``[]mut → []`를 일반 암묵 변환으로 허용하면 새 shared borrow의
수명과 원래 exclusive borrow의 재활성화를 결정하는 숨은 coercion/lifetime 시스템이
필요하다.
- 결정: 암묵 약화는 호출 인자 위치의 호출 기간 read-only reborrow만 허용한다. 원래
exclusive borrow는 원래 last-use까지 유지하며 일반 `let`/대입의 암묵 약화는 에러다.
- 근거: API 호출 편의는 확보하면서 수명 annotation 없이 함수-local R6 분석을 유지한다.
- 기각한 대안: arbitrary implicit reborrow/coercion과 `let s: &T = m` 허용. 대여 종료
시점이 숨고 compiler A/B가 별도 coercion graph를 가져야 하므로 기각했다.
- 구현 영향: check/own은 call argument에만 임시 shared view 전이를 만들고 assignment
conversion table에는 추가하지 않는다.
### R8 provenance lattice
- 문제: 여러 return path의 static/parameter 파생 결과, optional null 경로와 method의
추가 참조 인자를 합칠 명시 규칙이 없으면 caller borrow가 구현 순서에 따라 달라진다.
- 결정: provenance를 `Static``Param(N)`으로 정규화한다. method는 `Param(self)`만,
자유 함수는 유일한 참조성 parameter만 허용한다. `Static + Param(N)``Param(N)`,
서로 다른 `Param`의 합류는 에러다. null 경로는 caller borrow가 없는 경로다.
- 근거: provenance가 작은 lattice라 함수 본문만 보고 계산하고 시그니처로 전달할 수 있다.
lifetime annotation이나 interprocedural inference가 필요 없다.
- 기각한 대안: arbitrary parameter union provenance 또는 lifetime parameter. caller에서
숨은 alias set/전역 분석이 필요해 Ferro 철학과 맞지 않는다.
- 구현 영향: own.c가 return CFG에서 lattice를 합치고 lowered signature와 `.fei`
provenance metadata를 보존한다.
### M6 branch merge와 loop fixed point
- 문제: `Owned/Moved`, 초기화 여부와 live borrow가 branch/backedge에서 만날 때 단순히
한쪽 상태를 고르면 use-after-move를 놓치거나 안전한 borrow를 너무 일찍 푼다.
- 결정: `Owned + Moved → MaybeMoved`, 경로별 초기화 차이는 `MaybeUninit` 동등 상태로
합친다. live borrow는 합집합을 보수적으로 유지하고 incompatible borrow는 약화하지
않는다. loop은 진입/종료 상태를 합쳐 두 번째 pass를 돌리고 안정되지 않으면 에러다.
- 근거: 유한한 함수-local lattice와 기존 2-pass만으로 모든 iteration을 보수적으로
근사한다. 첫 iteration만 검사하는 불건전성을 피한다.
- 기각한 대안: 첫 pass만 검사, 또는 merge에서 borrow를 `Owned`로 되돌리기. loop-carried
alias와 조건부 move를 놓치므로 기각했다.
- 구현 영향: own.c는 branch exit 전에 last-use를 반영하고 merge table/bit state를
구현한다. runtime drop에는 `MaybeMoved` live flag가 필요하다.
### M7 contextual null/error-union construction
- 문제: `null`에 독립 타입을 주거나 error union을 일반 implicit conversion으로 다루면
타입 추론·overload 후보가 늘고 nominal error 경계가 흐려진다.
- 결정: `null`은 expected optional/pointer-like type이 유일할 때만 구성된다. expected
`E!T` 위치에서는 T가 success, E가 failure를 구성하며 return도 같다. E1/E2 및
nominal error/`core.Error` 자동 변환은 없다.
- 근거: contextual expected type 한 개만 보면 되어 compiler A/B의 local type checker가
결정적이고 nominal error 안전성도 유지된다.
- 기각한 대안: polymorphic null, 일반 union injection conversion, error widening. 숨은
conversion 및 overload resolution을 요구하므로 기각했다.
- 구현 영향: check는 expected-type 전달 위치에서만 null/error construction을 허용하고
문맥 없는 `let p = null`을 진단한다.
### M7 error declaration uniqueness
- 문제: code 0 외에도 한 nominal error 선언 안의 중복 member 이름이나 숫자 code는
match/format 결과를 모호하게 만든다.
- 결정: code 0, 중복 이름, 중복 숫자 code를 모두 compile error로 한다. 서로 다른 nominal
error 선언끼리는 같은 숫자를 사용할 수 있지만 타입은 계속 다르다.
- 근거: 선언 하나의 symbol/code table만 검사하면 되고 runtime representation은 바뀌지
않는다.
- 구현 영향: error declaration check가 두 deterministic set을 만들고 중복 위치를 note로
표시한다.
### M7 lazy recovery operators / non-Copy extraction
- 문제: `orelse`/`catch` RHS를 eager 평가하면 불필요한 side effect와 move가 생긴다.
또한 `Some(x)` pattern이나 projection이 non-Copy payload를 암묵 이동하면 R7과
조건부 drop이 불명확해진다.
- 결정: recovery RHS/handler는 failure 경로에서만 평가한다. optional pattern은 place의
borrow/view이고 Copy만 복사한다. non-Copy owned payload 추출은 `mem.replace`로만 하며
temporary optional 자동 추출 예외도 두지 않는다.
- 근거: 평가와 소유권 효과가 같은 CFG 경로를 따르고 기존 projection/R7 상태 기계를
재사용한다.
- 기각한 대안: pattern별 destructive move와 temporary 특례. hidden move와 추가 drop
상태를 만들고 source에서 비용이 보이지 않아 기각했다.
- 구현 영향: lower는 lazy branch를 만들고 own은 실행 경로별 effect를 합친다. pattern
binding은 place mutability에 따른 shared/mutable borrow다.
### Hierarchical dotted unit namespace
- 문제: 단일 `unit foo` namespace는 외부 source library가 늘 때 `util`, `types`, `parse`
같은 이름 충돌을 피할 수 없다.
- 결정: `unit_path := ident ('.' ident)*`, `import unit_path [as ident]`의 계층형 canonical
이름을 도입한다. import binding은 마지막 segment이고 항상 `binding.member`로 접근한다.
- 근거: Go와 비슷한 단순 unit 전체 import를 유지하면서 namespace 충돌만 해결한다.
resolver에는 relative scope walk나 symbol import가 필요 없다.
- 기각한 대안: relative/glob/selective imports, re-export, friend/package-private visibility.
이름 해석과 캐시 의존 graph가 복잡해져 v0.1에서 제외했다.
- 구현 영향: lexer keyword 추가는 없고 parser/resolve/diagnostic이 canonical dotted path와
optional alias를 보존한다.
### DOS-safe unit naming
- 문제: host의 case sensitivity와 FAT 8.3 규칙이 다르면 같은 source tree가 다른 unit을
찾거나 긴 이름 전송 시 변형될 수 있다.
- 결정: unit segment는 lowercase ASCII, 첫 글자 letter, 이후 letter/digit/underscore,
최대 8자로 제한한다. dotted path는 root 아래 `segment/.../last.fe`와 정확히 대응하고
비교는 규범적 ASCII case-insensitive mapping을 쓴다.
- 근거: unit identity가 DOS와 host에서 같고 8.3 alias 생성에 기대지 않는다.
- 기각한 대안: 일반 Ferro identifier/임의 길이 허용 후 host별 normalization. case-fold와
truncation 충돌이 platform-dependent라 기각했다.
- 구현 영향: entry path suffix로 project root를 계산하고 mismatch/case-variant duplicate를
진단한다. canonical identity는 항상 lowercase dotted form이다.
### Deterministic import-root resolution / ambiguity rejection
- 문제: project root, 여러 `-I`, std root를 first-match-wins로 검색하면 `-I` 순서나 host
directory 상태가 실제 선택 source를 바꾼다.
- 결정: 모든 candidate root를 조사하고 동일 unit에 서로 다른 canonical file이 둘 이상
있으면 ambiguous error와 path note를 낸다. 같은 실제 file alias만 dedup한다.
- 근거: build/order/platform과 무관한 source graph를 만들어 `.fei`, generated C와 M12
fixpoint를 안정시킨다.
- 기각한 대안: first-match-wins. 편하지만 shadowing이 command-line order에 숨어 결정성을
깨므로 기각했다.
- 구현 영향: driver는 후보를 canonicalize·정렬한 뒤 identity를 비교하고 첫 성공에서
검색을 중단하지 않는다.
### Reserved std namespace
- 문제: flat `io`, `mem`, `fmt`, `sys`는 user library 이름과 충돌하고 compiler 내장 std
root를 일반 user root처럼 검색하면 같은 이름이 환경에 따라 shadow된다.
- 결정: `std` top-level을 compiler-reserved로 하고 `std.io`, `std.mem`, `std.fmt`,
`std.sys`를 canonical unit으로 쓴다. 마지막 segment binding 때문에 사용 표면은
`io.write`, `mem.replace`로 유지한다. `str`은 import unit이 아니다.
- 근거: std lookup이 명시적이고 deterministic이며 향후 source package와 충돌하지 않는다.
- 구현 영향: M8에서 std source layout/unit 선언을 이동하고 builtin std root는 `std.*`에만
후보가 된다.
### Source-only external libraries
- 문제: v0.1에서 package manifest/solver나 stable binary ABI까지 정의하면 M8 범위를 넘어
`.fei` encoding과 target C ABI를 영구 호환 계약으로 굳히게 된다.
- 결정: 외부 library는 source tree를 `-I` root로 제공한다. package manager/registry/version
solver/manifest 문법과 `.fei + .obj/.lib` binary-only 배포 ABI는 지원하지 않는다.
- 근거: 언어 import 의미는 작게 유지하고 target/model별로 source에서 결정적으로
재컴파일할 수 있다. DOS 배포 도구도 단순하다.
- 기각한 대안: package manager를 언어 의미론에 결합, binary-only ABI. compiler A와
M12 전에 해결할 필요가 없고 호환 부담이 커 기각했다.
- 구현 영향: `-I`는 source-only candidate root이며 미래 도구도 root 구성만 담당한다.
### `.fei` interface/cache role과 deterministic schema
- 문제: source hash, public interface hash와 compile cache key가 섞여 있었고 `.fei`
최소 논리 정보·결정적 직렬화 조건이 없어 private 변경이 전체 rebuild를 유발하거나
host path/timestamp가 fixpoint에 섞일 수 있었다.
- 결정: 세 hash 개념을 분리하고 `.fei`에 version/target/model, canonical unit, public
signatures/layout, anonymous errors, exported generic body/support closure, direct dependency
names/hashes를 기록한다. canonical key 순으로 직렬화하며 absolute path/time/build dir를
금지한다.
- 근거: private non-generic 변경은 자기 unit만 재컴파일하고 interface가 같으면 dependent를
유지할 수 있다. 같은 graph+target의 `.fei`는 host/build order와 무관하게 byte-identical하다.
- 기각한 대안: source hash를 interface hash로 재사용, unordered serializer. 구현은 짧지만
불필요한 rebuild와 M12 비결정성을 만들어 기각했다.
- 구현 영향: `.fei` encoding은 자유지만 논리 schema와 sorted serialization을 만족하고
driver cache가 dependency interface hash를 사용해야 한다.
### Ferro visibility vs backend linkage
- 문제: Ferro private를 무조건 C `static`으로 방출하면 통합 `fe_generics.c`의 exported
instance가 definition unit private helper를 호출할 수 없다.
- 결정: Ferro private는 resolver visibility일 뿐 C linkage와 동일하지 않다. generic
support에 필요한 private top-level symbol은 deterministic unit-mangled linkage와 internal
prototype을 가질 수 있다.
- 근거: source 접근 권한은 유지하면서 단일 통합 generic body 방출을 가능하게 한다.
- 기각한 대안: `C static == Ferro private`, 또는 private helper를 instance마다 복제.
전자는 linkage 실패, 후자는 중복과 비결정적 출력 때문에 기각했다.
- 구현 영향: resolver는 여전히 cross-unit private 참조를 거부하고 backend/internal header만
`.fei` support metadata를 통해 symbol을 연결한다.
### M9 type-only generics
- 문제: user comptime value generic까지 허용하면 값 canonicalization, mangling, expression
evaluator와 instance explosion 정책을 M9에서 함께 설계해야 한다.
- 결정: v0.1 user generic parameter는 `type`만 지원한다. `struct Box(T)`
`comptime T: type` shorthand이며 builtin comptime value와 구별한다.
- 근거: List/Map과 compiler B에 필요한 추상화를 충족하면서 instance key를 canonical type
list로 제한한다. trait/bound도 추가하지 않는다.
- 기각한 대안: integer/string/bool value generics. M11 필수 기능이 아니고 구현/결정성
부담이 커 v0.1 이후로 미룬다.
- 구현 영향: parser/check가 user generic parameter type을 제한하고 value generic fixture를
명시적으로 거부한다.
### M9 no type inference
- 문제: `id(3)`에서 T를 추론하려면 argument constraints, conversion 후보와 향후 overload
규칙을 정의해야 하고 진단/instance 발견 순서도 복잡해진다.
- 결정: generic type argument는 `id(i32, 3)`처럼 항상 명시한다. compiler-known
`mem.create(value)` inference는 별도 intrinsic 규칙이다.
- 근거: call syntax만 보고 instance key가 결정되어 compiler A/B가 단순하고 deterministic하다.
- 기각한 대안: generic type inference. 편의보다 숨은 constraint solver 비용이 커 기각했다.
- 구현 영향: generic call arity/type argument check는 명시 목록만 검사하며 inference
fallback을 시도하지 않는다.
### Definition-site resolution
- 문제: generic body의 non-dependent 이름을 caller scope에서 다시 찾으면 caller의 import와
shadowing에 따라 같은 generic이 다른 코드를 만든다.
- 결정: 이름은 definition unit에서 고정하고 type-dependent operation만 instantiation 때
검사한다. `comptime if`의 선택되지 않은 branch는 parse만 하고 semantic 처리하지 않는다.
- 근거: lexical 의미와 private support를 유지하고 caller/build order와 무관한 instance를
만든다.
- 기각한 대안: use-site lookup과 selected-out branch의 eager type check. 전자는 의미가
불안정하고 후자는 type-specific branch를 불가능하게 해 기각했다.
- 구현 영향: `.fei`가 body token과 definition-scope symbol/support identity를 전달하고
generic.c가 그 환경에서 재검사한다.
### Deterministic monomorphization
- 문제: request 발견 순서대로 instance를 방출하면 unit traversal/hash iteration/parallel
build에 따라 `fe_generics.c`와 symbol 순서가 바뀐다.
- 결정: canonical definition unit + declaration identity + normalized canonical type args를
key로 dedup하고 byte ordering으로 정렬한다. prototype 전부를 먼저, body 전부를 나중에
같은 canonical 순서로 방출한다.
- 근거: alias instance가 중복되지 않고 recursion/cross-instance call을 지원하며 M12에서
byte-identical output을 만든다.
- 기각한 대안: first-request order 또는 pointer/insertion-order key. platform/build order에
의존해 기각했다.
- 구현 영향: driver/generic.c가 global request set을 정렬하고 alias를 underlying interned
identity로 normalize한다.
### Generic private-support closure
- 문제: exported generic이 private helper/type/const/private generic을 참조하면 signature만
담은 `.fei`로 다른 unit에서 안전하게 instantiate할 수 없다.
- 결정: `.fei`는 필요한 private support dependency의 transitive closure를 compiler-only
metadata로 제공한다. 이는 Ferro visibility를 public으로 바꾸지 않는다.
- 근거: definition-site semantics와 source private API를 동시에 지키며 `fe_generics.c`에서
정확한 backend symbol/layout을 사용할 수 있다.
- 기각한 대안: 모든 support를 source `pub`으로 강제하거나 generic body를 definition unit마다
static 복제. API 누출 또는 중복/링크 문제 때문에 기각했다.
- 구현 영향: interface hash는 exported generic이 관찰하는 support 변화에 반응하고
serializer는 closure를 canonical 순서로 기록한다.
### Recursive instantiation semantics
- 문제: 단순 재귀 호출이 같은 instance를 다시 요청할 때마다 depth를 올리면 정상 generic
recursion도 limit에 걸리고, 반대로 growing type chain을 dedup만으로 허용하면 무한 생성된다.
- 결정: pending/known 동일 key 재요청은 재사용하고 depth를 소비하지 않는다. 새로운 distinct
instance chain만 증가시키며 32 초과를 에러로 한다.
- 근거: ordinary recursion은 prototype-first 방출로 처리하고 실제 instance explosion만
유한한 local driver 상태로 차단한다.
- 구현 영향: generic.c는 pending/known set과 distinct chain stack을 구별하고 최초/현재
instantiation 위치를 note로 출력한다.
### Canonical C mangling and type identity
- 문제: unit이 계층화되고 generic이 통합 방출되면 host path, pointer address 또는 insertion
order 기반 이름은 충돌하거나 run마다 달라질 수 있다.
- 결정: mangling은 canonical dotted unit + declaration + normalized canonical type args만
사용한다. nominal identity는 fully-qualified defining unit+name, alias는 underlying identity다.
- 근거: collision-free backend linkage와 M12 fixpoint를 동시에 보장한다.
- 구현 영향: 정확한 escaping 문자는 구현 세부지만 deterministic separator encoding과
collision 검사가 필요하며 absolute path/address를 symbol에 포함할 수 없다.
### `try` enforcement is blocked on M7 contextual construction
- 문제: `SPEC.md``try`를 에러 유니온 반환 함수 안에서만 허용하는데, `check.c`의 검사가
`FE_N_EXPR_STMT`에만 걸려 있어 `var x = try e;``x = try e;`를 통과시킨다.
`fec/tests/m5/runtime.fe``run``owned.fe``main`이 이 구멍에 의존하고 있었다.
- 결정: 구멍은 M7과 함께 닫는다. M7 이전 master에서는 닫을 수 없다. (M7에서 해소됨)
- 근거: 검사를 `try` 표현식으로 옮기면 `run`이 에러 유니온을 반환해야 하는데, master는
`-> !i32`에서 `return <value>;`도, `-> !void`에서 명시적 `return;`도 거부한다
("return type mismatch" / "void expression returned from value function"). 둘 다
contextual success construction이 필요하고 그것은 M7 작업이다. `catch``@trap`
master에는 없어서 우회로가 없다. 실측으로 세 경로를 모두 확인했다.
- 구현 영향: M7 병합 시 `check.c`의 검사를 `check_expr``try` 분기로 옮기고
`FE_N_EXPR_STMT`의 중복 검사를 제거한다. `runtime.fe``run`은 그때 에러 유니온
반환으로 바꾸고 `runtime.c``extern long fe_m5_runtime_run(long)`을 함께 고친다.
`owned.fe``main`은 M7 없이도 합법인 `-> !void`로 먼저 고쳐 두었다.
+37 -373
View File
@@ -3,7 +3,8 @@
DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다.
파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. 파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`.
이 문서는 언어 명세 + 컴파일러 구현 지시서를 겸한다. 구현 중 애매한 부분은 §1 철학과 §5 소유권 규칙을 기준으로 결정한다. 이 문서는 Ferro 언어 명세만 다룬다. 컴파일러 구현 지시서와 표준 라이브러리 상세 명세는
별도 문서에서 다룬다. 명세 판단이 애매한 부분은 §1 철학과 §5 소유권 규칙을 기준으로 결정한다.
--- ---
@@ -13,7 +14,7 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성
2. **전역 분석 금지.** 모든 검사(타입, 소유권, 참조)는 함수 하나만 보고 완결되어야 한다. 이 제약이 라이프타임 표기를 없애고, 640KB 머신에서 셀프호스팅을 가능하게 한다. 2. **전역 분석 금지.** 모든 검사(타입, 소유권, 참조)는 함수 하나만 보고 완결되어야 한다. 이 제약이 라이프타임 표기를 없애고, 640KB 머신에서 셀프호스팅을 가능하게 한다.
3. **숨은 비용 없음.** 힙 할당, 복사, 소멸자 호출, 형변환이 전부 소스에 보인다. GC 없음, 예외 없음, 암묵 변환 없음. 3. **숨은 비용 없음.** 힙 할당, 복사, 소멸자 호출, 형변환이 전부 소스에 보인다. GC 없음, 예외 없음, 암묵 변환 없음.
4. **읽히는 문법.** `이름: 타입` 순서, 좌→우 파싱, LL(1) 재귀하강으로 처리 가능. 4. **읽히는 문법.** `이름: 타입` 순서, 좌→우 파싱, LL(1) 재귀하강으로 처리 가능.
5. **작게 시작.** 기능을 넣기 전에 뺄 이유를 먼저 찾는다. 뺀 것과 그 대체 수단은 §13에 기록한다. 5. **작게 시작.** 기능을 넣기 전에 뺄 이유를 먼저 찾는다. 뺀 것과 그 대체 수단은 §11에 기록한다.
6. **기존 도구체인 재사용.** 링커, `.OBJ`/`.LIB`/`.EXE` 포맷, DPMI 익스텐더를 새로 만들지 않는다. 6. **기존 도구체인 재사용.** 링커, `.OBJ`/`.LIB`/`.EXE` 포맷, DPMI 익스텐더를 새로 만들지 않는다.
--- ---
@@ -28,10 +29,8 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성
| 메모리 모델 | small, large | flat | | 메모리 모델 | small, large | flat |
| 시스템 호출 | INT 21h 직접 | DPMI 서비스 + 실모드 콜백 | | 시스템 호출 | INT 21h 직접 | DPMI 서비스 + 실모드 콜백 |
- CLI: `fec main.fe --target=bits16|bits32 [--model=small|large] [--strip-error-names]`
- 소스 분기: `comptime if @bits == 16 { ... } else { ... }` - 소스 분기: `comptime if @bits == 16 { ... } else { ... }`
- `bits32`에서 `far` 키워드를 쓰면 컴파일 에러. - `bits32`에서 `far` 키워드를 쓰면 컴파일 에러.
- 표준 라이브러리는 코어 공용, `std.sys` 유닛만 타깃별 구현.
--- ---
@@ -172,7 +171,7 @@ fn read_all(path: str) -> !^[]u8 { // 표준 io는 core.Error로 통일
- `try`는 에러 유니온 반환 함수 안에서만 허용한다. 피연산자의 nominal error 타입은 현재 함수의 error 타입과 정확히 같아야 한다. 다르면 `catch`에서 명시적으로 매핑한다. - `try`는 에러 유니온 반환 함수 안에서만 허용한다. 피연산자의 nominal error 타입은 현재 함수의 error 타입과 정확히 같아야 한다. 다르면 `catch`에서 명시적으로 매핑한다.
- `catch`는 현재 함수의 반환 타입과 무관하게 어디서든 에러를 그 자리에서 처리할 수 있다. - `catch`는 현재 함수의 반환 타입과 무관하게 어디서든 에러를 그 자리에서 처리할 수 있다.
- `try e`: 에러면 현재 함수에서 즉시 반환한다. - `try e`: 에러면 현재 함수에서 즉시 반환한다.
- `e catch |x| { ... }`: 블록은 값을 만들 수 없다. 결과 타입이 `void`이면 정상적으로 끝까지 실행할 수 있고, 값 결과가 필요하면 `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝나야 한다. 값이 필요하면 아래 짧은 형태를 쓴다. (언어에 블록 표현식을 도입하지 않기 위한 선택. §13 참조.) - `e catch |x| { ... }`: 블록은 값을 만들 수 없다. 결과 타입이 `void`이면 정상적으로 끝까지 실행할 수 있고, 값 결과가 필요하면 `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝나야 한다. 값이 필요하면 아래 짧은 형태를 쓴다. (언어에 블록 표현식을 도입하지 않기 위한 선택. §11 참조.)
- `e catch default_value`: 짧은 형태. 우변은 식이며 그 값이 결과가 된다. - `e catch default_value`: 짧은 형태. 우변은 식이며 그 값이 결과가 된다.
- 짧은 `catch`의 우변과 block `catch`의 handler는 피연산자가 error일 때만 평가·실행한다. success이면 handler의 부수 효과·이동·대여가 발생하지 않는다. - 짧은 `catch`의 우변과 block `catch`의 handler는 피연산자가 error일 때만 평가·실행한다. success이면 handler의 부수 효과·이동·대여가 발생하지 않는다.
- 서로 다른 error 타입 간 자동 변환 없음. `!T`(기본 에러 집합 `core.Error`)로 통일하거나 명시 매핑. - 서로 다른 error 타입 간 자동 변환 없음. `!T`(기본 에러 집합 `core.Error`)로 통일하거나 명시 매핑.
@@ -588,15 +587,13 @@ pub fn main() -> !void {
- 이항 연산자는 왼쪽 operand를 먼저, 오른쪽 operand를 나중에 평가한다. - 이항 연산자는 왼쪽 operand를 먼저, 오른쪽 operand를 나중에 평가한다.
- `and``or`는 왼쪽 operand로 결과가 정해지면 오른쪽을 평가하지 않는다. `orelse``catch`도 §4.5·§4.6에 따라 우변/handler가 필요한 경로에서만 평가한다. - `and``or`는 왼쪽 operand로 결과가 정해지면 오른쪽을 평가하지 않는다. `orelse``catch`도 §4.5·§4.6에 따라 우변/handler가 필요한 경로에서만 평가한다.
- 이 순서는 부수 효과뿐 아니라 move, borrow의 시작·마지막 사용, `try` 전파와 defer/drop cleanup 순서를 결정한다. - 이 순서는 부수 효과뿐 아니라 move, borrow의 시작·마지막 사용, `try` 전파와 defer/drop cleanup 순서를 결정한다.
- C backend는 C 자체의 미지정 평가 순서에 의존할 수 없다. C에서 순서가 보장되지 않는 호출 인자, 일반 이항 operand 등의 부수 효과·이동·대여는 lower 단계에서 순서가 명시된 temporary statement로 분해한다. `and`/`or` 같은 C의 단락 규칙을 직접 사용하더라도 Ferro의 lazy 의미를 그대로 보존해야 한다.
--- ---
## 8. 유닛과 빌드 ## 8. 유닛
파일 하나가 유닛 하나다. 유닛의 canonical identity는 fully-qualified dotted unit path이며 파일 하나가 유닛 하나다. 유닛의 canonical identity는 fully-qualified dotted unit path이며
모든 cross-unit 타입·선언·제네릭 identity, `.fei`, C mangling, cache key와 진단에서 같은 모든 cross-unit 타입·선언·제네릭 identity에서 같은 이름을 사용한다.
이름을 사용한다.
```fe ```fe
unit game.main; unit game.main;
@@ -612,8 +609,12 @@ import는 항상 유닛 전체를 가져오며 member는 local unit binding으
`parse.read(...)`, `import std.io;` 뒤에는 `io.write(...)`를 쓴다. `as`가 있으면 그 `parse.read(...)`, `import std.io;` 뒤에는 `io.write(...)`를 쓴다. `as`가 있으면 그
alias가 binding이다. alias가 binding이다.
`std` 최상위 namespace는 compiler-reserved이며 user unit은 선언할 수 없다. 표준 유닛은
`import std.io;`, `import std.mem;`, `import std.fmt;`, `import std.sys;`처럼 가져온다.
`str`은 계속 built-in `[]u8` alias와 alias-method namespace이며 import unit이 아니다.
v0.1은 relative import(`.foo`, `..foo`), glob/selective import, `pub import` re-export, v0.1은 relative import(`.foo`, `..foo`), glob/selective import, `pub import` re-export,
package-private/friend visibility를 지원하지 않는다. package-private/friend visibility, package manager를 지원하지 않는다.
### 8.1 unit 이름과 source path ### 8.1 unit 이름과 source path
@@ -631,42 +632,7 @@ tinyjson.parse -> tinyjson/parse.fe
game.world.map -> game/world/map.fe game.world.map -> game/world/map.fe
``` ```
entry source의 선언이 `unit game.main;`이고 실제 파일이 ### 8.2 binding과 visibility
`C:/PROJECT/SRC/GAME/MAIN.FE`이면 file path 끝의 `game/main.fe` suffix를 ASCII
case-insensitive 방식으로 비교해 제거하고 project source root `C:/PROJECT/SRC`를 얻는다.
unit path/file path 대응은 host filesystem의 기본 case 규칙이 아니라 이 규범적 비교를
써서 DOS와 모든 host에서 같게 처리한다. suffix가 일치하지 않으면 컴파일 에러이며 같은
unit 선언을 임의 위치에서 조용히 허용하지 않는다. case-sensitive host에 `game/main.fe`
`GAME/MAIN.FE`가 별도 실제 파일로 함께 있으면 §8.2의 서로 다른 후보이므로 ambiguous다.
### 8.2 import root와 모호성
candidate root는 다음 집합이다.
1. entry source에서 계산한 project source root
2. 사용자가 지정한 각 `-I <dir>`
3. `std.*`에 대해서만 compiler 내장 std root
`std` 최상위 namespace는 compiler-reserved이며 user unit은 선언할 수 없다. 일반 user
unit은 compiler std root에서 찾지 않는다. 표준 유닛은 `import std.io;`,
`import std.mem;`, `import std.fmt;`, `import std.sys;`처럼 가져온다. `str`은 계속 built-in
`[]u8` alias와 alias-method namespace이며 import unit이 아니다.
root를 순서대로 검사해 첫 성공을 고르지 않고 모든 candidate를 조사한다. 동일 canonical
unit path에 대해 서로 다른 실제 source file이 둘 이상 발견되면 다음 형태의 compile
error를 내고 각 실제 path를 note로 표시한다.
```
ambiguous unit 'foo.bar'
note: ...
note: ...
```
filesystem canonicalization 결과 같은 실제 파일이 여러 root 또는 path alias로 발견된
경우만 하나로 취급할 수 있다. 따라서 `-I` 순서, 디렉터리 열거 순서, host 환경이 source
선택을 바꾸지 않는다.
### 8.3 binding, visibility와 외부 library
한 unit에서 import binding은 다른 unit-scope declaration/import binding과 충돌할 수 없다. 한 unit에서 import binding은 다른 unit-scope declaration/import binding과 충돌할 수 없다.
`import foo.net; import bar.net;`은 둘 다 `net`을 만들므로 에러이며 두 번째를 `import foo.net; import bar.net;`은 둘 다 `net`을 만들므로 에러이며 두 번째를
@@ -679,74 +645,12 @@ function parameter/return, public field 등 외부 signature에 나타나는 nom
importer가 이름을 해석할 수 있어야 하며 private nominal type을 public API에 노출하면 importer가 이름을 해석할 수 있어야 하며 private nominal type을 public API에 노출하면
컴파일 에러다. 컴파일 에러다.
v0.1 외부 library는 source-only import root다. 예를 들어 `-I deps` ### 8.3 순환과 canonical identity
`deps/tinyjson/parse.fe`가 있으면 `import tinyjson.parse;`로 사용한다. package manager,
registry, version solver, manifest dependency 문법은 v0.1에 없다. 미래 package manager도
dependency source tree를 import root에 배치하고 `-I`를 구성하는 도구일 뿐 Ferro import
의미론을 바꾸지 않는다. `.fei + .obj/.lib`만 배포하는 binary-only package ABI도 v0.1은
지원하지 않는다.
### 8.4 `.fei` interface와 cache
`.fei`는 incremental compilation interface, build cache metadata, 다른 unit에서의 generic
instantiation을 위한 compiler interface다. 물리적 binary/text encoding은 구현 세부지만
논리적으로 최소한 다음을 표현할 수 있어야 한다.
- magic/format version, Ferro SPEC/compiler interface version
- target과 해당하는 경우 bits16 memory model
- canonical unit name
- public symbol signature와 public nominal type identity/layout
- anonymous `error.Name` name set
- exported generic declaration metadata와 body token stream
- exported generic이 요구하는 private support symbol metadata
- direct dependency canonical unit name과 dependency interface hash
`.fei` serialization은 결정적이어야 한다. unordered container iteration을 그대로 쓰지
않고 canonical key/name의 byte ordering으로 정렬해 serialize한다. absolute host path,
timestamp, build directory를 기록하지 않는다. 같은 source/interface graph와 target/model이면
build order와 host path에 관계없이 byte-identical `.fei`가 목표다.
다음 hash는 구별한다.
- **source hash**: 해당 unit source 내용 변화 감지
- **interface hash**: dependent unit이 관찰하는 `.fei` 의미 정보의 hash
- **compile cache key**: source hash, target/model/options와 실제 재컴파일에 필요한
direct/indirect dependency interface hash를 포함한 key
private non-generic 구현만 바뀌어 public/generic-visible interface가 같으면 해당 unit은
재컴파일하지만 interface hash는 유지되어 dependent unit을 재컴파일하지 않아도 된다.
public signature/layout 또는 exported generic이 관찰하는 private support 정보가 바뀌면
interface hash가 바뀐다.
### 8.5 순환과 canonical identity
순환 import는 컴파일 에러다. fully-qualified dotted unit path와 선언 이름이 nominal 순환 import는 컴파일 에러다. fully-qualified dotted unit path와 선언 이름이 nominal
identity의 기준이므로 `tinyjson.value.Value``tinyjson.value.Box`처럼 표시한다. nominal identity의 기준이므로 `tinyjson.value.Value``tinyjson.value.Box`처럼 표시한다. nominal
struct/enum/error는 defining unit + declaration name으로 구별되고 type alias는 새 nominal struct/enum/error는 defining unit + declaration name으로 구별되고 type alias는 새 nominal
identity를 만들지 않는다. 이 canonical identity 규칙은 §9 generic cache와 §11.4 C identity를 만들지 않는다.
mangling에도 그대로 적용한다.
```
fec main.fe --target=bits32 -o game.exe
fec main.fe --target=bits16 --model=large --no-checks -o game.exe
fec main.fe --emit-c -o out/ # 트랜스파일 결과만
fec --dump-ast main.fe
```
### 8.6 CLI 플래그 (전체)
| 플래그 | 의미 | 규정 |
|---|---|---|
| `--target=bits16\|bits32` | 타깃 선택 | §2 |
| `--model=small\|large` | `bits16` 메모리 모델 | §2 |
| `-o <경로>` | 출력 파일 또는 디렉터리 | §8 |
| `-I <디렉터리>` | source-only import candidate root 추가 | §8.2·§8.3 |
| `--emit-c` | 트랜스파일 결과만 생성 | §8 |
| `--dump-ast` | AST 덤프 | §8 |
| `--no-checks` | 경계·오버플로·`.?` 검사 제거 | §7.4 |
| `--strip-error-names` | 실행 파일에서 에러 이름 문자열 제거 | §4.6 |
| `--error-table=<파일>` | 유닛 단위 `--emit-c`용 확정 에러 코드 표 | §4.6 |
| `--deny-recursive-drop` | 재귀 drop 경고를 에러로 승격 | §5 R3 |
--- ---
@@ -798,16 +702,9 @@ var xs: List(u8) = List(u8).new();
identity + canonical type argument list**다. type alias는 새 nominal identity가 아니므로 identity + canonical type argument list**다. type alias는 새 nominal identity가 아니므로
underlying/interned canonical type identity로 정규화한다. 따라서 `const Word = i32;` 뒤의 underlying/interned canonical type identity로 정규화한다. 따라서 `const Word = i32;` 뒤의
`id(Word, 1)``id(i32, 2)`는 같은 instance다. `id(Word, 1)``id(i32, 2)`는 같은 instance다.
- 최종 build driver는 모든 unit의 instance request를 모아 canonical key로 중복 제거하고 - exported generic은 definition unit의 private symbol을 참조할 수 있다. 이는 컴파일러
key의 byte ordering으로 정렬한다. 먼저 필요한 prototype을 결정적 순서로 방출하고 이어서 수준의 처리이며 Ferro source visibility를 public으로 바꾸지 않는다 — 다른 Ferro source는
body를 같은 순서로 단일 `fe_generics.c`에 방출한다. request 발견 순서, hash iteration, 여전히 그 private 심볼을 직접 참조할 수 없다.
build order에 의존하거나 사용 unit별 external/static 중복 코드를 만들지 않는다.
- exported generic이 definition unit의 private symbol을 참조하면 `.fei`는 다른 unit에서
instantiate하는 데 필요한 support dependency의 transitive closure를 기록한다. private
non-generic function은 signature와 backend link identity, private nominal type은 필요한
identity/layout/signature, comptime const는 evaluated value/type, private generic은 body
token stream과 자기 support dependency를 제공한다. 이 compiler/link metadata는 Ferro
source visibility를 public으로 바꾸지 않는다.
```fe ```fe
unit lib; unit lib;
@@ -820,8 +717,8 @@ pub fn bump(comptime T: type, x: T) -> T {
} }
``` ```
다른 unit이 요청한 `lib.bump(i32)` instance는 generated internal C symbol을 통해 다른 unit이 요청한 `lib.bump(i32)` instance는 내부적으로 `helper`를 호출할 수 있지만,
`helper`를 호출할 수 있지만, 다른 Ferro source가 `lib.helper`를 직접 참조할 수는 없다. 다른 Ferro source가 `lib.helper`를 직접 참조할 수는 없다.
- 재귀적 인스턴스화의 distinct-instance chain 제한은 32다. 이미 pending/known인 동일 - 재귀적 인스턴스화의 distinct-instance chain 제한은 32다. 이미 pending/known인 동일
canonical instance key를 다시 요청하는 recursion은 pending instance를 재사용하고 depth를 canonical instance key를 다시 요청하는 recursion은 pending instance를 재사용하고 depth를
@@ -832,264 +729,31 @@ pub fn bump(comptime T: type, x: T) -> T {
--- ---
## 10. 표준 라이브러리 (최소 집합) ## 10. 표준 라이브러리
표준 라이브러리는 reserved `std` namespace 아래에 있으며 `import std.io;`처럼 명시적으로 표준 라이브러리 상세 명세는 별도 문서에서 다룬다. 표준 라이브러리는 reserved `std`
가져온다. import 뒤의 local binding은 마지막 segment라 기존처럼 `io.write`, `mem.replace` namespace 아래에 있으며 `import std.io;`처럼 명시적으로 가져온다. import 뒤의 local
형태로 사용한다. 실제 `fec/std` source 배치는 M8에서 이 canonical unit path에 맞춘다. binding은 마지막 segment라 `io.write`, `mem.replace` 형태로 사용한다.
- **`std.core`**: `panic`, `set_panic_handler`, `Error`(기본 에러 집합), `assert`. 아래는 언어 규칙(§4~§7)이 직접 참조하거나 언어 표면(빌트인, 예제)이 전제하는 최소
- **`std.mem`**: `create(value: T) -> !^T`(T는 값에서 추론), `destroy(p)`, `alloc_slice(T, n) -> !^[]T`, `replace(dst: &mut T, value: T) -> T`, `copy(dst: []mut u8, src: []u8)`, `set(dst: []mut u8, v: u8)`, `Arena{ init, alloc, reset, drop }`. 초기화되지 않은 힙을 안전 코드에 반환하는 `create(T)` 형태는 없다. `replace`는 이전 값을 이동해 반환하고 새 값으로 자리를 초기화하며 부분 이동과 재귀 구조의 반복 drop에 사용한다. 표면만 남긴 것이다. `std.list`, `std.map`, `std.io.File`의 전체 API, `std.sys`의 OS
- **문자열/바이트**: `str``[]u8` alias다. 내장 alias 메서드 `eq`, `find`, `starts_with`, `split_at`, `parse_int`, `trim`, `to_cstr`, `from_cstr``line.trim()`처럼 호출하며 `str` 이름의 import 유닛은 두지 않는다. 소유 문자열 `String``^[]u8`을 감싸고 `as_str(self: &Self) -> str`을 제공한다. 접근 함수 등 나머지 모듈의 정확한 시그니처는 표준 라이브러리 명세가 정의한다.
- **`std.list`**: `List(T)`.
- **`std.map`**: `Map(K, V)`(오픈 어드레싱, K는 정수 또는 `String`). `String` key map은 key buffer를 소유하고 조회에는 `get_str(self: &Self, key: str) -> ?&V`를 제공한다. - **`std.core`**: `panic`, `set_panic_handler`, `Error`(기본 에러 집합, §4.6), `assert`. `panic`/`set_panic_handler`는 §7.4 트랩 동작이 참조한다.
- **`std.fmt`**: sink를 소유하지 않는 순수 변환 함수 모음. `fmt_int_i8/i16/i32/u8/u16/u32(buf: []mut u8, v) -> str`, `fmt_hex_*`, `fmt_char`, `fmt_bool`, `fmt_error`, `fmt_int_pad`를 제공한다. 반환 slice는 buf에서 파생된 R8(a) 결과다. `fmt_error``--strip-error-names`를 따른다. - **`std.mem`**: `create(value: T) -> !^T`(T는 값에서 추론), `destroy(p)`, `alloc_slice(T, n) -> !^[]T`, `replace(dst: &mut T, value: T) -> T`, `copy(dst: []mut u8, src: []u8)`, `set(dst: []mut u8, v: u8)`, `Arena{ init, alloc, reset, drop }`. 초기화되지 않은 힙을 안전 코드에 반환하는 `create(T)` 형태는 없다. `replace`는 이전 값을 이동해 반환하고 새 값으로 자리를 초기화하며 부분 이동과 재귀 구조의 반복 drop에 사용한다(§4.5, §5 R3·R7·R11).
- **문자열/바이트**: `str``[]u8` alias다(§4.2). 내장 alias 메서드 `eq`, `find`, `starts_with`, `split_at`, `parse_int`, `trim`, `to_cstr`, `from_cstr``line.trim()`처럼 호출하며(§6.4 예제) `str` 이름의 import 유닛은 두지 않는다.
- **`std.fmt`**: sink를 소유하지 않는 순수 변환 함수 모음이며 `@print`/`@fprint`/`@sprint`(§6.3.1)가 의존한다. `fmt_int_i8/i16/i32/u8/u16/u32(buf: []mut u8, v) -> str`, `fmt_hex_*`, `fmt_char`, `fmt_bool`, `fmt_error`, `fmt_int_pad`를 제공한다. 반환 slice는 buf에서 파생된 R8(a) 결과다. `fmt_error``--strip-error-names`를 따른다.
- **`std.io`**: - **`std.io`**:
```fe ```fe
pub enum Writer { Stdout, Stderr, File(u16), Null } pub enum Writer { Stdout, Stderr, File(u16), Null }
pub enum Reader { Stdin, File(u16) } pub enum Reader { Stdin, File(u16) }
``` ```
둘 다 정수 payload만 가진 Copy handle이며 참조나 raw context pointer를 저장하지 않는다. `io.write(w: Writer, buf: []u8) -> !usize`, `io.read(r: Reader, buf: []mut u8) -> !usize`가 실제 I/O를 수행한다. 닫힌 fd 또는 재사용된 fd를 가진 복사 handle은 I/O 오류나 의도하지 않은 파일 접근이라는 논리 오류를 만들 수 있지만 dangling memory access는 만들지 않는다. 둘 다 정수 payload만 가진 Copy handle이며 참조나 raw context pointer를 저장하지 않는다(§5 R8 예제). `io.write(w: Writer, buf: []u8) -> !usize`, `io.read(r: Reader, buf: []mut u8) -> !usize`가 실제 I/O를 수행한다.
- `File{ open, create, read(self: &mut Self, []mut u8), write(self: &mut Self, []u8), seek, size, writer, reader, close }`. - **`std.sys`**: `exit`, `on_exit(f: fn() -> void) -> !void`. `on_exit`은 §7.4 트랩 동작이 참조하는 allocation 없는 고정 크기 callback registry이며 가득 차면 오류를 반환한다.
- `close(self: Self) -> !void`는 File을 소비하는 일반 메서드이며 `drop`이 아니다. 내부 handle을 먼저 invalid 상태로 만든 뒤 닫기 오류를 반환하므로 함수 종료의 자동 drop은 no-op이다. `drop`은 아직 열린 handle만 오류를 무시하고 닫는다. `drop` 직접 호출 금지는 유지한다.
- 안전한 표준 라이브러리 API는 대여 대상을 가리키는 raw pointer를 값에 숨겨 반환해서는 안 된다. 따라서 v0.1에는 함수 포인터/`*void` 기반 Writer·Reader나 buffer Writer가 없다. `@sprint`는 대상 slice에 직접 복사한다.
- **`std.sys`**: `exit`, `on_exit(f: fn() -> void) -> !void`, `args`, `env`, `ticks`, `int21(regs)`, `dpmi_*`(bits32), `port_in/out`, `far_copy`(bits16). `on_exit`은 allocation 없는 고정 크기 callback registry이며 가득 차면 오류를 반환한다.
--- ---
## 11. 컴파일러 구현 ## 11. 의도적으로 제외한 기능
### 11.1 부트스트랩 전략
1. **컴파일러 A** — C89로 작성. Ferro → C 트랜스파일러. 호스트는 현대 PC 또는 DOS. 출력 C는 DJGPP(gcc, bits32) / Open Watcom(bits16, bits32) / Borland C(bits16)로 컴파일.
2. **컴파일러 B** — Ferro로 A와 동일 구조를 재작성. A로 빌드.
3. **셀프호스팅** — B로 B를 빌드. 그 결과로 다시 B를 빌드해 출력이 바이트 동일(fixpoint)하면 완료. A 폐기.
4. **네이티브 백엔드** — B에 386 코드 생성기 추가, 이후 8086 코드 생성기.
A는 버릴 코드다. 최적화하지 말고 B를 컴파일할 수 있는 최소 언어 부분집합만 지원한다.
### 11.2 파이프라인
```
소스 → lexer → parser(AST) → resolve(이름/import) → check(타입)
→ own(소유권·참조) → lower(소멸자/defer/try/for 전개 → LIR)
→ emit_c(C 소스) [또는 emit_x86]
```
각 단계는 실패해도 가능한 한 진행해 에러를 모아 보고한다(문장 단위 복구).
### 11.3 디렉터리
```
fec/
src/
lexer.c/h 토큰화. 위치(파일, 줄, 열) 보존.
ast.c/h 노드 정의, 아레나 할당자.
parser.c/h LL(1) 재귀하강. 에러 복구는 다음 ';' 또는 '}'까지 스킵.
types.c/h 타입 인터닝(포인터 비교로 동등성), 레이아웃 계산(타깃별).
resolve.c/h 스코프 체인, 심볼 테이블, import, .fei 읽기/쓰기.
check.c/h 타입 검사, 리터럴 타입 결정, match 완전성, R4 위치 검사.
own.c/h §11.5 알고리즘.
lower.c/h AST → LIR. 소멸자/defer 삽입, try/catch/for/메서드 호출 전개.
emit_c.c/h LIR → C. §11.4 규칙.
generic.c/h 인스턴스 캐시, 토큰 재파싱.
driver.c CLI, 유닛 의존 순서, .fei 캐시, fe_errors.h와 fe_generics.c 생성,
외부 C 컴파일러 호출.
rt/ 런타임 (C): trap, 힙, 슬라이스 헬퍼, DPMI/INT21 shim
std/ 표준 라이브러리 (.fe)
tests/ §12
```
위는 목표 구조다. M5까지는 이름 해석·소유권·lower가 `check.c`/`emit_c.c`에 통합되어
있다. R1~R8 전체를 다루는 M6 착수 시점에 `own.c/h`를 분리한다.
### 11.4 C 방출 규칙
| Ferro | C |
|---|---|
| `i16`, `u32` 등 | `int16_t`, `uint32_t` (`<stdint.h>` 없으면 자체 typedef) |
| `usize` | `uint16_t`(bits16) / `uint32_t`(bits32) |
| `bool` | `unsigned char` |
| 일반 `^T`, `*T` | `T*` |
| `^[]T` | `typedef struct { T* p; fe_usize n; } fe_owned_slice_T;` |
| `&T` | `const T*` |
| `&mut T` | `T*` |
| `far X` | `__far X` (Watcom/Borland), bits32는 무시 |
| `[N]T` | `struct { T a[N]; }` (값 의미론 유지, 붕괴 방지) |
| `[]T`/`str` | `typedef struct { const T* p; fe_usize n; } fe_slice_T;` |
| `[]mut T` | `typedef struct { T* p; fe_usize n; } fe_mut_slice_T;` |
| `?T` (포인터류) | 원래 포인터, null 사용 |
| `?^[]T` | `struct { unsigned char has; fe_owned_slice_T v; }` |
| `?T` (그 외) | `struct { unsigned char has; T v; }` |
| `E!T` | `struct { uint16_t e; T v; }`, `!void`는 `uint16_t` |
| `shared [atomic] var x: T` | `volatile T x` |
| struct | `struct fe_<canonical-unit>_<Name>` |
| enum | `struct { uint8_t tag; union { ... } u; }`, 배리언트 256개 초과 시 `uint16_t tag` |
| 함수 | `fe_<canonical-unit>_<name>`, 메서드는 `fe_<canonical-unit>_<Type>_<name>` |
| 제네릭 인스턴스 | `fe_<canonical-unit>_<Name>__<canonical-type-args>` |
세부:
- **canonical mangling**: C symbol은 canonical dotted unit path + declaration name + canonical
type argument list를 collision-free하게 encode한다. `.`의 separator/escaping 문자는 구현
세부지만 host path, pointer address, insertion/hash iteration order를 사용할 수 없다.
type alias는 canonical underlying identity를 쓰고 nominal struct/enum/error는 fully-qualified
defining unit + name을 쓴다. generic instance 이름도 같은 key에서만 생성해 M12 fixpoint에서
byte-identical해야 한다.
- **Ferro visibility와 C linkage**: Ferro `private`는 source name visibility이며 반드시 C
`static`을 뜻하지 않는다. exported generic instance가 definition unit의 private helper를
호출할 수 있도록 generic body가 필요로 하는 private top-level function/global을
deterministic unit-mangled external C symbol로 방출하고 내부 generated header에 prototype을
제공할 수 있다. 이는 resolver의 private 접근을 완화하지 않으며 다른 Ferro unit source의
직접 참조는 계속 에러다. 필요한 closure는 §9와 `.fei` metadata가 제공한다.
- **오버플로 검사**: `fe_add_i16(a, b, LINE)` 인라인 함수. `--no-checks`면 매크로가 `((a)+(b))`로 축약.
- **경계 검사**: `fe_idx_T(s, i, LINE)` → `(i < s.n ? s.p[i] : (fe_trap_bounds(LINE), s.p[0]))`. `for` 루프는 직접 인덱스.
- **`try`**: `{ Ttmp t = expr; if (t.e) return (RetT){ t.e }; }` 후 `t.v` 사용. defer/소멸자가 있으면 return 전에 정리 코드 삽입.
- **`catch`**: `t.e`가 참일 때만 block 또는 짧은 RHS를 평가하고 바인딩 변수는 `t.e`다.
- **`defer`/소멸자**: lower 단계에서 스코프 종료 지점(정상 흐름, `return`, `break`, `continue`, `try` 전파)마다 역순 호출을 명시적으로 삽입. C의 goto 라벨을 써도 되고 복제해도 된다(A는 복제, B는 goto 권장).
- **조건부 이동**: 이동 여부가 분기에 따라 다르면 `unsigned char fe_live_<var> = 1;` 플래그 삽입, drop 전에 검사.
- **`match`**: `switch (x.tag)`. Copy payload는 지역 변수로 복사하고 non-Copy projection payload는 R7에 따라 참조로만 바인딩한다. 소유값 추출은 match 전 `mem.replace`로 수행한다.
- **`asm`**: Intel 문법으로 고정 저장. Watcom/Borland는 그대로, gcc는 `__asm__(".intel_syntax noprefix\n" ...)`로 감싼다.
- **`@print` 계열**: emit 단계에는 도달하지 않는다. lower 단계에서 `fmt.fmt_*`로 임시 `[]mut u8`에 변환하고 `io.write` 또는 `mem.copy`를 호출하는 나열로 전개한다. `@print`는 각 I/O 오류를 버리고, `@fprint`는 첫 오류를 전파하며, `@sprint`는 남은 길이를 추적해 잘라 쓴 실제 길이를 반환한다. 포맷 문자열 조각은 static const shared slice로 방출하고 동일 문자열은 중복 제거한다.
- **참조와 aliasing**: `&T` → `const T*` 방출은 aliasing 가정을 하지 않는다. `&mut T`에도 `restrict`를 붙이지 않으며, M13/M14 네이티브 백엔드도 noalias를 가정하지 않는다. R6의 배타성은 R10의 전역 대여 금지가 함께 성립할 때만 프로그램 전체에서 유지되므로, 방출 단계에서 이를 최적화 근거로 쓰지 않는다.
- **에러 코드**: 드라이버가 emit 전에 확정한 `error.Name`의 `u16` 코드를 단일 `fe_errors.h`의 `#define`으로 방출한다(§4.6). 모든 유닛 C가 이 헤더를 include하므로 에러 `match`를 `switch`로 방출할 수 있고 이름 집합 변경 시 C 재방출 없이 오브젝트만 무효화한다.
- **논리 연산**: `and`, `or`, `not`은 각각 C의 `&&`, `||`, `!`로 방출한다. `and`와 `or`는 C의 시퀀스 포인트와 단축 평가를 그대로 사용한다.
- **평가 순서**: §7.6의 callee-first, operand/argument left-to-right 순서를 지킨다. C가
순서를 보장하지 않는 일반 호출 인자와 이항 operand에 부수 효과·move·borrow가 있으면
lower가 순서대로 temporary statement를 만들고 emit은 그 결과만 조합한다. cleanup과
`try` 전파도 같은 순서를 따른다.
- **공유 상태와 임계 구역**: `shared`는 `volatile`로 방출한다. bits16의 `critical`은 compiler barrier → FLAGS 저장 → `cli` 순서로 진입하고 모든 이탈에서 저장한 FLAGS 복원 → compiler barrier 순서로 끝낸다. GCC 계열은 `asm volatile("" ::: "memory")`, Open Watcom/Borland는 optimizer가 내용을 볼 수 없는 별도 runtime 함수 호출 경계를 사용한다. 한 명령 크기의 `shared atomic` 단일 접근은 volatile load/store만 방출한다.
- **방출 순서**: `fe_errors.h` → typedef 전방선언 → struct 정의(동률을 canonical name으로
끊는 의존 위상 정렬) → 전역 → 함수 프로토타입 → 함수 본문 → 통합 `fe_generics.c`.
unordered container나 source 발견 순서에 기대지 않는다. `fe_generics.c`는 canonical
instance prototype을 먼저, body를 나중에 각각 key byte ordering으로 방출한다.
- 유닛 하나당 `.c` 하나, `.fei`에서 필요한 부분은 `.h`로 생성한다. 제네릭 인스턴스 본문은 유닛 C에 중복 방출하지 않는다.
### 11.5 own.c 알고리즘
함수 단위. 각 지역 변수/파라미터에 상태:
```
Uninit | Owned | Moved | MaybeMoved | Shared(n) | Exclusive
```
초기화 여부가 경로마다 다른 합류에는 `MaybeUninit` 또는 같은 의미의 별도 bit/state를
사용할 수 있다. projection은 root local/parameter를 찾는 데만 사용하고 field/index별
대여 상태는 만들지 않는다(R6).
1. 먼저 AST를 역방향 순회해 각 참조 변수의 경로별 마지막 사용을 계산한다. `defer` 안의 사용은 해당 스코프 끝으로 올린다.
2. AST를 §7.6의 평가 순서로 순회하며 상태 전이한다. 표현식의 place 사용을 읽기 / 이동 / `&` 대여 / `&mut` 대여 / 쓰기 / projection으로 분류하고 projection의 root 상태를 갱신한다.
3. 이동: `Owned → Moved`. `Moved`/`MaybeMoved` 사용 시 에러(최초 이동 위치를 표시). field/index/`.?` projection의 비-Copy 이동은 R7에 따라 거부하고 `mem.replace`만 허용한다.
4. `&place`: root의 `Owned → Shared(n+1)`. `&mut place`: root의 `Owned → Exclusive`. 역방향 pass가 계산한 마지막 사용에서 해제하며 임시는 문장 끝에 해제한다. 서로 다른 field/index도 같은 root 상태와 충돌한다.
5. 호출 인자의 `&mut → &`, `[]mut → []`만 Exclusive를 유지하는 호출 기간의 임시 shared view로 검사한다. 일반 `let`/대입에는 이 implicit transition을 적용하지 않는다.
6. `Shared`/`Exclusive` 상태에서 금지된 쓰기/이동/재대여를 진단한다(R6).
7. **분기 합류**: `if`/`match`의 각 branch를 독립 상태로 계산한다. branch exit 전에 last-use/liveness에 따라 끝난 borrow를 먼저 해제하고 다음 규칙으로 병합한다.
| 왼쪽 | 오른쪽 | 결과 |
|---|---|---|
| 같은 상태 | 같은 상태 | 같은 상태 |
| `Owned` | `Moved` | `MaybeMoved` |
| `Moved` | `Owned` | `MaybeMoved` |
| `MaybeMoved` | `Owned`/`Moved`/`MaybeMoved` | `MaybeMoved` |
| `Uninit` | `Owned` 등 initialized 상태 | `MaybeUninit` 또는 동등 상태 |
merge는 좌우 대칭이다. `MaybeMoved`/`MaybeUninit` 값의 이후 읽기·이동은 에러이고 drop은 필요한 runtime live
flag를 쓴다. 한 경로에서만 borrow가 계속 살아 있으면 합류 뒤에도 살아 있는 것으로
보수적으로 취급한다. `Shared(n)`과 `Shared(m)`은 필요한 live shared borrow의 합집합을
보존하고 단순 count 구현에서는 적어도 `Shared(max(n,m))`으로 합친다. 한쪽만
`Exclusive`가 live여도 합류 뒤 root를 `Exclusive` 효과로 잠근다. 서로 양립할 수 없는
`Shared`/`Exclusive` 상태는 더 약한 상태로 풀지 않고 양쪽 효과를 보존하는 보수적
상태로 합치거나 compile error를 낸다. 구현은 state enum/bitset을 확장할 수 있지만 이
의미를 만족해야 한다.
8. **루프 fixed point**: loop 진입 상태로 body를 한 번 분석하고 종료/backedge 상태를
진입 상태와 위 규칙으로 병합한다. 그 merged 상태로 body를 두 번째 분석한다. v0.1
compiler A는 이 2-pass를 사용하며 두 번째 분석 뒤에도 의미 상태가 안정되지 않으면
compile error다. loop 밖에서 생성되어 loop 안에서 이후 사용되는 borrow는 필요하면
loop 전체에 걸쳐 live로 보고, body에서 생성된 borrow가 backedge를 넘는 경우도 같은
fixed-point에 포함한다. 첫 iteration만 안전하다는 이유로 허용하지 않는다.
9. R4 위반은 check 단계에서 타입만 보고 거부한다. 단 `static str`은 initializer가 문자열 리터럴인지 함께 확인한다.
10. R8 반환 경로마다 `Static`/`Param(N)` provenance를 계산하고 §5 R8 lattice로 합류한다.
결과 바인딩을 허용하며 `Param(N)` 원본 대여를 결과의 마지막 사용까지 전파한다.
메서드는 `Param(self)`만, 자유 함수는 유일한 참조성 parameter의 `Param(N)`만 허용하고
provenance를 lowered signature/`.fei`에 기록한다.
에러 메시지 형식: `file:line:col: error: <설명>` + 관련 위치 `file:line:col: note: <최초 이동/대여 위치>`.
### 11.6 마일스톤
| # | 내용 | 완료 기준 |
|---|---|---|
| M1 | lexer, parser, AST 덤프 | `--dump-ast`가 std 소스 전체를 파싱 |
| M2 | 타입 검사 + C 방출: 정수, 함수, if, while | bits32 hello world 실행 |
| M3 | struct, enum, match, 배열, `[]T`/`[]mut T`, 경계 검사, `str` alias | 공유/배타 슬라이스와 문자열 처리 예제 통과 |
| M4 | **`@print`/`@fprint`/`@sprint` 빌트인** (§6.3.1), handle enum `io.Writer`, 순수 `fmt.fmt_*` | `@print` 오류 삼킴, `@fprint` 전파, `@sprint` 잘림/길이와 인자 타입 진단, safe Writer dangling 불가 |
| M5 | `^T`/`^[]T`, drop, defer, 이동·부분 이동 검사 | 누수/이중해제, 소비 close, `mem.replace` 테스트 통과 |
| M6 | `&`, `&mut`, 배타성 검사 (own.c 전체) | R1~R8 실패 테스트 통과 |
| M7 | `?T`, `E!T`, try/catch | `std.io` 유닛 동작 |
| M8 | 유닛/import/.fei, 의존 hash, `fe_errors.h`, 분리 컴파일, std 초안 | 다중 유닛 증분·결정적 빌드 |
| M9 | **제네릭** (통합 모노모피제이션) | `fe_generics.c`로 `List(T)`, `Map(K,V)` 중복 없이 빌드 |
| 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배 |
| M14 | 8086 네이티브 백엔드 | Watcom 없이 bits16 빌드 |
배치 근거:
- **M4(포매팅)를 앞에 두는 이유**: 구현이 작고(check + lower 합쳐 300줄 안팎) 언어 표면에 새 개념을 추가하지 않는다. 이후 모든 마일스톤의 디버깅과 M11의 컴파일러 B 에러 출력이 여기에 의존한다.
- **M9(제네릭)가 M10보다 앞인 이유**: 제네릭 없이 표준 라이브러리를 쓰는 기간을 최소화한다.
- **인터페이스(`dyn`)는 마일스톤에 없다**: 부트스트랩 경로에 불필요하고 타입 시스템 전반에 영향을 준다. §13의 v0.2 1순위로 미룬다. 그때까지 dangling이 불가능한 Copy handle enum `io.Writer`/`io.Reader`를 사용한다.
---
## 12. 테스트
```
tests/
m<N>/ 마일스톤별 fixture. bad-*.fe는 fail 규약을 따른다
pass/*.fe + *.expected 컴파일→실행→stdout 비교
fail/*.fe 첫 줄 "// ERROR:<line>:<메시지 일부>"
run16/*.fe bits16 빌드 후 QEMU FreeDOS 실행, 출력 파일 비교
boot/ A/B 출력 비교, fixpoint 검증
```
- `fail/`은 규칙별 최소 3개: R1(이동 후 사용), R3(직접 drop 호출), R4(필드에 공유/배타 slice, `*[]T`), R5(스코프 초과), R6(배타성 위반), R7(무효화·projection 부분 이동), R8(자유 함수 참조성 파라미터 2개, 메서드의 non-self 파생, 가변성 승격), R9(unsafe 밖 raw 역참조, `*void` 역참조), R10(전역 `var` 대여), match 완전성, 암묵 변환, 타입 불일치, `let`에서 mutable slice 생성.
- 포매팅(§6.3.1) 전용 `fail/` 케이스: `{}` 개수 > 인자 개수, 인자 개수 > `{}` 개수, 미지원 verb(`{q}`), 런타임 값 포맷 문자열, 대응 `fmt_*` 없는 타입, 닫히지 않은 `{`, `try @print(...)`(void).
- 포매팅 `pass/` 케이스: 각 verb 1개 이상, `{{` 이스케이프, 인자 0개, `@print` 오류 삼킴, `@sprint` 길이/잘림, handle enum `@fprint`, 동일 `fmt.fmt_*` 결과 재사용.
- R6·R8 전용 `pass/` 케이스: 참조의 마지막 사용 이후 원본 재접근, 분기별 마지막 사용의 합류, R8(a) 결과를 지역 변수에 바인딩한 뒤 대여 종료 후 원본 접근, R8(b)의 문자열 리터럴 반환, `line.trim()` 형태의 슬라이스 반환 연쇄.
- `@compile_error`, `@as_far_fn`, `@call_far`의 comptime/타깃/unsafe 제약과 `error.Name`의
`core.Error` 등록, 결정적 `fe_errors.h`, `--strip-error-names`, `fmt.fmt_error`를 각각 pass/fail로 검증한다.
- **M6 필수 edge case**:
- 서로 다른 struct field의 `&mut`라도 같은 root borrow 충돌, 서로 다른 array index도 같은 root borrow 충돌.
- 호출 인자의 `&mut → &`/`[]mut → []` 약화 성공과 일반 `let` binding의 같은 암묵 약화 실패.
- R8 `Static`/`Param(N)` 합류 성공, 서로 다른 `Param` provenance 합류 실패.
- 한 branch에서만 live인 borrow의 보수적 합류, `Owned`/`Moved` 및 초기화 상태 합류.
- loop-carried borrow와 move가 2-pass fixed point에서 안정되는 경우와 불안정해 거부되는 경우.
- **M7 필수 edge case**:
- 문맥 없는 `null` 실패와 parameter/명시 타입으로 결정되는 contextual `null` 성공.
- error declaration의 code 0, 중복 member 이름, 중복 숫자 code 실패.
- `E!T` expected 위치의 success/failure contextual construction과 nominal error 자동 변환 실패.
- `orelse`, 짧은/block `catch`의 lazy side effect·move·borrow.
- non-Copy `Some` pattern이 destructive extraction이 아님을 확인하고 projection 소유 추출에는 `mem.replace`가 필요함을 검증.
- **M8 필수 edge case**:
- dotted unit/import와 alias, 마지막 segment binding, binding conflict.
- unit path/file suffix mismatch, uppercase segment와 8자 초과 segment 거부.
- 서로 다른 root의 동일 canonical unit ambiguity와 같은 canonical file 중복 발견의 dedup.
- reserved `std.*` lookup, 일반 user unit이 builtin std root에서 발견되지 않음.
- private type을 public API에 노출하는 경우 실패와 dotted prefix가 private 권한을 주지 않음.
- private-only non-generic 구현 변경 뒤 dependency interface hash 안정 및 dependent cache hit.
- source/interface graph와 target/model이 같을 때 build order, `-I` order, absolute checkout path가 달라도 byte-identical `.fei`.
- **M9 필수 edge case**:
- value generic과 generic type inference 거부, 명시 type argument 성공.
- alias/underlying type의 instance dedup과 동일 canonical instance body 1회 방출.
- exported generic이 definition-unit private helper/private generic을 호출하는 support closure.
- 같은 instance recursion의 pending 재사용과 distinct growing-instance chain depth 32 초과 실패.
- generic request 발견 순서와 build order가 달라도 byte-identical `fe_generics.c`.
- invalid dependent operation의 definition 위치 primary error와 `instantiated here` chain note.
- 각 마일스톤은 해당 기능의 pass/fail 테스트와 함께 완료한다.
- 정식 회귀 실행은 QEMU FreeDOS 내부 Open Watcom의 `TEST-DOS.BAT`로 전 타깃·마일스톤
gate를 확인한다. host compiler 결과는 편집 보조일 뿐 완료 판정에 사용하지 않는다.
---
## 13. 의도적으로 제외한 기능
**등급 정의** **등급 정의**
- `영구` — §1 철학과 정면 충돌. v2.0에서도 넣지 않는다. - `영구` — §1 철학과 정면 충돌. v2.0에서도 넣지 않는다.
@@ -1100,7 +764,7 @@ tests/
| 기능 | 등급 | 제외 이유 | 대체 수단 | | 기능 | 등급 | 제외 이유 | 대체 수단 |
|---|---|---|---| |---|---|---|---|
| 트레잇/인터페이스 (`dyn`) | **v0.2 (1순위)** | 부트스트랩에 불필요, 타입 시스템 전반에 영향 | Copy handle enum (`io.Writer`, §10) | | 트레잇/인터페이스 (`dyn`) | **v0.2 (1순위)** | 부트스트랩에 불필요, 타입 시스템 전반에 영향 | Copy handle enum (`io.Writer`, §10) |
| bits32 interrupt/shared/critical | v0.2 | DPMI callback·vector 복원과 backend 지원이 M10 범위 밖 | bits16 실행 파일 또는 polling | | bits32 interrupt/shared/critical | v0.2 | DPMI callback·vector 복원과 backend 지원이 아직 범위 밖 | bits16 실행 파일 또는 polling |
| 클로저 | v0.2 | 캡처 = 참조 저장 = R4 위반 소지 | 콜백에 `ctx: *void` 전달 | | 클로저 | v0.2 | 캡처 = 참조 저장 = R4 위반 소지 | 콜백에 `ctx: *void` 전달 |
| 연산자 오버로딩 | v0.2 (인터페이스 이후) | 숨은 비용. 넣더라도 특정 인터페이스 구현으로만 제한 | 메서드 | | 연산자 오버로딩 | v0.2 (인터페이스 이후) | 숨은 비용. 넣더라도 특정 인터페이스 구현으로만 제한 | 메서드 |
| 튜플 / 다중 반환 | 편의 | 이름 없는 필드는 가독성 손해 | struct | | 튜플 / 다중 반환 | 편의 | 이름 없는 필드는 가독성 손해 | struct |
@@ -1117,7 +781,7 @@ tests/
| 암묵 형변환 | 영구 | 버그 원인 1위 | `as` | | 암묵 형변환 | 영구 | 버그 원인 1위 | `as` |
| 상속 | 영구 | 숨은 vtable, 취약한 기반 클래스 | 합성 | | 상속 | 영구 | 숨은 vtable, 취약한 기반 클래스 | 합성 |
### 13.1 인터페이스 설계 스케치 (v0.2 예정) ### 11.1 인터페이스 설계 스케치 (v0.2 예정)
지금 구현하지 않되, 나중에 `io.Writer` Copy handle enum을 무리 없이 대체할 수 있도록 방향만 고정해 둔다. 지금 구현하지 않되, 나중에 `io.Writer` Copy handle enum을 무리 없이 대체할 수 있도록 방향만 고정해 둔다.
@@ -1134,7 +798,7 @@ dump(&mut file, buf); // &mut File → &mut dyn Writer 자동 변환
- `dyn I`의 표현은 `(ctx, vtable)` 팻 포인터. vtable은 `(인터페이스, 구현 타입)` 쌍마다 `static const` 하나. - `dyn I`의 표현은 `(ctx, vtable)` 팻 포인터. vtable은 `(인터페이스, 구현 타입)` 쌍마다 `static const` 하나.
- **동적 디스패치 전용.** 제네릭 타입 제약(trait bound)으로는 쓸 수 없다 — 그걸 허용하면 전역 분석이 생긴다. - **동적 디스패치 전용.** 제네릭 타입 제약(trait bound)으로는 쓸 수 없다 — 그걸 허용하면 전역 분석이 생긴다.
- `&dyn I`는 참조이므로 R4가 적용된다(필드 저장 불가). 필드에 담으려면 `^dyn I`(힙 박싱). - `&dyn I`는 참조이므로 R4가 적용된다(필드 저장 불가). 필드에 담으려면 `^dyn I`(힙 박싱).
- `^dyn I`의 drop은 vtable 경유. 이 때문에 `?^dyn I`, drop 전개, 제네릭 인자로서의 `dyn` 등 타입 시스템 여러 곳에 케이스가 추가되므로 독립 마일스톤으로 다룬다. - `^dyn I`의 drop은 vtable 경유. 이 때문에 `?^dyn I`, drop 전개, 제네릭 인자로서의 `dyn` 등 타입 시스템 여러 곳에 케이스가 추가되므로 독립으로 다룬다.
- 도입 시 `io.Writer`/`io.Reader` handle enum을 `dyn` 기반 API로 교체한다. v0.1 safe API에는 이미 대여 대상을 숨긴 `*void`가 없으므로 이 전환은 기능 확장이지 안전성 수정이 아니다. - 도입 시 `io.Writer`/`io.Reader` handle enum을 `dyn` 기반 API로 교체한다. v0.1 safe API에는 이미 대여 대상을 숨긴 `*void`가 없으므로 이 전환은 기능 확장이지 안전성 수정이 아니다.
위 표에 없는 항목(링크타임 최적화, 디버그 정보 포맷, 언어 서버 등)은 도구 영역이며 v0.2 이후 별도 검토. 위 표에 없는 항목(링크타임 최적화, 디버그 정보 포맷, 언어 서버 등)은 도구 영역이며 v0.2 이후 별도 검토.