aboutsummaryrefslogtreecommitdiff
path: root/doc/DESIGN.md
blob: f9e3cef640689673d9b8edd2e249574b58d48c82 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# 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);
    }
}
```