diff --git a/include/swift/AST/DiagnosticsSema.def b/include/swift/AST/DiagnosticsSema.def index 073c59afa0b76..1023ac0a82559 100644 --- a/include/swift/AST/DiagnosticsSema.def +++ b/include/swift/AST/DiagnosticsSema.def @@ -5031,6 +5031,29 @@ ERROR(differentiable_programming_attr_used_without_required_module, none, "'@%0' attribute used without importing module %1", (StringRef, Identifier)) +ERROR(oslog_arg_must_be_bool_literal, none, + "argument must be a bool literal", ()) +ERROR(oslog_arg_must_be_integer_literal, none, + "argument must be an integer literal", ()) +ERROR(oslog_arg_must_be_string_literal, none, + "argument must be a string literal", ()) +ERROR(oslog_arg_must_be_float_literal, none, + "argument must be a floating-point literal", ()) +ERROR(oslog_arg_must_be_metatype_literal, none, + "argument must be a .self", ()) +ERROR(oslog_arg_must_be_closure, none, "argument must be a closure", ()) +ERROR(argument_must_be_constant, none, + "argument must be an expression with only literals", ()) +ERROR(oslog_message_must_be_string_interpolation, none, + "argument must be a string interpolation", ()) +ERROR(oslog_arg_must_be_enum_case, none, + "argument must be a case of enum %0", (Identifier)) +ERROR(oslog_arg_must_be_type_member_access, none, + "argument must be a static method or property of %0", (Identifier)) +ERROR(atomics_ordering_must_be_constant, none, + "ordering argument must be a static method or property of %0", + (Identifier)) + #ifndef DIAG_NO_UNDEF # if defined(DIAG) # undef DIAG diff --git a/include/swift/AST/KnownIdentifiers.def b/include/swift/AST/KnownIdentifiers.def index 58570dbc5d832..a255e03e6d855 100644 --- a/include/swift/AST/KnownIdentifiers.def +++ b/include/swift/AST/KnownIdentifiers.def @@ -203,6 +203,11 @@ IDENTIFIER_(nsError) // Custom string interpolation type used by os log APIs. IDENTIFIER(OSLogMessage) +// Atomics ordering type identifiers. +IDENTIFIER(AtomicLoadOrdering) +IDENTIFIER(AtomicStoreOrdering) +IDENTIFIER(AtomicUpdateOrdering) + // Differentiable programming IDENTIFIER(along) IDENTIFIER(differential) diff --git a/include/swift/AST/SemanticAttrs.def b/include/swift/AST/SemanticAttrs.def index 28be95549153b..49b3d55fe8c7c 100644 --- a/include/swift/AST/SemanticAttrs.def +++ b/include/swift/AST/SemanticAttrs.def @@ -68,9 +68,10 @@ SEMANTICS_ATTR(OPTIMIZE_SIL_SPECIALIZE_GENERIC_PARTIAL_NEVER, SEMANTICS_ATTR(OPTIMIZE_SIL_SPECIALIZE_GENERIC_SIZE_NEVER, "optimize.sil.specialize.generic.size.never") -SEMANTICS_ATTR(OSLOG_INTERPOLATION_INIT, "oslog.interpolation.init") SEMANTICS_ATTR(OSLOG_MESSAGE_INIT_INTERPOLATION, "oslog.message.init_interpolation") SEMANTICS_ATTR(OSLOG_MESSAGE_INIT_STRING_LITERAL, "oslog.message.init_stringliteral") +SEMANTICS_ATTR(OSLOG_REQUIRES_CONSTANT_ARGUMENTS, "oslog.requires_constant_arguments") +SEMANTICS_ATTR(ATOMICS_REQUIRES_CONSTANT_ORDERINGS, "atomics.requires_constant_orderings") SEMANTICS_ATTR(TYPE_CHECKER_OPEN_EXISTENTIAL, "typechecker._openExistential(_:do:)") SEMANTICS_ATTR(TYPE_CHECKER_TYPE, "typechecker.type(of:)") diff --git a/lib/Sema/CMakeLists.txt b/lib/Sema/CMakeLists.txt index feb7ee9d14c5a..3d83749fde4b6 100644 --- a/lib/Sema/CMakeLists.txt +++ b/lib/Sema/CMakeLists.txt @@ -10,6 +10,7 @@ add_swift_host_library(swiftSema STATIC CSFix.cpp CSDiagnostics.cpp CodeSynthesis.cpp + ConstantnessSemaDiagnostics.cpp Constraint.cpp ConstraintGraph.cpp ConstraintLocator.cpp diff --git a/lib/Sema/ConstantnessSemaDiagnostics.cpp b/lib/Sema/ConstantnessSemaDiagnostics.cpp new file mode 100644 index 0000000000000..1f15ba86cc8e2 --- /dev/null +++ b/lib/Sema/ConstantnessSemaDiagnostics.cpp @@ -0,0 +1,354 @@ +//===------------------------ ConstantnessSemaDiagnostics.cpp -------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +// +// This file implements checks for checking whether certain arguments to some +// specific APIs are compile-time constants (see below for the definition of +// constants). In particular, this code checks whether the new os_log APIs are +// invoked with constant arguments, and whether the primitive atomic operations +// are invoked with constant "orderings". These APIs are identified through +// @_semantics attributes. +// +// A "compile-time constant" is either a literal (including +// string/integer/float/boolean/string-interpolation literal) or a call to a +// "constant_evaluable" function (or property) with compile-time constant +// arguments. A closure expression is also considered a compile-time constant +// (it is a constant of a function type). +//===----------------------------------------------------------------------===// + +#include "MiscDiagnostics.h" +#include "TypeChecker.h" +#include "swift/AST/ASTContext.h" +#include "swift/AST/ASTWalker.h" +#include "swift/AST/ParameterList.h" +#include "swift/AST/SemanticAttrs.h" +using namespace swift; + +/// Check whether a given \p decl has a @_semantics attribute with the given +/// attribute name \c attrName. +static bool hasSemanticsAttr(ValueDecl *decl, StringRef attrName) { + for (auto semantics : decl->getAttrs().getAttributes()) { + if (semantics->Value.equals(attrName)) + return true; + } + return false; +} + +/// Return true iff the given \p structDecl has a name that matches one of the +/// known atomic orderings structs. +static bool isAtomicOrderingDecl(StructDecl *structDecl) { + ASTContext &astContext = structDecl->getASTContext(); + Identifier structName = structDecl->getName(); + return (structName == astContext.Id_AtomicLoadOrdering || + structName == astContext.Id_AtomicStoreOrdering || + structName == astContext.Id_AtomicUpdateOrdering); +} + +/// Return true iff the parameter \p param of function \c funDecl is required to +/// be a constant. This is true if either the function is an os_log function or +/// it is an atomics operation and the parameter represents the ordering. +static bool isParamRequiredToBeConstant(FuncDecl *funcDecl, ParamDecl *param) { + assert(funcDecl && param && "funcDecl and param must not be null"); + if (hasSemanticsAttr(funcDecl, semantics::OSLOG_REQUIRES_CONSTANT_ARGUMENTS)) + return true; + if (!hasSemanticsAttr(funcDecl, + semantics::ATOMICS_REQUIRES_CONSTANT_ORDERINGS)) + return false; + Type paramType = param->getType(); + StructDecl *structDecl = paramType->getStructOrBoundGenericStruct(); + if (!structDecl) + return false; + return isAtomicOrderingDecl(structDecl); +} + +/// Return true iff the \c decl is annotated as +/// @_semantics("constant_evaluable"). +static bool hasConstantEvaluableAttr(ValueDecl *decl) { + return hasSemanticsAttr(decl, semantics::CONSTANT_EVALUABLE); +} + +/// Check whether \p expr is a compile-time constant. It must either be a +/// literal_expr, which does not include array and dictionary literal, or a +/// closure expression, which is considered a compile-time constant of a +/// function type, or a call to a "constant_evaluable" function (or property) +/// whose arguments are themselves compile-time constants. +static Expr *checkConstantness(Expr *expr) { + SmallVector expressionsToCheck; + expressionsToCheck.push_back(expr); + while (!expressionsToCheck.empty()) { + Expr *expr = expressionsToCheck.pop_back_val(); + // Lookthrough identity_expr, tuple and inject_into_optional expressions. + if (IdentityExpr *identityExpr = dyn_cast(expr)) { + expressionsToCheck.push_back(identityExpr->getSubExpr()); + continue; + } + if (TupleExpr *tupleExpr = dyn_cast(expr)) { + for (Expr *element : tupleExpr->getElements()) + expressionsToCheck.push_back(element); + continue; + } + if (InjectIntoOptionalExpr *optionalExpr = + dyn_cast(expr)) { + expressionsToCheck.push_back(optionalExpr->getSubExpr()); + continue; + } + // Literal expressions also includes InterpolatedStringLiteralExpr. + if (isa(expr)) + continue; + if (isa(expr)) + continue; + // Closure expressions are always treated as constants. They are + // constants of function types. + if (isa(expr)) + continue; + // Default argument expressions of a constant_evaluable or a + // requires_constant function must be ensured to be a constant by the + // definition of the function. + if (isa(expr)) + continue; + + // If this is a member-ref, it has to be annotated constant evaluable. + if (MemberRefExpr *memberRef = dyn_cast(expr)) { + if (ValueDecl *memberDecl = memberRef->getMember().getDecl()) { + if (hasConstantEvaluableAttr(memberDecl)) + continue; + } + return expr; + } + + // If this is a variable, it has to be a known constant parameter of the + // enclosing function. + if (DeclRefExpr *declRef = dyn_cast(expr)) { + ValueDecl *decl = declRef->getDecl(); + if (!decl) + return expr; + ParamDecl *paramDecl = dyn_cast(decl); + if (!paramDecl) + return expr; + Decl *declContext = paramDecl->getDeclContext()->getAsDecl(); + if (!declContext) + return expr; + FuncDecl *funcDecl = dyn_cast(declContext); + if (!funcDecl || !isParamRequiredToBeConstant(funcDecl, paramDecl)) + return expr; + continue; + } + + if (!isa(expr)) + return expr; + + ApplyExpr *apply = cast(expr); + ValueDecl *calledValue = apply->getCalledValue(); + if (!calledValue) + return expr; + + // If this is an enum case, check whether the arguments are constants. + if (isa(calledValue)) { + expressionsToCheck.push_back(apply->getArg()); + continue; + } + + // If this is a constant_evaluable function, check whether the arguments are + // constants. + AbstractFunctionDecl *callee = dyn_cast(calledValue); + if (!callee || !hasConstantEvaluableAttr(callee)) + return expr; + expressionsToCheck.push_back(apply->getArg()); + } + return nullptr; +} + +/// Return true iff the norminal type decl \c numberDecl is a known stdlib +/// integer decl. +static bool isStdlibInteger(NominalTypeDecl *numberDecl) { + ASTContext &astCtx = numberDecl->getASTContext(); + return (numberDecl == astCtx.getIntDecl() || + numberDecl == astCtx.getInt8Decl() || + numberDecl == astCtx.getInt16Decl() || + numberDecl == astCtx.getInt32Decl() || + numberDecl == astCtx.getInt64Decl() || + numberDecl == astCtx.getUIntDecl() || + numberDecl == astCtx.getUInt8Decl() || + numberDecl == astCtx.getUInt16Decl() || + numberDecl == astCtx.getUInt32Decl() || + numberDecl == astCtx.getUInt64Decl()); +} + +/// Return true iff the given \p type is a Stdlib integer type. +static bool isIntegerType(Type type) { + NominalTypeDecl *nominalDecl = type->getNominalOrBoundGenericNominal(); + return nominalDecl && isStdlibInteger(nominalDecl); +} + +/// Return true iff the norminal type decl \c numberDecl is a known stdlib float +/// decl. +static bool isStdlibFloat(NominalTypeDecl *numberDecl) { + ASTContext &astCtx = numberDecl->getASTContext(); + return (numberDecl == astCtx.getFloatDecl() || + numberDecl == astCtx.getFloat80Decl() || + numberDecl == astCtx.getDoubleDecl()); +} + +/// Return true iff the given \p type is a Bool type. +static bool isFloatType(Type type) { + NominalTypeDecl *nominalDecl = type->getNominalOrBoundGenericNominal(); + return nominalDecl && isStdlibFloat(nominalDecl); +} + +/// Return true iff the given \p type is a String type. +static bool isStringType(Type type) { + NominalTypeDecl *nominalDecl = type->getNominalOrBoundGenericNominal(); + return nominalDecl && nominalDecl == type->getASTContext().getStringDecl(); +} + +/// Given an error expression \p errorExpr, diagnose the error based on the type +/// of the expression. For instance, if the expression's type is expressible by +/// a literal e.g. integer, boolean etc. report that it must be a literal. +/// Otherwise, if the expression is a nominal type, report that it must be +/// static member of the type. +static void diagnoseError(Expr *errorExpr, const ASTContext &astContext, + FuncDecl *funcDecl) { + DiagnosticEngine &diags = astContext.Diags; + Type exprType = errorExpr->getType(); + SourceLoc errorLoc = errorExpr->getLoc(); + + // Diagnose atomics ordering related error here. + if (hasSemanticsAttr(funcDecl, + semantics::ATOMICS_REQUIRES_CONSTANT_ORDERINGS)) { + NominalTypeDecl *nominalDecl = exprType->getNominalOrBoundGenericNominal(); + if (!nominalDecl) { + // This case should normally not happen. This is a safe guard against + // possible mismatch between the atomics library and the compiler. + diags.diagnose(errorLoc, diag::argument_must_be_constant); + } + diags.diagnose(errorLoc, diag::atomics_ordering_must_be_constant, + nominalDecl->getName()); + return; + } + + // Diagnose os_log specific errors here. + + // Diagnose primitive stdlib types. + if (exprType->isBool()) { + diags.diagnose(errorLoc, diag::oslog_arg_must_be_bool_literal); + return; + } + if (isStringType(exprType)) { + diags.diagnose(errorLoc, diag::oslog_arg_must_be_string_literal); + return; + } + if (isIntegerType(exprType)) { + diags.diagnose(errorLoc, diag::oslog_arg_must_be_integer_literal); + return; + } + if (isFloatType(exprType)) { + diags.diagnose(errorLoc, diag::oslog_arg_must_be_float_literal); + return; + } + if (exprType->is()) { + diags.diagnose(errorLoc, diag::oslog_arg_must_be_metatype_literal); + return; + } + if (exprType->is()) { + diags.diagnose(errorLoc, diag::oslog_arg_must_be_closure); + return; + } + if (EnumDecl *enumDecl = exprType->getEnumOrBoundGenericEnum()) { + diags.diagnose(errorLoc, diag::oslog_arg_must_be_enum_case, + enumDecl->getName()); + return; + } + NominalTypeDecl *nominalDecl = exprType->getNominalOrBoundGenericNominal(); + if (!nominalDecl) { + // This case should normally not happen. This is a safe guard against + // possible mismatch between the os overlay and the compiler. + diags.diagnose(errorLoc, diag::argument_must_be_constant); + return; + } + // If this is OSLogMessage, it should be a string-interpolation literal. + Identifier declName = nominalDecl->getName(); + if (declName == astContext.Id_OSLogMessage) { + diags.diagnose(errorLoc, diag::oslog_message_must_be_string_interpolation); + return; + } + diags.diagnose(errorLoc, diag::oslog_arg_must_be_type_member_access, + declName); +} + +/// Given a call \c callExpr, if some or all of its arguments are required to be +/// constants, check that property on the arguments. +static void diagnoseConstantArgumentRequirementOfCall(const CallExpr *callExpr, + const ASTContext &ctx) { + assert(callExpr && callExpr->getType() && + "callExpr should have a valid type"); + ValueDecl *calledDecl = callExpr->getCalledValue(); + if (!calledDecl || !isa(calledDecl)) + return; + FuncDecl *callee = cast(calledDecl); + + // Collect argument indices that are required to be constants. + SmallVector constantArgumentIndices; + auto paramList = callee->getParameters(); + for (unsigned i = 0; i < paramList->size(); i++) { + ParamDecl *param = paramList->get(i); + if (isParamRequiredToBeConstant(callee, param)) + constantArgumentIndices.push_back(i); + } + if (constantArgumentIndices.empty()) + return; + + // Check that the arguments at the constantArgumentIndices are constants. + Expr *argumentExpr = callExpr->getArg(); + SmallVector arguments; + if (TupleExpr *tupleExpr = dyn_cast(argumentExpr)) { + auto elements = tupleExpr->getElements(); + arguments.append(elements.begin(), elements.end()); + } else if (ParenExpr *parenExpr = dyn_cast(argumentExpr)) { + arguments.push_back(parenExpr->getSubExpr()); + } else { + arguments.push_back(argumentExpr); + } + + for (unsigned constantIndex : constantArgumentIndices) { + assert(constantIndex < arguments.size() && + "constantIndex exceeds the number of arguments to the function"); + Expr *argument = arguments[constantIndex]; + Expr *errorExpr = checkConstantness(argument); + if (errorExpr) + diagnoseError(errorExpr, ctx, callee); + } +} + +void swift::diagnoseConstantArgumentRequirement( + const Expr *expr, const DeclContext *declContext) { + class ConstantReqCallWalker : public ASTWalker { + const ASTContext &astContext; + + public: + ConstantReqCallWalker(ASTContext &ctx) : astContext(ctx) {} + + // Descend until we find a call expressions. Note that the input expression + // could be an assign expression or another expression that contains the + // call. + std::pair walkToExprPre(Expr *expr) override { + if (!expr || isa(expr) || !expr->getType()) + return {false, expr}; + if (auto *callExpr = dyn_cast(expr)) { + diagnoseConstantArgumentRequirementOfCall(callExpr, astContext); + return {false, expr}; + } + return {true, expr}; + } + }; + + ConstantReqCallWalker walker(declContext->getASTContext()); + const_cast(expr)->walk(walker); +} diff --git a/lib/Sema/MiscDiagnostics.cpp b/lib/Sema/MiscDiagnostics.cpp index 74a4253363ba3..079cf4b2fc9ae 100644 --- a/lib/Sema/MiscDiagnostics.cpp +++ b/lib/Sema/MiscDiagnostics.cpp @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -4331,6 +4331,7 @@ void swift::performSyntacticExprDiagnostics(const Expr *E, diagAvailability(E, const_cast(DC)); if (ctx.LangOpts.EnableObjCInterop) diagDeprecatedObjCSelectors(DC, E); + diagnoseConstantArgumentRequirement(E, DC); } void swift::performStmtDiagnostics(ASTContext &ctx, const Stmt *S) { diff --git a/lib/Sema/MiscDiagnostics.h b/lib/Sema/MiscDiagnostics.h index 8618ebe6cfa7f..7c385c52ae913 100644 --- a/lib/Sema/MiscDiagnostics.h +++ b/lib/Sema/MiscDiagnostics.h @@ -82,6 +82,12 @@ void diagnoseUnownedImmediateDeallocation(ASTContext &ctx, SourceLoc equalLoc, const Expr *initializer); +/// If \p expr is a call to a known function with a requirement that some +/// arguments must be constants, whether those arguments are passed only +/// constants. Otherwise, diagnose and emit errors. +void diagnoseConstantArgumentRequirement(const Expr *expr, + const DeclContext *declContext); + /// Attempt to fix the type of \p decl so that it's a valid override for /// \p base...but only if we're highly confident that we know what the user /// should have written. diff --git a/stdlib/private/OSLog/OSLogFloatingPointTypes.swift b/stdlib/private/OSLog/OSLogFloatingPointTypes.swift index 4ab9a97fb02a0..b7616b98215b8 100644 --- a/stdlib/private/OSLog/OSLogFloatingPointTypes.swift +++ b/stdlib/private/OSLog/OSLogFloatingPointTypes.swift @@ -29,6 +29,7 @@ extension OSLogInterpolation { /// - privacy: a privacy qualifier which is either private or public. /// It is auto-inferred by default. @_semantics("constant_evaluable") + @_semantics("oslog.requires_constant_arguments") @inlinable @_optimize(none) public mutating func appendInterpolation( @@ -44,6 +45,7 @@ extension OSLogInterpolation { /// - privacy: a privacy qualifier which is either private or public. /// It is auto-inferred by default. @_semantics("constant_evaluable") + @_semantics("oslog.requires_constant_arguments") @inlinable @_optimize(none) public mutating func appendInterpolation( diff --git a/stdlib/private/OSLog/OSLogIntegerTypes.swift b/stdlib/private/OSLog/OSLogIntegerTypes.swift index afc0fb3184d9c..248e90e90c57a 100644 --- a/stdlib/private/OSLog/OSLogIntegerTypes.swift +++ b/stdlib/private/OSLog/OSLogIntegerTypes.swift @@ -34,6 +34,7 @@ extension OSLogInterpolation { /// - privacy: a privacy qualifier which is either private or public. /// It is auto-inferred by default. @_semantics("constant_evaluable") + @_semantics("oslog.requires_constant_arguments") @inlinable @_optimize(none) public mutating func appendInterpolation( @@ -76,6 +77,7 @@ extension OSLogInterpolation { /// - privacy: a privacy qualifier which is either private or public. /// It is auto-inferred by default. @_semantics("constant_evaluable") + @_semantics("oslog.requires_constant_arguments") @inlinable @_optimize(none) public mutating func appendInterpolation( diff --git a/stdlib/private/OSLog/OSLogMessage.swift b/stdlib/private/OSLog/OSLogMessage.swift index cef756f0f9e3c..e49b2a1aa46be 100644 --- a/stdlib/private/OSLog/OSLogMessage.swift +++ b/stdlib/private/OSLog/OSLogMessage.swift @@ -175,7 +175,6 @@ public struct OSLogInterpolation : StringInterpolationProtocol { // constant evaluation and folding. Note that these methods will be inlined, // constant evaluated/folded and optimized in the context of a caller. - @_semantics("oslog.interpolation.init") @_semantics("constant_evaluable") @inlinable @_optimize(none) diff --git a/stdlib/private/OSLog/OSLogNSObjectType.swift b/stdlib/private/OSLog/OSLogNSObjectType.swift index 1feba5f5cfa66..3942999a9c682 100644 --- a/stdlib/private/OSLog/OSLogNSObjectType.swift +++ b/stdlib/private/OSLog/OSLogNSObjectType.swift @@ -30,6 +30,7 @@ extension OSLogInterpolation { /// - privacy: a privacy qualifier which is either private or public. /// It is auto-inferred by default. @_semantics("constant_evaluable") + @_semantics("oslog.requires_constant_arguments") @inlinable @_optimize(none) public mutating func appendInterpolation( diff --git a/stdlib/private/OSLog/OSLogStringTypes.swift b/stdlib/private/OSLog/OSLogStringTypes.swift index 1676e5ed222da..8a90b03c40598 100644 --- a/stdlib/private/OSLog/OSLogStringTypes.swift +++ b/stdlib/private/OSLog/OSLogStringTypes.swift @@ -33,6 +33,7 @@ extension OSLogInterpolation { /// - privacy: a privacy qualifier which is either private or public. /// It is auto-inferred by default. @_semantics("constant_evaluable") + @_semantics("oslog.requires_constant_arguments") @inlinable @_optimize(none) public mutating func appendInterpolation( diff --git a/stdlib/private/OSLog/OSLogTestHelper.swift b/stdlib/private/OSLog/OSLogTestHelper.swift index 6b7d134c8115e..85dde5ca81055 100644 --- a/stdlib/private/OSLog/OSLogTestHelper.swift +++ b/stdlib/private/OSLog/OSLogTestHelper.swift @@ -37,6 +37,7 @@ public let _noopClosure = { (x : String, y : UnsafeBufferPointer) in retu /// - message: An instance of `OSLogMessage` created from string interpolation /// - assertion: A closure that takes a format string and a pointer to a /// byte buffer and asserts a condition. +@_semantics("oslog.requires_constant_arguments") @_transparent @_optimize(none) public // @testable diff --git a/test/SILOptimizer/OSLogCompilerDiagnosticsTest.swift b/test/SILOptimizer/OSLogCompilerDiagnosticsTest.swift index 00936f957f80e..e5d43084abb33 100644 --- a/test/SILOptimizer/OSLogCompilerDiagnosticsTest.swift +++ b/test/SILOptimizer/OSLogCompilerDiagnosticsTest.swift @@ -3,62 +3,37 @@ // REQUIRES: OS=macosx || OS=ios || OS=tvos || OS=watchos // Tests for the diagnostics produced by the OSLogOptimization pass that -// performs compile-time analysis and optimization of the new os log prototype -// APIs. The tests here check whether bad user inputs are diagnosed correctly. -// The tests here model the possible invalid inputs to the os log methods. -// TODO: diagnostics will be improved. globalStringTablePointer builtin error -// must be suppressed. +// performs compile-time analysis and optimization of the new os log APIs. +// Note that many usage errors are caught by the Sema check: ConstantnessSemaDiagnostics. +// The tests here check only those diagnostics that are enforced at the SIL level. +// TODO: diagnostics must be improved, and globalStringTablePointer builtin error must be +// suppressed. import OSLogTestHelper -func testDynamicLogMessage(message: OSLogMessage) { - _osLogTestHelper(message) - // expected-error @-1 {{globalStringTablePointer builtin must used only on string literals}} +func testNonDecimalFormatOptionOnIntegers() { + _osLogTestHelper("Minimum integer value: \(Int.min, format: .hex)") + // expected-error @-1 {{evaluation of constant-evaluable function 'OSLogInterpolation.appendInterpolation(_:format:align:privacy:)' failed}} + // expected-note @-2 {{Fatal error: Signed integers must be formatted using .decimal}} + // expected-error @-3 {{globalStringTablePointer builtin must used only on string literals}} } -func testNonconstantFormatOption(formatOpt: OSLogIntegerFormatting) { - _osLogTestHelper("Minimum integer value: \(Int.min, format: formatOpt)") - // expected-error @-1 {{interpolation arguments like format and privacy options must be constants}} - // expected-error @-2 {{globalStringTablePointer builtin must used only on string literals}} +// Extending OSLogInterpolation (without the constant_evaluable attribute) would be an +// error. +struct A { + var i: Int } - -func testNonconstantPrivacyOption( privacyOpt: OSLogPrivacy) { - _osLogTestHelper("Minimum integer value: \(Int.min, privacy: privacyOpt)") - // expected-error @-1 {{interpolation arguments like format and privacy options must be constants}} - // expected-error @-2 {{globalStringTablePointer builtin must used only on string literals}} -} - -func testNoninlinedOSLogMessage() { - let logMessage: OSLogMessage = "Minimum integer value: \(Int.min)" - // expected-error @-1 {{OSLogMessage instance must not be explicitly created and must be deletable}} - _osLogTestHelper(logMessage) -} - -func testNoninlinedOSLogMessageComplex(b: Bool) { - let logMessage: OSLogMessage = "Maximum integer value: \(Int.max)" - // expected-error @-1 {{OSLogMessage instance must not be explicitly created and must be deletable}} - if !b { - return +extension OSLogInterpolation { + mutating func appendInterpolation(a: A) { + self.appendInterpolation(a.i) } - _osLogTestHelper(logMessage) - // expected-error @-1 {{globalStringTablePointer builtin must used only on string literals}} -} - -func testNoninlinedFormatOptions() { - let formatOption: OSLogIntegerFormatting = .hex(includePrefix: true) - _osLogTestHelper("Minimum integer value: \(Int.min, format: formatOption)") - // expected-error @-1 {{interpolation arguments like format and privacy options must be constants}} - // expected-error @-2 {{globalStringTablePointer builtin must used only on string literals}} } -func testNoninlinedFormatOptionsComplex(b: Bool) { - let formatOption: OSLogIntegerFormatting = .hex(includePrefix: true) - if !b { - return - } - _osLogTestHelper("Minimum integer value: \(Int.min, format: formatOption)") - // expected-error @-1 {{interpolation arguments like format and privacy options must be constants}} - // expected-error @-2 {{globalStringTablePointer builtin must used only on string literals}} +func testOSLogInterpolationExtension(a: A) { + _osLogTestHelper("Error at line: \(a: a)") + // expected-error @-1 {{evaluation of constant-evaluable function 'OSLogInterpolation.appendLiteral(_:)' failed}} + // expected-note @-2 {{value mutable by an unevaluated instruction is not a constant}} + // expected-error @-3 {{globalStringTablePointer builtin must used only on string literals}} } internal enum Color { diff --git a/test/Sema/diag_constantness_check.swift b/test/Sema/diag_constantness_check.swift new file mode 100644 index 0000000000000..1278d59b59141 --- /dev/null +++ b/test/Sema/diag_constantness_check.swift @@ -0,0 +1,330 @@ +// RUN: %target-typecheck-verify-swift -swift-version 5 + +// Tests for the diagnostics emitted in Sema for checking constantness of +// function arguments annotated to be so. Note that these annotation are +// specific to the new os log overlay and the low-level atomics library. + +// The following tests check different types of constants that are accepted +// by the constantness Sema check. It creates helper functions with the +// semnatics annotation: "oslog.requires_constant_arguments" which requires that +// all arguments passed to the function are constants. The annotation is meant +// to be used only by the os log overlay. The test helpers use it here only for +// the purpose of testing the functionality. + +// Check simple literals. +@_semantics("oslog.requires_constant_arguments") +func constantArgumentFunction(_ constArg: T) { +} + +func literalTest(x: Int) { + constantArgumentFunction(1) + constantArgumentFunction("Some string") + constantArgumentFunction(1.9) + constantArgumentFunction(true) + constantArgumentFunction(x) + // expected-error@-1 {{argument must be an integer literal}} + constantArgumentFunction(x + 2) + // expected-error@-1 {{argument must be an integer literal}} +} + +@_semantics("oslog.requires_constant_arguments") +func constArgFunctionWithReturn(_ constArg: T) -> T { + return constArg +} + +func testConstArgFuncWithReturn(x: Int, str: String) -> Int { + _ = constArgFunctionWithReturn("") + _ = constArgFunctionWithReturn(str) + // expected-error@-1 {{argument must be a string literal}} + constArgFunctionWithReturn(10) + // expected-warning@-1 {{result of call to 'constArgFunctionWithReturn' is unused}} + return constArgFunctionWithReturn(x) + // expected-error@-1 {{argument must be an integer literal}} +} + +@_semantics("oslog.requires_constant_arguments") +func constantOptionalArgument(_ constArg: Optional) { +} + +// Correct test cases. +func optionalTest(x: Int) { + constantOptionalArgument(nil) + constantOptionalArgument(0) + constantArgumentFunction(x + 2) + // expected-error@-1 {{argument must be an integer literal}} +} + +// Test string interpolation literals. We can only enforce constantness on custom string +// interpolation types. For string types, the constant is a string literal. + +struct CustomStringInterpolation : ExpressibleByStringLiteral, + ExpressibleByStringInterpolation { + struct StringInterpolation : StringInterpolationProtocol { + init(literalCapacity: Int, interpolationCount: Int) { } + mutating func appendLiteral(_ x: String) { } + + @_semantics("oslog.requires_constant_arguments") + mutating func appendInterpolation(_ x: Int) { } + } + init(stringLiteral value: String) { } + init(stringInterpolation: StringInterpolation) { } +} + +@_semantics("oslog.requires_constant_arguments") +func constantStringInterpolation(_ constArg: CustomStringInterpolation) {} + +func testStringInterpolationLiteral(x: Int) { + constantStringInterpolation("a string interpolation literal \(x)") + // expected-error@-1 {{argument must be an integer literal}} + constantStringInterpolation("a string interpolation literal \(10)") +} + +// Test multiple arguments. +@_semantics("oslog.requires_constant_arguments") +func multipleArguments(_ arg1: Int, _ arg2: Bool, _ arg3: String, _ arg4: Double) { +} + +func testMultipleArguments(_ x: String, _ y: Double) { + multipleArguments(56, false, "", 23.3) + multipleArguments(56, false, x, y) + // expected-error@-1 {{argument must be a string literal}} + // expected-error@-2 {{argument must be a floating-point literal}} +} + +// Test enum uses. +enum Color { + case red + case blue + case green + case rgb(r: Int, g: Int, b: Int) +} + +@_semantics("oslog.requires_constant_arguments") +func enumArgument(_ color: Color) { } + +func testEnumArgument(r: Int, c: Color) { + enumArgument(.rgb(r: 12, g: 0, b: 1)) + enumArgument(.green) + enumArgument(.rgb(r: r, g: 200, b: 453)) + // expected-error@-1 {{argument must be an integer literal}} + enumArgument(c) + // expected-error@-1 {{argument must be a case of enum 'Color'}} +} + +// Test type expressions. +@_semantics("oslog.requires_constant_arguments") +func typeArgument(_ t: T.Type) { } + +func testTypeArgument(_ t: S.Type) { + typeArgument(Int.self) + typeArgument(S.self) + typeArgument(t) + // expected-error@-1 {{argument must be a .self}} +} + +// Test constant evaluable function calls. +@_semantics("constant_evaluable") +func constantEval(_ x: Int, _ y: Bool) -> Int { x + 100 } + +func testConstantEvalArgument(x: Int) { + constantArgumentFunction(constantEval(90, true)) + constantArgumentFunction(constantEval(constantEval(500, true), false)) + constantArgumentFunction(constantEval(x, true)) + // expected-error@-1 {{argument must be an integer literal}} +} + +// Test constant evaluable function calls with default arguments. +@_semantics("constant_evaluable") +func constantEvalAdvanced(_ x: () -> Int, _ y: Bool = true, z: String) { } + +func testConstantEvalAdvanced(arg: Int) { + constantArgumentFunction(constantEvalAdvanced({ arg }, z: "")) +} + +// Test constant evaluable methods. +struct E { + // expected-note@-1 {{'E' declared here}} + func constantEvalMethod1() -> E { return self } + + @_semantics("constant_evaluable") + func constantEvalMethod2() -> E { return self } + + @_semantics("constant_evaluable") + static func constantEvalMethod3(x: Bool) -> E { return E() } + + @_semantics("constant_evaluable") + static func constantEvalMethod4() -> E { return E() } +} + +@_semantics("oslog.requires_constant_arguments") +func functionNeedingConstE(_ x: E) { } + +func testConstantEvalMethod(b: Bool) { + functionNeedingConstE(E().constantEvalMethod1()) + // expected-error@-1 {{argument must be a static method or property of 'E'}} + functionNeedingConstE(E().constantEvalMethod2()) + functionNeedingConstE(.constantEvalMethod3(x: true)) + functionNeedingConstE(.constantEvalMethod3(x: b)) + // expected-error@-1 {{argument must be a bool literal}} + functionNeedingConstE(.constantEvalMethod4()) +} + +// Test functions with autoclosures. +@_semantics("oslog.requires_constant_arguments") +func autoClosureArgument(_ number: @autoclosure @escaping () -> Int) { } + +func testAutoClosure(_ number: Int) { + autoClosureArgument(number) +} + +@_semantics("constant_evaluable") +func constEvalWithAutoClosure(_ number: @autoclosure @escaping () -> Int) -> Int { + return 0 +} + +func testConstantEvalAutoClosure(_ number: Int) { + constantArgumentFunction(constEvalWithAutoClosure(number)) +} + +// Test nested use of constant parameter. +@_semantics("oslog.requires_constant_arguments") +func testConstantArgumentRequirementPropagation(constParam: Int) { + constantArgumentFunction(constParam) +} + +// Test nested use of constant parameter in constant evaluable function. +@_semantics("oslog.requires_constant_arguments") +func testConstantArgumentWithConstEval(constParam: Int) { + constantArgumentFunction(constantEval(constParam, true)) +} + +// Test parital-apply of constantArgumentFunction. +@_semantics("oslog.requires_constant_arguments") +func constArg2(_ x: Int) -> Int { x } + +// This is not an error. +func testPartialApply() -> ((Int) -> Int) { + return constArg2 +} + +@_semantics("oslog.requires_constant_arguments") +func constArg3(_ x: (Int) -> Int) -> Int { x(0) } + +@_semantics("constant_evaluable") +func intIdentity(_ x: Int) -> Int { x } + +func testPartialApply2() -> Int { + return constArg3(intIdentity) + // expected-error@-1 {{argument must be a closure}} +} + +// Test struct and class constructions. Structs whose initializers are marked as +// constant_evaluable are considered as constants. + +struct AStruct { + var i: Int +} + +struct BStruct { + var i: Int + @_semantics("constant_evaluable") + init(_ value: Int) { + i = value + } +} + +class CClass { + var str: String + init() { + str = "" + } +} + +func testStructAndClasses(arg: Int) { + constantArgumentFunction(AStruct(i: 9)) + // expected-error@-1 {{argument must be a static method or property of 'AStruct'}} + constantArgumentFunction(BStruct(340)) + constantArgumentFunction(CClass()) + // expected-error@-1 {{argument must be a static method or property of 'CClass'}} +} + +// Test "requires_constant" annotation on protocol requirements. +protocol Proto { + @_semantics("oslog.requires_constant_arguments") + func method(arg1: Int, arg2: Bool) +} + +struct SConf : Proto { + func method(arg1: Int, arg2: Bool) { } +} + +func testProtocolMethods(b: Bool, p: T, p2: Proto, s: SConf) { + p.method(arg1: 6, arg2: true) + p.method(arg1: 6, arg2: b) + // expected-error@-1 {{argument must be a bool literal}} + p2.method(arg1: 6, arg2: b) + // expected-error@-1 {{argument must be a bool literal}} + // Note that even though 's' conforms to Proto, since its method is not + // annotated as requiring constant arg2, there will be no error here. + s.method(arg1: 6, arg2: b) +} + +// Check requiers annotation on a class method. +class ClassD { + @_semantics("oslog.requires_constant_arguments") + func method(_ arg1: Int, _ arg2: Bool) +} + +func testClassMethod(d: ClassD, b: Bool) { + d.method(10, true) + d.method(10, b) + // expected-error@-1 {{argument must be a bool literal}} +} + +// Test that the check is resilient to errors in the semantics attribute. +@_semantics("oslog.requires_constant_") +func funcWithWrongSemantics(x: Int) {} + +func testFunctionWithWrongSemantics(x: Int) { + funcWithWrongSemantics(x: x) +} + +// Test that the check is resilient to other type errors. +func testOtherTypeErrors() { + constantArgumentFunction(x) + // expected-error@-1 {{use of unresolved identifier 'x'}} + constantArgumentFunction(10 as String) + // expected-error@-1 {{cannot convert value of type 'Int' to type 'String' in coercion}} +} + +// Test constantness of the ordering used in the atomic operations. The atomic +// operations that requires a constant ordering are required to use the +// semantics annotation "atomics.requires_constant_orderings". + +internal struct AtomicLoadOrdering { + @_semantics("constant_evaluable") + internal static var acquiring: Self { Self() } + + @_semantics("constant_evaluable") + internal static var sequentiallyConsistent: Self { Self() } +} + +internal struct UnsafeAtomicIntStub { + @_semantics("atomics.requires_constant_orderings") + internal func load( + ordering: AtomicLoadOrdering = .sequentiallyConsistent + ) -> Int { + return 0 + } +} + +func testAtomicOrderingConstantness( + atomicInt: UnsafeAtomicIntStub, + myOrder: AtomicLoadOrdering +) { + _ = atomicInt.load() + _ = atomicInt.load(ordering: .acquiring) + _ = atomicInt.load(ordering: .sequentiallyConsistent) + _ = atomicInt.load(ordering: myOrder) + // expected-error@-1 {{ordering argument must be a static method or property of 'AtomicLoadOrdering'}} +} diff --git a/test/Sema/diag_constantness_check_os_log.swift b/test/Sema/diag_constantness_check_os_log.swift new file mode 100644 index 0000000000000..0997b458fec44 --- /dev/null +++ b/test/Sema/diag_constantness_check_os_log.swift @@ -0,0 +1,120 @@ +// RUN: %target-typecheck-verify-swift -swift-version 5 + +// REQUIRES: OS=macosx || OS=ios || OS=tvos || OS=watchos + +// Tests the constantness Sema diagnostics for the OSLogTestHelper module, +// which acts as a stub for the os overlay. + +import OSLogTestHelper + +func testDynamicLogMessage(message: OSLogMessage) { + _osLogTestHelper(message) + // expected-error@-1 {{argument must be a string interpolation}} +} + +func testNonconstantFormatOption( + formatOpt: OSLogIntegerFormatting, + explicitPositiveSign: Bool) { + _osLogTestHelper("Minimum integer value: \(Int.min, format: formatOpt)") + // expected-error@-1 {{argument must be a static method or property of 'OSLogIntegerFormatting'}} + + let uppercase = true + _osLogTestHelper("\(UInt.max, format: .hex(uppercase: uppercase))") + // expected-error@-1 {{argument must be a bool literal}} + + _osLogTestHelper("\(UInt.max, format: .hex)") // No error is expected here. +} + +func testNonconstantPrivacyOption(privacyOpt: OSLogPrivacy) { + _osLogTestHelper("Integer: \(Int.max, privacy: privacyOpt)") + // expected-error@-1 {{argument must be a case of enum 'OSLogPrivacy'}} +} + +func testNonconstantAlignmentOption(alignOpt: OSLogStringAlignment) { + _osLogTestHelper("Integer: \(Int.max, align: alignOpt)") + // expected-error@-1 {{argument must be a static method or property of 'OSLogStringAlignment'}} +} + +func testMultipleOptions( + formatOpt: OSLogIntegerFormatting, + privacyOpt: OSLogPrivacy +) { + _osLogTestHelper( + """ + \(2, format: formatOpt, align: .right(columns: 10), privacy: privacyOpt) + """) + // expected-error@-2 {{argument must be a static method or property of 'OSLogIntegerFormatting'}} + // expected-error@-3 {{argument must be a case of enum 'OSLogPrivacy'}} +} + +func testNoninlinedOSLogMessage() { + let logMessage: OSLogMessage = "Minimum integer value: \(Int.min)" + _osLogTestHelper(logMessage) + // expected-error@-1 {{argument must be a string interpolation}} +} + +let globalLogMessage: OSLogMessage = "A global message" + +func testGlobalLogMessage() { + _osLogTestHelper(globalLogMessage) + // expected-error@-1 {{argument must be a string interpolation}} +} + +// No errors are expected here. +func testValidLogCalls(x: Int) { + _osLogTestHelper("\(x, format: .hex, privacy: .private)") + _osLogTestHelper("\(x, format: OSLogIntegerFormatting.hex, privacy: .public)") + _osLogTestHelper("\(x, privacy: OSLogPrivacy.public)") + _osLogTestHelper("\((x + 1) * 2, privacy: .public)") +} + +// Check whether os-log-specific diagnostics do not crash when there +// are type errors. +func testTypeIncorrectLogCalls() { + let message = "test message" + + _osLogTestHelper(message) + // expected-error@-1 {{cannot convert value of type 'String' to expected argument type 'OSLogMessage'}} + _osLogTestHelper("prefix" + "\(x)") + // expected-error@-1 {{cannot convert value of type 'String' to expected argument type 'OSLogMessage'}} + _osLogTestHelper("prefix", "\(x)") + // expected-error@-1 {{cannot convert value of type 'String' to expected argument type '(String, UnsafeBufferPointer) -> Void'}} + // expected-error@-2 {{missing argument label 'assertion:' in call}} + + class TestClass { + } + let x = TestClass() + _osLogTestHelper("\(x, format: .hex)") + //expected-error@-1 {{no exact matches in call to instance method 'appendInterpolation'}} + + _osLogTestHelper("\(10, format: .myFormat, privacy: .private)") + //expected-error@-1 {{type 'OSLogIntegerFormatting' has no member 'myFormat'}} +} + +// Test diagnostics in extensions to OSLogInterpolation. This is not officially +// supported yet. +struct MyStruct { + var i: Int +} + +extension OSLogInterpolation { + mutating func appendInterpolation(a: MyStruct) { + self.appendInterpolation(a.i) + } + + mutating func appendInterpolation(a: MyStruct, format: OSLogIntegerFormatting) { + self.appendInterpolation(a.i, format: format) + // expected-error@-1 {{argument must be a static method or property of 'OSLogIntegerFormatting'}} + } + + @_semantics("oslog.requires_constant_arguments") + mutating func appendInterpolation(a: MyStruct, constFormat: OSLogIntegerFormatting) { + self.appendInterpolation(a.i, format: constFormat) + } +} + +func testOSLogInterpolationExtension(a: MyStruct) { + // The following is not a Sema error but would result in SIL diagnostics as + // the appendInterpolation overload is not marked as constant_evaluable. + _osLogTestHelper("Error at line: \(a: a)") +}