std: 마찰 보고의 F1/F3/F6/F7 처리 — 292줄이 244줄로

문법은 건드리지 않았다. 표본이 한 사람이 쓴 300줄 하나뿐인데 되돌리기
비싼 축을 움직일 수는 없다. std 보강만 했다.

추가: List.first/nth, String.join, std/option.cool, std/result.cool.

samples/app 재작성 결과 config.cool 196 → 148줄. head_or, second_or,
Pick, take_at, first_text, first_entry가 통째로 사라졌다. 예측 40줄,
실제 48줄 — F1의 값이 확인됐다.

F3은 줄 수로 값이 안 보인다. main.cool은 96줄 그대로다. 4단 중첩
String.concat이 4줄짜리 String.join 배열이 됐으니 줄 수가 같다. 읽기는
확실히 나아졌다. 줄 수는 읽기 좋음의 대리 지표일 뿐이고 여기서 그 대리가
깨진다 — 다음 개밥 먹기는 다른 것을 재야 한다.

F7: Interp.implemented와 std/*.cool 선언이 서로를 덮는지 테스트가 양방향
으로 검사한다. 어긋나면 빌드가 깨진다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
This commit is contained in:
2026-08-30 16:20:23 +09:00
co-authored by Claude Opus 5
parent 7be05be77f
commit a9f6527825
11 changed files with 260 additions and 90 deletions
+13 -61
View File
@@ -7,6 +7,8 @@
import "cool.dev/std/list" as List
import "cool.dev/std/string" as String
import "cool.dev/std/int" as Int
import "cool.dev/std/option" as Option
import "cool.dev/std/result" as Result
// 설정 값. 타입이 셋뿐이므로 열거형이 맞다.
pub enum Value {
@@ -75,14 +77,14 @@ pub fn parse_line(cfg: Config, no: Int, raw: String) -> Config {
cfg
} else if List.len(parts) != 2 {
add_problem(cfg, no, String.concat("= 가 하나여야 합니다: ", line))
} else if String.is_empty(String.trim(head_or(parts, ""))) {
add_problem(cfg, no, "이름이 비어 있습니다")
} else {
add_entry(cfg, Entry {
line: no,
key: String.trim(head_or(parts, "")),
value: parse_value(String.trim(second_or(parts, ""))),
})
let key = String.trim(Option.unwrap_or(List.nth(parts, 0), ""))
let val = String.trim(Option.unwrap_or(List.nth(parts, 1), ""))
if String.is_empty(key) {
add_problem(cfg, no, "이름이 비어 있습니다")
} else {
add_entry(cfg, Entry { line: no, key: key, value: parse_value(val) })
}
}
}
@@ -100,37 +102,6 @@ pub fn add_problem(cfg: Config, no: Int, msg: String) -> Config {
}
}
// 인덱싱 연산자가 없으므로 앞의 둘을 꺼내는 일은 이름 있는 함수가 한다.
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 {
@@ -154,29 +125,10 @@ pub copyable struct Numbered {
// 이름으로 찾는다. 없으면 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),
}
})
Result.map(
Option.ok_or(List.first(hit), String.concat("설정에 없습니다: ", key)),
fn(e) { e.value },
)
}
pub fn get_int(cfg: Config, key: String) -> Result[Int, String] {
+15 -15
View File
@@ -8,6 +8,7 @@ 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
import "cool.dev/std/option" as Option
pub capability Console {
fn print(s: String) effects {Console.print}
@@ -24,14 +25,17 @@ pub capability Args {
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(String.join("", [
" ", e.key, " = ", Cfg.show_value(e.value),
" (", 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)), "개")))))
c.print(String.join("", [
"항목 ", Int.show(List.len(cfg.entries)),
"개, 문제 ", Int.show(List.len(cfg.problems)), "개",
]))
if List.is_empty(cfg.problems) {
c.print("문제 없음")
@@ -39,8 +43,7 @@ pub fn report(c: Console, cfg: Cfg.Config) effects {Console.print} {
c.print("")
c.print("문제")
List.each(cfg.problems, fn(p) {
c.print(String.concat(" ", String.concat(Int.show(p.line),
String.concat("행: ", p.message))))
c.print(String.join("", [ " ", Int.show(p.line), "행: ", p.message ]))
})
}
}
@@ -56,25 +59,22 @@ pub fn check_known(c: Console, cfg: Cfg.Config) effects {Console.print} {
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))))),
Ok(n) => c.print(String.join("", [ " ", key, " = ", 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" })))),
Ok(b) => c.print(String.join("", [
" ", key, " = ", 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("설정 파일 경로가 필요합니다"),
}
Option.ok_or(List.first(xs), "설정 파일 경로가 필요합니다")
}
pub fn main(c: Console, f: File, a: Args)