본문으로 건너뛰기
김신건의 로그

[PLT] Semantic Analysis (의미 분석)

· 수정 · 📖 약 3분 · 1,030자/단어 #plt #compiler #semantic-analysis #scope #symbol-table
Semantic Analysis, 의미 분석, symbol table, 심볼 테이블, scope resolution, name resolution, type checking, lexical scope, dynamic scope, shadowing

정의

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:

  1. 선언 수집 pass: block 전체를 훑어 선언 등록
  2. 참조 해석 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) 은 프로그래밍 언어를 어떻게 설계하고, 정의하고, 처리하는가 를 다루는 컴퓨터 과학 분야입니…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기