fix(sandbox): compile rust as edition 2021

Merged
HA
8d ago
+252-24

Summary

Rust was already registered, but rustc compiled every submission as edition 2015 because no --edition was passed. Rust now compiles as edition 2021 by default, and a caller's compiler_flags: ["--edition=2018"] replaces that default instead of being added as a second --edition.

Motivation

Closes #117

#117 asks for Rust (edition 2021) on POST /api/v1/execute. The language entry, Docker package and API enum were already there, but without --edition rustc uses 2015, where TryFrom, TryInto and FromIterator are not in the prelude. Ordinary solutions such as let x: u8 = n.try_into()? failed with error[E0599]: no method named 'try_into' found, which a user can't explain because the same code compiles on any contest site.

Implementation notes

  • LangConfig.CompileDefaults (new, internal/sandbox/language.go): flags appended after CompileArgs unless a caller flag has the same name (the text before =). Rust sets CompileDefaults: ["--edition=2021"]; every other language leaves it empty, so its compile command is unchanged.
  • Why not just append --edition=2021 to CompileArgs: AllowedCompilerFlags already lets callers send --edition=2018 / --edition=2021, and rustc rejects a repeated option (error: Option 'edition' given more than once). The default has to be replaceable, not stacked.
  • -O stays in CompileArgs: rustc accepts -O twice (verified with rustc 1.85.1), so a caller -O was never a problem.
  • The rule lives in the language table, not the worker, so the worker stays language-agnostic.
  • Compile cache: keys are language + source + files + flags, held in memory, so nothing compiled under the old args survives a deploy.
  • Limits unchanged: in the real sandbox, a single-file program using generics and std collections compiles in 1.5 s / 76 MB, well inside the 20 s / 512 MB compile budget.

Verified against the real sandbox

I built docker/go-judge and sent commands made by the engine's own BuildCompileCmdWithOptions / BuildRunCmd (a throwaway program, not committed):

CaseCompile args (after rustc)Result
try_into, no flags-O -o /w/a.out /w/main.rs --edition=2021Accepted, 0.53 s / 57 MB, prints 42
compiler_flags: ["--edition=2018"]… --edition=2018 (one edition)E0599 as expected for 2018, so the caller's edition is the one used
compiler_flags: ["-O"]… --edition=2021 -OAccepted, prints 255
generics + collections… --edition=2021Accepted, 1.49 s / 76 MB
type error… --edition=2021Nonzero exit, rustc's error[E0308] kept in stderr

Before this change, the first case fails to compile with E0599.

Questions for the reviewer

Q1 — What existing tests cover the code you changed?
A: TestValidateCompilerFlags (which flags are allowed for rust), TestProbeLanguages and TestLanguageAvailability (rust's binaries come from CompileArgs[0] / RunArgs, which are unchanged), the TestCompileCache* tests (compile sharing and key, which this doesn't change), and the TestEffective* / TestBuildRunCmd* tests in internal/sandbox. All pass unchanged.

Q2 — What new tests did you add, and what cases do they cover?
A: internal/sandbox/language_test.go: TestBuildCompileCmdRustDefaults (no flags → --edition=2021; caller --edition=2018 replaces it; caller --edition=2021 isn't repeated; caller -O keeps the default edition), TestBuildCompileCmdWithoutDefaults (C++ compile args unchanged), and TestBuildCompileCmdNeverRepeatsAllowedFlag (for every language with defaults, no allowed caller flag can repeat one). internal/worker/rust_test.go uses a fake go-judge (httptest, no real sandbox runs): a Rust job compiles main.rs with --edition=2021 and runs the cached binary for every test; a caller edition replaces the default; a compile failure comes back with rustc's diagnostics.

Q3 — Does this change affect any authenticated or unauthenticated API paths? If yes, how?
A: No new paths, fields or validation. POST /api/v1/execute with language: "rust" now compiles as edition 2021; the request schema and the compiler_flags allowlist are unchanged, and it behaves the same for authenticated and unauthenticated callers.

Q4 — Did you run golangci-lint run locally and get 0 issues?
A: Yes. golangci-lint v2.15.0 reports 0 issues. (run in the golangci/golangci-lint:v2.15.0 image on an LF checkout of the commit).

Q5 — Is this the only open PR you have against development?
A: Yes.

Checklist

  • go test -race -count=1 -timeout=120s ./... passes locally (in a golang:1.27.2 container; internal/db needs testcontainers and runs in CI)
  • golangci-lint run --timeout=5m reports 0 issues
  • gofmt -s -l . reports no unformatted files
  • All five questions above are answered (not left as …)
  • No secrets or credentials in code / config
  • New dependencies reviewed and justified (none added; go mod tidy is clean)
  • Related issue linked in "Closes #" above

Summary by cubic

Fixes Rust submissions failing to compile as edition 2021 instead of rustc's default 2015, so solutions using try_into, TryFrom, or FromIterator no longer fail with error[E0599]. Closes #117.

  • Compile defaults were added to LangConfig: defaults apply unless a caller flag with the same name overrides them.
  • A caller's --edition=2018 replaces the default instead of stacking, which rustc rejects; other languages' compile args are unchanged.

Summary by CodeRabbit

  • New Features
    • Rust compilation now defaults to the 2021 edition. You can select another edition, such as 2018, through compiler flags; the selected edition replaces the default rather than being added alongside it.
  • Documentation
    • The supported-languages table now lists Rust compiler version 1.85 and its default optimization and edition flags. The changelog also describes the default edition and how to override it.