├── .gitignore ├── examples ├── import_errors │ ├── c.bella │ ├── a.bella │ └── b.bella ├── empty.bella ├── hello_world.bella ├── example_project │ ├── src │ │ ├── hello.bella │ │ ├── string │ │ │ └── join.bella │ │ └── example_project.bella │ └── bella.json ├── escape.bella ├── crlf.bella ├── input.bella ├── factorial.bella ├── lambda.bella ├── import.bella ├── partial_application.bella ├── countdown.bella ├── math.bella ├── try_throw.bella ├── record.bella ├── pattern_matching.bella └── list.bella ├── .vscode └── settings.json ├── deno.json ├── assets └── Bella-language.png ├── docs ├── style.md ├── functions.md ├── README.md └── language.md ├── src ├── main.ts ├── bella │ ├── utils.gleam │ ├── project.gleam │ ├── lexer │ │ └── token.gleam │ ├── error.gleam │ ├── evaluator │ │ ├── types.gleam │ │ └── functions.gleam │ ├── lexer.gleam │ ├── parser.gleam │ └── evaluator.gleam ├── ffi.ts ├── grammar.md └── bella.gleam ├── gleam.toml ├── README.md ├── LICENSE ├── .github └── workflows │ └── build.yml └── manifest.toml /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /examples/import_errors/c.bella: -------------------------------------------------------------------------------- 1 | 1 + '2' -------------------------------------------------------------------------------- /examples/empty.bella: -------------------------------------------------------------------------------- 1 | ; Should be a syntax error -------------------------------------------------------------------------------- /examples/hello_world.bella: -------------------------------------------------------------------------------- 1 | Io.println('Hello, world!') -------------------------------------------------------------------------------- /examples/example_project/src/hello.bella: -------------------------------------------------------------------------------- 1 | 'Hello there,' -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true 3 | } 4 | -------------------------------------------------------------------------------- /examples/escape.bella: -------------------------------------------------------------------------------- 1 | Io.println("\"Hello,\"\n\t" + '\'world!\'') -------------------------------------------------------------------------------- /examples/example_project/src/string/join.bella: -------------------------------------------------------------------------------- 1 | a -> b -> a + ' ' + b -------------------------------------------------------------------------------- /examples/crlf.bella: -------------------------------------------------------------------------------- 1 | Io.println('Hello, world!') 2 | 3 | ; Testing CRLF -------------------------------------------------------------------------------- /examples/import_errors/a.bella: -------------------------------------------------------------------------------- 1 | import_('examples/import_errors/b.bella') -------------------------------------------------------------------------------- /examples/import_errors/b.bella: -------------------------------------------------------------------------------- 1 | import_('examples/import_errors/c.bella') -------------------------------------------------------------------------------- /examples/input.bella: -------------------------------------------------------------------------------- 1 | Io.println("Hello, " + Io.readline("What is your name?")) -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "./src/": "./build/dev/javascript/bella/" 4 | } 5 | } -------------------------------------------------------------------------------- /assets/Bella-language.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MystPi/bella/HEAD/assets/Bella-language.png -------------------------------------------------------------------------------- /docs/style.md: -------------------------------------------------------------------------------- 1 | # Style Guide 2 | 3 | _[back to index](README.md)_ 4 | 5 | > _To be completed_ 6 | -------------------------------------------------------------------------------- /examples/example_project/bella.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example_project", 3 | "version": "0.1.0" 4 | } -------------------------------------------------------------------------------- /docs/functions.md: -------------------------------------------------------------------------------- 1 | # Builtin Functions 2 | 3 | _[back to index](README.md)_ 4 | 5 | > _To be completed_ 6 | -------------------------------------------------------------------------------- /examples/example_project/src/example_project.bella: -------------------------------------------------------------------------------- 1 | import hello 2 | import string/join 3 | 4 | Io.println(join(hello, 'this is my Bella project!')) -------------------------------------------------------------------------------- /examples/factorial.bella: -------------------------------------------------------------------------------- 1 | let fact = 2 | n -> 3 | if n == 0 then 4 | 1 5 | else 6 | n * fact(n - 1) 7 | in 8 | 9 | 4 |> fact |> Io.println -------------------------------------------------------------------------------- /examples/lambda.bella: -------------------------------------------------------------------------------- 1 | let 2 | foo = 3 | bar -> -> 4 | { baz: bar } 5 | 6 | foo42 = 7 | foo(42) 8 | in 9 | 10 | Io.println(foo42().baz) -------------------------------------------------------------------------------- /examples/import.bella: -------------------------------------------------------------------------------- 1 | ; See examples/example_project for usage of real imports 2 | 3 | let math = 4 | import_('./examples/math.bella') 5 | in 6 | 7 | Io.println(math.add(1, 2)) -------------------------------------------------------------------------------- /examples/partial_application.bella: -------------------------------------------------------------------------------- 1 | let 2 | add3tog = 3 | x -> y -> z -> 4 | x + y + z 5 | 6 | add5 = 7 | add3tog(5) 8 | 9 | add7 = 10 | add5(2) 11 | in 12 | 13 | Io.println(add7(6)) -------------------------------------------------------------------------------- /examples/countdown.bella: -------------------------------------------------------------------------------- 1 | let countdown = 2 | n -> 3 | if n == 0 then 4 | Io.println('Liftoff!') 5 | else ( 6 | Io.println(n) 7 | countdown(n - 1 ) 8 | ) 9 | in 10 | 11 | countdown(10) -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | // This file will be copied to `build/dev/javascript/bella/main.ts` after running 2 | // `gleam build`, where it can be used as the entrypoint for tools like `deno compile`. 3 | 4 | import { main } from './bella.mjs'; 5 | 6 | main(); 7 | -------------------------------------------------------------------------------- /examples/math.bella: -------------------------------------------------------------------------------- 1 | let 2 | add = 3 | a -> b -> 4 | a + b 5 | 6 | sub = 7 | a -> b -> 8 | a - b 9 | 10 | mul = 11 | a -> b -> 12 | a * b 13 | 14 | div = 15 | a -> b -> 16 | a / b 17 | in 18 | 19 | { add 20 | , sub 21 | , mul 22 | , div 23 | } -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Bella Documentation 2 | 3 | Documentation for various aspects of Bella can be found in this folder. 4 | 5 | - [Language tour](./language.md) 6 | - [Builtin functions](./functions.md) 7 | - [Style guide](./style.md) 8 | 9 | Code examples can be found in the [examples](../examples) folder. 10 | -------------------------------------------------------------------------------- /examples/try_throw.bella: -------------------------------------------------------------------------------- 1 | let divide = 2 | a -> b -> 3 | if b == 0 then 4 | throw 'Cannot divide by 0' 5 | else 6 | a / b 7 | in ( 8 | Io.println('Before try') 9 | 10 | try 11 | Io.println(divide(1, 0)) 12 | else 13 | msg -> Io.println(msg) 14 | 15 | Io.println('After try') 16 | ) -------------------------------------------------------------------------------- /src/bella/utils.gleam: -------------------------------------------------------------------------------- 1 | @external(javascript, "../ffi.ts", "getArgs") 2 | pub fn get_args() -> List(String) 3 | 4 | @external(javascript, "../ffi.ts", "unescape") 5 | pub fn unescape(a: String) -> String 6 | 7 | @external(javascript, "../ffi.ts", "stringify") 8 | pub fn stringify(a: String) -> String 9 | 10 | @external(javascript, "../ffi.ts", "readline") 11 | pub fn readline(message: String) -> String 12 | -------------------------------------------------------------------------------- /examples/record.bella: -------------------------------------------------------------------------------- 1 | let 2 | repository = 3 | { type: 'github' 4 | , user: 'MystPi' 5 | , repo: 'bella' 6 | } 7 | 8 | package = 9 | { name: 'Bella' 10 | , type: 'Programming language' 11 | , repository 12 | , tags: 13 | { fun: false 14 | , simple: true 15 | } 16 | } 17 | 18 | bella = 19 | package + { tags: package.tags + { fun: true } } 20 | in 21 | 22 | bella.tags.fun |> Io.println -------------------------------------------------------------------------------- /gleam.toml: -------------------------------------------------------------------------------- 1 | name = "bella" 2 | version = "0.1.0" 3 | description = "A simple functional programming language" 4 | target = "javascript" 5 | licences = ["MIT"] 6 | repository = { type = "github", user = "MystPi", repo = "bella" } 7 | gleam = ">= 0.32.0" 8 | 9 | [dependencies] 10 | gleam_stdlib = "~> 0.29" 11 | hug = "~> 0.1" 12 | gleam_community_ansi = "~> 1.1" 13 | gleam_json = "~> 0.5" 14 | simplifile = "~> 0.1" 15 | 16 | [javascript] 17 | runtime = "deno" 18 | 19 | [javascript.deno] 20 | allow_all = true 21 | -------------------------------------------------------------------------------- /src/bella/project.gleam: -------------------------------------------------------------------------------- 1 | import gleam/dynamic 2 | import gleam/json 3 | 4 | pub type Project { 5 | Project(name: String, version: String) 6 | } 7 | 8 | pub fn init(project_name: String) -> String { 9 | json.object([ 10 | #("name", json.string(project_name)), 11 | #("version", json.string("0.1.0")), 12 | ]) 13 | |> json.to_string 14 | } 15 | 16 | pub fn decode(json: String) -> Result(Project, json.DecodeError) { 17 | let project_decoder = 18 | dynamic.decode2( 19 | Project, 20 | dynamic.field("name", of: dynamic.string), 21 | dynamic.field("version", of: dynamic.string), 22 | ) 23 | 24 | json.decode(json, project_decoder) 25 | } 26 | -------------------------------------------------------------------------------- /src/ffi.ts: -------------------------------------------------------------------------------- 1 | import { toList } from './gleam.mjs'; 2 | 3 | export function getArgs() { 4 | return toList(Deno.args); 5 | } 6 | 7 | const escapeCodes = { 8 | '\\': '\\', 9 | '"': '"', 10 | "'": "'", 11 | n: '\n', 12 | r: '\r', 13 | t: '\t', 14 | }; 15 | 16 | export function unescape(string: string) { 17 | return string.replace(/\\(.)/g, (_, c) => { 18 | if (c in escapeCodes) { 19 | return escapeCodes[c as keyof typeof escapeCodes]; 20 | } 21 | return `\\${c}`; 22 | }); 23 | } 24 | 25 | export function stringify(string: string) { 26 | return JSON.stringify(string); 27 | } 28 | 29 | export function readline(message: string) { 30 | return prompt(message); 31 | } 32 | -------------------------------------------------------------------------------- /examples/pattern_matching.bella: -------------------------------------------------------------------------------- 1 | let 2 | test = [1, 2, 3] 3 | [a, b, _] = test 4 | ^test = [1, 2, 3] 5 | y = 42 6 | 42 = y 7 | x = false 8 | false = x 9 | ["add", a, b] = ["add", 50, 40] 10 | [x, ^Io.println(x * x), ^Io.println(x * x * x)] = [2, 4, 8] 11 | { name, cool: true } = { name: "MystPi", cool: true } 12 | { type: "dog", props: { name } } = { type: "dog", props: { name: "Bella" }} 13 | ^{ foo: true, bar: "blah" } = { foo: true, bar: "blah" } 14 | ^[1, 1 + 1, 1 + 1 + 1, 1 + 1 + 1 + 1] = [1, 2, 3, 4] 15 | [x, y | rest] = [1, 2, 3, 4] 16 | [x, ^(x + 1) as y, ^(x + 2) as z] = [1, 2, 3] 17 | ?Types.is_type("string") = "32" 18 | in 19 | 20 | Io.println("Everything matched successfully!") -------------------------------------------------------------------------------- /examples/list.bella: -------------------------------------------------------------------------------- 1 | let 2 | mylist = 3 | [1, 2, 3, 4, 5, 6, 7, 8, 9] 4 | 5 | map = 6 | list -> fn -> 7 | match list 8 | is [] then [] 9 | is [first | rest] then 10 | [fn(first)] + map(rest, fn) 11 | 12 | fold = 13 | list -> acc -> fn -> 14 | match list 15 | is [] then acc 16 | is [first | rest] then 17 | fold(rest, fn(first, acc), fn) 18 | 19 | reduce = 20 | list -> fn -> 21 | let [first | rest] = list in 22 | fold(rest, first, fn) 23 | in 24 | ( 25 | ; Squares 26 | map(mylist, x -> x * x) |> Io.println 27 | 28 | ; Sum 29 | reduce(mylist, x -> a -> x + a) |> Io.println 30 | 31 | ; Length 32 | fold(mylist, 0, x -> a -> a + 1) |> Io.println 33 | ) 34 | -------------------------------------------------------------------------------- /src/bella/lexer/token.gleam: -------------------------------------------------------------------------------- 1 | pub type Position { 2 | Position(line: Int, col: Int) 3 | } 4 | 5 | pub type Span { 6 | Span(from: Position, to: Position) 7 | } 8 | 9 | pub const useless_span = Span(Position(0, 0), Position(0, 0)) 10 | 11 | pub type Token = 12 | #(TokenType, Span) 13 | 14 | pub type Tokens = 15 | List(Token) 16 | 17 | pub type TokenType { 18 | Eof 19 | WhiteSpace 20 | Newline 21 | Comment 22 | 23 | LParen 24 | RParen 25 | LBrace 26 | RBrace 27 | LBracket 28 | RBracket 29 | Eq 30 | Colon 31 | Arrow 32 | Comma 33 | Dot 34 | 35 | Plus 36 | Minus 37 | Star 38 | Slash 39 | EqEq 40 | Neq 41 | GreaterEq 42 | LessEq 43 | Bar 44 | RPipe 45 | Greater 46 | Less 47 | Bang 48 | Caret 49 | Question 50 | 51 | Ident(String) 52 | String(String) 53 | Number(Float) 54 | 55 | Let 56 | In 57 | Match 58 | Is 59 | Try 60 | Throw 61 | If 62 | Then 63 | Else 64 | Or 65 | And 66 | Import 67 | As 68 | True 69 | False 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bella programming language 2 | 3 | ![Bella language logo](assets/Bella-language.png) 4 | 5 | Bella is a simple functional programming language, written in [Gleam](https://gleam.run). Still very much a work-in-progress. 🚧 6 | 7 | ## Using Bella 8 | 9 | [Download](https://github.com/MystPi/bella/releases) the appropriate executable for your system and add it to PATH, then you can: 10 | 11 | - Try out the [examples](/examples/) 12 | ```sh 13 | bella ./examples/hello_world.bella 14 | ``` 15 | - Create a project 16 | ```sh 17 | bella create my_project 18 | cd my_project 19 | bella run 20 | ``` 21 | 22 | ## Developing 23 | 24 | You will need: 25 | 26 | - Gleam 27 | - Deno _(This is Gleam's target runtime)_ 28 | - This repo 29 | 30 | ```sh 31 | gleam run ./examples/hello_world.bella 32 | ``` 33 | You can also compile an executable: 34 | 35 | ```sh 36 | # Build 37 | gleam clean && gleam build 38 | 39 | # Compile 40 | deno compile -o ./build/bella --unstable --allow-all ./build/dev/javascript/bella/main.ts 41 | 42 | # Run 43 | ./build/bella ./examples/countdown.bella 44 | ``` 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 MystPi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build executable 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | target: 14 | [ 15 | x86_64-unknown-linux-gnu, 16 | x86_64-pc-windows-msvc, 17 | x86_64-apple-darwin, 18 | aarch64-apple-darwin, 19 | ] 20 | 21 | steps: 22 | - name: Setup repo 23 | uses: actions/checkout@v3 24 | 25 | - name: Setup Deno 26 | uses: denoland/setup-deno@v1.1.2 27 | with: 28 | deno-version: v1.x 29 | 30 | - name: Setup Gleam 31 | uses: erlef/setup-beam@v1 32 | with: 33 | otp-version: false 34 | gleam-version: '0.32.1' 35 | 36 | - name: Build project 37 | run: gleam clean && gleam build 38 | 39 | - name: Compile executable 40 | run: deno compile -o ./build/bella-${{ matrix.target }} --unstable --allow-all --target ${{ matrix.target }} ./build/dev/javascript/bella/main.ts 41 | 42 | - name: List files 43 | run: ls build 44 | 45 | - name: Upload binaries to release 46 | uses: svenstaro/upload-release-action@v2 47 | with: 48 | repo_token: ${{ secrets.GITHUB_TOKEN }} 49 | file_glob: true 50 | file: ./build/bella-${{ matrix.target }}* 51 | tag: nightly 52 | overwrite: true 53 | body: 'These executables are updated to the latest commit.' 54 | -------------------------------------------------------------------------------- /src/grammar.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ``` 4 | module := import* expr+ 5 | 6 | import := 'import' Ident ( '/' Ident )* ( 'as' Ident )? 7 | 8 | expr := 9 | | pipe 10 | | lambda 11 | | let_expr 12 | | if_expr 13 | | throw_expr 14 | | try_expr 15 | | match_expr 16 | 17 | lambda := Ident? '->' expr 18 | let_expr := 'let' ( pattern '=' expr 'in' )+ expr 19 | if_expr := 'if' expr 'then' expr 'else' expr 20 | throw_expr := 'throw' expr 21 | try_expr := 'try' expr 'else' expr 22 | match_expr := 'match' match_clause+ 23 | match_clause := 'is' pattern 'then' expr 24 | 25 | pipe := logic_or ( '|>' logic_or ) * 26 | logic_or := logic_and ( 'or' logic_and )* 27 | logic_and := equality ( 'and' equality )* 28 | equality := comparison ( ( '==' | '!=' ) comparison )* 29 | comparison := term ( ( '>' | '>=' | '<' | '<=' ) term )* 30 | term := factor ( ( '+' | '-' ) factor )* 31 | factor := named_pat ( ( '/' | '*' ) named_pat )* 32 | named_pat := unary ( 'as' ident )? 33 | unary := ( '-' | '!' | '^' | '?' ) unary | call 34 | call := primary ( '(' comma_sep? ')' | '.' Ident )* 35 | 36 | // Pattern is validated after parse 37 | pattern := named_pat 38 | 39 | primary := 40 | | Ident 41 | | Number 42 | | String 43 | | bool 44 | | block 45 | | record 46 | | list 47 | 48 | record := '{' comma_sep? '}' 49 | record_item := Ident ( ':' expr )? 50 | list := '[' comma_sep? ('|' ident)? ']' 51 | block := '(' expr+ ')' 52 | bool := 'true' | 'false' 53 | 54 | comma_sep := r ( ',' r )* 55 | ``` 56 | -------------------------------------------------------------------------------- /manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "gleam_community_ansi", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_community_colour"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8B5A9677BC5A2738712BBAF2BA289B1D8195FDF962BBC769569976AD5E9794E1" }, 6 | { name = "gleam_community_colour", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "036C206886AFB9F153C552700A7A0B4D2864E3BC96A20C77E5F34A013C051BE3" }, 7 | { name = "gleam_json", version = "0.7.0", build_tools = ["gleam"], requirements = ["thoas", "gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "CB405BD93A8828BCD870463DE29375E7B2D252D9D124C109E5B618AAC00B86FC" }, 8 | { name = "gleam_stdlib", version = "0.32.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "ABF00CDCCB66FABBCE351A50060964C4ACE798F95A0D78622C8A7DC838792577" }, 9 | { name = "hug", version = "0.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_community_ansi"], otp_app = "hug", source = "hex", outer_checksum = "65D6F35877036EFCFE6DE0654A16C19B943C5DE804275EC764C16B26219A3377" }, 10 | { name = "simplifile", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "8F3C94B83F691CCFACD784A4D7C1F7E5A0437D93341549B908EE3B32E3477447" }, 11 | { name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" }, 12 | ] 13 | 14 | [requirements] 15 | gleam_community_ansi = { version = "~> 1.1" } 16 | gleam_json = { version = "~> 0.5" } 17 | gleam_stdlib = { version = "~> 0.29" } 18 | hug = { version = "~> 0.1" } 19 | simplifile = { version = "~> 0.1" } 20 | -------------------------------------------------------------------------------- /src/bella/error.gleam: -------------------------------------------------------------------------------- 1 | import gleam 2 | import gleam/io 3 | import hug 4 | import gleam_community/ansi 5 | import bella/lexer/token 6 | 7 | // TYPES ....................................................................... 8 | 9 | pub type Error { 10 | SyntaxError(String, token.Span) 11 | RuntimeError(String) 12 | RuntimeErrorPos(String, token.Span) 13 | ImportedError(Error, String, String) 14 | } 15 | 16 | // CONSTRUCTORS ................................................................ 17 | 18 | pub fn syntax_error(msg: String, pos: token.Span) { 19 | gleam.Error(SyntaxError(msg, pos)) 20 | } 21 | 22 | pub fn runtime_error(msg: String) { 23 | gleam.Error(RuntimeError(msg)) 24 | } 25 | 26 | pub fn runtime_error_pos(msg: String, pos: token.Span) { 27 | gleam.Error(RuntimeErrorPos(msg, pos)) 28 | } 29 | 30 | pub fn imported_error(err: Error, source: String, path: String) { 31 | gleam.Error(ImportedError(err, source, path)) 32 | } 33 | 34 | // UTILS ....................................................................... 35 | 36 | pub fn print_error(err: Error, source: String, path: String) -> Nil { 37 | case err { 38 | ImportedError(err, source, path2) -> { 39 | { "↓ imported from " <> ansi.italic(path) } 40 | |> ansi.dim 41 | |> io.println_error 42 | print_error(err, source, path2) 43 | } 44 | _ -> print_error_message(err, source, path) 45 | } 46 | } 47 | 48 | pub fn print_error_message(err: Error, source: String, path: String) -> Nil { 49 | case err { 50 | SyntaxError(msg, span) -> 51 | ansi.red("✕ ") <> hug.error( 52 | source, 53 | in: path, 54 | from: #(span.from.line, span.from.col), 55 | to: #(span.to.line, span.to.col), 56 | message: "invalid syntax", 57 | hint: ansi.blue("? ") <> msg, 58 | ) 59 | RuntimeErrorPos(msg, span) -> 60 | ansi.red("✕ ") <> hug.error( 61 | source, 62 | in: path, 63 | from: #(span.from.line, span.from.col), 64 | to: #(span.to.line, span.to.col), 65 | message: "runtime error", 66 | hint: ansi.blue("? ") <> msg, 67 | ) 68 | RuntimeError(msg) -> 69 | ansi.red("✕ runtime error") <> " in " <> ansi.blue(path) <> "\n" <> ansi.blue( 70 | "? ", 71 | ) <> msg 72 | _ -> "unknown error" 73 | } 74 | |> io.println_error 75 | } 76 | -------------------------------------------------------------------------------- /src/bella/evaluator/types.gleam: -------------------------------------------------------------------------------- 1 | import gleam/map 2 | import gleam/list 3 | import gleam/float 4 | import gleam/int 5 | import gleam/string 6 | import bella/error 7 | import bella/parser 8 | import bella/utils 9 | import bella/lexer/token 10 | 11 | // TYPES ....................................................................... 12 | 13 | pub type DataType { 14 | Number(Float) 15 | String(String) 16 | Bool(Bool) 17 | Record(fields: map.Map(String, DataType)) 18 | List(List(DataType)) 19 | Lambda(param: String, body: parser.Expr, closure: Scope) 20 | Lambda0(body: parser.Expr, closure: Scope) 21 | Function(func: fn(DataType, Scope, token.Span) -> Evaluated) 22 | } 23 | 24 | pub type Scope = 25 | map.Map(String, DataType) 26 | 27 | pub type Evaluated = 28 | Result(#(DataType, Scope), error.Error) 29 | 30 | // UTILS ....................................................................... 31 | 32 | pub fn to_string(x: DataType) -> String { 33 | case x { 34 | Number(n) -> 35 | case float.floor(n) == n { 36 | True -> int.to_string(float.truncate(n)) 37 | False -> float.to_string(n) 38 | } 39 | String(s) -> s 40 | Bool(b) -> 41 | case b { 42 | True -> "true" 43 | False -> "false" 44 | } 45 | Record(f) -> record_to_string(f) 46 | List(l) -> list_to_string(l) 47 | Lambda(param, ..) -> "#lambda<" <> param <> ">" 48 | Lambda0(..) -> "#lambda<>" 49 | Function(..) -> "#function" 50 | } 51 | } 52 | 53 | pub fn inspect(x: DataType) -> String { 54 | case x { 55 | String(s) -> utils.stringify(s) 56 | _ -> to_string(x) 57 | } 58 | } 59 | 60 | fn list_to_string(items: List(DataType)) -> String { 61 | let items = 62 | items 63 | |> list.map(inspect) 64 | |> string.join(", ") 65 | 66 | "[" <> items <> "]" 67 | } 68 | 69 | fn record_to_string(fields: map.Map(String, DataType)) -> String { 70 | let fields = 71 | fields 72 | |> map.to_list 73 | |> list.map(fn(field) { 74 | let #(name, value) = field 75 | name <> ": " <> inspect(value) 76 | }) 77 | |> string.join(", ") 78 | 79 | "{ " <> fields <> " }" 80 | } 81 | 82 | pub fn to_type(x: DataType) -> String { 83 | case x { 84 | Number(..) -> "number" 85 | String(..) -> "string" 86 | Bool(..) -> "boolean" 87 | Record(..) -> "record" 88 | List(..) -> "list" 89 | Lambda(..) | Lambda0(..) -> "lambda" 90 | Function(..) -> "function" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/bella/evaluator/functions.gleam: -------------------------------------------------------------------------------- 1 | import gleam/io 2 | import gleam/map 3 | import bella/error 4 | import bella/utils 5 | import bella/evaluator/types.{Bool, Function, List, Record, String} 6 | 7 | pub fn functions() { 8 | let bella_io = 9 | [ 10 | #("println", Function(println)), 11 | #("print", Function(print)), 12 | #("readline", Function(readline)), 13 | ] 14 | |> map.from_list 15 | |> Record 16 | 17 | let bella_types = 18 | [ 19 | #("to_string", Function(to_string)), 20 | #("typeof", Function(typeof_)), 21 | #("inspect", Function(inspect)), 22 | #("is_type", Function(is_type)), 23 | ] 24 | |> map.from_list 25 | |> Record 26 | 27 | let bella_list = 28 | [#("first", Function(first)), #("rest", Function(rest))] 29 | |> map.from_list 30 | |> Record 31 | 32 | [#("Io", bella_io), #("Types", bella_types), #("List", bella_list)] 33 | } 34 | 35 | // IO .......................................................................... 36 | 37 | fn println(x, scope, _) { 38 | io.println(types.to_string(x)) 39 | Ok(#(x, scope)) 40 | } 41 | 42 | fn print(x, scope, _) { 43 | io.print(types.to_string(x)) 44 | Ok(#(x, scope)) 45 | } 46 | 47 | fn readline(message, scope, _) { 48 | let result = utils.readline(types.to_string(message)) 49 | Ok(#(String(result), scope)) 50 | } 51 | 52 | // TYPES ....................................................................... 53 | 54 | fn to_string(x, scope, _) { 55 | Ok(#(String(types.to_string(x)), scope)) 56 | } 57 | 58 | fn typeof_(x, scope, _) { 59 | Ok(#(String(types.to_type(x)), scope)) 60 | } 61 | 62 | fn inspect(x, scope, _) { 63 | Ok(#(String(types.inspect(x)), scope)) 64 | } 65 | 66 | fn is_type(t, scope, _) { 67 | use x, _, pos <- nest_function(scope) 68 | 69 | case t { 70 | String(t) -> Ok(#(Bool(types.to_type(x) == t), scope)) 71 | _ -> error.runtime_error_pos("Type must be a string", pos) 72 | } 73 | } 74 | 75 | // LIST ........................................................................ 76 | 77 | fn first(x, scope, pos) { 78 | case x { 79 | List([first, ..]) -> Ok(#(first, scope)) 80 | List([]) -> 81 | error.runtime_error_pos("I got an empty list; there is no `first`", pos) 82 | _ -> 83 | error.runtime_error_pos( 84 | "I expected a list; instead got a " <> types.to_type(x), 85 | pos, 86 | ) 87 | } 88 | } 89 | 90 | fn rest(x, scope, pos) { 91 | case x { 92 | List([_, ..rest]) -> Ok(#(List(rest), scope)) 93 | List([]) -> 94 | error.runtime_error_pos("I got an empty list; there is no `rest`", pos) 95 | _ -> 96 | error.runtime_error_pos( 97 | "I expected a list; instead got a " <> types.to_type(x), 98 | pos, 99 | ) 100 | } 101 | } 102 | 103 | // UTILS ....................................................................... 104 | 105 | fn nest_function(scope, func) { 106 | Ok(#(Function(func), scope)) 107 | } 108 | -------------------------------------------------------------------------------- /src/bella.gleam: -------------------------------------------------------------------------------- 1 | import gleam/io 2 | import gleam/list 3 | import gleam/string 4 | import gleam/int 5 | import gleam/result.{try} 6 | import gleam_community/ansi 7 | import simplifile 8 | import bella/evaluator 9 | import bella/error 10 | import bella/utils 11 | import bella/project 12 | 13 | const usage = [ 14 | #("create ", "Create a new project"), 15 | #("run", "Run the current project"), 16 | #("", "Run the given file"), 17 | ] 18 | 19 | pub fn main() { 20 | case utils.get_args(), get_project() { 21 | _, #(True, Error(msg)) -> error(msg) 22 | ["create", name, ..], _ -> create_project(name) 23 | ["run", ..], #(True, Ok(project)) -> run_project(project.name) 24 | [path, ..], _ -> run_file(path) 25 | _, _ -> print_usage() 26 | } 27 | } 28 | 29 | fn print_usage() -> Nil { 30 | let assert Ok(max_len) = 31 | usage 32 | |> list.map(fn(x) { string.length(x.0) }) 33 | |> list.sort(int.compare) 34 | |> list.last 35 | 36 | io.println("Usage:") 37 | 38 | usage 39 | |> list.each(fn(x) { 40 | let #(command, desc) = x 41 | let left_over = max_len - string.length(command) + 2 42 | io.println(" bella " <> command <> string.repeat(" ", left_over) <> desc) 43 | }) 44 | } 45 | 46 | fn get_project() -> #(Bool, Result(project.Project, String)) { 47 | case simplifile.read("bella.json") { 48 | Ok(contents) -> 49 | case project.decode(contents) { 50 | Ok(project) -> #(True, Ok(project)) 51 | _ -> #(True, Error("Invalid bella.json")) 52 | } 53 | _ -> #(False, Error("Not in a project")) 54 | } 55 | } 56 | 57 | fn run_project(name: String) -> Nil { 58 | run_file("src/" <> name <> ".bella") 59 | } 60 | 61 | fn run_file(path: String) -> Nil { 62 | case simplifile.read(path) { 63 | Ok(contents) -> run_str(contents, path) 64 | _ -> error("I couldn't find the requested file: " <> path) 65 | } 66 | } 67 | 68 | fn run_str(str: String, path: String) -> Nil { 69 | case evaluator.evaluate_str(str) { 70 | Error(err) -> error.print_error(err, str, path) 71 | _ -> Nil 72 | } 73 | } 74 | 75 | fn create_project(name: String) -> Nil { 76 | case create_project_files(name) { 77 | Ok(_) -> success("Created project in ./" <> name <> "/") 78 | _ -> error("Failed to create project") 79 | } 80 | } 81 | 82 | fn create_project_files(name: String) -> Result(Nil, simplifile.FileError) { 83 | use _ <- try(simplifile.create_directory_all("./" <> name <> "/src")) 84 | use _ <- try(simplifile.write( 85 | project.init(name), 86 | "./" <> name <> "/bella.json", 87 | )) 88 | simplifile.write( 89 | "Io.println('Hello, world!')", 90 | "./" <> name <> "/src/" <> name <> ".bella", 91 | ) 92 | } 93 | 94 | // UTILS ....................................................................... 95 | 96 | fn error(msg: String) -> Nil { 97 | io.println_error(ansi.red("✕ ") <> msg) 98 | } 99 | 100 | fn success(msg: String) -> Nil { 101 | io.println(ansi.green("✓ ") <> msg) 102 | } 103 | -------------------------------------------------------------------------------- /docs/language.md: -------------------------------------------------------------------------------- 1 | # Language Tour 2 | 3 | > *Note: this document is not completely up-to-date, most notably with the addition of pattern matching. The examples should still work, but not every aspect of Bella is covered.* 4 | 5 | _[back to index](README.md)_ 6 | 7 | This document gives a quick overview of Bella's features. Basic knowledge of functional programming and programming in general is assumed. 8 | 9 | ## Hello World 10 | 11 | ```bella 12 | Io.println("Hello world!") 13 | ``` 14 | 15 | ## Comments 16 | 17 | _[back to top](#language-tour)_ 18 | 19 | Comments are prefixed with a semicolon and continue until the end of the line. 20 | 21 | ```bella 22 | ; This is a comment 23 | ``` 24 | 25 | There are no multi-line comments in Bella. 26 | 27 | ## Values 28 | 29 | _[back to top](#language-tour)_ 30 | 31 | The types of values Bella supports can be divided into two main categories: simple and complex. Simple values behave the same as in most programming languages. 32 | 33 | - Strings: `"hello there"`, `'good bye'` 34 | - Numbers: `5`, `42`, `3.14` 35 | - Booleans: `true`, `false` 36 | 37 | Complex values are, well, more complex, and can contain other values and [expressions](#expressions). 38 | 39 | - [Lists](#lists): `[1, 2, 3, 4]`, `["a", true, 3]` 40 | - [Records](#records): `{ name: "Bella", type: "dog" }` 41 | - [Lambdas](#lambdas): `x -> Io.print(x)`, `a -> b -> a + b` 42 | 43 | ## Operators 44 | 45 | _[back to top](#language-tour)_ 46 | 47 | - Arithemetic: `+`, `-`, `*`, `/` 48 | - Equality: `==`, `!=` 49 | - Comparison: `>`, `<`, `>=`, `<=` 50 | - Logical: `and`, `or` 51 | - Unary: `!` (not), `-` (negation) 52 | - Record access: `.` 53 | - Pipe: `|>` 54 | 55 | | Precendence | Operator(s) | 56 | | ----------- | ----------------- | 57 | | 9 (highest) | `.` | 58 | | 8 | `-` `!` (unary) | 59 | | 7 | `*` `/` | 60 | | 6 | `+` `-` | 61 | | 5 | `>` `<` `>=` `<=` | 62 | | 4 | `==` `!=` | 63 | | 3 | `and` | 64 | | 2 | `or` | 65 | | 1 (lowest) | `\|>` | 66 | 67 | ## Variables (`let ... in`) 68 | 69 | _[back to top](#language-tour)_ 70 | 71 | In Bella, variables (also known as _bindings_) are _immutable_; that is, they cannot be modified, reassigned, or changed in any way. The `let ... in` expression creates a variable for use in its body: 72 | 73 | ```bella 74 | let my_name = "MystPi" in 75 | 76 | Io.println(my_name) 77 | ``` 78 | 79 | Multiple variables can be defined at once: 80 | 81 | ```bella 82 | let 83 | x = 1 84 | y = x + 1 85 | z = y * 2 86 | cool = true 87 | in 88 | 89 | [x, y, z, cool] 90 | ``` 91 | 92 | The body of a `let` expression comes after the `in` keyword. The body is a **single** expression: 93 | 94 | ``` 95 | let x = 5 in 96 | 97 | ; Prints `5` 98 | Io.println(x) 99 | 100 | ; Error! There is no binding for `x` 101 | Io.println(x) 102 | ``` 103 | 104 | > This can be made clearer with indentation: 105 | > 106 | > ```bella 107 | > let x = 5 in 108 | > Io.println(x) 109 | > 110 | > ; Error! There is no binding for `x` 111 | > Io.println(x) 112 | > ``` 113 | 114 | > [As we will soon see](#expressions), parenthesis can be used to group multiple expressions into one. 115 | 116 | ## Lists 117 | 118 | _[back to top](#language-tour)_ 119 | 120 | A list is a sequence of values, wrapped in square brackets. 121 | 122 | ``` 123 | ["egg", "cheese", "lettuce", "tomato"] 124 | ``` 125 | 126 | Unlike some other programming languages, **any** combination of values can be in a list. They do not all have to be the same type. 127 | 128 | ``` 129 | [2, true, "foo", [x, y, z], 4.5] 130 | ``` 131 | 132 | Lists are immutable and cannot have items added or removed. The `+` operator helps with this by being able to concatenate two lists, returning a new one. 133 | 134 | ``` 135 | [1, 2, 3] + [4, 5, 6] 136 | ; => [1, 2, 3, 4, 5, 6] 137 | ``` 138 | 139 | > _This section is a work in progress_ 140 | 141 | ## Records 142 | 143 | _[back to top](#language-tour)_ 144 | 145 | A record is a collection of keys and values, also known as *fields*. The `.` operator is used to access a field. 146 | 147 | ```bella 148 | let bella = 149 | { name: "Bella" 150 | , type: "dog" 151 | , living: false 152 | } 153 | in 154 | 155 | bella.name 156 | ; => "Bella" 157 | ``` 158 | 159 | Records, like everything in Bella, are immutable—a record's fields cannot be changed. However, the `+` operator can merge two records and return the combined one. 160 | 161 | ```bella 162 | let 163 | first = 164 | { x: 1, y: 0 } 165 | 166 | second = 167 | { y: 2, z: 3} 168 | in 169 | 170 | first + second 171 | ; => { x: 1, y: 2, z: 3 } 172 | ``` 173 | 174 | > Tip: there is a helpful shorthand for writing record fields like `name: name` or `value: value`. In those cases, you can omit both the colon and the variable name: 175 | > 176 | > ```bella 177 | > { name 178 | > , value 179 | > , foo: true 180 | > } 181 | > ``` 182 | 183 | ## Lambdas 184 | 185 | _[back to top](#language-tour)_ 186 | 187 | Lambdas are expressions that can be called with an argument and return a value. 188 | 189 | ``` 190 | x -> x * x 191 | ``` 192 | 193 | Declaring a function in Bella does not use special syntax. Instead, you assign a lambda expression to new variable. 194 | 195 | ```bella 196 | let 197 | add_and_print = 198 | a -> 199 | Io.println(a + 100) 200 | in 201 | 202 | add_and_print(5) 203 | ``` 204 | 205 | Lambdas can have 0 or 1 parameters; no more, no less. 206 | 207 | ``` 208 | let 209 | calculate_magic_number = 210 | ; 0 parameters 211 | -> 42 212 | 213 | calculate_extra_magic_number = 214 | ; 1 parameter 215 | x -> 216 | x * calculate_magic_number() 217 | in 218 | 219 | calculate_extra_magic_number(25) ; => 1050 220 | ``` 221 | 222 | ``` 223 | let 224 | add = 225 | a -> b -> a + b 226 | 227 | add_100 = add(100) 228 | in 229 | 230 | add_100(50) ; => 150 231 | ``` 232 | 233 | However, since lambda calls are automatically [_curried_](https://en.wikipedia.org/wiki/Currying), they have the appearance of being able to accept multiple arguments. The following two calls are equivalent: 234 | 235 | ``` 236 | do_something(a, b, c) 237 | ``` 238 | 239 | ``` 240 | do_something(a)(b)(c) 241 | ``` 242 | 243 | The `|>` (pipe) operator is very useful for cleaning up convoluted calls like this one: 244 | 245 | ```bella 246 | Io.println(List.first(parse(lex(read_file(file))))) 247 | ``` 248 | 249 | It takes the expression on the left and calls the lambda on the right with it. The example above can be rewritten with the pipe operator like so: 250 | 251 | ```bella 252 | file 253 | |> read_file 254 | |> lex 255 | |> parse 256 | |> List.first 257 | |> Io.println 258 | ``` 259 | 260 | Ah, much better! 261 | 262 | > If you are confused, consider this smaller example: 263 | > 264 | > ```bella 265 | > "Hello" |> Types.typeof |> Io.println 266 | > ; that is the same as writing 267 | > Io.println(Types.typeof("Hello")) 268 | > ``` 269 | 270 | ## Control Flow: `if` and `try` 271 | 272 | _[back to top](#language-tour)_ 273 | 274 | There are only two control flow expressions: `if` and `try`. Loops, such as `while` or `for` in other languages, are accomplished via recursion in Bella. 275 | 276 | ### `if` 277 | 278 | The `if` expression takes a Boolean condition and determines which branch to return depending on if the condition evaluates to `true`. 279 | 280 | ```bella 281 | if 1 + 1 == 2 then 282 | "It's two!" 283 | else 284 | "Math must be broken" 285 | 286 | ; => "It's two!" 287 | ``` 288 | 289 | Every `if` **must** be followed by an `else`. Multiple `if`-`else`s can be chained together: 290 | 291 | ```bella 292 | if a then 293 | ; ... 294 | else if b then 295 | ; ... 296 | else if c then 297 | ; ... 298 | else 299 | ; ... 300 | ``` 301 | 302 | ### `try` (and `throw`) 303 | 304 | The `try` expression handles runtime errors. 305 | 306 | ```bella 307 | try 308 | ; Throws a 'cannot be called' error 309 | 5() 310 | else 311 | "There was an error!" 312 | ``` 313 | 314 | Like `if`, `try` must always be followed by an `else`. If the `else` expression is a lambda literal, it will be immediately called with the error message: 315 | 316 | ```bella 317 | try 318 | 1 + "2" 319 | else msg -> 320 | Io.println(msg) 321 | 322 | ; Prints "Operands of + must be..." 323 | ``` 324 | 325 | An error can be manually thrown with `throw`. 326 | 327 | ```bella 328 | throw "Oops!" 329 | ``` 330 | 331 | ```bella 332 | try 333 | throw "I am being thrown..." 334 | else msg -> 335 | msg + " Now I'm caught!" 336 | 337 | ; => "I am being thrown... Now I'm caught!" 338 | ``` 339 | 340 | ## Expressions 341 | 342 | _[back to top](#language-tour)_ 343 | 344 | I have been using the term 'expression' quite a lot. But what _is_ an expression? 345 | 346 | Quite simply, an expression is a bit of code that returns a value. `42`, `"hello"`, `5 * 6 + 3`, and `do_something(blah)` are all expressions. Even `if`, `try`, and `let` are expressions since they return values as well: 347 | 348 | ```bella 349 | if true then 5 else 6 350 | ; => 5 351 | ``` 352 | 353 | ```bella 354 | Io.println( 355 | try 356 | throw "Pointless throw" 357 | else msg -> msg 358 | ) 359 | 360 | ; => "Pointless throw" 361 | ``` 362 | 363 | ``` 364 | "Hello, " + ( 365 | let 366 | your_name = Io.readline("What is your name?") 367 | in 368 | 369 | if your_name == "MystPi" then 370 | "weirdo" 371 | else 372 | your_name 373 | ) + "!" 374 | ``` 375 | 376 | Parenthesis can be used to group multiple expressions into one. The last expression inside the parenthesis gets returned. 377 | 378 | ```bella 379 | (1 2 3) 380 | ; => 3 381 | ``` 382 | 383 | ```bella 384 | let x = 5 in 385 | ( 386 | x + 1 |> Io.println 387 | x - 2 |> Io.println 388 | x * 3 |> Io.println 389 | ) 390 | ``` 391 | 392 | They are also quite useful for grouping math operations: 393 | 394 | ```bella 395 | 1 + 2 * 3 396 | ; => 7 397 | 398 | (1 + 2) * 3 399 | ; => 9 400 | ``` 401 | -------------------------------------------------------------------------------- /src/bella/lexer.gleam: -------------------------------------------------------------------------------- 1 | // Heavily inspired by https://github.com/DanielleMaywood/glexer 2 | // Thank you very much! 3 | 4 | import gleam/list 5 | import gleam/result.{try} 6 | import gleam/int 7 | import gleam/float 8 | import gleam/string 9 | import bella/error 10 | import bella/utils 11 | import bella/lexer/token.{ 12 | type Position, type Span, type Token, type TokenType, type Tokens, Position, 13 | Span, 14 | } 15 | 16 | // TYPES ....................................................................... 17 | 18 | pub type LexResult = 19 | Result(#(String, Position, Token), error.Error) 20 | 21 | // LEXER ....................................................................... 22 | 23 | pub fn lex(str: String) -> Result(Tokens, error.Error) { 24 | use tokens <- try(lex_(str, Position(1, 1), [])) 25 | 26 | tokens 27 | |> list.reverse 28 | |> Ok 29 | } 30 | 31 | fn lex_( 32 | input: String, 33 | pos: Position, 34 | tokens: Tokens, 35 | ) -> Result(Tokens, error.Error) { 36 | case next(input, pos) { 37 | Ok(#(_, _, #(token.Eof, _) as token)) -> Ok([token, ..tokens]) 38 | Ok(#(rest, pos, token)) -> lex_(rest, pos, [token, ..tokens]) 39 | Error(err) -> Error(err) 40 | } 41 | } 42 | 43 | fn next(input: String, pos: Position) -> LexResult { 44 | case input { 45 | // End of file 46 | "" -> token("", pos, token.Eof, 0) 47 | 48 | // Newline 49 | "\r\n" <> rest | "\n" <> rest -> next(rest, advance_line(pos)) 50 | 51 | // Whitespace 52 | " " <> rest | "\t" <> rest -> next(rest, advance(pos, 1)) 53 | 54 | // Comment 55 | ";" <> rest -> comment(rest, advance(pos, 1)) 56 | 57 | // Grouping 58 | "(" <> rest -> token(rest, pos, token.LParen, 1) 59 | ")" <> rest -> token(rest, pos, token.RParen, 1) 60 | "{" <> rest -> token(rest, pos, token.LBrace, 1) 61 | "}" <> rest -> token(rest, pos, token.RBrace, 1) 62 | "[" <> rest -> token(rest, pos, token.LBracket, 1) 63 | "]" <> rest -> token(rest, pos, token.RBracket, 1) 64 | 65 | // Punctuation 66 | "|>" <> rest -> token(rest, pos, token.RPipe, 2) 67 | "|" <> rest -> token(rest, pos, token.Bar, 1) 68 | "->" <> rest -> token(rest, pos, token.Arrow, 2) 69 | "==" <> rest -> token(rest, pos, token.EqEq, 2) 70 | "!=" <> rest -> token(rest, pos, token.Neq, 2) 71 | ">=" <> rest -> token(rest, pos, token.GreaterEq, 2) 72 | "<=" <> rest -> token(rest, pos, token.LessEq, 2) 73 | "," <> rest -> token(rest, pos, token.Comma, 1) 74 | "." <> rest -> token(rest, pos, token.Dot, 1) 75 | "=" <> rest -> token(rest, pos, token.Eq, 1) 76 | ":" <> rest -> token(rest, pos, token.Colon, 1) 77 | "+" <> rest -> token(rest, pos, token.Plus, 1) 78 | "-" <> rest -> token(rest, pos, token.Minus, 1) 79 | "*" <> rest -> token(rest, pos, token.Star, 1) 80 | "/" <> rest -> token(rest, pos, token.Slash, 1) 81 | ">" <> rest -> token(rest, pos, token.Greater, 1) 82 | "<" <> rest -> token(rest, pos, token.Less, 1) 83 | "!" <> rest -> token(rest, pos, token.Bang, 1) 84 | "^" <> rest -> token(rest, pos, token.Caret, 1) 85 | "?" <> rest -> token(rest, pos, token.Question, 1) 86 | 87 | // String 88 | "\"" <> rest -> string(rest, pos, "", "\"") 89 | "'" <> rest -> string(rest, pos, "", "'") 90 | 91 | // Number 92 | "0" <> _ 93 | | "1" <> _ 94 | | "2" <> _ 95 | | "3" <> _ 96 | | "4" <> _ 97 | | "5" <> _ 98 | | "6" <> _ 99 | | "7" <> _ 100 | | "8" <> _ 101 | | "9" <> _ -> number(input, pos, "", IntMode) 102 | 103 | _ -> 104 | case string.pop_grapheme(input) { 105 | Ok(#(c, rest)) -> 106 | case is_alpha(c) { 107 | True -> ident(rest, pos, c) 108 | False -> 109 | error.syntax_error( 110 | "I don't understand what this means", 111 | pos_span(pos, 1), 112 | ) 113 | } 114 | _ -> next(input, pos) 115 | } 116 | } 117 | } 118 | 119 | fn pos_span(pos: Position, span: Int) -> Span { 120 | Span(from: pos, to: Position(pos.line, pos.col + span)) 121 | } 122 | 123 | fn advance_line(pos: Position) -> Position { 124 | Position(line: pos.line + 1, col: 1) 125 | } 126 | 127 | fn advance(pos: Position, offset: Int) -> Position { 128 | Position(line: pos.line, col: pos.col + offset) 129 | } 130 | 131 | fn token( 132 | input: String, 133 | pos: Position, 134 | token: TokenType, 135 | length: Int, 136 | ) -> LexResult { 137 | Ok(#(input, advance(pos, length), #(token, pos_span(pos, length)))) 138 | } 139 | 140 | fn comment(input: String, pos: Position) -> LexResult { 141 | case input { 142 | "\r\n" <> rest | "\n" <> rest -> next(rest, advance_line(pos)) 143 | 144 | source -> 145 | case string.pop_grapheme(source) { 146 | Ok(#(_, rest)) -> comment(rest, advance(pos, 1)) 147 | _ -> next(source, pos) 148 | } 149 | } 150 | } 151 | 152 | fn string( 153 | input: String, 154 | start: Position, 155 | content: String, 156 | delim: String, 157 | ) -> LexResult { 158 | case input { 159 | "\\" <> rest -> 160 | case string.pop_grapheme(rest) { 161 | Ok(#(c, rest)) -> string(rest, start, content <> "\\" <> c, delim) 162 | _ -> string(rest, start, content <> "\\", delim) 163 | } 164 | 165 | "\r\n" <> _ | "\n" <> _ -> 166 | error.syntax_error( 167 | "Unterminated string", 168 | pos_span(start, string.length(content) + 2), 169 | ) 170 | 171 | _ -> 172 | case string.pop_grapheme(input) { 173 | Ok(#(c, rest)) if c == delim -> 174 | token( 175 | rest, 176 | start, 177 | token.String( 178 | content 179 | |> utils.unescape, 180 | ), 181 | string.length(content) + 2, 182 | ) 183 | 184 | Ok(#(c, rest)) -> string(rest, start, content <> c, delim) 185 | 186 | _ -> 187 | error.syntax_error( 188 | "Unterminated string", 189 | pos_span(start, string.length(content) + 2), 190 | ) 191 | } 192 | } 193 | } 194 | 195 | type NumberMode { 196 | IntMode 197 | DotNoDecimal 198 | DotDecimal 199 | } 200 | 201 | fn decide(mode: NumberMode) -> NumberMode { 202 | case mode { 203 | IntMode -> IntMode 204 | DotNoDecimal -> DotDecimal 205 | DotDecimal -> DotDecimal 206 | } 207 | } 208 | 209 | fn number( 210 | input: String, 211 | start: Position, 212 | content: String, 213 | mode: NumberMode, 214 | ) -> LexResult { 215 | case input { 216 | "." <> rest if mode == IntMode -> 217 | number(rest, start, content <> ".", DotNoDecimal) 218 | 219 | "0" <> rest -> number(rest, start, content <> "0", decide(mode)) 220 | "1" <> rest -> number(rest, start, content <> "1", decide(mode)) 221 | "2" <> rest -> number(rest, start, content <> "2", decide(mode)) 222 | "3" <> rest -> number(rest, start, content <> "3", decide(mode)) 223 | "4" <> rest -> number(rest, start, content <> "4", decide(mode)) 224 | "5" <> rest -> number(rest, start, content <> "5", decide(mode)) 225 | "6" <> rest -> number(rest, start, content <> "6", decide(mode)) 226 | "7" <> rest -> number(rest, start, content <> "7", decide(mode)) 227 | "8" <> rest -> number(rest, start, content <> "8", decide(mode)) 228 | "9" <> rest -> number(rest, start, content <> "9", decide(mode)) 229 | 230 | _ -> 231 | case mode { 232 | IntMode | DotDecimal -> 233 | token( 234 | input, 235 | start, 236 | token.Number(to_float(content)), 237 | string.length(content), 238 | ) 239 | 240 | DotNoDecimal -> 241 | error.syntax_error( 242 | "Expected decimal after decimal point", 243 | pos_span(start, string.length(content)), 244 | ) 245 | } 246 | } 247 | } 248 | 249 | fn ident(input: String, start: Position, c: String) -> LexResult { 250 | let #(name, rest) = take_content(input, c, is_alphanum) 251 | 252 | let tok = case name { 253 | "let" -> token.Let 254 | "in" -> token.In 255 | "match" -> token.Match 256 | "is" -> token.Is 257 | "try" -> token.Try 258 | "throw" -> token.Throw 259 | "if" -> token.If 260 | "then" -> token.Then 261 | "else" -> token.Else 262 | "or" -> token.Or 263 | "and" -> token.And 264 | "import" -> token.Import 265 | "as" -> token.As 266 | "true" -> token.True 267 | "false" -> token.False 268 | _ -> token.Ident(name) 269 | } 270 | 271 | token(rest, start, tok, string.length(name)) 272 | } 273 | 274 | // UTILS ....................................................................... 275 | 276 | fn take_content( 277 | input: String, 278 | content: String, 279 | predicate: fn(String) -> Bool, 280 | ) -> #(String, String) { 281 | case string.pop_grapheme(input) { 282 | Ok(#(grapheme, rest)) -> { 283 | case predicate(grapheme) { 284 | True -> take_content(rest, content <> grapheme, predicate) 285 | False -> #(content, input) 286 | } 287 | } 288 | _ -> #(content, "") 289 | } 290 | } 291 | 292 | fn to_float(x: String) -> Float { 293 | case int.parse(x) { 294 | Ok(n) -> int.to_float(n) 295 | _ -> result.unwrap(float.parse(x), 0.0) 296 | } 297 | } 298 | 299 | fn is_alphanum(c: String) { 300 | is_alpha(c) || is_num(c) 301 | } 302 | 303 | fn is_num(c: String) -> Bool { 304 | case c { 305 | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -> True 306 | 307 | _ -> False 308 | } 309 | } 310 | 311 | fn is_alpha(c: String) -> Bool { 312 | case c { 313 | "_" 314 | | "a" 315 | | "b" 316 | | "c" 317 | | "d" 318 | | "e" 319 | | "f" 320 | | "g" 321 | | "h" 322 | | "i" 323 | | "j" 324 | | "k" 325 | | "l" 326 | | "m" 327 | | "n" 328 | | "o" 329 | | "p" 330 | | "q" 331 | | "r" 332 | | "s" 333 | | "t" 334 | | "u" 335 | | "v" 336 | | "w" 337 | | "x" 338 | | "y" 339 | | "z" 340 | | "A" 341 | | "B" 342 | | "C" 343 | | "D" 344 | | "E" 345 | | "F" 346 | | "G" 347 | | "H" 348 | | "I" 349 | | "J" 350 | | "K" 351 | | "L" 352 | | "M" 353 | | "N" 354 | | "O" 355 | | "P" 356 | | "Q" 357 | | "R" 358 | | "S" 359 | | "T" 360 | | "U" 361 | | "V" 362 | | "W" 363 | | "X" 364 | | "Y" 365 | | "Z" -> True 366 | 367 | _ -> False 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /src/bella/parser.gleam: -------------------------------------------------------------------------------- 1 | import gleam/list 2 | import gleam/result.{try} 3 | import bella/error 4 | import bella/lexer/token.{ 5 | type Span, type Token, type TokenType, type Tokens, Span, 6 | } 7 | 8 | // TYPES ....................................................................... 9 | 10 | pub type ExprAst { 11 | Var(String) 12 | String(String) 13 | Number(Float) 14 | Bool(Bool) 15 | Block(List(Expr)) 16 | Record(fields: List(#(String, Expr))) 17 | RecordAccess(record: Expr, field: String) 18 | List(List(Expr)) 19 | PatList(list: List(Expr), rest_name: String) 20 | BinOp(operator: Token, left: Expr, right: Expr) 21 | Unary(operator: Token, value: Expr) 22 | Lambda(param: String, body: Expr) 23 | Lambda0(body: Expr) 24 | Let(pattern: Expr, value: Expr, body: Expr) 25 | Call(callee: Expr, arg: Expr) 26 | Call0(callee: Expr) 27 | If(cond: Expr, true_branch: Expr, false_branch: Expr) 28 | Try(body: Expr, else: Expr) 29 | Throw(value: Expr) 30 | Match(value: Expr, cases: List(#(Expr, Expr))) 31 | NamedPat(value: Expr, name: String) 32 | } 33 | 34 | type Expr = 35 | #(ExprAst, Span) 36 | 37 | pub type Module { 38 | Module(imports: List(Import), body: List(Expr)) 39 | } 40 | 41 | pub type Import { 42 | Import(alias: String, path: String) 43 | } 44 | 45 | type Parsed = 46 | Result(#(Expr, Tokens), error.Error) 47 | 48 | type Parser = 49 | fn(Tokens) -> Parsed 50 | 51 | // PARSER ...................................................................... 52 | 53 | pub fn parse(tokens: Tokens) -> Result(Module, error.Error) { 54 | parse_module(tokens) 55 | } 56 | 57 | fn parse_module(tokens: Tokens) -> Result(Module, error.Error) { 58 | use #(imports, rest) <- try(parse_imports(tokens, [])) 59 | use exprs <- try(parse_exprs(rest, [])) 60 | Ok(Module(imports: imports, body: exprs)) 61 | } 62 | 63 | fn parse_imports( 64 | tokens: Tokens, 65 | imports: List(Import), 66 | ) -> Result(#(List(Import), Tokens), error.Error) { 67 | case tokens { 68 | [#(token.Import, _), #(token.Ident(name), _), ..rest] -> { 69 | use #(i, rest) <- try(finish_import(rest, name, name)) 70 | case rest { 71 | [#(token.As, _), #(token.Ident(alias), _), ..rest] -> 72 | parse_imports(rest, [Import(alias, i.path), ..imports]) 73 | [#(token.As, _), #(_, pos), ..] -> 74 | error.syntax_error("I expected an identifier", pos) 75 | _ -> parse_imports(rest, [i, ..imports]) 76 | } 77 | } 78 | [#(token.Import, _), #(_, pos), ..] -> 79 | error.syntax_error("I expected an identifier", pos) 80 | _ -> Ok(#(list.reverse(imports), tokens)) 81 | } 82 | } 83 | 84 | fn finish_import( 85 | tokens: Tokens, 86 | acc: String, 87 | name: String, 88 | ) -> Result(#(Import, Tokens), error.Error) { 89 | case tokens { 90 | [#(token.Slash, _), #(token.Ident(name), _), ..rest] -> 91 | finish_import(rest, acc <> "/" <> name, name) 92 | [#(token.Slash, _), #(_, pos), ..] -> 93 | error.syntax_error("I expected an identifier", pos) 94 | _ -> Ok(#(Import(name, acc), tokens)) 95 | } 96 | } 97 | 98 | fn parse_exprs( 99 | tokens: Tokens, 100 | exprs: List(Expr), 101 | ) -> Result(List(Expr), error.Error) { 102 | case tokens { 103 | [#(token.Eof, pos)] -> 104 | case exprs { 105 | [] -> error.syntax_error("Unexpected end of file", pos) 106 | _ -> Ok(list.reverse(exprs)) 107 | } 108 | _ -> { 109 | use #(expr, rest) <- try(parse_expr(tokens)) 110 | parse_exprs(rest, [expr, ..exprs]) 111 | } 112 | } 113 | } 114 | 115 | fn parse_expr(tokens: Tokens) -> Parsed { 116 | case tokens { 117 | [#(token.If, from), ..rest] -> parse_if(rest, from) 118 | [#(token.Let, from), ..rest] -> parse_let(rest, from) 119 | [#(token.Ident(n), from), #(token.Arrow, _), ..rest] -> { 120 | use #(body, rest) <- try(parse_expr(rest)) 121 | Ok(#(#(Lambda(n, body), span(from, body.1)), rest)) 122 | } 123 | [#(token.Arrow, from), ..rest] -> { 124 | use #(body, rest) <- try(parse_expr(rest)) 125 | Ok(#(#(Lambda0(body), span(from, body.1)), rest)) 126 | } 127 | [#(token.Throw, from), ..rest] -> { 128 | use #(value, rest) <- try(parse_expr(rest)) 129 | Ok(#(#(Throw(value), span(from, value.1)), rest)) 130 | } 131 | [#(token.Try, from), ..rest] -> parse_try(rest, from) 132 | [#(token.Match, from), ..rest] -> parse_match(rest, from) 133 | _ -> parse_pipe(tokens) 134 | } 135 | } 136 | 137 | fn parse_let(tokens: Tokens, from: Span) -> Parsed { 138 | use #(pattern, rest) <- try(parse_pattern(tokens)) 139 | use rest, _ <- expect(token.Eq, rest, "= after pattern") 140 | use #(value, rest) <- try(parse_expr(rest)) 141 | 142 | finish_let( 143 | rest, 144 | fn(body) { #(Let(pattern, value, body), span(from, body.1)) }, 145 | from, 146 | ) 147 | } 148 | 149 | fn finish_let( 150 | tokens: Tokens, 151 | constructor: fn(Expr) -> Expr, 152 | from: Span, 153 | ) -> Parsed { 154 | case tokens { 155 | [#(token.In, _), ..rest] -> { 156 | use #(body, rest) <- try(parse_expr(rest)) 157 | Ok(#(constructor(body), rest)) 158 | } 159 | _ -> { 160 | use #(pattern, rest) <- try(parse_pattern(tokens)) 161 | use rest, _ <- expect(token.Eq, rest, "= after pattern") 162 | use #(value, rest) <- try(parse_expr(rest)) 163 | use #(body, rest) <- try(finish_let( 164 | rest, 165 | fn(body) { #(Let(pattern, value, body), span(from, body.1)) }, 166 | from, 167 | )) 168 | Ok(#(constructor(body), rest)) 169 | } 170 | } 171 | } 172 | 173 | fn parse_pattern(tokens: Tokens) -> Parsed { 174 | use #(pattern, rest) <- try(parse_named_pat(tokens)) 175 | use _ <- try(validate_pattern(pattern)) 176 | Ok(#(pattern, rest)) 177 | } 178 | 179 | fn validate_pattern(pattern: Expr) -> Result(Nil, error.Error) { 180 | case pattern { 181 | #(Var(_), _) 182 | | #(String(_), _) 183 | | #(Number(_), _) 184 | | #(Bool(_), _) 185 | | #(Unary(#(token.Caret, _), _), _) 186 | | #(Unary(#(token.Question, _), _), _) -> Ok(Nil) 187 | #(NamedPat(p, _), _) -> validate_pattern(p) 188 | #(List(patterns), _) | #(PatList(patterns, _), _) -> 189 | list.try_map(patterns, validate_pattern) 190 | |> result.replace(Nil) 191 | #(Record(fields), _) -> 192 | { 193 | use #(_, field) <- list.try_map(fields) 194 | validate_pattern(field) 195 | } 196 | |> result.replace(Nil) 197 | 198 | #(_, pos) -> error.syntax_error("Invalid pattern", pos) 199 | } 200 | } 201 | 202 | fn parse_if(tokens: Tokens, from: Span) -> Parsed { 203 | use #(condition, rest) <- try(parse_expr(tokens)) 204 | use rest, _ <- expect(token.Then, rest, "`then` after condition") 205 | use #(true_branch, rest) <- try(parse_expr(rest)) 206 | use rest, _ <- expect(token.Else, rest, "`else` after true branch") 207 | use #(false_branch, rest) <- try(parse_expr(rest)) 208 | Ok(#( 209 | #(If(condition, true_branch, false_branch), span(from, false_branch.1)), 210 | rest, 211 | )) 212 | } 213 | 214 | fn parse_try(tokens: Tokens, from: Span) -> Parsed { 215 | use #(body, rest) <- try(parse_expr(tokens)) 216 | use rest, _ <- expect(token.Else, rest, "`else` after try body") 217 | use #(else, rest) <- try(parse_expr(rest)) 218 | Ok(#(#(Try(body, else), span(from, else.1)), rest)) 219 | } 220 | 221 | fn parse_match(tokens: Tokens, from: Span) -> Parsed { 222 | use #(value, rest) <- try(parse_expr(tokens)) 223 | use rest, _ <- expect(token.Is, rest, "`is` after match value") 224 | use #(pattern, rest) <- try(parse_pattern(rest)) 225 | use rest, _ <- expect(token.Then, rest, "`then` after match pattern") 226 | use #(body, rest) <- try(parse_expr(rest)) 227 | finish_match(rest, value, [#(pattern, body)], from, body.1) 228 | } 229 | 230 | fn finish_match( 231 | tokens: Tokens, 232 | value: Expr, 233 | clauses: List(#(Expr, Expr)), 234 | from: Span, 235 | to: Span, 236 | ) -> Parsed { 237 | case tokens { 238 | [#(token.Is, _), ..rest] -> { 239 | use #(pattern, rest) <- try(parse_pattern(rest)) 240 | use rest, _ <- expect(token.Then, rest, "`then` after match pattern") 241 | use #(body, rest) <- try(parse_expr(rest)) 242 | finish_match(rest, value, [#(pattern, body), ..clauses], from, body.1) 243 | } 244 | _ -> Ok(#(#(Match(value, list.reverse(clauses)), span(from, to)), tokens)) 245 | } 246 | } 247 | 248 | fn parse_pipe(tokens: Tokens) -> Parsed { 249 | parse_binop(tokens, [token.RPipe], parse_logic_or) 250 | } 251 | 252 | fn parse_logic_or(tokens: Tokens) -> Parsed { 253 | parse_binop(tokens, [token.Or], parse_logic_and) 254 | } 255 | 256 | fn parse_logic_and(tokens: Tokens) -> Parsed { 257 | parse_binop(tokens, [token.And], parse_equality) 258 | } 259 | 260 | fn parse_equality(tokens: Tokens) -> Parsed { 261 | parse_binop(tokens, [token.EqEq, token.Neq], parse_comparison) 262 | } 263 | 264 | fn parse_comparison(tokens: Tokens) -> Parsed { 265 | parse_binop( 266 | tokens, 267 | [token.Greater, token.GreaterEq, token.Less, token.LessEq], 268 | parse_term, 269 | ) 270 | } 271 | 272 | fn parse_term(tokens: Tokens) -> Parsed { 273 | parse_binop(tokens, [token.Plus, token.Minus], parse_factor) 274 | } 275 | 276 | fn parse_factor(tokens: Tokens) -> Parsed { 277 | parse_binop(tokens, [token.Star, token.Slash], parse_named_pat) 278 | } 279 | 280 | fn parse_named_pat(tokens: Tokens) -> Parsed { 281 | use #(expr, rest) <- try(parse_unary(tokens)) 282 | 283 | case rest { 284 | [#(token.As, _), #(token.Ident(name), to), ..rest] -> { 285 | use _ <- try(validate_pattern(expr)) 286 | Ok(#(#(NamedPat(expr, name), span(expr.1, to)), rest)) 287 | } 288 | [#(token.As, _), #(_, pos), ..] -> 289 | error.syntax_error("I expected an identifier after `as`", pos) 290 | _ -> Ok(#(expr, rest)) 291 | } 292 | } 293 | 294 | fn parse_unary(tokens: Tokens) -> Parsed { 295 | case tokens { 296 | [#(token.Minus, from) as op, ..rest] 297 | | [#(token.Bang, from) as op, ..rest] 298 | | [#(token.Caret, from) as op, ..rest] 299 | | [#(token.Question, from) as op, ..rest] -> { 300 | use #(expr, rest) <- try(parse_unary(rest)) 301 | Ok(#(#(Unary(op, expr), span(from, expr.1)), rest)) 302 | } 303 | _ -> parse_call(tokens) 304 | } 305 | } 306 | 307 | fn parse_call(tokens: Tokens) -> Parsed { 308 | use #(expr, rest) <- try(parse_primary(tokens)) 309 | finish_call(rest, expr, expr.1) 310 | } 311 | 312 | fn finish_call(tokens: Tokens, callee: Expr, from: Span) -> Parsed { 313 | case tokens { 314 | [#(token.LParen, _), #(token.RParen, to), ..rest] -> { 315 | finish_call(rest, #(Call0(callee), span(from, to)), from) 316 | } 317 | [#(token.LParen, _), ..rest] -> { 318 | use #(arg, rest) <- try(parse_expr(rest)) 319 | finish_call_args(rest, #(Call(callee, arg), from), from) 320 | } 321 | [#(token.Dot, _), ..rest] -> { 322 | case rest { 323 | [#(token.Ident(name), to), ..rest] -> { 324 | finish_call(rest, #(RecordAccess(callee, name), span(from, to)), from) 325 | } 326 | [#(_, pos), ..] -> 327 | error.syntax_error("I expected an identifier after .", pos) 328 | } 329 | } 330 | _ -> Ok(#(callee, tokens)) 331 | } 332 | } 333 | 334 | fn finish_call_args(tokens: Tokens, callee: Expr, from: Span) -> Parsed { 335 | case tokens { 336 | [#(token.RParen, to), ..rest] 337 | | [#(token.Comma, _), #(token.RParen, to), ..rest] -> 338 | finish_call(rest, complete_call_spans(callee, to), from) 339 | [#(token.Comma, _), ..rest] -> { 340 | use #(arg, rest) <- try(parse_expr(rest)) 341 | finish_call_args(rest, #(Call(callee, arg), from), from) 342 | } 343 | [#(_, pos), ..] -> error.syntax_error("I expected a , or )", pos) 344 | } 345 | } 346 | 347 | fn complete_call_spans(expr: Expr, to: Span) -> Expr { 348 | case expr { 349 | #(Call0(callee), from) -> #( 350 | Call0(complete_call_spans(callee, to)), 351 | span(from, to), 352 | ) 353 | #(Call(callee, arg), from) -> #( 354 | Call(complete_call_spans(callee, to), arg), 355 | span(from, to), 356 | ) 357 | _ -> expr 358 | } 359 | } 360 | 361 | fn parse_binop(tokens: Tokens, ops: List(TokenType), subrule: Parser) -> Parsed { 362 | use #(left, rest) <- try(subrule(tokens)) 363 | finish_binop(rest, left, ops, subrule) 364 | } 365 | 366 | fn finish_binop( 367 | tokens: Tokens, 368 | left: Expr, 369 | ops: List(TokenType), 370 | subrule: Parser, 371 | ) -> Parsed { 372 | let [op, ..rest] = tokens 373 | case list.contains(ops, op.0) { 374 | True -> { 375 | use #(right, rest) <- try(subrule(rest)) 376 | finish_binop( 377 | rest, 378 | #(BinOp(op, left, right), span(left.1, right.1)), 379 | ops, 380 | subrule, 381 | ) 382 | } 383 | False -> Ok(#(left, tokens)) 384 | } 385 | } 386 | 387 | fn parse_primary(tokens: Tokens) -> Parsed { 388 | case tokens { 389 | [#(token.Number(x), pos), ..rest] -> Ok(#(#(Number(x), pos), rest)) 390 | [#(token.String(x), pos), ..rest] -> Ok(#(#(String(x), pos), rest)) 391 | [#(token.Ident(x), pos), ..rest] -> Ok(#(#(Var(x), pos), rest)) 392 | [#(token.True, pos), ..rest] -> Ok(#(#(Bool(True), pos), rest)) 393 | [#(token.False, pos), ..rest] -> Ok(#(#(Bool(False), pos), rest)) 394 | [#(token.LParen, from), ..rest] -> parse_block(rest, [], from) 395 | [#(token.LBrace, from), ..rest] -> parse_record(rest, from) 396 | [#(token.LBracket, from), ..rest] -> parse_list(rest, from) 397 | [#(_, pos), ..] -> error.syntax_error("I wasn't expecting this", pos) 398 | } 399 | } 400 | 401 | fn parse_block(tokens: Tokens, acc: List(Expr), from: Span) -> Parsed { 402 | case tokens { 403 | [#(token.RParen, pos), ..rest] -> 404 | case acc { 405 | [] -> error.syntax_error("I expected an expression", pos) 406 | _ -> Ok(#(#(Block(list.reverse(acc)), span(from, pos)), rest)) 407 | } 408 | _ -> { 409 | use #(expr, rest) <- try(parse_expr(tokens)) 410 | parse_block(rest, [expr, ..acc], from) 411 | } 412 | } 413 | } 414 | 415 | fn parse_record_item(tokens: Tokens) { 416 | case tokens { 417 | [#(token.Ident(name), pos), #(token.Colon, _), ..rest] -> { 418 | use #(value, rest) <- try(parse_expr(rest)) 419 | Ok(#(#(name, value, pos), rest)) 420 | } 421 | [#(token.Ident(name), pos), ..rest] -> { 422 | Ok(#(#(name, #(Var(name), pos), pos), rest)) 423 | } 424 | [#(_, pos), ..] -> error.syntax_error("I expected an identifier", pos) 425 | } 426 | } 427 | 428 | fn parse_record(tokens: Tokens, from: Span) -> Parsed { 429 | case tokens { 430 | [#(token.RBrace, pos), ..rest] -> { 431 | Ok(#(#(Record([]), span(from, pos)), rest)) 432 | } 433 | _ -> { 434 | use #(#(name, value, _), rest) <- try(parse_record_item(tokens)) 435 | finish_record(rest, [#(name, value)], from) 436 | } 437 | } 438 | } 439 | 440 | fn finish_record( 441 | tokens: Tokens, 442 | fields: List(#(String, Expr)), 443 | from: Span, 444 | ) -> Parsed { 445 | case tokens { 446 | [#(token.RBrace, to), ..rest] 447 | | [#(token.Comma, _), #(token.RBrace, to), ..rest] -> 448 | Ok(#(#(Record(fields), span(from, to)), rest)) 449 | [#(token.Comma, _), ..rest] -> { 450 | use #(#(name, value, pos), rest) <- try(parse_record_item(rest)) 451 | case list.any(fields, fn(f) { f.0 == name }) { 452 | True -> error.syntax_error("Duplicate record field", pos) 453 | False -> finish_record(rest, [#(name, value), ..fields], from) 454 | } 455 | } 456 | [#(_, pos), ..] -> error.syntax_error("I expected a , or }", pos) 457 | } 458 | } 459 | 460 | fn parse_list(tokens: Tokens, from: Span) -> Parsed { 461 | case tokens { 462 | [#(token.RBracket, to), ..rest] -> Ok(#(#(List([]), span(from, to)), rest)) 463 | _ -> { 464 | use #(expr, rest) <- try(parse_expr(tokens)) 465 | finish_list(rest, [expr], from) 466 | } 467 | } 468 | } 469 | 470 | fn finish_list(tokens: Tokens, items: List(Expr), from: Span) -> Parsed { 471 | case tokens { 472 | [#(token.Bar, _), #(token.Ident(name), _), ..rest] -> { 473 | use rest, to <- expect(token.RBracket, rest, "] after identifier") 474 | Ok(#(#(PatList(list.reverse(items), name), span(from, to)), rest)) 475 | } 476 | [#(token.Bar, _), #(_, pos), ..] -> 477 | error.syntax_error("I expected an identifier", pos) 478 | [#(token.RBracket, to), ..rest] 479 | | [#(token.Comma, _), #(token.RBracket, to), ..rest] -> 480 | Ok(#(#(List(list.reverse(items)), span(from, to)), rest)) 481 | [#(token.Comma, _), ..rest] -> { 482 | use #(expr, rest) <- try(parse_expr(rest)) 483 | finish_list(rest, [expr, ..items], from) 484 | } 485 | [#(_, pos), ..] -> error.syntax_error("I expected a , or ]", pos) 486 | } 487 | } 488 | 489 | // UTILS ....................................................................... 490 | 491 | pub fn span(from: Span, to: Span) -> Span { 492 | Span(from.from, to.to) 493 | } 494 | 495 | fn expect( 496 | tok: TokenType, 497 | tokens: Tokens, 498 | msg: String, 499 | callback: fn(Tokens, Span) -> Parsed, 500 | ) -> Parsed { 501 | case tokens { 502 | [#(head, pos), ..tail] if head == tok -> callback(tail, pos) 503 | [#(_, pos), ..] -> error.syntax_error("I expected a " <> msg, pos) 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /src/bella/evaluator.gleam: -------------------------------------------------------------------------------- 1 | import gleam/map 2 | import gleam/list 3 | import gleam/result.{try} 4 | import simplifile 5 | import bella/error 6 | import bella/parser 7 | import bella/lexer 8 | import bella/lexer/token 9 | import bella/evaluator/functions 10 | import bella/evaluator/types.{ 11 | type DataType, type Evaluated, type Scope, Bool, Function, Lambda, Lambda0, 12 | List, Number, Record, String, 13 | } 14 | 15 | // EVALUATOR ................................................................... 16 | 17 | pub fn evaluate_str(str: String) -> Evaluated { 18 | use tokens <- try(lexer.lex(str)) 19 | use parsed <- try(parser.parse(tokens)) 20 | evaluate(parsed) 21 | } 22 | 23 | fn evaluate(mod: parser.Module) -> Evaluated { 24 | let functions = [ 25 | #("import_", Function(import_file_function)), 26 | ..functions.functions() 27 | ] 28 | use imported <- try({ 29 | use parser.Import(alias, path) <- list.try_map(mod.imports) 30 | use #(result, _) <- try(import_file("src/" <> path <> ".bella")) 31 | Ok(#(alias, result)) 32 | }) 33 | eval_all( 34 | mod.body, 35 | [], 36 | map.merge(map.from_list(functions), map.from_list(imported)), 37 | ) 38 | } 39 | 40 | fn eval_all( 41 | exprs: List(parser.Expr), 42 | evaled: List(DataType), 43 | scope: Scope, 44 | ) -> Evaluated { 45 | case exprs { 46 | [] -> 47 | case evaled { 48 | [first, ..] -> Ok(#(first, scope)) 49 | // Already enforced by the parser... but, why not? 50 | _ -> error.runtime_error("Exprs cannot be empty") 51 | } 52 | [expr, ..rest] -> { 53 | use #(result, _) <- try(eval(expr, scope)) 54 | eval_all(rest, [result, ..evaled], scope) 55 | } 56 | } 57 | } 58 | 59 | fn eval(expr: parser.Expr, scope: Scope) -> Evaluated { 60 | case expr { 61 | #(parser.Number(x), _) -> Ok(#(Number(x), scope)) 62 | #(parser.String(x), _) -> Ok(#(String(x), scope)) 63 | #(parser.Bool(x), _) -> Ok(#(Bool(x), scope)) 64 | #(parser.Lambda(arg, body), _) -> Ok(#(Lambda(arg, body, scope), scope)) 65 | #(parser.Lambda0(body), _) -> Ok(#(Lambda0(body, scope), scope)) 66 | #(parser.Var(x), pos) -> get_var(x, scope, pos) 67 | #(parser.Block(exprs), _) -> eval_all(exprs, [], scope) 68 | #(parser.Record(fields), _) -> eval_record(fields, map.new(), scope) 69 | #(parser.RecordAccess(record, field), pos) -> 70 | eval_record_access(record, field, scope, pos) 71 | #(parser.List(items), _) -> eval_list(items, scope) 72 | #(parser.BinOp(op, left, right), pos) -> 73 | eval_binop(op, left, right, scope, pos) 74 | #(parser.Unary(op, value), pos) -> eval_unary(op, value, scope, pos) 75 | #(parser.Call(callee, arg), pos) -> { 76 | use #(callee, _) <- try(eval(callee, scope)) 77 | use #(arg, _) <- try(eval(arg, scope)) 78 | eval_call(callee, arg, scope, pos) 79 | } 80 | #(parser.Call0(callee), pos) -> { 81 | use #(callee, _) <- try(eval(callee, scope)) 82 | eval_call0(callee, scope, pos) 83 | } 84 | #(parser.Let(pattern, value, body), _) -> 85 | eval_let(pattern, value, body, scope) 86 | #(parser.Match(value_expr, clauses), _) -> { 87 | use #(value, _) <- try(eval(value_expr, scope)) 88 | eval_match(value, clauses, scope, value_expr.1) 89 | } 90 | #(parser.If(cond, true_branch, false_branch), _) -> 91 | eval_if(cond, true_branch, false_branch, scope) 92 | #(parser.Throw(value), pos) -> eval_throw(value, scope, pos) 93 | #(parser.Try(body, else), _) -> eval_try(body, else, scope) 94 | #(parser.PatList(..), pos) -> 95 | error.runtime_error_pos("List with | is only allowed in patterns", pos) 96 | #(parser.NamedPat(..), pos) -> 97 | error.runtime_error_pos("`as` is only allowed in patterns", pos) 98 | } 99 | } 100 | 101 | fn get_var(name: String, scope: Scope, pos: token.Span) -> Evaluated { 102 | case map.get(scope, name) { 103 | Ok(x) -> Ok(#(x, scope)) 104 | _ -> 105 | error.runtime_error_pos("There is no binding for `" <> name <> "`", pos) 106 | } 107 | } 108 | 109 | fn eval_record( 110 | fields: List(#(String, parser.Expr)), 111 | result: map.Map(String, DataType), 112 | scope: Scope, 113 | ) -> Evaluated { 114 | case fields { 115 | [] -> Ok(#(Record(result), scope)) 116 | [#(name, value), ..rest] -> { 117 | use #(value, _) <- try(eval(value, scope)) 118 | eval_record(rest, map.insert(result, name, value), scope) 119 | } 120 | } 121 | } 122 | 123 | fn eval_record_access( 124 | record: parser.Expr, 125 | field: String, 126 | scope: Scope, 127 | pos: token.Span, 128 | ) -> Evaluated { 129 | use #(record, _) <- try(eval(record, scope)) 130 | case record { 131 | Record(fields) -> 132 | case map.get(fields, field) { 133 | Ok(x) -> Ok(#(x, scope)) 134 | _ -> 135 | error.runtime_error_pos("Record has no `" <> field <> "` field", pos) 136 | } 137 | _ -> 138 | error.runtime_error_pos( 139 | "Expression has no `" <> field <> "` field because it is not a record", 140 | pos, 141 | ) 142 | } 143 | } 144 | 145 | fn eval_list(items: List(parser.Expr), scope: Scope) -> Evaluated { 146 | use items <- try({ 147 | use item <- list.try_map(items) 148 | use #(item, _) <- try(eval(item, scope)) 149 | Ok(item) 150 | }) 151 | Ok(#(List(items), scope)) 152 | } 153 | 154 | fn eval_binop( 155 | op: token.Token, 156 | left: parser.Expr, 157 | right: parser.Expr, 158 | scope: Scope, 159 | pos: token.Span, 160 | ) -> Evaluated { 161 | use #(left, _) <- try(eval(left, scope)) 162 | use #(right, _) <- try(eval(right, scope)) 163 | case op { 164 | #(token.Plus, _) -> 165 | case left, right { 166 | Number(a), Number(b) -> Ok(#(Number(a +. b), scope)) 167 | String(a), String(b) -> Ok(#(String(a <> b), scope)) 168 | Record(a), Record(b) -> Ok(#(Record(map.merge(a, b)), scope)) 169 | List(a), List(b) -> Ok(#(List(list.append(a, b)), scope)) 170 | _, _ -> 171 | op_error( 172 | "+", 173 | "numbers, strings, records, or lists and be the same type", 174 | pos, 175 | ) 176 | } 177 | 178 | #(token.EqEq, _) -> Ok(#(Bool(left == right), scope)) 179 | 180 | #(token.Neq, _) -> Ok(#(Bool(left != right), scope)) 181 | 182 | #(token.Minus, _) -> 183 | case left, right { 184 | Number(a), Number(b) -> Ok(#(Number(a -. b), scope)) 185 | _, _ -> op_error("-", "numbers", pos) 186 | } 187 | 188 | #(token.Star, _) -> 189 | case left, right { 190 | Number(a), Number(b) -> Ok(#(Number(a *. b), scope)) 191 | _, _ -> op_error("*", "numbers", pos) 192 | } 193 | 194 | #(token.Slash, _) -> 195 | case left, right { 196 | Number(a), Number(b) -> Ok(#(Number(a /. b), scope)) 197 | _, _ -> op_error("/", "numbers", pos) 198 | } 199 | 200 | #(token.Less, _) -> 201 | case left, right { 202 | Number(a), Number(b) -> Ok(#(Bool(a <. b), scope)) 203 | _, _ -> op_error("<", "numbers", pos) 204 | } 205 | 206 | #(token.Greater, _) -> 207 | case left, right { 208 | Number(a), Number(b) -> Ok(#(Bool(a >. b), scope)) 209 | _, _ -> op_error(">", "numbers", pos) 210 | } 211 | 212 | #(token.LessEq, _) -> 213 | case left, right { 214 | Number(a), Number(b) -> Ok(#(Bool(a <=. b), scope)) 215 | _, _ -> op_error("<=", "numbers", pos) 216 | } 217 | 218 | #(token.GreaterEq, _) -> 219 | case left, right { 220 | Number(a), Number(b) -> Ok(#(Bool(a >=. b), scope)) 221 | _, _ -> op_error(">=", "numbers", pos) 222 | } 223 | 224 | #(token.And, _) -> 225 | case left, right { 226 | Bool(a), Bool(b) -> Ok(#(Bool(a && b), scope)) 227 | _, _ -> op_error("`and`", "Booleans", pos) 228 | } 229 | 230 | #(token.Or, _) -> 231 | case left, right { 232 | Bool(a), Bool(b) -> Ok(#(Bool(a || b), scope)) 233 | _, _ -> op_error("`or`", "Booleans", pos) 234 | } 235 | 236 | #(token.RPipe, _) -> eval_call(right, left, scope, pos) 237 | 238 | _ -> error.runtime_error("BinOp not implemented") 239 | } 240 | } 241 | 242 | fn eval_unary( 243 | op: token.Token, 244 | value: parser.Expr, 245 | scope: Scope, 246 | pos: token.Span, 247 | ) -> Evaluated { 248 | case op { 249 | #(token.Minus, _) -> { 250 | use #(value, _) <- try(eval(value, scope)) 251 | case value { 252 | Number(x) -> Ok(#(Number(0.0 -. x), scope)) 253 | x -> 254 | error.runtime_error_pos( 255 | "Unary - applies to numbers; instead got a " <> types.to_type(x), 256 | pos, 257 | ) 258 | } 259 | } 260 | #(token.Bang, _) -> { 261 | use #(value, _) <- try(eval(value, scope)) 262 | case value { 263 | Bool(x) -> Ok(#(Bool(!x), scope)) 264 | x -> 265 | error.runtime_error_pos( 266 | "Unary ! applies to Booleans; instead got a " <> types.to_type(x), 267 | pos, 268 | ) 269 | } 270 | } 271 | #(token.Caret, _) -> 272 | error.runtime_error_pos("^ in only allowed in patterns", pos) 273 | #(token.Question, _) -> 274 | error.runtime_error_pos("? is only allowed in patterns", pos) 275 | // Make the compiler happy :) 276 | _ -> error.runtime_error("Unary operator not implemented") 277 | } 278 | } 279 | 280 | fn create_var(name: String, value: DataType, scope: Scope) -> Scope { 281 | map.insert(scope, name, value) 282 | } 283 | 284 | fn eval_call( 285 | callee: DataType, 286 | arg: DataType, 287 | scope: Scope, 288 | pos: token.Span, 289 | ) -> Evaluated { 290 | case callee { 291 | Lambda(param, body, closure) -> { 292 | use #(result, _) <- try(eval( 293 | body, 294 | create_var(param, arg, map.merge(scope, closure)), 295 | )) 296 | Ok(#(result, scope)) 297 | } 298 | Function(func) -> func(arg, scope, pos) 299 | Lambda0(..) -> 300 | error.runtime_error_pos("Lambda must be called without an argument", pos) 301 | _ -> 302 | error.runtime_error_pos( 303 | "`" <> types.inspect(callee) <> "` cannot be called\nThis could be caused from calling something with too many arguments.\nFor example, `add(1, 2, 3)` desugars to `add(1)(2)(3)` which reduces to `3(3)`", 304 | pos, 305 | ) 306 | } 307 | } 308 | 309 | fn eval_call0(callee: DataType, scope: Scope, pos: token.Span) -> Evaluated { 310 | case callee { 311 | Lambda0(body, closure) -> { 312 | use #(result, _) <- try(eval(body, map.merge(scope, closure))) 313 | Ok(#(result, scope)) 314 | } 315 | Lambda(n, ..) -> 316 | error.runtime_error_pos( 317 | "Lambda must be called with an argument, `" <> n <> "`", 318 | pos, 319 | ) 320 | Function(..) -> 321 | error.runtime_error_pos("Function must be called with an argument", pos) 322 | _ -> 323 | error.runtime_error_pos( 324 | "`" <> types.inspect(callee) <> "` cannot be called", 325 | pos, 326 | ) 327 | } 328 | } 329 | 330 | fn eval_let( 331 | pattern: parser.Expr, 332 | value_expr: parser.Expr, 333 | body: parser.Expr, 334 | scope: Scope, 335 | ) -> Evaluated { 336 | use #(value, _) <- try(eval(value_expr, scope)) 337 | use pattern_scope <- try(pattern_match( 338 | pattern, 339 | value, 340 | scope, 341 | parser.span(pattern.1, value_expr.1), 342 | )) 343 | use #(result, _) <- try(eval(body, pattern_scope)) 344 | Ok(#(result, scope)) 345 | } 346 | 347 | fn eval_match( 348 | value: DataType, 349 | clauses: List(#(parser.Expr, parser.Expr)), 350 | scope: Scope, 351 | value_pos: token.Span, 352 | ) -> Evaluated { 353 | case clauses { 354 | [#(pattern, body), ..rest] -> { 355 | case pattern_match(pattern, value, scope, pattern.1) { 356 | Ok(scope) -> eval(body, scope) 357 | _ -> eval_match(value, rest, scope, value_pos) 358 | } 359 | } 360 | [] -> error.runtime_error_pos("No match found", value_pos) 361 | } 362 | } 363 | 364 | fn pattern_match( 365 | pattern: parser.Expr, 366 | value: DataType, 367 | scope: Scope, 368 | pos: token.Span, 369 | ) -> Result(Scope, error.Error) { 370 | case pattern, value { 371 | #(parser.Var(name), _), _ -> Ok(create_var(name, value, scope)) 372 | #(parser.String(string), _), String(value) if string == value -> Ok(scope) 373 | #(parser.Number(num), _), Number(value) if num == value -> Ok(scope) 374 | #(parser.Bool(bool), _), Bool(value) if bool == value -> Ok(scope) 375 | #(parser.NamedPat(pat, name), _), _ -> { 376 | use scope <- try(pattern_match(pat, value, scope, pos)) 377 | Ok(create_var(name, value, scope)) 378 | } 379 | #(parser.List(pats), _), _ -> { 380 | use #(scope, rest_list) <- try(pattern_match_list(pats, value, scope, pos)) 381 | 382 | case rest_list { 383 | [] -> Ok(scope) 384 | _ -> 385 | error.runtime_error_pos( 386 | "List did not match pattern: it is too long", 387 | pos, 388 | ) 389 | } 390 | } 391 | #(parser.PatList(pats, rest_name), _), _ -> { 392 | use #(scope, rest_list) <- try(pattern_match_list(pats, value, scope, pos)) 393 | Ok(create_var(rest_name, List(rest_list), scope)) 394 | } 395 | #(parser.Record(fields), _), _ -> 396 | pattern_match_record(fields, value, scope, pos) 397 | #(parser.Unary(#(token.Caret, _), unary_expr), _), _ -> { 398 | use #(unary_pattern, _) <- try(eval(unary_expr, scope)) 399 | use pattern <- try(to_pattern(unary_pattern, unary_expr.1)) 400 | pattern_match(pattern, value, scope, pos) 401 | } 402 | #(parser.Unary(#(token.Question, _), unary_expr), _), _ -> { 403 | use #(pattern_fn, _) <- try(eval(unary_expr, scope)) 404 | use #(result, _) <- try(eval_call(pattern_fn, value, scope, unary_expr.1)) 405 | 406 | case result { 407 | Bool(True) -> Ok(scope) 408 | Bool(False) -> 409 | error.runtime_error_pos( 410 | "Value `" <> types.inspect(value) <> "` did not match pattern function", 411 | pos, 412 | ) 413 | _ -> 414 | error.runtime_error_pos( 415 | "Pattern function must return a Boolean", 416 | unary_expr.1, 417 | ) 418 | } 419 | } 420 | 421 | _, _ -> 422 | error.runtime_error_pos( 423 | "Value `" <> types.inspect(value) <> "` did not match pattern", 424 | pos, 425 | ) 426 | } 427 | } 428 | 429 | fn to_pattern( 430 | value: DataType, 431 | pos: token.Span, 432 | ) -> Result(parser.Expr, error.Error) { 433 | case value { 434 | String(s) -> Ok(#(parser.String(s), pos)) 435 | Number(n) -> Ok(#(parser.Number(n), pos)) 436 | Bool(b) -> Ok(#(parser.Bool(b), pos)) 437 | List(values) -> { 438 | use patterns <- try(list.try_map(values, to_pattern(_, pos))) 439 | Ok(#(parser.List(patterns), pos)) 440 | } 441 | Record(fields) -> { 442 | use pattern_fields <- try({ 443 | use #(name, value) <- list.try_map(map.to_list(fields)) 444 | use pattern <- try(to_pattern(value, pos)) 445 | Ok(#(name, pattern)) 446 | }) 447 | Ok(#(parser.Record(pattern_fields), pos)) 448 | } 449 | _ -> 450 | error.runtime_error_pos( 451 | "Cannot convert `" <> types.inspect(value) <> "` to a valid pattern", 452 | pos, 453 | ) 454 | } 455 | } 456 | 457 | fn pattern_match_list( 458 | patterns: List(parser.Expr), 459 | value: DataType, 460 | scope: Scope, 461 | pos: token.Span, 462 | ) -> Result(#(Scope, List(DataType)), error.Error) { 463 | case patterns, value { 464 | [first_pat, ..rest_pats], List([first_val, ..rest_vals]) -> { 465 | use scope <- try(pattern_match(first_pat, first_val, scope, pos)) 466 | pattern_match_list(rest_pats, List(rest_vals), scope, pos) 467 | } 468 | [], List(rest) -> Ok(#(scope, rest)) 469 | _, List([]) -> 470 | error.runtime_error_pos( 471 | "List did not match pattern: it is too short", 472 | pos, 473 | ) 474 | _, _ -> 475 | error.runtime_error_pos( 476 | "Value did not match pattern: it is not a list", 477 | pos, 478 | ) 479 | } 480 | } 481 | 482 | fn pattern_match_record( 483 | pat_fields: List(#(String, parser.Expr)), 484 | value: DataType, 485 | scope: Scope, 486 | pos: token.Span, 487 | ) -> Result(Scope, error.Error) { 488 | case pat_fields, value { 489 | [#(name, pattern), ..rest], Record(value_fields) -> { 490 | case map.get(value_fields, name) { 491 | Ok(field_value) -> { 492 | use scope <- try(pattern_match(pattern, field_value, scope, pos)) 493 | pattern_match_record(rest, value, scope, pos) 494 | } 495 | _ -> 496 | error.runtime_error_pos( 497 | "Record did not match pattern: the field `" <> name <> "` is not present", 498 | pos, 499 | ) 500 | } 501 | } 502 | [], _ -> Ok(scope) 503 | } 504 | } 505 | 506 | fn eval_if( 507 | cond_expr: parser.Expr, 508 | true_branch: parser.Expr, 509 | false_branch: parser.Expr, 510 | scope: Scope, 511 | ) -> Evaluated { 512 | use #(cond, _) <- try(eval(cond_expr, scope)) 513 | case cond { 514 | Bool(x) -> 515 | case x { 516 | True -> eval(true_branch, scope) 517 | False -> eval(false_branch, scope) 518 | } 519 | _ -> 520 | error.runtime_error_pos( 521 | "The condition for `if` must be a Boolean", 522 | cond_expr.1, 523 | ) 524 | } 525 | } 526 | 527 | fn eval_throw(value: parser.Expr, scope: Scope, pos: token.Span) -> Evaluated { 528 | // TODO: allow any DataType to be thrown (requires new error type) 529 | use #(value, _) <- try(eval(value, scope)) 530 | case value { 531 | String(s) -> error.runtime_error_pos(s, pos) 532 | _ -> error.runtime_error_pos("Can only `throw` strings", pos) 533 | } 534 | } 535 | 536 | fn eval_try(body: parser.Expr, else: parser.Expr, scope: Scope) -> Evaluated { 537 | case eval(body, scope) { 538 | Ok(#(x, _)) -> Ok(#(x, scope)) 539 | Error(error.RuntimeError(msg)) | Error(error.RuntimeErrorPos(msg, _)) -> { 540 | case else { 541 | #(parser.Lambda(..), _) -> { 542 | use #(else, _) <- try(eval(else, scope)) 543 | eval_call( 544 | else, 545 | String(msg), 546 | scope, 547 | // Pass a 'useless' span to `eval_call` since it is guaranteed not to 548 | // error from the arguments passed to it 549 | token.useless_span, 550 | ) 551 | } 552 | _ -> eval(else, scope) 553 | } 554 | } 555 | } 556 | } 557 | 558 | // UTILS ....................................................................... 559 | 560 | fn op_error(op: String, must_be: String, pos: token.Span) -> Evaluated { 561 | error.runtime_error_pos("Operands of " <> op <> " must be " <> must_be, pos) 562 | } 563 | 564 | fn import_file(path: String) -> Evaluated { 565 | case simplifile.read(path) { 566 | Ok(contents) -> 567 | case evaluate_str(contents) { 568 | Ok(#(result, _)) -> Ok(#(result, map.new())) 569 | Error(err) -> error.imported_error(err, contents, path) 570 | } 571 | _ -> 572 | error.runtime_error( 573 | "I couldn't find the requested file to import: " <> path, 574 | ) 575 | } 576 | } 577 | 578 | fn import_file_function(path: DataType, _: Scope, pos: token.Span) -> Evaluated { 579 | case path { 580 | String(path) -> import_file(path) 581 | _ -> error.runtime_error_pos("Import path must be a string", pos) 582 | } 583 | } 584 | --------------------------------------------------------------------------------