TypeScript Syntax, Rust Borrow Checker, Go Philosophies
8 |
9 |
10 |
High Performance, Fearless Concurrency, Compile Time Checks
11 |
12 |
13 |
No Garbage Collection, Memory Safety Guarantee
14 |
15 |
16 |
17 | ## Hello World
18 |
19 | ```typescript
20 | import console from '@std/console'
21 |
22 | function main() {
23 | const text = 'Hello World'
24 | console.log(read text)
25 | }
26 | ```
27 |
28 | ## CLI Usage
29 |
30 | ```shell
31 | $ bsc main.bs
32 | $ ./main
33 | > "Hello World"
34 | ```
35 |
36 | ## Summary
37 |
38 | BorrowScript aims to be a language that offers a TypeScript inspired syntax with concepts borrowed from Rust and Go.
39 |
40 | Basically; "what are the minimum changes required to TypeScript for it to support a borrow checker"
41 |
42 | It's hoped that this will make using/learning a borrow checker more accessible while also offering a higher level language well suited to writing user-space applications - like desktop applications, web servers and web applications (through web assembly).
43 |
44 | BorrowScript does not expect to match the performance of Rust but it aims to be competitive with languages like Go - offering more consistent performance, smaller binaries and a sensible language to target client programs where multi-threading is often under-utilized, dangerous or inaccessible.
45 |
46 | # Language Design
47 |
48 | Please contribute your thoughts to the language design!
49 |
50 | ## Variable Declaration
51 |
52 | To declare a variable you can use the keywords `const` and `let`, which describe immutable and mutable bindings.
53 |
54 | ```typescript
55 | let foo = 'foo' // mutable
56 | const bar = 'bar' // immutable
57 | ```
58 |
59 | ## Types
60 |
61 | BorrowScript contains opinionated built in types:
62 |
63 | ```typescript
64 | const myString: string = "Hello World"
65 | const myString = "Hello World" // type inference
66 | ```
67 |
68 | Where Rust would have multiple types for different use cases:
69 |
70 | ```rust
71 | let myString: String = String::from("Hello World");
72 | let myString2: &str = "Hello World"
73 | ```
74 |
75 | All types are references to objects and can be mutated or reassigned if permitted. The types are as follows:
76 |
77 | ```typescript
78 | const s: string = ""
79 | const n: number = 0
80 | const b: boolean = true
81 | const z: null = null
82 | const a: Array = []
83 | const m: Map = new Map()
84 | const s: Set = new Set()
85 | ```
86 |
87 | ## Enums
88 |
89 | BorrowScript features Rust-inspired enum types and match statements
90 |
91 | ```typescript
92 | enum Foobar {
93 | Foo,
94 | Bar
95 | }
96 | ```
97 |
98 | ### Mutability
99 |
100 | Mutability is defined in the binding and affects the _entire_ value (deeply, unlike TypeScript).
101 |
102 | It's essentially a guarantee that any value assigned to the container will abide by the mutability rules defined on the binding.
103 |
104 | Reassignment to another binding will allow values to be changed from mutable/immutable as the binding defines the mutability rules.
105 |
106 | ```typescript
107 | const foo: string = 'Hello' // immutable string assignment
108 | let bar = foo // move the value from immutable "foo" into mutable "bar"
109 | // "foo" become inaccessible after it has been moved, "bar" can be used
110 | bar.push(' World')
111 | ```
112 |
113 | ## Function Declarations
114 |
115 | Functions can be defined in full or using shorthand lambda expressions
116 |
117 | ```typescript
118 | function foo() {} // immutable declaration
119 |
120 | // Shorthand
121 | const foo = () => {}
122 | let bar = () => {}
123 | ```
124 |
125 | ## Ownership
126 |
127 | The BorrowScript compiler will handle memory allocations and de-allocations at compile time, producing a binary that does not require a runtime garbage collector.
128 |
129 | This is done through the automatic de-allocation of values when they fall out of their owned scope.
130 |
131 | ```typescript
132 | function main() {
133 | const foo = 'Hello World' // "foo" is allocated and owned by "main"
134 |
135 | // <-- at the end of main's block, "foo" is de-allocated
136 | // avoiding the need for a garbage collector
137 | }
138 | ```
139 |
140 | ### Borrowing
141 |
142 | Ownership can be temporarily loaned out to another scope with `read` or `write` permissions.
143 |
144 | ```typescript
145 | function readFoo(read foo: string) {}
146 |
147 | function main() {
148 | let foo = "Hello World" // "foo" is owned by main
149 | readFoo(read foo) // "foo" is lent to "readFoo" with "read" permission
150 | // <---------------------- "foo" is still owned by "main" and is de-allocated when "main" completes
151 | }
152 | ```
153 |
154 | There can only be one owner of the value and either one scope with `write` access or unlimited scopes with `read` access.
155 |
156 | ```typescript
157 | function main() {
158 | let foo = "Hello World" // "foo" is owned by main
159 | readFoo(read foo) // "foo" loaned to "readFoo" with 1 of infinite read borrows
160 | // <---------------------- "readFoo" completes decrementing the read borrow to 0 of infinite read borrows
161 | writeFoo(write foo) // "foo" loaned to "writeFoo" with 1 of 1 write borrows
162 | // <---------------------- "writeFoo" completes decrementing the write borrow to 0 of 1 write borrows
163 | // <---------------------- "foo" is owned by "main" and is de-allocated when "main" completes
164 | }
165 | ```
166 |
167 | An owner can `move` a variable to another scope and doing so will make that value inaccessible in its original scope.
168 |
169 | ### Ownership Operators `read` `write` `move` `copy`
170 |
171 |
172 | ```typescript
173 | function moveFoo(let foo: string) { // "foo" is moved into "moveFoo" which consumes it as mutable
174 | // if "let" is omitted, the moved value assumes "const"
175 | readFoo(read foo)
176 | writeFoo(write foo)
177 | // <---------------------- "foo" is owned by "moveFoo" and is de-allocated when "moveFoo" completes
178 | }
179 |
180 | function main() {
181 | let foo = "Hello World" // "foo" is owned by main
182 | readFoo(read foo) // "foo" loaned to "readFoo" with 1 of infinite read borrows
183 | // <---------------------- "readFoo" completes decrementing the read borrow to 0 of infinite read borrows
184 | moveFoo(foo) // "foo" moved into "moveFoo"
185 | // <---------------------- "foo" is no longer available in "main"
186 | // console.log(foo) // Attempts to access "foo" in this scope will fail after it has been moved
187 | }
188 | ```
189 |
190 | A scope with `write` has `read`/`write`.
191 | A scope with `read` has `read` only.
192 | A scope can only lend out to another scope a permission equal or lower than the current held permission.
193 |
194 | A value can be copied, creating a new owned value
195 |
196 | ```typescript
197 | const foo = "foo"
198 | let bar = copy foo // same as foo.copy()
199 | bar.push('bar')
200 | ```
201 |
202 | ```typescript
203 | function main() {
204 | let foo = "Hello World" // "foo" is owned by main
205 | moveFoo(copy foo) // a new copy of "foo" is moved into "moveFoo"
206 | console.log(foo) // "foo" can still be accessed from this scope
207 | }
208 | ```
209 |
210 | ## Rust Examples of Ownership Operators
211 |
212 |
280 |
281 | ### Closures / Callbacks
282 |
283 | Callbacks in BorrowScript don't automatically have access to variables in their outer scope. In order for a callback to gain access to a variable from an outer scope, it must be explicitly imported from its parent scope.
284 |
285 | This is done using "gate" parameters within square brackets.
286 |
287 | ```typescript
288 | const message = 'Hello World'
289 |
290 | setTimeout([message]() => {
291 | console.log(message)
292 | }, 0)
293 | ```
294 |
295 | This is required because the compiler must move the value from the parent scope and into the nested callback scope
296 |
297 | Once moved, the original value is no longer accessible to the outer scope
298 |
299 | ```typescript
300 | const message = 'Hello World'
301 |
302 | setTimeout([message]() => {
303 | console.log(message)
304 | }, 0)
305 |
306 | // console.log(message) <- "message" has been moved and can no longer be accessed here
307 | ```
308 |
309 | This enables the principle of "fearless concurrency" - making race conditions in multi-threaded contexts impossible
310 |
311 | ```typescript
312 | import thread from "std:thread"
313 |
314 | function main() {
315 | let message = 'Hello World'
316 |
317 | thread.spawn([copy message]() => { // Create a new OS thread and copy "message" into that scope
318 | console.log(message)
319 | })
320 |
321 | console.log(message) // "message" is still available in "main"
322 |
323 | thread.spawn([message]() => { // Create a new OS thread and move "message" into that scope
324 | console.log(message)
325 | })
326 | }
327 | ```
328 |
329 | ## Multiple Owners & Multiple Mutable References
330 |
331 | Unless I can find a way to infer and automatically apply smart pointers and mutexes to shared references, they will need to be explicitly defined.
332 |
333 | ```typescript
334 | import { Error } from 'std:error'
335 | import thread, { Handle } from 'std:thread'
336 | import { Mutex, Arc } from 'std:sync'
337 |
338 | function main(): Result {
339 | const count = Arc.new(Mutex.new(0))
340 | let handles: Array = []
341 |
342 | for (const i in 0..10) {
343 | handles.push(thread.spawn([copy message]() => { // Spawn a thread and copy a reference to the mutex + value
344 | let count = count.lock() // Unlock the mutex and assign it to a mutable container
345 | count++ // Increment the count ("count" is a "&mut i32")
346 | }))
347 | }
348 |
349 | for (const handle in handles) handle.join()? // Wait for threads to complete, propagate the error if a thread failes
350 |
351 | console.log(message.lock()) // Unlock the mutex and print the inner value
352 | // Prints "10"
353 | }
354 | ```
355 |
356 | ## Class Declaration
357 |
358 | TODO
359 |
360 | BorrowScript will have FFI capabilities similar to Rust and will need to have structs to facilitate that.
361 |
362 | I personally like classes as a means to visually group methods with an object but if there are classes they will not be extendable.
363 |
364 | My preference is composition over inheritance and appreciate Go's ability to embed structs within other structs.
365 |
366 | At this stage, my feeling is that classes will not be part of the language unless I can find a way to make them fit naturally within the language
367 |
368 | ## Structural Types and Type Kung-Fu
369 |
370 | TODO: depends on struct syntax
371 |
372 | I love the ability in TypeScript to accept values by their shape and want this to be a part of BorrowScript
373 |
374 | ```typescript
375 | interface IFoo {
376 | foo(read self): void
377 | }
378 |
379 | function acceptFoo(read foo: IFoo) {} // Function that accepts a type that looks like the IFoo interface
380 |
381 | struct Foo {}
382 |
383 | impl Foo {
384 | foo(read self): void {}
385 | }
386 |
387 | struct AlsoFoo {}
388 |
389 | impl AlsoFoo {
390 | foo(read self): void {}
391 | }
392 |
393 | function main() {
394 | const foo1 = Foo{}
395 | const foo1 = AlsoFoo{}
396 |
397 | acceptFoo(read foo1)
398 | acceptFoo(read foo2)
399 | }
400 | ```
401 |
402 | In Rust, this is expressed using the `dyn` keyword - however there are cases where the unknown size of a value request a `Box`.
403 |
404 | There are also considerations on if/how interfaces need to be described as satisfied. Rust uses its `trait` system to describe this but this couples the struct to the type it satisfies.
405 |
406 | Go can accept structs as interfaces, implicitly inferring that the struct satisfies the type from its shape - however this is only one level deep (meaning interfaces that have methods that return interfaces don't work).
407 |
408 | ### Type Kung Fu (Algebraic Types)
409 |
410 | TODO
411 |
412 | I have seen this topic to be misunderstood and polarizing but ultimately it's simply the ability to unify and allow the compiler to discriminate types structurally
413 |
414 | ```typescript
415 | function main() {
416 | const stringOrNumber: string | number = 0
417 |
418 | match (typeof stringOrNumber) {
419 | number(value => console.log(value))
420 | string(value => console.log(value))
421 | }
422 | }
423 | ```
424 |
425 | Personally I quite like this as it can be used to compose types together
426 |
427 | ```typescript
428 | type Foo = { foo: string }
429 | type Bar = { bar: number }
430 | type Foobar = Foo & Bar
431 | ```
432 |
433 | And creating new types using TypeScript's `keyof` `in` `extends` keywords can be quite ergonomic.
434 |
435 | It is quite difficult to implement though and will need more thinking. I'd like to do as much in the compiler as possible and this may require that union variables live inside a container with that type metadata included.
436 |
437 |
438 | ## Generic Lifetimes
439 |
440 | TODO
441 |
442 | Notes:
443 |
444 | You can see a fantastic video summary by Bogdan Pshonyak here: [Let's get Rusty - The Rust Survival Guide](https://youtu.be/usJDUSrcwqI?si=rxhD7gEio_8o_qDn&t=602)
445 |
446 | I haven't thought of a way to apply this to BorrowScript without reaching for Rust's `'` generic type parameter.
447 |
448 |
449 | ## Async, Concurrency, Threads
450 |
451 | For BorrowScript to work in many different environments (wasm, napi, iot), it needs to give the developer control of how concurrency works.
452 |
453 | There will be the capability to create system threads using OS APIs
454 |
455 | ```typescript
456 | import thread from 'std:thread'
457 |
458 | function main() {
459 | thread
460 | .spawn(() => {
461 | console.log('hello world')
462 | })
463 | .join()
464 | .panicOnError()
465 | }
466 | ```
467 |
468 | Async will be supported using an async runtime selected by the developer, where the runtimes will be provided by the standard library
469 |
470 | ```typescript
471 | import asynch from 'std:asynch'
472 |
473 | function main() {
474 | let rt = asynch
475 | .runtime({ workers: 4 })
476 | .blockOn(async () => {
477 | console.log('hello world')
478 | })
479 | .panicOnError()
480 | }
481 | ```
482 |
483 | And simplified with the use of compiler macros
484 |
485 | ```typescript
486 | import asynch from 'std:asynch'
487 |
488 | @asynch.main!()
489 | async function main() {
490 | console.log('Hello World')
491 | }
492 | ```
493 |
494 | Where wasm, napi, etc can select the runtime which makes sense for them
495 |
496 | ```typescript
497 | import asynchWasm from 'std:asynch/wasm'
498 | import asynch from 'std:asynch'
499 |
500 | @asynch.main!(asynchWasm.default)
501 | async function main() {
502 | console.log('Hello World')
503 | }
504 | ```
505 |
506 | ## Error handling
507 |
508 | Will use Rust-style `Result` enum and pattern matching and the `?` operator to propagate errors upwards
509 |
510 | ## Consuming External Libraries (FFI)
511 |
512 | TODO
513 |
514 | tl;dr yes
515 |
--------------------------------------------------------------------------------
/assets/borrow-check.svg:
--------------------------------------------------------------------------------
1 |
32 |
--------------------------------------------------------------------------------
/assets/borrow-script.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/assets/fast.svg:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/assets/index.html:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/assets/multi-threaded.svg:
--------------------------------------------------------------------------------
1 |
28 |
--------------------------------------------------------------------------------
/assets/race-conditions.svg:
--------------------------------------------------------------------------------
1 |
42 |
--------------------------------------------------------------------------------
/assets/static-binary.svg:
--------------------------------------------------------------------------------
1 |
32 |
--------------------------------------------------------------------------------
/spec/std-console.md:
--------------------------------------------------------------------------------
1 | # Standard Library Console
2 |
3 | ```typescript
4 | import console from '@std/console'
5 | ```
6 |
7 | ## `log`
8 |
9 | `console.log()` will consume any object that satisfies its `IString` interface:
10 |
11 | ```typescript
12 | interface IString {
13 | toString(): string
14 | }
15 |
16 | interface Console {
17 | log(read ...args: IString[]): void
18 | }
19 | ```
20 |
21 | Where the usage would be:
22 | ```typescript
23 | const foo = 1337
24 |
25 | console.log(read foo.toString()) // 'Hello World'
26 | console.log(read foo) // 'Hello World'
27 |
28 | // read can be omitted
29 | console.log(foo.toString()) // 'Hello World'
30 | console.log(foo) // 'Hello World'
31 | ```
32 |
--------------------------------------------------------------------------------
/spec/type-number.md:
--------------------------------------------------------------------------------
1 | # `number`
2 |
3 | The `number` type represents a built-in `Number` object.
4 |
5 | _It may be required to split this into `float` and `int`_
6 |
--------------------------------------------------------------------------------
/spec/type-string.md:
--------------------------------------------------------------------------------
1 | # `string`
2 |
3 | The `string` type represents a built-in `String` object. This is a dynamic `char` implementation.
4 |
5 | ### Details
6 |
7 | ```typescript
8 | let s0: string = 'Hello World'
9 | let s1 = 'Hello World'
10 | let s2 = new String('Hello World')
11 | ```
12 |
13 | ### Interface
14 |
15 | ```typescript
16 | abstract class String {
17 | new(chars: any): this // There will be no char representation, however this could be a byte array
18 | toString(): String
19 | push(value: String)
20 | }
21 | ```
22 |
--------------------------------------------------------------------------------