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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ repo:
`scripts\build_tslang_runtime_<debug|release>_x86.bat` in `TypeScriptCompiler`, which
puts it in `__build\tslang-runtime\<debug|release>\x86\`.

### WebAssembly

With `llvm-ar` on `PATH` (or in `LLVM_AR`):

```
scripts\build_wasm.bat # wasm32 and wasm64, release and debug, rc/none/own
scripts\build_wasm.bat wasm32 release rc # just that one
scripts/build_wasm.sh wasm64 debug own # the same on a Linux host
```

The output goes to `__build\defaultlib\lib\<wasm32|wasm64>\unknown\unknown\unknown\<debug|release>\<rc|none|own>\libTypeScriptDefaultLib.a`.
It is `lib.ts` with the OS layer `src\lib.wasm.ts`, and no C++ wrappers: these targets have no C
library. tslang links a wasm program with `--allow-undefined`, so the C functions and the wrappers'
functions become imports, which the host that runs the program implements. The compiler repo's
`docs\how\wasm\test\host.mjs` is that host, for Node and browsers. There is no `gc` build, because
the collector is not built for wasm.

### Android

On a Windows host, with the Android NDK (tested with r30) in `ANDROID_NDK_HOME`:
Expand Down
122 changes: 122 additions & 0 deletions scripts/build_wasm.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
@rem Builds the default library for WebAssembly (wasm32-unknown-unknown, wasm64-unknown-unknown) on a
@rem Windows host, the wasm counterpart of scripts\build_android.bat. scripts/build_wasm.sh is the same
@rem for a Linux host.
@rem
@rem scripts\build_wasm.bat [arch] [release|debug] [rc|none|own]
@rem
@rem arch wasm32 | wasm64 (default: both)
@rem mode release | debug (default: both)
@rem model rc | none | own (default: all three; gc needs the Boehm collector, which has no wasm build)
@rem
@rem Needs:
@rem TOOL_PATH the directory holding tslang.exe; defaults to the compiler's release build tree next
@rem to this repository
@rem llvm-ar on PATH (LLVM's bin folder), or LLVM_AR set to it
@rem
@rem Output, into the one default-library tree every target shares:
@rem __build\defaultlib\lib\<arch>\unknown\unknown\unknown\<mode>\<model>\libTypeScriptDefaultLib.a
@rem __build\defaultlib\*.d.ts ... (shared by every target)
@rem
@rem Differences from the other targets:
@rem - No C++ wrappers: these targets have no C library to build them against. The archive is lib.ts
@rem and the OS layer lib.wasm.ts only. tslang links a wasm program with --allow-undefined, so what
@rem the wrappers and the C library would provide (snprintf, malloc, regexp_*, stdio_write, ...)
@rem become imports from the `env` module, which the host that runs the program implements - see
@rem TypeScriptCompiler\docs\how\wasm\test\host.mjs.
@rem - Only the static library: a wasm program is one module.
@echo off
setlocal

set ARCH=%~1
set MODE=%~2
set MM=%~3

if "%ARCH%"=="" (
call "%~f0" wasm32 "%MODE%" "%MM%" || exit /b 1
call "%~f0" wasm64 "%MODE%" "%MM%" || exit /b 1
exit /b 0
)
if "%MODE%"=="" (
call "%~f0" %ARCH% release "%MM%" || exit /b 1
call "%~f0" %ARCH% debug "%MM%" || exit /b 1
exit /b 0
)
if "%MM%"=="" (
call "%~f0" %ARCH% %MODE% rc || exit /b 1
call "%~f0" %ARCH% %MODE% none || exit /b 1
call "%~f0" %ARCH% %MODE% own || exit /b 1
exit /b 0
)

if not "%ARCH%"=="wasm32" if not "%ARCH%"=="wasm64" (
echo unknown arch "%ARCH%": wasm32 or wasm64
exit /b 1
)
set TRIPLE=%ARCH%-unknown-unknown

if "%MODE%"=="release" (
set DBG=--opt --opt_level=3
) else if "%MODE%"=="debug" (
set DBG=--di --opt_level=0
) else (
echo unknown mode "%MODE%": release or debug
exit /b 1
)

if "%MM%"=="gc" (
echo -mm=gc has no wasm build: the Boehm collector is not built for wasm. Use rc, none or own.
exit /b 1
)
if not "%MM%"=="rc" if not "%MM%"=="none" if not "%MM%"=="own" (
echo unknown memory model "%MM%": rc, none or own
exit /b 1
)

set ROOT=%~dp0..
if "%TOOL_PATH%"=="" set TOOL_PATH=%ROOT%\..\TypeScriptCompiler\__build\tslang\windows-msbuild-2026-release\bin
set TSLANG="%TOOL_PATH%\tslang.exe"
if not exist %TSLANG% (
echo tslang.exe not found in "%TOOL_PATH%": set TOOL_PATH
exit /b 1
)

if "%LLVM_AR%"=="" set LLVM_AR=llvm-ar
where "%LLVM_AR%" >nul 2>&1
if errorlevel 1 if not exist "%LLVM_AR%" (
echo llvm-ar not found: put LLVM's bin folder on PATH or set LLVM_AR
exit /b 1
)

set TSC=%TSLANG% -mtriple=%TRIPLE% %DBG% -mm=%MM% --emit=obj --export=none --nowarn --no-default-lib

rem The archive's folder, from tslang itself with the flags it compiles with below, composed the
rem way tslang later looks it up (see tslang/include/TypeScript/Defines.h).
set LIB_DIR=
for /f "usebackq delims=" %%d in (`"%TOOL_PATH%\tslang.exe" --print-default-lib-dir=lib -mtriple=%TRIPLE% %DBG% -mm=%MM%`) do set "LIB_DIR=%%d"
if "%LIB_DIR%"=="" (
echo %TSLANG% cannot name the output folder ^(--print-default-lib-dir^): it predates the per-target layout
exit /b 1
)
set "LIB_DIR=%LIB_DIR:/=\%"

set OUT=%ROOT%\__build\defaultlib
set OBJ=%ROOT%\__build\obj-%ARCH%-%MODE%-%MM%
set LIB_OUT=%ROOT%\__build\%LIB_DIR%

echo === %ARCH% %MODE% %MM%
if exist "%OBJ%" rd /s /q "%OBJ%"
if exist "%LIB_OUT%" rd /s /q "%LIB_OUT%"
md "%OBJ%" "%LIB_OUT%"

%TSC% "%ROOT%\src\lib.wasm.ts" -o "%OBJ%\lib.wasm.o" || exit /b 1
%TSC% "%ROOT%\src\lib.ts" -o "%OBJ%\lib.o" || exit /b 1

"%LLVM_AR%" rcs "%LIB_OUT%\libTypeScriptDefaultLib.a" "%OBJ%\lib.o" "%OBJ%\lib.wasm.o" || exit /b 1

rd /s /q "%OBJ%"

rem the declarations a program compiled against this tree reads (the same for every target), and
rem which compiler built this target's archives, beside the other targets' records
xcopy /E /I /Y /Q "%ROOT%\src" "%OUT%" > nul || exit /b 1
%TSLANG% --version > "%OUT%\COMPILER_VERSION.%ARCH%.txt" 2>&1
exit /b 0
73 changes: 73 additions & 0 deletions scripts/build_wasm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/bin/bash
# Builds the default library for WebAssembly (wasm32-unknown-unknown, wasm64-unknown-unknown) on a
# Linux host: scripts/build_wasm.bat's counterpart, which explains the differences from the other
# targets (no C++ wrappers: the host the program runs in implements them and the C library).
#
# scripts/build_wasm.sh [wasm32|wasm64] [release|debug] [rc|none|own] (default: every one)
#
# TOOL_PATH: the directory holding tslang (default: ../TypeScriptCompiler/__build/tslang/ninja/release/bin)
# LLVM_AR: llvm-ar (default: llvm-ar on PATH)
#
# Output: __build/defaultlib/lib/<arch>/unknown/unknown/unknown/<mode>/<model>/libTypeScriptDefaultLib.a

ROOT=$(cd "$(dirname "$0")/.." && pwd)
ARCHES=${1:-"wasm32 wasm64"}
MODES=${2:-"release debug"}
MODELS=${3:-"rc none own"}

TOOL_PATH=${TOOL_PATH:-$ROOT/../TypeScriptCompiler/__build/tslang/ninja/release/bin}
TSLANG=$TOOL_PATH/tslang
LLVM_AR=${LLVM_AR:-llvm-ar}

if [ ! -x "$TSLANG" ] ; then
echo "tslang not found in $TOOL_PATH: set TOOL_PATH"
exit 1
fi

if ! command -v "$LLVM_AR" > /dev/null ; then
echo "llvm-ar not found: put LLVM's bin folder on PATH or set LLVM_AR"
exit 1
fi

for ARCH in $ARCHES ; do
case "$ARCH" in wasm32|wasm64) ;; *) echo "unknown arch \"$ARCH\": wasm32 or wasm64" ; exit 1 ;; esac
TRIPLE=$ARCH-unknown-unknown
for MODE in $MODES ; do
case "$MODE" in
release) DBG="--opt --opt_level=3" ;;
debug) DBG="--di --opt_level=0" ;;
*) echo "unknown mode \"$MODE\": release or debug" ; exit 1 ;;
esac
for MM in $MODELS ; do
case "$MM" in
rc|none|own) ;;
gc) echo "-mm=gc has no wasm build: the Boehm collector is not built for wasm. Use rc, none or own." ; exit 1 ;;
*) echo "unknown memory model \"$MM\": rc, none or own" ; exit 1 ;;
esac

echo "=== $ARCH $MODE $MM"
# the archive's folder, from tslang itself with the flags it compiles with below
LIB_DIR=$("$TSLANG" --print-default-lib-dir=lib -mtriple=$TRIPLE $DBG -mm=$MM)
if [ -z "$LIB_DIR" ] ; then
echo "$TSLANG cannot name the output folder (--print-default-lib-dir): it predates the per-target layout"
exit 1
fi

OBJ=$ROOT/__build/obj-$ARCH-$MODE-$MM
LIB_OUT=$ROOT/__build/$LIB_DIR
rm -rf "$OBJ" "$LIB_OUT"
mkdir -p "$OBJ" "$LIB_OUT"

TSC="$TSLANG -mtriple=$TRIPLE $DBG -mm=$MM --emit=obj --export=none --nowarn --no-default-lib"
$TSC "$ROOT/src/lib.wasm.ts" -o "$OBJ/lib.wasm.o" || exit 1
$TSC "$ROOT/src/lib.ts" -o "$OBJ/lib.o" || exit 1
"$LLVM_AR" rcs "$LIB_OUT/libTypeScriptDefaultLib.a" "$OBJ/lib.o" "$OBJ/lib.wasm.o" || exit 1
rm -rf "$OBJ"
done
done
"$TSLANG" --version > "$ROOT/__build/defaultlib/COMPILER_VERSION.$ARCH.txt" 2>&1
done

# the declarations a program compiled against this tree reads (the same for every target)
mkdir -p "$ROOT/__build/defaultlib"
cp -r "$ROOT/src/"* "$ROOT/__build/defaultlib/"
140 changes: 140 additions & 0 deletions src/lib.wasm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/// <reference path="types/lib.types.d.ts" />

// The OS layer for wasm32-unknown-unknown and wasm64-unknown-unknown: the counterpart of
// lib.linux.ts and lib.win32.ts. These targets have no C library. tslang links a wasm program with
// --allow-undefined, so every C function the library calls is an import from the `env` module and
// the host that instantiates the module (a JavaScript runtime) implements it. Only functions are
// imported that way: undefined data is never resolved, so nothing here declares a C variable (on
// Linux gettimezone reads libc's `timezone`; here it asks the host).
//
// struct tm is the nine ints alone: the host writes and reads only those.

type time_t = long;

// returns int, as the snprintf tslang itself calls for number-to-string: wasm-ld checks that the
// two declarations agree
@varargs
@nodrops declare function snprintf(out: string, n: index, format: string): int;

export function convertNumber(bufferSize: int, format: string, value: number): string
{
let buffer : char[] = [];
buffer.length = bufferSize;
const s = <string> <Opaque> Ref(buffer[0]);
snprintf(s, bufferSize, format, value);
return s;
}

// `value: long` as core.os.d.ts declares it (wasm-ld checks the signatures); its callers' formats
// are %d, so it is passed on as an int
export function convertInteger(bufferSize: int, format: string, value: long): string
{
let buffer : char[] = [];
buffer.length = bufferSize;
const s = <string> <Opaque> Ref(buffer[0]);
snprintf(s, bufferSize, format, <i32>value);
return s;
}

// Date & Time
export function init_time() {
}

// milliseconds since the epoch (Date.now())
@nodrops declare function tslang_wasm_now(): number;
export function getMilliseconds(): i64 {
return <i64>tslang_wasm_now();
}

type tm = [tm_sec: i32, tm_min: i32, tm_hour: i32, tm_mday: i32, tm_mon: i32, tm_year: i32, tm_wday: i32, tm_yday: i32, tm_isdst: i32];

// seconds since the epoch, or -1; tm_year counts from 1900 and tm_mon from 0, as in C
@nodrops declare function timegm(tv: Reference<tm>): long;
export function makegmtime(year: i32, month: i32, day: i32, hour: i32, minutes: i32, seconds: i32, milliseconds: i32): i64 {
let tm1: tm = [seconds, minutes, hour, day, month, year, 0, 0, 0];
const sec = timegm(Ref(tm1));
if (sec == -1)
{
return sec;
}

return sec * 1000 + milliseconds;
}

@nodrops declare function mktime(tv: Reference<tm>): long;
export function maketime(year: i32, month: i32, day: i32, hour: i32, minutes: i32, seconds: i32, milliseconds: i32): i64 {
let tm1: tm = [seconds, minutes, hour, day, month, year, 0, 0, -1];
const sec = mktime(Ref(tm1));
if (sec == -1)
{
return sec;
}

return sec * 1000 + milliseconds;
}

@nodrops declare function gmtime_r(time: Reference<time_t>, tm: Reference<tm>) : Reference<tm>;
@nodrops declare function localtime_r(time: Reference<time_t>, tm: Reference<tm>) : Reference<tm>;
function c_time(time: long, isUtc: boolean): tm {
let tmDest: tm = [0, 0, 0, 0, 0, 0, 0, 0, 0];
let timeInSec: time_t = time / 1000;
if (isUtc) {
gmtime_r(Ref(timeInSec), Ref(tmDest));
} else {
localtime_r(Ref(timeInSec), Ref(tmDest));
}

return tmDest;
}

export function gmtime_tm(time: long): tm {
return c_time(time, true);
}

export function localtime_tm(time: long): tm {
return c_time(time, false);
}

// seconds west of UTC, as libc's `timezone`
@nodrops declare function tslang_wasm_timezone(): i32;
export function gettimezone(): i32 {
return tslang_wasm_timezone();
}

// returns `buffer`: a pointer, not copied
@nodrops declare function ctime_r(time: Reference<time_t>, buffer: string): Opaque;
export function timestamp_to_string(maxsize: index, time: long): string {
let timeInSec: time_t = time / 1000;
let buffer : char[] = [];
buffer.length = maxsize;
const s = <string> <Opaque> Ref(buffer[0]);
const result = ctime_r(Ref(timeInSec), s);
return s;
}

@nodrops declare function asctime_r(time: Reference<tm>, buffer: string): Opaque;
export function time_to_string(maxsize: index, time: long, isUtc: boolean): string {
let tm = c_time(time, isUtc);

let buffer : char[] = [];
buffer.length = maxsize;
const s = <string> <Opaque> Ref(buffer[0]);
const result = asctime_r(Ref(tm), s);
return s;
}

@nodrops declare function strftime(out: string, maxsize: index, format: string, tm: Reference<tm>): index;
export function time_format(maxsize: index, format: string, time: long, isUtc: boolean): string {
let tm = c_time(time, isUtc);

let buffer : char[] = [];
buffer.length = maxsize;
const s = <string> <Opaque> Ref(buffer[0]);
const len = strftime(s, maxsize, format, Ref(tm));
return s;
}

// the host has one locale: the format is the same whatever `locale` names
export function time_format_locale(maxsize: index, format: string, time: long, locale: string, isUtc: boolean): string {
return time_format(maxsize, format, time, isUtc);
}
Loading