plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-std/Os.plum
module std
import std/List
import std/Result
import std/Str
import std/Uuid
import std/Bool
import std/Number
# NOTE: the original file modeled `stdin`/`stdout`/`stderr` as bare top-level
# variable bindings and a `File(...)` constructor, but this language has no
# top-level `let` and `File` is never defined anywhere in the stdlib. Reduced
# to the underlying path so this at least parses; a real `File` type is
# still needed here.
fun stdin() -> Str =
"/dev/stdin"
fun stdout() -> Str =
"/dev/stdout"
fun stderr() -> Str =
"/dev/stderr"
# Writes the specified data, followed by the current line terminator, to the
# standard output stream. `extern` means this compiles to a genuine wasm import
# (`plum::printLn`) instead of a Plum body — printing isn't expressible in wasm
# on its own; the embedding host (`plum-cli run`/`plum build`) provides it.
extern fun printLn(s: Str)
# Raw host-provided filesystem primitives — each maps to a genuine wasm
# import backed by a real OS call in the embedder (`plum-cli`/`plum-runtime`),
# not something expressible in wasm on its own (same idea as `printLn`).
# Returns `Int` (0/1), not `Bool`, for the success/failure ones — the host
# closure that satisfies a wasm import runs BEFORE the module is instantiated
# (it's part of setting up the imports an instance needs), so it has no way
# to reach the instance's own `True`/`False` singleton globals that `Bool`
# values are represented as in this codegen; a plain `i64` has no such
# problem. `rawReadFile` returns `""` for a file that doesn't exist or can't
# be read (there's no way to signal an error through a bare `Str` return);
# callers needing to tell "empty file" apart from "missing file" should check
# `exists` first, which every wrapper below does.
extern fun rawReadFile(path: Str) -> Str
extern fun rawWriteFile(path: Str, data: Str) -> Int
extern fun rawExists(path: Str) -> Int
extern fun rawMkdir(path: Str) -> Int
extern fun rawRemove(path: Str) -> Int
extern fun rawRemoveDir(path: Str) -> Int
extern fun rawRename(old_path: Str, new_path: Str) -> Int
extern fun rawTruncate(path: Str, len: Int) -> Int
extern fun rawLink(existing_path: Str, new_path: Str) -> Int
extern fun rawSymlink(target: Str, path: Str) -> Int
extern fun rawReadlink(path: Str) -> Str
extern fun rawRealpath(path: Str) -> Str
extern fun rawChmod(path: Str, mode: Int) -> Int
# Every directory entry's name (not full path), joined with `\n`; `""` for a
# missing/unreadable directory (same ambiguity `rawReadFile` already has with
# an empty file — `exists`/`readdir` below both check existence first).
extern fun rawReadDir(path: Str) -> Str
# Reads the whole file at `path` as a `Str` (`Str` is a byte array here — see
# `libs/std/str.plum` — so this works for binary content too, not just text).
fun readFile(path: Str) -> Result[Str, Str] =
if !exists(path)
return Err("no such file: '{path}'")
return Ok(rawReadFile(path))
# Writes `data` to `path`, replacing its previous contents (or creating it).
fun writeFile(path: Str, data: Str) -> Result[Bool, Str] =
if rawWriteFile(path, data) == 1
return Ok(True)
return Err("failed to write file: '{path}'")
fun exists(path: Str) -> Bool =
return rawExists(path) == 1
fun mkdir(path: Str) -> Result[Bool, Str] =
if rawMkdir(path) == 1
return Ok(True)
return Err("failed to create directory: '{path}'")
# Removes the file (not directory) at `path`.
fun remove(path: Str) -> Result[Bool, Str] =
if !exists(path)
return Err("no such file: '{path}'")
if rawRemove(path) == 1
return Ok(True)
return Err("failed to remove: '{path}'")
# Whether `path` exists and is reachable — this VM has no notion of a
# process' effective uid/gid, so (unlike POSIX `access(2)`) there is no mode
# argument to check permission bits against.
fun access(path: Str) -> Bool =
return exists(path)
# Appends `data` to the file at `path`, creating it (with just `data` as its
# contents) if it doesn't already exist. There's no host-level "open for
# append" primitive, so this reads the existing bytes and writes the
# concatenation back — not atomic, and O(existing file size) per call.
fun appendFile(path: Str, data: Str) -> Result[Bool, Str] =
if exists(path)
match readFile(path)
Ok(existing) =>
return writeFile(path, existing + data)
Err(e) =>
return Err(e)
return writeFile(path, data)
# Unix permission bits only (e.g. `0o644`) — this VM has no Windows ACL
# equivalent to fall back to.
fun chmod(path: Str, mode: Int) -> Result[Bool, Str] =
if rawChmod(path, mode) == 1
return Ok(True)
return Err("failed to chmod: '{path}'")
# There is no host primitive for changing file ownership (it needs a uid/gid
# syscall this VM doesn't expose), unlike the permission-bits-only `chmod`.
fun chown(path: Str, uid: Int, gid: Int) =
todo
fun copyFile(src: Str, dest: Str) -> Result[Bool, Str] =
match readFile(src)
Ok(data) => writeFile(dest, data)
Err(e) => Err(e)
fun cp(src: Str, dest: Str) -> Result[Bool, Str] =
return copyFile(src, dest)
# Symlinks aren't modeled separately from regular files/dirs by this VM's
# host primitives (`rawChmod` always follows them), so this is the same as
# `chmod`.
fun lchmod(path: Str, mode: Int) -> Result[Bool, Str] =
return chmod(path, mode)
# See `chown` — no uid/gid syscall exposed by this VM.
fun lchown(path: Str, uid: Int, gid: Int) =
todo
# Setting a file's atime/mtime needs a syscall (`utimensat`/`SetFileTime`)
# this VM doesn't expose (`std::fs` itself has no equivalent either).
fun lutimes(path: Str, atime: Int, mtime: Int) =
todo
fun link(existing_path: Str, new_path: Str) -> Result[Bool, Str] =
if rawLink(existing_path, new_path) == 1
return Ok(True)
return Err("failed to link '{existing_path}' -> '{new_path}'")
# Would need a real `Stat` struct type (size/mode/mtime/uid/gid/...) to
# return anything useful — no such type exists in the stdlib yet.
fun lstat(path: Str) =
todo
# Creates a fresh directory named `prefix` followed by a random suffix,
# retrying on the (astronomically unlikely) chance of a collision, and
# returns the path actually created.
fun mkdtemp(prefix: Str) -> Result[Str, Str] =
path := prefix + v4().toStr()
if exists(path)
return mkdtemp(prefix)
match mkdir(path)
Ok(_) =>
return Ok(path)
Err(e) =>
return Err(e)
# Would need a host-side open-file-descriptor table (this VM's other file
# ops are all one-shot: open+read/write+close in a single host call) plus a
# `File` handle type — neither exists yet.
fun open(path: Str, flags: Int) =
todo
# See `open` — same missing handle-table/type problem, for directories.
fun opendir(path: Str) =
todo
# The names of `path`'s directory entries (not full paths).
fun readdir(path: Str) -> Result[List[Str], Str] =
if !exists(path)
return Err("no such directory: '{path}'")
# `reject`ing empty names handles both a genuinely empty directory (whose
# joined `""` would otherwise split into one bogus `""` entry) and
# `rawReadDir`'s "unreadable" fallback (already ruled out by the `exists`
# check above) the same way — no directory entry is ever named `""`.
return Ok(rawReadDir(path).split("\n", 0).reject(|n| n == ""))
fun readlink(path: Str) -> Result[Str, Str] =
if !exists(path)
return Err("no such file: '{path}'")
return Ok(rawReadlink(path))
fun realpath(path: Str) -> Result[Str, Str] =
if !exists(path)
return Err("no such file: '{path}'")
return Ok(rawRealpath(path))
fun rename(old_path: Str, new_path: Str) -> Result[Bool, Str] =
if !exists(old_path)
return Err("no such file: '{old_path}'")
if rawRename(old_path, new_path) == 1
return Ok(True)
return Err("failed to rename '{old_path}' -> '{new_path}'")
# Removes an empty directory. (`remove` above covers plain files.)
fun rmdir(path: Str) -> Result[Bool, Str] =
if !exists(path)
return Err("no such directory: '{path}'")
if rawRemoveDir(path) == 1
return Ok(True)
return Err("failed to remove directory: '{path}'")
# See `lstat` — needs a `Stat` struct type.
fun stat(path: Str) =
todo
# Needs a filesystem-info struct type, same gap as `stat`.
fun statfs(path: Str) =
todo
fun symlink(target: Str, path: Str) -> Result[Bool, Str] =
if rawSymlink(target, path) == 1
return Ok(True)
return Err("failed to symlink '{path}' -> '{target}'")
# Resizes the file at `path` to exactly `len` bytes — truncated if shorter,
# zero-padded if longer.
fun truncate(path: Str, len: Int) -> Result[Bool, Str] =
if !exists(path)
return Err("no such file: '{path}'")
if rawTruncate(path, len) == 1
return Ok(True)
return Err("failed to truncate: '{path}'")
# See `lutimes` — same missing syscall.
fun utimes(path: Str, atime: Int, mtime: Int) =
todo
# Would need an async/callback mechanism to deliver filesystem change events
# into a running Plum program — nothing like that exists in this VM (every
# host call here is a synchronous, one-shot request/response).
fun watch(filename: Str) =
todo
test "writeFile/readFile/appendFile/remove round-trip on a real file"
match mkdtemp("/tmp/plum_os_test_")
Ok(dir) =>
path := dir + "/a.txt"
assert writeFile(path, "hello").isOk() == True
assert readFile(path).unwrap() == "hello"
assert appendFile(path, " world").isOk() == True
assert readFile(path).unwrap() == "hello world"
assert exists(path) == True
assert remove(path).isOk() == True
assert exists(path) == False
assert rmdir(dir).isOk() == True
Err(_) =>
assert True == False
test "copyFile/cp/rename move and duplicate file contents correctly"
match mkdtemp("/tmp/plum_os_test_")
Ok(dir) =>
a := dir + "/a.txt"
b := dir + "/b.txt"
c := dir + "/c.txt"
writeFile(a, "data")
assert copyFile(a, b).isOk() == True
assert readFile(b).unwrap() == "data"
assert rename(b, c).isOk() == True
assert exists(b) == False
assert readFile(c).unwrap() == "data"
remove(a)
remove(c)
rmdir(dir)
Err(_) =>
assert True == False
test "readdir lists the entries created in a fresh directory"
match mkdtemp("/tmp/plum_os_test_")
Ok(dir) =>
assert readdir(dir).unwrap().length() == 0
writeFile(dir + "/only.txt", "x")
names := readdir(dir).unwrap()
assert names.length() == 1
assert names.join(",") == "only.txt"
remove(dir + "/only.txt")
rmdir(dir)
Err(_) =>
assert True == False
test "symlink/readlink/realpath resolve a linked file correctly"
match mkdtemp("/tmp/plum_os_test_")
Ok(dir) =>
target := dir + "/target.txt"
link_path := dir + "/link.txt"
writeFile(target, "linked")
assert symlink(target, link_path).isOk() == True
assert readlink(link_path).unwrap() == target
assert readFile(link_path).unwrap() == "linked"
assert realpath(link_path).isOk() == True
remove(link_path)
remove(target)
rmdir(dir)
Err(_) =>
assert True == False
test "truncate resizes a file's contents"
match mkdtemp("/tmp/plum_os_test_")
Ok(dir) =>
path := dir + "/a.txt"
writeFile(path, "hello world")
assert truncate(path, 5).isOk() == True
assert readFile(path).unwrap() == "hello"
remove(path)
rmdir(dir)
Err(_) =>
assert True == False
test "chmod changes a file's permission bits"
match mkdtemp("/tmp/plum_os_test_")
Ok(dir) =>
path := dir + "/a.txt"
writeFile(path, "x")
assert chmod(path, 384).isOk() == True # 0o600
remove(path)
rmdir(dir)
Err(_) =>
assert True == False
test "link creates a second name for the same file"
match mkdtemp("/tmp/plum_os_test_")
Ok(dir) =>
a := dir + "/a.txt"
b := dir + "/b.txt"
writeFile(a, "shared")
assert link(a, b).isOk() == True
assert readFile(b).unwrap() == "shared"
remove(a)
remove(b)
rmdir(dir)
Err(_) =>
assert True == False
test "readFile/remove/rmdir report errors for missing paths"
assert readFile("/tmp/plum_os_test_does_not_exist.txt").isErr() == True
assert remove("/tmp/plum_os_test_does_not_exist.txt").isErr() == True
assert rmdir("/tmp/plum_os_test_does_not_exist_dir").isErr() == True