[PLT] Semantic Analysis (의미 분석)
정의
Semantic Analysis (의미 분석) 는 파서가 생성한 AST 를 검사하여 문법적으로 올바르지만 의미상 오류인 경우 를 발견하고, 각 이름 (identifier) 이 무엇을 가리키는지 결정하는 단계입니다.
한 줄 요약: “코드가 문법적으로 맞는 건 알겠는데, 말이 되나?”
무엇을 검사하는가
파서가 통과시킨 것들:
undefinedVar + 1 // 정의되지 않은 변수
"hello" * 3 // 잘못된 타입 (문자열 * 숫자? 파이썬은 가능, JS 는?)
myFunc() // 함수 없음
const x = 1; x = 2; // const 재할당
return 42; // 함수 밖 return
파서는 이런 것을 발견 못 함. Semantic analyzer 가:
- 이름 해석: 각 identifier 가 어떤 declaration 을 가리키는지
- Scope 규칙: 어느 변수가 여기서 보이는지
- 타입 검사: 값의 타입이 문맥에 맞는지
- 컨트롤 흐름:
return이 함수 안에 있는지,break가 루프 안에 있는지 - 접근 제어: private 필드 접근 금지, immutable 재할당 금지
Symbol Table
이름 → 정보 매핑. Semantic analyzer 의 핵심 자료구조.
각 심볼의 정보:
- 이름
- 종류 (variable, function, class, module)
- 타입
- 선언 위치
- 가시성 (scope)
- 속성 (const, static, private)
interface Symbol {
name: string;
kind: 'variable' | 'function' | 'class' | 'parameter';
type: Type;
declLocation: SourceLocation;
isConst: boolean;
isExported: boolean;
}
class SymbolTable {
private scopes: Map<string, Symbol>[] = [new Map()];
enterScope() { this.scopes.push(new Map()); }
exitScope() { this.scopes.pop(); }
declare(sym: Symbol) {
const current = this.scopes[this.scopes.length - 1];
if (current.has(sym.name)) {
throw new Error(`Duplicate declaration: ${sym.name}`);
}
current.set(sym.name, sym);
}
lookup(name: string): Symbol | undefined {
for (let i = this.scopes.length - 1; i >= 0; i--) {
if (this.scopes[i].has(name)) {
return this.scopes[i].get(name);
}
}
return undefined;
}
}
Scope (범위)
Scope: 한 이름이 유효한 코드 영역.
Lexical Scope (정적 스코프)
소스 코드의 위치로 결정. 대부분 언어 (JavaScript, Python, Rust, Java, C).
function outer() {
const x = 1;
function inner() {
console.log(x); // ← outer 의 x
}
inner();
}
inner 안의 x 는 정의 시점의 outer 를 가리킴 (lexical).
Dynamic Scope (동적 스코프)
호출 시점의 스택 으로 결정. 초기 Lisp, Emacs Lisp, Perl local 등.
(defun outer () (let ((x 1)) (inner)))
(defun inner () (print x)) ; ← 호출한 outer 의 x
대부분 언어는 lexical scope. Dynamic scope 는 예측 어려워 지양.
Nested Scope
Scope 는 중첩 가능. 안쪽 scope 가 바깥 참조 가능:
Global scope
└── Module scope
└── Function scope
└── Block scope
└── (nested block)
이름 해석: 가장 안쪽부터 바깥으로 확인.
Shadowing (섀도잉)
내부 scope 가 외부 이름을 가림:
const x = 1;
{
const x = 2; // 바깥 x 를 가림
console.log(x); // 2
}
console.log(x); // 1
Rust 는 shadowing 을 명시적 언어 기능 으로:
let x = 5;
let x = "hello"; // 다른 타입도 가능
Python 은 함수 scope 안에서만 shadowing.
이름 해석 알고리즘
각 identifier 참조를 declaration 에 연결.
resolve(node, symbolTable):
switch node.kind:
case Identifier:
sym = symbolTable.lookup(node.name)
if sym is None:
error(f"Undefined: {node.name}")
node.resolvedSymbol = sym
case VariableDeclaration:
resolve(node.init, symbolTable)
sym = Symbol(name=node.name, type=...)
symbolTable.declare(sym)
case Block:
symbolTable.enterScope()
for stmt in node.statements:
resolve(stmt, symbolTable)
symbolTable.exitScope()
case Function:
fn = FunctionSymbol(...)
symbolTable.declare(fn)
symbolTable.enterScope()
for param in node.params:
symbolTable.declare(param)
resolve(node.body, symbolTable)
symbolTable.exitScope()
Hoisting (호이스팅)
일부 언어 (JavaScript) 는 선언을 scope 상단으로 끌어올림:
console.log(foo()); // OK, 호이스팅
function foo() { return 1; }
console.log(x); // undefined (var 는 호이스팅되지만 값 아님)
var x = 5;
console.log(y); // ReferenceError (let 은 TDZ)
let y = 5;
Semantic analyzer 는 두 pass:
- 선언 수집 pass: block 전체를 훑어 선언 등록
- 참조 해석 pass: 표현식의 identifier 해석
TypeScript, Rust 는 이 방식 (선언 순서와 무관하게 참조 가능).
Type Checking
각 표현식의 타입 을 결정하고 문맥에 맞는지 확인.
const x: number = "hello"; // 오류: string 을 number 에
add("hello", 1); // 오류 여부는 언어 나름
간단한 규칙:
- NumberLiteral: type = number
- StringLiteral: type = string
- BinaryOp(+):
- number + number → number
- string + string → string
- number + string → 언어별 (JS 는 coercion, Python 은 오류, Rust 는 오류)
Type inference (Hindley-Milner, TypeScript, Rust): 명시 없이도 타입 추론. 자세한 것은 Type Systems 참조.
Control Flow Check
return이 함수 안?break/continue가 loop / switch 안?- 모든 경로가 return 하는가? (
noImplicitReturns) - 예외 처리 (
throws선언 vs actual throw)
checkReturn(node, insideFunction=false):
switch node.kind:
case ReturnStatement:
if not insideFunction:
error("'return' outside function")
case FunctionDeclaration:
checkReturn(node.body, insideFunction=True)
case Block:
for stmt in node.statements:
checkReturn(stmt, insideFunction)
상수 검사
const 는 재할당 금지:
const x = 1;
x = 2; // 오류
const arr = [];
arr.push(1); // OK (내부 변경, 재할당 X)
Semantic analyzer 는 const 변수의 재할당 사이트 를 검사.
Symbol Table 구현 전략
Chained Hash Tables
각 scope 가 별도 map, 안쪽 → 바깥 순회.
Flat + Depth
하나의 map 에 (name, depth) 키. 스코프 depth 로 shadowing.
Persistent (Immutable)
각 scope 변화가 새 map. 함수형 언어에 자연스러움.
오류 메시지 개선
좋은 semantic error:
error[E0425]: cannot find value `undefined_var` in this scope
--> src/main.rs:5:9
|
5 | let x = undefined_var + 1;
| ^^^^^^^^^^^^^ not found in this scope
|
= help: did you mean `undefined_val`?
- 유사 이름 제안 (Levenshtein distance)
- Import 제안
- 정의 위치 표시
Rust 컴파일러가 이 방면 최상.
두 언어의 대비
JavaScript (동적 타입, hoisting)
- 대부분 검사는 런타임
undefined_var는 파싱/semantic 통과, 런타임ReferenceError- Static analysis 는 ESLint, TypeScript 가 담당
Rust (정적 타입, ownership 검사)
- 매우 엄격한 semantic 분석
- Ownership + borrow checking (semantic 안)
- 컴파일 오류가 대부분 실제 버그
Semantic Analysis 결과
AST 에 정보 주석 (annotated AST):
interface AnnotatedIdentifier extends Identifier {
resolvedSymbol: Symbol; // 이 identifier 가 가리키는 declaration
inferredType: Type;
}
이후 단계 (IR 생성, 최적화, 코드 생성) 가 이 정보 활용.
함정
WARNING
이름 해석 순서 실수. Hoisting 있는 언어는 2-pass 필요.
CAUTION
Scope stack 관리. enterScope/exitScope 짝 맞지 않으면 버그.
WARNING
좋은 에러 메시지 = 좋은 언어. 초기 개발 시부터 신경.
IMPORTANT
정적 분석은 완전 X. eval, exec, 리플렉션 있는 언어는 불가피한 undecidable.
CAUTION
캐시 이슈. 큰 IDE 는 심볼 테이블을 지속 유지. 파일 변경 시 정확한 무효화.
관련 위키
이 글의 용어 (9개)
- [Javascript] Lexical Environmentjavascript
- 정의 Lexical Environment (어휘적 환경) 는 ECMAScript 명세 (§9.1) 가 정의한 내부 객체. 함수가 호출될 때마다 새로 만들어지며, 이 호출의 변수들…
- [PLT] Abstract Syntax Tree (AST)plt
- 정의 Abstract Syntax Tree (AST, 추상 구문 트리) 는 프로그램의 문법 구조를 트리로 표현 한 자료구조입니다. 파서의 출력물이자, 이후 컴파일러/인터프리터의 …
- [PLT] Interpreter vs Compilerplt
- 정의 프로그래밍 언어의 실행 모델 두 접근: - Compiler (컴파일러): 소스 → 목표 코드 (기계어, 바이트코드) 로 변환. 실행은 나중. - Interpreter (인터…
- [PLT] IR, 최적화, 코드 생성 (Compiler Backend)plt
- 정의 컴파일러의 백엔드 는 semantic 분석까지 마친 AST 를 실제 실행 가능한 목표 코드로 변환합니다. 세 주요 단계: 1. IR (Intermediate Represen…
- [PLT] Lexical Analysis (어휘 분석)plt
- 정의 어휘 분석 (Lexical Analysis) 은 소스 코드 문자 시퀀스 를 의미 있는 최소 단위인 토큰 (Token) 으로 분해하는 과정입니다. 담당 컴포넌트를 Lexer …
- [PLT] Parsing & Grammarsplt
- 정의 Parsing (구문 분석) 은 lexer 의 토큰 스트림 을 언어의 문법 규칙 에 따라 구조화된 트리 (AST, Abstract Syntax Tree) 로 조립하는 과정입…
- [PLT] Type Systems (타입 시스템)plt
- 정의 타입 시스템 (Type System) 은 프로그램의 값과 표현식에 타입 (type) 을 부여하고, 이 타입들이 규칙에 맞게 결합되는지 검증하는 형식 체계입니다. 타입 = 값…
- [TypeScript] Strict Mode & tsconfigtypescript
- 정의 Strict Mode 는 여러 엄격한 타입 검사 플래그를 한 번에 켜는 옵션 ( ) 입니다. 강력한 타입 안전성을 제공하지만, 기존 코드베이스에는 대량의 오류를 발생시킬 수…
- 프로그래밍 언어론 (Programming Language Theory)plt
- 정의 프로그래밍 언어론 (Programming Language Theory, PLT) 은 프로그래밍 언어를 어떻게 설계하고, 정의하고, 처리하는가 를 다루는 컴퓨터 과학 분야입니…
💬 댓글