app: 개밥 먹기 — 일하는 프로그램 하나와 그 마찰 보고
1단계 최소 IO: File(읽기), Args capability. 권한의 출처는 여전히 런타임 하나이고, IO 오류는 Result[a, String]이다 — 런타임이 사용자 정의 enum을 만들 수 없고, 만들 수 있게 하면 런타임이 프로그램의 타입을 알아야 한다. 2단계 std 확장: fold, filter, push, concat, reverse, is_empty, String.split/trim/starts_with/contains, Int.parse. 3단계 samples/app: 설정 파서 + 리포트 도구, 2모듈 304줄. 검사기를 시험 하려고 쓴 것이 아니라 일을 하려고 쓴 첫 프로그램이다. 산출물은 프로그램이 아니라 docs/friction.md다. 요약: - 되돌리기 비싼 결정은 하나도 후회되지 않았다. capability 전달, effect 명시, 실패를 버릴 수 없음, 소진적 match — 300줄 내내 거추장스럽지 않았고 소진성은 실제로 실수를 잡았다(Value에 경우 하나 추가하니 고칠 자리 넷을 정확히 짚었다). - 불편은 전부 되돌리기 싼 것들이었다. 리스트 n번째 접근이 없어 fold로 우회(40줄), else if가 없어 3~4단 중첩, String.concat이 2항이라 중첩 지옥. 304줄 중 70줄쯤이 이 셋 때문에 존재한다. v0의 질문은 "되돌리기 비싼 결정이 옳은가"였고 답은 그렇다이다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
This commit is contained in:
@@ -60,6 +60,28 @@ m100.cool:15:3: match가 모든 경우를 덮지 않습니다 (빠진 경우: Tr
|
|||||||
m101.cool:21:3: match가 모든 경우를 덮지 않습니다 (빠진 경우: Up.Tri(_))
|
m101.cool:21:3: match가 모든 경우를 덮지 않습니다 (빠진 경우: Up.Tri(_))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 실제로 써본 결과
|
||||||
|
|
||||||
|
`samples/app`은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴
|
||||||
|
프로그램이다 — 설정 파서 + 리포트 도구, 2모듈 304줄.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ coolc run samples/app/main.cool samples/app/example.conf
|
||||||
|
설정
|
||||||
|
name = "coollang" (text)
|
||||||
|
threads = 4 (number)
|
||||||
|
...
|
||||||
|
항목 5개, 문제 2개
|
||||||
|
|
||||||
|
문제
|
||||||
|
10행: 이름이 비어 있습니다
|
||||||
|
11행: = 가 하나여야 합니다: broken = a = b
|
||||||
|
```
|
||||||
|
|
||||||
|
쓰면서 걸린 것들을 [`docs/friction.md`](docs/friction.md)에 남겼다.
|
||||||
|
요약하면: **되돌리기 비싼 결정은 하나도 후회되지 않았고, 불편은 전부
|
||||||
|
되돌리기 싼 것들이었다.**
|
||||||
|
|
||||||
## 빌드
|
## 빌드
|
||||||
|
|
||||||
OCaml 5.x와 dune이 필요하다.
|
OCaml 5.x와 dune이 필요하다.
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@ let usage =
|
|||||||
사용법:
|
사용법:
|
||||||
coolc check <file.cool>... 타입/effect/capability 검사 (import를 따라 모듈 그래프 전체)
|
coolc check <file.cool>... 타입/effect/capability 검사 (import를 따라 모듈 그래프 전체)
|
||||||
coolc iface <file.cool> interface 표면과 해시 출력
|
coolc iface <file.cool> interface 표면과 해시 출력
|
||||||
coolc run <file.cool> typed IR 인터프리터로 실행
|
coolc run <file.cool> [인자...] typed IR 인터프리터로 실행
|
||||||
coolc tokens <file.cool> 토큰 덤프 (렉서 디버깅)
|
coolc tokens <file.cool> 토큰 덤프 (렉서 디버깅)
|
||||||
coolc ast <file.cool> 구문 트리 덤프 (파서 디버깅)
|
coolc ast <file.cool> 구문 트리 덤프 (파서 디버깅)
|
||||||
coolc deps <file.cool> 외부 참조 목록 (모듈의 의존 표면)
|
coolc deps <file.cool> 외부 참조 목록 (모듈의 의존 표면)
|
||||||
@@ -76,9 +76,9 @@ let () =
|
|||||||
match List.tl argv with
|
match List.tl argv with
|
||||||
| "check" :: files -> check_graph files
|
| "check" :: files -> check_graph files
|
||||||
| [ "iface"; file ] -> dump_iface file
|
| [ "iface"; file ] -> dump_iface file
|
||||||
| [ "run"; file ] -> (
|
| "run" :: file :: args -> (
|
||||||
let st = Coollang.Session.create ~root:(Filename.dirname file) () in
|
let st = Coollang.Session.create ~root:(Filename.dirname file) () in
|
||||||
match Coollang.Session.run st file with
|
match Coollang.Session.run ~args st file with
|
||||||
| Ok out ->
|
| Ok out ->
|
||||||
print_string out;
|
print_string out;
|
||||||
0
|
0
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
# 개밥 먹기 보고 — samples/app을 쓰면서 걸린 것들
|
||||||
|
|
||||||
|
2026-08-30. coollang으로 처음 쓴 "일하는 프로그램" 하나(설정 파서 + 리포트
|
||||||
|
도구, 2모듈 304줄)에서 실제로 걸린 마찰을 적는다.
|
||||||
|
|
||||||
|
v0의 샘플 16개는 전부 검사기를 시험하려고 쓴 것이고, 그래서 "언어가 쓸
|
||||||
|
만한가"에 대해서는 아무것도 말해주지 않았다. 이 문서가 v0가 남기는 마지막
|
||||||
|
데이터이자 v1 설계의 첫 입력이다.
|
||||||
|
|
||||||
|
기록 원칙: **불편은 증거와 함께 적고, 해법은 제안까지만 한다.** 여기서
|
||||||
|
바로 고치면 그것은 개밥 먹기가 아니라 기능 추가가 된다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 잘 된 것부터
|
||||||
|
|
||||||
|
**1. 시그니처가 프로그램의 전부를 말한다.**
|
||||||
|
|
||||||
|
```cool
|
||||||
|
pub fn main(c: Console, f: File, a: Args)
|
||||||
|
effects {Console.print, File.read, Args.all}
|
||||||
|
```
|
||||||
|
|
||||||
|
이 한 줄을 읽으면 이 프로그램이 할 수 있는 일이 끝난다. 네트워크를 쓸 수
|
||||||
|
없고, 다른 파일을 쓸 수 없고, 프로세스를 띄울 수 없다 — 문서가 아니라
|
||||||
|
컴파일러가 보장한다. 300줄을 쓰는 내내 이것이 어색하지 않았다. 오히려
|
||||||
|
`f.read`를 쓰려고 `File`을 인자에 추가하는 순간이 "이 함수가 권한을 하나
|
||||||
|
더 갖는다"는 사실을 자각하게 만들었다.
|
||||||
|
|
||||||
|
**2. `?`가 기대대로 동작했다.**
|
||||||
|
|
||||||
|
```cool
|
||||||
|
let path = first_arg(a.all())?
|
||||||
|
let text = f.read(path)?
|
||||||
|
```
|
||||||
|
|
||||||
|
읽기 좋고, 실패 경로가 보이고, 삼켜지지 않는다.
|
||||||
|
|
||||||
|
**3. 소진성이 실전에서 값을 했다.**
|
||||||
|
|
||||||
|
`Value`에 경우 하나(`List`)를 추가해 봤다:
|
||||||
|
|
||||||
|
```
|
||||||
|
config.cool:54:5: 빠진 경우: List(_)
|
||||||
|
config.cool:62:5: 빠진 경우: List(_)
|
||||||
|
config.cool:196:5: 빠진 경우: List(_)
|
||||||
|
config.cool:204:5: 빠진 경우: List(_)
|
||||||
|
```
|
||||||
|
|
||||||
|
고쳐야 할 자리 넷을 전부, 정확히 짚었다. 이것이 없으면 `show_value`만
|
||||||
|
고치고 `get_int`를 잊는다.
|
||||||
|
|
||||||
|
**4. 모듈 경계가 자연스러웠다.** 파싱(`config`)과 출력(`main`)을 나누는 데
|
||||||
|
마찰이 없었고, `Cfg.Config` 같은 한정 이름이 오히려 읽기 좋았다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 걸린 것 — 심각한 순서로
|
||||||
|
|
||||||
|
### F1. 리스트의 n번째를 꺼낼 방법이 없다 (심각)
|
||||||
|
|
||||||
|
인덱싱 연산자를 뺀 결정 자체는 옳다고 본다. 그런데 `std`에 대안이 없다.
|
||||||
|
"첫 원소"를 꺼내려고 이 코드를 네 번 썼다:
|
||||||
|
|
||||||
|
```cool
|
||||||
|
pub fn first_text(xs: List[String]) -> Option[String] {
|
||||||
|
List.fold(xs, None, fn(acc, x) {
|
||||||
|
match acc {
|
||||||
|
Some(prev) => Some(prev),
|
||||||
|
None => Some(x),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`String.split(line, "=")`의 결과에서 앞의 둘을 꺼내는 데는 보조 struct
|
||||||
|
`Pick`까지 만들어야 했다 — 순전히 "몇 번째를 보고 있는가"를 나르려고.
|
||||||
|
**304줄 중 약 40줄이 이 문제 하나 때문에 존재한다.**
|
||||||
|
|
||||||
|
> 제안: `List.first`, `List.nth(xs, i) -> Option[a]`, 그리고 `String.split`
|
||||||
|
> 같은 자리에서 흔한 `List.split_first(xs) -> Option[(a, List[a])]`.
|
||||||
|
> 튜플이 없으므로 마지막 것은 문법 결정을 동반한다.
|
||||||
|
|
||||||
|
### F2. `else if`가 없다 (심각)
|
||||||
|
|
||||||
|
`if`가 식이고 `else`는 식 하나를 받으므로, 문법상 `else if`가 가능해야
|
||||||
|
하는데 파서가 받지 않는다. 그래서 이렇게 된다:
|
||||||
|
|
||||||
|
```cool
|
||||||
|
if raw == "true" {
|
||||||
|
Flag(true)
|
||||||
|
} else {
|
||||||
|
if raw == "false" {
|
||||||
|
Flag(false)
|
||||||
|
} else {
|
||||||
|
match Int.parse(raw) { ... }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
세 갈래 분기가 3단 중첩이 된다. `parse_line`은 4단까지 갔다.
|
||||||
|
**이것은 언어 결정이 아니라 파서의 빈틈으로 보인다.** 확인이 필요하다.
|
||||||
|
|
||||||
|
> 제안: `else` 뒤에 블록 대신 `if` 식을 허용한다. 새 개념이 아니라
|
||||||
|
> 이미 있는 규칙(else는 식을 받는다)의 적용이다.
|
||||||
|
|
||||||
|
### F3. `String.concat`이 2항이라 중첩 지옥이 된다 (심각)
|
||||||
|
|
||||||
|
리포트 한 줄이 이렇게 생겼다:
|
||||||
|
|
||||||
|
```cool
|
||||||
|
c.print(String.concat(" ", String.concat(e.key,
|
||||||
|
String.concat(" = ", String.concat(Cfg.show_value(e.value),
|
||||||
|
String.concat(" (", String.concat(Cfg.type_name(e.value), ")")))))))
|
||||||
|
```
|
||||||
|
|
||||||
|
읽을 수 없다. 이 프로그램에서 가장 나쁜 코드이고, 원인은 명확하다.
|
||||||
|
|
||||||
|
> 제안 두 가지. (a) `String.join(sep, List[String])` — 작고 안전하다.
|
||||||
|
> (b) 문자열 보간 `"${key} = ${value}"` — 훨씬 낫지만 문법과 타입 규칙을
|
||||||
|
> 정해야 하고, "한 개념 한 방식"에서 concat과 겹친다.
|
||||||
|
> 지금 판단으로는 (a)를 먼저 넣고 (b)는 체감 데이터를 더 모은 뒤.
|
||||||
|
|
||||||
|
### F4. struct의 한 필드만 바꿀 방법이 없다 (중간)
|
||||||
|
|
||||||
|
```cool
|
||||||
|
pub fn add_entry(cfg: Config, e: Entry) -> Config {
|
||||||
|
Config {
|
||||||
|
entries: List.push(cfg.entries, e),
|
||||||
|
problems: cfg.problems, // ← 안 바뀌는데 적어야 한다
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
필드가 둘이라 견딜 만하지만 다섯이면 못 쓴다. `take_at`은 세 필드를 매번
|
||||||
|
전부 나열한다.
|
||||||
|
|
||||||
|
> 제안: `Config { ..cfg, entries: x }`. affine 타입에서는 `cfg`가 move되는
|
||||||
|
> 것이므로 소유권 규칙과 충돌하지 않는다. 오히려 명시적이다.
|
||||||
|
|
||||||
|
### F5. fold에 인덱스가 없다 (중간)
|
||||||
|
|
||||||
|
줄 번호를 세려고 struct를 하나 더 만들었다:
|
||||||
|
|
||||||
|
```cool
|
||||||
|
pub copyable struct Numbered {
|
||||||
|
no: Int,
|
||||||
|
cfg: Config,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
"인덱스가 필요한 fold"는 드문 요구가 아니다.
|
||||||
|
|
||||||
|
> 제안: `List.fold_indexed`. 또는 `List.enumerate`가 더 조합적이지만
|
||||||
|
> 튜플이 없어서 지금은 불가능하다. F1의 튜플 문제와 같은 뿌리다.
|
||||||
|
|
||||||
|
### F6. Option/Result에 조작 함수가 하나도 없다 (중간)
|
||||||
|
|
||||||
|
`map`, `unwrap_or`, `or_else`가 없어서 전부 `match`로 풀었다. `match`가
|
||||||
|
나쁜 것은 아니지만, 세 줄이면 될 것이 여섯 줄이 된다.
|
||||||
|
|
||||||
|
> 제안: `Option.map/unwrap_or`, `Result.map/map_err/unwrap_or`.
|
||||||
|
> 타입 이름이 함수의 이름공간이라는 규칙이 이미 있으므로 문법 결정은 없다.
|
||||||
|
|
||||||
|
### F7. std를 늘릴 때마다 인터프리터를 고쳐야 한다 (구조)
|
||||||
|
|
||||||
|
`std/list.cool`에 `filter`를 적으면 `lib/interp.ml`에도 구현을 넣어야
|
||||||
|
한다. 두 곳이 어긋나면 검사는 통과하고 실행이 죽는다.
|
||||||
|
|
||||||
|
이것은 v0의 구조적 한계이고 v1에서 사라진다(std를 coollang으로 구현).
|
||||||
|
다만 v0 동안은 **std 시그니처와 런타임 구현이 일치하는지 검사하는 테스트**가
|
||||||
|
있어야 한다. 지금은 없다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 결론
|
||||||
|
|
||||||
|
v1로 넘길 때 **F1, F2, F3이 먼저다.** 셋 다 "언어가 틀렸다"가 아니라
|
||||||
|
"없어서 우회했다"이고, 우회 비용이 코드에 그대로 보인다 — 304줄 중
|
||||||
|
70줄쯤이 이 셋 때문에 존재한다.
|
||||||
|
|
||||||
|
반대로 **되돌리기 비싼 결정들은 하나도 후회되지 않았다.** capability를
|
||||||
|
인자로 나르는 것, effect를 시그니처에 적는 것, 실패를 버릴 수 없는 것,
|
||||||
|
소진적 match — 300줄을 쓰는 동안 이 넷이 거추장스러웠던 순간이 없었고,
|
||||||
|
소진성은 오히려 실수를 잡아줬다.
|
||||||
|
|
||||||
|
v0의 질문은 "되돌리기 비싼 결정이 옳은가"였다. 답은 **그렇다**이고,
|
||||||
|
남은 불편은 전부 되돌리기 싼 것들이다.
|
||||||
@@ -366,6 +366,14 @@ prelude는 없다. std도 명시적으로 가져온다 — 암묵적으로 끌
|
|||||||
것이고, 이것이 std를 "부채 상환"이 아니라 "검증"으로 본 이유다.
|
것이고, 이것이 std를 "부채 상환"이 아니라 "검증"으로 본 이유다.
|
||||||
결과를 버릴 방법이 언어에 없다는 성질도 여기서 처음 확인됐다.
|
결과를 버릴 방법이 언어에 없다는 성질도 여기서 처음 확인됐다.
|
||||||
|
|
||||||
|
■ 개밥 먹기 (2026-08)
|
||||||
|
samples/app — 설정 파서 + 리포트 도구, 2모듈 304줄. 검사기를 시험하려고
|
||||||
|
쓴 것이 아니라 일을 하려고 쓴 첫 프로그램이다.
|
||||||
|
결과: 되돌리기 비싼 결정은 하나도 후회되지 않았고(capability 전달, effect
|
||||||
|
명시, 실패를 버릴 수 없음, 소진적 match), 불편은 전부 되돌리기 싼 것들이었다
|
||||||
|
— 리스트 n번째 접근 없음, else if 없음, String.concat 2항.
|
||||||
|
전문과 증거는 docs/friction.md. 이것이 v1 설계의 첫 입력이다.
|
||||||
|
|
||||||
■ 측정 (2026-08, v0 fast path)
|
■ 측정 (2026-08, v0 fast path)
|
||||||
100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현.
|
100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현.
|
||||||
cold 전체 검사 245ms
|
cold 전체 검사 245ms
|
||||||
|
|||||||
+87
-1
@@ -81,8 +81,46 @@ let bind (env : env) n v : env =
|
|||||||
(* 런타임이 제공하는 것 *)
|
(* 런타임이 제공하는 것 *)
|
||||||
(* ------------------------------------------------------------------ *)
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
(* 문자열 도우미. 언어에 인덱싱 연산자가 없으므로 이 일은 런타임 몫이다. *)
|
||||||
|
let find_sub hay needle =
|
||||||
|
let n = String.length needle and h = String.length hay in
|
||||||
|
let rec go i =
|
||||||
|
if i + n > h then None
|
||||||
|
else if String.sub hay i n = needle then Some i
|
||||||
|
else go (i + 1)
|
||||||
|
in
|
||||||
|
if n = 0 then Some 0 else go 0
|
||||||
|
|
||||||
|
let split_on s sep =
|
||||||
|
if sep = "" then [ s ]
|
||||||
|
else
|
||||||
|
let n = String.length sep in
|
||||||
|
let rec go s acc =
|
||||||
|
match find_sub s sep with
|
||||||
|
| None -> List.rev (s :: acc)
|
||||||
|
| Some i ->
|
||||||
|
go
|
||||||
|
(String.sub s (i + n) (String.length s - i - n))
|
||||||
|
(String.sub s 0 i :: acc)
|
||||||
|
in
|
||||||
|
go s []
|
||||||
|
|
||||||
let out = Buffer.create 1024
|
let out = Buffer.create 1024
|
||||||
|
|
||||||
|
(* 프로그램 인자. 런타임이 들고 있다가 Args capability를 통해서만 준다 —
|
||||||
|
전역 변수로 아무 데서나 읽을 수 있으면 그것이 ambient authority다. *)
|
||||||
|
let argv : string list ref = ref []
|
||||||
|
|
||||||
|
let read_whole path =
|
||||||
|
let ic = open_in_bin path in
|
||||||
|
let n = in_channel_length ic in
|
||||||
|
let s = really_input_string ic n in
|
||||||
|
close_in ic;
|
||||||
|
s
|
||||||
|
|
||||||
|
(* IO 오류는 문자열로 돌려준다. 런타임이 사용자 정의 enum을 만들 수는 없고,
|
||||||
|
만들 수 있게 하면 런타임이 프로그램의 타입을 알아야 한다. v0의 선은
|
||||||
|
여기다 — Result[a, String]. *)
|
||||||
let root_capability name : value option =
|
let root_capability name : value option =
|
||||||
match name with
|
match name with
|
||||||
| "Console" ->
|
| "Console" ->
|
||||||
@@ -96,6 +134,26 @@ let root_capability name : value option =
|
|||||||
Buffer.add_char out '\n';
|
Buffer.add_char out '\n';
|
||||||
VUnit );
|
VUnit );
|
||||||
] ))
|
] ))
|
||||||
|
| "File" ->
|
||||||
|
Some
|
||||||
|
(VCap
|
||||||
|
( "File",
|
||||||
|
[
|
||||||
|
( "read",
|
||||||
|
fun args ->
|
||||||
|
match args with
|
||||||
|
| [ VStr path ] -> (
|
||||||
|
try VEnum ("Result", "Ok", [ VStr (read_whole path) ])
|
||||||
|
with Sys_error m ->
|
||||||
|
VEnum ("Result", "Err", [ VStr m ]))
|
||||||
|
| _ -> VEnum ("Result", "Err", [ VStr "read: 경로가 필요합니다" ])
|
||||||
|
);
|
||||||
|
] ))
|
||||||
|
| "Args" ->
|
||||||
|
Some
|
||||||
|
(VCap
|
||||||
|
( "Args",
|
||||||
|
[ ("all", fun _ -> VList (List.map (fun s -> VStr s) !argv)) ] ))
|
||||||
| "TaskScope" ->
|
| "TaskScope" ->
|
||||||
(* 루트 스코프. 구조적 동시성의 뿌리도 런타임이 준다 — 프로그램이
|
(* 루트 스코프. 구조적 동시성의 뿌리도 런타임이 준다 — 프로그램이
|
||||||
스스로 만들 수 있으면 부모 없는 작업이 생긴다. *)
|
스스로 만들 수 있으면 부모 없는 작업이 생긴다. *)
|
||||||
@@ -106,10 +164,24 @@ let builtin pos name (args : value list) : value =
|
|||||||
match (name, args) with
|
match (name, args) with
|
||||||
| "string.concat", [ VStr a; VStr b ] -> VStr (a ^ b)
|
| "string.concat", [ VStr a; VStr b ] -> VStr (a ^ b)
|
||||||
| "string.len", [ VStr a ] -> VInt (String.length a)
|
| "string.len", [ VStr a ] -> VInt (String.length a)
|
||||||
|
| "string.is_empty", [ VStr a ] -> VBool (a = "")
|
||||||
|
| "string.split", [ VStr s; VStr sep ] -> VList (List.map (fun x -> VStr x) (split_on s sep))
|
||||||
|
| "string.trim", [ VStr s ] -> VStr (String.trim s)
|
||||||
|
| "string.starts_with", [ VStr s; VStr p ] ->
|
||||||
|
VBool (String.length s >= String.length p && String.sub s 0 (String.length p) = p)
|
||||||
|
| "string.contains", [ VStr s; VStr n ] -> VBool (find_sub s n <> None)
|
||||||
| "int.show", [ VInt n ] -> VStr (string_of_int n)
|
| "int.show", [ VInt n ] -> VStr (string_of_int n)
|
||||||
| "int.abs", [ VInt n ] -> VInt (abs n)
|
| "int.abs", [ VInt n ] -> VInt (abs n)
|
||||||
|
| "int.parse", [ VStr s ] -> (
|
||||||
|
match int_of_string_opt (String.trim s) with
|
||||||
|
| Some n -> VEnum ("Result", "Ok", [ VInt n ])
|
||||||
|
| None -> VEnum ("Result", "Err", [ VStr (s ^ "은(는) 정수가 아닙니다") ]))
|
||||||
| "bool.show", [ VBool b ] -> VStr (if b then "true" else "false")
|
| "bool.show", [ VBool b ] -> VStr (if b then "true" else "false")
|
||||||
| "list.len", [ VList xs ] -> VInt (List.length xs)
|
| "list.len", [ VList xs ] -> VInt (List.length xs)
|
||||||
|
| "list.is_empty", [ VList xs ] -> VBool (xs = [])
|
||||||
|
| "list.push", [ VList xs; x ] -> VList (xs @ [ x ])
|
||||||
|
| "list.concat", [ VList xs; VList ys ] -> VList (xs @ ys)
|
||||||
|
| "list.reverse", [ VList xs ] -> VList (List.rev xs)
|
||||||
| _ ->
|
| _ ->
|
||||||
fail pos (Printf.sprintf "%s은(는) 런타임이 제공하지 않습니다 (표준 라이브러리가 아직 없습니다)" name)
|
fail pos (Printf.sprintf "%s은(는) 런타임이 제공하지 않습니다 (표준 라이브러리가 아직 없습니다)" name)
|
||||||
|
|
||||||
@@ -228,6 +300,19 @@ and apply st pos f args =
|
|||||||
match args with
|
match args with
|
||||||
| [ VList xs; f ] -> VList (List.map (fun x -> apply st pos f [ x ]) xs)
|
| [ VList xs; f ] -> VList (List.map (fun x -> apply st pos f [ x ]) xs)
|
||||||
| _ -> fail pos "list.map은 리스트와 함수를 받습니다")
|
| _ -> fail pos "list.map은 리스트와 함수를 받습니다")
|
||||||
|
| VBuiltin "list.filter" -> (
|
||||||
|
match args with
|
||||||
|
| [ VList xs; f ] ->
|
||||||
|
VList
|
||||||
|
(List.filter
|
||||||
|
(fun x -> match apply st pos f [ x ] with VBool b -> b | _ -> false)
|
||||||
|
xs)
|
||||||
|
| _ -> fail pos "list.filter는 리스트와 함수를 받습니다")
|
||||||
|
| VBuiltin "list.fold" -> (
|
||||||
|
match args with
|
||||||
|
| [ VList xs; init; f ] ->
|
||||||
|
List.fold_left (fun acc x -> apply st pos f [ acc; x ]) init xs
|
||||||
|
| _ -> fail pos "list.fold는 리스트, 초기값, 함수를 받습니다")
|
||||||
| VBuiltin n -> builtin pos n args
|
| VBuiltin n -> builtin pos n args
|
||||||
| VNative f -> f args
|
| VNative f -> f args
|
||||||
| other -> fail pos (Printf.sprintf "%s은(는) 부를 수 없습니다" (show other))
|
| other -> fail pos (Printf.sprintf "%s은(는) 부를 수 없습니다" (show other))
|
||||||
@@ -313,10 +398,11 @@ and eval_binary st env op a b pos =
|
|||||||
|
|
||||||
(* main이 선언한 capability만 런타임이 넘긴다. 선언하지 않은 권한은
|
(* main이 선언한 capability만 런타임이 넘긴다. 선언하지 않은 권한은
|
||||||
프로그램 안에 존재하지 않는다. *)
|
프로그램 안에 존재하지 않는다. *)
|
||||||
let run (prog : Ir.program) (entry : string)
|
let run ?(args = []) (prog : Ir.program) (entry : string)
|
||||||
(main_params : (string * string) list) : (string, Token.pos * string) result
|
(main_params : (string * string) list) : (string, Token.pos * string) result
|
||||||
=
|
=
|
||||||
Buffer.clear out;
|
Buffer.clear out;
|
||||||
|
argv := args;
|
||||||
let st = { prog } in
|
let st = { prog } in
|
||||||
match Hashtbl.find_opt prog.Ir.fns (entry ^ "#main") with
|
match Hashtbl.find_opt prog.Ir.fns (entry ^ "#main") with
|
||||||
| None -> Error (Token.{ line = 0; col = 0 }, "main 함수가 없습니다")
|
| None -> Error (Token.{ line = 0; col = 0 }, "main 함수가 없습니다")
|
||||||
|
|||||||
+2
-2
@@ -258,7 +258,7 @@ let main_params (m : Ast.modul) =
|
|||||||
| _ -> [])
|
| _ -> [])
|
||||||
m.items
|
m.items
|
||||||
|
|
||||||
let run st path : (string, error) result =
|
let run ?(args = []) st path : (string, error) result =
|
||||||
load st path;
|
load st path;
|
||||||
let errs = errors st in
|
let errs = errors st in
|
||||||
if errs <> [] then Error (List.hd errs)
|
if errs <> [] then Error (List.hd errs)
|
||||||
@@ -275,6 +275,6 @@ let run st path : (string, error) result =
|
|||||||
st.modules []
|
st.modules []
|
||||||
in
|
in
|
||||||
let prog = Ir.of_program mods in
|
let prog = Ir.of_program mods in
|
||||||
match Interp.run prog path (main_params e.ast) with
|
match Interp.run ~args prog path (main_params e.ast) with
|
||||||
| Ok out -> Ok out
|
| Ok out -> Ok out
|
||||||
| Error (pos, msg) -> Error (err_of path pos msg))
|
| Error (pos, msg) -> Error (err_of path pos msg))
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
// 설정 파일 파서.
|
||||||
|
//
|
||||||
|
// 이 파일은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴 것이다.
|
||||||
|
// 그것이 이 파일의 목적이다 — 언어가 실제로 쓸 만한지는 이런 코드에서만
|
||||||
|
// 드러난다.
|
||||||
|
|
||||||
|
import "cool.dev/std/list" as List
|
||||||
|
import "cool.dev/std/string" as String
|
||||||
|
import "cool.dev/std/int" as Int
|
||||||
|
|
||||||
|
// 설정 값. 타입이 셋뿐이므로 열거형이 맞다.
|
||||||
|
pub enum Value {
|
||||||
|
Text(String),
|
||||||
|
Number(Int),
|
||||||
|
Flag(Bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub copyable struct Entry {
|
||||||
|
line: Int,
|
||||||
|
key: String,
|
||||||
|
value: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 오류에 줄 번호가 있어야 사람이 고칠 수 있다.
|
||||||
|
pub copyable struct Problem {
|
||||||
|
line: Int,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub copyable struct Config {
|
||||||
|
entries: List[Entry],
|
||||||
|
problems: List[Problem],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 값 하나를 읽는다. 따옴표도 타입 표기도 없다 — 모양으로 정한다.
|
||||||
|
// true/false는 Flag, 정수로 읽히면 Number, 나머지는 Text.
|
||||||
|
pub fn parse_value(raw: String) -> Value {
|
||||||
|
if raw == "true" {
|
||||||
|
Flag(true)
|
||||||
|
} else {
|
||||||
|
if raw == "false" {
|
||||||
|
Flag(false)
|
||||||
|
} else {
|
||||||
|
match Int.parse(raw) {
|
||||||
|
Ok(n) => Number(n),
|
||||||
|
Err(_) => Text(raw),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_value(v: Value) -> String {
|
||||||
|
match v {
|
||||||
|
Text(s) => String.concat("\"", String.concat(s, "\"")),
|
||||||
|
Number(n) => Int.show(n),
|
||||||
|
Flag(b) => if b { "true" } else { "false" },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn type_name(v: Value) -> String {
|
||||||
|
match v {
|
||||||
|
Text(_) => "text",
|
||||||
|
Number(_) => "number",
|
||||||
|
Flag(_) => "flag",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 한 줄을 읽는다. 빈 줄과 주석은 값이 없고, 그것은 오류가 아니다.
|
||||||
|
// 그래서 Result가 아니라 Option[Result[...]]가 필요해 보이지만, 그러면
|
||||||
|
// 부르는 쪽이 두 겹을 벗겨야 한다. Config 하나에 둘 다 모으는 편이 낫다.
|
||||||
|
pub fn parse_line(cfg: Config, no: Int, raw: String) -> Config {
|
||||||
|
let line = String.trim(raw)
|
||||||
|
if String.is_empty(line) {
|
||||||
|
cfg
|
||||||
|
} else {
|
||||||
|
if String.starts_with(line, "#") {
|
||||||
|
cfg
|
||||||
|
} else {
|
||||||
|
let parts = String.split(line, "=")
|
||||||
|
if List.len(parts) == 2 {
|
||||||
|
let key = String.trim(head_or(parts, ""))
|
||||||
|
let val = String.trim(second_or(parts, ""))
|
||||||
|
if String.is_empty(key) {
|
||||||
|
add_problem(cfg, no, "이름이 비어 있습니다")
|
||||||
|
} else {
|
||||||
|
add_entry(cfg, Entry {
|
||||||
|
line: no,
|
||||||
|
key: key,
|
||||||
|
value: parse_value(val),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
add_problem(cfg, no, String.concat("= 가 하나여야 합니다: ", line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_entry(cfg: Config, e: Entry) -> Config {
|
||||||
|
Config {
|
||||||
|
entries: List.push(cfg.entries, e),
|
||||||
|
problems: cfg.problems,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_problem(cfg: Config, no: Int, msg: String) -> Config {
|
||||||
|
Config {
|
||||||
|
entries: cfg.entries,
|
||||||
|
problems: List.push(cfg.problems, Problem { line: no, message: msg }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 인덱싱 연산자가 없으므로 앞의 둘을 꺼내는 일은 이름 있는 함수가 한다.
|
||||||
|
pub fn head_or(xs: List[String], fallback: String) -> String {
|
||||||
|
List.fold(xs, Pick { taken: false, at: 0, found: fallback }, fn(p, x) {
|
||||||
|
take_at(p, x, 0)
|
||||||
|
}).found
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn second_or(xs: List[String], fallback: String) -> String {
|
||||||
|
List.fold(xs, Pick { taken: false, at: 0, found: fallback }, fn(p, x) {
|
||||||
|
take_at(p, x, 1)
|
||||||
|
}).found
|
||||||
|
}
|
||||||
|
|
||||||
|
// fold로 n번째를 고른다. 셋 다 필요하다 — 몇 번째를 보고 있는지(at),
|
||||||
|
// 이미 골랐는지(taken), 무엇을 골랐는지(found).
|
||||||
|
pub copyable struct Pick {
|
||||||
|
taken: Bool,
|
||||||
|
at: Int,
|
||||||
|
found: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_at(p: Pick, x: String, want: Int) -> Pick {
|
||||||
|
if p.taken {
|
||||||
|
Pick { taken: true, at: p.at + 1, found: p.found }
|
||||||
|
} else {
|
||||||
|
if p.at == want {
|
||||||
|
Pick { taken: true, at: p.at + 1, found: x }
|
||||||
|
} else {
|
||||||
|
Pick { taken: false, at: p.at + 1, found: p.found }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(text: String) -> Config {
|
||||||
|
let lines = String.split(text, "\n")
|
||||||
|
let start = Numbered {
|
||||||
|
no: 1,
|
||||||
|
cfg: Config { entries: [], problems: [] },
|
||||||
|
}
|
||||||
|
List.fold(lines, start, fn(acc, line) {
|
||||||
|
Numbered {
|
||||||
|
no: acc.no + 1,
|
||||||
|
cfg: parse_line(acc.cfg, acc.no, line),
|
||||||
|
}
|
||||||
|
}).cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// 줄 번호를 같이 나르기 위한 것. fold에 인덱스가 없으니 직접 센다.
|
||||||
|
pub copyable struct Numbered {
|
||||||
|
no: Int,
|
||||||
|
cfg: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이름으로 찾는다. 없으면 Err — 못 찾은 것은 오류지 빈 값이 아니다.
|
||||||
|
pub fn lookup(cfg: Config, key: String) -> Result[Value, String] {
|
||||||
|
let hit = List.filter(cfg.entries, fn(e) { e.key == key })
|
||||||
|
match first_entry(hit) {
|
||||||
|
Some(e) => Ok(e.value),
|
||||||
|
None => Err(String.concat("설정에 없습니다: ", key)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn first_entry(xs: List[Entry]) -> Option[Entry] {
|
||||||
|
List.fold(xs, None, fn(acc, e) {
|
||||||
|
match acc {
|
||||||
|
Some(prev) => Some(prev),
|
||||||
|
None => Some(e),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 리스트의 첫 원소. fold로 쓴다 — 언어에 인덱싱이 없다.
|
||||||
|
pub fn first_text(xs: List[String]) -> Option[String] {
|
||||||
|
List.fold(xs, None, fn(acc, x) {
|
||||||
|
match acc {
|
||||||
|
Some(prev) => Some(prev),
|
||||||
|
None => Some(x),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_int(cfg: Config, key: String) -> Result[Int, String] {
|
||||||
|
match lookup(cfg, key)? {
|
||||||
|
Number(n) => Ok(n),
|
||||||
|
Text(_) => Err(String.concat(key, "은(는) 정수가 아닙니다")),
|
||||||
|
Flag(_) => Err(String.concat(key, "은(는) 정수가 아닙니다")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_flag(cfg: Config, key: String) -> Result[Bool, String] {
|
||||||
|
match lookup(cfg, key)? {
|
||||||
|
Flag(b) => Ok(b),
|
||||||
|
Text(_) => Err(String.concat(key, "은(는) 참거짓이 아닙니다")),
|
||||||
|
Number(_) => Err(String.concat(key, "은(는) 참거짓이 아닙니다")),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 예제 설정
|
||||||
|
|
||||||
|
name = coollang
|
||||||
|
threads = 4
|
||||||
|
port = 8080
|
||||||
|
verbose = true
|
||||||
|
greeting = hello world
|
||||||
|
|
||||||
|
# 아래 두 줄은 일부러 틀렸다
|
||||||
|
= 42
|
||||||
|
broken = a = b
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// 설정 파일 리포트 도구.
|
||||||
|
//
|
||||||
|
// 파일을 읽고, 파싱하고, 문제를 보고하고, 요약을 낸다.
|
||||||
|
// 권한은 셋뿐이다 — 읽기(File), 인자(Args), 출력(Console). 그 밖의 일은
|
||||||
|
// 이 프로그램이 할 수 없다. 시그니처에 적힌 것이 이 프로그램의 전부다.
|
||||||
|
|
||||||
|
import "config" as Cfg
|
||||||
|
import "cool.dev/std/list" as List
|
||||||
|
import "cool.dev/std/string" as String
|
||||||
|
import "cool.dev/std/int" as Int
|
||||||
|
|
||||||
|
pub capability Console {
|
||||||
|
fn print(s: String) effects {Console.print}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub capability File {
|
||||||
|
fn read(path: String) effects {File.read} -> Result[String, String]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub capability Args {
|
||||||
|
fn all() effects {Args.all} -> List[String]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn report(c: Console, cfg: Cfg.Config) effects {Console.print} {
|
||||||
|
c.print("설정")
|
||||||
|
List.each(cfg.entries, fn(e) {
|
||||||
|
c.print(String.concat(" ", String.concat(e.key,
|
||||||
|
String.concat(" = ", String.concat(Cfg.show_value(e.value),
|
||||||
|
String.concat(" (", String.concat(Cfg.type_name(e.value), ")")))))))
|
||||||
|
})
|
||||||
|
|
||||||
|
c.print("")
|
||||||
|
c.print(String.concat("항목 ", String.concat(Int.show(List.len(cfg.entries)),
|
||||||
|
String.concat("개, 문제 ", String.concat(Int.show(List.len(cfg.problems)), "개")))))
|
||||||
|
|
||||||
|
if List.is_empty(cfg.problems) {
|
||||||
|
c.print("문제 없음")
|
||||||
|
} else {
|
||||||
|
c.print("")
|
||||||
|
c.print("문제")
|
||||||
|
List.each(cfg.problems, fn(p) {
|
||||||
|
c.print(String.concat(" ", String.concat(Int.show(p.line),
|
||||||
|
String.concat("행: ", p.message))))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 타입이 있는 조회. 없거나 타입이 다르면 Err이고, 둘 다 사람이 읽을 수 있다.
|
||||||
|
pub fn check_known(c: Console, cfg: Cfg.Config) effects {Console.print} {
|
||||||
|
c.print("")
|
||||||
|
c.print("알려진 설정")
|
||||||
|
show_int(c, cfg, "threads")
|
||||||
|
show_int(c, cfg, "port")
|
||||||
|
show_flag(c, cfg, "verbose")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_int(c: Console, cfg: Cfg.Config, key: String) effects {Console.print} {
|
||||||
|
match Cfg.get_int(cfg, key) {
|
||||||
|
Ok(n) => c.print(String.concat(" ", String.concat(key,
|
||||||
|
String.concat(" = ", Int.show(n))))),
|
||||||
|
Err(m) => c.print(String.concat(" ", m)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_flag(c: Console, cfg: Cfg.Config, key: String) effects {Console.print} {
|
||||||
|
match Cfg.get_flag(cfg, key) {
|
||||||
|
Ok(b) => c.print(String.concat(" ", String.concat(key,
|
||||||
|
String.concat(" = ", if b { "true" } else { "false" })))),
|
||||||
|
Err(m) => c.print(String.concat(" ", m)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn first_arg(xs: List[String]) -> Result[String, String] {
|
||||||
|
match Cfg.first_text(xs) {
|
||||||
|
Some(p) => Ok(p),
|
||||||
|
None => Err("설정 파일 경로가 필요합니다"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(c: Console, f: File, a: Args)
|
||||||
|
effects {Console.print, File.read, Args.all} {
|
||||||
|
match run(c, f, a) {
|
||||||
|
Ok(_) => unit,
|
||||||
|
Err(m) => c.print(String.concat("오류: ", m)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(c: Console, f: File, a: Args)
|
||||||
|
effects {Console.print, File.read, Args.all} -> Result[Unit, String] {
|
||||||
|
let path = first_arg(a.all())?
|
||||||
|
let text = f.read(path)?
|
||||||
|
let cfg = Cfg.parse(text)
|
||||||
|
report(c, cfg)
|
||||||
|
check_known(c, cfg)
|
||||||
|
Ok(unit)
|
||||||
|
}
|
||||||
@@ -2,3 +2,6 @@
|
|||||||
|
|
||||||
pub fn show(n: Int) -> String
|
pub fn show(n: Int) -> String
|
||||||
pub fn abs(n: Int) -> Int
|
pub fn abs(n: Int) -> Int
|
||||||
|
|
||||||
|
// 실패할 수 있으므로 Result다. 예외도, 0을 돌려주는 관례도 없다.
|
||||||
|
pub fn parse(s: String) -> Result[Int, String]
|
||||||
|
|||||||
+21
-2
@@ -2,14 +2,22 @@
|
|||||||
//
|
//
|
||||||
// 본문이 없다. 런타임이 구현하고, 이 파일은 그 계약을 말한다.
|
// 본문이 없다. 런타임이 구현하고, 이 파일은 그 계약을 말한다.
|
||||||
// 그래서 이 파일은 구현이 아니라 시험대다 — effect 다형성이 실제로 쓸 만한지가
|
// 그래서 이 파일은 구현이 아니라 시험대다 — effect 다형성이 실제로 쓸 만한지가
|
||||||
// 여기서 결정된다. each와 map이 effect 변수 하나로 표현되지 않으면 규칙이
|
// each, map, fold, filter에서 결정된다. 규칙이 틀렸으면 여기서 드러난다.
|
||||||
// 틀린 것이고, 그건 v1로 미룰 수 없는 발견이다.
|
|
||||||
//
|
//
|
||||||
// e는 파라미터의 effect 슬롯에 홀로 나타난다 (결정 위치). 호출 지점에서
|
// e는 파라미터의 effect 슬롯에 홀로 나타난다 (결정 위치). 호출 지점에서
|
||||||
// 인자의 시그니처를 읽어 묶인다 — 추론이 아니라 읽기다.
|
// 인자의 시그니처를 읽어 묶인다 — 추론이 아니라 읽기다.
|
||||||
|
|
||||||
pub fn len[a](xs: List[a]) -> Int
|
pub fn len[a](xs: List[a]) -> Int
|
||||||
|
|
||||||
|
pub fn is_empty[a](xs: List[a]) -> Bool
|
||||||
|
|
||||||
|
// 뒤에 하나 붙인 새 리스트. 제자리 수정이 아니다.
|
||||||
|
pub fn push[a](xs: List[a], x: a) -> List[a]
|
||||||
|
|
||||||
|
pub fn concat[a](xs: List[a], ys: List[a]) -> List[a]
|
||||||
|
|
||||||
|
pub fn reverse[a](xs: List[a]) -> List[a]
|
||||||
|
|
||||||
pub fn each[a, e: effects](
|
pub fn each[a, e: effects](
|
||||||
xs: List[a],
|
xs: List[a],
|
||||||
f: fn(a) effects e,
|
f: fn(a) effects e,
|
||||||
@@ -19,3 +27,14 @@ pub fn map[a, b, e: effects](
|
|||||||
xs: List[a],
|
xs: List[a],
|
||||||
f: fn(a) effects e -> b,
|
f: fn(a) effects e -> b,
|
||||||
) effects e -> List[b]
|
) effects e -> List[b]
|
||||||
|
|
||||||
|
pub fn filter[a, e: effects](
|
||||||
|
xs: List[a],
|
||||||
|
keep: fn(a) effects e -> Bool,
|
||||||
|
) effects e -> List[a]
|
||||||
|
|
||||||
|
pub fn fold[a, acc, e: effects](
|
||||||
|
xs: List[a],
|
||||||
|
init: acc,
|
||||||
|
f: fn(acc, a) effects e -> acc,
|
||||||
|
) effects e -> acc
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
// 표준 라이브러리: 문자열.
|
// 표준 라이브러리: 문자열.
|
||||||
|
//
|
||||||
|
// 인덱싱 연산자가 언어에 없다. 문자열을 자르는 일은 이름 있는 함수가 한다 —
|
||||||
|
// 한 개념 한 방식.
|
||||||
|
|
||||||
pub fn len(s: String) -> Int
|
pub fn len(s: String) -> Int
|
||||||
|
pub fn is_empty(s: String) -> Bool
|
||||||
pub fn concat(a: String, b: String) -> String
|
pub fn concat(a: String, b: String) -> String
|
||||||
|
|
||||||
|
// 구분자로 자른다. 구분자가 없으면 원본 하나짜리 리스트.
|
||||||
|
pub fn split(s: String, sep: String) -> List[String]
|
||||||
|
|
||||||
|
pub fn trim(s: String) -> String
|
||||||
|
pub fn starts_with(s: String, prefix: String) -> Bool
|
||||||
|
pub fn contains(s: String, needle: String) -> Bool
|
||||||
|
|||||||
@@ -3,4 +3,7 @@
|
|||||||
(libraries coollang)
|
(libraries coollang)
|
||||||
(deps
|
(deps
|
||||||
(glob_files %{workspace_root}/samples/*.cool)
|
(glob_files %{workspace_root}/samples/*.cool)
|
||||||
(glob_files %{workspace_root}/std/*.cool)))
|
(glob_files %{workspace_root}/std/*.cool)
|
||||||
|
(glob_files %{workspace_root}/samples/app/*)
|
||||||
|
(glob_files %{workspace_root}/samples/modules/*.cool)
|
||||||
|
(glob_files %{workspace_root}/samples/run/*.cool)))
|
||||||
|
|||||||
@@ -1232,3 +1232,42 @@ let () =
|
|||||||
in
|
in
|
||||||
List.exists (fun m -> has_sub m "가져왔지만 쓰지 않습니다") ms
|
List.exists (fun m -> has_sub m "가져왔지만 쓰지 않습니다") ms
|
||||||
&& List.exists (fun m -> has_sub m "String이(가) 필요한데 Int") ms)
|
&& List.exists (fun m -> has_sub m "String이(가) 필요한데 Int") ms)
|
||||||
|
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
(* 실제 프로그램 *)
|
||||||
|
(* *)
|
||||||
|
(* samples/app은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴 것이다. *)
|
||||||
|
(* 언어가 쓸 만한지는 이런 코드에서만 드러난다. *)
|
||||||
|
(* ------------------------------------------------------------------ *)
|
||||||
|
|
||||||
|
let () =
|
||||||
|
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
|
||||||
|
match Session.run ~args:[ "../samples/app/example.conf" ] st
|
||||||
|
"../samples/app/main.cool"
|
||||||
|
with
|
||||||
|
| Error e ->
|
||||||
|
Printf.printf " (실행 오류: %s)\n" (Session.string_of_error e);
|
||||||
|
check "app: 설정 리포트가 돈다" false
|
||||||
|
| Ok out ->
|
||||||
|
check "app: 항목과 문제를 센다"
|
||||||
|
(has_sub out "항목 5개, 문제 2개");
|
||||||
|
check "app: 값의 타입을 모양으로 정한다"
|
||||||
|
(has_sub out "threads = 4 (number)"
|
||||||
|
&& has_sub out "verbose = true (flag)"
|
||||||
|
&& has_sub out "name = \"coollang\" (text)");
|
||||||
|
check "app: 문제에 줄 번호가 붙는다"
|
||||||
|
(has_sub out "10행: 이름이 비어 있습니다"
|
||||||
|
&& has_sub out "11행: = 가 하나여야 합니다");
|
||||||
|
check "app: 타입 있는 조회" (has_sub out "port = 8080")
|
||||||
|
|
||||||
|
let () =
|
||||||
|
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
|
||||||
|
match Session.run st "../samples/app/main.cool" with
|
||||||
|
| Ok out -> check "app: 인자가 없으면 말해준다" (has_sub out "경로가 필요합니다")
|
||||||
|
| Error _ -> check "app: 인자가 없으면 말해준다" false
|
||||||
|
|
||||||
|
let () =
|
||||||
|
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
|
||||||
|
match Session.run ~args:[ "/없는/파일.conf" ] st "../samples/app/main.cool" with
|
||||||
|
| Ok out -> check "app: 없는 파일을 Err로 돌려준다" (has_sub out "오류: ")
|
||||||
|
| Error _ -> check "app: 없는 파일을 Err로 돌려준다" false
|
||||||
|
|||||||
Reference in New Issue
Block a user