Skip to content

Add C# bridge and generated SDK - #4074

Draft
rossirpaulo wants to merge 3 commits into
canaryfrom
paulo/csharp
Draft

Add C# bridge and generated SDK#4074
rossirpaulo wants to merge 3 commits into
canaryfrom
paulo/csharp

Conversation

@rossirpaulo

@rossirpaulo rossirpaulo commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a net10.0 baml-bridge host runtime that loads the versioned C API table and supports typed calls, generics, classes, enums, unions, aliases, callbacks, streams, media, resources, cancellation, errors, panics, and hard exits
  • add sdkgen_csharp plus baml generate integration with deterministic naming/routing, bounded embedded bytecode, exact generator/runtime version checks, and transactional generated-file ownership
  • add C# SDK fixtures, xUnit coverage, native parity tests, package normalization, eight-RID NuGet assembly, and reusable CI/release workflows
  • add the completed C# design, capability ledger, decision records, and stress-probe evidence under TASK/

Why

BAML had no complete generated C# SDK or managed bridge across the stable C ABI. This establishes the first supported C# boundary and fixes the public API, ownership, packaging, and compatibility contracts before publication.

User impact

C# consumers generate source under BamlSdk, reference the matching exact baml-bridge version, and call idiomatic sync/async APIs without building the native runtime themselves. The initial runtime supports one distinct compiled BAML program per process. Trimming and NativeAOT are explicit v1 non-goals.

Validation

  • cargo test --locked -p sdkgen_csharp: 18 passed
  • strict dependency-inclusive Clippy for sdkgen_csharp and baml_cli
  • Debug and Release managed solution builds: zero warnings
  • direct xUnit execution: 86/86 passed in both configurations
  • focused socket-free C# nextest matrix: 9/9 passed
  • safe-regeneration tests: 9 unit tests plus 2 CLI integration tests
  • Rust formatting, shell syntax, version coherence, Actionlint, and staged diff checks
  • deterministic atomic package probe: two byte-identical .nupkg/.snupkg assemblies with exactly eight native RID paths

Remaining release gates

Production publication remains disabled until real native binaries are inspected and exercised on all eight RID runners and the NuGet organization/trusted-publisher setup is complete. Two listener-backed fixtures could not be rerun in the current sandbox; their earlier focused runs and managed coverage are recorded in the implementation notes.

Summary by CodeRabbit

  • New Features

    • Added initial C# SDK support for generating and running BAML applications on .NET 10.
    • Added support for synchronous/asynchronous calls, callbacks, streaming, cancellation, errors, unions, generics, media, files, HTTP, CSV, glob, and task-group resources.
    • Added safe generated-source management with deterministic output, stale-file cleanup, and protection against overwriting user edits.
    • Added a single multi-platform NuGet package supporting Linux, macOS, and Windows architectures.
  • Bug Fixes

    • Improved Prompt AST value handling and event flushing across language boundaries.
  • Documentation

    • Added C# bridge design, usage, packaging, compatibility, and completeness documentation.

@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Jul 17, 2026 9:58am
promptfiddle Ready Ready Preview, Comment Jul 17, 2026 9:58am
promptfiddle2 Ready Ready Preview, Comment Jul 17, 2026 9:58am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 30fddb97-ed32-40fd-bc40-496148fc9120

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch paulo/csharp

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🧹 Nitpick comments (4)
baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlHttpRequest.cs (1)

21-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider using case-insensitive header keys.

HTTP header keys are fundamentally case-insensitive according to HTTP standards (RFC 2616). Using StringComparer.Ordinal forces exact-case lookups (e.g. checking Headers["Content-Type"] will throw a KeyNotFoundException if it was mapped as "content-type").

Consider using StringComparer.OrdinalIgnoreCase to align with the standard behavior of .NET's HttpHeaders, assuming the underlying BAML runtime logic does not strictly demand exact-case string matching.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlHttpRequest.cs`
at line 21, Update the headers dictionary initialization in BamlHttpRequest to
use StringComparer.OrdinalIgnoreCase instead of StringComparer.Ordinal, ensuring
header lookups remain case-insensitive while preserving the existing copy
behavior.
baml_language/sdk_tests/crates/csharp/csharp_csv/customizable/Program.cs (1)

82-93: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert every configured reader option.

The “round trip exactly” check omits SkipBlankRecords, Encoding, Bom, OnError, and MaxSkipped, allowing those fields to be dropped by the codec without failing this fixture.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/sdk_tests/crates/csharp/csharp_csv/customizable/Program.cs`
around lines 82 - 93, Extend the validation in the RoundTripCsvReaderOptions
check to assert the configured SkipBlankRecords, Encoding, Bom, OnError, and
MaxSkipped values alongside the existing reader options. Keep the
exact-round-trip failure behavior and use the corresponding fields on
returnedReaderOptions.
baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlClient.cs (1)

46-46: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use ArgumentNullException.ThrowIfNull.

For consistency with modern C# practices and your usage of ArgumentException.ThrowIfNullOrWhiteSpace elsewhere, consider using ArgumentNullException.ThrowIfNull(name) instead of the null-coalescing throw expression.

♻️ Proposed refactor
-        Name = name ?? throw new ArgumentNullException(nameof(name));
+        ArgumentNullException.ThrowIfNull(name);
+        Name = name;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlClient.cs` at
line 46, Update the BamlClient constructor’s name validation to call
ArgumentNullException.ThrowIfNull(name) before assigning Name, replacing the
null-coalescing throw expression while preserving the existing argument
validation behavior.
TASK/state-of-csharp-completeness.md (1)

61-61: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Escape the pipe inside the code span to prevent table parsing issues.

The unescaped pipe | within the inline code block `int | RecursiveNumbers[]` is parsed as a column separator by some Markdown parsers (and triggers markdownlint MD056), causing the table to have an incorrect column count.

♻️ Proposed refactor
-| Recursive type alias | yes | supported | generated nominal wrapper over recursive `BamlUnion` | `csharp_glob::dotnet` | P6 | Nested `int | RecursiveNumbers[]` passes native parity; erased outputs must match one structural arm, ambiguous shapes are rejected, and nullable recursive aliases retain a nominal wrapper. |
+| Recursive type alias | yes | supported | generated nominal wrapper over recursive `BamlUnion` | `csharp_glob::dotnet` | P6 | Nested `int \| RecursiveNumbers[]` passes native parity; erased outputs must match one structural arm, ambiguous shapes are rejected, and nullable recursive aliases retain a nominal wrapper. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@TASK/state-of-csharp-completeness.md` at line 61, Escape the pipe character
in the inline code example within the “Recursive type alias” table row so
Markdown parsers treat it as code content rather than a column separator, while
preserving the example’s meaning and the table’s column count.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/build2-csharp-sdk.reusable.yaml:
- Line 91: Update the checksum generation command in the C# package artifact
workflow to write paths relative to the uploaded artifact root, removing the
artifacts/csharp-package/ prefix from SHA256SUMS. Apply the same path handling
to the corresponding checksum-generation commands around the additionally
referenced lines so sha256sum --check works after artifact download.
- Around line 91-97: Update the provenance-generation step around the jq --arg
source_sha usage to pass inputs.source_sha through the workflow step’s env
configuration, then reference the resulting shell environment variable in Bash
instead of embedding the GitHub template expression directly. Keep the existing
version, package size, and provenance output unchanged.

In `@baml_language/crates/baml_cli/src/generate.rs`:
- Around line 424-443: The rollback handling around the transaction error must
track failures from removing, renaming, and directory-creation operations
instead of discarding them. In the rollback branch, only remove the staging
directory when every restoration succeeds; otherwise preserve it, return an
error indicating rollback was incomplete, and retain the original error context.
Add a fault-injection test covering a restoration failure and verifying the
staging backups remain available.

In
`@baml_language/sdk_tests/crates/csharp/csharp_resources/customizable/Program.cs`:
- Around line 80-99: Bound the header-reading loop in
FetchUrlAsync/ServeResponse by creating or reusing the timeout cancellation
token established near the existing timeout setup and passing it to each
stream.ReadAsync call. Ensure a client that never sends the "\r\n\r\n" header
terminator causes the read to cancel when the timeout expires rather than
waiting indefinitely, while preserving the current disconnect exception for a
zero-byte read.

In
`@baml_language/sdk_tests/crates/csharp/primitive_calls/customizable/Program.cs`:
- Around line 402-429: Update AssertHardExit in
baml_language/sdk_tests/crates/csharp/primitive_calls/customizable/Program.cs:402-429
to wait with a bounded timeout, then kill and reap the exit-probe process if it
does not exit. In
baml_language/sdk_tests/crates/csharp/llm_functions/customizable/Program.cs:39-51
and :262-301, ensure WaitAsync timeouts or startup failures kill the relevant
process tree and that cleanup executes unconditionally.

In `@baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlMedia.cs`:
- Around line 40-44: Update the static Create method to validate the
non-nullable value argument before calling NativeApi.CreateMedia, throwing the
established managed argument exception for null inputs and preserving the
existing native call for valid values.

In
`@baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Bridge/BridgeVersion.cs`:
- Around line 7-12: Update the BridgeVersion.Current metadata lookup to use
SingleOrDefault before accessing Value, allowing missing BamlSdkVersion metadata
to produce the existing BamlBridgeException. Preserve the current behavior for
valid metadata and avoid changing unrelated assembly-version handling.

In
`@baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Bridge/HostValueRegistry.cs`:
- Around line 220-225: Update the emergency completion in HostValueRegistry’s
nested catch around NativeApi.CompleteHostCall so it passes isError: true when
exception encoding fails. Preserve the empty payload fallback while ensuring the
failed host call remains on the error channel.

In `@baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Bridge/NativeApi.cs`:
- Around line 398-431: Restrict DevelopmentCandidates to explicit development
opt-in paths and stop traversing Environment.CurrentDirectory or its ancestors
during normal package loading. Update the native library load flow around
DevelopmentCandidates so production cannot discover target/debug or
target/release binaries from attacker-controlled working-directory trees, while
preserving intentional development probing when the opt-in is enabled.

In
`@baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Bridge/ProtoCodec.cs`:
- Around line 2033-2036: Update the class-valued union validation around the
BamlTy.TyOneofCase.ClassTy branch and its corresponding logic near the
additional referenced range so all codec-supported resource classes, including
BamlGlob, BamlGlobScanOptions, BamlCancelToken, and BamlTaskGroup, are
recognized. Reuse ProtoTypeCodec.Encode(targetType) as the shared descriptor
source or extend BuiltinClassName with these mappings, preserving existing
generated-class validation.
- Around line 1249-1258: Update DecodeValue and the related list/map/class
construction paths to use a shared decode context that tracks every handle
decoded during graph construction. If any child decoding or construction step
throws, dispose all handles owned by that context before propagating the
exception; transfer ownership only after successful construction so the existing
outer cleanup remains correct.

In
`@baml_language/sdks/csharp/bridge_csharp/tools/Baml.NuGet.Normalize/Program.cs`:
- Around line 18-20: Update the output handling around inputPath and outputPath
to reject existing output targets, including case-insensitive Windows aliases
and filesystem links that could refer to the input. Create the output package
exclusively so pre-existing files are never truncated, and track whether this
invocation successfully created it. In the failure cleanup path, delete only
that newly created output, never a pre-existing path or the input package.

In `@baml_language/sdks/csharp/sdkgen_csharp/src/lib.rs`:
- Around line 230-247: The csharp_string function must escape Unicode line
separators U+2028 and U+2029 so generated C# literals remain valid. Add explicit
handling for both characters using their Unicode escape sequences, and add
crate-local unit coverage verifying each is escaped.

In `@baml_language/sdks/csharp/sdkgen_csharp/src/models.rs`:
- Around line 103-110: Update the generated equality and hashing expressions in
the model-generation code around the `Equals` implementations and `GetHashCode`
output to use `EqualityComparer<T>.Default`, where T is the alias value type.
Ensure both comparisons and hash generation remain null-safe when `Value` is
nullable, while preserving the existing nullable `other` check and generated API
shape.
- Around line 325-333: Adjust the support decision in the method-generation
logic around is_static, class_supported, and supported so static methods are not
gated by class_supported or instance-property codec support. Preserve the
existing class_supported validation for instance methods, while allowing
otherwise supported static methods to be emitted normally instead of as
NotSupportedException.

In `@baml_language/sdks/csharp/sdkgen_csharp/src/routing.rs`:
- Around line 235-269: Update file_segment to escape reserved Windows device
names with a prefix that cannot occur in a valid C# identifier, avoiding
collisions with projected names such as _Con. Adjust
keeps_windows_device_names_out_of_generated_paths and add direct assertions for
transformed reserved and non-reserved segments, including that distinct inputs
produce distinct paths.

In `@TASK/codex/implementation-notes.md`:
- Around line 448-453: Update the question-9 coverage status in the
implementation notes to remove trimming/NativeAOT from the outstanding items,
while preserving the remaining supported-host matrix and version-skew work. Keep
the explicit v1 non-goal statements elsewhere unchanged.

---

Nitpick comments:
In `@baml_language/sdk_tests/crates/csharp/csharp_csv/customizable/Program.cs`:
- Around line 82-93: Extend the validation in the RoundTripCsvReaderOptions
check to assert the configured SkipBlankRecords, Encoding, Bom, OnError, and
MaxSkipped values alongside the existing reader options. Keep the
exact-round-trip failure behavior and use the corresponding fields on
returnedReaderOptions.

In `@baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlClient.cs`:
- Line 46: Update the BamlClient constructor’s name validation to call
ArgumentNullException.ThrowIfNull(name) before assigning Name, replacing the
null-coalescing throw expression while preserving the existing argument
validation behavior.

In `@baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlHttpRequest.cs`:
- Line 21: Update the headers dictionary initialization in BamlHttpRequest to
use StringComparer.OrdinalIgnoreCase instead of StringComparer.Ordinal, ensuring
header lookups remain case-insensitive while preserving the existing copy
behavior.

In `@TASK/state-of-csharp-completeness.md`:
- Line 61: Escape the pipe character in the inline code example within the
“Recursive type alias” table row so Markdown parsers treat it as code content
rather than a column separator, while preserving the example’s meaning and the
table’s column count.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d9268423-aa95-4787-b128-dd8f486e8419

📥 Commits

Reviewing files that changed from the base of the PR and between 7d9a665 and 1922827.

⛔ Files ignored due to path filters (1)
  • baml_language/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (109)
  • .github/workflows/build2-csharp-sdk.reusable.yaml
  • .github/workflows/cargo-tests.reusable.yaml
  • .github/workflows/release-baml-language.yml
  • TASK/bridge-csharp.md
  • TASK/codex/bytecode-carrier-probe.md
  • TASK/codex/design-decisions.md
  • TASK/codex/implementation-notes.md
  • TASK/codex/native-package-probe.md
  • TASK/codex/protocol-package-probe.md
  • TASK/codex/union-layout-probe.md
  • TASK/design.md
  • TASK/state-of-csharp-completeness.md
  • TASK/state-of-python-completeness.md
  • baml_language/.config/nextest.toml
  • baml_language/Cargo.toml
  • baml_language/crates/baml_cli/Cargo.toml
  • baml_language/crates/baml_cli/src/generate.rs
  • baml_language/crates/baml_cli/tests/exit_code_e2e.rs
  • baml_language/crates/baml_codegen_types/src/generator_fields.rs
  • baml_language/crates/bex_engine/src/conversion.rs
  • baml_language/crates/bex_engine/tests/prompt_ast_roundtrip.rs
  • baml_language/crates/bridge_cffi/src/api.rs
  • baml_language/sdk_tests/README.md
  • baml_language/sdk_tests/crates/csharp/Cargo.toml
  • baml_language/sdk_tests/crates/csharp/build.rs
  • baml_language/sdk_tests/crates/csharp/csharp_cancel_token/customizable/Program.cs
  • baml_language/sdk_tests/crates/csharp/csharp_csv/customizable/Program.cs
  • baml_language/sdk_tests/crates/csharp/csharp_glob/customizable/Program.cs
  • baml_language/sdk_tests/crates/csharp/csharp_llm_clients/customizable/Program.cs
  • baml_language/sdk_tests/crates/csharp/csharp_resources/customizable/Program.cs
  • baml_language/sdk_tests/crates/csharp/csharp_task_group/customizable/Program.cs
  • baml_language/sdk_tests/crates/csharp/function_calls/customizable/Program.cs
  • baml_language/sdk_tests/crates/csharp/llm_functions/customizable/Program.cs
  • baml_language/sdk_tests/crates/csharp/primitive_calls/customizable/Program.cs
  • baml_language/sdk_tests/crates/csharp/setup.ps1
  • baml_language/sdk_tests/crates/csharp/setup.sh
  • baml_language/sdk_tests/crates/csharp/src/lib.rs
  • baml_language/sdk_tests/fixtures/csharp_cancel_token/baml_src/main.baml
  • baml_language/sdk_tests/fixtures/csharp_csv/baml_src/main.baml
  • baml_language/sdk_tests/fixtures/csharp_glob/baml_src/main.baml
  • baml_language/sdk_tests/fixtures/csharp_llm_clients/baml_src/main.baml
  • baml_language/sdk_tests/fixtures/csharp_resources/baml_src/main.baml
  • baml_language/sdk_tests/fixtures/csharp_task_group/baml_src/main.baml
  • baml_language/sdk_tests/fixtures/function_calls/baml_src/ns_host_callable_tests/main.baml
  • baml_language/sdk_tests/fixtures/primitive_calls/baml_src/main.baml
  • baml_language/sdk_tests/harness_runner/src/lib.rs
  • baml_language/sdk_tests/harness_setup/Cargo.toml
  • baml_language/sdk_tests/harness_setup/src/csharp.rs
  • baml_language/sdk_tests/harness_setup/src/lib.rs
  • baml_language/sdks/csharp/bridge_csharp/.gitignore
  • baml_language/sdks/csharp/bridge_csharp/Baml.Bridge.slnx
  • baml_language/sdks/csharp/bridge_csharp/Directory.Packages.props
  • baml_language/sdks/csharp/bridge_csharp/README.md
  • baml_language/sdks/csharp/bridge_csharp/buildTransitive/baml-bridge.targets
  • baml_language/sdks/csharp/bridge_csharp/global.json
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Baml.Bridge.csproj
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlBridge.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlClient.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlCsv.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlHandle.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlHttpRequest.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlMedia.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlNullable.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlOptional.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlProgram.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlPromptAst.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlResources.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlStream.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlTypeAlias.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlUnion.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/BamlWireNameAttribute.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Bridge/BridgePlatform.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Bridge/BridgeVersion.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Bridge/CallDispatcher.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Bridge/GeneratedContracts.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Bridge/HostValueRegistry.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Bridge/IBamlNullableValue.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Bridge/IBamlStreamValue.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Bridge/IBamlUnionValue.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Bridge/NativeApi.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Bridge/NativeHandle.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Bridge/ProtoCodec.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Bridge/ProtoTypeCodec.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Exceptions.cs
  • baml_language/sdks/csharp/bridge_csharp/src/Baml.Bridge/Properties/AssemblyInfo.cs
  • baml_language/sdks/csharp/bridge_csharp/tests/Baml.Bridge.Tests/Baml.Bridge.Tests.csproj
  • baml_language/sdks/csharp/bridge_csharp/tests/Baml.Bridge.Tests/BamlBridgeTests.cs
  • baml_language/sdks/csharp/bridge_csharp/tests/Baml.Bridge.Tests/BamlNullableTests.cs
  • baml_language/sdks/csharp/bridge_csharp/tests/Baml.Bridge.Tests/BamlOptionalTests.cs
  • baml_language/sdks/csharp/bridge_csharp/tests/Baml.Bridge.Tests/BamlUnionTests.cs
  • baml_language/sdks/csharp/bridge_csharp/tests/Baml.Bridge.Tests/GlobalUsings.cs
  • baml_language/sdks/csharp/bridge_csharp/tests/Baml.Bridge.Tests/ProtoCodecTests.cs
  • baml_language/sdks/csharp/bridge_csharp/tools/Baml.NuGet.Normalize/Baml.NuGet.Normalize.csproj
  • baml_language/sdks/csharp/bridge_csharp/tools/Baml.NuGet.Normalize/Program.cs
  • baml_language/sdks/csharp/bridge_csharp/tools/Baml.Union.Generate/Baml.Union.Generate.csproj
  • baml_language/sdks/csharp/bridge_csharp/tools/Baml.Union.Generate/Program.cs
  • baml_language/sdks/csharp/bridge_csharp/tools/Baml.Union.LayoutProbe/Baml.Union.LayoutProbe.csproj
  • baml_language/sdks/csharp/bridge_csharp/tools/Baml.Union.LayoutProbe/Program.cs
  • baml_language/sdks/csharp/bridge_csharp/tools/pack-all-native.sh
  • baml_language/sdks/csharp/bridge_csharp/tools/pack-native.ps1
  • baml_language/sdks/csharp/bridge_csharp/tools/pack-native.sh
  • baml_language/sdks/csharp/sdkgen_csharp/Cargo.toml
  • baml_language/sdks/csharp/sdkgen_csharp/src/leaf.rs
  • baml_language/sdks/csharp/sdkgen_csharp/src/lib.rs
  • baml_language/sdks/csharp/sdkgen_csharp/src/models.rs
  • baml_language/sdks/csharp/sdkgen_csharp/src/names.rs
  • baml_language/sdks/csharp/sdkgen_csharp/src/routing.rs
  • baml_language/sdks/csharp/sdkgen_csharp/src/translate_ty.rs
  • scripts/baml-language-version

exit 1
fi

sha256sum "$package" artifacts/csharp-package/*.snupkg > artifacts/csharp-package/SHA256SUMS

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Generate artifact-relative checksum paths.

The manifest records artifacts/csharp-package/..., but that directory's contents become the uploaded artifact root. After download, sha256sum --check SHA256SUMS therefore cannot find the packages.

Proposed fix
-          sha256sum "$package" artifacts/csharp-package/*.snupkg > artifacts/csharp-package/SHA256SUMS
+          (
+            cd artifacts/csharp-package
+            sha256sum -- *.nupkg *.snupkg > SHA256SUMS
+          )

Also applies to: 99-104

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build2-csharp-sdk.reusable.yaml at line 91, Update the
checksum generation command in the C# package artifact workflow to write paths
relative to the uploaded artifact root, removing the artifacts/csharp-package/
prefix from SHA256SUMS. Apply the same path handling to the corresponding
checksum-generation commands around the additionally referenced lines so
sha256sum --check works after artifact download.

Comment on lines +91 to +97
sha256sum "$package" artifacts/csharp-package/*.snupkg > artifacts/csharp-package/SHA256SUMS
jq -n \
--arg source_sha "${{ inputs.source_sha }}" \
--arg version "$(jq -r .canonical_version release-plan.json)" \
--argjson package_size "$size" \
'{source_sha: $source_sha, version: $version, package_size: $package_size, package_size_ceiling: 200000000}' \
> artifacts/csharp-package/provenance.json

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Pass source_sha through the environment instead of template expansion.

Line 93 embeds a reusable-workflow input directly into Bash source. A crafted input can alter the command before execution; expose it through env and reference the shell variable.

Proposed fix
       - name: Pack and inspect baml-bridge
+        env:
+          SOURCE_SHA: ${{ inputs.source_sha }}
         run: |
           ...
           jq -n \
-            --arg source_sha "${{ inputs.source_sha }}" \
+            --arg source_sha "$SOURCE_SHA" \
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sha256sum "$package" artifacts/csharp-package/*.snupkg > artifacts/csharp-package/SHA256SUMS
jq -n \
--arg source_sha "${{ inputs.source_sha }}" \
--arg version "$(jq -r .canonical_version release-plan.json)" \
--argjson package_size "$size" \
'{source_sha: $source_sha, version: $version, package_size: $package_size, package_size_ceiling: 200000000}' \
> artifacts/csharp-package/provenance.json
- name: Pack and inspect baml-bridge
env:
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
sha256sum "$package" artifacts/csharp-package/*.snupkg > artifacts/csharp-package/SHA256SUMS
jq -n \
--arg source_sha "$SOURCE_SHA" \
--arg version "$(jq -r .canonical_version release-plan.json)" \
--argjson package_size "$size" \
'{source_sha: $source_sha, version: $version, package_size: $package_size, package_size_ceiling: 200000000}' \
> artifacts/csharp-package/provenance.json
🧰 Tools
🪛 zizmor (1.26.1)

[error] 93-93: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build2-csharp-sdk.reusable.yaml around lines 91 - 97,
Update the provenance-generation step around the jq --arg source_sha usage to
pass inputs.source_sha through the workflow step’s env configuration, then
reference the resulting shell environment variable in Bash instead of embedding
the GitHub template expression directly. Keep the existing version, package
size, and provenance output unchanged.

Source: Linters/SAST tools

Comment on lines +424 to +443
if let Err(error) = transaction {
if manifest_installed {
let _ = std::fs::remove_file(&manifest_path);
}
for relative in installed.iter().rev() {
let _ = std::fs::remove_file(output_dir.join(Path::new(relative)));
}
if manifest_backed_up {
let _ = std::fs::rename(staged_backup.join(GENERATED_MANIFEST_FILE), &manifest_path);
}
for relative in backed_up.iter().rev() {
let backup = staged_backup.join(Path::new(relative));
let destination = output_dir.join(Path::new(relative));
if let Some(parent) = destination.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::rename(backup, destination);
}
let _ = std::fs::remove_dir_all(&staging);
return Err(error.context("Generated output was rolled back"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve backups when rollback is incomplete.

Every restoration error is discarded, then Line 442 deletes the staging directory containing any backup that failed to restore. A filesystem failure can therefore leave output missing while reporting that it “was rolled back.”

Collect rollback failures and retain the staging state whenever restoration is incomplete; add a fault-injection test for this path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_cli/src/generate.rs` around lines 424 - 443, The
rollback handling around the transaction error must track failures from
removing, renaming, and directory-creation operations instead of discarding
them. In the rollback branch, only remove the staging directory when every
restoration succeeds; otherwise preserve it, return an error indicating rollback
was incomplete, and retain the original error context. Add a fault-injection
test covering a restoration failure and verifying the staging backups remain
available.

Comment on lines +80 to +99
var suffix = new Queue<byte>(4);
var buffer = new byte[1];
while (true)
{
if (await stream.ReadAsync(buffer) == 0)
{
throw new IOException("The loopback HTTP client disconnected before sending its headers.");
}

suffix.Enqueue(buffer[0]);
if (suffix.Count > 4)
{
suffix.Dequeue();
}

if (suffix.SequenceEqual("\r\n\r\n"u8.ToArray()))
{
break;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Bound the HTTP header read loop.

A client that connects but never sends \r\n\r\n leaves both FetchUrlAsync and ServeResponse waiting indefinitely; the timeout on Line 69 is reached only after the fetch returns. Pass a timed cancellation token to ReadAsync.

Proposed timeout
 static async Task ServeResponse(TcpListener listener)
 {
     using var client = await listener.AcceptTcpClientAsync().WaitAsync(TimeSpan.FromSeconds(10));
     await using var stream = client.GetStream();
+    using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
     var suffix = new Queue<byte>(4);
     var buffer = new byte[1];
     while (true)
     {
-        if (await stream.ReadAsync(buffer) == 0)
+        if (await stream.ReadAsync(buffer, timeout.Token) == 0)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var suffix = new Queue<byte>(4);
var buffer = new byte[1];
while (true)
{
if (await stream.ReadAsync(buffer) == 0)
{
throw new IOException("The loopback HTTP client disconnected before sending its headers.");
}
suffix.Enqueue(buffer[0]);
if (suffix.Count > 4)
{
suffix.Dequeue();
}
if (suffix.SequenceEqual("\r\n\r\n"u8.ToArray()))
{
break;
}
}
var suffix = new Queue<byte>(4);
var buffer = new byte[1];
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
while (true)
{
if (await stream.ReadAsync(buffer, timeout.Token) == 0)
{
throw new IOException("The loopback HTTP client disconnected before sending its headers.");
}
suffix.Enqueue(buffer[0]);
if (suffix.Count > 4)
{
suffix.Dequeue();
}
if (suffix.SequenceEqual("\r\n\r\n"u8.ToArray()))
{
break;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/sdk_tests/crates/csharp/csharp_resources/customizable/Program.cs`
around lines 80 - 99, Bound the header-reading loop in
FetchUrlAsync/ServeResponse by creating or reusing the timeout cancellation
token established near the existing timeout setup and passing it to each
stream.ReadAsync call. Ensure a client that never sends the "\r\n\r\n" header
terminator causes the read to cancel when the timeout expires rather than
waiting indefinitely, while preserving the current disconnect exception for a
zero-byte read.

Comment on lines +402 to +429
static async Task AssertHardExit(int expectedCode)
{
var executable = Environment.ProcessPath
?? throw new InvalidOperationException("The current executable path is unavailable.");
using var process = Process.Start(new ProcessStartInfo
{
FileName = executable,
ArgumentList = { "--exit-probe", expectedCode.ToString() },
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
}) ?? throw new InvalidOperationException("Failed to start the hard-exit child process.");

var stdout = process.StandardOutput.ReadToEndAsync();
var stderr = process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
if (process.ExitCode != expectedCode)
{
throw new InvalidOperationException(
$"Hard-exit child returned {process.ExitCode}, expected {expectedCode}. "
+ $"stdout={await stdout}; stderr={await stderr}");
}

if ((await stderr).Contains("returned unexpectedly", StringComparison.Ordinal))
{
throw new InvalidOperationException("baml.sys.exit returned instead of terminating the child.");
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- primitive_calls/customizable/Program.cs (around AssertHardExit) ---'
sed -n '360,460p' baml_language/sdk_tests/crates/csharp/primitive_calls/customizable/Program.cs

echo
echo '--- llm_functions/customizable/Program.cs (around RunBuildRequestProcess) ---'
sed -n '1,90p' baml_language/sdk_tests/crates/csharp/llm_functions/customizable/Program.cs

echo
echo '--- llm_functions/customizable/Program.cs (around RunReplay) ---'
sed -n '240,340p' baml_language/sdk_tests/crates/csharp/llm_functions/customizable/Program.cs

echo
echo '--- search for process cleanup / timeout helpers in these test crates ---'
rg -n "WaitForExitAsync|KillTree|Kill\(|Timeout|WaitAsync\\(TimeSpan|Process.Start\\(|CreateSelfStartInfo|StartSelf|Dispose|shutdown" \
  baml_language/sdk_tests/crates/csharp/primitive_calls/customizable/Program.cs \
  baml_language/sdk_tests/crates/csharp/llm_functions/customizable/Program.cs

Repository: BoundaryML/baml

Length of output: 12360


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '340,410p' baml_language/sdk_tests/crates/csharp/llm_functions/customizable/Program.cs

Repository: BoundaryML/baml

Length of output: 1320


Terminate timed-out child processes.

  • baml_language/sdk_tests/crates/csharp/primitive_calls/customizable/Program.cs#L402-L429: add a timeout and kill/reap the exit-probe child if it never exits.
  • baml_language/sdk_tests/crates/csharp/llm_functions/customizable/Program.cs#L39-L51 and #L262-L301: WaitAsync(...) can throw while the child/server keeps running; kill the process tree on timeout or startup failure, and ensure cleanup runs unconditionally.
📍 Affects 2 files
  • baml_language/sdk_tests/crates/csharp/primitive_calls/customizable/Program.cs#L402-L429 (this comment)
  • baml_language/sdk_tests/crates/csharp/llm_functions/customizable/Program.cs#L39-L51
  • baml_language/sdk_tests/crates/csharp/llm_functions/customizable/Program.cs#L262-L301
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@baml_language/sdk_tests/crates/csharp/primitive_calls/customizable/Program.cs`
around lines 402 - 429, Update AssertHardExit in
baml_language/sdk_tests/crates/csharp/primitive_calls/customizable/Program.cs:402-429
to wait with a bounded timeout, then kill and reap the exit-probe process if it
does not exit. In
baml_language/sdk_tests/crates/csharp/llm_functions/customizable/Program.cs:39-51
and :262-301, ensure WaitAsync timeouts or startup failures kill the relevant
process tree and that cleanup executes unconditionally.

Comment on lines +230 to +247
pub(crate) fn csharp_string(value: &str) -> String {
let mut out = String::from("\"");
for ch in value.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
ch if ch.is_control() => {
let _ = write!(out, "\\u{:04x}", ch as u32);
}
ch => out.push(ch),
}
}
out.push('"');
out
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- lib.rs around csharp_string ---'
sed -n '200,290p' baml_language/sdks/csharp/sdkgen_csharp/src/lib.rs

echo
echo '--- search for csharp_string and related tests ---'
rg -n "csharp_string|2028|2029|unicode separator|line separator" baml_language/sdks/csharp/sdkgen_csharp/src -S

echo
echo '--- file list in crate ---'
git ls-files baml_language/sdks/csharp/sdkgen_csharp/src baml_language/sdks/csharp/sdkgen_csharp/tests

Repository: BoundaryML/baml

Length of output: 6162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- tests in lib.rs around the existing assertions ---'
sed -n '340,460p' baml_language/sdks/csharp/sdkgen_csharp/src/lib.rs

echo
echo '--- any unit tests in sibling modules using csharp_string ---'
rg -n "assert!\\(|csharp_string\\(" baml_language/sdks/csharp/sdkgen_csharp/src/{models.rs,leaf.rs,translate_ty.rs} -S

Repository: BoundaryML/baml

Length of output: 8773


🌐 Web query:

C# string literal U+2028 U+2029 source newline line separator lexical grammar

💡 Result:

In the C# lexical grammar, the Unicode characters U+2028 (Line Separator) and U+2029 (Paragraph Separator) are explicitly classified as New_Line_Character components [1][2]. Because they are defined as New_Line_Character, they are treated as line terminators by the C# compiler [1][2]. This has specific implications for string literals: 1. Regular String Literals: The lexical grammar for regular string literals forbids the inclusion of raw new-line characters, including U+2028 and U+2029 [3][1]. To include these characters in a regular string literal, you must use their corresponding Unicode escape sequences (\u2028 and \u2029) [3]. 2. Raw String Literals: The content of a raw string literal also excludes these new-line characters [1]. The grammar for raw string literal content is defined as any character except the specified New_Line_Character set (which includes U+2028 and U+2029) [1]. In summary, U+2028 and U+2029 are treated as structural newline separators in C# source code [1][2]. They cannot appear literally within standard or raw string literals and must be escaped if you wish to represent them as data within a string [3][1].

Citations:


Escape C# Unicode line separators. csharp_string also needs to escape U+2028/U+2029; C# treats both as line terminators, so generated .g.cs literals can break when wire values contain them. Please add a crate-local unit test for both characters.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/sdks/csharp/sdkgen_csharp/src/lib.rs` around lines 230 - 247,
The csharp_string function must escape Unicode line separators U+2028 and U+2029
so generated C# literals remain valid. Add explicit handling for both characters
using their Unicode escape sequences, and add crate-local unit coverage
verifying each is escaped.

Source: Coding guidelines

Comment on lines +103 to +110
out,
" public bool Equals({class_name}? other) => other is not null && Value.Equals(other.Value);\n"
);
let _ = writeln!(
out,
" public override bool Equals(object? obj) => obj is {class_name} other && Equals(other);\n"
);
out.push_str(" public override int GetHashCode() => Value.GetHashCode();\n\n");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Make recursive-alias equality null-safe.

A nullable alias value makes Value.Equals(...) or Value.GetHashCode() throw. Generate equality and hashing through EqualityComparer<T>.Default, with a null-safe hash.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/sdks/csharp/sdkgen_csharp/src/models.rs` around lines 103 -
110, Update the generated equality and hashing expressions in the
model-generation code around the `Equals` implementations and `GetHashCode`
output to use `EqualityComparer<T>.Default`, where T is the alias value type.
Ensure both comparisons and hash generation remain null-safe when `Value` is
nullable, while preserving the existing nullable `other` check and generated API
shape.

Comment on lines +325 to +333
let is_static = context.receiver.is_static();
let is_async = variant.is_async();
let class_supported = context.class_supported;
let class_generic_params = context.class_generic_params;
let aliases = context.aliases;
let use_async_callback_types = variant.uses_async_callbacks();
let mut occupied = BTreeSet::new();
let mut parameters = Vec::new();
let mut supported = class_supported;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not gate static methods on instance-property codec support.

class_supported depends on the class properties, but static methods never encode self. A supported static method on a class containing an unsupported property is currently emitted as NotSupportedException.

Proposed fix
-    let mut supported = class_supported;
+    let mut supported = is_static || class_supported;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let is_static = context.receiver.is_static();
let is_async = variant.is_async();
let class_supported = context.class_supported;
let class_generic_params = context.class_generic_params;
let aliases = context.aliases;
let use_async_callback_types = variant.uses_async_callbacks();
let mut occupied = BTreeSet::new();
let mut parameters = Vec::new();
let mut supported = class_supported;
let is_static = context.receiver.is_static();
let is_async = variant.is_async();
let class_supported = context.class_supported;
let class_generic_params = context.class_generic_params;
let aliases = context.aliases;
let use_async_callback_types = variant.uses_async_callbacks();
let mut occupied = BTreeSet::new();
let mut parameters = Vec::new();
let mut supported = is_static || class_supported;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/sdks/csharp/sdkgen_csharp/src/models.rs` around lines 325 -
333, Adjust the support decision in the method-generation logic around
is_static, class_supported, and supported so static methods are not gated by
class_supported or instance-property codec support. Preserve the existing
class_supported validation for instance methods, while allowing otherwise
supported static methods to be emitted normally instead of as
NotSupportedException.

Comment on lines +235 to +269
fn file_segment(projected: &str) -> String {
let unescaped = projected.strip_prefix('@').unwrap_or(projected);
let upper = unescaped.to_ascii_uppercase();
let reserved = matches!(upper.as_str(), "CON" | "PRN" | "AUX" | "NUL")
|| upper.strip_prefix("COM").is_some_and(|suffix| {
matches!(suffix, "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9")
})
|| upper.strip_prefix("LPT").is_some_and(|suffix| {
matches!(suffix, "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9")
});
if reserved {
format!("_{unescaped}")
} else {
unescaped.to_string()
}
}

#[cfg(test)]
mod tests {
use baml_base::Name as BaseName;

use super::*;

#[test]
fn keeps_windows_device_names_out_of_generated_paths() {
let name = Name::new(
BaseName::new("user"),
vec![BaseName::new("con"), BaseName::new("lpt1")],
BaseName::new("probe"),
);

let leaf = route(&name);
assert_eq!(leaf.namespace, "BamlSdk.Con.Lpt1");
assert_eq!(leaf.path, PathBuf::from("_Con/_Lpt1"));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make Windows device-name escaping collision-free.

Con is rewritten to _Con, which collides with the valid projected identifier _Con. Distinct leaves can therefore receive the same Functions.g.cs/Types.g.cs path, and the path-keyed output map in sdkgen_csharp/src/lib.rs will replace one generated file.

Use a prefix impossible in a C# identifier and test the transformed segments directly.

Proposed fix
     if reserved {
-        format!("_{unescaped}")
+        format!("~{unescaped}")
     } else {
         unescaped.to_string()
     }
-        assert_eq!(leaf.path, PathBuf::from("_Con/_Lpt1"));
+        assert_eq!(leaf.path, PathBuf::from("~Con/~Lpt1"));
+        assert_ne!(
+            file_segment("Con").to_ascii_lowercase(),
+            file_segment("_Con").to_ascii_lowercase()
+        );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn file_segment(projected: &str) -> String {
let unescaped = projected.strip_prefix('@').unwrap_or(projected);
let upper = unescaped.to_ascii_uppercase();
let reserved = matches!(upper.as_str(), "CON" | "PRN" | "AUX" | "NUL")
|| upper.strip_prefix("COM").is_some_and(|suffix| {
matches!(suffix, "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9")
})
|| upper.strip_prefix("LPT").is_some_and(|suffix| {
matches!(suffix, "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9")
});
if reserved {
format!("_{unescaped}")
} else {
unescaped.to_string()
}
}
#[cfg(test)]
mod tests {
use baml_base::Name as BaseName;
use super::*;
#[test]
fn keeps_windows_device_names_out_of_generated_paths() {
let name = Name::new(
BaseName::new("user"),
vec![BaseName::new("con"), BaseName::new("lpt1")],
BaseName::new("probe"),
);
let leaf = route(&name);
assert_eq!(leaf.namespace, "BamlSdk.Con.Lpt1");
assert_eq!(leaf.path, PathBuf::from("_Con/_Lpt1"));
}
fn file_segment(projected: &str) -> String {
let unescaped = projected.strip_prefix('@').unwrap_or(projected);
let upper = unescaped.to_ascii_uppercase();
let reserved = matches!(upper.as_str(), "CON" | "PRN" | "AUX" | "NUL")
|| upper.strip_prefix("COM").is_some_and(|suffix| {
matches!(suffix, "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9")
})
|| upper.strip_prefix("LPT").is_some_and(|suffix| {
matches!(suffix, "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9")
});
if reserved {
format!("~{unescaped}")
} else {
unescaped.to_string()
}
}
#[cfg(test)]
mod tests {
use baml_base::Name as BaseName;
use super::*;
#[test]
fn keeps_windows_device_names_out_of_generated_paths() {
let name = Name::new(
BaseName::new("user"),
vec![BaseName::new("con"), BaseName::new("lpt1")],
BaseName::new("probe"),
);
let leaf = route(&name);
assert_eq!(leaf.namespace, "BamlSdk.Con.Lpt1");
assert_eq!(leaf.path, PathBuf::from("~Con/~Lpt1"));
assert_ne!(
file_segment("Con").to_ascii_lowercase(),
file_segment("_Con").to_ascii_lowercase()
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/sdks/csharp/sdkgen_csharp/src/routing.rs` around lines 235 -
269, Update file_segment to escape reserved Windows device names with a prefix
that cannot occur in a valid C# identifier, avoiding collisions with projected
names such as _Con. Adjust keeps_windows_device_names_out_of_generated_paths and
add direct assertions for transformed reserved and non-reserved segments,
including that distinct inputs produce distinct paths.

Comment on lines +448 to +453
This is still not the full question-9 probe. The complete supported-host
matrix, trimming/NativeAOT, and version-skew coverage remain outstanding.
Deterministic generation, direct/imported-schema invalidation, managed
compilation, package contents/dependencies, generated-source compilation,
primitive runtime round trips, and a clean package consumer now pass on Linux
x64.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove trimming/NativeAOT from the outstanding question-9 coverage.

They are explicit v1 non-goals elsewhere in this document, not pending protobuf-host validation. Keeping them here makes the completion status contradictory.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@TASK/codex/implementation-notes.md` around lines 448 - 453, Update the
question-9 coverage status in the implementation notes to remove
trimming/NativeAOT from the outstanding items, while preserving the remaining
supported-host matrix and version-skew work. Keep the explicit v1 non-goal
statements elsewhere unchanged.

@github-actions

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@github-actions

Copy link
Copy Markdown

Binary size checks failed

4 violations · ✅ 3 passed

⚠️ Please fix the size gate issues or acknowledge them by updating baselines.

Artifact Platform File Gzip Gated on Baseline Delta Status
baml-cli Linux 🔒 25.5 MB 10.8 MB file 24.5 MB +1.0 MB (+4.1%) FAIL
packed-program Linux 🔒 17.0 MB 7.0 MB file 17.0 MB +0 B (+0.0%) OK
baml-cli macOS 🔒 19.7 MB 9.4 MB file 18.9 MB +827.0 KB (+4.4%) FAIL
packed-program macOS 🔒 13.2 MB 6.2 MB file 13.2 MB +0 B (+0.0%) OK
baml-cli Windows 🔒 21.2 MB 9.6 MB file 20.4 MB +802.8 KB (+3.9%) FAIL
packed-program Windows 🔒 14.1 MB 6.2 MB file 14.1 MB -512 B (-0.0%) OK
bridge_wasm WASM 16.2 MB 🔒 4.4 MB gzip 4.3 MB +130.5 KB (+3.1%) FAIL

🔒 = the size this artifact is GATED on (ceiling + delta). Binaries gate on file size (installed binary); WASM gates on gzip (download size). The other size is shown for information only.

Details & how to fix

Violations:

  • baml-cli (Linux) file_bytes: 25.5 MB exceeds limit of 25.2 MB (exceeded by +271.2 KB, policy: max_file_bytes)
  • baml-cli (Linux) file_delta_pct: +4.1% exceeds limit of 3.0% (exceeded by +1.1pp, policy: max_delta_pct)
  • baml-cli (macOS) file_bytes: 19.7 MB exceeds limit of 19.5 MB (exceeded by +260.0 KB, policy: max_file_bytes)
  • baml-cli (macOS) file_delta_pct: +4.4% exceeds limit of 3.0% (exceeded by +1.4pp, policy: max_delta_pct)
  • baml-cli (Windows) file_bytes: 21.2 MB exceeds limit of 21.0 MB (exceeded by +190.0 KB, policy: max_file_bytes)
  • baml-cli (Windows) file_delta_pct: +3.9% exceeds limit of 3.0% (exceeded by +0.9pp, policy: max_delta_pct)
  • bridge_wasm (WASM) gzip_bytes: 4.4 MB exceeds limit of 4.4 MB (exceeded by +2.3 KB, policy: max_gzip_bytes)
  • bridge_wasm (WASM) gzip_delta_pct: +3.1% exceeds limit of 3.0% (exceeded by +0.1pp, policy: max_delta_pct)

Add/update baselines:

.ci/size-gate/aarch64-apple-darwin.toml:

[artifacts.baml-cli]
file_bytes = 19727136
stripped_bytes = 19727184
gzip_bytes = 9417000

.ci/size-gate/wasm32-unknown-unknown.toml:

[artifacts.bridge_wasm]
file_bytes = 16160601
gzip_bytes = 4402874

.ci/size-gate/x86_64-pc-windows-msvc.toml:

[artifacts.baml-cli]
file_bytes = 21229568
stripped_bytes = 21229568
gzip_bytes = 9625194

.ci/size-gate/x86_64-unknown-linux-gnu.toml:

[artifacts.baml-cli]
file_bytes = 25514064
stripped_bytes = 25514056
gzip_bytes = 10836002

Generated by cargo size-gate · workflow run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant