Compilers — Build a Small Language from Start to Finish
Scopes, Name Resolution and Type Checks Only Where Types Are Known
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
- Fill in
Decl(name, line, col, type_=None, arity=None, defined=True)andScopes(push,pop,declare(False if it already exists),lookup,innermost) in/root/mini/checker.py. - Fill in
error,declare,use,statements,stmt, andexprofChecker— let, blocks, print, expression statements, names, and assignment. Hand calls over tocall, operators totyped_expr, if and while tocondition, and fn and return tofunction_stmt. - Fill in
function_stmtandcall— the name first so that recursion works, the parameters and the body in one scope, a return outside a function, and the argument count. - Fill in
ARITH,ORDER,want, andtyped_expr— skip unknown types. - Fill in
conditionand the module'sanalyze(program)(→(오류 글자 목록, 이름 해석 표), the list of error texts and the name resolution table),check, andresolve. - 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).
- The grader compares the error lists of the error programs (
/opt/fixtures/mini/errors/check-*.miniand a few more). - The grader compares 120 random correct programs (which must have no false alarms) and 120 programs broken in one spot each.
Notes
python3 /root/mini/mini.py check 파일.miniprints the errors with your checker (the placeholder is the file).- The lexer, parser, and nodes are laid out when you start the lab (you may also paste in your own from the previous modules).
- Common mistakes: not discarding the scope when leaving a block (a leak), checking the initializer first so that
let x = x;refers to the outer x, flagging unknown types as errors (false alarms), and returning the errors in the order found. - The session starts at 60 minutes and you can extend it with the +time button, and when it ends
/root/minidisappears.
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.