Overview

Bishop is a modern compiled programming language with elegant syntax designed for readability and ease of use.

Type Safe
Strong static typing with type inference using :=
Error Handling
Elegant or keyword for handling errors inline
Concurrent
Built-in goroutines and channels for concurrency
Modern Syntax
Clean, readable code without boilerplate

Quick Start

Installation

Bishop is currently in alpha and available for Linux x64 only. Download the latest release from GitHub Releases.

Debian/Ubuntu

bash
# Download from https://github.com/chrishayen/bishop/releases
sudo dpkg -i bishop-linux-x64-v0.14.0.deb

Manual Installation

bash
# Download bishop-linux-x64-v0.14.0.tar.gz from
# https://github.com/chrishayen/bishop/releases
tar -xzf bishop-linux-x64-v0.14.0.tar.gz
cd bishop-linux-x64-v0.14.0

# Install binary, libraries, and includes
sudo mv bin/bishop /usr/local/bin/
sudo cp -r lib/* /usr/local/lib/
sudo cp -r include/* /usr/local/include/
sudo ldconfig

Usage

bash
# Run a Bishop program
bishop run examples/serve.b

# Run tests
bishop test tests/

Bishop source files use the .b extension.

Primitive Types

Type Description
intInteger
strString
boolBoolean (true/false)
f3232-bit float
f6464-bit float
u32Unsigned 32-bit integer
u64Unsigned 64-bit integer

Optional Types

Any type can be made optional by appending ?:

bishop
int? maybe_num = none;
int? value = 42;

// Check for none
if value is none { }
if value { }  // shorthand for is not none

Variables

Explicit Type Declaration

bishop
int x = 42;
str name = "Chris";
bool flag = true;
f64 pi = 3.14159;

Type Inference

Use := for type inference:

bishop
x := 100;        // inferred as int
name := "Hello"; // inferred as str
pi := 3.14;      // inferred as f64

Constants

Use const to declare immutable values:

bishop
const int MAX_SIZE = 100;
const str APP_NAME = "MyApp";

// With type inference
const MAX := 100;
const NAME := "Bishop";

Functions

Basic Function

bishop
fn add(int a, int b) -> int {
    return a + b;
}

// Void function (no return type)
fn greet() {
    print("Hello");
}

Function References

Functions can be passed as arguments:

bishop
fn apply_op(int x, int y, fn(int, int) -> int op) -> int {
    return op(x, y);
}

result := apply_op(3, 4, add);  // result = 7

Anonymous Functions

bishop
// Basic anonymous function
doubler := fn(int x) -> int { return x * 2; };
result := doubler(21);  // 42

// Closures capture variables
multiplier := 10;
scale := fn(int x) -> int { return x * multiplier; };
result := scale(4);  // 40

Lambdas with Goroutines

bishop
ch := Channel<int>();

go fn() {
    ch.send(42);
}();

val := ch.recv();  // 42

Structs

bishop
// Definition
Person :: struct {
    name str,
    age int
}

// Instantiation
p := Person { name: "Chris", age: 32 };

// Field access
print(p.name);
p.age = 33;

Pass by Reference

bishop
fn set_age(Person *p, int new_age) {
    p.age = new_age;  // auto-deref, always mutable
}

bob := Person { name: "Bob", age: 25 };
set_age(&bob, 26);
assert_eq(bob.age, 26);  // mutation visible

Methods

bishop
Person :: get_name(self) -> str {
    return self.name;
}

Person :: set_age(self, int new_age) {
    self.age = new_age;
}

p := Person { name: "Chris", age: 32 };
p.get_name();     // "Chris"
p.set_age(33);    // mutates p.age

Static Methods

Static methods belong to the type itself rather than an instance. A method is automatically static if its first parameter is NOT self:

bishop
Counter :: struct {
    value int
}

// Static method (no self parameter)
Counter :: create() -> Counter {
    return Counter { value: 0 };
}

// Static method with parameters
Counter :: add(int a, int b) -> int {
    return a + b;
}

// Call on type name
c := Counter.create();        // Counter { value: 0 }
result := Counter.add(10, 20); // 30

Unqualified Static Calls

Within the same struct, static methods can be called without qualification. The fully qualified form (StructName.method()) can be used to tighten scope when needed:

bishop
Calculator :: struct {
    value int
}

Calculator :: add(int a, int b) -> int {
    return a + b;
}

// Instance method calling static - unqualified (preferred)
Calculator :: compute(self, int x) -> int {
    return add(self.value, x);
}

// Static method calling another static - unqualified
Calculator :: double_add(int a, int b) -> int {
    return add(a, b) * 2;
}

// Fully qualified form also works
Calculator :: triple_add(int a, int b) -> int {
    return Calculator.add(a, b) * 3;
}

If/Else

bishop
if condition {
    // then
}

if condition {
    // then
} else {
    // else
}

Loops

While Loop

bishop
i := 0;

while i < 5 {
    print(i);
    i = i + 1;
}

Range-based For

bishop
for i in 0..10 {
    print(i);  // prints 0 through 9
}

Foreach

bishop
nums := [1, 2, 3];

for n in nums {
    print(n);
}

Strings

Strings can use double or single quotes:

bishop
str greeting = "Hello, World!";
str name = 'Alice';

// Raw strings (no escape processing)
pattern := r"\d+\.\d+";
path := r"C:\Users\name";
String Methods Reference
MethodDescription
length()Returns string length
empty()Returns true if empty
contains(str)Check if substring exists
starts_with(str)Check prefix
ends_with(str)Check suffix
substr(start, len)Extract substring
upper()Convert to uppercase
lower()Convert to lowercase
trim()Remove whitespace
split(sep)Split into list
replace(old, new)Replace first match
replace_all(old, new)Replace all matches
to_int()Parse as integer
to_float()Parse as float

Lists

bishop
// Creation
nums := List<int>();
names := ["a", "b", "c"];

// Methods
nums.append(42);
nums.length();
nums.get(0);
nums.first();
nums.last();
nums.pop();
nums.contains(42);

// String lists can join
parts := ["hello", "world"];
parts.join(" ");  // "hello world"

Pairs

Pairs hold exactly two values of the same type:

bishop
p := Pair<int>(10, 20);
x := p.first;   // 10
y := p.second;  // 20

// With default
x := p.get(0) default 0;
z := p.get(2) default 99;  // out of bounds, uses default

Tuples

Tuples hold 2-5 values of the same type:

bishop
t := Tuple<int>(10, 20, 30);
x := t.get(0) default 0;   // 10
y := t.get(1) default 0;   // 20

Error Types

bishop
// Simple error
ParseError :: err;

// Error with custom fields
IOError :: err {
    code int,
    path str
}

// Fallible function returning a value
fn read_config(str path) -> Config or err {
    if !fs.exists(path) {
        fail IOError { message: "not found", code: 404, path: path };
    }
    return parse(content);
}

// Fallible function returning nothing
fn save_config(str path, Config cfg) -> void or err {
    fs.write_file(path, cfg.to_str()) or fail err;
}

The or Keyword

Handle errors elegantly inline:

bishop
// Return early
x := fallible() or return;

// Return with default value
x := fallible() or return default_value;

// Propagate error as-is
x := fallible() or fail err;

// Wrap error in new type (auto-chains cause)
x := fallible() or fail NotFound;

// Standalone guard clause
fs.exists(path) or fail "skill not found";

// Block with error access
x := fallible() or {
    print("Error:", err.message);
    return;
};

// Pattern match on error type
x := fallible() or match err {
    IOError    => default_config,
    ParseError => fail err,
    _          => fail ConfigError { message: "unknown", cause: err }
};

// Continue/break in loops
for item in items {
    result := process(item) or continue;
}

Error Handling in main()

The or fail handler works specially in main(): it prints the error and exits with code 1.

bishop
fn main() {
    config := load_config("app.conf") or fail err;
    // If load_config fails, prints error and exits
    print("Loaded:", config.name);
}

Error Chaining

All errors have built-in fields for chaining:

Automatic Chaining

Using or fail ErrorType; automatically sets the original error as the cause:

bishop
NotFound :: err;
ParseError :: err;

fn load_file(str path) -> str or err {
    // If read_file fails, NotFound.cause = original error
    content := fs.read_file(path) or fail NotFound;
    return content;
}

fn load_config(str path) -> Config or err {
    // Chain: ParseError -> NotFound -> original fs error
    content := load_file(path) or fail ParseError;
    return parse(content);
}

Explicit Chaining

For custom messages, use the cause field explicitly:

bishop
content := fs.read_file(path)
    or fail ConfigError { message: "can't read " + path, cause: err };

Accessing the Error Chain

bishop
config := load_config("app.conf") or {
    print(err.message);             // "ParseError"
    print(err.cause.message);       // "NotFound"
    print(err.root_cause.message);  // original fs error
    return;
};

Goroutines

bishop
fn sender(Channel<int> ch, int val) {
    ch.send(val);
}

fn main() {
    ch := Channel<int>();
    go sender(ch, 42);  // spawn goroutine
    val := ch.recv();    // blocks until value available
    print(val);          // 42
}

Channels

bishop
ch := Channel<int>();
ch_str := Channel<str>();
ch_bool := Channel<bool>();

// Send and receive
ch.send(42);
val := ch.recv();

Select Statement

Wait on multiple channel operations:

bishop
ch1 := Channel<int>();
ch2 := Channel<int>();

go sender(ch1, 41);

select {
    case val := ch1.recv() {
        print("received from ch1:", val);
    }
    case val := ch2.recv() {
        print("received from ch2:", val);
    }
}

http

bishop
import http;

fn handle(http.Request req) -> http.Response {
    return http.text("Hello");
}

fn main() {
    http.serve(8080, handle);
}

App-based Routing

bishop
fn main() {
    app := http.App {};
    app.get("/", home);
    app.get("/about", about);
    app.listen(8080);
}

fs

bishop
import fs;

// Reading and writing
content := fs.read_file("path");
_ := fs.write_file("path", "content") or fail err;

// Checks
fs.exists("path");
fs.is_dir("path");
fs.is_file("path");

// Paths
fs.join("dir", "file.txt");
fs.dirname("/home/user/file.txt");
fs.basename("/home/user/file.txt");

// Directory walking
entries := fs.walk("src") or fail err;
for entry in entries {
    print(entry.path);
}

crypto

bishop
import crypto;

// Hashing
hash := crypto.sha256("hello") or return;
hash := crypto.md5("hello") or return;

// Base64
encoded := crypto.base64_encode("Hello!");
decoded := crypto.base64_decode(encoded) or return;

// UUID
id := crypto.uuid() or return;

net

bishop
import net;

// TCP Server
server := net.listen("127.0.0.1", 8080) or return;

with server as s {
    conn := s.accept() or return;
    data := conn.read(1024) or return;
    conn.write("Hello");
    conn.close();
}

// TCP Client
conn := net.connect("example.com", 80) or return;

process

bishop
import process;

// Execute command (blocking)
result := process.run("ls", ["-la"]) or return;
print(result.output);
print(result.exit_code);

// Streaming output over channels
proc := process.spawn("npm", ["install"]) or fail err;
while true {
    line := proc.stdout().recv();
    if line == "" { break; }
    print(line);
}
result := proc.wait() or fail err;

// Environment variables
home := process.env("HOME") or fail err;
process.set_env("MY_VAR", "value");

// Working directory
dir := process.cwd() or fail err;
args := process.args();

regex

bishop
import regex;

re := regex.compile(r"(\d+)-(\d+)") or return;

// Matching
re.matches("123");      // full match
re.contains("abc123");  // partial match

// Finding
m := re.find("Price: 100-200");
if m.found() {
    print(m.text);       // "100-200"
    print(m.group(1));   // "100"
}

// Replacement
re.replace("123-456", "$2-$1");  // "456-123"

math

bishop
import math;

// Constants
math.PI;   // 3.14159...
math.E;    // 2.71828...

// Operations
math.abs(-5.5);
math.sqrt(16.0);
math.pow(2.0, 10.0);
math.sin(math.PI / 2.0);
math.floor(3.7);
math.ceil(3.2);
math.min(3.0, 7.0);
math.max(3.0, 7.0);

time

bishop
import time;

// Duration
d := time.seconds(90);
d := time.minutes(5);
d := time.hours(2);

// Timestamp
now := time.now();
print(now.year, now.month, now.day);

// Arithmetic
future := now + time.days(7);
elapsed := time.since(start);

// Formatting
formatted := now.format("%Y-%m-%d %H:%M:%S");

random

bishop
import random;

dice := random.int(1, 6);
chance := random.float();
coin := random.bool();

items := ["a", "b", "c"];
pick := random.choice(items) or return;
random.shuffle(items);

// Seed for reproducibility
random.seed(42);

algo

bishop
import algo;

nums := [3, 1, 4, 1, 5];

// Sorting
algo.sort_int(nums);

// Aggregation
algo.sum_int(nums);
algo.min_int(nums) or return;

// Transformations
doubled := algo.map_int(nums, fn(int x) -> int { return x * 2; });
large := algo.filter_int(nums, fn(int x) -> bool { return x > 2; });

// Predicates
algo.all_int(nums, fn(int x) -> bool { return x > 0; });
algo.any_int(nums, fn(int x) -> bool { return x > 3; });

json

bishop
import json;

// Parsing
data := json.parse('{"name": "Alice", "age": 30}') or return;
name := data.get_str("name") or return;

// Creating
obj := json.object();
obj.set_str("name", "Charlie");
obj.set_int("age", 25);

// Serialization
json_str := json.stringify(obj);
json_str := json.stringify_pretty(obj);

log

bishop
import log;

log.debug("Debug info");
log.info("App started");
log.warn("High memory");
log.error("Connection failed");

// With key-value
log.info_kv("User login", "user_id", "12345");

// Configuration
log.set_level(log.INFO);
log.add_file("/var/log/app.log");

sync

bishop
import sync;

// Mutex
mtx := sync.mutex_create();
sync.mutex_lock(mtx);
// critical section
sync.mutex_unlock(mtx);

// WaitGroup
wg := sync.waitgroup_create();
sync.waitgroup_add(wg, 3);

go fn() {
    // work
    sync.waitgroup_done(wg);
}();

sync.waitgroup_wait(wg);

// Atomic
counter := sync.atomic_int_create(0);
sync.atomic_int_add(counter, 5);

yaml

bishop
import yaml;

data := yaml.parse("name: Alice\nage: 30") or return;
name := data.get_str("name") or return;

obj := yaml.object();
obj.set_str("name", "Charlie");

yaml_str := yaml.stringify(obj);

markdown

bishop
import markdown;

// Convert to HTML
html := markdown.to_html("# Title\n\n**bold**");

// Extract plain text
text := markdown.to_text("# Hello **World**");

// Document parsing
doc := markdown.parse("# Title") or return;
html := doc.to_html();

Import System

bishop
import http;
import fs;
import tests.testlib;

testlib.greet();
result := testlib.add(2, 3);

Using

Bring module members into local namespace:

bishop
import log;
using log.info, log.debug, log.warn, log.error;

fn main() {
    info("Application started");
}

// Wildcard import
import math;
using math.*;

x := sin(PI / 2.0);

Visibility

Use @private to restrict visibility to the current file:

bishop
@private
fn internal_helper() -> int {
    return 42;
}

@private
MyStruct :: struct {
    value int
}

FFI

TypeDescription
cintC int
cstrC string (const char*)
voidvoid return type
bishop
@extern("c")
fn puts(cstr s) -> cint;

@extern("m")
fn sqrt(f64 x) -> f64;

fn main() {
    puts("Hello from C!");
}

Resource Management

The with statement provides automatic resource cleanup:

bishop
Resource :: struct {
    name str
}

Resource :: close(self) {
    print("Closing resource");
}

fn main() {
    with create_resource("myfile") as res {
        print(res.name);
    }  // res.close() called automatically
}