Commit Graph
8 Commits
Author SHA1 Message Date
coolguy 63ad48cd8a 트랩은 검사가 쓰인 파일을 댄다
빌드 전체가 한 모듈이라 파일 이름도 하나였다. std.list 안에서 터진 경계 검사가
프로그램의 파일 이름을 대고 있었으니, 줄 번호는 맞는데 파일이 틀려서 엉뚱한 줄을
가리켰다 -- 이름을 안 대는 것보다 나쁘다.

모듈이 파일 표를 들고 트랩은 그 인덱스를 든다. 생성기는 파일마다 FE_FILE_n 을
한 번씩 찍는다.

  before: index out of bounds at main.fe:2
  after:  index out of bounds at pick.fe:6

그리고 Parser.on 이 구조체 리터럴 안에서 다시 try 를 쓴다. 앞서 그것이 깨졌던
것은 try 때문이 아니라 Parser 가 1 바이트로 자리잡았기 때문이었다.

224/224, 31/31.
2026-08-17 12:56:34 +09:00
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 4fe0073365 backend: 레지스터를 할당한다
임시값마다 스택 슬롯을 주던 것을 바꿨다. ebx, esi, edi 세 개를 나눠준다 --
eax/ecx/edx 는 이 방출기가 계산하는 자리이고, 남는 셋은 호출을 저장 없이
넘긴다.

IR 은 '임시값은 블록을 넘지 않는다'로 설계했지만 lowering 이 실제로는 넘기는
것을 만든다 -- 경계 검사가 인덱스를 계산한 자리와 쓰는 자리 사이에서 블록을
쪼갠다. 그래서 자격은 함수 전체를 보고 정하고, 블록 안의 선형 스캔은 살아남은
것만 다룬다.

그리고 목적지 레지스터에 직접 계산한다. 상수, 적재, 주소, 그리고 전폭 산술과
비교가 스크래치 레지스터를 거치지 않는다. 좁은 연산은 여전히 eax 를 거친다 --
esi 와 edi 에는 바이트 반쪽이 없다.

버그 둘:
- 프롤로그에 ebx 를 넣는 편집이 copy 의 push 까지 같이 바꿔서 pop 없는 push 가
  생겼다. 스택이 어긋나 17개가 죽었다.
- load_place_base 가 load_temp 을 거치지 않고 슬롯을 직접 읽었다. 레지스터에
  있는 포인터를 쓰지 않은 메모리에서 읽었다.

  calc      4437 -> 3796 줄  (-14%)
  wordfreq  6023 -> 5348 줄

214/214, 26/26.
2026-08-17 07:30:22 +09:00
coolguy c06f5c50c4 compiler: 조용히 잘리던 상한을 없앤다
lowering 의 변수·정리 목록·오류 이름 세 배열은 넘치면 오류가 아니라 넘친 것을
버리고 틀린 코드를 냈다. 한계에 닿는 방식 중 최악이다. 이제 자란다.

코드 생성기는 지역이 512개를 넘으면 함수를 아예 방출하지 않고 지나갔다. 이제
지역 수만큼 자리를 잡는다.

유닛 64 -> 256, 제네릭 인스턴스 512 -> 4096. 둘 다 원래 보고는 했지만 장난감을
기준으로 고른 숫자였다.

209 -> 213 fixture, exec 25/25.
2026-08-17 07:18:47 +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 174e6c569d lower: 전역과 문자열 리터럴, 그리고 defer 실행 검증
전역은 정적 저장소다. 초기값이 컴파일타임 상수면 이미지에 박고 아니면 0이다.
문자열 리터럴은 바이트를 이미지에 두고 포인터와 길이를 값으로 만든다. 같은
글자는 같은 저장소를 쓴다 -- 읽기 전용이라 공유가 공짜다.

defers 프로그램이 defer 순서를 실행으로 고정한다. 등록 역순이고, 이른 return
과 끝까지 간 경로 양쪽 다 돈다.
2026-08-17 06:08:33 +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
coolguy e5093e690d backend: i386 어셈블리를 내고 Windows 11 실행 파일을 만든다
fec --emit-asm -> wasm -> wlink -> .exe. 툴체인은 고정된 Open Watcom 그대로다.

레지스터 할당기가 없다. 임시값마다 스택 슬롯을 주고, 명령마다 피연산자를
고정 레지스터로 읽어 계산하고 다시 저장한다. 느린 코드지만 명백히 옳은
코드이고, 옳은 것이 먼저다. 나중에 할당기를 끼워도 나머지는 모른다 --
임시값이 어디 사는지만 바뀐다.

런타임 fec/rt/start.asm 은 진입 스텁과 fe_trap 이다. trap 은 이유와 파일과
줄을 stderr 에 쓰고 3으로 끝낸다.

처음으로 Ferro 프로그램이 실행됐다:

  1..10 합         -> 55
  (7*6-2)/4        -> 10
  루프+호출+분기   -> 1

tests/build.py 가 컴파일하고 링크하고 돌린다.
2026-08-17 05:55:34 +09:00