애플리케이션 만들기

리스펙스 규칙 실행하기

외부 작성 규칙을 애플리케이션 내부에서 실행하여 판단을 도출하되, 네트워크·시계·파일 시스템 접근 권한은 제어합니다.

비즈니스 로직상의 특정 판단은 소스 코드 내부에 둘 영역이 아닙니다. 요청 승인 여부, 고객 등급 분류, 청구 점수 산정 등이 이에 해당합니다. 이러한 규칙은 각기 다른 주기로 변경되며, 팀 외부에서 작성하는 경우도 많습니다. 본 가이드는 이와 같은 규칙을 별도 파일로 분리하고, 입력을 전달하여 판단 결과를 반환받는 구조를 설명합니다. 아울러 실행 과정에서 규칙이 다른 자원에 접근하지 못하도록 제어하는 방법을 안내합니다.

규칙은 리스펙스로 작성합니다. 리스펙스는 의사결정 규칙을 위한 결정론적 리스프이며, 동일한 규칙과 입력에 대해서는 항상 같은 결과를 반환합니다. 토파즈는 1회 준비 후 다회 평가(prepare-once/evaluate-many) 런타임 구조를 따릅니다. 준비된 규칙은 파일 열기, 시계 조회, 네트워크 접근이 불가능합니다. 허용된 작업량을 소진하면 해당 시점에서 실행을 중단합니다. 이렇게 구성된 패키지는 완전한 현재 프로필(complete-current-profile) 연동을 사용하는 리스펙스 결정 애플리케이션입니다. 본 문서의 남은 부분에서는 이러한 패키지를 직접 구축해 봅니다.

본 문서는 토파즈 패키지 작성 경험이 있음을 전제로 설명합니다. 토파즈 사용이 처음이라면 첫 애플리케이션 문서부터 확인하세요.

규칙 작성하기

규칙은 일반적인 리스펙스 소스 파일입니다. 아래 규칙은 두 숫자를 비교하여 두 문자열 중 하나를 반환합니다. rules/approve.lspx 파일로 저장하세요.

LISPEX
(if (< 10 15) "allow" "deny")

아래에서 만들 패키지는 추가적인 규칙 세트를 사용합니다. rules/classify.lspx는 문자열 대신 숫자를 반환합니다.

LISPEX
(+ 20 22)

rules/deadline-probe.lspx는 스스로 종료되지 않습니다. 애플리케이션 마감 시간이 동작하여 중단시킬 대상을 다음과 같이 마련합니다.

LISPEX
(letrec ((loop (lambda () (loop)))) (loop))

네 번째 규칙은 rules/approve.lspx를 훨씬 낮은 한도로 재사용하므로, 별도의 소스 파일을 두지 않습니다.

두 가지 한도 정하기

작업량 상한을 정의하는 문서는 두 종류로 나뉩니다. 한도 문서는 개별 규칙을 제어하고, 쿼터 문서는 애플리케이션 전체를 제어합니다.

rules/approve.limits.json은 단일 규칙을 준비하고 평가할 때의 상한을 고정합니다. 완전한 문서에는 모든 필드가 유지되어야 하며, 값은 낮출 수 있으나 필드를 추가하거나 삭제할 수는 없습니다. 아래 예시에는 두 가지 작업량 상한만 표시되어 있습니다.

JSON
{
  "schema": "topaz.lispex-embed-limits/v1",
  "prepare": {
    "prepare_work": 1000000
  },
  "evaluate": {
    "eval_work": 10000
  }
}

prepare_workeval_work는 경과 시간을 측정하지 않고, 평가기가 투입한 노력을 결정론적으로 측정합니다. 따라서 동일한 규칙과 동일한 입력은 모든 실행 환경에서 같은 작업량을 소비합니다.

규칙마다 고유한 한도 문서가 필요합니다. rules/classify.limits.jsonrules/deadline-probe.limits.json도 동일한 구조입니다. rules/semantic-limit-probe.limits.json은 같은 문서에서 eval_work 값만 1로 변경한 파일입니다. 이는 규칙이 완료되기에는 턱없이 부족한 값이며, 의도적으로 설정된 것입니다. 패키지는 이 규칙을 통해 리소스가 부족할 때 어떤 상태가 되는지 보여 줍니다.

rules/application.quotas.json은 개별 규칙이 아닌 호스트 단위의 한도를 설정합니다. 동시 평가 2회, 대기 평가 2회, 전체 평가 64회 및 유한한 벽시계 마감 시간을 허용합니다.

두 문서의 차이는 중단 시점에 드러납니다. 개별 eval_work 상한을 초과한 규칙은 의미 한도를 소진하며, 해당 평가는 애플리케이션 코드에서 정상적으로 수용할 수 있는 결과로 반환됩니다. 반면 쿼터를 초과한 애플리케이션은 호출 자체를 거부합니다. 두 결과는 서로 다른 타입이므로, 이를 동일하게 다루는 코드는 특정 상황에서 오류를 일으킵니다.

패키지 선언하기

매니페스트 파일이 구성 요소들을 결합합니다. 토파즈 언어 사양과 표준 라이브러리를 지정하고, 네이티브 배포 타깃을 선언하며, 완전한 현재 리스펙스 프로필과 애플리케이션 계약명을 명시하고, 쿼터 문서 경로와 전체 규칙 목록을 정의합니다. topaz.toml 파일로 저장하세요.

TOML
[package]
name = "complete_lispex_decision_application"
version = "0.1.0"
language = "5.19"
entry = "src/main.tpz"

[build]
target = "native"
deterministic = true

[dependencies]
std = "5.19"

[lispex]
profile = "lispex/r7rs-rule-current-profile-bounded/1"
application = "topaz/lispex-decision-application/2"
application_quotas = "rules/application.quotas.json"

[[lispex.rule]]
name = "approve"
source = "rules/approve.lspx"
limits = "rules/approve.limits.json"

[[lispex.rule]]
name = "classify"
source = "rules/classify.lspx"
limits = "rules/classify.limits.json"

[[lispex.rule]]
name = "deadline_probe"
source = "rules/deadline-probe.lspx"
limits = "rules/deadline-probe.limits.json"

[[lispex.rule]]
name = "semantic_limit_probe"
source = "rules/approve.lspx"
limits = "rules/semantic-limit-probe.limits.json"

languagestd는 임의로 선택하는 값이 아니라 애플리케이션 계약에 의해 고정되는 항목이며, 언어 모드가 일치하지 않는 툴체인은 패키지 준비 작업을 거부합니다. 툴체인 상태 문서에서 설치 환경의 컴파일러, 언어 모드, 런타임 정보를 확인할 수 있습니다.

profile은 정확한 완전한 현재 프로필 평가기를 가리키며, 식별자는 lispex/r7rs-rule-current-profile-bounded/1입니다. 제공자가 정한 이 토큰의 bounded는 고정된 리소스 경계를 뜻하며, 더 작은 5.18 호환 프로필을 선택한다는 의미가 아닙니다. application은 완성된 패키지를 배포하는 방식을 규정하는 5.19 계약을 가리킵니다. 두 항목 모두 조합을 선택하는 설정이 아니라 고정된 명칭입니다.

[[lispex.rule]] 항목에 포함되는 필드는 예외 없이 3개입니다. namestd.lispex.rules 모듈 내 함수로 생성되므로 approverules.approve() 형태가 됩니다. 이는 런타임에 해석되는 파일 경로가 아니며, 구성 요소를 선택하는 선택기도 아닙니다. approvesemantic_limit_probe처럼 두 항목이 하나의 소스 파일을 공유하면서 서로 다른 한도를 적용받을 수도 있습니다.

패키지 잠그기

잠금 작업은 각 규칙을 최초 1회 준비하고, 준비된 결과를 명확히 기록합니다. 패키지 루트에서 실행하세요.

BASH
topaz lock --root .

구성 요소나 준비 아티팩트의 다이제스트는 직접 수작업으로 작성하지 않습니다. 커밋을 진행하기 전에 topaz.lock 파일을 검토해 보세요. [lispex] 절은 프로필·애플리케이션 계약·구성 요소·평가기·ABI·값 코덱·계량 모델·아티팩트 계약·어댑터·쿼터·타깃 처분 방침 및 생성 핸들 카탈로그를 고정합니다. 각 [[lispex.rule]] 행은 해당 규칙의 소스, 한도, 준비 요청·제출 내역, 준비 아티팩트를 고정합니다.

여기서 구성 요소란 평가기 엔진 자체를 의미합니다. 잠금 파일이 다이제스트로 명시해 두므로, 이후 빌드 과정에서 다른 평가기로 임의 변경될 수 없습니다.

매니페스트, 규칙 소스, 한도 문서, 쿼터, 구성 요소, 프로필 중 하나라도 변경되면 잠금 파일을 다시 생성해야 합니다. --locked 빌드는 다른 규칙을 임의로 준비하지 않으며 변경 사항이 발생하면 동작을 거부합니다.

토파즈 코드에서 규칙 호출하기

잠금 작업은 std.lispex 모듈에 API를 생성하고, 선언된 규칙마다 대응하는 함수 하나를 std.lispex.rules에 만듭니다.

TOPAZ
let 판정 = evaluate(rules.approve(), input, defaultLimits(rules.approve()))
let 기록 = evaluateWithEvidence(
    rules.approve(),
    input,
    defaultLimits(rules.approve()),
)

evaluate는 판정 결과를 반환합니다. 판정은 단일 평가에 대한 타입화된 결과로, 규칙이 정상적으로 완료되었는지, 자체 조건에 의해 실패했는지, 또는 한도를 소진했는지를 알려 줍니다. defaultLimits는 잠금 파일이 해당 규칙에 기록해 둔 상한 값을 다시 읽어옵니다. 따라서 호출 시 패키지가 선언한 범위보다 더 큰 리소스를 요구할 수 없습니다.

input은 정본 값입니다. 여기서 정본이란 양 측이 합의한 바이트 표현을 의미하며, 하나의 입력이 항상 단일한 의미만을 갖도록 보장합니다. 평가는 실행할 때마다 게스트 메모리, 전역 변수, 계량기, 트랜스크립트 상태를 초기화합니다. 따라서 한 규칙의 실행이 다음 규칙에 잔재를 남길 수 없습니다.

결과만 필요할 때는 evaluate를 사용합니다. 결정론적 결과와 함께 검증 기록을 남겨야 할 때는 evaluateWithEvidence를 사용합니다.

증거 저장하고 검증하고 재실행하기

evaluateWithEvidence가 유효한 결과에 결정론적으로 도달하면 소비자 아티팩트도 함께 생성합니다. 아티팩트는 해당 평가 실행 기록을 내포합니다. 따라서 영구 저장하여 외부에 전달할 수 있으며, 결과를 생성한 프로그램을 신뢰하지 않더라도 사후 검증이 가능합니다.

전체 라이프사이클은 6개의 함수 호출로 구성됩니다.

  1. consumerArtifactBytes는 아티팩트 전체를 영구 저장 가능한 바이트 데이터로 변환합니다.
  2. consumerArtifactFromBytes는 저장된 바이트 데이터를 검사하여 다시 수용합니다.
  3. inspectConsumerArtifact는 아무것도 실행하지 않고 안정된 식별자 정보를 반환합니다.
  4. verifyConsumerArtifact는 구조, 다이제스트, 바인딩 상태를 검사합니다.
  5. portableCoreBytes는 아티팩트에 코어가 포함된 경우 리스펙스 형식의 바이트를 추출합니다.
  6. freshReplay는 잠긴 규칙과 동일한 입력을 신규 게스트 인스턴스에서 재평가하여, 동일한 아티팩트만을 수용합니다.

아티팩트를 통해 결론을 도출할 때는 주의해야 합니다. 아티팩트와 이식 가능 코어는 소비자 측에서 생성되며 별도의 검증 필인을 거치지 않습니다. 즉, 발행자 명시, 제공자 승인, 서명 권한, 구성 요소 인가, 외부 행위 권한 등을 포함하지 않습니다. 실행 거부, 취소, 안전 한도 선점, 엔진 오류 발생 시에는 이식 가능 코어가 생성되지 않습니다.

패키지 검사하고 실행하기

검증 단계를 먼저 수행한 후 실행 단계를 진행합니다. 두 과정 모두 잠금 파일에 정의된 명세에 따라 실행됩니다.

BASH
topaz check --root . --locked
topaz run --root . --locked -- all

all 인자는 전체 경계 조건을 검증하는 시나리오를 실행합니다. 일반 입력 24개를 평가하고, 두 규칙 간 상호 상태 격리를 확인하며, 특정 규칙을 의미 소진 상태까지 실행하고, 애플리케이션 쿼터(64회)에 도달하여 65번째 평가 요청을 거부하도록 처리합니다. 해당 패키지의 완전한 구현체는 메인 프로젝트에서 유지 관리하며, all 실행 시 정확히 한 줄의 출력을 반환합니다.

all:default:24:evidence:verified:replayed:isolation:2:complete:semantic-limit:exhausted:aggregate-quota:64:refused

다른 출력 문자열이 나오거나 종료 코드가 0이 아니면 검증 실패로 간주합니다.

deadline_probe 규칙은 의도적으로 all 시나리오에서 제외되었습니다. 마감 시간 검증은 릴리스 테스트 단계에서 별도로 처리합니다. 패키지를 복제하고 애플리케이션의 벽시계 쿼터를 100밀리초에서 1밀리초로 낮춘 뒤, 복제된 잠금 파일에 해당 쿼터를 다시 결합합니다. 이후 인터프리터와 네이티브 릴리스 빌드 양쪽에서 DeadlineExceeded 발생을 검증합니다. 반면 100밀리초 쿼터가 적용된 일반 애플리케이션은 정상적으로 완료되어야 합니다. 이 테스트를 분리해 둔 덕분에 위의 검증 출력 결과는 호스트 환경의 처리 속도에 영향을 받지 않습니다.

네이티브 제품 빌드하기

최적화된 관리형 네이티브 결과물을 패키지 루트 바깥 디렉터리에 생성합니다.

BASH
topaz build --root . --locked --release --out-dir ../complete-product

빌드를 수행한 운영체제와 아키텍처 환경에서 결과물을 실행합니다.

BASH
../complete-product/target/release/program all

Windows 환경에서는 다음 명령을 사용합니다.

POWERSHELL
..\complete-product\target\release\program.exe all

네이티브 결과물 실행 시에도 동일한 한 줄의 출력이 반환되어야 합니다. 빌드가 정상적으로 완료되었더라도 해당 실행 파일을 다른 타깃 환경으로 이식하여 사용할 수 있는 것은 아닙니다.

완성한 애플리케이션을 실행할 수 있는 곳

애플리케이션 계약은 명시적으로 허용된 배포 경로 집합만을 받아들입니다. 거부 타깃 경로는 파일 작성이나 실행 단계에 진입하기 전에 거부되며, 인터프리터나 네이티브 제품으로의 폴백을 수행하지 않습니다.

경로처분
interpreter잠긴 완전한 현재 프로필 애플리케이션 지원
native승인된 네이티브 릴리스 대상에서만 지원
generated-python아티팩트 작성 전 거부
raw-web아티팩트 작성 전 거부
worker-web아티팩트 작성 전 거부
managed-web아티팩트 작성 전 거부
http-service아티팩트 작성 전 거부
no-capability실행 또는 아티팩트 작성 전 거부
mcp-empty-component-set실행 전 거부

제한 프로필용 5.18 계약은 기존 패키지 지원을 위한 불변 호환 경로로만 유지됩니다. 이 경로는 자체 구성 요소·프로필·계약·준비 아티팩트 식별자를 그대로 사용하며, 선택기나 폴백으로 이 문서의 완전한 현재 프로필 애플리케이션을 대신할 수 없습니다.

관련 문서