Goose는 동적 값을 컴파일러가 관리하는 데이터 스택에 인라인으로 저장하는 시스템 언어로, 힙 할당기, GC, 참조 카운팅, 수명 표기 없이 메모리 안전성을 제공함
배열이 커져도 기존 참조가 유지되며, 컴파일러가 참조의 소유 변수와 수명을 추론해 소유자보다 오래 살아남는 참조를 거부함
- 중첩 문자열과 가변 크기 레코드를
연속된 바이트 블록으로 저장하고, 작은 상대 오프셋으로 링크를 표현해 메모리 사용량과 간접 접근을 줄임
- 저장소의
16개 벤치마크에서 C++ 및 안전한 Rust보다 높은 속도와 적은 메모리 사용량을 기록했으나, 가변 크기 배열의 임의 접근 불가와 전체 프로그램 컴파일 등 제약이 있음
- 전체 프로그램을
C로 컴파일하며 TinyCC를 통한 프로세스 내 실행도 지원함. 아직 초기 단계인 프로젝트로, 사람이 설계한 언어를 바탕으로 저장소 대부분을 AI로 구현함
데이터 스택으로 관리하는 메모리
힙 없이 네이티브 호출 스택, 정적 데이터, 컴파일러가 개수를 결정하는 N개의 데이터 스택을 사용함
- 데이터 스택은 큰 주소 공간을 예약하고 범프 포인터로 사용 영역을 늘림
- 각 스택에서 살아 있는 크기 조절 가능 값은 최대 하나이며 항상 맨 위에 있으므로, 확장 시 데이터를 이동하거나 용량을 검사하지 않음
- 이 조건은 컴파일 시 증명하며, 런타임은 범프 포인터만 추적함
스코프 종료가 메모리 해제가 되며, 깊게 중첩된 백만 원소 구조도 스택 상단 포인터를 한 번 저장해 해제함
push
는 새 원소의 참조를 반환하며, 이후 배열에 원소를 추가해도 기존 참조가 유효함
나 Rust Vec<T>
와 달리, 배열을 구성하면서 원소끼리 직접 연결할 수 있음
, 안전한 Rust는 u32
인덱스를 사용하는 반면, Goose는 타입이 있는 참조를 유지함
수명 표기 없는 안전한 참조
- 모든 참조와 슬라이스에는 대상의 수명을 제한하는 변수인
정적 루트(root) 가 있음
- 컴파일러가 루트를 추론하고 이에 맞게 함수를 특수화함
- 소유 변수보다 오래 살아남거나 호환되지 않는 타입으로 대상에 접근하는 참조를 거부함
- 수명 검사 오류는 참조의 루트와 저장 목적지를 알려줌
**수명 문법, 별칭 제한, 배타성 규칙, **unsafe
가 없음
T[:]
슬라이스는 어떤 배열 종류에서든 원소 복사 없이 범위를 전달함
split
은 입력을 가리키는 슬라이스를 반환하며, u8[:]
를 키로 사용하는 사전은 문자열 복사본 대신 뷰를 저장함
중첩 데이터와 배열의 인라인 배치
- 배열은 모두
[metadata][elements...]
형태로 인라인 배치되며, 별도의 원소 블록을 가리키는 포인터가 없음
T[k]
: 컴파일 시 크기가 정해짐
T[]
, T[varint]
: 생성 시 크기가 정해지고 이후 고정되며, 길이를 고정 폭 또는 가변 폭으로 저장함
T[..k]
, T[..]
: 용량을 인라인으로 보유하고 그 안에서 늘거나 줄어듦
T[>..]
: 확장만 가능하며 내부 참조가 유지됨
T[>..<]
: 확장과 축소가 가능하며 스택, 큐, 힙 자료구조에 사용함
배열임
u8[>..]
는 문자열 빌더, u8[]
는 완성된 인라인 문자열, u8[..16]
은 구조체 내부의 작은 문자열, u8[:]
는 뷰로 사용함
- 구조체의 가변 크기 필드는 선언 순서대로 인라인 배치됨
- 예제 주문 레코드는 Goose에서
29바이트, 할당 0회임
- 대응하는 C++
std::string
과 std::vector<Item>
조합은 160바이트와 할당 1회, Rust String
과 Vec<Item>
조합은 153바이트와 할당 4회임
가변 크기 원소 배열은 순차 접근만 가능함
- 순회는 가능하지만 인덱스로 접근하려면 원소 참조를 보관하는 별도 배열이 필요함
두 가지 ADT 저장 방식
대수적 데이터 타입(ADT) 이 유일한 동적 다형성 수단이며, 상속과 가상 함수 테이블은 없음
- 저장 위치마다
고정 모드와 가변 모드를 선택함
Shape
같은 고정 모드는 태그와 가장 큰 페이로드를 담을 공간을 확보하며, 인덱스 접근과 덮어쓰기가 가능하지만 페이로드 내부를 참조할 수 없음
Shape..
같은 가변 모드는 실제 변형의 크기만 사용하며, 배열은 순차 접근만 가능하고 변형을 바꿀 수 없는 대신 페이로드 내부 참조가 가능함
- 예제의 원, 사각형, 점은 고정 모드에서 각각 17바이트를 쓰지만 가변 모드에서는 각각 9, 17, 1바이트를 사용함
케이스 함수(case functions) 는 변형별 오버로드를 정의하는 디스패치 방식임
대비 메모리 4분의 1, 속도 2배를 기록함
상대 참조와 재사용 풀
는 8바이트 머신 주소이며, 상대 참조는 타입 검사를 받는 링크를 1, 2, 4바이트 오프셋으로 저장함
T&<u32>
는 필드 자체에서 같은 배열의 대상까지 거리를 저장해 위치 독립적인 구조를 만듦
T&<u32 in pool>
은 지정한 풀의 시작점을 기준으로 하므로 다른 배열에서도 풀 내부를 연결할 수 있음
T&<u32>?
는 오프셋 0을 null로 사용함
나 ->
가 필요 없고, .=
로 참조를 바인딩하거나 대상을 바꿈
Node?
같은 null 가능 참조는 if
, guard
, assert
로 좁힐 수 있음
- 수명이 중첩되지 않는 데이터에는
reusable
풀을 사용함
- 확장 전용 배열과 숨겨진 빈 슬롯 목록을 결합하며,
alloc_ref
가 슬롯을 재사용하거나 추가하고 free
가 슬롯을 반환함
- 반환된 슬롯을 가리키는 오래된 참조는 다음 사용자가 넣은 같은 타입의 값을 읽을 수 있음
- 이는 논리 오류가 될 수 있지만, 실제 메모리가 해제되지 않아 메모리 손상으로 이어지지는 않음
데이터 구조 자체를 파일 형식으로 사용
- 자기 상대적 링크로 구성한 데이터는 위치가 바뀌어도 같은 의미를 유지하므로,
바이트 쓰기와 읽기로 저장하고 불러올 수 있음
- 별도 직렬화기, 스키마, 포인터 보정이 필요 없음
to_bytes
는 프레이밍이 포함된 바이트 이미지를 만들고, from_bytes
는 값으로 변환하기 전에 검증함
- 프레이밍, 모든 태그, 길이, 링크를 검사함
- 손상되거나 악의적으로 조작된 입력은 잘못된 참조 대신
false
와 빈 배열을 반환함
최종 위치에서 생성하고 호출을 넘어 반환
- 제자리 생성 보장에 따라 값은 호출 깊이와 관계없이
최종 저장 위치에서 직접 생성됨
items.push(parse(line))
는 파싱한 레코드를 배열 안에 바로 씀
- 확장 전용 배열을 값으로 반환해도 호출자의 변수나 생성 중인 레코드 필드에 직접 기록하므로 복사 비용이 없음
- 문자열 포맷팅과
filter
결과에도 적용되며, 출력 매개변수는 대부분 필요하지 않음
return E from f
는 호출 스택에서 가장 안쪽에 활성화된 f
호출의 결과로 E
를 반환함
- 중간 함수는 일반 시그니처를 유지하며
Result
, 오류 매개변수, 호출마다 붙이는 ?
가 필요 없음
안에 있는지 정적으로 확인하므로 런타임에 처리되지 않은 반환이 남지 않음
- 스택 언와인더 대신 프레임마다 숨겨진 판별값을 검사하며, 오류 메시지도 최종 목적지에 직접 생성함
- 샘플 파서는 모든 구문 오류에 이 방식을 사용함
제네릭과 함수 블록
- 타입을 지정하지 않은 매개변수는
제네릭 매개변수이며, 모든 제네릭은 단형화됨
- 타입 인수는 실제 인수의 타입에서 추론하며, 추론할 수 없는 매개변수는 명시적으로 전달할 수 있음
함수 값은 컴파일 시점 개체로서 제네릭 매개변수로 전달됨
-
모든 호출은 직접 호출이며 인라인화할 수 있고, 클로저 객체나 함수 포인터가 없음
-
함수 값은 스코프 밖으로 탈출할 수 없으며, 고차 함수는 대응하는 반복문으로 컴파일됨
-
함수 블록의
return
은 어휘적으로 바깥에 있는 함수에서 반환할 수 있고, 중첩 함수는 바깥 함수의 지역 변수에 접근할 수 있음
메모리를 공유하지 않는 스레드
thread_fn
과 그 호출 대상은 독립 프로그램으로 컴파일되며, 자체 데이터 스택과 전역 변수 복사본을 가짐
-
공유 가변 메모리, 데이터 경쟁, 잠금, 원자 연산, 메모리 순서 개념이 언어에 없음
-
값은 타입마다 하나씩 있는
타입 지정 큐를 통해 이동함
-
전달 값은 어느 깊이에도 참조가 없는 평탄한 값이어야 함
-
픽셀을 포함한 실제 데이터가 연속 저장되므로 전달은
memcpy
로 이뤄짐
thread_spawn_contained
로 시작한 워커에서 검사 실패나 abort
가 발생하면 그 워커만 종료됨
- 오류 메시지는 stderr 대신 기다리는 호출자에게 전달되므로 감독 코드가 기록하고 새 워커를 시작할 수 있음
- 첫
thread_wait(id, msg)
가 메시지를 가져가며, 메시지를 요청하지 않으면 유실됨
C 연동과 실행 도구
extern fn
으로 Goose 시그니처를 C 심벌에 연결하며, math
와 os
모듈도 이 인터페이스를 사용함
- 지원하는 C 표현이 있는 타입만 경계를 넘을 수 있고, 지원하지 않는 시그니처는 선언 단계에서 거부함
- 슬라이스는
{ data, len }
으로 전달하고 구조체는 포인터를 통해 채울 수 있음
- 전체 프로그램을
하나의 C 파일로 출력하며, 번들 TinyCC는 Goose 컴파일러 프로세스 안에서 컴파일하고 실행할 수 있음
- 스레드 사용 프로그램은 TinyCC의 스레드 로컬 저장소 제약 때문에 네이티브 실행 파일로 빌드해야 함
- 기본 출력은 별도 런타임과 연결하며,
--standalone
으로 전체 런타임을 포함한 C 파일을 만들 수 있음
-
다른 컴파일러 버전으로 생성한 프로그램은 기존 런타임과 링크되지 않음
-
릴리스는 Windows, Linux, macOS용 사전 빌드 컴파일러와 그래픽, 오디오, UI, 물리 패키지를 제공함
-
소스 빌드는 CMake 3.27 이상과 C++20 컴파일러를 사용하며, 테스트 및 샘플 실행기는 Python 3가 필요함
-
외부 패키지는 미리 내장하거나 첫 사용 시 빌드할 수 있음
-
패키지 기능은 디렉터리 또는 특정 태그/커밋의 Git 저장소에서 라이브러리를 가져오며, 자체 C 소스와 CMake 라이브러리 연동도 지원함
디버그 모드는 오버플로, 범위, 태그 검사를 활성화하고 실패한 소스 줄을 알려줌
-
데이터 스택은 기본적으로 각각 2GB 주소 공간을 예약하고 사용량에 따라 커밋하며, 예약 크기 조정과 스택 사용 통계를 지원함
-
VS Code 확장은 구문 강조, JIT 실행, 언어 서버를 제공함
goose lsp
는 실시간 오류, 호버, 정의 이동, 자동 완성, 이름 변경, 심벌 기능을 지원함
- 다른 편집기 안내에 Neovim, Helix, Emacs 설정이 있음
벤치마크와 경계 검사
- 성능 이점은
힙 할당과 객체별 해제 제거, 연속 데이터, 좁은 링크, 실제 변형 크기만 사용하는 열거형에서 나옴
- 모든 인덱스 접근에
경계 검사를 적용하고, 불필요함을 증명한 검사는 제거함
- 슬라이스 길이의
assert
가 남은 검사를 제거하고 벡터화를 가능하게 하는 경우가 있음
--bce-lines
는 남은 검사를 보고하며, checkfree fn
은 경계 검사가 하나라도 남는 커널을 거부함
| 측정 항목 |
관용적 C++ 대비 |
수작업 최적화 C++ 대비 |
가장 빠른 안전한 Rust 대비 |
| MSVC 백엔드 속도 |
3.51배 |
1.24배 |
1.13배 |
| clang 백엔드 속도 |
3.54배 |
1.26배 |
1.19배 |
| 최대 메모리 사용량 |
약 1/1.95 |
약 1/1.32 |
약 1/1.25 |
- 도입부의 별도 요약 수치는 같은 비교 대상 순서로 속도
3.3배, 1.16배, 1.12배, 메모리 사용량 약 1/1.9, 1/1.3, 1/1.2임
- 벤치마크 요약은 이득과 손실을 함께 다루며, 전체 결과와 벤치마크 설계에서 측정값과 시험 범위를 확인할 수 있음
사용 제약과 현재 상태
- 데이터의
소유자와 스코프 수명을 고려해야 하며, 생성 함수의 스코프로 해결되지 않으면 reusable
풀을 사용함
-
재귀 함수는
재귀 호출을 가로질러 확장 가능한 데이터를 유지할 수 없음
-
임시 데이터의 수명은 호출 전에 끝내고, 호출 이후에도 필요한 데이터는 풀로 전달해야 함
-
탈출하는 클로저, 함수 포인터, ADT 태그 외의 동적 디스패치를 지원하지 않으며
전체 프로그램 컴파일만 가능함
-
Goose는 초기 단계로, 약
3만 줄의 C++ 컴파일러, 명세, 표준 라이브러리, 샘플, 테스트 모음을 갖춤
-
CI는 Windows, macOS, Linux에서 실행되며 별도 새니타이저 작업도 수행함
-
현재 범위에서 제외한 기능은 크기 조절 가능 값의 이동, 구조체당 둘 이상의 크기 조절 가능 값, 레이블이 있는
break
, 네임스페이스 비공개 접근 제어, 더 넓은 OS 및 라이브러리 접근임
문서와 샘플 생태계
- 튜토리얼은 예제로 시작하는 언어 입문을 제공함
- 언어 레퍼런스에는 기능별 설명과 컴파일 가능한 예제가 있음
- 언어 명세에서 정확한 언어 규칙을 확인할 수 있음
- 샘플은
31개 완성 프로그램으로, JSON 파서, 멀티스레드 Mandelbrot, 두 풀로 구성한 파일 트리, GPU 큐브, 물리 시뮬레이션, 위젯 기반 할 일 목록 등을 제공함
- 표준 라이브러리는
stdlib/
아래 읽을 수 있는 Goose 코드로 작성한 14개 모듈을 제공함
- 구현 노트에서 컴파일러의 단계별 구조와 테스트 방식을 확인할 수 있음
AI를 활용한 구현 과정
사람이 설계한 언어를 AI로 구현하는 실험으로, 저장소 대부분을 AI로 제작함
-
언어 설계는 수년 전에 시작했으나, Lobster로 개발하는 게임 스타트업 운영 때문에 초기 구현을 마치지 못했음
-
Fable 등장 이후 구현을 진행했고, 기존 Lobster 컴파일러의 스타일과 구조를 따르게 함
-
초기 설계에서 정하지 않았던 부분은 Fable과 반복적으로 검토하며 구체화함
-
구현 언어로
C++ 을 선택한 이유에는 C 출력에 필요한 기존 C/C++ 도구 체인 활용, 배포 단순화, libtcc
및 C 라이브러리 통합, 익숙한 Lobster 코드 구조 유지가 있음
- 설계자의 경험상 Fable은 언어별 구현 능력이 거의 비슷했고, Rust 같은 언어의 강한 안전장치가 이전만큼 절실하지 않았음
- 향후 Goose 자체로 구현할 가능성은 열어둠
댓글과 토론