Harden the Native AOT release path from the #1324 review - #1344
Merged
Conversation
added 6 commits
July 31, 2026 00:50
Release blockers found by reviewing the epic before the first tag: - Compile errors now print to stderr (Program.cs generic catch + explicit bundler failure). The tag-time macOS smoke greps stderr for PE-Packer's Mach-O refusal, which previously landed on stdout and would have failed the first release run. - publish.yml is reordered so nothing publishes before it is validated: build/pack -> all twelve managed+native artifact smokes -> a final tag-only release job that pushes NuGet and attaches every archive. A failing RID now blocks the release instead of leaving a missing asset behind a public package. workflow_dispatch runs the full matrix as a no-publish dry run to validate runner labels before the first tag. win/linux arm64 managed artifacts now smoke on real arm runners; native smokes gain the five Managed-SKU-only refusal assertions, the per-RID publish-warning ratchet, and CRLF normalization for Windows RIDs. Native-SKU behavior fixes: - ManagedStructuralClrReflection probes (TryGet*) return null under Native AOT instead of throwing: every call site is a structural probe with its own guest-level error path, and the open CLR universe is empty by construction in a native process. Plain-TypeScript programs hitting Proxy traps or descriptor coercion natively now get their ordinary "not a function"-style errors, not a reflection PNSE. - ManagedEmittedShapeReflection.IsShape answers false for every shape under native, so IsCallable/Invoke can no longer disagree; RequireManagedShape checks native before shape for the clearer error. - A native-SKU extraction failure of the embedded managed runtime is now a fatal compile error instead of warning-and-continue - it is the only soft-dependency mechanism the native SKU has. Guardrails: - The mandatory AOT publish flags (TrimMode=partial, IlcTrimMetadata=false, IlcGenerateCompleteTypeMetadata=true, StackTraceSupport=true) move into SharpTS.csproj behind PublishAot=true; workflows no longer duplicate them and a local publish matches CI. A Native AOT publish without SharpTSManagedRuntimePayloadPath is an error (opt-out property for probe builds), and the payload is validated as AnyCPU by PE Machine word - the arch-stamped-payload failure was previously reproduced in the wild. - features.ts gains Promise<UserClass> and typed user-class array shapes, so the TypeBuilderInstantiation fallback (not just the async builder's MethodBuilderInstantiation) executes on every CI run; both gates assert the new '2 2 33 7 11' output. - New architecture tests pin the seam routing: raw MakeGenericType/ MakeGenericMethod outside EmitGenerics/TypeProvider/ManagedDotNetInterop and any resurrected CustomAttributeBuilder use fail the suite. - CustomAttributeEncoder gets byte-exact blob tests plus a persisted- assembly round trip; null Type args encode as the 0xFF SerString and null value-type args throw a named error instead of NRE. EmbeddedManagedRuntime's atomic-write discipline is unit-tested via a stream overload split from the resource lookup. - Ratchet scripts print their per-record diff before failing (Write-Error under ErrorActionPreference=Stop swallowed it) and fail on a zero-line parse when the baseline expects warnings, closing the silent-green localization/format-drift vector. test-examples.ps1 gains -MinimumExecuted so a mass-skip cannot pass the corpus gate (CI floor 50). - TypeProvider is documented as the fifth, deliberately unguarded seam; stale NodeRegistry references in CONTRIBUTING.md and native-aot.md are rewritten to describe the shipped catalog + dispatch-switch design. Part of #1324.
Check() and CheckWithRecovery() share a copy-pasted prologue that had diverged: only the recovery path called HoistClassDeclarations, so a function body forward-referencing a class declared later in the file type-checked on the CLI path but failed through workers and Test262, which use Check() directly.
RealPackageSmokeTests runs a real npm install against registry.npmjs.org but its "npm" category was not in the test filters, so every CI and release run hit the live registry — the same hermeticity hazard the LiveNetwork quarantine exists for. The host-literal guardrail cannot catch package-name installs, so the category filter is the enforcement.
FileDiscovery documents itself as the single upward-walk policy (temp root and user profile are exclusive ceilings), but this resolver carried its own verbatim copy of the ceiling logic in FindVisibleTypeRoots, and its Ancestors loop -- used by ResolveLib -- had no ceilings at all, so lib resolution would ascend into %TEMP%/%USERPROFILE% that every other loader refuses. Both now derive parents via FileDiscovery.AmbientParent.
Checker messages carry no prefix by convention -- Diagnostic prepends it on render. These two carried a leading-space " Type Error: " that also defeated RecordTypeError''s exact-prefix stripping, so users saw doubled "Type Error: Type Error: Inferred type ..." output.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
An architectural review of the #1324 epic (before the first tagged release) confirmed the design is sound but found a handful of concrete defects and unenforced guarantees. This PR fixes all of them.
Release blockers
Program.csand the explicit-bundler failure branch wrote to stdout. The tag-time macOS native smoke greps stderr for PE-Packer's Mach-O refusal (cannot target), so the firstosx-*release job would have failed on a stream mismatch. One test asserted the old stdout behavior and was updated to the new contract.publish.ymlnow validates before it publishes. Previously thepublishjob pushed NuGet and created the GitHub Release first, and the artifact matrices merely uploaded afterwards — a failing RID left a silently missing asset behind an already-public package. New shape:build(pack + test) →binaries+native-binaries(all twelve artifact smokes) → tag-onlyreleasejob that pushes NuGet and attaches every archive. Also:workflow_dispatchruns the full matrix as a no-publish dry run (0.0.0-dryrun.Nversions) — run it once before the first real tag to validate thewindows-11-vs2026-arm/macos-15-intelrunner labels.-r, .NET interop,--gen-decl,--verify),features.ts, the per-RID publish-warning ratchet, and CRLF normalization sogrep -qxworks on Windows RIDs.Native-SKU behavior fixes
ManagedStructuralClrReflectionprobes returnnullunder Native AOT instead of throwing. Every call site (Proxy traps,FromAnyObject, vm context, KeyObject export…) is a structural probe with its own guest-level fall-through error, and the open CLR universe is empty by construction in a native process (arbitrary CLR objects can only enter via the .NET interop boundary, which rejects native first). A plain-TypeScript program running natively now gets its ordinary"…is not a function"error, never a reflectionPlatformNotSupportedException. Methods renamedTryGet*to make the probe semantics explicit.ManagedEmittedShapeReflection.IsShapeanswers false for every shape under native, soRuntimeCallableDispatcher.IsCallable/Invokecan no longer disagree (predicate said yes, action threw).RequireManagedShapechecks native before shape so unguarded callers get the clear named diagnostic.Guardrails
SharpTS.csprojbehindPublishAot=true(TrimMode=partial,IlcTrimMetadata=false,IlcGenerateCompleteTypeMetadata=true,StackTraceSupport=true); ci.yml/publish.yml no longer duplicate them, and a localdotnet publish -p:PublishAot=truematches CI instead of producing a broken binary.SharpTSManagedRuntimePayloadPathis a build error (opt-out:SharpTSAllowMissingManagedRuntimePayload=truefor probe builds), and the payload's PE Machine word must be AnyCPU/I386 — the arch-stamped-payload failure mode was previously reproduced in the wild ("SharpTS runtime not present" under a different arch's dotnet).features.tsnow forces theTypeBuilderInstantiationfallback (Promise<Counter>+ typedCounter[]close runtime open generics overTypeBuilderargs). Previously only the async builder'sMethodBuilderInstantiationfallback executed in CI, despite comments claiming both. Both gates assert the new2 2 33 7 11output.SharpTS.Tests/Architecture/) pin the seam routing: rawMakeGenericType/MakeGenericMethodoutsideEmitGenerics/TypeProvider/ManagedDotNetInterop, or any resurrectedCustomAttributeBuilderuse, fail the suite — a copy-pasted suppression can no longer satisfy the analyzer ratchet while breaking the native compiler.CustomAttributeEncodergets byte-exact §II.23.3 blob tests plus a persisted-assembly round trip; nullTypeargs now encode as the 0xFF null SerString and null value-type args throw a named error instead of NRE.EmbeddedManagedRuntime's atomic-write discipline (temp + rename, overwrite, cleanup, locked destination) is unit-tested via a stream overload split from the resource lookup.Write-ErrorunderErrorActionPreference=Stopterminated before the detail printed) and fail on a zero-line parse when the baseline expects warnings — closing the silent-green vector from localized runner output or diagnostic-format drift.test-examples.ps1 -MinimumExecutedfails the corpus gate if executed (non-skipped) cases drop below a floor (CI: 50 ≈ losing a whole path), so a refactor that mass-skips can't go green.TypeProvideris documented as the fifth, deliberately unguarded seam with its invariant stated; staleNodeRegistryreferences inCONTRIBUTING.mdanddocs/plans/native-aot.mdrewritten to describe the shipped catalog + dispatch-switch design;native-aot.mdupdated for the flag ownership, gated release flow, and probe semantics.Known follow-up (not in this PR)
The extraction/co-location path ships only
SharpTS.dll, no dependency closure —eval/vmcode reaching zlib needsZstdSharp.dll, which the test harness copies explicitly but production does not. Which dependencies the soft-dep contract promises is a design decision; deliberately left for its own issue.Test plan
Category!=LiveNetwork&LoadSensitive)scripts/aot-analyzer-report.ps1 -EnforceBaseline— inventory still exactly zerofeatures.tsinterpreted + compiled under JIT both print2 2 33 7 110xAA64);PublishAot=truewithout payload rejected with guidancepublish.ymlworkflow_dispatchdry run to validate the six native runner labels before the first tagPart of #1324.