├── .eslintrc ├── .gitattributes ├── .github └── workflows │ └── pr.yml ├── .gitignore ├── .nvmrc ├── .vscode ├── launch.json └── settings.json ├── README.md ├── docs ├── README.md ├── notes │ ├── devlog.md │ ├── language-benefits.md │ └── resources.md └── structural-subtyping.md ├── example.voyd ├── package-lock.json ├── package.json ├── reference ├── basics.md ├── control-flow.md ├── declare.md ├── functions.md ├── macros.md ├── memory.md ├── modules.md ├── mutability.md ├── spec │ ├── README.md │ ├── core.md │ └── surface.md ├── style-guide.md ├── syntax.md └── types │ ├── effects.md │ ├── intersections.md │ ├── objects.md │ ├── overview.md │ ├── structs.md │ ├── traits.md │ ├── tuples.md │ └── unions.md ├── src ├── __tests__ │ ├── compiler.test.ts │ └── fixtures │ │ └── e2e-file.ts ├── assembler.ts ├── assembler │ ├── index.ts │ ├── return-call.ts │ └── rtt │ │ ├── extension.ts │ │ ├── field-accessor.ts │ │ ├── index.ts │ │ └── rtt.ts ├── cli │ ├── cli-dev.ts │ ├── cli.ts │ └── exec.ts ├── compiler.ts ├── index.ts ├── lib │ ├── __tests__ │ │ └── murmur-hash.test.ts │ ├── binaryen-gc │ │ ├── README.md │ │ ├── index.ts │ │ ├── test.ts │ │ └── types.ts │ ├── config │ │ ├── README.md │ │ ├── arg-parser.ts │ │ ├── index.ts │ │ └── types.ts │ ├── fast-shift-array.ts │ ├── helpers.ts │ ├── index.ts │ ├── murmur-hash.ts │ ├── read-string.ts │ ├── resolve-src.ts │ └── wasm.ts ├── parser │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── parse-chars.test.ts.snap │ │ │ └── parser.test.ts.snap │ │ ├── fixtures │ │ │ ├── parse-text-voyd-file.ts │ │ │ ├── raw-parser-ast.ts │ │ │ └── voyd-file.ts │ │ ├── parse-chars.test.ts │ │ └── parser.test.ts │ ├── char-stream.ts │ ├── grammar.ts │ ├── index.ts │ ├── lexer.ts │ ├── parse-chars.ts │ ├── parser.ts │ ├── reader-macros │ │ ├── array-literal.ts │ │ ├── boolean.ts │ │ ├── comment.ts │ │ ├── float.ts │ │ ├── generics.ts │ │ ├── html │ │ │ ├── html-parser.ts │ │ │ └── html.ts │ │ ├── index.ts │ │ ├── int.ts │ │ ├── map-literal.ts │ │ ├── object-literal.ts │ │ ├── scientific-e-notation.ts │ │ ├── string.ts │ │ └── types.ts │ ├── syntax-macros │ │ ├── functional-notation.ts │ │ ├── index.ts │ │ ├── interpret-whitespace.ts │ │ ├── primary.ts │ │ └── types.ts │ ├── token.ts │ └── utils │ │ ├── index.ts │ │ ├── parse-directory.ts │ │ ├── parse-file.ts │ │ ├── parse-module.ts │ │ └── parse-std.ts ├── run.ts ├── semantics │ ├── README.md │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── modules.test.ts.snap │ │ │ └── regular-macros.test.ts.snap │ │ ├── fixtures │ │ │ ├── check-types.ts │ │ │ ├── regular-macros-ast.ts │ │ │ └── regular-macros-voyd-file.ts │ │ ├── modules.test.ts │ │ └── regular-macros.test.ts │ ├── check-types.ts │ ├── index.ts │ ├── init-entities.ts │ ├── init-primitive-types.ts │ ├── modules.ts │ ├── regular-macros.ts │ ├── resolution │ │ ├── __tests__ │ │ │ └── get-call-fn.test.ts │ │ ├── combine-types.ts │ │ ├── get-call-fn.ts │ │ ├── get-expr-type.ts │ │ ├── index.ts │ │ ├── resolve-call.ts │ │ ├── resolve-entities.ts │ │ ├── resolve-fn.ts │ │ ├── resolve-impl.ts │ │ ├── resolve-intersection.ts │ │ ├── resolve-match.ts │ │ ├── resolve-object-type.ts │ │ ├── resolve-trait.ts │ │ ├── resolve-type-expr.ts │ │ ├── resolve-union.ts │ │ ├── resolve-use.ts │ │ └── types-are-compatible.ts │ └── types.ts └── syntax-objects │ ├── README.md │ ├── block.ts │ ├── bool.ts │ ├── call.ts │ ├── declaration.ts │ ├── expr.ts │ ├── float.ts │ ├── fn.ts │ ├── global.ts │ ├── identifier.ts │ ├── implementation.ts │ ├── index.ts │ ├── int.ts │ ├── lib │ ├── child-list.ts │ ├── child.ts │ ├── get-id-str.ts │ ├── helpers.ts │ └── lexical-context.ts │ ├── list.ts │ ├── macro-lambda.ts │ ├── macro-variable.ts │ ├── macros.ts │ ├── match.ts │ ├── module.ts │ ├── named-entity.ts │ ├── nop.ts │ ├── object-literal.ts │ ├── parameter.ts │ ├── scoped-entity.ts │ ├── string-literal.ts │ ├── syntax.ts │ ├── trait.ts │ ├── types.ts │ ├── use.ts │ ├── variable.ts │ └── whitespace.ts ├── std ├── array.voyd ├── fixed_array.voyd ├── index.voyd ├── macros.voyd ├── map.voyd ├── operators.voyd ├── optional.voyd ├── string_lib.voyd └── utils.voyd └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/no-explicit-any": false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.voyd linguist-language=Swift 2 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | timeout-minutes: 5 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Use Node.js 12 | uses: actions/setup-node@v4 13 | with: 14 | node-version: "22.x" 15 | - run: npm ci 16 | - run: npm test 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | ast.json 4 | *output*.json 5 | *output*.wat 6 | .env 7 | test.voyd 8 | /playground.* 9 | /playground 10 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": ["/**"], 12 | "program": "${workspaceFolder}/dist/index.js", 13 | "args": ["example.dm"], 14 | "outFiles": ["${workspaceFolder}/**/*.js"] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "binaryen", 4 | "cond", 5 | "curlys", 6 | "Defun", 7 | "Deno", 8 | "desugarred", 9 | "EBNF", 10 | "elif", 11 | "elifs", 12 | "externref", 13 | "funcref", 14 | "impls", 15 | "Inout", 16 | "memcpy", 17 | "Multivalue", 18 | "popcnt", 19 | "printstr", 20 | "rotr", 21 | "structref", 22 | "Structs", 23 | "UFCS", 24 | "WASM", 25 | "webassembly" 26 | ], 27 | "debug.javascript.terminalOptions": { 28 | "skipFiles": ["/**"], 29 | "outFiles": ["${workspaceFolder}/**/*.js"] 30 | }, 31 | "editor.insertSpaces": true, 32 | "editor.tabSize": 2, 33 | "editor.wordSeparators": "`~!@#$%^&*()=[{]}\\|;:'\",.<>/?", 34 | "[markdown]": { 35 | "editor.unicodeHighlight.ambiguousCharacters": false, 36 | "editor.unicodeHighlight.invisibleCharacters": false, 37 | "diffEditor.ignoreTrimWhitespace": false, 38 | "editor.wordWrap": "on", 39 | "editor.quickSuggestions": { 40 | "comments": "off", 41 | "strings": "off", 42 | "other": "off" 43 | }, 44 | "editor.useTabStops": true, 45 | "editor.tabSize": 2, 46 | "cSpell.fixSpellingWithRenameProvider": true, 47 | "cSpell.advanced.feature.useReferenceProviderWithRename": true, 48 | "cSpell.advanced.feature.useReferenceProviderRemove": "/^#+\\s/" 49 | }, 50 | "[voyd]": { 51 | "editor.tabSize": 2 52 | }, 53 | "vitest.nodeExecutable": "" 54 | } 55 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Voyd Language Maintainer Docs 2 | 3 | Docs intended for maintainers of the voyd language 4 | -------------------------------------------------------------------------------- /docs/notes/language-benefits.md: -------------------------------------------------------------------------------- 1 | # Ergonomics 2 | 3 | ## Hybrid Nominal And Structural Sub-typing 4 | 5 | ``` 6 | obj Robot 7 | 8 | obj ArmRobot extends Robot { 9 | vise: Vise 10 | } 11 | 12 | obj HumanoidRobot extends Robot { 13 | vise: Vise 14 | legs: Legs 15 | } 16 | 17 | obj DogRobot extends Robot { 18 | legs: Legs 19 | } 20 | 21 | obj Human { 22 | legs: Legs 23 | } 24 | 25 | trait Gripper 26 | fn grip(self) async -> void 27 | fn un_grip(self) async -> void 28 | 29 | trait Moveable 30 | fn move_to(self, location: Location) async -> void 31 | 32 | // Provides implementation for ArmRobot and HumanoidRobot 33 | impl Gripper for Robot & { vise: Vise } 34 | fn grip(self) async -> void 35 | await! self.vise.close() 36 | 37 | fn un_grip(self) async -> void 38 | await! self.vise.open() 39 | 40 | // Provides implementation for DogRobot and HumanoidRobot, but not Human as its not a Robot 41 | impl Moveable for Robot & { legs: Legs } 42 | fn grip(self) async -> void 43 | await! self.vise.close() 44 | 45 | fn un_grip(self) async -> void 46 | await! self.vise.open() 47 | ``` 48 | 49 | # Common Errors In TypeScript Solved 50 | 51 | ## Failure to initialize all class properties 52 | 53 | ```typescript 54 | export class Parameter extends NamedEntity { 55 | readonly syntaxType = "parameter"; 56 | /** External label the parameter must be called with e.g. myFunc(label: value) */ 57 | label?: Identifier; 58 | type?: Type; 59 | typeExpr?: Expr; 60 | 61 | constructor( 62 | opts: NamedEntityOpts & { 63 | label?: Identifier; 64 | type?: Type; 65 | typeExpr?: Expr; 66 | } 67 | ) { 68 | super(opts); 69 | this.label = opts.label; 70 | this.type = opts.type; 71 | // This is missing and not caught by the compiler 72 | // this.typeExpr = opts.typeExpr; 73 | } 74 | } 75 | ``` 76 | 77 | Voyd avoyds this common error as the constructor / init function 78 | is not defined by the user. This problem still exists on overloaded initializers 79 | -------------------------------------------------------------------------------- /docs/notes/resources.md: -------------------------------------------------------------------------------- 1 | # Useful Resources 2 | 3 | - [Rust Compiler Architecture](https://rustc-dev-guide.rust-lang.org/part-2-intro.html) 4 | - [Scheme Programming Language Reference](https://www.scheme.com/tspl4/) 5 | - [Integrating Nominal and Structural Subtyping Paper](https://www.cs.cmu.edu/~aldrich/papers/ecoop08.pdf) 6 | -------------------------------------------------------------------------------- /docs/structural-subtyping.md: -------------------------------------------------------------------------------- 1 | 2 | Context: 3 | ```voyd 4 | obj A { 5 | x: i32 6 | } 7 | 8 | // b is any subtype of A that also has the field y: i32 9 | fn sum(b: A & { y: i32 }) -> i32 10 | // Field x can be directly accessed via wasm struct.get, as we know b is a supertype of a and a contains x 11 | b.x + 12 | 13 | // Field y cannot be directly accessed because we do not know the supertype of b that defines y field 14 | b.y 15 | 16 | obj B extends A { 17 | y: i32 18 | } 19 | 20 | fn main() -> i32 21 | let b = B { x: 3, y: 3 } 22 | sum(b) 23 | 24 | ``` 25 | 26 | Implementation (Psuedo Voyd / WASM hybrid): 27 | ```voyd 28 | // All objects implicitly extend Object 29 | type Object = { 30 | // All objects that can be used to access a member of an the object. 31 | // A member can be a field or a method. 32 | // This function is used on parameters who's type is a structural or a trait object 33 | // It is up to the caller to know the signature of funcref, although the first parameter is always self 34 | get_member: (member_id: i32) -> funcref 35 | } 36 | 37 | fn b_get_x(self: anyref) -> i32 38 | struct.get(ref.cast(self, B), x) 39 | 40 | fn b_get_y(self: anyref) -> i32 41 | struct.get(ref.cast(self, B), y) 42 | 43 | fn get_member_of_b(member_id: i32) -> funcref 44 | if member_id == hash_i32(x) 45 | return ref.func(b_get_x) 46 | 47 | if member_id == hash_i32(y) 48 | return ref.func(b_get_y) 49 | 50 | 51 | fn sum(b: A) -> i32 52 | // Field x can be directly accessed via wasm struct.get, as we know b is a supertype of a and a contains x 53 | b.x + 54 | 55 | // Field y cannot be directly accessed because we do not know the supertype of b that defines y field 56 | ref.call(b.get_member(hash_i32(x)), b) 57 | 58 | fn main() -> i32 59 | let b = B { 60 | x: 3, 61 | y: 3, 62 | get_member: ref.func(get_member_of_b) 63 | } 64 | ``` 65 | -------------------------------------------------------------------------------- /example.voyd: -------------------------------------------------------------------------------- 1 | use std::macros::all 2 | 3 | fn fib(n: i32) -> i32 4 | if n <= 1 then: 5 | n 6 | else: 7 | fib(n - 1) + fib(n - 2) 8 | 9 | fn main() 10 | let x = 10 + 11 | 20 + 12 | 30 13 | 14 | let y = if x > 10 15 | then: 16 | 10 17 | else: 18 | 20 19 | 20 | call this while () => if x > 10 then: 21 | x -= 1 22 | else: 23 | x += 1 24 | 25 | let n = if args.len() > 1 then: 26 | console.log("Hey there!") 27 | args.at(1).parseInt().unwrap() 28 | else: 29 | 10 30 | 31 | let x2 = 10 32 | let z = nothing() 33 | let test_spacing = fib n 34 | let result = fib(n) 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voyd", 3 | "version": "0.7.0", 4 | "description": "", 5 | "module": "dist/index.js", 6 | "type": "module", 7 | "types": "./src/index.ts", 8 | "exports": "./dist/index.js", 9 | "scripts": { 10 | "test": "vitest", 11 | "snapshot": "vitest -u", 12 | "build": "npx tsc", 13 | "prepublishOnly": "tsc" 14 | }, 15 | "bin": { 16 | "voyd": "./dist/cli/cli.js", 17 | "vt": "./src/cli/cli-dev.ts" 18 | }, 19 | "keywords": [], 20 | "author": "", 21 | "license": "ISC", 22 | "devDependencies": { 23 | "@types/node": "^22.5.1", 24 | "@typescript-eslint/eslint-plugin": "^4.4.0", 25 | "@typescript-eslint/parser": "^4.4.0", 26 | "@vitest/ui": "^2.0.5", 27 | "eslint": "^7.10.0", 28 | "ts-node": "^10.9.2", 29 | "typescript": "^5.6.2", 30 | "vitest": "^2.0.5" 31 | }, 32 | "dependencies": { 33 | "@types/uniqid": "^4.1.3", 34 | "binaryen": "^119.0.0", 35 | "commander": "^5.1.0", 36 | "eventemitter2": "^6.4.2", 37 | "glob": "^10.3.10", 38 | "uniqid": "^5.2.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /reference/basics.md: -------------------------------------------------------------------------------- 1 | # Comments 2 | 3 | ```voyd 4 | // This is a single line comment 5 | ``` 6 | # Identifiers 7 | 8 | Identifiers bind names to values, types, and other language constructs. They can 9 | contain letters, numbers, and underscores, and must start with a letter or underscore. 10 | 11 | ```voyd 12 | hey 13 | hey_there 14 | MyType 15 | MyType2 16 | ``` 17 | 18 | # Variables 19 | 20 | ```voyd 21 | // Declare an immutable variable 22 | let x = 5 23 | 24 | // Declare a mutable variable (see note) 25 | var y = 3 26 | ``` 27 | 28 | # String Literals 29 | 30 | ```voyd 31 | // Strings are defined with double quotes 32 | let name = "John" 33 | 34 | // Double quotes also support multi-line strings 35 | let address = " 36 | 123 Main St 37 | Anytown, USA 38 | " 39 | // String interpolation is also supported by double quotes and the {} syntax 40 | let greeting = "Hello, ${name}" 41 | ``` 42 | 43 | # Numeric Literals 44 | 45 | ```voyd 46 | // Integers 47 | let x = 5 48 | 49 | // Floats 50 | let y = 3.14 51 | ``` 52 | 53 | # Boolean Literals 54 | 55 | ```voyd 56 | let x = true 57 | let y = false 58 | ``` 59 | 60 | # Object literals 61 | 62 | ```voyd 63 | let value = { 64 | a: 5, 65 | b: 4 66 | } 67 | 68 | let x = value.a // x will be bound to 5 69 | ``` 70 | 71 | Field shorthand: 72 | 73 | ```voyd 74 | let a = 5 75 | let value = { a, b: 4 } 76 | 77 | // Equivalent to 78 | let value = { a: a, b: 4 } 79 | ``` 80 | 81 | # Tuple literals 82 | 83 | ```voyd 84 | let value = (5, 4) 85 | let x = value.0 // x will be bound to 5 86 | 87 | // Destructuring 88 | let (a, b) = value 89 | ``` 90 | 91 | # Control Flow 92 | 93 | ```voyd 94 | // If statements 95 | if x > 5 then: 96 | // Do something 97 | else: 98 | // Do something else 99 | 100 | // When else is not needed both `then:` and `else:` can be omitted 101 | if x > 5 102 | // Do something 103 | 104 | // While loops 105 | while x > 5 106 | // Do something 107 | 108 | // For loops 109 | for i in 0..10 110 | // Do something 111 | 112 | // Blocks* 113 | let y = ${ 114 | let x = 5 115 | 116 | // The result of the block is the result of the last statement, y will be bound to 8 117 | x + 3 118 | } 119 | 120 | // Blocks are implicitly defined by 2 space indentation, so this is also valid. 121 | let y = 122 | let x = 123 | 2 + 3 124 | x + 3 // y will be bound to 8 125 | ``` 126 | 127 | * The ${} syntax is useful to restore indentation sensitivity in non-indentation sensitive contexts, such as in function arguments, string literals, object literals etc. See the syntax reference for more details. 128 | 129 | # Expressions 130 | 131 | Expressions are statements that return a value. They can be used in a variety of contexts, such as variable assignment, function arguments, and more. 132 | 133 | ```voyd 134 | // Binary expressions 135 | let x = 5 + 3 136 | 137 | // Function calls 138 | let y = add(5, 3) 139 | ``` 140 | 141 | Virtually every statement in Voyd is an expression, including control flow statements. 142 | 143 | ```voyd 144 | let x = if true then: 1 else: 2 145 | 146 | // Blocks are also expressions, returning the result of the last statement 147 | let y = 148 | let x = 5 149 | x + 3 150 | 151 | // Both z and u will be bound to 5 152 | let z = let u = 5 153 | ``` 154 | -------------------------------------------------------------------------------- /reference/control-flow.md: -------------------------------------------------------------------------------- 1 | # Control Flow 2 | 3 | ## Match 4 | 5 | Used to narrow types. Can operate on object to narrow 6 | child objects or on unions to narrow union members 7 | 8 | Signature(s): 9 | ``` 10 | fn match(val: T, body: MatchBlock) -> U 11 | fn match(val: T, bind_identifier: Identifier, body: MatchBlock) -> U 12 | ``` 13 | 14 | Example: 15 | ```voyd 16 | obj Optional 17 | 18 | obj None extends Optional 19 | 20 | obj Some extends Optional { 21 | value: i32 22 | } 23 | 24 | fn divide(a: i32, b: i32) -> Optional 25 | if b == 0 26 | None {} 27 | else: 28 | Some { value: a / b } 29 | 30 | fn main(a: i32, b: i32) -> String 31 | let x = a.divide(b) 32 | match(x) 33 | Some: "The value is ${x}" 34 | None: "Error: divide by zero" 35 | else: "Bleh" 36 | ``` 37 | 38 | The second signature of match is useful when the value being matched against 39 | is not already bound to an identifier (i.e. dot pipelines): 40 | ```voyd 41 | fn main(a: i32, b: i32) -> String 42 | a.divide(b) 43 | .match(x) // Here, match binds the result of the previous expression to x 44 | Some: "The value is ${x}" 45 | None: "Error: divide by zero" 46 | ``` 47 | -------------------------------------------------------------------------------- /reference/declare.md: -------------------------------------------------------------------------------- 1 | 2 | ``` 3 | declare "namespace" 4 | pub fn hey() -> void 5 | ``` 6 | -------------------------------------------------------------------------------- /reference/memory.md: -------------------------------------------------------------------------------- 1 | # Memory 2 | 3 | ## Value vs Reference Types 4 | 5 | - Value types are passed by value. When a variable or parameter is assigned to a value type, they are given a copy of the value. 6 | - Reference types are passed by reference. When a variable or parameter is assigned to a reference type, they are given a reference to the value. 7 | 8 | Objects are reference types, and all other types are value types. 9 | 10 | ## Ownership 11 | 12 | Ownership is a set of rules that apply to reference types. 13 | 14 | They are as follows: 15 | - An owner refers to the variable or parameter that holds the reference to an instance of a reference type. 16 | - A mutable reference can only have one owner at a time. 17 | - An immutable reference can have any number of owners at a time. 18 | - References can be borrowed from an owner via assignment or function call. The borrow is returned once the new owner goes out of scope. 19 | - A mutable reference can be converted to an immutable reference by assigning it to an immutable reference variable or parameter. When ownership is returned to its original owner, the reference is converted back to a mutable reference. 20 | 21 | ## Mutability 22 | 23 | Normal Usage: 24 | 25 | ``` 26 | // Given an object 27 | obj Vec3 { 28 | x: i32 29 | y: i32 30 | z: i32 31 | } 32 | 33 | fn bump_x(v: &Vec3) -> Vec3 34 | v.x += 1 35 | 36 | fn main() 37 | let mutable = &Vec3 { x: 1, y: 2, z: 3 } 38 | mutable.bump_x() // OK 39 | mutable.x = 5 // OK 40 | 41 | // Examples of immutable behavior 42 | let immutable = Vec3 { x: 1, y: 2, z: 3 } 43 | 44 | // ERROR: Cannot borrow immutable reference as mutable 45 | immutable.bump_x() 46 | immutable.x = 5 47 | 48 | // Mutable assignment to a mutable reference is converted to an immutable reference unless explicitly borrowed 49 | let mutable2 = &Vec3 { x: 1, y: 2, z: 3 } 50 | let immutable2 = mutable2 51 | immutable2.x = 5 // ERROR: Cannot borrow immutable reference as mutable 52 | let mutable3 = &mutable2 53 | mutable3.x = 5 // OK 54 | mutable2.x = 5 // ERROR ownership of mutable2 has been transferred to mutable3, mutable2 is now immutable 55 | ``` 56 | 57 | Mutable references can only be made to object types: 58 | 59 | ```voyd 60 | // Error cannot borrow primitive type as mutable 61 | fn bump(x: &i32) -> i32 62 | x += 1 63 | 64 | fn main() 65 | var x = 5 66 | 67 | // Ok 68 | x = 3 69 | 70 | // Error 71 | bump(&x) 72 | ``` 73 | -------------------------------------------------------------------------------- /reference/mutability.md: -------------------------------------------------------------------------------- 1 | # Mutability In Voyd 2 | 3 | ## Variable Mutability 4 | 5 | Variable mutability determines if a variable can be **reassigned** or not. 6 | 7 | ``` 8 | // Immutable variable 9 | let x = 5 10 | x = 6 // Error: Cannot reassign to immutable variable 11 | 12 | // Mutable variable 13 | var y = 3 14 | y = 4 // y is now 4 15 | ``` 16 | 17 | ## Object Mutability 18 | 19 | Object mutability determines if the **fields** of an object can be **reassigned** 20 | or not. 21 | 22 | Note that variable mutability is different from object mutability. A mutable 23 | variable can still hold an immutable object and vice versa. 24 | 25 | ``` 26 | type Point = { 27 | x: Int, 28 | y: Int 29 | } 30 | 31 | // Immutable object 32 | let p1 = Point { x: 5, y: 4 } 33 | p1.x = 6 // Error: Cannot reassign to immutable field 34 | 35 | // Mutable object 36 | let p2 = &Point { x: 5, y: 4 } 37 | p2.x = 6 // p2.x is now 6 38 | 39 | // Variables can be mutable while holding an immutable object 40 | var p3 = Point { x: 5, y: 4 } 41 | p3.x = 6 // Error: Cannot reassign to immutable field 42 | 43 | p3 = Point { x: 6, y: 4 } // p3 is now a new object 44 | ``` 45 | 46 | When passing a mutable object to a function 47 | -------------------------------------------------------------------------------- /reference/spec/README.md: -------------------------------------------------------------------------------- 1 | # Voyd Language Spec 2 | 3 | This is the official specification for the Voyd Language. It is intended to be a 4 | comprehensive but is currently a work in progress. 5 | 6 | The eventual goal is to have a complete and accurate specification for the 7 | language, including both the surface and core languages. And should allow for 8 | alternative implementations to be written from it. 9 | 10 | **Audience** 11 | 12 | This spec is primary as a reference for Voyd Language developers. Though it may 13 | be useful for users of the language as well, especially those writing libraries 14 | and working with macros. 15 | 16 | **Structure** 17 | 18 | This specification is broken down into two main parts: 19 | 20 | - [The Surface Language Specification](./surface.md) (What users write and know 21 | as the Voyd language) 22 | - [The Core Language Specification](./core.md) (What macros expand into, 23 | resembles the structure the compiler works with directly) 24 | -------------------------------------------------------------------------------- /reference/spec/surface.md: -------------------------------------------------------------------------------- 1 | # The Surface Language Specification 2 | 3 | This specification defines the language users write, the "surface" voyd 4 | language. 5 | 6 | This surface language spec includes: 7 | 8 | - The surface grammar 9 | - The surface syntax 10 | - Macros 11 | - A standard library (Built in macros, types, and functions) 12 | - More? 13 | 14 | 15 | # The Surface Language Grammar 16 | 17 | ```ebnf 18 | (* Whitespace (Significant to the surface level language) *) 19 | Whitespace = Space | Tab | Newline 20 | Space = " "; 21 | Indent = " "; 22 | NewLine = "\n"; 23 | BackSlash = "\\"; // Single back slash character \ 24 | 25 | (* Comment *) 26 | Comment = "//", { AnyChar - NewLine }, NewLine; 27 | 28 | (* Brackets *) 29 | Bracket = "{" | "}" | "[" | "]" | "(" | ")"; 30 | 31 | (* Operator Characters (does not imply infix, prefix, or postfix) *) 32 | OpChar = "+" | "-" | "*" | "/" | "=" | ":" | "?" | "." | ";" | "<" | ">" | "$" | "!" | "@" | "%" | "^" | "&" | "~" | BackSlash; 33 | Operator = (OpChar, { OpChar }); 34 | 35 | (* Terminators *) 36 | Terminator = Bracket | Whitespace | TerminatingOperator | Quote | ","; 37 | Quote = '"' | "'" | "`"; 38 | TerminatingOperator = (":" | "?" | "!" | "." | ";" | BackSlash), { OpChar } ; 39 | 40 | (* Identifier *) 41 | Identifier = StandardIdentifier | QuotedIdentifier | SharpIdentifier; 42 | StandardIdentifier = (AnyChar - (Number | Terminator)), { AnyChar - Terminator }; 43 | QuotedIdentifier = "'", { AnyChar - "'" }, "'"; 44 | SharpIdentifier = "#", AnyChar - Whitespace, { AnyChar - Whitespace }; 45 | 46 | (* Numbers *) 47 | Number = ScientificNumber | Int | Float; 48 | ScientificNumber = #'^[+-]?\d(\.\d+)?[Ee][+-]?\d+$'; 49 | Int = #'^[+-]?\d+$'; 50 | Float = #'^[+-]?\d+\.\d+$'; 51 | 52 | (* A string is a sequence of characters surrounded by double quotes *) 53 | String = '"', { AnyChar - '"' }, '"'; 54 | 55 | (* Symbol *) 56 | Symbol = Number | Identifier | String | Operator; 57 | 58 | (* List *) 59 | List = "(", { Symbol | List }, ")" 60 | 61 | (* Other *) 62 | AlphabeticChar = #'[a-zA-Z]' 63 | AnyChar = ? all valid characters (including whitespace) ?; 64 | ``` 65 | 66 | ## The Syntax Pipeline 67 | 68 | In the spirit of lisp, Voyd language is designed to be hackable. As a result, 69 | the surface language syntax is implemented entirely in macros. This makes the 70 | language both easy to maintain, and easy to extend. 71 | 72 | There are three types of macros: 73 | 74 | - Reader Macros: Expanded during parsing, emit am ast 75 | - Syntax Macros: Expanded after parsing, are passed the ast from the parser 76 | and produce the final ast 77 | - Regular Macros: Expanded by a syntax macro 78 | 79 | At a high level, the pipeline looks something like this: `file.voyd -> parser + 80 | reader macros -> syntax macros -> ast (the core language)` 81 | 82 | In the next sections, the different macros will be defined in depth. 83 | 84 | # Examples 85 | 86 | ```voyd 87 | // Translated version of a swift example from https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/ 88 | 89 | let photos = await taskGroup(of: Optional(Data).self) | () => 90 | let photoNames = await listPhotos(inGallery: "Summer Vacation") 91 | 92 | for name in photoNames 93 | group.addTaskUnlessCancelled (isCancelled) => 94 | if not(isCancelled) 95 | await downloadPhoto(named: name) 96 | 97 | await group.filter() | (photo) => photo != nil 98 | 99 | photos.map name => 100 | let photo = await downloadPhoto(named: name) 101 | photo.map(processPhoto) 102 | ``` 103 | -------------------------------------------------------------------------------- /reference/style-guide.md: -------------------------------------------------------------------------------- 1 | # Voyd Style Guide 2 | 3 | ## Naming 4 | 5 | - UpperCamelCase for Types and Components 6 | - Acronyms should only capitalize the first letter, i.e `HtmlElement` 7 | - snake_case for everything else (including file names) 8 | - Parameters beyond the first two should generally be labeled 9 | 10 | ## Indentation 11 | 12 | Two space indentation. Enforced by the compiler. 13 | 14 | **Rational** 15 | This was a tough decision. I really wanted to use tabs, the 16 | first versions even enforced tabs. Unfortunately, in practice, 17 | tabs have two many shortcomings. They are far too large by 18 | default in browsers and terminals (8 spaces) and they are too 19 | uncommon for the primary target market (web development). 20 | 21 | I also tried 4 and 3 spaces. 3 was too weird and 4 was too much for 22 | html based components (which is a major feature of the language). 23 | I find I have no difficulty with 2 spaces, and other languages (Nim) 24 | use 2 spaces without issue. 25 | 26 | Initially, I found the accessibility argument of tabs compelling. But 27 | I struggled to find any examples of research or testimony that the lack 28 | of tabs posed an accessibility issue in practice. 29 | 30 | ## Rules of thumb 31 | 32 | tldr: 33 | Always prefer obviousness, simplicity and clarity to cleverness. 34 | 35 | - Prefer composition to inheritance. Objects should only extend when they 36 | are conceptually interchangeable with their parent. This does not happen 37 | often. 38 | - Don't use a macro when a normal function could also work 39 | - Don't use overloads unless the functions conceptually do the same thing. 40 | This generally means they take the same action, but operate on different types 41 | - Don't over use effects 42 | -------------------------------------------------------------------------------- /reference/types/effects.md: -------------------------------------------------------------------------------- 1 | # Effects 2 | 3 | Effects are resumable exceptions. AKA exceptions on steroids, AKA exceptions 4 | that are actually enjoyable to work with. 5 | 6 | Effects provide a number benefits to the language, library authors, and users: 7 | - Type-safe error handling 8 | - Type-safe dependency injection 9 | - Type-safe mocking and testing 10 | - Delimited continuations 11 | - And more! 12 | 13 | Effects are a powerful language abstraction. They allow for features like async/await, exceptions, generators, and coroutines to be implemented **IN THE LANGUAGE**, rather than the implementation. Many features that are traditionally implemented in the compiler can be implemented as libraries in Voyd. This allows for more flexibility and control over the behavior of these features. It also saves users from having to wait for the language to be updated to get new features. No more waiting on endless bikeshedding discussions about the syntax of async/await! 14 | 15 | Voyd's effect system takes heavy inspiration from: 16 | - [Koka Language](https://koka-lang.github.io), which largely inspired the effects syntax 17 | - [Effeckt Language](https://effekt-lang.org/) 18 | - The paper ["Structured Asynchrony with Algebraic Effects" by Daan Leijen"](https://www.microsoft.com/en-us/research/wp-content/uploads/2017/05/asynceffects-msr-tr-2017-21.pdf) 19 | 20 | 21 | ## Defining Effects 22 | 23 | ```voyd 24 | effect Exception 25 | // An effect that may be resumed by the handler 26 | ctl throw(msg: String) -> void 27 | 28 | // Effects with one control can be defined concisely as 29 | effect ctl throw(msg: String) -> void 30 | 31 | effect State 32 | // Tail resumptive effect, guaranteed to resume exactly once. 33 | // Are defined like normal functions 34 | fn get() -> Int 35 | fn set(x: Int) -> void 36 | 37 | // Tail resumptive effects with one function can be defined concisely as 38 | effect fn get() -> Int 39 | ``` 40 | 41 | ## Using Effects 42 | 43 | ```voyd 44 | effect Async 45 | ctl resolve(int: i32) -> void 46 | ctl reject(msg: String) -> void 47 | 48 | fn asyncTask(num: i32): Async -> Int 49 | if num > 0 50 | Async::resolve(num) 51 | else 52 | Async::reject("Number must be positive") 53 | 54 | fn main() 55 | try 56 | asyncTask(1) 57 | with: { 58 | ctl resolve(num) -> void 59 | println("Resolved: " + num) 60 | ctl reject(msg) -> void 61 | println("Rejected: " + msg) 62 | } 63 | 64 | // Effects are also inferred 65 | fn asyncTask(num: i32) -> Int // Inferred to be Async -> Int 66 | if num > 0 67 | Async::resolve(num) 68 | else 69 | Async::reject("Number must be positive") 70 | 71 | // A use statement can may the function a little cleaner 72 | use Async::{resolve, reject} 73 | 74 | fn asyncTask(num: i32) -> Int 75 | if num > 0 76 | resolve(num) 77 | else 78 | reject("Number must be positive") 79 | ``` 80 | -------------------------------------------------------------------------------- /reference/types/intersections.md: -------------------------------------------------------------------------------- 1 | # Intersections 2 | 3 | Voyd uses intersection types to combine the fields of multiple objects into a 4 | single type. 5 | 6 | An intersection type is defined by listing the types it is composed of separated 7 | by `&`. 8 | 9 | ``` 10 | type Vec2D = { 11 | a: i32, 12 | b: i32 13 | } 14 | 15 | type Vec3D = Vec2D & { 16 | c: i32 17 | } 18 | ``` 19 | 20 | the type expression of `Vec3D` resolves to: 21 | 22 | ``` 23 | type Vec3D = Object & { 24 | a: i32, 25 | b: i32, 26 | c: i32 27 | } 28 | ``` 29 | 30 | Note that the fields of an intersection cannot conflict: 31 | 32 | ``` 33 | type Vec2D = { 34 | a: i32, 35 | b: i32 36 | } 37 | 38 | type Vec3D = Vec2D & { 39 | // Error - Conflicts with intersected field b: i32 40 | b: string, 41 | c: i32 42 | } 43 | ``` 44 | 45 | ## Intersection Types and Nominal Objects 46 | 47 | When an intersection includes a nominal object, the object must be a subtype of 48 | that object. 49 | 50 | ``` 51 | obj Animal { 52 | name: String 53 | } 54 | 55 | type AnimalWithLives = Animal & { 56 | lives: i32 57 | } 58 | 59 | let newt: Cat = Animal { name: "Whiskers" } & { lives: 3 } 60 | 61 | // An implicit AnimalWithLives initializer is also available, to avoyd writing out the & 62 | let newt = AnimalWithLives { name: "Whiskers", lives: 3 } 63 | 64 | // We can define a new compatible nominal object 65 | obj Cat extends Animal { 66 | lives: i32 67 | } 68 | 69 | let cat = Cat { name: "Simba", lives: 9 } 70 | 71 | // Some form of initializer is needed, this example is missing the Animal nominal parent 72 | let bad_cat: Cat = { name: "Ghost", lives: 9 } // Error - { name: "Ghost", lives: 9 } is not an Animal 73 | ``` 74 | 75 | All object types of an intersection must be a subtype of the previous 76 | intersected object type, or a parent type of the previous intersected object type. 77 | 78 | ``` 79 | obj Animal { 80 | name: String 81 | } 82 | 83 | obj Cat extends Animal { 84 | lives_remaining: i32 85 | } 86 | 87 | obj Dog extends Animal { 88 | likes_belly_rubs: bool 89 | } 90 | 91 | type Chihuahua = 92 | Dog & { size: i32 } & 93 | Animal & { age: i32 } // This is ok since Animal is a parent of Dog 94 | 95 | // Resolves to: 96 | type Chihuahua = Dog & { 97 | name: String, 98 | likes_belly_rubs: bool, 99 | age: i32, 100 | size: i32 101 | } 102 | 103 | // Error Dog is not a subtype of Cat 104 | type Abomination = Cat & Dog 105 | ``` 106 | 107 | ## Intersection Types and Traits 108 | 109 | An intersection type can combine multiple traits to define a type that must 110 | satisfy all of the traits. 111 | 112 | ``` 113 | 114 | trait Image 115 | fn draw(self) -> Array[Rgb] 116 | 117 | trait Movable 118 | fn move(&self, x: i32, y: i32) -> void 119 | 120 | type MoveableImage = Movable & Drawable 121 | 122 | obj Shape { 123 | image: Array 124 | x: i32 125 | y: i32 126 | } 127 | 128 | impl Image for Shape 129 | fn draw(self) -> Array 130 | self.image 131 | 132 | impl Movable for Shape 133 | fn move(&self, x: i32, y: i32) -> void 134 | self.x += x 135 | self.y += y 136 | 137 | let shape: MoveableImage = Shape { image: [Rgb(0, 0, 0)], x: 0, y: 0 } 138 | ``` 139 | ## Technical Deep Dive 140 | 141 | An intersection is always made up of two parts: It's nominal supertype and its 142 | structural type. The structural type must always be compatible with the nominal 143 | type. 144 | 145 | ``` 146 | & 147 | ``` 148 | 149 | Chained intersections always resolve down to those two parts 150 | ``` 151 | obj Animal { 152 | name: string 153 | } 154 | 155 | type Cat = Animal & { age: i32 } & { lives: i32 } 156 | 157 | // Equivalent too 158 | type Cat = Animal & { 159 | name: string, 160 | age: i32, 161 | lives: i32 162 | } 163 | ``` 164 | 165 | All object literals are an intersection between `Object` and their structure 166 | ``` 167 | let Vec2D = { 168 | a: i32, 169 | b: i32 170 | } 171 | 172 | // Equivalent to: 173 | let Vec2D = Object & { 174 | a: i32, 175 | b: i32 176 | } 177 | ``` 178 | -------------------------------------------------------------------------------- /reference/types/overview.md: -------------------------------------------------------------------------------- 1 | # Types Overview 2 | 3 | The Voyd type system is structural at its core and supports nominal types 4 | through the use of objects and traits. 5 | 6 | - [Types Overview](#types-overview) 7 | - [Defining Types](#defining-types) 8 | - [Data Types](#data-types) 9 | - [Primitive Data Types](#primitive-data-types) 10 | - [Objects](#objects) 11 | 12 | A type comes in a few categories: 13 | - `Data` - The most basic types that store bits of information 14 | - `Function` - Types that represent functions 15 | - `Trait` - Types that represent a collection of function types that can be 16 | implemented by a type 17 | - `Effect` - Types that represent side effects a function can have 18 | 19 | All but effect types in Voyd are first class, that is they can be passed as 20 | arguments to functions, returned from functions, and assigned to variables. 21 | 22 | # Defining Types 23 | 24 | Types are typically defined using the `type` keyword followed by the type name, 25 | an equal sign, and a type expression representing how the type is satisfied. 26 | 27 | The most basic type definition is an alias for another type: 28 | 29 | ```voyd 30 | type Name = String 31 | ``` 32 | 33 | # Data Types 34 | 35 | Data types store bits of information that can be operated on. All data types 36 | are either value types or reference types. 37 | 38 | Value types are copied when passed to a function or assigned to a variable. They 39 | are stored on the stack. 40 | 41 | Reference types are heap allocated, passed by reference. This means that two 42 | variables can point to the same reference type. There are rules for this shared 43 | ownership which are detailed in the memory chapter. 44 | 45 | ## Primitive Data Types 46 | 47 | Primitive data types are the most basic data types in Voyd. They are value types 48 | that act as the building blocks for more complex data types. 49 | 50 | They include: 51 | - `i32` - 32 bit signed integer 52 | - `i64` - 64 bit signed integer 53 | - `u32` - 32 bit unsigned integer 54 | - `u64` - 64 bit unsigned integer 55 | - `f32` - 32 bit floating point number 56 | - `f64` - 64 bit floating point number 57 | - `v128` - 128 bit SIMD vector 58 | - `bool` - Boolean (technically an i32 considered to be false when 0 and true 59 | when non-zero) 60 | - `voyd` - The absence of a value 61 | 62 | TODO: Add more information about each of these types. 63 | 64 | ## Objects 65 | 66 | Objects are extensible data types that can be used to represent complex user 67 | defined data structures. See the [Objects](./objects.md) chapter for more details. 68 | -------------------------------------------------------------------------------- /reference/types/structs.md: -------------------------------------------------------------------------------- 1 | # Structs 2 | 3 | Structs are a value data type that represent a fixed collection of key value 4 | pairs (fields). 5 | 6 | Unlike objects, structs are copied when passed to a function or assigned to a 7 | variable. And they are stored on the stack. 8 | 9 | They are defined by listing their fields between curly braces prefixed with 10 | the percent sign `%{}`. 11 | 12 | ```voyd 13 | type MyStruct = %{ 14 | a: i32, 15 | b: i32 16 | } 17 | ``` 18 | 19 | Structs must be of fixed size and formed of either other structs or primitive 20 | types. They cannot contain reference types (for now). 21 | 22 | # Tuple Structs 23 | 24 | Tuple structs are a fixed sequence of values of different types. They 25 | are defined by listing their types between square braces prefixed with the 26 | percent sign `%()`. 27 | 28 | ```voyd 29 | type MyTupleStruct = %(i32, bool) 30 | 31 | let my_tuple_struct: MyTupleStruct = %(1, true) 32 | 33 | let x = my_tuple_struct.0 34 | ``` 35 | 36 | They are effectively syntactic sugar for a struct with incrementing 37 | integer keys. 38 | 39 | ```voyd 40 | type MyTupleStruct = %(i32, bool) 41 | 42 | // Resolves to 43 | type MyTupleStruct = %{ 44 | 0: i32, 45 | 1: bool 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /reference/types/traits.md: -------------------------------------------------------------------------------- 1 | # Traits 2 | 3 | > Status: Partially implemented 4 | 5 | Traits are first class types that define the behavior of a nominal object or intersection. 6 | 7 | ```voyd 8 | trait Run 9 | fn run(self) -> String 10 | fn stop(&self) -> void 11 | 12 | obj Car { 13 | speed: i32 14 | } 15 | 16 | impl Run for Car 17 | fn run(self) -> String 18 | "Vroom!" 19 | 20 | fn stop(&self) -> void 21 | self.speed = 0 22 | 23 | let car = Car { speed: 10 } 24 | log car.run() // "Vroom!" 25 | &car.stop() 26 | 27 | car has_trait Run // true 28 | 29 | // Because traits are first class types, they can be used to define parameters 30 | // that will accept any type that implements the trait 31 | fn run_thing(thing: Run) -> void 32 | log thing.run() 33 | 34 | run_thing(car) // Vroom! 35 | ``` 36 | 37 | ## Default Implementations 38 | 39 | > Status: Complete 40 | 41 | Traits can specify default implementations which are automatically applied 42 | on implementation, but may still be overridden by that impl if desired 43 | 44 | ```voyd 45 | trait One 46 | fn one() -> i32 47 | 1 48 | ``` 49 | 50 | ## Trait Requirements 51 | 52 | > Status: Not yet implemented 53 | 54 | Traits can specify that implementors must also implement other traits: 55 | 56 | ```voyd 57 | trait DoWork requires: This & That 58 | ``` 59 | 60 | ## Trait Scoping 61 | 62 | > Status: Not yet implemented 63 | 64 | Traits must be in scope to be used. If the `Run` trait were defined 65 | in a different file (or module), it would have to be imported before its 66 | methods could be used 67 | 68 | ```voyd 69 | car.run() // Error, no function found for run 70 | 71 | use other_file::{ Run } 72 | 73 | car.run() // Vroom! 74 | ``` 75 | 76 | ## Trait limitations 77 | 78 | Trait implementations cannot have overlapping target types: 79 | 80 | ```voyd 81 | obj Animal {} 82 | obj Dog {} 83 | 84 | trait Speak 85 | fn speak() -> void 86 | 87 | impl Speak for Animal 88 | fn speak() 89 | log "Glub glub" 90 | 91 | impl Speak for Dog // ERROR: Speak is already implemented for Dog via parent type Animal 92 | ``` 93 | -------------------------------------------------------------------------------- /reference/types/tuples.md: -------------------------------------------------------------------------------- 1 | # Tuples 2 | 3 | Tuples are a fixed sequence of values of different types. 4 | 5 | ```voyd 6 | type MyTuple = (i32, String, bool) 7 | let my_tuple: MyTuple = (1, "hello", true) 8 | 9 | let x = my_tuple.0 10 | let y = my_tuple.1 11 | let z = my_tuple.2 12 | 13 | // Tuples can also be destructured 14 | let (a, b, c) = my_tuple 15 | ``` 16 | -------------------------------------------------------------------------------- /reference/types/unions.md: -------------------------------------------------------------------------------- 1 | # Unions 2 | 3 | Union types represent a value that can be one of a predefined set of types. 4 | 5 | A union type is defined by listing each of the types it may be, separated by the 6 | pipe operator, `|`. 7 | 8 | ```voyd 9 | type Animal = Cat | Dog 10 | 11 | obj Cat { 12 | age: i32 13 | name: String 14 | } 15 | 16 | obj Dog { 17 | age: i32 18 | name: String 19 | } 20 | ``` 21 | 22 | In some cases, where the nominal object is only ever used as part of a union, 23 | union sugar can be used 24 | 25 | ```voyd 26 | union Drink 27 | Coffee { size: Size, sugar: Grams, cream: Grams } 28 | Tea { size: Size, sugar: Grams, cream: Grams } 29 | Soda { size: Size } 30 | Water 31 | 32 | let drink: Drink = Drink::Soda { size: Medium() } 33 | 34 | // Resolves to: 35 | type Drink = 36 | (obj Coffee { size: Size, sugar: Grams, cream: Grams }) | 37 | (obj Tea { size: Size, sugar: Grams, cream: Grams }) | 38 | (obj Soda { size: Size }) | 39 | (obj Water) | 40 | ``` 41 | 42 | ## Calling Methods Of A Union Type 43 | 44 | If all objects of a union have a method with the same signature 45 | (other than self (mutability excluded)). That method can be called 46 | directly from the union 47 | 48 | ```voyd 49 | type Animal = Cat | Dog 50 | 51 | obj Cat {} 52 | obj Dog {} 53 | 54 | impl Cat 55 | pub fn speak(self) 56 | self.meow() 57 | 58 | pub fn meow(self) 59 | log "Meow" 60 | 61 | impl Dog 62 | pub fn speak(self) 63 | self.meow() 64 | 65 | pub fn woof(self) 66 | log "Woof" 67 | 68 | fn main() 69 | let animal = Animal(Dog {}) 70 | animal.speak() // Woof! 71 | animal.woof() // Error 72 | ``` 73 | 74 | Internally, the method call is expanded to a match statement. 75 | -------------------------------------------------------------------------------- /src/__tests__/compiler.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | e2eVoydText, 3 | kitchenSink, 4 | goodTypeInferenceText, 5 | tcoText, 6 | } from "./fixtures/e2e-file.js"; 7 | import { compile } from "../compiler.js"; 8 | import { describe, test, vi } from "vitest"; 9 | import assert from "node:assert"; 10 | import { getWasmFn, getWasmInstance } from "../lib/wasm.js"; 11 | import * as rCallUtil from "../assembler/return-call.js"; 12 | import { readString } from "../lib/read-string.js"; 13 | 14 | describe("E2E Compiler Pipeline", () => { 15 | test("Compiler can compile and run a basic voyd program", async (t) => { 16 | const mod = await compile(e2eVoydText); 17 | const instance = getWasmInstance(mod); 18 | const fn = getWasmFn("main", instance); 19 | assert(fn, "Function exists"); 20 | t.expect(fn(), "Main function returns correct value").toEqual(55); 21 | }); 22 | 23 | test("Compiler has good inference", async (t) => { 24 | const mod = await compile(goodTypeInferenceText); 25 | const instance = getWasmInstance(mod); 26 | const fn = getWasmFn("main", instance); 27 | assert(fn, "Function exists"); 28 | t.expect(fn(), "Main function returns correct value").toEqual(55n); 29 | }); 30 | 31 | test("Compiler kitchen sink", async (t) => { 32 | const mod = await compile(kitchenSink); 33 | const instance = getWasmInstance(mod); 34 | t.expect(mod.validate(), "Module is valid"); 35 | const tests = (expectedValues: unknown[]) => 36 | expectedValues.forEach((v, i) => { 37 | const test = getWasmFn(`test${i + 1}`, instance); 38 | assert(test, `Test${i + 1} exists`); 39 | 40 | if (typeof v === "string") { 41 | t.expect( 42 | readString(test(), instance), 43 | `test ${i + 1} returns correct value` 44 | ).toEqual(v); 45 | return; 46 | } 47 | 48 | t.expect(test(), `test ${i + 1} returns correct value`).toEqual(v); 49 | }); 50 | 51 | tests([ 52 | 13, // Static method resolution tests 53 | 1, 54 | 2, 55 | 52, 56 | 52, // Match based type narrowing (and basic gc) 57 | 21, 58 | -1, 59 | 143, // Generic type test 60 | 7.5, // Generic object type test 61 | 12, 62 | 4, 63 | 597, // Modules 64 | 9, // Generic impls 65 | 17, 66 | 82, 67 | 3, 68 | 42, 69 | 2, // IntersectionType tests 70 | 20, // While loop 71 | "Hello, world! This is a test.", 72 | 12, // Array of objects test + advanced match 73 | 173, // Array test 74 | 4, // Structural object re-assignment 75 | "world", 76 | 8, // trait impls 77 | ]); 78 | }); 79 | 80 | test("Compiler can do tco", async (t) => { 81 | const spy = vi.spyOn(rCallUtil, "returnCall"); 82 | await compile(tcoText); 83 | const did = spy.mock.calls.some((call) => call[1].startsWith("fib")); 84 | t.expect(did); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /src/assembler/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./rtt/index.js"; 2 | export * from "./return-call.js"; 3 | -------------------------------------------------------------------------------- /src/assembler/return-call.ts: -------------------------------------------------------------------------------- 1 | import binaryen from "binaryen"; 2 | import { ExpressionRef, TypeRef } from "../lib/binaryen-gc/types.js"; 3 | 4 | export const returnCall = ( 5 | mod: binaryen.Module, 6 | fnId: string, 7 | args: ExpressionRef[], 8 | returnType: TypeRef 9 | ) => { 10 | return mod.return_call(fnId, args, returnType); 11 | }; 12 | -------------------------------------------------------------------------------- /src/assembler/rtt/extension.ts: -------------------------------------------------------------------------------- 1 | import binaryen from "binaryen"; 2 | import { AugmentedBinaryen } from "../../lib/binaryen-gc/types.js"; 3 | import { 4 | defineArrayType, 5 | arrayLen, 6 | arrayGet, 7 | arrayNewFixed, 8 | binaryenTypeToHeapType, 9 | } from "../../lib/binaryen-gc/index.js"; 10 | 11 | const bin = binaryen as unknown as AugmentedBinaryen; 12 | 13 | export const initExtensionHelpers = (mod: binaryen.Module) => { 14 | const i32Array = defineArrayType(mod, bin.i32, true); 15 | 16 | mod.addFunction( 17 | "__extends", 18 | // Extension Obj Id, Ancestor Ids Array 19 | bin.createType([bin.i32, i32Array]), 20 | bin.i32, 21 | [bin.i32], // Current index, Does Extend 22 | mod.block(null, [ 23 | mod.local.set(2, mod.i32.const(0)), // Current ancestor index 24 | mod.loop( 25 | "loop", 26 | mod.block(null, [ 27 | // We've reached the end of the ancestor table without finding a match, return false 28 | mod.if( 29 | mod.i32.eq( 30 | mod.local.get(2, bin.i32), 31 | arrayLen(mod, mod.local.get(1, i32Array)) 32 | ), 33 | mod.return(mod.i32.const(0)) 34 | ), 35 | 36 | // Check if we've found the ancestor 37 | mod.if( 38 | mod.i32.eq( 39 | mod.local.get(0, bin.i32), 40 | arrayGet( 41 | mod, 42 | mod.local.get(1, i32Array), 43 | mod.local.get(2, bin.i32), 44 | bin.i32, 45 | false 46 | ) 47 | ), 48 | 49 | // If we have, set doesExtend to true and break 50 | mod.return(mod.i32.const(1)) 51 | ), 52 | 53 | // Increment ancestor index 54 | mod.local.set( 55 | 2, 56 | mod.i32.add(mod.local.get(2, bin.i32), mod.i32.const(1)) 57 | ), 58 | mod.br("loop"), 59 | ]) 60 | ), 61 | ]) 62 | ); 63 | 64 | const initExtensionArray = (ancestorIds: number[]) => { 65 | return arrayNewFixed( 66 | mod, 67 | binaryenTypeToHeapType(i32Array), 68 | ancestorIds.map((id) => mod.i32.const(id)) 69 | ); 70 | }; 71 | 72 | return { initExtensionArray, i32Array }; 73 | }; 74 | -------------------------------------------------------------------------------- /src/assembler/rtt/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./extension.js"; 2 | export * from "./rtt.js"; 3 | export * from "./field-accessor.js"; 4 | -------------------------------------------------------------------------------- /src/assembler/rtt/rtt.ts: -------------------------------------------------------------------------------- 1 | export const PLACEHOLDER = 0; 2 | -------------------------------------------------------------------------------- /src/cli/cli-dev.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tsx 2 | import { exec } from "./exec.js"; 3 | 4 | /** 5 | * NOTE: 6 | * This file is the same as cli.js, but is run with ts-node. 7 | * I've found tsc -w to be buggy when changing git branches, 8 | * so this lets me bypass the compiler. 9 | */ 10 | 11 | exec(); 12 | -------------------------------------------------------------------------------- /src/cli/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { exec } from "./exec.js"; 3 | 4 | exec(); 5 | -------------------------------------------------------------------------------- /src/cli/exec.ts: -------------------------------------------------------------------------------- 1 | import { stdout } from "process"; 2 | import { getConfig } from "../lib/config/index.js"; 3 | import { run } from "../run.js"; 4 | import { processSemantics } from "../semantics/index.js"; 5 | import binaryen from "binaryen"; 6 | import { testGc } from "../lib/binaryen-gc/test.js"; 7 | import { parseFile, parseModuleFromSrc } from "../parser/index.js"; 8 | import { compileSrc } from "../compiler.js"; 9 | 10 | export const exec = () => main().catch(errorHandler); 11 | 12 | async function main() { 13 | const config = getConfig(); 14 | 15 | if (config.emitParserAst) { 16 | return emit(await getParserAst(config.index)); 17 | } 18 | 19 | if (config.emitCoreAst) { 20 | return emit(await getCoreAst(config.index)); 21 | } 22 | 23 | if (config.emitIrAst) { 24 | return emit(await getIrAST(config.index)); 25 | } 26 | 27 | if (config.emitWasmText) { 28 | return console.log( 29 | await getWasmText(config.index, config.runBinaryenOptimizationPass) 30 | ); 31 | } 32 | 33 | if (config.emitWasm) { 34 | return emitWasm(config.index, config.runBinaryenOptimizationPass); 35 | } 36 | 37 | if (config.run) { 38 | return runWasm(config.index, config.runBinaryenOptimizationPass); 39 | } 40 | 41 | if (config.internalTest) { 42 | return testGc(); 43 | } 44 | 45 | console.log( 46 | "I don't know what to do with the supplied options. Maybe try something else ¯_(ツ)_/¯" 47 | ); 48 | } 49 | 50 | async function getParserAst(index: string) { 51 | return parseFile(index); 52 | } 53 | 54 | async function getCoreAst(index: string) { 55 | return await getParserAst(index); 56 | } 57 | 58 | async function getIrAST(index: string) { 59 | const module = await parseModuleFromSrc(index); 60 | return processSemantics(module); 61 | } 62 | 63 | async function getWasmMod(index: string, optimize = false) { 64 | const mod = await compileSrc(index); 65 | 66 | if (optimize) { 67 | binaryen.setShrinkLevel(3); 68 | binaryen.setOptimizeLevel(3); 69 | mod.optimize(); 70 | } 71 | 72 | return mod; 73 | } 74 | 75 | async function getWasmText(index: string, optimize = false) { 76 | const mod = await getWasmMod(index, optimize); 77 | return mod.emitText(); 78 | } 79 | 80 | async function emitWasm(index: string, optimize = false) { 81 | const mod = await getWasmMod(index, optimize); 82 | 83 | if (!mod.validate()) { 84 | throw new Error("Module is invalid"); 85 | } 86 | 87 | stdout.write(mod.emitBinary()); 88 | } 89 | 90 | async function runWasm(index: string, optimize = false) { 91 | const mod = await getWasmMod(index, optimize); 92 | 93 | if (!mod.validate()) { 94 | throw new Error("Module is invalid"); 95 | } 96 | 97 | run(mod); 98 | } 99 | 100 | function emit(json: any) { 101 | console.log(JSON.stringify(json, undefined, 2)); 102 | } 103 | 104 | function errorHandler(error: Error) { 105 | console.error(error); 106 | process.exit(1); 107 | } 108 | -------------------------------------------------------------------------------- /src/compiler.ts: -------------------------------------------------------------------------------- 1 | import { processSemantics } from "./semantics/index.js"; 2 | import binaryen from "binaryen"; 3 | import { assemble } from "./assembler.js"; 4 | import { 5 | ParsedModule, 6 | parseModuleFromSrc, 7 | parseModule, 8 | } from "./parser/index.js"; 9 | 10 | export const compile = async (text: string) => { 11 | const parsedModule = await parseModule(text); 12 | return compileParsedModule(parsedModule); 13 | }; 14 | 15 | export const compileSrc = async (path: string) => { 16 | const parsedModule = await parseModuleFromSrc(path); 17 | return compileParsedModule(parsedModule); 18 | }; 19 | 20 | export const compileParsedModule = (module: ParsedModule): binaryen.Module => { 21 | const typeCheckedModule = processSemantics(module); 22 | return assemble(typeCheckedModule); 23 | }; 24 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./compiler.js"; 2 | -------------------------------------------------------------------------------- /src/lib/__tests__/murmur-hash.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, describe } from "vitest"; 2 | import { murmurHash3 } from "../murmur-hash.js"; 3 | 4 | describe("Murmur Hash", async () => { 5 | test("should return correct hash for an empty string", () => { 6 | const hash = murmurHash3(""); 7 | expect(hash).toBe(0); 8 | }); 9 | 10 | test("should return correct hash for a short string", () => { 11 | const hash = murmurHash3("abc"); 12 | expect(hash).toBe(3017643002); 13 | }); 14 | 15 | test("should return correct hash for a longer string", () => { 16 | const hash = murmurHash3("The quick brown fox jumps over the lazy dog"); 17 | expect(hash).toBe(776992547); 18 | }); 19 | 20 | test("should return correct hash for string with special characters", () => { 21 | const hash = murmurHash3("!@#$%^&*()"); 22 | expect(hash).toBe(3947575985); 23 | }); 24 | 25 | test("should return correct hash for numeric string", () => { 26 | const hash = murmurHash3("1234567890"); 27 | expect(hash).toBe(839148365); 28 | }); 29 | 30 | test("should return consistent hash for the same input", () => { 31 | const hash1 = murmurHash3("consistent"); 32 | const hash2 = murmurHash3("consistent"); 33 | expect(hash1).toBe(hash2); 34 | }); 35 | 36 | test("should return different hashes for different inputs", () => { 37 | const hash1 = murmurHash3("input1"); 38 | const hash2 = murmurHash3("input2"); 39 | expect(hash1).not.toBe(hash2); 40 | }); 41 | 42 | test("should handle non-ASCII characters", () => { 43 | const hash = murmurHash3("你好,世界"); 44 | expect(hash).toBe(1975738373); 45 | }); 46 | 47 | test("should return correct hash with non-zero seed", () => { 48 | const hash = murmurHash3("seeded", 123); 49 | expect(hash).toBe(1693092115); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/lib/binaryen-gc/README.md: -------------------------------------------------------------------------------- 1 | # Example Usage 2 | 3 | ```ts 4 | import binaryen from "binaryen"; 5 | import { AugmentedBinaryen } from "./lib/binaryen-gc/types.js"; 6 | import { 7 | defineStructType, 8 | initStruct, 9 | structGetFieldValue, 10 | } from "./lib/binaryen-gc/index.js"; 11 | 12 | const bin = binaryen as unknown as AugmentedBinaryen; 13 | 14 | export function main() { 15 | // Simple module with a function that returns a Vec, and a main function that reads the x value 16 | const mod = new binaryen.Module(); 17 | mod.setFeatures(binaryen.Features.All); 18 | // Simple Vec type { x: i32, y: i32, z: i32 }; 19 | const vecType = defineStructType(mod, { 20 | name: "Vec", 21 | fields: [ 22 | { name: "x", type: bin.i32, mutable: true }, 23 | { name: "y", type: bin.i32, mutable: false }, 24 | { name: "z", type: bin.i32, mutable: false }, 25 | ], 26 | }); 27 | const vecTypeRef = bin._BinaryenTypeGetHeapType(vecType); 28 | 29 | const newStruct = initStruct(mod, vecTypeRef, [ 30 | mod.i32.const(1), 31 | mod.i32.const(2), 32 | mod.i32.const(3), 33 | ]); 34 | 35 | mod.addFunction("createStruct", bin.createType([]), vecType, [], newStruct); 36 | 37 | // // // Main function that reads the x value of the Vec 38 | mod.addFunction( 39 | "main", 40 | bin.createType([vecType]), 41 | bin.i32, 42 | [], 43 | structGetFieldValue({ 44 | mod, 45 | fieldIndex: 0, 46 | fieldType: bin.i32, 47 | exprRef: mod.local.get(0, vecType), 48 | }) 49 | ); 50 | 51 | mod.addFunctionExport("main", "main"); 52 | 53 | mod.autoDrop(); 54 | 55 | mod.validate(); 56 | 57 | console.log(mod.emitText()); 58 | } 59 | ``` 60 | -------------------------------------------------------------------------------- /src/lib/binaryen-gc/test.ts: -------------------------------------------------------------------------------- 1 | import binaryen from "binaryen"; 2 | import { AugmentedBinaryen } from "./types.js"; 3 | import { 4 | arrayGet, 5 | arrayLen, 6 | arrayNew, 7 | arrayNewFixed, 8 | arraySet, 9 | binaryenTypeToHeapType, 10 | defineArrayType, 11 | } from "./index.js"; 12 | import { run } from "../../run.js"; 13 | 14 | const bin = binaryen as unknown as AugmentedBinaryen; 15 | 16 | export function testGc() { 17 | const mod = new binaryen.Module(); 18 | mod.setFeatures(binaryen.Features.All); 19 | 20 | const i32Array = defineArrayType(mod, bin.i32, true); 21 | 22 | const initExtensionArray = (ancestorIds: number[]) => { 23 | return arrayNewFixed( 24 | mod, 25 | binaryenTypeToHeapType(i32Array), 26 | ancestorIds.map((id) => mod.i32.const(id)) 27 | ); 28 | }; 29 | 30 | mod.addFunction( 31 | "__extends", 32 | // Extension Obj Id, Ancestor Ids Array 33 | bin.createType([bin.i32, i32Array]), 34 | bin.i32, 35 | [bin.i32, bin.i32], // Current index, Does Extend 36 | mod.block(null, [ 37 | mod.local.set(2, mod.i32.const(0)), // Current ancestor index 38 | mod.local.set(3, mod.i32.const(0)), // Does extend 39 | mod.block("break", [ 40 | mod.loop( 41 | "loop", 42 | mod.block(null, [ 43 | // Break if we've reached the end of the ancestors 44 | mod.br_if( 45 | "break", 46 | mod.i32.eq( 47 | mod.local.get(2, bin.i32), 48 | arrayLen(mod, mod.local.get(1, i32Array)) 49 | ) 50 | ), 51 | 52 | // Check if we've found the ancestor 53 | mod.if( 54 | mod.i32.eq( 55 | mod.local.get(0, bin.i32), 56 | arrayGet( 57 | mod, 58 | mod.local.get(1, i32Array), 59 | mod.local.get(2, bin.i32), 60 | bin.i32, 61 | false 62 | ) 63 | ), 64 | // If we have, set doesExtend to true and break 65 | mod.block(null, [ 66 | mod.local.set(3, mod.i32.const(1)), 67 | mod.br("break"), 68 | ]) 69 | ), 70 | 71 | // Increment ancestor index 72 | mod.local.set( 73 | 2, 74 | mod.i32.add(mod.local.get(2, bin.i32), mod.i32.const(1)) 75 | ), 76 | mod.br("loop"), 77 | ]) 78 | ), 79 | ]), 80 | mod.local.get(3, bin.i32), 81 | ]) 82 | ); 83 | 84 | mod.addGlobal( 85 | "extensionArray", 86 | i32Array, 87 | false, 88 | initExtensionArray([1, 2, 3]) 89 | ); 90 | 91 | mod.addFunction( 92 | "main", 93 | bin.createType([]), 94 | bin.i32, 95 | [i32Array], 96 | mod.block(null, [ 97 | mod.local.set(0, initExtensionArray([1, 2, 3])), 98 | mod.call( 99 | "__extends", 100 | [mod.i32.const(4), mod.global.get("extensionArray", i32Array)], 101 | bin.i32 102 | ), 103 | ]) 104 | ); 105 | 106 | mod.addFunctionExport("main", "main"); 107 | mod.autoDrop(); 108 | mod.validate(); 109 | 110 | // console.log(mod.emitText()); 111 | run(mod); 112 | } 113 | -------------------------------------------------------------------------------- /src/lib/config/README.md: -------------------------------------------------------------------------------- 1 | # Config Lib 2 | 3 | This folder contains a lib for working with voyd configuration. 4 | 5 | The idea is to merge all configuration sources (global and local config files, cli options etc) and 6 | return a single config object. 7 | -------------------------------------------------------------------------------- /src/lib/config/arg-parser.ts: -------------------------------------------------------------------------------- 1 | import { ParseArgsConfig, parseArgs } from "node:util"; 2 | import { VoydConfig } from "./types.js"; 3 | 4 | const options: ParseArgsConfig["options"] = { 5 | "emit-parser-ast": { 6 | type: "boolean", 7 | }, 8 | "emit-core-ast": { 9 | type: "boolean", 10 | }, 11 | "emit-ir-ast": { 12 | type: "boolean", 13 | }, 14 | "emit-wasm": { 15 | type: "boolean", 16 | }, 17 | "emit-wasm-text": { 18 | type: "boolean", 19 | }, 20 | /** Tells binaryen to run its standard optimization pass */ 21 | opt: { 22 | type: "boolean", 23 | }, 24 | run: { 25 | type: "boolean", 26 | short: "r", 27 | }, 28 | help: { 29 | type: "boolean", 30 | short: "h", 31 | }, 32 | version: { 33 | type: "boolean", 34 | short: "v", 35 | }, 36 | "internal-test": { 37 | type: "boolean", 38 | }, 39 | }; 40 | 41 | export const getConfigFromCli = (): VoydConfig => { 42 | const { values, positionals } = parseArgs({ 43 | options, 44 | allowPositionals: true, 45 | }); 46 | 47 | const index = positionals[0] ?? "./src"; 48 | 49 | return { 50 | index, 51 | emitParserAst: values["emit-parser-ast"] as boolean, 52 | emitCoreAst: values["emit-core-ast"] as boolean, 53 | emitIrAst: values["emit-ir-ast"] as boolean, 54 | emitWasm: values["emit-wasm"] as boolean, 55 | emitWasmText: values["emit-wasm-text"] as boolean, 56 | runBinaryenOptimizationPass: values["opt"] as boolean, 57 | showHelp: values["help"] as boolean, 58 | showVersion: values["version"] as boolean, 59 | run: values["run"] as boolean, 60 | internalTest: values["internal-test"] as boolean, 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /src/lib/config/index.ts: -------------------------------------------------------------------------------- 1 | import { getConfigFromCli } from "./arg-parser.js"; 2 | import { VoydConfig } from "./types.js"; 3 | 4 | let config: VoydConfig | undefined = undefined; 5 | export const getConfig = () => { 6 | if (config) return config; 7 | config = getConfigFromCli(); 8 | return config; 9 | }; 10 | -------------------------------------------------------------------------------- /src/lib/config/types.ts: -------------------------------------------------------------------------------- 1 | export type VoydConfig = { 2 | /** Write raw parser AST to stdout */ 3 | emitParserAst?: boolean; 4 | /** Write desurfaced AST to stdout */ 5 | emitCoreAst?: boolean; 6 | /** Emit ast ir expanded (post semantic phases) */ 7 | emitIrAst?: boolean; 8 | /** Write wasm bytecode to stdout */ 9 | emitWasm?: boolean; 10 | /** Write wasm bytecode to stdout (binaryen flavor) */ 11 | emitWasmText?: boolean; 12 | /** Have binaryen run an optimization pass */ 13 | runBinaryenOptimizationPass?: boolean; 14 | /** Emit CLI usage to stdout */ 15 | showHelp?: boolean; 16 | /** Emit voyd version to stdout */ 17 | showVersion?: boolean; 18 | /** Run the compiled wasm code */ 19 | run?: boolean; 20 | /** Specifies the entry voyd file */ 21 | index: string; 22 | /** Run the internal test script */ 23 | internalTest?: boolean; 24 | }; 25 | -------------------------------------------------------------------------------- /src/lib/fast-shift-array.ts: -------------------------------------------------------------------------------- 1 | import { at } from "vitest/dist/chunks/reporters.C_zwCd4j.js"; 2 | 3 | // TODO: Add map, filter, reduce, amd findIndex 4 | export class FastShiftArray { 5 | private items: T[]; 6 | private headIndex: number; 7 | 8 | constructor(...args: T[]) { 9 | this.items = args; 10 | this.headIndex = 0; 11 | } 12 | 13 | shift(): T | undefined { 14 | if (this.headIndex >= this.items.length) { 15 | return undefined; 16 | } 17 | const value = this.items[this.headIndex]; 18 | this.items[this.headIndex] = undefined as any; // Optional: clear the reference for garbage collection 19 | this.headIndex++; 20 | return value; 21 | } 22 | 23 | push(...items: T[]): number { 24 | this.items.push(...items); 25 | return this.length; 26 | } 27 | 28 | pop(): T | undefined { 29 | if (this.length === 0) return undefined; 30 | return this.items.pop(); 31 | } 32 | 33 | unshift(...items: T[]): number { 34 | this.items.splice(this.headIndex, 0, ...items); 35 | return this.length; 36 | } 37 | 38 | at(index: number): T | undefined { 39 | if (index < 0) { 40 | index = this.length + index; 41 | } 42 | return this.items[this.headIndex + index]; 43 | } 44 | 45 | set(index: number, value: T): boolean { 46 | const targetIndex = index < 0 ? this.length + index : index; 47 | if (targetIndex < 0 || targetIndex >= this.length) { 48 | return false; // Index out of bounds 49 | } 50 | this.items[this.headIndex + targetIndex] = value; 51 | return true; 52 | } 53 | 54 | get length(): number { 55 | return this.items.length - this.headIndex; 56 | } 57 | 58 | slice(start?: number, end?: number): T[] { 59 | const actualStart = 60 | start !== undefined 61 | ? this.headIndex + (start < 0 ? this.length + start : start) 62 | : this.headIndex; 63 | const actualEnd = 64 | end !== undefined 65 | ? this.headIndex + (end < 0 ? this.length + end : end) 66 | : this.items.length; 67 | return this.items.slice(actualStart, actualEnd); 68 | } 69 | 70 | splice(start: number, deleteCount: number = 0, ...items: T[]): T[] { 71 | const actualStart = 72 | this.headIndex + (start < 0 ? this.length + start : start); 73 | return this.items.splice(actualStart, deleteCount, ...items); 74 | } 75 | 76 | // Converts the FastShiftArray back to a normal array 77 | toArray(): T[] { 78 | return this.items.slice(this.headIndex); 79 | } 80 | 81 | // Optional: Method to reset the headIndex 82 | resetShift(): void { 83 | this.items.splice(0, this.headIndex); 84 | this.headIndex = 0; 85 | } 86 | 87 | forEach(callbackfn: (value: T, index: number, array: T[]) => void): void { 88 | this.items.slice(this.headIndex).forEach(callbackfn); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/lib/helpers.ts: -------------------------------------------------------------------------------- 1 | export function isCyclic(obj: any, count = 0): boolean { 2 | if (count > 75) return true; 3 | if (!obj) return false; 4 | const parent = obj.parent; 5 | if (parent) return isCyclic(parent, count + 1); 6 | return false; 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * as grammar from "../parser/grammar.js"; 2 | export * from "../parser/utils/parse-file.js"; 3 | -------------------------------------------------------------------------------- /src/lib/murmur-hash.ts: -------------------------------------------------------------------------------- 1 | export const murmurHash3 = (key: string, seed: number = 0): number => { 2 | let h1 = seed; 3 | const remainder = key.length % 4; 4 | const bytes = key.length - remainder; 5 | const c1 = 0xcc9e2d51; 6 | const c2 = 0x1b873593; 7 | 8 | for (let i = 0; i < bytes; i += 4) { 9 | let k1 = 10 | (key.charCodeAt(i) & 0xff) | 11 | ((key.charCodeAt(i + 1) & 0xff) << 8) | 12 | ((key.charCodeAt(i + 2) & 0xff) << 16) | 13 | ((key.charCodeAt(i + 3) & 0xff) << 24); 14 | 15 | k1 = Math.imul(k1, c1); 16 | k1 = (k1 << 15) | (k1 >>> 17); 17 | k1 = Math.imul(k1, c2); 18 | 19 | h1 ^= k1; 20 | h1 = (h1 << 13) | (h1 >>> 19); 21 | h1 = Math.imul(h1, 5) + 0xe6546b64; 22 | } 23 | 24 | let k1 = 0; 25 | 26 | switch (remainder) { 27 | case 3: 28 | k1 ^= (key.charCodeAt(bytes + 2) & 0xff) << 16; 29 | case 2: 30 | k1 ^= (key.charCodeAt(bytes + 1) & 0xff) << 8; 31 | case 1: 32 | k1 ^= key.charCodeAt(bytes) & 0xff; 33 | k1 = Math.imul(k1, c1); 34 | k1 = (k1 << 15) | (k1 >>> 17); 35 | k1 = Math.imul(k1, c2); 36 | h1 ^= k1; 37 | } 38 | 39 | h1 ^= key.length; 40 | 41 | h1 ^= h1 >>> 16; 42 | h1 = Math.imul(h1, 0x85ebca6b); 43 | h1 ^= h1 >>> 13; 44 | h1 = Math.imul(h1, 0xc2b2ae35); 45 | h1 ^= h1 >>> 16; 46 | 47 | return h1 >>> 0; 48 | }; 49 | -------------------------------------------------------------------------------- /src/lib/read-string.ts: -------------------------------------------------------------------------------- 1 | import { getWasmFn } from "./wasm.js"; 2 | 3 | /** Read a string returned by a voyd function call */ 4 | export const readString = (ref: Object, instance: WebAssembly.Instance) => { 5 | const newStringIterator = getWasmFn("new_string_iterator", instance)!; 6 | const readNextChar = getWasmFn("read_next_char", instance)!; 7 | const reader = newStringIterator(ref); 8 | 9 | let str = ""; 10 | while (true) { 11 | const char = readNextChar(reader); 12 | if (char < 0) { 13 | break; 14 | } 15 | str += String.fromCharCode(char); 16 | } 17 | 18 | return str; 19 | }; 20 | -------------------------------------------------------------------------------- /src/lib/resolve-src.ts: -------------------------------------------------------------------------------- 1 | import { stat } from "node:fs/promises"; 2 | import path from "node:path"; 3 | 4 | export type SrcInfo = { 5 | indexPath: string; 6 | srcRootPath?: string; 7 | }; 8 | 9 | /** 10 | * Resolves src code location information. 11 | * 12 | * Assumes either a single file to compile or a directory with an index file. 13 | * I.E. Single File === `src/test.voyd` or Directory === `src`. 14 | * 15 | * Will return only the index file path if a single file is provided. 16 | * Will return the srcRootPath and index file path as srcRootPath + index.voyd if a directory is provided. 17 | */ 18 | export async function resolveSrc(index: string): Promise { 19 | const indexPath = path.resolve(index); 20 | const parsedIndexPath = path.parse(indexPath); 21 | const indexStats = await stat(indexPath); 22 | 23 | if (!indexStats.isDirectory() && parsedIndexPath.ext !== ".voyd") { 24 | throw new Error(`Invalid file extension ${parsedIndexPath.ext}`); 25 | } 26 | 27 | if (indexStats.isDirectory()) { 28 | return { 29 | indexPath: path.join(indexPath, "index.voyd"), 30 | srcRootPath: indexPath, 31 | }; 32 | } 33 | 34 | return { indexPath }; 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/wasm.ts: -------------------------------------------------------------------------------- 1 | import binaryen from "binaryen"; 2 | 3 | export const getWasmInstance = ( 4 | mod: Uint8Array | binaryen.Module 5 | ): WebAssembly.Instance => { 6 | const bin = mod instanceof Uint8Array ? mod : mod.emitBinary(); 7 | const compiled = new WebAssembly.Module(bin); 8 | return new WebAssembly.Instance(compiled); 9 | }; 10 | 11 | export const getWasmFn = ( 12 | name: string, 13 | instance: WebAssembly.Instance 14 | ): Function | undefined => { 15 | const fn = instance.exports[name]; 16 | return typeof fn === "function" ? fn : undefined; 17 | }; 18 | -------------------------------------------------------------------------------- /src/parser/__tests__/__snapshots__/parse-chars.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`should parse the example file into a raw ast 1`] = ` 4 | [ 5 | "ast", 6 | [ 7 | " 8 | ", 9 | "fn", 10 | " ", 11 | "fib", 12 | [ 13 | "n", 14 | ":", 15 | " ", 16 | "i32", 17 | ], 18 | " ", 19 | "->", 20 | " ", 21 | "i32", 22 | " 23 | ", 24 | " ", 25 | "if", 26 | " ", 27 | "n", 28 | " ", 29 | "<=", 30 | " ", 31 | 1, 32 | " ", 33 | "then", 34 | ":", 35 | " 36 | ", 37 | " ", 38 | " ", 39 | "n", 40 | " 41 | ", 42 | " ", 43 | "else", 44 | ":", 45 | " 46 | ", 47 | " ", 48 | " ", 49 | "fib", 50 | [ 51 | "n", 52 | " ", 53 | "-", 54 | " ", 55 | 1, 56 | ], 57 | " ", 58 | "+", 59 | " ", 60 | "fib", 61 | [ 62 | "n", 63 | " ", 64 | "-", 65 | " ", 66 | 2, 67 | ], 68 | " 69 | ", 70 | " 71 | ", 72 | "fn", 73 | " ", 74 | "main", 75 | [], 76 | " 77 | ", 78 | " ", 79 | "let", 80 | " ", 81 | "x", 82 | " ", 83 | "=", 84 | " ", 85 | 10, 86 | " ", 87 | "+", 88 | " 89 | ", 90 | " ", 91 | " ", 92 | 20, 93 | " ", 94 | "+", 95 | " 96 | ", 97 | " ", 98 | " ", 99 | 30, 100 | " 101 | ", 102 | " 103 | ", 104 | " ", 105 | "let", 106 | " ", 107 | "y", 108 | " ", 109 | "=", 110 | " ", 111 | "if", 112 | " ", 113 | "x", 114 | " ", 115 | ">", 116 | " ", 117 | 10, 118 | " 119 | ", 120 | " ", 121 | " ", 122 | "then", 123 | ":", 124 | " 125 | ", 126 | " ", 127 | " ", 128 | " ", 129 | 10, 130 | " 131 | ", 132 | " ", 133 | " ", 134 | "else", 135 | ":", 136 | " 137 | ", 138 | " ", 139 | " ", 140 | " ", 141 | 20, 142 | " 143 | ", 144 | " 145 | ", 146 | " ", 147 | "call", 148 | " ", 149 | "this", 150 | " ", 151 | "while", 152 | " ", 153 | [], 154 | " ", 155 | "=>", 156 | " ", 157 | "if", 158 | " ", 159 | "x", 160 | " ", 161 | ">", 162 | " ", 163 | 10, 164 | " ", 165 | "then", 166 | ":", 167 | " 168 | ", 169 | " ", 170 | " ", 171 | "x", 172 | " ", 173 | "-=", 174 | " ", 175 | 1, 176 | " 177 | ", 178 | " ", 179 | "else", 180 | ":", 181 | " 182 | ", 183 | " ", 184 | " ", 185 | "x", 186 | " ", 187 | "+=", 188 | " ", 189 | 1, 190 | " 191 | ", 192 | " 193 | ", 194 | " ", 195 | "let", 196 | " ", 197 | "n", 198 | " ", 199 | "=", 200 | " ", 201 | "if", 202 | " ", 203 | "args", 204 | ".", 205 | "len", 206 | [], 207 | " ", 208 | ">", 209 | " ", 210 | 1, 211 | " ", 212 | "then", 213 | ":", 214 | " 215 | ", 216 | " ", 217 | " ", 218 | "console", 219 | ".", 220 | "log", 221 | [ 222 | [ 223 | "string", 224 | "Hey there!", 225 | ], 226 | ], 227 | " 228 | ", 229 | " ", 230 | " ", 231 | "args", 232 | ".", 233 | "at", 234 | [ 235 | 1, 236 | ], 237 | ".", 238 | "parseInt", 239 | [], 240 | ".", 241 | "unwrap", 242 | [], 243 | " 244 | ", 245 | " ", 246 | "else", 247 | ":", 248 | " 249 | ", 250 | " ", 251 | " ", 252 | 10, 253 | " 254 | ", 255 | " 256 | ", 257 | " ", 258 | "let", 259 | " ", 260 | "x2", 261 | " ", 262 | "=", 263 | " ", 264 | 10, 265 | " 266 | ", 267 | " ", 268 | "let", 269 | " ", 270 | "z", 271 | " ", 272 | "=", 273 | " ", 274 | "nothing", 275 | [], 276 | " 277 | ", 278 | " ", 279 | "let", 280 | " ", 281 | "test_spacing", 282 | " ", 283 | "=", 284 | " ", 285 | "fib", 286 | " ", 287 | "n", 288 | " 289 | ", 290 | " ", 291 | "let", 292 | " ", 293 | "result", 294 | " ", 295 | "=", 296 | " ", 297 | "fib", 298 | [ 299 | "n", 300 | ], 301 | " 302 | ", 303 | ], 304 | ] 305 | `; 306 | -------------------------------------------------------------------------------- /src/parser/__tests__/fixtures/parse-text-voyd-file.ts: -------------------------------------------------------------------------------- 1 | import { CharStream } from "../../char-stream.js"; 2 | 3 | export const parseFileVoydText = ` 4 | fn fib(n: i32) -> i32 5 | if n <= 1 then: 6 | n 7 | else: 8 | fib(n - 1) + fib(n - 2) 9 | 10 | fn main() 11 | let x = 10 + 12 | 20 + 13 | 30 14 | 15 | let y = if x > 10 16 | then: 17 | 10 18 | else: 19 | 20 20 | 21 | call this while () => if x > 10 then: 22 | x -= 1 23 | else: 24 | x += 1 25 | 26 | let n = if args.len() > 1 then: 27 | console.log("Hey there!") 28 | args.at(1).parseInt().unwrap() 29 | else: 30 | 10 31 | 32 | let x2 = 10 33 | let z = nothing() 34 | let test_spacing = fib n 35 | let result = fib(n) 36 | `; 37 | 38 | export const voydFile = new CharStream(parseFileVoydText, "beep/boop"); 39 | -------------------------------------------------------------------------------- /src/parser/__tests__/fixtures/raw-parser-ast.ts: -------------------------------------------------------------------------------- 1 | export const rawParserAST = [ 2 | "ast", 3 | ",", 4 | "\n", 5 | "fn", 6 | " ", 7 | "fib", 8 | ["n", ":", " ", "i32"], 9 | " ", 10 | "->", 11 | " ", 12 | "i32", 13 | "\n", 14 | " ", 15 | "if", 16 | " ", 17 | "n", 18 | " ", 19 | "<=", 20 | " ", 21 | 1, 22 | " ", 23 | "then", 24 | ":", 25 | "\n", 26 | " ", 27 | " ", 28 | "n", 29 | "\n", 30 | " ", 31 | "else", 32 | ":", 33 | "\n", 34 | " ", 35 | " ", 36 | "fib", 37 | ["n", " ", "-", " ", 1], 38 | " ", 39 | "+", 40 | " ", 41 | "fib", 42 | ["n", " ", "-", " ", 2], 43 | "\n", 44 | "\n", 45 | "fn", 46 | " ", 47 | "main", 48 | [], 49 | "\n", 50 | " ", 51 | "let", 52 | " ", 53 | "x", 54 | " ", 55 | "=", 56 | " ", 57 | 10, 58 | " ", 59 | "+", 60 | "\n", 61 | " ", 62 | " ", 63 | 20, 64 | " ", 65 | "+", 66 | "\n", 67 | " ", 68 | " ", 69 | 30, 70 | "\n", 71 | "\n", 72 | " ", 73 | "let", 74 | " ", 75 | "y", 76 | " ", 77 | "=", 78 | " ", 79 | "if", 80 | " ", 81 | "x", 82 | " ", 83 | ">", 84 | " ", 85 | 10, 86 | "\n", 87 | " ", 88 | " ", 89 | "then", 90 | ":", 91 | "\n", 92 | " ", 93 | " ", 94 | " ", 95 | 10, 96 | "\n", 97 | " ", 98 | " ", 99 | "else", 100 | ":", 101 | "\n", 102 | " ", 103 | " ", 104 | " ", 105 | 20, 106 | "\n", 107 | "\n", 108 | " ", 109 | "call", 110 | " ", 111 | "this", 112 | " ", 113 | "while", 114 | " ", 115 | [], 116 | " ", 117 | "=>", 118 | " ", 119 | "if", 120 | " ", 121 | "x", 122 | " ", 123 | ">", 124 | " ", 125 | 10, 126 | " ", 127 | "then", 128 | ":", 129 | "\n", 130 | " ", 131 | " ", 132 | "x", 133 | " ", 134 | "-=", 135 | " ", 136 | 1, 137 | "\n", 138 | " ", 139 | "else", 140 | ":", 141 | "\n", 142 | " ", 143 | " ", 144 | "x", 145 | " ", 146 | "+=", 147 | " ", 148 | 1, 149 | "\n", 150 | "\n", 151 | " ", 152 | "let", 153 | " ", 154 | "n", 155 | " ", 156 | "=", 157 | " ", 158 | "if", 159 | " ", 160 | "args", 161 | ".", 162 | "len", 163 | [], 164 | " ", 165 | ">", 166 | " ", 167 | 1, 168 | " ", 169 | "then", 170 | ":", 171 | "\n", 172 | " ", 173 | " ", 174 | "console", 175 | ".", 176 | "log", 177 | [["string", "Hey there!"]], 178 | "\n", 179 | " ", 180 | " ", 181 | "args", 182 | ".", 183 | "at", 184 | [1], 185 | ".", 186 | "parseInt", 187 | [], 188 | ".", 189 | "unwrap", 190 | [], 191 | "\n", 192 | " ", 193 | "else", 194 | ":", 195 | "\n", 196 | " ", 197 | " ", 198 | 10, 199 | "\n", 200 | "\n", 201 | " ", 202 | "let", 203 | " ", 204 | "x2", 205 | " ", 206 | "=", 207 | " ", 208 | 10, 209 | "\n", 210 | " ", 211 | "let", 212 | " ", 213 | "z", 214 | " ", 215 | "=", 216 | " ", 217 | "nothing", 218 | [], 219 | "\n", 220 | " ", 221 | "let", 222 | " ", 223 | "test_spacing", 224 | " ", 225 | "=", 226 | " ", 227 | "fib", 228 | " ", 229 | "n", 230 | "\n", 231 | " ", 232 | "let", 233 | " ", 234 | "result", 235 | " ", 236 | "=", 237 | " ", 238 | "fib", 239 | ["n"], 240 | "\n", 241 | ]; 242 | -------------------------------------------------------------------------------- /src/parser/__tests__/fixtures/voyd-file.ts: -------------------------------------------------------------------------------- 1 | export const voydFile = ` 2 | use std::macros::all 3 | use std::io::{ read, write: io_write } 4 | 5 | fn fib(n: i32) -> i32 6 | if n <= 1 then: 7 | n 8 | else: 9 | fib(n - 1) + fib(n - 2) 10 | 11 | macro_let extract_parameters = (definitions) => 12 | \`(parameters).concat definitions.slice(1) 13 | 14 | if x > 10 then: 15 | 10 16 | else: 17 | 20 18 | 19 | array.reduce(0, 1, 2) hey: () => 20 | log val 21 | acc + val 22 | with: () => 0 23 | 24 | 10 25 | + 3 26 | 27 | mul(&self, other: Vec) 28 | mul( 29 | &self, 30 | other: Vec 31 | ) 32 | 33 | let a = array 34 | .reduce(0) (acc, val) => 35 | acc + val 36 | + 10 37 | * 3 38 | 39 | let x = my_func( 40 | add 1 2, 41 | () => 42 | hello() 43 | , 44 | 3 + 4 45 | ) 46 | 47 | let vec = { a: hey there, b: 2 } 48 | 49 | closure_param_test(1, () => a, 3, () => 50 | hey there, 51 | 4, 52 | () => 5, 53 | () => 54 | 6, 55 | () => 56 | 7 57 | , 58 | 8 59 | ) 60 | 61 | let (x, y) = (1, 2) 62 | 63 | Array(1, 2, 3) + 3 64 | 65 | obj Test { 66 | c: i32 67 | } 68 | 69 | match(x) 70 | Some: Some { value: x.value + 1 } 71 | None: None {} 72 | 73 | arr 74 | .pop() 75 | .match(val) 76 | Some>: 77 | val.value 78 | None: 79 | None {} 80 | .match(v) 81 | Some: 82 | v.value 83 | None: 84 | -1 85 | 86 | arr.pop() 87 | .match(val) 88 | Some>: 89 | val.value 90 | None: 91 | None {} 92 | .match(v) 93 | Some: 94 | v.value 95 | None: 96 | -1 97 | 98 | fn test(a: 1) -> i32 99 | 100 | fn main() 101 | let a = ...test.hey + &other.now 102 | let x = 10 + 103 | 20 + 104 | 30 105 | 106 | let y = if x > 10 107 | then: 108 | 10 109 | else: 110 | 20 111 | 112 | let n = 113 | if args.len() > 1 then: 114 | console.log("Hey there!") 115 | args.at(1).parseInt().unwrap() 116 | else: 117 | 10 118 | 119 | let x2 = 10 120 | let z = nothing() 121 | let a = hello.boop(1) 122 | let test_spacing = fib n 123 | let result = fib(n) 124 | let x = &hey 125 | $hey 126 | $@(hey) 127 | $(hey) 128 | $(extract equals_expr 2) 129 | (block $body) 130 | x + 5 131 | x + y + 10 132 | x * y + 10 133 | x() 134 | x 135 | 136 | let vec = { 137 | x: 10, 138 | y: Point { x: 10, y: 20 }, 139 | z: { a: 10, b: 20 } 140 | } 141 | `; 142 | 143 | export const voydFileWithGenerics = ` 144 | use std::all 145 | 146 | type DsArrayi32 = DsArray 147 | 148 | pub fn main() 149 | let arr = new_fixed_array(10) 150 | arr.set(0, 1) 151 | arr.get(0) 152 | `; 153 | -------------------------------------------------------------------------------- /src/parser/__tests__/parse-chars.test.ts: -------------------------------------------------------------------------------- 1 | import { parseChars } from "../parse-chars.js"; 2 | import { voydFile } from "./fixtures/parse-text-voyd-file.js"; 3 | import { test } from "vitest"; 4 | 5 | test("should parse the example file into a raw ast", async ({ expect }) => { 6 | const parserOutput = parseChars(voydFile); 7 | expect(parserOutput).toMatchSnapshot(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/parser/__tests__/parser.test.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "../parser.js"; 2 | import { voydFile, voydFileWithGenerics } from "./fixtures/voyd-file.js"; 3 | import { test } from "vitest"; 4 | 5 | test("parser can parse a file into a syntax expanded ast", async (t) => { 6 | t.expect(parse(voydFile)).toMatchSnapshot(); 7 | }); 8 | 9 | test("parser supports generics", async (t) => { 10 | t.expect(parse(voydFileWithGenerics)).toMatchSnapshot(); 11 | }); 12 | -------------------------------------------------------------------------------- /src/parser/char-stream.ts: -------------------------------------------------------------------------------- 1 | import { SourceLocation } from "../syntax-objects/syntax.js"; 2 | 3 | export class CharStream { 4 | readonly filePath: string; 5 | readonly originalSize: number; 6 | readonly value: string[]; 7 | readonly location = { 8 | index: 0, 9 | line: 1, 10 | column: 0, 11 | }; 12 | 13 | constructor(contents: string, filePath: string) { 14 | this.value = contents.split("").reverse(); 15 | this.originalSize = this.value.length; 16 | this.filePath = filePath; 17 | } 18 | 19 | /** Current index the file is on */ 20 | get position() { 21 | return this.location.index; 22 | } 23 | 24 | get line() { 25 | return this.location.line; 26 | } 27 | 28 | get column() { 29 | return this.location.column; 30 | } 31 | 32 | get hasCharacters() { 33 | return !!this.value.length; 34 | } 35 | 36 | get next() { 37 | return this.value[this.value.length - 1]; 38 | } 39 | 40 | currentSourceLocation() { 41 | return new SourceLocation({ 42 | startIndex: this.position, 43 | endIndex: this.originalSize - this.value.length, 44 | line: this.line, 45 | column: this.column, 46 | filePath: this.filePath, 47 | }); 48 | } 49 | 50 | at(index: number): string | undefined { 51 | return this.value.at(-index - 1); 52 | } 53 | 54 | /** Returns the next character and removes it from the queue */ 55 | consumeChar(): string { 56 | const char = this.value.pop(); 57 | if (!char) { 58 | throw new Error("Out of characters"); 59 | } 60 | 61 | this.location.index += 1; 62 | this.location.column += 1; 63 | if (char === "\n") { 64 | this.location.line += 1; 65 | this.location.column = 0; 66 | } 67 | 68 | return char; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/parser/grammar.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "../syntax-objects/expr.js"; 2 | import { Identifier } from "../syntax-objects/identifier.js"; 3 | 4 | export const idIs = (id: Expr | undefined, value: string) => 5 | id?.isIdentifier() && id.is(value); 6 | 7 | export const isTerminator = (char: string) => 8 | isWhitespace(char) || 9 | isBracket(char) || 10 | isQuote(char) || 11 | isOpChar(char) || 12 | char === ","; 13 | 14 | export const isQuote = newTest(["'", '"', "`"]); 15 | 16 | export const isWhitespace = (char: string) => /\s/.test(char); 17 | 18 | export const isBracket = newTest(["{", "[", "(", ")", "]", "}"]); 19 | 20 | export const isOpChar = newTest([ 21 | "+", 22 | "-", 23 | "*", 24 | "/", 25 | "=", 26 | ":", 27 | "?", 28 | ".", 29 | ";", 30 | "<", 31 | ">", 32 | "$", 33 | "!", 34 | "@", 35 | "%", 36 | "^", 37 | "&", 38 | "~", 39 | "\\", 40 | "#", 41 | ]); 42 | 43 | export const isDigit = (char: string) => /[0-9]/.test(char); 44 | export const isDigitSign = (char: string) => char === "+" || char === "-"; 45 | 46 | /** Key is the operator, value is its precedence */ 47 | export type OpMap = Map; 48 | 49 | export const infixOps: OpMap = new Map([ 50 | ["+", 1], 51 | ["-", 1], 52 | ["*", 2], 53 | ["/", 2], 54 | ["^", 3], 55 | ["%", 2], 56 | ["==", 0], 57 | ["!=", 0], 58 | ["<", 0], 59 | [">", 0], 60 | ["<=", 0], 61 | [">=", 0], 62 | [".", 6], 63 | ["|>", 4], 64 | ["<|", 4], 65 | ["|", 4], 66 | ["&", 4], 67 | ["=", 0], 68 | ["+=", 4], 69 | ["-=", 4], 70 | ["*=", 4], 71 | ["/=", 4], 72 | ["=>", 5], 73 | [":", 0], 74 | ["?:", 0], 75 | ["::", 0], 76 | [";", 4], 77 | ["??", 3], 78 | ["and", 0], 79 | ["or", 0], 80 | ["xor", 0], 81 | ["as", 0], 82 | ["is", 0], 83 | ["is_subtype_of", 0], 84 | ["in", 0], 85 | ["has_trait", 0], 86 | ]); 87 | 88 | export const isInfixOp = (op?: Expr): op is Identifier => 89 | !!op?.isIdentifier() && isInfixOpIdentifier(op); 90 | 91 | export const isInfixOpIdentifier = (op?: Identifier) => 92 | !!op && !op.isQuoted && infixOps.has(op.value); 93 | 94 | export const isOp = (op?: Expr): boolean => isInfixOp(op) || isPrefixOp(op); 95 | 96 | export const prefixOps: OpMap = new Map([ 97 | ["#", 0], 98 | ["&", 7], 99 | ["!", 7], 100 | ["~", 7], 101 | ["%", 7], 102 | ["$", 7], 103 | ["@", 7], 104 | ["$@", 7], 105 | ["...", 5], 106 | ]); 107 | 108 | export const isPrefixOp = (op?: Expr): op is Identifier => 109 | !!op?.isIdentifier() && isPrefixOpIdentifier(op); 110 | 111 | export const isPrefixOpIdentifier = (op?: Identifier) => 112 | !!op && !op.isQuoted && prefixOps.has(op.value); 113 | 114 | export const greedyOps = new Set(["=>", "=", "<|", ";", "|"]); 115 | 116 | export const isGreedyOp = (expr?: Expr): expr is Identifier => { 117 | if (!expr?.isIdentifier()) return false; 118 | return isGreedyOpIdentifier(expr); 119 | }; 120 | 121 | export const isGreedyOpIdentifier = (op?: Identifier) => 122 | !!op && !op.isQuoted && greedyOps.has(op.value); 123 | 124 | export const isContinuationOp = (op?: Expr) => isInfixOp(op); 125 | 126 | function newTest(list: Set | Array) { 127 | const set = new Set(list); 128 | return (val: T) => set.has(val); 129 | } 130 | -------------------------------------------------------------------------------- /src/parser/index.ts: -------------------------------------------------------------------------------- 1 | export { parse } from "./parser.js"; 2 | export * from "./utils/index.js"; 3 | -------------------------------------------------------------------------------- /src/parser/lexer.ts: -------------------------------------------------------------------------------- 1 | import { Token } from "./token.js"; 2 | import { CharStream } from "./char-stream.js"; 3 | import { 4 | isOpChar, 5 | isTerminator, 6 | isWhitespace, 7 | isDigit, 8 | isDigitSign, 9 | } from "./grammar.js"; 10 | 11 | export const lexer = (chars: CharStream): Token => { 12 | const token = new Token({ 13 | location: chars.currentSourceLocation(), 14 | }); 15 | 16 | while (chars.hasCharacters) { 17 | const char = chars.next; 18 | 19 | if (!token.hasChars && char === " ") { 20 | consumeSpaces(chars, token); 21 | break; 22 | } 23 | 24 | if (!token.hasChars && nextIsNumber(chars)) { 25 | consumeNumber(chars, token); 26 | break; 27 | } 28 | 29 | if (!token.hasChars && char === ",") { 30 | token.addChar(chars.consumeChar()); 31 | break; 32 | } 33 | 34 | if (!token.hasChars && isOpChar(char)) { 35 | consumeOperator(chars, token); 36 | break; 37 | } 38 | 39 | if (!token.hasChars && isTerminator(char)) { 40 | token.addChar(chars.consumeChar()); 41 | break; 42 | } 43 | 44 | // Support sharp identifiers (Used by reader macros ignores non-whitespace terminators) 45 | if (token.first === "#" && !isWhitespace(char)) { 46 | token.addChar(chars.consumeChar()); 47 | continue; 48 | } 49 | 50 | if (char === "\t") { 51 | throw new Error( 52 | "Tabs are not supported, use four spaces for indentation" 53 | ); 54 | } 55 | 56 | if (isTerminator(char)) { 57 | break; 58 | } 59 | 60 | token.addChar(chars.consumeChar()); 61 | } 62 | 63 | token.location.endIndex = chars.position; 64 | token.location.endColumn = chars.column; 65 | return token; 66 | }; 67 | 68 | const consumeOperator = (chars: CharStream, token: Token) => { 69 | while (isOpChar(chars.next)) { 70 | if (token.value === ">" && (chars.next === ">" || chars.next === ":")) { 71 | break; // Ugly hack to support generics, means >> is not a valid operator. At least until we write a custom parser for the generics reader macro. 72 | } 73 | 74 | token.addChar(chars.consumeChar()); 75 | } 76 | }; 77 | 78 | const consumeNumber = (chars: CharStream, token: Token) => { 79 | const isValidNumber = (str: string) => 80 | /^[+-]?\d+(?:\.\d+)?([Ee]?[+-]?\d+|(?:i|f)(?:|3|6|32|64))?$/.test(str); 81 | const stillConsumingNumber = () => 82 | chars.next && 83 | (isValidNumber(token.value + chars.next) || 84 | isValidNumber(token.value + chars.next + chars.at(1))); 85 | 86 | while (stillConsumingNumber()) { 87 | token.addChar(chars.consumeChar()); 88 | } 89 | }; 90 | 91 | const nextIsNumber = (chars: CharStream) => 92 | isDigit(chars.next) || 93 | (isDigitSign(chars.next) && isDigit(chars.at(1) ?? "")); 94 | 95 | const consumeSpaces = (chars: CharStream, token: Token) => { 96 | while (chars.next === " " && token.length < 2) { 97 | token.addChar(chars.consumeChar()); 98 | } 99 | }; 100 | -------------------------------------------------------------------------------- /src/parser/parse-chars.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "../syntax-objects/expr.js"; 2 | import { Identifier } from "../syntax-objects/identifier.js"; 3 | import { List } from "../syntax-objects/list.js"; 4 | import { Whitespace } from "../syntax-objects/whitespace.js"; 5 | import { CharStream } from "./char-stream.js"; 6 | import { lexer } from "./lexer.js"; 7 | import { getReaderMacroForToken } from "./reader-macros/index.js"; 8 | import { Token } from "./token.js"; 9 | 10 | export type ParseCharsOpts = { 11 | nested?: boolean; 12 | terminator?: string; 13 | parent?: Expr; 14 | }; 15 | 16 | export const parseChars = ( 17 | file: CharStream, 18 | opts: ParseCharsOpts = {} 19 | ): List => { 20 | const list = new List({ 21 | location: file.currentSourceLocation(), 22 | parent: opts.parent, 23 | }); 24 | 25 | while (file.hasCharacters) { 26 | const token = lexer(file); 27 | 28 | if (processWithReaderMacro(token, list.last(), file, list)) { 29 | continue; 30 | } 31 | 32 | if (token.is("(")) { 33 | const subList = parseChars(file, { nested: true }); 34 | subList.setAttribute("tuple?", true); 35 | list.push(subList); 36 | continue; 37 | } 38 | 39 | if (token.is(")") || token.is(opts.terminator)) { 40 | if (opts.nested) break; 41 | continue; 42 | } 43 | 44 | if (token.isWhitespace) { 45 | list.push( 46 | new Whitespace({ 47 | value: token.value, 48 | location: token.location, 49 | }) 50 | ); 51 | continue; 52 | } 53 | 54 | list.push( 55 | new Identifier({ 56 | value: token.value, 57 | location: token.location, 58 | }) 59 | ); 60 | } 61 | 62 | list.location!.endIndex = file.position; 63 | return opts.nested ? list : new List(["ast", list]); 64 | }; 65 | 66 | /** Returns true if token was matched with and processed by a macro */ 67 | const processWithReaderMacro = ( 68 | token: Token, 69 | prev: Expr | undefined, 70 | file: CharStream, 71 | list: List 72 | ) => { 73 | const readerMacro = getReaderMacroForToken(token, prev, file.next); 74 | if (!readerMacro) return undefined; 75 | 76 | const result = readerMacro(file, { 77 | token, 78 | reader: (file, terminator) => 79 | parseChars(file, { 80 | nested: true, 81 | terminator, 82 | }), 83 | }); 84 | 85 | if (!result) return undefined; 86 | 87 | list.push(result); 88 | return true; 89 | }; 90 | -------------------------------------------------------------------------------- /src/parser/parser.ts: -------------------------------------------------------------------------------- 1 | import { List } from "../syntax-objects/index.js"; 2 | import { CharStream } from "./char-stream.js"; 3 | import { parseChars } from "./parse-chars.js"; 4 | import { expandSyntaxMacros } from "./syntax-macros/index.js"; 5 | 6 | export const parse = (text: string, filePath?: string): List => { 7 | const chars = new CharStream(text, filePath ?? "raw"); 8 | const rawAst = parseChars(chars); 9 | return expandSyntaxMacros(rawAst); 10 | }; 11 | -------------------------------------------------------------------------------- /src/parser/reader-macros/array-literal.ts: -------------------------------------------------------------------------------- 1 | import { ReaderMacro } from "./types.js"; 2 | 3 | export const arrayLiteralMacro: ReaderMacro = { 4 | match: (t) => t.value === "[", 5 | macro: (file, { reader }) => { 6 | const items = reader(file, "]"); 7 | return items.insert("array").insert(",", 1); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/parser/reader-macros/boolean.ts: -------------------------------------------------------------------------------- 1 | import { Bool } from "../../syntax-objects/index.js"; 2 | import { ReaderMacro } from "./types.js"; 3 | 4 | export const booleanMacro: ReaderMacro = { 5 | match: (t) => /^true|false$/.test(t.value), 6 | macro: (_, { token }) => 7 | new Bool({ 8 | value: token.is("true"), 9 | location: token.location, 10 | }), 11 | }; 12 | -------------------------------------------------------------------------------- /src/parser/reader-macros/comment.ts: -------------------------------------------------------------------------------- 1 | import { nop } from "../../syntax-objects/index.js"; 2 | import { ReaderMacro } from "./types.js"; 3 | 4 | export const comment: ReaderMacro = { 5 | match: (t) => /^\/\/[^\s]*$/.test(t.value), 6 | macro: (file) => { 7 | while (file.hasCharacters) { 8 | if (file.next === "\n") break; 9 | file.consumeChar(); 10 | } 11 | 12 | return nop(); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/parser/reader-macros/float.ts: -------------------------------------------------------------------------------- 1 | import { Float } from "../../syntax-objects/index.js"; 2 | import { ReaderMacro } from "./types.js"; 3 | 4 | export const floatMacro: ReaderMacro = { 5 | match: (t) => /^[+-]?\d+\.\d+(?:f64|f32)?$/.test(t.value), 6 | macro: (_, { token }) => { 7 | const value = 8 | token.value.at(-3) === "f" 9 | ? token.value.endsWith("f64") 10 | ? ({ type: "f64", value: Number(token.value.slice(0, -3)) } as const) 11 | : Number(token.value.slice(0, -3)) 12 | : ({ type: "f64", value: Number(token.value) } as const); // Default to f64 13 | 14 | return new Float({ value, location: token.location }); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/parser/reader-macros/generics.ts: -------------------------------------------------------------------------------- 1 | import { ReaderMacro } from "./types.js"; 2 | 3 | export const genericsMacro: ReaderMacro = { 4 | match: (t, prev) => { 5 | return t.value === "<" && !!prev?.isIdentifier(); 6 | }, 7 | macro: (file, { reader }) => { 8 | const items = reader(file, ">"); 9 | return items.insert("generics").insert(",", 1); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/parser/reader-macros/html/html.ts: -------------------------------------------------------------------------------- 1 | import { List } from "../../../syntax-objects/list.js"; 2 | import { lexer } from "../../lexer.js"; 3 | import { ReaderMacro } from "../types.js"; 4 | import { HTMLParser } from "./html-parser.js"; 5 | 6 | export const htmlMacro: ReaderMacro = { 7 | match: (t, prev, nextChar) => { 8 | return ( 9 | t.value === "<" && 10 | !!prev?.isWhitespace() && 11 | !!nextChar && 12 | /\w/.test(nextChar) 13 | ); 14 | }, 15 | macro: (file, { token, reader }) => { 16 | const parser = new HTMLParser(file, { 17 | onUnescapedCurlyBrace: () => { 18 | file.consumeChar(); 19 | return reader(file, "}"); 20 | }, 21 | }); 22 | const start = lexer(file); 23 | const html = parser.parse(start.value); 24 | return new List({ value: ["html", html], location: token.location }); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/parser/reader-macros/index.ts: -------------------------------------------------------------------------------- 1 | import { Token } from "../token.js"; 2 | import { arrayLiteralMacro } from "./array-literal.js"; 3 | import { booleanMacro } from "./boolean.js"; 4 | import { comment } from "./comment.js"; 5 | import { mapLiteralMacro } from "./map-literal.js"; 6 | import { floatMacro } from "./float.js"; 7 | import { intMacro } from "./int.js"; 8 | import { scientificENotationMacro } from "./scientific-e-notation.js"; 9 | import { stringMacro } from "./string.js"; 10 | import { objectLiteralMacro } from "./object-literal.js"; 11 | import { ReaderMacro } from "./types.js"; 12 | import { genericsMacro } from "./generics.js"; 13 | import { Expr } from "../../syntax-objects/expr.js"; 14 | import { htmlMacro } from "./html/html.js"; 15 | 16 | const macros = [ 17 | objectLiteralMacro, 18 | arrayLiteralMacro, 19 | mapLiteralMacro, 20 | intMacro, 21 | floatMacro, 22 | scientificENotationMacro, 23 | stringMacro, 24 | comment, 25 | booleanMacro, 26 | genericsMacro, 27 | htmlMacro, 28 | ]; 29 | 30 | export const getReaderMacroForToken = ( 31 | token: Token, 32 | prev?: Expr, 33 | /** Next char */ 34 | next?: string 35 | ): ReaderMacro["macro"] | undefined => 36 | macros.find((m) => m.match(token, prev, next))?.macro; 37 | -------------------------------------------------------------------------------- /src/parser/reader-macros/int.ts: -------------------------------------------------------------------------------- 1 | import { Int } from "../../syntax-objects/index.js"; 2 | import { ReaderMacro } from "./types.js"; 3 | 4 | export const intMacro: ReaderMacro = { 5 | match: (t) => /^[+-]?\d+(?:i64|i32)?$/.test(t.value), 6 | macro: (_, { token }) => { 7 | const value = 8 | token.value.at(-3) === "i" 9 | ? token.value.endsWith("i64") 10 | ? ({ 11 | type: "i64", 12 | value: BigInt(token.value.slice(0, -3)), 13 | } as const) 14 | : Number(token.value.slice(0, -3)) 15 | : Number(token.value); // Default to i32 16 | 17 | return new Int({ value, location: token.location }); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/parser/reader-macros/map-literal.ts: -------------------------------------------------------------------------------- 1 | import { ReaderMacro } from "./types.js"; 2 | 3 | export const mapLiteralMacro: ReaderMacro = { 4 | match: (t) => t.value === "#{", 5 | macro: (file, { reader }) => { 6 | const items = reader(file, "}"); 7 | return items.insert("map").insert(",", 1); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/parser/reader-macros/object-literal.ts: -------------------------------------------------------------------------------- 1 | import { ReaderMacro } from "./types.js"; 2 | 3 | export const objectLiteralMacro: ReaderMacro = { 4 | match: (t) => t.value === "{", 5 | macro: (dream, { reader }) => { 6 | const items = reader(dream, "}"); 7 | return items.insert("object").insert(",", 1); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/parser/reader-macros/scientific-e-notation.ts: -------------------------------------------------------------------------------- 1 | import { Identifier, List, StringLiteral } from "../../syntax-objects/index.js"; 2 | import { ReaderMacro } from "./types.js"; 3 | 4 | export const scientificENotationMacro: ReaderMacro = { 5 | /** Regex from Michael Dumas https://regexlib.com/REDetails.aspx?regexp_id=2457 */ 6 | match: (t) => /^[+-]?\d(\.\d+)?[Ee][+-]?\d+$/.test(t.value), 7 | macro: (_, { token }) => 8 | new List({ location: token.location }) 9 | .push(new Identifier({ value: "scientific-e-notion" })) 10 | .push( 11 | new StringLiteral({ 12 | value: token.value, 13 | location: token.location, 14 | }) 15 | ), 16 | }; 17 | -------------------------------------------------------------------------------- /src/parser/reader-macros/string.ts: -------------------------------------------------------------------------------- 1 | import { Identifier, StringLiteral } from "../../syntax-objects/index.js"; 2 | import { ReaderMacro } from "./types.js"; 3 | 4 | export const stringMacro: ReaderMacro = { 5 | match: (t) => /^[\"\']$/.test(t.value), 6 | macro: (file, { token }) => { 7 | const startChar = token.value; 8 | token.value = ""; 9 | while (file.hasCharacters) { 10 | const next = file.consumeChar(); 11 | 12 | if (next === "\\") { 13 | token.addChar(next); 14 | token.addChar(file.consumeChar()); 15 | continue; 16 | } 17 | 18 | if (next === startChar) { 19 | break; 20 | } 21 | 22 | token.addChar(next); 23 | } 24 | token.location.endIndex = file.position; 25 | 26 | if (startChar === "'") { 27 | return new Identifier({ 28 | value: token.value, 29 | location: token.location, 30 | isQuoted: true, 31 | }); 32 | } 33 | 34 | return new StringLiteral({ 35 | value: token.value, 36 | location: token.location, 37 | }); 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /src/parser/reader-macros/types.ts: -------------------------------------------------------------------------------- 1 | import { CharStream } from "../char-stream.js"; 2 | import { Expr, List } from "../../syntax-objects/index.js"; 3 | import { Token } from "../token.js"; 4 | 5 | export interface ReaderMacro { 6 | match: (token: Token, prev?: Expr, nextChar?: string) => boolean; 7 | macro: ( 8 | file: CharStream, 9 | opts: { 10 | token: Token; 11 | reader: (file: CharStream, terminator?: string) => List; 12 | } 13 | ) => Expr; 14 | } 15 | -------------------------------------------------------------------------------- /src/parser/syntax-macros/functional-notation.ts: -------------------------------------------------------------------------------- 1 | import { idIs, isOp } from "../grammar.js"; 2 | import { Expr, List, ListValue } from "../../syntax-objects/index.js"; 3 | 4 | // Note: The current version of this function was modified by GPT o1. 5 | // I wrote the original version an have made modifications to the version 6 | // produced by o1. The intent was to have o1 improve the performance. But 7 | // now I'm not sure the added complexity produced by o1 was worth the cost. 8 | // Might still be worth re-writing again to something similar to the original. 9 | 10 | export const functionalNotation = (list: List): List => { 11 | const array = list.toArray(); 12 | let isTuple = false; 13 | 14 | const { result } = array.reduce( 15 | (acc, expr, index) => { 16 | if (acc.skip > 0) { 17 | acc.skip--; 18 | return acc; 19 | } 20 | 21 | if (expr.isList()) { 22 | acc.result.push(functionalNotation(expr)); 23 | return acc; 24 | } 25 | 26 | if (expr.isWhitespace()) { 27 | acc.result.push(expr); 28 | return acc; 29 | } 30 | 31 | const nextExpr = array[index + 1]; 32 | 33 | if (nextExpr && nextExpr.isList() && !(isOp(expr) || idIs(expr, ","))) { 34 | return handleNextExpression(acc, expr, nextExpr, array, index); 35 | } 36 | 37 | if (list.getAttribute("tuple?") && idIs(expr, ",")) { 38 | isTuple = true; 39 | } 40 | 41 | acc.result.push(expr); 42 | return acc; 43 | }, 44 | { result: [], skip: 0 } as Accumulator 45 | ); 46 | 47 | return finalizeResult(result, isTuple, list); 48 | }; 49 | 50 | type Accumulator = { result: ListValue[]; skip: number }; 51 | 52 | const handleNextExpression = ( 53 | acc: Accumulator, 54 | expr: Expr, 55 | nextExpr: List, 56 | array: Expr[], 57 | index: number 58 | ) => { 59 | if (nextExpr.calls("generics")) { 60 | const generics = nextExpr; 61 | const nextNextExpr = array[index + 2]; 62 | if (nextNextExpr && nextNextExpr.isList()) { 63 | acc.result.push(processGenerics(expr, generics, nextNextExpr as List)); 64 | acc.skip = 2; // Skip next two expressions 65 | } else { 66 | acc.result.push(processGenerics(expr, generics)); 67 | acc.skip = 1; // Skip next expression 68 | } 69 | } else { 70 | acc.result.push(processParamList(expr, nextExpr as List)); 71 | acc.skip = 1; // Skip next expression 72 | } 73 | return acc; 74 | }; 75 | 76 | const finalizeResult = ( 77 | result: ListValue[], 78 | isTuple: boolean, 79 | originalList: List 80 | ): List => { 81 | if (isTuple) { 82 | result.unshift(","); 83 | result.unshift("tuple"); 84 | } 85 | return new List({ ...originalList.metadata, value: result }); 86 | }; 87 | 88 | const processGenerics = (expr: Expr, generics: List, params?: List): List => { 89 | generics.setAttribute("tuple?", false); 90 | 91 | const list = params || new List([]); 92 | list.insert(expr); 93 | list.insert(",", 1); 94 | list.setAttribute("tuple?", false); 95 | const functional = functionalNotation(list); 96 | 97 | functional.insert(functionalNotation(generics), 2); 98 | functional.insert(",", 3); 99 | return functional; 100 | }; 101 | 102 | const processParamList = (expr: Expr, params: List): List => { 103 | params.insert(expr); 104 | params.insert(",", 1); 105 | params.setAttribute("tuple?", false); 106 | return functionalNotation(params); 107 | }; 108 | -------------------------------------------------------------------------------- /src/parser/syntax-macros/index.ts: -------------------------------------------------------------------------------- 1 | import { List } from "../../syntax-objects/list.js"; 2 | import { functionalNotation } from "./functional-notation.js"; 3 | import { interpretWhitespace } from "./interpret-whitespace.js"; 4 | import { primary } from "./primary.js"; 5 | import { SyntaxMacro } from "./types.js"; 6 | 7 | /** Caution: Order matters */ 8 | const syntaxMacros: SyntaxMacro[] = [ 9 | functionalNotation, 10 | interpretWhitespace, 11 | primary, 12 | ]; 13 | 14 | export const expandSyntaxMacros = (expr: List): List => 15 | syntaxMacros.reduce((ast, macro) => macro(ast), expr); 16 | -------------------------------------------------------------------------------- /src/parser/syntax-macros/primary.ts: -------------------------------------------------------------------------------- 1 | import { infixOps, isInfixOp, isPrefixOp, prefixOps } from "../grammar.js"; 2 | import { Expr, List } from "../../syntax-objects/index.js"; 3 | 4 | /** 5 | * Primary surface language syntax macro. Post whitespace interpretation. 6 | * In charge of operator parsing and precedence. Operator-precedence parser 7 | */ 8 | export const primary = (list: List): List => parseList(list); 9 | 10 | const parseExpression = (expr: Expr): Expr => { 11 | if (!expr.isList()) return expr; 12 | return parseList(expr); 13 | }; 14 | 15 | const parseList = (list: List): List => { 16 | const transformed = new List({ ...list.metadata }); 17 | const hadSingleListChild = list.length === 1 && list.at(0)?.isList(); 18 | while (list.hasChildren) { 19 | transformed.push(parsePrecedence(list)); 20 | } 21 | 22 | const result = 23 | !hadSingleListChild && transformed.at(0)?.isList() 24 | ? transformed.listAt(0).push(...transformed.argsArray()) 25 | : transformed; 26 | 27 | // Handle expressions to the right of an operator { a: hello there, b: 2 } -> [object [: a [hello there] b [2]] 28 | if ( 29 | result.at(0)?.isIdentifier() && 30 | isInfixOp(result.identifierAt(0)) && 31 | result.length > 3 32 | ) { 33 | return result.slice(0, 2).push(parseList(result.slice(2))); 34 | } 35 | 36 | return result; 37 | }; 38 | 39 | const parseBinaryCall = (left: Expr, list: List): List => { 40 | const op = list.consume(); 41 | 42 | const right = parsePrecedence(list, (infixOpInfo(op) ?? -1) + 1); 43 | 44 | // Dot handling should maybe be moved to a macro? 45 | const result = isDotOp(op) 46 | ? parseDot(right, left) 47 | : new List({ ...op.metadata, value: [op, left, right] }); 48 | 49 | // Remove "tuple" from the list of parameters of a lambda 50 | // Functional notation macro isn't smart enough to identify lambda parameters 51 | // and so it converts those parameters to a tuple. We remove it here for now. 52 | if (isLambdaWithTupleArgs(result)) { 53 | return removeTupleFromLambdaParameters(result); 54 | } 55 | 56 | return result; 57 | }; 58 | 59 | const isDotOp = (op?: Expr): boolean => { 60 | return !!op?.isIdentifier() && op.is("."); 61 | }; 62 | 63 | const parseDot = (right: Expr, left: Expr): List => { 64 | if ( 65 | right.isList() && 66 | right.at(1)?.isList() && 67 | right.listAt(1).calls("generics") 68 | ) { 69 | right.insert(left, 2); 70 | return right; 71 | } 72 | 73 | if (right.isList()) { 74 | right.insert(left, 1); 75 | return right; 76 | } 77 | 78 | return new List({ value: [right, left] }); 79 | }; 80 | 81 | const parsePrecedence = (list: List, minPrecedence = 0): Expr => { 82 | const next = list.at(0); 83 | let expr = isPrefixOp(next) 84 | ? parseUnaryCall(list) 85 | : parseExpression(list.consume()); 86 | 87 | while ((infixOpInfo(list.first()) ?? -1) >= minPrecedence) { 88 | expr = parseBinaryCall(expr, list); 89 | } 90 | 91 | return expr; 92 | }; 93 | 94 | const parseUnaryCall = (list: List): List => { 95 | const op = list.consume(); 96 | const expr = parsePrecedence(list, unaryOpInfo(op) ?? -1); 97 | return new List({ value: [op, expr] }); 98 | }; 99 | 100 | const infixOpInfo = (op?: Expr): number | undefined => { 101 | if (!op?.isIdentifier() || op.isQuoted) return undefined; 102 | return infixOps.get(op.value); 103 | }; 104 | 105 | const unaryOpInfo = (op?: Expr): number | undefined => { 106 | if (!op?.isIdentifier()) return undefined; 107 | return prefixOps.get(op.value); 108 | }; 109 | 110 | const isLambdaWithTupleArgs = (list: List) => 111 | list.calls("=>") && list.at(1)?.isList() && list.listAt(1).calls("tuple"); 112 | 113 | const removeTupleFromLambdaParameters = (list: List) => { 114 | const parameters = list.listAt(1); 115 | parameters.remove(0); 116 | return list; 117 | }; 118 | -------------------------------------------------------------------------------- /src/parser/syntax-macros/types.ts: -------------------------------------------------------------------------------- 1 | import { List } from "../../syntax-objects/index.js"; 2 | 3 | /** Takes the whole ast, returns a transformed version of the whole ast */ 4 | export type SyntaxMacro = (list: List) => List; 5 | -------------------------------------------------------------------------------- /src/parser/token.ts: -------------------------------------------------------------------------------- 1 | import { SourceLocation } from "../syntax-objects/syntax.js"; 2 | 3 | export class Token { 4 | readonly location: SourceLocation; 5 | value = ""; 6 | 7 | constructor(opts: { location: SourceLocation; value?: string }) { 8 | const { value, location } = opts; 9 | this.value = value ?? ""; 10 | this.location = location; 11 | } 12 | 13 | get length() { 14 | return this.value.length; 15 | } 16 | 17 | get hasChars() { 18 | return !!this.value.length; 19 | } 20 | 21 | get isNumber() { 22 | return /^[0-9]+$/.test(this.value); 23 | } 24 | 25 | get first(): string | undefined { 26 | return this.value[0]; 27 | } 28 | 29 | get isWhitespace() { 30 | return /^\s+$/.test(this.value); 31 | } 32 | 33 | addChar(string: string) { 34 | this.value += string; 35 | } 36 | 37 | is(string?: string) { 38 | return this.value === string; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/parser/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./parse-directory.js"; 2 | export * from "./parse-file.js"; 3 | export * from "./parse-std.js"; 4 | export * from "./parse-module.js"; 5 | -------------------------------------------------------------------------------- /src/parser/utils/parse-directory.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "node:path"; 2 | import { glob } from "glob"; 3 | import { List } from "../../syntax-objects/index.js"; 4 | import { parseFile } from "./parse-file.js"; 5 | 6 | export type ParsedFiles = { [filePath: string]: List }; 7 | 8 | export const parseDirectory = async (path: string): Promise => { 9 | const files = await glob(resolve(path, "**/*.voyd")); 10 | 11 | const parsed = await Promise.all( 12 | files.map(async (filePath) => ({ 13 | filePath, 14 | ast: await parseFile(filePath), 15 | })) 16 | ); 17 | 18 | return parsed.reduce((acc, { filePath, ast }) => { 19 | acc[filePath] = ast; 20 | return acc; 21 | }, {} as ParsedFiles); 22 | }; 23 | -------------------------------------------------------------------------------- /src/parser/utils/parse-file.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import { parse } from "../parser.js"; 3 | import { List } from "../../syntax-objects/list.js"; 4 | 5 | export const parseFile = async (path: string): Promise => { 6 | const file = fs.readFileSync(path, { encoding: "utf8" }); 7 | return parse(file, path); 8 | }; 9 | -------------------------------------------------------------------------------- /src/parser/utils/parse-module.ts: -------------------------------------------------------------------------------- 1 | import { resolveSrc } from "../../lib/resolve-src.js"; 2 | import { parse } from "../parser.js"; 3 | import { ParsedFiles, parseDirectory } from "./parse-directory.js"; 4 | import { parseFile } from "./parse-file.js"; 5 | import { parseStd } from "./parse-std.js"; 6 | 7 | export type ParsedModule = { 8 | files: ParsedFiles; 9 | /** Path to src directory (a folder containing index.voyd that acts as entry) if available */ 10 | srcPath?: string; 11 | /** Path to root voyd file */ 12 | indexPath: string; 13 | }; 14 | 15 | /** Parses voyd text and std lib into a module unit */ 16 | export const parseModule = async (text: string): Promise => { 17 | return { 18 | files: { 19 | index: parse(text), 20 | ...(await parseStd()), 21 | }, 22 | indexPath: "index", 23 | }; 24 | }; 25 | 26 | /** Parses a voyd codebase source and std into a module unit */ 27 | export const parseModuleFromSrc = async ( 28 | path: string 29 | ): Promise => { 30 | const src = await resolveSrc(path); 31 | 32 | const srcFiles = src.srcRootPath 33 | ? await parseDirectory(src.srcRootPath) 34 | : { [src.indexPath]: await parseFile(src.indexPath) }; 35 | 36 | return { 37 | files: { 38 | ...srcFiles, 39 | ...(await parseStd()), 40 | }, 41 | srcPath: src.srcRootPath, 42 | indexPath: src.indexPath, 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /src/parser/utils/parse-std.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { ParsedFiles, parseDirectory } from "./parse-directory.js"; 3 | import { fileURLToPath } from "url"; 4 | 5 | export const stdPath = path.resolve( 6 | fileURLToPath(import.meta.url), 7 | "..", 8 | "..", 9 | "..", 10 | "..", 11 | "std" 12 | ); 13 | 14 | let cache: ParsedFiles | undefined = undefined; 15 | export const parseStd = async () => { 16 | if (cache) { 17 | return cloneParsedFiles(cache); 18 | } 19 | 20 | const parsed = await parseDirectory(stdPath); 21 | cache = cloneParsedFiles(parsed); 22 | return parsed; 23 | }; 24 | 25 | const cloneParsedFiles = (parsed: ParsedFiles) => 26 | Object.entries(parsed).reduce( 27 | (acc, [key, value]) => ({ ...acc, [key]: value.clone() }), 28 | {} as ParsedFiles 29 | ); 30 | 31 | // Convert the object 32 | -------------------------------------------------------------------------------- /src/run.ts: -------------------------------------------------------------------------------- 1 | import binaryen from "binaryen"; 2 | import { readString } from "./lib/read-string.js"; 3 | 4 | export function run(mod: binaryen.Module) { 5 | const binary = mod.emitBinary(); 6 | const compiled = new WebAssembly.Module(binary); 7 | const instance = new WebAssembly.Instance(compiled, { 8 | utils: { 9 | log: (val: number) => console.log(val), 10 | }, 11 | }); 12 | 13 | const fns = instance.exports as any; 14 | const result = fns.main(); 15 | 16 | if (typeof result === "object") { 17 | console.log(readString(result, instance)); 18 | return; 19 | } 20 | 21 | console.log(result); 22 | } 23 | -------------------------------------------------------------------------------- /src/semantics/README.md: -------------------------------------------------------------------------------- 1 | # Semantic Processing 2 | 3 | - Resolves Modules 4 | - Expands Regular (Function) Macros 5 | - Resolves Entities (Types, Functions, Variables, etc) 6 | - Checks Types 7 | 8 | ## Notes 9 | 10 | - Use statements are (currently) initialized by macro resolution, not init entities 11 | -------------------------------------------------------------------------------- /src/semantics/__tests__/fixtures/check-types.ts: -------------------------------------------------------------------------------- 1 | export const throwsWithMissingField = ` 2 | use std::all 3 | 4 | obj Point extends Vec { 5 | x: i32, 6 | y: i32, 7 | z: i32 8 | } 9 | 10 | pub fn main() -> i32 11 | let vec = Point { x: 1, y: 2 } 12 | vec.x 13 | `; 14 | 15 | export const throwsWithBadReturn = ` 16 | use std::all 17 | 18 | pub fn main() -> i32 19 | let fl = 1.23 20 | fl 21 | `; 22 | -------------------------------------------------------------------------------- /src/semantics/__tests__/fixtures/regular-macros-voyd-file.ts: -------------------------------------------------------------------------------- 1 | export const regularMacrosVoydFile = ` 2 | macro \`() 3 | quote quote $@body 4 | 5 | macro let() 6 | define equals_expr body.extract(0) 7 | \` define $(equals_expr.extract(1)) $(equals_expr.extract(2)) 8 | 9 | // Extracts typed parameters from a list where index 0 is fn name, and offset_index+ are labeled_expr 10 | macro_let extract_parameters = (definitions) => 11 | \`(parameters).concat definitions.slice(1) 12 | 13 | macro fn() 14 | let definitions = body.extract(0) 15 | let identifier = definitions.extract(0) 16 | let params = extract_parameters(definitions) 17 | 18 | let type_arrow_index = 19 | if body.extract(1) == "->" then: 20 | 1 21 | else: 22 | if body.extract(2) == "->" then: 2 else: -1 23 | 24 | let return_type = 25 | if type_arrow_index > -1 then: 26 | body.slice(type_arrow_index + 1, type_arrow_index + 2) 27 | else: \`() 28 | 29 | let expressions = 30 | if type_arrow_index > -1 then: 31 | body.slice(type_arrow_index + 2) 32 | else: body.slice(1) 33 | 34 | \`(define_function, 35 | $identifier, 36 | $params, 37 | (return_type $@return_type), 38 | $(\`(block).concat(expressions))) 39 | 40 | fn fib(n: i32) -> i32 41 | let base = 1 42 | if n <= base then: 43 | n 44 | else: 45 | fib(n - 1) + fib(n - 2) 46 | `; 47 | -------------------------------------------------------------------------------- /src/semantics/__tests__/regular-macros.test.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "../../parser/parser.js"; 2 | import path from "node:path"; 3 | import { registerModules } from "../modules.js"; 4 | import { expandRegularMacros } from "../regular-macros.js"; 5 | import { regularMacrosVoydFile } from "./fixtures/regular-macros-voyd-file.js"; 6 | import { test } from "vitest"; 7 | import { List } from "../../syntax-objects/list.js"; 8 | 9 | test("regular macro expansion", async (t) => { 10 | const parserOutput = parse(regularMacrosVoydFile); 11 | const files = { 12 | std: new List([]), 13 | test: parserOutput, 14 | }; 15 | const resolvedModules = registerModules({ 16 | files, 17 | srcPath: path.dirname("test"), 18 | indexPath: "test.voyd", 19 | }); 20 | const result = expandRegularMacros(resolvedModules); 21 | t.expect(result).toMatchSnapshot(); 22 | }); 23 | -------------------------------------------------------------------------------- /src/semantics/index.ts: -------------------------------------------------------------------------------- 1 | import { checkTypes } from "./check-types.js"; 2 | import { initPrimitiveTypes } from "./init-primitive-types.js"; 3 | import { initEntities } from "./init-entities.js"; 4 | import { SemanticProcessor } from "./types.js"; 5 | import { registerModules } from "./modules.js"; 6 | import { expandRegularMacros } from "./regular-macros.js"; 7 | import { ParsedModule } from "../parser/index.js"; 8 | import { Expr } from "../syntax-objects/expr.js"; 9 | import { resolveEntities } from "./resolution/resolve-entities.js"; 10 | 11 | const semanticPhases: SemanticProcessor[] = [ 12 | expandRegularMacros, // Also handles use and module declaration initialization 13 | initPrimitiveTypes, 14 | initEntities, 15 | resolveEntities, 16 | checkTypes, 17 | ]; 18 | 19 | export const processSemantics = (parsedModule: ParsedModule): Expr => { 20 | const expr = registerModules(parsedModule); 21 | return semanticPhases.reduce((e, checker) => checker(e), expr as Expr); 22 | }; 23 | -------------------------------------------------------------------------------- /src/semantics/init-primitive-types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | i32, 3 | f32, 4 | i64, 5 | f64, 6 | bool, 7 | dVoid, 8 | dVoyd, 9 | voydString, 10 | } from "../syntax-objects/types.js"; 11 | import { voydBaseObject } from "../syntax-objects/types.js"; 12 | import { SemanticProcessor } from "./types.js"; 13 | 14 | export const initPrimitiveTypes: SemanticProcessor = (expr) => { 15 | if (!expr.isModule()) return expr; 16 | expr.registerExport(i32); 17 | expr.registerExport(f32); 18 | expr.registerExport(i64); 19 | expr.registerExport(f64); 20 | expr.registerExport(bool); 21 | expr.registerExport(dVoid); 22 | expr.registerExport(dVoyd); 23 | expr.registerExport(voydString); 24 | expr.registerExport(voydBaseObject); 25 | return expr; 26 | }; 27 | -------------------------------------------------------------------------------- /src/semantics/modules.ts: -------------------------------------------------------------------------------- 1 | import { ParsedModule, stdPath } from "../parser/index.js"; 2 | import { List } from "../syntax-objects/list.js"; 3 | import { RootModule, VoydModule } from "../syntax-objects/module.js"; 4 | 5 | /** Registers submodules of a parsed module for future import resolution */ 6 | export const registerModules = (opts: ParsedModule): VoydModule => { 7 | const { srcPath, files } = opts; 8 | 9 | const rootModule = new RootModule({}); 10 | 11 | for (const [filePath, file] of Object.entries(files)) { 12 | const resolvedPath = filePathToModulePath( 13 | filePath, 14 | srcPath ?? opts.indexPath 15 | ); 16 | 17 | const parsedPath = resolvedPath.split("/").filter(Boolean); 18 | 19 | registerModule({ 20 | path: parsedPath, 21 | parentModule: rootModule, 22 | ast: file.slice(1), // Skip the first element (ast) 23 | isIndex: filePath === opts.indexPath, 24 | }); 25 | } 26 | 27 | return rootModule; 28 | }; 29 | 30 | const registerModule = ({ 31 | path, 32 | parentModule, 33 | ast, 34 | isIndex, 35 | }: { 36 | path: string[]; 37 | parentModule: VoydModule; 38 | ast: List; 39 | isIndex?: boolean; 40 | }): VoydModule | undefined => { 41 | const [name, ...rest] = path; 42 | 43 | if (!name) return; 44 | 45 | const existingModule = parentModule.resolveEntity(name); 46 | 47 | if (existingModule && !existingModule.isModule()) { 48 | throw new Error( 49 | `Cannot register module ${name} because it is already registered as ${existingModule.syntaxType}` 50 | ); 51 | } 52 | 53 | if (!existingModule && (name === "index" || name === "mod")) { 54 | parentModule.value = ast.toArray(); 55 | registerDefaultImports(parentModule); 56 | parentModule.getAllEntities().forEach((e) => { 57 | if (e.isModule()) parentModule.push(e); 58 | }); 59 | return; 60 | } 61 | 62 | const module = 63 | existingModule ?? 64 | new VoydModule({ 65 | ...(!rest.length ? { ...ast.metadata, value: ast.toArray() } : {}), 66 | name, 67 | isIndex, 68 | }); 69 | 70 | if (!existingModule) { 71 | parentModule.push(module); 72 | registerDefaultImports(module); 73 | } 74 | 75 | if (!existingModule && (module.name.is("src") || module.name.is("std"))) { 76 | parentModule.registerExport(module); 77 | } 78 | 79 | if (existingModule && !rest.length) { 80 | module.unshift(...ast.toArray().reverse()); 81 | } 82 | 83 | if (!rest.length) return; 84 | 85 | return registerModule({ path: rest, parentModule: module, ast }); 86 | }; 87 | 88 | const filePathToModulePath = (filePath: string, srcPath: string) => { 89 | let finalPath = filePath.startsWith(stdPath) 90 | ? filePath.replace(stdPath, "std") 91 | : filePath; 92 | 93 | finalPath = finalPath.startsWith(srcPath) 94 | ? finalPath.replace(srcPath, "src") 95 | : finalPath; 96 | 97 | finalPath = finalPath.replace(".voyd", ""); 98 | 99 | return finalPath; 100 | }; 101 | 102 | const registerDefaultImports = (module: VoydModule) => { 103 | module.unshift(new List(["use", ["::", "root", "all"]])); 104 | const mod = module.resolveModule("std"); 105 | if (mod) return; 106 | module.unshift(new List(["use", ["::", "std", "all"]])); 107 | }; 108 | -------------------------------------------------------------------------------- /src/semantics/resolution/combine-types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IntersectionType, 3 | ObjectType, 4 | Type, 5 | UnionType, 6 | } from "../../syntax-objects/types.js"; 7 | 8 | /** 9 | * Combines types into their least common denominator. 10 | * If all types are the same, it returns that type. 11 | * If all types are different (but still object types), it returns a UnionType. 12 | * If types are mixed and not all object types, it returns undefined. 13 | */ 14 | export const combineTypes = (types: Type[]): Type | undefined => { 15 | const firstType = types[0]; 16 | if (!types.length || !firstType?.isObjectType()) { 17 | return firstType; 18 | } 19 | 20 | let isLocalUnion = false; 21 | let topType: ObjectType | IntersectionType | UnionType = firstType; 22 | for (const type of types.slice(1)) { 23 | if (type.id === topType.id) { 24 | continue; 25 | } 26 | 27 | if (isObjectOrIntersection(type) && isObjectOrIntersection(topType)) { 28 | const union = new UnionType({ 29 | name: `CombinedTypeUnion`, 30 | }); 31 | union.types = [topType, type]; 32 | topType = union; 33 | isLocalUnion = true; 34 | continue; 35 | } 36 | 37 | if (isObjectOrIntersection(type) && topType.isUnionType() && isLocalUnion) { 38 | topType.types.push(type); 39 | continue; 40 | } 41 | 42 | // TODO: Fix (V-129) 43 | if (type.isUnionType() && isObjectOrIntersection(topType)) { 44 | const obj = topType; 45 | topType = type; 46 | if (isLocalUnion) type.types.push(obj); 47 | continue; 48 | } 49 | 50 | if (type.isUnionType() && topType.isUnionType() && isLocalUnion) { 51 | topType.types.push(...type.types); 52 | continue; 53 | } 54 | 55 | return undefined; 56 | } 57 | 58 | return topType; 59 | }; 60 | 61 | const isObjectOrIntersection = ( 62 | type: Type 63 | ): type is ObjectType | IntersectionType => { 64 | return type.isObjectType() || type.isIntersectionType(); 65 | }; 66 | -------------------------------------------------------------------------------- /src/semantics/resolution/get-call-fn.ts: -------------------------------------------------------------------------------- 1 | import { Call, Expr, Fn } from "../../syntax-objects/index.js"; 2 | import { getExprType } from "./get-expr-type.js"; 3 | import { typesAreCompatible } from "./types-are-compatible.js"; 4 | import { resolveFn } from "./resolve-fn.js"; 5 | import { resolveTypeExpr } from "./resolve-type-expr.js"; 6 | 7 | export const getCallFn = (call: Call): Fn | undefined => { 8 | if (isPrimitiveFnCall(call)) return undefined; 9 | 10 | const unfilteredCandidates = getCandidates(call); 11 | const candidates = filterCandidates(call, unfilteredCandidates); 12 | 13 | if (!candidates.length) { 14 | return undefined; 15 | } 16 | 17 | if (candidates.length === 1) return candidates[0]; 18 | 19 | throw new Error( 20 | `Ambiguous call ${JSON.stringify(call, null, 2)} at ${call.location}` 21 | ); 22 | }; 23 | 24 | const getCandidates = (call: Call): Fn[] => { 25 | const fns = call.resolveFns(call.fnName); 26 | 27 | // Check for methods of arg 1 28 | const arg1Type = getExprType(call.argAt(0)); 29 | if (arg1Type?.isObjectType()) { 30 | const isInsideImpl = call.parentImpl?.targetType?.id === arg1Type.id; 31 | const implFns = isInsideImpl 32 | ? [] // internal methods already in scope 33 | : arg1Type.implementations 34 | ?.flatMap((impl) => impl.exports) 35 | .filter((fn) => fn.name.is(call.fnName.value)); 36 | fns.push(...(implFns ?? [])); 37 | } 38 | 39 | return fns; 40 | }; 41 | 42 | const filterCandidates = (call: Call, candidates: Fn[]): Fn[] => 43 | candidates.flatMap((candidate) => { 44 | if (candidate.typeParameters) { 45 | return filterCandidateWithGenerics(call, candidate); 46 | } 47 | 48 | resolveFn(candidate); 49 | return parametersMatch(candidate, call) && typeArgsMatch(call, candidate) 50 | ? candidate 51 | : []; 52 | }); 53 | 54 | const filterCandidateWithGenerics = (call: Call, candidate: Fn): Fn[] => { 55 | // Resolve generics 56 | if (!candidate.genericInstances) resolveFn(candidate, call); 57 | 58 | // Fn not compatible with call 59 | if (!candidate.genericInstances?.length) return []; 60 | 61 | // First attempt 62 | const genericsInstances = filterCandidates(call, candidate.genericInstances); 63 | 64 | // If we have instances, return them 65 | if (genericsInstances.length) return genericsInstances; 66 | 67 | // If no instances, attempt to resolve generics with this call, as a compatible instance 68 | // is still possible 69 | const beforeLen = candidate.genericInstances.length; 70 | resolveFn(candidate, call); 71 | const afterLen = candidate.genericInstances.length; 72 | 73 | if (beforeLen === afterLen) { 74 | // No new instances were created, so this call is not compatible 75 | return []; 76 | } 77 | 78 | return filterCandidates(call, candidate.genericInstances); 79 | }; 80 | 81 | const typeArgsMatch = (call: Call, candidate: Fn): boolean => 82 | call.typeArgs && candidate.appliedTypeArgs 83 | ? candidate.appliedTypeArgs.every((t, i) => { 84 | const arg = call.typeArgs?.at(i); 85 | if (arg) resolveTypeExpr(arg); 86 | const argType = getExprType(arg); 87 | const appliedType = getExprType(t); 88 | return typesAreCompatible(argType, appliedType, { 89 | exactNominalMatch: true, 90 | }); 91 | }) 92 | : true; 93 | 94 | const parametersMatch = (candidate: Fn, call: Call) => 95 | candidate.parameters.every((p, i) => { 96 | const arg = call.argAt(i); 97 | if (!arg) return false; 98 | const argType = getExprType(arg); 99 | if (!argType) return false; 100 | const argLabel = getExprLabel(arg); 101 | const labelsMatch = p.label === argLabel; 102 | return typesAreCompatible(argType, p.type!) && labelsMatch; 103 | }); 104 | 105 | const getExprLabel = (expr?: Expr): string | undefined => { 106 | if (!expr?.isCall()) return; 107 | if (!expr.calls(":")) return; 108 | const id = expr.argAt(0); 109 | if (!id?.isIdentifier()) return; 110 | return id.value; 111 | }; 112 | 113 | const isPrimitiveFnCall = (call: Call): boolean => { 114 | const name = call.fnName.value; 115 | return ( 116 | name === "export" || 117 | name === "if" || 118 | name === "return" || 119 | name === "binaryen" || 120 | name === ":" || 121 | name === "=" || 122 | name === "while" || 123 | name === "for" || 124 | name === "break" || 125 | name === "mod" || 126 | name === "continue" || 127 | name === "::" 128 | ); 129 | }; 130 | -------------------------------------------------------------------------------- /src/semantics/resolution/get-expr-type.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "../../syntax-objects/expr.js"; 2 | import { Call, Identifier } from "../../syntax-objects/index.js"; 3 | import { 4 | Type, 5 | i32, 6 | f32, 7 | bool, 8 | i64, 9 | f64, 10 | voydString, 11 | } from "../../syntax-objects/types.js"; 12 | import { resolveCall } from "./resolve-call.js"; 13 | 14 | export const getExprType = (expr?: Expr): Type | undefined => { 15 | if (!expr) return; 16 | if (expr.isInt()) return typeof expr.value === "number" ? i32 : i64; 17 | if (expr.isFloat()) return typeof expr.value === "number" ? f32 : f64; 18 | if (expr.isStringLiteral()) return voydString; 19 | if (expr.isBool()) return bool; 20 | if (expr.isIdentifier()) return getIdentifierType(expr); 21 | if (expr.isCall()) return resolveCall(expr)?.type; 22 | if (expr.isFn()) return expr.getType(); 23 | if (expr.isTypeAlias()) return expr.type; 24 | if (expr.isType()) return expr; 25 | if (expr.isBlock()) return expr.type; 26 | if (expr.isObjectLiteral()) return expr.type; 27 | if (expr.isMatch()) return expr.type; 28 | if (expr.isUnionType()) return expr; 29 | }; 30 | 31 | export const getIdentifierType = (id: Identifier): Type | undefined => { 32 | const entity = id.resolve(); 33 | if (!entity) return; 34 | if (entity.isVariable()) return entity.type; 35 | if (entity.isGlobal()) return entity.type; 36 | if (entity.isParameter()) return entity.type; 37 | if (entity.isFn()) return entity.getType(); 38 | if (entity.isTypeAlias()) return entity.type; 39 | if (entity.isType()) return entity; 40 | }; 41 | -------------------------------------------------------------------------------- /src/semantics/resolution/index.ts: -------------------------------------------------------------------------------- 1 | export { resolveEntities } from "./resolve-entities.js"; 2 | export { typesAreCompatible } from "./types-are-compatible.js"; 3 | export { resolveModulePath } from "./resolve-use.js"; 4 | -------------------------------------------------------------------------------- /src/semantics/resolution/resolve-entities.ts: -------------------------------------------------------------------------------- 1 | import { Block } from "../../syntax-objects/block.js"; 2 | import { Expr } from "../../syntax-objects/expr.js"; 3 | import { nop } from "../../syntax-objects/lib/helpers.js"; 4 | import { List } from "../../syntax-objects/list.js"; 5 | import { VoydModule } from "../../syntax-objects/module.js"; 6 | import { ObjectLiteral } from "../../syntax-objects/object-literal.js"; 7 | import { 8 | ObjectType, 9 | TypeAlias, 10 | voydBaseObject, 11 | } from "../../syntax-objects/types.js"; 12 | import { Variable } from "../../syntax-objects/variable.js"; 13 | import { getExprType } from "./get-expr-type.js"; 14 | import { resolveCall } from "./resolve-call.js"; 15 | import { resolveFn } from "./resolve-fn.js"; 16 | import { resolveImpl } from "./resolve-impl.js"; 17 | import { resolveMatch } from "./resolve-match.js"; 18 | import { resolveObjectType } from "./resolve-object-type.js"; 19 | import { resolveTrait } from "./resolve-trait.js"; 20 | import { resolveTypeExpr } from "./resolve-type-expr.js"; 21 | import { resolveUse } from "./resolve-use.js"; 22 | 23 | /** 24 | * NOTE: Some mapping is preformed on the AST at this stage. 25 | * Returned tree not guaranteed to be same as supplied tree. 26 | */ 27 | export const resolveEntities = (expr: Expr | undefined): Expr => { 28 | if (!expr) return nop(); 29 | if (expr.isBlock()) return resolveBlock(expr); 30 | if (expr.isCall()) return resolveCall(expr); 31 | if (expr.isFn()) return resolveFn(expr); 32 | if (expr.isVariable()) return resolveVar(expr); 33 | if (expr.isModule()) return resolveModule(expr); 34 | if (expr.isList()) return resolveListTypes(expr); 35 | if (expr.isUse()) return resolveUse(expr, resolveModule); 36 | if (expr.isObjectType()) return resolveObjectType(expr); 37 | if (expr.isTypeAlias()) return resolveTypeAlias(expr); 38 | if (expr.isObjectLiteral()) return resolveObjectLiteral(expr); 39 | if (expr.isMatch()) return resolveMatch(expr); 40 | if (expr.isImpl()) return resolveImpl(expr); 41 | if (expr.isTrait()) return resolveTrait(expr); 42 | return expr; 43 | }; 44 | 45 | const resolveBlock = (block: Block): Block => { 46 | block.applyMap(resolveEntities); 47 | block.type = getExprType(block.body.at(-1)); 48 | return block; 49 | }; 50 | 51 | export const resolveVar = (variable: Variable): Variable => { 52 | const initializer = resolveEntities(variable.initializer); 53 | variable.initializer = initializer; 54 | variable.inferredType = getExprType(initializer); 55 | 56 | if (variable.typeExpr) { 57 | variable.typeExpr = resolveTypeExpr(variable.typeExpr); 58 | variable.annotatedType = getExprType(variable.typeExpr); 59 | } 60 | 61 | variable.type = variable.annotatedType ?? variable.inferredType; 62 | return variable; 63 | }; 64 | 65 | export const resolveModule = (mod: VoydModule): VoydModule => { 66 | if (mod.phase >= 3) return mod; 67 | mod.phase = 3; 68 | mod.each(resolveEntities); 69 | mod.phase = 4; 70 | return mod; 71 | }; 72 | 73 | const resolveListTypes = (list: List) => { 74 | console.log("Unexpected list"); 75 | console.log(JSON.stringify(list, undefined, 2)); 76 | return list.map(resolveEntities); 77 | }; 78 | 79 | const resolveTypeAlias = (alias: TypeAlias): TypeAlias => { 80 | if (alias.type) return alias; 81 | alias.typeExpr = resolveTypeExpr(alias.typeExpr); 82 | alias.type = getExprType(alias.typeExpr); 83 | return alias; 84 | }; 85 | 86 | const resolveObjectLiteral = (obj: ObjectLiteral) => { 87 | obj.fields.forEach((field) => { 88 | field.initializer = resolveEntities(field.initializer); 89 | field.type = getExprType(field.initializer); 90 | return field; 91 | }); 92 | 93 | if (!obj.type) { 94 | obj.type = new ObjectType({ 95 | ...obj.metadata, 96 | name: `ObjectLiteral-${obj.syntaxId}`, 97 | value: obj.fields.map((f) => ({ 98 | name: f.name, 99 | typeExpr: f.initializer, 100 | type: f.type, 101 | })), 102 | parentObj: voydBaseObject, 103 | isStructural: true, 104 | }); 105 | } 106 | 107 | return obj; 108 | }; 109 | -------------------------------------------------------------------------------- /src/semantics/resolution/resolve-fn.ts: -------------------------------------------------------------------------------- 1 | import { Call } from "../../syntax-objects/call.js"; 2 | import { Expr } from "../../syntax-objects/expr.js"; 3 | import { Fn } from "../../syntax-objects/fn.js"; 4 | import { Implementation } from "../../syntax-objects/implementation.js"; 5 | import { List } from "../../syntax-objects/list.js"; 6 | import { Parameter } from "../../syntax-objects/parameter.js"; 7 | import { TypeAlias } from "../../syntax-objects/types.js"; 8 | import { getExprType } from "./get-expr-type.js"; 9 | import { resolveEntities } from "./resolve-entities.js"; 10 | import { resolveTypeExpr } from "./resolve-type-expr.js"; 11 | 12 | export type ResolveFnTypesOpts = { 13 | typeArgs?: List; 14 | args?: List; 15 | }; 16 | 17 | /** Pass call to potentially resolve generics */ 18 | export const resolveFn = (fn: Fn, call?: Call): Fn => { 19 | if (fn.typesResolved) { 20 | // Already resolved 21 | return fn; 22 | } 23 | 24 | if (fn.typeParameters && call) { 25 | // May want to check if there is already a resolved instance with matching type args here 26 | // currently get-call-fn.ts does this, but it may be better to do it here 27 | return attemptToResolveFnWithGenerics(fn, call); 28 | } 29 | 30 | if (fn.typeParameters && !call) { 31 | return fn; 32 | } 33 | 34 | resolveFnSignature(fn); 35 | 36 | fn.typesResolved = true; 37 | fn.body = fn.body ? resolveEntities(fn.body) : undefined; 38 | fn.inferredReturnType = getExprType(fn.body); 39 | fn.returnType = fn.annotatedReturnType ?? fn.inferredReturnType; 40 | fn.parentImpl?.registerMethod(fn); // Maybe do this for module when not in an impl 41 | 42 | return fn; 43 | }; 44 | 45 | export const resolveFnSignature = (fn: Fn) => { 46 | resolveParameters(fn.parameters); 47 | if (fn.returnTypeExpr) { 48 | fn.returnTypeExpr = resolveTypeExpr(fn.returnTypeExpr); 49 | fn.annotatedReturnType = getExprType(fn.returnTypeExpr); 50 | fn.returnType = fn.annotatedReturnType; 51 | } 52 | 53 | return fn; 54 | }; 55 | 56 | const resolveParameters = (params: Parameter[]) => { 57 | params.forEach((p) => { 58 | if (p.type) return; 59 | 60 | if (p.name.is("self")) { 61 | const impl = getParentImpl(p); 62 | if (impl) p.type = impl.targetType; 63 | return; 64 | } 65 | 66 | if (!p.typeExpr) { 67 | throw new Error(`Unable to determine type for ${p}`); 68 | } 69 | 70 | p.typeExpr = resolveTypeExpr(p.typeExpr); 71 | p.type = getExprType(p.typeExpr); 72 | }); 73 | }; 74 | 75 | const attemptToResolveFnWithGenerics = (fn: Fn, call: Call): Fn => { 76 | if (call.typeArgs) { 77 | return resolveGenericsWithTypeArgs(fn, call.typeArgs); 78 | } 79 | 80 | // TODO try type inference with args 81 | return fn; 82 | }; 83 | 84 | const resolveGenericsWithTypeArgs = (fn: Fn, args: List): Fn => { 85 | const typeParameters = fn.typeParameters!; 86 | 87 | if (args.length !== typeParameters.length) { 88 | return fn; 89 | } 90 | 91 | const newFn = fn.clone(); 92 | newFn.typeParameters = undefined; 93 | newFn.appliedTypeArgs = []; 94 | 95 | /** Register resolved type entities for each type param */ 96 | typeParameters.forEach((typeParam, index) => { 97 | const typeArg = args.exprAt(index); 98 | const identifier = typeParam.clone(); 99 | const type = new TypeAlias({ 100 | name: identifier, 101 | typeExpr: typeArg.clone(), 102 | }); 103 | type.parent = newFn; 104 | resolveTypeExpr(typeArg); 105 | type.type = getExprType(typeArg); 106 | newFn.appliedTypeArgs?.push(type); 107 | newFn.registerEntity(type); 108 | }); 109 | 110 | if (!newFn.appliedTypeArgs.every((t) => (t as TypeAlias).type)) { 111 | throw new Error(`Unable to resolve all type args for ${newFn}`); 112 | } 113 | 114 | const resolvedFn = resolveFn(newFn); 115 | fn.registerGenericInstance(resolvedFn); 116 | return fn; 117 | }; 118 | 119 | const getParentImpl = (expr: Expr): Implementation | undefined => { 120 | if (expr.syntaxType === "implementation") return expr; 121 | if (expr.parent) return getParentImpl(expr.parent); 122 | return undefined; 123 | }; 124 | -------------------------------------------------------------------------------- /src/semantics/resolution/resolve-impl.ts: -------------------------------------------------------------------------------- 1 | import { nop } from "../../syntax-objects/lib/helpers.js"; 2 | import { Implementation } from "../../syntax-objects/implementation.js"; 3 | import { ObjectType, TypeAlias } from "../../syntax-objects/types.js"; 4 | import { getExprType } from "./get-expr-type.js"; 5 | import { resolveObjectType } from "./resolve-object-type.js"; 6 | import { resolveEntities } from "./resolve-entities.js"; 7 | import { resolveTypeExpr } from "./resolve-type-expr.js"; 8 | import { Trait } from "../../syntax-objects/trait.js"; 9 | import { typesAreCompatible } from "./types-are-compatible.js"; 10 | import { resolveFn } from "./resolve-fn.js"; 11 | 12 | export const resolveImpl = ( 13 | impl: Implementation, 14 | targetType?: ObjectType 15 | ): Implementation => { 16 | if (impl.typesResolved) return impl; 17 | targetType = targetType ?? getTargetType(impl); 18 | impl.targetType = targetType; 19 | impl.trait = getTrait(impl); 20 | 21 | if (!targetType) return impl; 22 | 23 | if (targetType.appliedTypeArgs) { 24 | targetType.appliedTypeArgs.forEach((arg, index) => { 25 | const typeParam = impl.typeParams.at(index); 26 | if (!typeParam) { 27 | throw new Error(`Type param not found for ${arg} at ${impl.location}`); 28 | } 29 | const type = new TypeAlias({ 30 | name: typeParam.clone(), 31 | typeExpr: nop(), 32 | }); 33 | resolveTypeExpr(arg); 34 | type.type = getExprType(arg); 35 | impl.registerEntity(type); 36 | }); 37 | } 38 | 39 | if (targetType?.isObjectType()) { 40 | targetType.implementations?.push(impl); 41 | } 42 | 43 | if (targetType?.isObjectType() && targetType.typeParameters?.length) { 44 | // Apply impl to existing generic instances 45 | targetType.genericInstances?.forEach((obj) => 46 | resolveImpl(impl.clone(), obj) 47 | ); 48 | 49 | return impl; 50 | } 51 | 52 | impl.typesResolved = true; 53 | impl.body.value = resolveEntities(impl.body.value); 54 | resolveDefaultTraitMethods(impl); 55 | 56 | return impl; 57 | }; 58 | 59 | const getTargetType = (impl: Implementation): ObjectType | undefined => { 60 | const expr = impl.targetTypeExpr.value; 61 | const type = expr.isIdentifier() 62 | ? expr.resolve() 63 | : expr.isCall() 64 | ? expr.fnName.resolve() 65 | : undefined; 66 | 67 | if (!type || !type.isObjectType()) return; 68 | 69 | if (type.typeParameters?.length && expr.isCall()) { 70 | const obj = resolveObjectType(type, expr); 71 | // Object fully resolved to non-generic version i.e. `Vec` 72 | if (!obj.typeParameters?.length) return obj; 73 | } 74 | 75 | // Generic impl with generic target type i.e. `impl for Vec` 76 | if (!implIsCompatible(impl, type)) return undefined; 77 | 78 | return type; 79 | }; 80 | 81 | const getTrait = (impl: Implementation): Trait | undefined => { 82 | const expr = impl.traitExpr.value; 83 | const type = expr?.isIdentifier() ? expr.resolve() : undefined; 84 | if (!type || !type.isTrait()) return; 85 | return type; 86 | }; 87 | 88 | export const implIsCompatible = ( 89 | impl: Implementation, 90 | obj: ObjectType 91 | ): boolean => { 92 | if (!impl.typeParams.length && !obj.typeParameters?.length) return true; 93 | 94 | // For now, only handles generic impls with no constraints that match the type arg length of the target type. 95 | if (impl.typeParams.length === obj.typeParameters?.length) return true; // impl for Vec 96 | if (impl.typeParams.length === obj.appliedTypeArgs?.length) return true; // impl for Vec 97 | 98 | return false; 99 | }; 100 | 101 | const resolveDefaultTraitMethods = (impl: Implementation): void => { 102 | if (!impl.trait) return; 103 | impl.trait.methods 104 | .toArray() 105 | .filter((m) => !!m.body) 106 | .forEach((m) => { 107 | const existing = impl.resolveFns(m.name.value); 108 | const clone = resolveFn(m.clone(impl)); 109 | 110 | if ( 111 | !existing.length || 112 | !existing.some((fn) => 113 | typesAreCompatible(fn.getType(), clone.getType()) 114 | ) 115 | ) { 116 | impl.registerMethod(clone); 117 | impl.registerExport(clone); 118 | return; 119 | } 120 | }); 121 | 122 | // All methods of a trait implementation are exported 123 | impl.methods.forEach((m) => impl.registerExport(m)); 124 | }; 125 | -------------------------------------------------------------------------------- /src/semantics/resolution/resolve-intersection.ts: -------------------------------------------------------------------------------- 1 | import { IntersectionType } from "../../syntax-objects/types.js"; 2 | import { getExprType } from "./get-expr-type.js"; 3 | import { resolveEntities } from "./resolve-entities.js"; 4 | import { resolveTypeExpr } from "./resolve-type-expr.js"; 5 | 6 | export const resolveIntersectionType = ( 7 | inter: IntersectionType 8 | ): IntersectionType => { 9 | inter.nominalTypeExpr.value = resolveTypeExpr(inter.nominalTypeExpr.value); 10 | inter.structuralTypeExpr.value = resolveTypeExpr( 11 | inter.structuralTypeExpr.value 12 | ); 13 | 14 | const nominalType = getExprType(inter.nominalTypeExpr.value); 15 | const structuralType = getExprType(inter.structuralTypeExpr.value); 16 | 17 | // TODO Error if not correct type 18 | inter.nominalType = nominalType?.isObjectType() ? nominalType : undefined; 19 | inter.structuralType = structuralType?.isObjectType() 20 | ? structuralType 21 | : undefined; 22 | 23 | return inter; 24 | }; 25 | -------------------------------------------------------------------------------- /src/semantics/resolution/resolve-match.ts: -------------------------------------------------------------------------------- 1 | import { Block } from "../../syntax-objects/block.js"; 2 | import { Call, Parameter, Type, Variable } from "../../syntax-objects/index.js"; 3 | import { Match, MatchCase } from "../../syntax-objects/match.js"; 4 | import { combineTypes } from "./combine-types.js"; 5 | import { getExprType } from "./get-expr-type.js"; 6 | import { resolveEntities, resolveVar } from "./resolve-entities.js"; 7 | import { resolveTypeExpr } from "./resolve-type-expr.js"; 8 | 9 | export const resolveMatch = (match: Match): Match => { 10 | match.operand = resolveEntities(match.operand); 11 | match.baseType = getExprType(match.operand); 12 | 13 | const binding = getBinding(match); 14 | resolveCases(binding, match); 15 | match.type = resolveMatchReturnType(match); 16 | 17 | return match; 18 | }; 19 | 20 | const resolveCases = (binding: Parameter | Variable, match: Match) => { 21 | match.cases = match.cases.map((c) => resolveCase(binding, c)); 22 | match.defaultCase = match.defaultCase 23 | ? resolveCase(binding, match.defaultCase) 24 | : undefined; 25 | }; 26 | 27 | const resolveCase = ( 28 | binding: Parameter | Variable, 29 | c: MatchCase 30 | ): MatchCase => { 31 | if (c.matchTypeExpr) resolveTypeExpr(c.matchTypeExpr); 32 | const type = getExprType(c.matchTypeExpr); 33 | 34 | const localBinding = binding.clone(); 35 | localBinding.originalType = binding.type; 36 | localBinding.type = type; 37 | localBinding.requiresCast = true; 38 | 39 | // NOTE: This binding is temporary and will be overwritten in the next case. 40 | // We may need to introduce an wrapping block and register it to the blocks scope 41 | // to avoid this. 42 | c.expr.registerEntity(localBinding); 43 | 44 | const expr = resolveEntities(c.expr) as Call | Block; 45 | 46 | return { 47 | matchType: type?.isObjectType() ? type : undefined, 48 | expr, 49 | matchTypeExpr: c.matchTypeExpr, 50 | }; 51 | }; 52 | 53 | const getBinding = (match: Match): Parameter | Variable => { 54 | if (match.bindVariable) { 55 | return resolveVar(match.bindVariable); 56 | } 57 | 58 | const binding = match.bindIdentifier.resolve(); 59 | 60 | if (!(binding?.isVariable() || binding?.isParameter())) { 61 | throw new Error(`Match binding must be a variable or parameter`); 62 | } 63 | 64 | return binding; 65 | }; 66 | 67 | const resolveMatchReturnType = (match: Match): Type | undefined => { 68 | const cases = match.cases 69 | .map((c) => c.expr.type) 70 | .concat(match.defaultCase?.expr.type) 71 | .filter((t) => t !== undefined); 72 | 73 | return combineTypes(cases); 74 | }; 75 | -------------------------------------------------------------------------------- /src/semantics/resolution/resolve-object-type.ts: -------------------------------------------------------------------------------- 1 | import { Call } from "../../syntax-objects/call.js"; 2 | import { nop } from "../../syntax-objects/lib/helpers.js"; 3 | import { List } from "../../syntax-objects/list.js"; 4 | import { 5 | ObjectType, 6 | TypeAlias, 7 | voydBaseObject, 8 | } from "../../syntax-objects/types.js"; 9 | import { getExprType } from "./get-expr-type.js"; 10 | import { implIsCompatible, resolveImpl } from "./resolve-impl.js"; 11 | import { typesAreCompatible } from "./types-are-compatible.js"; 12 | import { resolveTypeExpr } from "./resolve-type-expr.js"; 13 | 14 | export const resolveObjectType = (obj: ObjectType, call?: Call): ObjectType => { 15 | if (obj.typesResolved) return obj; 16 | 17 | if (obj.typeParameters) { 18 | return resolveGenericObjVersion(obj, call) ?? obj; 19 | } 20 | 21 | obj.fields.forEach((field) => { 22 | field.typeExpr = resolveTypeExpr(field.typeExpr); 23 | field.type = getExprType(field.typeExpr); 24 | }); 25 | 26 | if (obj.parentObjExpr) { 27 | obj.parentObjExpr = resolveTypeExpr(obj.parentObjExpr); 28 | const parentType = getExprType(obj.parentObjExpr); 29 | obj.parentObjType = parentType?.isObjectType() ? parentType : undefined; 30 | } else { 31 | obj.parentObjType = voydBaseObject; 32 | } 33 | 34 | obj.typesResolved = true; 35 | return obj; 36 | }; 37 | 38 | const resolveGenericObjVersion = ( 39 | type: ObjectType, 40 | call?: Call 41 | ): ObjectType | undefined => { 42 | if (!call?.typeArgs) return; 43 | const existing = type.genericInstances?.find((c) => typeArgsMatch(call, c)); 44 | if (existing) return existing; 45 | return resolveGenericsWithTypeArgs(type, call.typeArgs); 46 | }; 47 | 48 | const resolveGenericsWithTypeArgs = ( 49 | obj: ObjectType, 50 | args: List 51 | ): ObjectType => { 52 | const typeParameters = obj.typeParameters!; 53 | 54 | if (args.length !== typeParameters.length) { 55 | return obj; 56 | } 57 | 58 | const newObj = obj.clone(); 59 | newObj.typeParameters = undefined; 60 | newObj.appliedTypeArgs = []; 61 | newObj.genericParent = obj; 62 | 63 | /** Register resolved type entities for each type param */ 64 | let typesNotResolved = false; 65 | typeParameters.forEach((typeParam, index) => { 66 | const typeArg = args.exprAt(index); 67 | const identifier = typeParam.clone(); 68 | const type = new TypeAlias({ 69 | name: identifier, 70 | typeExpr: nop(), 71 | }); 72 | resolveTypeExpr(typeArg); 73 | type.type = getExprType(typeArg); 74 | if (!type.type) typesNotResolved = true; 75 | newObj.appliedTypeArgs?.push(type); 76 | newObj.registerEntity(type); 77 | }); 78 | 79 | if (typesNotResolved) return obj; 80 | const resolvedObj = resolveObjectType(newObj); 81 | obj.registerGenericInstance(resolvedObj); 82 | 83 | const implementations = newObj.implementations; 84 | newObj.implementations = []; // Clear implementations to avoid duplicates, resolveImpl will re-add them 85 | 86 | implementations 87 | .filter((impl) => implIsCompatible(impl, resolvedObj)) 88 | .map((impl) => resolveImpl(impl, resolvedObj)); 89 | 90 | return resolvedObj; 91 | }; 92 | 93 | const typeArgsMatch = (call: Call, candidate: ObjectType): boolean => 94 | call.typeArgs && candidate.appliedTypeArgs 95 | ? candidate.appliedTypeArgs.every((t, i) => { 96 | const argType = getExprType(call.typeArgs?.at(i)); 97 | const appliedType = getExprType(t); 98 | return typesAreCompatible(argType, appliedType, { 99 | exactNominalMatch: true, 100 | }); 101 | }) 102 | : true; 103 | -------------------------------------------------------------------------------- /src/semantics/resolution/resolve-trait.ts: -------------------------------------------------------------------------------- 1 | import { Trait } from "../../syntax-objects/trait.js"; 2 | import { resolveFn } from "./resolve-fn.js"; 3 | 4 | export const resolveTrait = (trait: Trait) => { 5 | trait.methods.applyMap((fn) => resolveFn(fn)); 6 | return trait; 7 | }; 8 | -------------------------------------------------------------------------------- /src/semantics/resolution/resolve-type-expr.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "path"; 2 | import { 3 | Call, 4 | Expr, 5 | FixedArrayType, 6 | nop, 7 | TypeAlias, 8 | } from "../../syntax-objects/index.js"; 9 | import { getExprType } from "./get-expr-type.js"; 10 | import { resolveIntersectionType } from "./resolve-intersection.js"; 11 | import { resolveObjectType } from "./resolve-object-type.js"; 12 | import { resolveUnionType } from "./resolve-union.js"; 13 | 14 | export const resolveTypeExpr = (typeExpr: Expr): Expr => { 15 | if (typeExpr.isIdentifier()) return typeExpr; 16 | if (typeExpr.isObjectType()) return resolveObjectType(typeExpr); 17 | if (typeExpr.isIntersectionType()) return resolveIntersectionType(typeExpr); 18 | if (typeExpr.isUnionType()) return resolveUnionType(typeExpr); 19 | if (typeExpr.isFixedArrayType()) return resolveFixedArrayType(typeExpr); 20 | if (typeExpr.isCall()) return resolveTypeCall(typeExpr); 21 | return typeExpr; 22 | }; 23 | 24 | /** Resolves type calls */ 25 | const resolveTypeCall = (call: Call): Call => { 26 | const type = call.fnName.resolve(); 27 | 28 | if (!type?.isType()) return call; 29 | 30 | if (call.typeArgs) { 31 | call.typeArgs = call.typeArgs.map(resolveTypeExpr); 32 | } 33 | 34 | if (type.isObjectType()) { 35 | call.fn = type; 36 | call.type = resolveObjectType(type, call); 37 | return call; 38 | } 39 | 40 | if (type.isFixedArrayType()) { 41 | call.type = resolveFixedArrayType(type); 42 | return call; 43 | } 44 | 45 | if (type.isUnionType()) { 46 | call.type = resolveUnionType(type); 47 | return call; 48 | } 49 | 50 | if (type.isIntersectionType()) { 51 | call.type = resolveIntersectionType(type); 52 | return call; 53 | } 54 | 55 | if (type.isTypeAlias()) { 56 | call = resolveTypeAlias(call, type); 57 | return call; 58 | } 59 | 60 | call.type = type; 61 | 62 | return call; 63 | }; 64 | 65 | const resolveFixedArrayType = (arr: FixedArrayType): FixedArrayType => { 66 | arr.elemTypeExpr = resolveTypeExpr(arr.elemTypeExpr); 67 | arr.elemType = getExprType(arr.elemTypeExpr); 68 | arr.id = `${arr.id}#${arr.elemType?.id}`; 69 | return arr; 70 | }; 71 | 72 | export const resolveTypeAlias = (call: Call, type: TypeAlias): Call => { 73 | const alias = type.clone(); 74 | 75 | if (alias.typeParameters) { 76 | alias.typeParameters.forEach((typeParam, index) => { 77 | const typeArg = call.typeArgs?.exprAt(index); 78 | const identifier = typeParam.clone(); 79 | const type = new TypeAlias({ 80 | name: identifier, 81 | typeExpr: nop(), 82 | }); 83 | type.type = getExprType(typeArg); 84 | alias.registerEntity(type); 85 | }); 86 | } 87 | 88 | alias.typeExpr = resolveTypeExpr(alias.typeExpr); 89 | alias.type = getExprType(alias.typeExpr); 90 | call.type = alias.type; 91 | call.fn = call.type?.isObjectType() ? call.type : undefined; 92 | return call; 93 | }; 94 | -------------------------------------------------------------------------------- /src/semantics/resolution/resolve-union.ts: -------------------------------------------------------------------------------- 1 | import { UnionType } from "../../syntax-objects/types.js"; 2 | import { getExprType } from "./get-expr-type.js"; 3 | import { resolveEntities } from "./resolve-entities.js"; 4 | 5 | export const resolveUnionType = (union: UnionType): UnionType => { 6 | union.childTypeExprs.applyMap((expr) => resolveEntities(expr)); 7 | union.types = union.childTypeExprs.toArray().flatMap((expr) => { 8 | const type = getExprType(expr); 9 | 10 | if (!type?.isObjectType()) { 11 | console.warn(`Union type must be an object type at ${expr.location}`); 12 | } 13 | 14 | return type?.isObjectType() ? [type] : []; 15 | }); 16 | 17 | return union; 18 | }; 19 | -------------------------------------------------------------------------------- /src/semantics/resolution/types-are-compatible.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "../../syntax-objects/index.js"; 2 | import { getExprType } from "./get-expr-type.js"; 3 | 4 | export const typesAreCompatible = ( 5 | /** A is the argument type, the type of the value being passed as b */ 6 | a?: Type, 7 | /** B is the parameter type, what a should be equivalent to */ 8 | b?: Type, 9 | opts: { 10 | /** Will not check that a is an extension of b if true */ 11 | structuralOnly?: boolean; 12 | 13 | /** Will ancestors, the type must be the same regardless of inheritance */ 14 | exactNominalMatch?: boolean; 15 | } = {} 16 | ): boolean => { 17 | if (!a || !b) return false; 18 | 19 | if (a.isPrimitiveType() && b.isPrimitiveType()) { 20 | return a.id === b.id; 21 | } 22 | 23 | if (a.isObjectType() && b.isObjectType()) { 24 | const structural = opts.structuralOnly || b.isStructural; 25 | 26 | if (structural) { 27 | return b.fields.every((field) => { 28 | const match = a.fields.find((f) => f.name === field.name); 29 | return match && typesAreCompatible(field.type, match.type, opts); 30 | }); 31 | } 32 | 33 | if (a.genericParent && a.genericParent.id === b.genericParent?.id) { 34 | return !!a.appliedTypeArgs?.every((arg, index) => 35 | typesAreCompatible( 36 | getExprType(arg), 37 | getExprType(b.appliedTypeArgs?.[index]), 38 | opts 39 | ) 40 | ); 41 | } 42 | 43 | if (opts.exactNominalMatch) return a.id === b.id; 44 | 45 | return a.extends(b); 46 | } 47 | 48 | if (a.isObjectType() && b.isUnionType()) { 49 | return b.types.some((type) => typesAreCompatible(a, type, opts)); 50 | } 51 | 52 | if (a.isUnionType() && b.isUnionType()) { 53 | return a.types.every((aType) => 54 | b.types.some((bType) => typesAreCompatible(aType, bType, opts)) 55 | ); 56 | } 57 | 58 | if (a.isObjectType() && b.isIntersectionType()) { 59 | if (!b.nominalType || !b.structuralType) return false; 60 | return a.extends(b.nominalType) && typesAreCompatible(a, b.structuralType); 61 | } 62 | 63 | if (a.isIntersectionType() && b.isIntersectionType()) { 64 | return ( 65 | typesAreCompatible(a.nominalType, b.nominalType) && 66 | typesAreCompatible(a.structuralType, b.structuralType) 67 | ); 68 | } 69 | 70 | if (a.isFixedArrayType() && b.isFixedArrayType()) { 71 | return typesAreCompatible(a.elemType, b.elemType); 72 | } 73 | 74 | if (a.isFnType() && b.isFnType()) { 75 | if (a.parameters.length !== b.parameters.length) return false; 76 | if (a.name.value !== b.name.value) return false; 77 | return ( 78 | typesAreCompatible(a.returnType, b.returnType) && 79 | a.parameters.every((p, i) => 80 | typesAreCompatible(p.type, b.parameters[i]?.type) 81 | ) 82 | ); 83 | } 84 | 85 | return false; 86 | }; 87 | -------------------------------------------------------------------------------- /src/semantics/types.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "../syntax-objects/expr.js"; 2 | 3 | export type SemanticProcessor = (expr: Expr) => Expr; 4 | -------------------------------------------------------------------------------- /src/syntax-objects/README.md: -------------------------------------------------------------------------------- 1 | # Syntax Objects 2 | 3 | Syntax objects are data structures that represent voyd's language constructs. This includes functions, function calls, variables etc. 4 | 5 | # Guidelines 6 | 7 | - Each Syntax Object should be part of the `Expr` union 8 | - Syntax objects must track their parent-child relationship. A child typically 9 | belongs to a parent when it was directly defined with the parent. I.E. 10 | parameters of functions, expressions / variables of block. These parent 11 | relationships are stored via the parent pointer and must stay up to date 12 | during clones. Use `ChildList` and `ChildMap` data types to help keep 13 | make this easier. 14 | - Resolved values should not be considered a child of the expression they 15 | were resolved from. The type expression (`typeExpr`) of a parameter is 16 | a child of the parameter, but the type it resolves to should never 17 | accidentally be marked as a child of the parameter. Nor should it be 18 | included in the clone (instead, they type resolution can be run again) 19 | -------------------------------------------------------------------------------- /src/syntax-objects/block.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { ChildList } from "./lib/child-list.js"; 3 | import { List } from "./list.js"; 4 | import { ScopedSyntax, ScopedSyntaxMetadata } from "./scoped-entity.js"; 5 | import { Type } from "./types.js"; 6 | 7 | export class Block extends ScopedSyntax { 8 | readonly syntaxType = "block"; 9 | #body = new ChildList(undefined, this); 10 | type?: Type; 11 | 12 | constructor( 13 | opts: ScopedSyntaxMetadata & { 14 | body: List | Expr[]; 15 | type?: Type; 16 | } 17 | ) { 18 | super(opts); 19 | const { body, type } = opts; 20 | this.#body.push(...(body instanceof Array ? body : body.toArray())); 21 | this.type = type; 22 | } 23 | 24 | get children() { 25 | return this.#body.toArray(); 26 | } 27 | 28 | get body() { 29 | return this.#body.toArray(); 30 | } 31 | 32 | set body(body: Expr[]) { 33 | this.#body = new ChildList(body, this); 34 | } 35 | 36 | lastExpr() { 37 | return this.body.at(-1); 38 | } 39 | 40 | each(fn: (expr: Expr, index: number, array: Expr[]) => Expr) { 41 | this.body.forEach(fn); 42 | return this; 43 | } 44 | 45 | /** Sets the parent on each element immediately before the mapping of the next */ 46 | applyMap(fn: (expr: Expr, index: number, array: Expr[]) => Expr) { 47 | this.#body.applyMap(fn); 48 | return this; 49 | } 50 | 51 | /** Calls the evaluator function on the block's body and returns the result of the last evaluation. */ 52 | evaluate(evaluator: (expr: Expr) => Expr): Expr | undefined { 53 | return this.body.map(evaluator).at(-1); 54 | } 55 | 56 | toJSON() { 57 | return ["block", ...this.body]; 58 | } 59 | 60 | clone(parent?: Expr) { 61 | return new Block({ 62 | ...this.getCloneOpts(parent), 63 | body: this.#body.clone(), 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/syntax-objects/bool.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { Syntax, SyntaxMetadata } from "./syntax.js"; 3 | 4 | export class Bool extends Syntax { 5 | readonly syntaxType = "bool"; 6 | value: boolean; 7 | 8 | constructor(opts: SyntaxMetadata & { value: boolean }) { 9 | super(opts); 10 | this.value = opts.value; 11 | } 12 | 13 | clone(parent?: Expr): Bool { 14 | return new Bool({ ...super.getCloneOpts(parent), value: this.value }); 15 | } 16 | 17 | toJSON() { 18 | return this.value; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/syntax-objects/call.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { Fn } from "./fn.js"; 3 | import { Identifier } from "./identifier.js"; 4 | import { LexicalContext } from "./lib/lexical-context.js"; 5 | import { List } from "./list.js"; 6 | import { Syntax, SyntaxMetadata } from "./syntax.js"; 7 | import { ObjectType, Type } from "./types.js"; 8 | 9 | /** Defines a function call */ 10 | export class Call extends Syntax { 11 | readonly syntaxType = "call"; 12 | fn?: Fn | ObjectType; 13 | fnName: Identifier; 14 | args: List; 15 | typeArgs?: List; 16 | #type?: Type; 17 | 18 | constructor( 19 | opts: SyntaxMetadata & { 20 | fnName: Identifier; 21 | fn?: Fn; 22 | args: List; 23 | type?: Type; 24 | lexicon?: LexicalContext; 25 | typeArgs?: List; 26 | } 27 | ) { 28 | super(opts); 29 | this.fnName = opts.fnName; 30 | this.fn = opts.fn; 31 | this.args = opts.args; 32 | this.args.parent = this; 33 | this.typeArgs = opts.typeArgs?.clone(this); 34 | if (this.typeArgs) this.typeArgs.parent = this; 35 | this.#type = opts.type; 36 | } 37 | 38 | get children() { 39 | return [...this.args.toArray(), ...(this.typeArgs?.toArray() ?? [])]; 40 | } 41 | 42 | set type(type: Type | undefined) { 43 | this.#type = type; 44 | } 45 | 46 | get type() { 47 | if (!this.#type && this.fn?.isFn()) { 48 | this.#type = this.fn.returnType; 49 | } 50 | 51 | if (!this.#type && this.fn?.isObjectType()) { 52 | this.#type = this.fn; 53 | } 54 | 55 | return this.#type; 56 | } 57 | 58 | eachArg(fn: (expr: Expr) => void) { 59 | this.args.each(fn); 60 | return this; 61 | } 62 | 63 | argAt(index: number) { 64 | return this.args.at(index); 65 | } 66 | 67 | exprArgAt(index: number): Expr { 68 | const expr = this.argAt(index); 69 | 70 | if (!expr) { 71 | throw new Error(`No expression found at ${index}`); 72 | } 73 | 74 | return expr; 75 | } 76 | 77 | // Returns the value of the labeled argument at the given index 78 | labeledArgAt(index: number): Expr { 79 | const label = this.args.at(index); 80 | 81 | if (!label?.isCall() || !label?.calls(":")) { 82 | throw new Error(`No label found at ${index}`); 83 | } 84 | 85 | return label.exprArgAt(1); 86 | } 87 | 88 | // Returns the value of the labeled argument at the given index 89 | optionalLabeledArgAt(index: number): Expr | undefined { 90 | try { 91 | return this.labeledArgAt(index); 92 | } catch (_e) { 93 | return undefined; 94 | } 95 | } 96 | 97 | callArgAt(index: number): Call { 98 | const call = this.args.at(index); 99 | if (!call?.isCall()) { 100 | throw new Error(`No call at ${index}`); 101 | } 102 | return call; 103 | } 104 | 105 | identifierArgAt(index: number): Identifier { 106 | const call = this.args.at(index); 107 | if (!call?.isIdentifier()) { 108 | throw new Error(`No identifier at ${index}`); 109 | } 110 | return call; 111 | } 112 | 113 | argsArray(): Expr[] { 114 | return this.args.toArray(); 115 | } 116 | 117 | argArrayMap(fn: (expr: Expr) => T): T[] { 118 | return this.args.toArray().map(fn); 119 | } 120 | 121 | calls(name: string) { 122 | return this.fnName.is(name); 123 | } 124 | 125 | toJSON() { 126 | return [this.fnName, ...this.args.toArray()]; 127 | } 128 | 129 | clone(parent?: Expr) { 130 | return new Call({ 131 | ...this.getCloneOpts(parent), 132 | fnName: this.fnName.clone(), 133 | args: this.args.clone(), 134 | typeArgs: this.typeArgs?.clone(), 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/syntax-objects/declaration.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { Fn } from "./fn.js"; 3 | import { Syntax, SyntaxMetadata } from "./syntax.js"; 4 | 5 | /** Defines a declared namespace for external function imports */ 6 | export class Declaration extends Syntax { 7 | readonly syntaxType = "declaration"; 8 | namespace: string; 9 | fns: Fn[]; 10 | 11 | constructor( 12 | opts: SyntaxMetadata & { 13 | namespace: string; 14 | fns?: Fn[]; 15 | } 16 | ) { 17 | super(opts); 18 | this.namespace = opts.namespace; 19 | this.fns = opts.fns ?? []; 20 | this.fns.forEach((fn) => (fn.parent = this)); 21 | } 22 | 23 | toJSON() { 24 | return ["declare", this.namespace, this.fns]; 25 | } 26 | 27 | clone(parent?: Expr) { 28 | return new Declaration({ 29 | ...this.getCloneOpts(parent), 30 | namespace: this.namespace, 31 | fns: this.fns.map((fn) => fn.clone()), 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/syntax-objects/expr.ts: -------------------------------------------------------------------------------- 1 | import type { Bool } from "./bool.js"; 2 | import type { Float } from "./float.js"; 3 | import type { Fn } from "./fn.js"; 4 | import type { Identifier } from "./identifier.js"; 5 | import type { Int } from "./int.js"; 6 | import type { List } from "./list.js"; 7 | import { Parameter } from "./parameter.js"; 8 | import type { StringLiteral } from "./string-literal.js"; 9 | import type { Type } from "./types.js"; 10 | import { Variable } from "./variable.js"; 11 | import type { Whitespace } from "./whitespace.js"; 12 | import type { Global } from "./global.js"; 13 | import { MacroVariable } from "./macro-variable.js"; 14 | import { Macro } from "./macros.js"; 15 | import { MacroLambda } from "./macro-lambda.js"; 16 | import { Call } from "./call.js"; 17 | import { Block } from "./block.js"; 18 | import { VoydModule } from "./module.js"; 19 | import { Declaration } from "./declaration.js"; 20 | import { Use } from "./use.js"; 21 | import { ObjectLiteral } from "./object-literal.js"; 22 | import { Match } from "./match.js"; 23 | import { Nop } from "./nop.js"; 24 | import { Implementation } from "./implementation.js"; 25 | import { Trait } from "./trait.js"; 26 | 27 | export type Expr = 28 | | PrimitiveExpr 29 | | Type 30 | | Fn 31 | | Macro 32 | | Variable 33 | | Parameter 34 | | Global 35 | | MacroVariable 36 | | MacroLambda 37 | | VoydModule 38 | | Call 39 | | Block 40 | | Declaration 41 | | Use 42 | | ObjectLiteral 43 | | Match 44 | | Nop 45 | | Implementation 46 | | Trait; 47 | 48 | /** 49 | * These are the Expr types that must be returned until all macros have been expanded (reader, syntax, and regular) 50 | */ 51 | export type PrimitiveExpr = 52 | | Bool 53 | | Int 54 | | Float 55 | | StringLiteral 56 | | Identifier 57 | | Whitespace 58 | | List; 59 | -------------------------------------------------------------------------------- /src/syntax-objects/float.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { Syntax, SyntaxMetadata } from "./syntax.js"; 3 | 4 | export type FloatOpts = SyntaxMetadata & { 5 | value: FloatValue; 6 | }; 7 | 8 | export type FloatValue = number | { type: "f64"; value: number }; 9 | 10 | export class Float extends Syntax { 11 | readonly syntaxType = "float"; 12 | value: FloatValue; 13 | 14 | constructor(opts: FloatOpts) { 15 | super(opts); 16 | this.value = opts.value; 17 | } 18 | 19 | clone(parent?: Expr): Float { 20 | return new Float({ 21 | ...super.getCloneOpts(parent), 22 | value: this.value, 23 | }); 24 | } 25 | 26 | toJSON() { 27 | return this.value; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/syntax-objects/fn.ts: -------------------------------------------------------------------------------- 1 | import type { Expr } from "./expr.js"; 2 | import { Identifier } from "./identifier.js"; 3 | import { ChildList } from "./lib/child-list.js"; 4 | import { Child } from "./lib/child.js"; 5 | import { ScopedNamedEntity, ScopedNamedEntityOpts } from "./named-entity.js"; 6 | import { Parameter } from "./parameter.js"; 7 | import { FnType, Type } from "./types.js"; 8 | import { Variable } from "./variable.js"; 9 | 10 | export class Fn extends ScopedNamedEntity { 11 | readonly syntaxType = "fn"; 12 | readonly #parameters = new ChildList([], this); 13 | readonly #body = new Child(undefined, this); 14 | readonly #returnTypeExpr = new Child(undefined, this); 15 | readonly #genericInstances = new ChildList([], this); 16 | #typeParams = new ChildList([], this); 17 | variables: Variable[] = []; 18 | returnType?: Type; // When a function has generics, resolved versions of the functions go here 19 | inferredReturnType?: Type; 20 | annotatedReturnType?: Type; 21 | appliedTypeArgs?: Type[] = []; 22 | typesResolved?: boolean; 23 | #iteration = 0; 24 | 25 | constructor( 26 | opts: ScopedNamedEntityOpts & { 27 | returnTypeExpr?: Expr; 28 | variables?: Variable[]; 29 | parameters?: Parameter[]; 30 | typeParameters?: Identifier[]; 31 | body?: Expr; 32 | } 33 | ) { 34 | super(opts); 35 | this.#parameters.push(...(opts.parameters ?? [])); 36 | this.#typeParams.push(...(opts.typeParameters ?? [])); 37 | this.returnTypeExpr = opts.returnTypeExpr; 38 | this.variables = opts.variables ?? []; 39 | this.body = opts.body; 40 | } 41 | 42 | get body() { 43 | return this.#body.value; 44 | } 45 | 46 | set body(body: Expr | undefined) { 47 | this.#body.value = body; 48 | } 49 | 50 | get parameters() { 51 | return this.#parameters.toArray(); 52 | } 53 | 54 | get returnTypeExpr() { 55 | return this.#returnTypeExpr.value; 56 | } 57 | 58 | set returnTypeExpr(returnTypeExpr: Expr | undefined) { 59 | this.#returnTypeExpr.value = returnTypeExpr; 60 | } 61 | 62 | get genericInstances() { 63 | const instances = this.#genericInstances.toArray(); 64 | return !instances.length ? undefined : instances; 65 | } 66 | 67 | get typeParameters() { 68 | const params = this.#typeParams.toArray(); 69 | return !params.length ? undefined : params; 70 | } 71 | 72 | set typeParameters(params: Identifier[] | undefined) { 73 | this.#typeParams = new ChildList(params ?? [], this); 74 | } 75 | 76 | // Register a version of this function with resolved generics 77 | registerGenericInstance(fn: Fn) { 78 | this.#genericInstances.push(fn); 79 | } 80 | 81 | getNameStr(): string { 82 | return this.name.value; 83 | } 84 | 85 | getType(): FnType { 86 | return new FnType({ 87 | ...super.getCloneOpts(this.parent), 88 | parameters: this.parameters, 89 | returnType: this.getReturnType(), 90 | }); 91 | } 92 | 93 | getIndexOfParameter(parameter: Parameter) { 94 | const index = this.parameters.findIndex((p) => p.id === parameter.id); 95 | if (index < 0) { 96 | throw new Error(`Parameter ${parameter} not registered with fn ${this}`); 97 | } 98 | return index; 99 | } 100 | 101 | getIndexOfVariable(variable: Variable) { 102 | const index = this.variables.findIndex((v) => v.id === variable.id); 103 | 104 | if (index < 0) { 105 | const newIndex = this.variables.push(variable) - 1; 106 | return newIndex + this.parameters.length; 107 | } 108 | 109 | return index + this.parameters.length; 110 | } 111 | 112 | getReturnType(): Type { 113 | if (this.returnType) { 114 | return this.returnType; 115 | } 116 | 117 | throw new Error( 118 | `Return type not yet resolved for fn ${this.name} at ${this.location}` 119 | ); 120 | } 121 | 122 | toString() { 123 | return this.id; 124 | } 125 | 126 | clone(parent?: Expr | undefined): Fn { 127 | // Don't clone generic instances 128 | return new Fn({ 129 | ...super.getCloneOpts(parent), 130 | id: `${this.id}#${this.#iteration++}`, 131 | returnTypeExpr: this.returnTypeExpr?.clone(), 132 | parameters: this.#parameters.clone(), 133 | typeParameters: this.#typeParams.clone(), 134 | body: this.body?.clone(), 135 | }); 136 | } 137 | 138 | toJSON(): unknown { 139 | return [ 140 | "fn", 141 | this.id, 142 | ["parameters", ...this.parameters], 143 | ["type-parameters", ...(this.#typeParams.toArray() ?? [])], 144 | ["return-type", this.#returnTypeExpr?.toJSON() ?? null], 145 | this.body, 146 | ]; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/syntax-objects/global.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { NamedEntity, NamedEntityOpts } from "./named-entity.js"; 3 | import { Type } from "./types.js"; 4 | 5 | export class Global extends NamedEntity { 6 | readonly isMutable: boolean; 7 | readonly type: Type; 8 | readonly syntaxType = "global"; 9 | readonly initializer: Expr; 10 | 11 | constructor( 12 | opts: NamedEntityOpts & { 13 | isMutable: boolean; 14 | initializer: Expr; 15 | type: Type; 16 | } 17 | ) { 18 | super(opts); 19 | this.isMutable = opts.isMutable; 20 | this.type = opts.type; 21 | this.initializer = opts.initializer; 22 | } 23 | 24 | toString() { 25 | return this.name.toString(); 26 | } 27 | 28 | toJSON() { 29 | return [ 30 | "define-global", 31 | this.name, 32 | this.type, 33 | ["is-mutable", this.isMutable], 34 | this.initializer, 35 | ]; 36 | } 37 | 38 | clone(parent?: Expr | undefined): Global { 39 | return new Global({ 40 | ...super.getCloneOpts(parent), 41 | isMutable: this.isMutable, 42 | initializer: this.initializer, 43 | type: this.type, 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/syntax-objects/identifier.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { NamedEntity } from "./named-entity.js"; 3 | import { Syntax, SyntaxMetadata } from "./syntax.js"; 4 | 5 | export type Id = string | Identifier; 6 | 7 | export type IdentifierOpts = SyntaxMetadata & { 8 | value: string; 9 | isQuoted?: boolean; 10 | }; 11 | 12 | export class Identifier extends Syntax { 13 | readonly syntaxType = "identifier"; 14 | /** Is surrounded by single quotes, allows identifiers to have spaces */ 15 | readonly isQuoted?: boolean; 16 | /** The given name of the identifier */ 17 | value: string; 18 | 19 | constructor(opts: string | IdentifierOpts) { 20 | if (typeof opts === "string") { 21 | opts = { value: opts }; 22 | } 23 | 24 | super(opts); 25 | this.isQuoted = opts.isQuoted; 26 | this.value = opts.value; 27 | } 28 | 29 | is(v: Id) { 30 | if (typeof v === "string") { 31 | return v === this.value; 32 | } 33 | 34 | return v.value === this.value; 35 | } 36 | 37 | isDefined() { 38 | return !!this.resolveEntity(this); 39 | } 40 | 41 | resolve() { 42 | return this.resolveEntity(this); 43 | } 44 | 45 | startsWith(search: string) { 46 | return this.value.startsWith(search); 47 | } 48 | 49 | replace(search: string, newVal: string): Identifier { 50 | return new Identifier({ 51 | ...super.getCloneOpts(), 52 | value: this.value.replace(search, newVal), 53 | }); 54 | } 55 | 56 | clone(parent?: Expr): Identifier { 57 | return new Identifier({ 58 | ...super.getCloneOpts(parent), 59 | value: this.value, 60 | isQuoted: this.isQuoted, 61 | }); 62 | } 63 | 64 | static from(str: string) { 65 | return new Identifier({ value: str }); 66 | } 67 | 68 | toString() { 69 | return this.value; 70 | } 71 | 72 | toJSON() { 73 | return this.value; 74 | } 75 | } 76 | 77 | export class MockIdentifier extends Identifier { 78 | readonly #entity?: NamedEntity; 79 | constructor( 80 | opts: IdentifierOpts & { 81 | entity?: NamedEntity; // The entity this identifier resolves to 82 | } 83 | ) { 84 | super(opts); 85 | this.#entity = opts.entity; 86 | } 87 | 88 | resolve() { 89 | return this.#entity; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/syntax-objects/implementation.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { Fn } from "./fn.js"; 3 | import { nop } from "./lib/helpers.js"; 4 | import { Identifier } from "./identifier.js"; 5 | import { ChildList } from "./lib/child-list.js"; 6 | import { Child } from "./lib/child.js"; 7 | import { ScopedSyntax, ScopedSyntaxMetadata } from "./scoped-entity.js"; 8 | import { Trait } from "./trait.js"; 9 | import { Type } from "./types.js"; 10 | 11 | export type ImplementationOpts = ScopedSyntaxMetadata & { 12 | typeParams: Identifier[]; 13 | targetTypeExpr: Expr; 14 | body: Expr; 15 | traitExpr?: Expr; 16 | }; 17 | 18 | export class Implementation extends ScopedSyntax { 19 | readonly syntaxType = "implementation"; 20 | readonly typeParams: ChildList; 21 | readonly targetTypeExpr: Child; 22 | readonly body: Child; 23 | readonly traitExpr: Child; 24 | readonly #exports = new Map(); // NO CLONE! 25 | readonly #methods = new Map(); // NO CLONE! 26 | typesResolved?: boolean; 27 | targetType?: Type; 28 | trait?: Trait; 29 | 30 | constructor(opts: ImplementationOpts) { 31 | super(opts); 32 | this.typeParams = new ChildList(opts.typeParams, this); 33 | this.targetTypeExpr = new Child(opts.targetTypeExpr, this); 34 | this.body = new Child(opts.body, this); 35 | this.traitExpr = new Child(opts.traitExpr, this); 36 | } 37 | 38 | get exports(): ReadonlyArray { 39 | return [...this.#exports.values()]; 40 | } 41 | 42 | get methods(): ReadonlyArray { 43 | return [...this.#methods.values()]; 44 | } 45 | 46 | registerExport(v: Fn): Implementation { 47 | this.#exports.set(v.id, v); 48 | this.registerEntity(v); // dirty way to make sure it's in the scope 49 | return this; 50 | } 51 | 52 | registerMethod(v: Fn): Implementation { 53 | this.#methods.set(v.id, v); 54 | return this; 55 | } 56 | 57 | clone(parent?: Expr) { 58 | const impl = new Implementation({ 59 | ...super.getCloneOpts(parent), 60 | typeParams: this.typeParams.clone(), 61 | targetTypeExpr: this.targetTypeExpr.clone(), 62 | body: nop(), 63 | traitExpr: this.traitExpr.clone(), 64 | }); 65 | impl.body.value = this.body.clone(impl); 66 | return impl; 67 | } 68 | 69 | toJSON(): unknown { 70 | return [ 71 | "impl", 72 | ["type-params", this.typeParams.toArray()], 73 | ["target", this.targetTypeExpr.toJSON()], 74 | ["body", this.body.toJSON()], 75 | ]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/syntax-objects/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./syntax.js"; 2 | export * from "./bool.js"; 3 | export * from "./expr.js"; 4 | export * from "./float.js"; 5 | export * from "./lib/helpers.js"; 6 | export * from "./identifier.js"; 7 | export * from "./int.js"; 8 | export * from "./list.js"; 9 | export * from "./string-literal.js"; 10 | export * from "./types.js"; 11 | export * from "./whitespace.js"; 12 | export * from "./fn.js"; 13 | export * from "./global.js"; 14 | export * from "./variable.js"; 15 | export * from "./parameter.js"; 16 | export * from "./block.js"; 17 | export * from "./call.js"; 18 | export * from "./module.js"; 19 | export * from "./macro-lambda.js"; 20 | export * from "./macro-variable.js"; 21 | export * from "./macros.js"; 22 | export * from "./declaration.js"; 23 | export * from "./use.js"; 24 | export * from "./object-literal.js"; 25 | -------------------------------------------------------------------------------- /src/syntax-objects/int.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { Syntax, SyntaxMetadata } from "./syntax.js"; 3 | 4 | export type IntOpts = SyntaxMetadata & { 5 | value: IntValue; 6 | }; 7 | 8 | export type IntValue = number | { type: "i64"; value: bigint }; 9 | 10 | export class Int extends Syntax { 11 | readonly syntaxType = "int"; 12 | value: IntValue; 13 | 14 | constructor(opts: IntOpts) { 15 | super(opts); 16 | this.value = opts.value; 17 | } 18 | 19 | clone(parent?: Expr): Int { 20 | return new Int({ 21 | ...super.getCloneOpts(parent), 22 | value: this.value, 23 | }); 24 | } 25 | 26 | toJSON() { 27 | if (typeof this.value === "number") { 28 | return this.value; 29 | } 30 | 31 | return this.value.value.toString() + "i64"; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/syntax-objects/lib/child-list.ts: -------------------------------------------------------------------------------- 1 | import { FastShiftArray } from "../../lib/fast-shift-array.js"; 2 | import { Expr } from "../expr.js"; 3 | import { NamedEntity } from "../named-entity.js"; 4 | 5 | export class ChildList { 6 | private store: FastShiftArray = new FastShiftArray(); 7 | #parent: Expr; 8 | 9 | constructor(children: T[] = [], parent: Expr) { 10 | this.#parent = parent; 11 | this.push(...children); 12 | } 13 | 14 | get parent() { 15 | return this.#parent; 16 | } 17 | 18 | set parent(parent: Expr) { 19 | this.#parent = parent; 20 | this.store.forEach((expr) => { 21 | expr.parent = parent; 22 | }); 23 | } 24 | 25 | get children() { 26 | return this.store.toArray(); 27 | } 28 | 29 | get hasChildren() { 30 | return !!this.store.length; 31 | } 32 | 33 | get length() { 34 | return this.store.length; 35 | } 36 | 37 | private registerExpr(expr: T) { 38 | expr.parent = this.parent; 39 | if (expr instanceof NamedEntity) { 40 | this.parent.registerEntity(expr); 41 | } 42 | } 43 | 44 | at(index: number): T | undefined { 45 | return this.store.at(index); 46 | } 47 | 48 | set(index: number, expr: T) { 49 | this.registerExpr(expr); 50 | this.store.set(index, expr); 51 | return this; 52 | } 53 | 54 | consume(): T { 55 | const next = this.store.shift(); 56 | if (!next) throw new Error("No remaining expressions"); 57 | return next; 58 | } 59 | 60 | /** Returns all but the first element in an array */ 61 | argsArray(): T[] { 62 | return this.store.toArray().slice(1); 63 | } 64 | 65 | pop(): T | undefined { 66 | return this.store.pop(); 67 | } 68 | 69 | push(...expr: T[]) { 70 | expr.forEach((ex) => { 71 | this.registerExpr(ex); 72 | this.store.push(ex); 73 | }); 74 | return this; 75 | } 76 | 77 | insert(expr: T, at = 0) { 78 | this.registerExpr(expr); 79 | this.store.splice(at, 0, expr); 80 | return this; 81 | } 82 | 83 | remove(index: number, count = 1) { 84 | this.store.splice(index, count); 85 | return this; 86 | } 87 | 88 | each(fn: (expr: T, index: number, array: T[]) => void): ChildList { 89 | this.toArray().forEach(fn); 90 | return this; 91 | } 92 | 93 | applyMap(fn: (expr: T, index: number, array: T[]) => T): ChildList { 94 | this.store.forEach((expr, index, array) => { 95 | this.set(index, fn(expr, index, array)); 96 | }); 97 | return this; 98 | } 99 | 100 | slice(start?: number, end?: number): T[] { 101 | return this.store.slice(start, end); 102 | } 103 | 104 | shift(): T | undefined { 105 | return this.store.shift(); 106 | } 107 | 108 | unshift(...expr: T[]) { 109 | expr.forEach((ex) => { 110 | this.registerExpr(ex); 111 | this.store.unshift(ex); 112 | }); 113 | 114 | return this; 115 | } 116 | 117 | reset(to?: T[]) { 118 | this.store = new FastShiftArray(...(to ?? [])); 119 | return this; 120 | } 121 | 122 | toArray(): T[] { 123 | return this.store.toArray(); 124 | } 125 | 126 | clone(): T[] { 127 | return this.toArray().map((expr: T): T => expr.clone() as T); 128 | } 129 | 130 | toJSON() { 131 | return this.toArray(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/syntax-objects/lib/child.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "../expr.js"; 2 | import { NamedEntity } from "../named-entity.js"; 3 | 4 | /** Use to store a child of a syntax object. Will keep track of parent */ 5 | export class Child { 6 | #value: T; 7 | #parent: Expr; 8 | 9 | constructor(value: T, parent: Expr) { 10 | this.#parent = parent; 11 | this.#value = value; 12 | if (this.#value) this.#value.parent = parent; 13 | } 14 | 15 | get parent() { 16 | return this.#parent; 17 | } 18 | 19 | set parent(parent: Expr) { 20 | this.#parent = parent; 21 | if (this.#value) this.#value.parent = parent; 22 | } 23 | 24 | get value() { 25 | return this.#value; 26 | } 27 | 28 | set value(value: T) { 29 | if (value) { 30 | if (value instanceof NamedEntity) this.#parent.registerEntity(value); 31 | value.parent = this.#parent; 32 | } 33 | 34 | this.#value = value; 35 | } 36 | 37 | toJSON() { 38 | return this.#value?.toJSON(); 39 | } 40 | 41 | clone(parent?: Expr): T { 42 | return this.#value?.clone(parent) as T; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/syntax-objects/lib/get-id-str.ts: -------------------------------------------------------------------------------- 1 | import { Id } from "../identifier.js"; 2 | 3 | export const getIdStr = (id: Id) => { 4 | if (!id) { 5 | throw new Error("no id"); 6 | } 7 | return typeof id === "string" ? id : id.value; 8 | }; 9 | -------------------------------------------------------------------------------- /src/syntax-objects/lib/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Nop } from "../nop.js"; 2 | import { Whitespace } from "../whitespace.js"; 3 | 4 | export const newLine = () => new Whitespace({ value: "\n" }); 5 | 6 | let nopCache: Nop | undefined = undefined; 7 | export const nop = () => { 8 | if (!nopCache) { 9 | const n = new Nop({}); 10 | nopCache = n; 11 | return n; 12 | } 13 | 14 | return nopCache; 15 | }; 16 | -------------------------------------------------------------------------------- /src/syntax-objects/lib/lexical-context.ts: -------------------------------------------------------------------------------- 1 | import type { Fn } from "../fn.js"; 2 | import { getIdStr } from "./get-id-str.js"; 3 | import type { Id } from "../identifier.js"; 4 | import { NamedEntity } from "../named-entity.js"; 5 | 6 | export class LexicalContext { 7 | private readonly fns: Map = new Map(); 8 | private readonly fnsById: Map = new Map(); 9 | private readonly entities: Map = new Map(); 10 | 11 | registerEntity(entity: NamedEntity, alias?: string) { 12 | const idStr = alias ?? getIdStr(entity.name); 13 | if (entity.isFn()) { 14 | if (!alias && this.fnsById.get(entity.id)) return; // Already registered 15 | const fns = this.fns.get(idStr) ?? []; 16 | fns.push(entity); 17 | this.fns.set(idStr, fns); 18 | this.fnsById.set(entity.id, entity); 19 | return; 20 | } 21 | 22 | this.entities.set(idStr, entity); 23 | } 24 | 25 | resolveEntity(name: Id): NamedEntity | undefined { 26 | // Intentionally does not check this.fns, those have separate resolution rules i.e. overloading that are handled elsewhere (for now) 27 | const id = getIdStr(name); 28 | return this.entities.get(id); 29 | } 30 | 31 | getAllEntities(): NamedEntity[] { 32 | return [...this.entities.values(), ...this.fnsById.values()]; 33 | } 34 | 35 | resolveFns(name: Id): Fn[] { 36 | const id = getIdStr(name); 37 | return this.fns.get(id) ?? []; 38 | } 39 | 40 | resolveFnById(id: string): Fn | undefined { 41 | return this.fnsById.get(id); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/syntax-objects/macro-lambda.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { Identifier } from "./identifier.js"; 3 | import { List } from "./list.js"; 4 | import { ScopedSyntax, ScopedSyntaxMetadata } from "./scoped-entity.js"; 5 | 6 | export class MacroLambda extends ScopedSyntax { 7 | readonly syntaxType = "macro-lambda"; 8 | readonly parameters: Identifier[] = []; 9 | readonly body: List; 10 | 11 | constructor( 12 | opts: ScopedSyntaxMetadata & { 13 | parameters?: Identifier[]; 14 | body: List; 15 | } 16 | ) { 17 | super(opts); 18 | this.parameters = opts.parameters ?? []; 19 | this.body = opts.body; 20 | this.body.parent = this; 21 | } 22 | 23 | toString() { 24 | return JSON.stringify(this.toJSON()); 25 | } 26 | 27 | clone(parent?: Expr | undefined): MacroLambda { 28 | return new MacroLambda({ 29 | ...super.getCloneOpts(parent), 30 | parameters: this.parameters.map((p) => p.clone()), 31 | body: this.body.clone(), 32 | }); 33 | } 34 | 35 | toJSON() { 36 | return ["macro-lambda", ["parameters", ...this.parameters], this.body]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/syntax-objects/macro-variable.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { NamedEntity, NamedEntityOpts } from "./named-entity.js"; 3 | 4 | export class MacroVariable extends NamedEntity { 5 | readonly isMutable: boolean; 6 | readonly syntaxType = "macro-variable"; 7 | value?: Expr; 8 | 9 | constructor( 10 | opts: NamedEntityOpts & { 11 | isMutable: boolean; 12 | value?: Expr; 13 | } 14 | ) { 15 | super(opts); 16 | this.isMutable = opts.isMutable; 17 | this.value = opts.value; 18 | } 19 | 20 | toString() { 21 | return this.name.toString(); 22 | } 23 | 24 | toJSON() { 25 | return [ 26 | "define-macro-variable", 27 | this.name, 28 | ["reserved-for-type"], 29 | ["is-mutable", this.isMutable], 30 | ]; 31 | } 32 | 33 | clone(parent?: Expr | undefined): MacroVariable { 34 | return new MacroVariable({ 35 | ...super.getCloneOpts(parent), 36 | location: this.location, 37 | isMutable: this.isMutable, 38 | value: this.value?.clone(), 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/syntax-objects/macros.ts: -------------------------------------------------------------------------------- 1 | import { Block } from "./block.js"; 2 | import type { Expr } from "./expr.js"; 3 | import { Identifier } from "./identifier.js"; 4 | import { ScopedNamedEntityOpts, ScopedNamedEntity } from "./named-entity.js"; 5 | 6 | export type Macro = RegularMacro; 7 | 8 | export class RegularMacro extends ScopedNamedEntity { 9 | readonly syntaxType = "macro"; 10 | readonly macroType = "regular"; 11 | readonly parameters: Identifier[] = []; 12 | readonly body: Block; 13 | 14 | constructor( 15 | opts: ScopedNamedEntityOpts & { 16 | parameters?: Identifier[]; 17 | body: Block; 18 | } 19 | ) { 20 | super(opts); 21 | this.parameters = opts.parameters ?? []; 22 | this.body = opts.body; 23 | this.body.parent = this; 24 | } 25 | 26 | evaluate(evaluator: (expr: Expr) => Expr): Expr | undefined { 27 | return this.body.evaluate(evaluator); 28 | } 29 | 30 | getName(): string { 31 | return this.name.value; 32 | } 33 | 34 | toString() { 35 | return this.id; 36 | } 37 | 38 | clone(parent?: Expr | undefined): RegularMacro { 39 | return new RegularMacro({ 40 | ...super.getCloneOpts(parent), 41 | parameters: this.parameters.map((p) => p.clone()), 42 | body: this.body.clone(), 43 | }); 44 | } 45 | 46 | toJSON() { 47 | return [ 48 | "regular-macro", 49 | this.id, 50 | ["parameters", ...this.parameters], 51 | this.body, 52 | ]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/syntax-objects/match.ts: -------------------------------------------------------------------------------- 1 | import { Block } from "./block.js"; 2 | import { Call } from "./call.js"; 3 | import { Expr } from "./expr.js"; 4 | import { Identifier } from "./identifier.js"; 5 | import { LexicalContext } from "./lib/lexical-context.js"; 6 | import { ScopedSyntax } from "./scoped-entity.js"; 7 | import { Syntax, SyntaxMetadata } from "./syntax.js"; 8 | import { ObjectType, Type } from "./types.js"; 9 | import { Variable } from "./variable.js"; 10 | 11 | export type MatchCase = { 12 | /** The type to match the base type against */ 13 | matchType?: ObjectType; 14 | matchTypeExpr?: Expr; 15 | expr: Block | Call; 16 | }; 17 | 18 | export type MatchOpts = SyntaxMetadata & { 19 | operand: Expr; 20 | cases: MatchCase[]; 21 | defaultCase?: MatchCase; 22 | type?: Type; 23 | baseType?: Type; 24 | bindVariable?: Variable; 25 | bindIdentifier: Identifier; 26 | }; 27 | 28 | export class Match extends Syntax implements ScopedSyntax { 29 | readonly syntaxType = "match"; 30 | lexicon = new LexicalContext(); 31 | /** Match expr return type */ 32 | type?: Type; 33 | /** Type being matched against */ 34 | baseType?: Type; 35 | operand: Expr; 36 | /** A variable to bind the operand to when needed */ 37 | bindVariable?: Variable; 38 | cases: MatchCase[]; 39 | defaultCase?: MatchCase; 40 | bindIdentifier: Identifier; 41 | 42 | constructor(opts: MatchOpts) { 43 | super(opts); 44 | this.operand = opts.operand; 45 | this.operand.parent = this; 46 | this.cases = opts.cases.map((c) => { 47 | if (c.matchTypeExpr) { 48 | c.matchTypeExpr.parent = this; 49 | } 50 | c.expr.parent = this; 51 | return c; 52 | }); 53 | 54 | this.defaultCase = opts.defaultCase; 55 | if (this.defaultCase) { 56 | this.defaultCase.expr.parent = this; 57 | } 58 | 59 | this.type = opts.type; 60 | this.baseType = opts.baseType; 61 | this.bindIdentifier = opts.bindIdentifier; 62 | this.bindIdentifier.parent = this; 63 | 64 | if (opts.bindVariable) { 65 | opts.bindVariable.parent = this; 66 | this.registerEntity(opts.bindVariable); 67 | this.bindVariable = opts.bindVariable; 68 | } 69 | } 70 | 71 | toJSON(): object { 72 | return ["match", this.operand.toJSON(), ...this.cases, this.defaultCase]; 73 | } 74 | 75 | clone(parent?: Expr): Match { 76 | return new Match({ 77 | ...this.getCloneOpts(parent), 78 | operand: this.operand.clone(), 79 | cases: this.cases.map((c) => ({ 80 | expr: c.expr.clone(), 81 | matchTypeExpr: c.matchTypeExpr?.clone(), 82 | matchType: undefined, 83 | })), 84 | defaultCase: this.defaultCase 85 | ? { 86 | expr: this.defaultCase.expr.clone(), 87 | matchTypeExpr: this.defaultCase.matchTypeExpr?.clone(), 88 | matchType: undefined, 89 | } 90 | : undefined, 91 | type: this.type, 92 | bindVariable: this.bindVariable?.clone(), 93 | bindIdentifier: this.bindIdentifier.clone(), 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/syntax-objects/module.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { Id } from "./identifier.js"; 3 | import { ChildList } from "./lib/child-list.js"; 4 | import { LexicalContext } from "./lib/lexical-context.js"; 5 | import { 6 | NamedEntity, 7 | ScopedNamedEntity, 8 | ScopedNamedEntityOpts, 9 | } from "./named-entity.js"; 10 | 11 | export type VoydModuleOpts = ScopedNamedEntityOpts & { 12 | value?: Expr[]; 13 | phase?: number; 14 | isIndex?: boolean; 15 | exports?: LexicalContext; 16 | }; 17 | 18 | export class VoydModule extends ScopedNamedEntity { 19 | readonly syntaxType = "module"; 20 | readonly exports: LexicalContext; 21 | readonly isRoot: boolean = false; 22 | /** This module is the entry point of the user src code */ 23 | isIndex = false; 24 | #value = new ChildList(undefined, this); 25 | /** 26 | * 0 = init, 27 | * 1 = expanding regular macros, 28 | * 2 = regular macros expanded, 29 | * 3 = checking types, 30 | * 4 = types checked 31 | */ 32 | phase = 0; 33 | 34 | constructor(opts: VoydModuleOpts) { 35 | super(opts); 36 | if (opts.value) this.push(...opts.value); 37 | this.exports = opts.exports ?? new LexicalContext(); 38 | this.phase = opts.phase ?? 0; 39 | this.isIndex = opts.isIndex ?? false; 40 | } 41 | 42 | get value() { 43 | return this.#value.toArray(); 44 | } 45 | 46 | set value(value: Expr[]) { 47 | this.#value = new ChildList(undefined, this); 48 | this.push(...value); 49 | } 50 | 51 | registerExport(entity: NamedEntity, alias?: string) { 52 | this.exports.registerEntity(entity, alias); 53 | } 54 | 55 | resolveExport(name: Id): NamedEntity[] { 56 | const start: NamedEntity[] = this.exports.resolveFns(name); 57 | const entity = this.exports.resolveEntity(name); 58 | if (entity) start.push(entity); 59 | return start; 60 | } 61 | 62 | getAllExports(): NamedEntity[] { 63 | return this.exports.getAllEntities(); 64 | } 65 | 66 | getPath(): string[] { 67 | const path = this.parentModule?.getPath() ?? []; 68 | return [...path, this.name.toString()]; 69 | } 70 | 71 | each(fn: (expr: Expr, index: number, array: Expr[]) => void): VoydModule { 72 | this.value.forEach(fn); 73 | return this; 74 | } 75 | 76 | map(fn: (expr: Expr, index: number, array: Expr[]) => Expr): VoydModule { 77 | return new VoydModule({ 78 | ...super.getCloneOpts(), 79 | value: this.value.map(fn), 80 | phase: this.phase, 81 | isIndex: this.isIndex, 82 | exports: this.exports, 83 | }); 84 | } 85 | 86 | applyMap(fn: (expr: Expr, index: number, array: Expr[]) => Expr): VoydModule { 87 | const old = this.value; 88 | this.value = []; 89 | old.forEach((expr, index, arr) => this.push(fn(expr, index, arr))); 90 | return this; 91 | } 92 | 93 | toString() { 94 | return this.id; 95 | } 96 | 97 | clone(parent?: Expr | undefined): VoydModule { 98 | return new VoydModule({ 99 | ...super.getCloneOpts(parent), 100 | value: this.value.map((expr) => expr.clone()), 101 | phase: this.phase, 102 | }); 103 | } 104 | 105 | toJSON() { 106 | return [ 107 | "module", 108 | this.name, 109 | ["exports", this.exports.getAllEntities().map((e) => e.id)], 110 | this.value, 111 | ]; 112 | } 113 | 114 | push(...expr: Expr[]) { 115 | this.#value.push(...expr); 116 | return this; 117 | } 118 | 119 | unshift(...expr: Expr[]) { 120 | this.#value.unshift(...expr); 121 | return this; 122 | } 123 | } 124 | 125 | export class RootModule extends VoydModule { 126 | readonly isRoot = true; 127 | 128 | constructor(opts: Omit) { 129 | super({ ...opts, name: "root" }); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/syntax-objects/named-entity.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { Id, Identifier } from "./identifier.js"; 3 | import { LexicalContext } from "./lib/lexical-context.js"; 4 | import { Syntax, SyntaxMetadata } from "./syntax.js"; 5 | 6 | export type NamedEntityOpts = SyntaxMetadata & { 7 | name: Id; 8 | id?: string; 9 | idNum?: number; 10 | isExported?: boolean; 11 | }; 12 | 13 | export abstract class NamedEntity extends Syntax { 14 | id: string; 15 | idNum: number; 16 | name: Identifier; 17 | 18 | constructor(opts: NamedEntityOpts) { 19 | super(opts); 20 | this.name = 21 | typeof opts.name === "string" ? Identifier.from(opts.name) : opts.name; 22 | this.id = opts.id ?? this.genId(); 23 | this.idNum = this.syntaxId; 24 | } 25 | 26 | private genId() { 27 | return `${this.name}#${this.syntaxId}`; 28 | } 29 | 30 | getCloneOpts(parent?: Expr): NamedEntityOpts { 31 | return { 32 | ...super.getCloneOpts(parent), 33 | id: this.id, 34 | idNum: this.idNum, 35 | name: this.name, 36 | }; 37 | } 38 | 39 | setName(name: string) { 40 | this.name = Identifier.from(name); 41 | this.id = this.genId(); 42 | } 43 | } 44 | 45 | export type ScopedNamedEntityOpts = NamedEntityOpts & { 46 | lexicon?: LexicalContext; 47 | }; 48 | 49 | export abstract class ScopedNamedEntity extends NamedEntity { 50 | readonly lexicon: LexicalContext; 51 | 52 | constructor(opts: ScopedNamedEntityOpts) { 53 | super(opts); 54 | this.lexicon = opts.lexicon ?? new LexicalContext(); 55 | } 56 | 57 | getCloneOpts(parent?: Expr): ScopedNamedEntityOpts { 58 | return { 59 | ...super.getCloneOpts(parent), 60 | // lexicon: this.lexicon, 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/syntax-objects/nop.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { Syntax, SyntaxMetadata } from "./syntax.js"; 3 | 4 | export class Nop extends Syntax { 5 | readonly syntaxType = "nop"; 6 | 7 | constructor(opts: SyntaxMetadata) { 8 | super(opts); 9 | } 10 | 11 | clone(parent?: Expr): Nop { 12 | return this; 13 | } 14 | 15 | toJSON() { 16 | return "nop"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/syntax-objects/object-literal.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { Syntax, SyntaxMetadata } from "./syntax.js"; 3 | import { ObjectType, Type } from "./types.js"; 4 | 5 | export class ObjectLiteral extends Syntax { 6 | readonly syntaxType = "object-literal"; 7 | type?: ObjectType; 8 | fields: ObjectLiteralField[]; 9 | 10 | constructor(opts: SyntaxMetadata & { fields: ObjectLiteralField[] }) { 11 | super(opts); 12 | this.fields = opts.fields; 13 | this.fields.forEach((f) => (f.initializer.parent = this)); 14 | } 15 | 16 | clone(parent?: Expr): ObjectLiteral { 17 | return new ObjectLiteral({ 18 | ...super.getCloneOpts(parent), 19 | fields: this.fields.map(({ name, initializer }) => ({ 20 | name, 21 | initializer: initializer.clone(), 22 | })), 23 | }); 24 | } 25 | 26 | toJSON(): object { 27 | return [ 28 | "object", 29 | `ObjectLiteral-${this.syntaxId}`, 30 | this.fields.map((f) => [f.name, f.initializer.toJSON()]), 31 | ]; 32 | } 33 | } 34 | 35 | export type ObjectLiteralField = { 36 | name: string; 37 | initializer: Expr; 38 | type?: Type; 39 | }; 40 | -------------------------------------------------------------------------------- /src/syntax-objects/parameter.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { Identifier } from "./identifier.js"; 3 | import { NamedEntity, NamedEntityOpts } from "./named-entity.js"; 4 | import { Type } from "./types.js"; 5 | 6 | export class Parameter extends NamedEntity { 7 | readonly syntaxType = "parameter"; 8 | /** External label the parameter must be called with e.g. myFunc(label: value) */ 9 | label?: Identifier; 10 | originalType?: Type; 11 | type?: Type; 12 | typeExpr?: Expr; 13 | requiresCast = false; 14 | 15 | constructor( 16 | opts: NamedEntityOpts & { 17 | label?: Identifier; 18 | type?: Type; 19 | typeExpr?: Expr; 20 | } 21 | ) { 22 | super(opts); 23 | this.label = opts.label; 24 | this.type = opts.type; 25 | this.typeExpr = opts.typeExpr; 26 | if (this.typeExpr) { 27 | this.typeExpr.parent = this; 28 | } 29 | } 30 | 31 | getIndex(): number { 32 | const index = this.parentFn?.getIndexOfParameter(this) ?? -1; 33 | if (index < -1) { 34 | throw new Error(`Parameter ${this} is not registered with a function`); 35 | } 36 | return index; 37 | } 38 | 39 | toString() { 40 | return this.name.toString(); 41 | } 42 | 43 | clone(parent?: Expr | undefined): Parameter { 44 | return new Parameter({ 45 | ...super.getCloneOpts(parent), 46 | label: this.label, 47 | typeExpr: this.typeExpr?.clone(), 48 | }); 49 | } 50 | 51 | toJSON() { 52 | return ["define-parameter", this.name, ["label", this.label], this.type]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/syntax-objects/scoped-entity.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { LexicalContext } from "./lib/lexical-context.js"; 3 | import { Syntax, SyntaxMetadata } from "./syntax.js"; 4 | 5 | export type ScopedEntity = Syntax & { 6 | lexicon: LexicalContext; 7 | }; 8 | 9 | export type ScopedSyntaxMetadata = SyntaxMetadata & { 10 | lexicon?: LexicalContext; 11 | }; 12 | 13 | export abstract class ScopedSyntax extends Syntax { 14 | readonly lexicon: LexicalContext; 15 | 16 | constructor(opts: ScopedSyntaxMetadata) { 17 | super(opts); 18 | this.lexicon = opts.lexicon ?? new LexicalContext(); 19 | } 20 | 21 | getCloneOpts(parent?: Expr | undefined): ScopedSyntaxMetadata { 22 | return { 23 | ...super.getCloneOpts(parent), 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/syntax-objects/string-literal.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { Syntax, SyntaxMetadata } from "./syntax.js"; 3 | 4 | export class StringLiteral extends Syntax { 5 | readonly syntaxType = "string-literal"; 6 | value: string; 7 | 8 | constructor(opts: SyntaxMetadata & { value: string }) { 9 | super(opts); 10 | this.value = opts.value; 11 | } 12 | 13 | clone(parent?: Expr): StringLiteral { 14 | return new StringLiteral({ 15 | ...super.getCloneOpts(parent), 16 | value: this.value, 17 | }); 18 | } 19 | 20 | toJSON() { 21 | return ["string", this.value]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/syntax-objects/trait.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { Fn } from "./fn.js"; 3 | import { ChildList } from "./lib/child-list.js"; 4 | import { ScopedNamedEntity, ScopedNamedEntityOpts } from "./named-entity.js"; 5 | 6 | export type TraitOpts = ScopedNamedEntityOpts & { 7 | methods: Fn[]; 8 | }; 9 | 10 | export class Trait extends ScopedNamedEntity { 11 | readonly syntaxType = "trait"; 12 | readonly methods: ChildList; 13 | 14 | constructor(opts: TraitOpts) { 15 | super(opts); 16 | this.methods = new ChildList(opts.methods, this); 17 | } 18 | 19 | clone(parent?: Expr): Expr { 20 | return new Trait({ 21 | ...super.getCloneOpts(parent), 22 | methods: this.methods.clone(), 23 | }); 24 | } 25 | 26 | toJSON(): unknown { 27 | return ["trait", this.name, ["methods", this.methods.toJSON()]]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/syntax-objects/use.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { Identifier } from "./identifier.js"; 3 | import { List } from "./list.js"; 4 | import { NamedEntity } from "./named-entity.js"; 5 | import { Syntax, SyntaxMetadata } from "./syntax.js"; 6 | 7 | export type UseEntities = { e: NamedEntity; alias?: string }[]; 8 | 9 | /** Defines a declared namespace for external function imports */ 10 | export class Use extends Syntax { 11 | readonly syntaxType = "use"; 12 | entities: UseEntities; 13 | path: List | Identifier; 14 | 15 | constructor( 16 | opts: SyntaxMetadata & { 17 | entities: UseEntities; 18 | path: List | Identifier; 19 | } 20 | ) { 21 | super(opts); 22 | this.entities = opts.entities; 23 | this.path = opts.path; 24 | } 25 | 26 | toJSON() { 27 | return ["use", this.path.toJSON()]; 28 | } 29 | 30 | clone(parent?: Expr) { 31 | return new Use({ 32 | ...this.getCloneOpts(parent), 33 | entities: this.entities, 34 | path: this.path, 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/syntax-objects/variable.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { Child } from "./lib/child.js"; 3 | import { NamedEntity, NamedEntityOpts } from "./named-entity.js"; 4 | import { Type } from "./types.js"; 5 | 6 | export class Variable extends NamedEntity { 7 | readonly syntaxType = "variable"; 8 | isMutable: boolean; 9 | type?: Type; 10 | /** Set before the type was narrowed by the type checker */ 11 | originalType?: Type; 12 | inferredType?: Type; 13 | annotatedType?: Type; 14 | #typeExpr = new Child(undefined, this); 15 | #initializer: Child; 16 | requiresCast = false; 17 | 18 | constructor( 19 | opts: NamedEntityOpts & { 20 | isMutable: boolean; 21 | initializer: Expr; 22 | type?: Type; 23 | typeExpr?: Expr; 24 | } 25 | ) { 26 | super(opts); 27 | this.isMutable = opts.isMutable; 28 | this.type = opts.type; 29 | this.typeExpr = opts.typeExpr; 30 | this.#initializer = new Child(opts.initializer, this); 31 | } 32 | 33 | get typeExpr(): Expr | undefined { 34 | return this.#typeExpr.value; 35 | } 36 | 37 | set typeExpr(value: Expr | undefined) { 38 | this.#typeExpr.value = value; 39 | } 40 | 41 | get initializer(): Expr { 42 | return this.#initializer.value; 43 | } 44 | 45 | set initializer(value: Expr) { 46 | this.#initializer.value = value; 47 | } 48 | 49 | getIndex(): number { 50 | const index = this.parentFn?.getIndexOfVariable(this) ?? -1; 51 | 52 | if (index < 0) { 53 | throw new Error(`Variable ${this} is not registered with a function`); 54 | } 55 | 56 | return index; 57 | } 58 | 59 | toString() { 60 | return this.name.toString(); 61 | } 62 | 63 | toJSON() { 64 | return [ 65 | "define-variable", 66 | this.name, 67 | this.type, 68 | ["is-mutable", this.isMutable], 69 | this.initializer, 70 | ]; 71 | } 72 | 73 | clone(parent?: Expr | undefined): Variable { 74 | return new Variable({ 75 | ...super.getCloneOpts(parent), 76 | isMutable: this.isMutable, 77 | initializer: this.#initializer.clone(), 78 | typeExpr: this.#typeExpr.clone(), 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/syntax-objects/whitespace.ts: -------------------------------------------------------------------------------- 1 | import { Expr } from "./expr.js"; 2 | import { Syntax, SyntaxMetadata } from "./syntax.js"; 3 | 4 | export class Whitespace extends Syntax { 5 | readonly syntaxType = "whitespace"; 6 | value: string; 7 | 8 | constructor(opts: SyntaxMetadata & { value: string }) { 9 | super(opts); 10 | this.value = opts.value; 11 | } 12 | 13 | get isNewline() { 14 | return this.value === "\n"; 15 | } 16 | 17 | get isSpace() { 18 | return !this.isNewline && !this.isIndent; 19 | } 20 | 21 | get isIndent() { 22 | return this.value === " "; 23 | } 24 | 25 | clone(parent?: Expr): Whitespace { 26 | return new Whitespace({ ...super.getCloneOpts(parent), value: this.value }); 27 | } 28 | 29 | toJSON() { 30 | return this.value; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /std/fixed_array.voyd: -------------------------------------------------------------------------------- 1 | use std::macros::all 2 | 3 | pub fn new_fixed_array(size: i32) -> FixedArray 4 | binaryen_gc_call( 5 | arrayNew, 6 | `(bin_type_to_heap_type(FixedArray), size), 7 | FixedArray 8 | ) 9 | 10 | pub fn get(arr: FixedArray, index: i32) -> T 11 | binaryen_gc_call( 12 | arrayGet, 13 | `(arr, index, BnrType, BnrConst(false)), 14 | T 15 | ) 16 | 17 | pub fn set(arr: FixedArray, index: i32, value: T) -> FixedArray 18 | binaryen_gc_call(arraySet, `(arr, index, value), FixedArray) 19 | arr 20 | 21 | pub fn copy(dest_array: FixedArray, opts: { 22 | from: FixedArray, 23 | to_index: i32, 24 | from_index: i32, 25 | count: i32 26 | }) -> FixedArray 27 | binaryen_gc_call(arrayCopy, `( 28 | dest_array, 29 | opts.to_index, 30 | opts.from, 31 | opts.from_index, 32 | opts.count 33 | ), FixedArray) 34 | dest_array 35 | 36 | pub fn length(arr: FixedArray) -> i32 37 | binaryen_gc_call(arrayLen, `(arr), i32) 38 | -------------------------------------------------------------------------------- /std/index.voyd: -------------------------------------------------------------------------------- 1 | use macros::all 2 | pub mod macros 3 | pub mod macros::all 4 | pub mod array::all 5 | pub mod operators::all 6 | pub mod optional::all 7 | pub mod utils::all 8 | pub mod string_lib::all 9 | pub mod fixed_array::all 10 | pub mod string_lib 11 | pub mod map::all 12 | -------------------------------------------------------------------------------- /std/macros.voyd: -------------------------------------------------------------------------------- 1 | export 2 | macro pub() 3 | // handle syntax like "pub fn me" and "pub fn(me)" 4 | if body.length == 1 and body.extract(0).is_list() then: 5 | quote export block($@body) 6 | else: 7 | quote export block($body) 8 | 9 | pub macro `() 10 | quote quote $@body 11 | 12 | pub macro let() 13 | define equals_expr body.extract(0) 14 | ` define $(equals_expr.extract(1)) $(equals_expr.extract(2)) 15 | 16 | pub macro var() 17 | define equals_expr body.extract(0) 18 | ` define_mut $(equals_expr.extract(1)) $(equals_expr.extract(2)) 19 | 20 | pub macro global() 21 | let mutability = body.extract(0) 22 | let equals_expr = body.extract(1) 23 | let function = 24 | if mutability == "let" then: 25 | ` define_global 26 | else: 27 | ` define_mut_global 28 | `($@function, 29 | $(equals_expr.extract(1)), 30 | $(equals_expr.extract(2))) 31 | 32 | pub macro ';'() 33 | let func = body.extract(0) 34 | let block = body.extract(1) 35 | let args = 36 | if block.extract(0) == "block" then: 37 | block.slice(1) 38 | else: 39 | block 40 | if is_list(func) then: 41 | func.concat(args) 42 | else: 43 | `($func).concat(args) 44 | 45 | // Extracts typed parameters from a list where index 0 is fn name, and offset_index+ are labeled_expr 46 | macro_let extract_parameters = (definitions) => 47 | `(parameters).concat definitions.slice(1) 48 | 49 | pub macro fn() 50 | let definitions = body.extract(0) 51 | let identifier = definitions.extract(0) 52 | let params = extract_parameters(definitions) 53 | 54 | let type_arrow_index = 55 | if body.extract(1) == "->" then: 56 | 1 57 | else: 58 | if body.extract(2) == "->" then: 2 else: -1 59 | 60 | let return_type = 61 | if type_arrow_index > -1 then: 62 | body.slice(type_arrow_index + 1, type_arrow_index + 2) 63 | else: `() 64 | 65 | let expressions = 66 | if type_arrow_index > -1 then: 67 | body.slice(type_arrow_index + 2) 68 | else: body.slice(1) 69 | 70 | `( 71 | define_function, 72 | $identifier, 73 | $params, 74 | (return_type $@return_type), 75 | $@expressions 76 | ) 77 | 78 | pub macro binaryen_gc_call(func, args, return_type) 79 | ` binaryen func: $func namespace: gc args: $args return_type: $return_type 80 | 81 | pub macro bin_type_to_heap_type(type) 82 | ` binaryen 83 | func: modBinaryenTypeToHeapType 84 | namespace: gc 85 | args: `(BnrType<($type)>) 86 | -------------------------------------------------------------------------------- /std/map.voyd: -------------------------------------------------------------------------------- 1 | use macros::all 2 | use array::all 3 | use operators::all 4 | use optional::all 5 | use string_lib::all 6 | 7 | obj Map { 8 | buckets: Array> 9 | } 10 | 11 | pub fn new_map() -> Map 12 | let buckets = new_array>({ with_size: 16 }) 13 | 14 | var i = 0 15 | while i < 16 do: 16 | buckets.push(new_array<{ key: string, value: T }>({ with_size: 4 })) 17 | i = i + 1 18 | 19 | Map { buckets: buckets } 20 | 21 | impl Map 22 | fn hash(self, key: string) -> i32 23 | var hash_value = 0 24 | var i = 0 25 | while i < key.length do: 26 | hash_value = (hash_value * 31 + key.char_code_at(i)) % self.buckets.length 27 | i = i + 1 28 | hash_value 29 | 30 | fn get_bucket_by_key(self, key: string) -> Optional> 31 | let index = self.hash(key) 32 | self.buckets.get(index) 33 | 34 | fn get_index_of_item_in_bucket(bucket: Array<{ key: string, value: T }>, key: string) -> i32 35 | var i = 0 36 | var index = -1 37 | while i < bucket.length do: 38 | bucket.get(i).match(v) 39 | Some<{ key: string, value: T }>: 40 | if v.value.key == key then: 41 | index = i 42 | break 43 | 0 44 | None: 45 | 0 46 | i = i + 1 47 | index 48 | 49 | pub fn set(self, key: string, value: T) -> Map 50 | self.get_bucket_by_key(key).match(bucket) 51 | Some>: 52 | add_to_bucket(bucket.value, key, value) 53 | self 54 | None: 55 | let new_bucket = new_array<{ key: string, value: T }>({ with_size: 1 }) 56 | new_bucket.set(0, { key: key, value: value }) 57 | let index = self.hash(key) 58 | self.buckets.set(index, new_bucket) 59 | self 60 | 61 | fn add_to_bucket( 62 | bucket: Array<{ key: string, value: T }>, 63 | key: string, 64 | value: T 65 | ) 66 | let index = get_index_of_item_in_bucket(bucket, key) 67 | 68 | if index < 0 then: 69 | bucket.push({ key: key, value: value }) 70 | else: 71 | bucket.set(index, { key: key, value: value }) 72 | 73 | pub fn get(self, key: string) -> Optional 74 | self.get_bucket_by_key(key).match(bucket) 75 | Some>: 76 | let index = get_index_of_item_in_bucket(bucket.value, key) 77 | if index < 0 then: 78 | None {} 79 | else: 80 | bucket.value.get(index).match(v) 81 | Some<{ key: string, value: T }>: 82 | Some { value: v.value.value } 83 | None: 84 | None {} 85 | None: None {} 86 | 87 | pub fn delete(self, key: string) -> Map 88 | self.get_bucket_by_key(key).match(bucket) 89 | Some>: 90 | let index = get_index_of_item_in_bucket(bucket.value, key) 91 | if index < 0 then: 92 | self 93 | else: 94 | bucket.value.remove(index) 95 | self 96 | None: self 97 | 98 | pub fn has(self, key: string) -> bool 99 | self.get_bucket_by_key(key).match(bucket) 100 | Some>: 101 | get_index_of_item_in_bucket(bucket.value, key) >= 0 102 | None: false 103 | -------------------------------------------------------------------------------- /std/optional.voyd: -------------------------------------------------------------------------------- 1 | use macros::all 2 | use operators::all 3 | 4 | pub obj Some { 5 | value: T 6 | } 7 | 8 | pub obj None {} 9 | 10 | pub type Optional = Some | None 11 | pub type Option = Optional 12 | 13 | // Todo equitable trait constraint 14 | pub fn equals(a: Option, b: Option) -> bool 15 | match(a) 16 | Some: 17 | match(b) 18 | Some: 19 | a.value == b.value 20 | None: 21 | false 22 | None: 23 | match(b) 24 | Some: 25 | false 26 | None: 27 | true 28 | -------------------------------------------------------------------------------- /std/string_lib.voyd: -------------------------------------------------------------------------------- 1 | use macros::all 2 | use fixed_array::all 3 | use operators::all 4 | 5 | fn create_string(size: i32) -> string 6 | binaryen_gc_call(arrayNew, `(bin_type_to_heap_type(string), size), string) 7 | 8 | fn copy_str(dest_str: string, opts: { 9 | from: string, 10 | to_index: i32, 11 | from_index: i32, 12 | count: i32 13 | }) -> string 14 | binaryen_gc_call(arrayCopy, `( 15 | dest_str, 16 | opts.to_index, 17 | opts.from, 18 | opts.from_index, 19 | opts.count 20 | ), string) 21 | dest_str 22 | 23 | pub fn length(str: string) -> i32 24 | binaryen_gc_call(arrayLen, `(str), i32) 25 | 26 | fn compute_index(index: i32, length: i32) -> i32 27 | if index < 0 then: length + index else: index 28 | 29 | pub fn slice(str: string, start: i32, end: i32) -> string 30 | let computed_start = compute_index(start, str.length) 31 | let computed_end = compute_index(end, str.length) 32 | 33 | if (computed_start >= str.length) or (computed_end >= str.length) or (computed_start >= computed_end) then: 34 | "" 35 | else: 36 | let new_length = computed_end - computed_start 37 | let new_chars = create_string(new_length) 38 | new_chars.copy_str({ 39 | from: str, 40 | to_index: 0, 41 | from_index: start, 42 | count: new_length 43 | }) 44 | 45 | pub fn char_code_at(str: string, index: i32) -> i32 46 | let computed_index = compute_index(index, str.length) 47 | 48 | if computed_index >= str.length then: 49 | -1 50 | else: 51 | binaryen_gc_call( 52 | arrayGet, 53 | `(str, computed_index, BnrType, BnrConst(false)), 54 | i32 55 | ) 56 | 57 | pub fn '+'(str: string, other: string) -> string 58 | let new_length = str.length + other.length 59 | let new_string = create_string(new_length) 60 | new_string.copy_str({ 61 | from: str, 62 | to_index: 0, 63 | from_index: 0, 64 | count: str.length 65 | }) 66 | new_string.copy_str({ 67 | from: other, 68 | to_index: str.length, 69 | from_index: 0, 70 | count: other.length 71 | }) 72 | 73 | obj StringIterator { 74 | str: string, 75 | index: i32 76 | } 77 | 78 | pub fn new_string_iterator(str: string) -> StringIterator 79 | StringIterator { str: str, index: 0 } 80 | 81 | pub fn read_next_char(iterator: StringIterator) -> i32 82 | if iterator.index >= iterator.str.length then: 83 | -1 84 | else: 85 | let char = iterator.str.char_code_at(iterator.index) 86 | iterator.index = iterator.index + 1 87 | char 88 | 89 | pub fn '=='(a: string, b: string) -> bool 90 | if a.length != b.length then: 91 | false 92 | else: 93 | var i = 0 94 | var result = true 95 | while i < a.length do: 96 | if a.char_code_at(i) != b.char_code_at(i) then: 97 | result = false 98 | break 99 | i = i + 1 100 | result 101 | -------------------------------------------------------------------------------- /std/utils.voyd: -------------------------------------------------------------------------------- 1 | use std::macros::all 2 | 3 | declare 'utils' 4 | pub fn log(val:i32) -> void 5 | pub fn log(val:f32) -> void 6 | pub fn log(val:f64) -> void 7 | pub fn log(val:i64) -> void 8 | --------------------------------------------------------------------------------