Commit Graph
17 Commits
Author SHA1 Message Date
coolguy 76cb7e254c lexer: Ferro 의 렉서를 Ferro 로 쓴다
셀프호스팅에 손대기 전의 강제 함수다. 아픈 자리를 전부 건드린다: R4 아래의
토큰 구조체, 태그드 유니온, 진단 출력, 유닛 경계.

토큰은 자기가 나온 글자를 담지 않는다. R4 가 대여를 집합 저장소에서 막으므로,
어디서 시작해 얼마나 긴지를 적고 소스는 옆에서 같이 다닌다. 위치도 &mut usize
로 옆에서 다닌다 -- 슬라이스와 함께 구조체에 들어갈 수 없기 때문이다. 이것이
R11 이 말하는 모양이고, 쓸 수 있다.

  first keyword unit @1 / number 42 @3 / text "hi" @3 / arrow -> @5
  keyword 6 name 7 number 1 text 1 punct 15 / total 30

길에서 고친 것:

- binding.Type.Variant 가 안 풀렸다. 유닛 경계 이름 조회가 심볼만 보고 타입을
  보지 않았다.
- 문자열 const 전역이 빈 슬라이스로 나갔다. 포인터는 링커만 아는 수라서 바이트에
  구멍을 두고 링커가 채우게 한다.
- exec.py 가 OUTPUT 마커를 여러 개 적어도 마지막 하나만 검사했다. 고치자마자
  readfile 의 낡은 기대가 드러났다.

run.py 217/217, exec.py 27/27.
2026-08-17 07:35:21 +09:00
coolguy ae83f5b142 std: mem.Arena 를 구현한다
SPEC R11 은 재귀·그래프 모양 데이터를 아레나가 값을 소유하고 정수 핸들이
가리키는 것으로 답한다. 그 답은 아레나가 실제로 있어야 쓸 수 있다.

핸들은 오프셋이라 아레나가 사는 동안 유효하고, 두 핸들을 비교하는 것은 두 수를
비교하는 것이다. 아레나는 몰래 자라지 않는다 -- 움직인 핸들은 더 이상 아무것도
가리키지 않기 때문이다.

길에서 고친 것 셋:

- Self 가 제네릭 인스턴스에서만 타입으로 묶여 있어서, 평범한 구조체의 Self{..}
  가 안 풀렸다. 이제 모든 메서드에서 묶는다.
- binding.Type.method() 가 식 자리에서 해석되지 않았다.
- 다른 유닛의 비제네릭 구조체 메서드가 lowering 되지 않고 extern 으로만 나갔다.
  파일을 나눌 때 그 가지가 빠졌다.

  handles 0 4 8 / value 65 / full / reset 0 / balanced

exec.py 26/26.
2026-08-17 07:21:40 +09:00
coolguy 0fd3f187f4 lower: 태그드 유니온을 해체한다 -- match 페이로드와 if let
AST 도 IR 도 본질이 태그드 유니온이다. 태그 비교만 되고 안을 꺼내지 못하면
검사 없이 필드를 읽어야 하고, 그러면 안전성 이야기가 통째로 무너진다.

무언가를 담는 변이는 메모리다: 태그가 앞, 페이로드가 뒤. 태그를 읽는 것은
어느 쪽이든 같은 질문이고 자리만 다르다. 페이로드는 가리키지 않고 복사한다 --
매치한 것의 소유권을 arm 이 가져가는 것이 보통이고, 그게 허용되는지는 검사기가
이미 판단했다.

레코드 변이의 필드 오프셋을 아무도 기록하지 않고 있었다. 레이아웃이 크기는
재면서 자리는 버렸다.

Enum.Variant{...} 와 Enum.Variant(x) 와 페이로드 없는 Enum.Variant 를 모두
만든다. 마지막 것은 페이로드를 가진 enum 안에서는 태그만 든 값이다.

  some 7 / none / point 3 4 / empty

exec.py 25/25.
2026-08-17 07:15:57 +09:00
coolguy 676fef88fb std: 프로그램이 바깥 세상과 이야기한다 -- 파일과 명령줄
런타임에 open/read/close 와 명령줄을 넣었다. std.io 가 그 위에 파일 열기,
읽기, 쓰기, 그리고 명령줄을 조각으로 나누는 것을 얹는다. 인용부호 처리는
런타임이 알 일이 아니라 라이브러리가 할 일이다.

길에서 고친 것들:

- *T 가 타입 시스템에 실체가 없어서 덩어리로 취급됐다. 이제 진짜 종류다 --
  주소일 뿐이고 추적할 대여도 실행할 drop 도 없는 Copy 타입. 그 결과 &u8 이
  *u8 에 자동으로 맞지 않게 됐는데, 그게 맞다: R9 는 그 변환을 unsafe 안의
  @ptr_cast 로만 허용한다.
- raw 포인터에 정수를 더하면 더 뒤의 주소다. 소유자나 대여에는 허용하지
  않는다 -- 자기 자리가 있는 것에서 걸어나가는 것이 *T 의 용도다.
- @volatile_load / @volatile_store / @ptr_cast 를 내린다.
- undefined 가 선언된 타입을 따른다. 없으면 손으로 타이핑할 수 있는 것보다
  큰 버퍼를 선언할 방법이 아예 없었다.

R8 이 정확히 동작하는 것도 확인했다: 참조성 파라미터가 둘인 함수는 슬라이스를
반환할 수 없다. 어디서 파생됐는지 시그니처가 말하지 않기 때문이다.

exec.py 24/24.
2026-08-17 07:11:56 +09:00
coolguy e014c95a75 lower: @print 과 @fprint 를 컴파일 단계에서 전개한다
SPEC 6.3.1: 포매팅 빌트인은 가변 인자 함수가 아니다. 호출 하나가 리터럴
조각마다 쓰기 하나, 값마다 쓰기 하나로 펴진다. 언어에 가변 인자 호출 규약이
생기지 않고, 포맷 문자열은 실행 시점에 이미 사라져 있다.

verb: {} 십진, {x} 16진, {c} 한 바이트, 문자열은 포인터와 길이, bool 은
분기 두 개와 리터럴 두 개.

코드 생성기가 호출됐지만 정의되지 않은 이름을 스스로 extern 선언한다. lowering
이 런타임 호출을 직접 내므로, 손으로 관리해야 하는 목록 대신 호출 자체에서
이름을 모은다.

  @print("n={} neg={} hex={x}\n", 42, 0-7, 255)  ->  n=42 neg=-7 hex=ff
2026-08-17 07:05:55 +09:00
coolguy 7f871a5d5f tests: 단어 빈도 프로그램
컬렉션과 문자열을 함께 쓴다. 구조체를 담는 제네릭 리스트, 슬라이스 비교,
처음 본 순서 유지, 그리고 할당과 해제가 맞는지 확인.

  the 3 / cat 2 / sat 1 / distinct 5 / balanced

run.py 209/209, exec.py 21/21.
2026-08-17 06:46:47 +09:00
coolguy 432d073104 lang: 배타 대여를 호출에 넘기는 것은 이동이 아니라 재대여다
&mut T 를 &mut T 파라미터에 넘기면 호출이 끝날 때 돌려받는다. 호출이 도는
동안 호출자는 그 값에 손댈 수 없으므로 별칭이 생기지 않는다. 이것이 없으면
배타 파라미터를 다시 넘기는 일이 함수당 한 번만 가능해서 &mut 가 사실상 쓸 수
없었다 -- 재귀 하강 파서를 쓰다가 걸렸다.

페이로드 없는 enum 은 이름 붙은 수라서 수로 읽을 수 있다. 반대 방향은 안 된다:
임의의 수는 변이가 아니다.

calc 프로그램: 재귀 하강 수식 계산기. 우선순위, 괄호, 오류 전파.

  1+2*3 = 7   (1+2)*3 = 9   2*(3+4)-5 = 9   10/3 = 3
  1+ = error  (1+2 = error
2026-08-17 06:45:19 +09:00
coolguy 206799d1cb std: str 과 list, 그리고 자동 drop 이 사용자 타입까지 닿는다
std.str 은 eq/starts_with/find/trim/parse_int 을 바이트 슬라이스 위에서 한다.
std.list 는 자라는 제네릭 시퀀스다 -- 버퍼를 소유하므로 리스트를 놓으면
원소도 놓인다. 성장은 두 배씩이라 push 당 복사량이 상수로 눌린다.

찾은 버그 넷:

- 메서드가 자기 타입의 유닛이 아니라 호출한 유닛에 속한 것으로 계산됐다.
  다른 유닛의 제네릭 타입을 쓰면 필드가 전부 private 으로 보였다.
- 참조로 도달한 메서드를 찾지 못했다. self.grow() 가 안 됐다.
- 이미 참조인 수신자의 주소를 한 번 더 떠서 넘겼다. 포인터의 포인터를 받은
  메서드가 그것을 구조체로 읽었다.
- 유닛으로 한정된 제네릭 타입(list.List(i32))이 타입 자리에서도 식 자리에서도
  해석되지 않았다.

drop 을 가진 타입은 인스턴스마다 그 메서드가 존재해야 한다 -- 이름으로 부르는
사람이 없어도 스코프 정리가 부른다. 그리고 자기 drop 안에서는 필드를 꺼낼 수
있다. 객체가 사라지는 중이라 뒤에 남아 읽힐 것이 없다.

run.py 207/207, exec.py 19/19.
2026-08-17 06:42:16 +09:00
coolguy b0c9338cf3 tests: pending-backend 격리를 없앤다
코드 생성기가 없어서 돌릴 수 없던 것들이다. 이제 돌아간다. 경계 트랩,
--no-checks 차등, 슬라이스 범위 검사, 소유권 해제가 전부 exec/ 에서
실행으로 검증되므로 격리할 이유가 없다.

run.py 205/205, exec.py 17/17.
2026-08-17 06:32:57 +09:00
coolguy 3a01cb4c51 lower: 소유 값을 스코프 끝에서 자동으로 해제한다
defer 목록을 '스코프가 아직 갚아야 할 것' 목록으로 일반화했다. defer 블록과
소유 값 해제가 같은 목록에 쓰인 순서대로 들어가고, 모든 이탈 경로가 역순으로
갚는다.

해제에는 값 옆에 플래그를 둔다. 값이 저장될 때 세우고 넘겨줄 때 지운다. 값이
아직 여기 있는 경로에서만 해제되는데, 그건 코드의 모양만 봐서는 알 수 없는
것이다. 검사기가 소유권을 넘기는 사용을 이미 표시해두므로 그것을 읽는다.

!void 함수의 빈 return 은 성공이다. 줄 값도 없고 오류도 없다는 뜻인데
프론트엔드가 타입 불일치로 거부하고 있었다.

런타임이 할당/해제 횟수를 센다. owndrop 프로그램이 그 둘이 일치함을
실행으로 증명한다 -- 이른 반환, 이미 넘긴 값, 스코프 끝 전부.
2026-08-17 06:32:15 +09:00
coolguy 9510f12643 lower: mem.create / alloc_slice / destroy / replace
할당하는 내장 함수들이다. 평범한 호출이 아니라서 여기서 편다. create 는 값을
받아 그 복사본을 가리키는 소유 포인터를 주고, 할당이 실패할 수 있으므로 결과가
에러 유니온이다. 실패 코드는 OutOfMemory 이고, 소스 어디에도 그 이름이 적혀
있지 않지만 다른 이름과 같은 표에 들어간다.

갓 할당한 저장소는 통째로 소유하므로 쓸 수 있다 -- 방해할 사람이 없다. 그래서
alloc_slice 는 ^[]mut T 를 준다. 소유 슬라이스는 포인터와 길이가 값 자체라서
.^ 로 통과할 것이 없고, destroy 는 그 안의 포인터를 푼다.

heap 프로그램이 할당·try·defer 해제·for 순회를 한꺼번에 돈다: sum 4950
2026-08-17 06:29:18 +09:00
coolguy 231c7d564b lower: 제네릭 인스턴스와 메서드
모노모피제이션이 실제로 코드를 만드는 자리가 여기다. 프론트엔드는 어떤
인스턴스가 존재하는지만 정했다. 검사기가 인스턴스마다 선언·바인딩·유닛·링크
이름을 기록하고, lowering 이 그 바인딩을 다시 걸고 같은 본문을 자기 이름으로
내린다. 제네릭 선언 자체는 코드가 없다.

comptime 인자는 값이 아니므로 호출에서 넘기지 않고 파라미터 자리도 잡지
않는다. 메서드 호출은 도달한 대상을 첫 인자로 넘긴다 -- self: Self 든
self: &Self 든 수신자의 주소로 같다. 구조체 메서드가 아예 lowering 되지
않고 있었다.

exec.py 13/13.
2026-08-17 06:26:20 +09:00
coolguy eba6f63530 lower: 슬라이싱
x[a..b] 는 인덱스가 아니라 포인터와 길이를 만든다. 양쪽 끝을 -- 서로에 대해,
그리고 있는 것에 대해 -- 검사한 뒤에 포인터를 만든다. 유효한 범위의 빈
슬라이스는 괜찮고 끝보다 늦게 시작하는 것은 아니다.

파서가 x[a] 와 x[a..] 를 구분하지 못했다. 둘 다 b 만 있고 c 가 없어서
모양이 같았다. '..' 가 있었으면 노드에 표시한다.

정수 출력이 된다: fizz 12345 -678
2026-08-17 06:20:17 +09:00
coolguy 0de10f37b5 std: io 와 fmt 를 Ferro 로 쓰고, 컴파일된 프로그램이 출력한다
io.Writer 는 핸들 하나짜리 enum 이다. 참조도 컨텍스트 포인터도 담지 않으므로
Copy 이고 자유롭게 오간다 (SPEC 5 R8). fmt 는 sink 를 소유하지 않는다 --
호출자가 버퍼를 주고 앞에서 몇 바이트가 쓰였는지 돌려받는다.

lowering 에 추가: enum 변이 상수, match, 정수 캐스트, 문자열 이스케이프.

프론트엔드 정밀도 하나: 항상 빠져나가는 분기의 상태를 병합하지 않는다. 그
분기가 소비한 값이 그 분기를 지나지 않은 경로에서도 소비된 것처럼 보였다.
fmt_i32 가 이것 때문에 못 쓰였다.

extern "c" 이름은 유닛 접두사를 붙이지 않는다. 링커가 이미 아는 이름이라는
것이 그 선언의 요점이다.

run.py 199/199, exec.py 11/11.
2026-08-17 06:18:16 +09:00
coolguy 174e6c569d lower: 전역과 문자열 리터럴, 그리고 defer 실행 검증
전역은 정적 저장소다. 초기값이 컴파일타임 상수면 이미지에 박고 아니면 0이다.
문자열 리터럴은 바이트를 이미지에 두고 포인터와 길이를 값으로 만든다. 같은
글자는 같은 저장소를 쓴다 -- 읽기 전용이라 공유가 공짜다.

defers 프로그램이 defer 순서를 실행으로 고정한다. 등록 역순이고, 이른 return
과 끝까지 간 경로 양쪽 다 돈다.
2026-08-17 06:08:33 +09:00
coolguy 096a5db411 lower: 옵셔널, 에러 유니온, try/catch/orelse, defer, for
옵셔널은 태그와 페이로드, 에러 유니온은 오류 코드와 페이로드다. 코드 0 이
'오류 없음'이다. 페이로드 위치 규칙을 types.c 로 옮겨서 레이아웃 패스와 코드
생성기가 같은 것을 본다.

try 는 분기다. 실패면 지금 함수의 에러 유니온을 그 코드로 만들어 나간다 --
그 전에 defer 를 돌린다. catch 와 orelse 는 오른쪽을 필요할 때만 평가하므로
역시 분기다.

for 는 세 형태를 공유한다: 세는 것, 원소를 도는 것, 위치까지 받는 것. 개수는
본문 전에 한 번 읽는다. 원소 바인딩은 참조다 -- 그래서 루프가 원본에 쓸 수
있다.

error.Name 은 빌드 전체에서 이름을 모아 철자 순으로 1부터 번호를 준다
(SPEC 4.6). 빌드 순서가 결과를 바꾸지 않는다.

run.py 197/197, exec.py 9/9.
2026-08-17 06:06:35 +09:00
coolguy 43555b261c lower+backend: 배열, 경계검사, 구조체, 그리고 실행 테스트 스위트
인덱스는 부호 없는 비교와 트랩으로 펴진다. --no-checks 는 메시지가 아니라
비교와 분기 자체를 없앤다 -- 그게 그 플래그의 전부다.

버그 셋:

- 값으로 넘긴 구조체 파라미터는 주소로 도착하는데 lowering 이 그걸 몰라서
  포인터를 구조체로 읽었다. 변수마다 by_address 를 기록한다.
- 덩어리 반환의 숨은 결과 인자를 지역이 아니라 임시값으로 다뤘다.
- store 에 폭이 없어서 1바이트 bool 을 4바이트로 썼다. 옆 지역변수가
  뭉개졌고 logic 프로그램이 틀린 답을 냈다.

tests/exec.py 가 새 스위트다. run.py 는 컴파일러가 프로그램에 대해 뭐라고
하는지 보고, 이쪽은 프로그램이 실제로 무엇을 하는지 본다. 보고만 되고
방출되지 않는 경계검사는 저기서는 통과하고 여기서는 실패한다.

run.py 194/194, exec.py 6/6.
2026-08-17 06:00:33 +09:00