# Ferro 언어 명세 v0.1.6 DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. 파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. 이 문서는 언어 명세 + 컴파일러 구현 지시서를 겸한다. 구현 중 애매한 부분은 §1 철학과 §5 소유권 규칙을 기준으로 결정한다. --- ## 1. 설계 철학 1. **안전은 기본, 위험은 명시.** 기본 코드는 메모리 안전(널 역참조, 버퍼 오버런, use-after-free, 이중 해제 불가). 위험한 연산은 `unsafe {}` 블록 안에서만. 2. **전역 분석 금지.** 모든 검사(타입, 소유권, 참조)는 함수 하나만 보고 완결되어야 한다. 이 제약이 라이프타임 표기를 없애고, 640KB 머신에서 셀프호스팅을 가능하게 한다. 3. **숨은 비용 없음.** 힙 할당, 복사, 소멸자 호출, 형변환이 전부 소스에 보인다. GC 없음, 예외 없음, 암묵 변환 없음. 4. **읽히는 문법.** `이름: 타입` 순서, 좌→우 파싱, LL(1) 재귀하강으로 처리 가능. 5. **작게 시작.** 기능을 넣기 전에 뺄 이유를 먼저 찾는다. 뺀 것과 그 대체 수단은 §13에 기록한다. 6. **기존 도구체인 재사용.** 링커, `.OBJ`/`.LIB`/`.EXE` 포맷, DPMI 익스텐더를 새로 만들지 않는다. --- ## 2. 타깃 | | `bits16` | `bits32` | |---|---|---| | CPU/모드 | 8086 리얼모드 | 386 보호모드 플랫 (DPMI) | | `usize`/`isize` | 16비트 | 32비트 | | 포인터 크기 | near 2B / far 4B | 4B (far 없음) | | 메모리 모델 | small, large | flat | | 시스템 호출 | INT 21h 직접 | DPMI 서비스 + 실모드 콜백 | - CLI: `fec main.fe --target=bits16|bits32 [--model=small|large] [--strip-error-names]` - 소스 분기: `comptime if @bits == 16 { ... } else { ... }` - `bits32`에서 `far` 키워드를 쓰면 컴파일 에러. - 표준 라이브러리는 코어 공용, `sys` 유닛만 타깃별 구현. --- ## 3. 어휘 구조 - 식별자: `[A-Za-z_][A-Za-z0-9_]*`. 대소문자 구분. - 주석: `//` 줄 끝까지, `/* */` **중첩 허용**. - 정수 리터럴: `123`, `0xFF`, `0b1010`, `0o17`, 자릿수 구분 `1_000_000`. - 문자 리터럴: `'a'`, `'\n'`, `'\x41'` → 타입 `char`. - 문자열 리터럴: `"abc"` → 타입 `str`. NUL 종료 아님. 이스케이프는 문자 리터럴과 동일. 인접 리터럴 자동 연결 없음. - 불린: `true`, `false`. 옵셔널 널: `null`. - 세미콜론 필수. 블록 중괄호 필수(단문 `if`도 `{}` 필요). **예약어:** ``` unit import pub fn struct packed enum error const static var let if else while for in match return break continue defer 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 ``` --- ## 4. 타입 시스템 ### 4.1 기본 타입 - 정수: `i8 i16 i32 u8 u16 u32 usize isize` - `bool` (1바이트, 정수와 상호 변환 없음) - `char` (`u8`과 크기 같지만 별개 타입). `char`와 `u8` 사이의 저장·대입·비교에는 반드시 명시적인 `as` 변환이 필요하며, 리터럴에도 문맥 기반 암묵 변환을 적용하지 않는다. - `void` (반환 타입으로만) - `type` (comptime 파라미터에서만, §9) **정수 규칙:** - 서로 다른 정수 타입 간 암묵 변환 없음. `as`로 명시. - `as`는 절단/부호확장을 수행하며 값 손실을 검사하지 않는다. - `+ - * / %`는 검사 빌드에서 오버플로 시 트랩. `+% -% *%`는 랩어라운드(항상 무검사). - `/`, `%`의 0 나눗셈은 항상 트랩(검사 빌드 여부 무관, CPU가 트랩함). - 시프트 `<< >>`: 우변은 `u8`. 시프트 양이 비트폭 이상이면 검사 빌드에서 트랩. - 비트 연산 `& | ^ ~`는 같은 타입끼리만. ### 4.2 복합 타입 | 문법 | 의미 | 표현 | |---|---|---| | `[N]T` | 배열, 값 타입, N은 컴파일타임 상수 | `N * sizeof(T)` | | `[]T` | 슬라이스 (참조성, §5 R4 적용) | `(ptr, len)` | | `str` | 불변 바이트 슬라이스. `[]u8`과 **별개 타입** | `(ptr, len)` | | `^T` | 소유 포인터 (힙, 단일 소유자) | 포인터 | | `&T` | 공유 참조 | 포인터 | | `&mut T` | 배타 참조 | 포인터 | | `*T` | raw 포인터 (`unsafe`에서만 역참조) | 포인터 | | `far ^T`, `far *T`, `far &T` | far 포인터 (`bits16` 전용) | 4바이트 | | `?T` | 옵셔널 | 널 표현 가능 타입은 크기 동일, 아니면 `(bool, T)` | | `E!T` / `!T` | 에러 유니온 (`!T`는 기본 에러 집합) | `(u16 err, T val)` | | `fn(A, B) -> R` | 함수 포인터 | 포인터 | - **배열은 포인터로 붕괴하지 않는다.** 함수에 넘기려면 `arr[..]`로 슬라이스를 만들거나 `&arr` / `^[N]T`를 쓴다. - 슬라이싱: `arr[..]`, `arr[a..b]`(반개구간, 경계 검사), `arr[a..]`, `slice[a..b]`. - 소유 슬라이스가 필요하면 `^[]T`(길이 있는 힙 버퍼)를 쓴다. `mem.alloc_slice(T, n)`가 반환. - `str`은 별칭이 아니라 별개의 내장 타입이다. `[]u8`에서 `str`로는 `as str`로 변환할 수 있다(가변성 약화이므로 안전). `str`에서 `[]u8`로의 변환은 불변성을 깨므로 금지한다. `str`의 원소는 읽기만 가능하며 `s[i] = v`는 컴파일 에러다. 문자열 리터럴은 읽기 전용 저장 영역에 놓이므로 이 구분이 필요하다. ### 4.3 구조체 ```fe pub struct Point { x: i32, y: i32, pub fn new(x: i32, y: i32) -> Point { return Point{ x: x, y: y }; } pub fn len2(self: &Self) -> i32 { return self.x*self.x + self.y*self.y; } pub fn shift(self: &mut Self, dx: i32) { self.x += dx; } } ``` - 리터럴: `Point{ x: 1, y: 2 }`. 모든 필드 명시 필수(기본값 없음). - 메서드는 struct 블록 안에 정의. 첫 파라미터가 `self: Self | &Self | &mut Self`면 메서드. - `x.f(y)`는 `Point.f(x, y)`의 설탕. 자동 참조 취함(`x.shift(1)`은 `Point.shift(&mut x, 1)`). - `Self`는 자기 타입의 별칭. - 필드 레이아웃은 선언 순서. 정렬은 타깃 규칙(`bits16`은 1바이트 정렬, `bits32`는 자연 정렬). `packed struct`로 정렬 강제 해제. - 소멸자: `fn drop(self: &mut Self)`를 정의하면 스코프 종료 시 자동 호출(§5 R3). ### 4.4 열거형 (태그드 유니온) ```fe pub enum Shape { Empty, Circle(i32), Rect{ w: i32, h: i32 }, } ``` - 표현: `struct { u8 tag; union {...} payload; }`. 배리언트 256개 초과 시 `u16` 태그. - 페이로드 없는 배리언트만 있는 열거형은 정수처럼 취급되며 `as u8` 가능. - 생성: `Shape.Circle(5)`, `Shape.Rect{ w: 3, h: 4 }`, `Shape.Empty`. - 해체는 `match` 또는 `if let`으로만. 직접 필드 접근 불가. ### 4.5 옵셔널 ```fe var p: ?^Node = null; if let Some(node) = p { node.value = 1; } // node: &mut Node (p가 mut일 때) let v = p.?; // null이면 트랩 let v = p orelse default_node; // null이면 우변 ``` - `?T`에서 T가 `^T`, `&T`, `*T`, `fn`이면 널 포인터를 널 표현으로 사용(크기 증가 없음). - 검사 없이 역참조 불가. `p.^`는 컴파일 에러, `p.?.^`가 필요. - `Some`과 `None`은 `if let`과 `match`의 패턴 위치에서만 옵셔널 해체를 의미하는 문맥 키워드다. 다른 위치에서는 일반 식별자이며 §3의 예약어가 아니다. ### 4.6 에러 ```fe pub error IoError { NotFound = 1, Denied = 2, Eof = 3, } fn read_all(path: str) -> IoError!^[]u8 { let f = try io.open(path); // 에러면 즉시 반환 defer f.close(); let n = f.size() catch |e| { return e; }; ... } ``` - `error` 선언은 `u16` 코드 집합. 코드 0은 "성공" 예약, 사용 불가. - `E!T` 함수만 `try`/`catch` 사용 가능. - `try e`: 에러면 현재 함수에서 즉시 반환(현재 함수도 에러 유니온을 반환해야 함). - `e catch |x| { ... }`: 블록은 값을 만들 수 없다. `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝나야 한다. 값이 필요하면 아래 짧은 형태를 쓴다. (언어에 블록 표현식을 도입하지 않기 위한 선택. §13 참조.) - `e catch default_value`: 짧은 형태. 우변은 식이며 그 값이 결과가 된다. - 서로 다른 error 타입 간 자동 변환 없음. `!T`(기본 에러 집합 `core.Error`)로 통일하거나 명시 매핑. - 에러는 값이다. 언와인딩, 스택 추적, 소멸자 이외의 자동 정리 없음. - 실패를 복구하지 않고 트랩으로 바꾸려면 `expr catch @trap()`을 쓴다. v0.1.2에는 별도 `must` 키워드를 두지 않는다. `error.Name`은 선언된 error 타입을 만들지 않고 기본 `core.Error`의 이름 있는 멤버를 참조하는 익명 에러 값이다. 각 유닛은 사용한 이름을 `.fei`에 기록한다. 최종 빌드에서 **드라이버가 emit 단계 이전에** 모든 유닛의 `.fei`에서 사용된 이름을 합치고 중복을 제거한 뒤 이름의 바이트순으로 정렬하여 1부터 `u16` 코드를 부여한다. 따라서 서로 다른 유닛의 `error.Name`은 같은 값이고 빌드 순서와 병렬 컴파일에도 결과가 결정적이며, 방출되는 C에서 **컴파일타임 정수 상수**이므로 `switch` case 라벨로 쓸 수 있다. 이름 집합이 바뀌면 코드가 재배치되므로 방출된 `.c`와 오브젝트 캐시는 전부 무효화된다. `.fei` 자체는 코드가 아니라 이름만 기록하므로 무효화되지 않는다. 유닛 단위 `--emit-c`는 전체 이름 집합을 알 수 없으므로 `--error-table=<파일>`로 확정된 표를 받아야 하며, 없으면 컴파일 에러다. 이름이 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 타입 동등성 이름 기반(nominal). 필드가 같아도 다른 이름이면 다른 타입. 별칭은 `const Alias = Type;`으로 만들며 완전 동일 취급. `str`은 이 의미의 별칭이 아니라 `[]u8`과 구별되는 별개의 내장 타입이다(§4.2). --- ## 5. 소유권과 참조 — 핵심 규칙 이 절이 언어의 핵심이다. 모든 규칙은 **함수 하나만 보고** 검사된다. **R1 (단일 소유자).** 모든 값의 소유자는 정확히 하나. 변수 대입, 함수 인자 전달, 반환은 **이동(move)**이다. 이동된 변수는 이후 사용 시 컴파일 에러. **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)` 사용). `^Self` 또는 `?^Self`를 재귀적으로 포함한 타입은 기본 필드 drop이 스택 깊이에 비례할 수 있으므로 컴파일러가 경고한다. 이런 연결 구조는 `mem.replace(&mut link, null)`로 소유 링크를 하나씩 꺼내 반복 해제하고 필드를 빈 값으로 남기는 사용자 `drop`을 정의해야 하며, `--deny-recursive-drop`으로 경고를 에러로 바꿀 수 있다. **R4 (참조는 2급 값).** `&T`, `&mut T`, `[]T`는 다음 위치에만 존재할 수 있다: - 함수 파라미터 - 지역 변수 (`let`/`var`) - 표현식 안의 임시값 다음은 **컴파일 에러**다: - struct/enum 필드의 타입 - 배열/슬라이스의 원소 타입 - 함수 반환 타입 (예외: R8) - `^T`, `*T`의 대상 타입 - 전역 변수의 타입 이 한 줄이 라이프타임 표기 전체를 불필요하게 만든다. **R5 (참조 수명).** 지역 참조 변수는 대상보다 오래 살 수 없다. R4 덕분에 대상은 항상 같은 함수의 지역 변수, 파라미터, 또는 `static` 전역이므로 스코프 중첩 확인만으로 검사된다. 가변 전역에 대한 대여는 R10이 금지한다. **R6 (배타성).** `&mut x`가 살아있는 동안 `x`에 대한 다른 참조 생성, 직접 읽기/쓰기, 이동이 금지된다. `&x`(공유)는 여러 개 동시 가능하지만 그동안 `x`에 쓰기/이동 금지. 참조의 생존 구간은 **참조 변수의 마지막 사용 지점까지**다. 그 이후에는 원본에 대한 접근·이동이 다시 허용된다. 조건부 흐름에서는 모든 경로의 마지막 사용 중 가장 나중 지점을 취한다. 임시 참조(`f(&x)`)는 그 문장 끝까지다. 이 판정은 함수 지역 liveness 분석이며 함수 밖 정보를 쓰지 않으므로 §1.2를 위반하지 않는다. ```fe var x: i32 = 0; let r = &mut x; r.^ = 1; // r의 마지막 사용 x += 1; // OK — 여기서 r의 대여는 이미 끝났다 ``` **R7 (참조 무효화).** 참조 대상이 이동되거나 재대입되면 그 참조는 이후 사용 시 에러. **R8 (파생 반환).** 함수는 다음 두 경우에 한해 `&T`, `&mut T`, `[]T`, `str`을 반환할 수 있다. **(a) 파라미터 파생.** 파라미터 중 참조성 타입(`&T`, `&mut T`, `[]T`, `str`)이 **정확히 하나**이고, 반환값이 그 파라미터에서 파생된 것임을 컴파일러가 함수 본문만 보고 확인할 수 있을 때. 파생이란 슬라이싱, 인덱싱, 필드 접근, `&`/`&mut` 취함, 그리고 다른 R8(a) 함수 호출의 연쇄를 말한다. 반환의 가변성은 원본 이하여야 한다(`&T`에서 `&mut T`를 만들 수 없다). 메서드의 `self`도 이 "하나"에 해당한다. **(b) 정적 파생.** 반환값이 문자열 리터럴 또는 `static` 선언에서 파생된 경우. 이때는 참조성 파라미터가 없어도 된다. 호출 지점에서 R8(a)의 결과는 **그 인자를 대여한 것으로 취급**한다. 즉 결과를 지역 변수에 바인딩할 수 있으며, 그 대여가 사는 동안 인자 원본에 R6·R7이 그대로 적용된다. R8(b)의 결과는 대여를 만들지 않는다. ```fe pub fn trim(s: str) -> str { ... } // R8(a) let t = str.trim(line); // OK. line은 t의 대여 구간 동안 잠긴다 list.at(0).x = 5; // OK let r = list.at(0); // OK. list를 &mut로 대여 list.push(1); // 에러: r이 list를 대여 중 (R6) pub fn name() -> str { return "main"; } // R8(b) ``` 참조성 파라미터가 둘 이상인 함수는 어느 쪽에서 파생되었는지가 시그니처만으로 결정되지 않으므로 참조성 반환을 할 수 없다. 그런 함수가 필요하면 인덱스(`usize`)나 핸들을 반환한다. **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`가 필요 없다. 인터럽트 핸들러와 메인 흐름이 함께 접근하는 값은 반드시 `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` 시그니처에 기록한다. - 전역에 대한 대여는 다음으로 제한한다. `static`(불변)은 `&`로 대여할 수 있다. 일반 전역 `var`는 `&`·`&mut` 모두 대여할 수 없으며 직접 읽기와 쓰기만 허용한다. `shared var`는 `critical` 안에서의 직접 접근만 허용하고 대여할 수 없다. 전역 값을 참조로 넘겨야 하면 지역 변수로 복사한 뒤 대여한다. - 이 제한의 근거는 R6다. 전역에 대한 대여가 살아 있는 동안 호출된 다른 함수가 같은 전역에 직접 접근할 수 있고, 그것은 함수 단위 지역 검사로 검출할 수 없다. 아래는 이 제한이 없으면 통과해 버리는 예다. ```fe var G: i32 = 0; fn f(r: &mut i32) { G = 5; } // r과 G가 같은 곳을 가리키는지 f는 알 수 없다 fn g() { f(&mut G); } // 제한이 없으면 g의 지역 검사는 통과한다 ``` - 전역에는 `^T`나 `drop` 있는 타입을 둘 수 없다. `shared`, `atomic`, `critical`, `interrupt fn`은 v0.1.2에서 `bits16` 전용이며 `bits32`에서 사용하면 컴파일 에러다. **R11 (재귀·그래프 구조).** `^T`는 R4의 2급 참조가 아니므로 소유가 한 방향인 단방향 리스트와 트리는 필드에 저장할 수 있다. 반면 양방향 리스트·순환·일반 그래프는 역방향 필드에 `^T`를 두면 R1의 단일 소유권을 위반하고 `&T`를 두면 R4를 위반한다. 이런 구조는 아레나/배열이 값을 소유하고 `u16`/`u32` 인덱스 핸들이 간선을 나타내도록 구현한다. 표준 라이브러리 `mem.Arena`를 사용할 수 있으며, 핸들 역참조 때 세대 번호 또는 경계 검사를 사용해 해제된 항목 접근을 막아야 한다. --- ## 6. 문법 ### 6.1 EBNF ``` unit := 'unit' ident ';' import* decl* import := 'import' ident ';' decl := ['pub'] (fn_decl | struct_decl | enum_decl | error_decl | const_decl | global_decl) fn_decl := ['extern' string] [('interrupt' | 'interrupt_safe')] 'fn' ident '(' [param (',' param)*] ')' ['->' type] (block | ';') param := ['comptime'] ident ':' type struct_decl := ['packed'] 'struct' ident '{' member* '}' member := ['pub'] (field | fn_decl) field := ident ':' type ',' enum_decl := 'enum' ident '{' variant (',' variant)* [','] '}' variant := ident | ident '(' type ')' | ident '{' vfield* '}' vfield := ident ':' type ',' error_decl := 'error' ident '{' ident '=' int (',' ident '=' int)* [','] '}' const_decl := 'const' ident [':' type] '=' expr ';' global_decl := 'static' ident ':' type '=' expr ';' | 'var' ident ':' type '=' expr ';' | 'shared' ['atomic'] 'var' ident ':' type '=' expr ';' block := '{' stmt* '}' stmt := 'let' ident [':' type] '=' expr ';' | 'var' ident [':' type] ['=' expr] ';' | 'const' ident [':' type] '=' expr ';' | lvalue ('=' | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<=' | '>>=') expr ';' | if_stmt | while_stmt | for_stmt | match_stmt | 'return' [expr] ';' | 'break' ';' | 'continue' ';' | 'defer' block | 'unsafe' block | 'critical' block | 'comptime' 'if' expr block ['else' (block | 'if' ...)] | 'asm' '{' asm_body '}' | expr ';' if_stmt := 'if' (expr | 'let' pattern '=' expr) block ['else' (block | if_stmt)] while_stmt := 'while' expr block for_stmt := 'for' ident [',' ident] 'in' expr block match_stmt := 'match' expr '{' arm+ '}' arm := pattern '=>' (expr ';' | block) pattern := ident // 배리언트, 페이로드 없음 | ident '(' ident ')' // 튜플형 배리언트 바인딩 | ident '{' ident (',' ident)* '}' // 필드형 배리언트 바인딩 | 'Some' '(' ident ')' | 'None' | int_literal | '_' 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)* ')' // 제네릭 인스턴스 catch_expr := expr 'catch' ['|' ident '|'] (expr | block) orelse_expr := expr 'orelse' expr ``` `member`의 `pub`은 필드와 메서드 모두에 개별로 붙는다(§8). 필드와 메서드는 순서를 섞어 쓸 수 있다. `catch`의 블록 형태는 값을 만들지 않으며 §4.6의 규칙을 따른다. ### 6.2 표현식 우선순위 (낮음 → 높음) 표현식 문법은 다음 우선순위 표가 규범이다. 각 단계는 명시가 없으면 좌결합 이항 연산으로 전개하며, 단항·후위·기본 단계에 나열된 형태가 그대로 프로덕션이 된다. `catch`와 `orelse`의 구체적 형태는 §6.1을 따른다. ``` 1 orelse, catch 2 or 3 and 4 == != < <= > >= 5 | ^ 6 & 7 << >> 8 + - +% -% 9 * / % *% 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`는 에러). ### 6.3 빌트인 ``` @size_of(T) -> usize @align_of(T) -> usize @bits -> comptime int @target -> comptime str @ptr_cast(T, p) -> *T (unsafe) @seg_ptr(seg: u16, off: u16) -> far *T (unsafe, bits16) @port_in8(p) @port_in16(p) @port_out8(p,v) @port_out16(p,v) (unsafe) @volatile_load(p) @volatile_store(p, v) (unsafe) @trap() -> never @unreachable() -> never (unsafe) @line() @file() // 진단용 @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 포매팅 빌트인 가변 인자를 언어에 도입하지 않는다. `@print` 계열은 **컴파일 단계에서 여러 호출로 전개되는 빌트인**이다. ```fe @print("x={} y={x} name={s}\n", a, b, s); ``` → lower 단계에서 다음으로 전개: ``` fmt.write_str(out, "x="); fmt.write_int_i32(out, a); fmt.write_str(out, " y="); fmt.write_hex_u16(out, b); fmt.write_str(out, " name="); fmt.write_str(out, s); fmt.write_str(out, "\n"); ``` 규칙: - 포맷 문자열은 **컴파일타임 문자열 리터럴 또는 `const`만**. 런타임 값이면 에러. - verb: `{}` 기본(정수/bool/char/str 자동), `{x}` 16진, `{c}` 문자, `{s}` 문자열/슬라이스, `{b}` 불린. `{{`는 `{` 이스케이프. - `{}` 개수와 인자 개수 불일치 → 컴파일 에러. - 인자 타입에 대응하는 `fmt.write_*` 함수가 없으면 컴파일 에러(메시지에 타입명 표시). - 자릿수/폭/정렬 지정자는 v0.1.1에 없음. 필요하면 `fmt.write_int_pad`를 직접 호출. - `@fprint`의 첫 인자는 `&mut io.Writer`(§10의 함수 포인터 struct). - `@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 예제 ```fe unit vga; import sys; const WIDTH: u16 = 320; const HEIGHT: u16 = 200; pub fn set_mode13() { unsafe { asm { mov ax, 0x0013; int 0x10; } } } pub fn put_pixel(x: u16, y: u16, c: u8) { if x >= WIDTH or y >= HEIGHT { return; } unsafe { let vram: far *u8 = @seg_ptr(0xA000, 0); @volatile_store(vram + (y * WIDTH + x) as usize, c); } } ``` ```fe unit main; import io; import list; import fmt; fn count_lines(path: str) -> !usize { let f = try io.open(path, io.Read); defer f.close(); var buf: [256]u8 = undefined; var n: usize = 0; while true { let got = try f.read(buf[..]); if got == 0 { break; } for c in buf[0..got] { if c.^ == '\n' as u8 { n += 1; } } } return n; } pub fn main() -> !void { let n = count_lines("data.txt") catch |e| { fmt.print_str("failed: "); fmt.print_int(e as u16); return e; }; fmt.print_int(n); } ``` --- ## 7. 의미론 세부 ### 7.1 변수와 초기화 - `let`은 불변, `var`는 가변 선언이다. 두 형태 모두 초기값이 있으면 타입을 추론할 수 있다. `var x: T;`와 `var x: T = undefined;`처럼 초기값이 없거나 `undefined`이면 타입 명시가 필수다. - 모든 변수는 사용 전 초기화 필수(정적 검사). 명시적 미초기화는 `= undefined`(unsafe 아님, 단 읽기 전 쓰기 필수는 여전히 검사). - 섀도잉 허용(같은 스코프에서 `let` 재선언). ### 7.2 제어 흐름 - `for x in slice`: `x`는 `&T`(가변 슬라이스면 `&mut T`). 값 접근은 `x.^`. - `for i, x in slice`: `i: usize`. - `for i in a..b`: 정수 범위. - 이 루프 형태들은 경계 검사를 생략한다(컴파일러가 안전을 보장). - `while`은 `bool` 조건만. - `match`는 **완전성 검사**. 모든 배리언트를 다루거나 `_` 필요. - `if`/`while`/`for`/`match`/`comptime if` 헤더 바로 뒤의 `{`는 항상 해당 제어 흐름의 본문 또는 arm 블록을 시작한다. 따라서 구조체 초기화식을 헤더의 최상위 식으로 직접 쓸 때는 `match (Point{ x: 1, y: 2 }) { ... }`처럼 괄호로 감싸 구조체 초기화의 `{`를 명시한다. 괄호 안의 구조체 초기화는 일반 식 규칙을 따른다. - `break`/`continue`는 가장 안쪽 루프에만 적용(레이블 없음). - `defer block`은 스코프 종료 시 역순 실행. 소멸자와 함께 선언 역순으로 병합 실행. `return`/`break`/에러 전파 경로에서도 실행. ### 7.3 함수 호출 규약 - 기본: `bits32`는 cdecl, `bits16`은 타깃 C 컴파일러 기본. - `extern "c" fn name(...) -> T;` — 본문 없이 선언, C 심볼과 링크. 이름 맹글링 없음. 인자/반환에 `^T`, 슬라이스, 에러 유니온 사용 불가(`*T`, `usize`만). - `interrupt fn name()` — 모든 레지스터 보존 + `iret`. 파라미터/반환 없음. 주소는 `@as_far_fn(name)`으로 획득. 호출 제한과 공유 상태 규칙은 R10을 따른다. - `interrupt_safe fn name(...)` — 인터럽트 문맥에서 호출 가능한 함수. ABI는 일반 함수와 같고 R10의 제한을 본문 검사로 만족해야 한다. - 큰 struct(> 4바이트)는 숨은 포인터로 반환(C ABI 따름). ### 7.4 검사와 트랩 트랩 발생 조건: 배열/슬라이스 경계 초과, 정수 오버플로, 0 나눗셈, `?T`의 `.?` 실패, `@trap()`. 동작: `core.panic(msg: str, file: str, line: u32)` 호출 → 메시지 출력 → `sys.exit(3)`. 사용자가 `core.set_panic_handler`로 교체 가능. `--no-checks` 빌드에서 제거되는 것: 경계 검사, 오버플로 검사, `.?` 검사. **절대 제거되지 않는 것:** 소유권/참조 검사, 옵셔널 타입 검사, `match` 완전성 — 전부 컴파일타임이므로. ### 7.5 comptime - `const` 선언의 초기값은 컴파일타임 평가(정수 연산, `@size_of`, `@bits`, 다른 const). - `comptime if`는 평가되지 않는 분기를 **파싱은 하되 타입 검사/코드 생성하지 않는다**(타깃별 분기용). - 함수의 `comptime` 파라미터는 §9 제네릭. - 재귀 평가 깊이 제한 256, 초과 시 에러. --- ## 8. 유닛과 빌드 - 파일 하나 = 유닛 하나. 첫 줄은 `unit <이름>;`이며 파일명과 일치해야 함. - `import bar;` → 같은 검색 경로의 `bar.fe`. 접근은 `bar.name`. - `pub` 붙은 선언만 외부 노출. 구조체 필드도 개별 `pub` 필요. - 순환 import 금지(에러). - 유닛 컴파일 시 `.fei` 생성: pub 선언 시그니처, 타입 레이아웃, 제네릭 본문 토큰. 소스 해시가 같으면 재컴파일 생략. - 검색 경로: `-I