Skip to content
Merged
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
5 changes: 5 additions & 0 deletions tslang/include/TypeScript/Defines.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
// borrows rather than owns - out of the assignment path. See MLIRGen's takeOwnershipOfLocal.
#define OWNED_LOCAL_ATTR_NAME "__owned"

// Marks a variable's storage (its ts.Variable, or its ts.Global) whose type was inferred from an
// integer literal - `let s = 0` is an s32 - so that storing a fraction into it can be warned about.
// See MLIRGen's warnIfIntegerLiteralVariableTruncates.
#define INTEGER_LITERAL_INFERRED_ATTR_NAME "__int_literal_inferred"

// Marks an operation whose result already carries a reference the receiver is expected to take
// over, rather than one it must retain for itself. `new C()` is the case that matters: it lowers
// to a call of the generated `C..new`, and every function retains its result before returning
Expand Down
73 changes: 72 additions & 1 deletion tslang/lib/TypeScript/MLIRGenImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -1382,7 +1382,7 @@ class MLIRGenImpl
VariableDeclarationInfo(CompileOptions& compileOptions) : compileOptions(compileOptions), variableName(), fullName(), initial(), type(), storage(), globalOp(), varClass(),
scope{VariableScope::Local}, isFullName{false}, isGlobal{false}, isConst{false}, isExternal{false}, isExport{false}, isImport{false},
isSpecialization{false}, allocateOutsideOfOperation{false}, allocateInContextThis{false}, comdat{Select::NotSet}, deleted{false}, isUsed{false},
needsIdentityStorage{false}, typeAndInitResolved{false}, disposeGuard()
needsIdentityStorage{false}, typeAndInitResolved{false}, inferredFromIntegerLiteral{false}, disposeGuard()
{
};

Expand Down Expand Up @@ -1797,6 +1797,9 @@ class MLIRGenImpl
bool isUsed;
bool needsIdentityStorage;
bool typeAndInitResolved;
// `let s = 0`: no type given and an integer literal to infer it from, so it is an s32 -
// see warnIfIntegerLiteralVariableTruncates
bool inferredFromIntegerLiteral;

// See createLocalVariable: set only for a `using` declaration hoisted out in front of a
// TryOp, and carried onto the VariableDeclarationDOM that mlirGenDisposable walks.
Expand All @@ -1821,6 +1824,68 @@ class MLIRGenImpl
mth.ownsHeapMemory(location, variableDeclarationInfo.type) && !blockIsInsideCatchOrFinally();
}

// `let s = 0`: no type given, and an integer literal to infer one from - integer literals are
// s32, so the variable is an integer where TypeScript would make it a number
static bool isInferredFromIntegerLiteral(struct VariableDeclarationInfo &variableDeclarationInfo, mlir::Type type)
{
if (variableDeclarationInfo.typeProvided != TypeProvided::No)
{
return false;
}

auto literalType = dyn_cast<mlir_ts::LiteralType>(type);
return literalType && isa<mlir::IntegerType>(literalType.getElementType());
}

// Storing a fraction into a variable whose type was inferred from an integer literal drops the
// fraction without a word: `let s = 0; s += 1.5` leaves 1, `let a = 0; a += values[i]` sums
// whole parts, where TypeScript keeps them. The inference stays (integer literals are s32);
// this says what happened and how to get a number.
void warnIfIntegerLiteralVariableTruncates(mlir::Location location, mlir::Value reference, mlir::Type destType,
mlir::Value value)
{
if (!isa<mlir::IntegerType>(destType))
{
return;
}

auto valueType = mth.stripLiteralType(value.getType());
if (!isa<mlir_ts::NumberType>(valueType) && !isa<mlir::FloatType>(valueType))
{
return;
}

// a whole number loses nothing
if (auto constantOp = value.getDefiningOp<mlir_ts::ConstantOp>())
{
if (auto floatAttr = dyn_cast<mlir::FloatAttr>(constantOp.getValue()))
{
auto number = floatAttr.getValueAsDouble();
if (number == std::trunc(number))
{
return;
}
}
}

auto *definingOp = reference.getDefiningOp();
auto inferred = definingOp && definingOp->hasAttr(INTEGER_LITERAL_INFERRED_ATTR_NAME);
if (auto addressOfOp = dyn_cast_or_null<mlir_ts::AddressOfOp>(definingOp))
{
auto globalOp = theModule.lookupSymbol<mlir_ts::GlobalOp>(addressOfOp.getGlobalName());
inferred = globalOp && globalOp->hasAttr(INTEGER_LITERAL_INFERRED_ATTR_NAME);
}

if (!inferred)
{
return;
}

emitWarning(location) << "a '" << to_print(valueType) << "' is stored in a '" << to_print(destType)
<< "', so any fraction is dropped: the variable's type was inferred from an integer literal. "
<< "Declare it 'number' (e.g. `let s: number = 0`) to keep fractions";
}

mlir::LogicalResult adjustLocalVariableType(mlir::Location location, struct VariableDeclarationInfo &variableDeclarationInfo, const GenContext &genContext)
{
auto type = variableDeclarationInfo.type;
Expand Down Expand Up @@ -1861,6 +1926,8 @@ class MLIRGenImpl
return mlir::success();
}

variableDeclarationInfo.inferredFromIntegerLiteral = isInferredFromIntegerLiteral(variableDeclarationInfo, type);

auto actualType = variableDeclarationInfo.typeProvided == TypeProvided::Yes ? type : mth.wideStorageType(type);

// this is 'let', if 'let' is func, it should be HybridFunction
Expand Down Expand Up @@ -1914,6 +1981,8 @@ class MLIRGenImpl

auto type = variableDeclarationInfo.type;

variableDeclarationInfo.inferredFromIntegerLiteral = isInferredFromIntegerLiteral(variableDeclarationInfo, type);

auto actualType = variableDeclarationInfo.typeProvided == TypeProvided::Yes ? type : mth.wideStorageType(type);

// A global other modules reach holds a function the way a local `let` does
Expand Down Expand Up @@ -5809,6 +5878,8 @@ class MLIRGenImpl
}
}

warnIfIntegerLiteralVariableTruncates(location, loadOp.getReference(), destType, savingValue);

syncSavingValue(destType);
if (!savingValue)
{
Expand Down
11 changes: 11 additions & 0 deletions tslang/lib/TypeScript/MLIRGenVariables.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,17 @@ namespace mlirgen
return variableDeclarationInfo.type;
}

// remembered on the storage for warnIfIntegerLiteralVariableTruncates
if (variableDeclarationInfo.inferredFromIntegerLiteral && !variableDeclarationInfo.deleted)
{
auto varOp = variableDeclarationInfo.storage ? variableDeclarationInfo.storage.getDefiningOp<mlir_ts::VariableOp>() : mlir_ts::VariableOp();
mlir::Operation *storageOp = varOp ? varOp.getOperation() : variableDeclarationInfo.globalOp.getOperation();
if (storageOp)
{
storageOp->setAttr(INTEGER_LITERAL_INFERRED_ATTR_NAME, builder.getUnitAttr());
}
}

//LLVM_DEBUG(variableDeclarationInfo.printDebugInfo(););

takeOwnershipOfLocal(location, variableDeclarationInfo, genContext);
Expand Down
21 changes: 21 additions & 0 deletions tslang/test/tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2634,6 +2634,27 @@ foreach(call_arity_case ${call_arity_cases})
FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed")
endforeach()

# Storing a fraction into a variable whose type was inferred from an integer literal (`let s = 0`
# is an s32) drops it, and is warned about; the inference itself stays.
foreach(truncation_case "global|3:1" "local|5:9")
string(REPLACE "|" ";" truncation_parts "${truncation_case}")
list(GET truncation_parts 0 truncation_name)
list(GET truncation_parts 1 truncation_position)
add_test(NAME test-compile-integer-literal-truncation-${truncation_name}
COMMAND $<TARGET_FILE:tslang> --emit=obj --no-default-lib -mm=none
"${PROJECT_SOURCE_DIR}/test/tester/integer-literal-truncation/${truncation_name}.ts"
-o "${CMAKE_CURRENT_BINARY_DIR}/integer-literal-truncation-${truncation_name}.obj")
set_tests_properties(test-compile-integer-literal-truncation-${truncation_name}
PROPERTIES PASS_REGULAR_EXPRESSION "${truncation_name}\\.ts:${truncation_position}: warning: a 'number' is stored in a 's32', so any fraction is dropped"
FAIL_REGULAR_EXPRESSION "error:|Stack dump|Assertion failed")
endforeach()
add_test(NAME test-compile-integer-literal-truncation-quiet
COMMAND $<TARGET_FILE:tslang> --emit=obj --no-default-lib -mm=none
"${PROJECT_SOURCE_DIR}/test/tester/integer-literal-truncation/quiet.ts"
-o "${CMAKE_CURRENT_BINARY_DIR}/integer-literal-truncation-quiet.obj")
set_tests_properties(test-compile-integer-literal-truncation-quiet
PROPERTIES FAIL_REGULAR_EXPRESSION "warning:|error:|Stack dump|Assertion failed")

# A number, string, boolean or array passed where an interface with members is expected is an
# error; it compiled to a cast lowering could not make, and the callee read garbage. An interface
# without members still takes any value.
Expand Down
4 changes: 4 additions & 0 deletions tslang/test/tester/integer-literal-truncation/global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// `let s = 0` is an s32 (integer literals are), so storing 1.5 drops the fraction: warned.
let s = 0;
s += 1.5;
print(s);
11 changes: 11 additions & 0 deletions tslang/test/tester/integer-literal-truncation/local.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// An accumulator started at the integer literal 0 sums only the whole parts: warned.
function sum(values: number[]): number {
let a = 0;
for (let i = 0; i < values.length; i++) {
a += values[i];
}

return a;
}

print(sum([1.5, 0, 2]));
16 changes: 16 additions & 0 deletions tslang/test/tester/integer-literal-truncation/quiet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// None of these drops a fraction it was not asked to drop: no warning.
function quiet() {
let n = 0;
n += 1;
n = n * 2;
n = 3.0;
let declared: s32 = 0;
declared = 2.5;
let f = 0.0;
f += 1.5;
let typed: number = 0;
typed += 1.5;
return n + declared + f + typed;
}

print(quiet());
Loading