diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..56a06c1 --- /dev/null +++ b/.clangd @@ -0,0 +1,6 @@ +# yaml-language-server: $schema=https://json.schemastore.org/clangd.json +CompileFlags: + Add: + - -xc + - -std=c89 + - -Ifec/src diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ad4b9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Legacy local QEMU state is no longer used, but may contain large user-owned images. +.qemu/ + +# Reproducible DOSBox-X/Open Watcom development cache and ephemeral runs +.dosboxx/ + +# Windows reserved-device artifact: `> nul` under Git Bash creates a real file. +# Committing it breaks checkout on Windows. +nul + +# Local logs and editor/OS metadata +*.log +*.tmp +*.swp +*~ +.DS_Store +Thumbs.db + +# Python/uv environment and caches +.venv/ +__pycache__/ +*.py[cod] + +# Node/tool caches +node_modules/ +.npm/ +.cache/ + +# host build output of the front end +.build/ diff --git a/.pi/agents/Explore.md b/.pi/agents/Explore.md new file mode 100644 index 0000000..801991f --- /dev/null +++ b/.pi/agents/Explore.md @@ -0,0 +1,36 @@ +--- +description: "Fast read-only search agent for locating code. Use it to find files by pattern (eg. \"src/components/**/*.tsx\"), grep for symbols or keywords (eg. \"API endpoints\"), or answer \"where is X defined / which files reference Y.\" Do NOT use it for code review, design-doc auditing, cross-file consistency checks, or open-ended analysis — it reads excerpts rather than whole files and will miss content past its read window. When calling, specify search breadth: \"quick\" for a single targeted lookup, \"medium\" for moderate exploration, or \"very thorough\" to search across multiple locations and naming conventions." +display_name: Explore +tools: read, bash, grep, find, ls +model: gpt-5.6-luna +prompt_mode: replace +--- + +# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS +You are a file search specialist. You excel at thoroughly navigating and exploring codebases. +Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access to file editing tools. + +You are STRICTLY PROHIBITED from: +- Creating new files +- Modifying existing files +- Deleting files +- Moving or copying files +- Creating temporary files anywhere, including /tmp +- Using redirect operators (>, >>, |) or heredocs to write to files +- Running ANY commands that change system state + +Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find, cat, head, tail. + +# Tool Usage +- Use the find tool for file pattern matching (NOT the bash find command) +- Use the grep tool for content search (NOT bash grep/rg command) +- Use the read tool for reading files (NOT bash cat/head/tail) +- Use Bash ONLY for read-only operations +- Make independent tool calls in parallel for efficiency +- Adapt search approach based on thoroughness level specified + +# Output +- Use absolute file paths in all references +- Report findings as regular messages +- Do not use emojis +- Be thorough and precise \ No newline at end of file diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..2a3aa0d --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,11 @@ +{ + "lsp": { + "clangd": { + "binary": { + "env": { + "INCLUDE": "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\BuildTools\\VC\\Tools\\MSVC\\14.29.30133\\include;C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\ucrt;C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\shared;C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\um;C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\winrt" + } + } + } + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..00e49bc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,89 @@ +# doslang 작업 규칙 + +DOS/Windows용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. 규범 문서는 +`SPEC.md`이며 이 파일은 그것을 구현할 때의 작업 규칙만 다룬다. + +## 문서 지도 + +| 파일 | 역할 | +|---|---| +| `SPEC.md` | 언어 명세. 유일한 규범 문서 | +| `IR.md` | 중간 표현. 프론트엔드와 기계 사이 | +| `TODO.md` | 남은 작업과 정해진 것. 언어 규칙은 `SPEC.md` 를 가리키기만 한다 | +| `fec/tests/*/README.md` | 각 fixture 디렉터리가 무엇을 검사하는지 | +| `audits/<날짜>-<주제>.md` | 그때 조사해보니 어땠는지. 불변 기록 | + +## 파이프라인 + +``` +.fe → fec → i386 asm → wasm → wlink → .exe + └ lexer parser resolve types own check (프론트엔드) + └ lower (IR) + └ x86 (백엔드) +``` + +`wasm`과 `wlink`는 고정된 Open Watcom의 어셈블러와 링커다 (WebAssembly와 무관). +`SPEC.md` §1 철학 6: 링커와 오브젝트 포맷을 새로 만들지 않는다. + +## 검증 + +```powershell +uv run python tests/run.py # 컴파일러가 프로그램에 대해 뭐라고 하는가 +uv run python tests/exec.py # 컴파일된 프로그램이 실제로 무엇을 하는가 +uv run python tests/build.py <프로그램.fe> # 하나만 빌드해서 돌려보기 +``` + +- **두 스위트를 모두 통과해야 한다.** `run.py`만 보면 진단은 옳은데 코드가 안 나오는 + 상태를 놓친다. 보고만 되고 방출되지 않는 경계 검사가 그 예다. +- 완료하려는 기능을 직접 검사하는 fixture가 통과해야 한다. 테스트가 증명하지 않는 + 기능은 완료로 처리하지 않는다. +- 거부를 기대하는 fixture는 첫 줄에 `// ERROR:<줄>:<문구>` 마커를 둔다. 마커가 없으면 + 파일 이름이 기대값이 된다 — `bad`로 시작하면 거부, 아니면 통과. +- 실행 프로그램은 첫 줄들에 `// EXIT:<코드>`, `// OUTPUT:<문구>`, `// NOCHECKS:<코드>`를 + 둔다. 마지막 것은 `--no-checks`로 다시 빌드해서 다른 결과를 요구한다. +- 툴체인은 `.dosboxx/watcom`에 고정되어 있고, 없으면 오류로 멈춘다. + +## 함정 + +- 표준 라이브러리는 프로그램이 아니라 컴파일러 옆에 있다. `--std=<디렉터리>`로 + 넘기며, 그 디렉터리 안에 `std/`가 있어야 한다. +- 유닛 경로의 각 segment는 소문자로 시작하고 `a-z0-9_`만, **최대 8자**다. + 파일 경로와 정확히 대응한다 (`std.io` ↔ `/std/io.fe`). +- `extern "c" fn`은 이름을 그대로 쓴다. 나머지는 `fe_<유닛>_<이름>`으로 맹글링하며 + 어셈블러가 받지 않는 문자는 밑줄이 된다. +- 슬라이스 배치(포인터 다음 길이)와 wrapper 페이로드 위치는 각각 한 군데에만 + 적혀 있다. 두 군데가 되면 어긋난다. + +## 파일 크기 + +**2,000 줄을 넘기지 않는다. 웬만하면 1,000 줄.** 넘어가면 나눈다. 나눌 때는 +줄 범위로 자르고 -- 주제별로 묶는 것보다 정확하다, 한 줄도 잃거나 겹치지 않으니 -- +공유하는 것은 비공개 헤더(`checkpri.h`, `lowerpri.h`)에 모은다. + +## 조사 기록 + +한 번 조사하고 끝나는 것 -- 명세와 구현의 대조, 진단 증거 수집, 외부 감사 -- +은 `audits/<날짜>-<주제>.md`에 남긴다. 날짜와 기준 커밋을 적는다. 조사에서 +나온 **결론**은 `SPEC.md`나 `TODO.md`로 옮기고, audit 자체는 그때 무엇을 +봤는지의 기록으로 둔다. + +본문은 고치지 않는다. 다만 해결되면 맨 위에 **해결 줄 하나**를 붙인다 -- +어느 커밋에서 어떻게 정리됐는지. 그것 없이는 읽는 사람이 아직 살아있는 +문제인지 알 수 없다. + +계획 문서는 두지 않는다 -- 끝난 계획은 git log 다. + +## 작업 흐름 + +- 명세 판단이 바뀌면 `SPEC.md`를 즉시 갱신한다. 구현이 명세와 다르면 둘 중 하나가 + 틀린 것이므로 그 자리에서 결론을 낸다. +- 언어 규칙을 완화하려거든 먼저 프로그램 쪽을 고쳐본다. 규칙이 진짜 언어를 못 쓰게 + 만들 때만 규칙을 건드리고, 무엇을 왜 바꿨는지 `TODO.md`에 남겨 사람이 판단하게 + 한다. +- 코드는 컴파일러 단계로 나눈다. 마일스톤 단위 분할은 폐기했다. +- 검증된 단위마다 커밋한다. primary 브랜치는 `master`다. +- `.dosboxx/`의 다운로드, 실행 작업공간, 로그는 커밋하지 않는다. + +## 현재 상태 + +두 스위트의 통과 수가 현재 상태다. 남은 작업은 `TODO.md`에 있다. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/IR.md b/IR.md new file mode 100644 index 0000000..856e1be --- /dev/null +++ b/IR.md @@ -0,0 +1,180 @@ +# Ferro IR + +`fec` 의 중간 표현. 이 문서는 IR 자체만 다룬다. 언어 규범은 `SPEC.md` 이고, +Ferro 의미론이 어떻게 이 형태로 펴지는지는 lowering 이 담당한다. + +IR 을 두는 이유는 하나다. **검사가 끝난 AST 와 기계 사이의 거리가 너무 멀다.** +`try` 하나가 분기 두 개와 임시값 하나로 펴지고, `defer` 는 함수의 모든 이탈 +경로에 복제되며, 배열 인덱스는 비교와 트랩을 낳는다. 그 전개를 명령어 선택과 +같은 자리에서 하면 둘 다 읽을 수 없게 된다. + +--- + +## 1. 형태 + +함수 단위다. 함수는 **기본 블록**의 목록이고, 블록은 **명령**의 목록과 하나의 +**종결자**로 끝난다. 블록 중간에서 분기하지 않고, 종결자 뒤에 명령이 없다. + +``` +fn @okid.test() -> i32 { + $0: i32 ; 지역 + b0: + %0 = const i32 7 + store $0, %0 + %1 = load i32 $0 + ret %1 +} +``` + +### 슬롯 + +| | | | +|---|---|---| +| `%n` | 임시값 | 한 번 정의되고 여러 번 쓰인다. 블록을 넘지 않는다 | +| `$n` | 지역 | 주소를 가진 스택 자리. 함수 진입 시 전부 잡는다 | +| `@name` | 전역·함수 | 링커가 보는 이름 | + +임시값이 블록을 넘지 않으므로 φ 노드가 없다. 블록을 넘겨야 하는 값은 지역에 +`store` 하고 다시 `load` 한다. 이것이 SSA 보다 코드를 조금 더 만들지만, +**레지스터 할당기를 블록 단위로 유지**해 준다. v0.1 에서는 그 교환이 맞다. + +### 기계 타입 + +``` +i8 i16 i32 ptr mem +``` + +Ferro 타입은 여기서 사라진다. 구조체·배열·슬라이스·옵셔널·에러 유니온은 전부 +`mem` 이고, 필드 접근은 lowering 이 계산한 **바이트 오프셋**이다. IR 은 +`Box(i32)` 라는 것을 모른다 — 크기 N 바이트짜리 메모리만 안다. + +`ptr` 은 4바이트다. 세그먼트가 없으므로 포인터 종류도 하나뿐이다 (`SPEC.md` §2). + +`bool` 은 `i8`, `char` 는 `i8`, `usize` 는 `i32` 다. + +--- + +## 2. 명령 + +12개다. + +``` +%d = const 상수 +%d = load 메모리에서 읽는다 + store , %v 메모리에 쓴다 +%d = addr 주소를 뜬다 +%d = %a, %b 산술·비트 연산 +%d = %a, %b 비교. 결과는 i8 +%d = cast %a 정수 폭 변환 +%d = call @f(%a, ...) 호출 + copy , , N N바이트 복사 +``` + +`` — `add sub mul div mod and or xor shl shr` +`` — `eq ne lt le gt ge` (부호 있음/없음은 `` 가 정한다) + +### place + +`load`·`store`·`addr`·`copy` 의 피연산자다. + +``` +$n 지역 +@name 전역 +%p 포인터 임시값이 가리키는 곳 +%p + 상수 오프셋. 필드 접근이 여기로 온다 +``` + +인덱스처럼 오프셋이 상수가 아니면 lowering 이 주소를 먼저 계산한다. + +``` +; a[i] 는 +%0 = load i32 $i +%1 = const i32 4 ; 원소 크기 +%2 = mul i32 %0, %1 +%3 = addr $a +%4 = add ptr %3, %2 +%5 = load i32 %4 +``` + +### 덩어리 + +**덩어리는 언제나 주소로 오간다.** 크기 임계값이 없다. + +``` +call @f(%p) ; f 가 mem 을 받으면 %p 는 그 주소다 +``` + +반환도 같다. `mem` 을 반환하는 함수는 **첫 인자로 결과를 쓸 주소를 받는다**. +호출자가 자리를 잡고 넘긴다. + +이 규약을 고른 이유는 단순해서만이 아니다. ISA 마다 다른 "구조체를 언제 +레지스터로 넘기는가" 규칙을 통째로 피해간다. `extern "c"` 경계에서는 C ABI 로 +변환해야 하며 그것은 백엔드의 일이다. + +--- + +## 3. 종결자 + +4개다. + +``` +jmp b 무조건 분기 +br %c, b, b %c 가 0이 아니면 b +ret [%v] 반환 +trap 중단 +``` + +`trap` 은 `fe_trap(reason, UNIT_FILE, line)` 이 된다. `reason` 은 작은 정수이고 +`UNIT_FILE` 은 **유닛당 하나뿐인 파일 이름 문자열**이다. 트랩 지점마다 문자열을 +두면 실행 파일이 부풀기 때문이다. + +`line` 은 컴파일 시점에 상수로 박힌다. 주소가 아니라 줄 번호를 남기는 방식은 +당대에 흔했고, 심볼 테이블 없이 실패 지점을 말할 수 있는 가장 싼 방법이다. + +reason 값: + +| | | +|---|---| +| 0 | 배열·슬라이스 경계 | +| 1 | 정수 오버플로 | +| 2 | 0으로 나눔 | +| 3 | 도달할 수 없는 곳에 도달 (`@unreachable`) | +| 4 | 명시적 `@trap()` | + +`--no-checks` 는 0·1·2 를 만드는 검사를 lowering 단계에서 생략한다. 3·4 는 +소스에 쓰인 것이므로 남는다. + +--- + +## 4. 함수와 전역 + +``` +fn @unit.name(, ...) -> { ... } +extern fn @name(, ...) -> +global @unit.name : mem = <초기값 바이트> +``` + +이름은 `유닛.이름` 이다. 제네릭 인스턴스도 여기서는 그냥 함수 하나다 — +모노모피제이션이 프론트엔드에서 끝나므로 **IR 에 제네릭이라는 개념이 없다.** + +--- + +## 5. 이 IR 이 하지 않는 것 + +- **최적화 없음.** 상수 접기도, 죽은 코드 제거도 없다. 나중에 붙일 자리는 있다 +- **φ 노드 없음.** 블록 간 값은 지역을 경유한다 +- **타입 검사 없음.** 검사는 프론트엔드에서 끝났다. IR 이 잘못되었다면 lowering 의 버그다 +- **예외·언와인딩 없음.** 에러는 값이고, `try` 는 분기다 + +--- + +## 6. 왜 이 크기인가 + +명령 12개와 종결자 4개는 **i386 으로 직접 번역할 수 있는 최소 집합**이다. +각각이 몇 개의 x86 명령으로 내려가고, 그 대응이 눈으로 확인된다. 이보다 높으면 +백엔드가 IR 을 다시 해석해야 하고, 이보다 낮으면 lowering 이 기계에 가까워져서 +다른 ISA 로 옮길 때 다시 써야 한다. + +같은 이유로 이 집합은 m68k·ARM·MIPS·RV32 에도 그대로 내려간다. 평평한 주소 +공간과 정수 연산만 쓰기 때문이다. 백엔드를 하나 더 만드는 비용은 명령 선택과 +레지스터 할당이지 IR 재설계가 아니다. diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..36253de --- /dev/null +++ b/SPEC.md @@ -0,0 +1,830 @@ +# Ferro 언어 명세 v0.1.8 + +DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. +파일 확장자 `.fe`, 컴파일러 이름 `fec`. 별도의 인터페이스 파일은 없다 — 빌드 하나가 모든 유닛의 소스를 함께 읽는다. + +이 문서는 Ferro 언어 명세만 다룬다. 컴파일러 구현 지시서와 표준 라이브러리 상세 명세는 +별도 문서에서 다룬다. 명세 판단이 애매한 부분은 §1 철학과 §5 소유권 규칙을 기준으로 결정한다. + +--- + +## 1. 설계 철학 + +1. **안전은 기본, 위험은 명시.** 기본 코드는 메모리 안전(널 역참조, 버퍼 오버런, use-after-free, 이중 해제 불가). 위험한 연산은 `unsafe {}` 블록 안에서만. +2. **전역 분석 금지.** 모든 검사(타입, 소유권, 참조)는 함수 하나만 보고 완결되어야 한다. 이 제약이 라이프타임 표기를 없애고, 진단을 위반 지점에 국소적으로 묶으며, 컴파일러를 작게 유지한다. +3. **숨은 비용 없음.** 힙 할당, 복사, 소멸자 호출, 형변환이 전부 소스에 보인다. GC 없음, 예외 없음, 암묵 변환 없음. +4. **읽히는 문법.** `이름: 타입` 순서, 좌→우 파싱, LL(1) 재귀하강으로 처리 가능. +5. **작게 시작.** 기능을 넣기 전에 뺄 이유를 먼저 찾는다. 뺀 것과 그 대체 수단은 §11에 기록한다. +6. **기존 도구체인 재사용.** 링커, `.OBJ`/`.LIB`/`.EXE` 포맷, DPMI 익스텐더를 새로 만들지 않는다. + +--- + +## 2. 타깃 + +타깃은 하나다. + +| | | +|---|---| +| CPU/모드 | i386 보호모드 플랫 | +| 실행 환경 | Windows 11, 그리고 DPMI 익스텐더 위의 DOS | +| 포인터 크기 | 4바이트 | +| 메모리 모델 | flat. 세그먼트 개념 없음 | +| `usize`/`isize` | 타깃의 포인터 폭 | + +- 타깃이 하나이므로 타깃에 따라 갈라지는 소스는 v0.1에 없다. +- `usize`/`isize`는 **타깃의 포인터 폭**이며 특정 비트 수를 약속하지 않는다. 오늘 + 그것은 32비트지만 `u32`와 자동으로 변환되지 않는다. 폭을 언어 의미론으로 + 새어나가게 두지 않는 이 구분이, 나중에 다른 폭의 타깃을 여는 유일한 장치다. +- **세그먼트 주소 지정은 언어에 없다.** far 포인터는 x86 리얼모드에만 있는 개념이고, + 평평한 주소 공간을 가진 다른 32비트 프로세서에는 대응물이 없다. + +### 2.1 컴파일러 자신이 도는 곳 + +컴파일러 `fec`도 32비트 보호모드 플랫에서 돈다 — 호스트에서든, DOS에서든 DPMI +익스텐더 위에서다. 당대의 Open Watcom 컴파일러 자신이 그렇게 돌았다. + +**8086 리얼모드는 이 명세의 범위 밖이다.** 640KB는 8086의 한계가 아니라 IBM PC가 +1MiB 주소 공간의 위쪽 384KB를 하드웨어에 예약해서 생긴 것이고, 그 안에 컴파일러를 +넣으려면 AST를 통째로 들지 않는 스트리밍 구조와 오버레이가 필요하다. 리얼모드 +타깃이 필요해지면 그것은 백엔드와 `usize` 폭의 문제이지 언어 설계의 문제가 아니다. + +--- + +## 3. 어휘 구조 + +- 식별자: `[A-Za-z_][A-Za-z0-9_]*`. 대소문자 구분. +- 주석: `//` 줄 끝까지, `/* */` **중첩 허용**. +- 정수 리터럴: `123`, `0xFF`, `0b1010`, `0o17`, 자릿수 구분 `1_000_000`. **타입은 문맥이 요구하는 정수 타입이고, 요구하는 것이 없으면 `i32`다.** 값이 그 타입의 범위를 벗어나면 컴파일 에러다 — `let b: u8 = 300;`은 44로 잘리는 것이 아니라 거부된다. 앞의 단항 `-`는 리터럴의 일부로 보아 `let n: i8 = -128;`은 허용하고 `let u: u8 = -1;`은 거부한다. +- 문자 리터럴: `'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 comptime asm try catch as extern +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` (반환 타입으로만. 단, 역참조 불가능한 `*void`의 대상 타입은 허용, R9) +- `type` (comptime 파라미터와 type alias의 `const` 초기값에서만, §4.7·§9) + +**정수 규칙:** +- 서로 다른 정수 타입 간 암묵 변환 없음. `as`로 명시. +- `as`는 절단/부호확장을 수행하며 값 손실을 검사하지 않는다. +- `+ - * / %`는 검사 빌드에서 오버플로 시 트랩. `+% -% *%`는 랩어라운드(항상 무검사). +- `/`, `%`의 0 나눗셈은 항상 트랩(검사 빌드 여부 무관, CPU가 트랩함). +- 시프트 `<< >>`: 우변은 `u8`. 시프트 양이 비트폭 이상이면 검사 빌드에서 트랩. +- 비트 연산 `& | ^ ~`는 같은 타입끼리만. + +### 4.2 복합 타입 + +| 문법 | 의미 | 표현 | +|---|---|---| +| `[N]T` | 배열, 값 타입, N은 컴파일타임 상수 | `N * sizeof(T)` | +| `[]T` | 공유·읽기 전용 슬라이스 (참조성, R4 적용) | `(const ptr, len)` | +| `[]mut T` | 배타·쓰기 가능 슬라이스 (참조성, R4 적용) | `(ptr, len)` | +| `str` | 미리 선언된 `[]u8`의 type alias | `[]u8`과 동일 | +| `^[]T` | 소유 버퍼. 일반 `^T`와 구별되는 독립 소유 타입 | `(ptr, len)` | +| `^T` | 일반 소유 포인터 (힙, 단일 소유자) | 포인터 | +| `&T` | 공유 참조 | 포인터 | +| `&mut T` | 배타 참조 | 포인터 | +| `*T` | raw 포인터 (`unsafe`에서만 역참조) | 포인터 | +| `?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]`. `let` 배열·공유 슬라이스에서는 `[]T`, `var` 배열·배타 슬라이스에서는 `[]mut T`가 생긴다. +- `[]mut T`는 `[]T`로, `&mut T`는 `&T`로 **호출 인자 위치에서만** 암묵 재대여할 수 있다. 이것은 호출 동안의 read-only view이며 원래 배타 대여는 원래 마지막 사용까지 유지된다. 일반 `let`/대입에는 이 암묵 약화를 적용하지 않는다. **반환 위치에서도 약화할 수 있다** — `[]mut T`를 `[]T`로, `&mut T`를 `&T`로 반환하는 것은 R8이 이미 그 파생을 허용한 뒤에 가진 것보다 적게 넘기는 일이므로 새 별칭을 만들지 않는다. `&Self` 메서드가 자기가 소유한 것의 읽기 전용 뷰를 내주는 길이 이것뿐이다. 장기 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`이다. 따라서 별도 변환 규칙이나 별도 표현은 없다. + +### 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`는 자기 타입의 별칭. +- 필드 레이아웃은 선언 순서. 정렬은 자연 정렬. `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일 때) +p.?.value = 1; // projection chain, 소유권 이동 없음 +let v = mem.replace(&mut p, null).?; // 소유값을 실제로 꺼냄 +``` + +- `?T`에서 T가 일반 `^T`, `&T`, `*T`, `fn`이면 널 포인터를 널 표현으로 사용한다. `?^[]T`는 빈 소유 버퍼와 null을 구별해야 하므로 `(bool, ^[]T)` 표현을 사용한다. +- `null`은 독립 runtime 타입이 없다. expected type으로 정확한 optional 또는 pointer-like 타입을 하나 결정할 수 있는 위치에서만 허용한다. `let p: ?^Node = null;`과 `takes_optional(null);`은 허용하지만 `let p = null;`처럼 문맥이 없거나 둘 이상의 타입으로 해석 가능한 경우는 컴파일 에러다. +- 검사 없이 역참조 불가. `p.^`는 컴파일 에러, `p.?.^`가 필요. +- `.?`, `.field`, `[i]`는 place projection이다. projection chain은 값을 이동하지 않는다. Copy 값은 읽기에서 복사되며 비-Copy 값을 projection에서 꺼내는 것은 R7에 따라 금지한다. 실제 추출은 `mem.replace`를 사용한다. +- `orelse`는 Copy payload를 복사한다. 비-Copy optional 변수 자체에 적용하면 optional 전체를 이동하며, field/index projection의 비-Copy optional에는 직접 적용할 수 없다. +- `orelse`는 optional이 `Some`이면 우변을 평가하지 않고, `None`일 때만 우변을 평가하는 lazy 연산이다. 부수 효과·이동·대여도 실행되는 경로에만 적용한다. +- `Some`과 `None`은 `if let`과 `match`의 패턴 위치에서만 옵셔널 해체를 의미하는 문맥 키워드다. 다른 위치에서는 일반 식별자이며 §3의 예약어가 아니다. 패턴은 place를 파괴적으로 추출하지 않는다. immutable place의 payload binding은 shared borrow/view, mutable place는 필요한 mutable borrow/view이고 Copy payload만 복사할 수 있다. non-Copy payload의 소유권을 꺼내려면 `mem.replace`가 필요하며 temporary optional만 자동 소유 추출하는 예외도 없다. + +### 4.6 에러 + +```fe +pub error ParseError { // 사용자 nominal error 예시 + InvalidDigit = 1, + Overflow = 2, +} + +fn read_all(path: str) -> !^[]u8 { // 표준 io는 core.Error로 통일 + var f = try io.open(path, io.Read); // 같은 core.Error면 즉시 반환 + defer { f.close() catch @trap(); } + let n = f.size() catch |e| { return e; }; + ... +} +``` + +- `error` 선언은 `u16` 코드 집합. 코드 0은 "성공" 예약이라 사용할 수 없고, 한 선언 안에서 member 이름이나 숫자 code가 중복되면 컴파일 에러다. 서로 다른 nominal error 선언은 같은 숫자 code를 사용할 수 있지만 여전히 다른 타입이다. +- expected type이 `E!T`인 위치에서는 `T` 값은 success, `E` 값은 failure를 구성한다. 함수의 `return`도 선언된 반환 타입이 `E!T`이면 같은 규칙을 쓴다. 이는 일반 implicit conversion이 아니라 error-union 전용 contextual construction이며, `E1`과 `E2` 또는 nominal error와 `core.Error` 사이의 자동 변환은 없다. +- `try`는 에러 유니온 반환 함수 안에서만 허용한다. 피연산자의 nominal error 타입은 현재 함수의 error 타입과 정확히 같아야 한다. 다르면 `catch`에서 명시적으로 매핑한다. +- `catch`는 현재 함수의 반환 타입과 무관하게 어디서든 에러를 그 자리에서 처리할 수 있다. +- `try e`: 에러면 현재 함수에서 즉시 반환한다. +- `e catch |x| { ... }`: 블록은 값을 만들 수 없다. 결과 타입이 `void`이면 정상적으로 끝까지 실행할 수 있고, 값 결과가 필요하면 `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝나야 한다. 값이 필요하면 아래 짧은 형태를 쓴다. (언어에 블록 표현식을 도입하지 않기 위한 선택. §11 참조.) +- `e catch default_value`: 짧은 형태. 우변은 식이며 그 값이 결과가 된다. +- 짧은 `catch`의 우변과 block `catch`의 handler는 피연산자가 error일 때만 평가·실행한다. success이면 handler의 부수 효과·이동·대여가 발생하지 않는다. +- 서로 다른 error 타입 간 자동 변환 없음. `!T`(기본 에러 집합 `core.Error`)로 통일하거나 명시 매핑. +- `try`와 `catch`도 field/index/optional projection에서 non-Copy payload를 숨게 이동시키지 않는다. projection에서 소유값을 추출해야 하면 먼저 `mem.replace`로 유효한 대체값을 남긴다. +- 에러는 값이다. 언와인딩, 스택 추적, 소멸자 이외의 자동 정리 없음. +- 실패를 복구하지 않고 트랩으로 바꾸려면 `expr catch @trap()`을 쓴다. v0.1에는 별도 `must` 키워드를 두지 않는다. + +`error.Name`은 선언된 error 타입을 만들지 않고 기본 `core.Error`의 이름 있는 +멤버를 참조하는 익명 에러 값이다. **드라이버가 emit 단계 이전에** 빌드에 든 모든 +유닛에서 쓰인 이름을 모아 중복을 제거하고 이름의 바이트순으로 정렬하여 1부터 +`u16` 코드를 부여한다. 따라서 서로 다른 유닛의 `error.Name`은 같은 값이고, +빌드 순서와 무관하게 결과가 결정적이다. +빌드 디렉터리 이력에 따라 번호가 달라지는 append-only 표는 금지한다. +번호는 빌드 전체를 봐야 정해지므로 유닛 하나만 따로 코드 생성까지 밀고 갈 수는 +없다. 이름이 65,535개를 넘으면 컴파일 에러다. 명시적인 `error` 선언은 여전히 nominal +타입이며, 같은 멤버 이름이나 숫자 코드를 가진 다른 선언 및 `core.Error`와 자동 +변환되지 않는다. `error.Name`의 타입은 `core.Error`이며 `core.Error!T` 또는 +축약형 `!T`를 반환하는 함수에서만 직접 반환할 수 있다. +`--strip-error-names`를 사용하면 실행 파일과 런타임 오류 문자열에서 이름을 +제거하지만 숫자 코드는 유지한다. +`fmt.fmt_error`는 이 정책에 따라 `core.Error` 값을 이름 또는 코드로 포맷한다. + +### 4.7 타입 동등성과 alias + +이름 기반(nominal). 필드가 같아도 다른 이름이면 다른 타입. `type` 값은 comptime 파라미터뿐 아니라 `const Alias = Type;`의 초기값에 허용하며, 이 선언은 새 nominal 타입이 아닌 완전 동일 alias를 만든다. 런타임 type 값은 없다. `str`은 이 규칙으로 미리 정의된 `[]u8` alias다(§4.2). + +--- + +## 5. 소유권과 참조 — 핵심 규칙 + +이 절이 언어의 핵심이다. 모든 규칙은 **함수 하나만 보고** 검사된다. + +**R1 (단일 소유자).** 모든 값의 소유자는 정확히 하나. 변수 대입, 함수 인자 전달, 반환은 **이동(move)**이다. 이동된 변수는 이후 사용 시 컴파일 에러. + +**R2 (Copy 타입).** 다음은 이동 대신 복사된다: 정수, `bool`, `char`, raw 포인터 `*T`, 공유 참조 `&T`, 공유 슬라이스 `[]T`(`str` 포함), 함수 포인터. `?T`, `E!T`, `[N]T`, struct/enum은 모든 포함 값이 Copy이고 `drop`이 없을 때 재귀적으로 Copy다. `^T`, `^[]T`, `&mut T`, `[]mut T`는 Copy가 아니다. + +**R3 (소멸자, RAII).** 일반 `^T`와 `^[]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`(`str` 포함), `[]mut T`는 다음 위치에만 존재할 수 있다: +- 함수 파라미터 +- 지역 변수 (`let`/`var`) +- 표현식 안의 임시값 + +다음은 **컴파일 에러**다: +- struct/enum 필드의 타입 +- 배열/슬라이스의 원소 타입 +- 함수 반환 타입 (예외: R8) +- 일반 `^T`, `*T`의 대상 타입 +- 전역 변수의 타입 + +예외는 독립 소유 타입 `^[]T`/`?^[]T`와 문자열 리터럴로 초기화한 `const`/`static str`뿐이다. `^[]T`는 참조를 저장하지 않고 버퍼 자체를 소유한다. 이 제한이 라이프타임 표기 전체를 불필요하게 만든다. + +**R5 (참조 수명).** 지역 참조 변수는 대상보다 오래 살 수 없다. R4 덕분에 대상은 항상 같은 함수의 지역 변수, 파라미터, 또는 `static` 전역이므로 스코프 중첩 확인만으로 검사된다. 가변 전역에 대한 대여는 R10이 금지한다. + +**R6 (배타성).** `&mut x`가 살아있는 동안 `x`에 대한 다른 참조 생성, 직접 읽기/쓰기, 이동이 금지된다. `&x`(공유)는 여러 개 동시 가능하지만 그동안 `x`에 쓰기/이동 금지. + +여기서 `x`는 변수가 아니라 **place**다. `p.a`와 `p.b`는 서로 다른 place이므로 한쪽을 대여해도 다른 쪽은 그대로 읽고 쓸 수 있다. 대여가 필드 단위로 갈라지는 것은 루트 변수의 직속 필드 한 겹까지이며, 그 아래(`p.a.b`)와 인덱스(`arr[i]`)·역참조(`p.^`)는 전체를 대여한 것으로 본다. 전체를 대여하면 모든 필드와 충돌하고, 필드를 대여하면 전체 및 같은 필드와 충돌한다. 한 값에서 동시에 갈라둘 수 있는 필드 수에는 구현 상한이 있고, 넘으면 전체 대여로 되돌아간다 — 더 많이 보고할 뿐 놓치지는 않는다. + +대여 상태는 **root local/parameter 와 그 직속 필드** 단위로 추적한다. `&mut xs[0]`과 `&mut xs[1]`은 서로 다른 index라는 이유로 분리하지 않고 같은 root `xs`의 충돌 대여로 본다 — index는 상수가 아닐 수 있고, 그것을 따지는 것은 함수-local 상태 기계가 감당할 일이 아니다. 필드 이름은 상수라 그 문제가 없으므로 갈라진다. + +```fe +var p = Pair{ a: 1, b: 2 }; +let r = &mut p.a; +p.b = 3; // ok: p.b는 다른 place +p.a = 3; // 에러: p.a가 배타 대여 중 +take(p); // 에러: 전체는 대여된 필드를 포함한다 +r.^ = 4; + +let a = &mut xs[0]; +let b = &mut xs[1]; // 에러: 둘 다 root xs를 대여 +``` + +참조의 생존 구간은 **참조 변수의 마지막 사용 지점까지**다. 그 이후에는 원본에 대한 접근·이동이 다시 허용된다. 조건부 흐름에서는 모든 경로의 마지막 사용 중 가장 나중 지점을 취한다. 임시 참조(`f(&x)`)는 그 문장 끝까지다. `defer` 블록에서 사용한 참조와 그 원본의 대여는 해당 defer가 실행되는 스코프 끝까지 연장한다. + +호출 인자 위치의 `&mut T → &T`, `[]mut T → []T` 약화는 새 장기 공유 대여가 아니라 기존 배타 대여의 읽기 전용 재대여다. callee를 평가한 뒤 해당 인자를 평가하는 시점부터 호출이 끝날 때까지만 임시 재대여가 존재하고, 원래 배타 대여는 원래 마지막 사용까지 유지된다. 일반 `let`/대입에서는 암묵 약화를 허용하지 않으므로 `let s: &i32 = m;`(`m: &mut i32`)은 컴파일 에러다. 별도의 lifetime/coercion 시스템은 두지 않는다. + +이 판정은 함수 지역 liveness 분석이며 함수 밖 정보를 쓰지 않으므로 §1.2를 위반하지 않는다. + +```fe +var x: i32 = 0; +let r = &mut x; +r.^ = 1; // r의 마지막 사용 +x += 1; // OK — 여기서 r의 대여는 이미 끝났다 +``` + +**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이 막으려는 "뒤에 남은 반쪽짜리 값"이 생기지 않는다. 다른 함수에서는 예외가 없다. + +**R8 (파생 반환).** 함수는 다음 두 경우에 한해 `&T`, `&mut T`, `[]T`, `[]mut T`와 이를 `?`로 감싼 타입을 반환할 수 있다. + +**(a) 파라미터 파생.** 메서드는 파생 원본이 항상 참조성 `self`여야 한다. 다른 참조성 인자를 추가로 받을 수 있지만 반환값은 그 인자에서 파생될 수 없고 그 인자의 임시 대여는 문장 끝에 풀린다. 자유 함수는 참조성 파라미터(`&T`, `&mut T`, `[]T`, `[]mut T`)가 **정확히 하나**여야 한다. 두 경우 모두 반환값이 정해진 원본에서 파생됐음을 컴파일러가 함수 본문만 보고 확인한다. 파생은 슬라이싱, 인덱싱, 필드 접근, projection, `&`/`&mut` 취함과 다른 R8(a) 호출의 연쇄다. 반환의 가변성은 원본 이하여야 한다. + +**(b) 정적 파생.** 반환값이 문자열 리터럴 또는 `static` 선언에서 파생된 경우. 이때는 참조성 파라미터가 없어도 된다. + +참조성 반환의 provenance는 인터페이스에서 다음 둘로 정규화한다. + +- `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는 함수 시그니처만 보고 결정할 수 있어야 한다. + +호출 지점에서 `Param(N)` 결과는 **정해진 파생 원본을 대여한 것으로 취급**한다. 즉 결과를 지역 변수에 바인딩할 수 있으며, 그 대여가 사는 동안 원본에 R6·R7이 그대로 적용된다. `Static` 결과는 caller local borrow를 만들지 않는다. + +```fe +let t = line.trim(); // 내장 alias 메서드 R8(a) // OK. line은 t의 대여 구간 동안 잠긴다 + +list.at_mut(0).x = 5; // &mut T, 배타 대여 +let r = list.at(0); // &T, 공유 대여 +list.push(1); // 에러: r이 list를 대여 중 (R6) + +map.get_str(key); // ?&V: self에서만 파생, key는 문장 끝에 해제 +pub fn name() -> str { return "main"; } // R8(b) +``` + +자유 함수에 참조성 파라미터가 둘 이상이면 어느 쪽에서 파생됐는지 시그니처만으로 결정되지 않으므로 참조성 반환을 할 수 없다. 그런 함수가 필요하면 메서드로 만들어 `self`를 원본으로 고정하거나 인덱스(`usize`)·핸들을 반환한다. + +**R9 (unsafe).** `unsafe {}` 안에서만 허용: raw 포인터 역참조, `*T` ↔ `^T`/`&T` 변환, `@ptr_cast`, `@volatile_*`, `@port_*`, `asm`, `*_unchecked` 함수. `*void`는 저장·비교·전달과 `@ptr_cast`에만 쓸 수 있고 직접 역참조할 수 없다. R1~R8은 `unsafe` 안에서도 그대로 유지된다. 특히 `unsafe`가 참조 반환·저장이나 대여 검사를 끄지 않으며, 프로그래머가 명시적으로 raw 포인터를 경유한 부분만 컴파일러의 메모리 안전 보장 밖에 놓인다. + +**R10 (전역).** `static`은 불변이며 컴파일타임 상수 초기화만 가능하다. 일반 전역 `var`의 읽기와 쓰기는 안전하며 `unsafe`가 필요 없다. +- 전역에 대한 대여는 다음으로 제한한다. `static`(불변)은 `&`로 대여할 수 있다. 일반 전역 `var`는 `&`·`&mut` 모두 대여할 수 없으며 직접 읽기와 쓰기만 허용한다. 전역 값을 참조로 넘겨야 하면 지역 변수로 복사한 뒤 대여한다. +- 이 제한의 근거는 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에 없다(§11). + +**R11 (재귀·그래프 구조).** `^T`는 R4의 2급 참조가 아니므로 소유가 한 방향인 단방향 리스트와 트리는 필드에 저장할 수 있다. 반면 양방향 리스트·순환·일반 그래프는 역방향 필드에 `^T`를 두면 R1의 단일 소유권을 위반하고 `&T`를 두면 R4를 위반한다. 이런 구조는 아레나/배열이 값을 소유하고 `u16`/`u32` 인덱스 핸들이 간선을 나타내도록 구현한다. 표준 라이브러리 `mem.Arena`를 사용할 수 있으며, 핸들 역참조 때 세대 번호 또는 경계 검사를 사용해 해제된 항목 접근을 막아야 한다. + +--- + +## 6. 문법 + +### 6.1 EBNF + +``` +unit := 'unit' unit_path ';' import* decl* +unit_path := ident ('.' ident)* +import := 'import' unit_path ['as' ident] ';' + +decl := ['pub'] (fn_decl | struct_decl | enum_decl | error_decl + | const_decl | global_decl) + +fn_decl := ['extern' string] 'fn' ident + '(' [param (',' param)*] ')' ['->' type] (block | ';') +param := ['comptime'] ident ':' type +generic_params := '(' ident (',' ident)* ')' +struct_decl := ['packed'] 'struct' ident [generic_params] '{' member* '}' + // field의 마지막 쉼표는 '}' 바로 앞에서 생략할 수 있다 +member := ['pub'] (field | fn_decl) +field := ident ':' type ',' +enum_decl := 'enum' ident [generic_params] '{' variant (',' variant)* [','] '}' +variant := ident | ident '(' type ')' | ident '{' vfield* '}' +vfield := ident ':' type ',' +error_decl := 'error' ident '{' ident '=' int_literal + (',' ident '=' int_literal)* [','] '}' +const_decl := 'const' ident [':' type] '=' expr ';' +global_decl := 'static' ident ':' type '=' expr ';' + | '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 + | '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' for_source block +for_source := expr ['..' expr] +match_stmt := 'match' expr '{' arm+ '}' +arm := pattern '=>' (expr ';' | block) +pattern := ident // 배리언트, 페이로드 없음 + | ident '(' ident ')' // 튜플형 배리언트 바인딩 + | ident '{' ident (',' ident)* '}' // 필드형 배리언트 바인딩 + | 'Some' '(' ident ')' | 'None' + | int_literal | char_literal | 'true' | 'false' | '_' + +type_name := [ident '.'] ident // [binding '.'] Name +type := type_name + | '?' type | '!' type | type_name '!' type + | '^' type | '&' ['mut'] type | '*' type + | '[' expr ']' type | '[' ']' ['mut'] type + | 'fn' '(' [type (',' type)*] ')' ['->' type] + | type_name '(' type (',' type)* ')' // 제네릭 인스턴스 + +catch_expr := expr 'catch' expr | expr 'catch' '|' ident '|' block +orelse_expr := expr 'orelse' expr +``` + +`member`의 `pub`은 필드와 메서드 모두에 개별로 붙는다(§8). 필드와 메서드는 순서를 +섞어 쓸 수 있다. `catch`의 두 형태는 서로 다른 일을 한다: 값을 주는 짧은 형태와, +에러를 받아 빠져나가는 블록 형태다. 블록은 값을 만들지 않으므로(§11) 바인딩이 +있는 쪽만 블록을 받는다(§4.6). `unit_path` segment의 lexical 제한과 source path +대응은 §8.1이 규정한다. + +타입 이름은 `[binding '.'] Name`이다. `import`는 unit path의 **마지막 segment**를 +바인딩하므로(§8.2) 점이 둘 이상인 타입 이름은 만들어질 수 없다. `unit_path` +자체는 `import`와 `unit` 선언에서만 쓴다. + +전역 `static`/`var`는 타입을 적는다. 다른 유닛이 읽는 링커 심볼이라 초기값의 +생김새에 타입을 맡기면 그쪽이 보는 것이 달라진다. `const`는 추론한다. + +### 6.2 표현식 우선순위 (낮음 → 높음) + +표현식 문법은 다음 우선순위 표가 규범이다. 각 단계는 명시가 없으면 좌결합 이항 +연산으로 전개하며, 단항·후위·기본 단계에 나열된 형태가 그대로 프로덕션이 된다. +`catch`와 `orelse`의 구체적 형태는 §6.1을 따른다. + +``` +1 orelse, catch +2 or +3 and +4 == != < <= > >= +5 | +6 ^ +7 & +8 << >> +9 + - +% -% +10 * / % *% +11 단항: - not ~ & &mut try +12 후위: .field .? .^ [i] [a..b] (args) as T +13 기본: literal, ident, '(' expr ')', struct_literal, @builtin(...) +``` + +- `and`, `or`는 단축 평가한다. 좌변이 답을 정하면 우변을 평가하지 않는다. +- `orelse`와 `catch`도 lazy다. 좌변이 각각 `Some`/success이면 우변 또는 handler를 + 평가하지 않는다(§4.5·§4.6). +- `as`는 후위 우선순위(단항보다 강함)지만 단항 연산자 바로 뒤에 `as`가 나타나면 모호한 비용을 숨기지 않도록 괄호를 강제한다. `(-x) as u32`와 `-(x as u32)`는 허용하고 `-x as u32`는 컴파일 에러다. +- `|`, `^`, `&`는 서로 다른 단계다. C와 같은 순서이며, 한 단계로 합치면 + `a | b ^ c`가 좌결합으로 `(a | b) ^ c`가 되어 C에서 온 사람을 속인다. +- `..`는 일반 표현식 연산자가 아니며 `for` 헤더에서만 쓸 수 있다. +- 비교 연산 체이닝 금지(`a < b < c`는 에러). +- `.field`, `[i]`, `[a..b]`, 메서드 호출은 `&`, `&mut`, `^`를 필요한 만큼 자동 projection한다. 값 자체의 역참조는 `.^`가 필요하며 raw `*T`와 optional `?T`는 자동 역참조하지 않는다. +- `x.f(args)`는 메서드를 우선 탐색한다. 함수 포인터 필드를 호출하려면 `(x.f)(args)`로 쓴다. + +### 6.3 빌트인 + +``` +@size_of(T) -> usize @align_of(T) -> usize +@target -> comptime str +@ptr_cast(T, p) -> *T (unsafe) +@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: []mut u8, fmt, ...) -> usize // 버퍼에 기록, 쓴 바이트 수 반환 +@compile_error(msg) // comptime에서 항상 컴파일 에러 +``` + +### 6.3.1 포매팅 빌트인 + +가변 인자를 언어에 도입하지 않는다. `@print` 계열은 **컴파일 단계에서 여러 호출로 전개되는 빌트인**이다. + +```fe +@print("x={} y={x} name={s}\n", a, b, s); +``` +→ lower 단계에서 개념적으로 다음처럼 전개한다. 아래 `io`와 `fmt`는 각각 canonical +`std.io`, `std.fmt` 유닛을 가리킨다. +``` +io.write(out, "x="); +var t1: [12]u8 = undefined; io.write(out, fmt.fmt_int_i32(t1[..], a)); +io.write(out, " y="); +var t2: [8]u8 = undefined; io.write(out, fmt.fmt_hex_u16(t2[..], b)); +io.write(out, " name="); io.write(out, s); io.write(out, "\n"); +``` + +`fmt.fmt_*`는 `[]mut u8` 임시 버퍼에 쓰고 그 버퍼에서 파생된 `str`을 반환하는 순수 함수다. R8(a)의 원본이 하나이므로 별도 lifetime 표기가 필요 없다. 포매팅과 sink를 분리해 `@print`, `@fprint`, `@sprint`가 같은 변환 함수 한 벌을 사용한다. + +규칙: +- 포맷 문자열은 **컴파일타임 문자열 리터럴 또는 `const`만**. 런타임 값이면 에러. +- verb: `{}` 기본(정수/bool/char/str 자동), `{x}` 16진, `{c}` 문자, `{s}` 문자열/슬라이스, `{b}` 불린. `{{`는 `{` 이스케이프. +- `{}` 개수와 인자 개수 불일치 → 컴파일 에러. +- 인자 타입에 대응하는 `fmt.fmt_*` 함수가 없으면 컴파일 에러(메시지에 타입명 표시). +- 자릿수/폭/정렬 지정자는 v0.1에 없음. 필요하면 `fmt.fmt_int_pad`를 직접 호출. +- `@fprint`의 첫 인자는 Copy 핸들 `io.Writer`(§10)다. +- `@print`는 `io.Writer.Stdout`에 기록하며 저수준 writer 오류를 삼키고 `void`를 반환한다. + 따라서 `try @print(...)`는 컴파일 에러다. +- `@fprint`는 writer 오류를 전파하여 `!void`를 반환한다. +- `@sprint`는 같은 `fmt.fmt_*` 결과를 대상 `[]mut u8`에 `mem.copy`로 이어 붙인다. 버퍼가 찬 뒤의 출력이 잘리더라도 트랩하지 않고 기록된 바이트 수를 `usize`로 반환한다. +- 전개된 `io.write` 호출은 위 반환 규칙에 맞게 lower 단계에서 오류를 전파하거나 무시한다. `fmt.fmt_error`는 `core.Error`의 이름/코드를 포맷한다. + +`@compile_error(msg)`의 `msg`는 comptime 문자열이어야 하며, 평가되는 분기에서 +항상 진단을 발생시킨다. `comptime if`의 제거되는 분기에서는 진단하지 않는다. + +### 6.4 예제 + +```fe +unit frame; + +const WIDTH: usize = 320; +const HEIGHT: usize = 200; + +pub struct Buffer { + pixels: []mut u8, + + pub fn put(self: &mut Self, x: usize, y: usize, c: u8) -> void { + if x >= WIDTH or y >= HEIGHT { return; } + self.pixels[y * WIDTH + x] = c; + } +} +``` + +```fe +unit main; +import std.io; + +fn count_lines(path: str) -> !usize { + var f = try io.open(path, io.Read); + defer { f.close() catch @trap(); } + + 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| { + @print("failed: {}\n", e); + return e; + }; + @print("{}\n", n); +} +``` + +--- + +## 7. 의미론 세부 + +### 7.1 변수와 초기화 + +- `let`은 불변, `var`는 가변 선언이다. 두 형태 모두 초기값이 있으면 타입을 추론할 수 있다. `var x: T;`와 `var x: T = undefined;`처럼 초기값이 없거나 `undefined`이면 타입 명시가 필수다. +- `&mut x`, mutable slice 생성과 `&mut Self` 메서드 호출은 `var` place에서만 가능하다. `let`이 `^T`를 보유해도 그 대상을 안전 코드에서 변경할 수 없다. by-value `self: Self`는 소비 메서드 안에서 자신의 필드를 무효 상태로 바꿀 수 있는 가변 local owner로 취급한다. +- 모든 변수는 사용 전 초기화 필수(정적 검사). 명시적 미초기화는 `= undefined`(unsafe 아님, 단 읽기 전 쓰기 필수는 여전히 검사). +- 섀도잉 허용(같은 스코프에서 `let` 재선언). +- **전역 `const`/`static`/`var`의 초기값은 컴파일 시점에 알 수 있어야 한다.** 저장소가 이미지에 들어가므로 초기값이 실행될 순간이 없다. 리터럴, 다른 `const`, 열거형 배리언트, `error.Name`, 그리고 그것들에 대한 연산과 캐스트·집합체 리터럴까지가 허용되며 함수 호출은 허용되지 않는다. 실행 시점에 계산해야 하는 값은 `main`에서 만든다. + +### 7.2 제어 흐름 + +- `for x in slice`: `x`는 `&T`(`[]mut 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 함수 호출 규약 + +- 기본: cdecl. +- `extern "c" fn name(...) -> T;` — 본문 없이 선언, C 심볼과 링크. 이름 맹글링 없음. 인자/반환에 `^T`, 슬라이스, 에러 유니온 사용 불가(`*T`, `usize`만). +- 큰 struct(> 4바이트)는 숨은 포인터로 반환(C ABI 따름). + +### 7.4 검사와 트랩 + +트랩 발생 조건: 배열/슬라이스 경계 초과, 정수 오버플로, 0 나눗셈, `?T`의 `.?` 실패, `@trap()`. + +동작: `core.panic(msg: str, file: str, line: u32)` 호출 → 등록된 `sys.on_exit(fn)` 정리 함수를 역순 호출 → 메시지 출력 → `sys.exit(3)`. 사용자가 `core.set_panic_handler`로 교체 가능. 일반 panic unwind나 defer 실행은 없지만 프로세스 종료 전에 반드시 복원해야 하는 자원은 allocation 없는 고정 크기 `on_exit` registry에 등록한다. + +`--no-checks` 빌드에서 제거되는 것: 경계 검사, 오버플로 검사, `.?` 검사. **오버플로 검사가 없을 때 `+ - * /`의 결과는 랩어라운드로 정의된다** — 타깃의 정수 연산이 그대로 하는 일이며, 미정의 동작으로 두지 않는다. 즉 `--no-checks`에서 `a + b`는 `a +% b`와 같은 값을 낸다. +**절대 제거되지 않는 것:** 소유권/참조 검사, 옵셔널 타입 검사, `match` 완전성 — 전부 컴파일타임이므로. + +### 7.5 comptime + +- `const` 선언의 초기값은 컴파일타임 평가(정수 연산, `@size_of`, 다른 const). +- `comptime if`는 평가되지 않는 분기를 **파싱은 하되 타입 검사/코드 생성하지 않는다**(타깃별 분기용). +- 함수의 `comptime` 파라미터는 §9 제네릭. +- 재귀 평가 깊이 제한 256, 초과 시 에러. + +### 7.6 표현식 평가 순서 + +- 일반 표현식의 평가 순서는 소스의 왼쪽에서 오른쪽이다. +- 함수·메서드 호출은 callee를 먼저 평가하고 인자를 소스 순서대로 왼쪽에서 오른쪽으로 평가한다. 자동 `self` projection도 callee 평가의 일부다. +- 이항 연산자는 왼쪽 operand를 먼저, 오른쪽 operand를 나중에 평가한다. +- `and`와 `or`는 왼쪽 operand로 결과가 정해지면 오른쪽을 평가하지 않는다. `orelse`와 `catch`도 §4.5·§4.6에 따라 우변/handler가 필요한 경로에서만 평가한다. +- 이 순서는 부수 효과뿐 아니라 move, borrow의 시작·마지막 사용, `try` 전파와 defer/drop cleanup 순서를 결정한다. + +--- + +### 7.7 작은 규칙들 + +구현자가 임의로 정하면 갈라지는 것들. 각각 한 줄이면 끝나므로 여기 모아 둔다. + +| | | +|---|---| +| `match` | **enum 값에만 쓴다.** 정수와 `char`에는 쓸 수 없으므로 완전성 검사가 배리언트 목록 하나로 정해진다. 정수 분기는 `if`/`else if`로 쓴다 | +| enum payload | `^T`처럼 소유하는 타입을 담을 수 있다. 다만 v0.1은 **활성 배리언트의 payload를 자동으로 놓아주지 않는다** — 담았다면 꺼내서 직접 놓아야 한다 | +| `for x in xs` | 순회는 `xs`를 **순회 동안 대여한다**. `x`가 그 안을 가리키는 참조이므로 몸통에서 `xs`에 쓰는 것은 R6 위반이다. 읽는 것은 공유 순회에서 허용된다 | +| `defer` 안의 `return` | **컴파일 에러.** 지연 블록은 함수가 무엇을 반환할지 이미 정한 뒤 스코프 정리 중에 돈다 | +| `undefined` 배열 | 초기화 추적은 **변수 단위이며 원소 단위가 아니다.** `undefined`로 선언한 배열은 선언 시점부터 쓰기 가능하고, 읽기 전에 무엇을 채웠는지는 검사하지 않는다. 슬라이스로 넘겨 채우는 것이 의도된 사용법이다 | +| 다른 유닛의 struct 리터럴 | 모든 필드를 명시해야 하므로 **`pub`이 아닌 필드가 하나라도 있으면 밖에서 리터럴을 쓸 수 없다.** 생성자 함수를 두어야 한다 | +| by-value `self` | `self`를 값으로 받아도 필드를 꺼내는 것은 R7 그대로 `mem.replace`가 필요하다. 예외는 자기 `drop` 안뿐이다 | + +## 8. 유닛 + +파일 하나가 유닛 하나다. 유닛의 canonical identity는 fully-qualified dotted unit path이며 +모든 cross-unit 타입·선언·제네릭 identity에서 같은 이름을 사용한다. + +```fe +unit game.main; + +import game.render; +import tinyjson.parse; +import net.http as http; +import std.io; +``` + +import는 항상 유닛 전체를 가져오며 member는 local unit binding으로 한정해 접근한다. +기본 binding은 마지막 segment이므로 `import tinyjson.parse;` 뒤에는 +`parse.read(...)`, `import std.io;` 뒤에는 `io.write(...)`를 쓴다. `as`가 있으면 그 +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, +package-private/friend visibility, package manager를 지원하지 않는다. + +### 8.1 unit 이름과 source path + +unit path의 각 segment는 ASCII lowercase `a`~`z`로 시작하고 이후에는 `a`~`z`, +`0`~`9`, `_`만 쓸 수 있으며 최대 8자다. 일반 Ferro identifier는 계속 case-sensitive이고 +이 제한은 unit path에만 적용한다. `game.main`, `tinyjson.parse`는 허용하지만 +`TinyJson.Parse`, `very_long_library_name`은 unit path로 허용하지 않는다. 이 규칙은 +FAT/DOS 8.3과 case-sensitive host에서 같은 source가 같은 유닛으로 해석되게 한다. + +unit path는 import root 아래의 상대 source path와 정확히 대응한다. canonical identity는 +항상 dotted path이고 실제 path separator만 host/DOS에 맞게 바꾼다. + +``` +tinyjson.parse -> tinyjson/parse.fe +game.world.map -> game/world/map.fe +``` + +### 8.2 binding과 visibility + +한 unit에서 import binding은 다른 unit-scope declaration/import binding과 충돌할 수 없다. +`import foo.net; import bar.net;`은 둘 다 `net`을 만들므로 에러이며 두 번째를 +`import bar.net as bar_net;`처럼 alias해야 한다. alias는 일반 Ferro identifier다. + +visibility는 private과 `pub` 두 단계뿐이다. private 선언은 같은 unit에서만 보이고, +`pub` 선언과 개별 `pub` field/method만 import한 모든 caller에서 보인다. dotted prefix는 +권한이 아니므로 `game.foo`와 `game.bar`는 서로의 private 선언에 접근할 수 없다. public +function parameter/return, public field 등 외부 signature에 나타나는 nominal type은 +importer가 이름을 해석할 수 있어야 하며 private nominal type을 public API에 노출하면 +컴파일 에러다. + +### 8.3 순환과 canonical identity + +순환 import는 컴파일 에러다. fully-qualified dotted unit path와 선언 이름이 nominal +identity의 기준이므로 `tinyjson.value.Value`와 `tinyjson.value.Box`처럼 표시한다. nominal +struct/enum/error는 defining unit + declaration name으로 구별되고 type alias는 새 nominal +identity를 만들지 않는다. + +--- + +## 9. 제네릭 + +`comptime` type 파라미터 기반 모노모피제이션. v0.1의 user-defined generic parameter는 +`type`만 지원한다. + +타입 파라미터는 본문에서 사용되지 않아도 된다. `struct Handle(T) { raw: u32 }`는 +정상이며 `Handle(Node)`와 `Handle(Kind)`는 서로 다른 nominal 인스턴스다 — 파라미터가 +하는 일이 저장소를 서술하는 것이 아니라 두 인스턴스를 갈라놓는 것뿐인 경우이고, +typed handle이 정확히 그 모양이다. 미사용 파라미터에 경고를 내지 않는다. + +```fe +pub struct List(T) { + items: ^[]T, + len: usize, + + pub fn new() -> List(T) { ... } + pub fn push(self: &mut Self, v: T) -> !void { ... } + pub fn at(self: &Self, i: usize) -> &T { ... } // R8 공유 + pub fn at_mut(self: &mut Self, i: usize) -> &mut T { ... } + pub fn drop(self: &mut Self) { ... } +} + +fn max(comptime T: type, a: T, b: T) -> T { + if a > b { return a; } + return b; +} + +let m = max(i32, 3, 7); +var xs: List(u8) = List(u8).new(); +``` + +- `fn id(comptime T: type, x: T) -> T`를 기본형으로 하며 `struct Box(T)`와 + `enum Maybe(T)`의 `T`는 `comptime T: type`의 shorthand다. `comptime N: usize`, + comptime string/bool 등 user value generic은 지원하지 않는다. compiler builtin의 기존 + comptime value는 user generic parameter가 아니다. +- generic type argument는 항상 명시한다. `id(i32, 3)`은 허용하지만 `id(3)`에서 T를 + 추론하지 않는다. `mem.create(value)`처럼 별도로 정의된 compiler-known intrinsic + inference는 일반 generic inference가 아니다. +- generic struct/enum의 method는 enclosing type parameter를 사용할 수 있다. 그러나 + method/function이 enclosing type parameter 외에 별도의 새 generic parameter list를 + 선언하는 generic-method 기능은 v0.1에 없다. +- 인스턴스화 시 타입 인자를 대입해 type-dependent operation을 재검사하고 코드를 + 생성한다. trait/bound와 overload resolution은 없다. 본문 연산이 해당 타입에서 invalid면 + definition/body의 실제 연산 위치를 primary error로 표시하고 각 caller에 `instantiated + here` note를 붙인다. nested instance는 가능한 범위에서 instantiation chain을 표시한다. +- generic body의 이름은 항상 definition unit scope에서 해석한다. non-dependent name은 + 정의 시 그 symbol로 고정되며 caller의 같은 이름은 영향을 주지 않는다. private support + symbol도 definition unit의 것을 쓴다. `comptime if`의 선택되지 않는 branch는 parse만 + 하고 semantic name resolution/type checking/codegen을 하지 않는다. +- generic instance의 canonical key는 **canonical definition unit + canonical declaration + identity + canonical type argument list**다. type alias는 새 nominal identity가 아니므로 + underlying/interned canonical type identity로 정규화한다. 따라서 `const Word = i32;` 뒤의 + `id(Word, 1)`과 `id(i32, 2)`는 같은 instance다. +- exported generic은 definition unit의 private symbol을 참조할 수 있다. 이는 컴파일러 + 수준의 처리이며 Ferro source visibility를 public으로 바꾸지 않는다 — 다른 Ferro source는 + 여전히 그 private 심볼을 직접 참조할 수 없다. + +```fe +unit lib; + +fn helper(x: i32) -> i32 { return x + 1; } + +pub fn bump(comptime T: type, x: T) -> T { + comptime if T == i32 { return helper(x); } + return x; +} +``` + +다른 unit이 요청한 `lib.bump(i32)` instance는 내부적으로 `helper`를 호출할 수 있지만, +다른 Ferro source가 `lib.helper`를 직접 참조할 수는 없다. + +- 재귀적 인스턴스화의 distinct-instance chain 제한은 32다. 이미 pending/known인 동일 + canonical instance key를 다시 요청하는 recursion은 pending instance를 재사용하고 depth를 + 소비하지 않는다. 새로운 distinct instance가 연쇄적으로 생길 때만 depth가 증가하며 + 32를 초과하면 최초/현재 위치와 instance chain을 포함한 compile error를 낸다. +- comptime에서 type 값의 `==`/`!=`, `@is_int(T)`, `@is_ptr(T)`를 허용한다. canonical + interned type identity로 평가하며 런타임 type reflection은 없다. + +--- + +## 10. 표준 라이브러리 + +표준 라이브러리 상세 명세는 별도 문서에서 다룬다. 표준 라이브러리는 reserved `std` +namespace 아래에 있으며 `import std.io;`처럼 명시적으로 가져온다. import 뒤의 local +binding은 마지막 segment라 `io.write`, `mem.replace` 형태로 사용한다. + +아래는 언어 규칙(§4~§7)이 직접 참조하거나 언어 표면(빌트인, 예제)이 전제하는 최소 +표면만 남긴 것이다. `std.list`, `std.map`, `std.io.File`의 전체 API, `std.sys`의 OS +접근 함수 등 나머지 모듈의 정확한 시그니처는 표준 라이브러리 명세가 정의한다. + +- **`std.core`**: `panic`, `set_panic_handler`, `Error`(기본 에러 집합, §4.6), `assert`. `panic`/`set_panic_handler`는 §7.4 트랩 동작이 참조한다. +- **`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`**: + ```fe + pub enum Writer { Stdout, Stderr, File(u16), Null } + pub enum Reader { Stdin, File(u16) } + ``` + 둘 다 정수 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를 수행한다. +- **`std.sys`**: `exit`, `on_exit(f: fn() -> void) -> !void`. `on_exit`은 §7.4 트랩 동작이 참조하는 allocation 없는 고정 크기 callback registry이며 가득 차면 오류를 반환한다. + +--- + +## 11. 의도적으로 제외한 기능 + +**등급 정의** +- `영구` — §1 철학과 정면 충돌. v2.0에서도 넣지 않는다. +- `구조적 불가` — 넣으면 R4를 풀어야 하고 전역 분석이 생긴다(§1 철학 2). 이 언어의 정의상 불가. +- `v0.2` — 넣을 예정. 순서 문제일 뿐 원칙 위반 아님. +- `편의` — 원칙 위반 없음, 구현도 쉬움. 여유 생기면 아무 때나. + +| 기능 | 등급 | 제외 이유 | 대체 수단 | +|---|---|---|---| +| 트레잇/인터페이스 (`dyn`) | **v0.2 (1순위)** | 부트스트랩에 불필요, 타입 시스템 전반에 영향 | Copy handle enum (`io.Writer`, §10) | +| 인터럽트 핸들러와 공유 상태 (`interrupt fn`, `shared`, `atomic`, `critical`) | v0.2 | 벡터 설치·복원과 배리어가 백엔드 지원을 요구하고, 타깃마다 다르다 | polling | +| far 포인터와 세그먼트 주소 지정 | **영구** | x86 리얼모드에만 있는 개념이고 평평한 주소 공간에는 대응물이 없다 (§2) | 없음. 리얼모드 타깃이 생기면 그때 다시 본다 | +| 클로저 | v0.2 | 캡처 = 참조 저장 = R4 위반 소지 | 콜백에 `ctx: *void` 전달 | +| 연산자 오버로딩 | v0.2 (인터페이스 이후) | 숨은 비용. 넣더라도 특정 인터페이스 구현으로만 제한 | 메서드 | +| 튜플 / 다중 반환 | 편의 | 이름 없는 필드는 가독성 손해 | struct | +| 레이블 있는 break | 편의 | — | 플래그 변수 | +| 슬라이스 패턴 매칭 | 편의 | — | 인덱스 비교 | +| `inline fn` | 편의 | — | 인라인 여부는 백엔드가 정한다 | +| `must` 키워드 | 편의 | 실패를 트랩으로 바꾸는 문법 설탕일 뿐 핵심 의미론이 아님 | `expr catch @trap()` | +| 블록 표현식 | 편의 | 값을 만드는 블록이 없으면 `catch`가 짧은 형태로 충분하고, 문법 표면이 작아진다 (§4.6) | `catch <식>`, `return`으로 탈출 | +| 라이프타임 표기 (`'a`) | **구조적 불가** | 전역 분석 필요, R4를 풀어야 함 | R4 (2급 참조), R8 파생 반환, 인덱스 핸들 | +| 선점형 스레드 | 구조적 불가 | DOS 기본 실행 모델에 없고 함수 단위 소유권 모델을 넘어서는 동기화가 필요 | — | +| 매크로 / 전처리기 | **영구** | 도구 지원과 컴파일 속도 파괴 | `const`, `comptime if`, 제네릭, `@print` | +| 예외 | 영구 | 언와인딩 기반 시설 없음, 숨은 비용 | 에러 유니온 | +| GC | 영구 | 결정적 비용 원칙 위반 | 소유권 + RAII + 아레나 | +| 암묵 형변환 | 영구 | 버그 원인 1위 | `as` | +| 상속 | 영구 | 숨은 vtable, 취약한 기반 클래스 | 합성 | +| 최상위 `comptime if` | v0.2 | 타깃이 하나이고 comptime 조건은 타입 술어뿐이라(§7.5) 유닛 바깥에는 물어볼 것이 없다 | 함수 안의 `comptime if`, 또는 유닛을 나눈다 | + +### 11.1 인터페이스 설계 스케치 (v0.2 예정) + +지금 구현하지 않되, 나중에 `io.Writer` Copy handle enum을 무리 없이 대체할 수 있도록 방향만 고정해 둔다. + +```fe +pub interface Writer { + fn write(self: &mut Self, buf: []u8) -> !usize; +} +impl Writer for File { ... } + +fn dump(w: &mut dyn Writer, data: []u8) -> !void { ... } +dump(&mut file, buf); // &mut File → &mut dyn Writer 자동 변환 +``` + +- `dyn I`의 표현은 `(ctx, vtable)` 팻 포인터. vtable은 `(인터페이스, 구현 타입)` 쌍마다 `static const` 하나. +- **동적 디스패치 전용.** 제네릭 타입 제약(trait bound)으로는 쓸 수 없다 — 그걸 허용하면 전역 분석이 생긴다. +- `&dyn I`는 참조이므로 R4가 적용된다(필드 저장 불가). 필드에 담으려면 `^dyn I`(힙 박싱). +- `^dyn I`의 drop은 vtable 경유. 이 때문에 `?^dyn I`, drop 전개, 제네릭 인자로서의 `dyn` 등 타입 시스템 여러 곳에 케이스가 추가되므로 독립적으로 다룬다. +- 도입 시 `io.Writer`/`io.Reader` handle enum을 `dyn` 기반 API로 교체한다. v0.1 safe API에는 이미 대여 대상을 숨긴 `*void`가 없으므로 이 전환은 기능 확장이지 안전성 수정이 아니다. + +위 표에 없는 항목(링크타임 최적화, 디버그 정보 포맷, 언어 서버 등)은 도구 영역이며 v0.2 이후 별도 검토. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..8cf6d8c --- /dev/null +++ b/TODO.md @@ -0,0 +1,101 @@ +# TODO + +``` +uv run python tests/run.py 245/245 컴파일러가 프로그램에 대해 뭐라고 하는가 +uv run python tests/exec.py 38/38 컴파일된 프로그램이 실제로 무엇을 하는가 +``` + +``` +.fe → fec → i386 asm → wasm → wlink → .exe → Windows 11 +``` + +--- + +## 셀프호스팅 전에 하기로 했던 것 — 전부 끝남 + +| | | +|---|---| +| `@print` / `@fprint` 전개 | SPEC §6.3.1 대로 컴파일 단계에서 편다. 진단 한 줄이 한 줄이다 | +| 파일과 명령줄 | 열기·읽기·쓰기·닫기, `argv`. 프로그램이 소스에 박힌 데이터 밖으로 나왔다 | +| `match` 페이로드와 `if let` | 태그드 유니온을 안전하게 해체한다 | +| 조용히 잘리던 상한 | 자란다. 넘쳐도 틀린 코드가 아니라 오류다 | +| `mem.Arena` | R11 이 말하는 아레나 + 핸들이 실제로 쓸 수 있다 | +| 레지스터 할당 | 블록 단위 선형 스캔. calc 4437 → 3796 줄 (-14%) | +| **Ferro 렉서를 Ferro 로** | 강제 함수. 돌아간다 | +| `std.map` | 키를 맵이 소유한 버퍼에 복사하고 슬롯은 위치만 든다 | +| **Ferro 파서를 Ferro 로** | 노드 배열 하나 + 인덱스. `1 + 2 * 3` 이 `(+ 1 (* 2 3))` 로 묶인다 | +| 필드 단위 대여 | `p.a` 와 `p.b` 는 다른 place 다. `std.map` 의 `keep` 이 다시 함수 하나가 됐다 | +| 외부 스펙 감사 대응 | 니치 옵셔널·store 폭·배치 세 버그, 스펙 빈칸, 죽은 C 백엔드, stdlib 다섯, 측정 | + +파서가 알려준 것: **자기 참조 자료구조는 인덱스로 짓는다.** 노드는 `^Node` 를 +들 수 없고(자식이 여럿이며 한 번씩 소유하지 않는다) `&Node` 도 들 수 없다(R4). +소유자 하나와 인덱스 여럿이 남는 유일한 모양이고, 그것으로 충분했다. + +--- + +## 셀프호스팅으로 가는 길 + +| # | 일 | 규모 | 비고 | +|---|---|---|---| +| 1 | 리졸버를 Ferro 로 | 중 | 도구는 다 있다: `intern`, `map`, `Node.bind`, `Map.clear` | +| 2 | 타입 검사를 Ferro 로 | 대 | 리졸버 다음 | +| 3 | `io.read` 로 줄 단위 읽기 | 소 | 지금은 버퍼 하나로 통째로 읽는다 | +| 4 | 여러 반환값 또는 out 파라미터 | 중 | `&mut` 재대여로 되지만 장황하다 | +| 5 | `fec` 을 Ferro 로 | 대 | 여기까지 오면 언어가 자기 무게를 견딘다 | + +## 언어에 남은 구멍 + +| | | +|---|---| +| `[value; count]` 배열 반복 리터럴 | 없다. 큰 버퍼는 `undefined` 로 선언한다 | +| 정수 폭 섞임 | `1 + 함수호출()` 같은 데서 뻑뻑하다. 리터럴이 늘 맞춰주지 않는다 | +| `@sprint` | 전개하지 않는다. `@print`/`@fprint` 만 | +| `interrupt` `shared` `atomic` `critical` | 파싱만 되고 의미 없음. SPEC §11 에서 v0.2 | +| lowering 미구현 진단 | `internal: cannot lower X`. 사용자 오류처럼 보이지 않는다 | +| 배열·enum 페이로드의 자동 해제 | `release_at` 은 구조체 필드까지만 내려간다. `?^T` 도 아직이라 `mem.replace` 로 직접 꺼내야 한다 | +| 컨테이너 두 원소의 동시 `&mut` | 인덱스는 갈라지지 않는다. `swap` 같은 것은 stdlib 안에서 해결한다 | +| `--strip-error-names` | 받아들이지만 아무것도 하지 않는다 (SPEC §4.6) | +| `fmt.fmt_error` | 없다. SPEC §4.6 이 약속만 하고 있다 | + +--- + +## 정해진 것 — 언어 + +`SPEC.md` 가 유일한 출처다. 여기는 **어디를 보는지만** 적는다. 문장을 옮겨 +적으면 한쪽만 고쳐져서 갈라진다. + +| | | +|---|---| +| 타깃, `usize`/`isize` | §2 | +| 정수 리터럴의 타입과 범위 | §3 | +| 호출 인자·반환 위치의 대여 약화 | §4.2 | +| 해제는 소유자를 따라 내려간다 | §5 R1 | +| 대여는 place 단위 | §5 R6 | +| 배타 대여를 호출에 넘기는 것은 재대여 | §4.2, §5 R6 | +| 자기 `drop` 안의 부분 이동 | §5 R7 | +| `--no-checks` 의 오버플로는 랩어라운드 | §7.4 | +| `match`·`defer`·순회·`undefined` 등 작은 규칙 일곱 | §7.7 | +| 제네릭은 모노모피제이션 | §9 | + +## 정해진 것 — 구현 + +`SPEC.md` 에 없는 것들. **여기가 출처다.** + +| | | 사는 곳 | +|---|---|---| +| 덩어리 전달 | 전부 주소로. ISA 마다 다른 구조체 전달 ABI 를 피해감 | `lower.c` | +| 슬라이스 배치 | 포인터 다음 길이 | `lowerpri.h` 한 군데에만 | +| store 폭 | 값이 아니라 **자리**가 정한다 | `lowerstm.c` `store_into` | +| 트랩 | `fe_trap(reason, FE_FILE_n, line)`. 파일은 검사가 쓰인 유닛 | `x86.c`, `rt/start.asm` | +| 오류 코드 | 빌드 전체에서 모아 철자 순으로 1부터 | `lowerstm.c` | +| 레지스터 | ebx·esi·edi 를 블록 안에 머무는 임시값에. eax/ecx/edx 는 스크래치 | `x86.c` | +| 제네릭 인스턴스 | 짓는 중에는 배치하지 않는다. 멤버가 안 정해진 집합 타입은 굳지 않고 물러난다 | `types.c` `layout_type` | +| unsafe 예산 | `std.mem`/`std.sys` 밖은 0. 늘어나면 빌드가 실패한다 | `tests/run.py` | + +--- + +## 파일 크기 규칙 + +**2,000 줄을 넘기지 않는다. 웬만하면 1,000 줄.** 넘으면 나눈다 -- `check.c` 는 +3,937 줄이었고 `lower.c` 는 1,913 줄이었다. 지금 가장 큰 것은 `checkcal.c` +1,044 줄이다. diff --git a/audits/2026-08-17-frontend-gaps.md b/audits/2026-08-17-frontend-gaps.md new file mode 100644 index 0000000..5312b13 --- /dev/null +++ b/audits/2026-08-17-frontend-gaps.md @@ -0,0 +1,77 @@ +# Frontend gap audit + +- 날짜: 2026-08-17 +- 기준 커밋: `6dc298d828872409fdf6b7d2e85830f18a118d9f` +- 범위: parser, checker, 전역 lowering의 경계 +- **해결: 11 건 전부와 `0b`/`0o` 리터럴까지. 모두 구현 쪽이었다.** + fixture 는 아래 표에 적었다. 본문은 조사 시점 그대로다. + +## 재현된 문제 + +아래 최소 입력은 발견 시점의 `fec --check`를 모두 통과했다. + +| ID | 문제 | 필요한 fixture | +|---|---|---| +| FRONT-01 | runtime 호출을 `const` 초기값으로 허용하고 lowering에서 초기값을 방출하지 않음 | `types/badcini.fe` | +| FRONT-02 | runtime 호출을 `static` 초기값으로 허용하고 저장소를 0으로 초기화 | `types/badsini.fe` | +| FRONT-03 | bool 비교 체이닝 허용: `true == false == true` | `types/badchain.fe` | +| FRONT-04 | 괄호 없는 단항식 뒤 cast 허용: `-x as u32` | `types/badunas.fe` | +| FRONT-05 | `unsafe` 밖에서 `asm` 허용 | `types/badasm.fe` | +| FRONT-06 | ABI 문자열 없는 `extern fn f();` 허용 | `types/badexns.fe` | +| FRONT-07 | `extern "c"` 이외 ABI 문자열 허용 | `types/badexab.fe` | +| FRONT-08 | extern 함수 본문 허용 | `types/badexbd.fe` | +| FRONT-09 | `extern`이 아닌 본문 없는 `fn f();`를 외부 심볼로 처리 | `types/badfnsm.fe` | +| FRONT-10 | 빈 enum 선언 허용 | `parse/bademen.fe` | +| FRONT-11 | 빈 error 선언 허용 | `parse/bademer.fe` | + +중복 struct field와 중복 enum variant 선언도 통과했지만, 중복 선언 규칙을 SPEC에서 먼저 +확정해야 하므로 위 목록에는 넣지 않았다. + +## 해결 + +SPEC 은 FRONT-03·04(§6.2), 05(§5 R9), 06~09(§6.1·§7.3), 10·11(§6.1)을 이미 +옳게 적고 있었다. 구현만 따라가지 않았다. FRONT-01·02 는 SPEC 에도 규칙이 +없어서 §7.1 에 문장을 넣었다 -- 전역 초기값은 컴파일 시점에 알 수 있어야 한다. + +| ID | fixture | +|---|---| +| FRONT-01 | `types/badcini.fe` | +| FRONT-02 | `types/badsini.fe` | +| FRONT-03 | `types/badchain.fe` | +| FRONT-04 | `types/badunas.fe` | +| FRONT-05 | `types/badasm.fe` | +| FRONT-06 | `types/badexns.fe` | +| FRONT-07 | `types/badexab.fe` | +| FRONT-08 | `types/badexbd.fe` | +| FRONT-09 | `types/badfnsm.fe` | +| FRONT-10 | `parse/bademen.fe` | +| FRONT-11 | `parse/bademer.fe` | +| 허용되는 짝 | `types/okglobin.fe` | +| `0b`/`0o` | `exec/radix.fe` | + +`-x as T` 와 비교 체이닝을 구별하려면 괄호가 트리에 남아야 해서 노드에 +`FE_NODE_PAREN` 을 두었다. 파싱 뒤에는 `-x as T` 와 `-(x as T)` 가 같은 +트리다. + +남은 것: `parse/` fixture 가 트리 내용을 비교하지 않는다는 지적은 그대로 +유효하다. 우선순위는 지금 `exec/bitnot.fe` 처럼 실행 결과로 구별한다. + +## 이미 알려진 실행 문제 + +`0b`와 `0o` 리터럴은 lexer가 받지만 값 계산이 진법을 반영하지 않는다. 실행 결과를 +고정하는 `exec/radix.fe`가 필요하다. + +## 테스트 기반의 빈틈 + +`parse/` fixture는 `--dump-ast`의 성공 여부만 검사하고 트리 내용은 비교하지 않는다. +따라서 연산자 우선순위나 postfix 결합 방향은 parse fixture만으로 고정되지 않는다. +이런 항목은 실행 결과로 구별하거나 선택적인 AST 기대값 검사를 추가해야 한다. + +## 검증 + +- `uv run python tests/run.py`: `245/245` 통과 +- 각 문제를 독립적인 최소 입력으로 만들어 `--check` 결과를 확인함 +- 조사용 임시 입력은 제거함 + +이 문서는 발견 시점의 상태를 기록한다. 작성 중인 미커밋 수정으로 일부 항목의 상태가 +바뀔 수 있으므로 해결 여부는 fixture와 두 테스트 suite로 확인한다. diff --git a/audits/2026-08-17-spec-fec-parser.md b/audits/2026-08-17-spec-fec-parser.md new file mode 100644 index 0000000..933200e --- /dev/null +++ b/audits/2026-08-17-spec-fec-parser.md @@ -0,0 +1,44 @@ +# SPEC–fec parser audit + +- 날짜: 2026-08-17 +- 기준 커밋: `52aaff62e490e37a0995aaaf7cbda47cf98e54a7` +- 범위: `SPEC.md` §6과 `fec/src/lexer.c`, `fec/src/parser.c` +- **해결: 일곱 전부. 구현 셋(PARSE-04a·05·06), SPEC 다섯(01·02·03·04b·07).** + 판정과 근거는 아래 표에 덧붙였다. 본문은 조사 시점 그대로다. + +## 현재 문제 + +| ID | SPEC | 현재 구현 | 재현 결과 | +|---|---|---|---| +| PARSE-01 | 최상위 `comptime if` 선언 허용 | `comptime if`는 문장에서만 처리 | 최상위 사용을 `expected declaration`으로 거부 | +| PARSE-02 | 타입 이름은 `ident ('.' ident)*` | 타입에서 점 하나만 처리 | `alpha.beta.Gamma`를 파싱하지 못함 | +| PARSE-03 | `catch` EBNF가 binding 없는 block과 binding 뒤 expression도 허용 | 짧은 `catch expr`과 `catch \|e\| block`만 처리 | 구현은 §4.6 설명과 맞고 §6.1 EBNF가 지나치게 넓음 | +| PARSE-04 | `\|`와 `^`는 같은 우선순위, 단항 비트 NOT은 `~` | 각각 우선순위 5와 6, `~` 토큰 없음, 단항 `^` 허용 | `~x`를 거부하고 `a \| b ^ c`를 `a \| (b ^ c)`로 파싱 | +| PARSE-05 | 전역 `static`과 `var`의 타입 필수 | 타입 표기를 선택적으로 처리하고 초기값에서 추론 | `static A = 1;`, `var B = 2;` 모두 검사 통과 | +| PARSE-06 | error code는 정수 literal | 일반 expression을 파싱하며 literal이 아니면 code 검증을 건너뜀 | `error E { Bad = 1 + 2, }`가 검사 통과 | +| PARSE-07 | struct field와 enum vfield의 쉼표 필수 | 닫는 `}` 바로 앞에서는 쉼표 생략 허용 | `struct S { x: i32 }`가 검사 통과 | + +## 해결 + +| ID | 어느 쪽이 틀렸나 | 무엇을 했나 | +|---|---|---| +| PARSE-01 | SPEC | comptime 조건은 타입 술어뿐이라(§7.5) 유닛 바깥에는 물어볼 것이 없다. `comptime_decl` 을 `decl` 에서 빼고 §11 v0.2 로 | +| PARSE-02 | SPEC | `import` 는 unit path 의 마지막 segment 를 바인딩하므로 점 둘 이상인 타입 이름은 만들어질 수 없다. 문법을 `type_name := [ident '.'] ident` 로 | +| PARSE-03 | SPEC | §4.6 과 §11(블록 표현식 배제)이 실제 규칙이고 EBNF 가 넓었다. 두 형태로 나눠 적었다 | +| PARSE-04a | 구현 | `~` 를 넣었다. 렉서·파서·검사·lowering(`xor` with all ones) | +| PARSE-04b | SPEC | `\|` 와 `^` 를 한 단계로 두면 `a \| b ^ c` 가 `(a\|b)^c` 가 되어 C 에서 온 사람을 속인다. 구현(C 순서)이 옳아서 표를 쪼갰다 | +| PARSE-05 | 구현 | 전역 `static`/`var` 는 타입 필수. `const` 는 그대로 추론 | +| PARSE-06 | 구현 | error code 는 정수 리터럴 하나만 받는다 | +| PARSE-07 | SPEC | 마지막 쉼표 생략은 흔하고 `enum` 은 이미 허용하고 있었다. 명세에 적었다 | + +fixture: `parse/badgtype.fe`, `parse/badecode.fe`, `parse/okglobal.fe`, +`exec/bitnot.fe`. 그리고 `tests/run.py` 가 마커를 진단 스트림에만 맞춘다 -- +`--dump-ast` 모드에서 AST 덤프가 먼저 나와 마커가 못 쓰이고 있었다. + +## 검증 + +- `uv run python tests/run.py`: `240/240` 통과 +- 위 항목의 최소 입력을 현재 `fec`에 직접 넣어 파싱 및 `--check` 결과를 확인함 +- 기존 fixture에는 위 괴리를 직접 고정하는 사례가 없음 + +이 문서는 조사 시점의 구현 상태를 기록한다. 언어 규칙의 기준은 `SPEC.md`다. diff --git a/editors/zed/extension.toml b/editors/zed/extension.toml new file mode 100644 index 0000000..5a28ded --- /dev/null +++ b/editors/zed/extension.toml @@ -0,0 +1,11 @@ +id = "ferro" +name = "Ferro" +version = "0.0.1" +schema_version = 1 +authors = ["sebastianrcnt"] +description = "Ferro syntax highlighting for Zed" +repository = "https://github.com/sebastianrcnt/doslang" + +[grammars.ferro] +repository = "https://github.com/sebastianrcnt/doslang" +rev = "40316bb34f5acbaf34979cdf2dce296ae752d81d" diff --git a/editors/zed/languages/ferro/config.toml b/editors/zed/languages/ferro/config.toml new file mode 100644 index 0000000..be09be8 --- /dev/null +++ b/editors/zed/languages/ferro/config.toml @@ -0,0 +1,6 @@ +name = "Ferro" +grammar = "ferro" +path_suffixes = ["fe"] +line_comments = ["// "] +tab_size = 4 +hard_tabs = false diff --git a/editors/zed/languages/ferro/highlights.scm b/editors/zed/languages/ferro/highlights.scm new file mode 100644 index 0000000..4dcca4b --- /dev/null +++ b/editors/zed/languages/ferro/highlights.scm @@ -0,0 +1,12 @@ +; Ferro syntax highlighting for Zed. + +(line_comment) @comment +(block_comment) @comment +(string_literal) @string +(char_literal) @string +(integer_literal) @number +(builtin) @function.builtin +(builtin_type) @type.builtin +(keyword) @keyword +(operator) @operator +(punctuation) @punctuation.delimiter diff --git a/fec/Makefile b/fec/Makefile new file mode 100644 index 0000000..a6eb4c5 --- /dev/null +++ b/fec/Makefile @@ -0,0 +1,20 @@ +CC ?= cc +CFLAGS ?= -O2 -Wall -Wextra -std=c89 +CPPFLAGS ?= -Isrc +SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/types.c src/m7.c src/own.c src/check.c src/lower.c src/emit_c.c src/driver.c +OBJ = $(SRC:.c=.o) + +.PHONY: all clean dos-build +all: fec + +fec: $(OBJ) + $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $(OBJ) + +src/%.o: src/%.c + $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $< + +dos-build: + @echo "Run build-dos.bat inside FreeDOS/Open Watcom." + +clean: + $(RM) $(OBJ) fec diff --git a/fec/check.err b/fec/check.err new file mode 100644 index 0000000..23db97b --- /dev/null +++ b/fec/check.err @@ -0,0 +1 @@ +src\check.c(2795): Error! E1118: ***FATAL*** No such file or directory diff --git a/fec/rt/start.asm b/fec/rt/start.asm new file mode 100644 index 0000000..27fe54c --- /dev/null +++ b/fec/rt/start.asm @@ -0,0 +1,395 @@ +; Ferro runtime: process entry and the trap handler. +; +; The entry point calls the program's `main` and hands its result to +; ExitProcess, so a Ferro program is an ordinary console executable. +; `fe_trap` prints where the program stopped and why, then exits 3. + +.386 +.model flat + +extern _ExitProcess@4 : near +extern _GetStdHandle@4 : near +extern _WriteFile@20 : near +extern fe_main_ : near + +_DATA segment dword public 'DATA' + +reasons dd offset r_bounds, offset r_overflow, offset r_divide + dd offset r_unreach, offset r_explicit +r_bounds db 'index out of bounds',0 +r_overflow db 'integer overflow',0 +r_divide db 'divide by zero',0 +r_unreach db 'reached unreachable code',0 +r_explicit db 'trap',0 +r_unknown db 'trap',0 +prefix db 'ferro: ',0 +at_word db ' at ',0 +colon db ':',0 +newline db 13,10,0 +numbuf db 24 dup(0) +written dd 0 +allocs dd 0 +frees dd 0 + +_DATA ends + +_TEXT segment dword public 'CODE' + +; write_cstr(esi = pointer to a NUL-terminated string) -> void +write_cstr proc near + push ebp + mov ebp, esp + push ebx + push esi + push edi + mov edi, esi + xor ecx, ecx +count_loop: + cmp byte ptr [edi], 0 + je count_done + inc edi + inc ecx + jmp count_loop +count_done: + test ecx, ecx + je write_done + push -11 ; STD_ERROR_HANDLE + call _GetStdHandle@4 + push 0 ; lpOverlapped + push offset written + push ecx + push esi + push eax + call _WriteFile@20 +write_done: + pop edi + pop esi + pop ebx + mov esp, ebp + pop ebp + ret +write_cstr endp + +; write_uint(eax = value) -> void +write_uint proc near + push ebp + mov ebp, esp + push ebx + mov edi, offset numbuf + 15 + mov byte ptr [edi], 0 + mov ebx, 10 +digit_loop: + xor edx, edx + div ebx + add dl, '0' + dec edi + mov [edi], dl + test eax, eax + jnz digit_loop + mov esi, edi + call write_cstr + pop ebx + mov esp, ebp + pop ebp + ret +write_uint endp + +; fe_trap(reason, file, line) -- cdecl, never returns +public fe_trap +fe_trap proc near + push ebp + mov ebp, esp + mov esi, offset prefix + call write_cstr + mov eax, [ebp+8] ; reason + cmp eax, 5 + jb reason_ok + mov esi, offset r_unknown + jmp reason_write +reason_ok: + mov esi, [reasons + eax*4] +reason_write: + call write_cstr + mov esi, offset at_word + call write_cstr + mov esi, [ebp+12] ; file + call write_cstr + mov esi, offset colon + call write_cstr + mov eax, [ebp+16] ; line + call write_uint + mov esi, offset newline + call write_cstr + push 3 + call _ExitProcess@4 +fe_trap endp + +; ---------------------------------------------------------------- primitives +; The standard library is written in Ferro; these are the few things it cannot +; say for itself. All cdecl. + +extern _GetProcessHeap@0 : near +extern _HeapAlloc@12 : near +extern _HeapFree@12 : near +extern _CreateFileA@28 : near +extern _ReadFile@20 : near +extern _CloseHandle@4 : near +extern _GetCommandLineA@0 : near + +; fe_rt_write(handle, ptr, len) -> bytes written +public fe_rt_write +fe_rt_write proc near + push ebp + mov ebp, esp + push ebx + mov eax, [ebp+8] ; 1 = stdout, 2 = stderr + cmp eax, 2 + je pick_err + push -11 + jmp pick_done +pick_err: + push -12 +pick_done: + call _GetStdHandle@4 + push 0 + push offset written + push dword ptr [ebp+16] + push dword ptr [ebp+12] + push eax + call _WriteFile@20 + mov eax, [written] + pop ebx + mov esp, ebp + pop ebp + ret +fe_rt_write endp + +; fe_rt_alloc(n) -> pointer, or zero +public fe_rt_alloc +fe_rt_alloc proc near + push ebp + mov ebp, esp + call _GetProcessHeap@0 + push dword ptr [ebp+8] + push 8 ; HEAP_ZERO_MEMORY + push eax + call _HeapAlloc@12 + inc dword ptr [allocs] + mov esp, ebp + pop ebp + ret +fe_rt_alloc endp + +; fe_rt_free(p) +public fe_rt_free +fe_rt_free proc near + push ebp + mov ebp, esp + mov eax, [ebp+8] + test eax, eax + je free_done + call _GetProcessHeap@0 + push dword ptr [ebp+8] + push 0 + push eax + call _HeapFree@12 + inc dword ptr [frees] +free_done: + mov esp, ebp + pop ebp + ret +fe_rt_free endp + +; fe_rt_allocs() / fe_rt_frees() -- what the allocator has been asked to do, +; so that a test can insist every allocation was released. +public fe_rt_allocs +fe_rt_allocs proc near + mov eax, [allocs] + ret +fe_rt_allocs endp + +public fe_rt_frees +fe_rt_frees proc near + mov eax, [frees] + ret +fe_rt_frees endp + +; fe_rt_write_int(handle, value, is_unsigned) -- decimal, with a sign when +; the value is negative and signed was asked for. +public fe_rt_write_int +fe_rt_write_int proc near + push ebp + mov ebp, esp + push ebx + push esi + push edi + mov edi, offset numbuf + 15 + mov byte ptr [edi], 0 + mov eax, [ebp+12] + xor ebx, ebx ; ebx = 1 when a '-' is needed + cmp dword ptr [ebp+16], 0 + jne int_digits + test eax, eax + jge int_digits + neg eax + mov ebx, 1 +int_digits: + mov ecx, 10 +int_loop: + xor edx, edx + div ecx + add dl, '0' + dec edi + mov [edi], dl + test eax, eax + jnz int_loop + test ebx, ebx + je int_write + dec edi + mov byte ptr [edi], '-' +int_write: + mov esi, offset numbuf + 15 + sub esi, edi + push esi + push edi + push dword ptr [ebp+8] + call fe_rt_write + add esp, 12 + pop edi + pop esi + pop ebx + mov esp, ebp + pop ebp + ret +fe_rt_write_int endp + +; fe_rt_write_hex(handle, value) +public fe_rt_write_hex +fe_rt_write_hex proc near + push ebp + mov ebp, esp + push ebx + push esi + push edi + mov edi, offset numbuf + 15 + mov byte ptr [edi], 0 + mov eax, [ebp+12] +hex_loop: + mov edx, eax + and edx, 15 + cmp dl, 10 + jb hex_digit + add dl, 'a' - 10 - '0' +hex_digit: + add dl, '0' + dec edi + mov [edi], dl + shr eax, 4 + test eax, eax + jnz hex_loop + mov esi, offset numbuf + 15 + sub esi, edi + push esi + push edi + push dword ptr [ebp+8] + call fe_rt_write + add esp, 12 + pop edi + pop esi + pop ebx + mov esp, ebp + pop ebp + ret +fe_rt_write_hex endp + +; fe_rt_open(path, write) -> handle, or -1 +; `path` is a NUL-terminated byte string. Reading opens what is there; writing +; creates or truncates. +public fe_rt_open +fe_rt_open proc near + push ebp + mov ebp, esp + push 0 ; hTemplateFile + push 128 ; FILE_ATTRIBUTE_NORMAL + cmp dword ptr [ebp+12], 0 + jne open_write + push 3 ; OPEN_EXISTING + push 0 + push 1 ; FILE_SHARE_READ + push 80000000h ; GENERIC_READ + jmp open_call +open_write: + push 2 ; CREATE_ALWAYS + push 0 + push 0 + push 40000000h ; GENERIC_WRITE +open_call: + push dword ptr [ebp+8] + call _CreateFileA@28 + mov esp, ebp + pop ebp + ret +fe_rt_open endp + +; fe_rt_read(handle, buf, len) -> bytes read, or -1 +public fe_rt_read +fe_rt_read proc near + push ebp + mov ebp, esp + push 0 + push offset written + push dword ptr [ebp+16] + push dword ptr [ebp+12] + push dword ptr [ebp+8] + call _ReadFile@20 + test eax, eax + jne read_ok + mov eax, -1 + jmp read_done +read_ok: + mov eax, [written] +read_done: + mov esp, ebp + pop ebp + ret +fe_rt_read endp + +; fe_rt_close(handle) +public fe_rt_close +fe_rt_close proc near + push ebp + mov ebp, esp + push dword ptr [ebp+8] + call _CloseHandle@4 + mov esp, ebp + pop ebp + ret +fe_rt_close endp + +; fe_rt_cmdline() -> pointer to the whole command line, NUL terminated. +; Splitting it is the standard library's job, not the runtime's. +public fe_rt_cmdline +fe_rt_cmdline proc near + call _GetCommandLineA@0 + ret +fe_rt_cmdline endp + +; fe_rt_exit(code) -- never returns +public fe_rt_exit +fe_rt_exit proc near + push ebp + mov ebp, esp + push dword ptr [ebp+8] + call _ExitProcess@4 +fe_rt_exit endp + +public fe_start_ +fe_start_ proc near + call fe_main_ + push eax + call _ExitProcess@4 +fe_start_ endp + +_TEXT ends + +end fe_start_ diff --git a/fec/src/arena.c b/fec/src/arena.c new file mode 100644 index 0000000..b7a0bf2 --- /dev/null +++ b/fec/src/arena.c @@ -0,0 +1,56 @@ +#include "arena.h" +#include +#include + +struct FeArenaBlock { + FeArenaBlock *next; + size_t used; + size_t size; + unsigned char data[1]; +}; + +void fe_arena_init(FeArena *a, size_t block_size) +{ + a->blocks = 0; + a->block_size = block_size ? block_size : 16384; +} + +void fe_arena_destroy(FeArena *a) +{ + FeArenaBlock *b = a->blocks; + while (b) { + FeArenaBlock *n = b->next; + free(b); + b = n; + } + a->blocks = 0; +} + +void *fe_arena_alloc(FeArena *a, size_t size) +{ + FeArenaBlock *b; + size_t need; + if (size == 0) size = 1; + need = (size + 7u) & ~(size_t)7u; + b = a->blocks; + if (!b || b->used + need > b->size) { + size_t bs = a->block_size > need ? a->block_size : need; + b = (FeArenaBlock *)malloc(sizeof(FeArenaBlock) + bs - 1); + if (!b) return 0; + b->next = a->blocks; + b->used = 0; + b->size = bs; + a->blocks = b; + } + b->used += need; + return b->data + b->used - need; +} + +char *fe_arena_strdup(FeArena *a, const char *s, size_t n) +{ + char *p = (char *)fe_arena_alloc(a, n + 1); + if (!p) return 0; + if (n) memcpy(p, s, n); + p[n] = '\0'; + return p; +} diff --git a/fec/src/arena.h b/fec/src/arena.h new file mode 100644 index 0000000..8e175f4 --- /dev/null +++ b/fec/src/arena.h @@ -0,0 +1,17 @@ +#ifndef FE_ARENA_H +#define FE_ARENA_H + +#include + +typedef struct FeArenaBlock FeArenaBlock; +typedef struct FeArena { + FeArenaBlock *blocks; + size_t block_size; +} FeArena; + +void fe_arena_init(FeArena *a, size_t block_size); +void fe_arena_destroy(FeArena *a); +void *fe_arena_alloc(FeArena *a, size_t size); +char *fe_arena_strdup(FeArena *a, const char *s, size_t n); + +#endif diff --git a/fec/src/ast.c b/fec/src/ast.c new file mode 100644 index 0000000..7bd93c4 --- /dev/null +++ b/fec/src/ast.c @@ -0,0 +1,39 @@ +#include "ast.h" +#include + +void fe_ast_init(FeAst *a) { fe_arena_init(&a->arena, 32768); a->root=0; } +void fe_ast_destroy(FeAst *a) { fe_arena_destroy(&a->arena); a->root=0; } +FeNode *fe_node(FeAst *a, FeNodeKind k, FeLoc loc, const char *text, unsigned long len) +{ + FeNode *n=(FeNode *)fe_arena_alloc(&a->arena,sizeof(FeNode)); + if (!n) return 0; + n->kind=k; n->loc=loc; n->text=text?fe_arena_strdup(&a->arena,text,len):0; + n->a=n->b=n->c=n->children=n->next=0; n->cname=0; n->aux_text=0; n->aux_cname=0; n->sem_type=0; n->sem_context=0; n->sem_decl=0; n->flags=0; return n; +} +void fe_node_add(FeNode *parent, FeNode *child) +{ + FeNode *p; + if (!child) return; + if (!parent->children) { parent->children=child; return; } + p=parent->children; while(p->next) p=p->next; p->next=child; +} +static void spaces(int n, FILE *out) { while(n-->0) fputc(' ',out); } +void fe_ast_dump(const FeNode *n, int indent, FILE *out) +{ + const FeNode *c; + if (!n) return; + spaces(indent,out); fprintf(out,"(%s",fe_node_name(n->kind)); + if (n->text) fprintf(out," %s",n->text); + fputc('\n',out); + if (n->a) fe_ast_dump(n->a,indent+2,out); + if (n->b) fe_ast_dump(n->b,indent+2,out); + if (n->c) fe_ast_dump(n->c,indent+2,out); + for(c=n->children;c;c=c->next) fe_ast_dump(c,indent+2,out); + spaces(indent,out); fputc(')',out); fputc('\n',out); +} +const char *fe_node_name(FeNodeKind k) +{ + static const char *names[] = {"unit","import","fn","struct","enum","error","const","global","field","param","variant","block","let","var","expr-stmt","assign","if","while","for","match","arm","return","break","continue","defer","unsafe","asm","type","expr","binary","unary","call","index","member","literal","ident","struct-init","array-init","error"}; + if ((unsigned)k >= sizeof(names)/sizeof(names[0])) return "node"; + return names[k]; +} diff --git a/fec/src/ast.h b/fec/src/ast.h new file mode 100644 index 0000000..f13f657 --- /dev/null +++ b/fec/src/ast.h @@ -0,0 +1,66 @@ +#ifndef FE_AST_H +#define FE_AST_H + +#include "arena.h" +#include "lexer.h" +#include + +typedef enum FeNodeKind { + FE_N_UNIT, FE_N_IMPORT, FE_N_FN, FE_N_STRUCT, FE_N_ENUM, FE_N_ERROR_DECL, FE_N_CONST, + FE_N_GLOBAL, FE_N_FIELD, FE_N_PARAM, FE_N_VARIANT, FE_N_BLOCK, FE_N_LET, FE_N_VAR, + FE_N_EXPR_STMT, FE_N_ASSIGN, FE_N_IF, FE_N_WHILE, FE_N_FOR, FE_N_MATCH, FE_N_ARM, + FE_N_RETURN, FE_N_BREAK, FE_N_CONTINUE, FE_N_DEFER, FE_N_UNSAFE, FE_N_ASM, + FE_N_TYPE, FE_N_EXPR, FE_N_BINARY, FE_N_UNARY, FE_N_CALL, FE_N_INDEX, FE_N_MEMBER, + FE_N_LITERAL, FE_N_IDENT, FE_N_STRUCT_INIT, FE_N_ARRAY_INIT, FE_N_ERROR_NODE +} FeNodeKind; + +typedef struct FeNode FeNode; +typedef struct FeType FeType; +struct FeNode { + FeNodeKind kind; + FeLoc loc; + char *text; + FeNode *a; + FeNode *b; + FeNode *c; + FeNode *children; + FeNode *next; + /* Semantic information filled by checking; kept out of AST dumps. */ + char *cname; + char *aux_text; + char *aux_cname; + FeType *sem_type; + /* Expected contextual wrapper, used by M7 for null/Some and E!T + success/failure construction without mutating the expression's type. */ + FeType *sem_context; + FeNode *sem_decl; + unsigned flags; +}; + +/* Bits in FeNode.flags. 0x100 and above belong to own.h. */ +#define FE_NODE_PACKED 0x1U +#define FE_NODE_STATIC 0x2U +#define FE_NODE_SHARED 0x4U +#define FE_NODE_PUB 0x8U +#define FE_NODE_COMPTIME 0x10U +#define FE_NODE_EXTERN 0x20U +/* An index expression that had `..` in it, so it makes a slice rather than + reaching an element. `x[a]` and `x[a..]` are otherwise the same shape. */ +#define FE_NODE_SLICE 0x40U +/* This expression was written inside parentheses. `-x as T` is a mistake + and `-(x as T)` is not, and after parsing they are the same tree. */ +#define FE_NODE_PAREN 0x80U + +typedef struct FeAst { + FeArena arena; + FeNode *root; +} FeAst; + +void fe_ast_init(FeAst *a); +void fe_ast_destroy(FeAst *a); +FeNode *fe_node(FeAst *a, FeNodeKind k, FeLoc loc, const char *text, unsigned long len); +void fe_node_add(FeNode *parent, FeNode *child); +void fe_ast_dump(const FeNode *n, int indent, FILE *out); +const char *fe_node_name(FeNodeKind k); + +#endif diff --git a/fec/src/check.c b/fec/src/check.c new file mode 100644 index 0000000..4240a4a --- /dev/null +++ b/fec/src/check.c @@ -0,0 +1,790 @@ +#include "checkpri.h" + +FeType *unknown(FeCheck *c) +{ + return fe_type_intern(&c->types, ""); +} + +void err(FeCheck *c, FeLoc loc, const char *msg) +{ + fe_diag_error(c->diags, loc, msg); +} + +/* Only numbers and characters have an order (SPEC 6.2). */ +int ordered_type(const FeType *t) +{ + return t && (t->kind==FE_TYPE_INT || t->kind==FE_TYPE_CHAR); +} + +int known(FeType *t) +{ + return t && t->kind != FE_TYPE_UNKNOWN && t->kind != FE_TYPE_ERROR; +} + +/* Is this a projection of `self` inside that type's own `drop`? */ +int in_own_drop(FeCheckerState *s, FeNode *n) +{ + FeNode *base; + if (!s->fn_node || !s->fn_node->text || strcmp(s->fn_node->text,"drop")!=0) + return 0; + base = n ? n->a : 0; + while (base && (base->kind==FE_N_MEMBER || base->kind==FE_N_INDEX)) + base = base->a; + return base && base->kind==FE_N_IDENT && base->text && + strcmp(base->text,"self")==0; +} + +void mark_moved(FeCheckerState *s, FeNode *n, FeType *t) +{ + FeSym *sym=0; + /* Inside a type's own `drop` the object is going away, so taking a field + out of it leaves nothing behind that anyone could read. That is the one + place R7 has nothing to protect. */ + if (n && (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX) && in_own_drop(s,n)) + return; + if (n && n->kind==FE_N_IDENT) + sym=find_symbol(s->scope,n->text ? n->text : ""); + if (s->defer_depth != 0) { + /* A defer capture keeps the owner live until scope cleanup; its body + is not an immediate consuming use. */ + fe_own_mark_consumed(s->c->diags, + sym ? &sym->moved : 0, + sym ? sym->decl : 0, + n,t,1); + return; + } + if (sym && t && !fe_own_is_copy_type(t)) { + if (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX) { + fe_diag_error(s->c->diags,n->loc, + "cannot move a non-Copy value out of a projection; use mem.replace"); + return; + } + /* Reaching an identifier already ran FE_OWN_READ over it, and that + read reported the value as gone if it was. Running the move as well + reports the same sentence at the same column a second time, so stop + at the state the read left behind. */ + if (sym->own.move != FE_OWN_AVAILABLE) return; + if (fe_own_access(s->c->diags,&sym->own,FE_OWN_MOVE,n->loc)) { + sym->moved=sym->own.move; + /* Keep the existing emitter contract: ownership-consuming AST + uses carry this flag, while FeOwnState is the diagnostic + authority. */ + fe_own_mark_consumed(s->c->diags,&sym->moved,sym->decl,n,t,0); + } + return; + } + fe_own_mark_consumed(s->c->diags, + sym ? &sym->moved : 0, + sym ? sym->decl : 0, + n,t,s->defer_depth != 0); +} + +int compatible(FeType *want, FeType *got, FeNode *value) +{ + FeNode *item; + unsigned long count; + if (want && got && value && value->kind==FE_N_ARRAY_INIT && + want->kind==FE_TYPE_ARRAY && got->kind==FE_TYPE_ARRAY) { + if (want->length != got->length) return 0; + count=0; + for (item=value->children; item; item=item->next) { + if (!compatible(want->elem,item->sem_type,item)) return 0; + if (item->kind==FE_N_LITERAL && item->text && + fe_type_is_integer(want->elem) && + fe_type_is_integer(item->sem_type) && + item->text[0]!='\'' && item->text[0]!='"') + item->sem_type=want->elem; + ++count; + } + if (count!=want->length) return 0; + value->sem_type=want; + return 1; + } + if (fe_type_equal(want, got)) return 1; + if (!known(want) || !known(got)) return 1; + return fe_type_is_integer(want) && fe_type_is_integer(got) && value && + value->kind == FE_N_LITERAL && value->text && + value->text[0] != '\'' && value->text[0] != '"'; +} + +/* Does passing `arg` to a parameter of type `param` lend it rather than give + it away? An exclusive borrow handed to a call comes back when the call + returns, so it is not a move. */ +int call_reborrows(const FeType *param, const FeType *arg) +{ + if (!param || !arg) return 0; + if (param->kind==FE_TYPE_REF && arg->kind==FE_TYPE_REF && + param->ref_mut && arg->ref_mut) return 1; + if (param->kind==FE_TYPE_SLICE && arg->kind==FE_TYPE_SLICE && + param->ref_mut && arg->ref_mut) return 1; + return 0; +} + +/* Handing back less than you hold. R8 says a returned reference has to be + derived from a parameter or a static; given that, returning the shared form + of an exclusive one is safe -- the caller cannot do anything with `[]T` that + it could not do with `[]mut T`. Without this a method on `&Self` cannot hand + out a read-only view of what it owns. */ +int return_weakens(const FeType *want, const FeType *got) +{ + if (!want || !got) return 0; + if (want->kind==FE_TYPE_SLICE && got->kind==FE_TYPE_SLICE && + !want->ref_mut && got->ref_mut) + return fe_type_equal(want->elem,got->elem); + if (want->kind==FE_TYPE_REF && got->kind==FE_TYPE_REF && + !want->ref_mut && got->ref_mut) + return fe_type_equal(want->elem,got->elem); + return 0; +} + +int explicit_castable(FeType *a, FeType *b) +{ + if (!a || !b) return 0; + /* An enum without a payload is a number with names on it, so reading it + as one is a widening or narrowing and nothing more. The other direction + is not allowed: an arbitrary number is not a variant. */ + if (a->kind == FE_TYPE_ENUM && !a->fields && + (fe_type_is_integer(b) || b->kind == FE_TYPE_CHAR)) return 1; + return (fe_type_is_integer(a) || a->kind == FE_TYPE_CHAR) && + (fe_type_is_integer(b) || b->kind == FE_TYPE_CHAR); +} + +FeType *node_type(FeCheck *c, FeNode *n) +{ + FeType *t; + if (!n) return unknown(c); + t = fe_type_from_ast(&c->types, n); + n->sem_type = t; + return t; +} + +/* A link-visible name. A unit path has dots in it and a generic instance has + brackets and commas, none of which an assembler will accept, so everything + outside the portable identifier set becomes an underscore. */ +char *unit_cname(FeCheck *c, const char *name) +{ + char *u; + char *p; + unsigned long n; + unsigned long i; + u = c->ast->root && c->ast->root->text ? c->ast->root->text : "unit"; + n = (unsigned long)strlen("fe_") + (unsigned long)strlen(u) + + (unsigned long)strlen(name ? name : "name") + 2UL; + p = (char *)fe_arena_alloc(&c->arena, n); + if (!p) return 0; + strcpy(p, "fe_"); + strcat(p, u); + strcat(p, "_"); + strcat(p, name ? name : "name"); + for (i = 0; p[i]; ++i) { + char ch = p[i]; + if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || ch == '_')) + p[i] = '_'; + } + return p; +} + +char *local_cname(FeCheck *c, const char *name) +{ + char number[24]; + char *p; + unsigned long n; + sprintf(number, "%u", c->local_serial++); + n = (unsigned long)strlen("fe_l_") + (unsigned long)strlen(name) + + (unsigned long)strlen(number) + 2UL; + p = (char *)fe_arena_alloc(&c->arena, n); + if (!p) return 0; + strcpy(p, "fe_l_"); + strcat(p, name ? name : "local"); + strcat(p, "_"); + strcat(p, number); + return p; +} + +FeScope *scope_new(FeCheckerState *s, FeScope *parent) +{ + FeScope *scope; + scope = (FeScope *)fe_arena_alloc(&s->c->arena, sizeof(FeScope)); + if (!scope) { + err(s->c, s->c->ast->root->loc, "out of memory creating scope"); + return parent; + } + scope->parent = parent; + scope->items = 0; + scope->count = 0; + scope->capacity = 0; + return scope; +} + +FeSym *find_current(FeScope *scope, const char *name) +{ + unsigned i; + if (!scope) return 0; + for (i = scope->count; i > 0; --i) + if (strcmp(scope->items[i - 1].name, name) == 0) + return &scope->items[i - 1]; + return 0; +} + +FeSym *find_symbol(FeScope *scope, const char *name) +{ + FeSym *sym; + while (scope) { + sym = find_current(scope, name); + if (sym) return sym; + scope = scope->parent; + } + return 0; +} + +FeSym *add_symbol(FeCheckerState *s, FeScope *scope, + const char *name, FeType *type, FeNode *fn, + int mutable, int initialized, char *cname, + FeNode *decl) +{ + FeSym *items; + unsigned capacity; + FeSym *sym; + if (!name) name = ""; + if (find_current(scope, name)) { + err(s->c, decl ? decl->loc : s->c->ast->root->loc, + "duplicate declaration in scope"); + return 0; + } + if (scope->count == scope->capacity) { + capacity = scope->capacity ? scope->capacity * 2U : 8U; + items = (FeSym *)fe_arena_alloc(&s->c->arena, + capacity * sizeof(FeSym)); + if (!items) { + err(s->c, decl ? decl->loc : s->c->ast->root->loc, + "out of memory growing symbol scope"); + return 0; + } + if (scope->items) + memcpy(items, scope->items, scope->count * sizeof(FeSym)); + scope->items = items; + scope->capacity = capacity; + } + sym = &scope->items[scope->count++]; + sym->name = name; + sym->cname = cname; + sym->type = type; + sym->fn = fn; + sym->mutable = mutable; + sym->initialized = initialized; + sym->moved = FE_OWN_AVAILABLE; + sym->decl = decl; + fe_own_state_init(&sym->own, initialized); + sym->borrow_root = 0; + sym->borrow_field = 0; + sym->borrow_mut = 0; + sym->borrow_defer = 0; + sym->owner = scope; + if (decl) { + decl->cname = cname; + decl->sem_type = type; + } + return sym; +} + +/* Make `unit` the one being checked. Types intern against its name, cnames + are built from it, and diagnostics quote its source rather than whichever + file happened to be parsed last. */ +void enter_unit(FeCheck *c, unsigned index) +{ + FeUnit *u = &c->build->units[index]; + c->unit = u; + c->ast = &u->ast; + c->types.unit_name = u->name[0] ? u->name : "unit"; + fe_diags_source(c->diags, u->source, u->size); +} + + + +static int enter_decl_hook(void *owner, const char *unit); +static void leave_decl_hook(void *owner, int back); + +void fe_check_init(FeCheck *c, FeBuild *build, FeDiags *diags, + unsigned pointer_bits, int no_checks) +{ + unsigned i; + fe_arena_init(&c->arena, 16384); + c->build = build; + c->unit = 0; + c->ast = build->count ? &build->units[0].ast : 0; + for (i = 0; i < FE_BUILD_UNIT_MAX; ++i) c->unit_scope[i] = 0; + c->diags = diags; + c->pointer_bits = pointer_bits; + c->local_serial = 0; + c->no_checks = no_checks; + fe_types_init(&c->types, &c->arena, pointer_bits); + c->types.unit_name = "unit"; + c->types.instantiate = instantiate_type_node; + c->types.instantiate_owner = c; + c->types.enter_decl = enter_decl_hook; + c->types.leave_decl = leave_decl_hook; + c->instances = (FeInstance *)fe_arena_alloc(&c->arena, + (unsigned long)FE_GENERIC_INSTANCE_MAX * sizeof(FeInstance)); + c->instance_count = 0; + c->instance_depth = 0; +} + +void fe_check_destroy(FeCheck *c) +{ + fe_arena_destroy(&c->arena); +} + +unsigned unit_index(FeCheck *c, const FeUnit *u) +{ + return (unsigned)(u - c->build->units); +} + +/* An import introduces a local binding, so `binding.name` reaches into the + unit it names. A local of the same spelling wins -- shadowing a binding is + legal and means the local -- so this only answers when the base name is not + otherwise in scope. */ +FeUnit *binding_unit(FeCheckerState *s, FeNode *base) +{ + if (!base || base->kind!=FE_N_IDENT || !base->text) return 0; + if (!s->c->build || !s->c->unit) return 0; + if (find_symbol(s->scope,base->text)) return 0; + return fe_build_binding(s->c->build,s->c->unit,base->text); +} + +/* SPEC 8.2: a declaration is visible outside its unit only with `pub`. */ +int decl_is_public(const FeNode *decl) +{ + return decl && (decl->flags & FE_NODE_PUB)!=0; +} + +FeSym *unit_member(FeCheck *c, FeUnit *u, const char *name) +{ + if (!u || !name) return 0; + return find_current(c->unit_scope[unit_index(c,u)],name); +} + +/* A type another unit declares, or null if it declares no such type. Interning + is keyed on the declaring unit, so this cannot collide with a same-named + type here. */ +FeType *unit_type(FeCheck *c, FeUnit *u, const char *name) +{ + FeType *t; + if (!u || !name) return 0; + for (t=c->types.types;t;t=t->next) + if (t->unit && strcmp(t->name,name)==0 && + strcmp(t->unit,u->name)==0 && t->kind!=FE_TYPE_UNKNOWN) return t; + return 0; +} + +/* A field type is written in the unit that declared the type, so it has to be + resolved with that unit's imports in scope -- not with whichever unit + happens to be current when the walk reaches it. Returns the index to go back + to, or -1 when there is nowhere to go. */ +int enter_declaring_unit(FeCheck *c, const char *unit_name) +{ + unsigned i; + unsigned here; + if (!unit_name || !c->build || !c->unit) return -1; + here = unit_index(c,c->unit); + for (i=0;ibuild->count;++i) + if (strcmp(c->build->units[i].name,unit_name)==0) { + if (i==here) return -1; + enter_unit(c,i); + return (int)here; + } + return -1; +} + +/* The type layer calls these; it knows nothing about units beyond a name. */ +static int enter_decl_hook(void *owner, const char *unit) +{ + return enter_declaring_unit((FeCheck *)owner, unit); +} + +static void leave_decl_hook(void *owner, int back) +{ + enter_unit((FeCheck *)owner, (unsigned)back); +} + +/* The AST declaration of a type another unit declares, for its visibility and + for its methods. */ +FeNode *unit_type_decl(FeCheck *c, FeUnit *u, const char *name) +{ + FeNode *n; + (void)c; + if (!u || !name) return 0; + for (n=u->ast.root ? u->ast.root->children : 0;n;n=n->next) + if ((n->kind==FE_N_STRUCT || n->kind==FE_N_ENUM || + n->kind==FE_N_ERROR_DECL) && n->text && + strcmp(n->text,name)==0) return n; + return 0; +} + +/* Resolve a type written in another unit's source. Names in a signature mean + what they meant where the signature was written, not where it is called. */ +FeType *node_type_in(FeCheck *c, const char *unit, FeNode *node) +{ + const char *save=c->types.unit_name; + FeType *t; + if (unit) c->types.unit_name=unit; + t=node_type(c,node); + c->types.unit_name=save; + return t; +} + + +FeNode *find_method(FeCheck *c, FeType *owner, const char *name) +{ + FeNode *decl; + FeNode *method; + if(!owner || !name) return 0; + if(owner->decl_node) { + for(method=owner->decl_node->children; method; method=method->next) + if(method->kind==FE_N_FN && method->text && + strcmp(method->text,name)==0) return method; + return 0; + } + for(decl=c->ast->root ? c->ast->root->children : 0; decl; decl=decl->next) + if(decl->kind==FE_N_STRUCT && decl->text && + strcmp(decl->text,owner->name)==0) + for(method=decl->children; method; method=method->next) + if(method->kind==FE_N_FN && method->text && + strcmp(method->text,name)==0) return method; + return 0; +} + +FeType *method_type(FeCheck *c, FeNode *node, FeType *owner) +{ + if(node && node->kind==FE_N_TYPE && node->text && + strcmp(node->text,"Self")==0) return owner; + if(node && node->kind==FE_N_TYPE && node->text && + (strcmp(node->text,"&")==0 || strcmp(node->text,"&mut")==0) && + node->a && node->a->text && strcmp(node->a->text,"Self")==0) + return fe_type_ref(&c->types,owner,strcmp(node->text,"&mut")==0); + /* The rest of a method's signature is written in the unit that declared + the type, so a name in it means what that unit means by it and not what + the caller happens to mean. */ + { + int back=enter_declaring_unit(c,owner ? owner->unit : 0); + FeType *t=node_type(c,node); + if (back>=0) enter_unit(c,(unsigned)back); + return t; + } +} + + +unsigned flow_capture(FeScope *scope, FeFlowSlot *slots, unsigned cap) +{ + unsigned count=0; + unsigned i; + FeScope *p; + for (p=scope; p && countparent) + for (i=0; icount && countitems[i]; + slots[count].moved=p->items[i].moved; + slots[count].initialized=p->items[i].initialized; + slots[count].own_move=p->items[i].own.move; + slots[count].own_initialized=p->items[i].own.initialized; + ++count; + } + return count; +} + +void flow_restore(FeFlowSlot *slots, unsigned count) +{ + unsigned i; + for (i=0; imoved=slots[i].moved; + slots[i].sym->initialized=slots[i].initialized; + slots[i].sym->own.move=slots[i].own_move; + slots[i].sym->own.initialized=slots[i].own_initialized; + } +} + +void flow_merge(FeFlowSlot *base, FeFlowSlot *left, FeFlowSlot *right, + unsigned count) +{ + unsigned i; + for (i=0; imoved=fe_own_merge_move(left[i].moved,right[i].moved); + base[i].sym->initialized=left[i].initialized && right[i].initialized; + base[i].sym->own.move=fe_own_merge_move(left[i].own_move,right[i].own_move); + base[i].sym->own.initialized=left[i].own_initialized && right[i].own_initialized; + } +} + +FeSym *own_root_symbol(FeCheckerState *s, FeNode *expr) +{ + FeOwnPlace place; + if (!fe_own_place_from_expr(expr,&place)) return 0; + return find_symbol(s->scope,place.root->text ? place.root->text : ""); +} + +int own_is_global(FeCheckerState *s, FeSym *sym) +{ + FeScope *p; + if (!s || !sym) return 0; + for (p=s->globals; p; p=p->parent) { + unsigned i; + for (i=0;icount;++i) if (&p->items[i]==sym) return 1; + } + return 0; +} + +/* Strip the `&`/`&mut` off an expression; the place underneath is what is + being reached. */ +static FeNode *own_strip_ref(FeNode *e) +{ + while (e && e->kind==FE_N_UNARY && e->text && + (strcmp(e->text,"&")==0 || strcmp(e->text,"&mut")==0)) + e=e->a; + return e; +} + +/* The first field projected off the root of `expr`, and that root. + `self.bytes.^[i]` projects `bytes` off `self`. An index (`arr[i]`) and a + dereference (`p.^`) name no field, so they answer for the whole value -- + which is what the checker did for everything before. */ +const char *own_projected_field(FeNode *expr, FeNode **root_out) +{ + FeNode *inner; + FeNode *outer=0; + if (root_out) *root_out=0; + inner=own_strip_ref(expr); + while (inner && (inner->kind==FE_N_MEMBER || inner->kind==FE_N_INDEX)) { + outer=inner; + inner=own_strip_ref(inner->a); + } + if (!inner || inner->kind!=FE_N_IDENT || !outer) return 0; + if (outer->kind!=FE_N_MEMBER) return 0; + /* A member node's own text is the token the postfix chain started at, not + the operator, so the spelling of the projection is what to look at: + `.?` carries nothing on the right and `.^` carries a caret. */ + if (outer->text && (strcmp(outer->text,".?")==0 || + strcmp(outer->text,".^")==0)) return 0; + if (!outer->b || !outer->b->text) return 0; + if (strcmp(outer->b->text,"^")==0) return 0; + if (root_out) *root_out=inner; + return outer->b->text; +} + +void own_borrow_expr(FeCheckerState *s, FeNode *expr, int mutable) +{ + FeSym *root=own_root_symbol(s,expr); + const char *field=own_projected_field(expr,0); + if (!root) return; + if (mutable && root->type && root->type->kind==FE_TYPE_REF && + !root->type->ref_mut) { + err(s->c,expr->loc,"cannot create mutable borrow from a shared reference"); + return; + } + if (own_is_global(s,root) && + !(root->decl && root->decl->kind==FE_N_GLOBAL && + (root->decl->flags & 2U) && !mutable)) { + err(s->c,expr->loc,"cannot borrow a mutable global"); + return; + } + fe_own_access_field(s->c->diags,&root->own,field, + mutable ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, + expr->loc); +} + +void own_release_temporary_borrow(FeCheckerState *s, FeNode *expr) +{ + FeSym *root; + if (!expr || expr->kind!=FE_N_UNARY || !expr->text) return; + if (strcmp(expr->text,"&")!=0 && strcmp(expr->text,"&mut")!=0) return; + root=own_root_symbol(s,expr->a); + if (!root) return; + { + const char *field=own_projected_field(expr->a,0); + if (strcmp(expr->text,"&mut")==0) + fe_own_release_exclusive_field(&root->own,field); + else fe_own_release_shared_field(&root->own,field); + } +} + +/* Return-reference provenance is represented at call sites by retaining a + borrow of the unique reference-derived argument (or method receiver). */ +FeSym *own_derived_call_root(FeCheckerState *s, FeNode *call) +{ + FeNode *param; + FeNode *arg; + FeNode *source=0; + unsigned refs=0; + if (!call || call->kind!=FE_N_CALL || !call->sem_type || + !fe_own_is_reference_like(call->sem_type)) return 0; + if (call->a && call->a->kind==FE_N_MEMBER && call->sem_decl) { + param=call->sem_decl->a ? call->sem_decl->a->children : 0; + if (param && param->text && strcmp(param->text,"self")==0) + return own_root_symbol(s,call->a->a); + } + if (!call->sem_decl) return 0; + param=call->sem_decl->a ? call->sem_decl->a->children : 0; + arg=call->children; + while (param && arg) { + FeType *t=node_type(s->c,param->a); + if (fe_own_is_reference_like(t)) { ++refs; source=arg; } + param=param->next; + arg=arg->next; + } + return refs==1 ? own_root_symbol(s,source) : 0; +} + +void own_bind_derived_call(FeCheckerState *s, FeSym *binding, + FeNode *value) +{ + FeSym *root; + if (!binding || !value || value->kind!=FE_N_CALL) return; + root=own_derived_call_root(s,value); + if (!root) return; /* Static provenance. */ + if (root->borrow_root) root=root->borrow_root; + if (value->sem_type->kind==FE_TYPE_REF && value->sem_type->ref_mut) + fe_own_access(s->c->diags,&root->own,FE_OWN_BORROW_MUT,value->loc); + else + fe_own_access(s->c->diags,&root->own,FE_OWN_BORROW_SHARED,value->loc); + binding->borrow_root=root; + binding->borrow_field=0; + binding->borrow_mut=value->sem_type->kind==FE_TYPE_REF && value->sem_type->ref_mut; +} + +int own_stmt_uses(FeNode *node, const char *name) +{ + FeNode *x; + if (!node || !name) return 0; + if (node->kind==FE_N_IDENT && node->text && strcmp(node->text,name)==0) + return 1; + if (own_stmt_uses(node->a,name) || own_stmt_uses(node->b,name) || + own_stmt_uses(node->c,name)) return 1; + for (x=node->children;x;x=x->next) + if (own_stmt_uses(x,name)) return 1; + return 0; +} + +int own_defer_uses(FeNode *node, const char *name) +{ + FeNode *x; + if (!node) return 0; + if (node->kind==FE_N_DEFER && own_stmt_uses(node->a,name)) return 1; + if (own_defer_uses(node->a,name) || own_defer_uses(node->b,name) || + own_defer_uses(node->c,name)) return 1; + for (x=node->children;x;x=x->next) + if (own_defer_uses(x,name)) return 1; + return 0; +} + +int own_contains_node(FeNode *node, FeNode *needle) +{ + FeNode *x; + if (!node || !needle) return 0; + if (node==needle) return 1; + if (own_contains_node(node->a,needle) || + own_contains_node(node->b,needle) || + own_contains_node(node->c,needle)) return 1; + for (x=node->children;x;x=x->next) + if (own_contains_node(x,needle)) return 1; + return 0; +} + +void own_release_after_stmt(FeCheckerState *s, FeScope *scope, + FeNode *stmt, int scope_end) +{ + unsigned i; + FeScope *p; + const FeOwnLastUse *last; + for (p=scope;p;p=scope_end ? 0 : p->parent) for (i=0;icount;++i) { + FeSym *ref=&p->items[i]; + if (!ref->borrow_root) continue; + last=fe_own_last_use(&s->liveness, + ref->decl && ref->decl->text ? ref->decl->text : ref->name); + if (!scope_end && (ref->borrow_defer || !last || last->defer_extended || + !own_contains_node(stmt,last->last_node))) continue; + if (ref->borrow_mut) + fe_own_release_exclusive_field(&ref->borrow_root->own, + ref->borrow_field); + else fe_own_release_shared_field(&ref->borrow_root->own, + ref->borrow_field); + ref->borrow_root=0; + ref->borrow_field=0; + } +} + +/* Full borrow snapshots live in the AST arena, rather than on the 16-bit + compiler stack. The compact FeFlowSlot arrays retain the pre-M6 move and + initialization flow handling. */ +FeOwnState *flow_own_new(FeCheckerState *s, unsigned count) +{ + if (!s || !count) return 0; + return (FeOwnState *)fe_arena_alloc(&s->c->arena, + count*sizeof(FeOwnState)); +} + +void flow_own_capture(FeFlowSlot *slots, FeOwnState *states, + unsigned count) +{ + unsigned i; + if (!states) return; + for (i=0;iown; +} + +void flow_own_restore(FeFlowSlot *slots, FeOwnState *states, + unsigned count) +{ + unsigned i; + if (!states) return; + for (i=0;iown=states[i]; +} + +void flow_own_merge(FeFlowSlot *slots, FeOwnState *left, + FeOwnState *right, unsigned count) +{ + unsigned i; + if (!left || !right) return; + for (i=0;iown=fe_own_merge_state(left[i],right[i]); +} + + +FeFlowBorrow *flow_borrow_new(FeCheckerState *s, unsigned count) +{ + if (!s || !count) return 0; + return (FeFlowBorrow *)fe_arena_alloc(&s->c->arena, + count*sizeof(FeFlowBorrow)); +} + +void flow_borrow_capture(FeFlowSlot *slots, FeFlowBorrow *states, + unsigned count) +{ + unsigned i; + if (!states) return; + for (i=0;iborrow_root; + states[i].field=slots[i].sym->borrow_field; + states[i].mutable=slots[i].sym->borrow_mut; + } +} + +void flow_borrow_restore(FeFlowSlot *slots, FeFlowBorrow *states, + unsigned count) +{ + unsigned i; + if (!states) return; + for (i=0;iborrow_root=states[i].root; + slots[i].sym->borrow_field=states[i].field; + slots[i].sym->borrow_mut=states[i].mutable; + } +} + +void flow_borrow_merge(FeFlowSlot *slots, FeFlowBorrow *left, + FeFlowBorrow *right, unsigned count) +{ + unsigned i; + if (!left || !right) return; + for (i=0;iborrow_root=left[i].root ? left[i].root : right[i].root; + slots[i].sym->borrow_field=left[i].root ? left[i].field : right[i].field; + slots[i].sym->borrow_mut=left[i].mutable || right[i].mutable; + } +} diff --git a/fec/src/check.h b/fec/src/check.h new file mode 100644 index 0000000..7d55ba7 --- /dev/null +++ b/fec/src/check.h @@ -0,0 +1,55 @@ +#ifndef FE_CHECK_H +#define FE_CHECK_H + +#include "types.h" +#include "diag.h" +#include "resolve.h" + +typedef struct FeScope FeScope; + +/* One generic instance, identified by declaring unit, declaration and the + spelling of its type arguments (SPEC 9). The table both deduplicates + requests and bounds how long a chain of new ones can get. */ +#define FE_GENERIC_KEY_MAX 320 +#define FE_GENERIC_INSTANCE_MAX 4096 +typedef struct FeInstance { + char key[FE_GENERIC_KEY_MAX]; + /* What lowering needs to build this instance's code: the declaration, the + arguments bound while it was checked, the unit those names belong to, + and the name the linker will see. */ + FeNode *decl; + FeTypeBind binds[FE_TYPE_PARAM_MAX]; + unsigned bind_count; + const char *home; + const char *cname; + FeType *owner; /* set when the instance is a method */ +} FeInstance; + +/* The checker spans a whole build, not one file. Names cross unit boundaries, + so every unit's declarations have to exist before any unit's bodies are + looked at, and they all have to be interned in one type context or the same + spelling in two units would not be the same type. */ +typedef struct FeCheck { + /* Scopes, symbols and types outlive whichever unit is current, so they + come from the checker's own arena rather than from an AST's. */ + FeArena arena; + FeBuild *build; + FeAst *ast; /* the unit being checked now */ + FeUnit *unit; /* its entry in the build */ + FeScope *unit_scope[FE_BUILD_UNIT_MAX]; + FeTypeCtx types; + FeDiags *diags; + unsigned pointer_bits; + unsigned local_serial; + int no_checks; + FeInstance *instances; + unsigned instance_count; + unsigned instance_depth; +} FeCheck; + +void fe_check_init(FeCheck *c, FeBuild *build, FeDiags *diags, + unsigned pointer_bits, int no_checks); +void fe_check_destroy(FeCheck *c); +int fe_check_program(FeCheck *c); + +#endif diff --git a/fec/src/checkcal.c b/fec/src/checkcal.c new file mode 100644 index 0000000..2347b2e --- /dev/null +++ b/fec/src/checkcal.c @@ -0,0 +1,1050 @@ +#include "checkpri.h" + +int is_error_set_member(FeCheckerState *s, FeNode *n) +{ + return n && n->kind==FE_N_MEMBER && n->a && n->a->kind==FE_N_IDENT && + n->a->text && strcmp(n->a->text,"error")==0 && + n->b && n->b->text && !find_symbol(s->scope,"error"); +} + +/* `binding.name` used as a value rather than called. */ +FeType *cross_unit_value(FeCheckerState *s, FeNode *n, int *handled) +{ + FeUnit *home=binding_unit(s,n->a); + FeSym *sym; + *handled=0; + if (!home) return 0; + *handled=1; + sym=unit_member(s->c,home,n->b && n->b->text ? n->b->text : ""); + if (!sym) { + /* A name in another unit can be a type as well as a value -- + `binding.Enum.Variant` reaches one through the other. */ + FeType *there=unit_type(s->c,home,n->b && n->b->text ? n->b->text : ""); + FeNode *decl=unit_type_decl(s->c,home, + n->b && n->b->text ? n->b->text : ""); + if (there && decl) { + if (!decl_is_public(decl)) { + err(s->c,n->loc,"type is private to its unit"); + return unknown(s->c); + } + n->sem_type=there; + return there; + } + err(s->c,n->loc,"unknown name"); + return unknown(s->c); + } + if (!decl_is_public(sym->decl)) { + err(s->c,n->loc,"name is private to its unit"); + return unknown(s->c); + } + n->cname=sym->cname; + n->sem_decl=sym->decl; + n->sem_type=sym->type; + return sym->type; +} + +/* `skip` leading parameters and arguments have already been consumed as + comptime type arguments. */ +FeType *check_call_args(FeCheckerState *s, FeNode *n, FeSym *sym, + const char *home, unsigned skip) +{ + FeCheck *c=s->c; + FeNode *param; + FeNode *arg; + FeType *a; + FeType *b; + unsigned k; + if (n->a) n->a->cname = sym->cname; + n->sem_decl = sym->fn; + if (!sym->fn) { + err(c, n->loc, "name is not a function"); + return unknown(c); + } + param = sym->fn->a ? sym->fn->a->children : 0; + arg = n->children; + for (k=0;knext; + if (arg) arg=arg->next; + } + while (param && arg) { + a = check_expr(s, arg); + b = node_type_in(c, home, param->a); + if (b && a && b->kind==FE_TYPE_REF && !b->ref_mut && + a->kind==FE_TYPE_REF && a->ref_mut) { + FeSym *root=own_root_symbol(s,arg); + if (root && root->borrow_root) root=root->borrow_root; + if (root) fe_own_call_shared_view(c->diags,&root->own,arg->loc); + } else if (b && a && b->kind==FE_TYPE_SLICE && !b->ref_mut && + a->kind==FE_TYPE_SLICE && a->ref_mut) { + /* Call-only []mut -> [] weakening is a temporary view. */ + } else if (call_reborrows(b, a)) { + /* Handing an exclusive borrow to a call lends it for the length of + that call and takes it back after: the caller cannot touch it + meanwhile, so nothing is aliased. Without this an exclusive + parameter could be passed onwards exactly once. */ + } else mark_moved(s,arg,a); + if (!compatible(b, a, arg) && + !(b && a && b->kind==FE_TYPE_SLICE && a->kind==FE_TYPE_SLICE && + !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && + !(b && a && b->kind==FE_TYPE_REF && a->kind==FE_TYPE_REF && + !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && + a->kind != FE_TYPE_UNKNOWN) + err(c, arg->loc, "argument type mismatch"); + own_release_temporary_borrow(s,arg); + param = param->next; + arg = arg->next; + } + if (param || arg) err(c, n->loc, "wrong number of arguments"); + a = sym->fn->b ? node_type_in(c, home, sym->fn->b) : + fe_type_intern(&c->types, "void"); + n->sem_type = a; + return a; +} + +FeType *check_call(FeCheckerState *s, FeNode *n) +{ + FeCheck *c; + FeNode *arg; + FeNode *value; + FeNode *param; + FeSym *sym; + FeType *a; + FeType *b; + FeType *expected; + c=s->c; + if (n->a && n->a->kind==FE_N_IDENT && n->a->text && + strcmp(n->a->text,"Some")==0) { + err(c,n->loc,"Some is only valid as an optional pattern"); + n->sem_type=unknown(c); + return n->sem_type; + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text) { + arg=n->children; + if (strcmp(n->a->b->text,"replace")==0) { + value=arg ? arg->next : 0; + if (!arg || !value || value->next) { + err(c,n->loc,"mem.replace requires destination and value"); + n->sem_type=unknown(c); + return n->sem_type; + } + a=check_expr(s,arg); + if (!a || a->kind!=FE_TYPE_REF || !a->ref_mut || + !arg->a || !lvalue_writable(s,arg->a)) + err(c,n->loc,"mem.replace destination must be a mutable place"); + expected=a && a->kind==FE_TYPE_REF ? a->elem : 0; + b=m7_check_expected(s,value,expected); + if (expected && !fe_type_equal(expected,b) && + !m7_actual_compatible(expected,b,value)) + err(c,value->loc,"mem.replace value type mismatch"); + mark_moved(s,value,value->sem_type ? value->sem_type : b); + own_release_temporary_borrow(s,arg); + n->sem_type=expected ? expected : unknown(c); + fe_type_require_replace(&c->types,n->sem_type); + return n->sem_type; + } + if (strcmp(n->a->b->text,"destroy")==0) { + a=arg ? check_expr(s,arg) : unknown(c); + if (!arg || arg->next || !a || a->kind!=FE_TYPE_OWNED) + err(c,n->loc,"mem.destroy requires exactly one owned pointer"); + else mark_moved(s,arg,a); + n->sem_type=fe_type_intern(&c->types,"void"); + return n->sem_type; + } + if (strcmp(n->a->b->text,"create")==0 || + strcmp(n->a->b->text,"alloc_slice")==0) + return check_expr_core(s,n); + } + if (n->a && n->a->kind==FE_N_IDENT) { + sym=find_symbol(s->scope,n->a->text ? n->a->text : ""); + if (!sym || !sym->fn) { + err(c,n->loc,"unknown function"); + n->sem_type=unknown(c); + return n->sem_type; + } + if (decl_is_generic(sym->fn)) + return check_generic_call(s,n,sym,current_unit(c)); + n->a->cname=sym->cname; + n->sem_decl=sym->fn; + param=sym->fn->a ? sym->fn->a->children : 0; + arg=n->children; + while (param && arg) { + b=node_type(c,param->a); + a=m7_check_expected(s,arg,b); + if (b && a && b->kind==FE_TYPE_REF && !b->ref_mut && + a->kind==FE_TYPE_REF && a->ref_mut) { + FeSym *root; + root=own_root_symbol(s,arg); + if (root && root->borrow_root) root=root->borrow_root; + if (root) fe_own_call_shared_view(c->diags,&root->own,arg->loc); + } else if (!(b && a && b->kind==FE_TYPE_SLICE && + a->kind==FE_TYPE_SLICE && !b->ref_mut && a->ref_mut) && + !call_reborrows(b, a)) + mark_moved(s,arg,arg->sem_type ? arg->sem_type : a); + if (!fe_type_equal(b,a) && !m7_actual_compatible(b,a,arg) && + !(b && a && b->kind==FE_TYPE_SLICE && a->kind==FE_TYPE_SLICE && + !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && + !(b && a && b->kind==FE_TYPE_REF && a->kind==FE_TYPE_REF && + !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && + a->kind!=FE_TYPE_UNKNOWN) + err(c,arg->loc,"argument type mismatch"); + own_release_temporary_borrow(s,arg); + param=param->next; + arg=arg->next; + } + if (param || arg) err(c,n->loc,"wrong number of arguments"); + n->sem_type=sym->fn->b ? node_type(c,sym->fn->b) : + fe_type_intern(&c->types,"void"); + return n->sem_type; + } + return check_expr_core(s,n); +} + +void m7_capture_flow(FeCheckerState *s, FeFlowSlot *slots, + FeOwnState **own, FeFlowBorrow **borrow, + unsigned *count) +{ + *count=flow_capture(s->scope,slots,FE_M7_FLOW_CAP); + *own=flow_own_new(s,*count); + *borrow=flow_borrow_new(s,*count); + flow_own_capture(slots,*own,*count); + flow_borrow_capture(slots,*borrow,*count); +} + +void m7_restore_flow(FeFlowSlot *slots, FeOwnState *own, + FeFlowBorrow *borrow, unsigned count) +{ + flow_restore(slots,count); + flow_own_restore(slots,own,count); + flow_borrow_restore(slots,borrow,count); +} + +void m7_merge_rhs_flow(FeCheckerState *s, FeFlowSlot *base, + FeOwnState *own_base, + FeFlowBorrow *borrow_base, + unsigned count, FeFlowSlot *rhs, + FeOwnState *own_rhs, + FeFlowBorrow *borrow_rhs) +{ + (void)s; + flow_merge(base,base,rhs,count); + flow_own_merge(base,own_base,own_rhs,count); + flow_borrow_merge(base,borrow_base,borrow_rhs,count); +} + +int m7_stmt_definitely_exits(FeNode *n) +{ + FeNode *last; + if (!n) return 0; + if (n->kind==FE_N_RETURN || n->kind==FE_N_BREAK || + n->kind==FE_N_CONTINUE) return 1; + if (n->kind==FE_N_BLOCK) { + last=n->children; + if (!last) return 0; + while (last->next) last=last->next; + return m7_stmt_definitely_exits(last); + } + if (n->kind==FE_N_IF && n->b && n->c) + return m7_stmt_definitely_exits(n->b) && + m7_stmt_definitely_exits(n->c); + return 0; +} + +FeType *m7_check_lazy(FeCheckerState *s, FeNode *n, + FeM7LazyKind kind) +{ + FeType *left_type; + FeType *payload; + FeType *right_type; + FeFlowSlot base[FE_M7_FLOW_CAP]; + FeFlowSlot rhs[FE_M7_FLOW_CAP]; + FeOwnState *own_base; + FeOwnState *own_rhs; + FeFlowBorrow *borrow_base; + FeFlowBorrow *borrow_rhs; + unsigned count; + unsigned rhs_count; + FeScope *old; + FeType *error_type; + left_type=check_expr(s,n->a); + if (kind==FE_M7_LAZY_ORELSE) { + if (!left_type || left_type->kind!=FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"orelse requires an optional left operand"); + n->sem_type=unknown(s->c); + return n->sem_type; + } + payload=left_type->elem; + if (!fe_own_is_copy_type(payload)) { + if (m7_place_is_projection(n->a)) + err(s->c,n->loc, + "non-Copy optional projection requires mem.replace before orelse"); + else + mark_moved(s,n->a,left_type); + } + m7_capture_flow(s,base,&own_base,&borrow_base,&count); + right_type=m7_check_expected(s,n->b,payload); + if (!fe_type_equal(payload,right_type) && + !m7_actual_compatible(payload,right_type,n->b)) + err(s->c,n->b ? n->b->loc : n->loc,"orelse fallback type mismatch"); + mark_moved(s,n->b,n->b && n->b->sem_type ? n->b->sem_type : right_type); + rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); + own_rhs=flow_own_new(s,rhs_count); + borrow_rhs=flow_borrow_new(s,rhs_count); + flow_own_capture(rhs,own_rhs,rhs_count); + flow_borrow_capture(rhs,borrow_rhs,rhs_count); + if (rhs_count==count) + m7_merge_rhs_flow(s,base,own_base,borrow_base,count, + rhs,own_rhs,borrow_rhs); + n->sem_type=payload; + return payload; + } + if (!left_type || left_type->kind!=FE_TYPE_ERROR_UNION) { + err(s->c,n->loc,"catch requires an error result"); + n->sem_type=unknown(s->c); + return n->sem_type; + } + payload=left_type->error_value; + mark_moved(s,n->a,left_type); + m7_capture_flow(s,base,&own_base,&borrow_base,&count); + if (n->c) { + old=s->scope; + s->scope=scope_new(s,old); + error_type=fe_m7_error_type(&s->c->types,left_type); + if (n->b && n->b->text) + add_symbol(s,s->scope,n->b->text,error_type,0,0,1, + local_cname(s->c,n->b->text),n->b); + check_stmt(s,n->c); + s->scope=old; + if (payload && payload->kind!=FE_TYPE_VOID && + !m7_stmt_definitely_exits(n->c)) + err(s->c,n->loc, + "catch block for a value result must exit instead of falling through"); + if (payload && payload->kind==FE_TYPE_VOID) { + rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); + own_rhs=flow_own_new(s,rhs_count); + borrow_rhs=flow_borrow_new(s,rhs_count); + flow_own_capture(rhs,own_rhs,rhs_count); + flow_borrow_capture(rhs,borrow_rhs,rhs_count); + if (rhs_count==count) + m7_merge_rhs_flow(s,base,own_base,borrow_base,count, + rhs,own_rhs,borrow_rhs); + } else { + m7_restore_flow(base,own_base,borrow_base,count); + } + n->sem_type=payload; + return payload; + } + right_type=m7_check_expected(s,n->b,payload); + if (!fe_type_equal(payload,right_type) && + !m7_actual_compatible(payload,right_type,n->b)) + err(s->c,n->b ? n->b->loc : n->loc,"catch fallback type mismatch"); + mark_moved(s,n->b,n->b && n->b->sem_type ? n->b->sem_type : right_type); + rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); + own_rhs=flow_own_new(s,rhs_count); + borrow_rhs=flow_borrow_new(s,rhs_count); + flow_own_capture(rhs,own_rhs,rhs_count); + flow_borrow_capture(rhs,borrow_rhs,rhs_count); + if (rhs_count==count) + m7_merge_rhs_flow(s,base,own_base,borrow_base,count, + rhs,own_rhs,borrow_rhs); + n->sem_type=payload; + return payload; +} + +static FeType *check_expr_dispatch(FeCheckerState *s, FeNode *n); + +/* Every expression goes through here, which is where a projection can tell + the identifier underneath it which field is actually being reached. */ +FeType *check_expr(FeCheckerState *s, FeNode *n) +{ + const char *save_field=s->proj_field; + FeNode *save_base=s->proj_base; + FeType *t; + if (n && (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX)) + s->proj_field=own_projected_field(n,&s->proj_base); + t=check_expr_dispatch(s,n); + s->proj_field=save_field; + s->proj_base=save_base; + return t; +} + +static FeType *check_expr_dispatch(FeCheckerState *s, FeNode *n) +{ + FeType *a; + FeType *b; + FeType *ret_error; + FeType *got_error; + FeM7LazyKind lazy; + const char *op; + if (!n) return unknown(s->c); + if (fe_m7_is_null(n)) { + err(s->c,n->loc,"null requires a contextual optional type"); + n->sem_type=unknown(s->c); + return n->sem_type; + } + if (n->kind==FE_N_IDENT) + return check_identifier(s,n); + if (n->kind==FE_N_LITERAL) + return check_expr_core(s,n); + if (n->kind==FE_N_CALL) + return check_call(s,n); + if (n->kind==FE_N_MEMBER) { + int handled; + FeType *cross; + if (is_error_set_member(s,n)) { + n->sem_type=fe_type_intern(&s->c->types,"core.Error"); + return n->sem_type; + } + cross=cross_unit_value(s,n,&handled); + if (handled) return cross; + a=check_expr(s,n->a); + return m7_member_field(s,n,a); + } + if (n->kind==FE_N_INDEX) + return check_index(s,n); + if (n->kind==FE_N_UNARY) { + op=n->text ? n->text : ""; + if (strcmp(op,"try")==0) { + a=check_expr(s,n->a); + if (!a || a->kind!=FE_TYPE_ERROR_UNION) { + err(s->c,n->loc,"try requires an error result"); + n->sem_type=unknown(s->c); + return n->sem_type; + } + if (!s->ret || s->ret->kind!=FE_TYPE_ERROR_UNION) { + err(s->c,n->loc,"try requires an enclosing error result"); + } else { + ret_error=fe_m7_error_type(&s->c->types,s->ret); + got_error=fe_m7_error_type(&s->c->types,a); + if (!ret_error || !got_error || !fe_type_equal(ret_error,got_error)) + err(s->c,n->loc, + "try error type must exactly match the enclosing error result"); + } + mark_moved(s,n->a,a); + n->sem_type=a->error_value; + return n->sem_type; + } + if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) { + a=check_expr(s,n->a); + own_borrow_expr(s,n->a,strcmp(op,"&mut")==0); + n->sem_type=fe_type_ref(&s->c->types,a,strcmp(op,"&mut")==0); + return n->sem_type; + } + return check_expr_core(s,n); + } + if (n->kind==FE_N_BINARY) { + lazy=fe_m7_lazy_kind(n); + if (lazy!=FE_M7_LAZY_NONE) + return m7_check_lazy(s,n,lazy); + op=n->text ? n->text : ""; + if ((strcmp(op,"==")==0 || strcmp(op,"!=")==0) && + (fe_m7_is_null(n->a) || fe_m7_is_null(n->b))) { + FeNode *nonnull; + FeNode *nullnode; + nonnull=fe_m7_is_null(n->a) ? n->b : n->a; + nullnode=fe_m7_is_null(n->a) ? n->a : n->b; + a=check_expr(s,nonnull); + if (!a || a->kind!=FE_TYPE_OPTIONAL) + err(s->c,n->loc,"null comparison requires an optional value"); + else { + nullnode->sem_type=a; + nullnode->sem_context=a; + } + n->sem_type=fe_type_intern(&s->c->types,"bool"); + return n->sem_type; + } + a=check_expr(s,n->a); + b=check_expr(s,n->b); + if (strcmp(op,"and")==0 || strcmp(op,"or")==0) { + if ((known(a) && a->kind!=FE_TYPE_BOOL) || + (known(b) && b->kind!=FE_TYPE_BOOL)) + err(s->c,n->loc,"logical operator requires bool operands"); + n->sem_type=fe_type_intern(&s->c->types,"bool"); + return n->sem_type; + } + if (strcmp(op,"==")==0 || strcmp(op,"!=")==0 || + strcmp(op,"<")==0 || strcmp(op,"<=")==0 || + strcmp(op,">")==0 || strcmp(op,">=")==0) { + if (known(a) && known(b) && !fe_type_equal(a,b) && + !m7_actual_compatible(a,b,n->b) && + !m7_actual_compatible(b,a,n->a)) + err(s->c,n->loc,"comparison operands have different types"); + /* Only numbers and characters have an order. */ + else if (strcmp(op,"==")!=0 && strcmp(op,"!=")!=0 && + ((known(a) && !ordered_type(a)) || + (known(b) && !ordered_type(b)))) + err(s->c,n->loc,"ordering requires integer or char operands"); + n->sem_type=fe_type_intern(&s->c->types,"bool"); + return n->sem_type; + } + /* A raw pointer plus a number is an address further along. Only raw + pointers: an owner or a borrow has a place it belongs to, and + walking away from it is what `*T` is for. */ + if (known(a) && a->kind==FE_TYPE_RAW && known(b) && + fe_type_is_integer(b) && op[0] && (op[0]=='+' || op[0]=='-')) { + n->sem_type=a; + return n->sem_type; + } + if ((known(a) && !fe_type_is_integer(a)) || + (known(b) && !fe_type_is_integer(b)) || + (known(a) && known(b) && !fe_type_equal(a,b) && + !m7_actual_compatible(a,b,n->b) && + !m7_actual_compatible(b,a,n->a))) + err(s->c,n->loc,"arithmetic operands must have the same integer type"); + n->sem_type=a; + return a; + } + if (n->kind==FE_N_TYPE && n->text && strcmp(n->text,"as")==0) + return check_expr_core(s,n); + if (n->kind==FE_N_STRUCT_INIT) + return check_struct_init(s,n); + if (n->kind==FE_N_ARRAY_INIT) + return check_array_init(s,n); + return check_expr_core(s,n); +} + +static FeType *check_lvalue_dispatch(FeCheckerState *s, FeNode *n, int read); + +/* An assignment target is a projection too, so it leaves the same word for the + identifier underneath: `self.room = x` reaches `room` and nothing else. */ +FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read) +{ + const char *save_field=s->proj_field; + FeNode *save_base=s->proj_base; + FeType *t; + if (n && (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX)) + s->proj_field=own_projected_field(n,&s->proj_base); + t=check_lvalue_dispatch(s,n,read); + s->proj_field=save_field; + s->proj_base=save_base; + return t; +} + +static FeType *check_lvalue_dispatch(FeCheckerState *s, FeNode *n, int read) +{ + FeType *base=0; + FeFieldType *field; + FeType *owner; + if (!n) return unknown(s->c); + if (n->kind==FE_N_MEMBER) { + base=check_expr(s,n->a); + if (base && base->kind==FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"optional value must be projected with '.?' first"); + return unknown(s->c); + } + if (base && base->kind==FE_TYPE_REF && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + if (!base->ref_mut) + err(s->c,n->loc,"cannot write through shared reference"); + n->sem_type=base->elem; + return base->elem; + } + owner=base; + if ((base->kind==FE_TYPE_REF || base->kind==FE_TYPE_OWNED) && + base->elem && base->elem->kind==FE_TYPE_STRUCT) + owner=base->elem; + if (owner && owner->kind==FE_TYPE_STRUCT && n->b && n->b->text) { + if (base->kind==FE_TYPE_REF && !base->ref_mut) + err(s->c,n->loc,"cannot write through shared reference"); + /* Writing a field still needs a writable place. This branch used to + be reached only by units mentioning M7 syntax, so it never had to + repeat the check the M6 path does. */ + if (base->kind!=FE_TYPE_REF && base->kind!=FE_TYPE_OWNED && + !lvalue_writable(s,n->a)) + err(s->c,n->loc,"cannot assign through immutable value"); + field=fe_type_field(owner,n->b->text); + if (!field) { + err(s->c,n->loc,"assignment requires a valid struct field"); + return unknown(s->c); + } + n->sem_type=field->type; + return field->type; + } + } + return check_lvalue_core(s,n,read,n->kind==FE_N_MEMBER ? base : 0); +} + +FeType *m7_pattern_binding_type(FeCheckerState *s, FeType *payload, + FeNode *source, int *borrow_mut) +{ + FeSym *root; + int mutable; + *borrow_mut=0; + if (fe_own_is_copy_type(payload)) return payload; + root=own_root_symbol(s,source); + mutable=root && root->mutable; + *borrow_mut=mutable; + if (payload->kind==FE_TYPE_OWNED && payload->elem) + return fe_type_ref(&s->c->types,payload->elem,mutable); + return fe_type_ref(&s->c->types,payload,mutable); +} + +void m7_check_if_let(FeCheckerState *s, FeNode *n) +{ + FeType *opt; + FeType *binding_type; + FeNode *binding; + FeSym *root; + FeScope *old; + FeFlowSlot base[FE_M7_FLOW_CAP]; + FeFlowSlot left[FE_M7_FLOW_CAP]; + FeFlowSlot right[FE_M7_FLOW_CAP]; + FeOwnState *own_base; + FeOwnState *own_left; + FeOwnState *own_right; + FeFlowBorrow *borrow_base; + FeFlowBorrow *borrow_left; + FeFlowBorrow *borrow_right; + unsigned count; + unsigned i; + int borrow_mut; + int is_some; + opt=check_expr(s,n->a); + if (!opt || opt->kind!=FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"if let Some/None requires an optional value"); + return; + } + is_some=n->aux_text && strcmp(n->aux_text,"Some")==0; + if (!is_some && (!n->aux_text || strcmp(n->aux_text,"None")!=0)) + err(s->c,n->loc,"if let optional pattern must be Some or None"); + m7_capture_flow(s,base,&own_base,&borrow_base,&count); + old=s->scope; + s->scope=scope_new(s,old); + root=0; + borrow_mut=0; + binding=n->children; + if (is_some && binding) { + binding_type=m7_pattern_binding_type(s,opt->elem,n->a,&borrow_mut); + add_symbol(s,s->scope,binding->text,binding_type,0,borrow_mut,1, + local_cname(s->c,binding->text),binding); + if (!fe_own_is_copy_type(opt->elem)) { + root=own_root_symbol(s,n->a); + if (root) + fe_own_access(s->c->diags,&root->own, + borrow_mut ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, + n->loc); + } + } + check_stmt(s,n->b); + if (root) { + if (borrow_mut) fe_own_release_exclusive(&root->own); + else fe_own_release_shared(&root->own); + } + s->scope=old; + flow_capture(s->scope,left,count); + own_left=flow_own_new(s,count); + borrow_left=flow_borrow_new(s,count); + flow_own_capture(left,own_left,count); + flow_borrow_capture(left,borrow_left,count); + m7_restore_flow(base,own_base,borrow_base,count); + if (n->c) check_stmt(s,n->c); + if (n->c) { + flow_capture(s->scope,right,count); + own_right=flow_own_new(s,count); + borrow_right=flow_borrow_new(s,count); + flow_own_capture(right,own_right,count); + flow_borrow_capture(right,borrow_right,count); + } else { + own_right=flow_own_new(s,count); + borrow_right=flow_borrow_new(s,count); + for (i=0;ichildren;arm;arm=arm->next) { + m7_restore_flow(base,own_base,borrow_base,count); + old=s->scope; + s->scope=scope_new(s,old); + root=0; + borrow_mut=0; + if (arm->text && strcmp(arm->text,"Some")==0) { + if (seen_some) err(s->c,arm->loc,"duplicate Some match arm"); + seen_some=1; + binding=arm->children; + if (binding) { + binding_type=m7_pattern_binding_type(s,opt->elem,n->a,&borrow_mut); + add_symbol(s,s->scope,binding->text,binding_type,0,borrow_mut,1, + local_cname(s->c,binding->text),binding); + if (!fe_own_is_copy_type(opt->elem)) { + root=own_root_symbol(s,n->a); + if (root) + fe_own_access(s->c->diags,&root->own, + borrow_mut ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, + arm->loc); + } + } + } else if (arm->text && strcmp(arm->text,"None")==0) { + if (seen_none) err(s->c,arm->loc,"duplicate None match arm"); + seen_none=1; + } else if (arm->text && strcmp(arm->text,"_")==0) { + wildcard=1; + } else { + err(s->c,arm->loc,"optional match arm must be Some, None, or _"); + } + if (arm->a && arm->a->kind==FE_N_BLOCK) check_stmt(s,arm->a); + else if (arm->a) check_expr(s,arm->a); + if (root) { + if (borrow_mut) fe_own_release_exclusive(&root->own); + else fe_own_release_shared(&root->own); + } + s->scope=old; + flow_capture(s->scope,current,count); + own_current=flow_own_new(s,count); + borrow_current=flow_borrow_new(s,count); + flow_own_capture(current,own_current,count); + flow_borrow_capture(current,borrow_current,count); + if (!have) { + for (i=0;ic,n->loc,"non-exhaustive optional match"); + if (have) m7_restore_flow(merged,own_merged,borrow_merged,count); +} + +void m7_check_match_stmt(FeCheckerState *s, FeNode *n) +{ + FeType *value; + value=check_expr(s,n->a); + if (value && value->kind==FE_TYPE_OPTIONAL) { + m7_check_optional_match(s,n,value); + n->sem_type=unknown(s->c); + return; + } + check_match(s,n); +} + +void m7_check_decl_stmt(FeCheckerState *s, FeNode *n, int mutable) +{ + FeType *expected; + FeType *actual; + FeType *stored; + FeSym *sym; + int initialized; + expected=n->a ? node_type(s->c,n->a) : 0; + if (n->b) + stored=m7_check_expected(s,n->b,expected); + else + stored=expected ? expected : unknown(s->c); + actual=n->b && n->b->sem_type ? n->b->sem_type : stored; + if (!expected) expected=stored; + if (!n->a && n->b && fe_m7_is_null(n->b)) + err(s->c,n->loc,"null initializer requires an explicit optional type"); + if (expected && expected->kind==FE_TYPE_VOID) + err(s->c,n->loc,"variable cannot have void type"); + if (n->b && !fe_type_equal(expected,stored) && + !m7_actual_compatible(expected,stored,n->b)) { + /* Say which rule was hit. Weakening &mut to & is a distinct thing from + two unrelated types not matching, and "type mismatch" told the reader + nothing about why the exclusive borrow could not be shared. */ + if (expected && stored && expected->kind==FE_TYPE_REF && + stored->kind==FE_TYPE_REF && !expected->ref_mut && stored->ref_mut) + err(s->c,n->loc, + "cannot rebind a mut borrow as a shared reference"); + else if (expected && stored && expected->kind==FE_TYPE_SLICE && + stored->kind==FE_TYPE_SLICE && !expected->ref_mut && + stored->ref_mut) + err(s->c,n->loc, + "cannot rebind a mut slice as a shared slice"); + else + err(s->c,n->loc,"initializer type mismatch"); + } + /* Rules the M6 declaration case carried that this one has to repeat now + that it is the only declaration case. */ + if (n->b && stored && stored->kind==FE_TYPE_VOID) + err(s->c,n->loc,"void expression cannot initialize a variable"); + if (!mutable && expected && expected->kind==FE_TYPE_SLICE && + expected->ref_mut) + err(s->c,n->loc,"let cannot bind a mutable slice"); + if (!n->b && !n->a) + err(s->c,n->loc,"uninitialized var requires an explicit type"); + if (n->b) mark_moved(s,n->b,actual); + initialized=n->b!=0; + sym=add_symbol(s,s->scope,n->text,expected,0,mutable,initialized, + local_cname(s->c,n->text ? n->text : "local"),n); + if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && + (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { + sym->borrow_root=own_root_symbol(s,n->b->a); + sym->borrow_field=own_projected_field(n->b->a,0); + sym->borrow_mut=strcmp(n->b->text,"&mut")==0; + sym->borrow_defer=s->defer_depth!=0 || + own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); + } + own_bind_derived_call(s,sym,n->b); +} + +void check_stmt(FeCheckerState *s, FeNode *n) +{ + FeScope *old; + FeNode *x; + FeType *expected; + FeType *actual; + FeType *stored; + FeSym *sym; + if (!n) return; + switch (n->kind) { + case FE_N_BLOCK: + old=s->scope; + s->scope=scope_new(s,old); + for (x=n->children;x;x=x->next) { + check_stmt(s,x); + own_release_after_stmt(s,s->scope,x,0); + } + own_release_after_stmt(s,s->scope,n,1); + s->scope=old; + break; + case FE_N_LET: + case FE_N_CONST: + m7_check_decl_stmt(s,n,0); + break; + case FE_N_VAR: + if (!n->a && !n->b) + err(s->c,n->loc,"uninitialized var requires an explicit type"); + m7_check_decl_stmt(s,n,1); + break; + case FE_N_ASSIGN: + expected=check_lvalue(s,n->a,compound_operator(n->text)); + stored=m7_check_expected(s,n->b,expected); + actual=n->b && n->b->sem_type ? n->b->sem_type : stored; + if (!fe_type_equal(expected,stored) && + !m7_actual_compatible(expected,stored,n->b)) + err(s->c,n->loc,"assignment type mismatch"); + mark_moved(s,n->b,actual); + sym=n->a && n->a->kind==FE_N_IDENT ? + find_symbol(s->scope,n->a->text) : 0; + if (sym && sym->mutable) { + sym->initialized=1; + fe_own_access(s->c->diags,&sym->own,FE_OWN_WRITE,n->a->loc); + sym->moved=sym->own.move; + /* Rebinding a reference, from the M6 assignment case: the new + source has to live at least as long as the reference does, and + the previous borrow has to be released. */ + if (n->b && n->b->kind==FE_N_UNARY && n->b->text && + (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0) && + fe_own_is_reference_like(sym->type)) { + FeSym *root=own_root_symbol(s,n->b->a); + if (root && root->owner!=sym->owner) + err(s->c,n->b->loc,"reference would outlive its source scope"); + else if (root) { + if (sym->borrow_root) { + if (sym->borrow_mut) + fe_own_release_exclusive(&sym->borrow_root->own); + else fe_own_release_shared(&sym->borrow_root->own); + } + sym->borrow_root=root; + sym->borrow_mut=strcmp(n->b->text,"&mut")==0; + } + } + } + break; + case FE_N_EXPR_STMT: + check_expr(s,n->a); + break; + case FE_N_DEFER: + ++s->defer_depth; + check_stmt(s,n->a); + --s->defer_depth; + break; + case FE_N_IF: + if (n->text && strcmp(n->text,"comptime if")==0) { + int taken=0; + if (!comptime_condition(s,n->a,&taken)) { + err(s->c,n->a?n->a->loc:n->loc, + "comptime condition must be decidable at compile time"); + break; + } + /* SPEC 9: the branch that is not taken is parsed and nothing more. */ + if (taken) check_stmt(s,n->b); + else if (n->c) check_stmt(s,n->c); + break; + } + if (n->text && strcmp(n->text,"if let")==0) + m7_check_if_let(s,n); + else { + FeType *cond; + FeFlowSlot base[FE_M7_FLOW_CAP]; + FeFlowSlot left[FE_M7_FLOW_CAP]; + FeFlowSlot right[FE_M7_FLOW_CAP]; + FeOwnState *own_base; + FeOwnState *own_left; + FeOwnState *own_right; + FeFlowBorrow *borrow_base; + FeFlowBorrow *borrow_left; + FeFlowBorrow *borrow_right; + unsigned count; + unsigned i; + cond=check_expr(s,n->a); + if (known(cond) && cond->kind!=FE_TYPE_BOOL) + err(s->c,n->loc,"if condition must be bool"); + /* Once a function is on the M7 checker path, branch bodies must + remain on that path as well. In particular, return T inside + E!T relies on contextual success construction even when the + branch itself contains no surface M7 syntax. */ + m7_capture_flow(s,base,&own_base,&borrow_base,&count); + check_stmt(s,n->b); + flow_capture(s->scope,left,count); + own_left=flow_own_new(s,count); + borrow_left=flow_borrow_new(s,count); + flow_own_capture(left,own_left,count); + flow_borrow_capture(left,borrow_left,count); + /* A branch that always leaves contributes nothing to what follows. + Merging its state would make a value it consumed look consumed + afterwards, on a path that never ran it. */ + if (m7_stmt_definitely_exits(n->b)) { + for (i=0;ic) check_stmt(s,n->c); + if (n->c) { + flow_capture(s->scope,right,count); + own_right=flow_own_new(s,count); + borrow_right=flow_borrow_new(s,count); + flow_own_capture(right,own_right,count); + flow_borrow_capture(right,borrow_right,count); + if (m7_stmt_definitely_exits(n->c)) { + for (i=0;idefer_depth != 0) + err(s->c, n->loc, "cannot return from inside defer"); + expected=s->ret; + if (n->a) + stored=m7_check_expected(s,n->a,expected); + else + stored=fe_type_intern(&s->c->types,"void"); + actual=n->a && n->a->sem_type ? n->a->sem_type : stored; + /* R8, from the M6 return case: a returned reference has to come from a + parameter or a static, never from a local. */ + if (expected && fe_own_is_reference_like(expected) && + !own_return_from_allowed_root(s,n->a)) + err(s->c,n->loc, + "reference return must be derived from a parameter or static"); + if (n->a && stored && stored->kind==FE_TYPE_VOID && + expected && expected->kind!=FE_TYPE_VOID) + err(s->c,n->loc,"void expression returned from value function"); + if (expected && expected->kind==FE_TYPE_ERROR_UNION && n->a && + actual && actual->kind==FE_TYPE_ERROR_UNION && + !fe_type_equal(expected,actual)) + err(s->c,n->loc,"error result type mismatch"); + /* A bare `return` in a function returning `!void` is the success case: + there is no value to give, and no error either. */ + else if (!n->a && expected && expected->kind==FE_TYPE_ERROR_UNION && + expected->error_value && + expected->error_value->kind==FE_TYPE_VOID) { } + else if (!fe_type_equal(expected,stored) && + !m7_actual_compatible(expected,stored,n->a) && + !return_weakens(expected,stored)) + err(s->c,n->loc,"return type mismatch"); + if (n->a) mark_moved(s,n->a,actual); + break; + case FE_N_WHILE: + case FE_N_FOR: + /* The core loop case carries the flow capture and merge that detects a + value moved on every iteration, and it already recurses into the body + through this function, so there is nothing to special-case here. The + M7 half used to skip all of it. */ + check_stmt_core(s,n); + break; + case FE_N_BREAK: + case FE_N_CONTINUE: + if (!s->loop_depth) + err(s->c,n->loc,"break or continue outside loop"); + break; + case FE_N_UNSAFE: + ++s->unsafe_depth; + check_stmt(s,n->a); + --s->unsafe_depth; + break; + case FE_N_ASM: + if (!s->unsafe_depth) + err(s->c,n->loc,"asm requires an unsafe block"); + break; + default: + check_stmt_core(s,n); + break; + } +} diff --git a/fec/src/checkexp.c b/fec/src/checkexp.c new file mode 100644 index 0000000..bf154a6 --- /dev/null +++ b/fec/src/checkexp.c @@ -0,0 +1,806 @@ +#include "checkpri.h" + +FeNode *find_const_node(FeCheck *c, const char *name) +{ + FeNode *n; + for (n=c->ast->root ? c->ast->root->children : 0; n; n=n->next) + if (n->kind==FE_N_CONST && n->text && name && strcmp(n->text,name)==0) + return n; + return 0; +} + + +const char *builtin_format(FeCheckerState *s, FeNode *fmt) +{ + FeNode *decl; + FeSym *sym; + if (fmt && fmt->kind==FE_N_LITERAL && fmt->text && fmt->text[0]=='"') + return fmt->text; + if (fmt && fmt->kind==FE_N_IDENT) { + sym=find_symbol(s->scope,fmt->text); + decl=sym && sym->decl && sym->decl->kind==FE_N_CONST ? + sym->decl : find_const_node(s->c,fmt->text); + if (decl && decl->b && decl->b->kind==FE_N_LITERAL && + decl->b->text && decl->b->text[0]=='"') { + if (!decl->a || format_is_slice_u8(fe_type_from_ast(&s->c->types,decl->a))) + return decl->b->text; + } + } + return 0; +} + +int format_is_slice_u8(FeType *t) +{ + return t && t->kind==FE_TYPE_SLICE && t->elem && + t->elem->kind==FE_TYPE_INT && strcmp(t->elem->name,"u8")==0; +} + +int format_is_writer_type(FeType *t) +{ + return t && t->kind==FE_TYPE_STRUCT && + (strcmp(t->name,"Writer")==0 || strcmp(t->name,"io.Writer")==0); +} + +int format_arg_ok(FeType *t, int verb) +{ + if (!t) return 0; + if (verb=='x') return fe_type_is_integer(t); + if (verb=='c') return t->kind==FE_TYPE_CHAR; + if (verb=='s') return format_is_slice_u8(t); + if (verb=='b') return t->kind==FE_TYPE_BOOL; + if (t->kind==FE_TYPE_INT || t->kind==FE_TYPE_BOOL || + t->kind==FE_TYPE_CHAR) return 1; + return format_is_slice_u8(t) || + (t->kind==FE_TYPE_ENUM && t->is_error); +} + +void check_format_call(FeCheckerState *s, FeNode *n) +{ + const char *fmt; + FeNode *fmt_node; + FeNode *arg; + FeNode *x; + FeType *t; + unsigned long i,j; + unsigned count=0; + unsigned argc=0; + unsigned offset=0; + int verb; + int bad=0; + int counted=0; + if (strcmp(n->text,"@fprint")==0) offset=1; + fmt_node=n->children; + if (offset) { + if (!fmt_node) { err(s->c,n->loc,"@fprint requires a writer"); return; } + t=check_expr(s,fmt_node); + if (!format_is_writer_type(t)) + err(s->c,fmt_node->loc,"@fprint requires io.Writer"); + fmt_node=fmt_node->next; + } + if (strcmp(n->text,"@sprint")==0) { + if (!fmt_node) { err(s->c,n->loc,"@sprint requires a buffer"); return; } + t=check_expr(s,fmt_node); + if (!format_is_slice_u8(t) || !t->ref_mut) + err(s->c,fmt_node->loc,"@sprint requires []mut u8 buffer"); + fmt_node=fmt_node->next; + } + fmt=builtin_format(s,fmt_node); + if (!fmt) { err(s->c,n->loc,"format must be a comptime string"); return; } + n->aux_text=(char *)fmt; + arg=fmt_node ? fmt_node->next : 0; + for (x=arg;x;x=x->next) { check_expr(s,x); ++argc; } + i=1; + while (fmt[i] && fmt[i]!='"') { + if (fmt[i]=='\\') { if (fmt[i+1]) ++i; ++i; continue; } + if (fmt[i]=='{' && fmt[i+1]=='{') { i+=2; continue; } + if (fmt[i]=='}' && fmt[i+1]=='}') { i+=2; continue; } + if (fmt[i]=='{') { + j=i+1; + while (fmt[j] && fmt[j]!='}') ++j; + if (!fmt[j]) { err(s->c,n->loc,"unterminated format placeholder"); bad=1; break; } + if (j==i+1) verb=' '; else if (j==i+2) verb=(unsigned char)fmt[i+1]; else verb='?'; + if (verb!=' ' && verb!='x' && verb!='c' && verb!='s' && verb!='b') { + err(s->c,n->loc,"unsupported format verb"); bad=1; + } + if (!arg) { + err(s->c,n->loc,"format argument count mismatch"); + bad=1; counted=1; + } + else { + t=arg->sem_type; + if (verb==' ' && t && t->kind==FE_TYPE_ENUM && t->is_error) verb='s'; + if (!format_arg_ok(t,verb)) { err(s->c,arg->loc,"no fmt writer for argument type"); bad=1; } + arg=arg->next; + } + ++count; i=j+1; continue; + } + if (fmt[i]=='}') { err(s->c,n->loc,"unmatched '}' in format"); bad=1; } + ++i; + } + /* Running out of arguments mid-string already said this. Saying it again + once the whole string has been walked adds nothing. */ + if (count!=argc && !counted) { err(s->c,n->loc,"format argument count mismatch"); bad=1; } + (void)bad; +} + +int is_format_builtin(const char *name) +{ + return name && (strcmp(name,"@print")==0 || strcmp(name,"@fprint")==0 || + strcmp(name,"@sprint")==0); +} + +int lvalue_writable(FeCheckerState *s, FeNode *n) +{ + FeSym *sym; + FeType *t; + if (!n) return 0; + if (n->kind == FE_N_IDENT) { + sym=find_symbol(s->scope,n->text ? n->text : ""); + return sym ? sym->mutable : 0; + } + if (n->kind == FE_N_MEMBER) { + t=n->a ? n->a->sem_type : 0; + if (t && t->kind==FE_TYPE_REF && n->b && n->b->text && + strcmp(n->b->text,"^")==0) return t->ref_mut; + /* Through an owner, what may be written is decided by what is owned, + not by whether the binding may be pointed somewhere else. `let p: + ^[]mut T` fixes p and leaves what it owns writable. */ + if (t && t->kind==FE_TYPE_OWNED && n->b && n->b->text && + strcmp(n->b->text,"^")==0) + return !t->elem || t->elem->kind!=FE_TYPE_SLICE || t->elem->ref_mut; + return lvalue_writable(s,n->a); + } + if (n->kind == FE_N_INDEX) { + /* An index into a slice asks the slice, not the binding. */ + t=n->a ? n->a->sem_type : 0; + if (t && t->kind==FE_TYPE_SLICE) return t->ref_mut; + return lvalue_writable(s,n->a); + } + return 0; +} + +int has_field(FeNode *list, const char *name) +{ + FeNode *f; + for (f=list; f; f=f->next) + if (f->text && name && strcmp(f->text,name)==0) return 1; + return 0; +} + +/* A field of a type declared elsewhere is reachable only with `pub`. Inside + the declaring unit every field is reachable, `pub` or not. */ +int field_is_visible(FeCheckerState *s, const FeType *t, + const FeFieldType *field) +{ + if (!t || !t->unit) return 1; + if (s->c->types.unit_name && + strcmp(t->unit,s->c->types.unit_name)==0) return 1; + return field && field->ast_node && + (field->ast_node->flags & FE_NODE_PUB)!=0; +} + +/* The field list of a struct literal, once the type is known. Reached from + both `Type{...}` and `binding.Type{...}`. */ +FeType *check_struct_fields(FeCheckerState *s, FeNode *n, FeType *t) +{ + FeFieldType *field; + FeNode *f; + FeType *v; + unsigned i; + for(f=n->children;f;f=f->next) if(f->kind==FE_N_FIELD) { + if(has_field(f->next,f->text)) { err(s->c,f->loc,"duplicate struct field"); } + field=fe_type_field(t,f->text); + if(!field) { err(s->c,f->loc,"invalid struct field"); continue; } + if(!field_is_visible(s,t,field)) { + err(s->c,f->loc,"field is private to its unit"); + continue; + } + v=check_expr(s,f->a); + mark_moved(s,f->a,v); + if(!compatible(field->type,v,f->a) && v->kind!=FE_TYPE_UNKNOWN) err(s->c,f->loc,"struct field type mismatch"); + } + for(i=0;ifield_count;i++) if(!has_field(n->children,t->fields[i].name)) err(s->c,n->loc,"missing struct field"); + n->sem_type=t; return t; +} + +FeType *check_struct_init(FeCheckerState *s, FeNode *n) +{ + FeType *t; + FeFieldType *field; + FeNode *f; + FeType *v; + FeType *et; + FeVariantType *variant; + if (n->a && n->a->kind == FE_N_MEMBER) { + FeUnit *home=binding_unit(s,n->a->a); + if (home) { + /* `binding.Type{...}` names a type in another unit. */ + const char *want=n->a->b && n->a->b->text ? n->a->b->text : ""; + FeNode *decl=unit_type_decl(s->c,home,want); + t=unit_type(s->c,home,want); + if (!t || !decl) { err(s->c,n->a->loc,"unknown name"); return unknown(s->c); } + if (!decl_is_public(decl)) { + err(s->c,n->a->loc,"type is private to its unit"); + return unknown(s->c); + } + if (t->kind!=FE_TYPE_STRUCT) { + err(s->c,n->loc,"unknown struct type"); + return unknown(s->c); + } + return check_struct_fields(s,n,t); + } + et=check_expr(s,n->a->a); + variant=et && et->kind==FE_TYPE_ENUM ? + fe_type_variant(et,n->a->b ? n->a->b->text : "") : 0; + if (!variant) { err(s->c,n->loc,"invalid enum variant"); return unknown(s->c); } + if (variant->field_count != 0) { + for (f=n->children; f; f=f->next) { + if (f->kind != FE_N_FIELD) continue; + field=0; + if (variant->fields) { + unsigned i; + for(i=0;ifield_count;i++) if(strcmp(variant->fields[i].name,f->text)==0) field=&variant->fields[i]; + } + if (!field) { err(s->c,f->loc,"invalid enum payload field"); continue; } + v=check_expr(s,f->a); + mark_moved(s,f->a,v); + if (!compatible(field->type,v,f->a) && v->kind!=FE_TYPE_UNKNOWN) err(s->c,f->loc,"enum payload type mismatch"); + } + } else if (n->children) err(s->c,n->loc,"empty enum variant cannot have payload"); + n->sem_type=et; return et; + } + if (n->a && n->a->kind==FE_N_CALL) { + /* `Name(args){...}` -- the same spelling a type annotation uses, so + the same resolver answers it. */ + int ok=0; + t=type_from_expr(s,n->a,&ok); + if (!ok || !t || t->kind!=FE_TYPE_STRUCT) { + err(s->c,n->a->loc,"unknown struct type"); + return unknown(s->c); + } + return check_struct_fields(s,n,t); + } + t=fe_type_intern(&s->c->types,n->text ? n->text : ""); + if (!t || t->kind!=FE_TYPE_STRUCT) { err(s->c,n->loc,"unknown struct type"); return unknown(s->c); } + return check_struct_fields(s,n,t); +} + +FeType *check_array_init(FeCheckerState *s, FeNode *n) +{ + FeNode *x; FeType *elem=0; FeType *v; unsigned long count=0; + for(x=n->children;x;x=x->next) { v=check_expr(s,x); mark_moved(s,x,v); if(!elem) elem=v; else if(!compatible(elem,v,x)&&v->kind!=FE_TYPE_UNKNOWN) err(s->c,x->loc,"array element type mismatch"); ++count; } + if(!elem) elem=unknown(s->c); + n->sem_type=fe_type_array(&s->c->types,count,elem); return n->sem_type; +} + +int array_slice_lvalue(FeNode *n) +{ + return n && (n->kind==FE_N_IDENT || n->kind==FE_N_MEMBER || + n->kind==FE_N_INDEX); +} + +FeType *check_index(FeCheckerState *s, FeNode *n) +{ + FeType *base=check_expr(s,n->a); FeType *idx; FeType *elem; + if(!fe_type_is_indexable(base)) { err(s->c,n->loc,"indexing requires an array or slice"); return unknown(s->c); } + if(n->b) { idx=check_expr(s,n->b); if(known(idx)&&!fe_type_is_integer(idx)) err(s->c,n->loc,"index must be an integer"); } + if(n->c || !n->b) { + if (base->kind==FE_TYPE_ARRAY && !array_slice_lvalue(n->a)) + err(s->c,n->loc,"array slicing requires a stable lvalue"); + if(n->c) { + idx=check_expr(s,n->c); + if(known(idx)&&!fe_type_is_integer(idx)) + err(s->c,n->loc,"slice bound must be an integer"); + } + elem=base->elem; + n->sem_type=(base->kind==FE_TYPE_SLICE ? base->ref_mut : + lvalue_writable(s,n->a)) ? + fe_type_mut_slice(&s->c->types,elem) : + fe_type_slice(&s->c->types,elem); + return n->sem_type; + } + n->sem_type=base->elem; return n->sem_type; +} + +FeType *check_identifier(FeCheckerState *s, FeNode *n) +{ + FeSym *sym; + sym = find_symbol(s->scope, n->text ? n->text : ""); + if (!sym) { + FeType *named=fe_type_intern(&s->c->types,n->text ? n->text : ""); + if(named->kind==FE_TYPE_STRUCT || named->kind==FE_TYPE_ENUM) { n->sem_type=named; return named; } + if(named->kind!=FE_TYPE_UNKNOWN) { + err(s->c, n->loc, "a type is not a value here"); + return unknown(s->c); + } + err(s->c, n->loc, "unknown name"); + return unknown(s->c); + } + n->cname = sym->cname; + n->sem_type = sym->type; + if (!sym->fn) { + /* When this identifier is the base of a projection, the read reaches + one field and not the whole value. The chain above left word. */ + const char *field = s->proj_base==n ? s->proj_field : 0; + fe_own_access_field(s->c->diags,&sym->own,field,FE_OWN_READ,n->loc); + sym->moved=sym->own.move; + } + return sym->type; +} + +FeType *check_expr_core(FeCheckerState *s, FeNode *n) +{ + FeCheck *c = s->c; + FeType *a; + FeType *b; + FeSym *sym; + FeNode *x; + FeNode *param; + FeNode *arg; + FeType *et; + FeFieldType *field; + FeVariantType *variant; + const char *op; + if (!n) return unknown(c); + if (n->kind == FE_N_IDENT) + return check_identifier(s, n); + if (n->kind == FE_N_LITERAL) { + if (!n->text) return unknown(c); + if (strcmp(n->text, "true") == 0 || strcmp(n->text, "false") == 0) + a = fe_type_intern(&c->types, "bool"); + else if (n->text[0] == '\'') + a = fe_type_intern(&c->types, "char"); + else if (n->text[0] == '"') + a = fe_type_intern(&c->types, "str"); + else + a = fe_type_intern(&c->types, "i32"); + n->sem_type = a; + return a; + } + if (n->kind == FE_N_STRUCT_INIT) return check_struct_init(s,n); + if (n->kind == FE_N_ARRAY_INIT) return check_array_init(s,n); + if (n->kind == FE_N_INDEX) return check_index(s,n); + if (n->kind == FE_N_MATCH) { check_match(s,n); n->sem_type=unknown(c); return n->sem_type; } + if (n->kind == FE_N_UNARY) { + a = check_expr(s, n->a); + op = n->text ? n->text : ""; + if (strcmp(op, "not") == 0) { + if (known(a) && a->kind != FE_TYPE_BOOL) + err(c, n->loc, "'not' requires bool"); + a = fe_type_intern(&c->types, "bool"); + } else if (strcmp(op, "-") == 0) { + if (known(a) && !fe_type_is_integer(a)) + err(c, n->loc, "unary '-' requires integer"); + } else if (strcmp(op, "~") == 0) { + /* Flipping every bit only means something where the bits are the + value (SPEC 6.2). */ + if (known(a) && !fe_type_is_integer(a)) + err(c, n->loc, "unary '~' requires integer"); + } else if (strcmp(op, "try") == 0) { + /* SPEC 6.4: try is only allowed inside a function returning an error + union. Checked on the expression rather than on the statement so + that it also covers `var x = try e;` and `x = try e;`, which the + statement-level check walked straight past. */ + if (!s->ret || s->ret->kind != FE_TYPE_ERROR_UNION) + err(c,n->loc,"try requires an enclosing error result"); + if (a && a->kind==FE_TYPE_ERROR_UNION) + a=a->error_value; + else { + err(c,n->loc,"try requires an error result"); + a=unknown(c); + } + } else if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) { + if (strcmp(op,"&mut")==0 && a && a->kind==FE_TYPE_REF && !a->ref_mut) + err(c,n->loc,"cannot create mutable borrow from a shared reference"); + own_borrow_expr(s,n->a,strcmp(op,"&mut")==0); + a=fe_type_ref(&c->types,a,strcmp(op,"&mut")==0); + } + n->sem_type = a; + return a; + } + if (n->kind == FE_N_TYPE && n->text && strcmp(n->text, "as") == 0) { + a = check_expr(s, n->a); + b = node_type(c, n->b); + if (b->kind == FE_TYPE_VOID) + err(c, n->loc, "cast target cannot be void"); + else if (known(a) && known(b) && !explicit_castable(a,b)) + err(c, n->loc, "'as' requires integer or char types"); + n->sem_type = b; + return b; + } + if (n->kind == FE_N_BINARY) { + a = check_expr(s, n->a); + b = check_expr(s, n->b); + op = n->text ? n->text : ""; + if (strcmp(op, "and") == 0 || strcmp(op, "or") == 0) { + if ((known(a) && a->kind != FE_TYPE_BOOL) || + (known(b) && b->kind != FE_TYPE_BOOL)) + err(c, n->loc, "logical operator requires bool operands"); + a = fe_type_intern(&c->types, "bool"); + } else if (strcmp(op, "==") == 0 || strcmp(op, "!=") == 0 || + strcmp(op, "<") == 0 || strcmp(op, "<=") == 0 || + strcmp(op, ">") == 0 || strcmp(op, ">=") == 0) { + if (known(a) && known(b) && !fe_type_equal(a, b) && + !compatible(a, b, n->b) && !compatible(b, a, n->a)) + err(c, n->loc, "comparison operands have different types"); + else if (strcmp(op,"==")!=0 && strcmp(op,"!=")!=0 && + ((known(a) && !ordered_type(a)) || + (known(b) && !ordered_type(b)))) + err(c, n->loc, "ordering requires integer or char operands"); + a = fe_type_intern(&c->types, "bool"); + } else { + if ((known(a) && !fe_type_is_integer(a)) || + (known(b) && !fe_type_is_integer(b)) || + (known(a) && known(b) && !fe_type_equal(a, b) && + !compatible(a, b, n->b) && !compatible(b, a, n->a))) + err(c, n->loc, + "arithmetic operands must have the same integer type"); + } + n->sem_type = a; + return a; + } + if (n->kind == FE_N_CALL) { + if (n->a && n->a->kind==FE_N_MEMBER && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"drop")==0) { + err(c,n->loc,"drop may only be invoked by scope cleanup"); + return unknown(c); + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text) { + FeNode *arg=n->children; + if (strcmp(n->a->b->text,"destroy")==0) { + a=arg ? check_expr(s,arg) : unknown(c); + if (!arg || arg->next || !a || a->kind!=FE_TYPE_OWNED) + err(c,n->loc,"mem.destroy requires exactly one owned pointer"); + else + mark_moved(s,arg,a); + n->sem_type=fe_type_intern(&c->types,"void"); + return n->sem_type; + } + if (strcmp(n->a->b->text,"create")==0) { + if (!arg || arg->next) + err(c,n->loc,"mem.create requires exactly one value"); + a=arg ? check_expr(s,arg) : unknown(c); + if(arg) mark_moved(s,arg,a); + a=fe_type_owned(&c->types,a); + n->sem_type=fe_type_error_union(&c->types,a); + return n->sem_type; + } + if (strcmp(n->a->b->text,"alloc_slice")==0) { + FeNode *count=arg ? arg->next : 0; + FeType *item; + { + /* The element type may be an instance -- `Slot(V)` -- and + not just a name. */ + int named=0; + item=arg ? type_from_expr(s,arg,&named) : unknown(c); + if(!arg || !named || !count || count->next) { + err(c,n->loc,"mem.alloc_slice requires a type and length"); + item=unknown(c); + } + } + b=count ? check_expr(s,count) : unknown(c); + if(known(b) && !fe_type_is_integer(b)) + err(c,count->loc,"slice length must be an integer"); + /* Freshly allocated storage is owned outright, so it is + writable: there is nobody else to disturb. */ + a=fe_type_owned(&c->types,fe_type_mut_slice(&c->types,item)); + n->sem_type=fe_type_error_union(&c->types,a); + return n->sem_type; + } + if (strcmp(n->a->b->text,"replace")==0) { + FeNode *value=arg ? arg->next : 0; + if(!arg || !value || value->next) + err(c,n->loc,"mem.replace requires destination and value"); + a=arg ? check_expr(s,arg) : unknown(c); + if(!a || a->kind!=FE_TYPE_REF || !a->ref_mut || + !arg->a || !lvalue_writable(s,arg->a)) + err(c,n->loc,"mem.replace destination must be a mutable place"); + b=value ? check_expr(s,value) : unknown(c); + if(a && a->kind==FE_TYPE_REF && !compatible(a->elem,b,value)) + err(c,value->loc,"mem.replace value type mismatch"); + if(value) mark_moved(s,value,b); + /* The destination is lent for the length of the call, the same + as any other argument. Without this the borrow stays live to + the end of the function and the place can never be read. */ + own_release_temporary_borrow(s,arg); + n->sem_type=a && a->kind==FE_TYPE_REF ? a->elem : unknown(c); + fe_type_require_replace(&c->types,n->sem_type); + return n->sem_type; + } + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"null_writer")==0) { + FeNode *arg=n->children; + if (arg) err(c,n->loc,"io.null_writer takes no arguments"); + n->sem_type=fe_type_intern(&c->types,"io.Writer"); + return n->sem_type; + } + if (n->text && is_format_builtin(n->text)) { + check_format_call(s,n); + if (strcmp(n->text,"@print")==0) + n->sem_type=fe_type_intern(&c->types,"void"); + else if (strcmp(n->text,"@sprint")==0) + n->sem_type=fe_type_intern(&c->types,"usize"); + else + n->sem_type=fe_type_error_union(&c->types,fe_type_intern(&c->types,"void")); + return n->sem_type; + } + if (!n->a && n->text && (strcmp(n->text,"@size_of")==0 || strcmp(n->text,"@align_of")==0)) { + FeNode *type_arg=n->children; + FeType *target=type_arg && type_arg->kind==FE_N_IDENT ? fe_type_intern(&c->types,type_arg->text) : unknown(c); + if(!target || !known(target)) err(c,n->loc,"size/align requires a known type"); + n->sem_type=fe_type_intern(&c->types,"usize"); return n->sem_type; + } + if (!n->a && n->text && strcmp(n->text,"@ptr_cast")==0) { + /* `@ptr_cast(T, p)`: the first argument names the type the result + points at, the second is the address. R9 keeps it in `unsafe`. */ + FeNode *type_arg=n->children; + FeNode *value=type_arg ? type_arg->next : 0; + FeType *target=type_arg && type_arg->kind==FE_N_IDENT ? + fe_type_intern(&c->types,type_arg->text) : unknown(c); + if (!type_arg || !value || value->next) + err(c,n->loc,"@ptr_cast requires a type and a pointer"); + if (value) check_expr(s,value); + n->sem_type=fe_type_raw(&c->types,target); + return n->sem_type; + } + if (n->a && n->a->kind == FE_N_MEMBER) { + FeNode *method; + FeNode *self_param; + FeUnit *home=binding_unit(s,n->a->a); + if (home) { + const char *want=n->a->b && n->a->b->text ? n->a->b->text : ""; + FeSym *fsym=unit_member(c,home,want); + if (!fsym) { + err(c,n->a->loc,"unknown name"); + for (x=n->children;x;x=x->next) check_expr(s,x); + return unknown(c); + } + if (!decl_is_public(fsym->decl)) { + err(c,n->a->loc,"name is private to its unit"); + for (x=n->children;x;x=x->next) check_expr(s,x); + return unknown(c); + } + if (decl_is_generic(fsym->fn)) + return check_generic_call(s,n,fsym,home); + return check_call_args(s,n,fsym,home->name,0); + } + { + int names_type=0; + FeType *owner_type=type_from_expr(s,n->a->a,&names_type); + if (names_type && owner_type && + owner_type->kind==FE_TYPE_STRUCT) { + FeNode *m=type_method(owner_type, + n->a->b ? n->a->b->text : ""); + if (!m) { err(c,n->a->loc,"unknown method"); return unknown(c); } + if (!method_is_static(m)) { + err(c,n->loc,"method requires a receiver"); + return unknown(c); + } + return check_static_method_call(s,n,owner_type,m); + } + } + et=check_expr(s,n->a->a); + /* A method can be reached through a reference or an owner as well + as through the value itself. */ + if (et && (et->kind==FE_TYPE_REF || et->kind==FE_TYPE_OWNED) && + et->elem && et->elem->kind==FE_TYPE_STRUCT && + find_method(c,et->elem,n->a->b ? n->a->b->text : "")) + et=et->elem; + method=et && et->kind==FE_TYPE_STRUCT ? + find_method(c,et,n->a->b ? n->a->b->text : "") : 0; + if(method) { + FeBindSave msave; + int bound=0; + self_param=method->a ? method->a->children : 0; + if(!self_param) { + err(c,n->loc,"method requires self parameter"); + return unknown(c); + } + /* A method of a generic instance reads its signature with that + instance's arguments bound. */ + if (et->bind_count) { + push_instance_bindings(c,&msave,et); + bind_self(c,et); + bound=1; + } + a=method_type(c,self_param->a,et); + if(a->kind==FE_TYPE_REF && a->ref_mut && + !lvalue_writable(s,n->a->a)) + err(c,n->loc,"mutable method requires a mutable receiver"); + if(a->kind!=FE_TYPE_REF) mark_moved(s,n->a->a,et); + param=self_param->next; + arg=n->children; + while(param && arg) { + a=check_expr(s,arg); + b=method_type(c,param->a,et); + /* A method argument gets the same call-only weakening a + free function's does: an exclusive view may be handed + over as a shared one for the length of the call, and an + exclusive borrow is lent rather than given. */ + if(!compatible(b,a,arg) && + !(b && a && b->kind==FE_TYPE_SLICE && + a->kind==FE_TYPE_SLICE && !b->ref_mut && a->ref_mut && + fe_type_equal(b->elem,a->elem)) && + !(b && a && b->kind==FE_TYPE_REF && a->kind==FE_TYPE_REF && + !b->ref_mut && a->ref_mut && + fe_type_equal(b->elem,a->elem)) && + a->kind!=FE_TYPE_UNKNOWN) + err(c,arg->loc,"method argument type mismatch"); + if(!call_reborrows(b,a) && + !(b && a && b->kind==FE_TYPE_SLICE && + a->kind==FE_TYPE_SLICE && !b->ref_mut && a->ref_mut)) + mark_moved(s,arg,a); + param=param->next; + arg=arg->next; + } + if(param || arg) err(c,n->loc,"wrong number of method arguments"); + n->sem_decl=method; + n->sem_type=method->b ? method_type(c,method->b,et) : + fe_type_intern(&c->types,"void"); + if (bound) { + pop_bindings(c,&msave); + check_instance_method(s,et,method,n->loc,n); + } + return n->sem_type; + } + if (et && (et->kind==FE_TYPE_SLICE || et->kind==FE_TYPE_STR) && + n->a->b && n->a->b->text && + strcmp(n->a->b->text,"trim")==0) { + if (n->children) err(c,n->loc,"trim takes no arguments"); + n->sem_type=fe_type_slice(&c->types,et->elem); + return n->sem_type; + } + variant=et && et->kind==FE_TYPE_ENUM ? + fe_type_variant(et,n->a->b ? n->a->b->text : "") : 0; + arg=n->children; + if (!variant) { err(c,n->loc,"invalid enum variant constructor"); return unknown(c); } + if (variant->field_count==1 && arg) { + FeType *av=check_expr(s,arg); + if(!compatible(variant->fields[0].type,av,arg)&&av->kind!=FE_TYPE_UNKNOWN) err(c,arg->loc,"enum payload type mismatch"); + } else if (variant->field_count != 0 || arg) err(c,n->loc,"wrong enum payload arity"); + n->sem_type=et; return et; + } + if (n->a && n->a->kind == FE_N_IDENT) { + sym = find_symbol(s->scope, n->a->text ? n->a->text : ""); + if (!sym) { + err(c, n->loc, "unknown function"); + return unknown(c); + } + if (decl_is_generic(sym->fn)) + return check_generic_call(s,n,sym,current_unit(c)); + return check_call_args(s, n, sym, 0, 0); + } + for (x = n->children; x; x = x->next) check_expr(s, x); + return unknown(c); + } + if (n->kind == FE_N_MEMBER) { + if (is_error_set_member(s,n)) { + n->sem_type=fe_type_intern(&c->types,"core.Error"); + return n->sem_type; + } + if (n->a && n->a->kind==FE_N_IDENT && n->a->text && + strcmp(n->a->text,"io")==0 && n->b && n->b->text && + (strcmp(n->b->text,"stdout")==0 || + strcmp(n->b->text,"stderr")==0)) { + n->sem_type=fe_type_intern(&c->types,"io.Writer"); + return n->sem_type; + } + a=check_expr(s,n->a); + if (a->kind == FE_TYPE_REF && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + n->sem_type=a->elem; + return a->elem; + } + if(a->kind==FE_TYPE_REF && a->elem && + a->elem->kind==FE_TYPE_STRUCT) { + field=fe_type_field(a->elem,n->b ? n->b->text : ""); + if(!field) { err(c,n->loc,"unknown struct field"); return unknown(c); } + n->sem_type=field->type; + return field->type; + } + if (a->kind == FE_TYPE_OWNED && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + n->sem_type=a->elem; + return a->elem; + } + if(a->kind==FE_TYPE_STRUCT) { + field=fe_type_field(a,n->b ? n->b->text : ""); + if(!field) { err(c,n->loc,"unknown struct field"); return unknown(c); } + n->sem_type=field->type; return field->type; + } + if(a->kind==FE_TYPE_ENUM) { + if(!fe_type_variant(a,n->b ? n->b->text : "")) err(c,n->loc,"unknown enum variant"); + n->sem_type=a; return a; + } + if((a->kind==FE_TYPE_SLICE || a->kind==FE_TYPE_STR) && n->b && + strcmp(n->b->text,"n")==0) { + n->sem_type=fe_type_intern(&c->types,"usize"); return n->sem_type; + } + return unknown(c); + } + return unknown(c); +} + +/* `base_in` is the already-checked type of a member expression's base. The M7 + lvalue path looks at that base before delegating here, and checking it a + second time reports any ownership violation on it a second time too. */ +FeType *check_lvalue_core(FeCheckerState *s, FeNode *n, int read, + FeType *base_in) +{ + FeSym *sym; + FeType *base; + FeFieldType *field; + if (n && n->kind == FE_N_IDENT) { + sym = find_symbol(s->scope, n->text ? n->text : ""); + if (!sym) { + err(s->c, n->loc, "unknown name"); + return unknown(s->c); + } + if (sym->fn) { + err(s->c, n->loc, "function is not assignable"); + return unknown(s->c); + } + if (!sym->mutable) + err(s->c, n->loc, "cannot assign to immutable let"); + n->cname = sym->cname; + n->sem_type = sym->type; + if (read) { + fe_own_access(s->c->diags,&sym->own,FE_OWN_READ,n->loc); + sym->moved=sym->own.move; + } + return sym->type; + } + if (n && n->kind == FE_N_MEMBER) { + base=base_in ? base_in : check_expr(s,n->a); + if (base && base->kind == FE_TYPE_REF && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + if (!base->ref_mut) + err(s->c,n->loc,"cannot write through shared reference"); + n->sem_type=base->elem; + return base->elem; + } + if(base && base->kind==FE_TYPE_REF && base->elem && + base->elem->kind==FE_TYPE_STRUCT) { + if(!base->ref_mut) + err(s->c,n->loc,"cannot write through shared reference"); + field=fe_type_field(base->elem,n->b ? n->b->text : ""); + if(!field) { err(s->c,n->loc,"assignment requires a valid struct field"); return unknown(s->c); } + n->sem_type=field->type; + return field->type; + } + if (base && base->kind == FE_TYPE_OWNED && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + n->sem_type=base->elem; + return base->elem; + } + if (!lvalue_writable(s,n->a)) + err(s->c,n->loc,"cannot assign through immutable value"); + field=base && base->kind==FE_TYPE_STRUCT ? fe_type_field(base,n->b ? n->b->text : "") : 0; + if(!field) { err(s->c,n->loc,"assignment requires a valid struct field"); return unknown(s->c); } + n->sem_type=field->type; return field->type; + } + if (n && n->kind == FE_N_INDEX) { + base=check_index(s,n); + if (n->a && n->a->sem_type && + n->a->sem_type->kind == FE_TYPE_SLICE && + !n->a->sem_type->ref_mut) + err(s->c,n->loc,"cannot write through shared slice"); + else if (n->a && n->a->sem_type && + n->a->sem_type->kind != FE_TYPE_SLICE && + !lvalue_writable(s,n->a)) + err(s->c,n->loc,"cannot assign through immutable value"); + return base; + } + if (n) err(s->c, n->loc, "assignment requires a variable"); + return unknown(s->c); +} + +int compound_operator(const char *op) +{ + return op && strcmp(op, "=") != 0; +} diff --git a/fec/src/checkgen.c b/fec/src/checkgen.c new file mode 100644 index 0000000..37522fe --- /dev/null +++ b/fec/src/checkgen.c @@ -0,0 +1,679 @@ +#include "checkpri.h" + +unsigned decl_type_param_count(const FeNode *decl) +{ + FeNode *p; + unsigned n=0; + if (!decl) return 0; + if (decl->kind==FE_N_FN) { + for (p=decl->a?decl->a->children:0;p;p=p->next) + if (p->flags & FE_NODE_COMPTIME) ++n; + return n; + } + if (decl->kind==FE_N_STRUCT || decl->kind==FE_N_ENUM) + for (p=decl->a?decl->a->children:0;p;p=p->next) ++n; + return n; +} + +FeNode *decl_type_param(const FeNode *decl, unsigned i) +{ + FeNode *p; + unsigned n=0; + if (!decl) return 0; + if (decl->kind==FE_N_FN) { + for (p=decl->a?decl->a->children:0;p;p=p->next) + if (p->flags & FE_NODE_COMPTIME) { if (n==i) return p; ++n; } + return 0; + } + for (p=decl->a?decl->a->children:0;p;p=p->next) { if (n==i) return p; ++n; } + return 0; +} + +int decl_is_generic(const FeNode *decl) +{ + return decl_type_param_count(decl)!=0; +} + +/* SPEC 9: v0.1 has comptime type parameters and no other kind. */ +void check_generic_params(FeCheck *c, FeNode *decl) +{ + FeNode *p; + if (!decl || decl->kind!=FE_N_FN) return; + for (p=decl->a?decl->a->children:0;p;p=p->next) { + if (!(p->flags & FE_NODE_COMPTIME)) continue; + if (!p->a || p->a->kind!=FE_N_TYPE || !p->a->text || + strcmp(p->a->text,"type")!=0) + err(c,p->loc,"a comptime parameter must be a type parameter"); + } +} + +void push_bindings(FeCheck *c, FeBindSave *save, FeNode *decl, + FeType **args, unsigned count) +{ + unsigned i; + save->count=c->types.param_count; + for (i=0;iparams[i]=c->types.params[i]; + c->types.param_count=0; + for (i=0;itypes.params[i].name=p && p->text ? p->text : "?"; + c->types.params[i].type=args[i]; + ++c->types.param_count; + } +} + +/* Restore the bindings recorded on an instance, so a method sees exactly the + environment its type was built with. */ +void push_instance_bindings(FeCheck *c, FeBindSave *save, FeType *t) +{ + unsigned i; + save->count=c->types.param_count; + for (i=0;iparams[i]=c->types.params[i]; + c->types.param_count=0; + for (i=0;ibind_count && itypes.params[c->types.param_count++]=t->binds[i]; +} + +void bind_self(FeCheck *c, FeType *owner) +{ + if (c->types.param_count>=FE_TYPE_PARAM_MAX) return; + c->types.params[c->types.param_count].name="Self"; + c->types.params[c->types.param_count].type=owner; + ++c->types.param_count; +} + +void pop_bindings(FeCheck *c, const FeBindSave *save) +{ + unsigned i; + for (i=0;itypes.params[i]=save->params[i]; + c->types.param_count=save->count; +} + +/* `unit.Name(arg,arg)` -- the canonical identity of one instance. + Nesting makes the readable spelling grow without bound, and a spelling that + got cut off would make two different instances look like the same one, so + past a length the arguments are written as serial numbers instead. Those are + unique, so identity stays exact even where the spelling stops being + readable. */ + +void instance_key(char *out, const char *unit, const char *name, + FeType **args, unsigned count) +{ + unsigned i; + unsigned long n=0; + unsigned long cap=(unsigned long)FE_GENERIC_NAME_READABLE; + const char *p; + char number[24]; + int readable=1; + for (p=unit?unit:"";*p;++p) { if (nname[0] ? args[i]->name : "?";*p;++p) { + if (nserial : 0U); + for (p=number;*p && ninstance_count;++i) + if (!strcmp(c->instances[i].key,key)) return c->instances[i].cname; + return 0; +} + +int instance_known(FeCheck *c, const char *key) +{ + unsigned i; + for (i=0;iinstance_count;++i) + if (strcmp(c->instances[i].key,key)==0) return 1; + return 0; +} + +int instance_record(FeCheck *c, const char *key, FeLoc loc, + FeNode *decl, FeUnit *home, FeType *owner) +{ + FeInstance *inst; + unsigned i; + if (instance_known(c,key)) return 0; + if (c->instance_count>=FE_GENERIC_INSTANCE_MAX) { + err(c,loc,"too many generic instances"); + return -1; + } + inst=&c->instances[c->instance_count]; + strcpy(inst->key,key); + inst->decl=decl; + inst->home=home ? home->name : 0; + inst->owner=owner; + inst->cname=unit_cname(c,key); + /* The bindings in force right now are the ones this instance was built + with, and lowering has to see exactly those again. */ + inst->bind_count=c->types.param_count; + for (i=0;itypes.param_count && ibinds[i]=c->types.params[i]; + ++c->instance_count; + return 1; +} + +/* One step further down a chain of instantiations. Chains that keep producing + new instances are the ones that never end, so the limit counts nesting. */ +int instance_descend(FeCheck *c, FeLoc loc) +{ + if (c->instance_depth>=FE_GENERIC_DEPTH_MAX) { + err(c,loc,"generic instantiation depth exceeded"); + return 0; + } + ++c->instance_depth; + return 1; +} + +FeUnit *current_unit(FeCheck *c) +{ + unsigned u; + for (u=0;ubuild->count;++u) + if (strcmp(c->build->units[u].name,c->types.unit_name)==0) + return &c->build->units[u]; + return c->unit; +} + +/* Build `Box(i32)`: the declaration's fields with the parameters bound, under + a name that records which arguments made it. */ +FeType *build_struct_instance(FeCheck *c, FeUnit *home, FeNode *decl, + const char *key, FeType **args, + unsigned count) +{ + FeBindSave save; + FeType *t; + FeNode *f; + unsigned fields=0; + unsigned i=0; + t=fe_type_intern_unit(&c->types,home->name,key); + if (!t || t->kind!=FE_TYPE_UNKNOWN) return t; + t->kind=FE_TYPE_STRUCT; + t->building=1; + t->packed=(decl->flags & FE_NODE_PACKED)!=0; + t->decl_node=decl; + t->bind_count=0; + for (i=0;ibinds[t->bind_count].name=p && p->text ? p->text : "?"; + t->binds[t->bind_count].type=args[i]; + ++t->bind_count; + } + t->cname=unit_cname(c,key); + for (f=decl->children;f;f=f->next) + if (f->kind==FE_N_FN && f->text && strcmp(f->text,"drop")==0) + t->has_drop=1; + for (f=decl->children;f;f=f->next) if (f->kind==FE_N_FIELD) ++fields; + t->field_count=fields; + if (fields) { + const char *save_unit=c->types.unit_name; + t->fields=(FeFieldType *)fe_arena_alloc(&c->arena, + fields*sizeof(FeFieldType)); + if (!t->fields) { t->field_count=0; return t; } + /* Field types are written in the unit that declared the struct, not in + whichever unit asked for this instance. */ + c->types.unit_name=home->name; + push_instance_bindings(c,&save,t); + bind_self(c,t); + i=0; + for (f=decl->children;f;f=f->next) if (f->kind==FE_N_FIELD) { + t->fields[i].name=f->text; + t->fields[i].type=node_type(c,f->a); + t->fields[i].offset=0; + t->fields[i].ast_node=f; + ++i; + } + pop_bindings(c,&save); + c->types.unit_name=save_unit; + } + t->building=0; + fe_type_layout_all(&c->types); + /* A type that says how to let go of itself needs that method to exist for + every instance, whether or not anyone calls it by name: scope cleanup + will. */ + { + FeNode *release; + for (release=decl->children;release;release=release->next) + if (release->kind==FE_N_FN && release->text && + !strcmp(release->text,"drop") && release->c) { + FeCheckerState s; + memset(&s,0,sizeof s); + s.c=c; + s.scope=c->unit_scope[unit_index(c,home)]; + s.globals=s.scope; + check_instance_method(&s,t,release,decl->loc,0); + break; + } + } + return t; +} + +FeType *instantiate_struct(FeCheck *c, FeUnit *home, const char *name, + FeType **args, unsigned count, FeLoc loc) +{ + FeNode *decl=unit_type_decl(c,home,name); + char key[FE_GENERIC_KEY_MAX]; + if (!decl || !decl_is_generic(decl)) { + err(c,loc,"type does not take generic arguments"); + return unknown(c); + } + if (decl->kind!=FE_N_STRUCT) { + err(c,loc,"only a generic struct can be instantiated"); + return unknown(c); + } + if (count!=decl_type_param_count(decl)) { + err(c,loc,"wrong number of generic arguments"); + return unknown(c); + } + instance_key(key,home->name,name,args,count); + if (instance_record(c,key,loc,decl,home,0)<0) return unknown(c); + return build_struct_instance(c,home,decl,key,args,count); +} + +/* `Name(args...)` written in type position. */ +FeType *instantiate_type_node(void *owner, const FeNode *node) +{ + FeCheck *c=(FeCheck *)owner; + FeUnit *home=current_unit(c); + const char *name=node->text; + FeNode *arg; + FeType *args[FE_TYPE_PARAM_MAX]; + unsigned count=0; + FeType *result; + /* `binding.Name` names a type in another unit. The binding is not itself a + type, so it has to be peeled off before anything is looked up. */ + if (node->a && node->a->kind==FE_N_IDENT && node->a->text && c->build && + c->unit) { + FeUnit *bound=fe_build_binding(c->build,c->unit,node->text); + if (bound) { home=bound; name=node->a->text; } + } + if (!node->children) { + FeNode *decl=unit_type_decl(c,home,name ? name : ""); + if (decl && decl_is_generic(decl)) { + /* A generic declaration is not a type until it has arguments. */ + err(c,node->loc,"generic type requires type arguments"); + return unknown(c); + } + if (name!=node->text) { + FeType *there=unit_type(c,home,name); + if (there) return there; + } + return fe_type_intern(&c->types,name); + } + if (!instance_descend(c,node->loc)) return unknown(c); + for (arg=node->children;arg;arg=arg->next) { + if (counttypes,arg); + ++count; + } + if (count>FE_TYPE_PARAM_MAX) { + err(c,node->loc,"wrong number of generic arguments"); + --c->instance_depth; + return unknown(c); + } + result=instantiate_struct(c,home,name ? name : "",args,count, + node->loc); + --c->instance_depth; + return result; +} + +/* A type written where an expression is: `i32`, `Box(i32)`. Only a comptime + argument position accepts one. */ +FeType *type_from_expr(FeCheckerState *s, FeNode *n, int *ok) +{ + FeCheck *c=s->c; + FeType *t; + unsigned i; + *ok=0; + if (!n) return unknown(c); + if (n->kind==FE_N_IDENT && n->text) { + for (i=0;itypes.param_count;++i) + if (strcmp(c->types.params[i].name,n->text)==0) { + *ok=1; + return c->types.params[i].type; + } + if (find_symbol(s->scope,n->text)) { + /* A const alias of a type is that type (SPEC 4.7). */ + FeSym *sym=find_symbol(s->scope,n->text); + if (sym && sym->decl && sym->decl->kind==FE_N_CONST && + sym->decl->b && sym->decl->b->kind==FE_N_IDENT) + return type_from_expr(s,sym->decl->b,ok); + return unknown(c); + } + t=fe_type_intern(&c->types,n->text); + if (t && t->kind!=FE_TYPE_UNKNOWN) { *ok=1; return t; } + return unknown(c); + } + /* `binding.Name` names a type in another unit. */ + if (n->kind==FE_N_MEMBER && n->a && n->a->kind==FE_N_IDENT && + n->b && n->b->text) { + FeUnit *bound=binding_unit(s,n->a); + if (bound) { + FeType *there=unit_type(c,bound,n->b->text); + if (there) { *ok=1; return there; } + } + return unknown(c); + } + if (n->kind==FE_N_CALL && n->a && + (n->a->kind==FE_N_IDENT || + (n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->b && n->a->b->text))) { + FeType *args[FE_TYPE_PARAM_MAX]; + unsigned count=0; + FeNode *arg; + FeType *result; + FeUnit *home=current_unit(c); + const char *want; + /* `Name(args)` here, `binding.Name(args)` when the declaration is in + another unit. */ + if (n->a->kind==FE_N_MEMBER) { + FeUnit *bound=binding_unit(s,n->a->a); + if (!bound) return unknown(c); + home=bound; + want=n->a->b->text; + } else { + want=n->a->text; + } + if (!want || !unit_type_decl(c,home,want)) return unknown(c); + if (!instance_descend(c,n->loc)) { *ok=1; return unknown(c); } + for (arg=n->children;arg;arg=arg->next) { + int inner=0; + if (countinstance_depth; return unknown(c); } + ++count; + } + if (count>FE_TYPE_PARAM_MAX) { --c->instance_depth; return unknown(c); } + result=instantiate_struct(c,home,want,args,count,n->loc); + --c->instance_depth; + *ok=1; + return result; + } + return unknown(c); +} + +/* Can this initializer be worked out before the program runs? + + A global's bytes go into the image, so there is no moment at which a call in + its initializer could happen -- the emitter had been quietly dropping the + work and leaving zeros. Anything that is a name for a value already known is + fine; anything that is work is not. */ +int const_foldable(FeCheckerState *s, FeNode *n) +{ + FeNode *x; + if (!n) return 1; + switch (n->kind) { + case FE_N_LITERAL: + return 1; + case FE_N_IDENT: { + /* Another `const` is a name for a value; a `static`/`var` is storage + that does not exist yet. */ + FeSym *sym=find_symbol(s->scope,n->text ? n->text : ""); + return sym && sym->decl && sym->decl->kind==FE_N_CONST; + } + case FE_N_MEMBER: + /* `E.Variant`, `error.Name`, `unit.CONST` -- a name, not work. */ + if (n->a && n->a->kind==FE_N_IDENT) return 1; + return const_foldable(s,n->a); + case FE_N_UNARY: + if (n->text && strcmp(n->text,"try")==0) return 0; + return const_foldable(s,n->a); + case FE_N_BINARY: + if (n->text && (strcmp(n->text,"catch")==0 || + strcmp(n->text,"orelse")==0)) return 0; + return const_foldable(s,n->a) && const_foldable(s,n->b); + case FE_N_TYPE: + return const_foldable(s,n->a); + case FE_N_EXPR: + return const_foldable(s,n->a); + case FE_N_STRUCT_INIT: + case FE_N_ARRAY_INIT: + for (x=n->children;x;x=x->next) + if (!const_foldable(s,x->kind==FE_N_FIELD ? x->a : x)) return 0; + return 1; + default: + return 0; + } +} + +/* A `comptime if` condition. Only the forms SPEC 9 allows: type equality and + the type predicates. Anything else is not decidable here. */ +int comptime_condition(FeCheckerState *s, FeNode *n, int *out) +{ + FeType *a; + FeType *b; + int ok=0; + int eq; + if (!n) return 0; + if (n->kind==FE_N_BINARY && n->text && + (strcmp(n->text,"==")==0 || strcmp(n->text,"!=")==0)) { + a=type_from_expr(s,n->a,&ok); + if (!ok) return 0; + b=type_from_expr(s,n->b,&ok); + if (!ok) return 0; + eq=fe_type_equal(a,b); + *out=strcmp(n->text,"==")==0 ? eq : !eq; + return 1; + } + if (n->kind==FE_N_CALL && n->text && + (strcmp(n->text,"@is_int")==0 || strcmp(n->text,"@is_ptr")==0)) { + a=type_from_expr(s,n->children,&ok); + if (!ok) return 0; + *out=strcmp(n->text,"@is_int")==0 ? fe_type_is_integer(a) : + (a && (a->kind==FE_TYPE_OWNED || a->kind==FE_TYPE_REF)); + return 1; + } + return 0; +} + +/* Check a generic body once, in the unit that declared it and with the + instance's arguments bound. Errors land on the operation that is wrong; the + call site gets a note, because the call is context and not the defect. */ +void instantiate_body(FeCheck *c, FeUnit *home, FeNode *decl, + FeType *owner, FeBindSave *bindings, FeLoc site) +{ + FeAst *save_ast=c->ast; + FeUnit *save_unit=c->unit; + const char *save_name=c->types.unit_name; + unsigned before=c->diags->errors; + (void)bindings; + c->ast=&home->ast; + c->unit=home; + c->types.unit_name=home->name; + fe_diags_source(c->diags,home->source,home->size); + if (owner) check_method(c,decl,c->unit_scope[unit_index(c,home)],owner); + else check_fn(c,decl,c->unit_scope[unit_index(c,home)]); + c->ast=save_ast; + c->unit=save_unit; + c->types.unit_name=save_name; + if (save_unit) fe_diags_source(c->diags,save_unit->source,save_unit->size); + if (c->diags->errors>before) + fe_diag_note_src(c->diags,site,"instantiated here"); +} + +/* A call to a generic function: read the type arguments, check the value + arguments against the bound signature, then check the body once. */ +FeType *check_generic_call(FeCheckerState *s, FeNode *n, FeSym *sym, + FeUnit *home) +{ + FeCheck *c=s->c; + FeNode *decl=sym->fn; + unsigned want=decl_type_param_count(decl); + FeType *args[FE_TYPE_PARAM_MAX]; + FeNode *arg=n->children; + unsigned i; + char key[FE_GENERIC_KEY_MAX]; + FeBindSave save; + FeType *result; + int fresh; + if (want>FE_TYPE_PARAM_MAX) { + err(c,n->loc,"too many generic parameters"); + return unknown(c); + } + for (i=0;iloc,"generic call requires explicit type arguments"); + return unknown(c); + } + args[i]=type_from_expr(s,arg,&ok); + if (!ok) { + err(c,arg->loc,"a comptime type argument must name a type"); + return unknown(c); + } + arg=arg->next; + } + instance_key(key,home->name,decl->text,args,want); + push_bindings(c,&save,decl,args,want); + result=check_call_args(s,n,sym,home->name,want); + fresh=instance_record(c,key,n->loc,decl,home,0); + pop_bindings(c,&save); + /* The call goes to this instance, not to the declaration it came from. */ + if (n->a) n->a->cname=(char *)instance_cname(c,key); + if (fresh>0) { + if (!instance_descend(c,n->loc)) return result; + push_bindings(c,&save,decl,args,want); + instantiate_body(c,home,decl,0,&save,n->loc); + pop_bindings(c,&save); + --c->instance_depth; + } + return result; +} + +/* `Type.method(...)` where Type is a generic instance and the method takes no + self parameter. */ +/* The unit a name belongs to, by name. */ +FeUnit *unit_named(FeCheck *c, const char *name) +{ + unsigned u; + if (!name) return 0; + for (u=0;ubuild->count;++u) + if (!strcmp(c->build->units[u].name,name)) return &c->build->units[u]; + return 0; +} + +FeType *check_static_method_call(FeCheckerState *s, FeNode *n, + FeType *owner, FeNode *method) +{ + FeCheck *c=s->c; + /* A method belongs to the unit that declared its type, not to whichever + unit happens to be calling it. */ + FeUnit *home=unit_named(c,owner ? owner->unit : 0); + FeBindSave save; + FeType *result; + char key[FE_GENERIC_KEY_MAX]; + FeType *self_args[1]; + int fresh; + FeSym fake; + if (!home) home=current_unit(c); + self_args[0]=owner; + instance_key(key,home->name,method->text,self_args,1); + memset(&fake,0,sizeof fake); + fake.name=method->text; + fake.cname=method->cname; + fake.fn=method; + fake.decl=method; + push_instance_bindings(c,&save,owner); + bind_self(c,owner); + result=check_call_args(s,n,&fake,home->name,0); + fresh=instance_record(c,key,n->loc,method,home,owner); + pop_bindings(c,&save); + if (n->a) n->a->cname=(char *)instance_cname(c,key); + if (fresh>0) { + if (!instance_descend(c,n->loc)) return result; + push_instance_bindings(c,&save,owner); + bind_self(c,owner); + instantiate_body(c,home,method,owner,&save,n->loc); + pop_bindings(c,&save); + --c->instance_depth; + } + return result; +} + +/* The body of a method on a generic instance, checked once per instance. */ +void check_instance_method(FeCheckerState *s, FeType *owner, + FeNode *method, FeLoc site, FeNode *call) +{ + FeCheck *c=s->c; + /* A method belongs to the unit that declared its type, not to whichever + unit happens to be calling it. */ + FeUnit *home=unit_named(c,owner ? owner->unit : 0); + FeBindSave save; + char key[FE_GENERIC_KEY_MAX]; + FeType *self_args[1]; + self_args[0]=owner; + if (!home) home=current_unit(c); + instance_key(key,home->name,method->text,self_args,1); + { + FeBindSave probe; + int fresh; + push_instance_bindings(c,&probe,owner); + bind_self(c,owner); + fresh=instance_record(c,key,site,method,home,owner); + pop_bindings(c,&probe); + /* The call names this instance's copy of the method. */ + if (call && call->a) call->a->cname=(char *)instance_cname(c,key); + if (fresh<=0) return; + } + if (!instance_descend(c,site)) return; + push_instance_bindings(c,&save,owner); + bind_self(c,owner); + instantiate_body(c,home,method,owner,&save,site); + pop_bindings(c,&save); + --c->instance_depth; +} + +/* SPEC 4.7: `const Word = i32;` is another spelling of a type, not a value. + It has no initializer to check and no storage. */ +int const_names_type(FeCheckerState *s, FeNode *n) +{ + FeType *t; + if (!n->b || n->b->kind!=FE_N_IDENT || !n->b->text) return 0; + if (n->a) return 0; + if (find_symbol(s->globals,n->b->text)) return 0; + t=fe_type_intern(&s->c->types,n->b->text); + return t && t->kind!=FE_TYPE_UNKNOWN; +} + +FeNode *type_method(FeType *t, const char *name) +{ + FeNode *m; + if (!t || !t->decl_node || !name) return 0; + for (m=t->decl_node->children;m;m=m->next) + if (m->kind==FE_N_FN && m->text && strcmp(m->text,name)==0) return m; + return 0; +} + +int method_is_static(const FeNode *method) +{ + FeNode *first=method && method->a ? method->a->children : 0; + return !first || !first->text || strcmp(first->text,"self")!=0; +} + +/* A call to a named function. `home` is the unit the signature was written in, + null when that is the unit being checked: parameter and return types have to + be read where they were written or a name would mean the caller's type. */ +/* `error.Name` is a member of the default error set. That set is open -- names + are collected across the build and numbered later, not declared -- so any + name is well formed here and the value's type is core.Error. */ diff --git a/fec/src/checkpri.h b/fec/src/checkpri.h new file mode 100644 index 0000000..5c112a3 --- /dev/null +++ b/fec/src/checkpri.h @@ -0,0 +1,266 @@ +#ifndef FE_CHECKPRI_H +#define FE_CHECKPRI_H + +/* The checker's own vocabulary, shared by the files it is split across. + Nothing outside the checker includes this. */ + +#include "check.h" +#include "m7.h" +#include + +#define FE_M7_FLOW_CAP 64U +#include "own.h" +#include +#include + +typedef struct FeSym FeSym; +/* FeScope is forward declared in check.h. */ + +struct FeSym { + const char *name; + char *cname; + FeType *type; + FeNode *fn; + int mutable; + int initialized; + int moved; + FeNode *decl; + /* M6 ownership is tracked at the root local/parameter. A reference + binding remembers that root so releasing the binding's last use can + release the root borrow without a separate alias engine. */ + FeOwnState own; + FeSym *borrow_root; + /* Which field of the root this binding borrowed, or null for all of it. */ + const char *borrow_field; + int borrow_mut; + int borrow_defer; + FeScope *owner; +}; + +struct FeScope { + FeScope *parent; + FeSym *items; + unsigned count; + unsigned capacity; +}; + + +typedef struct FeCheckerState { + FeCheck *c; + FeScope *scope; + FeScope *globals; + FeType *ret; + unsigned loop_depth; + unsigned defer_depth; + /* SPEC 5 R9 lists what only `unsafe` allows; `asm` is on it. */ + unsigned unsafe_depth; + FeOwnLiveness liveness; + FeNode *fn_node; + /* While a projection is being checked, which field of which base it + reaches. The read happens down at the identifier, which cannot see the + chain above it, so the chain leaves word here on the way down. */ + const char *proj_field; + FeNode *proj_base; +} FeCheckerState; + +/* The type bindings in force, saved across a nested instantiation. */ +typedef struct FeBindSave { + FeTypeBind params[FE_TYPE_PARAM_MAX]; + unsigned count; +} FeBindSave; + +typedef struct FeFlowSlot { + FeSym *sym; + int moved; + int initialized; + int own_move; + int own_initialized; +} FeFlowSlot; + +typedef struct FeFlowBorrow { + FeSym *root; + const char *field; + int mutable; +} FeFlowBorrow; + +/* How long a chain of new generic instances may get, and how long an + instance's readable spelling may be before it falls back to serials. */ +#define FE_GENERIC_DEPTH_MAX 32 +#define FE_GENERIC_NAME_READABLE 200 + +/* Every definition in the checker, so the split files can see each other. */ +FeType *unknown(FeCheck *c); +void err(FeCheck *c, FeLoc loc, const char *msg); +int ordered_type(const FeType *t); +int known(FeType *t); +int in_own_drop(FeCheckerState *s, FeNode *n); +void mark_moved(FeCheckerState *s, FeNode *n, FeType *t); +int compatible(FeType *want, FeType *got, FeNode *value); +int call_reborrows(const FeType *param, const FeType *arg); +int return_weakens(const FeType *want, const FeType *got); +int explicit_castable(FeType *a, FeType *b); +FeType *node_type(FeCheck *c, FeNode *n); +char *unit_cname(FeCheck *c, const char *name); +char *local_cname(FeCheck *c, const char *name); +FeScope *scope_new(FeCheckerState *s, FeScope *parent); +FeSym *find_current(FeScope *scope, const char *name); +FeSym *find_symbol(FeScope *scope, const char *name); +FeSym *add_symbol(FeCheckerState *s, FeScope *scope, + const char *name, FeType *type, FeNode *fn, + int mutable, int initialized, char *cname, + FeNode *decl); +void enter_unit(FeCheck *c, unsigned index); +unsigned unit_index(FeCheck *c, const FeUnit *u); +FeUnit *binding_unit(FeCheckerState *s, FeNode *base); +int decl_is_public(const FeNode *decl); +FeSym *unit_member(FeCheck *c, FeUnit *u, const char *name); +FeType *unit_type(FeCheck *c, FeUnit *u, const char *name); +int enter_declaring_unit(FeCheck *c, const char *unit_name); +FeNode *unit_type_decl(FeCheck *c, FeUnit *u, const char *name); +FeType *node_type_in(FeCheck *c, const char *unit, FeNode *node); +FeNode *find_method(FeCheck *c, FeType *owner, const char *name); +FeType *method_type(FeCheck *c, FeNode *node, FeType *owner); +unsigned flow_capture(FeScope *scope, FeFlowSlot *slots, unsigned cap); +void flow_restore(FeFlowSlot *slots, unsigned count); +void flow_merge(FeFlowSlot *base, FeFlowSlot *left, FeFlowSlot *right, + unsigned count); +FeSym *own_root_symbol(FeCheckerState *s, FeNode *expr); +const char *own_projected_field(FeNode *expr, FeNode **root_out); +int own_is_global(FeCheckerState *s, FeSym *sym); +void own_borrow_expr(FeCheckerState *s, FeNode *expr, int mutable); +void own_release_temporary_borrow(FeCheckerState *s, FeNode *expr); +FeSym *own_derived_call_root(FeCheckerState *s, FeNode *call); +void own_bind_derived_call(FeCheckerState *s, FeSym *binding, + FeNode *value); +int own_stmt_uses(FeNode *node, const char *name); +int own_defer_uses(FeNode *node, const char *name); +int own_contains_node(FeNode *node, FeNode *needle); +void own_release_after_stmt(FeCheckerState *s, FeScope *scope, + FeNode *stmt, int scope_end); +FeOwnState *flow_own_new(FeCheckerState *s, unsigned count); +void flow_own_capture(FeFlowSlot *slots, FeOwnState *states, + unsigned count); +void flow_own_restore(FeFlowSlot *slots, FeOwnState *states, + unsigned count); +void flow_own_merge(FeFlowSlot *slots, FeOwnState *left, + FeOwnState *right, unsigned count); +FeFlowBorrow *flow_borrow_new(FeCheckerState *s, unsigned count); +void flow_borrow_capture(FeFlowSlot *slots, FeFlowBorrow *states, + unsigned count); +void flow_borrow_restore(FeFlowSlot *slots, FeFlowBorrow *states, + unsigned count); +void flow_borrow_merge(FeFlowSlot *slots, FeFlowBorrow *left, + FeFlowBorrow *right, unsigned count); +FeNode *find_const_node(FeCheck *c, const char *name); +const char *builtin_format(FeCheckerState *s, FeNode *fmt); +int format_is_slice_u8(FeType *t); +int format_is_writer_type(FeType *t); +int format_arg_ok(FeType *t, int verb); +void check_format_call(FeCheckerState *s, FeNode *n); +int is_format_builtin(const char *name); +int lvalue_writable(FeCheckerState *s, FeNode *n); +int has_field(FeNode *list, const char *name); +int field_is_visible(FeCheckerState *s, const FeType *t, + const FeFieldType *field); +FeType *check_struct_fields(FeCheckerState *s, FeNode *n, FeType *t); +FeType *check_struct_init(FeCheckerState *s, FeNode *n); +FeType *check_array_init(FeCheckerState *s, FeNode *n); +int array_slice_lvalue(FeNode *n); +FeType *check_index(FeCheckerState *s, FeNode *n); +FeType *check_identifier(FeCheckerState *s, FeNode *n); +FeType *check_expr_core(FeCheckerState *s, FeNode *n); +FeType *check_lvalue_core(FeCheckerState *s, FeNode *n, int read, + FeType *base_in); +int compound_operator(const char *op); +void check_match(FeCheckerState *s, FeNode *n); +void check_for(FeCheckerState *s, FeNode *n); +void check_type_cycle(FeCheck *c, FeType *t); +void check_type_cycles(FeCheck *c); +int own_ast_reference_type(FeNode *type); +int own_ast_pointer_to_reference(FeNode *type); +void check_reference_storage(FeCheck *c, FeNode *decl); +int own_return_from_allowed_root(FeCheckerState *s, FeNode *expr); +void check_stmt_core(FeCheckerState *s, FeNode *n); +void check_fn(FeCheck *c, FeNode *fn, FeScope *globals); +void check_method(FeCheck *c, FeNode *fn, FeScope *globals, + FeType *owner); +int m7_actual_compatible(FeType *want, FeType *got, FeNode *value); +FeType *m7_check_expected(FeCheckerState *s, FeNode *value, + FeType *expected); +FeType *m7_member_field(FeCheckerState *s, FeNode *n, FeType *base); +int m7_place_is_projection(FeNode *n); +unsigned decl_type_param_count(const FeNode *decl); +FeNode *decl_type_param(const FeNode *decl, unsigned i); +int decl_is_generic(const FeNode *decl); +void check_generic_params(FeCheck *c, FeNode *decl); +void push_bindings(FeCheck *c, FeBindSave *save, FeNode *decl, + FeType **args, unsigned count); +void push_instance_bindings(FeCheck *c, FeBindSave *save, FeType *t); +void bind_self(FeCheck *c, FeType *owner); +void pop_bindings(FeCheck *c, const FeBindSave *save); +void instance_key(char *out, const char *unit, const char *name, + FeType **args, unsigned count); +const char *instance_cname(FeCheck *c, const char *key); +int instance_known(FeCheck *c, const char *key); +int instance_record(FeCheck *c, const char *key, FeLoc loc, + FeNode *decl, FeUnit *home, FeType *owner); +int instance_descend(FeCheck *c, FeLoc loc); +FeUnit *current_unit(FeCheck *c); +FeType *build_struct_instance(FeCheck *c, FeUnit *home, FeNode *decl, + const char *key, FeType **args, + unsigned count); +FeType *instantiate_struct(FeCheck *c, FeUnit *home, const char *name, + FeType **args, unsigned count, FeLoc loc); +FeType *instantiate_type_node(void *owner, const FeNode *node); +FeType *type_from_expr(FeCheckerState *s, FeNode *n, int *ok); +int comptime_condition(FeCheckerState *s, FeNode *n, int *out); +int const_foldable(FeCheckerState *s, FeNode *n); +void instantiate_body(FeCheck *c, FeUnit *home, FeNode *decl, + FeType *owner, FeBindSave *bindings, FeLoc site); +FeType *check_generic_call(FeCheckerState *s, FeNode *n, FeSym *sym, + FeUnit *home); +FeUnit *unit_named(FeCheck *c, const char *name); +FeType *check_static_method_call(FeCheckerState *s, FeNode *n, + FeType *owner, FeNode *method); +void check_instance_method(FeCheckerState *s, FeType *owner, + FeNode *method, FeLoc site, FeNode *call); +int const_names_type(FeCheckerState *s, FeNode *n); +FeNode *type_method(FeType *t, const char *name); +int method_is_static(const FeNode *method); +int is_error_set_member(FeCheckerState *s, FeNode *n); +FeType *cross_unit_value(FeCheckerState *s, FeNode *n, int *handled); +FeType *check_call_args(FeCheckerState *s, FeNode *n, FeSym *sym, + const char *home, unsigned skip); +FeType *check_call(FeCheckerState *s, FeNode *n); +void m7_capture_flow(FeCheckerState *s, FeFlowSlot *slots, + FeOwnState **own, FeFlowBorrow **borrow, + unsigned *count); +void m7_restore_flow(FeFlowSlot *slots, FeOwnState *own, + FeFlowBorrow *borrow, unsigned count); +void m7_merge_rhs_flow(FeCheckerState *s, FeFlowSlot *base, + FeOwnState *own_base, + FeFlowBorrow *borrow_base, + unsigned count, FeFlowSlot *rhs, + FeOwnState *own_rhs, + FeFlowBorrow *borrow_rhs); +int m7_stmt_definitely_exits(FeNode *n); +FeType *m7_check_lazy(FeCheckerState *s, FeNode *n, + FeM7LazyKind kind); +FeType *check_expr(FeCheckerState *s, FeNode *n); +FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read); +FeType *m7_pattern_binding_type(FeCheckerState *s, FeType *payload, + FeNode *source, int *borrow_mut); +void m7_check_if_let(FeCheckerState *s, FeNode *n); +void m7_check_optional_match(FeCheckerState *s, FeNode *n, + FeType *opt); +void m7_check_match_stmt(FeCheckerState *s, FeNode *n); +void m7_check_decl_stmt(FeCheckerState *s, FeNode *n, int mutable); +void check_stmt(FeCheckerState *s, FeNode *n); +int m7_ast_reference_storage(FeNode *type); +void m7_check_storage(FeCheck *c, FeNode *decl); +void m7_validate_error_decl(FeCheck *c, FeNode *decl); +void declare_unit(FeCheck *c); +FeScope *declare_unit_scope(FeCheck *c, FeCheckerState *s); +void check_unit_bodies(FeCheck *c, FeCheckerState *s); + +#endif diff --git a/fec/src/checkpro.c b/fec/src/checkpro.c new file mode 100644 index 0000000..e3a5f80 --- /dev/null +++ b/fec/src/checkpro.c @@ -0,0 +1,188 @@ +#include "checkpri.h" + +int m7_ast_reference_storage(FeNode *type) +{ + if (!type || !type->text) return 0; + if (strcmp(type->text,"&")==0 || strcmp(type->text,"&mut")==0 || + (strcmp(type->text,"[")==0 && !type->a) || + strcmp(type->text,"str")==0) + return 1; + if (strcmp(type->text,"?")==0) + return m7_ast_reference_storage(type->a); + if (strcmp(type->text,"^")==0) return 0; + return 0; +} + +void m7_check_storage(FeCheck *c, FeNode *decl) +{ + FeNode *m; + if (!decl) return; + if (decl->kind==FE_N_STRUCT || decl->kind==FE_N_ENUM) { + /* This pass exists for the shapes the other one cannot see, such as a + reference behind an optional. A plain `&T` field is seen by both, so + leave that one to check_reference_storage below. */ + for (m=decl->children;m;m=m->next) + if (m->kind==FE_N_FIELD && m7_ast_reference_storage(m->a) && + !own_ast_reference_type(m->a) && + !own_ast_pointer_to_reference(m->a)) + err(c,m->loc,"reference type is not allowed in aggregate storage"); + } + check_reference_storage(c,decl); +} + +void m7_validate_error_decl(FeCheck *c, FeNode *decl) +{ + FeNode *a; + FeNode *b; + unsigned long code; + unsigned long other; + if (!decl || decl->kind!=FE_N_ERROR_DECL) return; + for (a=decl->children;a;a=a->next) { + if (!a->a || a->a->kind!=FE_N_LITERAL || !a->a->text) continue; + code=strtoul(a->a->text,0,0); + if (code==0UL) + err(c,a->loc,"error code 0 is reserved for success"); + for (b=decl->children;b && b!=a;b=b->next) { + if (a->text && b->text && strcmp(a->text,b->text)==0) { + err(c,a->loc,"duplicate error member name"); + break; + } + if (b->a && b->a->kind==FE_N_LITERAL && b->a->text) { + other=strtoul(b->a->text,0,0); + if (other==code) { + err(c,a->loc,"duplicate error numeric code"); + break; + } + } + } + } +} + +/* Everything a unit declares, before any body anywhere is looked at. */ +void declare_unit(FeCheck *c) +{ + FeNode *n; + /* A generic declaration is not a type; only its instances are. */ + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_STRUCT && !decl_is_generic(n)) + fe_type_declare_struct(&c->types,n,(n->flags & FE_NODE_PACKED)!=0); + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) { + FeNode *m; + m7_check_storage(c,n); + if (n->kind==FE_N_ERROR_DECL) m7_validate_error_decl(c,n); + check_generic_params(c,n); + for (m=n->kind==FE_N_STRUCT ? n->children : 0;m;m=m->next) + if (m->kind==FE_N_FN) check_generic_params(c,m); + } + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_ENUM && !decl_is_generic(n)) + fe_type_declare_enum(&c->types,n); + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_ERROR_DECL) fe_type_declare_error(&c->types,n); +} + +/* The unit's top-level names, in a scope of their own so that another unit + can look into it later without inheriting anything else. */ +FeScope *declare_unit_scope(FeCheck *c, FeCheckerState *s) +{ + FeNode *n; + FeNode *m; + FeType *t; + FeScope *globals; + char method_name[128]; + globals=scope_new(s,0); + s->scope=globals; + s->globals=globals; + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) { + if (n->kind==FE_N_STRUCT) { + for (m=n->children;m;m=m->next) if (m->kind==FE_N_FN) { + sprintf(method_name,"%s_%s",n->text ? n->text : "Type", + m->text ? m->text : "method"); + m->cname=unit_cname(c,method_name); + } + } + if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { + t=n->a ? node_type(c,n->a) : unknown(c); + add_symbol(s,globals,n->text,t,0,n->kind==FE_N_GLOBAL, + n->b!=0,unit_cname(c,n->text ? n->text : "global"),n); + } + } + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_FN) { + t=fe_type_intern(&c->types,""); + /* `extern "c"` means the linker already knows this name, so it is + not decorated with the unit it was declared in. */ + add_symbol(s,globals,n->text,t,n,0,1, + (n->flags & FE_NODE_EXTERN) && n->text ? n->text : + unit_cname(c,n->text ? n->text : "fn"),n); + } + return globals; +} + +void check_unit_bodies(FeCheck *c, FeCheckerState *s) +{ + FeNode *n; + FeNode *m; + FeSym *sym; + FeType *t; + FeType *iv; + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { + sym=find_current(s->globals,n->text ? n->text : ""); + if (n->kind==FE_N_CONST && const_names_type(s,n)) continue; + if (n->b) { + if (!const_foldable(s,n->b)) + err(c,n->b->loc, + "a global initializer must be known at compile time"); + iv=m7_check_expected(s,n->b,sym ? sym->type : 0); + if (sym && sym->type->kind==FE_TYPE_UNKNOWN) { + sym->type=iv; + n->sem_type=iv; + } else if (sym && !fe_type_equal(sym->type,iv) && + !m7_actual_compatible(sym->type,iv,n->b)) + err(c,n->loc,"global initializer type mismatch"); + } + } + /* A generic body means nothing until its parameters are bound, so it is + checked once per instance and not here. */ + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_FN && !decl_is_generic(n)) check_fn(c,n,s->globals); + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_STRUCT && !decl_is_generic(n)) { + t=fe_type_intern(&c->types,n->text); + for (m=n->children;m;m=m->next) + if (m->kind==FE_N_FN) check_method(c,m,s->globals,t); + } +} + +int fe_check_program(FeCheck *c) +{ + FeCheckerState s; + unsigned u; + s.c=c; + s.scope=0; + s.globals=0; + s.ret=fe_type_intern(&c->types,"void"); + s.loop_depth=0; + s.defer_depth=0; + s.unsafe_depth=0; + s.fn_node=0; + fe_own_liveness_init(&s.liveness,&c->arena); + for (u=0;ubuild->count;++u) { enter_unit(c,u); declare_unit(c); } + /* Only now: a field may name a type in a unit that had not declared it + yet, and resolving it early would freeze the wrong answer in place. */ + for (u=0;ubuild->count;++u) { enter_unit(c,u); check_type_cycles(c); } + fe_type_layout_all(&c->types); + for (u=0;ubuild->count;++u) { + enter_unit(c,u); + c->unit_scope[u]=declare_unit_scope(c,&s); + } + for (u=0;ubuild->count;++u) { + enter_unit(c,u); + s.scope=c->unit_scope[u]; + s.globals=c->unit_scope[u]; + check_unit_bodies(c,&s); + } + fe_type_layout_all(&c->types); + return c->diags->errors==0; +} diff --git a/fec/src/checkstm.c b/fec/src/checkstm.c new file mode 100644 index 0000000..e5f1aa1 --- /dev/null +++ b/fec/src/checkstm.c @@ -0,0 +1,756 @@ +#include "checkpri.h" + +void check_match(FeCheckerState *s, FeNode *n) +{ + FeType *value; + FeNode *arm; + FeVariantType *variant; + int seen[256]; + int wildcard=0; + FeFlowSlot base[64], merged[64], current[64]; + unsigned flow_count; + int have_merged=0; + unsigned i; + for(i=0;i<256U;i++) seen[i]=0; + value=check_expr(s,n->a); + if(!value || value->kind!=FE_TYPE_ENUM) { err(s->c,n->loc,"match requires an enum value"); return; } + flow_count=flow_capture(s->scope,base,64); + for(arm=n->children;arm;arm=arm->next) { + FeScope *old=s->scope; + flow_restore(base,flow_count); + if(arm->text && strcmp(arm->text,"_")==0) wildcard=1; + else { + variant=fe_type_variant(value,arm->text); + if(!variant) { err(s->c,arm->loc,"unknown match variant"); continue; } + if(variant->tag<256U) { + if(seen[variant->tag]) err(s->c,arm->loc,"duplicate match variant"); + seen[variant->tag]=1; + } + s->scope=scope_new(s,old); + if(variant->field_count==1 && arm->children) { + add_symbol(s,s->scope,arm->children->text,variant->fields[0].type,0,0,1, + local_cname(s->c,arm->children->text),arm->children); + } else if(variant->field_count>0) { + FeNode *b=arm->children; + for(i=0;ifield_count && b;i++,b=b->next) { + FeFieldType *f=&variant->fields[i]; + add_symbol(s,s->scope,b->text,f->type,0,0,1, + local_cname(s->c,b->text),b); + } + } + } + if(arm->a && arm->a->kind==FE_N_BLOCK) check_stmt(s,arm->a); + else if(arm->a) check_expr(s,arm->a); + s->scope=old; + flow_capture(s->scope,current,flow_count); + if(!have_merged) { + for(i=0;ivariant_count && i<256U;i++) if(!seen[i]) err(s->c,n->loc,"non-exhaustive match"); +} + +void check_for(FeCheckerState *s, FeNode *n) +{ + FeType *start; + FeType *finish; + FeType *elem; + FeType *ref_type; + FeSym *iter_sym; + char *index_cname; + char *item_cname; + int iter_mut; + FeScope *old=s->scope; + if(!n->c) { + start=check_expr(s,n->a); + if (!fe_type_is_indexable(start)) { + err(s->c,n->loc,"for iterable must be an array, slice, or str"); + return; + } + elem=start->elem; + iter_sym=0; + if (n->a && n->a->kind==FE_N_IDENT) + iter_sym=find_symbol(s->scope,n->a->text ? n->a->text : ""); + else if (n->a && n->a->kind==FE_N_INDEX && n->a->a && + n->a->a->kind==FE_N_IDENT) + iter_sym=find_symbol(s->scope,n->a->a->text ? n->a->a->text : ""); + iter_mut=start->kind==FE_TYPE_SLICE ? start->ref_mut : + (iter_sym && iter_sym->mutable); + ref_type=fe_type_ref(&s->c->types,elem,iter_mut); + if (iter_mut) n->flags |= 4U; + s->scope=scope_new(s,old); + if (n->aux_text) { + index_cname=local_cname(s->c,n->text ? n->text : "index"); + item_cname=local_cname(s->c,n->aux_text); + add_symbol(s,s->scope,n->text,fe_type_intern(&s->c->types,"usize"),0,0,1, + index_cname,n); + add_symbol(s,s->scope,n->aux_text,ref_type,0,iter_mut,1, + item_cname,0); + n->cname=index_cname; + n->aux_cname=item_cname; + } else { + item_cname=local_cname(s->c,n->text ? n->text : "item"); + add_symbol(s,s->scope,n->text,ref_type,0,iter_mut,1, + item_cname,n); + n->cname=item_cname; + } + /* Walking a container borrows it for the length of the walk: the + item is a reference into it, so writing the container underneath + would move what that reference points at (SPEC 5 R6). */ + if (iter_sym) + fe_own_access(s->c->diags,&iter_sym->own, + iter_mut ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, + n->loc); + check_stmt(s,n->b); + if (iter_sym) { + if (iter_mut) fe_own_release_exclusive(&iter_sym->own); + else fe_own_release_shared(&iter_sym->own); + } + s->scope=old; + return; + } + start=check_expr(s,n->a); + finish=check_expr(s,n->c); + if(known(start)&&!fe_type_is_integer(start)) err(s->c,n->loc,"range start must be integer"); + if(known(finish)&&!fe_type_is_integer(finish)) err(s->c,n->loc,"range end must be integer"); + s->scope=scope_new(s,old); + index_cname=local_cname(s->c,n->text ? n->text : "index"); + add_symbol(s,s->scope,n->text,fe_type_intern(&s->c->types,"usize"),0,0,1, + index_cname,n); + n->cname=index_cname; + check_stmt(s,n->b); + s->scope=old; +} + +void check_type_cycle(FeCheck *c, FeType *t) +{ + unsigned i; + FeType *next; + if (!t || t->kind == FE_TYPE_SLICE || t->kind == FE_TYPE_STR || + t->kind == FE_TYPE_REF || t->kind == FE_TYPE_OWNED || + t->kind == FE_TYPE_INT || t->kind == FE_TYPE_BOOL || + t->kind == FE_TYPE_CHAR || t->kind == FE_TYPE_VOID || + t->kind == FE_TYPE_UNKNOWN || t->kind == FE_TYPE_ERROR) return; + if (t->kind == FE_TYPE_ERROR_UNION) { + check_type_cycle(c,t->error_value); + return; + } + if (t->cycle_state == 1) { + if (c->ast->root) err(c, c->ast->root->loc, "by-value recursive type"); + return; + } + if (t->cycle_state == 2) return; + t->cycle_state = 1; + if (t->kind == FE_TYPE_ARRAY) { + check_type_cycle(c,t->elem); + } else if (t->kind == FE_TYPE_STRUCT) { + int back=enter_declaring_unit(c,t->unit); + for (i=0;ifield_count;i++) + if (!t->fields[i].type && t->fields[i].ast_node) + t->fields[i].type=fe_type_from_ast(&c->types,t->fields[i].ast_node->a); + if (back>=0) enter_unit(c,(unsigned)back); + for (i=0;ifield_count;i++) check_type_cycle(c,t->fields[i].type); + } else if (t->kind == FE_TYPE_ENUM) { + int back=enter_declaring_unit(c,t->unit); + for (i=0;ivariant_count;i++) { + unsigned j; + for (j=0;jvariants[i].field_count;j++) + if (!t->variants[i].fields[j].type && t->variants[i].fields[j].ast_node) + t->variants[i].fields[j].type=fe_type_from_ast(&c->types, + t->variants[i].fields[j].ast_node->a); + } + if (back>=0) enter_unit(c,(unsigned)back); + for (i=0;ivariant_count;i++) { + unsigned j; + for (j=0;jvariants[i].field_count;j++) { + next=t->variants[i].fields[j].type; + check_type_cycle(c,next); + } + } + } + t->cycle_state=2; +} + +void check_type_cycles(FeCheck *c) +{ + FeType *t; + for (t=c->types.types;t;t=t->next) t->cycle_state=0; + for (t=c->types.types;t;t=t->next) check_type_cycle(c,t); +} + +int own_ast_reference_type(FeNode *type) +{ + if (!type || !type->text) return 0; + return strcmp(type->text,"&")==0 || strcmp(type->text,"&mut")==0 || + (strcmp(type->text,"[")==0 && !type->a) || strcmp(type->text,"str")==0; +} + +int own_ast_pointer_to_reference(FeNode *type) +{ + return type && type->text && strcmp(type->text,"*")==0 && + own_ast_reference_type(type->a); +} + +void check_reference_storage(FeCheck *c, FeNode *decl) +{ + FeNode *m; + if (!decl) return; + if (decl->kind==FE_N_STRUCT || decl->kind==FE_N_ENUM) { + for (m=decl->children;m;m=m->next) + if (m->kind==FE_N_FIELD && + (own_ast_reference_type(m->a) || own_ast_pointer_to_reference(m->a))) + err(c,m->loc,"reference type is not allowed in aggregate storage"); + } + if ((decl->kind==FE_N_GLOBAL || decl->kind==FE_N_CONST) && decl->a && + own_ast_reference_type(decl->a) && + !(decl->kind==FE_N_CONST && decl->a->text && strcmp(decl->a->text,"str")==0)) + err(c,decl->loc,"reference type is not allowed in global storage"); + if (decl->kind==FE_N_FN && decl->b && own_ast_pointer_to_reference(decl->b)) + err(c,decl->b->loc,"reference type is not allowed as a pointer target"); + if (decl->kind==FE_N_FN) + for (m=decl->a ? decl->a->children : 0;m;m=m->next) + if (own_ast_pointer_to_reference(m->a)) + err(c,m->loc,"reference type is not allowed as a pointer target"); +} + +int own_return_from_allowed_root(FeCheckerState *s, FeNode *expr) +{ + FeSym *root; + FeNode *p; + unsigned refs=0; + if (!expr) return 0; + root=own_root_symbol(s,expr); + if (!root) return 1; /* Static-producing builtins/methods are checked by + their declared R8 interface. */ + if (own_is_global(s,root)) + return root->decl && root->decl->kind==FE_N_GLOBAL && + (root->decl->flags & 2U); + if (!root->decl || root->decl->kind!=FE_N_PARAM) return 0; + for (p=s->fn_node && s->fn_node->a ? s->fn_node->a->children : 0; + p;p=p->next) { + FeType *t=p->sem_type ? p->sem_type : node_type(s->c,p->a); + if (fe_own_is_reference_like(t)) ++refs; + } + if (s->fn_node && s->fn_node->text && refs && + root->name && strcmp(root->name,"self")==0) return 1; + return refs==1; +} + +void check_stmt_core(FeCheckerState *s, FeNode *n) +{ + FeCheck *c = s->c; + FeScope *old; + FeType *a; + FeType *b; + FeSym *sym; + FeNode *x; + int initialized; + if (!n) return; + switch (n->kind) { + case FE_N_BLOCK: + old = s->scope; + s->scope = scope_new(s, old); + for (x = n->children; x; x = x->next) { + check_stmt(s,x); + own_release_after_stmt(s,s->scope,x,0); + } + own_release_after_stmt(s,s->scope,n,1); + s->scope = old; + break; + case FE_N_LET: + case FE_N_CONST: + a = n->a ? node_type(c, n->a) : unknown(c); + b = check_expr(s, n->b); + if (!n->a) a = b; + if (a->kind == FE_TYPE_VOID) + err(c, n->loc, "variable cannot have void type"); + if (n->a && !compatible(a, b, n->b) && b->kind != FE_TYPE_UNKNOWN) + err(c, n->loc, "initializer type mismatch"); + if (b->kind == FE_TYPE_VOID) + err(c, n->loc, "void expression cannot initialize a variable"); + if (n->kind==FE_N_LET && a->kind==FE_TYPE_SLICE && a->ref_mut) + err(c,n->loc,"let cannot bind a mutable slice"); + mark_moved(s,n->b,b); + sym=add_symbol(s, s->scope, n->text, a, 0, 0, 1, + local_cname(c, n->text ? n->text : "local"), n); + if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && + (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { + sym->borrow_root=own_root_symbol(s,n->b->a); + sym->borrow_field=own_projected_field(n->b->a,0); + sym->borrow_mut=strcmp(n->b->text,"&mut")==0; + sym->borrow_defer=s->defer_depth != 0 || + own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); + } + own_bind_derived_call(s,sym,n->b); + break; + case FE_N_VAR: + a = n->a ? node_type(c, n->a) : unknown(c); + if (!n->b && !n->a) + err(c, n->loc, "uninitialized var requires an explicit type"); + b = n->b ? check_expr(s, n->b) : unknown(c); + if (!n->a && n->b) a = b; + if (a->kind == FE_TYPE_VOID) + err(c, n->loc, "variable cannot have void type"); + if (n->b && !compatible(a, b, n->b) && b->kind != FE_TYPE_UNKNOWN) + err(c, n->loc, "initializer type mismatch"); + if (b->kind == FE_TYPE_VOID) + err(c, n->loc, "void expression cannot initialize a variable"); + mark_moved(s,n->b,b); + initialized = n->b != 0; + sym=add_symbol(s, s->scope, n->text, a, 0, 1, initialized, + local_cname(c, n->text ? n->text : "local"), n); + if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && + (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { + sym->borrow_root=own_root_symbol(s,n->b->a); + sym->borrow_field=own_projected_field(n->b->a,0); + sym->borrow_mut=strcmp(n->b->text,"&mut")==0; + sym->borrow_defer=s->defer_depth != 0 || + own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); + } + own_bind_derived_call(s,sym,n->b); + break; + case FE_N_ASSIGN: + b = check_expr(s, n->b); + a = check_lvalue(s, n->a, compound_operator(n->text)); + if (!compatible(a, b, n->b) && b->kind != FE_TYPE_UNKNOWN) + err(c, n->loc, "assignment type mismatch"); + mark_moved(s,n->b,b); + sym = n->a && n->a->kind == FE_N_IDENT ? + find_symbol(s->scope, n->a->text) : 0; + if (sym && sym->mutable) { + sym->initialized = 1; + fe_own_access(s->c->diags,&sym->own,FE_OWN_WRITE,n->a->loc); + sym->moved=sym->own.move; + if (n->b && n->b->kind==FE_N_UNARY && n->b->text && + (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0) && + fe_own_is_reference_like(sym->type)) { + FeSym *root=own_root_symbol(s,n->b->a); + if (root && root->owner!=sym->owner) + err(c,n->b->loc,"reference would outlive its source scope"); + else if (root) { + if (sym->borrow_root) { + if (sym->borrow_mut) fe_own_release_exclusive(&sym->borrow_root->own); + else fe_own_release_shared(&sym->borrow_root->own); + } + sym->borrow_root=root; + sym->borrow_mut=strcmp(n->b->text,"&mut")==0; + } + } + } + break; + case FE_N_EXPR_STMT: + /* The enclosing-error-result check lives on the try expression itself, + so a bare `try e;` needs nothing extra here. */ + check_expr(s, n->a); + break; + case FE_N_DEFER: + ++s->defer_depth; + check_stmt(s,n->a); + --s->defer_depth; + break; + case FE_N_IF: { + FeFlowSlot base[64], left[64], right[64]; + FeOwnState *own_base, *own_left, *own_right; + FeFlowBorrow *borrow_base, *borrow_left, *borrow_right; + unsigned flow_count; + a = check_expr(s, n->a); + if (known(a) && a->kind != FE_TYPE_BOOL) + err(c, n->loc, "if condition must be bool"); + flow_count=flow_capture(s->scope,base,64); + own_base=flow_own_new(s,flow_count); + own_left=flow_own_new(s,flow_count); + own_right=flow_own_new(s,flow_count); + borrow_base=flow_borrow_new(s,flow_count); + borrow_left=flow_borrow_new(s,flow_count); + borrow_right=flow_borrow_new(s,flow_count); + flow_own_capture(base,own_base,flow_count); + flow_borrow_capture(base,borrow_base,flow_count); + check_stmt(s, n->b); + flow_capture(s->scope,left,flow_count); + flow_own_capture(left,own_left,flow_count); + flow_borrow_capture(left,borrow_left,flow_count); + flow_restore(base,flow_count); + flow_own_restore(base,own_base,flow_count); + flow_borrow_restore(base,borrow_base,flow_count); + if (n->c) check_stmt(s, n->c); + if (n->c) { + flow_capture(s->scope,right,flow_count); + flow_own_capture(right,own_right,flow_count); + flow_borrow_capture(right,borrow_right,flow_count); + } + else { + unsigned i; + for (i=0;ia); + if (known(a) && a->kind != FE_TYPE_BOOL) + err(c, n->loc, "while condition must be bool"); + flow_count=flow_capture(s->scope,base,64); + own_base=flow_own_new(s,flow_count); + own_body=flow_own_new(s,flow_count); + own_entry2=flow_own_new(s,flow_count); + borrow_base=flow_borrow_new(s,flow_count); + borrow_body=flow_borrow_new(s,flow_count); + borrow_entry2=flow_borrow_new(s,flow_count); + flow_own_capture(base,own_base,flow_count); + flow_borrow_capture(base,borrow_base,flow_count); + if (s->loop_depth < 255U) ++s->loop_depth; + check_stmt(s, n->b); + if (s->loop_depth) --s->loop_depth; + flow_capture(s->scope,body,flow_count); + flow_own_capture(body,own_body,flow_count); + flow_borrow_capture(body,borrow_body,flow_count); + for (i=0;iloop_depth < 255U) ++s->loop_depth; + check_stmt(s,n->b); + if (s->loop_depth) --s->loop_depth; + flow_capture(s->scope,body,flow_count); + flow_own_capture(body,own_body,flow_count); + flow_borrow_capture(body,borrow_body,flow_count); + for(i=0;iloop_depth < 255U) ++s->loop_depth; + check_for(s,n); + if (s->loop_depth) --s->loop_depth; + break; + case FE_N_MATCH: + check_match(s,n); + break; + case FE_N_BREAK: + case FE_N_CONTINUE: + if (!s->loop_depth) err(c,n->loc,"break or continue outside loop"); + break; + case FE_N_RETURN: + /* A deferred block runs during scope cleanup, on the way out of a + function that has already decided what it returns. There is nothing + for a `return` in there to mean. */ + if (s->defer_depth != 0) + err(c, n->loc, "cannot return from inside defer"); + b = n->a ? check_expr(s, n->a) : fe_type_intern(&c->types, "void"); + if (s->ret && fe_own_is_reference_like(s->ret) && + !own_return_from_allowed_root(s,n->a)) + err(c,n->loc,"reference return must be derived from a parameter or static"); + mark_moved(s,n->a,b); + if (known(b) && b->kind == FE_TYPE_VOID && s->ret->kind != FE_TYPE_VOID) + err(c, n->loc, "void expression returned from value function"); + else if (return_weakens(s->ret,b)) { } + else if (known(s->ret) && known(b) && !fe_type_equal(s->ret, b) && + b->kind != FE_TYPE_UNKNOWN && + !compatible(s->ret,b,n->a)) + err(c, n->loc, "return type mismatch"); + break; + case FE_N_ASM: + if (!s->unsafe_depth) + err(c,n->loc,"asm requires an unsafe block"); + break; + case FE_N_UNSAFE: + check_stmt(s, n->a); + break; + default: + break; + } +} + +void check_fn(FeCheck *c, FeNode *fn, FeScope *globals) +{ + FeCheckerState s; + FeScope *old; + FeNode *x; + FeType *t; + s.c = c; + s.globals = globals; + s.scope = scope_new(&s, globals); + s.ret = fn->b ? node_type(c, fn->b) : fe_type_intern(&c->types, "void"); + s.loop_depth=0; + s.defer_depth=0; + s.unsafe_depth=0; + s.fn_node=fn; + fe_own_liveness_init(&s.liveness,&c->arena); + fe_own_collect_last_uses(&s.liveness,fn); + fn->sem_type = s.ret; + for (x = fn->a ? fn->a->children : 0; x; x = x->next) { + t = node_type(c, x->a); + if (t->kind == FE_TYPE_VOID) + err(c, x->loc, "parameter cannot have void type"); + add_symbol(&s, s.scope, x->text, t, 0, 1, 1, + local_cname(c, x->text ? x->text : "arg"), x); + } + old = s.scope; + if (fn->c) check_stmt(&s, fn->c); + s.scope = old; +} + +void check_method(FeCheck *c, FeNode *fn, FeScope *globals, + FeType *owner) +{ + FeCheckerState s; + FeNode *x; + FeType *t; + FeBindSave self_save; + /* `Self` names the type a method belongs to, wherever it appears -- in a + signature, and in `Self{ .. }`. Binding it as a type makes both work the + same way, and the same way a generic instance already worked. */ + self_save.count=c->types.param_count; + { + unsigned i; + for(i=0;itypes.params[i]; + } + bind_self(c,owner); + s.c=c; + s.globals=globals; + s.scope=scope_new(&s,globals); + s.ret=fn->b ? method_type(c,fn->b,owner) : fe_type_intern(&c->types,"void"); + s.loop_depth=0; + s.defer_depth=0; + s.unsafe_depth=0; + s.fn_node=fn; + fe_own_liveness_init(&s.liveness,&c->arena); + fe_own_collect_last_uses(&s.liveness,fn); + fn->sem_type=s.ret; + for(x=fn->a ? fn->a->children : 0; x; x=x->next) { + t=method_type(c,x->a,owner); + x->sem_type=t; + add_symbol(&s,s.scope,x->text,t,0,1,1, + local_cname(c,x->text ? x->text : "arg"),x); + } + if(fn->c) check_stmt(&s,fn->c); + pop_bindings(c,&self_save); +} + +int m7_actual_compatible(FeType *want, FeType *got, FeNode *value) +{ + if (fe_type_equal(want,got)) return 1; + return compatible(want,got,value); +} + +/* The magnitude an integer literal spells, ignoring any sign. The same shape + lowering uses on the same text, so the two cannot disagree about what was + written. */ +static unsigned long literal_magnitude(const char *s) +{ + unsigned long v = 0; + unsigned long base = 10UL; + if (!s) return 0; + if (s[0]=='0' && (s[1]=='x' || s[1]=='X')) { base = 16UL; s += 2; } + else if (s[0]=='0' && (s[1]=='b' || s[1]=='B')) { base = 2UL; s += 2; } + else if (s[0]=='0' && (s[1]=='o' || s[1]=='O')) { base = 8UL; s += 2; } + for (; *s; ++s) { + unsigned long d; + if (*s=='_') continue; + if (*s>='0' && *s<='9') d = (unsigned long)(*s-'0'); + else if (*s>='a' && *s<='f') d = (unsigned long)(*s-'a'+10); + else if (*s>='A' && *s<='F') d = (unsigned long)(*s-'A'+10); + else break; + if (d >= base) break; + v = v*base + d; + } + return v; +} + +/* SPEC 4.1: an integer literal takes the type its context asks for, and a + value that does not fit that type is a mistake where it is written rather + than a truncation nobody sees. */ +static int literal_fits(const FeType *want, const char *text, int negative) +{ + unsigned long v; + unsigned long limit; + unsigned bits; + if (!want || want->kind != FE_TYPE_INT || !text) return 1; + bits = want->bits ? want->bits : 32U; + if (bits > 32U) bits = 32U; + v = literal_magnitude(text); + if (want->is_unsigned) { + if (negative) return v == 0UL; + if (bits >= 32U) return 1; + return v <= (1UL << bits) - 1UL; + } + limit = bits >= 32U ? 2147483647UL : (1UL << (bits - 1U)) - 1UL; + return v <= (negative ? limit + 1UL : limit); +} + +/* Is this node a plain integer literal, rather than a character, a string, or + one of the word-shaped literals? */ +static int plain_int_literal(const FeNode *n) +{ + return n && n->kind==FE_N_LITERAL && n->text && + n->text[0]!='\'' && n->text[0]!='"' && + strcmp(n->text,"true") && strcmp(n->text,"false") && + strcmp(n->text,"null") && strcmp(n->text,"undefined"); +} + +FeType *m7_check_expected(FeCheckerState *s, FeNode *value, + FeType *expected) +{ + FeType *actual; + FeM7ContextKind context; + if (!value) return unknown(s->c); + /* `undefined` is not a value, it is the absence of one: it takes whatever + type was asked for, and says the storage starts out unset. Without this + there is no way to declare a buffer larger than you care to type out. */ + if (value->kind==FE_N_LITERAL && value->text && + !strcmp(value->text,"undefined") && expected) { + value->sem_type=expected; + return expected; + } + if (fe_m7_is_null(value)) { + if (!fe_m7_can_contextual_null(expected)) { + err(s->c,value->loc,"null requires a contextual optional type"); + value->sem_type=unknown(s->c); + return value->sem_type; + } + value->sem_type=expected; + value->sem_context=expected; + return expected; + } + /* An integer literal is `i32` on its own; where an integer type is asked + for it is that type instead, and it has to fit in it. */ + if (expected && expected->kind==FE_TYPE_INT) { + FeNode *lit = plain_int_literal(value) ? value : + (value->kind==FE_N_UNARY && value->text && + !strcmp(value->text,"-") && plain_int_literal(value->a) + ? value->a : 0); + if (lit) { + if (!literal_fits(expected, lit->text, lit!=value)) + err(s->c,value->loc,"integer literal out of range for its type"); + lit->sem_type=expected; + value->sem_type=expected; + return expected; + } + } + actual=check_expr(s,value); + if (!expected) return actual; + if (expected->kind==FE_TYPE_OPTIONAL && expected->elem && + m7_actual_compatible(expected->elem,actual,value)) { + value->sem_context=expected; + return expected; + } + if (expected->kind==FE_TYPE_ERROR_UNION) { + context=fe_m7_error_context(&s->c->types,expected,actual); + if (context!=FE_M7_CONTEXT_NONE) { + value->sem_context=expected; + return expected; + } + } + return actual; +} + +FeType *m7_member_field(FeCheckerState *s, FeNode *n, FeType *base) +{ + FeFieldType *field; + FeType *owner; + if (!base) return unknown(s->c); + if (n->text && strcmp(n->text,".?")==0) { + if (base->kind!=FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"optional projection '.?' requires an optional value"); + return unknown(s->c); + } + n->sem_type=base->elem; + return n->sem_type; + } + if (base->kind==FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"optional value must be projected with '.?' first"); + return unknown(s->c); + } + if (base->kind==FE_TYPE_REF && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + n->sem_type=base->elem; + return n->sem_type; + } + if (base->kind==FE_TYPE_OWNED && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + n->sem_type=base->elem; + return n->sem_type; + } + owner=base; + if ((base->kind==FE_TYPE_REF || base->kind==FE_TYPE_OWNED) && + base->elem && base->elem->kind==FE_TYPE_STRUCT) + owner=base->elem; + if (owner && owner->kind==FE_TYPE_STRUCT && n->b && n->b->text) { + field=fe_type_field(owner,n->b->text); + if (!field) { + err(s->c,n->loc,"unknown struct field"); + return unknown(s->c); + } + n->sem_type=field->type; + return field->type; + } + if (base->kind==FE_TYPE_ENUM && n->b && n->b->text) { + if (!fe_type_variant(base,n->b->text)) + err(s->c,n->loc,"unknown enum variant"); + n->sem_type=base; + return base; + } + if ((base->kind==FE_TYPE_SLICE || base->kind==FE_TYPE_STR) && + n->b && n->b->text && strcmp(n->b->text,"n")==0) { + n->sem_type=fe_type_intern(&s->c->types,"usize"); + return n->sem_type; + } + n->sem_type=unknown(s->c); + return n->sem_type; +} + +int m7_place_is_projection(FeNode *n) +{ + return n && (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX); +} + +/* ------------------------------------------------------------------------- * + * Generics (SPEC 9) + * + * A generic declaration is checked once per distinct list of type arguments. + * Those arguments are bound as types for the length of that check, so a name + * that is a type parameter simply is its argument -- in the body, in field + * types and in the signature alike. An instance is identified by its declaring + * unit, its declaration and the spelling of its arguments, so asking twice + * asks for the same instance, and a chain of new ones is bounded. + * ------------------------------------------------------------------------- */ + diff --git a/fec/src/diag.c b/fec/src/diag.c new file mode 100644 index 0000000..53919a3 --- /dev/null +++ b/fec/src/diag.c @@ -0,0 +1,95 @@ +#include "diag.h" + +#include + +FILE *fe_diag_stream(void) +{ + static FILE *stream=0; + if(!stream) stream=getenv("FE_DIAG_STDOUT") ? stdout : stderr; + return stream; +} + +static unsigned long digits(unsigned long n) +{ + unsigned long count=1; + while(n>=10UL){n/=10UL;++count;} + return count; +} + +static void excerpt(const FeDiags *d, FeLoc loc) +{ + const char *src; + unsigned long len; + unsigned long i=0; + unsigned long line=1; + unsigned long start; + unsigned long end; + unsigned long gutter; + unsigned long col; + char c; + if(!d || !d->source || !d->source_len || !loc.line || !loc.col) return; + src=d->source; + len=d->source_len; + while(ilen) return; + start=i; + end=start; + while(endstart) fwrite(src+start,1,(size_t)(end-start),fe_diag_stream()); + fputc('\n',fe_diag_stream()); + fputs(" ",fe_diag_stream()); + for(i=0;ierrors=0; + d->warnings=0; + d->source=source; + d->source_len=source_len; +} + +void fe_diags_source(FeDiags *d, const char *source, unsigned long source_len) +{ + d->source=source; + d->source_len=source_len; +} + +void fe_diag_error(FeDiags *d, FeLoc loc, const char *msg) +{ + d->errors++; + fprintf(fe_diag_stream(), "%s:%lu:%lu: error: %s\n", loc.file ? loc.file : "", loc.line, loc.col, msg); + excerpt(d,loc); +} + +void fe_diag_errorf(FeDiags *d, FeLoc loc, const char *msg, const char *arg) +{ + d->errors++; + fprintf(fe_diag_stream(), "%s:%lu:%lu: error: ", loc.file ? loc.file : "", loc.line, loc.col); + fprintf(fe_diag_stream(), msg, arg); + fputc('\n', fe_diag_stream()); + excerpt(d,loc); +} + +void fe_diag_note(FeLoc loc, const char *msg) +{ + fprintf(fe_diag_stream(), "%s:%lu:%lu: note: %s\n", loc.file ? loc.file : "", loc.line, loc.col, msg); +} + +void fe_diag_note_src(FeDiags *d, FeLoc loc, const char *msg) +{ + fe_diag_note(loc,msg); + excerpt(d,loc); +} diff --git a/fec/src/diag.h b/fec/src/diag.h new file mode 100644 index 0000000..b47f53b --- /dev/null +++ b/fec/src/diag.h @@ -0,0 +1,36 @@ +#ifndef FE_DIAG_H +#define FE_DIAG_H + +#include + +typedef struct FeLoc { + const char *file; + unsigned long line; + unsigned long col; +} FeLoc; + +typedef struct FeDiags { + unsigned long errors; + unsigned long warnings; + const char *source; + unsigned long source_len; +} FeDiags; + +/* Stream diagnostics are written to. Defaults to stderr; returns stdout when + FE_DIAG_STDOUT is set in the environment. DOS offers no way to redirect + handle 2 -- COMMAND.COM understands ">" and nothing else -- so under the test + runner every error message would otherwise be written straight to the screen + and lost. Interactive use is unaffected: both streams reach the console. */ +FILE *fe_diag_stream(void); + +void fe_diags_init(FeDiags *d, const char *source, unsigned long source_len); + +/* Point the excerpt printer at a different file. A build spans several + units, and an excerpt drawn from the wrong buffer is worse than none. */ +void fe_diags_source(FeDiags *d, const char *source, unsigned long source_len); +void fe_diag_error(FeDiags *d, FeLoc loc, const char *msg); +void fe_diag_errorf(FeDiags *d, FeLoc loc, const char *msg, const char *arg); +void fe_diag_note(FeLoc loc, const char *msg); +void fe_diag_note_src(FeDiags *d, FeLoc loc, const char *msg); + +#endif diff --git a/fec/src/driver.c b/fec/src/driver.c new file mode 100644 index 0000000..bc78d11 --- /dev/null +++ b/fec/src/driver.c @@ -0,0 +1,136 @@ +#include "parser.h" +#include "check.h" +#include "resolve.h" +#include "lower.h" +#include "x86.h" +#include "report.h" +#include +#include +#include + +static char *read_file(const char *name, unsigned long *size) +{ + FILE *f; long n; char *p; + f=fopen(name,"rb"); if(!f){fprintf(fe_diag_stream(),"fec: cannot open %s\n",name);return 0;} + if(fseek(f,0L,SEEK_END)!=0){fclose(f);return 0;} n=ftell(f); if(n<0){fclose(f);return 0;} rewind(f); + p=(char *)malloc((unsigned long)n+1); if(!p){fclose(f);return 0;} + if(n && fread(p,1,(size_t)n,f)!=(size_t)n){free(p);fclose(f);return 0;} fclose(f);p[n]='\0';*size=(unsigned long)n;return p; +} + +static void usage(void) +{ + puts("usage: fec [--dump-tokens|--dump-ast|--check|--dump-ir|--emit-asm] file.fe [-o out.asm] [--std=dir] [--no-checks]"); + puts(" fec [--report-unsafe|--report-instances] file.fe [--std=dir]"); +} + +static void dump_tokens(const char *src, unsigned long n, const char *file, + FeDiags *d) +{ + FeLexer lexer; + FeToken tok; + fe_lexer_init(&lexer,src,n,file,d); + do { + tok=fe_lexer_next(&lexer); + fprintf(stdout,"%lu:%lu\t%s\t",tok.loc.line,tok.loc.col, + fe_token_name(tok.kind)); + if(tok.length) fwrite(tok.begin,1,(size_t)tok.length,stdout); + else fputc('-',stdout); + fputc('\n',stdout); + } while(tok.kind!=FE_TOK_EOF); +} + +int main(int argc, char **argv) +{ + int i,dump=0,dump_tok=0,check_only=0,no_checks=0,dump_ir=0,emit_asm=0; + int rep_unsafe=0,rep_inst=0; + const char *file=0; + const char *out_path=0; + const char *std_root=0; + unsigned long n; + char *src; + FeDiags d; + FeAst ast; + FeParser p; + FeCheck check; + unsigned pointer_bits=FE_PTR_BITS; + if(argc<2){usage();return 2;} + for(i=1;i1){ + fprintf(fe_diag_stream(),"fec: choose only one output mode\n"); + return 2; + } + if(!file){fprintf(fe_diag_stream(),"fec: no input file\n");return 2;} + src=read_file(file,&n); + if(!src)return 2; + fe_diags_init(&d,src,n); + if(dump_tok){ + dump_tokens(src,n,file,&d); + free(src); + return d.errors?1:0; + } + fe_ast_init(&ast); + fe_parser_init(&p,&ast,src,n,file,&d); + ast.root=fe_parse_unit(&p); + if(dump){ + fe_ast_dump(ast.root,0,stdout); + fe_ast_destroy(&ast); + free(src); + return d.errors?1:0; + } + fe_ast_destroy(&ast); + free(src); + src=0; + /* Load the whole unit graph rooted at this file: identity, imports, + cycles and bindings. The entry file is parsed a second time as part of + it, which costs one file read and keeps the graph the single owner of + every unit's AST. */ + { + FeBuild build; + int ok=fe_build_load(&build,file,&d,std_root); + if(ok){ + fe_check_init(&check,&build,&d,pointer_bits,no_checks); + if(!fe_check_program(&check)) ok=0; + /* Reports describe the program that was checked, so they come + after checking and instead of code generation. */ + if(rep_unsafe) fe_report_unsafe(&build,stdout); + if(rep_inst) fe_report_instances(&check,stdout); + if(rep_unsafe||rep_inst) { dump_ir=0; emit_asm=0; } + if(ok && (dump_ir||emit_asm)){ + FeIrModule ir; + fe_ir_module_init(&ir); + if(!fe_lower_program(&check,&ir)) ok=0; + else if(dump_ir) fe_ir_dump(&ir,stdout); + else { + FILE *o=out_path?fopen(out_path,"w"):stdout; + if(!o){fprintf(fe_diag_stream(),"fec: cannot write %s\n",out_path);ok=0;} + else { fe_x86_emit(&ir,o); if(out_path) fclose(o); } + } + fe_ir_module_destroy(&ir); + } + fe_check_destroy(&check); + } + fe_build_destroy(&build); + /* Semantic analysis is the last pass there is. A code generator + attaches here; until then --check and the default path agree. */ + (void)check_only; + return (!ok||d.errors)?1:0; + } +} diff --git a/fec/src/ir.c b/fec/src/ir.c new file mode 100644 index 0000000..5ee0072 --- /dev/null +++ b/fec/src/ir.c @@ -0,0 +1,493 @@ +#include "ir.h" +#include +#include + +void fe_ir_module_init(FeIrModule *m) +{ + fe_arena_init(&m->arena, 16384); + m->file_count = 0; + m->entry_main = 0; + m->funcs = 0; + m->last_func = 0; + m->globals = 0; + m->last_global = 0; +} + +/* The index of this path in the module's file table, adding it if it is new. + Traps carry the index rather than the string so the generator emits each + name once. */ +unsigned fe_ir_file(FeIrModule *m, const char *path) +{ + unsigned i; + if (!path) path = ""; + for (i = 0; i < m->file_count; ++i) + if (!strcmp(m->files[i], path)) return i; + if (m->file_count >= FE_IR_FILE_MAX) return 0; + m->files[m->file_count] = path; + return m->file_count++; +} + +void fe_ir_module_destroy(FeIrModule *m) +{ + fe_arena_destroy(&m->arena); + m->funcs = 0; + m->last_func = 0; + m->globals = 0; + m->last_global = 0; +} + +static void *ir_alloc(FeIrModule *m, unsigned long size) +{ + return fe_arena_alloc(&m->arena, (size_t)size); +} + +FeIrFunc *fe_ir_func(FeIrModule *m, const char *name, FeIrType ret, + unsigned long ret_size) +{ + FeIrFunc *f = (FeIrFunc *)ir_alloc(m, sizeof(FeIrFunc)); + if (!f) return 0; + memset(f, 0, sizeof *f); + f->name = name; + f->ret = ret; + f->ret_size = ret_size; + /* An aggregate result is written through a hidden first parameter, so the + caller owns the storage and no size threshold has to be agreed on. */ + f->returns_by_address = ret == FE_IR_MEM; + if (m->last_func) m->last_func->next = f; + else m->funcs = f; + m->last_func = f; + return f; +} + +unsigned fe_ir_local(FeIrModule *m, FeIrFunc *f, FeIrType type, + unsigned long size, unsigned align, const char *name) +{ + if (f->local_count == f->local_capacity) { + unsigned cap = f->local_capacity ? f->local_capacity * 2U : 8U; + FeIrLocal *grown = (FeIrLocal *)ir_alloc(m, cap * sizeof(FeIrLocal)); + if (!grown) return 0; + if (f->locals) + memcpy(grown, f->locals, f->local_count * sizeof(FeIrLocal)); + f->locals = grown; + f->local_capacity = cap; + } + f->locals[f->local_count].type = type; + f->locals[f->local_count].size = size; + f->locals[f->local_count].align = align ? align : 1U; + f->locals[f->local_count].name = name; + return f->local_count++; +} + +unsigned fe_ir_temp(FeIrFunc *f) +{ + return f->temp_count++; +} + +FeIrBlock *fe_ir_block(FeIrModule *m, FeIrFunc *f) +{ + FeIrBlock *b = (FeIrBlock *)ir_alloc(m, sizeof(FeIrBlock)); + if (!b) return 0; + memset(b, 0, sizeof *b); + b->id = f->block_count++; + b->func = f; + /* Until something says otherwise a block falls off the end, which is only + correct for a void function; lowering always sets a real terminator. */ + b->term = FE_IR_RET; + if (f->last) f->last->next = b; + else f->first = b; + f->last = b; + return b; +} + +FeIrGlobal *fe_ir_global(FeIrModule *m, const char *name, FeIrType type, + unsigned long size, unsigned align, + const unsigned char *init) +{ + FeIrGlobal *g; + for (g = m->globals; g; g = g->next) + if (!strcmp(g->name, name)) return g; + g = (FeIrGlobal *)ir_alloc(m, sizeof(FeIrGlobal)); + if (!g) return 0; + memset(g, 0, sizeof *g); + g->name = name; + g->type = type; + g->size = size; + g->align = align ? align : 1U; + g->init = init; + if (m->last_global) m->last_global->next = g; + else m->globals = g; + m->last_global = g; + return g; +} + +void fe_ir_global_ref(FeIrModule *m, FeIrGlobal *g, unsigned long at, + const char *symbol) +{ + FeIrReloc *grown; + if (!g) return; + grown = (FeIrReloc *)ir_alloc(m, (g->reloc_count + 1) * sizeof(FeIrReloc)); + if (!grown) return; + if (g->relocs) memcpy(grown, g->relocs, g->reloc_count * sizeof(FeIrReloc)); + grown[g->reloc_count].at = at; + grown[g->reloc_count].symbol = symbol; + g->relocs = grown; + ++g->reloc_count; +} + +const char *fe_ir_string(FeIrModule *m, const char *bytes, unsigned long length) +{ + FeIrGlobal *g; + unsigned char *copy; + char *name; + unsigned serial = 0; + /* The same text twice is the same storage: string literals are read-only, + so sharing them is free. */ + for (g = m->globals; g; g = g->next) { + if (g->init && g->size == length && + !memcmp(g->init, bytes, (size_t)length)) return g->name; + ++serial; + } + copy = (unsigned char *)ir_alloc(m, length ? length : 1UL); + if (!copy) return 0; + if (length) memcpy(copy, bytes, (size_t)length); + name = (char *)ir_alloc(m, 32); + if (!name) return 0; + sprintf(name, "FE_STR_%u", serial); + g = fe_ir_global(m, name, FE_IR_MEM, length, 1, copy); + return g ? g->name : 0; +} + +FeIrPlace fe_ir_at_local(unsigned index, long offset) +{ + FeIrPlace p; + p.base = FE_PLACE_LOCAL; p.index = index; p.name = 0; p.offset = offset; + return p; +} + +FeIrPlace fe_ir_at_global(const char *name, long offset) +{ + FeIrPlace p; + p.base = FE_PLACE_GLOBAL; p.index = 0; p.name = name; p.offset = offset; + return p; +} + +FeIrPlace fe_ir_at_temp(unsigned temp, long offset) +{ + FeIrPlace p; + p.base = FE_PLACE_TEMP; p.index = temp; p.name = 0; p.offset = offset; + return p; +} + +static FeIrValue *emit(FeIrModule *m, FeIrBlock *b, FeIrOp op, FeIrType t) +{ + FeIrValue *v = (FeIrValue *)ir_alloc(m, sizeof(FeIrValue)); + if (!v) return 0; + memset(v, 0, sizeof *v); + v->op = op; + v->type = t; + if (b->last) b->last->next = v; + else b->first = v; + b->last = v; + return v; +} + +/* A result needs a fresh temporary, and the counter lives on the function, so + a block carries the function it is being built in. */ +static unsigned result(FeIrModule *m, FeIrBlock *b, FeIrValue *v) +{ + (void)m; + v->has_dest = 1; + v->dest = fe_ir_temp(b->func); + return v->dest; +} + +unsigned fe_ir_const(FeIrModule *m, FeIrBlock *b, FeIrType t, long value) +{ + FeIrValue *v = emit(m, b, FE_IR_CONST, t); + if (!v) return 0; + v->imm = value; + return result(m, b, v); +} + +unsigned fe_ir_load(FeIrModule *m, FeIrBlock *b, FeIrType t, FeIrPlace p) +{ + FeIrValue *v = emit(m, b, FE_IR_LOAD, t); + if (!v) return 0; + v->place = p; + return result(m, b, v); +} + +void fe_ir_store(FeIrModule *m, FeIrBlock *b, FeIrPlace p, unsigned value, + FeIrType t) +{ + FeIrValue *v = emit(m, b, FE_IR_STORE, t); + if (!v) return; + v->place = p; + v->a = value; +} + +unsigned fe_ir_addr(FeIrModule *m, FeIrBlock *b, FeIrPlace p) +{ + FeIrValue *v = emit(m, b, FE_IR_ADDR, FE_IR_PTR); + if (!v) return 0; + v->place = p; + return result(m, b, v); +} + +unsigned fe_ir_binary(FeIrModule *m, FeIrBlock *b, FeIrOp op, FeIrType t, + unsigned a, unsigned c, int is_unsigned) +{ + FeIrValue *v; + int is_cmp = op >= FE_IR_EQ && op <= FE_IR_GE; + v = emit(m, b, op, is_cmp ? FE_IR_I8 : t); + if (!v) return 0; + v->a = a; + v->b = c; + v->is_unsigned = is_unsigned; + /* A comparison reports i8 but reads its operands at `t`, so the width has + to survive somewhere the backend can see it. */ + if (is_cmp) v->imm = (long)t; + return result(m, b, v); +} + +unsigned fe_ir_cast(FeIrModule *m, FeIrBlock *b, FeIrType from, FeIrType to, + unsigned a, int is_unsigned) +{ + FeIrValue *v = emit(m, b, FE_IR_CAST, to); + if (!v) return 0; + v->a = a; + v->imm = (long)from; + v->is_unsigned = is_unsigned; + return result(m, b, v); +} + +unsigned fe_ir_call(FeIrModule *m, FeIrBlock *b, FeIrType ret, + const char *callee, unsigned *args, unsigned count) +{ + FeIrValue *v = emit(m, b, FE_IR_CALL, ret); + unsigned i; + if (!v) return 0; + v->callee = callee; + v->arg_count = count; + if (count) { + v->args = (unsigned *)ir_alloc(m, count * sizeof(unsigned)); + if (v->args) for (i = 0; i < count; ++i) v->args[i] = args[i]; + else v->arg_count = 0; + } + if (ret == FE_IR_VOID) return 0; + return result(m, b, v); +} + +void fe_ir_copy(FeIrModule *m, FeIrBlock *b, FeIrPlace dst, FeIrPlace src, + unsigned long size) +{ + FeIrValue *v = emit(m, b, FE_IR_COPY, FE_IR_VOID); + if (!v) return; + v->place = dst; + v->place2 = src; + v->imm = (long)size; +} + +void fe_ir_jmp(FeIrBlock *b, unsigned target) +{ + if (b->terminated) return; + b->terminated = 1; + b->term = FE_IR_JMP; + b->target = target; +} + +void fe_ir_br(FeIrBlock *b, unsigned cond, unsigned t, unsigned f) +{ + if (b->terminated) return; + b->terminated = 1; + b->term = FE_IR_BR; + b->cond = cond; + b->target = t; + b->target_else = f; +} + +void fe_ir_ret(FeIrBlock *b, unsigned value, int has_value) +{ + if (b->terminated) return; + b->terminated = 1; + b->term = FE_IR_RET; + b->ret_value = value; + b->has_ret_value = has_value; +} + +void fe_ir_trap(FeIrBlock *b, FeIrTrap reason, unsigned long line, + unsigned file) +{ + if (b->terminated) return; + b->terminated = 1; + b->term = FE_IR_TRAP; + b->trap = reason; + b->trap_line = line; + b->trap_file = file; +} + +const char *fe_ir_type_name(FeIrType t) +{ + switch (t) { + case FE_IR_VOID: return "void"; + case FE_IR_I8: return "i8"; + case FE_IR_I16: return "i16"; + case FE_IR_I32: return "i32"; + case FE_IR_PTR: return "ptr"; + case FE_IR_MEM: return "mem"; + } + return "?"; +} + +const char *fe_ir_op_name(FeIrOp op) +{ + switch (op) { + case FE_IR_CONST: return "const"; + case FE_IR_LOAD: return "load"; + case FE_IR_STORE: return "store"; + case FE_IR_ADDR: return "addr"; + case FE_IR_ADD: return "add"; + case FE_IR_SUB: return "sub"; + case FE_IR_MUL: return "mul"; + case FE_IR_DIV: return "div"; + case FE_IR_MOD: return "mod"; + case FE_IR_AND: return "and"; + case FE_IR_OR: return "or"; + case FE_IR_XOR: return "xor"; + case FE_IR_SHL: return "shl"; + case FE_IR_SHR: return "shr"; + case FE_IR_EQ: return "eq"; + case FE_IR_NE: return "ne"; + case FE_IR_LT: return "lt"; + case FE_IR_LE: return "le"; + case FE_IR_GT: return "gt"; + case FE_IR_GE: return "ge"; + case FE_IR_CAST: return "cast"; + case FE_IR_CALL: return "call"; + case FE_IR_COPY: return "copy"; + } + return "?"; +} + +static const char *trap_name(FeIrTrap t) +{ + switch (t) { + case FE_TRAP_BOUNDS: return "bounds"; + case FE_TRAP_OVERFLOW: return "overflow"; + case FE_TRAP_DIVIDE: return "divide"; + case FE_TRAP_UNREACHABLE: return "unreachable"; + case FE_TRAP_EXPLICIT: return "trap"; + } + return "?"; +} + +static void dump_place(const FeIrPlace *p, FILE *out) +{ + switch (p->base) { + case FE_PLACE_LOCAL: fprintf(out, "$%u", p->index); break; + case FE_PLACE_GLOBAL: fprintf(out, "@%s", p->name ? p->name : "?"); break; + case FE_PLACE_TEMP: fprintf(out, "%%%u", p->index); break; + } + if (p->offset) fprintf(out, " + %ld", p->offset); +} + +static void dump_value(const FeIrValue *v, FILE *out) +{ + unsigned i; + fputs(" ", out); + if (v->has_dest) fprintf(out, "%%%u = ", v->dest); + switch (v->op) { + case FE_IR_CONST: + fprintf(out, "const %s %ld", fe_ir_type_name(v->type), v->imm); + break; + case FE_IR_LOAD: + fprintf(out, "load %s ", fe_ir_type_name(v->type)); + dump_place(&v->place, out); + break; + case FE_IR_STORE: + fputs("store ", out); + dump_place(&v->place, out); + fprintf(out, ", %%%u", v->a); + break; + case FE_IR_ADDR: + fputs("addr ", out); + dump_place(&v->place, out); + break; + case FE_IR_CAST: + fprintf(out, "cast %s %s %%%u", + fe_ir_type_name((FeIrType)v->imm), + fe_ir_type_name(v->type), v->a); + break; + case FE_IR_CALL: + fprintf(out, "call @%s(", v->callee ? v->callee : "?"); + for (i = 0; i < v->arg_count; ++i) + fprintf(out, "%s%%%u", i ? ", " : "", v->args[i]); + fputc(')', out); + break; + case FE_IR_COPY: + fputs("copy ", out); + dump_place(&v->place, out); + fputs(", ", out); + dump_place(&v->place2, out); + fprintf(out, ", %ld", v->imm); + break; + default: + fprintf(out, "%s %s %%%u, %%%u", fe_ir_op_name(v->op), + fe_ir_type_name(v->op >= FE_IR_EQ && v->op <= FE_IR_GE ? + (FeIrType)v->imm : v->type), v->a, v->b); + if (v->is_unsigned) fputs(" u", out); + break; + } + fputc('\n', out); +} + +void fe_ir_dump(const FeIrModule *m, FILE *out) +{ + const FeIrFunc *f; + const FeIrBlock *b; + const FeIrValue *v; + const FeIrGlobal *g; + unsigned i; + for (i = 0; i < m->file_count; ++i) + fprintf(out, "; file %u %s\n", i, m->files[i]); + for (g = m->globals; g; g = g->next) + fprintf(out, "global @%s : %s %lu\n", g->name, + fe_ir_type_name(g->type), g->size); + for (f = m->funcs; f; f = f->next) { + if (f->is_extern) { + fprintf(out, "extern fn @%s -> %s\n", f->name, + fe_ir_type_name(f->ret)); + continue; + } + fprintf(out, "fn @%s -> %s%s {\n", f->name, fe_ir_type_name(f->ret), + f->returns_by_address ? " (by address)" : ""); + for (i = 0; i < f->local_count; ++i) { + fprintf(out, " $%u: %s", i, fe_ir_type_name(f->locals[i].type)); + if (f->locals[i].type == FE_IR_MEM) + fprintf(out, "<%lu>", f->locals[i].size); + if (i < f->param_count) fputs(" ; parameter", out); + if (f->locals[i].name) fprintf(out, " ; %s", f->locals[i].name); + fputc('\n', out); + } + for (b = f->first; b; b = b->next) { + fprintf(out, " b%u:\n", b->id); + for (v = b->first; v; v = v->next) dump_value(v, out); + switch (b->term) { + case FE_IR_JMP: + fprintf(out, " jmp b%u\n", b->target); break; + case FE_IR_BR: + fprintf(out, " br %%%u, b%u, b%u\n", b->cond, b->target, + b->target_else); break; + case FE_IR_RET: + if (b->has_ret_value) fprintf(out, " ret %%%u\n", b->ret_value); + else fputs(" ret\n", out); + break; + case FE_IR_TRAP: + fprintf(out, " trap %s %lu\n", trap_name(b->trap), + b->trap_line); + break; + } + } + fputs("}\n", out); + } +} diff --git a/fec/src/ir.h b/fec/src/ir.h new file mode 100644 index 0000000..8801844 --- /dev/null +++ b/fec/src/ir.h @@ -0,0 +1,216 @@ +#ifndef FE_IR_H +#define FE_IR_H + +#include "arena.h" +#include + +/* How many source files one build can trap from. */ +#define FE_IR_FILE_MAX 64 + +/* The intermediate representation. `IR.md` is the description; this is the + shape it takes in memory. + + Ferro types do not survive into here. A struct, a slice, an optional and an + error union are all `mem`, and a field is a byte offset that lowering + worked out. The machine types are what a register can hold plus a size. */ + +typedef enum FeIrType { + FE_IR_VOID, + FE_IR_I8, FE_IR_I16, FE_IR_I32, + FE_IR_PTR, + FE_IR_MEM /* size lives on the value or slot */ +} FeIrType; + +typedef enum FeIrOp { + FE_IR_CONST, FE_IR_LOAD, FE_IR_STORE, FE_IR_ADDR, + FE_IR_ADD, FE_IR_SUB, FE_IR_MUL, FE_IR_DIV, FE_IR_MOD, + FE_IR_AND, FE_IR_OR, FE_IR_XOR, FE_IR_SHL, FE_IR_SHR, + FE_IR_EQ, FE_IR_NE, FE_IR_LT, FE_IR_LE, FE_IR_GT, FE_IR_GE, + FE_IR_CAST, FE_IR_CALL, FE_IR_COPY +} FeIrOp; + +typedef enum FeIrTerm { + FE_IR_JMP, FE_IR_BR, FE_IR_RET, FE_IR_TRAP +} FeIrTerm; + +/* Why a program stopped. Kept small and stable: it is a number in the + executable, and the runtime turns it back into words. */ +typedef enum FeIrTrap { + FE_TRAP_BOUNDS = 0, + FE_TRAP_OVERFLOW = 1, + FE_TRAP_DIVIDE = 2, + FE_TRAP_UNREACHABLE = 3, + FE_TRAP_EXPLICIT = 4 +} FeIrTrap; + +/* Where an instruction reads or writes. A place is a base plus a constant + offset; anything computed goes through a pointer temporary instead. */ +typedef enum FeIrBase { + FE_PLACE_LOCAL, /* $n */ + FE_PLACE_GLOBAL, /* @name */ + FE_PLACE_TEMP /* %p */ +} FeIrBase; + +typedef struct FeIrPlace { + FeIrBase base; + unsigned index; /* local or temp number */ + const char *name; /* global name */ + long offset; +} FeIrPlace; + +typedef struct FeIrValue { + FeIrOp op; + FeIrType type; + unsigned dest; /* %dest, or 0 when the op has no result */ + int has_dest; + /* operands, by role -- only the ones the op uses are set */ + unsigned a, b; /* temporaries */ + long imm; /* const, cast width, copy size */ + FeIrPlace place; /* load / store / addr / copy destination */ + FeIrPlace place2; /* copy source */ + const char *callee; + unsigned *args; + unsigned arg_count; + int is_unsigned; /* picks the signed or unsigned instruction */ + unsigned long line; /* for diagnostics that survive into the backend */ + struct FeIrValue *next; +} FeIrValue; + +typedef struct FeIrFunc FeIrFunc; + +typedef struct FeIrBlock { + unsigned id; + FeIrFunc *func; /* the function this block is being built in */ + FeIrValue *first; + FeIrValue *last; + FeIrTerm term; + unsigned cond; /* br */ + unsigned target; /* jmp, br true */ + unsigned target_else; /* br false */ + unsigned ret_value; /* ret */ + int has_ret_value; + FeIrTrap trap; + unsigned long trap_line; + unsigned trap_file; /* index into the module's file table */ + /* Set once a terminator is chosen. Lowering asks before appending a + jump, so a `return` inside a branch is not overwritten by the jump + to the join block. */ + int terminated; + struct FeIrBlock *next; +} FeIrBlock; + +typedef struct FeIrLocal { + FeIrType type; + unsigned long size; /* for FE_IR_MEM */ + unsigned align; + const char *name; /* the Ferro name, for reading the dump */ +} FeIrLocal; + +struct FeIrFunc { + const char *name; /* unit.name */ + FeIrType ret; + unsigned long ret_size; /* when ret is FE_IR_MEM */ + /* A function returning mem takes the address to write as a hidden + first parameter, so the caller owns the storage. */ + int returns_by_address; + FeIrLocal *locals; + unsigned local_count; + unsigned local_capacity; + unsigned param_count; /* the first `param_count` locals are parameters */ + unsigned temp_count; + FeIrBlock *first; + FeIrBlock *last; + unsigned block_count; + int is_extern; + struct FeIrFunc *next; +}; + +/* A place inside a global's bytes that holds the address of something else. + The value is not known until the linker places it, so the bytes carry a hole + and this says what fills it. */ +typedef struct FeIrReloc { + unsigned long at; + const char *symbol; +} FeIrReloc; + +typedef struct FeIrGlobal { + const char *name; + FeIrType type; + unsigned long size; + unsigned align; + const unsigned char *init; /* size bytes, or null for zero */ + FeIrReloc *relocs; + unsigned reloc_count; + struct FeIrGlobal *next; +} FeIrGlobal; + +typedef struct FeIrModule { + FeArena arena; + /* Every unit in the build lands in one module, so a trap has to say which + file it came from rather than share one name with the whole program. */ + const char *files[FE_IR_FILE_MAX]; + unsigned file_count; + /* The entry unit's `main`, if it has one. The runtime's start stub + calls a fixed name, so the generator emits a jump to this one. */ + const char *entry_main; + FeIrFunc *funcs; + FeIrFunc *last_func; + FeIrGlobal *globals; + FeIrGlobal *last_global; +} FeIrModule; + +unsigned fe_ir_file(FeIrModule *m, const char *path); +void fe_ir_module_init(FeIrModule *m); +void fe_ir_module_destroy(FeIrModule *m); + +FeIrFunc *fe_ir_func(FeIrModule *m, const char *name, FeIrType ret, + unsigned long ret_size); +unsigned fe_ir_local(FeIrModule *m, FeIrFunc *f, FeIrType type, + unsigned long size, unsigned align, const char *name); +unsigned fe_ir_temp(FeIrFunc *f); +FeIrBlock *fe_ir_block(FeIrModule *m, FeIrFunc *f); +/* Static storage. `init` is `size` bytes to place there, or null for zero. */ +FeIrGlobal *fe_ir_global(FeIrModule *m, const char *name, FeIrType type, + unsigned long size, unsigned align, + const unsigned char *init); +/* Say that `at` bytes into `g` there is the address of `symbol`. */ +void fe_ir_global_ref(FeIrModule *m, FeIrGlobal *g, unsigned long at, + const char *symbol); +/* A string literal's bytes, interned so the same text is stored once. */ +const char *fe_ir_string(FeIrModule *m, const char *bytes, + unsigned long length); + +/* Places */ +FeIrPlace fe_ir_at_local(unsigned index, long offset); +FeIrPlace fe_ir_at_global(const char *name, long offset); +FeIrPlace fe_ir_at_temp(unsigned temp, long offset); + +/* Instructions. Each returns the destination temporary where there is one. */ +unsigned fe_ir_const(FeIrModule *m, FeIrBlock *b, FeIrType t, long v); +unsigned fe_ir_load(FeIrModule *m, FeIrBlock *b, FeIrType t, FeIrPlace p); +/* `t` is how wide the write is. Without it a one-byte value would be stored + four bytes wide and take its neighbours with it. */ +void fe_ir_store(FeIrModule *m, FeIrBlock *b, FeIrPlace p, unsigned v, + FeIrType t); +unsigned fe_ir_addr(FeIrModule *m, FeIrBlock *b, FeIrPlace p); +unsigned fe_ir_binary(FeIrModule *m, FeIrBlock *b, FeIrOp op, FeIrType t, + unsigned a, unsigned c, int is_unsigned); +unsigned fe_ir_cast(FeIrModule *m, FeIrBlock *b, FeIrType from, FeIrType to, + unsigned a, int is_unsigned); +unsigned fe_ir_call(FeIrModule *m, FeIrBlock *b, FeIrType ret, + const char *callee, unsigned *args, unsigned count); +void fe_ir_copy(FeIrModule *m, FeIrBlock *b, FeIrPlace dst, FeIrPlace src, + unsigned long size); + +/* Terminators */ +void fe_ir_jmp(FeIrBlock *b, unsigned target); +void fe_ir_br(FeIrBlock *b, unsigned cond, unsigned t, unsigned f); +void fe_ir_ret(FeIrBlock *b, unsigned value, int has_value); +void fe_ir_trap(FeIrBlock *b, FeIrTrap reason, unsigned long line, + unsigned file); + +void fe_ir_dump(const FeIrModule *m, FILE *out); +const char *fe_ir_type_name(FeIrType t); +const char *fe_ir_op_name(FeIrOp op); + +#endif diff --git a/fec/src/lexer.c b/fec/src/lexer.c new file mode 100644 index 0000000..20866d7 --- /dev/null +++ b/fec/src/lexer.c @@ -0,0 +1,172 @@ +#include "lexer.h" +#include +#include + +typedef struct FeKw { const char *s; FeTokKind k; } FeKw; +static const FeKw keywords[] = { + {"unit",FE_TOK_UNIT},{"import",FE_TOK_IMPORT},{"pub",FE_TOK_PUB},{"fn",FE_TOK_FN}, + {"struct",FE_TOK_STRUCT},{"enum",FE_TOK_ENUM},{"error",FE_TOK_ERROR_KW},{"const",FE_TOK_CONST}, + {"static",FE_TOK_STATIC},{"var",FE_TOK_VAR},{"let",FE_TOK_LET},{"mut",FE_TOK_MUT}, + {"if",FE_TOK_IF},{"else",FE_TOK_ELSE},{"while",FE_TOK_WHILE},{"for",FE_TOK_FOR},{"in",FE_TOK_IN}, + {"match",FE_TOK_MATCH},{"return",FE_TOK_RETURN},{"break",FE_TOK_BREAK},{"continue",FE_TOK_CONTINUE}, + {"defer",FE_TOK_DEFER},{"unsafe",FE_TOK_UNSAFE},{"comptime",FE_TOK_COMPTIME},{"asm",FE_TOK_ASM}, + {"try",FE_TOK_TRY},{"catch",FE_TOK_CATCH},{"as",FE_TOK_AS},{"extern",FE_TOK_EXTERN}, + {"interrupt",FE_TOK_INTERRUPT},{"interrupt_safe",FE_TOK_INTERRUPT_SAFE}, + {"true",FE_TOK_TRUE},{"false",FE_TOK_FALSE},{"null",FE_TOK_NULL},{"undefined",FE_TOK_UNDEFINED}, + {"shared",FE_TOK_SHARED},{"atomic",FE_TOK_ATOMIC},{"critical",FE_TOK_CRITICAL}, + {"self",FE_TOK_SELF},{"Self",FE_TOK_SELFTYPE},{"type",FE_TOK_TYPE}, + {"packed",FE_TOK_PACKED},{"orelse",FE_TOK_ORELSE},{"and",FE_TOK_AND_KW},{"or",FE_TOK_OR_KW},{"not",FE_TOK_NOT}, + {0,FE_TOK_UNKNOWN} +}; + +static int at(FeLexer *l, unsigned long n, char c) { return l->pos + n < l->length && l->src[l->pos+n] == c; } +static FeLoc here(FeLexer *l, unsigned long line, unsigned long col) +{ FeLoc x; x.file=l->file; x.line=line; x.col=col; return x; } +static char cur(FeLexer *l) { return l->pos < l->length ? l->src[l->pos] : '\0'; } +static void advance(FeLexer *l) +{ + if (l->pos >= l->length) return; + if (l->src[l->pos] == '\n') { l->line++; l->col = 1; } + else l->col++; + l->pos++; +} +static void skip_space(FeLexer *l) +{ + for (;;) { + while (isspace((unsigned char)cur(l))) advance(l); + if (at(l,0,'/') && at(l,1,'/')) { + while (cur(l) && cur(l) != '\n') advance(l); + continue; + } + if (at(l,0,'/') && at(l,1,'*')) { + unsigned long depth = 0; + advance(l); advance(l); depth = 1; + while (depth && cur(l)) { + if (at(l,0,'/') && at(l,1,'*')) { advance(l); advance(l); depth++; } + else if (at(l,0,'*') && at(l,1,'/')) { advance(l); advance(l); depth--; } + else advance(l); + } + if (depth) fe_diag_error(l->diags, here(l,l->line,l->col), "unterminated block comment"); + continue; + } + break; + } +} + +void fe_lexer_init(FeLexer *l, const char *src, unsigned long length, const char *file, FeDiags *d) +{ + l->src=src; l->length=length; l->pos=0; l->line=1; l->col=1; l->file=file; l->diags=d; +} + +static FeTokKind keyword(const char *s, unsigned long n) +{ + unsigned long i; + for (i=0; keywords[i].s; i++) { + if (strlen(keywords[i].s)==n && memcmp(keywords[i].s,s,n)==0) return keywords[i].k; + } + return FE_TOK_IDENT; +} +static FeToken tok(FeLexer *l, FeTokKind k, unsigned long start, unsigned long line, unsigned long col) +{ + FeToken t; t.kind=k; t.begin=l->src+start; t.length=l->pos-start; t.loc.file=l->file; t.loc.line=line; t.loc.col=col; return t; +} +static int digit_for_base(char c, int base) +{ + int d; + if (c >= '0' && c <= '9') d=c-'0'; + else if (c >= 'a' && c <= 'f') d=c-'a'+10; + else if (c >= 'A' && c <= 'F') d=c-'A'+10; + else return 0; + return d < base; +} + +FeToken fe_lexer_next(FeLexer *l) +{ + unsigned long start, line, col; + char c; + skip_space(l); + start=l->pos; line=l->line; col=l->col; c=cur(l); + if (!c) return tok(l,FE_TOK_EOF,start,line,col); + if (isalpha((unsigned char)c) || c=='_') { + advance(l); + while (isalnum((unsigned char)cur(l)) || cur(l)=='_') advance(l); + return tok(l,keyword(l->src+start,l->pos-start),start,line,col); + } + if (isdigit((unsigned char)c)) { + int base=10, had_digit=0; + if (c=='0' && (at(l,1,'x') || at(l,1,'X'))) { advance(l); advance(l); base=16; } + else if (c=='0' && (at(l,1,'b') || at(l,1,'B'))) { advance(l); advance(l); base=2; } + else if (c=='0' && (at(l,1,'o') || at(l,1,'O'))) { advance(l); advance(l); base=8; } + while (cur(l)=='_' || digit_for_base(cur(l),base)) { if(cur(l)!='_') had_digit=1; advance(l); } + if (!had_digit) fe_diag_error(l->diags,here(l,line,col),"integer literal has no digits"); + if (isalnum((unsigned char)cur(l))) { + fe_diag_error(l->diags,here(l,line,col),"invalid digit in integer literal"); + while (isalnum((unsigned char)cur(l)) || cur(l)=='_') advance(l); + } + return tok(l,FE_TOK_INT,start,line,col); + } + if (c=='\'' || c=='"') { + char quote=c; int bad=0, units=0; advance(l); + while (cur(l) && cur(l)!=quote) { + if (cur(l)=='\n' || cur(l)=='\r') { bad=1; break; } + units++; + if (cur(l)=='\\') { + advance(l); + if (!cur(l)) { bad=1; break; } + if (cur(l)=='x') { int i; advance(l); for(i=0;i<2;i++) { if(!digit_for_base(cur(l),16)) bad=1; else advance(l); } } + else if (cur(l)=='u') { int i; advance(l); for(i=0;i<4;i++) { if(!digit_for_base(cur(l),16)) bad=1; else advance(l); } } + else if (strchr("nrt\\'\"0",cur(l))) advance(l); + else { bad=1; advance(l); } + } else advance(l); + } + if (cur(l)==quote) advance(l); else bad=1; + if (quote=='\'' && units != 1) bad=1; + if (bad) fe_diag_error(l->diags,here(l,line,col),quote=='\''?"invalid character literal":"unterminated or invalid string literal"); + return tok(l,quote=='\''?FE_TOK_CHAR:FE_TOK_STRING,start,line,col); + } + advance(l); + switch(c) { + case '(': return tok(l,FE_TOK_LPAREN,start,line,col); case ')': return tok(l,FE_TOK_RPAREN,start,line,col); + case '{': return tok(l,FE_TOK_LBRACE,start,line,col); case '}': return tok(l,FE_TOK_RBRACE,start,line,col); + case '[': return tok(l,FE_TOK_LBRACKET,start,line,col); case ']': return tok(l,FE_TOK_RBRACKET,start,line,col); + case ',': return tok(l,FE_TOK_COMMA,start,line,col); case ';': return tok(l,FE_TOK_SEMI,start,line,col); + case ':': return tok(l,FE_TOK_COLON,start,line,col); case '@': return tok(l,FE_TOK_AT,start,line,col); + case '?': return tok(l,FE_TOK_QUESTION,start,line,col); + case '.': if (cur(l)=='.') { advance(l); return tok(l,FE_TOK_DOTDOT,start,line,col); } return tok(l,FE_TOK_DOT,start,line,col); + case '+': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_PLUS_EQ,start,line,col);} if(cur(l)=='%'){advance(l);return tok(l,FE_TOK_PLUS_WRAP,start,line,col);} return tok(l,FE_TOK_PLUS,start,line,col); + case '-': if(cur(l)=='>'){advance(l);return tok(l,FE_TOK_ARROW,start,line,col);} if(cur(l)=='='){advance(l);return tok(l,FE_TOK_MINUS_EQ,start,line,col);} if(cur(l)=='%'){advance(l);return tok(l,FE_TOK_MINUS_WRAP,start,line,col);} return tok(l,FE_TOK_MINUS,start,line,col); + case '*': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_STAR_EQ,start,line,col);} if(cur(l)=='%'){advance(l);return tok(l,FE_TOK_STAR_WRAP,start,line,col);} return tok(l,FE_TOK_STAR,start,line,col); + case '/': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_SLASH_EQ,start,line,col);} return tok(l,FE_TOK_SLASH,start,line,col); + case '%': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_PERCENT_EQ,start,line,col);} return tok(l,FE_TOK_PERCENT,start,line,col); + case '=': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_EQEQ,start,line,col);} if(cur(l)=='>'){advance(l);return tok(l,FE_TOK_FATARROW,start,line,col);} return tok(l,FE_TOK_EQ,start,line,col); + case '!': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_NE,start,line,col);} return tok(l,FE_TOK_BANG,start,line,col); + case '<': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_LE,start,line,col);} if(cur(l)=='<'){advance(l);if(cur(l)=='='){advance(l);return tok(l,FE_TOK_SHL_EQ,start,line,col);}return tok(l,FE_TOK_SHL,start,line,col);} return tok(l,FE_TOK_LT,start,line,col); + case '>': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_GE,start,line,col);} if(cur(l)=='>'){advance(l);if(cur(l)=='='){advance(l);return tok(l,FE_TOK_SHR_EQ,start,line,col);}return tok(l,FE_TOK_SHR,start,line,col);} return tok(l,FE_TOK_GT,start,line,col); + case '&': if(cur(l)=='&'){advance(l);fe_diag_error(l->diags,here(l,line,col),"&& is not a Ferro logical operator; use 'and'");return tok(l,FE_TOK_UNKNOWN,start,line,col);} if(cur(l)=='='){advance(l);return tok(l,FE_TOK_AND_EQ,start,line,col);} return tok(l,FE_TOK_AND,start,line,col); + case '|': if(cur(l)=='|'){advance(l);fe_diag_error(l->diags,here(l,line,col),"|| is not a Ferro logical operator; use 'or'");return tok(l,FE_TOK_UNKNOWN,start,line,col);} if(cur(l)=='='){advance(l);return tok(l,FE_TOK_OR_EQ,start,line,col);} return tok(l,FE_TOK_OR,start,line,col); + case '^': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_XOR_EQ,start,line,col);} return tok(l,FE_TOK_XOR,start,line,col); + case '~': return tok(l,FE_TOK_TILDE,start,line,col); + default: fe_diag_error(l->diags,here(l,line,col),"unknown character"); return tok(l,FE_TOK_UNKNOWN,start,line,col); + } +} + +const char *fe_token_name(FeTokKind k) +{ + switch(k) { + case FE_TOK_EOF:return "eof"; case FE_TOK_IDENT:return "identifier"; case FE_TOK_INT:return "integer"; + case FE_TOK_CHAR:return "character"; case FE_TOK_STRING:return "string"; case FE_TOK_UNIT:return "unit"; + case FE_TOK_FN:return "fn"; case FE_TOK_STRUCT:return "struct"; case FE_TOK_ENUM:return "enum"; + case FE_TOK_ERROR_KW:return "error"; case FE_TOK_CONST:return "const"; case FE_TOK_LET:return "let"; + case FE_TOK_VAR:return "var"; case FE_TOK_IF:return "if"; case FE_TOK_ELSE:return "else"; + case FE_TOK_WHILE:return "while"; case FE_TOK_FOR:return "for"; case FE_TOK_MATCH:return "match"; + case FE_TOK_RETURN:return "return"; case FE_TOK_BREAK:return "break"; case FE_TOK_CONTINUE:return "continue"; + case FE_TOK_TRUE:return "true"; case FE_TOK_FALSE:return "false"; case FE_TOK_NULL:return "null"; + case FE_TOK_UNDEFINED:return "undefined"; case FE_TOK_AND_KW:return "and"; case FE_TOK_OR_KW:return "or"; + case FE_TOK_NOT:return "not"; case FE_TOK_BANG:return "!"; + case FE_TOK_TILDE:return "~"; + case FE_TOK_LBRACE:return "{"; case FE_TOK_RBRACE:return "}"; case FE_TOK_LPAREN:return "("; case FE_TOK_RPAREN:return ")"; + case FE_TOK_SEMI:return ";"; case FE_TOK_COLON:return ":"; case FE_TOK_COMMA:return ","; + case FE_TOK_EQ:return "="; case FE_TOK_ARROW:return "->"; case FE_TOK_FATARROW:return "=>"; + default:return "token"; + } +} diff --git a/fec/src/lexer.h b/fec/src/lexer.h new file mode 100644 index 0000000..c60c2ba --- /dev/null +++ b/fec/src/lexer.h @@ -0,0 +1,50 @@ +#ifndef FE_LEXER_H +#define FE_LEXER_H + +#include "diag.h" +#include "arena.h" + +typedef enum FeTokKind { + FE_TOK_EOF, FE_TOK_ERROR, FE_TOK_IDENT, FE_TOK_INT, FE_TOK_CHAR, FE_TOK_STRING, + FE_TOK_UNIT, FE_TOK_IMPORT, FE_TOK_PUB, FE_TOK_FN, FE_TOK_STRUCT, FE_TOK_ENUM, + FE_TOK_ERROR_KW, FE_TOK_CONST, FE_TOK_STATIC, FE_TOK_VAR, FE_TOK_LET, FE_TOK_MUT, + FE_TOK_IF, FE_TOK_ELSE, FE_TOK_WHILE, FE_TOK_FOR, FE_TOK_IN, FE_TOK_MATCH, + FE_TOK_RETURN, FE_TOK_BREAK, FE_TOK_CONTINUE, FE_TOK_DEFER, FE_TOK_UNSAFE, + FE_TOK_COMPTIME, FE_TOK_ASM, FE_TOK_TRY, FE_TOK_CATCH, FE_TOK_AS, FE_TOK_EXTERN, + FE_TOK_INTERRUPT, FE_TOK_INTERRUPT_SAFE, FE_TOK_TRUE, FE_TOK_FALSE, FE_TOK_NULL, + FE_TOK_UNDEFINED, FE_TOK_SHARED, FE_TOK_ATOMIC, FE_TOK_CRITICAL, FE_TOK_SELF, + FE_TOK_SELFTYPE, FE_TOK_TYPE, FE_TOK_PACKED, FE_TOK_ORELSE, + FE_TOK_LPAREN, FE_TOK_RPAREN, FE_TOK_LBRACE, FE_TOK_RBRACE, FE_TOK_LBRACKET, FE_TOK_RBRACKET, + FE_TOK_COMMA, FE_TOK_SEMI, FE_TOK_COLON, FE_TOK_DOT, FE_TOK_DOTDOT, + FE_TOK_PLUS, FE_TOK_MINUS, FE_TOK_STAR, FE_TOK_SLASH, FE_TOK_PERCENT, + FE_TOK_PLUS_EQ, FE_TOK_MINUS_EQ, FE_TOK_STAR_EQ, FE_TOK_SLASH_EQ, FE_TOK_PERCENT_EQ, + FE_TOK_PLUS_WRAP, FE_TOK_MINUS_WRAP, FE_TOK_STAR_WRAP, + FE_TOK_EQ, FE_TOK_EQEQ, FE_TOK_NE, FE_TOK_LT, FE_TOK_LE, FE_TOK_GT, FE_TOK_GE, + FE_TOK_AND, FE_TOK_OR, FE_TOK_AND_KW, FE_TOK_OR_KW, FE_TOK_XOR, FE_TOK_TILDE, FE_TOK_NOT, FE_TOK_BANG, FE_TOK_SHL, FE_TOK_SHR, + FE_TOK_AND_EQ, FE_TOK_OR_EQ, FE_TOK_XOR_EQ, FE_TOK_SHL_EQ, FE_TOK_SHR_EQ, + FE_TOK_ANDAND, FE_TOK_OROR, FE_TOK_ARROW, FE_TOK_FATARROW, FE_TOK_AT, + FE_TOK_QUESTION, FE_TOK_UNKNOWN +} FeTokKind; + +typedef struct FeToken { + FeTokKind kind; + const char *begin; + unsigned long length; + FeLoc loc; +} FeToken; + +typedef struct FeLexer { + const char *src; + unsigned long length; + unsigned long pos; + unsigned long line; + unsigned long col; + const char *file; + FeDiags *diags; +} FeLexer; + +void fe_lexer_init(FeLexer *l, const char *src, unsigned long length, const char *file, FeDiags *d); +FeToken fe_lexer_next(FeLexer *l); +const char *fe_token_name(FeTokKind k); + +#endif diff --git a/fec/src/lower.c b/fec/src/lower.c new file mode 100644 index 0000000..4953540 --- /dev/null +++ b/fec/src/lower.c @@ -0,0 +1,638 @@ +#include "lowerpri.h" + +void fail(Lower *L, const char *why, FeNode *n) +{ + if (L->failed) return; + L->failed = 1; + fprintf(fe_diag_stream(), "%s:%lu:%lu: internal: cannot lower %s\n", + n && n->loc.file ? n->loc.file : "?", + n ? n->loc.line : 0UL, n ? n->loc.col : 0UL, why); +} + +/* ---------------------------------------------------------------- types --- */ + +/* A Ferro type becomes what a register can hold, or a size in memory. Anything + with more than one field is memory: the backend never has to decide whether + an aggregate fits somewhere. */ +FeIrType ir_type_of(const FeType *t) +{ + if (!t) return FE_IR_VOID; + switch (t->kind) { + case FE_TYPE_VOID: return FE_IR_VOID; + case FE_TYPE_BOOL: + case FE_TYPE_CHAR: return FE_IR_I8; + case FE_TYPE_INT: + if (t->bits <= 8U) return FE_IR_I8; + if (t->bits <= 16U) return FE_IR_I16; + return FE_IR_I32; + case FE_TYPE_REF: + case FE_TYPE_RAW: return FE_IR_PTR; + case FE_TYPE_OWNED: + /* An owned slice carries a length beside the pointer. */ + return t->elem && t->elem->kind == FE_TYPE_SLICE ? FE_IR_MEM : FE_IR_PTR; + case FE_TYPE_ENUM: + /* A payload-free enum is just its tag. */ + return t->variant_count && t->fields ? FE_IR_MEM : + (t->size <= 1UL ? FE_IR_I8 : + t->size <= 2UL ? FE_IR_I16 : FE_IR_I32); + default: + return FE_IR_MEM; + } +} + +int enum_has_payload(const FeType *t) +{ + unsigned i; + if (!t || t->kind != FE_TYPE_ENUM) return 0; + for (i = 0; i < t->variant_count; ++i) + if (t->variants[i].field_count) return 1; + return 0; +} + +FeIrType ir_type(const FeType *t) +{ + if (t && t->kind == FE_TYPE_ENUM && enum_has_payload(t)) return FE_IR_MEM; + return ir_type_of(t); +} + +unsigned long ir_size(const FeType *t) +{ + return t ? fe_type_size(t) : 0UL; +} + +unsigned ir_align(const FeType *t) +{ + return t ? fe_type_align(t) : 1U; +} + +int type_is_unsigned(const FeType *t) +{ + return t && t->kind == FE_TYPE_INT && t->is_unsigned; +} + +/* ---------------------------------------------------------------- slots --- */ + +Slot slot_value(unsigned temp, FeIrType t) +{ + Slot s; + s.is_place = 0; s.temp = temp; s.type = t; s.size = 0; + s.place = fe_ir_at_temp(0, 0); + return s; +} + +Slot slot_place(FeIrPlace p, FeIrType t, unsigned long size) +{ + Slot s; + s.is_place = 1; s.temp = 0; s.place = p; s.type = t; s.size = size; + return s; +} + +Slot slot_void(void) +{ + return slot_value(0, FE_IR_VOID); +} + +/* Read a slot as a value. An aggregate has no value form, so asking for one is + a lowering bug rather than a program error. */ +unsigned as_value(Lower *L, Slot s, FeNode *n) +{ + if (!s.is_place) return s.temp; + if (s.type == FE_IR_MEM) { fail(L, "an aggregate as a value", n); return 0; } + return fe_ir_load(L->m, L->b, s.type, s.place); +} + +/* The address of a slot. */ +unsigned as_address(Lower *L, Slot s, FeNode *n) +{ + if (!s.is_place) { fail(L, "the address of a temporary", n); return 0; } + return fe_ir_addr(L->m, L->b, s.place); +} + +/* --------------------------------------------------------------- locals --- */ + +/* Does letting go of this type have to do something? */ +int needs_release(const FeType *t) +{ + unsigned i; + if (!t) return 0; + if (t->kind == FE_TYPE_OWNED) return 1; + if (t->has_drop) return 1; + /* SPEC 5 R1: letting go of an owner lets go of what it owns. A struct that + holds an owner has something to do even when it says nothing itself -- + which is what lets one type hold another that has a `drop`, since + calling `drop` by hand is not allowed. */ + if (t->kind == FE_TYPE_STRUCT) + for (i = 0; i < t->field_count; ++i) + if (needs_release(t->fields[i].type)) return 1; + return 0; +} + +int lower_reserve(Lower *L, void **items, unsigned *capacity, unsigned needed, + unsigned long item_size) +{ + unsigned want; + void *grown; + if (needed < *capacity) return 1; + want = *capacity ? *capacity * 2U : 16U; + while (want <= needed) want *= 2U; + grown = fe_arena_alloc(&L->m->arena, (size_t)(want * item_size)); + if (!grown) { fail(L, "a function this large", 0); return 0; } + if (*items) memcpy(grown, *items, (size_t)(*capacity * item_size)); + *items = grown; + *capacity = want; + return 1; +} + +unsigned declare_var(Lower *L, const char *cname, const FeType *t, + const char *name) +{ + unsigned local = fe_ir_local(L->m, L->fn, ir_type(t), ir_size(t), + ir_align(t), name); + if (lower_reserve(L, (void **)&L->vars, &L->var_capacity, L->var_count, + (unsigned long)sizeof(LowerVar))) { + L->vars[L->var_count].cname = cname; + L->vars[L->var_count].local = local; + L->vars[L->var_count].by_address = 0; + ++L->var_count; + } + if (needs_release(t) && + lower_reserve(L, (void **)&L->owed, &L->owed_capacity, L->owed_count, + (unsigned long)sizeof *L->owed)) { + unsigned flag = fe_ir_local(L->m, L->fn, FE_IR_I8, 1, 1, "live"); + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I8, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(flag, 0), zero, FE_IR_I8); + L->owed[L->owed_count].block = 0; + L->owed[L->owed_count].local = local; + L->owed[L->owed_count].flag = flag; + L->owed[L->owed_count].type = (FeType *)t; + ++L->owed_count; + } + return local; +} + +/* The liveness flag beside a local, or none. */ +int release_flag(Lower *L, unsigned local, unsigned *flag) +{ + unsigned i; + for (i = L->owed_count; i > 0; --i) + if (!L->owed[i - 1].block && L->owed[i - 1].local == local) { + *flag = L->owed[i - 1].flag; + return 1; + } + return 0; +} + +LowerVar *find_var(Lower *L, const char *cname) +{ + unsigned i; + if (!cname) return 0; + for (i = L->var_count; i > 0; --i) + if (L->vars[i - 1].cname && strcmp(L->vars[i - 1].cname, cname) == 0) + return &L->vars[i - 1]; + return 0; +} + +/* --------------------------------------------------------------- blocks --- */ + +FeIrBlock *new_block(Lower *L) +{ + return fe_ir_block(L->m, L->fn); +} + +/* Which file a trap raised right now came from. The whole build lowers into + one module, so the unit being lowered is the only thing that knows. */ +unsigned trap_file(Lower *L) +{ + return fe_ir_file(L->m, L->c->unit ? L->c->unit->path : ""); +} + +/* A check that must hold. `ok` is a condition; when it is false the program + stops where it is. `--no-checks` removes the comparison and the branch, not + just the message, which is the whole point of the flag. */ +void guard(Lower *L, unsigned ok, FeIrTrap reason, unsigned long line) +{ + FeIrBlock *bad = new_block(L); + FeIrBlock *cont = new_block(L); + fe_ir_br(L->b, ok, cont->id, bad->id); + L->b = bad; + fe_ir_trap(L->b, reason, line, trap_file(L)); + L->b = cont; +} + +/* A tag says which of the two things a wrapper holds. An optional is one byte + at the front unless the payload has a spare representation; an error union is + a two-byte error code, and zero means there is no error. */ +FeIrType tag_type(const FeType *t) +{ + return t && t->kind == FE_TYPE_ERROR_UNION ? FE_IR_I16 : FE_IR_I8; +} + +int uses_niche(const FeType *t) +{ + return t && t->kind == FE_TYPE_OPTIONAL && fe_m7_optional_uses_niche(t->elem); +} + +/* Somewhere to build an aggregate that has no home of its own yet. */ +unsigned scratch(Lower *L, const FeType *t, const char *why) +{ + return fe_ir_local(L->m, L->fn, ir_type(t), ir_size(t), ir_align(t), why); +} + + +/* The number of elements an indexable place holds, and where the first element + is. An array is its own storage; a slice points at someone else's. */ +void indexable_parts(Lower *L, Slot base, const FeType *t, + unsigned *data, unsigned *length, FeNode *n) +{ + if (t && t->kind == FE_TYPE_ARRAY) { + *data = as_address(L, base, n); + *length = fe_ir_const(L->m, L->b, FE_IR_I32, (long)t->length); + return; + } + if (!base.is_place) { fail(L, "a slice with no place", n); *data = 0; *length = 0; return; } + *data = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_temp(as_address(L, base, n), SLICE_PTR_OFFSET)); + { + FeIrPlace lp = base.place; + lp.offset += SLICE_LEN_OFFSET; + *length = fe_ir_load(L->m, L->b, FE_IR_I32, lp); + } +} + +/* ------------------------------------------------------- error codes ----- */ + +void note_error_name(Lower *L, const char *name) +{ + unsigned i; + unsigned at; + if (!name) return; + if (!lower_reserve(L, (void **)&L->error_names, &L->error_capacity, + L->error_count, (unsigned long)sizeof(const char *))) + return; + for (i = 0; i < L->error_count; ++i) + if (!strcmp(L->error_names[i], name)) return; + /* Kept sorted as it is built, so the numbering is the spelling order. */ + at = L->error_count; + while (at > 0 && strcmp(L->error_names[at - 1], name) > 0) { + L->error_names[at] = L->error_names[at - 1]; + --at; + } + L->error_names[at] = name; + ++L->error_count; +} + +void collect_error_names(Lower *L, FeNode *n) +{ + FeNode *x; + if (!n) return; + /* Allocation reports failure with a name like any other, so it has to be + in the table even though no source line writes it. */ + if (n->kind == FE_N_CALL && n->a && n->a->kind == FE_N_MEMBER && + n->a->a && n->a->a->kind == FE_N_IDENT && n->a->a->text && + !strcmp(n->a->a->text, "mem") && n->a->b && n->a->b->text && + (!strcmp(n->a->b->text, "create") || + !strcmp(n->a->b->text, "alloc_slice"))) + note_error_name(L, "OutOfMemory"); + if (n->kind == FE_N_MEMBER && n->a && n->a->kind == FE_N_IDENT && + n->a->text && !strcmp(n->a->text, "error") && n->b && n->b->text) + note_error_name(L, n->b->text); + collect_error_names(L, n->a); + collect_error_names(L, n->b); + collect_error_names(L, n->c); + for (x = n->children; x; x = x->next) collect_error_names(L, x); +} + +long error_code(Lower *L, const char *name) +{ + unsigned i; + for (i = 0; i < L->error_count; ++i) + if (!strcmp(L->error_names[i], name)) return (long)(i + 1); + return 0; +} + +/* ---------------------------------------------------------- expressions --- */ + +FeIrOp binary_op(const char *op, int *is_cmp) +{ + *is_cmp = 0; + if (!op) return FE_IR_ADD; + if (!strcmp(op, "+") || !strcmp(op, "+%")) return FE_IR_ADD; + if (!strcmp(op, "-") || !strcmp(op, "-%")) return FE_IR_SUB; + if (!strcmp(op, "*") || !strcmp(op, "*%")) return FE_IR_MUL; + if (!strcmp(op, "/")) return FE_IR_DIV; + if (!strcmp(op, "%")) return FE_IR_MOD; + if (!strcmp(op, "&")) return FE_IR_AND; + if (!strcmp(op, "|")) return FE_IR_OR; + if (!strcmp(op, "^")) return FE_IR_XOR; + if (!strcmp(op, "<<")) return FE_IR_SHL; + if (!strcmp(op, ">>")) return FE_IR_SHR; + *is_cmp = 1; + if (!strcmp(op, "==")) return FE_IR_EQ; + if (!strcmp(op, "!=")) return FE_IR_NE; + if (!strcmp(op, "<")) return FE_IR_LT; + if (!strcmp(op, "<=")) return FE_IR_LE; + if (!strcmp(op, ">")) return FE_IR_GT; + if (!strcmp(op, ">=")) return FE_IR_GE; + *is_cmp = 0; + return FE_IR_ADD; +} + +long literal_value(FeNode *n) +{ + const char *s = n->text; + long v = 0; + int neg = 0; + if (!s) return 0; + if (!strcmp(s, "true")) return 1; + if (!strcmp(s, "false")) return 0; + if (!strcmp(s, "null") || !strcmp(s, "undefined")) return 0; + if (*s == '\'') { + /* A character literal; the lexer kept the quotes. */ + if (s[1] == '\\') { + switch (s[2]) { + case 'n': return 10; + case 't': return 9; + case 'r': return 13; + case '0': return 0; + default: return (long)(unsigned char)s[2]; + } + } + return (long)(unsigned char)s[1]; + } + if (*s == '-') { neg = 1; ++s; } + { + /* SPEC 3 spells four radices. Reading `0b1010` as decimal stops at the + `b` and answers zero, which is a number and so goes unnoticed. */ + int base = 10; + if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { base = 16; s += 2; } + else if (s[0] == '0' && (s[1] == 'b' || s[1] == 'B')) { base = 2; s += 2; } + else if (s[0] == '0' && (s[1] == 'o' || s[1] == 'O')) { base = 8; s += 2; } + for (; *s; ++s) { + int d; + if (*s == '_') continue; + if (*s >= '0' && *s <= '9') d = *s - '0'; + else if (*s >= 'a' && *s <= 'f') d = *s - 'a' + 10; + else if (*s >= 'A' && *s <= 'F') d = *s - 'A' + 10; + else break; + if (d >= base) break; + v = v * base + d; + } + } + return neg ? -v : v; +} + +/* `and` and `or` do not evaluate the right side unless they have to, so they + are control flow rather than an operation. */ +Slot lower_logical(Lower *L, FeNode *n, int is_and) +{ + unsigned result = fe_ir_local(L->m, L->fn, FE_IR_I8, 1, 1, "logical"); + FeIrBlock *rhs = new_block(L); + FeIrBlock *join = new_block(L); + FeIrBlock *entry = L->b; + unsigned left; + unsigned right; + L->b = entry; + left = as_value(L, lower_expr(L, n->a), n->a); + fe_ir_store(L->m, L->b, fe_ir_at_local(result, 0), left, FE_IR_I8); + if (is_and) fe_ir_br(L->b, left, rhs->id, join->id); + else fe_ir_br(L->b, left, join->id, rhs->id); + L->b = rhs; + right = as_value(L, lower_expr(L, n->b), n->b); + fe_ir_store(L->m, L->b, fe_ir_at_local(result, 0), right, FE_IR_I8); + fe_ir_jmp(L->b, join->id); + L->b = join; + return slot_place(fe_ir_at_local(result, 0), FE_IR_I8, 1); +} + +/* The builtins that are not calls at all: they are a constant, or they stop + the program. `@print` is expanded separately because it becomes several + calls rather than one thing. */ +int lower_builtin(Lower *L, FeNode *n, Slot *out) +{ + const char *name = n->text; + if (!name || name[0] != '@') return 0; + if (!strcmp(name, "@trap")) { + fe_ir_trap(L->b, FE_TRAP_EXPLICIT, n->loc.line, trap_file(L)); + L->b = new_block(L); + *out = slot_void(); + return 1; + } + if (!strcmp(name, "@unreachable")) { + fe_ir_trap(L->b, FE_TRAP_UNREACHABLE, n->loc.line, trap_file(L)); + L->b = new_block(L); + *out = slot_void(); + return 1; + } + if (!strcmp(name, "@size_of") || !strcmp(name, "@align_of")) { + FeNode *arg = n->children; + FeType *t = arg && arg->kind == FE_N_IDENT + ? fe_type_intern(&L->c->types, arg->text) : 0; + long v = !strcmp(name, "@size_of") ? (long)ir_size(t) + : (long)ir_align(t); + *out = slot_value(fe_ir_const(L->m, L->b, FE_IR_I32, v), FE_IR_I32); + return 1; + } + if (!strcmp(name, "@volatile_load")) { + /* Reading through a raw pointer. Nothing here reorders loads yet, so + volatile and ordinary read the same; the keyword is what marks the + access as deliberate, and the checker already required `unsafe`. */ + FeNode *arg = n->children; + unsigned p = as_value(L, lower_expr(L, arg), arg); + FeIrType t = ir_type(n->sem_type); + if (t == FE_IR_VOID || t == FE_IR_MEM) t = FE_IR_I8; + *out = slot_place(fe_ir_at_temp(p, 0), t, ir_size(n->sem_type)); + return 1; + } + if (!strcmp(name, "@volatile_store")) { + FeNode *arg = n->children; + FeNode *value = arg ? arg->next : 0; + unsigned p = as_value(L, lower_expr(L, arg), arg); + Slot v = lower_expr(L, value); + FeIrType t = value && value->sem_type ? ir_type(value->sem_type) + : FE_IR_I8; + fe_ir_store(L->m, L->b, fe_ir_at_temp(p, 0), as_value(L, v, value), t); + *out = slot_void(); + return 1; + } + if (!strcmp(name, "@ptr_cast")) { + /* A pointer is a pointer; the type it is said to point at is the + checker's business and leaves no trace here. */ + FeNode *arg = n->children; + FeNode *value = arg ? arg->next : 0; + *out = slot_value(as_value(L, lower_expr(L, value), value), FE_IR_PTR); + return 1; + } + if (!strcmp(name, "@line")) { + *out = slot_value(fe_ir_const(L->m, L->b, FE_IR_I32, + (long)n->loc.line), FE_IR_I32); + return 1; + } + return 0; +} + +/* ------------------------------------------------------------- mem.* ----- * + * The allocating intrinsics. They are not ordinary calls: `mem.create` takes a + * value and gives back an owned pointer to a copy of it, and the result is an + * error union because the allocation can fail. The runtime does the allocating; + * everything else about the shape is decided here. + * -------------------------------------------------------------------------- */ + +static const char *RT_ALLOC = "fe_rt_alloc"; +static const char *RT_FREE = "fe_rt_free"; + +int is_mem_call(const FeNode *n, const char *what) +{ + return n && n->a && n->a->kind == FE_N_MEMBER && + n->a->a && n->a->a->kind == FE_N_IDENT && n->a->a->text && + !strcmp(n->a->a->text, "mem") && + n->a->b && n->a->b->text && !strcmp(n->a->b->text, what); +} + +/* Build `!^T`: zero and the pointer when the allocation worked, the + out-of-memory code when it did not. */ +Slot allocation_result(Lower *L, FeNode *n, unsigned pointer) +{ + FeType *t = n->sem_type; + unsigned local = scratch(L, t, "allocated"); + long payload_at = (long)fe_type_payload_offset(t); + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_PTR, 0); + unsigned ok = fe_ir_binary(L->m, L->b, FE_IR_NE, FE_IR_PTR, pointer, zero, 1); + FeIrBlock *good = new_block(L); + FeIrBlock *bad = new_block(L); + FeIrBlock *join = new_block(L); + fe_ir_br(L->b, ok, good->id, bad->id); + L->b = good; + { + unsigned none = fe_ir_const(L->m, L->b, FE_IR_I16, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), none, FE_IR_I16); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, payload_at), pointer, + FE_IR_PTR); + } + fe_ir_jmp(L->b, join->id); + L->b = bad; + { + unsigned code = fe_ir_const(L->m, L->b, FE_IR_I16, + error_code(L, "OutOfMemory")); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), code, FE_IR_I16); + } + fe_ir_jmp(L->b, join->id); + L->b = join; + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); +} + +int lower_mem(Lower *L, FeNode *n, Slot *out) +{ + unsigned args[2]; + if (is_mem_call(n, "create")) { + FeNode *arg = n->children; + FeType *value = arg ? arg->sem_type : 0; + unsigned size = fe_ir_const(L->m, L->b, FE_IR_I32, + (long)ir_size(value)); + unsigned p; + Slot v; + args[0] = size; + p = fe_ir_call(L->m, L->b, FE_IR_PTR, RT_ALLOC, args, 1); + /* The value is written through the new pointer, not copied into a + local first: `create` moves what it was given. */ + v = lower_expr(L, arg); + store_into(L, fe_ir_at_temp(p, 0), v, arg, ir_size(value)); + *out = allocation_result(L, n, p); + return 1; + } + if (is_mem_call(n, "alloc_slice")) { + FeNode *type_arg = n->children; + FeNode *count_arg = type_arg ? type_arg->next : 0; + FeType *t = n->sem_type; + /* `!^[]T` -- the payload is an owned slice, a pointer and a length. */ + FeType *owned = t ? t->error_value : 0; + FeType *slice = owned ? owned->elem : 0; + FeType *elem = slice ? slice->elem : 0; + unsigned each = fe_ir_const(L->m, L->b, FE_IR_I32, (long)ir_size(elem)); + unsigned howmany = count_arg + ? as_value(L, lower_expr(L, count_arg), count_arg) + : fe_ir_const(L->m, L->b, FE_IR_I32, 0); + unsigned bytes = fe_ir_binary(L->m, L->b, FE_IR_MUL, FE_IR_I32, + howmany, each, 1); + unsigned p; + unsigned local = scratch(L, t, "allocated"); + long payload_at = (long)fe_type_payload_offset(t); + unsigned zero; + unsigned ok; + FeIrBlock *good; + FeIrBlock *bad; + FeIrBlock *join; + args[0] = bytes; + p = fe_ir_call(L->m, L->b, FE_IR_PTR, RT_ALLOC, args, 1); + zero = fe_ir_const(L->m, L->b, FE_IR_PTR, 0); + ok = fe_ir_binary(L->m, L->b, FE_IR_NE, FE_IR_PTR, p, zero, 1); + good = new_block(L); + bad = new_block(L); + join = new_block(L); + fe_ir_br(L->b, ok, good->id, bad->id); + L->b = good; + { + unsigned none = fe_ir_const(L->m, L->b, FE_IR_I16, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), none, FE_IR_I16); + fe_ir_store(L->m, L->b, + fe_ir_at_local(local, payload_at + SLICE_PTR_OFFSET), + p, FE_IR_PTR); + fe_ir_store(L->m, L->b, + fe_ir_at_local(local, payload_at + SLICE_LEN_OFFSET), + howmany, FE_IR_I32); + } + fe_ir_jmp(L->b, join->id); + L->b = bad; + { + unsigned code = fe_ir_const(L->m, L->b, FE_IR_I16, + error_code(L, "OutOfMemory")); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), code, FE_IR_I16); + } + fe_ir_jmp(L->b, join->id); + L->b = join; + *out = slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); + return 1; + } + if (is_mem_call(n, "destroy")) { + FeNode *arg = n->children; + Slot p = lower_expr(L, arg); + /* An owned slice is a pointer and a length; what was allocated is the + pointer. */ + if (p.type == FE_IR_MEM) { + FeIrPlace at = p.place; + at.offset += SLICE_PTR_OFFSET; + args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR, at); + } else { + args[0] = as_value(L, p, arg); + } + fe_ir_call(L->m, L->b, FE_IR_VOID, RT_FREE, args, 1); + *out = slot_void(); + return 1; + } + if (is_mem_call(n, "replace")) { + /* Read what is there, put the new value in its place, hand back the + old one. This is how a value is taken out of a field without ever + leaving the field uninitialised (SPEC 5 R7). */ + FeNode *dst = n->children; + FeNode *value = dst ? dst->next : 0; + FeType *t = n->sem_type; + unsigned target = as_value(L, lower_expr(L, dst), dst); + unsigned old = scratch(L, t, "replaced"); + Slot fresh; + fe_ir_copy(L->m, L->b, fe_ir_at_local(old, 0), fe_ir_at_temp(target, 0), + ir_size(t)); + fresh = lower_expr(L, value); + store_into(L, fe_ir_at_temp(target, 0), fresh, value, ir_size(t)); + *out = slot_place(fe_ir_at_local(old, 0), ir_type(t), ir_size(t)); + return 1; + } + return 0; +} + +/* ------------------------------------------------------------ printing --- * + * SPEC 6.3.1: the formatting builtins are not variadic functions. A call is + * expanded here into one write per literal chunk and one per value, so the + * language never grows a variadic calling convention and the format string is + * gone by the time anything runs. + * -------------------------------------------------------------------------- */ + +/* Write `len` bytes of a literal that is already in the image. */ diff --git a/fec/src/lower.h b/fec/src/lower.h new file mode 100644 index 0000000..2d0c3dc --- /dev/null +++ b/fec/src/lower.h @@ -0,0 +1,18 @@ +#ifndef FE_LOWER_H +#define FE_LOWER_H + +#include "check.h" +#include "ir.h" + +/* Turn the checked program into IR. + + The checker leaves every expression with a type and every declaration with a + link-visible name; lowering reads those and produces the flat form the + backend wants. Everything Ferro-shaped is expanded here -- `try` becomes a + branch, `defer` is copied onto each exit path, an index becomes a comparison + and a trap -- so that neither the checker nor the backend has to know about + the other's world. */ + +int fe_lower_program(FeCheck *c, FeIrModule *out); + +#endif diff --git a/fec/src/lowerexp.c b/fec/src/lowerexp.c new file mode 100644 index 0000000..ecaa36a --- /dev/null +++ b/fec/src/lowerexp.c @@ -0,0 +1,560 @@ +#include "lowerpri.h" + +Slot lower_expr_core(Lower *L, FeNode *n) +{ + FeType *t; + FeIrType it; + if (!n || L->failed) return slot_void(); + t = n->sem_type; + it = ir_type(t); + switch (n->kind) { + case FE_N_LITERAL: + if (n->text && n->text[0] == '"') { + /* The bytes live in the image; the value is a pointer to them and + how many there are. The lexer keeps the quotes and the escapes, + so this is where ` +` becomes one byte. */ + char text[1024]; + unsigned long raw = strlen(n->text); + unsigned long len = 0; + unsigned long i; + const char *label; + unsigned local; + unsigned p; + unsigned c; + if (raw >= 2) raw -= 2; + for (i = 0; i < raw && len + 1 < sizeof text; ++i) { + char ch = n->text[1 + i]; + if (ch == 92 && i + 1 < raw) { /* a backslash */ + ++i; + switch (n->text[1 + i]) { + case 'n': ch = 10; break; + case 't': ch = 9; break; + case 'r': ch = 13; break; + case '0': ch = 0; break; + default: ch = n->text[1 + i]; break; + } + } + text[len++] = ch; + } + label = fe_ir_string(L->m, text, len); + if (!label) { fail(L, "a string literal", n); return slot_void(); } + local = scratch(L, t, "text"); + p = fe_ir_addr(L->m, L->b, fe_ir_at_global(label, 0)); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, SLICE_PTR_OFFSET), p, + FE_IR_PTR); + c = fe_ir_const(L->m, L->b, FE_IR_I32, (long)len); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, SLICE_LEN_OFFSET), c, + FE_IR_I32); + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); + } + return slot_value(fe_ir_const(L->m, L->b, + it == FE_IR_VOID ? FE_IR_I32 : it, + literal_value(n)), + it == FE_IR_VOID ? FE_IR_I32 : it); + case FE_N_IDENT: { + LowerVar *var = find_var(L, n->cname); + if (var) { + if (var->by_address) { + unsigned p = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(var->local, 0)); + return slot_place(fe_ir_at_temp(p, 0), it, ir_size(t)); + } + return slot_place(fe_ir_at_local(var->local, 0), it, ir_size(t)); + } + if (n->cname) + return slot_place(fe_ir_at_global(n->cname, 0), it, ir_size(t)); + fail(L, "an unresolved name", n); + return slot_void(); + } + case FE_N_BINARY: { + int is_cmp = 0; + FeIrOp op; + unsigned a; + unsigned b; + FeIrType operand; + if (n->text && !strcmp(n->text, "orelse")) return lower_lazy(L, n, 0); + if (n->text && !strcmp(n->text, "catch")) return lower_lazy(L, n, 1); + if (n->text && (!strcmp(n->text, "and") || !strcmp(n->text, "or"))) + return lower_logical(L, n, !strcmp(n->text, "and")); + op = binary_op(n->text, &is_cmp); + /* Comparing an optional with `null` asks about its tag, not about the + bytes of the whole wrapper -- which has no value form at all. */ + if ((op == FE_IR_EQ || op == FE_IR_NE) && n->a && n->b) { + FeNode *w = fe_m7_is_null(n->b) ? n->a : + (fe_m7_is_null(n->a) ? n->b : 0); + FeType *wt = w ? w->sem_type : 0; + if (wt && wt->kind == FE_TYPE_OPTIONAL) { + Slot s = lower_expr(L, w); + unsigned t0; + unsigned z; + if (!s.is_place) { + fail(L, "an optional with no place", w); + return slot_void(); + } + t0 = wrapper_tag(L, s, wt, w); + z = fe_ir_const(L->m, L->b, + uses_niche(wt) ? FE_IR_PTR : FE_IR_I8, 0); + return slot_value(fe_ir_binary(L->m, L->b, op, + uses_niche(wt) ? FE_IR_PTR : FE_IR_I8, t0, z, 1), + FE_IR_I8); + } + } + operand = ir_type(n->a ? n->a->sem_type : 0); + if (operand == FE_IR_VOID || operand == FE_IR_MEM) operand = FE_IR_I32; + a = as_value(L, lower_expr(L, n->a), n->a); + b = as_value(L, lower_expr(L, n->b), n->b); + return slot_value(fe_ir_binary(L->m, L->b, op, operand, a, b, + type_is_unsigned(n->a ? n->a->sem_type + : 0)), + is_cmp ? FE_IR_I8 : operand); + } + case FE_N_UNARY: + if (n->text && !strcmp(n->text, "try")) return lower_try(L, n); + if (n->text && !strcmp(n->text, "-")) { + unsigned zero = fe_ir_const(L->m, L->b, it, 0); + unsigned v = as_value(L, lower_expr(L, n->a), n->a); + return slot_value(fe_ir_binary(L->m, L->b, FE_IR_SUB, it, zero, v, + 0), it); + } + if (n->text && !strcmp(n->text, "~")) { + /* Every bit flipped is every bit exchanged with a one. */ + unsigned ones = fe_ir_const(L->m, L->b, it, -1L); + unsigned v = as_value(L, lower_expr(L, n->a), n->a); + return slot_value(fe_ir_binary(L->m, L->b, FE_IR_XOR, it, v, ones, + 0), it); + } + if (n->text && !strcmp(n->text, "not")) { + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I8, 0); + unsigned v = as_value(L, lower_expr(L, n->a), n->a); + return slot_value(fe_ir_binary(L->m, L->b, FE_IR_EQ, FE_IR_I8, v, + zero, 0), FE_IR_I8); + } + if (n->text && (!strcmp(n->text, "&") || !strcmp(n->text, "&mut"))) { + Slot inner = lower_expr(L, n->a); + return slot_value(as_address(L, inner, n->a), FE_IR_PTR); + } + fail(L, "this unary operator", n); + return slot_void(); + case FE_N_MEMBER: + /* A variant used as a value carries nothing but its tag. When no + variant of the enum carries anything the whole value is that tag; + otherwise it is a tag sitting in front of an unused payload. */ + if (t && t->kind == FE_TYPE_ENUM && n->b && n->b->text) { + FeVariantType *v = fe_type_variant(t, n->b->text); + if (v && !enum_has_payload(t)) + return slot_value(fe_ir_const(L->m, L->b, ir_type(t), + (long)v->tag), ir_type(t)); + if (v && !v->field_count) { + unsigned local = scratch(L, t, "variant"); + unsigned tag = fe_ir_const(L->m, L->b, tag_type_of(t), + (long)v->tag); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), tag, + tag_type_of(t)); + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, + ir_size(t)); + } + } + /* `error.Name` is a member of the open default set: a code, and + nothing to look up. */ + if (n->a && n->a->kind == FE_N_IDENT && n->a->text && + !strcmp(n->a->text, "error") && n->b && n->b->text) + return slot_value(fe_ir_const(L->m, L->b, FE_IR_I16, + error_code(L, n->b->text)), + FE_IR_I16); + /* `.?` is the payload of an optional the checker already proved is + there. */ + if (n->text && !strcmp(n->text, ".?")) { + FeType *bt = n->a ? n->a->sem_type : 0; + return wrapper_payload(L, lower_expr(L, n->a), bt); + } + /* `p.^` reads through a pointer -- except for an owned slice, whose + pointer and length are the value itself, so there is nothing to + step through. */ + if (n->text && !strcmp(n->text, ".^")) { + Slot base = lower_expr(L, n->a); + unsigned p; + if (base.type == FE_IR_MEM) + return slot_place(base.place, it, ir_size(t)); + p = as_value(L, base, n->a); + return slot_place(fe_ir_at_temp(p, 0), it, ir_size(t)); + } + /* `.n` is how many elements there are, which an array knows at + compile time and a slice carries beside its pointer. Only for those: + a struct is free to have a field called `n`, and reading it as a + length would quietly hand back the wrong four bytes. */ + if (n->b && n->b->text && !strcmp(n->b->text, "n") && + n->a && n->a->sem_type && + (n->a->sem_type->kind == FE_TYPE_ARRAY || + n->a->sem_type->kind == FE_TYPE_SLICE || + n->a->sem_type->kind == FE_TYPE_STR)) { + FeType *bt = n->a->sem_type; + Slot base; + if (bt && bt->kind == FE_TYPE_ARRAY) + return slot_value(fe_ir_const(L->m, L->b, FE_IR_I32, + (long)bt->length), FE_IR_I32); + base = lower_expr(L, n->a); + if (!base.is_place) { fail(L, "a length of a temporary", n); return slot_void(); } + base.place.offset += SLICE_LEN_OFFSET; + return slot_place(base.place, FE_IR_I32, 4); + } + /* A field is a constant offset from the base. */ + { + FeType *base = n->a ? n->a->sem_type : 0; + FeFieldType *field; + Slot b; + if (base && (base->kind == FE_TYPE_REF || + base->kind == FE_TYPE_OWNED)) base = base->elem; + field = fe_type_field(base, n->b && n->b->text ? n->b->text : ""); + /* `binding.name` is not a field of anything: it is a constant or a + global in another unit, and the checker already turned it into a + link name. */ + if (!field && n->cname) + return slot_place(fe_ir_at_global(n->cname, 0), it, + ir_size(t)); + if (!field) { fail(L, "an unresolved field", n); return slot_void(); } + b = lower_expr(L, n->a); + if (n->a->sem_type && (n->a->sem_type->kind == FE_TYPE_REF || + n->a->sem_type->kind == FE_TYPE_OWNED)) { + unsigned p = as_value(L, b, n->a); + return slot_place(fe_ir_at_temp(p, (long)field->offset), it, + ir_size(t)); + } + if (!b.is_place) { fail(L, "a field of a temporary", n); return slot_void(); } + b.place.offset += (long)field->offset; + return slot_place(b.place, it, ir_size(t)); + } + case FE_N_INDEX: { + FeType *bt = n->a ? n->a->sem_type : 0; + FeType *elem = bt ? bt->elem : 0; + Slot base; + unsigned data; + unsigned length; + unsigned index; + unsigned scale; + unsigned offset; + unsigned addr; + if (n->flags & FE_NODE_SLICE) return lower_slice(L, n); + base = lower_expr(L, n->a); + indexable_parts(L, base, bt, &data, &length, n); + index = as_value(L, lower_expr(L, n->b), n->b); + if (!L->c->no_checks) { + unsigned ok = fe_ir_binary(L->m, L->b, FE_IR_LT, FE_IR_I32, + index, length, 1); + guard(L, ok, FE_TRAP_BOUNDS, n->loc.line); + } + scale = fe_ir_const(L->m, L->b, FE_IR_I32, (long)ir_size(elem)); + offset = fe_ir_binary(L->m, L->b, FE_IR_MUL, FE_IR_I32, index, scale, 1); + addr = fe_ir_binary(L->m, L->b, FE_IR_ADD, FE_IR_PTR, data, offset, 1); + return slot_place(fe_ir_at_temp(addr, 0), ir_type(elem), ir_size(elem)); + } + case FE_N_ARRAY_INIT: { + unsigned local = scratch(L, t, "array"); + FeType *elem = t ? t->elem : 0; + unsigned long step = ir_size(elem); + long at = 0; + FeNode *x; + for (x = n->children; x; x = x->next) { + Slot v = lower_expr(L, x); + store_into(L, fe_ir_at_local(local, at), v, x, step); + at += (long)step; + } + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); + } + case FE_N_STRUCT_INIT: { + unsigned local; + FeNode *f; + /* `Enum.Variant{ .. }` builds a variant, not a struct: the tag first, + then the named fields inside the payload area. */ + if (t && t->kind == FE_TYPE_ENUM && n->a && n->a->kind == FE_N_MEMBER) { + const FeVariantType *v = fe_type_variant(t, + n->a->b && n->a->b->text ? n->a->b->text : ""); + long base = (long)fe_type_payload_offset(t); + unsigned tag; + if (!v) { fail(L, "an unknown variant", n); return slot_void(); } + local = scratch(L, t, "variant"); + tag = fe_ir_const(L->m, L->b, tag_type_of(t), (long)v->tag); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), tag, + tag_type_of(t)); + for (f = n->children; f; f = f->next) { + unsigned i; + if (f->kind != FE_N_FIELD) continue; + for (i = 0; i < v->field_count; ++i) + if (f->text && v->fields[i].name && + !strcmp(v->fields[i].name, f->text)) break; + if (i == v->field_count) { + fail(L, "an unknown variant field", f); + return slot_void(); + } + store_into(L, fe_ir_at_local(local, + base + (long)v->fields[i].offset), + lower_expr(L, f->a), f, ir_size(v->fields[i].type)); + } + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); + } + local = scratch(L, t, "struct"); + for (f = n->children; f; f = f->next) { + FeFieldType *field; + Slot v; + if (f->kind != FE_N_FIELD) continue; + field = fe_type_field(t, f->text); + if (!field) { fail(L, "an unresolved field", f); return slot_void(); } + v = lower_expr(L, f->a); + store_into(L, fe_ir_at_local(local, (long)field->offset), v, f, + ir_size(field->type)); + } + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); + } + case FE_N_CALL: + return lower_call(L, n); + case FE_N_TYPE: + /* `x as T`: the operand is `a` and the target type is the node's own. + Between integers this only changes how wide the value is and whether + the top bits repeat the sign. */ + if (n->a) { + FeType *from = n->a->sem_type; + unsigned v = as_value(L, lower_expr(L, n->a), n->a); + if (ir_type(from) == it) return slot_value(v, it); + return slot_value(fe_ir_cast(L->m, L->b, ir_type(from), it, v, + type_is_unsigned(from)), it); + } + fail(L, "this type expression", n); + return slot_void(); + case FE_N_EXPR: + return lower_expr(L, n->a); + default: + fail(L, "this expression", n); + return slot_void(); + } +} + +/* The link name of the `drop` method for this type, found through the instance + the checker recorded. */ +const char *drop_name(Lower *L, const FeType *t) +{ + unsigned i; + FeNode *method = 0; + if (!t || !t->decl_node) return 0; + for (method = t->decl_node->children; method; method = method->next) + if (method->kind == FE_N_FN && method->text && + !strcmp(method->text, "drop")) break; + if (!method) return 0; + for (i = 0; i < L->c->instance_count; ++i) + if (L->c->instances[i].decl == method && + L->c->instances[i].owner == t) + return L->c->instances[i].cname; + return method->cname; +} + +/* Let go of one value sitting at `at`. A type that says how to let go of + itself is asked first; then whatever it holds is let go of in turn, so a + struct that owns a struct that owns a buffer settles all three without + anyone writing a `drop` (SPEC 5 R1). */ +void release_at(Lower *L, const FeType *t, FeIrPlace at) +{ + unsigned args[1]; + unsigned i; + if (!t) return; + if (t->has_drop) { + const char *how = drop_name(L, t); + args[0] = fe_ir_addr(L->m, L->b, at); + if (how) fe_ir_call(L->m, L->b, FE_IR_VOID, how, args, 1); + } + if (t->kind == FE_TYPE_OWNED) { + FeIrPlace p = at; + if (t->elem && t->elem->kind == FE_TYPE_SLICE) + p.offset += SLICE_PTR_OFFSET; + args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR, p); + fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_free", args, 1); + return; + } + if (t->kind == FE_TYPE_STRUCT) + for (i = 0; i < t->field_count; ++i) { + FeIrPlace p = at; + if (!needs_release(t->fields[i].type)) continue; + p.offset += (long)t->fields[i].offset; + release_at(L, t->fields[i].type, p); + } +} + +/* Settle what a scope owes, most recent first. A `return` in the middle of a + function still owes everything, so every exit path calls this. */ +void run_deferred(Lower *L, unsigned from) +{ + unsigned i; + for (i = L->owed_count; i > from; --i) { + if (L->owed[i - 1].block) { + lower_stmt(L, L->owed[i - 1].block); + continue; + } + { + /* Release only where the value is still here. */ + unsigned live = fe_ir_load(L->m, L->b, FE_IR_I8, + fe_ir_at_local(L->owed[i - 1].flag, 0)); + FeIrBlock *doit = new_block(L); + FeIrBlock *skip = new_block(L); + FeType *t = L->owed[i - 1].type; + fe_ir_br(L->b, live, doit->id, skip->id); + L->b = doit; + release_at(L, t, fe_ir_at_local(L->owed[i - 1].local, 0)); + fe_ir_jmp(L->b, skip->id); + L->b = skip; + } + } +} + +/* ------------------------------------------------------- wrappers -------- * + * An optional is a tag and a payload; an error union is an error code and a + * payload, where a code of zero means there is no error. Both are memory, and + * both are built the same way: write the tag, then write the value after it. + * -------------------------------------------------------------------------- */ + +Slot wrap_context(Lower *L, Slot v, FeNode *n) +{ + FeType *want = n->sem_context; + unsigned local; + long payload_at; + if (!want) return v; + local = scratch(L, want, "wrapped"); + payload_at = (long)fe_type_payload_offset(want); + if (want->kind == FE_TYPE_OPTIONAL) { + if (fe_m7_is_null(n)) { + /* A payload with a spare representation uses it for "nothing" + instead of carrying a separate tag. */ + unsigned z = fe_ir_const(L->m, L->b, + uses_niche(want) ? FE_IR_PTR : FE_IR_I8, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), z, + uses_niche(want) ? FE_IR_PTR : FE_IR_I8); + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(want)); + } + if (!uses_niche(want)) { + unsigned one = fe_ir_const(L->m, L->b, FE_IR_I8, 1); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), one, FE_IR_I8); + } + store_into(L, fe_ir_at_local(local, payload_at), v, n, + ir_size(want->elem)); + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(want)); + } + if (want->kind == FE_TYPE_ERROR_UNION) { + FeType *value_type = want->error_value; + if (n->sem_type && n->sem_type->is_error) { + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), + as_value(L, v, n), FE_IR_I16); + } else { + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I16, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), zero, FE_IR_I16); + if (value_type && value_type->kind != FE_TYPE_VOID) + store_into(L, fe_ir_at_local(local, payload_at), v, n, + ir_size(value_type)); + } + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(want)); + } + return v; +} + +/* The tag of a wrapper that is already in memory. */ +unsigned wrapper_tag(Lower *L, Slot w, const FeType *t, FeNode *n) +{ + FeIrPlace p; + if (!w.is_place) { fail(L, "a wrapper with no place", n); return 0; } + p = w.place; + if (uses_niche(t)) return fe_ir_load(L->m, L->b, FE_IR_PTR, p); + return fe_ir_load(L->m, L->b, tag_type(t), p); +} + +Slot wrapper_payload(Lower *L, Slot w, const FeType *t) +{ + FeType *payload = t ? (t->kind == FE_TYPE_ERROR_UNION ? t->error_value + : t->elem) : 0; + FeIrPlace p = w.place; + (void)L; + p.offset += (long)fe_type_payload_offset(t); + return slot_place(p, ir_type(payload), ir_size(payload)); +} + +/* Leave the function with this error code, after the deferred blocks. */ +void return_error(Lower *L, unsigned err, FeNode *n) +{ + FeType *ret = L->ret_type; + unsigned local = scratch(L, ret, "failure"); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), err, FE_IR_I16); + run_deferred(L, 0); + if (L->fn->returns_by_address) { + unsigned dst = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(L->ret_local, 0)); + fe_ir_copy(L->m, L->b, fe_ir_at_temp(dst, 0), fe_ir_at_local(local, 0), + ir_size(ret)); + fe_ir_ret(L->b, 0, 0); + return; + } + fe_ir_ret(L->b, fe_ir_load(L->m, L->b, ir_type(ret), + fe_ir_at_local(local, 0)), 1); + (void)n; +} + +/* `try e` -- if e failed, leave with its error; otherwise the value. */ +Slot lower_try(Lower *L, FeNode *n) +{ + FeType *t = n->a ? n->a->sem_type : 0; + Slot e = lower_expr(L, n->a); + unsigned err = wrapper_tag(L, e, t, n); + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I16, 0); + unsigned ok = fe_ir_binary(L->m, L->b, FE_IR_EQ, FE_IR_I16, err, zero, 1); + FeIrBlock *bad = new_block(L); + FeIrBlock *good = new_block(L); + fe_ir_br(L->b, ok, good->id, bad->id); + L->b = bad; + return_error(L, err, n); + L->b = good; + return wrapper_payload(L, e, t); +} + +/* `e orelse d` and `e catch d` both mean "the value, or that instead". The + right-hand side is only evaluated when it is needed, so it is a branch. */ +Slot lower_lazy(Lower *L, FeNode *n, int is_catch) +{ + FeType *t = n->a ? n->a->sem_type : 0; + FeType *payload = t ? (is_catch ? t->error_value : t->elem) : 0; + Slot e; + unsigned tag; + unsigned zero; + unsigned ok; + unsigned result; + FeIrBlock *other; + FeIrBlock *join; + FeIrBlock *have; + e = lower_expr(L, n->a); + tag = wrapper_tag(L, e, t, n); + zero = fe_ir_const(L->m, L->b, is_catch || uses_niche(t) ? FE_IR_PTR + : FE_IR_I8, 0); + /* An error union is fine when its code is zero; an optional is fine when + its tag is not. */ + ok = fe_ir_binary(L->m, L->b, is_catch ? FE_IR_EQ : FE_IR_NE, + is_catch ? FE_IR_I16 : (uses_niche(t) ? FE_IR_PTR + : FE_IR_I8), + tag, zero, 1); + result = scratch(L, payload, "result"); + have = new_block(L); + other = new_block(L); + join = new_block(L); + fe_ir_br(L->b, ok, have->id, other->id); + L->b = have; + store_into(L, fe_ir_at_local(result, 0), wrapper_payload(L, e, t), n, + ir_size(payload)); + fe_ir_jmp(L->b, join->id); + L->b = other; + if (is_catch && n->c) { + /* The block form handles the error and must not fall through with a + value, so whatever it leaves behind is what the checker allowed. */ + lower_stmt(L, n->c); + } else { + Slot d = lower_expr(L, n->b); + store_into(L, fe_ir_at_local(result, 0), d, n->b, ir_size(payload)); + } + fe_ir_jmp(L->b, join->id); + L->b = join; + return slot_place(fe_ir_at_local(result, 0), ir_type(payload), + ir_size(payload)); +} + +/* ----------------------------------------------------------- statements --- */ diff --git a/fec/src/lowerpri.h b/fec/src/lowerpri.h new file mode 100644 index 0000000..6b87c6b --- /dev/null +++ b/fec/src/lowerpri.h @@ -0,0 +1,171 @@ +#ifndef FE_LOWERPRI_H +#define FE_LOWERPRI_H + +/* Lowering's own vocabulary, shared by the files it is split across. */ + +#include "lower.h" +#include "m7.h" +#include "own.h" +#include +#include + +#include +#include "m7.h" +#include "own.h" +#include + +/* ------------------------------------------------------------------------- * + * Lowering + * + * One function at a time, one statement at a time. A `Slot` is what an + * expression produced: either a value already in a temporary, or a place in + * memory that a value can be read from or written to. Aggregates are always + * places -- they are never carried in a temporary, because a temporary is a + * register and an aggregate does not fit in one. + * ------------------------------------------------------------------------- */ + + +typedef struct LowerVar { + const char *cname; + unsigned local; + /* An aggregate parameter arrives as an address, so the slot holds a + pointer and the value is one dereference away. */ + int by_address; +} LowerVar; + +typedef struct Lower { + FeCheck *c; + FeIrModule *m; + FeIrFunc *fn; + FeIrBlock *b; /* the block being appended to */ + FeType *ret_type; + unsigned ret_local; /* hidden result address, when returning mem */ + /* These three grow. A fixed size here does not report a program that is + too big -- it quietly drops what does not fit and generates wrong code, + which is the worst way for a limit to be reached. */ + LowerVar *vars; + unsigned var_count; + unsigned var_capacity; + /* Loop targets, for break and continue. */ + unsigned break_target[32]; + unsigned continue_target[32]; + unsigned loop_depth; + /* What a scope still owes when it ends: `defer` blocks to run and owned + values to release, in the order they were written. Every exit path runs + what is live, last first. + + A drop carries a flag beside the value. The flag is set when the value + is stored and cleared wherever it is moved away, so the release happens + exactly on the paths where the value is still there -- which is not + something the shape of the code can tell you on its own. */ + struct { + FeNode *block; /* a `defer`, when set */ + unsigned local; /* the owned value, otherwise */ + unsigned flag; + FeType *type; + } *owed; + unsigned owed_count; + unsigned owed_capacity; + /* Every `error.Name` used anywhere in the build, sorted, numbered from one. + SPEC 4.6: the names are collected rather than declared, and the order is + fixed by the spelling so that the same program always gets the same + codes however the build was ordered. */ + const char **error_names; + unsigned error_count; + unsigned error_capacity; + int failed; +} Lower; + +typedef struct Slot { + int is_place; + unsigned temp; /* the value, when is_place is 0 */ + FeIrPlace place; /* where it lives, when is_place is 1 */ + FeIrType type; + unsigned long size; /* for FE_IR_MEM */ +} Slot; + + + +/* A slice is a pointer and a length, in that order. Both the compiler and the + runtime read it this way, so the offsets live here and nowhere else. */ +#define SLICE_PTR_OFFSET 0L +#define SLICE_LEN_OFFSET 4L + +/* Grow one of the checker's own arrays. Returns zero when there is no more + memory, which the caller reports rather than ignores. */ +int lower_reserve(Lower *L, void **items, unsigned *capacity, unsigned needed, + unsigned long item_size); + +/* Every definition in lowering, so the split files can see each other. */ +FeIrType tag_type_of(const FeType *t); +int struct_is_generic(const FeNode *decl); +void lower_if_let(Lower *L, FeNode *n); +unsigned wrapper_tag(Lower *L, Slot w, const FeType *t, FeNode *n); +void bind_payload(Lower *L, Slot subject, const FeType *t, + const FeVariantType *v, FeNode *arm); +void fail(Lower *L, const char *why, FeNode *n); +FeIrType ir_type_of(const FeType *t); +int enum_has_payload(const FeType *t); +FeIrType ir_type(const FeType *t); +unsigned long ir_size(const FeType *t); +unsigned ir_align(const FeType *t); +int type_is_unsigned(const FeType *t); +Slot slot_value(unsigned temp, FeIrType t); +Slot slot_place(FeIrPlace p, FeIrType t, unsigned long size); +Slot slot_void(void); +unsigned as_value(Lower *L, Slot s, FeNode *n); +unsigned as_address(Lower *L, Slot s, FeNode *n); +int needs_release(const FeType *t); +unsigned declare_var(Lower *L, const char *cname, const FeType *t, + const char *name); +int release_flag(Lower *L, unsigned local, unsigned *flag); +LowerVar *find_var(Lower *L, const char *cname); +FeIrBlock *new_block(Lower *L); +unsigned trap_file(Lower *L); +void guard(Lower *L, unsigned ok, FeIrTrap reason, unsigned long line); +FeIrType tag_type(const FeType *t); +int uses_niche(const FeType *t); +unsigned scratch(Lower *L, const FeType *t, const char *why); +void indexable_parts(Lower *L, Slot base, const FeType *t, + unsigned *data, unsigned *length, FeNode *n); +void note_error_name(Lower *L, const char *name); +void collect_error_names(Lower *L, FeNode *n); +long error_code(Lower *L, const char *name); +FeIrOp binary_op(const char *op, int *is_cmp); +long literal_value(FeNode *n); +Slot lower_logical(Lower *L, FeNode *n, int is_and); +int lower_builtin(Lower *L, FeNode *n, Slot *out); +int is_mem_call(const FeNode *n, const char *what); +Slot allocation_result(Lower *L, FeNode *n, unsigned pointer); +int lower_mem(Lower *L, FeNode *n, Slot *out); +void emit_text(Lower *L, unsigned handle, const char *text, + unsigned long len); +void emit_value_text(Lower *L, unsigned handle, FeNode *arg, int verb); +int lower_print(Lower *L, FeNode *n, Slot *out); +Slot lower_call(Lower *L, FeNode *n); +Slot lower_expr(Lower *L, FeNode *n); +Slot lower_expr_core(Lower *L, FeNode *n); +const char *drop_name(Lower *L, const FeType *t); +void release_at(Lower *L, const FeType *t, FeIrPlace at); +void run_deferred(Lower *L, unsigned from); +Slot wrap_context(Lower *L, Slot v, FeNode *n); +unsigned wrapper_tag(Lower *L, Slot w, const FeType *t, FeNode *n); +Slot wrapper_payload(Lower *L, Slot w, const FeType *t); +void return_error(Lower *L, unsigned err, FeNode *n); +Slot lower_try(Lower *L, FeNode *n); +Slot lower_lazy(Lower *L, FeNode *n, int is_catch); +void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, + unsigned long size); +void lower_return(Lower *L, FeNode *n); +void lower_if(Lower *L, FeNode *n); +void lower_while(Lower *L, FeNode *n); +Slot lower_slice(Lower *L, FeNode *n); +void lower_for(Lower *L, FeNode *n); +void lower_match(Lower *L, FeNode *n); +void lower_stmt(Lower *L, FeNode *n); +void lower_global(Lower *L, FeNode *n); +int fn_is_generic(const FeNode *fn); +void lower_fn_as(Lower *L, FeNode *fn, const char *name); +void lower_fn(Lower *L, FeNode *fn); + +#endif diff --git a/fec/src/lowerprn.c b/fec/src/lowerprn.c new file mode 100644 index 0000000..83f7ecb --- /dev/null +++ b/fec/src/lowerprn.c @@ -0,0 +1,257 @@ +#include "lowerpri.h" + +void emit_text(Lower *L, unsigned handle, const char *text, + unsigned long len) +{ + unsigned args[3]; + const char *label; + if (!len) return; + label = fe_ir_string(L->m, text, len); + if (!label) return; + args[0] = fe_ir_const(L->m, L->b, FE_IR_I32, (long)handle); + args[1] = fe_ir_addr(L->m, L->b, fe_ir_at_global(label, 0)); + args[2] = fe_ir_const(L->m, L->b, FE_IR_I32, (long)len); + fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_write", args, 3); +} + +/* Write one value, the way the verb asked for. */ +void emit_value_text(Lower *L, unsigned handle, FeNode *arg, int verb) +{ + FeType *t = arg ? arg->sem_type : 0; + Slot v = lower_expr(L, arg); + unsigned args[3]; + if (t && (t->kind == FE_TYPE_SLICE || t->kind == FE_TYPE_STR)) { + FeIrPlace at = v.place; + if (!v.is_place) { fail(L, "text with no place", arg); return; } + args[0] = fe_ir_const(L->m, L->b, FE_IR_I32, (long)handle); + at.offset = v.place.offset + SLICE_PTR_OFFSET; + args[1] = fe_ir_load(L->m, L->b, FE_IR_PTR, at); + at.offset = v.place.offset + SLICE_LEN_OFFSET; + args[2] = fe_ir_load(L->m, L->b, FE_IR_I32, at); + fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_write", args, 3); + return; + } + if (t && t->kind == FE_TYPE_BOOL) { + /* Two literals and a branch: cheaper than a runtime that knows about + Ferro's names for truth. */ + FeIrBlock *yes = new_block(L); + FeIrBlock *no = new_block(L); + FeIrBlock *join = new_block(L); + fe_ir_br(L->b, as_value(L, v, arg), yes->id, no->id); + L->b = yes; + emit_text(L, handle, "true", 4); + fe_ir_jmp(L->b, join->id); + L->b = no; + emit_text(L, handle, "false", 5); + fe_ir_jmp(L->b, join->id); + L->b = join; + return; + } + if (verb == 'c' || (t && t->kind == FE_TYPE_CHAR)) { + /* One byte, written from a slot of its own so it has an address. */ + unsigned cell = fe_ir_local(L->m, L->fn, FE_IR_I8, 1, 1, "char"); + fe_ir_store(L->m, L->b, fe_ir_at_local(cell, 0), as_value(L, v, arg), + FE_IR_I8); + args[0] = fe_ir_const(L->m, L->b, FE_IR_I32, (long)handle); + args[1] = fe_ir_addr(L->m, L->b, fe_ir_at_local(cell, 0)); + args[2] = fe_ir_const(L->m, L->b, FE_IR_I32, 1); + fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_write", args, 3); + return; + } + args[0] = fe_ir_const(L->m, L->b, FE_IR_I32, (long)handle); + args[1] = as_value(L, v, arg); + if (verb == 'x') { + fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_write_hex", args, 2); + return; + } + args[2] = fe_ir_const(L->m, L->b, FE_IR_I32, + type_is_unsigned(t) || (t && t->kind == FE_TYPE_ENUM) + ? 1 : 0); + fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_write_int", args, 3); +} + +/* `@print(fmt, ...)`, `@fprint(w, fmt, ...)`. The checker has already agreed + that the string is a literal and that the count matches. */ +int lower_print(Lower *L, FeNode *n, Slot *out) +{ + const char *name = n->text; + int to_writer; + unsigned handle; + FeNode *fmt; + FeNode *arg; + const char *text; + unsigned long raw; + unsigned long i; + unsigned long chunk; + char plain[1024]; + unsigned long plain_len; + if (!name || (strcmp(name, "@print") != 0 && strcmp(name, "@fprint") != 0)) + return 0; + to_writer = strcmp(name, "@fprint") == 0; + fmt = n->children; + if (to_writer) { + /* A Writer is a handle; Stdout is 1 and Stderr is 2 (std.io). */ + Slot w = lower_expr(L, fmt); + handle = 0; + (void)w; + fmt = fmt ? fmt->next : 0; + } + handle = to_writer ? 2 : 1; + if (!fmt || !fmt->text || fmt->text[0] != '"') { + fail(L, "a format string that is not a literal", n); + *out = slot_void(); + return 1; + } + text = fmt->text + 1; + raw = strlen(fmt->text); + if (raw >= 2) raw -= 2; + arg = fmt->next; + plain_len = 0; + chunk = 0; + (void)chunk; + for (i = 0; i < raw; ++i) { + char ch = text[i]; + if (ch == 92 && i + 1 < raw) { /* an escape */ + ++i; + switch (text[i]) { + case 'n': ch = 10; break; + case 't': ch = 9; break; + case 'r': ch = 13; break; + case '0': ch = 0; break; + default: ch = text[i]; break; + } + if (plain_len + 1 < sizeof plain) plain[plain_len++] = ch; + continue; + } + if (ch == '{') { + int verb = ' '; + unsigned long close = i + 1; + while (close < raw && text[close] != '}') ++close; + if (close == i + 2) verb = text[i + 1]; + emit_text(L, handle, plain, plain_len); + plain_len = 0; + emit_value_text(L, handle, arg, verb); + if (arg) arg = arg->next; + i = close; + continue; + } + if (ch == '}') continue; /* `}}` is one brace */ + if (plain_len + 1 < sizeof plain) plain[plain_len++] = ch; + } + emit_text(L, handle, plain, plain_len); + *out = slot_void(); + return 1; +} + +Slot lower_call(Lower *L, FeNode *n) +{ + unsigned args[16]; + unsigned count = 0; + FeNode *arg = n->children; + FeType *ret = n->sem_type; + FeIrType rt = ir_type(ret); + unsigned result_local = 0; + const char *callee = n->a && n->a->cname ? n->a->cname : + (n->sem_decl && n->sem_decl->cname ? + n->sem_decl->cname : 0); + { + Slot built; + if (lower_builtin(L, n, &built)) return built; + if (lower_mem(L, n, &built)) return built; + if (lower_print(L, n, &built)) return built; + } + /* `Enum.Variant(payload)` is a constructor, not a call. */ + if (ret && ret->kind == FE_TYPE_ENUM && n->a && n->a->kind == FE_N_MEMBER && + n->a->b && n->a->b->text) { + const FeVariantType *v = fe_type_variant(ret, n->a->b->text); + if (v) { + unsigned local = scratch(L, ret, "variant"); + unsigned tag = fe_ir_const(L->m, L->b, tag_type_of(ret), + (long)v->tag); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), tag, + tag_type_of(ret)); + if (v->field_count && n->children) + store_into(L, + fe_ir_at_local(local, + (long)fe_type_payload_offset(ret) + + (long)v->fields[0].offset), + lower_expr(L, n->children), n->children, + ir_size(v->fields[0].type)); + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, + ir_size(ret)); + } + } + if (!callee) { fail(L, "a call with no target", n); return slot_void(); } + /* An aggregate result is written through a hidden first argument. */ + if (rt == FE_IR_MEM) { + result_local = fe_ir_local(L->m, L->fn, FE_IR_MEM, ir_size(ret), + ir_align(ret), "result"); + args[count++] = fe_ir_addr(L->m, L->b, fe_ir_at_local(result_local, 0)); + } + /* A method call passes what it was reached through as its first argument. + `self: Self` and `self: &Self` are the same thing here: the address of + the receiver, because an aggregate never travels in a register. */ + if (n->a && n->a->kind == FE_N_MEMBER && n->sem_decl) { + FeNode *first = n->sem_decl->a ? n->sem_decl->a->children : 0; + if (first && first->text && !strcmp(first->text, "self")) { + FeType *rt = n->a->a ? n->a->a->sem_type : 0; + Slot recv = lower_expr(L, n->a->a); + /* A receiver that is already a reference or an owner is a pointer + already; taking its address would pass a pointer to the + pointer. */ + if (rt && (rt->kind == FE_TYPE_REF || + (rt->kind == FE_TYPE_OWNED && ir_type(rt) == FE_IR_PTR))) + args[count++] = as_value(L, recv, n->a->a); + else + args[count++] = recv.is_place ? as_address(L, recv, n->a->a) + : recv.temp; + } + } + /* A generic call passes its type arguments first. They were consumed when + the instance was chosen and carry no value, so they are not passed. */ + { + FeNode *p; + for (p = n->sem_decl && n->sem_decl->a ? n->sem_decl->a->children : 0; + p && arg; p = p->next) { + if (!(p->flags & FE_NODE_COMPTIME)) break; + arg = arg->next; + } + } + for (; arg; arg = arg->next) { + Slot a = lower_expr(L, arg); + if (count >= 16) { fail(L, "too many arguments", n); break; } + args[count++] = a.type == FE_IR_MEM ? as_address(L, a, arg) + : as_value(L, a, arg); + } + if (rt == FE_IR_MEM) { + fe_ir_call(L->m, L->b, FE_IR_VOID, callee, args, count); + return slot_place(fe_ir_at_local(result_local, 0), FE_IR_MEM, + ir_size(ret)); + } + if (rt == FE_IR_VOID) { + fe_ir_call(L->m, L->b, FE_IR_VOID, callee, args, count); + return slot_void(); + } + return slot_value(fe_ir_call(L->m, L->b, rt, callee, args, count), rt); +} + + +/* Every expression may be standing where a wrapper is expected, so the wrap is + applied once, here, rather than at each place that could need it. */ +Slot lower_expr(Lower *L, FeNode *n) +{ + Slot v; + if (!n || L->failed) return slot_void(); + v = lower_expr_core(L, n); + /* The checker marked the uses that hand ownership away. Where one names a + local we track, the value is no longer ours to release. */ + if ((n->flags & FE_OWN_NODE_CONSUMED) && n->kind == FE_N_IDENT) { + LowerVar *var = find_var(L, n->cname); + unsigned flag; + if (var && release_flag(L, var->local, &flag)) { + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I8, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(flag, 0), zero, FE_IR_I8); + } + } + return n->sem_context ? wrap_context(L, v, n) : v; +} diff --git a/fec/src/lowerstm.c b/fec/src/lowerstm.c new file mode 100644 index 0000000..78862ac --- /dev/null +++ b/fec/src/lowerstm.c @@ -0,0 +1,706 @@ +#include "lowerpri.h" + +void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, + unsigned long size) +{ + if (value.type == FE_IR_MEM) { + if (!value.is_place) { fail(L, "an aggregate value", n); return; } + fe_ir_copy(L->m, L->b, dst, value.place, size); + return; + } + /* How wide the store is belongs to the place, not to the value. An + integer literal is `i32` until something narrower asks for it, so + `let b: u8 = 200;` arrives here as four bytes going into one -- and + writing four wipes out whatever the frame put next to it. */ + fe_ir_store(L->m, L->b, dst, as_value(L, value, n), + size == 1UL ? FE_IR_I8 : + size == 2UL ? FE_IR_I16 : value.type); +} + +void lower_return(Lower *L, FeNode *n) +{ + Slot v; + if (!n->a) { + /* A bare return from a `!void` function still has to say that nothing + went wrong. */ + if (L->ret_type && L->ret_type->kind == FE_TYPE_ERROR_UNION) { + unsigned local = scratch(L, L->ret_type, "success"); + unsigned none = fe_ir_const(L->m, L->b, FE_IR_I16, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), none, FE_IR_I16); + run_deferred(L, 0); + if (L->fn->returns_by_address) { + unsigned dst = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(L->ret_local, 0)); + fe_ir_copy(L->m, L->b, fe_ir_at_temp(dst, 0), + fe_ir_at_local(local, 0), ir_size(L->ret_type)); + fe_ir_ret(L->b, 0, 0); + return; + } + fe_ir_ret(L->b, fe_ir_load(L->m, L->b, ir_type(L->ret_type), + fe_ir_at_local(local, 0)), 1); + return; + } + run_deferred(L, 0); + fe_ir_ret(L->b, 0, 0); + return; + } + /* The value is computed before the deferred blocks run, because they may + destroy what it was read from. */ + v = lower_expr(L, n->a); + if (v.type != FE_IR_MEM && v.is_place) + v = slot_value(as_value(L, v, n->a), v.type); + run_deferred(L, 0); + if (L->fn->returns_by_address) { + unsigned dst = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(L->ret_local, 0)); + store_into(L, fe_ir_at_temp(dst, 0), v, n, ir_size(L->ret_type)); + fe_ir_ret(L->b, 0, 0); + return; + } + fe_ir_ret(L->b, as_value(L, v, n->a), 1); +} + +void lower_if(Lower *L, FeNode *n) +{ + FeIrBlock *then_b = new_block(L); + FeIrBlock *else_b = n->c ? new_block(L) : 0; + FeIrBlock *join = new_block(L); + unsigned cond = as_value(L, lower_expr(L, n->a), n->a); + fe_ir_br(L->b, cond, then_b->id, else_b ? else_b->id : join->id); + L->b = then_b; + lower_stmt(L, n->b); + fe_ir_jmp(L->b, join->id); + if (else_b) { + L->b = else_b; + lower_stmt(L, n->c); + fe_ir_jmp(L->b, join->id); + } + L->b = join; +} + +void lower_while(Lower *L, FeNode *n) +{ + FeIrBlock *head = new_block(L); + FeIrBlock *body = new_block(L); + FeIrBlock *done = new_block(L); + unsigned cond; + fe_ir_jmp(L->b, head->id); + L->b = head; + cond = as_value(L, lower_expr(L, n->a), n->a); + fe_ir_br(L->b, cond, body->id, done->id); + if (L->loop_depth < 32) { + L->break_target[L->loop_depth] = done->id; + L->continue_target[L->loop_depth] = head->id; + ++L->loop_depth; + } + L->b = body; + lower_stmt(L, n->b); + fe_ir_jmp(L->b, head->id); + if (L->loop_depth) --L->loop_depth; + L->b = done; +} + +/* `x[a..b]` makes a pointer and a length out of part of something indexable. + Both ends are checked -- against each other and against what is there -- + before the pointer is formed. An empty slice of a valid range is fine; one + that starts past its end is not. */ +Slot lower_slice(Lower *L, FeNode *n) +{ + FeType *bt = n->a ? n->a->sem_type : 0; + FeType *elem = bt ? bt->elem : 0; + FeType *t = n->sem_type; + Slot base = lower_expr(L, n->a); + unsigned data; + unsigned length; + unsigned from; + unsigned to; + unsigned local; + unsigned scale; + unsigned off; + unsigned at; + unsigned count; + indexable_parts(L, base, bt, &data, &length, n); + from = n->b ? as_value(L, lower_expr(L, n->b), n->b) + : fe_ir_const(L->m, L->b, FE_IR_I32, 0); + to = n->c ? as_value(L, lower_expr(L, n->c), n->c) : length; + if (!L->c->no_checks) { + unsigned ordered = fe_ir_binary(L->m, L->b, FE_IR_LE, FE_IR_I32, + from, to, 1); + unsigned within; + guard(L, ordered, FE_TRAP_BOUNDS, n->loc.line); + within = fe_ir_binary(L->m, L->b, FE_IR_LE, FE_IR_I32, to, length, 1); + guard(L, within, FE_TRAP_BOUNDS, n->loc.line); + } + scale = fe_ir_const(L->m, L->b, FE_IR_I32, (long)ir_size(elem)); + off = fe_ir_binary(L->m, L->b, FE_IR_MUL, FE_IR_I32, from, scale, 1); + at = fe_ir_binary(L->m, L->b, FE_IR_ADD, FE_IR_PTR, data, off, 1); + count = fe_ir_binary(L->m, L->b, FE_IR_SUB, FE_IR_I32, to, from, 1); + local = scratch(L, t, "slice"); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, SLICE_PTR_OFFSET), at, + FE_IR_PTR); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, SLICE_LEN_OFFSET), count, + FE_IR_I32); + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); +} + +/* Three shapes share the keyword. + + for i in a..b { } counts + for x in thing { } walks, binding a reference to each element + for i, x in thing { } walks, binding the position as well + + The count is read once before the body, so a thing that grows underneath the + loop cannot walk past what was measured. The element binding is a reference + (`x.^` reads it), which is what lets a loop write back into the thing. */ +void lower_for(Lower *L, FeNode *n) +{ + FeIrBlock *head; + FeIrBlock *body; + FeIrBlock *step; + FeIrBlock *done; + unsigned counter; + unsigned limit; + + if (n->c) { + /* The counting form: the variable is the count itself. */ + unsigned from = as_value(L, lower_expr(L, n->a), n->a); + unsigned to; + counter = declare_var(L, n->cname, 0, n->text); + L->fn->locals[counter].type = FE_IR_I32; + L->fn->locals[counter].size = 4; + L->fn->locals[counter].align = 4; + fe_ir_store(L->m, L->b, fe_ir_at_local(counter, 0), from, FE_IR_I32); + to = as_value(L, lower_expr(L, n->c), n->c); + limit = fe_ir_local(L->m, L->fn, FE_IR_I32, 4, 4, "limit"); + fe_ir_store(L->m, L->b, fe_ir_at_local(limit, 0), to, FE_IR_I32); + head = new_block(L); + body = new_block(L); + step = new_block(L); + done = new_block(L); + fe_ir_jmp(L->b, head->id); + L->b = head; + { + unsigned i = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(counter, 0)); + unsigned e = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(limit, 0)); + unsigned more = fe_ir_binary(L->m, L->b, FE_IR_LT, FE_IR_I32, i, e, 1); + fe_ir_br(L->b, more, body->id, done->id); + } + } else { + FeType *bt = n->a ? n->a->sem_type : 0; + FeType *elem = bt ? bt->elem : 0; + Slot base = lower_expr(L, n->a); + unsigned data; + unsigned length; + unsigned data_local; + unsigned item; + indexable_parts(L, base, bt, &data, &length, n); + data_local = fe_ir_local(L->m, L->fn, FE_IR_PTR, 4, 4, "data"); + fe_ir_store(L->m, L->b, fe_ir_at_local(data_local, 0), data, FE_IR_PTR); + limit = fe_ir_local(L->m, L->fn, FE_IR_I32, 4, 4, "count"); + fe_ir_store(L->m, L->b, fe_ir_at_local(limit, 0), length, FE_IR_I32); + /* With two names the first is the position and the second the element; + with one it is the element. */ + counter = fe_ir_local(L->m, L->fn, FE_IR_I32, 4, 4, "index"); + if (n->aux_cname) { + (void)lower_reserve(L, (void **)&L->vars, &L->var_capacity, + L->var_count, + (unsigned long)sizeof(LowerVar)); + L->vars[L->var_count].cname = n->cname; + L->vars[L->var_count].local = counter; + L->vars[L->var_count].by_address = 0; + ++L->var_count; + item = fe_ir_local(L->m, L->fn, FE_IR_PTR, 4, 4, n->aux_text); + (void)lower_reserve(L, (void **)&L->vars, &L->var_capacity, + L->var_count, + (unsigned long)sizeof(LowerVar)); + L->vars[L->var_count].cname = n->aux_cname; + L->vars[L->var_count].local = item; + L->vars[L->var_count].by_address = 0; + ++L->var_count; + } else { + item = fe_ir_local(L->m, L->fn, FE_IR_PTR, 4, 4, n->text); + (void)lower_reserve(L, (void **)&L->vars, &L->var_capacity, + L->var_count, + (unsigned long)sizeof(LowerVar)); + L->vars[L->var_count].cname = n->cname; + L->vars[L->var_count].local = item; + L->vars[L->var_count].by_address = 0; + ++L->var_count; + } + { + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I32, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(counter, 0), zero, FE_IR_I32); + } + head = new_block(L); + body = new_block(L); + step = new_block(L); + done = new_block(L); + fe_ir_jmp(L->b, head->id); + L->b = head; + { + unsigned i = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(counter, 0)); + unsigned e = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(limit, 0)); + unsigned more = fe_ir_binary(L->m, L->b, FE_IR_LT, FE_IR_I32, i, e, 1); + fe_ir_br(L->b, more, body->id, done->id); + } + L->b = body; + { + unsigned i = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(counter, 0)); + unsigned scale = fe_ir_const(L->m, L->b, FE_IR_I32, + (long)ir_size(elem)); + unsigned off = fe_ir_binary(L->m, L->b, FE_IR_MUL, FE_IR_I32, i, + scale, 1); + unsigned p = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(data_local, 0)); + unsigned at = fe_ir_binary(L->m, L->b, FE_IR_ADD, FE_IR_PTR, p, + off, 1); + fe_ir_store(L->m, L->b, fe_ir_at_local(item, 0), at, FE_IR_PTR); + } + L->b = head; + } + + if (L->loop_depth < 32) { + L->break_target[L->loop_depth] = done->id; + L->continue_target[L->loop_depth] = step->id; + ++L->loop_depth; + } + L->b = body; + lower_stmt(L, n->b); + fe_ir_jmp(L->b, step->id); + L->b = step; + { + unsigned i = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(counter, 0)); + unsigned one = fe_ir_const(L->m, L->b, FE_IR_I32, 1); + unsigned next = fe_ir_binary(L->m, L->b, FE_IR_ADD, FE_IR_I32, i, one, 1); + fe_ir_store(L->m, L->b, fe_ir_at_local(counter, 0), next, FE_IR_I32); + } + fe_ir_jmp(L->b, head->id); + if (L->loop_depth) --L->loop_depth; + L->b = done; +} + +/* `match` over a payload-free enum or an integer: compare the tag against each + arm's pattern in turn. The checker already proved the arms cover everything, + so falling off the end cannot happen in a program that compiled -- but the + generated code has to go somewhere, and going to the join is right. */ +/* The width of a tag: an enum's own, or the byte an optional puts in front. */ +FeIrType tag_type_of(const FeType *t) +{ + if (!t) return FE_IR_I8; + if (t->kind == FE_TYPE_ERROR_UNION) return FE_IR_I16; + if (t->kind == FE_TYPE_ENUM) return t->bits > 8U ? FE_IR_I16 : FE_IR_I8; + return FE_IR_I8; +} + +/* Give an arm's names somewhere to live and put the variant's payload there. + The payload is copied rather than pointed at: an arm that takes ownership of + what it matched is the normal case, and the checker has already decided + whether that was allowed. */ +void bind_payload(Lower *L, Slot subject, const FeType *t, + const FeVariantType *v, FeNode *arm) +{ + FeNode *name; + unsigned i; + long base; + if (!v || !v->field_count || !arm->children || !subject.is_place) return; + base = (long)fe_type_payload_offset(t); + name = arm->children; + for (i = 0; i < v->field_count && name; ++i, name = name->next) { + FeType *ft = v->fields[i].type; + unsigned local = declare_var(L, name->cname, ft, name->text); + FeIrPlace from = subject.place; + from.offset += base + (long)v->fields[i].offset; + store_into(L, fe_ir_at_local(local, 0), + slot_place(from, ir_type(ft), ir_size(ft)), name, + ir_size(ft)); + } +} + +/* `if let Some(x) = opt { .. } else { .. }` -- and its None twin. + + The optional is read once into a place, the tag decides the branch, and the + binding gets what was inside. A binding whose type is a reference gets the + address instead of a copy: the checker chose that when the payload was not + something you may quietly duplicate. */ +void lower_if_let(Lower *L, FeNode *n) +{ + FeType *opt = n->a ? n->a->sem_type : 0; + Slot value = lower_expr(L, n->a); + FeNode *binding = n->children; + int is_some = n->aux_text && !strcmp(n->aux_text, "Some"); + unsigned tag; + FeIrBlock *present; + FeIrBlock *absent; + FeIrBlock *join; + if (!value.is_place) { fail(L, "if let over a temporary", n); return; } + tag = wrapper_tag(L, value, opt, n); + present = new_block(L); + absent = new_block(L); + join = new_block(L); + fe_ir_br(L->b, tag, present->id, absent->id); + /* Which side runs the body depends on which pattern was written. */ + L->b = is_some ? present : absent; + if (is_some && binding) { + FeType *bt = binding->sem_type; + Slot payload = wrapper_payload(L, value, opt); + unsigned local = declare_var(L, binding->cname, bt, binding->text); + if (bt && (bt->kind == FE_TYPE_REF || bt->kind == FE_TYPE_RAW)) { + /* The binding is a reference either way, but for two different + reasons. When the payload is itself a single pointer (`^T`, + `&T`) the binding *is* that pointer, so it has to be read out. + When the payload is a value the binding points at where it sits + inside the wrapper, so the address is what is wanted. Taking the + address in the first case gives a pointer to the pointer, and + the program reads an address where it expects a value. */ + FeType *pl = opt ? (opt->kind == FE_TYPE_ERROR_UNION + ? opt->error_value : opt->elem) : 0; + unsigned p = pl && ir_type(pl) == FE_IR_PTR + ? as_value(L, payload, n) : as_address(L, payload, n); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), p, FE_IR_PTR); + } else + store_into(L, fe_ir_at_local(local, 0), payload, n, ir_size(bt)); + } + lower_stmt(L, n->b); + fe_ir_jmp(L->b, join->id); + L->b = is_some ? absent : present; + if (n->c) lower_stmt(L, n->c); + fe_ir_jmp(L->b, join->id); + L->b = join; +} + +void lower_match(Lower *L, FeNode *n) +{ + FeType *t = n->a ? n->a->sem_type : 0; + FeIrType it = ir_type(t); + Slot subject = lower_expr(L, n->a); + unsigned value; + FeIrBlock *join; + FeNode *arm; + /* A variant that carries something is memory: the tag comes first and the + payload after it. Reading the tag is then the same question either way, + just from a different place. */ + if (it == FE_IR_MEM) { + if (!subject.is_place) { fail(L, "a match over a temporary", n); return; } + it = tag_type_of(t); + value = fe_ir_load(L->m, L->b, it, subject.place); + } else { + value = as_value(L, subject, n->a); + } + join = new_block(L); + for (arm = n->children; arm; arm = arm->next) { + FeIrBlock *body; + FeIrBlock *next; + unsigned want; + unsigned same; + FeVariantType *v; + if (arm->kind != FE_N_ARM) continue; + if (arm->text && !strcmp(arm->text, "_")) { + lower_stmt(L, arm->a); + fe_ir_jmp(L->b, join->id); + L->b = join; + return; + } + v = t && t->kind == FE_TYPE_ENUM && arm->text + ? fe_type_variant(t, arm->text) : 0; + want = fe_ir_const(L->m, L->b, it, + v ? (long)v->tag : literal_value(arm)); + same = fe_ir_binary(L->m, L->b, FE_IR_EQ, it, value, want, 1); + body = new_block(L); + next = new_block(L); + fe_ir_br(L->b, same, body->id, next->id); + L->b = body; + bind_payload(L, subject, t, v, arm); + lower_stmt(L, arm->a); + fe_ir_jmp(L->b, join->id); + L->b = next; + } + fe_ir_jmp(L->b, join->id); + L->b = join; +} + +void lower_stmt(Lower *L, FeNode *n) +{ + FeNode *x; + if (!n || L->failed) return; + switch (n->kind) { + case FE_N_BLOCK: { + unsigned outer = L->owed_count; + for (x = n->children; x; x = x->next) lower_stmt(L, x); + /* Leaving a block normally settles what it owes. An exit that jumped + away already settled on its way out. */ + if (!L->b->terminated) run_deferred(L, outer); + L->owed_count = outer; + return; + } + case FE_N_LET: + case FE_N_VAR: + case FE_N_CONST: { + unsigned local = declare_var(L, n->cname, n->sem_type, n->text); + /* `undefined` says the storage starts out unset, so there is nothing + to write into it. */ + if (n->b && n->b->kind == FE_N_LITERAL && n->b->text && + !strcmp(n->b->text, "undefined")) return; + if (n->b) { + Slot v = lower_expr(L, n->b); + unsigned flag; + store_into(L, fe_ir_at_local(local, 0), v, n, ir_size(n->sem_type)); + if (release_flag(L, local, &flag)) { + unsigned one = fe_ir_const(L->m, L->b, FE_IR_I8, 1); + fe_ir_store(L->m, L->b, fe_ir_at_local(flag, 0), one, FE_IR_I8); + } + } + return; + } + case FE_N_ASSIGN: { + Slot dst = lower_expr(L, n->a); + Slot v = lower_expr(L, n->b); + if (!dst.is_place) { fail(L, "an assignment to a value", n); return; } + store_into(L, dst.place, v, n, dst.size); + return; + } + case FE_N_EXPR_STMT: + lower_expr(L, n->a); + return; + case FE_N_RETURN: + lower_return(L, n); + return; + case FE_N_IF: + if (n->text && !strcmp(n->text, "if let")) { lower_if_let(L, n); return; } + lower_if(L, n); + return; + case FE_N_WHILE: + lower_while(L, n); + return; + case FE_N_BREAK: + if (L->loop_depth) fe_ir_jmp(L->b, L->break_target[L->loop_depth - 1]); + return; + case FE_N_CONTINUE: + if (L->loop_depth) + fe_ir_jmp(L->b, L->continue_target[L->loop_depth - 1]); + return; + case FE_N_UNSAFE: + lower_stmt(L, n->a); + return; + case FE_N_DEFER: + if (lower_reserve(L, (void **)&L->owed, &L->owed_capacity, + L->owed_count, (unsigned long)sizeof *L->owed)) { + L->owed[L->owed_count].block = n->a; + L->owed[L->owed_count].local = 0; + L->owed[L->owed_count].flag = 0; + L->owed[L->owed_count].type = 0; + ++L->owed_count; + } + return; + case FE_N_FOR: + lower_for(L, n); + return; + case FE_N_MATCH: + lower_match(L, n); + return; + default: + fail(L, "this statement", n); + return; + } +} + +/* ------------------------------------------------------------ functions --- */ + +/* A global is static storage. SPEC 7.1: its initializer is evaluated at + compile time, so what reaches here is either a constant to place in the + image or nothing, and the storage starts as zeroes. */ +void lower_global(Lower *L, FeNode *n) +{ + FeType *t = n->sem_type; + unsigned char *init = 0; + unsigned long size = ir_size(t); + if (!n->cname) return; + /* A text constant is a pointer and a length. The pointer is not a number + anyone knows yet, so the bytes carry a hole and the linker fills it. */ + if (n->b && n->b->kind == FE_N_LITERAL && n->b->text && + n->b->text[0] == '"' && t && + (t->kind == FE_TYPE_SLICE || t->kind == FE_TYPE_STR)) { + char text[1024]; + unsigned long raw = strlen(n->b->text); + unsigned long len = 0; + unsigned long i; + const char *label; + FeIrGlobal *g; + if (raw >= 2) raw -= 2; + for (i = 0; i < raw && len + 1 < sizeof text; ++i) { + char ch = n->b->text[1 + i]; + if (ch == 92 && i + 1 < raw) { + ++i; + switch (n->b->text[1 + i]) { + case 'n': ch = 10; break; + case 't': ch = 9; break; + case 'r': ch = 13; break; + case '0': ch = 0; break; + default: ch = n->b->text[1 + i]; break; + } + } + text[len++] = ch; + } + label = fe_ir_string(L->m, text, len); + init = (unsigned char *)fe_arena_alloc(&L->m->arena, 8); + if (!init || !label) return; + for (i = 0; i < 8; ++i) init[i] = 0; + for (i = 0; i < 4; ++i) init[4 + i] = (unsigned char)((len >> (i * 8)) & 0xFF); + g = fe_ir_global(L->m, n->cname, FE_IR_MEM, 8, 4, init); + fe_ir_global_ref(L->m, g, (unsigned long)SLICE_PTR_OFFSET, label); + return; + } + if (n->b && n->b->kind == FE_N_LITERAL && size && size <= 8) { + long v = literal_value(n->b); + unsigned long i; + init = (unsigned char *)fe_arena_alloc(&L->m->arena, (size_t)size); + if (init) + for (i = 0; i < size; ++i) + init[i] = (unsigned char)((v >> (i * 8)) & 0xFF); + } + fe_ir_global(L->m, n->cname, ir_type(t), size, ir_align(t), init); +} + +/* A declaration with type parameters is a pattern, not code. */ +int struct_is_generic(const FeNode *decl) +{ + return decl && decl->a && decl->a->children != 0; +} + +int fn_is_generic(const FeNode *fn) +{ + FeNode *p; + if (!fn) return 0; + for (p = fn->a ? fn->a->children : 0; p; p = p->next) + if (p->flags & FE_NODE_COMPTIME) return 1; + return 0; +} + +void lower_fn_as(Lower *L, FeNode *fn, const char *name) +{ + FeNode *p; + FeType *ret = fn->b ? fe_type_from_ast(&L->c->types, fn->b) : 0; + FeIrFunc *f; + if (!name) return; + f = fe_ir_func(L->m, name, ir_type(ret), ir_size(ret)); + if (!f) return; + L->fn = f; + L->ret_type = ret; + L->var_count = 0; + L->loop_depth = 0; + /* A hidden first parameter holds where an aggregate result goes. */ + if (f->returns_by_address) + L->ret_local = fe_ir_local(L->m, f, FE_IR_PTR, 4, 4, "result"); + for (p = fn->a ? fn->a->children : 0; p; p = p->next) { + FeType *pt; + int by_address; + unsigned local; + /* A comptime parameter was consumed at compile time; it has no + storage and takes no argument slot. */ + if (p->flags & FE_NODE_COMPTIME) continue; + pt = fe_type_from_ast(&L->c->types, p->a); + /* An aggregate parameter arrives as an address. */ + by_address = ir_type(pt) == FE_IR_MEM; + local = by_address + ? fe_ir_local(L->m, f, FE_IR_PTR, 4, 4, p->text) + : fe_ir_local(L->m, f, ir_type(pt), ir_size(pt), ir_align(pt), + p->text); + if (lower_reserve(L, (void **)&L->vars, &L->var_capacity, + L->var_count, (unsigned long)sizeof(LowerVar))) { + L->vars[L->var_count].cname = p->cname; + L->vars[L->var_count].local = local; + L->vars[L->var_count].by_address = by_address; + ++L->var_count; + } + } + f->param_count = f->local_count; + L->b = fe_ir_block(L->m, f); + lower_stmt(L, fn->c); + /* A void function may just run off the end. */ + fe_ir_ret(L->b, 0, 0); +} + +void lower_fn(Lower *L, FeNode *fn) +{ + lower_fn_as(L, fn, fn->cname); +} + +int fe_lower_program(FeCheck *c, FeIrModule *out) +{ + Lower L; + unsigned u; + FeNode *n; + memset(&L, 0, sizeof L); + L.c = c; + L.m = out; + /* The codes have to be known while the bodies are lowered, so the names + are gathered from the whole build first. */ + for (u = 0; u < c->build->count; ++u) + collect_error_names(&L, c->build->units[u].ast.root); + for (u = 0; u < c->build->count; ++u) { + FeUnit *unit = &c->build->units[u]; + c->ast = &unit->ast; + c->unit = unit; + c->types.unit_name = unit->name[0] ? unit->name : "unit"; + for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) + if (n->kind == FE_N_GLOBAL || n->kind == FE_N_CONST) + lower_global(&L, n); + else if (n->kind == FE_N_STRUCT && !struct_is_generic(n)) { + /* A method is a function whose first parameter is the value it + was reached through; the storage is the same either way. */ + FeNode *m; + for (m = n->children; m; m = m->next) + if (m->kind == FE_N_FN && m->c) lower_fn(&L, m); + } + else if (n->kind == FE_N_FN && !n->c) { + /* A declaration with no body is something the linker will + find: the runtime, or a C library. */ + FeType *ret = n->b ? fe_type_from_ast(&c->types, n->b) : 0; + FeIrFunc *f; + if (!n->cname) continue; + f = fe_ir_func(out, n->cname, ir_type(ret), ir_size(ret)); + if (f) f->is_extern = 1; + } + else if (n->kind == FE_N_FN && n->c && !fn_is_generic(n)) { + lower_fn(&L, n); + /* The entry unit is the one the build was rooted at. */ + if (u == 0 && n->text && !strcmp(n->text, "main")) + out->entry_main = n->cname; + } + } + /* Each instance the checker reached is a function of its own: the same + body, read with different types bound, under its own link name. This is + where monomorphisation actually produces code -- the front end only + decided which instances exist. */ + for (u = 0; u < c->instance_count && !L.failed; ++u) { + FeInstance *inst = &c->instances[u]; + FeUnit *home; + FeTypeBind save[FE_TYPE_PARAM_MAX]; + unsigned save_count; + unsigned k; + if (!inst->decl || !inst->decl->c || !inst->cname || !inst->home) + continue; + home = 0; + for (k = 0; k < c->build->count; ++k) + if (!strcmp(c->build->units[k].name, inst->home)) + home = &c->build->units[k]; + if (!home) continue; + c->ast = &home->ast; + c->unit = home; + c->types.unit_name = home->name; + save_count = c->types.param_count; + for (k = 0; k < FE_TYPE_PARAM_MAX; ++k) save[k] = c->types.params[k]; + c->types.param_count = inst->bind_count; + for (k = 0; k < inst->bind_count && k < FE_TYPE_PARAM_MAX; ++k) + c->types.params[k] = inst->binds[k]; + lower_fn_as(&L, inst->decl, inst->cname); + c->types.param_count = save_count; + for (k = 0; k < FE_TYPE_PARAM_MAX; ++k) c->types.params[k] = save[k]; + } + return !L.failed; +} diff --git a/fec/src/m7.c b/fec/src/m7.c new file mode 100644 index 0000000..0cfd374 --- /dev/null +++ b/fec/src/m7.c @@ -0,0 +1,123 @@ +#include "m7.h" +#include +#include + +static char *m7_generated_name(FeTypeCtx *ctx, const char *prefix) +{ + char number[24]; + char *p; + unsigned long n; + sprintf(number, "%u", ctx->generated_serial++); + n = (unsigned long)strlen(prefix) + (unsigned long)strlen(number) + 1UL; + p = (char *)fe_arena_alloc(ctx->arena, n); + if (!p) return 0; + strcpy(p, prefix); + strcat(p, number); + return p; +} + +int fe_m7_optional_uses_niche(const FeType *payload) +{ + if (!payload) return 0; + if (payload->kind == FE_TYPE_REF) return 1; + if (payload->kind == FE_TYPE_OWNED && + !(payload->elem && payload->elem->kind == FE_TYPE_SLICE)) + return 1; + return 0; +} + +FeType *fe_m7_optional_type(FeTypeCtx *ctx, FeType *payload) +{ + char key[128]; + FeType *t; + if (!ctx || !payload) return 0; + sprintf(key, "?%s", payload->name); + t = fe_type_intern(ctx, key); + if (!t) return 0; + if (t->kind == FE_TYPE_UNKNOWN) { + t->kind = FE_TYPE_OPTIONAL; + t->elem = payload; + t->unwrap_cname = m7_generated_name(ctx, "fe_unwrap_option_"); + t->drop_cname = m7_generated_name(ctx, "fe_drop_option_"); + if (!fe_m7_optional_uses_niche(payload)) { + t->cname = m7_generated_name(ctx, "struct fe_option_"); + t->maker = m7_generated_name(ctx, "fe_make_option_"); + t->none_cname = m7_generated_name(ctx, "fe_none_option_"); + } + } + return t; +} + +int fe_m7_can_contextual_null(const FeType *expected) +{ + return expected && expected->kind == FE_TYPE_OPTIONAL; +} + +FeType *fe_m7_error_union_type(FeTypeCtx *ctx, FeType *error_type, + FeType *value_type) +{ + char key[160]; + FeType *t; + if (!ctx || !value_type) return 0; + if (!error_type || strcmp(error_type->name, "core.Error") == 0) + return fe_type_error_union(ctx, value_type); + sprintf(key, "%s!%s", error_type->name, value_type->name); + t = fe_type_intern(ctx, key); + if (!t) return 0; + if (t->kind == FE_TYPE_UNKNOWN) { + t->kind = FE_TYPE_ERROR_UNION; + t->elem = error_type; + t->error_value = value_type; + t->drop_cname = m7_generated_name(ctx, "fe_drop_result_"); + if (value_type->kind != FE_TYPE_VOID) { + t->cname = m7_generated_name(ctx, "struct fe_result_"); + t->maker = m7_generated_name(ctx, "fe_make_result_"); + t->none_cname = m7_generated_name(ctx, "fe_fail_result_"); + t->alloc_cname = m7_generated_name(ctx, "fe_alloc_result_"); + } + } + return t; +} + +FeType *fe_m7_error_type(FeTypeCtx *ctx, const FeType *error_union) +{ + if (!ctx || !error_union || error_union->kind != FE_TYPE_ERROR_UNION) + return 0; + if (error_union->elem) return error_union->elem; + return fe_type_intern(ctx, "core.Error"); +} + +FeM7ContextKind fe_m7_error_context(FeTypeCtx *ctx, const FeType *expected, + const FeType *actual) +{ + FeType *error_type; + if (!ctx || !expected || !actual || + expected->kind != FE_TYPE_ERROR_UNION) + return FE_M7_CONTEXT_NONE; + if (expected->error_value && fe_type_equal(expected->error_value, actual)) + return FE_M7_CONTEXT_SUCCESS; + error_type = fe_m7_error_type(ctx, expected); + if (error_type && fe_type_equal(error_type, actual)) + return FE_M7_CONTEXT_FAILURE; + return FE_M7_CONTEXT_NONE; +} + +FeM7LazyKind fe_m7_lazy_kind(const FeNode *node) +{ + if (!node || !node->text) return FE_M7_LAZY_NONE; + if (strcmp(node->text, "orelse") == 0) return FE_M7_LAZY_ORELSE; + if (strcmp(node->text, "catch") == 0) return FE_M7_LAZY_CATCH; + return FE_M7_LAZY_NONE; +} + +int fe_m7_is_try(const FeNode *node) +{ + return node && node->kind == FE_N_UNARY && node->text && + strcmp(node->text, "try") == 0; +} + +int fe_m7_is_null(const FeNode *node) +{ + return node && node->kind == FE_N_LITERAL && node->text && + strcmp(node->text, "null") == 0; +} diff --git a/fec/src/m7.h b/fec/src/m7.h new file mode 100644 index 0000000..fe0b078 --- /dev/null +++ b/fec/src/m7.h @@ -0,0 +1,32 @@ +#ifndef FE_M7_H +#define FE_M7_H + +#include "types.h" + +typedef enum FeM7ContextKind { + FE_M7_CONTEXT_NONE = 0, + FE_M7_CONTEXT_SUCCESS, + FE_M7_CONTEXT_FAILURE +} FeM7ContextKind; + +typedef enum FeM7LazyKind { + FE_M7_LAZY_NONE = 0, + FE_M7_LAZY_ORELSE, + FE_M7_LAZY_CATCH +} FeM7LazyKind; + +FeType *fe_m7_optional_type(FeTypeCtx *ctx, FeType *payload); +int fe_m7_optional_uses_niche(const FeType *payload); +int fe_m7_can_contextual_null(const FeType *expected); + +FeType *fe_m7_error_union_type(FeTypeCtx *ctx, FeType *error_type, + FeType *value_type); +FeType *fe_m7_error_type(FeTypeCtx *ctx, const FeType *error_union); +FeM7ContextKind fe_m7_error_context(FeTypeCtx *ctx, const FeType *expected, + const FeType *actual); + +FeM7LazyKind fe_m7_lazy_kind(const FeNode *node); +int fe_m7_is_try(const FeNode *node); +int fe_m7_is_null(const FeNode *node); + +#endif diff --git a/fec/src/own.c b/fec/src/own.c new file mode 100644 index 0000000..7217289 --- /dev/null +++ b/fec/src/own.c @@ -0,0 +1,748 @@ +#include "own.h" +#include + +static FeLoc fe_own_no_loc(void) +{ + FeLoc loc; + loc.file = 0; + loc.line = 0; + loc.col = 0; + return loc; +} + +static void fe_own_error_note(FeDiags *diags, FeLoc loc, const char *msg, + FeLoc note, const char *note_msg) +{ + fe_diag_error(diags, loc, msg); + if (note.file) fe_diag_note_src(diags, note, note_msg); +} + +int fe_own_is_copy_type(FeType *type) +{ + unsigned i; + if (!type) return 1; + if (type->kind == FE_TYPE_OWNED) return 0; + if (type->kind == FE_TYPE_REF || type->kind == FE_TYPE_SLICE) + return !type->ref_mut; + if (type->kind == FE_TYPE_OPTIONAL) + return fe_own_is_copy_type(type->elem); + if (type->kind == FE_TYPE_ERROR_UNION) + return fe_own_is_copy_type(type->error_value); + if (type->kind == FE_TYPE_ARRAY) + return fe_own_is_copy_type(type->elem); + if (type->kind == FE_TYPE_STRUCT) { + if (type->has_drop) return 0; + for (i = 0; i < type->field_count; ++i) + if (!fe_own_is_copy_type(type->fields[i].type)) return 0; + } + if (type->kind == FE_TYPE_ENUM) { + for (i = 0; i < type->variant_count; ++i) { + unsigned j; + for (j = 0; j < type->variants[i].field_count; ++j) + if (!fe_own_is_copy_type(type->variants[i].fields[j].type)) + return 0; + } + } + return 1; +} + +int fe_own_is_reference_like(FeType *type) +{ + return type && (type->kind == FE_TYPE_REF || + type->kind == FE_TYPE_SLICE || + type->kind == FE_TYPE_STR); +} + +static FeNode *fe_own_root_expr(FeNode *expr) +{ + if (!expr) return 0; + if (expr->kind == FE_N_IDENT) return expr; + if (expr->kind == FE_N_MEMBER || expr->kind == FE_N_INDEX) + return fe_own_root_expr(expr->a); + if (expr->kind == FE_N_UNARY && expr->text && + (strcmp(expr->text, "&") == 0 || strcmp(expr->text, "&mut") == 0)) + return fe_own_root_expr(expr->a); + return 0; +} + +static int fe_own_expr_has_projection(FeNode *expr) +{ + if (!expr) return 0; + if (expr->kind == FE_N_MEMBER || expr->kind == FE_N_INDEX) return 1; + if (expr->kind == FE_N_UNARY && expr->text && + (strcmp(expr->text, "&") == 0 || strcmp(expr->text, "&mut") == 0)) + return fe_own_expr_has_projection(expr->a); + return 0; +} + +int fe_own_place_from_expr(FeNode *expr, FeOwnPlace *place) +{ + FeNode *root; + if (!place) return 0; + place->root = 0; + place->root_cname = 0; + place->projected = 0; + root = fe_own_root_expr(expr); + if (!root) return 0; + place->root = root; + place->root_cname = root->cname ? root->cname : root->text; + place->projected = fe_own_expr_has_projection(expr); + return place->root_cname != 0; +} + +void fe_own_state_init(FeOwnState *state, int initialized) +{ + unsigned i; + if (!state) return; + state->move = FE_OWN_AVAILABLE; + state->initialized = initialized != 0; + state->shared = 0; + state->exclusive = 0; + state->borrow_conflict = 0; + state->move_loc = fe_own_no_loc(); + state->borrow_loc = fe_own_no_loc(); + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) { + state->fields[i].name = 0; + state->fields[i].shared = 0; + state->fields[i].exclusive = 0; + state->fields[i].loc = fe_own_no_loc(); + } +} + +static int fe_own_require_value(FeDiags *diags, FeOwnState *state, FeLoc loc) +{ + if (state->move == FE_OWN_MOVED) { + fe_own_error_note(diags, loc, "use of moved value", state->move_loc, + "value was moved here"); + return 0; + } + if (state->move == FE_OWN_MAYBE_MOVED) { + fe_diag_error(diags, loc, "use of possibly moved value"); + return 0; + } + if (!state->initialized) { + fe_diag_error(diags, loc, "use of uninitialized variable"); + return 0; + } + return 1; +} + +static int fe_own_require_stable_borrow(FeDiags *diags, FeOwnState *state, + FeLoc loc) +{ + if (!state->borrow_conflict) return 1; + fe_own_error_note(diags, loc, + "incompatible borrow state across control-flow paths", + state->borrow_loc, "borrow originated here"); + return 0; +} + +/* Whole-value state only: what a field access has to get past before it looks + at its own entry. `check` reports and decides; `apply` also records. */ +static int fe_own_access_whole(FeDiags *diags, FeOwnState *state, + FeOwnAccessKind access, FeLoc loc); +static int fe_own_access_whole_check(FeDiags *diags, FeOwnState *state, + FeOwnAccessKind access, FeLoc loc); + +/* The entry for this field, or null. `make` asks for one to be created. */ +static FeOwnField *fe_own_field_slot(FeOwnState *state, const char *field, + int make) +{ + unsigned i; + unsigned free_slot = FE_OWN_FIELD_MAX; + if (!state || !field) return 0; + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) { + if (state->fields[i].name && + strcmp(state->fields[i].name, field) == 0) return &state->fields[i]; + if (!state->fields[i].name && free_slot == FE_OWN_FIELD_MAX) + free_slot = i; + } + if (!make || free_slot == FE_OWN_FIELD_MAX) return 0; + state->fields[free_slot].name = field; + state->fields[free_slot].shared = 0; + state->fields[free_slot].exclusive = 0; + state->fields[free_slot].loc = fe_own_no_loc(); + return &state->fields[free_slot]; +} + +/* A live borrow of some field, for the accesses that reach the whole value. */ +static const FeOwnField *fe_own_field_live(const FeOwnState *state, + int mut_only) +{ + unsigned i; + if (!state) return 0; + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) { + const FeOwnField *f = &state->fields[i]; + if (!f->name) continue; + if (f->exclusive) return f; + if (!mut_only && f->shared) return f; + } + return 0; +} + +void fe_own_release_shared_field(FeOwnState *state, const char *field) +{ + FeOwnField *f = fe_own_field_slot(state, field, 0); + if (!f || !f->shared) { fe_own_release_shared(state); return; } + --f->shared; + if (!f->shared && !f->exclusive) f->name = 0; +} + +void fe_own_release_exclusive_field(FeOwnState *state, const char *field) +{ + FeOwnField *f = fe_own_field_slot(state, field, 0); + if (!f || !f->exclusive) { fe_own_release_exclusive(state); return; } + f->exclusive = 0; + if (!f->shared) f->name = 0; +} + +int fe_own_access(FeDiags *diags, FeOwnState *state, + FeOwnAccessKind access, FeLoc loc) +{ + return fe_own_access_field(diags, state, 0, access, loc); +} + +int fe_own_access_field(FeDiags *diags, FeOwnState *state, const char *field, + FeOwnAccessKind access, FeLoc loc) +{ + FeOwnField *f; + const FeOwnField *other; + if (!state) return 0; + if (access == FE_OWN_PROJECTION) return 1; + if (!field) { + /* Reaching the whole value: a borrow of any part of it is in the way. + A shared borrow of a field still lets the whole be read. */ + other = fe_own_field_live(state, access == FE_OWN_READ); + if (other) { + fe_own_error_note(diags, loc, + access == FE_OWN_WRITE ? "cannot write while value is borrowed" : + access == FE_OWN_MOVE ? "cannot move while value is borrowed" : + access == FE_OWN_READ ? + "cannot read directly while value is mutably borrowed" : + "cannot borrow while a field of the value is borrowed", + other->loc, "borrow originated here"); + return 0; + } + return fe_own_access_whole(diags, state, access, loc); + } + /* Reaching one field: a borrow of the whole value is in the way, and so is + a borrow of this same field. A borrow of a different field is not. */ + if (!fe_own_access_whole_check(diags, state, access, loc)) return 0; + f = fe_own_field_slot(state, field, + access == FE_OWN_BORROW_SHARED || + access == FE_OWN_BORROW_MUT); + if (!f) { + /* No room left in the table, so this borrow covers the whole value. + That reports more than it has to and never less. */ + if (access == FE_OWN_BORROW_SHARED || access == FE_OWN_BORROW_MUT) + return fe_own_access_whole(diags, state, access, loc); + return 1; + } + switch (access) { + case FE_OWN_READ: + if (f->exclusive) { + fe_own_error_note(diags, loc, + "cannot read directly while value is mutably borrowed", + f->loc, "mutable borrow originated here"); + return 0; + } + return 1; + case FE_OWN_WRITE: + case FE_OWN_MOVE: + if (f->shared || f->exclusive) { + fe_own_error_note(diags, loc, + access == FE_OWN_WRITE ? "cannot write while value is borrowed" + : "cannot move while value is borrowed", + f->loc, "borrow originated here"); + return 0; + } + return 1; + case FE_OWN_BORROW_SHARED: + if (f->exclusive) { + fe_own_error_note(diags, loc, + "cannot create shared borrow while mutable borrow is live", + f->loc, "mutable borrow originated here"); + return 0; + } + if (!f->shared) f->loc = loc; + ++f->shared; + return 1; + case FE_OWN_BORROW_MUT: + if (f->shared || f->exclusive) { + fe_own_error_note(diags, loc, + "cannot create mutable borrow while another borrow is live", + f->loc, "existing borrow originated here"); + return 0; + } + f->exclusive = 1; + f->loc = loc; + return 1; + default: + break; + } + return 1; +} + +static int fe_own_access_whole_check(FeDiags *diags, FeOwnState *state, + FeOwnAccessKind access, FeLoc loc) +{ + if (!fe_own_require_stable_borrow(diags, state, loc)) return 0; + if (access == FE_OWN_WRITE) { + if (state->shared || state->exclusive) { + fe_own_error_note(diags, loc, "cannot write while value is borrowed", + state->borrow_loc, "borrow originated here"); + return 0; + } + return 1; + } + if (!fe_own_require_value(diags, state, loc)) return 0; + if (access == FE_OWN_READ || access == FE_OWN_BORROW_SHARED) { + if (state->exclusive) { + fe_own_error_note(diags, loc, access == FE_OWN_READ ? + "cannot read directly while value is mutably borrowed" : + "cannot create shared borrow while mutable borrow is live", + state->borrow_loc, "mutable borrow originated here"); + return 0; + } + return 1; + } + if (state->shared || state->exclusive) { + fe_own_error_note(diags, loc, access == FE_OWN_MOVE ? + "cannot move while value is borrowed" : + "cannot create mutable borrow while another borrow is live", + state->borrow_loc, "existing borrow originated here"); + return 0; + } + return 1; +} + +static int fe_own_access_whole(FeDiags *diags, FeOwnState *state, + FeOwnAccessKind access, FeLoc loc) +{ + if (!state) return 0; + if (access == FE_OWN_PROJECTION) return 1; + + if (!fe_own_require_stable_borrow(diags, state, loc)) return 0; + + if (access == FE_OWN_WRITE) { + if (state->shared || state->exclusive) { + fe_own_error_note(diags, loc, "cannot write while value is borrowed", + state->borrow_loc, "borrow originated here"); + return 0; + } + state->move = FE_OWN_AVAILABLE; + state->initialized = 1; + state->move_loc = fe_own_no_loc(); + return 1; + } + + if (!fe_own_require_value(diags, state, loc)) return 0; + + switch (access) { + case FE_OWN_READ: + if (state->exclusive) { + fe_own_error_note(diags, loc, + "cannot read directly while value is mutably borrowed", + state->borrow_loc, "mutable borrow originated here"); + return 0; + } + return 1; + case FE_OWN_MOVE: + if (state->shared || state->exclusive) { + fe_own_error_note(diags, loc, "cannot move while value is borrowed", + state->borrow_loc, "borrow originated here"); + return 0; + } + state->move = FE_OWN_MOVED; + state->initialized = 0; + state->move_loc = loc; + return 1; + case FE_OWN_BORROW_SHARED: + if (state->exclusive) { + fe_own_error_note(diags, loc, + "cannot create shared borrow while mutable borrow is live", + state->borrow_loc, "mutable borrow originated here"); + return 0; + } + if (!state->shared) state->borrow_loc = loc; + ++state->shared; + return 1; + case FE_OWN_BORROW_MUT: + if (state->shared || state->exclusive) { + fe_own_error_note(diags, loc, + "cannot create mutable borrow while another borrow is live", + state->borrow_loc, "existing borrow originated here"); + return 0; + } + state->exclusive = 1; + state->borrow_loc = loc; + return 1; + default: + break; + } + return 1; +} + +int fe_own_call_shared_view(FeDiags *diags, FeOwnState *state, FeLoc loc) +{ + if (!state) return 0; + if (!fe_own_require_stable_borrow(diags, state, loc)) return 0; + if (!fe_own_require_value(diags, state, loc)) return 0; + if (!state->exclusive) { + fe_diag_error(diags, loc, + "read-only reborrow requires a live mutable borrow"); + return 0; + } + return 1; +} + +void fe_own_release_shared(FeOwnState *state) +{ + if (!state || !state->shared) return; + --state->shared; + if (!state->shared && !state->exclusive) + state->borrow_loc = fe_own_no_loc(); +} + +void fe_own_release_exclusive(FeOwnState *state) +{ + if (!state) return; + state->exclusive = 0; + if (!state->shared) state->borrow_loc = fe_own_no_loc(); +} + +/* Merging two paths through the code: a borrow that is live on either side is + live after, because the checker cannot know which side ran. */ +static void fe_own_merge_fields(FeOwnState *out, const FeOwnState *left, + const FeOwnState *right) +{ + unsigned i; + unsigned j; + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) out->fields[i] = left->fields[i]; + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) { + const FeOwnField *r = &right->fields[i]; + if (!r->name) continue; + for (j = 0; j < FE_OWN_FIELD_MAX; ++j) { + if (out->fields[j].name && + strcmp(out->fields[j].name, r->name) != 0) continue; + if (!out->fields[j].name) out->fields[j] = *r; + else { + if (r->shared > out->fields[j].shared) + out->fields[j].shared = r->shared; + if (r->exclusive && !out->fields[j].exclusive) { + out->fields[j].exclusive = 1; + out->fields[j].loc = r->loc; + } + } + break; + } + } +} + +FeOwnState fe_own_merge_state(FeOwnState left, FeOwnState right) +{ + FeOwnState out; + fe_own_merge_fields(&out, &left, &right); + out.move = left.move == right.move ? left.move : + fe_own_merge_move(left.move, right.move); + out.initialized = left.initialized && right.initialized; + out.shared = left.shared > right.shared ? left.shared : right.shared; + out.exclusive = left.exclusive || right.exclusive; + out.borrow_conflict = left.borrow_conflict || right.borrow_conflict || + (out.shared != 0 && out.exclusive != 0); + out.move_loc = left.move != FE_OWN_AVAILABLE ? left.move_loc : right.move_loc; + if (left.shared || left.exclusive || left.borrow_conflict) + out.borrow_loc = left.borrow_loc; + else + out.borrow_loc = right.borrow_loc; + return out; +} + +int fe_own_state_equal(const FeOwnState *left, const FeOwnState *right) +{ + if (!left || !right) return 0; + { + unsigned i; + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) { + const FeOwnField *a = &left->fields[i]; + const FeOwnField *b = &right->fields[i]; + if (!a->name != !b->name) return 0; + if (a->name && strcmp(a->name, b->name) != 0) return 0; + if (a->shared != b->shared || a->exclusive != b->exclusive) + return 0; + } + } + return left->move == right->move && + left->initialized == right->initialized && + left->shared == right->shared && + left->exclusive == right->exclusive && + left->borrow_conflict == right->borrow_conflict; +} + +int fe_own_loop_merge_state(FeOwnState entry, FeOwnState backedge, + FeOwnState *merged) +{ + if (!merged) return 0; + *merged = fe_own_merge_state(entry, backedge); + return fe_own_state_equal(&entry, merged); +} + +FeOwnProvenance fe_own_provenance_static(void) +{ + FeOwnProvenance p; + p.kind = FE_OWN_PROV_STATIC; + p.param_index = 0; + return p; +} + +FeOwnProvenance fe_own_provenance_param(unsigned param_index) +{ + FeOwnProvenance p; + p.kind = FE_OWN_PROV_PARAM; + p.param_index = param_index; + return p; +} + +FeOwnProvenance fe_own_merge_provenance(FeOwnProvenance left, + FeOwnProvenance right) +{ + FeOwnProvenance invalid; + invalid.kind = FE_OWN_PROV_INVALID; + invalid.param_index = 0; + if (left.kind == FE_OWN_PROV_INVALID || right.kind == FE_OWN_PROV_INVALID) + return invalid; + if (left.kind == FE_OWN_PROV_STATIC) return right; + if (right.kind == FE_OWN_PROV_STATIC) return left; + if (left.param_index == right.param_index) return left; + return invalid; +} + +void fe_own_liveness_init(FeOwnLiveness *live, FeArena *arena) +{ + if (!live) return; + live->arena = arena; + live->items = 0; + live->count = 0; + live->capacity = 0; + live->ordinal = 0; +} + +static FeOwnLastUse *fe_own_live_find(FeOwnLiveness *live, const char *cname) +{ + unsigned i; + if (!live || !cname) return 0; + for (i = 0; i < live->count; ++i) + if (live->items[i].cname == cname || + strcmp(live->items[i].cname, cname) == 0) + return &live->items[i]; + return 0; +} + +static int fe_own_live_add(FeOwnLiveness *live, FeNode *decl) +{ + FeOwnLastUse *items; + FeOwnLastUse *slot; + unsigned capacity; + const char *cname; + if (!live || !decl || !live->arena) + return 1; + cname = decl->cname ? decl->cname : decl->text; + if (!cname || fe_own_live_find(live, cname)) return 1; + if (live->count == live->capacity) { + capacity = live->capacity ? live->capacity * 2U : 8U; + items = (FeOwnLastUse *)fe_arena_alloc(live->arena, + capacity * sizeof(FeOwnLastUse)); + if (!items) return 0; + if (live->items) + memcpy(items, live->items, live->count * sizeof(FeOwnLastUse)); + live->items = items; + live->capacity = capacity; + } + slot = &live->items[live->count++]; + slot->cname = cname; + slot->decl = decl; + slot->last_node = 0; + slot->last_ordinal = 0; + slot->defer_extended = 0; + return 1; +} + +static unsigned long fe_own_node_weight(FeNode *node); + +static unsigned long fe_own_list_weight(FeNode *node) +{ + unsigned long count; + count = 0; + while (node) { + count += fe_own_node_weight(node); + node = node->next; + } + return count; +} + +static unsigned long fe_own_node_weight(FeNode *node) +{ + unsigned long count; + if (!node) return 0; + count = 1; + count += fe_own_node_weight(node->a); + count += fe_own_node_weight(node->b); + count += fe_own_node_weight(node->c); + count += fe_own_list_weight(node->children); + return count; +} + +static int fe_own_live_visit(FeOwnLiveness *live, FeNode *node, + unsigned long block_end, + unsigned long defer_until); + +static int fe_own_live_visit_list(FeOwnLiveness *live, FeNode *node, + unsigned long block_end, + unsigned long defer_until) +{ + while (node) { + if (!fe_own_live_visit(live, node, block_end, defer_until)) return 0; + node = node->next; + } + return 1; +} + +static int fe_own_live_visit(FeOwnLiveness *live, FeNode *node, + unsigned long block_end, + unsigned long defer_until) +{ + FeOwnLastUse *slot; + unsigned long end; + unsigned long effective; + if (!node) return 1; + ++live->ordinal; + + if (node->kind == FE_N_IDENT) { + slot = fe_own_live_find(live, node->cname ? node->cname : node->text); + if (slot) { + effective = defer_until ? defer_until : live->ordinal; + if (effective >= slot->last_ordinal) { + slot->last_ordinal = effective; + slot->last_node = node; + if (defer_until) slot->defer_extended = 1; + } + } + return 1; + } + + if (node->kind == FE_N_BLOCK) { + end = live->ordinal + fe_own_list_weight(node->children); + return fe_own_live_visit_list(live, node->children, end, defer_until); + } + + if (node->kind == FE_N_DEFER) { + effective = defer_until ? defer_until : block_end; + return fe_own_live_visit(live, node->a, block_end, effective); + } + + if (!fe_own_live_visit(live, node->a, block_end, defer_until)) return 0; + if (!fe_own_live_visit(live, node->b, block_end, defer_until)) return 0; + if (!fe_own_live_visit(live, node->c, block_end, defer_until)) return 0; + if (!fe_own_live_visit_list(live, node->children, block_end, defer_until)) + return 0; + + if (node->kind == FE_N_LET || node->kind == FE_N_VAR || + node->kind == FE_N_CONST) + return fe_own_live_add(live, node); + return 1; +} + +int fe_own_collect_last_uses(FeOwnLiveness *live, FeNode *fn) +{ + FeNode *param; + if (!live || !fn || fn->kind != FE_N_FN) return 0; + live->items = 0; + live->count = 0; + live->capacity = 0; + live->ordinal = 0; + for (param = fn->a ? fn->a->children : 0; param; param = param->next) + if (!fe_own_live_add(live, param)) return 0; + return fe_own_live_visit(live, fn->c, 0, 0); +} + +const FeOwnLastUse *fe_own_last_use(const FeOwnLiveness *live, + const char *cname) +{ + unsigned i; + if (!live || !cname) return 0; + for (i = 0; i < live->count; ++i) + if (live->items[i].cname == cname || + strcmp(live->items[i].cname, cname) == 0) + return &live->items[i]; + return 0; +} + +static int fe_own_replace_unwrap(FeNode *expr) +{ + FeNode *call; + FeNode *member; + if (!expr || expr->kind != FE_N_MEMBER || !expr->text || + strcmp(expr->text,".?") != 0) + return 0; + call=expr->a; + if (!call || call->kind != FE_N_CALL || !call->a || + call->a->kind != FE_N_MEMBER) + return 0; + member=call->a; + return member->a && member->a->kind==FE_N_IDENT && member->a->text && + strcmp(member->a->text,"mem")==0 && member->b && member->b->text && + strcmp(member->b->text,"replace")==0; +} + +void fe_own_mark_consumed(FeDiags *diags, int *state, FeNode *decl, + FeNode *expr, FeType *type, int in_defer) +{ + if (!expr || !type || fe_own_is_copy_type(type)) return; + if (expr->kind == FE_N_INDEX && type->kind == FE_TYPE_SLICE && + (expr->c || !expr->b)) + return; + if ((expr->kind == FE_N_MEMBER || expr->kind == FE_N_INDEX) && + !fe_own_replace_unwrap(expr)) { + fe_diag_error(diags, expr->loc, + "cannot move a non-Copy value out of a projection; use mem.replace"); + return; + } + if (expr->kind != FE_N_IDENT || !state) return; + + if (in_defer) { + if (decl) decl->flags |= FE_OWN_NODE_DEFER_CAPTURE; + return; + } + + *state = FE_OWN_MOVED; + expr->flags |= FE_OWN_NODE_CONSUMED; +} + +void fe_own_check_use(FeDiags *diags, int state, FeLoc loc) +{ + if (state == FE_OWN_MOVED) + fe_diag_error(diags, loc, "use of moved value"); + else if (state == FE_OWN_MAYBE_MOVED) + fe_diag_error(diags, loc, "use of possibly moved value"); +} + +int fe_own_merge_move(int left, int right) +{ + if (left == FE_OWN_MOVED && right == FE_OWN_MOVED) + return FE_OWN_MOVED; + if (left != FE_OWN_AVAILABLE || right != FE_OWN_AVAILABLE) + return FE_OWN_MAYBE_MOVED; + return FE_OWN_AVAILABLE; +} + +int fe_own_loop_entry(int before, int after) +{ + if (before == after) return before; + return FE_OWN_MAYBE_MOVED; +} + +int fe_own_loop_exit(int state, int after) +{ + if (after != FE_OWN_AVAILABLE) return FE_OWN_MAYBE_MOVED; + return state; +} diff --git a/fec/src/own.h b/fec/src/own.h new file mode 100644 index 0000000..a591b6d --- /dev/null +++ b/fec/src/own.h @@ -0,0 +1,126 @@ +#ifndef FE_OWN_H +#define FE_OWN_H + +#include "types.h" +#include "diag.h" + +#define FE_OWN_NODE_CONSUMED 0x100U +#define FE_OWN_NODE_DEFER_CAPTURE 0x200U + +enum FeOwnMoveState { + FE_OWN_AVAILABLE = 0, + FE_OWN_MOVED = 1, + FE_OWN_MAYBE_MOVED = 2 +}; + +typedef enum FeOwnAccessKind { + FE_OWN_READ, + FE_OWN_WRITE, + FE_OWN_MOVE, + FE_OWN_BORROW_SHARED, + FE_OWN_BORROW_MUT, + FE_OWN_PROJECTION +} FeOwnAccessKind; + +typedef enum FeOwnProvenanceKind { + FE_OWN_PROV_INVALID, + FE_OWN_PROV_STATIC, + FE_OWN_PROV_PARAM +} FeOwnProvenanceKind; + +typedef struct FeOwnPlace { + FeNode *root; + const char *root_cname; + int projected; +} FeOwnPlace; + +/* How many distinct fields of one value can be borrowed at once. Past this + a borrow falls back to covering the whole value, which reports more than it + has to but never less. */ +#define FE_OWN_FIELD_MAX 4 + +/* A borrow of one field rather than of the whole value. `self.bytes` and + `self.used_bytes` are different places, so borrowing one has to leave the + other readable -- otherwise a method cannot write through one field while + reading another, which is most of what a method does. */ +typedef struct FeOwnField { + const char *name; + unsigned shared; + int exclusive; + FeLoc loc; +} FeOwnField; + +typedef struct FeOwnState { + int move; + int initialized; + unsigned shared; + int exclusive; + int borrow_conflict; + FeLoc move_loc; + FeLoc borrow_loc; + /* Whole-value state is above; these cover one field each. A whole-value + borrow conflicts with every field, and a field borrow conflicts with + the whole value and with itself. */ + FeOwnField fields[FE_OWN_FIELD_MAX]; +} FeOwnState; + +typedef struct FeOwnProvenance { + FeOwnProvenanceKind kind; + unsigned param_index; +} FeOwnProvenance; + +typedef struct FeOwnLastUse { + const char *cname; + FeNode *decl; + FeNode *last_node; + unsigned long last_ordinal; + int defer_extended; +} FeOwnLastUse; + +typedef struct FeOwnLiveness { + FeArena *arena; + FeOwnLastUse *items; + unsigned count; + unsigned capacity; + unsigned long ordinal; +} FeOwnLiveness; + +int fe_own_is_copy_type(FeType *type); +int fe_own_is_reference_like(FeType *type); +int fe_own_place_from_expr(FeNode *expr, FeOwnPlace *place); + +void fe_own_state_init(FeOwnState *state, int initialized); +int fe_own_access(FeDiags *diags, FeOwnState *state, + FeOwnAccessKind access, FeLoc loc); +/* The same, but reaching only one field of the value. A null `field` is the + whole value and behaves exactly as `fe_own_access`. */ +int fe_own_access_field(FeDiags *diags, FeOwnState *state, const char *field, + FeOwnAccessKind access, FeLoc loc); +int fe_own_call_shared_view(FeDiags *diags, FeOwnState *state, FeLoc loc); +void fe_own_release_shared(FeOwnState *state); +void fe_own_release_exclusive(FeOwnState *state); +void fe_own_release_shared_field(FeOwnState *state, const char *field); +void fe_own_release_exclusive_field(FeOwnState *state, const char *field); +FeOwnState fe_own_merge_state(FeOwnState left, FeOwnState right); +int fe_own_state_equal(const FeOwnState *left, const FeOwnState *right); +int fe_own_loop_merge_state(FeOwnState entry, FeOwnState backedge, + FeOwnState *merged); + +FeOwnProvenance fe_own_provenance_static(void); +FeOwnProvenance fe_own_provenance_param(unsigned param_index); +FeOwnProvenance fe_own_merge_provenance(FeOwnProvenance left, + FeOwnProvenance right); + +void fe_own_liveness_init(FeOwnLiveness *live, FeArena *arena); +int fe_own_collect_last_uses(FeOwnLiveness *live, FeNode *fn); +const FeOwnLastUse *fe_own_last_use(const FeOwnLiveness *live, + const char *cname); + +void fe_own_mark_consumed(FeDiags *diags, int *state, FeNode *decl, + FeNode *expr, FeType *type, int in_defer); +void fe_own_check_use(FeDiags *diags, int state, FeLoc loc); +int fe_own_merge_move(int left, int right); +int fe_own_loop_entry(int before, int after); +int fe_own_loop_exit(int state, int after); + +#endif diff --git a/fec/src/parser.c b/fec/src/parser.c new file mode 100644 index 0000000..23224ba --- /dev/null +++ b/fec/src/parser.c @@ -0,0 +1,394 @@ +#include "parser.h" +#include +#include + +static FeToken next(FeParser *p) { p->previous=p->current; p->current=fe_lexer_next(&p->lexer); return p->current; } +static int is(FeParser *p, FeTokKind k) { return p->current.kind==k; } +static int eat(FeParser *p, FeTokKind k) { if(is(p,k)){next(p);return 1;}return 0; } +static FeNode *toknode(FeParser *p, FeNodeKind k, FeToken t) { return fe_node(p->ast,k,t.loc,t.begin,t.length); } +static void error(FeParser *p, const char *s) { fe_diag_error(p->diags,p->current.loc,s); } +static int want(FeParser *p, FeTokKind k, const char *what) +{ if(eat(p,k)) return 1; error(p,what); return 0; } +static int is_name(FeParser *p) { return is(p,FE_TOK_IDENT)||is(p,FE_TOK_SELF)||is(p,FE_TOK_SELFTYPE); } +static FeNode *expr(FeParser *p, int minprec); +static FeNode *delimited_expr(FeParser *p); +static FeNode *type(FeParser *p); +static FeNode *statement(FeParser *p); +static FeNode *block(FeParser *p); + +void fe_parser_init(FeParser *p, FeAst *ast, const char *src, unsigned long length, const char *file, FeDiags *d) +{ + p->ast=ast; p->diags=d; p->forbid_struct_literal=0; fe_lexer_init(&p->lexer,src,length,file,d); + p->previous=p->current=fe_lexer_next(&p->lexer); +} + +static void recover(FeParser *p) +{ + while(!is(p,FE_TOK_EOF) && !is(p,FE_TOK_SEMI) && !is(p,FE_TOK_RBRACE)) next(p); + if(is(p,FE_TOK_SEMI)) next(p); +} + +static FeNode *type_prefix(FeParser *p, FeTokKind k, FeToken op) +{ + FeNode *n; + (void)k; + n=toknode(p,FE_N_TYPE,op); n->a=type(p); return n; +} +static FeNode *type(FeParser *p) +{ + FeToken t=p->current; FeNode *n; + if (is(p,FE_TOK_QUESTION)||is(p,FE_TOK_BANG)||is(p,FE_TOK_STAR)||is(p,FE_TOK_XOR)) { + next(p); return type_prefix(p,t.kind,t); + } + if (is(p,FE_TOK_AND)) { + next(p); n=toknode(p,FE_N_TYPE,t); if(eat(p,FE_TOK_MUT)) n->text=fe_arena_strdup(&p->ast->arena,"&mut",4); n->a=type(p); return n; + } + if (is(p,FE_TOK_LBRACKET)) { + next(p); n=toknode(p,FE_N_TYPE,t); + if(!eat(p,FE_TOK_RBRACKET)) { n->a=expr(p,0); want(p,FE_TOK_RBRACKET,"expected ']' in array type"); } + else if(eat(p,FE_TOK_MUT)) n->text=fe_arena_strdup(&p->ast->arena,"[]mut",5); + n->b=type(p); return n; + } + if (is(p,FE_TOK_FN)) { + next(p); n=toknode(p,FE_N_TYPE,t); want(p,FE_TOK_LPAREN,"expected '(' in function type"); + while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)) { fe_node_add(n,type(p)); if(!eat(p,FE_TOK_COMMA)) break; } + want(p,FE_TOK_RPAREN,"expected ')' in function type"); if(eat(p,FE_TOK_ARROW)) n->a=type(p); return n; + } + if (is_name(p) || is(p,FE_TOK_TYPE)) { + next(p); n=toknode(p,FE_N_TYPE,t); + if (eat(p,FE_TOK_DOT)) { + if(is_name(p)) { + FeToken mt=p->current; + FeNode *m=toknode(p,FE_N_IDENT,mt); + n->a=m; + next(p); + } else error(p,"expected type name after '.'"); + } + if (eat(p,FE_TOK_BANG)) { FeNode *e=toknode(p,FE_N_TYPE,p->previous); e->a=n; e->b=type(p); return e; } + if (eat(p,FE_TOK_LPAREN)) { while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n,type(p));if(!eat(p,FE_TOK_COMMA))break;} want(p,FE_TOK_RPAREN,"expected ')' in generic type"); } + return n; + } + error(p,"expected type"); next(p); return fe_node(p->ast,FE_N_TYPE,t.loc,"error",5); +} + +/* Is this binary node one of the six comparisons? They share a precedence + level and SPEC 6.2 forbids chaining them. The operator is the node's text. */ +static int is_comparison(const char *op) +{ + if(!op) return 0; + return !strcmp(op,"==")||!strcmp(op,"!=")||!strcmp(op,"<")|| + !strcmp(op,"<=")||!strcmp(op,">")||!strcmp(op,">="); +} +static int precedence(FeTokKind k) +{ + switch(k) { + case FE_TOK_ORELSE: case FE_TOK_CATCH:return 1; + case FE_TOK_OR_KW:return 2; case FE_TOK_AND_KW:return 3; + case FE_TOK_EQEQ: case FE_TOK_NE: case FE_TOK_LT: case FE_TOK_LE: case FE_TOK_GT: case FE_TOK_GE:return 4; + case FE_TOK_OR:return 5; case FE_TOK_XOR:return 6; case FE_TOK_AND:return 7; + case FE_TOK_SHL: case FE_TOK_SHR:return 8; + case FE_TOK_PLUS: case FE_TOK_MINUS: case FE_TOK_PLUS_WRAP: case FE_TOK_MINUS_WRAP:return 9; + case FE_TOK_STAR: case FE_TOK_SLASH: case FE_TOK_PERCENT: case FE_TOK_STAR_WRAP:return 10; + default:return 0; + } +} +static FeNode *primary(FeParser *p) +{ + FeToken t=p->current; FeNode *n; + if (is(p,FE_TOK_LBRACKET)) { + FeNode *a=toknode(p,FE_N_ARRAY_INIT,t); next(p); + while(!is(p,FE_TOK_RBRACKET)&&!is(p,FE_TOK_EOF)) { + fe_node_add(a,delimited_expr(p)); + if(!eat(p,FE_TOK_COMMA)) break; + } + want(p,FE_TOK_RBRACKET,"expected ']' after array literal"); + return a; + } + if(is(p,FE_TOK_INT)||is(p,FE_TOK_CHAR)||is(p,FE_TOK_STRING)||is(p,FE_TOK_TRUE)||is(p,FE_TOK_FALSE)||is(p,FE_TOK_NULL)||is(p,FE_TOK_UNDEFINED)) {next(p);return toknode(p,FE_N_LITERAL,t);} + if(is_name(p) || is(p,FE_TOK_ERROR_KW)) { + next(p); n=toknode(p,FE_N_IDENT,t); + if(is(p,FE_TOK_LBRACE) && !p->forbid_struct_literal) { + FeNode *s=toknode(p,FE_N_STRUCT_INIT,t); next(p); + while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)) { FeNode *f; + if(!is_name(p)){error(p,"expected field name");recover(p);break;} f=toknode(p,FE_N_FIELD,p->current);next(p);want(p,FE_TOK_COLON,"expected ':' after field");f->a=expr(p,0);fe_node_add(s,f);if(!eat(p,FE_TOK_COMMA))break; + } want(p,FE_TOK_RBRACE,"expected '}' in struct literal"); return s; + } + return n; + } + if(eat(p,FE_TOK_LPAREN)) { int old=p->forbid_struct_literal; p->forbid_struct_literal=0; n=expr(p,0); p->forbid_struct_literal=old; want(p,FE_TOK_RPAREN,"expected ')'"); if(n) n->flags|=FE_NODE_PAREN; return n; } + if(eat(p,FE_TOK_AT)) { + FeToken name=p->current; if(!is_name(p)){error(p,"expected builtin name after '@'");return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"builtin",7);} next(p); + n=toknode(p,FE_N_CALL,name); n->text=fe_arena_strdup(&p->ast->arena,name.begin-1,name.length+1); + if(eat(p,FE_TOK_LPAREN)){while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n,delimited_expr(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after builtin");} + return n; + } + error(p,"expected expression"); next(p); return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"expression",10); +} +static FeNode *postfix(FeParser *p) +{ + FeNode *n=primary(p); + for(;;) { + FeToken t=p->current; FeNode *m; + if(eat(p,FE_TOK_LPAREN)) { m=toknode(p,FE_N_CALL,t); m->a=n; while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(m,delimited_expr(p));if(!eat(p,FE_TOK_COMMA))break;} want(p,FE_TOK_RPAREN,"expected ')' after call"); n=m; + /* `Name(args){...}` builds an instance of a generic struct. Without + this the arguments have nowhere to go and only `Self{...}` or a + constructor can name one. Same `{` ambiguity as `Name{...}` + above, and the same guard settles it. */ + if(is(p,FE_TOK_LBRACE) && !p->forbid_struct_literal) { + FeNode *s=toknode(p,FE_N_STRUCT_INIT,t); s->a=n; next(p); + while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)) { FeNode *f; + if(!is_name(p)){error(p,"expected field name");recover(p);break;} + f=toknode(p,FE_N_FIELD,p->current);next(p); + want(p,FE_TOK_COLON,"expected ':' after field"); + f->a=expr(p,0);fe_node_add(s,f); + if(!eat(p,FE_TOK_COMMA))break; + } + want(p,FE_TOK_RBRACE,"expected '}' in struct literal"); n=s; + } } + else if(eat(p,FE_TOK_LBRACKET)) { + m=toknode(p,FE_N_INDEX,t);m->a=n; + if(is(p,FE_TOK_DOTDOT)) { m->b=0; m->flags|=FE_NODE_SLICE; } else m->b=delimited_expr(p); + if(eat(p,FE_TOK_DOTDOT)) { m->flags|=FE_NODE_SLICE; if(!is(p,FE_TOK_RBRACKET)) m->c=delimited_expr(p); } + want(p,FE_TOK_RBRACKET,"expected ']' after index");n=m; + } + else if(eat(p,FE_TOK_DOT)) { + m=toknode(p,FE_N_MEMBER,t);m->a=n; + if(is_name(p)){m->b=toknode(p,FE_N_IDENT,p->current);next(p);} + else if(eat(p,FE_TOK_QUESTION)){m->text=fe_arena_strdup(&p->ast->arena,".?",2);} + else if(eat(p,FE_TOK_XOR)){m->text=fe_arena_strdup(&p->ast->arena,".^",2);m->b=fe_node(p->ast,FE_N_IDENT,p->previous.loc,"^",1);} + else error(p,"expected member name"); + n=m; + if(is(p,FE_TOK_LBRACE) && !p->forbid_struct_literal) { + FeNode *s=toknode(p,FE_N_STRUCT_INIT,t); s->a=n; next(p); + while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)) { FeNode *f; + if(!is_name(p)){error(p,"expected variant field");recover(p);break;} + f=toknode(p,FE_N_FIELD,p->current);next(p);want(p,FE_TOK_COLON,"expected ':' after variant field");f->a=expr(p,0);fe_node_add(s,f);if(!eat(p,FE_TOK_COMMA))break; + } + want(p,FE_TOK_RBRACE,"expected '}' in variant constructor");n=s; + } + } + else if(eat(p,FE_TOK_AS)) { m=toknode(p,FE_N_TYPE,t);m->a=n;m->b=type(p);n=m; } + else break; + } + return n; +} +static FeNode *expr(FeParser *p, int minprec) +{ + FeToken t=p->current; FeNode *left,*n; int prec; + if(is(p,FE_TOK_MINUS)||is(p,FE_TOK_NOT)||is(p,FE_TOK_TILDE)||is(p,FE_TOK_XOR)||is(p,FE_TOK_AND)||is(p,FE_TOK_STAR)||is(p,FE_TOK_TRY)) { next(p); n=toknode(p,FE_N_UNARY,t); if(t.kind==FE_TOK_AND && eat(p,FE_TOK_MUT)) n->text=fe_arena_strdup(&p->ast->arena,"&mut",4); n->a=expr(p,11); + if(n->a && n->a->kind==FE_N_TYPE && n->a->b && + !(n->a->flags & FE_NODE_PAREN)) + error(p,"parenthesise: '-x as T' is read as -(x as T)"); + left=n; } + else left=postfix(p); + for(;;) { t=p->current;prec=precedence(t.kind);if(prec<=minprec)break; + if(prec==4 && left && left->kind==FE_N_BINARY && + !(left->flags & FE_NODE_PAREN) && is_comparison(left->text)) + error(p,"comparisons do not chain; write 'a < b and b < c'"); + next(p);n=toknode(p,FE_N_BINARY,t);n->a=left;if(t.kind==FE_TOK_CATCH && eat(p,FE_TOK_OR)){if(is_name(p))n->b=toknode(p,FE_N_IDENT,p->current),next(p);else error(p,"expected catch binding");want(p,FE_TOK_OR,"expected '|' after catch binding");n->c=block(p);}else n->b=expr(p,prec);left=n; } + return left; +} + +static FeNode *header_expr(FeParser *p) +{ + FeNode *n; + int old=p->forbid_struct_literal; + p->forbid_struct_literal=1; + n=expr(p,0); + p->forbid_struct_literal=old; + return n; +} + +static FeNode *delimited_expr(FeParser *p) +{ + FeNode *n; + int old=p->forbid_struct_literal; + p->forbid_struct_literal=0; + n=expr(p,0); + p->forbid_struct_literal=old; + return n; +} + +static FeNode *params(FeParser *p) +{ + FeNode *list=fe_node(p->ast,FE_N_BLOCK,p->current.loc,"params",6); + want(p,FE_TOK_LPAREN,"expected '(' after function name"); + while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)) { FeToken t=p->current; FeNode *q; int ct=0; + if(eat(p,FE_TOK_COMPTIME)) { t=p->previous; ct=1; } + if(!is_name(p)){error(p,"expected parameter name");recover(p);break;} q=toknode(p,FE_N_PARAM,p->current);if(ct){q->flags|=FE_NODE_COMPTIME;q->loc=t.loc;}next(p);want(p,FE_TOK_COLON,"expected ':' in parameter");q->a=type(p);fe_node_add(list,q);if(!eat(p,FE_TOK_COMMA))break; + } + want(p,FE_TOK_RPAREN,"expected ')' after parameters"); return list; +} +static FeNode *fn_decl(FeParser *p, int pub, int external, int interrupt, int interrupt_safe) +{ + FeToken t=p->current, name; FeNode *n; + (void)interrupt; (void)interrupt_safe; + want(p,FE_TOK_FN,"expected 'fn'"); if(!is_name(p)){error(p,"expected function name");return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"fn",2);} + name=p->current; n=toknode(p,FE_N_FN,t); if(pub) n->flags|=FE_NODE_PUB; if(external) n->flags|=FE_NODE_EXTERN; n->text=fe_arena_strdup(&p->ast->arena,name.begin,name.length); next(p); n->a=params(p); if(eat(p,FE_TOK_ARROW)) n->b=type(p); + if(eat(p,FE_TOK_SEMI)) { + /* A body-less function is a promise that someone else defines it, and + `extern` is how that promise is made. Without it the name is + mangled into this unit and nothing anywhere defines it. */ + if(!external) error(p,"a function without a body must be extern"); + return n; + } + if(external) error(p,"an extern function has no body"); + n->c=block(p); return n; +} +static FeNode *field(FeParser *p, int pub) +{ + FeToken t=p->current; FeNode *n; + if(!is_name(p)){error(p,"expected field name");recover(p);return 0;} next(p);n=toknode(p,FE_N_FIELD,t);if(pub)n->flags|=FE_NODE_PUB;want(p,FE_TOK_COLON,"expected ':' after field");n->a=type(p);if(!eat(p,FE_TOK_COMMA) && !is(p,FE_TOK_RBRACE)) error(p,"expected ',' after field");return n; +} +static FeNode *decl(FeParser *p) +{ + int pub=0, external=0, interrupt=0, interrupt_safe=0, shared=0, atomic=0; FeToken t=p->current; FeNode *n; FeTokKind before; + (void)shared; (void)atomic; + if(eat(p,FE_TOK_PUB)) pub=1; + if(eat(p,FE_TOK_EXTERN)) { + external=1; + if(!is(p,FE_TOK_STRING)) error(p,"extern requires an ABI string"); + else { + /* The token keeps its quotes, so "c" is four characters. */ + FeToken abi=p->current; + if(abi.length!=3 || abi.begin[1]!='c') + error(p,"the only ABI is \"c\""); + next(p); + } + } + if(eat(p,FE_TOK_INTERRUPT)) interrupt=1; + if(eat(p,FE_TOK_INTERRUPT_SAFE)) interrupt_safe=1; + if(!is(p,FE_TOK_PACKED)) t=p->current; + if(is(p,FE_TOK_FN)) return fn_decl(p,pub,external,interrupt,interrupt_safe); + if(eat(p,FE_TOK_PACKED)) t=p->previous; + if(eat(p,FE_TOK_STRUCT)) { n=toknode(p,FE_N_STRUCT,t);if(pub)n->flags|=FE_NODE_PUB;if(t.kind==FE_TOK_PACKED)n->flags|=FE_NODE_PACKED;if(!is_name(p)){error(p,"expected struct name");return n;}next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);if(eat(p,FE_TOK_LPAREN)){n->a=fe_node(p->ast,FE_N_BLOCK,p->current.loc,"generics",8);while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n->a,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in struct");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){int mpub=eat(p,FE_TOK_PUB);if(is(p,FE_TOK_FN))fe_node_add(n,fn_decl(p,mpub,0,0,0));else fe_node_add(n,field(p,mpub));}want(p,FE_TOK_RBRACE,"expected '}' after struct");return n; } + if(eat(p,FE_TOK_ENUM)) { n=toknode(p,FE_N_ENUM,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected enum name");if(eat(p,FE_TOK_LPAREN)){n->a=fe_node(p->ast,FE_N_BLOCK,p->current.loc,"generics",8);while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n->a,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in enum");if(is(p,FE_TOK_RBRACE))error(p,"an enum needs at least one variant");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected variant name");recover(p);break;}if(eat(p,FE_TOK_LPAREN)){v->a=type(p);want(p,FE_TOK_RPAREN,"expected ')' in variant");}else if(eat(p,FE_TOK_LBRACE)){while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF))fe_node_add(v,field(p,1));want(p,FE_TOK_RBRACE,"expected '}' in variant");}fe_node_add(n,v);if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RBRACE,"expected '}' after enum");return n; } + if(eat(p,FE_TOK_ERROR_KW)) { n=toknode(p,FE_N_ERROR_DECL,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected error name");want(p,FE_TOK_LBRACE,"expected '{' in error declaration");if(is(p,FE_TOK_RBRACE))error(p,"an error declaration needs at least one member");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected error member");recover(p);break;}want(p,FE_TOK_EQ,"expected '=' in error member");if(is(p,FE_TOK_INT)){v->a=toknode(p,FE_N_LITERAL,p->current);next(p);}else error(p,"an error code must be an integer literal");if(!is(p,FE_TOK_COMMA)&&!is(p,FE_TOK_RBRACE)){error(p,"an error code must be an integer literal");recover(p);break;}want(p,FE_TOK_COMMA,"expected ',' in error declaration");fe_node_add(n,v);}want(p,FE_TOK_RBRACE,"expected '}' after error");return n; } + if(eat(p,FE_TOK_SHARED)) { shared=1; if(eat(p,FE_TOK_ATOMIC)) atomic=1; if(!is(p,FE_TOK_VAR)) error(p,"expected 'var' after shared"); } + if(is(p,FE_TOK_CONST)||is(p,FE_TOK_STATIC)||is(p,FE_TOK_VAR)) { FeTokKind kk=p->current.kind;next(p);n=toknode(p,kk==FE_TOK_CONST?FE_N_CONST:FE_N_GLOBAL,t);if(pub)n->flags|=FE_NODE_PUB;if(kk==FE_TOK_STATIC)n->flags|=FE_NODE_STATIC;if(shared)n->flags|=FE_NODE_SHARED;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected declaration name");if(eat(p,FE_TOK_COLON))n->a=type(p);else if(kk!=FE_TOK_CONST)error(p,"a global declaration requires an explicit type");want(p,FE_TOK_EQ,"expected '=' in declaration");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after declaration");return n; } + error(p,"expected declaration"); before=p->current.kind; recover(p); + if (p->current.kind==before && p->current.kind!=FE_TOK_EOF) next(p); + return 0; +} + +static FeNode *block(FeParser *p) +{ + FeToken t=p->current; FeNode *n=toknode(p,FE_N_BLOCK,t);want(p,FE_TOK_LBRACE,"expected '{'");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *s=statement(p);if(s)fe_node_add(n,s);}want(p,FE_TOK_RBRACE,"expected '}'");return n; +} +static FeNode *statement(FeParser *p) +{ + FeToken t=p->current; FeNode *n,*e; + if(is(p,FE_TOK_LBRACE)) return block(p); + if(eat(p,FE_TOK_LET)) { n=toknode(p,FE_N_LET,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected variable name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in let");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } + if(eat(p,FE_TOK_VAR)) { n=toknode(p,FE_N_VAR,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected variable name");if(eat(p,FE_TOK_COLON))n->a=type(p);if(eat(p,FE_TOK_EQ))n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } + if(eat(p,FE_TOK_CONST)) { n=toknode(p,FE_N_CONST,t);if(is_name(p)){n->text=fe_arena_strdup(&p->ast->arena,p->current.begin,p->current.length);next(p);}else error(p,"expected constant name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in const");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } + if(eat(p,FE_TOK_IF)) { + n=toknode(p,FE_N_IF,t); + if(eat(p,FE_TOK_LET)) { + FeToken pt=p->current; + n->text=fe_arena_strdup(&p->ast->arena,"if let",6); + if(is_name(p)) { + n->aux_text=fe_arena_strdup(&p->ast->arena,p->current.begin,p->current.length); + next(p); + } else error(p,"expected if let pattern"); + if(eat(p,FE_TOK_LPAREN)) { + if(is_name(p)) { + FeNode *binding=toknode(p,FE_N_IDENT,p->current); + next(p); + fe_node_add(n,binding); + } else error(p,"expected if let binding"); + want(p,FE_TOK_RPAREN,"expected ')' in if let pattern"); + } + want(p,FE_TOK_EQ,"expected '=' in if let"); + (void)pt; + } + n->a=header_expr(p);n->b=block(p);if(eat(p,FE_TOK_ELSE))n->c=is(p,FE_TOK_IF)?statement(p):block(p);return n; + } + if(eat(p,FE_TOK_COMPTIME)) { n=toknode(p,FE_N_IF,t);want(p,FE_TOK_IF,"expected 'if' after comptime");n->text=fe_arena_strdup(&p->ast->arena,"comptime if",11);n->a=header_expr(p);n->b=block(p);if(eat(p,FE_TOK_ELSE))n->c=is(p,FE_TOK_IF)?statement(p):block(p);return n; } + if(eat(p,FE_TOK_WHILE)) {n=toknode(p,FE_N_WHILE,t);n->a=header_expr(p);n->b=block(p);return n;} + if(eat(p,FE_TOK_FOR)) {n=toknode(p,FE_N_FOR,t);if(is_name(p)){n->text=fe_arena_strdup(&p->ast->arena,p->current.begin,p->current.length);next(p);}else error(p,"expected loop variable");if(eat(p,FE_TOK_COMMA)){if(is_name(p)){n->aux_text=fe_arena_strdup(&p->ast->arena,p->current.begin,p->current.length);next(p);}else error(p,"expected second loop variable");}want(p,FE_TOK_IN,"expected 'in' in for");n->a=header_expr(p);if(eat(p,FE_TOK_DOTDOT))n->c=header_expr(p);n->b=block(p);return n;} + if(eat(p,FE_TOK_MATCH)) { + int old=p->forbid_struct_literal; + n=toknode(p,FE_N_MATCH,t); p->forbid_struct_literal=1; n->a=header_expr(p); p->forbid_struct_literal=old; + want(p,FE_TOK_LBRACE,"expected '{' after match expression"); + while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)) { + FeNode *arm=toknode(p,FE_N_ARM,p->current); + FeToken pt=p->current; + if(is_name(p)||is(p,FE_TOK_INT)||is(p,FE_TOK_CHAR)||is(p,FE_TOK_NULL)||is(p,FE_TOK_TRUE)||is(p,FE_TOK_FALSE)) { + arm->text=fe_arena_strdup(&p->ast->arena,pt.begin,pt.length); next(p); + } else { error(p,"expected match pattern"); recover(p); continue; } + if(eat(p,FE_TOK_LPAREN)) { + while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)) { + if(is_name(p)) { fe_node_add(arm,toknode(p,FE_N_IDENT,p->current)); next(p); } + else { error(p,"expected pattern binding"); recover(p); break; } + if(!eat(p,FE_TOK_COMMA)) break; + } + want(p,FE_TOK_RPAREN,"expected ')' after match pattern"); + } else if(eat(p,FE_TOK_LBRACE)) { + while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)) { + if(is_name(p)) { fe_node_add(arm,toknode(p,FE_N_IDENT,p->current)); next(p); } + else { error(p,"expected field binding"); recover(p); break; } + if(!eat(p,FE_TOK_COMMA)) break; + } + want(p,FE_TOK_RBRACE,"expected '}' after match pattern"); + } + want(p,FE_TOK_FATARROW,"expected '=>' in match arm"); + if(is(p,FE_TOK_LBRACE)) arm->a=block(p); + else { arm->a=expr(p,0); want(p,FE_TOK_SEMI,"expected ';' in match arm"); } + fe_node_add(n,arm); + } + want(p,FE_TOK_RBRACE,"expected '}' after match"); return n; + } + if(eat(p,FE_TOK_RETURN)) {n=toknode(p,FE_N_RETURN,t);if(!is(p,FE_TOK_SEMI))n->a=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after return");return n;} + if(eat(p,FE_TOK_BREAK)){n=toknode(p,FE_N_BREAK,t);want(p,FE_TOK_SEMI,"expected ';'");return n;} + if(eat(p,FE_TOK_CONTINUE)){n=toknode(p,FE_N_CONTINUE,t);want(p,FE_TOK_SEMI,"expected ';'");return n;} + if(eat(p,FE_TOK_DEFER)){n=toknode(p,FE_N_DEFER,t);n->a=block(p);return n;} + if(eat(p,FE_TOK_UNSAFE)){n=toknode(p,FE_N_UNSAFE,t);n->a=block(p);return n;} + if(eat(p,FE_TOK_CRITICAL)){n=toknode(p,FE_N_UNSAFE,t);n->text=fe_arena_strdup(&p->ast->arena,"critical",8);n->a=block(p);return n;} + if(eat(p,FE_TOK_ASM)){n=toknode(p,FE_N_ASM,t);want(p,FE_TOK_LBRACE,"expected '{' after asm");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF))next(p);want(p,FE_TOK_RBRACE,"expected '}' after asm");return n;} + e=expr(p,0); if(is(p,FE_TOK_EQ)||is(p,FE_TOK_PLUS_EQ)||is(p,FE_TOK_MINUS_EQ)||is(p,FE_TOK_STAR_EQ)||is(p,FE_TOK_SLASH_EQ)||is(p,FE_TOK_PERCENT_EQ)||is(p,FE_TOK_AND_EQ)||is(p,FE_TOK_OR_EQ)||is(p,FE_TOK_XOR_EQ)||is(p,FE_TOK_SHL_EQ)||is(p,FE_TOK_SHR_EQ)){n=toknode(p,FE_N_ASSIGN,p->current);n->a=e;next(p);n->b=expr(p,0);}else{n=toknode(p,FE_N_EXPR_STMT,t);n->a=e;}want(p,FE_TOK_SEMI,"expected ';' after statement");return n; +} + +/* A unit path is dotted: `game.world.map`. It is stored canonically, dots and + all, because that spelling is the unit's identity everywhere else. */ +static char *unit_path(FeParser *p) +{ + char buf[256]; + unsigned long len=0; + if(!is_name(p)) return 0; + for(;;) { + unsigned long n=p->current.length; + if(len && len+1=sizeof buf){error(p,"unit path is too long");return 0;} + memcpy(buf+len,p->current.begin,n); + len+=n; + next(p); + if(!eat(p,FE_TOK_DOT)) break; + if(!is_name(p)){error(p,"expected a name after '.' in unit path");return 0;} + } + return fe_arena_strdup(&p->ast->arena,buf,len); +} + +FeNode *fe_parse_unit(FeParser *p) +{ + FeToken t=p->current; FeNode *root; char *path; + if(!eat(p,FE_TOK_UNIT)){error(p,"source must start with 'unit'");return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"unit",4);} + root=toknode(p,FE_N_UNIT,t); + path=unit_path(p); + if(path) root->text=path; else error(p,"expected unit name"); + want(p,FE_TOK_SEMI,"expected ';' after unit name"); + while(eat(p,FE_TOK_IMPORT)){ + FeToken it=p->previous;FeNode *i=toknode(p,FE_N_IMPORT,it); + path=unit_path(p); + if(path) i->text=path; else error(p,"expected import name"); + /* `as` renames the binding; without it the binding is the last segment. */ + if(eat(p,FE_TOK_AS)) { + if(is_name(p)){i->aux_text=fe_arena_strdup(&p->ast->arena,p->current.begin,p->current.length);next(p);} + else error(p,"expected an alias name after 'as'"); + } + want(p,FE_TOK_SEMI,"expected ';' after import"); + fe_node_add(root,i); + } + while(!is(p,FE_TOK_EOF)){FeNode *d=decl(p);if(d)fe_node_add(root,d);} + return root; +} diff --git a/fec/src/parser.h b/fec/src/parser.h new file mode 100644 index 0000000..8b6f5fe --- /dev/null +++ b/fec/src/parser.h @@ -0,0 +1,18 @@ +#ifndef FE_PARSER_H +#define FE_PARSER_H + +#include "ast.h" + +typedef struct FeParser { + FeLexer lexer; + FeToken current; + FeToken previous; + FeAst *ast; + FeDiags *diags; + int forbid_struct_literal; +} FeParser; + +void fe_parser_init(FeParser *p, FeAst *ast, const char *src, unsigned long length, const char *file, FeDiags *d); +FeNode *fe_parse_unit(FeParser *p); + +#endif diff --git a/fec/src/report.c b/fec/src/report.c new file mode 100644 index 0000000..90ca99f --- /dev/null +++ b/fec/src/report.c @@ -0,0 +1,120 @@ +#include "report.h" +#include + +/* What `--report-unsafe` and `--report-instances` print. + * + * Both answer a question that is easy to ask and easy to let slide: how much of + * the program is outside what the checker can promise, and how much code the + * generic instances are about to become. A number nobody can produce is not a + * budget, so these are here rather than in a comment somewhere. */ + +typedef struct Counts { + unsigned unsafe_blocks; + unsigned raw_types; + unsigned unchecked_calls; +} Counts; + +/* Does this name end in `_unchecked`? Those are the deliberate holes in the + checked surface, and they are worth counting separately from `unsafe` + because they do not need a block around them. */ +static int is_unchecked(const char *name) +{ + unsigned long n; + unsigned long m = 10UL; /* strlen("_unchecked") */ + if (!name) return 0; + n = (unsigned long)strlen(name); + if (n < m) return 0; + return strcmp(name + (n - m), "_unchecked") == 0; +} + +static void walk(const FeNode *n, Counts *c) +{ + const FeNode *x; + if (!n) return; + if (n->kind == FE_N_UNSAFE) ++c->unsafe_blocks; + if (n->kind == FE_N_TYPE && n->text && strcmp(n->text, "*") == 0) + ++c->raw_types; + if (n->kind == FE_N_CALL) { + const char *callee = n->text; + if (!callee && n->a) { + if (n->a->kind == FE_N_IDENT) callee = n->a->text; + else if (n->a->kind == FE_N_MEMBER && n->a->b) + callee = n->a->b->text; + } + if (is_unchecked(callee)) ++c->unchecked_calls; + } + walk(n->a, c); + walk(n->b, c); + walk(n->c, c); + for (x = n->children; x; x = x->next) walk(x, c); +} + +/* The standard library is where the unchecked things are supposed to live, so + it is reported but kept out of the total a program is judged on. */ +static int is_std(const char *unit) +{ + return unit && strncmp(unit, "std.", 4) == 0; +} + +void fe_report_unsafe(const FeBuild *build, FILE *out) +{ + unsigned u; + Counts total; + Counts outside; + total.unsafe_blocks = 0; total.raw_types = 0; total.unchecked_calls = 0; + outside = total; + fprintf(out, "%-20s %8s %8s %10s\n", "unit", "unsafe", "*T", "unchecked"); + for (u = 0; u < build->count; ++u) { + const FeUnit *unit = &build->units[u]; + Counts c; + c.unsafe_blocks = 0; c.raw_types = 0; c.unchecked_calls = 0; + walk(unit->ast.root, &c); + if (!c.unsafe_blocks && !c.raw_types && !c.unchecked_calls) continue; + fprintf(out, "%-20s %8u %8u %10u\n", unit->name, c.unsafe_blocks, + c.raw_types, c.unchecked_calls); + total.unsafe_blocks += c.unsafe_blocks; + total.raw_types += c.raw_types; + total.unchecked_calls += c.unchecked_calls; + if (!is_std(unit->name)) { + outside.unsafe_blocks += c.unsafe_blocks; + outside.raw_types += c.raw_types; + outside.unchecked_calls += c.unchecked_calls; + } + } + fprintf(out, "%-20s %8u %8u %10u\n", "total", total.unsafe_blocks, + total.raw_types, total.unchecked_calls); + fprintf(out, "%-20s %8u %8u %10u\n", "outside std", outside.unsafe_blocks, + outside.raw_types, outside.unchecked_calls); +} + +void fe_report_instances(const FeCheck *c, FILE *out) +{ + unsigned i; + unsigned types = 0; + unsigned methods = 0; + unsigned long bytes = 0; + fprintf(out, "%-52s %6s %8s\n", "instance", "kind", "size"); + for (i = 0; i < c->instance_count; ++i) { + const FeInstance *inst = &c->instances[i]; + unsigned long size = 0; + if (inst->owner) ++methods; + else { + ++types; + /* A struct instance is code only through its methods; what it + costs on its own is the storage one value of it takes. */ + { + const FeType *t; + for (t = c->types.types; t; t = t->next) + if (t->name[0] && !strcmp(t->name, inst->key)) { + size = t->size; + break; + } + } + bytes += size; + } + fprintf(out, "%-52s %6s %8lu\n", inst->key, + inst->owner ? "method" : "type", size); + } + fprintf(out, "\n%u instances: %u types (%lu bytes of storage), %u methods\n", + c->instance_count, types, bytes, methods); +} diff --git a/fec/src/report.h b/fec/src/report.h new file mode 100644 index 0000000..d7546f2 --- /dev/null +++ b/fec/src/report.h @@ -0,0 +1,13 @@ +#ifndef FE_REPORT_H +#define FE_REPORT_H + +#include "check.h" +#include + +/* How much of the build is outside what the checker promises. */ +void fe_report_unsafe(const FeBuild *build, FILE *out); + +/* What the generic instances came to. */ +void fe_report_instances(const FeCheck *c, FILE *out); + +#endif diff --git a/fec/src/resolve.c b/fec/src/resolve.c new file mode 100644 index 0000000..70baa30 --- /dev/null +++ b/fec/src/resolve.c @@ -0,0 +1,328 @@ +#include "resolve.h" +#include "parser.h" + +#include +#include +#include + +static int segment_ok(const char *s, unsigned long n, const char **why) +{ + unsigned long i; + if (!n) { *why = "unit path segment is empty"; return 0; } + if (n > FE_UNIT_SEGMENT_MAX) { + *why = "unit path segment is longer than eight characters"; + return 0; + } + if (s[0] < 'a' || s[0] > 'z') { + *why = "unit path segment must start with a lowercase letter"; + return 0; + } + for (i = 1; i < n; ++i) { + char c = s[i]; + if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') continue; + *why = "unit path segment may only contain lowercase letters, digits and '_'"; + return 0; + } + return 1; +} + +/* Compare a dotted unit path against the source path it was read from. + `game.world.map` matches `.../game/world/map.fe` and nothing else. Only the + trailing segments are compared, since the leading part is the import root. */ +static int path_matches(const char *unit, const char *source) +{ + unsigned long ulen = strlen(unit), slen = strlen(source); + unsigned long u, s; + if (slen < 3 || strcmp(source + slen - 3, ".fe") != 0) return 0; + slen -= 3; + u = ulen; + s = slen; + while (u > 0) { + char uc, sc; + --u; + if (s == 0) return 0; + --s; + uc = unit[u]; + sc = source[s]; + if (uc == '.') { + if (sc != '/' && sc != '\\') return 0; + continue; + } + /* Host filesystems may be case-insensitive; the unit name is the + authority and is lowercase by 8.1, so fold the path side down. */ + if (sc >= 'A' && sc <= 'Z') sc = (char)(sc - 'A' + 'a'); + if (uc != sc) return 0; + } + /* What remains of the source path is the import root, and must end there. */ + return s == 0 || source[s - 1] == '/' || source[s - 1] == '\\'; +} + +static char *read_source(const char *path, unsigned long *size) +{ + FILE *f; + long n; + char *p; + f = fopen(path, "rb"); + if (!f) return 0; + fseek(f, 0, SEEK_END); + n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { fclose(f); return 0; } + p = (char *)malloc((unsigned long)n + 1); + if (!p) { fclose(f); return 0; } + if (fread(p, 1, (unsigned long)n, f) != (unsigned long)n) { + fclose(f); free(p); return 0; + } + p[n] = 0; + fclose(f); + *size = (unsigned long)n; + return p; +} + +/* /a/b.fe for the unit a.b */ +static void unit_source_path(char *out, unsigned long cap, + const char *root, const char *unit) +{ + unsigned long i = 0, j = 0; + while (root[i] && j + 1 < cap) out[j++] = root[i++]; + if (j && out[j - 1] != '/' && out[j - 1] != '\\' && j + 1 < cap) out[j++] = '/'; + for (i = 0; unit[i] && j + 1 < cap; ++i) + out[j++] = unit[i] == '.' ? '/' : unit[i]; + if (j + 3 < cap) { out[j++] = '.'; out[j++] = 'f'; out[j++] = 'e'; } + out[j] = 0; +} + +/* Strip the unit's own path from the file it was read from; what is left is + the import root that every other unit is looked up under. */ +static void import_root(char *out, unsigned long cap, + const char *source, const char *unit) +{ + unsigned long slen = strlen(source); + unsigned long dots = 0, i, cut; + for (i = 0; unit[i]; ++i) if (unit[i] == '.') ++dots; + if (slen >= 3) slen -= 3; + cut = slen; + for (i = 0; i <= dots; ++i) { + while (cut > 0 && source[cut - 1] != '/' && source[cut - 1] != '\\') --cut; + if (i < dots && cut > 0) --cut; + } + if (cut >= cap) cut = cap - 1; + memcpy(out, source, cut); + out[cut] = 0; + if (!cut) { out[0] = '.'; out[1] = 0; } +} + +static FeUnit *find_unit(FeBuild *b, const char *name) +{ + unsigned i; + for (i = 0; i < b->count; ++i) + if (strcmp(b->units[i].name, name) == 0) return &b->units[i]; + return 0; +} + +const char *fe_import_binding(const FeNode *import) +{ + const char *dot; + if (!import) return 0; + if (import->aux_text) return import->aux_text; + dot = import->text ? strrchr(import->text, '.') : 0; + return dot ? dot + 1 : import->text; +} + +int fe_resolve_unit_identity(FeAst *ast, FeDiags *diags, const char *source_path) +{ + FeNode *root = ast ? ast->root : 0; + const char *name, *why; + const char *seg; + unsigned long i, len; + int ok = 1; + + if (!root || root->kind != FE_N_UNIT || !root->text) return 0; + name = root->text; + len = strlen(name); + + seg = name; + for (i = 0; i <= len; ++i) { + if (i != len && name[i] != '.') continue; + if (!segment_ok(seg, (unsigned long)(name + i - seg), &why)) { + fe_diag_error(diags, root->loc, why); + ok = 0; + } + seg = name + i + 1; + } + + if (ok && source_path && !path_matches(name, source_path)) { + fe_diag_errorf(diags, root->loc, + "unit %s must be declared in a source file matching its path", + name); + ok = 0; + } + return ok; +} + +/* Depth-first load. `stack` is the chain of units currently being loaded, so + meeting one again is a cycle rather than a repeat visit. */ +static int load_unit(FeBuild *b, const char *name, FeLoc from, int have_from, + const char **stack, unsigned depth) +{ + FeUnit *unit; + FeNode *n; + FeParser p; + unsigned long size; + unsigned i; + int ok = 1; + + for (i = 0; i < depth; ++i) { + if (strcmp(stack[i], name) == 0) { + fe_diag_errorf(b->diags, from, "import of %s forms a cycle", name); + return 0; + } + } + if (find_unit(b, name)) return 1; + if (b->count >= FE_BUILD_UNIT_MAX) { + fe_diag_error(b->diags, from, "too many units in one build"); + return 0; + } + if (strlen(name) >= FE_UNIT_PATH_MAX) { + fe_diag_errorf(b->diags, from, "unit path is too long: %s", name); + return 0; + } + unit = &b->units[b->count]; + memset(unit, 0, sizeof *unit); + strcpy(unit->name, name); + /* `std` is reserved (SPEC 10) and lives with the compiler, not with the + program, so it is looked up under its own root. */ + unit_source_path(unit->path, sizeof unit->path, + (name[0]=='s' && name[1]=='t' && name[2]=='d' && + (name[3]=='.' || name[3]==0) && b->std_root[0]) + ? b->std_root : b->root, + name); + unit->source = read_source(unit->path, &size); + if (!unit->source) { + if (have_from) + fe_diag_errorf(b->diags, from, "import %s has no source file", name); + else + fe_diag_errorf(b->diags, from, "cannot open %s", unit->path); + return 0; + } + b->count++; + unit->size = size; + fe_ast_init(&unit->ast); + /* Diagnostics from here on belong to this file. */ + fe_diags_source(b->diags, unit->source, size); + fe_parser_init(&p, &unit->ast, unit->source, size, unit->path, b->diags); + unit->ast.root = fe_parse_unit(&p); + unit->loaded = 1; + if (!fe_resolve_unit_identity(&unit->ast, b->diags, unit->path)) ok = 0; + + stack[depth] = unit->name; + for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) { + if (n->kind != FE_N_IMPORT || !n->text) continue; + /* The import statement is where the reader has to make a change, so + the diagnostic points there rather than at the unit it names. */ + if (!load_unit(b, n->text, n->loc, 1, stack, depth + 1)) ok = 0; + fe_diags_source(b->diags, unit->source, unit->size); + } + stack[depth] = 0; + return ok; +} + +/* A binding names one unit inside one importer; two imports cannot claim it. */ +static int check_bindings(FeBuild *b, FeUnit *unit) +{ + FeNode *n, *m; + int ok = 1; + for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) { + const char *a; + if (n->kind != FE_N_IMPORT) continue; + a = fe_import_binding(n); + if (!a) continue; + for (m = unit->ast.root->children; m != n; m = m->next) { + const char *other; + if (m->kind != FE_N_IMPORT) continue; + other = fe_import_binding(m); + if (other && strcmp(a, other) == 0) { + fe_diag_errorf(b->diags, n->loc, + "import binding %s is already taken; use an alias", a); + ok = 0; + } + } + } + return ok; +} + +int fe_build_load(FeBuild *build, const char *entry, FeDiags *diags, + const char *std_root) +{ + const char *stack[FE_BUILD_UNIT_MAX]; + FeAst probe; + FeParser p; + char *source; + char name[FE_UNIT_PATH_MAX]; + FeLoc loc; + unsigned long size; + unsigned i; + int ok; + + memset(build, 0, sizeof *build); + build->diags = diags; + if (std_root) { + unsigned long k = 0; + while (std_root[k] && k + 1 < sizeof build->std_root) { + build->std_root[k] = std_root[k]; + ++k; + } + build->std_root[k] = 0; + } + + /* The entry file fixes the import root, so it has to be parsed far enough + to know its own name before anything else can be found. */ + source = read_source(entry, &size); + if (!source) { + FeLoc none; + none.file = entry; none.line = 0; none.col = 0; + fe_diag_errorf(diags, none, "cannot open %s", entry); + return 0; + } + fe_ast_init(&probe); + fe_parser_init(&p, &probe, source, size, entry, diags); + probe.root = fe_parse_unit(&p); + if (!probe.root || !probe.root->text || diags->errors) { + fe_ast_destroy(&probe); + free(source); + return 0; + } + import_root(build->root, sizeof build->root, entry, probe.root->text); + strncpy(name, probe.root->text, sizeof name - 1); + name[sizeof name - 1] = 0; + loc = probe.root->loc; + fe_ast_destroy(&probe); + free(source); + + ok = load_unit(build, name, loc, 0, stack, 0); + for (i = 0; i < build->count; ++i) + if (!check_bindings(build, &build->units[i])) ok = 0; + return ok && diags->errors == 0; +} + +void fe_build_destroy(FeBuild *build) +{ + unsigned i; + for (i = 0; i < build->count; ++i) { + if (build->units[i].loaded) fe_ast_destroy(&build->units[i].ast); + free(build->units[i].source); + } + build->count = 0; +} + +FeUnit *fe_build_binding(FeBuild *build, FeUnit *unit, const char *binding) +{ + FeNode *n; + for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) { + const char *bound; + if (n->kind != FE_N_IMPORT || !n->text) continue; + bound = fe_import_binding(n); + if (bound && strcmp(bound, binding) == 0) return find_unit(build, n->text); + } + return 0; +} diff --git a/fec/src/resolve.h b/fec/src/resolve.h new file mode 100644 index 0000000..69448c1 --- /dev/null +++ b/fec/src/resolve.h @@ -0,0 +1,63 @@ +#ifndef FE_RESOLVE_H +#define FE_RESOLVE_H + +#include "ast.h" +#include "diag.h" + +/* Unit-level resolution: identity, import bindings, and the unit graph. + This runs between parsing and semantic checking. It answers questions that + need more than one file -- what a unit is called, what it imports, and + whether those imports exist and terminate -- so that check.c can keep + looking at one function at a time. */ + +/* SPEC 8.1: each segment is ASCII lowercase, starts with a letter, continues + with letters, digits or '_', and is at most eight characters. The limit is + what makes a unit path map to a FAT/DOS 8.3 source path unambiguously. */ +#define FE_UNIT_SEGMENT_MAX 8 +#define FE_UNIT_PATH_MAX 128 +#define FE_BUILD_UNIT_MAX 256 + +typedef struct FeUnit { + char name[FE_UNIT_PATH_MAX]; /* canonical dotted path */ + char path[260]; /* source file it was read from */ + FeAst ast; + char *source; /* owned; freed with the build */ + unsigned long size; + int loaded; + int checked; +} FeUnit; + +typedef struct FeBuild { + FeUnit units[FE_BUILD_UNIT_MAX]; + unsigned count; + char root[260]; /* import root: where unit paths start */ + /* Where `std.*` is looked for. The standard library is not under the + program's root -- it ships with the compiler. */ + char std_root[260]; + FeDiags *diags; +} FeBuild; + +/* Validate the `unit` declaration against SPEC 8.1, and against the file it was + read from: the path must match the dotted name, so `game.world.map` has to + come from `game/world/map.fe`. `source_path` may be null to skip that half. + Returns non-zero when the unit is well formed. */ +int fe_resolve_unit_identity(FeAst *ast, FeDiags *diags, const char *source_path); + +/* Load `entry` and everything it imports, transitively. + + The import root is derived from the entry file: a unit named `a.b` read from + `/a/b.fe` fixes ``, so a sibling `import c.d;` is looked for at + `/c/d.fe`. Reports missing imports, import cycles, and binding + conflicts. Returns non-zero when the whole graph loaded cleanly. */ +int fe_build_load(FeBuild *build, const char *entry, FeDiags *diags, + const char *std_root); +void fe_build_destroy(FeBuild *build); + +/* The unit a binding refers to inside `unit`, or null. + The binding is the last segment of the import path unless `as` renamed it. */ +FeUnit *fe_build_binding(FeBuild *build, FeUnit *unit, const char *binding); + +/* The local name an import introduces: its alias, or the last path segment. */ +const char *fe_import_binding(const FeNode *import); + +#endif diff --git a/fec/src/types.c b/fec/src/types.c new file mode 100644 index 0000000..46bb024 --- /dev/null +++ b/fec/src/types.c @@ -0,0 +1,782 @@ +#include "types.h" +#include "m7.h" +#include +#include +#include + +static FeType *new_type(FeTypeCtx *ctx, const char *name, FeTypeKind kind) +{ + FeType *t; + unsigned i; + t = (FeType *)fe_arena_alloc(ctx->arena, sizeof(FeType)); + if (!t) return 0; + for (i = 0; i + 1U < sizeof(t->name) && name && name[i]; ++i) + t->name[i] = name[i]; + t->name[i] = '\0'; + t->kind = kind; + t->unit = 0; + t->cname = 0; + t->maker = 0; + t->none_cname = 0; + t->unwrap_cname = 0; + t->indexer = 0; + t->slicer = 0; + t->full_slicer = 0; + t->tail_slicer = 0; + t->drop_cname = 0; + t->alloc_cname = 0; + t->replace_cname = 0; + t->bits = 0; + t->is_unsigned = 0; + t->packed = 0; + t->is_error = 0; + t->has_drop = 0; + t->length = 0; + t->size = 0; + t->align = 1; + t->elem = 0; + t->error_value = 0; + t->ref_mut = 0; + t->fields = 0; + t->field_count = 0; + t->variants = 0; + t->variant_count = 0; + t->serial = ctx->generated_serial++; + t->decl_node = 0; + t->bind_count = 0; + t->next = ctx->types; + t->emit_state = 0; + t->cycle_state = 0; + t->layout_state = 0; + t->building = 0; + ctx->types = t; + return t; +} + +void fe_types_init(FeTypeCtx *ctx, FeArena *arena, unsigned pointer_bits) +{ + ctx->arena = arena; + ctx->types = 0; + ctx->pointer_bits = pointer_bits; + ctx->unit_name = "unit"; + ctx->generated_serial = 0; + ctx->param_count = 0; + ctx->instantiate = 0; + ctx->instantiate_owner = 0; + ctx->enter_decl = 0; + ctx->leave_decl = 0; +} + +/* Does this type answer to `name` for someone checking `unit`? A type with no + unit is shared by everyone; one with a unit answers only inside it. */ +static int type_visible_as(const FeType *t, const char *unit, const char *name) +{ + if (strcmp(t->name, name) != 0) return 0; + if (!t->unit) return 1; + return unit && strcmp(t->unit, unit) == 0; +} + +FeType *fe_type_intern_unit(FeTypeCtx *ctx, const char *unit, const char *name) +{ + FeType *t; + if (!name) name = ""; + if (!unit) return fe_type_intern(ctx, name); + for (t = ctx->types; t; t = t->next) + if (t->unit && strcmp(t->name, name) == 0 && + strcmp(t->unit, unit) == 0) return t; + t = new_type(ctx, name, FE_TYPE_UNKNOWN); + if (t) t->unit = unit; + return t; +} + +FeType *fe_type_intern(FeTypeCtx *ctx, const char *name) +{ + FeType *t; + unsigned bits = 0; + int uns = 0; + FeTypeKind kind = FE_TYPE_UNKNOWN; + unsigned i; + if (!name) name = ""; + /* A bound type parameter is its argument, and shadows everything. */ + for (i = 0; i < ctx->param_count; ++i) + if (strcmp(ctx->params[i].name, name) == 0) return ctx->params[i].type; + for (t = ctx->types; t; t = t->next) + if (type_visible_as(t, ctx->unit_name, name)) return t; + if (strcmp(name, "void") == 0) kind = FE_TYPE_VOID; + else if (strcmp(name, "bool") == 0) kind = FE_TYPE_BOOL; + else if (strcmp(name, "char") == 0) kind = FE_TYPE_CHAR; + else if (strcmp(name, "str") == 0) + return fe_type_slice(ctx, fe_type_intern(ctx, "u8")); + else if (strcmp(name, "io.Writer") == 0) { + kind = FE_TYPE_STRUCT; + } + /* The default error set. Its members are collected across the build rather + than declared, so it carries an identity but no variant list. */ + else if (strcmp(name, "core.Error") == 0) { + kind = FE_TYPE_ENUM; bits = 16; uns = 1; + } + else if (strcmp(name, "i8") == 0 || strcmp(name, "u8") == 0) { + kind = FE_TYPE_INT; bits = 8; uns = name[0] == 'u'; + } else if (strcmp(name, "i16") == 0 || strcmp(name, "u16") == 0) { + kind = FE_TYPE_INT; bits = 16; uns = name[0] == 'u'; + } else if (strcmp(name, "i32") == 0 || strcmp(name, "u32") == 0) { + kind = FE_TYPE_INT; bits = 32; uns = name[0] == 'u'; + } else if (strcmp(name, "usize") == 0 || strcmp(name, "isize") == 0) { + kind = FE_TYPE_INT; bits = ctx->pointer_bits; uns = name[0] == 'u'; + } + t = new_type(ctx, name, kind); + if (!t) return 0; + t->bits = bits; + t->is_unsigned = uns; + if (strcmp(name,"core.Error")==0) { t->is_error = 1; t->size = 2; t->align = 2; } + if (strcmp(name,"io.Writer")==0) { + t->cname=fe_arena_strdup(ctx->arena,"fe_writer",10); + t->size=4; + t->align=1; + return t; + } + if (kind == FE_TYPE_STR) { + t->cname = fe_arena_strdup(ctx->arena, "fe_str", 6); + t->elem = fe_type_intern(ctx, "u8"); + t->indexer = "fe_idx_str"; + t->slicer = "fe_slice_str"; + t->full_slicer = "fe_full_slice_str"; + t->tail_slicer = "fe_tail_slice_str"; + } + return t; +} + +static char *generated_name(FeTypeCtx *ctx, const char *prefix, + const char *name) +{ + char number[24]; + unsigned long n; + char *p; + sprintf(number, "%u", ctx->generated_serial++); + n = (unsigned long)strlen(prefix) + (unsigned long)strlen(name) + + (unsigned long)strlen(number) + 2UL; + p = (char *)fe_arena_alloc(ctx->arena, n); + if (!p) return 0; + strcpy(p, prefix); + strcat(p, name); + strcat(p, "_"); + strcat(p, number); + return p; +} + +FeType *fe_type_array(FeTypeCtx *ctx, unsigned long length, FeType *elem) +{ + char key[320]; + FeType *t; + sprintf(key, "[%lu]%s", length, elem ? elem->name : "?"); + t = fe_type_intern(ctx, key); + if (t->kind == FE_TYPE_UNKNOWN) { + t->kind = FE_TYPE_ARRAY; + t->length = length; + t->elem = elem; + t->cname = generated_name(ctx, "struct fe_arr_", "type"); + t->maker = generated_name(ctx, "fe_make_arr_", "type"); + t->drop_cname = generated_name(ctx, "fe_drop_arr_", "type"); + t->indexer = generated_name(ctx, "fe_idx_arr_", "type"); + t->slicer = generated_name(ctx, "fe_slice_arr_", "type"); + t->full_slicer = generated_name(ctx, "fe_full_arr_", "type"); + t->tail_slicer = generated_name(ctx, "fe_tail_arr_", "type"); + } + return t; +} + +FeType *fe_type_slice(FeTypeCtx *ctx, FeType *elem) +{ + char key[320]; + FeType *t; + sprintf(key, "[]%s", elem ? elem->name : "?"); + t = fe_type_intern(ctx, key); + if (t->kind == FE_TYPE_UNKNOWN) { + t->kind = FE_TYPE_SLICE; + t->elem = elem; + t->cname = generated_name(ctx, "fe_slice_", "type"); + t->maker = generated_name(ctx, "fe_make_slice_", "type"); + t->indexer = generated_name(ctx, "fe_idx_slice_", "type"); + t->slicer = generated_name(ctx, "fe_slice_slice_", "type"); + t->full_slicer = generated_name(ctx, "fe_full_slice_", "type"); + t->tail_slicer = generated_name(ctx, "fe_tail_slice_", "type"); + } + return t; +} + +FeType *fe_type_mut_slice(FeTypeCtx *ctx, FeType *elem) +{ + char key[320]; + FeType *t; + sprintf(key, "[]mut %s", elem ? elem->name : "?"); + t = fe_type_intern(ctx, key); + if (t->kind == FE_TYPE_UNKNOWN) { + t->kind = FE_TYPE_SLICE; + t->elem = elem; + t->ref_mut = 1; + t->cname = generated_name(ctx, "fe_mut_slice_", "type"); + t->maker = generated_name(ctx, "fe_make_mut_slice_", "type"); + t->indexer = generated_name(ctx, "fe_idx_mut_slice_", "type"); + t->slicer = generated_name(ctx, "fe_slice_mut_slice_", "type"); + t->full_slicer = generated_name(ctx, "fe_full_mut_slice_", "type"); + t->tail_slicer = generated_name(ctx, "fe_tail_mut_slice_", "type"); + } + return t; +} + +FeType *fe_type_ref(FeTypeCtx *ctx, FeType *elem, int mutable) +{ + char key[320]; + FeType *t; + sprintf(key,"%s%s",mutable ? "&mut " : "&",elem ? elem->name : "?"); + t=fe_type_intern(ctx,key); + if(t->kind==FE_TYPE_UNKNOWN) { + t->kind=FE_TYPE_REF; + t->elem=elem; + t->ref_mut=mutable; + } + return t; +} + +FeType *fe_type_raw(FeTypeCtx *ctx, FeType *elem) +{ + char key[320]; + FeType *t; + sprintf(key, "*%s", elem ? elem->name : "?"); + t = fe_type_intern(ctx, key); + if (t->kind == FE_TYPE_UNKNOWN) { + t->kind = FE_TYPE_RAW; + t->elem = elem; + t->size = FE_PTR_SIZE; + t->align = FE_PTR_ALIGN; + } + return t; +} + +FeType *fe_type_owned(FeTypeCtx *ctx, FeType *elem) +{ + char key[320]; + FeType *t; + sprintf(key,"^%s",elem ? elem->name : "?"); + t=fe_type_intern(ctx,key); + if(t->kind==FE_TYPE_UNKNOWN) { + t->kind=FE_TYPE_OWNED; + t->elem=elem; + if(elem && elem->kind==FE_TYPE_SLICE) { + t->cname=generated_name(ctx,"fe_owned_slice_","type"); + t->maker=generated_name(ctx,"fe_make_owned_slice_","type"); + } + } + return t; +} + +FeType *fe_type_error_union(FeTypeCtx *ctx, FeType *value) +{ + char key[320]; + FeType *t; + sprintf(key,"!%s",value ? value->name : "?"); + t=fe_type_intern(ctx,key); + if(t->kind==FE_TYPE_UNKNOWN) { + t->kind=FE_TYPE_ERROR_UNION; + t->error_value=value; + t->drop_cname=generated_name(ctx,"fe_drop_result_","value"); + if (value && value->kind != FE_TYPE_VOID) { + t->cname=generated_name(ctx,"struct fe_result_","value"); + t->maker=generated_name(ctx,"fe_make_result_","value"); + t->none_cname=generated_name(ctx,"fe_fail_result_","value"); + t->alloc_cname=generated_name(ctx,"fe_alloc_result_","value"); + } + } + return t; +} + +void fe_type_require_replace(FeTypeCtx *ctx, FeType *type) +{ + if(type && !type->replace_cname) + type->replace_cname=generated_name(ctx,"fe_replace_","type"); +} + +FeType *fe_type_declare_struct(FeTypeCtx *ctx, const FeNode *node, int packed) +{ + FeType *t; + FeNode *f; + unsigned count = 0; + unsigned i = 0; + char *cname; + if (!node || !node->text) return 0; + t = fe_type_intern_unit(ctx, ctx->unit_name, node->text); + if (t->kind != FE_TYPE_UNKNOWN && t->kind != FE_TYPE_STRUCT) return t; + if (t->kind == FE_TYPE_STRUCT) return t; + t->kind = FE_TYPE_STRUCT; + t->packed = packed; + t->decl_node = node; + for (f = node->children; f; f = f->next) + if (f->kind==FE_N_FN && f->text && strcmp(f->text,"drop")==0) + t->has_drop=1; + cname = (char *)fe_arena_alloc(ctx->arena, + (unsigned long)strlen("struct fe_") + strlen(ctx->unit_name) + + strlen(node->text) + 2UL); + if (!cname) return t; + strcpy(cname, "struct fe_"); + strcat(cname, ctx->unit_name); + strcat(cname, "_"); + strcat(cname, node->text); + t->cname = cname; + t->maker = generated_name(ctx, "fe_make_", node->text); + t->drop_cname = generated_name(ctx, "fe_drop_", node->text); + for (f = node->children; f; f = f->next) + if (f->kind == FE_N_FIELD) ++count; + t->field_count = count; + if (count) { + t->fields = (FeFieldType *)fe_arena_alloc(ctx->arena, + count * sizeof(FeFieldType)); + if (!t->fields) return t; + for (f = node->children; f; f = f->next) if (f->kind == FE_N_FIELD) { + t->fields[i].name = f->text; + t->fields[i].type = 0; + t->fields[i].offset = 0; + t->fields[i].ast_node = f; + ++i; + } + } + return t; +} + +FeType *fe_type_declare_enum(FeTypeCtx *ctx, const FeNode *node) +{ + FeType *t; + FeNode *v; + unsigned count = 0; + unsigned i = 0; + char *cname; + if (!node || !node->text) return 0; + t = fe_type_intern_unit(ctx, ctx->unit_name, node->text); + if (t->kind != FE_TYPE_UNKNOWN && t->kind != FE_TYPE_ENUM) return t; + if (t->kind == FE_TYPE_ENUM) return t; + t->kind = FE_TYPE_ENUM; + t->decl_node = node; + cname = (char *)fe_arena_alloc(ctx->arena, + (unsigned long)strlen("struct fe_") + strlen(ctx->unit_name) + + strlen(node->text) + 2UL); + if (!cname) return t; + strcpy(cname, "struct fe_"); + strcat(cname, ctx->unit_name); + strcat(cname, "_"); + strcat(cname, node->text); + t->cname = cname; + for (v = node->children; v; v = v->next) ++count; + t->variant_count = count; + if (count) { + t->variants = (FeVariantType *)fe_arena_alloc(ctx->arena, + count * sizeof(FeVariantType)); + if (!t->variants) return t; + for (v = node->children; v; v = v->next) { + t->variants[i].name = v->text; + t->variants[i].fields = 0; + t->variants[i].field_count = 0; + if (node->kind==FE_N_ERROR_DECL && v->a && + v->a->kind==FE_N_LITERAL && v->a->text) + t->variants[i].tag=(unsigned)strtoul(v->a->text,0,0); + else t->variants[i].tag = i; + t->variants[i].ast_node = v; + t->variants[i].maker = generated_name(ctx, "fe_make_variant_", v->text ? v->text : "variant"); + if (v->a && v->a->kind == FE_N_TYPE) { + t->variants[i].field_count = 1; + t->variants[i].fields = (FeFieldType *)fe_arena_alloc(ctx->arena, sizeof(FeFieldType)); + if (t->variants[i].fields) { + t->variants[i].fields[0].name = "value"; + t->variants[i].fields[0].type = fe_type_from_ast(ctx, v->a); + t->variants[i].fields[0].offset = 0; + t->variants[i].fields[0].ast_node = v; + } + } + if (!v->a) { + FeNode *f; + unsigned fc = 0; + unsigned j = 0; + for (f = v->children; f; f = f->next) + if (f->kind == FE_N_FIELD) ++fc; + t->variants[i].field_count = fc; + if (fc) { + t->variants[i].fields = (FeFieldType *)fe_arena_alloc( + ctx->arena, fc * sizeof(FeFieldType)); + if (t->variants[i].fields) for (f = v->children; f; f=f->next) + if (f->kind == FE_N_FIELD) { + t->variants[i].fields[j].name = f->text; + t->variants[i].fields[j].type = 0; + t->variants[i].fields[j].offset = 0; + t->variants[i].fields[j].ast_node = f; + ++j; + } + } + } + ++i; + } + } + return t; +} + +FeType *fe_type_declare_error(FeTypeCtx *ctx, const FeNode *node) +{ + FeType *t=fe_type_declare_enum(ctx,node); + if (t) t->is_error=1; + return t; +} + +static unsigned long round_up(unsigned long x, unsigned a) +{ + unsigned long rem; + if (a <= 1U) return x; + rem = x % (unsigned long)a; + return rem ? x + (unsigned long)a - rem : x; +} + +unsigned long fe_type_size(const FeType *t) +{ + return t ? t->size : 0; +} + +unsigned long fe_type_payload_offset(const FeType *t) +{ + if (!t) return 0; + if (t->kind == FE_TYPE_ERROR_UNION) { + if (!t->error_value || t->error_value->kind == FE_TYPE_VOID) return 2; + return round_up(2UL, fe_type_align(t->error_value)); + } + if (t->kind == FE_TYPE_OPTIONAL) { + if (fe_m7_optional_uses_niche(t->elem)) return 0; + return round_up(1UL, fe_type_align(t->elem)); + } + if (t->kind == FE_TYPE_ENUM) return round_up(t->bits / 8U, t->align); + return 0; +} + +unsigned fe_type_align(const FeType *t) +{ + return t && t->align ? t->align : 1U; +} + +/* Resolve this type's fields where they were written. Without the callback + installed -- or for a type nobody declared -- everything stays where it is, + which is what the non-checking users of this layer want. */ +static int enter_decl_unit(FeTypeCtx *ctx, const char *unit, const char **was) +{ + *was = ctx->unit_name; + if (!ctx->enter_decl || !unit) return -1; + return ctx->enter_decl(ctx->instantiate_owner, unit); +} + +/* Put back both halves: the unit the checker was in, and the name this layer + was interning under -- an instantiation moves the second without the + first, so restoring one is not restoring the other. */ +static void leave_decl_unit(FeTypeCtx *ctx, int back, const char *was) +{ + if (back >= 0 && ctx->leave_decl) + ctx->leave_decl(ctx->instantiate_owner, back); + ctx->unit_name = was; +} + +/* Did every field end up with a size? A struct whose members are not settled + cannot be settled either -- and freezing it here is worse than leaving it, + because nothing recomputes a type that already has a size. */ +static int members_ready(const FeType *t) +{ + unsigned i; + unsigned j; + if (t->kind == FE_TYPE_STRUCT) { + for (i = 0; i < t->field_count; ++i) { + if (!t->fields[i].type) return 0; + if (t->fields[i].type->layout_state != 2) return 0; + } + return 1; + } + for (i = 0; i < t->variant_count; ++i) + for (j = 0; j < t->variants[i].field_count; ++j) { + if (!t->variants[i].fields[j].type) return 0; + if (t->variants[i].fields[j].type->layout_state != 2) return 0; + } + return 1; +} + +static void layout_type(FeTypeCtx *ctx, FeType *t) +{ + unsigned i; + unsigned align; + unsigned long off; + unsigned long max_size; + unsigned max_align; + if (!t || t->size) return; + if (t->building) return; + if (t->layout_state == 1) { + t->size = 1; + t->align = 1; + return; + } + t->layout_state = 1; + if (t->kind == FE_TYPE_VOID || t->kind == FE_TYPE_UNKNOWN || + t->kind == FE_TYPE_ERROR) { t->size = 0; t->align = 1; t->layout_state = 2; return; } + if (t->kind == FE_TYPE_ERROR_UNION) { + if (t->error_value && t->error_value->kind != FE_TYPE_VOID) { + layout_type(ctx,t->error_value); + t->align=fe_type_align(t->error_value); + t->size=round_up(2UL,t->align)+fe_type_size(t->error_value); + t->size=round_up(t->size,t->align); + } else { + t->size=2; + t->align=2U; + } + t->layout_state = 2; return; + } + if (t->kind == FE_TYPE_OPTIONAL) { + layout_type(ctx,t->elem); + if (fe_m7_optional_uses_niche(t->elem)) { + t->size=fe_type_size(t->elem); + t->align=fe_type_align(t->elem); + } else { + t->align=fe_type_align(t->elem); + t->size=round_up(1UL,t->align)+fe_type_size(t->elem); + t->size=round_up(t->size,t->align); + } + t->layout_state=2; return; + } + if (t->kind == FE_TYPE_BOOL || t->kind == FE_TYPE_CHAR) { + t->size = 1; t->align = 1; t->layout_state = 2; return; + } + if (t->kind == FE_TYPE_INT) { + t->size = (t->bits + 7U) / 8U; + t->align = t->size; + if (t->size > 4UL) t->size = 4UL; + t->layout_state = 2; return; + } + if (t->kind == FE_TYPE_REF || t->kind == FE_TYPE_RAW) { + t->size = FE_PTR_SIZE; + t->align = FE_PTR_ALIGN; + t->layout_state = 2; return; + } + if (t->kind == FE_TYPE_OWNED) { + t->size = t->elem && t->elem->kind==FE_TYPE_SLICE ? + 2UL * FE_PTR_SIZE : FE_PTR_SIZE; + t->align = FE_PTR_ALIGN; + t->layout_state = 2; return; + } + if (t->kind == FE_TYPE_SLICE || t->kind == FE_TYPE_STR) { + t->size = 2UL * FE_PTR_SIZE; + t->align = FE_PTR_ALIGN; + t->layout_state = 2; return; + } + if (t->kind == FE_TYPE_ARRAY) { + layout_type(ctx, t->elem); + t->align = t->packed ? 1U : fe_type_align(t->elem); + t->size = t->length * fe_type_size(t->elem); + t->layout_state = 2; return; + } + if (t->kind == FE_TYPE_STRUCT) { + const char *was; + int back = enter_decl_unit(ctx, t->unit, &was); + for (i = 0; i < t->field_count; ++i) + if (!t->fields[i].type && t->fields[i].ast_node) + t->fields[i].type = fe_type_from_ast(ctx, t->fields[i].ast_node->a); + leave_decl_unit(ctx, back, was); + off = 0; max_align = 1; + for (i = 0; i < t->field_count; ++i) { + layout_type(ctx, t->fields[i].type); + align = t->packed ? 1U : fe_type_align(t->fields[i].type); + if (align > max_align) max_align = align; + off = round_up(off, align); + t->fields[i].offset = off; + off += fe_type_size(t->fields[i].type); + } + if (!members_ready(t)) { t->layout_state = 0; return; } + t->align = max_align; + t->size = round_up(off, max_align); + t->layout_state = 2; + return; + } + if (t->kind == FE_TYPE_ENUM) { + const char *was; + int back = enter_decl_unit(ctx, t->unit, &was); + for (i = 0; i < t->variant_count; ++i) { + unsigned j; + for (j = 0; j < t->variants[i].field_count; ++j) + if (!t->variants[i].fields[j].type && t->variants[i].fields[j].ast_node) + t->variants[i].fields[j].type = fe_type_from_ast( + ctx, t->variants[i].fields[j].ast_node->a); + } + leave_decl_unit(ctx, back, was); + max_size = 0; max_align = 1; + for (i = 0; i < t->variant_count; ++i) { + unsigned j; + off = 0; + for (j = 0; j < t->variants[i].field_count; ++j) { + layout_type(ctx, t->variants[i].fields[j].type); + if (fe_type_align(t->variants[i].fields[j].type) > max_align) + max_align = fe_type_align(t->variants[i].fields[j].type); + /* Where this field sits inside the payload area, which the + code generator needs and nobody was recording. */ + off = round_up(off, fe_type_align(t->variants[i].fields[j].type)); + t->variants[i].fields[j].offset = off; + off += fe_type_size(t->variants[i].fields[j].type); + } + if (off > max_size) max_size = off; + } + if (!members_ready(t)) { t->layout_state = 0; return; } + t->bits = t->variant_count > 256U ? 16U : 8U; + off = round_up(t->bits / 8U, max_align); + t->size = round_up(off + max_size, max_align); + t->align = max_align; + t->layout_state = 2; + } +} + +void fe_type_layout_all(FeTypeCtx *ctx) +{ + FeType *t; + int again = 1; + unsigned rounds = 0; + /* One pass settles a type only if everything under it is already settled, + so a type that had to wait is picked up by the next round. Sixteen is + far past any real nesting; it is here so a cycle cannot spin. */ + while (again && rounds < 16U) { + again = 0; + for (t = ctx->types; t; t = t->next) { + if (t->size || t->layout_state == 2) continue; + layout_type(ctx, t); + if (t->layout_state == 2) again = 1; + } + ++rounds; + } +} + +FeFieldType *fe_type_field(FeType *t, const char *name) +{ + unsigned i; + if (!t || t->kind != FE_TYPE_STRUCT || !name) return 0; + for (i = 0; i < t->field_count; ++i) + if (strcmp(t->fields[i].name, name) == 0) return &t->fields[i]; + return 0; +} + +FeVariantType *fe_type_variant(FeType *t, const char *name) +{ + unsigned i; + if (!t || t->kind != FE_TYPE_ENUM || !name) return 0; + for (i = 0; i < t->variant_count; ++i) + if (strcmp(t->variants[i].name, name) == 0) return &t->variants[i]; + return 0; +} + +FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node) +{ + unsigned long length = 0; + char qualified[128]; + if (!node) return fe_type_intern(ctx, ""); + if (node->kind != FE_N_TYPE) return fe_type_intern(ctx, ""); + if (node->text && strcmp(node->text, "as") == 0) + return fe_type_from_ast(ctx, node->b); + if (node->text && strcmp(node->text, "str") == 0) + return fe_type_slice(ctx, fe_type_intern(ctx, "u8")); + if (node->a && node->a->kind==FE_N_IDENT && node->text && + strcmp(node->text,"io")==0 && node->a->text) { + sprintf(qualified,"%s.%s",node->text,node->a->text); + return fe_type_intern(ctx,qualified); + } + if (node->text && (strcmp(node->text, "&") == 0 || + strcmp(node->text, "&mut") == 0)) + return fe_type_ref(ctx, fe_type_from_ast(ctx,node->a), + strcmp(node->text,"&mut") == 0); + if (node->text && strcmp(node->text,"^")==0) + return fe_type_owned(ctx,fe_type_from_ast(ctx,node->a)); + if (node->text && strcmp(node->text,"?")==0) + return fe_m7_optional_type(ctx,fe_type_from_ast(ctx,node->a)); + if (node->text && (strcmp(node->text, "[") == 0 || + strcmp(node->text, "[]mut") == 0)) { + if (node->a) { + if (node->a->kind == FE_N_LITERAL && node->a->text) + length = strtoul(node->a->text, 0, 0); + return fe_type_array(ctx, length, fe_type_from_ast(ctx, node->b)); + } + return strcmp(node->text,"[]mut")==0 ? + fe_type_mut_slice(ctx, fe_type_from_ast(ctx,node->b)) : + fe_type_slice(ctx, fe_type_from_ast(ctx, node->b)); + } + if (node->text && strcmp(node->text, "!") == 0) { + if (node->b) + return fe_m7_error_union_type(ctx,fe_type_from_ast(ctx,node->a), + fe_type_from_ast(ctx,node->b)); + return fe_type_error_union(ctx,fe_type_from_ast(ctx,node->a)); + } + if (node->text && strcmp(node->text, "*") == 0) + return fe_type_raw(ctx, fe_type_from_ast(ctx, node->a)); + if (node->text && strcmp(node->text, "fn") == 0) + return fe_type_intern(ctx, ""); + /* A plain named type may be a generic declaration -- with arguments it is + an instance, without them it is a mistake -- and only the checker knows + the declarations, so it decides. */ + if (ctx->instantiate) + return ctx->instantiate(ctx->instantiate_owner, node); + return fe_type_intern(ctx, node->text); +} + +int fe_type_equal(const FeType *a, const FeType *b) +{ + if (a == b) return 1; + if (!a || !b) return 0; + if (strcmp(a->name, b->name) != 0) return 0; + /* The same spelling is not the same type across a unit boundary. */ + if (!a->unit || !b->unit) return a->unit == b->unit; + return strcmp(a->unit, b->unit) == 0; +} + +int fe_type_is_integer(const FeType *t) +{ + return t && t->kind == FE_TYPE_INT; +} + +int fe_type_is_indexable(const FeType *t) +{ + return t && (t->kind == FE_TYPE_ARRAY || t->kind == FE_TYPE_SLICE || + t->kind == FE_TYPE_STR); +} + +const char *fe_type_c_name(const FeType *t, unsigned pointer_bits) +{ + if (!t) return "long"; + if (t->kind == FE_TYPE_OPTIONAL && fe_m7_optional_uses_niche(t->elem)) + return fe_type_c_name(t->elem,pointer_bits); + if (t->cname) return t->cname; + if (t->kind == FE_TYPE_VOID) return "void"; + if (t->kind == FE_TYPE_ERROR_UNION) { + if (t->error_value && t->error_value->kind != FE_TYPE_VOID && t->cname) + return t->cname; + return "unsigned short"; + } + if (t->kind == FE_TYPE_BOOL || t->kind == FE_TYPE_CHAR) return "unsigned char"; + if (t->kind == FE_TYPE_REF) { + static char ref_name[128]; + if (t->ref_mut) { + strcpy(ref_name,fe_type_c_name(t->elem,pointer_bits)); + strcat(ref_name," *"); + } else { + strcpy(ref_name,"const "); + strcat(ref_name,fe_type_c_name(t->elem,pointer_bits)); + strcat(ref_name," *"); + } + return ref_name; + } + if (t->kind == FE_TYPE_OWNED) { + static char owned_name[128]; + strcpy(owned_name,fe_type_c_name(t->elem,pointer_bits)); + strcat(owned_name," *"); + return owned_name; + } + if (t->kind != FE_TYPE_INT) return "long"; + if (strcmp(t->name, "usize") == 0) return "unsigned long"; + if (strcmp(t->name, "isize") == 0) return "long"; + if (strcmp(t->name, "i8") == 0) return "signed char"; + if (strcmp(t->name, "u8") == 0) return "unsigned char"; + if (strcmp(t->name, "i16") == 0) return "short"; + if (strcmp(t->name, "u16") == 0) return "unsigned short"; + if (strcmp(t->name, "i32") == 0) return "long"; + if (strcmp(t->name, "u32") == 0) return "unsigned long"; + return "long"; +} diff --git a/fec/src/types.h b/fec/src/types.h new file mode 100644 index 0000000..c15d9a3 --- /dev/null +++ b/fec/src/types.h @@ -0,0 +1,166 @@ +#ifndef FE_TYPES_H +#define FE_TYPES_H + +#include "ast.h" + +typedef enum FeTypeKind { + FE_TYPE_ERROR, FE_TYPE_ERROR_UNION, FE_TYPE_OPTIONAL, + FE_TYPE_VOID, FE_TYPE_BOOL, FE_TYPE_CHAR, FE_TYPE_INT, + FE_TYPE_STRUCT, FE_TYPE_ENUM, FE_TYPE_ARRAY, FE_TYPE_SLICE, FE_TYPE_STR, + FE_TYPE_REF, FE_TYPE_OWNED, + /* `*T`. A machine address and nothing else: no borrow to track, no drop + to run, Copy. Everything it is good for is behind `unsafe`. */ + FE_TYPE_RAW, + FE_TYPE_UNKNOWN +} FeTypeKind; + +/* One target, one pointer width (SPEC 2). usize and isize are that width and + are not promised to be any particular number of bits, which is what keeps a + different width possible later. */ +#define FE_PTR_SIZE 4UL +#define FE_PTR_ALIGN 4U +#define FE_PTR_BITS 32U + +typedef struct FeFieldType FeFieldType; + +/* A type parameter bound to an argument while an instance is checked. */ +#define FE_TYPE_PARAM_MAX 8 +typedef struct FeTypeBind { + const char *name; + FeType *type; +} FeTypeBind; + +typedef struct FeVariantType FeVariantType; + +struct FeFieldType { + char *name; + FeType *type; + unsigned long offset; + const FeNode *ast_node; +}; + +struct FeVariantType { + char *name; + FeFieldType *fields; + unsigned field_count; + unsigned tag; + const FeNode *ast_node; + char *maker; +}; + +struct FeType { + FeTypeKind kind; + /* Long enough for a nested instance spelling such as + `Box(Box(Box(i32)))` at the depth limit. */ + char name[256]; + /* The unit that declared this type, for the nominal kinds. NULL for + builtins and for structural types like `[]u8`, which every unit + shares. Two units declaring the same name declare two types. */ + const char *unit; + char *cname; + char *maker; + char *none_cname; + char *unwrap_cname; + char *indexer; + char *slicer; + char *full_slicer; + char *tail_slicer; + char *drop_cname; + char *alloc_cname; + char *replace_cname; + unsigned bits; + int is_unsigned; + int packed; + int is_error; + int has_drop; + unsigned long length; + unsigned long size; + unsigned align; + /* Element/payload type for refs, owners, slices and optionals. For an + error union this is the nominal error identity; NULL means core.Error. */ + FeType *elem; + /* Success value for an error union. */ + FeType *error_value; + int ref_mut; + FeFieldType *fields; + unsigned field_count; + FeVariantType *variants; + unsigned variant_count; + /* The declaration this type came from, and the bindings that made it if + it is a generic instance. A method has to be checked with the same + bindings the instance was built with. */ + /* A small unique number, used to name an instance whose readable + spelling would be too long to keep distinct. */ + unsigned serial; + const FeNode *decl_node; + FeTypeBind binds[FE_TYPE_PARAM_MAX]; + unsigned bind_count; + FeType *next; + int emit_state; + int cycle_state; + /* Separate from `cycle_state`: the checker's by-value recursion walk and + this layer's size computation run inside one another, and sharing one + marker made a struct in the middle of the first look complete to the + second -- one byte wide, with every field on top of the next. */ + int layout_state; + /* Set while a generic instance is being filled in. Its field array + exists but says nothing yet, and a size taken from it would be + wrong and would then be frozen. */ + int building; +}; + +typedef struct FeTypeCtx { + FeArena *arena; + FeType *types; + unsigned pointer_bits; + const char *unit_name; + unsigned generated_serial; + /* Bindings in force right now. A name that is a bound parameter is + that argument's type and nothing else. */ + FeTypeBind params[FE_TYPE_PARAM_MAX]; + unsigned param_count; + /* Instantiate `Name(args...)`. Only the checker knows the declarations, + so it installs this and the type layer calls back into it. */ + FeType *(*instantiate)(void *owner, const FeNode *node); + void *instantiate_owner; + /* A field type is written in the unit that declared it, so resolving one + has to happen with that unit's imports in scope. The checker owns that + knowledge, so it installs this pair and the type layer calls back. + `enter` answers with what to hand `leave`, or -1 for "stayed put". */ + int (*enter_decl)(void *owner, const char *unit); + void (*leave_decl)(void *owner, int back); +} FeTypeCtx; + +void fe_types_init(FeTypeCtx *ctx, FeArena *arena, unsigned pointer_bits); +FeType *fe_type_intern(FeTypeCtx *ctx, const char *name); +/* Intern a nominal type belonging to `unit` rather than to whichever unit + is being checked. Used to name a type across a unit boundary. */ +FeType *fe_type_intern_unit(FeTypeCtx *ctx, const char *unit, + const char *name); +FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node); +FeType *fe_type_array(FeTypeCtx *ctx, unsigned long length, FeType *elem); +FeType *fe_type_slice(FeTypeCtx *ctx, FeType *elem); +FeType *fe_type_mut_slice(FeTypeCtx *ctx, FeType *elem); +FeType *fe_type_ref(FeTypeCtx *ctx, FeType *elem, int mutable); +FeType *fe_type_owned(FeTypeCtx *ctx, FeType *elem); +FeType *fe_type_raw(FeTypeCtx *ctx, FeType *elem); +FeType *fe_type_error_union(FeTypeCtx *ctx, FeType *value); +void fe_type_require_replace(FeTypeCtx *ctx, FeType *type); +FeType *fe_type_declare_struct(FeTypeCtx *ctx, const FeNode *node, int packed); +FeType *fe_type_declare_enum(FeTypeCtx *ctx, const FeNode *node); +FeType *fe_type_declare_error(FeTypeCtx *ctx, const FeNode *node); +void fe_type_layout_all(FeTypeCtx *ctx); +FeFieldType *fe_type_field(FeType *t, const char *name); +FeVariantType *fe_type_variant(FeType *t, const char *name); +int fe_type_equal(const FeType *a, const FeType *b); +int fe_type_is_integer(const FeType *t); +int fe_type_is_indexable(const FeType *t); +const char *fe_type_c_name(const FeType *t, unsigned pointer_bits); +unsigned long fe_type_size(const FeType *t); +/* Where the payload of an optional or an error union sits. The tag comes + first and the value is aligned after it; both the layout pass and the + code generator have to agree, so the rule lives in one place. */ +unsigned long fe_type_payload_offset(const FeType *t); +unsigned fe_type_align(const FeType *t); + +#endif diff --git a/fec/src/x86.c b/fec/src/x86.c new file mode 100644 index 0000000..69315b7 --- /dev/null +++ b/fec/src/x86.c @@ -0,0 +1,677 @@ +#include "x86.h" +#include +#include + +/* ------------------------------------------------------------------------- * + * i386 code generation + * + * The frame, from EBP downwards: + * + * [ebp + 8 + 4k] incoming argument k + * [ebp + 4] return address + * [ebp] saved ebp + * [ebp - ...] parameters, copied in from the argument area + * [ebp - ...] locals + * [ebp - ...] one slot per temporary + * + * Parameters are copied into the frame rather than read in place so that a + * parameter and a local are the same thing to everything below. + * ------------------------------------------------------------------------- */ + +/* Which register a temporary lives in, or none. Only ebx, esi and edi are + handed out: eax, ecx and edx are the scratch this emitter computes in, and + the three that are left survive a call without being saved. */ +#define REG_NONE 0 +#define REG_COUNT 3 +static const char *const REGS[REG_COUNT] = { "ebx", "esi", "edi" }; + +typedef struct Frame { + const FeIrFunc *f; + long *local_off; /* [ebp + off] for each local */ + long temp_base; /* first temporary slot */ + long size; /* bytes to subtract from esp */ + /* 0 means the temporary lives in its stack slot. */ + unsigned char *temp_reg; +} Frame; + +static long align_up(long v, long a) +{ + long r = v % a; + return r ? v + a - r : v; +} + +static unsigned long slot_bytes(const FeIrLocal *l) +{ + switch (l->type) { + case FE_IR_I8: return 1; + case FE_IR_I16: return 2; + case FE_IR_I32: return 4; + case FE_IR_PTR: return 4; + case FE_IR_MEM: return l->size ? l->size : 1; + default: return 4; + } +} + +/* Every temporary is four bytes: a temporary only ever holds something that + fits in a register, and narrower values are kept zero- or sign-extended. */ +#define TEMP_SLOT 4L + +static void frame_layout(Frame *fr, const FeIrFunc *f, long *storage) +{ + unsigned i; + long off = 0; + fr->f = f; + fr->local_off = storage; + for (i = 0; i < f->local_count; ++i) { + unsigned long size = slot_bytes(&f->locals[i]); + long a = (long)f->locals[i].align; + if (a < 1) a = 1; + if (a > 4) a = 4; + off = align_up(off + (long)size, a); + storage[i] = -off; + } + off = align_up(off, 4); + fr->temp_base = -off; + off += (long)f->temp_count * TEMP_SLOT; + fr->size = align_up(off, 4); +} + +/* Which temporaries a value reads. Returns how many it wrote into `used`. */ +static unsigned reads_of(const FeIrValue *v, unsigned *used) +{ + unsigned n = 0; + unsigned i; + switch (v->op) { + case FE_IR_CONST: break; + case FE_IR_LOAD: + case FE_IR_ADDR: + if (v->place.base == FE_PLACE_TEMP) used[n++] = v->place.index; + break; + case FE_IR_STORE: + if (v->place.base == FE_PLACE_TEMP) used[n++] = v->place.index; + used[n++] = v->a; + break; + case FE_IR_COPY: + if (v->place.base == FE_PLACE_TEMP) used[n++] = v->place.index; + if (v->place2.base == FE_PLACE_TEMP) used[n++] = v->place2.index; + break; + case FE_IR_CAST: + used[n++] = v->a; + break; + case FE_IR_CALL: + for (i = 0; i < v->arg_count && n < 18; ++i) used[n++] = v->args[i]; + break; + default: + used[n++] = v->a; + used[n++] = v->b; + break; + } + return n; +} + +/* Give registers to the temporaries that can hold one. + + A temporary that is defined in one block and read in another has to go + through memory: this walks one block at a time and knows nothing about the + others. Lowering does produce such temporaries -- a bounds check splits a + block between computing an index and using it -- so eligibility is decided + over the whole function first, and the scan inside a block only considers + what survived that. */ +static void allocate_registers(Frame *fr, const FeIrFunc *f) +{ + unsigned n = f->temp_count; + unsigned char *single; /* 1 while the temporary stays in one block */ + unsigned *home; /* the block it was defined in */ + unsigned *last; /* the last instruction in that block to read it */ + const FeIrBlock *b; + const FeIrValue *v; + unsigned used[20]; + unsigned i, k, at; + if (!n) { fr->temp_reg = 0; return; } + fr->temp_reg = (unsigned char *)calloc(n, 1); + single = (unsigned char *)calloc(n, 1); + home = (unsigned *)calloc(n, sizeof(unsigned)); + last = (unsigned *)calloc(n, sizeof(unsigned)); + if (!fr->temp_reg || !single || !home || !last) { + free(single); free(home); free(last); + return; + } + for (i = 0; i < n; ++i) { single[i] = 1; home[i] = 0xFFFFFFFFU; } + for (b = f->first; b; b = b->next) { + for (v = b->first; v; v = v->next) { + if (v->has_dest) { + if (home[v->dest] != 0xFFFFFFFFU) single[v->dest] = 0; + home[v->dest] = b->id; + } + k = reads_of(v, used); + for (i = 0; i < k; ++i) + if (used[i] < n && home[used[i]] != b->id) single[used[i]] = 0; + } + if (b->term == FE_IR_BR && b->cond < n && home[b->cond] != b->id) + single[b->cond] = 0; + if (b->term == FE_IR_RET && b->has_ret_value && b->ret_value < n && + home[b->ret_value] != b->id) + single[b->ret_value] = 0; + } + + for (b = f->first; b; b = b->next) { + unsigned char busy[REG_COUNT]; + unsigned owner[REG_COUNT]; + for (i = 0; i < REG_COUNT; ++i) { busy[i] = 0; owner[i] = 0; } + /* When each temporary is last read in this block. */ + at = 0; + for (v = b->first; v; v = v->next, ++at) { + k = reads_of(v, used); + for (i = 0; i < k; ++i) + if (used[i] < n && single[used[i]]) last[used[i]] = at; + } + if (b->term == FE_IR_BR && b->cond < n && single[b->cond]) + last[b->cond] = at; + if (b->term == FE_IR_RET && b->has_ret_value && b->ret_value < n && + single[b->ret_value]) last[b->ret_value] = at; + + at = 0; + for (v = b->first; v; v = v->next, ++at) { + /* Free whatever was read for the last time before this. */ + for (i = 0; i < REG_COUNT; ++i) + if (busy[i] && last[owner[i]] < at) busy[i] = 0; + if (!v->has_dest || !single[v->dest]) continue; + /* A call clobbers the scratch registers but not these three, so a + result can still be kept in one across the call that made it. */ + for (i = 0; i < REG_COUNT; ++i) + if (!busy[i]) { + busy[i] = 1; + owner[i] = v->dest; + fr->temp_reg[v->dest] = (unsigned char)(i + 1); + break; + } + } + } + free(single); free(home); free(last); +} + +static long temp_off(const Frame *fr, unsigned t) +{ + return fr->temp_base - (long)(t + 1) * TEMP_SLOT; +} + +static const char *word_of(FeIrType t) +{ + switch (t) { + case FE_IR_I8: return "byte ptr"; + case FE_IR_I16: return "word ptr"; + default: return "dword ptr"; + } +} + +static const char *reg_of(FeIrType t, int which) +{ + /* which: 0 -> a, 1 -> c, 2 -> d */ + switch (t) { + case FE_IR_I8: return which == 0 ? "al" : which == 1 ? "cl" : "dl"; + case FE_IR_I16: return which == 0 ? "ax" : which == 1 ? "cx" : "dx"; + default: return which == 0 ? "eax" : which == 1 ? "ecx" : "edx"; + } +} + +/* Write the effective address of a place into `buf`. A place is a base plus a + constant, and the only base that is not already an address is a temporary, + which holds a pointer. */ +static void place_addr(const Frame *fr, const FeIrPlace *p, char *buf) +{ + switch (p->base) { + case FE_PLACE_LOCAL: + sprintf(buf, "[ebp%+ld]", fr->local_off[p->index] + p->offset); + break; + case FE_PLACE_GLOBAL: + if (p->offset) sprintf(buf, "[%s%+ld]", p->name, p->offset); + else sprintf(buf, "[%s]", p->name); + break; + case FE_PLACE_TEMP: + sprintf(buf, "[edx%+ld]", p->offset); + break; + } +} + +/* A temporary-based place needs its pointer in a register first. */ +static void load_temp(const Frame *fr, unsigned t, const char *reg, + FILE *out); + +static void load_place_base(const Frame *fr, const FeIrPlace *p, FILE *out) +{ + if (p->base != FE_PLACE_TEMP) return; + /* Through load_temp, not straight from the slot: the pointer may be living + in a register, in which case the slot was never written. */ + load_temp(fr, p->index, "edx", out); +} + +static void load_temp(const Frame *fr, unsigned t, const char *reg, FILE *out) +{ + if (fr->temp_reg && fr->temp_reg[t]) { + const char *from = REGS[fr->temp_reg[t] - 1]; + if (strcmp(from, reg) != 0) + fprintf(out, " mov %s, %s\n", reg, from); + return; + } + fprintf(out, " mov %s, [ebp%+ld]\n", reg, temp_off(fr, t)); +} + +static void store_temp(const Frame *fr, unsigned t, const char *reg, FILE *out) +{ + if (fr->temp_reg && fr->temp_reg[t]) { + const char *to = REGS[fr->temp_reg[t] - 1]; + if (strcmp(to, reg) != 0) + fprintf(out, " mov %s, %s\n", to, reg); + return; + } + fprintf(out, " mov [ebp%+ld], %s\n", temp_off(fr, t), reg); +} + +/* The register a temporary lives in, or null when it lives in its slot. */ +static const char *reg_home(const Frame *fr, unsigned t) +{ + if (!fr->temp_reg || !fr->temp_reg[t]) return 0; + return REGS[fr->temp_reg[t] - 1]; +} + +/* Something an instruction can take as its right-hand operand: a register, or + the temporary's slot read in place. */ +static void operand_of(const Frame *fr, unsigned t, char *buf) +{ + const char *r = reg_home(fr, t); + if (r) strcpy(buf, r); + else sprintf(buf, "dword ptr [ebp%+ld]", temp_off(fr, t)); +} + +static const char *simple_op(FeIrOp op) +{ + switch (op) { + case FE_IR_ADD: return "add "; + case FE_IR_SUB: return "sub "; + case FE_IR_AND: return "and "; + case FE_IR_OR: return "or "; + case FE_IR_XOR: return "xor "; + case FE_IR_MUL: return "imul"; + default: return 0; + } +} + +static const char *cmp_set(FeIrOp op, int is_unsigned) +{ + switch (op) { + case FE_IR_EQ: return "sete"; + case FE_IR_NE: return "setne"; + case FE_IR_LT: return is_unsigned ? "setb" : "setl"; + case FE_IR_LE: return is_unsigned ? "setbe" : "setle"; + case FE_IR_GT: return is_unsigned ? "seta" : "setg"; + case FE_IR_GE: return is_unsigned ? "setae" : "setge"; + default: return "sete"; + } +} + +static void emit_binary(const Frame *fr, const FeIrValue *v, FILE *out) +{ + int is_cmp = v->op >= FE_IR_EQ && v->op <= FE_IR_GE; + FeIrType t = is_cmp ? (FeIrType)v->imm : v->type; + const char *a = reg_of(t, 0); + const char *c = reg_of(t, 1); + /* When the result has a register of its own and the operation is one that + can work on any register, the whole thing happens there: no trip through + the scratch register and no trip through memory. + + Only the full-width operations qualify. esi and edi have no byte halves, + so a narrow operation still goes through eax, where they do. */ + if (!is_cmp && v->has_dest && (t == FE_IR_I32 || t == FE_IR_PTR) && + simple_op(v->op)) { + const char *d = reg_home(fr, v->dest); + const char *rb = reg_home(fr, v->b); + if (d && !(rb && strcmp(rb, d) == 0)) { + char right[64]; + load_temp(fr, v->a, d, out); + operand_of(fr, v->b, right); + fprintf(out, " %s %s, %s\n", simple_op(v->op), d, right); + return; + } + } + /* A full-width comparison can read both sides where they already are; the + answer still has to come out of `al`, which is why it lands in eax when + the result has no register of its own. */ + if (is_cmp && (t == FE_IR_I32 || t == FE_IR_PTR)) { + const char *left = reg_home(fr, v->a); + const char *d = reg_home(fr, v->dest); + char right[64]; + if (!left) { load_temp(fr, v->a, "eax", out); left = "eax"; } + operand_of(fr, v->b, right); + fprintf(out, " cmp %s, %s\n", left, right); + fprintf(out, " %s al\n", cmp_set(v->op, v->is_unsigned)); + fprintf(out, " movzx %s, al\n", d ? d : "eax"); + if (!d) store_temp(fr, v->dest, "eax", out); + return; + } + load_temp(fr, v->a, "eax", out); + load_temp(fr, v->b, "ecx", out); + if (is_cmp) { + fprintf(out, " cmp %s, %s\n", a, c); + fprintf(out, " %s al\n", cmp_set(v->op, v->is_unsigned)); + fprintf(out, " movzx eax, al\n"); + store_temp(fr, v->dest, "eax", out); + return; + } + switch (v->op) { + case FE_IR_ADD: fprintf(out, " add %s, %s\n", a, c); break; + case FE_IR_SUB: fprintf(out, " sub %s, %s\n", a, c); break; + case FE_IR_MUL: fprintf(out, " imul %s, %s\n", a, c); break; + case FE_IR_AND: fprintf(out, " and %s, %s\n", a, c); break; + case FE_IR_OR: fprintf(out, " or %s, %s\n", a, c); break; + case FE_IR_XOR: fprintf(out, " xor %s, %s\n", a, c); break; + case FE_IR_SHL: fprintf(out, " shl %s, cl\n", a); break; + case FE_IR_SHR: + fprintf(out, " %s %s, cl\n", + v->is_unsigned ? "shr" : "sar", a); + break; + case FE_IR_DIV: + case FE_IR_MOD: + /* The divide instructions use edx:eax, so the operands have to be + widened to 32 bits whatever the declared width is. */ + if (v->is_unsigned) fprintf(out, " xor edx, edx\n"); + else fprintf(out, " cdq\n"); + fprintf(out, " %s ecx\n", v->is_unsigned ? "div " : "idiv"); + if (v->op == FE_IR_MOD) fprintf(out, " mov eax, edx\n"); + break; + default: break; + } + store_temp(fr, v->dest, "eax", out); +} + +static void emit_value(const Frame *fr, const FeIrValue *v, FILE *out) +{ + char addr[128]; + unsigned i; + switch (v->op) { + case FE_IR_CONST: { + const char *d = reg_home(fr, v->dest); + fprintf(out, " mov %s, %ld\n", d ? d : "eax", v->imm); + if (!d) store_temp(fr, v->dest, "eax", out); + break; + } + case FE_IR_LOAD: { + const char *d = reg_home(fr, v->dest); + const char *into = d ? d : "eax"; + load_place_base(fr, &v->place, out); + place_addr(fr, &v->place, addr); + if (v->type == FE_IR_I8) + fprintf(out, " movzx %s, byte ptr %s\n", into, addr); + else if (v->type == FE_IR_I16) + fprintf(out, " movzx %s, word ptr %s\n", into, addr); + else + fprintf(out, " mov %s, dword ptr %s\n", into, addr); + if (!d) store_temp(fr, v->dest, "eax", out); + break; + } + case FE_IR_STORE: { + const char *from = reg_home(fr, v->a); + load_place_base(fr, &v->place, out); + place_addr(fr, &v->place, addr); + /* A full-width value already in a register goes straight out; a narrow + one needs a byte or word half, which only eax has here. */ + if (from && (v->type == FE_IR_I32 || v->type == FE_IR_PTR)) { + fprintf(out, " mov %s %s, %s\n", word_of(v->type), addr, + from); + break; + } + load_temp(fr, v->a, "eax", out); + fprintf(out, " mov %s %s, %s\n", word_of(v->type), addr, + reg_of(v->type, 0)); + break; + } + case FE_IR_ADDR: { + const char *d = reg_home(fr, v->dest); + load_place_base(fr, &v->place, out); + place_addr(fr, &v->place, addr); + fprintf(out, " lea %s, %s\n", d ? d : "eax", addr); + if (!d) store_temp(fr, v->dest, "eax", out); + break; + } + case FE_IR_CAST: + load_temp(fr, v->a, "eax", out); + /* Narrowing is free once everything is kept in a 32-bit slot; widening + has to say whether the top bits are copies of the sign. */ + if (v->type == FE_IR_I8) + fprintf(out, " %s eax, al\n", + v->is_unsigned ? "movzx" : "movsx"); + else if (v->type == FE_IR_I16) + fprintf(out, " %s eax, ax\n", + v->is_unsigned ? "movzx" : "movsx"); + store_temp(fr, v->dest, "eax", out); + break; + case FE_IR_CALL: + /* cdecl: arguments pushed right to left, the caller pops them. */ + for (i = v->arg_count; i > 0; --i) { + load_temp(fr, v->args[i - 1], "eax", out); + fprintf(out, " push eax\n"); + } + fprintf(out, " call %s\n", v->callee); + if (v->arg_count) + fprintf(out, " add esp, %u\n", v->arg_count * 4U); + if (v->has_dest) store_temp(fr, v->dest, "eax", out); + break; + case FE_IR_COPY: { + char dst[128]; + char src[128]; + /* Both addresses are worked out in the scratch registers first, and + only then does the block copy take over esi and edi -- which may be + holding temporaries, so it hands them back. */ + if (v->place2.base == FE_PLACE_TEMP) { + load_temp(fr, v->place2.index, "eax", out); + if (v->place2.offset) + fprintf(out, " add eax, %ld\n", v->place2.offset); + } else { + place_addr(fr, &v->place2, src); + fprintf(out, " lea eax, %s\n", src); + } + if (v->place.base == FE_PLACE_TEMP) { + load_temp(fr, v->place.index, "edx", out); + if (v->place.offset) + fprintf(out, " add edx, %ld\n", v->place.offset); + } else { + place_addr(fr, &v->place, dst); + fprintf(out, " lea edx, %s\n", dst); + } + fprintf(out, " push esi\n"); + fprintf(out, " push edi\n"); + fprintf(out, " mov esi, eax\n"); + fprintf(out, " mov edi, edx\n"); + fprintf(out, " mov ecx, %ld\n", v->imm); + fprintf(out, " cld\n"); + fprintf(out, " rep movsb\n"); + fprintf(out, " pop edi\n pop esi\n"); + break; + } + default: + emit_binary(fr, v, out); + break; + } +} + +static void emit_func(const FeIrModule *m, const FeIrFunc *f, FILE *out) +{ + Frame fr; + long *storage; + const FeIrBlock *b; + const FeIrValue *v; + unsigned i; + long arg = 8; + if (f->is_extern || !f->first) return; + /* One offset per local, however many there are. A fixed array here would + silently stop emitting a function that had too many. */ + storage = (long *)malloc((size_t)(f->local_count ? f->local_count : 1) * + sizeof(long)); + if (!storage) return; + frame_layout(&fr, f, storage); + allocate_registers(&fr, f); + + fprintf(out, "\npublic %s\n", f->name); + fprintf(out, "%s proc near\n", f->name); + fprintf(out, " push ebp\n"); + fprintf(out, " mov ebp, esp\n"); + if (fr.size) fprintf(out, " sub esp, %ld\n", fr.size); + fprintf(out, " push ebx\n push esi\n" + " push edi\n"); + /* Copy the incoming arguments into the frame. */ + for (i = 0; i < f->param_count; ++i) { + fprintf(out, " mov eax, [ebp+%ld]\n", arg); + fprintf(out, " mov %s [ebp%+ld], %s\n", + word_of(f->locals[i].type), storage[i], + reg_of(f->locals[i].type, 0)); + arg += 4; + } + + for (b = f->first; b; b = b->next) { + fprintf(out, "L%s_%u:\n", f->name, b->id); + for (v = b->first; v; v = v->next) emit_value(&fr, v, out); + switch (b->term) { + case FE_IR_JMP: + fprintf(out, " jmp L%s_%u\n", f->name, b->target); + break; + case FE_IR_BR: + load_temp(&fr, b->cond, "eax", out); + fprintf(out, " test eax, eax\n"); + fprintf(out, " jnz L%s_%u\n", f->name, b->target); + fprintf(out, " jmp L%s_%u\n", f->name, b->target_else); + break; + case FE_IR_RET: + if (b->has_ret_value) load_temp(&fr, b->ret_value, "eax", out); + fprintf(out, " pop edi\n pop esi\n" + " pop ebx\n"); + fprintf(out, " mov esp, ebp\n pop ebp\n"); + fprintf(out, " ret\n"); + break; + case FE_IR_TRAP: + fprintf(out, " push %lu\n", b->trap_line); + fprintf(out, " push offset FE_FILE_%u\n", + b->trap_file); + fprintf(out, " push %u\n", (unsigned)b->trap); + fprintf(out, " call fe_trap\n"); + fprintf(out, " add esp, 12\n"); + break; + } + } + fprintf(out, "%s endp\n", f->name); + free(storage); + free(fr.temp_reg); + (void)m; +} + +static void emit_string(const char *s, FILE *out) +{ + int in = 0; + fputs(" db ", out); + for (; s && *s; ++s) { + unsigned char c = (unsigned char)*s; + if (c >= 32 && c < 127 && c != '\'' && c != '"') { + if (!in) { fputc('\'', out); in = 1; } + fputc(c, out); + } else { + if (in) { fputs("',", out); in = 0; } + fprintf(out, "%u,", c); + } + } + if (in) fputc('\'', out); + else fputc('0', out); + if (in) fputs(",0", out); + fputc('\n', out); +} + +void fe_x86_emit(const FeIrModule *m, FILE *out) +{ + const FeIrFunc *f; + const FeIrGlobal *g; + int any_trap = 0; + const FeIrBlock *b; + unsigned i; + + for (f = m->funcs; f && !any_trap; f = f->next) + for (b = f->first; b; b = b->next) + if (b->term == FE_IR_TRAP) { any_trap = 1; break; } + + fputs(".386\n.model flat\n\n", out); + for (f = m->funcs; f; f = f->next) + if (f->is_extern || !f->first) + fprintf(out, "extern %s : near\n", f->name); + /* Anything called but not defined here lives somewhere else -- the runtime, + or a library. Lowering emits such calls directly (allocating, writing, + trapping), so the names are collected from the calls themselves rather + than from a list that would have to be kept in step. */ + { + const char *seen[64]; + unsigned count = 0; + const FeIrValue *v; + const FeIrFunc *g; + unsigned i; + for (f = m->funcs; f; f = f->next) + for (b = f->first; b; b = b->next) + for (v = b->first; v; v = v->next) { + if (v->op != FE_IR_CALL || !v->callee) continue; + for (g = m->funcs; g; g = g->next) + if (!strcmp(g->name, v->callee)) break; + if (g) continue; + for (i = 0; i < count; ++i) + if (!strcmp(seen[i], v->callee)) break; + if (i < count || count >= 64) continue; + seen[count++] = v->callee; + fprintf(out, "extern %s : near\n", v->callee); + } + } + if (any_trap) fputs("extern fe_trap : near\n", out); + + fputs("\n_DATA segment dword public 'DATA'\n", out); + /* One name per file a trap can come from. A build is many units in + one module, and a trap that names the wrong file is worse than + one that names none. */ + for (i = 0; i < m->file_count; ++i) { + fprintf(out, "public FE_FILE_%u\nFE_FILE_%u label byte\n", i, i); + emit_string(m->files[i], out); + } + for (g = m->globals; g; g = g->next) { + unsigned long i; + fprintf(out, "public %s\n%s label byte\n", g->name, g->name); + if (!g->init) { + fprintf(out, " db %lu dup(0)\n", g->size ? g->size : 1UL); + continue; + } + for (i = 0; i < g->size; ) { + unsigned r; + unsigned long j; + for (r = 0; r < g->reloc_count; ++r) + if (g->relocs[r].at == i) break; + if (r < g->reloc_count) { + /* A hole the linker fills with an address. */ + fprintf(out, " dd offset %s\n", + g->relocs[r].symbol); + i += 4; + continue; + } + fputs(" db ", out); + j = 0; + while (i < g->size && j < 16) { + unsigned q; + for (q = 0; q < g->reloc_count; ++q) + if (g->relocs[q].at == i) break; + if (q < g->reloc_count) break; + fprintf(out, "%s%u", j ? "," : "", g->init[i]); + ++i; ++j; + } + fputc('\n', out); + } + if (!g->size) fputs(" db 0\n", out); + } + fputs("_DATA ends\n", out); + + fputs("\n_TEXT segment dword public 'CODE'\n", out); + for (f = m->funcs; f; f = f->next) emit_func(m, f, out); + /* The runtime's entry stub calls one fixed name, so point it here. */ + if (m->entry_main) + fprintf(out, "\npublic fe_main_\nfe_main_ proc near\n" + " jmp %s\nfe_main_ endp\n", m->entry_main); + fputs("\n_TEXT ends\n\nend\n", out); +} diff --git a/fec/src/x86.h b/fec/src/x86.h new file mode 100644 index 0000000..9f3229e --- /dev/null +++ b/fec/src/x86.h @@ -0,0 +1,16 @@ +#ifndef FE_X86_H +#define FE_X86_H + +#include "ir.h" + +/* IR to i386 assembly, in the syntax Open Watcom's `wasm` accepts. + + There is no register allocator. Every temporary gets a stack slot, and every + instruction loads its operands into fixed registers, computes, and stores + the result back. That is slow code and obviously correct code, and correct + comes first: a register allocator can be dropped in later without the rest + of the compiler noticing, because it only changes where a temporary lives. */ + +void fe_x86_emit(const FeIrModule *m, FILE *out); + +#endif diff --git a/fec/std/arena.fe b/fec/std/arena.fe new file mode 100644 index 0000000..8fa5117 --- /dev/null +++ b/fec/std/arena.fe @@ -0,0 +1,166 @@ +unit std.arena; + +import std.mem; + +// An arena of `T` reached by handle, and the handle that reaches it. +// +// This is the shape SPEC R11 asks for when data points at data: the arena owns +// the values and a handle is a number, so nothing here is a reference and R4 +// has nothing to object to. `mem.Arena` next door is a different structure -- +// a block of bytes handed out by offset, for data whose size varies. This one +// holds a fixed `T` per slot and can take slots back. +// +// A handle is checked, not trusted. It carries the generation of the slot it +// was made from, the epoch of the arena, and which arena it came from, so +// using it after the slot was freed, after the arena was reset, or against +// some other arena all answer `null` rather than a neighbouring value. +// +// `T` is expected to be Copy -- integers, handles, small records. `get` +// answers with a copy, which is the whole reason a borrow never outlives the +// statement it was taken in. For a `T` that owns something, `take` moves it +// out and leaves a replacement (SPEC 5 R7). + +/// No slot. Every real index is smaller. +pub const NONE: u32 = 4294967295; + +/// Eight bytes, and the same eight in every build: `--no-checks` skips the +/// comparison, never the layout. A handle that changed shape with a flag +/// would not survive being written down. +pub struct Handle(T) { + pub index: u32, + /// `arena 8 | epoch 8 | generation 16` + pub tag: u32, + + pub fn none() -> Self { return Self{ index: NONE, tag: 0 }; } + + pub fn is_none(self: &Self) -> bool { return self.index == NONE; } + + pub fn same(self: &Self, other: Handle(T)) -> bool { + return self.index == other.index and self.tag == other.tag; + } +} + +struct Slot(T) { + value: T, + /// Bumped every time the slot is freed, so old handles stop matching. + gen: u32, + live: bool, + /// The next slot on the free list, or `NONE`. + next: u32, +} + +pub struct Arena(T) { + slots: ^[]mut Slot(T), + /// How many slots have ever been handed out; slots past this are untouched. + high: usize, + /// How many are live right now. + count: usize, + free: u32, + id: u32, + epoch: u32, + + /// `id` tells one arena from another in a handle. A program with a handful + /// of arenas numbers them itself; eight bits is more than that needs. + pub fn with_capacity(id: u32, n: usize) -> !Self { + let room: ^[]mut Slot(T) = try mem.alloc_slice(Slot(T), n); + return Self{ slots: room, high: 0, count: 0, free: NONE, + id: id % 256, epoch: 0 }; + } + + pub fn len(self: &Self) -> usize { return self.count; } + + pub fn room(self: &Self) -> usize { return self.slots.^.n; } + + fn tag_of(self: &Self, gen: u32) -> u32 { + return (self.id * 16777216) + (self.epoch * 65536) + gen; + } + + /// Is this handle still talking about a live slot in this arena? + pub fn valid(self: &Self, h: Handle(T)) -> bool { + if h.index == NONE { return false; } + if (h.index as usize) >= self.high { return false; } + if not self.slots.^[h.index as usize].live { return false; } + return self.slots.^[h.index as usize].gen == h.tag; + } + + pub fn alloc(self: &mut Self, v: T) -> !Handle(T) { + var at: usize = 0; + if self.free != NONE { + at = self.free as usize; + self.free = self.slots.^[at].next; + } else { + if self.high == self.slots.^.n { return error.OutOfMemory; } + at = self.high; + self.high = self.high + 1; + self.slots.^[at].gen = self.tag_of(0); + } + self.slots.^[at].value = v; + self.slots.^[at].live = true; + self.slots.^[at].next = NONE; + self.count = self.count + 1; + return Handle(T){ index: at as u32, tag: self.slots.^[at].gen }; + } + + /// A copy of what the slot holds, or nothing when the handle is stale. + /// The borrow of the arena ends with this statement, which is what lets a + /// caller read one slot while writing another. + pub fn get(self: &Self, h: Handle(T)) -> ?T { + if not self.valid(h) { return null; } + return self.slots.^[h.index as usize].value; + } + + /// Overwrite in place. Says whether the handle was good. + pub fn set(self: &mut Self, h: Handle(T), v: T) -> bool { + if not self.valid(h) { return false; } + self.slots.^[h.index as usize].value = v; + return true; + } + + /// Move the value out and leave `replacement` behind (SPEC 5 R7). This is + /// how a `T` that owns something leaves the arena. + pub fn take(self: &mut Self, h: Handle(T), replacement: T) -> ?T { + if not self.valid(h) { return null; } + return mem.replace(&mut self.slots.^[h.index as usize].value, + replacement); + } + + /// Exchange what two slots hold. Both handles have to be good. + pub fn swap(self: &mut Self, a: Handle(T), b: Handle(T)) -> bool { + if not self.valid(a) { return false; } + if not self.valid(b) { return false; } + let ai: usize = a.index as usize; + let bi: usize = b.index as usize; + if ai == bi { return true; } + let first: T = self.slots.^[ai].value; + let second: T = mem.replace(&mut self.slots.^[bi].value, first); + self.slots.^[ai].value = second; + return true; + } + + /// Give the slot back. Every handle to it stops matching. A slot whose + /// generation has run out is retired rather than reused -- wrapping around + /// would make an old handle valid again, which is the one thing the + /// generation is there to prevent. + pub fn free(self: &mut Self, h: Handle(T)) -> bool { + if not self.valid(h) { return false; } + let at: usize = h.index as usize; + self.slots.^[at].live = false; + self.count = self.count - 1; + let gen: u32 = self.slots.^[at].gen % 65536; + if gen == 65535 { return true; } + self.slots.^[at].gen = self.slots.^[at].gen + 1; + self.slots.^[at].next = self.free; + self.free = h.index; + return true; + } + + /// Forget everything at once. The epoch moves, so every handle made before + /// now is stale without having to touch a single slot. + pub fn reset(self: &mut Self) -> void { + self.epoch = (self.epoch + 1) % 256; + self.high = 0; + self.count = 0; + self.free = NONE; + return; + } +} diff --git a/fec/std/core.fe b/fec/std/core.fe new file mode 100644 index 0000000..ca49229 --- /dev/null +++ b/fec/std/core.fe @@ -0,0 +1,12 @@ +unit std.core; + +// The default error set is open: `error.Name` names a member of it without +// declaring one, and the build assigns the codes (SPEC 4.6). + +pub fn assert(ok: bool) -> void { + if not ok { @trap(); } +} + +pub fn min(a: i32, b: i32) -> i32 { if a < b { return a; } return b; } +pub fn max(a: i32, b: i32) -> i32 { if a > b { return a; } return b; } +pub fn abs(v: i32) -> i32 { if v < 0 { return 0 - v; } return v; } diff --git a/fec/std/fmt.fe b/fec/std/fmt.fe new file mode 100644 index 0000000..dabea45 --- /dev/null +++ b/fec/std/fmt.fe @@ -0,0 +1,32 @@ +unit std.fmt; + +// Pure conversion. Nothing here owns a sink: the caller supplies the buffer +// and is told how many bytes at the front of it were written (SPEC 10). + +pub fn fmt_u32(buf: []mut u8, v: u32) -> usize { + var tmp: [10]u8 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + var value: u32 = v; + var count: usize = 0; + while true { + tmp[count] = ((value % 10) as u8) + ('0' as u8); + count = count + 1; + value = value / 10; + if value == 0 { break; } + if count == 10 { break; } + } + var i: usize = 0; + while i < count { + if i < buf.n { buf[i] = tmp[count - 1 - i]; } + i = i + 1; + } + return count; +} + +pub fn fmt_i32(buf: []mut u8, v: i32) -> usize { + if v >= 0 { return fmt_u32(buf, v as u32); } + if buf.n == 0 { return 0; } + buf[0] = '-' as u8; + var rest: []mut u8 = buf[1..buf.n]; + let digits: usize = fmt_u32(rest, (0 - v) as u32); + return digits + 1; +} diff --git a/fec/std/intern.fe b/fec/std/intern.fe new file mode 100644 index 0000000..613b9b1 --- /dev/null +++ b/fec/std/intern.fe @@ -0,0 +1,142 @@ +unit std.intern; + +import std.map; +import std.mem; + +// One copy of every distinct name, and a number that stands for it. +// +// A compiler compares names constantly and stores them everywhere. Comparing +// two `StrId` is comparing two integers; storing one costs four bytes and no +// ownership. That is the whole point. +// +// There is deliberately no way to get a `str` back out. A borrow of the text +// would be a borrow of the interner, and the interner is exactly the thing you +// want to keep adding to while holding names -- the parser reads an identifier +// and registers the next one in the same breath. Everything you would open the +// text for is here instead: compare it, measure it, hash it, write it. + +/// A name, as a number. Copy, four bytes, and meaningless to any other +/// interner -- which is fine, because a program has one. +pub struct StrId { + pub raw: u32, + + pub fn same(self: &Self, other: StrId) -> bool { + return self.raw == other.raw; + } +} + +/// No name. +pub const NONE: u32 = 4294967295; + +/// A name written out as map key bytes. `std.map` keys on bytes, so a name +/// used as a key is just its number -- four of them, and nothing allocated. +/// A symbol table is `Map(V)` keyed on this. +pub fn key_of(id: StrId, out: []mut u8) -> []u8 { + out[0] = (id.raw % 256) as u8; + out[1] = ((id.raw / 256) % 256) as u8; + out[2] = ((id.raw / 65536) % 256) as u8; + out[3] = ((id.raw / 16777216) % 256) as u8; + return out[0..4]; +} + +struct Entry { + at: usize, + len: usize, +} + +pub struct Interner { + /// Every name end to end. Nothing is ever removed, so an offset stays + /// good for as long as the interner does. + bytes: ^[]mut u8, + used: usize, + names: ^[]mut Entry, + count: usize, + /// Text to id, so interning the same name twice gives the same number. + seen: map.Map(u32), + + pub fn with_capacity(n: usize) -> !Self { + let text: ^[]mut u8 = try mem.alloc_slice(u8, n * 8); + let table: ^[]mut Entry = try mem.alloc_slice(Entry, n); + let index: map.Map(u32) = try map.Map(u32).with_capacity(n); + return Self{ bytes: text, used: 0, names: table, count: 0, + seen: index }; + } + + pub fn count_of(self: &Self) -> usize { return self.count; } + + /// The number for this name, making one if it is new. + pub fn intern(self: &mut Self, text: []u8) -> !StrId { + let found: u32 = self.seen.get(text, NONE); + if found != NONE { return StrId{ raw: found }; } + if self.count == self.names.^.n { return error.OutOfMemory; } + let at: usize = self.used; + if at + text.n > self.bytes.^.n { return error.OutOfMemory; } + var i: usize = 0; + while i < text.n { + self.bytes.^[at + i] = text[i]; + i = i + 1; + } + let id: usize = self.count; + self.names.^[id].at = at; + self.names.^[id].len = text.n; + self.used = at + text.n; + self.count = id + 1; + try self.seen.put(text, id as u32); + return StrId{ raw: id as u32 }; + } + + /// Is this a name it has seen? `NONE` when not. + pub fn find(self: &Self, text: []u8) -> u32 { + return self.seen.get(text, NONE); + } + + pub fn len_of(self: &Self, id: StrId) -> usize { + if (id.raw as usize) >= self.count { return 0; } + return self.names.^[id.raw as usize].len; + } + + /// Does this id spell this text? The comparison every `if name == "fn"` + /// in a parser turns into. + pub fn eq(self: &Self, id: StrId, text: []u8) -> bool { + if (id.raw as usize) >= self.count { return false; } + let e: usize = id.raw as usize; + if self.names.^[e].len != text.n { return false; } + var i: usize = 0; + while i < text.n { + if self.bytes.^[self.names.^[e].at + i] != text[i] { return false; } + i = i + 1; + } + return true; + } + + /// FNV-1a over the stored bytes, for anything that wants to bucket names + /// without opening them. + pub fn hash_of(self: &Self, id: StrId) -> u32 { + if (id.raw as usize) >= self.count { return 0; } + let e: usize = id.raw as usize; + var h: u32 = 2166136261; + var i: usize = 0; + while i < self.names.^[e].len { + h = h ^ (self.bytes.^[self.names.^[e].at + i] as u32); + h = h * 16777619; + i = i + 1; + } + return h; + } + + /// Copy the name into `out` and say how many bytes it took. This is how a + /// name reaches a diagnostic without the interner being borrowed past the + /// statement. + pub fn copy_into(self: &Self, id: StrId, out: []mut u8) -> usize { + if (id.raw as usize) >= self.count { return 0; } + let e: usize = id.raw as usize; + var n: usize = self.names.^[e].len; + if n > out.n { n = out.n; } + var i: usize = 0; + while i < n { + out[i] = self.bytes.^[self.names.^[e].at + i]; + i = i + 1; + } + return n; + } +} diff --git a/fec/std/io.fe b/fec/std/io.fe new file mode 100644 index 0000000..75e2cde --- /dev/null +++ b/fec/std/io.fe @@ -0,0 +1,120 @@ +unit std.io; +import std.sys; + +// A writer is a handle and nothing else: an integer the runtime understands. +// It stores no reference and no context pointer, so it is Copy and can be +// passed and returned freely (SPEC 5 R8). +pub enum Writer { Null, Stdout, Stderr } + +pub fn write(w: Writer, bytes: []u8) -> usize { + if w == Writer.Null { return bytes.n; } + var handle: i32 = 1; + if w == Writer.Stderr { handle = 2; } + var done: i32 = 0; + unsafe { done = sys.raw_write(handle, @ptr_cast(u8, &bytes[0]), bytes.n); } + if done < 0 { return 0; } + return done as usize; +} + +pub fn print(bytes: []u8) -> usize { + return write(Writer.Stdout, bytes); +} + +pub fn println(bytes: []u8) -> usize { + let n: usize = write(Writer.Stdout, bytes); + return n + write(Writer.Stdout, "\n"); +} + +// Files. A handle is what the operating system gave back; -1 means it did not +// give one. The path has to be NUL terminated because that is what the system +// call wants, and `to_cstr` is how a Ferro string becomes one. + +pub fn open_read(path: []mut u8) -> !i32 { + var handle: i32 = 0; + unsafe { handle = sys.raw_open(@ptr_cast(u8, &path[0]), 0); } + if handle == 0 - 1 { return error.NoSuchFile; } + return handle; +} + +pub fn open_write(path: []mut u8) -> !i32 { + var handle: i32 = 0; + unsafe { handle = sys.raw_open(@ptr_cast(u8, &path[0]), 1); } + if handle == 0 - 1 { return error.CannotWrite; } + return handle; +} + +pub fn read(handle: i32, into: []mut u8) -> !usize { + var got: i32 = 0; + unsafe { got = sys.raw_read(handle, @ptr_cast(u8, &into[0]), into.n); } + if got < 0 { return error.ReadFailed; } + return got as usize; +} + +pub fn close(handle: i32) -> void { + sys.raw_close(handle); +} + +/// Put `text` into `buf` with a NUL after it and say how many bytes that took, +/// the NUL included. A system call cannot be told a length, so it needs this. +/// +/// The length comes back rather than a slice of `buf`: with two reference-like +/// parameters the signature cannot say which one a returned slice came from, +/// and R8 will not guess. +pub fn to_cstr(buf: []mut u8, text: []u8) -> usize { + var i: usize = 0; + while i < text.n { + if i + 1 >= buf.n { break; } + buf[i] = text[i]; + i = i + 1; + } + if i < buf.n { buf[i] = 0; } + return i + 1; +} + +pub fn write_file(handle: i32, bytes: []u8) -> !usize { + var done: i32 = 0; + unsafe { done = sys.raw_write(handle, @ptr_cast(u8, &bytes[0]), bytes.n); } + if done < 0 { return error.WriteFailed; } + return done as usize; +} + +/// Copy the command line into `buf` and say how long it is. It arrives as one +/// string with the program's own name first; `arg` picks a piece out of it. +pub fn cmdline(buf: []mut u8) -> usize { + let raw: *u8 = sys.raw_cmdline(); + var i: usize = 0; + unsafe { + while i + 1 < buf.n { + let c: u8 = @volatile_load(raw + i); + if c == 0 { break; } + buf[i] = c; + i = i + 1; + } + } + return i; +} + +/// The `n`th whitespace-separated piece of `line`, or an empty slice when +/// there is no such piece. Quoting is not handled; nothing here needs it yet. +pub fn arg(line: []u8, n: usize) -> []u8 { + var at: usize = 0; + var seen: usize = 0; + while at < line.n { + while at < line.n { + if line[at] != 32 { break; } + at = at + 1; + } + var stop: usize = at; + while stop < line.n { + if line[stop] == 32 { break; } + stop = stop + 1; + } + if stop > at { + if seen == n { return line[at..stop]; } + seen = seen + 1; + } + at = stop; + } + return line[0..0]; +} + diff --git a/fec/std/list.fe b/fec/std/list.fe new file mode 100644 index 0000000..d9bb717 --- /dev/null +++ b/fec/std/list.fe @@ -0,0 +1,87 @@ +unit std.list; + +// A growable sequence. The buffer is owned, so a List owns its elements and +// releasing it releases them (SPEC 5 R1) -- which is also why there is no +// `drop` here: letting go of a List lets go of its buffer on its own. Growth +// doubles, which keeps the total copying proportional to the number of +// pushes. + +pub struct List(T) { + items: ^[]mut T, + len: usize, + + pub fn with_capacity(n: usize) -> !Self { + let room: ^[]mut T = try mem.alloc_slice(T, n); + return Self{ items: room, len: 0 }; + } + + pub fn count(self: &Self) -> usize { return self.len; } + + pub fn at(self: &Self, i: usize) -> T { + return self.items.^[i]; + } + + pub fn set(self: &mut Self, i: usize, v: T) -> void { + self.items.^[i] = v; + } + + pub fn push(self: &mut Self, v: T) -> !void { + if self.len == self.items.^.n { try self.grow(); } + self.items.^[self.len] = v; + self.len = self.len + 1; + return; + } + + /// Take the last one off. Nothing to take is `null`, not a trap. + pub fn pop(self: &mut Self) -> ?T { + if self.len == 0 { return null; } + self.len = self.len - 1; + return self.items.^[self.len]; + } + + /// Move one out and leave `replacement` where it was (SPEC 5 R7). This is + /// how a `T` that owns something leaves the list without the list ending + /// up with a hole in it. + pub fn take(self: &mut Self, i: usize, replacement: T) -> T { + return mem.replace(&mut self.items.^[i], replacement); + } + + /// Exchange two elements. + pub fn swap(self: &mut Self, i: usize, j: usize) -> void { + if i == j { return; } + let first: T = self.items.^[i]; + let second: T = mem.replace(&mut self.items.^[j], first); + self.items.^[i] = second; + return; + } + + /// The elements as a slice, so `for x in xs.slice()` walks them. R8(a): + /// derived from `self`, so the borrow belongs to the caller. + pub fn slice(self: &Self) -> []T { + return self.items.^[0..self.len]; + } + + pub fn slice_mut(self: &mut Self) -> []mut T { + return self.items.^[0..self.len]; + } + + /// Forget the elements and keep the buffer. + pub fn clear(self: &mut Self) -> void { self.len = 0; return; } + + /// Move to a buffer twice the size. Kept apart from `push` because the + /// borrow that hands over the old buffer must not be live while the old + /// buffer is still being read (SPEC 5 R6). + fn grow(self: &mut Self) -> !void { + var room: usize = self.items.^.n * 2; + if room == 0 { room = 4; } + let bigger: ^[]mut T = try mem.alloc_slice(T, room); + var i: usize = 0; + while i < self.len { + bigger.^[i] = self.items.^[i]; + i = i + 1; + } + let old: ^[]mut T = mem.replace(&mut self.items, bigger); + mem.destroy(old); + return; + } +} diff --git a/fec/std/map.fe b/fec/std/map.fe new file mode 100644 index 0000000..58f4900 --- /dev/null +++ b/fec/std/map.fe @@ -0,0 +1,175 @@ +unit std.map; + +// A table from a run of bytes to a value. +// +// A compiler looks names up constantly and a linear scan over a list is the +// wrong shape for that. Keys are copied into one buffer the map owns and each +// slot records where in it the key sits -- the arena-and-handle shape R11 asks +// for, which also means letting go of the map is two frees and not one per +// entry. Both buffers are owned, so R1 does the letting go and no `drop` is +// written here. +// +// Open addressing with linear probing. The table is a power of two so the +// index is a mask rather than a division, and it grows at three quarters full +// because probing gets long well before the table gets full. + +pub struct Slot(V) { + at: usize, + len: usize, + used: bool, + value: V, +} + +pub struct Map(V) { + slots: ^[]mut Slot(V), + bytes: ^[]mut u8, + used_bytes: usize, + count: usize, + + pub fn with_capacity(n: usize) -> !Self { + var room: usize = 8; + while room < n * 2 { room = room * 2; } + let table: ^[]mut Slot(V) = try mem.alloc_slice(Slot(V), room); + let text: ^[]mut u8 = try mem.alloc_slice(u8, 64); + var i: usize = 0; + while i < room { + table.^[i].used = false; + i = i + 1; + } + return Self{ slots: table, bytes: text, used_bytes: 0, count: 0 }; + } + + pub fn count_of(self: &Self) -> usize { return self.count; } + + /// Forget every key but keep the storage. A scope that ends can hand its + /// table to the next one without going back to the allocator, which is + /// what a resolver wants: one table per nesting level, not per function. + pub fn clear(self: &mut Self) -> void { + var i: usize = 0; + while i < self.slots.^.n { + self.slots.^[i].used = false; + i = i + 1; + } + self.used_bytes = 0; + self.count = 0; + return; + } + + pub fn room(self: &Self) -> usize { return self.slots.^.n; } + + /// Where `key` sits in the table: the slot holding it, or the first free + /// slot it could go in. Probing stops at a free slot, which is why a slot + /// is never cleared -- only ever filled. + fn find(self: &Self, key: []u8) -> usize { + let mask: usize = self.slots.^.n - 1; + var at: usize = hash(key) & mask; + while true { + if not self.slots.^[at].used { return at; } + if self.same(at, key) { return at; } + at = (at + 1) & mask; + } + return 0; + } + + fn same(self: &Self, slot: usize, key: []u8) -> bool { + if self.slots.^[slot].len != key.n { return false; } + let from: usize = self.slots.^[slot].at; + var i: usize = 0; + while i < key.n { + if self.bytes.^[from + i] != key[i] { return false; } + i = i + 1; + } + return true; + } + + pub fn has(self: &Self, key: []u8) -> bool { + return self.slots.^[self.find(key)].used; + } + + pub fn get(self: &Self, key: []u8, missing: V) -> V { + let at: usize = self.find(key); + if self.slots.^[at].used { return self.slots.^[at].value; } + return missing; + } + + pub fn put(self: &mut Self, key: []u8, value: V) -> !void { + if self.count * 4 >= self.slots.^.n * 3 { try self.regrow(); } + let at: usize = self.find(key); + if self.slots.^[at].used { + self.slots.^[at].value = value; + return; + } + try self.keep(key, at); + self.slots.^[at].value = value; + self.slots.^[at].used = true; + self.count = self.count + 1; + return; + } + + /// Copy `key` into the byte buffer and point the slot at it, moving to a + /// bigger buffer first if it does not fit. + fn keep(self: &mut Self, key: []u8, slot: usize) -> !void { + let at: usize = self.used_bytes; + if at + key.n > self.bytes.^.n { + var room: usize = self.bytes.^.n; + while room < at + key.n { room = room * 2; } + let bigger: ^[]mut u8 = try mem.alloc_slice(u8, room); + var k: usize = 0; + while k < at { + bigger.^[k] = self.bytes.^[k]; + k = k + 1; + } + let old: ^[]mut u8 = mem.replace(&mut self.bytes, bigger); + mem.destroy(old); + } + var i: usize = 0; + while i < key.n { + self.bytes.^[at + i] = key[i]; + i = i + 1; + } + self.slots.^[slot].at = at; + self.slots.^[slot].len = key.n; + self.used_bytes = at + key.n; + return; + } + + /// Twice the slots, everything placed again. The keys do not move: they + /// live in the byte buffer and the slots only point at them. + fn regrow(self: &mut Self) -> !void { + let bigger: ^[]mut Slot(V) = try mem.alloc_slice(Slot(V), + self.slots.^.n * 2); + let mask: usize = bigger.^.n - 1; + var i: usize = 0; + while i < bigger.^.n { + bigger.^[i].used = false; + i = i + 1; + } + i = 0; + while i < self.slots.^.n { + if self.slots.^[i].used { + let from: usize = self.slots.^[i].at; + let len: usize = self.slots.^[i].len; + var at: usize = hash(self.bytes.^[from..from + len]) & mask; + while bigger.^[at].used { at = (at + 1) & mask; } + bigger.^[at] = self.slots.^[i]; + } + i = i + 1; + } + let old: ^[]mut Slot(V) = mem.replace(&mut self.slots, bigger); + mem.destroy(old); + return; + } +} + +/// FNV-1a. Small, fast, and good enough for identifiers; nothing here has to +/// resist an adversary choosing the keys. +pub fn hash(key: []u8) -> usize { + var h: u32 = 2166136261; + var i: usize = 0; + while i < key.n { + h = h ^ (key[i] as u32); + h = h * 16777619; + i = i + 1; + } + return h as usize; +} diff --git a/fec/std/mem.fe b/fec/std/mem.fe new file mode 100644 index 0000000..831cccc --- /dev/null +++ b/fec/std/mem.fe @@ -0,0 +1,60 @@ +unit std.mem; +import std.sys; + +// `create`, `destroy`, `alloc_slice` and `replace` are compiler intrinsics: +// they need to know the type they are handed, which no signature can say. +// What is written here is what can be written in Ferro. + +/// A block of storage handed out in pieces, released all at once. +/// +/// SPEC R11 answers recursive and graph-shaped data with an arena that owns +/// the values and integer handles that reference them. This is that arena. A +/// handle is an offset, so it stays valid while the arena does, and comparing +/// two handles is comparing two numbers. The block itself is owned, so nothing +/// here says how to let go of it -- R1 already does. +pub struct Arena { + bytes: ^[]mut u8, + used: usize, + + pub fn with_capacity(n: usize) -> !Self { + let room: ^[]mut u8 = try mem.alloc_slice(u8, n); + return Self{ bytes: room, used: 0 }; + } + + pub fn size(self: &Self) -> usize { return self.used; } + + pub fn room(self: &Self) -> usize { return self.bytes.^.n; } + + /// Reserve `n` bytes aligned to `align` and give back where they start. + /// Failure is running out of room, which the caller decides what to do + /// about; the arena never grows behind your back, because a handle that + /// moved would no longer mean anything. + pub fn alloc(self: &mut Self, n: usize, align: usize) -> !usize { + var at: usize = self.used; + if align > 1 { + let over: usize = at % align; + if over != 0 { at = at + align - over; } + } + if at + n > self.bytes.^.n { return error.ArenaFull; } + self.used = at + n; + return at; + } + + /// One byte at a handle. Reading and writing go through here so that a + /// handle can be checked once, in one place. + pub fn at(self: &Self, handle: usize) -> !u8 { + if handle >= self.used { return error.BadHandle; } + return self.bytes.^[handle]; + } + + pub fn put(self: &mut Self, handle: usize, value: u8) -> !void { + if handle >= self.used { return error.BadHandle; } + self.bytes.^[handle] = value; + return; + } + + /// Forget everything handed out so far. Every handle from before is stale; + /// that is the trade an arena makes. + pub fn reset(self: &mut Self) -> void { self.used = 0; } + +} diff --git a/fec/std/str.fe b/fec/std/str.fe new file mode 100644 index 0000000..0e4dbc5 --- /dev/null +++ b/fec/std/str.fe @@ -0,0 +1,66 @@ +unit std.str; + +// `str` is `[]u8` (SPEC 4.2), so these take and give plain byte slices. + +pub fn eq(a: []u8, b: []u8) -> bool { + if a.n != b.n { return false; } + var i: usize = 0; + while i < a.n { + if a[i] != b[i] { return false; } + i = i + 1; + } + return true; +} + +pub fn starts_with(s: []u8, prefix: []u8) -> bool { + if prefix.n > s.n { return false; } + return eq(s[0..prefix.n], prefix); +} + +/// Where `needle` first appears in `s`, or the length of `s` when it does not. +/// An index past the end is how "not found" is said without an optional. +pub fn find(s: []u8, needle: []u8) -> usize { + if needle.n == 0 { return 0; } + if needle.n > s.n { return s.n; } + var at: usize = 0; + let last: usize = s.n - needle.n; + while at <= last { + if eq(s[at..at + needle.n], needle) { return at; } + at = at + 1; + } + return s.n; +} + +pub fn trim(s: []u8) -> []u8 { + var from: usize = 0; + var to: usize = s.n; + while from < to { + if s[from] != 32 and s[from] != 9 and s[from] != 10 and s[from] != 13 { + break; + } + from = from + 1; + } + while to > from { + let c: u8 = s[to - 1]; + if c != 32 and c != 9 and c != 10 and c != 13 { break; } + to = to - 1; + } + return s[from..to]; +} + +pub fn parse_int(s: []u8) -> ?i32 { + if s.n == 0 { return null; } + var value: i32 = 0; + var i: usize = 0; + var negative: bool = false; + if s[0] == 45 { negative = true; i = 1; } + if i >= s.n { return null; } + while i < s.n { + let c: u8 = s[i]; + if c < 48 or c > 57 { return null; } + value = value * 10 + ((c - 48) as i32); + i = i + 1; + } + if negative { return 0 - value; } + return value; +} diff --git a/fec/std/sys.fe b/fec/std/sys.fe new file mode 100644 index 0000000..53c3db0 --- /dev/null +++ b/fec/std/sys.fe @@ -0,0 +1,53 @@ +unit std.sys; + +// The few things the language cannot say for itself. The runtime provides +// them; everything else in the standard library is written in Ferro. +extern "c" fn fe_rt_write(handle: i32, bytes: *u8, len: usize) -> i32; +extern "c" fn fe_rt_alloc(n: usize) -> *u8; +extern "c" fn fe_rt_free(p: *u8); +extern "c" fn fe_rt_exit(code: i32); +extern "c" fn fe_rt_allocs() -> i32; +extern "c" fn fe_rt_frees() -> i32; +extern "c" fn fe_rt_open(path: *u8, write: i32) -> i32; +extern "c" fn fe_rt_read(handle: i32, buf: *u8, len: usize) -> i32; +extern "c" fn fe_rt_close(handle: i32); +extern "c" fn fe_rt_cmdline() -> *u8; + +pub fn exit(code: i32) -> void { + unsafe { fe_rt_exit(code); } +} + +pub fn raw_write(handle: i32, bytes: *u8, len: usize) -> i32 { + unsafe { return fe_rt_write(handle, bytes, len); } +} + +pub fn raw_alloc(n: usize) -> *u8 { + unsafe { return fe_rt_alloc(n); } +} + +pub fn raw_free(p: *u8) -> void { + unsafe { fe_rt_free(p); } +} + +// How many times the allocator was asked to hand out memory, and to take it +// back. A test can insist the two agree; nothing else should care. +pub fn allocs() -> i32 { unsafe { return fe_rt_allocs(); } } +pub fn frees() -> i32 { unsafe { return fe_rt_frees(); } } + +pub fn raw_open(path: *u8, write: i32) -> i32 { + unsafe { return fe_rt_open(path, write); } +} + +pub fn raw_read(handle: i32, buf: *u8, len: usize) -> i32 { + unsafe { return fe_rt_read(handle, buf, len); } +} + +pub fn raw_close(handle: i32) -> void { + unsafe { fe_rt_close(handle); } +} + +/// The whole command line as one NUL-terminated string. Splitting it into +/// arguments is `std.io`'s job: the runtime should not know about quoting. +pub fn raw_cmdline() -> *u8 { + unsafe { return fe_rt_cmdline(); } +} diff --git a/fec/tests/exec/arena.fe b/fec/tests/exec/arena.fe new file mode 100644 index 0000000..88da9b7 --- /dev/null +++ b/fec/tests/exec/arena.fe @@ -0,0 +1,39 @@ +// EXIT:0 +// OUTPUT:handles 0 4 8 +// OUTPUT:value 65 +// OUTPUT:full +// OUTPUT:reset 0 +// OUTPUT:balanced +unit arena; +import std.io; +import std.mem; +import std.sys; + +// SPEC R11: recursive and graph-shaped data is answered by an arena that owns +// the values and integer handles that point into it. This is that shape. + +fn run() -> !void { + var a: mem.Arena = try mem.Arena.with_capacity(16); + let first: usize = try a.alloc(4, 4); + let second: usize = try a.alloc(4, 4); + let third: usize = try a.alloc(4, 4); + @print("handles {} {} {}\n", first, second, third); + try a.put(first, 65); + let got: u8 = try a.at(first); + @print("value {}\n", got); + let over: usize = a.alloc(64, 1) catch |e| { + @print("full\n"); + a.reset(); + @print("reset {}\n", a.size()); + return; + }; + @print("unexpected room {}\n", over); + return; +} + +fn main() -> i32 { + run() catch |e| { @print("failed\n"); return 1; }; + if sys.allocs() != sys.frees() { @print("leaked\n"); return 2; } + @print("balanced\n"); + return 0; +} diff --git a/fec/tests/exec/arenat.fe b/fec/tests/exec/arenat.fe new file mode 100644 index 0000000..41469bd --- /dev/null +++ b/fec/tests/exec/arenat.fe @@ -0,0 +1,81 @@ +// EXIT:0 +// OUTPUT:alloc 10 20 30 len 3 +// OUTPUT:freed len 2 stale -1 live 30 +// OUTPUT:reused index 1 old -1 new 99 +// OUTPUT:swap 99 10 +// OUTPUT:set 77 take 77 after 5 +// OUTPUT:reset len 0 before -1 +// OUTPUT:other -1 +// OUTPUT:full yes +// OUTPUT:balanced +unit arenat; + +import std.io; +import std.sys; +import std.arena; + +// SPEC R11: the arena owns the values and a handle is a number. The point of +// the number carrying a generation is that using it after the slot was given +// back answers `null` rather than whatever moved in afterwards. + +fn run() -> !void { + var a: arena.Arena(i32) = try arena.Arena(i32).with_capacity(1, 3); + let x: arena.Handle(i32) = try a.alloc(10); + let y: arena.Handle(i32) = try a.alloc(20); + let z: arena.Handle(i32) = try a.alloc(30); + @print("alloc {} {} {} len {}\n", a.get(x) orelse -1, a.get(y) orelse -1, + a.get(z) orelse -1, a.len()); + + // Give one back. Its handle stops meaning anything; the others do not. + let gone: bool = a.free(y); + @print("freed len {} stale {} live {}\n", a.len(), a.get(y) orelse -1, + a.get(z) orelse -1); + + // The slot comes back on the free list, and the old handle still does not + // match the new occupant. + let again: arena.Handle(i32) = try a.alloc(99); + @print("reused index {} old {} new {}\n", again.index, + a.get(y) orelse -1, a.get(again) orelse -1); + + let ok: bool = a.swap(x, again); + @print("swap {} {}\n", a.get(x) orelse -1, a.get(again) orelse -1); + + let wrote: bool = a.set(x, 77); + let took: i32 = a.take(x, 5) orelse -1; + @print("set {} take {} after {}\n", 77, took, a.get(x) orelse -1); + + // Reset moves the epoch, so every handle made before it is stale without + // a single slot being touched. + a.reset(); + @print("reset len {} before {}\n", a.len(), a.get(x) orelse -1); + + // A handle from one arena means nothing to another. + var b: arena.Arena(i32) = try arena.Arena(i32).with_capacity(2, 2); + let h: arena.Handle(i32) = try b.alloc(41); + @print("other {}\n", a.get(h) orelse -1); + + // Running out of room is an error, not a trap. + var full: bool = false; + fill(&mut b) catch |e| { full = true; }; + @print("full {}\n", yesno(full)); + return; +} + +/// Two more into an arena that has room for one. +fn fill(b: &mut arena.Arena(i32)) -> !void { + let p: arena.Handle(i32) = try b.alloc(1); + let q: arena.Handle(i32) = try b.alloc(2); + return; +} + +fn yesno(b: bool) -> []u8 { + if b { return "yes"; } + return "no"; +} + +fn main() -> i32 { + run() catch |e| { @print("failed\n"); return 1; }; + if sys.allocs() == sys.frees() { @print("balanced\n"); } + else { @print("leaked\n"); } + return 0; +} diff --git a/fec/tests/exec/arith.fe b/fec/tests/exec/arith.fe new file mode 100644 index 0000000..e9cb2ff --- /dev/null +++ b/fec/tests/exec/arith.fe @@ -0,0 +1,12 @@ +// EXIT:10 +unit arith; + +fn main() -> i32 { + let a: i32 = 7; + let b: i32 = 6; + var r: i32 = a * b; + r = r - 2; + r = r / 4; + if r == 10 { return r; } + return 99; +} diff --git a/fec/tests/exec/array.fe b/fec/tests/exec/array.fe new file mode 100644 index 0000000..cf1e7ce --- /dev/null +++ b/fec/tests/exec/array.fe @@ -0,0 +1,13 @@ +// EXIT:100 +unit array; + +fn main() -> i32 { + let a: [4]i32 = [10, 20, 30, 40]; + var sum: i32 = 0; + var i: i32 = 0; + while i < 4 { + sum = sum + a[i]; + i = i + 1; + } + return sum; +} diff --git a/fec/tests/exec/bitnot.fe b/fec/tests/exec/bitnot.fe new file mode 100644 index 0000000..426b970 --- /dev/null +++ b/fec/tests/exec/bitnot.fe @@ -0,0 +1,37 @@ +// EXIT:0 +// OUTPUT:i32 -1 -6 4 +// OUTPUT:u8 255 250 +// OUTPUT:mask 240 15 +// OUTPUT:prec 7 3 +unit bitnot; + +import std.io; + +// `~` flips every bit (SPEC §6.2, 단항). `^` is taken by xor and by `.^`, so +// bitwise NOT needs its own spelling. +// +// `|`, `^` and `&` are three different levels, in C's order. Merging `|` and +// `^` would make `a | b ^ c` bind as `(a | b) ^ c`, which is not what anyone +// coming from C reads it as. + +fn main() -> i32 { + let a: i32 = 0; + let b: i32 = 5; + let c: i32 = -5; + @print("i32 {} {} {}\n", ~a, ~b, ~c); + + let u: u8 = 0; + let v: u8 = 5; + @print("u8 {} {}\n", (~u) as i32, (~v) as i32); + + // Clearing bits is what the operator is for. + let bits: u8 = 255; + let low: u8 = 15; + @print("mask {} {}\n", (bits & ~low) as i32, (bits & low) as i32); + + // 1 | 2 ^ 4 is 1 | (2 ^ 4) = 1 | 6 = 7, not (1 | 2) ^ 4 = 3 ^ 4 = 7. + // Those agree, so pick operands that do not: 3 | 1 ^ 2 is 3 | 3 = 3, + // while (3 | 1) ^ 2 would be 3 ^ 2 = 1. The 3 is the proof. + @print("prec {} {}\n", 1 | 2 ^ 4, 3 | 1 ^ 2); + return 0; +} diff --git a/fec/tests/exec/bounds.fe b/fec/tests/exec/bounds.fe new file mode 100644 index 0000000..3772539 --- /dev/null +++ b/fec/tests/exec/bounds.fe @@ -0,0 +1,10 @@ +// EXIT:3 +// OUTPUT:index out of bounds +// NOCHECKS:0 +unit bounds; + +fn main() -> i32 { + let a: [2]i32 = [1, 2]; + let x: i32 = a[2]; + return x - x; +} diff --git a/fec/tests/exec/calc.fe b/fec/tests/exec/calc.fe new file mode 100644 index 0000000..0f6b95a --- /dev/null +++ b/fec/tests/exec/calc.fe @@ -0,0 +1,120 @@ +// EXIT:0 +// OUTPUT:1+2*3 = 7 +// OUTPUT:(1+2)*3 = 9 +// OUTPUT:2*(3+4)-5 = 9 +// OUTPUT:10/3 = 3 +// OUTPUT:1+ = error +// OUTPUT:(1+2 = error +unit calc; +import std.io; +import std.fmt; + +// A recursive-descent evaluator over a byte slice. +// +// The position travels in a `&mut usize` rather than in a struct beside the +// text: a struct cannot hold a slice, because a slice is a borrowed view and +// R4 keeps borrows out of aggregate storage. Passing both is the honest way +// to say "this text, and how far we have read". + +fn done(src: []u8, at: usize) -> bool { return at >= src.n; } + +fn peek(src: []u8, at: usize) -> u8 { + if done(src, at) { return 0; } + return src[at]; +} + +fn skip_spaces(src: []u8, at: &mut usize) -> void { + while not done(src, at.^) { + if src[at.^] != 32 { break; } + at.^ = at.^ + 1; + } +} + +fn number(src: []u8, at: &mut usize) -> !i32 { + var value: i32 = 0; + var digits: usize = 0; + while not done(src, at.^) { + let c: u8 = src[at.^]; + if c < 48 or c > 57 { break; } + value = value * 10 + ((c - 48) as i32); + digits = digits + 1; + at.^ = at.^ + 1; + } + if digits == 0 { return error.BadNumber; } + return value; +} + +fn factor(src: []u8, at: &mut usize) -> !i32 { + skip_spaces(src, at); + if peek(src, at.^) == 40 { + at.^ = at.^ + 1; + let inner: i32 = try expr(src, at); + skip_spaces(src, at); + if peek(src, at.^) != 41 { return error.Unbalanced; } + at.^ = at.^ + 1; + return inner; + } + return number(src, at); +} + +fn term(src: []u8, at: &mut usize) -> !i32 { + var left: i32 = try factor(src, at); + while true { + skip_spaces(src, at); + let op: u8 = peek(src, at.^); + if op != 42 and op != 47 { break; } + at.^ = at.^ + 1; + let right: i32 = try factor(src, at); + if op == 42 { left = left * right; } + else { + if right == 0 { return error.DivideByZero; } + left = left / right; + } + } + return left; +} + +fn expr(src: []u8, at: &mut usize) -> !i32 { + var left: i32 = try term(src, at); + while true { + skip_spaces(src, at); + let op: u8 = peek(src, at.^); + if op != 43 and op != 45 { break; } + at.^ = at.^ + 1; + let right: i32 = try term(src, at); + if op == 43 { left = left + right; } + else { left = left - right; } + } + return left; +} + +fn evaluate(text: []u8) -> !i32 { + var at: usize = 0; + let value: i32 = try expr(text, &mut at); + skip_spaces(text, &mut at); + if not done(text, at) { return error.Trailing; } + return value; +} + +fn show(text: []u8) -> void { + var buf: [16]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + io.print(text); + io.print(" = "); + let value: i32 = evaluate(text) catch |e| { + io.print("error\n"); + return; + }; + let n: usize = fmt.fmt_i32(buf[..], value); + io.print(buf[0..n]); + io.print("\n"); +} + +fn main() -> i32 { + show("1+2*3"); + show("(1+2)*3"); + show("2*(3+4)-5"); + show("10/3"); + show("1+"); + show("(1+2"); + return 0; +} diff --git a/fec/tests/exec/cmdargs.fe b/fec/tests/exec/cmdargs.fe new file mode 100644 index 0000000..4cb53ae --- /dev/null +++ b/fec/tests/exec/cmdargs.fe @@ -0,0 +1,21 @@ +// EXIT:0 +// OUTPUT:args ok +unit cmdargs; +import std.io; +import std.str; + +// The command line reaches the program. A compiler is told which file to read +// this way and no other. + +fn main() -> i32 { + var line: [512]u8 = undefined; + let n: usize = io.cmdline(line[..]); + let program: []u8 = io.arg(line[0..n], 0); + if program.n == 0 { @print("no program name\n"); return 1; } + if str.find(program, "cmdargs") == program.n { + @print("unexpected program name: {}\n", program); + return 2; + } + @print("args ok\n"); + return 0; +} diff --git a/fec/tests/exec/defers.fe b/fec/tests/exec/defers.fe new file mode 100644 index 0000000..e9e03ab --- /dev/null +++ b/fec/tests/exec/defers.fe @@ -0,0 +1,22 @@ +// EXIT:21921 +// body(true): defer 2 then defer 1 -> 2, 21 +// body(false): note(9) first -> 219, 2192, 21921 +unit defers; + +var log: i32 = 0; + +fn note(v: i32) -> void { log = log * 10 + v; } + +fn body(early: bool) -> i32 { + defer { note(1); } + defer { note(2); } + if early { return 0; } + note(9); + return 0; +} + +fn main() -> i32 { + body(true); + body(false); + return log; +} diff --git a/fec/tests/exec/errunion.fe b/fec/tests/exec/errunion.fe new file mode 100644 index 0000000..2293507 --- /dev/null +++ b/fec/tests/exec/errunion.fe @@ -0,0 +1,18 @@ +// EXIT:9 +unit errunion; + +fn half(v: i32) -> !i32 { + if v == 0 { return error.Empty; } + return v / 2; +} + +fn chain(v: i32) -> !i32 { + let h: i32 = try half(v); + return h + 1; +} + +fn main() -> i32 { + let good: i32 = chain(16) catch 100; + let bad: i32 = chain(0) catch 0; + return good + bad; +} diff --git a/fec/tests/exec/fieldbrw.fe b/fec/tests/exec/fieldbrw.fe new file mode 100644 index 0000000..c512a74 --- /dev/null +++ b/fec/tests/exec/fieldbrw.fe @@ -0,0 +1,81 @@ +// EXIT:0 +// OUTPUT:a 40 b 3 +// OUTPUT:used 5 room 8 +// OUTPUT:first 9 +// OUTPUT:balanced +unit fieldbrw; + +import std.io; +import std.sys; +import std.mem; + +// Borrowing is per field. `p.a` and `p.b` are different places, so lending one +// out has to leave the other readable -- otherwise a method cannot write +// through one field while reading another, which is most of what a method +// does. + +struct Pair { a: i32, b: i32, } + +fn scale(v: &mut i32, by: i32) -> void { + v.^ = v.^ * by; + return; +} + +struct Box { + bytes: ^[]mut u8, + used: usize, + room: usize, + + fn with_capacity(n: usize) -> !Self { + let room: ^[]mut u8 = try mem.alloc_slice(u8, n); + return Self{ bytes: room, used: 0, room: n }; + } + + /// Move to a bigger buffer, then go on reading the other fields. The + /// borrow that hands over the buffer covers `bytes` and nothing else. + fn grow(self: &mut Self, want: usize) -> !void { + let bigger: ^[]mut u8 = try mem.alloc_slice(u8, want); + var i: usize = 0; + while i < self.used { + bigger.^[i] = self.bytes.^[i]; + i = i + 1; + } + let old: ^[]mut u8 = mem.replace(&mut self.bytes, bigger); + mem.destroy(old); + self.room = want; + return; + } + + fn push(self: &mut Self, v: u8) -> !void { + if self.used == self.room { try self.grow(self.room * 2); } + self.bytes.^[self.used] = v; + self.used = self.used + 1; + return; + } +} + +fn run() -> !void { + var p: Pair = Pair{ a: 4, b: 2 }; + let left: &mut i32 = &mut p.a; + // Writing another field while `a` is lent out. + p.b = 3; + scale(left, 10); + @print("a {} b {}\n", p.a, p.b); + + var box: Box = try Box.with_capacity(4); + try box.push(9); + try box.push(8); + try box.push(7); + try box.push(6); + try box.push(5); + @print("used {} room {}\n", box.used, box.room); + @print("first {}\n", box.bytes.^[0]); + return; +} + +fn main() -> i32 { + run() catch |e| { @print("failed\n"); return 1; }; + if sys.allocs() == sys.frees() { @print("balanced\n"); } + else { @print("leaked\n"); } + return 0; +} diff --git a/fec/tests/exec/fieldn.fe b/fec/tests/exec/fieldn.fe new file mode 100644 index 0000000..cfd148f --- /dev/null +++ b/fec/tests/exec/fieldn.fe @@ -0,0 +1,49 @@ +// EXIT:0 +// OUTPUT:n 777 m 999 +// OUTPUT:len 12 first 7 +// OUTPUT:count 3 +// OUTPUT:balanced +unit fieldn; + +import std.io; +import std.sys; +import std.mem; + +// `.n` on a slice is its length. On a struct it is whatever field is called +// `n` -- and a struct is allowed to call a field that. Reading one as the +// other hands back the four bytes beside the pointer, which is a plausible +// number and so goes unnoticed. + +struct Box { + room: ^[]mut u8, + n: usize, + m: usize, +} + +struct Counter { + n: usize, + + fn bump(self: &mut Self) -> void { self.n = self.n + 1; return; } +} + +fn run() -> !void { + let r: ^[]mut u8 = try mem.alloc_slice(u8, 12); + r.^[0] = 7; + let b: Box = Box{ room: r, n: 777, m: 999 }; + @print("n {} m {}\n", b.n, b.m); + // The slice beside it still answers with its length. + @print("len {} first {}\n", b.room.^.n, b.room.^[0]); + var c: Counter = Counter{ n: 0 }; + c.bump(); + c.bump(); + c.bump(); + @print("count {}\n", c.n); + return; +} + +fn main() -> i32 { + run() catch |e| { @print("failed\n"); return 1; }; + if sys.allocs() == sys.frees() { @print("balanced\n"); } + else { @print("leaked\n"); } + return 0; +} diff --git a/fec/tests/exec/forloop.fe b/fec/tests/exec/forloop.fe new file mode 100644 index 0000000..8eb3cc2 --- /dev/null +++ b/fec/tests/exec/forloop.fe @@ -0,0 +1,11 @@ +// EXIT:60 +unit forloop; + +fn main() -> i32 { + let a: [5]i32 = [4, 8, 12, 16, 20]; + var sum: i32 = 0; + for v in a { + sum = sum + v.^; + } + return sum; +} diff --git a/fec/tests/exec/generic.fe b/fec/tests/exec/generic.fe new file mode 100644 index 0000000..a817005 --- /dev/null +++ b/fec/tests/exec/generic.fe @@ -0,0 +1,21 @@ +// EXIT:39 +unit generic; + +struct Box(T) { + value: T, + + pub fn new(v: T) -> Self { return Self{ value: v }; } + pub fn get(self: &Self) -> T { return self.value; } +} + +fn id(comptime T: type, v: T) -> T { return v; } + +fn twice(comptime T: type, v: T) -> T { return v + v; } + +fn main() -> i32 { + let a: i32 = id(i32, 7); + let b: u8 = id(u8, 9 as u8); + let c: i32 = twice(i32, 10); + let box: Box(i32) = Box(i32).new(3); + return a + (b as i32) + c + box.get(); +} diff --git a/fec/tests/exec/geninst/README.md b/fec/tests/exec/geninst/README.md new file mode 100644 index 0000000..7bfdd80 --- /dev/null +++ b/fec/tests/exec/geninst/README.md @@ -0,0 +1,8 @@ +# 제네릭 인스턴스 리터럴 + +`Name(args){...}` 와 `binding.Name(args){...}` 로 제네릭의 인스턴스를 짓는다. +전에는 `Self{...}` 나 생성자 함수로만 만들 수 있었다. + +`Handle(T)` 는 `T` 를 본문에서 쓰지 않는다 -- typed handle 의 자연스러운 모양이고, +`Handle(Node)` 와 `Handle(Kind)` 를 갈라놓는 것 말고는 하는 일이 없다. 그 둘이 +실제로 다른 타입이라는 것은 `generic/badphant.fe` 가 거부로 고정한다. diff --git a/fec/tests/exec/geninst/hold.fe b/fec/tests/exec/geninst/hold.fe new file mode 100644 index 0000000..44646f8 --- /dev/null +++ b/fec/tests/exec/geninst/hold.fe @@ -0,0 +1,14 @@ +unit hold; + +// Fields are `pub` so another unit can write the literal directly. A generic +// with a `pub` field is the only way to reach `binding.Name(args){...}`. +pub struct Cell(T) { + pub v: T, +} + +// `T` is never used in the body. That is the natural shape of a typed handle: +// the parameter is there to keep `Handle(Node)` and `Handle(Type)` apart, not +// to describe any storage. +pub struct Handle(T) { + pub raw: u32, +} diff --git a/fec/tests/exec/geninst/main.fe b/fec/tests/exec/geninst/main.fe new file mode 100644 index 0000000..b469b2a --- /dev/null +++ b/fec/tests/exec/geninst/main.fe @@ -0,0 +1,38 @@ +// EXIT:0 +// OUTPUT:cell 7 41 +// OUTPUT:handle 3 4 sum 7 +// OUTPUT:local 9 +unit main; + +import std.io; +import hold; + +struct Node { v: i32, } +struct Kind { v: i32, } + +// A generic declared here, instantiated with an explicit argument below. +struct Boxed(T) { + v: T, +} + +// Two instances of the same phantom generic are different nominal types, so +// this only accepts one of them. +fn only_node(h: hold.Handle(Node)) -> u32 { return h.raw; } +fn only_kind(h: hold.Handle(Kind)) -> u32 { return h.raw; } + +fn main() -> i32 { + // `binding.Name(args){...}` -- a generic instance from another unit. + let a: hold.Cell(i32) = hold.Cell(i32){ v: 7 }; + let b: hold.Cell(u8) = hold.Cell(u8){ v: 41 }; + @print("cell {} {}\n", a.v, b.v); + + let n: hold.Handle(Node) = hold.Handle(Node){ raw: 3 }; + let k: hold.Handle(Kind) = hold.Handle(Kind){ raw: 4 }; + @print("handle {} {} sum {}\n", only_node(n), only_kind(k), + only_node(n) + only_kind(k)); + + // `Name(args){...}` -- a generic declared in this unit. + let c: Boxed(i32) = Boxed(i32){ v: 9 }; + @print("local {}\n", c.v); + return 0; +} diff --git a/fec/tests/exec/heap.fe b/fec/tests/exec/heap.fe new file mode 100644 index 0000000..03367b8 --- /dev/null +++ b/fec/tests/exec/heap.fe @@ -0,0 +1,30 @@ +// EXIT:0 +// OUTPUT:sum 4950 +unit heap; +import std.io; +import std.fmt; + +fn build(n: usize) -> !^[]mut i32 { + var cells: ^[]mut i32 = try mem.alloc_slice(i32, n); + var i: usize = 0; + while i < n { + cells.^[i] = i as i32; + i = i + 1; + } + return cells; +} + +fn main() -> i32 { + let cells: ^[]mut i32 = build(100) catch |e| { return 1; }; + defer { mem.destroy(cells); } + var sum: i32 = 0; + for v in cells.^ { + sum = sum + v.^; + } + var buf: [16]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let k: usize = fmt.fmt_i32(buf[..], sum); + io.print("sum "); + io.print(buf[0..k]); + io.print("\n"); + return 0; +} diff --git a/fec/tests/exec/hello.fe b/fec/tests/exec/hello.fe new file mode 100644 index 0000000..e39fcb6 --- /dev/null +++ b/fec/tests/exec/hello.fe @@ -0,0 +1,9 @@ +// EXIT:0 +// OUTPUT:hello from ferro +unit hello; +import std.io; + +fn main() -> i32 { + io.println("hello from ferro"); + return 0; +} diff --git a/fec/tests/exec/interns.fe b/fec/tests/exec/interns.fe new file mode 100644 index 0000000..af68a10 --- /dev/null +++ b/fec/tests/exec/interns.fe @@ -0,0 +1,73 @@ +// EXIT:0 +// OUTPUT:ids 0 1 2 count 3 +// OUTPUT:again 0 same yes count 3 +// OUTPUT:eq yes no len 4 +// OUTPUT:find 1 missing yes +// OUTPUT:hash steady yes apart yes +// OUTPUT:copied unit 4 +// OUTPUT:scope x 10 y 20 of 2 +// OUTPUT:balanced +unit interns; + +import std.io; +import std.sys; +import std.intern; +import std.map; + +// One copy of every distinct name, and a number that stands for it. Comparing +// two names is comparing two integers; storing one costs four bytes and no +// ownership. +// +// There is deliberately no way to get a `str` back out: a borrow of the text +// would be a borrow of the interner, and the interner is exactly what a parser +// wants to keep adding to while it holds names. + +fn run() -> !void { + var t: intern.Interner = try intern.Interner.with_capacity(8); + let a: intern.StrId = try t.intern("unit"); + let b: intern.StrId = try t.intern("fn"); + let c: intern.StrId = try t.intern("struct"); + @print("ids {} {} {} count {}\n", a.raw, b.raw, c.raw, t.count_of()); + + // The same name twice is the same number, and costs nothing new. + let again: intern.StrId = try t.intern("unit"); + @print("again {} same {} count {}\n", again.raw, yesno(a.same(again)), + t.count_of()); + + @print("eq {} {} len {}\n", yesno(t.eq(a, "unit")), yesno(t.eq(a, "fn")), + t.len_of(a)); + + @print("find {} missing {}\n", t.find("fn"), + yesno(t.find("nope") == intern.NONE)); + + // A name hashes the same every time, and two names do not collide here. + @print("hash steady {} apart {}\n", yesno(t.hash_of(a) == t.hash_of(again)), + yesno(t.hash_of(a) != t.hash_of(b))); + + // The only way to see the text: copy it somewhere you own. + var buf: [8]u8 = undefined; + let n: usize = t.copy_into(a, buf[..]); + @print("copied {} {}\n", buf[0..n], n); + + // A symbol table is a Map keyed on the name's number. std.map keys on + // bytes, so no separate integer-keyed map is needed. + var scope: map.Map(i32) = try map.Map(i32).with_capacity(8); + var key: [4]u8 = undefined; + try scope.put(intern.key_of(a, key[..]), 10); + try scope.put(intern.key_of(b, key[..]), 20); + @print("scope x {} y {} of {}\n", scope.get(intern.key_of(a, key[..]), -1), + scope.get(intern.key_of(b, key[..]), -1), scope.count_of()); + return; +} + +fn yesno(b: bool) -> []u8 { + if b { return "yes"; } + return "no"; +} + +fn main() -> i32 { + run() catch |e| { @print("failed\n"); return 1; }; + if sys.allocs() == sys.frees() { @print("balanced\n"); } + else { @print("leaked\n"); } + return 0; +} diff --git a/fec/tests/exec/lexer/README.md b/fec/tests/exec/lexer/README.md new file mode 100644 index 0000000..176c2c5 --- /dev/null +++ b/fec/tests/exec/lexer/README.md @@ -0,0 +1,15 @@ +# Ferro 로 쓴 Ferro 프런트엔드 + +셀프호스팅의 강제 함수. 언어가 자기 컴파일러를 쓸 만한지는 써 봐야 안다. + +| 파일 | 무엇 | +|---|---| +| `tok.fe` | 토큰. 텍스트를 들지 않고 소스 안의 위치를 든다 (R4) | +| `scan.fe` | 렉서. `next(src, &mut at, &mut line)` | +| `main.fe` | 렉서 프로그램. 종류별로 세고 몇 개를 찍는다 | +| `ast.fe` | 노드. 자식은 노드 배열 안의 인덱스다 | +| `parse.fe` | 재귀 하강 파서. 토큰 하나를 앞서 본다 | +| `tree.fe` | 파서 프로그램. 식을 전위 표기로 다시 찍는다 | + +전위 표기로 찍는 것이 요점이다. `1 + 2 * 3` 이 `(+ 1 (* 2 3))` 로 나오는 것 +말고는 우선순위가 맞았는지 볼 방법이 없다. diff --git a/fec/tests/exec/lexer/ast.fe b/fec/tests/exec/lexer/ast.fe new file mode 100644 index 0000000..e27c56c --- /dev/null +++ b/fec/tests/exec/lexer/ast.fe @@ -0,0 +1,53 @@ +unit ast; + +// The tree the parser builds. +// +// Nodes live in one growing array and refer to each other by index, which is +// what SPEC R11 asks for: an owner that holds the values and handles that +// point at them. A node cannot hold a `^Node` for its children because a node +// has several and they are not each owned once; it cannot hold a `&Node` +// because R4 keeps borrows out of aggregate storage. An index is neither. + +pub enum Shape { + Unit, // a: name + Fn, // a: name, b: first statement + Let, // a: name, b: value + Return, // a: value, or NONE + Binary, // a: left, b: right, from/len: the operator + Number, + Name, + Text, + Error, +} + +/// No node. Zero is a real index, so the empty handle is the largest one. +pub const NONE: usize = 4294967295; + +pub struct Node { + pub shape: Shape, + pub from: usize, // where in the source this came from + pub len: usize, + pub line: usize, + pub a: usize, // handles into the same tree + pub b: usize, + pub next: usize, // the following statement, when there is one + /// What a `Name` resolved to: the handle of the `Let` or `Fn` that + /// declared it. The resolver writes this back so the tree carries its own + /// answers and nothing has to look the name up a second time. + pub bind: usize, +} + +pub fn name_of(s: Shape) -> []u8 { + match s { + Unit => { return "unit"; } + Fn => { return "fn"; } + Let => { return "let"; } + Return => { return "return"; } + Binary => { return "binary"; } + Number => { return "number"; } + Name => { return "name"; } + Text => { return "text"; } + Error => { return "error"; } + } + return "?"; +} diff --git a/fec/tests/exec/lexer/main.fe b/fec/tests/exec/lexer/main.fe new file mode 100644 index 0000000..032dfa1 --- /dev/null +++ b/fec/tests/exec/lexer/main.fe @@ -0,0 +1,61 @@ +// EXIT:0 +// OUTPUT:first keyword unit @1 +// OUTPUT:number 42 @3 +// OUTPUT:text "hi" @3 +// OUTPUT:arrow -> @5 +// OUTPUT:keyword 6 name 7 number 1 text 1 punct 15 +// OUTPUT:total 30 +unit main; +import std.io; +import tok; +import scan; + +// The Ferro lexer, written in Ferro. This is the shape a self-hosted `fec` +// would take: read a source, hand back tokens, say where each came from. + +const SOURCE: str = "unit demo;\n\nfn answer() { let n = 42; let s = \"hi\"; }\n// a comment\nfn arrow() -> i32 { return n; }\n"; + +fn main() -> i32 { + var at: usize = 0; + var line: usize = 1; + var keywords: usize = 0; + var names: usize = 0; + var numbers: usize = 0; + var texts: usize = 0; + var puncts: usize = 0; + var total: usize = 0; + var first: bool = true; + while true { + let t: tok.Token = scan.next(SOURCE, &mut at, &mut line); + if t.kind == tok.Kind.End { break; } + total = total + 1; + if first { + @print("first {} {} @{}\n", tok.name_of(t.kind), + tok.text(SOURCE, t), t.line); + first = false; + } + match t.kind { + Keyword => { keywords = keywords + 1; } + Name => { names = names + 1; } + Number => { + numbers = numbers + 1; + @print("number {} @{}\n", tok.text(SOURCE, t), t.line); + } + Text => { + texts = texts + 1; + @print("text {} @{}\n", tok.text(SOURCE, t), t.line); + } + Punct => { + puncts = puncts + 1; + if t.len == 2 { + @print("arrow {} @{}\n", tok.text(SOURCE, t), t.line); + } + } + _ => { @print("unexpected {}\n", tok.name_of(t.kind)); } + } + } + @print("keyword {} name {} number {} text {} punct {}\n", + keywords, names, numbers, texts, puncts); + @print("total {}\n", total); + return 0; +} diff --git a/fec/tests/exec/lexer/parse.fe b/fec/tests/exec/lexer/parse.fe new file mode 100644 index 0000000..e991c51 --- /dev/null +++ b/fec/tests/exec/lexer/parse.fe @@ -0,0 +1,256 @@ +unit parse; + +import std.list; +import std.str; +import tok; +import scan; +import ast; + +// Recursive descent over the lexer's tokens. +// +// The source is not a field: R4 keeps borrows out of aggregate storage, so +// `src` is passed to every step, the same way the lexer passes it. What the +// parser does own is the node array, and every parent points at its children +// by index into it. +// +// One token of lookahead lives in `cur`. That is all this grammar needs. + +pub struct Parser { + nodes: list.List(ast.Node), + at: usize, + line: usize, + cur: tok.Token, + pub errors: usize, + + pub fn on(src: []u8) -> !Self { + var p: Self = Self{ + nodes: try list.List(ast.Node).with_capacity(16), + at: 0, + line: 1, + cur: tok.Token{ kind: tok.Kind.End, from: 0, len: 0, line: 1 }, + errors: 0, + }; + p.bump(src); + return p; + } + + fn bump(self: &mut Self, src: []u8) -> void { + self.cur = scan.next(src, &mut self.at, &mut self.line); + return; + } + + fn done(self: &Self) -> bool { return self.cur.kind == tok.Kind.End; } + + /// Is the token in hand this exact spelling? + fn is(self: &Self, src: []u8, want: []u8) -> bool { + return str.eq(tok.text(src, self.cur), want); + } + + /// Take the token in hand if it is this spelling; say whether it was. + fn eat(self: &mut Self, src: []u8, want: []u8) -> bool { + if not self.is(src, want) { return false; } + self.bump(src); + return true; + } + + /// Demand this spelling. A miss is counted and the token stays put, so the + /// caller decides how to get back on its feet. + fn want(self: &mut Self, src: []u8, w: []u8) -> bool { + if self.eat(src, w) { return true; } + self.errors = self.errors + 1; + return false; + } + + fn add(self: &mut Self, s: ast.Shape, t: tok.Token, a: usize, + b: usize) -> !usize { + let n: ast.Node = ast.Node{ + shape: s, from: t.from, len: t.len, line: t.line, + a: a, b: b, next: ast.NONE, + // The parser does not resolve names; the resolver writes this. + bind: ast.NONE, + }; + let i: usize = self.nodes.count(); + try self.nodes.push(n); + return i; + } + + // -- reading the tree back ------------------------------------------ + + pub fn count(self: &Self) -> usize { return self.nodes.count(); } + + pub fn node(self: &Self, i: usize) -> ast.Node { + return self.nodes.at(i); + } + + + // -- the grammar ---------------------------------------------------- + // + // unit := "unit" NAME ";" item* + // item := "fn" NAME "(" ")" block + // block := "{" stmt* "}" + // stmt := "let" NAME "=" expr ";" | "return" expr? ";" + // expr := term (("+" | "-") term)* + // term := factor (("*" | "/") factor)* + // factor := NUMBER | NAME | TEXT | "(" expr ")" + + pub fn unit_decl(self: &mut Self, src: []u8) -> !usize { + let ok: bool = self.want(src, "unit"); + let name: tok.Token = self.cur; + if ok { self.bump(src); } + let semi: bool = self.want(src, ";"); + let root: usize = try self.add(ast.Shape.Unit, name, ast.NONE, + ast.NONE); + var first: usize = ast.NONE; + var last: usize = ast.NONE; + while not self.done() { + let it: usize = try self.item(src); + if first == ast.NONE { first = it; } + else { self.link(last, it); } + last = it; + } + self.set_a(root, first); + return root; + } + + /// Point one statement or item at the one after it. + fn link(self: &mut Self, from: usize, to: usize) -> void { + var n: ast.Node = self.nodes.at(from); + n.next = to; + self.nodes.set(from, n); + return; + } + + fn set_a(self: &mut Self, at: usize, a: usize) -> void { + var n: ast.Node = self.nodes.at(at); + n.a = a; + self.nodes.set(at, n); + return; + } + + fn item(self: &mut Self, src: []u8) -> !usize { + if not self.is(src, "fn") { + let bad: tok.Token = self.cur; + self.errors = self.errors + 1; + self.skip_stmt(src); + return try self.add(ast.Shape.Error, bad, ast.NONE, ast.NONE); + } + self.bump(src); + let name: tok.Token = self.cur; + self.bump(src); + let open: bool = self.want(src, "("); + let close: bool = self.want(src, ")"); + let body: usize = try self.block(src); + return try self.add(ast.Shape.Fn, name, ast.NONE, body); + } + + fn block(self: &mut Self, src: []u8) -> !usize { + let open: bool = self.want(src, "{"); + var first: usize = ast.NONE; + var last: usize = ast.NONE; + while true { + if self.done() { break; } + if self.is(src, "}") { break; } + let s: usize = try self.stmt(src); + if first == ast.NONE { first = s; } + else { self.link(last, s); } + last = s; + } + let close: bool = self.want(src, "}"); + return first; + } + + fn stmt(self: &mut Self, src: []u8) -> !usize { + if self.is(src, "let") { + self.bump(src); + let name: tok.Token = self.cur; + self.bump(src); + let has_eq: bool = self.want(src, "="); + let value: usize = try self.expr(src); + let semi: bool = self.want(src, ";"); + return try self.add(ast.Shape.Let, name, ast.NONE, value); + } + if self.is(src, "return") { + let head: tok.Token = self.cur; + self.bump(src); + if self.is(src, ";") { + self.bump(src); + return try self.add(ast.Shape.Return, head, ast.NONE, + ast.NONE); + } + let value: usize = try self.expr(src); + let semi: bool = self.want(src, ";"); + return try self.add(ast.Shape.Return, head, value, ast.NONE); + } + let bad: tok.Token = self.cur; + self.errors = self.errors + 1; + self.skip_stmt(src); + return try self.add(ast.Shape.Error, bad, ast.NONE, ast.NONE); + } + + /// Get back to a statement boundary after something unrecognised. Stopping + /// at `;` or `}` means one bad statement costs one diagnostic, not a run + /// of them. + fn skip_stmt(self: &mut Self, src: []u8) -> void { + while not self.done() { + if self.is(src, "}") { return; } + if self.is(src, ";") { self.bump(src); return; } + self.bump(src); + } + return; + } + + fn expr(self: &mut Self, src: []u8) -> !usize { + var left: usize = try self.term(src); + while true { + if self.done() { break; } + let plus: bool = self.is(src, "+"); + let minus: bool = self.is(src, "-"); + if not plus and not minus { break; } + let op: tok.Token = self.cur; + self.bump(src); + let right: usize = try self.term(src); + left = try self.add(ast.Shape.Binary, op, left, right); + } + return left; + } + + fn term(self: &mut Self, src: []u8) -> !usize { + var left: usize = try self.factor(src); + while true { + if self.done() { break; } + let star: bool = self.is(src, "*"); + let slash: bool = self.is(src, "/"); + if not star and not slash { break; } + let op: tok.Token = self.cur; + self.bump(src); + let right: usize = try self.factor(src); + left = try self.add(ast.Shape.Binary, op, left, right); + } + return left; + } + + fn factor(self: &mut Self, src: []u8) -> !usize { + let t: tok.Token = self.cur; + if t.kind == tok.Kind.Number { + self.bump(src); + return try self.add(ast.Shape.Number, t, ast.NONE, ast.NONE); + } + if t.kind == tok.Kind.Name { + self.bump(src); + return try self.add(ast.Shape.Name, t, ast.NONE, ast.NONE); + } + if t.kind == tok.Kind.Text { + self.bump(src); + return try self.add(ast.Shape.Text, t, ast.NONE, ast.NONE); + } + if self.is(src, "(") { + self.bump(src); + let inner: usize = try self.expr(src); + let close: bool = self.want(src, ")"); + return inner; + } + self.errors = self.errors + 1; + self.bump(src); + return try self.add(ast.Shape.Error, t, ast.NONE, ast.NONE); + } +} diff --git a/fec/tests/exec/lexer/scan.fe b/fec/tests/exec/lexer/scan.fe new file mode 100644 index 0000000..81e9ac5 --- /dev/null +++ b/fec/tests/exec/lexer/scan.fe @@ -0,0 +1,122 @@ +unit scan; +import tok; + +// A scanner over a byte slice. The position travels in a `&mut usize` beside +// the source rather than inside a struct with it, because a struct cannot hold +// a slice: a slice is a borrowed view and R4 keeps borrows out of aggregates. + +fn is_space(c: u8) -> bool { return c == 32 or c == 9 or c == 13 or c == 10; } +fn is_digit(c: u8) -> bool { return c >= 48 and c <= 57; } + +fn is_name_start(c: u8) -> bool { + if c >= 97 and c <= 122 { return true; } + if c >= 65 and c <= 90 { return true; } + return c == 95; +} + +fn is_name_part(c: u8) -> bool { + return is_name_start(c) or is_digit(c); +} + +const KEYWORDS: usize = 12; + +fn is_keyword(word: []u8) -> bool { + if same(word, "unit") { return true; } + if same(word, "import") { return true; } + if same(word, "pub") { return true; } + if same(word, "fn") { return true; } + if same(word, "struct") { return true; } + if same(word, "enum") { return true; } + if same(word, "let") { return true; } + if same(word, "var") { return true; } + if same(word, "if") { return true; } + if same(word, "else") { return true; } + if same(word, "while") { return true; } + if same(word, "return") { return true; } + return false; +} + +fn same(a: []u8, b: []u8) -> bool { + if a.n != b.n { return false; } + var i: usize = 0; + while i < a.n { + if a[i] != b[i] { return false; } + i = i + 1; + } + return true; +} + +/// Step over anything that is not a token: spaces, newlines, and `//` to the +/// end of the line. `line` counts what was crossed so a token can say where it +/// came from. +fn skip_gaps(src: []u8, at: &mut usize, line: &mut usize) -> void { + while at.^ < src.n { + let c: u8 = src[at.^]; + if c == 10 { line.^ = line.^ + 1; at.^ = at.^ + 1; } + else if is_space(c) { at.^ = at.^ + 1; } + else if c == 47 and at.^ + 1 < src.n and src[at.^ + 1] == 47 { + while at.^ < src.n { + if src[at.^] == 10 { break; } + at.^ = at.^ + 1; + } + } + else { break; } + } +} + +pub fn next(src: []u8, at: &mut usize, line: &mut usize) -> tok.Token { + skip_gaps(src, at, line); + let start: usize = at.^; + let where: usize = line.^; + if start >= src.n { + return tok.Token{ kind: tok.Kind.End, from: start, len: 0, line: where }; + } + let c: u8 = src[start]; + if is_name_start(c) { + while at.^ < src.n { + if not is_name_part(src[at.^]) { break; } + at.^ = at.^ + 1; + } + let word: []u8 = src[start..at.^]; + var kind: tok.Kind = tok.Kind.Name; + if is_keyword(word) { kind = tok.Kind.Keyword; } + return tok.Token{ kind: kind, from: start, len: at.^ - start, + line: where }; + } + if is_digit(c) { + while at.^ < src.n { + if not is_digit(src[at.^]) { break; } + at.^ = at.^ + 1; + } + return tok.Token{ kind: tok.Kind.Number, from: start, + len: at.^ - start, line: where }; + } + if c == 34 { + at.^ = at.^ + 1; + while at.^ < src.n { + if src[at.^] == 34 { break; } + if src[at.^] == 92 and at.^ + 1 < src.n { at.^ = at.^ + 1; } + at.^ = at.^ + 1; + } + if at.^ >= src.n { + return tok.Token{ kind: tok.Kind.Bad, from: start, + len: at.^ - start, line: where }; + } + at.^ = at.^ + 1; + return tok.Token{ kind: tok.Kind.Text, from: start, len: at.^ - start, + line: where }; + } + at.^ = at.^ + 1; + // Two-byte punctuation the language actually uses. + if at.^ < src.n { + let d: u8 = src[at.^]; + if c == 45 and d == 62 { at.^ = at.^ + 1; } + else if c == 61 and d == 61 { at.^ = at.^ + 1; } + else if c == 33 and d == 61 { at.^ = at.^ + 1; } + else if c == 60 and d == 61 { at.^ = at.^ + 1; } + else if c == 62 and d == 61 { at.^ = at.^ + 1; } + else if c == 46 and d == 46 { at.^ = at.^ + 1; } + } + return tok.Token{ kind: tok.Kind.Punct, from: start, len: at.^ - start, + line: where }; +} diff --git a/fec/tests/exec/lexer/tok.fe b/fec/tests/exec/lexer/tok.fe new file mode 100644 index 0000000..a5b9cb3 --- /dev/null +++ b/fec/tests/exec/lexer/tok.fe @@ -0,0 +1,39 @@ +unit tok; + +// What the lexer produces. A token does not hold the text it came from: R4 +// keeps borrows out of aggregate storage, so it records where in the source it +// starts and how long it is, and the source travels beside it. + +pub enum Kind { + End, + Name, + Number, + Text, + Punct, + Keyword, + Bad, +} + +pub struct Token { + pub kind: Kind, + pub from: usize, + pub len: usize, + pub line: usize, +} + +pub fn text(src: []u8, t: Token) -> []u8 { + return src[t.from..t.from + t.len]; +} + +pub fn name_of(k: Kind) -> []u8 { + match k { + End => { return "end"; } + Name => { return "name"; } + Number => { return "number"; } + Text => { return "text"; } + Punct => { return "punct"; } + Keyword => { return "keyword"; } + Bad => { return "bad"; } + } + return "?"; +} diff --git a/fec/tests/exec/lexer/tree.fe b/fec/tests/exec/lexer/tree.fe new file mode 100644 index 0000000..4f7e2e6 --- /dev/null +++ b/fec/tests/exec/lexer/tree.fe @@ -0,0 +1,107 @@ +// EXIT:0 +// OUTPUT:unit demo +// OUTPUT:fn answer @2 +// OUTPUT: let n = (+ 1 (* 2 3)) +// OUTPUT: return n +// OUTPUT:fn greet @3 +// OUTPUT: let s = "hi" +// OUTPUT: return +// OUTPUT:fn muddle @4 +// OUTPUT: let x = +// OUTPUT:nodes 17 errors 2 +// OUTPUT:balanced +unit tree; + +import std.io; +import std.sys; +import tok; +import ast; +import parse; + +// The Ferro parser, written in Ferro. +// +// The lexer next to this file showed that a token can say where it came from +// instead of holding the text. A tree is the same idea one level up: a node +// cannot hold `^Node` children -- it has several and owns none of them once -- +// and R4 keeps `&Node` out of aggregate storage. So the parser owns one array +// of nodes and every child is an index into it. +// +// Printing the expressions back in prefix form is the point of the test: it is +// the only way to see that `1 + 2 * 3` bound the way the grammar says. + +const SOURCE: str = "unit demo;\nfn answer() { let n = 1 + 2 * 3; return n; }\nfn greet() { let s = \"hi\"; return; }\nfn muddle() { let x = ; }\n"; + +fn show_expr(p: &parse.Parser, src: []u8, i: usize) -> void { + if i == ast.NONE { return; } + let n: ast.Node = p.node(i); + match n.shape { + Binary => { + @print("({} ", src[n.from..n.from + n.len]); + show_expr(p, src, n.a); + @print(" "); + show_expr(p, src, n.b); + @print(")"); + } + Error => { @print(""); } + _ => { @print("{}", src[n.from..n.from + n.len]); } + } + return; +} + +fn show_stmt(p: &parse.Parser, src: []u8, i: usize) -> void { + let n: ast.Node = p.node(i); + match n.shape { + Let => { + @print(" let {} = ", src[n.from..n.from + n.len]); + show_expr(p, src, n.b); + @print("\n"); + } + Return => { + if n.a == ast.NONE { @print(" return\n"); } + else { + @print(" return "); + show_expr(p, src, n.a); + @print("\n"); + } + } + _ => { @print(" \n"); } + } + return; +} + +fn show_item(p: &parse.Parser, src: []u8, i: usize) -> void { + let n: ast.Node = p.node(i); + match n.shape { + Fn => { + @print("fn {} @{}\n", src[n.from..n.from + n.len], n.line); + var s: usize = n.b; + while s != ast.NONE { + show_stmt(p, src, s); + s = p.node(s).next; + } + } + _ => { @print(" \n"); } + } + return; +} + +fn run() -> !void { + var p: parse.Parser = try parse.Parser.on(SOURCE); + let root: usize = try p.unit_decl(SOURCE); + let head: ast.Node = p.node(root); + @print("unit {}\n", SOURCE[head.from..head.from + head.len]); + var it: usize = head.a; + while it != ast.NONE { + show_item(&p, SOURCE, it); + it = p.node(it).next; + } + @print("nodes {} errors {}\n", p.count(), p.errors); + return; +} + +fn main() -> i32 { + run() catch |e| { @print("out of memory\n"); return 1; }; + if sys.allocs() == sys.frees() { @print("balanced\n"); } + else { @print("leaked {}\n", sys.allocs() - sys.frees()); } + return 0; +} diff --git a/fec/tests/exec/listmor.fe b/fec/tests/exec/listmor.fe new file mode 100644 index 0000000..dda291e --- /dev/null +++ b/fec/tests/exec/listmor.fe @@ -0,0 +1,55 @@ +// EXIT:0 +// OUTPUT:walk 6 count 3 +// OUTPUT:swapped 3 1 +// OUTPUT:took 2 left 9 +// OUTPUT:pop 1 then 9 empty -1 +// OUTPUT:cleared 0 room 4 +// OUTPUT:balanced +unit listmor; + +import std.io; +import std.sys; +import std.list; + +// The rest of the List surface a compiler needs: walk it, exchange two, move +// one out and leave something valid behind, take the last off, and empty it +// without going back to the allocator. + +fn run() -> !void { + var xs: list.List(i32) = try list.List(i32).with_capacity(4); + try xs.push(1); + try xs.push(2); + try xs.push(3); + + // `slice()` is a shared view derived from `self` (SPEC 5 R8(a)), which is + // what lets `for` walk it. + var sum: i32 = 0; + for x in xs.slice() { sum = sum + x.^; } + @print("walk {} count {}\n", sum, xs.count()); + + xs.swap(0, 2); + @print("swapped {} {}\n", xs.at(0), xs.at(2)); + + // SPEC 5 R7: what leaves a projection leaves a replacement behind. + let old: i32 = xs.take(1, 9); + @print("took {} left {}\n", old, xs.at(1)); + + let a: i32 = xs.pop() orelse -1; + let b: i32 = xs.pop() orelse -1; + let c: i32 = xs.pop() orelse -1; + let d: i32 = xs.pop() orelse -1; + @print("pop {} then {} empty {}\n", a, b, d); + + try xs.push(7); + let room: usize = 4; + xs.clear(); + @print("cleared {} room {}\n", xs.count(), room); + return; +} + +fn main() -> i32 { + run() catch |e| { @print("failed\n"); return 1; }; + if sys.allocs() == sys.frees() { @print("balanced\n"); } + else { @print("leaked\n"); } + return 0; +} diff --git a/fec/tests/exec/listuse.fe b/fec/tests/exec/listuse.fe new file mode 100644 index 0000000..2c0d703 --- /dev/null +++ b/fec/tests/exec/listuse.fe @@ -0,0 +1,42 @@ +// EXIT:0 +// OUTPUT:count 20 sum 190 balanced +unit listuse; +import std.io; +import std.fmt; +import std.list; +import std.sys; + +fn show(label: []u8, v: i32) -> void { + var buf: [16]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let n: usize = fmt.fmt_i32(buf[..], v); + io.print(label); + io.print(" "); + io.print(buf[0..n]); + io.print(" "); +} + +fn build() -> !i32 { + var xs: list.List(i32) = try list.List(i32).with_capacity(2); + var i: i32 = 0; + while i < 20 { + try xs.push(i); + i = i + 1; + } + var sum: i32 = 0; + var k: usize = 0; + while k < xs.count() { + sum = sum + xs.at(k); + k = k + 1; + } + show("count", xs.count() as i32); + show("sum", sum); + return sum; +} + +fn main() -> i32 { + let sum: i32 = build() catch |e| { return 1; }; + if sum != 190 { return 2; } + if sys.allocs() != sys.frees() { io.print("leaked\n"); return 3; } + io.print("balanced\n"); + return 0; +} diff --git a/fec/tests/exec/logic.fe b/fec/tests/exec/logic.fe new file mode 100644 index 0000000..dc65c94 --- /dev/null +++ b/fec/tests/exec/logic.fe @@ -0,0 +1,12 @@ +// EXIT:1 +unit logic; + +fn side(v: i32) -> bool { return v > 0; } + +fn main() -> i32 { + let a: bool = true and side(1); + let b: bool = false or side(2); + let c: bool = not side(0); + if a and b and c { return 1; } + return 0; +} diff --git a/fec/tests/exec/loopcall.fe b/fec/tests/exec/loopcall.fe new file mode 100644 index 0000000..f3b3bb1 --- /dev/null +++ b/fec/tests/exec/loopcall.fe @@ -0,0 +1,14 @@ +// EXIT:55 +unit loopcall; + +fn add(a: i32, b: i32) -> i32 { return a + b; } + +fn main() -> i32 { + var total: i32 = 0; + var i: i32 = 0; + while i < 10 { + total = total + add(i, 1); + i = i + 1; + } + return total; +} diff --git a/fec/tests/exec/maps.fe b/fec/tests/exec/maps.fe new file mode 100644 index 0000000..8c0d8a4 --- /dev/null +++ b/fec/tests/exec/maps.fe @@ -0,0 +1,70 @@ +// EXIT:0 +// OUTPUT:count 5 +// OUTPUT:fn 2 let 3 missing 0 +// OUTPUT:grown 64 +// OUTPUT:after 40 +// OUTPUT:cleared 0 room 64 gone 0 +// OUTPUT:refilled 3 +// OUTPUT:balanced +unit maps; +import std.io; +import std.map; +import std.fmt; +import std.sys; + +fn run() -> !void { + var seen: map.Map(i32) = try map.Map(i32).with_capacity(4); + try seen.put("unit", 1); + try seen.put("fn", 2); + try seen.put("let", 3); + try seen.put("struct", 4); + try seen.put("return", 5); + @print("count {}\n", seen.count_of()); + @print("fn {} let {} missing {}\n", seen.get("fn", 0), seen.get("let", 0), + seen.get("nope", 0)); + // Enough keys to make it grow more than once. + var buf: [8]u8 = undefined; + var i: usize = 0; + while i < 40 { + let n: usize = fmt.fmt_i32(buf[..], i as i32); + try seen.put(buf[0..n], (i as i32) + 100); + i = i + 1; + } + @print("grown {}\n", seen.room()); + var found: usize = 0; + i = 0; + while i < 40 { + let n: usize = fmt.fmt_i32(buf[..], i as i32); + if seen.get(buf[0..n], 0) == (i as i32) + 100 { found = found + 1; } + i = i + 1; + } + @print("after {}\n", found); + + // `clear` forgets every key and keeps the storage, which is what a scope + // that ends wants: hand the table to the next one without going back to + // the allocator. The room has to survive and the keys have to not. + let room: usize = seen.room(); + seen.clear(); + var gone: usize = 0; + i = 0; + while i < 40 { + let n: usize = fmt.fmt_i32(buf[..], i as i32); + if seen.has(buf[0..n]) { gone = gone + 1; } + i = i + 1; + } + @print("cleared {} room {} gone {}\n", seen.count_of(), room, gone); + + // And it is usable again afterwards, reusing the same buffers. + try seen.put("a", 1); + try seen.put("b", 2); + try seen.put("c", 3); + @print("refilled {}\n", seen.count_of()); + return; +} + +fn main() -> i32 { + run() catch |e| { @print("failed\n"); return 1; }; + if sys.allocs() != sys.frees() { @print("leaked\n"); return 2; } + @print("balanced\n"); + return 0; +} diff --git a/fec/tests/exec/narrow.fe b/fec/tests/exec/narrow.fe new file mode 100644 index 0000000..bf7c30d --- /dev/null +++ b/fec/tests/exec/narrow.fe @@ -0,0 +1,32 @@ +// EXIT:0 +// OUTPUT:a 5 b 200 c 44 d 9 +// OUTPUT:e 5 f 1 g 65535 +// OUTPUT:sum 253 +unit narrow; + +import std.io; + +// How wide a store is belongs to the place, not to the value. An integer +// literal is `i32` until something narrower asks for it, so `let b: u8 = 200;` +// arrives at the store as four bytes going into one -- and writing four wipes +// out whatever the frame put beside it. +// +// Several locals of mixed width, next to each other, is what it takes to see +// it: each narrow store used to reach back over the one declared before it. + +fn main() -> i32 { + let a: i32 = 5; + let b: u8 = 200; + let c: u8 = 44; + let d: i16 = 9; + @print("a {} b {} c {} d {}\n", a, b, c, d); + + let e = 5; // no annotation: i32 (SPEC 4.1) + let f: i8 = 1; + let g: u16 = 65535; + @print("e {} f {} g {}\n", e, f as i32, g); + + // And the values are still there after everything else was written. + @print("sum {}\n", (b as i32) + (c as i32) + (d as i32) + e - (a as i32)); + return 0; +} diff --git a/fec/tests/exec/optional.fe b/fec/tests/exec/optional.fe new file mode 100644 index 0000000..8908b84 --- /dev/null +++ b/fec/tests/exec/optional.fe @@ -0,0 +1,15 @@ +// EXIT:42 +unit optional; + +fn pick(flag: bool) -> ?i32 { + if flag { return 42; } + return null; +} + +fn main() -> i32 { + let a: ?i32 = pick(true); + let b: ?i32 = pick(false); + let x: i32 = a orelse 0; + let y: i32 = b orelse 0; + return x + y; +} diff --git a/fec/tests/exec/optref.fe b/fec/tests/exec/optref.fe new file mode 100644 index 0000000..50725ae --- /dev/null +++ b/fec/tests/exec/optref.fe @@ -0,0 +1,93 @@ +// EXIT:0 +// OUTPUT:int 5 none 0 +// OUTPUT:own 5 absent -1 +// OUTPUT:ref 5 absent yes +// OUTPUT:cmp yes no +// OUTPUT:unwrap 5 +// OUTPUT:balanced +unit optref; + +import std.io; +import std.sys; +import std.mem; + +// An optional whose payload cannot be null keeps no separate tag: the payload's +// own impossible value is `null`. `?i32` carries a tag; `?^T` and `?&T` do not. +// +// Both shapes have to answer the same questions, and the pointer-shaped ones +// were answering with the address of the slot the pointer sits in rather than +// with the pointer -- one level of indirection too many, in code that compiled +// and ran. + +fn opt_int(on: bool) -> ?i32 { + if on { return 5; } + return null; +} + +/// A niche optional over an owner. The `if let` binding is `&i32` -- the +/// pointer itself, not where it is kept -- and `.?` moves the owner out so it +/// can be let go of. +fn take(o: ?^i32) -> i32 { + var box: ?^i32 = o; + var v: i32 = -1; + if let Some(p) = box { v = p.^; } + if box == null { return v; } + // SPEC 5 R7: a non-Copy value leaves a projection through `mem.replace`, + // which puts something valid back where it was. + let owner: ^i32 = mem.replace(&mut box, null).?; + mem.destroy(owner); + return v; +} + +fn make(v: i32) -> ?^i32 { + let p: ^i32 = mem.create(v) catch |e| { return null; }; + return p; +} + +struct Bag { + items: ^[]mut i32, + + /// SPEC 5 R8(a): derived from `self`, so the borrow is the caller's. + fn at(self: &Self, i: usize) -> ?&i32 { + if i >= self.items.^.n { return null; } + return &self.items.^[i]; + } +} + +fn run() -> !void { + // A tag in front of the payload. + var got: i32 = 0; + if let Some(a) = opt_int(true) { got = a; } + let none: i32 = opt_int(false) orelse 0; + @print("int {} none {}\n", got, none); + + // A niche over `^i32`. + @print("own {} absent {}\n", take(make(5)), take(null)); + + // A niche over `&i32`. + let room: ^[]mut i32 = try mem.alloc_slice(i32, 2); + room.^[0] = 5; + let b: Bag = Bag{ items: room }; + var seen: i32 = 0; + if let Some(r) = b.at(0) { seen = r.^; } + var past: bool = false; + if let Some(r) = b.at(9) { seen = seen; } else { past = true; } + @print("ref {} absent {}\n", seen, yesno(past)); + + // Comparing against null reads the tag, not the bytes of the wrapper. + @print("cmp {} {}\n", yesno(b.at(9) == null), yesno(b.at(0) == null)); + @print("unwrap {}\n", b.at(0).?.^); + return; +} + +fn yesno(b: bool) -> []u8 { + if b { return "yes"; } + return "no"; +} + +fn main() -> i32 { + run() catch |e| { @print("failed\n"); return 1; }; + if sys.allocs() == sys.frees() { @print("balanced\n"); } + else { @print("leaked\n"); } + return 0; +} diff --git a/fec/tests/exec/owndrop.fe b/fec/tests/exec/owndrop.fe new file mode 100644 index 0000000..f12cabd --- /dev/null +++ b/fec/tests/exec/owndrop.fe @@ -0,0 +1,28 @@ +// EXIT:0 +// OUTPUT:balanced +unit owndrop; +import std.io; +import std.sys; + +fn take(p: ^i32) -> void { mem.destroy(p); } + +fn scoped() -> !void { + let a: ^i32 = try mem.create(1); + let b: ^i32 = try mem.create(2); + take(b); +} + +fn early(flag: bool) -> !void { + let p: ^i32 = try mem.create(3); + if flag { return; } + take(p); +} + +fn main() -> i32 { + scoped() catch |e| { return 1; }; + early(true) catch |e| { return 2; }; + early(false) catch |e| { return 3; }; + if sys.allocs() == sys.frees() { io.print("balanced\n"); return 0; } + io.print("leaked\n"); + return 4; +} diff --git a/fec/tests/exec/patterns.fe b/fec/tests/exec/patterns.fe new file mode 100644 index 0000000..dfd7eba --- /dev/null +++ b/fec/tests/exec/patterns.fe @@ -0,0 +1,35 @@ +// EXIT:0 +// OUTPUT:some 7 +// OUTPUT:none +// OUTPUT:point 3 4 +// OUTPUT:empty +unit patterns; + +enum Shape { + Empty, + Point(i32), + Pair { x: i32, y: i32 }, +} + +fn pick(flag: bool) -> ?i32 { + if flag { return 7; } + return null; +} + +fn describe(s: Shape) -> void { + match s { + Empty => { @print("empty\n"); } + Point(v) => { @print("point {}\n", v); } + Pair { x, y } => { @print("point {} {}\n", x, y); } + } +} + +fn main() -> i32 { + if let Some(v) = pick(true) { @print("some {}\n", v); } + else { @print("unexpected\n"); } + if let None = pick(false) { @print("none\n"); } + else { @print("unexpected\n"); } + describe(Shape.Pair{ x: 3, y: 4 }); + describe(Shape.Empty); + return 0; +} diff --git a/fec/tests/exec/printfmt.fe b/fec/tests/exec/printfmt.fe new file mode 100644 index 0000000..31d69b3 --- /dev/null +++ b/fec/tests/exec/printfmt.fe @@ -0,0 +1,14 @@ +// EXIT:0 +// OUTPUT:n=42 neg=-7 hex=ff +// OUTPUT:yes=true no=false +// OUTPUT:text=hello char=A +// OUTPUT:braces {} done +unit printfmt; + +fn main() -> i32 { + @print("n={} neg={} hex={x}\n", 42, 0 - 7, 255); + @print("yes={} no={}\n", true, false); + @print("text={} char={c}\n", "hello", 'A'); + @print("braces {} done\n", "{}"); + return 0; +} diff --git a/fec/tests/exec/printnum.fe b/fec/tests/exec/printnum.fe new file mode 100644 index 0000000..8e94185 --- /dev/null +++ b/fec/tests/exec/printnum.fe @@ -0,0 +1,20 @@ +// EXIT:0 +// OUTPUT:fizz 12345 -678 +unit printnum; +import std.io; +import std.fmt; + +fn show(v: i32) -> void { + var buf: [16]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let n: usize = fmt.fmt_i32(buf[..], v); + io.print(" "); + io.print(buf[0..n]); +} + +fn main() -> i32 { + io.print("fizz"); + show(12345); + show(0 - 678); + io.print("\n"); + return 0; +} diff --git a/fec/tests/exec/radix.fe b/fec/tests/exec/radix.fe new file mode 100644 index 0000000..378e5e1 --- /dev/null +++ b/fec/tests/exec/radix.fe @@ -0,0 +1,29 @@ +// EXIT:0 +// OUTPUT:bin 10 255 0 +// OUTPUT:oct 15 511 8 +// OUTPUT:hex 255 4095 16 +// OUTPUT:dec 1000 1000000 +// OUTPUT:same yes yes +unit radix; + +import std.io; + +// SPEC §3 spells four radices. Reading `0b1010` as decimal stops at the `b` +// and answers zero -- which is a number, so nothing looks wrong until the +// program does the wrong thing. + +fn main() -> i32 { + @print("bin {} {} {}\n", 0b1010, 0b11111111, 0b0); + @print("oct {} {} {}\n", 0o17, 0o777, 0o10); + @print("hex {} {} {}\n", 0xFF, 0xfff, 0x10); + @print("dec {} {}\n", 1_000, 1_000_000); + + // The same number four ways. + @print("same {} {}\n", yesno(0b1111 == 0o17), yesno(0o17 == 0xF)); + return 0; +} + +fn yesno(b: bool) -> []u8 { + if b { return "yes"; } + return "no"; +} diff --git a/fec/tests/exec/readfile.fe b/fec/tests/exec/readfile.fe new file mode 100644 index 0000000..f904eb8 --- /dev/null +++ b/fec/tests/exec/readfile.fe @@ -0,0 +1,37 @@ +// EXIT:0 +// OUTPUT:read 27 bytes +// OUTPUT:first line: // EXIT:0 +unit readfile; +import std.io; + +// Reads its own source and reports the first line. A program that can open a +// file is a program that can be a compiler. + +fn main() -> i32 { + var path: [64]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let n: usize = io.to_cstr(path[..], "fec/tests/exec/readfile.fe"); + let handle: i32 = io.open_read(path[0..n]) catch |e| { + @print("cannot open\n"); + return 1; + }; + var buf: [64]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let got: usize = io.read(handle, buf[..]) catch |e| { + io.close(handle); + return 2; + }; + io.close(handle); + @print("read {} bytes\n", n); + var stop: usize = 0; + while stop < got { + if buf[stop] == 10 { break; } + stop = stop + 1; + } + @print("first line: {}\n", buf[0..stop]); + return 0; +} diff --git a/fec/tests/exec/sliceok.fe b/fec/tests/exec/sliceok.fe new file mode 100644 index 0000000..43bd34a --- /dev/null +++ b/fec/tests/exec/sliceok.fe @@ -0,0 +1,14 @@ +// EXIT:9 +unit sliceok; + +fn total(s: []i32) -> i32 { + var sum: i32 = 0; + for v in s { sum = sum + v.^; } + return sum; +} + +fn main() -> i32 { + let a: [5]i32 = [1, 2, 3, 4, 5]; + let mid: []i32 = a[1..4]; + return total(mid); +} diff --git a/fec/tests/exec/slicerng.fe b/fec/tests/exec/slicerng.fe new file mode 100644 index 0000000..78aef2f --- /dev/null +++ b/fec/tests/exec/slicerng.fe @@ -0,0 +1,11 @@ +// EXIT:3 +// OUTPUT:index out of bounds +// NOCHECKS:0 +unit slicerng; + +fn main() -> i32 { + let a: [2]i32 = [1, 2]; + let s: []i32 = a[0..3]; + let n: i32 = s.n as i32; + return n - n; +} diff --git a/fec/tests/exec/strings.fe b/fec/tests/exec/strings.fe new file mode 100644 index 0000000..fb2afb9 --- /dev/null +++ b/fec/tests/exec/strings.fe @@ -0,0 +1,29 @@ +// EXIT:0 +// OUTPUT:ok 42 -7 +unit strings; +import std.io; +import std.str; +import std.fmt; + +fn show(v: i32) -> void { + var buf: [16]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let n: usize = fmt.fmt_i32(buf[..], v); + io.print(" "); + io.print(buf[0..n]); +} + +fn main() -> i32 { + if not str.eq(str.trim(" hello "), "hello") { return 1; } + if not str.starts_with("ferro", "fer") { return 2; } + if str.find("abcdef", "cd") != 2 { return 3; } + if str.find("abcdef", "zz") != 6 { return 4; } + let a: i32 = str.parse_int("42") orelse 0; + let b: i32 = str.parse_int("-7") orelse 0; + let bad: i32 = str.parse_int("12x") orelse 0; + if bad != 0 { return 5; } + io.print("ok"); + show(a); + show(b); + io.print("\n"); + return 0; +} diff --git a/fec/tests/exec/structs.fe b/fec/tests/exec/structs.fe new file mode 100644 index 0000000..826966b --- /dev/null +++ b/fec/tests/exec/structs.fe @@ -0,0 +1,20 @@ +// EXIT:97 +unit structs; + +struct Point { x: i32, y: i32, } + +fn make(a: i32, b: i32) -> Point { return Point{ x: a, y: b }; } +fn swap(p: Point) -> Point { return Point{ x: p.y, y: p.x }; } + +fn main() -> i32 { + let p: Point = make(3, 8); + let q: Point = swap(p); + let grid: [3]Point = [make(1,1), make(2,2), make(3,3)]; + var s: i32 = 0; + var i: i32 = 0; + while i < 3 { + s = s + grid[i].x * grid[i].y; + i = i + 1; + } + return q.x * 10 + q.y + s; +} diff --git a/fec/tests/exec/trapsrc/README.md b/fec/tests/exec/trapsrc/README.md new file mode 100644 index 0000000..e8d0803 --- /dev/null +++ b/fec/tests/exec/trapsrc/README.md @@ -0,0 +1,5 @@ +# 트랩이 대는 파일 + +빌드 전체가 한 모듈이 되므로 트랩은 자기가 어느 유닛에서 왔는지 스스로 말해야 +한다. 여기서는 경계 검사가 `pick.fe` 에 있고 프로그램은 `main.fe` 다. 트랩이 +`main.fe` 를 대면 줄 번호가 맞아도 엉뚱한 줄을 가리킨다. diff --git a/fec/tests/exec/trapsrc/main.fe b/fec/tests/exec/trapsrc/main.fe new file mode 100644 index 0000000..12f89d7 --- /dev/null +++ b/fec/tests/exec/trapsrc/main.fe @@ -0,0 +1,20 @@ +// EXIT:3 +// OUTPUT:index out of bounds +// OUTPUT:pick.fe:6 +// NOCHECKS:0 +unit main; + +import pick; + +// A build is many units in one module. A trap has to name the file the check +// was written in, not the file the program was started from. + +const S: str = "abc"; + +fn main() -> i32 { + let c: u8 = pick.at(S, 9); + // Without the checks the read runs off the end and `c` is whatever + // was there, so nothing is decided by its value. + if c == 0 { return 0; } + return 0; +} diff --git a/fec/tests/exec/trapsrc/pick.fe b/fec/tests/exec/trapsrc/pick.fe new file mode 100644 index 0000000..628d989 --- /dev/null +++ b/fec/tests/exec/trapsrc/pick.fe @@ -0,0 +1,7 @@ +unit pick; + +// The failing index is here, two units away from the program that runs it. + +pub fn at(s: []u8, i: usize) -> u8 { + return s[i]; +} diff --git a/fec/tests/exec/wordfreq.fe b/fec/tests/exec/wordfreq.fe new file mode 100644 index 0000000..8088934 --- /dev/null +++ b/fec/tests/exec/wordfreq.fe @@ -0,0 +1,82 @@ +// EXIT:0 +// OUTPUT:the 3 +// OUTPUT:cat 2 +// OUTPUT:sat 1 +// OUTPUT:distinct 5 +// OUTPUT:balanced +unit wordfreq; +import std.io; +import std.fmt; +import std.str; +import std.list; +import std.sys; + +// Count how often each word appears, in the order the words were first seen. +// A word is a run of anything that is not a space. + +struct Word { + from: usize, + len: usize, + count: i32, +} + +fn report(text: []u8, w: Word) -> void { + var buf: [16]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let n: usize = fmt.fmt_i32(buf[..], w.count); + io.print(text[w.from..w.from + w.len]); + io.print(" "); + io.print(buf[0..n]); + io.print("\n"); +} + +fn tally(text: []u8) -> !i32 { + var words: list.List(Word) = try list.List(Word).with_capacity(4); + var at: usize = 0; + while at < text.n { + var stop: usize = at; + while stop < text.n { + if text[stop] == 32 { break; } + stop = stop + 1; + } + if stop > at { + let word: []u8 = text[at..stop]; + var seen: bool = false; + var i: usize = 0; + while i < words.count() { + let known: Word = words.at(i); + if str.eq(text[known.from..known.from + known.len], word) { + words.set(i, Word{ from: known.from, len: known.len, + count: known.count + 1 }); + seen = true; + break; + } + i = i + 1; + } + if not seen { + try words.push(Word{ from: at, len: stop - at, count: 1 }); + } + } + at = stop + 1; + } + var k: usize = 0; + while k < words.count() { + let w: Word = words.at(k); + if w.count > 1 or k < 3 { report(text, w); } + k = k + 1; + } + return words.count() as i32; +} + +fn main() -> i32 { + let distinct: i32 = tally("the cat sat on the mat the cat") catch |e| { + return 1; + }; + var buf: [16]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let n: usize = fmt.fmt_i32(buf[..], distinct); + io.print("distinct "); + io.print(buf[0..n]); + io.print("\n"); + if sys.allocs() != sys.frees() { io.print("leaked\n"); return 2; } + io.print("balanced\n"); + return 0; +} diff --git a/fec/tests/format/bad_ari.fe b/fec/tests/format/bad_ari.fe new file mode 100644 index 0000000..4dde572 --- /dev/null +++ b/fec/tests/format/bad_ari.fe @@ -0,0 +1,7 @@ +// ERROR:5:format argument count mismatch +unit bad_ari; + +fn main() -> i32 { + @print("{} {}", 1); + return 0; +} diff --git a/fec/tests/format/bad_bufw.fe b/fec/tests/format/bad_bufw.fe new file mode 100644 index 0000000..2ee7c87 --- /dev/null +++ b/fec/tests/format/bad_bufw.fe @@ -0,0 +1,10 @@ +// ERROR:9:unknown name +// io.buf_writer was the callback-based writer that 351e5db replaced with the +// Writer handle enum. This fixture keeps it from coming back. +unit bad_bufw; + +fn main() -> void { + var raw: [4]u8 = [0, 0, 0, 0]; + var buf: []mut u8 = raw[..]; + let w = io.buf_writer(buf); +} diff --git a/fec/tests/format/bad_cls.fe b/fec/tests/format/bad_cls.fe new file mode 100644 index 0000000..0871f16 --- /dev/null +++ b/fec/tests/format/bad_cls.fe @@ -0,0 +1,7 @@ +// ERROR:5:unmatched '}' in format +unit bad_cls; + +fn main() -> i32 { + @print("}", 1); + return 0; +} diff --git a/fec/tests/format/bad_many.fe b/fec/tests/format/bad_many.fe new file mode 100644 index 0000000..d7608d6 --- /dev/null +++ b/fec/tests/format/bad_many.fe @@ -0,0 +1,7 @@ +// ERROR:5:format argument count mismatch +unit bad_many; + +fn main() -> i32 { + @print("{}", 1, 2); + return 0; +} diff --git a/fec/tests/format/bad_open.fe b/fec/tests/format/bad_open.fe new file mode 100644 index 0000000..00a59f2 --- /dev/null +++ b/fec/tests/format/bad_open.fe @@ -0,0 +1,7 @@ +// ERROR:5:unterminated format placeholder +unit bad_open; + +fn main() -> i32 { + @print("{", 1); + return 0; +} diff --git a/fec/tests/format/bad_run.fe b/fec/tests/format/bad_run.fe new file mode 100644 index 0000000..d5d81b7 --- /dev/null +++ b/fec/tests/format/bad_run.fe @@ -0,0 +1,8 @@ +// ERROR:6:format must be a comptime string +unit bad_run; + +fn main() -> i32 { + var fmt: str = "{}"; + @print(fmt, 1); + return 0; +} diff --git a/fec/tests/format/bad_try.fe b/fec/tests/format/bad_try.fe new file mode 100644 index 0000000..b09ecea --- /dev/null +++ b/fec/tests/format/bad_try.fe @@ -0,0 +1,7 @@ +// ERROR:5:try requires an error result +unit bad_try; + +fn main() -> i32 { + try @print("nope"); + return 0; +} diff --git a/fec/tests/format/bad_type.fe b/fec/tests/format/bad_type.fe new file mode 100644 index 0000000..1b9ff35 --- /dev/null +++ b/fec/tests/format/bad_type.fe @@ -0,0 +1,10 @@ +// ERROR:8:no fmt writer for argument type +unit bad_type; + +struct Point { x: i32, } + +fn main() -> i32 { + let p: Point = Point{ x: 1 }; + @print("{}", p); + return 0; +} diff --git a/fec/tests/format/bad_verb.fe b/fec/tests/format/bad_verb.fe new file mode 100644 index 0000000..e683f90 --- /dev/null +++ b/fec/tests/format/bad_verb.fe @@ -0,0 +1,7 @@ +// ERROR:5:unsupported format verb +unit bad_verb; + +fn main() -> i32 { + @print("{q}", 1); + return 0; +} diff --git a/fec/tests/format/bad_writ.fe b/fec/tests/format/bad_writ.fe new file mode 100644 index 0000000..6331833 --- /dev/null +++ b/fec/tests/format/bad_writ.fe @@ -0,0 +1,8 @@ +// ERROR:6:@fprint requires io.Writer +unit bad_writ; + +fn main() -> i32 { + var x: i32 = 0; + @fprint(x, "bad"); + return 0; +} diff --git a/fec/tests/format/ok_forma.fe b/fec/tests/format/ok_forma.fe new file mode 100644 index 0000000..a830b93 --- /dev/null +++ b/fec/tests/format/ok_forma.fe @@ -0,0 +1,30 @@ +unit ok_forma; + +const FMT: str = "n={} hex={x} c={c} s={s} b={b} {{ok}}\n"; + +fn main() -> i32 { + var raw: [8]u8 = [0, 0, 0, 0, 0, 0, 0, 0]; + var raw2: [8]u8 = [0, 0, 0, 0, 0, 0, 0, 0]; + var raw3: [8]u8 = [0, 0, 0, 0, 0, 0, 0, 0]; + var raw4: [8]u8 = [0, 0, 0, 0, 0, 0, 0, 0]; + var raw5: [8]u8 = [0, 0, 0, 0, 0, 0, 0, 0]; + var buf: []mut u8 = raw[..]; + var buf2: []mut u8 = raw2[..]; + var buf3: []mut u8 = raw3[..]; + var buf4: []mut u8 = raw4[..]; + var buf5: []mut u8 = raw5[..]; + let w: io.Writer = io.null_writer(); + const LOCAL_FMT: str = "value={}\n"; + @print(FMT, 7, 15, 'A', "yes", true); + @fprint(w, LOCAL_FMT, 12); + let n: usize = @sprint(buf2, "A\x42\u0043defghi"); + let n2: usize = @sprint(buf3, "xy"); + let inner_n: usize = @sprint(buf4, "xy"); + let outer_n: usize = @sprint(buf5, "n={}", @sprint(buf4, "xy")); + if n == 8 and buf2[0] == ('A' as u8) and + buf2[1] == ('B' as u8) and buf2[2] == ('C' as u8) and + n2 == 2 and buf3[0] == ('x' as u8) and + inner_n == 2 and outer_n == 3 and + buf5[0] == ('n' as u8) and buf5[2] == ('2' as u8) { return 0; } + return 1; +} diff --git a/fec/tests/format/ok_prop.fe b/fec/tests/format/ok_prop.fe new file mode 100644 index 0000000..49a0051 --- /dev/null +++ b/fec/tests/format/ok_prop.fe @@ -0,0 +1,5 @@ +unit ok_prop; + +pub fn propagate(w: io.Writer) -> !void { + try @fprint(w, "a{}b", 1); +} diff --git a/fec/tests/format/ok_try_f.fe b/fec/tests/format/ok_try_f.fe new file mode 100644 index 0000000..5429146 --- /dev/null +++ b/fec/tests/format/ok_try_f.fe @@ -0,0 +1,8 @@ +unit ok_try_f; + +fn main() -> !void { + var raw: [4]u8 = [0, 0, 0, 0]; + var buf: []mut u8 = raw[..]; + let w: io.Writer = io.null_writer(); + try @fprint(w, "ok"); +} diff --git a/fec/tests/generic/README.md b/fec/tests/generic/README.md new file mode 100644 index 0000000..b00050e --- /dev/null +++ b/fec/tests/generic/README.md @@ -0,0 +1,20 @@ +# 제네릭 + +이 디렉터리는 제네릭 타입/함수/인스턴스화 관련 고정 fixture입니다. + +실행 방법: + +`uv run python tests/run.py -k generic` + +현재 수록 케이스: + +- `okalias.fe`, `okbox.fe`, `okdedup.fe`, `okid.fe`, `okisint.fe` +- `okmulti.fe`, `oknested.fe`, `okpair.fe`, `okscope/main.fe`, `okscope/lib.fe` +- `oksamrec.fe`, `okskip.fe`, `oktypeeq.fe` +- `badarity.fe`, `badarg.fe`, `badbody.fe`, `baddepth.fe`, `baddist.fe` +- `badfew.fe`, `badinfer.fe`, `badop.fe`, `badtype.fe`, `badvalue.fe` +- `defscope/main.fe`, `defscope/lib.fe` + +미구현/미완료 구간: + +현재 다수의 제네릭 케이스가 통과하지 않습니다. diff --git a/fec/tests/generic/badarg.fe b/fec/tests/generic/badarg.fe new file mode 100644 index 0000000..18e207e --- /dev/null +++ b/fec/tests/generic/badarg.fe @@ -0,0 +1,10 @@ +// ERROR:9:comptime +unit badarg; + +fn id(comptime T: type, v: T) -> T { + return v; +} + +fn bad() -> i32 { + return id(7, 8); +} diff --git a/fec/tests/generic/badarity.fe b/fec/tests/generic/badarity.fe new file mode 100644 index 0000000..6692d28 --- /dev/null +++ b/fec/tests/generic/badarity.fe @@ -0,0 +1,10 @@ +// ERROR:9:argument +unit badarity; + +struct Box(T) { + value: T, +} + +fn bad() -> void { + var x: Box(i32, u8); +} diff --git a/fec/tests/generic/badbody.fe b/fec/tests/generic/badbody.fe new file mode 100644 index 0000000..51c613b --- /dev/null +++ b/fec/tests/generic/badbody.fe @@ -0,0 +1,12 @@ +// ERROR:7:arithmetic operands +// SPEC 9: the defect is the operation in the body, so that is the primary +// error; the call gets an `instantiated here` note. +unit badbody; + +fn add(comptime T: type, a: T, b: T) -> T { + return a + b; +} + +fn bad() -> bool { + return add(bool, true, false); +} diff --git a/fec/tests/generic/baddepth.fe b/fec/tests/generic/baddepth.fe new file mode 100644 index 0000000..a6d981b --- /dev/null +++ b/fec/tests/generic/baddepth.fe @@ -0,0 +1,10 @@ +// ERROR:9:depth +unit baddepth; + +struct Box(T) { + value: T, +} + +fn bad() -> void { + var x: Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(i32))))))))))))))))))))))))))))))))); +} diff --git a/fec/tests/generic/baddist.fe b/fec/tests/generic/baddist.fe new file mode 100644 index 0000000..bd1425e --- /dev/null +++ b/fec/tests/generic/baddist.fe @@ -0,0 +1,10 @@ +// ERROR:7:depth +unit baddist; + +struct Box(T) { value: T, } + +fn grow(comptime T: type) -> void { + grow(Box(T)); +} + +fn bad() -> void { grow(i32); } diff --git a/fec/tests/generic/badfew.fe b/fec/tests/generic/badfew.fe new file mode 100644 index 0000000..6b43ae3 --- /dev/null +++ b/fec/tests/generic/badfew.fe @@ -0,0 +1,10 @@ +// ERROR:9:generic +unit badfew; + +struct Box(T) { + value: T, +} + +fn bad() -> void { + var x: Box; +} diff --git a/fec/tests/generic/badinfer.fe b/fec/tests/generic/badinfer.fe new file mode 100644 index 0000000..8bcccb8 --- /dev/null +++ b/fec/tests/generic/badinfer.fe @@ -0,0 +1,8 @@ +// ERROR:7:generic +unit badinfer; + +fn id(comptime T: type, value: T) -> T { return value; } + +fn bad() -> i32 { + return id(7); +} diff --git a/fec/tests/generic/badop.fe b/fec/tests/generic/badop.fe new file mode 100644 index 0000000..1c1b473 --- /dev/null +++ b/fec/tests/generic/badop.fe @@ -0,0 +1,19 @@ +// ERROR:11:ordering requires +// SPEC 9: the primary error is the comparison in the body. The call at the +// bottom gets an `instantiated here` note. +unit badop; + +struct Token { + value: i32, +} + +fn max(comptime T: type, a: T, b: T) -> T { + if a > b { return a; } + return b; +} + +fn bad() -> Token { + let a = Token{ value: 1 }; + let b = Token{ value: 2 }; + return max(Token, a, b); +} diff --git a/fec/tests/generic/badphant.fe b/fec/tests/generic/badphant.fe new file mode 100644 index 0000000..6735b73 --- /dev/null +++ b/fec/tests/generic/badphant.fe @@ -0,0 +1,15 @@ +// ERROR:14:argument type mismatch +unit badphant; + +// A type parameter that the body never mentions still tells two instances +// apart. `Handle(Node)` and `Handle(Kind)` are different nominal types. +struct Handle(T) { raw: u32, } + +struct Node { v: i32, } +struct Kind { v: i32, } + +fn only_node(h: Handle(Node)) -> u32 { return h.raw; } + +fn bad() -> u32 { + return only_node(Handle(Kind){ raw: 1 }); +} diff --git a/fec/tests/generic/badscope/lib.fe b/fec/tests/generic/badscope/lib.fe new file mode 100644 index 0000000..002e41b --- /dev/null +++ b/fec/tests/generic/badscope/lib.fe @@ -0,0 +1,5 @@ +unit lib; + +pub fn call(comptime T: type, v: T) -> T { + return helper(v); +} diff --git a/fec/tests/generic/badscope/main.fe b/fec/tests/generic/badscope/main.fe new file mode 100644 index 0000000..d840235 --- /dev/null +++ b/fec/tests/generic/badscope/main.fe @@ -0,0 +1,12 @@ +// ERROR:unknown function +// SPEC 9: a generic body resolves its names in the unit that declared it, +// so lib.call cannot see this unit's `helper`. The mirror of defscope/, +// where the definition unit's own private helper is reachable. +unit main; +import lib; + +fn helper(v: i32) -> i32 { return v + 10; } + +fn main() -> i32 { + return lib.call(i32, 1); +} diff --git a/fec/tests/generic/badtype.fe b/fec/tests/generic/badtype.fe new file mode 100644 index 0000000..b0b8325 --- /dev/null +++ b/fec/tests/generic/badtype.fe @@ -0,0 +1,6 @@ +// ERROR:5:type +unit badtype; + +fn bad() -> void { + let t = i32; +} diff --git a/fec/tests/generic/badvalue.fe b/fec/tests/generic/badvalue.fe new file mode 100644 index 0000000..1e8ff97 --- /dev/null +++ b/fec/tests/generic/badvalue.fe @@ -0,0 +1,4 @@ +// ERROR:4:comptime +unit badvalue; + +fn id(comptime N: usize, value: i32) -> i32 { return value; } diff --git a/fec/tests/generic/defscope/lib.fe b/fec/tests/generic/defscope/lib.fe new file mode 100644 index 0000000..4dcb06b --- /dev/null +++ b/fec/tests/generic/defscope/lib.fe @@ -0,0 +1,13 @@ +unit lib; + +fn bump_i32(v: i32) -> i32 { + return v + 1; +} + +pub fn bump(comptime T: type, v: T) -> T { + comptime if T == i32 { + return bump_i32(v); + } else { + return v; + } +} diff --git a/fec/tests/generic/defscope/main.fe b/fec/tests/generic/defscope/main.fe new file mode 100644 index 0000000..9c63d25 --- /dev/null +++ b/fec/tests/generic/defscope/main.fe @@ -0,0 +1,6 @@ +unit main; +import lib; + +fn main() -> i32 { + return lib.bump(i32, 4); +} diff --git a/fec/tests/generic/okalias.fe b/fec/tests/generic/okalias.fe new file mode 100644 index 0000000..a331e8d --- /dev/null +++ b/fec/tests/generic/okalias.fe @@ -0,0 +1,11 @@ +unit okalias; + +const Word = i32; + +fn id(comptime T: type, v: T) -> T { + return v; +} + +fn test() -> Word { + return id(Word, 12); +} diff --git a/fec/tests/generic/okbox.fe b/fec/tests/generic/okbox.fe new file mode 100644 index 0000000..5a50c49 --- /dev/null +++ b/fec/tests/generic/okbox.fe @@ -0,0 +1,18 @@ +unit okbox; + +struct Box(T) { + value: T, + + pub fn new(v: T) -> Self { + return Self{ value: v }; + } + + pub fn get(self: &Self) -> &T { + return &self.value; + } +} + +fn test() -> i32 { + var b: Box(i32) = Box(i32).new(7); + return b.get().^; +} diff --git a/fec/tests/generic/okdedup.fe b/fec/tests/generic/okdedup.fe new file mode 100644 index 0000000..f243e91 --- /dev/null +++ b/fec/tests/generic/okdedup.fe @@ -0,0 +1,8 @@ +unit okdedup; + +fn id(comptime T: type, v: T) -> T { + return v; +} + +fn a() -> i32 { return id(i32, 1); } +fn b() -> i32 { return id(i32, 2); } diff --git a/fec/tests/generic/okid.fe b/fec/tests/generic/okid.fe new file mode 100644 index 0000000..972f6e2 --- /dev/null +++ b/fec/tests/generic/okid.fe @@ -0,0 +1,9 @@ +unit okid; + +fn id(comptime T: type, v: T) -> T { + return v; +} + +fn test() -> i32 { + return id(i32, 7); +} diff --git a/fec/tests/generic/okisint.fe b/fec/tests/generic/okisint.fe new file mode 100644 index 0000000..fba420c --- /dev/null +++ b/fec/tests/generic/okisint.fe @@ -0,0 +1,13 @@ +unit okisint; + +fn is_integer(comptime T: type) -> bool { + comptime if @is_int(T) { + return true; + } else { + return false; + } +} + +fn test() -> bool { + return is_integer(u16); +} diff --git a/fec/tests/generic/okmulti.fe b/fec/tests/generic/okmulti.fe new file mode 100644 index 0000000..cc20def --- /dev/null +++ b/fec/tests/generic/okmulti.fe @@ -0,0 +1,11 @@ +unit okmulti; + +fn id(comptime T: type, v: T) -> T { + return v; +} + +fn test() -> i32 { + let a: i32 = id(i32, 7); + let b: u8 = id(u8, 9 as u8); + return a + (b as i32); +} diff --git a/fec/tests/generic/oknested.fe b/fec/tests/generic/oknested.fe new file mode 100644 index 0000000..912786e --- /dev/null +++ b/fec/tests/generic/oknested.fe @@ -0,0 +1,15 @@ +unit oknested; + +struct Box(T) { + value: T, + + pub fn new(v: T) -> Self { + return Self{ value: v }; + } +} + +fn test() -> i32 { + let inner: Box(i32) = Box(i32).new(3); + let outer: Box(Box(i32)) = Box(Box(i32)).new(inner); + return outer.value.value; +} diff --git a/fec/tests/generic/okpair.fe b/fec/tests/generic/okpair.fe new file mode 100644 index 0000000..40af14e --- /dev/null +++ b/fec/tests/generic/okpair.fe @@ -0,0 +1,15 @@ +unit okpair; + +struct Pair(A, B) { + first: A, + second: B, + + pub fn new(a: A, b: B) -> Self { + return Self{ first: a, second: b }; + } +} + +fn test() -> i32 { + let p: Pair(i32, u8) = Pair(i32, u8).new(4, 5 as u8); + return p.first + (p.second as i32); +} diff --git a/fec/tests/generic/okphant.fe b/fec/tests/generic/okphant.fe new file mode 100644 index 0000000..dad55a0 --- /dev/null +++ b/fec/tests/generic/okphant.fe @@ -0,0 +1,16 @@ +unit okphant; + +// The parameter is used nowhere in the body. That is legal, and it is the +// natural shape of a typed handle. +struct Handle(T) { raw: u32, } + +struct Node { v: i32, } +struct Kind { v: i32, } + +fn only_node(h: Handle(Node)) -> u32 { return h.raw; } +fn only_kind(h: Handle(Kind)) -> u32 { return h.raw; } + +fn ok() -> u32 { + return only_node(Handle(Node){ raw: 1 }) + + only_kind(Handle(Kind){ raw: 2 }); +} diff --git a/fec/tests/generic/oksamrec.fe b/fec/tests/generic/oksamrec.fe new file mode 100644 index 0000000..2419703 --- /dev/null +++ b/fec/tests/generic/oksamrec.fe @@ -0,0 +1,7 @@ +unit oksamrec; + +fn repeat(comptime T: type, value: T) -> T { + return repeat(T, value); +} + +fn test() -> i32 { return repeat(i32, 1); } diff --git a/fec/tests/generic/okskip.fe b/fec/tests/generic/okskip.fe new file mode 100644 index 0000000..e5fd24c --- /dev/null +++ b/fec/tests/generic/okskip.fe @@ -0,0 +1,11 @@ +unit okskip; + +fn select(comptime T: type, value: T) -> T { + comptime if T == i32 { + return value; + } else { + return unknown(value); + } +} + +fn test() -> i32 { return select(i32, 1); } diff --git a/fec/tests/generic/oktypeeq.fe b/fec/tests/generic/oktypeeq.fe new file mode 100644 index 0000000..49f7b9c --- /dev/null +++ b/fec/tests/generic/oktypeeq.fe @@ -0,0 +1,13 @@ +unit oktypeeq; + +fn kind(comptime T: type) -> i32 { + comptime if T == i32 { + return 1; + } else { + return 0; + } +} + +fn test() -> i32 { + return kind(i32); +} diff --git a/fec/tests/optional/README.md b/fec/tests/optional/README.md new file mode 100644 index 0000000..d9212a1 --- /dev/null +++ b/fec/tests/optional/README.md @@ -0,0 +1,12 @@ +# 옵션/에러유니온 + +이 디렉터리는 `?T`/`null`/`try`/`catch`/`orelse`/명목 에러 유니온 동작을 검증합니다. + +실행 방법: + +`uv run python tests/run.py -k optional` + +- `ok*.fe`는 통과해야 합니다. +- `bad*.fe`는 실패해야 하며, 첫 줄 `// ERROR:...` 마커가 있으면 그 패턴을 따른다. + +실행은 프런트엔드(`--check`) 기준이며, 코드 생성 단계는 포함하지 않습니다. diff --git a/fec/tests/optional/badcatch.fe b/fec/tests/optional/badcatch.fe new file mode 100644 index 0000000..97ed75c --- /dev/null +++ b/fec/tests/optional/badcatch.fe @@ -0,0 +1,17 @@ +// ERROR:13:catch +unit badcatch; + +error E { + Bad = 1, +} + +fn leaf() -> E!i32 { + return E.Bad; +} + +fn bad() -> i32 { + let v = leaf() catch |e| { + let ignored: i32 = 1; + }; + return v; +} diff --git a/fec/tests/optional/baddef.fe b/fec/tests/optional/baddef.fe new file mode 100644 index 0000000..2ff2b0f --- /dev/null +++ b/fec/tests/optional/baddef.fe @@ -0,0 +1,14 @@ +// ERROR:13:type +unit baddef; + +error E { + Bad = 1, +} + +fn leaf() -> E!i32 { + return E.Bad; +} + +fn bad() -> i32 { + return leaf() catch false; +} diff --git a/fec/tests/optional/baddir.fe b/fec/tests/optional/baddir.fe new file mode 100644 index 0000000..46e0715 --- /dev/null +++ b/fec/tests/optional/baddir.fe @@ -0,0 +1,10 @@ +// ERROR:9:optional +unit baddir; + +struct Node { + value: i32, +} + +fn bad(p: ?^Node) -> i32 { + return p.^.value; +} diff --git a/fec/tests/optional/badercod.fe b/fec/tests/optional/badercod.fe new file mode 100644 index 0000000..ad5b34c --- /dev/null +++ b/fec/tests/optional/badercod.fe @@ -0,0 +1,7 @@ +// ERROR:6:duplicate +unit badercod; + +error E { + One = 1, + Two = 1, +} diff --git a/fec/tests/optional/badernam.fe b/fec/tests/optional/badernam.fe new file mode 100644 index 0000000..661e8c0 --- /dev/null +++ b/fec/tests/optional/badernam.fe @@ -0,0 +1,7 @@ +// ERROR:6:duplicate +unit badernam; + +error E { + One = 1, + One = 2, +} diff --git a/fec/tests/optional/badetype.fe b/fec/tests/optional/badetype.fe new file mode 100644 index 0000000..af5bf39 --- /dev/null +++ b/fec/tests/optional/badetype.fe @@ -0,0 +1,18 @@ +// ERROR:17:error +unit badetype; + +error A { + Bad = 1, +} + +error B { + Bad = 1, +} + +fn leaf() -> A!i32 { + return A.Bad; +} + +fn bad() -> B!i32 { + return try leaf(); +} diff --git a/fec/tests/optional/badnull.fe b/fec/tests/optional/badnull.fe new file mode 100644 index 0000000..4ed896b --- /dev/null +++ b/fec/tests/optional/badnull.fe @@ -0,0 +1,6 @@ +// ERROR:5:null +unit badnull; + +fn bad() -> void { + let p = null; +} diff --git a/fec/tests/optional/badoref.fe b/fec/tests/optional/badoref.fe new file mode 100644 index 0000000..641206e --- /dev/null +++ b/fec/tests/optional/badoref.fe @@ -0,0 +1,6 @@ +// ERROR:5:reference +unit badoref; + +struct Bad { + value: ?&i32, +} diff --git a/fec/tests/optional/badorel.fe b/fec/tests/optional/badorel.fe new file mode 100644 index 0000000..3a79145 --- /dev/null +++ b/fec/tests/optional/badorel.fe @@ -0,0 +1,10 @@ +// ERROR:9:non-Copy +unit badorel; + +struct Node { + next: ?^Node, +} + +fn bad(p: ^Node, fallback: ^Node) -> ^Node { + return p.next orelse fallback; +} diff --git a/fec/tests/optional/badproj.fe b/fec/tests/optional/badproj.fe new file mode 100644 index 0000000..a7d987c --- /dev/null +++ b/fec/tests/optional/badproj.fe @@ -0,0 +1,11 @@ +// ERROR:9:mem.replace +unit badproj; + +struct Node { + next: ?^Node, +} + +fn bad(p: ^Node) -> void { + let q = p.next; + mem.destroy(p); +} diff --git a/fec/tests/optional/badqmark.fe b/fec/tests/optional/badqmark.fe new file mode 100644 index 0000000..36cdc7b --- /dev/null +++ b/fec/tests/optional/badqmark.fe @@ -0,0 +1,7 @@ +// ERROR:6:optional +unit badqmark; + +fn bad() -> i32 { + let x: i32 = 1; + return x.?; +} diff --git a/fec/tests/optional/badret.fe b/fec/tests/optional/badret.fe new file mode 100644 index 0000000..84842e2 --- /dev/null +++ b/fec/tests/optional/badret.fe @@ -0,0 +1,14 @@ +// ERROR:13:error +unit badret; + +error A { + Bad = 1, +} + +error B { + Bad = 1, +} + +fn bad() -> B!i32 { + return A.Bad; +} diff --git a/fec/tests/optional/badsome.fe b/fec/tests/optional/badsome.fe new file mode 100644 index 0000000..f35a461 --- /dev/null +++ b/fec/tests/optional/badsome.fe @@ -0,0 +1,7 @@ +// ERROR:5:Some +unit badsome; + +fn bad() -> i32 { + let x = Some(1); + return x; +} diff --git a/fec/tests/optional/badtry.fe b/fec/tests/optional/badtry.fe new file mode 100644 index 0000000..a41273e --- /dev/null +++ b/fec/tests/optional/badtry.fe @@ -0,0 +1,14 @@ +// ERROR:13:try +unit badtry; + +error E { + Bad = 1, +} + +fn leaf() -> E!i32 { + return E.Bad; +} + +fn bad() -> i32 { + return try leaf(); +} diff --git a/fec/tests/optional/badzero.fe b/fec/tests/optional/badzero.fe new file mode 100644 index 0000000..da86afb --- /dev/null +++ b/fec/tests/optional/badzero.fe @@ -0,0 +1,6 @@ +// ERROR:5:0 +unit badzero; + +error E { + Zero = 0, +} diff --git a/fec/tests/optional/okcatch.fe b/fec/tests/optional/okcatch.fe new file mode 100644 index 0000000..557fd12 --- /dev/null +++ b/fec/tests/optional/okcatch.fe @@ -0,0 +1,16 @@ +unit okcatch; + +error E { + Bad = 1, +} + +fn leaf() -> E!i32 { + return E.Bad; +} + +fn top() -> E!i32 { + let v = leaf() catch |e| { + return e; + }; + return v; +} diff --git a/fec/tests/optional/okcatmov.fe b/fec/tests/optional/okcatmov.fe new file mode 100644 index 0000000..c2c7b37 --- /dev/null +++ b/fec/tests/optional/okcatmov.fe @@ -0,0 +1,8 @@ +unit okcatmov; + +error E { Bad = 1, } +struct Node { value: i32, } + +fn recover(result: E!^Node, fallback: ^Node) -> ^Node { + return result catch fallback; +} diff --git a/fec/tests/optional/okcvoid.fe b/fec/tests/optional/okcvoid.fe new file mode 100644 index 0000000..4c1c2ab --- /dev/null +++ b/fec/tests/optional/okcvoid.fe @@ -0,0 +1,15 @@ +unit okcvoid; + +error E { + Bad = 1, +} + +fn leaf() -> E!void { + return E.Bad; +} + +fn top() -> void { + leaf() catch |e| { + return; + }; +} diff --git a/fec/tests/optional/okdeflt.fe b/fec/tests/optional/okdeflt.fe new file mode 100644 index 0000000..b62d66c --- /dev/null +++ b/fec/tests/optional/okdeflt.fe @@ -0,0 +1,13 @@ +unit okdeflt; + +error E { + Bad = 1, +} + +fn leaf() -> E!i32 { + return E.Bad; +} + +fn top() -> i32 { + return leaf() catch 11; +} diff --git a/fec/tests/optional/okiflet.fe b/fec/tests/optional/okiflet.fe new file mode 100644 index 0000000..3192c4b --- /dev/null +++ b/fec/tests/optional/okiflet.fe @@ -0,0 +1,8 @@ +unit okiflet; + +fn value(p: ?i32) -> i32 { + if let Some(v) = p { + return v; + } + return 0; +} diff --git a/fec/tests/optional/okmatch.fe b/fec/tests/optional/okmatch.fe new file mode 100644 index 0000000..6335cff --- /dev/null +++ b/fec/tests/optional/okmatch.fe @@ -0,0 +1,8 @@ +unit okmatch; + +fn value(p: ?i32) -> i32 { + match p { + Some(v) => { return v; } + None => { return 0; } + } +} diff --git a/fec/tests/optional/oknull.fe b/fec/tests/optional/oknull.fe new file mode 100644 index 0000000..c12a917 --- /dev/null +++ b/fec/tests/optional/oknull.fe @@ -0,0 +1,10 @@ +unit oknull; + +struct Node { value: i32, } + +fn accepts(p: ?^Node) -> bool { return p == null; } + +fn test() -> bool { + let p: ?^Node = null; + return accepts(p); +} diff --git a/fec/tests/optional/okorelse.fe b/fec/tests/optional/okorelse.fe new file mode 100644 index 0000000..9ff537e --- /dev/null +++ b/fec/tests/optional/okorelse.fe @@ -0,0 +1,5 @@ +unit okorelse; + +fn value(p: ?i32) -> i32 { + return p orelse 9; +} diff --git a/fec/tests/optional/okpatvw.fe b/fec/tests/optional/okpatvw.fe new file mode 100644 index 0000000..5797b46 --- /dev/null +++ b/fec/tests/optional/okpatvw.fe @@ -0,0 +1,12 @@ +unit okpatvw; + +struct Node { value: i32, } + +fn keep(p: ?^Node) -> void { + var slot = p; + if let Some(node) = slot { + let value = node.value; + } + let owned = mem.replace(&mut slot, null).?; + mem.destroy(owned); +} diff --git a/fec/tests/optional/okproj.fe b/fec/tests/optional/okproj.fe new file mode 100644 index 0000000..f54768a --- /dev/null +++ b/fec/tests/optional/okproj.fe @@ -0,0 +1,9 @@ +unit okproj; + +struct Node { + value: i32, +} + +fn touch(p: ?&mut Node) -> void { + p.?.value = 7; +} diff --git a/fec/tests/optional/okrepl.fe b/fec/tests/optional/okrepl.fe new file mode 100644 index 0000000..2710132 --- /dev/null +++ b/fec/tests/optional/okrepl.fe @@ -0,0 +1,11 @@ +unit okrepl; + +struct Node { + value: i32, +} + +fn take(p: ?^Node) -> void { + var q: ?^Node = p; + let n = mem.replace(&mut q, null).?; + mem.destroy(n); +} diff --git a/fec/tests/optional/oktrdef.fe b/fec/tests/optional/oktrdef.fe new file mode 100644 index 0000000..1b0831c --- /dev/null +++ b/fec/tests/optional/oktrdef.fe @@ -0,0 +1,14 @@ +unit oktrdef; + +error E { + Bad = 1, +} + +fn leaf() -> E!i32 { + return E.Bad; +} + +fn top() -> E!i32 { + defer { let x: i32 = 1; } + return try leaf(); +} diff --git a/fec/tests/optional/oktry.fe b/fec/tests/optional/oktry.fe new file mode 100644 index 0000000..f0066db --- /dev/null +++ b/fec/tests/optional/oktry.fe @@ -0,0 +1,14 @@ +unit oktry; + +error E { + Bad = 1, +} + +fn leaf(ok: bool) -> E!i32 { + if ok { return 7; } + return E.Bad; +} + +fn top() -> E!i32 { + return try leaf(true); +} diff --git a/fec/tests/own/README.md b/fec/tests/own/README.md new file mode 100644 index 0000000..b1fcff9 --- /dev/null +++ b/fec/tests/own/README.md @@ -0,0 +1,12 @@ +# 소유권과 대여 + +이 디렉터리는 `fec/tests`에서 소유권(Ownership)과 대여(Borrow) 동작을 검증하는 고정 fixture 입니다. + +실행 방법: + +`uv run python tests/run.py -k own` + +- `ok*.fe`는 통과해야 합니다. +- `bad*.fe`는 실패해야 하며, 첫 줄 `// ERROR:...` 마커가 있으면 그 패턴을 따른다. + +현재 구현 범위는 타입 검사/의미 검사를 통과한 소스 기반만 포함하고, 별도 코드 생성/런타임 동작은 포함하지 않습니다. diff --git a/fec/tests/own/bad_clos.fe b/fec/tests/own/bad_clos.fe new file mode 100644 index 0000000..298b0d5 --- /dev/null +++ b/fec/tests/own/bad_clos.fe @@ -0,0 +1,14 @@ +// ERROR:13:use of moved value +unit bad_clos; + +struct FileLike { + handle: i32, + fn close(self: Self) -> !void { self.handle = 0; } + fn drop(self: &mut Self) { self.handle = 0; } +} + +fn bad() -> !void { + let file: FileLike = FileLike{ handle: 7 }; + try file.close(); + file.close(); +} diff --git a/fec/tests/own/bad_cond.fe b/fec/tests/own/bad_cond.fe new file mode 100644 index 0000000..a869374 --- /dev/null +++ b/fec/tests/own/bad_cond.fe @@ -0,0 +1,9 @@ +// ERROR:8:use of possibly moved value +unit bad_cond; + +fn take(p: ^i32) -> void { mem.destroy(p); } + +fn bad(p: ^i32, flag: bool) -> void { + if flag { take(p); } + p.^ = 3; +} diff --git a/fec/tests/own/bad_dbl.fe b/fec/tests/own/bad_dbl.fe new file mode 100644 index 0000000..52a7cca --- /dev/null +++ b/fec/tests/own/bad_dbl.fe @@ -0,0 +1,7 @@ +// ERROR:6:use of moved value +unit bad_dbl; + +fn bad(p: ^i32) -> void { + mem.destroy(p); + mem.destroy(p); +} diff --git a/fec/tests/own/bad_dest.fe b/fec/tests/own/bad_dest.fe new file mode 100644 index 0000000..3db184a --- /dev/null +++ b/fec/tests/own/bad_dest.fe @@ -0,0 +1,6 @@ +// ERROR:5:mem.destroy requires exactly one owned pointer +unit bad_dest; + +fn bad(x: i32) -> void { + mem.destroy(x); +} diff --git a/fec/tests/own/bad_drop.fe b/fec/tests/own/bad_drop.fe new file mode 100644 index 0000000..a80a0a8 --- /dev/null +++ b/fec/tests/own/bad_drop.fe @@ -0,0 +1,12 @@ +// ERROR:11:drop may only be invoked by scope cleanup +unit bad_drop; + +struct Box { + value: i32, + fn drop(self: &mut Self) { self.value = 0; } +} + +fn bad() -> void { + var b: Box = Box{ value: 1 }; + b.drop(); +} diff --git a/fec/tests/own/bad_loop.fe b/fec/tests/own/bad_loop.fe new file mode 100644 index 0000000..3f74193 --- /dev/null +++ b/fec/tests/own/bad_loop.fe @@ -0,0 +1,8 @@ +// ERROR:7:use of possibly moved value +unit bad_loop; + +fn take(p: ^i32) -> void { mem.destroy(p); } + +fn bad(p: ^i32, again: bool) -> void { + while again { take(p); } +} diff --git a/fec/tests/own/bad_move.fe b/fec/tests/own/bad_move.fe new file mode 100644 index 0000000..01496ba --- /dev/null +++ b/fec/tests/own/bad_move.fe @@ -0,0 +1,9 @@ +// ERROR:8:use of moved value +unit bad_move; + +fn take(p: ^i32) -> void { mem.destroy(p); } + +fn twice(p: ^i32) -> void { + take(p); + take(p); +} diff --git a/fec/tests/own/bad_proj.fe b/fec/tests/own/bad_proj.fe new file mode 100644 index 0000000..ba5fd64 --- /dev/null +++ b/fec/tests/own/bad_proj.fe @@ -0,0 +1,9 @@ +// ERROR:8:cannot move a non-Copy value out of a projection; use mem.replace +unit bad_proj; + +struct Holder { p: ^i32 } +fn take(p: ^i32) -> void { mem.destroy(p); } + +fn bad(h: Holder) -> void { + take(h.p); +} diff --git a/fec/tests/own/badarg.fe b/fec/tests/own/badarg.fe new file mode 100644 index 0000000..7e50114 --- /dev/null +++ b/fec/tests/own/badarg.fe @@ -0,0 +1,10 @@ +// ERROR:8:derived from a parameter +unit badarg; + +struct Box { + value: i32, + + fn bad(self: &Self, other: &i32) -> &i32 { + return other; + } +} diff --git a/fec/tests/own/badbinit.fe b/fec/tests/own/badbinit.fe new file mode 100644 index 0000000..b6eb632 --- /dev/null +++ b/fec/tests/own/badbinit.fe @@ -0,0 +1,10 @@ +// ERROR:9:initialized +unit badbinit; + +fn bad(assign: bool) -> i32 { + var value: i32; + if assign { + value = 1; + } + return value; +} diff --git a/fec/tests/own/badbrmov.fe b/fec/tests/own/badbrmov.fe new file mode 100644 index 0000000..f07825d --- /dev/null +++ b/fec/tests/own/badbrmov.fe @@ -0,0 +1,9 @@ +// ERROR:8:move +unit badbrmov; + +fn bad(p: ^i32, consume: bool) -> void { + if consume { + mem.destroy(p); + } + mem.destroy(p); +} diff --git a/fec/tests/own/baddefer.fe b/fec/tests/own/baddefer.fe new file mode 100644 index 0000000..faf7839 --- /dev/null +++ b/fec/tests/own/baddefer.fe @@ -0,0 +1,12 @@ +// ERROR:10:borrow +unit baddefer; + +fn read(r: &i32) -> i32 { return r.^; } + +fn bad() -> i32 { + var x: i32 = 0; + let r = &x; + defer { let y = read(r); } + x = 1; + return x; +} diff --git a/fec/tests/own/badfld.fe b/fec/tests/own/badfld.fe new file mode 100644 index 0000000..d4090ff --- /dev/null +++ b/fec/tests/own/badfld.fe @@ -0,0 +1,6 @@ +// ERROR:5:reference +unit badfld; + +struct Bad { + value: &i32, +} diff --git a/fec/tests/own/badforwr.fe b/fec/tests/own/badforwr.fe new file mode 100644 index 0000000..f3553e9 --- /dev/null +++ b/fec/tests/own/badforwr.fe @@ -0,0 +1,12 @@ +// ERROR:9:borrow +unit badforwr; + +// Walking a container borrows it: the item is a reference into it, so writing +// the container underneath moves what that reference points at. + +fn bad(xs: []mut i32) -> void { + for x in xs { + xs[0] = 9; + } + return; +} diff --git a/fec/tests/own/badglob.fe b/fec/tests/own/badglob.fe new file mode 100644 index 0000000..41a107c --- /dev/null +++ b/fec/tests/own/badglob.fe @@ -0,0 +1,9 @@ +// ERROR:7:global +unit badglob; + +var VALUE: i32 = 0; + +fn bad() -> i32 { + let r = &VALUE; + return r.^; +} diff --git a/fec/tests/own/badinv.fe b/fec/tests/own/badinv.fe new file mode 100644 index 0000000..88f483c --- /dev/null +++ b/fec/tests/own/badinv.fe @@ -0,0 +1,9 @@ +// ERROR:6:borrow +unit badinv; + +fn bad(p: ^i32, q: ^i32) -> void { + let r = &p; + p = q; + let keep = r; + mem.destroy(p); +} diff --git a/fec/tests/own/badloop.fe b/fec/tests/own/badloop.fe new file mode 100644 index 0000000..1cb6a4f --- /dev/null +++ b/fec/tests/own/badloop.fe @@ -0,0 +1,9 @@ +// ERROR:6:move +unit badloop; + +fn bad(p: ^i32, again: bool) -> void { + while again { + mem.destroy(p); + } + mem.destroy(p); +} diff --git a/fec/tests/own/badmove.fe b/fec/tests/own/badmove.fe new file mode 100644 index 0000000..e746062 --- /dev/null +++ b/fec/tests/own/badmove.fe @@ -0,0 +1,10 @@ +// ERROR:8:borrow +unit badmove; + +fn take(p: ^i32) -> void { mem.destroy(p); } + +fn bad(p: ^i32) -> void { + let r = &p; + take(p); + let q = r; +} diff --git a/fec/tests/own/badmut.fe b/fec/tests/own/badmut.fe new file mode 100644 index 0000000..90aab1f --- /dev/null +++ b/fec/tests/own/badmut.fe @@ -0,0 +1,10 @@ +// ERROR:7:borrow +unit badmut; + +fn bad() -> i32 { + var x: i32 = 0; + let r = &mut x; + x = 1; + r.^ = 2; + return x; +} diff --git a/fec/tests/own/badmut2.fe b/fec/tests/own/badmut2.fe new file mode 100644 index 0000000..790e251 --- /dev/null +++ b/fec/tests/own/badmut2.fe @@ -0,0 +1,11 @@ +// ERROR:7:borrow +unit badmut2; + +fn bad() -> i32 { + var x: i32 = 0; + let a = &mut x; + let b = &mut x; + a.^ = 1; + b.^ = 2; + return x; +} diff --git a/fec/tests/own/badptr.fe b/fec/tests/own/badptr.fe new file mode 100644 index 0000000..36e9490 --- /dev/null +++ b/fec/tests/own/badptr.fe @@ -0,0 +1,6 @@ +// ERROR:4:reference +unit badptr; + +fn bad(p: *&i32) -> void { + return; +} diff --git a/fec/tests/own/badrall.fe b/fec/tests/own/badrall.fe new file mode 100644 index 0000000..630a374 --- /dev/null +++ b/fec/tests/own/badrall.fe @@ -0,0 +1,14 @@ +// ERROR:11:borrow +unit badrall; + +struct Pair { a: i32, b: i32, } + +fn take(q: Pair) -> i32 { return q.a; } + +fn bad() -> i32 { + var p = Pair{ a: 1, b: 2 }; + let left = &mut p.a; + let n = take(p); + left.^ = 4; + return n; +} diff --git a/fec/tests/own/badret.fe b/fec/tests/own/badret.fe new file mode 100644 index 0000000..5a9a777 --- /dev/null +++ b/fec/tests/own/badret.fe @@ -0,0 +1,7 @@ +// ERROR:6:reference +unit badret; + +fn bad() -> &i32 { + let x: i32 = 1; + return &x; +} diff --git a/fec/tests/own/badrfld.fe b/fec/tests/own/badrfld.fe new file mode 100644 index 0000000..90a16e3 --- /dev/null +++ b/fec/tests/own/badrfld.fe @@ -0,0 +1,11 @@ +// ERROR:9:borrow +unit badrfld; + +struct Pair { a: i32, b: i32, } + +fn bad() -> void { + var p = Pair{ a: 1, b: 2 }; + let left = &mut p.a; + p.a = 3; + left.^ = 4; +} diff --git a/fec/tests/own/badridx.fe b/fec/tests/own/badridx.fe new file mode 100644 index 0000000..d7597c5 --- /dev/null +++ b/fec/tests/own/badridx.fe @@ -0,0 +1,10 @@ +// ERROR:7:borrow +unit badridx; + +fn bad() -> void { + var xs: [2]i32 = [1, 2]; + let a = &mut xs[0]; + let b = &mut xs[1]; + a.^ = 3; + b.^ = 4; +} diff --git a/fec/tests/own/badrsame.fe b/fec/tests/own/badrsame.fe new file mode 100644 index 0000000..b2e6342 --- /dev/null +++ b/fec/tests/own/badrsame.fe @@ -0,0 +1,12 @@ +// ERROR:9:borrow +unit badrsame; + +struct Pair { a: i32, b: i32, } + +fn bad() -> void { + var p = Pair{ a: 1, b: 2 }; + let one = &mut p.a; + let two = &mut p.a; + one.^ = 1; + two.^ = 2; +} diff --git a/fec/tests/own/badscop.fe b/fec/tests/own/badscop.fe new file mode 100644 index 0000000..3bbeded --- /dev/null +++ b/fec/tests/own/badscop.fe @@ -0,0 +1,12 @@ +// ERROR:9:reference +unit badscop; + +fn bad(cond: bool) -> i32 { + let outer: i32 = 0; + var r: &i32 = &outer; + if cond { + let inner: i32 = 3; + r = &inner; + } + return r.^; +} diff --git a/fec/tests/own/badself.fe b/fec/tests/own/badself.fe new file mode 100644 index 0000000..fe80c84 --- /dev/null +++ b/fec/tests/own/badself.fe @@ -0,0 +1,10 @@ +// ERROR:8:self +unit badself; + +struct Box { + value: i32, + + fn bad(self: Self) -> &i32 { + return &self.value; + } +} diff --git a/fec/tests/own/badshwr.fe b/fec/tests/own/badshwr.fe new file mode 100644 index 0000000..015d14f --- /dev/null +++ b/fec/tests/own/badshwr.fe @@ -0,0 +1,11 @@ +// ERROR:9:borrow +unit badshwr; + +fn read(r: &i32) -> i32 { return r.^; } + +fn bad() -> i32 { + var x: i32 = 0; + let r = &x; + x = 1; + return read(r); +} diff --git a/fec/tests/own/badtwo.fe b/fec/tests/own/badtwo.fe new file mode 100644 index 0000000..dcdfa0e --- /dev/null +++ b/fec/tests/own/badtwo.fe @@ -0,0 +1,6 @@ +// ERROR:5:reference +unit badtwo; + +fn choose(a: &i32, b: &i32) -> &i32 { + return a; +} diff --git a/fec/tests/own/badup.fe b/fec/tests/own/badup.fe new file mode 100644 index 0000000..34f2b7e --- /dev/null +++ b/fec/tests/own/badup.fe @@ -0,0 +1,10 @@ +// ERROR:8:mut +unit badup; + +struct Box { + value: i32, + + fn bad(self: &Self) -> &mut i32 { + return &mut self.value; + } +} diff --git a/fec/tests/own/badweak.fe b/fec/tests/own/badweak.fe new file mode 100644 index 0000000..8da6bc8 --- /dev/null +++ b/fec/tests/own/badweak.fe @@ -0,0 +1,9 @@ +// ERROR:7:mut +unit badweak; + +fn bad() -> void { + var x: i32 = 0; + let m = &mut x; + let s: &i32 = m; + let v = s.^; +} diff --git a/fec/tests/own/bdefret.fe b/fec/tests/own/bdefret.fe new file mode 100644 index 0000000..045b296 --- /dev/null +++ b/fec/tests/own/bdefret.fe @@ -0,0 +1,10 @@ +// ERROR:8:cannot return from inside defer +unit bdefret; + +// A deferred block runs during scope cleanup, on the way out of a function +// that has already decided what it returns. + +fn bad() -> i32 { + defer { return 1; } + return 0; +} diff --git a/fec/tests/own/globalm.fe b/fec/tests/own/globalm.fe new file mode 100644 index 0000000..1a2ed8d --- /dev/null +++ b/fec/tests/own/globalm.fe @@ -0,0 +1,9 @@ +// ERROR:7:global +unit globalm; + +var VALUE: i32 = 0; + +fn bad() -> void { + let r = &mut VALUE; + r.^ = 1; +} diff --git a/fec/tests/own/localesc.fe b/fec/tests/own/localesc.fe new file mode 100644 index 0000000..1d62f43 --- /dev/null +++ b/fec/tests/own/localesc.fe @@ -0,0 +1,7 @@ +// ERROR:6:reference +unit localesc; + +fn bad() -> []u8 { + let a: [2]u8 = [1 as u8, 2 as u8]; + return a[..]; +} diff --git a/fec/tests/own/ok_defer.fe b/fec/tests/own/ok_defer.fe new file mode 100644 index 0000000..e4e2fef --- /dev/null +++ b/fec/tests/own/ok_defer.fe @@ -0,0 +1,5 @@ +unit ok_defer; + +pub fn cleanup(p: ^i32) -> void { + defer { mem.destroy(p); } +} diff --git a/fec/tests/own/ok_owned.fe b/fec/tests/own/ok_owned.fe new file mode 100644 index 0000000..2881628 --- /dev/null +++ b/fec/tests/own/ok_owned.fe @@ -0,0 +1,9 @@ +unit ok_owned; + +fn main() -> !void { + var p: ^i32 = try mem.create(0); + p = try mem.create(0); + p.^ = 7; + let value: i32 = p.^; + defer { mem.destroy(p); } +} diff --git a/fec/tests/own/okbranch.fe b/fec/tests/own/okbranch.fe new file mode 100644 index 0000000..f6964b8 --- /dev/null +++ b/fec/tests/own/okbranch.fe @@ -0,0 +1,15 @@ +unit okbranch; + +fn read(r: &i32) -> i32 { return r.^; } + +fn test(cond: bool) -> i32 { + var x: i32 = 3; + let r = &x; + if cond { + let a = read(r); + } else { + let b = read(r); + } + x += 1; + return x; +} diff --git a/fec/tests/own/okdefer.fe b/fec/tests/own/okdefer.fe new file mode 100644 index 0000000..1556b46 --- /dev/null +++ b/fec/tests/own/okdefer.fe @@ -0,0 +1,13 @@ +unit okdefer; + +fn read(r: &i32) -> i32 { return r.^; } + +fn test() -> i32 { + var x: i32 = 3; + if true { + let r = &x; + defer { let y = read(r); } + } + x += 1; + return x; +} diff --git a/fec/tests/own/okforrd.fe b/fec/tests/own/okforrd.fe new file mode 100644 index 0000000..3451761 --- /dev/null +++ b/fec/tests/own/okforrd.fe @@ -0,0 +1,11 @@ +unit okforrd; + +// A shared walk still allows reading -- of the item and of the container. + +fn ok(xs: []i32) -> i32 { + var sum: i32 = 0; + for x in xs { + sum = sum + x.^ + (xs.n as i32); + } + return sum; +} diff --git a/fec/tests/own/okglobcp.fe b/fec/tests/own/okglobcp.fe new file mode 100644 index 0000000..e0c3e95 --- /dev/null +++ b/fec/tests/own/okglobcp.fe @@ -0,0 +1,10 @@ +unit okglobcp; + +var VALUE: i32 = 7; + +fn read(r: &i32) -> i32 { return r.^; } + +fn test() -> i32 { + let local = VALUE; + return read(&local); +} diff --git a/fec/tests/own/oklast.fe b/fec/tests/own/oklast.fe new file mode 100644 index 0000000..2e1e9f1 --- /dev/null +++ b/fec/tests/own/oklast.fe @@ -0,0 +1,9 @@ +unit oklast; + +fn test() -> i32 { + var x: i32 = 0; + let r = &mut x; + r.^ = 1; + x += 1; + return x; +} diff --git a/fec/tests/own/okr8free.fe b/fec/tests/own/okr8free.fe new file mode 100644 index 0000000..0ae189e --- /dev/null +++ b/fec/tests/own/okr8free.fe @@ -0,0 +1,13 @@ +unit okr8free; + +fn head(s: []u8) -> &u8 { + return &s[0]; +} + +fn test() -> u8 { + var a: [2]u8 = [1 as u8, 2 as u8]; + let r = head(a[..]); + let v = r.^; + a[0] = 7 as u8; + return v; +} diff --git a/fec/tests/own/okr8join.fe b/fec/tests/own/okr8join.fe new file mode 100644 index 0000000..b2e67dd --- /dev/null +++ b/fec/tests/own/okr8join.fe @@ -0,0 +1,14 @@ +unit okr8join; + +fn select(s: str, from_param: bool) -> str { + if from_param { return s; } + return "static"; +} + +fn test() -> u8 { + var bytes: [1]u8 = [7 as u8]; + let view = select(bytes[..], true); + let value = view[0]; + bytes[0] = 8 as u8; + return value; +} diff --git a/fec/tests/own/okr8meth.fe b/fec/tests/own/okr8meth.fe new file mode 100644 index 0000000..8f49258 --- /dev/null +++ b/fec/tests/own/okr8meth.fe @@ -0,0 +1,17 @@ +unit okr8meth; + +struct Box { + value: i32, + + pub fn get(self: &Self) -> &i32 { + return &self.value; + } +} + +fn test() -> i32 { + var b = Box{ value: 4 }; + let r = b.get(); + let v = r.^; + b.value = 5; + return v; +} diff --git a/fec/tests/own/okr8stat.fe b/fec/tests/own/okr8stat.fe new file mode 100644 index 0000000..5db67d4 --- /dev/null +++ b/fec/tests/own/okr8stat.fe @@ -0,0 +1,5 @@ +unit okr8stat; + +fn name() -> str { + return "main"; +} diff --git a/fec/tests/own/okrebor.fe b/fec/tests/own/okrebor.fe new file mode 100644 index 0000000..b92ebf8 --- /dev/null +++ b/fec/tests/own/okrebor.fe @@ -0,0 +1,11 @@ +unit okrebor; + +fn read(r: &i32) -> i32 { return r.^; } + +fn test() -> i32 { + var x: i32 = 1; + let r = &mut x; + let v = read(r); + r.^ = v + 1; + return r.^; +} diff --git a/fec/tests/own/okrfld.fe b/fec/tests/own/okrfld.fe new file mode 100644 index 0000000..5d68c28 --- /dev/null +++ b/fec/tests/own/okrfld.fe @@ -0,0 +1,14 @@ +unit okrfld; + +// Borrowing one field leaves the others alone. `p.a` and `p.b` are different +// places, so a borrow of one says nothing about the other. + +struct Pair { a: i32, b: i32, } + +fn ok() -> i32 { + var p = Pair{ a: 1, b: 2 }; + let left = &mut p.a; + p.b = 3; + left.^ = 4; + return p.a + p.b; +} diff --git a/fec/tests/own/okrtlast.fe b/fec/tests/own/okrtlast.fe new file mode 100644 index 0000000..bd87c4e --- /dev/null +++ b/fec/tests/own/okrtlast.fe @@ -0,0 +1,11 @@ +unit okrtlast; + +struct Pair { a: i32, b: i32, } + +fn test() -> i32 { + var p = Pair{ a: 1, b: 2 }; + let left = &mut p.a; + left.^ = 3; + p.b = 4; + return p.a + p.b; +} diff --git a/fec/tests/own/okshare.fe b/fec/tests/own/okshare.fe new file mode 100644 index 0000000..1740394 --- /dev/null +++ b/fec/tests/own/okshare.fe @@ -0,0 +1,12 @@ +unit okshare; + +fn add(a: &i32, b: &i32) -> i32 { + return a.^ + b.^; +} + +fn test() -> i32 { + let x: i32 = 4; + let a = &x; + let b = &x; + return add(a, b); +} diff --git a/fec/tests/own/okslreb.fe b/fec/tests/own/okslreb.fe new file mode 100644 index 0000000..7b8a6a7 --- /dev/null +++ b/fec/tests/own/okslreb.fe @@ -0,0 +1,11 @@ +unit okslreb; + +fn first(s: []u8) -> u8 { return s[0]; } + +fn test() -> u8 { + var a: [2]u8 = [1 as u8, 2 as u8]; + var s = a[..]; + let v = first(s); + s[0] = 9 as u8; + return v; +} diff --git a/fec/tests/own/okstatic.fe b/fec/tests/own/okstatic.fe new file mode 100644 index 0000000..ae2487e --- /dev/null +++ b/fec/tests/own/okstatic.fe @@ -0,0 +1,11 @@ +unit okstatic; + +static VALUE: i32 = 7; + +fn get() -> &i32 { + return &VALUE; +} + +fn test() -> i32 { + return get().^; +} diff --git a/fec/tests/own/oktemp.fe b/fec/tests/own/oktemp.fe new file mode 100644 index 0000000..d88b1f7 --- /dev/null +++ b/fec/tests/own/oktemp.fe @@ -0,0 +1,10 @@ +unit oktemp; + +fn read(r: &i32) -> i32 { return r.^; } + +fn test() -> i32 { + var x: i32 = 1; + let v = read(&x); + x += 1; + return v + x; +} diff --git a/fec/tests/own/oktrim.fe b/fec/tests/own/oktrim.fe new file mode 100644 index 0000000..cf416ac --- /dev/null +++ b/fec/tests/own/oktrim.fe @@ -0,0 +1,5 @@ +unit oktrim; + +fn trimmed(line: str) -> str { + return line.trim(); +} diff --git a/fec/tests/own/okwcall.fe b/fec/tests/own/okwcall.fe new file mode 100644 index 0000000..a53efe2 --- /dev/null +++ b/fec/tests/own/okwcall.fe @@ -0,0 +1,11 @@ +unit okwcall; + +fn read(r: &i32) -> i32 { return r.^; } + +fn test() -> i32 { + var x: i32 = 5; + let m = &mut x; + let value = read(m); + m.^ = value + 1; + return m.^; +} diff --git a/fec/tests/own/self_fld.fe b/fec/tests/own/self_fld.fe new file mode 100644 index 0000000..c025b1f --- /dev/null +++ b/fec/tests/own/self_fld.fe @@ -0,0 +1,6 @@ +// ERROR:5:reference +unit self_fld; + +struct Bad { + bytes: []u8, +} diff --git a/fec/tests/parse/badecode.fe b/fec/tests/parse/badecode.fe new file mode 100644 index 0000000..eab8829 --- /dev/null +++ b/fec/tests/parse/badecode.fe @@ -0,0 +1,5 @@ +// ERROR:5:integer literal +unit badecode; + +// SPEC §6.1: error code 는 정수 리터럴이다. 식이면 중복도 예약된 0 도 검사할 수 없다. +error E { Bad = 1 + 2, } diff --git a/fec/tests/parse/bademen.fe b/fec/tests/parse/bademen.fe new file mode 100644 index 0000000..9c72977 --- /dev/null +++ b/fec/tests/parse/bademen.fe @@ -0,0 +1,3 @@ +// ERROR:3:at least one variant +unit bademen; +enum E { } diff --git a/fec/tests/parse/bademer.fe b/fec/tests/parse/bademer.fe new file mode 100644 index 0000000..a690859 --- /dev/null +++ b/fec/tests/parse/bademer.fe @@ -0,0 +1,3 @@ +// ERROR:3:at least one member +unit bademer; +error E { } diff --git a/fec/tests/parse/badgtype.fe b/fec/tests/parse/badgtype.fe new file mode 100644 index 0000000..ed9c9a1 --- /dev/null +++ b/fec/tests/parse/badgtype.fe @@ -0,0 +1,6 @@ +// ERROR:6:requires an explicit type +unit badgtype; + +// SPEC §6.1: 전역은 타입을 적는다. 다른 유닛이 읽는 링커 심볼이라 +// 초기값의 생김새에 타입을 맡기면 그쪽이 보는 것이 달라진다. +static A = 1; diff --git a/fec/tests/parse/basic.fe b/fec/tests/parse/basic.fe new file mode 100644 index 0000000..86d4062 --- /dev/null +++ b/fec/tests/parse/basic.fe @@ -0,0 +1,25 @@ +unit basic; +import core; + +/* outer comment /* nested comment */ still active */ +const LIMIT: u16 = 1_000; +pub struct Point { + pub x: i32, + y: i32, + pub fn new(x: i32, y: i32) -> Point { return Point{ x: x, y: y }; } + pub fn shift(self: &mut Self, dx: i32) { self.x += dx; } +} +pub enum Shape { + Empty, + Circle(i32), + Rect{ w: i32, h: i32 }, +} +pub error IoError { NotFound = 1, Denied = 2, } + +pub fn main() -> !void { + let p: Point = Point{ x: 1, y: 2 }; + if p.x > 0 and true { p.shift(1); } else { p.x = 0; } + while p.x < 10 { p.x += 1; if p.x == 5 { continue; } } + for i, x in p.x..10 { let _n: usize = i; let _q = x; } + return; +} diff --git a/fec/tests/parse/keybuilt.fe b/fec/tests/parse/keybuilt.fe new file mode 100644 index 0000000..9f2eba6 --- /dev/null +++ b/fec/tests/parse/keybuilt.fe @@ -0,0 +1,9 @@ +unit keybuilt; + +pub fn demo() { + let a = true and not false; + let b = a or false; + @print("selected branch"); + let p = @as_far_fn(handler); + @call_far(p); +} diff --git a/fec/tests/parse/literals.fe b/fec/tests/parse/literals.fe new file mode 100644 index 0000000..775e9be --- /dev/null +++ b/fec/tests/parse/literals.fe @@ -0,0 +1,12 @@ +unit literals; +const A: u32 = 0xFF; +const B: u16 = 0b1010; +const C: u16 = 0o17; +const D: u32 = 1_000_000; +pub fn strings() { + let a = "hello\\nworld"; + let b = '\x41'; + let c = true; + let d = null; + let e = a orelse "fallback"; +} diff --git a/fec/tests/parse/logical.fe b/fec/tests/parse/logical.fe new file mode 100644 index 0000000..8137071 --- /dev/null +++ b/fec/tests/parse/logical.fe @@ -0,0 +1,3 @@ +// ERROR:logical operator +unit logical; +fn main() { let x = true && false; } diff --git a/fec/tests/parse/misssemi.fe b/fec/tests/parse/misssemi.fe new file mode 100644 index 0000000..a43b432 --- /dev/null +++ b/fec/tests/parse/misssemi.fe @@ -0,0 +1,3 @@ +// ERROR:expected ';' +unit misssemi; +fn main() { let x: i32 = 1 } diff --git a/fec/tests/parse/okglobal.fe b/fec/tests/parse/okglobal.fe new file mode 100644 index 0000000..02443a6 --- /dev/null +++ b/fec/tests/parse/okglobal.fe @@ -0,0 +1,8 @@ +unit okglobal; + +// `const` 는 여전히 추론한다 -- SPEC §6.1 이 타입을 요구하는 것은 전역 둘뿐이다. +const A = 1; +static B: i32 = 2; +var C: i32 = 3; + +error E { Bad = 1, Worse = 0x10, } diff --git a/fec/tests/parse/unclcomm.fe b/fec/tests/parse/unclcomm.fe new file mode 100644 index 0000000..7cab161 --- /dev/null +++ b/fec/tests/parse/unclcomm.fe @@ -0,0 +1,3 @@ +// ERROR:unterminated block comment +unit unclcomm; +/* no ending delimiter diff --git a/fec/tests/parse/v012form.fe b/fec/tests/parse/v012form.fe new file mode 100644 index 0000000..6175e55 --- /dev/null +++ b/fec/tests/parse/v012form.fe @@ -0,0 +1,18 @@ +unit v012form; + +shared atomic var ticks: u16 = 0; +packed struct Packet { + tag: u8, + value: u16, +} +interrupt_safe fn poll() { } +interrupt fn timer() { } + +pub fn demo() { + var count = undefined; + count = 1; + critical { ticks += 1; } + let x = true and not false or false; + let y = x orelse true; + let e = error.NotFound; +} diff --git a/fec/tests/types/bad_ari.fe b/fec/tests/types/bad_ari.fe new file mode 100644 index 0000000..4a26425 --- /dev/null +++ b/fec/tests/types/bad_ari.fe @@ -0,0 +1,10 @@ +// ERROR:9:wrong number of arguments +unit bad_ari; + +fn add(a: i32, b: i32) -> i32 { + return a + b; +} + +fn main() -> i32 { + return add(1); +} diff --git a/fec/tests/types/bad_asgn.fe b/fec/tests/types/bad_asgn.fe new file mode 100644 index 0000000..8171754 --- /dev/null +++ b/fec/tests/types/bad_asgn.fe @@ -0,0 +1,8 @@ +// ERROR:6:cannot assign to immutable let +unit bad_asgn; + +fn main() -> i32 { + let value: i32 = 1; + value = 2; + return value; +} diff --git a/fec/tests/types/bad_cast.fe b/fec/tests/types/bad_cast.fe new file mode 100644 index 0000000..5881636 --- /dev/null +++ b/fec/tests/types/bad_cast.fe @@ -0,0 +1,7 @@ +// ERROR:5:'as' requires integer or char types +unit bad_cast; + +fn main() -> i32 { + let x: i32 = true as i32; + return x; +} diff --git a/fec/tests/types/bad_cond.fe b/fec/tests/types/bad_cond.fe new file mode 100644 index 0000000..72e2347 --- /dev/null +++ b/fec/tests/types/bad_cond.fe @@ -0,0 +1,7 @@ +// ERROR:5:if condition must be bool +unit bad_cond; + +fn main() -> i32 { + if 1 { return 0; } + return 1; +} diff --git a/fec/tests/types/bad_mlet.fe b/fec/tests/types/bad_mlet.fe new file mode 100644 index 0000000..7211577 --- /dev/null +++ b/fec/tests/types/bad_mlet.fe @@ -0,0 +1,7 @@ +// ERROR:6:let cannot bind a mutable slice +unit bad_mlet; + +fn bad() -> void { + var raw: [2]u8 = [1, 2]; + let s: []mut u8 = raw[..]; +} diff --git a/fec/tests/types/bad_ret.fe b/fec/tests/types/bad_ret.fe new file mode 100644 index 0000000..367a1f5 --- /dev/null +++ b/fec/tests/types/bad_ret.fe @@ -0,0 +1,6 @@ +// ERROR:5:return type mismatch +unit bad_ret; + +fn main() -> i32 { + return true; +} diff --git a/fec/tests/types/bad_shwr.fe b/fec/tests/types/bad_shwr.fe new file mode 100644 index 0000000..c67fc29 --- /dev/null +++ b/fec/tests/types/bad_shwr.fe @@ -0,0 +1,6 @@ +// ERROR:5:cannot write through shared slice +unit bad_shwr; + +fn bad(s: []u8) -> void { + s[0] = 1; +} diff --git a/fec/tests/types/bad_type.fe b/fec/tests/types/bad_type.fe new file mode 100644 index 0000000..ba421b9 --- /dev/null +++ b/fec/tests/types/bad_type.fe @@ -0,0 +1,10 @@ +// ERROR:9:argument type mismatch +unit bad_type; + +fn add(a: i32, b: i32) -> i32 { + return a + b; +} + +fn main() -> i32 { + return add(true, 1); +} diff --git a/fec/tests/types/bad_unit.fe b/fec/tests/types/bad_unit.fe new file mode 100644 index 0000000..be33f54 --- /dev/null +++ b/fec/tests/types/bad_unit.fe @@ -0,0 +1,7 @@ +// ERROR:6:use of uninitialized variable +unit bad_unit; + +fn main() -> i32 { + var value: i32; + return value; +} diff --git a/fec/tests/types/bad_unk.fe b/fec/tests/types/bad_unk.fe new file mode 100644 index 0000000..12ab2c4 --- /dev/null +++ b/fec/tests/types/bad_unk.fe @@ -0,0 +1,6 @@ +// ERROR:5:unknown name +unit bad_unk; + +fn main() -> i32 { + return missing_name; +} diff --git a/fec/tests/types/bad_void.fe b/fec/tests/types/bad_void.fe new file mode 100644 index 0000000..4f983f2 --- /dev/null +++ b/fec/tests/types/bad_void.fe @@ -0,0 +1,11 @@ +// ERROR:9:initializer type mismatch +unit bad_void; + +fn noop() { + return; +} + +fn main() -> i32 { + let value: i32 = noop(); + return value; +} diff --git a/fec/tests/types/badarr.fe b/fec/tests/types/badarr.fe new file mode 100644 index 0000000..0fc94b5 --- /dev/null +++ b/fec/tests/types/badarr.fe @@ -0,0 +1,6 @@ +// ERROR:4:array element type mismatch +unit badarr; +fn main() -> i32 { + let a: [2]i32 = [1, true, 3]; + return a[0]; +} diff --git a/fec/tests/types/badasm.fe b/fec/tests/types/badasm.fe new file mode 100644 index 0000000..9cb70e4 --- /dev/null +++ b/fec/tests/types/badasm.fe @@ -0,0 +1,3 @@ +// ERROR:3:unsafe block +unit badasm; +fn f() -> void { asm { "nop" } return; } diff --git a/fec/tests/types/badchain.fe b/fec/tests/types/badchain.fe new file mode 100644 index 0000000..9aea447 --- /dev/null +++ b/fec/tests/types/badchain.fe @@ -0,0 +1,3 @@ +// ERROR:3:do not chain +unit badchain; +fn f() -> bool { return true == false == true; } diff --git a/fec/tests/types/badchar.fe b/fec/tests/types/badchar.fe new file mode 100644 index 0000000..dbe2c10 --- /dev/null +++ b/fec/tests/types/badchar.fe @@ -0,0 +1,7 @@ +// ERROR:5:initializer type mismatch +unit badchar; + +fn main() -> i32 { + let u: u8 = 'A'; + return u; +} diff --git a/fec/tests/types/badcini.fe b/fec/tests/types/badcini.fe new file mode 100644 index 0000000..c52a9dd --- /dev/null +++ b/fec/tests/types/badcini.fe @@ -0,0 +1,4 @@ +// ERROR:4:known at compile time +unit badcini; +fn r() -> i32 { return 1; } +const A: i32 = r(); diff --git a/fec/tests/types/badcycle.fe b/fec/tests/types/badcycle.fe new file mode 100644 index 0000000..94fbd43 --- /dev/null +++ b/fec/tests/types/badcycle.fe @@ -0,0 +1,7 @@ +// ERROR:2:by-value recursive type +unit badcycle; + +struct A { b: B, } +struct B { a: A, } + +fn main() -> i32 { return 0; } diff --git a/fec/tests/types/badexab.fe b/fec/tests/types/badexab.fe new file mode 100644 index 0000000..1ab2d2a --- /dev/null +++ b/fec/tests/types/badexab.fe @@ -0,0 +1,3 @@ +// ERROR:3:only ABI +unit badexab; +extern "stdcall" fn f(); diff --git a/fec/tests/types/badexbd.fe b/fec/tests/types/badexbd.fe new file mode 100644 index 0000000..06252f5 --- /dev/null +++ b/fec/tests/types/badexbd.fe @@ -0,0 +1,3 @@ +// ERROR:3:no body +unit badexbd; +extern "c" fn f() -> i32 { return 1; } diff --git a/fec/tests/types/badexns.fe b/fec/tests/types/badexns.fe new file mode 100644 index 0000000..b51ce2f --- /dev/null +++ b/fec/tests/types/badexns.fe @@ -0,0 +1,3 @@ +// ERROR:3:ABI string +unit badexns; +extern fn f(); diff --git a/fec/tests/types/badfield.fe b/fec/tests/types/badfield.fe new file mode 100644 index 0000000..bfda363 --- /dev/null +++ b/fec/tests/types/badfield.fe @@ -0,0 +1,9 @@ +// ERROR:7:cannot assign through immutable value +unit badfield; + +struct Point { x: i32, y: i32, } +fn main() -> i32 { + let p: Point = Point{ x: 1, y: 2 }; + p.x = 3; + return p.x; +} diff --git a/fec/tests/types/badfld.fe b/fec/tests/types/badfld.fe new file mode 100644 index 0000000..8ef5366 --- /dev/null +++ b/fec/tests/types/badfld.fe @@ -0,0 +1,7 @@ +// ERROR:5:missing struct field +unit badfld; +struct Point { x: i32, y: i32, } +fn main() -> i32 { + let p: Point = Point{ x: 1 }; + return p.z; +} diff --git a/fec/tests/types/badfnsm.fe b/fec/tests/types/badfnsm.fe new file mode 100644 index 0000000..cd3c37a --- /dev/null +++ b/fec/tests/types/badfnsm.fe @@ -0,0 +1,3 @@ +// ERROR:4:must be extern +unit badfnsm; +fn f() -> i32; diff --git a/fec/tests/types/badindex.fe b/fec/tests/types/badindex.fe new file mode 100644 index 0000000..9db50a9 --- /dev/null +++ b/fec/tests/types/badindex.fe @@ -0,0 +1,8 @@ +// ERROR:6:cannot assign through immutable value +unit badindex; + +fn main() -> i32 { + let a: [2]i32 = [1, 2]; + a[0] = 3; + return a[0]; +} diff --git a/fec/tests/types/badletwk.fe b/fec/tests/types/badletwk.fe new file mode 100644 index 0000000..c1c6d5c --- /dev/null +++ b/fec/tests/types/badletwk.fe @@ -0,0 +1,8 @@ +// ERROR:6:cannot rebind a mut borrow +unit badletwk; + +// 반환은 약화해도 `let` 은 여전히 안 된다 (SPEC §4.2). +fn bad(m: &mut i32) -> i32 { + let s: &i32 = m; + return s.^; +} diff --git a/fec/tests/types/badlit.fe b/fec/tests/types/badlit.fe new file mode 100644 index 0000000..ede7d0a --- /dev/null +++ b/fec/tests/types/badlit.fe @@ -0,0 +1,10 @@ +// ERROR:8:integer literal out of range +unit badlit; + +// SPEC 4.1: an integer literal takes the type its context asks for. 300 is not +// a u8, and truncating it silently is the one thing that must not happen. + +fn bad() -> u8 { + let n: u8 = 300; + return n; +} diff --git a/fec/tests/types/badlitng.fe b/fec/tests/types/badlitng.fe new file mode 100644 index 0000000..b222970 --- /dev/null +++ b/fec/tests/types/badlitng.fe @@ -0,0 +1,8 @@ +// ERROR:6:integer literal out of range +unit badlitng; + +// An unsigned type has no negative values to truncate to. +fn bad() -> u8 { + let n: u8 = -1; + return n; +} diff --git a/fec/tests/types/badmat.fe b/fec/tests/types/badmat.fe new file mode 100644 index 0000000..cc235a7 --- /dev/null +++ b/fec/tests/types/badmat.fe @@ -0,0 +1,7 @@ +// ERROR:5:non-exhaustive match +unit badmat; +enum Shape { Empty, Circle(i32), } +fn main() -> i32 { + match Shape.Empty { Empty => 0; } + return 0; +} diff --git a/fec/tests/types/badsini.fe b/fec/tests/types/badsini.fe new file mode 100644 index 0000000..c585800 --- /dev/null +++ b/fec/tests/types/badsini.fe @@ -0,0 +1,4 @@ +// ERROR:4:known at compile time +unit badsini; +fn r() -> i32 { return 1; } +static A: i32 = r(); diff --git a/fec/tests/types/badstr.fe b/fec/tests/types/badstr.fe new file mode 100644 index 0000000..8bcee1f --- /dev/null +++ b/fec/tests/types/badstr.fe @@ -0,0 +1,7 @@ +// ERROR:5:cannot write through shared slice +unit badstr; +fn main() -> i32 { + var text: str = "abc"; + text[0] = 'z'; + return 0; +} diff --git a/fec/tests/types/badunas.fe b/fec/tests/types/badunas.fe new file mode 100644 index 0000000..849ef40 --- /dev/null +++ b/fec/tests/types/badunas.fe @@ -0,0 +1,3 @@ +// ERROR:3:parenthesise +unit badunas; +fn f(x: i32) -> u32 { return -x as u32; } diff --git a/fec/tests/types/ok_arra1.fe b/fec/tests/types/ok_arra1.fe new file mode 100644 index 0000000..487b1ec --- /dev/null +++ b/fec/tests/types/ok_arra1.fe @@ -0,0 +1,7 @@ +unit ok_arra1; + +fn main() -> i32 { + let bytes: [3]u8 = [1, 2, 3]; + if bytes[0] == 1 and bytes[2] == 3 { return 0; } + return 1; +} diff --git a/fec/tests/types/ok_array.fe b/fec/tests/types/ok_array.fe new file mode 100644 index 0000000..bf748bc --- /dev/null +++ b/fec/tests/types/ok_array.fe @@ -0,0 +1,9 @@ +unit ok_array; + +fn main() -> i32 { + let a: [3]i32 = [1, 2, 3]; + let s: []i32 = a[..]; + let t: []i32 = s[1..3]; + if a[0] + s[1] + t[0] == 5 and s.n == 3 { return 0; } + return 1; +} diff --git a/fec/tests/types/ok_castw.fe b/fec/tests/types/ok_castw.fe new file mode 100644 index 0000000..2e962f5 --- /dev/null +++ b/fec/tests/types/ok_castw.fe @@ -0,0 +1,11 @@ +unit ok_castw; + +pub fn main() -> i32 { + var x: i16 = 0; + while x < 3 { + x += 1; + } + let y: i32 = x as i32; + if y == 3 { return 0; } + return 1; +} diff --git a/fec/tests/types/ok_char.fe b/fec/tests/types/ok_char.fe new file mode 100644 index 0000000..e6939b7 --- /dev/null +++ b/fec/tests/types/ok_char.fe @@ -0,0 +1,9 @@ +unit ok_char; + +fn main() -> i32 { + let c: char = '\u0041'; + let u: u8 = c as u8; + let d: char = u as char; + if c == d and u == ('A' as u8) { return 0; } + return 1; +} diff --git a/fec/tests/types/ok_enum.fe b/fec/tests/types/ok_enum.fe new file mode 100644 index 0000000..581cbb3 --- /dev/null +++ b/fec/tests/types/ok_enum.fe @@ -0,0 +1,16 @@ +unit ok_enum; + +enum Shape { Empty, Circle(i32), Rect { w: i32, h: i32, }, } + +fn score(s: Shape) -> i32 { + match s { + Empty => { return 0; } + Circle(value) => { return value; } + Rect { w, h } => { return w * h; } + } +} + +fn main() -> i32 { + if score(Shape.Circle(5)) == 5 and score(Shape.Rect{ w: 2, h: 3 }) == 6 { return 0; } + return 1; +} diff --git a/fec/tests/types/ok_for.fe b/fec/tests/types/ok_for.fe new file mode 100644 index 0000000..3aab7bf --- /dev/null +++ b/fec/tests/types/ok_for.fe @@ -0,0 +1,18 @@ +unit ok_for; + +fn main() -> i32 { + var total: i32 = 0; + let a: [3]i32 = [1, 2, 3]; + let s: []i32 = a[..]; + var m: [1]i32 = [1]; + let ready: bool = true; + if ready { total += 0; } + while false { total += 100; } + for x in a { total += x.^; } + for i, x in a { if i == 1 and x.^ == 2 { total += 1; } } + for x in "ab" { if x.^ == ('a' as u8) { total += 1; } } + for x in s { if x.^ == 3 { total += 1; } } + for x in m { x.^ = 2; } + if total == 9 and m[0] == 2 { return 0; } + return 1; +} diff --git a/fec/tests/types/ok_hello.fe b/fec/tests/types/ok_hello.fe new file mode 100644 index 0000000..e55fedb --- /dev/null +++ b/fec/tests/types/ok_hello.fe @@ -0,0 +1,23 @@ +unit ok_hello; + +fn add(a: i32, b: i32) -> i32 { + return a + b; +} + +fn is_answer(value: i32) -> bool { + return value == 42; +} + +fn touch() { + return; +} + +pub fn main() -> i32 { + touch(); + let value: i32 = add(20, 22); + if is_answer(value) { + return 0; + } else { + return 1; + } +} diff --git a/fec/tests/types/ok_mutab.fe b/fec/tests/types/ok_mutab.fe new file mode 100644 index 0000000..dc596a2 --- /dev/null +++ b/fec/tests/types/ok_mutab.fe @@ -0,0 +1,11 @@ +unit ok_mutab; + +fn takes_shared(s: []u8) -> u8 { return s[0]; } + +fn main() -> i32 { + var raw: [3]u8 = [1, 2, 3]; + var s: []mut u8 = raw[..]; + s[1] = 9; + if takes_shared(s) == 1 and raw[1] == 9 { return 0; } + return 1; +} diff --git a/fec/tests/types/ok_neste.fe b/fec/tests/types/ok_neste.fe new file mode 100644 index 0000000..e8c0485 --- /dev/null +++ b/fec/tests/types/ok_neste.fe @@ -0,0 +1,10 @@ +unit ok_neste; + +struct Outer { inner: Inner, } +struct Inner { value: i32, } + +fn main() -> i32 { + let x: Outer = Outer{ inner: Inner{ value: 7 } }; + if x.inner.value == 7 { return 0; } + return 1; +} diff --git a/fec/tests/types/ok_scope.fe b/fec/tests/types/ok_scope.fe new file mode 100644 index 0000000..153398f --- /dev/null +++ b/fec/tests/types/ok_scope.fe @@ -0,0 +1,15 @@ +unit ok_scope; + +fn register(switch: i32) -> i32 { + let auto: i32 = switch; + if true { + let auto: i32 = auto + 1; + if auto == 2 { return 0; } + } + if auto == 1 { return 0; } + return 1; +} + +pub fn main() -> i32 { + return register(1); +} diff --git a/fec/tests/types/ok_str.fe b/fec/tests/types/ok_str.fe new file mode 100644 index 0000000..fec3064 --- /dev/null +++ b/fec/tests/types/ok_str.fe @@ -0,0 +1,7 @@ +unit ok_str; + +fn main() -> i32 { + let text: str = "abc"; + if text[1] == ('b' as u8) and text.n == 3 { return 0; } + return 1; +} diff --git a/fec/tests/types/ok_struc.fe b/fec/tests/types/ok_struc.fe new file mode 100644 index 0000000..1754eb9 --- /dev/null +++ b/fec/tests/types/ok_struc.fe @@ -0,0 +1,14 @@ +unit ok_struc; + +struct Point { x: i32, y: i32, } +packed struct PackedPoint { x: u8, y: i32, } +struct Natural { a: u8, b: i32, } + +fn main() -> i32 { + let p: Point = Point{ x: 3, y: 4 }; + let q: PackedPoint = PackedPoint{ x: 1, y: 6 }; + let n: Natural = Natural{ a: 1, b: 2 }; + if p.x + p.y == 7 and q.y == 6 and n.b == 2 and + (Point{ x: 1, y: 2 }.x == 1) and @size_of(Natural) == 8 { return 0; } + return 1; +} diff --git a/fec/tests/types/okglobin.fe b/fec/tests/types/okglobin.fe new file mode 100644 index 0000000..9549013 --- /dev/null +++ b/fec/tests/types/okglobin.fe @@ -0,0 +1,23 @@ +unit okglobin; + +// 거부되는 것들의 짝. 전역 초기값은 컴파일 시점에 알 수 있어야 하고, +// extern 은 ABI 를 대고 본문이 없으며, 비교는 괄호로 묶으면 이어 쓸 수 있고, +// 단항 뒤의 as 는 괄호가 어느 쪽인지 말해주면 되고, asm 은 unsafe 안이면 된다. +import std.sys; + +const A: i32 = 1; +const B: i32 = A + 2; +const S: str = "hi"; +static C: i32 = -A; +var D: u32 = 0xFF; + +enum E { One, Two, } +error Er { Bad = 1, } + +extern "c" fn fe_rt_allocs() -> i32; + +fn f(x: i32) -> u32 { return (-x) as u32; } +fn g(x: i32) -> u32 { return -(x as u32); } +fn h(a: i32, b: i32, c: i32) -> bool { return a < b and b < c; } +fn i(a: i32, b: i32) -> bool { return (a == b) == true; } +fn j() -> void { unsafe { asm { "nop" } } return; } diff --git a/fec/tests/types/oklit.fe b/fec/tests/types/oklit.fe new file mode 100644 index 0000000..7394afb --- /dev/null +++ b/fec/tests/types/oklit.fe @@ -0,0 +1,17 @@ +unit oklit; + +// The edges of each type are in range, and a literal with no context is i32. + +fn ok() -> i32 { + let a: u8 = 255; + let b: i8 = -128; + let c: i8 = 127; + let d: u16 = 65535; + let e: i32 = 2147483647; + let f: i32 = -2147483648; + let g: u8 = 0xFF; + let h = 5; + let i: usize = 4294967295; + return h + (a as i32) + (b as i32) + (c as i32) + (d as i32) + + (e - e) + (f - f) + (g as i32) + (i as i32); +} diff --git a/fec/tests/types/okretwk.fe b/fec/tests/types/okretwk.fe new file mode 100644 index 0000000..f904f06 --- /dev/null +++ b/fec/tests/types/okretwk.fe @@ -0,0 +1,12 @@ +unit okretwk; + +// R8 이 파생을 허용한 뒤라면 가진 것보다 적게 넘기는 것은 안전하다. + +struct Buf { + items: ^[]mut i32, + + fn all(self: &Self) -> []i32 { return self.items.^[0..2]; } + fn all_mut(self: &mut Self) -> []mut i32 { return self.items.^[0..2]; } +} + +fn one(p: &mut i32) -> &i32 { return p; } diff --git a/fec/tests/units/README.md b/fec/tests/units/README.md new file mode 100644 index 0000000..96134df --- /dev/null +++ b/fec/tests/units/README.md @@ -0,0 +1,30 @@ +# 단위 모듈 + +이 디렉터리는 `unit` 선언, `import` 경로/별칭, 다중 파일 단위 경계를 다룹니다. + +실행 방법: + +`uv run python tests/run.py -k units` + +현재 수록 케이스: + +- `alias/` +- `basic/` +- `badlong/` +- `badupper/` +- `bindconf/` +- `cycle/` +- `dotpriv/` +- `dotted/` +- `errdet/` +- `errnom/` +- `errsame/` +- `missing/` +- `privfld/` +- `privfn/` +- `pubfld/` +- `pubpriv/` +- `unitbad/` + +미구현/미완료 구간: +현재 다수 케이스가 완전 통과하지 않고, `badlong`, `badupper`, `unitbad`는 규칙 고의 위반 케이스입니다. diff --git a/fec/tests/units/alias/acme/math.fe b/fec/tests/units/alias/acme/math.fe new file mode 100644 index 0000000..ee6d1e8 --- /dev/null +++ b/fec/tests/units/alias/acme/math.fe @@ -0,0 +1,3 @@ +unit acme.math; + +pub fn answer() -> i32 { return 42; } diff --git a/fec/tests/units/alias/main.fe b/fec/tests/units/alias/main.fe new file mode 100644 index 0000000..7e189d2 --- /dev/null +++ b/fec/tests/units/alias/main.fe @@ -0,0 +1,4 @@ +unit main; +import acme.math as calc; + +fn main() -> i32 { return calc.answer(); } diff --git a/fec/tests/units/badlong/main.fe b/fec/tests/units/badlong/main.fe new file mode 100644 index 0000000..ae41a21 --- /dev/null +++ b/fec/tests/units/badlong/main.fe @@ -0,0 +1,4 @@ +// ERROR:2:unit +unit toolonggg; + +fn main() -> void {} diff --git a/fec/tests/units/badupper/main.fe b/fec/tests/units/badupper/main.fe new file mode 100644 index 0000000..7ae7913 --- /dev/null +++ b/fec/tests/units/badupper/main.fe @@ -0,0 +1,4 @@ +// ERROR:2:unit +unit Upper; + +fn main() -> void {} diff --git a/fec/tests/units/basic/main.fe b/fec/tests/units/basic/main.fe new file mode 100644 index 0000000..98eae1e --- /dev/null +++ b/fec/tests/units/basic/main.fe @@ -0,0 +1,6 @@ +unit main; +import util; + +fn main() -> i32 { + return util.answer(); +} diff --git a/fec/tests/units/basic/util.fe b/fec/tests/units/basic/util.fe new file mode 100644 index 0000000..8fb20a6 --- /dev/null +++ b/fec/tests/units/basic/util.fe @@ -0,0 +1,5 @@ +unit util; + +pub fn answer() -> i32 { + return 42; +} diff --git a/fec/tests/units/bindconf/alpha/net.fe b/fec/tests/units/bindconf/alpha/net.fe new file mode 100644 index 0000000..02af990 --- /dev/null +++ b/fec/tests/units/bindconf/alpha/net.fe @@ -0,0 +1 @@ +unit alpha.net; diff --git a/fec/tests/units/bindconf/beta/net.fe b/fec/tests/units/bindconf/beta/net.fe new file mode 100644 index 0000000..1c5f6f1 --- /dev/null +++ b/fec/tests/units/bindconf/beta/net.fe @@ -0,0 +1 @@ +unit beta.net; diff --git a/fec/tests/units/bindconf/main.fe b/fec/tests/units/bindconf/main.fe new file mode 100644 index 0000000..6b1a90d --- /dev/null +++ b/fec/tests/units/bindconf/main.fe @@ -0,0 +1,6 @@ +// ERROR:4:binding +unit main; +import alpha.net; +import beta.net; + +fn main() -> void {} diff --git a/fec/tests/units/cycle/a.fe b/fec/tests/units/cycle/a.fe new file mode 100644 index 0000000..938e94a --- /dev/null +++ b/fec/tests/units/cycle/a.fe @@ -0,0 +1,5 @@ +// ERROR:cycle +unit a; +import b; + +pub fn a_value() -> i32 { return b.b_value(); } diff --git a/fec/tests/units/cycle/b.fe b/fec/tests/units/cycle/b.fe new file mode 100644 index 0000000..c6ee482 --- /dev/null +++ b/fec/tests/units/cycle/b.fe @@ -0,0 +1,5 @@ +// ERROR:cycle +unit b; +import a; + +pub fn b_value() -> i32 { return 1; } diff --git a/fec/tests/units/dotpriv/game/bar.fe b/fec/tests/units/dotpriv/game/bar.fe new file mode 100644 index 0000000..3dba2e9 --- /dev/null +++ b/fec/tests/units/dotpriv/game/bar.fe @@ -0,0 +1,5 @@ +// ERROR:5:private +unit game.bar; +import game.foo; + +pub fn read() -> i32 { return foo.hidden(); } diff --git a/fec/tests/units/dotpriv/game/foo.fe b/fec/tests/units/dotpriv/game/foo.fe new file mode 100644 index 0000000..ef2220e --- /dev/null +++ b/fec/tests/units/dotpriv/game/foo.fe @@ -0,0 +1,3 @@ +unit game.foo; + +fn hidden() -> i32 { return 1; } diff --git a/fec/tests/units/dotpriv/main.fe b/fec/tests/units/dotpriv/main.fe new file mode 100644 index 0000000..3873e01 --- /dev/null +++ b/fec/tests/units/dotpriv/main.fe @@ -0,0 +1,8 @@ +// ERROR:private +// The violation is in game/bar.fe, which this unit imports. Reaching it from +// any entry has to be rejected, so the marker pins the message and not a line +// in another file. +unit main; +import game.bar; + +fn main() -> i32 { return bar.read(); } diff --git a/fec/tests/units/dotted/acme/math.fe b/fec/tests/units/dotted/acme/math.fe new file mode 100644 index 0000000..ee6d1e8 --- /dev/null +++ b/fec/tests/units/dotted/acme/math.fe @@ -0,0 +1,3 @@ +unit acme.math; + +pub fn answer() -> i32 { return 42; } diff --git a/fec/tests/units/dotted/main.fe b/fec/tests/units/dotted/main.fe new file mode 100644 index 0000000..df3abae --- /dev/null +++ b/fec/tests/units/dotted/main.fe @@ -0,0 +1,4 @@ +unit main; +import acme.math; + +fn main() -> i32 { return math.answer(); } diff --git a/fec/tests/units/errdet/alpha.fe b/fec/tests/units/errdet/alpha.fe new file mode 100644 index 0000000..1d8ec1d --- /dev/null +++ b/fec/tests/units/errdet/alpha.fe @@ -0,0 +1,5 @@ +unit alpha; + +pub fn fail() -> !void { + return error.Busy; +} diff --git a/fec/tests/units/errdet/beta.fe b/fec/tests/units/errdet/beta.fe new file mode 100644 index 0000000..31e6b00 --- /dev/null +++ b/fec/tests/units/errdet/beta.fe @@ -0,0 +1,5 @@ +unit beta; + +pub fn fail() -> !void { + return error.Busy; +} diff --git a/fec/tests/units/errdet/main.fe b/fec/tests/units/errdet/main.fe new file mode 100644 index 0000000..5386bb4 --- /dev/null +++ b/fec/tests/units/errdet/main.fe @@ -0,0 +1,6 @@ +unit main; +import beta; +import alpha; + +fn one() -> !void { return beta.fail(); } +fn two() -> !void { return alpha.fail(); } diff --git a/fec/tests/units/errnom/lib.fe b/fec/tests/units/errnom/lib.fe new file mode 100644 index 0000000..58d1e15 --- /dev/null +++ b/fec/tests/units/errnom/lib.fe @@ -0,0 +1,9 @@ +unit lib; + +pub error LibError { + Bad = 1, +} + +pub fn value() -> LibError!i32 { + return LibError.Bad; +} diff --git a/fec/tests/units/errnom/main.fe b/fec/tests/units/errnom/main.fe new file mode 100644 index 0000000..ee27d19 --- /dev/null +++ b/fec/tests/units/errnom/main.fe @@ -0,0 +1,7 @@ +// ERROR:6:error +unit main; +import lib; + +fn bad() -> !i32 { + return try lib.value(); +} diff --git a/fec/tests/units/errsame/alpha.fe b/fec/tests/units/errsame/alpha.fe new file mode 100644 index 0000000..1d8ec1d --- /dev/null +++ b/fec/tests/units/errsame/alpha.fe @@ -0,0 +1,5 @@ +unit alpha; + +pub fn fail() -> !void { + return error.Busy; +} diff --git a/fec/tests/units/errsame/beta.fe b/fec/tests/units/errsame/beta.fe new file mode 100644 index 0000000..31e6b00 --- /dev/null +++ b/fec/tests/units/errsame/beta.fe @@ -0,0 +1,5 @@ +unit beta; + +pub fn fail() -> !void { + return error.Busy; +} diff --git a/fec/tests/units/errsame/main.fe b/fec/tests/units/errsame/main.fe new file mode 100644 index 0000000..0e46b1b --- /dev/null +++ b/fec/tests/units/errsame/main.fe @@ -0,0 +1,6 @@ +unit main; +import alpha; +import beta; + +fn one() -> !void { return alpha.fail(); } +fn two() -> !void { return beta.fail(); } diff --git a/fec/tests/units/missing/main.fe b/fec/tests/units/missing/main.fe new file mode 100644 index 0000000..c95ad78 --- /dev/null +++ b/fec/tests/units/missing/main.fe @@ -0,0 +1,5 @@ +// ERROR:3:import +unit main; +import absent; + +fn main() -> void {} diff --git a/fec/tests/units/privfld/data.fe b/fec/tests/units/privfld/data.fe new file mode 100644 index 0000000..cc36f31 --- /dev/null +++ b/fec/tests/units/privfld/data.fe @@ -0,0 +1,5 @@ +unit data; + +pub struct Record { + value: i32, +} diff --git a/fec/tests/units/privfld/main.fe b/fec/tests/units/privfld/main.fe new file mode 100644 index 0000000..b870879 --- /dev/null +++ b/fec/tests/units/privfld/main.fe @@ -0,0 +1,8 @@ +// ERROR:6:private +unit main; +import data; + +fn main() -> i32 { + let r = data.Record{ value: 7 }; + return r.value; +} diff --git a/fec/tests/units/privfn/main.fe b/fec/tests/units/privfn/main.fe new file mode 100644 index 0000000..d89c472 --- /dev/null +++ b/fec/tests/units/privfn/main.fe @@ -0,0 +1,7 @@ +// ERROR:6:private +unit main; +import util; + +fn main() -> i32 { + return util.hidden(); +} diff --git a/fec/tests/units/privfn/util.fe b/fec/tests/units/privfn/util.fe new file mode 100644 index 0000000..ce93270 --- /dev/null +++ b/fec/tests/units/privfn/util.fe @@ -0,0 +1,5 @@ +unit util; + +fn hidden() -> i32 { + return 1; +} diff --git a/fec/tests/units/pubfld/data.fe b/fec/tests/units/pubfld/data.fe new file mode 100644 index 0000000..7b04233 --- /dev/null +++ b/fec/tests/units/pubfld/data.fe @@ -0,0 +1,5 @@ +unit data; + +pub struct Record { + pub value: i32, +} diff --git a/fec/tests/units/pubfld/main.fe b/fec/tests/units/pubfld/main.fe new file mode 100644 index 0000000..2af2ed1 --- /dev/null +++ b/fec/tests/units/pubfld/main.fe @@ -0,0 +1,7 @@ +unit main; +import data; + +fn main() -> i32 { + let r = data.Record{ value: 7 }; + return r.value; +} diff --git a/fec/tests/units/pubpriv/lib.fe b/fec/tests/units/pubpriv/lib.fe new file mode 100644 index 0000000..0e30a9b --- /dev/null +++ b/fec/tests/units/pubpriv/lib.fe @@ -0,0 +1,5 @@ +unit lib; + +struct Hidden { value: i32, } +// ERROR:4:private +pub fn make() -> Hidden { return Hidden{ value: 1 }; } diff --git a/fec/tests/units/pubpriv/main.fe b/fec/tests/units/pubpriv/main.fe new file mode 100644 index 0000000..c53e7c5 --- /dev/null +++ b/fec/tests/units/pubpriv/main.fe @@ -0,0 +1,4 @@ +unit main; +import lib; + +fn main() -> void { let value = lib.make(); } diff --git a/fec/tests/units/unitbad/main.fe b/fec/tests/units/unitbad/main.fe new file mode 100644 index 0000000..3a2350f --- /dev/null +++ b/fec/tests/units/unitbad/main.fe @@ -0,0 +1,4 @@ +// ERROR:2:unit +unit other; + +fn main() -> void {} diff --git a/grammar.js b/grammar.js new file mode 100644 index 0000000..adb33ec --- /dev/null +++ b/grammar.js @@ -0,0 +1,111 @@ +// Minimal Tree-sitter grammar for Ferro syntax highlighting in Zed. +// `parser.c` is normative; this file is the editor-copy. + +module.exports = grammar({ + name: "ferro", + + extras: $ => [ + /[\s\uFEFF\u2060\u200B]/, + ], + + rules: { + source_file: $ => repeat(choice( + $.unit_decl, + $.import_decl, + $.line_comment, + $.block_comment, + $.string_literal, + $.char_literal, + $.integer_literal, + $.builtin, + $.builtin_type, + $.keyword, + $.identifier, + $.operator, + $.punctuation, + )), + + line_comment: _ => token(seq("//", /[^\n]*/)), + + unit_decl: $ => seq("unit", $.dotted_identifier, ";"), + + import_decl: $ => seq("import", $.dotted_identifier, + optional(seq("as", $.identifier)), ";"), + + // Ferro block comments may nest. Keeping this rule recursive makes syntax + // highlighting follow the compiler lexer instead of flattening nested /* */. + block_comment: $ => seq( + "/*", + repeat(choice( + $.block_comment, + /[^*/]+/, + /\*[^/]/, + /\/[^*]/, + )), + "*/", + ), + + string_literal: _ => token(seq( + '"', + repeat(choice( + /[^"\\\n\r]/, + /\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|[nrt\\'"0])/, + )), + '"', + )), + + char_literal: _ => token(seq( + "'", + choice( + /[^'\\\n\r]/, + /\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|[nrt\\'"0])/, + ), + "'", + )), + + integer_literal: _ => token(prec(2, choice( + /0[xX][0-9A-Fa-f_]+/, + /0[bB][01_]+/, + /0[oO][0-7_]+/, + /[0-9][0-9_]*/, + ))), + + builtin: _ => token(prec(3, /@[A-Za-z_][A-Za-z0-9_]*/)), + + builtin_type: _ => token(prec(3, choice( + "i8", "i16", "i32", + "u8", "u16", "u32", + "usize", "isize", + "bool", "char", "void", "str", + ))), + + keyword: _ => token(prec(3, choice( + "unit", "import", "pub", "fn", "struct", "packed", "enum", "error", + "const", "static", "var", "let", "mut", + "if", "else", "while", "for", "in", "match", + "return", "break", "continue", "defer", + "unsafe", "critical", "shared", "atomic", "comptime", "asm", + "try", "catch", "orelse", "as", "extern", + "interrupt", "interrupt_safe", "far", + "true", "false", "null", "undefined", + "self", "Self", "type", + "and", "or", "not", + ))), + + identifier: _ => /[A-Za-z_][A-Za-z0-9_]*/, + + operator: _ => token(choice( + "<<=", ">>=", + "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", + "==", "!=", "<=", ">=", "<<", ">>", "->", "=>", "..", + "+%", "-%", "*%", + "+", "-", "*", "/", "%", "=", "<", ">", "&", "|", "^", "!", "?", + )), + + dotted_identifier: _ => /[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*/, + + punctuation: _ => token(choice( + "(", ")", "{", "}", "[", "]", ",", ";", ":", ".", + )), + }, +}); diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..635c397 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "ferrolang" +version = "0.1.0" +description = "Ferro language compiler and reproducible DOS development tools" +requires-python = ">=3.12" +dependencies = [ + "pytest>=9.0.0", +] + +[project.scripts] +ferro-dos = "ferrolang_vm.dos_cli:main" +ferro-test = "ferrolang_vm.test_cli:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/ferrolang_vm"] diff --git a/tests/build.py b/tests/build.py new file mode 100644 index 0000000..e1eeb52 --- /dev/null +++ b/tests/build.py @@ -0,0 +1,105 @@ +"""Compile a Ferro program to a Windows executable and run it. + + fec --emit-asm -> wasm -> wlink (+ the runtime, + kernel32) -> .exe + +The toolchain is the pinned Open Watcom under `.dosboxx/watcom`, hosted: the +assembler and linker there produce PE binaries as happily as they produce DOS +ones. Nothing about this step needs a virtual machine. +""" +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +WATCOM = ROOT / ".dosboxx" / "watcom" +RUNTIME = ROOT / "fec" / "rt" / "start.asm" + + +def _env() -> dict: + env = dict(os.environ) + env.update(WATCOM=str(WATCOM), + INCLUDE=f"{WATCOM / 'h'};{WATCOM / 'h' / 'nt'}", + PATH=f"{WATCOM / 'binnt'}{os.pathsep}{env.get('PATH', '')}") + return env + + +def _run(cmd, cwd) -> subprocess.CompletedProcess: + return subprocess.run([str(c) for c in cmd], cwd=cwd, env=_env(), + capture_output=True, text=True, timeout=120) + + +def build(fec: Path, source: Path, out_dir: Path, no_checks: bool = False): + """Returns (exe_path, log). exe_path is None when a step failed.""" + out_dir.mkdir(parents=True, exist_ok=True) + stem = source.stem + asm = out_dir / f"{stem}.asm" + log = [] + + # `std` ships with the compiler, so it is looked for beside it rather + # than beside the program. + cmd = [fec, "--emit-asm", source, "-o", asm, f"--std={ROOT / 'fec'}"] + if no_checks: + cmd.append("--no-checks") + step = _run(cmd, out_dir) + log.append(("fec", step.returncode, step.stdout + step.stderr)) + if step.returncode != 0 or not asm.is_file(): + return None, log + + wasm = WATCOM / "binnt" / "wasm.exe" + for src, obj in ((asm, out_dir / f"{stem}.obj"), + (RUNTIME, out_dir / "start.obj")): + step = _run([wasm, "-q", "-zq", src, f"-fo={obj}"], out_dir) + log.append(("wasm " + src.name, step.returncode, + step.stdout + step.stderr)) + if step.returncode != 0: + return None, log + + exe = out_dir / f"{stem}.exe" + step = _run([WATCOM / "binnt" / "wlink.exe", + "system", "nt", + "file", out_dir / f"{stem}.obj", + "file", out_dir / "start.obj", + "library", WATCOM / "lib386" / "nt" / "kernel32.lib", + "name", exe, + "option", "quiet"], out_dir) + log.append(("wlink", step.returncode, step.stdout + step.stderr)) + if step.returncode != 0 or not exe.is_file(): + return None, log + return exe, log + + +def run(exe: Path): + done = subprocess.run([str(exe)], capture_output=True, text=True, + timeout=30) + return done.returncode, done.stdout + done.stderr + + +def main() -> int: + if len(sys.argv) < 2: + print("usage: build.py [--no-checks]") + return 2 + source = Path(sys.argv[1]).resolve() + fec = ROOT / ".build" / "fec.exe" + if not fec.is_file(): + print("build the front end first: uv run python tests/run.py") + return 2 + exe, log = build(fec, source, ROOT / ".build" / "out", + "--no-checks" in sys.argv) + for name, code, text in log: + if code != 0 or text.strip(): + print(f"--- {name} (exit {code})") + print(text.rstrip()) + if not exe: + return 1 + code, text = run(exe) + if text: + print(text, end="") + print(f"{exe.name} exited {code}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/exec.py b/tests/exec.py new file mode 100644 index 0000000..75ffd5f --- /dev/null +++ b/tests/exec.py @@ -0,0 +1,113 @@ +"""Compile the programs under `fec/tests/exec/`, run them, and check what they do. + +A program says what it should do in its first lines: + + // EXIT:55 the process must exit with this code + // OUTPUT:hello this text must appear in what it wrote + // NOCHECKS:0 build it a second time with --no-checks and expect + this exit code instead + +The point of this suite is different from `run.py`. That one checks what the +compiler says about a program; this one checks what the program does. A bounds +check that is reported but never emitted passes there and fails here. +""" +from __future__ import annotations + +import argparse +import re +import shutil +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import build as builder # noqa: E402 + +ROOT = Path(__file__).resolve().parent.parent +PROGRAMS = ROOT / "fec" / "tests" / "exec" + + +def expectations(path: Path) -> dict: + want = {} + for line in path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line.startswith("//"): + break + m = re.match(r"//\s*(EXIT|OUTPUT|NOCHECKS):(.*)", line) + if m: + key, value = m.group(1), m.group(2).strip() + if key == "OUTPUT": + # Every OUTPUT line has to appear. Keeping only the last one + # would let the earlier ones rot unnoticed. + want.setdefault("OUTPUT", []).append(value) + else: + want[key] = int(value) + return want + + +def check_one(fec: Path, path: Path, out_dir: Path) -> tuple[bool, str]: + want = expectations(path) + + exe, log = builder.build(fec, path, out_dir) + if not exe: + detail = "\n".join(f" {n} (exit {c}): {t.strip()}" + for n, c, t in log if c != 0 or t.strip()) + return False, "did not build\n" + detail + code, text = builder.run(exe) + if code != want["EXIT"]: + return False, f"exited {code}, expected {want['EXIT']}\n {text.strip()}" + for line in want.get("OUTPUT", []): + if line not in text: + return False, f"output has no {line!r}\n {text.strip()}" + + if "NOCHECKS" in want: + exe2, log2 = builder.build(fec, path, out_dir / "nochecks", + no_checks=True) + if not exe2: + return False, "did not build with --no-checks" + code2, _ = builder.run(exe2) + if code2 != want["NOCHECKS"]: + return False, (f"--no-checks exited {code2}, expected " + f"{want['NOCHECKS']}") + return True, "" + + +def main() -> int: + ap = argparse.ArgumentParser(description="run the compiled programs") + ap.add_argument("-k", dest="select") + ap.add_argument("-v", dest="verbose", action="store_true") + args = ap.parse_args() + + fec = ROOT / ".build" / "fec.exe" + if not fec.is_file(): + print("build the front end first: uv run python tests/run.py") + return 2 + out_dir = ROOT / ".build" / "exec" + if out_dir.exists(): + shutil.rmtree(out_dir, ignore_errors=True) + + # A file with no `// EXIT:` is a unit some program imports, not a program. + cases = [p for p in sorted(PROGRAMS.rglob("*.fe")) + if "EXIT" in expectations(p)] + if args.select: + cases = [p for p in cases if args.select in p.as_posix()] + if not cases: + print("no programs found") + return 1 + + failed = [] + for path in cases: + ok, why = check_one(fec, path, out_dir / path.stem) + rel = path.relative_to(PROGRAMS).as_posix() + if ok: + if args.verbose: + print(f" ok {rel}") + else: + failed.append((rel, why)) + for rel, why in failed: + print(f"FAIL {rel}: {why}") + print(f"\n{len(cases) - len(failed)}/{len(cases)} programs behaved") + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/run.py b/tests/run.py new file mode 100644 index 0000000..8cb5d84 --- /dev/null +++ b/tests/run.py @@ -0,0 +1,176 @@ +"""Run every fixture through the front end and check what it reports. + +A fixture is checked by running `fec` on it and looking at two things: whether +it was accepted, and, when it was rejected, whether the diagnostic is the one +the fixture asked for. + +A fixture states its expectation in its first line: + + // ERROR:8:self rejected at line 8, with "self" in the message + // ERROR:expected ';' rejected, message only -- the parse fixtures, where + the line is not the interesting part + +A fixture with no marker whose name starts with `bad` must be rejected but does +not pin the message yet. Anything else must be accepted. + +Fixtures under `parse/` are checked with --dump-ast rather than --check: they +exercise the grammar, and several are deliberately not well-typed. + +This runs on the host in about a second. It checks what the compiler says; what +the compiled programs actually do is `exec.py`. +""" +from __future__ import annotations + +import argparse +import os +import re +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +FIXTURES = ROOT / "fec" / "tests" +WATCOM = ROOT / ".dosboxx" / "watcom" +SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own", + "check", "checkexp", "checkstm", "checkgen", "checkcal", "checkpro", + "resolve", "ir", "lower", "lowerprn", "lowerexp", "lowerstm", "x86", "report", "driver") + +MARKER = re.compile(r"^//\s*ERROR:(?:(\d+):)?(.*)$") + + +@dataclass +class Expectation: + rejected: bool + line: int | None = None + text: str | None = None + + +def expectation(path: Path) -> Expectation: + first = path.read_text(encoding="utf-8", errors="replace").split("\n", 1)[0] + m = MARKER.match(first.strip()) + if m: + line = int(m.group(1)) if m.group(1) else None + return Expectation(True, line, m.group(2).strip()) + name = path.stem + return Expectation(name.startswith("bad") or "-bad-" in name or name.startswith("own-bad")) + + +def build(out: Path) -> Path: + """Build the front end with the pinned toolchain, hosted.""" + wcl = WATCOM / "binnt" / "wcl386.exe" + if not wcl.is_file(): + sys.exit(f"pinned Open Watcom not found at {WATCOM}") + out.mkdir(parents=True, exist_ok=True) + env = dict(os.environ) + env.update(WATCOM=str(WATCOM), INCLUDE=f"{WATCOM / 'h'};{WATCOM / 'h' / 'nt'}", + PATH=f"{WATCOM / 'binnt'}{os.pathsep}{env.get('PATH', '')}") + src = ROOT / "fec" / "src" + cmd = [str(wcl), "-q", "-za", "-wx", "-bt=nt", "-fe=fec.exe", f"-i={src}"] + cmd += [str(src / f"{n}.c") for n in SOURCES] + done = subprocess.run(cmd, cwd=out, capture_output=True, text=True, env=env) + if done.returncode != 0 or (done.stdout + done.stderr).strip(): + sys.exit("front end does not build clean:\n" + done.stdout + done.stderr) + return out / "fec.exe" + + +def run_case(fec: Path, path: Path) -> tuple[bool, str]: + want = expectation(path) + # The grammar fixtures are not all well-typed; stop after parsing. + mode = "--dump-ast" if path.parent.name == "parse" else "--check" + done = subprocess.run([str(fec), mode, str(path), + f"--std={ROOT / 'fec'}"], + capture_output=True, text=True, timeout=30) + output = (done.stdout + done.stderr).strip() + # Diagnostics go to the diag stream. `--dump-ast` also writes the tree to + # stdout, so a marker matched against the two together would read the tree + # and never reach the error. + diags = done.stderr.strip() or output + rejected = done.returncode != 0 + + if want.rejected != rejected: + verb = "accepted" if rejected else "rejected" + return False, f"expected to be {'rejected' if want.rejected else verb}" + if not want.rejected: + return True, "" + if want.line is None: + if want.text and want.text.lower() not in output.lower(): + got = output.splitlines()[0] if output else "(silent)" + return False, f"marker wants {want.text!r}\n {got}" + return True, "" + # The marker pins where and roughly what, so a rule can be moved or reworded + # only deliberately. + first = diags.split("\n", 1)[0] if diags else "" + at = re.search(r":(\d+):\d+: error:", first) + if not at: + return False, f"no diagnostic to match marker\n got: {first or '(silent)'}" + if int(at.group(1)) != want.line: + return False, f"marker says line {want.line}, diagnostic is line {at.group(1)}\n {first}" + if want.text and want.text.lower() not in diags.lower(): + return False, f"marker wants {want.text!r}\n {first}" + return True, "" + + +def main() -> int: + ap = argparse.ArgumentParser(description="run the front-end fixtures") + ap.add_argument("-k", dest="select", help="only fixtures whose path contains this") + ap.add_argument("-v", dest="verbose", action="store_true") + args = ap.parse_args() + + fec = build(ROOT / ".build") + cases = sorted(FIXTURES.rglob("*.fe")) + if args.select: + cases = [p for p in cases if args.select in p.as_posix()] + + failed = [] + for path in cases: + ok, why = run_case(fec, path) + rel = path.relative_to(FIXTURES).as_posix() + if ok: + if args.verbose: + print(f" ok {rel}") + else: + failed.append((rel, why)) + for rel, why in failed: + print(f"FAIL {rel}: {why}") + marked = sum(1 for p in cases if expectation(p).line is not None) + print(f"\n{len(cases) - len(failed)}/{len(cases)} passed " + f"({marked} pin a line and message)") + if not args.select: + leak = unsafe_budget(fec) + if leak: + print(leak) + return 1 + return 1 if failed else 0 + + +# The programs the budget is measured on: the Ferro front end, and the ones +# that lean hardest on the standard library. +BUDGETED = ("exec/lexer/tree.fe", "exec/interns.fe", "exec/arenat.fe", + "exec/maps.fe", "exec/wordfreq.fe") + + +def unsafe_budget(fec: Path) -> str: + """`unsafe` and `*T` belong to std.mem and std.sys. Anywhere else they are + a hole in what the checker promises, so the count outside std has to stay + at zero and a regression has to fail the build rather than be noticed.""" + for rel in BUDGETED: + path = FIXTURES / rel + if not path.is_file(): + return f"budget: {rel} is gone" + done = subprocess.run([str(fec), "--report-unsafe", str(path), + f"--std={ROOT / 'fec'}"], + capture_output=True, text=True) + line = [l for l in done.stdout.splitlines() + if l.startswith("outside std")] + if not line: + return f"budget: no report for {rel}\n{done.stdout}{done.stderr}" + counts = line[0].split()[2:] + if any(c != "0" for c in counts): + return (f"budget: {rel} has unsafe/raw pointers outside std: " + f"{line[0]}") + return "" + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..500d600 --- /dev/null +++ b/uv.lock @@ -0,0 +1,75 @@ +version = 1 +revision = 1 +requires-python = ">=3.12" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "ferrolang" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [{ name = "pytest", specifier = ">=9.0.0" }] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956 }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151 }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536 }, +]