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.
This commit is contained in:
2026-08-17 07:35:21 +09:00
parent 4fe0073365
commit 76cb7e254c
10 changed files with 337 additions and 12 deletions
+12 -6
View File
@@ -35,14 +35,17 @@ def expectations(path: Path) -> dict:
m = re.match(r"//\s*(EXIT|OUTPUT|NOCHECKS):(.*)", line)
if m:
key, value = m.group(1), m.group(2).strip()
want[key] = int(value) if key in ("EXIT", "NOCHECKS") else value
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)
if "EXIT" not in want:
return False, "no // EXIT: marker"
exe, log = builder.build(fec, path, out_dir)
if not exe:
@@ -52,8 +55,9 @@ def check_one(fec: Path, path: Path, out_dir: Path) -> tuple[bool, str]:
code, text = builder.run(exe)
if code != want["EXIT"]:
return False, f"exited {code}, expected {want['EXIT']}\n {text.strip()}"
if "OUTPUT" in want and want["OUTPUT"] not in text:
return False, f"output has no {want['OUTPUT']!r}\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",
@@ -81,7 +85,9 @@ def main() -> int:
if out_dir.exists():
shutil.rmtree(out_dir, ignore_errors=True)
cases = sorted(PROGRAMS.rglob("*.fe"))
# 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: