Skip to content

Commit

Permalink
Add another level of indirection for projects.
Browse files Browse the repository at this point in the history
This is a step towards project dependencies... I hope.

I realized that I don't want a separate build directory for every
project; just for the one that is being built. if my package depends
on gleam_stdlib, I need the files in that package to be in the build
directory for this one.

I think I'm still doing it wrong, though because gleam_stdlib is the
package name and right now if you tried to build something like that
you'd need to import from gleam_stdlib. But e.g. gleam/io is not in
a gleam_stdlib namespace.
  • Loading branch information
dusty-phillips committed Aug 27, 2024
1 parent 87fde3a commit 3984586
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 92 deletions.
1 change: 1 addition & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ argv = ">= 1.0.2 and < 2.0.0"
simplifile = ">= 2.0.1 and < 3.0.0"
filepath = ">= 1.0.0 and < 2.0.0"
glexer = ">= 1.0.1 and < 2.0.0"
tom = ">= 1.0.1 and < 2.0.0"

[dev-dependencies]
gleescript = ">= 1.4.0 and < 2.0.0"
Expand Down
1 change: 1 addition & 0 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ glexer = { version = ">= 1.0.1 and < 2.0.0" }
pprint = { version = ">= 1.0.3 and < 2.0.0" }
simplifile = { version = ">= 2.0.1 and < 3.0.0" }
temporary = { version = ">= 1.0.0 and < 2.0.0" }
tom = { version = ">= 1.0.1 and < 2.0.0" }
39 changes: 12 additions & 27 deletions src/compiler.gleam
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import compiler/generator
import compiler/package
import compiler/project
import compiler/transformer
import glance
import gleam/dict
import gleam/list
import gleam/option
import gleam/result
import gleam/string

pub fn compile_module(glance_module: glance.Module) -> String {
glance_module
Expand All @@ -16,34 +13,22 @@ pub fn compile_module(glance_module: glance.Module) -> String {

pub fn compile_package(package: package.GleamPackage) -> package.CompiledPackage {
package.CompiledPackage(
config: package.config,
base_directory: package.base_directory,
main_module: dict.get(package.modules, package.main_module)
|> result.try(fn(mod) { mod.functions |> has_main_function })
|> result.replace(package.main_module |> string.drop_right(6))
|> option.from_result,
modules: package.modules
|> dict.map_values(fn(_key, value) { compile_module(value) }),
external_import_files: package.external_import_files,
)
}

pub fn has_main_function(
functions: List(glance.Definition(glance.Function)),
) -> Result(Bool, Nil) {
functions
|> list.find(fn(x) {
case x {
glance.Definition(
definition: glance.Function(
name: "main",
publicity: glance.Public,
parameters: [],
..,
),
..,
) -> True
_ -> False
}
})
|> result.replace(True)
pub fn compile_project(
gleam_project: project.GleamProject,
) -> project.CompiledProject {
// TODO: Compile dependencies
project.CompiledProject(
base_directory: gleam_project.base_directory,
build_directory: project.build_directory(gleam_project.base_directory),
main_package: compile_package(gleam_project.main_package),
main_module: gleam_project.main_module,
)
}
84 changes: 65 additions & 19 deletions src/compiler/package.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import filepath
import glance
import gleam/dict
import gleam/list
import gleam/option
import gleam/result
import gleam/set
import gleam/string
import simplifile
import tom

pub type GleamPackage {
GleamPackage(
config: Config,
base_directory: String,
main_module: String,
modules: dict.Dict(String, glance.Module),
Expand All @@ -27,33 +28,55 @@ pub type GleamPackage {

pub type CompiledPackage {
CompiledPackage(
config: Config,
base_directory: String,
main_module: option.Option(String),
modules: dict.Dict(String, String),
external_import_files: set.Set(String),
)
}

pub type Config {
Config(name: String)
}

/// Load the entry_point file and recursively load and parse any modules it
///returns.
pub fn load_package(
source_directory: String,
base_directory: String,
) -> Result(GleamPackage, errors.Error) {
source_directory
|> simplifile.is_directory
|> result.map_error(errors.FileOrDirectoryNotFound(source_directory, _))
|> result.try(fn(_) { find_entrypoint(source_directory) })
|> result.try(fn(entrypoint) {
load_module(
GleamPackage(
source_directory,
entrypoint,
dict.new(),
external_import_files: set.new(),
),
use _ <- result.try(
base_directory
|> simplifile.is_directory
|> result.map_error(errors.FileOrDirectoryNotFound(base_directory, _)),
)
use config <- result.try(load_config(base_directory))
use entrypoint <- result.try(find_entrypoint(base_directory))
load_module(
GleamPackage(
config,
base_directory,
entrypoint,
)
})
dict.new(),
external_import_files: set.new(),
),
entrypoint,
)
}

pub fn load_config(base_directory: String) -> Result(Config, errors.Error) {
let config_path = filepath.join(base_directory, "gleam.toml")
use config_contents <- result.try(
simplifile.read(config_path)
|> result.map_error(errors.FileReadError(config_path, _)),
)
use parsed <- result.try(
tom.parse(config_contents)
|> result.map_error(errors.TomlError(config_path, _)),
)
use name <- result.try(
tom.get_string(parsed, ["name"]) |> result.map_error(errors.InvalidConfig),
)
Ok(Config(name))
}

pub fn find_entrypoint(source_directory: String) -> Result(String, errors.Error) {
Expand All @@ -68,8 +91,31 @@ pub fn source_directory(base_directory: String) -> String {
filepath.join(base_directory, "src")
}

pub fn build_directory(base_directory: String) -> String {
filepath.join(base_directory, "build")
pub fn has_main_function(package: GleamPackage) -> Result(String, Nil) {
dict.get(package.modules, package.main_module)
|> result.try(fn(mod) { mod.functions |> function_list_has_main_function })
|> result.replace(package.main_module |> string.drop_right(6))
}

fn function_list_has_main_function(
functions: List(glance.Definition(glance.Function)),
) -> Result(Bool, Nil) {
functions
|> list.find(fn(x) {
case x {
glance.Definition(
definition: glance.Function(
name: "main",
publicity: glance.Public,
parameters: [],
..,
),
..,
) -> True
_ -> False
}
})
|> result.replace(True)
}

/// Parse the module and add it to the package's modules, if it can be parsed.
Expand Down
52 changes: 52 additions & 0 deletions src/compiler/project.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//// A project encompasses everything needed to build a package
//// and all its dependencies (which are other packages).
////
//// For the most part, a project is just a wrapper of a package
//// and a build directory for that package.
////
//// Note that any one project can have multiple packages in it,
//// with one main package and any number of dependency packages.
//// Each of those dependency packages is probably the main package
//// of its own project from the point of view of the person developing
//// that project, but the current developer only cares about the current
//// package.
////
//// I really hope that makes sense cause it took we a while to puzzle
//// it out.

import compiler/package
import errors
import filepath
import gleam/option
import gleam/result

pub type GleamProject {
GleamProject(
base_directory: String,
main_package: package.GleamPackage,
main_module: option.Option(String),
)
}

pub type CompiledProject {
CompiledProject(
base_directory: String,
build_directory: String,
main_package: package.CompiledPackage,
main_module: option.Option(String),
)
}

pub fn load_project(
base_directory: String,
) -> Result(GleamProject, errors.Error) {
use main_package <- result.try(package.load_package(base_directory))
let main_module =
main_package |> package.has_main_function |> option.from_result
// TODO: dependencies need to come from gleam.toml
Ok(GleamProject(base_directory, main_package, main_module))
}

pub fn build_directory(base_directory: String) -> String {
filepath.join(base_directory, "build")
}
5 changes: 5 additions & 0 deletions src/errors.gleam
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import glance
import internal/errors as internal
import simplifile
import tom

pub type Error {
TomlError(path: String, error: tom.ParseError)
InvalidConfig(error: tom.GetError)
FileOrDirectoryNotFound(path: String, error: simplifile.FileError)
FileReadError(path: String, error: simplifile.FileError)
FileWriteError(path: String, error: simplifile.FileError)
Expand All @@ -14,6 +17,8 @@ pub type Error {

pub fn format_error(error: Error) -> String {
case error {
TomlError(path, _) -> "Error reading Toml file " <> path
InvalidConfig(_error) -> "Invalid config file (missing name?)"
FileOrDirectoryNotFound(filename, _) ->
"File or directory not found " <> filename
FileReadError(filename, simplifile.Enoent) -> "File not found " <> filename
Expand Down
67 changes: 40 additions & 27 deletions src/macabre.gleam
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import argv
import compiler
import compiler/package
import compiler/project
import errors
import filepath
import gleam/dict
import gleam/io
import gleam/option
import gleam/result
import gleam/set
import output
Expand All @@ -14,9 +16,9 @@ pub fn main() {
[] -> usage("Not enough arguments")
[directory] ->
directory
|> package.load_package
|> result.map(compiler.compile_package)
|> result.try(write_package)
|> project.load_project
|> result.map(compiler.compile_project)
|> result.try(write_project)
|> result.map_error(output.write_error)
|> result.unwrap_both
// both nil
Expand All @@ -28,32 +30,43 @@ pub fn usage(message: String) -> Nil {
io.println("Usage: macabre <filename.gleam>\n\n" <> message)
}

pub fn write_package(
package: package.CompiledPackage,
pub fn write_project(
project: project.CompiledProject,
) -> Result(Nil, errors.Error) {
let build_directory = package.build_directory(package.base_directory)
let source_directory = package.source_directory(package.base_directory)
output.delete(build_directory)
|> result.try(fn(_) { output.create_directory(build_directory) })
|> result.try(fn(_) { output.write_prelude_file(build_directory) })
use _ <- result.try(output.delete(project.build_directory))
use _ <- result.try(output.create_directory(project.build_directory))
use _ <- result.try(output.write_prelude_file(project.build_directory))
// TODO: write dependencies
write_package(
project.main_package,
filepath.join(project.build_directory, project.main_package.config.name),
)
|> result.try(fn(_) {
output.write_py_main(build_directory, package.main_module)
})
|> result.try(fn(_) {
output.copy_externals(
build_directory,
source_directory,
package.external_import_files |> set.to_list,
output.write_py_main(
project.build_directory,
option.map(project.main_module, fn(name) {
project.main_package.config.name <> "." <> name
}),
)
})
|> result.try(fn(_) {
dict.fold(package.modules, Ok(Nil), fn(state, name, module) {
result.try(state, fn(_) {
build_directory
|> filepath.join(name)
|> output.replace_extension()
|> output.write(module, _)
})
})
})
}

pub fn write_package(
package: package.CompiledPackage,
build_directory: String,
) -> Result(Nil, errors.Error) {
let source_directory = package.source_directory(package.base_directory)
use _ <- result.try(output.copy_externals(
build_directory,
source_directory,
package.external_import_files |> set.to_list,
))
use state, name, module <- dict.fold(package.modules, Ok(Nil))
{
use _ <- result.try(state)
build_directory
|> filepath.join(name)
|> output.replace_extension()
|> output.write(module, _)
}
}
Loading

0 comments on commit 3984586

Please sign in to comment.