// 설정 파일 파서. // // 이 파일은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴 것이다. // 그것이 이 파일의 목적이다 — 언어가 실제로 쓸 만한지는 이런 코드에서만 // 드러난다. 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) 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 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, ""))), }) } } 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, "은(는) 참거짓이 아닙니다")), } }