File I/O

Luma treats files as values that reference paths, not as open handles.
Creating a file value never opens the file or touches the filesystem.
All file operations are explicit, atomic, and predictable.

Creating a file reference

f: file = file("data.txt")

This creates a reference to a path.
No file is opened, created, or validated at this point.

You can safely create references to files that do not exist yet.

Methods summary

Method Arguments Returns Description
read() str, ?str, [str], ?[str], [byte], ?[byte] Read entire file (type-based dispatch)
read_at() offset: int, length: int [byte] or str Read bytes at offset (type-based dispatch)
write() content: str or data: [byte] Replace file contents
write_at() offset: int, content: str or data: [byte] Write at offset without truncating
add() line: str Append text with newline
append() text: str Append raw text without newline
truncate() size: int Set file size in bytes
exists() bool Check if file exists
size() int File size in bytes
name() str File name (e.g. "report.txt")
ext() str Extension (e.g. ".txt")
path() str Full path as provided
dir() str Directory portion of path
create() Create empty file
delete() Delete file
copy() dest: str Copy file to destination
move() dest: str Move or rename file
lock() Acquire exclusive file lock
unlock() Release file lock

Reading files

Luma uses type-based dispatch for reading files.
The same method (read()) adapts its behavior based on the declared result type.

Read entire file as text

content: str = file("config.txt").read()
  • Reads the entire file as a string
  • Panics if the file does not exist

Use this when the file is required.

Read entire file as optional text

content: ?str = file("config.txt").read()
if content != nil {
    print(content)
}
  • Returns nil if the file does not exist
  • Never panics

Use this for optional configuration or cache files.

Read file as lines

lines: [str] = file("data.txt").read()
lines.walk(i, line) -> {
    print(line)
}
  • Splits on line breaks
  • Handles both Unix and Windows line endings
  • Panics if the file does not exist

Use this for logs, CSV files, and line-oriented data.

Read file as optional lines

lines: ?[str] = file("data.txt").read()
if lines != nil {
    print(lines.len())
}
  • Returns nil if the file does not exist

Read file as bytes

data: [byte] = file("image.png").read()
print(data.len())
  • Reads raw binary data
  • Panics if the file does not exist

Use this for binary formats, media files, or protocol data.

Read file as optional bytes

data: ?[byte] = file("image.png").read()
if data != nil {
    // process bytes
}

Writing files

Write text (replace content)

file("output.txt").write("Hello, world")
  • Creates the file if it does not exist
  • Replaces existing content if it does

Write bytes (replace content)

data: [byte] = [0x48, 0x65, 0x6C, 0x6C, 0x6F]
file("output.bin").write(data)

Use this for binary output.

Add a line (append with newline)

log: file = file("app.log")
log.add("Application started")
log.add("User logged in")
  • Appends text
  • Automatically adds a newline
  • Creates the file if it does not exist

This is the recommended way to write logs.

Append raw data (no newline)

log.append("[CONTINUED] ")
log.add("Recovered after restart")

Use this only when you explicitly want to control formatting.

File information

Check existence

if file("data.txt").exists() {
    print("File exists")
}

Get file size

size: int = file("large.dat").size()

Returns size in bytes.
Panics if the file does not exist.

Get file name

name: str = file("docs/report.txt").name()  // "report.txt"

Get file extension

ext: str = file("image.png").ext()  // ".png"

Returns an empty string if there is no extension.

Get full path

path: str = file("data/file.txt").path()

Returns the path exactly as provided to file().

Get directory path

dir: str = file("docs/report.txt").dir()  // "docs"

File actions

Create file

file("new.txt").create()
  • Creates an empty file
  • Safe to call even if the file already exists

Delete file

file("temp.txt").delete()

Panics if the file does not exist.

Copy file

file("document.txt").copy("backup/document.txt")
  • Creates destination if needed
  • Overwrites destination if it exists

Move (rename) file

file("old.txt").move("new.txt")

Note: after moving, the original file reference still points to the old path.

Pattern matching with glob

files: [file] = glob("logs/*.log")

Supported patterns include:

  • * – any sequence of characters
  • ? – any single character
  • [abc] – character classes
  • [a-z] – character ranges
  • ** – recursive directory matching

Examples

glob("*.txt")
glob("logs/*.log")
glob("**/*.luma")
glob("data[0-9].txt")

If no files match, an empty list is returned.

Random access I/O

Standard read() and write() operate on the entire file.
Random access methods let you read and write at specific byte offsets — useful for binary formats, paged storage engines, and database files.

Read at offset

f: file = file("data.bin")
data: [byte] = f.read_at(4096, 512)   // read 512 bytes starting at offset 4096

Like read(), read_at() uses type-based dispatch:

chunk: [byte] = f.read_at(0, 100)     // returns byte slice
text: str     = f.read_at(0, 100)     // returns string
  • Panics if the file cannot be opened
  • Returns fewer bytes if the file is shorter than offset + length

Write at offset

f: file = file("data.bin")
f.write_at(3, "BBB")                  // write string at byte offset 3

write_at() dispatches on the second argument type, just like write():

f.write_at(0, "HEADER")               // write string at offset
f.write_at(0, data)                    // write byte slice at offset
  • Creates the file if it does not exist
  • Does not truncate — only overwrites bytes at the given offset
  • The file may grow if writing past the current end

Truncate

f.truncate(8192)                       // set file size to 8192 bytes
  • Shrinks or extends the file to the given size
  • If extending, the new bytes are zero-filled

Example: patch bytes in a file

f: file = file("record.dat")
f.write("AAAAAAAAAA")
f.write_at(3, "BBB")
print(f.read())     // "AAABBBAAAA"

f.truncate(5)
print(f.read())     // "AAABB"

File locking

File locks prevent concurrent processes from corrupting shared data.
Luma provides exclusive locking via lock() and unlock().

f: file = file("shared.db")
f.lock()
// ... safe to read and write ...
f.unlock()
  • lock() acquires an exclusive lock (blocks until acquired)
  • unlock() releases the lock
  • Calling lock() on an already-locked file panics
  • Calling unlock() on an unlocked file panics

Locking uses flock and is available on Linux and macOS.

Common patterns

Safe configuration loading

fn load_config(path: str) -> str {
    config: ?str = file(path).read()
    if config != nil {
        return config
    }
    return "default=value"
}

Process multiple files

files: [file] = glob("data/*.txt")
files.walk(i, f) -> {
    print(f.name())
}

Ensure file exists

f: file = file("settings.txt")
if !f.exists() {
    f.create()
    f.write("default settings")
}

Planned features

The following methods are planned but not yet implemented:

created() -> date   // file creation time
updated() -> date   // last modification time

These will become available once Luma introduces a date type.

Design notes

  • Files are path references, not open handles
  • All operations are atomic (open → operate → close) — except lock()/unlock(), which hold an open file descriptor between calls
  • Error handling uses optional types instead of exceptions
  • No explicit open/close or streaming APIs are required
  • Random access methods (read_at, write_at, truncate) follow the same stateless pattern as all other file methods
  • File locking is Unix-only (Linux and macOS); Windows support is planned
  • The common case remains simple, while rare cases stay possible
Last updated on