Skip to content

Commit ee222c3

Browse files
committed
[Constant Evaluator] Add support for string appends and equals to
the constant evaluator.
1 parent ba2157b commit ee222c3

File tree

4 files changed

+180
-20
lines changed

4 files changed

+180
-20
lines changed

lib/SILOptimizer/Utils/ConstExpr.cpp

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,25 @@ enum class WellKnownFunction {
4141
// String.init()
4242
StringInitEmpty,
4343
// String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
44-
StringMakeUTF8
44+
StringMakeUTF8,
45+
// static String.+= infix(_: inout String, _: String)
46+
StringAppend,
47+
// static String.== infix(_: String)
48+
StringEquals
4549
};
4650

4751
static llvm::Optional<WellKnownFunction> classifyFunction(SILFunction *fn) {
4852
if (fn->hasSemanticsAttr("string.init_empty"))
4953
return WellKnownFunction::StringInitEmpty;
54+
// There are two string initializers in the standard library with the
55+
// semantics "string.makeUTF8". They are identical from the perspective of
56+
// the interpreter. One of those functions is probably redundant and not used.
5057
if (fn->hasSemanticsAttr("string.makeUTF8"))
5158
return WellKnownFunction::StringMakeUTF8;
59+
if (fn->hasSemanticsAttr("string.append"))
60+
return WellKnownFunction::StringAppend;
61+
if (fn->hasSemanticsAttr("string.equals"))
62+
return WellKnownFunction::StringEquals;
5263
return None;
5364
}
5465

@@ -587,17 +598,76 @@ ConstExprFunctionState::computeWellKnownCallResult(ApplyInst *apply,
587598
conventions.getNumIndirectSILResults() == 0 &&
588599
conventions.getNumParameters() == 4 && "unexpected signature");
589600
auto literal = getConstantValue(apply->getOperand(1));
590-
if (literal.getKind() != SymbolicValue::String)
591-
break;
601+
if (literal.getKind() != SymbolicValue::String) {
602+
return evaluator.getUnknown((SILInstruction *)apply,
603+
UnknownReason::Default);
604+
}
592605
auto literalVal = literal.getStringValue();
593606

594607
auto byteCount = getConstantValue(apply->getOperand(2));
595608
if (byteCount.getKind() != SymbolicValue::Integer ||
596-
byteCount.getIntegerValue().getLimitedValue() != literalVal.size())
597-
break;
609+
byteCount.getIntegerValue().getLimitedValue() != literalVal.size()) {
610+
return evaluator.getUnknown((SILInstruction *)apply,
611+
UnknownReason::Default);
612+
}
598613
setValue(apply, literal);
599614
return None;
600615
}
616+
case WellKnownFunction::StringAppend: {
617+
// static String.+= infix(_: inout String, _: String)
618+
assert(conventions.getNumDirectSILResults() == 0 &&
619+
conventions.getNumIndirectSILResults() == 0 &&
620+
conventions.getNumParameters() == 3 &&
621+
"unexpected String.+=() signature");
622+
623+
auto firstOperand = apply->getOperand(1);
624+
auto firstString = getConstAddrAndLoadResult(firstOperand);
625+
if (firstString.getKind() != SymbolicValue::String) {
626+
return evaluator.getUnknown((SILInstruction *)apply,
627+
UnknownReason::Default);
628+
}
629+
630+
auto otherString = getConstantValue(apply->getOperand(2));
631+
if (otherString.getKind() != SymbolicValue::String) {
632+
return evaluator.getUnknown((SILInstruction *)apply,
633+
UnknownReason::Default);
634+
}
635+
636+
auto result = SmallString<8>(firstString.getStringValue());
637+
result.append(otherString.getStringValue());
638+
auto resultVal =
639+
SymbolicValue::getString(result, evaluator.getASTContext());
640+
computeFSStore(resultVal, firstOperand);
641+
return None;
642+
}
643+
case WellKnownFunction::StringEquals: {
644+
// static String.== infix(_: String, _: String)
645+
assert(conventions.getNumDirectSILResults() == 1 &&
646+
conventions.getNumIndirectSILResults() == 0 &&
647+
conventions.getNumParameters() == 3 &&
648+
"unexpected String.==() signature");
649+
650+
auto firstString = getConstantValue(apply->getOperand(1));
651+
if (firstString.getKind() != SymbolicValue::String) {
652+
return evaluator.getUnknown((SILInstruction *)apply,
653+
UnknownReason::Default);
654+
}
655+
656+
auto otherString = getConstantValue(apply->getOperand(2));
657+
if (otherString.getKind() != SymbolicValue::String) {
658+
return evaluator.getUnknown((SILInstruction *)apply,
659+
UnknownReason::Default);
660+
}
661+
662+
// The result is a Swift.Bool which is a struct that wraps an Int1.
663+
int isEqual = firstString.getStringValue() == otherString.getStringValue();
664+
auto intVal =
665+
SymbolicValue::getInteger(APInt(1, isEqual), evaluator.getASTContext());
666+
auto result = SymbolicValue::getAggregate(ArrayRef<SymbolicValue>(intVal),
667+
evaluator.getASTContext());
668+
setValue(apply, result);
669+
return None;
670+
}
601671
}
602672
llvm_unreachable("unhandled WellKnownFunction");
603673
}

stdlib/public/core/String.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,7 @@ extension String {
561561

562562
// String append
563563
@inlinable // Forward inlinability to append
564+
@_semantics("string.append")
564565
public static func += (lhs: inout String, rhs: String) {
565566
lhs.append(rhs)
566567
}

stdlib/public/core/StringComparable.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ extension StringProtocol {
6767
extension String : Equatable {
6868
@inlinable @inline(__always) // For the bitwise comparision
6969
@_effects(readonly)
70+
@_semantics("string.equals")
7071
public static func == (lhs: String, rhs: String) -> Bool {
7172
return _stringCompare(lhs._guts, rhs._guts, expecting: .equal)
7273
}

test/SILOptimizer/pound_assert.swift

Lines changed: 103 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -458,20 +458,15 @@ func testStructPassedAsProtocols() {
458458

459459
//===----------------------------------------------------------------------===//
460460
// Strings
461-
//
462-
// TODO: The constant evaluator does not implement string accesses/comparisons
463-
// so theses tests cannot test that the implemented string operations produce
464-
// correct values in the arrays. These tests only test that the implemented
465-
// string operations do not crash or produce unknown values. As soon as we have
466-
// string accesses/comparisons, modify these tests to check the values in the
467-
// strings.
468461
//===----------------------------------------------------------------------===//
469462

470463
struct ContainsString {
471464
let x: Int
472465
let str: String
473466
}
474467

468+
// Test string initialization
469+
475470
func stringInitEmptyTopLevel() {
476471
let c = ContainsString(x: 1, str: "")
477472
#assert(c.x == 1)
@@ -482,20 +477,113 @@ func stringInitNonEmptyTopLevel() {
482477
#assert(c.x == 1)
483478
}
484479

485-
func stringInitEmptyFlowSensitive() -> ContainsString {
486-
return ContainsString(x: 1, str: "")
480+
// Test string equality (==)
481+
482+
func emptyString() -> String {
483+
return ""
484+
}
485+
486+
func asciiString() -> String {
487+
return "test string"
488+
}
489+
490+
func dollarSign() -> String {
491+
return "dollar sign: \u{24}"
492+
}
493+
494+
func flag() -> String {
495+
return "flag: \u{1F1FA}\u{1F1F8}"
487496
}
488497

489-
func invokeStringInitEmptyFlowSensitive() {
490-
#assert(stringInitEmptyFlowSensitive().x == 1)
498+
func compareWithIdenticalStrings() {
499+
#assert(emptyString() == "")
500+
#assert(asciiString() == "test string")
501+
#assert(dollarSign() == "dollar sign: $")
502+
#assert(flag() == "flag: 🇺🇸")
503+
}
504+
505+
func compareWithUnequalStrings() {
506+
#assert(emptyString() == "Nonempty") // expected-error {{assertion failed}}
507+
#assert(asciiString() == "") // expected-error {{assertion failed}}
508+
#assert(dollarSign() == flag()) // expected-error {{assertion failed}}
509+
#assert(flag() == "flag: \u{1F496}") // expected-error {{assertion failed}}
510+
}
511+
512+
// Test string appends (+=)
513+
514+
// String.+= when used at the top-level of #assert cannot be folded as the
515+
// interpreter cannot extract the relevant instructions to interpret.
516+
// (This is because append is a mutating function and there will be more than
517+
// one writer to the string.) Nonetheless, flow-sensitive uses of String.+=
518+
// will be interpretable.
519+
func testStringAppendTopLevel() {
520+
var a = "a"
521+
a += "b"
522+
#assert(a == "ab") // expected-error {{#assert condition not constant}}
523+
// expected-note@-1 {{could not fold operation}}
524+
}
525+
526+
func appendedAsciiString() -> String {
527+
var str = "test "
528+
str += "string"
529+
return str
530+
}
531+
532+
func appendedDollarSign() -> String {
533+
var d = "dollar sign: "
534+
d += "\u{24}"
535+
return d
536+
}
537+
538+
func appendedFlag() -> String {
539+
var flag = "\u{1F1FA}"
540+
flag += "\u{1F1F8}"
541+
return flag
542+
}
543+
544+
func testStringAppend() {
545+
#assert(appendedAsciiString() == asciiString())
546+
#assert(appendedDollarSign() == dollarSign())
547+
#assert(appendedFlag() == "🇺🇸")
548+
549+
#assert(appendedAsciiString() == "") // expected-error {{assertion failed}}
550+
#assert(appendedDollarSign() == "") // expected-error {{assertion failed}}
551+
#assert(appendedFlag() == "") // expected-error {{assertion failed}}
552+
}
553+
554+
func conditionalAppend(_ b: Bool, _ str1: String, _ str2: String) -> String {
555+
let suffix = "One"
556+
var result = ""
557+
if b {
558+
result = str1
559+
result += suffix
560+
} else {
561+
result = str2
562+
result += suffix
563+
}
564+
return result
565+
}
566+
567+
func testConditionalAppend() {
568+
let first = "first"
569+
let second = "second"
570+
#assert(conditionalAppend(true, first, second) == "firstOne")
571+
#assert(conditionalAppend(false, first, second) == "secondOne")
572+
}
573+
574+
struct ContainsMutableString {
575+
let x: Int
576+
var str: String
491577
}
492578

493-
func stringInitNonEmptyFlowSensitive() -> ContainsString {
494-
return ContainsString(x: 1, str: "hello world")
579+
func appendOfStructProperty() -> ContainsMutableString {
580+
var c = ContainsMutableString(x: 0, str: "broken")
581+
c.str += " arrow"
582+
return c
495583
}
496584

497-
func invokeStringInitNonEmptyFlowSensitive() {
498-
#assert(stringInitNonEmptyFlowSensitive().x == 1)
585+
func testAppendOfStructProperty() {
586+
#assert(appendOfStructProperty().str == "broken arrow")
499587
}
500588

501589
//===----------------------------------------------------------------------===//

0 commit comments

Comments
 (0)