format.tpz
// Korean identifiers are used throughout; English glosses are added below each comment.
// Key identifiers: Json=recursive JSON AST enum, Pair=object member (key + value),
// 조각=parse result (AST value + next index), 문자열조각=string fragment (raw + next index),
// 오류=Error, 오류종류=error kind (payload-less enum), 메시지=error-message renderer, 위치=position,
// 값=value, 종류=kind, 다음=next index, 최대깊이=maxDepth, 글자화=toScalars, 공백인가=isWhitespace,
// 공백건너뛰기=skipWhitespace, 숫자인가=isDigit, 십육진인가=isHexDigit, 매치=match,
// 들여쓰기=indent, 이스케이프HTML=escapeHTML, 직렬화=serialize, 문자열읽기=readString,
// 숫자읽기=readNumber, 값읽기=readValue, 객체읽기=readObject, 배열읽기=readArray,
// 줄열=lineCol, 오류표시=renderError, 포맷=format; 글자들=scalars, 색인=index, 원문=raw,
// 쌍들=members, 요소들=elements, 줄=line, 열=col.
// JSON 포매터/검증기. Topaz로만 작성한 완전한 재귀 하강 JSON 파서입니다.
// 파서는 입력을 재귀 열거형 AST(enum Json)로 만들고, 별도의 직렬화 함수가 2칸 들여쓰기로 렌더링합니다.
// 전체 JSON 문법을 검증하고 2칸 들여쓰기로 포맷합니다. 구문 오류가 나면
// line:column 형식으로 한국어 메시지를 보고합니다. 숫자 토큰은 그대로 보존합니다. 숫자는
// 텍스트 그대로 보존하며 부동소수점으로 재해석하거나 반올림하지 않습니다. 문자열 토큰도 보존하되, 안전한
// \uXXXX 이스케이프(" 나 \ 가 아닌 출력 가능한 스칼라)는 해당 문자로 디코딩합니다.
// 순수하고 결정적입니다. 같은 입력은 인터프리터와 네이티브/wasm 빌드에서 같은 출력을 냅니다.
// Validates the full JSON grammar and pretty-prints with 2-space indentation. The parser builds a
// recursive enum AST (enum Json) and a separate serializer renders it. On a syntax
// error, reports a Korean message in line:column form. Number tokens are preserved verbatim:
// numbers are preserved as text, with no floating-point reinterpretation or rounding. String tokens are
// preserved too, except safe \uXXXX escapes (printable scalars other than " or \) are decoded
// to the actual character. Pure and deterministic: the same input yields the same output under
// the interpreter and the native/wasm builds.
// JSON 값을 재귀 열거형 AST로 표현한다. 5.6의 재귀 enum이 문자열-태그·평행배열 인코딩을 대체한다.
// Represent a JSON value as a recursive enum AST. 5.6 recursive enums replace string-tag/parallel-array encodings.
// 숫자·문자열 토큰은 원문 그대로 보존한다(Num/Str). 들여쓰기는 파싱이 아니라 직렬화 단계에서 붙인다.
// Number/string tokens are kept verbatim (Num/Str). Indentation is applied by the serializer, not the parser.
enum Json { Nul, Bool(bool), Num(string), Str(string), Arr(Array<Json>), Obj(Array<Pair>) }
// 객체 멤버(키 원문 + 값). key 는 이미 직렬화된 따옴표 포함 문자열이다.
// An object member (raw key + value). key is the already-serialized quoted string.
record Pair { key: string, val: Json }
// 파서 결과를 명목 레코드로 둔다. 조각=값읽기 결과(AST 값 + 다음 색인), 문자열조각=문자열읽기 결과(원문 + 다음 색인).
// Parser results as nominal records. 조각=readValue result (AST value + next index), 문자열조각=readString result (raw + next index).
record 조각 { 값: Json, 다음: int }
record 문자열조각 { 원문: string, 다음: int }
// 닫힌 오류 종류 집합을 페이로드 없는 열거형으로 둔다. 경계 문자열은 메시지() 한 곳에서만 렌더링한다.
// The closed set of error kinds as a payload-less enum. Boundary strings render in exactly one place, 메시지().
enum 오류종류 {
문자열미종료, 유니코드이스케이프, 잘못된이스케이프, 제어문자,
숫자필요, 소수점숫자필요, 지수숫자필요,
너무깊음, 값필요, 알수없는값,
키필요, 콜론필요, 닫는중괄호필요, 쉼표나닫는중괄호,
닫는대괄호필요, 쉼표나닫는대괄호, 잉여내용
}
// 파스 오류: 스칼라 위치와 오류 종류. 표시할 문자열은 메시지()가 만든다.
// A parse error: scalar position and error kind. The displayed string comes from 메시지().
record 오류 { 위치: int, 종류: 오류종류 }
let 최대깊이 = 200
// 스캐닝 핫 루프에서 매번 배열을 다시 만들지 않도록 상수 조회 테이블을 모듈 수준 Set으로 올린다.
// Hoist constant lookup tables to module-level Sets so the scanning hot loops don't rebuild arrays each call.
let 공백집합 = Set.of(" ", "\n", "\t", "\r")
let 숫자집합 = Set.of("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")
let 알파헥스집합 = Set.of("a", "b", "c", "d", "e", "f", "A", "B", "C", "D", "E", "F")
// 오류 종류를 사용자 대상 한국어 문자열로 렌더링한다. 모든 경계 문자열이 이 exhaustive match 한 곳에 모인다.
// Renders an error kind to its user-facing Korean string. Every boundary string lives in this one exhaustive match.
function 메시지(종류: 오류종류) -> string {
match 종류 {
case 문자열미종료 => "문자열이 끝나지 않았습니다"
case 유니코드이스케이프 => "\\u 뒤에 16진수 4자리가 필요합니다"
case 잘못된이스케이프 => "잘못된 이스케이프 문자입니다"
case 제어문자 => "문자열 안의 제어문자는 이스케이프해야 합니다"
case 숫자필요 => "숫자가 필요합니다"
case 소수점숫자필요 => "소수점 뒤에 숫자가 필요합니다"
case 지수숫자필요 => "지수 뒤에 숫자가 필요합니다"
case 너무깊음 => "중첩이 너무 깊습니다 (최대 200단계)"
case 값필요 => "값이 필요합니다"
case 알수없는값 => "알 수 없는 값입니다"
case 키필요 => "키(문자열)가 필요합니다"
case 콜론필요 => "콜론(:)이 필요합니다"
case 닫는중괄호필요 => "닫는 중괄호(\})가 필요합니다"
case 쉼표나닫는중괄호 => "쉼표(,)나 닫는 중괄호(\})가 필요합니다"
case 닫는대괄호필요 => "닫는 대괄호(])가 필요합니다"
case 쉼표나닫는대괄호 => "쉼표(,)나 닫는 대괄호(])가 필요합니다"
case 잉여내용 => "값 뒤에 불필요한 내용이 있습니다"
}
}
function 글자화(s: string) -> Array<string> { s.scalars() }
function 공백인가(글: string) -> bool {
글 in 공백집합
}
function 공백건너뛰기(글자들: Array<string>, 색인: int) -> int {
let mut i = 색인
while i < 글자들.length && 공백인가(글자들[i]) { i = i + 1 }
i
}
function 숫자인가(글: string) -> bool {
글 in 숫자집합
}
function 십육진인가(글: string) -> bool {
숫자인가(글) || 글 in 알파헥스집합
}
function 매치(글자들: Array<string>, 색인: int, 낱말: string) -> bool {
let 길이 = 낱말.scalars().length
색인 + 길이 <= 글자들.length && 글자들.slice(색인, 색인 + 길이).join("") == 낱말
}
function 들여쓰기(깊이: int) -> string {
let mut s = ""
for _ in 0..<깊이 { s += " " }
s
}
function 이스케이프HTML(s: string) -> string {
// & 를 가장 먼저 치환해야 뒤 항목이 만든 엔티티를 다시 훑지 않는다.
// Replace & first so the entities it introduces are not re-scanned by the later replaces.
s.replace("&", "&").replace("<", "<").replace(">", ">")
}
// JSON 문자열을 읽습니다. 여는 따옴표부터 닫는 따옴표까지 이스케이프를 검증하며 텍스트는 보존합니다.
// Reads a JSON string: validates escapes from opening to closing quote, preserving the text,
// 단 안전한 \uXXXX 이스케이프는 해당 문자로 디코딩합니다(아래 \u 블록 참고).
// except safe \uXXXX escapes, which are decoded to the actual character (see the \u block below).
function 문자열읽기(글자들: Array<string>, 색인: int) -> Result<문자열조각, 오류> {
let mut i = 색인 + 1
let mut 원문 = "\""
while i < 글자들.length {
let c = 글자들[i]
if c == "\"" {
return Ok(문자열조각 { 원문: 원문 + "\"", 다음: i + 1 })
} else if c == "\\" {
if i + 1 >= 글자들.length { return Err(오류 { 위치: i, 종류: 오류종류.문자열미종료 }) }
let e = 글자들[i + 1]
if e == "\"" || e == "\\" || e == "/" || e == "b" || e == "f" || e == "n" || e == "r" || e == "t" {
원문 = 원문 + c + e
i = i + 2
} else if e == "u" {
if i + 5 >= 글자들.length { return Err(오류 { 위치: i, 종류: 오류종류.유니코드이스케이프 }) }
let mut k = 0
while k < 4 {
if !십육진인가(글자들[i + 2 + k]) {
return Err(오류 { 위치: i + 2 + k, 종류: 오류종류.유니코드이스케이프 })
}
k = k + 1
}
// JSON 문자열에서 안전할 때 \uXXXX 이스케이프를 실제 문자로 디코딩합니다.
// Decode a \uXXXX escape to its actual character when it is safe to do so in a JSON string:
// 즉 `"`(34) 나 `\`(92) 가 아닌 출력 가능한 스칼라. 제어문자, 따옴표,
// that is, a printable scalar other than `"`(34) or `\`(92). Control chars, quotes,
// 백슬래시, 잘못된 스칼라(고립 대리쌍)는 이스케이프 상태로 둡니다. 이 앱이 끌어낸 컴파일러
// backslashes, and invalid scalars (lone surrogates) stay escaped. Chains the compiler
// 기능을 연결합니다. toIntRadix(hex, 16) 다음 fromCodePoint(codepoint).
// features this app drove: toIntRadix(hex, 16) then fromCodePoint(codepoint).
let 십육 = 글자들[i + 2] + 글자들[i + 3] + 글자들[i + 4] + 글자들[i + 5]
let 코드 = toIntRadix(십육, 16) ?? -1
let 문자 = fromCodePoint(코드) ?? ""
if 코드 >= 32 && 코드 != 34 && 코드 != 92 && 문자.byteLength() > 0 {
원문 = 원문 + 문자
} else {
원문 = 원문 + "\\u" + 십육
}
i = i + 6
} else {
return Err(오류 { 위치: i + 1, 종류: 오류종류.잘못된이스케이프 })
}
} else {
// RFC 8259. 원시 제어문자 U+0000..U+001F는 문자열 안에서 반드시 이스케이프해야 합니다.
// RFC 8259: raw control chars U+0000..U+001F must be escaped inside a string.
let 코드 = c.codePointAt(0) ?? 0
if 코드 < 32 { return Err(오류 { 위치: i, 종류: 오류종류.제어문자 }) }
원문 = 원문 + c
i = i + 1
}
}
Err(오류 { 위치: 색인, 종류: 오류종류.문자열미종료 })
}
// JSON 숫자. -? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE][+-]?[0-9]+)? 형식이며 텍스트는 보존합니다.
// JSON number of the form -? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE][+-]?[0-9]+)?, text preserved.
function 숫자읽기(글자들: Array<string>, 색인: int) -> Result<조각, 오류> {
let mut i = 색인
if i < 글자들.length && 글자들[i] == "-" { i = i + 1 }
// RFC 8259 정수부. `0` 단독, 또는 `[1-9][0-9]*`. 앞자리 0(01, 00)은 잘못된 값입니다.
// RFC 8259 integer part: bare `0`, or `[1-9][0-9]*`. Leading zeros (01, 00) are invalid.
if i >= 글자들.length || !숫자인가(글자들[i]) { return Err(오류 { 위치: i, 종류: 오류종류.숫자필요 }) }
if 글자들[i] == "0" {
i = i + 1
} else {
while i < 글자들.length && 숫자인가(글자들[i]) { i = i + 1 }
}
if i < 글자들.length && 글자들[i] == "." {
i = i + 1
let 소수시작 = i
while i < 글자들.length && 숫자인가(글자들[i]) { i = i + 1 }
if i == 소수시작 { return Err(오류 { 위치: i, 종류: 오류종류.소수점숫자필요 }) }
}
if i < 글자들.length && (글자들[i] == "e" || 글자들[i] == "E") {
i = i + 1
if i < 글자들.length && (글자들[i] == "+" || 글자들[i] == "-") { i = i + 1 }
let 지수시작 = i
while i < 글자들.length && 숫자인가(글자들[i]) { i = i + 1 }
if i == 지수시작 { return Err(오류 { 위치: i, 종류: 오류종류.지수숫자필요 }) }
}
let 원문 = 글자들.slice(색인, i).join("")
Ok(조각 { 값: Json.Num(원문), 다음: i })
}
function 값읽기(글자들: Array<string>, 색인: int, 깊이: int) -> Result<조각, 오류> {
if 깊이 > 최대깊이 {
return Err(오류 { 위치: 색인, 종류: 오류종류.너무깊음 })
}
let i = 공백건너뛰기(글자들, 색인)
if i >= 글자들.length { return Err(오류 { 위치: i, 종류: 오류종류.값필요 }) }
let c = 글자들[i]
if c == "\{" { return 객체읽기(글자들, i, 깊이) }
if c == "[" { return 배열읽기(글자들, i, 깊이) }
if c == "\"" {
let s = 문자열읽기(글자들, i)?
return Ok(조각 { 값: Json.Str(s.원문), 다음: s.다음 })
}
if c == "-" || 숫자인가(c) { return 숫자읽기(글자들, i) }
if 매치(글자들, i, "true") { return Ok(조각 { 값: Json.Bool(true), 다음: i + 4 }) }
if 매치(글자들, i, "false") { return Ok(조각 { 값: Json.Bool(false), 다음: i + 5 }) }
if 매치(글자들, i, "null") { return Ok(조각 { 값: Json.Nul, 다음: i + 4 }) }
Err(오류 { 위치: i, 종류: 오류종류.알수없는값 })
}
function 객체읽기(글자들: Array<string>, 색인: int, 깊이: int) -> Result<조각, 오류> {
let mut j = 공백건너뛰기(글자들, 색인 + 1)
if j < 글자들.length && 글자들[j] == "\}" { return Ok(조각 { 값: Json.Obj([]), 다음: j + 1 }) }
let mut 쌍들: Array<Pair> = []
while true {
j = 공백건너뛰기(글자들, j)
if j >= 글자들.length || 글자들[j] != "\"" { return Err(오류 { 위치: j, 종류: 오류종류.키필요 }) }
let 키 = 문자열읽기(글자들, j)?
j = 공백건너뛰기(글자들, 키.다음)
if j >= 글자들.length || 글자들[j] != ":" { return Err(오류 { 위치: j, 종류: 오류종류.콜론필요 }) }
let 값 = 값읽기(글자들, j + 1, 깊이 + 1)?
쌍들.push(Pair { key: 키.원문, val: 값.값 })
j = 공백건너뛰기(글자들, 값.다음)
if j >= 글자들.length { return Err(오류 { 위치: j, 종류: 오류종류.닫는중괄호필요 }) }
if 글자들[j] == "\}" { return Ok(조각 { 값: Json.Obj(쌍들), 다음: j + 1 }) }
if 글자들[j] != "," { return Err(오류 { 위치: j, 종류: 오류종류.쉼표나닫는중괄호 }) }
j = j + 1
}
}
function 배열읽기(글자들: Array<string>, 색인: int, 깊이: int) -> Result<조각, 오류> {
let mut j = 공백건너뛰기(글자들, 색인 + 1)
if j < 글자들.length && 글자들[j] == "]" { return Ok(조각 { 값: Json.Arr([]), 다음: j + 1 }) }
let mut 요소들: Array<Json> = []
while true {
let 값 = 값읽기(글자들, j, 깊이 + 1)?
요소들.push(값.값)
j = 공백건너뛰기(글자들, 값.다음)
if j >= 글자들.length { return Err(오류 { 위치: j, 종류: 오류종류.닫는대괄호필요 }) }
if 글자들[j] == "]" { return Ok(조각 { 값: Json.Arr(요소들), 다음: j + 1 }) }
if 글자들[j] != "," { return Err(오류 { 위치: j, 종류: 오류종류.쉼표나닫는대괄호 }) }
j = j + 1
}
}
// 스칼라 색인을 "line:col"(1부터 시작)로 바꿉니다. 해당 위치 앞의 개행 수를 셉니다.
// Converts a scalar index to "line:col" (1-based) by counting newlines before that position.
function 줄열(글자들: Array<string>, 위치: int) -> string {
let mut 줄 = 1
let mut 열 = 1
let mut i = 0
while i < 위치 && i < 글자들.length {
if 글자들[i] == "\n" { 줄 = 줄 + 1; 열 = 1 } else { 열 = 열 + 1 }
i = i + 1
}
"{줄}:{열}"
}
// AST를 2칸 들여쓰기 문자열로 직렬화합니다. 파싱과 분리되어, exhaustive match로 각 변형을 렌더링합니다.
// Serializes the AST to a 2-space-indented string. Separate from parsing; an exhaustive match renders each variant.
function 직렬화(값: Json, 깊이: int) -> string {
match 값 {
case Nul => "null"
case Bool(b) => if b { "true" } else { "false" }
case Num(원문) => 원문
case Str(원문) => 원문
case Arr(요소들) => {
if 요소들.length == 0 {
"[]"
} else {
"[\n" + 요소들.map(요소 => 들여쓰기(깊이 + 1) + 직렬화(요소, 깊이 + 1)).join(",\n") + "\n" + 들여쓰기(깊이) + "]"
}
}
case Obj(쌍들) => {
if 쌍들.length == 0 {
"\{\}"
} else {
"\{\n" + 쌍들.map(쌍 => 들여쓰기(깊이 + 1) + 쌍.key + ": " + 직렬화(쌍.val, 깊이 + 1)).join(",\n") + "\n" + 들여쓰기(깊이) + "\}"
}
}
}
}
// 오류 한 줄을 렌더링합니다. 성공 파스 뒤의 잉여 내용과 파스 오류가 같은 경계를 공유합니다.
// Renders one error line. Trailing content after a successful parse and a parse error share this boundary.
function 오류표시(글자들: Array<string>, 위치: int, 종류: 오류종류) -> string {
"<div class=\"err\">오류 " + 줄열(글자들, 위치) + ": " + 이스케이프HTML(메시지(종류)) + "</div>"
}
function 포맷(원본: string) -> string {
let 글자들 = 글자화(원본)
match 값읽기(글자들, 0, 0) {
case Ok(조) => {
let 끝 = 공백건너뛰기(글자들, 조.다음)
if 끝 < 글자들.length {
오류표시(글자들, 끝, 오류종류.잉여내용)
} else {
"<pre class=\"ok\">" + 이스케이프HTML(직렬화(조.값, 0)) + "</pre>"
}
}
case Err(오) => 오류표시(글자들, 오.위치, 오.종류)
}
}
print(포맷(input()))