Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Compilation/EmittedRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1537,6 +1537,10 @@ public class EmittedRuntime
public MethodBuilder FsWriteSyncString { get; set; } = null!;
public MethodBuilder FsFstatSync { get; set; } = null!;
public MethodBuilder FsFtruncateSync { get; set; } = null!;
// Long-tail fd primitives (#976): fsync, fd→path, and statfs.
public MethodBuilder FsFsyncSync { get; set; } = null!;
public MethodBuilder FsFdPath { get; set; } = null!;
public MethodBuilder FsStatfsRaw { get; set; } = null!;
public FieldBuilder FsFileDescriptorTable { get; set; } = null!;

// File descriptor low-level helpers (reflection-based for standalone DLLs)
Expand Down
43 changes: 43 additions & 0 deletions Compilation/Emitters/Modules/FsModuleEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public sealed class FsModuleEmitter : IBuiltInModuleEmitter
"symlinkSync", "readlinkSync", "realpathSync", "utimesSync",
// File descriptor APIs
"openSync", "closeSync", "readSync", "writeSync", "fstatSync", "ftruncateSync",
// Long-tail fd primitives (#976)
"fsyncSync", "fdPath", "statfsRaw",
// Directory utilities
"mkdtempSync", "opendirSync",
// Hard links
Expand Down Expand Up @@ -74,6 +76,10 @@ public bool TryEmitMethodCall(IEmitterContext emitter, string methodName, List<E
"writeSync" => EmitWriteSync(emitter, arguments),
"fstatSync" => EmitFstatSync(emitter, arguments),
"ftruncateSync" => EmitFtruncateSync(emitter, arguments),
// Long-tail fd primitives (#976)
"fsyncSync" => EmitFsyncSync(emitter, arguments),
"fdPath" => EmitFdPath(emitter, arguments),
"statfsRaw" => EmitStatfsRaw(emitter, arguments),
// Directory utilities
"mkdtempSync" => EmitMkdtempSync(emitter, arguments),
"opendirSync" => EmitOpendirSync(emitter, arguments),
Expand Down Expand Up @@ -925,6 +931,43 @@ private static bool EmitFtruncateSync(IEmitterContext emitter, List<Expr> argume
return true;
}

// Long-tail fd primitives (#976): fsyncSync (flush), fdPath (fd → path),
// statfsRaw (filesystem stats). Each emits its single arg and calls the helper.

private static bool EmitFsyncSync(IEmitterContext emitter, List<Expr> arguments)
{
var ctx = emitter.Context;
var il = ctx.IL;
if (arguments.Count == 0) { il.Emit(OpCodes.Ldnull); return true; }
emitter.EmitExpression(arguments[0]);
emitter.EmitBoxIfNeeded(arguments[0]);
il.Emit(OpCodes.Call, ctx.Runtime!.FsFsyncSync);
il.Emit(OpCodes.Ldnull); // undefined return
return true;
}

private static bool EmitFdPath(IEmitterContext emitter, List<Expr> arguments)
{
var ctx = emitter.Context;
var il = ctx.IL;
if (arguments.Count == 0) { il.Emit(OpCodes.Ldnull); return true; }
emitter.EmitExpression(arguments[0]);
emitter.EmitBoxIfNeeded(arguments[0]);
il.Emit(OpCodes.Call, ctx.Runtime!.FsFdPath);
return true;
}

private static bool EmitStatfsRaw(IEmitterContext emitter, List<Expr> arguments)
{
var ctx = emitter.Context;
var il = ctx.IL;
if (arguments.Count == 0) { il.Emit(OpCodes.Ldnull); return true; }
emitter.EmitExpression(arguments[0]);
emitter.EmitBoxIfNeeded(arguments[0]);
il.Emit(OpCodes.Call, ctx.Runtime!.FsStatfsRaw);
return true;
}

#endregion

#region Directory Utilities
Expand Down
3 changes: 3 additions & 0 deletions Compilation/RuntimeEmitter.FsHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ private void EmitFsModuleMethods(TypeBuilder typeBuilder, EmittedRuntime runtime
EmitFsFstatSync(typeBuilder, runtime);
EmitFsFtruncateSync(typeBuilder, runtime);

// Long-tail fd primitives (#976): fsync, fd→path, statfs
EmitFsLongTail(typeBuilder, runtime);

// Directory utilities
EmitFsMkdtempSync(typeBuilder, runtime);
EmitFsOpendirSync(typeBuilder, runtime);
Expand Down
198 changes: 198 additions & 0 deletions Compilation/RuntimeEmitter.FsLongTail.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
using System.IO;
using System.Reflection.Emit;

namespace SharpTS.Compilation;

public partial class RuntimeEmitter
{
/// <summary>
/// Emits the long-tail fd primitives (#976): FsFsyncSync (flush an fd),
/// FsFdPath (resolve an fd to its path so the TS facade can route fchmod/
/// fchown/futimes through the path ops), and FsStatfsRaw (filesystem stats
/// from DriveInfo). Mirrors FsModuleInterpreter so both modes agree. BCL-only
/// (standalone): FileStream/DriveInfo/Path/Math are all in the BCL.
/// </summary>
private void EmitFsLongTail(TypeBuilder typeBuilder, EmittedRuntime runtime)
{
EmitFsFsyncSync(typeBuilder, runtime);
EmitFsFdPath(typeBuilder, runtime);
EmitFsStatfsRaw(typeBuilder, runtime);
}

/// <summary>void FsFsyncSync(object fd) — flush the fd's buffered writes to disk.</summary>
private void EmitFsFsyncSync(TypeBuilder typeBuilder, EmittedRuntime runtime)
{
var method = typeBuilder.DefineMethod("FsFsyncSync",
System.Reflection.MethodAttributes.Public | System.Reflection.MethodAttributes.Static,
_types.Void, [_types.Object]);
runtime.FsFsyncSync = method;

var il = method.GetILGenerator();

il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, runtime.ToNumber);
il.Emit(OpCodes.Conv_I4);
var fdLocal = il.DeclareLocal(_types.Int32);
il.Emit(OpCodes.Stloc, fdLocal);

il.Emit(OpCodes.Ldnull);
var pathLocal = il.DeclareLocal(_types.String);
il.Emit(OpCodes.Stloc, pathLocal);

EmitWithFsErrorHandling(il, runtime, pathLocal, "fsync", afterTry =>
{
var flushMethod = typeof(FileStream).GetMethod("Flush", [typeof(bool)])!;

il.Emit(OpCodes.Ldsfld, runtime.FileDescriptorTableInstance);
il.Emit(OpCodes.Ldloc, fdLocal);
il.Emit(OpCodes.Callvirt, runtime.FileDescriptorTableGet);
il.Emit(OpCodes.Ldc_I4_1); // flushToDisk = true
il.Emit(OpCodes.Callvirt, flushMethod);
il.Emit(OpCodes.Leave, afterTry);
});
il.Emit(OpCodes.Ret);
}

/// <summary>object FsFdPath(object fd) — the open fd's file path (FileStream.Name).</summary>
private void EmitFsFdPath(TypeBuilder typeBuilder, EmittedRuntime runtime)
{
var method = typeBuilder.DefineMethod("FsFdPath",
System.Reflection.MethodAttributes.Public | System.Reflection.MethodAttributes.Static,
_types.Object, [_types.Object]);
runtime.FsFdPath = method;

var il = method.GetILGenerator();
var resultLocal = il.DeclareLocal(_types.Object);

il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, runtime.ToNumber);
il.Emit(OpCodes.Conv_I4);
var fdLocal = il.DeclareLocal(_types.Int32);
il.Emit(OpCodes.Stloc, fdLocal);

il.Emit(OpCodes.Ldnull);
var pathLocal = il.DeclareLocal(_types.String);
il.Emit(OpCodes.Stloc, pathLocal);

EmitWithFsErrorHandling(il, runtime, pathLocal, "fstat", afterTry =>
{
var nameGetter = typeof(FileStream).GetProperty("Name")!.GetMethod!;

il.Emit(OpCodes.Ldsfld, runtime.FileDescriptorTableInstance);
il.Emit(OpCodes.Ldloc, fdLocal);
il.Emit(OpCodes.Callvirt, runtime.FileDescriptorTableGet);
il.Emit(OpCodes.Callvirt, nameGetter); // string is already an object
il.Emit(OpCodes.Stloc, resultLocal);
il.Emit(OpCodes.Leave, afterTry);
});
il.Emit(OpCodes.Ldloc, resultLocal);
il.Emit(OpCodes.Ret);
}

/// <summary>
/// object FsStatfsRaw(object path) — flat statfs record {type,bsize,blocks,
/// bfree,bavail,files,ffree}. Mirrors FsModuleInterpreter.BuildStatfsRecord:
/// bsize fixed at 4096; block counts from DriveInfo; inode counts reported 0;
/// any DriveInfo failure (virtual/unknown filesystem) yields zeros, never throws.
/// </summary>
private void EmitFsStatfsRaw(TypeBuilder typeBuilder, EmittedRuntime runtime)
{
var method = typeBuilder.DefineMethod("FsStatfsRaw",
System.Reflection.MethodAttributes.Public | System.Reflection.MethodAttributes.Static,
_types.Object, [_types.Object]);
runtime.FsStatfsRaw = method;

var il = method.GetILGenerator();

// path = Stringify(arg0)
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, runtime.Stringify);
var pathLocal = il.DeclareLocal(_types.String);
il.Emit(OpCodes.Stloc, pathLocal);

// blocks / bfree / bavail default to 0.0
var blocksL = il.DeclareLocal(_types.Double);
var bfreeL = il.DeclareLocal(_types.Double);
var bavailL = il.DeclareLocal(_types.Double);

var getFullPath = typeof(Path).GetMethod("GetFullPath", [typeof(string)])!;
var getPathRoot = typeof(Path).GetMethod("GetPathRoot", [typeof(string)])!;
var isNullOrEmpty = typeof(string).GetMethod("IsNullOrEmpty", [typeof(string)])!;
var driveCtor = typeof(DriveInfo).GetConstructor([typeof(string)])!;
var totalSizeGet = typeof(DriveInfo).GetProperty("TotalSize")!.GetMethod!;
var totalFreeGet = typeof(DriveInfo).GetProperty("TotalFreeSpace")!.GetMethod!;
var availFreeGet = typeof(DriveInfo).GetProperty("AvailableFreeSpace")!.GetMethod!;
var floor = typeof(Math).GetMethod("Floor", [typeof(double)])!;

var driveLocal = il.DeclareLocal(typeof(DriveInfo));

// try { root = GetPathRoot(GetFullPath(path)); if (!IsNullOrEmpty(root)) { drive=...; blocks/bfree/bavail } } catch { /* zeros */ }
il.BeginExceptionBlock();
{
il.Emit(OpCodes.Ldloc, pathLocal);
il.Emit(OpCodes.Call, getFullPath);
il.Emit(OpCodes.Call, getPathRoot);
var rootLocal = il.DeclareLocal(_types.String);
il.Emit(OpCodes.Stloc, rootLocal);

var skipDrive = il.DefineLabel();
il.Emit(OpCodes.Ldloc, rootLocal);
il.Emit(OpCodes.Call, isNullOrEmpty);
il.Emit(OpCodes.Brtrue, skipDrive);

il.Emit(OpCodes.Ldloc, rootLocal);
il.Emit(OpCodes.Newobj, driveCtor);
il.Emit(OpCodes.Stloc, driveLocal);

void Blocks(System.Reflection.MethodInfo getter, LocalBuilder dest)
{
il.Emit(OpCodes.Ldloc, driveLocal);
il.Emit(OpCodes.Callvirt, getter);
il.Emit(OpCodes.Conv_R8);
il.Emit(OpCodes.Ldc_R8, 4096.0);
il.Emit(OpCodes.Div);
il.Emit(OpCodes.Call, floor);
il.Emit(OpCodes.Stloc, dest);
}
Blocks(totalSizeGet, blocksL);
Blocks(totalFreeGet, bfreeL);
Blocks(availFreeGet, bavailL);

il.MarkLabel(skipDrive);
// No manual Leave: BeginCatchBlock emits the try's exit leave, and
// EndExceptionBlock emits the catch's — both to the block's end.
}
il.BeginCatchBlock(_types.Exception);
il.Emit(OpCodes.Pop); // swallow → zeros
il.EndExceptionBlock();

// Build the record 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);
var dict = il.DeclareLocal(dictType);
il.Emit(OpCodes.Stloc, dict);

void AddEntry(string key, Action loadDouble)
{
il.Emit(OpCodes.Ldloc, dict);
il.Emit(OpCodes.Ldstr, key);
loadDouble();
il.Emit(OpCodes.Box, _types.Double);
il.Emit(OpCodes.Call, addMethod);
}

AddEntry("type", () => il.Emit(OpCodes.Ldc_R8, 0.0));
AddEntry("bsize", () => il.Emit(OpCodes.Ldc_R8, 4096.0));
AddEntry("blocks", () => il.Emit(OpCodes.Ldloc, blocksL));
AddEntry("bfree", () => il.Emit(OpCodes.Ldloc, bfreeL));
AddEntry("bavail", () => il.Emit(OpCodes.Ldloc, bavailL));
AddEntry("files", () => il.Emit(OpCodes.Ldc_R8, 0.0));
AddEntry("ffree", () => il.Emit(OpCodes.Ldc_R8, 0.0));

il.Emit(OpCodes.Ldloc, dict);
il.Emit(OpCodes.Call, runtime.CreateObject);
il.Emit(OpCodes.Ret);
}
}
64 changes: 64 additions & 0 deletions Runtime/BuiltIns/Modules/Interpreter/FsModuleInterpreter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ private static void WrapFsOperation(string syscall, string? path, Action operati
["writeSync"] = BuiltInMethod.CreateV2("writeSync", 2, 5, WriteSync),
["fstatSync"] = BuiltInMethod.CreateV2("fstatSync", 1, 1, FstatSync),
["ftruncateSync"] = BuiltInMethod.CreateV2("ftruncateSync", 1, 2, FtruncateSync),
// Long-tail fd primitives (#976) — the TS facade derives fsync/fdatasync,
// fchmod/fchown/futimes (via fdPath), and statfs from these.
["fsyncSync"] = BuiltInMethod.CreateV2("fsyncSync", 1, 1, FsyncSync),
["fdPath"] = BuiltInMethod.CreateV2("fdPath", 1, 1, FdPath),
["statfsRaw"] = BuiltInMethod.CreateV2("statfsRaw", 1, 1, StatfsRaw),
// Directory utilities
["mkdtempSync"] = BuiltInMethod.CreateV2("mkdtempSync", 1, 1, MkdtempSync),
["opendirSync"] = BuiltInMethod.CreateV2("opendirSync", 1, 1, OpendirSync),
Expand Down Expand Up @@ -958,6 +963,65 @@ private static RuntimeValue FtruncateSync(Interp interpreter, RuntimeValue recei
return RuntimeValue.Null;
}

// #976 long-tail fd primitives. The TS facade builds fsync/fdatasync and the
// fd-variant metadata ops (fchmod/fchown/futimes) on top of these so both
// execution modes share one Node-shape implementation.

/// <summary>fsync/fdatasync: flush an open fd's buffered writes to disk.</summary>
private static RuntimeValue FsyncSync(Interp interpreter, RuntimeValue receiver, ReadOnlySpan<RuntimeValue> args)
{
var fd = Convert.ToInt32(args[0].ToObject());
WrapFsOperation("fsync", null, () => _fdTable.Get(fd).Flush(true));
return RuntimeValue.Null;
}

/// <summary>Resolves an open fd to its file path — lets the facade route the
/// fd-variant metadata ops (fchmod/fchown/futimes) through the path ops.</summary>
private static RuntimeValue FdPath(Interp interpreter, RuntimeValue receiver, ReadOnlySpan<RuntimeValue> args)
{
var fd = Convert.ToInt32(args[0].ToObject());
return RuntimeValue.FromBoxed(WrapFsOperation<object?>("fstat", null, () => _fdTable.Get(fd).Name));
}

/// <summary>statfs: flat filesystem-stats record (type/bsize/blocks/bfree/
/// bavail/files/ffree) the TS StatFs class shapes. Derived from DriveInfo.</summary>
private static RuntimeValue StatfsRaw(Interp interpreter, RuntimeValue receiver, ReadOnlySpan<RuntimeValue> args)
{
var path = args[0].ToObject()?.ToString() ?? "";
return RuntimeValue.FromBoxed(WrapFsOperation<object?>("statfs", path, () => BuildStatfsRecord(path)));
}

/// <summary>Builds the statfs record from DriveInfo (bsize fixed at 4096; the
/// inode counts are unavailable through the BCL, reported as 0 like libuv on
/// filesystems that don't expose them).</summary>
internal static SharpTSObject BuildStatfsRecord(string path)
{
const double bsize = 4096.0;
double blocks = 0, bfree = 0, bavail = 0;
try
{
var root = Path.GetPathRoot(Path.GetFullPath(path));
if (!string.IsNullOrEmpty(root))
{
var drive = new DriveInfo(root);
blocks = Math.Floor(drive.TotalSize / bsize);
bfree = Math.Floor(drive.TotalFreeSpace / bsize);
bavail = Math.Floor(drive.AvailableFreeSpace / bsize);
}
}
catch { /* unknown/virtual filesystem → zeros */ }
return new SharpTSObject(new Dictionary<string, object?>
{
["type"] = 0.0,
["bsize"] = bsize,
["blocks"] = blocks,
["bfree"] = bfree,
["bavail"] = bavail,
["files"] = 0.0,
["ffree"] = 0.0,
});
}

#endregion

#region Directory Utilities
Expand Down
Loading
Loading