Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion codegen/snippet-tests/output/Classes.pkl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ extension Classes {
case butterfly = "butterfly"
case beetle = #"beetle""#
case beetleOne = "beetle one"
case beetle_one = "beetle_one"
case beetleOne2 = "beetle_one"
}

public struct Module: PklRegisteredType, Decodable, Hashable, Sendable {
Expand Down
7 changes: 5 additions & 2 deletions codegen/snippet-tests/output/Enums.pkl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ extension Enums {
}
}

public enum BugBug: String, CaseIterable, CodingKeyRepresentable, Decodable, Hashable, Sendable {
case bugBug = "bug bug"
case bugBug2 = "bugBug"
}

public enum HorseOrBug: Decodable, Hashable, Sendable {
case horse(Horse)
case string(String)
Expand Down Expand Up @@ -178,8 +183,6 @@ extension Enums {
}
}

public typealias BugBug = String

/// Load the Pkl module at the given source and evaluate it into `Enums.Module`.
///
/// - Parameter source: The source of the Pkl module.
Expand Down
9 changes: 2 additions & 7 deletions codegen/src/Generator.pkl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//===----------------------------------------------------------------------===//
// Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
// Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -36,19 +36,14 @@ indent: String = " "
typealias EnumMember =
reflect.DeclaredType | reflect.StringLiteralType | reflect.NullableType | reflect.NothingType

function hasDistinctEnumNames(members: List<reflect.StringLiteralType>) =
let (names = members.map((it) -> utils.normalizeEnumName(it.value)))
names.isDistinct

// noinspection TypeMismatch
function isEnumLike(decl: reflect.TypeDeclaration) =
decl is reflect.TypeAlias
&& let (referent = decl.referent)
referent is reflect.UnionType
&& referent.members.every((t) -> t is EnumMember)
&& if (referent.members is List<reflect.StringLiteralType>)
referent.members.every((it) -> utils.canBeNormalizedToEnumCaseName(it.value))
&& hasDistinctEnumNames(referent.members)
referent.members.every((it: reflect.StringLiteralType) -> utils.canBeNormalizedToEnumCaseName(it.value))
else
true

Expand Down
21 changes: 16 additions & 5 deletions codegen/src/internal/EnumGen.pkl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//===----------------------------------------------------------------------===//
// Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
// Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -45,10 +45,21 @@ local enumBaseMembers: List<EnumMember> =
type = typegen.generateType(it, module.mapping.source, module.mappings)
})

local enumStringLiteralMembers =
enumBaseMembers.map((it) -> (it) {
name = utils.normalizeEnumName((it.pklType as reflect.StringLiteralType).value)
})
/// Appends a numeric suffix to `candidate` until it no longer collides with an
/// already-assigned case name. Two distinct Pkl string values can normalize to the
/// same Swift identifier (e.g. "beetle one" and "beetle_one" both -> "beetleOne");
/// suffixing keeps every case distinct so the union can still generate as an enum.
local function uniqueEnumCaseName(candidate: String, taken: List<String>, n: Int): String =
let (name = if (n == 1) candidate else "\(candidate)\(n)")
if (taken.contains(name)) uniqueEnumCaseName(candidate, taken, n + 1) else name

local enumStringLiteralMembers: List<EnumMember> =
enumBaseMembers.fold(List(), (acc, it) ->
let (baseName = utils.normalizeEnumName((it.pklType as reflect.StringLiteralType).value))
acc.add((it) {
name = uniqueEnumCaseName(baseName, acc.map((m) -> m.name), 1)
})
)

local enumNormalMembers =
enumBaseMembers.map((it) -> (it) {
Expand Down
28 changes: 25 additions & 3 deletions codegen/src/internal/utils.pkl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//===----------------------------------------------------------------------===//
// Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
// Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -133,12 +133,34 @@ function renderHeaderComment(`module`: reflect.Module) =
function canBeNormalizedToEnumCaseName(name: String) =
name == "" || !name.split(Regex(#"(?u)[^\p{L}\d]"#)).isEmpty

local function isAllUpper(s: String) = s != "" && s == s.toUpperCase() && s != s.toLowerCase()

local function lowerFirst(s: String) = if (s == "") s else s[0].toLowerCase() + s.drop(1)

local digitRegex: Regex = Regex(#"\d"#)

function normalizeEnumName(name: String) =
if (name == "")
"empty"
else
let (normalized = normalizeName(name))
normalized[0].toLowerCase() + normalized.drop(1)
let (parts = name.split(Regex(#"(?u)[^\p{L}\d]"#)).filter((p) -> p != ""))
let (
camel =
if (parts.isEmpty)
"empty"
else
let (firstRaw = parts.first)
let (first = if (isAllUpper(firstRaw)) firstRaw.toLowerCase() else lowerFirst(firstRaw))
first
+ parts
.drop(1)
.map((p) -> if (isAllUpper(p)) p.toLowerCase().capitalize() else p.capitalize())
.join("")
)
if (camel[0].matches(digitRegex) || keywords.contains(camel))
"`\(camel)`"
else
camel

function normalizeName(name: String) =
if (keywords.contains(name))
Expand Down
15 changes: 14 additions & 1 deletion codegen/src/tests/utils.pkl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//===----------------------------------------------------------------------===//
// Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
// Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -27,6 +27,19 @@ facts {
utils.normalizeName("Swift111") == "Swift111"
utils.normalizeName("snake_case") == "snake_case"
}
["normalizeEnumName"] {
utils.normalizeEnumName("foo") == "foo"
utils.normalizeEnumName("FooBar") == "fooBar"
utils.normalizeEnumName("San Francisco") == "sanFrancisco"
utils.normalizeEnumName("bugBug") == "bugBug"
utils.normalizeEnumName("ANDROID") == "android"
utils.normalizeEnumName("TEST_RUNNER") == "testRunner"
utils.normalizeEnumName("HTTP_STATUS_CODE") == "httpStatusCode"
utils.normalizeEnumName("400") == "`400`"
utils.normalizeEnumName("404_not_found") == "`404NotFound`"
utils.normalizeEnumName("") == "empty"
utils.normalizeEnumName("___") == "empty"
}
["toSwiftString"] {
utils.toSwiftString("foo") == #""foo""#
utils.toSwiftString("你好") == #""你好""#
Expand Down
Loading