Skip to content

Commit 971574c

Browse files
committed
🚧 Start work on project support
1 parent febad89 commit 971574c

23 files changed

+302
-116
lines changed

‎examples/TestProject/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

‎examples/TestProject/spark.toml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
name = "TestProject"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import Spark/IO
2+
import Spark/List
3+
4+
external "import { add } from './testproject_ffi.mjs';"
5+
external "let $id = 0;"
6+
7+
def pub fresh_id =
8+
external "return $id++"
9+
10+
const squares = List.map([1, 2, 3], \x -> x * x)
11+
12+
def pub main =
13+
IO.println("Hello, world!") ;
14+
IO.println("Goodbye, now.") ;
15+
IO.println(squares) ;
16+
IO.println(@(fresh_id(), fresh_id(), fresh_id()))
17+
18+
# Atoms don't need a name! Unnamed atoms are basically tuples.
19+
const me = @("Joe", 30)
20+
21+
def pub first\@(a, _) = a
22+
def pub second\@(_, b) = b
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function add(a, b) {
2+
return a + b;
3+
}

‎examples/blah/build/Blah.mjs

Whitespace-only changes.

‎examples/blah/build/Spark/IO.mjs

Whitespace-only changes.

‎examples/blah/build/spark.prelude.mjs

Whitespace-only changes.

‎examples/blah/spark.toml

-1
This file was deleted.

‎examples/blah/src/Blah.spark

-6
This file was deleted.

‎gleam.toml

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ gleam_community_ansi = ">= 1.4.0 and < 2.0.0"
1919
hug = "~> 1.0"
2020
glam = ">= 2.0.0 and < 3.0.0"
2121
dedent = ">= 1.0.0 and < 2.0.0"
22+
filepath = ">= 1.0.0 and < 2.0.0"
23+
simplifile = ">= 1.7.0 and < 2.0.0"
2224

2325
[dev-dependencies]
2426
gleeunit = "~> 1.0"

‎manifest.toml

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
packages = [
55
{ name = "chomp", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "../chomp-nibble" },
66
{ name = "dedent", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "dedent", source = "hex", outer_checksum = "591C78F019CFE8B4F138BDCA5DB57EB429D95F90CD12B51262A3FC6455EC1AEF" },
7+
{ name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
78
{ name = "glam", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "66EC3BCD632E51EED029678F8DF419659C1E57B1A93D874C5131FE220DFAD2B2" },
89
{ 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" },
910
{ 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 = [
1213
{ name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
1314
{ 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" },
1415
{ name = "pprint", version = "1.0.2", build_tools = ["gleam"], requirements = ["glam", "gleam_stdlib"], otp_app = "pprint", source = "hex", outer_checksum = "3EB2A7A8F72322F73EEF342374D0354AE39E7F9C64678B960BE8B2DC1B564AE1" },
16+
{ name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
1517
{ name = "thoas", version = "1.2.0", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "540C8CB7D9257F2AD0A14145DC23560F91ACDCA995F0CCBA779EB33AF5D859D1" },
1618
]
1719

1820
[requirements]
1921
chomp = { path = "../chomp-nibble" }
2022
dedent = { version = ">= 1.0.0 and < 2.0.0" }
23+
filepath = { version = ">= 1.0.0 and < 2.0.0" }
2124
glam = { version = ">= 2.0.0 and < 3.0.0" }
2225
gleam_community_ansi = { version = ">= 1.4.0 and < 2.0.0" }
2326
gleam_stdlib = { version = "~> 0.34 or ~> 1.0" }
2427
gleeunit = { version = "~> 1.0" }
2528
hug = { version = "~> 1.0" }
2629
pprint = { version = "~> 1.0" }
30+
simplifile = { version = ">= 1.7.0 and < 2.0.0" }

‎src/spark.gleam

+35-76
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,46 @@
1+
import filepath
12
import gleam/io
2-
import gleam/result
3-
import spark/compile
4-
import spark/error
5-
import spark/lex
6-
import spark/parse
3+
import gleam/result.{try}
4+
import gleam/string
5+
import spark/file
6+
import spark/project
7+
import gleam_community/ansi
78

89
pub fn main() {
9-
let source =
10-
"
11-
import Spark/IO
12-
13-
external \"let $id = 0;\"
14-
15-
def pub fresh_id =
16-
external \"return $id++\"
17-
18-
def pub map\\over, fn =
19-
case over
20-
| [] as list = list
21-
| [x : xs] = fn(x) : map(xs, fn)
22-
23-
const squares = map([1, 2, 3], \\x -> x * x)
24-
25-
const foo =
26-
\\sum <- add_with_cb(5, 6)
27-
sum * (2 + 3 * external \"return 5\")
28-
29-
const bar =
30-
[1, 2, 3]
31-
|> baz
32-
|> map(\\x -> x * x)
33-
34-
def pub main =
35-
IO.println(\"Hello, world!\") ;
36-
37-
\"Isn't this language nice?\"
38-
|> IO.println ;
39-
40-
2 * 3
41-
42-
const dsf =
43-
case { x: 3, y: 5, z: 5 }
44-
| { x, y, z } = x + y + z
45-
| { x, y } = x + y
10+
let result = {
11+
use project <- try(project.from("TestProject", "examples/TestProject"))
12+
build(project)
13+
}
4614

47-
def println\\text =
48-
@IO {
49-
perform: \\ -> external \"
50-
console.log(text);
51-
return $.nil;
52-
\"
53-
}
15+
case result {
16+
Ok(_) -> Nil
17+
Error(e) -> io.println_error(e)
18+
}
19+
}
5420

55-
def then\\@IO { perform }, f =
56-
@IO {
57-
perform: \\ -> f(perform()).perform()
58-
}
21+
/// Build a project, with the stdlib and prelude included, to the /build directory.
22+
///
23+
fn build(project: project.Project) -> Result(Nil, String) {
24+
let build_dir = filepath.join(project.dir, "build")
5925

60-
def test =
61-
\\_ <- println(\"Hello!\") |> then
62-
println(\"Goodbye, now.\")
26+
report("Compiling", "Spark")
27+
use template <- try(project.from("Spark", "./templates"))
28+
use _ <- try(project.compile(template, to: build_dir))
6329

64-
# Atoms don't need a name! Unnamed atoms are basically tuples.
65-
const me = @(\"Joe\", 30)
30+
report("Compiling", project.name)
31+
use _ <- try(project.compile(project, to: build_dir))
6632

67-
def pub first\\@(a, _) = a
68-
def pub second\\@(_, b) = b
69-
"
33+
report("Creating", "index file")
34+
let path = filepath.join(project.dir, "build/index.mjs")
35+
let contents = "import { main } from './" <> project.name <> ".mjs';\nmain();"
36+
use _ <- try(file.write_all(path, contents))
7037

71-
let result = {
72-
use tokens <- result.try(lex.lex(source))
73-
use ast <- result.try(parse.parse(tokens))
74-
let compiled = compile.compile(ast)
75-
Ok(compiled)
76-
}
38+
report("Compiled", "successfully")
39+
report("Entry", path)
40+
Ok(Nil)
41+
}
7742

78-
case result {
79-
Ok(js) -> {
80-
io.println(js)
81-
Nil
82-
}
83-
Error(e) ->
84-
error.to_string(e, source, "file.spark")
85-
|> io.println_error
86-
}
43+
fn report(status: String, message: String) {
44+
let padding = string.repeat(" ", 9 - string.length(status))
45+
io.println(padding <> ansi.bold(ansi.blue(status)) <> " " <> message)
8746
}

‎src/spark/ast.gleam

+5-6
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,15 @@ pub type Module {
1212

1313
/// An import consists of the following:
1414
///
15-
/// import foo/bar/baz as blah
15+
/// import Foo/Bar/Baz as Blah
1616
///
17-
/// - The base module name: "foo"
18-
/// - Any paths: ["bar", "baz"]
19-
/// - An optional rename: Some("blah")
17+
/// - import segments: ["Foo", "Bar", "Baz"]
18+
/// - An optional rename: Some("Blah")
2019
///
21-
/// The path list is guaranteed to have at least one element by the parser.
20+
/// The segments list is guaranteed to have at least one element by the parser.
2221
///
2322
pub type Import {
24-
Import(path: List(String), rename: Option(String))
23+
Import(segments: List(String), rename: Option(String))
2524
}
2625

2726
/// A declaration is something at the top-level of a module.

‎src/spark/compile.gleam

+18-13
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,34 @@ import gleam/option.{type Option, None, Some}
1919
import gleam/string
2020
import spark/ast
2121
import spark/compile/pattern
22-
import spark/compile/util
22+
import spark/module
23+
import spark/util
2324

2425
const max_width = 80
2526

2627
// ---- COMPILATION ------------------------------------------------------------
2728

28-
pub fn compile(module: ast.Module) -> String {
29-
module
30-
|> compile_module
29+
pub fn compile(module_ast: ast.Module, module: module.Module) -> String {
30+
module_ast
31+
|> compile_module(module)
3132
|> doc.to_string(max_width)
3233
}
3334

3435
// -- Modules --
3536

36-
fn compile_module(module: ast.Module) -> Document {
37-
// TODO: actually resolve imports
38-
let ast.Module(imports, declarations) = module
37+
fn compile_module(module_ast: ast.Module, module: module.Module) -> Document {
38+
let ast.Module(imports, declarations) = module_ast
3939

40-
let prelude = doc.from_string("import * as $ from './spark.prelude.mjs';")
40+
let prelude =
41+
doc.from_string(
42+
"import * as $ from '"
43+
<> module.resolve_import(module, ["spark.prelude"])
44+
<> "';",
45+
)
4146

4247
let imports =
4348
imports
44-
|> list.map(compile_import)
49+
|> list.map(compile_import(_, module))
4550
|> list.prepend(prelude)
4651
|> doc.join(doc.line)
4752

@@ -53,16 +58,16 @@ fn compile_module(module: ast.Module) -> Document {
5358
doc.concat([imports, doc.lines(2), declarations])
5459
}
5560

56-
fn compile_import(i: ast.Import) -> Document {
57-
let ast.Import(import_path, rename) = i
61+
fn compile_import(i: ast.Import, module: module.Module) -> Document {
62+
let ast.Import(import_segments, rename) = i
5863

59-
let path = "./" <> string.join(import_path, "/") <> ".mjs"
64+
let path = module.resolve_import(module, import_segments)
6065

6166
let name =
6267
case rename {
6368
Some(rename) -> rename
6469
None -> {
65-
let assert Ok(last) = list.last(import_path)
70+
let assert Ok(last) = list.last(import_segments)
6671
last
6772
}
6873
}

‎src/spark/compile/pattern.gleam

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import gleam/option.{None, Some}
66
import gleam/pair
77
import gleam/string
88
import spark/ast
9-
import spark/compile/util
9+
import spark/util
1010

1111
// ---- TYPES ------------------------------------------------------------------
1212

‎src/spark/file.gleam

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import filepath
2+
import gleam/result.{try}
3+
import gleam_community/ansi
4+
import simplifile
5+
6+
/// Write a file, creating directories as needed.
7+
///
8+
pub fn write_all(path: String, contents: String) -> Result(Nil, String) {
9+
let dir = filepath.directory_name(path)
10+
use _ <- try(
11+
simplifile.create_directory_all(dir)
12+
|> result.replace_error(file_error("create this directory", dir)),
13+
)
14+
simplifile.write(path, contents)
15+
|> result.replace_error(file_error("write to this file", path))
16+
}
17+
18+
/// Read a file.
19+
///
20+
pub fn read(path: String) -> Result(String, String) {
21+
simplifile.read(path)
22+
|> result.replace_error(file_error("read this file", path))
23+
}
24+
25+
/// Copy a file at the first path to the second path.
26+
///
27+
pub fn copy(from: String, to: String) -> Result(Nil, String) {
28+
simplifile.copy_file(from, to)
29+
|> result.replace_error(file_error("copy this file", from))
30+
}
31+
32+
/// List files in a directory.
33+
///
34+
pub fn get_files(dir: String) -> Result(List(String), String) {
35+
simplifile.get_files(dir)
36+
|> result.replace_error(file_error("read this directory", dir))
37+
}
38+
39+
fn file_error(message, path) -> String {
40+
ansi.red("I was unable to " <> message <> ": ") <> path
41+
}

‎src/spark/module.gleam

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import filepath
2+
import gleam/list
3+
import gleam/string
4+
import spark/util
5+
6+
// ---- TYPES ------------------------------------------------------------------
7+
8+
pub type Module {
9+
Module(segments: List(String), path: String)
10+
}
11+
12+
// ---- CONSTRUCTOR ------------------------------------------------------------
13+
14+
pub fn from_path(path: String, inside_dir: String) -> Result(Module, Nil) {
15+
let stripped_path =
16+
path
17+
|> util.remove_prefix(inside_dir <> "/")
18+
19+
let segments = filepath.split(stripped_path)
20+
21+
case
22+
segments
23+
|> list.all(fn(segment) {
24+
util.capitalise(segment) == segment
25+
&& {
26+
filepath.extension(segment) == Ok("spark")
27+
|| filepath.strip_extension(segment) == segment
28+
}
29+
})
30+
{
31+
False -> Error(Nil)
32+
True ->
33+
segments
34+
|> list.map(filepath.strip_extension)
35+
|> Module(path)
36+
|> Ok
37+
}
38+
}
39+
40+
// ---- FUNCTIONS --------------------------------------------------------------
41+
42+
/// Resolves an import from the given module to an import path, from the root of
43+
/// the project.
44+
///
45+
pub fn resolve_import(module: Module, import_segments: List(String)) -> String {
46+
let prefix = case module.segments {
47+
[_] | [] -> "./"
48+
[_, ..rest] ->
49+
rest
50+
|> list.map(fn(_) { ".." })
51+
|> string.join("/")
52+
<> "/"
53+
}
54+
55+
prefix <> string.join(import_segments, with: "/") <> ".mjs"
56+
}

0 commit comments

Comments
 (0)