├── .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 |
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 |
317 |
--------------------------------------------------------------------------------