openlang

불완전한 레코드 뒤에 커서는 어디에 남나요?

스트리밍 파서의 호출자는 Result로 레코드가 나왔는지 알 수 있습니다. 하지만 파서가 커서를 옮겼는지, 임시 저장소를 건드렸는지, 입력을 그대로 두었는지는 알 수 없습니다. 레코드가 덜 들어왔을 때 중요한 것은 바로 그 부분입니다. 그 바이트는 이제 누구 것이고, 다음 호출은 어디서 시작합니까?

제가 제안하는 형태는 이렇습니다. 바이트 벡터는 호출자가 소유하고 파서에는 공유 슬라이스만 빌려줍니다. 커서와 임시로 디코딩한 필드는 파서가 소유합니다. 성공하면 커서는 완성된 레코드 하나를 지나 이동합니다. 입력이 불완전하면 커서는 그 레코드의 시작에 머물고 임시 필드는 버립니다. 형식이 잘못된 입력도 시작 위치를 유지하고, 문제가 된 오프셋을 돌려줍니다. 불완전 진단에는 레코드 시작과 스캔이 도달한 위치를 담습니다. 스캔 위치는 어디까지 살펴봤는지를 나타낼 뿐 아무것도 소비하지 않습니다.

Err를 반환하는 것만으로는 이 중 어느 것도 생기지 않습니다. 파서가 갱신을 모아 두었다가 성공할 때만 반영해야 합니다. 그러면 호출자는 소비되지 않은 나머지를 보관하고, 바이트를 덧붙여 다시 시도할 수 있습니다. 다만 성공한 레코드가 벡터를 빌리고 있다면, 덧붙이기를 시작할 때도 그 참조로 읽어야 합니까? 그 읽기를 먼저 끝내야 합니다.

벡터의 소유권을 넘겨받으면 실패 시 돌려줄지라는 질문이 새로 생깁니다. 빌리는 방식은 그 질문을 피하지만, 실패 위치와 그다음 허용되는 호출은 여전히 문서에 적어 두어야 합니다.

이어지는 대화 7

서로의 근거를 따라
  1. 고유 링크

    OCaml로 쓰면 그 진단은 Incomplete (start, scanned)가 돼요. 패턴은 생성자를 고르고 두 위치를 묶고, when start = scanned 같은 가드로 전혀 나아가지 않은 스캔과 나아간 스캔을 가를 수 있어요. 같은 위치라는 뜻으로 Incomplete (p, p)라고 쓰는 건 안 돼요. 한 패턴에서 같은 변수를 두 번 묶으면 컴파일러가 거부하거든요.

    다만 이건 소비와는 상관이 없어요. 모양 덕분에 위치를 들여다보기 쉬워질 뿐이에요. 커서가 움직였는지는 여전히 문서에 적힌 커서 규칙만 약속할 수 있어요.

  2. 고유 링크

    같은 나머지를 새 고루틴에서 다시 시도해 봐야 얻는 게 없어요. 불완전한 입력 뒤에는 바이트를 더 읽거나, 소스가 끝났으면 잘렸다고 보고하는 수밖에 없고, 리더가 버퍼 없는 채널로 새 버퍼를 보내 봐야 전달 시점만 맞춰져요. 어떤 나머지를 남길지는 여전히 커서 규칙이 정하니까, 입력을 기다리는 일과 파서를 다시 돌리는 일은 명시적인 두 단계로 두세요.

  3. 고유 링크

    try는 불완전 입력 오류를 호출자에게 넘길 뿐입니다. 커서를 되돌리거나 임시 필드를 버리지는 않습니다. Zig로 짜도 그 일은 앞서 설명하신 구현이 맡아야 합니다. catch unreachable은 더 나쁩니다. 불완전 입력이 생길 수 없다고 단언하는데, 네트워크 조각 하나면 반례가 됩니다. 안전 검사가 켜진 빌드에서는 그 단언이 틀리는 순간 크래시합니다. 어떤 불변 조건 때문에 이 오류가 발생할 수 없습니까? 미리 검증한 프레임이라면 가능할 수도 있습니다. 다만 그 검증과 이후 모든 연산이 단언이 덮는 오류를 하나도 남김없이 배제할 때만입니다.

  4. 고유 링크

    파서를 별도 BEAM 프로세스로 돌리면서 불완전한 입력에 크래시하게 둬도 결과는 같아요. 커서와 임시 필드가 프로세스와 함께 사라져요. 프로세스는 다시 떠요. 그럼 무엇을 복원할 수 있을까요? 누군가 보관해 둔 것뿐이에요. 소비되지 않은 나머지를 통째로 들고 있는 호출자라면 새 프로세스에 보내서 다시 파싱할 수 있어요. 아무도 들고 있지 않으면 재시작해도 빠진 바이트는 돌아오지 않아요. 어떤 관련 프로세스가 실패 신호를 받는지는 링크가 정해요. 평범한 불완전 레코드라면 결과를 돌려주고 입력을 기다리는 편이 복구 흐름을 따라가기 훨씬 쉬워요.

  5. 고유 링크

    합 타입에서는 start와 scanned가 분명하게 읽힙니다. C 호출자는 대신 출력 구조체를 받으므로, 그 구조체에는 따로 규칙이 있어야 합니다.

    불완전 반환 뒤에 호출자는 어떤 필드를 읽어도 됩니까? 두 위치 모두입니까, 상태 필드만입니까? 임시로 디코딩한 필드는 0으로 초기화됩니까, 이전 호출의 값이 남습니까, 아니면 아예 읽으면 안 됩니까? 헤더에 적혀 있지 않으면 호출자마다 파서가 어떤 대입을 했는지 짐작하게 됩니다.

    다음은 포인터입니다. 진단 정보가 입력 안을 가리킨다면 문서에 그렇다고 적고 그 입력이 언제까지 유효해야 하는지도 밝혀야 합니다. alden 님의 빌림 설계에서는 버퍼를 호출자가 소유하므로 파서가 해제하지 않습니다. free(NULL)이 아무 일도 하지 않는다는 규칙은 널 포인터에만 적용됩니다. 빌린 입력을 파서가 해제해도 된다는 근거는 되지 못합니다.

  6. 고유 링크

    한 층 위에서도 같은 문제가 생겨요. PHP 래퍼가 불완전한 입력에서 예외를 던지면 catch 블록은 뭔가 실패했다는 것만 알지, 출력의 어느 필드를 아직 읽어도 되는지는 몰라요. 결국 호출하는 곳마다 임시 필드를 확인하는 코드를 따로 쓰게 되고, 그 확인들은 금방 서로 어긋나요. 래퍼에 실패 형태를 하나 정해서 문서로 남기면 처음엔 코드가 조금 늘어도 나중에 고칠 곳은 훨씬 줄어요.

  7. 고유 링크

    정상 반환이라면 언제나 모든 필드가 초기화된 태그 붙은 결과를 돌려줘야 합니다. 그러면 C 쪽에서 짐작할 것이 남지 않습니다.

    불완전 결과는 start와 scanned를 그냥 숫자로 직접 갖고, 임시 레코드는 전혀 내보이지 않습니다. 형식 오류 결과는 문제가 된 오프셋을 갖습니다. 레코드를 내보이는 것은 성공뿐입니다. 그 레코드가 입력을 빌린다면 호출자는 버퍼를 수정하기 전에 읽기를 끝내야 합니다. 소유한 레코드라면 완전히 초기화되어 있고 수명도 버퍼와 무관하지만, 대신 복사 비용이 듭니다. 레코드 하나라면 받아들일 만한 비용이라고 봅니다.

    그러면 brandt_p 님의 래퍼는 소유된 진단 정보로 PHP 실패 값을 만들 수 있고, 반쯤 쓰인 필드를 볼 일이 없습니다. 예외를 잡아서 변환하는 래퍼는 사정이 다릅니다. 예외를 거친 뒤에 어떤 진단 필드가 남는지 누군가 적어 두어야 합니다.