├── test ├── hello_world │ ├── src │ │ ├── nest │ │ │ ├── bird.js │ │ │ ├── bird!.gleam │ │ │ └── bird.gleam │ │ ├── hello_world.gleam │ │ ├── other.gleam │ │ └── hello_world.app.src │ ├── rebar.lock │ ├── gleam.toml │ ├── gen │ │ ├── src │ │ │ ├── other.erl │ │ │ ├── hello_world.erl │ │ │ └── nest@bird.erl │ │ └── test │ │ │ └── hello_test.erl │ ├── test │ │ └── hello_test.gleam │ ├── README.md │ ├── rebar.config │ └── .gitignore ├── core_language │ ├── .gitignore │ ├── gleam.toml │ ├── src │ │ ├── mod_with_numbers_0123456789.gleam │ │ ├── record_update.gleam │ │ ├── shadowed_module.gleam │ │ ├── should.gleam │ │ └── core_language_tests.app.src │ ├── test │ │ ├── floats_test.gleam │ │ ├── mod_with_numbers_test.gleam │ │ ├── should_test.gleam │ │ ├── gleam_should.erl │ │ ├── shadowed_module_test.gleam │ │ ├── call_returned_function_test.gleam │ │ ├── binary_operators_test.gleam │ │ ├── record_update_test.gleam │ │ ├── unicode_test.gleam │ │ ├── ints_test.gleam │ │ └── bit_string_test.gleam │ ├── rebar.config │ └── README.md ├── build_with_gleam │ ├── src │ │ ├── bob_native.erl │ │ └── bob.gleam │ ├── test │ │ └── bob_test.gleam │ ├── .gitignore │ ├── _build │ │ └── default │ │ │ └── lib │ │ │ ├── gleam_helper │ │ │ ├── src │ │ │ │ └── helper │ │ │ │ │ ├── one.gleam │ │ │ │ │ └── two.gleam │ │ │ └── gleam.toml │ │ │ ├── gleam_otp │ │ │ └── gleam.toml │ │ │ └── gleam_stdlib │ │ │ └── gleam.toml │ └── gleam.toml └── errors │ └── type_unify_int_string │ ├── src │ └── type_unify_int_string.gleam │ └── gleam.toml ├── Cross.toml ├── .gitignore ├── containers ├── scratch.dockerfile ├── elixir-slim.dockerfile ├── elixir.dockerfile ├── erlang-slim.dockerfile ├── erlang.dockerfile ├── elixir-alpine.dockerfile └── erlang-alpine.dockerfile ├── .editorconfig ├── images └── gleam-logo-readme.png ├── templates ├── documentation_page.html ├── highlightjs-gleam.js ├── documentation_module.html └── gleam.js ├── RELEASE.md ├── src ├── eunit │ └── eunit_runner.erl ├── build │ ├── compile_escript.erl │ ├── compile_package.rs │ ├── project_root.rs │ ├── dep_tree.rs │ ├── project_compiler.rs │ └── package_compiler.rs ├── line_numbers.rs ├── parse │ ├── extra.rs │ ├── token.rs │ ├── error.rs │ └── tests.rs ├── shell.rs ├── typ │ ├── test_helpers.rs │ ├── fields.rs │ ├── hydrator.rs │ └── prelude.rs ├── cli.rs ├── docs │ ├── source_links.rs │ ├── tests.rs │ └── command.rs ├── ast │ ├── constant.rs │ ├── untyped.rs │ └── typed.rs ├── diagnostic.rs ├── config.rs ├── eunit.rs ├── format │ └── command.rs ├── codegen.rs ├── metadata │ └── tests.rs ├── build.rs ├── metadata.rs ├── pretty │ └── tests.rs ├── project │ └── source_tree.rs ├── project.rs └── main.rs ├── .gitattributes ├── Makefile ├── deny.toml ├── Cargo.toml ├── .github └── workflows │ ├── release-containers.yaml │ ├── release.yaml │ └── ci.yaml ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── schema.capnp └── README.md /test/hello_world/src/nest/bird.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/hello_world/rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /test/hello_world/src/hello_world.gleam: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/hello_world/src/nest/bird!.gleam: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/hello_world/src/nest/bird.gleam: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/core_language/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | gen/ 3 | *.beam 4 | -------------------------------------------------------------------------------- /test/build_with_gleam/src/bob_native.erl: -------------------------------------------------------------------------------- 1 | -module(bob_native). 2 | -------------------------------------------------------------------------------- /test/core_language/gleam.toml: -------------------------------------------------------------------------------- 1 | name = "core_language_tests" 2 | 3 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-musl] 2 | image = "clux/muslrust" -------------------------------------------------------------------------------- /test/hello_world/gleam.toml: -------------------------------------------------------------------------------- 1 | name = "hello_world" 2 | tool = "other" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.rs.bk 2 | **/*.beam 3 | /target/ 4 | /tmp 5 | .idea/ 6 | .idea/* 7 | -------------------------------------------------------------------------------- /containers/scratch.dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | COPY gleam /bin 3 | CMD ["gleam"] 4 | -------------------------------------------------------------------------------- /containers/elixir-slim.dockerfile: -------------------------------------------------------------------------------- 1 | FROM elixir:slim 2 | COPY gleam /bin 3 | CMD ["gleam"] 4 | -------------------------------------------------------------------------------- /containers/elixir.dockerfile: -------------------------------------------------------------------------------- 1 | FROM elixir:latest 2 | COPY gleam /bin 3 | CMD ["gleam"] 4 | -------------------------------------------------------------------------------- /containers/erlang-slim.dockerfile: -------------------------------------------------------------------------------- 1 | FROM erlang:slim 2 | COPY gleam /bin 3 | CMD ["gleam"] 4 | -------------------------------------------------------------------------------- /containers/erlang.dockerfile: -------------------------------------------------------------------------------- 1 | FROM erlang:latest 2 | COPY gleam /bin 3 | CMD ["gleam"] 4 | -------------------------------------------------------------------------------- /containers/elixir-alpine.dockerfile: -------------------------------------------------------------------------------- 1 | FROM elixir:alpine 2 | COPY gleam /bin 3 | CMD ["gleam"] 4 | -------------------------------------------------------------------------------- /containers/erlang-alpine.dockerfile: -------------------------------------------------------------------------------- 1 | FROM erlang:alpine 2 | COPY gleam /bin 3 | CMD ["gleam"] 4 | -------------------------------------------------------------------------------- /test/build_with_gleam/test/bob_test.gleam: -------------------------------------------------------------------------------- 1 | pub fn bob_test() { 2 | assert 1 = 2 3 | 1 4 | } 5 | -------------------------------------------------------------------------------- /test/core_language/src/mod_with_numbers_0123456789.gleam: -------------------------------------------------------------------------------- 1 | pub fn hello() { 2 | "world" 3 | } 4 | -------------------------------------------------------------------------------- /test/hello_world/gen/src/other.erl: -------------------------------------------------------------------------------- 1 | -module(other). 2 | -compile(no_auto_import). 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/build_with_gleam/.gitignore: -------------------------------------------------------------------------------- 1 | _build/**/*.erl 2 | _build/**/*.app 3 | _build/default/lib/bob 4 | -------------------------------------------------------------------------------- /test/build_with_gleam/_build/default/lib/gleam_helper/src/helper/one.gleam: -------------------------------------------------------------------------------- 1 | pub external type One 2 | -------------------------------------------------------------------------------- /test/build_with_gleam/_build/default/lib/gleam_helper/src/helper/two.gleam: -------------------------------------------------------------------------------- 1 | pub external type Two 2 | -------------------------------------------------------------------------------- /test/core_language/src/record_update.gleam: -------------------------------------------------------------------------------- 1 | pub type Box(a) { 2 | Box(tag: String, value: a) 3 | } 4 | -------------------------------------------------------------------------------- /test/hello_world/test/hello_test.gleam: -------------------------------------------------------------------------------- 1 | import hello_world 2 | 3 | fn run(x, y) { 4 | x + y 5 | } 6 | -------------------------------------------------------------------------------- /test/hello_world/gen/src/hello_world.erl: -------------------------------------------------------------------------------- 1 | -module(hello_world). 2 | -compile(no_auto_import). 3 | 4 | 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*.html] 5 | indent_style = space 6 | indent_size = 2 -------------------------------------------------------------------------------- /images/gleam-logo-readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleam-lang/gleam-compiler-ci-test/main/images/gleam-logo-readme.png -------------------------------------------------------------------------------- /test/hello_world/src/other.gleam: -------------------------------------------------------------------------------- 1 | pub fn add(a, b) { 2 | a + b 3 | } 4 | 5 | pub fn main() { 6 | 1 7 | |> add() 8 | } 9 | -------------------------------------------------------------------------------- /test/build_with_gleam/_build/default/lib/gleam_helper/gleam.toml: -------------------------------------------------------------------------------- 1 | name = "gleam_helper" 2 | tool = "gleam" 3 | version = "0.8" 4 | -------------------------------------------------------------------------------- /test/errors/type_unify_int_string/src/type_unify_int_string.gleam: -------------------------------------------------------------------------------- 1 | pub fn hello_world() { 2 | let x: Int = "Eh?" 3 | x 4 | } 5 | -------------------------------------------------------------------------------- /test/hello_world/README.md: -------------------------------------------------------------------------------- 1 | hello_world 2 | ===== 3 | 4 | An OTP library 5 | 6 | Build 7 | ----- 8 | 9 | $ rebar3 compile 10 | -------------------------------------------------------------------------------- /test/hello_world/gen/src/nest@bird.erl: -------------------------------------------------------------------------------- 1 | -module(nest@bird). 2 | -compile(no_auto_import). 3 | 4 | -export([ffok/0]). 5 | 6 | ffok() -> 7 | {ok, 1}. 8 | -------------------------------------------------------------------------------- /test/build_with_gleam/_build/default/lib/gleam_otp/gleam.toml: -------------------------------------------------------------------------------- 1 | name = "gleam_otp" 2 | tool = "gleam" 3 | version = "0.2.0" 4 | 5 | [dependencies] 6 | gleam_helper = "1.0.0" 7 | -------------------------------------------------------------------------------- /test/build_with_gleam/_build/default/lib/gleam_stdlib/gleam.toml: -------------------------------------------------------------------------------- 1 | name = "gleam_stdlib" 2 | tool = "gleam" 3 | version = "0.8.0" 4 | 5 | [dependencies] 6 | gleam_helper = "1.0.0" 7 | -------------------------------------------------------------------------------- /test/core_language/test/floats_test.gleam: -------------------------------------------------------------------------------- 1 | import should 2 | 3 | pub fn division_by_zero_test() { 4 | should.equal(2.0 /. 2.0, 1.0) 5 | should.equal(2.0 /. 0.0, 0.0) 6 | } 7 | -------------------------------------------------------------------------------- /test/core_language/src/shadowed_module.gleam: -------------------------------------------------------------------------------- 1 | pub type Person { 2 | Person(age: Int) 3 | } 4 | 5 | pub fn celebrate_birthday(person: Person) -> Person { 6 | Person(age: person.age + 1) 7 | } 8 | -------------------------------------------------------------------------------- /test/errors/type_unify_int_string/gleam.toml: -------------------------------------------------------------------------------- 1 | name = "type_unify_int_string" 2 | 3 | # [docs] 4 | # links = [ 5 | # { title = 'GitHub', href = 'https://github.com/username/project_name' } 6 | # ] 7 | -------------------------------------------------------------------------------- /test/hello_world/gen/test/hello_test.erl: -------------------------------------------------------------------------------- 1 | -module(hello_test). 2 | -compile(no_auto_import). 3 | 4 | -export([app_test/0]). 5 | 6 | run(X, Y) -> 7 | X + Y. 8 | 9 | app_test() -> 10 | 1.....1. 11 | -------------------------------------------------------------------------------- /test/core_language/test/mod_with_numbers_test.gleam: -------------------------------------------------------------------------------- 1 | import mod_with_numbers_0123456789 2 | import should 3 | 4 | pub fn import_test() { 5 | mod_with_numbers_0123456789.hello() 6 | |> should.equal("world") 7 | } 8 | -------------------------------------------------------------------------------- /test/build_with_gleam/src/bob.gleam: -------------------------------------------------------------------------------- 1 | import helper/one 2 | 3 | pub type CanWeFixIt = 4 | Bool 5 | 6 | pub type Box { 7 | Box(inner: one.One) 8 | } 9 | 10 | pub fn can_we_fix_it() -> String { 11 | "Yes we can!" 12 | } 13 | -------------------------------------------------------------------------------- /templates/documentation_page.html: -------------------------------------------------------------------------------- 1 | {% extends "documentation_layout.html" %} 2 | 3 | {% block title %} 4 | {{ title }} - {{ project_name }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 | {{ content|safe }} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /test/core_language/src/should.gleam: -------------------------------------------------------------------------------- 1 | pub external type Expectation 2 | 3 | pub external fn equal(a, a) -> Expectation = 4 | "gleam_should" "should_equal" 5 | 6 | pub external fn not_equal(a, a) -> Expectation = 7 | "gleam_should" "should_not_equal" 8 | -------------------------------------------------------------------------------- /test/hello_world/rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info]}. 2 | {src_dirs, ["src", "gen/src"]}. 3 | 4 | {profiles, [ 5 | {test, [ 6 | {src_dirs, ["src", "test", "gen/src", "gen/test"]} 7 | ]} 8 | ]}. 9 | 10 | {deps, [ 11 | ]}. 12 | -------------------------------------------------------------------------------- /test/hello_world/.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | .idea 17 | *.iml 18 | rebar3.crashdump 19 | doc 20 | -------------------------------------------------------------------------------- /test/core_language/test/should_test.gleam: -------------------------------------------------------------------------------- 1 | import should 2 | 3 | pub fn equal_test() { 4 | should.equal(1, 1) 5 | should.equal(True, True) 6 | } 7 | 8 | pub fn not_equal_test() { 9 | should.not_equal(1, 2) 10 | should.not_equal(True, False) 11 | } 12 | -------------------------------------------------------------------------------- /test/core_language/test/gleam_should.erl: -------------------------------------------------------------------------------- 1 | -module(gleam_should). 2 | -include_lib("eunit/include/eunit.hrl"). 3 | 4 | -export([should_equal/2, should_not_equal/2]). 5 | 6 | should_equal(Actual, Expected) -> ?assertEqual(Expected, Actual). 7 | should_not_equal(Actual, Expected) -> ?assertNotEqual(Expected, Actual). 8 | -------------------------------------------------------------------------------- /test/core_language/test/shadowed_module_test.gleam: -------------------------------------------------------------------------------- 1 | import should 2 | import shadowed_module.{Person} 3 | 4 | pub fn shadowed_module_test() { 5 | let shadowed_module = Person(18) 6 | let shadowed_module = shadowed_module.celebrate_birthday(shadowed_module) 7 | 8 | should.equal(shadowed_module.age, 19) 9 | } 10 | -------------------------------------------------------------------------------- /test/core_language/rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info, warnings_as_errors]}. 2 | {src_dirs, ["src", "gen/src"]}. 3 | 4 | {profiles, [ 5 | {test, [ 6 | {pre_hooks, [{compile, "cargo run -- build ."}]}, 7 | {src_dirs, ["src", "test", "gen/src", "gen/test"]} 8 | ]} 9 | ]}. 10 | 11 | {deps, []}. 12 | 13 | -------------------------------------------------------------------------------- /test/hello_world/src/hello_world.app.src: -------------------------------------------------------------------------------- 1 | {application, hello_world, 2 | [{description, "An OTP library"}, 3 | {vsn, "0.1.0"}, 4 | {registered, []}, 5 | {applications, 6 | [kernel, 7 | stdlib 8 | ]}, 9 | {env,[]}, 10 | {modules, []}, 11 | 12 | {maintainers, []}, 13 | {licenses, ["Apache 2.0"]}, 14 | {links, []} 15 | ]}. 16 | -------------------------------------------------------------------------------- /test/core_language/test/call_returned_function_test.gleam: -------------------------------------------------------------------------------- 1 | import should 2 | 3 | type FnBox { 4 | FnBox(f: fn(Int) -> Int) 5 | } 6 | 7 | pub fn call_record_access_function_test() { 8 | let b = FnBox(f: fn(x) { x }) 9 | 10 | should.equal(5, b.f(5)) 11 | } 12 | 13 | pub fn call_tuple_access_function_test() { 14 | let t = tuple(fn(x) { x }) 15 | 16 | should.equal(5, t.0(5)) 17 | } 18 | -------------------------------------------------------------------------------- /test/build_with_gleam/gleam.toml: -------------------------------------------------------------------------------- 1 | name = "bob" 2 | tool = "gleam" 3 | version = "1.1.0" 4 | description = "Construction documentary" 5 | 6 | public_modules = [ 7 | "bob", 8 | "bob/the/builder", 9 | ] 10 | 11 | [dependencies] 12 | gleam_stdlib = "0.8.0" 13 | gleam_otp = "0.2.0" 14 | 15 | [dev-dependencies] 16 | gleam_test = "0.3.0" 17 | 18 | [docs] 19 | pages = [ 20 | { title = "t", path = "p", source = "s" }, 21 | ] 22 | -------------------------------------------------------------------------------- /test/core_language/test/binary_operators_test.gleam: -------------------------------------------------------------------------------- 1 | import should 2 | 3 | pub fn precedence_test() { 4 | should.equal(1 + 2 * 3, 7) 5 | should.equal(3 * 1 + 2, 5) 6 | should.equal({ 1 + 2 } * 3, 9) 7 | should.equal(1 + 2 * 3, 7) 8 | should.equal(3 * { 1 + 2 }, 9) 9 | should.equal(1 + 2 * 3 + 4, 11) 10 | should.equal(2 * 3 + 4 * 5, 26) 11 | should.equal(2 * { 3 + 1 } / 2, 4) 12 | should.equal(5 + 3 / 3 * 2 - 6 * 4, -17) 13 | } 14 | -------------------------------------------------------------------------------- /test/core_language/src/core_language_tests.app.src: -------------------------------------------------------------------------------- 1 | {application,core_language_tests, 2 | [{description,"Core language tests for the Gleam programming language"}, 3 | {vsn,"0.7.0"}, 4 | {registered,[]}, 5 | {applications,[kernel,stdlib]}, 6 | {env,[]}, 7 | {modules,[]}, 8 | {licenses,["Apache 2.0"]}, 9 | {links,[]}, 10 | {include_files, ["gleam.toml", "gen"]}]}. 11 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release checklist 2 | 3 | 1. Update the version in `Cargo.toml`. 4 | 2. Update versions in `src/new.rs` for stdlib etc if required. 5 | 3. Run `make test build`. 6 | 4. Create and test a new project to verify it works. 7 | 5. Update CHANGELOG.md with new version and link to blog post (if present) 8 | 6. Git commit, tag, push, push tags. 9 | 7. Wait for CI release build to finish. 10 | 8. Publish release on GitHub from draft made by CI. 11 | 9. Update version in `Cargo.toml` to next-dev. 12 | -------------------------------------------------------------------------------- /test/core_language/README.md: -------------------------------------------------------------------------------- 1 | # Gleam Core Language Tests 2 | 3 | This is a collection of basic tests and sanity checks of the core language written in gleam. 4 | 5 | # Running 6 | 7 | From this directory run `rebar3 eunit`. 8 | 9 | This will first build gleam from source, then compile the gleam modules in this project 10 | and then run the tests. You must have `rust`, `rebar3`, and `erlang` installed. 11 | 12 | # Adding Tests 13 | 14 | Any function ending in `_test` in a module in the `test` directory will get run as a test by 15 | eunit. 16 | -------------------------------------------------------------------------------- /test/core_language/test/record_update_test.gleam: -------------------------------------------------------------------------------- 1 | import should 2 | import record_update 3 | 4 | pub type Person { 5 | Person(name: String, age: Int, country: String) 6 | } 7 | 8 | pub fn record_update_test() { 9 | let past = Person("Quinn", 27, "Canada") 10 | let present = Person(..past, country: "USA", age: past.age + 1) 11 | 12 | should.equal(present, Person("Quinn", 28, "USA")) 13 | 14 | let module_box = record_update.Box("a", 5) 15 | let updated = record_update.Box(..module_box, value: 6) 16 | 17 | should.equal(updated, record_update.Box("a", 6)) 18 | } 19 | -------------------------------------------------------------------------------- /test/core_language/test/unicode_test.gleam: -------------------------------------------------------------------------------- 1 | import should 2 | 3 | external fn to_graphemes(String) -> List(List(Int)) = 4 | "string" "to_graphemes" 5 | 6 | pub fn unicode_overflow_test() { 7 | // In erlang, literally creating binaries can cause entries to overflow. 8 | // For example `<<"🌵">> == <<"5">>` evaluates to true. 9 | // This checks that we are not doing that. 10 | // See: https://github.com/gleam-lang/gleam/issues/457 11 | "🌵" 12 | |> should.not_equal("5") 13 | 14 | "🤷‍♂️" 15 | |> to_graphemes 16 | |> should.equal([[129335, 8205, 9794, 65039]]) 17 | } 18 | -------------------------------------------------------------------------------- /src/eunit/eunit_runner.erl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env erlang 2 | -mode(compile). 3 | 4 | main([EbinPaths, AllModules])-> 5 | true = code:add_patha(filename:dirname(escript:script_name())), 6 | SeperatedEbinPaths = string:tokens(EbinPaths, ","), 7 | ok = code:add_paths(SeperatedEbinPaths), 8 | SeperatedModules = string:tokens(AllModules, ","), 9 | Modules = lists:map(fun(X) -> list_to_atom(X) end, SeperatedModules), 10 | code:load_file(eunit_progress), 11 | halt(case eunit:test(Modules, [inparallel, verbose, no_tty, {report, {eunit_progress, [{colored, true}]}}]) of ok -> 0; error -> 1 end). 12 | -------------------------------------------------------------------------------- /src/build/compile_escript.erl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env erlang 2 | 3 | % TODO: build in parallel 4 | main([ProfilePath]) -> 5 | ErlangFiles = filelib:wildcard([ProfilePath, "/**/{src,test}/*.erl"]), 6 | lists:foreach(fun(F) -> compile(F) end, ErlangFiles). 7 | 8 | compile(ErlangFile) -> 9 | EBin = ebin_path(ErlangFile), 10 | ok = filelib:ensure_dir([EBin, $/]), 11 | ErlangName = filename:rootname(ErlangFile), 12 | {ok, _} = compile:file(ErlangName, [{outdir, EBin}, return_errors]). 13 | 14 | ebin_path(File) -> 15 | PackageRoot = filename:dirname(filename:dirname(File)), 16 | filename:join(PackageRoot, "ebin"). 17 | -------------------------------------------------------------------------------- /src/line_numbers.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub struct LineNumbers { 3 | line_starts: Vec, 4 | } 5 | 6 | impl LineNumbers { 7 | pub fn new(src: &str) -> Self { 8 | Self { 9 | line_starts: std::iter::once(0) 10 | .chain(src.match_indices('\n').map(|(i, _)| i + 1)) 11 | .collect(), 12 | } 13 | } 14 | 15 | /// Get the line number for a byte index 16 | pub fn line_number(&self, byte_index: usize) -> usize { 17 | self.line_starts 18 | .binary_search(&byte_index) 19 | .unwrap_or_else(|next_line| next_line - 1) 20 | + 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/build/compile_package.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | build::{Origin, PackageCompiler}, 3 | fs::FileSystemAccessor, 4 | CompilePackage, Result, 5 | }; 6 | use std::collections::HashMap; 7 | 8 | pub fn command(options: CompilePackage) -> Result<()> { 9 | // TODO: Load precompiled libraries 10 | tracing::info!("Reading precompiled module metadata files"); 11 | let mut type_manifests = HashMap::new(); 12 | let mut defined_modules = HashMap::new(); 13 | 14 | let mut compiler = options 15 | .into_package_compiler_options() 16 | .into_compiler(FileSystemAccessor::new())? 17 | .compile(&mut type_manifests, &mut defined_modules)?; 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # We want fancy syntax highlighting on GitHub, though GitHub doesn't know how 2 | # to speak Gleam. Until it does (maybe one day!) we'll tell GitHub that these 3 | # files are Rust, which has a similar enough syntax for the highlighting to 4 | # work in most cases. 5 | # The only caveat is that we need to add a `;` after each use of the `type` 6 | # keyword as our type syntax is different to theirs in a way that confuses 7 | # GitHub. 8 | *.gleam linguist-language=Rust 9 | 10 | # Erlang files generated by the Gleam compiler 11 | */gen/*/*.erl linguist-generated=true 12 | /docs/**/* linguist-generated=true 13 | 14 | # Generate Cap'n Proto code 15 | src/bindata_capnp.rs linguist-generated=true 16 | -------------------------------------------------------------------------------- /test/core_language/test/ints_test.gleam: -------------------------------------------------------------------------------- 1 | import should 2 | 3 | const hex_int = 0xF 4 | 5 | pub fn bases_test() { 6 | let octal_int = 0o17 7 | let binary_int = 0b00001111 8 | 9 | let tuple(x, y) = tuple(octal_int, binary_int) 10 | 11 | should.equal(x, 15) 12 | should.equal(y, 15) 13 | } 14 | 15 | pub fn minus_lexing_test() { 16 | // 1-1 should lex as 1 - 1 17 | should.equal({1-1}, 0) 18 | // a-1 should lex as a - 1 19 | let a = 1 20 | should.equal({a-1}, 0) 21 | // 1- 1 should lex as 1 - 1 22 | should.equal({1- 1}, 0) 23 | } 24 | 25 | pub fn division_by_zero_test() { 26 | should.equal(1 / 1, 1) 27 | should.equal(1 / 0, 0) 28 | should.equal(3 % 2, 1) 29 | should.equal(3 % 0, 0) 30 | } 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Goals to be specified by user 3 | # 4 | 5 | .PHONY: help 6 | help: 7 | @cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 8 | 9 | .PHONY: build 10 | build: ## Build the compiler 11 | cargo build --release 12 | 13 | .PHONY: install 14 | install: ## Build the Gleam compiler and place it on PATH 15 | cargo install --path . --force --locked 16 | 17 | .PHONY: test ## Run all tests 18 | test: 19 | cargo test 20 | 21 | .PHONY: test-watch 22 | test-watch: ## Run compiler tests when files change 23 | watchexec -e rs,html,capnp "cargo test" 24 | 25 | # Debug print vars with `make print-VAR_NAME` 26 | print-%: ; @echo $*=$($*) 27 | -------------------------------------------------------------------------------- /src/parse/extra.rs: -------------------------------------------------------------------------------- 1 | use crate::ast::SrcSpan; 2 | 3 | #[derive(Debug, PartialEq)] 4 | pub struct ModuleExtra { 5 | pub module_comments: Vec, 6 | pub doc_comments: Vec, 7 | pub comments: Vec, 8 | pub empty_lines: Vec, 9 | } 10 | 11 | impl ModuleExtra { 12 | pub fn new() -> Self { 13 | ModuleExtra { 14 | module_comments: vec![], 15 | doc_comments: vec![], 16 | comments: vec![], 17 | empty_lines: vec![], 18 | } 19 | } 20 | } 21 | 22 | #[derive(Debug, PartialEq)] 23 | pub struct Comment<'a> { 24 | pub start: usize, 25 | pub content: &'a str, 26 | } 27 | 28 | impl<'a> From<(&SrcSpan, &'a str)> for Comment<'a> { 29 | fn from(src: (&SrcSpan, &'a str)) -> Comment<'a> { 30 | Comment { 31 | start: src.0.start, 32 | content: &src.1[src.0.start..src.0.end], 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/shell.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | build::{self, project_root::ProjectRoot}, 3 | error::{Error, GleamExpect}, 4 | }; 5 | use std::path::PathBuf; 6 | use std::process::Command; 7 | 8 | pub fn command(root_string: String) -> Result<(), Error> { 9 | let root_path = PathBuf::from(root_string); 10 | let root = ProjectRoot::new(root_path.clone()); 11 | let config = root.root_config()?; 12 | 13 | // Build project 14 | let _ = build::main(config, root_path)?; 15 | 16 | // Don't exit on ctrl+c as it is used by child erlang shell 17 | ctrlc::set_handler(move || {}).gleam_expect("Error setting Ctrl-C handler"); 18 | 19 | // Prepare the Erlang shell command 20 | let mut command = Command::new("erl"); 21 | 22 | // Specify locations of .beam files 23 | for entry in crate::fs::read_dir(root.default_build_lib_path())?.filter_map(Result::ok) { 24 | let _ = command.arg("-pa"); 25 | let _ = command.arg(entry.path().join("ebin")); 26 | } 27 | 28 | crate::cli::print_running("erl"); 29 | 30 | // Run the shell 31 | tracing::trace!("Running OS process {:?}", command); 32 | let status = command.status().map_err(|e| Error::ShellCommand { 33 | command: "erl".to_string(), 34 | err: Some(e.kind()), 35 | })?; 36 | 37 | if status.success() { 38 | Ok(()) 39 | } else { 40 | Err(Error::ShellCommand { 41 | command: "erl".to_string(), 42 | err: None, 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/typ/test_helpers.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub fn env_types_with(things: &[&str]) -> Vec { 4 | let mut types: Vec<_> = env_types(); 5 | for thing in things { 6 | types.push(thing.to_string()); 7 | } 8 | types 9 | } 10 | 11 | pub fn env_types() -> Vec { 12 | Environment::new(&mut 0, &[], &HashMap::new(), &mut vec![]) 13 | .module_types 14 | .keys() 15 | .map(|s| s.to_string()) 16 | .collect() 17 | } 18 | 19 | pub fn env_vars_with(things: &[&str]) -> Vec { 20 | let mut types: Vec<_> = env_vars(); 21 | for thing in things { 22 | types.push(thing.to_string()); 23 | } 24 | types 25 | } 26 | 27 | pub fn env_vars() -> Vec { 28 | Environment::new(&mut 0, &[], &HashMap::new(), &mut vec![]) 29 | .local_values 30 | .keys() 31 | .map(|s| s.to_string()) 32 | .collect() 33 | } 34 | 35 | pub fn sort_options(e: Error) -> Error { 36 | match e { 37 | Error::UnknownType { 38 | location, 39 | name, 40 | mut types, 41 | } => { 42 | types.sort(); 43 | Error::UnknownType { 44 | location, 45 | name, 46 | types, 47 | } 48 | } 49 | 50 | Error::UnknownVariable { 51 | location, 52 | name, 53 | mut variables, 54 | } => { 55 | variables.sort(); 56 | Error::UnknownVariable { 57 | location, 58 | name, 59 | variables, 60 | } 61 | } 62 | 63 | Error::UnknownLabels { 64 | unknown, 65 | mut valid, 66 | supplied, 67 | } => { 68 | valid.sort(); 69 | Error::UnknownLabels { 70 | unknown, 71 | valid, 72 | supplied, 73 | } 74 | } 75 | 76 | _ => e, 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | targets = [] 2 | 3 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 4 | [advisories] 5 | db-path = "~/.cargo/advisory-db" 6 | db-urls = ["https://github.com/rustsec/advisory-db"] 7 | vulnerability = "deny" 8 | unmaintained = "warn" 9 | yanked = "warn" 10 | notice = "warn" 11 | ignore = [ 12 | # https://rustsec.org/advisories/RUSTSEC-2020-0041.html 13 | # sized-chunks: Multiple soundness issues in Chunk and InlineArray 14 | "RUSTSEC-2020-0041", 15 | # https://rustsec.org/advisories/RUSTSEC-2020-0095.html 16 | # difference is unmaintained 17 | "RUSTSEC-2020-0095", 18 | # https://rustsec.org/advisories/RUSTSEC-2020-0096.html 19 | # `TreeFocus` that unconditionally implements `Send` and `Sync` 20 | "RUSTSEC-2020-0096", 21 | ] 22 | 23 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 24 | [licenses] 25 | unlicensed = "deny" 26 | allow = ["MIT", "Apache-2.0", "MPL-2.0", "ISC", "OpenSSL"] 27 | default = "deny" 28 | confidence-threshold = 0.8 29 | 30 | [[licenses.clarify]] 31 | name = "ring" 32 | version = "*" 33 | expression = "MIT AND ISC AND OpenSSL" 34 | license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] 35 | 36 | [licenses.private] 37 | ignore = false 38 | registries = [] 39 | 40 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 41 | [bans] 42 | multiple-versions = "warn" 43 | wildcards = "allow" 44 | highlight = "all" 45 | allow = [] 46 | deny = [] 47 | skip = [ 48 | { name = "ansi_term", version = "=0.11.0" }, 49 | { name = "cfg-if", version = "=0.1.10" }, 50 | { name = "pin-project", version = "=0.4.27" }, 51 | { name = "pin-project-internal", version = "=0.4.27" }, 52 | { name = "rand_core", version = "=0.5.1" }, 53 | { name = "redox_syscall", version = "0.1.57" }, 54 | { name = "strsim", version = "=0.8.0" }, 55 | ] 56 | skip-tree = [] 57 | 58 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 59 | [sources] 60 | unknown-registry = "warn" 61 | unknown-git = "warn" 62 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 63 | allow-git = [] 64 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gleam" 3 | version = "0.14.0-dev" 4 | authors = ["Louis Pilfold "] 5 | edition = "2018" 6 | license-file = "LICENCE" 7 | 8 | [dependencies] 9 | # OS SIGINT and SIGTERM signal handling 10 | ctrlc = { version = "3.1.7", features = ["termination"] } 11 | # Command line interface 12 | clap = "2.33.3" 13 | structopt = "0.3.21" 14 | # Immutable data structures 15 | im = "15.0.0" 16 | # Extra iter methods 17 | itertools = "0.10.0" 18 | # String case conversion 19 | heck = "0.3.2" 20 | # Parsing 21 | regex = "1.4.3" 22 | # Graph data structures 23 | petgraph = "0.5.1" 24 | # Pretty error messages 25 | codespan = "0.11" 26 | codespan-reporting = "0.11" 27 | termcolor = "1.1.2" 28 | # Initialize complex static values at runtime 29 | lazy_static = "1.4.0" 30 | # Data (de)serialisation 31 | serde = { version = "1.0.119", features = ["derive"] } 32 | serde_derive = "1.0.119" 33 | # toml config file parsing 34 | toml = "0.5.8" 35 | # Levenshtein string distance for typo suggestions 36 | strsim = "0.10.0" 37 | # Recursively traversing directories 38 | ignore = "0.4.17" 39 | walkdir = "2.3.1" 40 | # Enum trait impl macros 41 | strum = "0.20.0" 42 | strum_macros = "0.20.1" 43 | # Doc rendering 44 | askama = "0.10.5" 45 | # Markdown in docs 46 | pulldown-cmark = "0.8.0" 47 | # Check for tty 48 | atty = "0.2.14" 49 | # Hex package manager client 50 | hexpm = "1.2.0" 51 | # Allow user to type in sensitive information without showing it in the shell 52 | rpassword = "5.0.0" 53 | # Async runtime 54 | tokio = { version = "1.0.1", features = ["rt", "rt-multi-thread"] } 55 | # Creation of tar file archives 56 | tar = "0.4.30" 57 | # gzip compression 58 | flate2 = "1.0.19" 59 | # Byte array data type 60 | bytes = "1.0" 61 | # Further file system functions (i.e. copy directory) 62 | fs_extra = "1.2.0" 63 | # Logging 64 | tracing = "0.1.22" 65 | tracing-subscriber = "0.2.15" 66 | # Cap'n Proto binary format runtime 67 | capnp = "0.14.0" 68 | 69 | [build-dependencies] 70 | # Data (de)serialisation 71 | serde_derive = "1.0.119" 72 | # Cap'n Proto binary format codegen 73 | capnpc = "0.14.2" 74 | 75 | [dev-dependencies] 76 | pretty_assertions = "0.6.1" 77 | -------------------------------------------------------------------------------- /.github/workflows/release-containers.yaml: -------------------------------------------------------------------------------- 1 | name: release-containers 2 | on: 3 | release: 4 | types: 5 | - "published" 6 | 7 | jobs: 8 | publish-container-images: 9 | name: publish-container-images 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | base-image: 14 | - scratch 15 | - erlang 16 | - erlang-slim 17 | - erlang-alpine 18 | - elixir 19 | - elixir-slim 20 | - elixir-alpine 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v2 25 | 26 | - name: Authenticate with GitHub container registry 27 | uses: docker/login-action@v1 28 | with: 29 | registry: ghcr.io 30 | username: lpil 31 | password: ${{ secrets.CONTAINER_REGISTRY_PERSONAL_ACCESS_TOKEN }} 32 | 33 | - name: Set up Docker Buildx 34 | uses: docker/setup-buildx-action@v1 35 | 36 | - name: Download Gleam archive from GitHub release 37 | run: | 38 | # Strip git ref prefix from version 39 | VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') 40 | 41 | # Download archive 42 | URL=https://github.com/${{ github.repository }}/releases/download/$VERSION/gleam-$VERSION-linux-amd64.tar.gz 43 | echo Downloading $URL 44 | curl -Lo gleam.tar.gz $URL 45 | 46 | - name: Unpack Gleam binary from archive 47 | run: tar xf gleam.tar.gz 48 | 49 | - name: Prepare container meta information 50 | id: container_meta 51 | uses: crazy-max/ghaction-docker-meta@v1 52 | with: 53 | images: ghcr.io/${{ github.repository }} 54 | tag-sha: true 55 | tag-semver: | 56 | {{version}}-${{ matrix.base-image }} 57 | {{major}}.{{minor}}-${{ matrix.base-image }} 58 | 59 | - name: Build and push 60 | uses: docker/build-push-action@v2 61 | with: 62 | context: . 63 | file: containers/${{ matrix.base-image }}.dockerfile 64 | push: true 65 | tags: ${{ steps.container_meta.outputs.tags }} 66 | labels: ${{ steps.container_meta.outputs.labels }} 67 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Error, StandardIOAction}; 2 | use std::io::Write; 3 | use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor}; 4 | 5 | pub fn ask(question: &str) -> Result { 6 | print!("{}: ", question); 7 | std::io::stdout().flush().unwrap(); 8 | let mut answer = String::new(); 9 | let _ = std::io::stdin() 10 | .read_line(&mut answer) 11 | .map_err(|e| Error::StandardIO { 12 | action: StandardIOAction::Read, 13 | err: Some(e.kind()), 14 | })?; 15 | Ok(answer.trim().to_string()) 16 | } 17 | 18 | pub fn ask_password(question: &str) -> Result { 19 | let prompt = format!("{} (will not be printed as you type): ", question); 20 | rpassword::read_password_from_tty(Some(prompt.as_str())) 21 | .map_err(|e| Error::StandardIO { 22 | action: StandardIOAction::Read, 23 | err: Some(e.kind()), 24 | }) 25 | .map(|s| s.trim().to_string()) 26 | } 27 | 28 | pub fn print_compiling(text: &str) { 29 | print_green_prefix("Compiling", text) 30 | } 31 | 32 | pub fn print_running(text: &str) { 33 | print_green_prefix(" Running", text) 34 | } 35 | 36 | pub fn print_green_prefix(prefix: &str, text: &str) { 37 | let buffer_writer = stdout_buffer_writer(); 38 | let mut buffer = buffer_writer.buffer(); 39 | buffer 40 | .set_color(ColorSpec::new().set_bold(true).set_fg(Some(Color::Green))) 41 | .unwrap(); 42 | write!(buffer, "{}", prefix).unwrap(); 43 | buffer.set_color(&ColorSpec::new()).unwrap(); 44 | writeln!(buffer, " {}", text).unwrap(); 45 | buffer_writer.print(&buffer).unwrap(); 46 | } 47 | 48 | pub fn stderr_buffer_writer() -> BufferWriter { 49 | // Don't add color codes to the output if standard error isn't connected to a terminal 50 | termcolor::BufferWriter::stderr(color_choice()) 51 | } 52 | 53 | pub fn stdout_buffer_writer() -> BufferWriter { 54 | // Don't add color codes to the output if standard error isn't connected to a terminal 55 | termcolor::BufferWriter::stdout(color_choice()) 56 | } 57 | 58 | fn color_choice() -> ColorChoice { 59 | if atty::is(atty::Stream::Stderr) { 60 | termcolor::ColorChoice::Auto 61 | } else { 62 | termcolor::ColorChoice::Never 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/core_language/test/bit_string_test.gleam: -------------------------------------------------------------------------------- 1 | import should 2 | 3 | // Helpers 4 | fn integer_fn() { 5 | 1 6 | } 7 | 8 | // Valid values 9 | pub fn function_as_value_test() { 10 | let <> = <> 11 | 12 | should.equal(a, 1) 13 | } 14 | 15 | pub fn integer_to_binary_test() { 16 | let <> = <<1, 17, 42:16>> 17 | 18 | should.equal(a, 1) 19 | should.equal(rest, <<17, 0, 42>>) 20 | } 21 | 22 | // Sizes 23 | pub fn size_variable_from_match_test() { 24 | let << 25 | name_size:8, 26 | name:binary-size(name_size), 27 | " the ":utf8, 28 | species:binary, 29 | >> = <<5, "Frank the Walrus":utf8>> 30 | 31 | should.equal(name, <<"Frank":utf8>>) 32 | should.equal(species, <<"Walrus":utf8>>) 33 | } 34 | 35 | pub fn sizes_with_expressions_test() { 36 | let a = 1 37 | let b = <> 38 | 39 | should.equal(b, <<1:2, 1:4>>) 40 | } 41 | 42 | // Units 43 | pub fn units_test() { 44 | let a = <<1:size(1)-unit(8), 2:size(1)-unit(16)>> 45 | 46 | should.equal(a, <<1, 0, 2>>) 47 | } 48 | 49 | // Strings 50 | pub fn string_test() { 51 | let a = "test" 52 | let <> = a 53 | 54 | should.equal(b, <<"te":utf8>>) 55 | } 56 | 57 | pub fn explicit_utf8_test() { 58 | let a = <<"test":utf8>> 59 | let <> = a 60 | 61 | should.equal(b, <<"te":utf8>>) 62 | } 63 | 64 | pub fn emoji_test() { 65 | let a = <<"😁😀":utf8>> 66 | let <> = a 67 | 68 | should.equal(b, <<"😁":utf8>>) 69 | } 70 | 71 | pub fn codepoint_conversion_test() { 72 | let <> = <<"🐍":utf8>> 73 | let <> = <> 74 | 75 | should.equal(snake_int, 128013) 76 | } 77 | 78 | type StringHaver { 79 | StringHaver(value: String) 80 | } 81 | 82 | pub fn non_literal_strings_test() { 83 | let v = "x" 84 | let t = tuple("y") 85 | let c = StringHaver(value: "z") 86 | let f = fn() { "ß" } 87 | 88 | let y = <> 89 | let <> = y 90 | 91 | should.equal(var_out, 120) 92 | should.equal(tuple_out, 121) 93 | should.equal(custom_type_out, 122) 94 | should.equal(function_out, 50079) // "ß" is encoded as C3 9F in utf8 95 | should.equal(literal_out, 14845585) // "↑" is encoded as E2 86 91 in utf8 96 | } 97 | -------------------------------------------------------------------------------- /src/docs/source_links.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | ast::SrcSpan, 3 | config::{PackageConfig, Repository}, 4 | line_numbers::LineNumbers, 5 | project::Analysed, 6 | }; 7 | use std::path::{Path, PathBuf}; 8 | 9 | pub struct SourceLinker { 10 | line_numbers: LineNumbers, 11 | url_pattern: Option<(String, String)>, 12 | } 13 | 14 | impl SourceLinker { 15 | pub fn new( 16 | project_root: impl AsRef, 17 | project_config: &PackageConfig, 18 | module: &Analysed, 19 | ) -> Self { 20 | let path_in_repo = get_path_in_repo(project_root, &module.path); 21 | 22 | let url_pattern = match &project_config.repository { 23 | Repository::GitHub { user, repo } => Some(( 24 | format!( 25 | "https://github.com/{}/{}/blob/v{}/{}#L", 26 | user, repo, project_config.version, path_in_repo 27 | ), 28 | "-L".to_string(), 29 | )), 30 | Repository::GitLab { user, repo } => Some(( 31 | format!( 32 | "https://gitlab.com/{}/{}/-/blob/v{}/{}#L", 33 | user, repo, project_config.version, path_in_repo 34 | ), 35 | "-".to_string(), 36 | )), 37 | Repository::BitBucket { user, repo } => Some(( 38 | format!( 39 | "https://bitbucket.com/{}/{}/src/v{}/{}#lines-", 40 | user, repo, project_config.version, path_in_repo 41 | ), 42 | ":".to_string(), 43 | )), 44 | Repository::Custom { .. } | Repository::None => None, 45 | }; 46 | 47 | SourceLinker { 48 | line_numbers: LineNumbers::new(&module.src), 49 | url_pattern, 50 | } 51 | } 52 | pub fn url(&self, span: &SrcSpan) -> String { 53 | match &self.url_pattern { 54 | Some((base, line_sep)) => { 55 | let start_line = self.line_numbers.line_number(span.start); 56 | let end_line = self.line_numbers.line_number(span.end); 57 | format!("{}{}{}{}", base, start_line, line_sep, end_line) 58 | } 59 | 60 | None => "".to_string(), 61 | } 62 | } 63 | } 64 | 65 | fn get_path_in_repo(project_root: impl AsRef, path: &PathBuf) -> String { 66 | path.strip_prefix(&project_root) 67 | .ok() 68 | .and_then(Path::to_str) 69 | .unwrap_or("") 70 | .to_string() 71 | } 72 | -------------------------------------------------------------------------------- /src/ast/constant.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::typ::HasType; 3 | 4 | pub type TypedConstant = Constant, String>; 5 | pub type UntypedConstant = Constant<(), ()>; 6 | 7 | #[derive(Debug, PartialEq, Clone)] 8 | pub enum Constant { 9 | Int { 10 | location: SrcSpan, 11 | value: String, 12 | }, 13 | 14 | Float { 15 | location: SrcSpan, 16 | value: String, 17 | }, 18 | 19 | String { 20 | location: SrcSpan, 21 | value: String, 22 | }, 23 | 24 | Tuple { 25 | location: SrcSpan, 26 | elements: Vec, 27 | }, 28 | 29 | List { 30 | location: SrcSpan, 31 | elements: Vec, 32 | typ: T, 33 | }, 34 | 35 | Record { 36 | location: SrcSpan, 37 | module: Option, 38 | name: String, 39 | args: Vec>, 40 | tag: RecordTag, 41 | typ: T, 42 | }, 43 | 44 | BitString { 45 | location: SrcSpan, 46 | segments: Vec>, 47 | }, 48 | } 49 | 50 | impl TypedConstant { 51 | pub fn typ(&self) -> Arc { 52 | match self { 53 | Constant::Int { .. } => crate::typ::int(), 54 | Constant::Float { .. } => crate::typ::float(), 55 | Constant::String { .. } => crate::typ::string(), 56 | Constant::List { typ, .. } => typ.clone(), 57 | Constant::Record { typ, .. } => typ.clone(), 58 | Constant::BitString { .. } => crate::typ::bit_string(), 59 | Constant::Tuple { elements, .. } => { 60 | crate::typ::tuple(elements.iter().map(|e| e.typ()).collect()) 61 | } 62 | } 63 | } 64 | } 65 | 66 | impl HasType for TypedConstant { 67 | fn typ(&self) -> Arc { 68 | self.typ() 69 | } 70 | } 71 | 72 | impl Constant { 73 | pub fn location(&self) -> SrcSpan { 74 | match self { 75 | Constant::Int { location, .. } 76 | | Constant::List { location, .. } 77 | | Constant::Float { location, .. } 78 | | Constant::Tuple { location, .. } 79 | | Constant::String { location, .. } 80 | | Constant::Record { location, .. } 81 | | Constant::BitString { location, .. } => *location, 82 | } 83 | } 84 | 85 | pub fn is_simple(&self) -> bool { 86 | matches!(self, Self::Int { .. } | Self::Float { .. } | Self::String { .. }) 87 | } 88 | } 89 | 90 | impl HasLocation for Constant { 91 | fn location(&self) -> SrcSpan { 92 | self.location() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/diagnostic.rs: -------------------------------------------------------------------------------- 1 | pub use codespan_reporting::diagnostic::{LabelStyle, Severity}; 2 | use codespan_reporting::{diagnostic::Label, files::SimpleFile, term::emit}; 3 | use termcolor::Buffer; 4 | 5 | pub struct DiagnosticLabel { 6 | pub style: LabelStyle, 7 | pub location: crate::ast::SrcSpan, 8 | pub label: String, 9 | } 10 | pub struct Diagnostic { 11 | pub file: String, 12 | pub location: crate::ast::SrcSpan, 13 | pub src: String, 14 | pub title: String, 15 | pub label: String, 16 | } 17 | 18 | pub struct MultiLineDiagnostic { 19 | pub file: String, 20 | pub src: String, 21 | pub title: String, 22 | pub labels: Vec, 23 | } 24 | 25 | pub fn write(buffer: &mut Buffer, d: Diagnostic, severity: Severity) { 26 | let diagnostic = MultiLineDiagnostic { 27 | file: d.file, 28 | src: d.src, 29 | title: d.title, 30 | labels: vec![DiagnosticLabel { 31 | style: LabelStyle::Primary, 32 | location: d.location, 33 | label: d.label, 34 | }], 35 | }; 36 | 37 | write_diagnostic(buffer, diagnostic, severity) 38 | } 39 | 40 | pub fn write_diagnostic(mut buffer: &mut Buffer, d: MultiLineDiagnostic, severity: Severity) { 41 | let file = SimpleFile::new(d.file, d.src); 42 | 43 | let labels = d 44 | .labels 45 | .iter() 46 | .map(|l| { 47 | Label::new(l.style, (), (l.location.start)..(l.location.end)) 48 | .with_message(l.label.clone()) 49 | }) 50 | .collect(); 51 | 52 | let diagnostic = codespan_reporting::diagnostic::Diagnostic::new(severity) 53 | .with_message(d.title) 54 | .with_labels(labels); 55 | 56 | let config = codespan_reporting::term::Config::default(); 57 | emit(&mut buffer, &config, &file, &diagnostic).unwrap(); 58 | } 59 | 60 | /// Describes an error encountered while compiling the project (eg. a name collision 61 | /// between files). 62 | /// 63 | pub struct ProjectErrorDiagnostic { 64 | pub title: String, 65 | pub label: String, 66 | } 67 | 68 | pub fn write_title(buffer: &mut Buffer, title: &str) { 69 | use std::io::Write; 70 | use termcolor::{Color, ColorSpec, WriteColor}; 71 | buffer 72 | .set_color(ColorSpec::new().set_bold(true).set_fg(Some(Color::Red))) 73 | .unwrap(); 74 | write!(buffer, "error").unwrap(); 75 | buffer.set_color(ColorSpec::new().set_bold(true)).unwrap(); 76 | write!(buffer, ": {}\n\n", title).unwrap(); 77 | buffer.set_color(&ColorSpec::new()).unwrap(); 78 | } 79 | 80 | pub fn write_project(buffer: &mut Buffer, d: ProjectErrorDiagnostic) { 81 | use std::io::Write; 82 | use termcolor::{ColorSpec, WriteColor}; 83 | write_title(buffer, d.title.as_ref()); 84 | buffer.set_color(&ColorSpec::new()).unwrap(); 85 | writeln!(buffer, "{}", d.label).unwrap(); 86 | } 87 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | create-release: 8 | name: create-release 9 | runs-on: ubuntu-latest 10 | outputs: 11 | upload_url: ${{ steps.release.outputs.upload_url }} 12 | steps: 13 | - name: Create GitHub release 14 | id: release 15 | uses: actions/create-release@v1 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | with: 19 | tag_name: ${{ github.ref }} 20 | release_name: ${{ github.ref }} 21 | draft: true 22 | 23 | build-release: 24 | name: build-release 25 | needs: create-release 26 | runs-on: ${{ matrix.os }} 27 | strategy: 28 | matrix: 29 | build: [linux-amd64, macos, windows-64bit] 30 | toolchain: [stable] 31 | include: 32 | - build: linux-amd64 33 | os: ubuntu-latest 34 | target: x86_64-unknown-linux-musl 35 | use-cross: true 36 | - build: macos 37 | os: macos-latest 38 | target: x86_64-apple-darwin 39 | use-cross: false 40 | - build: windows-64bit 41 | os: windows-latest 42 | target: x86_64-pc-windows-msvc 43 | use-cross: false 44 | steps: 45 | - name: Checkout repository 46 | uses: actions/checkout@v2 47 | 48 | - name: Install Rust toolchain 49 | uses: actions-rs/toolchain@v1 50 | with: 51 | toolchain: ${{ matrix.toolchain }} 52 | target: ${{ matrix.target }} 53 | profile: minimal 54 | override: true 55 | 56 | - name: Build release binary 57 | uses: actions-rs/cargo@v1 58 | with: 59 | use-cross: ${{ matrix.use-cross }} 60 | command: build 61 | args: --release --target ${{ matrix.target }} 62 | 63 | - name: Build archive 64 | shell: bash 65 | run: | 66 | VERSION="${GITHUB_REF#refs/tags/}" 67 | 68 | if [ "${{ matrix.os }}" = "windows-latest" ]; then 69 | ARCHIVE="gleam-$VERSION-${{ matrix.build }}.zip" 70 | cp "target/${{ matrix.target }}/release/gleam.exe" "gleam.exe" 71 | 7z a "$ARCHIVE" "gleam.exe" 72 | echo "ASSET=$ARCHIVE" >> $GITHUB_ENV 73 | else 74 | ARCHIVE="gleam-$VERSION-${{ matrix.build }}.tar.gz" 75 | cp "target/${{ matrix.target }}/release/gleam" "gleam" 76 | tar -czvf "$ARCHIVE" "gleam" 77 | echo "ASSET=$ARCHIVE" >> $GITHUB_ENV 78 | fi 79 | 80 | - name: Upload release archive 81 | uses: actions/upload-release-asset@v1 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | with: 85 | upload_url: ${{ needs.create-release.outputs.upload_url }} 86 | asset_path: ${{ env.ASSET }} 87 | asset_name: ${{ env.ASSET }} 88 | asset_content_type: application/gzip 89 | -------------------------------------------------------------------------------- /src/build/project_root.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | build::Origin, 3 | config::{self, PackageConfig}, 4 | error::Error, 5 | }; 6 | use std::collections::HashMap; 7 | use std::path::PathBuf; 8 | 9 | // Directory names 10 | const DIR_NAME_BUILD: &str = "_build"; 11 | const DIR_NAME_PROFILE_DEFAULT: &str = "default"; 12 | const DIR_NAME_LIB: &str = "lib"; 13 | const DIR_NAME_PACKAGE_SRC: &str = "src"; 14 | const DIR_NAME_PACKAGE_TEST: &str = "test"; 15 | const DIR_NAME_PACKAGE_EBIN: &str = "ebin"; 16 | 17 | #[derive(Debug)] 18 | pub struct ProjectRoot { 19 | pub root: PathBuf, 20 | } 21 | 22 | impl ProjectRoot { 23 | pub fn new(root: PathBuf) -> Self { 24 | Self { root } 25 | } 26 | 27 | pub fn root_config(&self) -> Result { 28 | config::read_project_config(&self.root) 29 | } 30 | 31 | /// Load the gleam.toml config files for all packages except the 32 | /// top level package. 33 | /// 34 | pub fn package_configs( 35 | &self, 36 | root_name: &str, 37 | ) -> Result, Error> { 38 | let mut configs = HashMap::with_capacity(25); 39 | for dir_entry in crate::fs::read_dir(self.default_build_lib_path())?.filter_map(Result::ok) 40 | { 41 | let config = config::read_project_config(dir_entry.path())?; 42 | if config.name != root_name { 43 | configs.insert(config.name.clone(), config); 44 | } 45 | } 46 | Ok(configs) 47 | } 48 | 49 | pub fn src_path(&self) -> PathBuf { 50 | self.root.join(DIR_NAME_PACKAGE_SRC) 51 | } 52 | 53 | pub fn build_path(&self) -> PathBuf { 54 | self.root.join(DIR_NAME_BUILD) 55 | } 56 | 57 | pub fn default_build_lib_path(&self) -> PathBuf { 58 | self.build_path() 59 | .join(DIR_NAME_PROFILE_DEFAULT) 60 | .join(DIR_NAME_LIB) 61 | } 62 | 63 | pub fn default_build_lib_package_path(&self, name: &str) -> PathBuf { 64 | self.default_build_lib_path().join(name) 65 | } 66 | 67 | pub fn default_build_lib_package_source_path(&self, name: &str, origin: Origin) -> PathBuf { 68 | match origin { 69 | Origin::Src => self.default_build_lib_package_src_path(name), 70 | Origin::Test => self.default_build_lib_package_test_path(name), 71 | } 72 | } 73 | 74 | pub fn default_build_lib_package_src_path(&self, name: &str) -> PathBuf { 75 | self.default_build_lib_package_path(name) 76 | .join(DIR_NAME_PACKAGE_SRC) 77 | } 78 | 79 | pub fn default_build_lib_package_test_path(&self, name: &str) -> PathBuf { 80 | self.default_build_lib_package_path(name) 81 | .join(DIR_NAME_PACKAGE_TEST) 82 | } 83 | 84 | pub fn default_build_lib_package_ebin_path(&self, name: &str) -> PathBuf { 85 | self.default_build_lib_package_path(name) 86 | .join(DIR_NAME_PACKAGE_EBIN) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Gleam 2 | 3 | Thanks for contributing to Gleam! 4 | 5 | Before continuing please read our [code of conduct][code-of-conduct] which all 6 | contributors are expected to adhere to. 7 | 8 | [code-of-conduct]: https://github.com/gleam-lang/gleam/blob/main/CODE_OF_CONDUCT.md 9 | 10 | 11 | ## Contributing bug reports 12 | 13 | If you have found a bug in Gleam please check to see if there is an open 14 | ticket for this problem on [our GitHub issue tracker][issues]. If you cannot 15 | find an existing ticket for the bug please open a new one. 16 | 17 | [issues]: https://github.com/gleam-lang/gleam/issues 18 | 19 | A bug may be a technical problem such as a compiler crash or an incorrect 20 | return value from a library function, or a user experience issue such as 21 | unclear or absent documentation. If you are unsure if your problem is a bug 22 | please open a ticket and we will work it out together. 23 | 24 | 25 | ## Contributing code changes 26 | 27 | Code changes to Gleam are welcomed via the process below. 28 | 29 | 1. Find or open a GitHub issue relevant to the change you wish to make and 30 | comment saying that you wish to work on this issue. If the change 31 | introduces new functionality or behaviour this would be a good time to 32 | discuss the details of the change to ensure we are in agreement as to how 33 | the new functionality should work. 34 | 2. Open a GitHub pull request with your changes and ensure the tests and build 35 | pass on CI. 36 | 3. A Gleam team member will review the changes and may provide feedback to 37 | work on. Depending on the change there may be multiple rounds of feedback. 38 | 4. Once the changes have been approved the code will be rebased into the 39 | `main` branch. 40 | 41 | ## Local development 42 | 43 | To run the compiler tests. 44 | 45 | ```shell 46 | cargo test 47 | ``` 48 | 49 | If you don't have Rust or Cargo installed you can run the above command in a docker sandbox. 50 | Run the command below from this directory. 51 | 52 | ```shell 53 | docker run -v $(pwd):/opt/app -it -w /opt/app rust:1.44.0 bash 54 | ``` 55 | 56 | ## Rust development 57 | 58 | Here are some tips and guidelines for writing Rust code in the Gleam compiler: 59 | 60 | Never write code that can cause the compiler to panic (`panic!`, `unwrap`, 61 | `expect`) as a compiler panic is confusing to the user. When possible rewrite 62 | the code in a way that makes the error impossible. If that cannot be done and 63 | the error is either common or due to a mistake by the user return an error 64 | value that will be printed with an appropriate helpful error message. If the 65 | error _should_ never happen and its occurrence indicates a fatal compiler bug 66 | the `.gleam_expect` method of the `GleamExpect` trait can be used. This is 67 | similar to `.expect` but prints a more helpful error message to the user. 68 | 69 | The `GLEAM_LOG` environment variable can be used to cause the compiler to 70 | print more information for debugging and introspection. i.e. 71 | `GLEAM_LOG=trace`. 72 | -------------------------------------------------------------------------------- /templates/highlightjs-gleam.js: -------------------------------------------------------------------------------- 1 | hljs.registerLanguage("gleam", function (hljs) { 2 | const KEYWORDS = 3 | "as assert case const external fn if import let " + 4 | "opaque pub todo try tuple type"; 5 | const STRING = { 6 | className: "string", 7 | variants: [{ begin: /"/, end: /"/ }], 8 | contains: [hljs.BACKSLASH_ESCAPE], 9 | relevance: 0, 10 | }; 11 | const NAME = { 12 | className: "variable", 13 | begin: "\\b[a-z][a-z0-9_]*\\b", 14 | relevance: 0, 15 | }; 16 | const DISCARD_NAME = { 17 | className: "comment", 18 | begin: "\\b_[a-z][a-z0-9_]*\\b", 19 | relevance: 0, 20 | }; 21 | const NUMBER = { 22 | className: "number", 23 | variants: [ 24 | { 25 | begin: "\\b0b([01_]+)", 26 | }, 27 | { 28 | begin: "\\b0o([0-7_]+)", 29 | }, 30 | { 31 | begin: "\\b0x([A-Fa-f0-9_]+)", 32 | }, 33 | { 34 | begin: "\\b(\\d[\\d_]*(\\.[0-9_]+)?([eE][+-]?[0-9_]+)?)", 35 | }, 36 | ], 37 | relevance: 0, 38 | }; 39 | 40 | return { 41 | name: "Gleam", 42 | aliases: ["gleam"], 43 | contains: [ 44 | hljs.C_LINE_COMMENT_MODE, 45 | STRING, 46 | { 47 | // bitstrings 48 | begin: "<<", 49 | end: ">>", 50 | contains: [ 51 | { 52 | className: "keyword", 53 | beginKeywords: 54 | "binary bytes int float bit_string bits utf8 utf16 utf32 " + 55 | "utf8_codepoint utf16_codepoint utf32_codepoint signed unsigned " + 56 | "big little native unit size", 57 | }, 58 | STRING, 59 | NUMBER, 60 | NAME, 61 | DISCARD_NAME, 62 | ], 63 | relevance: 10, 64 | }, 65 | { 66 | className: "function", 67 | beginKeywords: "fn", 68 | end: "\\(", 69 | excludeEnd: true, 70 | contains: [ 71 | { 72 | className: "title", 73 | begin: "[a-zA-Z0-9_]\\w*", 74 | relevance: 0, 75 | }, 76 | ], 77 | }, 78 | { 79 | className: "keyword", 80 | beginKeywords: KEYWORDS, 81 | }, 82 | { 83 | // Type names and constructors 84 | className: "title", 85 | begin: "\\b[A-Z][A-Za-z0-9_]*\\b", 86 | relevance: 0, 87 | }, 88 | { 89 | // float operators 90 | className: "operator", 91 | begin: "(\\+\\.|-\\.|\\*\\.|/\\.|<\\.|>\\.)", 92 | relevance: 10, 93 | }, 94 | { 95 | className: "operator", 96 | begin: "(->|\\|>|<<|>>|\\+|-|\\*|/|>=|<=|<|<|%|\\.\\.|\\|=|==|!=)", 97 | relevance: 0, 98 | }, 99 | NUMBER, 100 | NAME, 101 | DISCARD_NAME, 102 | ], 103 | }; 104 | }); 105 | document.querySelectorAll("pre code").forEach((block) => { 106 | if (block.className === "") { 107 | block.classList.add("gleam"); 108 | } 109 | hljs.highlightBlock(block); 110 | }); 111 | -------------------------------------------------------------------------------- /src/docs/tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::{config::PackageConfig, fs::OutputFile, project::Input}; 3 | 4 | #[test] 5 | fn module_docs_test() { 6 | let src = r#" 7 | //// module comment 8 | 9 | /// A constant value 10 | pub const hello = "test" 11 | 12 | /// doc comment 13 | // regular comment 14 | pub fn public_fun(x: Int) -> Int { 15 | x 16 | } 17 | 18 | pub fn implicit_return() { 19 | "testing" 20 | } 21 | 22 | fn private_fun() { 23 | 1 24 | } 25 | 26 | pub fn complicated_fun( 27 | over thing: a, 28 | from initial: b, 29 | with fun: fn(a, b) -> b, 30 | ) -> b { 31 | fun(thing, initial) 32 | } 33 | "#; 34 | 35 | let input = Input { 36 | origin: ModuleOrigin::Src, 37 | path: PathBuf::from("/src/test.gleam"), 38 | source_base_path: PathBuf::from("/src"), 39 | src: src.to_string(), 40 | }; 41 | 42 | let config = PackageConfig { 43 | name: "test".to_string(), 44 | docs: Default::default(), 45 | tool: Default::default(), 46 | version: Default::default(), 47 | repository: Default::default(), 48 | description: Default::default(), 49 | dependencies: Default::default(), 50 | otp_start_module: None, 51 | }; 52 | 53 | let mut analysed = crate::project::analysed(vec![input]).expect("Compilation failed"); 54 | analysed 55 | .iter_mut() 56 | .for_each(|a| a.attach_doc_and_module_comments()); 57 | 58 | let output_files = generate_html( 59 | PathBuf::from("."), 60 | &config, 61 | analysed.as_slice(), 62 | &[], 63 | &PathBuf::from("/docs"), 64 | ); 65 | let module_page = output_files 66 | .iter() 67 | .find(|page| page.path == PathBuf::from("/docs/test/index.html")) 68 | .expect("Missing docs page"); 69 | 70 | // Comments 71 | module_page.should_contain("module comment"); 72 | module_page.should_contain("doc comment"); 73 | module_page.should_not_contain("regular comment"); 74 | 75 | // Constants 76 | module_page.should_contain("pub const hello: String = "test""); 77 | module_page.should_contain("A constant value"); 78 | 79 | // Functions 80 | module_page.should_contain("pub fn public_fun(x: Int) -> Int"); 81 | module_page.should_contain("pub fn implicit_return() -> String"); 82 | module_page.should_not_contain("private_fun()"); 83 | 84 | module_page.should_contain( 85 | "pub fn complicated_fun( 86 | over thing: a, 87 | from initial: b, 88 | with fun: fn(a, b) -> b, 89 | ) -> b", 90 | ); 91 | } 92 | 93 | impl OutputFile { 94 | fn should_contain(&self, text: &str) { 95 | assert!( 96 | self.text.contains(&text.to_string()), 97 | "Generated docs page did not contain: `{}`", 98 | text 99 | ); 100 | } 101 | 102 | fn should_not_contain(&self, text: &str) { 103 | assert!( 104 | !self.text.contains(&text.to_string()), 105 | "Generated docs page was not supposed to contain: `{}`", 106 | text 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Error, FileIOAction, FileKind}; 2 | use serde::Deserialize; 3 | use std::collections::HashMap; 4 | use std::path::{Path, PathBuf}; 5 | 6 | fn default_version() -> String { 7 | "1.0.0".to_string() 8 | } 9 | 10 | #[derive(Deserialize, Debug, PartialEq, Default)] 11 | pub struct PackageConfig { 12 | pub name: String, 13 | #[serde(default = "default_version")] 14 | pub version: String, 15 | #[serde(default)] 16 | pub description: String, 17 | #[serde(default)] 18 | pub tool: BuildTool, 19 | #[serde(default)] 20 | pub docs: Docs, 21 | #[serde(default)] 22 | pub dependencies: HashMap, 23 | #[serde(default)] 24 | pub otp_start_module: Option, 25 | #[serde(default)] 26 | pub repository: Repository, 27 | } 28 | 29 | #[derive(Deserialize, Debug, PartialEq, Clone, Copy)] 30 | #[serde(rename_all = "kebab-case")] 31 | pub enum BuildTool { 32 | Gleam, 33 | Other, 34 | } 35 | 36 | impl Default for BuildTool { 37 | fn default() -> Self { 38 | Self::Other 39 | } 40 | } 41 | 42 | #[derive(Deserialize, Debug, PartialEq)] 43 | #[serde(tag = "type", rename_all = "lowercase")] 44 | pub enum Repository { 45 | GitHub { user: String, repo: String }, 46 | GitLab { user: String, repo: String }, 47 | BitBucket { user: String, repo: String }, 48 | Custom { url: String }, 49 | None, 50 | } 51 | 52 | impl Repository { 53 | pub fn url(&self) -> Option { 54 | match self { 55 | Repository::GitHub { repo, user } => { 56 | Some(format!("https://github.com/{}/{}", user, repo)) 57 | } 58 | Repository::GitLab { repo, user } => { 59 | Some(format!("https://gitlab.com/{}/{}", user, repo)) 60 | } 61 | Repository::BitBucket { repo, user } => { 62 | Some(format!("https://bitbucket.com/{}/{}", user, repo)) 63 | } 64 | Repository::Custom { url } => Some(url.clone()), 65 | Repository::None => None, 66 | } 67 | } 68 | } 69 | 70 | impl Default for Repository { 71 | fn default() -> Self { 72 | Self::None 73 | } 74 | } 75 | 76 | #[derive(Deserialize, Default, Debug, PartialEq)] 77 | pub struct Docs { 78 | #[serde(default)] 79 | pub pages: Vec, 80 | #[serde(default)] 81 | pub links: Vec, 82 | } 83 | 84 | #[derive(Deserialize, Debug, PartialEq, Clone)] 85 | pub struct DocsPage { 86 | pub title: String, 87 | pub path: String, 88 | pub source: PathBuf, 89 | } 90 | 91 | #[derive(Deserialize, Debug, PartialEq, Clone)] 92 | pub struct DocsLink { 93 | pub title: String, 94 | pub href: String, 95 | } 96 | 97 | pub fn read_project_config(root: impl AsRef) -> Result { 98 | let config_path = root.as_ref().join("gleam.toml"); 99 | let toml = crate::fs::read(&config_path)?; 100 | toml::from_str(&toml).map_err(|e| Error::FileIO { 101 | action: FileIOAction::Parse, 102 | kind: FileKind::File, 103 | path: config_path.clone(), 104 | err: Some(e.to_string()), 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at louis at lpil.uk. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /src/eunit.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | build::{self, project_root::ProjectRoot, Origin}, 3 | error::Error, 4 | fs::OutputFile, 5 | }; 6 | use itertools::Itertools; 7 | use std::{path::PathBuf, process::Command}; 8 | 9 | #[derive(Debug)] 10 | struct EunitFile { 11 | should_be_compiled: bool, 12 | path: PathBuf, 13 | content: String, 14 | } 15 | 16 | pub fn command(root_string: String) -> Result<(), Error> { 17 | let root_path = PathBuf::from(root_string); 18 | let root = ProjectRoot::new(root_path.clone()); 19 | let config = root.root_config()?; 20 | 21 | // Build project 22 | let packages = build::main(config, root_path)?; 23 | 24 | crate::cli::print_running("eunit"); 25 | 26 | // Build a list of test modules 27 | let test_modules = packages 28 | .into_iter() 29 | .flat_map(|(_, p)| p.modules.into_iter()) 30 | .filter(|m| m.origin == Origin::Test) 31 | .map(|m| m.name.replace("/", "@")) 32 | .join(","); 33 | 34 | // Prepare eunit runner and its dependencies. 35 | let eunit_files = vec![ 36 | EunitFile { 37 | should_be_compiled: true, 38 | path: root.build_path().join("eunit_progress.erl"), 39 | content: std::include_str!("eunit/eunit_progress.erl").to_string(), 40 | }, 41 | EunitFile { 42 | should_be_compiled: false, 43 | path: root.build_path().join("eunit_runner.erl"), 44 | content: std::include_str!("eunit/eunit_runner.erl").to_string(), 45 | }, 46 | ]; 47 | 48 | eunit_files.iter().try_for_each(|file| { 49 | crate::fs::write_output(&OutputFile { 50 | path: file.path.clone(), 51 | text: file.content.to_owned(), 52 | }) 53 | })?; 54 | 55 | // compile eunit runner dependencies in the build path 56 | let mut compile_command = Command::new("erlc"); 57 | let _ = compile_command.arg("-o"); 58 | let _ = compile_command.arg(root.build_path()); 59 | 60 | eunit_files 61 | .iter() 62 | .filter(|&file| file.should_be_compiled) 63 | .for_each(|file| { 64 | let _ = compile_command.arg(file.path.clone()); 65 | }); 66 | 67 | tracing::trace!("Running OS process {:?}", compile_command); 68 | let _ = compile_command.status().map_err(|e| Error::ShellCommand { 69 | command: "erlc".to_string(), 70 | err: Some(e.kind()), 71 | })?; 72 | 73 | // Prepare the escript command for running tests 74 | let mut command = Command::new("escript"); 75 | let _ = command.arg(root.build_path().join("eunit_runner.erl")); 76 | 77 | let ebin_paths: String = crate::fs::read_dir(root.default_build_lib_path())? 78 | .filter_map(Result::ok) 79 | .map(|entry| entry.path().join("ebin").as_path().display().to_string()) 80 | .join(","); 81 | 82 | // we supply two parameters to the escript. First is a comma seperated 83 | let _ = command.arg(ebin_paths); 84 | let _ = command.arg(test_modules); 85 | 86 | // Run the shell 87 | tracing::trace!("Running OS process {:?}", command); 88 | let status = command.status().map_err(|e| Error::ShellCommand { 89 | command: "escript".to_string(), 90 | err: Some(e.kind()), 91 | })?; 92 | 93 | if status.success() { 94 | Ok(()) 95 | } else { 96 | Err(Error::ShellCommand { 97 | command: "escript".to_string(), 98 | err: None, 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/format/command.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::{Error, FileIOAction, FileKind, Result, StandardIOAction}, 3 | fs::OutputFile, 4 | }; 5 | use std::{io::Read, path::PathBuf, str::FromStr}; 6 | 7 | #[derive(Debug, PartialEq)] 8 | pub struct Unformatted { 9 | pub source: PathBuf, 10 | pub destination: PathBuf, 11 | pub input: String, 12 | pub output: String, 13 | } 14 | 15 | pub fn run(stdin: bool, check: bool, files: Vec) -> Result<()> { 16 | if stdin { 17 | process_stdin(check) 18 | } else { 19 | process_files(check, files) 20 | } 21 | } 22 | 23 | fn process_stdin(check: bool) -> Result<()> { 24 | let src = read_stdin()?; 25 | let mut out = String::new(); 26 | crate::format::pretty(&mut out, src.as_str())?; 27 | 28 | if !check { 29 | print!("{}", out); 30 | return Ok(()); 31 | } 32 | 33 | if src != out { 34 | return Err(Error::Format { 35 | problem_files: vec![Unformatted { 36 | source: PathBuf::from(""), 37 | destination: PathBuf::from(""), 38 | input: src, 39 | output: out, 40 | }], 41 | }); 42 | } 43 | 44 | Ok(()) 45 | } 46 | 47 | fn process_files(check: bool, files: Vec) -> Result<()> { 48 | if check { 49 | check_files(files) 50 | } else { 51 | format_files(files) 52 | } 53 | } 54 | 55 | fn check_files(files: Vec) -> Result<()> { 56 | let problem_files = unformatted_files(files)?; 57 | 58 | if problem_files.is_empty() { 59 | Ok(()) 60 | } else { 61 | Err(Error::Format { problem_files }) 62 | } 63 | } 64 | 65 | fn format_files(files: Vec) -> Result<()> { 66 | for file in unformatted_files(files)?.into_iter() { 67 | crate::fs::write_output(&OutputFile { 68 | path: file.destination, 69 | text: file.output, 70 | })?; 71 | } 72 | Ok(()) 73 | } 74 | 75 | pub fn unformatted_files(files: Vec) -> Result> { 76 | let mut problem_files = Vec::with_capacity(files.len()); 77 | 78 | for file_path in files { 79 | let path = PathBuf::from_str(&file_path).map_err(|e| Error::FileIO { 80 | action: FileIOAction::Open, 81 | kind: FileKind::File, 82 | path: PathBuf::from(file_path), 83 | err: Some(e.to_string()), 84 | })?; 85 | 86 | if path.is_dir() { 87 | for path in crate::fs::gleam_files_excluding_gitignore(&path).into_iter() { 88 | format_file(&mut problem_files, path)?; 89 | } 90 | } else { 91 | format_file(&mut problem_files, path)?; 92 | } 93 | } 94 | 95 | Ok(problem_files) 96 | } 97 | 98 | fn format_file(problem_files: &mut Vec, path: PathBuf) -> Result<()> { 99 | let src = crate::fs::read(&path)?; 100 | let mut output = String::new(); 101 | crate::format::pretty(&mut output, src.as_str())?; 102 | 103 | if src != output { 104 | problem_files.push(Unformatted { 105 | source: path.clone(), 106 | destination: path, 107 | input: src, 108 | output, 109 | }); 110 | } 111 | Ok(()) 112 | } 113 | 114 | pub fn read_stdin() -> Result { 115 | let mut src = String::new(); 116 | let _ = std::io::stdin() 117 | .read_to_string(&mut src) 118 | .map_err(|e| Error::StandardIO { 119 | action: StandardIOAction::Read, 120 | err: Some(e.kind()), 121 | })?; 122 | Ok(src) 123 | } 124 | -------------------------------------------------------------------------------- /schema.capnp: -------------------------------------------------------------------------------- 1 | @0xb533a99cfdbcedbe; 2 | 3 | # This Cap'n Proto schema is compiled into Rust code for use in the compiler. 4 | # 5 | # We don't want the compiler build to depend on the Cap'n Proto compiler so 6 | # the Cap'n Proto to Rust build step is commented out in `build.rs`. 7 | # 8 | # This schema is not considered a stable API and may change at any time. 9 | 10 | struct Property(Value) { 11 | key @0 :Text; 12 | value @1 :Value; 13 | } 14 | 15 | struct Option(Value) { 16 | union { 17 | none @0 :Void; 18 | some @1 :Value; 19 | } 20 | } 21 | 22 | struct Module { 23 | name @0 :List(Text); 24 | types @1 :List(Property(TypeConstructor)); 25 | values @2 :List(Property(ValueConstructor)); 26 | accessors @3 :List(Property(AccessorsMap)); 27 | } 28 | 29 | struct TypeConstructor { 30 | type @0 :Type; 31 | # TODO: convert this to an int as we only need to reconstruct type vars, 32 | # not other types 33 | # TODO: test 34 | parameters @1 :List(Type); 35 | module @2 :List(Text); 36 | } 37 | 38 | struct AccessorsMap { 39 | type @0 :Type; 40 | accessors @1 :List(Property(RecordAccessor)); 41 | } 42 | 43 | struct RecordAccessor { 44 | type @0 :Type; 45 | index @1 :UInt16; 46 | } 47 | 48 | struct Type { 49 | union { 50 | app :group { 51 | name @0 :Text; 52 | module @1 :List(Text); 53 | parameters @2 :List(Type); 54 | } 55 | 56 | fn :group { 57 | arguments @3 :List(Type); 58 | return @4 :Type; 59 | } 60 | 61 | var :group { 62 | id @5 :UInt16; 63 | } 64 | 65 | tuple :group { 66 | elements @6 :List(Type); 67 | } 68 | } 69 | } 70 | 71 | struct ValueConstructor { 72 | type @0 :Type; 73 | variant @1 :ValueConstructorVariant; 74 | } 75 | 76 | struct ValueConstructorVariant { 77 | union { 78 | moduleConstant @0 :Constant; 79 | 80 | moduleFn :group { 81 | name @1 :Text; 82 | fieldMap @2 :Option(FieldMap); 83 | module @3 :List(Text); 84 | arity @4 :UInt16; 85 | } 86 | 87 | record :group { 88 | name @5 :Text; 89 | arity @6 :UInt16; 90 | fieldMap @7 :Option(FieldMap); 91 | } 92 | } 93 | } 94 | 95 | # Cap'n Proto only permits pointer types to be used as type parameters 96 | struct BoxedUInt16 { 97 | value @0 :UInt16; 98 | } 99 | 100 | struct FieldMap { 101 | arity @0 :UInt32; 102 | fields @1 :List(Property(BoxedUInt16)); 103 | } 104 | 105 | struct Constant { 106 | int @0 :Text; 107 | float @1 :Text; 108 | string @2 :Text; 109 | tuple @3 :List(Constant); 110 | 111 | list :group { 112 | elements @4 :List(Constant); 113 | type @5 :Type; 114 | } 115 | 116 | record :group { 117 | args @6 :List(Constant); 118 | tag @7 :Text; 119 | typ @8 :Type; 120 | } 121 | 122 | bitString @9 :List(BitStringSegment); 123 | } 124 | 125 | struct BitStringSegment { 126 | value @0 :Constant; 127 | options @1 :List(BitStringSegmentOption); 128 | type @2 :Type; 129 | } 130 | 131 | struct BitStringSegmentOption { 132 | union { 133 | binary @0 :Void; 134 | 135 | integer @1 :Void; 136 | 137 | float @2 :Void; 138 | 139 | bitstring @3 :Void; 140 | 141 | utf8 @4 :Void; 142 | 143 | utf16 @5 :Void; 144 | 145 | utf32 @6 :Void; 146 | 147 | utf8Codepoint @7 :Void; 148 | 149 | utf16Codepoint @8 :Void; 150 | 151 | utf32Codepoint @9 :Void; 152 | 153 | signed @10 :Void; 154 | 155 | unsigned @11 :Void; 156 | 157 | big @12 :Void; 158 | 159 | little @13 :Void; 160 | 161 | native @14 :Void; 162 | 163 | size :group { 164 | value @15 :Constant; 165 | shortForm @16 :Bool; 166 | } 167 | 168 | unit :group { 169 | value @17 :Constant; 170 | shortForm @18 :Bool; 171 | } 172 | } 173 | } 174 | 175 | -------------------------------------------------------------------------------- /src/docs/command.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | cli, 3 | error::{Error, GleamExpect}, 4 | project, 5 | }; 6 | use bytes::Bytes; 7 | use hexpm::Client; 8 | use std::path::PathBuf; 9 | 10 | static TOKEN_NAME: &str = concat!(env!("CARGO_PKG_NAME"), " (", env!("CARGO_PKG_VERSION"), ")"); 11 | static DOCS_DIR_NAME: &str = "docs"; 12 | 13 | pub fn remove(package: String, version: String) -> Result<(), Error> { 14 | // Start event loop so we can run async functions to call the Hex API 15 | let runtime = 16 | tokio::runtime::Runtime::new().gleam_expect("Unable to start Tokio async runtime"); 17 | 18 | // Get login creds from user 19 | let username = cli::ask("https://hex.pm username")?; 20 | let password = cli::ask_password("https://hex.pm password")?; 21 | 22 | // Remove docs from API 23 | runtime.block_on(async { 24 | hexpm::UnauthenticatedClient::new() 25 | .authenticate(username.as_str(), password.as_str(), TOKEN_NAME) 26 | .await 27 | .map_err(|e| Error::Hex(e.to_string()))? 28 | .remove_docs(package.as_str(), version.as_str()) 29 | .await 30 | .map_err(|e| Error::Hex(e.to_string())) 31 | })?; 32 | 33 | // Done! 34 | println!( 35 | "The docs for {} {} have been removed from HexDocs", 36 | package, version 37 | ); 38 | Ok(()) 39 | } 40 | 41 | pub fn build(project_root: String, version: String, to: Option) -> Result<(), Error> { 42 | let project_root = PathBuf::from(&project_root).canonicalize().map_err(|_| { 43 | Error::UnableToFindProjectRoot { 44 | path: project_root.clone(), 45 | } 46 | })?; 47 | 48 | let output_dir = to.map(PathBuf::from).unwrap_or_else(|| { 49 | project_root 50 | .join(project::OUTPUT_DIR_NAME) 51 | .join(DOCS_DIR_NAME) 52 | }); 53 | 54 | // Build 55 | let (config, outputs) = super::build_project(&project_root, version, &output_dir)?; 56 | 57 | // Write 58 | crate::fs::delete_dir(&output_dir)?; 59 | crate::fs::write_outputs(outputs.as_slice())?; 60 | 61 | println!( 62 | "\nThe docs for {package} have been rendered to {output_dir}", 63 | package = config.name, 64 | output_dir = output_dir.to_string_lossy() 65 | ); 66 | // We're done! 67 | Ok(()) 68 | } 69 | 70 | pub fn publish(project_root: String, version: String) -> Result<(), Error> { 71 | let project_root = PathBuf::from(&project_root).canonicalize().map_err(|_| { 72 | Error::UnableToFindProjectRoot { 73 | path: project_root.clone(), 74 | } 75 | })?; 76 | 77 | let output_dir = PathBuf::new(); 78 | 79 | // Build 80 | let (config, outputs) = super::build_project(&project_root, version.clone(), &output_dir)?; 81 | 82 | // Create gzipped tarball of docs 83 | let archive = crate::fs::create_tar_archive(outputs)?; 84 | 85 | // Start event loop so we can run async functions to call the Hex API 86 | let runtime = 87 | tokio::runtime::Runtime::new().gleam_expect("Unable to start Tokio async runtime"); 88 | 89 | // Get login creds from user 90 | let username = cli::ask("https://hex.pm username")?; 91 | let password = cli::ask_password("https://hex.pm password")?; 92 | 93 | // Upload to hex 94 | runtime.block_on(async { 95 | hexpm::UnauthenticatedClient::new() 96 | .authenticate(username.as_str(), password.as_str(), TOKEN_NAME) 97 | .await 98 | .map_err(|e| Error::Hex(e.to_string()))? 99 | .publish_docs(config.name.as_str(), version.as_str(), Bytes::from(archive)) 100 | .await 101 | .map_err(|e| Error::Hex(e.to_string())) 102 | })?; 103 | 104 | println!( 105 | " 106 | The docs for {package} have been published to HexDocs: 107 | 108 | https://hexdocs.pm/{package}", 109 | package = config.name 110 | ); 111 | 112 | // We're done! 113 | Ok(()) 114 | } 115 | -------------------------------------------------------------------------------- /src/codegen.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | build::Module, config::PackageConfig, erl, fs::FileSystemWriter, line_numbers::LineNumbers, 3 | Result, 4 | }; 5 | use itertools::Itertools; 6 | use std::{fmt::Debug, path::Path}; 7 | 8 | /// A code generator that creates a .erl Erlang module and record header files 9 | /// for each Gleam module in the package. 10 | #[derive(Debug)] 11 | pub struct Erlang<'a> { 12 | output_directory: &'a Path, 13 | } 14 | 15 | impl<'a> Erlang<'a> { 16 | pub fn new(output_directory: &'a Path) -> Self { 17 | Self { output_directory } 18 | } 19 | 20 | pub fn render(&self, writer: &impl FileSystemWriter, modules: &[Module]) -> Result<()> { 21 | for module in modules { 22 | let erl_name = module.name.replace("/", "@"); 23 | self.erlang_module(writer, module, erl_name.as_str())?; 24 | self.erlang_record_headers(writer, module, erl_name.as_str())?; 25 | } 26 | Ok(()) 27 | } 28 | 29 | fn erlang_module( 30 | &self, 31 | writer: &impl FileSystemWriter, 32 | module: &Module, 33 | erl_name: &str, 34 | ) -> Result<()> { 35 | let name = format!("{}.erl", erl_name); 36 | let path = self.output_directory.join(&name); 37 | let mut file = writer.open(path.as_path())?; 38 | let line_numbers = LineNumbers::new(module.code.as_str()); 39 | let res = erl::module(&module.ast, &line_numbers, &mut file); 40 | tracing::trace!(name = ?name, "Generated Erlang module"); 41 | res 42 | } 43 | 44 | fn erlang_record_headers( 45 | &self, 46 | writer: &dyn FileSystemWriter, 47 | module: &Module, 48 | erl_name: &str, 49 | ) -> Result<()> { 50 | for (name, text) in erl::records(&module.ast).into_iter() { 51 | let name = format!("{}_{}.hrl", erl_name, name); 52 | tracing::trace!(name = ?name, "Generated Erlang header"); 53 | writer 54 | .open(self.output_directory.join(name).as_path())? 55 | .write(text.as_bytes())?; 56 | } 57 | Ok(()) 58 | } 59 | } 60 | 61 | /// A code generator that creates a .app Erlang application file for the package 62 | #[derive(Debug)] 63 | pub struct ErlangApp<'a> { 64 | output_directory: &'a Path, 65 | } 66 | 67 | impl<'a> ErlangApp<'a> { 68 | pub fn new(output_directory: &'a Path) -> Self { 69 | Self { output_directory } 70 | } 71 | 72 | pub fn render( 73 | &self, 74 | writer: &impl FileSystemWriter, 75 | config: &PackageConfig, 76 | modules: &[Module], 77 | ) -> Result<()> { 78 | fn tuple(key: &str, value: &str) -> String { 79 | format!(" {{{}, {}}},\n", key, value) 80 | } 81 | 82 | let path = self.output_directory.join(format!("{}.app", &config.name)); 83 | 84 | let start_module = match &config.otp_start_module { 85 | None => "".to_string(), 86 | Some(module) => tuple("mod", format!("'{}'", module).as_str()), 87 | }; 88 | 89 | let modules = modules 90 | .iter() 91 | .map(|m| m.name.replace("/", "@")) 92 | .sorted() 93 | .join(",\n "); 94 | 95 | let mut applications: Vec<_> = config.dependencies.iter().map(|m| m.0).collect(); 96 | applications.sort(); 97 | let applications = applications.into_iter().join(",\n "); 98 | 99 | let text = format!( 100 | r#"{{application, {package}, [ 101 | {start_module} {{vsn, "{version}"}}, 102 | {{applications, [{applications}]}}, 103 | {{description, "{description}"}}, 104 | {{modules, [{modules}]}}, 105 | {{registered, []}}, 106 | ]}}. 107 | "#, 108 | applications = applications, 109 | description = config.description, 110 | modules = modules, 111 | package = config.name, 112 | start_module = start_module, 113 | version = config.version, 114 | ); 115 | 116 | writer.open(&path)?.write(text.as_bytes()) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/metadata/tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::{ 3 | fs::test::InMemoryFile, 4 | typ::{self, TypeVar}, 5 | }; 6 | use std::{io::BufReader, iter::FromIterator}; 7 | 8 | // TODO: test type links 9 | 10 | fn roundtrip(input: &Module) -> Module { 11 | let mut buffer = InMemoryFile::new(); 12 | ModuleEncoder::new(input).write(buffer.clone()).unwrap(); 13 | let buffer = buffer.into_contents().unwrap(); 14 | ModuleDecoder::new() 15 | .read(BufReader::new(buffer.as_slice())) 16 | .unwrap() 17 | } 18 | 19 | #[test] 20 | fn empty_module() { 21 | let module = Module { 22 | name: vec!["one".to_string(), "two".to_string()], 23 | types: HashMap::new(), 24 | values: HashMap::new(), 25 | accessors: HashMap::new(), 26 | }; 27 | assert_eq!(roundtrip(&module), module); 28 | } 29 | 30 | #[test] 31 | fn module_with_app_type() { 32 | let module = Module { 33 | name: vec!["a".to_string(), "b".to_string()], 34 | types: [( 35 | "ListIntType".to_string(), 36 | TypeConstructor { 37 | typ: typ::list(typ::int()), 38 | public: true, 39 | origin: Default::default(), 40 | module: vec!["the".to_string(), "module".to_string()], 41 | parameters: vec![], 42 | }, 43 | )] 44 | .iter() 45 | .cloned() 46 | .collect(), 47 | values: HashMap::new(), 48 | accessors: HashMap::new(), 49 | }; 50 | assert_eq!(roundtrip(&module), module); 51 | } 52 | 53 | #[test] 54 | fn module_with_fn_type() { 55 | let module = Module { 56 | name: vec!["a".to_string(), "b".to_string()], 57 | types: [( 58 | "FnType".to_string(), 59 | TypeConstructor { 60 | typ: typ::fn_(vec![typ::nil(), typ::float()], typ::int()), 61 | public: true, 62 | origin: Default::default(), 63 | module: vec!["the".to_string(), "module".to_string()], 64 | parameters: vec![], 65 | }, 66 | )] 67 | .iter() 68 | .cloned() 69 | .collect(), 70 | values: HashMap::new(), 71 | accessors: HashMap::new(), 72 | }; 73 | assert_eq!(roundtrip(&module), module); 74 | } 75 | 76 | #[test] 77 | fn module_with_tuple_type() { 78 | let module = Module { 79 | name: vec!["a".to_string(), "b".to_string()], 80 | types: [( 81 | "TupleType".to_string(), 82 | TypeConstructor { 83 | typ: typ::tuple(vec![typ::nil(), typ::float(), typ::int()]), 84 | public: true, 85 | origin: Default::default(), 86 | module: vec!["the".to_string(), "module".to_string()], 87 | parameters: vec![], 88 | }, 89 | )] 90 | .iter() 91 | .cloned() 92 | .collect(), 93 | values: HashMap::new(), 94 | accessors: HashMap::new(), 95 | }; 96 | assert_eq!(roundtrip(&module), module); 97 | } 98 | 99 | #[test] 100 | fn module_with_generic_type() { 101 | let t0 = typ::generic_var(0); 102 | let t1 = typ::generic_var(1); 103 | let t7 = typ::generic_var(7); 104 | let t8 = typ::generic_var(8); 105 | 106 | fn make(t1: Arc, t2: Arc) -> Module { 107 | Module { 108 | name: vec!["a".to_string(), "b".to_string()], 109 | types: [( 110 | "TupleType".to_string(), 111 | TypeConstructor { 112 | typ: typ::tuple(vec![t1.clone(), t1.clone(), t2.clone()]), 113 | public: true, 114 | origin: Default::default(), 115 | module: vec!["the".to_string(), "module".to_string()], 116 | parameters: vec![t1, t2], 117 | }, 118 | )] 119 | .iter() 120 | .cloned() 121 | .collect(), 122 | values: HashMap::new(), 123 | accessors: HashMap::new(), 124 | } 125 | } 126 | 127 | assert_eq!(roundtrip(&make(t7, t8)), make(t0, t1)); 128 | } 129 | -------------------------------------------------------------------------------- /src/build.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings)] 2 | 3 | // TODO: Avoid rebuilding clean modules 4 | // TODO: Download deps from Hex 5 | // TODO: Support compilation of rebar3 packages 6 | // TODO: Track removed files in src and test so they can be removed from _build 7 | // TODO: Test profile and default profile 8 | // TODO: Only compile test code in test profile 9 | // TODO: Full .app generation 10 | // TODO: Validate config.otp_start_module does not contain ' 11 | // TODO: Validate config.otp_start_module has a start function 12 | // - custom output paths 13 | // - no .app generation 14 | // - no Erlang generation 15 | 16 | pub mod compile_package; 17 | mod dep_tree; 18 | pub mod package_compiler; 19 | mod project_compiler; 20 | pub mod project_root; 21 | 22 | #[cfg(test)] 23 | mod package_compilation_tests; 24 | 25 | pub use self::package_compiler::PackageCompiler; 26 | 27 | use crate::{ 28 | ast::TypedModule, 29 | build::{project_compiler::ProjectCompiler, project_root::ProjectRoot}, 30 | config::{self, PackageConfig}, 31 | erl, 32 | error::{Error, FileIOAction, FileKind, GleamExpect}, 33 | fs::OutputFile, 34 | typ, 35 | }; 36 | use itertools::Itertools; 37 | use std::{collections::HashMap, ffi::OsString, fs::DirEntry, path::PathBuf, process}; 38 | 39 | pub fn main(root_config: PackageConfig, path: PathBuf) -> Result, Error> { 40 | let root = ProjectRoot::new(path); 41 | 42 | tracing::info!("Copying root package to _build"); 43 | copy_root_package_to_build(&root, &root_config)?; 44 | 45 | tracing::info!("Reading package configs from _build"); 46 | let configs = root.package_configs(&root_config.name)?; 47 | 48 | tracing::info!("Compiling packages"); 49 | let packages = ProjectCompiler::new(&root, root_config, configs).compile()?; 50 | 51 | tracing::info!("Compiling Erlang source code to BEAM bytecode"); 52 | compile_erlang_to_beam(&root, &packages)?; 53 | 54 | Ok(packages) 55 | } 56 | 57 | #[derive(Debug)] 58 | pub struct Package { 59 | pub name: String, 60 | pub modules: Vec, 61 | } 62 | 63 | #[derive(Debug)] 64 | pub struct Module { 65 | pub name: String, 66 | pub code: String, 67 | pub path: PathBuf, 68 | pub origin: Origin, 69 | pub ast: TypedModule, 70 | } 71 | 72 | #[derive(Debug, Clone, Copy, PartialEq)] 73 | pub enum Origin { 74 | Src, 75 | Test, 76 | } 77 | 78 | fn compile_erlang_to_beam( 79 | root: &ProjectRoot, 80 | packages: &HashMap, 81 | ) -> Result<(), Error> { 82 | crate::cli::print_compiling("Erlang code"); 83 | 84 | let escript_path = root.build_path().join("compile_escript.erl"); 85 | let escript_source = std::include_str!("build/compile_escript.erl").to_string(); 86 | 87 | crate::fs::write_output(&OutputFile { 88 | path: escript_path.clone(), 89 | text: escript_source, 90 | })?; 91 | 92 | // Run escript to compile Erlang to beam files 93 | let mut command = process::Command::new("escript"); 94 | command.arg(escript_path); 95 | command.arg(root.build_path()); 96 | 97 | tracing::trace!("Running OS process {:?}", command); 98 | let status = command.status().map_err(|e| Error::ShellCommand { 99 | command: "escript".to_string(), 100 | err: Some(e.kind()), 101 | })?; 102 | 103 | if status.success() { 104 | Ok(()) 105 | } else { 106 | Err(Error::ShellCommand { 107 | command: "escript".to_string(), 108 | err: None, 109 | }) 110 | } 111 | } 112 | 113 | fn copy_root_package_to_build( 114 | root: &ProjectRoot, 115 | root_config: &PackageConfig, 116 | ) -> Result<(), Error> { 117 | let target = root.default_build_lib_package_path(&root_config.name); 118 | let path = &root.root; 119 | 120 | // Reset _build dir 121 | crate::fs::delete_dir(&target)?; 122 | crate::fs::mkdir(&target)?; 123 | 124 | // Copy source files across 125 | crate::fs::copy(path.join("gleam.toml"), target.join("gleam.toml"))?; 126 | crate::fs::copy_dir(path.join("src"), &target)?; 127 | crate::fs::copy_dir(path.join("test"), &target)?; 128 | 129 | Ok(()) 130 | } 131 | -------------------------------------------------------------------------------- /src/build/dep_tree.rs: -------------------------------------------------------------------------------- 1 | use crate::error::GleamExpect; 2 | use petgraph::{algo::Cycle, graph::NodeIndex, Direction}; 3 | use std::collections::{HashMap, HashSet}; 4 | use std::hash::Hash; 5 | 6 | #[derive(Debug, Default)] 7 | pub struct DependencyTree { 8 | graph: petgraph::Graph, 9 | indexes: HashMap, 10 | values: HashMap, 11 | } 12 | 13 | /// Take a sequence of values and their deps, and return the values in 14 | /// order so that deps come before the dependants. 15 | /// 16 | /// Any deps that are not nodes are ignored and presumed to be nodes 17 | /// that do not need processing. 18 | /// 19 | /// Errors if there are duplicate values, unknown deps, or cycles. 20 | /// 21 | pub fn toposort_deps(inputs: Vec<(String, Vec)>) -> Result, Error> { 22 | let mut graph = petgraph::Graph::<(), ()>::with_capacity(inputs.len(), inputs.len() * 5); 23 | let mut values = HashMap::with_capacity(inputs.len()); 24 | let mut indexes = HashMap::with_capacity(inputs.len()); 25 | 26 | for (value, _deps) in inputs.iter() { 27 | let index = graph.add_node(()); 28 | indexes.insert(value.clone(), index); 29 | values.insert(index, value.clone()); 30 | } 31 | 32 | for (value, deps) in inputs { 33 | let from_index = indexes 34 | .get(value.as_str()) 35 | .gleam_expect("Finding index for value"); 36 | for dep in deps.into_iter() { 37 | if let Some(to_index) = indexes.get(dep.as_str()) { 38 | graph.add_edge(*from_index, *to_index, ()); 39 | } 40 | } 41 | } 42 | 43 | match petgraph::algo::toposort(&graph, None) { 44 | Err(e) => Err(Error::Cycle(import_cycle(e, &graph, values))), 45 | 46 | Ok(seq) => Ok(seq 47 | .into_iter() 48 | .map(|i| values.remove(&i).gleam_expect("Finding value for index")) 49 | .rev() 50 | .collect()), 51 | } 52 | } 53 | 54 | // TODO: test 55 | fn import_cycle( 56 | cycle: Cycle, 57 | graph: &petgraph::Graph<(), ()>, 58 | mut values: HashMap, 59 | ) -> Vec { 60 | let origin = cycle.node_id(); 61 | let mut path = vec![]; 62 | find_cycle(origin, origin, &graph, &mut path, &mut HashSet::new()); 63 | path.iter() 64 | .map(|index| { 65 | values 66 | .remove(index) 67 | .gleam_expect("dep_tree::import_cycle(): cannot find values for index") 68 | }) 69 | .collect() 70 | } 71 | 72 | fn find_cycle( 73 | origin: NodeIndex, 74 | parent: NodeIndex, 75 | graph: &petgraph::Graph<(), ()>, 76 | path: &mut Vec, 77 | seen: &mut HashSet, 78 | ) -> bool { 79 | seen.insert(parent); 80 | for node in graph.neighbors_directed(parent, Direction::Outgoing) { 81 | if node == origin { 82 | path.push(node); 83 | return true; 84 | } 85 | if seen.contains(&node) { 86 | continue; 87 | } 88 | if find_cycle(origin, node, graph, path, seen) { 89 | path.push(node); 90 | return true; 91 | } 92 | } 93 | false 94 | } 95 | 96 | #[test] 97 | fn toposort_deps_test() { 98 | // All deps are nodes 99 | assert_eq!( 100 | toposort_deps(vec![ 101 | ("a".to_string(), vec!["b".to_string()]), 102 | ("c".to_string(), vec![]), 103 | ("b".to_string(), vec!["c".to_string()]) 104 | ]), 105 | Ok(vec![ 106 | "c".to_string().to_string(), 107 | "b".to_string().to_string(), 108 | "a".to_string().to_string() 109 | ]) 110 | ); 111 | 112 | // No deps 113 | assert_eq!( 114 | toposort_deps(vec![ 115 | ("no-deps-1".to_string(), vec![]), 116 | ("no-deps-2".to_string(), vec![]) 117 | ]), 118 | Ok(vec!["no-deps-1".to_string(), "no-deps-2".to_string(),]) 119 | ); 120 | 121 | // Some deps are not nodes (and thus are ignored) 122 | assert_eq!( 123 | toposort_deps(vec![ 124 | ("a".to_string(), vec!["b".to_string(), "z".to_string()]), 125 | ("b".to_string(), vec!["x".to_string()]) 126 | ]), 127 | Ok(vec!["b".to_string(), "a".to_string()]) 128 | ); 129 | } 130 | 131 | #[derive(Debug, PartialEq)] 132 | pub enum Error { 133 | Cycle(Vec), 134 | } 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Gleam logo 3 |

4 | 5 |

6 | Fast, friendly, functional! 7 |

8 | 9 |

10 | GitHub release 11 | Discord chat 12 | 13 |

14 | 15 | 16 | 17 |
 
18 | 19 | Gleam is a statically typed functional programming language for building 20 | scalable concurrent systems. It compiles to [Erlang](http://www.erlang.org/) 21 | and has straightforward interop with other BEAM languages such as Erlang, 22 | Elixir and LFE. 23 | 24 | For more information see the Gleam website: [https://gleam.run](https://gleam.run). 25 | 26 | ## Sponsors 27 | 28 | Gleam is kindly supported by its sponsors. If you would like to support Gleam 29 | please consider sponsoring its development [on GitHub](https://github.com/sponsors/lpil). 30 | 31 | Thank you to our top sponsors! Gleam would not be possible without you. 32 | 33 |

34 | Alexander Babin 35 |   36 | NineFX 37 |

38 | 39 | 40 | 41 | - [Adam Mokan](https://github.com/amokan) 42 | - [Alexander Babin](https://github.com/mudriyjo) 43 | - [Ali Farhadi](https://github.com/farhadi) 44 | - [Arian Daneshvar](https://github.com/bees) 45 | - [Ben Myles](https://github.com/benmyles) 46 | - [Chew Choon Keat](https://github.com/choonkeat) 47 | - [Christian Meunier](https://github.com/tlvenn) 48 | - [clangley](https://github.com/clangley) 49 | - [Clever Bunny LTD](https://github.com/cleverbunny) 50 | - [Cole Lawrence](https://github.com/colelawrence) 51 | - [Connor Lay (Clay)](https://github.com/connorlay) 52 | - [Dan Mueller](https://github.com/unthought) 53 | - [Dave Lucia](https://github.com/davydog187) 54 | - [David McKay](https://github.com/rawkode) 55 | - [Dennis Dang](https://github.com/dangdennis) 56 | - [Eric Meadows-Jönsson](https://github.com/ericmj) 57 | - [Erik Terpstra](https://github.com/eterps) 58 | - [Florian Kraft](https://github.com/floriank) 59 | - [Guilherme Pasqualino](https://github.com/ggpasqualino) 60 | - [Hendrik Richter](https://github.com/hendi) 61 | - [Herdy Handoko](https://github.com/hhandoko) 62 | - [human154](https://github.com/human154) 63 | - [Ingmar Gagen](https://github.com/igagen) 64 | - [Ivar Vong](https://github.com/ivarvong) 65 | - [James MacAulay](https://github.com/jamesmacaulay) 66 | - [Jechol Lee](https://github.com/jechol) 67 | - [Jeff Kreeftmeijer](https://github.com/jeffkreeftmeijer) 68 | - [jiangplus](https://github.com/jiangplus) 69 | - [Joe Corkerton](https://github.com/joecorkerton) 70 | - [John Palgut](https://github.com/Jwsonic) 71 | - [José Valim](https://github.com/josevalim) 72 | - [João Veiga](https://github.com/jveiga) 73 | - [Kapp Technology](https://github.com/kapp-technology) 74 | - [Lars Wikman](https://github.com/lawik) 75 | - [Mario Vellandi](https://github.com/mvellandi) 76 | - [mario](https://github.com/mario-mazo) 77 | - [Mark Markaryan](https://github.com/markmark206) 78 | - [Matthew Cheely](https://github.com/MattCheely) 79 | - [Michael Jones](https://github.com/michaeljones) 80 | - [Mike Roach](https://github.com/mroach) 81 | - [Milad](https://github.com/slashmili) 82 | - [Nick Reynolds](https://github.com/ndreynolds) 83 | - [NineFX](http://www.ninefx.com) 84 | - [Parker Selbert](https://github.com/sorentwo) 85 | - [Pete Jodo](https://github.com/PeteJodo) 86 | - [qingliangcn](https://github.com/qingliangcn) 87 | - [Raphael Megzari](https://github.com/happysalada) 88 | - [Raúl Humberto Chouza Delgado](https://github.com/chouzar) 89 | - [René Klačan](https://github.com/reneklacan) 90 | - [Scott Wey](https://github.com/scottwey) 91 | - [Sean Jensen-Grey](https://github.com/seanjensengrey) 92 | - [Shritesh Bhattarai](https://github.com/shritesh) 93 | - [Tomochika Hara](https://github.com/thara) 94 | - [Topher Hunt](https://github.com/topherhunt) 95 | - [Tristan Sloughter](https://github.com/tsloughter) 96 | - [Wojtek Mach](https://github.com/wojtekmach) 97 | -------------------------------------------------------------------------------- /src/typ/fields.rs: -------------------------------------------------------------------------------- 1 | use super::Error; 2 | use crate::ast::{CallArg, SrcSpan}; 3 | use itertools::Itertools; 4 | use std::collections::{HashMap, HashSet}; 5 | 6 | #[derive(Debug, Clone, PartialEq)] 7 | pub struct FieldMap { 8 | pub arity: usize, 9 | pub fields: HashMap, 10 | } 11 | 12 | #[derive(Debug, Clone, Copy)] 13 | pub struct DuplicateField; 14 | 15 | impl FieldMap { 16 | pub fn new(arity: usize) -> Self { 17 | Self { 18 | arity, 19 | fields: HashMap::new(), 20 | } 21 | } 22 | 23 | pub fn insert(&mut self, label: String, index: usize) -> Result<(), DuplicateField> { 24 | match self.fields.insert(label, index) { 25 | Some(_) => Err(DuplicateField), 26 | None => Ok(()), 27 | } 28 | } 29 | 30 | pub fn into_option(self) -> Option { 31 | if self.fields.is_empty() { 32 | None 33 | } else { 34 | Some(self) 35 | } 36 | } 37 | 38 | /// Reorder an argument list so that labelled fields supplied out-of-order are 39 | /// in the correct order. 40 | /// 41 | pub fn reorder(&self, args: &mut Vec>, location: SrcSpan) -> Result<(), Error> { 42 | let mut labelled_arguments_given = false; 43 | let mut seen_labels = std::collections::HashSet::new(); 44 | let mut unknown_labels = Vec::new(); 45 | 46 | if self.arity != args.len() { 47 | return Err(Error::IncorrectArity { 48 | labels: self.incorrect_arity_labels(args), 49 | location, 50 | expected: self.arity, 51 | given: args.len(), 52 | }); 53 | } 54 | 55 | for arg in args.iter() { 56 | match &arg.label { 57 | Some(_) => { 58 | labelled_arguments_given = true; 59 | } 60 | 61 | None => { 62 | if labelled_arguments_given { 63 | return Err(Error::PositionalArgumentAfterLabelled { 64 | location: arg.location, 65 | }); 66 | } 67 | } 68 | } 69 | } 70 | 71 | let mut i = 0; 72 | while i < args.len() { 73 | let (label, location) = match &args[i].label { 74 | // A labelled argument, we may need to reposition it in the array vector 75 | Some(l) => (l, &args[i].location), 76 | 77 | // Not a labelled argument 78 | None => { 79 | i += 1; 80 | continue; 81 | } 82 | }; 83 | 84 | let position = match self.fields.get(label) { 85 | None => { 86 | unknown_labels.push((label.clone(), *location)); 87 | i += 1; 88 | continue; 89 | } 90 | 91 | Some(p) => *p, 92 | }; 93 | 94 | // If the argument is already in the right place 95 | if position == i { 96 | let _ = seen_labels.insert(label.clone()); 97 | i += 1; 98 | } else { 99 | if seen_labels.contains(label) { 100 | return Err(Error::DuplicateArgument { 101 | location: *location, 102 | label: label.to_string(), 103 | }); 104 | } 105 | let _ = seen_labels.insert(label.clone()); 106 | 107 | args.swap(position, i); 108 | } 109 | } 110 | 111 | if unknown_labels.is_empty() { 112 | Ok(()) 113 | } else { 114 | Err(Error::UnknownLabels { 115 | valid: self.fields.keys().map(|t| t.to_string()).collect(), 116 | unknown: unknown_labels, 117 | supplied: seen_labels.into_iter().collect(), 118 | }) 119 | } 120 | } 121 | 122 | pub fn incorrect_arity_labels(&self, args: &[CallArg]) -> Vec { 123 | let mut given = HashSet::with_capacity(args.len()); 124 | for arg in args { 125 | if let Some(label) = &arg.label { 126 | let _ = given.insert(label.as_ref()); 127 | } 128 | } 129 | self.fields 130 | .keys() 131 | .cloned() 132 | .filter(|f| !given.contains(f.as_str())) 133 | .sorted() 134 | .collect() 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | test: 9 | name: test 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | toolchain: [stable] 14 | build: [linux-amd64, linux-musl-amd64, macos, windows] 15 | include: 16 | - build: linux-amd64 17 | os: ubuntu-latest 18 | target: x86_64-unknown-linux-gnu 19 | use-cross: false 20 | - build: linux-musl-amd64 21 | os: ubuntu-latest 22 | target: x86_64-unknown-linux-musl 23 | use-cross: true 24 | - build: macos 25 | os: macos-latest 26 | target: x86_64-apple-darwin 27 | use-cross: false 28 | - build: windows 29 | os: windows-latest 30 | target: x86_64-pc-windows-msvc 31 | use-cross: false 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v2 35 | 36 | - name: Install Rust toolchain 37 | uses: actions-rs/toolchain@v1 38 | with: 39 | toolchain: ${{ matrix.toolchain }} 40 | target: ${{ matrix.target }} 41 | profile: minimal 42 | override: true 43 | 44 | - name: Build binary 45 | uses: actions-rs/cargo@v1 46 | with: 47 | use-cross: ${{ matrix.use-cross }} 48 | command: build 49 | args: --release --target ${{ matrix.target }} 50 | 51 | - name: Run tests 52 | uses: actions-rs/cargo@v1 53 | with: 54 | use-cross: ${{ matrix.use-cross }} 55 | command: test 56 | args: --workspace --target ${{ matrix.target }} 57 | 58 | - name: Upload artifact (Ubuntu) 59 | if: matrix.build == 'linux-amd64' 60 | uses: actions/upload-artifact@v2 61 | with: 62 | name: gleam 63 | path: target/${{ matrix.target }}/release/gleam 64 | 65 | rustfmt: 66 | name: rustfmt 67 | runs-on: ubuntu-latest 68 | steps: 69 | - name: Checkout repository 70 | uses: actions/checkout@v2 71 | 72 | - name: Install Rust toolchain 73 | uses: actions-rs/toolchain@v1 74 | with: 75 | toolchain: stable 76 | override: true 77 | profile: minimal 78 | components: rustfmt 79 | 80 | - name: Check formatting 81 | run: cargo fmt --all -- --check 82 | 83 | cargo_deny: 84 | name: Cargo deny 85 | runs-on: ubuntu-latest 86 | steps: 87 | - name: Checkout repository 88 | uses: actions/checkout@v2 89 | 90 | - name: Install Rust toolchain 91 | uses: actions-rs/toolchain@v1 92 | with: 93 | toolchain: stable 94 | override: true 95 | profile: minimal 96 | - run: | 97 | set -e 98 | curl -L https://github.com/EmbarkStudios/cargo-deny/releases/download/0.8.5/cargo-deny-0.8.5-x86_64-unknown-linux-musl.tar.gz | tar xzf - 99 | mv cargo-deny-*-x86_64-unknown-linux-musl/cargo-deny cargo-deny 100 | echo `pwd` >> $GITHUB_PATH 101 | - run: cargo deny check 102 | 103 | test-core-language: 104 | name: test-core-language 105 | needs: test 106 | runs-on: ubuntu-latest 107 | steps: 108 | - name: Checkout repository 109 | uses: actions/checkout@v2.0.0 110 | 111 | - name: Install Erlang 112 | uses: gleam-lang/setup-erlang@v1.1.0 113 | with: 114 | otp-version: 22.1 115 | 116 | - name: Download test artifact 117 | uses: actions/download-artifact@v2 118 | with: 119 | name: gleam 120 | path: ./test/core_language 121 | 122 | - name: Configure ./test/core_language to use Gleam binary 123 | run: | 124 | echo $PWD/ >> $GITHUB_PATH 125 | chmod +x ./gleam 126 | sed -i 's/cargo run --/gleam/' rebar.config 127 | working-directory: ./test/core_language 128 | 129 | - run: rebar3 eunit 130 | working-directory: ./test/core_language 131 | 132 | # Test app template 133 | - run: gleam new app_project --template=app 134 | - run: rebar3 eunit 135 | working-directory: ./app_project 136 | 137 | # Test lib template 138 | - run: gleam new lib_project --template=lib 139 | - run: rebar3 eunit 140 | working-directory: ./lib_project 141 | 142 | # Test escript template 143 | - run: gleam new escript_project --template=escript 144 | - run: rebar3 eunit 145 | working-directory: ./escript_project 146 | - run: rebar3 escriptize 147 | working-directory: ./escript_project 148 | - run: _build/default/bin/escript_project 149 | working-directory: ./escript_project 150 | -------------------------------------------------------------------------------- /templates/documentation_module.html: -------------------------------------------------------------------------------- 1 | {% extends "documentation_layout.html" %} 2 | 3 | {% block sidebar_content %} 4 | {% if !types.is_empty() %} 5 |

Types

6 |
11 | {% endif %} 12 | 13 | {% if !constants.is_empty() %} 14 |

Constants

15 | 20 | {% endif %} 21 | 22 | {% if !functions.is_empty() %} 23 |

Functions

24 | 29 | {% endif %} 30 | {% endblock %} 31 | 32 | {% block content %} 33 |

34 | {{ module_name }} 35 | 36 |

37 | {{ documentation|safe }} 38 | 39 | {% if !types.is_empty() %} 40 |
41 |

42 | Types 43 | 44 |

45 | 46 | {% for typ in types %} 47 |
48 |
49 |

50 | 51 | {{ typ.name }} 52 | 53 |

54 | {% if !typ.source_url.is_empty() %} 55 | 56 | </> 57 | 58 | {% endif %} 59 |
60 |
61 |
{{ typ.documentation|safe }}
62 |
{{ typ.definition }}
63 | {% if !typ.constructors.is_empty() %} 64 |

65 | Constructors 66 |

67 |
    68 | {% for constructor in typ.constructors %} 69 |
  • 70 | 71 |
    {{ constructor.definition }}
    72 | {{ constructor.documentation|safe }} 73 |
  • 74 | {% endfor %} 75 |
76 | {% endif %} 77 |
78 |
79 | {% endfor %} 80 |
81 | {% endif %} 82 | 83 | {% if !constants.is_empty() %} 84 |
85 |

86 | Constants 87 | 88 |

89 | 90 | {% for constant in constants %} 91 |
92 |
93 |

94 | 95 | {{ constant.name }} 96 | 97 |

98 | {% if !constant.source_url.is_empty() %} 99 | 100 | </> 101 | 102 | {% endif %} 103 |
104 |
{{ constant.definition }}
105 |
{{ constant.documentation|safe }}
106 |
107 | {% endfor %} 108 |
109 | {% endif %} 110 | 111 | {% if !functions.is_empty() %} 112 |
113 |

114 | Functions 115 | 116 |

117 | {% for function in functions %} 118 |
119 |
120 |

121 | 122 | {{ function.name }} 123 | 124 |

125 | {% if !function.source_url.is_empty() %} 126 | 127 | </> 128 | 129 | {% endif %} 130 |
131 |
{{ function.signature }}
132 |
{{ function.documentation|safe }}
133 |
134 | {% endfor %} 135 |
136 | {% endif %} 137 | {% endblock %} 138 | -------------------------------------------------------------------------------- /src/build/project_compiler.rs: -------------------------------------------------------------------------------- 1 | use codegen::ErlangApp; 2 | 3 | use crate::{ 4 | build::{ 5 | dep_tree, package_compiler::PackageCompiler, project_root::ProjectRoot, Origin, Package, 6 | }, 7 | codegen, 8 | config::PackageConfig, 9 | fs::FileSystemAccessor, 10 | typ, Error, GleamExpect, 11 | }; 12 | use std::{collections::HashMap, path::PathBuf}; 13 | 14 | use super::package_compiler; 15 | 16 | #[derive(Debug)] 17 | pub struct ProjectCompiler<'a> { 18 | root: &'a ProjectRoot, 19 | root_config: PackageConfig, 20 | configs: HashMap, 21 | packages: HashMap, 22 | type_manifests: HashMap, 23 | defined_modules: HashMap, 24 | } 25 | 26 | // TODO: test top level package has test modules compiled 27 | // TODO: test that tests cannot be imported into src 28 | // TODO: test that dep cycles are not allowed between packages 29 | 30 | impl<'a> ProjectCompiler<'a> { 31 | pub fn new( 32 | root: &'a ProjectRoot, 33 | root_config: PackageConfig, 34 | configs: HashMap, 35 | ) -> Self { 36 | let estimated_number_of_modules = configs.len() * 5; 37 | Self { 38 | packages: HashMap::with_capacity(configs.len()), 39 | type_manifests: HashMap::with_capacity(estimated_number_of_modules), 40 | defined_modules: HashMap::with_capacity(estimated_number_of_modules), 41 | root_config, 42 | configs, 43 | root, 44 | } 45 | } 46 | 47 | pub fn compile(mut self) -> Result, Error> { 48 | // Determine package processing order 49 | let sequence = order_packages(&self.configs)?; 50 | 51 | // Read and type check deps packages 52 | for name in sequence.into_iter() { 53 | let config = self 54 | .configs 55 | .remove(name.as_str()) 56 | .gleam_expect("Missing package config"); 57 | self.compile_package(name, config, SourceLocations::Src)?; 58 | } 59 | 60 | // Read and type check top level package 61 | let root_config = std::mem::replace(&mut self.root_config, Default::default()); 62 | let name = root_config.name.clone(); 63 | self.compile_package(name, root_config, SourceLocations::SrcAndTest)?; 64 | 65 | Ok(self.packages) 66 | } 67 | 68 | fn compile_package( 69 | &mut self, 70 | name: String, 71 | config: PackageConfig, 72 | locations: SourceLocations, 73 | ) -> Result<(), Error> { 74 | crate::cli::print_compiling(name.as_str()); 75 | let test_path = match locations { 76 | SourceLocations::SrcAndTest => { 77 | Some(self.root.default_build_lib_package_test_path(&name)) 78 | } 79 | _ => None, 80 | }; 81 | 82 | // TODO: this isn't the right location. We may want multiple output locations. 83 | let out_path = self.root.default_build_lib_package_src_path(&name); 84 | let options = package_compiler::Options { 85 | src_path: self.root.default_build_lib_package_src_path(&name), 86 | out_path: out_path.clone(), 87 | test_path, 88 | name: name.clone(), 89 | }; 90 | 91 | let mut compiler = options.into_compiler(FileSystemAccessor::new())?; 92 | 93 | // Compile project 94 | let compiled = compiler.compile(&mut self.type_manifests, &mut self.defined_modules)?; 95 | ErlangApp::new(out_path.as_path()).render( 96 | &FileSystemAccessor::new(), 97 | &config, 98 | compiled.modules.as_slice(), 99 | )?; 100 | 101 | self.packages.insert(name, compiled); 102 | Ok(()) 103 | } 104 | } 105 | 106 | #[derive(Debug, PartialEq)] 107 | enum SourceLocations { 108 | Src, 109 | SrcAndTest, 110 | } 111 | 112 | fn order_packages(configs: &HashMap) -> Result, Error> { 113 | dep_tree::toposort_deps(configs.values().map(package_deps_for_graph).collect()) 114 | .map_err(convert_deps_tree_error) 115 | } 116 | 117 | fn convert_deps_tree_error(e: dep_tree::Error) -> Error { 118 | match e { 119 | dep_tree::Error::Cycle(packages) => Error::PackageCycle { packages }, 120 | } 121 | } 122 | 123 | fn package_deps_for_graph(config: &PackageConfig) -> (String, Vec) { 124 | let name = config.name.to_string(); 125 | let deps: Vec<_> = config 126 | .dependencies 127 | .iter() 128 | .map(|(dep, _)| dep.to_string()) 129 | .collect(); 130 | (name, deps) 131 | } 132 | -------------------------------------------------------------------------------- /src/ast/untyped.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Debug, PartialEq, Clone)] 4 | pub enum UntypedExpr { 5 | Int { 6 | location: SrcSpan, 7 | value: String, 8 | }, 9 | 10 | Float { 11 | location: SrcSpan, 12 | value: String, 13 | }, 14 | 15 | String { 16 | location: SrcSpan, 17 | value: String, 18 | }, 19 | 20 | Seq { 21 | first: Box, 22 | then: Box, 23 | }, 24 | 25 | Var { 26 | location: SrcSpan, 27 | name: String, 28 | }, 29 | 30 | Fn { 31 | location: SrcSpan, 32 | is_capture: bool, 33 | args: Vec>, 34 | body: Box, 35 | return_annotation: Option, 36 | }, 37 | 38 | ListNil { 39 | location: SrcSpan, 40 | }, 41 | 42 | ListCons { 43 | location: SrcSpan, 44 | head: Box, 45 | tail: Box, 46 | }, 47 | 48 | Call { 49 | location: SrcSpan, 50 | fun: Box, 51 | args: Vec>, 52 | }, 53 | 54 | BinOp { 55 | location: SrcSpan, 56 | name: BinOp, 57 | left: Box, 58 | right: Box, 59 | }, 60 | 61 | Pipe { 62 | location: SrcSpan, 63 | left: Box, 64 | right: Box, 65 | }, 66 | 67 | Let { 68 | location: SrcSpan, 69 | value: Box, 70 | pattern: Pattern<(), ()>, 71 | then: Box, 72 | kind: BindingKind, 73 | annotation: Option, 74 | }, 75 | 76 | Case { 77 | location: SrcSpan, 78 | subjects: Vec, 79 | clauses: Vec>, 80 | }, 81 | 82 | FieldAccess { 83 | location: SrcSpan, 84 | label: String, 85 | container: Box, 86 | }, 87 | 88 | Tuple { 89 | location: SrcSpan, 90 | elems: Vec, 91 | }, 92 | 93 | TupleIndex { 94 | location: SrcSpan, 95 | index: u64, 96 | tuple: Box, 97 | }, 98 | 99 | Todo { 100 | location: SrcSpan, 101 | label: Option, 102 | }, 103 | 104 | BitString { 105 | location: SrcSpan, 106 | segments: Vec, 107 | }, 108 | 109 | RecordUpdate { 110 | location: SrcSpan, 111 | constructor: Box, 112 | spread: RecordUpdateSpread, 113 | args: Vec, 114 | }, 115 | } 116 | 117 | impl UntypedExpr { 118 | pub fn location(&self) -> SrcSpan { 119 | match self { 120 | Self::Seq { then, .. } => then.location(), 121 | Self::Let { then, .. } => then.location(), 122 | Self::Pipe { right, .. } => right.location(), 123 | Self::Fn { location, .. } 124 | | Self::Var { location, .. } 125 | | Self::Int { location, .. } 126 | | Self::Todo { location, .. } 127 | | Self::Case { location, .. } 128 | | Self::Call { location, .. } 129 | | Self::Float { location, .. } 130 | | Self::BinOp { location, .. } 131 | | Self::Tuple { location, .. } 132 | | Self::String { location, .. } 133 | | Self::ListNil { location, .. } 134 | | Self::ListCons { location, .. } 135 | | Self::TupleIndex { location, .. } 136 | | Self::FieldAccess { location, .. } 137 | | Self::BitString { location, .. } 138 | | Self::RecordUpdate { location, .. } => *location, 139 | } 140 | } 141 | 142 | pub fn start_byte_index(&self) -> usize { 143 | match self { 144 | Self::Seq { first, .. } => first.start_byte_index(), 145 | Self::Pipe { left, .. } => left.start_byte_index(), 146 | Self::Let { location, .. } => location.start, 147 | _ => self.location().start, 148 | } 149 | } 150 | 151 | pub fn binop_precedence(&self) -> u8 { 152 | match self { 153 | Self::BinOp { name, .. } => name.precedence(), 154 | Self::Pipe { .. } => 5, 155 | _ => std::u8::MAX, 156 | } 157 | } 158 | 159 | pub fn is_simple_constant(&self) -> bool { 160 | matches!(self, Self::String { .. } | Self::Int { .. } | Self::Float { .. }) 161 | } 162 | 163 | pub fn is_literal(&self) -> bool { 164 | matches!(self, 165 | Self::Int { .. } | Self::Float { .. } | Self::ListNil { .. } | Self::ListCons { .. } 166 | | Self::Tuple { .. } | Self::String { .. } | Self::BitString { .. }) 167 | } 168 | } 169 | 170 | impl HasLocation for UntypedExpr { 171 | fn location(&self) -> SrcSpan { 172 | self.location() 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/parse/token.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Clone, Debug, PartialEq)] 4 | pub enum Tok { 5 | Name { name: String }, 6 | UpName { name: String }, 7 | DiscardName { name: String }, 8 | Int { value: String }, 9 | Float { value: String }, 10 | String { value: String }, 11 | // Groupings 12 | Lpar, 13 | Rpar, 14 | Lsqb, 15 | Rsqb, 16 | Lbrace, 17 | Rbrace, 18 | // Int Operators 19 | Plus, 20 | Minus, 21 | Star, 22 | Slash, 23 | Less, 24 | Greater, 25 | LessEqual, 26 | GreaterEqual, 27 | Percent, 28 | // Float Operators 29 | PlusDot, // '+.' 30 | MinusDot, // '-.' 31 | StarDot, // '*.' 32 | SlashDot, // '/.' 33 | LessDot, // '<.' 34 | GreaterDot, // '>.' 35 | LessEqualDot, // '<=.' 36 | GreaterEqualDot, // '>=.' 37 | // Other Punctuation 38 | Colon, 39 | Comma, 40 | Equal, 41 | EqualEqual, // '==' 42 | NotEqual, // '!=' 43 | Vbar, // '|' 44 | VbarVbar, // '||' 45 | AmperAmper, // '&&' 46 | LtLt, // '<<' 47 | GtGt, // '>>' 48 | Pipe, // '|>' 49 | Dot, // '.' 50 | RArrow, // '->' 51 | DotDot, // '..' 52 | ListNil, // '[]' 53 | EndOfFile, 54 | // Extra 55 | CommentNormal, 56 | CommentDoc, 57 | CommentModule, 58 | EmptyLine, 59 | // Keywords (alphabetically): 60 | As, 61 | Assert, 62 | Case, 63 | Const, 64 | External, 65 | Fn, 66 | If, 67 | Import, 68 | Let, 69 | Opaque, 70 | Pub, 71 | Todo, 72 | Try, 73 | Tuple, 74 | Type, 75 | } 76 | 77 | impl Tok { 78 | pub fn guard_precedence(&self) -> Option { 79 | match self { 80 | Self::VbarVbar => Some(1), 81 | 82 | Self::AmperAmper => Some(2), 83 | 84 | Self::EqualEqual | Self::NotEqual => Some(3), 85 | 86 | Self::Less 87 | | Self::LessEqual 88 | | Self::LessDot 89 | | Self::LessEqualDot 90 | | Self::GreaterEqual 91 | | Self::Greater 92 | | Self::GreaterEqualDot 93 | | Self::GreaterDot => Some(4), 94 | 95 | _ => None, 96 | } 97 | } 98 | } 99 | 100 | impl fmt::Display for Tok { 101 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 102 | let s = match self { 103 | Tok::Name { name } => name, 104 | Tok::UpName { name } => name, 105 | Tok::DiscardName { name } => name, 106 | Tok::Int { value } => value, 107 | Tok::Float { value } => value, 108 | Tok::String { value } => value, 109 | Tok::Lpar => "(", 110 | Tok::Rpar => ")", 111 | Tok::Lsqb => "[", 112 | Tok::Rsqb => "]", 113 | Tok::Lbrace => "{", 114 | Tok::Rbrace => "}", 115 | Tok::Plus => "+", 116 | Tok::Minus => "-", 117 | Tok::Star => "*", 118 | Tok::Slash => "/", 119 | Tok::Less => "<", 120 | Tok::Greater => ">", 121 | Tok::LessEqual => "<=", 122 | Tok::GreaterEqual => ">=", 123 | Tok::Percent => "%", 124 | Tok::PlusDot => "+.", 125 | Tok::MinusDot => "-.", 126 | Tok::StarDot => "*.", 127 | Tok::SlashDot => "/.", 128 | Tok::LessDot => "<.", 129 | Tok::GreaterDot => ">.", 130 | Tok::LessEqualDot => "<=.", 131 | Tok::GreaterEqualDot => ">=.", 132 | Tok::Colon => ":", 133 | Tok::Comma => ",", 134 | Tok::Equal => "=", 135 | Tok::EqualEqual => "==", 136 | Tok::NotEqual => "!=", 137 | Tok::Vbar => "|", 138 | Tok::VbarVbar => "||", 139 | Tok::AmperAmper => "&&", 140 | Tok::LtLt => "<<", 141 | Tok::GtGt => ">>", 142 | Tok::Pipe => "|>", 143 | Tok::Dot => ".", 144 | Tok::RArrow => "->", 145 | Tok::DotDot => "..", 146 | Tok::ListNil => "[]", 147 | Tok::EndOfFile => "EOF", 148 | Tok::CommentNormal => "//", 149 | Tok::CommentDoc => "///", 150 | Tok::CommentModule => "////", 151 | Tok::EmptyLine => "EMPTYLINE", 152 | Tok::As => "as", 153 | Tok::Assert => "assert", 154 | Tok::Case => "case", 155 | Tok::Const => "const", 156 | Tok::External => "external", 157 | Tok::Fn => "fn", 158 | Tok::If => "if", 159 | Tok::Import => "import", 160 | Tok::Let => "let", 161 | Tok::Opaque => "opaque", 162 | Tok::Pub => "pub", 163 | Tok::Todo => "todo", 164 | Tok::Try => "try", 165 | Tok::Tuple => "tuple", 166 | Tok::Type => "type", 167 | }; 168 | write!(f, "\"{}\"", s) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/parse/error.rs: -------------------------------------------------------------------------------- 1 | use crate::ast::SrcSpan; 2 | use heck::CamelCase; 3 | use heck::SnakeCase; 4 | 5 | #[derive(Debug, PartialEq, Clone)] 6 | pub struct LexicalError { 7 | pub error: LexicalErrorType, 8 | pub location: SrcSpan, 9 | } 10 | 11 | #[derive(Debug, PartialEq, Clone)] 12 | pub enum LexicalErrorType { 13 | BadStringEscape, // string contains an unescaped slash 14 | DigitOutOfRadix, // 0x012 , 2 is out of radix 15 | NumTrailingUnderscore, // 1_000_ is not allowed 16 | RadixIntNoValue, // 0x, 0b, 0o without a value 17 | UnexpectedStringEnd, // Unterminated string literal 18 | UnrecognizedToken { tok: char }, 19 | BadName { name: String }, 20 | BadDiscardName { name: String }, 21 | BadUpname { name: String }, 22 | } 23 | 24 | #[derive(Debug, PartialEq)] 25 | pub struct ParseError { 26 | pub error: ParseErrorType, 27 | pub location: SrcSpan, 28 | } 29 | 30 | #[derive(Debug, PartialEq)] 31 | pub enum ParseErrorType { 32 | ExpectedExpr, // after "->" in a case clause 33 | ExpectedName, // any token used when a Name was expected 34 | ExpectedPattern, // after ':' where a pattern is expected 35 | ExpectedType, // after ':' or '->' where a type annotation is expected 36 | ExpectedUpName, // any token used when a UpName was expected 37 | ExpectedValue, // no value after "=" 38 | ExprLparStart, // it seems "(" was used to start an expression 39 | ExprTailBinding, // a binding in the tail position of an expression sequence 40 | ExtraSeparator, // tuple(1,,) <- the 2nd comma is an extra separator 41 | IncorrectName, // UpName or DiscardName used when Name was expected 42 | IncorrectUpName, // Name or DiscardName used when UpName was expected 43 | InvalidBitStringSegment, // <<7:hello>> `hello` is an invalid bitstring segment 44 | InvalidBitStringUnit, // in <<1:unit(x)>> x must be 1 <= x <= 256 45 | InvalidTailPattern, // only name and _name are allowed after ".." in list pattern 46 | InvalidTupleAccess, // only positive int literals for tuple access 47 | LexError { error: LexicalError }, 48 | ListNilNotAllowed, // [] is not allowed here 49 | NestedBitStringPattern, // <<<<1>>, 2>>, <<1>> is not allowed in there 50 | NoConstructors, // A type "A {}" must have at least one constructor 51 | NoCaseClause, // a case with no claueses 52 | NoExpression, // between "{" and "}" in expression position, there must be an expression 53 | NoValueAfterEqual, // = 54 | NotConstType, // :fn(), name, _ are not valid const types 55 | OpNakedRight, // Operator with no value to the right 56 | OpaqueTypeAlias, // Type aliases cannot be opaque 57 | TooManyArgHoles, // a function call can have at most 1 arg hole 58 | UnexpectedEOF, 59 | UnexpectedReservedWord, // reserved word used when a name was expected 60 | UnexpectedToken { expected: Vec }, 61 | } 62 | 63 | impl LexicalError { 64 | pub fn to_parse_error_info(&self) -> (&str, Vec) { 65 | match &self.error { 66 | LexicalErrorType::BadStringEscape => ( 67 | "I don't understand this escape code", 68 | vec![ 69 | "Hint: Add another backslash before it.".to_string(), 70 | "See: https://gleam.run/book/tour/strings.html#escape-sequences".to_string(), 71 | ], 72 | ), 73 | LexicalErrorType::DigitOutOfRadix => { 74 | ("This digit is too big for the specified radix.", vec![]) 75 | } 76 | LexicalErrorType::NumTrailingUnderscore => ( 77 | "Numbers cannot have a trailing underscore.", 78 | vec!["Hint: remove it.".to_string()], 79 | ), 80 | LexicalErrorType::RadixIntNoValue => ("This integer has no value.", vec![]), 81 | LexicalErrorType::UnexpectedStringEnd => { 82 | ("The string starting here was left open.", vec![]) 83 | } 84 | LexicalErrorType::UnrecognizedToken { .. } => ( 85 | "I can't figure out what to do with this character.", 86 | vec!["Hint: Is it a typo?".to_string()], 87 | ), 88 | LexicalErrorType::BadName { name } => ( 89 | "This is not a valid name.", 90 | vec![ 91 | "Hint: Names start with a lowercase letter and contain a-z, 0-9, or _." 92 | .to_string(), 93 | format!("Try: {}", name.to_snake_case()), 94 | ], 95 | ), 96 | LexicalErrorType::BadDiscardName { name } => ( 97 | "This is not a valid discard name.", 98 | vec![ 99 | "Hint: Discard names start with _ and contain a-z, 0-9, or _.".to_string(), 100 | format!("Try: _{}", name.to_snake_case()), 101 | ], 102 | ), 103 | LexicalErrorType::BadUpname { name } => ( 104 | "This is not a valid upname.", 105 | vec![ 106 | "Hint: Upnames start with an uppercase letter and contain".to_string(), 107 | "only lowercase letters, numbers, and uppercase letters.".to_string(), 108 | format!("Try: {}", name.to_camel_case()), 109 | ], 110 | ), 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/parse/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::ast::SrcSpan; 2 | use crate::parse::error::{LexicalError, LexicalErrorType, ParseError, ParseErrorType}; 3 | 4 | macro_rules! assert_error { 5 | ($src:expr, $error:expr $(,)?) => { 6 | let result = crate::parse::parse_expression_sequence($src).expect_err("should not parse"); 7 | assert_eq!(($src, $error), ($src, result),); 8 | }; 9 | } 10 | 11 | #[test] 12 | fn int_tests() { 13 | // bad binary digit 14 | assert_error!( 15 | "0b012", 16 | ParseError { 17 | error: ParseErrorType::LexError { 18 | error: LexicalError { 19 | error: LexicalErrorType::DigitOutOfRadix, 20 | location: SrcSpan { start: 4, end: 4 }, 21 | } 22 | }, 23 | location: SrcSpan { start: 4, end: 4 }, 24 | } 25 | ); 26 | // bad octal digit 27 | assert_error!( 28 | "0o12345678", 29 | ParseError { 30 | error: ParseErrorType::LexError { 31 | error: LexicalError { 32 | error: LexicalErrorType::DigitOutOfRadix, 33 | location: SrcSpan { start: 9, end: 9 }, 34 | } 35 | }, 36 | location: SrcSpan { start: 9, end: 9 }, 37 | } 38 | ); 39 | // no int value 40 | assert_error!( 41 | "0x", 42 | ParseError { 43 | error: ParseErrorType::LexError { 44 | error: LexicalError { 45 | error: LexicalErrorType::RadixIntNoValue, 46 | location: SrcSpan { start: 1, end: 1 }, 47 | } 48 | }, 49 | location: SrcSpan { start: 1, end: 1 }, 50 | } 51 | ); 52 | // trailing underscore 53 | assert_error!( 54 | "1_000_", 55 | ParseError { 56 | error: ParseErrorType::LexError { 57 | error: LexicalError { 58 | error: LexicalErrorType::NumTrailingUnderscore, 59 | location: SrcSpan { start: 5, end: 5 }, 60 | } 61 | }, 62 | location: SrcSpan { start: 5, end: 5 }, 63 | } 64 | ); 65 | } 66 | 67 | #[test] 68 | fn string_tests() { 69 | // bad character escape 70 | assert_error!( 71 | r#""\g""#, 72 | ParseError { 73 | error: ParseErrorType::LexError { 74 | error: LexicalError { 75 | error: LexicalErrorType::BadStringEscape, 76 | location: SrcSpan { start: 1, end: 1 }, 77 | } 78 | }, 79 | location: SrcSpan { start: 1, end: 1 }, 80 | } 81 | ); 82 | 83 | // still bad character escape 84 | assert_error!( 85 | r#""\\\g""#, 86 | ParseError { 87 | error: ParseErrorType::LexError { 88 | error: LexicalError { 89 | error: LexicalErrorType::BadStringEscape, 90 | location: SrcSpan { start: 3, end: 3 }, 91 | } 92 | }, 93 | location: SrcSpan { start: 3, end: 3 }, 94 | } 95 | ); 96 | } 97 | 98 | #[test] 99 | fn bit_string_tests() { 100 | // non int value in BitString unit option 101 | assert_error!( 102 | "let x = <<1:unit(0)>> x", 103 | ParseError { 104 | error: ParseErrorType::InvalidBitStringUnit, 105 | location: SrcSpan { start: 17, end: 18 } 106 | } 107 | ); 108 | 109 | assert_error!( 110 | "let x = <<1:unit(257)>> x", 111 | ParseError { 112 | error: ParseErrorType::InvalidBitStringUnit, 113 | location: SrcSpan { start: 17, end: 20 } 114 | } 115 | ); 116 | 117 | // patterns cannot be nested 118 | assert_error!( 119 | "case <<>> { <<<<1>>:bit_string>> -> 1 }", 120 | ParseError { 121 | error: ParseErrorType::NestedBitStringPattern, 122 | location: SrcSpan { start: 14, end: 19 } 123 | } 124 | ); 125 | } 126 | 127 | #[test] 128 | fn name_tests() { 129 | assert_error!( 130 | "let xS = 1", 131 | ParseError { 132 | error: ParseErrorType::LexError { 133 | error: LexicalError { 134 | error: LexicalErrorType::BadName { 135 | name: "xS".to_string() 136 | }, 137 | location: SrcSpan { start: 4, end: 6 }, 138 | } 139 | }, 140 | location: SrcSpan { start: 4, end: 6 }, 141 | } 142 | ); 143 | 144 | assert_error!( 145 | "let _xS = 1", 146 | ParseError { 147 | error: ParseErrorType::LexError { 148 | error: LexicalError { 149 | error: LexicalErrorType::BadDiscardName { 150 | name: "_xS".to_string() 151 | }, 152 | location: SrcSpan { start: 4, end: 7 }, 153 | } 154 | }, 155 | location: SrcSpan { start: 4, end: 7 }, 156 | } 157 | ); 158 | 159 | assert_error!( 160 | "type S_m = String", 161 | ParseError { 162 | error: ParseErrorType::LexError { 163 | error: LexicalError { 164 | error: LexicalErrorType::BadUpname { 165 | name: "S_m".to_string() 166 | }, 167 | location: SrcSpan { start: 5, end: 8 }, 168 | } 169 | }, 170 | location: SrcSpan { start: 5, end: 8 }, 171 | } 172 | ); 173 | } 174 | -------------------------------------------------------------------------------- /src/metadata.rs: -------------------------------------------------------------------------------- 1 | //! Seriaisation and deserialisation of Gleam compiler metadata into binary files 2 | //! using the Cap'n Proto schema. 3 | 4 | // TODO: remove 5 | #![allow(unused)] 6 | 7 | mod module_encoder; 8 | 9 | #[cfg(test)] 10 | mod tests; 11 | 12 | pub use self::module_encoder::ModuleEncoder; 13 | 14 | use crate::{ 15 | schema_capnp as schema, 16 | typ::{self, AccessorsMap, Module, Type, TypeConstructor, ValueConstructor}, 17 | Result, 18 | }; 19 | use std::{collections::HashMap, io::BufRead, sync::Arc}; 20 | 21 | #[derive(Debug, Default)] 22 | pub struct ModuleDecoder { 23 | next_type_var_id: usize, 24 | type_var_id_map: HashMap, 25 | } 26 | 27 | impl ModuleDecoder { 28 | pub fn new() -> Self { 29 | Default::default() 30 | } 31 | 32 | pub fn read(&mut self, reader: impl BufRead) -> Result { 33 | let message_reader = 34 | capnp::serialize_packed::read_message(reader, capnp::message::ReaderOptions::new())?; 35 | let module = message_reader.get_root::>()?; 36 | 37 | Ok(Module { 38 | name: name(&module.get_name()?)?, 39 | types: self.module_types(&module)?, 40 | values: self.module_values(&module)?, 41 | accessors: self.module_accessors(&module)?, 42 | }) 43 | } 44 | 45 | fn module_types( 46 | &mut self, 47 | reader: &schema::module::Reader<'_>, 48 | ) -> Result> { 49 | let types_reader = reader.get_types()?; 50 | let mut types = HashMap::with_capacity(types_reader.len() as usize); 51 | for prop in types_reader.into_iter() { 52 | let name = prop.get_key()?; 53 | let type_ = self.type_constructor(&prop.get_value()?)?; 54 | let _ = types.insert(name.to_string(), type_); 55 | } 56 | Ok(types) 57 | } 58 | 59 | fn type_constructor( 60 | &mut self, 61 | reader: &schema::type_constructor::Reader<'_>, 62 | ) -> Result { 63 | let type_ = self.type_(&reader.get_type()?)?; 64 | let module = name(&reader.get_module()?)?; 65 | let reader = reader.get_parameters()?; 66 | let mut parameters = Vec::with_capacity(reader.len() as usize); 67 | for reader in reader.into_iter() { 68 | parameters.push(self.type_(&reader)?); 69 | } 70 | Ok(TypeConstructor { 71 | public: true, 72 | origin: Default::default(), 73 | module, 74 | parameters, 75 | typ: type_, 76 | }) 77 | } 78 | 79 | fn types( 80 | &mut self, 81 | reader: &capnp::struct_list::Reader<'_, schema::type_::Owned>, 82 | ) -> Result>> { 83 | let mut types = Vec::with_capacity(reader.len() as usize); 84 | for reader in reader.into_iter() { 85 | types.push(self.type_(&reader)?); 86 | } 87 | Ok(types) 88 | } 89 | 90 | fn type_(&mut self, reader: &schema::type_::Reader<'_>) -> Result> { 91 | use schema::type_::Which; 92 | match reader.which()? { 93 | Which::App(reader) => self.type_app(&reader), 94 | Which::Fn(reader) => self.type_fn(&reader), 95 | Which::Tuple(reader) => self.type_tuple(&reader), 96 | Which::Var(reader) => self.type_var(&reader), 97 | } 98 | } 99 | 100 | fn type_app(&mut self, reader: &schema::type_::app::Reader<'_>) -> Result> { 101 | let module = name(&reader.get_module()?)?; 102 | let name = reader.get_name()?.to_string(); 103 | let args = self.types(&reader.get_parameters()?)?; 104 | Ok(Arc::new(Type::App { 105 | public: true, 106 | module, 107 | name, 108 | args, 109 | })) 110 | } 111 | 112 | fn type_fn(&mut self, reader: &schema::type_::fn_::Reader<'_>) -> Result> { 113 | let retrn = self.type_(&reader.get_return()?)?; 114 | let args = self.types(&reader.get_arguments()?)?; 115 | Ok(Arc::new(Type::Fn { args, retrn })) 116 | } 117 | 118 | fn type_tuple(&mut self, reader: &schema::type_::tuple::Reader<'_>) -> Result> { 119 | let elems = self.types(&reader.get_elements()?)?; 120 | Ok(Arc::new(Type::Tuple { elems })) 121 | } 122 | 123 | fn type_var(&mut self, reader: &schema::type_::var::Reader<'_>) -> Result> { 124 | let serialized_id = reader.get_id() as usize; 125 | let id = match self.type_var_id_map.get(&serialized_id) { 126 | Some(id) => *id, 127 | None => { 128 | let new_id = self.next_type_var_id; 129 | self.next_type_var_id += 1; 130 | let _ = self.type_var_id_map.insert(serialized_id, new_id); 131 | new_id 132 | } 133 | }; 134 | Ok(typ::generic_var(id)) 135 | } 136 | 137 | fn module_values( 138 | &self, 139 | reader: &schema::module::Reader<'_>, 140 | ) -> Result> { 141 | // TODO 142 | Ok(HashMap::new()) 143 | } 144 | 145 | fn module_accessors( 146 | &self, 147 | reader: &schema::module::Reader<'_>, 148 | ) -> Result> { 149 | // TODO 150 | Ok(HashMap::new()) 151 | } 152 | } 153 | 154 | fn name(module: &capnp::text_list::Reader<'_>) -> Result> { 155 | Ok(module 156 | .iter() 157 | .map(|s| s.map(String::from)) 158 | .collect::>()?) 159 | } 160 | -------------------------------------------------------------------------------- /src/pretty/tests.rs: -------------------------------------------------------------------------------- 1 | use super::Document::*; 2 | use super::Mode::*; 3 | use super::*; 4 | 5 | #[test] 6 | fn fits_test() { 7 | // Negative limits never fit 8 | assert!(!fits(-1, vector![])); 9 | 10 | // If no more documents it always fits 11 | assert!(fits(0, vector![])); 12 | 13 | // ForceBreak never fits 14 | assert!(!fits(100, vector![(0, Unbroken, ForceBreak)])); 15 | assert!(!fits(100, vector![(0, Broken, ForceBreak)])); 16 | 17 | // Break in Broken fits always 18 | assert!(fits( 19 | 1, 20 | vector![( 21 | 0, 22 | Broken, 23 | Break { 24 | broken: "12", 25 | unbroken: "", 26 | } 27 | )] 28 | )); 29 | 30 | // Break in Unbroken mode fits if `unbroken` fits 31 | assert!(fits( 32 | 3, 33 | vector![( 34 | 0, 35 | Unbroken, 36 | Break { 37 | broken: "", 38 | unbroken: "123", 39 | } 40 | )] 41 | )); 42 | assert!(!fits( 43 | 2, 44 | vector![( 45 | 0, 46 | Unbroken, 47 | Break { 48 | broken: "", 49 | unbroken: "123", 50 | } 51 | )] 52 | )); 53 | 54 | // Line always fits 55 | assert!(fits(0, vector![(0, Broken, Line(100))])); 56 | assert!(fits(0, vector![(0, Unbroken, Line(100))])); 57 | 58 | // String fits if smaller than limit 59 | assert!(fits(5, vector![(0, Broken, String("Hello".to_string()))])); 60 | assert!(fits(5, vector![(0, Unbroken, String("Hello".to_string()))])); 61 | assert!(!fits(4, vector![(0, Broken, String("Hello".to_string()))])); 62 | assert!(!fits( 63 | 4, 64 | vector![(0, Unbroken, String("Hello".to_string()))] 65 | )); 66 | 67 | // Cons fits if combined smaller than limit 68 | assert!(fits( 69 | 2, 70 | vector![( 71 | 0, 72 | Broken, 73 | String("1".to_string()).append(String("2".to_string())) 74 | )] 75 | )); 76 | assert!(fits( 77 | 2, 78 | vector![( 79 | 0, 80 | Unbroken, 81 | String("1".to_string()).append(String("2".to_string())) 82 | )] 83 | )); 84 | assert!(!fits( 85 | 1, 86 | vector![( 87 | 0, 88 | Broken, 89 | String("1".to_string()).append(String("2".to_string())) 90 | )] 91 | )); 92 | assert!(!fits( 93 | 1, 94 | vector![( 95 | 0, 96 | Unbroken, 97 | String("1".to_string()).append(String("2".to_string())) 98 | )] 99 | )); 100 | 101 | // Nest fits if combined smaller than limit 102 | assert!(fits( 103 | 2, 104 | vector![(0, Broken, Nest(1, Box::new(String("12".to_string())),))] 105 | )); 106 | assert!(fits( 107 | 2, 108 | vector![(0, Unbroken, Nest(1, Box::new(String("12".to_string())),))] 109 | )); 110 | assert!(!fits( 111 | 1, 112 | vector![(0, Broken, Nest(1, Box::new(String("12".to_string())),))] 113 | )); 114 | assert!(!fits( 115 | 1, 116 | vector![(0, Unbroken, Nest(1, Box::new(String("12".to_string()))))] 117 | )); 118 | 119 | // Nest fits if combined smaller than limit 120 | assert!(fits( 121 | 2, 122 | vector![(0, Broken, NestCurrent(Box::new(String("12".to_string())),))] 123 | )); 124 | assert!(fits( 125 | 2, 126 | vector![( 127 | 0, 128 | Unbroken, 129 | NestCurrent(Box::new(String("12".to_string())),) 130 | )] 131 | )); 132 | assert!(!fits( 133 | 1, 134 | vector![(0, Broken, NestCurrent(Box::new(String("12".to_string())),))] 135 | )); 136 | assert!(!fits( 137 | 1, 138 | vector![(0, Unbroken, NestCurrent(Box::new(String("12".to_string()))))] 139 | )); 140 | } 141 | 142 | #[test] 143 | fn format_test() { 144 | let doc = String("Hi".to_string()); 145 | assert_eq!("Hi".to_string(), doc.to_pretty_string(10)); 146 | 147 | let doc = String("Hi".to_string()).append(String(", world!".to_string())); 148 | assert_eq!("Hi, world!".to_string(), doc.to_pretty_string(10)); 149 | 150 | let doc = Nil; 151 | assert_eq!("".to_string(), doc.to_pretty_string(10)); 152 | 153 | let doc = Break { 154 | broken: "broken", 155 | unbroken: "unbroken", 156 | } 157 | .group(); 158 | assert_eq!("unbroken".to_string(), doc.to_pretty_string(10)); 159 | 160 | let doc = Break { 161 | broken: "broken", 162 | unbroken: "unbroken", 163 | } 164 | .group(); 165 | assert_eq!("broken\n".to_string(), doc.to_pretty_string(5)); 166 | 167 | let doc = Nest( 168 | 2, 169 | Box::new(String("1".to_string()).append(Line(1).append(String("2".to_string())))), 170 | ); 171 | assert_eq!("1\n 2".to_string(), doc.to_pretty_string(1)); 172 | 173 | let doc = String("111".to_string()).append(NestCurrent(Box::new( 174 | Line(1).append(String("2".to_string())), 175 | ))); 176 | assert_eq!("111\n 2".to_string(), doc.to_pretty_string(1)); 177 | 178 | let doc = ForceBreak.append(Break { 179 | broken: "broken", 180 | unbroken: "unbroken", 181 | }); 182 | assert_eq!("broken\n".to_string(), doc.to_pretty_string(100)); 183 | } 184 | 185 | #[test] 186 | fn let_left_side_fits_test() { 187 | let elems = break_("", "").append("1").nest(2).append(break_("", "")); 188 | let list = "[".to_doc().append(elems).append("]").group(); 189 | let doc = list.clone().append(" = ").append(list); 190 | 191 | assert_eq!( 192 | "[1] = [ 193 | 1 194 | ]", 195 | doc.clone().to_pretty_string(7) 196 | ); 197 | 198 | assert_eq!( 199 | "[ 200 | 1 201 | ] = [ 202 | 1 203 | ]", 204 | doc.clone().to_pretty_string(2) 205 | ); 206 | 207 | assert_eq!("[1] = [1]", doc.clone().to_pretty_string(16)); 208 | } 209 | -------------------------------------------------------------------------------- /templates/gleam.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | window.Gleam = function() { 4 | /* Global Object */ 5 | const self = {}; 6 | 7 | /* Public Properties */ 8 | 9 | self.hashOffset = undefined; 10 | 11 | /* Public Methods */ 12 | 13 | self.getProperty = function(property) { 14 | let value; 15 | try { 16 | value = localStorage.getItem(`Gleam.${property}`); 17 | } 18 | catch (_error) {} 19 | if (-1 < [null, undefined].indexOf(value)) { 20 | return gleamConfig[property].values[0].value; 21 | } 22 | return value; 23 | }; 24 | 25 | self.icons = function() { 26 | return Array.from(arguments).reduce( 27 | (acc, name) => 28 | `${acc} 29 | `, 30 | "" 31 | ); 32 | } 33 | 34 | self.scrollToHash = function() { 35 | const locationHash = arguments[0] || window.location.hash; 36 | const query = locationHash ? locationHash : "body"; 37 | const hashTop = document.querySelector(query).offsetTop; 38 | window.scrollTo(0, hashTop - self.hashOffset); 39 | return locationHash; 40 | }; 41 | 42 | self.toggleSidebar = function() { 43 | const previousState = 44 | bodyClasses.contains("drawer-open") ? "open" : "closed"; 45 | 46 | let state; 47 | if (0 < arguments.length) { 48 | state = false === arguments[0] ? "closed" : "open"; 49 | } 50 | else { 51 | state = "open" === previousState ? "closed" : "open"; 52 | } 53 | 54 | bodyClasses.remove(`drawer-${previousState}`); 55 | bodyClasses.add(`drawer-${state}`); 56 | 57 | if ("open" === state) { 58 | document.addEventListener("click", closeSidebar, false); 59 | } 60 | }; 61 | 62 | /* Private Properties */ 63 | 64 | const html = document.documentElement; 65 | const body = document.body; 66 | const bodyClasses = body.classList; 67 | const sidebar = document.querySelector(".sidebar"); 68 | const sidebarToggles = document.querySelectorAll(".sidebar-toggle"); 69 | const displayControls = document.createElement("div"); 70 | 71 | displayControls.classList.add("display-controls"); 72 | sidebar.appendChild(displayControls); 73 | 74 | /* Private Methods */ 75 | 76 | const initProperty = function(property) { 77 | const config = gleamConfig[property]; 78 | 79 | displayControls.insertAdjacentHTML( 80 | "beforeend", 81 | config.values.reduce( 82 | (acc, item, index) => { 83 | const tooltip = 84 | item.label 85 | ? `alt="${item.label}" title="${item.label}"` 86 | : ""; 87 | let inner; 88 | if (item.icons) { 89 | inner = self.icons(...item.icons); 90 | } 91 | else if (item.label) { 92 | inner = item.label; 93 | } 94 | else { 95 | inner = ""; 96 | } 97 | return ` 98 | ${acc} 99 | 100 | ${inner} 101 | 102 | `; 103 | }, 104 | ` 110 | ` 111 | ); 112 | 113 | setProperty(null, property, function() { 114 | return self.getProperty(property); 115 | }); 116 | }; 117 | 118 | const setProperty = function(_event, property) { 119 | const previousValue = self.getProperty(property); 120 | 121 | const update = 122 | 2 < arguments.length ? arguments[2] : gleamConfig[property].update; 123 | const value = update(); 124 | 125 | try { 126 | localStorage.setItem("Gleam." + property, value); 127 | } 128 | catch (_error) {} 129 | 130 | bodyClasses.remove(`${property}-${previousValue}`); 131 | bodyClasses.add(`${property}-${value}`); 132 | 133 | const isDefault = value === gleamConfig[property].values[0].value; 134 | const toggleClasses = 135 | document.querySelector(`#${property}-toggle`).classList; 136 | toggleClasses.remove(`toggle-${isDefault ? 1 : 0}`); 137 | toggleClasses.add(`toggle-${isDefault ? 0 : 1}`); 138 | 139 | try { 140 | gleamConfig[property].callback(value); 141 | } 142 | catch(_error) {} 143 | 144 | return value; 145 | } 146 | 147 | const setHashOffset = function() { 148 | const el = document.createElement("div"); 149 | el.style.cssText = 150 | ` 151 | height: var(--hash-offset); 152 | pointer-events: none; 153 | position: absolute; 154 | visibility: hidden; 155 | width: 0; 156 | `; 157 | body.appendChild(el); 158 | self.hashOffset = parseInt( 159 | getComputedStyle(el).getPropertyValue("height") || "0" 160 | ); 161 | body.removeChild(el); 162 | }; 163 | 164 | const closeSidebar = function(event) { 165 | if (! event.target.closest(".sidebar-toggle")) { 166 | document.removeEventListener("click", closeSidebar, false); 167 | self.toggleSidebar(false); 168 | } 169 | }; 170 | 171 | const init = function() { 172 | for (const property in gleamConfig) { 173 | initProperty(property); 174 | const toggle = document.querySelector(`#${property}-toggle`); 175 | toggle.addEventListener("click", function(event) { 176 | setProperty(event, property); 177 | }); 178 | } 179 | 180 | sidebarToggles.forEach(function(sidebarToggle) { 181 | sidebarToggle.addEventListener("click", function(event) { 182 | event.preventDefault(); 183 | self.toggleSidebar(); 184 | }); 185 | }); 186 | 187 | setHashOffset(); 188 | window.addEventListener("load", function(_event) { 189 | self.scrollToHash(); 190 | }); 191 | window.addEventListener("hashchange", function(_event) { 192 | self.scrollToHash(); 193 | }); 194 | 195 | document.querySelectorAll(` 196 | .module-name > a, 197 | .member-name a[href^='#'] 198 | `).forEach(function(title) { 199 | title.innerHTML = 200 | title.innerHTML.replace(/([A-Z])|([_/])/g, "$2$1"); 201 | }); 202 | }; 203 | 204 | /* Initialise */ 205 | 206 | init(); 207 | 208 | return self; 209 | }(); 210 | -------------------------------------------------------------------------------- /src/project/source_tree.rs: -------------------------------------------------------------------------------- 1 | use super::{GleamExpect, Input, Module, ModuleOrigin}; 2 | use crate::error::Error; 3 | use petgraph::{algo::Cycle, graph::NodeIndex, Direction}; 4 | use std::collections::{HashMap, HashSet}; 5 | 6 | #[derive(Debug, Default)] 7 | pub struct SourceTree { 8 | graph: petgraph::Graph, 9 | indexes: HashMap, 10 | modules: HashMap, 11 | } 12 | 13 | impl SourceTree { 14 | pub fn new(inputs: Vec) -> Result { 15 | let mut graph: Self = Default::default(); 16 | for input in inputs.into_iter() { 17 | graph.insert(input)?; 18 | } 19 | graph.calculate_dependencies()?; 20 | Ok(graph) 21 | } 22 | 23 | pub fn consume(&mut self) -> Result + '_, Error> { 24 | let iter = petgraph::algo::toposort(&self.graph, None) 25 | .map_err(|e| self.import_cycle(e))? 26 | .into_iter() 27 | .map(move |i| { 28 | self.modules 29 | .remove(&i) 30 | .gleam_expect("SourceTree.consume(): Unknown graph index") 31 | }); 32 | Ok(iter) 33 | } 34 | 35 | fn import_cycle(&mut self, cycle: Cycle) -> Error { 36 | let origin = cycle.node_id(); 37 | let mut path = vec![]; 38 | let _ = self.find_cycle(origin, origin, &mut path, &mut HashSet::new()); 39 | let modules: Vec<_> = path 40 | .iter() 41 | .map(|index| { 42 | self.modules 43 | .remove(index) 44 | .gleam_expect("SourceTree.import_cycle(): cannot find module for index") 45 | .module 46 | .name 47 | .join("/") 48 | }) 49 | .collect(); 50 | Error::ImportCycle { modules } 51 | } 52 | 53 | fn find_cycle( 54 | &self, 55 | origin: NodeIndex, 56 | parent: NodeIndex, 57 | path: &mut Vec, 58 | seen: &mut HashSet, 59 | ) -> bool { 60 | let _ = seen.insert(parent); 61 | for node in self.graph.neighbors_directed(parent, Direction::Outgoing) { 62 | if node == origin { 63 | path.push(node); 64 | return true; 65 | } 66 | if seen.contains(&node) { 67 | continue; 68 | } 69 | if self.find_cycle(origin, node, path, seen) { 70 | path.push(node); 71 | return true; 72 | } 73 | } 74 | false 75 | } 76 | 77 | fn calculate_dependencies(&mut self) -> Result<(), Error> { 78 | for module in self.modules.values() { 79 | let module_name = module.module.name_string(); 80 | let src = module.src.clone(); 81 | let path = module.path.clone(); 82 | let deps = module.module.dependencies(); 83 | let module_index = self.indexes.get(&module_name).gleam_expect( 84 | "SourceTree.calculate_dependencies(): Unable to find module index for name", 85 | ); 86 | let module = self.modules.get(module_index).gleam_expect( 87 | "SourceTree.calculate_dependencies(): Unable to find module for index", 88 | ); 89 | 90 | for (dep, location) in deps { 91 | let dep_index = self.indexes.get(&dep).ok_or_else(|| Error::UnknownImport { 92 | module: module_name.clone(), 93 | import: dep.clone(), 94 | src: src.clone(), 95 | path: path.clone(), 96 | modules: self 97 | .modules 98 | .values() 99 | .map(|m| m.module.name_string()) 100 | .collect(), 101 | location, 102 | })?; 103 | 104 | if module.origin == ModuleOrigin::Src 105 | && self 106 | .modules 107 | .get(dep_index) 108 | .gleam_expect("SourceTree.calculate_dependencies(): Unable to find module for dep index") 109 | .origin 110 | == ModuleOrigin::Test 111 | { 112 | return Err(Error::SrcImportingTest { 113 | path: path.clone(), 114 | src: src.clone(), 115 | location, 116 | src_module: module_name, 117 | test_module: dep, 118 | }); 119 | } 120 | 121 | let _ = self.graph.add_edge(*dep_index, *module_index, ()); 122 | } 123 | } 124 | Ok(()) 125 | } 126 | 127 | fn insert(&mut self, input: Input) -> Result<(), Error> { 128 | // Determine the module name 129 | let name = input 130 | .path 131 | .strip_prefix(&input.source_base_path) 132 | .unwrap() 133 | .parent() 134 | .unwrap() 135 | .join(input.path.file_stem().unwrap()) 136 | .to_str() 137 | .unwrap() 138 | .to_string() 139 | .replace("\\", "/"); 140 | 141 | // Parse the source 142 | let (mut module, module_extra) = 143 | crate::parse::parse_module(&input.src).map_err(|e| Error::Parse { 144 | path: input.path.clone(), 145 | src: input.src.clone(), 146 | error: e, 147 | })?; 148 | 149 | // Store the name 150 | module.name = name.split('/').map(|s| s.to_string()).collect(); 151 | 152 | // Check to see if we already have a module with this name 153 | if let Some(Module { path, .. }) = self.indexes.get(&name).and_then(|i| self.modules.get(i)) 154 | { 155 | return Err(Error::DuplicateModule { 156 | module: name.clone(), 157 | first: path.clone(), 158 | second: input.path, 159 | }); 160 | } 161 | 162 | // Register the module 163 | let index = self.graph.add_node(name.clone()); 164 | let _ = self.indexes.insert(name, index); 165 | let _ = self.modules.insert( 166 | index, 167 | Module { 168 | src: input.src, 169 | path: input.path, 170 | origin: input.origin, 171 | source_base_path: input.source_base_path, 172 | module, 173 | module_extra, 174 | }, 175 | ); 176 | Ok(()) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/ast/typed.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::typ::{HasType, Type}; 3 | 4 | #[derive(Debug, PartialEq, Clone)] 5 | pub enum TypedExpr { 6 | Int { 7 | location: SrcSpan, 8 | typ: Arc, 9 | value: String, 10 | }, 11 | 12 | Float { 13 | location: SrcSpan, 14 | typ: Arc, 15 | value: String, 16 | }, 17 | 18 | String { 19 | location: SrcSpan, 20 | typ: Arc, 21 | value: String, 22 | }, 23 | 24 | Seq { 25 | typ: Arc, 26 | first: Box, 27 | then: Box, 28 | }, 29 | 30 | Var { 31 | location: SrcSpan, 32 | constructor: ValueConstructor, 33 | name: String, 34 | }, 35 | 36 | Fn { 37 | location: SrcSpan, 38 | typ: Arc, 39 | is_capture: bool, 40 | args: Vec>>, 41 | body: Box, 42 | return_annotation: Option, 43 | }, 44 | 45 | ListNil { 46 | location: SrcSpan, 47 | typ: Arc, 48 | }, 49 | 50 | ListCons { 51 | location: SrcSpan, 52 | typ: Arc, 53 | head: Box, 54 | tail: Box, 55 | }, 56 | 57 | Call { 58 | location: SrcSpan, 59 | typ: Arc, 60 | fun: Box, 61 | args: Vec>, 62 | }, 63 | 64 | BinOp { 65 | location: SrcSpan, 66 | typ: Arc, 67 | name: BinOp, 68 | left: Box, 69 | right: Box, 70 | }, 71 | 72 | Pipe { 73 | location: SrcSpan, 74 | typ: Arc, 75 | left: Box, 76 | right: Box, 77 | }, 78 | 79 | Let { 80 | location: SrcSpan, 81 | typ: Arc, 82 | value: Box, 83 | pattern: Pattern>, 84 | then: Box, 85 | kind: BindingKind, 86 | }, 87 | 88 | Case { 89 | location: SrcSpan, 90 | typ: Arc, 91 | subjects: Vec, 92 | clauses: Vec, String>>, 93 | }, 94 | 95 | RecordAccess { 96 | location: SrcSpan, 97 | typ: Arc, 98 | label: String, 99 | index: u64, 100 | record: Box, 101 | }, 102 | 103 | ModuleSelect { 104 | location: SrcSpan, 105 | typ: Arc, 106 | label: String, 107 | module_name: Vec, 108 | module_alias: String, 109 | constructor: ModuleValueConstructor, 110 | }, 111 | 112 | Tuple { 113 | location: SrcSpan, 114 | typ: Arc, 115 | elems: Vec, 116 | }, 117 | 118 | TupleIndex { 119 | location: SrcSpan, 120 | typ: Arc, 121 | index: u64, 122 | tuple: Box, 123 | }, 124 | 125 | Todo { 126 | location: SrcSpan, 127 | label: Option, 128 | typ: Arc, 129 | }, 130 | 131 | BitString { 132 | location: SrcSpan, 133 | typ: Arc, 134 | segments: Vec, 135 | }, 136 | 137 | RecordUpdate { 138 | location: SrcSpan, 139 | typ: Arc, 140 | spread: Box, 141 | args: Vec, 142 | }, 143 | } 144 | 145 | impl TypedExpr { 146 | pub fn non_zero_compile_time_number(&self) -> bool { 147 | use regex::Regex; 148 | lazy_static! { 149 | static ref NON_ZERO: Regex = Regex::new(r"[1-9]").unwrap(); 150 | } 151 | match self { 152 | Self::Int { value, .. } | Self::Float { value, .. } => NON_ZERO.is_match(value), 153 | _ => false, 154 | } 155 | } 156 | 157 | pub fn location(&self) -> SrcSpan { 158 | match self { 159 | Self::Let { then, .. } | Self::Seq { then, .. } => then.location(), 160 | Self::Fn { location, .. } 161 | | Self::Int { location, .. } 162 | | Self::Var { location, .. } 163 | | Self::Todo { location, .. } 164 | | Self::Case { location, .. } 165 | | Self::Call { location, .. } 166 | | Self::Pipe { location, .. } 167 | | Self::Float { location, .. } 168 | | Self::BinOp { location, .. } 169 | | Self::Tuple { location, .. } 170 | | Self::String { location, .. } 171 | | Self::ListNil { location, .. } 172 | | Self::ListCons { location, .. } 173 | | Self::TupleIndex { location, .. } 174 | | Self::ModuleSelect { location, .. } 175 | | Self::RecordAccess { location, .. } 176 | | Self::BitString { location, .. } 177 | | Self::RecordUpdate { location, .. } => *location, 178 | } 179 | } 180 | 181 | pub fn try_binding_location(&self) -> SrcSpan { 182 | match self { 183 | Self::Let { 184 | kind: BindingKind::Try, 185 | location, 186 | .. 187 | } 188 | | Self::Fn { location, .. } 189 | | Self::Int { location, .. } 190 | | Self::Var { location, .. } 191 | | Self::Todo { location, .. } 192 | | Self::Case { location, .. } 193 | | Self::Call { location, .. } 194 | | Self::Pipe { location, .. } 195 | | Self::Float { location, .. } 196 | | Self::BinOp { location, .. } 197 | | Self::Tuple { location, .. } 198 | | Self::String { location, .. } 199 | | Self::ListNil { location, .. } 200 | | Self::ListCons { location, .. } 201 | | Self::TupleIndex { location, .. } 202 | | Self::ModuleSelect { location, .. } 203 | | Self::RecordAccess { location, .. } 204 | | Self::BitString { location, .. } 205 | | Self::RecordUpdate { location, .. } => *location, 206 | 207 | Self::Let { then, .. } | Self::Seq { then, .. } => then.try_binding_location(), 208 | } 209 | } 210 | } 211 | 212 | impl HasLocation for TypedExpr { 213 | fn location(&self) -> SrcSpan { 214 | self.location() 215 | } 216 | } 217 | 218 | impl TypedExpr { 219 | fn typ(&self) -> Arc { 220 | match self { 221 | Self::Fn { typ, .. } => typ.clone(), 222 | Self::ListNil { typ, .. } => typ.clone(), 223 | Self::Let { typ, .. } => typ.clone(), 224 | Self::Int { typ, .. } => typ.clone(), 225 | Self::Seq { then, .. } => then.typ(), 226 | Self::Todo { typ, .. } => typ.clone(), 227 | Self::Case { typ, .. } => typ.clone(), 228 | Self::ListCons { typ, .. } => typ.clone(), 229 | Self::Call { typ, .. } => typ.clone(), 230 | Self::Pipe { typ, .. } => typ.clone(), 231 | Self::Float { typ, .. } => typ.clone(), 232 | Self::BinOp { typ, .. } => typ.clone(), 233 | Self::Tuple { typ, .. } => typ.clone(), 234 | Self::String { typ, .. } => typ.clone(), 235 | Self::TupleIndex { typ, .. } => typ.clone(), 236 | Self::Var { constructor, .. } => constructor.typ.clone(), 237 | Self::ModuleSelect { typ, .. } => typ.clone(), 238 | Self::RecordAccess { typ, .. } => typ.clone(), 239 | Self::BitString { typ, .. } => typ.clone(), 240 | Self::RecordUpdate { typ, .. } => typ.clone(), 241 | } 242 | } 243 | } 244 | 245 | impl HasType for TypedExpr { 246 | fn typ(&self) -> Arc { 247 | self.typ() 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/typ/hydrator.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::ast::TypeAst; 3 | use std::sync::Arc; 4 | 5 | /// The Hydrator takes an AST representing a type (i.e. a type annotation 6 | /// for a function argument) and returns a Type for that annotation. 7 | /// 8 | /// If a valid Type cannot be constructed it returns an error. 9 | /// 10 | /// It keeps track of any type variables created. This is useful for: 11 | /// 12 | /// - Determining if a generic type variable should be made into an 13 | /// unbound type varable during type instantiation. 14 | /// - Ensuring that the same type is constructed if the programmer 15 | /// uses the same name for a type variable multiple times. 16 | /// 17 | #[derive(Debug)] 18 | pub struct Hydrator { 19 | created_type_variables: im::HashMap>, 20 | created_type_variable_ids: im::HashSet, 21 | permit_new_type_variables: bool, 22 | permit_holes: bool, 23 | } 24 | 25 | #[derive(Debug)] 26 | pub struct ScopeResetData { 27 | created_type_variables: im::HashMap>, 28 | created_type_variable_ids: im::HashSet, 29 | } 30 | 31 | impl Hydrator { 32 | pub fn new() -> Self { 33 | Self { 34 | created_type_variables: im::hashmap![], 35 | created_type_variable_ids: im::hashset![], 36 | permit_new_type_variables: true, 37 | permit_holes: false, 38 | } 39 | } 40 | 41 | pub fn open_new_scope(&mut self) -> ScopeResetData { 42 | let created_type_variables = self.created_type_variables.clone(); 43 | let created_type_variable_ids = self.created_type_variable_ids.clone(); 44 | ScopeResetData { 45 | created_type_variables, 46 | created_type_variable_ids, 47 | } 48 | } 49 | 50 | pub fn close_scope(&mut self, data: ScopeResetData) { 51 | self.created_type_variables = data.created_type_variables; 52 | self.created_type_variable_ids = data.created_type_variable_ids; 53 | } 54 | 55 | pub fn disallow_new_type_variables(&mut self) { 56 | self.permit_new_type_variables = false 57 | } 58 | 59 | pub fn permit_holes(&mut self, flag: bool) { 60 | self.permit_holes = flag 61 | } 62 | 63 | pub fn is_created_generic_type(&self, id: &usize) -> bool { 64 | self.created_type_variable_ids.contains(id) 65 | } 66 | 67 | pub fn type_from_option_ast<'a, 'b>( 68 | &mut self, 69 | ast: &Option, 70 | environment: &mut Environment<'a, 'b>, 71 | ) -> Result, Error> { 72 | match ast { 73 | Some(ast) => self.type_from_ast(ast, environment), 74 | None => Ok(environment.new_unbound_var(environment.level)), 75 | } 76 | } 77 | 78 | /// Construct a Type from an AST Type annotation. 79 | /// 80 | pub fn type_from_ast<'a, 'b>( 81 | &mut self, 82 | ast: &TypeAst, 83 | environment: &mut Environment<'a, 'b>, 84 | ) -> Result, Error> { 85 | match ast { 86 | TypeAst::Constructor { 87 | location, 88 | module, 89 | name, 90 | args, 91 | } => { 92 | // Hydrate the type argument AST into types 93 | let mut argument_types = Vec::with_capacity(args.len()); 94 | for t in args { 95 | let typ = self.type_from_ast(t, environment)?; 96 | argument_types.push((t.location(), typ)); 97 | } 98 | 99 | // Look up the constructor 100 | let TypeConstructor { 101 | parameters, 102 | typ: return_type, 103 | .. 104 | } = environment 105 | .get_type_constructor(module, name) 106 | .map_err(|e| convert_get_type_constructor_error(e, location))? 107 | .clone(); 108 | 109 | // Register the type constructor as being used if it is unqualifed. 110 | // We do not track use of qualified type constructors as they may be 111 | // used in another module. 112 | if module.is_none() { 113 | environment.increment_usage(name.as_str()); 114 | } 115 | 116 | // Ensure that the correct number of arguments have been given to the constructor 117 | if args.len() != parameters.len() { 118 | return Err(Error::IncorrectTypeArity { 119 | location: *location, 120 | name: name.to_string(), 121 | expected: parameters.len(), 122 | given: args.len(), 123 | }); 124 | } 125 | 126 | // Instantiate the constructor type for this specific usage 127 | let mut type_vars = hashmap![]; 128 | let mut parameter_types = Vec::with_capacity(parameters.len()); 129 | for typ in parameters { 130 | let t = environment.instantiate(typ, 0, &mut type_vars, self); 131 | 132 | parameter_types.push(t); 133 | } 134 | let return_type = environment.instantiate(return_type, 0, &mut type_vars, self); 135 | 136 | // Unify argument types with instantiated parameter types so that the correct types 137 | // are inserted into the return type 138 | for (parameter, (location, argument)) in 139 | parameter_types.iter().zip(argument_types.iter()) 140 | { 141 | environment 142 | .unify(parameter.clone(), argument.clone()) 143 | .map_err(|e| convert_unify_error(e, *location))?; 144 | } 145 | 146 | Ok(return_type) 147 | } 148 | 149 | TypeAst::Tuple { elems, .. } => Ok(tuple( 150 | elems 151 | .iter() 152 | .map(|t| self.type_from_ast(t, environment)) 153 | .collect::>()?, 154 | )), 155 | 156 | TypeAst::Fn { args, retrn, .. } => { 157 | let args = args 158 | .iter() 159 | .map(|t| self.type_from_ast(t, environment)) 160 | .collect::>()?; 161 | let retrn = self.type_from_ast(retrn, environment)?; 162 | Ok(fn_(args, retrn)) 163 | } 164 | 165 | TypeAst::Var { name, location, .. } => { 166 | match self.created_type_variables.get(name.as_str()) { 167 | Some(var) => Ok(var.clone()), 168 | 169 | None if self.permit_new_type_variables => { 170 | let var = environment.new_generic_var(); 171 | let _ = self 172 | .created_type_variable_ids 173 | .insert(environment.previous_uid()); 174 | let _ = self 175 | .created_type_variables 176 | .insert(name.clone(), var.clone()); 177 | Ok(var) 178 | } 179 | 180 | None => Err(Error::UnknownType { 181 | name: name.to_string(), 182 | location: *location, 183 | types: environment 184 | .module_types 185 | .keys() 186 | .map(|t| t.to_string()) 187 | .collect(), 188 | }), 189 | } 190 | } 191 | 192 | TypeAst::Hole { .. } if self.permit_holes => { 193 | Ok(environment.new_unbound_var(environment.level)) 194 | } 195 | 196 | TypeAst::Hole { location, .. } => Err(Error::UnexpectedTypeHole { 197 | location: *location, 198 | }), 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/typ/prelude.rs: -------------------------------------------------------------------------------- 1 | use super::{Environment, Type, TypeConstructor, TypeVar, ValueConstructorVariant}; 2 | use crate::error::GleamExpect; 3 | use std::{cell::RefCell, sync::Arc}; 4 | 5 | pub fn int() -> Arc { 6 | Arc::new(Type::App { 7 | public: true, 8 | name: "Int".to_string(), 9 | module: vec![], 10 | args: vec![], 11 | }) 12 | } 13 | 14 | pub fn float() -> Arc { 15 | Arc::new(Type::App { 16 | args: vec![], 17 | public: true, 18 | name: "Float".to_string(), 19 | module: vec![], 20 | }) 21 | } 22 | 23 | pub fn bool() -> Arc { 24 | Arc::new(Type::App { 25 | args: vec![], 26 | public: true, 27 | name: "Bool".to_string(), 28 | module: vec![], 29 | }) 30 | } 31 | 32 | pub fn string() -> Arc { 33 | Arc::new(Type::App { 34 | args: vec![], 35 | public: true, 36 | name: "String".to_string(), 37 | module: vec![], 38 | }) 39 | } 40 | 41 | pub fn nil() -> Arc { 42 | Arc::new(Type::App { 43 | args: vec![], 44 | public: true, 45 | name: "Nil".to_string(), 46 | module: vec![], 47 | }) 48 | } 49 | 50 | pub fn list(t: Arc) -> Arc { 51 | Arc::new(Type::App { 52 | public: true, 53 | name: "List".to_string(), 54 | module: vec![], 55 | args: vec![t], 56 | }) 57 | } 58 | 59 | pub fn result(a: Arc, e: Arc) -> Arc { 60 | Arc::new(Type::App { 61 | public: true, 62 | name: "Result".to_string(), 63 | module: vec![], 64 | args: vec![a, e], 65 | }) 66 | } 67 | 68 | pub fn tuple(elems: Vec>) -> Arc { 69 | Arc::new(Type::Tuple { elems }) 70 | } 71 | 72 | pub fn fn_(args: Vec>, retrn: Arc) -> Arc { 73 | Arc::new(Type::Fn { retrn, args }) 74 | } 75 | 76 | pub fn bit_string() -> Arc { 77 | Arc::new(Type::App { 78 | args: vec![], 79 | public: true, 80 | name: "BitString".to_string(), 81 | module: vec![], 82 | }) 83 | } 84 | 85 | pub fn utf_codepoint() -> Arc { 86 | Arc::new(Type::App { 87 | args: vec![], 88 | public: true, 89 | name: "UtfCodepoint".to_string(), 90 | module: vec![], 91 | }) 92 | } 93 | 94 | pub fn generic_var(id: usize) -> Arc { 95 | Arc::new(Type::Var { 96 | typ: Arc::new(RefCell::new(TypeVar::Generic { id })), 97 | }) 98 | } 99 | 100 | pub fn unbound_var(id: usize, level: usize) -> Arc { 101 | Arc::new(Type::Var { 102 | typ: Arc::new(RefCell::new(TypeVar::Unbound { id, level })), 103 | }) 104 | } 105 | 106 | pub fn register_prelude<'a, 'b>(mut typer: Environment<'a, 'b>) -> Environment<'a, 'b> { 107 | typer 108 | .insert_type_constructor( 109 | "Int".to_string(), 110 | TypeConstructor { 111 | parameters: vec![], 112 | typ: int(), 113 | origin: Default::default(), 114 | module: vec![], 115 | public: true, 116 | }, 117 | ) 118 | .gleam_expect("prelude inserting Int type"); 119 | 120 | typer.insert_variable( 121 | "True".to_string(), 122 | ValueConstructorVariant::Record { 123 | name: "True".to_string(), 124 | field_map: None, 125 | arity: 0, 126 | }, 127 | bool(), 128 | Default::default(), 129 | ); 130 | typer.insert_variable( 131 | "False".to_string(), 132 | ValueConstructorVariant::Record { 133 | name: "False".to_string(), 134 | field_map: None, 135 | arity: 0, 136 | }, 137 | bool(), 138 | Default::default(), 139 | ); 140 | typer 141 | .insert_type_constructor( 142 | "Bool".to_string(), 143 | TypeConstructor { 144 | origin: Default::default(), 145 | parameters: vec![], 146 | typ: bool(), 147 | module: vec![], 148 | public: true, 149 | }, 150 | ) 151 | .gleam_expect("prelude inserting Bool type"); 152 | 153 | let list_parameter = typer.new_generic_var(); 154 | typer 155 | .insert_type_constructor( 156 | "List".to_string(), 157 | TypeConstructor { 158 | origin: Default::default(), 159 | parameters: vec![list_parameter.clone()], 160 | typ: list(list_parameter), 161 | module: vec![], 162 | public: true, 163 | }, 164 | ) 165 | .gleam_expect("prelude inserting List type"); 166 | 167 | typer 168 | .insert_type_constructor( 169 | "Float".to_string(), 170 | TypeConstructor { 171 | origin: Default::default(), 172 | parameters: vec![], 173 | typ: float(), 174 | module: vec![], 175 | public: true, 176 | }, 177 | ) 178 | .gleam_expect("prelude inserting Float type"); 179 | 180 | typer 181 | .insert_type_constructor( 182 | "String".to_string(), 183 | TypeConstructor { 184 | origin: Default::default(), 185 | parameters: vec![], 186 | typ: string(), 187 | module: vec![], 188 | public: true, 189 | }, 190 | ) 191 | .gleam_expect("prelude inserting String type"); 192 | 193 | let result_value = typer.new_generic_var(); 194 | let result_error = typer.new_generic_var(); 195 | typer 196 | .insert_type_constructor( 197 | "Result".to_string(), 198 | TypeConstructor { 199 | origin: Default::default(), 200 | parameters: vec![result_value.clone(), result_error.clone()], 201 | typ: result(result_value, result_error), 202 | module: vec![], 203 | public: true, 204 | }, 205 | ) 206 | .gleam_expect("prelude inserting Result type"); 207 | 208 | typer.insert_variable( 209 | "Nil".to_string(), 210 | ValueConstructorVariant::Record { 211 | name: "Nil".to_string(), 212 | arity: 0, 213 | field_map: None, 214 | }, 215 | nil(), 216 | Default::default(), 217 | ); 218 | typer 219 | .insert_type_constructor( 220 | "Nil".to_string(), 221 | TypeConstructor { 222 | origin: Default::default(), 223 | parameters: vec![], 224 | typ: nil(), 225 | module: vec![], 226 | public: true, 227 | }, 228 | ) 229 | .gleam_expect("prelude inserting Nil type"); 230 | 231 | typer 232 | .insert_type_constructor( 233 | "BitString".to_string(), 234 | TypeConstructor { 235 | origin: Default::default(), 236 | parameters: vec![], 237 | typ: bit_string(), 238 | module: vec![], 239 | public: true, 240 | }, 241 | ) 242 | .gleam_expect("prelude inserting BitString type"); 243 | 244 | typer 245 | .insert_type_constructor( 246 | "UtfCodepoint".to_string(), 247 | TypeConstructor { 248 | origin: Default::default(), 249 | parameters: vec![], 250 | typ: utf_codepoint(), 251 | module: vec![], 252 | public: true, 253 | }, 254 | ) 255 | .gleam_expect("prelude inserting UTF Codepoint type"); 256 | 257 | let ok = typer.new_generic_var(); 258 | let error = typer.new_generic_var(); 259 | typer.insert_variable( 260 | "Ok".to_string(), 261 | ValueConstructorVariant::Record { 262 | name: "Ok".to_string(), 263 | field_map: None, 264 | arity: 1, 265 | }, 266 | fn_(vec![ok.clone()], result(ok, error)), 267 | Default::default(), 268 | ); 269 | 270 | let ok = typer.new_generic_var(); 271 | let error = typer.new_generic_var(); 272 | typer.insert_variable( 273 | "Error".to_string(), 274 | ValueConstructorVariant::Record { 275 | name: "Error".to_string(), 276 | field_map: None, 277 | arity: 1, 278 | }, 279 | fn_(vec![error.clone()], result(ok, error)), 280 | Default::default(), 281 | ); 282 | 283 | typer 284 | } 285 | -------------------------------------------------------------------------------- /src/build/package_compiler.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | ast::{SrcSpan, TypedModule, UntypedModule}, 3 | build::{dep_tree, project_root::ProjectRoot, Module, Origin, Package}, 4 | codegen::Erlang, 5 | config::PackageConfig, 6 | error, 7 | fs::FileSystemWriter, 8 | typ, Error, GleamExpect, Result, Warning, 9 | }; 10 | use std::collections::HashMap; 11 | use std::path::{Path, PathBuf}; 12 | 13 | #[derive(Debug)] 14 | pub struct Options { 15 | pub name: String, 16 | pub src_path: PathBuf, 17 | pub test_path: Option, 18 | pub out_path: PathBuf, 19 | } 20 | 21 | impl Options { 22 | pub fn into_compiler( 23 | self, 24 | writer: Writer, 25 | ) -> Result> { 26 | let mut compiler = PackageCompiler { 27 | options: self, 28 | sources: vec![], 29 | writer, 30 | }; 31 | compiler.read_source_files()?; 32 | Ok(compiler) 33 | } 34 | } 35 | 36 | #[derive(Debug)] 37 | pub struct PackageCompiler { 38 | pub options: Options, 39 | pub sources: Vec, 40 | pub writer: Writer, 41 | } 42 | 43 | // TODO: ensure this is not a duplicate module 44 | // TODO: tests 45 | // Including cases for: 46 | // - modules that don't import anything 47 | impl PackageCompiler { 48 | pub fn new(options: Options, writer: Writer) -> Self { 49 | Self { 50 | options, 51 | writer, 52 | sources: vec![], 53 | } 54 | } 55 | 56 | pub fn compile( 57 | mut self, 58 | existing_modules: &mut HashMap, 59 | already_defined_modules: &mut HashMap, 60 | ) -> Result { 61 | let span = tracing::info_span!("compile", package = self.options.name.as_str()); 62 | let _enter = span.enter(); 63 | 64 | tracing::info!("Parsing source code"); 65 | let parsed_modules = 66 | parse_sources(std::mem::take(&mut self.sources), already_defined_modules)?; 67 | 68 | // Determine order in which modules are to be processed 69 | let sequence = 70 | dep_tree::toposort_deps(parsed_modules.values().map(module_deps_for_graph).collect()) 71 | .map_err(convert_deps_tree_error)?; 72 | 73 | tracing::info!("Type checking modules"); 74 | let modules = type_check(sequence, parsed_modules, existing_modules)?; 75 | 76 | tracing::info!("Performing code generation"); 77 | self.perform_codegen(modules.as_slice())?; 78 | 79 | // TODO: write metadata 80 | 81 | Ok(Package { 82 | name: self.options.name, 83 | modules, 84 | }) 85 | } 86 | 87 | pub fn read_source_files(&mut self) -> Result<()> { 88 | let span = tracing::info_span!("load", package = self.options.name.as_str()); 89 | let _enter = span.enter(); 90 | tracing::info!("Reading source files"); 91 | 92 | // Src 93 | for path in crate::fs::gleam_files(&self.options.src_path) { 94 | let name = module_name(&self.options.src_path, &path); 95 | let code = crate::fs::read(&path)?; 96 | self.sources.push(Source { 97 | name, 98 | path, 99 | code, 100 | origin: Origin::Src, 101 | }); 102 | } 103 | 104 | // Test 105 | if let Some(test_path) = &self.options.test_path { 106 | for path in crate::fs::gleam_files(test_path) { 107 | let name = module_name(test_path, &path); 108 | let code = crate::fs::read(&path)?; 109 | self.sources.push(Source { 110 | name, 111 | path, 112 | code, 113 | origin: Origin::Test, 114 | }); 115 | } 116 | } 117 | Ok(()) 118 | } 119 | 120 | fn perform_codegen(&self, modules: &[Module]) -> Result<()> { 121 | Erlang::new(self.options.out_path.as_path()).render(&self.writer, modules) 122 | } 123 | } 124 | 125 | fn type_check( 126 | sequence: Vec, 127 | mut parsed_modules: HashMap, 128 | module_types: &mut HashMap, 129 | ) -> Result, Error> { 130 | let mut warnings = vec![]; 131 | let mut modules = Vec::with_capacity(parsed_modules.len()); 132 | let mut uid = 0; 133 | 134 | for name in sequence { 135 | let Parsed { 136 | name, 137 | code, 138 | ast, 139 | path, 140 | origin, 141 | } = parsed_modules 142 | .remove(&name) 143 | .gleam_expect("Getting parsed module for name"); 144 | 145 | tracing::trace!(module = ?name, "Type checking"); 146 | let ast = 147 | typ::infer_module(&mut uid, ast, module_types, &mut warnings).map_err(|error| { 148 | Error::Type { 149 | path: path.clone(), 150 | src: code.clone(), 151 | error, 152 | } 153 | })?; 154 | 155 | module_types.insert(name.clone(), (origin, ast.type_info.clone())); 156 | 157 | modules.push(Module { 158 | origin, 159 | name, 160 | code, 161 | ast, 162 | path, 163 | }); 164 | } 165 | 166 | // TODO: do something with warnings 167 | 168 | Ok(modules) 169 | } 170 | 171 | fn convert_deps_tree_error(e: dep_tree::Error) -> Error { 172 | match e { 173 | dep_tree::Error::Cycle(modules) => Error::ImportCycle { modules }, 174 | } 175 | } 176 | 177 | fn module_deps_for_graph(module: &Parsed) -> (String, Vec) { 178 | let name = module.name.clone(); 179 | let deps: Vec<_> = module 180 | .ast 181 | .dependencies() 182 | .into_iter() 183 | .map(|(dep, _span)| dep) 184 | .collect(); 185 | (name, deps) 186 | } 187 | 188 | fn parse_sources( 189 | sources: Vec, 190 | already_defined_modules: &mut HashMap, 191 | ) -> Result, Error> { 192 | let mut parsed_modules = HashMap::with_capacity(sources.len()); 193 | for source in sources.into_iter() { 194 | let Source { 195 | name, 196 | code, 197 | path, 198 | origin, 199 | } = source; 200 | let (mut ast, _) = 201 | crate::parse::parse_module(code.as_str()).map_err(|error| Error::Parse { 202 | path: path.clone(), 203 | src: code.clone(), 204 | error, 205 | })?; 206 | 207 | // Store the name 208 | ast.name = name.as_str().split("/").map(String::from).collect(); // TODO: store the module name as a string 209 | 210 | let module = Parsed { 211 | origin, 212 | path, 213 | name, 214 | code, 215 | ast, 216 | }; 217 | 218 | // Ensure there are no modules defined that already have this name 219 | if let Some(first) = 220 | already_defined_modules.insert(module.name.clone(), module.path.clone()) 221 | { 222 | return Err(Error::DuplicateModule { 223 | module: module.name.clone(), 224 | first, 225 | second: module.path.clone(), 226 | }); 227 | } 228 | 229 | // Register the parsed module 230 | parsed_modules.insert(module.name.clone(), module); 231 | } 232 | Ok(parsed_modules) 233 | } 234 | 235 | fn module_name(package_path: &Path, full_module_path: &Path) -> String { 236 | // /path/to/project/_build/default/lib/the_package/src/my/module.gleam 237 | 238 | // my/module.gleam 239 | let mut module_path = full_module_path 240 | .strip_prefix(package_path) 241 | .gleam_expect("Stripping package prefix from module path") 242 | .to_path_buf(); 243 | 244 | // my/module 245 | module_path.set_extension(""); 246 | 247 | // Stringify 248 | let name = module_path 249 | .to_str() 250 | .gleam_expect("Module name path to str") 251 | .to_string(); 252 | 253 | // normalise windows paths 254 | name.replace("\\", "/") 255 | } 256 | 257 | #[derive(Debug)] 258 | pub struct Source { 259 | pub path: PathBuf, 260 | pub name: String, 261 | pub code: String, 262 | pub origin: Origin, // TODO: is this used? 263 | } 264 | 265 | #[derive(Debug)] 266 | struct Parsed { 267 | path: PathBuf, 268 | name: String, 269 | code: String, 270 | origin: Origin, 271 | ast: UntypedModule, 272 | } 273 | -------------------------------------------------------------------------------- /src/project.rs: -------------------------------------------------------------------------------- 1 | mod source_tree; 2 | #[cfg(test)] 3 | mod tests; 4 | 5 | use crate::{ 6 | ast::{SrcSpan, TypedModule}, 7 | build::Origin, 8 | config::{self, PackageConfig}, 9 | error::{Error, FileIOAction, FileKind, GleamExpect}, 10 | parse::extra::Comment, 11 | typ, 12 | warning::Warning, 13 | }; 14 | use source_tree::SourceTree; 15 | use std::collections::HashMap; 16 | use std::iter::Peekable; 17 | use std::path::{Path, PathBuf}; 18 | 19 | pub const OUTPUT_DIR_NAME: &str = "gen"; 20 | 21 | #[derive(Debug, PartialEq)] 22 | pub struct Input { 23 | pub source_base_path: PathBuf, 24 | pub path: PathBuf, 25 | pub src: String, 26 | pub origin: ModuleOrigin, 27 | } 28 | 29 | #[derive(Debug, PartialEq)] 30 | pub struct Analysed { 31 | pub ast: TypedModule, 32 | pub src: String, 33 | pub name: Vec, 34 | pub path: PathBuf, 35 | pub origin: ModuleOrigin, 36 | pub type_info: typ::Module, 37 | pub source_base_path: PathBuf, 38 | pub warnings: Vec, 39 | pub module_extra: crate::parse::extra::ModuleExtra, 40 | } 41 | impl Analysed { 42 | pub fn attach_doc_and_module_comments(&mut self) { 43 | // Module Comments 44 | self.ast.documentation = self 45 | .module_extra 46 | .module_comments 47 | .iter() 48 | .map(|span| Comment::from((span, self.src.as_str())).content.to_string()) 49 | .collect(); 50 | 51 | // Doc Comments 52 | let mut doc_comments = self.module_extra.doc_comments.iter().peekable(); 53 | for statement in &mut self.ast.statements { 54 | let docs: Vec<&str> = comments_before( 55 | &mut doc_comments, 56 | statement.location().start, 57 | self.src.as_str(), 58 | ); 59 | if !docs.is_empty() { 60 | let doc = docs.join("\n"); 61 | statement.put_doc(doc); 62 | } 63 | 64 | if let crate::ast::Statement::CustomType { constructors, .. } = statement { 65 | for constructor in constructors { 66 | let docs: Vec<&str> = comments_before( 67 | &mut doc_comments, 68 | constructor.location.start, 69 | self.src.as_str(), 70 | ); 71 | if !docs.is_empty() { 72 | let doc = docs.join("\n"); 73 | constructor.put_doc(doc); 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | fn comments_before<'a>( 82 | comment_spans: &mut Peekable>, 83 | byte: usize, 84 | src: &'a str, 85 | ) -> Vec<&'a str> { 86 | let mut comments = vec![]; 87 | while let Some(SrcSpan { start, .. }) = comment_spans.peek() { 88 | if start <= &byte { 89 | let comment = comment_spans.next().unwrap(); 90 | comments.push(Comment::from((comment, src)).content) 91 | } else { 92 | break; 93 | } 94 | } 95 | comments 96 | } 97 | 98 | #[derive(Debug, PartialEq, Clone)] 99 | pub enum ModuleOrigin { 100 | Src, 101 | Test, 102 | Dependency, 103 | } 104 | 105 | impl ModuleOrigin { 106 | pub fn dir_name(&self) -> &'static str { 107 | match self { 108 | Self::Src | Self::Dependency => "src", 109 | Self::Test => "test", 110 | } 111 | } 112 | 113 | pub fn to_origin(&self) -> Origin { 114 | match self { 115 | Self::Test => Origin::Test, 116 | Self::Src | Self::Dependency => Origin::Src, 117 | } 118 | } 119 | } 120 | 121 | #[derive(Debug)] 122 | pub struct Module { 123 | src: String, 124 | path: PathBuf, 125 | source_base_path: PathBuf, 126 | origin: ModuleOrigin, 127 | module: crate::ast::UntypedModule, 128 | module_extra: crate::parse::extra::ModuleExtra, 129 | } 130 | 131 | pub fn read_and_analyse(root: impl AsRef) -> Result<(PackageConfig, Vec), Error> { 132 | let project_config = config::read_project_config(&root)?; 133 | let mut srcs = vec![]; 134 | let root = root.as_ref(); 135 | let lib_dir = root.join("_build").join("default").join("lib"); 136 | let checkouts_dir = root.join("_checkouts"); 137 | let mix_lib_dir = root.join("deps"); 138 | 139 | for project_dir in [lib_dir, checkouts_dir, mix_lib_dir] 140 | .iter() 141 | .filter_map(|d| std::fs::read_dir(d).ok()) 142 | .flat_map(|d| d.filter_map(Result::ok)) 143 | .map(|d| d.path()) 144 | .filter(|p| { 145 | p.file_name().and_then(|os_string| os_string.to_str()) != Some(&project_config.name) 146 | }) 147 | { 148 | collect_source(project_dir.join("src"), ModuleOrigin::Dependency, &mut srcs)?; 149 | } 150 | 151 | // Collect source code from top level project 152 | collect_source(root.join("src"), ModuleOrigin::Src, &mut srcs)?; 153 | collect_source(root.join("test"), ModuleOrigin::Test, &mut srcs)?; 154 | 155 | // Analyse source 156 | let analysed = analysed(srcs)?; 157 | 158 | Ok((project_config, analysed)) 159 | } 160 | 161 | pub fn analysed(inputs: Vec) -> Result, Error> { 162 | let module_count = inputs.len(); 163 | let mut source_tree = SourceTree::new(inputs)?; 164 | let mut modules_type_infos = HashMap::new(); 165 | let mut compiled_modules = Vec::with_capacity(module_count); 166 | let mut uid = 0; 167 | 168 | struct Out { 169 | source_base_path: PathBuf, 170 | name_string: String, 171 | name: Vec, 172 | path: PathBuf, 173 | origin: ModuleOrigin, 174 | ast: TypedModule, 175 | src: String, 176 | warnings: Vec, 177 | module_extra: crate::parse::extra::ModuleExtra, 178 | } 179 | 180 | for Module { 181 | src, 182 | path, 183 | module, 184 | origin, 185 | source_base_path, 186 | module_extra, 187 | } in source_tree.consume()? 188 | { 189 | let name = module.name.clone(); 190 | let name_string = module.name_string(); 191 | 192 | println!("Compiling {}", name_string.as_str()); 193 | 194 | let mut warnings = vec![]; 195 | let result = crate::typ::infer_module(&mut uid, module, &modules_type_infos, &mut warnings); 196 | let warnings = warnings 197 | .into_iter() 198 | .map(|warning| Warning::Type { 199 | path: path.clone(), 200 | src: src.clone(), 201 | warning, 202 | }) 203 | .collect(); 204 | 205 | let ast = result.map_err(|error| Error::Type { 206 | path: path.clone(), 207 | src: src.clone(), 208 | error, 209 | })?; 210 | 211 | let _ = modules_type_infos.insert( 212 | name_string.clone(), 213 | (origin.to_origin(), ast.type_info.clone()), 214 | ); 215 | 216 | compiled_modules.push(Out { 217 | name, 218 | name_string, 219 | path, 220 | source_base_path, 221 | origin, 222 | ast, 223 | src, 224 | warnings, 225 | module_extra, 226 | }); 227 | } 228 | 229 | Ok(compiled_modules 230 | .into_iter() 231 | .map(|out| { 232 | let Out { 233 | name, 234 | source_base_path, 235 | path, 236 | name_string, 237 | origin, 238 | ast, 239 | src, 240 | warnings, 241 | module_extra, 242 | } = out; 243 | Analysed { 244 | ast, 245 | src, 246 | name, 247 | path, 248 | source_base_path, 249 | origin, 250 | type_info: modules_type_infos 251 | .remove(&name_string) 252 | .gleam_expect("project::compile(): Merging module type info") 253 | .1, 254 | warnings, 255 | module_extra, 256 | } 257 | }) 258 | .collect()) 259 | } 260 | 261 | pub fn collect_source( 262 | src_dir: PathBuf, 263 | origin: ModuleOrigin, 264 | srcs: &mut Vec, 265 | ) -> Result<(), Error> { 266 | let src_dir = match src_dir.canonicalize() { 267 | Ok(d) => d, 268 | Err(_) => return Ok(()), 269 | }; 270 | 271 | for path in crate::fs::gleam_files(&src_dir) { 272 | let src = std::fs::read_to_string(&path).map_err(|err| Error::FileIO { 273 | action: FileIOAction::Read, 274 | kind: FileKind::File, 275 | err: Some(err.to_string()), 276 | path: path.clone(), 277 | })?; 278 | 279 | srcs.push(Input { 280 | path: path 281 | .canonicalize() 282 | .gleam_expect("project::collect_source(): path canonicalize"), 283 | source_base_path: src_dir.clone(), 284 | origin: origin.clone(), 285 | src, 286 | }) 287 | } 288 | Ok(()) 289 | } 290 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | #![warn( 3 | clippy::all, 4 | clippy::doc_markdown, 5 | clippy::dbg_macro, 6 | clippy::todo, 7 | clippy::empty_enum, 8 | clippy::enum_glob_use, 9 | clippy::mem_forget, 10 | // TODO: enable once the false positive bug is solved 11 | // clippy::use_self, 12 | clippy::filter_map_next, 13 | clippy::needless_continue, 14 | clippy::needless_borrow, 15 | clippy::match_wildcard_for_single_variants, 16 | clippy::if_let_mutex, 17 | clippy::mismatched_target_os, 18 | clippy::await_holding_lock, 19 | clippy::match_on_vec_items, 20 | clippy::imprecise_flops, 21 | clippy::suboptimal_flops, 22 | clippy::lossy_float_literal, 23 | clippy::rest_pat_in_fully_bound_structs, 24 | clippy::fn_params_excessive_bools, 25 | clippy::inefficient_to_string, 26 | clippy::linkedlist, 27 | clippy::macro_use_imports, 28 | clippy::option_option, 29 | clippy::verbose_file_reads, 30 | clippy::unnested_or_patterns, 31 | rust_2018_idioms, 32 | missing_debug_implementations, 33 | missing_copy_implementations, 34 | trivial_casts, 35 | trivial_numeric_casts, 36 | unstable_features, 37 | nonstandard_style, 38 | unused_import_braces, 39 | unused_qualifications, 40 | unused_results, 41 | // Safety 42 | unsafe_code, 43 | clippy::unimplemented, 44 | clippy::expect_used, 45 | clippy::unwrap_used, 46 | clippy::ok_expect, 47 | clippy::integer_division, 48 | clippy::indexing_slicing, 49 | clippy::mem_forget 50 | )] 51 | 52 | #[macro_use] 53 | mod pretty; 54 | 55 | mod ast; 56 | mod bit_string; 57 | mod build; 58 | mod cli; 59 | mod codegen; 60 | mod config; 61 | mod diagnostic; 62 | mod docs; 63 | mod erl; 64 | mod error; 65 | mod eunit; 66 | mod format; 67 | mod fs; 68 | mod line_numbers; 69 | mod metadata; 70 | mod new; 71 | mod parse; 72 | mod project; 73 | mod shell; 74 | mod typ; 75 | mod warning; 76 | 77 | mod schema_capnp { 78 | #![allow(dead_code, unused_qualifications)] 79 | include!("../generated/schema_capnp.rs"); 80 | } 81 | 82 | #[macro_use] 83 | extern crate im; 84 | 85 | #[cfg(test)] 86 | #[macro_use] 87 | extern crate pretty_assertions; 88 | 89 | #[macro_use] 90 | extern crate lazy_static; 91 | 92 | pub use self::{ 93 | error::{Error, GleamExpect, Result}, 94 | warning::Warning, 95 | }; 96 | 97 | use self::build::package_compiler; 98 | 99 | use std::path::PathBuf; 100 | use structopt::{clap::AppSettings, StructOpt}; 101 | use strum::VariantNames; 102 | 103 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 104 | 105 | #[derive(StructOpt, Debug)] 106 | #[structopt(global_settings = &[AppSettings::ColoredHelp, AppSettings::VersionlessSubcommands])] 107 | enum Command { 108 | #[structopt( 109 | name = "build", 110 | about = "Compile a project", 111 | setting = AppSettings::Hidden, 112 | )] 113 | Build { 114 | #[structopt(help = "location of the project root", default_value = ".")] 115 | project_root: String, 116 | }, 117 | 118 | #[structopt(name = "docs", about = "Render HTML documentation")] 119 | Docs(Docs), 120 | 121 | #[structopt(name = "new", about = "Create a new project")] 122 | New(NewOptions), 123 | 124 | #[structopt(name = "format", about = "Format source code")] 125 | Format { 126 | #[structopt(help = "files to format", default_value = ".")] 127 | files: Vec, 128 | 129 | #[structopt(help = "read source from standard in", long = "stdin")] 130 | stdin: bool, 131 | 132 | #[structopt( 133 | help = "check if inputs are formatted without changing them", 134 | long = "check" 135 | )] 136 | check: bool, 137 | }, 138 | 139 | #[structopt( 140 | name = "shell", 141 | about = "Start an Erlang shell", 142 | setting = AppSettings::Hidden, 143 | )] 144 | Shell { 145 | #[structopt(help = "location of the project root", default_value = ".")] 146 | project_root: String, 147 | }, 148 | 149 | #[structopt( 150 | name = "eunit", 151 | about = "Run eunit tests", 152 | setting = AppSettings::Hidden, 153 | )] 154 | Eunit { 155 | #[structopt(help = "location of the project root", default_value = ".")] 156 | project_root: String, 157 | }, 158 | 159 | #[structopt( 160 | name = "compile-package", 161 | about = "Compile a single Gleam package", 162 | setting = AppSettings::Hidden, 163 | )] 164 | CompilePackage(CompilePackage), 165 | } 166 | 167 | #[derive(StructOpt, Debug)] 168 | #[structopt(flatten)] 169 | pub struct NewOptions { 170 | #[structopt(help = "name of the project")] 171 | pub name: String, 172 | 173 | #[structopt( 174 | long = "description", 175 | help = "description of the project", 176 | default_value = "A Gleam project" 177 | )] 178 | pub description: String, 179 | 180 | #[structopt(help = "location of the project root")] 181 | pub project_root: Option, 182 | 183 | #[structopt( 184 | long = "template", 185 | possible_values = &new::Template::VARIANTS, 186 | case_insensitive = true, 187 | default_value = "lib" 188 | )] 189 | pub template: new::Template, 190 | } 191 | 192 | #[derive(StructOpt, Debug)] 193 | #[structopt(flatten)] 194 | pub struct CompilePackage { 195 | #[structopt(help = "The name of the package being compiled", long = "name")] 196 | package_name: String, 197 | 198 | #[structopt(help = "A directory of source Gleam code", long = "src")] 199 | src_directory: PathBuf, 200 | 201 | #[structopt(help = "A directory of test Gleam code", long = "test")] 202 | test_directory: Option, 203 | 204 | #[structopt(help = "A directory to write compiled code to", long = "out")] 205 | output_directory: PathBuf, 206 | 207 | #[structopt(help = "A path to a compiled dependency library", long = "lib")] 208 | libraries: Vec, 209 | } 210 | 211 | impl CompilePackage { 212 | pub fn into_package_compiler_options(self) -> package_compiler::Options { 213 | package_compiler::Options { 214 | name: self.package_name, 215 | src_path: self.src_directory, 216 | test_path: self.test_directory, 217 | out_path: self.output_directory, 218 | } 219 | } 220 | } 221 | 222 | #[derive(StructOpt, Debug)] 223 | enum Docs { 224 | #[structopt(name = "build", about = "Render HTML docs locally")] 225 | Build { 226 | #[structopt(help = "location of the project root", default_value = ".")] 227 | project_root: String, 228 | 229 | #[structopt(help = "the directory to write the docs to", long = "to")] 230 | to: Option, 231 | 232 | #[structopt(help = "the version to publish", long = "version")] 233 | version: String, 234 | }, 235 | 236 | #[structopt(name = "publish", about = "Publish HTML docs to HexDocs")] 237 | Publish { 238 | #[structopt(help = "location of the project root", default_value = ".")] 239 | project_root: String, 240 | 241 | #[structopt(help = "the version to publish", long = "version")] 242 | version: String, 243 | }, 244 | 245 | #[structopt(name = "remove", about = "Remove HTML docs from HexDocs")] 246 | Remove { 247 | #[structopt(help = "the name of the package", long = "package")] 248 | package: String, 249 | 250 | #[structopt(help = "the version of the docs to remove", long = "version")] 251 | version: String, 252 | }, 253 | } 254 | 255 | fn main() { 256 | initialise_logger(); 257 | 258 | let result = match Command::from_args() { 259 | Command::Build { project_root } => command_build(project_root), 260 | 261 | Command::Docs(Docs::Build { 262 | project_root, 263 | version, 264 | to, 265 | }) => docs::command::build(project_root, version, to), 266 | 267 | Command::Docs(Docs::Publish { 268 | project_root, 269 | version, 270 | }) => docs::command::publish(project_root, version), 271 | 272 | Command::Docs(Docs::Remove { package, version }) => docs::command::remove(package, version), 273 | 274 | Command::Format { 275 | stdin, 276 | files, 277 | check, 278 | } => format::command::run(stdin, check, files), 279 | 280 | Command::New(options) => new::create(options, VERSION), 281 | 282 | Command::Shell { project_root } => shell::command(project_root), 283 | 284 | Command::Eunit { project_root } => eunit::command(project_root), 285 | 286 | Command::CompilePackage(opts) => build::compile_package::command(opts), 287 | }; 288 | 289 | match result { 290 | Ok(_) => { 291 | tracing::info!("Successfully completed"); 292 | } 293 | Err(error) => { 294 | tracing::error!(error = ?error, "Failed"); 295 | error.pretty_print(); 296 | std::process::exit(1); 297 | } 298 | } 299 | } 300 | 301 | fn command_build(root: String) -> Result<(), Error> { 302 | let root = PathBuf::from(&root); 303 | let config = config::read_project_config(&root)?; 304 | 305 | // Use new build tool 306 | if config.tool == config::BuildTool::Gleam { 307 | return build::main(config, root).map(|_| ()); 308 | } 309 | 310 | // Read and type check project 311 | let (_config, analysed) = project::read_and_analyse(&root)?; 312 | 313 | // Generate Erlang code 314 | let output_files = erl::generate_erlang(analysed.as_slice()); 315 | 316 | // Reset output directory 317 | fs::delete_dir(&root.join(project::OUTPUT_DIR_NAME))?; 318 | 319 | // Print warnings 320 | warning::print_all(analysed.as_slice()); 321 | 322 | // Delete the gen directory before generating the newly compiled files 323 | fs::write_outputs(output_files.as_slice())?; 324 | 325 | println!("Done!"); 326 | 327 | Ok(()) 328 | } 329 | 330 | fn initialise_logger() { 331 | tracing_subscriber::fmt() 332 | .with_env_filter(&std::env::var("GLEAM_LOG").unwrap_or_else(|_| "off".to_string())) 333 | .with_target(false) 334 | .without_time() 335 | .init(); 336 | } 337 | --------------------------------------------------------------------------------