희소 동기 모델(SSM)에 기반한 고수준 반응형 실시간 언어 Scoria와 그 컴파일러를 소개하고, Haskell EDSL로 구현한 설계 선택, I/O 처리, 테스트 방법, 성능 평가 결과를 정리한다.
Robert Krook∗, John Hui†, Bo Joel Svensson∗, Stephen A. Edwards†, Koen Claessen∗∗
∗ Chalmers University of Technology, Gothenburg, Sweden
† Columbia University, New York, USA
Email: krookr@chalmers.se, j-hui@cs.columbia.edu, joels@chalmers.se, sedwards@cs.columbia.edu, koen@chalmers.se
새로운 프로그래밍 언어 Scoria와 그 컴파일러의 개발을 설명한다. Scoria는 희소 동기 모델(SSM, sparse synchronous model)에 기반한 고수준 반응형 실시간 언어로, 작은 IoT 장치에서 실행 가능한 시간/전력 효율적인 저수준 C 코드를 생성하도록 설계되었다. 컴파일러는 아직 전력 사용을 의미 있게 측정할 수 있는 단계는 아니지만, 우리는 타이밍 동작을 면밀히 프로파일링하고 성능을 개선할 수 있는 병목을 식별한다. 언어와 컴파일러는 Haskell 위에 임베디드 도메인 특화 언어(EDSL)로 구현되었다.
색인어 — 실시간, IoT, 컴파일러, 임베디드 도메인 특화 언어
사물인터넷(IoT) 장치는 일반적으로 센서, 액추에이터, 무선 통신을 위한 하드웨어를 포함하며, 수명 기대치가 수년인 배터리로 동작해야 하는 경우가 많다. 이러한 주변장치와 자원은 수명과 신뢰성을 최대화하기 위해 세심하게 관리되어야 한다. 이들 장치는 저수준 성격, 타이밍, 전력 요구사항 때문에 보통 C로 프로그래밍된다. 외부 이벤트(예: 센서 트리거)에 반응해야 하므로 소프트웨어는 반응형 스타일로 작성된다. 이런 프로그램은 종종 수많은 콜백 함수를 등록해 이벤트를 처리하고 프로그램의 다른 부분과 통신한다. 그 결과 비동기 코드는 오류가 나기 쉽고 유지보수가 어려워지며, 흔히 “콜백 지옥(callback hell)”이라 불린다.
우리의 언어 Scoria는 콜백 대신 외부/내부 이벤트에서 블로킹할 수 있는 경량 스레드를 사용하며, 코드가 실행되는 시간을 정밀하게 제어할 수 있게 한다. 우리는 Scoria를 Haskell에 임베딩[1]했는데(따라서 문법적 특이성이 존재한다), 이를 통해 컴파일러를 빠르게 개발할 수 있었고, 하드웨어 타이머 및 기타 주변장치와 인터페이스하는 런타임 시스템(RTS)을 사용하는 C 코드를 생성한다.
그림 1은 간단한 Scoria 프로그램이다. BLE(Bluetooth Low Energy) 무선 패킷으로 주파수를 조정할 수 있는 원격 제어 구형파 신호 발생기다. entry 루틴(15–18행)은 환경에서 BLE 수신기와 GPIO 핀을 기대하고(15행), 공유 변수 hperiod를 생성한 다음 sigGen과 remoteControl을 동시에 실행한다(18행).
sigGen 루틴(1–4행)은 미래의 출력 이벤트를 스케줄하고 그 이벤트에서 블로킹함으로써 GPIO 핀에서 정밀 타이밍의 구형파를 생성한다. 이 루틴은 hperiod 변수에 대한 참조를 받아 무한 루프에 들어가 GPIO 핀에 대한 미래의 토글 업데이트를 스케줄한 뒤(3행), 업데이트가 실제로 발생할 때까지 블로킹한다(4행). 토글 이벤트 사이의 지연은 루프 실행 시간과 무관하게 정확히 hperiod 변수가 설정한 모델 시간이다—Scoria의 모델 시간은 wait 같은 블로킹 문장에서만 진행한다. after 지시어는 모델 시간에 따라 업데이트를 스케줄한다.
remoteControl 루틴(6–13행)은 BLE 패킷을 기다렸다가(10행), 그에 따라 hperiod를 두 배 또는 절반으로 바꾼다(12–13행). enableScan 호출(8행)은 블루투스 수신기를 활성화하고, 이후 수신기는 scanref ?ble이 가리키는 변수에 이벤트를 생성한다.
그림 2는 컴파일 시간에 신호 발생기 프로그램의 환경을 초기화하는 코드를 보여준다. 3–4행에서 GPIO 핀과 BLE 컨트롤러에 대한 참조를 얻고, 5–6행에서 환경(물음표로 표시됨)에 추가한다. 7–9행에서는 SSM ready 큐를 entry 루틴과 GPIO 핀 및 BLE에 대한 I/O 핸들러로 채운다. I/O 핸들러는 IV절에서 설명한다.
이 작은 예제는 대부분의 IoT 애플리케이션에 필요한 Scoria의 핵심 기능을 보여준다. 즉, 이벤트에서 블로킹할 수 있는 경량 스레드로 동시성을 프로그래밍하는 것이다. C에서는 하나의 소스에서 오는 이벤트를 기다리는 프로그램을 폴링 루프로 쉽게 작성할 수 있지만, 여러 이벤트를 기다리는 것은 어렵다. 중앙 이벤트 핸들러 루프는 가능하지만, 이벤트 사이에 제어 상태를 버리게 되므로 프로그래머는 어떤 상태(즉, 다음에 무엇을 할지)를 명시적으로 유지해야 한다. 다수의 이벤트는 보통 OS가 제공하는 스레드로 처리하는데, 이는 프로그래머가 단일 이벤트에서만 블로킹하는 순차 코드로 작성할 수 있게 하지만, IoT 장치는 스레드마다 별도의 스택을 둘 메모리가 없는 경우가 많다.
Protothreads[2]는 스레드당 추가 프로그램 카운터 하나만 저장하면 되는 C 매크로 기반의 극도로 가벼운 스레드 패키지이지만, 다른 모든 상태는 전역으로 저장하는 부담이 여전히 프로그래머에게 남는다. Edwards & Hui의 희소 동기 모델(SSM)[3]은 이러한 블로킹 스레드 기반 동시성 모델을 시간 모델과 통합하여, 기능적/시간적 동작이 결정적인 프로그램을 만든다.
SSM에서 실행되는 프로그램은 이산 이벤트 시뮬레이션처럼 동작한다. 시간은 프로그램이 실행되는 ‘인스턴트(instants)’로 나뉜다. 프로그램은 항상 현재 어떤 인스턴트에 있는지만 알고 있으며, 미래 인스턴트를 위한 변수 업데이트를 스케줄할 수 있다. SSM 스케줄러는 모델 시간을 실시간과 동기화하려 최선을 다하지만, 어떤 편차가 생겨도 프로그램의 동작은 영향을 받지 않는다. 단일 인스턴트 내에서 스레드 실행은 완전 순서(total order)를 갖도록 정렬되어 레이스를 제거하고 결정성을 보장한다.
SSM 위에 구축된 Scoria는 새로운 언어 기능을 빠르게 프로토타이핑하고 언어 설계 실험을 단순화하기 위해 설계된 프로그래밍 언어다. 우리 컴파일러는 스택 대신 힙에 상주하는 활성 레코드(activation record)에 제어 상태와 모든 지역 변수를 저장하는 C 함수로 동시성을 구현한다. 이는 프로그래머가 Protothreads로 작성할 법한 코드와 유사하지만, Scoria는 지역 변수 및 블로킹 wait 문 같은 더 친숙한 추상화를 제공한다.
이 논문은 Scoria의 현재 상태와 개발 과정에서의 선택을 다음과 같이 설명한다.
Scoria 같은 실시간 프로그래밍 언어를 동기부여하기 위해, 임베디드 시스템 프로그래밍에서 “hello world”로 흔히 쓰이는 “blinky” 애플리케이션을 생각해보자. 이는 지정된 주파수로 LED를 토글한다. 그림 3은 Zephyr 실시간 운영체제(RTOS)에 포함된 예제를 보여준다.
C로 작성된 blinky는 임베디드 프로그래밍 프레임워크의 기본 I/O 및 타이밍 기능을 보여주기에는 충분하지만, 타이밍 동작을 잘못 전달한다. blinky는 실제로 2초마다 한 번 깜빡이지 않는다. 이 불일치는 두 가지 지연 원인 때문이다. 루프마다 LED 토글에 시간이 걸리며(k_msleep 호출 사이), k_msleep 타이머가 만료된 뒤 Zephyr가 blinky 프로그램을 다시 스케줄하고 재개하는 데도 시간이 든다.
LED 토글 시간이 Δl, 재스케줄링 시간이 Δs라면 각 루프는 1000ms가 아니라 1000 + Δl + Δs ms가 된다. 시간이 지나면서 이 지연은 누적되어 이상적인 동작에서 점점 뒤처지는 드리프트로 나타난다(그림 4). 이 지연을 보상해 드리프트를 제거하려면, blinky는 이상적인 타이밍을 따라잡는 데 필요한 만큼만 잠들어야 한다. 이를 위해 “보정된” 구현(그림 A4)은 논리적 시계(얼마나 시간이 흘렀어야 하는지)를 유지하고, 논리 시간과 실제 시간의 차이에 따라 sleep 시간을 줄인다. 결과 동작(그림 5)은 드리프트가 없고 Δs의 안정적인 위상 오차만 가진다.
하지만 이 기법은 복잡성을 대가로 한다. 그림 A4의 구현은 45 LOC이며 Zephyr API와 깊게 얽혀 있다. 특정 설정 코드는 단순화/생략할 수 있지만, 타이머를 인터럽트 핸들러로 처리해야 하므로 프로그램의 제어 흐름과 타이밍 로직이 난독화된다. 또한 이 기법만으로는 순차 프로그램을 넘어 확장하기가 어렵다. 여러 동시 스레드가 제한된 하드웨어 타이머들을 동기화해 공유해야 할 수 있기 때문이다.
Scoria는 논리적 시간 개념을 동시성 프로그래밍 모델에 통합하여 이러한 복잡성을 극복한다. 이를 통해 Scoria는 플랫폼 의존적인 타이머 API 위에서 추상화하면서 의도한 타이밍 동작을 표현하게 해준다. Scoria는 런타임이 가능한 한 지원되는 타이밍 능력을 활용해 의도한 동작을 근사하도록 한다. 비교를 위해 그림 A1은 Scoria로 작성한 blinky를 보여주며, 15 LOC에 불과하다.
인터럽트 핸들러 사용이 코드를 불필요하게 복잡하게 만드는 또 다른 예가 그림 6에 있다. 두 버튼의 눌림 시퀀스에 따라 두 LED 중 하나를 켜는 간단한 프로그램의 인터럽트 핸들러다. 이 프로그램은 이전에 어떤 버튼이 눌렸는지 기억해야 하므로 전역 변수에 의존해야 하고, 이후 LED를 켜기 전에 그 전역 상태를 조회해야 한다.
Scoria 버전(그림 7)은 전역 변수에 의존하지 않는다. 어떤 코드가 실행 중인지로부터 어떤 이벤트가 발생했는지 알 수 있으므로 전역이 아니라 지역 정보에 기반해 동작한다.
그림 8은 Scoria의 핵심 API를 나열한다.
Scoria는 IoT 애플리케이션을 위한 임베디드 도메인 특화 언어이다. 동시성과 타이밍 제어 시설을 통해 IoT 요구에 맞게 특화되어 있어, 학습/컴파일/최적화는 쉬워지는 대신 범위를 벗어난 작업은 더 어려워질 수 있다. Scoria는 호스트 언어 Haskell에 임베딩되어, 기존 Haskell 도구와 컴파일러의 이점을 누리면서도 사용자에게는 편리한 새로운 추상화를 제공한다.
Haskell은 예를 들어 파서와 타입 체커를 제공하며, 우리는 호스트 수준 평가를 일종의 매크로 시스템으로 사용한다(V절 참조). 이런 접근은 Scoria를 훨씬 빠르게 개발하게 했고 새로운 언어 기능을 실험하기 쉽게 했다. 우리는 Edwards & Hui[3]의 제안을 기반으로 Scoria를 모델링하되 Haskell 임베딩에 맞게 적응했다.
그림 8의 핵심 API는 Scoria의 원시 연산으로 생각할 수 있지만, 실제로는 Haskell의 do 표기법으로 합성된 Haskell 함수들이다. 임베디드(Scoria) 값은 Exp a 타입이며, 호스트 언어 값은 a 타입이다. 호스트 수준 값은 Scoria 컴파일 시점에 평가된다.
routine 키워드는 Scoria 프로시저의 본문을 도입한다. Scoria 컴파일러는 routine 블록 밖의 모든 Haskell 코드를 단순히 실행한다. 자세한 내용은 V절에서 다룬다.
변수 참조는 var로 만들고, <∼(“assign”) 연산자로 값을 대입하며, deref로 읽는다. changed는 주어진 참조가 현재 인스턴트에서(예: 동시 스레드의 대입에 의해) 쓰였는지 여부를 반환한다.
after는 미래 인스턴트에서 참조에 대한 대입을 스케줄한다. 첫 번째 인자는 (현재 인스턴트 기준) 지연 시간이고, 나머지는 업데이트할 변수와 줄 값인데(이 값은 즉시 평가된다).
wait는 주어진 변수(참조) 중 하나 이상이 즉시 대입(<∼) 또는 예약 대입(after)에 의해 쓰일 때까지 블로킹한다. wait는 단일 참조 또는 다양한 크기의 참조 튜플을 받을 수 있다. Haskell의 타입클래스[6]로 이를 오버로드한다.
if-then-else와 while은 일반적인 조건/루프 구문이다. 각 분기 코드는 블로킹하거나, 블로킹하는 함수를 호출할 수 있다.
마지막으로 fork는 병렬 함수 호출 구성으로, 동시 실행되는 자식 프로세스를 생성하고 모든 자식이 종료될 때까지(단지 블로킹된 상태가 아니라) 블로킹한다. 자식들의 순서는 중요하며, 인스턴트 내 실행 순서를 규정해 완전 순서에 의한 결정적 동시성을 강제한다. 특히 먼저 실행되는 자식은 같은 인스턴트에서 이후 자식이 읽을 공유 변수를 쓸 수 있지만, 그 반대는 불가능하다(먼저 실행되는 자식은 이후 자식이 과거 인스턴트 또는 예약 업데이트에서 대입한 데이터만 읽을 수 있다).
그림 2에서 보듯 Scoria는 컴파일 시점에 실행되는 Compile 모나드도 제공한다. 이는 주변장치 같은 것을 정적으로 초기화하고, 프로그램 나머지 부분에서 접근 가능하게 만드는 데 사용된다. 예를 들어 사용자가 전역 변수 gv를 만들고 메인 루틴에 참조를 공급하려면 다음과 같이 할 수 있다.
haskellprogram :: Compile () program = do gv <- global @Word64 let ?gv = gv schedule main main :: (?gv :: Ref Word64) => SSM () main = routine $ ?gv <∼ 5
Scoria 프로그램을 시작하려면 적어도 하나의 루틴을 schedule 지시어로 ready 큐에 추가해야 한다. schedule의 각 호출은 더 낮은 우선순위 레벨에 루틴을 추가한다. 먼저 스케줄된 것이 인스턴트마다 먼저 실행되며, fork와 유사하다.
Edwards & Hui[3]는 C로 작성된 SSM RTS를 제공한다. 우리는 Scoria 프로그램의 추상 구문을 받아 이 RTS를 호출하는 C 코드를 생성하는 코드 생성기를 작성했다. 생성된 C 코드는 플랫폼 독립적이며, 예를 들어 미래의 웨이크업 알람을 설정하는 등의 플랫폼 특화 코드가 추가로 필요하다. 우리는 Zephyr OS[4]용 바인딩과, VI절에서 설명하는 트레이스(trace) 플랫폼 바인딩을 작성했다.
Scoria 프로그램은 변수를 통해 환경과 통신한다. Scoria 런타임은 인터럽트 서비스 루틴(ISR)—전통적인 비동기 C 구현의 콜백 함수에 해당—으로부터 입력 이벤트를 수집하여 SSM 이벤트 큐로 전달한다. 출력 변수에 대한 쓰기는 특수 I/O 핸들러 루틴이 관찰하며, 보통 실제 출력 수행을 위해 외부 C 함수를 호출한다.
그림 9는 입력 이벤트 처리, 시스템 타이머 관리, 인스턴트마다 시스템을 실행하는 Scoria 런타임 구조를 보여준다. 메인 tick 루프(그림 11)는 ISR이 넣은 이벤트를 가져오고(그림 10), Edwards & Hui[3]의 SSM RTS의 tick()을 호출해(Scoria 컴파일러가 생성한 코드 포함) 시스템을 한 인스턴트 실행한 다음, 다음 예약 이벤트 시각에 맞춰 알람을 설정하고 알람 또는 주변장치 입력으로 깨어날 때까지 잠든다.
입력 이벤트는 ISR로부터 수집되어 결국 Scoria 프로그램에 예약된 변수 업데이트로 나타나지만, 안전성과 효율을 위해 여러 단계로 처리된다. Scoria 런타임은 과도한 동기화 비용을 피하기 위해 두 개의 이벤트 큐를 유지한다. 입력 큐(input queue)는 ISR이 비동기적으로 적재하고 메인 tick 루프가 비우며, 스레드 안전하게 만들기 쉬운 단순 링 버퍼다. 반면 메인 이벤트 큐(event queue)는 우선순위 큐로, tick 루프와 여러 동시 루틴이 순서를 바꿔가며 이벤트를 추가/제거할 수 있다. 그러나 이들은 동기적으로 실행되므로 메인 이벤트 큐는 비동기적으로 접근되지 않으며 스레드 안전일 필요가 없다.
두 큐가 필요한 이유는 여러 가지다. 동기 가설은 계산이 순간적이라 tick() 호출이 즉시 반환한다고 가정하지만, 현실에서는 물리 시간이 흐른다. 외부 이벤트를 도착 즉시 시스템에 전달하면 tick()이 미래를 들여다보게 되어 결정성이 깨지고 데이터 레이스에 취약해진다. 또한 외부 이벤트에 타임스탬프가 없다면, 시스템이 이전 이벤트를 관찰하기도 전에 다음 외부 이벤트가 도착해 덮어써 버려 사실상 이벤트가 유실될 수 있다. SSM 런타임은 단일 시간에 대해 참조당 하나의 outstanding 이벤트만 허용한다.
그림 10은 입력 인터럽트 핸들러의 구조다. 먼저 현재 시스템 시간을 기록하고, 그 타임스탬프와 주변장치의 새 데이터, 그리고 바인딩된 참조를 포함하는 이벤트를 입력 큐에 넣으려 시도한다. 입력 큐는 modest한 입력 백로그를 수용할 만큼 충분히 큰 링 버퍼이며, 이벤트 드롭/덮어쓰기를 피한다. enqueue/dequeue는 복사를 최소화하기 위해 allocate/commit 및 peek/release 프로토콜로 in-place 수행된다.
ISR에서는 가능한 빨리 현재 시간을 기록하고 입력 큐에 공간을 확보하는 것이 중요하다. 그래야 입력 이벤트가 비감소 타임스탬프(nondecreasing timestamp)를 갖는다. 더 높은 우선순위 인터럽트가 낮은 우선순위 인터럽트 처리 중 발생할 수 있지만, 낮은 우선순위 인터럽트의 타임스탬프를 캡처한 뒤에만 발생하게 된다.
그림 11은 메인 tick 루프를 보여준다. 이는 OS가 제공하는 세마포어(ssm_sem)를 기다리며, 세마포어는 주변장치 ISR 또는 시스템 클록 ISR에서 post된다. tick 루프의 각 반복에서 런타임은 입력 큐를 확인해 SSM 이벤트 큐에 스케줄할 이벤트가 있는지 본다. 시스템 타이머가 단조 증가한다고 가정하므로 입력 큐의 이벤트는 증가 순서다. 그러나 SSM 프로그램의 논리 시간이 물리 시간보다 뒤처진 상태에서 외부 입력이 도착했다면(input->time > model_time), 또는 입력이 없으면 런타임은 ssm_tick()을 호출해 SSM 프로그램을 실행한다. 마지막으로 처리할 입력도 없고 실행할 내부 이벤트도 없으면, 메인 스레드는 다음 예약 이벤트 또는 새 입력이 올 때까지 세마포어에서 블로킹하며 잠든다.
입력과 마찬가지로 출력 주변장치도 일반 Scoria 참조에 바인딩되며, 그 참조에 대한 쓰기는 출력으로 환경에 방출된다. 내부적으로 그 쓰기는 시스템 제공 C 출력 함수를 호출하는 방식으로 실제 출력이 된다.
출력을 구현하며 고려해야 했던 설계 문제는, 한 인스턴트 안에서 정확히 언제 출력 함수를 호출할지였다. SSM 모델은 인스턴트의 모든 코드가 0 시간에 실행된다고 말하지만, 실제로는 인스턴트의 코드는 유한한 시간 구간에 펼쳐져 실행된다. 출력 함수를 너무 일찍 호출하면 같은 인스턴트에서 이후에 출력 변수에 대한 업데이트가 발생해도 무시될 수 있고, 너무 늦게 호출하면 출력 지연이 생기며 관련 없는 계산의 실행 시간에 의해 지터(jitter)까지 생길 수 있다.
예를 들어 다음 세 개의 동시 프로세스를 고려하자(우선순위가 높은 순서대로 나열됨).
haskellw1 :: Ref Button -> Ref LED -> SSM () w1 button led = routine $ do while true (do wait button led <∼ true) w2 :: Ref Button -> Ref LED -> SSM () w2 button led = routine $ do while true (do wait button led <∼ false) r :: Ref Button -> SSM () r button led = routine $ do while true (do wait button -- 어떤 비용 큰 계산 )
LED가 w1의 true 대입 직후 즉시 점등된다면, w2의 false 대입이 누락되어 오래된 값이 방출될 수 있다. 반대로 LED를 가능한 한 늦게(인스턴트 끝에서) 점등한다면, 시스템은 r이 끝날 때까지 기다려야 해서 지연이 발생한다. 애플리케이션에 따라 어느 쪽도 허용되지 않을 수 있다.
우리 설계는 물리적 출력이 언제 일어나야 하는지 사용자가 결정할 수 있도록, 출력 장치 요청 시 얻는 출력 핸들러 프로세스 안에 출력을 캡슐화하고 사용자가 이를 스케줄하도록 한다. 어떤 프로세스가 참조에 즉시 대입을 수행하면 그 대입은 더 낮은 우선순위 프로세스에만 보인다. 단, 지연 대입은 이런 제약이 없으며 대기 중인 모든 프로세스를 깨운다.
예를 들어 사용자가 출력 핸들러를 w1과 w2의 가능한 대입 이후에, 그러나 r 이전에 실행되도록 스케줄하려면 다음처럼 할 수 있다.
haskellinitProgram :: Compile () initProgram = do button <- input 0 (led, handler) <- output 0 schedule $ w1 button led schedule $ w2 button led schedule $ handler schedule $ r button led
이는 r이 효과 있는(effectful) 즉시 대입을 수행하는 것을 배제하지만, r의 효과 있는 지연 대입은 여전히 허용한다.
이 방식은 지터의 두 가지 원인을 사용자에게 제어 가능하게 한다. 첫째는 계산 시간 변동으로 인한 지터다. 출력 핸들러가 같은 인스턴트 내에서 더 높은 우선순위 프로세스 이후에 스케줄되면 실제 출력 시각은 앞선 프로세스 속도에 의존한다. 이를 없애려면 출력 핸들러를 인스턴트 시작에 스케줄하면 되지만, 그 대신 즉시 대입의 표현력이 제한된다. 신뢰성과 표현력 간 균형은 사용자 몫이다.
둘째는 입력 응답 시간 차이에서 오는 지터다. 런타임은 이미 깨어 있을 때가 잠들어 있을 때보다 입력을 약간 더 빠르게 처리할 수 있다. 어떤 입력이 즉시 출력으로 이어져야 한다면, 입력이 시스템이 잠든 동안 도착했는지 깨어 있는 동안 도착했는지에 따라 입력-출력 지연이 달라진다.
Scoria는 Haskell 라이브러리로 구현되며, 임베디드 언어 인터페이스를 노출해 Haskell의 파서와 타입 체커를 재사용하고, 빠른 언어 개발을 위해 Haskell의 다양한 기능을 활용한다. 이 절에서는 언어 인터페이스를 견고하고 표현력 있으며 쓰기 쉽게 만들기 위해 사용하는 임베딩 기법을 설명한다.
Haskell의 함수 적용은 타입 안전성이 보장되도록 검사된다. routine body는 body와 같은 타입이므로, Scoria 프로시저는 여전히 일반 Haskell 함수처럼 보이고, 호출은 표준 Haskell 함수 적용으로 이뤄진다. 그 결과 Haskell 컴파일러가 Scoria 프로시저 호출의 타입을 ‘공짜로’ 검사해준다. 이런 타입 검사는 언어 API 전반에서 일어나 불법 프로그램을 구성하기 어렵게 한다.
임베디드 언어의 호스트 언어는 매크로 시스템처럼 사용될 수 있어 언어 설계 실험이 쉽다. 예로 고정 시간만큼 블로킹하는 delay 문을 생각해보자. 이는 Scoria나 SSM의 원시 연산은 아니지만, 기존 원시 연산으로 동작을 만들 수 있다.
haskelldelay :: Exp Time -> SSM () delay x = do sync <- var event after x sync event wait sync
이 Haskell 함수는 routine 키워드로 장식되어 있지 않으므로 호출 가능한 Scoria 루틴으로 취급되지 않는다. 대신 프로그램이 delay를 호출하면 Scoria는 delay 본문을 인라인한다. 하지만 매번 호출 지점의 활성 레코드에 새로운 지역 변수 sync를 추가한다는 단점이 있다.
대안으로 delay를 Scoria 프로시저로 만드는 구현은 다음과 같다.
haskelldelay' :: Exp Time -> SSM () delay' x = fork [delayRoutine x] delayRoutine :: Exp Time -> SSM () delayRoutine x = routine $ delay x
프로그램이 delay'를 호출하면 인라인된 문은 보조 루틴 delayRoutine을 호출하는 fork 문이 된다.
Scoria는 routine 키워드로 Haskell 함수와 Scoria 프로시저를 구분한다. 후자의 경우 컴파일러는 본문을 직접 평가하는 대신 루틴 정의에 해당하는 AST 노드를 구성해야 한다. 이런 간접층 덕분에 Scoria 프로그램은 루틴 본문을 끝없이 인라인하지 않고 재귀를 수행할 수 있다.
그러나 루틴 정의를 Haskell에 단순히 임베딩할 수는 없다. 함수가 자신의 이름과 인자 이름을 반영(reflect)해야 하기 때문이다. 실제로 초기 컴파일러는 make_routine을 사용했는데, 루틴 이름과 인자 이름을 추가 인자로 요구했다. 이는 불필요한 문법 중복을 만들었다.
haskellf = make_routine "f" ["x", "y"] $ \x y -> body
이를 해결하기 위해 Scoria는 Haskell 컴파일러 GHC의 플러그인 지원을 활용해 make_routine에 필요한 인자를 추론한다. 플러그인은 routine 호출을 찾아 필요한 소스 정보를 포함한 make_routine 호출로 대체한다. 예를 들어 아래 코드는 위의 보일러플레이트로 변환된다.
haskellf x y = routine body
Edwards & Hui[3]를 따라, 우리는 모델 시간의 인스턴트를 단계적으로 진행하지만 벽시계 시간과 동기화하려고 시도하지 않는 Haskell 인터프리터를 구현했다. 이 인터프리터를 컴파일러 출력 검증을 위한 기준(reference)으로 사용한다.
Scoria 컴파일러를 디버깅하기 위해 QuickCheck(QC)[5]를 사용했다. QC는 무작위 값을 생성해 성질(property)을 검사하고, 성질이 반례로 깨지면 더 작은 실패 사례를 찾도록 테스트를 shrink한다. QC가 생성한 무작위 프로그램은 보통 너무 커서 shrink 없이 진단하기 어렵다.
우리는 두 가지 성질을 테스트한다.
생성된 C 코드가 컴파일되고 Valgrind 기준 메모리 오류/누수 없이 실행되는지. 이를 통해 이벤트 큐 관련 버그를 찾았다. 변수가 해제될 때 예약된 이벤트가 취소되지 않아 RTS가 오래된 포인터로 업데이트를 수행해 상태를 손상시키는 문제였다. 우리는 루틴이 반환하기 전 지역 변수에 대한 예약 업데이트를 모두 해제하도록 보장해 수정했다.
그림 12처럼 생성 프로그램의 의미적 동등성. 테스트 시 컴파일러는 생성 코드에 print 문을 추가해 이벤트 트레이스를 만들고, 이를 기준 인터프리터 트레이스와 비교한다. 이는 C 생성 문제를 찾는 데 도움이 됐다. 특히 초기에는 Scoria의 부호 있는 정수를 C의 signed integer로 구현했는데, 테스트가 오버플로 산술(정의되지 않은 동작)로 인해 분기 결과가 달라지는 사례를 찾아냈다. 이를 C의 더 잘 정의된 unsigned integer로 바꿔 해결했다.
우리는 20개의 버그를 기록했으며 대부분 의미 오류였다(표 I).
Zephyr 기반 런타임 시스템의 성능을 다양한 부하로 평가한다. 모든 실험은 Nordic Semi NRF52840-DK 보드(64 MHz Cortex-M4, 256 kB RAM, 1 MB flash, BLE 지원, 16 MHz 크리스탈 타임 베이스)에서 수행한다. 펄스 생성기로 GPIO 입력 핀을 구동해 부하를 주고, 오실로스코프로 출력 핀을 측정한다.
높은 입력 부하에 대한 견고성을 평가하기 위해 그림 13의 작은 Scoria 주파수 카운터를 테스트한다. 이 프로그램은 버튼 입력 이벤트를 1초 동안 카운트해 주파수를 측정한다. 보고되는 카운트는 버튼 한 번 누름에 상승/하강 에지 두 이벤트가 있으므로 실제 주파수의 두 배다. 벤치마크를 위해 생성 코드에 print 문을 넣어 주파수를 보고하며, 높은 부하에서 보고 오버헤드가 카운팅에 영향을 주지 않도록 카운팅과 보고를 번갈아 수행한다.
sw 참조는 NRF52840-DK의 input 0에 바인딩된다. 해당 GPIO에 함수 발생기를 연결해 다양한 주파수의 펄스를 생성했다. 주파수 카운터는 29 kHz(입력 14.5 kHz, 에지당 1 이벤트)까지 ±2 Hz 오차로 측정 가능했다. 이를 넘으면 입력 큐가 소모 속도보다 빠르게 채워져 이벤트가 드롭되고 스래싱(thrashing)이 발생한다. 입력 주파수를 14 kHz 아래로 낮추면 스래싱에서 회복한다.
SSM 이벤트 스케줄링 오버헤드를 측정하기 위해, Scoria 버전과 기능적으로 동일한 수작업 C 코드(그림 14)를 비교한다. C 구현은 SSM 스케줄러를 생략하고 Zephyr의 동일한 세마포어 API로 main_loop에서 동기 제어 흐름을 직접 관리한다. 또한 입력 큐를 생략하고 입력 ISR에서 count를 직접 증가시킨다. 이 최적화 버전은 34 kHz까지 ±5 Hz 오차로 측정 가능했지만, 더 장황하고 오류 가능성이 높으며 표현력과 유연성이 떨어진다.
동기 로직이 없는 시스템을 이해하기 위해 그림 15의 “button-to-blink”를 먼저 평가한다. 버튼이 눌리면 LED를 켜고, 놓이면 끈다. LED 핸들러는 메인 b2b 프로그램 뒤에 스케줄해, 즉시 대입이 인스턴트 끝에서 LED에 기록되게 한다.
b2b의 즉시 대입은 실질적으로는 피할 수 있지만 이론적으로는 중요한 지연을 유발한다. 이를 ‘휴지(at-rest) 입력 지연’ δr이라 부르며, 시스템이 잠에서 깨어 외부 입력에 반응하는 데 걸리는 시간이다. 프로파일링 결과 NRF52840-DK에서 δr은 약 60 μs다.
입력이 1/δr보다 높은 빈도로 들어오면, 시스템은 다음 입력이 오기 전에 하나의 입력을 처리하지 못해 δr이 중요한 수치가 된다. 시스템이 벽시계 시간보다 더 뒤처질수록 동작은 악화되어 Scoria 프로그램의 시간적 동작이 없는 비동기 시스템으로 변한다.
관련 지표로 ‘비행 중(in-flight) 입력 지연’ δf도 있다. 이는 바쁜 시스템이 외부 입력에 반응하는 데 걸리는 시간이다. 이벤트가 δf 내 간격으로 도착하면 메인 tick 루프가 입력 큐를 다시 확인할 때 이미 새 이벤트가 들어 있으므로, 잠들지 않고 ticking을 계속할 수 있다. δf는 잠/깨움 시간을 제거하므로 δr보다 짧고, 보드에서 약 45 μs로 측정되었다.
표 II에서 보듯 “button-to-blink”는 12 kHz까지 유지 가능했고, 그 이상에서는 스래싱 및 이벤트 드롭이 발생했다. 그러나 연속 이벤트 간격이 δr보다 짧아지면 시스템이 반응하더라도 인스턴트당 계산 시간 예측이 어려워지고, 이는 8 kHz 이상에서 지터 증가로 나타났다(구형파 반주기 62.5 μs).
δr = 60 μs의 원인을 찾기 위해, 특정 이벤트에 대해 GPIO 핀에 4비트 코드를 출력하고 로직 분석기로 기록해 그림 16의 타임라인을 재구성했다. 이 방법은 침습적이지만 GPIO 쓰기 시간이 60 ns에 불과해 측정값에 비해 무시 가능했다. 우리는 Zephyr의 ISR/프로세스 스케줄링이 24.3 μs를 직접 도입하고, ISR에서 sem_post로 tick 루프 스레드를 깨우는 Zephyr 세마포어 구현이 추가로 5.8 μs를 도입함을 확인했다. 표 II의 δr과 그림 16은 Zephyr 버전 차이로 수치가 약간 다르다.
비교를 위해 C로 작성된 두 개의 Zephyr 프로그램의 δr도 측정했다. 하나는 비현실적으로 버튼을 지속 폴링하며 이론적 최소 응답 시간을 나타낸다(그림 A2). 다른 하나는 ISR이 버튼 이벤트를 Scoria 런타임과 동일한 링 버퍼에 넣고 세마포어로 메인 스레드를 깨우는 현실적인 버전이다(그림 A3). 메인 스레드는 링 버퍼에서 이벤트를 꺼내 LED를 업데이트한다. 이는 SSM “tick” 스케줄러 없이 Scoria 런타임을 모델링한다. 입력 주파수를 2 kHz로 표준화한 결과는 표 III이다. Scoria 버전이 세 가지 중 가장 느리다.
그림 16은 Zephyr가 입력 ISR을 찾아 실행하는 데 약 33 μs가 걸렸음을 보여주며, 이는 표 III의 현실적 Zephyr 예제의 응답 시간과 비슷하다. Scoria 런타임의 오버헤드는 주로 입력 큐에서 이벤트를 꺼내 Scoria 환경에 삽입하고, Scoria tick 루프를 호출할 프로세스를 스케줄하는 부분(그림 16의 녹색 tick loop 바)이다.
이제 그림 1의 신호 발생기로 돌아가, 생성 가능한 최고 주파수(최단 주기)를 측정한다. 평가를 위해 버튼 누름으로 반주기 hperiod를 2 μs씩 선형 증감하도록 예제를 수정했다(그림 A10).
그림 1의 신호 발생기는 반주기 76 μs(약 6.6 kHz)까지 안정적으로 생성했으며, 지터는 500 ns 미만으로 오실로스코프 정밀도 수준이었다. 반주기 70–76 μs에서는 정확도/일관성이 악화되어 반주기가 60.2–83.4 μs 사이에서 변동했다. 더 낮은 반주기에서는 계산 부하에 압도되어 출력이 멈춘다. 반주기를 76 μs 이상으로 늘리면, 고주파 출력 버스트 이후 벽시계 시간을 따라잡는 과정에서 결국 회복한다.
표 IV는 Scoria 구현과 비교 가능한 C 구현(그림 A9)을 비교한다. 두 구현의 최대 주파수를 측정했다. C 구현은 알람이 울리면 링 버퍼를 채우고 세마포어로 메인 스레드를 깨운 다음 LED를 토글한다. Zephyr 프로그램이 Scoria보다 성능이 좋다(표 IV). 반주기가 파형 이벤트 하나를 처리하는 시간보다 작아지면 신호가 불안정해진다. Scoria 런타임은 이벤트 반응 시(그림 16) 오버헤드가 있으므로 Zephyr 프로그램만큼 낮은 반주기를 처리하지 못한다. Zephyr 프로그램은 약 두 배 높은 주파수를 생성할 수 있다.
조금 더 복잡한 예로, BLE로 통신하는 두 보드 애플리케이션을 작성했다. 한 보드는 BLE로 가변 주파수를 광고(advertise)하고, 다른 보드는 이를 스캔한다. 메시지를 가로채면 해당 주파수의 신호를 생성하면서 계속 스캔한다.
C 버전은 콜백, 클록, 카운터, 전역 상태 등을 관리해야 하며 이들 어디든 버그가 숨어 있을 수 있다. Scoria 버전에는 이들이 없고, 구현 방식이 아니라 애플리케이션 로직만 기술한다. 그림 A5–A8에 두 버전을 보였다.
Scoria의 프로그래밍 모델 SSM[3]은 동기 가설(synchrony hypothesis)을 채택한다. 동시 실행은 동기화된 0시간 인스턴트들의 시퀀스로 진행된다는 가정이다[7]. 디지털 하드웨어 설계자는 반세기 넘게 이 모델을 사용해 왔고, 1990년대 프랑스 연구자들은 이 개념을 소프트웨어 언어 Lustre, Esterel, Signal로 가져왔다[8]–[10]. 동기성은 비동기의 비결정성을 제거해 동시 타이밍 동작에 대한 추론을 단순화했으며, 이후 Timber[11], Lingua Franca[12] 같은 언어 및 Haski[13], Copilot[14] 같은 EDSL에도 등장한다.
그러나 “프랑스 학파” 언어(Lustre, Esterel, Signal)는 시간(time)을 일급 언어 구성요소로 포함하지 않는다. 대신 연속 인스턴트로 계산을 기술하고, 계산이 없는 인스턴트까지 포함해 모든 인스턴트마다 주기적으로 호출되어야 하는 tick 함수를 생성하며, 이를 위해 사이클릭 이그제큐티브(cyclic executive)가 필요하다. 반면 Scoria의 after는 구체적 시간 지연을 지정(또는 계산)할 수 있고, 희소 실행 모델 덕분에 런타임이 비활성 인스턴트를 계산할 의무가 없다.
또한 많은 동기 언어(Lustre, Signal, Lingua Franca)는 데이터플로 지향이며, 정적인 유한 데이터플로 그래프를 명시하고 고정 채널로 통신한다. 반면 Scoria는 명령형이며(제어 흐름을 기술), fork로 프로세스를 동적으로 생성하고, 데이터플로는 암묵적이다. Esterel도 명령형이고 Scoria의 fork/wait 같은 동시 제어 흐름 구문을 포함하지만, 함수 호출이 없어 프로세스 수가 유한하다는 제약이 있다[9].
Haski[13]와 Copilot[14]은 Haskell에 깊게 임베딩된 DSL[15]로 마이크로컨트롤러용 C 코드를 생성한다. Haski는 보안 IoT 프로그래밍을 위한 Lustre의 임베딩이고, Copilot은 상수 시간/공간 C 코드를 약속하는 모니터링 EDSL이다. 둘 다 데이터플로 언어여서 Haskell 임베딩은 Scoria의 구문 트리 대신 추상 스트림을 구성한다.
수작업 회귀 테스트 스위트가 컴파일러 개발의 표준이지만, 무작위 생성 및 shrink를 이용한 테스트 케이스 생성도 많이 연구되어 왔다[16], [17]. 우리 작업은 RandIR[18]과 가장 유사하다. RandIR은 ScalaCheck[19]로 LMS[20](임베디드 DSL을 위한 코드 생성 프레임워크)를 테스트하며, 테스트 후보를 Scala로 번역해 다른 언어 백엔드와 비교한다. 반면 Scoria의 테스트 오라클은 생성된 Scoria IR을 직접 인터프리트한다.
잘 확립된 언어는 여러 컴파일러 구현(C의 CSmith[21])이나 최적화 레벨(예: GHC에 대한 Pałka 등[22])을 이용한 차분 테스트가 가능하다. Scoria는 신생 언어라 이런 기회가 제한되지만, Scoria의 동기 의미론을 활용하는 최적화를 구현하기 시작하면 테스트 주도 컴파일러 개발 접근을 확장해 버그를 찾을 계획이다.
Ptides[23]는 이산 이벤트 실시간 시스템을 구현하는 프로그래밍 모델로, SSM과 Scoria에 영감을 주었다. 다만 Ptides는 분산 이산 이벤트 시스템을 구현하며, 더 이른 타임스탬프 메시지가 더 이상 도착할 수 없을 때만 메시지를 처리하는 것이 안전하다고 신중히 판단한다. 우리는 Ptides를 따라 분산 Scoria를 구현할 계획이다.
Scoria는 현재 제한된, 사전 정의된 데이터 타입 집합만 지원한다. 배열 및 사용자 정의 대수적 데이터 타입(ADT) 같은 더 복잡한 타입을 지원할 예정이다. McDonnell 등[24]을 기반으로 Haskell 프로그래머에게 익숙한 패턴 매칭 인터페이스를 제공할 수 있다. ADT는 IoT 프로그램 작성에 잘 맞을 것으로 보이며, 주변장치에 대한 안전한 인터페이스를 개발하는 데 특히 도움이 될 것이다.
GHC에는 최근 선형 타입(linear types)[25]이 추가되어, 특정 불변식을 타입에 인코딩하고 컴파일러가 검사할 수 있다. 예를 들어 output과 schedule에 더 정밀한 선형 타입을 주면 I/O 핸들러를 스케줄하도록 강제할 수 있다.
haskelloutput :: Int -> (Ref LED -> SSM () %1 -> Compile ()) -> Compile () output i continuation = ... schedule :: SSM () %1 -> Compile () schedule handler = ...
output의 타입은 LED를 식별하는 정수와, LED 참조와 핸들러를 인자로 받는 continuation을 받음을 나타낸다. 여기서 %1은 handler가 정확히 한 번 소비되어야 함을 뜻한다. 이는 다음처럼 사용할 수 있다.
haskellprogram :: Compile () program = output 0 $ \ref handler -> do schedule handler -- 이후 프로그램 설정
핸들러가 continuation 내부에서 스케줄되므로, 핸들러 스케줄을 잊거나(또는 여러 번 스케줄)하면 GHC 컴파일 시간 오류가 된다. 그렇지 않으면 이는 코드 생성 시점(호스트 언어 런타임)에서만 검사 가능하다.
실험적으로 Scoria는 C/Zephyr로 직접 구현한 대응물보다 상당히 느린 몇 가지 비효율 원인을 확인했다. C/Zephyr 버전은 연관 ISR이 호출되자마자 이벤트를 처리하는 반면, Scoria는 메시지를 큐에 넣고, tick 스레드를 깨워 이벤트를 꺼내 프로그램을 실행해야 하므로 오버헤드가 생긴다.
벤치마크 프로그램은 Zephyr를 이용하는 대신 베어메탈에서 필요한 OS 기능을 직접 개발하면 더 나은 성능을 낼 수 있다. Zephyr는 많은 편리한 추상화에 빠르게 접근하게 해주지만, 다양한 종류의 애플리케이션에 적합하도록 매우 일반적으로 개발되어 있다. Scoria 요구에 맞게 이 추상화를 맞춤화할 수 있다면 속도 향상을 얻을 수 있을 것이다. 또한 다른 운영체제도 시도할 예정이다.
Scoria는 아직 단순한 언어지만, Scoria 프로그램은 짧고 군더더기가 없으며 컴파일러는 자원 효율적인 코드를 생성한다. Scoria를 EDSL로 구현한 것은 언어 및 컴파일러 개발에 여러 장점을 가져왔다. 호스트 언어가 제공하는 기능과 도구를 활용해 언어를 빠르게 프로토타이핑할 수 있었다. 원시 연산으로부터 delay 같은 새로운 연산을 도출하는 것은 쉽고 투명하다. EDSL은 AST를 구성하는 Haskell 라이브러리에 불과하므로, 프론트엔드와의 조율 부담이 적은 상태에서 컴파일러 내부를 쉽게 노출하고 수정하는 실험이 가능했다.
우리는 초기에 Scoria 인터프리터를 개발했고, 이를 언어 오라클로 사용하여 컴파일러를 자신 있게 반복 개선할 수 있었다. 인터프리터를 기준으로 QuickCheck를 활용해 언어의 코너 케이스를 철저하고 효율적으로 테스트했으며, 그렇지 않았다면 훨씬 오래 걸렸을 여러 버그를 찾아냈다. shrinker는 최소 실패 테스트 프로그램을 만들어 각 버그의 원인을 쉽게 가리키게 해주었다.
Edwards & Hui[3]의 RTS를 Zephyr OS 바인딩으로 확장해 실제 하드웨어에서 Scoria 프로그램을 실행했다. I/O 주변장치는 Scoria 참조로 노출되어 Scoria 프로그램이 주변장치와 자연스럽게 대화할 수 있으며, IoT 애플리케이션에서 흔한 콜백 지옥을 피한다. 이 상호작용 모델 덕분에 GPIO와 BLE 등 일부 주변장치 지원을 쉽게 추가할 수 있었다.
Scoria는 I/O 처리 코드의 우선순위를 사용자 코드와의 관계에서 정밀하게 지정할 수 있어, 애플리케이션별 반응성/민감도 트레이드오프를 선택할 수 있다. 스트레스 테스트는 견고한 오버플로 동작을 가진 실시간 애플리케이션을 작성할 수 있음을 확인해준다.
Scoria는 아직 실험적 언어지만, 지금까지의 성과는 EDSL 구현이 실험적 언어 구현에 더 빠른 경로를 제공함을 보여준다. 또한 Scoria는 희소 동기 언어의 설계—특히 외부 세계와의 인터페이스를 어떻게 규정해야 하는가—에 대해 많은 질문을 던졌고, 이러한 문제에 대한 해결책을 빠르게 실험할 수 있는 길을 제공했다. Scoria에 대한 작업은 계속될 것이며, 최소한 유사 도메인의 언어 설계에 유용한 통찰을 제공할 것이다.
Hui와 Edwards는 NIH(National Institutes of Health) grant 1RF1MH120034-01의 부분 지원을 받았다. Krook과 Claessen은 Swedish Foundation for Strategic Research(SSF)의 Octopi 프로젝트(Ref. RIT17-0023) 및 Swedish research agency Vetenskapsrådet의 SyTeC 프로젝트(Ref. 2016-06204)의 지원을 받았다. 또한 본 논문에서 설명한 플러그인 작성에 도움을 준 Agustín Mista에게 감사한다.
[1] P. Hudak, “Building domain-specific embedded languages,” ACM Computing Surveys, vol. 28, no. 4es, pp. 196–es, Dec. 1996. https://doi.org/10.1145/242224.242477
[2] A. Dunkels, O. Schmidt, T. Voigt, and M. Ali, “Protothreads: Simplifying event-driven programming of memory-constrained embedded systems,” in SenSys ’06, 2006, pp. 29–42. https://doi.org/10.1145/1182807.1182811
[3] S. A. Edwards and J. Hui, “The sparse synchronous model,” in FDL, Sep. 2020, pp. 1–8. https://doi.org/10.1109/FDL50818.2020.9232938
[4] The Zephyr Project, “The Zephyr Project,” https://www.zephyrproject.org/, 2021.
[5] K. Claessen and J. Hughes, “Quickcheck: a lightweight tool for random testing of haskell programs,” in ICFP, vol. 35, no. 9, Sep. 2000, pp. 268–279. https://doi.org/10.1145/357766.351266
[6] C. V. Hall, K. Hammond, S. L. Peyton Jones, and P. L. Wadler, “Type classes in haskell,” ACM TOPLAS, vol. 18, no. 2, pp. 109–138, 1996.
[7] A. Benveniste et al., “The synchronous languages 12 years later,” Proceedings of the IEEE, vol. 91, no. 1, pp. 64–83, Jan. 2003.
[8] P. Caspi et al., “Lustre: A declarative language for real-time programming,” in POPL ’87, 1987, pp. 178–188. https://doi.org/10.1145/41625.41641
[9] G. Berry and G. Gonthier, “The esterel synchronous programming language: Design, semantics, implementation,” Science of Computer Programming, vol. 19, no. 2, pp. 87–152, 1992.
[10] A. Benveniste, P. Le Guernic, and C. Jacquemot, “Synchronous programming with events and relations: the signal language and its semantics,” Science of Computer Programming, vol. 16, no. 2, pp. 103–149, 1991.
[11] M. Carlsson, J. Nordlander, and D. Kieburtz, “The semantic layers of Timber,” in APLAS, 2003, pp. 339–356. https://doi.org/10.1007/978-3-540-40018-9_22
[12] M. Lohstroh et al., “Toward a lingua franca for deterministic concurrent systems,” ACM TECS, vol. 20, no. 4, pp. 1–27, Jul. 2021. https://doi.org/10.1145/3448128
[13] N. Valliappan et al., “Towards secure iot programming in haskell,” in Haskell 2020, 2020, pp. 136–150. https://doi.org/10.1145/3406088.3409027
[14] L. Pike et al., “Copilot: A hard real-time runtime monitor,” in Runtime Verification, 2010, pp. 345–359.
[15] J. Svenningsson and E. Axelsson, “Combining deep and shallow embedding for edsl,” in Trends in Functional Programming, 2013, pp. 21–36.
[16] A. Boujarwah and K. Saleh, “Compiler test case generation methods: a survey and assessment,” Information and Software Technology, vol. 39, no. 9, pp. 617–625, 1997.
[17] J. Chen et al., “A survey of compiler testing,” ACM Computing Surveys, vol. 53, no. 1, pp. 1–36, May 2020. https://doi.org/10.1145/3363562
[18] G. Ofenbeck, T. Rompf, and M. Püschel, “RandIR: differential testing for embedded compilers,” in Scala ’16, Oct. 2016. https://doi.org/10.1145/2998392.2998397
[19] R. Nilsson, ScalaCheck: The Definitive Guide. Artima Press, 2014.
[20] T. Rompf and M. Odersky, “Lightweight modular staging…,” Communications of the ACM, vol. 55, no. 6, pp. 121–130, Jun. 2012. https://doi.org/10.1145/2184319.2184345
[21] X. Yang et al., “Finding and understanding bugs in c compilers,” in PLDI ’11, 2011, pp. 283–294. https://doi.org/10.1145/1993498.1993532
[22] M. H. Pałka et al., “Testing an optimising compiler by generating random lambda terms,” in AST ’11, 2011, pp. 91–97. https://doi.org/10.1145/1982595.1982615
[23] P. Derler et al., “Ptides…,” Tech. Rep., 2008.
[24] T. L. McDonell et al., “Embedded pattern matching,” arXiv:2108.13114, 2021.
[25] J.-P. Bernardy et al., “Linear haskell…,” Proceedings of the ACM on Programming Languages, vol. 2, no. POPL, pp. 1–29, Jan. 2018. https://doi.org/10.1145/3158093
부록의 코드(그림 A1–A11)는 원문과 동일한 구조로 포함되어 있으며, 본문에서 언급한 예제(Scoria blinky, Zephyr C 구현들, BLE mime 예제들, 생성된 C 코드 등)를 보여준다.
(부록의 각 그림 캡션 및 코드 블록은 원문에 포함된 그대로이며, 길이 관계상 이 텍스트 버전에서는 그림 A1–A11의 코드 내용을 원문과 동일한 형태로 유지했다.)