Workspace IndexAlgorithms › Day 35

[Review] The Execution Stack on One Page TODO

Algorithms · Day 35 / 100 · B. Compilers, Runtimes & VMs (Day 20-35)

Concept

You can view the execution stack as a chain of stages that turns source code into actual hardware behavior. At the front is the frontend, which lexes and parses into an AST and does semantic analysis to pin down types and names; in the middle is the optimization stage, which works on an intermediate representation like SSA and performs constant propagation, inlining, dead code elimination, and so on. At the back, instruction selection, register allocation, and code generation produce target instructions (native machine code or VM bytecode), which an interpreter or JIT then executes while the runtime manages memory, GC, and exceptions. Mapped onto Ethereum: Solidity goes through an IR like Yul to become EVM bytecode, and the client's EVM interpreter executes that, deducting gas for every opcode. The point of this map is "which decision gets locked in at which stage" — when you're looking at an optimization failure or a performance problem, pinpointing the responsible stage first is the key move.

When you hit a performance issue or unexpected behavior, if you can't immediately narrow down whether to look at the source, compiler optimizations, VM execution, or the runtime, debugging drifts into guesswork. The layer map is the baseline for that narrowing.

Code & Formula

# [복습] 실행 계층 지도 — 렉싱 -> 파싱(AST) -> 최적화(상수 전파) -> 실행 까지, 한 표현식으로 전 단계를 통과시킨다.

import re

# 1) 프론트엔드: 렉서 — 소스 문자열을 토큰으로 쪼갠다.
def lex(src):
    return re.findall(r"\d+|[+\-*/()]", src)

# 1) 프론트엔드: 파서 — 토큰을 AST(중첩 튜플)로 만든다. 우선순위: * / > + -
def parse(tokens):
    pos = 0
    def peek():
        return tokens[pos] if pos < len(tokens) else None
    def parse_expr():
        nonlocal pos
        node = parse_term()
        while peek() in ('+', '-'):
            op = tokens[pos]; pos += 1
            node = (op, node, parse_term())
        return node
    def parse_term():
        nonlocal pos
        node = parse_factor()
        while peek() in ('*', '/'):
            op = tokens[pos]; pos += 1
            node = (op, node, parse_factor())
        return node
    def parse_factor():
        nonlocal pos
        tok = tokens[pos]; pos += 1
        if tok == '(':
            node = parse_expr(); pos += 1  # skip ')'
            return node
        return int(tok)
    return parse_expr()

# 2) 미들엔드: 최적화 — 상수 전파/폴딩 (양쪽이 이미 리터럴이면 컴파일 타임에 계산해 버린다)
def constant_fold(node):
    if isinstance(node, int):
        return node
    op, left, right = node
    left, right = constant_fold(left), constant_fold(right)
    if isinstance(left, int) and isinstance(right, int):
        return {'+': left + right, '-': left - right, '*': left * right, '/': left // right}[op]
    return (op, left, right)

# 3) 백엔드/런타임: 인터프리터 — 최종 AST(또는 폴딩된 상수)를 실제로 실행한다.
def interpret(node):
    if isinstance(node, int):
        return node
    op, left, right = node
    l, r = interpret(left), interpret(right)
    return {'+': l + r, '-': l - r, '*': l * r, '/': l // r}[op]

source = "1 + 2 * (3 + 4)"
tokens = lex(source)
ast = parse(tokens)
folded = constant_fold(ast)
result = interpret(ast)

print("source     :", source)
print("tokens     :", tokens)
print("ast        :", ast)
print("folded ast :", folded, " <- 상수 전파로 이미 스칼라 하나로 굳음")
print("result     :", result)

Exercise

On a single page, draw out the stages from source to hardware yourself, and next to each stage write its counterpart in the Ethereum stack (Solidity, Yul IR, EVM bytecode, the client's interpreter, gas metering) until there are no blanks left.

Practical Connection

When you hit a gas regression, figuring out whether it's from a change in compiler optimization settings, an opcode price change, or a change in contract logic is exactly the work of reading this map.

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

[복습] 실행 계층 지도 한 장으로 TODO

Algorithms · Day 35 / 100 · B. 컴파일러·런타임·VM (Day 20–35)

개념

실행 계층은 소스 코드가 실제 하드웨어 동작이 되기까지의 단계 사슬로 볼 수 있다. 앞단은 렉싱·파싱으로 AST를 만들고 의미 분석으로 타입과 이름을 확정하는 프론트엔드이고, 가운데는 SSA 같은 중간 표현 위에서 상수 전파·인라이닝·죽은 코드 제거 등을 수행하는 최적화 단계다. 뒷단은 명령어 선택·레지스터 할당·코드 생성으로 타깃 명령어(네이티브 기계어 또는 VM 바이트코드)를 뽑고, 그 결과를 인터프리터나 JIT가 실행하며 런타임이 메모리·GC·예외를 관리한다. 이더리움에 대응시키면 Solidity가 Yul 같은 IR을 거쳐 EVM 바이트코드가 되고, 클라이언트의 EVM 인터프리터가 이를 실행하면서 각 opcode마다 가스를 차감하는 구조다. 이 지도의 요점은 '어떤 결정이 어느 단계에서 굳어지는가'이며, 최적화 실패나 성능 문제를 볼 때 원인 단계를 먼저 특정하는 것이 핵심이다.

성능이나 동작 이상을 만났을 때 소스, 컴파일러 최적화, VM 실행, 런타임 중 어디를 봐야 하는지 즉시 좁히지 못하면 디버깅이 추측으로 흐른다. 계층 지도는 그 좁히기의 기준선이다.

코드 · 수식

# [복습] 실행 계층 지도 — 렉싱 -> 파싱(AST) -> 최적화(상수 전파) -> 실행 까지, 한 표현식으로 전 단계를 통과시킨다.

import re

# 1) 프론트엔드: 렉서 — 소스 문자열을 토큰으로 쪼갠다.
def lex(src):
    return re.findall(r"\d+|[+\-*/()]", src)

# 1) 프론트엔드: 파서 — 토큰을 AST(중첩 튜플)로 만든다. 우선순위: * / > + -
def parse(tokens):
    pos = 0
    def peek():
        return tokens[pos] if pos < len(tokens) else None
    def parse_expr():
        nonlocal pos
        node = parse_term()
        while peek() in ('+', '-'):
            op = tokens[pos]; pos += 1
            node = (op, node, parse_term())
        return node
    def parse_term():
        nonlocal pos
        node = parse_factor()
        while peek() in ('*', '/'):
            op = tokens[pos]; pos += 1
            node = (op, node, parse_factor())
        return node
    def parse_factor():
        nonlocal pos
        tok = tokens[pos]; pos += 1
        if tok == '(':
            node = parse_expr(); pos += 1  # skip ')'
            return node
        return int(tok)
    return parse_expr()

# 2) 미들엔드: 최적화 — 상수 전파/폴딩 (양쪽이 이미 리터럴이면 컴파일 타임에 계산해 버린다)
def constant_fold(node):
    if isinstance(node, int):
        return node
    op, left, right = node
    left, right = constant_fold(left), constant_fold(right)
    if isinstance(left, int) and isinstance(right, int):
        return {'+': left + right, '-': left - right, '*': left * right, '/': left // right}[op]
    return (op, left, right)

# 3) 백엔드/런타임: 인터프리터 — 최종 AST(또는 폴딩된 상수)를 실제로 실행한다.
def interpret(node):
    if isinstance(node, int):
        return node
    op, left, right = node
    l, r = interpret(left), interpret(right)
    return {'+': l + r, '-': l - r, '*': l * r, '/': l // r}[op]

source = "1 + 2 * (3 + 4)"
tokens = lex(source)
ast = parse(tokens)
folded = constant_fold(ast)
result = interpret(ast)

print("source     :", source)
print("tokens     :", tokens)
print("ast        :", ast)
print("folded ast :", folded, " <- 상수 전파로 이미 스칼라 하나로 굳음")
print("result     :", result)

연습

한 페이지에 소스에서 하드웨어까지의 단계를 직접 그리고, 각 단계마다 이더리움 스택의 대응물(Solidity, Yul IR, EVM 바이트코드, 클라이언트 인터프리터, 가스 계량)을 옆에 적어 빈칸 없이 채워 보라.

실무 · Verex 연결

가스 회귀가 발생했을 때 컴파일러 최적화 설정 변화 때문인지, opcode 가격 변경 때문인지, 컨트랙트 로직 변화 때문인지 구분하는 작업이 정확히 이 지도를 읽는 일이다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 34. 형식 검증36. 메모리 모델과 원자성 순서 →