interp: 실패해도 그때까지의 출력을 보여준다

0으로 나누는 프로그램을 돌려보니 실패 전에 출력한 것이 통째로 사라졌다.
Interp.run이 출력을 버퍼에 모았다가 성공했을 때만 돌려주고 실패하면 버렸다.

print는 실제로 일어난 effect다. 일어난 일을 안 보여주면 "어디까지 갔나"를
알 수 없고, 그게 실패했을 때 가장 먼저 보고 싶은 것이다.

Interp.run과 Session.run이 이제 (출력, 실패 여부)를 함께 돌려준다.
result가 아니라 짝인 이유는 둘이 배타가 아니기 때문이다 — 실패했어도
출력은 있다.

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 17:21:34 +09:00
co-authored by Claude Opus 5
parent bb19a39e04
commit 8ff35c5d9b
4 changed files with 53 additions and 25 deletions
+33 -12
View File
@@ -1032,9 +1032,10 @@ let console =
let outputs ?(use = []) src expected =
match run_src (imports use ^ console ^ src) with
| Ok out -> out = expected
| Error e ->
Printf.printf " (실행 오류: %s)\n" (Session.string_of_error e);
| out, None -> out = expected
| out, Some e ->
Printf.printf " (실행 오류: %s / 그때까지 출력: %S)\n"
(Session.string_of_error e) out;
false
let () =
@@ -1113,7 +1114,7 @@ let () =
"a\nb\nafter\n");
(* 권한은 런타임에서만 온다. main이 선언하지 않으면 존재하지 않는다. *)
check "선언하지 않은 capability는 실행 시점에도 없다"
(match run_src "pub fn main() { }" with Ok "" -> true | _ -> false);
(match run_src "pub fn main() { }" with "", None -> true | _ -> false);
check "런타임이 모르는 capability는 거절한다"
(match
run_src
@@ -1122,8 +1123,28 @@ let () =
}\n\n\
pub fn main(d: Db) effects {Db.read} -> Int { d.read() }"
with
| Error e -> has_sub e.message "제공하지 않습니다"
| Ok _ -> false)
| _, Some e -> has_sub e.message "제공하지 않습니다"
| _, None -> false)
(* 실패해도 그때까지의 출력은 남는다. print는 실제로 일어난 effect이고,
일어난 일을 안 보여주면 "어디까지 갔나"를 알 수 없다. *)
let () =
match
run_src
(imports [ "Int" ] ^ console
^ "pub fn risky(a: Int, b: Int) -> Int { a / b }\n\n\
pub fn main(c: Console) effects {Console.print} {\n\
\ c.print(\"\")\n\
\ c.print(Int.show(risky(10, 0)))\n\
\ c.print(\"\")\n\
}")
with
| out, Some e ->
check "0으로 나누면 실행 시점 오류다" (has_sub e.message "0으로 나눌 수 없습니다");
check "실패해도 그 전 출력은 남는다" (out = "\n")
| _, None ->
check "0으로 나누면 실행 시점 오류다" false;
check "실패해도 그 전 출력은 남는다" false
(* 표준 라이브러리 시그니처가 실제 검사에 쓰이는지.
@@ -1250,10 +1271,10 @@ let () =
~args:[ "../samples/app/example.conf" ]
st "../samples/app/main.cool"
with
| Error e ->
| _, Some e ->
Printf.printf " (실행 오류: %s)\n" (Session.string_of_error e);
check "app: 설정 리포트가 돈다" false
| Ok out ->
| out, None ->
check "app: 항목과 문제를 센다" (has_sub out "항목 5개, 문제 2개");
check "app: 값의 타입을 모양으로 정한다"
(has_sub out "threads = 4 (number)"
@@ -1266,14 +1287,14 @@ let () =
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
| out, None -> check "app: 인자가 없으면 말해준다" (has_sub out "경로가 필요합니다")
| _, Some _ -> 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
| out, None -> check "app: 없는 파일을 Err로 돌려준다" (has_sub out "오류: ")
| _, Some _ -> check "app: 없는 파일을 Err로 돌려준다" false
(* std 선언과 런타임 구현이 어긋나면 검사는 통과하고 실행이 죽는다.
v0에서 둘은 다른 파일에 있으므로 일치는 테스트가 지킨다 (friction F7). *)