diff --git a/.gitignore b/.gitignore index 501884c..34f496d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ *.lib *.pdb *.log -__jit/ \ No newline at end of file +__jit/ +__fs_test/ diff --git a/README.md b/README.md index ab6ba06..518d175 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,7 @@ A test that reads standard input (`console.readLine`) gets it from `tests/ ## Docs - [fetch / Headers / Response](docs/fetch.md) — built-in HTTP client +- [process / fs](docs/process-fs.md) — the command line, the environment and the file system ## Implemented classes & functions @@ -145,7 +146,7 @@ Globals (`src/lib.ts`): - `Date` - `RegExp`, `MatchResults`, `MatchIndicesResults` - `String`: `String.fromCharCode` (each code written as UTF-8, a surrogate pair as one - character), plus the `string` prototype methods it wraps: `at`, `charAt`, `charCodeAt`, + character), `String.fromCodePoint` (each code point as UTF-8; outside 0 to 0x10FFFF a `RangeError`), plus the `string` prototype methods it wraps: `at`, `charAt`, `charCodeAt`, `codePointAt`, `concat`, `endsWith`, `includes`, `indexOf`, `lastIndexOf`, `localeCompare`, `match`, `matchAll`, `normalize`, `padEnd`, `padStart`, `repeat`, `replace`, `replaceAll`, `search`, `slice`, `split`, `startsWith`, `substring`, @@ -192,7 +193,10 @@ Generics (`src/generics/lib.generics.ts`): `Int16Array`, `Uint16Array`, `Int32Array`, `Uint32Array`, `BigInt64Array`, `BigUint64Array`, `Float32Array`, `Float64Array`). A typed array is made from an array of its elements (`new Uint8Array([1, 2, 3])`) or a length (`new Uint8Array(4096)`, each element 0) -- `Map` +- `T[]` and `Array` also have `flatMap`, `toSpliced` (`splice` on a copy), and `T[][]` has + `flat()`, which takes away one level only: a depth would make the result's type depend on a number +- `Map`, and `Map.groupBy(items, keySelector)`, a `Map` of each key to its items, the keys in + the order they first come (not under `-mm=own`, whose Map values cannot be arrays) - `Set` - `Atomics`: `add`, `sub`, `and`, `or`, `xor`, `exchange`, `compareExchange`, `load`, `store`, `isLockFree` over the integer typed arrays, and `wait`/`notify` over an @@ -290,6 +294,25 @@ Generics (`src/generics/lib.generics.ts`): const text = new TextDecoder().decode(Stdio.readAll()); Stdio.write(new TextEncoder().encode(text.toUpperCase())); ``` +- `process`, as Node's, with functions where Node has properties: `argv()`, `getEnv(name)` (or + `undefined`), `setEnv`, `unsetEnv`, `env()` (`[name, value]` pairs), `cwd()`, `chdir`, `pid()`, `platform()`, + `exit(code)`. See [docs/process-fs.md](docs/process-fs.md). +- `fs`, synchronous, by Node's names: `existsSync`, `statSync`, `readFileSync` (a `Uint8Array`), + `readTextFileSync` (a string), `writeFileSync`, `writeTextFileSync`, `appendFileSync`, + `appendTextFileSync`, `readdirSync` (sorted), `mkdirSync` (`{ recursive }`), `rmSync` + (`{ recursive, force }`), `rmdirSync`, `renameSync`, `copyFileSync`. An error is an `Error` with + Node's message, `"ENOENT: no such file or directory, open 'a.txt'"`. See + [docs/process-fs.md](docs/process-fs.md). + + ```ts + const args = process.argv(); + const name = args[args.length - 1]; + if (!fs.existsSync(name)) { + console.error(`${name}: not found`); + process.exit(1); + } + fs.writeTextFileSync(name + ".upper", fs.readTextFileSync(name).toUpperCase()); + ``` Errors (`src/core/core.d.ts`): diff --git a/docs/process-fs.md b/docs/process-fs.md new file mode 100644 index 0000000..9fe13c1 --- /dev/null +++ b/docs/process-fs.md @@ -0,0 +1,103 @@ +# process / fs + +The command line, the environment, the working directory and the file system, synchronously. Both +are namespaces in `src/generics/lib.generics.ts` over the C++ wrapper `src/wrappers/os.cpp`. + +## process + +Node's `process`, with functions where Node has properties (a namespace in the library holds no +state of its own, so `argv` and `env` are read when they are asked for): + +```ts +namespace process { + function argv(): string[]; // Node: process.argv + function getEnv(name: string): string | undefined; // Node: process.env[name] + function setEnv(name: string, value: string): void; + function unsetEnv(name: string): void; // Node: delete process.env[name] + function env(): [string, string][]; // every variable, [name, value] + function cwd(): string; + function chdir(directory: string): void; + function pid(): int; + function platform(): string; // "win32", "linux", "android", "darwin" + function exit(code?: int): void; +} +``` + +- `argv()[0]` is the program. A compiled program's arguments follow it. Under the JIT the program + is `tslang`, so its options come next, then the script, then the script's own arguments - the way + `node script.js a b` gives `["node", "script.js", "a", "b"]`. On Linux and Android the arguments + are read from `/proc/self/cmdline`, on Windows from `GetCommandLineW`. +- `setEnv` and `unsetEnv` change the environment of this process and of the processes it starts. + A name that is empty or has an `=` in it is an `Error` (`EINVAL`). Unsetting a variable that is + not set is no error. On Windows the variables are read and written through the Win32 API, which + every C runtime in the process shares (under the JIT this library has a C runtime of its own), + and names are case-insensitive there, as always on Windows. +- `exit(code)` flushes every C stream and ends the process: nothing after it runs, no `finally` + block either. Under the JIT on Windows it flushes this library's streams (`console.log`, + `Stdio`), not the host's, so text from `print()` not yet flushed may be lost there. + +## fs + +```ts +namespace fs { + function existsSync(path: string): boolean; + function statSync(path: string): FileStats; // { isFile, isDirectory, size, mtimeMs } + function readFileSync(path: string): Uint8Array; + function readTextFileSync(path: string): string; + function writeFileSync(path: string, data: Uint8Array): void; + function writeTextFileSync(path: string, text: string): void; + function appendFileSync(path: string, data: Uint8Array): void; + function appendTextFileSync(path: string, text: string): void; + function readdirSync(path: string): string[]; + function mkdirSync(path: string, options?: { recursive?: boolean }): void; + function rmSync(path: string, options?: { recursive?: boolean, force?: boolean }): void; + function rmdirSync(path: string): void; + function renameSync(oldPath: string, newPath: string): void; + function copyFileSync(src: string, dest: string): void; +} +``` + +- The names are Node's. Node's `readFileSync(path, "utf8")` and its `writeFileSync` that takes a + string are `readTextFileSync` and `writeTextFileSync` here (Deno's names), since a function here + has one result type: `readFileSync` gives bytes, `writeFileSync` takes them. +- Text is UTF-8, and a string ends at its first 0, so a file that is not text is read with + `readFileSync`. Paths are UTF-8 on every platform; on Windows they go through the wide API, so a + name outside the ANSI code page works. +- `statSync` follows symbolic links. `size` is in bytes; `mtimeMs` is the last modification in + milliseconds since 1970, as `new Date(ms)` takes it (to the second on Windows). +- `readdirSync` gives the names in the directory, without `.` and `..`, sorted: the operating system + gives them in no set order. +- `mkdirSync` without `recursive` needs the parent, and a directory already there is `EEXIST`; with + it every missing parent is made too and a directory already there is no error. +- `rmSync` removes a file; a directory only with `recursive`, and then all it holds (`EISDIR` + without). With `force` a path that is not there is no error. `rmdirSync` removes an empty + directory. +- `renameSync` and `copyFileSync` replace a file already at the destination. +- Files of 2 GB or more are an `EFBIG` error: lengths are an `int`, as `Stdio`'s are. + +An error is an `Error` whose message is Node's: the code, what it means, the call and the path. + +```ts +try { + fs.readTextFileSync("settings.json"); +} catch (e: Error) { + console.log(e.message); // ENOENT: no such file or directory, open 'settings.json' +} +``` + +The code is one of `ENOENT`, `EEXIST`, `EACCES`, `EPERM`, `ENOTDIR`, `EISDIR`, `ENOTEMPTY`, `EBUSY`, +`EINVAL`, `ENAMETOOLONG`, `EXDEV`, `EFBIG`, `ENOSPC`, `EROFS`, `EMFILE`, or `EIO` for any other; what +it means is the C runtime's wording, so its letter case differs between platforms. + +## Example + +```ts +function main() { + const args = process.argv(); + const dir = args.length > 1 ? args[args.length - 1] : process.cwd(); + for (const name of fs.readdirSync(dir)) { + const stats = fs.statSync(dir + "/" + name); + console.log(stats.isDirectory ? `${name}/` : `${name} ${stats.size}`); + } +} +``` diff --git a/scripts/build.sh b/scripts/build.sh index d1cf8e4..99b5bd3 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -87,12 +87,13 @@ $TOOL $DBG_GCC $CPP_FLAGS -c $SRC/src/wrappers/regex.cpp -o $OUTPUT/$LIB_OUT/reg $TOOL $DBG_GCC $CPP_FLAGS $WRAPPER_MM_DEF -c $SRC/src/wrappers/thread.cpp -o $OUTPUT/$LIB_OUT/thread.o $TOOL $DBG_GCC $CPP_FLAGS -c $SRC/src/wrappers/http_linux.cpp -o $OUTPUT/$LIB_OUT/http_linux.o $TOOL $DBG_GCC $CPP_FLAGS -c $SRC/src/wrappers/memory.cpp -o $OUTPUT/$LIB_OUT/memory.o +$TOOL $DBG_GCC $CPP_FLAGS -c $SRC/src/wrappers/os.cpp -o $OUTPUT/$LIB_OUT/os.o $BIN_PATH/$TOOL_NAME $DBG_OPTS $MM_OPT --emit=obj --export=none --nowarn --no-default-lib $SRC/src/lib.linux.ts $PIC -o $OUTPUT/$LIB_OUT/lib.linux.o # Build Lib $BIN_PATH/$TOOL_NAME $DBG_OPTS $MM_OPT --emit=obj --export=none --nowarn --no-default-lib $SRC/src/lib.ts $PIC -o $OUTPUT/$LIB_OUT/lib.o -$ARC rcs $OUTPUT/$LIB_OUT/libTypeScriptDefaultLibCore.a $OUTPUT/$LIB_OUT/lib.o $OUTPUT/$LIB_OUT/lib.linux.o $OUTPUT/$LIB_OUT/io.o $OUTPUT/$LIB_OUT/datetime.o $OUTPUT/$LIB_OUT/regex.o $OUTPUT/$LIB_OUT/thread.o $OUTPUT/$LIB_OUT/http_linux.o $OUTPUT/$LIB_OUT/memory.o +$ARC rcs $OUTPUT/$LIB_OUT/libTypeScriptDefaultLibCore.a $OUTPUT/$LIB_OUT/lib.o $OUTPUT/$LIB_OUT/lib.linux.o $OUTPUT/$LIB_OUT/io.o $OUTPUT/$LIB_OUT/datetime.o $OUTPUT/$LIB_OUT/regex.o $OUTPUT/$LIB_OUT/thread.o $OUTPUT/$LIB_OUT/http_linux.o $OUTPUT/$LIB_OUT/memory.o $OUTPUT/$LIB_OUT/os.o # A static archive cannot record its own dependencies on ELF (there is no ld.bfd equivalent of # the `#pragma comment(lib, ...)` http.cpp uses on Windows), so programs linking @@ -110,7 +111,7 @@ EOF # undefined on purpose: they must bind to the one collector already in the process # (libTypeScriptRuntime.so under the JIT, libgc.a in an exe); linking libgc.a in here would # give the library a second, separate heap. -g++ -shared $DBG_GCC $OUTPUT/$LIB_OUT/lib.o $OUTPUT/$LIB_OUT/lib.linux.o $OUTPUT/$LIB_OUT/io.o $OUTPUT/$LIB_OUT/datetime.o $OUTPUT/$LIB_OUT/regex.o $OUTPUT/$LIB_OUT/thread.o $OUTPUT/$LIB_OUT/http_linux.o $OUTPUT/$LIB_OUT/memory.o -lcurl -lm -o $OUTPUT/$DLL_OUT/libTypeScriptDefaultLib.so +g++ -shared $DBG_GCC $OUTPUT/$LIB_OUT/lib.o $OUTPUT/$LIB_OUT/lib.linux.o $OUTPUT/$LIB_OUT/io.o $OUTPUT/$LIB_OUT/datetime.o $OUTPUT/$LIB_OUT/regex.o $OUTPUT/$LIB_OUT/thread.o $OUTPUT/$LIB_OUT/http_linux.o $OUTPUT/$LIB_OUT/memory.o $OUTPUT/$LIB_OUT/os.o -lcurl -lm -o $OUTPUT/$DLL_OUT/libTypeScriptDefaultLib.so # Copy # Stage into a single shared defaultlib tree with per-target, per-build and per-model subfolders diff --git a/scripts/build_android.bat b/scripts/build_android.bat index 99e0653..b08802c 100644 --- a/scripts/build_android.bat +++ b/scripts/build_android.bat @@ -120,14 +120,14 @@ md "%OBJ%" "%LIB_OUT%" rem thread.cpp registers the threads it starts with the collector, which only the gc build links set WRAPPER_MM_DEF= if "%MM%"=="gc" set WRAPPER_MM_DEF=-DTSLANG_MM_GC -for %%w in (io datetime regex thread http_stub memory) do ( +for %%w in (io datetime regex thread http_stub memory os) do ( %CXX% %WRAPPER_MM_DEF% -c "%ROOT%\src\wrappers\%%w.cpp" -o "%OBJ%\%%w.o" || exit /b 1 ) %TSC% "%ROOT%\src\lib.linux.ts" -o "%OBJ%\lib.linux.o" || exit /b 1 %TSC% "%ROOT%\src\lib.ts" -o "%OBJ%\lib.o" || exit /b 1 -set OBJS="%OBJ%\lib.o" "%OBJ%\lib.linux.o" "%OBJ%\io.o" "%OBJ%\datetime.o" "%OBJ%\regex.o" "%OBJ%\thread.o" "%OBJ%\http_stub.o" "%OBJ%\memory.o" +set OBJS="%OBJ%\lib.o" "%OBJ%\lib.linux.o" "%OBJ%\io.o" "%OBJ%\datetime.o" "%OBJ%\regex.o" "%OBJ%\thread.o" "%OBJ%\http_stub.o" "%OBJ%\memory.o" "%OBJ%\os.o" %AR% rcs "%LIB_OUT%\libTypeScriptDefaultLib.a" %OBJS% || exit /b 1 rd /s /q "%OBJ%" diff --git a/scripts/build_core.bat b/scripts/build_core.bat index a167b7f..99bc060 100644 --- a/scripts/build_core.bat +++ b/scripts/build_core.bat @@ -225,6 +225,7 @@ rem Build native wrappers for C++ code %TSLANG_CC% %DBG_CL% /EHsc /Wall %WRAPPER_MM_DEF% /c /Fo%OUTPUT%\%LIB_OUT%\ %SRC%\src\wrappers\thread.cpp %TSLANG_CC% %DBG_CL% /EHsc /Wall /c /Fo%OUTPUT%\%LIB_OUT%\ %SRC%\src\wrappers\http.cpp %TSLANG_CC% %DBG_CL% /EHsc /Wall /c /Fo%OUTPUT%\%LIB_OUT%\ %SRC%\src\wrappers\memory.cpp +%TSLANG_CC% %DBG_CL% /EHsc /Wall /c /Fo%OUTPUT%\%LIB_OUT%\ %SRC%\src\wrappers\os.cpp rem Build OS-specific Lib echo Build OS-specific Lib @@ -232,7 +233,7 @@ echo Build OS-specific Lib rem Build DLL echo Build DLL -%TOOL_PATH%\%TOOL_NAME%.exe %TRIPLE_OPT% %DBG% %MM_OPT% --emit=dll --gc-lib-path=%GC_SHARED_LIB_PATH% %TSLANG_RUNTIME_OPT% --embed-declarations=false --nowarn --no-default-lib %SRC%\src\lib.ts --obj=%OUTPUT%\%LIB_OUT%\lib.win32.obj --obj=%OUTPUT%\%LIB_OUT%\io.obj --obj=%OUTPUT%\%LIB_OUT%\datetime.obj --obj=%OUTPUT%\%LIB_OUT%\regex.obj --obj=%OUTPUT%\%LIB_OUT%\thread.obj --obj=%OUTPUT%\%LIB_OUT%\http.obj --obj=%OUTPUT%\%LIB_OUT%\memory.obj -o %OUTPUT%\%DLL_OUT%\TypeScriptDefaultLib.dll +%TOOL_PATH%\%TOOL_NAME%.exe %TRIPLE_OPT% %DBG% %MM_OPT% --emit=dll --gc-lib-path=%GC_SHARED_LIB_PATH% %TSLANG_RUNTIME_OPT% --embed-declarations=false --nowarn --no-default-lib %SRC%\src\lib.ts --obj=%OUTPUT%\%LIB_OUT%\lib.win32.obj --obj=%OUTPUT%\%LIB_OUT%\io.obj --obj=%OUTPUT%\%LIB_OUT%\datetime.obj --obj=%OUTPUT%\%LIB_OUT%\regex.obj --obj=%OUTPUT%\%LIB_OUT%\thread.obj --obj=%OUTPUT%\%LIB_OUT%\http.obj --obj=%OUTPUT%\%LIB_OUT%\memory.obj --obj=%OUTPUT%\%LIB_OUT%\os.obj -o %OUTPUT%\%DLL_OUT%\TypeScriptDefaultLib.dll rem Build Lib echo Build Lib @@ -240,7 +241,7 @@ echo Build Lib rem %TOOL_PATH%\%TOOL_NAME%.exe %TRIPLE_OPT% %DBG% %MM_OPT% --emit=llvm --export=none %SRC%\src\lib.ts -o %OUTPUT%\%LIB_OUT%\lib.ll rem %TOOL_PATH%\%TOOL_NAME%.exe %TRIPLE_OPT% %DBG% %MM_OPT% --emit=mlir --export=none %SRC%\src\lib.ts 2> %OUTPUT%\%LIB_OUT%\lib.mlir -%TSLANG_AR% /out:%OUTPUT%\%LIB_OUT%\TypeScriptDefaultLib.lib %OUTPUT%\%LIB_OUT%\lib.obj %OUTPUT%\%LIB_OUT%\lib.win32.obj %OUTPUT%\%LIB_OUT%\io.obj %OUTPUT%\%LIB_OUT%\datetime.obj %OUTPUT%\%LIB_OUT%\regex.obj %OUTPUT%\%LIB_OUT%\thread.obj %OUTPUT%\%LIB_OUT%\http.obj %OUTPUT%\%LIB_OUT%\memory.obj +%TSLANG_AR% /out:%OUTPUT%\%LIB_OUT%\TypeScriptDefaultLib.lib %OUTPUT%\%LIB_OUT%\lib.obj %OUTPUT%\%LIB_OUT%\lib.win32.obj %OUTPUT%\%LIB_OUT%\io.obj %OUTPUT%\%LIB_OUT%\datetime.obj %OUTPUT%\%LIB_OUT%\regex.obj %OUTPUT%\%LIB_OUT%\thread.obj %OUTPUT%\%LIB_OUT%\http.obj %OUTPUT%\%LIB_OUT%\memory.obj %OUTPUT%\%LIB_OUT%\os.obj del %OUTPUT%\%LIB_OUT%\lib.obj del %OUTPUT%\%LIB_OUT%\lib.win32.obj @@ -250,6 +251,7 @@ del %OUTPUT%\%LIB_OUT%\regex.obj del %OUTPUT%\%LIB_OUT%\thread.obj del %OUTPUT%\%LIB_OUT%\http.obj del %OUTPUT%\%LIB_OUT%\memory.obj +del %OUTPUT%\%LIB_OUT%\os.obj rem Stage into a single shared defaultlib tree with per-target, per-build, per-model subfolders rem under dll\ and lib\. Only the current build's subfolders are refreshed so the other diff --git a/src/generics/lib.generics.ts b/src/generics/lib.generics.ts index 05cac58..322dba2 100644 --- a/src/generics/lib.generics.ts +++ b/src/generics/lib.generics.ts @@ -34,6 +34,31 @@ function __is_any(t: V): t is V { return true; } +// What toSpliced gives, for T[] and Array alike: a copy of `source` with `deleteCount` elements at +// `start` replaced by `items`. A negative start counts from the end; both are clamped into range. +// The items are moved in (their caller gives them up, as push's do); the rest are shared, as slice's. +function __toSpliced(source: T[], start: int, deleteCount: int, items: T[]): T[] { + const length = source.length; + if (start < 0) { + start = start + length < 0 ? 0 : start + length; + } else if (start > length) { + start = length; + } + + let count = deleteCount; + if (count < 0) { + count = 0; + } else if (count > length - start) { + count = length - start; + } + + const result: T[] = []; + for (let i = 0; i < start; i++) result.push(source[i]); + while (items.length > 0) result.push(items.shift()); + for (let i = start + count; i < length; i++) result.push(source[i]); + return result; +} + namespace __Array { function at(this: T[], index: int) { return this[index]; @@ -457,6 +482,38 @@ namespace __Array { newArray[index] = value; return newArray; } + + // One level of nesting taken away: [[1, 2], [3]].flat() is [1, 2, 3]. Only one level: a depth + // would make the result's type depend on a number. + @sharesElements + function flat(this: T[][]): T[] { + const result: T[] = []; + for (const inner of this) { + for (const v of inner) result.push(v); + } + + return result; + } + + // map, then flat: each element becomes the elements of the array `func` gives for it + @sharesElements + function flatMap(this: T[], func: (v: T, i: number) => V[]): V[] { + const result: V[] = []; + for (let i = 0; i < this.length; i++) { + // moved out of the array func made, which goes when this loop turns + const mapped = func(this[i], i); + while (mapped.length > 0) result.push(mapped.shift()); + } + + return result; + } + + // splice on a copy: the array itself stays as it is. Without deleteCount every element from + // `start` on goes. + @sharesElements + function toSpliced(this: T[], start: int, deleteCount?: int, ...items: T[]): T[] { + return __toSpliced(this, start, deleteCount === undefined ? this.length : deleteCount, items); + } } // -mm=own: the array owns its elements. push, unshift and splice take what they are given (their @@ -699,6 +756,16 @@ class Array { public _with(index: int, value: T) { return this.data._with(index, value); } + + @sharesElements + public flatMap(func: (v: T, i: number) => V[]): V[] { + return this.data.flatMap(func); + } + + @sharesElements + public toSpliced(start: int, deleteCount?: int, ...items: T[]): T[] { + return __toSpliced(this.data, start, deleteCount === undefined ? this.data.length : deleteCount, items); + } } static class Array { @@ -1304,6 +1371,302 @@ namespace Stdio { } } +// The text an os call left (wrappers/os.cpp), `length` bytes of it, as a string of its own. +// T is the length's type, taken from the argument: a type parameter nothing names can't be +// worked out where one generic function calls another. +function __os_text(length: T): string { + const buffer: u8[] = []; + buffer.length = length + 1; + buffer[length] = 0; + if (length > 0) { + tslang_os_result_copy_to(Ref(buffer[0]), length); + } + + // a copy: buffer goes with this function + return Ref(buffer[0]) + ""; +} + +// The same, as bytes. +function __os_bytes(length: T): Uint8Array { + const bytes: u8[] = []; + bytes.length = length; + if (length > 0) { + tslang_os_result_copy_to(Ref(bytes[0]), length); + } + + return new Uint8Array(bytes); +} + +// The error an os call ran into, in Node's words: "ENOENT: no such file or directory, open 'a.txt'". +// `length` is tslang_os_result_length() +function __os_error(length: T): Error { + return new Error(__os_text(length)); +} + +// The process, as Node's `process` has it, with functions where Node has properties: process.argv() +// rather than process.argv. See docs/process-fs.md. +namespace process { + // The command line: argv()[0] is the program. Under the JIT the program is tslang, so its + // options come next, then the script, then the script's arguments - as with `node script.js`. + function argv(): string[] { + const args: string[] = []; + const count = tslang_process_argc(); + for (let i = 0; i < count; i++) { + args.push(__os_text(tslang_process_argv(i))); + } + + return args; + } + + // the variable's value, or undefined when it is not set + function getEnv(name: string): string | undefined { + const length = tslang_process_getenv(name); + if (length < 0) { + return undefined; + } + + return __os_text(length); + } + + // sets the variable for this process and the processes it starts; an empty name, or one with + // an "=", is an Error + function setEnv(name: string, value: string): void { + if (tslang_process_setenv(name, value) != 0) { + throw __os_error(tslang_os_result_length()); + } + } + + // a variable that is not set is not an error + function unsetEnv(name: string): void { + if (tslang_process_unsetenv(name) != 0) { + throw __os_error(tslang_os_result_length()); + } + } + + // Every variable, [name, value]. Not a Map: this is above the Map class, and naming + // Map here made that type before Map was declared - the program's own + // Map then had no members. + function env(): [string, string][] { + const vars: [string, string][] = []; + const count = tslang_process_env_count(); + for (let i = 0; i < count; i++) { + const entry = __os_text(tslang_os_entry(i)); + const equals = entry.indexOf("="); + if (equals > 0) { + // typed: a bare literal is a string[], not a pair + const pair: [string, string] = [entry.substring(0, equals), entry.substring(equals + 1)]; + vars.push(pair); + } + } + + return vars; + } + + // the working directory + function cwd(): string { + const length = tslang_process_cwd(); + if (length < 0) { + throw __os_error(tslang_os_result_length()); + } + + return __os_text(length); + } + + function chdir(directory: string): void { + if (tslang_process_chdir(directory) != 0) { + throw __os_error(tslang_os_result_length()); + } + } + + function pid(): int { + return tslang_process_pid(); + } + + // "win32", "linux", "android" or "darwin", as Node's process.platform + function platform(): string { + return __os_text(tslang_process_platform()); + } + + // Ends the process with `code`, once every stream is flushed. Nothing after it runs: no + // finally block, no destructor. + function exit(code = 0): void { + tslang_process_exit(code); + } +} + +// What fs.statSync tells of a path; symbolic links are followed. +interface FileStats { + isFile: boolean; + isDirectory: boolean; + // in bytes + size: number; + // the last modification, in milliseconds since 1970 as Date takes them + mtimeMs: number; +} + +interface MkdirOptions { + // every missing parent too, and a directory already there is no error + recursive?: boolean; +} + +interface RmOptions { + // a directory, and all it holds + recursive?: boolean; + // nothing there is no error + force?: boolean; +} + +// The file system, synchronously, by Node's names: readFileSync gives bytes, readTextFileSync (as +// Deno names it) a string. An error is an Error with Node's message. Paths are UTF-8. +// See docs/process-fs.md. +namespace fs { + function existsSync(path: string): boolean { + let size: number = 0; + let mtimeMs: number = 0; + return tslang_fs_stat(path, Ref(size), Ref(mtimeMs)) > 0; + } + + function statSync(path: string): FileStats { + let size: number = 0; + let mtimeMs: number = 0; + const kind = tslang_fs_stat(path, Ref(size), Ref(mtimeMs)); + if (kind < 0) { + throw __os_error(tslang_os_result_length()); + } + + return { isFile: kind == 1, isDirectory: kind == 2, size: size, mtimeMs: mtimeMs }; + } + + // the file's bytes + function readFileSync(path: string): Uint8Array { + const length = tslang_fs_read_file(path); + if (length < 0) { + throw __os_error(tslang_os_result_length()); + } + + return __os_bytes(length); + } + + // The file as text, UTF-8. A string ends at its first 0, so a 0 byte ends the text there: + // readFileSync for a file that is not text. + function readTextFileSync(path: string): string { + const length = tslang_fs_read_file(path); + if (length < 0) { + throw __os_error(tslang_os_result_length()); + } + + return __os_text(length); + } + + // Writes the bytes to the file, which it creates or else empties first. + function writeFileSync(path: string, data: Uint8Array): void { + const empty = ""; + const status = data.length > 0 + ? tslang_fs_write_file(path, data.__elementRef(0), data.length, 0) + : tslang_fs_write_file(path, empty, 0, 0); + if (status != 0) { + throw __os_error(tslang_os_result_length()); + } + } + + function writeTextFileSync(path: string, text: string): void { + if (tslang_fs_write_file(path, text, text.length, 0) != 0) { + throw __os_error(tslang_os_result_length()); + } + } + + // Adds the bytes at the file's end; a file that is not there is created. + function appendFileSync(path: string, data: Uint8Array): void { + if (data.length == 0) { + const empty = ""; + if (tslang_fs_write_file(path, empty, 0, 1) != 0) { + throw __os_error(tslang_os_result_length()); + } + + return; + } + + if (tslang_fs_write_file(path, data.__elementRef(0), data.length, 1) != 0) { + throw __os_error(tslang_os_result_length()); + } + } + + function appendTextFileSync(path: string, text: string): void { + if (tslang_fs_write_file(path, text, text.length, 1) != 0) { + throw __os_error(tslang_os_result_length()); + } + } + + // the names in the directory, sorted; not "." or ".." + function readdirSync(path: string): string[] { + const count = tslang_fs_read_dir(path); + if (count < 0) { + throw __os_error(tslang_os_result_length()); + } + + const names: string[] = []; + for (let i = 0; i < count; i++) { + names.push(__os_text(tslang_os_entry(i))); + } + + return names; + } + + function mkdirSync(path: string, options?: MkdirOptions): void { + let recursive = false; + if (options != undefined) { + if (options.recursive != undefined) { + recursive = options.recursive; + } + } + + if (tslang_fs_mkdir(path, recursive ? 1 : 0) != 0) { + throw __os_error(tslang_os_result_length()); + } + } + + // A file, or with `recursive` a directory and all it holds. With `force` a path that is not + // there is no error. + function rmSync(path: string, options?: RmOptions): void { + let recursive = false; + let force = false; + if (options != undefined) { + if (options.recursive != undefined) { + recursive = options.recursive; + } + + if (options.force != undefined) { + force = options.force; + } + } + + if (tslang_fs_remove(path, recursive ? 1 : 0, force ? 1 : 0) != 0) { + throw __os_error(tslang_os_result_length()); + } + } + + // an empty directory + function rmdirSync(path: string): void { + if (tslang_fs_rmdir(path) != 0) { + throw __os_error(tslang_os_result_length()); + } + } + + // a file already at `newPath` is replaced + function renameSync(oldPath: string, newPath: string): void { + if (tslang_fs_rename(oldPath, newPath) != 0) { + throw __os_error(tslang_os_result_length()); + } + } + + // a file already at `dest` is replaced + function copyFileSync(src: string, dest: string): void { + if (tslang_fs_copy_file(src, dest) != 0) { + throw __os_error(tslang_os_result_length()); + } + } +} + // Strings are UTF-8 already, so encoding is a copy of their bytes. UTF-8 only, as in JavaScript. // Generic, as every class here is: a plain one would be built into the library and into every // program, and a debug link then finds it twice. `new TextEncoder()` takes the default. @@ -2040,6 +2403,37 @@ class Map { } +static class Map { + // The items in groups, by the key `keySelector` gives each: the groups in the order their keys + // first come, the items in each in their own order. Each group is made whole before it goes into + // the map, a pass over the items for each, rather than pushed to through map.get(key). + // Not under -mm=own, where a Map's values are values, strings, `any` or Shared, not arrays. + public groupBy(items: T[], keySelector: (item: T, index: number) => K): Map { + const keys: K[] = []; + for (let i = 0; i < items.length; i++) { + keys.push(keySelector(items[i], i)); + } + + const groups = new Map(); + for (let i = 0; i < keys.length; i++) { + if (groups.has(keys[i])) { + continue; + } + + const group: T[] = []; + for (let j = i; j < keys.length; j++) { + if (keys[j] == keys[i]) { + group.push(items[j]); + } + } + + groups.set(keys[i], group); + } + + return groups; + } +} + type SetEntry = { hashCode: uint, diff --git a/src/lib.d.ts b/src/lib.d.ts index eef1473..8c6cf35 100644 --- a/src/lib.d.ts +++ b/src/lib.d.ts @@ -375,6 +375,8 @@ declare class String { static fromCharCode(...codes: int[]): string; + static fromCodePoint(...codePoints: int[]): string; + at(index: int): string; charAt(this: string, index: int): string; diff --git a/src/lib.ts b/src/lib.ts index 368e8bb..32ad456 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -1353,6 +1353,25 @@ class StringIterator implements ClassIterator { } */ +// `code`, a code point, as its UTF-8 bytes +function utf8Push(buffer: char[], code: int): void { + if (code < 0x80) { + buffer.push(code); + } else if (code < 0x800) { + buffer.push((0xC0 | (code >> 6))); + buffer.push((0x80 | (code & 0x3F))); + } else if (code < 0x10000) { + buffer.push((0xE0 | (code >> 12))); + buffer.push((0x80 | ((code >> 6) & 0x3F))); + buffer.push((0x80 | (code & 0x3F))); + } else { + buffer.push((0xF0 | (code >> 18))); + buffer.push((0x80 | ((code >> 12) & 0x3F))); + buffer.push((0x80 | ((code >> 6) & 0x3F))); + buffer.push((0x80 | (code & 0x3F))); + } +} + @final export class String { @@ -1375,21 +1394,27 @@ export class String { } } - if (code < 0x80) { - buffer.push(code); - } else if (code < 0x800) { - buffer.push((0xC0 | (code >> 6))); - buffer.push((0x80 | (code & 0x3F))); - } else if (code < 0x10000) { - buffer.push((0xE0 | (code >> 12))); - buffer.push((0x80 | ((code >> 6) & 0x3F))); - buffer.push((0x80 | (code & 0x3F))); - } else { - buffer.push((0xF0 | (code >> 18))); - buffer.push((0x80 | ((code >> 12) & 0x3F))); - buffer.push((0x80 | ((code >> 6) & 0x3F))); - buffer.push((0x80 | (code & 0x3F))); + utf8Push(buffer, code); + } + + buffer.push(0); + // a copy: buffer goes with this function + return Ref(buffer[0]) + ""; + } + + // Each code point as its UTF-8 bytes (strings are UTF-8): `String.fromCodePoint(0x1F600)` is "šŸ˜€", + // one character of four bytes. A code point outside 0 to 0x10FFFF is a RangeError; a lone + // surrogate (0xD800 to 0xDFFF) is written as is, as JavaScript keeps it. A string ends at its + // first 0, so a 0 code point ends the result there. + public static fromCodePoint(...codePoints: int[]): string { + let buffer: char[] = []; + for (let i = 0; i < codePoints.length; i++) { + const code = codePoints[i]; + if (code < 0 || code > 0x10FFFF) { + throw new RangeError(`Invalid code point ${code}`); } + + utf8Push(buffer, code); } buffer.push(0); diff --git a/src/native/lib.native.d.ts b/src/native/lib.native.d.ts index c3b5179..6d3f0dd 100644 --- a/src/native/lib.native.d.ts +++ b/src/native/lib.native.d.ts @@ -149,3 +149,32 @@ declare function GC_gcollect(): void; @nodrops declare function http_response_body_length(r: Opaque): index; @nodrops declare function http_response_body_copy_to(r: Opaque, buffer: string, count: index): void; @nodrops declare function http_response_free(r: Opaque): void; + +// process and fs (wrappers/os.cpp). A call leaves its text - a file's bytes, an argument, an entry, +// or an error's message - in a per-thread buffer and gives its length, -1 for an error (or for +// "not there": an unset variable, an argument past the last); tslang_os_result_copy_to copies it out. +@nodrops declare function tslang_os_result_length(): int; +@nodrops declare function tslang_os_result_copy_to(buffer: Opaque, count: int): void; +// entry `i` of the last tslang_process_env_count or tslang_fs_read_dir +@nodrops declare function tslang_os_entry(i: int): int; +@nodrops declare function tslang_process_argc(): int; +@nodrops declare function tslang_process_argv(i: int): int; +@nodrops declare function tslang_process_getenv(name: string): int; +@nodrops declare function tslang_process_setenv(name: string, value: string): int; +@nodrops declare function tslang_process_unsetenv(name: string): int; +@nodrops declare function tslang_process_env_count(): int; +@nodrops declare function tslang_process_cwd(): int; +@nodrops declare function tslang_process_chdir(path: string): int; +@nodrops declare function tslang_process_pid(): int; +@nodrops declare function tslang_process_platform(): int; +@nodrops declare function tslang_process_exit(code: int): void; +@nodrops declare function tslang_fs_read_file(path: string): int; +@nodrops declare function tslang_fs_write_file(path: string, bytes: Opaque, count: int, append: int): int; +// 1 a file, 2 a directory, 3 something else, -1 an error +@nodrops declare function tslang_fs_stat(path: string, size: Reference, mtimeMs: Reference): int; +@nodrops declare function tslang_fs_read_dir(path: string): int; +@nodrops declare function tslang_fs_mkdir(path: string, recursive: int): int; +@nodrops declare function tslang_fs_remove(path: string, recursive: int, force: int): int; +@nodrops declare function tslang_fs_rmdir(path: string): int; +@nodrops declare function tslang_fs_rename(from: string, to: string): int; +@nodrops declare function tslang_fs_copy_file(from: string, to: string): int; diff --git a/src/wrappers/os.cpp b/src/wrappers/os.cpp new file mode 100644 index 0000000..91a99fc --- /dev/null +++ b/src/wrappers/os.cpp @@ -0,0 +1,772 @@ +// process and fs (src/generics/lib.generics.ts): the command line, the environment, the working +// directory and the file system, synchronously. +// +// The same shape as stdio_read_line and http_response_*: a call leaves its text - a file's bytes, +// an argument, a directory entry, or the message of the error it ran into - in a per-thread buffer +// and gives its length (-1 for an error); tslang_os_result_copy_to then copies it into a buffer the +// program owns. Lengths are an int, as Stdio's are: a file of 2 GB or more is an EFBIG error. +// +// Paths and every other string cross as UTF-8. On Windows they are turned into UTF-16 and the wide +// API is used throughout, so a name outside the ANSI code page works. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifdef _WIN32 +#include +#include +#pragma comment(lib, "shell32.lib") +#else +#include +extern char **environ; +#endif + +#include "tslang_export.h" + +namespace fs = std::filesystem; + +static thread_local std::string result; +// readdirSync's names and env()'s entries, taken in one go and handed out one by one +static thread_local std::vector entries; + +static int set_result(const std::string &text) +{ + result = text; + return static_cast(result.size()); +} + +// ---- UTF-8 <-> the platform's own strings ---- + +#ifdef _WIN32 +static std::wstring widen(const char *s) +{ + if (s == nullptr || *s == 0) + { + return std::wstring(); + } + + auto size = MultiByteToWideChar(CP_UTF8, 0, s, -1, nullptr, 0); + if (size <= 0) + { + return std::wstring(); + } + + std::wstring wide(static_cast(size), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, s, -1, &wide[0], size); + wide.resize(static_cast(size - 1)); + return wide; +} + +static std::string narrow(const wchar_t *s, int length = -1) +{ + if (s == nullptr || length == 0 || (length < 0 && *s == 0)) + { + return std::string(); + } + + auto size = WideCharToMultiByte(CP_UTF8, 0, s, length, nullptr, 0, nullptr, nullptr); + if (size <= 0) + { + return std::string(); + } + + std::string text(static_cast(size), '\0'); + WideCharToMultiByte(CP_UTF8, 0, s, length, &text[0], size, nullptr, nullptr); + if (length < 0) + { + // the terminating 0 is counted when the length is not given + text.resize(static_cast(size - 1)); + } + + return text; +} + +static fs::path to_path(const char *s) +{ + return fs::path(widen(s)); +} + +static std::string from_path(const fs::path &p) +{ + auto wide = p.wstring(); + return narrow(wide.c_str(), static_cast(wide.size())); +} +#else +static fs::path to_path(const char *s) +{ + return fs::path(s == nullptr ? "" : s); +} + +static std::string from_path(const fs::path &p) +{ + return p.string(); +} +#endif + +// ---- errors, the way Node words them: "ENOENT: no such file or directory, open 'x.txt'" ---- + +static const char *error_name(const std::error_condition &condition) +{ + if (condition.category() != std::generic_category()) + { + return "EIO"; + } + + switch (static_cast(condition.value())) + { + case std::errc::no_such_file_or_directory: + return "ENOENT"; + case std::errc::file_exists: + return "EEXIST"; + case std::errc::permission_denied: + return "EACCES"; + case std::errc::operation_not_permitted: + return "EPERM"; + case std::errc::not_a_directory: + return "ENOTDIR"; + case std::errc::is_a_directory: + return "EISDIR"; + case std::errc::directory_not_empty: + return "ENOTEMPTY"; + case std::errc::device_or_resource_busy: + return "EBUSY"; + case std::errc::invalid_argument: + return "EINVAL"; + case std::errc::filename_too_long: + return "ENAMETOOLONG"; + case std::errc::cross_device_link: + return "EXDEV"; + case std::errc::file_too_large: + return "EFBIG"; + case std::errc::no_space_on_device: + return "ENOSPC"; + case std::errc::read_only_file_system: + return "EROFS"; + case std::errc::too_many_files_open: + return "EMFILE"; + default: + return "EIO"; + } +} + +static int fail(const std::error_code &ec, const char *op, const char *path, const char *dest = nullptr) +{ + auto condition = ec.default_error_condition(); + std::string message = error_name(condition); + message += ": "; + message += condition.message(); + message += ", "; + message += op; + if (path != nullptr) + { + message += " '"; + message += path; + message += "'"; + } + + if (dest != nullptr) + { + message += " -> '"; + message += dest; + message += "'"; + } + + set_result(message); + return -1; +} + +static int fail_errno(int error, const char *op, const char *path) +{ + return fail(std::error_code(error, std::generic_category()), op, path); +} + +static int fail_errc(std::errc error, const char *op, const char *path) +{ + return fail(std::make_error_code(error), op, path); +} + +// ---- the result buffer ---- + +extern "C" TSLANG_EXPORT int tslang_os_result_length() +{ + return static_cast(result.size()); +} + +extern "C" TSLANG_EXPORT void tslang_os_result_copy_to(char *buffer, int count) +{ + if (buffer == nullptr || count <= 0) + { + return; + } + + auto size = result.size() < static_cast(count) ? result.size() : static_cast(count); + result.copy(buffer, size); +} + +// ---- process ---- + +// The command line as the operating system gave it to the process: under the JIT that is tslang's +// own, with the script after its options. Taken once: it does not change. +static std::vector arguments; +static std::once_flag argumentsTaken; + +static void take_arguments() +{ +#ifdef _WIN32 + int count = 0; + auto argv = CommandLineToArgvW(GetCommandLineW(), &count); + if (argv != nullptr) + { + for (int i = 0; i < count; i++) + { + arguments.push_back(narrow(argv[i])); + } + + LocalFree(argv); + } +#else + // /proc/self/cmdline: the arguments, each ended by a 0 (Linux and Android) + auto file = std::fopen("/proc/self/cmdline", "rb"); + if (file == nullptr) + { + return; + } + + std::string all; + char chunk[4096]; + std::size_t read; + while ((read = std::fread(chunk, 1, sizeof(chunk), file)) > 0) + { + all.append(chunk, read); + } + + std::fclose(file); + + std::size_t start = 0; + while (start < all.size()) + { + auto end = all.find('\0', start); + if (end == std::string::npos) + { + end = all.size(); + } + + arguments.push_back(all.substr(start, end - start)); + start = end + 1; + } +#endif +} + +extern "C" TSLANG_EXPORT int tslang_process_argc() +{ + std::call_once(argumentsTaken, take_arguments); + return static_cast(arguments.size()); +} + +// argument `i`'s length, or -1 past the last one +extern "C" TSLANG_EXPORT int tslang_process_argv(int i) +{ + std::call_once(argumentsTaken, take_arguments); + if (i < 0 || static_cast(i) >= arguments.size()) + { + return -1; + } + + return set_result(arguments[static_cast(i)]); +} + +// The variable's length, or -1 when it is not set. On Windows the process environment is read and +// written through the Win32 API, not the C runtime's copy of it: under the JIT this library has a +// C runtime of its own, whose copy the program's would not see. +extern "C" TSLANG_EXPORT int tslang_process_getenv(const char *name) +{ + if (name == nullptr || *name == 0) + { + return -1; + } + +#ifdef _WIN32 + auto wideName = widen(name); + auto size = GetEnvironmentVariableW(wideName.c_str(), nullptr, 0); + if (size == 0) + { + // not set, or set to "" - which the second call tells apart + SetLastError(ERROR_SUCCESS); + wchar_t empty[1]; + GetEnvironmentVariableW(wideName.c_str(), empty, 1); + if (GetLastError() == ERROR_ENVVAR_NOT_FOUND) + { + return -1; + } + + return set_result(std::string()); + } + + std::wstring value(size, L'\0'); + auto length = GetEnvironmentVariableW(wideName.c_str(), &value[0], size); + return set_result(narrow(value.c_str(), static_cast(length))); +#else + auto value = std::getenv(name); + if (value == nullptr) + { + return -1; + } + + return set_result(value); +#endif +} + +static bool valid_name(const char *name) +{ + return name != nullptr && *name != 0 && std::strchr(name, '=') == nullptr; +} + +extern "C" TSLANG_EXPORT int tslang_process_setenv(const char *name, const char *value) +{ + if (!valid_name(name)) + { + return fail_errc(std::errc::invalid_argument, "setenv", name); + } + +#ifdef _WIN32 + auto wideName = widen(name); + auto wideValue = widen(value); + if (!SetEnvironmentVariableW(wideName.c_str(), wideValue.c_str())) + { + return fail(std::error_code(static_cast(GetLastError()), std::system_category()), "setenv", name); + } + + // the C runtime's copy as well, for getenv in C code linked with the program + _wputenv_s(wideName.c_str(), wideValue.c_str()); +#else + if (setenv(name, value == nullptr ? "" : value, 1) != 0) + { + return fail_errno(errno, "setenv", name); + } +#endif + + return 0; +} + +extern "C" TSLANG_EXPORT int tslang_process_unsetenv(const char *name) +{ + if (!valid_name(name)) + { + return fail_errc(std::errc::invalid_argument, "unsetenv", name); + } + +#ifdef _WIN32 + auto wideName = widen(name); + // a variable that is not set is not an error, as with unsetenv + if (!SetEnvironmentVariableW(wideName.c_str(), nullptr) && GetLastError() != ERROR_ENVVAR_NOT_FOUND) + { + return fail(std::error_code(static_cast(GetLastError()), std::system_category()), "unsetenv", name); + } + + // "" removes it from the C runtime's copy + _wputenv_s(wideName.c_str(), L""); +#else + if (unsetenv(name) != 0) + { + return fail_errno(errno, "unsetenv", name); + } +#endif + + return 0; +} + +// How many variables are set: each is then tslang_process_env_entry(i), "NAME=value". +extern "C" TSLANG_EXPORT int tslang_process_env_count() +{ + entries.clear(); + +#ifdef _WIN32 + auto block = GetEnvironmentStringsW(); + if (block != nullptr) + { + for (auto entry = block; *entry != 0; entry += wcslen(entry) + 1) + { + // "=C:=C:\\dir": the working directory of each drive, which cmd.exe keeps; not a variable + if (*entry != L'=') + { + entries.push_back(narrow(entry)); + } + } + + FreeEnvironmentStringsW(block); + } +#else + if (environ != nullptr) + { + for (auto entry = environ; *entry != nullptr; entry++) + { + entries.push_back(*entry); + } + } +#endif + + return static_cast(entries.size()); +} + +// entry `i` of the last tslang_process_env_count or tslang_fs_read_dir, its length; -1 past the last +extern "C" TSLANG_EXPORT int tslang_os_entry(int i) +{ + if (i < 0 || static_cast(i) >= entries.size()) + { + return -1; + } + + return set_result(entries[static_cast(i)]); +} + +extern "C" TSLANG_EXPORT int tslang_process_cwd() +{ + std::error_code ec; + auto path = fs::current_path(ec); + if (ec) + { + return fail(ec, "uv_cwd", nullptr); + } + + return set_result(from_path(path)); +} + +extern "C" TSLANG_EXPORT int tslang_process_chdir(const char *path) +{ + std::error_code ec; + fs::current_path(to_path(path), ec); + if (ec) + { + return fail(ec, "chdir", path); + } + + return 0; +} + +extern "C" TSLANG_EXPORT int tslang_process_pid() +{ +#ifdef _WIN32 + return static_cast(GetCurrentProcessId()); +#else + return static_cast(getpid()); +#endif +} + +// what Node's process.platform says +extern "C" TSLANG_EXPORT int tslang_process_platform() +{ +#if defined(_WIN32) + return set_result("win32"); +#elif defined(__ANDROID__) + return set_result("android"); +#elif defined(__APPLE__) + return set_result("darwin"); +#elif defined(__linux__) + return set_result("linux"); +#else + return set_result("unknown"); +#endif +} + +// Every stream flushed first, console.log's and Stdio's among them. +extern "C" TSLANG_EXPORT void tslang_process_exit(int code) +{ + std::fflush(nullptr); + std::exit(code); +} + +// ---- fs ---- + +static std::FILE *open_file(const char *path, const char *mode) +{ +#ifdef _WIN32 + std::FILE *file = nullptr; + auto error = _wfopen_s(&file, widen(path).c_str(), widen(mode).c_str()); + if (error != 0) + { + errno = error; + return nullptr; + } + + return file; +#else + return std::fopen(path, mode); +#endif +} + +// the whole file into the result: its length, or -1 +extern "C" TSLANG_EXPORT int tslang_fs_read_file(const char *path) +{ + auto file = open_file(path, "rb"); + if (file == nullptr) + { + return fail_errno(errno, "open", path); + } + + std::string bytes; + char chunk[65536]; + for (;;) + { + auto read = std::fread(chunk, 1, sizeof(chunk), file); + if (read > 0) + { + if (bytes.size() + read > static_cast(INT_MAX)) + { + std::fclose(file); + return fail_errc(std::errc::file_too_large, "read", path); + } + + bytes.append(chunk, read); + } + + if (read < sizeof(chunk)) + { + break; + } + } + + // a directory opens on Linux, and its read then fails with EISDIR + auto failed = std::ferror(file) != 0; + auto error = errno; + std::fclose(file); + if (failed) + { + return fail_errno(error != 0 ? error : EIO, "read", path); + } + + result.swap(bytes); + return static_cast(result.size()); +} + +// `count` bytes to the file, which it creates, or empties first unless `append` +extern "C" TSLANG_EXPORT int tslang_fs_write_file(const char *path, const char *bytes, int count, int append) +{ + auto file = open_file(path, append ? "ab" : "wb"); + if (file == nullptr) + { + return fail_errno(errno, "open", path); + } + + if (bytes != nullptr && count > 0 && std::fwrite(bytes, 1, static_cast(count), file) != static_cast(count)) + { + auto error = errno; + std::fclose(file); + return fail_errno(error != 0 ? error : EIO, "write", path); + } + + if (std::fclose(file) != 0) + { + return fail_errno(errno, "close", path); + } + + return 0; +} + +// 1 a file, 2 a directory, 3 something else (a device, a pipe); -1 an error, ENOENT when there is +// nothing there. Symbolic links are followed. The modification time is in milliseconds since 1970. +extern "C" TSLANG_EXPORT int tslang_fs_stat(const char *path, double *size, double *mtimeMs) +{ + int mode; +#ifdef _WIN32 + struct _stat64 info; + if (_wstat64(widen(path).c_str(), &info) != 0) + { + return fail_errno(errno, "stat", path); + } + + mode = info.st_mode; + *size = static_cast(info.st_size); + *mtimeMs = static_cast(info.st_mtime) * 1000.0; +#else + struct stat info; + if (stat(path, &info) != 0) + { + return fail_errno(errno, "stat", path); + } + + mode = info.st_mode; + *size = static_cast(info.st_size); + *mtimeMs = static_cast(info.st_mtim.tv_sec) * 1000.0 + static_cast(info.st_mtim.tv_nsec / 1000000); +#endif + +#ifdef _WIN32 + const int typeMask = _S_IFMT, regular = _S_IFREG, directory = _S_IFDIR; +#else + const int typeMask = S_IFMT, regular = S_IFREG, directory = S_IFDIR; +#endif + + if ((mode & typeMask) == regular) + { + return 1; + } + + if ((mode & typeMask) == directory) + { + return 2; + } + + return 3; +} + +// The names in the directory, sorted (the operating system gives them in no set order): how many, +// each then tslang_os_entry(i). Not "." or "..". +extern "C" TSLANG_EXPORT int tslang_fs_read_dir(const char *path) +{ + entries.clear(); + + std::error_code ec; + fs::directory_iterator it(to_path(path), ec); + if (ec) + { + return fail(ec, "scandir", path); + } + + for (; it != fs::directory_iterator(); it.increment(ec)) + { + entries.push_back(from_path(it->path().filename())); + } + + if (ec) + { + entries.clear(); + return fail(ec, "scandir", path); + } + + std::sort(entries.begin(), entries.end()); + return static_cast(entries.size()); +} + +// Not `recursive`: the parent must be there and the directory must not. `recursive`: every missing +// parent too, and a directory already there is no error. +extern "C" TSLANG_EXPORT int tslang_fs_mkdir(const char *path, int recursive) +{ + auto p = to_path(path); + std::error_code ec; + if (recursive) + { + fs::create_directories(p, ec); + if (ec) + { + return fail(ec, "mkdir", path); + } + + return 0; + } + + // create_directory gives false, with no error, for a directory that is already there + if (!fs::create_directory(p, ec)) + { + return ec ? fail(ec, "mkdir", path) : fail_errc(std::errc::file_exists, "mkdir", path); + } + + return 0; +} + +// Node's rmSync: a file, or with `recursive` a directory and all it holds; with `force` nothing +// there is not an error. +extern "C" TSLANG_EXPORT int tslang_fs_remove(const char *path, int recursive, int force) +{ + auto p = to_path(path); + std::error_code ec; + auto status = fs::symlink_status(p, ec); + if (ec || !fs::exists(status)) + { + if (force && (!ec || ec == std::errc::no_such_file_or_directory)) + { + return 0; + } + + return ec ? fail(ec, "rm", path) : fail_errc(std::errc::no_such_file_or_directory, "rm", path); + } + + if (fs::is_directory(status)) + { + if (!recursive) + { + return fail_errc(std::errc::is_a_directory, "rm", path); + } + + fs::remove_all(p, ec); + } + else + { + fs::remove(p, ec); + } + + if (ec) + { + return fail(ec, "rm", path); + } + + return 0; +} + +// Node's rmdirSync: an empty directory +extern "C" TSLANG_EXPORT int tslang_fs_rmdir(const char *path) +{ + auto p = to_path(path); + std::error_code ec; + auto status = fs::symlink_status(p, ec); + if (ec) + { + return fail(ec, "rmdir", path); + } + + if (!fs::exists(status)) + { + return fail_errc(std::errc::no_such_file_or_directory, "rmdir", path); + } + + if (!fs::is_directory(status)) + { + return fail_errc(std::errc::not_a_directory, "rmdir", path); + } + + fs::remove(p, ec); + if (ec) + { + return fail(ec, "rmdir", path); + } + + return 0; +} + +// A file replaces one already at `to`, as rename(2) does on every platform. +extern "C" TSLANG_EXPORT int tslang_fs_rename(const char *from, const char *to) +{ + std::error_code ec; + fs::rename(to_path(from), to_path(to), ec); + if (ec) + { + return fail(ec, "rename", from, to); + } + + return 0; +} + +// A copy of the file's bytes, over a file already at `to`, as Node's copyFileSync. +extern "C" TSLANG_EXPORT int tslang_fs_copy_file(const char *from, const char *to) +{ + std::error_code ec; + fs::copy_file(to_path(from), to_path(to), fs::copy_options::overwrite_existing, ec); + if (ec) + { + return fail(ec, "copyfile", from, to); + } + + return 0; +} diff --git a/tests/array_flat.ts b/tests/array_flat.ts new file mode 100644 index 0000000..708d15e --- /dev/null +++ b/tests/array_flat.ts @@ -0,0 +1,45 @@ +// flat, flatMap and toSpliced, on T[] and on the Array class. +function main() { + // flat: one level of nesting taken away + const nested: number[][] = [[1, 2], [], [3]]; + const flat = nested.flat(); + assert(flat.length == 3 && flat.join(",") == "1,2,3", "flat"); + assert(nested.length == 3, "flat leaves the array as it was"); + + const words: string[][] = [["a", "b"], ["c"]]; + assert(words.flat().join("") == "abc", "flat, strings"); + + // flatMap: map, then flat + // each callback takes the index too, and the arrays are typed: a literal [1, 2, 3] is an s32[], + // which a callback taking a number does not match + const ones: number[] = [1, 2, 3]; + const doubled = ones.flatMap((v: number, i: number) => [v, v * 10]); + assert(doubled.join(",") == "1,10,2,20,3,30", "flatMap"); + const indexes = ["x", "y"].flatMap((v: string, i: number) => [`${v}${i}`]); + assert(indexes.join(",") == "x0,y1", "flatMap with the index"); + const none = ones.flatMap((v: number, i: number) => { const empty: number[] = []; return empty; }); + assert(none.length == 0, "flatMap to nothing"); + const split = ["a b", "c"].flatMap((v: string, i: number) => v.split(" ")); + assert(split.join(",") == "a,b,c", "flatMap of strings made at run time"); + + // toSpliced: splice on a copy + const months = ["Jan", "Mar", "Apr", "May"]; + const inserted = months.toSpliced(1, 0, "Feb"); + assert(inserted.join(",") == "Jan,Feb,Mar,Apr,May", "toSpliced inserts"); + assert(months.join(",") == "Jan,Mar,Apr,May", "and leaves the array as it was"); + assert(months.toSpliced(1, 2).join(",") == "Jan,May", "toSpliced removes"); + assert(months.toSpliced(1, 1, "Feb", "Mar").join(",") == "Jan,Feb,Mar,Apr,May", "toSpliced replaces"); + assert(months.toSpliced(2).join(",") == "Jan,Mar", "no deleteCount: to the end"); + assert(months.toSpliced(-1, 1).join(",") == "Jan,Mar,Apr", "a negative start counts from the end"); + assert(months.toSpliced(10, 1, "Jun").join(",") == "Jan,Mar,Apr,May,Jun", "a start past the end"); + assert(months.toSpliced(0, 99).length == 0, "a deleteCount past the end"); + + // the Array class + const a = new Array(); + a.push("a", "b", "c"); + assert(a.toSpliced(1, 1, "x", "y").join(",") == "a,x,y,c", "Array.toSpliced"); + assert(a.length == 3, "Array as it was"); + assert(a.flatMap((v: string, i: number) => [v, v]).join("") == "aabbcc", "Array.flatMap"); + + console.log("ALL DONE"); +} diff --git a/tests/fs.ts b/tests/fs.ts new file mode 100644 index 0000000..4cf33e4 --- /dev/null +++ b/tests/fs.ts @@ -0,0 +1,89 @@ +// fs: files and directories, synchronously. Works in __fs_test under the working directory, the +// repository root, which it removes first and last. +function errorOf(f: () => void): string { + try { + f(); + } catch (e: Error) { + return e.message; + } + + return ""; +} + +function main() { + const dir = "__fs_test"; + fs.rmSync(dir, { recursive: true, force: true }); + assert(!fs.existsSync(dir), "nothing there to start with"); + + // directories + fs.mkdirSync(dir); + assert(fs.existsSync(dir), "made"); + assert(fs.statSync(dir).isDirectory, "a directory"); + assert(errorOf(() => { fs.mkdirSync(dir); }).startsWith("EEXIST"), "made twice"); + fs.mkdirSync(dir, { recursive: true }); + fs.mkdirSync(dir + "/a/b/c", { recursive: true }); + assert(fs.statSync(dir + "/a/b/c").isDirectory, "and its parents"); + assert(errorOf(() => { fs.mkdirSync(dir + "/x/y"); }).startsWith("ENOENT"), "a missing parent"); + + // text + const file = dir + "/hello.txt"; + fs.writeTextFileSync(file, "hello"); + assert(fs.readTextFileSync(file) == "hello", "written and read"); + fs.appendTextFileSync(file, ", world\n"); + assert(fs.readTextFileSync(file) == "hello, world\n", "appended"); + fs.writeTextFileSync(file, "aĆ©ā‚¬šŸ˜€"); + assert(fs.readTextFileSync(file) == "aĆ©ā‚¬šŸ˜€", "UTF-8, and written over"); + + const stats = fs.statSync(file); + assert(stats.isFile && !stats.isDirectory, "a file"); + assert(stats.size == 10, "10 bytes"); + assert(stats.mtimeMs > 0, "a modification time"); + + fs.writeTextFileSync(file, ""); + assert(fs.readTextFileSync(file) == "" && fs.statSync(file).size == 0, "emptied"); + + // bytes, a 0 among them + const data = dir + "/data.bin"; + fs.writeFileSync(data, new Uint8Array([1, 0, 255, 10])); + fs.appendFileSync(data, new Uint8Array([13])); + const bytes = fs.readFileSync(data); + assert(bytes.length == 5, "5 bytes"); + assert(bytes[0] == 1 && bytes[1] == 0 && bytes[2] == 255 && bytes[3] == 10 && bytes[4] == 13, "as they were"); + fs.writeFileSync(data, new Uint8Array([])); + assert(fs.readFileSync(data).length == 0, "no bytes"); + + // copy, rename, list + fs.writeTextFileSync(file, "copied"); + fs.copyFileSync(file, dir + "/copy.txt"); + assert(fs.readTextFileSync(dir + "/copy.txt") == "copied", "copied"); + fs.renameSync(dir + "/copy.txt", dir + "/renamed.txt"); + assert(!fs.existsSync(dir + "/copy.txt") && fs.readTextFileSync(dir + "/renamed.txt") == "copied", "renamed"); + + const names = fs.readdirSync(dir); + assert(names.length == 4, "four entries"); + assert(names[0] == "a" && names[1] == "data.bin" && names[2] == "hello.txt" && names[3] == "renamed.txt", "sorted"); + assert(fs.readdirSync(dir + "/a/b/c").length == 0, "an empty directory"); + + // errors + assert(errorOf(() => { fs.readTextFileSync(dir + "/missing.txt"); }).startsWith("ENOENT"), "read what is not there"); + assert(errorOf(() => { fs.readFileSync(dir + "/missing.txt"); }).includes("missing.txt"), "the message names the path"); + assert(errorOf(() => { fs.statSync(dir + "/missing.txt"); }).startsWith("ENOENT"), "stat what is not there"); + assert(errorOf(() => { fs.readdirSync(file); }).startsWith("ENOTDIR") || errorOf(() => { fs.readdirSync(file); }).startsWith("ENOENT"), "list a file"); + assert(errorOf(() => { fs.renameSync(dir + "/missing.txt", dir + "/x.txt"); }).startsWith("ENOENT"), "rename what is not there"); + assert(errorOf(() => { fs.writeTextFileSync(dir + "/x/y/z.txt", "no"); }).startsWith("ENOENT"), "write under a missing directory"); + + // removing + assert(errorOf(() => { fs.rmSync(dir + "/a"); }).startsWith("EISDIR"), "rm a directory without recursive"); + assert(errorOf(() => { fs.rmdirSync(dir + "/a"); }) != "", "rmdir a directory that is not empty"); + fs.rmdirSync(dir + "/a/b/c"); + assert(!fs.existsSync(dir + "/a/b/c"), "rmdir an empty one"); + fs.rmSync(file); + assert(!fs.existsSync(file), "rm a file"); + assert(errorOf(() => { fs.rmSync(file); }).startsWith("ENOENT"), "rm it again"); + fs.rmSync(file, { force: true }); + + fs.rmSync(dir, { recursive: true }); + assert(!fs.existsSync(dir), "and all of it"); + + console.log("ALL DONE"); +} diff --git a/tests/map_groupBy.ts b/tests/map_groupBy.ts new file mode 100644 index 0000000..872761a --- /dev/null +++ b/tests/map_groupBy.ts @@ -0,0 +1,33 @@ +// not own: Map.groupBy gives arrays as a Map's values, which -mm=own does not take. +function main() { + const items = ["apple", "avocado", "banana", "blueberry", "cherry"]; + // substring, not item[0]: a string key, as get("a") gives + const byLetter = Map.groupBy(items, (item: string, i: number) => item.substring(0, 1)); + assert(byLetter.size == 3, "three groups"); + for (const [key, group] of byLetter) { + console.log(key, group.join(",")); + } + + const a = byLetter.get("a"); + assert(a != undefined && a.join(",") == "apple,avocado", "the items, in their order"); + const b = byLetter.get("b"); + assert(b != undefined && b.join(",") == "banana,blueberry", "group b"); + const c = byLetter.get("c"); + assert(c != undefined && c.length == 1, "group c"); + assert(!byLetter.has("d"), "no group without items"); + + // the groups in the order their keys first come + let order = ""; + for (const key of byLetter.keys()) order += key; + assert(order == "abc", "the order of the keys"); + + // a number key, and the index + const evenOdd = Map.groupBy([10, 11, 12, 13, 14], (v: number, i: number) => i % 2); + const even = evenOdd.get(0); + assert(even != undefined && even.join(",") == "10,12,14", "by index"); + + const empty: number[] = []; + assert(Map.groupBy(empty, (v: number, i: number) => v).size == 0, "nothing to group"); + + console.log("ALL DONE"); +} diff --git a/tests/process.ts b/tests/process.ts new file mode 100644 index 0000000..c0523ef --- /dev/null +++ b/tests/process.ts @@ -0,0 +1,71 @@ +// process: the command line, the environment, the working directory, and what the process is. +function errorOf(f: () => void): string { + try { + f(); + } catch (e: Error) { + return e.message; + } + + return ""; +} + +function envValue(name: string): string { + for (const [k, v] of process.env()) { + if (k == name) { + return v; + } + } + + return "(not there)"; +} + +function main() { + // argv()[0] is the program: the test's executable, or tslang under the JIT, followed there by + // the script. Either way one of them names this test. + const args = process.argv(); + assert(args.length >= 1, "the program is the first argument"); + let named = false; + for (const arg of args) { + if (arg.includes("process")) { + named = true; + } + } + assert(named, "the command line names this test"); + + // the environment + assert(process.getEnv("TSLANG_TEST_NOT_SET_ANYWHERE") == undefined, "a variable that is not set"); + process.setEnv("TSLANG_TEST_VAR", "value one"); + assert(process.getEnv("TSLANG_TEST_VAR") == "value one", "a variable just set"); + process.setEnv("TSLANG_TEST_VAR", "Ć© and €"); + assert(process.getEnv("TSLANG_TEST_VAR") == "Ć© and €", "set again, to UTF-8"); + process.setEnv("TSLANG_TEST_EMPTY", ""); + assert(process.getEnv("TSLANG_TEST_EMPTY") == "", "set to nothing is still set"); + + assert(envValue("TSLANG_TEST_VAR") == "Ć© and €", "env() has it"); + assert(process.env().length > 1, "env() has more than this test's own"); + + process.unsetEnv("TSLANG_TEST_VAR"); + assert(process.getEnv("TSLANG_TEST_VAR") == undefined, "unset"); + process.unsetEnv("TSLANG_TEST_VAR"); + assert(envValue("TSLANG_TEST_VAR") == "(not there)", "env() no longer has it"); + + assert(errorOf(() => { process.setEnv("A=B", "x"); }).startsWith("EINVAL"), "a name with ="); + assert(errorOf(() => { process.setEnv("", "x"); }).startsWith("EINVAL"), "an empty name"); + + // the working directory + const start = process.cwd(); + assert(start.length > 0, "a working directory"); + process.chdir("tests"); + const inTests = process.cwd(); + assert(inTests != start && inTests.endsWith("tests"), "chdir into tests"); + process.chdir(".."); + assert(process.cwd() == start, "and back"); + assert(errorOf(() => { process.chdir("no such directory here"); }).startsWith("ENOENT"), "chdir to nowhere"); + + assert(process.pid() > 0, "a process id"); + + const platform = process.platform(); + assert(platform == "win32" || platform == "linux" || platform == "android" || platform == "darwin", "a platform"); + + console.log("ALL DONE"); +} diff --git a/tests/process_exit.ts b/tests/process_exit.ts new file mode 100644 index 0000000..254fee4 --- /dev/null +++ b/tests/process_exit.ts @@ -0,0 +1,6 @@ +// process.exit ends the program there, with what it wrote so far written out. +function main() { + console.log("ALL DONE"); + process.exit(0); + console.log("Error: process.exit returned"); +} diff --git a/tests/string_fromCodePoint.ts b/tests/string_fromCodePoint.ts new file mode 100644 index 0000000..ec0861f --- /dev/null +++ b/tests/string_fromCodePoint.ts @@ -0,0 +1,35 @@ +// String.fromCodePoint: each code point as its UTF-8 bytes. +function rangeError(f: () => void): boolean { + try { + f(); + } catch (e: RangeError) { + return true; + } + + return false; +} + +function main() { + assert(String.fromCodePoint() == "", "no code points"); + assert(String.fromCodePoint(72, 105) == "Hi", "ASCII"); + + const e = String.fromCodePoint(0xE9); + assert(e.length == 2 && e == "Ć©", "two bytes"); + const euro = String.fromCodePoint(0x20AC); + assert(euro.length == 3 && euro == "€", "three bytes"); + + // one code point, not a surrogate pair as fromCharCode takes + const smile = String.fromCodePoint(0x1F600); + assert(smile.length == 4 && smile == "šŸ˜€", "four bytes"); + assert(smile == String.fromCharCode(0xD83D, 0xDE00), "the same as the surrogate pair"); + assert(String.fromCodePoint(0x10FFFF).length == 4, "the last code point"); + + assert(rangeError(() => { String.fromCodePoint(0x110000); }), "past the last code point"); + assert(rangeError(() => { String.fromCodePoint(-1); }), "negative"); + + // fromCharCode still the same after sharing its encoder + assert(String.fromCharCode(0xD83D, 0xDE00) == "šŸ˜€" && String.fromCharCode(0xE9) == "Ć©", "fromCharCode"); + + console.log(String.fromCodePoint(0x48, 0x1F600, 0x20AC)); + console.log("ALL DONE"); +}