From ef7939f6095529c5232c5084ab5fc76747511482 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 16:12:53 +0900 Subject: [PATCH] =?UTF-8?q?GOAL=20P2:=20SPEC=20=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=A3=BD=EC=9D=80=20C=20=EB=B0=B1=EC=97=94=EB=93=9C=EB=A5=BC?= =?UTF-8?q?=20=EA=B1=B7=EC=96=B4=EB=82=B8=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 파이프라인은 .fe → i386 asm → wasm → wlink → .exe 인데 명세는 아직 C 를 방출하는 컴파일러를 서술하고 있었다. 외부 감사가 그 문서를 충실히 읽고 존재하지 않는 문제(C 의 부호 있는 오버플로 UB)를 보고했다 -- 명세가 거짓말을 하면 그것을 읽는 사람이 틀린 답을 낸다. 걷어낸 것: .fei 심볼 파일과 그 위에 얹힌 증분 빌드 서술, fe_errors.h, --emit-c 와 --error-table, '호스트 C 방출', 'C 방출 시 static inline', '별도 C 표현'. 오류 코드 절은 실제대로 다시 썼다: 빌드 하나가 모든 유닛의 소스를 함께 읽고, 드라이버가 emit 전에 쓰인 이름을 모아 철자순으로 1부터 매긴다. 유닛 하나만 따로 코드 생성까지 밀고 갈 수 없다는 것도 그 결과로 적었다. R7 이 컴파일러 소스 파일 이름(own.c)을 대고 있던 것도 언어의 말로 바꿨고, 이동은 변수 단위이고 대여는 place 단위라는 구분을 붙였다. --target= 과 --model= 은 드라이버에서 없앴다. 타깃이 하나인데 받아들여서 무시하는 플래그는 안 받는 것보다 나쁘다. SPEC 이 약속만 하고 구현이 없는 것 셋을 TODO 에 적었다: --strip-error-names, fmt.fmt_error, 0b/0o 리터럴 값 계산. 240/240, 35/35. --- SPEC.md | 34 +++++++++++++--------------------- TODO.md | 3 +++ fec/src/driver.c | 5 ++++- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/SPEC.md b/SPEC.md index 9d4a4b8..60baaa6 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,7 +1,7 @@ # Ferro 언어 명세 v0.1.8 DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. -파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. +파일 확장자 `.fe`, 컴파일러 이름 `fec`. 별도의 인터페이스 파일은 없다 — 빌드 하나가 모든 유닛의 소스를 함께 읽는다. 이 문서는 Ferro 언어 명세만 다룬다. 컴파일러 구현 지시서와 표준 라이브러리 상세 명세는 별도 문서에서 다룬다. 명세 판단이 애매한 부분은 §1 철학과 §5 소유권 규칙을 기준으로 결정한다. @@ -112,7 +112,7 @@ and or not orelse - `[]mut T`는 `[]T`로, `&mut T`는 `&T`로 **호출 인자 위치에서만** 암묵 재대여할 수 있다. 이것은 호출 동안의 read-only view이며 원래 배타 대여는 원래 마지막 사용까지 유지된다. 일반 `let`/대입에는 이 암묵 약화를 적용하지 않는다. 장기 shared borrow가 필요하면 root/place에서 명시적으로 새 `&` 또는 shared slice를 만들고 R6 검사를 받는다. - **배타 대여를 호출에 넘기는 것은 이동이 아니라 그 호출 동안의 재대여다.** `&mut T`를 `&mut T` 파라미터에, `[]mut T`를 `[]mut T` 파라미터에 넘기면 호출이 끝날 때 돌려받는다. 호출이 도는 동안 호출자는 그 값에 손댈 수 없으므로 별칭이 생기지 않는다. 이것이 없으면 배타 파라미터를 다시 넘기는 일이 함수당 한 번만 가능해져서 `&mut`가 사실상 쓸 수 없게 된다. - `^[]T`는 "슬라이스를 가리키는 포인터"가 아니라 길이를 함께 소유하는 독립 타입이다. R4의 일반 `^T` 대상 제한의 예외이며 `?^[]T`도 허용한다. `*[]T`/`*[]mut T`는 계속 금지한다. `mem.alloc_slice(T, n)`가 반환하고 drop 시 버퍼를 해제한다. -- `str`은 nominal 타입이 아니라 미리 선언된 `const str = []u8;` type alias다. UTF-8 검증을 보장하지 않으며 문자열 리터럴은 정적 읽기 전용 `[]u8`이다. 따라서 별도 변환 규칙이나 별도 C 표현은 없다. +- `str`은 nominal 타입이 아니라 미리 선언된 `const str = []u8;` type alias다. UTF-8 검증을 보장하지 않으며 문자열 리터럴은 정적 읽기 전용 `[]u8`이다. 따라서 별도 변환 규칙이나 별도 표현은 없다. ### 4.3 구조체 @@ -196,25 +196,18 @@ fn read_all(path: str) -> !^[]u8 { // 표준 io는 core.Error로 통일 - 실패를 복구하지 않고 트랩으로 바꾸려면 `expr catch @trap()`을 쓴다. v0.1에는 별도 `must` 키워드를 두지 않는다. `error.Name`은 선언된 error 타입을 만들지 않고 기본 `core.Error`의 이름 있는 -멤버를 참조하는 익명 에러 값이다. 각 유닛은 사용한 이름을 `.fei`에 기록한다. -최종 빌드에서 **드라이버가 emit 단계 이전에** 모든 유닛의 `.fei`에서 사용된 이름을 -합치고 중복을 제거한 뒤 이름의 바이트순으로 정렬하여 1부터 `u16` 코드를 부여한다. -드라이버는 이 표를 단일 생성 헤더 `fe_errors.h`의 `#define` 정수 상수로 방출한다. -따라서 서로 다른 유닛의 `error.Name`은 같은 값이고 빌드 순서와 병렬 컴파일에도 -결과가 결정적이며 `switch` case 라벨로 쓸 수 있다. -이름 집합이 바뀌면 `fe_errors.h`와 그 헤더에 의존하는 오브젝트를 무효화하지만, -각 유닛의 `.c`는 재방출하지 않는다. 각 `.fei`는 그 unit이 사용하는 이름 집합만 -기록하므로 global 번호 재배정 때문에 다시 쓰지 않으며, source에서 이름 사용 자체가 -바뀐 unit의 `.fei` interface만 갱신한다. +멤버를 참조하는 익명 에러 값이다. **드라이버가 emit 단계 이전에** 빌드에 든 모든 +유닛에서 쓰인 이름을 모아 중복을 제거하고 이름의 바이트순으로 정렬하여 1부터 +`u16` 코드를 부여한다. 따라서 서로 다른 유닛의 `error.Name`은 같은 값이고, +빌드 순서와 무관하게 결과가 결정적이다. 빌드 디렉터리 이력에 따라 번호가 달라지는 append-only 표는 금지한다. -유닛 단위 `--emit-c`는 전체 이름 집합을 알 수 없으므로 `--error-table=<파일>`로 -확정된 표를 받아야 하며, 없으면 컴파일 에러다. -이름이 65,535개를 넘으면 컴파일 에러다. 명시적인 `error` 선언은 여전히 nominal +번호는 빌드 전체를 봐야 정해지므로 유닛 하나만 따로 코드 생성까지 밀고 갈 수는 +없다. 이름이 65,535개를 넘으면 컴파일 에러다. 명시적인 `error` 선언은 여전히 nominal 타입이며, 같은 멤버 이름이나 숫자 코드를 가진 다른 선언 및 `core.Error`와 자동 변환되지 않는다. `error.Name`의 타입은 `core.Error`이며 `core.Error!T` 또는 축약형 `!T`를 반환하는 함수에서만 직접 반환할 수 있다. `--strip-error-names`를 사용하면 실행 파일과 런타임 오류 문자열에서 이름을 -제거하지만 숫자 코드와 `.fei`의 타입/코드 일관성 정보는 유지한다. +제거하지만 숫자 코드는 유지한다. `fmt.fmt_error`는 이 정책에 따라 `core.Error` 값을 이름 또는 코드로 포맷한다. ### 4.7 타입 동등성과 alias @@ -280,7 +273,7 @@ r.^ = 1; // r의 마지막 사용 x += 1; // OK — 여기서 r의 대여는 이미 끝났다 ``` -**R7 (참조 무효화와 부분 이동).** 참조 대상이 이동되거나 재대입되면 그 참조는 이후 사용 시 에러. own.c는 변수 단위 상태만 추적하므로 field/index/`.?` projection에서 비-Copy 소유값을 이동해 꺼내는 것은 금지한다. `mem.replace(&mut place, replacement)`로 유효한 대체값을 남기면서 꺼내야 한다. 배열의 선택적 소유 원소는 `?^T`로 두고 `mem.replace(&mut arr[i], null).?`로 꺼낸다. projection chain 자체(`p.?.^`, `s.field.x`)는 값을 소비하지 않는다. +**R7 (참조 무효화와 부분 이동).** 참조 대상이 이동되거나 재대입되면 그 참조는 이후 사용 시 에러. 이동 상태는 변수 단위로만 추적하므로 field/index/`.?` projection에서 비-Copy 소유값을 이동해 꺼내는 것은 금지한다(대여는 §5 R6대로 place 단위로 갈라지지만, 이동은 그렇지 않다). `mem.replace(&mut place, replacement)`로 유효한 대체값을 남기면서 꺼내야 한다. 배열의 선택적 소유 원소는 `?^T`로 두고 `mem.replace(&mut arr[i], null).?`로 꺼낸다. projection chain 자체(`p.?.^`, `s.field.x`)는 값을 소비하지 않는다. 이 금지에는 예외가 하나 있다. **타입 자신의 `drop` 안에서는 `self`의 projection에서 값을 꺼낼 수 있다.** 그 객체는 사라지는 중이고 `drop`이 돌아간 뒤에 그것을 읽을 수 있는 코드가 없으므로, R7이 막으려는 "뒤에 남은 반쪽짜리 값"이 생기지 않는다. 다른 함수에서는 예외가 없다. @@ -295,7 +288,7 @@ x += 1; // OK — 여기서 r의 대여는 이미 끝났다 - `Static`: 문자열 리터럴 또는 `static`에서 파생되어 caller local borrow를 만들지 않는다. - `Param(N)`: 시그니처로 정해진 하나의 참조성 parameter에서 파생된다. 메서드는 `Param(self)`만 허용하며 다른 참조성 인자에서 파생되면 에러다. 자유 함수는 기존 규칙대로 참조성 parameter가 정확히 하나여야 한다. -control-flow 합류는 `Static + Static → Static`, `Static + Param(N) → Param(N)`, `Param(N) + Param(N) → Param(N)`이다. 서로 다른 `Param` provenance가 합류하면 컴파일 에러다. `?&T`/`?[]T`의 `null` 반환 경로는 caller borrow를 만들지 않는 경로이므로 static/null 경로와 `Param(N)` 경로가 합쳐지면 전체를 보수적으로 `Param(N)`으로 본다. 이 provenance는 함수 시그니처와 lowered `.fei` interface metadata에 기록할 수 있어야 한다. +control-flow 합류는 `Static + Static → Static`, `Static + Param(N) → Param(N)`, `Param(N) + Param(N) → Param(N)`이다. 서로 다른 `Param` provenance가 합류하면 컴파일 에러다. `?&T`/`?[]T`의 `null` 반환 경로는 caller borrow를 만들지 않는 경로이므로 static/null 경로와 `Param(N)` 경로가 합쳐지면 전체를 보수적으로 `Param(N)`으로 본다. 이 provenance는 함수 시그니처만 보고 결정할 수 있어야 한다. 호출 지점에서 `Param(N)` 결과는 **정해진 파생 원본을 대여한 것으로 취급**한다. 즉 결과를 지역 변수에 바인딩할 수 있으며, 그 대여가 사는 동안 원본에 R6·R7이 그대로 적용된다. `Static` 결과는 caller local borrow를 만들지 않는다. @@ -422,8 +415,7 @@ orelse_expr := expr 'orelse' expr 12 기본: literal, ident, '(' expr ')', struct_literal, @builtin(...) ``` -- `and`, `or`는 단축 평가한다. 호스트 C 방출에서는 각각 `&&`, `||`로 - 매핑하며, 평가 순서와 단락 규칙은 Ferro 의미론을 그대로 유지한다. +- `and`, `or`는 단축 평가한다. 좌변이 답을 정하면 우변을 평가하지 않는다. - `orelse`와 `catch`도 lazy다. 좌변이 각각 `Some`/success이면 우변 또는 handler를 평가하지 않는다(§4.5·§4.6). - `as`는 후위 우선순위(단항보다 강함)지만 단항 연산자 바로 뒤에 `as`가 나타나면 모호한 비용을 숨기지 않도록 괄호를 강제한다. `(-x) as u32`와 `-(x as u32)`는 허용하고 `-x as u32`는 컴파일 에러다. @@ -789,7 +781,7 @@ binding은 마지막 segment라 `io.write`, `mem.replace` 형태로 사용한다 | 튜플 / 다중 반환 | 편의 | 이름 없는 필드는 가독성 손해 | struct | | 레이블 있는 break | 편의 | — | 플래그 변수 | | 슬라이스 패턴 매칭 | 편의 | — | 인덱스 비교 | -| `inline fn` | 편의 | — | C 방출 시 `static inline` | +| `inline fn` | 편의 | — | 인라인 여부는 백엔드가 정한다 | | `must` 키워드 | 편의 | 실패를 트랩으로 바꾸는 문법 설탕일 뿐 핵심 의미론이 아님 | `expr catch @trap()` | | 블록 표현식 | 편의 | 값을 만드는 블록이 없으면 `catch`가 짧은 형태로 충분하고, 문법 표면이 작아진다 (§4.6) | `catch <식>`, `return`으로 탈출 | | 라이프타임 표기 (`'a`) | **구조적 불가** | 전역 분석 필요, R4를 풀어야 함 | R4 (2급 참조), R8 파생 반환, 인덱스 핸들 | diff --git a/TODO.md b/TODO.md index e5142df..d00362f 100644 --- a/TODO.md +++ b/TODO.md @@ -53,6 +53,9 @@ uv run python tests/exec.py 31/31 컴파일된 프로그램이 실제로 | `interrupt` `shared` `atomic` `critical` | 파싱만 되고 의미 없음. SPEC §11 에서 v0.2 | | lowering 미구현 진단 | `internal: cannot lower X`. 사용자 오류처럼 보이지 않는다 | | 배열·enum 페이로드의 자동 해제 | `release_at` 은 구조체 필드까지만 내려간다 | +| `--strip-error-names` | 받아들이지만 아무것도 하지 않는다 (SPEC §4.6) | +| `fmt.fmt_error` | 없다. SPEC §4.6 이 약속만 하고 있다 | +| `0b` / `0o` 리터럴 | 렉서는 받지만 값 계산이 10진과 16진만 안다 | --- diff --git a/fec/src/driver.c b/fec/src/driver.c index 2a6ce9b..5b721bd 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -60,7 +60,10 @@ int main(int argc, char **argv) else if(strcmp(argv[i],"-o")==0 && i+1