The standard-library conversion applies -tags purego
Reference index · Summary of this topic
The converted standard library reproduces Go built with -tags purego, not the default amd64/arm64 build. This is a fidelity decision, not a convenience one: Go implements hot cryptographic and hashing functions in hand-written .s assembly, with the Go source carrying only a bodyless declaration (e.g. crypto/sha256/sha256block_decl.go is //go:build (386 || amd64 || s390x || ppc64le || ppc64) && !purego and declares func block(dig *digest, p []byte) with no body). A transpiler works from Go source and cannot convert assembly, so those declarations become throwing stubs — they compile (they are part of the 302-package clean-compile milestone) but cannot run, which blocks Phase-4 validation. A managed C# runtime can never execute those .s files, so “Go built with -tags purego” is a claim go2cs can actually honor, whereas “the default amd64 build” is one it fails on every asm-backed function. The purego build tag selects the portable pure-Go variants instead (sha256block_generic.go is //go:build purego || !(386 || …) and has a real body), replacing ~42 stubs across ~13 packages with convertible code. Adopting the tag drops zero compiling packages (302/302 with the tag, 302/302 without) and unblocks running.
How the tag is applied — and made visible, not magic. -stdlib applies -tags purego by default; an explicit -tags on the same command overrides it verbatim (including -tags= to clear it and reproduce the asm-backed default build). -tests gets the SAME default (resolveBuildTags): a -tests run reconverts the package’s PRODUCTION sources and recompiles them into the test assembly, so it must select the exact same source files the committed corpus was built from — that tree is Go-under-purego. Without it, a package whose asm and pure-Go variants are gated !purego/purego (crypto/subtle’s xor_amd64.go and xor_generic.go, both declaring func xorBytes) has BOTH files converted and collides (CS0111 duplicate member), and the regenerated production .cs diverges from the committed purego emission. The default is scoped to -stdlib and -tests — the whole-library corpus and its test validation — because the purego claim is about what the corpus is, so it must hold for every such invocation (direct or via any script), not depend on remembering a flag. Scripts-only (having the deploy/convert scripts pass the tag) was rejected for that reason: the canonical go2cs -stdlib invocation is used throughout the repo and docs, and a script-gated tag would make a bare -stdlib produce a different corpus. Crucially the default does not touch -recurse end-user conversions or single-file/dir conversions — there the user’s own build tags govern, and a hidden default would be least welcome. Discoverability is threefold: the -stdlib and -tags --help text both state the default, and the stdlib converter prints the effective tags at the start of every run (Applying build tags: purego (default; pass -tags to override), or … none (-tags= cleared the purego default) when overridden). The tag threads into both the go/packages loader and the converter’s own BuildConstraintEvaluator (seeding only the loader would silently re-exclude the file the tag just selected).
purego is not the only spelling of this decision — math_big_pure_go is the same one. The tag set the corpus applies is purego, math_big_pure_go, because purego is a convention the crypto and hashing packages adopted, not a language rule, and math/big predates it. math/big gates its own portable fallbacks on math_big_pure_go: arith_decl.go is //go:build !math_big_pure_go and declares eight bodyless //go:linkname///go:noescape functions — addVV, subVV, addVW, subVW, shlVU, shrVU, mulAddVWW, addMulVVW — whose bodies are arith_$GOARCH.s, while arith_decl_pure.go is //go:build math_big_pure_go and forwards each to the _g pure-Go implementation that already sits in arith.go. With only purego seeded the converter selected the declaration file, so all eight became throwing partial stubs: math/big compiled clean and could not run — every big.Int, big.Float and big.Rat arithmetic path raised on first use. (It surfaced as a single time verdict, TestTruncateRound → big.Int.Mul → mulAddVWW, a good illustration of how little a visible symptom says about the size of its root.) Selecting arith_decl_pure.go also drops arith_amd64.go, whose only declaration is support_adx — read exclusively by the assembly that is not there either. The general rule this states: a package’s portable-fallback tag belongs in the default set whatever it is called; the taxonomy below classifies by whether a portable sibling exists, not by whether it happens to be spelled purego. defaultStdLibBuildTags is content-pinned by TestDefaultStdLibBuildTagsContent, which fails on an undocumented addition as well as a silent removal.
The asm-stub taxonomy. A Go declaration that a platform build binds to assembly falls into one of three buckets, each with a distinct treatment:
purego-gated — the file is… && !puregoand a portable… || puregosibling exists (crypto/hashing hot paths:crypto/sha256,crypto/sha512,crypto/md5,crypto/sha1,crypto/aes,vendor/…/poly1305,vendor/…/chacha20poly1305,vendor/…/sha3,maphash, …). The tag selects the real body — no hand-owning needed. This is the large majority and the whole point of adopting purego.- GOARCH-gated with no
puregoescape — the platform file is gated only on architecture (amd64 || arm64) with no|| puregoalternative, so the tag cannot reach a pure-Go variant (internal/chacha8rand’schacha8_amd64.s;hash/crc32’scrc32_amd64.go, which carries no//go:buildline at all — its only constraint is the_amd64filename suffix, sogo list -tags purego hash/crc32selects exactly the same files as the default build). These are hand-owned: either as a<name>_impl.cscompanion or as a whole-file rewrite (the mechanisms in Manually-Converted Declarations below). Hand-owning does not have to mean stubbing — where .NET exposes the same instructions the.sfile issues, the arch layer can be realized for real; see Realizing an asm-backed arch layer with managed hardware intrinsics. - Genuinely raw-metal — memory-layout math, type-descriptor walking, or
*.asmwith no portable Go form at all (internal/abi,internal/bytealg, parts ofruntime). These are hand-owned, as whole-file[module: GoManualConversion]replacements or*_impl.cscompanions (internal/bytealgusesbytealg_impl.cs), wherever a package’s tests need them; golib’s goroutine model, for example, answers Go’sgetg(). Any that remain compile against a generated throwing stub until Phase 5 supplies them.
Known accepted divergence — crypto/elliptic P256 Inverse panics under purego
One package regresses behaviorally under purego relative to the default build, and it is recorded as a known divergence, not fixed, because matching it is fidelity: real Go panics there too under -tags purego. Upstream crypto/elliptic/nistec_p256.go is gated //go:build amd64 || arm64 without && !purego, while its dependency crypto/internal/nistec/p256_ordinv.go is (amd64 || arm64) && !purego. So under purego the ordinv fallback p256_ordinv_noasm.go — which returns errors.New("unimplemented") — is selected, and crypto/elliptic’s Inverse (reached via the deprecated invertible interface) treats any error from the nistec scalar inverse as an invariant violation and panics crypto/elliptic: nistec rejected normalized scalar. (crypto/ecdsa handles the same error correctly with a Fermat-little-theorem fallback, so ECDSA is unaffected.) Verified against real Go: the default build returns a valid inverse, and go run -tags purego panics with that exact message. This is an upstream Go inconsistency in a largely-superseded package; go2cs reproduces Go-under-purego faithfully, so the panic is expected. It is a divergence from the default (-tags=) build only, which binds the asm ordinv.
← Source Generators · Index · Manually-Converted Declarations →