GOAL P2: SPEC 에서 죽은 C 백엔드를 걷어낸다
파이프라인은 .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.
This commit is contained in:
@@ -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 파생 반환, 인덱스 핸들 |
|
||||
|
||||
@@ -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진만 안다 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+4
-1
@@ -60,7 +60,10 @@ int main(int argc, char **argv)
|
||||
else if(strcmp(argv[i],"-o")==0 && i+1<argc) out_path=argv[++i];
|
||||
else if(strncmp(argv[i],"--std=",6)==0) std_root=argv[i]+6;
|
||||
else if(strcmp(argv[i],"--no-checks")==0) no_checks=1;
|
||||
else if(strncmp(argv[i],"--target=",9)==0 || strncmp(argv[i],"--model=",8)==0 || strcmp(argv[i],"--strip-error-names")==0) { }
|
||||
/* One target (SPEC 2), so --target= and --model= are gone: a flag
|
||||
that is accepted and does nothing is worse than one that is not
|
||||
accepted at all. */
|
||||
else if(strcmp(argv[i],"--strip-error-names")==0) { }
|
||||
else if(argv[i][0]!='-') file=argv[i];
|
||||
else if(strcmp(argv[i],"--help")==0){usage();return 0;}
|
||||
else {fprintf(fe_diag_stream(),"fec: unknown option %s\n",argv[i]);return 2;}
|
||||
|
||||
Reference in New Issue
Block a user