Skip to content

Commit 4f2a55b

Browse files
committed
[stdlib/private][OSLog] Support expressive, fprintf-style formatting
options for integers passed to os log string interpolation.
1 parent 1709b61 commit 4f2a55b

12 files changed

+505
-103
lines changed

lib/SILOptimizer/Utils/ConstExpr.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,9 @@ SymbolicValue ConstExprFunctionState::computeConstantValue(SILValue value) {
481481
evaluator.getAllocator());
482482
}
483483

484+
if (auto *convertEscapeInst = dyn_cast<ConvertEscapeToNoEscapeInst>(value))
485+
return getConstantValue(convertEscapeInst->getOperand());
486+
484487
LLVM_DEBUG(llvm::dbgs() << "ConstExpr Unknown simple: " << *value << "\n");
485488

486489
// Otherwise, we don't know how to handle this.

stdlib/private/OSLog/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ add_swift_target_library(swiftOSLogPrototype
44

55
OSLog.swift
66
OSLogMessage.swift
7+
OSLogIntegerFormatting.swift
8+
OSLogStringAlignment.swift
79
OSLogIntegerTypes.swift
810
OSLogStringTypes.swift
911
OSLogNSObjectType.swift
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
@frozen
2+
public struct OSLogIntegerFormatting: Equatable {
3+
/// The base to use for the string representation. `radix` must be at least 2
4+
/// and at most 36. The default is 10.
5+
public var radix: Int
6+
7+
/// When set, a `+` will be printed for all non-negative integers.
8+
public var explicitPositiveSign: Bool
9+
10+
/// When set, a prefix: 0b or 0o or 0x will be added when the radix is 2, 8 or
11+
/// 16 respectively.
12+
public var includePrefix: Bool
13+
14+
/// Whether to use uppercase letters to represent numerals
15+
/// greater than 9 (default is to use lowercase).
16+
public var uppercase: Bool
17+
18+
/// Minimum number of digits to display. Numbers having fewer digits than
19+
/// minDigits will be displayed with leading zeros.
20+
public var minDigits: Int
21+
22+
/// - Parameters:
23+
/// - radix: The base to use for the string representation. `radix` must be
24+
/// at least 2 and at most 36. The default is 10.
25+
/// - explicitPositiveSign: Pass `true` to add a + sign to non-negative
26+
/// numbers. Default is `false`.
27+
/// - includePrefix: Pass `true` to add a prefix: 0b, 0o, 0x to corresponding
28+
/// radices. Default is `false`.
29+
/// - uppercase: Pass `true` to use uppercase letters to represent numerals
30+
/// greater than 9, or `false` to use lowercase letters. The default is
31+
/// `false`.
32+
/// - minDigits: minimum number of digits to display. Numbers will be
33+
/// prefixed with zeros if necessary to meet the minimum. The default is 1.
34+
@_semantics("constant_evaluable")
35+
@inlinable
36+
@_optimize(none)
37+
public init(
38+
radix: Int = 10,
39+
explicitPositiveSign: Bool = false,
40+
includePrefix: Bool = false,
41+
uppercase: Bool = false,
42+
minDigits: Int = 1
43+
) {
44+
precondition(radix >= 2 && radix <= 36)
45+
46+
self.radix = radix
47+
self.explicitPositiveSign = explicitPositiveSign
48+
self.includePrefix = includePrefix
49+
self.uppercase = uppercase
50+
self.minDigits = minDigits
51+
}
52+
53+
/// - Parameters:
54+
/// - explicitPositiveSign: Pass `true` to add a + sign to non-negative
55+
/// numbers. Default is `false`.
56+
/// - minDigits: minimum number of digits to display. Numbers will be
57+
/// prefixed with zeros if necessary to meet the minimum. The default is 1.
58+
@_semantics("constant_evaluable")
59+
@inlinable
60+
@_optimize(none)
61+
public static func decimal(
62+
explicitPositiveSign: Bool = false,
63+
minDigits: Int = 1
64+
) -> OSLogIntegerFormatting {
65+
return OSLogIntegerFormatting(
66+
radix: 10,
67+
explicitPositiveSign: explicitPositiveSign,
68+
minDigits: minDigits)
69+
}
70+
71+
/// Default decimal format.
72+
@_semantics("constant_evaluable")
73+
@inlinable
74+
@_optimize(none)
75+
public static var decimal: OSLogIntegerFormatting { .decimal() }
76+
77+
/// - Parameters:
78+
/// - explicitPositiveSign: Pass `true` to add a + sign to non-negative
79+
/// numbers. Default is `false`.
80+
/// - includePrefix: Pass `true` to add a prefix: 0b, 0o, 0x to corresponding
81+
/// radices. Default is `false`.
82+
/// - uppercase: Pass `true` to use uppercase letters to represent numerals
83+
/// greater than 9, or `false` to use lowercase letters. The default is
84+
/// `false`.
85+
/// - minDigits: minimum number of digits to display. Numbers will be
86+
/// prefixed with zeros if necessary to meet the minimum. The default is 1.
87+
@_semantics("constant_evaluable")
88+
@inlinable
89+
@_optimize(none)
90+
public static func hex(
91+
explicitPositiveSign: Bool = false,
92+
includePrefix: Bool = false,
93+
uppercase: Bool = false,
94+
minDigits: Int = 1
95+
) -> OSLogIntegerFormatting {
96+
return OSLogIntegerFormatting(
97+
radix: 16,
98+
explicitPositiveSign: explicitPositiveSign,
99+
includePrefix: includePrefix,
100+
uppercase: uppercase,
101+
minDigits: minDigits)
102+
}
103+
104+
/// Default hexadecimal format.
105+
@_semantics("constant_evaluable")
106+
@inlinable
107+
@_optimize(none)
108+
public static var hex: OSLogIntegerFormatting { .hex() }
109+
110+
/// - Parameters:
111+
/// - explicitPositiveSign: Pass `true` to add a + sign to non-negative
112+
/// numbers. Default is `false`.
113+
/// - includePrefix: Pass `true` to add a prefix: 0b, 0o, 0x to corresponding
114+
/// radices. Default is `false`.
115+
/// - uppercase: Pass `true` to use uppercase letters to represent numerals
116+
/// greater than 9, or `false` to use lowercase letters. The default is
117+
/// `false`.
118+
/// - minDigits: minimum number of digits to display. Numbers will be
119+
/// prefixed with zeros if necessary to meet the minimum. The default is 1.
120+
@_semantics("constant_evaluable")
121+
@inlinable
122+
@_optimize(none)
123+
public static func octal(
124+
explicitPositiveSign: Bool = false,
125+
includePrefix: Bool = false,
126+
uppercase: Bool = false,
127+
minDigits: Int = 1
128+
) -> OSLogIntegerFormatting {
129+
OSLogIntegerFormatting(
130+
radix: 8,
131+
explicitPositiveSign: explicitPositiveSign,
132+
includePrefix: includePrefix,
133+
uppercase: uppercase,
134+
minDigits: minDigits)
135+
}
136+
137+
/// Default octal format.
138+
@_semantics("constant_evaluable")
139+
@inlinable
140+
@_optimize(none)
141+
public static var octal: OSLogIntegerFormatting { .octal() }
142+
}
143+
144+
extension OSLogIntegerFormatting {
145+
/// The prefix for the radix in the Swift literal syntax.
146+
@_semantics("constant_evaluable")
147+
@inlinable
148+
@_optimize(none)
149+
internal var _prefix: String {
150+
guard includePrefix else { return "" }
151+
switch radix {
152+
case 2: return "0b"
153+
case 8: return "0o"
154+
case 16: return "0x"
155+
default: return ""
156+
}
157+
}
158+
}
159+
160+
extension OSLogIntegerFormatting {
161+
162+
/// Returns a fprintf-compatible length modifier for a given argument type.
163+
@_semantics("constant_evaluable")
164+
@inlinable
165+
@_optimize(none)
166+
internal static func formatSpecifierLengthModifier<I: FixedWidthInteger>(
167+
_ type: I.Type
168+
) -> String? {
169+
// IEEE Std 1003.1-2017, length modifiers:
170+
switch type {
171+
// hh - d, i, o, u, x, or X conversion specifier applies to
172+
// (signed|unsigned) char
173+
case is CChar.Type: return "hh"
174+
case is CUnsignedChar.Type: return "hh"
175+
176+
// h - d, i, o, u, x, or X conversion specifier applies to
177+
// (signed|unsigned) short
178+
case is CShort.Type: return "h"
179+
case is CUnsignedShort.Type: return "h"
180+
181+
case is CInt.Type: return ""
182+
case is CUnsignedInt.Type: return ""
183+
184+
// l - d, i, o, u, x, or X conversion specifier applies to
185+
// (signed|unsigned) long
186+
case is CLong.Type: return "l"
187+
case is CUnsignedLong.Type: return "l"
188+
189+
// ll - d, i, o, u, x, or X conversion specifier applies to
190+
// (signed|unsigned) long long
191+
case is CLongLong.Type: return "ll"
192+
case is CUnsignedLongLong.Type: return "ll"
193+
194+
default: return nil
195+
}
196+
}
197+
198+
/// Constructs an os_log format specifier for the given type `type`
199+
/// using the specified alignment `align` and privacy qualifier `privacy`.
200+
@_semantics("constant_evaluable")
201+
@inlinable
202+
@_optimize(none)
203+
@_effects(readonly)
204+
internal func formatSpecifier<I: FixedWidthInteger>(
205+
for type: I.Type,
206+
align: OSLogStringAlignment,
207+
privacy: OSLogPrivacy
208+
) -> String {
209+
// Based on IEEE Std 1003.1-2017
210+
// `d`/`i` is the only signed integral conversions allowed
211+
if (type.isSigned && radix != 10) {
212+
fatalError("Signed integers must be formatted using .decimal")
213+
}
214+
215+
// IEEE: Each conversion specification is introduced by the '%' character
216+
// after which the following appear in sequence:
217+
// 1. Zero or more flags (in any order), which modify the meaning of the
218+
// conversion specification.
219+
// 2. An optional minimum field width (for alignment). If the converted
220+
// value has fewer bytes than the field width, it shall be padded with
221+
// <space> characters by default on the left; it shall be padded on the
222+
// right if the left-adjustment flag ( '-' ), is given to the
223+
// field width.
224+
// 3. An optional precision that gives the minimum number of digits to
225+
// display for the d, i, o, u, x, and X conversion specifiers.
226+
// 4. An optional length modifier that specifies the size of the argument.
227+
// 5. A conversion specifier character that indicates the type of
228+
// conversion to be applied.
229+
230+
// Use Swift style prefixes rather than fprintf style prefixes.
231+
var specification = _prefix
232+
specification += "%"
233+
234+
// Add privacy qualifier after % sign within curly braces. This is an
235+
// os log specific flag.
236+
switch privacy {
237+
case .private:
238+
specification += "{private}"
239+
case .public:
240+
specification += "{public}"
241+
default:
242+
break
243+
}
244+
245+
//
246+
// 1. Flags
247+
//
248+
// Use `+` flag if signed, otherwise prefix a literal `+` for unsigned.
249+
if explicitPositiveSign {
250+
// IEEE: `+` The result of a signed conversion shall always begin with a
251+
// sign ( '+' or '-' )
252+
if type.isSigned {
253+
specification += "+"
254+
} else {
255+
var newSpecification = "+"
256+
newSpecification += specification
257+
specification = newSpecification
258+
}
259+
}
260+
261+
// IEEE: `-` The result of the conversion shall be left-justified within
262+
// the field. The conversion is right-justified if this flag is not
263+
// specified.
264+
switch align.anchor {
265+
case OSLogCollectionBound.start:
266+
specification += "-"
267+
default:
268+
break
269+
}
270+
271+
// 2. Minimumn field width
272+
// IEEE: When field width is prefixed by `0`, leading zeros (following any
273+
// indication of sign or base) are used to pad to the field width rather
274+
// than performing space padding. If the '0' and '-' flags both appear,
275+
// the '0' flag is ignored. If a precision is specified, the '0' flag shall
276+
// be ignored.
277+
//
278+
// Commentary: `0` is prefixed to field width when the user doesn't want to
279+
// use precision (minDigits). This allows sign and prefix characters to be
280+
// counted towards field width (they wouldn't be counted towards precision).
281+
// This is more useful for floats, where precision is digits after the radix.
282+
// In our case, we're already handling prefix ourselves; we choose not to
283+
// support this functionality. In our case, alignment always pads spaces (
284+
// to the left or right) until the minimum field width is met.
285+
if align.minimumColumnWidth > 0 {
286+
specification += align.minimumColumnWidth.description
287+
}
288+
289+
// 3. Precision
290+
291+
// Default precision for integers is 1, otherwise use the requested precision.
292+
if minDigits != 1 {
293+
specification += "."
294+
specification += minDigits.description
295+
}
296+
297+
// 4. Length modifier
298+
guard let lengthModifier =
299+
OSLogIntegerFormatting.formatSpecifierLengthModifier(type) else {
300+
fatalError("Integer type has unknown byte length")
301+
}
302+
specification += lengthModifier
303+
304+
// 5. The conversion specifier
305+
switch radix {
306+
case 10:
307+
specification += type.isSigned ? "d" : "u"
308+
case 8:
309+
specification += "o"
310+
case 16:
311+
specification += uppercase ? "X" : "x"
312+
default:
313+
fatalError("radix must be 10, 8 or 16")
314+
}
315+
return specification
316+
}
317+
}

0 commit comments

Comments
 (0)