├── README.md ├── assets ├── borrow-check.svg ├── borrow-script.svg ├── fast.svg ├── index.html ├── multi-threaded.svg ├── race-conditions.svg └── static-binary.svg └── spec ├── std-console.md ├── type-number.md └── type-string.md /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

 

5 | 6 | 7 |

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 | 213 | 214 | 215 | 231 | 247 | 263 | 279 |
OperatorBorrowScriptRust
Read 216 | 217 | ```typescript 218 | function readFoo(read foo: string) { 219 | console.log(foo) 220 | } 221 | ``` 222 | 223 | 224 | ```rust 225 | fn read_foo(foo: &String) { 226 | print!("{}", foo); 227 | } 228 | ``` 229 | 230 |
Write 232 | 233 | ```typescript 234 | function writeFoo(write foo: string) { 235 | foo.push("bar") 236 | } 237 | ``` 238 | 239 | 240 | ```rust 241 | fn write_foo(foo: &mut String) { 242 | foo.push_str("bar") 243 | } 244 | ``` 245 | 246 |
Move (mutable) 248 | 249 | ```typescript 250 | function moveMutFoo(let foo: string) { 251 | foo.push("bar") 252 | } 253 | ``` 254 | 255 | 256 | ```rust 257 | fn move_mut_foo(mut foo: String) { 258 | foo.push_str("bar") 259 | } 260 | ``` 261 | 262 |
Move (immutable) 264 | 265 | ```typescript 266 | function moveFoo(foo: string) { 267 | console.log(foo) 268 | } 269 | ``` 270 | 271 | 272 | ```rust 273 | fn move_foo(foo: String) { 274 | print!("{}", foo); 275 | } 276 | ``` 277 | 278 |
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 | 2 | 12 | Checkbox 13 | 20 | 31 | 32 | -------------------------------------------------------------------------------- /assets/borrow-script.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | BorrowScript[ Design Phase ] -------------------------------------------------------------------------------- /assets/fast.svg: -------------------------------------------------------------------------------- 1 | 2 | Rocket 3 | 13 | 20 | 21 | -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /assets/multi-threaded.svg: -------------------------------------------------------------------------------- 1 | 2 | Shuffle 3 | 13 | 20 | 27 | 28 | -------------------------------------------------------------------------------- /assets/race-conditions.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | Car 13 | 21 | 31 | 41 | 42 | -------------------------------------------------------------------------------- /assets/static-binary.svg: -------------------------------------------------------------------------------- 1 | 2 | Reader 3 | 13 | 24 | 31 | 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 | --------------------------------------------------------------------------------