Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement remaining File methods. #177

Merged
merged 2 commits into from
Mar 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 35 additions & 16 deletions spec/myst/file_spec.mt
Original file line number Diff line number Diff line change
@@ -1,28 +1,47 @@
require "stdlib/spec.mt"


describe("File#open") do
it("returns a new File instance for the given file") do
file = File.open("spec/support/misc/fixed_size_file.txt", "r")
describe("File") do
describe(".open") do
it("returns a new File instance for the given file") do
file = File.open("spec/support/misc/fixed_size_file.txt", "r")

assert(file.type.to_s).equals("File")
assert(file.type.to_s).equals("File")
end

it("defaults to 'read' mode if no mode is given") do
file = File.open("spec/support/misc/fixed_size_file.txt")
assert(file.mode).equals("r")
end
end

it("defaults to 'read' mode if no mode is given") do
file = File.open("spec/support/misc/fixed_size_file.txt")
assert(file.mode).equals("r")

describe("#close") do
# TODO: test that the file descriptor is properly closed.
end
end

describe("File#close") do
# TODO: test that the file descriptor is properly closed.
end
describe("#size") do
it("returns an Integer of the number of bytes the file contains") do
# `fixed_size_file.txt` should always be 63 bytes in size.
file = %File{"spec/support/misc/fixed_size_file.txt", "r"}

assert(file.size).equals(63)
end
end

describe("#mode") do
it("returns the mode string that the file was opened with") do
file = %File{"spec/support/misc/fixed_size_file.txt", "r"}

assert(file.mode).equals("r")
end
end

describe("File#size") do
it("returns an Integer of the number of bytes the file contains") do
# `fixed_size_file.txt` should always be 63 bytes in size.
file = %File{"spec/support/misc/fixed_size_file.txt", "r"}
describe("#path") do
it("returns the path string given when opening the file") do
file = %File{"spec/support/misc/fixed_size_file.txt", "r"}

assert(file.size).equals(63)
assert(file.path).equals("spec/support/misc/fixed_size_file.txt")
end
end
end
41 changes: 41 additions & 0 deletions src/myst/interpreter/native_lib.cr
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,47 @@ module Myst
end
end

# Generate the code needed to create a passthrough method for some native
# Crystal method. `call` is a `Call` node representing the structure of the
# method being wrapped. For example:
#
# NativeLib.passthrough File.basename(path : String)
#
# This will generate the code:
#
# NativeLib.method :passthrough_File_basename, MTValue, path : String do
# File.basename(path)
# end
#
# If a method is expected to return nil, set `return_nil` to true. This will
# ensure that the method _explicitly_ returns Myst's `TNil` value.
#
# If the native method may raise an error, set `may_raise` to the type of the
# expected errors. This will ensure that any raised error is captured and
# instead raised as a Myst runtime error (meaning users will be able to catch
# it at runtime). Errors that are not of the given type will _not_ be captured
# and will be shown as Interpreter Errors to the user.
#
# Note that using this method _requires_ that the given Call provide type
# restrictions for every argument.
macro passthrough(call, return_nil=false, may_raise=nil)
NativeLib.method :passthrough_{{call.receiver}}_{{call.name}}, MTValue, {{*call.args}} do
{{call.receiver}}.{{call.name}}({{ *call.args.map{ |a| a.var } }})
{% if return_nil %}
TNil.new
{% end %}

{% begin %}
{% if may_raise %}
rescue ex : {{may_raise}}
__raise_runtime_error(ex.message || "Unknown error in native method `{{call.name}}`")
{% end %}
{% end %}

end

end

macro def_method(type, name, impl_name)
{{type}}.scope["{{name.id}}"] = TFunctor.new("{{name.id}}", [
->{{impl_name.id}}(MTValue, Array(MTValue), TFunctor?).as(Callable)
Expand Down
115 changes: 112 additions & 3 deletions src/myst/interpreter/native_lib/file.cr
Original file line number Diff line number Diff line change
@@ -1,11 +1,96 @@
module Myst
class Interpreter
NativeLib.method :file_init, TInstance, name : String, mode : String do
file = File.open(name, mode)
NativeLib.passthrough(File.basename(path : String))
NativeLib.passthrough(File.chmod(path : String, mode : Int64), return_nil: true)
NativeLib.passthrough(File.delete(path : String), return_nil: true, may_raise: Errno)
NativeLib.passthrough(File.directory?(path : String))
NativeLib.passthrough(File.dirname(path : String))
NativeLib.passthrough(File.empty?(path : String))
NativeLib.passthrough(File.executable?(path : String))
NativeLib.passthrough(File.exists?(path : String))
NativeLib.passthrough(File.expand_path(path : String))
NativeLib.passthrough(File.extname(path : String))
NativeLib.passthrough(File.file?(path : String))
NativeLib.passthrough(File.read(path : String))
NativeLib.passthrough(File.readable?(path : String))
NativeLib.passthrough(File.real_path(path : String))
NativeLib.passthrough(File.symlink?(path : String))
NativeLib.passthrough(File.writable?(path : String))
NativeLib.passthrough(File.touch(path : String), return_nil: true)

# TODO: For some weird reason, generating these with the
# `NativeLib.passthrough` macro causes a compile error saying "undefined
# method 'check_no_null_byte' for Nil" for the arguments, even though they
# are being restricted to just `String`.
NativeLib.method :passthrough_File_link, MTValue, old_path : String, new_path : String do
File.link(old_path, new_path)
TNil.new
end

NativeLib.method :passthrough_File_rename, MTValue, old_name : String, new_name : String do
File.rename(old_name, new_name)
TNil.new
end

NativeLib.method :passthrough_File_symlink, MTValue, old_path : String, new_path : String do
File.symlink(old_path, new_path)
TNil.new
end

NativeLib.method :passthrough_File_write, MTValue, path : String, content : String do
File.write(path, content)
TNil.new
end

# TODO: This method cannot currently be generated as a passthrough because
# it returns a `UInt64`. Currently, it has to manually be cast into an
# `Int64`, because Myst does not have an unsigned numeric type.
# This may also cause a casting error with very-large files.
NativeLib.method :passthrough_File_size, MTValue, path : String do
File.size(path).to_i64
end

NativeLib.method :static_File_join, MTValue do
# Crystal's `File.join` expects the arguments to be "string-like". To
# match this, the given arguments need to be "casted" into Strings. Any
# non-string arguments
string_args = __args.map do |arg|
case arg
when String
arg
else
arg_string = NativeLib.call_func_by_name(self, arg, "to_s", [] of MTValue)
__raise_runtime_error("`File.join` expects only String arguments, but was given `#{arg_string}` (#{__typeof(arg).name})")
end
end

File.join(string_args)
end


NativeLib.method :static_File_each_line, MTValue, file_name : String do
if block
File.each_line(file_name) do |line|
NativeLib.call_func(self, block, [line] of MTValue, nil)
end
end

TNil.new
end

NativeLib.method :static_File_lines, MTValue, file_name : String do
TList.new(File.each_line(file_name).map{ |l| l.as(MTValue) }.to_a)
end



NativeLib.method :file_init, TInstance, path : String, mode : String do
file = File.open(path, mode)
@fd_pool[file.fd] = file

this.ivars["@fd"] = file.fd.to_i64
this.ivars["@path"] = path
this.ivars["@mode"] = mode
this.ivars["@fd"] = file.fd.to_i64
this
end

Expand All @@ -24,10 +109,34 @@ module Myst
end



def init_file(kernel : TModule, fd_type : TType)
file_type = TType.new("File", kernel.scope, fd_type)
kernel.scope["File"] = file_type

NativeLib.def_method(file_type, :basename, :passthrough_File_basename)
NativeLib.def_method(file_type, :chmod, :passthrough_File_chmod)
NativeLib.def_method(file_type, :delete, :passthrough_File_delete)
NativeLib.def_method(file_type, :directory?, :passthrough_File_directory?)
NativeLib.def_method(file_type, :dirname, :passthrough_File_dirname)
NativeLib.def_method(file_type, :each_line, :static_File_each_line)
NativeLib.def_method(file_type, :empty?, :passthrough_File_empty?)
NativeLib.def_method(file_type, :executable?, :passthrough_File_executable?)
NativeLib.def_method(file_type, :exists?, :passthrough_File_exists?)
NativeLib.def_method(file_type, :expand_path, :passthrough_File_expand_path)
NativeLib.def_method(file_type, :extname, :passthrough_File_extname)
NativeLib.def_method(file_type, :file?, :passthrough_File_file?)
NativeLib.def_method(file_type, :lines, :static_File_lines)
NativeLib.def_method(file_type, :join, :static_File_join)
NativeLib.def_method(file_type, :readable?, :passthrough_File_readable?)
NativeLib.def_method(file_type, :read, :passthrough_File_read)
NativeLib.def_method(file_type, :real_path, :passthrough_File_real_path)
NativeLib.def_method(file_type, :size, :passthrough_File_size)
NativeLib.def_method(file_type, :symlink, :passthrough_File_symlink)
NativeLib.def_method(file_type, :touch, :passthrough_File_touch)
NativeLib.def_method(file_type, :writable?, :passthrough_File_writable?)
NativeLib.def_method(file_type, :write, :passthrough_File_write)

NativeLib.def_instance_method(file_type, :initialize, :file_init)
NativeLib.def_instance_method(file_type, :close, :file_close)
NativeLib.def_instance_method(file_type, :size, :file_size)
Expand Down
1 change: 1 addition & 0 deletions stdlib/file.mt
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ deftype File
%File{name, mode}
end

def path; @path; end
def mode; @mode; end
end