SkillAgentSearch skills...

BorrowScript

TypeScript with a Borrow Checker. Multi-threaded, Tiny binaries. No GC. Easy to write.

Install / Use

/learn @alshdavid/BorrowScript
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<br> <img align="left" height="50px" src="./assets/borrow-script.svg"> <br> <p> &nbsp</p> <img align="left" height="30px" src="./assets/borrow-check.svg"> <p>TypeScript Syntax, Rust Borrow Checker, Go Philosophies</p> <img align="left" height="30px" src="./assets/race-conditions.svg"> <p>High Performance, Fearless Concurrency, Compile Time Checks</p> <img align="left" height="30px" src="./assets/fast.svg"> <p>No Garbage Collection, Memory Safety Guarantee</p> <br>

Hello World

import console from '@std/console'

function main() {
  const text = 'Hello World'
  console.log(read text)
}

CLI Usage

$ bsc main.bs
$ ./main
> "Hello World"

Summary

BorrowScript aims to be a language that offers a TypeScript inspired syntax with concepts borrowed from Rust and Go.

Basically; "what are the minimum changes required to TypeScript for it to support a borrow checker"

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).

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.

Language Design

<i>Please contribute your thoughts to the language design!</i>

Variable Declaration

To declare a variable you can use the keywords const and let, which describe immutable and mutable bindings.

let foo = 'foo'   // mutable
const bar = 'bar' // immutable

Types

BorrowScript contains opinionated built in types:

const myString: string = "Hello World"
const myString = "Hello World" // type inference

Where Rust would have multiple types for different use cases:

let myString: String = String::from("Hello World");
let myString2: &str = "Hello World"

All types are references to objects and can be mutated or reassigned if permitted. The types are as follows:

const s: string = ""
const n: number = 0
const b: boolean = true
const z: null = null
const a: Array<string> = [] 
const m: Map<string, string> = new Map()
const s: Set<string> = new Set()

Enums

BorrowScript features Rust-inspired enum types and match statements

enum Foobar {
  Foo,
  Bar
}

Mutability

Mutability is defined in the binding and affects the entire value (deeply, unlike TypeScript).

It's essentially a guarantee that any value assigned to the container will abide by the mutability rules defined on the binding.

Reassignment to another binding will allow values to be changed from mutable/immutable as the binding defines the mutability rules.

const foo: string = 'Hello' // immutable string assignment
let bar = foo               // move the value from immutable "foo" into mutable "bar"
                            // "foo" become inaccessible after it has been moved, "bar" can be used
bar.push(' World')

Function Declarations

Functions can be defined in full or using shorthand lambda expressions

function foo() {} // immutable declaration

// Shorthand
const foo = () => {}
let bar = () => {}

Ownership

The BorrowScript compiler will handle memory allocations and de-allocations at compile time, producing a binary that does not require a runtime garbage collector.

This is done through the automatic de-allocation of values when they fall out of their owned scope.

function main() {
  const foo = 'Hello World' // "foo" is allocated and owned by "main"

  // <-- at the end of main's block, "foo" is de-allocated
  //     avoiding the need for a garbage collector
}

Borrowing

Ownership can be temporarily loaned out to another scope with read or write permissions.

function readFoo(read foo: string) {}

function main() {
  let foo = "Hello World" // "foo" is owned by main
  readFoo(read foo)       // "foo" is lent to "readFoo" with "read" permission
  // <---------------------- "foo" is still owned by "main" and is de-allocated when "main" completes
}

There can only be one owner of the value and either one scope with write access or unlimited scopes with read access.

function main() {
  let foo = "Hello World" // "foo" is owned by main
  readFoo(read foo)       // "foo" loaned to "readFoo" with 1 of infinite read borrows
  // <---------------------- "readFoo" completes decrementing the read borrow to 0 of infinite read borrows
  writeFoo(write foo)     // "foo" loaned to "writeFoo" with 1 of 1 write borrows
  // <---------------------- "writeFoo" completes decrementing the write borrow to 0 of 1 write borrows
  // <---------------------- "foo" is owned by "main" and is de-allocated when "main" completes
}

An owner can move a variable to another scope and doing so will make that value inaccessible in its original scope.

Ownership Operators read write move copy

function moveFoo(let foo: string) { // "foo" is moved into "moveFoo" which consumes it as mutable
                                    // if "let" is omitted, the moved value assumes "const"
  readFoo(read foo)
  writeFoo(write foo)
  // <---------------------- "foo" is owned by "moveFoo" and is de-allocated when "moveFoo" completes
}

function main() {
  let foo = "Hello World" // "foo" is owned by main
  readFoo(read foo)       // "foo" loaned to "readFoo" with 1 of infinite read borrows
  // <---------------------- "readFoo" completes decrementing the read borrow to 0 of infinite read borrows
  moveFoo(foo)            // "foo" moved into "moveFoo"
  // <---------------------- "foo" is no longer available in "main"
  // console.log(foo)     // Attempts to access "foo" in this scope will fail after it has been moved
}

A scope with write has read/write. <br> A scope with read has read only. <br> A scope can only lend out to another scope a permission equal or lower than the current held permission.

A value can be copied, creating a new owned value

const foo = "foo"
let bar = copy foo        // same as foo.copy()
bar.push('bar')
function main() {
  let foo = "Hello World" // "foo" is owned by main
  moveFoo(copy foo)       // a new copy of "foo" is moved into "moveFoo"
  console.log(foo)        // "foo" can still be accessed from this scope
}

Rust Examples of Ownership Operators

<table> <tr><th>Operator</th><th>BorrowScript</th><th>Rust</th></tr> <tr><td>Read</td><td>
function readFoo(read foo: string) {
  console.log(foo)
}
</td><td>
fn read_foo(foo: &String) {
  print!("{}", foo);
}
</td></tr> <tr><td>Write</td><td>
function writeFoo(write foo: string) {
  foo.push("bar")
}
</td><td>
fn write_foo(foo: &mut String) {
  foo.push_str("bar")
}
</td></tr> <tr><td>Move (mutable)</td><td>
function moveMutFoo(let foo: string) {
  foo.push("bar")
}
</td><td>
fn move_mut_foo(mut foo: String) {
  foo.push_str("bar")
}
</td></tr> <tr><td>Move (immutable)</td><td>
function moveFoo(foo: string) {
  console.log(foo)
}
</td><td>
fn move_foo(foo: String) {
  print!("{}", foo);
}
</td></tr> </table>

Closures / Callbacks

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.

This is done using "gate" parameters within square brackets.

const message = 'Hello World'

setTimeout([message]() => {
  console.log(message)
}, 0)

This is required because the compiler must move the value from the parent scope and into the nested callback scope

Once moved, the original value is no longer accessible to the outer scope

const message = 'Hello World'

setTimeout([message]() => {
  console.log(message)
}, 0)

// console.log(message)               <- "message" has been moved and can no longer be accessed here

This enables the principle of "fearless concurrency" - making race conditions in multi-threaded contexts impossible

import thread from "std:thread"

function main() {
  let message = 'Hello World'

  thread.spawn([copy message]() => { // Create a new OS thread and copy "message" into that scope
    console.log(message)
  })

  console.log(message)               // "message" is still available in "main"

  thread.spawn([message]() => {      // Create a new OS thread and move "message" into that scope 
    console.log(message)
  })
}

Multiple Owners & Multiple Mutable References

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.

import { Error } from 'std:error'
import thread, { Handle } from 'std:thread'
import { Mutex, Arc } from 'std:sync'

function main(): Result<void, Error> {
  const count = Arc.new(Mutex.new(0))
  let handles: Array<Handle> = []

  for (const i in 0..10) {  
    handles.push(thread.spawn([copy message]() => { // Spawn a thread and copy a reference to the mutex + value 
      let count = count.lock()                      // Unlock the mutex and assign it to a mutable container
      count++                                       // Increment the count ("count" is a "&mut i32")
    }))
  }
  
  for (const handle in handles) handle.join()?      // Wait for threads to complete, propagate the error if a thread failes
                
  console.log(message.lock())                       // Unlock the mutex and print the inner value 
                                        
View on GitHub
GitHub Stars1.5k
CategoryDevelopment
Updated1mo ago
Forks16

Languages

HTML

Security Score

80/100

Audited on Feb 20, 2026

No findings