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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -873,7 +873,7 @@ scripts\build_tslang_runtime_release_android.bat

Build the default library for Android with `scripts\build_android.bat` in the
[Default Library repo](https://github.com/ASDAlexander77/TypeScriptCompilerDefaultLib/). It
builds the release and debug libraries under gc, rc and none into the same `__build\defaultlib` tree as
builds the release and debug libraries under gc, rc, none and own into the same `__build\defaultlib` tree as
every other target, so `--default-lib-path=<TypeScriptCompilerDefaultLib>\__build` finds them.

## License
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ With these, `lib.ts` compiles under gc, rc, none and own, release and `--di`. De
the caller owns nothing) says so with `@lends`, added 2026-10-20. Covered by `own_lends.ts`, the
`own_err_lends_*` cases, and `import_own_markers` / `import_own_markers_lent_outlives` across
modules.
- **A ternary that picks an array literal or `null`** (`return i < 3 ? ["a"] : null`) fails MLIR
verification ("operand #0 does not dominate this use") under rc and own. This is unrelated.
- **Android** still builds gc, rc and none only.
- **A ternary that picks an array literal or `null`** (`return i < 3 ? ["a"] : null`) failed MLIR
verification ("operand #0 does not dominate this use") under every model, gc too. Fixed
2026-10-20. The ternary was a `const_array | null`, and the generated function that cast it to
`string[] | null` narrowed the literal to a copy that its narrowed name owned, given back
outside the branch. A literal array in a branch of `?:` is now an array there, and the branch
hands it out with a reference of its own, as `new C()` is, so own accepts array ternaries
(`c ? ["a"] : ["b"]` was rejected before). `if (a)` of a `string[] | null` now narrows it too.
Covered by `tests/00narrow_array_null.ts` under gc, rc, none and own.
- **Android** builds own as well, 2026-10-20 (DefaultLib `scripts/build_android.bat`). The
Android link test links an own executable and shared library, and an own program ran correctly
on the API 29 x86_64 emulator.
62 changes: 58 additions & 4 deletions tslang/lib/TypeScript/MLIRGenExpressions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -734,19 +734,73 @@ namespace mlirgen

if (resultTrue && resultFalse)
{
auto defaultUnionType = getUnionType(location, resultTrue.getType(), resultFalse.getType());
// An array literal is an array here, as TypeScript has `i < 3 ? ["a"] : null` a
// `string[] | null`: each branch makes its own. Left a `const_array | null`, the union had
// to be cast to `string[] | null` by a generated function, whose narrowing of the literal
// to an array was a copy given back outside the branch it was made in ("operand #0 does
// not dominate this use").
auto widenArrayLiteral = [&](mlir::Type type) {
return isa<mlir_ts::ConstArrayType>(type) ? mth.removeConstType(type) : type;
};
auto trueType = widenArrayLiteral(resultTrue.getType());
auto falseType = widenArrayLiteral(resultFalse.getType());
auto defaultUnionType = getUnionType(location, trueType, falseType);
auto merged = false;
auto resultType = mth.findBaseType(resultTrue.getType(), resultFalse.getType(), merged, defaultUnionType);
auto resultType = mth.findBaseType(trueType, falseType, merged, defaultUnionType);

ifOp.getResult(0).setType(resultType);

CAST_A(falseRes, location, resultType, resultFalse, genContext)
// The array a branch makes of its literal is a new block, and the branch hands it out
// with a reference of its own, as `new C()` is (markFreshBlockOwned): the `ts.If` then
// carries one reference whichever branch ran (consumeConditionalResults), and -mm=own
// sees the merge as fresh rather than as two values neither of which owns it.
// (A literal whose element type the receiver gave is built as an array already.)
auto arrayOfLiteral = [&](mlir::Value value) -> mlir::Value {
auto array = value;
if (isa<mlir_ts::ConstArrayType>(value.getType()))
{
auto result = cast(location, mth.removeConstType(value.getType()), value, genContext);
if (result.failed_or_no_value())
{
return mlir::Value();
}

array = V(result);
}
else if (!value.getDefiningOp<mlir_ts::CreateArrayOp>())
{
return value;
}

auto *definingOp = array.getDefiningOp();
if (compileOptions.tracksOwnership() && definingOp && !definingOp->hasAttr(OWNED_RESULT_ATTR_NAME))
{
builder.create<mlir_ts::RetainOp>(location, array);
definingOp->setAttr(OWNED_RESULT_ATTR_NAME, builder.getUnitAttr());
}

return array;
};

auto falseArray = arrayOfLiteral(resultFalse);
if (!falseArray)
{
return mlir::failure();
}

CAST_A(falseRes, location, resultType, falseArray, genContext)
builder.create<mlir_ts::ResultOp>(location, mlir::ValueRange{falseRes});

// finish type of IfOp and WhenTrue clause
builder.setInsertionPointToEnd(&ifOp.getThenRegion().back());

CAST_A(trueRes, location, resultType, resultTrue, genContext);
auto trueArray = arrayOfLiteral(resultTrue);
if (!trueArray)
{
return mlir::failure();
}

CAST_A(trueRes, location, resultType, trueArray, genContext);
builder.create<mlir_ts::ResultOp>(location, mlir::ValueRange{trueRes});
}
else
Expand Down
17 changes: 12 additions & 5 deletions tslang/lib/TypeScript/MLIRGenImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -4298,14 +4298,21 @@ class MLIRGenImpl
return addSafeCastStatement(exprVal, optType.getElementType(), inverse, elseSafeCase, genContext);
}

// `number | null`, `{ k: number } | null`: a union with a tag and null. What is truthy is not
// null, as after `x !== null`; what is not may be null or a falsy value (0, ""), so only the
// branch where it is truthy is narrowed - the then branch of `if (x)`, the else of `if (!x)`
// `number | null`, `{ k: number } | null`: a union with a tag and null; and `string[] | null`,
// which has none, an array holding null in its own pointer. What is truthy is not null, as
// after `x !== null`; what is not may be null or a falsy value (0, ""), so only the branch
// where it is truthy is narrowed - the then branch of `if (x)`, the else of `if (!x)`.
// Without the array case `if (a) a[0]` of a `string[] | null` indexed the union. (A
// `Shared<T> | null` is a counted handle, which own reads through the variable itself.)
if (auto unionType = dyn_cast_or_null<mlir_ts::UnionType>(exprEval))
{
auto isNull = [](mlir::Type member) { return isa<mlir_ts::NullType>(member); };
mlir::Type baseType;
if (mth.isUnionTypeNeedsTag(loc(exprVal), unionType, baseType)
&& llvm::any_of(unionType.getTypes(), [](mlir::Type member) { return isa<mlir_ts::NullType>(member); }))
auto arrayOrNull = llvm::all_of(unionType.getTypes(), [&](mlir::Type member) {
return isNull(member) || isa<mlir_ts::ArrayType>(member);
});
if ((mth.isUnionTypeNeedsTag(loc(exprVal), unionType, baseType) || arrayOrNull)
&& llvm::any_of(unionType.getTypes(), isNull))
{
if (!inverse)
{
Expand Down
6 changes: 6 additions & 0 deletions tslang/test/tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ tslang_add_test(NAME test-compile-00-owned-closures COMMAND test-runner "${PROJE
tslang_add_test(NAME test-compile-00-owned-any-boxing COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_any_boxing.ts")
tslang_add_test(NAME test-compile-00-owned-strings COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_strings.ts")
tslang_add_test(NAME test-compile-00-string-length-resize COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_length_resize.ts")
tslang_add_test(NAME test-compile-00-narrow-array-null COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00narrow_array_null.ts")
tslang_add_test(NAME test-compile-00-array-length-grow-zeroed COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_grow_zeroed.ts")
tslang_add_test(NAME test-compile-00-rest-args-pack-owned COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00rest_args_pack_owned.ts")
tslang_add_test(NAME test-compile-00-switch-char-string-cases COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00switch_char_string_cases.ts")
Expand Down Expand Up @@ -1069,6 +1070,10 @@ tslang_add_test(NAME test-jit-00-owned-closures COMMAND test-runner -jit "${PROJ
tslang_add_test(NAME test-jit-00-owned-any-boxing COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_any_boxing.ts")
tslang_add_test(NAME test-jit-00-owned-strings COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_strings.ts")
tslang_add_test(NAME test-jit-00-string-length-resize COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_length_resize.ts")
tslang_add_test(NAME test-jit-00-narrow-array-null COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00narrow_array_null.ts")
# own sees each branch's array of its literal as fresh
tslang_add_test(NAME test-jit-own-00-narrow-array-null COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00narrow_array_null.ts")
tslang_add_test(NAME test-compile-own-00-narrow-array-null COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00narrow_array_null.ts")
tslang_add_test(NAME test-jit-00-array-length-grow-zeroed COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_grow_zeroed.ts")
tslang_add_test(NAME test-jit-00-rest-args-pack-owned COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00rest_args_pack_owned.ts")
tslang_add_test(NAME test-jit-00-switch-char-string-cases COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00switch_char_string_cases.ts")
Expand Down Expand Up @@ -2376,6 +2381,7 @@ set(TSLANG_CORPUS
typeGuardOfFormTypeOfBoolean.ts
types_vs_interfaces.ts
00any_number_compare.ts
00narrow_array_null.ts
00opaque_nullable_compare.ts
)

Expand Down
38 changes: 25 additions & 13 deletions tslang/test/tester/android-link.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -99,38 +99,50 @@ elseif(MODE STREQUAL "link")
else()
set(default_lib "${REPO_DIR}/../TypeScriptCompilerDefaultLib/__build")
endif()
# the one default-library tree, every target in it; tslang names this one's folder
execute_process(COMMAND "${TSLANG}" --print-default-lib-dir=lib -mm=gc -mtriple=${triple}
OUTPUT_VARIABLE default_lib_subdir
OUTPUT_STRIP_TRAILING_WHITESPACE
COMMAND_ERROR_IS_FATAL ANY)
# the one default-library tree, every target in it; tslang names each model's folder. gc, and
# own, whose default library is built for Android too: what it links differs from gc's (no
# collector, the default library's own releases)
set(link_models gc own)
foreach(mm ${link_models})
execute_process(COMMAND "${TSLANG}" --print-default-lib-dir=lib -mm=${mm} -mtriple=${triple}
OUTPUT_VARIABLE default_lib_subdir_${mm}
OUTPUT_STRIP_TRAILING_WHITESPACE
COMMAND_ERROR_IS_FATAL ANY)
endforeach()

if("$ENV{ANDROID_NDK_HOME}" STREQUAL "")
message("SKIPPED: ANDROID_NDK_HOME is not set")
return()
endif()
foreach(file "${gc_lib}/libgc.a" "${runtime_lib}/libTypeScriptAsyncRuntime.a"
"${default_lib}/${default_lib_subdir}/libTypeScriptDefaultLib.a")
set(required "${gc_lib}/libgc.a" "${runtime_lib}/libTypeScriptAsyncRuntime.a")
foreach(mm ${link_models})
list(APPEND required "${default_lib}/${default_lib_subdir_${mm}}/libTypeScriptDefaultLib.a")
endforeach()
foreach(file ${required})
if(NOT EXISTS "${file}")
message("SKIPPED: ${file} is not built")
return()
endif()
endforeach()

set(libs "--default-lib-path=${default_lib}" "--gc-lib-path=${gc_lib}" "--tslang-lib-path=${runtime_lib}")
run("--emit=exe for ${triple}" TRUE
"${TSLANG}" --emit=exe --opt -mm=gc -mtriple=${triple} ${libs} app.ts -o app)
run("--emit=dll for ${triple}" TRUE
"${TSLANG}" --emit=dll --opt -mm=gc -mtriple=${triple} ${libs} app.ts -o libapp.so)
set(binaries)
foreach(mm ${link_models})
run("--emit=exe -mm=${mm} for ${triple}" TRUE
"${TSLANG}" --emit=exe --opt -mm=${mm} -mtriple=${triple} ${libs} app.ts -o app-${mm})
run("--emit=dll -mm=${mm} for ${triple}" TRUE
"${TSLANG}" --emit=dll --opt -mm=${mm} -mtriple=${triple} ${libs} app.ts -o libapp-${mm}.so)
list(APPEND binaries app-${mm} libapp-${mm}.so)
endforeach()

foreach(binary app libapp.so)
foreach(binary ${binaries})
file(READ "${WORK_DIR}/${binary}" magic LIMIT 4 HEX)
if(NOT magic STREQUAL "7f454c46")
message(FATAL_ERROR "${binary} is not an ELF file")
endif()
endforeach()

message(STATUS "an executable and a shared library linked for ${triple}")
message(STATUS "an executable and a shared library linked for ${triple}, under ${link_models}")
else()
message(FATAL_ERROR "MODE must be codegen or link, not '${MODE}'")
endif()
83 changes: 83 additions & 0 deletions tslang/test/tester/tests/00narrow_array_null.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// An array or null, made by a conditional expression and narrowed by testing it.
//
// `i < 3 ? ["a"] : null` was a `const_array | null`, and making it the `string[] | null` it is
// returned as called a generated cast function, whose `else` narrowed `t` to a copy of the
// literal as an array: a local that owned it, given back at the end of the function, outside the
// branch it was made in - "operand #0 does not dominate this use", under every model. A literal
// array in a branch of `?:` is now an array there, as TypeScript has it, and the branch hands it
// out with a reference of its own, as `new C()` is, so -mm=own sees the merge as one fresh array
// instead of rejecting it (`either`, which never failed to verify, was rejected by own too).
//
// And `if (a)` of a `string[] | null` did not narrow it, since an array holds null in its own
// pointer and the union has no tag: `a[0]` then indexed the union.

function pick(i: number) {
return i < 3 ? ["a"] : null;
}

function picked(i: number): string[] | null {
return i < 3 ? ["a", "b"] : null;
}

function either(i: number): string[] {
return i < 3 ? ["a"] : ["b", "c"];
}

class Vec {
x: number;

constructor(x: number) {
this.x = x;
}
}

// Allocate over whatever has just been freed, so a use-after-free reads something else.
function churn() {
for (let i = 0; i < 64; i++) {
let filler = new Vec(999);
}
}

function made(i: number): string[] | null {
if (i < 3) return ["c"];
return null;
}

function main() {
const a = pick(1);
const b = pick(5);
assert(a != null && b == null);
if (a) assert(a[0] == "a");

const c = picked(1);
if (c) {
assert(c.length == 2 && c[1] == "b");
}

assert(picked(9) == null);

const g = either(1);
const h = either(7);
churn();
assert(g.length == 1 && g[0] == "a" && h.length == 2 && h[1] == "c");
g.push("z");
assert(g.length == 2 && h.length == 2);

const d = made(1);
if (!d) assert(false);
else assert(d[0] == "c");

let count = 0;
let e = made(2);
while (e) {
count += e.length;
e = null;
}

for (let f = made(0); f; f = null) {
count += f.length;
}

assert(count == 2);
print("done.");
}
Loading