diff --git a/Compilation/RuntimeEmitter.FsHelpers.cs b/Compilation/RuntimeEmitter.FsHelpers.cs index bb7380a..6f8ea2f 100644 --- a/Compilation/RuntimeEmitter.FsHelpers.cs +++ b/Compilation/RuntimeEmitter.FsHelpers.cs @@ -715,4 +715,166 @@ private void EmitFsAccessSync(TypeBuilder typeBuilder, EmittedRuntime runtime) }); il.Emit(OpCodes.Ret); } + + /// + /// Emits wrapper methods for fs module functions to support named imports. + /// Each wrapper takes individual object parameters (compatible with TSFunction.Invoke). + /// + private void EmitFsModuleMethodWrappers(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // existsSync(path) -> bool + EmitFsMethodWrapperSimple(typeBuilder, runtime, "existsSync", 1, + il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.FsExistsSync); + il.Emit(OpCodes.Box, _types.Boolean); + }); + + // readFileSync(path, encoding?) -> object + EmitFsMethodWrapperSimple(typeBuilder, runtime, "readFileSync", 2, + il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.FsReadFileSync); + }); + + // writeFileSync(path, data) -> undefined + EmitFsMethodWrapperSimple(typeBuilder, runtime, "writeFileSync", 2, + il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.FsWriteFileSync); + il.Emit(OpCodes.Ldnull); + }); + + // appendFileSync(path, data) -> undefined + EmitFsMethodWrapperSimple(typeBuilder, runtime, "appendFileSync", 2, + il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.FsAppendFileSync); + il.Emit(OpCodes.Ldnull); + }); + + // unlinkSync(path) -> undefined + EmitFsMethodWrapperSimple(typeBuilder, runtime, "unlinkSync", 1, + il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.FsUnlinkSync); + il.Emit(OpCodes.Ldnull); + }); + + // mkdirSync(path, options?) -> undefined + EmitFsMethodWrapperSimple(typeBuilder, runtime, "mkdirSync", 2, + il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.FsMkdirSync); + il.Emit(OpCodes.Ldnull); + }); + + // rmdirSync(path, options?) -> undefined + EmitFsMethodWrapperSimple(typeBuilder, runtime, "rmdirSync", 2, + il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.FsRmdirSync); + il.Emit(OpCodes.Ldnull); + }); + + // readdirSync(path) -> List + EmitFsMethodWrapperSimple(typeBuilder, runtime, "readdirSync", 1, + il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.FsReaddirSync); + }); + + // statSync(path) -> object + EmitFsMethodWrapperSimple(typeBuilder, runtime, "statSync", 1, + il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.FsStatSync); + }); + + // lstatSync(path) -> object (same as statSync for now) + EmitFsMethodWrapperSimple(typeBuilder, runtime, "lstatSync", 1, + il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.FsStatSync); + }); + + // renameSync(oldPath, newPath) -> undefined + EmitFsMethodWrapperSimple(typeBuilder, runtime, "renameSync", 2, + il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.FsRenameSync); + il.Emit(OpCodes.Ldnull); + }); + + // copyFileSync(src, dest) -> undefined + EmitFsMethodWrapperSimple(typeBuilder, runtime, "copyFileSync", 2, + il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.FsCopyFileSync); + il.Emit(OpCodes.Ldnull); + }); + + // accessSync(path, mode?) -> undefined + EmitFsMethodWrapperSimple(typeBuilder, runtime, "accessSync", 2, + il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.FsAccessSync); + il.Emit(OpCodes.Ldnull); + }); + } + + /// + /// Emits a wrapper method for a single fs module function. + /// Takes individual object parameters (compatible with TSFunction.Invoke). + /// + private void EmitFsMethodWrapperSimple( + TypeBuilder typeBuilder, + EmittedRuntime runtime, + string methodName, + int paramCount, + Action emitCall) + { + // Create parameter types - all object + var paramTypes = new Type[paramCount]; + for (int i = 0; i < paramCount; i++) + paramTypes[i] = _types.Object; + + var method = typeBuilder.DefineMethod( + $"Fs_{methodName}_Wrapper", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + paramTypes + ); + + var il = method.GetILGenerator(); + + // Emit the actual method call + emitCall(il); + + il.Emit(OpCodes.Ret); + + // Register the wrapper for named imports + runtime.RegisterBuiltInModuleMethod("fs", methodName, method); + } } diff --git a/Compilation/RuntimeEmitter.PathModule.cs b/Compilation/RuntimeEmitter.PathModule.cs index 1dff0cb..83d6a4c 100644 --- a/Compilation/RuntimeEmitter.PathModule.cs +++ b/Compilation/RuntimeEmitter.PathModule.cs @@ -10,9 +10,827 @@ public partial class RuntimeEmitter /// private void EmitPathModuleMethods(TypeBuilder typeBuilder, EmittedRuntime runtime) { + // PathFormat is used by the format wrapper and PathModuleEmitter EmitPathFormat(typeBuilder, runtime); } + /// Emits: public static string PathJoin(object[] args) + private void EmitPathJoin(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "PathJoin", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + [_types.ObjectArray] + ); + + var il = method.GetILGenerator(); + + // If no args, return "." + var hasArgs = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Brtrue, hasArgs); + il.Emit(OpCodes.Ldstr, "."); + il.Emit(OpCodes.Ret); + + il.MarkLabel(hasArgs); + + // Start with first arg + var resultLocal = il.DeclareLocal(_types.String); + var indexLocal = il.DeclareLocal(_types.Int32); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Stloc, resultLocal); + + // Loop from index 1 + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Stloc, indexLocal); + + var loopStart = il.DefineLabel(); + var loopEnd = il.DefineLabel(); + + il.MarkLabel(loopStart); + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Bge, loopEnd); + + // result = Path.Combine(result, args[i].ToString()) + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "Combine", _types.String, _types.String)); + il.Emit(OpCodes.Stloc, resultLocal); + + // 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); + + runtime.RegisterBuiltInModuleMethod("path", "join", method); + } + + /// Emits: public static string PathResolve(object[] args) + private void EmitPathResolve(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "PathResolve", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + [_types.ObjectArray] + ); + + var il = method.GetILGenerator(); + + // Start with current working directory + var resultLocal = il.DeclareLocal(_types.String); + var indexLocal = il.DeclareLocal(_types.Int32); + + il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.Directory, "GetCurrentDirectory")); + il.Emit(OpCodes.Stloc, resultLocal); + + // Loop through all args + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, indexLocal); + + var loopStart = il.DefineLabel(); + var loopEnd = il.DefineLabel(); + + il.MarkLabel(loopStart); + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Bge, loopEnd); + + // result = Path.Combine(result, args[i].ToString()) + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, indexLocal); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "Combine", _types.String, _types.String)); + il.Emit(OpCodes.Stloc, resultLocal); + + // 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); + + // Return GetFullPath to resolve . and .. + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetFullPath", _types.String)); + il.Emit(OpCodes.Ret); + + runtime.RegisterBuiltInModuleMethod("path", "resolve", method); + } + + /// Emits: public static string PathBasename(object[] args) + private void EmitPathBasename(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "PathBasename", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + [_types.ObjectArray] + ); + + var il = method.GetILGenerator(); + + // If no args, return "" + var hasArgs = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Brtrue, hasArgs); + il.Emit(OpCodes.Ldstr, ""); + il.Emit(OpCodes.Ret); + + il.MarkLabel(hasArgs); + + // Get filename + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetFileName", _types.String)); + + // If second arg (ext), strip it + var noExt = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Ldc_I4_2); + il.Emit(OpCodes.Blt, noExt); + + // Store filename + var filenameLocal = il.DeclareLocal(_types.String); + il.Emit(OpCodes.Stloc, filenameLocal); + + // Get extension to strip + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.Stringify); + var extLocal = il.DeclareLocal(_types.String); + il.Emit(OpCodes.Stloc, extLocal); + + // Check if filename ends with ext + il.Emit(OpCodes.Ldloc, filenameLocal); + il.Emit(OpCodes.Ldloc, extLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "EndsWith", _types.String)); + + var skipStrip = il.DefineLabel(); + var done = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, skipStrip); + + // Strip: filename.Substring(0, filename.Length - ext.Length) + il.Emit(OpCodes.Ldloc, filenameLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldloc, filenameLocal); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.String, "Length").GetMethod!); + il.Emit(OpCodes.Ldloc, extLocal); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.String, "Length").GetMethod!); + il.Emit(OpCodes.Sub); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "Substring", _types.Int32, _types.Int32)); + il.Emit(OpCodes.Br, done); + + il.MarkLabel(skipStrip); + il.Emit(OpCodes.Ldloc, filenameLocal); + il.Emit(OpCodes.Br, done); + + il.MarkLabel(noExt); + il.MarkLabel(done); + il.Emit(OpCodes.Ret); + + runtime.RegisterBuiltInModuleMethod("path", "basename", method); + } + + /// Emits: public static string PathDirname(object[] args) + private void EmitPathDirname(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "PathDirname", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + [_types.ObjectArray] + ); + + var il = method.GetILGenerator(); + + // If no args, return "." + var hasArgs = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Brtrue, hasArgs); + il.Emit(OpCodes.Ldstr, "."); + il.Emit(OpCodes.Ret); + + il.MarkLabel(hasArgs); + + // Get directory name + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetDirectoryName", _types.String)); + + // If null, return "/" + var notNull = il.DefineLabel(); + var done = il.DefineLabel(); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brtrue, notNull); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldstr, "/"); + il.Emit(OpCodes.Br, done); + il.MarkLabel(notNull); + il.MarkLabel(done); + il.Emit(OpCodes.Ret); + + runtime.RegisterBuiltInModuleMethod("path", "dirname", method); + } + + /// Emits: public static string PathExtname(object[] args) + private void EmitPathExtname(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "PathExtname", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + [_types.ObjectArray] + ); + + var il = method.GetILGenerator(); + + // If no args, return "" + var hasArgs = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Brtrue, hasArgs); + il.Emit(OpCodes.Ldstr, ""); + il.Emit(OpCodes.Ret); + + il.MarkLabel(hasArgs); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetExtension", _types.String)); + il.Emit(OpCodes.Ret); + + runtime.RegisterBuiltInModuleMethod("path", "extname", method); + } + + /// Emits: public static string PathNormalize(object[] args) + private void EmitPathNormalize(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "PathNormalize", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + [_types.ObjectArray] + ); + + var il = method.GetILGenerator(); + + // If no args, return "." + var hasArgs = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Brtrue, hasArgs); + il.Emit(OpCodes.Ldstr, "."); + il.Emit(OpCodes.Ret); + + il.MarkLabel(hasArgs); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetFullPath", _types.String)); + il.Emit(OpCodes.Ret); + + runtime.RegisterBuiltInModuleMethod("path", "normalize", method); + } + + /// Emits: public static object PathIsAbsolute(object[] args) + private void EmitPathIsAbsolute(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "PathIsAbsolute", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + [_types.ObjectArray] + ); + + var il = method.GetILGenerator(); + + // If no args, return false + var hasArgs = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Brtrue, hasArgs); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Box, _types.Boolean); + il.Emit(OpCodes.Ret); + + il.MarkLabel(hasArgs); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "IsPathRooted", _types.String)); + il.Emit(OpCodes.Box, _types.Boolean); + il.Emit(OpCodes.Ret); + + runtime.RegisterBuiltInModuleMethod("path", "isAbsolute", method); + } + + /// Emits: public static string PathRelative(object[] args) + private void EmitPathRelative(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "PathRelative", + MethodAttributes.Public | MethodAttributes.Static, + _types.String, + [_types.ObjectArray] + ); + + var il = method.GetILGenerator(); + + // Need at least 2 args + var hasEnoughArgs = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Ldc_I4_2); + il.Emit(OpCodes.Bge, hasEnoughArgs); + il.Emit(OpCodes.Ldstr, ""); + il.Emit(OpCodes.Ret); + + il.MarkLabel(hasEnoughArgs); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.Stringify); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.Stringify); + + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetRelativePath", _types.String, _types.String)); + il.Emit(OpCodes.Ret); + + runtime.RegisterBuiltInModuleMethod("path", "relative", method); + } + + /// Emits: public static object PathParse(object[] args) + private void EmitPathParse(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "PathParse", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + [_types.ObjectArray] + ); + + var il = method.GetILGenerator(); + + // Get path argument + var pathLocal = il.DeclareLocal(_types.String); + var hasArgs = il.DefineLabel(); + var storePathLabel = il.DefineLabel(); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Brtrue, hasArgs); + + // No args: use empty string + il.Emit(OpCodes.Ldstr, ""); + il.Emit(OpCodes.Br, storePathLabel); + + // Has args: extract first arg + il.MarkLabel(hasArgs); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Call, runtime.Stringify); + + il.MarkLabel(storePathLabel); + il.Emit(OpCodes.Stloc, pathLocal); + + // Create dictionary + var dictType = _types.DictionaryStringObject; + var dictCtor = _types.GetDefaultConstructor(dictType); + var addMethod = _types.GetMethod(dictType, "Add", _types.String, _types.Object); + + il.Emit(OpCodes.Newobj, dictCtor); + + // root + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldstr, "root"); + il.Emit(OpCodes.Ldloc, pathLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetPathRoot", _types.String)); + EmitNullToEmptyString(il); + il.Emit(OpCodes.Call, addMethod); + + // dir + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldstr, "dir"); + il.Emit(OpCodes.Ldloc, pathLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetDirectoryName", _types.String)); + EmitNullToEmptyString(il); + il.Emit(OpCodes.Call, addMethod); + + // base + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldstr, "base"); + il.Emit(OpCodes.Ldloc, pathLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetFileName", _types.String)); + il.Emit(OpCodes.Call, addMethod); + + // name + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldstr, "name"); + il.Emit(OpCodes.Ldloc, pathLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetFileNameWithoutExtension", _types.String)); + il.Emit(OpCodes.Call, addMethod); + + // ext + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldstr, "ext"); + il.Emit(OpCodes.Ldloc, pathLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetExtension", _types.String)); + il.Emit(OpCodes.Call, addMethod); + + // Wrap in SharpTSObject + il.Emit(OpCodes.Call, runtime.CreateObject); + il.Emit(OpCodes.Ret); + + runtime.RegisterBuiltInModuleMethod("path", "parse", method); + } + + /// + /// Emits wrapper methods for path module to support named imports. + /// Each wrapper takes individual object parameters (compatible with TSFunction.Invoke). + /// + private void EmitPathModulePropertyWrappers(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // basename(path, ext?) -> string + EmitPathMethodWrapper(typeBuilder, runtime, "basename", 2, il => + { + // Get filename: Path.GetFileName(Stringify(arg0)) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetFileName", _types.String)); + + // If arg1 (ext) is not null and filename ends with it, strip it + var doneLabel = il.DefineLabel(); + var noExtLabel = il.DefineLabel(); + var filenameLocal = il.DeclareLocal(_types.String); + il.Emit(OpCodes.Stloc, filenameLocal); + + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Brfalse, noExtLabel); + + // Has ext argument + var extLocal = il.DeclareLocal(_types.String); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Stloc, extLocal); + + // Check if filename ends with ext + il.Emit(OpCodes.Ldloc, filenameLocal); + il.Emit(OpCodes.Ldloc, extLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "EndsWith", _types.String)); + il.Emit(OpCodes.Brfalse, noExtLabel); + + // Strip: filename.Substring(0, filename.Length - ext.Length) + il.Emit(OpCodes.Ldloc, filenameLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldloc, filenameLocal); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.String, "Length").GetMethod!); + il.Emit(OpCodes.Ldloc, extLocal); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.String, "Length").GetMethod!); + il.Emit(OpCodes.Sub); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.String, "Substring", _types.Int32, _types.Int32)); + il.Emit(OpCodes.Br, doneLabel); + + il.MarkLabel(noExtLabel); + il.Emit(OpCodes.Ldloc, filenameLocal); + + il.MarkLabel(doneLabel); + }); + + // dirname(path) -> string + EmitPathMethodWrapper(typeBuilder, runtime, "dirname", 1, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetDirectoryName", _types.String)); + + // If null, return "/" + var notNull = il.DefineLabel(); + var done = il.DefineLabel(); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brtrue, notNull); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldstr, "/"); + il.Emit(OpCodes.Br, done); + il.MarkLabel(notNull); + il.MarkLabel(done); + }); + + // extname(path) -> string + EmitPathMethodWrapper(typeBuilder, runtime, "extname", 1, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetExtension", _types.String)); + }); + + // normalize(path) -> string + EmitPathMethodWrapper(typeBuilder, runtime, "normalize", 1, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetFullPath", _types.String)); + }); + + // isAbsolute(path) -> bool + EmitPathMethodWrapper(typeBuilder, runtime, "isAbsolute", 1, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "IsPathRooted", _types.String)); + il.Emit(OpCodes.Box, _types.Boolean); + }); + + // relative(from, to) -> string + EmitPathMethodWrapper(typeBuilder, runtime, "relative", 2, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetRelativePath", _types.String, _types.String)); + }); + + // join(path1, path2, ...) -> string - max 8 args + EmitPathJoinWrapper(typeBuilder, runtime); + + // resolve(path1, path2, ...) -> string - max 8 args + EmitPathResolveWrapper(typeBuilder, runtime); + + // parse(path) -> object + EmitPathParseWrapper(typeBuilder, runtime); + + // format(pathObject) -> string + EmitPathMethodWrapper(typeBuilder, runtime, "format", 1, il => + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.PathFormat); + }); + + // sep - directory separator (property but callable) + EmitPathMethodWrapper(typeBuilder, runtime, "sep", 0, il => + { + il.Emit(OpCodes.Ldsfld, _types.GetField(_types.Path, "DirectorySeparatorChar")); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Char, "ToString", _types.Char)); + }); + + // delimiter - path separator (property but callable) + EmitPathMethodWrapper(typeBuilder, runtime, "delimiter", 0, il => + { + il.Emit(OpCodes.Ldsfld, _types.GetField(_types.Path, "PathSeparator")); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Char, "ToString", _types.Char)); + }); + } + + private void EmitPathMethodWrapper( + 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( + $"Path_{methodName}_Wrapper", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + paramTypes + ); + + var il = method.GetILGenerator(); + emitCall(il); + il.Emit(OpCodes.Ret); + + runtime.RegisterBuiltInModuleMethod("path", methodName, method); + } + + private void EmitPathJoinWrapper(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // join takes variable args, support up to 8 + var method = typeBuilder.DefineMethod( + "Path_join_Wrapper", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + [_types.Object, _types.Object, _types.Object, _types.Object, + _types.Object, _types.Object, _types.Object, _types.Object] + ); + + var il = method.GetILGenerator(); + var resultLocal = il.DeclareLocal(_types.String); + + // Start with empty string + il.Emit(OpCodes.Ldstr, ""); + il.Emit(OpCodes.Stloc, resultLocal); + + // For each arg, if not null, combine + for (int i = 0; i < 8; i++) + { + var skipLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg, i); + il.Emit(OpCodes.Brfalse, skipLabel); + + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldarg, i); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "Combine", _types.String, _types.String)); + il.Emit(OpCodes.Stloc, resultLocal); + + il.MarkLabel(skipLabel); + } + + // If result is empty, return "." + var notEmpty = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.String, "Length").GetMethod!); + il.Emit(OpCodes.Brtrue, notEmpty); + il.Emit(OpCodes.Ldstr, "."); + il.Emit(OpCodes.Ret); + + il.MarkLabel(notEmpty); + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ret); + + runtime.RegisterBuiltInModuleMethod("path", "join", method); + } + + private void EmitPathResolveWrapper(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + // resolve takes variable args, support up to 8 + var method = typeBuilder.DefineMethod( + "Path_resolve_Wrapper", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + [_types.Object, _types.Object, _types.Object, _types.Object, + _types.Object, _types.Object, _types.Object, _types.Object] + ); + + var il = method.GetILGenerator(); + var resultLocal = il.DeclareLocal(_types.String); + + // Start with current directory + il.Emit(OpCodes.Call, _types.GetMethodNoParams(_types.Directory, "GetCurrentDirectory")); + il.Emit(OpCodes.Stloc, resultLocal); + + // For each arg, if not null, combine + for (int i = 0; i < 8; i++) + { + var skipLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldarg, i); + il.Emit(OpCodes.Brfalse, skipLabel); + + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Ldarg, i); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "Combine", _types.String, _types.String)); + il.Emit(OpCodes.Stloc, resultLocal); + + il.MarkLabel(skipLabel); + } + + // Return GetFullPath to resolve . and .. + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetFullPath", _types.String)); + il.Emit(OpCodes.Ret); + + runtime.RegisterBuiltInModuleMethod("path", "resolve", method); + } + + private void EmitPathParseWrapper(TypeBuilder typeBuilder, EmittedRuntime runtime) + { + var method = typeBuilder.DefineMethod( + "Path_parse_Wrapper", + MethodAttributes.Public | MethodAttributes.Static, + _types.Object, + [_types.Object] + ); + + var il = method.GetILGenerator(); + + // Get path string + var pathLocal = il.DeclareLocal(_types.String); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.Stringify); + il.Emit(OpCodes.Stloc, pathLocal); + + // Create dictionary + var dictType = _types.DictionaryStringObject; + var dictCtor = _types.GetDefaultConstructor(dictType); + var addMethod = _types.GetMethod(dictType, "Add", _types.String, _types.Object); + + il.Emit(OpCodes.Newobj, dictCtor); + + // root + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldstr, "root"); + il.Emit(OpCodes.Ldloc, pathLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetPathRoot", _types.String)); + EmitNullToEmptyString(il); + il.Emit(OpCodes.Call, addMethod); + + // dir + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldstr, "dir"); + il.Emit(OpCodes.Ldloc, pathLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetDirectoryName", _types.String)); + EmitNullToEmptyString(il); + il.Emit(OpCodes.Call, addMethod); + + // base + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldstr, "base"); + il.Emit(OpCodes.Ldloc, pathLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetFileName", _types.String)); + il.Emit(OpCodes.Call, addMethod); + + // name + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldstr, "name"); + il.Emit(OpCodes.Ldloc, pathLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetFileNameWithoutExtension", _types.String)); + il.Emit(OpCodes.Call, addMethod); + + // ext + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldstr, "ext"); + il.Emit(OpCodes.Ldloc, pathLocal); + il.Emit(OpCodes.Call, _types.GetMethod(_types.Path, "GetExtension", _types.String)); + il.Emit(OpCodes.Call, addMethod); + + // Wrap in SharpTSObject + il.Emit(OpCodes.Call, runtime.CreateObject); + il.Emit(OpCodes.Ret); + + runtime.RegisterBuiltInModuleMethod("path", "parse", method); + } + + private void EmitNullToEmptyString(ILGenerator il) + { + var notNull = il.DefineLabel(); + var done = il.DefineLabel(); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brtrue, notNull); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldstr, ""); + il.Emit(OpCodes.Br, done); + il.MarkLabel(notNull); + il.MarkLabel(done); + } + /// /// Emits: public static string PathFormat(object pathObject) /// Implements path.format() which reconstructs a path from a parsed path object. diff --git a/Compilation/RuntimeEmitter.cs b/Compilation/RuntimeEmitter.cs index 2c020a9..50cf7fc 100644 --- a/Compilation/RuntimeEmitter.cs +++ b/Compilation/RuntimeEmitter.cs @@ -1300,6 +1300,9 @@ private void EmitRuntimeClass(ModuleBuilder moduleBuilder, EmittedRuntime runtim // Built-in module methods (path, fs, os) EmitPathModuleMethods(typeBuilder, runtime); EmitFsModuleMethods(typeBuilder, runtime); + // Emit wrapper methods for named imports + EmitFsModuleMethodWrappers(typeBuilder, runtime); + EmitPathModulePropertyWrappers(typeBuilder, runtime); // Process global methods (env, argv) EmitProcessMethods(typeBuilder, runtime); // Console extensions (error, warn, clear, time, timeEnd, timeLog) diff --git a/SharpTS.Tests/CompilerTests/NamedBuiltInImportTests.cs b/SharpTS.Tests/CompilerTests/NamedBuiltInImportTests.cs new file mode 100644 index 0000000..35979e6 --- /dev/null +++ b/SharpTS.Tests/CompilerTests/NamedBuiltInImportTests.cs @@ -0,0 +1,215 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.CompilerTests; + +/// +/// Tests for named imports from built-in modules in IL compilation mode. +/// These tests verify that import { func } from 'module' works correctly. +/// +public class NamedBuiltInImportTests +{ + [Fact] + public void Fs_NamedImport_ExistsSync_Works() + { + var testFile = Path.GetTempFileName(); + try + { + var files = new Dictionary + { + ["main.ts"] = $$""" + import { existsSync } from 'fs'; + console.log(existsSync('{{testFile.Replace("\\", "\\\\")}}')); + """ + }; + + var result = TestHarness.RunModulesCompiled(files, "main.ts").TrimEnd(); + Assert.Equal("true", result.ToLower()); + } + finally + { + File.Delete(testFile); + } + } + + [Fact] + public void Fs_NamedImport_WriteFileSync_ReadFileSync_Works() + { + var testFile = Path.Combine(Path.GetTempPath(), $"sharptstest_{Guid.NewGuid()}.txt"); + try + { + var files = new Dictionary + { + ["main.ts"] = $$""" + import { writeFileSync, readFileSync } from 'fs'; + writeFileSync('{{testFile.Replace("\\", "\\\\")}}', 'hello world'); + const content = readFileSync('{{testFile.Replace("\\", "\\\\")}}', 'utf-8'); + console.log(content); + """ + }; + + var result = TestHarness.RunModulesCompiled(files, "main.ts").TrimEnd(); + Assert.Equal("hello world", result); + } + finally + { + if (File.Exists(testFile)) + File.Delete(testFile); + } + } + + [Fact] + public void Fs_NamedImport_MultipleImports_Work() + { + var testFile = Path.Combine(Path.GetTempPath(), $"sharptstest_{Guid.NewGuid()}.txt"); + try + { + var files = new Dictionary + { + ["main.ts"] = $$""" + import { writeFileSync, existsSync, unlinkSync } from 'fs'; + writeFileSync('{{testFile.Replace("\\", "\\\\")}}', 'test'); + console.log(existsSync('{{testFile.Replace("\\", "\\\\")}}')); + unlinkSync('{{testFile.Replace("\\", "\\\\")}}'); + console.log(existsSync('{{testFile.Replace("\\", "\\\\")}}')); + """ + }; + + var result = TestHarness.RunModulesCompiled(files, "main.ts"); + var lines = result.Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(l => l.Trim().ToLower()) + .ToArray(); + Assert.Equal(["true", "false"], lines); + } + finally + { + if (File.Exists(testFile)) + File.Delete(testFile); + } + } + + [Fact] + public void Path_NamedImport_Basename_Works() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { basename } from 'path'; + console.log(basename('/foo/bar/baz.txt')); + """ + }; + + var result = TestHarness.RunModulesCompiled(files, "main.ts").TrimEnd(); + Assert.Equal("baz.txt", result); + } + + [Fact] + public void Path_NamedImport_Dirname_Works() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { dirname } from 'path'; + console.log(dirname('/foo/bar/baz.txt')); + """ + }; + + var result = TestHarness.RunModulesCompiled(files, "main.ts").TrimEnd(); + // On Windows, this might be /foo/bar or \foo\bar + Assert.Contains("foo", result); + Assert.Contains("bar", result); + } + + [Fact] + public void Path_NamedImport_Extname_Works() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { extname } from 'path'; + console.log(extname('test.ts')); + """ + }; + + var result = TestHarness.RunModulesCompiled(files, "main.ts").TrimEnd(); + Assert.Equal(".ts", result); + } + + [Fact] + public void Path_NamedImport_Join_Works() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { join } from 'path'; + const result = join('foo', 'bar', 'baz.txt'); + console.log(result.includes('foo')); + console.log(result.includes('bar')); + console.log(result.includes('baz.txt')); + """ + }; + + var result = TestHarness.RunModulesCompiled(files, "main.ts"); + var lines = result.Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(l => l.Trim().ToLower()) + .ToArray(); + Assert.Equal(["true", "true", "true"], lines); + } + + [Fact] + public void Path_NamedImport_IsAbsolute_Works() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { isAbsolute } from 'path'; + console.log(isAbsolute('foo/bar')); + """ + }; + + var result = TestHarness.RunModulesCompiled(files, "main.ts").TrimEnd().ToLower(); + // Relative path is never absolute + Assert.Equal("false", result); + } + + [Fact] + public void Path_NamedImport_MultipleImports_Work() + { + var files = new Dictionary + { + ["main.ts"] = """ + import { basename, dirname, extname } from 'path'; + console.log(basename('/foo/bar/test.ts')); + console.log(extname('test.ts')); + """ + }; + + var result = TestHarness.RunModulesCompiled(files, "main.ts"); + var lines = result.Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(l => l.Trim()) + .ToArray(); + Assert.Equal("test.ts", lines[0]); + Assert.Equal(".ts", lines[1]); + } + + [Fact] + public void NamespaceImports_StillWork() + { + // Verify namespace imports still work after our changes + var files = new Dictionary + { + ["main.ts"] = """ + import * as path from 'path'; + console.log(path.basename('/foo/bar/baz.txt')); + console.log(path.extname('test.ts')); + """ + }; + + var result = TestHarness.RunModulesCompiled(files, "main.ts"); + var lines = result.Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(l => l.Trim()) + .ToArray(); + Assert.Equal("baz.txt", lines[0]); + Assert.Equal(".ts", lines[1]); + } +}