Mini Shell은 앞의 셋이 못 건드린 모양을 친다 — 파이프 하나는 끝이 둘이고 각각 다른 자식에게 간다. 부모에게 아무것도 안 남고 어느 블록에도 묶이지 않는다. 가장 어려운 자원 코드가 렉시컬하지 않다. 파싱을 자원에서 떼어 놓아 개밥이 처음으로 실제로 돈다 (test 5/5 통과). 보안 정리 (ii)의 구멍 셋 — 전부 "부분 move가 없다"는 하나의 규칙이 닿아야 할 자리가 비어 있던 것: - 열거형 생성자가 인자를 소비하지 않았다. Some(f)를 두 번 쓰면 복제됐고, LRU가 쓰는 우회(affine 값을 열거형으로 나르기)가 그래서 무의미했다 - 소유한 열거형을 두 번 분해할 수 있었다. match가 scrutinee를 늘 빌려서 조각만 owned로 나갔다 — 같은 파이프에서 같은 fd를 두 번 꺼낼 수 있었다 - 점 왼쪽을 이름 해소가 검사하지 않았다. Nonsense.whatever(1)이 통과했고 samples 셋(02, 04, 06)이 이 구멍에 기대고 있었다 (D1과 같은 병) 누수 lint를 넣었다. ?와 return 자리에서 소유하고 있고 아직 안 넘긴 affine 값을 말한다. 저장소 전체 네 곳, 오탐 0건 — naive 판만 걸리고 careful 판은 조용하다. 타입 층 정책은 그대로다 (오용 금지, 누수 허용). 결정: resource라는 새 종류는 넣지 않는다. capability가 이미 그것이고 own self가 정리를 말한다. with는 미룬다 — 두 갈래 중 고르기에 데이터가 부족하고, 셸이 with가 닿지 않는 모양을 보여줬다. List.rest 추가 (첫 원소를 뺀 나머지). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
152 lines
5.8 KiB
Plaintext
152 lines
5.8 KiB
Plaintext
// Mini Shell — 자원을 다루는 부분.
|
|
//
|
|
// 앞의 세 개밥(파일, 풀, LRU)이 못 건드린 데를 친다.
|
|
// 파일: 자원 하나, 렉시컬
|
|
// 풀: 자원 두 층, 렉시컬
|
|
// LRU: 정리 지점이 비렉시컬. 다만 자원은 한 종류
|
|
// 여기: **자원이 여럿이고, 하나가 둘로 갈라져 서로 다른 곳으로 간다**
|
|
//
|
|
// 파이프 하나는 끝이 둘이고 각각 다른 자식에게 간다. 부모는 넘긴 뒤
|
|
// 아무것도 안 남는다. 어느 블록에도 묶이지 않는다 — `with`가 닿지 않는
|
|
// 모양이 무엇인지 여기서 정확히 보인다.
|
|
|
|
import "cool.dev/std/list" as List
|
|
import "../std-draft/os" as Os
|
|
import "./parse" as Parse
|
|
|
|
// ------------------------------------------------------------------
|
|
// 한 줄 실행
|
|
// ------------------------------------------------------------------
|
|
|
|
// 파이프라인 하나를 돌리고 마지막 단계의 종료 코드를 준다.
|
|
//
|
|
// 빈 파이프라인(사람이 엔터만 침)은 0이다. 아무 일도 안 한 것은 성공이다.
|
|
pub fn run(os: Os.Os, p: Parse.Pipeline)
|
|
effects {Os.pipe, Os.spawn, Child.wait, ReadFd.close, WriteFd.close}
|
|
-> Result[Int, Os.OsError] {
|
|
if List.is_empty(p.stages) {
|
|
Ok(0)
|
|
} else {
|
|
from(os, p.stages, InheritIn)
|
|
}
|
|
}
|
|
|
|
// 단계 목록을 앞에서부터 실행한다. stdin은 앞 단계가 정해 준다.
|
|
//
|
|
// 재귀가 필요하다. 자식을 리스트에 모아 두고 나중에 한꺼번에 거두는 쪽이
|
|
// 자연스러운데 그렇게 못 한다 — 아래 "리스트에 넣으면 못 꺼낸다" 참고.
|
|
// 그래서 "뒤를 먼저 다 띄우고, 돌아오면서 자기 자식을 거둔다"가 된다.
|
|
pub fn from(os: Os.Os, stages: List[Parse.Command], own stdin: Os.Stdin)
|
|
effects {Os.pipe, Os.spawn, Child.wait, ReadFd.close, WriteFd.close}
|
|
-> Result[Int, Os.OsError] {
|
|
match List.first(stages) {
|
|
// 여기 오면 stdin이 샌다. 언어는 말하지 않는다 (D3).
|
|
None => Ok(0),
|
|
Some(c) => {
|
|
let tail = List.rest(stages)
|
|
if List.is_empty(tail) {
|
|
last(os, c, stdin)
|
|
} else {
|
|
middle(os, c, stdin, tail)
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
// 마지막 단계. 출력은 부모 것을 그대로 쓴다.
|
|
pub fn last(os: Os.Os, c: Parse.Command, own stdin: Os.Stdin)
|
|
effects {Os.spawn, Child.wait} -> Result[Int, Os.OsError] {
|
|
match os.spawn(c.program, c.args, stdin, InheritOut) {
|
|
Err(e) => Err(e),
|
|
Ok(child) => child.wait(),
|
|
}
|
|
}
|
|
|
|
// 가운데 단계. 파이프를 만들어 쓰는 끝을 자식에게 주고,
|
|
// 읽는 끝을 다음 단계에 넘긴다.
|
|
//
|
|
// 이 함수가 이 개밥 먹기의 본체다. 자원 셋(stdin, 읽는 끝, 쓰는 끝)이
|
|
// 서로 다른 곳으로 가고, 실패하면 셋의 상태가 제각각이다.
|
|
pub fn middle(
|
|
os: Os.Os,
|
|
c: Parse.Command,
|
|
own stdin: Os.Stdin,
|
|
rest: List[Parse.Command],
|
|
) effects {Os.pipe, Os.spawn, Child.wait, ReadFd.close, WriteFd.close}
|
|
-> Result[Int, Os.OsError] {
|
|
match os.pipe() {
|
|
// stdin이 샌다
|
|
Err(e) => Err(e),
|
|
Ok(p) => match p {
|
|
Ends(r, w) => match os.spawn(c.program, c.args, stdin, ToPipe(w)) {
|
|
// 여기서는 r이 샌다. w는 spawn이 가져갔는지 아닌지
|
|
// 시그니처만 봐서는 알 수 없다 — 아래 발견 참고.
|
|
Err(e) => Err(e),
|
|
// 자식이 떴다. 부모는 쓰는 끝을 이미 넘겼고, 읽는 끝을
|
|
// 다음 단계에 넘긴다. 부모에게는 아무것도 안 남는다.
|
|
Ok(child) => reap(child, from(os, rest, FromPipe(r))),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// 자식을 거두고 뒤쪽의 결과를 돌려준다.
|
|
//
|
|
// 뒤쪽 결과가 앞선다 — 파이프라인의 종료 코드는 마지막 단계의 것이다.
|
|
// 앞 단계가 죽어도 셸은 마지막 것을 보고한다.
|
|
//
|
|
// 거두기 실패는 일부러 버린다. 자식이 이미 사라진 것은 우리가 할 일이
|
|
// 없고, 뒤쪽 결과가 사람이 알고 싶은 것이다.
|
|
pub fn reap(own child: Os.Child, r: Result[Int, Os.OsError])
|
|
effects {Child.wait} -> Result[Int, Os.OsError] {
|
|
let _ = child.wait()
|
|
r
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// 되풀이
|
|
// ------------------------------------------------------------------
|
|
|
|
// 읽고, 읽어서 나눈 것을 돌리고, 다시 읽는다.
|
|
//
|
|
// 내장 명령은 자식으로 띄우지 않는다. cd를 자식에서 하면 자식의 디렉터리가
|
|
// 바뀌고 자식은 곧 죽는다 — 아무 일도 안 일어난 것과 같다.
|
|
pub fn once(os: Os.Os, line: String)
|
|
effects {Os.pipe, Os.spawn, Os.chdir, Child.wait,
|
|
ReadFd.close, WriteFd.close}
|
|
-> Result[Int, Os.OsError] {
|
|
match Parse.line(line) {
|
|
// 파싱 실패는 셸을 죽이지 않는다. 사람이 오타를 낸 것뿐이다.
|
|
Err(_) => Ok(2),
|
|
Ok(p) => match List.first(p.stages) {
|
|
None => Ok(0),
|
|
Some(head) => {
|
|
if Parse.is_builtin(head) && List.len(p.stages) == 1 {
|
|
builtin(os, head)
|
|
} else {
|
|
run(os, p)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// 내장 명령. cd만 실제로 뭔가 한다.
|
|
//
|
|
// exit은 여기서 처리할 수 없다 — 되풀이를 멈추는 일은 부르는 쪽의
|
|
// 제어 흐름이지 이 함수의 결과값이 아니다. 종료 코드로 신호를 보낸다.
|
|
pub fn builtin(os: Os.Os, c: Parse.Command)
|
|
effects {Os.chdir} -> Result[Int, Os.OsError] {
|
|
if c.program == "exit" {
|
|
Ok(0 - 1)
|
|
} else {
|
|
match List.first(c.args) {
|
|
None => Ok(0),
|
|
Some(dir) => match os.chdir(dir) {
|
|
Err(e) => Err(e),
|
|
Ok(_) => Ok(0),
|
|
},
|
|
}
|
|
}
|
|
}
|