LabHub
Get started
배우기 러닝패스 코스

Compilers — Build a Small Language from Start to Finish

Scopes, Name Resolution and Type Checks Only Where Types Are Known

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

파서가 만든 트리를 걸으며 이름마다 선언을 잇고(이름 해석), 실행 전에 알 수 있는 잘못 — 없는 이름, 중복 선언, 자기 초기값 읽기, 함수 밖 return, 인자 수, 알 수 있는 곳의 타입 — 을 위치 순으로 모두 모읍니다.

왜 중요한가

실행해야만 드러나는 오류는 그 줄이 돌 때까지 숨어 있습니다. 반대로 검사기가 멀쩡한 프로그램을 거절하면 사람들은 검사기를 끕니다. 그래서 의미 분석의 핵심은 어디까지를 확실히 아는가를 정하는 것이고, 이 실습의 채점은 '빠진 오류' 와 '거짓 경보' 를 똑같이 무겁게 봅니다.

규칙

스코프     맨 아래가 전역. 블록마다, 함수마다 하나씩 쌓는다. 찾기는 안쪽부터.
           함수의 매개변수와 몸체의 맨 바깥 선언은 한 스코프다.
let        이름을 '정의 전' 으로 먼저 넣고 → 초기값 검사 → '정의됨'. 선언의 타입 = 초기값의 타입.
fn         이름을 (fn, 인자 수) 로 먼저 넣고 → 새 스코프에 매개변수(타입 모름) → 몸체.
타입       int · bool · fn · 모름(None). 양쪽을 다 알 때만 검사한다.
           + - * / % ^ 앞 - : int → int      < <= > >= : int → bool
           == != : 두 쪽이 같은 타입 → bool   ! && || : bool → bool
           대입: 이름의 타입과 값의 타입을 다 알면 같아야. 대입식의 타입 = 값의 타입. 호출의 결과 = 모름.
이름 해석  "쓴 곳 줄:칸" → "선언 줄:칸" (선언 위치는 let·fn 은 이름 토큰, 매개변수는 그 이름 토큰)
오류 글자  (줄, 칸, 메시지)를 모아 두었다가 (줄, 칸) 순으로 "줄:칸: 메시지"
  undefined variable 'x'                         쓴 곳
  'x' is already declared in this scope          두 번째 선언의 이름
  cannot read 'x' in its own initializer         쓴 곳
  return outside function                        return 키워드
  function 'f' takes 2 arguments, got 3          여는 괄호(이름으로 직접 부를 때만)
  cannot call int                                여는 괄호(부르는 쪽 타입이 int·bool 일 때)
  operator '+' expects int, got bool             연산자 (피연산자마다 따로)
  operator '==' compares int with bool           연산자
  cannot assign bool to 'x' (int)                =
  condition must be bool, got int                if·while 키워드

단계

  1. /root/mini/checker.pyDecl(name, line, col, type_=None, arity=None, defined=True)Scopes(push·pop·declare(이미 있으면 False)·lookup·innermost)를 채웁니다.
  2. Checkererror·declare·use·statements·stmt·expr 를 채웁니다 — let·블록·print·식 문장, 이름·대입. 호출은 call, 연산자는 typed_expr, if·while 은 condition, fn·return 은 function_stmt 로 넘깁니다.
  3. function_stmtcall 을 채웁니다 — 재귀가 되도록 이름을 먼저, 매개변수와 몸체는 한 스코프, 함수 밖 return, 인자 수.
  4. ARITH·ORDER·want·typed_expr 를 채웁니다 — 모르는 타입은 넘어갑니다.
  5. condition 과 모듈의 analyze(program)(→ (오류 글자 목록, 이름 해석 표)), check, resolve 를 채웁니다.
  6. 채점기가 고정 프로그램과 까다로운 스코프(클로저가 본 이름, 가지마다 스코프, 반복문 안 섀도잉)의 이름 해석 표를 통째로 대조합니다.
  7. 채점기가 오류 프로그램(/opt/fixtures/mini/errors/check-*.mini 와 몇 편 더)의 오류 목록을 대조합니다.
  8. 채점기가 무작위 올바른 프로그램 120편(거짓 경보가 없어야)과 한 군데씩 망가뜨린 120편을 대조합니다.

참고

스코프 스택

stack 은 사전의 목록이고 맨 아래가 전역입니다. declare 는 맨 위 사전에만 넣고, 이미 있으면 아무것도 바꾸지 않고 False 입니다. lookup 은 reversed(stack) 으로 안쪽부터 찾습니다. innermost 는 맨 위 사전만 봅니다.

이름을 선언에 잇는다

let 은 Decl(defined=False) 를 먼저 declare 하고, 초기값의 타입을 구한 뒤 defined=True 로 바꿉니다. use 는 innermost 가 '정의 전' 이면 자기 초기값 읽기 오류, lookup 이 None 이면 없는 이름, 찾으면 resolved 에 '쓴 곳 → 선언' 을 적습니다. 블록은 push·pop 으로 감쌉니다.

함수, return, 호출

fn 은 Decl(이름, 'fn', 인자 수) 를 지금 스코프에 먼저 넣고, push 한 뒤 매개변수를 넣고 몸체의 문장들을 그 스코프에서 바로 검사합니다(몸체 블록을 또 push 하지 않습니다). 함수 깊이를 세어 두면 return 이 함수 밖인지 알 수 있습니다.

알 수 있는 곳만 보는 타입

want(노드, 연산자, 받은 타입, 기대 타입) 은 받은 타입이 None 이면 아무 말도 하지 않습니다. 피연산자를 하나씩 따로 검사하므로 1 + true 는 오류가 하나, true + false 는 둘입니다. 결과 타입은 검사 결과와 상관없이 연산자가 정합니다.

조건과 오류 정리

condition 은 조건의 타입이 알려져 있는데 bool 이 아니면 키워드 위치로 오류를 내고, 가지(블록)를 stmt 로 검사합니다 — 블록이 스코프를 쌓아 줍니다. analyze 는 오류 목록을 (줄, 칸) 으로 정렬한 뒤 글자로 바꿉니다.

섀도잉과 스코프 누수

새 코드는 없습니다. 이름 해석 표가 어긋나면 어느 쓴 곳이 엉뚱한 선언에 이어졌는지 메시지가 알려 줍니다. 블록이 끝난 뒤의 이름이 안쪽 선언에 이어졌다면 pop 을 빠뜨린 것이고, if 가지의 선언이 서로 부딪힌다면 가지를 블록으로 검사하지 않은 것입니다.

오류 프로그램을 한 글자까지

새 코드는 없습니다. 메시지 글자·위치·순서가 모두 같아야 합니다. 인자 수 오류는 이름으로 직접 부를 때만(값으로 받은 함수는 모름), 대입 오류는 = 의 위치, 조건 오류는 키워드의 위치입니다.

거짓 경보와 놓친 오류를 함께 잰다

새 코드는 없습니다. 올바른 무작위 프로그램에서 오류를 낸다면 모르는 타입(매개변수·호출 결과)을 검사하고 있는 것이고, 망가뜨린 프로그램에서 오류가 모자라면 이름을 못 찾았을 때 멈추거나 같은 식의 두 번째 오류를 버리고 있는 것입니다.