├── .gitignore ├── crates ├── ast │ ├── .gitignore │ ├── src │ │ ├── lib.rs │ │ ├── expr.rs │ │ └── stmt.rs │ └── Cargo.toml ├── interpreter │ ├── tests │ │ ├── tests_sources │ │ │ ├── empty_fn.th │ │ │ ├── while_loop.th │ │ │ ├── add_fns.th │ │ │ ├── assign_index.th │ │ │ ├── index_object.th │ │ │ ├── stack.th │ │ │ ├── timing.th │ │ │ ├── break_continue.th │ │ │ ├── queue.th │ │ │ ├── cyclic_arrays.th │ │ │ ├── fib.th │ │ │ └── primes.th │ │ └── run_tests.rs │ ├── Cargo.toml │ ├── src │ │ ├── lib.rs │ │ ├── error.rs │ │ ├── callable.rs │ │ ├── stack.rs │ │ ├── stdlib.rs │ │ ├── value.rs │ │ └── context.rs │ ├── gen_demo_urls.js │ └── README.md ├── Cargo.toml ├── parser │ ├── src │ │ ├── lex │ │ │ ├── mod.rs │ │ │ ├── error.rs │ │ │ ├── token.rs │ │ │ └── lexers.rs │ │ ├── parse │ │ │ ├── mod.rs │ │ │ ├── common_parsers.rs │ │ │ ├── tokens_ext.rs │ │ │ ├── error.rs │ │ │ ├── expr_parsers.rs │ │ │ └── stmt_parsers.rs │ │ └── lib.rs │ ├── Cargo.toml │ └── README.md ├── cli │ ├── Cargo.toml │ └── src │ │ └── main.rs └── wasm │ ├── Cargo.toml │ └── src │ ├── println.rs │ └── lib.rs ├── .firebaserc ├── packages └── demo │ ├── src │ ├── vite-env.d.ts │ ├── main.ts │ ├── app.css │ ├── App.svelte │ └── demoItems.ts │ ├── public │ ├── logo.png │ ├── github-mark.svg │ └── logo.svg │ ├── postcss.config.js │ ├── tsconfig.node.json │ ├── svelte.config.js │ ├── .gitignore │ ├── vite.config.ts │ ├── .eslintrc.cjs │ ├── index.html │ ├── tsconfig.json │ ├── tailwind.config.js │ └── package.json ├── test.sh ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── rustfmt.toml ├── firebase.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .firebase 4 | -------------------------------------------------------------------------------- /crates/ast/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /crates/interpreter/tests/tests_sources/empty_fn.th: -------------------------------------------------------------------------------- 1 | fn add(a, b){ 2 | } 3 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "thrax-language-demo" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /crates/interpreter/tests/tests_sources/while_loop.th: -------------------------------------------------------------------------------- 1 | while (false){ 2 | let a = 1 + 1; 3 | } 4 | -------------------------------------------------------------------------------- /packages/demo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | cd ./crates 6 | 7 | cargo test 8 | cargo test --release 9 | -------------------------------------------------------------------------------- /crates/interpreter/tests/tests_sources/add_fns.th: -------------------------------------------------------------------------------- 1 | fn test(n){ 2 | return n; 3 | } 4 | 5 | test(1 + 2) + test(2); 6 | -------------------------------------------------------------------------------- /packages/demo/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elijah-potter/thrax-language/HEAD/packages/demo/public/logo.png -------------------------------------------------------------------------------- /crates/ast/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod expr; 2 | mod stmt; 3 | 4 | pub use expr::*; 5 | pub use stmt::*; 6 | 7 | pub type Program = Vec; 8 | -------------------------------------------------------------------------------- /packages/demo/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /crates/ast/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ast" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | is-macro = "0.2.1" 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /crates/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "ast", 4 | "cli", 5 | "interpreter", 6 | "parser", 7 | "wasm", 8 | ] 9 | resolver = "2" 10 | -------------------------------------------------------------------------------- /crates/interpreter/tests/tests_sources/assign_index.th: -------------------------------------------------------------------------------- 1 | let arr = [1, 2, 3]; 2 | 3 | arr; 4 | arr[2]; 5 | 6 | arr[2] = 23; 7 | arr[2] += 20; 8 | 9 | return arr; 10 | -------------------------------------------------------------------------------- /packages/demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from "./App.svelte"; 2 | 3 | const app = new App({ 4 | target: document.getElementById("app"), 5 | }); 6 | 7 | export default app; 8 | -------------------------------------------------------------------------------- /packages/demo/src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body, 7 | #app { 8 | width: 100%; 9 | height: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /crates/parser/src/lex/mod.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod lexers; 3 | mod token; 4 | 5 | pub use error::Error; 6 | pub use lexers::lex_to_end; 7 | pub use token::{ShallowTokenKind, Token, TokenKind}; 8 | -------------------------------------------------------------------------------- /crates/interpreter/tests/tests_sources/index_object.th: -------------------------------------------------------------------------------- 1 | let k = { 2 | t: 23, 3 | sdf: "testing", 4 | rec: { 5 | a: "inner" 6 | } 7 | }; 8 | 9 | k["t"]; 10 | k["sdf"]; 11 | k["rec"]; 12 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_impl_items = true 2 | reorder_imports = true 3 | group_imports = "StdExternalCrate" 4 | imports_granularity = "Module" 5 | use_field_init_shorthand = true 6 | normalize_comments = true 7 | 8 | -------------------------------------------------------------------------------- /crates/parser/src/parse/mod.rs: -------------------------------------------------------------------------------- 1 | mod common_parsers; 2 | mod error; 3 | mod expr_parsers; 4 | mod stmt_parsers; 5 | mod tokens_ext; 6 | 7 | pub use error::{Error, ErrorKind}; 8 | pub use stmt_parsers::parse_stmt_list; 9 | -------------------------------------------------------------------------------- /packages/demo/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler" 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /crates/interpreter/tests/tests_sources/stack.th: -------------------------------------------------------------------------------- 1 | let stack = [1, 3, 2, 6, 23]; 2 | 3 | // We can push items to the end of the array 4 | push(stack, "new item"); 5 | 6 | // We can pop items off the end 7 | pop(stack); 8 | 9 | return pop(stack); 10 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - uses: dtolnay/rust-toolchain@stable 10 | - run: $PWD/test.sh 11 | -------------------------------------------------------------------------------- /packages/demo/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | }; 8 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "packages/demo/dist", 4 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 5 | "rewrites": [ 6 | { 7 | "source": "**", 8 | "destination": "/index.html" 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /crates/parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "parser" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | is-macro = "0.2.1" 8 | ast = { path = "../ast" } 9 | thiserror = "1.0.37" 10 | paste = "1.0.9" 11 | 12 | [dev-dependencies] 13 | interpreter = { path = "../interpreter" } 14 | -------------------------------------------------------------------------------- /crates/interpreter/tests/tests_sources/timing.th: -------------------------------------------------------------------------------- 1 | // Get the time (in milliseconds) since the Unix Epoch 2 | let time = timestamp(); 3 | 4 | // Similar to Python, `a ** b` raises a to the power of b 5 | let i = 10 ** 5; 6 | 7 | while (i > 0){ 8 | i -= 1; 9 | } 10 | 11 | return timestamp() - time; 12 | -------------------------------------------------------------------------------- /crates/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cli" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ast = { path = "../ast" } 8 | clap = { version = "4.0.26", features = ["derive"] } 9 | parser = { path = "../parser" } 10 | interpreter = { path = "../interpreter" } 11 | crossterm = "0.25.0" 12 | -------------------------------------------------------------------------------- /crates/interpreter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interpreter" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ast = { path = "../ast" } 8 | gc = { version = "0.4.1", features = ["derive"] } 9 | is-macro = "0.2.1" 10 | paste = "1.0.9" 11 | thiserror = "1.0.37" 12 | 13 | [dev-dependencies] 14 | parser = { path = "../parser" } 15 | -------------------------------------------------------------------------------- /crates/interpreter/tests/tests_sources/break_continue.th: -------------------------------------------------------------------------------- 1 | let i = 10 ** 3; 2 | 3 | // Simulate a do-while loop 4 | while (true){ 5 | i -= 1; 6 | 7 | if (i == 0){ 8 | break; 9 | } 10 | 11 | continue; 12 | 13 | while(true){ 14 | // It should never reach this point, it should have continued to the next iteration of loop. 15 | } 16 | } 17 | 18 | return i; 19 | -------------------------------------------------------------------------------- /crates/interpreter/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | mod callable; 4 | mod context; 5 | mod error; 6 | mod stack; 7 | mod stdlib; 8 | mod value; 9 | 10 | pub use callable::{Callable, InterpretedFn, NativeFn}; 11 | pub use context::{BlockExit, Context}; 12 | pub use error::Error; 13 | pub use gc::GcCell; 14 | pub use value::{GcValue, ShallowValue, Value}; 15 | -------------------------------------------------------------------------------- /packages/demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /crates/parser/src/lex/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | #[derive(Debug)] 4 | pub struct Error { 5 | pub index: usize, 6 | } 7 | 8 | impl std::error::Error for Error {} 9 | 10 | impl Display for Error { 11 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 12 | write!(f, "Lexer did not expect character at index {}", self.index) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crates/wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | 9 | [dependencies] 10 | interpreter = { path = "../interpreter" } 11 | wasm-bindgen = "0.2.87" 12 | ast = { path = "../ast" } 13 | parser = { path = "../parser" } 14 | js-sys = "0.3.64" 15 | gc = { version = "0.4.1", features = ["derive"] } 16 | -------------------------------------------------------------------------------- /crates/interpreter/tests/tests_sources/queue.th: -------------------------------------------------------------------------------- 1 | let queue = [8, 2, 32, 64]; 2 | 3 | // Add items to the front 4 | unshift(queue, 2 ** 16); 5 | 6 | while (len(queue) > 0){ 7 | // You can take items off the front of the array 8 | let item = shift(queue); 9 | 10 | if (item > 1){ 11 | // .. and push items onto the back 12 | push(queue, item / 2); 13 | } 14 | } 15 | 16 | return queue; 17 | -------------------------------------------------------------------------------- /crates/interpreter/gen_demo_urls.js: -------------------------------------------------------------------------------- 1 | #! /bin/node 2 | 3 | let fs = require("fs"); 4 | 5 | let items = []; 6 | 7 | fs.readdirSync("./tests/tests_sources").forEach((file) => { 8 | items.push({ 9 | name: file, 10 | href: `/?code=${encodeURIComponent( 11 | fs.readFileSync(`./tests/tests_sources/${file}`, "utf8"), 12 | )}`, 13 | }); 14 | }); 15 | 16 | console.log(JSON.stringify(items)); 17 | -------------------------------------------------------------------------------- /packages/demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { svelte } from "@sveltejs/vite-plugin-svelte"; 3 | import wasm from "vite-plugin-wasm"; 4 | import topLevelAwait from "vite-plugin-top-level-await"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | server: { 9 | port: 3000, 10 | }, 11 | plugins: [wasm(), topLevelAwait(), svelte()], 12 | }); 13 | -------------------------------------------------------------------------------- /packages/demo/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | plugins: ["@typescript-eslint", "prettier"], 4 | env: { 5 | node: true, 6 | }, 7 | extends: [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:prettier/recommended", 11 | "plugin:svelte/recommended", 12 | ], 13 | rules: { 14 | "prettier/prettier": "warn", 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /crates/interpreter/tests/tests_sources/cyclic_arrays.th: -------------------------------------------------------------------------------- 1 | // I honestly do not remember why I wrote this. 2 | // My best guess is that it is to test the GC 3 | let k = 0; 4 | 5 | while (k < 10){ 6 | let test_arr = []; 7 | let test_arr_2 = []; 8 | 9 | let i = 0; 10 | while (i < 10){ 11 | push(test_arr, i); 12 | push(test_arr, test_arr_2); 13 | let temp = test_arr; 14 | test_arr = test_arr_2; 15 | test_arr_2 = temp; 16 | i = i + 1; 17 | } 18 | 19 | k = k + 1; 20 | } 21 | -------------------------------------------------------------------------------- /packages/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Thrax Language Demo 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /crates/interpreter/tests/tests_sources/fib.th: -------------------------------------------------------------------------------- 1 | // This is a really slow implementation of fib. 2 | // It can be used for testing the speed of the scripting engine by increased the value of `up_to`. 3 | // Inspired by a similar script in Rhai's examples 4 | 5 | fn fib(n) { 6 | if (n < 2) { 7 | return n; 8 | } else { 9 | return fib(n - 1) + fib(n - 2); 10 | } 11 | } 12 | 13 | let results = []; 14 | 15 | let i = /* this is a block comment */ 0; 16 | let up_to = 18; 17 | 18 | while (i < up_to){ 19 | let c = fib(i); 20 | push(results, c); 21 | i += 1; 22 | } 23 | 24 | return results; 25 | -------------------------------------------------------------------------------- /crates/parser/README.md: -------------------------------------------------------------------------------- 1 | # Parser 2 | 3 | ## Quick Start 4 | 5 | This crate is fairly straightforward to use. 6 | 7 | To quickly parse a string into an executable (via the interpreter) AST, use [`parse_string`]: 8 | 9 | ```rust 10 | let source = r#" 11 | fn greet(name){ 12 | return "Hello " + name; 13 | } 14 | 15 | return greet("world!"); 16 | "#; 17 | 18 | let program = parser::parse_string(source).unwrap(); 19 | 20 | // ... run it or something 21 | # let mut context = interpreter::Context::new(); 22 | # context.eval_program(&program).unwrap(); 23 | ``` 24 | 25 | While the above is what you likely want to do in most situations, 26 | you can also use the individual [`lex_string`] and [`parse_tokens`] functions. 27 | -------------------------------------------------------------------------------- /packages/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | /** 9 | * Typecheck JS in `.svelte` and `.js` files by default. 10 | * Disable checkJs if you'd like to use dynamic types in JS. 11 | * Note that setting allowJs false does not prevent the use 12 | * of JS in `.svelte` files. 13 | */ 14 | "allowJs": true, 15 | "checkJs": true, 16 | "isolatedModules": true 17 | }, 18 | "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], 19 | "references": [{ "path": "./tsconfig.node.json" }] 20 | } 21 | -------------------------------------------------------------------------------- /packages/demo/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./src/**/*.{html,js,svelte,ts}", 5 | "./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}", 6 | ], 7 | plugins: [require("flowbite/plugin")], 8 | theme: { 9 | extend: { 10 | colors: { 11 | // flowbite-svelte 12 | primary: { 13 | 50: "#FFF5F2", 14 | 100: "#FFF1EE", 15 | 200: "#FFE4DE", 16 | 300: "#FFD5CC", 17 | 400: "#FFBCAD", 18 | 500: "#FE795D", 19 | 600: "#EF562F", 20 | 700: "#EB4F27", 21 | 800: "#CC4522", 22 | 900: "#A5371B", 23 | }, 24 | }, 25 | }, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /crates/interpreter/tests/tests_sources/primes.th: -------------------------------------------------------------------------------- 1 | // Implementation of the Seive of Eratosthenes 2 | 3 | fn array_filled_with(array_size, with){ 4 | let arr = []; 5 | 6 | while (len(arr) < array_size){ 7 | push(arr, with); 8 | } 9 | 10 | return arr; 11 | } 12 | 13 | // Finds all primes from 2..=n-1 14 | fn primes_up_to(n){ 15 | let i = 2; 16 | let a = array_filled_with(n, true); 17 | 18 | while (i < n){ 19 | if (a[i]){ 20 | let j = i * 2; 21 | 22 | while (j < n){ 23 | a[j] = false; 24 | 25 | j += i; 26 | } 27 | } 28 | 29 | i += 1; 30 | } 31 | 32 | let primes = []; 33 | let prime_n = 2; 34 | 35 | while(prime_n < len(a)){ 36 | if (a[prime_n]){ 37 | push(primes, prime_n); 38 | } 39 | 40 | prime_n += 1; 41 | } 42 | 43 | return primes; 44 | } 45 | 46 | return primes_up_to(300); 47 | -------------------------------------------------------------------------------- /packages/demo/public/github-mark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/wasm/src/println.rs: -------------------------------------------------------------------------------- 1 | use gc::{Finalize, Trace}; 2 | use interpreter::{Callable, Value}; 3 | use js_sys::Function; 4 | use wasm_bindgen::JsValue; 5 | 6 | #[derive(Debug, Clone, Trace, Finalize)] 7 | pub struct PrintLn { 8 | #[unsafe_ignore_trace] 9 | inner_log: Function, 10 | } 11 | 12 | impl PrintLn { 13 | pub fn new(inner_log: Function) -> Self { 14 | Self { inner_log } 15 | } 16 | } 17 | 18 | impl Callable for PrintLn { 19 | fn call( 20 | &self, 21 | _context: &mut interpreter::Context, 22 | args: &[interpreter::GcValue], 23 | ) -> Result { 24 | let mut line = String::new(); 25 | 26 | for arg in args { 27 | line.push_str(&format!("{}", arg)); 28 | } 29 | 30 | let _ = self 31 | .inner_log 32 | .call1(&JsValue::NULL, &JsValue::from_str(&line)); 33 | 34 | Ok((Value::Null).into_gc()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /crates/ast/src/expr.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use is_macro::Is; 4 | 5 | #[derive(Debug, Is, Clone)] 6 | pub enum Expr { 7 | Ident(String), 8 | NumberLiteral(f64), 9 | StringLiteral(String), 10 | BoolLiteral(bool), 11 | ArrayLiteral(Vec), 12 | ObjectLiteral(HashMap), 13 | BinaryOp(BinaryOp), 14 | FnCall(FnCall), 15 | Member(Member), 16 | } 17 | 18 | #[derive(Debug, Clone)] 19 | pub struct BinaryOp { 20 | pub kind: BinaryOpKind, 21 | pub a: Box, 22 | pub b: Box, 23 | } 24 | 25 | #[derive(Debug, Is, Clone, Copy)] 26 | pub enum BinaryOpKind { 27 | Add, 28 | Subtract, 29 | Multiply, 30 | Divide, 31 | GreaterThan, 32 | LessThan, 33 | Equals, 34 | Pow, 35 | } 36 | 37 | #[derive(Debug, Is, Clone, Copy)] 38 | pub enum AssignOpKind { 39 | NoOp, 40 | Op(BinaryOpKind), 41 | } 42 | 43 | #[derive(Debug, Clone)] 44 | pub struct FnCall { 45 | pub ident: String, 46 | pub args: Vec, 47 | } 48 | 49 | #[derive(Debug, Clone)] 50 | pub struct Member { 51 | pub parent: Box, 52 | pub child: Box, 53 | } 54 | -------------------------------------------------------------------------------- /crates/ast/src/stmt.rs: -------------------------------------------------------------------------------- 1 | use is_macro::Is; 2 | 3 | use crate::{AssignOpKind, Expr}; 4 | 5 | #[derive(Debug, Is, Clone)] 6 | pub enum Stmt { 7 | VarDecl(VarDecl), 8 | VarAssign(VarAssign), 9 | FnDecl(FnDecl), 10 | WhileLoop(WhileLoop), 11 | BlockExit(BlockExit), 12 | IfElse(IfElse), 13 | Expr(Expr), 14 | } 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct VarDecl { 18 | pub ident: String, 19 | pub initializer: Expr, 20 | } 21 | 22 | #[derive(Debug, Clone)] 23 | pub struct VarAssign { 24 | pub to: Expr, 25 | pub value: Expr, 26 | pub op: AssignOpKind, 27 | } 28 | 29 | #[derive(Debug, Clone)] 30 | pub struct FnDecl { 31 | pub ident: String, 32 | pub prop_idents: Vec, 33 | pub body: Vec, 34 | } 35 | 36 | #[derive(Debug, Clone)] 37 | pub struct WhileLoop { 38 | pub condition: Expr, 39 | pub body: Vec, 40 | } 41 | 42 | #[derive(Debug, Clone)] 43 | pub enum BlockExit { 44 | FnReturn(Option), 45 | Break, 46 | Continue, 47 | } 48 | 49 | #[derive(Debug, Clone)] 50 | pub struct IfElse { 51 | pub condition: Expr, 52 | pub true_branch: Vec, 53 | pub else_branch: Vec, 54 | } 55 | -------------------------------------------------------------------------------- /crates/interpreter/tests/run_tests.rs: -------------------------------------------------------------------------------- 1 | use interpreter::{BlockExit, Context}; 2 | 3 | macro_rules! create_test { 4 | ($filename:ident, $e:pat) => { 5 | paste::paste! { 6 | #[test] 7 | fn [](){ 8 | let source = include_str!(concat!("./tests_sources/", stringify!($filename), ".th")); 9 | 10 | let ast = parser::parse_string(&source).unwrap(); 11 | let mut context = Context::new(); 12 | context.add_stdlib(); 13 | 14 | assert!(matches!(context.eval_program(&ast), Ok($e))); 15 | } 16 | } 17 | }; 18 | } 19 | 20 | create_test!(while_loop, BlockExit::Completed); 21 | create_test!(fib, BlockExit::Returned(Some(_))); 22 | create_test!(empty_fn, BlockExit::Completed); 23 | create_test!(add_fns, BlockExit::Completed); 24 | create_test!(cyclic_arrays, BlockExit::Completed); 25 | create_test!(index_object, BlockExit::Completed); 26 | create_test!(timing, BlockExit::Returned(Some(_))); 27 | create_test!(break_continue, BlockExit::Returned(Some(_))); 28 | create_test!(stack, BlockExit::Returned(Some(_))); 29 | create_test!(queue, BlockExit::Returned(Some(_))); 30 | create_test!(primes, BlockExit::Returned(Some(_))); 31 | -------------------------------------------------------------------------------- /crates/wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod println; 2 | 3 | use std::rc::Rc; 4 | 5 | use interpreter::{GcCell, Value}; 6 | use js_sys::Function; 7 | use println::PrintLn; 8 | use wasm_bindgen::{prelude::wasm_bindgen, throw_str}; 9 | 10 | #[wasm_bindgen] 11 | pub struct Context { 12 | inner: interpreter::Context, 13 | } 14 | 15 | #[wasm_bindgen] 16 | impl Context { 17 | #[wasm_bindgen(constructor)] 18 | pub fn new(log_fn: Function) -> Context { 19 | let mut inner = interpreter::Context::new(); 20 | inner.add_stdlib(); 21 | 22 | inner.add_callable("println", Rc::new(GcCell::new(PrintLn::new(log_fn)))); 23 | 24 | Self { inner } 25 | } 26 | 27 | pub fn easy_eval(&mut self, program: &str) -> String { 28 | let program = match parser::parse_string(program) { 29 | Ok(program) => program, 30 | Err(err) => throw_str(&err.to_string()), 31 | }; 32 | 33 | match self.inner.eval_program(&program) { 34 | Err(err) => throw_str(&err.to_string()), 35 | Ok(returned) => format!( 36 | "{}", 37 | returned 38 | .returned() 39 | .flatten() 40 | .unwrap_or(Value::Null.into_gc()) 41 | ), 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-check --tsconfig ./tsconfig.json", 11 | "format": "prettier --write --plugin-search-dir=. ./**/*" 12 | }, 13 | "devDependencies": { 14 | "@sveltejs/vite-plugin-svelte": "^2.0.4", 15 | "@tsconfig/svelte": "^4.0.1", 16 | "@typescript-eslint/eslint-plugin": "^5.57.0", 17 | "@typescript-eslint/parser": "^5.57.0", 18 | "autoprefixer": "^10.4.14", 19 | "eslint": "^8.37.0", 20 | "eslint-config-prettier": "^8.8.0", 21 | "eslint-plugin-prettier": "^4.2.1", 22 | "eslint-plugin-svelte": "^2.32.1", 23 | "postcss": "^8.4.24", 24 | "prettier": "^2.8.7", 25 | "prettier-plugin-svelte": "^2.10.1", 26 | "svelte": "^3.58.0", 27 | "svelte-check": "^3.3.1", 28 | "tailwindcss": "^3.3.2", 29 | "tslib": "^2.5.0", 30 | "typescript": "^5.0.2", 31 | "vite": "^4.3.9", 32 | "vite-plugin-top-level-await": "^1.3.1", 33 | "vite-plugin-wasm": "^3.2.2", 34 | "wasm": "file:../../crates/wasm/pkg" 35 | }, 36 | "dependencies": { 37 | "@popperjs/core": "^2.11.8", 38 | "firebase": "^10.5.0", 39 | "flowbite": "^1.6.6", 40 | "flowbite-svelte": "^0.39.2", 41 | "svelte-ace": "^1.0.21", 42 | "tailwind-merge": "^1.13.2", 43 | "xterm": "^5.2.1", 44 | "xterm-addon-fit": "^0.7.0", 45 | "xterm-addon-webgl": "^0.15.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/interpreter/src/error.rs: -------------------------------------------------------------------------------- 1 | use ast::BinaryOpKind; 2 | 3 | use crate::value::ShallowValue; 4 | 5 | #[derive(Debug, thiserror::Error)] 6 | pub enum Error { 7 | #[error("A value as already been assigned to {0}.")] 8 | Redeclaration(String), 9 | #[error("Assignment to an undeclared variable {0}")] 10 | Undeclared(String), 11 | /// Represent that a type is being used where another type should. 12 | /// 13 | /// 0 => Should 14 | /// 15 | /// 1 => Used 16 | #[error("Attempted to use a {1} as a {0}.")] 17 | TypeError(ShallowValue, ShallowValue), 18 | #[error("Attempted to perform a `{:?}` operation with {0} and {1}. This is invalid.", .2)] 19 | InvalidBinaryOpArgs(ShallowValue, ShallowValue, BinaryOpKind), 20 | #[error("Attempted to access non-existant variable {0}.")] 21 | UndefinedStackAccess(String), 22 | #[error("Attempted to access non-existant heap item {0}.")] 23 | UndefinedHeapAccess(usize), 24 | /// Represent that function is being supplied too many arguments. 25 | /// 26 | /// 0 => # of args requested 27 | /// 28 | /// 1 => # of args supplied 29 | #[error("Function requires {1} arguments, but was supplied {0}")] 30 | IncorrectArgumentCount(usize, usize), 31 | #[error("Expected integer value, got {0}")] 32 | ExpectedInteger(f64), 33 | #[error("Requested string or integer index {0} is out of bounds.")] 34 | IndexOutOfBounds(usize), 35 | #[error("Requested key {0} in object does not exist in object.")] 36 | ObjectMissingKey(String), 37 | #[error("Requestion type {0} cannot be indexed.")] 38 | CannotIndexType(ShallowValue), 39 | } 40 | -------------------------------------------------------------------------------- /crates/interpreter/README.md: -------------------------------------------------------------------------------- 1 | # Interpreter 2 | 3 | ## Quick Start 4 | 5 | The core of this crate is the `Context` struct. 6 | 7 | The simplest usage, assuming you have a parsed program is as follows: 8 | 9 | ```rust 10 | let source = "1 + 2;"; 11 | 12 | // See the parser crate for more information on this. 13 | let program = parser::parse_string(source).unwrap(); 14 | 15 | let mut context = interpreter::Context::new(); 16 | 17 | // Many of the core functions of the language require this (like pushing and popping from arrays), 18 | // but if you want to build a DSL, you don't have to do this. 19 | context.add_stdlib(); 20 | 21 | context.eval_program(&program).unwrap(); 22 | ``` 23 | 24 | ## Adding Native Functions 25 | 26 | You can add Rust functions to the interpreter [`Context`] with [`Context::add_native_function`]. 27 | 28 | ```rust 29 | use interpreter::{Error, Context, GcValue, NativeFn}; 30 | 31 | let mut context = Context::new(); 32 | 33 | let add_fn = |context: &mut Context, arguments: &[GcValue]| { 34 | // We are only looking at the first two arguments, but there can be an unlimited number. 35 | if arguments.len() < 2{ 36 | return Err(Error::IncorrectArgumentCount(2, arguments.len())); 37 | } 38 | 39 | let a = arguments[0].borrow(); 40 | let b = arguments[1].borrow(); 41 | 42 | Ok(a.add(&b)?.into_gc()) 43 | }; 44 | 45 | context.add_native_fn("add".to_string(), NativeFn(add_fn)); 46 | ``` 47 | 48 | ## Examples 49 | 50 | The best example of using this crate is the CLI, which can be found in the `crates` directory of the main repo. 51 | 52 | Specifically, for an example of adding native functions, see the `add_io` function inside the CLI `main.rs`. 53 | -------------------------------------------------------------------------------- /crates/parser/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | mod lex; 4 | mod parse; 5 | 6 | use ast::Program; 7 | pub use lex::{Error as LexError, Token, TokenKind}; 8 | pub use parse::{Error as ParseError, ErrorKind as ParseErrorKind}; 9 | 10 | #[derive(Debug, thiserror::Error)] 11 | pub enum Error { 12 | #[error("A problem was encountered while lexing: {0}")] 13 | Lex(#[from] LexError), 14 | #[error("A problem was encountered while parsing: {0}")] 15 | Parse(#[from] ParseError), 16 | } 17 | 18 | /// Completely lex a string into a series of tokens. 19 | pub fn lex_string(source: &str) -> Result, LexError> { 20 | let seperated: Vec<_> = source.chars().collect(); 21 | 22 | lex::lex_to_end(&seperated) 23 | } 24 | 25 | /// Completely parse tokens into an AST. 26 | pub fn parse_tokens(tokens: &[Token]) -> Result { 27 | parse::parse_stmt_list(tokens) 28 | } 29 | 30 | /// Function that does both [`lex_string`] and [`parse_tokens`]. 31 | /// 32 | /// This is mosty likely what you want to use, unless there is some special circumstance that involves 33 | /// caching tokens. 34 | pub fn parse_string(source: &str) -> Result { 35 | let tokens = lex_string(source)?; 36 | let program = parse::parse_stmt_list(&tokens)?; 37 | 38 | Ok(program) 39 | } 40 | 41 | #[cfg(test)] 42 | mod test_utils { 43 | use crate::lex::{lex_to_end, Token}; 44 | 45 | /// Helper function to make tests more declarative. 46 | pub fn tokenize(source: &str) -> Vec { 47 | let chars: Vec = source.chars().collect(); 48 | lex_to_end(&chars).expect("Failed to tokenize.") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ## What is it? 7 | 8 | Thrax is a little toy programming language I threw together in my free time near the end of 2022. 9 | My primary motivation was to learn more of the fine-point details of how dynamically-typed interpreted languages worked (JavaScript, Python and Ruby). 10 | In the future, I intend of doing a full write-up of the process and what it taught me on the way. 11 | 12 | I was also working on a [project](https://hdiharmony.web.app/) at [work](https://archytasinc.com/) that involved low-level parsing and interpretation of JavaScript code. 13 | While we were using SWC for the actual parsing, writing my own parser gave me a much better understanding of how everything fit together. 14 | 15 | ## [Live Sandbox](https://thrax.elijahpotter.dev) 16 | 17 | You don't have to download or install anything to start using Thrax. 18 | Play around with the language and experiment with a few demo scripts at the [interactive sandbox](https://thrax.elijahpotter.dev). 19 | 20 | ## Should You Use It? 21 | 22 | While it should be trivial to add Thrax to your Rust project, I do not recommend doing so for anything important. 23 | At the moment, I do not have the time to maintain or otherwise continue working on Thrax. 24 | 25 | ## Local Development 26 | 27 | Want to explore Thrax on your own machine? 28 | 29 | As long as you have Node.js, Rust, and Yarn, you can run and build everything using: 30 | 31 | ```bash 32 | ./build.sh --release # or --debug 33 | ``` 34 | 35 | Or, to run all unit tests: 36 | 37 | ```bash 38 | ./test.sh 39 | ``` 40 | 41 | ## What's in a Name? 42 | 43 | In Ancient Greece, "Thrax" usually meant "from Thrace." 44 | In this case, however, the name is a reference to one of the earliest grammarians, [Dionysis Thrax](https://en.wikipedia.org/wiki/Dionysius_Thrax). 45 | Just like how Dionysis Thrax had no formal education on grammar, and thus had to discover everything himself, I found myself discovering different aspects of recursive descent parsers and dynamic interpreters on my own. 46 | Thus the name, "Thrax". 47 | -------------------------------------------------------------------------------- /crates/interpreter/src/callable.rs: -------------------------------------------------------------------------------- 1 | use ast::Stmt; 2 | use gc::{Finalize, Trace}; 3 | 4 | use crate::{BlockExit, Context, Error, GcValue, Value}; 5 | 6 | pub trait Callable: Trace + Finalize { 7 | fn call(&self, context: &mut Context, args: &[GcValue]) -> Result; 8 | } 9 | 10 | #[derive(Debug, Clone, Trace, Finalize)] 11 | pub struct InterpretedFn { 12 | stack_height: usize, 13 | prop_idents: Vec, 14 | #[unsafe_ignore_trace] 15 | body: Vec, 16 | } 17 | 18 | impl InterpretedFn { 19 | pub fn new(stack_height: usize, prop_idents: Vec, body: Vec) -> Self { 20 | Self { 21 | stack_height, 22 | prop_idents, 23 | body, 24 | } 25 | } 26 | } 27 | 28 | impl Callable for InterpretedFn { 29 | fn call(&self, context: &mut Context, args: &[GcValue]) -> Result { 30 | if args.len() != self.prop_idents.len() { 31 | return Err(Error::IncorrectArgumentCount( 32 | self.prop_idents.len(), 33 | args.len(), 34 | )); 35 | } 36 | 37 | let popped = context.stack.pop_until_index(self.stack_height); 38 | context.stack.open_frame(); 39 | 40 | for (ident, value) in self.prop_idents.iter().zip(args.iter()) { 41 | context.stack.push_value(ident.clone(), value.clone()); 42 | } 43 | 44 | let res = context.eval_program(&self.body)?; 45 | 46 | context.stack.pop_frame(); 47 | context.stack.push_popped_stack(popped); 48 | 49 | if let BlockExit::Returned(r) = res { 50 | return Ok(r.unwrap_or_else(|| (Value::Null).into_gc())); 51 | } 52 | 53 | Ok((Value::Null).into_gc()) 54 | } 55 | } 56 | 57 | #[derive(Trace, Finalize)] 58 | pub struct NativeFn( 59 | #[unsafe_ignore_trace] pub fn(context: &mut Context, args: &[GcValue]) -> Result, 60 | ); 61 | 62 | impl Callable for NativeFn { 63 | fn call(&self, context: &mut Context, args: &[GcValue]) -> Result { 64 | self.0(context, args) 65 | } 66 | } 67 | 68 | // pub type NativeFn = fn(&mut Context, &[GcValue]) -> Result; 69 | 70 | // #[derive(Trace, Finalize)] 71 | // pub enum Fn { 72 | // Native(#[unsafe_ignore_trace] NativeFn), 73 | // /// This is only expressly different from `ast::FnDecl` in that it does not include an ident. 74 | // /// TODO: Store stack frame alongside function 75 | // Interpreted { 76 | // #[unsafe_ignore_trace] 77 | // prop_idents: Vec, 78 | // #[unsafe_ignore_trace] 79 | // body: Vec, 80 | // }, 81 | // } 82 | -------------------------------------------------------------------------------- /crates/interpreter/src/stack.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use gc::{Finalize, Trace}; 4 | 5 | pub struct FoundIdent { 6 | pub value: T, 7 | pub index: usize, 8 | } 9 | 10 | #[derive(Clone)] 11 | pub struct PoppedStack { 12 | values: Vec<(String, T)>, 13 | frames: Vec, 14 | } 15 | 16 | #[derive(Debug, Clone, Trace, Finalize)] 17 | pub struct Stack 18 | where 19 | T: Clone, 20 | { 21 | values: Vec<(String, T)>, 22 | /// Each item is start of each stack frame 23 | frames: Vec, 24 | } 25 | 26 | impl Stack 27 | where 28 | T: Clone, 29 | { 30 | pub fn new() -> Self { 31 | Self { 32 | values: Vec::new(), 33 | frames: vec![0], 34 | } 35 | } 36 | 37 | pub fn pop_frame(&mut self) -> Option> { 38 | let frame = self.frames.pop()?; 39 | 40 | Some(self.values.split_off(frame)) 41 | } 42 | 43 | pub fn open_frame(&mut self) { 44 | self.frames.push(self.values.len()) 45 | } 46 | 47 | pub fn push_frame(&mut self, mut values: Vec<(String, T)>) { 48 | self.frames.push(self.values.len()); 49 | self.values.append(&mut values); 50 | } 51 | 52 | pub fn push_value(&mut self, ident: String, value: T) { 53 | self.values.push((ident, value)) 54 | } 55 | 56 | pub fn value_len(&self) -> usize { 57 | self.values.len() 58 | } 59 | 60 | pub fn frame_len(&self) -> usize { 61 | self.frames.len() 62 | } 63 | 64 | /// Pop all elements after specific index 65 | pub fn pop_until_index(&mut self, index: usize) -> PoppedStack { 66 | let values = self.values.split_off(index + 1); 67 | 68 | let containing_frame = self 69 | .frames 70 | .iter() 71 | .enumerate() 72 | .rev() 73 | .find_map(|(i, f)| (*f <= index).then_some(i)) 74 | .unwrap(); 75 | 76 | let frames = self.frames.split_off(containing_frame + 1); 77 | 78 | PoppedStack { values, frames } 79 | } 80 | 81 | pub fn push_popped_stack(&mut self, popped: PoppedStack) { 82 | let PoppedStack { 83 | mut values, 84 | mut frames, 85 | } = popped; 86 | self.values.append(&mut values); 87 | self.frames.append(&mut frames); 88 | } 89 | 90 | pub fn find_with_ident<'a>(&'a self, ident: &str) -> Option> { 91 | let (index, value) = self 92 | .values 93 | .iter() 94 | .enumerate() 95 | .rev() 96 | .find_map(|(index, s)| s.0.eq(ident).then_some((index, s.1.clone())))?; 97 | 98 | Some(FoundIdent { value, index }) 99 | } 100 | 101 | pub fn iter_values(&'_ self) -> impl Iterator + '_ { 102 | self.values.iter().map(|(_, value)| value.clone()) 103 | } 104 | } 105 | 106 | impl Default for Stack 107 | where 108 | T: Clone, 109 | { 110 | fn default() -> Self { 111 | Self::new() 112 | } 113 | } 114 | 115 | impl Display for Stack 116 | where 117 | T: Display + Clone, 118 | { 119 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 120 | let mut current_frame = 0; 121 | 122 | for i in 0..self.values.len() { 123 | if let Some(next_frame_start) = self.frames.get(current_frame + 1) { 124 | if i > *next_frame_start { 125 | current_frame += 1; 126 | writeln!(f, "FRAME: {current_frame}")?; 127 | } 128 | } 129 | 130 | writeln!(f, "\t{}: {}", self.values[i].0, self.values[i].1)?; 131 | } 132 | 133 | Ok(()) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /crates/interpreter/src/stdlib.rs: -------------------------------------------------------------------------------- 1 | use crate::{Context, Error, GcValue, NativeFn, ShallowValue, Value}; 2 | use std::time::{SystemTime, UNIX_EPOCH}; 3 | 4 | pub fn add_stdlib(context: &mut Context) { 5 | context.add_native_fn("timestamp".to_string(), NativeFn(timestamp)); 6 | 7 | context.add_native_fn("push".to_string(), NativeFn(push)); 8 | 9 | context.add_native_fn("pop".to_string(), NativeFn(pop)); 10 | 11 | context.add_native_fn("unshift".to_string(), NativeFn(unshift)); 12 | 13 | context.add_native_fn("shift".to_string(), NativeFn(shift)); 14 | 15 | context.add_native_fn("len".to_string(), NativeFn(len)); 16 | } 17 | 18 | fn timestamp(_ctx: &mut Context, _args: &[GcValue]) -> Result { 19 | let time_in_ms = SystemTime::now() 20 | .duration_since(UNIX_EPOCH) 21 | .unwrap() 22 | .as_millis(); 23 | 24 | Ok(Value::Number(time_in_ms as f64).into_gc()) 25 | } 26 | 27 | fn push(_ctx: &mut Context, args: &[GcValue]) -> Result { 28 | if args.len() < 2 { 29 | return Err(Error::IncorrectArgumentCount(2, args.len())); 30 | } 31 | 32 | let mut args_iter = args.iter(); 33 | 34 | let mut first = (*args_iter.next().unwrap()).borrow_mut(); 35 | 36 | let Value::Array(arr) = &mut *first else { 37 | return Err(Error::TypeError(ShallowValue::Array, (*first).as_shallow())); 38 | }; 39 | 40 | for arg in args_iter { 41 | arr.push_back(arg.clone()) 42 | } 43 | 44 | Ok((Value::Null).into_gc()) 45 | } 46 | 47 | fn pop(_ctx: &mut Context, args: &[GcValue]) -> Result { 48 | if args.is_empty() { 49 | return Err(Error::IncorrectArgumentCount(1, args.len())); 50 | } 51 | 52 | let mut args_iter = args.iter(); 53 | 54 | let mut first = (*args_iter.next().unwrap()).borrow_mut(); 55 | 56 | let Value::Array(arr) = &mut *first else { 57 | return Err(Error::TypeError(ShallowValue::Array, (*first).as_shallow())); 58 | }; 59 | 60 | Ok(arr.pop_back().unwrap_or_else(|| Value::Null.into_gc())) 61 | } 62 | fn unshift(_ctx: &mut Context, args: &[GcValue]) -> Result { 63 | if args.len() < 2 { 64 | return Err(Error::IncorrectArgumentCount(2, args.len())); 65 | } 66 | 67 | let mut args_iter = args.iter(); 68 | 69 | let mut first = (*args_iter.next().unwrap()).borrow_mut(); 70 | 71 | let Value::Array(arr) = &mut *first else { 72 | return Err(Error::TypeError(ShallowValue::Array, (*first).as_shallow())); 73 | }; 74 | 75 | for arg in args_iter { 76 | arr.push_front(arg.clone()) 77 | } 78 | 79 | Ok((Value::Null).into_gc()) 80 | } 81 | fn shift(_ctx: &mut Context, args: &[GcValue]) -> Result { 82 | if args.is_empty() { 83 | return Err(Error::IncorrectArgumentCount(1, args.len())); 84 | } 85 | 86 | let mut args_iter = args.iter(); 87 | 88 | let mut first = (*args_iter.next().unwrap()).borrow_mut(); 89 | 90 | let Value::Array(arr) = &mut *first else { 91 | return Err(Error::TypeError(ShallowValue::Array, (*first).as_shallow())); 92 | }; 93 | 94 | Ok(arr.pop_front().unwrap_or_else(|| Value::Null.into_gc())) 95 | } 96 | 97 | fn len(_ctx: &mut Context, args: &[GcValue]) -> Result { 98 | if args.is_empty() { 99 | return Err(Error::IncorrectArgumentCount(1, args.len())); 100 | } 101 | 102 | let mut args_iter = args.iter(); 103 | 104 | let mut first = (*args_iter.next().unwrap()).borrow_mut(); 105 | 106 | let len = match &mut *first { 107 | Value::String(s) => s.len(), 108 | Value::Array(a) => a.len(), 109 | Value::Object(o) => o.len(), 110 | _ => return Err(Error::CannotIndexType((*first).as_shallow())), 111 | }; 112 | 113 | Ok(Value::Number(len as f64).into_gc()) 114 | } 115 | -------------------------------------------------------------------------------- /packages/demo/src/App.svelte: -------------------------------------------------------------------------------- 1 | 86 | 87 | 88 |

Example Scripts

89 | 90 | {item["name"]} 91 | 92 | 98 |
99 | 100 |
101 |
102 | 103 | 104 |
105 |
106 | 107 |
108 |
109 |
110 | -------------------------------------------------------------------------------- /crates/parser/src/lex/token.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | use ast::{AssignOpKind, BinaryOpKind}; 4 | use is_macro::Is; 5 | 6 | #[derive(Debug, Clone, Copy)] 7 | pub struct Span { 8 | pub start: usize, 9 | pub end: usize, 10 | } 11 | 12 | impl Span { 13 | pub fn new(start: usize, end: usize) -> Self { 14 | Self { start, end } 15 | } 16 | } 17 | 18 | #[derive(Debug, Clone)] 19 | pub struct Token { 20 | pub span: Span, 21 | pub kind: TokenKind, 22 | } 23 | 24 | macro_rules! define_token_types { 25 | ($($kind:ident$(($contains:ty))?),*) => { 26 | #[derive(Debug, Clone, PartialEq, Is)] 27 | pub enum TokenKind{ 28 | $( 29 | $kind $(($contains))?, 30 | )* 31 | } 32 | 33 | impl TokenKind { 34 | pub fn as_shallow(&self) -> ShallowTokenKind{ 35 | match self{ 36 | $( 37 | Self::$kind$((::paste::paste!{[<_$contains:snake>]}))? => ShallowTokenKind::$kind, 38 | )* 39 | } 40 | } 41 | } 42 | 43 | impl Display for TokenKind{ 44 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 45 | match self { 46 | $( 47 | Self::$kind$((::paste::paste!{[<_$contains:snake>]}))? => { 48 | write!(f, "{}", stringify!($kind))?; 49 | $(write!(f, ": {}", ::paste::paste!{[<_$contains:snake>]})?;)? 50 | Ok(()) 51 | }, 52 | )* 53 | } 54 | } 55 | } 56 | 57 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Is)] 58 | pub enum ShallowTokenKind{ 59 | $( 60 | $kind, 61 | )* 62 | } 63 | 64 | impl Display for ShallowTokenKind{ 65 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 66 | match self { 67 | $( 68 | Self::$kind => write!(f, stringify!($kind)), 69 | )* 70 | } 71 | } 72 | } 73 | }; 74 | } 75 | 76 | impl TokenKind { 77 | pub fn as_binary_op(&self) -> Option { 78 | match self { 79 | TokenKind::Plus => Some(BinaryOpKind::Add), 80 | TokenKind::Minus => Some(BinaryOpKind::Subtract), 81 | TokenKind::Asterisk => Some(BinaryOpKind::Multiply), 82 | TokenKind::ForwardSlash => Some(BinaryOpKind::Divide), 83 | TokenKind::DoubleEquals => Some(BinaryOpKind::Equals), 84 | TokenKind::GreaterThan => Some(BinaryOpKind::GreaterThan), 85 | TokenKind::DoubleAsterisk => Some(BinaryOpKind::Pow), 86 | TokenKind::LessThan => Some(BinaryOpKind::LessThan), 87 | _ => None, 88 | } 89 | } 90 | 91 | pub fn as_assign_op(&self) -> Option { 92 | match self { 93 | TokenKind::Equals => Some(AssignOpKind::NoOp), 94 | TokenKind::AddEquals => Some(AssignOpKind::Op(BinaryOpKind::Add)), 95 | TokenKind::SubtractEquals => Some(AssignOpKind::Op(BinaryOpKind::Subtract)), 96 | TokenKind::MultiplyEquals => Some(AssignOpKind::Op(BinaryOpKind::Multiply)), 97 | TokenKind::DivideEquals => Some(AssignOpKind::Op(BinaryOpKind::Divide)), 98 | _ => None, 99 | } 100 | } 101 | } 102 | 103 | define_token_types! { 104 | Number(f64), 105 | String(String), 106 | Ident(String), 107 | LeftParen, 108 | RightParen, 109 | LeftBrace, 110 | RightBrace, 111 | LeftBracket, 112 | RightBracket, 113 | Comma, 114 | Equals, 115 | DoubleEquals, 116 | AddEquals, 117 | SubtractEquals, 118 | MultiplyEquals, 119 | DivideEquals, 120 | GreaterThan, 121 | LessThan, 122 | Let, 123 | Fn, 124 | Return, 125 | Break, 126 | Continue, 127 | While, 128 | If, 129 | Else, 130 | Plus, 131 | Minus, 132 | Asterisk, 133 | DoubleAsterisk, 134 | ForwardSlash, 135 | True, 136 | False, 137 | Colon, 138 | Semicolon 139 | } 140 | -------------------------------------------------------------------------------- /crates/parser/src/parse/common_parsers.rs: -------------------------------------------------------------------------------- 1 | use ast::Expr; 2 | 3 | use super::expr_parsers::parse_expr; 4 | use super::tokens_ext::TokensExt; 5 | use super::Error; 6 | use crate::lex::{ShallowTokenKind, Token}; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct FoundExprList { 10 | pub items: Vec, 11 | pub next_index: usize, 12 | } 13 | 14 | impl FoundExprList { 15 | /// Consuming iterator over constituent `Expr`s 16 | pub fn iter_exprs(self) -> impl Iterator { 17 | self.items.into_iter().map(|t| t.expr) 18 | } 19 | } 20 | 21 | #[derive(Debug, Clone)] 22 | pub struct FoundExprListItem { 23 | pub expr: Expr, 24 | pub found_at: usize, 25 | } 26 | 27 | #[derive(Debug, Clone)] 28 | pub struct FoundPropIdentList { 29 | pub prop_idents: Vec, 30 | pub next_index: usize, 31 | } 32 | 33 | pub fn parse_expr_list( 34 | tokens: &[Token], 35 | separator: ShallowTokenKind, 36 | open: ShallowTokenKind, 37 | close: ShallowTokenKind, 38 | ) -> Result { 39 | let closing_index = tokens.locate_last_matched_right(open, close)?; 40 | 41 | if closing_index == 1 { 42 | return Ok(FoundExprList { 43 | items: Vec::new(), 44 | next_index: 2, 45 | }); 46 | } 47 | 48 | let mut current_start = 1; 49 | let mut items = Vec::new(); 50 | let mut d = 0; 51 | 52 | while current_start < closing_index { 53 | let current = &tokens[current_start..closing_index - d]; 54 | 55 | if current.is_empty() { 56 | return Err(Error::no_valid_expr(current_start)).map_err(Error::unrecoverable); 57 | } 58 | 59 | if d != 0 && tokens.get_token_kind(closing_index - d, separator).is_err() { 60 | d += 1; 61 | continue; 62 | } 63 | 64 | match parse_expr(current) { 65 | Ok(expr) => { 66 | items.push(FoundExprListItem { 67 | expr, 68 | found_at: current_start, 69 | }); 70 | current_start += current.len() + 1; 71 | d = 0; 72 | } 73 | Err(err) => { 74 | if let Error { 75 | is_recoverable: true, 76 | .. 77 | } = err 78 | { 79 | d += 1 80 | } else { 81 | return Err(err); 82 | } 83 | } 84 | } 85 | } 86 | 87 | Ok(FoundExprList { 88 | items, 89 | next_index: closing_index + 1, 90 | }) 91 | } 92 | 93 | /// Parses the arguments for a function call 94 | pub fn parse_prop_ident_list(tokens: &[Token]) -> Result { 95 | let FoundExprList { items, next_index } = parse_expr_list( 96 | tokens, 97 | ShallowTokenKind::Comma, 98 | ShallowTokenKind::LeftParen, 99 | ShallowTokenKind::RightParen, 100 | )?; 101 | 102 | let mut prop_idents = Vec::new(); 103 | 104 | for item in items { 105 | if let Some(ident) = item.expr.ident() { 106 | prop_idents.push(ident); 107 | } else { 108 | return Err(Error::expected_token( 109 | item.found_at, 110 | ShallowTokenKind::Ident, 111 | Some(tokens[item.found_at].clone()), 112 | ) 113 | .unrecoverable()); 114 | } 115 | } 116 | 117 | Ok(FoundPropIdentList { 118 | prop_idents, 119 | next_index, 120 | }) 121 | } 122 | 123 | #[cfg(test)] 124 | mod tests { 125 | use super::{parse_expr_list, parse_prop_ident_list}; 126 | use crate::lex::ShallowTokenKind; 127 | use crate::test_utils::tokenize; 128 | 129 | #[test] 130 | fn parses_empty_expr_list() { 131 | let tokens = tokenize("()"); 132 | 133 | let res = parse_expr_list( 134 | &tokens, 135 | ShallowTokenKind::Comma, 136 | ShallowTokenKind::LeftParen, 137 | ShallowTokenKind::RightParen, 138 | ); 139 | 140 | res.unwrap(); 141 | } 142 | 143 | #[test] 144 | fn parses_prop_list() { 145 | let tokens = tokenize("(a, b, c)"); 146 | 147 | let props = parse_prop_ident_list(&tokens).unwrap(); 148 | 149 | assert_eq!(props.prop_idents, vec!["a", "b", "c"]) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /crates/parser/src/parse/tokens_ext.rs: -------------------------------------------------------------------------------- 1 | use ast::{AssignOpKind, BinaryOpKind}; 2 | 3 | use crate::lex::{ShallowTokenKind, Token}; 4 | use crate::parse::Error; 5 | use crate::TokenKind; 6 | 7 | pub struct LocatedBinaryOp { 8 | pub op: BinaryOpKind, 9 | pub location: usize, 10 | } 11 | 12 | pub struct LocatedAssignOp { 13 | pub op: AssignOpKind, 14 | pub location: usize, 15 | } 16 | 17 | pub trait TokensExt { 18 | fn get_token_kind( 19 | &'_ self, 20 | index: usize, 21 | kind: ShallowTokenKind, 22 | ) -> Result<&'_ TokenKind, Error>; 23 | fn locate_first(&self, starting_at: usize, kind: ShallowTokenKind) -> Result; 24 | fn get_binary_op(&self, index: usize) -> Result; 25 | fn locate_last_matched_right( 26 | &self, 27 | left: ShallowTokenKind, 28 | right: ShallowTokenKind, 29 | ) -> Result; 30 | fn locate_first_binary_op(&self, starting_at: usize) -> Result; 31 | fn locate_first_assign_op(&self, starting_at: usize) -> Result; 32 | } 33 | 34 | impl TokensExt for [Token] { 35 | fn get_token_kind( 36 | &'_ self, 37 | index: usize, 38 | kind: ShallowTokenKind, 39 | ) -> Result<&'_ TokenKind, Error> { 40 | let token = self 41 | .get(index) 42 | .ok_or_else(|| Error::expected_token(index, kind, None))?; 43 | 44 | if token.kind.as_shallow() == kind { 45 | Ok(&token.kind) 46 | } else { 47 | Err(Error::expected_token(index, kind, Some(token.clone()))) 48 | } 49 | } 50 | 51 | fn locate_first(&self, starting_at: usize, kind: ShallowTokenKind) -> Result { 52 | if self.is_empty() { 53 | return Err(Error::expected_token(0, kind, None)); 54 | } 55 | 56 | self.iter() 57 | .enumerate() 58 | .skip(starting_at) 59 | .find_map(|(index, token)| (token.kind.as_shallow() == kind).then_some(index)) 60 | .ok_or_else(|| Error::expected_token(self.len() - 1, kind, self.last().cloned())) 61 | } 62 | 63 | fn get_binary_op(&self, index: usize) -> Result { 64 | let token = self 65 | .get(index) 66 | .ok_or_else(|| Error::expected_binary_operator(index, None))?; 67 | 68 | token 69 | .kind 70 | .as_binary_op() 71 | .ok_or_else(|| Error::expected_binary_operator(index, Some(token.clone()))) 72 | } 73 | 74 | /// Meant for brackets. 75 | /// 76 | /// For example, finding the final `]` in `[a + [b]]`. 77 | /// 78 | /// First token must be `left` 79 | /// 80 | /// Returns an error for expected `right` if not found. 81 | fn locate_last_matched_right( 82 | &self, 83 | left: ShallowTokenKind, 84 | right: ShallowTokenKind, 85 | ) -> Result { 86 | self.get_token_kind(0, left)?; 87 | 88 | let mut left_count = 1; 89 | 90 | for (index, token) in self.iter().enumerate().skip(1) { 91 | if token.kind.as_shallow() == left { 92 | left_count += 1 93 | } else if token.kind.as_shallow() == right { 94 | if left_count == 1 { 95 | return Ok(index); 96 | } else { 97 | left_count -= 1; 98 | } 99 | } 100 | } 101 | 102 | Err(Error::expected_token(self.len() - 1, right, None)) 103 | } 104 | 105 | fn locate_first_binary_op(&self, starting_at: usize) -> Result { 106 | if self.is_empty() { 107 | return Err(Error::expected_binary_operator(0, None)); 108 | } 109 | 110 | for (index, token) in self.iter().enumerate().skip(starting_at) { 111 | if let Some(op) = token.kind.as_binary_op() { 112 | return Ok(LocatedBinaryOp { 113 | op, 114 | location: index, 115 | }); 116 | } 117 | } 118 | 119 | Err(Error::expected_binary_operator( 120 | self.len() - 1, 121 | self.last().cloned(), 122 | )) 123 | } 124 | 125 | fn locate_first_assign_op(&self, starting_at: usize) -> Result { 126 | if self.is_empty() { 127 | return Err(Error::expected_assignment_operator(0, None)); 128 | } 129 | 130 | for (index, token) in self.iter().enumerate().skip(starting_at) { 131 | if let Some(op) = token.kind.as_assign_op() { 132 | return Ok(LocatedAssignOp { 133 | op, 134 | location: index, 135 | }); 136 | } 137 | } 138 | 139 | Err(Error::expected_assignment_operator( 140 | self.len() - 1, 141 | self.last().cloned(), 142 | )) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /crates/parser/src/parse/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | use is_macro::Is; 4 | 5 | use crate::lex::{ShallowTokenKind, Token}; 6 | 7 | #[derive(Debug)] 8 | pub struct Error { 9 | // TODO: Make this a span 10 | pub index: usize, 11 | pub kind: ErrorKind, 12 | // Whether the error can be recovered from. 13 | // 14 | // For example, a parser of a while loop can return recoverable if the `while` token isn't found, 15 | // but not if the boolean expression associated with it is malformed. 16 | // 17 | // Additionally, if this is `false` the error should always be pushed to the top. 18 | pub is_recoverable: bool, 19 | } 20 | 21 | impl std::error::Error for Error {} 22 | 23 | impl Display for Error { 24 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 25 | write!(f, "") 26 | } 27 | } 28 | 29 | #[derive(Debug, Is)] 30 | pub enum ErrorKind { 31 | ExpectedToken { 32 | expected: ShallowTokenKind, 33 | received: Option, 34 | }, 35 | ExpectedBinaryOperator { 36 | received: Option, 37 | }, 38 | ExpectedAssignmentOperator { 39 | received: Option, 40 | }, 41 | ExpectedLiteral { 42 | received: Option, 43 | }, 44 | FailedToConsume, 45 | NoValidExpr, 46 | NoTokensProvided, 47 | } 48 | 49 | impl Error { 50 | /// Adjusts [`Self::index`] by an index. 51 | pub fn offset(mut self, by: usize) -> Self { 52 | self.index += by; 53 | self 54 | } 55 | 56 | /// Sets the consumed [`Self::is_recoverable`] to `false` 57 | pub fn unrecoverable(mut self) -> Self { 58 | self.is_recoverable = false; 59 | self 60 | } 61 | 62 | pub fn expected_token( 63 | at_index: usize, 64 | expected: ShallowTokenKind, 65 | received: Option, 66 | ) -> Self { 67 | Self { 68 | index: at_index, 69 | kind: ErrorKind::ExpectedToken { expected, received }, 70 | is_recoverable: true, 71 | } 72 | } 73 | 74 | pub fn expected_binary_operator(at_index: usize, received: Option) -> Self { 75 | Self { 76 | index: at_index, 77 | kind: ErrorKind::ExpectedBinaryOperator { received }, 78 | is_recoverable: true, 79 | } 80 | } 81 | 82 | pub fn expected_assignment_operator(at_index: usize, received: Option) -> Self { 83 | Self { 84 | index: at_index, 85 | kind: ErrorKind::ExpectedAssignmentOperator { received }, 86 | is_recoverable: true, 87 | } 88 | } 89 | 90 | pub fn expected_literal(at_index: usize, received: Option) -> Self { 91 | Self { 92 | index: at_index, 93 | kind: ErrorKind::ExpectedLiteral { received }, 94 | is_recoverable: true, 95 | } 96 | } 97 | 98 | pub fn failed_to_consume(at_index: usize) -> Self { 99 | Self { 100 | index: at_index, 101 | kind: ErrorKind::FailedToConsume, 102 | 103 | is_recoverable: true, 104 | } 105 | } 106 | 107 | pub fn no_valid_expr(at_index: usize) -> Self { 108 | Self { 109 | index: at_index, 110 | kind: ErrorKind::NoValidExpr, 111 | is_recoverable: true, 112 | } 113 | } 114 | 115 | pub fn no_tokens_provided() -> Self { 116 | Self { 117 | index: 0, 118 | kind: ErrorKind::NoTokensProvided, 119 | is_recoverable: true, 120 | } 121 | } 122 | } 123 | 124 | impl Display for ErrorKind { 125 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 126 | match self { 127 | ErrorKind::ExpectedToken { expected, received } => { 128 | write!(f, "Expected {expected} got ")?; 129 | match received { 130 | Some(token) => write!(f, "{}", token.kind), 131 | None => write!(f, "to the end of the buffer."), 132 | } 133 | } 134 | ErrorKind::ExpectedBinaryOperator { received } => { 135 | write!(f, "Expected binary operator got ")?; 136 | match received { 137 | Some(token) => write!(f, "{}", token.kind), 138 | None => write!(f, "to the end of the buffer."), 139 | } 140 | } 141 | ErrorKind::ExpectedAssignmentOperator { received } => { 142 | write!(f, "Expected assignment operator got ")?; 143 | match received { 144 | Some(token) => write!(f, "{}", token.kind), 145 | None => write!(f, "to the end of the buffer."), 146 | } 147 | } 148 | ErrorKind::ExpectedLiteral { received } => { 149 | write!(f, "Expected literal got ")?; 150 | match received { 151 | Some(token) => write!(f, "{}", token.kind), 152 | None => write!(f, "to the end of the buffer."), 153 | } 154 | } 155 | ErrorKind::FailedToConsume => write!(f, "Failed to consume the provided input."), 156 | ErrorKind::NoValidExpr => write!(f, "No valid expression was found."), 157 | ErrorKind::NoTokensProvided => write!(f, "No tokens were provided."), 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /packages/demo/src/demoItems.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | name: "tour.th", 4 | href: "/?code=println(%22Welcome%20to%20Thrax!%22)%3B%0A%0A%2F%2F%20This%20is%20a%20comment%0A%2F%2F%20Below%20is%20an%20%60if%60%20statement%20and%20a%20boolean%20expression.%0Aif%20(3%20%3D%3D%203)%7B%0A%20%20%20%20println(%22Thrax%20syntax%20should%20be%20pretty%20familiar%20to%20anyone%20who%20has%20used%20JavaScript%22)%3B%0A%7D%0A%0A%2F%2F%20%60while%60%20loops%20are%20the%20only%20option.%0Alet%20i%20%3D%200%3B%0Awhile(i%20%3C%205)%7B%0A%20%20%20%20println(%22This%20is%20a%20loop!%22)%3B%0A%20%20%20%20i%20%2B%3D%201%3B%0A%7D%0A%0Alet%20arr%20%3D%20%5B%22arrays%22%2C%20%22look%22%2C%20%22like%22%2C%20%22this%22%5D%3B%0A%0Ai%20%3D%200%3B%0Awhile(i%20%3C%20len(arr))%7B%0A%20%20%20%20println(arr%5Bi%5D)%3B%0A%20%20%20%20i%20%2B%3D%201%3B%0A%7D%0A%0Aprintln(%22We%20can%20define%20and%20use%20functions%20like%20this%3A%22)%3B%0A%0Afn%20greet(name)%7B%0A%20%20%20%20%2F%2F%20Strings%20can%20be%20concatonated%20with%20%60%2B%60%0A%20%20%20%20println(%22Hello%2C%20%22%20%2B%20name%20%2B%20%22!%22)%3B%0A%7D%0A%0Agreet(%22world%22)%3B", 5 | }, 6 | { 7 | name: "add_fns.th", 8 | href: "/?code=fn%20test(n)%7B%0A%20%20return%20n%3B%0A%7D%0A%0Atest(1%20%2B%202)%20%2B%20test(2)%3B%0A", 9 | }, 10 | { 11 | name: "assign_index.th", 12 | href: "/?code=let%20arr%20%3D%20%5B1%2C%202%2C%203%5D%3B%0A%0Aarr%3B%0Aarr%5B2%5D%3B%0A%0Aarr%5B2%5D%20%3D%2023%3B%0A%0Areturn%20arr%3B%0A", 13 | }, 14 | { 15 | name: "break_continue.th", 16 | href: "/?code=let%20i%20%3D%2010%20**%203%3B%0A%0A%2F%2F%20Simulate%20a%20do-while%20loop%0Awhile%20(true)%7B%0A%20%20i%20-%3D%201%3B%20%0A%0A%20%20if%20(i%20%3D%3D%200)%7B%0A%20%20%20%20break%3B%0A%20%20%7D%0A%0A%20%20continue%3B%0A%0A%20%20while(true)%7B%0A%20%20%20%20%2F%2F%20It%20should%20never%20reach%20this%20point%2C%20it%20should%20have%20continued%20to%20the%20next%20iteration%20of%20loop.%0A%20%20%7D%0A%7D%0A%0Areturn%20i%3B%0A", 17 | }, 18 | { 19 | name: "cyclic_arrays.th", 20 | href: "/?code=%2F%2F%20I%20honestly%20do%20not%20remember%20why%20I%20wrote%20this.%0A%2F%2F%20My%20best%20guess%20is%20that%20it%20is%20to%20test%20the%20GC%0Alet%20k%20%3D%200%3B%0A%0Awhile%20(k%20%3C%2010)%7B%0A%20%20let%20test_arr%20%3D%20%5B%5D%3B%0A%20%20let%20test_arr_2%20%3D%20%5B%5D%3B%0A%0A%20%20let%20i%20%3D%200%3B%0A%20%20while%20(i%20%3C%2010)%7B%0A%20%20%20%20push(test_arr%2C%20i)%3B%0A%20%20%20%20push(test_arr%2C%20test_arr_2)%3B%0A%20%20%20%20let%20temp%20%3D%20test_arr%3B%0A%20%20%20%20test_arr%20%3D%20test_arr_2%3B%0A%20%20%20%20test_arr_2%20%3D%20temp%3B%0A%20%20%20%20i%20%3D%20i%20%2B%201%3B%0A%20%20%7D%0A%0A%20%20k%20%3D%20k%20%2B%201%3B%0A%7D%0A", 21 | }, 22 | { name: "empty_fn.th", href: "/?code=fn%20add(a%2C%20b)%7B%0A%7D%0A" }, 23 | { 24 | name: "fib.th", 25 | href: "/?code=%2F%2F%20This%20is%20a%20really%20slow%20implementation%20of%20fib.%0A%2F%2F%20It%20can%20be%20used%20for%20testing%20the%20speed%20of%20the%20scripting%20engine%20by%20increased%20the%20value%20of%20%60up_to%60.%0A%2F%2F%20Inspired%20by%20a%20similar%20script%20in%20Rhai's%20examples%0A%0Afn%20fib(n)%20%7B%0A%20%20if%20(n%20%3C%202)%20%7B%0A%20%20%20%20return%20n%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20return%20fib(n%20-%201)%20%2B%20fib(n%20-%202)%3B%0A%20%20%7D%0A%7D%0A%0Alet%20results%20%3D%20%5B%5D%3B%0A%0Alet%20i%20%3D%20%2F*%20this%20is%20a%20block%20comment%20*%2F%200%3B%0Alet%20up_to%20%3D%2018%3B%0A%0Awhile%20(i%20%3C%20up_to)%7B%0A%20%20let%20c%20%3D%20fib(i)%3B%0A%20%20push(results%2C%20c)%3B%0A%20%20i%20%2B%3D%201%3B%0A%7D%0A%0Areturn%20results%3B%0A", 26 | }, 27 | { 28 | name: "index_object.th", 29 | href: "/?code=let%20k%20%3D%20%7B%0A%20%20t%3A%2023%2C%0A%20%20sdf%3A%20%22testing%22%2C%0A%20%20rec%3A%20%7B%0A%20%20%20%20a%3A%20%22inner%22%0A%20%20%7D%0A%7D%3B%0A%0Ak%5B%22t%22%5D%3B%0Ak%5B%22sdf%22%5D%3B%0Ak%5B%22rec%22%5D%3B%0A", 30 | }, 31 | { 32 | name: "primes.th", 33 | href: "/?code=%2F%2F%20Implementation%20of%20the%20Seive%20of%20Eratosthenes%0A%0Afn%20array_filled_with(array_size%2C%20with)%7B%0A%20%20let%20arr%20%3D%20%5B%5D%3B%0A%0A%20%20while%20(len(arr)%20%3C%20array_size)%7B%0A%20%20%20%20push(arr%2C%20with)%3B%0A%20%20%7D%0A%0A%20%20return%20arr%3B%0A%7D%0A%0A%2F%2F%20Finds%20all%20primes%20from%202..%3Dn-1%0Afn%20primes_up_to(n)%7B%0A%20%20let%20i%20%3D%202%3B%20%20%0A%20%20let%20a%20%3D%20array_filled_with(n%2C%20true)%3B%0A%0A%20%20while%20(i%20%3C%20n)%7B%0A%20%20%20%20if%20(a%5Bi%5D)%7B%0A%20%20%20%20%20%20let%20j%20%3D%20i%20*%202%3B%0A%0A%20%20%20%20%20%20while%20(j%20%3C%20n)%7B%0A%20%20%20%20%20%20%20%20a%5Bj%5D%20%3D%20false%3B%20%20%20%20%20%20%20%20%0A%0A%20%20%20%20%20%20%20%20j%20%2B%3D%20i%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%20%0A%0A%20%20%20%20i%20%2B%3D%201%3B%0A%20%20%7D%0A%0A%20%20let%20primes%20%3D%20%5B%5D%3B%20%20%0A%20%20let%20prime_n%20%3D%202%3B%0A%0A%20%20while(prime_n%20%3C%20len(a))%7B%0A%20%20%20%20if%20(a%5Bprime_n%5D)%7B%0A%20%20%20%20%20%20push(primes%2C%20prime_n)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20prime_n%20%2B%3D%201%3B%0A%20%20%7D%0A%0A%20%20return%20primes%3B%0A%7D%0A%0Areturn%20primes_up_to(300)%3B%0A", 34 | }, 35 | { 36 | name: "queue.th", 37 | href: "/?code=let%20queue%20%3D%20%5B8%2C%202%2C%2032%2C%2064%5D%3B%0A%0A%2F%2F%20Add%20items%20to%20the%20front%0Aunshift(queue%2C%202%20**%2016)%3B%0A%0Awhile%20(len(queue)%20%3E%200)%7B%0A%20%20%2F%2F%20You%20can%20take%20items%20off%20the%20front%20of%20the%20array%0A%20%20let%20item%20%3D%20shift(queue)%3B%0A%0A%20%20if%20(item%20%3E%201)%7B%0A%20%20%20%20%2F%2F%20..%20and%20push%20items%20onto%20the%20back%0A%20%20%20%20push(queue%2C%20item%20%2F%202)%3B%0A%20%20%7D%0A%7D%0A%0Areturn%20queue%3B%0A", 38 | }, 39 | { 40 | name: "stack.th", 41 | href: "/?code=let%20stack%20%3D%20%5B1%2C%203%2C%202%2C%206%2C%2023%5D%3B%0A%0A%2F%2F%20We%20can%20push%20items%20to%20the%20end%20of%20the%20array%20%0Apush(stack%2C%20%22new%20item%22)%3B%0A%0A%2F%2F%20We%20can%20pop%20items%20off%20the%20end%0Apop(stack)%3B%0A%0Areturn%20pop(stack)%3B%0A", 42 | }, 43 | { 44 | name: "timing.th", 45 | href: "/?code=%2F%2F%20Get%20the%20time%20(in%20milliseconds)%20since%20the%20Unix%20Epoch%0Alet%20time%20%3D%20timestamp()%3B%0A%0A%2F%2F%20Similar%20to%20Python%2C%20%60a%20**%20b%60%20raises%20a%20to%20the%20power%20of%20b%0Alet%20i%20%3D%2010%20**%205%3B%0A%0Awhile%20(i%20%3E%200)%7B%0A%20%20i%20-%3D%201%3B%0A%7D%0A%0Areturn%20timestamp()%20-%20time%3B%0A", 46 | }, 47 | { 48 | name: "while_loop.th", 49 | href: "/?code=while%20(false)%7B%0A%20%20let%20a%20%3D%201%20%2B%201%3B%0A%7D%0A", 50 | }, 51 | ]; 52 | -------------------------------------------------------------------------------- /crates/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs::read; 2 | use std::io::stderr; 3 | use std::path::PathBuf; 4 | 5 | use ast::Program; 6 | use clap::{Parser, Subcommand}; 7 | use crossterm::execute; 8 | use crossterm::style::{Color, Print, ResetColor, SetForegroundColor}; 9 | use interpreter::{BlockExit, Context, NativeFn, Value}; 10 | use parser::{lex_string, parse_tokens}; 11 | 12 | #[derive(Parser, Debug)] 13 | #[command(author, version, about, long_about = None)] 14 | struct Args { 15 | #[command(subcommand)] 16 | subcommand: Action, 17 | } 18 | 19 | #[derive(Debug, Subcommand)] 20 | enum Action { 21 | Run { filename: PathBuf }, 22 | Ast { filename: PathBuf }, 23 | } 24 | 25 | fn main() { 26 | let args = Args::parse(); 27 | 28 | match args.subcommand { 29 | Action::Run { filename } => { 30 | let Some(ast) = load_ast(&filename) else { 31 | print_err("Could not load AST"); 32 | return; 33 | }; 34 | 35 | let mut context = Context::new(); 36 | context.add_stdlib(); 37 | add_io(&mut context); 38 | 39 | match context.eval_program(&ast) { 40 | Err(err) => println!("{:#?}", err), 41 | Ok(BlockExit::Returned(Some(_v))) => { 42 | println!("{}", _v); 43 | } 44 | _ => (), 45 | } 46 | } 47 | Action::Ast { filename } => { 48 | let Some(ast) = load_ast(&filename) else { 49 | print_err("Could not load AST"); 50 | return; 51 | }; 52 | eprintln!("{:#?}", ast); 53 | } 54 | } 55 | } 56 | 57 | fn load_ast(filename: &PathBuf) -> Option { 58 | let Ok(file) = read(filename) else { 59 | print_err("Could not load file from disk."); 60 | return None; 61 | }; 62 | 63 | let Ok(source) = String::from_utf8(file) else { 64 | print_err("Could not parse UTF-8 source."); 65 | return None; 66 | }; 67 | 68 | process_ast(&source) 69 | } 70 | 71 | fn process_ast(source: &str) -> Option { 72 | let tokens = match lex_string(source) { 73 | Ok(tokens) => tokens, 74 | Err(err) => { 75 | let (line, col) = line_col_from_index(err.index, source).unwrap(); 76 | 77 | println!("{:#?}", err); 78 | println!("At line {}, row {}", line, col); 79 | return None; 80 | } 81 | }; 82 | 83 | match parse_tokens(&tokens) { 84 | Ok(ast) => Some(ast), 85 | Err(err) => { 86 | let token = &tokens[err.index]; 87 | 88 | let (start_line, start_col) = line_col_from_index(token.span.start, source).unwrap(); 89 | let (end_line, end_col) = line_col_from_index(token.span.end, source).unwrap(); 90 | 91 | print_line_col(start_line, end_line, start_col, end_col, source); 92 | 93 | if err.is_recoverable { 94 | print!("Recoverable Error: "); 95 | } else { 96 | print!("Unrecoverable Error: ") 97 | } 98 | 99 | println!("{}", err.kind); 100 | println!("At line {}, row {}", start_line, start_col); 101 | None 102 | } 103 | } 104 | } 105 | 106 | /// Computes the line:column an index is at in source code. 107 | /// Returns `None` if the index is outside the source. 108 | fn line_col_from_index(index: usize, source: &str) -> Option<(usize, usize)> { 109 | let mut traversed = 0; 110 | 111 | for (nth_line, source_line) in source.lines().enumerate() { 112 | let line_len = source_line.chars().count(); 113 | 114 | if (traversed..traversed + line_len).contains(&index) { 115 | return Some((nth_line + 1, index - traversed + 1)); 116 | } 117 | 118 | traversed += line_len + 1; 119 | } 120 | 121 | None 122 | } 123 | 124 | fn print_line_col( 125 | start_line: usize, 126 | end_line: usize, 127 | start_col: usize, 128 | end_col: usize, 129 | source: &str, 130 | ) { 131 | let end_line_nr_str = format!("{}", start_line); 132 | let padding = " ".repeat(end_line_nr_str.len()); 133 | 134 | let mut stderr = stderr(); 135 | 136 | for (index, line) in source.lines().enumerate() { 137 | if start_line > 1 && index == start_line - 2 { 138 | execute!( 139 | stderr, 140 | SetForegroundColor(Color::Blue), 141 | Print(format!("{} |\n", padding)), 142 | ResetColor 143 | ) 144 | .unwrap(); 145 | } else if index == end_line { 146 | execute!( 147 | stderr, 148 | SetForegroundColor(Color::Blue), 149 | Print(format!("{} | ", padding)), 150 | SetForegroundColor(Color::Red), 151 | Print(format!( 152 | "{}{}\n", 153 | " ".repeat(start_col - 1), 154 | "^".repeat(end_col - start_col) 155 | )), 156 | ResetColor 157 | ) 158 | .unwrap(); 159 | } else if index >= start_line - 1 && index < end_line { 160 | let nr_string = format!("{}", index + 1); 161 | execute!( 162 | stderr, 163 | SetForegroundColor(Color::Blue), 164 | Print(format!( 165 | "{}{} |", 166 | " ".repeat(padding.len() - nr_string.len()), 167 | nr_string 168 | )), 169 | SetForegroundColor(Color::White), 170 | Print(format!(" {}\n", line)), 171 | ResetColor 172 | ) 173 | .unwrap(); 174 | } 175 | } 176 | } 177 | 178 | fn print_err(err: &str) { 179 | let mut stderr = stderr(); 180 | 181 | execute!( 182 | stderr, 183 | SetForegroundColor(Color::Red), 184 | Print(err), 185 | Print("\n"), 186 | ResetColor 187 | ) 188 | .unwrap(); 189 | } 190 | 191 | fn add_io(context: &mut Context) { 192 | context.add_native_fn( 193 | "println".to_string(), 194 | NativeFn(|_context, args| { 195 | for arg in args { 196 | print!("{}", arg); 197 | } 198 | println!(); 199 | Ok((Value::Null).into_gc()) 200 | }), 201 | ); 202 | 203 | context.add_native_fn( 204 | "print".to_string(), 205 | NativeFn(|_context, args| { 206 | for arg in args { 207 | print!("{}", arg); 208 | } 209 | Ok(Value::Null.into_gc()) 210 | }), 211 | ); 212 | } 213 | -------------------------------------------------------------------------------- /crates/parser/src/lex/lexers.rs: -------------------------------------------------------------------------------- 1 | use super::token::{Span, Token, TokenKind}; 2 | use super::Error; 3 | 4 | #[derive(Debug)] 5 | pub struct FoundToken { 6 | /// The index of the character __after__ the lexed token 7 | pub next_index: usize, 8 | /// Token lexed 9 | pub token: TokenKind, 10 | } 11 | 12 | /// Lex all tokens, if possible. 13 | pub fn lex_to_end(source: &[char]) -> Result, Error> { 14 | let mut cursor = 0; 15 | let mut tokens = Vec::new(); 16 | 17 | loop { 18 | cursor += lex_ignorables(&source[cursor..]); 19 | 20 | if cursor == source.len() { 21 | return Ok(tokens); 22 | } 23 | 24 | if let Some(FoundToken { token, next_index }) = lex_token(&source[cursor..]) { 25 | tokens.push(Token { 26 | span: Span::new(cursor, cursor + next_index), 27 | kind: token, 28 | }); 29 | cursor += next_index; 30 | } else { 31 | return Err(Error { index: cursor }); 32 | } 33 | } 34 | } 35 | 36 | /// Runs all lexers over supplied source, returning the first success 37 | pub fn lex_token(source: &[char]) -> Option { 38 | let lexers = [lex_number, lex_string, lex_keyword, lex_ident]; 39 | 40 | for lexer in lexers { 41 | if let Some(ft) = lexer(source) { 42 | return Some(ft); 43 | } 44 | } 45 | 46 | None 47 | } 48 | 49 | /// Find the first token _after_ all ignorables, including whitespace and comments 50 | pub fn lex_ignorables(source: &[char]) -> usize { 51 | let mut cursor = 0; 52 | 53 | loop { 54 | let last_cursor = cursor; 55 | 56 | cursor += lex_whitespace(&source[cursor..]); 57 | cursor += lex_comments(&source[cursor..]); 58 | 59 | if last_cursor == cursor { 60 | break; 61 | } 62 | } 63 | 64 | cursor 65 | } 66 | 67 | /// Find the first token _after_ whitespace. 68 | pub fn lex_whitespace(source: &[char]) -> usize { 69 | for (index, c) in source.iter().enumerate() { 70 | if !c.is_whitespace() { 71 | return index; 72 | } 73 | } 74 | 75 | source.len() 76 | } 77 | 78 | /// Find the first token _after_ any comment. 79 | pub fn lex_comments(source: &[char]) -> usize { 80 | let i = lex_line_comment(source); 81 | lex_block_comment(&source[i..]) + i 82 | } 83 | 84 | /// Find the first token _after_ a line comment. 85 | pub fn lex_line_comment(source: &[char]) -> usize { 86 | if let (Some('/'), Some('/')) = (source.get(0), source.get(1)) { 87 | source 88 | .iter() 89 | .enumerate() 90 | .find_map(|(index, v)| (*v == '\n').then_some(index)) 91 | .unwrap_or(source.len()) 92 | } else { 93 | 0 94 | } 95 | } 96 | 97 | /// Find the first token _after_ a block comment. 98 | pub fn lex_block_comment(source: &[char]) -> usize { 99 | if let (Some('/'), Some('*')) = (source.get(0), source.get(1)) { 100 | let mut i = source.iter().enumerate().peekable(); 101 | 102 | while let Some((index, c)) = i.next() { 103 | let c2 = i.peek(); 104 | 105 | if let ('*', Some((_, '/'))) = (c, c2) { 106 | return index + 2; 107 | } 108 | } 109 | 110 | source.len() 111 | } else { 112 | 0 113 | } 114 | } 115 | 116 | pub fn lex_number(source: &[char]) -> Option { 117 | if source.is_empty() { 118 | return None; 119 | } 120 | 121 | { 122 | let s: String = source.iter().collect(); 123 | 124 | if let Ok(n) = s.parse::() { 125 | return Some(FoundToken { 126 | token: TokenKind::Number(n), 127 | next_index: source.len(), 128 | }); 129 | } 130 | } 131 | 132 | lex_number(&source[0..source.len() - 1]) 133 | } 134 | 135 | fn is_ident_terminator(c: char) -> bool { 136 | c.is_whitespace() || "(){},;:[]".contains(c) 137 | } 138 | 139 | fn lex_ident(source: &[char]) -> Option { 140 | let mut ident = String::new(); 141 | 142 | for (index, c) in source.iter().enumerate() { 143 | if is_ident_terminator(*c) { 144 | return Some(FoundToken { 145 | next_index: index, 146 | token: TokenKind::Ident(ident), 147 | }); 148 | } else { 149 | ident.push(*c) 150 | } 151 | } 152 | 153 | None 154 | } 155 | 156 | pub fn lex_string(source: &[char]) -> Option { 157 | if *source.first()? != '\"' { 158 | return None; 159 | } 160 | 161 | let mut text = String::new(); 162 | 163 | for (index, c) in source.iter().enumerate().skip(1) { 164 | if *c == '\"' { 165 | return Some(FoundToken { 166 | next_index: index + 1, 167 | token: TokenKind::String(text), 168 | }); 169 | } else { 170 | text.push(*c); 171 | } 172 | } 173 | 174 | None 175 | } 176 | 177 | fn lex_characters(source: &[char], cs: &str, token: TokenKind) -> Option { 178 | let sep: Vec<_> = cs.chars().collect(); 179 | 180 | if source.get(0..cs.len())? == sep { 181 | Some(FoundToken { 182 | token, 183 | next_index: cs.len(), 184 | }) 185 | } else { 186 | None 187 | } 188 | } 189 | 190 | macro_rules! lex_chars_to { 191 | ($($text:literal => $res:ident),*) => { 192 | fn lex_keyword(source: &[char]) -> Option { 193 | $( 194 | if let Some(found) = lex_characters(source, $text, TokenKind::$res){ 195 | return Some(found); 196 | } 197 | )* 198 | 199 | None 200 | } 201 | }; 202 | } 203 | 204 | lex_chars_to! { 205 | "(" => LeftParen, 206 | ")" => RightParen, 207 | "{" => LeftBrace, 208 | "}" => RightBrace, 209 | "[" => LeftBracket, 210 | "]" => RightBracket, 211 | "," => Comma, 212 | "==" => DoubleEquals, 213 | "=" => Equals, 214 | "+=" => AddEquals, 215 | "-=" => SubtractEquals, 216 | "*=" => MultiplyEquals, 217 | "/=" => DivideEquals, 218 | ">" => GreaterThan, 219 | "<" => LessThan, 220 | ":" => Colon, 221 | ";" => Semicolon, 222 | "+" => Plus, 223 | "-" => Minus, 224 | "**" => DoubleAsterisk, 225 | "*" => Asterisk, 226 | "/" => ForwardSlash, 227 | "true" => True, 228 | "false" => False, 229 | "let" => Let, 230 | "fn" => Fn, 231 | "while" => While, 232 | "return" => Return, 233 | "break" => Break, 234 | "continue" => Continue, 235 | "if" => If, 236 | "else" => Else 237 | } 238 | -------------------------------------------------------------------------------- /crates/parser/src/parse/expr_parsers.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use ast::{BinaryOp, Expr, FnCall, Member}; 4 | 5 | use super::common_parsers::parse_expr_list; 6 | use super::tokens_ext::{LocatedBinaryOp, TokensExt}; 7 | use super::Error; 8 | use crate::lex::{ShallowTokenKind, Token, TokenKind}; 9 | 10 | /// Runs all parsers over supplied source, returning the first success or last failure 11 | pub fn parse_expr(tokens: &[Token]) -> Result { 12 | let parsers = [ 13 | parse_binary_op, 14 | parse_fn_call, 15 | parse_member, 16 | parse_single_token, 17 | parse_array_literal, 18 | parse_object_literal, 19 | ]; 20 | 21 | let mut last_failure = None; 22 | 23 | for parser in parsers { 24 | match parser(tokens) { 25 | Ok(fe) => return Ok(fe), 26 | Err(err) => { 27 | if !err.is_recoverable { 28 | return Err(err); 29 | } else { 30 | last_failure = Some(err); 31 | } 32 | } 33 | } 34 | } 35 | 36 | Err(last_failure.unwrap()) 37 | } 38 | 39 | fn parse_single_token(tokens: &[Token]) -> Result { 40 | let token = tokens 41 | .get(0) 42 | .ok_or_else(|| Error::expected_literal(0, None))? 43 | .clone(); 44 | 45 | let expr = match token.kind { 46 | TokenKind::Number(n) => Expr::NumberLiteral(n), 47 | TokenKind::String(s) => Expr::StringLiteral(s), 48 | TokenKind::Ident(i) => Expr::Ident(i), 49 | TokenKind::True => Expr::BoolLiteral(true), 50 | TokenKind::False => Expr::BoolLiteral(false), 51 | _ => return Err(Error::expected_literal(0, Some(token))), 52 | }; 53 | 54 | if tokens.len() != 1 { 55 | return Err(Error::failed_to_consume(1)); 56 | } 57 | 58 | Ok(expr) 59 | } 60 | 61 | fn parse_binary_op(tokens: &[Token]) -> Result { 62 | // Iterate over binary ops 63 | // Check if possible to do: parse_expr() 64 | // If not, check next op 65 | let mut scan_start = 0; 66 | 67 | let (a, b, kind) = loop { 68 | let LocatedBinaryOp { op, location } = tokens.locate_first_binary_op(scan_start)?; 69 | 70 | // a + b 71 | let a_tokens = &tokens[..location]; 72 | let Ok(a) = parse_expr(a_tokens) else { 73 | scan_start = location + 1; 74 | continue; 75 | }; 76 | 77 | let b_tokens = &tokens[location + 1..]; 78 | let Ok(b) = parse_expr(b_tokens) else { 79 | scan_start = location + 1; 80 | continue; 81 | }; 82 | 83 | let consumed_token_count = a_tokens.len() + b_tokens.len() + 1; 84 | if consumed_token_count != tokens.len() { 85 | return Err(Error::failed_to_consume(consumed_token_count)); 86 | } 87 | 88 | break (a, b, op); 89 | }; 90 | 91 | Ok(Expr::BinaryOp(BinaryOp { 92 | kind, 93 | a: Box::new(a), 94 | b: Box::new(b), 95 | })) 96 | } 97 | 98 | fn parse_fn_call(tokens: &[Token]) -> Result { 99 | let identifier = tokens.get_token_kind(0, ShallowTokenKind::Ident)?; 100 | 101 | let found_list = parse_expr_list( 102 | &tokens[1..], 103 | ShallowTokenKind::Comma, 104 | ShallowTokenKind::LeftParen, 105 | ShallowTokenKind::RightParen, 106 | ) 107 | .map_err(|err| err.offset(1))?; 108 | 109 | if found_list.next_index + 1 != tokens.len() { 110 | return Err(Error::failed_to_consume(found_list.next_index)); 111 | } 112 | 113 | Ok(Expr::FnCall(FnCall { 114 | ident: identifier.clone().ident().unwrap(), 115 | args: found_list.iter_exprs().collect(), 116 | })) 117 | } 118 | 119 | fn parse_member(tokens: &[Token]) -> Result { 120 | let open = tokens.locate_first(1, ShallowTokenKind::LeftBracket)?; 121 | 122 | let close = tokens[open..] 123 | .locate_last_matched_right( 124 | ShallowTokenKind::LeftBracket, 125 | ShallowTokenKind::RightBracket, 126 | ) 127 | .map_err(|err| err.offset(open))? 128 | + open; 129 | 130 | if close < tokens.len() - 1 { 131 | return Err(Error::failed_to_consume(close)); 132 | } 133 | 134 | let child = parse_expr(&tokens[open + 1..close]).map_err(|err| err.offset(open + 1))?; 135 | let parent = parse_expr(&tokens[0..open])?; 136 | 137 | Ok(Expr::Member(Member { 138 | parent: Box::new(parent), 139 | child: Box::new(child), 140 | })) 141 | } 142 | 143 | fn parse_array_literal(tokens: &[Token]) -> Result { 144 | let found_list = parse_expr_list( 145 | tokens, 146 | ShallowTokenKind::Comma, 147 | ShallowTokenKind::LeftBracket, 148 | ShallowTokenKind::RightBracket, 149 | )?; 150 | 151 | if found_list.next_index != tokens.len() { 152 | return Err(Error::failed_to_consume(found_list.next_index)); 153 | } 154 | 155 | Ok(Expr::ArrayLiteral(found_list.iter_exprs().collect())) 156 | } 157 | 158 | fn parse_object_literal(tokens: &[Token]) -> Result { 159 | let closing_index = tokens 160 | .locate_last_matched_right(ShallowTokenKind::LeftBrace, ShallowTokenKind::RightBrace)?; 161 | 162 | if closing_index == 1 { 163 | return Ok(Expr::ObjectLiteral(HashMap::new())); 164 | } 165 | 166 | if closing_index != tokens.len() - 1 { 167 | return Err(Error::failed_to_consume(closing_index)); 168 | } 169 | 170 | let mut current_start = 1; 171 | let mut items = HashMap::new(); 172 | let mut d = 0; 173 | 174 | while current_start < closing_index { 175 | let ident = tokens.get_token_kind(current_start, ShallowTokenKind::Ident)?; 176 | tokens.get_token_kind(current_start + 1, ShallowTokenKind::Colon)?; 177 | let current = &tokens[current_start + 2..closing_index - d]; 178 | 179 | if current.is_empty() { 180 | return Err(Error::no_valid_expr(current_start + 3)); 181 | } 182 | 183 | if d != 0 184 | && tokens 185 | .get_token_kind(closing_index - d, ShallowTokenKind::Comma) 186 | .is_err() 187 | { 188 | d += 1; 189 | continue; 190 | } 191 | 192 | match parse_expr(current) { 193 | Ok(expr) => { 194 | items.insert(ident.as_ident().unwrap().to_string(), expr); 195 | current_start += current.len() + 3; 196 | d = 0; 197 | } 198 | Err(_) => d += 1, 199 | } 200 | } 201 | 202 | Ok(Expr::ObjectLiteral(items)) 203 | } 204 | 205 | #[cfg(test)] 206 | mod tests { 207 | use super::{parse_array_literal, parse_binary_op}; 208 | use crate::parse::expr_parsers::{parse_fn_call, parse_object_literal}; 209 | use crate::test_utils::tokenize; 210 | 211 | // TODO: ADD WAY MORE TESTS 212 | 213 | #[test] 214 | fn parses_add() { 215 | let tokens = tokenize("1 + 23 / 2"); 216 | 217 | let res = parse_binary_op(&tokens); 218 | 219 | res.unwrap(); 220 | } 221 | 222 | #[test] 223 | fn parses_fn_call() { 224 | let tokens = tokenize("test(a + 12, b)"); 225 | 226 | let res = parse_fn_call(&tokens); 227 | 228 | res.unwrap(); 229 | } 230 | 231 | #[test] 232 | fn parses_array_literal() { 233 | let tokens = tokenize("[a, b, \"test\", 23, [1, 2]]"); 234 | 235 | let res = parse_array_literal(&tokens); 236 | 237 | res.unwrap(); 238 | } 239 | 240 | #[test] 241 | fn parses_object_literal() { 242 | let tokens = tokenize("{ a: 1, b: 2, arr: [1, 2], str: \"test string\" }"); 243 | 244 | let res = parse_object_literal(&tokens); 245 | 246 | res.unwrap(); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /crates/interpreter/src/value.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, VecDeque}; 2 | use std::fmt::Display; 3 | use std::ops::Deref; 4 | use std::rc::Rc; 5 | 6 | use ast::BinaryOpKind; 7 | use gc::{Finalize, Gc, GcCell, GcCellRef, GcCellRefMut, Trace}; 8 | 9 | use crate::error::Error; 10 | use crate::Callable; 11 | 12 | #[derive(Clone, Trace, Finalize)] 13 | pub enum Value { 14 | Number(f64), 15 | String(String), 16 | Bool(bool), 17 | Array(VecDeque), 18 | Object(HashMap), 19 | Callable(Rc>), 20 | Null, 21 | } 22 | 23 | impl Value { 24 | pub fn as_shallow(&self) -> ShallowValue { 25 | match self { 26 | Value::Number(_) => ShallowValue::Number, 27 | Value::String(_) => ShallowValue::String, 28 | Value::Bool(_) => ShallowValue::Bool, 29 | Value::Array(_) => ShallowValue::Array, 30 | Value::Object(_) => ShallowValue::Object, 31 | Value::Callable(_) => ShallowValue::Callable, 32 | Value::Null => ShallowValue::Null, 33 | } 34 | } 35 | } 36 | 37 | #[derive(Debug, Clone, Copy)] 38 | pub enum ShallowValue { 39 | Number, 40 | String, 41 | Bool, 42 | Array, 43 | Object, 44 | Callable, 45 | Null, 46 | } 47 | 48 | impl Display for ShallowValue { 49 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 50 | let m = match self { 51 | ShallowValue::Number => "Number", 52 | ShallowValue::String => "String", 53 | ShallowValue::Bool => "Bool", 54 | ShallowValue::Array => "Array", 55 | ShallowValue::Object => "Object", 56 | ShallowValue::Callable => "Callable", 57 | ShallowValue::Null => "Null", 58 | }; 59 | 60 | write!(f, "{m}") 61 | } 62 | } 63 | 64 | #[derive(Clone, Trace, Finalize)] 65 | pub struct GcValue { 66 | inner: Gc>, 67 | } 68 | 69 | impl GcValue { 70 | pub fn new(value: Value) -> Self { 71 | Self { 72 | inner: Gc::new(GcCell::new(value)), 73 | } 74 | } 75 | 76 | pub fn borrow(&self) -> GcCellRef<'_, Value> { 77 | self.inner.borrow() 78 | } 79 | 80 | pub fn borrow_mut(&self) -> GcCellRefMut<'_, Value> { 81 | self.inner.borrow_mut() 82 | } 83 | 84 | /// For when you want to pass a value either by referance or by value depending on it's type. 85 | pub fn shallow_copy(&self) -> Self { 86 | match &*self.borrow() { 87 | Value::Number(n) => Value::Number(*n).into_gc(), 88 | Value::String(s) => Value::String(s.clone()).into_gc(), 89 | Value::Bool(b) => Value::Bool(*b).into_gc(), 90 | Value::Null => Value::Null.into_gc(), 91 | _ => self.clone(), 92 | } 93 | } 94 | } 95 | 96 | impl Display for GcValue { 97 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 98 | match self.borrow().deref() { 99 | Value::Number(n) => write!(f, "{n}"), 100 | Value::String(s) => write!(f, "{s}"), 101 | Value::Bool(b) => write!(f, "{b}"), 102 | Value::Array(arr) => { 103 | let mut s = String::new(); 104 | s.push('['); 105 | 106 | if arr.len() > 1 { 107 | for item in arr.iter().take(arr.len() - 1) { 108 | s.push_str(format!("{item}, ").as_str()) 109 | } 110 | } 111 | 112 | if let Some(item) = arr.iter().last() { 113 | s.push_str(format!("{item}").as_str()) 114 | } 115 | 116 | s.push(']'); 117 | 118 | write!(f, "{s}") 119 | } 120 | Value::Object(obj) => { 121 | let mut s = String::new(); 122 | 123 | s.push('{'); 124 | 125 | for (key, value) in obj.iter() { 126 | s.push_str(key); 127 | 128 | s.push_str(": "); 129 | 130 | s.push_str(format!("{value}").as_str()); 131 | 132 | s.push_str(", "); 133 | } 134 | 135 | s.push('}'); 136 | 137 | write!(f, "{s}") 138 | } 139 | Value::Callable(_) => write!(f, "Function"), 140 | Value::Null => write!(f, "Null"), 141 | } 142 | } 143 | } 144 | 145 | impl std::fmt::Debug for GcValue { 146 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 147 | Display::fmt(self, f) 148 | } 149 | } 150 | 151 | impl Value { 152 | pub fn into_gc(self) -> GcValue { 153 | GcValue::new(self) 154 | } 155 | } 156 | 157 | macro_rules! impl_op { 158 | ($op_kind:ident, $($variant:ident => $op:expr),*) => { 159 | paste::paste!{ 160 | pub fn [<$op_kind:snake>](&self, other: &Self) -> Result{ 161 | let res = match self{ 162 | $( 163 | Value::$variant(a) => { 164 | let b = match other{ 165 | Value::$variant(b) => b, 166 | _ => return Err(Error::TypeError(ShallowValue::$variant, other.as_shallow())) 167 | }; 168 | 169 | let v = $op; 170 | 171 | v(a, b) 172 | }, 173 | )* 174 | _ => return Err(Error::InvalidBinaryOpArgs(self.as_shallow(), other.as_shallow(), BinaryOpKind::$op_kind)) 175 | }; 176 | 177 | Ok(res) 178 | } 179 | } 180 | }; 181 | } 182 | 183 | impl Value { 184 | impl_op!(Add, 185 | Number => |a, b| Value::Number(a + b), 186 | String => |a: &str, b: &str| { 187 | let mut s = String::with_capacity(a.len() + b.len()); 188 | s.push_str(a); 189 | s.push_str(b); 190 | Value::String(s) 191 | } 192 | ); 193 | 194 | impl_op!(Subtract, 195 | Number => |a, b| Value::Number(a - b) 196 | ); 197 | 198 | impl_op!(Multiply, 199 | Number => |a, b| Value::Number(a * b) 200 | ); 201 | 202 | impl_op!(Divide, 203 | Number => |a, b| Value::Number(a / b) 204 | ); 205 | 206 | impl_op!(GreaterThan, 207 | Number => |a, b| Value::Bool(a > b) 208 | ); 209 | 210 | impl_op!(Pow, 211 | Number => |a: &f64, b: &f64| Value::Number(a.powf(*b)) 212 | ); 213 | 214 | impl_op!(LessThan, 215 | Number => |a, b| Value::Bool(a < b) 216 | ); 217 | 218 | impl_op!(Equals, 219 | Number => |a, b| Value::Bool(a == b), 220 | Bool => |a, b| Value::Bool(a == b), 221 | String => |a:&str, b:&str| Value::Bool(a.eq(b)) 222 | ); 223 | 224 | pub fn run_binary_op(&self, other: &Self, op: BinaryOpKind) -> Result { 225 | match op { 226 | ast::BinaryOpKind::Add => self.add(other), 227 | ast::BinaryOpKind::Subtract => self.subtract(other), 228 | ast::BinaryOpKind::Multiply => self.multiply(other), 229 | ast::BinaryOpKind::Divide => self.divide(other), 230 | ast::BinaryOpKind::GreaterThan => self.greater_than(other), 231 | ast::BinaryOpKind::LessThan => self.less_than(other), 232 | ast::BinaryOpKind::Pow => self.pow(other), 233 | ast::BinaryOpKind::Equals => self.equals(other), 234 | } 235 | } 236 | } 237 | 238 | impl From for Value { 239 | fn from(v: String) -> Self { 240 | Self::String(v) 241 | } 242 | } 243 | 244 | impl From>> for Value { 245 | fn from(value: Rc>) -> Self { 246 | Value::Callable(value) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /crates/parser/src/parse/stmt_parsers.rs: -------------------------------------------------------------------------------- 1 | use ast::{BlockExit, FnDecl, Stmt, VarAssign, VarDecl}; 2 | 3 | use super::common_parsers::{parse_prop_ident_list, FoundPropIdentList}; 4 | use super::expr_parsers::parse_expr; 5 | use super::tokens_ext::{LocatedAssignOp, TokensExt}; 6 | use super::Error; 7 | use crate::lex::{ShallowTokenKind, Token}; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct FoundStmt { 11 | pub stmt: Stmt, 12 | pub next_index: usize, 13 | } 14 | 15 | pub fn parse_stmt_list(tokens: &[Token]) -> Result, Error> { 16 | let mut stmts = Vec::new(); 17 | 18 | let mut current_index = 0; 19 | 20 | while current_index < tokens.len() { 21 | let FoundStmt { stmt, next_index } = 22 | parse_stmt(&tokens[current_index..]).map_err(|err| err.offset(current_index))?; 23 | 24 | current_index += next_index; 25 | stmts.push(stmt); 26 | } 27 | 28 | Ok(stmts) 29 | } 30 | 31 | /// Runs all parsers over supplied source, returning the first success or last failure 32 | pub fn parse_stmt(tokens: &[Token]) -> Result { 33 | let parsers = [ 34 | parse_var_decl, 35 | parse_var_assign, 36 | parse_fn_decl, 37 | parse_while_loop, 38 | parse_if_else, 39 | parse_return, 40 | parse_break_continue, 41 | parse_expr_stmt, 42 | ]; 43 | 44 | let mut last_failure = None; 45 | 46 | for parser in parsers { 47 | match parser(tokens) { 48 | Ok(fe) => return Ok(fe), 49 | Err(err) => { 50 | if !err.is_recoverable { 51 | return Err(err); 52 | } else { 53 | last_failure = Some(err); 54 | } 55 | } 56 | } 57 | } 58 | 59 | Err(last_failure.unwrap()) 60 | } 61 | 62 | fn parse_var_decl(tokens: &[Token]) -> Result { 63 | tokens.get_token_kind(0, ShallowTokenKind::Let)?; 64 | 65 | let identifier = tokens 66 | .get_token_kind(1, ShallowTokenKind::Ident) 67 | .map_err(Error::unrecoverable)?; 68 | 69 | tokens 70 | .get_token_kind(2, ShallowTokenKind::Equals) 71 | .map_err(Error::unrecoverable)?; 72 | 73 | let semi_location = tokens 74 | .locate_first(3, ShallowTokenKind::Semicolon) 75 | .map_err(Error::unrecoverable)?; 76 | 77 | let expr = 78 | parse_expr(&tokens[3..semi_location]).map_err(|err| err.offset(3).unrecoverable())?; 79 | 80 | Ok(FoundStmt { 81 | stmt: Stmt::VarDecl(VarDecl { 82 | ident: identifier.clone().ident().unwrap(), 83 | initializer: expr, 84 | }), 85 | next_index: semi_location + 1, 86 | }) 87 | } 88 | 89 | fn parse_var_assign(tokens: &[Token]) -> Result { 90 | let LocatedAssignOp { op, location } = tokens.locate_first_assign_op(1)?; 91 | 92 | let semi_location = tokens.locate_first(0, ShallowTokenKind::Semicolon)?; 93 | 94 | let to = parse_expr(&tokens[0..location])?; 95 | 96 | let value = 97 | parse_expr(&tokens[location + 1..semi_location]).map_err(|err| err.offset(location + 1))?; 98 | 99 | Ok(FoundStmt { 100 | stmt: Stmt::VarAssign(VarAssign { to, value, op }), 101 | next_index: semi_location + 1, 102 | }) 103 | } 104 | 105 | fn parse_fn_decl(tokens: &[Token]) -> Result { 106 | tokens.get_token_kind(0, ShallowTokenKind::Fn)?; 107 | 108 | let identifier = tokens 109 | .get_token_kind(1, ShallowTokenKind::Ident) 110 | .map_err(Error::unrecoverable)?; 111 | 112 | let FoundPropIdentList { 113 | prop_idents, 114 | next_index, 115 | } = parse_prop_ident_list(&tokens[2..]).map_err(|err| err.offset(2).unrecoverable())?; 116 | 117 | let FoundBody { 118 | body, 119 | next_index: after_body, 120 | } = parse_body(&tokens[next_index + 2..]) 121 | .map_err(|err| err.offset(next_index + 2).unrecoverable())?; 122 | 123 | Ok(FoundStmt { 124 | stmt: Stmt::FnDecl(FnDecl { 125 | ident: identifier.clone().ident().unwrap(), 126 | prop_idents, 127 | body, 128 | }), 129 | next_index: next_index + 2 + after_body, 130 | }) 131 | } 132 | 133 | fn parse_while_loop(tokens: &[Token]) -> Result { 134 | tokens.get_token_kind(0, ShallowTokenKind::While)?; 135 | 136 | let closing_paren_index = tokens[1..] 137 | .locate_last_matched_right(ShallowTokenKind::LeftParen, ShallowTokenKind::RightParen) 138 | .map_err(|err| err.offset(1).unrecoverable())? 139 | + 1; 140 | 141 | let expr = 142 | parse_expr(&tokens[2..closing_paren_index]).map_err(|err| err.offset(2).unrecoverable())?; 143 | 144 | let FoundBody { 145 | body, 146 | next_index: after_body, 147 | } = parse_body(&tokens[closing_paren_index + 1..]) 148 | .map_err(|err| err.offset(closing_paren_index + 1).unrecoverable())?; 149 | 150 | Ok(FoundStmt { 151 | stmt: Stmt::WhileLoop(ast::WhileLoop { 152 | condition: expr, 153 | body, 154 | }), 155 | next_index: closing_paren_index + 1 + after_body, 156 | }) 157 | } 158 | 159 | fn parse_return(tokens: &[Token]) -> Result { 160 | tokens.get_token_kind(0, ShallowTokenKind::Return)?; 161 | 162 | let final_semi = tokens 163 | .locate_first(0, ShallowTokenKind::Semicolon) 164 | .map_err(Error::unrecoverable)?; 165 | 166 | let expr = if final_semi == 1 { 167 | None 168 | } else { 169 | Some(parse_expr(&tokens[1..final_semi]).map_err(|err| err.offset(1).unrecoverable())?) 170 | }; 171 | 172 | Ok(FoundStmt { 173 | stmt: Stmt::BlockExit(BlockExit::FnReturn(expr)), 174 | next_index: final_semi + 1, 175 | }) 176 | } 177 | 178 | /// Parse either a `break` or a `continue` 179 | fn parse_break_continue(tokens: &[Token]) -> Result { 180 | tokens.get_token_kind(1, ShallowTokenKind::Semicolon)?; 181 | 182 | if let Ok(_found) = tokens.get_token_kind(0, ShallowTokenKind::Break) { 183 | return Ok(FoundStmt { 184 | stmt: Stmt::BlockExit(BlockExit::Break), 185 | next_index: 2, 186 | }); 187 | } 188 | 189 | tokens.get_token_kind(0, ShallowTokenKind::Continue)?; 190 | Ok(FoundStmt { 191 | stmt: Stmt::BlockExit(BlockExit::Continue), 192 | next_index: 2, 193 | }) 194 | } 195 | 196 | fn parse_if_else(tokens: &[Token]) -> Result { 197 | tokens.get_token_kind(0, ShallowTokenKind::If)?; 198 | 199 | let closing_paren_index = tokens[1..] 200 | .locate_last_matched_right(ShallowTokenKind::LeftParen, ShallowTokenKind::RightParen) 201 | .map_err(|err| err.offset(1).unrecoverable())? 202 | + 1; 203 | 204 | let condition = 205 | parse_expr(&tokens[2..closing_paren_index]).map_err(|err| err.offset(2).unrecoverable())?; 206 | 207 | let FoundBody { 208 | body: true_branch, 209 | next_index: after_body, 210 | } = parse_body(&tokens[closing_paren_index + 1..]) 211 | .map_err(|err| err.offset(closing_paren_index + 1).unrecoverable())?; 212 | 213 | let after_body = after_body + closing_paren_index + 1; 214 | 215 | if tokens 216 | .get_token_kind(after_body, ShallowTokenKind::Else) 217 | .is_ok() 218 | { 219 | // Check if it is an `else if` statement 220 | if let Ok(FoundStmt { 221 | stmt, 222 | next_index: after_second_body, 223 | }) = parse_if_else(&tokens[after_body + 1..]) 224 | { 225 | Ok(FoundStmt { 226 | stmt: Stmt::IfElse(ast::IfElse { 227 | condition, 228 | true_branch, 229 | else_branch: vec![stmt], 230 | }), 231 | next_index: after_body + 1 + after_second_body, 232 | }) 233 | } 234 | // Otherwise it's just an `else` statement 235 | else { 236 | let FoundBody { 237 | body: else_branch, 238 | next_index: after_second_body, 239 | } = parse_body(&tokens[after_body + 1..]) 240 | .map_err(|err| err.offset(after_body + 1).unrecoverable())?; 241 | Ok(FoundStmt { 242 | stmt: Stmt::IfElse(ast::IfElse { 243 | condition, 244 | true_branch, 245 | else_branch, 246 | }), 247 | next_index: after_body + 1 + after_second_body, 248 | }) 249 | } 250 | } else { 251 | Ok(FoundStmt { 252 | stmt: Stmt::IfElse(ast::IfElse { 253 | condition, 254 | true_branch, 255 | else_branch: Vec::new(), 256 | }), 257 | next_index: after_body, 258 | }) 259 | } 260 | } 261 | 262 | fn parse_expr_stmt(tokens: &[Token]) -> Result { 263 | let final_semi = tokens.locate_first(0, ShallowTokenKind::Semicolon)?; 264 | 265 | let expr = parse_expr(&tokens[..final_semi]).map_err(Error::unrecoverable)?; 266 | 267 | Ok(FoundStmt { 268 | stmt: Stmt::Expr(expr), 269 | next_index: final_semi + 1, 270 | }) 271 | } 272 | 273 | struct FoundBody { 274 | body: Vec, 275 | next_index: usize, 276 | } 277 | 278 | fn parse_body(tokens: &[Token]) -> Result { 279 | let closing_brace_index = tokens 280 | .locate_last_matched_right(ShallowTokenKind::LeftBrace, ShallowTokenKind::RightBrace)?; 281 | 282 | let body = parse_stmt_list(&tokens[1..closing_brace_index]) 283 | .map_err(|err| err.offset(1).unrecoverable())?; 284 | 285 | Ok(FoundBody { 286 | body, 287 | next_index: closing_brace_index + 1, 288 | }) 289 | } 290 | 291 | #[cfg(test)] 292 | mod tests { 293 | use super::{parse_fn_decl, parse_if_else}; 294 | use crate::parse::stmt_parsers::parse_while_loop; 295 | use crate::test_utils::tokenize; 296 | 297 | #[test] 298 | fn parses_fn_decl() { 299 | let tokens = tokenize("fn main(a, b) { let cat = 2 + 3 / 2; }"); 300 | 301 | let res = parse_fn_decl(&tokens); 302 | 303 | res.unwrap(); 304 | } 305 | 306 | #[test] 307 | fn parses_while_loop() { 308 | let tokens = tokenize("while (true){ test(); }"); 309 | 310 | let res = parse_while_loop(&tokens); 311 | 312 | res.unwrap(); 313 | } 314 | 315 | #[test] 316 | fn parses_if() { 317 | let tokens = tokenize("if (true){ test(); }"); 318 | 319 | let res = parse_if_else(&tokens); 320 | 321 | res.unwrap(); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /crates/interpreter/src/context.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, VecDeque}; 2 | use std::fmt::{Display, Formatter}; 3 | use std::rc::Rc; 4 | 5 | use ast::{ 6 | AssignOpKind, BinaryOp, Expr, FnCall, FnDecl, Member, Program, Stmt, VarAssign, VarDecl, 7 | WhileLoop, 8 | }; 9 | use gc::GcCell; 10 | use is_macro::Is; 11 | 12 | use crate::error::Error; 13 | use crate::stack::{FoundIdent, Stack}; 14 | use crate::stdlib::add_stdlib; 15 | use crate::value::{GcValue, ShallowValue, Value}; 16 | use crate::{Callable, InterpretedFn, NativeFn}; 17 | 18 | #[derive(Debug, Clone, Is)] 19 | pub enum BlockExit { 20 | Returned(Option), 21 | Break, 22 | Continue, 23 | Completed, 24 | } 25 | 26 | impl Display for BlockExit { 27 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 28 | match self { 29 | BlockExit::Returned(v) => write!( 30 | f, 31 | "Returned: {}", 32 | v.as_ref() 33 | .map(|v| v.to_string()) 34 | .unwrap_or("Nothing".to_string()) 35 | ), 36 | BlockExit::Break => write!(f, "Break"), 37 | BlockExit::Continue => write!(f, "Continue"), 38 | BlockExit::Completed => write!(f, "Completed"), 39 | } 40 | } 41 | } 42 | 43 | #[derive(Clone)] 44 | pub struct Context { 45 | pub stack: Stack, 46 | } 47 | 48 | impl Context { 49 | pub fn new() -> Self { 50 | Self { 51 | stack: Stack::new(), 52 | } 53 | } 54 | 55 | pub fn add_native_fn(&mut self, ident: impl ToString, native_fn: NativeFn) { 56 | self.stack.push_value( 57 | ident.to_string(), 58 | Value::Callable(Rc::new(GcCell::new(native_fn))).into_gc(), 59 | ) 60 | } 61 | 62 | pub fn add_callable(&mut self, ident: impl ToString, callable: Rc>) { 63 | self.stack 64 | .push_value(ident.to_string(), Value::Callable(callable).into_gc()) 65 | } 66 | 67 | /// Courtesey wrapper for [`crate::stdlib::add_stdlib`] 68 | pub fn add_stdlib(&mut self) { 69 | add_stdlib(self) 70 | } 71 | 72 | pub fn with_stdlib(mut self) -> Self { 73 | self.add_stdlib(); 74 | self 75 | } 76 | 77 | pub fn eval_program(&mut self, program: &Program) -> Result { 78 | for stmt in program { 79 | let res = self.eval_stmt(stmt)?; 80 | 81 | if !matches!(res, BlockExit::Completed) { 82 | return Ok(res); 83 | } 84 | } 85 | 86 | Ok(BlockExit::Completed) 87 | } 88 | 89 | pub fn eval_stmt(&mut self, stmt: &Stmt) -> Result { 90 | match stmt { 91 | Stmt::VarDecl(var_decl) => self.eval_var_decl(var_decl).map(|_| BlockExit::Completed), 92 | Stmt::VarAssign(var_assign) => self 93 | .eval_var_assign(var_assign) 94 | .map(|_| BlockExit::Completed), 95 | Stmt::FnDecl(fn_decl) => self.eval_fn_decl(fn_decl).map(|_| BlockExit::Completed), 96 | Stmt::Expr(expr) => self 97 | .eval_expr(expr) 98 | .map(|_| ()) 99 | .map(|_| BlockExit::Completed), 100 | Stmt::IfElse(if_else) => self.eval_if_else(if_else), 101 | Stmt::WhileLoop(while_loop) => self.eval_while_loop(while_loop), 102 | Stmt::BlockExit(block_exit) => { 103 | let exit = match block_exit { 104 | ast::BlockExit::FnReturn(res) => { 105 | if let Some(expr) = res { 106 | BlockExit::Returned(Some(self.eval_expr(expr)?)) 107 | } else { 108 | BlockExit::Returned(None) 109 | } 110 | } 111 | ast::BlockExit::Break => BlockExit::Break, 112 | ast::BlockExit::Continue => BlockExit::Continue, 113 | }; 114 | 115 | Ok(exit) 116 | } 117 | } 118 | } 119 | 120 | pub fn eval_expr(&mut self, expr: &Expr) -> Result { 121 | match expr { 122 | Expr::Ident(i) => self.find_with_ident(i).map(|v| v.value), 123 | Expr::NumberLiteral(n) => Ok(Value::Number(*n).into_gc()), 124 | Expr::StringLiteral(s) => Ok(Value::String(s.clone()).into_gc()), 125 | Expr::BoolLiteral(b) => Ok(Value::Bool(*b).into_gc()), 126 | Expr::ArrayLiteral(arr) => self.eval_array_lit(arr), 127 | Expr::ObjectLiteral(obj) => self.eval_object_lit(obj), 128 | Expr::BinaryOp(bin_op) => self.eval_binary_op(bin_op), 129 | Expr::FnCall(f) => self.run_fn(f), 130 | Expr::Member(m) => self.eval_member(m), 131 | } 132 | } 133 | 134 | fn eval_var_decl(&mut self, var_decl: &VarDecl) -> Result<(), Error> { 135 | let Err(_) = self.find_with_ident(&var_decl.ident) else { 136 | return Err(Error::Redeclaration(var_decl.ident.clone())); 137 | }; 138 | 139 | let initialized = self.eval_expr(&var_decl.initializer)?.shallow_copy(); 140 | 141 | self.stack.push_value(var_decl.ident.clone(), initialized); 142 | 143 | Ok(()) 144 | } 145 | 146 | fn eval_var_assign(&mut self, var_assign: &VarAssign) -> Result<(), Error> { 147 | let new_value = self.eval_expr(&var_assign.value)?.shallow_copy(); 148 | let new_value = new_value.borrow(); 149 | 150 | let value = self.eval_expr(&var_assign.to)?; 151 | let mut value = value.borrow_mut(); 152 | 153 | match var_assign.op { 154 | AssignOpKind::NoOp => { 155 | *value = new_value.clone(); 156 | } 157 | AssignOpKind::Op(op) => { 158 | let arith_res = value.run_binary_op(&new_value, op)?; 159 | 160 | *value = arith_res; 161 | } 162 | } 163 | 164 | Ok(()) 165 | } 166 | 167 | fn eval_fn_decl(&mut self, fn_decl: &FnDecl) -> Result<(), Error> { 168 | let Err(_) = self.find_with_ident(&fn_decl.ident) else { 169 | return Err(Error::Redeclaration(fn_decl.ident.clone())); 170 | }; 171 | 172 | self.stack.push_value( 173 | fn_decl.ident.clone(), 174 | Value::Callable(Rc::new(GcCell::new(InterpretedFn::new( 175 | self.stack.value_len(), 176 | fn_decl.prop_idents.clone(), 177 | fn_decl.body.clone(), 178 | )))) 179 | .into_gc(), 180 | ); 181 | 182 | Ok(()) 183 | } 184 | 185 | fn eval_while_loop(&mut self, while_loop: &WhileLoop) -> Result { 186 | while let Value::Bool(true) = { 187 | let res = self.eval_expr(&while_loop.condition)?; 188 | let res = res.borrow(); 189 | res.equals(&Value::Bool(true))? 190 | } { 191 | self.stack.open_frame(); 192 | 193 | let res = self.eval_program(&while_loop.body)?; 194 | 195 | self.stack.pop_frame(); 196 | 197 | match res { 198 | BlockExit::Returned(r) => return Ok(BlockExit::Returned(r)), 199 | BlockExit::Break => return Ok(BlockExit::Completed), 200 | _ => (), 201 | } 202 | } 203 | 204 | Ok(BlockExit::Completed) 205 | } 206 | 207 | fn eval_if_else(&mut self, if_else: &ast::IfElse) -> Result { 208 | let branch = match { 209 | let res = self.eval_expr(&if_else.condition)?; 210 | let res = res.borrow(); 211 | res.equals(&Value::Bool(true))? 212 | } { 213 | Value::Bool(true) => &if_else.true_branch, 214 | Value::Bool(false) => &if_else.else_branch, 215 | _ => panic!(), 216 | }; 217 | 218 | self.stack.open_frame(); 219 | 220 | let res = self.eval_program(branch); 221 | 222 | self.stack.pop_frame(); 223 | 224 | res 225 | } 226 | 227 | fn eval_member(&mut self, member: &Member) -> Result { 228 | let child = self.eval_expr(&member.child)?; 229 | let child = child.borrow(); 230 | 231 | let parent = self.eval_expr(&member.parent)?; 232 | let parent = parent.borrow_mut(); 233 | 234 | match &*parent { 235 | Value::String(s) => { 236 | if let Value::Number(index) = *child { 237 | let rounded = index.floor(); 238 | if rounded == index { 239 | s.chars() 240 | .nth(rounded as usize) 241 | .ok_or(Error::IndexOutOfBounds(rounded as usize)) 242 | .map(|c| Value::String(c.to_string()).into_gc()) // TODO: Once we add chars, remove this last bit 243 | } else { 244 | Err(Error::ExpectedInteger(index)) 245 | } 246 | } else { 247 | Err(Error::TypeError(ShallowValue::Number, child.as_shallow())) 248 | } 249 | } 250 | Value::Array(arr) => { 251 | if let Value::Number(index) = *child { 252 | let rounded = index.floor(); 253 | 254 | if rounded == index { 255 | arr.get(rounded as usize) 256 | .cloned() 257 | .ok_or(Error::IndexOutOfBounds(rounded as usize)) 258 | } else { 259 | Err(Error::ExpectedInteger(index)) 260 | } 261 | } else { 262 | Err(Error::TypeError(ShallowValue::Number, child.as_shallow())) 263 | } 264 | } 265 | Value::Object(obj) => { 266 | if let Value::String(index) = &*child { 267 | obj.get(index) 268 | .cloned() 269 | .ok_or_else(|| Error::ObjectMissingKey(index.clone())) 270 | } else { 271 | Err(Error::TypeError(ShallowValue::Number, child.as_shallow())) 272 | } 273 | } 274 | _ => Err(Error::CannotIndexType(parent.as_shallow())), 275 | } 276 | } 277 | 278 | fn eval_array_lit(&mut self, arr: &[Expr]) -> Result { 279 | let mut results = VecDeque::with_capacity(arr.len()); 280 | for expr in arr.iter() { 281 | let result = self.eval_expr(expr)?; 282 | results.push_back(result); 283 | } 284 | Ok((Value::Array(results)).into_gc()) 285 | } 286 | 287 | fn eval_object_lit(&mut self, obj: &HashMap) -> Result { 288 | let mut results = HashMap::with_capacity(obj.len()); 289 | 290 | for (key, expr) in obj { 291 | let result = self.eval_expr(expr)?; 292 | results.insert(key.to_string(), result); 293 | } 294 | 295 | Ok(Value::Object(results).into_gc()) 296 | } 297 | 298 | fn eval_binary_op(&mut self, bin_op: &BinaryOp) -> Result { 299 | let BinaryOp { kind, a, b } = bin_op; 300 | 301 | let c_a = self.eval_expr(a)?; 302 | let c_a = c_a.borrow(); 303 | let c_b = self.eval_expr(b)?; 304 | let c_b = c_b.borrow(); 305 | 306 | let arith_res = c_a.run_binary_op(&c_b, *kind)?; 307 | 308 | Ok((arith_res).into_gc()) 309 | } 310 | 311 | fn run_fn(&mut self, fn_call: &FnCall) -> Result { 312 | let mut args = Vec::with_capacity(fn_call.args.len()); 313 | 314 | for arg in &fn_call.args { 315 | let result = self.eval_expr(arg)?; 316 | args.push(result.shallow_copy()); 317 | } 318 | 319 | let fn_def = { 320 | let FoundIdent { 321 | value: definition, .. 322 | } = self.find_with_ident(&fn_call.ident)?; 323 | 324 | let definition = definition.borrow(); 325 | 326 | let Value::Callable(df) = &*definition else{ 327 | return Err(Error::TypeError(ShallowValue::Callable, definition.as_shallow())); 328 | }; 329 | 330 | df.clone() 331 | }; 332 | 333 | let fn_def = fn_def.borrow(); 334 | fn_def.call(self, &args) 335 | } 336 | 337 | fn find_with_ident(&self, ident: &str) -> Result, Error> { 338 | self.stack 339 | .find_with_ident(ident) 340 | .ok_or_else(|| Error::Undeclared(ident.to_string())) 341 | } 342 | } 343 | 344 | impl Default for Context { 345 | fn default() -> Self { 346 | Self::new() 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /packages/demo/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 35 | 54 | 72 | 86 | 108 | 109 | 119 | 120 | 133 | 134 | 135 | 137 | 138 | 139 | 140 | 142 | 313 | 314 | 315 | 316 | 317 | --------------------------------------------------------------------------------