diff --git a/README.md b/README.md index 518d175..14609dd 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,23 @@ repo: `scripts\build_tslang_runtime__x86.bat` in `TypeScriptCompiler`, which puts it in `__build\tslang-runtime\\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\\unknown\unknown\unknown\\\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`: diff --git a/scripts/build_wasm.bat b/scripts/build_wasm.bat new file mode 100644 index 0000000..7542b86 --- /dev/null +++ b/scripts/build_wasm.bat @@ -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\\unknown\unknown\unknown\\\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 diff --git a/scripts/build_wasm.sh b/scripts/build_wasm.sh new file mode 100755 index 0000000..099c798 --- /dev/null +++ b/scripts/build_wasm.sh @@ -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//unknown/unknown/unknown///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/" diff --git a/src/lib.wasm.ts b/src/lib.wasm.ts new file mode 100644 index 0000000..f7fb7e3 --- /dev/null +++ b/src/lib.wasm.ts @@ -0,0 +1,140 @@ +/// + +// 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 = 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 = Ref(buffer[0]); + snprintf(s, bufferSize, format, 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 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): 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): 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, tm: Reference) : Reference; +@nodrops declare function localtime_r(time: Reference, tm: Reference) : Reference; +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, 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 = Ref(buffer[0]); + const result = ctime_r(Ref(timeInSec), s); + return s; +} + +@nodrops declare function asctime_r(time: Reference, 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 = Ref(buffer[0]); + const result = asctime_r(Ref(tm), s); + return s; +} + +@nodrops declare function strftime(out: string, maxsize: index, format: string, tm: Reference): 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 = 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); +}