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. at Go 1.26.10 crypto/internal/fips140/sha256/sha256block_amd64.go is //go:build !purego and declares func blockAVX2(dig *Digest, p []byte) and func blockSHANI(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 clean-compile milestone) but cannot run, which blocks running the package’s tests. 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_noasm.go is //go:build (!386 && !amd64 && … ) || purego and has a real body), replacing the stubs with convertible code. Adopting the tag dropped no compiling package 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/internal/fips140/subtle’s xor_asm.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 purego default does not touch -recurse end-user conversions or single-file/dir conversions — there the user’s own build tags govern. (-recurse has a separate, narrower default of its own — the safe tag, defaultModuleBuildTags, which libraries use to choose their fallback over unsafe pointer arithmetic into Go runtime internals; it is described in the -module-safe-tag help text, switched off with -module-safe-tag=false, and replaced by any explicit -tags.) 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,math_big_pure_go (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 six bodyless //go:noescape functions — addVV, subVV, lshVU, rshVU, mulAddVWW, addMulVVWW — 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 six 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 test failure, 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 hasADX — 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,crypto/internal/fips140/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.
Under purego the P256 scalar inverse is a stub, and its one caller steps around it
Under -tags purego, crypto/internal/fips140/nistec builds p256_ordinv_noasm.go
(//go:build (!amd64 && !arm64) || purego) in place of the assembly-backed p256_ordinv.go
((amd64 || arm64) && !purego), so its P256OrdInverse always returns errors.New("unimplemented"). Real Go
built with -tags purego has the same stub, and go2cs converts it as it is. At Go 1.26.10 the stub reaches no
panic. Its only caller is ECDSA’s inverse (crypto/internal/fips140/ecdsa/ecdsa.go), through the P-256 curve’s
ordInverse, and on any error that function computes the inverse by Fermat’s method instead,
kInv.Exp(k, c.nMinus2, c.N), as Go’s own comment there allows for platforms without the fast form. So ECDSA is
correct under purego and differs from the default build only in how it inverts.
← Source Generators · Index · Manually-Converted Declarations →