Skip to content

Commit

Permalink
[Constant Evaluator] Add support for string appends and equals to
Browse files Browse the repository at this point in the history
the constant evaluator.
  • Loading branch information
ravikandhadai committed Jan 26, 2019
1 parent ba2157b commit ee222c3
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 20 deletions.
80 changes: 75 additions & 5 deletions lib/SILOptimizer/Utils/ConstExpr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,25 @@ enum class WellKnownFunction {
// String.init()
StringInitEmpty,
// String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
StringMakeUTF8
StringMakeUTF8,
// static String.+= infix(_: inout String, _: String)
StringAppend,
// static String.== infix(_: String)
StringEquals
};

static llvm::Optional<WellKnownFunction> classifyFunction(SILFunction *fn) {
if (fn->hasSemanticsAttr("string.init_empty"))
return WellKnownFunction::StringInitEmpty;
// There are two string initializers in the standard library with the
// semantics "string.makeUTF8". They are identical from the perspective of
// the interpreter. One of those functions is probably redundant and not used.
if (fn->hasSemanticsAttr("string.makeUTF8"))
return WellKnownFunction::StringMakeUTF8;
if (fn->hasSemanticsAttr("string.append"))
return WellKnownFunction::StringAppend;
if (fn->hasSemanticsAttr("string.equals"))
return WellKnownFunction::StringEquals;
return None;
}

Expand Down Expand Up @@ -587,17 +598,76 @@ ConstExprFunctionState::computeWellKnownCallResult(ApplyInst *apply,
conventions.getNumIndirectSILResults() == 0 &&
conventions.getNumParameters() == 4 && "unexpected signature");
auto literal = getConstantValue(apply->getOperand(1));
if (literal.getKind() != SymbolicValue::String)
break;
if (literal.getKind() != SymbolicValue::String) {
return evaluator.getUnknown((SILInstruction *)apply,
UnknownReason::Default);
}
auto literalVal = literal.getStringValue();

auto byteCount = getConstantValue(apply->getOperand(2));
if (byteCount.getKind() != SymbolicValue::Integer ||
byteCount.getIntegerValue().getLimitedValue() != literalVal.size())
break;
byteCount.getIntegerValue().getLimitedValue() != literalVal.size()) {
return evaluator.getUnknown((SILInstruction *)apply,
UnknownReason::Default);
}
setValue(apply, literal);
return None;
}
case WellKnownFunction::StringAppend: {
// static String.+= infix(_: inout String, _: String)
assert(conventions.getNumDirectSILResults() == 0 &&
conventions.getNumIndirectSILResults() == 0 &&
conventions.getNumParameters() == 3 &&
"unexpected String.+=() signature");

auto firstOperand = apply->getOperand(1);
auto firstString = getConstAddrAndLoadResult(firstOperand);
if (firstString.getKind() != SymbolicValue::String) {
return evaluator.getUnknown((SILInstruction *)apply,
UnknownReason::Default);
}

auto otherString = getConstantValue(apply->getOperand(2));
if (otherString.getKind() != SymbolicValue::String) {
return evaluator.getUnknown((SILInstruction *)apply,
UnknownReason::Default);
}

auto result = SmallString<8>(firstString.getStringValue());
result.append(otherString.getStringValue());
auto resultVal =
SymbolicValue::getString(result, evaluator.getASTContext());
computeFSStore(resultVal, firstOperand);
return None;
}
case WellKnownFunction::StringEquals: {
// static String.== infix(_: String, _: String)
assert(conventions.getNumDirectSILResults() == 1 &&
conventions.getNumIndirectSILResults() == 0 &&
conventions.getNumParameters() == 3 &&
"unexpected String.==() signature");

auto firstString = getConstantValue(apply->getOperand(1));
if (firstString.getKind() != SymbolicValue::String) {
return evaluator.getUnknown((SILInstruction *)apply,
UnknownReason::Default);
}

auto otherString = getConstantValue(apply->getOperand(2));
if (otherString.getKind() != SymbolicValue::String) {
return evaluator.getUnknown((SILInstruction *)apply,
UnknownReason::Default);
}

// The result is a Swift.Bool which is a struct that wraps an Int1.
int isEqual = firstString.getStringValue() == otherString.getStringValue();
auto intVal =
SymbolicValue::getInteger(APInt(1, isEqual), evaluator.getASTContext());
auto result = SymbolicValue::getAggregate(ArrayRef<SymbolicValue>(intVal),
evaluator.getASTContext());
setValue(apply, result);
return None;
}
}
llvm_unreachable("unhandled WellKnownFunction");
}
Expand Down
1 change: 1 addition & 0 deletions stdlib/public/core/String.swift
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ extension String {

// String append
@inlinable // Forward inlinability to append
@_semantics("string.append")
public static func += (lhs: inout String, rhs: String) {
lhs.append(rhs)
}
Expand Down
1 change: 1 addition & 0 deletions stdlib/public/core/StringComparable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ extension StringProtocol {
extension String : Equatable {
@inlinable @inline(__always) // For the bitwise comparision
@_effects(readonly)
@_semantics("string.equals")
public static func == (lhs: String, rhs: String) -> Bool {
return _stringCompare(lhs._guts, rhs._guts, expecting: .equal)
}
Expand Down
118 changes: 103 additions & 15 deletions test/SILOptimizer/pound_assert.swift
Original file line number Diff line number Diff line change
Expand Up @@ -458,20 +458,15 @@ func testStructPassedAsProtocols() {

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

struct ContainsString {
let x: Int
let str: String
}

// Test string initialization

func stringInitEmptyTopLevel() {
let c = ContainsString(x: 1, str: "")
#assert(c.x == 1)
Expand All @@ -482,20 +477,113 @@ func stringInitNonEmptyTopLevel() {
#assert(c.x == 1)
}

func stringInitEmptyFlowSensitive() -> ContainsString {
return ContainsString(x: 1, str: "")
// Test string equality (==)

func emptyString() -> String {
return ""
}

func asciiString() -> String {
return "test string"
}

func dollarSign() -> String {
return "dollar sign: \u{24}"
}

func flag() -> String {
return "flag: \u{1F1FA}\u{1F1F8}"
}

func invokeStringInitEmptyFlowSensitive() {
#assert(stringInitEmptyFlowSensitive().x == 1)
func compareWithIdenticalStrings() {
#assert(emptyString() == "")
#assert(asciiString() == "test string")
#assert(dollarSign() == "dollar sign: $")
#assert(flag() == "flag: 🇺🇸")
}

func compareWithUnequalStrings() {
#assert(emptyString() == "Nonempty") // expected-error {{assertion failed}}
#assert(asciiString() == "") // expected-error {{assertion failed}}
#assert(dollarSign() == flag()) // expected-error {{assertion failed}}
#assert(flag() == "flag: \u{1F496}") // expected-error {{assertion failed}}
}

// Test string appends (+=)

// String.+= when used at the top-level of #assert cannot be folded as the
// interpreter cannot extract the relevant instructions to interpret.
// (This is because append is a mutating function and there will be more than
// one writer to the string.) Nonetheless, flow-sensitive uses of String.+=
// will be interpretable.
func testStringAppendTopLevel() {
var a = "a"
a += "b"
#assert(a == "ab") // expected-error {{#assert condition not constant}}
// expected-note@-1 {{could not fold operation}}
}

func appendedAsciiString() -> String {
var str = "test "
str += "string"
return str
}

func appendedDollarSign() -> String {
var d = "dollar sign: "
d += "\u{24}"
return d
}

func appendedFlag() -> String {
var flag = "\u{1F1FA}"
flag += "\u{1F1F8}"
return flag
}

func testStringAppend() {
#assert(appendedAsciiString() == asciiString())
#assert(appendedDollarSign() == dollarSign())
#assert(appendedFlag() == "🇺🇸")

#assert(appendedAsciiString() == "") // expected-error {{assertion failed}}
#assert(appendedDollarSign() == "") // expected-error {{assertion failed}}
#assert(appendedFlag() == "") // expected-error {{assertion failed}}
}

func conditionalAppend(_ b: Bool, _ str1: String, _ str2: String) -> String {
let suffix = "One"
var result = ""
if b {
result = str1
result += suffix
} else {
result = str2
result += suffix
}
return result
}

func testConditionalAppend() {
let first = "first"
let second = "second"
#assert(conditionalAppend(true, first, second) == "firstOne")
#assert(conditionalAppend(false, first, second) == "secondOne")
}

struct ContainsMutableString {
let x: Int
var str: String
}

func stringInitNonEmptyFlowSensitive() -> ContainsString {
return ContainsString(x: 1, str: "hello world")
func appendOfStructProperty() -> ContainsMutableString {
var c = ContainsMutableString(x: 0, str: "broken")
c.str += " arrow"
return c
}

func invokeStringInitNonEmptyFlowSensitive() {
#assert(stringInitNonEmptyFlowSensitive().x == 1)
func testAppendOfStructProperty() {
#assert(appendOfStructProperty().str == "broken arrow")
}

//===----------------------------------------------------------------------===//
Expand Down

0 comments on commit ee222c3

Please sign in to comment.