From e18895a18427782447f45e661a0041302e65f357 Mon Sep 17 00:00:00 2001 From: Seggan Date: Wed, 1 Nov 2023 21:17:43 -0400 Subject: [PATCH] Add a zunch of `path` --- docs/lang/core/io.papyri | 7 +- docs/lang/std/path.papyri | 33 +++++++++- .../metis/runtime/intrinsics/Intrinsics.kt | 21 ++++++ .../metis/runtime/intrinsics/NativeLibrary.kt | 65 +++++++------------ .../metis/runtime/intrinsics/NativeObjects.kt | 35 ++++++++-- lang/src/main/resources/core/io.metis | 13 +++- lang/src/main/resources/std/path.metis | 54 +++++++++++++++ 7 files changed, 177 insertions(+), 51 deletions(-) create mode 100644 lang/src/main/resources/std/path.metis diff --git a/docs/lang/core/io.papyri b/docs/lang/core/io.papyri index d5b3fad..bcbe418 100644 --- a/docs/lang/core/io.papyri +++ b/docs/lang/core/io.papyri @@ -58,8 +58,13 @@ Closes the stream. @h3 { flush(self): null } Flushes the stream. -@h3 { write(self, buffer: bytes): number } +@h3 { write(self, buffer: bytes, off: number \| null, len: number \| null): number } Writes a sequence of bytes from the buffer to the stream. Returns the number of bytes written. +If `off` is provided, the function starts writing from the byte at that offset. If `len` is +provided, the function writes at most that many bytes. If `off` and `len` are both provided, +the function writes at most `len` bytes starting from the byte at offset `off`. If `off` is +not provided, the function starts writing from the first byte in the buffer. If `len` is not +provided, the function writes to the end of the buffer. @h3 { write_line(self, line: string): null } Writes a line to the stream. diff --git a/docs/lang/std/path.papyri b/docs/lang/std/path.papyri index 80f9139..08e4f2d 100644 --- a/docs/lang/std/path.papyri +++ b/docs/lang/std/path.papyri @@ -17,9 +17,25 @@ The path separator for the current platform. This is either `/` or `\`. @h3 { absolute(path: string): string } Returns the absolute path of the given path. -@h3 { base_name(path: string): string } +@h3 { file_name(path: string): string } Returns the filename of the given path. +@h3 { copy(from: string, to: string): null } +Copies the file at the given path `from` to the given path `to`. + +@h3 { create_dir(path: string): string } +Creates a directory at the given path and returns the path of the created directory. + +@h3 { create_dirs(path: string): string } +Creates a directory at the given path and all parent directories that do not exist yet. Returns +the path of the created directory. + +@h3 { delete(path: string): null } +Deletes the file or directory at the given path. Directories must be empty. + +@h3 { delete_recursive(path: string): null } +Deletes the file or directory at the given path recursively. + @h3 { exists(path: string): boolean } Returns a boolean value indicating whether the file or directory at the given path exists. @@ -41,6 +57,9 @@ Returns a boolean value indicating whether the given path is a symbolic link. @h3 { list(path: string): list\[string\] } Returns a list of the files and directories in the given directory. +@h3 { move(from: string, to: string): null } +Moves the file at the given path `from` to the given path `to`. + @h3 { normalize(path: string): string } Normalizes the given path and returns the normalized path as a string. @@ -53,8 +72,20 @@ Opens the file at the given path for writing and returns an output stream. @h3 { parent(path: string): string } Returns the parent directory of the given path. +@h3 { read_all(path: string): bytes } +Reads the entire file at the given path and returns the contents as a byte array. + +@h3 { read_text(path: string): string } +Reads the entire file at the given path and returns the contents as a string. + @h3 { resolve(base: string, other: string): string } Resolves the given path `other` against the base path `base` and returns the resolved path as a string. @h3 { root(path: string): string } Returns the root of the given path. + +@h3 { write_all(path: string, data: bytes) } +Writes the given byte array to the file at the given path. + +@h3 { write_text(path: string, data: string) } +Writes the given string to the file at the given path. diff --git a/lang/src/main/kotlin/io/github/seggan/metis/runtime/intrinsics/Intrinsics.kt b/lang/src/main/kotlin/io/github/seggan/metis/runtime/intrinsics/Intrinsics.kt index def2449..4452a27 100644 --- a/lang/src/main/kotlin/io/github/seggan/metis/runtime/intrinsics/Intrinsics.kt +++ b/lang/src/main/kotlin/io/github/seggan/metis/runtime/intrinsics/Intrinsics.kt @@ -145,4 +145,25 @@ inline fun threeArgFunction( val a = state.stack.pop() state.stack.push(state.fn(a, b, c)) } +} + +/** + * Creates a [OneShotFunction] with four arguments. + * + * @param requiresSelf Whether the function requires a `self` argument. + * @param fn The function to execute. + * @return The created function. + * @see OneShotFunction + */ +inline fun fourArgFunction( + requiresSelf: Boolean = false, + crossinline fn: State.(Value, Value, Value, Value) -> Value +): OneShotFunction = object : OneShotFunction(Arity(4, requiresSelf)) { + override fun execute(state: State, nargs: Int) { + val d = state.stack.pop() + val c = state.stack.pop() + val b = state.stack.pop() + val a = state.stack.pop() + state.stack.push(state.fn(a, b, c, d)) + } } \ No newline at end of file diff --git a/lang/src/main/kotlin/io/github/seggan/metis/runtime/intrinsics/NativeLibrary.kt b/lang/src/main/kotlin/io/github/seggan/metis/runtime/intrinsics/NativeLibrary.kt index ae0356f..130a729 100644 --- a/lang/src/main/kotlin/io/github/seggan/metis/runtime/intrinsics/NativeLibrary.kt +++ b/lang/src/main/kotlin/io/github/seggan/metis/runtime/intrinsics/NativeLibrary.kt @@ -2,9 +2,6 @@ package io.github.seggan.metis.runtime.intrinsics import io.github.seggan.metis.runtime.* import io.github.seggan.metis.util.push -import java.io.IOException -import java.nio.file.InvalidPathException -import java.nio.file.NoSuchFileException import java.nio.file.Path import java.util.regex.PatternSyntaxException import kotlin.collections.set @@ -18,9 +15,8 @@ import kotlin.math.* */ abstract class NativeLibrary(val name: String) : OneShotFunction(Arity.ZERO) { final override fun execute(state: State, nargs: Int) { - val table = buildTable(::init) - state.globals[name] = table - state.stack.push(table) + state.globals.putAll(buildTable(::init)) + state.stack.push(Value.Null) } abstract fun init(lib: MutableMap) @@ -108,32 +104,31 @@ object OsLib : NativeLibrary("os") { } /** - * The `path` library + * The `path` library's native functions */ -object PathLib : NativeLibrary("path") { +object PathLib : NativeLibrary("__path") { private inline fun pathFunction(crossinline fn: (Path) -> Value) = oneArgFunction { self -> - try { - fn(currentDir.resolve(fileSystem.getPath(self.stringValue()))) - } catch (e: InvalidPathException) { - Value.Null - } catch (e: IOException) { - throw MetisRuntimeException("IoError", e.message ?: "Unknown IO error", cause = e) - } + translateIoError { fn(toPath(self)) } } + private fun State.toPath(value: Value) = currentDir.resolve(fileSystem.getPath(value.stringValue())) + + @OptIn(ExperimentalPathApi::class) override fun init(lib: MutableMap) { lib["separator"] = Value.String(System.getProperty("file.separator")) lib["normalize"] = pathFunction { it.normalize().toString().metisValue() } lib["absolute"] = pathFunction { it.toAbsolutePath().toString().metisValue() } lib["resolve"] = twoArgFunction { self, other -> - currentDir.resolve(fileSystem.getPath(self.stringValue())) - .resolve(other.stringValue()) - .toString() - .metisValue() + translateIoError { + toPath(self) + .resolve(other.stringValue()) + .toString() + .metisValue() + } } lib["parent"] = pathFunction { it.parent.toString().metisValue() } - lib["base_name"] = pathFunction { it.fileName.toString().metisValue() } + lib["file_name"] = pathFunction { it.fileName.toString().metisValue() } lib["root"] = pathFunction { it.root.toString().metisValue() } lib["is_absolute"] = pathFunction { it.isAbsolute.metisValue() } lib["list"] = pathFunction { path -> @@ -148,28 +143,18 @@ object PathLib : NativeLibrary("path") { lib["is_dir"] = pathFunction { it.isDirectory().metisValue() } lib["is_symlink"] = pathFunction { it.isSymbolicLink().metisValue() } lib["is_hidden"] = pathFunction { it.isHidden().metisValue() } - lib["open_write"] = pathFunction { - try { - wrapOutStream(it.outputStream()) - } catch (e: FileAlreadyExistsException) { - throw MetisRuntimeException( - "IoError", - "File already exists: ${it.absolutePathString()}", - Value.Table(mutableMapOf("path".metisValue() to it.absolutePathString().metisValue())) - ) - } - } - lib["open_read"] = pathFunction { - try { - wrapInStream(it.inputStream()) - } catch (e: NoSuchFileException) { - throw MetisRuntimeException( - "IoError", - "File not found: ${it.absolutePathString()}", - Value.Table(mutableMapOf("path".metisValue() to it.absolutePathString().metisValue())) - ) + lib["move"] = twoArgFunction { src, dest -> + translateIoError { + toPath(src).moveTo(toPath(dest)) } + Value.Null } + lib["delete"] = pathFunction { it.deleteIfExists().metisValue() } + lib["create_dir"] = pathFunction { it.createDirectory().absolutePathString().metisValue() } + lib["create_dirs"] = pathFunction { it.createDirectories().absolutePathString().metisValue() } + lib["delete_recursive"] = pathFunction { it.deleteRecursively(); Value.Null } + lib["open_write"] = pathFunction { wrapOutStream(it.outputStream()) } + lib["open_read"] = pathFunction { wrapInStream(it.inputStream()) } } } diff --git a/lang/src/main/kotlin/io/github/seggan/metis/runtime/intrinsics/NativeObjects.kt b/lang/src/main/kotlin/io/github/seggan/metis/runtime/intrinsics/NativeObjects.kt index 6570ce6..c4820f0 100644 --- a/lang/src/main/kotlin/io/github/seggan/metis/runtime/intrinsics/NativeObjects.kt +++ b/lang/src/main/kotlin/io/github/seggan/metis/runtime/intrinsics/NativeObjects.kt @@ -3,25 +3,46 @@ package io.github.seggan.metis.runtime.intrinsics import io.github.seggan.metis.runtime.* +import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream import java.io.OutputStream +import java.nio.file.InvalidPathException /** * Translates an [IOException] into a [MetisRuntimeException]. * * @param block The block to execute. */ -inline fun translateIoError(block: () -> T): T = try { - block() -} catch (e: IOException) { - throw MetisRuntimeException("IoError", e.message ?: "Unknown IO error", cause = e) +inline fun translateIoError(block: () -> T): T { + val message = try { + null to block() + } catch (e: InvalidPathException) { + "Invalid path: ${e.message}" to null + } catch (e: FileNotFoundException) { + "File not found: ${e.message}" to null + } catch (e: NoSuchFileException) { + "File not found: ${e.message}" to null + } catch (e: FileAlreadyExistsException) { + "File already exists: ${e.message}" to null + } catch (e: SecurityException) { + "Permission denied: ${e.message}" to null + } catch (e: IOException) { + e.message to null + } + if (message.first != null) { + throw MetisRuntimeException("IoError", message.first!!) + } else { + return message.second!! + } } internal val outStreamMetatable = buildTable { table -> - table["write"] = twoArgFunction(true) { self, value -> - val toBeWritten = value.convertTo().value - translateIoError { self.asObj().write(toBeWritten) } + table["write"] = fourArgFunction(true) { self, buffer, off, len -> + val toBeWritten = buffer.convertTo().value + val offset = if (off == Value.Null) 0 else off.intValue() + val length = if (len == Value.Null) toBeWritten.size else len.intValue() + translateIoError { self.asObj().write(toBeWritten, offset, length) } toBeWritten.size.toDouble().metisValue() } table["flush"] = oneArgFunction(true) { self -> diff --git a/lang/src/main/resources/core/io.metis b/lang/src/main/resources/core/io.metis index 569dbb7..34cbe2c 100644 --- a/lang/src/main/resources/core/io.metis +++ b/lang/src/main/resources/core/io.metis @@ -30,9 +30,18 @@ fn io.in_stream.read_line(self) return str(result) end +fn io.in_stream.copy_to(self, out) + let buffer = bytes.allocate(BUFSIZE) + let read = self.read(buffer) + while read is not null and read > 0 + out.write(buffer, 0, read) + read = self.read(buffer) + end +end + fn io.out_stream.write_line(self, line) - self.write(line) - self.write("\n") + self.write(line.encode()) + self.write('\n') end fn io.out_stream.write_text(self, text) diff --git a/lang/src/main/resources/std/path.metis b/lang/src/main/resources/std/path.metis new file mode 100644 index 0000000..1dac184 --- /dev/null +++ b/lang/src/main/resources/std/path.metis @@ -0,0 +1,54 @@ +import __path + +globals().merge(__path) + +global fn read_all(p) + let result = null + let stream = __path.open_read(p) + do + result = stream.read_all() + finally + stream.close() + end + return result +end + +global fn read_text(p) + let result = null + let stream = __path.open_read(p) + do + result = stream.read_text() + finally + stream.close() + end + return result +end + +global fn write_all(p, data) + let stream = __path.open_write(p) + do + stream.write(data) + finally + stream.close() + end +end + +global fn write_text(p, data) + let stream = __path.open_write(p) + do + stream.write_text(data) + finally + stream.close() + end +end + +global fn copy(src, dest) + let source = __path.open_read(src) + let destination = __path.open_write(dest) + do + source.copy_to(destination) + finally + source.close() + destination.close() + end +end \ No newline at end of file