From f8549d40b139254bc653b813e339451de9c502a7 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 11 Oct 2026 13:33:07 +0100 Subject: [PATCH 1/2] Warn when a fraction is stored into a variable inferred from an integer Integer literals are s32, so `let s = 0` is an integer and `s += 1.5`, `s = s + x` or `let a = 0; a += values[i]` drop the fraction without a word, where TypeScript keeps it. The inference stays; storing a number with a fraction into such a variable now warns, and says to declare it `number`. A whole-number constant (`s = 3.0`), a variable with a declared integer type and one inferred from `0.0` are not warned about. The declaration marks its storage (ts.Variable or ts.Global) with INTEGER_LITERAL_INFERRED_ATTR_NAME; the store checks the mark. Reported in docs/bugs/native-ts-graphics-findings.md (issue 3). Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/Defines.h | 5 ++ tslang/lib/TypeScript/MLIRGenImpl.h | 73 ++++++++++++++++++- tslang/lib/TypeScript/MLIRGenVariables.cpp | 11 +++ tslang/test/tester/CMakeLists.txt | 21 ++++++ .../integer-literal-truncation/global.ts | 4 + .../integer-literal-truncation/local.ts | 11 +++ .../integer-literal-truncation/quiet.ts | 16 ++++ 7 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 tslang/test/tester/integer-literal-truncation/global.ts create mode 100644 tslang/test/tester/integer-literal-truncation/local.ts create mode 100644 tslang/test/tester/integer-literal-truncation/quiet.ts diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 885cf9d56..20eb53c15 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -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 diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 1f60a28e6..fc191f672 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -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() { }; @@ -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. @@ -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(type); + return literalType && isa(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(destType)) + { + return; + } + + auto valueType = mth.stripLiteralType(value.getType()); + if (!isa(valueType) && !isa(valueType)) + { + return; + } + + // a whole number loses nothing + if (auto constantOp = value.getDefiningOp()) + { + if (auto floatAttr = dyn_cast(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(definingOp)) + { + auto globalOp = theModule.lookupSymbol(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) + << "', dropping its fraction: 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; @@ -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 @@ -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 @@ -5803,6 +5872,8 @@ class MLIRGenImpl } } + warnIfIntegerLiteralVariableTruncates(location, loadOp.getReference(), destType, savingValue); + syncSavingValue(destType); if (!savingValue) { diff --git a/tslang/lib/TypeScript/MLIRGenVariables.cpp b/tslang/lib/TypeScript/MLIRGenVariables.cpp index c5ef94733..b171d7011 100644 --- a/tslang/lib/TypeScript/MLIRGenVariables.cpp +++ b/tslang/lib/TypeScript/MLIRGenVariables.cpp @@ -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::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); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 7a9a4d06e..43c58d640 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2613,6 +2613,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 $ --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', dropping its fraction" + FAIL_REGULAR_EXPRESSION "error:|Stack dump|Assertion failed") +endforeach() +add_test(NAME test-compile-integer-literal-truncation-quiet + COMMAND $ --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 call to the collector's API (GC_gcollect, or the weak-link functions DefaultLib's WeakRef uses) # is an error under every model but gc, which alone links the collector - it used to reach the # linker as an unresolved symbol. Under gc the same file compiles. diff --git a/tslang/test/tester/integer-literal-truncation/global.ts b/tslang/test/tester/integer-literal-truncation/global.ts new file mode 100644 index 000000000..337b868be --- /dev/null +++ b/tslang/test/tester/integer-literal-truncation/global.ts @@ -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); diff --git a/tslang/test/tester/integer-literal-truncation/local.ts b/tslang/test/tester/integer-literal-truncation/local.ts new file mode 100644 index 000000000..45d7be27b --- /dev/null +++ b/tslang/test/tester/integer-literal-truncation/local.ts @@ -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])); diff --git a/tslang/test/tester/integer-literal-truncation/quiet.ts b/tslang/test/tester/integer-literal-truncation/quiet.ts new file mode 100644 index 000000000..784751f1a --- /dev/null +++ b/tslang/test/tester/integer-literal-truncation/quiet.ts @@ -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()); From 2b781cb35d4a02c9f736f492d780e1d354b01517 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 11 Oct 2026 13:39:33 +0100 Subject: [PATCH 2/2] Say "any fraction is dropped": the value may be whole at run time Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 2 +- tslang/test/tester/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index fc191f672..5240fc062 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1882,7 +1882,7 @@ class MLIRGenImpl } emitWarning(location) << "a '" << to_print(valueType) << "' is stored in a '" << to_print(destType) - << "', dropping its fraction: the variable's type was inferred from an integer literal. " + << "', 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"; } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 43c58d640..3001e4f1e 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2624,7 +2624,7 @@ foreach(truncation_case "global|3:1" "local|5:9") "${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', dropping its fraction" + 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