SmallLang이라는 작은 언어를 만들기 시작했습니다

안녕하세요. 이번에는 SmallLang이라는 작은 언어를 사부작 만들고 있습니다.

아직은 정말 작습니다. 언어라고 부르기엔 조금 민망할 정도로요… ^^;

그래도 이제 첫 문법이 조금 더 SmallLang다워졌습니다.


getName: -> Text {

"dimohy"

}

square: Int -> Int {

it * it

}

main {

getName -> name

7 -> square -> num

"Hello, {name}. square = {num}" -> print

}

출력은 이렇습니다.


Hello, dimohy. square = 49

그리고 이 코드는 LLVM IR로 내려간 뒤 Windows x64 네이티브 실행 파일로 링크됩니다. 현재 생성된 exe 크기는:


1,088 bytes

헐… 아직 기능이 작긴 하지만, 실제 실행 파일까지 만들어지는 걸 보면 재미있습니다.

문법은 어디로 가고 있나?

처음부터 정하고 싶었던 것은 let이나 var 없이도 자연스럽게 읽히는 바인딩이었습니다.


name = "dimohy"

여기에 최근에는 값의 흐름을 더 잘 보이게 하는 -> 문법을 넣었습니다.


getName -> name

7 -> square -> num

"Hello, {name}. square = {num}" -> print

getName -> namegetName 함수를 호출해서 그 결과를 name에 바인딩합니다.

7 -> square -> num은 7이라는 값이 square 함수로 흘러가고, 그 결과가 num에 바인딩됩니다.

괄호로 쓰면 num = square(7) 같은 형태도 가능하지만, SmallLang에서는 일단 "값이 어디로 흘러가는지"가 눈에 보이는 쪽을 선호해보려고 합니다.

함수 타입도 같은 방향성을 갖습니다.


square: Int -> Int {

it * it

}

지금은 입력이 하나인 함수에서 그 입력을 암묵적으로 it으로 받습니다. 그래서 it * it이 됩니다. 조금 과감한 선택이긴 한데, 작은 함수에서는 꽤 단순하게 읽히네요.

문자열과 숫자

문자열 보간은 가장 작은 형태로 시작했습니다.


"Hello, {name}. square = {num}"

아직은 {name}처럼 바인딩 이름을 넣는 정도입니다. 복잡한 표현식을 문자열 안에 바로 넣는 건 조금 더 생각해보려고 합니다. 너무 빨리 다 넣으면 언어가 금방 무거워지더군요.

숫자는 현재 64-bit 정수 리터럴과 +, *를 지원합니다. *+보다 먼저 묶이고, 둘 다 왼쪽 결합입니다.

내부는 어떻게 되어 있나?

컴파일러는 최신 .NET과 C# Preview 기준으로 만들고 있습니다.

처음에는 빠르게 만들다 보니 Program.cs에 Lexer까지 들어갔는데… 아아… 이건 바로 정리했습니다. 지금은 역할별로 나누어 두었습니다.

  • Cli

  • Lexing

  • Parsing

  • Syntax

  • Semantics

  • CodeGen

  • Tooling

Lexer와 Parser도 손으로 직접 박아두지 않고, 작은 규칙 파일에서 소스 생성기로 만들어지게 했습니다.

Lexer 규칙은 이런 식입니다.


token Identifier = identifier

token String = quoted_string

token Number = number

token Plus = "+"

token Star = "*"

token Arrow = "->"

token Colon = ":"

token Equal = "="

Parser 규칙도 별도 파일에 둡니다.


rule FunctionDeclaration = Identifier Colon FunctionSignature LeftBrace NewLine* Expression NewLine* RightBrace

rule FunctionSignature = Arrow TypeName | TypeName Arrow TypeName

rule FlowExpression = AdditiveExpression (Arrow Path)*

rule AdditiveExpression = MultiplicativeExpression (Plus MultiplicativeExpression)*

rule MultiplicativeExpression = PrimaryExpression (Star PrimaryExpression)*

Roslyn incremental source generator가 syntax/smalllang.lexer, syntax/smalllang.grammar를 읽고 Lexer, TokenKind, Parser를 생성합니다.

문법을 표현하는 곳과 C# 구현 코드를 분리하고 싶었습니다. 언어를 만드는 중인데, 언어 문법이 C# 코드 안에 너무 묻혀 있으면 조금 아쉽더군요.

실제로는 어떻게 실행되나?

현재 파이프라인은 이렇습니다.


SmallLang source

-> generated lexer

-> generated parser

-> AST

-> semantic analysis

-> LLVM IR

-> clang object

-> lld-link exe

LLVM은 저장소에 넣지 않습니다. 빌드 스크립트가 없으면 .tools 아래로 내려받고, 도구와 산출물은 Git에 들어가지 않게 했습니다.

이번 샘플은 컴파일 시점에 전체 문자열 하나로 접어버릴 수도 있습니다. 하지만 일부러 런타임 함수 호출이 보이도록 했습니다.

생성된 LLVM IR에는 이런 형태가 들어갑니다.


define internal i64 @smalllang_fn_square(i64 %it) #0 {

entry:

%mul0 = mul nsw i64 %it, %it

ret i64 %mul0

}

그리고 main 쪽에서는 실제로 이렇게 호출합니다.


%call4 = call i64 @smalllang_fn_square(i64 7)

출력은 아직 print 하나입니다. 표면에서는:


"Hello, {name}. square = {num}" -> print

이지만 내부적으로는 문자열 조각을 UTF-8 정적 데이터로 두고, 정수는 런타임에서 십진수로 변환해 WriteFile로 바로 씁니다. CRT는 붙이지 않았습니다.

아직 남은 것들

아직 정해야 할 게 많습니다.

타입 시스템, 모듈 시스템, 메모리 모델, 크로스 플랫폼 출력, 오류 처리, 문자열 escape, println 여부…

후덜덜하네요 ^^;

그래도 첫 느낌은 괜찮습니다. 아주 작은 문법에서 시작해서, 직접 토큰화하고, 파싱하고, LLVM으로 내려가고, 실제 실행 파일이 만들어지는 흐름까지는 연결됐습니다.

다음은 문법을 조금씩 키워보면서도 "단순함"을 잃지 않는 쪽으로 가보려고 합니다.

가장 작은 언어가 어디까지 아름다워질 수 있을까요?

불타보아요… ^^

10 Likes

■■■■■(에스렝) 글자가 감춰지네요;;

4 Likes

Small Language를 의도하신 것 같긴 하지만, 그렇게 읽히기 보다 (잘 아시겠지만) 아래처럼 읽힐 확률이 높은 네이밍입니다. :sob:

https://en.dict.naver.com/#/entry/enko/de64f24500b5455a96b824263042bdca

번거로우시겠지만 오해를 피하려면 SmallLang 정도로 이름을 고치시는 것은 어떨지 검토해보시면 좋을 것 같습니다. 아니면 SLang이 Slang이 아니라는 것을 설명하기 위한 노력이 많이 필요하실 수 있을 것 같아요. ㅠㅠ

6 Likes

언어의 경계가 LLM에 의해 모호해 지는 상황에서 역설적으로 새로운 언어를 만드는 것 또한 즐거운 일 같습니다. 한번 해보고 싶어요 :rofl:

4 Likes

SmallLang으로 이름을 변경해야겠습니다.

3 Likes

사족입니다만 이제 이런 새로운 언어를 창작하는 것 마저도 AI의 도움을 받을 수 있게 될테니 재미난걸 더 많이 해볼 수도 있겠네요! Lex/Yacc 문법 공부하는게 그렇게 어려웠는데. ㅋㅋ

3 Likes

n = ? 입력 후 구구단 출력

main {
    "n = ? " -> readInt -> n

    each i in 1..9 {
        n * i -> value
        "{n} x {i} = {value}" -> println
    }
}

반복문을 이렇게 쓸 수 있게도 했습니다.

    1..9 -> each {
        n * it -> value
        "{n} x {it} = {value}" -> println
    }
n = ? 4
4 x 1 = 4
4 x 2 = 8
4 x 3 = 12
4 x 4 = 16
4 x 5 = 20
4 x 6 = 24
4 x 7 = 28
4 x 8 = 32
4 x 9 = 36

image

2 Likes

저랑 비슷한 작업을 하고 계시네요!
(다만 저는 실무적으로 ..)

저도 요즘 이직 후 표현식을 만들고 런타임시 평가 할 수 있는 자체 익스프레션 엔진을 만들고 있습니다.

피지컬 AI 로봇 관련 백앤드를 담당하고 있는데

로봇을 구동하기 위한 방식으로 여러가지 [블록]을 배치해서 구동 시키는데,
블록의 속성중 Expression 을 넣고 평가해서 구동 되도록 하고 있습니다.

처음엔 ai 없이 일단 기본적인 구조를 만들자 해서
신텍스 검증 → 평가식 토큰으로 파싱 → 토큰을 ATS 처리 일일히 하다가

실제 언어 처럼 자체 함수와 if문, for문, 비트 연산 등 여러가지 요구사항이 생기면서 AI에 100% 의존해서 하고 있습니다..ㅋㅋ

어떤 언어로 발전될지 기대 되는군요!
지켜봐 보겠습니다 ㅎㅎㅎ

6 Likes

안녕하세요. 이번에는 SmallLang에 WebAssembly 타깃을 붙여봤습니다.

처음에는 Windows x64 실행 파일을 만들고, 그 다음에는 WSL을 통해 Linux x64까지 붙였는데요.
이번에는 LLVM에서 WASM으로 내려가서 브라우저에서 실행되는 흐름까지 연결했습니다.

아직은 아주 작은 단계입니다. 그래도 SmallLang 코드가 .wasm이 되고, 그걸 브라우저가 직접 실행해서 화면에 출력하는 흐름까지 보니 꽤 재미있네요.

이번 샘플

새로 추가한 예제는 이렇습니다.

title: -> Text {
    "SmallLang WebAssembly"
}

square: Int -> Int {
    it * it
}

main {
    title() -> runtimeName
    8 -> square() -> value
    1..5 -> fold 0 sum, i {
        sum + i
    } -> total

    "Hello from {runtimeName}" -> println()
    "8 squared = {value}" -> println()
    "1..5 sum = {total}" -> println()
}

출력은 이렇게 나옵니다.

이제 이 출력이 콘솔 exe만이 아니라 브라우저 화면에서도 나옵니다.

어떻게 붙였나?

파이프라인은 기존 LLVM 흐름을 최대한 그대로 사용했습니다.

SmallLang source
-> Lexer / Parser
-> Semantic lowering
-> LLVM IR
-> clang wasm32 object
-> wasm-ld
-> .wasm
-> browser

새 타깃 이름은 wasm32-browser입니다.

.\scripts\smalllang.ps1 `
  -Source examples\23-webassembly-browser.sl `
  -Output artifacts\23-webassembly-browser.wasm `
  -Target wasm32-browser `
  -KeepTemps

브라우저 쪽은 아주 단순하게 만들었습니다.

WASM 모듈은 smalllang_startmemory를 export하고, 출력은 브라우저가 제공하는 import 함수로 넘깁니다.

env.smalllang_browser_write(ptr, len)

JavaScript는 이 포인터와 길이로 WASM linear memory에서 UTF-8 바이트를 읽고, 화면의 <pre>에 붙입니다.

뭔가 거창한 런타임을 넣은 것은 아니고요.
지금은 stdout에 해당하는 가장 작은 경계만 만든 셈입니다.

브라우저에서 실행

정적 파일 서버만 있으면 됩니다.

python -m http.server 5080

그리고 브라우저에서 열면 됩니다.

http://localhost:5080/examples/browser/

이번에 실제로 확인한 .wasm 크기는 622 bytes였습니다.

물론 아직 기능이 작아서 가능한 크기입니다. 앞으로 런타임이 커지면 당연히 달라지겠죠. 그래도 첫 브라우저 실행 결과로는 나쁘지 않은 출발입니다.

일부러 하지 않은 것

이번에는 readInt를 브라우저 prompt로 억지 매핑하거나, 파일 API를 LocalStorage 같은 것으로 슬쩍 바꾸지는 않았습니다.

그런 식의 fallback은 나중에 원인을 숨기기 쉽더군요.

그래서 현재 브라우저 타깃은 출력만 명확히 지원합니다. 입력이나 파일 런타임은 일단 명시적으로 실패하도록 두었습니다. 나중에 브라우저용 런타임 모델을 정하면 그때 제대로 붙이는 쪽이 좋겠다고 봤습니다.

확인한 것

이번 변경은 이렇게 확인했습니다.

dotnet build SmallLang.slnx
dotnet run --project tests\SmallLang.ExampleTests\SmallLang.ExampleTests.csproj --no-build

예제 테스트는 12개 모두 통과했습니다.

그리고 Node에서 직접 WASM을 instantiate해서 exit=0과 출력도 확인했고, Chrome headless로 실제 브라우저 페이지 DOM에 출력이 들어오는 것도 확인했습니다.

작은 언어를 만들고 있는데, 어느새 같은 코드가 native exe도 되고, Linux ELF도 되고, 브라우저 WASM도 되네요.

아직 갈 길은 멉니다.
그런데 이런 작은 연결점들이 하나씩 생기는 게 꽤 즐겁습니다.

SmallLang이 어디까지 작고 단순하게 갈 수 있을지 계속 이어가보겠습니다.

3 Likes

바이브 코딩이 되니 아주 빠르게 문법이 정리되고 있습니다. 아무리 생각해도 함수를 func()로 표현하는게 올드한 것 같아서… ->는 호출, =>는 바인딩으로 통일하니 좀더 깔끔해졌습니다.

(5, 4) -> sum => result

이제 이런 형태가 가능해지는거죠… 그런데 왜 고전적인 result = sum(5,4) 형태를 안썼냐고요? 왼쪽에서 오른쪽으로 흘러가는 자연스러운 코딩 형태를 원했기 때문입니다. 그게 문법적으로 가능하면 C#이나 Java에서 조금 어거지 스러운 Fluent API 표현이 언어적으로 자연스럽게 파이프라인으로 흘러가게 될 테니까요.

2 Likes

SmallLang 컨테이너 성능을 한번 재봤습니다

안녕하세요. 디모이입니다.

SmallLang에 배열과 사전 자료구조를 넣고 나서, 어느 정도 성능이 나오는지 궁금해서 간단한 벤치마크를 돌려봤습니다.

아직 언어도 구현도 계속 만드는 중이라 절대적인 결론을 내릴 단계는 아니지만, 생각보다 재미있는 결과가 나왔네요.

이번에 비교한 대상은 SmallLang, C#, Go, Rust(hashbrown)입니다.
같은 머신에서 5번씩 실행하고 중앙값을 기준으로 정리했습니다.

테스트한 내용은 다음과 같습니다.

  • 동적 배열에 1..50,000,000 값 추가
  • 완성된 배열을 20번 순회하면서 합산
  • 사전에 0..8,000,000 정수 key/value 삽입
  • 완성된 사전에서 1..8,000,000 key lookup 후 합산

결과는 이렇습니다.

항목 SmallLang C# Go Rust hashbrown
배열 build 250ms 253ms 462ms 249ms
배열 scan 344ms 542ms 512ms 386ms
사전 build 188ms 156ms 1,187ms 357ms
사전 lookup 94ms 55ms 259ms 150ms

처음 사전 구현은 훨씬 단순했습니다.
그때는 사전 build가 422ms, lookup이 234ms 정도였는데, 몇 가지 최적화를 적용한 뒤 build는 188ms, lookup은 94ms까지 내려왔습니다.

이번에 적용한 최적화는 대략 이런 것들입니다.

  • power-of-two capacity를 이용한 bitmask probing
  • 정수 key에 맞춘 가벼운 hash
  • lookup 전용 probe 경로
  • control byte 초기화에 LLVM memset 사용
  • grow threshold 조정
  • Windows native 빌드 -O3 적용

C# Dictionary<long,long>은 여전히 빠릅니다.
특히 lookup은 C#이 55ms라서 아직 차이가 꽤 있네요.

그래도 SmallLang 쪽도 현재 구현 수준을 생각하면 나쁘지 않은 출발인 것 같습니다.
지금 사전은 SwissTable 느낌의 control byte와 h2 fingerprint는 쓰고 있지만, 아직 group probing까지는 들어가지 않았습니다. 그래서 다음 단계는 Abseil, Go, hashbrown 쪽에서 쓰는 것처럼 control byte를 그룹 단위로 비교하는 구조가 될 것 같습니다.

SmallLang은 GC 없이, 소유권 기반으로 메모리를 관리하는 방향을 잡고 있습니다.
그래서 단순히 “돌아간다”에서 끝나는 게 아니라, 안전한 메모리 모델 안에서 성능도 같이 가져갈 수 있는지가 중요한데요.

이번 결과는 그 방향이 꽤 가능성이 있다는 신호처럼 보여서 기록으로 남겨둡니다.

벤치마크와 최적화 기록은 여기 정리해두었습니다.

6 Likes

SmallLang에서 Sollang이 되기까지

안녕하세요. 디모이입니다.

한동안 작은 프로그래밍 언어를 하나 만들고 있었습니다. 처음 이름은 SmallLang이었는데요. 문법 몇 가지를 실험하는 작은 언어로 시작했지만, 하나씩 실제 동작으로 옮기다 보니 어느새 LLVM으로 Windows와 Linux 네이티브 실행 파일을 만들고, 언어 자신으로 작성한 컴파일러가 다시 자기 자신을 컴파일하는 단계까지 왔습니다.

그래서 지금은 이름도 Sollang으로 바꾸었습니다. 소스 확장자도 .sl에서 .slg로 바뀌었습니다.

이 글에서는 SmallLang이 어떻게 Sollang이 되었는지, 지금까지 무엇을 실제로 구현했는지, 그리고 가장 최근에는 어떤 문제를 풀고 있는지 한 번 정리해 보려고 합니다.

왜 Sollang인가

Sollang의 Sol에는 한 가지가 아니라 네 가지 의미를 담았습니다.

첫 번째는 태양입니다. 코드가 햇빛처럼 의도를 잘 드러냈으면 합니다. 값이 어디에서 와서 어디로 가는지, 누가 소유하는지, 오류가 왜 생겼는지를 가능하면 숨기지 않는 언어를 만들고 싶었습니다.

두 번째는 음악의 음이름인 입니다. 비슷한 개념은 비슷한 모양으로 표현되고, 코드가 앞에서 뒤로 자연스럽게 읽히는 리듬이 있었으면 합니다.

세 번째는 Solution입니다. 문법 자체가 멋있어 보이는 데 그치지 않고, 복잡한 문제를 실제로 더 단순하게 풀어 주는 언어여야 한다고 생각합니다.

네 번째는 S·O·L, 즉 Simple, Original, Logical입니다. 단순한 형식으로 독창적인 생각을 표현하되, 그 규칙은 논리적으로 조합되어야 한다는 뜻입니다.

이 네 가지 중 하나만 대표 철학으로 고른 것은 아닙니다. 밝고, 조화롭고, 문제 해결에 도움이 되며, 창작자의 생각을 담을 수 있는 언어를 함께 지향하고 있습니다.

Sollang 문법의 중심

Sollang은 값을 왼쪽에서 오른쪽으로 흘려보내는 표현을 좋아합니다.

getName: -> Text {
    "dimohy"
}

square value: Int -> Int {
    value * value
}

main {
    getName => name
    7 -> square => squared
    "Hello, $name. square = $squared" -> println
}

->는 값을 함수나 연산으로 보내는 흐름이고, =>는 계산된 값을 이름이나 대상에 연결하는 바인딩입니다.

입력이 없는 함수는 getName()처럼 호출하지 않고 getName으로 사용합니다. 입력이 없는데 빈 괄호를 반복할 필요가 없다고 보았고, 지금은 getName()을 컴파일 오류로 처리합니다.

조건과 패턴 매칭도 같은 방향으로 읽힙니다.

result -> when {
    Ok(value) => value
    Err(error) => 0
} => answer

여기서는 result의 타입을 알고 있으므로 Result<Int, Text>.Ok(value)처럼 타입 이름을 반복하지 않습니다. OkErr를 문맥에서 추론합니다.

범위와 컬렉션은 컴파일 시점에 펼칠 수도 있습니다.

[1..10] => numbers
[1..10 -> each { it + 1 }] => incremented
{1..3 -> each { it: it * 10 }} => lookup

결과를 컴파일 시점에 알 수 있다면 런타임 루프를 만들기보다 평범한 배열이나 사전 리터럴로 변환합니다.

지금까지 구현된 언어 기능

처음에는 함수와 출력부터 시작했지만, 현재는 다음 영역까지 실제 컴파일러와 누적 예제로 검증하고 있습니다.

문법과 제어 흐름

  • main 블록과 최상위 실행문
  • value -> function => result 형태의 흐름 호출과 바인딩
  • 로컬 함수와 구조체 안의 중첩 구조체
  • 표현식 기반 if, when, 범위 패턴, 간결한 guard와 break/continue
  • each, fold, repeat, parallel, tryParallel 같은 블록 함수
  • # 한 줄 주석과 공통 들여쓰기를 제거하는 """ 여러 줄 문자열
  • $name, $(expression) 문자열 보간
  • 문맥으로 타입을 추론하는 Ok, Err, Some, None 패턴
  • 인자가 없는 함수를 속성처럼 사용하는 괄호 없는 호출

타입과 제네릭

  • 구조체, 열거형, trait, associated type과 타입 동등성 제약
  • 배열처럼 보이지 않는 <T, R, E> 타입 제네릭
  • <N: Int> 형태의 컴파일 타임 값 제네릭
  • 고정 배열 [T; N], 가변 배열 [T; ~], 사전 {K: V}
  • Option<T>Result<T, E>, postfix ? 오류 전파
  • 구조적으로 추론되는 로컬/private 함수 입력·반환 타입
  • Int8, Int16, Int32, Int64와 대응하는 unsigned 타입
  • Float32, Float64, 안정적인 기본 별칭 IntFloat
  • Raspberry Pi Pico 같은 타깃에서도 의미가 흔들리지 않도록 한 고정 폭 기본 타입과, 포인터 크기를 따르는 Size/UIntSize
  • UTF-8 Text와 Unicode CodePoint

배열과 사전

  • 값 타입 전반에 적용되는 제네릭 배열과 Swiss-table 방식 사전
  • 구조체 키를 포함한 정적 hash/equality witness 특수화
  • 배열/사전의 each, 변환, 조회, 갱신과 소유권 이전
  • data![index] = valuevalue => data![index] 두 형태의 인덱스 대입
  • 문맥으로 원소 타입을 알 수 있을 때 구조체 이름을 반복하지 않는 리터럴
  • 인덱스의 타입도 문맥에서 알 수 있으면 SymbolKey 같은 키 타입 이름을 생략하는 표현
  • 컴파일 타임 범위와 변환으로 배열·사전 리터럴을 미리 생성하는 기능

메모리와 소유권

Sollang은 기본 GC를 두지 않고, 값의 소유권과 파괴 시점을 컴파일러가 정적으로 다루는 방향입니다.

  • 작은 non-escaping readonly 배열과 사전은 자동으로 스택에 올리고, 크기나 생명주기상 필요할 때 힙으로 전환
  • 함수 진입 시 계획된 스택 슬롯을 분기와 반복문에서 재사용
  • !가 붙은 이름으로 가변 소유자를 구분
  • readonly view, mutable borrow, 명시적인 movetake
  • box를 통한 명시적 힙 소유와 재귀 구조체 drop glue
  • 배열·사전·구조체 필드의 부분 move, 재초기화, 정확히 한 번의 파괴
  • 소유 배열/사전 원소 교체 시 이전 값을 먼저 파괴하고 새 소유자를 이전
  • 빌린 Text가 원본보다 오래 살아남지 않도록 origin과 사용 구간을 추론
  • 서로 다른 구조체 필드와 서로 다른 상수 인덱스를 구분하는 projected-place borrow 분석
  • 안전한 코드에서는 raw pointer를 직접 노출하지 않으면서도 큰 파일을 다루는 언어 수준 memory map
  • 위치 기반 byte arena와 affine 파일/작업 핸들

객체를 무조건 힙에 두거나 스택에 둔다는 규칙보다는, escape 여부와 크기, 소유권 이전 가능성을 보고 가장 싼 저장소를 선택하는 쪽을 지향합니다. 필요한 경우에는 명시적인 box로 힙 소유를 표현합니다.

비동기와 병렬 처리

  • async/await와 타입을 보존하는 Task<T>
  • OS thread 하나를 Task 하나에 대응시키지 않는 cooperative executor
  • tail await, 연속 await, if/when/while 안의 suspension state
  • yield, typed Duration, timer queue, sleep과 cancellation
  • cancellation 시 살아 있는 frame owner를 정확히 파괴하는 affine task
  • 비동기 파일 열기·읽기·쓰기·sync
  • Windows와 Linux가 공유하는 구조화된 실행 모델
  • 순서를 보존하는 parallel/tryParallel과 bounded native worker pool
  • worker 출력이 서로 섞이지 않도록 하는 메모리 출력 sink
  • 첫 실패 이전 결과만 보존하고 나머지 소유 결과를 정리하는 결정적 실패 처리
  • 병렬 callback이 가변 값이나 구조적으로 전송할 수 없는 값을 안전하지 않게 캡처하지 못하도록 하는 정적 검사
  • uses Console, File, Clock, ... 형태의 명시적 effect capability

파일, 프로세스와 큰 데이터

  • Windows/Linux 네이티브 파일 owner와 결정적 close
  • readAt<T>, writeAt<T>, 비동기 random-access I/O
  • shell 문자열을 거치지 않는 argv 기반 자식 프로세스 실행
  • OS의 명령행 인자를 읽기 전용 host view로 제공
  • 환경 변수에서 “없음”과 “빈 문자열”을 구분
  • mmap/file mapping을 언어 문법에 가까운 형태로 제공
  • 매우 큰 데이터도 전체 복사를 전제로 하지 않고 bounded view와 affine owner로 처리

모듈, 프로젝트와 패키지

  • 여러 .slg 파일을 하나의 프로그램으로 컴파일
  • import 경로를 따라 파일을 자동 발견하고 순환/중복 모듈을 진단
  • import의 as를 생략하면 마지막 namespace를 기본 별칭으로 사용
  • 선언은 기본 internal, 필요한 API만 public
  • sollang.project의 root와 여러 product
  • SemVer 기반 로컬 패키지 의존성
  • 명시적인 sollang.workspace와 결정적인 workspace lock
  • 전체 revision으로 고정한 Git 의존성과 source-tree SHA-256 검증
  • checksummed ZIP을 사용하는 정적 registry index
  • --locked 재현 빌드와 canonical sollang.lock

빌드와 도구

  • C# reference compiler
  • Sollang으로 작성 중인 self-host compiler
  • 하나의 언어 규칙에서 lexer/parser 자료를 생성하는 source generator와 grammar VM
  • Windows x64, Linux x64 LLVM 네이티브 출력
  • 브라우저용 WebAssembly 출력
  • 현재 .slg 문법에 맞춘 VS Code syntax highlighting 확장
  • 테스트 진행 상황을 n/total로 계속 보여 주는 병렬 테스트 runner
  • 변경 계층에 따라 관련 회귀만 고르는 fast/affected suite
  • exact-input warm build에서 parsing, semantic analysis, LLVM emission, linking까지 건너뛰는 content-validated cache
  • 모듈 interface fingerprint, transitive invalidation, stable semantic identity
  • 변경되지 않은 함수 body의 semantic 결과와 LLVM codegen unit 재사용
  • 원자적 cache publication과 손상된 cache의 안전한 폐기

그리고 Sollang 0.1은 Windows x64와 Linux x64용 self-contained 패키지, 체크섬과 함께 공개되어 있습니다.

self-host는 어디까지 왔나

여기서 말하는 Stage는 다음 뜻입니다.

  • Stage 1: C# reference compiler가 Sollang으로 작성된 Sollang compiler를 빌드합니다.
  • Stage 2: 그렇게 만들어진 Sollang compiler가 다시 Sollang compiler와 실제 프로그램을 빌드합니다.
  • Stage 3: Stage 2 compiler가 다시 compiler를 만들었을 때 Stage 2와 같은 결과가 나오는지 확인합니다.

현재 Sollang으로 작성된 compiler는 lexer, parser/CST/AST, semantic, typed IR, ownership analysis, module cache, LLVM emitter로 나뉘어 있습니다. 한 번 빌드한 native self-host compiler를 재사용하도록 검증 구조도 바꾸어서, 매 테스트마다 거대한 compiler를 다시 해석하던 병목을 줄였습니다.

공식 roadmap은 기능을 60개의 auditable gate로 나눕니다. 최신 완료 기준점은 다음과 같습니다.

  • 49 complete, 8 partial, 3 missing
  • 가중치 기준 53/60, 88.3%
  • 남은 equivalent gate는 7개
  • 핵심 문법과 제어 흐름은 10/10
  • 모듈·visibility·build는 8/8
  • 타입·trait·generic은 11/12
  • compiler-construction primitive는 11.5/12

단순히 “컴파일된다”는 말로 완료 처리하지 않습니다. 각 gate는 누적 .slg 예제 또는 자동화된 compiler test가 있어야 완료로 계산합니다.

가장 최근에 한 작업

최근 작업의 중심은 소유권과 borrow analysis를 self-host compiler의 실제 production 진단으로 끌어올리는 일이었습니다.

직접 반환한 Text view가 어느 SourceText owner에서 왔는지 추론하는 것부터 시작해서 다음을 순서대로 구현했습니다.

  • 마지막 사용 지점까지만 borrow를 유지하는 non-lexical region
  • if, when, loop의 CFG를 따라가는 borrow liveness
  • 여러 입력 중 하나에서 반환될 수 있는 view의 union origin
  • mutable reference 재대입 때 현재 origin을 교체·병합하는 분석
  • 구조체·배열·사전 안에 들어간 borrowed Text의 aggregate origin
  • 서로 다른 field와 상수 index를 구분하는 projected place 충돌 분석
  • 부분 move 후 도달 가능한 use만 E17로 차단하는 production 진단
  • 병렬 callback 안의 local function call graph를 끝까지 따라가 mutable outer capture를 E18로 차단하는 분석

가장 최신 완료 checkpoint는 D213L입니다. parallel이나 tryParallel 본문이 local helper를 호출하고, 그 helper가 다시 다른 helper를 호출하더라도 바깥의 가변 binding을 숨길 수 없게 했습니다. 재귀 호출 그래프는 visited set으로 끝내고, 같은 binding을 여러 번 읽어도 callback당 진단은 한 번만 냅니다.

이 검사를 실제 self-host compiler에 켰더니 compiler 자신의 typed-IR parallelizer가 여섯 개의 큰 가변 table을 캡처하고 있었습니다. 검사를 약하게 만드는 대신, 그 table들을 복사하지 않고 move 기반 freeze helper로 불변 owner에 넘긴 뒤 worker가 readonly로 빌리도록 바꾸었습니다. 결과적으로 안전성 검사를 통과하면서 compiler 크기의 table을 추가 복사하지 않는 zero-copy 전환이 되었습니다.

최신 완료 기준점의 검증 결과는 다음과 같습니다.

  • Release build: warning 0, error 0
  • Windows 전체 예제: 666/666
  • Linux 전체 예제: 666/666
  • Windows Stage 2: 7/7
  • Linux Stage 2: 6/6
  • Stage 3: Stage 2와 byte-for-byte 동일
  • Stage 3 LLVM text: 11,840,360 bytes
  • SHA-256: E0B91E9140B90D04F3417926■■0C3B2BE38CF5B35EC975D119757B8C75C2BBF9

즉 C# compiler가 만든 경로만 통과한 것이 아니라, Sollang compiler가 만든 compiler로도 같은 안전성 진단과 LLVM 출력을 확인하고 있습니다.

아직 끝난 것은 아닙니다

88.3%는 꽤 멀리 왔다는 뜻이지, 언어가 완성되었다는 뜻은 아닙니다.

지금 다음 경계는 병렬 callback의 구조적 non-sendable capture인 E19를 local helper 호출 그래프까지 정확히 추적하고, branch나 loop가 부분 이동 상태로 빠져나가는 E20을 production 진단으로 완성하는 일입니다.

더 넓게는 다음 영역이 남아 있습니다.

  • ownership/storage의 남은 정밀도와 escape reference 규칙
  • generic과 compiler primitive의 마지막 빈 gate
  • 아직 partial인 표준 라이브러리 영역과 더 풍부한 진단
  • 실사용에 필요한 package/registry 운영 경험과 tooling 완성도
  • compiler 자체의 속도와 메모리 사용량을 계속 낮추는 일

특히 self-host compiler는 “동작하는 척하는 데모”가 아니라 실제로 자기 자신을 만들고, 그 결과가 다시 동일해지는지를 확인하는 쪽으로 진행하고 있습니다. 그래서 새 기능 하나를 넣을 때마다 C#과 Sollang 두 compiler의 결과를 비교하고, Windows와 Linux 전체 예제, Stage 2, 주기적인 Stage 3를 통과시킨 뒤 작은 checkpoint로 커밋하고 있습니다.

마무리

SmallLang은 정말 작은 문법 실험으로 시작했습니다. 그런데 간결한 문법을 고민하다 보면 타입 추론으로 이어지고, 타입을 진지하게 다루다 보면 소유권과 메모리로 이어지고, 거기까지 가면 결국 compiler가 자기 언어를 얼마나 정직하게 설명할 수 있는지를 보게 되는 것 같습니다.

그래서 Sollang으로 이름을 바꾼 것은 단순한 branding 변경이라기보다, 지금까지 쌓인 방향을 다시 선언한 일에 가깝습니다.

밝게 읽히는 코드, 리듬 있게 이어지는 표현, 실제 문제를 단순하게 푸는 도구, 그리고 Simple·Original·Logical한 창작의 언어.

아직 갈 길은 남았지만, 현재까지의 결과에는 만족하고 있습니다. 이 프로젝트는 GPT-5.6 Sol Medium과 함께 만들고 있으며, 그 협업 과정도 Sollang의 역사로 README에 기록해 두었습니다.

관심 있으신 분은 저장소의 examples부터 보시면 지금까지의 문법 변화와 compiler가 실제로 검증하는 범위를 순서대로 확인하실 수 있습니다. 의견이나 날카로운 지적도 환영합니다.

4 Likes