diff --git a/examples/TestProject/.gitignore b/examples/TestProject/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/examples/TestProject/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/examples/TestProject/spark.toml b/examples/TestProject/spark.toml new file mode 100644 index 0000000..0408a6a --- /dev/null +++ b/examples/TestProject/spark.toml @@ -0,0 +1 @@ +name = "TestProject" \ No newline at end of file diff --git a/examples/TestProject/src/TestProject.spark b/examples/TestProject/src/TestProject.spark new file mode 100644 index 0000000..6c8f690 --- /dev/null +++ b/examples/TestProject/src/TestProject.spark @@ -0,0 +1,22 @@ +import Spark/IO +import Spark/List + +external "import { add } from './testproject_ffi.mjs';" +external "let $id = 0;" + +def pub fresh_id = + external "return $id++" + +const squares = List.map([1, 2, 3], \x -> x * x) + +def pub main = + IO.println("Hello, world!") ; + IO.println("Goodbye, now.") ; + IO.println(squares) ; + IO.println(@(fresh_id(), fresh_id(), fresh_id())) + +# Atoms don't need a name! Unnamed atoms are basically tuples. +const me = @("Joe", 30) + +def pub first\@(a, _) = a +def pub second\@(_, b) = b diff --git a/examples/TestProject/src/testproject_ffi.mjs b/examples/TestProject/src/testproject_ffi.mjs new file mode 100644 index 0000000..7d65831 --- /dev/null +++ b/examples/TestProject/src/testproject_ffi.mjs @@ -0,0 +1,3 @@ +export function add(a, b) { + return a + b; +} diff --git a/examples/blah/build/Blah.mjs b/examples/blah/build/Blah.mjs deleted file mode 100644 index e69de29..0000000 diff --git a/examples/blah/build/Spark/IO.mjs b/examples/blah/build/Spark/IO.mjs deleted file mode 100644 index e69de29..0000000 diff --git a/examples/blah/build/spark.prelude.mjs b/examples/blah/build/spark.prelude.mjs deleted file mode 100644 index e69de29..0000000 diff --git a/examples/blah/spark.toml b/examples/blah/spark.toml deleted file mode 100644 index a809a8e..0000000 --- a/examples/blah/spark.toml +++ /dev/null @@ -1 +0,0 @@ -name = "Blah" \ No newline at end of file diff --git a/examples/blah/src/Blah.spark b/examples/blah/src/Blah.spark deleted file mode 100644 index 75ad651..0000000 --- a/examples/blah/src/Blah.spark +++ /dev/null @@ -1,6 +0,0 @@ -import Spark/IO - -def pub main = - \_ <- IO.println("Hello, world!") |> IO.then - \_ <- IO.println("This is the Blah module") |> IO.then - IO.println("Bye!") \ No newline at end of file diff --git a/gleam.toml b/gleam.toml index f7b19ec..41cfe45 100644 --- a/gleam.toml +++ b/gleam.toml @@ -19,6 +19,8 @@ gleam_community_ansi = ">= 1.4.0 and < 2.0.0" hug = "~> 1.0" glam = ">= 2.0.0 and < 3.0.0" dedent = ">= 1.0.0 and < 2.0.0" +filepath = ">= 1.0.0 and < 2.0.0" +simplifile = ">= 1.7.0 and < 2.0.0" [dev-dependencies] gleeunit = "~> 1.0" diff --git a/manifest.toml b/manifest.toml index bc8ed3b..4ba5f44 100644 --- a/manifest.toml +++ b/manifest.toml @@ -4,6 +4,7 @@ packages = [ { name = "chomp", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "../chomp-nibble" }, { name = "dedent", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "dedent", source = "hex", outer_checksum = "591C78F019CFE8B4F138BDCA5DB57EB429D95F90CD12B51262A3FC6455EC1AEF" }, + { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, { name = "glam", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "66EC3BCD632E51EED029678F8DF419659C1E57B1A93D874C5131FE220DFAD2B2" }, { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" }, { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, @@ -12,15 +13,18 @@ packages = [ { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, { name = "hug", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib"], otp_app = "hug", source = "hex", outer_checksum = "D7D8C4F5A3093FB0B6A0228288D94E95966AADC5500133F9E983BA4634E92CC8" }, { name = "pprint", version = "1.0.2", build_tools = ["gleam"], requirements = ["glam", "gleam_stdlib"], otp_app = "pprint", source = "hex", outer_checksum = "3EB2A7A8F72322F73EEF342374D0354AE39E7F9C64678B960BE8B2DC1B564AE1" }, + { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, { name = "thoas", version = "1.2.0", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "540C8CB7D9257F2AD0A14145DC23560F91ACDCA995F0CCBA779EB33AF5D859D1" }, ] [requirements] chomp = { path = "../chomp-nibble" } dedent = { version = ">= 1.0.0 and < 2.0.0" } +filepath = { version = ">= 1.0.0 and < 2.0.0" } glam = { version = ">= 2.0.0 and < 3.0.0" } gleam_community_ansi = { version = ">= 1.4.0 and < 2.0.0" } gleam_stdlib = { version = "~> 0.34 or ~> 1.0" } gleeunit = { version = "~> 1.0" } hug = { version = "~> 1.0" } pprint = { version = "~> 1.0" } +simplifile = { version = ">= 1.7.0 and < 2.0.0" } diff --git a/src/spark.gleam b/src/spark.gleam index b917476..0d19f7f 100644 --- a/src/spark.gleam +++ b/src/spark.gleam @@ -1,87 +1,46 @@ +import filepath import gleam/io -import gleam/result -import spark/compile -import spark/error -import spark/lex -import spark/parse +import gleam/result.{try} +import gleam/string +import spark/file +import spark/project +import gleam_community/ansi pub fn main() { - let source = - " - import Spark/IO - - external \"let $id = 0;\" - - def pub fresh_id = - external \"return $id++\" - - def pub map\\over, fn = - case over - | [] as list = list - | [x : xs] = fn(x) : map(xs, fn) - - const squares = map([1, 2, 3], \\x -> x * x) - - const foo = - \\sum <- add_with_cb(5, 6) - sum * (2 + 3 * external \"return 5\") - - const bar = - [1, 2, 3] - |> baz - |> map(\\x -> x * x) - - def pub main = - IO.println(\"Hello, world!\") ; - - \"Isn't this language nice?\" - |> IO.println ; - - 2 * 3 - - const dsf = - case { x: 3, y: 5, z: 5 } - | { x, y, z } = x + y + z - | { x, y } = x + y + let result = { + use project <- try(project.from("TestProject", "examples/TestProject")) + build(project) + } - def println\\text = - @IO { - perform: \\ -> external \" - console.log(text); - return $.nil; - \" - } + case result { + Ok(_) -> Nil + Error(e) -> io.println_error(e) + } +} - def then\\@IO { perform }, f = - @IO { - perform: \\ -> f(perform()).perform() - } +/// Build a project, with the stdlib and prelude included, to the /build directory. +/// +fn build(project: project.Project) -> Result(Nil, String) { + let build_dir = filepath.join(project.dir, "build") - def test = - \\_ <- println(\"Hello!\") |> then - println(\"Goodbye, now.\") + report("Compiling", "Spark") + use template <- try(project.from("Spark", "./templates")) + use _ <- try(project.compile(template, to: build_dir)) - # Atoms don't need a name! Unnamed atoms are basically tuples. - const me = @(\"Joe\", 30) + report("Compiling", project.name) + use _ <- try(project.compile(project, to: build_dir)) - def pub first\\@(a, _) = a - def pub second\\@(_, b) = b - " + report("Creating", "index file") + let path = filepath.join(project.dir, "build/index.mjs") + let contents = "import { main } from './" <> project.name <> ".mjs';\nmain();" + use _ <- try(file.write_all(path, contents)) - let result = { - use tokens <- result.try(lex.lex(source)) - use ast <- result.try(parse.parse(tokens)) - let compiled = compile.compile(ast) - Ok(compiled) - } + report("Compiled", "successfully") + report("Entry", path) + Ok(Nil) +} - case result { - Ok(js) -> { - io.println(js) - Nil - } - Error(e) -> - error.to_string(e, source, "file.spark") - |> io.println_error - } +fn report(status: String, message: String) { + let padding = string.repeat(" ", 9 - string.length(status)) + io.println(padding <> ansi.bold(ansi.blue(status)) <> " " <> message) } diff --git a/src/spark/ast.gleam b/src/spark/ast.gleam index 21dc697..cc5fd3c 100644 --- a/src/spark/ast.gleam +++ b/src/spark/ast.gleam @@ -12,16 +12,15 @@ pub type Module { /// An import consists of the following: /// -/// import foo/bar/baz as blah +/// import Foo/Bar/Baz as Blah /// -/// - The base module name: "foo" -/// - Any paths: ["bar", "baz"] -/// - An optional rename: Some("blah") +/// - import segments: ["Foo", "Bar", "Baz"] +/// - An optional rename: Some("Blah") /// -/// The path list is guaranteed to have at least one element by the parser. +/// The segments list is guaranteed to have at least one element by the parser. /// pub type Import { - Import(path: List(String), rename: Option(String)) + Import(segments: List(String), rename: Option(String)) } /// A declaration is something at the top-level of a module. diff --git a/src/spark/compile.gleam b/src/spark/compile.gleam index ada5a46..c36d277 100644 --- a/src/spark/compile.gleam +++ b/src/spark/compile.gleam @@ -19,29 +19,34 @@ import gleam/option.{type Option, None, Some} import gleam/string import spark/ast import spark/compile/pattern -import spark/compile/util +import spark/module +import spark/util const max_width = 80 // ---- COMPILATION ------------------------------------------------------------ -pub fn compile(module: ast.Module) -> String { - module - |> compile_module +pub fn compile(module_ast: ast.Module, module: module.Module) -> String { + module_ast + |> compile_module(module) |> doc.to_string(max_width) } // -- Modules -- -fn compile_module(module: ast.Module) -> Document { - // TODO: actually resolve imports - let ast.Module(imports, declarations) = module +fn compile_module(module_ast: ast.Module, module: module.Module) -> Document { + let ast.Module(imports, declarations) = module_ast - let prelude = doc.from_string("import * as $ from './spark.prelude.mjs';") + let prelude = + doc.from_string( + "import * as $ from '" + <> module.resolve_import(module, ["spark.prelude"]) + <> "';", + ) let imports = imports - |> list.map(compile_import) + |> list.map(compile_import(_, module)) |> list.prepend(prelude) |> doc.join(doc.line) @@ -53,16 +58,16 @@ fn compile_module(module: ast.Module) -> Document { doc.concat([imports, doc.lines(2), declarations]) } -fn compile_import(i: ast.Import) -> Document { - let ast.Import(import_path, rename) = i +fn compile_import(i: ast.Import, module: module.Module) -> Document { + let ast.Import(import_segments, rename) = i - let path = "./" <> string.join(import_path, "/") <> ".mjs" + let path = module.resolve_import(module, import_segments) let name = case rename { Some(rename) -> rename None -> { - let assert Ok(last) = list.last(import_path) + let assert Ok(last) = list.last(import_segments) last } } diff --git a/src/spark/compile/pattern.gleam b/src/spark/compile/pattern.gleam index b4caebe..5146205 100644 --- a/src/spark/compile/pattern.gleam +++ b/src/spark/compile/pattern.gleam @@ -6,7 +6,7 @@ import gleam/option.{None, Some} import gleam/pair import gleam/string import spark/ast -import spark/compile/util +import spark/util // ---- TYPES ------------------------------------------------------------------ diff --git a/src/spark/file.gleam b/src/spark/file.gleam new file mode 100644 index 0000000..7ddff3e --- /dev/null +++ b/src/spark/file.gleam @@ -0,0 +1,41 @@ +import filepath +import gleam/result.{try} +import gleam_community/ansi +import simplifile + +/// Write a file, creating directories as needed. +/// +pub fn write_all(path: String, contents: String) -> Result(Nil, String) { + let dir = filepath.directory_name(path) + use _ <- try( + simplifile.create_directory_all(dir) + |> result.replace_error(file_error("create this directory", dir)), + ) + simplifile.write(path, contents) + |> result.replace_error(file_error("write to this file", path)) +} + +/// Read a file. +/// +pub fn read(path: String) -> Result(String, String) { + simplifile.read(path) + |> result.replace_error(file_error("read this file", path)) +} + +/// Copy a file at the first path to the second path. +/// +pub fn copy(from: String, to: String) -> Result(Nil, String) { + simplifile.copy_file(from, to) + |> result.replace_error(file_error("copy this file", from)) +} + +/// List files in a directory. +/// +pub fn get_files(dir: String) -> Result(List(String), String) { + simplifile.get_files(dir) + |> result.replace_error(file_error("read this directory", dir)) +} + +fn file_error(message, path) -> String { + ansi.red("I was unable to " <> message <> ": ") <> path +} diff --git a/src/spark/module.gleam b/src/spark/module.gleam new file mode 100644 index 0000000..c1e70d6 --- /dev/null +++ b/src/spark/module.gleam @@ -0,0 +1,56 @@ +import filepath +import gleam/list +import gleam/string +import spark/util + +// ---- TYPES ------------------------------------------------------------------ + +pub type Module { + Module(segments: List(String), path: String) +} + +// ---- CONSTRUCTOR ------------------------------------------------------------ + +pub fn from_path(path: String, inside_dir: String) -> Result(Module, Nil) { + let stripped_path = + path + |> util.remove_prefix(inside_dir <> "/") + + let segments = filepath.split(stripped_path) + + case + segments + |> list.all(fn(segment) { + util.capitalise(segment) == segment + && { + filepath.extension(segment) == Ok("spark") + || filepath.strip_extension(segment) == segment + } + }) + { + False -> Error(Nil) + True -> + segments + |> list.map(filepath.strip_extension) + |> Module(path) + |> Ok + } +} + +// ---- FUNCTIONS -------------------------------------------------------------- + +/// Resolves an import from the given module to an import path, from the root of +/// the project. +/// +pub fn resolve_import(module: Module, import_segments: List(String)) -> String { + let prefix = case module.segments { + [_] | [] -> "./" + [_, ..rest] -> + rest + |> list.map(fn(_) { ".." }) + |> string.join("/") + <> "/" + } + + prefix <> string.join(import_segments, with: "/") <> ".mjs" +} diff --git a/src/spark/project.gleam b/src/spark/project.gleam new file mode 100644 index 0000000..6983c8b --- /dev/null +++ b/src/spark/project.gleam @@ -0,0 +1,78 @@ +import filepath +import gleam/list +import gleam/result.{try} +import gleam/string +import spark/compile +import spark/error +import spark/file +import spark/lex +import spark/module.{type Module} +import spark/parse +import spark/util + +// ---- TYPES ------------------------------------------------------------------ + +pub type Project { + Project(name: String, modules: List(Module), extra_files: List(String), dir: String) +} + +// ---- CONSTRUCTOR ------------------------------------------------------------ + +pub fn from(name: String, dir: String) -> Result(Project, String) { + use #(modules, extra_files) <- try( + scan_project_files(filepath.join(dir, "src")), + ) + Ok(Project(name, modules, extra_files, dir)) +} + +fn scan_project_files( + dir: String, +) -> Result(#(List(Module), List(String)), String) { + use files <- try(file.get_files(dir)) + + files + |> list.fold(#([], []), fn(acc, path) { + case module.from_path(path, dir) { + Ok(module) -> #([module, ..acc.0], acc.1) + Error(_) -> #(acc.0, [path, ..acc.1]) + } + }) + |> Ok +} + +// ---- FUNCTIONS -------------------------------------------------------------- + +/// Compile a project to the given build directory. +/// +pub fn compile(project: Project, to build_dir: String) -> Result(Nil, String) { + use _ <- try( + project.modules + |> list.try_each(fn(module) { + use contents <- try(file.read(module.path)) + + let result = { + use lexed <- try(lex.lex(contents)) + use parsed <- try(parse.parse(lexed)) + Ok(compile.compile(parsed, module)) + } + + case result { + Ok(compiled) -> { + let path = + filepath.join( + build_dir, + string.join(module.segments, "/") <> ".mjs", + ) + file.write_all(path, compiled) + } + Error(e) -> Error(error.to_string(e, contents, module.path)) + } + }), + ) + + project.extra_files + |> list.try_each(fn(path) { + let new_path = util.remove_prefix(path, filepath.join(project.dir, "src")) + file.copy(path, filepath.join(build_dir, new_path)) + }) +} diff --git a/src/spark/compile/util.gleam b/src/spark/util.gleam similarity index 74% rename from src/spark/compile/util.gleam rename to src/spark/util.gleam index 6ee5b1c..9ad9228 100644 --- a/src/spark/compile/util.gleam +++ b/src/spark/util.gleam @@ -1,3 +1,5 @@ +import gleam/string + pub fn legalize(name: String) -> String { case name { "await" @@ -59,3 +61,17 @@ pub fn legalize(name: String) -> String { _ -> name } } + +pub fn remove_prefix(string: String, prefix: String) -> String { + case string.starts_with(string, prefix) { + True -> string.drop_left(string, string.length(prefix)) + False -> string + } +} + +pub fn capitalise(string: String) -> String { + case string.pop_grapheme(string) { + Ok(#(first, rest)) -> string.uppercase(first) <> rest + _ -> string + } +} \ No newline at end of file diff --git a/src/templates/Spark/IO.spark b/src/templates/Spark/IO.spark deleted file mode 100644 index 726ccbb..0000000 --- a/src/templates/Spark/IO.spark +++ /dev/null @@ -1,13 +0,0 @@ -def pub println\text = - \ <- action - external " - console.log(text); - return $.nil; - " - -def pub then\@IO { perform }, f = - \ <- action - f(perform()).perform() - -def action\perform = - @IO { perform } \ No newline at end of file diff --git a/templates/src/Spark/IO.spark b/templates/src/Spark/IO.spark new file mode 100644 index 0000000..9d30196 --- /dev/null +++ b/templates/src/Spark/IO.spark @@ -0,0 +1,5 @@ +def pub println\text = + external " + console.log(text); + return $.nil; + " diff --git a/templates/src/Spark/List.spark b/templates/src/Spark/List.spark new file mode 100644 index 0000000..86beb38 --- /dev/null +++ b/templates/src/Spark/List.spark @@ -0,0 +1,14 @@ +def pub map\list, fn = + external " + return over.map((x) => fn(x)); + " + +def pub index_map\list, fn = + external " + return over.map((x, i) => fn(x, i)); + " + +def pub fold\over, acc, fn = + external " + return over.reduce((acc, x) => fn(acc, x), acc); + " diff --git a/src/templates/spark.prelude.mjs b/templates/src/spark.prelude.mjs similarity index 100% rename from src/templates/spark.prelude.mjs rename to templates/src/spark.prelude.mjs