diff --git a/README.md b/README.md index 65fac8f..66f5712 100644 --- a/README.md +++ b/README.md @@ -6,51 +6,57 @@ Advent of Code 2024. For the language to be almost operational it should have: -- an interpreter, -- syntax highlighting, -- basic LSP. +- [x] an interpreter, +- [ ] syntax highlighting, +- [ ] basic LSP. -## Wishlist: +## Usage + +1. Clone this repository: + ```sh + git clone https://github.com/viddrobnic/aoc-lang.git + ``` +2. Build and install the interpreter: + ```sh + cd aoc-lang + cargo install --path . + ``` +3. Run some code: + ```sh + aoc-lang examples/hello_world.aoc + ``` + +## Language features + +AoC language supports the following features: -### Language features: - -- [x] integers - - from float - - from string -- [x] floats - - from int - - from string -- [x] booleans - - from string -- [x] strings - - concatenate - - append - - split -- [x] arrays - - concatenate - - push - - pop - - unpacking via assignment: `[a, b] = [42, "foo"]` -- [x] hash maps - - add - - remove -- [x] arithmetic operations (`+`, `-`, `*`, `/`, `%`) -- [x] bit-wise operations (`&`, `|`, `!`) -- [x] comparison operations (`<`, `>`, `<=`, `>=`, `==`, `!=`) -- [x] logical operations (`!`, `&`, `|`) -- [x] variables -- [x] if/else statements -- [x] while loop -- [x] for loop -- [x] break -- [x] continue -- [x] functions - - return - - recursion -- [x] comments -- [x] stdin, stdout -- [x] imports -- [x] error reporting with line numbers +- integers +- floats +- booleans +- strings: +- arrays +- hash maps +- arithmetic operations (`+`, `-`, `*`, `/`, `%`) +- bit-wise operations (`&`, `|`, `!`) +- comparison operations (`<`, `>`, `<=`, `>=`, `==`, `!=`) +- logical operations (`!`, `&`, `|`) +- variables +- multi variable assignment (`[a, b] = [10, 20]`) +- if/else statements +- while loop +- for loop +- break +- continue +- functions +- comments +- stdin, stdout +- imports +- error reporting with line numbers + +For more detailed overview of the syntax, see `examples` directory, +which contains examples of code with comments. + +## Wishlist: ### Syntax highlighting diff --git a/examples/aoc_day_01.aoc b/examples/aoc_day_01.aoc new file mode 100644 index 0000000..a7fd9e1 --- /dev/null +++ b/examples/aoc_day_01.aoc @@ -0,0 +1,96 @@ +// Solution for day 01 of advent of code 2024 +// https://adventofcode.com/2023/day/1 +// +// Run it as: +// aoc-lang examples/aoc_day_01.aoc < input + +// Read input +data = [] +for (line = input(); line; line = input()) { + push(data, line) +} + +// Part one +res = 0 +for (i = 0; i < len(data); i = i + 1) { + chars = split(data[i], "") + n = 0 + + // First number + for (j = 0; j < len(chars); j = j + 1) { + if (int(chars[j])) { + n = int(chars[j]) + break + } + } + + // Last number + for (j = len(chars) - 1; j >= 0; j = j - 1) { + if (int(chars[j])) { + n = n * 10 + int(chars[j]) + break + } + } + + res = res + n +} + +print("Part one: " + str(res)) + +// Part two +substr_is = fn(target_ch, position, lookup_ch) { + if (position + len(lookup_ch) > len(target_ch)) { + return false + } + + for (i = 0; i < len(lookup_ch); i = i + 1) { + if (target_ch[position + i] != lookup_ch[i]) { + return false + } + } + + return true +} + +digits = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"] +digit = fn(chars, position) { + if (int(chars[position])) { + return int(chars[position]) + } + + for (d = 0; d < len(digits); d = d + 1) { + if (substr_is(chars, position, split(digits[d], ""))) { + return d + 1 + } + } +} + + +res = 0 +for (i = 0; i < len(data); i = i + 1) { + chars = split(data[i], "") + n = 0 + + // First number + for (j = 0; j < len(chars); j = j + 1) { + d = digit(chars, j) + if (d) { + n = d + break + } + + } + + // Last number + for (j = len(chars) - 1; j >= 0; j = j - 1) { + d = digit(chars, j) + if (d) { + n = n * 10 + d + break + } + } + + res = res + n +} + +print("Part two: " + str(res)) diff --git a/examples/fibonacci.aoc b/examples/fibonacci.aoc new file mode 100644 index 0000000..95162c8 --- /dev/null +++ b/examples/fibonacci.aoc @@ -0,0 +1,15 @@ +// Recursive fibonacci function implementation, +// which also uses a for loop to print the first +// 10 number in the sequence. + +fib = fn(n) { + if (n <= 2) { + return 1 + } else { + fib(n-1) + fib(n-2) + } +} + +for (i = 1; i <= 10; i = i + 1) { + print(fib(i)) +} diff --git a/examples/hello_world.aoc b/examples/hello_world.aoc new file mode 100644 index 0000000..139a060 --- /dev/null +++ b/examples/hello_world.aoc @@ -0,0 +1,3 @@ +print("What is your name?") +name = input() +print("Hello " + name + "!") diff --git a/examples/overview.aoc b/examples/overview.aoc new file mode 100644 index 0000000..35901e3 --- /dev/null +++ b/examples/overview.aoc @@ -0,0 +1,208 @@ +// This file contains the overview of the language. +// Let's start with the basics: primitive data types. + +// We have ints: +42 + +// floats +4.2 + +// booleans +true +false + +// string +"foo" + +// We have two composite data types, that we will take a look at later +// First, variables: +foo = 42 +bar = 4.2 +[x, y] = [-1, 1] + +// As one would expect, we have all the binary operations you could want: +1 == 1 +1 < 2 == 4 > 3 +1 + 2 * 3 - 4 / 5 + (-1 * 2) + +// We also have modulo, which is euiclid rem, and not the weird c thing +-1 % 2 == 1 + +// Now we can move onto if/else +if (1 < 2) { + print("math works!") +} else if (1 == 2) { + print("huh, that's weird...") +} else { + print("how did we get here???") +} + +// The fun part about if and else is, that it's an expression +string = "foo" +length = if (len(string) < 3) { + "short" +} else if (len(string) >= 3 & len(string) < 5 ) { + "medium" +} else { + "long" +} +print(length) // short + +// Let's take a look at loops now +i = 0 +sum = 0 +while (i < 10) { + sum = sum + i + i = i + 1 +} +print(sum) + +// That is a bit long. We can use for loop instead! +sum = 0 +for (i = 0; i < 10; i = i + 1) { + sum = sum + i +} +print(sum) + +// if/else and loops take anything, not just booleans. We have +// is truthy behavior. Additionally, we don't have exceptions, things +// just return null most of the time. We can use this two things together +// to check if stuff was successful. For instance, conversion from string to int: +foo = "10" +if (int(foo)) { + // We have an int, let's double it + print("foo * 2 = " + str(int(foo) * 2)) +} + +foo = "asdf" +if (int(foo)) { + // Do something with an int +} else { + print("foo is not an int :(") +} + + +// Now let's return to composite data types. First arrays: +arr = [1, 2] +print(len(arr)) // 2 + +// We can add an element to it +push(arr, 3) + +// And we can remove it +last = pop(arr) +print(last) // 3 + +// We can also access any element we want +for (i = 0; i < len(arr); i = i + 1) { + print(arr[i]) +} + +// Access outside of bounds return null: +print(arr[-1]) // null + +// Second, dictionaries. +dict = {"foo": 42} + +// We can add an element to it +dict["bar"] = 69 +print(dict["bar"]) + +// We can also remove it +print(del(dict, "bar")) // 69 +print(dict["bar"]) // null + +// We also have a syntax sugar for accessing dictionary elements. +// If the key is a string in the form of identifier, we can use +// a dot notation: +print(dict.foo) // 42 + +// Combined with closures, this makes dictionaries a poor man version +// of objects :) + +// Speaking of closures: +fun = fn() { return 420 } +print(fun()) + +// Since everything is an expression, we can omit return +fun = fn() { 420 } +print(fun()) + +// Let's look at the poor man's objects: +obj = { + "value": 21, +} +obj.double = fn() { + obj.value * 2 +} +print(obj.double()) + +// And lastly, of course we have recursion: +rec_sum = fn(n) { + if (n == 0) { + return 0 + } + + rec_sum(n-1) + n +} +print(rec_sum(10)) // 55 + +// Now let's do a speedrun through the builtin functions: +len([1, 2]) // 2 +len({1: 2}) // 1 +len("foo") // 3 + +str(1) // "1" +str(1.2) // "1.2" +str(true) // "true" +str("foo") // "foo" + +int(1) // 1 +int(1.1) // 1 +int("1") // 1 +int("foo") // null + +float(1.1) // 1.1 +float(1) // 1.0 +float("1.1") // 1.1 +float("asdf") // null + +bool(false) // false +bool("true") // true + +floor(1.9) // 1.0 +ceil(1.1) // 2.0 +round(1.4) // 1.0 +round(1.6) // 2.0 + +trim_start(" asdf ") // "asdf " +trim_end(" asdf ") // " asdf" +trim(" asdf ") // "asdf" + +split("foo bar", " ") // ["foo", "bar"] +split("asdf", "") // ["a", "s", "d", "f"] +split("ab,bc", ",") // ["ab", "bc"] + +push([], 1) // [1] +pop([]) // null +pop([1]) // 1 + +del({}, "foo") // null +del({"foo": 42}, "foo") // 42 + +print("asdf") // prints stuff to stdout +// input() reads a single line from stdin. Returns null if eof. +// See `examples/hello_world.aoc` + +// This file is getting a little long now, perhaps we would like to split it +// into two. We can do that with `use`: +// use "import.aoc" +// Use will execute the file as a function in a context, where current variables +// are not available. Whatever the function returns (implicitly as last expression, +// or explicitly with return), is returned by the use statements. +// This means that an imported file could return a function, and we could do: +// fun = use "import_fun.aoc" +// fun() + +// See `src/runtime/test_import/` for examples + diff --git a/runtime/src/builtin.rs b/runtime/src/builtin.rs index 6c2bdbf..2b737b4 100644 --- a/runtime/src/builtin.rs +++ b/runtime/src/builtin.rs @@ -2,7 +2,7 @@ use std::{fmt::Display, io, rc::Rc}; use crate::{ error::ErrorKind, - object::{self, Array, Object}, + object::{self, Array, Dictionary, HashKey, Object}, vm::gc::GarbageCollector, }; @@ -26,6 +26,7 @@ pub enum Builtin { Push, Pop, + Del, Print, Input, @@ -48,6 +49,7 @@ impl Display for Builtin { Builtin::Split => write!(f, "split"), Builtin::Push => write!(f, "push"), Builtin::Pop => write!(f, "pop"), + Builtin::Del => write!(f, "del"), Builtin::Print => write!(f, "print"), Builtin::Input => write!(f, "input"), } @@ -71,6 +73,7 @@ impl Builtin { "split" => Self::Split, "push" => Self::Push, "pop" => Self::Pop, + "del" => Self::Del, "print" => Self::Print, "input" => Self::Input, @@ -102,6 +105,7 @@ impl Builtin { Builtin::Push => call_push(args), Builtin::Pop => call_pop(args), + Builtin::Del => call_del(args), Builtin::Print => call_print(args), Builtin::Input => call_input(args), @@ -207,6 +211,7 @@ fn call_bool(args: &[Object]) -> Result { validate_args_len(args, 1)?; let res = match &args[0] { + Object::Boolean(bool) => *bool, Object::String(str) => match str.parse() { Ok(res) => res, Err(_) => return Ok(Object::Null), @@ -320,6 +325,25 @@ fn call_pop(args: &[Object]) -> Result { } } +fn call_del(args: &[Object]) -> Result { + validate_args_len(args, 2)?; + + let Object::Dictionary(Dictionary(dict)) = &args[0] else { + return Err(ErrorKind::InvalidBuiltinArg { + builtin: Builtin::Del, + data_type: (&args[0]).into(), + }); + }; + + let key: HashKey = args[1].clone().try_into()?; + let rc = dict.value.upgrade().unwrap(); + let obj = rc.borrow_mut().remove(&key); + match obj { + Some(obj) => Ok(obj), + None => Ok(Object::Null), + } +} + fn call_print(args: &[Object]) -> Result { validate_args_len(args, 1)?; diff --git a/runtime/src/error.rs b/runtime/src/error.rs index 360d02b..0fcf133 100644 --- a/runtime/src/error.rs +++ b/runtime/src/error.rs @@ -48,6 +48,7 @@ pub enum ErrorKind { expected: usize, got: usize, }, + IndexOutOfBounds, InvalidBuiltinArg { builtin: Builtin, @@ -134,6 +135,7 @@ impl Display for ErrorKind { ErrorKind::ReturnOutsideOfFunction => write!(f, "Return can't be used outside of a function."), ErrorKind::InvalidFunctionCalee(dt) => write!(f,"Can only call functions, not {dt}"), ErrorKind::InvalidNrOfArgs { expected, got } => write!(f, "Invalid number of arguments, expected: {expected}, got: {got}"), + ErrorKind::IndexOutOfBounds => write!(f, "Index you are assigning to is out of bounds"), ErrorKind::InvalidBuiltinArg { builtin, data_type } => write!(f, "Can't call {builtin} on {data_type}."), ErrorKind::InputError => write!(f, "Could not read from stdin"), diff --git a/runtime/src/vm/mod.rs b/runtime/src/vm/mod.rs index e86c54b..9ae81a4 100644 --- a/runtime/src/vm/mod.rs +++ b/runtime/src/vm/mod.rs @@ -297,9 +297,17 @@ impl VirtualMachine { return Err(ErrorKind::InvalidIndexType(index.into())); }; - // TODO: Handle out of bounds + if idx < 0 { + return Err(ErrorKind::IndexOutOfBounds); + } + let idx = idx as usize; + let rc = arr.0.value.upgrade().unwrap(); - rc.borrow_mut()[idx as usize] = value; + let mut arr = rc.borrow_mut(); + if idx >= arr.len() { + return Err(ErrorKind::IndexOutOfBounds); + } + arr[idx] = value; } Object::Dictionary(dict) => { let key: HashKey = index.try_into()?; diff --git a/runtime/src/vm/test.rs b/runtime/src/vm/test.rs index 4eafa10..bc904d4 100644 --- a/runtime/src/vm/test.rs +++ b/runtime/src/vm/test.rs @@ -752,6 +752,18 @@ fn builtin_pop() { } } +#[test] +fn builtin_del() { + let tests = [ + ("del({}, \"foo\")", Object::Null), + ("del({\"foo\": 42}, \"foo\")", Object::Integer(42)), + ]; + + for (input, expected) in tests { + run_test(input, Ok(expected)); + } +} + #[test] fn use_statement() { let tests = [