TT Lab
Get started
Learn Learning paths Courses

Compilers — Build a Small Language from Start to Finish

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

Continue in TT Lab

Goal

You walk the tree the parser built, connect each name to its declaration (name resolution), and collect, in position order, all the mistakes that can be known before running — a missing name, a duplicate declaration, reading your own initializer, a return outside a function, the argument count, and types where they can be known.

Why it matters

An error that shows up only by running stays hidden until that line executes. Conversely, if a checker rejects a perfectly good program, people turn the checker off. So the core of semantic analysis is deciding how far you know for sure, and this lab's grading weighs "missed errors" and "false alarms" equally heavily.

The rules

스코프     맨 아래가 전역. 블록마다, 함수마다 하나씩 쌓는다. 찾기는 안쪽부터.
           함수의 매개변수와 몸체의 맨 바깥 선언은 한 스코프다.
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 키워드

Steps

  1. Fill in Decl(name, line, col, type_=None, arity=None, defined=True) and Scopes (push, pop, declare (False if it already exists), lookup, innermost) in /root/mini/checker.py.
  2. Fill in error, declare, use, statements, stmt, and expr of Checker — let, blocks, print, expression statements, names, and assignment. Hand calls over to call, operators to typed_expr, if and while to condition, and fn and return to function_stmt.
  3. Fill in function_stmt and call — the name first so that recursion works, the parameters and the body in one scope, a return outside a function, and the argument count.
  4. Fill in ARITH, ORDER, want, and typed_expr — skip unknown types.
  5. Fill in condition and the module's analyze(program) (→ (오류 글자 목록, 이름 해석 표), the list of error texts and the name resolution table), check, and resolve.
  6. The grader compares the name resolution table as a whole on fixed programs and on tricky scopes (a name seen by a closure, a scope per branch, shadowing inside a loop).
  7. The grader compares the error lists of the error programs (/opt/fixtures/mini/errors/check-*.mini and a few more).
  8. The grader compares 120 random correct programs (which must have no false alarms) and 120 programs broken in one spot each.

Notes

The scope stack

stack is a list of dictionaries and the bottom is global. declare puts into the top dictionary only, and if it already exists, it changes nothing and returns False. lookup searches from the inside with reversed(stack). innermost looks only at the top dictionary.

Connect names to declarations

let first declares a Decl(defined=False), gets the type of the initial value, and then changes it to defined=True. In use, if innermost is "before definition," it is a read-own-initializer error; if lookup is None, it is a missing name; and if found, write "used place → declaration" in resolved. Wrap blocks in push and pop.

Functions, return, and calls

fn first puts Decl(name, 'fn', argument count) into the current scope, pushes, puts in the parameters, and checks the body's statements directly in that scope (it does not push again for the body block). If you keep a count of function depth, you can tell whether a return is outside a function.

Types only where they can be known

want(node, operator, received type, expected type) says nothing if the received type is None. Because it checks operands one at a time, 1 + true has one error and true + false has two. The result type is decided by the operator regardless of the check result.

Conditions and collecting errors

condition raises an error at the keyword position if the condition's type is known and is not bool, and checks the branches (blocks) with stmt — the block builds the scope. analyze sorts the error list by (line, column) and then converts it to text.

Shadowing and scope leaks

There is no new code. If the name resolution table goes off, the message tells you which used place was connected to the wrong declaration. If a name after a block ends was connected to an inner declaration, you forgot pop, and if declarations in if branches collide with each other, you did not check the branches as blocks.

Error programs down to the last character

There is no new code. The message text, position, and order must all be the same. An argument count error applies only when calling directly by name (a function received as a value is unknown), an assignment error is at the position of =, and a condition error is at the position of the keyword.

Measure false alarms and missed errors together

There is no new code. If it raises errors on correct random programs, you are checking unknown types (parameters, call results), and if errors are lacking on broken programs, you stop when you fail to find a name or drop the second error of the same expression.