🍓 ScriptRowberry Language Specification
v2.0 · 2025 · Status: DRAFT EXTENDED
JavaScript의 생산성 × Game Boy의 극한 하드웨어 제어 철학 — 비트 레벨 정밀도로 WebAssembly/WebGPU 파이프라인을 구동하는 초경량 시스템 언어의 완전 명세서.
2.1 임의 비트 정수형 (Custom-Width Integers)
부호 없는 정수 uN과 부호 있는 정수 iN을 지원하며, N은 1 이상 32 이하의 임의 정수입니다.
| 타입 | 범위 공식 | 예시 (N=3) |
| uN | [0, 2ᴺ − 1] | u3 → [0, 7], 8가지 표현 |
| iN | [−2ᴺ⁻¹, 2ᴺ⁻¹ − 1] | i3 → [−4, 3] |
| u32 / i32 | 하드웨어 가속 표준형 | Wasm 네이티브 지원 |
| f32 | IEEE 754 단정밀도 | GPU 친화적 부동소수점 |
| bool | {false, true} | 내부적으로 u1과 동일 |
최소 비트 추론 규칙
고유 상태 K개가 필요할 때, 컴파일러는 2ᴺ⁻¹ < K ≤ 2ᴺ 를 만족하는 최소 N을 자동 선택. K=5이면 N=3 (u3). 개발자가 명시하지 않아도 컴파일러가 추론 가능.
2.2 정적 구조체 및 메모리 레이아웃
struct ObjectState {
id: u16; // 16비트 — 객체 고유 식별자 (0~65535)
active: u1; // 1비트 — 활성/비활성 플래그
category: u3; // 3비트 — 최대 8종 카테고리
priority: u4; // 4비트 — 0~15 우선순위 레벨
}
// 실제 비트 합계: 16+1+3+4 = 24비트 = 3바이트
// 컴파일러가 바이트 정렬 처리 → 32비트(4바이트)로 패딩 가능
// 패킹 최적화 시 여러 ObjectState를 하나의 레지스터에 압축 가능
패딩(Padding) 규칙 — 보완 명세
구조체 필드의 비트 합계가 32의 배수가 아닐 경우, @packed 어노테이션이 없는 한 컴파일러는 다음 32비트 경계까지 0-패딩을 삽입합니다. @packed struct를 명시하면 패딩을 생략하고 비트 단위 밀집 배치를 강제하나, 정렬 비용이 증가할 수 있습니다.
2.3 포인터 및 참조 타입 신규
GC-Free 환경에서 안전한 메모리 참조를 위해 두 종류의 참조 타입을 제공합니다.
// 불변 참조 — 읽기 전용, 여러 곳에서 동시 참조 가능
let r: &ObjectState = &obj;
// 가변 참조 — 한 시점에 하나만 존재 가능 (소유권 규칙)
let rm: &mut ObjectState = &mut obj;
// 원시 포인터 — unsafe 블록 내에서만 역참조 가능
let p: *u32 = 0x2000 as *u32; // 하드웨어 메모리 주소 직접 접근
2.4 정적 배열 (Fixed-Length Array) 신규
// 크기가 컴파일 타임에 확정되는 배열만 허용
let palette: [u3; 8] = [0b3, 1b3, 2b3, 3b3, 4b3, 5b3, 6b3, 7b3];
// GPU 버퍼용 대형 정적 배열 — 힙 영역에 컴파일 타임 배치
static TILEMAP: [u8; 1024] = [0; 1024]; // 0으로 초기화된 1024개의 u8
설계 근거
동적 크기 배열(Vec 같은 구조)은 힙 할당을 요구합니다. ScriptRowberry는 힙 할당을 언어 레벨에서 완전히 배제하므로, 배열 크기는 반드시 컴파일 타임 상수여야 합니다. 런타임에 가변 크기 컬렉션이 필요한 경우, 최대 크기를 정적 선언한 후 실제 사용량을 별도 u32로 추적하는 패턴을 사용합니다.
3.1 선형 메모리 구조
ScriptRowberry는 WebAssembly의 선형 메모리(Linear Memory) 위에서 동작합니다. 전체 메모리는 컴파일 타임에 결정된 하나의 연속 바이트 배열이며, 런타임에 크기를 변경할 수 없습니다 (--wasm-pages 빌드 옵션으로 페이지 수 고정). 이를 통해 GC 없이 결정론적 메모리 동작을 보장합니다.
총 메모리 = wasm_pages × 65536 bytes (64KB/page)
3.2 소유권 규칙 (Ownership Rules)
GC-Free 언어에서 메모리 안전성을 보장하기 위해 컴파일 타임에 강제되는 소유권 3원칙을 따릅니다.
// 규칙 1: 모든 값은 정확히 하나의 소유자(owner)를 가진다
let a: u32 = 42;
let b = a; // u32는 Copy 타입 → 복사됨, a는 여전히 유효
let s1: ObjectState = ObjectState { id: 1, active: 1b1, category: 3b3, priority: 5b4 };
let s2 = s1; // 소유권 이동(Move) — s1은 더 이상 접근 불가
// s1.id; ← 컴파일 에러: use of moved value 's1'
// 규칙 2: 불변 참조는 동시에 여러 개 허용
let r1 = &s2;
let r2 = &s2; // OK — 동시 불변 참조 허용
// 규칙 3: 가변 참조는 동시에 하나만 존재
let rm = &mut s2;
// let rm2 = &mut s2; ← 컴파일 에러: cannot borrow as mutable twice
3.3 Copy 트레이트와 Move 시맨틱
원시 정수 타입(uN, iN, f32, bool)은 자동으로 Copy 트레이트를 구현하므로 대입 시 비트 단위 복사가 발생합니다. Copy를 구현하지 않는 구조체는 소유권 이동(Move) 시맨틱을 따릅니다. 구조체에 @derive(Copy)를 명시하면 컴파일러가 모든 필드가 Copy 타입인지 검증한 후 자동 구현합니다.
@derive(Copy)
struct Vec2 {
x: f32;
y: f32;
}
// Vec2는 이제 Copy — 대입 시 자동 복사, 원본 유효
4.1 비트 리터럴 접미사
| 리터럴 표현 | 의미 | 이진 표현 |
| 4b3 | 3비트 공간의 숫자 4 | 100₂ |
| 12b4 | 4비트 공간의 숫자 12 | 1100₂ |
| 0xFF_u8 | 16진수 리터럴 with 타입 힌트 | 1111_1111₂ |
| 0b1010_u4 | 2진수 리터럴 with 타입 힌트 | 1010₂ |
| 42 | 접미사 없음 → 문맥에 따라 추론 | i32 기본값으로 추론 |
4.2 제어 구문 전체 목록 확장
// ① 함수 선언 — 파라미터·반환 타입 명시 필수
fn computeNext(state: u3, delta: u2): u3 {
return state + (delta as u3);
}
// ② 블록 스코프 변수
let x: u3 = 5b3;
let mut counter: u32 = 0; // mut 없으면 불변(immutable)
// ③ 조건 분기 — 블록은 값을 반환 가능
let label = if x > 4b3 { "high" } else { "low" };
// ④ 카운팅 루프 — 컴파일 타임 상수 범위만 허용 (루프 전개 가능)
for i in 0..8 {
counter = counter + 1;
}
// ⑤ while 루프 — 조건 분기가 있는 루프
while counter < 100 {
counter = counter + 1;
}
// ⑥ unsafe 블록 — 원시 포인터 연산, 인라인 Wasm 삽입 허용
unsafe {
let raw: *u8 = 0x8000 as *u8;
*raw = 0xFF;
}
4.3 컴파일 타임 상수 및 어노테이션 신규
// const — 컴파일 타임에 완전히 평가되는 상수, LUT 생성에 사용
const MAX_SPRITES: u16 = 512;
const TILE_SIZE: u8 = 8;
const SCREEN_W: u16 = MAX_SPRITES * TILE_SIZE as u16; // 상수 산술 허용
// @inline — 함수 인라이닝 강제 지시 (컴파일러 힌트)
@inline
fn clamp3(v: u3): u3 { return v & 7b4; }
// @no_inline — 코드 크기 절약을 위해 인라이닝 금지
@no_inline
fn expensiveSetup(): u32 { ... }
// @export — 이 함수를 Wasm export 테이블에 노출 (JS에서 호출 가능)
@export
fn tick(dt: f32): u32 { ... }
5.1 파일 기반 모듈
각 .srb 파일은 독립적인 모듈입니다. 모듈 경계는 파일 시스템 디렉토리 구조를 그대로 반영합니다.
// src/graphics/sprite.srb
pub struct Sprite { // pub — 모듈 외부에서 접근 가능
x: u16;
y: u16;
tile_id: u8;
}
pub fn draw(s: &Sprite): u1 { ... }
fn _internal(): u32 { ... } // pub 없으면 모듈 내부에서만 접근
// src/main.srb
use graphics::sprite::{Sprite, draw}; // 선택적 임포트
use graphics::sprite::*; // 와일드카드 임포트 (권장하지 않음)
@export
fn main(): u32 {
let s = Sprite { x: 10, y: 20, tile_id: 3 };
draw(&s);
return 0;
}
5.2 조건부 컴파일 (Conditional Compilation)
// @cfg — 빌드 플래그에 따라 코드 블록 포함 여부 결정
@cfg(target = "wasm")
fn platformInit(): u32 {
// Wasm 전용 초기화 로직
return 1;
}
@cfg(target = "gpu")
fn platformInit(): u32 {
// GPU 타겟 전용 초기화 로직
return 2;
}
@cfg(debug = "true")
fn debugDump(state: &ObjectState): u32 {
// 릴리즈 빌드에서는 컴파일러가 이 함수를 완전히 제거
...
}
6.1 비트 최적화 열거형 (Enum)
열거형의 배리언트 수로부터 컴파일러가 최소 비트 표현을 자동 계산합니다. 5종류의 배리언트가 있다면 u3으로 표현합니다.
enum Direction { // 4종 → 컴파일러가 u2로 최적화
North, // = 0b00
South, // = 0b01
East, // = 0b10
West, // = 0b11
}
enum GameState { // 데이터를 품는 배리언트 — Tagged Union
Loading { progress: u8 },
Playing { score: u32, lives: u3 },
GameOver { final_score: u32 },
Paused,
}
// 총 4종 → 태그는 u2. 최대 페이로드 크기에 맞춰 유니온 메모리 계산.
6.2 패턴 매칭 (match)
match 구문은 열거형의 모든 배리언트를 커버해야 하며, 누락 시 컴파일 에러가 발생합니다 (Exhaustiveness Check). 컴파일러는 match를 LUT 또는 점프 테이블로 최적화합니다.
fn update(gs: GameState): GameState {
match gs {
GameState::Loading { progress } if progress >= 100 => {
GameState::Playing { score: 0, lives: 3b3 }
},
GameState::Loading { progress } => {
GameState::Loading { progress: progress + 1 }
},
GameState::Playing { score, lives: 0b3 } => {
GameState::GameOver { final_score: score }
},
GameState::Playing { score, lives } => {
GameState::Playing { score: score + 10, lives }
},
GameState::GameOver { .. } | GameState::Paused => gs,
}
}
7.1 트레이트 (Trait) 정의
트레이트는 타입이 반드시 구현해야 하는 인터페이스를 정의합니다. 컴파일 타임에 정적 디스패치로 해소되므로 런타임 vtable 비용이 없습니다.
trait Drawable {
fn draw(&self, x: u16, y: u16): u1;
fn bounding_bits(): u8; // 정적 메서드 (self 없음)
}
impl Drawable for Sprite {
fn draw(&self, x: u16, y: u16): u1 {
// 실제 타일 렌더링 로직
return 1b1;
}
fn bounding_bits(): u8 { return 8; }
}
7.2 제네릭 함수와 단형화 (Monomorphization)
// T는 반드시 Copy 트레이트를 구현해야 함 (제약 조건)
fn swap<T: Copy>(a: T, b: T): (T, T) {
return (b, a);
}
// 컴파일러는 실제 사용된 타입마다 전용 코드를 생성 (단형화)
// swap::u3(), swap::u16() 각각 별도 최적화
let (y, x) = swap::<u3>(3b3, 5b3);
// 제네릭 구조체
struct Pair<A: Copy, B: Copy> {
first: A;
second: B;
}
8.1 클로저 캡처 규칙
ScriptRowberry의 클로저는 동적 힙 할당 없이 스택 또는 정적 메모리에 배치됩니다. 캡처 방식은 캡처된 변수의 타입에 따라 컴파일러가 결정합니다.
let base: u3 = 2b3;
// Copy 타입은 자동으로 값 캡처 (비트 복사)
let add_base = |x: u3| -> u3 { x + base };
// gpu_map 클로저 — GPU 셰이더로 변환됨
// 캡처 변수는 GPU 유니폼(Uniform) 버퍼로 자동 바인딩됨
let result = data.gpu_map(|x: u3| -> u3 { x + 1b3 });
// 고차 함수 — 제네릭으로 구현 (런타임 함수 포인터 없음)
fn map_array<T: Copy, U: Copy, F: Fn(T) -> U>(
arr: &[T; 8], f: F
): [U; 8] { ... }
9.1 전체 컴파일 파이프라인
① Lexer & Tokenizer (어휘 분석 및 토큰화)소스 코드로부터 비트 수식 접미사(예: `1b1`, `6b3`), 16진수/2진수 리터럴, 문자열 이스케이프 시퀀스를 추출하고 세미콜론 자동 삽입(ASI) 규칙을 적용합니다.
② Parser & AST 생성 (구문 분석 및 추상 구문 트리 생성)문법 규칙에 맞춰 타입 노드, 식 노드, 문 노드들로 구성된 계층적 AST를 형성하며, 리터럴 값의 비트 경계선 오버플로우 검사를 병행합니다.
③ Type Checker (타입 검사 및 소유권/참조 수명 분석)변수의 타입 일치성 검사(E002), 소유권 이동 후 재사용 감지(E003), 동시 다중 가변 참조 제한(E004) 및 주소 노출 분석(Escape Analysis)을 수행합니다. 주소가 취해지는 변수들은 `this.addressTakenVars` 테이블에 영구 등록합니다.
④ AST Optimizer (AST 중간 단계 최적화 패스) ← NEW상수 접기(Constant Folding), 상수 전파(Constant Propagation), 그리고 무효 분기문을 정리하는 데드 브랜치 프루닝(Dead Branch Pruning)을 가동하여 트리 전체를 경량화합니다.
⑤ Code Generator (WAT 코드 생성기 및 물리적 레지스터 승격)AST 최적화가 완료된 트리를 기반으로 Wasm 텍스트 형식(WAT) 명령어를 생성합니다. 구조체 레지스터 승격(SROA), 하이브리드 루프 차등 변환 및 비트 마스킹 주입 최적화가 이 단계에서 최종 완결됩니다.
9.2 비트 마스킹 (Bit-Masking) 연산 제어
ScriptRowberry는 `u1`부터 `u31`까지의 비표준 비트 정수형을 지원하지만, 하드웨어(WebAssembly 가상 머신)는 오직 32비트와 64비트 레지스터만을 제공합니다. 컴파일러는 타입 안전성을 물리적으로 보장하기 위해, 연산 결과가 해당 비트 타입의 상한선을 넘지 않도록 비트 연산 후 즉시 AND 필터 마스크를 주입하는 최적화를 수행합니다.
비트 마스크 값 (M) = 2ᴺ − 1 | u3 타입 마스크: 2³ - 1 = 7 (0b0111)
// [소스 코드]
let a: u3 = 6b3;
let b: u3 = 3b3;
let c = a + b;
// [컴파일러 빌드 시 주입되는 Wasm 바이트코드 매핑 (WAT)]
local.get $a
local.get $b
i32.add
i32.const 7 ;; u3의 최댓값(7)을 나타내는 마스크 주입
i32.and ;; 연산 결과를 3비트 공간으로 바인딩 (결과값: 1)
local.set $c
이 마스킹 필터는 대입(`Assignment`), 변수 초기화(`VariableDeclaration`) 단계뿐 아니라 복합적인 연산의 중간 전파 경로마다 주입되어 하드웨어 레벨의 비트 순환(Wrapping)을 추가 비용 없이 무상태로 재현합니다.
9.3 SROA (Scalar Replacement of Aggregates) 구조체 레지스터 승격
WebAssembly에서 모든 로컬 구조체를 선형 메모리(Linear Memory Stack)에 올리는 기존 컴파일 방식은 구조체 필드를 한 번 읽고 쓸 때마다 무거운 오프셋 주소 가산 연산 및 `i32.load`/`i32.store` 명령어를 야기하여 심각한 런타임 성능 저하를 일으킵니다.
ScriptRowberry는 컴파일 타임에 탈출 분석(Escape Analysis)을 가동하여 주소 참조 연산자(`&` 또는 `&mut`)가 사용되지 않은 비탈출 구조체 변수들을 검출합니다. 해당 변수들은 메모리에 배치되지 않으며, 모든 멤버 필드를 개별 Wasm 로컬 변수(가상 레지스터)로 쪼개어 승격시킵니다.
// [구조체 정의 및 비탈출 로컬 구조체 사용]
struct Vector2 {
x: u32;
y: u32;
}
fn main(): u32 {
let v = Vector2 { x: 100, y: 200 };
return v.x + v.y;
}
// [컴파일러 코드 생성 결과 (SROA 적용)]
(func $main (result i32)
;; 1. 구조체 변수 v를 메모리에 할당하지 않고, 개별 local 레지스터로 전개 선언
(local $v__field__x i32)
(local $v__field__y i32)
;; 2. 구조체 멤버 초기화 -> local.set으로 고속 레지스터 대입
i32.const 100
local.set $v__field__x
i32.const 200
local.set $v__field__y
;; 3. 구조체 멤버 접근 -> 메모리 load 대신 local.get으로 직접 레지스터 읽기
local.get $v__field__x
local.get $v__field__y
i32.add
return
)
포인터 역참조와 재귀적 타입 추론 (Member access on Dereference)
구조체의 포인터(예: `let ptr = &v;`)를 활용해 가변 참조하는 경로(`(*ptr).x`)가 발생하면, 타입 체커 단계 이후의 코드 생성기(Code Generator)는 스코프 환경을 유실하게 됩니다. 이를 해결하기 위해 ScriptRowberry는 소스 코드 생성 단계에서 로컬 타입 레지스터 테이블(`localTypes`) 및 구조체 명세 등록기(`structRegistry`)를 이용해 식의 타입을 컴파일 타임에 재귀적으로 유도해 내는 getTypeOfNode(node) 엔진을 탑재하고 있습니다.
주소가 사용된(`addressTakenVars`에 등록된) 구조체 변수의 경우, 자동으로 선형 스택 메모리 프레임(`$__stack_ptr` 가산 및 오프셋 기반 저장) 경로로 안전하게 폴백(Fallback)되어 포인터의 메모리 안전성을 원천 보장합니다.
9.4 하이브리드 루프 컴파일 제어 트리
ScriptRowberry는 고도의 코드 크기 보존과 런타임 분기 오버헤드 최소화를 모두 잡기 위해 루프의 성격에 따라 두 가지 전략을 선택적으로 혼용하는 하이브리드 엔진을 갖추고 있습니다.
| 구문 조건 |
컴파일 방법 |
기계어 수준 최적화 특징 |
| 시작과 끝이 상수이며, 전체 범위 크기가 16 이하일 때 |
Loop Unrolling (루프 완전 전개) |
분기 예측 실패 패널티 제거, 루프 제어 변수 연산 비용 제거 |
| 전체 범위 크기가 16을 초과하거나, 루프 상한선이 변수일 때 |
Dynamic Loop (동적 컴파일) |
Wasm native `block`/`loop`/`br_if` 흐름 제어, 코드 크기 팽창 억제 |
// [상수 범위 루프 (5회 반복) - Loop Unrolling]
for i in 0..5 {
acc = acc + i;
}
// [생성되는 WAT 결과]
;; === UNROLLED for i in 0..5 ===
i32.const 0 | local.set $i | [body 코드]
i32.const 1 | local.set $i | [body 코드]
i32.const 2 | local.set $i | [body 코드]
i32.const 3 | local.set $i | [body 코드]
i32.const 4 | local.set $i | [body 코드]
// [동적 변수 범위 루프 - Dynamic Compiler Path]
for i in 0..limit {
acc = acc + i;
}
// [생성되는 WAT 결과]
;; === DYNAMIC for i ===
local.get $limit ;; 상한값 보존
local.set $__limit_scratch
i32.const 0
local.set $i ;; 초기화: i = 0
block $for_block
loop $for_loop
;; 조건 확인: i >= limit 이면 탈출
local.get $i
local.get $__limit_scratch
i32.ge_s
br_if $for_block
;; 루프 본문
local.get $acc
local.get $i
i32.add
local.set $acc
;; 증감식: i = i + 1
local.get $i
i32.const 1
i32.add
local.set $i
;; 루프 백점프
br $for_loop
end
end
9.5 컴파일 타임 상수 접기 (Constant Folding) 및 조건 제거 패스
컴파일 속도와 정적 최적화를 위해 AST Optimizer 단계에서 리터럴과 상수로만 구성된 표현식을 미리 실행하고 그 단일 결과 노드로 AST 자체를 치환합니다.
이를 통해 런타임 Wasm 인터프리터 연산 비용이 완전히 소멸하며, 상수 값에 의해 실행 여부가 정해진 `IfStatement` 분기가 발생하면 조건이 거짓인 브랜치는 AST 트리에서 제거(Pruning)되어 바이너리 크기를 혁신적으로 경감시킵니다.
// [최적화 적용 전]
const SCALE: u32 = 5;
let value = (10 + 20) * SCALE;
if true {
fillRect(value, 0, 10, 10, "#FF0000");
} else {
fillRect(0, 0, 0, 0, "#000000");
}
// [AST Optimizer 최적화 통과 후 코드 생성기에 전달되는 가상의 형태]
let value = 150; ;; (10 + 20) * 5 가 150으로 상수 접기 및 전파 완료
fillRect(value, 0, 10, 10, "#FF0000"); ;; else 블록 및 if 조건문 식 완전 소멸
9.6 비트 패킹 (Register Packing)과 LUT 컴파일
// u3 변수 10개 -> 3×10 = 30비트 -> 32비트 레지스터 1개에 압축
let states: [u3; 10] = [...];
// 컴파일러 내부 표현 (의사 Wasm)
// packed_reg: u32 = state[0] | (state[1] << 3) | (state[2] << 6) | ...
// 메모리 로드 1회로 10개 상태 동시 취득 -> 메모리 대역폭 10배 향상
이러한 레지스터 내 비트 오프셋 할당 최적화는 대용량 병렬 SIMD 연산 시 스택 대역폭 병목을 극적으로 돌파하는 성능 지지대 역할을 담당합니다.
10.1 Result 타입 기반 에러 전파
예외(Exception)는 런타임 오버헤드가 있으며 제어 흐름을 예측 불가능하게 만듭니다. ScriptRowberry는 에러를 값으로 취급하는 Result<T, E> 패턴을 채택합니다. 런타임 예외 메커니즘은 존재하지 않습니다.
enum ParseError {
Overflow,
InvalidRange { min: u32, max: u32, got: u32 },
UnexpectedEof,
}
fn parseSprite(data: &[u8; 4]): Result<Sprite, ParseError> {
if data[0] > 200 {
return Err(ParseError::InvalidRange { min: 0, max: 200, got: data[0] as u32 });
}
return Ok(Sprite { x: data[0] as u16, y: data[1] as u16, tile_id: data[2] });
}
// ? 연산자 — 에러 조기 반환 단축 문법 (JS의 try-catch 없이)
fn loadScene(): Result<u32, ParseError> {
let s = parseSprite(&[10, 20, 3, 0])?; // 에러면 즉시 return Err(...)
return Ok(s.x as u32);
}
10.2 컴파일 타임 에러 분류
| 에러 코드 | 발생 조건 | 예시 |
| E001: LiteralOverflow | 리터럴 값이 타입 최댓값 초과 | let x: u3 = 8b3 |
| E002: ImplicitCast | 서로 다른 비트 타입 간 암묵적 연산 | u3 + u4 직접 연산 |
| E003: UseAfterMove | 소유권 이동 후 원본 접근 | move 후 원본 변수 사용 |
| E004: MultipleMutBorrow | 가변 참조 동시 2개 이상 | &mut x 두 번 빌림 |
| E005: NonExhaustiveMatch | match에서 배리언트 누락 | enum 케이스 미처리 |
| E006: UnsafeOutsideBlock | unsafe 연산을 일반 블록에서 시도 | 원시 포인터 역참조 |
| E007: DynamicSizeArray | 런타임 크기 배열 선언 시도 | let arr: [u8; n] (n이 변수) |
11.1 JS → SRB 호출 (Export)
// ScriptRowberry 측 — @export 함수가 Wasm 인스턴스에 노출됨
@export
fn tick(dt: f32): u32 {
// 게임 루프 업데이트
return 0;
}
// JavaScript 측 — 일반 Wasm 호출 방식 그대로
// JS 코드
const { instance } = await WebAssembly.instantiateStreaming(
fetch('game.wasm')
);
instance.exports.tick(16.67); // f32 인자 직접 전달
11.2 SRB → JS 호출 (Import)
// SRB 측 — @import로 JS 함수를 선언만 함 (구현은 JS에 있음)
@import("env", "log")
fn js_log(value: u32): u32; // 구현 없음 — JS에서 제공
@export
fn debugMe(): u32 {
js_log(42); // Wasm → JS 역방향 호출
return 0;
}
// JS 측 — importObject로 구현 제공
const importObject = {
env: {
log: (v) => console.log(`SRB says: ${v}`)
}
};
const { instance } = await WebAssembly.instantiateStreaming(
fetch('game.wasm'), importObject
);
11.3 공유 메모리 버퍼 접근
구조체 배열 같은 복잡 데이터는 Wasm 선형 메모리를 통해 JS와 제로 복사로 공유합니다.
// SRB 측 — 공유 버퍼 시작 주소 노출
static SPRITES: [ObjectState; 512] = [ObjectState::default(); 512];
@export
fn getSpritesPtr(): u32 { return &SPRITES as u32; }
@export
fn getSpritesLen(): u32 { return 512; }
// JS 측 — 같은 메모리를 TypedArray로 직접 뷰잉
const ptr = instance.exports.getSpritesPtr();
const len = instance.exports.getSpritesLen();
// Uint32Array로 뷰 — 복사 없음, 실시간 반영
const spritesView = new Uint32Array(instance.exports.memory.buffer, ptr, len);
12.1 CPU 타겟: WebAssembly SIMD
| 데이터 타입 | 128비트 레지스터에 몇 개? | JS 대비 속도 |
| u32 (일반 JS Number) | 4개 | 기준 (1×) |
| u16 | 8개 | ~2× |
| u8 | 16개 | ~4× |
| u4 | 32개 | ~8× |
| u3 (10개 패킹 시) | ~42개 | ~10×+ |
| u1 (bool 배열) | 128개 | ~32× |
12.2 GPU 타겟: WebGPU (WGSL)
// ScriptRowberry 입력 — 1줄로 1000만 개 데이터 GPU 병렬 처리
let result = data.gpu_map(|x: u3| -> u3 { x + 1b3 });
// 컴파일러 자동 생성 WGSL (의미 동등)
@group(0) @binding(0) var<storage, read_write> data: array<u32>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
let idx = id.x;
// 3비트 마스킹(&7u) 자동 주입 — 수천 개 GPU 코어가 동시 실행
data[idx] = (data[idx] + 1u) & 7u;
}
gpu_map 캡처 바인딩 규칙
클로저 내부에서 캡처된 외부 상수(const) 변수는 자동으로 WGSL @group(1) @binding(N) 유니폼으로 변환됩니다. 구조체 참조가 캡처되면 해당 구조체가 Storage Buffer로 바인딩됩니다. 개발자는 바인딩 레이아웃을 직접 작성할 필요가 없습니다.
14.1 빌드 옵션 레퍼런스
# Wasm CPU 타겟 릴리즈 빌드
srbuild --target=wasm --release --wasm-pages=16 src/main.srb -o dist/game.wasm
# GPU 타겟 빌드 (WGSL 출력)
srbuild --target=gpu --release src/main.srb -o dist/shader.wgsl
# 디버그 빌드 — 소유권 검사 정보, 소스맵 포함
srbuild --target=wasm --debug --sourcemap src/main.srb -o dist/game.wasm
# 크기 최적화 최우선 모드 (Game Boy 모드)
srbuild --target=wasm --release --optimize=size --strip src/main.srb -o dist/game.wasm
# 성능 최적화 최우선 모드
srbuild --target=wasm --release --optimize=speed --simd src/main.srb -o dist/game.wasm
14.2 srb.toml 프로젝트 매니페스트
[package]
name = "my-game"
version = "0.1.0"
entry = "src/main.srb"
[build]
target = "wasm"
wasm_pages = 16 # 16 × 64KB = 1MB 총 메모리
optimize = "speed"
simd = true
[features]
debug_overlay = false # @cfg(feature="debug_overlay") 토글
gpu_particles = true # @cfg(feature="gpu_particles") 토글
원본 v1.0의 3가지 규칙을 7가지로 확장하고 근거를 보강합니다.
| # | 규칙 | 컴파일러 동작 |
| 1 | 암묵적 형변환 금지 | u3 + u4 시도 시 E002 에러, as 캐스팅 강제 |
| 2 | 리터럴 경계선 초과 검사 | let x: u3 = 8b3 → E001: "Literal 8 overflows u3 (max 7)" |
| 3 | 세미콜론 자동 삽입 (ASI) | 줄바꿈+연산자 배치 분석으로 안전하게 구분 |
| 4 | 모든 match 완전성 강제 | 배리언트 누락 시 E005, 미처리 케이스 목록 출력 |
| 5 | unsafe 블록 경계 강제 | 원시 포인터 역참조, 인라인 Wasm은 unsafe {} 내에서만 |
| 6 | 컴파일 타임 크기 배열만 허용 | [T; N]에서 N이 런타임 값이면 E007 에러 |
| 7 | @export 함수 타입 제한 | JS 전달 가능 타입(u32, i32, f32, u64, i64)만 파라미터/반환 허용. 구조체는 포인터로 전달 |
v2.0 보완 요약
v1.0 대비 추가된 섹션: 메모리 모델·소유권(§3), 모듈 시스템·조건부 컴파일(§5), 열거형·패턴 매칭(§6), 제네릭·트레이트(§7), 클로저 캡처 규칙(§8), 에러 처리 Result 타입(§10), JS 인터롭 양방향 명세(§11), 표준 라이브러리(§13), 빌드 시스템(§14). 기존 섹션에는 포인터·참조 타입(§2.3), 정적 배열(§2.4), 패딩 규칙 명문화(§2.2), DCE(§9.5), @export 타입 제한(§15-7)이 보완되었습니다.