lowstruct 매뉴얼←↑
0.0.1 · 마크다운 매뉴얼에서 생성됨

lowstruct 형식 명세 — 판 0.0.1

English · 한국어

정본은 영문 명세(lowstruct.md)이고, 이 문서는 그 번역입니다. 둘이 어긋나면 영문이 옳습니다.
  • 형식 이름: lowstruct. 확장자 .lows. 매체 형식(비공식): text/x-lowstruct.
  • 로우엔트(Lowent)와 어휘를 공유하지만 별개의 형식이다(부록 A). .lows 파일은 로우엔트 프로그램이 아니다.
  • 상태: 판 0.0.1 (초안). 호환을 약속하지 아니한다.
  • 규범은 영문 명세다. 세 구현(c/·js/·python/)과 적합성 사례(conformance/)는 명세를 따르고, 명세와 어긋나면 구현이나 사례를 고친다.
  • 문장의 끝이 «…한다» 이면 요구이고, «…아니한다» 이면 금지다. (참고) 로 시작하는 단락은 규범이 아니다.

1. 개요

lowstruct 는 사람이 손으로 고치는 설정 파일 형식이다. 로우엔트(Lowent) 언어의 어휘를 그대로 빌린다.

rem 서버 설정
title "my-app" .

server do
  host "0.0.0.0" .
  ports 80 443 .
  tls do
    cert "a.pem" .
    key  "a.key" .
  end
end

windir "C:\\Windows" .
  • 문장은 이름들(경로) + 리터럴들(값) + . 이다. =·[ ]·, 가 없다.
  • do … end 는 공통 앞부분을 묶는다.
  • 값이 여럿이면 목록이다.
  • 역슬래시는 닫힌 이스케이프 집합으로만 읽힌다. 여러 줄 원문은 text heredoc 으로 적는다.

2. 파일

(1) 파일은 UTF-8 이다. 유효하지 않은 UTF-8 은 거부한다(E-LOWS-UTF8). 유효함은 RFC 3629 를 따른다 — 지나치게 긴 부호화, 서러게이트(U+D800–U+DFFF), U+10FFFF 초과는 유효하지 않다.

(2) 파일 맨 앞의 BOM(EF BB BF)은 거부한다(E-LOWS-BOM).

(3) 줄 끝은 LF 또는 CRLF 다. 처리기는 CRLF 를 LF 로 바꾼 뒤에 읽는다. 그 뒤에도 남은 CR 은 거부한다(E-LOWS-CR). 그러므로 text heredoc 의 값에 CRLF 는 남지 아니한다.

(4) 비 ASCII 글자는 주석, 문자열·문자 리터럴, heredoc 본문 안에서만 쓸 수 있다. 그 밖의 자리에 있는 글자가 아래 어휘 어디에도 속하지 않으면 거부한다(E-LOWS-CHARSET). NUL 과 [·=·#·;·, 도 여기에 든다.

(참고) 그래서 INI 나 TOML 파일을 잘못 넘기면 첫 문장에서 멈춘다.

3. 어휘

3.1 공백과 주석

(1) 공백은 스페이스(U+0020)·탭(U+0009)·줄바꿈(U+000A)이며, 토큰을 가르는 것 말고 뜻이 없다.

(2) 낱말 rem 부터 그 줄의 끝까지는 주석이다.

(3) note TAG 로 시작하는 여러 줄 주석은 같은 TAG 만 있는 줄에서 끝난다(3.5 의 heredoc 과 같은 규칙). 값을 만들지 아니한다.

3.2 이름과 예약어

(1) 이름은 [A-Za-z_][A-Za-z0-9_]* 다. 대소문자를 가린다. 점이 없다.

(2) 다음 일곱 낱말은 예약되어 있고 이름이 될 수 없다: rem note text do end true false. 이들은 어휘 단계에서 이미 주석·heredoc·블록·불리언이 되므로, «예약어를 이름으로 썼다» 는 진단은 따로 없다 — 그 자리에서 문법이 깨진다(예: server text 1 . 은 E-LOWS-TAG).

(3) 이름 바로 뒤에 따옴표가 붙으면 그 이름은 접두사다(3.4). u·U 밖의 접두사는 거부한다(E-LOWS-PREFIX). 띄어 쓰면 접두사가 아니다 — u "x" 는 이름 u 와 문자열 "x" 다.

3.3 수

(1) 수의 모양은 다음과 같다. 위의 갈래부터 차례로 맞춰 보고, 처음 맞는 것을 쓴다.

number    = [ "+" | "-" ] , ( hexfloat | decfloat | hex | bin | dec ) ;
dec       = digit , { [ "_" ] , digit } ;
hex       = "0" , ( "x" | "X" ) , hexdigit , { [ "_" ] , hexdigit } ;
bin       = "0" , ( "b" | "B" ) , bindigit , { [ "_" ] , bindigit } ;
decfloat  = dec , "." , dec , [ dexp ] | dec , dexp ;
hexfloat  = hex , [ "." , hexdigit , { [ "_" ] , hexdigit } ] , ( "p" | "P" ) , [ "+" | "-" ] , dec ;
dexp      = ( "e" | "E" ) , [ "+" | "-" ] , dec ;

(2) 부호는 붙여 써야 부호다. - 5 는 수가 아니다.

(3) _ 는 자릿수 사이에만 온다. 앞의 0 은 팔진이 아니다 — 0755 는 755 다. 팔진 표기는 없다.

(4) 점 앞뒤에 숫자가 있어야 부동소수다. 80. 은 정수 80 과 문장 끝이고, .5 는 수가 아니다.

(5) 수 바로 뒤에 [A-Za-z0-9_] 가 붙어 있으면 그 수는 잘못된 것이다(E-LOWS-NUMBER). 그래서 0x·0b(숫자 없는 진법 표시), 0o7, 1__0, 0x_1, 1_, 0x1p(지수 숫자 없음)가 거부된다.

(6) 정수의 값은 −2⁶³ 이상 2⁶⁴−1 이하여야 한다(E-LOWS-RANGE). -0 은 0 이다.

(7) 부동소수의 값은 IEEE 754 binary64 로, 가장 가까운 값으로 반올림하고 한가운데면 짝수 쪽으로 간다 (십진·십육진 모두). 결과가 유한하지 않으면 거부한다(E-LOWS-RANGE). inf·nan 을 적는 방법은 없다.

3.4 문자열과 문자

(1) 문자열은 "…", 문자는 '…' 다. 앞에 접두사 u·U 를 붙일 수 있다.

(2) 리터럴은 한 줄 안에서 닫혀야 한다(E-LOWS-UNCLOSED).

(3) 역슬래시로 시작하는 이스케이프는 다음 열넷뿐이다. 그 밖의 것은 거부한다(E-LOWS-ESCAPE).

적는 것값
\\ \" \'0x5C · 0x22 · 0x27
\a \b \f \n \r \t \v \00x07 · 0x08 · 0x0C · 0x0A · 0x0D · 0x09 · 0x0B · 0x00
\xNN바이트 하나 (십육진 정확히 두 자리)
\uXXXX · \UXXXXXXXX코드포인트 (정확히 네·여덟 자리). 서러게이트와 U+10FFFF 초과는 거부

(4) 값은 두 단계로 만든다.

  1. 리터럴을 먼저 바이트열로 만든다. 적힌 글자는 그 UTF-8 바이트가, \xNN 은 그 바이트가, \u·\U 는 그 코드포인트의 UTF-8 바이트가 된다.
  2. 접두사가 원소를 정한다. 접두사가 없으면 바이트가 원소다. u 는 바이트열을 UTF-8 로 풀어 UTF-16 코드 유닛으로, U 는 코드포인트로 센다. 풀 수 없으면(예: u"\xE9") 거부한다(E-LOWS-ESCAPE).

(5) 문자 리터럴은 (4) 로 만든 원소가 정확히 하나여야 한다. 없으면 E-LOWS-CHAR-EMPTY, 둘 이상이면 E-LOWS-CHAR-WIDTH 다. 'é' 는 바이트 둘이라 거부되고 u'é' 는 된다.

(6) 접두사 없는 문자열은 UTF-8 이라는 보장이 없는 바이트열이다("\xE9", "a\0b"). 텍스트가 필요한 읽는 쪽은 UTF-8 검사를 한다. C 로 읽을 때는 NUL 로 끝을 찾지 않는다.

3.5 heredoc

(1) text [u|U] TAG 로 시작해, 앞뒤 공백·탭을 빼면 TAG 만 남는 줄에서 끝난다. TAG 는 이름이다. 여는 줄에 다른 것이 있으면 E-LOWS-TAG, 접두사 자리에 u·U 밖의 이름이 있으면 E-LOWS-PREFIX, 닫는 줄이 없으면 E-LOWS-UNCLOSED 다.

(2) 본문은 여는 줄 다음 줄부터 닫는 줄 앞 줄까지이며, 줄 사이를 LF 로 잇는다. 마지막 줄 뒤의 LF 는 넣지 아니한다. 이스케이프를 풀지 아니한다. 값은 3.4 (4) 의 2 단계를 거친다.

3.6 윈도우 경로

(1) 한 줄짜리 값의 역슬래시는 \\ 로 적는다: windir "C:\\Windows" .

(2) (참고) "C:\temp\new" 는 거부되지 않는다. \t·\n 이 집합 안에 있어 탭과 줄바꿈이 된다. (1) 을 지키면 생기지 않는다.

4. 문장과 데이터 모델

4.1 문법

file   = { stmt } ;
stmt   = path , ( "do" , stmt , { stmt } , "end" | { value } , "." ) ;
path   = NAME , { NAME } ;
value  = NUMBER | "true" | "false" | STRING | CHAR | TEXT ;

(1) 경로는 이름들이고 값은 리터럴들이다. 첫 리터럴에서 경로가 끝난다. 값 뒤에 이름이 오면 거부한다(E-LOWS-ORDER).

(2) 문장은 이름으로 시작한다(E-LOWS-PATH). do 없는 end 는 E-LOWS-END, 닫히지 않은 문장·블록은 E-LOWS-UNCLOSED 다.

(3) a b c 1 . 과 a do b do c 1 . end end 는 같은 나무를 만든다.

4.2 나무

(1) 문서는 나무 하나다. 가지는 이름 → 자식이고, 자식의 순서는 소스 순서다. 잎은 값의 목록이다.

(2) 한 잎의 값은 모두 한 종류다(E-LOWS-MIX). 종류는 아홉이다.

종류리터럴원소
int42 -5 0xFF−2⁶³ … 2⁶⁴−1
float1.5 0x1p3binary64, 유한
booltrue false
str"…", text TAG바이트
u_stru"…", text u TAGUTF-16 코드 유닛
U_strU"…", text U TAG코드포인트
char · u_char · U_char'a' · u'é' · U'😀'원소 하나

int 와 float 도 다른 종류다 — 1 2.0 은 섞인 것이다.

(3) 값이 하나인 잎이 스칼라다. 값이 없는 잎(flag .)은 빈 목록이며, 그 키가 있다는 표시다. 구현은 이 잎의 종류를 empty 로 보인다.

(4) null 과 날짜 타입은 없다. 목록 안의 목록, 목록 안의 표도 없다 — 같은 종류 여럿은 이름 붙인 블록으로 적는다.

remote origin do
  url "https://example.invalid/r.git" .
end
remote backup do
  url "https://example.invalid/b.git" .
end

4.3 한 경로에는 한 작성자

규칙진단
같은 잎을 두 번 쓸 수 없다E-LOWS-DUP
잎이면서 가지일 수 없다 (a 1 . 과 a b 2 .)E-LOWS-SHAPE
블록으로 연 경로는 그 블록만 쓴다. 다시 열 수도, 밖에서 평평한 문장으로 더할 수도 없다E-LOWS-SEALED
이미 있는 경로를 블록으로 열 수 없다E-LOWS-SEALED
빈 블록E-LOWS-EMPTY
블록은 64 겹까지 겹친다. 65 번째 do 는 거부한다E-LOWS-DEPTH

평평한 문장끼리는 앞부분을 나눠 써도 된다(package name … / package version …).

5. 진단

(1) 거부는 진단 하나로 알린다: 줄(1 부터), 열(1 부터, 코드포인트 단위), 코드, 문장. 코드는 안정된 이름이다. 문장은 판마다 바뀔 수 있다.

(2) 처리기는 다음 순서로 읽고, 처음 만난 잘못 하나를 알린다.

  1. 파일 전체의 UTF-8 → BOM → CR (2 절).
  2. 파일 전체의 어휘(3 절) — 그러므로 어휘 잘못은 그보다 앞에 있는 구조 잘못보다 먼저 알린다.
  3. 문장과 나무(4 절), 소스 순서대로.

(3) 위치는 다음 자리다.

코드자리
E-LOWS-UTF8 · E-LOWS-CR · E-LOWS-CHARSET그 바이트·글자
E-LOWS-BOM1:1
E-LOWS-ESCAPE (이스케이프 모양·코드포인트)그 역슬래시
E-LOWS-ESCAPE (UTF-8 로 못 풂) · E-LOWS-UNCLOSED(리터럴) · E-LOWS-CHAR-* · E-LOWS-PREFIX(리터럴) · E-LOWS-NUMBER · E-LOWS-RANGE그 리터럴·수의 첫 글자(접두사·부호 포함)
E-LOWS-TAG · E-LOWS-PREFIX(heredoc) · E-LOWS-UNCLOSED(heredoc) · heredoc 값의 E-LOWS-ESCAPEnote·text 낱말
E-LOWS-ORDER값 뒤에 온 이름
E-LOWS-UNCLOSED(문장)마침표가 와야 할 자리의 토큰. 파일이 끝났으면 문장의 첫 이름
E-LOWS-UNCLOSED(블록)파일의 마지막 토큰
E-LOWS-EMPTY · E-LOWS-DEPTH그 do
E-LOWS-PATH · E-LOWS-END그 토큰
E-LOWS-MIX · E-LOWS-DUP · E-LOWS-SHAPE · E-LOWS-SEALED문장의 첫 이름

(4) 코드 전체: UTF8 BOM CR CHARSET UNCLOSED ESCAPE PREFIX TAG CHAR-EMPTY CHAR-WIDTH NUMBER RANGE PATH END ORDER MIX DUP SHAPE SEALED EMPTY DEPTH (각각 앞에 E-LOWS-).

6. 구현이 값을 보이는 모양 (참고)

종류C (lowstruct.h)Node.jsPython
int{magnitude, negative} · lows_get_i64/u64BigIntint
floatdoubleNumberfloat
boolboolBooleanbool
str바이트 포인터 + 길이Uint8Arraybytes
u_str · U_stru16·u32 포인터 + 길이수의 배열수의 리스트
char 셋u32Numberint

부록 A. 로우엔트와의 관계

  • lowstruct 는 로우엔트와 문법을 공유하는 별개의 형식이다. 저장소·명세·판·진단·일정이 따로 있고, 이 문서만이 lowstruct 의 규범이다. 로우엔트 문서는 출처일 뿐 규범이 아니다.
  • 리터럴(3.3–3.5)은 2026-09-26 의 로우엔트 명세(언어 개정 1.3) §6.1.4·부록 A.6 과 같게 고정했다. 로우엔트가 뒤에 리터럴을 바꿔도 lowstruct 는 따라 바뀌지 아니한다 — 들이려면 lowstruct 의 새 판으로 정한다. 판 0.0.1 에서 같은 리터럴이 두 곳에서 다른 값을 내면 결함이다. (참고) 판 0.0.1 을 만들며 한 차분 검사에서 lowentc 가 0x 를 0 으로 받던 결함이 드러났고 로우엔트 쪽에서 고쳤다.
  • 로우엔트 패키지 매니페스트 pkg.low 는 고치지 않고 lowstruct 로 읽힌다(사례 08-lowent-pkg).
  • make point do x 1 . y 2 . end 의 몸통은 lowstruct 블록과 같은 모양이다. 다음 판의 스키마(부록 D)의 출발점이다.
  • lowstruct 파일은 로우엔트 프로그램이 아니다.

부록 B. 적합성

(1) conformance/accept/*.lows 는 받아들여야 하며, 부록 C 의 덤프가 같은 이름의 .dump 와 바이트 단위로 같아야 한다.

(2) conformance/reject/*.lows 는 거부해야 하며, 코드가 파일 첫 줄 rem expect E-LOWS-… 와 같아야 한다.

(3) 적합한 구현은 (1)·(2) 를 모두 통과한다. 위치(5 (3))는 세 구현이 차분 퍼즈(tools/difffuzz.py)로 서로 맞춘다.

부록 C. 정규 덤프

구현을 서로 견주려고 쓰는 줄 단위 표현이다. 부동소수를 글자로 옮기는 방법이 언어마다 달라, 비트를 적는다.

lowstruct-dump 1
<경로>\t<종류>\t<값> <값> …
  • 잎마다 한 줄, 나무를 소스 순서로 깊이 먼저 돈다. 경로는 이름을 . 으로 잇는다.
  • 값: int 는 십진(음수는 -), float 는 binary64 비트를 큰 끝 순서 십육진 16 자리(소문자), bool 은 true/false, str 은 x: 뒤에 바이트 십육진(소문자), u_str·U_str 는 [65,66] 처럼 십진을 쉼표로 이은 것, 문자 셋은 십진. 값이 없으면 종류는 empty 이고 값 칸은 비어 있다.
  • 파일은 LF 로 끝난다.

부록 D. 다음 판으로 미룬 것

  1. 로우엔트 struct 를 스키마로 쓰기. make … do … end 몸통 = lowstruct 블록. 모든 칸 채움 = 필수 키 검사, 선언에 없는 칸 = 오타 검사, 칸 타입 = 값 범위·종류 검사. 목록 ↔ slice, 이름 붙인 블록 ↔ 맵, 빈 잎 ↔ bool/option 의 대응을 정해야 한다.
  2. 이름 붙은 값(심벌). level debug . 는 지금 level 가지 아래 debug 잎이다. be 로 경계를 밝히는 안이 있다.
  3. 블록 봉인(4.3)을 풀지. 판 0.0.1 은 엄격한 쪽이다.
  4. 정규형 포매터. 한 줄 값의 역슬래시는 언제나 \\ 로 낸다.
  5. 경로 모양 문자열 경고. 드라이브 문자나 \\server 로 시작하는 문자열 안의 \t·\n 등을 경고하는 안.