Skip to content

Commit 94683ca

Browse files
authored
feat(compiler): add compiler engine switch and differential harness (#987)
* feat(compiler): add compiler engine switch and engine-namespaced caches Select the compiler engine with SQLBUILD_COMPILER_ENGINE (python by default, or native) or the hidden --compiler-engine global option. All frontier stages run through one compile_frontier seam; the native engine currently falls back to the Python stages. Compile JSON and sqb debug report the engine, compile reuse keys include it, and the native engine keeps its compiler, Rules and lineage stores in -native-v1 directories so the Python stores stay untouched. * test(compiler): add compiler engine differential harness and corpora Compare exit codes, normalised stdout and stderr, the compiled tree, manifest, DAG and per-model query fingerprints between two engines over the e2e fixtures, website examples, seeded generated projects, a failure corpus and dense projects, with optional per-stage captures. * ci: run the compiler engine differential per pull request and nightly * fix(compiler): defer stage-capture annotations for Python 3.12 itertools.count is not subscriptable at runtime before Python 3.13, so the ClassVar annotation failed at import on 3.12. * test(compile): leave the engine label out of semantic compile fingerprints * test(cli): match the engine choice error across Python versions
1 parent 3d329df commit 94683ca

102 files changed

Lines changed: 5670 additions & 107 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Compiler Differential
2+
3+
on:
4+
schedule:
5+
- cron: "43 3 * * *"
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
11+
concurrency:
12+
group: compiler-differential-${{ github.ref }}
13+
cancel-in-progress: false
14+
15+
jobs:
16+
dense:
17+
name: Dense compiler differential (${{ matrix.models }} models)
18+
runs-on: ubuntu-24.04
19+
timeout-minutes: 90
20+
strategy:
21+
fail-fast: false
22+
matrix:
23+
models: [3000, 10000]
24+
steps:
25+
- uses: actions/checkout@v4
26+
- uses: astral-sh/setup-uv@v5
27+
with:
28+
version: "0.12.3"
29+
enable-cache: true
30+
cache-dependency-glob: |
31+
uv.lock
32+
Cargo.lock
33+
- uses: dtolnay/rust-toolchain@master
34+
with:
35+
toolchain: stable
36+
- name: Install Python
37+
run: uv python install 3.12
38+
- name: Install dependencies
39+
run: uv sync --locked --all-groups --all-extras
40+
- name: Compare dense projects with and without custom rules
41+
env:
42+
MODELS: ${{ matrix.models }}
43+
run: make compiler-differential-dense COMPILER_DIFFERENTIAL_DENSE_MODELS="${MODELS}"
44+

‎.github/workflows/verify.yml‎

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ jobs:
1717
runs-on: ubuntu-24.04
1818
outputs:
1919
checks: ${{ steps.scope.outputs.checks }}
20+
compiler_differential: ${{ steps.scope.outputs.compiler_differential }}
2021
duckdb: ${{ steps.scope.outputs.duckdb }}
2122
postgres: ${{ steps.scope.outputs.postgres }}
2223
performance: ${{ steps.scope.outputs.performance }}
@@ -43,11 +44,12 @@ jobs:
4344
echo "rules_performance=$5"
4445
echo "checks=$6"
4546
echo "wheels=$7"
47+
echo "compiler_differential=$8"
4648
} >> "${GITHUB_OUTPUT}"
4749
}
4850
4951
if [[ "${EVENT_NAME}" != "pull_request" ]]; then
50-
write_scope true true true true true true true
52+
write_scope true true true true true true true true
5153
exit 0
5254
fi
5355
@@ -57,6 +59,7 @@ jobs:
5759
format_performance=false
5860
rules_performance=false
5961
wheels=false
62+
compiler_differential=false
6063
broad=false
6164
changed=false
6265
# A change that only touches the website, root-level Markdown, the README demo GIFs or the
@@ -77,6 +80,11 @@ jobs:
7780
wheels=true
7881
;;
7982
esac
83+
case "${path}" in
84+
scripts/compiler_differential/*|scripts/run_compiler_differential.py|tests/*/scripts/compiler_differential/*|.github/workflows/verify.yml|Makefile)
85+
compiler_differential=true
86+
;;
87+
esac
8088
case "${path}" in
8189
crates/*|Cargo.toml|Cargo.lock|pyproject.toml|uv.lock)
8290
broad=true
@@ -132,7 +140,7 @@ jobs:
132140
done < <(git diff --name-only -z "${BASE_SHA}...${HEAD_SHA}")
133141
134142
if [[ "${changed}" != "true" ]]; then
135-
write_scope true true true true true true true
143+
write_scope true true true true true true true true
136144
exit 0
137145
fi
138146
@@ -145,6 +153,7 @@ jobs:
145153
duckdb=true
146154
postgres=true
147155
format_performance=true
156+
compiler_differential=true
148157
fi
149158
write_scope \
150159
"${duckdb}" \
@@ -153,7 +162,8 @@ jobs:
153162
"${format_performance}" \
154163
"${rules_performance}" \
155164
"${checks}" \
156-
"${wheels}"
165+
"${wheels}" \
166+
"${compiler_differential}"
157167
158168
lockfile:
159169
name: Lockfile consistency
@@ -258,6 +268,31 @@ jobs:
258268
- name: Run DuckDB E2E shard
259269
run: make ${{ matrix.target }}
260270

271+
compiler_differential:
272+
name: Compiler engine differential
273+
needs: [changes, lockfile]
274+
if: needs.changes.outputs.compiler_differential == 'true'
275+
runs-on: ubuntu-24.04
276+
timeout-minutes: 30
277+
steps:
278+
- uses: actions/checkout@v4
279+
- uses: astral-sh/setup-uv@v5
280+
with:
281+
version: "0.12.3"
282+
enable-cache: true
283+
cache-dependency-glob: |
284+
uv.lock
285+
Cargo.lock
286+
- uses: dtolnay/rust-toolchain@master
287+
with:
288+
toolchain: stable
289+
- name: Install Python
290+
run: uv python install 3.12
291+
- name: Install dependencies
292+
run: uv sync --locked --all-groups --all-extras
293+
- name: Compare fixtures, examples, generated seeds and the failure corpus
294+
run: make compiler-differential COMPILER_DIFFERENTIAL_ARGS=--stage-captures
295+
261296
format_performance:
262297
name: Formatter performance guards
263298
needs: [changes, lockfile]
@@ -675,6 +710,7 @@ jobs:
675710
lockfile,
676711
checks,
677712
e2e_shards,
713+
compiler_differential,
678714
format_performance,
679715
performance,
680716
cold_compile_performance,
@@ -694,6 +730,7 @@ jobs:
694730
LOCKFILE_RESULT: ${{ needs.lockfile.result }}
695731
CHECKS_RESULT: ${{ needs.checks.result }}
696732
E2E_SHARDS_RESULT: ${{ needs.e2e_shards.result }}
733+
COMPILER_DIFFERENTIAL_RESULT: ${{ needs.compiler_differential.result }}
697734
FORMAT_PERFORMANCE_RESULT: ${{ needs.format_performance.result }}
698735
PERFORMANCE_RESULT: ${{ needs.performance.result }}
699736
COLD_COMPILE_PERFORMANCE_RESULT: ${{ needs.cold_compile_performance.result }}
@@ -715,6 +752,7 @@ jobs:
715752
fi
716753
for result in \
717754
"${E2E_SHARDS_RESULT}" \
755+
"${COMPILER_DIFFERENTIAL_RESULT}" \
718756
"${FORMAT_PERFORMANCE_RESULT}" \
719757
"${PERFORMANCE_RESULT}" \
720758
"${COLD_COMPILE_PERFORMANCE_RESULT}" \

‎Makefile‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ SHELL := /bin/bash
66
test-e2e-duckdb-cli-data test-e2e-duckdb-cli test-e2e-duckdb-inspection \
77
test-e2e-duckdb-integrations test-e2e-performance \
88
test-e2e-cold-compile-performance test-e2e-cache-compile-performance \
9-
test-e2e-dense-compile-performance test-e2e-varied-cache-performance
9+
test-e2e-dense-compile-performance test-e2e-varied-cache-performance \
10+
compiler-differential compiler-differential-dense
1011

1112
format:
1213
uv run ruff format .
@@ -130,6 +131,7 @@ E2E_DUCKDB_CLI_DATA_PATHS := \
130131
E2E_DUCKDB_CLI_PATHS := \
131132
tests/e2e/scripts/cli_preview \
132133
tests/e2e/scripts/compile_performance_ratio \
134+
tests/e2e/scripts/compiler_differential \
133135
tests/e2e/scripts/release_performance \
134136
tests/e2e/src/sqlbuild/cli/commands/main/adapters \
135137
tests/e2e/src/sqlbuild/cli/commands/main/audit \
@@ -285,6 +287,21 @@ test-e2e-varied-cache-performance:
285287
-vv -rP --log-level=INFO --log-cli-level=INFO --color=yes
286288

287289

290+
COMPILER_DIFFERENTIAL_JOBS ?= 4
291+
COMPILER_DIFFERENTIAL_SEEDS ?= 12
292+
COMPILER_DIFFERENTIAL_DENSE_MODELS ?= 3000
293+
COMPILER_DIFFERENTIAL_ARGS ?=
294+
295+
compiler-differential:
296+
env PYTHONUNBUFFERED=1 uv run python -m scripts.run_compiler_differential \
297+
--jobs $(COMPILER_DIFFERENTIAL_JOBS) --seeds $(COMPILER_DIFFERENTIAL_SEEDS) \
298+
$(COMPILER_DIFFERENTIAL_ARGS)
299+
300+
compiler-differential-dense:
301+
env PYTHONUNBUFFERED=1 uv run python -m scripts.run_compiler_differential \
302+
--corpus dense --dense-models $(COMPILER_DIFFERENTIAL_DENSE_MODELS) \
303+
--jobs $(COMPILER_DIFFERENTIAL_JOBS) $(COMPILER_DIFFERENTIAL_ARGS)
304+
288305

289306
skills:
290307
uv run python -m scripts.generate_docs_skill

‎crates/sqlbuild-rules/src/engine/_helpers/cache.rs‎

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,32 @@ pub(crate) struct RuleCacheEntry {
1616
}
1717

1818
pub(crate) struct Cache {
19-
root: PathBuf,
19+
native_rules_path: PathBuf,
2020
}
2121

2222
impl Cache {
23-
pub(crate) fn open(project_dir: &Path) -> Result<Self, String> {
24-
Ok(Self {
25-
root: project_dir.join("target/rules-cache"),
26-
})
23+
pub(crate) fn open(project_dir: &Path, rules_cache_path: &str) -> Result<Self, String> {
24+
let native_rules_path = if rules_cache_path.is_empty() {
25+
project_dir.join("target/rules-cache/bulk/native.json")
26+
} else {
27+
PathBuf::from(rules_cache_path)
28+
};
29+
Ok(Self { native_rules_path })
2730
}
2831

2932
pub(crate) fn native_rules_bucket(
3033
&self,
3134
ruleset_identity: &str,
3235
) -> Result<RuleCacheBucket, String> {
33-
read_bucket(&self.root.join("bulk/native.json"), ruleset_identity)
36+
read_bucket(&self.native_rules_path, ruleset_identity)
3437
}
3538

3639
pub(crate) fn put_native_rules_bucket(
3740
&self,
3841
ruleset_identity: &str,
3942
bucket: &RuleCacheBucket,
4043
) -> Result<(), String> {
41-
write_bucket(
42-
&self.root.join("bulk/native.json"),
43-
ruleset_identity,
44-
bucket,
45-
)
44+
write_bucket(&self.native_rules_path, ruleset_identity, bucket)
4645
}
4746
}
4847

‎crates/sqlbuild-rules/src/engine/_helpers/evaluation.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ pub(crate) fn evaluate_request(request: EvaluateRequest) -> Result<String, Strin
320320
.transpose()?
321321
.unwrap_or_default();
322322
let cache = cache_enabled
323-
.then(|| Cache::open(Path::new(&request.project_dir)))
323+
.then(|| Cache::open(Path::new(&request.project_dir), &request.rules_cache_path))
324324
.transpose()?;
325325
let built_in_started = Instant::now();
326326
let mut raw_faults = request.initial_findings.clone();

‎crates/sqlbuild-rules/src/engine/tests/test_incremental_rules.rs‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,47 @@ fn given_removed_model_file_when_cache_is_rewritten_then_only_its_entry_is_dropp
330330
Ok(())
331331
}
332332

333+
#[test]
334+
fn given_configured_cache_path_when_evaluating_then_only_that_file_is_used() -> Result<(), String> {
335+
let test_cases = [test_types::ConfiguredCachePathTestCase {
336+
description: "an engine-owned cache file replaces the default bulk cache",
337+
cache_file: "target/rules-cache-native-v1/bulk/native.json",
338+
expected_default_cache_exists: false,
339+
expected_warm_hits: 3,
340+
}];
341+
for test_case in &test_cases {
342+
let project_dir = TempDir::new().map_err(|error| error.to_string())?;
343+
let mut rest = split_request(
344+
&project_dir,
345+
&json!({"select": ["SQBR"], "cache": {"enabled": true}}),
346+
);
347+
let cache_path = project_dir.path().join(test_case.cache_file);
348+
rest["rules_cache_path"] = json!(cache_path);
349+
let models = orders_models();
350+
351+
let cold = evaluate_split(&rest, &models)?;
352+
let warm = evaluate_split(&rest, &models)?;
353+
354+
assert!(cache_path.is_file(), "{}", test_case.description);
355+
assert_eq!(
356+
project_dir
357+
.path()
358+
.join("target/rules-cache/bulk/native.json")
359+
.exists(),
360+
test_case.expected_default_cache_exists,
361+
"{}",
362+
test_case.description
363+
);
364+
assert_eq!(
365+
warm["cache_hits"], test_case.expected_warm_hits,
366+
"{}",
367+
test_case.description
368+
);
369+
assert_eq!(warm["faults"], cold["faults"], "{}", test_case.description);
370+
}
371+
Ok(())
372+
}
373+
333374
#[test]
334375
fn given_corrupt_model_cache_when_evaluating_then_every_model_reevaluates() -> Result<(), String> {
335376
let test_cases = [test_types::ModelCacheReuseTestCase {

‎crates/sqlbuild-rules/src/engine/tests/test_types.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,13 @@ pub(crate) struct ModelCacheReuseTestCase {
242242
pub(crate) expected_misses: u64,
243243
}
244244

245+
pub(crate) struct ConfiguredCachePathTestCase {
246+
pub(crate) description: &'static str,
247+
pub(crate) cache_file: &'static str,
248+
pub(crate) expected_default_cache_exists: bool,
249+
pub(crate) expected_warm_hits: u64,
250+
}
251+
245252
pub(crate) struct CachePruningTestCase {
246253
pub(crate) description: &'static str,
247254
pub(crate) expected_paths: &'static [&'static str],

‎crates/sqlbuild-rules/src/models.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,7 @@ pub struct EvaluateRequest {
729729
pub initial_findings: Vec<Fault>,
730730
pub defer_suppressions: bool,
731731
pub custom_rules: Vec<CustomRule>,
732+
pub rules_cache_path: String,
732733
}
733734

734735
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -765,6 +766,7 @@ impl Default for EvaluateRequest {
765766
initial_findings: vec![],
766767
defer_suppressions: false,
767768
custom_rules: vec![],
769+
rules_cache_path: String::new(),
768770
}
769771
}
770772
}

‎scripts/cold_compile_performance/main/semantic_compile_fingerprint.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def semantic_compile_fingerprint(*, payload: dict[str, object], compiled_dir: Pa
1414

1515
semantic_payload: dict[str, object] = dict(payload)
1616
semantic_payload.pop("compile_timings", None)
17+
semantic_payload.pop("compiler_engine", None)
1718
semantic_payload.pop("version", None)
1819
fingerprint: Any = hashlib.sha256(orjson.dumps(semantic_payload, option=orjson.OPT_SORT_KEYS))
1920
for artifact_path in sorted(path for path in compiled_dir.rglob("*") if path.is_file()):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Differential harness comparing the Python and native compiler engines."""

0 commit comments

Comments
 (0)