├── .gitignore ├── mise.toml ├── www ├── .prettierignore ├── src │ ├── vite-env.d.ts │ ├── assets │ │ ├── examples │ │ │ ├── factorial.val │ │ │ ├── loop.val │ │ │ ├── newton.val │ │ │ ├── newton_sqrt.val │ │ │ ├── fibonacci.val │ │ │ ├── hoc.val │ │ │ ├── hof.val │ │ │ ├── primes.val │ │ │ ├── strings.val │ │ │ └── math.val │ │ └── react.svg │ ├── lib │ │ ├── utils.ts │ │ ├── types.ts │ │ ├── examples.ts │ │ └── highlight.ts │ ├── main.tsx │ ├── components │ │ ├── ui │ │ │ ├── sonner.tsx │ │ │ ├── label.tsx │ │ │ ├── switch.tsx │ │ │ ├── resizable.tsx │ │ │ ├── button.tsx │ │ │ ├── dialog.tsx │ │ │ └── select.tsx │ │ ├── ast-node.tsx │ │ ├── editor-settings-dialog.tsx │ │ └── editor.tsx │ ├── hooks │ │ └── use-persisted-state.tsx │ ├── providers │ │ └── editor-settings-provider.tsx │ ├── index.css │ └── App.tsx ├── packages │ └── val-wasm │ │ ├── val_bg.wasm │ │ ├── package.json │ │ ├── val_bg.wasm.d.ts │ │ ├── val.d.ts │ │ └── val.js ├── tsconfig.node.json ├── .gitignore ├── public │ └── icon.svg ├── vite.config.ts ├── .prettierrc ├── index.html ├── .eslintrc.cjs ├── components.json ├── tsconfig.json └── package.json ├── .gitattributes ├── tools └── example-generator │ ├── .python-version │ ├── justfile │ ├── pyproject.toml │ ├── main.py │ └── uv.lock ├── screenshot.png ├── examples ├── factorial.val ├── loop.val ├── newton.val ├── fibonacci.val ├── hof.val ├── primes.val ├── strings.val └── math.val ├── rustfmt.toml ├── .editorconfig ├── bin ├── forbid └── package ├── CONTRIBUTING ├── crates └── val-wasm │ ├── src │ ├── error.rs │ ├── range.rs │ ├── lib.rs │ └── ast_node.rs │ └── Cargo.toml ├── src ├── config.rs ├── consts.rs ├── function.rs ├── eval_result.rs ├── error.rs ├── main.rs ├── lib.rs ├── rounding_mode.rs ├── value.rs ├── float_ext.rs ├── ast.rs ├── arguments.rs └── highlighter.rs ├── Cargo.toml ├── .github └── workflows │ ├── web.yaml │ ├── ci.yaml │ └── release.yaml ├── GRAMMAR.txt ├── justfile ├── benches └── main.rs ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.DS_Store 2 | /target 3 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | python = "3.12" 3 | -------------------------------------------------------------------------------- /www/.prettierignore: -------------------------------------------------------------------------------- 1 | packages/val-wasm 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.val linguist-language=Rust 2 | -------------------------------------------------------------------------------- /tools/example-generator/.python-version: -------------------------------------------------------------------------------- 1 | 3.12.3 2 | -------------------------------------------------------------------------------- /www/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terror/val/HEAD/screenshot.png -------------------------------------------------------------------------------- /www/packages/val-wasm/val_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terror/val/HEAD/www/packages/val-wasm/val_bg.wasm -------------------------------------------------------------------------------- /examples/factorial.val: -------------------------------------------------------------------------------- 1 | fn factorial(n) { 2 | if (n <= 1) { 3 | return 1 4 | } else { 5 | return n * factorial(n - 1) 6 | } 7 | } 8 | 9 | println(factorial(5)); 10 | -------------------------------------------------------------------------------- /examples/loop.val: -------------------------------------------------------------------------------- 1 | sum = 0; i = 0 2 | 3 | loop { 4 | if (i >= 5) { 5 | break 6 | } 7 | 8 | sum = sum + i; i = i + 1 9 | } 10 | 11 | println('sum: ' + sum, 'i: ' + i) 12 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | imports_granularity = "One" 3 | max_width = 80 4 | newline_style = "Unix" 5 | tab_spaces = 2 6 | use_field_init_shorthand = true 7 | use_try_shorthand = true 8 | -------------------------------------------------------------------------------- /www/src/assets/examples/factorial.val: -------------------------------------------------------------------------------- 1 | fn factorial(n) { 2 | if (n <= 1) { 3 | return 1 4 | } else { 5 | return n * factorial(n - 1) 6 | } 7 | } 8 | 9 | println(factorial(5)); 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /www/src/assets/examples/loop.val: -------------------------------------------------------------------------------- 1 | sum = 0; i = 0 2 | 3 | loop { 4 | if (i >= 5) { 5 | break 6 | } 7 | 8 | sum = sum + i; i = i + 1 9 | } 10 | 11 | println('sum: ' + sum, 'i: ' + i) 12 | -------------------------------------------------------------------------------- /www/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /bin/forbid: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | which rg 6 | 7 | ! rg \ 8 | --color always \ 9 | --ignore-case \ 10 | --glob !bin/forbid \ 11 | --glob !www \ 12 | 'dbg!|fixme|todo|xxx' 13 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Unless you explicitly state otherwise, any contribution intentionally 5 | submitted for inclusion in the work by you shall be licensed as in 6 | LICENSE, without any additional terms or conditions. 7 | -------------------------------------------------------------------------------- /tools/example-generator/justfile: -------------------------------------------------------------------------------- 1 | set dotenv-load 2 | 3 | export EDITOR := 'nvim' 4 | 5 | alias f := fmt 6 | 7 | default: 8 | just --list 9 | 10 | [group: 'format'] 11 | fmt: 12 | uv run ruff check --select I --fix && uv run ruff format 13 | -------------------------------------------------------------------------------- /www/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /www/packages/val-wasm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "val-wasm", 3 | "version": "0.0.0", 4 | "files": [ 5 | "val_bg.wasm", 6 | "val.js", 7 | "val.d.ts" 8 | ], 9 | "module": "val.js", 10 | "types": "val.d.ts", 11 | "sideEffects": [ 12 | "./snippets/*" 13 | ] 14 | } -------------------------------------------------------------------------------- /crates/val-wasm/src/error.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Serialize)] 4 | pub enum ErrorKind { 5 | Evaluator, 6 | Parser, 7 | } 8 | 9 | #[derive(Clone, Debug, Serialize)] 10 | pub struct ValError { 11 | pub kind: ErrorKind, 12 | pub message: String, 13 | pub range: Range, 14 | } 15 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | !.vscode/extensions.json 2 | *.local 3 | *.log 4 | *.njsproj 5 | *.ntvs* 6 | *.sln 7 | *.suo 8 | *.sw? 9 | .DS_Store 10 | .idea 11 | .vscode/* 12 | dist 13 | dist-ssr 14 | lerna-debug.log* 15 | logs 16 | node_modules 17 | npm-debug.log* 18 | pnpm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug)] 2 | pub struct Config { 3 | pub precision: usize, 4 | pub rounding_mode: astro_float::RoundingMode, 5 | } 6 | 7 | impl Default for Config { 8 | fn default() -> Self { 9 | Self { 10 | precision: 1024, 11 | rounding_mode: astro_float::RoundingMode::ToEven, 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/consts.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | thread_local! { 4 | static CONSTS: RefCell = RefCell::new(Consts::new().expect("failed to initialize astro_float constants")); 5 | } 6 | 7 | pub(crate) fn with_consts(f: impl FnOnce(&mut Consts) -> T) -> T { 8 | CONSTS.with(|cell| { 9 | let mut consts = cell.borrow_mut(); 10 | f(&mut consts) 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /www/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type AstNode = { 2 | kind: string; 3 | range: Range; 4 | children: AstNode[]; 5 | }; 6 | 7 | export type Range = { 8 | start: number; 9 | end: number; 10 | }; 11 | 12 | export type ValErrorKind = 'Parser' | 'Evaluator'; 13 | 14 | export type ValError = { 15 | kind: ValErrorKind; 16 | message: string; 17 | range: Range; 18 | }; 19 | -------------------------------------------------------------------------------- /www/public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/val-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "val-wasm" 3 | version = "0.0.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | console_error_panic_hook = "0.1.7" 12 | serde = { version = "1.0.228", features = ["derive"] } 13 | serde-wasm-bindgen = "0.6.5" 14 | val = { path = "../.." } 15 | wasm-bindgen = "0.2.105" 16 | -------------------------------------------------------------------------------- /www/vite.config.ts: -------------------------------------------------------------------------------- 1 | import tailwindcss from '@tailwindcss/vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import path from 'path'; 4 | import { defineConfig } from 'vite'; 5 | 6 | export default defineConfig({ 7 | base: '/val', 8 | plugins: [react(), tailwindcss()], 9 | resolve: { 10 | alias: { 11 | '@': path.resolve(__dirname, './src'), 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /www/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "importOrder": ["^[./]"], 3 | "importOrderSeparation": true, 4 | "importOrderSortSpecifiers": true, 5 | "jsxSingleQuote": true, 6 | "plugins": [ 7 | "@trivago/prettier-plugin-sort-imports", 8 | "prettier-plugin-tailwindcss" 9 | ], 10 | "proseWrap": "always", 11 | "semi": true, 12 | "singleQuote": true, 13 | "tabWidth": 2, 14 | "trailingComma": "es5" 15 | } 16 | -------------------------------------------------------------------------------- /src/function.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub struct BuiltinFunctionPayload<'a> { 4 | pub arguments: Vec>, 5 | pub config: Config, 6 | pub span: Span, 7 | } 8 | 9 | pub type BuiltinFunction<'a> = 10 | fn(BuiltinFunctionPayload<'a>) -> Result, Error>; 11 | 12 | #[derive(Clone, Debug)] 13 | pub enum Function<'a> { 14 | Builtin(BuiltinFunction<'a>), 15 | UserDefined(Value<'a>), 16 | } 17 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | val 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /www/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react-hooks/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | plugins: ['react-refresh'], 11 | rules: { 12 | 'react-refresh/only-export-components': 'warn', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /examples/newton.val: -------------------------------------------------------------------------------- 1 | fn newton_sqrt(x) { 2 | if (x < 0) { 3 | println('Cannot compute square root of negative number') 4 | return 0 5 | } 6 | 7 | if (x == 0) { 8 | return 0 9 | } 10 | 11 | guess = x / 2 12 | epsilon = 0.0001 13 | 14 | while (abs(guess * guess - x) > epsilon) { 15 | guess = (guess + x / guess) / 2 16 | } 17 | 18 | return guess 19 | } 20 | 21 | println('Square root of 16 is approximately ' + newton_sqrt(16)) 22 | println('Square root of 2 is approximately ' + newton_sqrt(2)) 23 | -------------------------------------------------------------------------------- /www/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /www/src/assets/examples/newton.val: -------------------------------------------------------------------------------- 1 | fn newton_sqrt(x) { 2 | if (x < 0) { 3 | println('Cannot compute square root of negative number') 4 | return 0 5 | } 6 | 7 | if (x == 0) { 8 | return 0 9 | } 10 | 11 | guess = x / 2 12 | epsilon = 0.0001 13 | 14 | while (abs(guess * guess - x) > epsilon) { 15 | guess = (guess + x / guess) / 2 16 | } 17 | 18 | return guess 19 | } 20 | 21 | println('Square root of 16 is approximately ' + newton_sqrt(16)) 22 | println('Square root of 2 is approximately ' + newton_sqrt(2)) 23 | -------------------------------------------------------------------------------- /www/src/assets/examples/newton_sqrt.val: -------------------------------------------------------------------------------- 1 | fn newton_sqrt(x) { 2 | if (x < 0) { 3 | println('Cannot compute square root of negative number') 4 | return 0 5 | } 6 | 7 | if (x == 0) { 8 | return 0 9 | } 10 | 11 | guess = x / 2 12 | epsilon = 0.0001 13 | 14 | while (abs(guess * guess - x) > epsilon) { 15 | guess = (guess + x / guess) / 2 16 | } 17 | 18 | return guess 19 | } 20 | 21 | println('Square root of 16 is approximately ' + newton_sqrt(16)) 22 | println('Square root of 2 is approximately ' + newton_sqrt(2)) 23 | -------------------------------------------------------------------------------- /www/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster } from '@/components/ui/sonner'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom/client'; 4 | 5 | import App from './App.tsx'; 6 | import './index.css'; 7 | import { EditorSettingsProvider } from './providers/editor-settings-provider.tsx'; 8 | 9 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /tools/example-generator/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "example-generator" 3 | version = "0.0.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.12.3" 7 | dependencies = [ 8 | "arrg>=0.1.0", 9 | "ruff>=0.11.6", 10 | ] 11 | 12 | [tool.ruff] 13 | src = ["src"] 14 | indent-width = 2 15 | line-length = 100 16 | 17 | [tool.ruff.lint] 18 | select = ["E", "F", "I"] 19 | 20 | [tool.ruff.format] 21 | docstring-code-format = true 22 | docstring-code-line-length = 20 23 | indent-style = "space" 24 | quote-style = "single" 25 | -------------------------------------------------------------------------------- /examples/fibonacci.val: -------------------------------------------------------------------------------- 1 | fn fibonacci(n) { 2 | if (n <= 0) { 3 | println("Please enter a positive number") 4 | return 5 | } 6 | 7 | a = 0 8 | b = 1 9 | 10 | println("Fibonacci Sequence (first " + n + " numbers):") 11 | 12 | if (n >= 1) { 13 | println(a) 14 | } 15 | 16 | if (n >= 2) { 17 | println(b) 18 | } 19 | 20 | count = 2 21 | 22 | while (count < n) { 23 | next = a + b 24 | a = b 25 | b = next 26 | count = count + 1 27 | println(b) 28 | } 29 | } 30 | 31 | num = int(input("How many Fibonacci numbers would you like to see? ")) 32 | 33 | fibonacci(num) 34 | -------------------------------------------------------------------------------- /crates/val-wasm/src/range.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Serialize)] 4 | pub struct Range { 5 | pub start: u32, 6 | pub end: u32, 7 | } 8 | 9 | impl From for Range { 10 | fn from(span: val::Span) -> Self { 11 | let range = span.into_range(); 12 | 13 | Range { 14 | start: range.start as u32, 15 | end: range.end as u32, 16 | } 17 | } 18 | } 19 | 20 | impl From<&Span> for Range { 21 | fn from(span: &val::Span) -> Self { 22 | let range = span.into_range(); 23 | 24 | Range { 25 | start: range.start as u32, 26 | end: range.end as u32, 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /www/src/assets/examples/fibonacci.val: -------------------------------------------------------------------------------- 1 | fn fibonacci(n) { 2 | if (n <= 0) { 3 | println("Please enter a positive number") 4 | return 5 | } 6 | 7 | a = 0 8 | b = 1 9 | 10 | println("Fibonacci Sequence (first " + n + " numbers):") 11 | 12 | if (n >= 1) { 13 | println(a) 14 | } 15 | 16 | if (n >= 2) { 17 | println(b) 18 | } 19 | 20 | count = 2 21 | 22 | while (count < n) { 23 | next = a + b 24 | a = b 25 | b = next 26 | count = count + 1 27 | println(b) 28 | } 29 | } 30 | 31 | num = int(input("How many Fibonacci numbers would you like to see? ")) 32 | 33 | fibonacci(num) 34 | -------------------------------------------------------------------------------- /www/src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from 'next-themes'; 2 | import { Toaster as Sonner, ToasterProps } from 'sonner'; 3 | 4 | const Toaster = ({ ...props }: ToasterProps) => { 5 | const { theme = 'system' } = useTheme(); 6 | 7 | return ( 8 | 20 | ); 21 | }; 22 | 23 | export { Toaster }; 24 | -------------------------------------------------------------------------------- /src/eval_result.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub enum EvalResult<'a> { 4 | Break, 5 | Continue, 6 | Return(Value<'a>), 7 | Value(Value<'a>), 8 | } 9 | 10 | impl<'a> EvalResult<'a> { 11 | pub fn unwrap(&self) -> Value<'a> { 12 | match self { 13 | EvalResult::Value(v) | EvalResult::Return(v) => v.clone(), 14 | EvalResult::Break | EvalResult::Continue => Value::Null, 15 | } 16 | } 17 | 18 | pub fn is_return(&self) -> bool { 19 | matches!(self, EvalResult::Return(_)) 20 | } 21 | 22 | pub fn is_break(&self) -> bool { 23 | matches!(self, EvalResult::Break) 24 | } 25 | 26 | pub fn is_continue(&self) -> bool { 27 | matches!(self, EvalResult::Continue) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /www/packages/val-wasm/val_bg.wasm.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export const memory: WebAssembly.Memory; 4 | export const start: () => void; 5 | export const parse: (a: number, b: number) => [number, number, number]; 6 | export const evaluate: (a: number, b: number) => [number, number, number]; 7 | export const __wbindgen_free: (a: number, b: number, c: number) => void; 8 | export const __wbindgen_malloc: (a: number, b: number) => number; 9 | export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; 10 | export const __wbindgen_export_3: WebAssembly.Table; 11 | export const __externref_table_dealloc: (a: number) => void; 12 | export const __wbindgen_start: () => void; 13 | -------------------------------------------------------------------------------- /www/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowImportingTsExtensions": true, 4 | "baseUrl": ".", 5 | "isolatedModules": true, 6 | "jsx": "react-jsx", 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "module": "ESNext", 9 | "moduleResolution": "bundler", 10 | "noEmit": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "paths": { "@/*": ["./src/*"] }, 15 | "resolveJsonModule": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "target": "ES2020", 19 | "useDefineForClassFields": true 20 | }, 21 | "include": ["src"], 22 | "references": [{ "path": "./tsconfig.node.json" }] 23 | } 24 | -------------------------------------------------------------------------------- /www/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | import * as LabelPrimitive from '@radix-ui/react-label'; 5 | import * as React from 'react'; 6 | 7 | function Label({ 8 | className, 9 | ...props 10 | }: React.ComponentProps) { 11 | return ( 12 | 20 | ); 21 | } 22 | 23 | export { Label }; 24 | -------------------------------------------------------------------------------- /www/src/lib/examples.ts: -------------------------------------------------------------------------------- 1 | // This file is generated by `example-generator`. Do not edit manually. 2 | import factorial from '../assets/examples/factorial.val?raw'; 3 | import fibonacci from '../assets/examples/fibonacci.val?raw'; 4 | import hof from '../assets/examples/hof.val?raw'; 5 | import loop from '../assets/examples/loop.val?raw'; 6 | import math from '../assets/examples/math.val?raw'; 7 | import newton from '../assets/examples/newton.val?raw'; 8 | import primes from '../assets/examples/primes.val?raw'; 9 | import strings from '../assets/examples/strings.val?raw'; 10 | 11 | export const examples = { 12 | factorial: factorial, 13 | fibonacci: fibonacci, 14 | hof: hof, 15 | loop: loop, 16 | math: math, 17 | newton: newton, 18 | primes: primes, 19 | strings: strings, 20 | }; 21 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Debug, PartialEq)] 4 | pub struct Error { 5 | pub span: Span, 6 | pub message: String, 7 | } 8 | 9 | impl Error { 10 | pub fn new(span: Span, message: impl Into) -> Self { 11 | Self { 12 | span, 13 | message: message.into(), 14 | } 15 | } 16 | 17 | pub fn report<'a>(&self, id: &'a str) -> Report<'a, (&'a str, Range)> { 18 | let span_range = self.span.into_range(); 19 | 20 | let mut report = Report::build( 21 | ReportKind::Custom("error", Color::Red), 22 | (id, span_range.clone()), 23 | ) 24 | .with_message(&self.message); 25 | 26 | report = report.with_label( 27 | Label::new((id, span_range)) 28 | .with_message(&self.message) 29 | .with_color(Color::Red), 30 | ); 31 | 32 | report.finish() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/hof.val: -------------------------------------------------------------------------------- 1 | fn map(l, f) { 2 | i = 0 3 | 4 | result = [] 5 | 6 | while (i < len(l)) { 7 | result = append(result, f(l[i])) 8 | i = i + 1 9 | } 10 | 11 | return result 12 | } 13 | 14 | fn double(x) { 15 | return x * 2 16 | } 17 | 18 | fn even(x) { 19 | return x % 2 == 0 20 | } 21 | 22 | fn filter(l, f) { 23 | i = 0 24 | 25 | result = [] 26 | 27 | while (i < len(l)) { 28 | if (f(l[i])) { 29 | result = append(result, l[i]) 30 | } 31 | 32 | i = i + 1 33 | } 34 | 35 | return result 36 | } 37 | 38 | fn reduce(l, f, initial) { 39 | i = 0 40 | 41 | result = initial 42 | 43 | while (i < len(l)) { 44 | result = f(result, l[i]) 45 | i = i + 1 46 | } 47 | 48 | return result 49 | } 50 | 51 | fn sum(a, b) { 52 | return a + b 53 | } 54 | 55 | fn max(a, b) { 56 | if (a > b) { 57 | return a 58 | } else { 59 | return b 60 | } 61 | } 62 | 63 | l = [1, 2, 3, 4, 5] 64 | 65 | println(map(l, double)) 66 | println(filter(l, even)) 67 | println(reduce(l, sum, 0)) 68 | println(reduce(l, max, l[0])) 69 | -------------------------------------------------------------------------------- /www/src/assets/examples/hoc.val: -------------------------------------------------------------------------------- 1 | fn map(l, f) { 2 | i = 0 3 | 4 | result = [] 5 | 6 | while (i < len(l)) { 7 | result = append(result, f(l[i])) 8 | i = i + 1 9 | } 10 | 11 | return result 12 | } 13 | 14 | fn double(x) { 15 | return x * 2 16 | } 17 | 18 | fn even(x) { 19 | return x % 2 == 0 20 | } 21 | 22 | fn filter(l, f) { 23 | i = 0 24 | 25 | result = [] 26 | 27 | while (i < len(l)) { 28 | if (f(l[i])) { 29 | result = append(result, l[i]) 30 | } 31 | 32 | i = i + 1 33 | } 34 | 35 | return result 36 | } 37 | 38 | fn reduce(l, f, initial) { 39 | i = 0 40 | 41 | result = initial 42 | 43 | while (i < len(l)) { 44 | result = f(result, l[i]) 45 | i = i + 1 46 | } 47 | 48 | return result 49 | } 50 | 51 | fn sum(a, b) { 52 | return a + b 53 | } 54 | 55 | fn max(a, b) { 56 | if (a > b) { 57 | return a 58 | } else { 59 | return b 60 | } 61 | } 62 | 63 | l = [1, 2, 3, 4, 5] 64 | 65 | println(map(l, double)) 66 | println(filter(l, even)) 67 | println(reduce(l, sum, 0)) 68 | println(reduce(l, max, l[0])) 69 | -------------------------------------------------------------------------------- /www/src/assets/examples/hof.val: -------------------------------------------------------------------------------- 1 | fn map(l, f) { 2 | i = 0 3 | 4 | result = [] 5 | 6 | while (i < len(l)) { 7 | result = append(result, f(l[i])) 8 | i = i + 1 9 | } 10 | 11 | return result 12 | } 13 | 14 | fn double(x) { 15 | return x * 2 16 | } 17 | 18 | fn even(x) { 19 | return x % 2 == 0 20 | } 21 | 22 | fn filter(l, f) { 23 | i = 0 24 | 25 | result = [] 26 | 27 | while (i < len(l)) { 28 | if (f(l[i])) { 29 | result = append(result, l[i]) 30 | } 31 | 32 | i = i + 1 33 | } 34 | 35 | return result 36 | } 37 | 38 | fn reduce(l, f, initial) { 39 | i = 0 40 | 41 | result = initial 42 | 43 | while (i < len(l)) { 44 | result = f(result, l[i]) 45 | i = i + 1 46 | } 47 | 48 | return result 49 | } 50 | 51 | fn sum(a, b) { 52 | return a + b 53 | } 54 | 55 | fn max(a, b) { 56 | if (a > b) { 57 | return a 58 | } else { 59 | return b 60 | } 61 | } 62 | 63 | l = [1, 2, 3, 4, 5] 64 | 65 | println(map(l, double)) 66 | println(filter(l, even)) 67 | println(reduce(l, sum, 0)) 68 | println(reduce(l, max, l[0])) 69 | -------------------------------------------------------------------------------- /examples/primes.val: -------------------------------------------------------------------------------- 1 | fn is_prime(n) { 2 | if (n <= 1) { 3 | return false 4 | } 5 | 6 | if (n <= 3) { 7 | return true 8 | } 9 | 10 | if (n % 2 == 0 || n % 3 == 0) { 11 | return false 12 | } 13 | 14 | i = 5 15 | 16 | while (i * i <= n) { 17 | if (n % i == 0 || n % (i + 2) == 0) { 18 | return false 19 | } 20 | 21 | i = i + 6 22 | } 23 | 24 | return true 25 | } 26 | 27 | fn find_primes(start, end) { 28 | println("Prime numbers between " + start + " and " + end + ":") 29 | 30 | count = 0 31 | 32 | current = start 33 | 34 | while (current <= end) { 35 | if (is_prime(current)) { 36 | print(current + " ") 37 | 38 | count = count + 1 39 | 40 | if (count % 10 == 0) { 41 | println("") 42 | } 43 | } 44 | 45 | current = current + 1 46 | } 47 | 48 | if (count % 10 != 0) { 49 | println("") 50 | } 51 | 52 | println("Found " + count + " prime numbers.") 53 | } 54 | 55 | lower = int(input("Enter lower bound: ")) 56 | upper = int(input("Enter upper bound: ")) 57 | 58 | find_primes(lower, upper) 59 | -------------------------------------------------------------------------------- /www/src/assets/examples/primes.val: -------------------------------------------------------------------------------- 1 | fn is_prime(n) { 2 | if (n <= 1) { 3 | return false 4 | } 5 | 6 | if (n <= 3) { 7 | return true 8 | } 9 | 10 | if (n % 2 == 0 || n % 3 == 0) { 11 | return false 12 | } 13 | 14 | i = 5 15 | 16 | while (i * i <= n) { 17 | if (n % i == 0 || n % (i + 2) == 0) { 18 | return false 19 | } 20 | 21 | i = i + 6 22 | } 23 | 24 | return true 25 | } 26 | 27 | fn find_primes(start, end) { 28 | println("Prime numbers between " + start + " and " + end + ":") 29 | 30 | count = 0 31 | 32 | current = start 33 | 34 | while (current <= end) { 35 | if (is_prime(current)) { 36 | print(current + " ") 37 | 38 | count = count + 1 39 | 40 | if (count % 10 == 0) { 41 | println("") 42 | } 43 | } 44 | 45 | current = current + 1 46 | } 47 | 48 | if (count % 10 != 0) { 49 | println("") 50 | } 51 | 52 | println("Found " + count + " prime numbers.") 53 | } 54 | 55 | lower = int(input("Enter lower bound: ")) 56 | upper = int(input("Enter upper bound: ")) 57 | 58 | find_primes(lower, upper) 59 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "val" 3 | version = "0.3.6" 4 | authors = ["Liam "] 5 | categories = ["science", "parser-implementations", "command-line-interface"] 6 | description = "An arbitrary precision calculator language" 7 | edition = "2024" 8 | homepage = "https://github.com/terror/val" 9 | keywords = ["productivity", "compilers"] 10 | license = "CC0-1.0" 11 | repository = "https://github.com/terror/val" 12 | resolver = "2" 13 | 14 | [workspace] 15 | members = [".", "crates/*"] 16 | 17 | [[bench]] 18 | name = "main" 19 | harness = false 20 | 21 | [dependencies] 22 | anyhow = "1.0.100" 23 | ariadne = "0.5.1" 24 | astro-float = { version = "0.9.5", default-features = false, features = ["std"] } 25 | chumsky = "0.10.1" 26 | clap = { version = "4.5.51", features = ["derive"] } 27 | 28 | [target.'cfg(not(target_family = "wasm"))'.dependencies] 29 | dirs = "6.0.0" 30 | regex = "1.12.2" 31 | rustyline = "15.0.0" 32 | 33 | [dev-dependencies] 34 | criterion = { version = "0.5.1", features = ["html_reports"] } 35 | executable-path = "1.0.0" 36 | indoc = "2.0.7" 37 | pretty_assertions = "1.4.1" 38 | tempfile = "3.23.0" 39 | unindent = "0.2.4" 40 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use { 2 | clap::Parser, 3 | rustyline::error::ReadlineError, 4 | std::{backtrace::BacktraceStatus, process, thread}, 5 | val::arguments::Arguments, 6 | }; 7 | 8 | fn main() { 9 | let arguments = Arguments::parse(); 10 | 11 | let stack_size = arguments.stack_size * 1024 * 1024; 12 | 13 | let result = thread::Builder::new() 14 | .stack_size(stack_size) 15 | .spawn(move || arguments.run()) 16 | .unwrap() 17 | .join(); 18 | 19 | if let Err(error) = 20 | result.unwrap_or_else(|_| Err(anyhow::anyhow!("Thread panicked"))) 21 | { 22 | if let Some(&ReadlineError::Eof | &ReadlineError::Interrupted) = 23 | error.downcast_ref::() 24 | { 25 | return; 26 | } 27 | 28 | eprintln!("error: {error}"); 29 | 30 | for (i, error) in error.chain().skip(1).enumerate() { 31 | if i == 0 { 32 | eprintln!(); 33 | eprintln!("because:"); 34 | } 35 | 36 | eprintln!("- {error}"); 37 | } 38 | 39 | let backtrace = error.backtrace(); 40 | 41 | if backtrace.status() == BacktraceStatus::Captured { 42 | eprintln!("backtrace:"); 43 | eprintln!("{backtrace}"); 44 | } 45 | 46 | process::exit(1); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /www/src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import * as SwitchPrimitive from '@radix-ui/react-switch'; 3 | import * as React from 'react'; 4 | 5 | function Switch({ 6 | className, 7 | ...props 8 | }: React.ComponentProps) { 9 | return ( 10 | 18 | 24 | 25 | ); 26 | } 27 | 28 | export { Switch }; 29 | -------------------------------------------------------------------------------- /bin/package: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euxo pipefail 4 | 5 | VERSION=${REF#"refs/tags/"} 6 | DIST=$(pwd)/dist 7 | 8 | echo "Packaging val $VERSION for $TARGET..." 9 | 10 | test -f Cargo.lock || cargo generate-lockfile 11 | 12 | echo "Installing rust toolchain for $TARGET..." 13 | rustup target add "$TARGET" 14 | 15 | if [[ $TARGET == aarch64-unknown-linux-musl ]]; then 16 | export CC=aarch64-linux-gnu-gcc 17 | fi 18 | 19 | echo "Building val..." 20 | 21 | RUSTFLAGS="--deny warnings --codegen target-feature=+crt-static $TARGET_RUSTFLAGS" \ 22 | cargo build --bin val --target "$TARGET" --release 23 | 24 | EXECUTABLE=target/$TARGET/release/val 25 | 26 | if [[ $OS == windows-latest ]]; then 27 | EXECUTABLE=$EXECUTABLE.exe 28 | fi 29 | 30 | echo "Copying release files..." 31 | 32 | mkdir dist 33 | 34 | cp -r \ 35 | "$EXECUTABLE" \ 36 | Cargo.lock \ 37 | Cargo.toml \ 38 | LICENSE \ 39 | README.md \ 40 | "$DIST" 41 | 42 | cd "$DIST" 43 | 44 | echo "Creating release archive..." 45 | 46 | case $OS in 47 | ubuntu-latest | macos-latest) 48 | ARCHIVE=val-$VERSION-$TARGET.tar.gz 49 | tar czf "$ARCHIVE" ./* 50 | echo "archive=$DIST/$ARCHIVE" >> "$GITHUB_OUTPUT" 51 | ;; 52 | windows-latest) 53 | ARCHIVE=val-$VERSION-$TARGET.zip 54 | 7z a "$ARCHIVE" ./* 55 | echo "archive=$(pwd -W)/$ARCHIVE" >> "$GITHUB_OUTPUT" 56 | ;; 57 | esac 58 | -------------------------------------------------------------------------------- /.github/workflows/web.yaml: -------------------------------------------------------------------------------- 1 | name: Web 2 | 3 | on: 4 | push: 5 | branches: ['master'] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: 'pages' 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup Bun 26 | uses: oven-sh/setup-bun@v1 27 | with: 28 | bun-version: latest 29 | 30 | - name: Setup Pages 31 | uses: actions/configure-pages@v4 32 | 33 | - name: Install dependencies 34 | working-directory: ./www 35 | run: bun install 36 | 37 | - name: Test 38 | working-directory: ./www 39 | run: bun test --pass-with-no-tests 40 | 41 | - name: Build 42 | working-directory: ./www 43 | run: bun run build 44 | 45 | - name: Upload artifact 46 | uses: actions/upload-pages-artifact@v3 47 | with: 48 | path: './www/dist' 49 | 50 | deploy: 51 | environment: 52 | name: github-pages 53 | url: ${{ steps.deployment.outputs.page_url }} 54 | 55 | runs-on: ubuntu-latest 56 | 57 | needs: build 58 | 59 | steps: 60 | - name: Deploy to GitHub Pages 61 | id: deployment 62 | uses: actions/deploy-pages@v4 63 | -------------------------------------------------------------------------------- /www/src/hooks/use-persisted-state.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react'; 2 | 3 | export function usePersistedState( 4 | key: string, 5 | initialValue: T, 6 | options?: { 7 | serialize?: (value: T) => string; 8 | deserialize?: (value: string) => T; 9 | } 10 | ): [T, (action: Partial | ((prevState: T) => Partial)) => void] { 11 | const [state, setFullState] = useState(() => { 12 | const savedValue = localStorage.getItem(key); 13 | 14 | if (savedValue !== null) { 15 | try { 16 | return options?.deserialize 17 | ? options.deserialize(savedValue) 18 | : JSON.parse(savedValue); 19 | } catch (error) { 20 | console.warn(`Error reading ${key} from localStorage:`, error); 21 | return initialValue; 22 | } 23 | } 24 | 25 | return initialValue; 26 | }); 27 | 28 | useEffect(() => { 29 | try { 30 | localStorage.setItem( 31 | key, 32 | options?.serialize ? options.serialize(state) : JSON.stringify(state) 33 | ); 34 | } catch (error) { 35 | console.warn(`Error saving ${key} to localStorage:`, error); 36 | } 37 | }, [key, state, options]); 38 | 39 | const setState = useCallback( 40 | (action: Partial | ((prevState: T) => Partial)) => { 41 | setFullState((prevState) => ({ 42 | ...prevState, 43 | ...(typeof action === 'function' ? action(prevState) : action), 44 | })); 45 | }, 46 | [] 47 | ); 48 | 49 | return [state, setState]; 50 | } 51 | -------------------------------------------------------------------------------- /GRAMMAR.txt: -------------------------------------------------------------------------------- 1 | Program → Statement (';' Statement)* ';'? 2 | 3 | Statement → Assignment | Block | ExpressionStmt | Function | If | Return | While 4 | Assignment → Identifier '=' Expression 5 | Block → '{' Statement* '}' 6 | ExpressionStmt → Expression 7 | Function → 'fn' Identifier '(' (Identifier (',' Identifier)*)? ','? ')' '{' Statement* '}' 8 | If → 'if' '(' Expression ')' '{' Statement* '}' ('else' '{' Statement* '}')? 9 | Return → 'return' Expression? 10 | While → 'while' '(' Expression ')' '{' Statement* '}' 11 | 12 | Expression → LogicalOr 13 | LogicalOr → LogicalOr '||' LogicalAnd | LogicalAnd 14 | LogicalAnd → LogicalAnd '&&' Equality | Equality 15 | Equality → Equality ('==' | '!=') Relational | Relational 16 | Relational → Relational ('>=' | '<=' | '>' | '<') Sum | Sum 17 | Sum → Sum ('+' | '-') Product | Product 18 | Product → Product ('*' | '/' | '%' | '^') Unary | Unary 19 | Unary → ('-' | '!') Unary | ListAccess 20 | ListAccess → Atom ('[' Expression ']')* | Atom 21 | Atom → Number | Boolean | String | List | FunctionCall | Identifier | '(' Expression ')' 22 | 23 | Number → Digit+ ('.' Digit+)? 24 | Boolean → 'true' | 'false' 25 | String → '"' [^"]* '"' | "'" [^']* "'" 26 | List → '[' (Expression (',' Expression)*)? ','? ']' 27 | FunctionCall → Identifier '(' (Expression (',' Expression)*)? ','? ')' 28 | Identifier → Letter (Letter | Digit)* 29 | Digit → '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 30 | Letter → 'a' | 'b' | ... | 'z' | 'A' | 'B' | ... | 'Z' | '_' 31 | -------------------------------------------------------------------------------- /www/src/providers/editor-settings-provider.tsx: -------------------------------------------------------------------------------- 1 | import { usePersistedState } from '@/hooks/use-persisted-state'; 2 | import { ReactNode, createContext, useContext } from 'react'; 3 | 4 | export interface EditorSettings { 5 | fontSize: number; 6 | keybindings: 'default' | 'vim'; 7 | lineNumbers: boolean; 8 | lineWrapping: boolean; 9 | tabSize: number; 10 | } 11 | 12 | const defaultSettings: EditorSettings = { 13 | fontSize: 14, 14 | keybindings: 'default', 15 | lineNumbers: true, 16 | lineWrapping: true, 17 | tabSize: 2, 18 | }; 19 | 20 | type EditorSettingsContextType = { 21 | settings: EditorSettings; 22 | updateSettings: (settings: Partial) => void; 23 | }; 24 | 25 | const EditorSettingsContext = createContext< 26 | EditorSettingsContextType | undefined 27 | >(undefined); 28 | 29 | export const useEditorSettings = () => { 30 | const context = useContext(EditorSettingsContext); 31 | 32 | if (context === undefined) { 33 | throw new Error( 34 | 'useEditorSettings must be used within an EditorSettingsProvider' 35 | ); 36 | } 37 | 38 | return context; 39 | }; 40 | 41 | export const EditorSettingsProvider = ({ 42 | children, 43 | }: { 44 | children: ReactNode; 45 | }) => { 46 | const [settings, setSettings] = usePersistedState( 47 | 'editor-settings', 48 | defaultSettings 49 | ); 50 | 51 | const updateSettings = (newSettings: Partial) => { 52 | setSettings((prevSettings) => ({ ...prevSettings, ...newSettings })); 53 | }; 54 | 55 | return ( 56 | 57 | {children} 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | push: 8 | branches: 9 | - master 10 | 11 | defaults: 12 | run: 13 | shell: bash 14 | 15 | env: 16 | RUSTFLAGS: --deny warnings 17 | 18 | jobs: 19 | lint: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - uses: Swatinem/rust-cache@v2 26 | 27 | - name: Clippy 28 | run: cargo clippy --all --all-targets 29 | 30 | - name: Format 31 | run: cargo fmt --all -- --check 32 | 33 | - name: Install Dependencies 34 | run: | 35 | sudo apt-get update 36 | sudo apt-get install ripgrep shellcheck 37 | 38 | - name: Check for Forbidden Words 39 | run: ./bin/forbid 40 | 41 | - name: Check /bin scripts 42 | run: shellcheck bin/* 43 | 44 | msrv: 45 | runs-on: ubuntu-latest 46 | 47 | steps: 48 | - uses: actions/checkout@v4 49 | 50 | - uses: actions-rust-lang/setup-rust-toolchain@v1 51 | 52 | - uses: Swatinem/rust-cache@v2 53 | 54 | - name: Check 55 | run: cargo check 56 | 57 | test: 58 | strategy: 59 | matrix: 60 | os: 61 | - ubuntu-latest 62 | - macos-latest 63 | - windows-latest 64 | 65 | runs-on: ${{matrix.os}} 66 | 67 | steps: 68 | - uses: actions/checkout@v4 69 | 70 | - name: Remove Broken WSL bash executable 71 | if: ${{ matrix.os == 'windows-latest' }} 72 | shell: cmd 73 | run: | 74 | takeown /F C:\Windows\System32\bash.exe 75 | icacls C:\Windows\System32\bash.exe /grant administrators:F 76 | del C:\Windows\System32\bash.exe 77 | 78 | - uses: Swatinem/rust-cache@v2 79 | 80 | - name: Test 81 | run: cargo test --all 82 | -------------------------------------------------------------------------------- /examples/strings.val: -------------------------------------------------------------------------------- 1 | fn reverse_string(str) { 2 | n = len(str) 3 | 4 | chars = list(str) 5 | result = list(str) 6 | 7 | i = 0 8 | 9 | while (i < n) { 10 | result[i] = chars[n - i - 1] 11 | i = i + 1 12 | } 13 | 14 | return join(result, "") 15 | } 16 | 17 | fn is_palindrome(str) { 18 | n = len(str) 19 | 20 | chars = list(str) 21 | 22 | i = 0 23 | j = n - 1 24 | 25 | while (i < j) { 26 | if (chars[i] != chars[j]) { 27 | return false 28 | } 29 | 30 | i = i + 1 31 | j = j - 1 32 | } 33 | 34 | return true 35 | } 36 | 37 | fn count_words(text) { 38 | if (len(text) == 0) { 39 | return 0 40 | } 41 | 42 | words = split(text, ' ') 43 | 44 | return len(list(words)) 45 | } 46 | 47 | fn common_prefix(str1, str2) { 48 | chars1 = list(str1) 49 | chars2 = list(str2) 50 | 51 | len1 = len(str1) 52 | len2 = len(str2) 53 | 54 | max_check = len1 55 | 56 | if (len2 < len1) { 57 | max_check = len2 58 | } 59 | 60 | prefix_chars = [] 61 | 62 | i = 0 63 | 64 | while (i < max_check) { 65 | if (chars1[i] == chars2[i]) { 66 | prefix_chars = prefix_chars + [chars1[i]] 67 | } else { 68 | break 69 | } 70 | 71 | i = i + 1 72 | } 73 | 74 | return join(prefix_chars, "") 75 | } 76 | 77 | println("Testing string functions:") 78 | 79 | test_str = "racecar" 80 | println("Original: " + test_str) 81 | println("Reversed: " + reverse_string(test_str)) 82 | println("Is palindrome: " + is_palindrome(test_str)) 83 | 84 | test_str2 = "hello world" 85 | println("Original: " + test_str2) 86 | println("Reversed: " + reverse_string(test_str2)) 87 | println("Is palindrome: " + is_palindrome(test_str2)) 88 | println("Word count: " + count_words(test_str2)) 89 | 90 | str1 = "programming" 91 | str2 = "progress" 92 | println("Common prefix of '" + str1 + "' and '" + str2 + "': " + common_prefix(str1, str2)) 93 | -------------------------------------------------------------------------------- /www/packages/val-wasm/val.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export function start(): void; 4 | export function parse(input: string): any; 5 | export function evaluate(input: string): any; 6 | 7 | export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; 8 | 9 | export interface InitOutput { 10 | readonly memory: WebAssembly.Memory; 11 | readonly start: () => void; 12 | readonly parse: (a: number, b: number) => [number, number, number]; 13 | readonly evaluate: (a: number, b: number) => [number, number, number]; 14 | readonly __wbindgen_free: (a: number, b: number, c: number) => void; 15 | readonly __wbindgen_malloc: (a: number, b: number) => number; 16 | readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; 17 | readonly __wbindgen_export_3: WebAssembly.Table; 18 | readonly __externref_table_dealloc: (a: number) => void; 19 | readonly __wbindgen_start: () => void; 20 | } 21 | 22 | export type SyncInitInput = BufferSource | WebAssembly.Module; 23 | /** 24 | * Instantiates the given `module`, which can either be bytes or 25 | * a precompiled `WebAssembly.Module`. 26 | * 27 | * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. 28 | * 29 | * @returns {InitOutput} 30 | */ 31 | export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; 32 | 33 | /** 34 | * If `module_or_path` is {RequestInfo} or {URL}, makes a request and 35 | * for everything else, calls `WebAssembly.instantiate` directly. 36 | * 37 | * @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. 38 | * 39 | * @returns {Promise} 40 | */ 41 | export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; 42 | -------------------------------------------------------------------------------- /www/src/assets/examples/strings.val: -------------------------------------------------------------------------------- 1 | fn reverse_string(str) { 2 | n = len(str) 3 | 4 | chars = list(str) 5 | result = list(str) 6 | 7 | i = 0 8 | 9 | while (i < n) { 10 | result[i] = chars[n - i - 1] 11 | i = i + 1 12 | } 13 | 14 | return join(result, "") 15 | } 16 | 17 | fn is_palindrome(str) { 18 | n = len(str) 19 | 20 | chars = list(str) 21 | 22 | i = 0 23 | j = n - 1 24 | 25 | while (i < j) { 26 | if (chars[i] != chars[j]) { 27 | return false 28 | } 29 | 30 | i = i + 1 31 | j = j - 1 32 | } 33 | 34 | return true 35 | } 36 | 37 | fn count_words(text) { 38 | if (len(text) == 0) { 39 | return 0 40 | } 41 | 42 | words = split(text, ' ') 43 | 44 | return len(list(words)) 45 | } 46 | 47 | fn common_prefix(str1, str2) { 48 | chars1 = list(str1) 49 | chars2 = list(str2) 50 | 51 | len1 = len(str1) 52 | len2 = len(str2) 53 | 54 | max_check = len1 55 | 56 | if (len2 < len1) { 57 | max_check = len2 58 | } 59 | 60 | prefix_chars = [] 61 | 62 | i = 0 63 | 64 | while (i < max_check) { 65 | if (chars1[i] == chars2[i]) { 66 | prefix_chars = prefix_chars + [chars1[i]] 67 | } else { 68 | break 69 | } 70 | 71 | i = i + 1 72 | } 73 | 74 | return join(prefix_chars, "") 75 | } 76 | 77 | println("Testing string functions:") 78 | 79 | test_str = "racecar" 80 | println("Original: " + test_str) 81 | println("Reversed: " + reverse_string(test_str)) 82 | println("Is palindrome: " + is_palindrome(test_str)) 83 | 84 | test_str2 = "hello world" 85 | println("Original: " + test_str2) 86 | println("Reversed: " + reverse_string(test_str2)) 87 | println("Is palindrome: " + is_palindrome(test_str2)) 88 | println("Word count: " + count_words(test_str2)) 89 | 90 | str1 = "programming" 91 | str2 = "progress" 92 | println("Common prefix of '" + str1 + "' and '" + str2 + "': " + common_prefix(str1, str2)) 93 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod consts; 2 | 3 | pub(crate) use { 4 | crate::consts::with_consts, 5 | ariadne::{Color, Label, Report, ReportKind, Source}, 6 | astro_float::{BigFloat as Float, Consts, Radix, Sign}, 7 | chumsky::prelude::*, 8 | clap::Parser as Clap, 9 | std::{ 10 | cell::RefCell, 11 | collections::HashMap, 12 | fmt::{self, Display, Formatter}, 13 | fs, 14 | ops::Range, 15 | path::PathBuf, 16 | process, 17 | }, 18 | }; 19 | 20 | #[cfg(not(target_family = "wasm"))] 21 | pub(crate) use { 22 | crate::highlighter::Highlighter, 23 | regex::Regex, 24 | rustyline::{ 25 | Context, Editor, Helper, 26 | completion::{Completer, FilenameCompleter, Pair}, 27 | config::{Builder, ColorMode, CompletionType, EditMode}, 28 | error::ReadlineError, 29 | highlight::{CmdKind, Highlighter as RustylineHighlighter}, 30 | hint::{Hinter, HistoryHinter}, 31 | history::DefaultHistory, 32 | validate::Validator, 33 | }, 34 | std::borrow::Cow::{self, Owned}, 35 | }; 36 | 37 | pub use crate::{ 38 | arguments::Arguments, 39 | ast::{BinaryOp, Expression, Program, Statement, UnaryOp}, 40 | config::Config, 41 | environment::Environment, 42 | error::Error, 43 | eval_result::EvalResult, 44 | evaluator::Evaluator, 45 | float_ext::FloatExt, 46 | function::{BuiltinFunction, BuiltinFunctionPayload, Function}, 47 | parser::parse, 48 | rounding_mode::RoundingMode, 49 | value::Value, 50 | }; 51 | 52 | pub type Span = SimpleSpan; 53 | 54 | type Result = std::result::Result; 55 | type Spanned = (T, Span); 56 | 57 | #[doc(hidden)] 58 | pub mod arguments; 59 | 60 | #[cfg(not(target_family = "wasm"))] 61 | mod highlighter; 62 | 63 | mod ast; 64 | mod config; 65 | mod environment; 66 | mod error; 67 | mod eval_result; 68 | mod evaluator; 69 | mod float_ext; 70 | mod function; 71 | mod parser; 72 | mod rounding_mode; 73 | mod value; 74 | -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "www", 3 | "private": true, 4 | "version": "0.0.0", 5 | "workspaces": [ 6 | "packages/*" 7 | ], 8 | "type": "module", 9 | "scripts": { 10 | "dev": "vite", 11 | "build": "tsc && vite build", 12 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 13 | "preview": "vite preview" 14 | }, 15 | "dependencies": { 16 | "@codemirror/lang-rust": "^6.0.1", 17 | "@codemirror/language": "^6.11.3", 18 | "@codemirror/lint": "^6.9.1", 19 | "@codemirror/state": "^6.5.2", 20 | "@codemirror/view": "^6.38.6", 21 | "@radix-ui/react-dialog": "^1.1.7", 22 | "@radix-ui/react-label": "^2.1.3", 23 | "@radix-ui/react-select": "^2.1.7", 24 | "@radix-ui/react-slot": "^1.2.0", 25 | "@radix-ui/react-switch": "^1.1.4", 26 | "@replit/codemirror-vim": "^6.3.0", 27 | "@tailwindcss/vite": "^4.1.3", 28 | "@uiw/react-codemirror": "^4.23.10", 29 | "class-variance-authority": "^0.7.1", 30 | "clsx": "^2.1.1", 31 | "install": "^0.13.0", 32 | "lucide-react": "^0.487.0", 33 | "next-themes": "^0.4.6", 34 | "react": "^18.2.0", 35 | "react-dom": "^18.2.0", 36 | "react-resizable-panels": "^2.1.7", 37 | "sonner": "^2.0.3", 38 | "tailwind-merge": "^3.2.0", 39 | "tailwindcss": "^4.1.3", 40 | "tw-animate-css": "^1.2.5", 41 | "val-wasm": "workspace:*" 42 | }, 43 | "devDependencies": { 44 | "@trivago/prettier-plugin-sort-imports": "^5.2.2", 45 | "@types/node": "^22.14.1", 46 | "@types/react": "^18.0.37", 47 | "@types/react-dom": "^18.0.11", 48 | "@typescript-eslint/eslint-plugin": "^5.59.0", 49 | "@typescript-eslint/parser": "^5.59.0", 50 | "@vitejs/plugin-react": "^4.0.0", 51 | "eslint": "^8.38.0", 52 | "eslint-plugin-react-hooks": "^4.6.0", 53 | "eslint-plugin-react-refresh": "^0.3.4", 54 | "prettier-plugin-tailwindcss": "^0.6.11", 55 | "typescript": "^5.0.2", 56 | "vite": "^4.3.9" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /www/src/lib/highlight.ts: -------------------------------------------------------------------------------- 1 | import { StateEffect, Transaction } from '@codemirror/state'; 2 | import { 3 | Decoration, 4 | DecorationSet, 5 | ViewPlugin, 6 | ViewUpdate, 7 | } from '@codemirror/view'; 8 | 9 | const highlightMark = Decoration.mark({ class: 'cm-highlighted-node' }); 10 | 11 | export const addHighlightEffect = StateEffect.define<{ 12 | from: number; 13 | to: number; 14 | }>(); 15 | 16 | export const removeHighlightEffect = StateEffect.define(); 17 | 18 | export const highlightExtension = ViewPlugin.fromClass( 19 | class { 20 | decorations: DecorationSet; 21 | 22 | constructor() { 23 | this.decorations = Decoration.none; 24 | } 25 | 26 | update(update: ViewUpdate) { 27 | const effects = update.transactions 28 | .flatMap((tr: Transaction) => tr.effects) 29 | .filter((e: StateEffect) => 30 | e.is(addHighlightEffect) || e.is(removeHighlightEffect) 31 | ); 32 | 33 | if (!effects.length) return; 34 | 35 | for (const effect of effects) { 36 | if (effect.is(addHighlightEffect)) { 37 | const { from, to } = effect.value; 38 | 39 | const doc = update.state.doc; 40 | 41 | let effectiveTo = to; 42 | 43 | for (let pos = to - 1; pos >= from; pos--) { 44 | const char = doc.sliceString(pos, pos + 1); 45 | 46 | if (!/\s/.test(char)) { 47 | effectiveTo = pos + 1; 48 | break; 49 | } 50 | } 51 | 52 | if (effectiveTo > from) { 53 | this.decorations = Decoration.set([ 54 | highlightMark.range(from, effectiveTo), 55 | ]); 56 | } else { 57 | this.decorations = Decoration.none; 58 | } 59 | } else if (effect.is(removeHighlightEffect)) { 60 | this.decorations = Decoration.none; 61 | } 62 | } 63 | } 64 | }, 65 | { 66 | decorations: (v: { decorations: DecorationSet }) => v.decorations, 67 | } 68 | ); 69 | -------------------------------------------------------------------------------- /crates/val-wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | ast_node::AstNode, 4 | error::{ErrorKind, ValError}, 5 | range::Range, 6 | }, 7 | serde::Serialize, 8 | serde_wasm_bindgen::to_value, 9 | val::{ 10 | Environment, Evaluator, Expression, Program, RoundingMode, Span, Statement, 11 | }, 12 | wasm_bindgen::prelude::*, 13 | }; 14 | 15 | mod ast_node; 16 | mod error; 17 | mod range; 18 | 19 | #[wasm_bindgen(start)] 20 | fn start() { 21 | console_error_panic_hook::set_once(); 22 | } 23 | 24 | #[wasm_bindgen] 25 | pub fn parse(input: &str) -> Result { 26 | match val::parse(input) { 27 | Ok((ast, span)) => Ok(to_value(&AstNode::from((&ast, &span))).unwrap()), 28 | Err(errors) => Err( 29 | to_value( 30 | &errors 31 | .into_iter() 32 | .map(|error| ValError { 33 | kind: ErrorKind::Parser, 34 | message: error.message, 35 | range: Range::from(error.span), 36 | }) 37 | .collect::>(), 38 | ) 39 | .unwrap(), 40 | ), 41 | } 42 | } 43 | 44 | #[wasm_bindgen] 45 | pub fn evaluate(input: &str) -> Result { 46 | match val::parse(input) { 47 | Ok(ast) => { 48 | let mut evaluator = Evaluator::from(Environment::new(val::Config { 49 | precision: 53, 50 | rounding_mode: RoundingMode::FromZero.into(), 51 | })); 52 | 53 | match evaluator.eval(&ast) { 54 | Ok(value) => Ok(to_value(&value.to_string()).unwrap()), 55 | Err(error) => Err( 56 | to_value(&[ValError { 57 | kind: ErrorKind::Evaluator, 58 | message: error.message, 59 | range: Range::from(error.span), 60 | }]) 61 | .unwrap(), 62 | ), 63 | } 64 | } 65 | Err(errors) => Err( 66 | to_value( 67 | &errors 68 | .into_iter() 69 | .map(|error| ValError { 70 | kind: ErrorKind::Parser, 71 | message: error.message, 72 | range: Range::from(error.span), 73 | }) 74 | .collect::>(), 75 | ) 76 | .unwrap(), 77 | ), 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /examples/math.val: -------------------------------------------------------------------------------- 1 | println("π (pi): " + pi) 2 | println("e: " + e) 3 | println("τ (tau): " + tau) 4 | println("φ (phi): " + phi) 5 | 6 | a = 7 7 | b = 3 8 | 9 | println(a + " + " + b + " = " + (a + b)) 10 | println(a + " - " + b + " = " + (a - b)) 11 | println(a + " * " + b + " = " + (a * b)) 12 | println(a + " / " + b + " = " + (a / b)) 13 | println(a + " % " + b + " = " + (a % b)) 14 | println(a + " ^ " + b + " = " + (a ^ b)) 15 | 16 | angle = pi / 4 17 | 18 | println("Angle: " + angle + " radians") 19 | println("sin(" + angle + ") = " + sin(angle)) 20 | println("cos(" + angle + ") = " + cos(angle)) 21 | println("tan(" + angle + ") = " + tan(angle)) 22 | println("csc(" + angle + ") = " + csc(angle)) 23 | println("sec(" + angle + ") = " + sec(angle)) 24 | println("cot(" + angle + ") = " + cot(angle)) 25 | 26 | value = 0.5 27 | 28 | println("asin(" + value + ") = " + asin(value)) 29 | println("acos(" + value + ") = " + acos(value)) 30 | println("arc(" + value + ") = " + arc(value)) 31 | println("acsc(" + 1.5 + ") = " + acsc(1.5)) 32 | println("asec(" + 1.5 + ") = " + asec(1.5)) 33 | println("acot(" + value + ") = " + acot(value)) 34 | 35 | println("sinh(" + value + ") = " + sinh(value)) 36 | println("cosh(" + value + ") = " + cosh(value)) 37 | println("tanh(" + value + ") = " + tanh(value)) 38 | 39 | number = 10 40 | 41 | println("ln(" + number + ") = " + ln(number)) 42 | println("log2(" + number + ") = " + log2(number)) 43 | println("log10(" + number + ") = " + log10(number)) 44 | 45 | println("e(" + 2 + ") = " + e(2)) 46 | 47 | println("sqrt(" + number + ") = " + sqrt(number)) 48 | println("sqrt(2) = " + sqrt(2)) 49 | println("abs(-10) = " + abs(-10)) 50 | 51 | decimal = 3.7 52 | 53 | println("ceil(" + decimal + ") = " + ceil(decimal)) 54 | println("floor(" + decimal + ") = " + floor(decimal)) 55 | 56 | x = 48 57 | y = 18 58 | 59 | println("gcd(" + x + ", " + y + ") = " + gcd(x, y)) 60 | println("lcm(" + x + ", " + y + ") = " + lcm(x, y)) 61 | 62 | println("sin(π/2) = " + sin(pi/2) + " (should be 1)") 63 | println("cos(π) = " + cos(pi) + " (should be -1)") 64 | println("sin²(x) + cos²(x) = " + (sin(angle)^2 + cos(angle)^2) + " (should be 1)") 65 | println("e^(ln(5)) = " + e(ln(5)) + " (should be 5)") 66 | -------------------------------------------------------------------------------- /www/src/assets/examples/math.val: -------------------------------------------------------------------------------- 1 | println("π (pi): " + pi) 2 | println("e: " + e) 3 | println("τ (tau): " + tau) 4 | println("φ (phi): " + phi) 5 | 6 | a = 7 7 | b = 3 8 | 9 | println(a + " + " + b + " = " + (a + b)) 10 | println(a + " - " + b + " = " + (a - b)) 11 | println(a + " * " + b + " = " + (a * b)) 12 | println(a + " / " + b + " = " + (a / b)) 13 | println(a + " % " + b + " = " + (a % b)) 14 | println(a + " ^ " + b + " = " + (a ^ b)) 15 | 16 | angle = pi / 4 17 | 18 | println("Angle: " + angle + " radians") 19 | println("sin(" + angle + ") = " + sin(angle)) 20 | println("cos(" + angle + ") = " + cos(angle)) 21 | println("tan(" + angle + ") = " + tan(angle)) 22 | println("csc(" + angle + ") = " + csc(angle)) 23 | println("sec(" + angle + ") = " + sec(angle)) 24 | println("cot(" + angle + ") = " + cot(angle)) 25 | 26 | value = 0.5 27 | 28 | println("asin(" + value + ") = " + asin(value)) 29 | println("acos(" + value + ") = " + acos(value)) 30 | println("arc(" + value + ") = " + arc(value)) 31 | println("acsc(" + 1.5 + ") = " + acsc(1.5)) 32 | println("asec(" + 1.5 + ") = " + asec(1.5)) 33 | println("acot(" + value + ") = " + acot(value)) 34 | 35 | println("sinh(" + value + ") = " + sinh(value)) 36 | println("cosh(" + value + ") = " + cosh(value)) 37 | println("tanh(" + value + ") = " + tanh(value)) 38 | 39 | number = 10 40 | 41 | println("ln(" + number + ") = " + ln(number)) 42 | println("log2(" + number + ") = " + log2(number)) 43 | println("log10(" + number + ") = " + log10(number)) 44 | 45 | println("e(" + 2 + ") = " + e(2)) 46 | 47 | println("sqrt(" + number + ") = " + sqrt(number)) 48 | println("sqrt(2) = " + sqrt(2)) 49 | println("abs(-10) = " + abs(-10)) 50 | 51 | decimal = 3.7 52 | 53 | println("ceil(" + decimal + ") = " + ceil(decimal)) 54 | println("floor(" + decimal + ") = " + floor(decimal)) 55 | 56 | x = 48 57 | y = 18 58 | 59 | println("gcd(" + x + ", " + y + ") = " + gcd(x, y)) 60 | println("lcm(" + x + ", " + y + ") = " + lcm(x, y)) 61 | 62 | println("sin(π/2) = " + sin(pi/2) + " (should be 1)") 63 | println("cos(π) = " + cos(pi) + " (should be -1)") 64 | println("sin²(x) + cos²(x) = " + (sin(angle)^2 + cos(angle)^2) + " (should be 1)") 65 | println("e^(ln(5)) = " + e(ln(5)) + " (should be 5)") 66 | -------------------------------------------------------------------------------- /src/rounding_mode.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Eq, PartialEq, Debug, Copy, Clone)] 4 | pub enum RoundingMode { 5 | None = 1, 6 | Up = 2, 7 | Down = 4, 8 | ToZero = 8, 9 | FromZero = 16, 10 | ToEven = 32, 11 | ToOdd = 64, 12 | } 13 | 14 | impl std::fmt::Display for RoundingMode { 15 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 | let s = match self { 17 | RoundingMode::None => "none", 18 | RoundingMode::Up => "up", 19 | RoundingMode::Down => "down", 20 | RoundingMode::ToZero => "to-zero", 21 | RoundingMode::FromZero => "from-zero", 22 | RoundingMode::ToEven => "to-even", 23 | RoundingMode::ToOdd => "to-odd", 24 | }; 25 | write!(f, "{}", s) 26 | } 27 | } 28 | 29 | impl std::str::FromStr for RoundingMode { 30 | type Err = String; 31 | 32 | fn from_str(s: &str) -> Result { 33 | match s.to_lowercase().as_str() { 34 | "none" => Ok(RoundingMode::None), 35 | "up" => Ok(RoundingMode::Up), 36 | "down" => Ok(RoundingMode::Down), 37 | "tozero" | "to_zero" | "to-zero" | "toward_zero" | "toward-zero" => { 38 | Ok(RoundingMode::ToZero) 39 | } 40 | "fromzero" | "from_zero" | "from-zero" | "away_from_zero" 41 | | "away-from-zero" => Ok(RoundingMode::FromZero), 42 | "toeven" | "to_even" | "to-even" | "nearest_even" | "bankers" => { 43 | Ok(RoundingMode::ToEven) 44 | } 45 | "toodd" | "to_odd" | "to-odd" | "nearest_odd" => Ok(RoundingMode::ToOdd), 46 | _ => Err(format!("Unknown rounding mode: {}", s)), 47 | } 48 | } 49 | } 50 | 51 | impl From for astro_float::RoundingMode { 52 | fn from(mode: RoundingMode) -> Self { 53 | match mode { 54 | RoundingMode::None => astro_float::RoundingMode::None, 55 | RoundingMode::Up => astro_float::RoundingMode::Up, 56 | RoundingMode::Down => astro_float::RoundingMode::Down, 57 | RoundingMode::ToZero => astro_float::RoundingMode::ToZero, 58 | RoundingMode::FromZero => astro_float::RoundingMode::FromZero, 59 | RoundingMode::ToEven => astro_float::RoundingMode::ToEven, 60 | RoundingMode::ToOdd => astro_float::RoundingMode::ToOdd, 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /www/src/components/ui/resizable.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import { GripVerticalIcon } from 'lucide-react'; 3 | import * as React from 'react'; 4 | import * as ResizablePrimitive from 'react-resizable-panels'; 5 | 6 | function ResizablePanelGroup({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ); 20 | } 21 | 22 | function ResizablePanel({ 23 | ...props 24 | }: React.ComponentProps) { 25 | return ; 26 | } 27 | 28 | function ResizableHandle({ 29 | withHandle, 30 | className, 31 | ...props 32 | }: React.ComponentProps & { 33 | withHandle?: boolean; 34 | }) { 35 | return ( 36 | div]:rotate-90', 40 | className 41 | )} 42 | {...props} 43 | > 44 | {withHandle && ( 45 |
46 | 47 |
48 | )} 49 |
50 | ); 51 | } 52 | 53 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; 54 | -------------------------------------------------------------------------------- /www/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import { Slot } from '@radix-ui/react-slot'; 3 | import { type VariantProps, cva } from 'class-variance-authority'; 4 | import * as React from 'react'; 5 | 6 | const buttonVariants = cva( 7 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', 13 | destructive: 14 | 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', 15 | outline: 16 | 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', 17 | secondary: 18 | 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', 19 | ghost: 20 | 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', 21 | link: 'text-primary underline-offset-4 hover:underline', 22 | }, 23 | size: { 24 | default: 'h-9 px-4 py-2 has-[>svg]:px-3', 25 | sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', 26 | lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', 27 | icon: 'size-9', 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: 'default', 32 | size: 'default', 33 | }, 34 | } 35 | ); 36 | 37 | function Button({ 38 | className, 39 | variant, 40 | size, 41 | asChild = false, 42 | ...props 43 | }: React.ComponentProps<'button'> & 44 | VariantProps & { 45 | asChild?: boolean; 46 | }) { 47 | const Comp = asChild ? Slot : 'button'; 48 | 49 | return ( 50 | 55 | ); 56 | } 57 | 58 | export { Button, buttonVariants }; 59 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set dotenv-load 2 | 3 | default: 4 | just --list 5 | 6 | alias f := fmt 7 | alias r := run 8 | alias t := test 9 | 10 | all: build test clippy fmt-check 11 | 12 | [group: 'dev'] 13 | bench: 14 | cargo bench 15 | open target/criterion/report/index.html 16 | 17 | [group: 'dev'] 18 | build: 19 | cargo build --all --all-targets 20 | 21 | [group: 'web'] 22 | build-wasm: 23 | wasm-pack build crates/val-wasm --target web \ 24 | --out-name val \ 25 | --out-dir ../../www/packages/val-wasm 26 | 27 | rm -rf www/packages/val-wasm/.gitignore 28 | 29 | uv run --with arrg tools/example-generator/main.py examples www/src/lib/examples.ts 30 | 31 | [group: 'check'] 32 | check: 33 | cargo check 34 | 35 | [group: 'check'] 36 | ci: test clippy forbid 37 | cargo +nightly fmt --all -- --check 38 | cargo update --locked --package val 39 | 40 | [group: 'check'] 41 | clippy: 42 | cargo clippy --all --all-targets 43 | 44 | [group: 'format'] 45 | fmt: 46 | cargo +nightly fmt 47 | 48 | [group: 'format'] 49 | fmt-check: 50 | cargo +nightly fmt --all -- --check 51 | 52 | [group: 'format'] 53 | fmt-web: 54 | cd www && prettier --write . 55 | 56 | [group: 'check'] 57 | forbid: 58 | ./bin/forbid 59 | 60 | [group: 'misc'] 61 | install: 62 | cargo install -f val 63 | 64 | [group: 'dev'] 65 | install-dev-deps: 66 | rustup install nightly 67 | rustup update nightly 68 | cargo install cargo-watch 69 | cargo install wasm-bindgen-cli 70 | curl -fsSL https://bun.sh/install | bash 71 | 72 | [group: 'release'] 73 | publish: 74 | #!/usr/bin/env bash 75 | set -euxo pipefail 76 | rm -rf tmp/release 77 | gh repo clone https://github.com/terror/val tmp/release 78 | cd tmp/release 79 | VERSION=`sed -En 's/version[[:space:]]*=[[:space:]]*"([^"]+)"/\1/p' Cargo.toml | head -1` 80 | git tag -a $VERSION -m "Release $VERSION" 81 | git push origin $VERSION 82 | cargo publish 83 | cd ../.. 84 | rm -rf tmp/release 85 | 86 | [group: 'misc'] 87 | readme: 88 | present --in-place README.md 89 | 90 | [group: 'dev'] 91 | run *args: 92 | cargo run {{ args }} 93 | 94 | [group: 'web'] 95 | serve-web: build-wasm 96 | cd www && bun run dev 97 | 98 | [group: 'test'] 99 | test: 100 | cargo test 101 | 102 | [group: 'test'] 103 | test-release-workflow: 104 | -git tag -d test-release 105 | -git push origin :test-release 106 | git tag test-release 107 | git push origin test-release 108 | 109 | [group: 'release'] 110 | update-changelog: 111 | echo >> CHANGELOG.md 112 | git log --pretty='format:- %s' >> CHANGELOG.md 113 | 114 | [group: 'dev'] 115 | watch +COMMAND='test': 116 | cargo watch --clear --exec "{{COMMAND}}" 117 | -------------------------------------------------------------------------------- /benches/main.rs: -------------------------------------------------------------------------------- 1 | use { 2 | criterion::{Criterion, black_box, criterion_group, criterion_main}, 3 | val::{Environment, Evaluator}, 4 | }; 5 | 6 | fn bench_increment_value(criterion: &mut Criterion) { 7 | let mut group = criterion.benchmark_group("increment_value"); 8 | 9 | for &number in &[10_u32, 50, 100, 500] { 10 | let program = 11 | format!("a = 0.001; while (a < {number}) {{ a = a + 0.001 }}; a"); 12 | 13 | let ast = val::parse(&program).unwrap(); 14 | 15 | group.bench_function(format!("n = {number}"), |bencher| { 16 | bencher.iter(|| { 17 | black_box(Evaluator::from(Environment::default()).eval(&ast)).unwrap(); 18 | }) 19 | }); 20 | } 21 | 22 | group.finish(); 23 | } 24 | 25 | fn bench_prime_count(criterion: &mut Criterion) { 26 | let mut group = criterion.benchmark_group("prime_count"); 27 | 28 | for &number in &[5_000_u32, 10_000_u32] { 29 | let program = format!( 30 | r#" 31 | fn prime(n) {{ 32 | if (n < 2) {{ 33 | return false 34 | }} 35 | 36 | i = 2 37 | 38 | while (i * i <= n) {{ 39 | if (n % i == 0) {{ 40 | return false 41 | }} 42 | 43 | i = i + 1 44 | }} 45 | 46 | return true 47 | }} 48 | 49 | fn count(limit) {{ 50 | count = 0 51 | 52 | i = 2 53 | 54 | while (i <= limit) {{ 55 | if (prime(i)) {{ 56 | count = count + 1 57 | }} 58 | 59 | i = i + 1 60 | }} 61 | 62 | return count 63 | }} 64 | 65 | count({number}) 66 | "# 67 | ); 68 | 69 | let ast = val::parse(&program).unwrap(); 70 | 71 | group.bench_function(format!("n = {number}"), |bencher| { 72 | bencher.iter(|| { 73 | black_box(Evaluator::from(Environment::default()).eval(&ast)).unwrap(); 74 | }); 75 | }); 76 | } 77 | 78 | group.finish(); 79 | } 80 | 81 | fn bench_recursive_factorial(criterion: &mut Criterion) { 82 | let mut group = criterion.benchmark_group("recursive_factorial"); 83 | 84 | for &number in &[10_u32, 50, 100, 500] { 85 | let program = format!( 86 | "fn f(x) {{ if ( x <= 1) {{ return 1 }} else {{ return x * f(x - 1) }} }} f({number})" 87 | ); 88 | 89 | let ast = val::parse(&program).unwrap(); 90 | 91 | group.bench_function(format!("n = {number}"), |bencher| { 92 | bencher.iter(|| { 93 | black_box(Evaluator::from(Environment::default()).eval(&ast)).unwrap(); 94 | }) 95 | }); 96 | } 97 | 98 | group.finish(); 99 | } 100 | 101 | criterion_group!( 102 | benches, 103 | bench_increment_value, 104 | bench_prime_count, 105 | bench_recursive_factorial 106 | ); 107 | 108 | criterion_main!(benches); 109 | -------------------------------------------------------------------------------- /www/src/components/ast-node.tsx: -------------------------------------------------------------------------------- 1 | import { addHighlightEffect, removeHighlightEffect } from '@/lib/highlight'; 2 | import { AstNode as AstNodeType } from '@/lib/types'; 3 | import { EditorView } from '@codemirror/view'; 4 | import { ChevronDown, ChevronRight } from 'lucide-react'; 5 | import React, { memo, useCallback, useState } from 'react'; 6 | 7 | interface AstNodeProps { 8 | node: AstNodeType; 9 | depth?: number; 10 | editorView?: EditorView | null; 11 | } 12 | 13 | export const AstNode: React.FC = memo( 14 | ({ node, depth = 0, editorView }) => { 15 | const [isExpanded, setIsExpanded] = useState(true); 16 | const [isHovered, setIsHovered] = useState(false); 17 | 18 | const hasChildren = node.children && node.children.length > 0; 19 | 20 | const isValidRange = node.range.start < node.range.end; 21 | 22 | const style = { 23 | backgroundColor: isHovered ? 'rgba(59, 130, 246, 0.1)' : 'transparent', 24 | borderRadius: '2px', 25 | paddingLeft: `${depth * 16}px`, 26 | }; 27 | 28 | const handleMouseOver = useCallback(() => { 29 | setIsHovered(true); 30 | 31 | if (editorView && isValidRange) { 32 | editorView.dispatch({ 33 | effects: [ 34 | addHighlightEffect.of({ 35 | from: node.range.start, 36 | to: node.range.end, 37 | }), 38 | EditorView.scrollIntoView(node.range.start, { y: 'center' }), 39 | ], 40 | }); 41 | } 42 | }, [editorView, node.range.start, node.range.end, isValidRange]); 43 | 44 | const handleMouseLeave = useCallback(() => { 45 | setIsHovered(false); 46 | 47 | if (editorView && isValidRange) { 48 | editorView.dispatch({ 49 | effects: removeHighlightEffect.of(null), 50 | }); 51 | } 52 | }, [editorView, isValidRange]); 53 | 54 | const toggleExpanded = useCallback(() => { 55 | setIsExpanded((prev) => !prev); 56 | }, []); 57 | 58 | return ( 59 |
60 |
67 | 68 | {hasChildren ? ( 69 | isExpanded ? ( 70 | 71 | ) : ( 72 | 73 | ) 74 | ) : ( 75 | 76 | )} 77 | 78 | 79 | {node.kind} 80 | 81 | 82 | [{node.range.start}: {node.range.end}]{!isValidRange && ' (empty)'} 83 | 84 |
85 | 86 | {isExpanded && hasChildren && ( 87 |
88 | {node.children.map((child, index) => ( 89 | 95 | ))} 96 |
97 | )} 98 |
99 | ); 100 | } 101 | ); 102 | 103 | AstNode.displayName = 'AstNode'; 104 | -------------------------------------------------------------------------------- /tools/example-generator/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import shutil 4 | from textwrap import dedent 5 | 6 | from arrg import app, argument 7 | 8 | 9 | def snake_to_camel(snake_str): 10 | components = snake_str.split('_') 11 | return components[0] + ''.join(x.title() for x in components[1:]) 12 | 13 | 14 | @app( 15 | description='Generate a JavaScript dictionary from val source files using imports.', 16 | formatter_class=argparse.RawDescriptionHelpFormatter, 17 | epilog=dedent( 18 | """\ 19 | Examples: 20 | %(prog)s ./src/tests ./frontend/src/lib/examples.js 21 | %(prog)s /path/to/val/files /path/to/output.js 22 | """ 23 | ), 24 | ) 25 | class App: 26 | source_dir: str = argument(help='Directory containing the val source files') 27 | output_file: str = argument(help='Path to the output JavaScript file') 28 | examples_dir: str = argument( 29 | help='Path to the assets/examples directory', default=None, nargs='?' 30 | ) 31 | 32 | def run(self) -> int: 33 | try: 34 | self.generate() 35 | return 0 36 | except Exception as e: 37 | print(f'error: {e}') 38 | return 1 39 | 40 | def generate(self): 41 | if not os.path.exists(self.source_dir): 42 | raise FileNotFoundError(f"Source directory '{self.source_dir}' does not exist") 43 | 44 | if self.examples_dir is None: 45 | output_dir = os.path.dirname(self.output_file) 46 | base_dir = os.path.dirname(output_dir) 47 | self.examples_dir = os.path.join(base_dir, 'assets', 'examples') 48 | 49 | os.makedirs(self.examples_dir, exist_ok=True) 50 | os.makedirs(os.path.dirname(self.output_file), exist_ok=True) 51 | 52 | val_files = sorted([f for f in os.listdir(self.source_dir) if f.endswith('.val')]) 53 | 54 | if not val_files: 55 | print(f'Warning: No .val files found in {self.source_dir}') 56 | return 57 | 58 | for file in val_files: 59 | source_file = os.path.join(self.source_dir, file) 60 | target_file = os.path.join(self.examples_dir, file) 61 | shutil.copy2(source_file, target_file) 62 | print(f'Copied {file} to {self.examples_dir}') 63 | 64 | imports = [] 65 | dictionary_entries = [] 66 | 67 | for file in val_files: 68 | base_name = file.replace('.val', '') 69 | var_name = snake_to_camel(base_name) 70 | rel_path = os.path.relpath(self.examples_dir, os.path.dirname(self.output_file)) 71 | imports.append(f"import {var_name} from '{rel_path}/{file}?raw';") 72 | dictionary_entries.append(f' {var_name}: {var_name}') 73 | 74 | content = self._generate_js_dictionary(imports, dictionary_entries) 75 | 76 | with open(self.output_file, 'w') as f: 77 | f.write(content) 78 | 79 | print(f'Successfully generated code at {self.output_file}') 80 | print(f'Processed {len(val_files)} val files') 81 | 82 | def _generate_js_dictionary(self, imports, dictionary_entries): 83 | lines = [ 84 | '// This file is generated by `example-generator`. Do not edit manually.', 85 | '', 86 | ] 87 | 88 | for import_line in imports: 89 | lines.append(import_line) 90 | 91 | lines.append('') 92 | lines.append('export const examples = {') 93 | 94 | dictionary_text = ',\n'.join(dictionary_entries) 95 | lines.append(dictionary_text) 96 | 97 | lines.append('};') 98 | lines.append('') 99 | 100 | return '\n'.join(lines) 101 | 102 | 103 | if __name__ == '__main__': 104 | exit(App.from_args().run()) 105 | -------------------------------------------------------------------------------- /src/value.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug)] 4 | pub enum Value<'src> { 5 | Boolean(bool), 6 | BuiltinFunction(&'src str, BuiltinFunction<'src>), 7 | Function( 8 | &'src str, 9 | Vec<&'src str>, 10 | Vec>>, 11 | Environment<'src>, 12 | ), 13 | List(Vec), 14 | Null, 15 | Number(Float), 16 | String(&'src str), 17 | } 18 | 19 | impl Display for Value<'_> { 20 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 21 | match self { 22 | Value::Boolean(boolean) => write!(f, "{boolean}"), 23 | Value::BuiltinFunction(name, _) => write!(f, ""), 24 | Value::Function(name, _, _, _) => write!(f, ""), 25 | Value::List(list) => write!( 26 | f, 27 | "[{}]", 28 | list 29 | .iter() 30 | .map(|item| match item { 31 | Value::String(string) => format!("\'{string}\'"), 32 | _ => item.to_string(), 33 | }) 34 | .collect::>() 35 | .join(", ") 36 | ), 37 | Value::Null => write!(f, "null"), 38 | Value::Number(number) => write!(f, "{}", number.display()), 39 | Value::String(string) => write!(f, "{string}"), 40 | } 41 | } 42 | } 43 | 44 | impl PartialEq for Value<'_> { 45 | fn eq(&self, other: &Self) -> bool { 46 | match (self, other) { 47 | (Value::Boolean(a), Value::Boolean(b)) => a == b, 48 | ( 49 | Value::BuiltinFunction(a_name, _), 50 | Value::BuiltinFunction(b_name, _), 51 | ) => a_name == b_name, 52 | (Value::Function(a_name, _, _, _), Value::Function(b_name, _, _, _)) => { 53 | a_name == b_name 54 | } 55 | (Value::List(a), Value::List(b)) => { 56 | a.len() == b.len() && a.iter().zip(b.iter()).all(|(a, b)| a == b) 57 | } 58 | (Value::Null, Value::Null) => true, 59 | (Value::Number(a), Value::Number(b)) => a == b, 60 | (Value::String(a), Value::String(b)) => a == b, 61 | _ => false, 62 | } 63 | } 64 | } 65 | 66 | impl<'a> Value<'a> { 67 | pub fn boolean(&self, span: Span) -> Result { 68 | if let Value::Boolean(x) = self { 69 | Ok(*x) 70 | } else { 71 | Err(Error { 72 | span, 73 | message: format!("'{}' is not a boolean", self), 74 | }) 75 | } 76 | } 77 | 78 | pub fn list(&self, span: Span) -> Result>, Error> { 79 | if let Value::List(x) = self { 80 | Ok(x.clone()) 81 | } else { 82 | Err(Error { 83 | span, 84 | message: format!("'{}' is not a list", self), 85 | }) 86 | } 87 | } 88 | 89 | pub fn number(&self, span: Span) -> Result { 90 | if let Value::Number(x) = self { 91 | Ok(x.clone()) 92 | } else { 93 | Err(Error { 94 | span, 95 | message: format!("'{}' is not a number", self), 96 | }) 97 | } 98 | } 99 | 100 | pub fn string(&self, span: Span) -> Result<&str, Error> { 101 | if let Value::String(x) = self { 102 | Ok(*x) 103 | } else { 104 | Err(Error { 105 | span, 106 | message: format!("'{}' is not a string", self), 107 | }) 108 | } 109 | } 110 | 111 | pub fn type_name(&self) -> &'static str { 112 | match self { 113 | Value::Boolean(_) => "boolean", 114 | Value::BuiltinFunction(_, _) => "function", 115 | Value::Function(_, _, _, _) => "function", 116 | Value::List(_) => "list", 117 | Value::Null => "null", 118 | Value::Number(_) => "number", 119 | Value::String(_) => "string", 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /www/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import * as DialogPrimitive from '@radix-ui/react-dialog'; 3 | import { XIcon } from 'lucide-react'; 4 | import * as React from 'react'; 5 | 6 | function Dialog({ 7 | ...props 8 | }: React.ComponentProps) { 9 | return ; 10 | } 11 | 12 | function DialogTrigger({ 13 | ...props 14 | }: React.ComponentProps) { 15 | return ; 16 | } 17 | 18 | function DialogPortal({ 19 | ...props 20 | }: React.ComponentProps) { 21 | return ; 22 | } 23 | 24 | function DialogClose({ 25 | ...props 26 | }: React.ComponentProps) { 27 | return ; 28 | } 29 | 30 | function DialogOverlay({ 31 | className, 32 | ...props 33 | }: React.ComponentProps) { 34 | return ( 35 | 43 | ); 44 | } 45 | 46 | function DialogContent({ 47 | className, 48 | children, 49 | ...props 50 | }: React.ComponentProps) { 51 | return ( 52 | 53 | 54 | 62 | {children} 63 | 64 | 65 | Close 66 | 67 | 68 | 69 | ); 70 | } 71 | 72 | function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { 73 | return ( 74 |
79 | ); 80 | } 81 | 82 | function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) { 83 | return ( 84 |
92 | ); 93 | } 94 | 95 | function DialogTitle({ 96 | className, 97 | ...props 98 | }: React.ComponentProps) { 99 | return ( 100 | 105 | ); 106 | } 107 | 108 | function DialogDescription({ 109 | className, 110 | ...props 111 | }: React.ComponentProps) { 112 | return ( 113 | 118 | ); 119 | } 120 | 121 | export { 122 | Dialog, 123 | DialogClose, 124 | DialogContent, 125 | DialogDescription, 126 | DialogFooter, 127 | DialogHeader, 128 | DialogOverlay, 129 | DialogPortal, 130 | DialogTitle, 131 | DialogTrigger, 132 | }; 133 | -------------------------------------------------------------------------------- /www/src/components/editor-settings-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogHeader, 7 | DialogTitle, 8 | } from '@/components/ui/dialog'; 9 | import { Label } from '@/components/ui/label'; 10 | import { 11 | Select, 12 | SelectContent, 13 | SelectItem, 14 | SelectTrigger, 15 | SelectValue, 16 | } from '@/components/ui/select'; 17 | import { Switch } from '@/components/ui/switch'; 18 | import { useEditorSettings } from '@/providers/editor-settings-provider'; 19 | import { Settings } from 'lucide-react'; 20 | import { useState } from 'react'; 21 | 22 | export const EditorSettingsDialog = () => { 23 | const { settings, updateSettings } = useEditorSettings(); 24 | 25 | const [settingsOpen, setSettingsOpen] = useState(false); 26 | 27 | return ( 28 | <> 29 | 38 | 39 | 40 | 41 | 42 | Settings 43 | 44 | Customize your editor experience with these settings. 45 | 46 | 47 |
48 |
49 | 50 | 53 | updateSettings({ lineNumbers: checked }) 54 | } 55 | /> 56 |
57 | 58 |
59 | 60 | 63 | updateSettings({ lineWrapping: checked }) 64 | } 65 | /> 66 |
67 | 68 |
69 | 70 | 86 |
87 | 88 |
89 | 90 | 104 |
105 | 106 |
107 | 108 | 123 |
124 |
125 |
126 |
127 | 128 | ); 129 | }; 130 | -------------------------------------------------------------------------------- /www/src/index.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @import 'tw-animate-css'; 3 | 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | @theme inline { 7 | --radius-sm: calc(var(--radius) - 4px); 8 | --radius-md: calc(var(--radius) - 2px); 9 | --radius-lg: var(--radius); 10 | --radius-xl: calc(var(--radius) + 4px); 11 | --color-background: var(--background); 12 | --color-foreground: var(--foreground); 13 | --color-card: var(--card); 14 | --color-card-foreground: var(--card-foreground); 15 | --color-popover: var(--popover); 16 | --color-popover-foreground: var(--popover-foreground); 17 | --color-primary: var(--primary); 18 | --color-primary-foreground: var(--primary-foreground); 19 | --color-secondary: var(--secondary); 20 | --color-secondary-foreground: var(--secondary-foreground); 21 | --color-muted: var(--muted); 22 | --color-muted-foreground: var(--muted-foreground); 23 | --color-accent: var(--accent); 24 | --color-accent-foreground: var(--accent-foreground); 25 | --color-destructive: var(--destructive); 26 | --color-border: var(--border); 27 | --color-input: var(--input); 28 | --color-ring: var(--ring); 29 | --color-chart-1: var(--chart-1); 30 | --color-chart-2: var(--chart-2); 31 | --color-chart-3: var(--chart-3); 32 | --color-chart-4: var(--chart-4); 33 | --color-chart-5: var(--chart-5); 34 | --color-sidebar: var(--sidebar); 35 | --color-sidebar-foreground: var(--sidebar-foreground); 36 | --color-sidebar-primary: var(--sidebar-primary); 37 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 38 | --color-sidebar-accent: var(--sidebar-accent); 39 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 40 | --color-sidebar-border: var(--sidebar-border); 41 | --color-sidebar-ring: var(--sidebar-ring); 42 | } 43 | 44 | :root { 45 | --radius: 0.625rem; 46 | --background: oklch(1 0 0); 47 | --foreground: oklch(0.145 0 0); 48 | --card: oklch(1 0 0); 49 | --card-foreground: oklch(0.145 0 0); 50 | --popover: oklch(1 0 0); 51 | --popover-foreground: oklch(0.145 0 0); 52 | --primary: oklch(0.205 0 0); 53 | --primary-foreground: oklch(0.985 0 0); 54 | --secondary: oklch(0.97 0 0); 55 | --secondary-foreground: oklch(0.205 0 0); 56 | --muted: oklch(0.97 0 0); 57 | --muted-foreground: oklch(0.556 0 0); 58 | --accent: oklch(0.97 0 0); 59 | --accent-foreground: oklch(0.205 0 0); 60 | --destructive: oklch(0.577 0.245 27.325); 61 | --border: oklch(0.922 0 0); 62 | --input: oklch(0.922 0 0); 63 | --ring: oklch(0.708 0 0); 64 | --chart-1: oklch(0.646 0.222 41.116); 65 | --chart-2: oklch(0.6 0.118 184.704); 66 | --chart-3: oklch(0.398 0.07 227.392); 67 | --chart-4: oklch(0.828 0.189 84.429); 68 | --chart-5: oklch(0.769 0.188 70.08); 69 | --sidebar: oklch(0.985 0 0); 70 | --sidebar-foreground: oklch(0.145 0 0); 71 | --sidebar-primary: oklch(0.205 0 0); 72 | --sidebar-primary-foreground: oklch(0.985 0 0); 73 | --sidebar-accent: oklch(0.97 0 0); 74 | --sidebar-accent-foreground: oklch(0.205 0 0); 75 | --sidebar-border: oklch(0.922 0 0); 76 | --sidebar-ring: oklch(0.708 0 0); 77 | } 78 | 79 | .dark { 80 | --background: oklch(0.145 0 0); 81 | --foreground: oklch(0.985 0 0); 82 | --card: oklch(0.205 0 0); 83 | --card-foreground: oklch(0.985 0 0); 84 | --popover: oklch(0.205 0 0); 85 | --popover-foreground: oklch(0.985 0 0); 86 | --primary: oklch(0.922 0 0); 87 | --primary-foreground: oklch(0.205 0 0); 88 | --secondary: oklch(0.269 0 0); 89 | --secondary-foreground: oklch(0.985 0 0); 90 | --muted: oklch(0.269 0 0); 91 | --muted-foreground: oklch(0.708 0 0); 92 | --accent: oklch(0.269 0 0); 93 | --accent-foreground: oklch(0.985 0 0); 94 | --destructive: oklch(0.704 0.191 22.216); 95 | --border: oklch(1 0 0 / 10%); 96 | --input: oklch(1 0 0 / 15%); 97 | --ring: oklch(0.556 0 0); 98 | --chart-1: oklch(0.488 0.243 264.376); 99 | --chart-2: oklch(0.696 0.17 162.48); 100 | --chart-3: oklch(0.769 0.188 70.08); 101 | --chart-4: oklch(0.627 0.265 303.9); 102 | --chart-5: oklch(0.645 0.246 16.439); 103 | --sidebar: oklch(0.205 0 0); 104 | --sidebar-foreground: oklch(0.985 0 0); 105 | --sidebar-primary: oklch(0.488 0.243 264.376); 106 | --sidebar-primary-foreground: oklch(0.985 0 0); 107 | --sidebar-accent: oklch(0.269 0 0); 108 | --sidebar-accent-foreground: oklch(0.985 0 0); 109 | --sidebar-border: oklch(1 0 0 / 10%); 110 | --sidebar-ring: oklch(0.556 0 0); 111 | } 112 | 113 | @layer base { 114 | * { 115 | @apply border-border outline-ring/50; 116 | } 117 | body { 118 | @apply bg-background text-foreground; 119 | } 120 | } 121 | 122 | .cm-highlighted-node { 123 | background-color: rgba(59, 130, 246, 0.15); 124 | outline: 1px solid rgba(59, 130, 246, 0.3); 125 | border-radius: 2px; 126 | } 127 | 128 | ::-webkit-scrollbar { 129 | width: 3px; 130 | height: 3px; 131 | } 132 | 133 | ::-webkit-scrollbar-track { 134 | background: transparent; 135 | } 136 | 137 | ::-webkit-scrollbar-thumb { 138 | background-color: rgb(182, 182, 182); 139 | border-radius: 20px; 140 | border: transparent; 141 | } 142 | 143 | ::-webkit-scrollbar-thumb:hover { 144 | background-color: rgb(150, 150, 150); 145 | } 146 | -------------------------------------------------------------------------------- /www/src/components/editor.tsx: -------------------------------------------------------------------------------- 1 | import { highlightExtension } from '@/lib/highlight'; 2 | import { ValError } from '@/lib/types'; 3 | import { useEditorSettings } from '@/providers/editor-settings-provider'; 4 | import { rust } from '@codemirror/lang-rust'; 5 | import { 6 | bracketMatching, 7 | defaultHighlightStyle, 8 | indentOnInput, 9 | syntaxHighlighting, 10 | } from '@codemirror/language'; 11 | import { Diagnostic, linter } from '@codemirror/lint'; 12 | import { vim } from '@replit/codemirror-vim'; 13 | import CodeMirror, { EditorState, EditorView } from '@uiw/react-codemirror'; 14 | import { 15 | forwardRef, 16 | useCallback, 17 | useEffect, 18 | useImperativeHandle, 19 | useRef, 20 | } from 'react'; 21 | 22 | interface EditorProps { 23 | errors: ValError[]; 24 | onChange?: (value: string, viewUpdate: any) => void; 25 | onEditorReady?: (view: EditorView) => void; 26 | value: string; 27 | } 28 | 29 | export interface EditorRef { 30 | view: EditorView | null; 31 | } 32 | 33 | export const Editor = forwardRef( 34 | ({ value, errors, onChange, onEditorReady }, ref) => { 35 | const { settings } = useEditorSettings(); 36 | 37 | const viewRef = useRef(null); 38 | 39 | useImperativeHandle(ref, () => ({ 40 | get view() { 41 | return viewRef.current; 42 | }, 43 | })); 44 | 45 | useEffect(() => { 46 | if (viewRef.current && onEditorReady) { 47 | onEditorReady(viewRef.current); 48 | } 49 | }, [viewRef.current, onEditorReady]); 50 | 51 | const createExtensions = useCallback(() => { 52 | const extensions = [ 53 | EditorState.tabSize.of(settings.tabSize), 54 | bracketMatching(), 55 | highlightExtension, 56 | indentOnInput(), 57 | linter(diagnostics()), 58 | rust(), 59 | syntaxHighlighting(defaultHighlightStyle), 60 | ]; 61 | 62 | if (settings.lineWrapping) { 63 | extensions.push(EditorView.lineWrapping); 64 | } 65 | 66 | if (settings.keybindings === 'vim') { 67 | extensions.push(vim()); 68 | } 69 | 70 | return extensions; 71 | }, [settings]); 72 | 73 | const createTheme = useCallback( 74 | () => 75 | EditorView.theme({ 76 | '&': { 77 | height: '100%', 78 | fontSize: `${settings.fontSize}px`, 79 | display: 'flex', 80 | flexDirection: 'column', 81 | }, 82 | '&.cm-editor': { 83 | height: '100%', 84 | }, 85 | '.cm-scroller': { 86 | overflow: 'auto', 87 | flex: '1 1 auto', 88 | fontFamily: 89 | 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace', 90 | }, 91 | '.cm-line': { 92 | padding: '0 10px', 93 | }, 94 | '.cm-content': { 95 | padding: '10px 0', 96 | }, 97 | '.cm-gutters': { 98 | backgroundColor: 'transparent', 99 | borderRight: 'none', 100 | paddingRight: '8px', 101 | }, 102 | '.cm-activeLineGutter': { 103 | backgroundColor: 'rgba(59, 130, 246, 0.1)', 104 | }, 105 | '.cm-activeLine': { 106 | backgroundColor: 'rgba(59, 130, 246, 0.1)', 107 | }, 108 | '.cm-fat-cursor': { 109 | backgroundColor: 'rgba(59, 130, 246, 0.5)', 110 | borderLeft: 'none', 111 | width: '0.6em', 112 | }, 113 | '.cm-cursor-secondary': { 114 | backgroundColor: 'rgba(59, 130, 246, 0.3)', 115 | }, 116 | }), 117 | [settings] 118 | ); 119 | 120 | const diagnostics = () => 121 | useCallback( 122 | (_view: EditorView): Diagnostic[] => { 123 | return errors.map((error) => { 124 | try { 125 | return { 126 | from: error.range.start, 127 | to: error.range.end, 128 | severity: 'error', 129 | message: error.message, 130 | source: error.kind.toString(), 131 | }; 132 | } catch (e) { 133 | console.warn('Failed to create diagnostic:', e, error); 134 | 135 | return { 136 | from: 0, 137 | to: 0, 138 | severity: 'error', 139 | message: error.message, 140 | source: error.kind.toString(), 141 | }; 142 | } 143 | }); 144 | }, 145 | [errors] 146 | ); 147 | 148 | const handleEditorCreate = (view: EditorView) => { 149 | viewRef.current = view; 150 | 151 | if (onEditorReady) { 152 | onEditorReady(view); 153 | } 154 | }; 155 | 156 | return ( 157 | 171 | ); 172 | } 173 | ); 174 | 175 | Editor.displayName = 'Editor'; 176 | -------------------------------------------------------------------------------- /www/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { AstNode } from '@/components/ast-node'; 2 | import { 3 | Select, 4 | SelectContent, 5 | SelectItem, 6 | SelectTrigger, 7 | SelectValue, 8 | } from '@/components/ui/select'; 9 | import type { AstNode as AstNodeType, ValError } from '@/lib/types'; 10 | import { EditorView } from '@codemirror/view'; 11 | import { Radius } from 'lucide-react'; 12 | import { useEffect, useRef, useState } from 'react'; 13 | import { toast } from 'sonner'; 14 | import init, { parse } from 'val-wasm'; 15 | 16 | import { Editor, EditorRef } from './components/editor'; 17 | import { EditorSettingsDialog } from './components/editor-settings-dialog'; 18 | import { 19 | ResizableHandle, 20 | ResizablePanel, 21 | ResizablePanelGroup, 22 | } from './components/ui/resizable'; 23 | import { examples } from './lib/examples'; 24 | 25 | const STORAGE_KEY_CODE = 'val-editor-code'; 26 | const STORAGE_KEY_EXAMPLE = 'val-editor-example'; 27 | 28 | function App() { 29 | const [ast, setAst] = useState(null); 30 | 31 | const [code, setCode] = useState(() => { 32 | const savedCode = localStorage.getItem(STORAGE_KEY_CODE); 33 | return savedCode || examples.factorial; 34 | }); 35 | 36 | const [currentExample, setCurrentExample] = useState(() => { 37 | const savedExample = localStorage.getItem(STORAGE_KEY_EXAMPLE); 38 | return savedExample || 'factorial'; 39 | }); 40 | 41 | const [editorView, setEditorView] = useState(null); 42 | const [errors, setErrors] = useState([]); 43 | const [wasmLoaded, setWasmLoaded] = useState(false); 44 | 45 | const editorRef = useRef(null); 46 | 47 | const handleEditorReady = (view: EditorView) => { 48 | setEditorView(view); 49 | }; 50 | 51 | useEffect(() => { 52 | init() 53 | .then(() => { 54 | setWasmLoaded(true); 55 | }) 56 | .catch((error) => { 57 | toast.error(error); 58 | }); 59 | }, []); 60 | 61 | useEffect(() => { 62 | if (!wasmLoaded) return; 63 | 64 | try { 65 | setAst(parse(code)); 66 | } catch (error) { 67 | setErrors(error as ValError[]); 68 | } 69 | }, [code, wasmLoaded]); 70 | 71 | useEffect(() => { 72 | if (editorRef.current?.view && !editorView) { 73 | setEditorView(editorRef.current.view); 74 | } 75 | }, [editorRef.current?.view, editorView]); 76 | 77 | useEffect(() => { 78 | localStorage.setItem(STORAGE_KEY_CODE, code); 79 | }, [code]); 80 | 81 | useEffect(() => { 82 | localStorage.setItem(STORAGE_KEY_EXAMPLE, currentExample); 83 | }, [currentExample]); 84 | 85 | const handleExampleChange = (value: string) => { 86 | setCurrentExample(value); 87 | setCode(examples[value as keyof typeof examples]); 88 | }; 89 | 90 | if (!wasmLoaded) return null; 91 | 92 | return ( 93 |
94 | 102 | 106 | 111 |
112 |
113 |
114 |
115 | 130 |
131 | 132 |
133 |
134 | 141 |
142 |
143 |
144 |
145 | 146 | 151 |
152 | {ast ? ( 153 | 154 | ) : ( 155 |
No AST available
156 | )} 157 |
158 |
159 |
160 |
161 | ); 162 | } 163 | 164 | export default App; 165 | -------------------------------------------------------------------------------- /tools/example-generator/uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.12.3" 4 | 5 | [[package]] 6 | name = "arrg" 7 | version = "0.1.0" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/90/53/862db83f09ac980a4af0c3d40d423f07e5cad300730a0fea117040e3e62f/arrg-0.1.0.tar.gz", hash = "sha256:58746665a54c361ee1a7b39f414b0bf373f5255d37cc938b22bbbe49daeda155", size = 17318 } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/5b/a3/54cfbbae8928e812fefac1cb1272a5819b26863ae4873728ca809af3b9b5/arrg-0.1.0-py3-none-any.whl", hash = "sha256:0b4315b6f5339d6b264808614b20f6a123359532d47b9a0eeecdf10f56cfd8fb", size = 14175 }, 12 | ] 13 | 14 | [[package]] 15 | name = "example-generator" 16 | version = "0.0.0" 17 | source = { virtual = "." } 18 | dependencies = [ 19 | { name = "arrg" }, 20 | { name = "ruff" }, 21 | ] 22 | 23 | [package.metadata] 24 | requires-dist = [ 25 | { name = "arrg", specifier = ">=0.1.0" }, 26 | { name = "ruff", specifier = ">=0.11.6" }, 27 | ] 28 | 29 | [[package]] 30 | name = "ruff" 31 | version = "0.11.6" 32 | source = { registry = "https://pypi.org/simple" } 33 | sdist = { url = "https://files.pythonhosted.org/packages/d9/11/bcef6784c7e5d200b8a1f5c2ddf53e5da0efec37e6e5a44d163fb97e04ba/ruff-0.11.6.tar.gz", hash = "sha256:bec8bcc3ac228a45ccc811e45f7eb61b950dbf4cf31a67fa89352574b01c7d79", size = 4010053 } 34 | wheels = [ 35 | { url = "https://files.pythonhosted.org/packages/6e/1f/8848b625100ebcc8740c8bac5b5dd8ba97dd4ee210970e98832092c1635b/ruff-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:d84dcbe74cf9356d1bdb4a78cf74fd47c740bf7bdeb7529068f69b08272239a1", size = 10248105 }, 36 | { url = "https://files.pythonhosted.org/packages/e0/47/c44036e70c6cc11e6ee24399c2a1e1f1e99be5152bd7dff0190e4b325b76/ruff-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9bc583628e1096148011a5d51ff3c836f51899e61112e03e5f2b1573a9b726de", size = 11001494 }, 37 | { url = "https://files.pythonhosted.org/packages/ed/5b/170444061650202d84d316e8f112de02d092bff71fafe060d3542f5bc5df/ruff-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2959049faeb5ba5e3b378709e9d1bf0cab06528b306b9dd6ebd2a312127964a", size = 10352151 }, 38 | { url = "https://files.pythonhosted.org/packages/ff/91/f02839fb3787c678e112c8865f2c3e87cfe1744dcc96ff9fc56cfb97dda2/ruff-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63c5d4e30d9d0de7fedbfb3e9e20d134b73a30c1e74b596f40f0629d5c28a193", size = 10541951 }, 39 | { url = "https://files.pythonhosted.org/packages/9e/f3/c09933306096ff7a08abede3cc2534d6fcf5529ccd26504c16bf363989b5/ruff-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4b9a4e1439f7d0a091c6763a100cef8fbdc10d68593df6f3cfa5abdd9246e", size = 10079195 }, 40 | { url = "https://files.pythonhosted.org/packages/e0/0d/a87f8933fccbc0d8c653cfbf44bedda69c9582ba09210a309c066794e2ee/ruff-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5edf270223dd622218256569636dc3e708c2cb989242262fe378609eccf1308", size = 11698918 }, 41 | { url = "https://files.pythonhosted.org/packages/52/7d/8eac0bd083ea8a0b55b7e4628428203441ca68cd55e0b67c135a4bc6e309/ruff-0.11.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f55844e818206a9dd31ff27f91385afb538067e2dc0beb05f82c293ab84f7d55", size = 12319426 }, 42 | { url = "https://files.pythonhosted.org/packages/c2/dc/d0c17d875662d0c86fadcf4ca014ab2001f867621b793d5d7eef01b9dcce/ruff-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d8f782286c5ff562e4e00344f954b9320026d8e3fae2ba9e6948443fafd9ffc", size = 11791012 }, 43 | { url = "https://files.pythonhosted.org/packages/f9/f3/81a1aea17f1065449a72509fc7ccc3659cf93148b136ff2a8291c4bc3ef1/ruff-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01c63ba219514271cee955cd0adc26a4083df1956d57847978383b0e50ffd7d2", size = 13949947 }, 44 | { url = "https://files.pythonhosted.org/packages/61/9f/a3e34de425a668284e7024ee6fd41f452f6fa9d817f1f3495b46e5e3a407/ruff-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15adac20ef2ca296dd3d8e2bedc6202ea6de81c091a74661c3666e5c4c223ff6", size = 11471753 }, 45 | { url = "https://files.pythonhosted.org/packages/df/c5/4a57a86d12542c0f6e2744f262257b2aa5a3783098ec14e40f3e4b3a354a/ruff-0.11.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4dd6b09e98144ad7aec026f5588e493c65057d1b387dd937d7787baa531d9bc2", size = 10417121 }, 46 | { url = "https://files.pythonhosted.org/packages/58/3f/a3b4346dff07ef5b862e2ba06d98fcbf71f66f04cf01d375e871382b5e4b/ruff-0.11.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:45b2e1d6c0eed89c248d024ea95074d0e09988d8e7b1dad8d3ab9a67017a5b03", size = 10073829 }, 47 | { url = "https://files.pythonhosted.org/packages/93/cc/7ed02e0b86a649216b845b3ac66ed55d8aa86f5898c5f1691797f408fcb9/ruff-0.11.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd40de4115b2ec4850302f1a1d8067f42e70b4990b68838ccb9ccd9f110c5e8b", size = 11076108 }, 48 | { url = "https://files.pythonhosted.org/packages/39/5e/5b09840fef0eff1a6fa1dea6296c07d09c17cb6fb94ed5593aa591b50460/ruff-0.11.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:77cda2dfbac1ab73aef5e514c4cbfc4ec1fbef4b84a44c736cc26f61b3814cd9", size = 11512366 }, 49 | { url = "https://files.pythonhosted.org/packages/6f/4c/1cd5a84a412d3626335ae69f5f9de2bb554eea0faf46deb1f0cb48534042/ruff-0.11.6-py3-none-win32.whl", hash = "sha256:5151a871554be3036cd6e51d0ec6eef56334d74dfe1702de717a995ee3d5b287", size = 10485900 }, 50 | { url = "https://files.pythonhosted.org/packages/42/46/8997872bc44d43df986491c18d4418f1caff03bc47b7f381261d62c23442/ruff-0.11.6-py3-none-win_amd64.whl", hash = "sha256:cce85721d09c51f3b782c331b0abd07e9d7d5f775840379c640606d3159cae0e", size = 11558592 }, 51 | { url = "https://files.pythonhosted.org/packages/d7/6a/65fecd51a9ca19e1477c3879a7fda24f8904174d1275b419422ac00f6eee/ruff-0.11.6-py3-none-win_arm64.whl", hash = "sha256:3567ba0d07fb170b1b48d944715e3294b77f5b7679e8ba258199a250383ccb79", size = 10682766 }, 52 | ] 53 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | defaults: 9 | run: 10 | shell: bash 11 | 12 | env: 13 | RUSTFLAGS: --deny warnings 14 | 15 | jobs: 16 | prerelease: 17 | runs-on: ubuntu-latest 18 | 19 | outputs: 20 | value: ${{ steps.prerelease.outputs.value }} 21 | 22 | steps: 23 | - name: Prerelease Check 24 | id: prerelease 25 | run: | 26 | if [[ ${{ github.ref_name }} =~ ^[0-9]+[.][0-9]+[.][0-9]+$ ]]; then 27 | echo value=false >> "$GITHUB_OUTPUT" 28 | else 29 | echo value=true >> "$GITHUB_OUTPUT" 30 | fi 31 | 32 | package: 33 | strategy: 34 | matrix: 35 | target: 36 | - aarch64-apple-darwin 37 | - aarch64-unknown-linux-musl 38 | - arm-unknown-linux-musleabihf 39 | - armv7-unknown-linux-musleabihf 40 | - x86_64-apple-darwin 41 | - x86_64-pc-windows-msvc 42 | - aarch64-pc-windows-msvc 43 | - x86_64-unknown-linux-musl 44 | include: 45 | - target: aarch64-apple-darwin 46 | os: macos-latest 47 | target_rustflags: '' 48 | - target: aarch64-unknown-linux-musl 49 | os: ubuntu-latest 50 | target_rustflags: '--codegen linker=aarch64-linux-gnu-gcc' 51 | - target: arm-unknown-linux-musleabihf 52 | os: ubuntu-latest 53 | target_rustflags: '--codegen linker=arm-linux-gnueabihf-gcc' 54 | - target: armv7-unknown-linux-musleabihf 55 | os: ubuntu-latest 56 | target_rustflags: '--codegen linker=arm-linux-gnueabihf-gcc' 57 | - target: x86_64-apple-darwin 58 | os: macos-latest 59 | target_rustflags: '' 60 | - target: x86_64-pc-windows-msvc 61 | os: windows-latest 62 | - target: aarch64-pc-windows-msvc 63 | os: windows-latest 64 | target_rustflags: '' 65 | - target: x86_64-unknown-linux-musl 66 | os: ubuntu-latest 67 | target_rustflags: '' 68 | 69 | runs-on: ${{matrix.os}} 70 | 71 | needs: prerelease 72 | 73 | steps: 74 | - uses: actions/checkout@v4 75 | 76 | - name: Install AArch64 Toolchain 77 | if: ${{ matrix.target == 'aarch64-unknown-linux-musl' }} 78 | run: | 79 | sudo apt-get update 80 | sudo apt-get install gcc-aarch64-linux-gnu libc6-dev-i386 81 | 82 | - name: Install musl tools 83 | if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' || matrix.target == 'arm-unknown-linux-musleabihf' || matrix.target == 'armv7-unknown-linux-musleabihf' }} 84 | run: | 85 | sudo apt-get update 86 | sudo apt-get install -y musl-tools 87 | 88 | - name: Install ARM Toolchain 89 | if: ${{ matrix.target == 'arm-unknown-linux-musleabihf' || matrix.target == 'armv7-unknown-linux-musleabihf' }} 90 | run: | 91 | sudo apt-get update 92 | sudo apt-get install gcc-arm-linux-gnueabihf 93 | 94 | - name: Install AArch64 Toolchain (Windows) 95 | if: ${{ matrix.target == 'aarch64-pc-windows-msvc' }} 96 | run: | 97 | rustup target add aarch64-pc-windows-msvc 98 | 99 | - name: Configure compiler for musl targets 100 | if: ${{ contains(matrix.target, 'musl') }} 101 | run: | 102 | if [[ "${{ matrix.target }}" == "x86_64-unknown-linux-musl" ]]; then 103 | { 104 | echo "CC=gcc" 105 | echo "CC_x86_64_unknown_linux_musl=gcc" 106 | echo "CFLAGS=-D_FORTIFY_SOURCE=0 -O2" 107 | } >> "$GITHUB_ENV" 108 | elif [[ "${{ matrix.target }}" == "aarch64-unknown-linux-musl" ]]; then 109 | { 110 | echo "CC=aarch64-linux-gnu-gcc" 111 | echo "CC_aarch64_unknown_linux_musl=aarch64-linux-gnu-gcc" 112 | echo "CFLAGS=-D_FORTIFY_SOURCE=0 -O2" 113 | } >> "$GITHUB_ENV" 114 | elif [[ "${{ matrix.target }}" == "arm-unknown-linux-musleabihf" ]]; then 115 | { 116 | echo "CC=arm-linux-gnueabihf-gcc" 117 | echo "CC_arm_unknown_linux_musleabihf=arm-linux-gnueabihf-gcc" 118 | echo "CFLAGS=-D_FORTIFY_SOURCE=0 -O2" 119 | } >> "$GITHUB_ENV" 120 | elif [[ "${{ matrix.target }}" == "armv7-unknown-linux-musleabihf" ]]; then 121 | { 122 | echo "CC=arm-linux-gnueabihf-gcc" 123 | echo "CC_armv7_unknown_linux_musleabihf=arm-linux-gnueabihf-gcc" 124 | echo "CFLAGS=-D_FORTIFY_SOURCE=0 -O2" 125 | } >> "$GITHUB_ENV" 126 | fi 127 | 128 | - name: Package 129 | id: package 130 | env: 131 | TARGET: ${{ matrix.target }} 132 | REF: ${{ github.ref }} 133 | OS: ${{ matrix.os }} 134 | TARGET_RUSTFLAGS: ${{ matrix.target_rustflags }} 135 | run: ./bin/package 136 | shell: bash 137 | 138 | - name: Publish Archive 139 | uses: softprops/action-gh-release@v2.2.1 140 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 141 | with: 142 | draft: false 143 | files: ${{ steps.package.outputs.archive }} 144 | prerelease: ${{ needs.prerelease.outputs.value }} 145 | env: 146 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 147 | 148 | checksum: 149 | runs-on: ubuntu-latest 150 | 151 | needs: 152 | - package 153 | - prerelease 154 | 155 | steps: 156 | - name: Download Release Archives 157 | env: 158 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 159 | run: >- 160 | gh release download 161 | --repo terror/val 162 | --pattern '*' 163 | --dir release 164 | ${{ github.ref_name }} 165 | 166 | - name: Create Checksums 167 | run: | 168 | cd release 169 | shasum -a 256 ./* > ../SHA256SUMS 170 | 171 | - name: Publish Checksums 172 | uses: softprops/action-gh-release@v2.2.1 173 | with: 174 | draft: false 175 | files: SHA256SUMS 176 | prerelease: ${{ needs.prerelease.outputs.value }} 177 | env: 178 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 179 | -------------------------------------------------------------------------------- /crates/val-wasm/src/ast_node.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Serialize)] 4 | pub struct AstNode { 5 | pub kind: String, 6 | pub range: Range, 7 | pub children: Vec, 8 | } 9 | 10 | impl From<(&Program<'_>, &Span)> for AstNode { 11 | fn from(value: (&Program<'_>, &Span)) -> Self { 12 | let (program, span) = value; 13 | 14 | let range = Range::from(span); 15 | 16 | let mut children = Vec::new(); 17 | 18 | match program { 19 | Program::Statements(statements) => { 20 | for (statement, span) in statements { 21 | children.push(Self::from((statement, span))); 22 | } 23 | 24 | Self { 25 | kind: program.kind(), 26 | range, 27 | children, 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | impl From<(&Statement<'_>, &Span)> for AstNode { 35 | fn from(value: (&Statement<'_>, &Span)) -> Self { 36 | let (statement, span) = value; 37 | 38 | let range = Range::from(span); 39 | 40 | let mut children = Vec::new(); 41 | 42 | match statement { 43 | Statement::Assignment(lhs, rhs) => { 44 | children.push(Self::from((&lhs.0, &lhs.1))); 45 | children.push(Self::from((&rhs.0, &rhs.1))); 46 | 47 | Self { 48 | kind: statement.kind(), 49 | range, 50 | children, 51 | } 52 | } 53 | Statement::Block(statements) => { 54 | for (statement, span) in statements { 55 | children.push(Self::from((statement, span))); 56 | } 57 | 58 | Self { 59 | kind: statement.kind(), 60 | range, 61 | children, 62 | } 63 | } 64 | Statement::Break => Self { 65 | kind: statement.kind(), 66 | range, 67 | children, 68 | }, 69 | Statement::Continue => Self { 70 | kind: statement.kind(), 71 | range, 72 | children, 73 | }, 74 | Statement::Expression(expression) => { 75 | children.push(Self::from((&expression.0, &expression.1))); 76 | 77 | Self { 78 | kind: statement.kind(), 79 | range, 80 | children, 81 | } 82 | } 83 | Statement::Function(_, _, body) => { 84 | for (statement, span) in body { 85 | children.push(Self::from((statement, span))); 86 | } 87 | 88 | Self { 89 | kind: statement.kind(), 90 | range, 91 | children, 92 | } 93 | } 94 | Statement::If(condition, then_branch, else_branch) => { 95 | children.push(Self::from((&condition.0, &condition.1))); 96 | 97 | for (statement, span) in then_branch { 98 | children.push(Self::from((statement, span))); 99 | } 100 | 101 | if let Some(else_statements) = else_branch { 102 | for (statement, span) in else_statements { 103 | children.push(Self::from((statement, span))); 104 | } 105 | } 106 | 107 | Self { 108 | kind: statement.kind(), 109 | range, 110 | children, 111 | } 112 | } 113 | Statement::Loop(body) => { 114 | for (statement, span) in body { 115 | children.push(Self::from((statement, span))); 116 | } 117 | 118 | Self { 119 | kind: statement.kind(), 120 | range, 121 | children, 122 | } 123 | } 124 | Statement::Return(expression) => { 125 | if let Some(expression) = expression { 126 | children.push(Self::from((&expression.0, &expression.1))); 127 | } 128 | 129 | Self { 130 | kind: statement.kind(), 131 | range, 132 | children, 133 | } 134 | } 135 | Statement::While(condition, body) => { 136 | children.push(Self::from((&condition.0, &condition.1))); 137 | 138 | for (statement, span) in body { 139 | children.push(Self::from((statement, span))); 140 | } 141 | 142 | Self { 143 | kind: statement.kind(), 144 | range, 145 | children, 146 | } 147 | } 148 | } 149 | } 150 | } 151 | 152 | impl From<(&Expression<'_>, &Span)> for AstNode { 153 | fn from(value: (&Expression<'_>, &Span)) -> Self { 154 | let (expression, span) = value; 155 | 156 | let range = Range::from(span); 157 | 158 | let mut children = Vec::new(); 159 | 160 | match expression { 161 | Expression::BinaryOp(_, lhs, rhs) => { 162 | children.push(Self::from((&lhs.0, &lhs.1))); 163 | children.push(Self::from((&rhs.0, &rhs.1))); 164 | 165 | Self { 166 | kind: expression.kind(), 167 | range, 168 | children, 169 | } 170 | } 171 | Expression::Boolean(_) => Self { 172 | kind: expression.kind(), 173 | range, 174 | children, 175 | }, 176 | Expression::FunctionCall(_, arguments) => { 177 | for (ast, span) in arguments { 178 | children.push(Self::from((ast, span))); 179 | } 180 | 181 | Self { 182 | kind: expression.kind(), 183 | range, 184 | children, 185 | } 186 | } 187 | Expression::Identifier(_) => Self { 188 | kind: expression.kind(), 189 | range, 190 | children, 191 | }, 192 | Expression::List(items) => { 193 | for (item, span) in items { 194 | children.push(Self::from((item, span))); 195 | } 196 | 197 | Self { 198 | kind: expression.kind(), 199 | range, 200 | children, 201 | } 202 | } 203 | Expression::ListAccess(list, index) => { 204 | children.push(Self::from((&list.0, &list.1))); 205 | children.push(Self::from((&index.0, &index.1))); 206 | 207 | Self { 208 | kind: expression.kind(), 209 | range, 210 | children, 211 | } 212 | } 213 | Expression::Null => Self { 214 | kind: expression.kind(), 215 | range, 216 | children, 217 | }, 218 | Expression::Number(_) => Self { 219 | kind: expression.kind(), 220 | range, 221 | children, 222 | }, 223 | Expression::String(_) => Self { 224 | kind: expression.kind(), 225 | range, 226 | children, 227 | }, 228 | Expression::UnaryOp(_, rhs) => { 229 | children.push(Self::from((&rhs.0, &rhs.1))); 230 | 231 | Self { 232 | kind: expression.kind(), 233 | range, 234 | children, 235 | } 236 | } 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /www/src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import * as SelectPrimitive from '@radix-ui/react-select'; 3 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react'; 4 | import * as React from 'react'; 5 | 6 | function Select({ 7 | ...props 8 | }: React.ComponentProps) { 9 | return ; 10 | } 11 | 12 | function SelectGroup({ 13 | ...props 14 | }: React.ComponentProps) { 15 | return ; 16 | } 17 | 18 | function SelectValue({ 19 | ...props 20 | }: React.ComponentProps) { 21 | return ; 22 | } 23 | 24 | function SelectTrigger({ 25 | className, 26 | size = 'default', 27 | children, 28 | ...props 29 | }: React.ComponentProps & { 30 | size?: 'sm' | 'default'; 31 | }) { 32 | return ( 33 | 42 | {children} 43 | 44 | 45 | 46 | 47 | ); 48 | } 49 | 50 | function SelectContent({ 51 | className, 52 | children, 53 | position = 'popper', 54 | ...props 55 | }: React.ComponentProps) { 56 | return ( 57 | 58 | 69 | 70 | 77 | {children} 78 | 79 | 80 | 81 | 82 | ); 83 | } 84 | 85 | function SelectLabel({ 86 | className, 87 | ...props 88 | }: React.ComponentProps) { 89 | return ( 90 | 95 | ); 96 | } 97 | 98 | function SelectItem({ 99 | className, 100 | children, 101 | ...props 102 | }: React.ComponentProps) { 103 | return ( 104 | 112 | 113 | 114 | 115 | 116 | 117 | {children} 118 | 119 | ); 120 | } 121 | 122 | function SelectSeparator({ 123 | className, 124 | ...props 125 | }: React.ComponentProps) { 126 | return ( 127 | 132 | ); 133 | } 134 | 135 | function SelectScrollUpButton({ 136 | className, 137 | ...props 138 | }: React.ComponentProps) { 139 | return ( 140 | 148 | 149 | 150 | ); 151 | } 152 | 153 | function SelectScrollDownButton({ 154 | className, 155 | ...props 156 | }: React.ComponentProps) { 157 | return ( 158 | 166 | 167 | 168 | ); 169 | } 170 | 171 | export { 172 | Select, 173 | SelectContent, 174 | SelectGroup, 175 | SelectItem, 176 | SelectLabel, 177 | SelectScrollDownButton, 178 | SelectScrollUpButton, 179 | SelectSeparator, 180 | SelectTrigger, 181 | SelectValue, 182 | }; 183 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /src/float_ext.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub trait FloatExt { 4 | fn display(&self) -> String; 5 | fn to_f64(&self, rounding_mode: astro_float::RoundingMode) -> Option; 6 | } 7 | 8 | impl FloatExt for Float { 9 | fn display(&self) -> String { 10 | if self.is_nan() { 11 | return "nan".into(); 12 | } 13 | 14 | if self.is_inf_pos() { 15 | return "inf".into(); 16 | } 17 | 18 | if self.is_inf_neg() { 19 | return "-inf".into(); 20 | } 21 | 22 | if self.is_zero() { 23 | return "0".into(); 24 | } 25 | 26 | let formatted = with_consts(|consts| { 27 | self.format(Radix::Dec, astro_float::RoundingMode::None, consts) 28 | }) 29 | .expect("failed to format Float as decimal"); 30 | 31 | let Some((mantissa_with_sign, exponent_str)) = formatted.split_once('e') 32 | else { 33 | return formatted; 34 | }; 35 | 36 | let Ok(exponent) = exponent_str.parse::() else { 37 | return formatted; 38 | }; 39 | 40 | let (sign, mantissa) = 41 | if let Some(rest) = mantissa_with_sign.strip_prefix('-') { 42 | ("-", rest) 43 | } else if let Some(rest) = mantissa_with_sign.strip_prefix('+') { 44 | ("", rest) 45 | } else { 46 | ("", mantissa_with_sign) 47 | }; 48 | 49 | let mut parts = mantissa.split('.'); 50 | let int_part = parts.next().unwrap_or(""); 51 | let frac_part = parts.next().unwrap_or(""); 52 | 53 | let mut digits = String::with_capacity(int_part.len() + frac_part.len()); 54 | digits.push_str(int_part); 55 | digits.push_str(frac_part); 56 | 57 | let length = int_part.len() as i32 + exponent; 58 | let digits_len = digits.len() as i32; 59 | 60 | let mut result = if length <= 0 { 61 | let zeros = (-length) as usize; 62 | let mut out = 63 | String::with_capacity(sign.len() + 2 + zeros + digits.len()); 64 | out.push_str(sign); 65 | out.push('0'); 66 | out.push('.'); 67 | out.extend(std::iter::repeat_n('0', zeros)); 68 | out.push_str(&digits); 69 | out 70 | } else if length >= digits_len { 71 | let zeros = (length - digits_len) as usize; 72 | let mut out = String::with_capacity(sign.len() + digits.len() + zeros); 73 | out.push_str(sign); 74 | out.push_str(&digits); 75 | out.extend(std::iter::repeat_n('0', zeros)); 76 | out 77 | } else { 78 | let split_at = length as usize; 79 | let (left, right) = digits.split_at(split_at); 80 | let mut out = 81 | String::with_capacity(sign.len() + left.len() + 1 + right.len()); 82 | out.push_str(sign); 83 | out.push_str(left); 84 | out.push('.'); 85 | out.push_str(right); 86 | out 87 | }; 88 | 89 | if result.contains('.') { 90 | while result.ends_with('0') { 91 | result.pop(); 92 | } 93 | 94 | if result.ends_with('.') { 95 | result.pop(); 96 | } 97 | } 98 | 99 | result 100 | } 101 | 102 | fn to_f64(&self, rounding_mode: astro_float::RoundingMode) -> Option { 103 | if self.is_nan() { 104 | return None; 105 | } 106 | 107 | if self.is_inf_pos() { 108 | return Some(f64::INFINITY); 109 | } 110 | 111 | if self.is_inf_neg() { 112 | return Some(f64::NEG_INFINITY); 113 | } 114 | 115 | if self.is_zero() { 116 | return Some(0.0); 117 | } 118 | 119 | let mut big_float = self.clone(); 120 | big_float.set_precision(64, rounding_mode).ok()?; 121 | 122 | let sign = big_float.sign()?; 123 | let mut exponent = big_float.exponent()? as isize; 124 | let mantissa_digits = big_float.mantissa_digits()?; 125 | let mantissa = *mantissa_digits.first().unwrap_or(&0); 126 | 127 | const F64_EXPONENT_BIAS: isize = 0x3ff; 128 | const F64_EXPONENT_MAX: isize = 0x7ff; 129 | const F64_SIGNIFICAND_BITS: usize = 52; 130 | const INTERNAL_SHIFT: usize = 12; 131 | const SIGN_MASK: u64 = 1u64 << 63; 132 | 133 | if mantissa == 0 { 134 | return Some(if sign == Sign::Neg { 135 | f64::from_bits(SIGN_MASK) 136 | } else { 137 | 0.0 138 | }); 139 | } 140 | 141 | exponent += F64_EXPONENT_BIAS; 142 | 143 | if exponent >= F64_EXPONENT_MAX { 144 | return Some(match sign { 145 | Sign::Pos => f64::INFINITY, 146 | Sign::Neg => f64::NEG_INFINITY, 147 | }); 148 | } 149 | 150 | let sign_bit = if sign == Sign::Neg { SIGN_MASK } else { 0 }; 151 | 152 | if exponent <= 0 { 153 | let shift = (-exponent) as usize; 154 | 155 | if shift >= F64_SIGNIFICAND_BITS { 156 | return Some(f64::from_bits(sign_bit)); 157 | } 158 | 159 | let fraction = mantissa >> (shift + INTERNAL_SHIFT); 160 | 161 | return Some(f64::from_bits(sign_bit | fraction)); 162 | } 163 | 164 | let adjusted_mantissa = mantissa << 1; 165 | let adjusted_exponent = (exponent - 1) as u64; 166 | let exponent_bits = adjusted_exponent << F64_SIGNIFICAND_BITS; 167 | let fraction_bits = adjusted_mantissa >> INTERNAL_SHIFT; 168 | 169 | Some(f64::from_bits(sign_bit | exponent_bits | fraction_bits)) 170 | } 171 | } 172 | 173 | #[cfg(test)] 174 | mod tests { 175 | use super::*; 176 | 177 | fn float_from_str(s: &str) -> Float { 178 | with_consts(|consts| { 179 | Float::parse( 180 | s, 181 | Radix::Dec, 182 | 128, 183 | astro_float::RoundingMode::FromZero, 184 | consts, 185 | ) 186 | }) 187 | } 188 | 189 | #[test] 190 | fn specials() { 191 | assert_eq!(format!("{}", Float::from(0).display()), "0"); 192 | 193 | assert_eq!(format!("{}", Float::from(f64::INFINITY).display()), "inf"); 194 | 195 | assert_eq!( 196 | format!("{}", Float::from(f64::NEG_INFINITY).display()), 197 | "-inf" 198 | ); 199 | 200 | assert_eq!(format!("{}", Float::nan(None).display()), "nan"); 201 | } 202 | 203 | #[test] 204 | fn integers() { 205 | assert_eq!(Float::from(1).display(), "1"); 206 | assert_eq!(Float::from(-1).display(), "-1"); 207 | assert_eq!(Float::from(123456789).display(), "123456789"); 208 | assert_eq!(Float::from(-123456789).display(), "-123456789"); 209 | } 210 | 211 | #[test] 212 | fn trailing_zeros() { 213 | assert_eq!(float_from_str("1.2300e2").display(), "123"); 214 | } 215 | 216 | #[test] 217 | fn scientific_notation_positive_exponent() { 218 | assert_eq!(float_from_str("1.23e2").display(), "123"); 219 | assert_eq!(float_from_str("1.23e3").display(), "1230"); 220 | } 221 | 222 | #[test] 223 | fn scientific_notation_negative_exponent() { 224 | assert_eq!(float_from_str("1.23e-5").display(), "0.0000123"); 225 | assert_eq!(float_from_str("-1.23e-2").display(), "-0.0123"); 226 | assert_eq!(float_from_str("-1.23e-5").display(), "-0.0000123"); 227 | } 228 | 229 | #[test] 230 | fn large_numbers() { 231 | assert_eq!(float_from_str("1e15").display(), "1000000000000000"); 232 | assert_eq!(float_from_str("-1e15").display(), "-1000000000000000"); 233 | assert_eq!(float_from_str("1.23e15").display(), "1230000000000000"); 234 | } 235 | 236 | #[test] 237 | fn convert_to_double_precision() { 238 | assert_eq!( 239 | Float::from(0.0).to_f64(astro_float::RoundingMode::ToEven), 240 | Some(0.0) 241 | ); 242 | 243 | assert_eq!( 244 | Float::from(1.0).to_f64(astro_float::RoundingMode::ToEven), 245 | Some(1.0) 246 | ); 247 | 248 | assert_eq!( 249 | Float::from(-1.0).to_f64(astro_float::RoundingMode::ToEven), 250 | Some(-1.0) 251 | ); 252 | } 253 | 254 | #[test] 255 | fn convert_special_values_to_double_precision() { 256 | assert_eq!( 257 | Float::from(f64::INFINITY).to_f64(astro_float::RoundingMode::ToEven), 258 | Some(f64::INFINITY) 259 | ); 260 | 261 | assert_eq!( 262 | Float::from(f64::NEG_INFINITY).to_f64(astro_float::RoundingMode::ToEven), 263 | Some(f64::NEG_INFINITY) 264 | ); 265 | 266 | assert_eq!( 267 | Float::nan(None).to_f64(astro_float::RoundingMode::ToEven), 268 | None 269 | ); 270 | } 271 | 272 | #[test] 273 | fn convert_underflow_preserves_sign() { 274 | let tiny_negative = float_from_str("-1e-4000"); 275 | 276 | let result = tiny_negative.to_f64(astro_float::RoundingMode::ToEven); 277 | 278 | assert!(result.is_some()); 279 | 280 | let value = result.unwrap(); 281 | 282 | assert!(value.is_sign_negative()); 283 | 284 | assert_eq!(value, -0.0); 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/ast.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Debug, Clone)] 4 | pub enum Program<'a> { 5 | Statements(Vec>>), 6 | } 7 | 8 | impl Display for Program<'_> { 9 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 10 | match self { 11 | Program::Statements(statements) => { 12 | write!( 13 | f, 14 | "statements({})", 15 | statements 16 | .iter() 17 | .map(|s| s.0.to_string()) 18 | .collect::>() 19 | .join(", ") 20 | ) 21 | } 22 | } 23 | } 24 | } 25 | 26 | impl Program<'_> { 27 | pub fn kind(&self) -> String { 28 | String::from(match self { 29 | Program::Statements(_) => "statements", 30 | }) 31 | } 32 | } 33 | 34 | #[derive(Debug, Clone)] 35 | pub enum Statement<'a> { 36 | Assignment(Spanned>, Spanned>), 37 | Block(Vec>>), 38 | Break, 39 | Continue, 40 | Expression(Spanned>), 41 | Function(&'a str, Vec<&'a str>, Vec>>), 42 | If( 43 | Spanned>, 44 | Vec>>, 45 | Option>>>, 46 | ), 47 | Loop(Vec>>), 48 | Return(Option>>), 49 | While(Spanned>, Vec>>), 50 | } 51 | 52 | impl Display for Statement<'_> { 53 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 54 | match self { 55 | Statement::Assignment(lhs, rhs) => { 56 | write!(f, "assignment({}, {})", lhs.0, rhs.0) 57 | } 58 | Statement::Block(statements) => { 59 | write!( 60 | f, 61 | "block({})", 62 | statements 63 | .iter() 64 | .map(|s| s.0.to_string()) 65 | .collect::>() 66 | .join(", ") 67 | ) 68 | } 69 | Statement::Break => write!(f, "break"), 70 | Statement::Continue => write!(f, "continue"), 71 | Statement::Expression(expression) => { 72 | write!(f, "expression({})", expression.0) 73 | } 74 | Statement::Function(name, params, body) => { 75 | write!( 76 | f, 77 | "function({}, [{}], block({}))", 78 | name, 79 | params.join(", "), 80 | body 81 | .iter() 82 | .map(|s| s.0.to_string()) 83 | .collect::>() 84 | .join(", ") 85 | ) 86 | } 87 | Statement::If(condition, then_branch, else_branch) => { 88 | let then_str = then_branch 89 | .iter() 90 | .map(|s| s.0.to_string()) 91 | .collect::>() 92 | .join(", "); 93 | 94 | match else_branch { 95 | Some(else_statements) => { 96 | write!( 97 | f, 98 | "if({}, block({}), block({}))", 99 | condition.0, 100 | then_str, 101 | else_statements 102 | .iter() 103 | .map(|s| s.0.to_string()) 104 | .collect::>() 105 | .join(", ") 106 | ) 107 | } 108 | None => { 109 | write!(f, "if({}, block({}))", condition.0, then_str) 110 | } 111 | } 112 | } 113 | Statement::Loop(body) => { 114 | write!( 115 | f, 116 | "loop(block({}))", 117 | body 118 | .iter() 119 | .map(|s| s.0.to_string()) 120 | .collect::>() 121 | .join(", ") 122 | ) 123 | } 124 | Statement::Return(expr) => match expr { 125 | Some(expression) => write!(f, "return({})", expression.0), 126 | None => write!(f, "return()"), 127 | }, 128 | Statement::While(condition, body) => { 129 | write!( 130 | f, 131 | "while({}, block({}))", 132 | condition.0, 133 | body 134 | .iter() 135 | .map(|s| s.0.to_string()) 136 | .collect::>() 137 | .join(", ") 138 | ) 139 | } 140 | } 141 | } 142 | } 143 | 144 | impl Statement<'_> { 145 | pub fn kind(&self) -> String { 146 | String::from(match self { 147 | Statement::Assignment(_, _) => "assignment", 148 | Statement::Block(_) => "block", 149 | Statement::Break => "break", 150 | Statement::Continue => "continue", 151 | Statement::Expression(_) => "expression", 152 | Statement::Function(_, _, _) => "function", 153 | Statement::If(_, _, _) => "if", 154 | Statement::Loop(_) => "loop", 155 | Statement::Return(_) => "return", 156 | Statement::While(_, _) => "while", 157 | }) 158 | } 159 | } 160 | 161 | #[derive(Debug, Clone)] 162 | pub enum UnaryOp { 163 | Negate, 164 | Not, 165 | } 166 | 167 | impl Display for UnaryOp { 168 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 169 | match self { 170 | UnaryOp::Negate => write!(f, "-"), 171 | UnaryOp::Not => write!(f, "!"), 172 | } 173 | } 174 | } 175 | 176 | #[derive(Debug, Clone)] 177 | pub enum BinaryOp { 178 | Add, 179 | Divide, 180 | Equal, 181 | GreaterThan, 182 | GreaterThanEqual, 183 | LessThan, 184 | LessThanEqual, 185 | LogicalAnd, 186 | LogicalOr, 187 | Modulo, 188 | Multiply, 189 | NotEqual, 190 | Power, 191 | Subtract, 192 | } 193 | 194 | impl Display for BinaryOp { 195 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 196 | match self { 197 | BinaryOp::Add => write!(f, "+"), 198 | BinaryOp::Divide => write!(f, "/"), 199 | BinaryOp::Equal => write!(f, "=="), 200 | BinaryOp::GreaterThan => write!(f, ">"), 201 | BinaryOp::GreaterThanEqual => write!(f, ">="), 202 | BinaryOp::LessThan => write!(f, "<"), 203 | BinaryOp::LessThanEqual => write!(f, "<="), 204 | BinaryOp::LogicalAnd => write!(f, "&&"), 205 | BinaryOp::LogicalOr => write!(f, "||"), 206 | BinaryOp::Modulo => write!(f, "%"), 207 | BinaryOp::Multiply => write!(f, "*"), 208 | BinaryOp::NotEqual => write!(f, "!="), 209 | BinaryOp::Power => write!(f, "^"), 210 | BinaryOp::Subtract => write!(f, "-"), 211 | } 212 | } 213 | } 214 | 215 | #[derive(Debug, Clone)] 216 | pub enum Expression<'a> { 217 | BinaryOp(BinaryOp, Box>, Box>), 218 | Boolean(bool), 219 | FunctionCall(&'a str, Vec>), 220 | Identifier(&'a str), 221 | List(Vec>), 222 | ListAccess(Box>, Box>), 223 | Null, 224 | Number(Float), 225 | String(&'a str), 226 | UnaryOp(UnaryOp, Box>), 227 | } 228 | 229 | impl Display for Expression<'_> { 230 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 231 | match self { 232 | Expression::BinaryOp(op, lhs, rhs) => { 233 | write!(f, "binary_op({}, {}, {})", op, lhs.0, rhs.0) 234 | } 235 | Expression::Boolean(boolean) => write!(f, "boolean({})", boolean), 236 | Expression::FunctionCall(name, arguments) => { 237 | write!( 238 | f, 239 | "function_call({},{})", 240 | name, 241 | arguments 242 | .iter() 243 | .map(|a| a.0.to_string()) 244 | .collect::>() 245 | .join(", ") 246 | ) 247 | } 248 | Expression::Identifier(identifier) => { 249 | write!(f, "identifier({})", identifier) 250 | } 251 | Expression::List(list) => { 252 | write!( 253 | f, 254 | "list({})", 255 | list 256 | .iter() 257 | .map(|item| item.0.to_string()) 258 | .collect::>() 259 | .join(", ") 260 | ) 261 | } 262 | Expression::ListAccess(list, index) => { 263 | write!(f, "list_access({}, {})", list.0, index.0) 264 | } 265 | Expression::Null => write!(f, "null"), 266 | Expression::Number(number) => write!(f, "number({})", number.display()), 267 | Expression::String(string) => write!(f, "string(\"{}\")", string), 268 | Expression::UnaryOp(op, expr) => { 269 | write!(f, "unary_op({}, {})", op, expr.0) 270 | } 271 | } 272 | } 273 | } 274 | 275 | impl Expression<'_> { 276 | pub fn kind(&self) -> String { 277 | String::from(match self { 278 | Expression::BinaryOp(_, _, _) => "binary_op", 279 | Expression::Boolean(_) => "boolean", 280 | Expression::FunctionCall(_, _) => "function_call", 281 | Expression::Identifier(_) => "identifier", 282 | Expression::List(_) => "list", 283 | Expression::ListAccess(_, _) => "list_access", 284 | Expression::Null => "null", 285 | Expression::Number(_) => "number", 286 | Expression::String(_) => "string", 287 | Expression::UnaryOp(_, _) => "unary_op", 288 | }) 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/arguments.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clap, Debug)] 4 | #[clap( 5 | about, 6 | author, 7 | version, 8 | help_template = "\ 9 | {before-help}{name} {version} 10 | {author} 11 | {about} 12 | 13 | \x1b[1;4mUsage\x1b[0m: {usage} 14 | 15 | {all-args}{after-help} 16 | " 17 | )] 18 | pub struct Arguments { 19 | #[clap( 20 | short, 21 | long, 22 | conflicts_with = "filename", 23 | help = "Expression to evaluate" 24 | )] 25 | expression: Option, 26 | 27 | #[clap(conflicts_with = "expression", help = "File to evaluate")] 28 | filename: Option, 29 | 30 | #[clap( 31 | short, 32 | long, 33 | conflicts_with = "filename", 34 | help = "Load files before entering the REPL" 35 | )] 36 | load: Option>, 37 | 38 | #[clap( 39 | short, 40 | long, 41 | default_value = "1024", 42 | help = "Binary precision (bits) to use for calculations" 43 | )] 44 | precision: usize, 45 | 46 | #[clap( 47 | short, 48 | long, 49 | value_parser = clap::value_parser!(RoundingMode), 50 | default_value = "to-even", 51 | help = "Rounding mode to use for calculations", 52 | )] 53 | rounding_mode: RoundingMode, 54 | 55 | #[clap( 56 | long, 57 | default_value = "128", 58 | help = "Stack size in MB for evaluations" 59 | )] 60 | pub stack_size: usize, 61 | } 62 | 63 | impl Arguments { 64 | pub fn run(self) -> Result { 65 | match (&self.filename, &self.expression) { 66 | (Some(filename), _) => self.eval(filename.clone()), 67 | (_, Some(expression)) => self.eval_expression(expression.clone()), 68 | _ => { 69 | #[cfg(not(target_family = "wasm"))] 70 | { 71 | self.read() 72 | } 73 | #[cfg(target_family = "wasm")] 74 | { 75 | Err(anyhow::anyhow!("Interactive mode not supported in WASM")) 76 | } 77 | } 78 | } 79 | } 80 | 81 | fn eval(&self, filename: PathBuf) -> Result { 82 | let content = fs::read_to_string(&filename)?; 83 | 84 | let filename = filename.to_string_lossy().to_string(); 85 | 86 | let mut evaluator = Evaluator::from(Environment::new(Config { 87 | precision: self.precision, 88 | rounding_mode: self.rounding_mode.into(), 89 | })); 90 | 91 | match parse(&content) { 92 | Ok(ast) => match evaluator.eval(&ast) { 93 | Ok(_) => Ok(()), 94 | Err(error) => { 95 | error 96 | .report(&filename) 97 | .eprint((filename.as_str(), Source::from(content)))?; 98 | 99 | process::exit(1); 100 | } 101 | }, 102 | Err(errors) => { 103 | for error in errors { 104 | error 105 | .report(&filename) 106 | .eprint((filename.as_str(), Source::from(&content)))?; 107 | } 108 | 109 | process::exit(1); 110 | } 111 | } 112 | } 113 | 114 | fn eval_expression(&self, value: String) -> Result { 115 | let mut evaluator = Evaluator::from(Environment::new(Config { 116 | precision: self.precision, 117 | rounding_mode: self.rounding_mode.into(), 118 | })); 119 | 120 | match parse(&value) { 121 | Ok(ast) => match evaluator.eval(&ast) { 122 | Ok(value) => { 123 | if let Value::Null = value { 124 | return Ok(()); 125 | } 126 | 127 | println!("{}", value); 128 | 129 | Ok(()) 130 | } 131 | Err(error) => { 132 | error 133 | .report("") 134 | .eprint(("", Source::from(value)))?; 135 | 136 | process::exit(1); 137 | } 138 | }, 139 | Err(errors) => { 140 | for error in errors { 141 | error 142 | .report("") 143 | .eprint(("", Source::from(&value)))?; 144 | } 145 | 146 | process::exit(1); 147 | } 148 | } 149 | } 150 | 151 | #[cfg(not(target_family = "wasm"))] 152 | fn read(&self) -> Result { 153 | let history = dirs::home_dir().unwrap_or_default().join(".val_history"); 154 | 155 | let editor_config = Builder::new() 156 | .color_mode(ColorMode::Enabled) 157 | .edit_mode(EditMode::Emacs) 158 | .history_ignore_space(true) 159 | .completion_type(CompletionType::Circular) 160 | .max_history_size(1000)? 161 | .build(); 162 | 163 | let mut editor = 164 | Editor::::with_config(editor_config)?; 165 | 166 | editor.set_helper(Some(Highlighter::new())); 167 | editor.load_history(&history).ok(); 168 | 169 | let mut evaluator = Evaluator::from(Environment::new(Config { 170 | precision: self.precision, 171 | rounding_mode: self.rounding_mode.into(), 172 | })); 173 | 174 | if let Some(filenames) = &self.load { 175 | for filename in filenames { 176 | let content: &'static str = 177 | Box::leak(fs::read_to_string(filename)?.into_boxed_str()); 178 | 179 | let filename = filename.to_string_lossy().to_string(); 180 | 181 | match parse(content) { 182 | Ok(ast) => match evaluator.eval(&ast) { 183 | Ok(_) => {} 184 | Err(error) => { 185 | error 186 | .report(&filename) 187 | .eprint((filename.as_str(), Source::from(content)))?; 188 | 189 | process::exit(1); 190 | } 191 | }, 192 | Err(errors) => { 193 | for error in errors { 194 | error 195 | .report(&filename) 196 | .eprint((filename.as_str(), Source::from(&content)))?; 197 | } 198 | 199 | process::exit(1); 200 | } 201 | } 202 | } 203 | } 204 | 205 | loop { 206 | let line = editor.readline("> ")?; 207 | 208 | editor.add_history_entry(&line)?; 209 | editor.save_history(&history)?; 210 | 211 | let line: &'static str = Box::leak(line.into_boxed_str()); 212 | 213 | match parse(line) { 214 | Ok(ast) => match evaluator.eval(&ast) { 215 | Ok(value) if !matches!(value, Value::Null) => println!("{value}"), 216 | Ok(_) => {} 217 | Err(error) => error 218 | .report("") 219 | .eprint(("", Source::from(line)))?, 220 | }, 221 | Err(errors) => { 222 | for error in errors { 223 | error 224 | .report("") 225 | .eprint(("", Source::from(line)))?; 226 | } 227 | } 228 | } 229 | } 230 | } 231 | } 232 | 233 | #[cfg(test)] 234 | mod tests { 235 | use {super::*, clap::Parser, std::path::PathBuf}; 236 | 237 | #[test] 238 | fn filename_only() { 239 | let arguments = Arguments::parse_from(vec!["program", "file.txt"]); 240 | 241 | assert!(arguments.filename.is_some()); 242 | assert!(arguments.expression.is_none()); 243 | 244 | assert_eq!(arguments.filename.unwrap(), PathBuf::from("file.txt")); 245 | } 246 | 247 | #[test] 248 | fn expression_only() { 249 | let arguments = 250 | Arguments::parse_from(vec!["program", "--expression", "1 + 2"]); 251 | 252 | assert!(arguments.filename.is_none()); 253 | assert!(arguments.expression.is_some()); 254 | 255 | assert_eq!(arguments.expression.unwrap(), "1 + 2"); 256 | } 257 | 258 | #[test] 259 | fn expression_short_form() { 260 | let arguments = Arguments::parse_from(vec!["program", "-e", "1 + 2"]); 261 | 262 | assert!(arguments.filename.is_none()); 263 | assert!(arguments.expression.is_some()); 264 | 265 | assert_eq!(arguments.expression.unwrap(), "1 + 2"); 266 | } 267 | 268 | #[test] 269 | fn both_should_fail() { 270 | assert!( 271 | Arguments::try_parse_from(vec![ 272 | "program", 273 | "file.txt", 274 | "--expression", 275 | "1 + 2" 276 | ]) 277 | .is_err() 278 | ); 279 | } 280 | 281 | #[test] 282 | fn neither_provided() { 283 | let arguments = Arguments::parse_from(vec!["program"]); 284 | 285 | assert!(arguments.filename.is_none()); 286 | assert!(arguments.expression.is_none()); 287 | } 288 | 289 | #[test] 290 | fn conflict_error_message() { 291 | let result = Arguments::try_parse_from(vec![ 292 | "program", 293 | "file.txt", 294 | "--expression", 295 | "1 + 2", 296 | ]); 297 | 298 | assert!(result.is_err()); 299 | 300 | let error = result.unwrap_err().to_string(); 301 | 302 | assert!( 303 | error.contains("cannot be used with"), 304 | "Error should mention conflicts: {}", 305 | error 306 | ); 307 | } 308 | 309 | #[test] 310 | fn load_conflicts_with_filename() { 311 | let result = Arguments::try_parse_from(vec![ 312 | "program", 313 | "file.txt", 314 | "--load", 315 | "prelude.val", 316 | ]); 317 | 318 | assert!(result.is_err(), "Parser should reject filename + --load"); 319 | 320 | let error = result.unwrap_err().to_string(); 321 | 322 | assert!( 323 | error.contains("cannot be used with"), 324 | "Error should mention conflicts: {}", 325 | error 326 | ); 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /www/packages/val-wasm/val.js: -------------------------------------------------------------------------------- 1 | let wasm; 2 | 3 | const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); 4 | 5 | if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; 6 | 7 | let cachedUint8ArrayMemory0 = null; 8 | 9 | function getUint8ArrayMemory0() { 10 | if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { 11 | cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); 12 | } 13 | return cachedUint8ArrayMemory0; 14 | } 15 | 16 | function getStringFromWasm0(ptr, len) { 17 | ptr = ptr >>> 0; 18 | return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); 19 | } 20 | 21 | let WASM_VECTOR_LEN = 0; 22 | 23 | const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } ); 24 | 25 | const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' 26 | ? function (arg, view) { 27 | return cachedTextEncoder.encodeInto(arg, view); 28 | } 29 | : function (arg, view) { 30 | const buf = cachedTextEncoder.encode(arg); 31 | view.set(buf); 32 | return { 33 | read: arg.length, 34 | written: buf.length 35 | }; 36 | }); 37 | 38 | function passStringToWasm0(arg, malloc, realloc) { 39 | 40 | if (realloc === undefined) { 41 | const buf = cachedTextEncoder.encode(arg); 42 | const ptr = malloc(buf.length, 1) >>> 0; 43 | getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); 44 | WASM_VECTOR_LEN = buf.length; 45 | return ptr; 46 | } 47 | 48 | let len = arg.length; 49 | let ptr = malloc(len, 1) >>> 0; 50 | 51 | const mem = getUint8ArrayMemory0(); 52 | 53 | let offset = 0; 54 | 55 | for (; offset < len; offset++) { 56 | const code = arg.charCodeAt(offset); 57 | if (code > 0x7F) break; 58 | mem[ptr + offset] = code; 59 | } 60 | 61 | if (offset !== len) { 62 | if (offset !== 0) { 63 | arg = arg.slice(offset); 64 | } 65 | ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; 66 | const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); 67 | const ret = encodeString(arg, view); 68 | 69 | offset += ret.written; 70 | ptr = realloc(ptr, len, offset, 1) >>> 0; 71 | } 72 | 73 | WASM_VECTOR_LEN = offset; 74 | return ptr; 75 | } 76 | 77 | let cachedDataViewMemory0 = null; 78 | 79 | function getDataViewMemory0() { 80 | if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { 81 | cachedDataViewMemory0 = new DataView(wasm.memory.buffer); 82 | } 83 | return cachedDataViewMemory0; 84 | } 85 | 86 | function debugString(val) { 87 | // primitive types 88 | const type = typeof val; 89 | if (type == 'number' || type == 'boolean' || val == null) { 90 | return `${val}`; 91 | } 92 | if (type == 'string') { 93 | return `"${val}"`; 94 | } 95 | if (type == 'symbol') { 96 | const description = val.description; 97 | if (description == null) { 98 | return 'Symbol'; 99 | } else { 100 | return `Symbol(${description})`; 101 | } 102 | } 103 | if (type == 'function') { 104 | const name = val.name; 105 | if (typeof name == 'string' && name.length > 0) { 106 | return `Function(${name})`; 107 | } else { 108 | return 'Function'; 109 | } 110 | } 111 | // objects 112 | if (Array.isArray(val)) { 113 | const length = val.length; 114 | let debug = '['; 115 | if (length > 0) { 116 | debug += debugString(val[0]); 117 | } 118 | for(let i = 1; i < length; i++) { 119 | debug += ', ' + debugString(val[i]); 120 | } 121 | debug += ']'; 122 | return debug; 123 | } 124 | // Test for built-in 125 | const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); 126 | let className; 127 | if (builtInMatches && builtInMatches.length > 1) { 128 | className = builtInMatches[1]; 129 | } else { 130 | // Failed to match the standard '[object ClassName]' 131 | return toString.call(val); 132 | } 133 | if (className == 'Object') { 134 | // we're a user defined class or Object 135 | // JSON.stringify avoids problems with cycles, and is generally much 136 | // easier than looping through ownProperties of `val`. 137 | try { 138 | return 'Object(' + JSON.stringify(val) + ')'; 139 | } catch (_) { 140 | return 'Object'; 141 | } 142 | } 143 | // errors 144 | if (val instanceof Error) { 145 | return `${val.name}: ${val.message}\n${val.stack}`; 146 | } 147 | // TODO we could test for more things here, like `Set`s and `Map`s. 148 | return className; 149 | } 150 | 151 | export function start() { 152 | wasm.start(); 153 | } 154 | 155 | function takeFromExternrefTable0(idx) { 156 | const value = wasm.__wbindgen_export_3.get(idx); 157 | wasm.__externref_table_dealloc(idx); 158 | return value; 159 | } 160 | /** 161 | * @param {string} input 162 | * @returns {any} 163 | */ 164 | export function parse(input) { 165 | const ptr0 = passStringToWasm0(input, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 166 | const len0 = WASM_VECTOR_LEN; 167 | const ret = wasm.parse(ptr0, len0); 168 | if (ret[2]) { 169 | throw takeFromExternrefTable0(ret[1]); 170 | } 171 | return takeFromExternrefTable0(ret[0]); 172 | } 173 | 174 | /** 175 | * @param {string} input 176 | * @returns {any} 177 | */ 178 | export function evaluate(input) { 179 | const ptr0 = passStringToWasm0(input, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 180 | const len0 = WASM_VECTOR_LEN; 181 | const ret = wasm.evaluate(ptr0, len0); 182 | if (ret[2]) { 183 | throw takeFromExternrefTable0(ret[1]); 184 | } 185 | return takeFromExternrefTable0(ret[0]); 186 | } 187 | 188 | async function __wbg_load(module, imports) { 189 | if (typeof Response === 'function' && module instanceof Response) { 190 | if (typeof WebAssembly.instantiateStreaming === 'function') { 191 | try { 192 | return await WebAssembly.instantiateStreaming(module, imports); 193 | 194 | } catch (e) { 195 | if (module.headers.get('Content-Type') != 'application/wasm') { 196 | console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); 197 | 198 | } else { 199 | throw e; 200 | } 201 | } 202 | } 203 | 204 | const bytes = await module.arrayBuffer(); 205 | return await WebAssembly.instantiate(bytes, imports); 206 | 207 | } else { 208 | const instance = await WebAssembly.instantiate(module, imports); 209 | 210 | if (instance instanceof WebAssembly.Instance) { 211 | return { instance, module }; 212 | 213 | } else { 214 | return instance; 215 | } 216 | } 217 | } 218 | 219 | function __wbg_get_imports() { 220 | const imports = {}; 221 | imports.wbg = {}; 222 | imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) { 223 | let deferred0_0; 224 | let deferred0_1; 225 | try { 226 | deferred0_0 = arg0; 227 | deferred0_1 = arg1; 228 | console.error(getStringFromWasm0(arg0, arg1)); 229 | } finally { 230 | wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); 231 | } 232 | }; 233 | imports.wbg.__wbg_new_405e22f390576ce2 = function() { 234 | const ret = new Object(); 235 | return ret; 236 | }; 237 | imports.wbg.__wbg_new_78feb108b6472713 = function() { 238 | const ret = new Array(); 239 | return ret; 240 | }; 241 | imports.wbg.__wbg_new_8a6f238a6ece86ea = function() { 242 | const ret = new Error(); 243 | return ret; 244 | }; 245 | imports.wbg.__wbg_set_37837023f3d740e8 = function(arg0, arg1, arg2) { 246 | arg0[arg1 >>> 0] = arg2; 247 | }; 248 | imports.wbg.__wbg_set_3f1d0b984ed272ed = function(arg0, arg1, arg2) { 249 | arg0[arg1] = arg2; 250 | }; 251 | imports.wbg.__wbg_stack_0ed75d68575b0f3c = function(arg0, arg1) { 252 | const ret = arg1.stack; 253 | const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 254 | const len1 = WASM_VECTOR_LEN; 255 | getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); 256 | getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); 257 | }; 258 | imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { 259 | const ret = debugString(arg1); 260 | const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 261 | const len1 = WASM_VECTOR_LEN; 262 | getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); 263 | getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); 264 | }; 265 | imports.wbg.__wbindgen_init_externref_table = function() { 266 | const table = wasm.__wbindgen_export_3; 267 | const offset = table.grow(4); 268 | table.set(0, undefined); 269 | table.set(offset + 0, undefined); 270 | table.set(offset + 1, null); 271 | table.set(offset + 2, true); 272 | table.set(offset + 3, false); 273 | ; 274 | }; 275 | imports.wbg.__wbindgen_number_new = function(arg0) { 276 | const ret = arg0; 277 | return ret; 278 | }; 279 | imports.wbg.__wbindgen_string_new = function(arg0, arg1) { 280 | const ret = getStringFromWasm0(arg0, arg1); 281 | return ret; 282 | }; 283 | imports.wbg.__wbindgen_throw = function(arg0, arg1) { 284 | throw new Error(getStringFromWasm0(arg0, arg1)); 285 | }; 286 | 287 | return imports; 288 | } 289 | 290 | function __wbg_init_memory(imports, memory) { 291 | 292 | } 293 | 294 | function __wbg_finalize_init(instance, module) { 295 | wasm = instance.exports; 296 | __wbg_init.__wbindgen_wasm_module = module; 297 | cachedDataViewMemory0 = null; 298 | cachedUint8ArrayMemory0 = null; 299 | 300 | 301 | wasm.__wbindgen_start(); 302 | return wasm; 303 | } 304 | 305 | function initSync(module) { 306 | if (wasm !== undefined) return wasm; 307 | 308 | 309 | if (typeof module !== 'undefined') { 310 | if (Object.getPrototypeOf(module) === Object.prototype) { 311 | ({module} = module) 312 | } else { 313 | console.warn('using deprecated parameters for `initSync()`; pass a single object instead') 314 | } 315 | } 316 | 317 | const imports = __wbg_get_imports(); 318 | 319 | __wbg_init_memory(imports); 320 | 321 | if (!(module instanceof WebAssembly.Module)) { 322 | module = new WebAssembly.Module(module); 323 | } 324 | 325 | const instance = new WebAssembly.Instance(module, imports); 326 | 327 | return __wbg_finalize_init(instance, module); 328 | } 329 | 330 | async function __wbg_init(module_or_path) { 331 | if (wasm !== undefined) return wasm; 332 | 333 | 334 | if (typeof module_or_path !== 'undefined') { 335 | if (Object.getPrototypeOf(module_or_path) === Object.prototype) { 336 | ({module_or_path} = module_or_path) 337 | } else { 338 | console.warn('using deprecated parameters for the initialization function; pass a single object instead') 339 | } 340 | } 341 | 342 | if (typeof module_or_path === 'undefined') { 343 | module_or_path = new URL('val_bg.wasm', import.meta.url); 344 | } 345 | const imports = __wbg_get_imports(); 346 | 347 | if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { 348 | module_or_path = fetch(module_or_path); 349 | } 350 | 351 | __wbg_init_memory(imports); 352 | 353 | const { instance, module } = await __wbg_load(await module_or_path, imports); 354 | 355 | return __wbg_finalize_init(instance, module); 356 | } 357 | 358 | export { initSync }; 359 | export default __wbg_init; 360 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## val 2 | 3 | [![release](https://img.shields.io/github/release/terror/val.svg?label=release&style=flat&labelColor=282c34&logo=github)](https://github.com/terror/val/releases/latest) 4 | [![crates.io](https://shields.io/crates/v/val.svg)](https://crates.io/crates/val) 5 | [![CI](https://github.com/terror/val/actions/workflows/ci.yaml/badge.svg)](https://github.com/terror/val/actions/workflows/ci.yaml) 6 | [![docs.rs](https://img.shields.io/docsrs/val)](https://docs.rs/val) 7 | [![dependency status](https://deps.rs/repo/github/terror/val/status.svg)](https://deps.rs/repo/github/terror/val) 8 | 9 | **val** (e**val**) is a simple arbitrary precision calculator language built 10 | on top of [**chumsky**](https://github.com/zesterer/chumsky) and 11 | [**ariadne**](https://github.com/zesterer/ariadne). 12 | 13 | val 14 | 15 | ## Installation 16 | 17 | `val` should run on any system, including Linux, MacOS, and the BSDs. 18 | 19 | The easiest way to install it is by using [cargo](https://doc.rust-lang.org/cargo/index.html), 20 | the Rust package manager: 21 | 22 | ```bash 23 | cargo install val 24 | ``` 25 | 26 | Otherwise, see below for the complete package list: 27 | 28 | #### Cross-platform 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
Package ManagerPackageCommand
Cargovalcargo install val
Homebrewterror/tap/valbrew install terror/tap/val
51 | 52 | ### Pre-built binaries 53 | 54 | Pre-built binaries for Linux, MacOS, and Windows can be found on [the releases 55 | page](https://github.com/terror/val/releases). 56 | 57 | ## Usage 58 | 59 | The primary way to use **val** is via the provided command-line interface. There 60 | is currently ongoing work on a Rust library and web playground, which will 61 | provide a few extra ways to interact with the runtime. 62 | 63 | Below is the output of `val --help`, which describes some of the 64 | arguments/options we support: 65 | 66 | ```present cargo run -- --help 67 | val 0.3.6 68 | Liam 69 | An arbitrary precision calculator language 70 | 71 | Usage: val [OPTIONS] [FILENAME] 72 | 73 | Arguments: 74 | [FILENAME] File to evaluate 75 | 76 | Options: 77 | -e, --expression Expression to evaluate 78 | -l, --load Load files before entering the REPL 79 | -p, --precision Binary precision (bits) to use for calculations [default: 1024] 80 | -r, --rounding-mode Rounding mode to use for calculations [default: to-even] 81 | --stack-size Stack size in MB for evaluations [default: 128] 82 | -h, --help Print help 83 | -V, --version Print version 84 | ``` 85 | 86 | Running **val** on its own will spawn a repl (read–eval–print loop) environment, 87 | where you can evaluate arbitrary **val** code and see its output immediately. We 88 | use [rustyline](https://github.com/kkawakam/rustyline) for its implementation, 89 | and we support a few quality of life features: 90 | 91 | - Syntax highlighting (see image above) 92 | - Persistent command history 93 | - Emacs-style editing support by default 94 | - Filename completions 95 | - Hints (virtual text pulled from history) 96 | 97 | The **val** language supports not only expressions, but quite a few 98 | [statements](https://github.com/terror/val/blob/ea0c163934ee3f4afe118384b1281d296f116539/src/ast.rs#L35) as well. 99 | You may want to save **val** programs and execute them later, so 100 | the command-line interface provides a way to evaluate entire files. 101 | 102 | For instance, lets say you have the following **val** program at 103 | `factorial.val`: 104 | 105 | ```rust 106 | fn factorial(n) { 107 | if (n <= 1) { 108 | return 1 109 | } else { 110 | return n * factorial(n - 1) 111 | } 112 | } 113 | 114 | println(factorial(5)); 115 | ``` 116 | 117 | You can execute this program by running `val factorial.val`, which will write to 118 | standard output `120`. 119 | 120 | Lastly, you may want to evaluate a **val** expression and use it within another 121 | program. The tool supports executing arbitrary expressions inline using the 122 | `--expression` or `-e` option: 123 | 124 | ```bash 125 | val -p 53 -e 'sin(2) * e ^ pi * cos(sum([1, 2, 3]))' 126 | 20.203684508229124193 127 | ``` 128 | 129 | **n.b.** The `--expression` option and `filename` argument are mutually 130 | exclusive. 131 | 132 | ## Features 133 | 134 | This section describes some of the language features **val** implements in 135 | detail, and should serve as a guide to anyone wanting to write a **val** 136 | program. 137 | 138 | ### Statements 139 | 140 | **val** supports a few statement constructs such as `if`, `while`, `loop`, `fn`, 141 | `return`, etc. Check out the [grammar](https://github.com/terror/val/blob/master/GRAMMAR.txt) 142 | for all of the various statement types. 143 | 144 | Here's an example showcasing most of them in action: 145 | 146 | ```rust 147 | fn fib(n) { 148 | if (n <= 1) { 149 | return n 150 | } 151 | 152 | return fib(n - 1) + fib(n - 2) 153 | } 154 | 155 | i = 0 156 | 157 | while (i < 10) { 158 | println("fib(" + i + ") = " + fib(i)) 159 | i = i + 1 160 | } 161 | ``` 162 | 163 | ### Expressions 164 | 165 | **val** supports a variety of expressions that can be combined to form more 166 | complex operations: 167 | 168 | | Category | Operation | Syntax | Example | 169 | | -------------- | --------------------- | ----------------------------- | ------------------------------------ | 170 | | **Arithmetic** | Addition | `a + b` | `1 + 2` | 171 | | | Subtraction | `a - b` | `5 - 3` | 172 | | | Multiplication | `a * b` | `4 * 2` | 173 | | | Division | `a / b` | `10 / 2` | 174 | | | Modulo | `a % b` | `7 % 3` | 175 | | | Exponentiation | `a ^ b` | `2 ^ 3` | 176 | | | Negation | `-a` | `-5` | 177 | | **Logical** | And | `a && b` | `true && false` | 178 | | | Or | a || b | true || false | 179 | | | Not | `!a` | `!true` | 180 | | **Comparison** | Equal | `a == b` | `x == 10` | 181 | | | Not Equal | `a != b` | `y != 20` | 182 | | | Less Than | `a < b` | `a < b` | 183 | | | Less Than or Equal | `a <= b` | `i <= 5` | 184 | | | Greater Than | `a > b` | `count > 0` | 185 | | | Greater Than or Equal | `a >= b` | `value >= 100` | 186 | | **Other** | Function Call | `function(args)` | `sin(x)` | 187 | | | List Indexing | `list[index]` | `numbers[0]` | 188 | | | List Creation | `[item1, item2, ...]` | `[1, 2, 3]` | 189 | | | List Concatenation | `list1 + list2` | `[1, 2] + [3, 4]` | 190 | | | String Concatenation | `string1 + string2` | `"Hello, " + name` | 191 | | | Variable Reference | `identifier` | `x` | 192 | 193 | ### Values 194 | 195 | **val** has several primitive value types: 196 | 197 | #### Number 198 | 199 | Numeric values are represented as arbitrary precision floating point numbers (using 200 | [astro_float](https://docs.rs/astro-float/latest/astro_float/index.html) under 201 | the hood): 202 | 203 | ```rust 204 | > pi 205 | 3.141592653589793115997963468544185161590576171875 206 | > e 207 | 2.718281828459045090795598298427648842334747314453125 208 | > sin(2) * e ^ pi * cos(sum([1, 2, 3])) 209 | 16.4814557939128835908118223753548409318930600432600320575175542910885566534716862696709583557263450637540094805515971245058657340687939442764118452427864231041058959960049996970569867866035825048029794926250103816423751837050040821914044725396611746570949840536443560831710407959633707222226883928822125018007 210 | > 211 | ``` 212 | 213 | You can specify the rounding mode, and what sort of precision you'd like to see 214 | in the output by using the `--rounding-mode` and `--precision` options (note 215 | that `--precision` controls binary precision, measured in bits, not decimal 216 | digits). 217 | respectively. 218 | 219 | #### Boolean 220 | 221 | Boolean values represent truth values: 222 | 223 | ```rust 224 | a = true 225 | b = false 226 | c = a && b 227 | d = a || b 228 | e = !a 229 | ``` 230 | 231 | #### String 232 | 233 | Text values enclosed in single or double quotes: 234 | 235 | ```rust 236 | greeting = "Hello" 237 | name = 'World' 238 | message = greeting + ", " + name + "!" 239 | ``` 240 | 241 | #### List 242 | 243 | Collections of values of any type: 244 | 245 | ```rust 246 | numbers = [1, 2, 3, 4, 5] 247 | mixed = [1, "two", true, [3, 4]] 248 | empty = [] 249 | first = numbers[0] 250 | numbers[0] = 10 251 | combined = numbers + [6, 7] 252 | ``` 253 | 254 | #### Function 255 | 256 | A function is a value, and can be used in assignments, passed around to other 257 | functions, etc. 258 | 259 | Check out the [higher order functions example](https://github.com/terror/val/blob/master/examples/hoc.val) 260 | for how this works. 261 | 262 | ```rust 263 | fn reduce(l, f, initial) { 264 | i = 0 265 | 266 | result = initial 267 | 268 | while (i < len(l)) { 269 | result = f(result, l[i]) 270 | i = i + 1 271 | } 272 | 273 | return result 274 | } 275 | 276 | fn sum(a, b) { 277 | return a + b 278 | } 279 | 280 | l = [1, 2, 3, 4, 5] 281 | 282 | println(reduce(l, sum, 0)) 283 | ``` 284 | 285 | #### Null 286 | 287 | Represents the absence of a value. 288 | 289 | ```rust 290 | fn search(l, x) { 291 | i = 0 292 | 293 | while (i < len(l)) { 294 | if (l[i] == x) { 295 | return i 296 | } 297 | 298 | i = i + 1 299 | } 300 | } 301 | 302 | l = [1, 2, 3, 4, 5] 303 | 304 | index = search(l, 6) 305 | 306 | if (index == null) { 307 | println("Value not found") 308 | } else { 309 | println("Value found at index " + index) 310 | } 311 | ``` 312 | 313 | ### Built-ins 314 | 315 | **val** offers a many built-in functions and constants: 316 | 317 | | Category | Function/Constant | Description | Example | 318 | | ----------------- | ------------------- | ---------------------------------- | ------------------------ | 319 | | **Constants** | `pi` | Mathematical constant π (≈3.14159) | `area = pi * r^2` | 320 | | | `e` | Mathematical constant e (≈2.71828) | `growth = e^rate` | 321 | | | `phi` | Golden ratio φ (≈1.61803) | `ratio = phi * width` | 322 | | | `tau` | Tau constant τ (≈6.28318, 2π) | `circum = tau * r` | 323 | | **Trigonometric** | `sin(x)` | Sine of x (radians) | `sin(pi/2)` | 324 | | | `cos(x)` | Cosine of x (radians) | `cos(0)` | 325 | | | `tan(x)` | Tangent of x (radians) | `tan(pi/4)` | 326 | | | `csc(x)` | Cosecant of x (radians) | `csc(pi/6)` | 327 | | | `sec(x)` | Secant of x (radians) | `sec(0)` | 328 | | | `cot(x)` | Cotangent of x (radians) | `cot(pi/4)` | 329 | | **Inverse Trig** | `asin(x)` | Arc sine (-1≤x≤1) | `asin(0.5)` | 330 | | | `acos(x)` | Arc cosine (-1≤x≤1) | `acos(0.5)` | 331 | | | `arc(x)` | Arc tangent | `arc(1)` | 332 | | | `acsc(x)` | Arc cosecant (abs(x)≥1) | `acsc(2)` | 333 | | | `asec(x)` | Arc secant (abs(x)≥1) | `asec(2)` | 334 | | | `acot(x)` | Arc cotangent | `acot(1)` | 335 | | **Hyperbolic** | `sinh(x)` | Hyperbolic sine | `sinh(1)` | 336 | | | `cosh(x)` | Hyperbolic cosine | `cosh(1)` | 337 | | | `tanh(x)` | Hyperbolic tangent | `tanh(1)` | 338 | | **Logarithmic** | `ln(x)` | Natural logarithm | `ln(e)` | 339 | | | `log2(x)` | Base-2 logarithm | `log2(8)` | 340 | | | `log10(x)` | Base-10 logarithm | `log10(100)` | 341 | | | `e(x)` | e raised to power x | `e(2)` | 342 | | **Numeric** | `sqrt(x)` | Square root (x≥0) | `sqrt(16)` | 343 | | | `ceil(x)` | Round up to integer | `ceil(4.3)` | 344 | | | `floor(x)` | Round down to integer | `floor(4.7)` | 345 | | | `abs(x)` | Absolute value | `abs(-5)` | 346 | | | `gcd(a, b)` | Greatest common divisor | `gcd(12, 8)` | 347 | | | `lcm(a, b)` | Least common multiple | `lcm(4, 6)` | 348 | | **Collections** | `len(x)` | Length of list or string | `len("hello")` | 349 | | | `sum(list)` | Sum list elements | `sum([1,2,3])` | 350 | | | `append(list, val)` | Add element to end of list | `append([1,2], 3)` | 351 | | **Conversion** | `int(x)` | Convert to integer | `int("42")` | 352 | | | `float(x)` | Convert to float | `float("3.14")` | 353 | | | `bool(x)` | Convert to boolean | `bool(1)` | 354 | | | `list(x)` | Convert to list | `list("abc")` | 355 | | **I/O** | `print(...)` | Print without newline | `print("Hello")` | 356 | | | `println(...)` | Print with newline | `println("World")` | 357 | | | `input([prompt])` | Read line from stdin | `name = input("Name: ")` | 358 | | **String** | `split(str, delim)` | Split string | `split("a,b,c", ",")` | 359 | | | `join(list, delim)` | Join list elements | `join(["a","b"], "-")` | 360 | | **Program** | `exit([code])` | Exit program | `exit(1)` | 361 | | | `quit([code])` | Alias for exit | `quit(0)` | 362 | 363 | ## Prior Art 364 | 365 | [bc(1)](https://linux.die.net/man/1/bc) - An arbitrary precision calculator 366 | language 367 | -------------------------------------------------------------------------------- /src/highlighter.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | const COLOR_BOOLEAN: &str = "\x1b[33m"; // Yellow 4 | const COLOR_ERROR: &str = "\x1b[31m"; // Red 5 | const COLOR_FUNCTION: &str = "\x1b[34m"; // Blue 6 | const COLOR_IDENTIFIER: &str = "\x1b[37m"; // White 7 | const COLOR_KEYWORD: &str = "\x1b[35m"; // Magenta 8 | const COLOR_NUMBER: &str = "\x1b[33m"; // Yellow 9 | const COLOR_OPERATOR: &str = "\x1b[36m"; // Cyan 10 | const COLOR_RESET: &str = "\x1b[0m"; 11 | const COLOR_STRING: &str = "\x1b[32m"; // Green 12 | 13 | pub struct TreeHighlighter<'src> { 14 | content: &'src str, 15 | } 16 | 17 | impl<'src> TreeHighlighter<'src> { 18 | pub fn new(content: &'src str) -> Self { 19 | Self { content } 20 | } 21 | 22 | pub fn highlight(&self) -> Cow<'src, str> { 23 | match parse(self.content) { 24 | Ok(ast) => self.colorize_ast(&ast), 25 | Err(_) => { 26 | Owned(format!("{}{}{}", COLOR_ERROR, self.content, COLOR_RESET)) 27 | } 28 | } 29 | } 30 | 31 | fn colorize_ast(&self, program: &Spanned>) -> Cow<'src, str> { 32 | let mut color_spans = Vec::new(); 33 | self.collect_color_spans(program, &mut color_spans); 34 | color_spans.sort_by_key(|span| span.0); 35 | self.apply_color_spans(&color_spans) 36 | } 37 | 38 | fn apply_color_spans( 39 | &self, 40 | spans: &[(usize, usize, &str)], 41 | ) -> Cow<'src, str> { 42 | if spans.is_empty() { 43 | return Cow::Borrowed(self.content); 44 | } 45 | 46 | let mut result = 47 | String::with_capacity(self.content.len() + spans.len() * 10); 48 | 49 | let mut last_end = 0; 50 | 51 | for &(start, end, color) in spans { 52 | if start > last_end { 53 | result.push_str(&self.content[last_end..start]); 54 | } 55 | 56 | result.push_str(color); 57 | result.push_str(&self.content[start..end]); 58 | result.push_str(COLOR_RESET); 59 | 60 | last_end = end; 61 | } 62 | 63 | if last_end < self.content.len() { 64 | result.push_str(&self.content[last_end..]); 65 | } 66 | 67 | Owned(result) 68 | } 69 | 70 | fn collect_color_spans( 71 | &self, 72 | program: &Spanned>, 73 | spans: &mut Vec<(usize, usize, &'static str)>, 74 | ) { 75 | let (node, _) = program; 76 | 77 | match node { 78 | Program::Statements(statements) => { 79 | for statement in statements { 80 | self.collect_statement_spans(statement, spans); 81 | } 82 | } 83 | } 84 | } 85 | 86 | fn collect_statement_spans( 87 | &self, 88 | statement: &Spanned>, 89 | spans: &mut Vec<(usize, usize, &'static str)>, 90 | ) { 91 | let (node, span) = statement; 92 | 93 | let (start, end) = (span.start, span.end); 94 | 95 | match node { 96 | Statement::Assignment(lhs, rhs) => { 97 | self.collect_expression_spans(lhs, spans); 98 | 99 | if let Some(eq_pos) = self.content[start..end].find('=') { 100 | spans.push((start + eq_pos, start + eq_pos + 1, COLOR_OPERATOR)); 101 | } 102 | 103 | self.collect_expression_spans(rhs, spans); 104 | } 105 | Statement::Break => { 106 | if let Some(break_pos) = self.content[start..end].find("break") { 107 | spans.push((start + break_pos, start + break_pos + 5, COLOR_KEYWORD)); 108 | } 109 | } 110 | 111 | Statement::Block(statements) => { 112 | if let Some(open_brace) = self.content[start..end].find('{') { 113 | spans.push(( 114 | start + open_brace, 115 | start + open_brace + 1, 116 | COLOR_OPERATOR, 117 | )); 118 | } 119 | 120 | if let Some(close_brace) = self.content[start..end].rfind('}') { 121 | spans.push(( 122 | start + close_brace, 123 | start + close_brace + 1, 124 | COLOR_OPERATOR, 125 | )); 126 | } 127 | 128 | for statement in statements { 129 | self.collect_statement_spans(statement, spans); 130 | } 131 | } 132 | Statement::Continue => { 133 | if let Some(continue_pos) = self.content[start..end].find("continue") { 134 | spans.push(( 135 | start + continue_pos, 136 | start + continue_pos + 8, 137 | COLOR_KEYWORD, 138 | )); 139 | } 140 | } 141 | Statement::Expression(expression) => { 142 | self.collect_expression_spans(expression, spans); 143 | } 144 | Statement::Function(name, params, body) => { 145 | if let Some(fn_pos) = self.content[start..end].find("fn") { 146 | spans.push((start + fn_pos, start + fn_pos + 2, COLOR_KEYWORD)); 147 | } 148 | 149 | let name_span = self.find_identifier_span(start, name); 150 | 151 | if let Some((name_start, name_end)) = name_span { 152 | spans.push((name_start, name_end, COLOR_FUNCTION)); 153 | } 154 | 155 | for param in params { 156 | let param_span = self.find_identifier_span(start, param); 157 | if let Some((param_start, param_end)) = param_span { 158 | spans.push((param_start, param_end, COLOR_IDENTIFIER)); 159 | } 160 | } 161 | 162 | if let Some(open_paren) = self.content[start..end].find('(') { 163 | spans.push(( 164 | start + open_paren, 165 | start + open_paren + 1, 166 | COLOR_OPERATOR, 167 | )); 168 | } 169 | 170 | if let Some(close_paren) = self.content[start..end].find(')') { 171 | spans.push(( 172 | start + close_paren, 173 | start + close_paren + 1, 174 | COLOR_OPERATOR, 175 | )); 176 | } 177 | 178 | if let Some(open_brace) = self.content[start..end].find('{') { 179 | spans.push(( 180 | start + open_brace, 181 | start + open_brace + 1, 182 | COLOR_OPERATOR, 183 | )); 184 | } 185 | 186 | if let Some(close_brace) = self.content[start..end].rfind('}') { 187 | spans.push(( 188 | start + close_brace, 189 | start + close_brace + 1, 190 | COLOR_OPERATOR, 191 | )); 192 | } 193 | 194 | for statement in body { 195 | self.collect_statement_spans(statement, spans); 196 | } 197 | } 198 | Statement::If(condition, then_branch, else_branch) => { 199 | if let Some(if_pos) = self.content[start..end].find("if") { 200 | spans.push((start + if_pos, start + if_pos + 2, COLOR_KEYWORD)); 201 | } 202 | 203 | self.collect_expression_spans(condition, spans); 204 | 205 | for statement in then_branch { 206 | self.collect_statement_spans(statement, spans); 207 | } 208 | 209 | if let Some(else_statements) = else_branch { 210 | if let Some(else_pos) = self.content[start..end].find("else") { 211 | spans.push((start + else_pos, start + else_pos + 4, COLOR_KEYWORD)); 212 | } 213 | 214 | for statement in else_statements { 215 | self.collect_statement_spans(statement, spans); 216 | } 217 | } 218 | } 219 | Statement::Loop(body) => { 220 | if let Some(loop_pos) = self.content[start..end].find("loop") { 221 | spans.push((start + loop_pos, start + loop_pos + 4, COLOR_KEYWORD)); 222 | } 223 | 224 | if let Some(open_brace) = self.content[start..end].find('{') { 225 | spans.push(( 226 | start + open_brace, 227 | start + open_brace + 1, 228 | COLOR_OPERATOR, 229 | )); 230 | } 231 | 232 | if let Some(close_brace) = self.content[start..end].rfind('}') { 233 | spans.push(( 234 | start + close_brace, 235 | start + close_brace + 1, 236 | COLOR_OPERATOR, 237 | )); 238 | } 239 | 240 | for statement in body { 241 | self.collect_statement_spans(statement, spans); 242 | } 243 | } 244 | Statement::Return(expr_opt) => { 245 | if let Some(return_pos) = self.content[start..end].find("return") { 246 | spans.push(( 247 | start + return_pos, 248 | start + return_pos + 6, 249 | COLOR_KEYWORD, 250 | )); 251 | } 252 | 253 | if let Some(expr) = expr_opt { 254 | self.collect_expression_spans(expr, spans); 255 | } 256 | } 257 | Statement::While(condition, body) => { 258 | if let Some(while_pos) = self.content[start..end].find("while") { 259 | spans.push((start + while_pos, start + while_pos + 5, COLOR_KEYWORD)); 260 | } 261 | 262 | self.collect_expression_spans(condition, spans); 263 | 264 | if let Some(open_paren) = self.content[start..end].find('(') { 265 | spans.push(( 266 | start + open_paren, 267 | start + open_paren + 1, 268 | COLOR_OPERATOR, 269 | )); 270 | } 271 | 272 | if let Some(close_paren) = self.content[start..end].find(')') { 273 | spans.push(( 274 | start + close_paren, 275 | start + close_paren + 1, 276 | COLOR_OPERATOR, 277 | )); 278 | } 279 | 280 | for statement in body { 281 | self.collect_statement_spans(statement, spans); 282 | } 283 | } 284 | } 285 | } 286 | 287 | fn collect_expression_spans( 288 | &self, 289 | expression: &Spanned>, 290 | spans: &mut Vec<(usize, usize, &'static str)>, 291 | ) { 292 | let (node, span) = expression; 293 | 294 | let (start, end) = (span.start, span.end); 295 | 296 | match node { 297 | Expression::BinaryOp(op, lhs, rhs) => { 298 | self.collect_expression_spans(lhs, spans); 299 | self.collect_expression_spans(rhs, spans); 300 | 301 | let op_str = op.to_string(); 302 | 303 | if let Some(op_pos) = self.find_operator(&op_str, lhs, rhs) { 304 | spans.push((op_pos, op_pos + op_str.len(), COLOR_OPERATOR)); 305 | } 306 | } 307 | Expression::Boolean(value) => { 308 | let value_str = if *value { "true" } else { "false" }; 309 | 310 | if let Some(bool_pos) = self.content[start..end].find(value_str) { 311 | spans.push(( 312 | start + bool_pos, 313 | start + bool_pos + value_str.len(), 314 | COLOR_BOOLEAN, 315 | )); 316 | } 317 | } 318 | Expression::FunctionCall(name, arguments) => { 319 | let name_span = self.find_identifier_span(start, name); 320 | 321 | if let Some((name_start, name_end)) = name_span { 322 | spans.push((name_start, name_end, COLOR_FUNCTION)); 323 | } 324 | 325 | if let Some(open_paren) = self.content[start..end].find('(') { 326 | spans.push(( 327 | start + open_paren, 328 | start + open_paren + 1, 329 | COLOR_OPERATOR, 330 | )); 331 | } 332 | 333 | if let Some(close_paren) = self.content[start..end].rfind(')') { 334 | spans.push(( 335 | start + close_paren, 336 | start + close_paren + 1, 337 | COLOR_OPERATOR, 338 | )); 339 | } 340 | 341 | for argument in arguments { 342 | self.collect_expression_spans(argument, spans); 343 | } 344 | } 345 | Expression::Identifier(name) => { 346 | let name_span = self.find_identifier_span(start, name); 347 | 348 | if let Some((name_start, name_end)) = name_span { 349 | spans.push((name_start, name_end, COLOR_IDENTIFIER)); 350 | } 351 | } 352 | Expression::List(items) => { 353 | if let Some(open_bracket) = self.content[start..end].find('[') { 354 | spans.push(( 355 | start + open_bracket, 356 | start + open_bracket + 1, 357 | COLOR_OPERATOR, 358 | )); 359 | } 360 | 361 | if let Some(close_bracket) = self.content[start..end].rfind(']') { 362 | spans.push(( 363 | start + close_bracket, 364 | start + close_bracket + 1, 365 | COLOR_OPERATOR, 366 | )); 367 | } 368 | 369 | for item in items { 370 | self.collect_expression_spans(item, spans); 371 | } 372 | } 373 | Expression::ListAccess(list, index) => { 374 | self.collect_expression_spans(list, spans); 375 | 376 | if let Some(open_bracket) = self.content[list.1.end..end].find('[') { 377 | spans.push(( 378 | list.1.end + open_bracket, 379 | list.1.end + open_bracket + 1, 380 | COLOR_OPERATOR, 381 | )); 382 | } 383 | 384 | if let Some(close_bracket) = self.content[list.1.end..end].rfind(']') { 385 | spans.push(( 386 | list.1.end + close_bracket, 387 | list.1.end + close_bracket + 1, 388 | COLOR_OPERATOR, 389 | )); 390 | } 391 | 392 | self.collect_expression_spans(index, spans); 393 | } 394 | Expression::Null => { 395 | if let Some(null_pos) = self.content[start..end].find("null") { 396 | spans.push((start + null_pos, start + null_pos + 4, COLOR_KEYWORD)); 397 | } 398 | } 399 | Expression::Number(_) => { 400 | let number_pattern = self.find_number_span(start, end); 401 | 402 | if let Some((num_start, num_end)) = number_pattern { 403 | spans.push((num_start, num_end, COLOR_NUMBER)); 404 | } 405 | } 406 | Expression::String(value) => { 407 | let quoted_value = format!("'{}'", value); 408 | 409 | if let Some(str_pos) = self.content[start..end].find("ed_value) { 410 | spans.push(( 411 | start + str_pos, 412 | start + str_pos + quoted_value.len(), 413 | COLOR_STRING, 414 | )); 415 | } else { 416 | let double_quoted = format!("\"{}\"", value); 417 | 418 | if let Some(str_pos) = self.content[start..end].find(&double_quoted) { 419 | spans.push(( 420 | start + str_pos, 421 | start + str_pos + double_quoted.len(), 422 | COLOR_STRING, 423 | )); 424 | } 425 | } 426 | } 427 | Expression::UnaryOp(op, expr) => { 428 | let op_str = op.to_string(); 429 | 430 | if let Some(op_pos) = self.content[start..expr.1.start].find(&op_str) { 431 | spans.push(( 432 | start + op_pos, 433 | start + op_pos + op_str.len(), 434 | COLOR_OPERATOR, 435 | )); 436 | } 437 | 438 | self.collect_expression_spans(expr, spans); 439 | } 440 | } 441 | } 442 | 443 | fn find_identifier_span( 444 | &self, 445 | start_search: usize, 446 | name: &str, 447 | ) -> Option<(usize, usize)> { 448 | Regex::new(&format!(r"\b{}\b", regex::escape(name))) 449 | .ok()? 450 | .find(&self.content[start_search..]) 451 | .map(|mat| (start_search + mat.start(), start_search + mat.end())) 452 | } 453 | 454 | fn find_operator( 455 | &self, 456 | op: &str, 457 | lhs: &Spanned>, 458 | rhs: &Spanned>, 459 | ) -> Option { 460 | let (start, end) = (lhs.1.end, rhs.1.start); 461 | 462 | self.content[start..end].find(op).map(|pos| start + pos) 463 | } 464 | 465 | fn find_number_span( 466 | &self, 467 | start: usize, 468 | end: usize, 469 | ) -> Option<(usize, usize)> { 470 | Regex::new(r"[-+]?\d+(\.\d+)?") 471 | .ok()? 472 | .find(&self.content[start..end]) 473 | .map(|mat| (start + mat.start(), start + mat.end())) 474 | } 475 | } 476 | 477 | pub struct Highlighter { 478 | completer: FilenameCompleter, 479 | hinter: HistoryHinter, 480 | } 481 | 482 | impl Highlighter { 483 | pub fn new() -> Self { 484 | Self { 485 | completer: FilenameCompleter::new(), 486 | hinter: HistoryHinter::new(), 487 | } 488 | } 489 | } 490 | 491 | impl Completer for Highlighter { 492 | type Candidate = Pair; 493 | fn complete( 494 | &self, 495 | line: &str, 496 | pos: usize, 497 | ctx: &Context<'_>, 498 | ) -> Result<(usize, Vec), ReadlineError> { 499 | self.completer.complete(line, pos, ctx) 500 | } 501 | } 502 | 503 | impl Helper for Highlighter {} 504 | 505 | impl Hinter for Highlighter { 506 | type Hint = String; 507 | 508 | fn hint(&self, line: &str, a: usize, b: &Context) -> Option { 509 | self.hinter.hint(line, a, b) 510 | } 511 | } 512 | 513 | impl RustylineHighlighter for Highlighter { 514 | fn highlight_char(&self, _: &str, _: usize, _: CmdKind) -> bool { 515 | true 516 | } 517 | 518 | fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { 519 | Owned(format!("\x1b[90m{}\x1b[0m", hint)) 520 | } 521 | 522 | fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { 523 | TreeHighlighter::new(line).highlight() 524 | } 525 | } 526 | 527 | impl Validator for Highlighter {} 528 | --------------------------------------------------------------------------------