From 5f3a410e23219ccb9317adafc14e65c68488fac5 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Wed, 14 Jan 2026 17:02:08 -0800 Subject: [PATCH 1/2] Add tests for built-in modules: assert, querystring, and url - Implemented comprehensive unit tests for the 'assert' module, covering various assertion methods including ok, strictEqual, notStrictEqual, deepStrictEqual, and fail. - Added tests for the 'querystring' module, validating parsing, stringifying, and encoding/decoding functionalities. - Created tests for the 'url' module, ensuring correct parsing, formatting, and resolving of URLs. - Enhanced the process global tests with additional checks for hrtime, uptime, and memory usage. - Updated the TypeSystem to include type definitions for the new querystring, assert, and url modules. --- Compilation/EmittedRuntime.cs | 24 + .../Emitters/Modules/AssertModuleEmitter.cs | 239 +++++++ .../Modules/QuerystringModuleEmitter.cs | 170 +++++ .../Emitters/Modules/UrlModuleEmitter.cs | 102 +++ Compilation/Emitters/ProcessStaticEmitter.cs | 62 ++ Compilation/ILCompiler.cs | 6 + Compilation/RuntimeEmitter.AssertHelpers.cs | 635 ++++++++++++++++++ Compilation/RuntimeEmitter.ProcessHelpers.cs | 273 +++++++- .../RuntimeEmitter.QuerystringHelpers.cs | 472 +++++++++++++ Compilation/RuntimeEmitter.UrlHelpers.cs | 258 +++++++ Compilation/RuntimeEmitter.cs | 6 + Compilation/TypeProvider.cs | 6 +- .../BuiltIns/Modules/BuiltInModuleRegistry.cs | 5 +- .../Interpreter/AssertModuleInterpreter.cs | 455 +++++++++++++ .../Interpreter/BuiltInModuleValues.cs | 5 +- .../QuerystringModuleInterpreter.cs | 163 +++++ .../Interpreter/UrlModuleInterpreter.cs | 221 ++++++ Runtime/BuiltIns/ProcessBuiltIns.cs | 67 ++ Runtime/Types/SharpTSURL.cs | 245 +++++++ .../BuiltInModules/AssertModuleTests.cs | 347 ++++++++++ .../BuiltInModules/ProcessGlobalTests.cs | 167 +++++ .../BuiltInModules/QuerystringModuleTests.cs | 229 +++++++ .../BuiltInModules/UrlModuleTests.cs | 253 +++++++ .../BuiltInModules/InterpreterModuleTests.cs | 309 +++++++++ TypeSystem/BuiltInModuleTypes.cs | 157 +++++ 25 files changed, 4872 insertions(+), 4 deletions(-) create mode 100644 Compilation/Emitters/Modules/AssertModuleEmitter.cs create mode 100644 Compilation/Emitters/Modules/QuerystringModuleEmitter.cs create mode 100644 Compilation/Emitters/Modules/UrlModuleEmitter.cs create mode 100644 Compilation/RuntimeEmitter.AssertHelpers.cs create mode 100644 Compilation/RuntimeEmitter.QuerystringHelpers.cs create mode 100644 Compilation/RuntimeEmitter.UrlHelpers.cs create mode 100644 Runtime/BuiltIns/Modules/Interpreter/AssertModuleInterpreter.cs create mode 100644 Runtime/BuiltIns/Modules/Interpreter/QuerystringModuleInterpreter.cs create mode 100644 Runtime/BuiltIns/Modules/Interpreter/UrlModuleInterpreter.cs create mode 100644 Runtime/Types/SharpTSURL.cs create mode 100644 SharpTS.Tests/CompilerTests/BuiltInModules/AssertModuleTests.cs create mode 100644 SharpTS.Tests/CompilerTests/BuiltInModules/QuerystringModuleTests.cs create mode 100644 SharpTS.Tests/CompilerTests/BuiltInModules/UrlModuleTests.cs diff --git a/Compilation/EmittedRuntime.cs b/Compilation/EmittedRuntime.cs index c75dfd6..8a75e98 100644 --- a/Compilation/EmittedRuntime.cs +++ b/Compilation/EmittedRuntime.cs @@ -401,6 +401,30 @@ public class EmittedRuntime // Process module methods public MethodBuilder ProcessGetEnv { get; set; } = null!; public MethodBuilder ProcessGetArgv { get; set; } = null!; + public MethodBuilder ProcessHrtime { get; set; } = null!; + public MethodBuilder ProcessUptime { get; set; } = null!; + public MethodBuilder ProcessMemoryUsage { get; set; } = null!; + + // Querystring module methods + public MethodBuilder QuerystringParse { get; set; } = null!; + public MethodBuilder QuerystringStringify { get; set; } = null!; + + // Assert module methods + public MethodBuilder AssertOk { get; set; } = null!; + public MethodBuilder AssertStrictEqual { get; set; } = null!; + public MethodBuilder AssertNotStrictEqual { get; set; } = null!; + public MethodBuilder AssertDeepStrictEqual { get; set; } = null!; + public MethodBuilder AssertNotDeepStrictEqual { get; set; } = null!; + public MethodBuilder AssertThrows { get; set; } = null!; + public MethodBuilder AssertDoesNotThrow { get; set; } = null!; + public MethodBuilder AssertFail { get; set; } = null!; + public MethodBuilder AssertEqual { get; set; } = null!; + public MethodBuilder AssertNotEqual { get; set; } = null!; + + // URL module methods + public MethodBuilder UrlParse { get; set; } = null!; + public MethodBuilder UrlFormat { get; set; } = null!; + public MethodBuilder UrlResolve { get; set; } = null!; // Built-in module methods (module name -> method name -> MethodBuilder) // Used for creating TSFunction wrappers when importing named exports diff --git a/Compilation/Emitters/Modules/AssertModuleEmitter.cs b/Compilation/Emitters/Modules/AssertModuleEmitter.cs new file mode 100644 index 0000000..65bcec7 --- /dev/null +++ b/Compilation/Emitters/Modules/AssertModuleEmitter.cs @@ -0,0 +1,239 @@ +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation.Emitters.Modules; + +/// +/// Emits IL code for the Node.js 'assert' module. +/// +public sealed class AssertModuleEmitter : IBuiltInModuleEmitter +{ + public string ModuleName => "assert"; + + private static readonly string[] _exportedMembers = + [ + "ok", "strictEqual", "notStrictEqual", "deepStrictEqual", "notDeepStrictEqual", + "throws", "doesNotThrow", "fail", "equal", "notEqual" + ]; + + public IReadOnlyList GetExportedMembers() => _exportedMembers; + + public bool TryEmitMethodCall(IEmitterContext emitter, string methodName, List arguments) + { + return methodName switch + { + "ok" => EmitOk(emitter, arguments), + "strictEqual" => EmitStrictEqual(emitter, arguments), + "notStrictEqual" => EmitNotStrictEqual(emitter, arguments), + "deepStrictEqual" => EmitDeepStrictEqual(emitter, arguments), + "notDeepStrictEqual" => EmitNotDeepStrictEqual(emitter, arguments), + "throws" => EmitThrows(emitter, arguments), + "doesNotThrow" => EmitDoesNotThrow(emitter, arguments), + "fail" => EmitFail(emitter, arguments), + "equal" => EmitEqual(emitter, arguments), + "notEqual" => EmitNotEqual(emitter, arguments), + _ => false + }; + } + + public bool TryEmitPropertyGet(IEmitterContext emitter, string propertyName) + { + // assert has no properties + return false; + } + + private static bool EmitOk(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + // Call runtime helper: AssertOk(object? value, object? message) + if (arguments.Count > 0) + { + emitter.EmitBoxIfNeeded(arguments[0]); + } + else + { + il.Emit(OpCodes.Ldnull); + } + + if (arguments.Count > 1) + { + emitter.EmitBoxIfNeeded(arguments[1]); + } + else + { + il.Emit(OpCodes.Ldnull); + } + + il.Emit(OpCodes.Call, ctx.Runtime!.AssertOk); + return true; + } + + private static bool EmitStrictEqual(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + // Call runtime helper: AssertStrictEqual(object? actual, object? expected, object? message) + EmitThreeArgs(emitter, arguments); + il.Emit(OpCodes.Call, ctx.Runtime!.AssertStrictEqual); + return true; + } + + private static bool EmitNotStrictEqual(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + EmitThreeArgs(emitter, arguments); + il.Emit(OpCodes.Call, ctx.Runtime!.AssertNotStrictEqual); + return true; + } + + private static bool EmitDeepStrictEqual(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + EmitThreeArgs(emitter, arguments); + il.Emit(OpCodes.Call, ctx.Runtime!.AssertDeepStrictEqual); + return true; + } + + private static bool EmitNotDeepStrictEqual(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + EmitThreeArgs(emitter, arguments); + il.Emit(OpCodes.Call, ctx.Runtime!.AssertNotDeepStrictEqual); + return true; + } + + private static bool EmitThrows(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + // Call runtime helper: AssertThrows(object? fn, object? message) + if (arguments.Count > 0) + { + emitter.EmitBoxIfNeeded(arguments[0]); + } + else + { + il.Emit(OpCodes.Ldnull); + } + + if (arguments.Count > 1) + { + emitter.EmitBoxIfNeeded(arguments[1]); + } + else + { + il.Emit(OpCodes.Ldnull); + } + + il.Emit(OpCodes.Call, ctx.Runtime!.AssertThrows); + return true; + } + + private static bool EmitDoesNotThrow(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + if (arguments.Count > 0) + { + emitter.EmitBoxIfNeeded(arguments[0]); + } + else + { + il.Emit(OpCodes.Ldnull); + } + + if (arguments.Count > 1) + { + emitter.EmitBoxIfNeeded(arguments[1]); + } + else + { + il.Emit(OpCodes.Ldnull); + } + + il.Emit(OpCodes.Call, ctx.Runtime!.AssertDoesNotThrow); + return true; + } + + private static bool EmitFail(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + // Call runtime helper: AssertFail(object? message) + if (arguments.Count > 0) + { + emitter.EmitBoxIfNeeded(arguments[0]); + } + else + { + il.Emit(OpCodes.Ldnull); + } + + il.Emit(OpCodes.Call, ctx.Runtime!.AssertFail); + return true; + } + + private static bool EmitEqual(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + EmitThreeArgs(emitter, arguments); + il.Emit(OpCodes.Call, ctx.Runtime!.AssertEqual); + return true; + } + + private static bool EmitNotEqual(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + EmitThreeArgs(emitter, arguments); + il.Emit(OpCodes.Call, ctx.Runtime!.AssertNotEqual); + return true; + } + + private static void EmitThreeArgs(IEmitterContext emitter, List arguments) + { + var il = emitter.Context.IL; + + if (arguments.Count > 0) + { + emitter.EmitBoxIfNeeded(arguments[0]); + } + else + { + il.Emit(OpCodes.Ldnull); + } + + if (arguments.Count > 1) + { + emitter.EmitBoxIfNeeded(arguments[1]); + } + else + { + il.Emit(OpCodes.Ldnull); + } + + if (arguments.Count > 2) + { + emitter.EmitBoxIfNeeded(arguments[2]); + } + else + { + il.Emit(OpCodes.Ldnull); + } + } +} diff --git a/Compilation/Emitters/Modules/QuerystringModuleEmitter.cs b/Compilation/Emitters/Modules/QuerystringModuleEmitter.cs new file mode 100644 index 0000000..f3cc817 --- /dev/null +++ b/Compilation/Emitters/Modules/QuerystringModuleEmitter.cs @@ -0,0 +1,170 @@ +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation.Emitters.Modules; + +/// +/// Emits IL code for the Node.js 'querystring' module. +/// +public sealed class QuerystringModuleEmitter : IBuiltInModuleEmitter +{ + public string ModuleName => "querystring"; + + private static readonly string[] _exportedMembers = + [ + "parse", "stringify", "escape", "unescape", "decode", "encode" + ]; + + public IReadOnlyList GetExportedMembers() => _exportedMembers; + + public bool TryEmitMethodCall(IEmitterContext emitter, string methodName, List arguments) + { + return methodName switch + { + "parse" or "decode" => EmitParse(emitter, arguments), + "stringify" or "encode" => EmitStringify(emitter, arguments), + "escape" => EmitEscape(emitter, arguments), + "unescape" => EmitUnescape(emitter, arguments), + _ => false + }; + } + + public bool TryEmitPropertyGet(IEmitterContext emitter, string propertyName) + { + // querystring has no properties + return false; + } + + private static bool EmitParse(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + // Call runtime helper: QuerystringParse(string str, string? sep, string? eq) + // Argument 0: str + if (arguments.Count > 0) + { + EmitToString(emitter, arguments[0]); + } + else + { + il.Emit(OpCodes.Ldstr, ""); + } + + // Argument 1: sep (default "&") + if (arguments.Count > 1) + { + EmitToString(emitter, arguments[1]); + } + else + { + il.Emit(OpCodes.Ldstr, "&"); + } + + // Argument 2: eq (default "=") + if (arguments.Count > 2) + { + EmitToString(emitter, arguments[2]); + } + else + { + il.Emit(OpCodes.Ldstr, "="); + } + + il.Emit(OpCodes.Call, ctx.Runtime!.QuerystringParse); + return true; + } + + private static bool EmitStringify(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + // Call runtime helper: QuerystringStringify(object? obj, string sep, string eq) + // Argument 0: obj + if (arguments.Count > 0) + { + emitter.EmitExpression(arguments[0]); + } + else + { + il.Emit(OpCodes.Ldnull); + } + + // Argument 1: sep (default "&") + if (arguments.Count > 1) + { + EmitToString(emitter, arguments[1]); + } + else + { + il.Emit(OpCodes.Ldstr, "&"); + } + + // Argument 2: eq (default "=") + if (arguments.Count > 2) + { + EmitToString(emitter, arguments[2]); + } + else + { + il.Emit(OpCodes.Ldstr, "="); + } + + il.Emit(OpCodes.Call, ctx.Runtime!.QuerystringStringify); + return true; + } + + private static bool EmitEscape(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + // Uri.EscapeDataString(str) + if (arguments.Count > 0) + { + EmitToString(emitter, arguments[0]); + } + else + { + il.Emit(OpCodes.Ldstr, ""); + } + + il.Emit(OpCodes.Call, typeof(Uri).GetMethod("EscapeDataString", [typeof(string)])!); + return true; + } + + private static bool EmitUnescape(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + // Replace '+' with ' ' then Uri.UnescapeDataString(str) + if (arguments.Count > 0) + { + EmitToString(emitter, arguments[0]); + } + else + { + il.Emit(OpCodes.Ldstr, ""); + } + + // Call string.Replace('+', ' ') + il.Emit(OpCodes.Ldc_I4, '+'); + il.Emit(OpCodes.Ldc_I4, ' '); + il.Emit(OpCodes.Callvirt, ctx.Types.GetMethod(ctx.Types.String, "Replace", ctx.Types.Char, ctx.Types.Char)); + + // Call Uri.UnescapeDataString + il.Emit(OpCodes.Call, typeof(Uri).GetMethod("UnescapeDataString", [typeof(string)])!); + return true; + } + + private static void EmitToString(IEmitterContext emitter, Expr expr) + { + var ctx = emitter.Context; + var il = ctx.IL; + + emitter.EmitBoxIfNeeded(expr); + il.Emit(OpCodes.Callvirt, ctx.Types.GetMethod(ctx.Types.Object, "ToString")); + } +} diff --git a/Compilation/Emitters/Modules/UrlModuleEmitter.cs b/Compilation/Emitters/Modules/UrlModuleEmitter.cs new file mode 100644 index 0000000..c45d16f --- /dev/null +++ b/Compilation/Emitters/Modules/UrlModuleEmitter.cs @@ -0,0 +1,102 @@ +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation.Emitters.Modules; + +/// +/// Emits IL code for the Node.js 'url' module. +/// +public sealed class UrlModuleEmitter : IBuiltInModuleEmitter +{ + public string ModuleName => "url"; + + private static readonly string[] _exportedMembers = + [ + "URL", "URLSearchParams", "parse", "format", "resolve" + ]; + + public IReadOnlyList GetExportedMembers() => _exportedMembers; + + public bool TryEmitMethodCall(IEmitterContext emitter, string methodName, List arguments) + { + return methodName switch + { + "parse" => EmitParse(emitter, arguments), + "format" => EmitFormat(emitter, arguments), + "resolve" => EmitResolve(emitter, arguments), + _ => false + }; + } + + public bool TryEmitPropertyGet(IEmitterContext emitter, string propertyName) + { + // URL and URLSearchParams are handled as class constructors, not properties + // They require special handling in the compiler + return false; + } + + private static bool EmitParse(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + // Call runtime helper: UrlParse(object? url) + if (arguments.Count > 0) + { + emitter.EmitBoxIfNeeded(arguments[0]); + } + else + { + il.Emit(OpCodes.Ldnull); + } + + il.Emit(OpCodes.Call, ctx.Runtime!.UrlParse); + return true; + } + + private static bool EmitFormat(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + if (arguments.Count > 0) + { + emitter.EmitBoxIfNeeded(arguments[0]); + } + else + { + il.Emit(OpCodes.Ldnull); + } + + il.Emit(OpCodes.Call, ctx.Runtime!.UrlFormat); + return true; + } + + private static bool EmitResolve(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + // Call runtime helper: UrlResolve(object? from, object? to) + if (arguments.Count > 0) + { + emitter.EmitBoxIfNeeded(arguments[0]); + } + else + { + il.Emit(OpCodes.Ldnull); + } + + if (arguments.Count > 1) + { + emitter.EmitBoxIfNeeded(arguments[1]); + } + else + { + il.Emit(OpCodes.Ldnull); + } + + il.Emit(OpCodes.Call, ctx.Runtime!.UrlResolve); + return true; + } +} diff --git a/Compilation/Emitters/ProcessStaticEmitter.cs b/Compilation/Emitters/ProcessStaticEmitter.cs index a83f62f..55fa3b0 100644 --- a/Compilation/Emitters/ProcessStaticEmitter.cs +++ b/Compilation/Emitters/ProcessStaticEmitter.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Reflection.Emit; using System.Runtime.InteropServices; using SharpTS.Parsing; @@ -41,6 +42,18 @@ public bool TryEmitStaticCall(IEmitterContext emitter, string methodName, List + /// Emits IL for process.hrtime(prev?). + /// Returns a [seconds, nanoseconds] array. + /// + private static void EmitHrtime(IEmitterContext emitter, List arguments) + { + var ctx = emitter.Context; + var il = ctx.IL; + + // Call runtime helper that handles hrtime logic + // The helper takes an optional previous time array + if (arguments.Count > 0) + { + emitter.EmitExpression(arguments[0]); + } + else + { + il.Emit(OpCodes.Ldnull); + } + il.Emit(OpCodes.Call, ctx.Runtime!.ProcessHrtime); + } + + /// + /// Emits IL for process.uptime(). + /// Returns the number of seconds the process has been running. + /// + private static void EmitUptime(IEmitterContext emitter) + { + var ctx = emitter.Context; + var il = ctx.IL; + + // Call runtime helper + il.Emit(OpCodes.Call, ctx.Runtime!.ProcessUptime); + il.Emit(OpCodes.Box, ctx.Types.Double); + } + + /// + /// Emits IL for process.memoryUsage(). + /// Returns an object with memory usage information. + /// + private static void EmitMemoryUsage(IEmitterContext emitter) + { + var ctx = emitter.Context; + var il = ctx.IL; + + // Call runtime helper + il.Emit(OpCodes.Call, ctx.Runtime!.ProcessMemoryUsage); + } } diff --git a/Compilation/ILCompiler.cs b/Compilation/ILCompiler.cs index d804b57..3ac9905 100644 --- a/Compilation/ILCompiler.cs +++ b/Compilation/ILCompiler.cs @@ -448,6 +448,9 @@ public void Compile(List statements, TypeMap typeMap, DeadCodeInfo? deadCo _builtInModuleEmitterRegistry.Register(new PathModuleEmitter()); _builtInModuleEmitterRegistry.Register(new OsModuleEmitter()); _builtInModuleEmitterRegistry.Register(new FsModuleEmitter()); + _builtInModuleEmitterRegistry.Register(new QuerystringModuleEmitter()); + _builtInModuleEmitterRegistry.Register(new AssertModuleEmitter()); + _builtInModuleEmitterRegistry.Register(new UrlModuleEmitter()); // Phase 5: Collect all arrow functions and generate methods/display classes CollectAndDefineArrowFunctions(statements); @@ -660,6 +663,9 @@ public void CompileModules(List modules, ModuleResolver resolver, _builtInModuleEmitterRegistry.Register(new PathModuleEmitter()); _builtInModuleEmitterRegistry.Register(new OsModuleEmitter()); _builtInModuleEmitterRegistry.Register(new FsModuleEmitter()); + _builtInModuleEmitterRegistry.Register(new QuerystringModuleEmitter()); + _builtInModuleEmitterRegistry.Register(new AssertModuleEmitter()); + _builtInModuleEmitterRegistry.Register(new UrlModuleEmitter()); // Phase 6: Collect all arrow functions CollectAndDefineArrowFunctions(allStatements); diff --git a/Compilation/RuntimeEmitter.AssertHelpers.cs b/Compilation/RuntimeEmitter.AssertHelpers.cs new file mode 100644 index 0000000..1c48e54 --- /dev/null +++ b/Compilation/RuntimeEmitter.AssertHelpers.cs @@ -0,0 +1,635 @@ +using System.Reflection; +using System.Reflection.Emit; +using SharpTS.Runtime.BuiltIns.Modules.Interpreter; + +namespace SharpTS.Compilation; + +public partial class RuntimeEmitter +{ + /// + /// Emits assert module helper methods. + /// + private void EmitAssertMethods(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + EmitAssertOk(typeBuilder, runtime); + EmitAssertStrictEqual(typeBuilder, runtime); + EmitAssertNotStrictEqual(typeBuilder, runtime); + EmitAssertDeepStrictEqual(typeBuilder, runtime); + EmitAssertNotDeepStrictEqual(typeBuilder, runtime); + EmitAssertThrows(typeBuilder, runtime); + EmitAssertDoesNotThrow(typeBuilder, runtime); + EmitAssertFail(typeBuilder, runtime); + EmitAssertEqual(typeBuilder, runtime); + EmitAssertNotEqual(typeBuilder, runtime); + EmitAssertMethodWrappers(typeBuilder, runtime); + } + + /// + /// Emits wrapper methods for assert functions that can be used as first-class values. + /// + private void EmitAssertMethodWrappers(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // ok(value, message?) - 2 params + EmitAssertWrapperSimple(typeBuilder, runtime, "ok", 2, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.AssertOk); + il.Emit(OpCodes.Ldnull); + }); + + // strictEqual(actual, expected, message?) - 3 params + EmitAssertWrapperSimple(typeBuilder, runtime, "strictEqual", 3, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.AssertStrictEqual); + il.Emit(OpCodes.Ldnull); + }); + + // notStrictEqual(actual, expected, message?) - 3 params + EmitAssertWrapperSimple(typeBuilder, runtime, "notStrictEqual", 3, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.AssertNotStrictEqual); + il.Emit(OpCodes.Ldnull); + }); + + // deepStrictEqual(actual, expected, message?) - 3 params + EmitAssertWrapperSimple(typeBuilder, runtime, "deepStrictEqual", 3, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.AssertDeepStrictEqual); + il.Emit(OpCodes.Ldnull); + }); + + // notDeepStrictEqual(actual, expected, message?) - 3 params + EmitAssertWrapperSimple(typeBuilder, runtime, "notDeepStrictEqual", 3, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.AssertNotDeepStrictEqual); + il.Emit(OpCodes.Ldnull); + }); + + // throws(fn, message?) - 2 params + EmitAssertWrapperSimple(typeBuilder, runtime, "throws", 2, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.AssertThrows); + il.Emit(OpCodes.Ldnull); + }); + + // doesNotThrow(fn, message?) - 2 params + EmitAssertWrapperSimple(typeBuilder, runtime, "doesNotThrow", 2, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.AssertDoesNotThrow); + il.Emit(OpCodes.Ldnull); + }); + + // fail(message?) - 1 param + EmitAssertWrapperSimple(typeBuilder, runtime, "fail", 1, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.AssertFail); + il.Emit(OpCodes.Ldnull); + }); + + // equal(actual, expected, message?) - 3 params + EmitAssertWrapperSimple(typeBuilder, runtime, "equal", 3, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.AssertEqual); + il.Emit(OpCodes.Ldnull); + }); + + // notEqual(actual, expected, message?) - 3 params + EmitAssertWrapperSimple(typeBuilder, runtime, "notEqual", 3, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.AssertNotEqual); + il.Emit(OpCodes.Ldnull); + }); + } + + private void EmitAssertWrapperSimple( + TypeBuilder typeBuilder, + EmittedRuntime runtime, + string methodName, + int paramCount, + Action emitCall) + { + var paramTypes = new Type[paramCount]; + for (int i = 0; i < paramCount; i++) + paramTypes[i] = _types.Object; + + var method = typeBuilder.DefineMethod( + $"Assert_{methodName}_Wrapper", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + paramTypes + ); + + var il = method.GetILGenerator(); + emitCall(il); + il.Emit(OpCodes.Ret); + + runtime.RegisterBuiltInModuleMethod("assert", methodName, method); + } + + /// + /// Emits: public static void AssertOk(object? value, object? message) + /// + private void EmitAssertOk(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "AssertOk", + MethodAttributes.Public | MethodAttributes.Static, + _types.Void, + [_types.Object, _types.Object] + ); + runtime.AssertOk = method; + + var il = method.GetILGenerator(); + + // Call AssertHelpers.Ok(value, message) which handles the message correctly + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, typeof(AssertHelpers).GetMethod("Ok", BindingFlags.Public | BindingFlags.Static)!); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits: public static void AssertStrictEqual(object? actual, object? expected, object? message) + /// + private void EmitAssertStrictEqual(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "AssertStrictEqual", + MethodAttributes.Public | MethodAttributes.Static, + _types.Void, + [_types.Object, _types.Object, _types.Object] + ); + runtime.AssertStrictEqual = method; + + var il = method.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, typeof(AssertHelpers).GetMethod("StrictEqual", BindingFlags.Public | BindingFlags.Static)!); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits: public static void AssertNotStrictEqual(object? actual, object? expected, object? message) + /// + private void EmitAssertNotStrictEqual(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "AssertNotStrictEqual", + MethodAttributes.Public | MethodAttributes.Static, + _types.Void, + [_types.Object, _types.Object, _types.Object] + ); + runtime.AssertNotStrictEqual = method; + + var il = method.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, typeof(AssertHelpers).GetMethod("NotStrictEqual", BindingFlags.Public | BindingFlags.Static)!); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits: public static void AssertDeepStrictEqual(object? actual, object? expected, object? message) + /// + private void EmitAssertDeepStrictEqual(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "AssertDeepStrictEqual", + MethodAttributes.Public | MethodAttributes.Static, + _types.Void, + [_types.Object, _types.Object, _types.Object] + ); + runtime.AssertDeepStrictEqual = method; + + var il = method.GetILGenerator(); + + // For now, call the interpreter version via reflection + // This is a placeholder - full deep equality comparison would be complex to emit in IL + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, typeof(AssertHelpers).GetMethod("DeepStrictEqual", BindingFlags.Public | BindingFlags.Static)!); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits: public static void AssertNotDeepStrictEqual(object? actual, object? expected, object? message) + /// + private void EmitAssertNotDeepStrictEqual(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "AssertNotDeepStrictEqual", + MethodAttributes.Public | MethodAttributes.Static, + _types.Void, + [_types.Object, _types.Object, _types.Object] + ); + runtime.AssertNotDeepStrictEqual = method; + + var il = method.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, typeof(AssertHelpers).GetMethod("NotDeepStrictEqual", BindingFlags.Public | BindingFlags.Static)!); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits: public static void AssertThrows(object? fn, object? message) + /// + private void EmitAssertThrows(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "AssertThrows", + MethodAttributes.Public | MethodAttributes.Static, + _types.Void, + [_types.Object, _types.Object] + ); + runtime.AssertThrows = method; + + var il = method.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, typeof(AssertHelpers).GetMethod("Throws", BindingFlags.Public | BindingFlags.Static)!); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits: public static void AssertDoesNotThrow(object? fn, object? message) + /// + private void EmitAssertDoesNotThrow(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "AssertDoesNotThrow", + MethodAttributes.Public | MethodAttributes.Static, + _types.Void, + [_types.Object, _types.Object] + ); + runtime.AssertDoesNotThrow = method; + + var il = method.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, typeof(AssertHelpers).GetMethod("DoesNotThrow", BindingFlags.Public | BindingFlags.Static)!); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits: public static void AssertFail(object? message) + /// + private void EmitAssertFail(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "AssertFail", + MethodAttributes.Public | MethodAttributes.Static, + _types.Void, + [_types.Object] + ); + runtime.AssertFail = method; + + var il = method.GetILGenerator(); + + // Get message or default + var notNull = il.DefineLabel(); + var afterMsg = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Brtrue, notNull); + il.Emit(OpCodes.Ldstr, "Failed"); + il.Emit(OpCodes.Br, afterMsg); + il.MarkLabel(notNull); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); + il.MarkLabel(afterMsg); + + // throw new AssertionError(message) + il.Emit(OpCodes.Ldnull); // actual + il.Emit(OpCodes.Ldnull); // expected + il.Emit(OpCodes.Ldstr, "fail"); // operator + il.Emit(OpCodes.Newobj, typeof(AssertionError).GetConstructor([typeof(string), typeof(object), typeof(object), typeof(string)])!); + il.Emit(OpCodes.Throw); + } + + /// + /// Emits: public static void AssertEqual(object? actual, object? expected, object? message) + /// + private void EmitAssertEqual(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "AssertEqual", + MethodAttributes.Public | MethodAttributes.Static, + _types.Void, + [_types.Object, _types.Object, _types.Object] + ); + runtime.AssertEqual = method; + + var il = method.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, typeof(AssertHelpers).GetMethod("Equal", BindingFlags.Public | BindingFlags.Static)!); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits: public static void AssertNotEqual(object? actual, object? expected, object? message) + /// + private void EmitAssertNotEqual(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "AssertNotEqual", + MethodAttributes.Public | MethodAttributes.Static, + _types.Void, + [_types.Object, _types.Object, _types.Object] + ); + runtime.AssertNotEqual = method; + + var il = method.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, typeof(AssertHelpers).GetMethod("NotEqual", BindingFlags.Public | BindingFlags.Static)!); + il.Emit(OpCodes.Ret); + } + + private void EmitThrowAssertionError(ILGenerator il, string defaultMessage, string @operator) + { + // Check if custom message provided (arg1 or arg2 depending on method) + il.Emit(OpCodes.Ldstr, defaultMessage); + il.Emit(OpCodes.Ldnull); // actual + il.Emit(OpCodes.Ldnull); // expected + il.Emit(OpCodes.Ldstr, @operator); + il.Emit(OpCodes.Newobj, typeof(AssertionError).GetConstructor([typeof(string), typeof(object), typeof(object), typeof(string)])!); + il.Emit(OpCodes.Throw); + } +} + +/// +/// Static helper methods for assert module in compiled mode. +/// These are called from emitted IL for complex assertion logic. +/// +public static class AssertHelpers +{ + public static void Ok(object? value, object? message) + { + if (!IsTruthy(value)) + { + var msg = message?.ToString() ?? "The expression evaluated to a falsy value"; + throw new AssertionError(msg, value, true, "ok"); + } + } + + public static void StrictEqual(object? actual, object? expected, object? message) + { + if (!StrictEquals(actual, expected)) + { + var msg = message?.ToString() ?? "Expected values to be strictly equal"; + throw new AssertionError(msg, actual, expected, "strictEqual"); + } + } + + public static void NotStrictEqual(object? actual, object? expected, object? message) + { + if (StrictEquals(actual, expected)) + { + var msg = message?.ToString() ?? "Expected values not to be strictly equal"; + throw new AssertionError(msg, actual, expected, "notStrictEqual"); + } + } + + public static void DeepStrictEqual(object? actual, object? expected, object? message) + { + if (!DeepEquals(actual, expected)) + { + var msg = message?.ToString() ?? $"Expected values to be deeply equal"; + throw new AssertionError(msg, actual, expected, "deepStrictEqual"); + } + } + + public static void NotDeepStrictEqual(object? actual, object? expected, object? message) + { + if (DeepEquals(actual, expected)) + { + var msg = message?.ToString() ?? $"Expected values not to be deeply equal"; + throw new AssertionError(msg, actual, expected, "notDeepStrictEqual"); + } + } + + public static void Throws(object? fn, object? message) + { + if (fn == null) + { + throw new AssertionError(message?.ToString() ?? "Missing function to test", null, null, "throws"); + } + + bool threw = false; + try + { + // Try to invoke the function + if (fn is Delegate del) + { + del.DynamicInvoke([]); + } + else + { + // Try via reflection for TSFunction + var invokeMethod = fn.GetType().GetMethod("Invoke"); + if (invokeMethod != null) + { + invokeMethod.Invoke(fn, [Array.Empty()]); + } + else + { + throw new AssertionError("First argument must be a function", fn, null, "throws"); + } + } + } + catch (AssertionError) + { + // Re-throw assertion errors from nested assertions + throw; + } + catch (Exception ex) when (ex is not AssertionError) + { + threw = true; + } + + if (!threw) + { + throw new AssertionError(message?.ToString() ?? "Missing expected exception", null, null, "throws"); + } + } + + public static void DoesNotThrow(object? fn, object? message) + { + if (fn == null) + { + throw new AssertionError(message?.ToString() ?? "Missing function to test", null, null, "doesNotThrow"); + } + + try + { + if (fn is Delegate del) + { + del.DynamicInvoke([]); + } + else + { + var invokeMethod = fn.GetType().GetMethod("Invoke"); + if (invokeMethod != null) + { + invokeMethod.Invoke(fn, [Array.Empty()]); + } + else + { + throw new AssertionError("First argument must be a function", fn, null, "doesNotThrow"); + } + } + } + catch (Exception ex) when (ex is not AssertionError) + { + throw new AssertionError(message?.ToString() ?? $"Got unwanted exception: {ex.Message}", ex, null, "doesNotThrow"); + } + } + + public static void Equal(object? actual, object? expected, object? message) + { + if (!LooseEquals(actual, expected)) + { + var msg = message?.ToString() ?? "Expected values to be loosely equal"; + throw new AssertionError(msg, actual, expected, "equal"); + } + } + + public static void NotEqual(object? actual, object? expected, object? message) + { + if (LooseEquals(actual, expected)) + { + var msg = message?.ToString() ?? "Expected values not to be loosely equal"; + throw new AssertionError(msg, actual, expected, "notEqual"); + } + } + + private static bool DeepEquals(object? a, object? b) + { + if (a == null && b == null) return true; + if (a == null || b == null) return false; + + // Primitives + if (a is double || a is string || a is bool || b is double || b is string || b is bool) + { + return StrictEquals(a, b); + } + + // Arrays (List in compiled mode) + if (a is List listA && b is List listB) + { + if (listA.Count != listB.Count) return false; + for (int i = 0; i < listA.Count; i++) + { + if (!DeepEquals(listA[i], listB[i])) return false; + } + return true; + } + + // Objects (Dictionary in compiled mode) + if (a is Dictionary dictA && b is Dictionary dictB) + { + if (dictA.Count != dictB.Count) return false; + foreach (var kvp in dictA) + { + if (!dictB.TryGetValue(kvp.Key, out var valueB)) + return false; + if (!DeepEquals(kvp.Value, valueB)) + return false; + } + return true; + } + + return ReferenceEquals(a, b); + } + + private static bool StrictEquals(object? a, object? b) + { + if (a == null && b == null) return true; + if (a == null || b == null) return false; + if (a.GetType() != b.GetType()) return false; + + return a switch + { + double da when b is double db => da.Equals(db), + string sa when b is string sb => sa == sb, + bool ba when b is bool bb => ba == bb, + _ => ReferenceEquals(a, b) + }; + } + + private static bool LooseEquals(object? a, object? b) + { + if (a == null && b == null) return true; + if (a == null || b == null) return false; + + if (a.GetType() == b.GetType()) + return StrictEquals(a, b); + + // Try numeric coercion + if (TryToNumber(a, out var numA) && TryToNumber(b, out var numB)) + return numA == numB; + + return a.ToString() == b.ToString(); + } + + private static bool TryToNumber(object? value, out double result) + { + result = 0; + if (value == null) return false; + if (value is double d) { result = d; return true; } + if (value is int i) { result = i; return true; } + if (value is string s && double.TryParse(s, out result)) return true; + if (value is bool b) { result = b ? 1 : 0; return true; } + return false; + } + + private static bool IsTruthy(object? value) + { + return value switch + { + null => false, + bool b => b, + double d => d != 0 && !double.IsNaN(d), + string s => s.Length > 0, + _ => true + }; + } +} diff --git a/Compilation/RuntimeEmitter.ProcessHelpers.cs b/Compilation/RuntimeEmitter.ProcessHelpers.cs index 387c7a4..29508bf 100644 --- a/Compilation/RuntimeEmitter.ProcessHelpers.cs +++ b/Compilation/RuntimeEmitter.ProcessHelpers.cs @@ -6,12 +6,15 @@ namespace SharpTS.Compilation; public partial class RuntimeEmitter { /// - /// Emits process global helper methods (GetEnv, GetArgv). + /// Emits process global helper methods (GetEnv, GetArgv, Hrtime, Uptime, MemoryUsage). /// private void EmitProcessMethods(TypeBuilder typeBuilder, EmittedRuntime runtime) { EmitProcessGetEnv(typeBuilder, runtime); EmitProcessGetArgv(typeBuilder, runtime); + EmitProcessHrtime(typeBuilder, runtime); + EmitProcessUptime(typeBuilder, runtime); + EmitProcessMemoryUsage(typeBuilder, runtime); } /// @@ -121,4 +124,272 @@ private void EmitProcessGetArgv(TypeBuilder typeBuilder, EmittedRuntime runtime) il.Emit(OpCodes.Ret); } + + /// + /// Emits: public static object ProcessHrtime(object? prev) + /// Returns a [seconds, nanoseconds] tuple as a SharpTSArray. + /// + private void EmitProcessHrtime(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "ProcessHrtime", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + [_types.Object] + ); + runtime.ProcessHrtime = method; + + var il = method.GetILGenerator(); + + // Get static field references for start timestamp and frequency + var stopwatchType = _types.Stopwatch; + var getTimestampMethod = _types.GetMethodNoParams(stopwatchType, "GetTimestamp"); + var frequencyField = _types.GetField(stopwatchType, "Frequency"); + + // Store initial values + // We need to store current ticks first + il.Emit(OpCodes.Call, getTimestampMethod); + var currentTicksLocal = il.DeclareLocal(_types.Int64); + il.Emit(OpCodes.Stloc, currentTicksLocal); + + // Get frequency + il.Emit(OpCodes.Ldsfld, frequencyField); + var frequencyLocal = il.DeclareLocal(_types.Int64); + il.Emit(OpCodes.Stloc, frequencyLocal); + + // Calculate total nanoseconds: (currentTicks * 1_000_000_000.0) / frequency + il.Emit(OpCodes.Ldloc, currentTicksLocal); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Ldc_R8, 1_000_000_000.0); + il.Emit(OpCodes.Mul); + il.Emit(OpCodes.Ldloc, frequencyLocal); + il.Emit(OpCodes.Conv_R8); + il.Emit(OpCodes.Div); + var totalNanosLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Stloc, totalNanosLocal); + + // Check if prev argument is not null and is a List + var noPrevTime = il.DefineLabel(); + + il.Emit(OpCodes.Ldarg_0); // prev + il.Emit(OpCodes.Brfalse, noPrevTime); + + // Try to check if prev is a List + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.ListOfObject); + il.Emit(OpCodes.Brfalse, noPrevTime); + + // prev is a List, use it directly + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.ListOfObject); + var elementsLocal = il.DeclareLocal(_types.ListOfObject); + il.Emit(OpCodes.Stloc, elementsLocal); + + // Check if we have at least 2 elements + il.Emit(OpCodes.Ldloc, elementsLocal); + il.Emit(OpCodes.Callvirt, _types.GetPropertyGetter(_types.ListOfObject, "Count")); + il.Emit(OpCodes.Ldc_I4_2); + il.Emit(OpCodes.Blt, noPrevTime); + + // Get prevSeconds = elements[0] + il.Emit(OpCodes.Ldloc, elementsLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ListOfObject, "get_Item", _types.Int32)); + il.Emit(OpCodes.Unbox_Any, _types.Double); + var prevSecondsLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Stloc, prevSecondsLocal); + + // Get prevNanos = elements[1] + il.Emit(OpCodes.Ldloc, elementsLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ListOfObject, "get_Item", _types.Int32)); + il.Emit(OpCodes.Unbox_Any, _types.Double); + var prevNanosLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Stloc, prevNanosLocal); + + // Calculate prevTotalNanos = prevSeconds * 1_000_000_000.0 + prevNanos + il.Emit(OpCodes.Ldloc, prevSecondsLocal); + il.Emit(OpCodes.Ldc_R8, 1_000_000_000.0); + il.Emit(OpCodes.Mul); + il.Emit(OpCodes.Ldloc, prevNanosLocal); + il.Emit(OpCodes.Add); + + // Subtract from totalNanos + il.Emit(OpCodes.Ldloc, totalNanosLocal); + il.Emit(OpCodes.Sub); + il.Emit(OpCodes.Neg); // We computed (prev - current), need (current - prev) + il.Emit(OpCodes.Stloc, totalNanosLocal); + + il.MarkLabel(noPrevTime); + + // Calculate seconds = floor(totalNanos / 1_000_000_000.0) + il.Emit(OpCodes.Ldloc, totalNanosLocal); + il.Emit(OpCodes.Ldc_R8, 1_000_000_000.0); + il.Emit(OpCodes.Div); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Math, "Floor", _types.Double)); + var secondsLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Stloc, secondsLocal); + + // Calculate nanos = totalNanos % 1_000_000_000.0 + il.Emit(OpCodes.Ldloc, totalNanosLocal); + il.Emit(OpCodes.Ldc_R8, 1_000_000_000.0); + il.Emit(OpCodes.Rem); + var nanosLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Stloc, nanosLocal); + + // Create new List + il.Emit(OpCodes.Newobj, _types.GetDefaultConstructor(_types.ListOfObject)); + var resultLocal = il.DeclareLocal(_types.ListOfObject); + il.Emit(OpCodes.Stloc, resultLocal); + + // Add seconds (boxed) + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldloc, secondsLocal); + il.Emit(OpCodes.Box, _types.Double); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ListOfObject, "Add", _types.Object)); + + // Add nanos (boxed) + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldloc, nanosLocal); + il.Emit(OpCodes.Box, _types.Double); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ListOfObject, "Add", _types.Object)); + + // Return the list directly (compiled arrays are List) + il.Emit(OpCodes.Ldloc, resultLocal); + + il.Emit(OpCodes.Ret); + } + + /// + /// Emits: public static double ProcessUptime() + /// Returns the number of seconds the process has been running. + /// + private void EmitProcessUptime(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "ProcessUptime", + MethodAttributes.Public | MethodAttributes.Static, + _types.Double, + Type.EmptyTypes + ); + runtime.ProcessUptime = method; + + var il = method.GetILGenerator(); + + // Get current process: Process.GetCurrentProcess() + il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.Process, "GetCurrentProcess")); + var processLocal = il.DeclareLocal(_types.Process); + il.Emit(OpCodes.Stloc, processLocal); + + // Get start time: process.StartTime + il.Emit(OpCodes.Ldloc, processLocal); + il.Emit(OpCodes.Callvirt, _types.GetPropertyGetter(_types.Process, "StartTime")); + var startTimeLocal = il.DeclareLocal(_types.DateTime); + il.Emit(OpCodes.Stloc, startTimeLocal); + + // Get current time: DateTime.UtcNow + il.Emit(OpCodes.Call, _types.GetPropertyGetter(_types.DateTime, "UtcNow")); + var nowLocal = il.DeclareLocal(_types.DateTime); + il.Emit(OpCodes.Stloc, nowLocal); + + // Convert start time to UTC + il.Emit(OpCodes.Ldloca, startTimeLocal); + il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.DateTime, "ToUniversalTime")); + il.Emit(OpCodes.Stloc, startTimeLocal); + + // Calculate difference: now - startTime + il.Emit(OpCodes.Ldloc, nowLocal); + il.Emit(OpCodes.Ldloc, startTimeLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.DateTime, "op_Subtraction", _types.DateTime, _types.DateTime)); + var spanLocal = il.DeclareLocal(_types.TimeSpan); + il.Emit(OpCodes.Stloc, spanLocal); + + // Get TotalSeconds + il.Emit(OpCodes.Ldloca, spanLocal); + il.Emit(OpCodes.Call, _types.GetPropertyGetter(_types.TimeSpan, "TotalSeconds")); + + il.Emit(OpCodes.Ret); + } + + /// + /// Emits: public static object ProcessMemoryUsage() + /// Returns an object with memory usage information. + /// + private void EmitProcessMemoryUsage(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "ProcessMemoryUsage", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + Type.EmptyTypes + ); + runtime.ProcessMemoryUsage = method; + + var il = method.GetILGenerator(); + + // Get current process for WorkingSet64 + il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.Process, "GetCurrentProcess")); + var processLocal = il.DeclareLocal(_types.Process); + il.Emit(OpCodes.Stloc, processLocal); + + // Get rss (WorkingSet64) + il.Emit(OpCodes.Ldloc, processLocal); + il.Emit(OpCodes.Callvirt, _types.GetPropertyGetter(_types.Process, "WorkingSet64")); + il.Emit(OpCodes.Conv_R8); + var rssLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Stloc, rssLocal); + + // Get heapUsed (GC.GetTotalMemory(false)) + il.Emit(OpCodes.Ldc_I4_0); // false + il.Emit(OpCodes.Call, _types.GetMethod(_types.GC, "GetTotalMemory", _types.Boolean)); + il.Emit(OpCodes.Conv_R8); + var heapUsedLocal = il.DeclareLocal(_types.Double); + il.Emit(OpCodes.Stloc, heapUsedLocal); + + // Create Dictionary + il.Emit(OpCodes.Newobj, _types.GetDefaultConstructor(_types.DictionaryStringObject)); + var dictLocal = il.DeclareLocal(_types.DictionaryStringObject); + il.Emit(OpCodes.Stloc, dictLocal); + + // Add rss + il.Emit(OpCodes.Ldloc, dictLocal); + il.Emit(OpCodes.Ldstr, "rss"); + il.Emit(OpCodes.Ldloc, rssLocal); + il.Emit(OpCodes.Box, _types.Double); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "set_Item")); + + // Add heapTotal (same as heapUsed for now) + il.Emit(OpCodes.Ldloc, dictLocal); + il.Emit(OpCodes.Ldstr, "heapTotal"); + il.Emit(OpCodes.Ldloc, heapUsedLocal); + il.Emit(OpCodes.Box, _types.Double); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "set_Item")); + + // Add heapUsed + il.Emit(OpCodes.Ldloc, dictLocal); + il.Emit(OpCodes.Ldstr, "heapUsed"); + il.Emit(OpCodes.Ldloc, heapUsedLocal); + il.Emit(OpCodes.Box, _types.Double); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "set_Item")); + + // Add external (0.0) + il.Emit(OpCodes.Ldloc, dictLocal); + il.Emit(OpCodes.Ldstr, "external"); + il.Emit(OpCodes.Ldc_R8, 0.0); + il.Emit(OpCodes.Box, _types.Double); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "set_Item")); + + // Add arrayBuffers (0.0) + il.Emit(OpCodes.Ldloc, dictLocal); + il.Emit(OpCodes.Ldstr, "arrayBuffers"); + il.Emit(OpCodes.Ldc_R8, 0.0); + il.Emit(OpCodes.Box, _types.Double); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "set_Item")); + + // Wrap in SharpTSObject + il.Emit(OpCodes.Ldloc, dictLocal); + il.Emit(OpCodes.Call, runtime.CreateObject); + + il.Emit(OpCodes.Ret); + } } diff --git a/Compilation/RuntimeEmitter.QuerystringHelpers.cs b/Compilation/RuntimeEmitter.QuerystringHelpers.cs new file mode 100644 index 0000000..53269ca --- /dev/null +++ b/Compilation/RuntimeEmitter.QuerystringHelpers.cs @@ -0,0 +1,472 @@ +using System.Reflection; +using System.Reflection.Emit; + +namespace SharpTS.Compilation; + +public partial class RuntimeEmitter +{ + /// + /// Emits querystring module helper methods. + /// + private void EmitQuerystringMethods(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + EmitQuerystringParse(typeBuilder, runtime); + EmitQuerystringStringify(typeBuilder, runtime); + EmitQuerystringMethodWrappers(typeBuilder, runtime); + } + + /// + /// Emits wrapper methods for querystring functions that can be used as first-class values. + /// Uses individual object parameters (compatible with TSFunction.Invoke). + /// + private void EmitQuerystringMethodWrappers(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // escape(str) - 1 param + EmitQuerystringWrapperSimple(typeBuilder, runtime, "escape", 1, il => + { + // arg0?.ToString() ?? "" + var notNull = il.DefineLabel(); + var afterToString = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brtrue, notNull); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldstr, ""); + il.Emit(OpCodes.Br, afterToString); + il.MarkLabel(notNull); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); + il.MarkLabel(afterToString); + il.Emit(OpCodes.Call, typeof(Uri).GetMethod("EscapeDataString", [typeof(string)])!); + }); + + // unescape(str) - 1 param + EmitQuerystringWrapperSimple(typeBuilder, runtime, "unescape", 1, il => + { + // arg0?.ToString() ?? "" + var notNull = il.DefineLabel(); + var afterToString = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brtrue, notNull); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldstr, ""); + il.Emit(OpCodes.Br, afterToString); + il.MarkLabel(notNull); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); + il.MarkLabel(afterToString); + // Replace + with space, then unescape + il.Emit(OpCodes.Ldc_I4, '+'); + il.Emit(OpCodes.Ldc_I4, ' '); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "Replace", _types.Char, _types.Char)); + il.Emit(OpCodes.Call, typeof(Uri).GetMethod("UnescapeDataString", [typeof(string)])!); + }); + + // parse(str, sep?, eq?) - 3 params + EmitQuerystringParseWrapper(typeBuilder, runtime); + + // stringify(obj, sep?, eq?) - 3 params + EmitQuerystringStringifyWrapper(typeBuilder, runtime); + } + + /// + /// Emits a simple wrapper method for a querystring function. + /// + private void EmitQuerystringWrapperSimple( + TypeBuilder typeBuilder, + EmittedRuntime runtime, + string methodName, + int paramCount, + Action emitCall) + { + var paramTypes = new Type[paramCount]; + for (int i = 0; i < paramCount; i++) + paramTypes[i] = _types.Object; + + var method = typeBuilder.DefineMethod( + $"Querystring_{methodName}_Wrapper", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + paramTypes + ); + + var il = method.GetILGenerator(); + emitCall(il); + il.Emit(OpCodes.Ret); + + runtime.RegisterBuiltInModuleMethod("querystring", methodName, method); + } + + private void EmitQuerystringParseWrapper(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // parse(str, sep?, eq?) -> takes 3 object params + var method = typeBuilder.DefineMethod( + "Querystring_parse_Wrapper", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + [_types.Object, _types.Object, _types.Object] + ); + + var il = method.GetILGenerator(); + + // str = arg0?.ToString() ?? "" + var strNotNull = il.DefineLabel(); + var afterStr = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brtrue, strNotNull); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldstr, ""); + il.Emit(OpCodes.Br, afterStr); + il.MarkLabel(strNotNull); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); + il.MarkLabel(afterStr); + + // sep = arg1?.ToString() ?? "&" + var sepNotNull = il.DefineLabel(); + var afterSep = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brtrue, sepNotNull); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldstr, "&"); + il.Emit(OpCodes.Br, afterSep); + il.MarkLabel(sepNotNull); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); + il.MarkLabel(afterSep); + + // eq = arg2?.ToString() ?? "=" + var eqNotNull = il.DefineLabel(); + var afterEq = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brtrue, eqNotNull); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldstr, "="); + il.Emit(OpCodes.Br, afterEq); + il.MarkLabel(eqNotNull); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); + il.MarkLabel(afterEq); + + il.Emit(OpCodes.Call, runtime.QuerystringParse); + il.Emit(OpCodes.Ret); + + runtime.RegisterBuiltInModuleMethod("querystring", "parse", method); + runtime.RegisterBuiltInModuleMethod("querystring", "decode", method); // alias + } + + private void EmitQuerystringStringifyWrapper(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // stringify(obj, sep?, eq?) -> takes 3 object params + var method = typeBuilder.DefineMethod( + "Querystring_stringify_Wrapper", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + [_types.Object, _types.Object, _types.Object] + ); + + var il = method.GetILGenerator(); + + // obj = arg0 (pass as-is) + il.Emit(OpCodes.Ldarg_0); + + // sep = arg1?.ToString() ?? "&" + var sepNotNull = il.DefineLabel(); + var afterSep = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brtrue, sepNotNull); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldstr, "&"); + il.Emit(OpCodes.Br, afterSep); + il.MarkLabel(sepNotNull); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); + il.MarkLabel(afterSep); + + // eq = arg2?.ToString() ?? "=" + var eqNotNull = il.DefineLabel(); + var afterEq = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brtrue, eqNotNull); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldstr, "="); + il.Emit(OpCodes.Br, afterEq); + il.MarkLabel(eqNotNull); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); + il.MarkLabel(afterEq); + + il.Emit(OpCodes.Call, runtime.QuerystringStringify); + il.Emit(OpCodes.Ret); + + runtime.RegisterBuiltInModuleMethod("querystring", "stringify", method); + runtime.RegisterBuiltInModuleMethod("querystring", "encode", method); // alias + } + + /// + /// Emits: public static object QuerystringParse(string str, string sep, string eq) + /// Parses a query string into a Dictionary. + /// + private void EmitQuerystringParse(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "QuerystringParse", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + [_types.String, _types.String, _types.String] + ); + runtime.QuerystringParse = method; + + var il = method.GetILGenerator(); + + // Create new Dictionary + il.Emit(OpCodes.Newobj, _types.GetDefaultConstructor(_types.DictionaryStringObject)); + var resultLocal = il.DeclareLocal(_types.DictionaryStringObject); + il.Emit(OpCodes.Stloc, resultLocal); + + // If str is null or empty, return empty dict + var notEmpty = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); // str + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "IsNullOrEmpty", _types.String)); + il.Emit(OpCodes.Brfalse, notEmpty); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notEmpty); + + // Split string by separator + // str.Split(sep, StringSplitOptions.RemoveEmptyEntries) + il.Emit(OpCodes.Ldarg_0); // str + il.Emit(OpCodes.Ldarg_1); // sep + il.Emit(OpCodes.Ldc_I4_1); // StringSplitOptions.RemoveEmptyEntries + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "Split", _types.String, _types.StringSplitOptions)); + var pairsLocal = il.DeclareLocal(_types.StringArray); + il.Emit(OpCodes.Stloc, pairsLocal); + + // Loop through pairs + var indexLocal = il.DeclareLocal(_types.Int32); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, indexLocal); + + var loopStart = il.DefineLabel(); + var loopEnd = il.DefineLabel(); + + il.MarkLabel(loopStart); + // if (i >= pairs.Length) goto loopEnd + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldloc, pairsLocal); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Bge, loopEnd); + + // pair = pairs[i] + il.Emit(OpCodes.Ldloc, pairsLocal); + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldelem_Ref); + var pairLocal = il.DeclareLocal(_types.String); + il.Emit(OpCodes.Stloc, pairLocal); + + // eqIndex = pair.IndexOf(eq) + il.Emit(OpCodes.Ldloc, pairLocal); + il.Emit(OpCodes.Ldarg_2); // eq + il.Emit(OpCodes.Ldc_I4_4); // StringComparison.Ordinal + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "IndexOf", _types.String, _types.Resolve("System.StringComparison"))); + var eqIndexLocal = il.DeclareLocal(_types.Int32); + il.Emit(OpCodes.Stloc, eqIndexLocal); + + // Declare key and value locals + var keyLocal = il.DeclareLocal(_types.String); + var valueLocal = il.DeclareLocal(_types.String); + + // if (eqIndex >= 0) + var noEq = il.DefineLabel(); + var afterKV = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, eqIndexLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Blt, noEq); + + // key = pair.Substring(0, eqIndex) + il.Emit(OpCodes.Ldloc, pairLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldloc, eqIndexLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "Substring", _types.Int32, _types.Int32)); + // Replace + with space + il.Emit(OpCodes.Ldc_I4, '+'); + il.Emit(OpCodes.Ldc_I4, ' '); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "Replace", _types.Char, _types.Char)); + // Uri.UnescapeDataString + il.Emit(OpCodes.Call, typeof(Uri).GetMethod("UnescapeDataString", [typeof(string)])!); + il.Emit(OpCodes.Stloc, keyLocal); + + // value = pair.Substring(eqIndex + eq.Length) + il.Emit(OpCodes.Ldloc, pairLocal); + il.Emit(OpCodes.Ldloc, eqIndexLocal); + il.Emit(OpCodes.Ldarg_2); // eq + il.Emit(OpCodes.Callvirt, _types.GetPropertyGetter(_types.String, "Length")); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "Substring", _types.Int32)); + // Replace + with space + il.Emit(OpCodes.Ldc_I4, '+'); + il.Emit(OpCodes.Ldc_I4, ' '); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "Replace", _types.Char, _types.Char)); + // Uri.UnescapeDataString + il.Emit(OpCodes.Call, typeof(Uri).GetMethod("UnescapeDataString", [typeof(string)])!); + il.Emit(OpCodes.Stloc, valueLocal); + il.Emit(OpCodes.Br, afterKV); + + // else: key = pair, value = "" + il.MarkLabel(noEq); + il.Emit(OpCodes.Ldloc, pairLocal); + il.Emit(OpCodes.Ldc_I4, '+'); + il.Emit(OpCodes.Ldc_I4, ' '); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "Replace", _types.Char, _types.Char)); + il.Emit(OpCodes.Call, typeof(Uri).GetMethod("UnescapeDataString", [typeof(string)])!); + il.Emit(OpCodes.Stloc, keyLocal); + il.Emit(OpCodes.Ldstr, ""); + il.Emit(OpCodes.Stloc, valueLocal); + + il.MarkLabel(afterKV); + + // result[key] = value (simplified - doesn't handle arrays) + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldloc, keyLocal); + il.Emit(OpCodes.Ldloc, valueLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "set_Item")); + + // i++ + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, indexLocal); + il.Emit(OpCodes.Br, loopStart); + + il.MarkLabel(loopEnd); + + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits: public static string QuerystringStringify(object? obj, string sep, string eq) + /// Serializes an object into a query string. + /// + private void EmitQuerystringStringify(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "QuerystringStringify", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + [_types.Object, _types.String, _types.String] + ); + runtime.QuerystringStringify = method; + + var il = method.GetILGenerator(); + + // If obj is null, return "" + var notNull = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Brtrue, notNull); + il.Emit(OpCodes.Ldstr, ""); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notNull); + + // Create StringBuilder + il.Emit(OpCodes.Newobj, _types.GetDefaultConstructor(_types.StringBuilder)); + var sbLocal = il.DeclareLocal(_types.StringBuilder); + il.Emit(OpCodes.Stloc, sbLocal); + + // Check if obj is Dictionary + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _types.DictionaryStringObject); + var notDict = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, notDict); + + // Cast to Dictionary + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, _types.DictionaryStringObject); + var dictLocal = il.DeclareLocal(_types.DictionaryStringObject); + il.Emit(OpCodes.Stloc, dictLocal); + + // Get enumerator + il.Emit(OpCodes.Ldloc, dictLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.DictionaryStringObject, "GetEnumerator")); + var enumeratorLocal = il.DeclareLocal(_types.DictionaryStringObjectEnumerator); + il.Emit(OpCodes.Stloc, enumeratorLocal); + + var firstLocal = il.DeclareLocal(_types.Boolean); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Stloc, firstLocal); + + // Loop through dictionary entries + var dictLoopStart = il.DefineLabel(); + var dictLoopEnd = il.DefineLabel(); + + il.MarkLabel(dictLoopStart); + il.Emit(OpCodes.Ldloca, enumeratorLocal); + il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.DictionaryStringObjectEnumerator, "MoveNext")); + il.Emit(OpCodes.Brfalse, dictLoopEnd); + + // Get current key-value pair + il.Emit(OpCodes.Ldloca, enumeratorLocal); + il.Emit(OpCodes.Call, _types.GetPropertyGetter(_types.DictionaryStringObjectEnumerator, "Current")); + var kvpLocal = il.DeclareLocal(_types.KeyValuePairStringObject); + il.Emit(OpCodes.Stloc, kvpLocal); + + // If not first, append sep + var skipSep = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, firstLocal); + il.Emit(OpCodes.Brtrue, skipSep); + il.Emit(OpCodes.Ldloc, sbLocal); + il.Emit(OpCodes.Ldarg_1); // sep + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + il.MarkLabel(skipSep); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, firstLocal); + + // Append escaped key + il.Emit(OpCodes.Ldloc, sbLocal); + il.Emit(OpCodes.Ldloca, kvpLocal); + il.Emit(OpCodes.Call, _types.GetPropertyGetter(_types.KeyValuePairStringObject, "Key")); + il.Emit(OpCodes.Call, typeof(Uri).GetMethod("EscapeDataString", [typeof(string)])!); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + // Append eq + il.Emit(OpCodes.Ldloc, sbLocal); + il.Emit(OpCodes.Ldarg_2); // eq + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + // Append escaped value + il.Emit(OpCodes.Ldloc, sbLocal); + il.Emit(OpCodes.Ldloca, kvpLocal); + il.Emit(OpCodes.Call, _types.GetPropertyGetter(_types.KeyValuePairStringObject, "Value")); + // value?.ToString() ?? "" + var valueNotNullLabel = il.DefineLabel(); + var afterValueLabel = il.DefineLabel(); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brtrue, valueNotNullLabel); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldstr, ""); + il.Emit(OpCodes.Br, afterValueLabel); + il.MarkLabel(valueNotNullLabel); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString")); + il.MarkLabel(afterValueLabel); + il.Emit(OpCodes.Call, typeof(Uri).GetMethod("EscapeDataString", [typeof(string)])!); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.StringBuilder, "Append", _types.String)); + il.Emit(OpCodes.Pop); + + il.Emit(OpCodes.Br, dictLoopStart); + + il.MarkLabel(dictLoopEnd); + + il.MarkLabel(notDict); + + // Return sb.ToString() + il.Emit(OpCodes.Ldloc, sbLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.StringBuilder, "ToString")); + il.Emit(OpCodes.Ret); + } +} diff --git a/Compilation/RuntimeEmitter.UrlHelpers.cs b/Compilation/RuntimeEmitter.UrlHelpers.cs new file mode 100644 index 0000000..6a603a3 --- /dev/null +++ b/Compilation/RuntimeEmitter.UrlHelpers.cs @@ -0,0 +1,258 @@ +using System.Reflection; +using System.Reflection.Emit; +using SharpTS.Runtime.Types; + +namespace SharpTS.Compilation; + +public partial class RuntimeEmitter +{ + /// + /// Emits url module helper methods. + /// + private void EmitUrlMethods(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + EmitUrlParse(typeBuilder, runtime); + EmitUrlFormat(typeBuilder, runtime); + EmitUrlResolve(typeBuilder, runtime); + EmitUrlMethodWrappers(typeBuilder, runtime); + } + + /// + /// Emits wrapper methods for url functions that can be used as first-class values. + /// + private void EmitUrlMethodWrappers(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // parse(url) - 1 param + EmitUrlWrapperSimple(typeBuilder, runtime, "parse", 1, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.UrlParse); + }); + + // format(urlObj) - 1 param + EmitUrlWrapperSimple(typeBuilder, runtime, "format", 1, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.UrlFormat); + }); + + // resolve(from, to) - 2 params + EmitUrlWrapperSimple(typeBuilder, runtime, "resolve", 2, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.UrlResolve); + }); + } + + private void EmitUrlWrapperSimple( + TypeBuilder typeBuilder, + EmittedRuntime runtime, + string methodName, + int paramCount, + Action emitCall) + { + var paramTypes = new Type[paramCount]; + for (int i = 0; i < paramCount; i++) + paramTypes[i] = _types.Object; + + var method = typeBuilder.DefineMethod( + $"Url_{methodName}_Wrapper", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + paramTypes + ); + + var il = method.GetILGenerator(); + emitCall(il); + il.Emit(OpCodes.Ret); + + runtime.RegisterBuiltInModuleMethod("url", methodName, method); + } + + /// + /// Emits: public static object UrlParse(object? url) + /// + private void EmitUrlParse(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "UrlParse", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + [_types.Object] + ); + runtime.UrlParse = method; + + var il = method.GetILGenerator(); + + // Call static helper: UrlHelpers.Parse(url) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, typeof(UrlHelpers).GetMethod("Parse", BindingFlags.Public | BindingFlags.Static)!); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits: public static string UrlFormat(object? urlObj) + /// + private void EmitUrlFormat(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "UrlFormat", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + [_types.Object] + ); + runtime.UrlFormat = method; + + var il = method.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, typeof(UrlHelpers).GetMethod("Format", BindingFlags.Public | BindingFlags.Static)!); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits: public static string UrlResolve(object? from, object? to) + /// + private void EmitUrlResolve(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "UrlResolve", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + [_types.Object, _types.Object] + ); + runtime.UrlResolve = method; + + var il = method.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, typeof(UrlHelpers).GetMethod("Resolve", BindingFlags.Public | BindingFlags.Static)!); + il.Emit(OpCodes.Ret); + } +} + +/// +/// Static helper methods for url module in compiled mode. +/// +public static class UrlHelpers +{ + public static object Parse(object? url) + { + if (url == null) + return new Dictionary(); + + var urlString = url.ToString()!; + + try + { + var uri = new Uri(urlString, UriKind.Absolute); + return CreateUrlObject(uri); + } + catch + { + // Try parsing as relative URL + try + { + var uri = new Uri("http://localhost/" + urlString.TrimStart('/'), UriKind.Absolute); + return CreateUrlObject(uri, isRelative: true, originalPath: urlString); + } + catch + { + // Return partial object for invalid URLs + return new Dictionary + { + ["href"] = urlString, + ["path"] = urlString + }; + } + } + } + + private static Dictionary CreateUrlObject(Uri uri, bool isRelative = false, string? originalPath = null) + { + var result = new Dictionary + { + ["protocol"] = uri.Scheme + ":", + ["slashes"] = true, + ["auth"] = string.IsNullOrEmpty(uri.UserInfo) ? null : uri.UserInfo, + ["host"] = uri.IsDefaultPort ? uri.Host : $"{uri.Host}:{uri.Port}", + ["port"] = uri.IsDefaultPort ? null : uri.Port.ToString(), + ["hostname"] = uri.Host, + ["hash"] = string.IsNullOrEmpty(uri.Fragment) ? null : uri.Fragment, + ["search"] = string.IsNullOrEmpty(uri.Query) ? null : uri.Query, + ["query"] = string.IsNullOrEmpty(uri.Query) ? null : uri.Query.TrimStart('?'), + ["pathname"] = uri.AbsolutePath, + ["path"] = uri.PathAndQuery, + ["href"] = uri.AbsoluteUri + }; + + if (isRelative && originalPath != null) + { + result["protocol"] = null; + result["slashes"] = null; + result["host"] = null; + result["hostname"] = null; + result["path"] = originalPath; + result["pathname"] = originalPath.Split('?')[0]; + result["href"] = originalPath; + } + + return result; + } + + public static string Format(object? urlObj) + { + if (urlObj == null) + return ""; + + if (urlObj is SharpTSURL url) + return url.Href; + + if (urlObj is string s) + return s; + + if (urlObj is Dictionary dict) + { + // Build URL from object parts + var protocol = dict.GetValueOrDefault("protocol")?.ToString() ?? ""; + var hostname = dict.GetValueOrDefault("hostname")?.ToString() ?? + dict.GetValueOrDefault("host")?.ToString() ?? ""; + var port = dict.GetValueOrDefault("port")?.ToString(); + var pathname = dict.GetValueOrDefault("pathname")?.ToString() ?? "/"; + var search = dict.GetValueOrDefault("search")?.ToString() ?? ""; + var hash = dict.GetValueOrDefault("hash")?.ToString() ?? ""; + + var host = !string.IsNullOrEmpty(port) ? $"{hostname}:{port}" : hostname; + + var slashes = dict.GetValueOrDefault("slashes"); + var slashStr = slashes is true || (protocol.Length > 0 && slashes is not false) ? "//" : ""; + + return $"{protocol}{slashStr}{host}{pathname}{search}{hash}"; + } + + return urlObj.ToString() ?? ""; + } + + public static string Resolve(object? from, object? to) + { + var fromStr = from?.ToString() ?? ""; + var toStr = to?.ToString() ?? ""; + + if (string.IsNullOrEmpty(fromStr)) + return toStr; + + try + { + var baseUri = new Uri(fromStr, UriKind.Absolute); + var resolvedUri = new Uri(baseUri, toStr); + return resolvedUri.AbsoluteUri; + } + catch + { + // If base isn't absolute, try best effort + return toStr.StartsWith('/') ? toStr : $"{fromStr.TrimEnd('/')}/{toStr}"; + } + } +} diff --git a/Compilation/RuntimeEmitter.cs b/Compilation/RuntimeEmitter.cs index 43c8d27..3ed94b7 100644 --- a/Compilation/RuntimeEmitter.cs +++ b/Compilation/RuntimeEmitter.cs @@ -1305,6 +1305,12 @@ private void EmitRuntimeClass(ModuleBuilder moduleBuilder, EmittedRuntime runtim EmitPathModulePropertyWrappers(typeBuilder, runtime); // Process global methods (env, argv) EmitProcessMethods(typeBuilder, runtime); + // Querystring module methods + EmitQuerystringMethods(typeBuilder, runtime); + // Assert module methods + EmitAssertMethods(typeBuilder, runtime); + // URL module methods + EmitUrlMethods(typeBuilder, runtime); // Console extensions (error, warn, clear, time, timeEnd, timeLog) EmitConsoleExtensions(typeBuilder, runtime); diff --git a/Compilation/TypeProvider.cs b/Compilation/TypeProvider.cs index 88f9d65..99a7c65 100644 --- a/Compilation/TypeProvider.cs +++ b/Compilation/TypeProvider.cs @@ -119,6 +119,8 @@ private TypeProvider(Assembly coreAssembly) public Type Stopwatch => Resolve("System.Diagnostics.Stopwatch"); public Type Interlocked => Resolve("System.Threading.Interlocked"); public Type Version => Resolve("System.Version"); + public Type GC => Resolve("System.GC"); + public Type Process => Resolve("System.Diagnostics.Process"); #endregion @@ -347,7 +349,9 @@ private Type ResolveCore(string fullName) ?? typeof(System.Console).Assembly.GetType(fullName) ?? typeof(System.Text.StringBuilder).Assembly.GetType(fullName) ?? typeof(System.Convert).Assembly.GetType(fullName) - ?? typeof(System.Text.Json.JsonSerializer).Assembly.GetType(fullName); + ?? typeof(System.Text.Json.JsonSerializer).Assembly.GetType(fullName) + ?? typeof(System.Diagnostics.Process).Assembly.GetType(fullName) + ?? typeof(System.Diagnostics.Stopwatch).Assembly.GetType(fullName); } if (type == null) diff --git a/Runtime/BuiltIns/Modules/BuiltInModuleRegistry.cs b/Runtime/BuiltIns/Modules/BuiltInModuleRegistry.cs index b57ed75..1efc7ef 100644 --- a/Runtime/BuiltIns/Modules/BuiltInModuleRegistry.cs +++ b/Runtime/BuiltIns/Modules/BuiltInModuleRegistry.cs @@ -11,7 +11,10 @@ public static class BuiltInModuleRegistry [ "fs", "path", - "os" + "os", + "querystring", + "assert", + "url" ]; /// diff --git a/Runtime/BuiltIns/Modules/Interpreter/AssertModuleInterpreter.cs b/Runtime/BuiltIns/Modules/Interpreter/AssertModuleInterpreter.cs new file mode 100644 index 0000000..064cb9d --- /dev/null +++ b/Runtime/BuiltIns/Modules/Interpreter/AssertModuleInterpreter.cs @@ -0,0 +1,455 @@ +using SharpTS.Runtime.Types; +using Interp = SharpTS.Execution.Interpreter; + +namespace SharpTS.Runtime.BuiltIns.Modules.Interpreter; + +/// +/// Interpreter-mode implementation of the Node.js 'assert' module. +/// +/// +/// Provides assertion functions for testing. +/// Throws AssertionError when assertions fail. +/// +public static class AssertModuleInterpreter +{ + /// + /// Gets all exported values for the assert module. + /// + public static Dictionary GetExports() + { + return new Dictionary + { + ["ok"] = new BuiltInMethod("ok", 1, 2, Ok), + ["strictEqual"] = new BuiltInMethod("strictEqual", 2, 3, StrictEqual), + ["notStrictEqual"] = new BuiltInMethod("notStrictEqual", 2, 3, NotStrictEqual), + ["deepStrictEqual"] = new BuiltInMethod("deepStrictEqual", 2, 3, DeepStrictEqual), + ["notDeepStrictEqual"] = new BuiltInMethod("notDeepStrictEqual", 2, 3, NotDeepStrictEqual), + ["throws"] = new BuiltInMethod("throws", 1, 2, Throws), + ["doesNotThrow"] = new BuiltInMethod("doesNotThrow", 1, 2, DoesNotThrow), + ["fail"] = new BuiltInMethod("fail", 0, 1, Fail), + ["equal"] = new BuiltInMethod("equal", 2, 3, Equal), + ["notEqual"] = new BuiltInMethod("notEqual", 2, 3, NotEqual) + }; + } + + /// + /// assert.ok(value, message?) - throws if value is falsy. + /// + private static object? Ok(Interp interpreter, object? receiver, List args) + { + var value = args.Count > 0 ? args[0] : null; + var message = args.Count > 1 ? args[1]?.ToString() : null; + + if (!IsTruthy(value)) + { + ThrowAssertionError( + message ?? "The expression evaluated to a falsy value", + value, + true, + "ok" + ); + } + + return null; + } + + /// + /// assert.strictEqual(actual, expected, message?) - throws if actual !== expected. + /// + private static object? StrictEqual(Interp interpreter, object? receiver, List args) + { + var actual = args.Count > 0 ? args[0] : null; + var expected = args.Count > 1 ? args[1] : null; + var message = args.Count > 2 ? args[2]?.ToString() : null; + + if (!StrictEquals(actual, expected)) + { + ThrowAssertionError( + message ?? $"Expected values to be strictly equal:\n{Stringify(actual)}\nshould equal\n{Stringify(expected)}", + actual, + expected, + "strictEqual" + ); + } + + return null; + } + + /// + /// assert.notStrictEqual(actual, expected, message?) - throws if actual === expected. + /// + private static object? NotStrictEqual(Interp interpreter, object? receiver, List args) + { + var actual = args.Count > 0 ? args[0] : null; + var expected = args.Count > 1 ? args[1] : null; + var message = args.Count > 2 ? args[2]?.ToString() : null; + + if (StrictEquals(actual, expected)) + { + ThrowAssertionError( + message ?? $"Expected values to be strictly unequal: {Stringify(actual)}", + actual, + expected, + "notStrictEqual" + ); + } + + return null; + } + + /// + /// assert.deepStrictEqual(actual, expected, message?) - deep comparison. + /// + private static object? DeepStrictEqual(Interp interpreter, object? receiver, List args) + { + var actual = args.Count > 0 ? args[0] : null; + var expected = args.Count > 1 ? args[1] : null; + var message = args.Count > 2 ? args[2]?.ToString() : null; + + if (!DeepEquals(actual, expected)) + { + ThrowAssertionError( + message ?? $"Expected values to be deeply equal:\n{Stringify(actual)}\nshould equal\n{Stringify(expected)}", + actual, + expected, + "deepStrictEqual" + ); + } + + return null; + } + + /// + /// assert.notDeepStrictEqual(actual, expected, message?) - throws if deep equal. + /// + private static object? NotDeepStrictEqual(Interp interpreter, object? receiver, List args) + { + var actual = args.Count > 0 ? args[0] : null; + var expected = args.Count > 1 ? args[1] : null; + var message = args.Count > 2 ? args[2]?.ToString() : null; + + if (DeepEquals(actual, expected)) + { + ThrowAssertionError( + message ?? $"Expected values not to be deeply equal: {Stringify(actual)}", + actual, + expected, + "notDeepStrictEqual" + ); + } + + return null; + } + + /// + /// assert.throws(fn, message?) - throws if fn doesn't throw. + /// + private static object? Throws(Interp interpreter, object? receiver, List args) + { + var fn = args.Count > 0 ? args[0] : null; + var message = args.Count > 1 ? args[1]?.ToString() : null; + + if (fn == null) + { + ThrowAssertionError(message ?? "Missing function to test", null, null, "throws"); + return null; + } + + bool threw = false; + try + { + if (fn is SharpTSFunction tsFunc) + { + tsFunc.Call(interpreter, []); + } + else if (fn is BuiltInMethod builtIn) + { + builtIn.Call(interpreter, []); + } + else + { + ThrowAssertionError("First argument must be a function", fn, null, "throws"); + } + } + catch + { + threw = true; + } + + if (!threw) + { + ThrowAssertionError( + message ?? "Missing expected exception", + null, + null, + "throws" + ); + } + + return null; + } + + /// + /// assert.doesNotThrow(fn, message?) - throws if fn throws. + /// + private static object? DoesNotThrow(Interp interpreter, object? receiver, List args) + { + var fn = args.Count > 0 ? args[0] : null; + var message = args.Count > 1 ? args[1]?.ToString() : null; + + if (fn == null) + { + ThrowAssertionError(message ?? "Missing function to test", null, null, "doesNotThrow"); + return null; + } + + try + { + if (fn is SharpTSFunction tsFunc) + { + tsFunc.Call(interpreter, []); + } + else if (fn is BuiltInMethod builtIn) + { + builtIn.Call(interpreter, []); + } + else + { + ThrowAssertionError("First argument must be a function", fn, null, "doesNotThrow"); + } + } + catch (Exception ex) + { + ThrowAssertionError( + message ?? $"Got unwanted exception: {ex.Message}", + ex, + null, + "doesNotThrow" + ); + } + + return null; + } + + /// + /// assert.fail(message?) - always throws. + /// + private static object? Fail(Interp interpreter, object? receiver, List args) + { + var message = args.Count > 0 ? args[0]?.ToString() : null; + ThrowAssertionError(message ?? "Failed", null, null, "fail"); + return null; + } + + /// + /// assert.equal(actual, expected, message?) - loose equality (==). + /// + private static object? Equal(Interp interpreter, object? receiver, List args) + { + var actual = args.Count > 0 ? args[0] : null; + var expected = args.Count > 1 ? args[1] : null; + var message = args.Count > 2 ? args[2]?.ToString() : null; + + if (!LooseEquals(actual, expected)) + { + ThrowAssertionError( + message ?? $"Expected values to be loosely equal:\n{Stringify(actual)}\nshould equal\n{Stringify(expected)}", + actual, + expected, + "equal" + ); + } + + return null; + } + + /// + /// assert.notEqual(actual, expected, message?) - loose inequality (!=). + /// + private static object? NotEqual(Interp interpreter, object? receiver, List args) + { + var actual = args.Count > 0 ? args[0] : null; + var expected = args.Count > 1 ? args[1] : null; + var message = args.Count > 2 ? args[2]?.ToString() : null; + + if (LooseEquals(actual, expected)) + { + ThrowAssertionError( + message ?? $"Expected values not to be loosely equal: {Stringify(actual)}", + actual, + expected, + "notEqual" + ); + } + + return null; + } + + // Helper methods + + private static bool IsTruthy(object? value) + { + return value switch + { + null => false, + bool b => b, + double d => d != 0 && !double.IsNaN(d), + string s => s.Length > 0, + _ => true + }; + } + + private static bool StrictEquals(object? a, object? b) + { + if (a == null && b == null) return true; + if (a == null || b == null) return false; + if (a.GetType() != b.GetType()) return false; + + return a switch + { + double da when b is double db => da.Equals(db), + string sa when b is string sb => sa == sb, + bool ba when b is bool bb => ba == bb, + _ => ReferenceEquals(a, b) + }; + } + + private static bool LooseEquals(object? a, object? b) + { + if (a == null && b == null) return true; + if (a == null || b == null) + { + // In JS, null == undefined + return false; + } + + // Same type - use strict equality + if (a.GetType() == b.GetType()) + { + return StrictEquals(a, b); + } + + // Try numeric coercion + if (TryToNumber(a, out var numA) && TryToNumber(b, out var numB)) + { + return numA == numB; + } + + // String comparison + return a.ToString() == b.ToString(); + } + + private static bool TryToNumber(object? value, out double result) + { + result = 0; + if (value == null) return false; + if (value is double d) { result = d; return true; } + if (value is int i) { result = i; return true; } + if (value is string s && double.TryParse(s, out result)) return true; + if (value is bool b) { result = b ? 1 : 0; return true; } + return false; + } + + private static bool DeepEquals(object? a, object? b) + { + if (a == null && b == null) return true; + if (a == null || b == null) return false; + + // Primitives + if (a is double || a is string || a is bool || b is double || b is string || b is bool) + { + return StrictEquals(a, b); + } + + // Arrays + if (a is List listA && b is List listB) + { + if (listA.Count != listB.Count) return false; + for (int i = 0; i < listA.Count; i++) + { + if (!DeepEquals(listA[i], listB[i])) return false; + } + return true; + } + + if (a is SharpTSArray arrA && b is SharpTSArray arrB) + { + if (arrA.Elements.Count != arrB.Elements.Count) return false; + for (int i = 0; i < arrA.Elements.Count; i++) + { + if (!DeepEquals(arrA.Elements[i], arrB.Elements[i])) return false; + } + return true; + } + + // Objects + if (a is SharpTSObject objA && b is SharpTSObject objB) + { + if (objA.Fields.Count != objB.Fields.Count) return false; + foreach (var kvp in objA.Fields) + { + if (!objB.Fields.TryGetValue(kvp.Key, out var valueB)) + return false; + if (!DeepEquals(kvp.Value, valueB)) + return false; + } + return true; + } + + if (a is Dictionary dictA && b is Dictionary dictB) + { + if (dictA.Count != dictB.Count) return false; + foreach (var kvp in dictA) + { + if (!dictB.TryGetValue(kvp.Key, out var valueB)) + return false; + if (!DeepEquals(kvp.Value, valueB)) + return false; + } + return true; + } + + // Fallback to reference equality + return ReferenceEquals(a, b); + } + + private static string Stringify(object? value) + { + if (value == null) return "null"; + if (value is string s) return $"\"{s}\""; + if (value is bool b) return b ? "true" : "false"; + if (value is double d) return d.ToString(System.Globalization.CultureInfo.InvariantCulture); + if (value is List list) + return $"[{string.Join(", ", list.Select(Stringify))}]"; + if (value is SharpTSArray arr) + return $"[{string.Join(", ", arr.Elements.Select(Stringify))}]"; + if (value is SharpTSObject obj) + return $"{{{string.Join(", ", obj.Fields.Select(kvp => $"{kvp.Key}: {Stringify(kvp.Value)}"))}}}"; + if (value is Dictionary dict) + return $"{{{string.Join(", ", dict.Select(kvp => $"{kvp.Key}: {Stringify(kvp.Value)}"))}}}"; + return value.ToString() ?? "undefined"; + } + + private static void ThrowAssertionError(string message, object? actual, object? expected, string @operator) + { + throw new AssertionError(message, actual, expected, @operator); + } +} + +/// +/// Custom error type for assertion failures. +/// +public class AssertionError : Exception +{ + public object? Actual { get; } + public object? Expected { get; } + public string Operator { get; } + + public AssertionError(string message, object? actual, object? expected, string @operator) + : base($"AssertionError: {message}") + { + Actual = actual; + Expected = expected; + Operator = @operator; + } +} diff --git a/Runtime/BuiltIns/Modules/Interpreter/BuiltInModuleValues.cs b/Runtime/BuiltIns/Modules/Interpreter/BuiltInModuleValues.cs index ae4656f..69c041f 100644 --- a/Runtime/BuiltIns/Modules/Interpreter/BuiltInModuleValues.cs +++ b/Runtime/BuiltIns/Modules/Interpreter/BuiltInModuleValues.cs @@ -23,6 +23,9 @@ public static class BuiltInModuleValues "fs" => FsModuleInterpreter.GetExports(), "path" => PathModuleInterpreter.GetExports(), "os" => OsModuleInterpreter.GetExports(), + "querystring" => QuerystringModuleInterpreter.GetExports(), + "assert" => AssertModuleInterpreter.GetExports(), + "url" => UrlModuleInterpreter.GetExports(), _ => throw new Exception($"Unknown built-in module: {moduleName}") }; } @@ -32,6 +35,6 @@ public static class BuiltInModuleValues /// public static bool HasInterpreterSupport(string moduleName) { - return moduleName is "fs" or "path" or "os"; + return moduleName is "fs" or "path" or "os" or "querystring" or "assert" or "url"; } } diff --git a/Runtime/BuiltIns/Modules/Interpreter/QuerystringModuleInterpreter.cs b/Runtime/BuiltIns/Modules/Interpreter/QuerystringModuleInterpreter.cs new file mode 100644 index 0000000..ad5b365 --- /dev/null +++ b/Runtime/BuiltIns/Modules/Interpreter/QuerystringModuleInterpreter.cs @@ -0,0 +1,163 @@ +using SharpTS.Runtime.Types; +using Interp = SharpTS.Execution.Interpreter; + +namespace SharpTS.Runtime.BuiltIns.Modules.Interpreter; + +/// +/// Interpreter-mode implementation of the Node.js 'querystring' module. +/// +/// +/// Provides runtime values for query string parsing and formatting. +/// +public static class QuerystringModuleInterpreter +{ + /// + /// Gets all exported values for the querystring module. + /// + public static Dictionary GetExports() + { + return new Dictionary + { + // Methods + ["parse"] = new BuiltInMethod("parse", 1, 4, Parse), + ["stringify"] = new BuiltInMethod("stringify", 1, 4, Stringify), + ["escape"] = new BuiltInMethod("escape", 1, 1, Escape), + ["unescape"] = new BuiltInMethod("unescape", 1, 1, Unescape), + // Aliases + ["decode"] = new BuiltInMethod("decode", 1, 4, Parse), + ["encode"] = new BuiltInMethod("encode", 1, 4, Stringify) + }; + } + + /// + /// Parses a query string into an object. + /// parse(str, sep='&', eq='=', options?) + /// + private static object? Parse(Interp interpreter, object? receiver, List args) + { + if (args.Count == 0) + return new SharpTSObject(new Dictionary()); + + var str = args[0]?.ToString() ?? ""; + var sep = args.Count > 1 && args[1] != null ? args[1].ToString()! : "&"; + var eq = args.Count > 2 && args[2] != null ? args[2].ToString()! : "="; + + var result = new Dictionary(); + + if (string.IsNullOrEmpty(str)) + return new SharpTSObject(result); + + var pairs = str.Split(sep, StringSplitOptions.RemoveEmptyEntries); + foreach (var pair in pairs) + { + var eqIndex = pair.IndexOf(eq, StringComparison.Ordinal); + string key, value; + + if (eqIndex >= 0) + { + key = Uri.UnescapeDataString(pair[..eqIndex].Replace('+', ' ')); + value = Uri.UnescapeDataString(pair[(eqIndex + eq.Length)..].Replace('+', ' ')); + } + else + { + key = Uri.UnescapeDataString(pair.Replace('+', ' ')); + value = ""; + } + + // If key already exists, convert to array or add to existing array + if (result.TryGetValue(key, out var existing)) + { + if (existing is SharpTSArray arr) + { + arr.Elements.Add(value); + } + else + { + result[key] = new SharpTSArray([existing, value]); + } + } + else + { + result[key] = value; + } + } + + return new SharpTSObject(result); + } + + /// + /// Serializes an object into a query string. + /// stringify(obj, sep='&', eq='=', options?) + /// + private static object? Stringify(Interp interpreter, object? receiver, List args) + { + if (args.Count == 0 || args[0] == null) + return ""; + + var sep = args.Count > 1 && args[1] != null ? args[1].ToString()! : "&"; + var eq = args.Count > 2 && args[2] != null ? args[2].ToString()! : "="; + + var pairs = new List(); + var obj = args[0]; + + if (obj is SharpTSObject tsObj) + { + foreach (var kvp in tsObj.Fields) + { + AddPairs(pairs, kvp.Key, kvp.Value, sep, eq); + } + } + else if (obj is Dictionary dict) + { + foreach (var kvp in dict) + { + AddPairs(pairs, kvp.Key, kvp.Value, sep, eq); + } + } + + return string.Join(sep, pairs); + } + + private static void AddPairs(List pairs, string key, object? value, string sep, string eq) + { + var encodedKey = Uri.EscapeDataString(key); + + if (value is SharpTSArray arr) + { + foreach (var item in arr.Elements) + { + var encodedValue = Uri.EscapeDataString(item?.ToString() ?? ""); + pairs.Add($"{encodedKey}{eq}{encodedValue}"); + } + } + else + { + var encodedValue = Uri.EscapeDataString(value?.ToString() ?? ""); + pairs.Add($"{encodedKey}{eq}{encodedValue}"); + } + } + + /// + /// Percent-encodes a string for use in a URL query string. + /// + private static object? Escape(Interp interpreter, object? receiver, List args) + { + if (args.Count == 0) + return ""; + + var str = args[0]?.ToString() ?? ""; + return Uri.EscapeDataString(str); + } + + /// + /// Decodes a percent-encoded string. + /// + private static object? Unescape(Interp interpreter, object? receiver, List args) + { + if (args.Count == 0) + return ""; + + var str = args[0]?.ToString() ?? ""; + return Uri.UnescapeDataString(str.Replace('+', ' ')); + } +} diff --git a/Runtime/BuiltIns/Modules/Interpreter/UrlModuleInterpreter.cs b/Runtime/BuiltIns/Modules/Interpreter/UrlModuleInterpreter.cs new file mode 100644 index 0000000..e63536b --- /dev/null +++ b/Runtime/BuiltIns/Modules/Interpreter/UrlModuleInterpreter.cs @@ -0,0 +1,221 @@ +using SharpTS.Runtime.Types; +using Interp = SharpTS.Execution.Interpreter; + +namespace SharpTS.Runtime.BuiltIns.Modules.Interpreter; + +/// +/// Interpreter-mode implementation of the Node.js 'url' module. +/// +/// +/// Provides WHATWG URL API and legacy url.parse/url.format functions. +/// +public static class UrlModuleInterpreter +{ + /// + /// Gets all exported values for the url module. + /// + public static Dictionary GetExports() + { + return new Dictionary + { + // WHATWG URL API classes + ["URL"] = new UrlConstructor(), + ["URLSearchParams"] = new UrlSearchParamsConstructor(), + // Legacy functions + ["parse"] = new BuiltInMethod("parse", 1, 3, Parse), + ["format"] = new BuiltInMethod("format", 1, 1, Format), + ["resolve"] = new BuiltInMethod("resolve", 2, 2, Resolve) + }; + } + + /// + /// Legacy url.parse() - parses a URL string into an object. + /// + private static object? Parse(Interp interpreter, object? receiver, List args) + { + if (args.Count == 0 || args[0] == null) + return new SharpTSObject(new Dictionary()); + + var urlString = args[0].ToString()!; + + try + { + var uri = new Uri(urlString, UriKind.Absolute); + return CreateUrlObject(uri); + } + catch + { + // Try parsing as relative URL + try + { + var uri = new Uri("http://localhost/" + urlString.TrimStart('/'), UriKind.Absolute); + return CreateUrlObject(uri, isRelative: true, originalPath: urlString); + } + catch + { + // Return partial object for invalid URLs + return new SharpTSObject(new Dictionary + { + ["href"] = urlString, + ["path"] = urlString + }); + } + } + } + + private static SharpTSObject CreateUrlObject(Uri uri, bool isRelative = false, string? originalPath = null) + { + var result = new Dictionary + { + ["protocol"] = uri.Scheme + ":", + ["slashes"] = true, + ["auth"] = string.IsNullOrEmpty(uri.UserInfo) ? null : uri.UserInfo, + ["host"] = uri.IsDefaultPort ? uri.Host : $"{uri.Host}:{uri.Port}", + ["port"] = uri.IsDefaultPort ? null : uri.Port.ToString(), + ["hostname"] = uri.Host, + ["hash"] = string.IsNullOrEmpty(uri.Fragment) ? null : uri.Fragment, + ["search"] = string.IsNullOrEmpty(uri.Query) ? null : uri.Query, + ["query"] = string.IsNullOrEmpty(uri.Query) ? null : uri.Query.TrimStart('?'), + ["pathname"] = uri.AbsolutePath, + ["path"] = uri.PathAndQuery, + ["href"] = uri.AbsoluteUri + }; + + if (isRelative && originalPath != null) + { + result["protocol"] = null; + result["slashes"] = null; + result["host"] = null; + result["hostname"] = null; + result["path"] = originalPath; + result["pathname"] = originalPath.Split('?')[0]; + result["href"] = originalPath; + } + + return new SharpTSObject(result); + } + + /// + /// Legacy url.format() - formats a URL object into a string. + /// + private static object? Format(Interp interpreter, object? receiver, List args) + { + if (args.Count == 0 || args[0] == null) + return ""; + + if (args[0] is SharpTSURL url) + return url.Href; + + if (args[0] is string s) + return s; + + if (args[0] is SharpTSObject obj) + { + // Build URL from object parts + var protocol = obj.Fields.GetValueOrDefault("protocol")?.ToString() ?? ""; + var hostname = obj.Fields.GetValueOrDefault("hostname")?.ToString() ?? + obj.Fields.GetValueOrDefault("host")?.ToString() ?? ""; + var port = obj.Fields.GetValueOrDefault("port")?.ToString(); + var pathname = obj.Fields.GetValueOrDefault("pathname")?.ToString() ?? "/"; + var search = obj.Fields.GetValueOrDefault("search")?.ToString() ?? ""; + var hash = obj.Fields.GetValueOrDefault("hash")?.ToString() ?? ""; + + var host = !string.IsNullOrEmpty(port) ? $"{hostname}:{port}" : hostname; + + var slashes = obj.Fields.GetValueOrDefault("slashes"); + var slashStr = slashes is true || (protocol.Length > 0 && slashes is not false) ? "//" : ""; + + return $"{protocol}{slashStr}{host}{pathname}{search}{hash}"; + } + + return args[0].ToString() ?? ""; + } + + /// + /// Legacy url.resolve() - resolves a target URL relative to a base URL. + /// + private static object? Resolve(Interp interpreter, object? receiver, List args) + { + if (args.Count < 2) + return args.Count > 0 ? args[0]?.ToString() : ""; + + var from = args[0]?.ToString() ?? ""; + var to = args[1]?.ToString() ?? ""; + + try + { + var baseUri = new Uri(from, UriKind.Absolute); + var resolvedUri = new Uri(baseUri, to); + return resolvedUri.AbsoluteUri; + } + catch + { + // If base isn't absolute, try best effort + return to.StartsWith('/') ? to : $"{from.TrimEnd('/')}/{to}"; + } + } +} + +/// +/// Constructor class for URL (used as `new URL(...)`). +/// +public class UrlConstructor +{ + public SharpTSURL Construct(List args) + { + if (args.Count == 0) + throw new Exception("Failed to construct 'URL': 1 argument required"); + + var urlString = args[0]?.ToString() ?? ""; + + if (args.Count > 1 && args[1] != null) + { + var baseUrl = args[1].ToString()!; + return new SharpTSURL(urlString, baseUrl); + } + + return new SharpTSURL(urlString); + } + + public override string ToString() => "function URL() { [native code] }"; +} + +/// +/// Constructor class for URLSearchParams (used as `new URLSearchParams(...)`). +/// +public class UrlSearchParamsConstructor +{ + public SharpTSURLSearchParams Construct(List args) + { + if (args.Count == 0 || args[0] == null) + return new SharpTSURLSearchParams(); + + if (args[0] is string s) + return new SharpTSURLSearchParams(s.TrimStart('?')); + + // Handle object/dictionary initialization + if (args[0] is SharpTSObject obj) + { + var searchParams = new SharpTSURLSearchParams(); + foreach (var kvp in obj.Fields) + { + searchParams.Append(kvp.Key, kvp.Value?.ToString() ?? ""); + } + return searchParams; + } + + if (args[0] is Dictionary dict) + { + var searchParams = new SharpTSURLSearchParams(); + foreach (var kvp in dict) + { + searchParams.Append(kvp.Key, kvp.Value?.ToString() ?? ""); + } + return searchParams; + } + + return new SharpTSURLSearchParams(args[0].ToString() ?? ""); + } + + public override string ToString() => "function URLSearchParams() { [native code] }"; +} diff --git a/Runtime/BuiltIns/ProcessBuiltIns.cs b/Runtime/BuiltIns/ProcessBuiltIns.cs index 59b816b..d87a170 100644 --- a/Runtime/BuiltIns/ProcessBuiltIns.cs +++ b/Runtime/BuiltIns/ProcessBuiltIns.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Runtime.InteropServices; using SharpTS.Execution; using SharpTS.Runtime.Types; @@ -20,11 +21,21 @@ public static class ProcessBuiltIns // Cache static methods to avoid allocation on every access private static readonly BuiltInMethod _cwd = new("cwd", 0, Cwd); private static readonly BuiltInMethod _exit = new("exit", 0, 1, Exit); + private static readonly BuiltInMethod _hrtime = new("hrtime", 0, 1, Hrtime); + private static readonly BuiltInMethod _uptime = new("uptime", 0, Uptime); + private static readonly BuiltInMethod _memoryUsage = new("memoryUsage", 0, MemoryUsage); // Lazily create env and argv objects private static SharpTSObject? _envObject; private static SharpTSArray? _argvArray; + // Process start time for uptime calculation + private static readonly DateTime _processStartTime = Process.GetCurrentProcess().StartTime.ToUniversalTime(); + + // Stopwatch frequency for hrtime calculations + private static readonly long _startTimestamp = Stopwatch.GetTimestamp(); + private static readonly double _tickFrequency = Stopwatch.Frequency; + /// /// Gets a member of the process object by name. /// @@ -43,6 +54,9 @@ public static class ProcessBuiltIns // Methods "cwd" => _cwd, "exit" => _exit, + "hrtime" => _hrtime, + "uptime" => _uptime, + "memoryUsage" => _memoryUsage, _ => null }; @@ -128,4 +142,57 @@ public static SharpTSArray GetArgv() Environment.Exit(exitCode); return null; // Never reached } + + /// + /// Returns the current high-resolution real time in a [seconds, nanoseconds] tuple. + /// If a previous hrtime result is passed, returns the difference. + /// + private static object? Hrtime(Interpreter i, object? r, List args) + { + long currentTicks = Stopwatch.GetTimestamp() - _startTimestamp; + double totalNanoseconds = currentTicks * 1_000_000_000.0 / _tickFrequency; + + // If a previous time is provided, calculate the difference + if (args.Count > 0 && args[0] is SharpTSArray prev && prev.Elements.Count >= 2) + { + var prevSeconds = Convert.ToDouble(prev.Elements[0]); + var prevNanos = Convert.ToDouble(prev.Elements[1]); + double prevTotalNanos = prevSeconds * 1_000_000_000.0 + prevNanos; + totalNanoseconds -= prevTotalNanos; + } + + double seconds = Math.Floor(totalNanoseconds / 1_000_000_000.0); + double nanos = totalNanoseconds % 1_000_000_000.0; + + // Ensure non-negative values + if (seconds < 0) seconds = 0; + if (nanos < 0) nanos = 0; + + return new SharpTSArray([seconds, nanos]); + } + + /// + /// Returns the number of seconds the process has been running. + /// + private static object? Uptime(Interpreter i, object? r, List args) + { + return (DateTime.UtcNow - _processStartTime).TotalSeconds; + } + + /// + /// Returns an object describing the memory usage of the process. + /// + private static object? MemoryUsage(Interpreter i, object? r, List args) + { + var process = Process.GetCurrentProcess(); + + return new SharpTSObject(new Dictionary + { + ["rss"] = (double)process.WorkingSet64, + ["heapTotal"] = (double)GC.GetTotalMemory(false), + ["heapUsed"] = (double)GC.GetTotalMemory(false), + ["external"] = 0.0, + ["arrayBuffers"] = 0.0 + }); + } } diff --git a/Runtime/Types/SharpTSURL.cs b/Runtime/Types/SharpTSURL.cs new file mode 100644 index 0000000..bec96db --- /dev/null +++ b/Runtime/Types/SharpTSURL.cs @@ -0,0 +1,245 @@ +namespace SharpTS.Runtime.Types; + +/// +/// Runtime representation of the WHATWG URL API. +/// Wraps System.Uri and provides Node.js/browser-compatible URL properties. +/// +public class SharpTSURL +{ + private readonly Uri _uri; + private SharpTSURLSearchParams? _searchParams; + + public SharpTSURL(string url) + { + _uri = new Uri(url, UriKind.Absolute); + } + + public SharpTSURL(string url, string baseUrl) + { + var baseUri = new Uri(baseUrl, UriKind.Absolute); + _uri = new Uri(baseUri, url); + } + + /// + /// Gets the full URL string. + /// + public string Href => _uri.AbsoluteUri; + + /// + /// Gets the protocol (scheme) with trailing colon, e.g. "https:". + /// + public string Protocol => _uri.Scheme + ":"; + + /// + /// Gets the host (hostname + port if non-default). + /// + public string Host => _uri.IsDefaultPort ? _uri.Host : $"{_uri.Host}:{_uri.Port}"; + + /// + /// Gets the hostname without port. + /// + public string Hostname => _uri.Host; + + /// + /// Gets the port as a string, or empty string if default. + /// + public string Port => _uri.IsDefaultPort ? "" : _uri.Port.ToString(); + + /// + /// Gets the pathname (path portion). + /// + public string Pathname => _uri.AbsolutePath; + + /// + /// Gets the search string including leading '?', or empty if none. + /// + public string Search => string.IsNullOrEmpty(_uri.Query) ? "" : _uri.Query; + + /// + /// Gets the URLSearchParams object for this URL. + /// + public SharpTSURLSearchParams SearchParams + { + get + { + _searchParams ??= new SharpTSURLSearchParams(Search.TrimStart('?')); + return _searchParams; + } + } + + /// + /// Gets the hash (fragment) including leading '#', or empty if none. + /// + public string Hash => string.IsNullOrEmpty(_uri.Fragment) ? "" : _uri.Fragment; + + /// + /// Gets the origin (protocol + host). + /// + public string Origin => $"{Protocol}//{Host}"; + + /// + /// Gets the username portion of the URL. + /// + public string Username => Uri.UnescapeDataString(_uri.UserInfo.Split(':')[0]); + + /// + /// Gets the password portion of the URL. + /// + public string Password + { + get + { + var parts = _uri.UserInfo.Split(':'); + return parts.Length > 1 ? Uri.UnescapeDataString(parts[1]) : ""; + } + } + + /// + /// Returns the URL as a string. + /// + public override string ToString() => Href; + + /// + /// Returns the URL as JSON (same as href). + /// + public string ToJSON() => Href; +} + +/// +/// Runtime representation of URLSearchParams API. +/// Provides methods to work with the query string of a URL. +/// +public class SharpTSURLSearchParams +{ + private readonly List<(string Key, string Value)> _params = []; + + public SharpTSURLSearchParams() + { + } + + public SharpTSURLSearchParams(string init) + { + if (string.IsNullOrEmpty(init)) + return; + + foreach (var pair in init.Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + var eqIndex = pair.IndexOf('='); + if (eqIndex >= 0) + { + var key = Uri.UnescapeDataString(pair[..eqIndex].Replace('+', ' ')); + var value = Uri.UnescapeDataString(pair[(eqIndex + 1)..].Replace('+', ' ')); + _params.Add((key, value)); + } + else + { + var key = Uri.UnescapeDataString(pair.Replace('+', ' ')); + _params.Add((key, "")); + } + } + } + + /// + /// Gets the first value associated with a given key. + /// + public string? Get(string name) + { + foreach (var (key, value) in _params) + { + if (key == name) + return value; + } + return null; + } + + /// + /// Gets all values associated with a given key. + /// + public List GetAll(string name) + { + var result = new List(); + foreach (var (key, value) in _params) + { + if (key == name) + result.Add(value); + } + return result; + } + + /// + /// Returns true if a parameter with the specified key exists. + /// + public bool Has(string name) + { + foreach (var (key, _) in _params) + { + if (key == name) + return true; + } + return false; + } + + /// + /// Sets the value associated with a given key. Removes other values. + /// + public void Set(string name, string value) + { + // Remove all existing entries with this key + _params.RemoveAll(p => p.Key == name); + // Add new entry + _params.Add((name, value)); + } + + /// + /// Appends a specified key/value pair. + /// + public void Append(string name, string value) + { + _params.Add((name, value)); + } + + /// + /// Deletes all occurrences of a given key. + /// + public void Delete(string name) + { + _params.RemoveAll(p => p.Key == name); + } + + /// + /// Returns all keys. + /// + public List Keys() + { + var result = new List(); + foreach (var (key, _) in _params) + { + if (!result.Contains(key)) + result.Add(key); + } + return result; + } + + /// + /// Returns all values. + /// + public List Values() + { + return _params.Select(p => p.Value).ToList(); + } + + /// + /// Returns the query string. + /// + public override string ToString() + { + var pairs = _params.Select(p => + $"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}"); + return string.Join("&", pairs); + } + + /// + /// Gets the number of parameters. + /// + public int Size => _params.Count; +} diff --git a/SharpTS.Tests/CompilerTests/BuiltInModules/AssertModuleTests.cs b/SharpTS.Tests/CompilerTests/BuiltInModules/AssertModuleTests.cs new file mode 100644 index 0000000..c8f732c --- /dev/null +++ b/SharpTS.Tests/CompilerTests/BuiltInModules/AssertModuleTests.cs @@ -0,0 +1,347 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.CompilerTests.BuiltInModules; + +/// +/// Tests for the built-in 'assert' module in compiled mode. +/// +public class AssertModuleTests +{ + [Fact] + public void Assert_Ok_PassesForTruthyValues() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { ok } from 'assert'; + ok(true); + ok(1); + ok('hello'); + ok({}); + ok([]); + console.log('all passed'); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("all passed\n", output); + } + + [Fact] + public void Assert_Ok_ThrowsForFalsy() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { ok } from 'assert'; + try { + ok(false); + console.log('should not reach'); + } catch (e) { + console.log('caught'); + } + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("caught\n", output); + } + + [Fact] + public void Assert_StrictEqual_PassesForEqualValues() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { strictEqual } from 'assert'; + strictEqual(1, 1); + strictEqual('hello', 'hello'); + strictEqual(true, true); + console.log('all passed'); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("all passed\n", output); + } + + [Fact] + public void Assert_StrictEqual_ThrowsForUnequalValues() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { strictEqual } from 'assert'; + try { + strictEqual(1, 2); + console.log('should not reach'); + } catch (e) { + console.log('caught'); + } + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("caught\n", output); + } + + [Fact] + public void Assert_StrictEqual_ThrowsForDifferentTypes() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { strictEqual } from 'assert'; + try { + strictEqual(1, '1'); + console.log('should not reach'); + } catch (e) { + console.log('caught type mismatch'); + } + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("caught type mismatch\n", output); + } + + [Fact] + public void Assert_NotStrictEqual_PassesForUnequalValues() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { notStrictEqual } from 'assert'; + notStrictEqual(1, 2); + notStrictEqual('a', 'b'); + notStrictEqual(true, false); + console.log('all passed'); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("all passed\n", output); + } + + [Fact] + public void Assert_NotStrictEqual_ThrowsForEqualValues() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { notStrictEqual } from 'assert'; + try { + notStrictEqual(1, 1); + console.log('should not reach'); + } catch (e) { + console.log('caught'); + } + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("caught\n", output); + } + + [Fact] + public void Assert_DeepStrictEqual_PassesForEqualObjects() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { deepStrictEqual } from 'assert'; + deepStrictEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); + console.log('objects passed'); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("objects passed\n", output); + } + + [Fact] + public void Assert_DeepStrictEqual_PassesForEqualArrays() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { deepStrictEqual } from 'assert'; + deepStrictEqual([1, 2, 3], [1, 2, 3]); + console.log('arrays passed'); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("arrays passed\n", output); + } + + [Fact] + public void Assert_DeepStrictEqual_ThrowsForDifferentObjects() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { deepStrictEqual } from 'assert'; + try { + deepStrictEqual({ a: 1 }, { a: 2 }); + console.log('should not reach'); + } catch (e) { + console.log('caught'); + } + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("caught\n", output); + } + + [Fact] + public void Assert_DeepStrictEqual_ThrowsForDifferentArrays() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { deepStrictEqual } from 'assert'; + try { + deepStrictEqual([1, 2], [1, 3]); + console.log('should not reach'); + } catch (e) { + console.log('caught'); + } + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("caught\n", output); + } + + [Fact] + public void Assert_Equal_PassesForLooselyEqualValues() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { equal } from 'assert'; + equal(1, 1); + equal('1', '1'); + console.log('all passed'); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("all passed\n", output); + } + + [Fact] + public void Assert_NotEqual_PassesForDifferentValues() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { notEqual } from 'assert'; + notEqual(1, 2); + notEqual('a', 'b'); + console.log('all passed'); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("all passed\n", output); + } + + [Fact] + public void Assert_Fail_AlwaysThrows() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { fail } from 'assert'; + try { + fail('custom message'); + console.log('should not reach'); + } catch (e) { + console.log('caught'); + } + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("caught\n", output); + } + + [Fact] + public void Assert_Fail_WithDefaultMessage() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { fail } from 'assert'; + try { + fail(); + console.log('should not reach'); + } catch (e) { + console.log('caught default'); + } + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("caught default\n", output); + } + + [Fact] + public void Assert_NamespaceImport_Works() + { + var files = new Dictionary + { + ["main.ts"] = """ + import * as assert from 'assert'; + assert.ok(true); + assert.strictEqual(1, 1); + console.log('namespace import works'); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("namespace import works\n", output); + } + + [Fact] + public void Assert_CustomMessage_IsUsed() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { ok } from 'assert'; + try { + ok(false, 'custom error message'); + } catch (e) { + // Just check that it throws - message is in the error + console.log('threw with message'); + } + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("threw with message\n", output); + } + + [Fact] + public void Assert_MultipleAssertions_InSequence() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { ok, strictEqual, deepStrictEqual } from 'assert'; + ok(true); + strictEqual(42, 42); + deepStrictEqual({ x: 1 }, { x: 1 }); + console.log('all assertions passed'); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("all assertions passed\n", output); + } +} diff --git a/SharpTS.Tests/CompilerTests/BuiltInModules/ProcessGlobalTests.cs b/SharpTS.Tests/CompilerTests/BuiltInModules/ProcessGlobalTests.cs index 51cf56c..caa1c07 100644 --- a/SharpTS.Tests/CompilerTests/BuiltInModules/ProcessGlobalTests.cs +++ b/SharpTS.Tests/CompilerTests/BuiltInModules/ProcessGlobalTests.cs @@ -136,4 +136,171 @@ public void Process_MultipleProperties_WorkTogether() var output = TestHarness.RunCompiled(source); Assert.Equal("true\ntrue\ntrue\ntrue\n", output); } + + // ============ PROCESS ENHANCEMENT TESTS ============ + + [Fact] + public void Process_Hrtime_ReturnsArray() + { + var source = """ + const hr = process.hrtime(); + console.log(Array.isArray(hr)); + console.log(hr.length === 2); + """; + + var output = TestHarness.RunCompiled(source); + Assert.Equal("true\ntrue\n", output); + } + + [Fact] + public void Process_Hrtime_ReturnsPositiveSeconds() + { + var source = """ + const hr = process.hrtime(); + console.log(hr[0] >= 0); + """; + + var output = TestHarness.RunCompiled(source); + Assert.Equal("true\n", output); + } + + [Fact] + public void Process_Hrtime_ReturnsValidNanoseconds() + { + var source = """ + const hr = process.hrtime(); + // Nanoseconds should be 0-999999999 + console.log(hr[1] >= 0); + console.log(hr[1] < 1000000000); + """; + + var output = TestHarness.RunCompiled(source); + Assert.Equal("true\ntrue\n", output); + } + + [Fact] + public void Process_Hrtime_WithPrevious_ReturnsDiff() + { + var source = """ + const start = process.hrtime(); + // Small busy loop + let sum = 0; + for (let i = 0; i < 10000; i++) { + sum += i; + } + const diff = process.hrtime(start); + console.log(Array.isArray(diff)); + console.log(diff.length === 2); + // diff[0] should be >= 0 (seconds elapsed) + console.log(diff[0] >= 0); + """; + + var output = TestHarness.RunCompiled(source); + Assert.Equal("true\ntrue\ntrue\n", output); + } + + [Fact] + public void Process_Uptime_ReturnsPositiveNumber() + { + var source = """ + const up = process.uptime(); + console.log(typeof up === 'number'); + console.log(up >= 0); + """; + + var output = TestHarness.RunCompiled(source); + Assert.Equal("true\ntrue\n", output); + } + + [Fact] + public void Process_Uptime_IsSmallForNewProcess() + { + var source = """ + const up = process.uptime(); + // For a new process, uptime should be small (less than 60 seconds typically) + console.log(up < 120); + """; + + var output = TestHarness.RunCompiled(source); + Assert.Equal("true\n", output); + } + + [Fact] + public void Process_MemoryUsage_ReturnsObject() + { + var source = """ + const mem = process.memoryUsage(); + console.log(typeof mem === 'object'); + """; + + var output = TestHarness.RunCompiled(source); + Assert.Equal("true\n", output); + } + + [Fact] + public void Process_MemoryUsage_HasRss() + { + var source = """ + const mem = process.memoryUsage(); + console.log(typeof mem.rss === 'number'); + console.log(mem.rss > 0); + """; + + var output = TestHarness.RunCompiled(source); + Assert.Equal("true\ntrue\n", output); + } + + [Fact] + public void Process_MemoryUsage_HasHeapTotal() + { + var source = """ + const mem = process.memoryUsage(); + console.log(typeof mem.heapTotal === 'number'); + console.log(mem.heapTotal > 0); + """; + + var output = TestHarness.RunCompiled(source); + Assert.Equal("true\ntrue\n", output); + } + + [Fact] + public void Process_MemoryUsage_HasHeapUsed() + { + var source = """ + const mem = process.memoryUsage(); + console.log(typeof mem.heapUsed === 'number'); + console.log(mem.heapUsed > 0); + """; + + var output = TestHarness.RunCompiled(source); + Assert.Equal("true\ntrue\n", output); + } + + [Fact] + public void Process_MemoryUsage_HeapUsedLessThanTotal() + { + var source = """ + const mem = process.memoryUsage(); + console.log(mem.heapUsed <= mem.heapTotal); + """; + + var output = TestHarness.RunCompiled(source); + Assert.Equal("true\n", output); + } + + [Fact] + public void Process_AllEnhancements_WorkTogether() + { + var source = """ + const hr = process.hrtime(); + const up = process.uptime(); + const mem = process.memoryUsage(); + console.log(hr.length === 2); + console.log(up >= 0); + console.log(mem.rss > 0); + """; + + var output = TestHarness.RunCompiled(source); + Assert.Equal("true\ntrue\ntrue\n", output); + } } diff --git a/SharpTS.Tests/CompilerTests/BuiltInModules/QuerystringModuleTests.cs b/SharpTS.Tests/CompilerTests/BuiltInModules/QuerystringModuleTests.cs new file mode 100644 index 0000000..e5da869 --- /dev/null +++ b/SharpTS.Tests/CompilerTests/BuiltInModules/QuerystringModuleTests.cs @@ -0,0 +1,229 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.CompilerTests.BuiltInModules; + +/// +/// Tests for the built-in 'querystring' module in compiled mode. +/// +public class QuerystringModuleTests +{ + [Fact] + public void Querystring_Parse_ParsesSimpleString() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { parse } from 'querystring'; + const result = parse('foo=bar&baz=qux'); + console.log(result.foo); + console.log(result.baz); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("bar\nqux\n", output); + } + + [Fact] + public void Querystring_Parse_HandlesUrlEncoding() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { parse } from 'querystring'; + const result = parse('name=John%20Doe&city=New%20York'); + console.log(result.name); + console.log(result.city); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("John Doe\nNew York\n", output); + } + + [Fact] + public void Querystring_Parse_HandlesPlusAsSpace() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { parse } from 'querystring'; + const result = parse('name=John+Doe'); + console.log(result.name); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("John Doe\n", output); + } + + [Fact] + public void Querystring_Parse_HandlesEmptyValue() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { parse } from 'querystring'; + const result = parse('foo=&bar=value'); + console.log(result.foo === ''); + console.log(result.bar); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("true\nvalue\n", output); + } + + [Fact] + public void Querystring_Parse_CustomSeparator() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { parse } from 'querystring'; + const result = parse('foo=bar;baz=qux', ';'); + console.log(result.foo); + console.log(result.baz); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("bar\nqux\n", output); + } + + [Fact] + public void Querystring_Stringify_CreatesQueryString() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { stringify } from 'querystring'; + const str = stringify({ foo: 'bar', baz: 'qux' }); + console.log(str.includes('foo=bar')); + console.log(str.includes('baz=qux')); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("true\ntrue\n", output); + } + + [Fact] + public void Querystring_Stringify_EncodesSpecialChars() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { stringify } from 'querystring'; + const str = stringify({ name: 'hello world' }); + console.log(str); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Contains("hello%20world", output); + } + + [Fact] + public void Querystring_Escape_EncodesString() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { escape } from 'querystring'; + console.log(escape('hello world')); + console.log(escape('a=b&c=d')); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Contains("hello%20world", output); + Assert.Contains("%26", output); + } + + [Fact] + public void Querystring_Unescape_DecodesString() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { unescape } from 'querystring'; + console.log(unescape('hello%20world')); + console.log(unescape('hello+world')); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("hello world\nhello world\n", output); + } + + [Fact] + public void Querystring_DecodeAlias_WorksLikeParse() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { decode } from 'querystring'; + const result = decode('foo=bar'); + console.log(result.foo); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("bar\n", output); + } + + [Fact] + public void Querystring_EncodeAlias_WorksLikeStringify() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { encode } from 'querystring'; + const str = encode({ key: 'value' }); + console.log(str); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("key=value\n", output); + } + + [Fact] + public void Querystring_NamespaceImport_Works() + { + var files = new Dictionary + { + ["main.ts"] = """ + import * as qs from 'querystring'; + const parsed = qs.parse('a=1'); + console.log(parsed.a); + const str = qs.stringify({ b: '2' }); + console.log(str); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("1\nb=2\n", output); + } + + [Fact] + public void Querystring_RoundTrip_PreservesData() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { parse, stringify } from 'querystring'; + const original = { name: 'test', value: '123' }; + const encoded = stringify(original); + const decoded = parse(encoded); + console.log(decoded.name); + console.log(decoded.value); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("test\n123\n", output); + } +} diff --git a/SharpTS.Tests/CompilerTests/BuiltInModules/UrlModuleTests.cs b/SharpTS.Tests/CompilerTests/BuiltInModules/UrlModuleTests.cs new file mode 100644 index 0000000..5df5c6b --- /dev/null +++ b/SharpTS.Tests/CompilerTests/BuiltInModules/UrlModuleTests.cs @@ -0,0 +1,253 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.CompilerTests.BuiltInModules; + +/// +/// Tests for the built-in 'url' module in compiled mode. +/// +public class UrlModuleTests +{ + [Fact] + public void Url_Parse_ParsesFullUrl() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { parse } from 'url'; + const parsed = parse('https://example.com:8080/path?query=value#hash'); + console.log(parsed.protocol); + console.log(parsed.hostname); + console.log(parsed.port); + console.log(parsed.pathname); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Contains("https:", output); + Assert.Contains("example.com", output); + Assert.Contains("8080", output); + Assert.Contains("/path", output); + } + + [Fact] + public void Url_Parse_ParsesQueryString() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { parse } from 'url'; + const parsed = parse('https://example.com?foo=bar'); + console.log(parsed.search); + console.log(parsed.query); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Contains("?foo=bar", output); + Assert.Contains("foo=bar", output); + } + + [Fact] + public void Url_Parse_ParsesHash() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { parse } from 'url'; + const parsed = parse('https://example.com#section'); + console.log(parsed.hash); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Contains("#section", output); + } + + [Fact] + public void Url_Parse_HandlesDefaultPort() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { parse } from 'url'; + const parsed = parse('https://example.com/path'); + console.log(parsed.port === null); + console.log(parsed.hostname); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Contains("true", output); + Assert.Contains("example.com", output); + } + + [Fact] + public void Url_Format_CreatesUrlString() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { format } from 'url'; + const formatted = format({ + protocol: 'https:', + hostname: 'example.com', + pathname: '/path', + search: '?key=value' + }); + console.log(formatted); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Contains("https://example.com/path?key=value", output); + } + + [Fact] + public void Url_Format_HandlesPort() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { format } from 'url'; + const formatted = format({ + protocol: 'http:', + hostname: 'localhost', + port: '3000', + pathname: '/api' + }); + console.log(formatted); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Contains("localhost:3000", output); + } + + [Fact] + public void Url_Resolve_ResolvesRelativeUrl() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { resolve } from 'url'; + const resolved = resolve('https://example.com/base/', '../other/path'); + console.log(resolved); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Contains("example.com/other/path", output); + } + + [Fact] + public void Url_Resolve_ResolvesAbsoluteUrl() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { resolve } from 'url'; + const resolved = resolve('https://example.com/base/', '/absolute'); + console.log(resolved); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Contains("example.com/absolute", output); + } + + [Fact] + public void Url_Resolve_KeepsFullUrl() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { resolve } from 'url'; + const resolved = resolve('https://example.com/', 'https://other.com/path'); + console.log(resolved); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Contains("other.com/path", output); + } + + [Fact] + public void Url_NamespaceImport_Works() + { + var files = new Dictionary + { + ["main.ts"] = """ + import * as url from 'url'; + const parsed = url.parse('https://example.com/path'); + console.log(parsed.hostname); + const formatted = url.format({ protocol: 'http:', hostname: 'test.com', pathname: '/' }); + console.log(formatted.includes('test.com')); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Contains("example.com", output); + Assert.Contains("true", output); + } + + [Fact] + public void Url_ParseFormat_RoundTrip() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { parse, format } from 'url'; + const original = 'https://example.com/path?query=value'; + const parsed = parse(original); + const formatted = format(parsed); + console.log(formatted.includes('example.com')); + console.log(formatted.includes('/path')); + console.log(formatted.includes('query=value')); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Equal("true\ntrue\ntrue\n", output); + } + + [Fact] + public void Url_Parse_HandlesHttpProtocol() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { parse } from 'url'; + const parsed = parse('http://localhost:8080/api/users'); + console.log(parsed.protocol); + console.log(parsed.hostname); + console.log(parsed.port); + console.log(parsed.pathname); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Contains("http:", output); + Assert.Contains("localhost", output); + Assert.Contains("8080", output); + Assert.Contains("/api/users", output); + } + + [Fact] + public void Url_Parse_HandlesFileProtocol() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { parse } from 'url'; + const parsed = parse('file:///home/user/file.txt'); + console.log(parsed.protocol); + console.log(parsed.pathname.includes('file.txt')); + """ + }; + + var output = TestHarness.RunModulesCompiled(files, "main.ts"); + Assert.Contains("file:", output); + Assert.Contains("true", output); + } +} diff --git a/SharpTS.Tests/InterpreterTests/BuiltInModules/InterpreterModuleTests.cs b/SharpTS.Tests/InterpreterTests/BuiltInModules/InterpreterModuleTests.cs index 9dacc7a..1625b37 100644 --- a/SharpTS.Tests/InterpreterTests/BuiltInModules/InterpreterModuleTests.cs +++ b/SharpTS.Tests/InterpreterTests/BuiltInModules/InterpreterModuleTests.cs @@ -473,4 +473,313 @@ public void MixedModuleImports_WorkTogether() var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); Assert.Equal("mixed test\n", output); } + + // ============ QUERYSTRING MODULE TESTS ============ + + [Fact] + public void Querystring_Parse_ParsesSimpleString() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { parse } from 'querystring'; + const result = parse('foo=bar&baz=qux'); + console.log(result.foo); + console.log(result.baz); + """ + }; + + var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); + Assert.Equal("bar\nqux\n", output); + } + + [Fact] + public void Querystring_Parse_HandlesUrlEncoding() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { parse } from 'querystring'; + const result = parse('name=John%20Doe'); + console.log(result.name); + """ + }; + + var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); + Assert.Equal("John Doe\n", output); + } + + [Fact] + public void Querystring_Stringify_CreatesQueryString() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { stringify } from 'querystring'; + const str = stringify({ foo: 'bar', baz: 'qux' }); + console.log(str.includes('foo=bar')); + console.log(str.includes('baz=qux')); + """ + }; + + var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); + Assert.Equal("true\ntrue\n", output); + } + + [Fact] + public void Querystring_Escape_EncodesString() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { escape } from 'querystring'; + console.log(escape('hello world')); + """ + }; + + var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); + Assert.Contains("hello%20world", output); + } + + [Fact] + public void Querystring_Unescape_DecodesString() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { unescape } from 'querystring'; + console.log(unescape('hello%20world')); + console.log(unescape('hello+world')); + """ + }; + + var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); + Assert.Equal("hello world\nhello world\n", output); + } + + // ============ ASSERT MODULE TESTS ============ + + [Fact] + public void Assert_Ok_PassesForTruthyValues() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { ok } from 'assert'; + ok(true); + ok(1); + ok('hello'); + console.log('all passed'); + """ + }; + + var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); + Assert.Equal("all passed\n", output); + } + + [Fact] + public void Assert_Ok_ThrowsForFalsy() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { ok } from 'assert'; + try { + ok(false); + console.log('should not reach'); + } catch (e) { + console.log('caught'); + } + """ + }; + + var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); + Assert.Equal("caught\n", output); + } + + [Fact] + public void Assert_StrictEqual_PassesForEqualValues() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { strictEqual } from 'assert'; + strictEqual(1, 1); + strictEqual('hello', 'hello'); + console.log('all passed'); + """ + }; + + var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); + Assert.Equal("all passed\n", output); + } + + [Fact] + public void Assert_StrictEqual_ThrowsForUnequalValues() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { strictEqual } from 'assert'; + try { + strictEqual(1, 2); + console.log('should not reach'); + } catch (e) { + console.log('caught'); + } + """ + }; + + var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); + Assert.Equal("caught\n", output); + } + + [Fact] + public void Assert_DeepStrictEqual_PassesForEqualObjects() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { deepStrictEqual } from 'assert'; + deepStrictEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); + console.log('objects passed'); + """ + }; + + var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); + Assert.Equal("objects passed\n", output); + } + + [Fact] + public void Assert_Fail_AlwaysThrows() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { fail } from 'assert'; + try { + fail('custom message'); + console.log('should not reach'); + } catch (e) { + console.log('caught'); + } + """ + }; + + var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); + Assert.Equal("caught\n", output); + } + + // ============ URL MODULE TESTS ============ + + [Fact] + public void Url_Parse_ParsesFullUrl() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { parse } from 'url'; + const parsed = parse('https://example.com:8080/path?query=value#hash'); + console.log(parsed.protocol); + console.log(parsed.hostname); + console.log(parsed.port); + console.log(parsed.pathname); + """ + }; + + var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); + Assert.Contains("https:", output); + Assert.Contains("example.com", output); + Assert.Contains("8080", output); + Assert.Contains("/path", output); + } + + [Fact] + public void Url_Format_CreatesUrlString() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { format } from 'url'; + const formatted = format({ + protocol: 'https:', + hostname: 'example.com', + pathname: '/path', + search: '?key=value' + }); + console.log(formatted); + """ + }; + + var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); + Assert.Contains("https://example.com/path?key=value", output); + } + + [Fact] + public void Url_Resolve_ResolvesRelativeUrl() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { resolve } from 'url'; + const resolved = resolve('https://example.com/base/', '../other/path'); + console.log(resolved); + """ + }; + + var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); + Assert.Contains("example.com/other/path", output); + } + + // ============ PROCESS ENHANCEMENT TESTS ============ + + [Fact] + public void Process_Hrtime_ReturnsArray() + { + var files = new Dictionary + { + ["main.ts"] = """ + const hr = process.hrtime(); + console.log(Array.isArray(hr)); + console.log(hr.length === 2); + """ + }; + + var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); + Assert.Equal("true\ntrue\n", output); + } + + [Fact] + public void Process_Uptime_ReturnsPositiveNumber() + { + var files = new Dictionary + { + ["main.ts"] = """ + const up = process.uptime(); + console.log(typeof up === 'number'); + console.log(up >= 0); + """ + }; + + var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); + Assert.Equal("true\ntrue\n", output); + } + + [Fact] + public void Process_MemoryUsage_ReturnsObject() + { + var files = new Dictionary + { + ["main.ts"] = """ + const mem = process.memoryUsage(); + console.log(typeof mem === 'object'); + console.log(mem.rss > 0); + console.log(mem.heapTotal > 0); + console.log(mem.heapUsed > 0); + """ + }; + + var output = TestHarness.RunModulesInterpreted(files, "./main.ts"); + Assert.Equal("true\ntrue\ntrue\ntrue\n", output); + } } diff --git a/TypeSystem/BuiltInModuleTypes.cs b/TypeSystem/BuiltInModuleTypes.cs index 42e4b58..3ed08fa 100644 --- a/TypeSystem/BuiltInModuleTypes.cs +++ b/TypeSystem/BuiltInModuleTypes.cs @@ -206,6 +206,160 @@ [new TypeInfo.String(), numberType], }; } + /// + /// Gets the exported types for the querystring module. + /// + public static Dictionary GetQuerystringModuleTypes() + { + var stringType = new TypeInfo.String(); + var anyType = new TypeInfo.Any(); + + return new Dictionary + { + // parse(str, sep?, eq?, options?) -> object + ["parse"] = new TypeInfo.Function( + [stringType, stringType, stringType, anyType], + anyType, + RequiredParams: 1 + ), + // stringify(obj, sep?, eq?, options?) -> string + ["stringify"] = new TypeInfo.Function( + [anyType, stringType, stringType, anyType], + stringType, + RequiredParams: 1 + ), + // escape(str) -> string + ["escape"] = new TypeInfo.Function([stringType], stringType), + // unescape(str) -> string + ["unescape"] = new TypeInfo.Function([stringType], stringType), + // decode is alias for parse + ["decode"] = new TypeInfo.Function( + [stringType, stringType, stringType, anyType], + anyType, + RequiredParams: 1 + ), + // encode is alias for stringify + ["encode"] = new TypeInfo.Function( + [anyType, stringType, stringType, anyType], + stringType, + RequiredParams: 1 + ) + }; + } + + /// + /// Gets the exported types for the assert module. + /// + public static Dictionary GetAssertModuleTypes() + { + var anyType = new TypeInfo.Any(); + var stringType = new TypeInfo.String(); + var voidType = new TypeInfo.Void(); + + return new Dictionary + { + // ok(value, message?) -> void + ["ok"] = new TypeInfo.Function( + [anyType, stringType], + voidType, + RequiredParams: 1 + ), + // strictEqual(actual, expected, message?) -> void + ["strictEqual"] = new TypeInfo.Function( + [anyType, anyType, stringType], + voidType, + RequiredParams: 2 + ), + // notStrictEqual(actual, expected, message?) -> void + ["notStrictEqual"] = new TypeInfo.Function( + [anyType, anyType, stringType], + voidType, + RequiredParams: 2 + ), + // deepStrictEqual(actual, expected, message?) -> void + ["deepStrictEqual"] = new TypeInfo.Function( + [anyType, anyType, stringType], + voidType, + RequiredParams: 2 + ), + // notDeepStrictEqual(actual, expected, message?) -> void + ["notDeepStrictEqual"] = new TypeInfo.Function( + [anyType, anyType, stringType], + voidType, + RequiredParams: 2 + ), + // throws(fn, message?) -> void + ["throws"] = new TypeInfo.Function( + [anyType, stringType], + voidType, + RequiredParams: 1 + ), + // doesNotThrow(fn, message?) -> void + ["doesNotThrow"] = new TypeInfo.Function( + [anyType, stringType], + voidType, + RequiredParams: 1 + ), + // fail(message?) -> void + ["fail"] = new TypeInfo.Function( + [stringType], + voidType, + RequiredParams: 0 + ), + // equal(actual, expected, message?) -> void (loose equality) + ["equal"] = new TypeInfo.Function( + [anyType, anyType, stringType], + voidType, + RequiredParams: 2 + ), + // notEqual(actual, expected, message?) -> void (loose equality) + ["notEqual"] = new TypeInfo.Function( + [anyType, anyType, stringType], + voidType, + RequiredParams: 2 + ) + }; + } + + /// + /// Gets the exported types for the url module. + /// + public static Dictionary GetUrlModuleTypes() + { + var stringType = new TypeInfo.String(); + var anyType = new TypeInfo.Any(); + + // URL class type (simplified - represents the URL constructor/class) + var urlClassType = new TypeInfo.Any(); // Full class typing would require more infrastructure + + // URLSearchParams class type + var urlSearchParamsType = new TypeInfo.Any(); + + return new Dictionary + { + // URL class constructor + ["URL"] = urlClassType, + // URLSearchParams class constructor + ["URLSearchParams"] = urlSearchParamsType, + // parse function (legacy) + ["parse"] = new TypeInfo.Function( + [stringType, stringType, anyType], + anyType, + RequiredParams: 1 + ), + // format function (legacy) + ["format"] = new TypeInfo.Function( + [anyType], + stringType + ), + // resolve function (legacy) + ["resolve"] = new TypeInfo.Function( + [stringType, stringType], + stringType + ) + }; + } + /// /// Gets the exported types for a built-in module by name. /// @@ -218,6 +372,9 @@ [new TypeInfo.String(), numberType], "path" => GetPathModuleTypes(), "os" => GetOsModuleTypes(), "fs" => GetFsModuleTypes(), + "querystring" => GetQuerystringModuleTypes(), + "assert" => GetAssertModuleTypes(), + "url" => GetUrlModuleTypes(), _ => null }; } From f4c9a6fd9eede4f42eb6308822e082ba0730dfbb Mon Sep 17 00:00:00 2001 From: nickna Date: Wed, 14 Jan 2026 17:45:51 -0800 Subject: [PATCH 2/2] Enhance EmitExpression calls in Assert, Querystring, and Url module emitters for improved argument handling --- .../Emitters/Modules/AssertModuleEmitter.cs | 20 +++++++++++++++++++ .../Modules/QuerystringModuleEmitter.cs | 1 + .../Emitters/Modules/UrlModuleEmitter.cs | 4 ++++ 3 files changed, 25 insertions(+) diff --git a/Compilation/Emitters/Modules/AssertModuleEmitter.cs b/Compilation/Emitters/Modules/AssertModuleEmitter.cs index 65bcec7..8e85a03 100644 --- a/Compilation/Emitters/Modules/AssertModuleEmitter.cs +++ b/Compilation/Emitters/Modules/AssertModuleEmitter.cs @@ -50,6 +50,7 @@ private static bool EmitOk(IEmitterContext emitter, List arguments) // Call runtime helper: AssertOk(object? value, object? message) if (arguments.Count > 0) { + emitter.EmitExpression(arguments[0]); emitter.EmitBoxIfNeeded(arguments[0]); } else @@ -59,6 +60,7 @@ private static bool EmitOk(IEmitterContext emitter, List arguments) if (arguments.Count > 1) { + emitter.EmitExpression(arguments[1]); emitter.EmitBoxIfNeeded(arguments[1]); } else @@ -67,6 +69,7 @@ private static bool EmitOk(IEmitterContext emitter, List arguments) } il.Emit(OpCodes.Call, ctx.Runtime!.AssertOk); + il.Emit(OpCodes.Ldnull); // Push null for expression statement Pop return true; } @@ -78,6 +81,7 @@ private static bool EmitStrictEqual(IEmitterContext emitter, List argument // Call runtime helper: AssertStrictEqual(object? actual, object? expected, object? message) EmitThreeArgs(emitter, arguments); il.Emit(OpCodes.Call, ctx.Runtime!.AssertStrictEqual); + il.Emit(OpCodes.Ldnull); // Push null for expression statement Pop return true; } @@ -88,6 +92,7 @@ private static bool EmitNotStrictEqual(IEmitterContext emitter, List argum EmitThreeArgs(emitter, arguments); il.Emit(OpCodes.Call, ctx.Runtime!.AssertNotStrictEqual); + il.Emit(OpCodes.Ldnull); // Push null for expression statement Pop return true; } @@ -98,6 +103,7 @@ private static bool EmitDeepStrictEqual(IEmitterContext emitter, List argu EmitThreeArgs(emitter, arguments); il.Emit(OpCodes.Call, ctx.Runtime!.AssertDeepStrictEqual); + il.Emit(OpCodes.Ldnull); // Push null for expression statement Pop return true; } @@ -108,6 +114,7 @@ private static bool EmitNotDeepStrictEqual(IEmitterContext emitter, List a EmitThreeArgs(emitter, arguments); il.Emit(OpCodes.Call, ctx.Runtime!.AssertNotDeepStrictEqual); + il.Emit(OpCodes.Ldnull); // Push null for expression statement Pop return true; } @@ -119,6 +126,7 @@ private static bool EmitThrows(IEmitterContext emitter, List arguments) // Call runtime helper: AssertThrows(object? fn, object? message) if (arguments.Count > 0) { + emitter.EmitExpression(arguments[0]); emitter.EmitBoxIfNeeded(arguments[0]); } else @@ -128,6 +136,7 @@ private static bool EmitThrows(IEmitterContext emitter, List arguments) if (arguments.Count > 1) { + emitter.EmitExpression(arguments[1]); emitter.EmitBoxIfNeeded(arguments[1]); } else @@ -136,6 +145,7 @@ private static bool EmitThrows(IEmitterContext emitter, List arguments) } il.Emit(OpCodes.Call, ctx.Runtime!.AssertThrows); + il.Emit(OpCodes.Ldnull); // Push null for expression statement Pop return true; } @@ -146,6 +156,7 @@ private static bool EmitDoesNotThrow(IEmitterContext emitter, List argumen if (arguments.Count > 0) { + emitter.EmitExpression(arguments[0]); emitter.EmitBoxIfNeeded(arguments[0]); } else @@ -155,6 +166,7 @@ private static bool EmitDoesNotThrow(IEmitterContext emitter, List argumen if (arguments.Count > 1) { + emitter.EmitExpression(arguments[1]); emitter.EmitBoxIfNeeded(arguments[1]); } else @@ -163,6 +175,7 @@ private static bool EmitDoesNotThrow(IEmitterContext emitter, List argumen } il.Emit(OpCodes.Call, ctx.Runtime!.AssertDoesNotThrow); + il.Emit(OpCodes.Ldnull); // Push null for expression statement Pop return true; } @@ -174,6 +187,7 @@ private static bool EmitFail(IEmitterContext emitter, List arguments) // Call runtime helper: AssertFail(object? message) if (arguments.Count > 0) { + emitter.EmitExpression(arguments[0]); emitter.EmitBoxIfNeeded(arguments[0]); } else @@ -182,6 +196,7 @@ private static bool EmitFail(IEmitterContext emitter, List arguments) } il.Emit(OpCodes.Call, ctx.Runtime!.AssertFail); + il.Emit(OpCodes.Ldnull); // Push null for expression statement Pop return true; } @@ -192,6 +207,7 @@ private static bool EmitEqual(IEmitterContext emitter, List arguments) EmitThreeArgs(emitter, arguments); il.Emit(OpCodes.Call, ctx.Runtime!.AssertEqual); + il.Emit(OpCodes.Ldnull); // Push null for expression statement Pop return true; } @@ -202,6 +218,7 @@ private static bool EmitNotEqual(IEmitterContext emitter, List arguments) EmitThreeArgs(emitter, arguments); il.Emit(OpCodes.Call, ctx.Runtime!.AssertNotEqual); + il.Emit(OpCodes.Ldnull); // Push null for expression statement Pop return true; } @@ -211,6 +228,7 @@ private static void EmitThreeArgs(IEmitterContext emitter, List arguments) if (arguments.Count > 0) { + emitter.EmitExpression(arguments[0]); emitter.EmitBoxIfNeeded(arguments[0]); } else @@ -220,6 +238,7 @@ private static void EmitThreeArgs(IEmitterContext emitter, List arguments) if (arguments.Count > 1) { + emitter.EmitExpression(arguments[1]); emitter.EmitBoxIfNeeded(arguments[1]); } else @@ -229,6 +248,7 @@ private static void EmitThreeArgs(IEmitterContext emitter, List arguments) if (arguments.Count > 2) { + emitter.EmitExpression(arguments[2]); emitter.EmitBoxIfNeeded(arguments[2]); } else diff --git a/Compilation/Emitters/Modules/QuerystringModuleEmitter.cs b/Compilation/Emitters/Modules/QuerystringModuleEmitter.cs index f3cc817..be8c652 100644 --- a/Compilation/Emitters/Modules/QuerystringModuleEmitter.cs +++ b/Compilation/Emitters/Modules/QuerystringModuleEmitter.cs @@ -164,6 +164,7 @@ private static void EmitToString(IEmitterContext emitter, Expr expr) var ctx = emitter.Context; var il = ctx.IL; + emitter.EmitExpression(expr); emitter.EmitBoxIfNeeded(expr); il.Emit(OpCodes.Callvirt, ctx.Types.GetMethod(ctx.Types.Object, "ToString")); } diff --git a/Compilation/Emitters/Modules/UrlModuleEmitter.cs b/Compilation/Emitters/Modules/UrlModuleEmitter.cs index c45d16f..2d30b13 100644 --- a/Compilation/Emitters/Modules/UrlModuleEmitter.cs +++ b/Compilation/Emitters/Modules/UrlModuleEmitter.cs @@ -43,6 +43,7 @@ private static bool EmitParse(IEmitterContext emitter, List arguments) // Call runtime helper: UrlParse(object? url) if (arguments.Count > 0) { + emitter.EmitExpression(arguments[0]); emitter.EmitBoxIfNeeded(arguments[0]); } else @@ -61,6 +62,7 @@ private static bool EmitFormat(IEmitterContext emitter, List arguments) if (arguments.Count > 0) { + emitter.EmitExpression(arguments[0]); emitter.EmitBoxIfNeeded(arguments[0]); } else @@ -80,6 +82,7 @@ private static bool EmitResolve(IEmitterContext emitter, List arguments) // Call runtime helper: UrlResolve(object? from, object? to) if (arguments.Count > 0) { + emitter.EmitExpression(arguments[0]); emitter.EmitBoxIfNeeded(arguments[0]); } else @@ -89,6 +92,7 @@ private static bool EmitResolve(IEmitterContext emitter, List arguments) if (arguments.Count > 1) { + emitter.EmitExpression(arguments[1]); emitter.EmitBoxIfNeeded(arguments[1]); } else