1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
# aup v0 — 한 언어, 한 모듈 형식
2026-09-25. 상태: v0a(scalar), v0b(바이트·영속 문서·text·커서), v0c(레코드·리스트·인덱스·조건식·표 보기·fn·use) 구현.
text와 리스트 문법은 [002](002-dsl-direction.md).
착수 기준:
- 바꿀 결정: Pure 앱을 두 `.pure` 방언과 두 실행기(워크벤치 Zig scalar VM, 문서 `PureProgram` 해석기)로
계속 만들지, aup 한 언어 → Pure 모듈 한 형식 → Pure 실행기 하나로 합칠지.
- 남은 질문: 작은 언어 하나와 형식 하나로 두 앱의 관찰 가능한 동작을 특수 규칙 없이 표현할 수 있는가?
런타임은 소스를 몰라도 로드 전에 모듈을 검증할 수 있는가?
- 최소 확인: v0a는 워크벤치 예제를 aup로 옮겨, 같은 이벤트열에서 Pure 실행기의 상태가 워크벤치
Zig 엔진과 비트 단위로 같아야 한다. v0b는 문서 편집 앱을 옮겨 기존 CLI와 저장 결과가 같아야 한다.
- 중단 기준: 한쪽 앱에 그 앱만을 위한 규칙이 필요하거나, Pure 실행기가 기존 두 실행기의 합보다
커지면 설계를 다시 본다.
## 의심한 관례와 aup의 선택
| 관례 | aup |
|---|---|
| 앱이 저장·불러오기·직렬화 코드를 쓴다 | 상태를 선언하면 런타임이 소유한다(영속화는 v0b) |
| 트랜잭션과 롤백을 앱이 관리한다 | 모든 이벤트가 트랜잭션이다. 성공하면 반영하고, 실패하면 아무것도 바뀌지 않는다 |
| 요청/응답 핸들러와 라우터 | 입력은 이름 있는 이벤트다. 키·CLI·HTTP는 호스트 어댑터가 입력으로 바꾼다 |
| 화면용 모델을 따로 만든다 | `view`는 상태의 순수 함수다. 호스트와 나중의 Vega가 읽어 표시한다 |
| 런타임이 소스를 해석한다 | 툴체인이 모듈로 컴파일하고, 런타임은 검증한 뒤 실행한다(JVM의 class 파일처럼) |
| 이벤트 처리 시간에 상한이 없다 | 반복이 없고 분기는 앞으로만 한다. 이벤트당 실행량의 상한이 모듈에서 정해진다 |
## v0a 문법
```text
# 주석은 줄 끝까지
state count = 0 # f64 상태. 초기값은 부호 있는 숫자 상수
input tap = key Space # 키 입력. tap.down, tap.up 이벤트
view label = count # 상태의 순수 함수
view width = 40 + min(count, 20) * 8
on tap.down {
count = count + 1
}
on frame { # 1/60초 진행. 핸들러 안에서 dt를 쓸 수 있다
if count > 20 {
count = 0
} else {
count = count + dt
}
}
```
- 선언과 문장은 줄바꿈이나 `;`로 끝난다. 이름은 ASCII `[A-Za-z_][A-Za-z_0-9]*`, 31바이트 이하다.
- 예약어: `state input view on if else key frame dt min max clamp message by bytes u32 reply require record each fn use`.
- 키: `Space Enter ArrowLeft ArrowRight ArrowUp ArrowDown KeyA..KeyZ Digit0..Digit9`. 입력 이름과
키는 각각 한 번만 쓴다. 상태·입력·보기의 이름공간은 서로 겹칠 수 없다.
- 식: 숫자, 상태 이름, `dt`(핸들러 안에서만), 괄호, 단항 `- !`, 이항 `* /`, `+ -`,
`< <= > >=`, `== !=`, `&&`, `||`(뒤로 갈수록 우선순위가 낮다), `min(a,b) max(a,b) clamp(x,lo,hi)`.
- 문장: `이름 = 식`, `if 식 { … }`, `if 식 { … } else { … }`.
- 같은 이벤트의 핸들러가 여럿이면 소스 순서로 하나의 이벤트 안에서 실행한다.
## 의미 (워크벤치 엔진과 같음)
- 값은 IEEE binary64다. 연산 결과나 읽은 값이 유한하지 않으면, 0으로 나누면, `clamp`의 `lo > hi`이면
이벤트 전체가 실패하고 상태는 이벤트 전과 같다.
- 비교와 논리 연산은 0 또는 1을 낸다. `&& ||`는 양쪽을 모두 계산한다. 0이 아니면 참이다.
- `if`의 조건은 한 번 계산한다. 문장은 순서대로 실행한다.
- `frame`의 `dt`는 1/60이고 키 이벤트의 `dt`는 0이다.
- 한도: 상태 32, 입력 32, 보기 64, 핸들러 64, 명령 512, 스택 64, 소스 32 KiB.
- 모듈을 교체하면 같은 이름의 상태 값은 유지하고, 새 상태는 초기값을 쓰고, 없어진 상태는 버린다.
교체에 실패하면 이전 모듈과 상태가 그대로다.
- **선언한 상태는 영속 상태다.** 호스트가 저장소를 주면(`pure-run 앱.pmod --store 경로`) 모든 상태(수·text·커서·
리스트·인덱스)가 저장소에 살고, 앱은 다음 실행에서 그대로 이어진다. 저장·불러오기·직렬화 코드가 없다.
키·text·메시지 이벤트는 바꾼 상태를 한 커밋으로 기록하고(메시지의 문서와 함께), frame의 변경은 다음 이벤트와
함께 기록된다. 레코드가 바뀐 리스트는 필드 이름으로 옮겨진다([Pure 008](/srv/git/pure.git) 상태 영속).
워크벤치와 다른 점은 셋이다. 입력 종류를 `key`로 적고, 도형 대신 `view`로 값을 내보내고, `else`가 있다.
도형과 인터페이스 표현은 Vega가 맡는다. 포인터 입력, 값 입력, 레코드·규칙은 v0a에 없다.
## 모듈
모듈 형식은 Pure가 소유한다([Pure 설계 008](/srv/git/pure.git): `docs/design/008-module-format.md`).
aup는 그 형식의 생산자일 뿐이다. 모듈에는 이름표·초기값·입력 키·보기·핸들러·명령이 들어가고,
Pure는 로드할 때 범위·이름·분기·스택 높이를 모두 정적으로 검증한다. 검증을 통과한 모듈은 실행 중에
스택 넘침이나 잘못된 참조를 만들 수 없다.
## 툴체인
- `aup check FILE`: 진단만 한다. 오류는 `파일:줄:열: 메시지` 한 줄이다(라이브러리 안이면 그 파일).
- `use 이름`은 `이름.aup`을 FILE 옆, 그다음 `AUP_PATH`의 디렉터리 순서로 찾는다.
- `aup build FILE -o OUT.pmod`: 모듈을 만든다. 같은 소스는 바이트 단위로 같은 모듈을 만든다.
- C17, 외부 의존성 없음. 컴파일러는 Pure를 링크하지 않는다. 시험에서는 Pure의 검증기로 결과를 확인한다.
## v0b: 바이트와 영속 문서 (구현)
```text
state documents: bytes by key # 키마다 바이트 값 하나. 런타임이 버전을 붙여 영속화한다
input show = message by key # 키(와 선택적 기준 버전)를 가진 메시지
input edit = message by key {
start: u32 # 필드는 u32, number, bytes, text
remove: u32
insert: bytes
}
on show {
reply documents[key]
}
on edit {
documents[key].replace(edit.start .. edit.start + edit.remove, edit.insert)
require documents[key].bytes <= 8 # 선택: 크기 정책은 require 한 줄
reply edit.insert
}
```
- `on 메시지 { }`는 메시지가 도착할 때 한 번 실행된다. 필드는 `메시지.필드`, 키는 `key`로만 쓴다.
- 필드 타입은 `u32`, `number`(유한한 수), `bytes`, `text`(UTF-8)이고 가변 필드(bytes, text)는 몇 개든 어디에나 둘 수 있다.
`input add = message { title: text, points: number }`. HTTP 호스트(`pure-serve`)는 JSON 본문의 객체를 이 필드들로
옮긴다. 도착한 메시지의 text가 UTF-8이 아니거나 수가 유한하지 않으면 그 이벤트는 실패한다.
- 필드를 그룹으로 묶을 수 있다: `to: { city: text, zip: u32 }`. 그룹 안의 필드는 `메시지.그룹.필드`(`ship.to.city`)로
읽고, JSON 본문에서는 안의 객체(`{"to": {"city": "Seoul", "zip": 4}}`), 쿼리에서는 경로(`?to.zip=4`)다. 그룹은 겹칠 수
있고, 경로는 31자 이하, 한 메시지의 필드는 그룹 안까지 합쳐 64개 이하다. 모듈에서는 `to.city` 같은 이름의 필드로 펼쳐지므로
실행 비용이 없다.
- `이름[key]`는 그 메시지 키의 문서다. 읽으면 bytes, `.bytes`는 크기(없으면 0)다. 없는 문서를 읽으면 `not_found`.
- `replace(a .. b, 값)`는 문서의 `[a, b)`를 바꾼다. 정수가 아니거나 범위 밖이면 이벤트가 `limit`으로 실패한다.
- `require 조건`이 거짓이면 이벤트가 `limit`으로 실패한다. 크기 정책처럼 저장소의 특수 인자였던 것이 문장 하나가 된다.
- 이벤트가 곧 Pure draft 트랜잭션이다. 기준 버전이 현재와 다르면 바로 `conflict`, 핸들러가 성공하면 커밋, 실패하면 아무것도 바뀌지 않는다.
- `get/put`, `store.read/splice`, `request.revision`은 언어에서 사라졌다. CLI·HTTP 호스트가 입력과 응답으로 바꾼다.
검증: Pure의 `pure-edit-module`로 실행한 `examples/editor.aup`·`bounded-editor.aup`가, 기존 `pure-edit`로 실행한
`editor.pure`·`bounded-editor.pure`와 무작위 show·edit·apply 1,280회(성공·충돌·범위·없음·예산·잘못된 키 포함)에서
stdout·stderr·종료 코드가 바이트 단위로 같고, 서로의 저장소를 같게 읽는다(`make check`).
|