Quellcode durchsuchen

add asys Haxe APIs, asys test suite

Aurel Bílý vor 5 Jahren
Ursprung
Commit
de2b204496
100 geänderte Dateien mit 5823 neuen und 3 gelöschten Zeilen
  1. 52 0
      std/asys/AsyncFileSystem.hx
  2. 78 0
      std/asys/CurrentProcess.hx
  3. 17 0
      std/asys/DirectoryEntry.hx
  4. 16 0
      std/asys/FileAccessMode.hx
  5. 12 0
      std/asys/FileCopyFlags.hx
  6. 75 0
      std/asys/FileOpenFlags.hx
  7. 90 0
      std/asys/FilePermissions.hx
  8. 36 0
      std/asys/FileStat.hx
  9. 222 0
      std/asys/FileSystem.hx
  10. 82 0
      std/asys/FileWatcher.hx
  11. 14 0
      std/asys/FileWatcherEvent.hx
  12. 71 0
      std/asys/Net.hx
  13. 318 0
      std/asys/Process.hx
  14. 16 0
      std/asys/ProcessExit.hx
  15. 10 0
      std/asys/ProcessIO.hx
  16. 9 0
      std/asys/SymlinkType.hx
  17. 57 0
      std/asys/Timer.hx
  18. 47 0
      std/asys/io/AsyncFile.hx
  19. 88 0
      std/asys/io/File.hx
  20. 42 0
      std/asys/io/FileInput.hx
  21. 46 0
      std/asys/io/FileOutput.hx
  22. 20 0
      std/asys/io/FileReadStream.hx
  23. 13 0
      std/asys/io/FileWriteStream.hx
  24. 21 0
      std/asys/io/IpcMessage.hx
  25. 50 0
      std/asys/io/IpcSerializer.hx
  26. 85 0
      std/asys/io/IpcUnserializer.hx
  27. 21 0
      std/asys/net/Address.hx
  28. 223 0
      std/asys/net/AddressTools.hx
  29. 24 0
      std/asys/net/Dns.hx
  30. 16 0
      std/asys/net/DnsLookupOptions.hx
  31. 9 0
      std/asys/net/IpFamily.hx
  32. 206 0
      std/asys/net/Server.hx
  33. 477 0
      std/asys/net/Socket.hx
  34. 15 0
      std/asys/net/SocketAddress.hx
  35. 41 0
      std/asys/net/SocketOptions.hx
  36. 249 0
      std/asys/net/UdpSocket.hx
  37. 14 0
      std/asys/uv/UVConstants.hx
  38. 12 0
      std/asys/uv/UVDirentType.hx
  39. 81 0
      std/asys/uv/UVErrorType.hx
  40. 7 0
      std/asys/uv/UVFsEventType.hx
  41. 18 0
      std/asys/uv/UVProcessSpawnFlags.hx
  42. 7 0
      std/asys/uv/UVRunMode.hx
  43. 50 0
      std/asys/uv/UVStat.hx
  44. 8 0
      std/eval/Uv.hx
  45. 14 0
      std/eval/_std/asys/AsyncFileSystem.hx
  46. 154 0
      std/eval/_std/asys/FileSystem.hx
  47. 50 0
      std/eval/_std/asys/io/AsyncFile.hx
  48. 45 0
      std/eval/_std/asys/io/File.hx
  49. 45 0
      std/eval/_std/asys/io/FileReadStream.hx
  50. 25 0
      std/eval/_std/asys/net/Dns.hx
  51. 25 0
      std/eval/uv/DirectoryEntry.hx
  52. 11 0
      std/eval/uv/FileWatcher.hx
  53. 25 0
      std/eval/uv/Pipe.hx
  54. 32 0
      std/eval/uv/Process.hx
  55. 19 0
      std/eval/uv/Socket.hx
  56. 71 0
      std/eval/uv/Stat.hx
  57. 17 0
      std/eval/uv/Stream.hx
  58. 8 0
      std/eval/uv/Timer.hx
  59. 28 0
      std/eval/uv/UdpSocket.hx
  60. 112 3
      std/haxe/Error.hx
  61. 5 0
      std/haxe/ErrorType.hx
  62. 10 0
      std/haxe/NoData.hx
  63. 42 0
      std/haxe/async/ArraySignal.hx
  64. 69 0
      std/haxe/async/Callback.hx
  65. 11 0
      std/haxe/async/Defer.hx
  66. 19 0
      std/haxe/async/Listener.hx
  67. 38 0
      std/haxe/async/Signal.hx
  68. 51 0
      std/haxe/async/WrappedSignal.hx
  69. 137 0
      std/haxe/io/Duplex.hx
  70. 51 0
      std/haxe/io/FilePath.hx
  71. 12 0
      std/haxe/io/IDuplex.hx
  72. 63 0
      std/haxe/io/IReadable.hx
  73. 15 0
      std/haxe/io/IWritable.hx
  74. 240 0
      std/haxe/io/Readable.hx
  75. 21 0
      std/haxe/io/StreamTools.hx
  76. 94 0
      std/haxe/io/Transform.hx
  77. 91 0
      std/haxe/io/Writable.hx
  78. 12 0
      tests/asys/Main.hx
  79. 111 0
      tests/asys/Test.hx
  80. 71 0
      tests/asys/TestBase.hx
  81. 13 0
      tests/asys/TestConstants.hx
  82. 2 0
      tests/asys/build-common.hxml
  83. 4 0
      tests/asys/build-eval.hxml
  84. 3 0
      tests/asys/build-hl-c.hxml
  85. 10 0
      tests/asys/build-hl.hxml
  86. 3 0
      tests/asys/build-neko.hxml
  87. 17 0
      tests/asys/impl/FastSource.hx
  88. 22 0
      tests/asys/impl/SlowSource.hx
  89. BIN
      tests/asys/resources-ro/binary.bin
  90. 3 0
      tests/asys/resources-ro/hello.txt
  91. 19 0
      tests/asys/test-helpers/src/IpcEcho.hx
  92. 133 0
      tests/asys/test/TestAsyncFile.hx
  93. 113 0
      tests/asys/test/TestAsyncFileSystem.hx
  94. 56 0
      tests/asys/test/TestDns.hx
  95. 79 0
      tests/asys/test/TestFile.hx
  96. 194 0
      tests/asys/test/TestFileSystem.hx
  97. 73 0
      tests/asys/test/TestIpc.hx
  98. 60 0
      tests/asys/test/TestMisc.hx
  99. 21 0
      tests/asys/test/TestProcess.hx
  100. 89 0
      tests/asys/test/TestStreams.hx

+ 52 - 0
std/asys/AsyncFileSystem.hx

@@ -0,0 +1,52 @@
+package asys;
+
+import haxe.Error;
+import haxe.NoData;
+import haxe.async.Callback;
+import haxe.io.Bytes;
+import haxe.io.FilePath;
+import asys.*;
+
+/**
+	This class provides methods for asynchronous operations on files and
+	directories. For synchronous operations, see `asys.FileSystem`.
+
+	All methods here are asynchronous versions of the functions in
+	`asys.FileSystem`. Please see them for a description of the arguments and
+	use of each method.
+
+	Any synchronous method that returns no value (`Void` return type) has an
+	extra `callback:Callback<NoData>` argument.
+
+	Any synchronous method that returns a value has an extra
+	`callback:Callback<T>` argument, where `T` is the return type of the
+	synchronous method.
+
+	Errors are communicated through the callbacks or in some cases thrown
+	immediately.
+**/
+extern class AsyncFileSystem {
+	static function access(path:FilePath, ?mode:FileAccessMode = FileAccessMode.Ok, callback:Callback<NoData>):Void;
+	static function chmod(path:FilePath, mode:FilePermissions, ?followSymLinks:Bool = true, callback:Callback<NoData>):Void;
+	static function chown(path:FilePath, uid:Int, gid:Int, ?followSymLinks:Bool = true, callback:Callback<NoData>):Void;
+	//static function copyFile(src:FilePath, dest:FilePath, ?flags:FileCopyFlags, callback:Callback<NoData>):Void;
+	static function exists(path:FilePath, callback:Callback<Bool>):Void;
+	static function link(existingPath:FilePath, newPath:FilePath, callback:Callback<NoData>):Void;
+	static function mkdir(path:FilePath, ?recursive:Bool, ?mode:FilePermissions, callback:Callback<NoData>):Void;
+	static function mkdtemp(prefix:FilePath, callback:Callback<FilePath>):Void;
+	static function readdir(path:FilePath, callback:Callback<Array<FilePath>>):Void;
+	static function readdirTypes(path:FilePath, callback:Callback<Array<DirectoryEntry>>):Void;
+	static function readlink(path:FilePath, callback:Callback<FilePath>):Void;
+	static function realpath(path:FilePath, callback:Callback<FilePath>):Void;
+	static function rename(oldPath:FilePath, newPath:FilePath, callback:Callback<NoData>):Void;
+	static function rmdir(path:FilePath, callback:Callback<NoData>):Void;
+	static function stat(path:FilePath, ?followSymLinks:Bool = true, callback:Callback<FileStat>):Void;
+	static function symlink(target:FilePath, path:FilePath, ?type:String, callback:Callback<NoData>):Void;
+	static function truncate(path:FilePath, len:Int, callback:Callback<NoData>):Void;
+	static function unlink(path:FilePath, callback:Callback<NoData>):Void;
+	static function utimes(path:FilePath, atime:Date, mtime:Date, callback:Callback<NoData>):Void;
+	static function appendFile(path:FilePath, data:Bytes, ?flags:FileOpenFlags, ?mode:FilePermissions, callback:Callback<NoData>):Void;
+	static function open(path:FilePath, ?flags:FileOpenFlags, ?mode:FilePermissions, ?binary:Bool = true, callback:Callback<sys.io.File>):Void;
+	static function readFile(path:FilePath, ?flags:FileOpenFlags, callback:Callback<Bytes>):Void;
+	static function writeFile(path:FilePath, data:Bytes, ?flags:FileOpenFlags, ?mode:FilePermissions, callback:Callback<NoData>):Void;
+}

+ 78 - 0
std/asys/CurrentProcess.hx

@@ -0,0 +1,78 @@
+package asys;
+
+import haxe.async.*;
+import asys.net.Socket;
+import asys.io.*;
+
+#if hl
+import hl.Uv;
+#elseif eval
+import eval.Uv;
+#elseif neko
+import neko.Uv;
+#end
+
+/**
+	Methods to control the current process and IPC interaction with the parent
+	process.
+**/
+class CurrentProcess {
+	/**
+		Emitted when a message is received over IPC. `initIpc` must be called first
+		to initialise the IPC channel.
+	**/
+	public static final messageSignal:Signal<IpcMessage> = new ArraySignal();
+
+	static var ipc:Socket;
+	static var ipcOut:IpcSerializer;
+	static var ipcIn:IpcUnserializer;
+
+	/**
+		Initialise the IPC channel on the given file descriptor `fd`. This should
+		only be used when the current process was spawned with `Process.spawn` from
+		another Haxe process. `fd` should correspond to the index of the `Ipc`
+		entry in `options.stdio`.
+	**/
+	public static function initIpc(fd:Int):Void {
+		if (ipc != null)
+			throw "IPC already initialised";
+		ipc = Socket.create();
+		ipcOut = @:privateAccess new IpcSerializer(ipc);
+		ipcIn = @:privateAccess new IpcUnserializer(ipc);
+		ipc.connectFd(true, fd);
+		ipc.errorSignal.on(err -> trace("IPC error", err));
+		ipcIn.messageSignal.on(message -> messageSignal.emit(message));
+	}
+
+	/**
+		Sends a message over IPC. `initIpc` must be called first to initialise the
+		IPC channel.
+	**/
+	public static function send(message:IpcMessage):Void {
+		if (ipc == null)
+			throw "IPC not connected";
+		ipcOut.write(message);
+	}
+
+	public static function initUv():Void {
+		#if !doc_gen
+		Uv.init();
+		#end
+	}
+
+	public static function runUv(?mode:asys.uv.UVRunMode = RunDefault):Bool {
+		#if doc_gen
+		return false;
+		#else
+		return Uv.run(mode);
+		#end
+	}
+
+	public static function stopUv():Void {
+		#if !doc_gen
+		Uv.stop();
+		Uv.run(RunDefault);
+		Uv.close();
+		#end
+	}
+}

+ 17 - 0
std/asys/DirectoryEntry.hx

@@ -0,0 +1,17 @@
+package asys;
+
+import haxe.io.FilePath;
+
+/**
+	An entry returned from `asys.FileSystem.readdirTypes`.
+**/
+interface DirectoryEntry {
+	var name(get, never):FilePath;
+	function isBlockDevice():Bool;
+	function isCharacterDevice():Bool;
+	function isDirectory():Bool;
+	function isFIFO():Bool;
+	function isFile():Bool;
+	function isSocket():Bool;
+	function isSymbolicLink():Bool;
+}

+ 16 - 0
std/asys/FileAccessMode.hx

@@ -0,0 +1,16 @@
+package asys;
+
+/**
+	Wrapper for file access modes. See `asys.FileSystem.access`.
+**/
+enum abstract FileAccessMode(Int) {
+	var Ok = 0;
+	var Execute = 1 << 0;
+	var Write = 1 << 1;
+	var Read = 1 << 2;
+
+	inline function get_raw():Int return this;
+
+	@:op(A | B)
+	inline function join(other:FileAccessMode) return this | other.get_raw();
+}

+ 12 - 0
std/asys/FileCopyFlags.hx

@@ -0,0 +1,12 @@
+package asys;
+
+enum abstract FileCopyFlags(Int) {
+	var FailIfExists = 1 << 0; // fail if destination exists
+	var COWClone = 1 << 1; // copy-on-write reflink if possible
+	var COWCloneForce = 1 << 2; // copy-on-write reflink or fail
+
+	inline function get_raw():Int return this;
+
+	@:op(A | B)
+	inline function join(other:FileCopyFlags) return this | other.get_raw();
+}

+ 75 - 0
std/asys/FileOpenFlags.hx

@@ -0,0 +1,75 @@
+package asys;
+
+/**
+	Flags used when opening a file with `asys.FileSystem.open` or other file
+	functions. Specify whether the opened file:
+
+	- will be readable
+	- will be writable
+	- will be truncated (all data lost) first
+	- will be in append mode
+	- will be opened exclusively by this process
+
+	Instances of this type can be created by combining flags with the bitwise or
+	operator:
+
+	```haxe
+	Truncate | Create | WriteOnly
+	```
+
+	Well-known combinations of flags can be specified with a string. The
+	supported modes are: `r`, `r+`, `rs+`, `sr+`, `w`, `w+`, `a`, `a+`, `wx`,
+	`xw`, `wx+`, `xw+`, `ax`, `xa`, `as`, `sa`, `ax+`, `xa+`, `as+`, `sa+`.
+**/
+enum abstract FileOpenFlags(Int) {
+	@:from public static function fromString(flags:String):FileOpenFlags {
+		return (switch (flags) {
+			case "r": ReadOnly;
+			case "r+": ReadWrite;
+			case "rs+": ReadWrite | Sync;
+			case "sr+": ReadWrite | Sync;
+			case "w": Truncate | Create | WriteOnly;
+			case "w+": Truncate | Create | ReadWrite;
+			case "a": Append | Create | WriteOnly;
+			case "a+": Append | Create | ReadWrite;
+			case "wx": Truncate | Create | WriteOnly | Excl;
+			case "xw": Truncate | Create | WriteOnly | Excl;
+			case "wx+": Truncate | Create | ReadWrite | Excl;
+			case "xw+": Truncate | Create | ReadWrite | Excl;
+			case "ax": Append | Create | WriteOnly | Excl;
+			case "xa": Append | Create | WriteOnly | Excl;
+			case "as": Append | Create | WriteOnly | Sync;
+			case "sa": Append | Create | WriteOnly | Sync;
+			case "ax+": Append | Create | ReadWrite | Excl;
+			case "xa+": Append | Create | ReadWrite | Excl;
+			case "as+": Append | Create | ReadWrite | Sync;
+			case "sa+": Append | Create | ReadWrite | Sync;
+			case _: throw "invalid file open flags";
+		});
+	}
+
+	function new(value:Int)
+		this = value;
+
+	inline function get_raw():Int return this;
+
+	@:op(A | B)
+	inline function join(other:FileOpenFlags) return new FileOpenFlags(this | other.get_raw());
+
+	// TODO: some of these don't make sense in Haxe-wrapped libuv
+	var Append = 0x400;
+	var Create = 0x40;
+	var Direct = 0x4000;
+	var Directory = 0x10000;
+	var Dsync = 0x1000;
+	var Excl = 0x80;
+	var NoAtime = 0x40000;
+	var NoCtty = 0x100;
+	var NoFollow = 0x20000;
+	var NonBlock = 0x800;
+	var ReadOnly = 0x0;
+	var ReadWrite = 0x2;
+	var Sync = 0x101000;
+	var Truncate = 0x200;
+	var WriteOnly = 0x1;
+}

+ 90 - 0
std/asys/FilePermissions.hx

@@ -0,0 +1,90 @@
+package asys;
+
+/**
+	File permissions in specify whether a file can be read, written, or executed
+	by its owner, its owning group, and everyone else. Instances of this type
+	can be constructed by combining individual file permissions with the `|`
+	operator:
+
+	```haxe
+	ReadOwner | WriteOwner | ReadGroup | ReadOthers
+	```
+
+	Alternatively, file permissions may be specified as a string with exactly 9
+	characters, in the format `rwxrwxrwx`, where each letter may instead be a
+	`-` character. The first three characters represent the permissions of the
+	owner, the second three characters represent the permissions of the owning
+	group, and the last three characters represent the permissions of everyone
+	else.
+
+	```haxe
+	"rw-r--r--"
+	```
+
+	Finally, file permissions may be constructed from an octal representation
+	using the `fromOctal` function.
+
+	```haxe
+	FilePermissions.fromOctal("644")
+	```
+**/
+enum abstract FilePermissions(Int) {
+	@:from public static function fromString(s:String):FilePermissions {
+		inline function bit(cc:Int, expect:Int):Int {
+			return (if (cc == expect)
+				1;
+			else if (cc == "-".code)
+				0;
+			else
+				throw "invalid file permissions string");
+		}
+		switch (s.length) {
+			case 9: // rwxrwxrwx
+				return new FilePermissions(bit(s.charCodeAt(0), "r".code) << 8
+					| bit(s.charCodeAt(1), "w".code) << 7
+					| bit(s.charCodeAt(2), "x".code) << 6
+					| bit(s.charCodeAt(3), "r".code) << 5
+					| bit(s.charCodeAt(4), "w".code) << 4
+					| bit(s.charCodeAt(5), "x".code) << 3
+					| bit(s.charCodeAt(6), "r".code) << 2
+					| bit(s.charCodeAt(7), "w".code) << 1
+					| bit(s.charCodeAt(8), "x".code));
+			case _:
+				throw "invalid file permissions string";
+		}
+	}
+
+	public static function fromOctal(s:String):FilePermissions {
+		inline function digit(n:Int):Int {
+			if (n >= "0".code && n <= "7".code) return n - "0".code;
+			throw "invalid octal file permissions";
+		}
+		switch (s.length) {
+			case 3: // 777
+				return new FilePermissions(digit(s.charCodeAt(0)) << 6
+					| digit(s.charCodeAt(1)) << 3
+					| digit(s.charCodeAt(2)));
+			case _:
+				throw "invalid octal file permissions";
+		}
+	}
+
+	var None = 0;
+	var ExecuteOthers = 1 << 0;
+	var WriteOthers = 1 << 1;
+	var ReadOthers = 1 << 2;
+	var ExecuteGroup = 1 << 3;
+	var WriteGroup = 1 << 4;
+	var ReadGroup = 1 << 5;
+	var ExecuteOwner = 1 << 6;
+	var WriteOwner = 1 << 7;
+	var ReadOwner = 1 << 8;
+
+	inline function new(value:Int)
+		this = value;
+
+	inline function get_raw():Int return this;
+
+	@:op(A | B)
+	inline function join(other:FilePermissions) return new FilePermissions(this | other.get_raw());
+}

+ 36 - 0
std/asys/FileStat.hx

@@ -0,0 +1,36 @@
+package asys;
+
+typedef FileStatData = {
+    // sys.FileStat compatibility
+    var atime:Date;
+    var ctime:Date;
+    var dev:Int;
+    var gid:Int;
+    var ino:Int;
+    var mode:Int;
+    var mtime:Date;
+    var nlink:Int;
+    var rdev:Int;
+    var size:Int;
+    var uid:Int;
+    
+    // node compatibility
+    var blksize:Int;
+    var blocks:Int;
+    var atimeMs:Float;
+    var ctimeMs:Float;
+    var mtimeMs:Float;
+    var birthtime:Date;
+    var birthtimeMs:Float;
+  };
+
+@:forward
+abstract FileStat(FileStatData) from FileStatData {
+  public function isBlockDevice():Bool return false;
+  public function isCharacterDevice():Bool return false;
+  public function isDirectory():Bool return false;
+  public function isFIFO():Bool return false;
+  public function isFile():Bool return false;
+  public function isSocket():Bool return false;
+  public function isSymbolicLink():Bool return false;
+}

+ 222 - 0
std/asys/FileSystem.hx

@@ -0,0 +1,222 @@
+package asys;
+
+import haxe.Error;
+import haxe.io.Bytes;
+import haxe.io.FilePath;
+import asys.io.*;
+
+typedef FileReadStreamCreationOptions = {
+	?flags:FileOpenFlags,
+	?mode:FilePermissions
+} &
+	asys.io.FileReadStream.FileReadStreamOptions;
+
+/**
+	This class provides methods for synchronous operations on files and
+	directories. For asynchronous operations, see `asys.async.FileSystem`.
+
+	Passing `null` as a path to any of the functions in this class will result
+	in unspecified behaviour.
+**/
+extern class FileSystem {
+	public static inline final async = asys.AsyncFileSystem;
+
+	/**
+		Tests specific user permissions for the file specified by `path`. If the
+		check fails, throws an exception. `mode` is one or more `FileAccessMode`
+		values:
+
+		- `FileAccessMode.Ok` - file is visible to the calling process (it exists)
+		- `FileAccessMode.Execute` - file can be executed by the calling proces
+		- `FileAccessMode.Write` - file can be written to by the calling proces
+		- `FileAccessMode.Read` - file can be read from by the calling proces
+
+		Mode values can be combined with the bitwise or operator, e.g. calling
+		`access` with the `mode`:
+
+		```haxe
+		FileAccessMode.Execute | FileAccessMode.Read
+		```
+
+		will check that the file is both readable and executable.
+
+		The result of this call should not be used in a condition before a call to
+		e.g. `open`, because this would introduce a race condition (the file could
+		be deleted after the `access` call, but before the `open` call). Instead,
+		the latter function should be called immediately and errors should be
+		handled with a `try ... catch` block.
+	**/
+	static function access(path:FilePath, ?mode:FileAccessMode = FileAccessMode.Ok):Void;
+
+	/**
+		Appends `data` at the end of the file located at `path`.
+	**/
+	static function appendFile(path:FilePath, data:Bytes, ?flags:FileOpenFlags /* a */, ?mode:FilePermissions /* 0666 */):Void;
+
+	/**
+		Changes the permissions of the file specific by `path` to `mode`.
+
+		If `path` points to a symbolic link, this function will change the
+		permissions of the target file, not the symbolic link itself, unless
+		`followSymLinks` is set to `false`.
+
+		TODO: `followSymLinks == false` is not implemented and will throw.
+	**/
+	static function chmod(path:FilePath, mode:FilePermissions, ?followSymLinks:Bool = true):Void;
+
+	/**
+		Changes the owner and group of the file specific by `path` to `uid` and
+		`gid`, respectively.
+
+		If `path` points to a symbolic link, this function will change the
+		permissions of the target file, not the symbolic link itself, unless
+		`followSymLinks` is set to `false`.
+
+		TODO: `followSymLinks == false` is not implemented and will throw.
+	**/
+	static function chown(path:FilePath, uid:Int, gid:Int, ?followSymLinks:Bool = true):Void;
+
+	/**
+		Copies the file at `src` to `dest`. If `dest` exists, it is overwritten.
+	**/
+	static function copyFile(src:FilePath, dest:FilePath /* , ?flags:FileCopyFlags */):Void;
+
+	/**
+		Creates a read stream (an instance of `IReadable`) for the given path.
+		`options` can be used to specify how the file is opened, as well as which
+		part of the file will be read by the stream.
+
+		- `options.flags` - see `open`.
+		- `options.mode` - see `open`.
+		- `options.autoClose` - whether the file should be closed automatically
+			once the stream is fully consumed.
+		- `options.start` - starting position in bytes (inclusive).
+		- `options.end` - end position in bytes (non-inclusive).
+	**/
+	static function createReadStream(path:FilePath, ?options:FileReadStreamCreationOptions):FileReadStream;
+
+	// static function createWriteStream(path:FilePath, ?options:{?flags:FileOpenFlags, ?mode:FilePermissions, ?autoClose:Bool, ?start:Int}):FileWriteStream;
+
+	/**
+		Returns `true` if the file or directory specified by `path` exists.
+
+		The result of this call should not be used in a condition before a call to
+		e.g. `open`, because this would introduce a race condition (the file could
+		be deleted after the `exists` call, but before the `open` call). Instead,
+		the latter function should be called immediately and errors should be
+		handled with a `try ... catch` block.
+	**/
+	static function exists(path:FilePath):Bool;
+
+	static function link(existingPath:FilePath, newPath:FilePath):Void;
+
+	/**
+		Creates a directory at the path `path`, with file mode `mode`.
+
+		If `recursive` is `false` (default), this function can only create one
+		directory at a time, the last component of `path`. If `recursive` is `true`,
+		intermediate directories will be created as needed.
+	**/
+	static function mkdir(path:FilePath, ?recursive:Bool = false, ?mode:FilePermissions /* 0777 */):Void;
+
+	/**
+		Creates a unique temporary directory. `prefix` should be a path template
+		ending in six `X` characters, which will be replaced with random characters.
+		Returns the path to the created directory.
+
+		The generated directory needs to be manually deleted by the process.
+	**/
+	static function mkdtemp(prefix:FilePath):FilePath;
+
+	/**
+		Opens the file located at `path`.
+	**/
+	static function open(path:FilePath, ?flags:FileOpenFlags /* a */, ?mode:FilePermissions /* 0666 */, ?binary:Bool = true):File;
+
+	/**
+		Reads the contents of a directory specified by `path`. Returns an array of
+		`FilePath`s relative to the specified directory (i.e. the paths are not
+		absolute). The array will not include `.` or `..`.
+	**/
+	static function readdir(path:FilePath):Array<FilePath>;
+
+	/**
+		Same as `readdir`, but returns an array of `DirectoryEntry` values instead.
+	**/
+	static function readdirTypes(path:FilePath):Array<DirectoryEntry>;
+
+	/**
+		Reads all the bytes of the file located at `path`.
+	**/
+	static function readFile(path:FilePath, ?flags:FileOpenFlags /* r */):Bytes;
+
+	/**
+		Returns the contents (target path) of the symbolic link located at `path`.
+	**/
+	static function readlink(path:FilePath):FilePath;
+
+	/**
+		Returns the canonical path name of `path` (which may be a relative path)
+		by resolving `.`, `..`, and symbolic links.
+	**/
+	static function realpath(path:FilePath):FilePath;
+
+	/**
+		Renames the file or directory located at `oldPath` to `newPath`. If a file
+		already exists at `newPath`, it is overwritten. If a directory already
+		exists at `newPath`, an exception is thrown.
+	**/
+	static function rename(oldPath:FilePath, newPath:FilePath):Void;
+
+	/**
+		Deletes the directory located at `path`. If the directory is not empty or
+		cannot be deleted, an error is thrown.
+	**/
+	static function rmdir(path:FilePath):Void;
+
+	/**
+		Returns information about the file located at `path`.
+
+		If `path` points to a symbolic link, this function will return information
+		about the target file, not the symbolic link itself, unless `followSymLinks`
+		is set to `false`.
+	**/
+	static function stat(path:FilePath, ?followSymLinks:Bool = true):asys.FileStat;
+
+	/**
+		Creates a symbolic link at `path`, pointing to `target`.
+
+		The `type` argument is ignored on all platforms except `Windows`.
+	**/
+	static function symlink(target:FilePath, path:FilePath, ?type:SymlinkType = SymlinkType.SymlinkDir):Void;
+
+	/**
+		Truncates the file located at `path` to exactly `len` bytes. If the file was
+		larger than `len` bytes, the extra data is lost. If the file was smaller
+		than `len` bytes, the file is extended with null bytes.
+	**/
+	static function truncate(path:FilePath, ?len:Int = 0):Void;
+
+	/**
+		Deletes the file located at `path`.
+	**/
+	static function unlink(path:FilePath):Void;
+
+	/**
+		Modifies the system timestamps of the file located at `path`.
+	**/
+	static function utimes(path:FilePath, atime:Date, mtime:Date):Void;
+
+	/**
+		Creates a file watcher for `path`.
+
+		@param recursive If `true`, the file watcher will signal for changes in
+			sub-directories of `path` as well.
+	**/
+	static function watch(path:FilePath, ?recursive:Bool = false):FileWatcher;
+
+	/**
+		Writes `data` to the file located at `path`.
+	**/
+	static function writeFile(path:FilePath, data:Bytes, ?flags:FileOpenFlags /* w */, ?mode:FilePermissions /* 0666 */):Void;
+}

+ 82 - 0
std/asys/FileWatcher.hx

@@ -0,0 +1,82 @@
+package asys;
+
+import haxe.Error;
+import haxe.NoData;
+import haxe.async.*;
+import haxe.io.FilePath;
+
+typedef FileWatcherNative =
+	#if doc_gen
+	{function ref():Void; function unref():Void;};
+	#elseif eval
+	eval.uv.FileWatcher;
+	#elseif hl
+	hl.uv.FileWatcher;
+	#elseif neko
+	neko.uv.FileWatcher;
+	#else
+	#error "file watcher not supported on this platform"
+	#end
+
+/**
+	File watchers can be obtained with the `asys.FileSystem.watch` method.
+	Instances of this class will emit signals whenever any file in their watched
+	path is modified.
+**/
+class FileWatcher {
+	/**
+		Emitted when a watched file is modified.
+	**/
+	public final changeSignal:Signal<FileWatcherEvent> = new ArraySignal();
+
+	/**
+		Emitted when `this` watcher is fully closed. No further signals will be
+		emitted.
+	**/
+	public final closeSignal:Signal<NoData> = new ArraySignal();
+
+	/**
+		Emitted when an error occurs.
+	**/
+	public final errorSignal:Signal<Error> = new ArraySignal();
+
+	private var native:FileWatcherNative;
+
+	private function new(filename:FilePath, recursive:Bool) {
+		#if !doc_gen
+		native = new FileWatcherNative(filename, recursive, (err, event) -> {
+			if (err != null)
+				return errorSignal.emit(err);
+			changeSignal.emit(event);
+		});
+		#end
+	}
+
+	/**
+		Closes `this` watcher. This operation is asynchronous and will emit the
+		`closeSignal` once done. If `listener` is given, it will be added to the
+		`closeSignal`.
+	**/
+	public function close(?listener:Listener<NoData>):Void {
+		if (listener != null)
+			closeSignal.once(listener);
+		#if doc_gen
+		var err:haxe.Error = null;
+		({
+		#else
+		native.close((err, _) -> {
+		#end
+			if (err != null)
+				errorSignal.emit(err);
+			closeSignal.emit(new NoData());
+		});
+	}
+
+	public function ref():Void {
+		native.ref();
+	}
+
+	public function unref():Void {
+		native.unref();
+	}
+}

+ 14 - 0
std/asys/FileWatcherEvent.hx

@@ -0,0 +1,14 @@
+package asys;
+
+import haxe.io.FilePath;
+
+/**
+	Events emitted by the `changeSignal` of a `sys.FileWatcher`. Any file change
+	consists of a name change (`Rename`), a content change (`Change`), or both
+	(`RenameChange`).
+**/
+enum FileWatcherEvent {
+	Rename(newPath:FilePath);
+	Change(path:FilePath);
+	RenameChange(path:FilePath);
+}

+ 71 - 0
std/asys/Net.hx

@@ -0,0 +1,71 @@
+package asys;
+
+import haxe.NoData;
+import haxe.async.*;
+import asys.net.*;
+import asys.net.SocketOptions.SocketConnectTcpOptions;
+import asys.net.SocketOptions.SocketConnectIpcOptions;
+import asys.net.Server.ServerOptions;
+import asys.net.Server.ServerListenTcpOptions;
+import asys.net.Server.ServerListenIpcOptions;
+
+enum SocketConnect {
+	Tcp(options:SocketConnectTcpOptions);
+	Ipc(options:SocketConnectIpcOptions);
+}
+
+enum ServerListen {
+	Tcp(options:ServerListenTcpOptions);
+	Ipc(options:ServerListenIpcOptions);
+}
+
+typedef SocketCreationOptions = SocketOptions & {?connect:SocketConnect};
+
+typedef ServerCreationOptions = ServerOptions & {?listen:ServerListen};
+
+/**
+	Network utilities.
+**/
+class Net {
+	/**
+		Constructs a socket with the given `options`. If `options.connect` is
+		given, an appropriate `connect` method is called on the socket. If `cb` is
+		given, it is passed to the `connect` method, so it will be called once the
+		socket successfully connects or an error occurs during connecting.
+
+		The `options` object is given both to the `Socket` constructor and to the
+		`connect` method.
+	**/
+	public static function createConnection(options:SocketCreationOptions, ?cb:Callback<NoData>):Socket {
+		var socket = Socket.create(options);
+		if (options.connect != null)
+			switch (options.connect) {
+				case Tcp(options):
+					socket.connectTcp(options, cb);
+				case Ipc(options):
+					socket.connectIpc(options, cb);
+			}
+		return socket;
+	}
+
+	/**
+		Constructs a server with the given `options`. If `options.listen` is
+		given, an appropriate `listen` method is called on the server. If `cb` is
+		given, it is passed to the `listen` method, so it will be called for each
+		client that connects to the server.
+
+		The `options` object is given both to the `Server` constructor and to the
+		`listen` method.
+	**/
+	public static function createServer(?options:ServerCreationOptions, ?listener:Listener<Socket>):Server {
+		var server = new Server(options);
+		if (options.listen != null)
+			switch (options.listen) {
+				case Tcp(options):
+					server.listenTcp(options, listener);
+				case Ipc(options):
+					server.listenIpc(options, listener);
+			}
+		return server;
+	}
+}

+ 318 - 0
std/asys/Process.hx

@@ -0,0 +1,318 @@
+package asys;
+
+import haxe.Error;
+import haxe.NoData;
+import haxe.async.*;
+import haxe.io.*;
+import asys.net.Socket;
+import asys.io.*;
+import asys.uv.UVProcessSpawnFlags;
+
+private typedef Native =
+	#if doc_gen
+	Void;
+	#elseif eval
+	eval.uv.Process;
+	#elseif hl
+	hl.uv.Process;
+	#elseif neko
+	neko.uv.Process;
+	#else
+	#error "process not supported on this platform"
+	#end
+
+private typedef NativeProcessIO =
+	#if doc_gen
+	Void;
+	#elseif eval
+	eval.uv.Process.ProcessIO;
+	#elseif hl
+	hl.uv.Process.ProcessIO;
+	#elseif neko
+	neko.uv.Process.ProcessIO;
+	#else
+	#error "process not supported on this platform"
+	#end
+
+/**
+	Options for spawning a process. See `Process.spawn`.
+**/
+typedef ProcessSpawnOptions = {
+	?cwd:FilePath,
+	?env:Map<String, String>,
+	?argv0:String,
+	?stdio:Array<ProcessIO>,
+	?detached:Bool,
+	?uid:Int,
+	?gid:Int,
+	// ?shell:?,
+	?windowsVerbatimArguments:Bool,
+	?windowsHide:Bool
+};
+
+/**
+	Class representing a spawned process.
+**/
+class Process {
+	/**
+		Execute the given `command` with `args` (none by default). `options` can be
+		specified to change the way the process is spawned.
+
+		`options.stdio` is an optional array of `ProcessIO` specifications which
+		can be used to define the file descriptors for the new process:
+
+		- `Ignore` - skip the current position. No stream or pipe will be open for
+			this index.
+		- `Inherit` - inherit the corresponding file descriptor from the current
+			process. Shares standard input, standard output, and standard error in
+			index 0, 1, and 2, respectively. In index 3 or higher, `Inherit` has the
+			same effect as `Ignore`.
+		- `Pipe(readable, writable, ?pipe)` - create or use a pipe. `readable` and
+			`writable` specify whether the pipe will be readable and writable from
+			the point of view of the spawned process. If `pipe` is given, it is used
+			directly, otherwise a new pipe is created.
+		- `Ipc` - create an IPC (inter-process comunication) pipe. Only one may be
+			specified in `options.stdio`. This special pipe will not have an entry in
+			the `stdio` array of the resulting process; instead, messages can be sent
+			using the `send` method, and received over `messageSignal`. IPC pipes
+			allow sending and receiving structured Haxe data, as well as connected
+			sockets and pipes.
+
+		Pipes are made available in the `stdio` array afther the process is
+		spawned. Standard file descriptors have their own variables:
+
+		- `stdin` - set to point to a pipe in index 0, if it exists and is
+			read-only for the spawned process.
+		- `stdout` - set to point to a pipe in index 1, if it exists and is
+			write-only for the spawned process.
+		- `stderr` - set to point to a pipe in index 2, if it exists and is
+			write-only for the spawned process.
+
+		If `options.stdio` is not given,
+		`[Pipe(true, false), Pipe(false, true), Pipe(false, true)]` is used as a
+		default.
+
+		@param options.cwd Path to the working directory. Defaults to the current
+			working directory if not given.
+		@param options.env Environment variables. Defaults to the environment
+			variables of the current process if not given.
+		@param options.argv0 First entry in the `argv` array for the spawned
+			process. Defaults to `command` if not given.
+		@param options.stdio Array of `ProcessIO` specifications, see above.
+		@param options.detached When `true`, creates a detached process which can
+			continue running after the current process exits. Note that `unref` must
+			be called on the spawned process otherwise the event loop of the current
+			process is kept allive.
+		@param options.uid User identifier.
+		@param options.gid Group identifier.
+		@param options.windowsVerbatimArguments (Windows only.) Do not perform
+			automatic quoting or escaping of arguments.
+		@param options.windowsHide (Windows only.) Automatically hide the window of
+			the spawned process.
+	**/
+	public static function spawn(command:String, ?args:Array<String>, ?options:ProcessSpawnOptions):Process {
+		var proc = new Process();
+		var flags:UVProcessSpawnFlags = None;
+		if (options == null)
+			options = {};
+		if (options.detached)
+			flags |= UVProcessSpawnFlags.Detached;
+		if (options.uid != null)
+			flags |= UVProcessSpawnFlags.SetUid;
+		if (options.gid != null)
+			flags |= UVProcessSpawnFlags.SetGid;
+		if (options.windowsVerbatimArguments)
+			flags |= UVProcessSpawnFlags.WindowsVerbatimArguments;
+		if (options.windowsHide)
+			flags |= UVProcessSpawnFlags.WindowsHide;
+		if (options.stdio == null)
+			options.stdio = [Pipe(true, false), Pipe(false, true), Pipe(false, true)];
+		var stdin:IWritable = null;
+		var stdout:IReadable = null;
+		var stderr:IReadable = null;
+		var stdioPipes = [];
+		var ipc:Socket = null;
+		var nativeStdio:Array<NativeProcessIO> = [
+			for (i in 0...options.stdio.length)
+				switch (options.stdio[i]) {
+					case Ignore:
+						Ignore;
+					case Inherit:
+						Inherit;
+					case Pipe(r, w, pipe):
+						if (pipe == null) {
+							pipe = Socket.create();
+							@:privateAccess pipe.initPipe(false);
+						} else {
+							if (@:privateAccess pipe.native == null)
+								throw "invalid pipe";
+						}
+						switch (i) {
+							case 0 if (r && !w):
+								stdin = pipe;
+							case 1 if (!r && w):
+								stdout = pipe;
+							case 2 if (!r && w):
+								stderr = pipe;
+							case _:
+						}
+						stdioPipes[i] = pipe;
+						Pipe(r, w, @:privateAccess pipe.native);
+					case Ipc:
+						if (ipc != null)
+							throw "only one IPC pipe can be specified for a process";
+						ipc = Socket.create();
+						@:privateAccess ipc.initPipe(true);
+						Ipc(@:privateAccess ipc.native);
+				}
+		];
+		var args = args != null ? args : [];
+		if (options.argv0 != null)
+			args.unshift(options.argv0);
+		else
+			args.unshift(command);
+		var native = new Native(
+			(err, data) -> proc.exitSignal.emit(data),
+			command,
+			args,
+			options.env != null ? [ for (k => v in options.env) '$k=$v' ] : [],
+			options.cwd != null ? @:privateAccess options.cwd.get_raw() : Sys.getCwd(),
+			flags,
+			nativeStdio,
+			options.uid != null ? options.uid : 0,
+			options.gid != null ? options.gid : 0
+		);
+		proc.native = native;
+		if (ipc != null) {
+			proc.connected = true;
+			proc.ipc = ipc;
+			proc.ipcOut = @:privateAccess new asys.io.IpcSerializer(ipc);
+			proc.ipcIn = @:privateAccess new asys.io.IpcUnserializer(ipc);
+			proc.messageSignal = new ArraySignal(); //proc.ipcIn.messageSignal;
+			proc.ipcIn.messageSignal.on(message -> proc.messageSignal.emit(message));
+		}
+		proc.stdin = stdin;
+		proc.stdout = stdout;
+		proc.stderr = stderr;
+		proc.stdio = stdioPipes;
+		return proc;
+	}
+
+	/**
+		Emitted when `this` process and all of its pipes are closed.
+	**/
+	public final closeSignal:Signal<NoData> = new ArraySignal();
+
+	// public final disconnectSignal:Signal<NoData> = new ArraySignal(); // IPC
+
+	/**
+		Emitted when an error occurs during communication with `this` process.
+	**/
+	public final errorSignal:Signal<Error> = new ArraySignal();
+
+	/**
+		Emitted when `this` process exits, potentially due to a signal.
+	**/
+	public final exitSignal:Signal<ProcessExit> = new ArraySignal();
+
+	/**
+		Emitted when a message is received over IPC. The process must be created
+		with an `Ipc` entry in `options.stdio`; see `Process.spawn`.
+	**/
+	public var messageSignal(default, null):Signal<IpcMessage>;
+
+	public var connected(default, null):Bool = false;
+	public var killed:Bool;
+
+	private function get_pid():Int {
+		return native.getPid();
+	}
+
+	/**
+		Process identifier of `this` process. A PID uniquely identifies a process
+		on its host machine for the duration of its lifetime.
+	**/
+	public var pid(get, never):Int;
+
+	/**
+		Standard input. May be `null` - see `options.stdio` in `spawn`.
+	**/
+	public var stdin:IWritable;
+
+	/**
+		Standard output. May be `null` - see `options.stdio` in `spawn`.
+	**/
+	public var stdout:IReadable;
+
+	/**
+		Standard error. May be `null` - see `options.stdio` in `spawn`.
+	**/
+	public var stderr:IReadable;
+
+	/**
+		Pipes created between the current (host) process and `this` (spawned)
+		process. The order corresponds to the `ProcessIO` specifiers in
+		`options.stdio` in `spawn`. This array can be used to access non-standard
+		pipes, i.e. file descriptors 3 and higher, as well as file descriptors 0-2
+		with non-standard read/write access.
+	**/
+	public var stdio:Array<Socket>;
+
+	var native:Native;
+	var ipc:Socket;
+	var ipcOut:asys.io.IpcSerializer;
+	var ipcIn:asys.io.IpcUnserializer;
+
+	// public function disconnect():Void; // IPC
+
+	/**
+		Send a signal to `this` process.
+	**/
+	public function kill(?signal:Int = 7):Void {
+		native.kill(signal);
+	}
+
+	/**
+		Close `this` process handle and all pipes in `stdio`.
+	**/
+	public function close(?cb:Callback<NoData>):Void {
+		var needed = 1;
+		var closed = 0;
+		function close(err:Error, _:NoData):Void {
+			closed++;
+			if (closed == needed && cb != null)
+				cb(null, new NoData());
+		}
+		for (pipe in stdio) {
+			if (pipe != null) {
+				needed++;
+				pipe.destroy(close);
+			}
+		}
+		if (connected) {
+			needed++;
+			ipc.destroy(close);
+		}
+		native.close(close);
+	}
+
+	/**
+		Send `data` to the process over the IPC channel. The process must be
+		created with an `Ipc` entry in `options.stdio`; see `Process.spawn`.
+	**/
+	public function send(message:IpcMessage):Void {
+		if (!connected)
+			throw "IPC not connected";
+		ipcOut.write(message);
+	}
+
+	public function ref():Void {
+		native.ref();
+	}
+
+	public function unref():Void {
+		native.unref();
+	}
+
+	private function new() {}
+}

+ 16 - 0
std/asys/ProcessExit.hx

@@ -0,0 +1,16 @@
+package asys;
+
+/**
+	Represents how a process exited.
+**/
+typedef ProcessExit = {
+	/**
+		Exit code of the process. Non-zero values usually indicate an error.
+		Specific meanings of exit codes differ from program to program.
+	**/
+	var code:Int;
+	/**
+		Signal that cause the process to exit, or zero if none.
+	**/
+	var signal:Int;
+};

+ 10 - 0
std/asys/ProcessIO.hx

@@ -0,0 +1,10 @@
+package asys;
+
+enum ProcessIO {
+	Ignore;
+	Inherit;
+	Pipe(readable:Bool, writable:Bool, ?pipe:asys.net.Socket);
+	Ipc;
+	// Stream(_);
+	// Fd(_);
+}

+ 9 - 0
std/asys/SymlinkType.hx

@@ -0,0 +1,9 @@
+package asys;
+
+enum abstract SymlinkType(Int) {
+  var SymlinkFile = 0;
+  var SymlinkDir = 1;
+  var SymlinkJunction = 2; // Windows only
+  
+  inline function get_raw():Int return this;
+}

+ 57 - 0
std/asys/Timer.hx

@@ -0,0 +1,57 @@
+package asys;
+
+private typedef Native =
+	#if doc_gen
+	Void;
+	#elseif eval
+	eval.uv.Timer;
+	#elseif hl
+	hl.uv.Timer;
+	#elseif neko
+	neko.uv.Timer;
+	#else
+	#error "timer not supported on this platform"
+	#end
+
+class Timer {
+	public static function delay(f:() -> Void, timeMs:Int):Timer {
+		var t = new Timer(timeMs);
+		t.run = function() {
+			t.stop();
+			f();
+		};
+		return t;
+	}
+
+	public static function measure<T>(f:()->T, ?pos:haxe.PosInfos):T {
+		var t0 = stamp();
+		var r = f();
+		haxe.Log.trace((stamp() - t0) + "s", pos);
+		return r;
+	}
+
+	public static function stamp():Float {
+		// TODO: libuv?
+		return Sys.time();
+	}
+
+	var native:Native;
+
+	public function new(timeMs:Int) {
+		native = new Native(timeMs, () -> run());
+	}
+
+	public dynamic function run():Void {}
+
+	public function stop():Void {
+		native.close((err) -> {});
+	}
+
+	public function ref():Void {
+		native.ref();
+	}
+
+	public function unref():Void {
+		native.unref();
+	}
+}

+ 47 - 0
std/asys/io/AsyncFile.hx

@@ -0,0 +1,47 @@
+package asys.io;
+
+import haxe.Error;
+import haxe.NoData;
+import haxe.async.*;
+import haxe.io.Bytes;
+import haxe.io.Encoding;
+import asys.*;
+
+/**
+	This class provides methods for asynchronous operations on files instances.
+	For synchronous operations, see `asys.io.File`. To obtain an instance of
+	this class, use the `async` field of `asys.io.File`.
+
+	```haxe
+	var file = asys.FileSystem.open("example.txt", "r");
+	file.async.readFile(contents -> trace(contents.toString()));
+	```
+
+	All methods here are asynchronous versions of the functions in
+	`asys.io.File`. Please see them for a description of the arguments and
+	use of each method.
+
+	Any synchronous method that returns no value (`Void` return type) has an
+	extra `callback:Callback<NoData>` argument.
+
+	Any synchronous method that returns a value has an extra
+	`callback:Callback<T>` argument, where `T` is the return type of the
+	synchronous method.
+
+	Errors are communicated through the callbacks or in some cases thrown
+	immediately.
+**/
+extern class AsyncFile {
+	function chmod(mode:FilePermissions, callback:Callback<NoData>):Void;
+	function chown(uid:Int, gid:Int, callback:Callback<NoData>):Void;
+	function close(callback:Callback<NoData>):Void;
+	function datasync(callback:Callback<NoData>):Void;
+	function readBuffer(buffer:Bytes, offset:Int, length:Int, position:Int, callback:Callback<{bytesRead:Int, buffer:Bytes}>):Void;
+	function readFile(callback:Callback<Bytes>):Void;
+	function stat(callback:Callback<FileStat>):Void;
+	function sync(callback:Callback<NoData>):Void;
+	function truncate(?len:Int = 0, callback:Callback<NoData>):Void;
+	function utimes(atime:Date, mtime:Date, callback:Callback<NoData>):Void;
+	function writeBuffer(buffer:Bytes, offset:Int, length:Int, position:Int, callback:Callback<{bytesWritten:Int, buffer:Bytes}>):Void;
+	function writeString(str:String, ?position:Int, ?encoding:Encoding, callback:Callback<{bytesWritten:Int, buffer:Bytes}>):Void;
+}

+ 88 - 0
std/asys/io/File.hx

@@ -0,0 +1,88 @@
+package asys.io;
+
+import haxe.Error;
+import haxe.io.Bytes;
+import haxe.io.Encoding;
+import asys.*;
+
+/**
+	Class representing an open file. Some methods in this class are instance
+	variants of the same methods in `asys.FileSystem`.
+**/
+extern class File {
+	private function get_async():AsyncFile;
+
+	var async(get, never):AsyncFile;
+
+	/**
+		See `asys.FileSystem.chmod`.
+	**/
+	function chmod(mode:FilePermissions):Void;
+
+	/**
+		See `asys.FileSystem.chown`.
+	**/
+	function chown(uid:Int, gid:Int):Void;
+
+	/**
+		Closes the file. Any operation after this method is called is invalid.
+	**/
+	function close():Void;
+
+	/**
+		Same as `sync`, but metadata is not flushed unless needed for subsequent
+		data reads to be correct. E.g. changes to the modification times are not
+		flushed, but changes to the filesize do.
+	**/
+	function datasync():Void;
+
+	/**
+		Reads a part of `this` file into the given `buffer`.
+
+		@param buffer Buffer to which data will be written.
+		@param offset Position in `buffer` at which to start writing.
+		@param length Number of bytes to read from `this` file.
+		@param position Position in `this` file at which to start reading.
+	**/
+	function readBuffer(buffer:Bytes, offset:Int, length:Int, position:Int):{bytesRead:Int, buffer:Bytes};
+
+	/**
+		Reads the entire contents of `this` file.
+	**/
+	function readFile():Bytes;
+
+	/**
+		See `asys.FileSystem.stat`.
+	**/
+	function stat():FileStat;
+
+	/**
+		Flushes all modified data and metadata of `this` file to the disk.
+	**/
+	function sync():Void;
+
+	/**
+		See `asys.FileSystem.truncate`.
+	**/
+	function truncate(?len:Int = 0):Void;
+
+	/**
+		See `asys.FileSystem.utimes`.
+	**/
+	function utimes(atime:Date, mtime:Date):Void;
+
+	/**
+		Writes a part of the given `buffer` into `this` file.
+
+		@param buffer Buffer from which data will be read.
+		@param offset Position in `buffer` at which to start reading.
+		@param length Number of bytes to write to `this` file.
+		@param position Position in `this` file at which to start writing.
+	**/
+	function writeBuffer(buffer:Bytes, offset:Int, length:Int, position:Int):{bytesWritten:Int, buffer:Bytes};
+
+	/**
+		Writes a string to `this` file at `position`.
+	**/
+	function writeString(str:String, ?position:Int, ?encoding:Encoding):{bytesWritten:Int, buffer:Bytes};
+}

+ 42 - 0
std/asys/io/FileInput.hx

@@ -0,0 +1,42 @@
+package asys.io;
+
+import haxe.io.Bytes;
+
+class FileInput extends haxe.io.Input {
+	final file:asys.io.File;
+	var position:Int = 0;
+
+	function new(file:asys.io.File) {
+		this.file = file;
+	}
+
+	public function seek(p:Int, pos:sys.io.FileSeek):Void {
+		position = (switch (pos) {
+			case SeekBegin: p;
+			case SeekCur: position + p;
+			case SeekEnd: file.stat().size + p;
+		});
+	}
+
+	public function tell():Int {
+		return position;
+	}
+
+	override public function readByte():Int {
+		var buf = Bytes.alloc(1);
+		file.readBuffer(buf, 0, 1, position++);
+		return buf.get(0);
+	}
+
+	override public function readBytes(buf:Bytes, pos:Int, len:Int):Int {
+		if (pos < 0 || len < 0 || pos + len > buf.length)
+			throw haxe.io.Error.OutsideBounds;
+		var read = file.readBuffer(buf, pos, len, position).bytesRead;
+		position += read;
+		return read;
+	}
+
+	override public function close():Void {
+		file.close();
+	}
+}

+ 46 - 0
std/asys/io/FileOutput.hx

@@ -0,0 +1,46 @@
+package asys.io;
+
+import haxe.io.Bytes;
+
+class FileOutput extends haxe.io.Output {
+	final file:asys.io.File;
+	var position:Int = 0;
+
+	function new(file:asys.io.File) {
+		this.file = file;
+	}
+
+	public function seek(p:Int, pos:sys.io.FileSeek):Void {
+		position = (switch (pos) {
+			case SeekBegin: p;
+			case SeekCur: position + p;
+			case SeekEnd: file.stat().size + p;
+		});
+	}
+
+	public function tell():Int {
+		return position;
+	}
+
+	override public function writeByte(byte:Int):Void {
+		var buf = Bytes.alloc(1);
+		buf.set(1, byte);
+		file.writeBuffer(buf, 0, 1, position++);
+	}
+
+	override public function writeBytes(buf:Bytes, pos:Int, len:Int):Int {
+		if (pos < 0 || len < 0 || pos + len > buf.length)
+			throw haxe.io.Error.OutsideBounds;
+		var written = file.writeBuffer(buf, pos, len, position).bytesWritten;
+		position += written;
+		return written;
+	}
+
+	override public function flush():Void {
+		file.datasync();
+	}
+
+	override public function close():Void {
+		file.close();
+	}
+}

+ 20 - 0
std/asys/io/FileReadStream.hx

@@ -0,0 +1,20 @@
+package asys.io;
+
+import haxe.NoData;
+import haxe.async.Signal;
+
+typedef FileReadStreamOptions = {
+	?autoClose:Bool,
+	?start:Int,
+	?end:Int,
+	?highWaterMark:Int
+};
+
+extern class FileReadStream extends haxe.io.Readable {
+	final openSignal:Signal<File>;
+	final readySignal:Signal<NoData>;
+
+	var bytesRead:Int;
+	var path:String;
+	var pending:Bool;
+}

+ 13 - 0
std/asys/io/FileWriteStream.hx

@@ -0,0 +1,13 @@
+package asys.io;
+
+import haxe.NoData;
+import haxe.async.Signal;
+
+extern class FileWriteStream extends haxe.io.Writable {
+  final openSignal:Signal<File>;
+  final readySignal:Signal<NoData>;
+  
+  var bytesWritten:Int;
+  var path:String;
+  var pending:Bool;
+}

+ 21 - 0
std/asys/io/IpcMessage.hx

@@ -0,0 +1,21 @@
+package asys.io;
+
+import asys.net.Socket;
+
+/**
+	A message sent over an IPC channel. Sent with `Process.send` to a sub-process
+	or with `CurrentProcess.send` to the parent process. Received with
+	`Process.messageSignal` from a sub-process, or `CurrentProcess.messageSignal`
+	from the parent process.
+**/
+typedef IpcMessage = {
+	/**
+		The actual message. May be any data that is serializable with
+		`haxe.Serializer`.
+	**/
+	var message:Dynamic;
+	/**
+		Sockets and pipes associated with the message. Must be connected.
+	**/
+	var ?sockets:Array<Socket>;
+};

+ 50 - 0
std/asys/io/IpcSerializer.hx

@@ -0,0 +1,50 @@
+package asys.io;
+
+import haxe.Error;
+import haxe.NoData;
+import haxe.async.*;
+import haxe.io.*;
+import asys.net.Socket;
+
+/**
+	Class used internally to send messages and handles over an IPC channel. See
+	`Process.spawn` for creating an IPC channel and `Process.send` for sending
+	messages over the channel.
+**/
+class IpcSerializer {
+	static var activeSerializer:IpcSerializer = null;
+	static var dummyBuffer = Bytes.ofString("s");
+
+	final pipe:Socket;
+	// final chunkSockets:Array<Socket> = [];
+
+	function new(pipe:Socket) {
+		this.pipe = pipe;
+	}
+
+	/**
+		Sends `data` over the pipe. `data` will be serialized with a call to
+		`haxe.Serializer.run`. Objects of type `Socket` can be sent along with the
+		data if `handles` is provided.
+	**/
+	public function write(message:IpcMessage):Void {
+		activeSerializer = this;
+		if (message.sockets != null)
+			for (socket in message.sockets) {
+				if (!socket.connected)
+					throw "cannot send unconnected socket over IPC";
+				pipe.writeHandle(dummyBuffer, socket);
+			}
+		var serial = haxe.Serializer.run(message.message);
+		pipe.write(Bytes.ofString('${serial.length}:$serial'));
+		// chunkSockets.resize(0);
+		activeSerializer = null;
+	}
+
+	/**
+		// TODO: see `Socket.hxUnserialize` comment
+		Sends `data` over the pipe. `data` will be serialized with a call to
+		`haxe.Serializer.run`. However, objects of type `asys.async.net.Socket`
+		will also be correctly serialized and can be received by the other end.
+	**/
+}

+ 85 - 0
std/asys/io/IpcUnserializer.hx

@@ -0,0 +1,85 @@
+package asys.io;
+
+import haxe.Error;
+import haxe.NoData;
+import haxe.async.*;
+import haxe.io.*;
+import asys.net.Socket;
+
+/**
+	Class used internally to receive messages and handles over an IPC channel.
+	See `CurrentProcess.initIpc` for initialising IPC for a process.
+**/
+class IpcUnserializer {
+	static var activeUnserializer:IpcUnserializer = null;
+
+	public final messageSignal:Signal<IpcMessage> = new ArraySignal();
+	public final errorSignal:Signal<Dynamic> = new ArraySignal();
+
+	final pipe:Socket;
+	// var chunkSockets:Array<Socket> = [];
+	var chunkLenbuf:String = "";
+	var chunkBuf:StringBuf;
+	var chunkSize:Null<Int> = 0;
+	var chunkSocketCount:Int = 0;
+
+	function new(pipe:Socket) {
+		this.pipe = pipe;
+		pipe.dataSignal.on(handleData);
+	}
+
+	function handleData(data:Bytes):Void {
+		if (data.length == 0)
+			return;
+		try {
+			var data = data.toString();
+			while (data != null) {
+				if (chunkSize == 0) {
+					chunkLenbuf += data;
+					var colonPos = chunkLenbuf.indexOf(":");
+					if (colonPos != -1) {
+						chunkSocketCount = 0;
+						while (chunkLenbuf.charAt(chunkSocketCount) == "s")
+							chunkSocketCount++;
+						chunkSize = Std.parseInt(chunkLenbuf.substr(chunkSocketCount, colonPos));
+						if (chunkSize == null || chunkSize <= 0) {
+							chunkSize = 0;
+							throw "invalid chunk size received";
+						}
+						chunkBuf = new StringBuf();
+						chunkBuf.add(chunkLenbuf.substr(colonPos + 1));
+						chunkLenbuf = "";
+						// chunkSockets.resize(0);
+					}
+				} else {
+					chunkBuf.add(data);
+				}
+				data = null;
+				if (chunkSize != 0) {
+					if (chunkBuf.length >= chunkSize) {
+						var serial = chunkBuf.toString();
+						if (serial.length > chunkSize) {
+							data = serial.substr(chunkSize);
+							serial = serial.substr(0, chunkSize);
+						}
+						chunkBuf = null;
+						var chunkSockets = [];
+						if (chunkSocketCount > pipe.handlesPending)
+							throw "not enough handles received";
+						for (i in 0...chunkSocketCount)
+							chunkSockets.push(pipe.readHandle());
+						activeUnserializer = this;
+						var message = haxe.Unserializer.run(serial);
+						messageSignal.emit({message: message, sockets: chunkSockets});
+						chunkSize = 0;
+						chunkSocketCount = 0;
+						// chunkSockets.resize(0);
+						activeUnserializer = null;
+					}
+				}
+			}
+		} catch (e:Dynamic) {
+			errorSignal.emit(e);
+		}
+	}
+}

+ 21 - 0
std/asys/net/Address.hx

@@ -0,0 +1,21 @@
+package asys.net;
+
+import haxe.io.Bytes;
+
+/**
+	Represents a resolved IP address. The methods from `asys.net.AddressTools`
+	are always available on `Address` instances.
+**/
+@:using(asys.net.AddressTools)
+enum Address {
+	/**
+		32-bit IPv4 address. As an example, the IP address `127.0.0.1` is
+		represented as `Ipv4(0x7F000001)`.
+	**/
+	Ipv4(raw:Int);
+
+	/**
+		128-bit IPv6 address.
+	**/
+	Ipv6(raw:Bytes);
+}

+ 223 - 0
std/asys/net/AddressTools.hx

@@ -0,0 +1,223 @@
+package asys.net;
+
+import haxe.io.Bytes;
+import asys.net.IpFamily;
+
+/**
+	Methods for converting to and from `Address` instances.
+**/
+class AddressTools {
+	static final v4re = {
+		final v4seg = "(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])";
+		final v4str = '${v4seg}\\.${v4seg}\\.${v4seg}\\.${v4seg}';
+		new EReg('^${v4str}$$', "");
+	};
+
+	static final v6re = {
+		final v4seg = "(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])";
+		final v4str = '${v4seg}\\.${v4seg}\\.${v4seg}\\.${v4seg}';
+		final v6seg = "(?:[0-9a-fA-F]{1,4})";
+		new EReg("^("
+			+ '(?:${v6seg}:){7}(?:${v6seg}|:)|'
+			+ '(?:${v6seg}:){6}(?:${v4str}|:${v6seg}|:)|'
+			+ '(?:${v6seg}:){5}(?::${v4str}|(:${v6seg}){1,2}|:)|'
+			+ '(?:${v6seg}:){4}(?:(:${v6seg}){0,1}:${v4str}|(:${v6seg}){1,3}|:)|'
+			+ '(?:${v6seg}:){3}(?:(:${v6seg}){0,2}:${v4str}|(:${v6seg}){1,4}|:)|'
+			+ '(?:${v6seg}:){2}(?:(:${v6seg}){0,3}:${v4str}|(:${v6seg}){1,5}|:)|'
+			+ '(?:${v6seg}:){1}(?:(:${v6seg}){0,4}:${v4str}|(:${v6seg}){1,6}|:)|'
+			+ '(?::((?::${v6seg}){0,5}:${v4str}|(?::${v6seg}){1,7}|:))'
+			+ ")$", // "(%[0-9a-zA-Z]{1,})?$", // TODO: interface not supported
+			"");
+	};
+
+	/**
+		Returns the IP address representing all hosts for the given IP family.
+
+		- For IPv4, the address is `0.0.0.0`.
+		- For IPv6, the address is `::`.
+	**/
+	public static function all(family:IpFamily):Address {
+		return (switch (family) {
+			case Ipv4: Ipv4(0);
+			case Ipv6: Ipv6(Bytes.ofHex("00000000000000000000000000000000"));
+		});
+	}
+
+	/**
+		Returns the IP address representing the local hosts for the given IP family.
+
+		- For IPv4, the address is `127.0.0.1`.
+		- For IPv6, the address is `::1`.
+	**/
+	public static function localhost(family:IpFamily):Address {
+		return (switch (family) {
+			case Ipv4: Ipv4(0x7F000001);
+			case Ipv6: Ipv6(Bytes.ofHex("00000000000000000000000000000001"));
+		});
+	}
+
+	/**
+		Converts an `Address` to a `String`.
+
+		- IPv4 addresses are represented with the dotted quad format, e.g.
+			`192.168.0.1`.
+		- IPv6 addresses are represented with the standard lowercased hexadecimal
+			representation, with `::` used to mark a long stretch of zeros.
+	**/
+	public static function toString(address:Address):String {
+		return (switch (address) {
+			case Ipv4(ip):
+				'${ip >>> 24}.${(ip >> 16) & 0xFF}.${(ip >> 8) & 0xFF}.${ip & 0xFF}';
+			case Ipv6(ip):
+				var groups = [for (i in 0...8) (ip.get(i * 2) << 8) | ip.get(i * 2 + 1)];
+				var longestRun = -1;
+				var longestPos = -1;
+				for (i in 0...8) {
+					if (groups[i] != 0)
+						continue;
+					var run = 1;
+					// TODO: skip if the longest run cannot be beaten
+					for (j in i + 1...8) {
+						if (groups[j] != 0)
+							break;
+						run++;
+					}
+					if (run > longestRun) {
+						longestRun = run;
+						longestPos = i;
+					}
+				}
+				inline function hex(groups:Array<Int>):String {
+					return groups.map(value -> StringTools.hex(value, 1).toLowerCase()).join(":");
+				}
+				if (longestRun > 1) {
+					hex(groups.slice(0, longestPos)) + "::" + hex(groups.slice(longestPos + longestRun));
+				} else {
+					hex(groups);
+				}
+		});
+	}
+
+	/**
+		Returns `true` if `address` represents a valid IPv4 or IPv6 address.
+	**/
+	public static function isIp(address:String):Bool {
+		return isIpv4(address) || isIpv6(address);
+	}
+
+	/**
+		Returns `true` if `address` represents a valid IPv4 address.
+	**/
+	public static function isIpv4(address:String):Bool {
+		return v4re.match(address);
+	}
+
+	/**
+		Returns `true` if `address` represents a valid IPv6 address.
+	**/
+	public static function isIpv6(address:String):Bool {
+		return v6re.match(address);
+	}
+
+	/**
+		Tries to convert the `String` `address` to an `Address` instance. Returns
+		the parsed `Address` or `null` if `address` does not represent a valid IP
+		address.
+	**/
+	public static function toIp(address:String):Null<Address> {
+		var ipv4 = toIpv4(address);
+		return ipv4 != null ? ipv4 : toIpv6(address);
+	}
+
+	/**
+		Tries to convert the `String` `address` to an IPv4 `Address` instance.
+		Returns the parsed `Address` or `null` if `address` does not represent a
+		valid IPv4 address.
+	**/
+	public static function toIpv4(address:String):Null<Address> {
+		if (!isIpv4(address))
+			return null;
+		var components = address.split(".").map(Std.parseInt);
+		return Ipv4((components[0] << 24) | (components[1] << 16) | (components[2] << 8) | components[3]);
+	}
+
+	/**
+		Tries to convert the `String` `address` to an IPv6 `Address` instance.
+		Returns the parsed `Address` or `null` if `address` does not represent a
+		valid IPv6 address.
+	**/
+	public static function toIpv6(address:String):Null<Address> {
+		if (!isIpv6(address))
+			return null;
+		var buffer = Bytes.alloc(16);
+		buffer.fill(0, 16, 0);
+		function parse(component:String, res:Int):Void {
+			var value = Std.parseInt('0x0$component');
+			buffer.set(res, value >> 8);
+			buffer.set(res + 1, value & 0xFF);
+		}
+		var stretch = address.split("::");
+		var components = stretch[0].split(":");
+		for (i in 0...components.length)
+			parse(components[i], i * 2);
+		if (stretch.length > 1) {
+			var end = 16;
+			components = stretch[1].split(":");
+			if (isIpv4(components[components.length - 1])) {
+				end -= 4;
+				var ip = components.pop().split(".").map(Std.parseInt);
+				for (i in 0...4)
+					buffer.set(end + i, ip[i]);
+			}
+			end -= components.length * 2;
+			for (i in 0...components.length)
+				parse(components[i], end + i);
+		}
+		return Ipv6(buffer);
+	}
+
+	/**
+		Returns the IPv6 version of the given `address`. IPv6 addresses are
+		returned unmodified, IPv4 addresses are mapped to IPv6 using the
+		`:ffff:0:0/96` IPv4 transition prefix.
+
+		```haxe
+		"127.0.0.1".toIpv4().mapToIpv6().toString(); // ::ffff:7f00:1
+		```
+	**/
+	public static function mapToIpv6(address:Address):Address {
+		return (switch (address) {
+			case Ipv4(ip):
+				var buffer = Bytes.alloc(16);
+				buffer.set(10, 0xFF);
+				buffer.set(11, 0xFF);
+				buffer.set(12, ip >>> 24);
+				buffer.set(13, (ip >> 16) & 0xFF);
+				buffer.set(14, (ip >> 8) & 0xFF);
+				buffer.set(15, ip & 0xFF);
+				Ipv6(buffer);
+			case _:
+				address;
+		});
+	}
+
+	/**
+		Returns `true` if `a` and `b` are the same IP address.
+
+		If `ipv6mapped` is `true`, bot `a` and `b` are mapped to IPv6 (using
+		`mapToIpv6`) before the comparison.
+	**/
+	public static function equals(a:Address, b:Address, ?ipv6mapped:Bool = false):Bool {
+		if (ipv6mapped) {
+			return (switch [mapToIpv6(a), mapToIpv6(b)] {
+				case [Ipv6(a), Ipv6(b)]: a.compare(b) == 0;
+				case _: false; // cannot happen?
+			});
+		}
+		return (switch [a, b] {
+			case [Ipv4(a), Ipv4(b)]: a == b;
+			case [Ipv6(a), Ipv6(b)]: a.compare(b) == 0;
+			case _: false;
+		});
+	}
+}

+ 24 - 0
std/asys/net/Dns.hx

@@ -0,0 +1,24 @@
+package asys.net;
+
+import haxe.async.*;
+
+/**
+	Asynchronous Domain Name System (DNS) methods.
+**/
+extern class Dns {
+	/**
+		Looks up the given `hostname`. `callback` will be called once the operation
+		completes. In case of success, the data given to callback is an array of
+		`asys.net.Address` instances representing all the IP addresses found
+		associated with the hostname.
+
+		- `lookupOptions.family` - if not `null`, only addresses of the given IP
+			family will be returned.
+	**/
+	static function lookup(hostname:String, ?lookupOptions:DnsLookupOptions, callback:Callback<Array<Address>>):Void;
+
+	/**
+		Looks up a reverse DNS entry for the given `ip`.
+	**/
+	static function reverse(ip:Address, callback:Callback<Array<String>>):Void;
+}

+ 16 - 0
std/asys/net/DnsLookupOptions.hx

@@ -0,0 +1,16 @@
+package asys.net;
+
+typedef DnsLookupOptions = {
+	?family:IpFamily,
+	?hints:DnsHints
+};
+
+enum abstract DnsHints(Int) from Int {
+	var AddrConfig = 1 << 0;
+	var V4Mapped = 1 << 1;
+
+	inline function get_raw():Int return this;
+
+	@:op(A | B)
+	inline function join(other:DnsHints):DnsHints return this | other.get_raw();
+}

+ 9 - 0
std/asys/net/IpFamily.hx

@@ -0,0 +1,9 @@
+package asys.net;
+
+/**
+	Represents a family of the Internet Protocol (IP).
+**/
+enum IpFamily {
+	Ipv4;
+	Ipv6;
+}

+ 206 - 0
std/asys/net/Server.hx

@@ -0,0 +1,206 @@
+package asys.net;
+
+import haxe.Error;
+import haxe.NoData;
+import haxe.async.*;
+
+typedef ServerOptions = {
+	?allowHalfOpen:Bool,
+	?pauseOnConnect:Bool
+};
+
+typedef ServerListenTcpOptions = {
+	?port:Int,
+	?host:String,
+	?address:Address,
+	?backlog:Int,
+	?exclusive:Bool,
+	?ipv6only:Bool
+};
+
+typedef ServerListenIpcOptions = {
+	path:String,
+	?backlog:Int,
+	?exclusive:Bool,
+	?readableAll:Bool,
+	?writableAll:Bool
+};
+
+private typedef NativeStream =
+	#if doc_gen
+	Void;
+	#elseif eval
+	eval.uv.Stream;
+	#elseif hl
+	hl.uv.Stream;
+	#elseif neko
+	neko.uv.Stream;
+	#else
+	#error "socket not supported on this platform"
+	#end
+
+private typedef NativeSocket =
+	#if doc_gen
+	Void;
+	#elseif eval
+	eval.uv.Socket;
+	#elseif hl
+	hl.uv.Socket;
+	#elseif neko
+	neko.uv.Socket;
+	#else
+	#error "socket not supported on this platform"
+	#end
+
+private typedef NativePipe =
+	#if doc_gen
+	Void;
+	#elseif eval
+	eval.uv.Pipe;
+	#elseif hl
+	hl.uv.Pipe;
+	#elseif neko
+	neko.uv.Pipe;
+	#else
+	#error "socket not supported on this platform"
+	#end
+
+class Server {
+	public final closeSignal:Signal<NoData> = new ArraySignal<NoData>();
+	public final connectionSignal:Signal<Socket> = new ArraySignal<Socket>();
+	public final errorSignal:Signal<Error> = new ArraySignal<Error>();
+	public final listeningSignal:Signal<NoData> = new ArraySignal<NoData>();
+
+	public var listening(default, null):Bool;
+	public var maxConnections:Int; // TODO
+
+	function get_localAddress():Null<SocketAddress> {
+		if (!listening)
+			return null;
+		return nativeSocket.getSockName();
+	}
+
+	public var localAddress(get, never):Null<SocketAddress>;
+
+	public function new(?options:ServerOptions) {}
+
+	// function address():SocketAddress;
+
+	public function close(?callback:Callback<NoData>):Void {
+		native.close(Callback.nonNull(callback));
+	}
+
+	// function getConnections(callback:Callback<Int>):Void;
+	// function listenSocket(socket:Socket, ?backlog:Int, ?listener:Listener<NoData>):Void;
+	// function listenServer(server:Server, ?backlog:Int, ?listener:Listener<NoData>):Void;
+	// function listenFile(file:sys.io.File, ?backlog:Int, ?listener:Listener<NoData>):Void;
+	public function listenIpc(options:ServerListenIpcOptions, ?listener:Listener<Socket>):Void {
+		if (listening || listenDefer != null)
+			throw "already listening";
+		if (listener != null)
+			connectionSignal.on(listener);
+
+		nativePipe = new NativePipe(false);
+		native = nativePipe.asStream();
+
+		listening = true;
+		try {
+			// TODO: probably prepend "\\?\pipe\" to the path on Windows
+			nativePipe.bindIpc(options.path);
+			native.listen(options.backlog == null ? 511 : options.backlog, (err) -> {
+				if (err != null)
+					return errorSignal.emit(err);
+				try {
+					var client = @:privateAccess new Socket();
+					@:privateAccess client.nativePipe = nativePipe.accept();
+					@:privateAccess client.native = @:privateAccess client.nativePipe.asStream();
+					@:privateAccess client.connected = true;
+					@:privateAccess client.serverSpawn = true;
+					connectionSignal.emit(client);
+				} catch (e:haxe.Error) {
+					errorSignal.emit(e);
+				}
+			});
+			listeningSignal.emit(new NoData());
+		} catch (e:haxe.Error) {
+			errorSignal.emit(e);
+		}
+	}
+
+	public function listenTcp(options:ServerListenTcpOptions, ?listener:Listener<Socket>):Void {
+		if (listening || listenDefer != null)
+			throw "already listening";
+		if (listener != null)
+			connectionSignal.on(listener);
+
+		if (options.host != null && options.address != null)
+			throw "cannot specify both host and address";
+
+		nativeSocket = new NativeSocket();
+		native = nativeSocket.asStream();
+
+		// take a copy since we reuse the object asynchronously
+		var options = {
+			port: options.port,
+			host: options.host,
+			address: options.address,
+			backlog: options.backlog,
+			exclusive: options.exclusive,
+			ipv6only: options.ipv6only
+		};
+
+		function listen(address:Address):Void {
+			listenDefer = null;
+			listening = true;
+			if (options.ipv6only == null)
+				options.ipv6only = false;
+			try {
+				nativeSocket.bindTcp(address, options.port == null ? 0 : options.port, options.ipv6only);
+				native.listen(options.backlog == null ? 511 : options.backlog, (err) -> {
+					if (err != null)
+						return errorSignal.emit(err);
+					try {
+						var client = @:privateAccess new Socket();
+						@:privateAccess client.nativeSocket = nativeSocket.accept();
+						@:privateAccess client.native = @:privateAccess client.nativeSocket.asStream();
+						@:privateAccess client.connected = true;
+						@:privateAccess client.serverSpawn = true;
+						connectionSignal.emit(client);
+					} catch (e:haxe.Error) {
+						errorSignal.emit(e);
+					}
+				});
+				listeningSignal.emit(new NoData());
+			} catch (e:haxe.Error) {
+				errorSignal.emit(e);
+			}
+		}
+
+		if (options.address != null) {
+			listenDefer = Defer.nextTick(() -> listen(options.address));
+			return;
+		}
+		if (options.host == null)
+			options.host = "";
+		Dns.lookup(options.host, null, (err, entries) -> {
+			if (err != null)
+				return errorSignal.emit(err);
+			if (entries.length == 0)
+				throw "!";
+			listen(entries[0]);
+		});
+	}
+
+	public function ref():Void {
+		native.ref();
+	}
+
+	public function unref():Void {
+		native.unref();
+	}
+
+	var native:NativeStream;
+	var nativeSocket:NativeSocket;
+	var nativePipe:NativePipe;
+	var listenDefer:asys.Timer;
+}

+ 477 - 0
std/asys/net/Socket.hx

@@ -0,0 +1,477 @@
+package asys.net;
+
+import haxe.Error;
+import haxe.NoData;
+import haxe.async.*;
+import haxe.io.*;
+import haxe.io.Readable.ReadResult;
+import asys.io.*;
+import asys.net.SocketOptions.SocketConnectTcpOptions;
+import asys.net.SocketOptions.SocketConnectIpcOptions;
+
+private typedef NativeStream =
+	#if doc_gen
+	Void;
+	#elseif eval
+	eval.uv.Stream;
+	#elseif hl
+	hl.uv.Stream;
+	#elseif neko
+	neko.uv.Stream;
+	#else
+	#error "socket not supported on this platform"
+	#end
+
+private typedef NativeSocket =
+	#if doc_gen
+	Void;
+	#elseif eval
+	eval.uv.Socket;
+	#elseif hl
+	hl.uv.Socket;
+	#elseif neko
+	neko.uv.Socket;
+	#else
+	#error "socket not supported on this platform"
+	#end
+
+private typedef NativePipe =
+	#if doc_gen
+	Void;
+	#elseif eval
+	eval.uv.Pipe;
+	#elseif hl
+	hl.uv.Pipe;
+	#elseif neko
+	neko.uv.Pipe;
+	#else
+	#error "socket not supported on this platform"
+	#end
+
+/**
+	Socket object, used for clients and servers for TCP communications and IPC
+	(inter-process communications) over Windows named pipes and Unix local domain
+	sockets.
+
+	An IPC pipe is a communication channel between two processes. It may be
+	uni-directional or bi-directional, depending on how it is created. Pipes can
+	be automatically created for spawned subprocesses with `Process.spawn`.
+**/
+class Socket extends Duplex {
+	/**
+		Creates an unconnected socket or pipe instance.
+
+		@param options.allowHalfOpen
+		@param options.readable Whether the socket should be readable to the
+			current process.
+		@param options.writable Whether the socket should be writable to the
+			current process.
+	**/
+	public static function create(?options:SocketOptions):Socket {
+		// TODO: use options
+		return new Socket();
+	}
+
+	/**
+		Emitted when the socket connects to a remote endpoint.
+	**/
+	public final closeSignal:Signal<NoData> = new ArraySignal();
+
+	public final connectSignal:Signal<NoData> = new ArraySignal();
+
+	// endSignal
+
+	/**
+		(TCP only.) Emitted after the IP address of the hostname given in
+		`connectTcp` is resolved, but before the socket connects.
+	**/
+	public final lookupSignal:Signal<Address> = new ArraySignal();
+
+	/**
+		Emitted when a timeout occurs. See `setTimeout`.
+	**/
+	public final timeoutSignal:Signal<NoData> = new ArraySignal();
+
+	private function get_localAddress():Null<SocketAddress> {
+		if (nativeSocket != null)
+			return nativeSocket.getSockName();
+		if (nativePipe != null)
+			return nativePipe.getSockName();
+		return null;
+	}
+
+	/**
+		The address of the local side of the socket connection, or `null` if not
+		connected.
+	**/
+	public var localAddress(get, never):Null<SocketAddress>;
+
+	private function get_remoteAddress():Null<SocketAddress> {
+		if (nativeSocket != null)
+			return nativeSocket.getPeerName();
+		if (nativePipe != null)
+			return nativePipe.getPeerName();
+		return null;
+	}
+
+	/**
+		The address of the remote side of the socket connection, or `null` if not
+		connected.
+	**/
+	public var remoteAddress(get, never):Null<SocketAddress>;
+
+	private function get_handlesPending():Int {
+		if (nativePipe == null)
+			throw "not connected via IPC";
+		return nativePipe.pendingCount();
+	}
+
+	/**
+		(IPC only.) Number of pending sockets or pipes. Accessible using
+		`readHandle`.
+	**/
+	public var handlesPending(get, never):Int;
+
+	/**
+		`true` when `this` socket is connected to a remote host or an IPC pipe.
+	**/
+	public var connected(default, null):Bool = false;
+
+	/**
+		Connect `this` socket via TCP to the given remote.
+
+		If neither `options.host` nor `options.address` is specified, the host
+		`localhost` is resolved via DNS and used as the address. At least one of
+		`options.host` or `options.address` must be `null`.
+
+		`options.localAddress` and `options.localPort` can be used to specify what
+		address and port to use on the local machine for the outgoing connection.
+		If `null` or not specified, an address and/or a port will be chosen
+		automatically by the system when connecting. The local address and port can
+		be obtained using the `localAddress`.
+
+		@param options.port Remote port to connect to.
+		@param options.host Hostname to connect to, will be resolved using
+			`Dns.resolve` to an address. `lookupSignal` will be emitted with the
+			resolved address before the connection is attempted.
+		@param options.address IPv4 or IPv6 address to connect to.
+		@param options.localAddress Local IPv4 or IPv6 address to connect from.
+		@param options.localPort Local port to connect from.
+		@param options.family Limit DNS lookup to the given family.
+	**/
+	public function connectTcp(options:SocketConnectTcpOptions, ?cb:Callback<NoData>):Void {
+		if (connectStarted || connected)
+			throw "already connected";
+
+		if (options.host != null && options.address != null)
+			throw "cannot specify both host and address";
+
+		connectStarted = true;
+		nativeSocket = new NativeSocket();
+		native = nativeSocket.asStream();
+
+		// take a copy since we reuse the object asynchronously
+		var options = {
+			port: options.port,
+			host: options.host,
+			address: options.address,
+			localAddress: options.localAddress,
+			localPort: options.localPort,
+			family: options.family
+		};
+
+		function connect(address:Address):Void {
+			connectDefer = null;
+			// TODO: bindTcp for localAddress and localPort, if specified
+			try {
+				nativeSocket.connectTcp(address, options.port, (err, nd) -> {
+					timeoutReset();
+					if (err == null)
+						connected = true;
+					if (cb != null)
+						cb(err, nd);
+					if (err == null)
+						connectSignal.emit(new NoData());
+				});
+			} catch (err:haxe.Error) {
+				if (cb != null)
+					cb(err, new NoData());
+			}
+		}
+
+		if (options.address != null) {
+			connectDefer = Defer.nextTick(() -> connect(options.address));
+			return;
+		}
+		if (options.host == null)
+			options.host = "localhost";
+		Dns.lookup(options.host, {family: options.family}, (err, entries) -> {
+			timeoutReset();
+			if (err != null)
+				return errorSignal.emit(err);
+			if (entries.length == 0)
+				throw "!";
+			lookupSignal.emit(entries[0]);
+			connect(entries[0]);
+		});
+	}
+
+	/**
+		Connect `this` socket to an IPC pipe.
+
+		@param options.path Pipe path.
+	**/
+	public function connectIpc(options:SocketConnectIpcOptions, ?cb:Callback<NoData>):Void {
+		if (connectStarted || connected)
+			throw "already connected";
+
+		connectStarted = true;
+		nativePipe = new NativePipe(false);
+		native = nativePipe.asStream();
+
+		try {
+			nativePipe.connectIpc(options.path, (err, nd) -> {
+				timeoutReset();
+				if (err == null)
+					connected = true;
+				if (cb != null)
+					cb(err, nd);
+				if (err == null)
+					connectSignal.emit(new NoData());
+			});
+		} catch (err:haxe.Error) {
+			if (cb != null)
+				cb(err, new NoData());
+		}
+	}
+
+	/**
+		Connect `this` socket to a file descriptor. Used internally to establish
+		IPC channels between Haxe processes.
+
+		@param ipc Whether IPC features (sending sockets) should be enabled.
+	**/
+	public function connectFd(ipc:Bool, fd:Int):Void {
+		if (connectStarted || connected)
+			throw "already connected";
+
+		connectStarted = true;
+		nativePipe = new NativePipe(ipc);
+		nativePipe.open(fd);
+		connected = true;
+		native = nativePipe.asStream();
+
+		// TODO: signal consistency with other connect methods
+	}
+
+	/**
+		Closes `this` socket and all underlying resources.
+	**/
+	public function destroy(?cb:Callback<NoData>):Void {
+		if (readStarted)
+			native.stopRead();
+		native.close((err, nd) -> {
+			if (err != null)
+				errorSignal.emit(err);
+			if (cb != null)
+				cb(err, nd);
+			closeSignal.emit(new NoData());
+		});
+	}
+
+	/**
+		(TCP only.) Enable or disable TCP keep-alive.
+
+		@param initialDelay Initial delay in seconds. Ignored if `enable` is
+			`false`.
+	**/
+	public function setKeepAlive(?enable:Bool = false, ?initialDelay:Int = 0):Void {
+		if (nativeSocket == null)
+			throw "not connected via TCP";
+		nativeSocket.setKeepAlive(enable, initialDelay);
+	}
+
+	/**
+		(TCP only.) Enable or disable TCP no-delay. Enabling no-delay disables
+		Nagle's algorithm.
+	**/
+	public function setNoDelay(?noDelay:Bool = true):Void {
+		if (nativeSocket == null)
+			throw "not connected via TCP";
+		nativeSocket.setNoDelay(noDelay);
+	}
+
+	/**
+		Set a timeout for socket oprations. Any time activity is detected on the
+		socket (see below), the timer is reset to `timeout`. When the timer runs
+		out, `timeoutSignal` is emitted. Note that a timeout will not automatically
+		do anything to the socket - it is up to the `timeoutSignal` handler to
+		perform an action, e.g. ping the remote host or close the socket.
+
+		Socket activity which resets the timer:
+
+		- A chunk of data is received.
+		- An error occurs during reading.
+		- A chunk of data is written to the socket.
+		- Connection is established.
+		- (TCP only.) DNS lookup is finished (successfully or not).
+
+		@param timeout Timeout in seconds, or `0` to disable.
+	**/
+	public function setTimeout(timeout:Int, ?listener:Listener<NoData>):Void {
+		timeoutTime = timeout;
+		timeoutReset();
+		if (listener != null)
+			timeoutSignal.once(listener);
+	}
+
+	/**
+		(IPC only.) Send a socket or pipe in along with the given `data`. The
+		socket must be connected.
+	**/
+	public function writeHandle(data:Bytes, handle:Socket):Void {
+		if (nativePipe == null)
+			throw "not connected via IPC";
+		nativePipe.writeHandle(data, handle.native, writeDone);
+	}
+
+	/**
+		(IPC only.) Receive a socket or pipe. Should only be called when
+		`handlesPending` is greater than zero.
+	**/
+	public function readHandle():Socket {
+		if (nativePipe == null)
+			throw "not connected via IPC";
+		var ret = new Socket();
+		switch (nativePipe.acceptPending()) {
+			case Socket(nativeSocket):
+				ret.nativeSocket = nativeSocket;
+				ret.native = nativeSocket.asStream();
+			case Pipe(nativePipe):
+				ret.nativePipe = nativePipe;
+				ret.native = nativePipe.asStream();
+		}
+		ret.connected = true;
+		return ret;
+	}
+
+	public function ref():Void {
+		if (native == null)
+			throw "not connected";
+		native.ref();
+	}
+
+	public function unref():Void {
+		if (native == null)
+			throw "not connected";
+		native.unref();
+	}
+
+	var connectDefer:asys.Timer;
+	var native:NativeStream;
+	var nativeSocket:NativeSocket;
+	var nativePipe:NativePipe;
+	var internalReadCalled = false;
+	var readStarted = false;
+	var connectStarted = false;
+	var serverSpawn:Bool = false;
+	var timeoutTime:Int = 0;
+	var timeoutTimer:asys.Timer;
+
+	function new() {
+		super();
+	}
+
+	function initPipe(ipc:Bool):Void {
+		nativePipe = new NativePipe(ipc);
+		native = nativePipe.asStream();
+		connected = true;
+	}
+
+	override function internalRead(remaining):ReadResult {
+		if (internalReadCalled)
+			return None;
+		internalReadCalled = true;
+
+		function start():Void {
+			readStarted = true;
+			native.startRead((err, chunk) -> {
+				timeoutReset();
+				if (err != null) {
+					switch (err.type) {
+						case UVError(EOF):
+							asyncRead([], true);
+						case _:
+							errorSignal.emit(err);
+					}
+				} else {
+					asyncRead([chunk], false);
+				}
+			});
+		}
+
+		if (connected)
+			start();
+		else
+			connectSignal.once(start);
+
+		return None;
+	}
+
+	// TODO: keep track of pending writes for finish event emission
+	// in `internalWrite` and `writeHandle`
+	function writeDone(err:Error, nd:NoData):Void {
+		timeoutReset();
+		if (err != null)
+			errorSignal.emit(err);
+		// TODO: destroy stream and socket
+	}
+
+	override function internalWrite():Void {
+		while (inputBuffer.length > 0) {
+			native.write(pop(), writeDone);
+		}
+	}
+
+	function timeoutTrigger():Void {
+		timeoutTimer = null;
+		timeoutSignal.emit(new NoData());
+	}
+
+	function timeoutReset():Void {
+		if (timeoutTimer != null)
+			timeoutTimer.stop();
+		timeoutTimer = null;
+		if (timeoutTime != 0) {
+			timeoutTimer = asys.Timer.delay(timeoutTrigger, timeoutTime);
+			timeoutTimer.unref();
+		}
+	}
+
+	/*
+	// TODO: #8263 (static hxUnserialize)
+	// Automatic un/serialisation will not work here since hxUnserialize needs to
+	// call super, otherwise the socket is unusable; for now sockets are
+	// delivered separately in IPC.
+
+	@:access(asys.io.IpcSerializer)
+	private function hxSerialize(_):Void {
+		if (IpcSerializer.activeSerializer == null)
+			throw "cannot serialize socket";
+		IpcSerializer.activeSerializer.chunkSockets.push(this);
+	}
+
+	@:access(asys.io.IpcUnserializer)
+	private function hxUnserialize(_):Void {
+		if (IpcUnserializer.activeUnserializer == null)
+			throw "cannot unserialize socket";
+		trace(dataSignal, input);
+		var source:Socket = IpcUnserializer.activeUnserializer.chunkSockets.shift();
+		this.native = source.native;
+		this.nativePipe = source.nativePipe;
+		this.nativeSocket = source.nativeSocket;
+		this.connected = true;
+		trace("successfully unserialized", this.nativeSocket);
+	}
+	*/
+}

+ 15 - 0
std/asys/net/SocketAddress.hx

@@ -0,0 +1,15 @@
+package asys.net;
+
+/**
+	Reperesents the address of a connected or bound `Socket` object.
+**/
+enum SocketAddress {
+	/**
+		Address of a socket connected or bound to an IPv4 or IPv6 address and port.
+	**/
+	Network(address:Address, port:Int);
+	/**
+		Filepath of a IPC pipe (Windows named pipe or Unix local domain socket).
+	**/
+	Unix(path:String);
+}

+ 41 - 0
std/asys/net/SocketOptions.hx

@@ -0,0 +1,41 @@
+package asys.net;
+
+/**
+	See `Socket.create`.
+**/
+typedef SocketOptions = {
+	// ?file:asys.io.File, // fd in Node
+	?allowHalfOpen:Bool,
+	?readable:Bool,
+	?writable:Bool
+};
+
+/**
+	See `Socket.connectTcp`.
+**/
+typedef SocketConnectTcpOptions = {
+	port:Int,
+	?host:String,
+	?address:Address,
+	?localAddress:Address,
+	?localPort:Int,
+	?family:IpFamily
+};
+
+/**
+	See `Socket.connectIpc`.
+**/
+typedef SocketConnectIpcOptions = {
+	path:String
+};
+
+/**
+	See `UdpSocket.create`.
+**/
+typedef UdpSocketOptions = {
+	?reuseAddr:Bool,
+	?ipv6Only:Bool,
+	?recvBufferSize:Int,
+	?sendBufferSize:Int,
+	// ?lookup:DnsLookupFunction
+};

+ 249 - 0
std/asys/net/UdpSocket.hx

@@ -0,0 +1,249 @@
+package asys.net;
+
+import haxe.Error;
+import haxe.NoData;
+import haxe.async.*;
+import haxe.io.Bytes;
+import asys.net.SocketOptions.UdpSocketOptions;
+
+private typedef Native =
+	#if doc_gen
+	Void;
+	#elseif eval
+	eval.uv.UdpSocket;
+	#elseif hl
+	hl.uv.UdpSocket;
+	#elseif neko
+	neko.uv.UdpSocket;
+	#else
+	#error "UDP socket not supported on this platform"
+	#end
+
+class UdpSocket {
+	public static function create(type:IpFamily, ?options:UdpSocketOptions, ?listener:Listener<UdpMessage>):UdpSocket {
+		var res = new UdpSocket(type);
+		// TODO: use other options, register listener
+		if (options == null)
+			options = {};
+		if (options.recvBufferSize != null)
+			res.recvBufferSize = options.recvBufferSize;
+		if (options.sendBufferSize != null)
+			res.sendBufferSize = options.sendBufferSize;
+		return res;
+	}
+
+	public final type:IpFamily;
+
+	/**
+		Remote address and port that `this` socket is connected to. See `connect`.
+	**/
+	public var remoteAddress(default, null):Null<SocketAddress>;
+
+	private function get_localAddress():Null<SocketAddress> {
+		return try native.getSockName() catch (e:Dynamic) null;
+	}
+
+	public var localAddress(get, never):Null<SocketAddress>;
+
+	private function get_recvBufferSize():Int {
+		return native.getRecvBufferSize();
+	}
+
+	private function set_recvBufferSize(size:Int):Int {
+		return native.setRecvBufferSize(size);
+	}
+
+	public var recvBufferSize(get, set):Int;
+
+	private function get_sendBufferSize():Int {
+		return native.getSendBufferSize();
+	}
+
+	private function set_sendBufferSize(size:Int):Int {
+		return native.setSendBufferSize(size);
+	}
+
+	public var sendBufferSize(get, set):Int;
+
+	// final closeSignal:Signal<NoData>;
+	// final connectSignal:Signal<NoData>;
+	// final listeningSignal:Signal<NoData>;
+
+	public final errorSignal:Signal<Error> = new ArraySignal();
+
+	/**
+		Emitted when a message is received by `this` socket. See `UdpMessage`.
+	**/
+	public final messageSignal:Signal<UdpMessage> = new ArraySignal();
+
+	/**
+		Joins the given multicast group.
+	**/
+	public function addMembership(multicastAddress:String, ?multicastInterface:String):Void {
+		if (multicastInterface == null)
+			multicastInterface = "";
+		native.addMembership(multicastAddress, multicastInterface);
+	}
+
+	/**
+		Leaves the given multicast group.
+	**/
+	public function dropMembership(multicastAddress:String, ?multicastInterface:String):Void {
+		if (multicastInterface == null)
+			multicastInterface = "";
+		native.dropMembership(multicastAddress, multicastInterface);
+	}
+
+	/**
+		Binds `this` socket to a local address and port. Packets sent to the bound
+		address will arrive via `messageSignal`. Outgoing packets will be sent from
+		the given address and port. If any packet is sent without calling `bind`
+		first, an address and port is chosen automatically by the system - it can
+		be obtained with `localAddress`.
+	**/
+	public function bind(?address:Address, ?port:Int):Void {
+		if (address == null)
+			address = AddressTools.all(type);
+		if (port == null)
+			port = 0;
+		native.bindTcp(address, port, false);
+		native.startRead((err, msg) -> {
+			if (err != null)
+				return errorSignal.emit(err);
+			messageSignal.emit(msg);
+		});
+	}
+
+	/**
+		Closes `this` socket and all underlying resources.
+	**/
+	public function close(?cb:Callback<NoData>):Void {
+		native.stopRead();
+		native.close(Callback.nonNull(cb));
+	}
+
+	/**
+		Connects `this` socket to a remote address and port. Any `send` calls after
+		`connect` is called must not specify `address` nor `port`, they will
+		automatically use the ones specified in the `connect` call.
+	**/
+	public function connect(?address:Address, port:Int):Void {
+		if (remoteAddress != null)
+			throw "already connected";
+		if (address == null)
+			address = AddressTools.localhost(type);
+		remoteAddress = Network(address, port);
+	}
+
+	/**
+		Clears any remote address and port previously set with `connect`.
+	**/
+	public function disconnect():Void {
+		if (remoteAddress == null)
+			throw "not connected";
+		remoteAddress = null;
+	}
+
+	/**
+		Sends a message.
+
+		@param msg Buffer from which to read the message data.
+		@param offset Position in `msg` at which to start reading.
+		@param length Length of message in bytes.
+		@param address Address to send the message to. Must be `null` if `this`
+			socket is connected.
+		@param port Port to send the message to. Must be `null` if `this` socket is
+			connected.
+	**/
+	public function send(msg:Bytes, offset:Int, length:Int, ?address:Address, ?port:Int, ?cb:Callback<NoData>):Void {
+		if (address == null && port == null) {
+			if (remoteAddress == null)
+				throw "not connected";
+		} else if (address != null && port != null) {
+			if (remoteAddress != null)
+				throw "already connected";
+		} else
+			throw "invalid arguments";
+		if (address == null) {
+			switch (remoteAddress) {
+				case Network(a, p):
+					address = a;
+					port = p;
+				case _:
+					throw "!";
+			}
+		}
+		native.send(msg, offset, length, address, port, cb);
+	}
+
+	/**
+		Sets broadcast on or off.
+	**/
+	public function setBroadcast(flag:Bool):Void {
+		native.setBroadcast(flag);
+	}
+
+	/**
+		Sets the multicast interface on which to send and receive data.
+	**/
+	public function setMulticastInterface(multicastInterface:String):Void {
+		native.setMulticastInterface(multicastInterface);
+	}
+
+	/**
+		Set IP multicast loopback on or off. Makes multicast packets loop back to
+		local sockets.
+	**/
+	public function setMulticastLoopback(flag:Bool):Void {
+		native.setMulticastLoopback(flag);
+	}
+
+	/**
+		Sets the multicast TTL (time-to-live).
+	**/
+	public function setMulticastTTL(ttl:Int):Void {
+		native.setMulticastTTL(ttl);
+	}
+
+	/**
+		Sets the TTL (time-to-live) for outgoing packets.
+
+		@param ttl Number of hops.
+	**/
+	public function setTTL(ttl:Int):Void {
+		native.setTTL(ttl);
+	}
+
+	public function ref():Void {
+		native.asStream().ref();
+	}
+
+	public function unref():Void {
+		native.asStream().unref();
+	}
+
+	var native:Native;
+
+	function new(type) {
+		native = new Native();
+		this.type = type;
+	}
+}
+
+/**
+	A packet received emitted by `messageSignal` of a `UdpSocket`.
+**/
+typedef UdpMessage = {
+	/**
+		Message data.
+	**/
+	var data:Bytes;
+	/**
+		Remote IPv4 or IPv6 address from which the message originates.
+	**/
+	var remoteAddress:Address;
+	/**
+		Remote port from which the message originates.
+	**/
+	var remotePort:Int;
+};

+ 14 - 0
std/asys/uv/UVConstants.hx

@@ -0,0 +1,14 @@
+package asys.uv;
+
+class UVConstants {
+	public static inline final S_IFMT = 0xF000;
+	public static inline final S_PERM = 0x0FFF;
+
+	public static inline final S_IFBLK = 0x6000;
+	public static inline final S_IFCHR = 0x2000;
+	public static inline final S_IFDIR = 0x4000;
+	public static inline final S_IFIFO = 0x1000;
+	public static inline final S_IFLNK = 0xA000;
+	public static inline final S_IFREG = 0x8000;
+	public static inline final S_IFSOCK = 0xC000;
+}

+ 12 - 0
std/asys/uv/UVDirentType.hx

@@ -0,0 +1,12 @@
+package asys.uv;
+
+enum abstract UVDirentType(Int) {
+  var DirentUnknown = 0;
+  var DirentFile;
+  var DirentDir;
+  var DirentLink;
+  var DirentFifo;
+  var DirentSocket;
+  var DirentChar;
+  var DirentBlock;
+}

+ 81 - 0
std/asys/uv/UVErrorType.hx

@@ -0,0 +1,81 @@
+package asys.uv;
+
+enum abstract UVErrorType(Int) {
+	var E2BIG = -7; // "argument list too long"
+	var EACCES = -13; // "permission denied"
+	var EADDRINUSE = -48; // "address already in use"
+	var EADDRNOTAVAIL = -49; // "address not available"
+	var EAFNOSUPPORT = -47; // "address family not supported"
+	var EAGAIN = -35; // "resource temporarily unavailable"
+	var EAI_ADDRFAMILY = -3000; // "address family not supported"
+	var EAI_AGAIN = -3001; // "temporary failure"
+	var EAI_BADFLAGS = -3002; // "bad ai_flags value"
+	var EAI_BADHINTS = -3013; // "invalid value for hints"
+	var EAI_CANCELED = -3003; // "request canceled"
+	var EAI_FAIL = -3004; // "permanent failure"
+	var EAI_FAMILY = -3005; // "ai_family not supported"
+	var EAI_MEMORY = -3006; // "out of memory"
+	var EAI_NODATA = -3007; // "no address"
+	var EAI_NONAME = -3008; // "unknown node or service"
+	var EAI_OVERFLOW = -3009; // "argument buffer overflow"
+	var EAI_PROTOCOL = -3014; // "resolved protocol is unknown"
+	var EAI_SERVICE = -3010; // "service not available for socket type"
+	var EAI_SOCKTYPE = -3011; // "socket type not supported"
+	var EALREADY = -37; // "connection already in progress"
+	var EBADF = -9; // "bad file descriptor"
+	var EBUSY = -16; // "resource busy or locked"
+	var ECANCELED = -89; // "operation canceled"
+	var ECHARSET = -4080; // "invalid Unicode character"
+	var ECONNABORTED = -53; // "software caused connection abort"
+	var ECONNREFUSED = -61; // "connection refused"
+	var ECONNRESET = -54; // "connection reset by peer"
+	var EDESTADDRREQ = -39; // "destination address required"
+	var EEXIST = -17; // "file already exists"
+	var EFAULT = -14; // "bad address in system call argument"
+	var EFBIG = -27; // "file too large"
+	var EHOSTUNREACH = -65; // "host is unreachable"
+	var EINTR = -4; // "interrupted system call"
+	var EINVAL = -22; // "invalid argument"
+	var EIO = -5; // "i/o error"
+	var EISCONN = -56; // "socket is already connected"
+	var EISDIR = -21; // "illegal operation on a directory"
+	var ELOOP = -62; // "too many symbolic links encountered"
+	var EMFILE = -24; // "too many open files"
+	var EMSGSIZE = -40; // "message too long"
+	var ENAMETOOLONG = -63; // "name too long"
+	var ENETDOWN = -50; // "network is down"
+	var ENETUNREACH = -51; // "network is unreachable"
+	var ENFILE = -23; // "file table overflow"
+	var ENOBUFS = -55; // "no buffer space available"
+	var ENODEV = -19; // "no such device"
+	var ENOENT = -2; // "no such file or directory"
+	var ENOMEM = -12; // "not enough memory"
+	var ENONET = -4056; // "machine is not on the network"
+	var ENOPROTOOPT = -42; // "protocol not available"
+	var ENOSPC = -28; // "no space left on device"
+	var ENOSYS = -78; // "function not implemented"
+	var ENOTCONN = -57; // "socket is not connected"
+	var ENOTDIR = -20; // "not a directory"
+	var ENOTEMPTY = -66; // "directory not empty"
+	var ENOTSOCK = -38; // "socket operation on non-socket"
+	var ENOTSUP = -45; // "operation not supported on socket"
+	var EPERM = -1; // "operation not permitted"
+	var EPIPE = -32; // "broken pipe"
+	var EPROTO = -100; // "protocol error"
+	var EPROTONOSUPPORT = -43; // "protocol not supported"
+	var EPROTOTYPE = -41; // "protocol wrong type for socket"
+	var ERANGE = -34; // "result too large"
+	var EROFS = -30; // "read-only file system"
+	var ESHUTDOWN = -58; // "cannot send after transport endpoint shutdown"
+	var ESPIPE = -29; // "invalid seek"
+	var ESRCH = -3; // "no such process"
+	var ETIMEDOUT = -60; // "connection timed out"
+	var ETXTBSY = -26; // "text file is busy"
+	var EXDEV = -18; // "cross-device link not permitted"
+	var UNKNOWN = -4094; // "unknown error"
+	var EOF = -4095; // "end of file"
+	var ENXIO = -6; // "no such device or address"
+	var EMLINK = -31; // "too many links"
+	var EHOSTDOWN = -64; // "host is down"
+	var EOTHER = 0;
+}

+ 7 - 0
std/asys/uv/UVFsEventType.hx

@@ -0,0 +1,7 @@
+package asys.uv;
+
+enum abstract UVFsEventType(Int) {
+	var Rename = 1;
+	var Change = 2;
+	var RenameChange = 3;
+}

+ 18 - 0
std/asys/uv/UVProcessSpawnFlags.hx

@@ -0,0 +1,18 @@
+package asys.uv;
+
+enum abstract UVProcessSpawnFlags(Int) {
+	var None = 0;
+	var SetUid = 1 << 0;
+	var SetGid = 1 << 1;
+	var WindowsVerbatimArguments = 1 << 2;
+	var Detached = 1 << 3;
+	var WindowsHide = 1 << 4;
+
+	function new(raw:Int)
+		this = raw;
+
+	inline function get_raw():Int return this;
+
+	@:op(A | B)
+	inline function join(other:UVProcessSpawnFlags) return new UVProcessSpawnFlags(this | other.get_raw());
+}

+ 7 - 0
std/asys/uv/UVRunMode.hx

@@ -0,0 +1,7 @@
+package asys.uv;
+
+enum abstract UVRunMode(Int) {
+	var RunDefault = 0;
+	var RunOnce;
+	var RunNoWait;
+}

+ 50 - 0
std/asys/uv/UVStat.hx

@@ -0,0 +1,50 @@
+package asys.uv;
+
+class UVStat {
+	public final dev:Int;
+	public final mode:Int;
+	public final nlink:Int;
+	public final uid:Int;
+	public final gid:Int;
+	public final rdev:Int;
+	public final ino:Int;
+	public final size:Int;
+	public final blksize:Int;
+	public final blocks:Int;
+	public final flags:Int;
+	public final gen:Int;
+
+	public function new(st_dev:Int, st_mode:Int, st_nlink:Int, st_uid:Int, st_gid:Int, st_rdev:Int, st_ino:Int, st_size:Int, st_blksize:Int, st_blocks:Int,
+			st_flags:Int, st_gen:Int) {
+		dev = st_dev;
+		mode = st_mode;
+		nlink = st_nlink;
+		uid = st_uid;
+		gid = st_gid;
+		rdev = st_rdev;
+		ino = st_ino;
+		size = st_size;
+		blksize = st_blksize;
+		blocks = st_blocks;
+		flags = st_flags;
+		gen = st_gen;
+	}
+
+	public function isBlockDevice():Bool return (mode & asys.uv.UVConstants.S_IFMT) == asys.uv.UVConstants.S_IFBLK;
+
+	public function isCharacterDevice():Bool return (mode & asys.uv.UVConstants.S_IFMT) == asys.uv.UVConstants.S_IFCHR;
+
+	public function isDirectory():Bool return (mode & asys.uv.UVConstants.S_IFMT) == asys.uv.UVConstants.S_IFDIR;
+
+	public function isFIFO():Bool return (mode & asys.uv.UVConstants.S_IFMT) == asys.uv.UVConstants.S_IFIFO;
+
+	public function isFile():Bool return (mode & asys.uv.UVConstants.S_IFMT) == asys.uv.UVConstants.S_IFREG;
+
+	public function isSocket():Bool return (mode & asys.uv.UVConstants.S_IFMT) == asys.uv.UVConstants.S_IFSOCK;
+
+	public function isSymbolicLink():Bool return (mode & asys.uv.UVConstants.S_IFMT) == asys.uv.UVConstants.S_IFLNK;
+
+	function get_permissions():FilePermissions return @:privateAccess new FilePermissions(mode & asys.uv.UVConstants.S_PERM);
+
+	public var permissions(get, never):FilePermissions;
+}

+ 8 - 0
std/eval/Uv.hx

@@ -0,0 +1,8 @@
+package eval;
+
+extern class Uv {
+	static function init():Void;
+	static function run(mode:asys.uv.UVRunMode):Bool;
+	static function stop():Void;
+	static function close():Void;
+}

+ 14 - 0
std/eval/_std/asys/AsyncFileSystem.hx

@@ -0,0 +1,14 @@
+package asys;
+
+import haxe.NoData;
+import haxe.async.Callback;
+import haxe.io.FilePath;
+
+class AsyncFileSystem {
+	extern public static function access(path:FilePath, ?mode:FileAccessMode = FileAccessMode.Ok, cb:Callback<NoData>):Void;
+	extern public static function exists(path:FilePath, cb:Callback<Bool>):Void;
+	public static function readdir(path:FilePath, callback:Callback<Array<FilePath>>):Void
+		readdirTypes(path, (error, entries) -> callback(error, error == null ? entries.map(entry -> entry.name) : null));
+	extern public static function readdirTypes(path:FilePath, callback:Callback<Array<eval.uv.DirectoryEntry>>):Void;
+	extern public static function stat(path:FilePath, ?followSymLinks:Bool = true, cb:Callback<eval.uv.Stat>):Void;
+}

+ 154 - 0
std/eval/_std/asys/FileSystem.hx

@@ -0,0 +1,154 @@
+package asys;
+
+import haxe.Error;
+import haxe.io.Bytes;
+import haxe.io.FilePath;
+import asys.io.FileReadStream;
+
+typedef FileReadStreamCreationOptions = {
+	?flags:FileOpenFlags,
+	?mode:FilePermissions
+} &
+	asys.io.FileReadStream.FileReadStreamOptions;
+
+class FileSystem {
+	public static inline final async = asys.AsyncFileSystem;
+
+	extern public static function access(path:FilePath, ?mode:FileAccessMode = FileAccessMode.Ok):Void;
+
+	extern public static function chmod(path:FilePath, mode:FilePermissions, ?followSymLinks:Bool = true):Void;
+
+	extern public static function chown(path:FilePath, uid:Int, gid:Int, ?followSymLinks:Bool = true):Void;
+
+	public static function copyFile(src:FilePath, dest:FilePath /* , ?flags:FileCopyFlags */):Void {
+		throw "not implemented";
+	}
+
+	public static function createReadStream(path:FilePath, ?options:FileReadStreamCreationOptions):FileReadStream {
+		if (options == null)
+			options = {};
+		return new FileReadStream(open(path, options.flags, options.mode), options);
+	}
+
+	// static function createWriteStream(path:FilePath, ?options:{?flags:FileOpenFlags, ?mode:FilePermissions, ?autoClose:Bool, ?start:Int}):FileWriteStream;
+
+	extern public static function exists(path:FilePath):Bool;
+
+	extern public static function link(existingPath:FilePath, newPath:FilePath):Void;
+
+	extern static function mkdir_native(path:FilePath, mode:FilePermissions):Void;
+
+	public static function mkdir(path:FilePath, ?recursive:Bool = false, ?mode:FilePermissions):Void {
+		if (mode == null)
+			mode = @:privateAccess new FilePermissions(511); // 0777
+		if (!recursive)
+			return mkdir_native(path, mode);
+		var pathBuffer:FilePath = null;
+		for (component in path.components) {
+			if (pathBuffer == null)
+				pathBuffer = component;
+			else
+				pathBuffer = pathBuffer / component;
+			try {
+				mkdir_native(pathBuffer, mode);
+			} catch (e:Error) {
+				if (e.type.match(UVError(asys.uv.UVErrorType.EEXIST)))
+					continue;
+				throw e;
+			}
+		}
+	}
+
+	extern public static function mkdtemp(prefix:FilePath):FilePath;
+
+	public static function readdir(path:FilePath):Array<FilePath> {
+		return readdirTypes(path).map(entry -> entry.name);
+	}
+
+	extern public static function readdirTypes(path:FilePath):Array<DirectoryEntry>;
+
+	extern public static function readlink(path:FilePath):FilePath;
+
+	extern public static function realpath(path:FilePath):FilePath;
+
+	extern public static function rename(oldPath:FilePath, newPath:FilePath):Void;
+
+	extern public static function rmdir(path:FilePath):Void;
+
+	extern public static function stat(path:FilePath, ?followSymLinks:Bool = true):eval.uv.Stat;
+
+	extern public static function symlink(target:FilePath, path:FilePath, ?type:SymlinkType = SymlinkType.SymlinkDir):Void;
+
+	public static function truncate(path:FilePath, ?len:Int = 0):Void {
+		var f = open(path, FileOpenFlags.ReadWrite);
+		try {
+			f.truncate(len);
+		} catch (e:Dynamic) {
+			f.close();
+			throw e;
+		}
+		f.close();
+	}
+
+	extern public static function unlink(path:FilePath):Void;
+
+	extern static function utimes_native(path:FilePath, atime:Float, mtime:Float):Void;
+
+	public static function utimes(path:FilePath, atime:Date, mtime:Date):Void {
+		utimes_native(path, atime.getTime(), mtime.getTime());
+	}
+
+	public static inline function watch(path:FilePath, ?recursive:Bool = false):FileWatcher {
+		return @:privateAccess new FileWatcher(path, recursive);
+	}
+
+	extern static function open_native(path:FilePath, flags:FileOpenFlags, mode:FilePermissions, binary:Bool):asys.io.File;
+
+	public static function open(path:FilePath, ?flags:FileOpenFlags = FileOpenFlags.ReadOnly, ?mode:FilePermissions, ?binary:Bool = true):asys.io.File {
+		if (mode == null)
+			mode = @:privateAccess new FilePermissions(438); // 0666
+		return open_native(path, flags, mode, binary);
+	}
+
+	public static function readFile(path:FilePath, ?flags:FileOpenFlags = FileOpenFlags.ReadOnly):Bytes {
+		var file = open(path, flags);
+		var buffer:haxe.io.Bytes;
+		try {
+			var size = file.stat().size;
+			buffer = Bytes.alloc(size);
+			file.readBuffer(buffer, 0, size, 0);
+		} catch (e:Dynamic) {
+			file.close();
+			throw e;
+		}
+		file.close();
+		return buffer;
+	}
+
+	@:access(asys.FileOpenFlags)
+	public static function writeFile(path:FilePath, data:Bytes, ?flags:FileOpenFlags, ?mode:FilePermissions):Void {
+		if (flags == null)
+			flags = "w";
+		if (mode == null)
+			mode = @:privateAccess new FilePermissions(438) /* 0666 */;
+		var file = open(path, flags, mode);
+		var offset = 0;
+		var length = data.length;
+		var position:Null<Int> = null;
+		if (flags.get_raw() & FileOpenFlags.Append.get_raw() == 0)
+			position = 0;
+		try {
+			while (length > 0) {
+				var written = file.writeBuffer(data, offset, length, position).bytesWritten;
+				offset += written;
+				length -= written;
+				if (position != null) {
+					position += written;
+				}
+			}
+		} catch (e:Dynamic) {
+			file.close();
+			throw e;
+		}
+	}
+}

+ 50 - 0
std/eval/_std/asys/io/AsyncFile.hx

@@ -0,0 +1,50 @@
+package asys.io;
+
+import haxe.NoData;
+import haxe.async.*;
+import haxe.io.Bytes;
+import haxe.io.Encoding;
+
+class AsyncFile {
+	extern public function chmod(mode:FilePermissions, callback:Callback<NoData>):Void;
+
+	extern public function chown(uid:Int, gid:Int, callback:Callback<NoData>):Void;
+
+	extern public function close(callback:Callback<NoData>):Void;
+
+	extern public function datasync(callback:Callback<NoData>):Void;
+
+	extern public function readBuffer(buffer:Bytes, offset:Int, length:Int, position:Int, callback:Callback<{bytesRead:Int, buffer:Bytes}>):Void;
+
+	public function readFile(callback:Callback<Bytes>):Void {
+		stat((err, stat) -> {
+			if (err != null)
+				return callback(err, null);
+			var buffer = Bytes.alloc(stat.size);
+			readBuffer(buffer, 0, buffer.length, 0, (err, res) -> {
+				if (err != null)
+					return callback(err, null);
+				callback(null, buffer);
+			});
+		});
+	}
+
+	extern public function stat(callback:Callback<eval.uv.Stat>):Void;
+
+	extern public function sync(callback:Callback<NoData>):Void;
+
+	extern public function truncate(?len:Int = 0, callback:Callback<NoData>):Void;
+
+	extern function utimes_native(atime:Float, mtime:Float, callback:Callback<NoData>):Void;
+
+	public function utimes(atime:Date, mtime:Date, callback:Callback<NoData>):Void {
+		utimes_native(atime.getTime() / 1000, mtime.getTime() / 1000, callback);
+	}
+
+	extern public function writeBuffer(buffer:Bytes, offset:Int, length:Int, position:Int, callback:Callback<{bytesWritten:Int, buffer:Bytes}>):Void;
+
+	public function writeString(str:String, ?position:Int, ?encoding:Encoding, callback:Callback<{bytesWritten:Int, buffer:Bytes}>):Void {
+		var buffer = Bytes.ofString(str, encoding);
+		writeBuffer(buffer, 0, buffer.length, position, callback);
+	}
+}

+ 45 - 0
std/eval/_std/asys/io/File.hx

@@ -0,0 +1,45 @@
+package asys.io;
+
+import haxe.io.Bytes;
+import haxe.io.Encoding;
+
+class File {
+	extern function get_async():AsyncFile;
+
+	public var async(get, never):AsyncFile;
+
+	extern public function chmod(mode:FilePermissions):Void;
+
+	extern public function chown(uid:Int, gid:Int):Void;
+
+	extern public function close():Void;
+
+	extern public function datasync():Void;
+
+	extern public function readBuffer(buffer:Bytes, offset:Int, length:Int, position:Int):{bytesRead:Int, buffer:Bytes};
+
+	public function readFile():Bytes {
+		var buffer = Bytes.alloc(stat().size);
+		readBuffer(buffer, 0, buffer.length, 0);
+		return buffer;
+	}
+
+	extern public function stat():eval.uv.Stat;
+
+	extern public function sync():Void;
+
+	extern public function truncate(?len:Int = 0):Void;
+
+	extern function utimes_native(atime:Float, mtime:Float):Void;
+
+	public function utimes(atime:Date, mtime:Date):Void {
+		utimes_native(atime.getTime() / 1000, mtime.getTime() / 1000);
+	}
+
+	extern public function writeBuffer(buffer:Bytes, offset:Int, length:Int, position:Int):{bytesWritten:Int, buffer:Bytes};
+
+	public function writeString(str:String, ?position:Int, ?encoding:Encoding):{bytesWritten:Int, buffer:Bytes} {
+		var buffer = Bytes.ofString(str, encoding);
+		return writeBuffer(buffer, 0, buffer.length, position);
+	}
+}

+ 45 - 0
std/eval/_std/asys/io/FileReadStream.hx

@@ -0,0 +1,45 @@
+package asys.io;
+
+import haxe.io.*;
+import haxe.io.Readable.ReadResult;
+
+typedef FileReadStreamOptions = {
+	?autoClose:Bool,
+	?start:Int,
+	?end:Int,
+	?highWaterMark:Int
+};
+
+class FileReadStream extends Readable {
+	final file:File;
+	var position:Int;
+	final end:Int;
+	var readInProgress:Bool = false;
+
+	public function new(file:File, ?options:FileReadStreamOptions) {
+		super();
+		if (options == null)
+			options = {};
+		this.file = file;
+		position = options.start != null ? options.start : 0;
+		end = options.end != null ? options.end : 0xFFFFFFFF;
+	}
+
+	override function internalRead(remaining):ReadResult {
+		if (readInProgress)
+			return None;
+		readInProgress = true;
+		// TODO: check errors
+		var chunk = Bytes.alloc(remaining);
+		// TODO: check EOF for file as well
+		var willEnd = (position + remaining) >= end;
+		file.async.readBuffer(chunk, 0, remaining, position, (err, _) -> {
+			readInProgress = false;
+			if (err != null)
+				errorSignal.emit(err);
+			asyncRead([chunk], willEnd);
+		});
+		position += remaining;
+		return None;
+	}
+}

+ 25 - 0
std/eval/_std/asys/net/Dns.hx

@@ -0,0 +1,25 @@
+package asys.net;
+
+import haxe.async.Callback;
+
+using asys.net.AddressTools;
+
+class Dns {
+	static extern function lookup_native(hostname:String, ?lookupOptions:DnsLookupOptions, callback:Callback<Array<Address>>);
+
+	public static function lookup(hostname:String, ?lookupOptions:DnsLookupOptions, callback:Callback<Array<Address>>):Void {
+		lookup_native(hostname, lookupOptions, function (err, res:Array<Address>):Void {
+			if (err != null)
+				return callback(err, null);
+			var lastRes:Address = null;
+			callback(null, [ for (entry in res) {
+				// TODO: report more information rather than suppress duplicates?
+				if (lastRes != null && lastRes.equals(entry))
+					continue;
+				lastRes = entry;
+			} ]);
+		});
+	}
+
+	public static extern function reverse(ip:Address, callback:Callback<Array<String>>):Void;
+}

+ 25 - 0
std/eval/uv/DirectoryEntry.hx

@@ -0,0 +1,25 @@
+package eval.uv;
+
+import haxe.io.FilePath;
+
+class DirectoryEntry implements asys.DirectoryEntry {
+	public var name(get, never):FilePath;
+
+	extern function get_type():asys.uv.UVDirentType;
+
+	extern function get_name():FilePath;
+
+	public function isBlockDevice():Bool return get_type() == DirentBlock;
+
+	public function isCharacterDevice():Bool return get_type() == DirentChar;
+
+	public function isDirectory():Bool return get_type() == DirentDir;
+
+	public function isFIFO():Bool return get_type() == DirentFifo;
+
+	public function isFile():Bool return get_type() == DirentFile;
+
+	public function isSocket():Bool return get_type() == DirentSocket;
+
+	public function isSymbolicLink():Bool return get_type() == DirentLink;
+}

+ 11 - 0
std/eval/uv/FileWatcher.hx

@@ -0,0 +1,11 @@
+package eval.uv;
+
+import haxe.async.*;
+import haxe.io.FilePath;
+
+extern class FileWatcher {
+	function new(filename:FilePath, recursive:Bool, cb:Callback<asys.FileWatcherEvent>);
+	function close(cb:Callback<haxe.NoData>):Void;
+	function ref():Void;
+	function unref():Void;
+}

+ 25 - 0
std/eval/uv/Pipe.hx

@@ -0,0 +1,25 @@
+package eval.uv;
+
+import haxe.NoData;
+import haxe.async.Callback;
+import haxe.io.Bytes;
+import asys.net.*;
+
+extern class Pipe {
+	function new(ipc:Bool);
+	function open(fd:Int):Void;
+	function connectIpc(path:String, cb:Callback<NoData>):Void;
+	function bindIpc(path:String):Void;
+	function accept():Pipe;
+	function writeHandle(data:Bytes, handle:eval.uv.Stream, cb:Callback<NoData>):Void;
+	function pendingCount():Int;
+	function acceptPending():PipeAccept;
+	function getSockName():SocketAddress;
+	function getPeerName():SocketAddress;
+	function asStream():Stream;
+}
+
+enum PipeAccept {
+	Socket(_:eval.uv.Socket);
+	Pipe(_:eval.uv.Pipe);
+}

+ 32 - 0
std/eval/uv/Process.hx

@@ -0,0 +1,32 @@
+package eval.uv;
+
+import haxe.NoData;
+import haxe.async.*;
+
+extern class Process {
+	function new(
+		exitCb:Callback<{code:Int, signal:Int}>,
+		file:String,
+		args:Array<String>,
+		env:Array<String>,
+		cwd:String,
+		flags:asys.uv.UVProcessSpawnFlags,
+		stdio:Array<ProcessIO>,
+		uid:Int,
+		gid:Int
+	);
+	function kill(signal:Int):Void;
+	function getPid():Int;
+	function close(cb:Callback<NoData>):Void;
+	function ref():Void;
+	function unref():Void;
+}
+
+enum ProcessIO {
+	Ignore;
+	Inherit;
+	Pipe(readable:Bool, writable:Bool, pipe:eval.uv.Stream);
+	Ipc(pipe:eval.uv.Stream);
+	// Stream(_);
+	// Fd(_);
+}

+ 19 - 0
std/eval/uv/Socket.hx

@@ -0,0 +1,19 @@
+package eval.uv;
+
+import haxe.NoData;
+import haxe.async.Callback;
+import haxe.io.Bytes;
+import asys.net.*;
+
+extern class Socket {
+	function new();
+	function connectTcp(address:Address, port:Int, cb:Callback<NoData>):Void;
+	function bindTcp(host:Address, port:Int, ipv6only:Bool):Void;
+	function accept():Socket;
+	function close(cb:Callback<NoData>):Void;
+	function setKeepAlive(enable:Bool, initialDelay:Int):Void;
+	function setNoDelay(noDelay:Bool):Void;
+	function getSockName():SocketAddress;
+	function getPeerName():SocketAddress;
+	function asStream():Stream;
+}

+ 71 - 0
std/eval/uv/Stat.hx

@@ -0,0 +1,71 @@
+package eval.uv;
+
+import asys.FilePermissions;
+
+class Stat {
+	extern function get_dev():Int;
+
+	public var dev(get, never):Int;
+
+	extern function get_mode():Int;
+
+	public var mode(get, never):Int;
+
+	extern function get_nlink():Int;
+
+	public var nlink(get, never):Int;
+
+	extern function get_uid():Int;
+
+	public var uid(get, never):Int;
+
+	extern function get_gid():Int;
+
+	public var gid(get, never):Int;
+
+	extern function get_rdev():Int;
+
+	public var rdev(get, never):Int;
+
+	extern function get_ino():Int;
+
+	public var ino(get, never):Int;
+
+	extern function get_size():Int;
+
+	public var size(get, never):Int;
+
+	extern function get_blksize():Int;
+
+	public var blksize(get, never):Int;
+
+	extern function get_blocks():Int;
+
+	public var blocks(get, never):Int;
+
+	extern function get_flags():Int;
+
+	public var flags(get, never):Int;
+
+	extern function get_gen():Int;
+
+	public var gen(get, never):Int;
+
+	public function isBlockDevice():Bool return (mode & asys.uv.UVConstants.S_IFMT) == asys.uv.UVConstants.S_IFBLK;
+
+	public function isCharacterDevice():Bool return (mode & asys.uv.UVConstants.S_IFMT) == asys.uv.UVConstants.S_IFCHR;
+
+	public function isDirectory():Bool return (mode & asys.uv.UVConstants.S_IFMT) == asys.uv.UVConstants.S_IFDIR;
+
+	public function isFIFO():Bool return (mode & asys.uv.UVConstants.S_IFMT) == asys.uv.UVConstants.S_IFIFO;
+
+	public function isFile():Bool return (mode & asys.uv.UVConstants.S_IFMT) == asys.uv.UVConstants.S_IFREG;
+
+	public function isSocket():Bool return (mode & asys.uv.UVConstants.S_IFMT) == asys.uv.UVConstants.S_IFSOCK;
+
+	public function isSymbolicLink():Bool return (mode & asys.uv.UVConstants.S_IFMT) == asys.uv.UVConstants.S_IFLNK;
+
+	function get_permissions():FilePermissions return @:privateAccess new FilePermissions(mode & asys.uv.UVConstants.S_PERM);
+
+	public var permissions(get, never):FilePermissions;
+}

+ 17 - 0
std/eval/uv/Stream.hx

@@ -0,0 +1,17 @@
+package eval.uv;
+
+import haxe.NoData;
+import haxe.async.Callback;
+import haxe.io.Bytes;
+import asys.net.*;
+
+extern class Stream {
+	function write(data:Bytes, cb:Callback<NoData>):Void;
+	function end(cb:Callback<NoData>):Void;
+	function startRead(cb:Callback<Bytes>):Void;
+	function stopRead():Void;
+	function listen(backlog:Int, cb:Callback<NoData>):Void;
+	function close(cb:Callback<NoData>):Void;
+	function ref():Void;
+	function unref():Void;
+}

+ 8 - 0
std/eval/uv/Timer.hx

@@ -0,0 +1,8 @@
+package eval.uv;
+
+extern class Timer {
+	function new(timeMs:Int, cb:Void->Void);
+	function close(cb:haxe.async.Callback<haxe.NoData>):Void;
+	function ref():Void;
+	function unref():Void;
+}

+ 28 - 0
std/eval/uv/UdpSocket.hx

@@ -0,0 +1,28 @@
+package eval.uv;
+
+import haxe.NoData;
+import haxe.async.Callback;
+import haxe.io.Bytes;
+import asys.net.*;
+
+extern class UdpSocket {
+	function new();
+	function addMembership(multicastAddress:String, multicastInterface:String):Void;
+	function dropMembership(multicastAddress:String, multicastInterface:String):Void;
+	function send(msg:Bytes, offset:Int, length:Int, address:Address, port:Int, callback:Callback<NoData>):Void;
+	function close(callback:Callback<NoData>):Void;
+	function bindTcp(address:Address, port:Int, ipv6only:Bool):Void;
+	function startRead(callback:Callback<{data:Bytes, remoteAddress:Address, remotePort:Int}>):Void;
+	function stopRead():Void;
+	function getSockName():SocketAddress;
+	function setBroadcast(flag:Bool):Void;
+	function setMulticastInterface(intfc:String):Void;
+	function setMulticastLoopback(flag:Bool):Void;
+	function setMulticastTTL(ttl:Int):Void;
+	function setTTL(ttl:Int):Void;
+	function getRecvBufferSize():Int;
+	function getSendBufferSize():Int;
+	function setRecvBufferSize(size:Int):Int;
+	function setSendBufferSize(size:Int):Int;
+	function asStream():Stream;
+}

+ 112 - 3
std/haxe/Error.hx

@@ -1,7 +1,116 @@
 package haxe;
 
-extern class Error {
+import asys.uv.UVErrorType;
+import haxe.PosInfos;
+
+/**
+	Common class for errors.
+**/
+class Error {
+	function get_message():String {
+		return (switch (type) {
+			case UVError(UVErrorType.E2BIG): "argument list too long";
+			case UVError(UVErrorType.EACCES): "permission denied";
+			case UVError(UVErrorType.EADDRINUSE): "address already in use";
+			case UVError(UVErrorType.EADDRNOTAVAIL): "address not available";
+			case UVError(UVErrorType.EAFNOSUPPORT): "address family not supported";
+			case UVError(UVErrorType.EAGAIN): "resource temporarily unavailable";
+			case UVError(UVErrorType.EAI_ADDRFAMILY): "address family not supported";
+			case UVError(UVErrorType.EAI_AGAIN): "temporary failure";
+			case UVError(UVErrorType.EAI_BADFLAGS): "bad ai_flags value";
+			case UVError(UVErrorType.EAI_BADHINTS): "invalid value for hints";
+			case UVError(UVErrorType.EAI_CANCELED): "request canceled";
+			case UVError(UVErrorType.EAI_FAIL): "permanent failure";
+			case UVError(UVErrorType.EAI_FAMILY): "ai_family not supported";
+			case UVError(UVErrorType.EAI_MEMORY): "out of memory";
+			case UVError(UVErrorType.EAI_NODATA): "no address";
+			case UVError(UVErrorType.EAI_NONAME): "unknown node or service";
+			case UVError(UVErrorType.EAI_OVERFLOW): "argument buffer overflow";
+			case UVError(UVErrorType.EAI_PROTOCOL): "resolved protocol is unknown";
+			case UVError(UVErrorType.EAI_SERVICE): "service not available for socket type";
+			case UVError(UVErrorType.EAI_SOCKTYPE): "socket type not supported";
+			case UVError(UVErrorType.EALREADY): "connection already in progress";
+			case UVError(UVErrorType.EBADF): "bad file descriptor";
+			case UVError(UVErrorType.EBUSY): "resource busy or locked";
+			case UVError(UVErrorType.ECANCELED): "operation canceled";
+			case UVError(UVErrorType.ECHARSET): "invalid Unicode character";
+			case UVError(UVErrorType.ECONNABORTED): "software caused connection abort";
+			case UVError(UVErrorType.ECONNREFUSED): "connection refused";
+			case UVError(UVErrorType.ECONNRESET): "connection reset by peer";
+			case UVError(UVErrorType.EDESTADDRREQ): "destination address required";
+			case UVError(UVErrorType.EEXIST): "file already exists";
+			case UVError(UVErrorType.EFAULT): "bad address in system call argument";
+			case UVError(UVErrorType.EFBIG): "file too large";
+			case UVError(UVErrorType.EHOSTUNREACH): "host is unreachable";
+			case UVError(UVErrorType.EINTR): "interrupted system call";
+			case UVError(UVErrorType.EINVAL): "invalid argument";
+			case UVError(UVErrorType.EIO): "i/o error";
+			case UVError(UVErrorType.EISCONN): "socket is already connected";
+			case UVError(UVErrorType.EISDIR): "illegal operation on a directory";
+			case UVError(UVErrorType.ELOOP): "too many symbolic links encountered";
+			case UVError(UVErrorType.EMFILE): "too many open files";
+			case UVError(UVErrorType.EMSGSIZE): "message too long";
+			case UVError(UVErrorType.ENAMETOOLONG): "name too long";
+			case UVError(UVErrorType.ENETDOWN): "network is down";
+			case UVError(UVErrorType.ENETUNREACH): "network is unreachable";
+			case UVError(UVErrorType.ENFILE): "file table overflow";
+			case UVError(UVErrorType.ENOBUFS): "no buffer space available";
+			case UVError(UVErrorType.ENODEV): "no such device";
+			case UVError(UVErrorType.ENOENT): "no such file or directory";
+			case UVError(UVErrorType.ENOMEM): "not enough memory";
+			case UVError(UVErrorType.ENONET): "machine is not on the network";
+			case UVError(UVErrorType.ENOPROTOOPT): "protocol not available";
+			case UVError(UVErrorType.ENOSPC): "no space left on device";
+			case UVError(UVErrorType.ENOSYS): "function not implemented";
+			case UVError(UVErrorType.ENOTCONN): "socket is not connected";
+			case UVError(UVErrorType.ENOTDIR): "not a directory";
+			case UVError(UVErrorType.ENOTEMPTY): "directory not empty";
+			case UVError(UVErrorType.ENOTSOCK): "socket operation on non-socket";
+			case UVError(UVErrorType.ENOTSUP): "operation not supported on socket";
+			case UVError(UVErrorType.EPERM): "operation not permitted";
+			case UVError(UVErrorType.EPIPE): "broken pipe";
+			case UVError(UVErrorType.EPROTO): "protocol error";
+			case UVError(UVErrorType.EPROTONOSUPPORT): "protocol not supported";
+			case UVError(UVErrorType.EPROTOTYPE): "protocol wrong type for socket";
+			case UVError(UVErrorType.ERANGE): "result too large";
+			case UVError(UVErrorType.EROFS): "read-only file system";
+			case UVError(UVErrorType.ESHUTDOWN): "cannot send after transport endpoint shutdown";
+			case UVError(UVErrorType.ESPIPE): "invalid seek";
+			case UVError(UVErrorType.ESRCH): "no such process";
+			case UVError(UVErrorType.ETIMEDOUT): "connection timed out";
+			case UVError(UVErrorType.ETXTBSY): "text file is busy";
+			case UVError(UVErrorType.EXDEV): "cross-device link not permitted";
+			case UVError(UVErrorType.UNKNOWN): "unknown error";
+			case UVError(UVErrorType.EOF): "end of file";
+			case UVError(UVErrorType.ENXIO): "no such device or address";
+			case UVError(UVErrorType.EMLINK): "too many links";
+			case UVError(UVErrorType.EHOSTDOWN): "host is down";
+			case UVError(UVErrorType.EOTHER): "other UV error";
+			case _: "unknown error";
+		});
+	}
+
+	/**
+		A human-readable representation of the error.
+	**/
 	public var message(get, never):String;
-	public final posInfos:haxe.PosInfos;
-	public final type:Int;
+
+	/**
+		Position where the error was thrown. By default, this is the place where the error is constructed.
+	**/
+	public final posInfos:PosInfos;
+
+	/**
+		Error type, usable for discerning error types with `switch` statements.
+	**/
+	public final type:ErrorType;
+
+	public function new(type:ErrorType, ?posInfos:PosInfos) {
+		this.type = type;
+		this.posInfos = posInfos;
+	}
+
+	public function toString():String {
+		return '$message at $posInfos';
+	}
 }

+ 5 - 0
std/haxe/ErrorType.hx

@@ -0,0 +1,5 @@
+package haxe;
+
+enum ErrorType {
+	UVError(errno:asys.uv.UVErrorType);
+}

+ 10 - 0
std/haxe/NoData.hx

@@ -0,0 +1,10 @@
+package haxe;
+
+/**
+	Data type used to indicate the absence of a value, especially in types with
+	type parameters.
+**/
+abstract NoData({}) {
+	public inline function new()
+		this = {};
+}

+ 42 - 0
std/haxe/async/ArraySignal.hx

@@ -0,0 +1,42 @@
+package haxe.async;
+
+/**
+	Basic implementation of a `haxe.async.Signal`. Uses an array for storing
+	listeners for the signal.
+**/
+class ArraySignal<T> implements Signal<T> {
+	final listeners:Array<Listener<T>> = [];
+
+	function get_listenerCount():Int {
+		return listeners.length;
+	}
+
+	public var listenerCount(get, never):Int;
+
+	public function new() {}
+
+	public function on(listener:Listener<T>):Void {
+		listeners.push(listener);
+	}
+
+	public function once(listener:Listener<T>):Void {
+		listeners.push(function wrapped(data:T):Void {
+			listeners.remove(wrapped);
+			listener(data);
+		});
+	}
+
+	public function off(?listener:Listener<T>):Void {
+		if (listener != null) {
+			listeners.remove(listener);
+		} else {
+			listeners.resize(0);
+		}
+	}
+
+	public function emit(data:T):Void {
+		for (listener in listeners) {
+			listener(data);
+		}
+	}
+}

+ 69 - 0
std/haxe/async/Callback.hx

@@ -0,0 +1,69 @@
+package haxe.async;
+
+import haxe.Error;
+import haxe.NoData;
+
+typedef CallbackData<T> = (?error:Error, ?result:T) -> Void;
+
+/**
+	A callback. All callbacks in the standard library are functions which accept
+	two arguments: an error (`haxe.Error`) and a result (`T`). If error is 
+	non-`null`, result must be `null`. The callback type is declared in 	`CallbackData`.
+
+	This abstract defines multiple `@:from` conversions to improve readability of
+	callback code.
+**/
+@:callable
+abstract Callback<T>(CallbackData<T>) from CallbackData<T> {
+	/**
+		Returns a callback of the same type as `cb` which is guaranteed to be
+		non-`null`. If `cb` is given and is not `null` it is returned directly.
+		If `cb` is `null` a dummy callback which does nothing is returned instead.
+	**/
+	public static function nonNull<T>(?cb:Callback<T>):Callback<T> {
+		if (cb == null)
+			return (_, _) -> {};
+		return cb;
+	}
+
+	/**
+		Wraps a function which takes a single optional `haxe.Error` argument into
+		a callback of type `Callback<NoData>`. Allows:
+
+		```haxe
+		var cb:Callback<NoData> = (?err) -> trace("error!", err);
+		```
+	**/
+	@:from public static inline function fromOptionalErrorOnly(f:(?error:Error) -> Void):Callback<NoData> {
+		return (?err:Error, ?result:NoData) -> f(err);
+	}
+
+	/**
+		Wraps a function which takes a single `haxe.Error` argument into a callback
+		of type `Callback<NoData>`. Allows:
+
+		```haxe
+		var cb:Callback<NoData> = (err) -> trace("error!", err);
+		```
+	**/
+	@:from public static inline function fromErrorOnly(f:(error:Error) -> Void):Callback<NoData> {
+		return (?err:Error, ?result:NoData) -> f(err);
+	}
+
+	/*
+	// this should not be encouraged, may mess up from(Optional)ErrorOnly
+	@:from static inline function fromResultOnly<T>(f:(?result:T) -> Void):Callback<T> return (?err:Error, ?result:T) -> f(result);
+	*/
+
+	/**
+		Wraps a callback function declared without `?` (optional) arguments into a
+		callback.
+	**/
+	@:from public static inline function fromErrorResult<T>(f:(error:Error, result:T) -> Void):Callback<T> {
+		return (?err:Error, ?result:T) -> f(err, result);
+	}
+
+	#if (hl || neko)
+	private inline function toUVNoData() return (error) -> this(error, null);
+	#end
+}

+ 11 - 0
std/haxe/async/Defer.hx

@@ -0,0 +1,11 @@
+package haxe.async;
+
+class Defer {
+	/**
+		Schedules the given function to run during the next processing tick.
+		Convenience shortcut for `Timer.delay(f, 0)`.
+	**/
+	public static inline function nextTick(f:() -> Void):asys.Timer {
+		return asys.Timer.delay(f, 0);
+	}
+}

+ 19 - 0
std/haxe/async/Listener.hx

@@ -0,0 +1,19 @@
+package haxe.async;
+
+import haxe.NoData;
+
+typedef ListenerData<T> = (data:T) -> Void;
+
+/**
+	Signal listener. A signal listener is a function which accepts one argument
+	and has a `Void` return type.
+**/
+@:callable
+abstract Listener<T>(ListenerData<T>) from ListenerData<T> {
+	/**
+		This function allows a listener to a `Signal<NoData>` to be defined as a
+		function which accepts no arguments.
+	**/
+	@:from static inline function fromNoArguments(f:() -> Void):Listener<NoData>
+		return(data:NoData) -> f();
+}

+ 38 - 0
std/haxe/async/Signal.hx

@@ -0,0 +1,38 @@
+package haxe.async;
+
+/**
+	Signals are a type-safe system to emit events. A signal will calls its
+	listeners whenever _something_ (the event that the signal represents) happens,
+	passing along any relevant associated data.
+
+	Signals which have no associated data should use `haxe.NoData` as their type
+	parameter.
+**/
+interface Signal<T> {
+	/**
+		Number of listeners to `this` signal.
+	**/
+	var listenerCount(get, never):Int;
+
+	/**
+		Adds a listener to `this` signal, which will be called for all signal
+		emissions until it is removed with `off`.
+	**/
+	function on(listener:Listener<T>):Void;
+
+	/**
+		Adds a listener to `this` signal, which will be called only once, the next
+		time the signal emits.
+	**/
+	function once(listener:Listener<T>):Void;
+
+	/**
+		Removes the given listener from `this` signal.
+	**/
+	function off(?listener:Listener<T>):Void;
+
+	/**
+		Emits `data` to all current listeners of `this` signal.
+	**/
+	function emit(data:T):Void;
+}

+ 51 - 0
std/haxe/async/WrappedSignal.hx

@@ -0,0 +1,51 @@
+package haxe.async;
+
+import haxe.NoData;
+
+/**
+	An implementation of `haxe.async.Signal` which will listen for changes in its
+	listeners. This is useful when a class changes its behavior depending on
+	whether there are any listeners to some of its signals, e.g. a `Readable`
+	stream will not emit data signals when there are no data handlers.
+**/
+class WrappedSignal<T> implements Signal<T> {
+	final listeners:Array<Listener<T>> = [];
+	public final changeSignal:Signal<NoData> = new ArraySignal<NoData>();
+
+	function get_listenerCount():Int {
+		return listeners.length;
+	}
+
+	public var listenerCount(get, never):Int;
+
+	public function new() {}
+
+	public function on(listener:Listener<T>):Void {
+		listeners.push(listener);
+		changeSignal.emit(new NoData());
+	}
+
+	public function once(listener:Listener<T>):Void {
+		listeners.push(function wrapped(data:T):Void {
+			listeners.remove(wrapped);
+			changeSignal.emit(new NoData());
+			listener(data);
+		});
+		changeSignal.emit(new NoData());
+	}
+
+	public function off(?listener:Listener<T>):Void {
+		if (listener != null) {
+			listeners.remove(listener);
+		} else {
+			listeners.resize(0);
+		}
+		changeSignal.emit(new NoData());
+	}
+
+	public function emit(data:T):Void {
+		for (listener in listeners) {
+			listener(data);
+		}
+	}
+}

+ 137 - 0
std/haxe/io/Duplex.hx

@@ -0,0 +1,137 @@
+package haxe.io;
+
+import haxe.Error;
+import haxe.NoData;
+import haxe.async.*;
+import haxe.ds.List;
+import haxe.io.Readable.ReadResult;
+
+/**
+	A stream which is both readable and writable.
+
+	This is an abstract base class that should never be used directly. Instead,
+	child classes should override the `internalRead` and `internalWrite` methods.
+	See `haxe.io.Readable` and `haxe.io.Writable`.
+**/
+@:access(haxe.io.Readable)
+@:access(haxe.io.Writable)
+class Duplex implements IReadable implements IWritable {
+	public final dataSignal:Signal<Bytes>;
+	public final endSignal:Signal<NoData>;
+	public final errorSignal:Signal<Error>;
+	public final pauseSignal:Signal<NoData>;
+	public final resumeSignal:Signal<NoData>;
+
+	public final drainSignal:Signal<NoData>;
+	public final finishSignal:Signal<NoData>;
+	public final pipeSignal:Signal<IReadable>;
+	public final unpipeSignal:Signal<IReadable>;
+
+	final input:Writable;
+	final output:Readable;
+	final inputBuffer:List<Bytes>;
+	final outputBuffer:List<Bytes>;
+
+	function get_inputBufferLength() {
+		return input.bufferLength;
+	}
+	var inputBufferLength(get, never):Int;
+
+	function get_outputBufferLength() {
+		return output.bufferLength;
+	}
+	var outputBufferLength(get, never):Int;
+
+	function new() {
+		input = new DuplexWritable(this);
+		output = new DuplexReadable(this);
+		dataSignal = output.dataSignal;
+		endSignal = output.endSignal;
+		errorSignal = output.errorSignal;
+		pauseSignal = output.pauseSignal;
+		resumeSignal = output.resumeSignal;
+		drainSignal = input.drainSignal;
+		finishSignal = input.finishSignal;
+		pipeSignal = input.pipeSignal;
+		unpipeSignal = input.unpipeSignal;
+		inputBuffer = input.buffer;
+		outputBuffer = output.buffer;
+	}
+
+	// override by implementing classes
+	function internalRead(remaining:Int):ReadResult {
+		throw "not implemented";
+	}
+
+	function internalWrite():Void {
+		throw "not implemented";
+	}
+
+	inline function pop():Bytes {
+		return input.pop();
+	}
+
+	inline function push(chunk:Bytes):Void {
+		output.push(chunk);
+	}
+
+	inline function asyncRead(chunks:Array<Bytes>, eof:Bool):Void {
+		output.asyncRead(chunks, eof);
+	}
+
+	public inline function write(chunk:Bytes):Bool {
+		return input.write(chunk);
+	}
+
+	public function end():Void {
+		input.end();
+		output.asyncRead(null, true);
+	}
+
+	public inline function pause():Void {
+		output.pause();
+	}
+
+	public inline function resume():Void {
+		output.resume();
+	}
+
+	public inline function pipe(to:IWritable):Void {
+		output.pipe(to);
+	}
+
+	public inline function cork():Void {
+		input.cork();
+	}
+
+	public inline function uncork():Void {
+		input.uncork();
+	}
+}
+
+@:access(haxe.io.Duplex)
+private class DuplexWritable extends Writable {
+	final parent:Duplex;
+
+	public function new(parent:Duplex) {
+		this.parent = parent;
+	}
+
+	override function internalWrite():Void {
+		parent.internalWrite();
+	}
+}
+
+@:access(haxe.io.Duplex)
+private class DuplexReadable extends Readable {
+	final parent:Duplex;
+
+	public function new(parent:Duplex) {
+		super();
+		this.parent = parent;
+	}
+
+	override function internalRead(remaining):ReadResult {
+		return parent.internalRead(remaining);
+	}
+}

+ 51 - 0
std/haxe/io/FilePath.hx

@@ -0,0 +1,51 @@
+package haxe.io;
+
+/**
+	Represents a relative or absolute file path.
+**/
+abstract FilePath(String) from String {
+	@:from public static function encode(bytes:Bytes):FilePath {
+		// TODO: standard UTF-8 decoding, except any invalid bytes is replaced
+		// with (for example) U+FFFD, followed by the byte itself as a codepoint
+		return null;
+	}
+
+	public function decode():Bytes {
+		return null;
+	}
+
+	/**
+		The components of `this` path.
+	**/
+	public var components(get, never):Array<FilePath>;
+
+	private function get_components():Array<FilePath> {
+		return this.split("/");
+	}
+
+	@:op(A / B)
+	public function addComponent(other:FilePath):FilePath {
+		return this + "/" + other.get_raw();
+	}
+
+	private function get_raw():String
+		return this;
+
+	#if hl
+	private function decodeNative():hl.Bytes {
+		return @:privateAccess this.toUtf8();
+	}
+
+	private static function encodeNative(data:hl.Bytes):FilePath {
+		return ((@:privateAccess String.fromUCS2(data)) : FilePath);
+	}
+	#elseif neko
+	private function decodeNative():neko.NativeString {
+		return neko.NativeString.ofString(this);
+	}
+
+	private static function encodeNative(data:neko.NativeString):FilePath {
+		return (neko.NativeString.toString(data) : FilePath);
+	}
+	#end
+}

+ 12 - 0
std/haxe/io/IDuplex.hx

@@ -0,0 +1,12 @@
+package haxe.io;
+
+/**
+	A stream which is both readable and writable.
+
+	This interface should be used wherever an object that is both readable and
+	writable is expected, regardless of a specific implementation. See `Duplex`
+	for an abstract base class that can be used to implement an `IDuplex`.
+
+	See also `IReadable` and `IWritable`.
+**/
+interface IDuplex extends IReadable extends IWritable {}

+ 63 - 0
std/haxe/io/IReadable.hx

@@ -0,0 +1,63 @@
+package haxe.io;
+
+import haxe.Error;
+import haxe.NoData;
+import haxe.async.*;
+
+/**
+	A readable stream.
+
+	This interface should be used wherever an object that is readable is
+	expected, regardless of a specific implementation. See `Readable` for an
+	abstract base class that can be used to implement an `IReadable`.
+**/
+interface IReadable {
+	/**
+		Emitted whenever a chunk of data is available.
+	**/
+	final dataSignal:Signal<Bytes>;
+
+	/**
+		Emitted when the stream is finished. No further signals will be emitted by
+		`this` instance after `endSignal` is emitted.
+	**/
+	final endSignal:Signal<NoData>;
+
+	/**
+		Emitted for any error that occurs during reading.
+	**/
+	final errorSignal:Signal<Error>;
+
+	/**
+		Emitted when `this` stream is paused.
+	**/
+	final pauseSignal:Signal<NoData>;
+
+	/**
+		Emitted when `this` stream is resumed.
+	**/
+	final resumeSignal:Signal<NoData>;
+
+	/**
+		Resumes flow of data. Note that this method is called automatically
+		whenever listeners to either `dataSignal` or `endSignal` are added.
+	**/
+	function resume():Void;
+
+	/**
+		Pauses flow of data.
+	**/
+	function pause():Void;
+
+	/**
+		Pipes the data from `this` stream to `target`.
+	**/
+	function pipe(target:IWritable):Void;
+
+	/**
+		Indicates to `this` stream that an additional `amount` bytes should be read
+		from the underlying data source. Note that the actual data will arrive via
+		`dataSignal`.
+	**/
+	// function read(amount:Int):Void;
+}

+ 15 - 0
std/haxe/io/IWritable.hx

@@ -0,0 +1,15 @@
+package haxe.io;
+
+import haxe.NoData;
+import haxe.async.Signal;
+
+interface IWritable {
+	final drainSignal:Signal<NoData>;
+	final finishSignal:Signal<NoData>;
+	final pipeSignal:Signal<IReadable>;
+	final unpipeSignal:Signal<IReadable>;
+	function write(chunk:Bytes):Bool;
+	function end():Void;
+	function cork():Void;
+	function uncork():Void;
+}

+ 240 - 0
std/haxe/io/Readable.hx

@@ -0,0 +1,240 @@
+package haxe.io;
+
+import haxe.Error;
+import haxe.NoData;
+import haxe.async.*;
+import haxe.ds.List;
+
+/**
+	A readable stream.
+
+	This is an abstract base class that should never be used directly. Instead,
+	subclasses should override the `internalRead` method.
+**/
+class Readable implements IReadable {
+	/**
+		See `IReadable.dataSignal`.
+	**/
+	public final dataSignal:Signal<Bytes>;
+
+	/**
+		See `IReadable.endSignal`.
+	**/
+	public final endSignal:Signal<NoData>;
+
+	/**
+		See `IReadable.errorSignal`.
+	**/
+	public final errorSignal:Signal<Error> = new ArraySignal();
+
+	/**
+		See `IReadable.pauseSignal`.
+	**/
+	public final pauseSignal:Signal<NoData> = new ArraySignal();
+
+	/**
+		See `IReadable.resumeSignal`.
+	**/
+	public final resumeSignal:Signal<NoData> = new ArraySignal();
+
+	/**
+		High water mark. `Readable` will call `internalRead` pre-emptively to fill
+		up the internal buffer up to this value when possible. Set to `0` to
+		disable pre-emptive reading.
+	**/
+	public var highWaterMark = 8192;
+
+	/**
+		Total amount of data currently in the internal buffer, in bytes.
+	**/
+	public var bufferLength(default, null) = 0;
+
+	/**
+		Whether data is flowing at the moment. When flowing, data signals will be
+		emitted and the internal buffer will be empty.
+	**/
+	public var flowing(default, null) = false;
+
+	/**
+		Whether this stream is finished. When `true`, no further signals will be
+		emmited by `this` instance.
+	**/
+	public var done(default, null) = false;
+
+	var buffer = new List<Bytes>();
+	var deferred:asys.Timer;
+	var willEof = false;
+
+	@:dox(show)
+	function new(?highWaterMark:Int = 8192) {
+		this.highWaterMark = highWaterMark;
+		var dataSignal = new WrappedSignal<Bytes>();
+		dataSignal.changeSignal.on(() -> {
+			if (dataSignal.listenerCount > 0)
+				resume();
+		});
+		this.dataSignal = dataSignal;
+		var endSignal = new WrappedSignal<NoData>();
+		endSignal.changeSignal.on(() -> {
+			if (endSignal.listenerCount > 0)
+				resume();
+		});
+		this.endSignal = endSignal;
+	}
+
+	inline function shouldFlow():Bool {
+		return !done && (dataSignal.listenerCount > 0 || endSignal.listenerCount > 0);
+	}
+
+	function process():Void {
+		deferred = null;
+		if (!shouldFlow())
+			flowing = false;
+		if (!flowing)
+			return;
+
+		var reschedule = false;
+
+		// pre-emptive read until HWM
+		if (!willEof && !done)
+			while (bufferLength < highWaterMark) {
+				switch (internalRead(highWaterMark - bufferLength)) {
+					case None:
+						break;
+					case Data(chunks, eof):
+						reschedule = true;
+						for (chunk in chunks)
+							push(chunk);
+						if (eof) {
+							willEof = true;
+							break;
+						}
+				}
+			}
+
+		// emit data
+		while (buffer.length > 0 && flowing && shouldFlow()) {
+			reschedule = true;
+			dataSignal.emit(pop());
+		}
+
+		if (willEof) {
+			endSignal.emit(new NoData());
+			flowing = false;
+			done = true;
+			return;
+		}
+
+		if (!shouldFlow())
+			flowing = false;
+		else if (reschedule)
+			scheduleProcess();
+	}
+
+	inline function scheduleProcess():Void {
+		if (deferred == null)
+			deferred = Defer.nextTick(process);
+	}
+
+	function push(chunk:Bytes):Bool {
+		if (done)
+			throw "stream already done";
+		buffer.add(chunk);
+		bufferLength += chunk.length;
+		return bufferLength < highWaterMark;
+	}
+
+	/**
+		This method should be used internally from `internalRead` to provide data
+		resulting from asynchronous operations. The arguments to this method are
+		the same as `ReadableResult.Data`. See `internalRead` for more details.
+	**/
+	@:dox(show)
+	function asyncRead(chunks:Array<Bytes>, eof:Bool):Void {
+		if (done || willEof)
+			throw "stream already done";
+		if (chunks != null)
+			for (chunk in chunks)
+				push(chunk);
+		if (eof)
+			willEof = true;
+		if (chunks != null || eof)
+			scheduleProcess();
+	}
+
+	function pop():Bytes {
+		if (done)
+			throw "stream already done";
+		var chunk = buffer.pop();
+		bufferLength -= chunk.length;
+		return chunk;
+	}
+
+	/**
+		This method should be overridden by a subclass.
+
+		This method will be called as needed by `Readable`. The `remaining`
+		argument is an indication of how much data is needed to fill the internal
+		buffer up to the high water mark, or the current requested amount of data.
+		This method is called in a cycle until the read cycle is stopped with a
+		`None` return or an EOF is indicated, as described below.
+
+		If a call to this method returns `None`, the current read cycle is
+		ended. This value should be returned when there is no data available at the
+		moment, but a read request was scheduled and will later be fulfilled by a
+		call to `asyncRead`.
+
+		If a call to this method returns `Data(chunks, eof)`, `chunks` will be
+		added to the internal buffer. If `eof` is `true`, the read cycle is ended
+		and the readable stream signals an EOF (end-of-file). After an EOF, no
+		further calls will be made. `chunks` should not be an empty array if `eof`
+		is `false`.
+
+		Code inside this method should only call `asyncRead` (asynchronously from
+		a callback) or provide data using the return value.
+	**/
+	@:dox(show)
+	function internalRead(remaining:Int):ReadResult {
+		throw "not implemented";
+	}
+
+	/**
+		See `IReadable.resume`.
+	**/
+	public function resume():Void {
+		if (done)
+			return;
+		if (!flowing) {
+			resumeSignal.emit(new NoData());
+			flowing = true;
+			scheduleProcess();
+		}
+	}
+
+	/**
+		See `IReadable.pause`.
+	**/
+	public function pause():Void {
+		if (done)
+			return;
+		if (flowing) {
+			pauseSignal.emit(new NoData());
+			flowing = false;
+		}
+	}
+
+	/**
+		See `IReadable.pipe`.
+	**/
+	public function pipe(to:IWritable):Void {
+		throw "!";
+	}
+}
+
+/**
+	See `Readable.internalRead`.
+**/
+enum ReadResult {
+	None;
+	Data(chunks:Array<Bytes>, eof:Bool);
+}

+ 21 - 0
std/haxe/io/StreamTools.hx

@@ -0,0 +1,21 @@
+package haxe.io;
+
+class StreamTools {
+	/**
+		Creates a pipeline out of the given streams. `input` is piped to the first
+		element in `intermediate`, which is piped to the next element in
+		`intermediate`, and so on, until the last stream is piped to `output`. If
+		`intermediate` is `null`, it is treated as an empty array and `input` is
+		connected directly to `output`.
+	**/
+	public static function pipeline(input:IReadable, ?intermediate:Array<IDuplex>, output:IWritable):Void {
+		if (intermediate == null || intermediate.length == 0)
+			return input.pipe(output);
+
+		input.pipe(intermediate[0]);
+		for (i in 0...intermediate.length - 1) {
+			intermediate[i].pipe(intermediate[i + 1]);
+		}
+		intermediate[intermediate.length - 1].pipe(output);
+	}
+}

+ 94 - 0
std/haxe/io/Transform.hx

@@ -0,0 +1,94 @@
+package haxe.io;
+
+import haxe.Error;
+import haxe.NoData;
+import haxe.async.*;
+
+@:access(haxe.io.Readable)
+@:access(haxe.io.Writable)
+class Transform implements IReadable implements IWritable {
+	public final dataSignal:Signal<Bytes>;
+	public final endSignal:Signal<NoData>;
+	public final errorSignal:Signal<Error>;
+	public final pauseSignal:Signal<NoData>;
+	public final resumeSignal:Signal<NoData>;
+
+	public final drainSignal:Signal<NoData>;
+	public final finishSignal:Signal<NoData>;
+	public final pipeSignal:Signal<IReadable>;
+	public final unpipeSignal:Signal<IReadable>;
+
+	final input:Writable;
+	final output:Readable;
+
+	var transforming:Bool = false;
+
+	function new() {
+		input = new TransformWritable(this);
+		output = @:privateAccess new Readable(0);
+		dataSignal = output.dataSignal;
+		endSignal = output.endSignal;
+		errorSignal = output.errorSignal;
+		pauseSignal = output.pauseSignal;
+		resumeSignal = output.resumeSignal;
+		drainSignal = input.drainSignal;
+		finishSignal = input.finishSignal;
+		pipeSignal = input.pipeSignal;
+		unpipeSignal = input.unpipeSignal;
+	}
+
+	function internalTransform(chunk:Bytes):Void {
+		throw "not implemented";
+	}
+
+	function push(chunk:Bytes):Void {
+		transforming = false;
+		output.asyncRead([chunk], false);
+		input.internalWrite();
+	}
+
+	public inline function write(chunk:Bytes):Bool {
+		return input.write(chunk);
+	}
+
+	public function end():Void {
+		input.end();
+		output.asyncRead(null, true);
+	}
+
+	public inline function pause():Void {
+		output.pause();
+	}
+
+	public inline function resume():Void {
+		output.resume();
+	}
+
+	public inline function pipe(to:IWritable):Void {
+		output.pipe(to);
+	}
+
+	public inline function cork():Void {
+		input.cork();
+	}
+
+	public inline function uncork():Void {
+		input.uncork();
+	}
+}
+
+@:access(haxe.io.Transform)
+private class TransformWritable extends Writable {
+	final parent:Transform;
+
+	public function new(parent:Transform) {
+		this.parent = parent;
+	}
+
+	override function internalWrite():Void {
+		if (buffer.length > 0) {
+			parent.transforming = true;
+			parent.internalTransform(pop());
+		}
+	}
+}

+ 91 - 0
std/haxe/io/Writable.hx

@@ -0,0 +1,91 @@
+package haxe.io;
+
+import haxe.NoData;
+import haxe.async.*;
+import haxe.ds.List;
+
+/**
+	A writable stream.
+
+	This is an abstract base class that should never be used directly. Instead,
+	subclasses should override the `internalWrite` method.
+**/
+class Writable implements IWritable {
+	public final drainSignal:Signal<NoData> = new ArraySignal<NoData>();
+	public final finishSignal:Signal<NoData> = new ArraySignal<NoData>();
+	public final pipeSignal:Signal<IReadable> = new ArraySignal<IReadable>();
+	public final unpipeSignal:Signal<IReadable> = new ArraySignal<IReadable>();
+
+	public var highWaterMark = 8192;
+	public var bufferLength(default, null) = 0;
+	public var corkCount(default, null) = 0;
+	public var done(default, null) = false;
+
+	var willDrain = false;
+	var willFinish = false;
+	var deferred:asys.Timer;
+	var buffer = new List<Bytes>();
+
+	// for use by implementing classes
+	function pop():Bytes {
+		var chunk = buffer.pop();
+		bufferLength -= chunk.length;
+		if (willDrain && buffer.length == 0) {
+			willDrain = false;
+			if (deferred == null)
+				deferred = Defer.nextTick(() -> {
+					deferred = null;
+					drainSignal.emit(new NoData());
+				});
+		}
+		if (willFinish && buffer.length == 0) {
+			willFinish = false;
+			Defer.nextTick(() -> finishSignal.emit(new NoData()));
+		}
+		return chunk;
+	}
+
+	// override by implementing classes
+	function internalWrite():Void {
+		throw "not implemented";
+	}
+
+	// for producers
+	public function write(chunk:Bytes):Bool {
+		if (done)
+			throw "stream already done";
+		buffer.add(chunk);
+		bufferLength += chunk.length;
+		if (corkCount <= 0)
+			internalWrite();
+		if (bufferLength >= highWaterMark) {
+			willDrain = true;
+			return false;
+		}
+		return true;
+	}
+
+	public function end():Void {
+		corkCount = 0;
+		if (buffer.length > 0)
+			internalWrite();
+		if (buffer.length > 0)
+			willFinish = true;
+		else
+			finishSignal.emit(new NoData());
+		done = true;
+	}
+
+	public function cork():Void {
+		if (done)
+			return;
+		corkCount++;
+	}
+
+	public function uncork():Void {
+		if (done || corkCount <= 0)
+			return;
+		if (--corkCount == 0 && buffer.length > 0)
+			internalWrite();
+	}
+}

+ 12 - 0
tests/asys/Main.hx

@@ -0,0 +1,12 @@
+import utest.Runner;
+import utest.ui.Report;
+
+class Main {
+	public static function main():Void {
+		var runner = new Runner();
+		runner.addCases(test);
+		runner.onTestStart.add(test -> trace("running", Type.getClassName(Type.getClass(test.fixture.target)), test.fixture.method));
+		Report.create(runner);
+		runner.run();
+	}
+}

+ 111 - 0
tests/asys/Test.hx

@@ -0,0 +1,111 @@
+import utest.Assert;
+import utest.Async;
+import haxe.io.Bytes;
+
+// copy of Test from Haxe unit test sources
+// + beq, noExc, sub
+class Test implements utest.ITest {
+	public function new() {}
+
+	var asyncDone = 0;
+	var asyncExpect = 0;
+
+	function setup() {
+		TestBase.uvSetup();
+		asyncDone = 0;
+		asyncExpect = 0;
+	}
+
+	function sub(async:Async, f:(done:() -> Void) -> Void, ?localExpect:Int = 1):Void {
+		asyncExpect += localExpect;
+		var localDone = 0;
+		f(() -> {
+			localDone++;
+			asyncDone++;
+			if (asyncDone > asyncExpect || localDone > localExpect)
+				assert("too many done calls");
+			if (asyncDone == asyncExpect)
+				async.done();
+		});
+	}
+
+	function teardown() {
+		if (asyncDone < asyncExpect)
+			assert("not enough done calls");
+		TestBase.uvTeardown();
+	}
+
+	function eq<T>(v:T, v2:T, ?pos:haxe.PosInfos) {
+		Assert.equals(v, v2, pos);
+	}
+
+	function neq<T>(v:T, v2:T, ?pos:haxe.PosInfos) {
+		Assert.notEquals(v, v2, pos);
+	}
+
+	function feq(v:Float, v2:Float, ?pos:haxe.PosInfos) {
+		Assert.floatEquals(v, v2, pos);
+	}
+
+	function aeq<T>(expected:Array<T>, actual:Array<T>, ?pos:haxe.PosInfos) {
+		Assert.same(expected, actual, pos);
+	}
+
+	function beq(a:Bytes, b:Bytes, ?pos:haxe.PosInfos) {
+		Assert.isTrue(a.compare(b) == 0, pos);
+	}
+
+	function t(v, ?pos:haxe.PosInfos) {
+		Assert.isTrue(v, pos);
+	}
+
+	function f(v, ?pos:haxe.PosInfos) {
+		Assert.isFalse(v, pos);
+	}
+
+	function assert(?message:String, ?pos:haxe.PosInfos) {
+		Assert.fail(message, pos);
+	}
+
+	function exc(f:Void->Void, ?pos:haxe.PosInfos) {
+		Assert.raises(f, pos);
+	}
+
+	function noExc(f:Void->Void, ?pos:haxe.PosInfos) {
+		Assert.isTrue(try {
+			f();
+			true;
+		} catch (e:Dynamic) false, pos);
+	}
+
+	function unspec(f:Void->Void, ?pos) {
+		try {
+			f();
+		} catch (e:Dynamic) {}
+		noAssert();
+	}
+
+	function allow<T>(v:T, values:Array<T>, ?pos) {
+		Assert.contains(v, values, pos);
+	}
+
+	function noAssert(?pos:haxe.PosInfos) {
+		t(true, pos);
+	}
+
+	function hf(c:Class<Dynamic>, n:String, ?pos:haxe.PosInfos) {
+		t(Lambda.has(Type.getInstanceFields(c), n));
+	}
+
+	function nhf(c:Class<Dynamic>, n:String, ?pos:haxe.PosInfos) {
+		f(Lambda.has(Type.getInstanceFields(c), n));
+	}
+
+	function hsf(c:Class<Dynamic>, n:String, ?pos:haxe.PosInfos) {
+		t(Lambda.has(Type.getClassFields(c), n));
+	}
+
+	function nhsf(c:Class<Dynamic>, n:String, ?pos:haxe.PosInfos) {
+		f(Lambda.has(Type.getClassFields(c), n));
+	}
+}

+ 71 - 0
tests/asys/TestBase.hx

@@ -0,0 +1,71 @@
+import haxe.io.Bytes;
+import asys.io.*;
+import asys.*;
+import utest.Assert;
+#if hl
+import hl.Uv;
+#elseif eval
+import eval.Uv;
+#elseif neko
+import neko.Uv;
+#end
+
+class TestBase {
+	static var helpers:Map<Process, {?exit:ProcessExit}> = [];
+
+	public static function uvSetup():Void {
+		Uv.init();
+	}
+
+	public static function uvTeardown():Void {
+		helperTeardown();
+		Uv.run(RunDefault);
+		Uv.close();
+	}
+
+	public static function uvRun(?mode:asys.uv.UVRunMode = asys.uv.UVRunMode.RunDefault):Bool {
+		return Uv.run(mode);
+	}
+
+	/**
+		The helper script should be in `test-helpers/<current target>`:
+
+		- `eval` - `test-helpers/eval/<name>.hxml`; will be executed with the hxml
+			and `--run <Name>` appended in order to support passing arguments.
+		- `hl` - `test-helpers/hl/<name>.hl`
+	**/
+	public static function helperStart(name:String, ?args:Array<String>, ?options:asys.Process.ProcessSpawnOptions):Process {
+		if (args == null)
+			args = [];
+		var proc:Process;
+		#if eval
+		args.unshift(name.charAt(0).toUpperCase() + name.substr(1));
+		args.unshift("--run");
+		args.unshift('test-helpers/eval/$name.hxml');
+		name = "/DevProjects/Repos/haxe/haxe";
+		#elseif hl
+		args.unshift('test-helpers/hl/$name.hl');
+		name = "/DevProjects/Repos/hashlink/hl";
+		#else
+		throw "unsupported platform for helperStart";
+		#end
+		proc = Process.spawn(name, args, options);
+		helpers[proc] = {};
+		proc.exitSignal.on(exit -> helpers[proc].exit = exit);
+		return proc;
+	}
+
+	public static function helperTeardown():Void {
+		var anyFail = false;
+		for (proc => res in helpers) {
+			if (res.exit == null) {
+				proc.kill();
+				proc.close();
+				anyFail = true;
+			}
+		}
+		helpers = [];
+		if (anyFail)
+			Assert.fail("helper script(s) not terminated properly");
+	}
+}

+ 13 - 0
tests/asys/TestConstants.hx

@@ -0,0 +1,13 @@
+import haxe.io.Bytes;
+
+class TestConstants {
+	// contents of resources-ro/hello.txt
+	public static var helloString = "hello world
+symbols ◊†¶•¬
+non-BMP 🐄";
+	public static var helloBytes = Bytes.ofString(helloString);
+
+	// contents of resources-ro/binary.bin
+	// - contains invalid Unicode, should not be used as string
+	public static var binaryBytes = Bytes.ofHex("5554462D3820686572652C20627574207468656E3A2000FFFAFAFAFAF2F2F2F2F200C2A0CCD880E2ED9FBFEDA0800D0A");
+}

+ 2 - 0
tests/asys/build-common.hxml

@@ -0,0 +1,2 @@
+--main Main
+--library utest

+ 4 - 0
tests/asys/build-eval.hxml

@@ -0,0 +1,4 @@
+build-common.hxml
+# TODO: tests hang without disabling DCE
+-dce no
+--interp

+ 3 - 0
tests/asys/build-hl-c.hxml

@@ -0,0 +1,3 @@
+build-common.hxml
+-p ../hl-impl
+--hl bin/hlc/main.c

+ 10 - 0
tests/asys/build-hl.hxml

@@ -0,0 +1,10 @@
+build-common.hxml
+-p ../hl-impl
+--hl bin/test.hl
+
+--next
+-p ../common-impl
+-p ../hl-impl
+-p test-helpers/src
+--main IpcEcho
+--hl test-helpers/hl/ipcEcho.hl

+ 3 - 0
tests/asys/build-neko.hxml

@@ -0,0 +1,3 @@
+build-common.hxml
+-p ../neko-impl
+--neko bin/test.n

+ 17 - 0
tests/asys/impl/FastSource.hx

@@ -0,0 +1,17 @@
+package impl;
+
+import haxe.io.*;
+import haxe.io.Readable.ReadResult;
+
+class FastSource extends Readable {
+	final data:Array<Bytes>;
+
+	public function new(data:Array<Bytes>, ?highWaterMark:Int) {
+		super(highWaterMark);
+		this.data = data.copy();
+	}
+
+	override function internalRead(remaining):ReadResult {
+		return Data([data.shift()], data.length == 0);
+	}
+}

+ 22 - 0
tests/asys/impl/SlowSource.hx

@@ -0,0 +1,22 @@
+package impl;
+
+import haxe.io.*;
+import haxe.io.Readable.ReadResult;
+
+class SlowSource extends Readable {
+	final data:Array<Bytes>;
+
+	public function new(data:Array<Bytes>) {
+		super();
+		this.data = data.copy();
+	}
+
+	override function internalRead(remaining):ReadResult {
+		if (data.length > 0) {
+			var nextChunk = data.shift();
+			var nextEof = data.length == 0;
+			asys.Timer.delay(() -> asyncRead([nextChunk], nextEof), 10);
+		}
+		return None;
+	}
+}

BIN
tests/asys/resources-ro/binary.bin


+ 3 - 0
tests/asys/resources-ro/hello.txt

@@ -0,0 +1,3 @@
+hello world
+symbols ◊†¶•¬
+non-BMP 🐄

+ 19 - 0
tests/asys/test-helpers/src/IpcEcho.hx

@@ -0,0 +1,19 @@
+import asys.CurrentProcess;
+
+class IpcEcho {
+	public static function main():Void {
+		CurrentProcess.initUv();
+		CurrentProcess.initIpc(0);
+		var done = false;
+		CurrentProcess.messageSignal.on(message -> {
+			CurrentProcess.send(message);
+			@:privateAccess CurrentProcess.ipc.destroy((err) -> {
+				if (err != null) trace("err", err);
+				done = true;
+			});
+		});
+		while (!done)
+			CurrentProcess.runUv(RunOnce);
+		CurrentProcess.stopUv();
+	}
+}

+ 133 - 0
tests/asys/test/TestAsyncFile.hx

@@ -0,0 +1,133 @@
+package test;
+
+import utest.Async;
+import haxe.io.Bytes;
+import asys.FileSystem as NewFS;
+import asys.io.File as NewFile;
+import sys.FileSystem as OldFS;
+import sys.io.File as OldFile;
+
+class TestAsyncFile extends Test {
+	/**
+		Tests read functions.
+	**/
+	function testRead(async:Async):Void {
+		// ASCII
+		sub(async, done -> {
+			var file = NewFS.open("resources-ro/hello.txt");
+			var buffer = Bytes.alloc(5);
+			file.async.readBuffer(buffer, 0, 5, 0, (err, res) -> {
+				eq(err, null);
+				eq(res.buffer, buffer);
+				eq(res.bytesRead, 5);
+				beq(res.buffer, Bytes.ofString("hello"));
+				file.close();
+				done();
+			});
+		});
+
+		sub(async, done -> {
+			var file = NewFS.open("resources-ro/hello.txt");
+			var buffer = Bytes.alloc(5);
+			file.async.readBuffer(buffer, 0, 5, 6, (err, res) -> {
+				eq(err, null);
+				eq(res.buffer, buffer);
+				eq(res.bytesRead, 5);
+				beq(res.buffer, Bytes.ofString("world"));
+				file.close();
+				done();
+			});
+		});
+
+		// invalid arguments throw synchronous errors
+		var file = NewFS.open("resources-ro/hello.txt");
+		var buffer = Bytes.alloc(5);
+		exc(() -> file.async.readBuffer(buffer, 0, 6, 0, (_, _) -> assert()));
+		exc(() -> file.async.readBuffer(buffer, -1, 5, 0, (_, _) -> assert()));
+		exc(() -> file.async.readBuffer(buffer, 0, 0, 0, (_, _) -> assert()));
+		exc(() -> file.async.readBuffer(buffer, 0, 0, -1, (_, _) -> assert()));
+		file.close();
+
+		sub(async, done -> {
+			var file = NewFS.open("resources-ro/hello.txt");
+			var buffer = Bytes.alloc(15);
+			file.async.readBuffer(buffer, 0, 5, 0, (err, res) -> {
+				eq(err, null);
+				eq(res.bytesRead, 5);
+				file.async.readBuffer(buffer, 5, 5, 0, (err, res) -> {
+					eq(err, null);
+					eq(res.bytesRead, 5);
+					file.async.readBuffer(buffer, 10, 5, 0, (err, res) -> {
+						eq(err, null);
+						beq(buffer, Bytes.ofString("hellohellohello"));
+						file.close();
+						done();
+					});
+				});
+			});
+		});
+
+		// binary (+ invalid UTF-8)
+		sub(async, done -> {
+			var file = NewFS.open("resources-ro/binary.bin");
+			var buffer = Bytes.alloc(TestConstants.binaryBytes.length);
+			file.async.readBuffer(buffer, 0, buffer.length, 0, (err, res) -> {
+				eq(err, null);
+				eq(res.bytesRead, buffer.length);
+				beq(buffer, TestConstants.binaryBytes);
+				file.close();
+				done();
+			});
+		});
+
+		eq(asyncDone, 0);
+		TestBase.uvRun();
+	}
+
+	/**
+		Tests write functions.
+	**/
+	function testWrite(async:Async) {
+		sub(async, done -> {
+			var file = NewFS.open("resources-rw/hello.txt", "w");
+			var buffer = Bytes.ofString("hello");
+			file.async.writeBuffer(buffer, 0, 5, 0, (err, res) -> {
+				eq(err, null);
+				eq(res.bytesWritten, 5);
+				file.close();
+				beq(OldFile.getBytes("resources-rw/hello.txt"), buffer);
+				OldFS.deleteFile("resources-rw/hello.txt");
+				done();
+			});
+		});
+
+		sub(async, done -> {
+			var file = NewFS.open("resources-rw/unicode.txt", "w");
+			var buffer = TestConstants.helloBytes;
+			file.async.writeBuffer(buffer, 0, buffer.length, 0, (err, res) -> {
+				eq(err, null);
+				eq(res.bytesWritten, buffer.length);
+				file.close();
+				beq(OldFile.getBytes("resources-rw/unicode.txt"), buffer);
+				OldFS.deleteFile("resources-rw/unicode.txt");
+				done();
+			});
+		});
+
+		sub(async, done -> {
+			var file = NewFS.open("resources-rw/unicode2.txt", "w");
+			var buffer = TestConstants.helloBytes;
+			file.async.writeString(TestConstants.helloString, 0, (err, res) -> {
+				eq(err, null);
+				eq(res.bytesWritten, TestConstants.helloBytes.length);
+				file.close();
+				beq(OldFile.getBytes("resources-rw/unicode2.txt"), TestConstants.helloBytes);
+				OldFS.deleteFile("resources-rw/unicode2.txt");
+				done();
+			});
+		});
+
+		eq(asyncDone, 0);
+		TestBase.uvRun();
+	}
+}

+ 113 - 0
tests/asys/test/TestAsyncFileSystem.hx

@@ -0,0 +1,113 @@
+package test;
+
+import utest.Async;
+import asys.FileSystem as NewFS;
+import asys.io.File as NewFile;
+import sys.FileSystem as OldFS;
+import sys.io.File as OldFile;
+
+class TestAsyncFileSystem extends Test {
+	function testAsync(async:Async) {
+		sub(async, done -> NewFS.async.exists("resources-ro/hello.txt", (error, exists) -> {
+			t(exists);
+			done();
+		}));
+		sub(async, done -> NewFS.async.exists("resources-ro/non-existent-file", (error, exists) -> {
+			f(exists);
+			done();
+		}));
+		sub(async, done -> NewFS.async.readdir("resources-ro", (error, names) -> {
+			aeq(names, ["binary.bin", "hello.txt"]);
+			done();
+		}));
+
+		eq(asyncDone, 0);
+		TestBase.uvRun();
+	}
+
+	function testStat(async:Async) {
+		sub(async, done -> {
+			NewFS.async.stat("resources-ro", (error, stat) -> {
+				eq(error, null);
+				t(stat.isDirectory());
+				done();
+			});
+		});
+
+		sub(async, done -> {
+			NewFS.async.stat("resources-ro/hello.txt", (error, stat) -> {
+				eq(error, null);
+				eq(stat.size, TestConstants.helloBytes.length);
+				t(stat.isFile());
+				done();
+			});
+		});
+
+		sub(async, done -> {
+			NewFS.async.stat("resources-ro/binary.bin", (error, stat) -> {
+				eq(error, null);
+				eq(stat.size, TestConstants.binaryBytes.length);
+				t(stat.isFile());
+				done();
+			});
+		});
+
+		sub(async, done -> {
+			var file = NewFS.open("resources-ro/binary.bin");
+			file.async.stat((err, stat) -> {
+				eq(err, null);
+				eq(stat.size, TestConstants.binaryBytes.length);
+				t(stat.isFile());
+				file.close();
+				done();
+			});
+		});
+
+		sub(async, done -> {
+			NewFS.async.stat("resources-ro/non-existent-file", (error, nd) -> {
+				neq(error, null);
+				eq(nd, null);
+				done();
+			});
+		});
+
+		eq(asyncDone, 0);
+		TestBase.uvRun();
+	}
+
+	@:timeout(3000)
+	function testWatcher(async:Async) {
+		var dir = "resources-rw/watch";
+		sys.FileSystem.createDirectory(dir);
+		var events = [];
+
+		var watcher = NewFS.watch(dir, true);
+		watcher.closeSignal.on(_ -> {
+			async.done();
+			OldFS.deleteDirectory(dir);
+		});
+		watcher.errorSignal.on(e -> assert('unexpected error: ${e.message}'));
+		watcher.changeSignal.on(events.push);
+
+		NewFS.mkdir('$dir/foo');
+
+		TestBase.uvRun(RunOnce);
+		t(events.length == 1 && events[0].match(Rename("foo")));
+		events.resize(0);
+
+		var file = NewFS.open('$dir/foo/hello.txt', "w");
+		file.truncate(10);
+		file.close();
+		NewFS.unlink('$dir/foo/hello.txt');
+
+		NewFS.rmdir('$dir/foo');
+
+		TestBase.uvRun(RunOnce);
+		t(events.length == 2 && events[0].match(Rename("foo/hello.txt")));
+		t(events.length == 2 && events[1].match(Rename("foo")));
+		events.resize(0);
+
+		watcher.close();
+		TestBase.uvRun(RunOnce);
+	}
+}

+ 56 - 0
tests/asys/test/TestDns.hx

@@ -0,0 +1,56 @@
+package test;
+
+import haxe.io.Bytes;
+import utest.Async;
+
+class TestDns extends Test {
+	function testLocalhost(async:Async) {
+		sub(async, done -> asys.net.Dns.lookup("localhost", {family: Ipv4}, (err, res) -> {
+			eq(err, null);
+			t(res[0].match(Ipv4(0x7F000001)));
+			done();
+		}));
+
+		TestBase.uvRun();
+	}
+
+	function testIpv4(async:Async) {
+		sub(async, done -> asys.net.Dns.lookup("127.0.0.1", {family: Ipv4}, (err, res) -> {
+			eq(err, null);
+			t(res[0].match(Ipv4(0x7F000001)));
+			done();
+		}));
+		sub(async, done -> asys.net.Dns.lookup("123.32.10.1", {family: Ipv4}, (err, res) -> {
+			eq(err, null);
+			t(res[0].match(Ipv4(0x7B200A01)));
+			done();
+		}));
+		sub(async, done -> asys.net.Dns.lookup("255.255.255.255", {family: Ipv4}, (err, res) -> {
+			eq(err, null);
+			t(res[0].match(Ipv4(0xFFFFFFFF)));
+			done();
+		}));
+
+		TestBase.uvRun();
+	}
+
+	function testIpv6(async:Async) {
+		sub(async, done -> asys.net.Dns.lookup("::1", {family: Ipv6}, (err, res) -> {
+			eq(err, null);
+			t(res[0].match(Ipv6(beq(_, Bytes.ofHex("00000000000000000000000000000001")) => _)));
+			done();
+		}));
+		sub(async, done -> asys.net.Dns.lookup("2001:db8:1234:5678:11:2233:4455:6677", {family: Ipv6}, (err, res) -> {
+			eq(err, null);
+			t(res[0].match(Ipv6(beq(_, Bytes.ofHex("20010DB8123456780011223344556677")) => _)));
+			done();
+		}));
+		sub(async, done -> asys.net.Dns.lookup("4861:7865:2069:7320:6177:6573:6F6D:6521", {family: Ipv6}, (err, res) -> {
+			eq(err, null);
+			t(res[0].match(Ipv6(beq(_, Bytes.ofHex("4861786520697320617765736F6D6521")) => _)));
+			done();
+		}));
+
+		TestBase.uvRun();
+	}
+}

+ 79 - 0
tests/asys/test/TestFile.hx

@@ -0,0 +1,79 @@
+package test;
+
+import haxe.io.Bytes;
+import asys.FileSystem as NewFS;
+import asys.io.File as NewFile;
+import sys.FileSystem as OldFS;
+import sys.io.File as OldFile;
+
+class TestFile extends Test {
+	/**
+		Tests read functions.
+	**/
+	function testRead() {
+		// ASCII
+		var file = NewFS.open("resources-ro/hello.txt");
+		var buffer = Bytes.alloc(5);
+
+		eq(file.readBuffer(buffer, 0, 5, 0).bytesRead, 5);
+		beq(buffer, Bytes.ofString("hello"));
+
+		eq(file.readBuffer(buffer, 0, 5, 6).buffer, buffer);
+		beq(buffer, Bytes.ofString("world"));
+
+		exc(() -> file.readBuffer(buffer, 0, 6, 0));
+		exc(() -> file.readBuffer(buffer, -1, 5, 0));
+		exc(() -> file.readBuffer(buffer, 0, 0, 0));
+		exc(() -> file.readBuffer(buffer, 0, 0, -1));
+
+		buffer = Bytes.alloc(15);
+		eq(file.readBuffer(buffer, 0, 5, 0).bytesRead, 5);
+		eq(file.readBuffer(buffer, 5, 5, 0).bytesRead, 5);
+		eq(file.readBuffer(buffer, 10, 5, 0).bytesRead, 5);
+		beq(buffer, Bytes.ofString("hellohellohello"));
+
+		file.close();
+
+		// binary (+ invalid UTF-8)
+		var file = NewFS.open("resources-ro/binary.bin");
+		var buffer = Bytes.alloc(TestConstants.binaryBytes.length);
+		eq(file.readBuffer(buffer, 0, buffer.length, 0).bytesRead, buffer.length);
+		beq(buffer, TestConstants.binaryBytes);
+		file.close();
+
+		// readFile
+		var file = NewFS.open("resources-ro/hello.txt");
+		beq(file.readFile(), TestConstants.helloBytes);
+		file.close();
+	}
+
+	/**
+		Tests write functions.
+	**/
+	function testWrite() {
+		var file = NewFS.open("resources-rw/hello.txt", "w");
+		var buffer = Bytes.ofString("hello");
+		eq(file.writeBuffer(buffer, 0, 5, 0).bytesWritten, 5);
+		file.close();
+
+		beq(OldFile.getBytes("resources-rw/hello.txt"), buffer);
+
+		var file = NewFS.open("resources-rw/unicode.txt", "w");
+		var buffer = TestConstants.helloBytes;
+		eq(file.writeBuffer(buffer, 0, buffer.length, 0).bytesWritten, buffer.length);
+		file.close();
+
+		beq(OldFile.getBytes("resources-rw/unicode.txt"), buffer);
+
+		var file = NewFS.open("resources-rw/unicode2.txt", "w");
+		eq(file.writeString(TestConstants.helloString, 0).bytesWritten, TestConstants.helloBytes.length);
+		file.close();
+
+		beq(OldFile.getBytes("resources-rw/unicode2.txt"), TestConstants.helloBytes);
+
+		// cleanup
+		OldFS.deleteFile("resources-rw/hello.txt");
+		OldFS.deleteFile("resources-rw/unicode.txt");
+		OldFS.deleteFile("resources-rw/unicode2.txt");
+	}
+}

+ 194 - 0
tests/asys/test/TestFileSystem.hx

@@ -0,0 +1,194 @@
+package test;
+
+import haxe.io.Bytes;
+import asys.FileSystem as NewFS;
+import asys.io.File as NewFile;
+import sys.FileSystem as OldFS;
+import sys.io.File as OldFile;
+
+using StringTools;
+
+class TestFileSystem extends Test {
+	/**
+		Tests `FileSystem.access`, `perm` from `FileSystem.stat`, and
+		`FileSystem.chmod`.
+	**/
+	function testAccess():Void {
+		// create a file
+		OldFile.saveContent("resources-rw/access.txt", "");
+
+		NewFS.chmod("resources-rw/access.txt", None);
+		eq(NewFS.stat("resources-rw/access.txt").permissions, None);
+		noExc(() -> NewFS.access("resources-rw/access.txt"));
+		exc(() -> NewFS.access("resources-rw/access.txt", Read));
+
+		NewFS.chmod("resources-rw/access.txt", "r-------x");
+		eq(NewFS.stat("resources-rw/access.txt").permissions, "r-------x");
+		noExc(() -> NewFS.access("resources-rw/access.txt", Read));
+		exc(() -> NewFS.access("resources-rw/access.txt", Write));
+		exc(() -> NewFS.access("resources-rw/access.txt", Execute));
+
+		// cleanup
+		OldFS.deleteFile("resources-rw/access.txt");
+	}
+
+	function testExists():Void {
+		t(NewFS.exists("resources-ro/hello.txt"));
+		t(NewFS.exists("resources-ro/binary.bin"));
+		f(NewFS.exists("resources-ro/non-existent-file"));
+	}
+
+	function testMkdir():Void {
+		// initially these directories don't exist
+		f(OldFS.exists("resources-rw/mkdir"));
+		f(OldFS.exists("resources-rw/mkdir/nested/dir"));
+
+		// without `recursive`, this should not succeed
+		exc(() -> NewFS.mkdir("resources-rw/mkdir/nested/dir"));
+
+		// create a single directory
+		NewFS.mkdir("resources-rw/mkdir");
+
+		// create a directory recursively
+		NewFS.mkdir("resources-rw/mkdir/nested/dir", true);
+
+		t(OldFS.exists("resources-rw/mkdir"));
+		t(OldFS.exists("resources-rw/mkdir/nested/dir"));
+		f(OldFS.exists("resources-rw/mkdir/dir"));
+
+		// raise if target already exists if not `recursive`
+		exc(() -> NewFS.mkdir("resources-rw/mkdir/nested/dir"));
+
+		// cleanup
+		OldFS.deleteDirectory("resources-rw/mkdir/nested/dir");
+		OldFS.deleteDirectory("resources-rw/mkdir/nested");
+		OldFS.deleteDirectory("resources-rw/mkdir");
+	}
+
+	function testMkdtemp():Void {
+		// empty `resources-rw` to begin with
+		aeq(OldFS.readDirectory("resources-rw"), []);
+
+		// create some temporary directories
+		var dirs = [ for (i in 0...3) NewFS.mkdtemp("resources-rw/helloXXXXXX") ];
+
+		for (f in OldFS.readDirectory("resources-rw")) {
+			t(f.startsWith("hello"));
+			t(OldFS.isDirectory('resources-rw/$f'));
+			OldFS.deleteDirectory('resources-rw/$f');
+		}
+
+		// cleanup
+		for (f in OldFS.readDirectory("resources-rw")) {
+			OldFS.deleteDirectory('resources-rw/$f');
+		}
+	}
+
+	function testReaddir():Void {
+		aeq(NewFS.readdir("resources-rw"), []);
+		aeq(NewFS.readdirTypes("resources-rw"), []);
+		aeq(NewFS.readdir("resources-ro"), ["binary.bin", "hello.txt"]);
+		var res = NewFS.readdirTypes("resources-ro");
+		eq(res.length, 2);
+		eq(res[0].name, "binary.bin");
+		eq(res[0].isBlockDevice(), false);
+		eq(res[0].isCharacterDevice(), false);
+		eq(res[0].isDirectory(), false);
+		eq(res[0].isFIFO(), false);
+		eq(res[0].isFile(), true);
+		eq(res[0].isSocket(), false);
+		eq(res[0].isSymbolicLink(), false);
+
+		// raises if target is not a directory or does not exist
+		exc(() -> NewFS.readdir("resources-ro/hello.txt"));
+		exc(() -> NewFS.readdir("resources-ro/non-existent-directory"));
+	}
+
+	function testRename():Void {
+		// setup
+		OldFile.saveContent("resources-rw/hello.txt", TestConstants.helloString);
+		OldFile.saveContent("resources-rw/other.txt", "");
+		OldFS.createDirectory("resources-rw/sub");
+		OldFile.saveContent("resources-rw/sub/foo.txt", "");
+
+		t(OldFS.exists("resources-rw/hello.txt"));
+		f(OldFS.exists("resources-rw/world.txt"));
+
+		// rename a file
+		NewFS.rename("resources-rw/hello.txt", "resources-rw/world.txt");
+
+		f(OldFS.exists("resources-rw/hello.txt"));
+		t(OldFS.exists("resources-rw/world.txt"));
+		eq(OldFile.getContent("resources-rw/world.txt"), TestConstants.helloString);
+
+		// raises if the old path is non-existent
+		exc(() -> NewFS.rename("resources-rw/non-existent", "resources-rw/foobar"));
+
+		// raises if renaming file to directory
+		exc(() -> NewFS.rename("resources-rw/world.txt", "resources-rw/sub"));
+
+		// raises if renaming directory to file
+		exc(() -> NewFS.rename("resources-rw/sub", "resources-rw/world.txt"));
+
+		// rename a directory
+		NewFS.rename("resources-rw/sub", "resources-rw/resub");
+
+		f(OldFS.exists("resources-rw/sub"));
+		t(OldFS.exists("resources-rw/resub"));
+		aeq(OldFS.readDirectory("resources-rw/resub"), ["foo.txt"]);
+
+		// renaming to existing file overrides it
+		NewFS.rename("resources-rw/world.txt", "resources-rw/other.txt");
+
+		f(OldFS.exists("resources-rw/world.txt"));
+		t(OldFS.exists("resources-rw/other.txt"));
+		eq(OldFile.getContent("resources-rw/other.txt"), TestConstants.helloString);
+
+		// cleanup
+		OldFS.deleteFile("resources-rw/other.txt");
+		OldFS.deleteFile("resources-rw/resub/foo.txt");
+		OldFS.deleteDirectory("resources-rw/resub");
+	}
+
+	function testStat():Void {
+		var stat = NewFS.stat("resources-ro");
+		t(stat.isDirectory());
+
+		var stat = NewFS.stat("resources-ro/hello.txt");
+		eq(stat.size, TestConstants.helloBytes.length);
+		t(stat.isFile());
+
+		var stat = NewFS.stat("resources-ro/binary.bin");
+		eq(stat.size, TestConstants.binaryBytes.length);
+		t(stat.isFile());
+
+		var file = NewFS.open("resources-ro/binary.bin");
+		var stat = file.stat();
+		eq(stat.size, TestConstants.binaryBytes.length);
+		t(stat.isFile());
+		file.close();
+
+		exc(() -> NewFS.stat("resources-ro/non-existent-file"));
+	}
+
+	/**
+		Tests old filesystem APIs.
+		`exists` is tested in `testExists`.
+	**/
+	/*
+	function testCompat():Void {
+		eq(NewFS.readFile("resources-ro/hello.txt").toString(), TestConstants.helloString);
+		beq(NewFS.readFile("resources-ro/hello.txt"), TestConstants.helloBytes);
+		beq(NewFS.readFile("resources-ro/binary.bin"), TestConstants.binaryBytes);
+		t(NewFS.isDirectory("resources-ro"));
+		f(NewFS.isDirectory("resources-ro/hello.txt"));
+		aeq(NewFS.readDirectory("resources-ro"), ["binary.bin", "hello.txt"]);
+
+		NewFS.createDirectory("resources-rw/foo");
+		t(OldFS.exists("resources-rw/foo"));
+		t(OldFS.isDirectory("resources-rw/foo"));
+		NewFS.deleteDirectory("resources-rw/foo");
+		f(OldFS.exists("resources-rw/foo"));
+	}
+	*/
+}

+ 73 - 0
tests/asys/test/TestIpc.hx

@@ -0,0 +1,73 @@
+package test;
+
+import haxe.io.Bytes;
+import utest.Async;
+
+class TestIpc extends Test {
+	function testEcho(async:Async) {
+		sub(async, done -> {
+			var server:asys.net.Server = null;
+			server = asys.Net.createServer({
+				listen: Ipc({
+					path: "resources-rw/ipc-pipe"
+				})
+			}, client -> client.dataSignal.on(chunk -> {
+				beq(chunk, TestConstants.helloBytes);
+				client.write(chunk);
+				client.destroy();
+				server.close((err) -> {
+					eq(err, null);
+					done();
+				});
+			}));
+			server.errorSignal.on(err -> assert());
+		});
+
+		sub(async, done -> {
+			var client:asys.net.Socket = null;
+			client = asys.Net.createConnection({
+				connect: Ipc({
+					path: "resources-rw/ipc-pipe"
+				})
+			}, (err) -> {
+					eq(err, null);
+					t(client.remoteAddress.match(Unix("resources-rw/ipc-pipe")));
+					client.errorSignal.on(err -> assert());
+					client.write(TestConstants.helloBytes);
+					client.dataSignal.on(chunk -> {
+						beq(chunk, TestConstants.helloBytes);
+						client.destroy((err) -> {
+							eq(err, null);
+							done();
+						});
+					});
+				});
+		});
+
+		TestBase.uvRun();
+	}
+
+	/*
+	// TODO: this segfaults ?
+	function testIpcEcho(async:Async) {
+		var proc = TestBase.helperStart("ipcEcho", [], {
+			stdio: [Ipc, Inherit, Inherit]
+		});
+		proc.messageSignal.on((message:{message:{a:Array<Int>, b:String, d:Bool}}) -> {
+			t(switch (message.message) {
+				case {a: [1, 2], b: "c", d: true}: true;
+				case _: false;
+			});
+			trace("ok, closing?");
+			proc.close(err -> {
+				trace("closed?", err);
+				eq(err, null);
+				async.done();
+			});
+		});
+		proc.send({message: {a: [1, 2], b: "c", d: true}});
+
+		TestBase.uvRun();
+	}
+	*/
+}

+ 60 - 0
tests/asys/test/TestMisc.hx

@@ -0,0 +1,60 @@
+package test;
+
+import haxe.io.Bytes;
+import asys.FilePermissions;
+
+using asys.net.AddressTools;
+
+class TestMisc extends Test {
+	/**
+		Tests `sys.FilePermissions`. No actual system calls are tested here; see
+		e.g. `TestFileSystem.testAccess`.
+	**/
+	function testFilePermissions() {
+		eq(("---------" : FilePermissions), None);
+		eq(("r--------" : FilePermissions), ReadOwner);
+		eq(("-w-------" : FilePermissions), WriteOwner);
+		eq(("--x------" : FilePermissions), ExecuteOwner);
+		eq(("---r-----" : FilePermissions), ReadGroup);
+		eq(("----w----" : FilePermissions), WriteGroup);
+		eq(("-----x---" : FilePermissions), ExecuteGroup);
+		eq(("------r--" : FilePermissions), ReadOthers);
+		eq(("-------w-" : FilePermissions), WriteOthers);
+		eq(("--------x" : FilePermissions), ExecuteOthers);
+		eq(("rwx------" : FilePermissions), ReadOwner | WriteOwner | ExecuteOwner);
+		eq(("---rwx---" : FilePermissions), ReadGroup | WriteGroup | ExecuteGroup);
+		eq(("------rwx" : FilePermissions), ReadOthers | WriteOthers | ExecuteOthers);
+		eq(("rw-rw-rw-" : FilePermissions), ReadOwner | WriteOwner | ReadGroup | WriteGroup | ReadOthers | WriteOthers);
+
+		eq(ReadOwner, FilePermissions.fromOctal("400"));
+		eq(ReadOwner | WriteOwner | ExecuteOwner, FilePermissions.fromOctal("700"));
+		eq(ReadOwner | WriteOwner | ReadGroup | WriteGroup | ReadOthers | WriteOthers, FilePermissions.fromOctal("666"));
+	}
+
+	function testAddressTools() {
+		f("127.256.0.1".isIpv4());
+		f("127.0.1".isIpv4());
+
+		f("::1::".isIpv6());
+		f("1::2::3".isIpv6());
+		f("1::127.0.1".isIpv6());
+		f("::127.0.0.1:ffff:127.0.0.1".isIpv6());
+		f("1234:1234:1234:1234::1234:1234:1234:1234".isIpv6());
+
+		t("0.0.0.0".toIpv4().match(Ipv4(0)));
+		t("255.255.255.255".toIpv4().match(Ipv4(0xFFFFFFFF)));
+		t("127.0.0.1".toIpv4().match(Ipv4(0x7F000001)));
+		t("123.32.1.0".toIpv4().match(Ipv4(0x7B200100)));
+
+		"::".toIpv6().match(Ipv6(beq(_, Bytes.ofHex("00000000000000000000000000000000")) => _));
+		"::1".toIpv6().match(Ipv6(beq(_, Bytes.ofHex("00000000000000000000000000000001")) => _));
+		"1::".toIpv6().match(Ipv6(beq(_, Bytes.ofHex("00010000000000000000000000000000")) => _));
+		"2001:db8:1234:5678:11:2233:4455:6677".toIpv6().match(Ipv6(beq(_, Bytes.ofHex("20010DB8123456780011223344556677")) => _));
+		"4861:7865:2069:7320:6177:6573:6F6D:6521".toIpv6().match(Ipv6(beq(_, Bytes.ofHex("4861786520697320617765736F6D6521")) => _));
+		"1:2:3::ffff:127.0.0.1".toIpv6().match(Ipv6(beq(_, Bytes.ofHex("00010002000300000000FFFF7F000001")) => _));
+
+		t("123.32.1.0".toIp().match(Ipv4(0x7B200100)));
+		"123.32.1.0".toIp().mapToIpv6().match(Ipv6(beq(_, Bytes.ofHex("00000000000000000000FFFF7B200100")) => _));
+		"1:2:3::ffff:127.0.0.1".toIp().match(Ipv6(beq(_, Bytes.ofHex("00010002000300000000FFFF7F000001")) => _));
+	}
+}

+ 21 - 0
tests/asys/test/TestProcess.hx

@@ -0,0 +1,21 @@
+package test;
+
+import haxe.io.Bytes;
+import utest.Async;
+
+class TestProcess extends Test {
+	function testPipes(async:Async) {
+		var proc = asys.Process.spawn("cat");
+		proc.stdout.dataSignal.on(data -> {
+			beq(data, TestConstants.helloBytes);
+			proc.kill();
+			proc.close((err) -> {
+				eq(err, null);
+				async.done();
+			});
+		});
+		proc.stdin.write(TestConstants.helloBytes);
+
+		TestBase.uvRun();
+	}
+}

+ 89 - 0
tests/asys/test/TestStreams.hx

@@ -0,0 +1,89 @@
+package test;
+
+import haxe.io.*;
+import impl.*;
+import utest.Async;
+
+class TestStreams extends Test {
+	static var bytes123 = ["1", "22", "333"].map(Bytes.ofString.bind(_, null));
+	static var bytes555 = ["aaaaa", "bbbbb", "ccccc"].map(Bytes.ofString.bind(_, null));
+
+	function testRead(async:Async) {
+		var calls = [];
+		var stream = new SlowSource(bytes123);
+		stream.dataSignal.on((chunk) -> calls.push(chunk.length));
+		stream.endSignal.once(() -> {
+			aeq(calls, [1, 2, 3]);
+			async.done();
+		});
+
+		TestBase.uvRun();
+	}
+
+	function testReadHWM(async:Async) {
+		sub(async, done -> {
+			var calls = [];
+			var stream = new FastSource(bytes555, 4);
+			stream.dataSignal.on((chunk) -> {
+				eq(stream.bufferLength, 0);
+				calls.push(chunk.length);
+			});
+			stream.endSignal.once(() -> {
+				aeq(calls, [5, 5, 5]);
+				done();
+			});
+		});
+
+		sub(async, done -> {
+			var lens = [];
+			var calls = [];
+			var stream = new FastSource(bytes555, 15);
+			stream.dataSignal.on((chunk) -> {
+				lens.push(stream.bufferLength);
+				calls.push(chunk.length);
+			});
+			stream.endSignal.once(() -> {
+				aeq(lens, [10, 5, 0]);
+				aeq(calls, [5, 5, 5]);
+				done();
+			});
+		});
+
+		sub(async, done -> {
+			var lens = [];
+			var calls = [];
+			var stream = new FastSource(bytes555, 10);
+			stream.dataSignal.on((chunk) -> {
+				lens.push(stream.bufferLength);
+				calls.push(chunk.length);
+			});
+			stream.endSignal.once(() -> {
+				aeq(lens, [5, 0, 0]);
+				aeq(calls, [5, 5, 5]);
+				done();
+			});
+		});
+
+		TestBase.uvRun();
+	}
+
+	function testPassiveRead(async:Async) {
+		var calls = [];
+		var stream = new SlowSource(bytes123);
+
+		// add a listener but immediately pause - no calls should be made yet
+		stream.dataSignal.on((chunk) -> calls.push(10 + chunk.length));
+		stream.pause();
+
+		Sys.sleep(.05);
+		TestBase.uvRun(RunOnce);
+
+		stream.dataSignal.on((chunk) -> calls.push(chunk.length));
+		stream.endSignal.once(() -> {
+			aeq(calls, [11, 1, 12, 2, 13, 3]);
+			async.done();
+		});
+
+		TestBase.uvRun();
+	}
+}

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.