plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


plum-std/Os.plum
141de54 1
module std
b7071c9 2
import std/List
b7071c9 3
import std/Result
b7071c9 4
import std/Str
b7071c9 5
import std/Uuid
73b5e55 6
import std/Bool
ca5fd6f 7
import std/Number
afb772e 8
141de54 9
# NOTE: the original file modeled `stdin`/`stdout`/`stderr` as bare top-level
141de54 10
# variable bindings and a `File(...)` constructor, but this language has no
141de54 11
# top-level `let` and `File` is never defined anywhere in the stdlib. Reduced
141de54 12
# to the underlying path so this at least parses; a real `File` type is
141de54 13
# still needed here.
0fe3528 14
fun stdin() -> Str =
141de54 15
  "/dev/stdin"
141de54 16
0fe3528 17
fun stdout() -> Str =
141de54 18
  "/dev/stdout"
141de54 19
0fe3528 20
fun stderr() -> Str =
141de54 21
  "/dev/stderr"
afb772e 22
a271f34 23
# Writes the specified data, followed by the current line terminator, to the
a271f34 24
# standard output stream. `extern` means this compiles to a genuine wasm import
a271f34 25
# (`plum::printLn`) instead of a Plum body — printing isn't expressible in wasm
a271f34 26
# on its own; the embedding host (`plum-cli run`/`plum build`) provides it.
a271f34 27
extern fun printLn(s: Str)
a271f34 28
a271f34 29
# Raw host-provided filesystem primitives — each maps to a genuine wasm
a271f34 30
# import backed by a real OS call in the embedder (`plum-cli`/`plum-runtime`),
a271f34 31
# not something expressible in wasm on its own (same idea as `printLn`).
a271f34 32
# Returns `Int` (0/1), not `Bool`, for the success/failure ones — the host
a271f34 33
# closure that satisfies a wasm import runs BEFORE the module is instantiated
a271f34 34
# (it's part of setting up the imports an instance needs), so it has no way
a271f34 35
# to reach the instance's own `True`/`False` singleton globals that `Bool`
a271f34 36
# values are represented as in this codegen; a plain `i64` has no such
a271f34 37
# problem. `rawReadFile` returns `""` for a file that doesn't exist or can't
a271f34 38
# be read (there's no way to signal an error through a bare `Str` return);
a271f34 39
# callers needing to tell "empty file" apart from "missing file" should check
a271f34 40
# `exists` first, which every wrapper below does.
a271f34 41
extern fun rawReadFile(path: Str) -> Str
a271f34 42
extern fun rawWriteFile(path: Str, data: Str) -> Int
a271f34 43
extern fun rawExists(path: Str) -> Int
a271f34 44
extern fun rawMkdir(path: Str) -> Int
a271f34 45
extern fun rawRemove(path: Str) -> Int
2ec05c8 46
extern fun rawRemoveDir(path: Str) -> Int
2ec05c8 47
extern fun rawRename(old_path: Str, new_path: Str) -> Int
2ec05c8 48
extern fun rawTruncate(path: Str, len: Int) -> Int
2ec05c8 49
extern fun rawLink(existing_path: Str, new_path: Str) -> Int
2ec05c8 50
extern fun rawSymlink(target: Str, path: Str) -> Int
2ec05c8 51
extern fun rawReadlink(path: Str) -> Str
2ec05c8 52
extern fun rawRealpath(path: Str) -> Str
2ec05c8 53
extern fun rawChmod(path: Str, mode: Int) -> Int
2ec05c8 54
# Every directory entry's name (not full path), joined with `\n`; `""` for a
2ec05c8 55
# missing/unreadable directory (same ambiguity `rawReadFile` already has with
2ec05c8 56
# an empty file — `exists`/`readdir` below both check existence first).
2ec05c8 57
extern fun rawReadDir(path: Str) -> Str
a271f34 58
a271f34 59
# Reads the whole file at `path` as a `Str` (`Str` is a byte array here — see
a271f34 60
# `libs/std/str.plum` — so this works for binary content too, not just text).
a271f34 61
fun readFile(path: Str) -> Result[Str, Str] =
a271f34 62
  if !exists(path)
a271f34 63
    return Err("no such file: '{path}'")
a271f34 64
  return Ok(rawReadFile(path))
a271f34 65
a271f34 66
# Writes `data` to `path`, replacing its previous contents (or creating it).
a271f34 67
fun writeFile(path: Str, data: Str) -> Result[Bool, Str] =
a271f34 68
  if rawWriteFile(path, data) == 1
a271f34 69
    return Ok(True)
a271f34 70
  return Err("failed to write file: '{path}'")
a271f34 71
a271f34 72
fun exists(path: Str) -> Bool =
a271f34 73
  return rawExists(path) == 1
a271f34 74
a271f34 75
fun mkdir(path: Str) -> Result[Bool, Str] =
a271f34 76
  if rawMkdir(path) == 1
a271f34 77
    return Ok(True)
a271f34 78
  return Err("failed to create directory: '{path}'")
a271f34 79
a271f34 80
# Removes the file (not directory) at `path`.
a271f34 81
fun remove(path: Str) -> Result[Bool, Str] =
a271f34 82
  if !exists(path)
a271f34 83
    return Err("no such file: '{path}'")
a271f34 84
  if rawRemove(path) == 1
a271f34 85
    return Ok(True)
a271f34 86
  return Err("failed to remove: '{path}'")
afb772e 87
2ec05c8 88
# Whether `path` exists and is reachable — this VM has no notion of a
2ec05c8 89
# process' effective uid/gid, so (unlike POSIX `access(2)`) there is no mode
2ec05c8 90
# argument to check permission bits against.
2ec05c8 91
fun access(path: Str) -> Bool =
2ec05c8 92
  return exists(path)
2ec05c8 93
2ec05c8 94
# Appends `data` to the file at `path`, creating it (with just `data` as its
2ec05c8 95
# contents) if it doesn't already exist. There's no host-level "open for
2ec05c8 96
# append" primitive, so this reads the existing bytes and writes the
2ec05c8 97
# concatenation back — not atomic, and O(existing file size) per call.
2ec05c8 98
fun appendFile(path: Str, data: Str) -> Result[Bool, Str] =
2ec05c8 99
  if exists(path)
2ec05c8 100
    match readFile(path)
2ec05c8 101
      Ok(existing) =>
2ec05c8 102
        return writeFile(path, existing + data)
2ec05c8 103
      Err(e) =>
2ec05c8 104
        return Err(e)
2ec05c8 105
  return writeFile(path, data)
2ec05c8 106
2ec05c8 107
# Unix permission bits only (e.g. `0o644`) — this VM has no Windows ACL
2ec05c8 108
# equivalent to fall back to.
2ec05c8 109
fun chmod(path: Str, mode: Int) -> Result[Bool, Str] =
2ec05c8 110
  if rawChmod(path, mode) == 1
2ec05c8 111
    return Ok(True)
2ec05c8 112
  return Err("failed to chmod: '{path}'")
afb772e 113
2ec05c8 114
# There is no host primitive for changing file ownership (it needs a uid/gid
2ec05c8 115
# syscall this VM doesn't expose), unlike the permission-bits-only `chmod`.
0fe3528 116
fun chown(path: Str, uid: Int, gid: Int) =
afb772e 117
  todo
afb772e 118
2ec05c8 119
fun copyFile(src: Str, dest: Str) -> Result[Bool, Str] =
2ec05c8 120
  match readFile(src)
a9a0147 121
    Ok(data) => writeFile(dest, data)
a9a0147 122
    Err(e) => Err(e)
afb772e 123
2ec05c8 124
fun cp(src: Str, dest: Str) -> Result[Bool, Str] =
2ec05c8 125
  return copyFile(src, dest)
afb772e 126
2ec05c8 127
# Symlinks aren't modeled separately from regular files/dirs by this VM's
2ec05c8 128
# host primitives (`rawChmod` always follows them), so this is the same as
2ec05c8 129
# `chmod`.
2ec05c8 130
fun lchmod(path: Str, mode: Int) -> Result[Bool, Str] =
2ec05c8 131
  return chmod(path, mode)
afb772e 132
2ec05c8 133
# See `chown` — no uid/gid syscall exposed by this VM.
0fe3528 134
fun lchown(path: Str, uid: Int, gid: Int) =
afb772e 135
  todo
afb772e 136
2ec05c8 137
# Setting a file's atime/mtime needs a syscall (`utimensat`/`SetFileTime`)
2ec05c8 138
# this VM doesn't expose (`std::fs` itself has no equivalent either).
0fe3528 139
fun lutimes(path: Str, atime: Int, mtime: Int) =
afb772e 140
  todo
afb772e 141
2ec05c8 142
fun link(existing_path: Str, new_path: Str) -> Result[Bool, Str] =
2ec05c8 143
  if rawLink(existing_path, new_path) == 1
2ec05c8 144
    return Ok(True)
2ec05c8 145
  return Err("failed to link '{existing_path}' -> '{new_path}'")
afb772e 146
2ec05c8 147
# Would need a real `Stat` struct type (size/mode/mtime/uid/gid/...) to
2ec05c8 148
# return anything useful — no such type exists in the stdlib yet.
0fe3528 149
fun lstat(path: Str) =
afb772e 150
  todo
afb772e 151
2ec05c8 152
# Creates a fresh directory named `prefix` followed by a random suffix,
2ec05c8 153
# retrying on the (astronomically unlikely) chance of a collision, and
2ec05c8 154
# returns the path actually created.
2ec05c8 155
fun mkdtemp(prefix: Str) -> Result[Str, Str] =
2ec05c8 156
  path := prefix + v4().toStr()
2ec05c8 157
  if exists(path)
2ec05c8 158
    return mkdtemp(prefix)
2ec05c8 159
  match mkdir(path)
2ec05c8 160
    Ok(_) =>
2ec05c8 161
      return Ok(path)
2ec05c8 162
    Err(e) =>
2ec05c8 163
      return Err(e)
2ec05c8 164
2ec05c8 165
# Would need a host-side open-file-descriptor table (this VM's other file
2ec05c8 166
# ops are all one-shot: open+read/write+close in a single host call) plus a
2ec05c8 167
# `File` handle type — neither exists yet.
0fe3528 168
fun open(path: Str, flags: Int) =
afb772e 169
  todo
afb772e 170
2ec05c8 171
# See `open` — same missing handle-table/type problem, for directories.
0fe3528 172
fun opendir(path: Str) =
afb772e 173
  todo
afb772e 174
2ec05c8 175
# The names of `path`'s directory entries (not full paths).
2ec05c8 176
fun readdir(path: Str) -> Result[List[Str], Str] =
2ec05c8 177
  if !exists(path)
2ec05c8 178
    return Err("no such directory: '{path}'")
2ec05c8 179
  # `reject`ing empty names handles both a genuinely empty directory (whose
2ec05c8 180
  # joined `""` would otherwise split into one bogus `""` entry) and
2ec05c8 181
  # `rawReadDir`'s "unreadable" fallback (already ruled out by the `exists`
2ec05c8 182
  # check above) the same way — no directory entry is ever named `""`.
2ec05c8 183
  return Ok(rawReadDir(path).split("\n", 0).reject(|n| n == ""))
2ec05c8 184
2ec05c8 185
fun readlink(path: Str) -> Result[Str, Str] =
2ec05c8 186
  if !exists(path)
2ec05c8 187
    return Err("no such file: '{path}'")
2ec05c8 188
  return Ok(rawReadlink(path))
afb772e 189
2ec05c8 190
fun realpath(path: Str) -> Result[Str, Str] =
2ec05c8 191
  if !exists(path)
2ec05c8 192
    return Err("no such file: '{path}'")
2ec05c8 193
  return Ok(rawRealpath(path))
afb772e 194
2ec05c8 195
fun rename(old_path: Str, new_path: Str) -> Result[Bool, Str] =
2ec05c8 196
  if !exists(old_path)
2ec05c8 197
    return Err("no such file: '{old_path}'")
2ec05c8 198
  if rawRename(old_path, new_path) == 1
2ec05c8 199
    return Ok(True)
2ec05c8 200
  return Err("failed to rename '{old_path}' -> '{new_path}'")
afb772e 201
a271f34 202
# Removes an empty directory. (`remove` above covers plain files.)
2ec05c8 203
fun rmdir(path: Str) -> Result[Bool, Str] =
2ec05c8 204
  if !exists(path)
2ec05c8 205
    return Err("no such directory: '{path}'")
2ec05c8 206
  if rawRemoveDir(path) == 1
2ec05c8 207
    return Ok(True)
2ec05c8 208
  return Err("failed to remove directory: '{path}'")
afb772e 209
2ec05c8 210
# See `lstat` — needs a `Stat` struct type.
0fe3528 211
fun stat(path: Str) =
afb772e 212
  todo
afb772e 213
2ec05c8 214
# Needs a filesystem-info struct type, same gap as `stat`.
0fe3528 215
fun statfs(path: Str) =
afb772e 216
  todo
afb772e 217
2ec05c8 218
fun symlink(target: Str, path: Str) -> Result[Bool, Str] =
2ec05c8 219
  if rawSymlink(target, path) == 1
2ec05c8 220
    return Ok(True)
2ec05c8 221
  return Err("failed to symlink '{path}' -> '{target}'")
afb772e 222
2ec05c8 223
# Resizes the file at `path` to exactly `len` bytes — truncated if shorter,
2ec05c8 224
# zero-padded if longer.
2ec05c8 225
fun truncate(path: Str, len: Int) -> Result[Bool, Str] =
2ec05c8 226
  if !exists(path)
2ec05c8 227
    return Err("no such file: '{path}'")
2ec05c8 228
  if rawTruncate(path, len) == 1
2ec05c8 229
    return Ok(True)
2ec05c8 230
  return Err("failed to truncate: '{path}'")
afb772e 231
2ec05c8 232
# See `lutimes` — same missing syscall.
0fe3528 233
fun utimes(path: Str, atime: Int, mtime: Int) =
afb772e 234
  todo
afb772e 235
2ec05c8 236
# Would need an async/callback mechanism to deliver filesystem change events
2ec05c8 237
# into a running Plum program — nothing like that exists in this VM (every
2ec05c8 238
# host call here is a synchronous, one-shot request/response).
0fe3528 239
fun watch(filename: Str) =
afb772e 240
  todo
2ec05c8 241
2ec05c8 242
test "writeFile/readFile/appendFile/remove round-trip on a real file"
2ec05c8 243
  match mkdtemp("/tmp/plum_os_test_")
2ec05c8 244
    Ok(dir) =>
2ec05c8 245
      path := dir + "/a.txt"
a9a0147 246
      assert writeFile(path, "hello").isOk() == True
a9a0147 247
      assert readFile(path).unwrap() == "hello"
a9a0147 248
      assert appendFile(path, " world").isOk() == True
a9a0147 249
      assert readFile(path).unwrap() == "hello world"
a9a0147 250
      assert exists(path) == True
a9a0147 251
      assert remove(path).isOk() == True
a9a0147 252
      assert exists(path) == False
a9a0147 253
      assert rmdir(dir).isOk() == True
2ec05c8 254
    Err(_) =>
a9a0147 255
      assert True == False
2ec05c8 256
2ec05c8 257
test "copyFile/cp/rename move and duplicate file contents correctly"
2ec05c8 258
  match mkdtemp("/tmp/plum_os_test_")
2ec05c8 259
    Ok(dir) =>
2ec05c8 260
      a := dir + "/a.txt"
2ec05c8 261
      b := dir + "/b.txt"
2ec05c8 262
      c := dir + "/c.txt"
2ec05c8 263
      writeFile(a, "data")
a9a0147 264
      assert copyFile(a, b).isOk() == True
a9a0147 265
      assert readFile(b).unwrap() == "data"
a9a0147 266
      assert rename(b, c).isOk() == True
a9a0147 267
      assert exists(b) == False
a9a0147 268
      assert readFile(c).unwrap() == "data"
2ec05c8 269
      remove(a)
2ec05c8 270
      remove(c)
2ec05c8 271
      rmdir(dir)
2ec05c8 272
    Err(_) =>
a9a0147 273
      assert True == False
2ec05c8 274
2ec05c8 275
test "readdir lists the entries created in a fresh directory"
2ec05c8 276
  match mkdtemp("/tmp/plum_os_test_")
2ec05c8 277
    Ok(dir) =>
a9a0147 278
      assert readdir(dir).unwrap().length() == 0
2ec05c8 279
      writeFile(dir + "/only.txt", "x")
2ec05c8 280
      names := readdir(dir).unwrap()
a9a0147 281
      assert names.length() == 1
a9a0147 282
      assert names.join(",") == "only.txt"
2ec05c8 283
      remove(dir + "/only.txt")
2ec05c8 284
      rmdir(dir)
2ec05c8 285
    Err(_) =>
a9a0147 286
      assert True == False
2ec05c8 287
2ec05c8 288
test "symlink/readlink/realpath resolve a linked file correctly"
2ec05c8 289
  match mkdtemp("/tmp/plum_os_test_")
2ec05c8 290
    Ok(dir) =>
2ec05c8 291
      target := dir + "/target.txt"
2ec05c8 292
      link_path := dir + "/link.txt"
2ec05c8 293
      writeFile(target, "linked")
a9a0147 294
      assert symlink(target, link_path).isOk() == True
a9a0147 295
      assert readlink(link_path).unwrap() == target
a9a0147 296
      assert readFile(link_path).unwrap() == "linked"
a9a0147 297
      assert realpath(link_path).isOk() == True
2ec05c8 298
      remove(link_path)
2ec05c8 299
      remove(target)
2ec05c8 300
      rmdir(dir)
2ec05c8 301
    Err(_) =>
a9a0147 302
      assert True == False
2ec05c8 303
2ec05c8 304
test "truncate resizes a file's contents"
2ec05c8 305
  match mkdtemp("/tmp/plum_os_test_")
2ec05c8 306
    Ok(dir) =>
2ec05c8 307
      path := dir + "/a.txt"
2ec05c8 308
      writeFile(path, "hello world")
a9a0147 309
      assert truncate(path, 5).isOk() == True
a9a0147 310
      assert readFile(path).unwrap() == "hello"
2ec05c8 311
      remove(path)
2ec05c8 312
      rmdir(dir)
2ec05c8 313
    Err(_) =>
a9a0147 314
      assert True == False
2ec05c8 315
2ec05c8 316
test "chmod changes a file's permission bits"
2ec05c8 317
  match mkdtemp("/tmp/plum_os_test_")
2ec05c8 318
    Ok(dir) =>
2ec05c8 319
      path := dir + "/a.txt"
2ec05c8 320
      writeFile(path, "x")
a9a0147 321
      assert chmod(path, 384).isOk() == True # 0o600
2ec05c8 322
      remove(path)
2ec05c8 323
      rmdir(dir)
2ec05c8 324
    Err(_) =>
a9a0147 325
      assert True == False
2ec05c8 326
2ec05c8 327
test "link creates a second name for the same file"
2ec05c8 328
  match mkdtemp("/tmp/plum_os_test_")
2ec05c8 329
    Ok(dir) =>
2ec05c8 330
      a := dir + "/a.txt"
2ec05c8 331
      b := dir + "/b.txt"
2ec05c8 332
      writeFile(a, "shared")
a9a0147 333
      assert link(a, b).isOk() == True
a9a0147 334
      assert readFile(b).unwrap() == "shared"
2ec05c8 335
      remove(a)
2ec05c8 336
      remove(b)
2ec05c8 337
      rmdir(dir)
2ec05c8 338
    Err(_) =>
a9a0147 339
      assert True == False
2ec05c8 340
2ec05c8 341
test "readFile/remove/rmdir report errors for missing paths"
a9a0147 342
  assert readFile("/tmp/plum_os_test_does_not_exist.txt").isErr() == True
a9a0147 343
  assert remove("/tmp/plum_os_test_does_not_exist.txt").isErr() == True
a9a0147 344
  assert rmdir("/tmp/plum_os_test_does_not_exist_dir").isErr() == True