Skip to content

Conversation

@an0
Copy link
Contributor

@an0 an0 commented Nov 27, 2025

Summary

Fixes #85701 - NS_CLOSED_ENUM now properly validates raw values in init?(rawValue:), returning nil for invalid values instead of creating invalid enum instances that crash later.

Problem

When importing C enums marked with NS_CLOSED_ENUM (which map to @frozen Swift enums), the synthesized init?(rawValue:) used Builtin.reinterpretCast without validation. This allowed creating enum instances with invalid raw values that would crash with "Fatal error: unexpected enum case" when used in switch statements.

Solution

  • For @frozen enums (NS_CLOSED_ENUM): Generate a switch statement that validates the raw value against all declared cases, returning nil for invalid values
  • For non-frozen enums (NS_ENUM): Preserve existing reinterpretCast behavior for C compatibility

Implementation

Modified synthesizeEnumRawValueConstructorBody() in lib/ClangImporter/SwiftDeclSynthesizer.cpp to detect frozen enums and generate switch-based validation, following the same pattern used for native Swift enums in DerivedConformanceRawRepresentable.cpp.

Testing

Added comprehensive execution tests that verify:

  • Valid raw values correctly initialize enum instances
  • Invalid raw values return nil (not crash)
  • Non-contiguous and negative raw values handled correctly
  • Open enums still accept arbitrary values for C compatibility

Performance

Matches native Swift enums.

Copy link
Contributor

@j-hui j-hui left a comment

Choose a reason for hiding this comment

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

Thank you for your contribution! The compiler changes look good to me, aside from a question and a nit. I would like the test to be migrated to use StdlibUnittest though.

// RUN: %empty-directory(%t)
// RUN: %target-build-swift %s -import-objc-header %S/Inputs/enum-closed-raw-value.h -o %t/a.out
// RUN: %target-codesign %t/a.out
// RUN: %target-run %t/a.out | %FileCheck %s
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please use StdlibUnittest and its expectEqual() functions, rather than pipe the output to FileCheck? See test/Interop/Cxx/enum/hashable-enums.swift for an example.

@@ -0,0 +1,84 @@
// RUN: %empty-directory(%t)
// RUN: %target-build-swift %s -import-objc-header %S/Inputs/enum-closed-raw-value.h -o %t/a.out
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please test this both with and without C++ interop?


auto body = BraceStmt::create(ctx, SourceLoc(), {switchStmt}, SourceLoc(),
/*implicit*/ true);
return {body, /*isTypeChecked=*/false};
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please elaborate on why isTypeChecked is returned as false here?

auto body = BraceStmt::create(ctx, SourceLoc(), {switchStmt}, SourceLoc(),
/*implicit*/ true);
return {body, /*isTypeChecked=*/false};
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: no need for else branch, since the "then" branch returns at the end. This also minimizes the diff.

@j-hui
Copy link
Contributor

j-hui commented Nov 27, 2025

@swift-ci please test

an0 added 2 commits November 27, 2025 14:36
Add execution tests verifying that NS_CLOSED_ENUM (imported as @Frozen)
properly validates raw values in init?(rawValue:), returning nil for
invalid values instead of creating poison enum instances.

These tests currently fail, demonstrating issue swiftlang#85701 where invalid
raw values are accepted and later crash in switch statements.
Generate switch-based validation for @Frozen enum init?(rawValue:)
instead of using unchecked reinterpretCast. This ensures only declared
enum cases are accepted, returning nil for invalid raw values.

For non-frozen enums (NS_ENUM), preserve existing reinterpretCast
behavior for C compatibility.

Fixes swiftlang#85701
@an0
Copy link
Contributor Author

an0 commented Nov 27, 2025

@j-hui I've addressed all the review feedback:

  • ✅ Converted tests to StdlibUnittest framework with expectEqual(), expectNil(), etc.
  • ✅ Added C++ interop test variant (two RUN lines)
  • ✅ Removed unnecessary else branch in SwiftDeclSynthesizer.cpp
  • ✅ Added comments explaining isTypeChecked reasoning for both code paths

auto *caseBody = BraceStmt::create(ctx, SourceLoc(), {caseAssign},
SourceLoc(), /*implicit*/ true);

auto *caseStmt = CaseStmt::createImplicit(ctx, CaseParentKind::Switch,

Choose a reason for hiding this comment

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

Great contribution! You could save a few bytes by generating one case statement with each known case as a series of labels, as their bodies are the same (reinterpret cast)

equivalent to the source:

switch rawValue {
case .first, .second, third:
  reinterpretCast
default:
  return nil
}

function merging may consolidate these blocks on the back end but better not to emit them in the first place

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.

Imported enum from NS_CLOSED_ENUM shouldn't allow arbitrary raw value

3 participants