메뉴
HN
Hacker News 24일 전

AI가 러스트로 짠 PHP 엔진, 공식 테스트 17% 통과

IMP
8/10
핵심 요약

러스트(Rust) 문법을 전혀 모르는 개발자가 AI 코딩 에이전트를 지시만 해서 기존 PHP 소스 코드 없이 밑바닥부터 새롭게 구축된 PHP 인터프리터(Phargo)를 개발했습니다. 이 프로젝트는 AI가 자체적으로 결과를 판단하지 못하도록 30년간 축적된 약 2만 2천 개의 공식 PHP 테스트 스위트(suite)를 엄격한 기준으로 삼아 진행되었으며, 현재 전체 테스트의 17.4%를 통과하며 워드프레스 렌더링을 성공적으로 구현했습니다.

번역된 본문

원문 제목: 내가 AI로 구축한 러스트 기반 PHP 엔진이 PHP-src 테스트의 17%를 통과하고 워드프레스를 렌더링하다 출처: 해커뉴스(hackernews)

나는 러스트(Rust)를 모른다. 그럼에도 내 AI는 기어코 PHP를 러스트로 다시 작성하고 있다.

며칠 전 밤, 나는 내 터미널이 26KB짜리 워드프레스(WordPress) 프론트 페이지를 출력하는 것을 지켜보았다. <title>Phargo Test Site</title> 태그, 블록 라이브러리 CSS, SQLite 데이터베이스에서 불러온 "Hello world!", 그리고 마지막에 깔끔하게 닫힌 </html>이었다. 지극히 평범한 결과물이었지만, 단 하나 다른 점이 있었다. 이 모든 것을 제공한 PHP 엔진에는 실제 PHP 소스 코드가 단 한 줄도 포함되어 있지 않다는 사실이다. 이것은 러스트로 처음부터 새로 작성된 인터프리터(interpreter)다.

여기서 당신이 곰곰이 생각해 봤으면 하는 부분이 있다. 나는 러스트를 할 줄 모른다. 어휘 분석기(Lexer)를 작성해 본 적도 없다. 다른 탭에서 위키피디아를 찾아보지 않고서는 "트리 워킹 평가자(tree-walking evaluator)"가 무엇인지 설명할 수도 없다. 만약 파티장에서 누군가 나를 구석으로 몰아세우고 PHP의 가비지 컬렉터가 어떻게 작동하는지 묻는다면, 나는 전화가 온 척하며 도망칠 것이다.

이 엔진의 이름은 'Phargo'다. 이 프로젝트에서 내 역할은 대략 '방향을 잡아주는 것'이다. AI가 코드를 작성한다. 내가 목표를 가리키면, 중세 시대의 왕이 해도를 검토하듯 받아쓴 결과물을 읽는다(엄숙하게 고개를 끄덕일 뿐, 이해는 전혀 못한다). 그리고 다음과 같이 현대 소프트웨어 개발에서 가장 강력한 문장을 타이핑한다. "좋아 보이네, 계속해."

실험: '극단적인 정직'을 빌드 시스템으로 삼다

요즘 세상에는 모두가 각자의 반려식물처럼 AI로 만든 프로젝트를 하나씩 가지고 있다. 그리고 그런 프로젝트들은 모두 "작동한다!"라는 반박할 수 없는 주장을 내세운다. 대체 누구 기준으로 작동한다는 걸까? 코드를 작성한 AI 기준인가? 네 번째 다시 찍은 데모 영상 기준인가?

그래서 이번 실험 전체는 하나의 아이디어에 뿌리를 두고 있다. 번(Bun) 팀이 자바스크립트 런타임을 실제 테스트 스위트로 검증하는 것을 보고 얻은 아이디어인데, 바로 "AI가 자기 숙제를 스스로 채점하게 두지 마라"는 것이다.

PHP는 자체 테스트 스위트를 제공한다. PHP 내부 개발팀이 30년에 걸쳐 작성한 약 22,000개의 .phpt 파일이다. 내가 작성한 테스트도 아니고, AI가 작성한 테스트도 아니다. 이 테스트들은 DateTime의 일광 절약 시간제 계산법부터 var_dump()가 부동소수점(float)을 어떻게 출력하는지까지, 언어의 모든 괴상한 구석을 빠짐없이 담아내고 있다. 이 테스트 스위트가 바로 절대적인 기준(Oracle)이다.

점수판은 이 모든 테스트를 실행하며, 통과율은 매 실행 후 저장소에 자동으로 기록된다. 이 숫자는 아첨을 받지 않고, 협상에 응하지 않으며, 프롬프트를 잘 넣는다고 기분이 좋아지지 않는다. bug40261.phpt 테스트가 통과하면 통과하는 것이고, 아니면 실패하는 것이다.

현재 점수: 22,037개 중 3,844개 통과. 전체 상위 PHP 테스트 스위트의 17.4%다. 17%라고 비웃기 전에 알아둘 것이 있다. 현실적인 통과율 한계치는 약 40~45% 수준이다. 나머지 테스트는 명시적으로 범위에서 제외한 C 확장 모듈(GD, curl, SOAP, intl, MySQL 드라이버 등)을 테스트하기 때문이다. 실제 경기장 안에서를 기준으로 보면, 0에서 시작한 것을 감안하면 이 상승세는 매우 현실적이고 가파른 것이다.

인간으로서 내가 반복하는 루프는 부끄러울 정도로 단순하다.

  • AI가 전체 테스트 데이터에 대한 실패 히스토그램을 분석하여 실제로 고칠 수 있는 가장 큰 실패 그룹을 찾는다.
  • AI가 해당 기능을 구현한다.
  • 약 22,000개의 테스트 점수판을 실행한다.(팬이 돌아가는 소음 약 7분)
  • 숫자가 오르면: 커밋(commit)하고, 푸시(push)하고, 다시 반복한다.
  • 숫자가 내리면: 내가 할 수 있는 말은 또 하나뿐이다. "음, 성능이 후퇴했네(regressed). 다시 확인해 봐."

끝이다. 이게 내가 하는 일의 전부다. 나는 위임의 최고 경지에 도달했고, 조금도 미안하지 않다.

신탁(Oracle)은 매수될 수 없다. 하지만 테스트 도구는 내 얼굴에 대놓고 거짓말을 했다.

초반에 통과율은 어딘가 석연찮은 방식으로 정체되었다. 분명히 간단한 테스트 카테고리 전체가 예상 출력과 완전히 동일해 보이는 상태에서도 계속 실패로 처리되었다. 나는 '다른 점 찾기' 퍼즐의 두 똑같은 사진을 바라보듯, 아무것도 발견하지 못한 채 그 차이 화면만 뚫어지게 쳐다보았다.

그 차이는 눈에 보이지 않았기 때문에 보이지 않았던 것이다. 바로 캐리지 리턴(carriage return, carriage returns)이었다. 테스트 데이터가 CRLF 줄바꿈 방식을 사용하는 윈도우(Windows)에서 체크아웃되었고, 우리의 점수판은 결과를 바이트 단위로 일일이 비교하고 있었다. PHP의 기본 테스트 러너는 비교하기 전에 줄바꿈 방식을 통일(normalize)하는 반면, 우리 시스템은 그렇게 하지 않았던 것이다.

즉, 우리 테스트 도구는 수주 동안 말 그대로 말줄바꿈 문자만을 이유로 말줄바꿈이 여러 줄인 테스트를 조용히 실패시키고 있었던 것이다. 코드 한 줄을 수정해 줄바꿈을 표준화(normalize)하자, 수백 개의 테스트가 즉시 초록불(통과)로 바뀌었다. 이 교훈은 프로젝트에 아주 깊이 새겨졌다.

원문 보기
원문 보기 (영어)
&larr; essays I Don&rsquo;t Know Rust. My AI Is Rewriting PHP in It Anyway. A few nights ago I watched my terminal print out a 26 KB WordPress front page — <title>Phargo Test Site</title> , the block-library CSS, &ldquo;Hello world!&rdquo; pulled from a SQLite database, and a clean </html> at the bottom. Completely unremarkable output, except for one detail: the PHP engine that served it contains zero lines of PHP&rsquo;s actual source code . It&rsquo;s a from-scratch interpreter written in Rust. Here&rsquo;s the part I need you to sit with: I don&rsquo;t know Rust. I have never written a lexer. I could not explain to you what a &ldquo;tree-walking evaluator&rdquo; is without reading the Wikipedia article in another tab. If you cornered me at a party and asked how PHP&rsquo;s garbage collector works, I would fake a phone call. The engine is called Phargo , and my contribution to it is, roughly, aiming. An AI writes the code. I point it at a target, read what comes back like a medieval king reviewing naval charts — solemn nod, zero comprehension — and type the most powerful phrase in modern software development: &ldquo;looks good, continue.&rdquo; The Experiment: Radical Honesty as a Build System Everyone and their houseplant has an AI-built project right now, and every single one comes with the same unfalsifiable claim: &ldquo;it works!&rdquo; Works according to whom? The AI that wrote it? The demo that was recorded on the fourth take? So the whole experiment rests on one idea, borrowed from watching the Bun team drive their JavaScript runtime against real-world test suites: don&rsquo;t let the AI grade its own homework. PHP ships with its own test suite — about 22,000 .phpt files written by the PHP internals team over three decades. Tests I didn&rsquo;t write. Tests the AI didn&rsquo;t write. Tests that encode every cursed corner of the language, from DateTime daylight-saving math to what exactly var_dump() prints for a float. That suite is the oracle. The scoreboard runs all of it, and the pass rate is auto-generated into the repo after every run. The number cannot be flattered, negotiated with, or prompted into a better mood. Either bug40261.phpt passes or it doesn&rsquo;t. Current score: 3,844 of 22,037 — 17.4% of the entire upstream PHP test suite. And before you snort-laugh at 17%: the realistic ceiling is around 40–45%, because the rest of the suite tests C extensions (GD, curl, SOAP, intl, MySQL drivers…) that are explicitly out of scope. Within the actual playing field, the climb is very real — it started at zero. My loop as the human is almost embarrassingly thin: The AI runs a failure histogram over the whole corpus to find the biggest cluster of failing tests it can actually fix It implements the thing It runs the ~22,000-test scoreboard (about 7 minutes of fan noise) If the number went up: commit, push, repeat If the number went down: I get to say my other line, &ldquo;hmm, that regressed, look again&rdquo; That&rsquo;s it. That&rsquo;s the job. I&rsquo;ve achieved peak delegation and I&rsquo;m not even sorry. The Oracle Cannot Be Bribed. The Harness, However, Lied to My Face. Early on, the pass rate plateaued in a way that felt wrong. Whole categories of tests — obviously simple ones — kept failing with diffs that looked identical to the expected output. I stared at those diffs like a man staring at two identical photos in a spot-the-difference puzzle, finding nothing. The difference was invisible because it was literally invisible: carriage returns. The test corpus had been checked out on Windows with CRLF line endings, and our scoreboard compared output byte-for-byte. PHP&rsquo;s own test runner normalizes line endings before comparing. Ours didn&rsquo;t. Which means the harness was silently failing essentially every multi-line test in the corpus on line endings alone, and had been for weeks. One line of normalization code. Hundreds of tests flipped to green instantly. The lesson tattooed itself onto the project: measure your measurement. Your oracle is only as honest as the plumbing that connects you to it. We now normalize exactly the way run-tests.php does, and every suspicious plateau since has triggered the same question first — is the engine wrong, or is the scoreboard lying? PHP&rsquo;s Test Suite Is a Minefield With a Readme Here&rsquo;s something nobody tells you about running somebody else&rsquo;s 22,000-file test corpus: some of those files are bombs. Not malicious ones — accidental ones. Regression tests for ancient memory bugs that allocate absurd structures, generator tests that expand into infinity, tests that were only ever meant to run inside PHP&rsquo;s own carefully-fenced CI. I found this out the way all great discoveries are made: my development machine hard-restarted . Not &ldquo;the program crashed.&rdquo; Not &ldquo;the terminal froze.&rdquo; The entire computer went black and rebooted, because a generator test convinced our engine to eat every byte of RAM in the house like a rocket-powered shopping cart with no brakes. The aftermath turned the engine paranoid, and honestly it wears paranoia well: A capped global allocator — the engine physically cannot allocate more than 6 GiB, no matter how creative the test gets A step limit, so infinite loops die with an error instead of a space heater Caps on string sizes, array nodes, output length, generator expansion A breadcrumb file the scoreboard updates with the current test name, so when something hangs, we know exactly which file to glare at None of this is glamorous language-implementation work. All of it is the difference between &ldquo;research project&rdquo; and &ldquo;thing that can safely chew through 22,000 hostile files unattended while I make coffee.&rdquo; The Features That Were Quietly Lying My favorite genre of bug — and the corpus finds them relentlessly — is the feature that exists, parses, runs without error, and does absolutely nothing. The Potemkin builtin. Over the months the suite has exposed, among others: clone — parsed fine, evaluated to NULL . Engine-wide. Every DateTimeImmutable in every test had been silently broken forever, because immutable date math is made of clone unset($arr[$key]) — a total no-op. The key simply… stayed trim($str, $charlist) — ignored the charlist argument since the beginning of time and trimmed whitespace anyway $$variableVariables — didn&rsquo;t exist static function variables — didn&rsquo;t exist spl_autoload_register() — accepted your autoloader with a warm smile and never called it catch (\Throwable) — matched nothing, which is a very funny property for a catch-all to have Each of these would survive a demo. Each would survive a code review by someone (me) who doesn&rsquo;t read Rust. None of them survived the corpus. That&rsquo;s the entire thesis of the experiment in one bullet list: I can&rsquo;t audit the code, so the 22,000 tests audit it for me, with a thoroughness no human reviewer could sustain past lunch. And Then It Served a WordPress Page The north star was always WordPress — it&rsquo;s the final boss of PHP compatibility, a codebase old enough to contain sedimentary layers of every PHP idiom since 2003. Getting wp-load.php to even bootstrap burned through a chain of blockers that reads like a language-lawyer&rsquo;s fever dream: goto (yes, WordPress&rsquo;s HTML parser uses goto ), str_replace &rsquo;s by-reference $count parameter, \xNN escapes inside regex character classes, function_exists() being blind to half our builtins. The installer then corrupted its own database because preg_split was ignoring the PREG_SPLIT_DELIM_CAPTURE flag inside wpdb::prepare — a bug four layers below anything I could have diagnosed, found and fixed by the AI while I supervised with the confidence of a man watching open-heart surgery through frosted glass. And then one evening: wp_install() completes. Admin user created, options table populated, three posts in SQLite. The front page renders — real theme, real posts,