writing / 2026

Why Xazz has a typed IRXazz에 Typed IR을 넣은 이유

The first version of Xazz parsed source twice. The CLI parsed a .xzz file into an AST, and then the execution engine parsed that AST again against Polars and Burn. Two parses meant two chances for the two sides to drift apart. If a column name meant something different to the checker and the runtime, the mismatch only showed up at runtime.

Xazz 초기 버전은 소스를 두 번 해석했습니다. CLI가 .xzz 파일을 AST로 파싱하고, 실행 엔진이 그 AST를 Polars와 Burn에 다시 맞춰 해석했어요. 두 번 해석한다는 건 어긋날 여지도 두 번이라는 뜻입니다. 컬럼 이름 하나를 체커와 런타임이 다르게 읽으면 그 불일치는 런타임에나 드러났습니다.

In v0.3.0 the pipeline was flattened: lexer → parser → static checker → typed IR → optimizer. The compiler produces the IR once, and the runtime consumes the IR once. There is nothing left to re-interpret.

v0.3.0에서 파이프라인을 단순하게 만들었습니다. 렉서, 파서, 정적 체커, 타입드 IR, 옵티마이저. 컴파일러가 IR을 한 번 만들고, 런타임은 그 IR을 한 번만 소비합니다. 다시 해석할 부분이 남지 않았습니다.

The IR also gave the optimizer a place to live. Three passes in opt.rs: constant folding, merging consecutive selects, and pushing filters before selects — all meaning-preserving. The interesting part was the null semantics. In the DSL, a filter keeps rows where the expression is true, and null counts as false. So a filter can only move past a select or withColumn if every column it reads still exists on the other side. A filter that references a newly-created column does not move. Division by zero is never folded, either — folding it would change runtime behavior.

IR이 있으니 옵티마이저도 들어갈 자리가 생겼습니다. opt.rs에는 세 가지 패스가 있습니다. 상수 폴딩, 연속 select 병합, select 앞으로의 filter 푸시다운. 전부 의미를 보존합니다. 까다로웠던 건 널 의미론이었어요. 이 DSL에서 filter는 식이 true인 행만 남기고 null은 false로 취급합니다. 그래서 filter는 자기가 읽는 모든 컬럼이 이동 경계 반대편에도 존재할 때만 select나 withColumn을 건너뛸 수 있습니다. 새로 만든 컬럼을 참조하는 filter는 움직이지 않습니다. 0으로 나누는 것도 절대 폴딩하지 않습니다. 폴딩하면 런타임 동작이 달라지기 때문입니다.

One design decision I'm glad I made: the IR is backend-independent. The optimizer works on the typed IR, not on Polars or Burn. So when a GPU backend eventually lands, the passes keep working without changes.

돌아보면 잘한 결정은 IR을 백엔드와 무관하게 둔 것입니다. 옵티마이저는 Polars나 Burn이 아니라 타입드 IR 위에서 동작합니다. 나중에 GPU 백엔드가 붙어도 패스는 그대로 동작할 겁니다.

The whole rewrite came down to one idea: decide what a program means in one place, and let everything else just execute it.

이번 리팩터링의 핵심은 하나였습니다. 프로그램의 의미는 한 곳에서 정하고, 나머지는 실행만 하게 하자.

← portfoliofounder githubx1zz.com