무작위 프로그램을 생성해 Gleam 컴파일러의 버그를 찾는 구조 인식 퍼징 실험과 그 결과를 살펴봅니다.
게시일: 2026년 8월 25일 화요일
저는 Gleam의 변경 로그와 이슈 추적기를 정기적으로 확인합니다. 저는 이 프로젝트와 여기에 기여하는 사람들을 매우 좋아합니다. 하지만 코드 생성이나 Erlang과 JavaScript 간의 서로 다른 출력에 관한 이슈를 볼 때마다, 사실상 “모든 Gleam 프로그램을 계산해” 실행하고 문제가 있는지 확인할 방법이 없었다는 점이 계속 마음에 걸렸습니다.
저는 이를 체스판처럼 상상합니다. 체스판에는 준무한한 수의 가능한 말 배치가 있습니다. 하지만 우리는 그 체스판이 Gleam 프로그램으로 채워지기를 원하며, 테스트되지 않은 예외 상황을 찾아낼 수 있는 프로그램들의 무한히 큰 데이터베이스를 원합니다.
이와 비슷한 일을 하려던 첫 번째 시도는 사실 LLM에 프롬프트를 주는 것이었습니다. 과거 Gleam 이슈를 대량으로 읽고 “열심히 생각해서” 더 많은 예외 상황을 찾으라고 지시했습니다. 온갖 종류의 비트 배열 조합, 중첩 익명 함수, 중첩 use 패턴을 내놓았습니다. 예상할 수 있듯, 이 접근법은 많은 결과를 내지 못했습니다.
토큰에 20달러를 쓴 뒤, 정확히 하나의 이슈를 찾았고 바로 보고되어 수정되었습니다: https://github.com/gleam-lang/gleam/issues/5613. 하나는 분명히 0보다 많습니다. 하지만 “LLM 퍼징”에는 여러 문제가 있습니다. 비용이 많이 들고, 결정론적이지 않으며, 슬롯머신 레버를 당기는 것과 조금 비슷합니다.
하지만 솔직히 말해 작업량이 너무 많아 보인다는 이유로 추구하지 않았던 또 다른 아이디어가 있었습니다. 바로 구조 인식 퍼징입니다.
소프트웨어를 작성하는 일은 어렵고, 인간은 이를 아주 잘하지 못합니다. 이를 돕기 위해 버그 탐색을 부분적으로 자동화할 수 있는 다른 소프트웨어를 만들었습니다.
이러한 프로그램 중 하나가 퍼저입니다. 퍼저는 우리 프로그램에 입력할 무작위 입력을 생성합니다. 전제는 대규모로 보면 이러한 무작위 입력이 아직 생각하지 못한 예외 상황을 드러내는 방식으로 분포하게 된다는 것입니다.
퍼저는 완전히 무작위로 뒤섞인 바이트부터 고도로 구조화된 문법 인식 AST까지 다양합니다.
프로그램에 완전히 무작위인 바이트를 입력하는 방식은 보통 이미지, 파일, 네트워크 요청, 프로토콜 등을 다루는 사용 사례에 쓰입니다. 퍼징이 오픈 소스 소프트웨어의 실제 보안 결함과 버그를 찾아낸 사례는 많습니다. 예를 들어 Firefox에서 zzuf가 발견한 이슈는 이미지 파일의 몇몇 비트를 뒤집으면 브라우저가 충돌하는 것이었습니다: https://nvd.nist.gov/vuln/detail/CVE-2007-6715. 하지만 퍼저는 버퍼 오버플로를 통해 실제로 악용 가능한 보안 결함도 발견했습니다.
Google에는 중요한 수많은 오픈 소스 프로젝트를 지속적으로 퍼징하는 “OSS Fuzz” 프로그램이 있습니다: https://google.github.io/oss-fuzz/
우리의 경우에는 브라우저나 네트워크 프로토콜을 다루지 않습니다. 컴파일러를 다룹니다. 그리고 이는 구조 인식 퍼징의 가능성을 열어 줍니다. 즉, 무작위 바이트 스트림이 아니라 소스 코드나 AST 형태의 코드 스트림을 생성하는 것입니다.
Gleam에는 특히 흥미로운 퍼징 후보가 되게 하는 몇 가지 요소가 있습니다.
Gleam은 JavaScript와 Erlang이라는 두 타깃을 위한 코드를 생성합니다. 두 타깃에서 같은 프로그램의 출력을 비교하고 차이가 있으면 표시할 수 있습니다.
Gleam은 최소주의적인 문법을 갖고 있습니다. 적어도 널리 사용되는 대부분의 다른 프로그래밍 언어와 비교하면 그렇습니다. 상대적으로 적은 코드로 언어가 제공하는 거의 모든 개념을 포괄하는 유효한 프로그램을 생성할 수 있습니다.
정적 타입도 있습니다. 말할 필요도 없이, 프로그램이 실행 시간에 충돌하지 않도록 보장할 수 있게 해 주는 훌륭한 기능입니다. 그렇다고 타입 시스템에 버그가 없다는 뜻은 아닙니다. 과거에는 타입 추론과 관련된 이슈가 있었습니다. 하지만 나중에 살펴보겠지만, 언어의 각 측면에는 저마다의 테스트 접근법이 필요합니다.
함수형 특성과 모든 것이 표현식이라는 사실은 프로그램을 조합하고 구조화하기에 매우 편리합니다.
Rust도 있습니다. 이 점은 쉽게 간과할 수 있지만, Gleam 컴파일러 자체가 Rust로 작성되었다는 사실 덕분에 기존 퍼징 도구를 아주 쉽게 통합할 수 있습니다. .gleam 파일 하나를 실행하지 않고도 컴파일러의 일부를 테스트할 수 있습니다.
이제 퍼저의 더 기술적인 측면을 살펴보겠습니다. 하지만 많은 코드나 세부 사항까지 다루지는 않을 것입니다. 이에 관해 더 읽고 싶다면 Nick Fitzgerald의 이 글과 블로그를 꼭 확인해 보세요. 이 프로젝트의 주된 영감이 되었습니다: https://fitzgen.com/2020/08/24/writing-a-test-case-generator.html
우리 퍼저는 변이 기반이 아니라 생성 기반이 될 것입니다. 차이를 더 잘 이해하고 싶다면 이 글을 읽는 것을 권합니다: https://fitzgen.com/2026/06/01/structure-aware-fuzzing-experiment.html
이 글에서 저자는 적어도 wasm에 대해서는 변이 기반 접근법이 생성 기반 접근법보다 훨씬 더 많은 이슈를 찾았다는 결론에 이릅니다. 그러므로 앞으로 이 프로젝트를 위해 구현할 가치가 있을 것입니다!
주제를 더욱 깊이 있게 살펴보고 싶다면 이 자료를 확인하세요: https://www.fuzzingbook.org/.
Gleam 퍼저의 전체 코드는 제 Gleam 포크의 이 브랜치에서 찾을 수 있습니다: https://github.com/daniellionel01/gleam/tree/fuzzing
퍼저를 위한 중요한 설계 선택은 공개 컴파일러 API를 사용하는 것입니다. 컴파일러 API에 안정성 보장이 없을 수는 있지만, 이렇게 하면 Gleam의 미래 버전과 호환성을 유지하기 쉽습니다. 또한 구현 세부 사항을 만지작거리는 일을 피할 수 있는데, 이는 거짓 양성이나 거짓 음성을 만들지 않도록 보장하는 좋은 방법입니다.
파서가 출력을 포착하고 분류하는 몇 가지 예시는 다음과 같습니다:
$ cargo run -p fuzzing-core --example classify
"pub fn main() { 1 }" -> compiled (js: 39B, ts: 32B, erl: 238B)
"pub fn main() { let f = fn(x) { x + 1 }; f(41) }" -> parse error
"pub fn main() { 1 +. \"x\" }" -> analysis rejected (javascript)
"pub fn main() {" -> parse error
fuzz 크레이트와 일부 래퍼 코드를 사용하면, 더 많은 맥락을 담은 오류 메시지를 주는 대신 컴파일러를 충돌시킬 수 있는지 확인하기 위해 Gleam 컴파일러에 무작위로 생성된 입력을 매우 빠르게 대량으로 넣을 수 있습니다(아직 구조화되지는 않았습니다).
출력이 상당히 크므로 1초만 실행하겠습니다:
$ cargo +nightly fuzz run parse_only --fuzz-dir fuzzing-harness -- -max_total_time=1 -timeout=10
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 302379076
INFO: Loaded 1 modules (740945 inline 8-bit counters): 740945 [0x105eeac70, 0x105f9fac1),
INFO: Loaded 1 PC tables (740945 PCs): 740945 [0x105f9fac8,0x106aedfd8),
INFO: 2466 files found in fuzzing-harness/corpus/parse_only
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: seed corpus: files: 2466 min: 1b max: 4046b total: 425422b rss: 62Mb
#2467 INITED cov: 2434 ft: 8563 corp: 1249/171Kb exec/s: 0 rss: 108Mb
#2513 REDUCE cov: 2434 ft: 8563 corp: 1249/171Kb lim: 3764 exec/s: 0 rss: 108Mb L: 48/3753 MS: 1 EraseBytes-
#2645 REDUCE cov: 2434 ft: 8563 corp: 1249/171Kb lim: 3764 exec/s: 0 rss: 108Mb L: 8/3753 MS: 2 ChangeBit-EraseBytes-
#2656 REDUCE cov: 2434 ft: 8563 corp: 1249/171Kb lim: 3764 exec/s: 0 rss: 109Mb L: 2/3753 MS: 1 EraseBytes-
#2937 REDUCE cov: 2434 ft: 8563 corp: 1249/171Kb lim: 3764 exec/s: 0 rss: 109Mb L: 314/3753 MS: 1 EraseBytes-
#3183 NEW cov: 2434 ft: 8578 corp: 1250/172Kb lim: 3764 exec/s: 0 rss: 110Mb L: 1054/3753 MS: 1 CopyPart-
#3591 REDUCE cov: 2434 ft: 8578 corp: 1250/172Kb lim: 3764 exec/s: 0 rss: 111Mb L: 99/3753 MS: 3 ShuffleBytes-CrossOver-EraseBytes-
#3934 NEW cov: 2434 ft: 8585 corp: 1251/173Kb lim: 3764 exec/s: 0 rss: 112Mb L: 399/3753 MS: 3 CMP-CopyPart-CopyPart- DE: "\010\000\000\000\000\000\000\000"-
# ...
NEW_FUNC[1/7]: 0x0001031cfb88 in _RINvNtCs3kGMwX4aip8_4core3ptr9drop_glueINtNtCshX1O598ANu2_5alloc3vec3VecINtNtNtCs846PmCUGaYz_10gleam_core3ast8constant8ConstantuEEEB1f_+0x0 (parse_only:arm64+0x1002abb88)
NEW_FUNC[2/7]: 0x00010323bff4 in _RINvNtCs3kGMwX4aip8_4core3ptr9drop_glueINtNtNtCs846PmCUGaYz_10gleam_core3ast8constant8ConstantuEEBI_+0x0 (parse_only:arm64+0x100317ff4)
#16785 NEW cov: 2483 ft: 8694 corp: 1262/177Kb lim: 3786 exec/s: 16785 rss: 144Mb L: 85/3753 MS: 1 CrossOver-
#18061 REDUCE cov: 2483 ft: 8694 corp: 1262/177Kb lim: 3797 exec/s: 18061 rss: 149Mb L: 403/3753 MS: 1 EraseBytes-
NEW_FUNC[1/4]: 0x00010312c3b0 in _RINvMs_NtCs846PmCUGaYz_10gleam_core5parseINtB5_6ParserINtNtB5_5lexer5LexerINtBT_14NewlineHandlerINtNtNtNtCs3kGMwX4aip8_4core4iter8adapters3map3MapNtNtNtB1F_3str4iter11CharIndicesNCNvBT_14make_tokenizer0EEEE23parse_bit_array_segmentNtNtNtB7_3ast7untyped11UntypedExprNCNCNvB2_21parse_expression_units6_00NvB2_17expect_expressionNvB5_24bit_array_expression_intEB7_+0x0 (parse_only:arm64+0x1002083b0)
NEW_FUNC[2/4]: 0x00010318893c in _RINvNtCs3kGMwX4aip8_4core3ptr9drop_glueINtNtCs846PmCUGaYz_10gleam_core3ast15BitArraySegmentNtNtBE_7untyped11UntypedExpruEEBG_+0x0 (parse_only:arm64+0x10026493c)
# ...
###### Recommended dictionary. ######
"\010\000\000\000\000\000\000\000" # Uses: 879
"\201\000" # Uses: 941
###### End of recommended dictionary. ######
Done 24887 runs in 2 second(s)
좋습니다. 생성된 일부 산출물을 보면 어떤 종류의 입력이 만들어지는지 알 수 있습니다:
fn
ar(n,n,n,///A#
o
ఌఌ「彸䕅䕅ⅅ䕅+�``">\u{000000000000000000.%\f0
fn
ar(n
ar:rn
a(
이 방식의 좋은 점은 gleam 바이너리를 전혀 실행하지 않고도 모든 것을 테스트할 수 있다는 것입니다. Rust의 컴파일러 파이프라인과 함께 메모리 내에서 실행됩니다.
그리고 놀랍게도! 이 퍼저를 꽤 오랫동안 실행했더니 nightly에서 회귀를 실제로 발견했습니다. 이 글을 작성할 당시 Gleam의 최신 버전이었던 v1.18.1에서는 발생하지 않았던 문제입니다:
$ cargo +nightly fuzz run --fuzz-dir fuzzing-harness parse_only fuzzing-harness/artifacts/parse_only/crash-8b14db5e4bf152924501e0818787026e9f5ea229
=fuzzing-harness/artifacts/parse_only/ fuzzing-harness/artifacts/parse_only/crash-8b14db5e4bf152924501e0818787026e9f5ea229`
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 3949856390
INFO: Loaded 1 modules (750122 inline 8-bit counters): 750122 0x107773860, 0x10782aa8a),
INFO: Loaded 1 PC tables (750122 PCs): 750122 [0x10782aa90,0x10839cd30),
fuzzing-harness/target/aarch64-apple-darwin/release/parse_only: Running 1 inputs 1 time(s) each.
Running: fuzzing-harness/artifacts/parse_only/crash-8b14db5e4bf152924501e0818787026e9f5ea229
thread '<unnamed>' (23346909) panicked at /gleam/compiler-core/src/parse.rs:5226:52:
Token could not be converted to binop.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
==41428== ERROR: libFuzzer: deadly signal
#0 0x000109a27654 in __sanitizer_print_stack_trace+0x28 (librustc-nightly_rt.asan.dylib:arm64+0x87654)
#1 0x000106bcdf0c in fuzzer::PrintStackTrace()+0x30 (parse_only:arm64+0x1024a9f0c)
#2 0x000106bc1f48 in fuzzer::Fuzzer::CrashCallback()+0x54 (parse_only:arm64+0x10249df48)
#3 0x000181edb740 in _sigtramp+0x34 (libsystem_platform.dylib:arm64e+0x3740)
#4 0x000181ed18d4 in pthread_kill+0x124 (libsystem_pthread.dylib:arm64e+0x68d4)
# ... shortened ...
NOTE: libFuzzer has rudimentary signal handlers.
Combine libFuzzer with AddressSanitizer or similar for better crash reports.
SUMMARY: libFuzzer: deadly signal
────────────────────────────────────────────────────────────────────────────────
Error: Fuzz target exited with exit status: 77
핵심 입력은 const 표현식 안의 파이프라인 |>였습니다. 예를 들면 const b = 1 |> 2와 같습니다.
이슈를 찾으면 해당 이슈에 git bisect도 사용합니다. 이를 통해 nightly의 회귀인지, 최신 버전의 Gleam에도 현재 존재하는 이슈인지 구분할 수 있습니다.
물론 이상적으로는 이를 1초만이 아니라 여러 시간 동안 실행합니다.
공정하게 말하자면, 이 단계에서 발견하는 버그도 잡아내면 좋겠지만, 코드 생성 버그를 찾을 가능성은 낮습니다.
그리고 이 글을 게시하는 시점에는 타입 안전 프로그램에 초점을 맞추기 위해 브랜치에서 libFuzzer 패키지를 제거했습니다. 이 프로젝트의 후속 단계에서는 더 표적화되고 효율적인 컴파일러 충돌 테스트를 계획하고 있습니다(즉 [tree-splicer).
타입 안전 Gleam 프로그램을 생성하기 위해 “스미스”를 만들 것입니다. Gleam AST의 단순화된 자체 버전을 만들고, 이를 대상으로 확률적인 방식으로 프로그램을 생성합니다. 그러면 스미스가 “Int로 해석되는 표현식이 필요하다”고 결정했을 때, 3 같은 리터럴 값이나 익명 함수 fn() { 3 }() 또는 변수 등을 제공할 수 있습니다. 이것이 결국 새로운 예외 상황을 발견하기 위해 프로그램 간에 많은 다양성을 안정적으로 만드는 방법입니다.
이런 프로그램 하나는 다음과 같은 모습입니다:
pub const k_seed: Bool = False
pub const k_e: Int = 5
pub const k_golden: String = "data"
pub type V0 {
Number(value: String, inner: List(Int))
}
fn walk(xs: List(Int), acc: Int) -> Int {
case xs {
[] -> acc
[x, ..rest] -> walk(rest, acc + x)
}
}
fn f0(arguments: #(Float, String), l: Int, item: #(Float, Bool)) -> List(Int) {
[]
}
fn f1(class: Int, acc: String) -> Int {
100
}
fn f2(pair: Int) -> Float {
case "" <> "abc" {
"data" <> rest as whole -> fn(v1) { {
let whole = []
0.0
} }(0.0)
inner | "ab" <> inner -> {
let self_ = 3.14
fn(v2, v3) { self_ }("x", 4)
}
"b" <> b -> case fn(v4) { 42 }(2.0) {
9 -> {
1.0
} *. {
2.0
}
item -> 3.14
}
}
}
pub fn main() {
let v = walk([5], k_e) - 100
let z = f2(v)
echo [2]
echo {
case "res" <> "data", f0(#(1.0, "data"), v, #(0.5, False)) {
_, [] -> fn(v5, v6) { 10.0 }(False, "x")
"b" <> _, [] -> z +. {
2.0
}
k_seed, [2, h, ..] as whole -> 10.0
_, _ -> f2(5)
}
} /. {
0.5
}
echo {
fn(v7) { 100.0 }(False)
} *. {
case "b", 2 {
"ab", 1 -> {
let rest = k_seed
z
}
"x", 5 -> 2.0
_, v8 -> z +. {
0.25
}
}
}
}
완전히 난삽한 코드입니다. 그리고 그게 핵심입니다! 무작위로 선택한 유효한 표현식을 조합한 프로그램을 생성하여, 한쪽 또는 양쪽 타깃에서 잘못된 동작을 유발하는 조합을 찾아내고자 합니다.
하지만 Gleam 프로그램이 성공적으로 컴파일되더라도 잘못된 동작을 만드는지 어떻게 알 수 있을까요? 여기서 두 컴파일 타깃이 등장합니다. 예를 들어 case 표현식을 포함하는 프로그램에 버그가 있다면, Erlang에서 논리가 올바르게 구현되었을 경우 이를 찾아낼 수 있습니다. 각 분기에 다른 값을 제공하면 JavaScript 타깃에서 다른 출력을 얻게 되기 때문입니다.
이는 100% 완벽하지는 않지만, 시작하기에는 탄탄한 접근법입니다. 두 타깃 모두에서 발생하는 컴파일러 버그는 여전히 있을 수 있으므로, 이 접근법에서 거짓 양성을 얻을 수 있습니다. 언제나 그렇듯이 은탄환은 없습니다.
Gleam 스미스를 살펴보기 전에, 약간 옆길로 새야 합니다.
echo 문제문제는 이렇습니다. JavaScript와 Erlang은 실행 시간에 값을 표현하는 방식에 대해 서로 다른 생각을 합니다. 예를 들어 JavaScript에는 전용 정수 및 부동 소수점 타입이 없습니다. Number가 있습니다. 일부 값은 문자열로 변환하고 Gleam의 내장 echo 키워드를 사용할 때 다르게 표현됩니다.
다음 프로그램을 보세요:
pub type Wibble {
Wibble(wobble: Int)
}
pub fn main() {
echo "hello"
echo <<1, 2, 3>>
echo <<"a":utf8, "b":utf8, "c":utf8>>
echo [1, 2, 3]
echo Wibble(3)
echo 1.2
echo 1.0
}
다음은 Erlang과 JavaScript에서 이 프로그램이 내는 출력입니다:
$ gleam run --target erlang
src/app.gleam:6
"hello"
src/app.gleam:7
"\u{0001}\u{0002}\u{0003}"
src/app.gleam:8
"abc"
src/app.gleam:9
[1, 2, 3]
src/app.gleam:10
Wibble(3)
src/app.gleam:11
1.2
src/app.gleam:12
1.0
$ gleam run --target javascript
src/app.gleam:6
"hello"
src/app.gleam:7
<<1, 2, 3>>
src/app.gleam:8
<<97, 98, 99>>
src/app.gleam:9
[1, 2, 3]
src/app.gleam:10
Wibble(wobble: 3)
src/app.gleam:11
1.2
src/app.gleam:12
1
JavaScript에서는 1.0이 1로 출력되는 반면, Erlang에서는 비트 배열 <<1, 2, 3>>이 "\u{0001}\u{0002}\u{0003}"로 출력됩니다. 레코드 Wibble은 Erlang에서 어떤 레이블도 포함하지 않습니다. 퍼저에서 출력을 직접 비교하려 한다면 이상적이지 않습니다.
이 문제를 우회하기 위해 생각할 수 있는 선택지는 몇 가지가 있습니다:
echo 출력이 달라지는 것으로 알려진 값은 의도적으로 테스트하지 않습니다.echo 함수를 만들고 생성된 모듈에 주입합니다.Module AST를 통해 Gleam에서 어떤 값이 echo될지 알고 있으므로, Rust로 사용자 정의 “Gleam에서 이 출력을 파싱하기”를 만듭니다.이 버전에서는 세 번째 선택지를 택했습니다. 이는 제약이 매우 명확한 문제였으므로, Gleam 프로그램에서 나오는 모든 echo 출력을 파싱하고 이를 Rust 열거형으로 파싱하여 예측 가능하게 출력을 비교하는 수많은 테스트 사례와 შესაბამის 코드를 생성했습니다. 분명 완벽과는 거리가 멀지만, 현재로서는 잘 작동하는 듯합니다.
좋습니다. 이제 모든 준비가 되었으니 퍼저를 가동하고 프로그램 10만 개를 생성하기만 하면 되는 것 아닐까요? 음, 여기서 또 다른 옆길로 새야 합니다.
퍼저를 실행하면 비슷한 형태의 프로그램을 많이 생성하게 됩니다. 따라서 버그를 하나 발견하면, 그 버그를 재현하는 프로그램을 계속 생성하게 됩니다.
여기에도 대응하는 몇 가지 방법이 있습니다:
string.contains 구문을 찾을 수 있습니다.이 글을 작성하는 시점에는 세 번째 접근법을 택했습니다. 패치 접근법도 시도해 보았지만, 이는 PR이나 수정 사항이 확실히 올바르고 새 버그를 도입하지 않을 것이라고 가정합니다. 공식 저장소의 정확한 Gleam 상태에 의존하는 편이 더 낫습니다.
예를 들어 JavaScript에서 코드 생성 이슈 2개를 발견했으므로, 현재 퍼저는 공식 저장소에 보고된 이슈와 비슷한 시그니처를 생성하는 모든 이슈를 완전히 무시합니다. 이는 비슷한 오류 시그니처를 가진 다른 코드 생성 이슈를 건너뛸 가능성이 있다는 뜻입니다. 프로그램 AST에 이러한 이슈를 일으키는 정확한 표현식이 포함되어 있는지에 관한 더 많은 휴리스틱과 분석을 도입할 수 있습니다. 더 큰 규모의 접근법이라면 이는 분명 좋은 생각일 것입니다. 현재 저는 퍼저를 프로그램 100개 단위 배치로 실행하고, 건너뛰거나 “새 버그”로 표시하는 발견 사항을 수동으로 확인하기 때문입니다. 프로그램 100개 중에서는 보통 몇 개에 불과하므로, 한 사람이 관리하기에도 여전히 가능합니다.
// https://github.com/gleam-lang/gleam/issues/6182
fn is_gleam_issue_6182(raw: &str) -> bool {
raw.contains("SyntaxError: Unexpected token '&&'")
|| raw.contains("SyntaxError: Unexpected token ')'")
}
// https://github.com/gleam-lang/gleam/issues/6212
fn is_gleam_issue_6212(raw: &str) -> bool {
raw.contains("TypeError:") && raw.contains("is not a function")
}
저장소에서 직접 이것을 시험해 보고 싶다면:
$ cargo run -p fuzzing-cli -- run 948
=== fuzz ===
gleam: /daniellionel01/gleam/target/release/gleam
seed: 948
erlang exit: 0
nodejs exit: 0
match: OK
--- values ---
[0] == erl=Bool(false) js=Bool(false)
[1] == erl=String("bcx") js=String("bcx")
[2] == erl=Bool(false) js=Bool(false)
# A mismatch in the second `echo`'d value!
$ cargo run -p fuzzing-cli -- run 947
=== fuzz ===
gleam: /daniellionel01/gleam/target/release/gleam
seed: 947
erlang exit: 0
nodejs exit: 0
match: NO
--- values ---
[0] == erl=Int(2) js=Int(2)
[1] != erl=Int(42) js=Int(103)
[2] == erl=Bool(true) js=Bool(true)
실행 중인 프로그램을 stdout으로 출력할 수도 있습니다:
$ cargo run -p fuzzing-cli -- print 947
pub const golden_value: String = "constructor"
pub const limit_value: Float = 2.0
pub const seed_value: Int = 42
fn yield(constructor: Float, prototype: Int, default: Float) -> List(Int) {
[7]
}
# ... lots more code ...
그리고 다음과 같이 프로그램 배치를 퍼징할 수 있습니다:
# seed 947 is a known bug, but there is no good heuristic to detect it right now.
$ cargo run -p fuzzing-cli -- batch 900 100
[fuzzing-cli] gleam: /daniellionel01/gleam/target/release/gleam
[fuzzing-cli] seeds 900..999
[fuzzing-cli] 10/100 run, 0 mismatches, 0 skipped
[fuzzing-cli] 20/100 run, 0 mismatches, 0 skipped
[fuzzing-cli] 30/100 run, 0 mismatches, 0 skipped
[fuzzing-cli] 40/100 run, 0 mismatches, 0 skipped
[fuzzing-cli] DIVERGENCE seed 947 -> erlang:0 nodejs:0
[fuzzing-cli] 50/100 run, 1 mismatches, 0 skipped
[fuzzing-cli] 60/100 run, 1 mismatches, 0 skipped
[fuzzing-cli] 70/100 run, 1 mismatches, 0 skipped
[fuzzing-cli] 80/100 run, 1 mismatches, 0 skipped
[fuzzing-cli] 90/100 run, 1 mismatches, 0 skipped
[fuzzing-cli] 100/100 run, 1 mismatches, 0 skipped
[fuzzing-cli] done: 100 programs, 1 mismatch(es), 0 skipped
이것으로 끝입니다! 저는 한 번에 100개씩 배치를 살펴보고, Erlang과 JavaScript 출력의 충돌이나 차이를 확인한 뒤, 그것이 알려진 프로그램 형태인지 아니면 새로운 것인지 살펴봅니다.
지금까지 퍼저는 이미 9개의 이슈를 찾아냈습니다! 그중 하나는 종료되었고 Erlang/OTP에 업스트림으로 보고되었습니다! 정말 멋진 발견입니다: https://github.com/erlang/otp/issues/11494
Record라는 타입이 Erlang에서 경고를 발생시킴<<_:utf8>>에 대한 매칭이 JavaScript에서 잘못된 분기를 선택함<<"":utf8>>에 대한 매칭이 잘못된 JavaScript를 생성함echo를 사용하면 Erlang이 예외를 던짐const에서 |>를 사용할 때 nightly의 컴파일러 충돌wibble로 호출된 함수가 JavaScript에서 지역 변수를 가림let이 바깥 변수를 가릴 때 JS 코드 생성이 잘못된 변수를 사용함재미있는 점은 퍼저의 현재 상태로는 Gleam에서 가능한 모든 표현식을 아직 포괄하지도 못한다는 것입니다. 이는 제한된 Gleam 프로그램 부분집합을 이용한 퍼징 접근법입니다. Gleam의 가능한 모든 표현식과 문장으로 이를 확장하고 프로그램 정확성을 테스트하는 더 많은 방법을 도입하면, 이 도구를 더욱 견고하게 만드는 Gleam 커뮤니티에 엄청나게 유용한 도구가 될 것입니다.
또한 JavaScript로 컴파일하는 esbuild, Babel, Elm 같은 더 큰 프로젝트도 수년간 존재해 왔음에도 JavaScript 코드 생성과 관련된 이슈가 여전히 발생하고 있다는 점은 주목할 가치가 있습니다(evanw/esbuild#4520, evanw/esbuild#4517, evanw/esbuild#4495, babel/babel#16009, babel/babel#15111, babel/babel#15145, elm/compiler#2360, elm/compiler#1223, elm/compiler#1636). 코드 생성, 특히 JavaScript를 위한 코드 생성은 정말 정말 어려운 문제입니다. Gleam이 JavaScript와 이보다 더 다를 수 없는 Erlang에도 컴파일하면서 두 타깃을 놀라울 정도로 잘 다루고 있다는 점을 고려하면 더욱 그렇습니다!
이 프로젝트는 아직 초기 단계이며, 앞으로 작업할 일이 많습니다:
gleam_stdlib가 그렇습니다. 이전에도 두 타깃 간 동작 불일치가 있었습니다.변경 사항의 통합과 테스트에 대해 불안해하지 않고 무언가를 작업할 수 있는 코드베이스에서 작업할 때, 저는 코드를 작성하는 일이 정말 즐겁습니다.
그리고 테스트 스위트의 가장 좋은 점은 보통 누적적이라는 것입니다. 모든 새로운 예외 상황과 버그 수정은 앞으로 더 큰 안전성을 제공할 수 있습니다.
저는 Gleam의 미래가 무척 기대되며, 여기에 기여하는 일은 매우 보람찼습니다.
아직 참여하지 않았다면 커뮤니티, 특히 Discord에 참여하세요!
건승을 빕니다!