# Amalgam design document Amalgam is a simple language with few keywords but at the same time is very powerful. All functions are closures. Assigning a closure to a variable is how you make regular functions. ## Hello world ``` const main = fn { stderr.writeln("hello, world!") } ``` ## Conditions ``` const main = fn { var value = 23 + 50; if value == 0 stderr.writeln("zero!"); else if value < 23 stderr.writeln("less!"); else stderr.writeln("more!"); while value > 0 { stderr.writeln("value: {}", value); value -= 1; } } ``` ## Closure Parentheses after `fn` is only required when the closure has parameters or returns data ``` const apply = fn(func: fn() bool) { const result = func(); } const main = fn { apply(fn() bool { return true; }); // Or store in a variable and use it const func = fn { return true; } apply(func); } ``` ## Data types ``` const main = fn() !void { var v1: i32 = 50; var v2: u32 = 50; v1 = v2; // error, v2 can't be implicitly cast to v1 because i32 can't represent the same values as u32 v1 = @cast(i32, v2); // ok, explicitly cast u32 to i32 var str1 = "hello"; var str2 = "world"; var str3 = try str1 + " " + str2; // `try` is needed here, because str concat can fail var str4 = try str1 + 20; // error, can't add number to string. Preferable use str.fmt or explicitly cast to string var str5 = try str1 + str(20); // ok, number explicitly cast to string const str6 = "hello"; const str7 = "world"; const str8 = str6 + " " + str7; // ok, all variable involved are const. They can be combined at compile-time stderr.writeln("{}, {} | {}", str1, str2, str3); // prints hello world | hello world } ``` ## Dynamic allocation (array) ``` const ArrayList = @import("std.array.ArrayList"); const main = fn() !void { var list = ArrayList(i32); try list.add(23); try list.add(50); var value = list.get(40); for val in list { stdout.writeln("value: {}", val); } } ``` ## Structures and instances ``` const User = struct { name: str, age: i32, level = 1 // default value is 1 and type is i32 } const levelUp = fn(self: &User) { self.level += 1; } const main = fn { const user1 = User { name: "John", age: 24 } var user2 = User { name: "Titor", age: 50, level: 100 } levelUp(user2); // syntax sugar for calling a function with the first argument as // the variable before the dot (same thing as levelUp(user2)) user2.levelUp(); } ``` ## Named parameters Functions call be called with arguments in position that matches the parameters or by using the names of the parameters. ``` const User = struct { name: str, age: i32, level: i32 } const createUser = fn(name: str, age: i32, level: i32 = 1) User { return User { name: name, age: age, level: level } } const main = fn { createUser(name: "John", level: 30, age: 30); createUser(age: 40, name: "Titor"); } ``` ## Encapsulation By default declared data can't be used outside the file they were declared in. To make the declared data accessible from other files you can use the `pub` keyword. json.amal ``` pub const Object = struct { // fields... } pub const to_object = fn(data: &str) Object { // code to convert string to Object... } ``` main.amal: ``` const json = @import("json.amal"); const main = fn { // Type is json.Object var json_obj = json.to_object("{ \"key\": 42 }"); } ``` ## Generic programming ``` const add = fn(comptime T: type, a: T, b: T) !T { return try a + b; } const main = fn { var numberValue = add(20, 40); var stringValue = add("hello", "world"); } ``` ## Ownership Like Rust, Amalgam has a concept of ownership but with less cumbersome syntax. There is one issue with ownership and that is references to data that gets reallocated. Rust doesn't handle this but Amalgam does it using #reallocatable(instance). Reallocatable should be ignored if the reference that taken from the reallocatable memory doesn't change location after realloc, which would be the case for pointers. ``` const ArrayList = @import("std.array.ArrayList"); const User = struct { name: str, level: i32 } const addUserToList = fn(list: &ArrayList(User), user: User) { // this is not actually needed for ArrayList because ArrayList uses #reallocatable internally for list.add and list.remove @reallocatable list.add(move user) } const main = fn { var users = ArrayList(User); users.add(User { name: "John", level: 34 }); const user1 = User { name: "David", level: 55 } // error, addUserToList expects user1 to be moved or copied // addUserToList(users, user1); // addUserToList(users, clone user1); // ok, user1 has been copied to function scope addUserToList(users, move user1); // ok, user1 has been moved to function scope next(move users); } const getUserAtIndex = fn(list: &ArrayList(User), index: usize) User { return list.get(index); } const next = fn(users: ArrayList(User)) { const user = getUserAtIndex(users, 0); // Reallocatable example: addUserToList(users, User { name: "John", level: 34 }); // error, "user" can't be safely used because addUserToList on line XXX can reallocate "users" which "user" belongs to stdout.writeln("user name: {}", user.name); } ``` ## Table (inspired by lua) ``` const main = fn { const values = { "name": "John", "age": 42, "dogs": [ "spot", "doggy" ] } printMap(values); // stdout.writeln("{}", values) can also be used directly as it supports tables } const printTable = fn(value: TableValue) { switch @type(value) { array => { // value type is automatically cast to array here, same with other cases in the switch for index, val in value { stdout.write("[{}] = ", index); printTable(val); stdout.writeln(","); } } map => { stdout.writeln("{") for key, val in value { stdout.write("'{}': ", key); printTable(val); stdout.writeln(","); } stdout.writeln("}"); } else => stdout.write(value); } } ```