문법은 건드리지 않았다. 표본이 한 사람이 쓴 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
149 lines
4.4 KiB
Plaintext
149 lines
4.4 KiB
Plaintext
// 설정 파일 파서.
|
|
//
|
|
// 이 파일은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴 것이다.
|
|
// 그것이 이 파일의 목적이다 — 언어가 실제로 쓸 만한지는 이런 코드에서만
|
|
// 드러난다.
|
|
|
|
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 {
|
|
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)
|
|
let parts = String.split(line, "=")
|
|
if String.is_empty(line) {
|
|
cfg
|
|
} else if String.starts_with(line, "#") {
|
|
cfg
|
|
} else if List.len(parts) != 2 {
|
|
add_problem(cfg, no, String.concat("= 가 하나여야 합니다: ", line))
|
|
} else {
|
|
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) })
|
|
}
|
|
}
|
|
}
|
|
|
|
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 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 })
|
|
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] {
|
|
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, "은(는) 참거짓이 아닙니다")),
|
|
}
|
|
}
|