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:
2026-08-30 16:06:08 +09:00
co-authored by Claude Opus 5
parent 5ac899df68
commit 5831af7760
14 changed files with 703 additions and 9 deletions
+87 -1
View File
@@ -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
(* 프로그램 인자. 런타임이 들고 있다가 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 =
match name with
| "Console" ->
@@ -96,6 +134,26 @@ let root_capability name : value option =
Buffer.add_char out '\n';
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" ->
(* 루트 스코프. 구조적 동시성의 뿌리도 런타임이 준다 — 프로그램이
스스로 만들 수 있으면 부모 없는 작업이 생긴다. *)
@@ -106,10 +164,24 @@ let builtin pos name (args : value list) : value =
match (name, args) with
| "string.concat", [ VStr a; VStr b ] -> VStr (a ^ b)
| "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.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")
| "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)
@@ -228,6 +300,19 @@ and apply st pos f args =
match args with
| [ VList xs; f ] -> VList (List.map (fun x -> apply st pos f [ x ]) xs)
| _ -> 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
| VNative f -> f args
| other -> fail pos (Printf.sprintf "%s은(는) 부를 수 없습니다" (show other))
@@ -313,10 +398,11 @@ and eval_binary st env op a b pos =
(* 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
=
Buffer.clear out;
argv := args;
let st = { prog } in
match Hashtbl.find_opt prog.Ir.fns (entry ^ "#main") with
| None -> Error (Token.{ line = 0; col = 0 }, "main 함수가 없습니다")
+2 -2
View File
@@ -258,7 +258,7 @@ let main_params (m : Ast.modul) =
| _ -> [])
m.items
let run st path : (string, error) result =
let run ?(args = []) st path : (string, error) result =
load st path;
let errs = errors st in
if errs <> [] then Error (List.hd errs)
@@ -275,6 +275,6 @@ let run st path : (string, error) result =
st.modules []
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
| Error (pos, msg) -> Error (err_of path pos msg))