Compiler 계약
Compiler는 template parsing, validation, lowering, artifact emission을 소유한다. Runtime package는 이미 compile한 program을 실행한다. Render request에서 source를 parse하거나 host compiler를 호출하지 않는다.
compiler와 runtime의 단일 계약 원본은 packages/compiler/interface.json이다. 생성 선언부와 Mermaid 도표는 이 manifest와 일치해야 한다. TypeScript, Go, Rust, PHP, Python mapping은 표기와 오류 전달 방식을 바꿀 수 있지만 소유 관계, field 순서, operation 위치, 상태 전이를 바꿀 수 없다.
Manifest의 evidence section은 이 계약을 읽는 기계 판독 경로다. 명세 순서, 적합성 fixture, 실행 가능한 예제, 생성 artifact, 필수 검증 명령을 연결한다. Checker가 연결 대상의 누락을 거부하므로 읽는 사람은 중복 요약에 의존하지 않고 계약에서 실행 증거까지 따라갈 수 있다.
Pipeline
flowchart LR
Source["SourceGraph<br/>entry + templates"] --> Parse["Compiler.parseSourceGraph"]
Parse --> AST["canonical AST graph"]
Types["TypeManifest<br/>entry + root + fields + records + functions + defines + templates"] --> Lower["Compiler.lowerSourceGraph"]
AST --> Lower
Lower --> Typed["TypedProgram<br/>dynamicRoot + entry + fields + records + functions + definitions + templates"]
Typed --> AstArtifact["AST artifact"]
Typed --> Backend["LanguageBackend"]
Backend --> GeneratedArtifact["generated host artifact"]
AstArtifact --> AstProgram["AstProgram"]
GeneratedArtifact --> GeneratedProgram["GeneratedProgram"]
AstProgram --> Program["Program.prepare + Program.render"]
GeneratedProgram --> Program
RuntimeBindings["RuntimeBindings"] --> AstProgram
RuntimeBindings --> GeneratedProgram
RuntimeEnvironment["RuntimeEnvironment"] --> AstProgram
RuntimeEnvironment --> GeneratedProgramclassDiagram
class Compiler {
+sourceGraph
+typeManifest
+typedProgram
+loadSourceGraph(source manifest) SourceGraph
+parseSourceGraph(SourceGraph) canonical AST graph
+lowerSourceGraph(canonical AST graph, TypeManifest) TypedProgram
+emitArtifact(TypedProgram, CompileMode, LanguageBackend) ArtifactManifest and files
}
class LanguageBackend {
+target
+emitDeclarations(TypedProgram) host declarations
+emitRuntime(TypedProgram) host runtime bindings
+emitTemplates(TypedProgram) host template functions
+emitEntry(TypedProgram) host program entry
}
class ArtifactStore {
+artifactManifest
+files
+load(ArtifactManifest) Program
+regenerate(CompileMode, LanguageBackend) ArtifactManifest
+loadOrRefresh(CompileMode, ArtifactRefresh) Program
}
class Engine {
+program
+prepare(RenderRequest) PreparedRender
+render(RenderRequest) UTF-8 string
}
class PageCache {
+html
+ttl
+get(cacheKey) UTF-8 string or miss
+put(cacheKey, html, ttl) void
+getOrSet(cacheKey, ttl, render) UTF-8 string
}
class RuntimeEnvironment {
+limits
+hostFunctions
+register(name, function) void or error
+limits() Limits
+hostFunction(name) HostFunction or missing
}
class SourceGraph {
+entry
+templates
}
class TypeManifest {
+entry
+root
+fields
+records
+functions
+defines
+templates
}
class TypedProgram {
+dynamicRoot
+entry
+fields
+records
+functions
+definitions
+templates
}
class ArtifactManifest {
+schema
+mode
+target
+entry
+delimiters
+sourceDigest
+typeDigest
+contractDigest
+compilerDigest
+files
}
class RenderRequest {
+target
+assign
+define
+env
}
class Program {
+prepare(target, assign, options) PreparedRender or error
+render(target, assign, options) UTF-8 string or error
}
class AstProgram
class GeneratedProgram
class RuntimeBindings {
+truthy(value) bool
+unary(operator, operand, frame, span) value or error
+binary(operator, left, right, frame, span) value or error
+stringify(value, frame, span) string or error
+escape(value, frame, span) string or error
+number(value, frame, span) number or error
+finite(number, frame, span) number or error
+equal(left, right, strict) bool
+compare(left, right, frame, span) ordering or error
+member(container, key, frame, span) value or error
+memberCall(container, method, args, frame, span) value or error
+classCall(className, method, args, frame, span) value or error
+index(container, key, frame, span) value or error
+depth(value, frame, span) value or error
+entries(value, frame, span) entries or error
+listSpread(value, frame, span) list or error
+mapSpread(value, frame, span) map or error
+call(name, args, frame, span) value or error
+limit(kind, count, frame, span) void or error
+error(frame, span, code, message) error
}
class RuntimeServices {
+limits() Limits
+hostFunction(name) HostFunction or missing
+classFunction(className, method) HostFunction or missing
}
Program <|-- AstProgram
Program <|-- GeneratedProgram
Compiler --> TypedProgram
Compiler --> LanguageBackend
LanguageBackend --> ArtifactManifest
ArtifactStore --> ArtifactManifest
ArtifactStore --> Program
Engine --> Program
RuntimeServices <|.. RuntimeEnvironment
AstProgram --> RuntimeEnvironment
GeneratedProgram --> RuntimeEnvironment
AstProgram --> RuntimeBindings
GeneratedProgram --> RuntimeBindings하나의 source graph와 명시적인 type manifest를 하나의 typed program으로 lower한다. ast는 언어 중립적인 canonical AST artifact를 생성한다. gen은 같은 typed program을 선택한 TypeScript, Go, Rust, PHP, Python backend로 전달한다. JavaScript ESM은 TypeScript backend가 만드는 배포 artifact이며 별도 의미 구현이 아니다.
Compile request는 최초 delimiter 쌍을 전달한다. Canonical artifact manifest는 이 값을 기록하고 source digest는 모든 template byte와 함께 delimiter를 결합한다. 따라서 delimiter만 바뀌어도 AST와 여기서 파생한 모든 generated artifact가 무효화된다.
Source compiler는 .tpl 파일에서 시작해 모든 정적 include와 block 경로를 따라간다. 참조된 template은 어떤 확장자도 사용할 수 있으며, 발견된 파일의 byte·AST·line index는 같은 source graph에 포함된다. 동적 conformance manifest는 include 대상의 자유 입력을 도출하고 중첩 include 경로로 전파해 호출자 local을 명시적으로 결합한다.
Canonical artifact manifest는 각 template의 digest 항목과 함께 source 각 줄의 시작 byte offset을 기록한다. Generated program은 이 line index와 보존된 node·expression span을 조합해 runtime에 template source를 읽지 않고 원본 line과 column을 보고한다.
각 backend는 별도 LanguageBackend 구현이다. Backend는 구조화한 code writer를 통해 선언부와 직접적인 template 제어 흐름을 생성한다. Scenario 이름, 고정 request data, 예상 output 또는 직렬화한 AST node interpreter를 포함하지 않는다.
공개 구조
TypeManifest.root는 Assign 또는 any다. Assign은 고정된 root field를 선언한다. any는 입력 계약이 의도적으로 동적일 때 알 수 없는 member를 any?로 취급하는 map 형태 root를 선언한다. 두 형태 모두 record field, definition target, template input, host-function signature는 명시한다. 한 request 값에서 type을 추론하지 않는다. 개별 field나 parameter의 any는 별도 실행 mode를 만들지 않고 runtime value model을 사용하며, root가 Assign일 때 field 검사를 끄지 않는다.
Typed IR은 모든 symbol, template path, definition target, function signature, input type을 해석하고 모든 node와 expression의 source span을 보존해 generated runtime 실패가 원본 template을 가리키게 한다. 변수 누락, 정적 타입에 없는 member, 선언하지 않은 definition, 잘못된 include path, 알 수 없는 함수 구현 종류, 호환되지 않는 block input은 compile 실패다. 명시적인 동적 any의 member, index, spread, loop는 IR에 남아 RuntimeBindings를 사용한다. Built-in과 host signature는 같은 call node를 사용하고 program load를 위해 구현 종류를 보존한다.
Local 변수는 직선 코드에서 마지막 assignment의 type을 가진다. Branch나 loop body가 다른 type의 값으로 assign하는 local은 loop body 앞과 branch 또는 loop 뒤에서 그 assignment 전체를 병합한 type을 가진다. Loop body는 이전 반복이 assign한 값을 읽고 branch 뒤의 코드는 실행된 branch의 값을 읽기 때문이다. 서로 다른 두 type은 any로 병합한다.
동적 source graph는 parse된 template과 요청 definition schema에서 생성한 type manifest를 사용한다. 이 manifest는 참조한 함수, definition target과 block input을 선언하되 root value를 any로 유지하며 sample JSON에서 정적 host type을 추측하지 않는다. 적합성 gate는 host 언어 artifact를 컴파일하기 전에 parse 가능한 모든 fixture의 이 도출 과정을 검사한다.
도출한 manifest는 알고 있는 각 내장 함수의 선언된 minArgs와 maxArgs를 복사한다. 동적 conformance source는 잘못된 호출이 AST 실행과 같은 위치의 E_RUNTIME_ARITY를 내도록 arity 검사를 runtime에 남기며, 명시적인 정적 manifest는 lowering 중에 그 호출을 거부할 수 있다. 알 수 없는 함수는 열린 arity 호출로 남아 runtime registry에 도달하고, 구현이 없으면 일반 runtime 오류를 낸다.
모든 generated module은 논리 구조 Assign, Definitions, Input<T>, GeneratedProgram을 노출한다. 다른 target의 module은 DefinitionData<T>, Definition<T>, ArtifactManifest도 노출한다. Rust module은 program이 definition을 선언할 때만 Definition<T>를 선언하고 그 template과 data는 block이 읽을 때만 두며, block이 definition data를 merge하는 template에만 DefinitionData<T>를, 선언된 type이 가리키는 record를, template이 읽는 Assign의 field를, 값을 다시 변환할 때만 to_value를 선언하고 ArtifactManifest는 선언하지 않는다. module을 private module로 포함하는 crate는 쓰이지 않는 구조, field, method를 dead code로 보고하기 때문이다. program이 읽지 않는 root field나 definition data도 그 type의 값을 만들지 않고 선언된 type에 대해 검사되며, 읽는 값과 같은 field 경로로 실패한다. Generated program은 AST program과 같은 Program.prepare(RenderRequest), Program.render(RenderRequest) operation을 구현한다. Template별 함수는 private이고 include와 block target에서 서로 직접 호출한다.
다섯 generated backend 모두 generated template 함수에 runtime RenderScope를 전달한다. Include는 같은 scope를 전달하므로 내부 assign이 호출자에게 유지된다. Block은 새 scope를 만들고 root data, definition data, 명시적인 block 인자만 넣는다. Loop binding은 실행 중에만 설치하고 종료 후 이전 상태로 복원한다. 각 generated loop는 entry를 구체화하고 반복 전에 size와 마지막 index를 한 번 계산하며, 매 반복에서 현재 index, key, value, first, last metadata를 scope에 할당한다. Block의 root 전용 fallback은 IR에서 일반적인 scope 인식 동적 조회와 별도로 표현한다. Generated 지원 수준은 다섯 core 언어와 tests/cases의 모든 canonical case에서 완료되었다.
RuntimeServices는 두 program mode가 함께 사용하는 runtime interface다. 구체적인 RuntimeEnvironment가 자원 제한, 등록 함수, 논리 class function을 소유하고 등록을 한 번 검증하며 이 interface를 구현한다. 각 AstProgram과 GeneratedProgram은 environment 하나를 소유한다. AST에만 필요한 template loading은 AST renderer에 속하며 environment나 generated 실행 상태에는 들어가지 않는다. RuntimeBindings가 RuntimeServices를 사용하므로 generated 코드는 AST loader나 interpreter에 의존하지 않고 같은 값·오류 의미를 사용한다.
Generated expression은 unary와 eager binary 연산, truthiness, 문자열 변환, escaping, 숫자 변환, 유한 산술 결과 검증, 동등성, 정렬, lookup, 반복 entry, 함수, limit, error를 target runtime의 RuntimeBindings로 처리한다. AST evaluator도 같은 unary와 binary 연산을 사용하며 evaluator와 generated 제어 흐름에는 &&, ||, ??에 필요한 lazy branch 선택만 남는다. 실제 선언은 compiler manifest와 대조한다. Backend는 typed operand에서 data-model 규칙과 정확히 같은 경우에만 native operation을 생성할 수 있다.
TypeScript backend는 bind한 root, source line index가 있는 frame, render context, runtime bindings를 직접 template 함수에 전달한다. 이 함수들은 context output builder에 기록하므로 generated 실행도 AST 실행과 같은 UTF-8 output 제한과 위치 오류를 사용한다. Include와 block은 공통 render chain에 enter·leave하면서 generated template 함수를 직접 호출한다.
PHP backend도 같은 실행 경계를 따른다. 생성된 template method는 Context, Frame, RuntimeBindings와 bind한 root map을 받고 context output limiter를 통해 출력하며 값 연산과 함수 호출을 package runtime에 위임한다. Typed map은 MapValue로 유지하므로 generated lookup, truthiness, ordering, spread가 PHP 배열의 key 변환에 영향을 받지 않고 data model을 보존한다.
PHP backend는 compiler 호출자가 정하는 namespace를 요구한다. compileFiles의 phpNamespace 옵션 또는 compiler 명령의 --php-namespace 옵션으로 지정한다. Namespace는 \로 구분한 하나 이상의 PHP 식별자다. 유효한 namespace 없이 PHP로 compile하면 source를 생성하기 전에 실패한다. PHP target은 출력 directory에 class마다 PSR-4 파일 하나를 쓰며, 각 파일은 자신의 class 이름을 갖고 그 namespace 안에 class를 선언하고, class 이름은 PascalCase다(InputLayoutTpl, DefinitionDataLayoutTpl): record, Assign, Definition, Definitions, ArtifactManifest, template마다 static method를 가진 GeneratedTemplates, program이 쓰는 helper를 가진 GeneratedRuntime, GeneratedExecution, GeneratedProgram이다. compileFiles는 파일을 경로별로 반환하고, compileSource는 파일 하나를 쓰는 target을 위한 것이다. Directory 옆의 artifact manifest는 모든 파일과 그 byte의 digest를 나열한다. Host는 namespace를 directory에 대응시키는 Composer의 PSR-4 autoload(composer.json의 autoload.psr-4 또는 ClassLoader::addPsr4)로 program을 load한다. Program이 내장 binding schema로부터 instance를 만드는 class도 같은 namespace에서 해석한다. 따라서 namespace가 다른 두 generated program은 한 PHP process에서 load하고 render할 수 있으며, 어느 쪽도 generated directory 밖의 class와 충돌할 수 있는 전역 이름을 선언하지 않는다.
모든 target은 program이 쓰는 helper, type, method, import만 그 언어의 formatter 형식과 style로 생성하므로 소비자의 lint가 program에서 통과한다: Go program은 Go 관례의 header // Code generated by @polyspec/template-compiler. DO NOT EDIT.로 시작하고 gofmt가 바꾸지 않으며, Python program은 ruff format이 바꾸지 않으며, PHP 파일은 120열 제한을 포함해 PSR-12를 따른다: 그 제한을 넘을 인자 목록, 배열, 제어 구조의 조건, 연산자 연결은 PSR-12가 나누는 대로 한 줄에 항목 하나씩, 한 단계 들여 나눈다. make generated-lint-check는 showcase의 generated program을 각 생태계의 tool로 lint한다.
모든 backend는 template text, 문자열 literal, template 이름, 경로, message를 포함한 program의 모든 문자열을 target 언어의 정확한 문자열 literal로 기록한다. 따라서 generated program은 유효한 모든 UTF-8 text에 대해 AST program과 같은 byte를 출력한다. 제어 문자와, 큰따옴표 PHP 문자열의 $처럼 target 언어가 문자열 literal 안에서 해석하는 문자도 마찬가지다. PHP backend는 \와 '만 escape한 작은따옴표 literal을 기록하고, 모든 제어 문자는 큰따옴표 escape("\n", "\r", "\xHH")로 기록해 .로 잇는다. 따라서 어떤 literal도 generated source의 두 줄에 걸치지 않고 source는 제어 문자를 갖지 않는다. Rust backend는 \와 "를 escape하고 모든 제어 문자를 \u{…}로 기록한다. Go backend는 \와 "를 escape하고 모든 제어 문자를 \u00XX로 기록한다. TypeScript backend는 JSON 문자열 literal을 기록하며, 이는 정확한 JavaScript 문자열 literal이다.
Go backend는 typed template 제어 흐름을 생성하되 관찰 가능한 값 연산을 render.RuntimeBindings로 전달한다. 생성 함수는 render.Context, source line index가 있는 frame과 render chain을 공유한다. 내부 오류 전파는 code와 source position을 포함한 원래의 구조화 template 오류를 보존하고, native typed value는 runtime 경계에서 canonical value model로 bind한다.
Rust backend도 생성된 template 함수에서 Result를 직접 반환하고 값 연산을 RuntimeBindings에 위임한다. 검증된 artifact 데이터에서 LineIndex를 복원하고 RenderContext를 통해 출력하며 구조화 오류를 panic이 아닌 값으로 전달한다. Panic은 공개 prepare와 render operation에서만 잡아 E_INTERNAL로 보고한다(ERR-13). Typed 경계의 직렬화는 생성 record와 collection을 canonical Value model과 상호 변환한다. 소비자는 generated Rust program을 crate의 tree에 속한 module 파일이 아니라 include!로 crate에 넣는다. build.rs가 compile한 뒤의 OUT_DIR에서, 또는 commit한 파일에서 가져온다. rustfmt는 crate의 module 파일을 format하고 include된 파일은 format하지 않으며 그 배치는 compiler의 것이다. clippy는 include된 코드를 lint하며, 그 코드에는 쓰지 않는 item과 clippy 지적이 없다.
Rust generated source module은 polyspec-template, derive feature를 켠 serde, preserve_order와 arbitrary_precision feature를 켠 serde_json에 직접 의존한다. 이는 generated module의 compile-time 의존성이므로 host crate가 직접 선언해야 한다. 격리 Cargo 설치 프로젝트는 압축을 푼 .crate package를 대상으로 generated module을 compile해 이 경계를 강제한다.
record를 담은 typed 값은 모든 runtime 연산에 canonical 값으로 도달한다. 함수, class 함수, method의 인자, &&, ||, ??를 제외한 연산자의 피연산자, echo의 식이 이에 해당한다. record는 선언 순서의 선언된 필드로 된 map이 되고, record의 list나 map은 모든 깊이에서 그런 map의 list나 map이 된다. 따라서 모든 backend에서 host 함수는 VAL-21 형태를 받고 연산자는 EXP-34를 적용한다. Typed member 접근과 loop는 typed record를 유지한다. 이유: AST program은 같은 데이터에 map을 넘긴다. Go backend는 record를 native object로 넘겼고, TypeScript와 PHP backend에서는 record를 함수에 넘기는 렌더가 E_INTERNAL로 실패했다. make generated-native-check는 tests/fixtures/typed-values를 typed manifest로 모든 backend에서 컴파일하고 출력을 AST program과 비교한다.
내장 함수는 compiler가 알고 있다. 참조한 host 함수에는 manifest signature와 runtime 구현이 필요하다. Signature가 없으면 emission 전에 실패한다. Runtime 구현 누락, arity 오류, host 실패는 AST 실행과 같은 error field를 사용한다.
Artifact 갱신
Compile mode와 artifact 갱신은 서로 독립적인 build 설정이다.
dev는 compiler를 호출할 때마다 parse, validate, emit한다. Development watcher는 정적으로 연결한 Go 또는 Rust process를 rebuild하고 restart한다.true는 source, type, contract, compiler 구현 digest를 계산하고 digest가 달라졌을 때만 emit한다.false는 template source를 읽지 않고 아무것도 emit하지 않는다. 배포 artifact를 검증해 사용한다.
Generated file은 임시 위치에서 완성한 뒤 원자적으로 교체한다. Artifact manifest는 mode, target, entry, source digest, type digest, contract digest, compiler digest와 file을 기록한다. Compiler digest는 해당 target을 만든 parser/compiler module을 포괄하므로 compiler 구현이 바뀌면 나머지 입력이 같아도 artifact를 갱신한다. digest는 추적되는 source로 계산한다. AST artifact는 packages/ts의 source와 build 설정, package-lock.json, artifact writer로, generated artifact는 packages/compiler의 module로 계산하므로, 검사가 build 없이 읽는다(make artifact-digest-check). Artifact가 없거나 손상됐거나 호환되지 않으면 runtime 시작 전에 실패한다.
구현 상태
AST compiler와 runtime은 구현됐다. 제품 compiler는 TypeScript, Go, Rust, PHP, Python의 구체적인 GeneratedProgram을 생성하고 showcase는 TypeScript, Go, Rust, PHP, Python artifact를 직접 실행한다. Generated backend는 native object와 class call operation을 생성한다. TypeScript, Go, Rust, PHP, Python에서 모든 canonical case의 generated matrix가 통과하며, 별도의 generated native-call fixture가 TypeScript, Go, Rust, PHP, Python에서 같은 assign object field, instance method, class function 결과를 출력하며, AST program과 같은 호스트 인자(VAL-21)와 native object 동등성(EXP-39)을 가진다.
Generated artifact는 canonical AST artifact와 같은 갱신 경계를 사용한다. dev는 항상 새 source 파일과 manifest를 생성하고, true는 source·type·contract·compiler digest를 검증한 뒤 재생성 여부를 결정하며, false는 배포된 generated source와 manifest만 읽어 검증한다. Source와 manifest는 원자적으로 교체하며 manifest를 마지막에 반영한다.
make conformance-generated-ts는 canonical case 각각에 대해 새로운 generated TypeScript program을 만든다. Compile 진단, 입력 binding 진단, runtime 진단, 성공한 UTF-8 출력을 AST 실행과 같은 expected artifact에 대조한다. make conformance-generated-php는 각 case마다 별도로 생성하고 문법 검사한 PHP 소스로 같은 내용을 증명한다. make generated-native-check는 모든 backend로 dynamic-root program 하나를 compile하고 native object assign, public field read, instance method call, 등록된 class function call을 실행한다. 같은 matrix에서 public member 누락, 등록되지 않은 class function, native method와 class function의 실패, native 인자의 잘못된 타입과 개수도 검사한다. 모든 backend는 같은 호출 위치에서 같은 오류 코드를 반환해야 한다. 같은 객체는 include한 template, block 인자, template define 데이터에 원본 호스트 객체로 도달해야 한다(VAL-18). PHP backend는 namespace 두 개로 두 번 compile하며, 두 program은 GeneratedProgram과 Assign이라는 전역 class와 함께 한 PHP process에서 load하고 render한다. 유효한 namespace 없이 PHP로 compile하면 실패해야 한다.
두 program mode는 같은 실행 상태 분리를 사용한다. RenderFrame은 name, lines, context만 소유하며 AST를 보유할 수 없다. RenderScope는 locals와 loops를 소유하고 lookup과 loopMeta를 제공하며 include에서 공유되고 block render마다 새로 만들어진다. Interface gate가 다섯 언어의 필드와 연산을 검사한다.
같은 gate가 oxc-parser의 TypeScript AST, Go AST, Rust syntax tree, PHP Reflection으로 구체적인 RuntimeEnvironment의 field 순서, operation 소속과 인자 수를 검사한다. AST program이 limits나 host 함수 상태를 중복 소유하지 않고 environment를 소유하는지도 함께 확인한다.