{"id":13527790,"url":"https://github.com/alshdavid/BorrowScript","last_synced_at":"2025-04-01T10:30:31.013Z","repository":{"id":42474332,"uuid":"401572203","full_name":"alshdavid/BorrowScript","owner":"alshdavid","description":"TypeScript with a Borrow Checker. Multi-threaded, Tiny binaries. No GC. Easy to write.","archived":false,"fork":false,"pushed_at":"2024-09-18T00:27:51.000Z","size":233,"stargazers_count":1443,"open_issues_count":0,"forks_count":16,"subscribers_count":32,"default_branch":"main","last_synced_at":"2024-09-18T04:21:52.778Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alshdavid.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2021-08-31T04:28:39.000Z","updated_at":"2024-09-18T00:27:54.000Z","dependencies_parsed_at":"2023-11-11T23:24:29.063Z","dependency_job_id":"45baad00-f226-42f9-a0e5-7ee86adc6ca5","html_url":"https://github.com/alshdavid/BorrowScript","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alshdavid%2FBorrowScript","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alshdavid%2FBorrowScript/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alshdavid%2FBorrowScript/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alshdavid%2FBorrowScript/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alshdavid","download_url":"https://codeload.github.com/alshdavid/BorrowScript/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":222715679,"owners_count":17027699,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T06:02:01.842Z","updated_at":"2024-11-02T12:32:26.978Z","avatar_url":"https://github.com/alshdavid.png","language":"HTML","funding_links":[],"categories":["Embed-Script/VM/","HTML","TypeScript to JavaScript"],"sub_categories":["Lua"],"readme":"\u003cbr\u003e\n\u003cimg align=\"left\" height=\"50px\" src=\"./assets/borrow-script.svg\"\u003e\n\u003cbr\u003e\n\u003cp\u003e \u0026nbsp\u003c/p\u003e\n\n\u003cimg align=\"left\" height=\"30px\" src=\"./assets/borrow-check.svg\"\u003e\n\u003cp\u003eTypeScript Syntax, Rust Borrow Checker, Go Philosophies\u003c/p\u003e\n\n\u003cimg align=\"left\" height=\"30px\" src=\"./assets/race-conditions.svg\"\u003e\n\u003cp\u003eHigh Performance, Fearless Concurrency, Compile Time Checks\u003c/p\u003e\n\n\u003cimg align=\"left\" height=\"30px\" src=\"./assets/fast.svg\"\u003e\n\u003cp\u003eNo Garbage Collection, Memory Safety Guarantee\u003c/p\u003e\n\n\u003cbr\u003e\n\n## Hello World\n\n```typescript\nimport console from '@std/console'\n\nfunction main() {\n  const text = 'Hello World'\n  console.log(read text)\n}\n```\n\n## CLI Usage\n\n```shell\n$ bsc main.bs\n$ ./main\n\u003e \"Hello World\"\n```\n\n## Summary\n\nBorrowScript aims to be a language that offers a TypeScript inspired syntax with concepts borrowed from Rust and Go. \n\nBasically; \"what are the minimum changes required to TypeScript for it to support a borrow checker\"\n\nIt'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).\n\nBorrowScript 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.\n\n# Language Design\n\n\u003ci\u003ePlease contribute your thoughts to the language design!\u003c/i\u003e\n\n## Variable Declaration\n\nTo declare a variable you can use the keywords `const` and `let`, which describe immutable and mutable bindings.\n\n```typescript\nlet foo = 'foo'   // mutable\nconst bar = 'bar' // immutable\n```\n\n## Types\n\nBorrowScript contains opinionated built in types:\n\n```typescript\nconst myString: string = \"Hello World\"\nconst myString = \"Hello World\" // type inference\n```\n\nWhere Rust would have multiple types for different use cases:\n\n```rust\nlet myString: String = String::from(\"Hello World\");\nlet myString2: \u0026str = \"Hello World\"\n```\n\nAll types are references to objects and can be mutated or reassigned if permitted. The types are as follows:\n\n```typescript\nconst s: string = \"\"\nconst n: number = 0\nconst b: boolean = true\nconst z: null = null\nconst a: Array\u003cstring\u003e = [] \nconst m: Map\u003cstring, string\u003e = new Map()\nconst s: Set\u003cstring\u003e = new Set()\n```\n\n## Enums\n\nBorrowScript features Rust-inspired enum types and match statements\n\n```typescript\nenum Foobar {\n  Foo,\n  Bar\n}\n```\n\n### Mutability\n\nMutability is defined in the binding and affects the _entire_ value (deeply, unlike TypeScript). \n\nIt's essentially a guarantee that any value assigned to the container will abide by the mutability rules defined on the binding.\n\nReassignment to another binding will allow values to be changed from mutable/immutable as the binding defines the mutability rules.\n\n```typescript\nconst foo: string = 'Hello' // immutable string assignment\nlet bar = foo               // move the value from immutable \"foo\" into mutable \"bar\"\n                            // \"foo\" become inaccessible after it has been moved, \"bar\" can be used\nbar.push(' World')\n```\n\n## Function Declarations\n\nFunctions can be defined in full or using shorthand lambda expressions\n\n```typescript\nfunction foo() {} // immutable declaration\n\n// Shorthand\nconst foo = () =\u003e {}\nlet bar = () =\u003e {}\n```\n\n## Ownership\n\nThe BorrowScript compiler will handle memory allocations and de-allocations at compile time, producing a binary that does not require a runtime garbage collector. \n\nThis is done through the automatic de-allocation of values when they fall out of their owned scope.\n\n```typescript\nfunction main() {\n  const foo = 'Hello World' // \"foo\" is allocated and owned by \"main\"\n\n  // \u003c-- at the end of main's block, \"foo\" is de-allocated\n  //     avoiding the need for a garbage collector\n}\n```\n\n### Borrowing \n\nOwnership can be temporarily loaned out to another scope with `read` or `write` permissions.\n\n```typescript\nfunction readFoo(read foo: string) {}\n\nfunction main() {\n  let foo = \"Hello World\" // \"foo\" is owned by main\n  readFoo(read foo)       // \"foo\" is lent to \"readFoo\" with \"read\" permission\n  // \u003c---------------------- \"foo\" is still owned by \"main\" and is de-allocated when \"main\" completes\n}\n```\n\nThere can only be one owner of the value and either one scope with `write` access or unlimited scopes with `read` access.\n\n```typescript\nfunction main() {\n  let foo = \"Hello World\" // \"foo\" is owned by main\n  readFoo(read foo)       // \"foo\" loaned to \"readFoo\" with 1 of infinite read borrows\n  // \u003c---------------------- \"readFoo\" completes decrementing the read borrow to 0 of infinite read borrows\n  writeFoo(write foo)     // \"foo\" loaned to \"writeFoo\" with 1 of 1 write borrows\n  // \u003c---------------------- \"writeFoo\" completes decrementing the write borrow to 0 of 1 write borrows\n  // \u003c---------------------- \"foo\" is owned by \"main\" and is de-allocated when \"main\" completes\n}\n```\n\nAn owner can `move` a variable to another scope and doing so will make that value inaccessible in its original scope.\n\n### Ownership Operators `read` `write` `move` `copy`\n\n\n```typescript\nfunction moveFoo(let foo: string) { // \"foo\" is moved into \"moveFoo\" which consumes it as mutable\n                                    // if \"let\" is omitted, the moved value assumes \"const\"\n  readFoo(read foo)\n  writeFoo(write foo)\n  // \u003c---------------------- \"foo\" is owned by \"moveFoo\" and is de-allocated when \"moveFoo\" completes\n}\n\nfunction main() {\n  let foo = \"Hello World\" // \"foo\" is owned by main\n  readFoo(read foo)       // \"foo\" loaned to \"readFoo\" with 1 of infinite read borrows\n  // \u003c---------------------- \"readFoo\" completes decrementing the read borrow to 0 of infinite read borrows\n  moveFoo(foo)            // \"foo\" moved into \"moveFoo\"\n  // \u003c---------------------- \"foo\" is no longer available in \"main\"\n  // console.log(foo)     // Attempts to access \"foo\" in this scope will fail after it has been moved\n}\n```\n\nA scope with `write` has `read`/`write`. \u003cbr\u003e\nA scope with `read` has `read` only. \u003cbr\u003e\nA scope can only lend out to another scope a permission equal or lower than the current held permission.\n\nA value can be copied, creating a new owned value\n\n```typescript\nconst foo = \"foo\"\nlet bar = copy foo        // same as foo.copy()\nbar.push('bar')\n```\n\n```typescript\nfunction main() {\n  let foo = \"Hello World\" // \"foo\" is owned by main\n  moveFoo(copy foo)       // a new copy of \"foo\" is moved into \"moveFoo\"\n  console.log(foo)        // \"foo\" can still be accessed from this scope\n}\n```\n\n## Rust Examples of Ownership Operators\n\n\u003ctable\u003e\n\u003ctr\u003e\u003cth\u003eOperator\u003c/th\u003e\u003cth\u003eBorrowScript\u003c/th\u003e\u003cth\u003eRust\u003c/th\u003e\u003c/tr\u003e\n\n\u003ctr\u003e\u003ctd\u003eRead\u003c/td\u003e\u003ctd\u003e\n\n```typescript\nfunction readFoo(read foo: string) {\n  console.log(foo)\n}\n```\n\u003c/td\u003e\u003ctd\u003e\n\n```rust\nfn read_foo(foo: \u0026String) {\n  print!(\"{}\", foo);\n}\n```\n\n\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003eWrite\u003c/td\u003e\u003ctd\u003e\n\n```typescript\nfunction writeFoo(write foo: string) {\n  foo.push(\"bar\")\n}\n```\n\u003c/td\u003e\u003ctd\u003e\n\n```rust\nfn write_foo(foo: \u0026mut String) {\n  foo.push_str(\"bar\")\n}\n```\n\n\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003eMove (mutable)\u003c/td\u003e\u003ctd\u003e\n\n```typescript\nfunction moveMutFoo(let foo: string) {\n  foo.push(\"bar\")\n}\n```\n\u003c/td\u003e\u003ctd\u003e\n\n```rust\nfn move_mut_foo(mut foo: String) {\n  foo.push_str(\"bar\")\n}\n```\n\n\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003eMove (immutable)\u003c/td\u003e\u003ctd\u003e\n\n```typescript\nfunction moveFoo(foo: string) {\n  console.log(foo)\n}\n```\n\u003c/td\u003e\u003ctd\u003e\n\n```rust\nfn move_foo(foo: String) {\n  print!(\"{}\", foo);\n}\n```\n\n\u003c/td\u003e\u003c/tr\u003e\n\u003c/table\u003e\n\n### Closures / Callbacks\n\nCallbacks 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.\n\nThis is done using \"gate\" parameters within square brackets.\n\n```typescript\nconst message = 'Hello World'\n\nsetTimeout([message]() =\u003e {\n  console.log(message)\n}, 0)\n```\n\nThis is required because the compiler must move the value from the parent scope and into the nested callback scope\n\nOnce moved, the original value is no longer accessible to the outer scope\n\n```typescript\nconst message = 'Hello World'\n\nsetTimeout([message]() =\u003e {\n  console.log(message)\n}, 0)\n\n// console.log(message)               \u003c- \"message\" has been moved and can no longer be accessed here\n```\n\nThis enables the principle of \"fearless concurrency\" - making race conditions in multi-threaded contexts impossible\n\n```typescript\nimport thread from \"std:thread\"\n\nfunction main() {\n  let message = 'Hello World'\n\n  thread.spawn([copy message]() =\u003e { // Create a new OS thread and copy \"message\" into that scope\n    console.log(message)\n  })\n\n  console.log(message)               // \"message\" is still available in \"main\"\n\n  thread.spawn([message]() =\u003e {      // Create a new OS thread and move \"message\" into that scope \n    console.log(message)\n  })\n}\n```\n\n## Multiple Owners \u0026 Multiple Mutable References\n\nUnless I can find a way to infer and automatically apply smart pointers and mutexes to shared references, they will need to be explicitly defined.\n\n```typescript\nimport { Error } from 'std:error'\nimport thread, { Handle } from 'std:thread'\nimport { Mutex, Arc } from 'std:sync'\n\nfunction main(): Result\u003cvoid, Error\u003e {\n  const count = Arc.new(Mutex.new(0))\n  let handles: Array\u003cHandle\u003e = []\n\n  for (const i in 0..10) {  \n    handles.push(thread.spawn([copy message]() =\u003e { // Spawn a thread and copy a reference to the mutex + value \n      let count = count.lock()                      // Unlock the mutex and assign it to a mutable container\n      count++                                       // Increment the count (\"count\" is a \"\u0026mut i32\")\n    }))\n  }\n  \n  for (const handle in handles) handle.join()?      // Wait for threads to complete, propagate the error if a thread failes\n                \n  console.log(message.lock())                       // Unlock the mutex and print the inner value \n                                                    // Prints \"10\"\n}\n```\n\n## Class Declaration\n\nTODO\n\nBorrowScript will have FFI capabilities similar to Rust and will need to have structs to facilitate that.\n\nI personally like classes as a means to visually group methods with an object but if there are classes they will not be extendable.\n\nMy preference is composition over inheritance and appreciate Go's ability to embed structs within other structs.\n\nAt 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\n\n## Structural Types and Type Kung-Fu\n\nTODO: depends on struct syntax\n\nI love the ability in TypeScript to accept values by their shape and want this to be a part of BorrowScript\n\n```typescript\ninterface IFoo {\n  foo(read self): void\n}\n\nfunction acceptFoo(read foo: IFoo) {}        // Function that accepts a type that looks like the IFoo interface\n\nstruct Foo {}\n\nimpl Foo {\n  foo(read self): void {}\n}\n\nstruct AlsoFoo {}\n\nimpl AlsoFoo {\n  foo(read self): void {}\n}\n\nfunction main() {\n  const foo1 = Foo{}\n  const foo1 = AlsoFoo{}\n\n  acceptFoo(read foo1)\n  acceptFoo(read foo2)\n}\n```\n\nIn Rust, this is expressed using the `dyn` keyword - however there are cases where the unknown size of a value request a `Box\u003cdyn T\u003e`. \n\nThere 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.\n\nGo 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).\n\n### Type Kung Fu (Algebraic Types)\n\nTODO\n\nI 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\n\n```typescript\nfunction main() {\n  const stringOrNumber: string | number = 0\n\n  match (typeof stringOrNumber) {\n    number(value =\u003e console.log(value))\n    string(value =\u003e console.log(value))\n  }\n}\n```\n\nPersonally I quite like this as it can be used to compose types together\n\n```typescript\ntype Foo = { foo: string }\ntype Bar = { bar: number }\ntype Foobar = Foo \u0026 Bar\n```\n\nAnd creating new types using TypeScript's `keyof` `in` `extends` keywords can be quite ergonomic.\n\nIt 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.\n\n\n## Generic Lifetimes\n\nTODO\n\nNotes:\n\nYou 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\u0026t=602)\n\nI haven't thought of a way to apply this to BorrowScript without reaching for Rust's `'` generic type parameter.\n\n\n## Async, Concurrency, Threads\n\nFor BorrowScript to work in many different environments (wasm, napi, iot), it needs to give the developer control of how concurrency works.\n\nThere will be the capability to create system threads using OS APIs\n\n```typescript\nimport thread from 'std:thread'\n\nfunction main() {\n  thread\n    .spawn(() =\u003e {\n      console.log('hello world')\n    })\n    .join()\n    .panicOnError()\n}\n```\n\nAsync will be supported using an async runtime selected by the developer, where the runtimes will be provided by the standard library\n\n```typescript\nimport asynch from 'std:asynch'\n\nfunction main() {\n  let rt = asynch\n    .runtime({ workers: 4 })\n    .blockOn(async () =\u003e {\n      console.log('hello world')\n    })\n    .panicOnError()\n}\n```\n\nAnd simplified with the use of compiler macros\n\n```typescript\nimport asynch from 'std:asynch'\n\n@asynch.main!()\nasync function main() {\n  console.log('Hello World')\n}\n```\n\nWhere wasm, napi, etc can select the runtime which makes sense for them\n\n```typescript\nimport asynchWasm from 'std:asynch/wasm'\nimport asynch from 'std:asynch'\n\n@asynch.main!(asynchWasm.default)\nasync function main() {\n  console.log('Hello World')\n}\n```\n\n## Error handling\n\nWill use Rust-style `Result` enum and pattern matching and the `?` operator to propagate errors upwards\n\n## Consuming External Libraries (FFI)\n\nTODO\n\ntl;dr yes\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falshdavid%2FBorrowScript","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falshdavid%2FBorrowScript","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falshdavid%2FBorrowScript/lists"}