Skip to content

Commit

Permalink
Implement listing directories in Inko
Browse files Browse the repository at this point in the history
std::fs::dir.list is now located at std::fs::path::Path.list, and is
implemented entirely using Inko, instead of relying on runtime library
functions. This requires some OS specific bits, but helps us decouple
Inko more from Rust, and avoids the need for heap allocating Rust
structures just to work around its lack of a stable ABI.

As part of these changes, the return type is also changed: instead of
returning Array[Path], Path.list returns in iterator that yields
DirectoryEntry values, containing both a path and a file type.

This commit also introduces the module std::fs, containing general file
system types such as FileType and DirectoryEntry. This module is meant
for types that aren't really "owned" by more specific modules, such as
std::fs::path.

The internal bits are currently marked as public in a few places, as we
currently lack a notion of "namespace private" or "project private"
types/methods, but this will probably change in the future.

We also introduce some syntax and FFI changes: identifiers may now
contain dollar signs, as AMD64 macOS code needs to use e.g.
opendir$INODE64() instead of regular opendir(). The compiler code is
tweaked here and there to correctly compile calls such as module.foo()
where foo() is an external method, something that didn't work before.

This fixes #517.

Changelog: changed
  • Loading branch information
yorickpeterse committed Jul 10, 2023
1 parent 3e3da16 commit 0c963af
Show file tree
Hide file tree
Showing 25 changed files with 527 additions and 143 deletions.
5 changes: 4 additions & 1 deletion ast/src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const SPACE: u8 = 32;
const EXCLAMATION: u8 = 33;
const DOUBLE_QUOTE: u8 = 34;
const HASH: u8 = 35;
const DOLLAR: u8 = 36;
const PERCENT: u8 = 37;
const AMPERSAND: u8 = 38;
const SINGLE_QUOTE: u8 = 39;
Expand Down Expand Up @@ -1226,7 +1227,8 @@ impl Lexer {
ZERO..=NINE
| LOWER_A..=LOWER_Z
| UPPER_A..=UPPER_Z
| UNDERSCORE => self.position += 1,
| UNDERSCORE
| DOLLAR => self.position += 1,
QUESTION => {
self.position += 1;
break;
Expand Down Expand Up @@ -2086,6 +2088,7 @@ mod tests {
#[test]
fn test_lexer_identifiers() {
assert_token!("foo", Identifier, "foo", 1..=1, 1..=3);
assert_token!("foo$bar", Identifier, "foo$bar", 1..=1, 1..=7);
assert_token!("baz", Identifier, "baz", 1..=1, 1..=3);
assert_token!("foo_bar", Identifier, "foo_bar", 1..=1, 1..=7);
assert_token!("foo_BAR", Identifier, "foo_BAR", 1..=1, 1..=7);
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/llvm/layouts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ impl<'ctx> Layouts<'ctx> {
}

for mod_id in mir.modules.keys() {
for &method in mod_id.extern_methods(db) {
for &method in mod_id.extern_methods(db).values() {
let mut args: Vec<BasicMetadataTypeEnum> =
Vec::with_capacity(method.number_of_arguments(db) + 1);

Expand Down
46 changes: 22 additions & 24 deletions compiler/src/mir/passes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1861,33 +1861,31 @@ impl<'a> LowerMethod<'a> {

reg
}
types::Receiver::Class(id) => {
// These methods can't move, be dynamic or async, so we can skip
// the code that comes after this.
if info.id.is_extern(self.db()) {
self.current_block_mut()
.call_extern(result, info.id, arguments, location);

if info.id.return_type(self.db()).is_never(self.db()) {
self.add_current_block();
} else if !info.id.has_return_type(self.db()) {
self.current_block_mut().nil_literal(result, location);
}
types::Receiver::Extern => {
self.current_block_mut()
.call_extern(result, info.id, arguments, location);

// We don't reduce for extern calls for two reasons:
//
// 1. They are typically exposed through regular Inko
// methods, which would result in two reductions per call
// instead of one.
// 2. This allows us to check `errno` after a call, without
// having to worry about the process being rescheduled in
// between the call and the check.
} else {
self.current_block_mut()
.call_static(result, id, info.id, arguments, location);
self.reduce_call(info.returns, location);
if info.id.return_type(self.db()).is_never(self.db()) {
self.add_current_block();
} else if !info.id.has_return_type(self.db()) {
self.current_block_mut().nil_literal(result, location);
}

// We don't reduce for extern calls for two reasons:
//
// 1. They are typically exposed through regular Inko
// methods, which would result in two reductions per call
// instead of one.
// 2. This allows us to check `errno` after a call, without
// having to worry about the process being rescheduled in
// between the call and the check.
return result;
}
types::Receiver::Class(id) => {
self.current_block_mut()
.call_static(result, id, info.id, arguments, location);
self.reduce_call(info.returns, location);

return result;
}
};
Expand Down
21 changes: 11 additions & 10 deletions compiler/src/type_check/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2538,7 +2538,7 @@ impl<'a> CheckMethodBody<'a> {
match rec_id.lookup_method(self.db(), name, module, false) {
MethodLookup::Ok(method) => {
let rec_info =
Receiver::class_or_implicit(self.db(), method);
Receiver::without_receiver(self.db(), method);

(rec, rec_id, rec_info, method)
}
Expand Down Expand Up @@ -2594,7 +2594,7 @@ impl<'a> CheckMethodBody<'a> {
(
TypeRef::module(id),
TypeId::Module(id),
Receiver::Class(id.class(self.db())),
Receiver::with_module(self.db(), id, method),
method,
)
} else {
Expand Down Expand Up @@ -2660,6 +2660,9 @@ impl<'a> CheckMethodBody<'a> {
let rec_id = rec.type_id(self.db()).unwrap();

match rec_id.lookup_method(self.db(), name, module, true) {
MethodLookup::Ok(method) if method.is_extern(self.db()) => {
(rec, rec_id, Receiver::Extern, method)
}
MethodLookup::Ok(method) => {
self.check_if_self_is_allowed(scope, &node.location);

Expand All @@ -2668,7 +2671,7 @@ impl<'a> CheckMethodBody<'a> {
}

let rec_info =
Receiver::class_or_implicit(self.db(), method);
Receiver::without_receiver(self.db(), method);

(rec, rec_id, rec_info, method)
}
Expand Down Expand Up @@ -2712,7 +2715,7 @@ impl<'a> CheckMethodBody<'a> {
(
TypeRef::module(id),
TypeId::Module(id),
Receiver::Class(id.class(self.db())),
Receiver::with_module(self.db(), id, method),
method,
)
} else {
Expand Down Expand Up @@ -3314,8 +3317,7 @@ impl<'a> CheckMethodBody<'a> {
call.check_sendable(self.state, &node.location);

let returns = call.return_type;

let rec_info = Receiver::class_or_explicit(self.db(), receiver);
let rec_info = Receiver::with_receiver(self.db(), receiver, method);

node.kind = CallKind::Call(CallInfo {
id: method,
Expand Down Expand Up @@ -3643,8 +3645,7 @@ impl<'a> CheckMethodBody<'a> {
call.check_sendable(self.state, &node.location);

let returns = call.return_type;

let rec_info = Receiver::class_or_explicit(self.db(), receiver);
let rec_info = Receiver::with_receiver(self.db(), receiver, method);

node.kind = CallKind::Call(CallInfo {
id: method,
Expand Down Expand Up @@ -3675,7 +3676,7 @@ impl<'a> CheckMethodBody<'a> {
}

let rec_info =
Receiver::class_or_implicit(self.db(), method);
Receiver::without_receiver(self.db(), method);

(rec_info, rec, rec_id, method)
}
Expand Down Expand Up @@ -3708,7 +3709,7 @@ impl<'a> CheckMethodBody<'a> {
let mod_typ = TypeRef::Owned(id);

(
Receiver::Class(mod_id.class(self.db())),
Receiver::with_module(self.db(), mod_id, method),
mod_typ,
id,
method,
Expand Down
10 changes: 8 additions & 2 deletions docs/source/guides/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,14 @@ Identifiers are referred to by just using their name:
this_is_an_identifier
```

Identifiers are limited to ASCII, though they may end in a `?` (used for
predicate methods):
Identifiers can contain the following characters: `a-z`, `A-Z`, `0-9`, `_`, `$`,
and may end with a `?`.

!!! warning
While identifiers can contain dollar signs (e.g. `foo$bar`), this is only
supported to allow use of C functions using a `$` in the name, such as
`readdir$INODE64` on macOS. **Do not** use dollar signs in regular Inko
method or variable names.

The `self` keyword is used to refer to the receiver of a method. This keyword is
available in all methods, including module and static methods.
Expand Down
31 changes: 1 addition & 30 deletions rt/src/runtime/fs.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::mem::{Array, Bool, ByteArray, Float, Int, String as InkoString};
use crate::mem::{Bool, ByteArray, Float, Int, String as InkoString};
use crate::process::ProcessPointer;
use crate::result::Result as InkoResult;
use crate::runtime::helpers::read_into;
Expand Down Expand Up @@ -307,35 +307,6 @@ pub unsafe extern "system" fn inko_directory_remove_recursive(
.unwrap_or_else(InkoResult::io_error)
}

#[no_mangle]
pub unsafe extern "system" fn inko_directory_list(
state: *const State,
process: ProcessPointer,
path: *const InkoString,
) -> InkoResult {
let state = &*state;
let mut paths = Vec::new();
let entries =
match process.blocking(|| fs::read_dir(InkoString::read(path))) {
Ok(entries) => entries,
Err(err) => return InkoResult::io_error(err),
};

for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(err) => return InkoResult::io_error(err),
};

let path = entry.path().to_string_lossy().to_string();
let pointer = InkoString::alloc(state.string_class, path);

paths.push(pointer as *mut u8);
}

InkoResult::ok(Array::alloc(state.array_class, paths) as _)
}

unsafe fn open_file(
process: ProcessPointer,
options: OpenOptions,
Expand Down
77 changes: 77 additions & 0 deletions std/src/std/fs.inko
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# General types for filesystem operations.
import std::cmp::Equal
import std::fmt::(Format, Formatter)
import std::fs::path::Path

# A type describing the type of file, such as a file or directory.
class pub enum FileType {
# The file is a file.
case File

# The file is a directory.
case Directory

# The file is a symbolic link.
case SymbolicLink

# The file is something else that isn't explicitly covered by this type.
case Other
}

impl Equal[FileType] for FileType {
fn pub ==(other: ref FileType) -> Bool {
match (self, other) {
case (File, File) -> true
case (Directory, Directory) -> true
case (SymbolicLink, SymbolicLink) -> true
case (Other, Other) -> true
case _ -> false
}
}
}

impl Format for FileType {
fn pub fmt(formatter: mut Formatter) {
let write = match self {
case File -> 'File'
case Directory -> 'Directory'
case SymbolicLink -> 'SymbolicLink'
case Other -> 'Other'
}

formatter.write(write)
}
}

# An entry in a directory.
#
# Instances of this type are typically produced by `std::fs::path::Path.list`,
# so see that method's documentation for additional details.
class pub DirectoryEntry {
# The path to the entry.
let pub @path: Path

# The type of this entry.
let pub @type: FileType
}

impl Equal[DirectoryEntry] for DirectoryEntry {
fn pub ==(other: ref DirectoryEntry) -> Bool {
@path == other.path and @type == other.type
}
}

impl Format for DirectoryEntry {
fn pub fmt(formatter: mut Formatter) {
formatter.write('DirectoryEntry { ')

formatter.descend fn {
formatter.write('@path = ')
@path.fmt(formatter)
formatter.write(', @type = ')
@type.fmt(formatter)
}

formatter.write(' }')
}
}
34 changes: 0 additions & 34 deletions std/src/std/fs/dir.inko
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@ fn extern inko_directory_create_recursive(
path: String,
) -> AnyResult

fn extern inko_directory_list(
state: Pointer[Int8],
process: Pointer[Int8],
path: String,
) -> AnyResult

fn extern inko_directory_remove(
process: Pointer[Int8],
path: String,
Expand Down Expand Up @@ -128,31 +122,3 @@ fn pub remove_all(path: ref ToString) -> Result[Nil, Error] {
case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e as Int))
}
}

# Returns an `Array` containing the paths to the contents of the directory.
#
# # Errors
#
# This method returns an `Error` if any of the following conditions are met:
#
# 1. The user lacks the necessary permissions to read the contents of the
# directory.
# 2. The path does not point to a valid directory.
#
# # Examples
#
# Listing the contents of a directory:
#
# import std::fs::dir
#
# let paths = dir.list('.').unwrap
#
# paths[0].to_string # => 'README.md'
fn pub list(path: ref ToString) -> Result[Array[Path], Error] {
match inko_directory_list(_INKO.state, _INKO.process, path.to_string) {
case { @tag = 0, @value = v } -> Result.Ok(
(v as Array[String]).into_iter.map fn (path) { Path.new(path) }.to_array
)
case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e as Int))
}
}
Loading

0 comments on commit 0c963af

Please sign in to comment.