Nasper

Nasper is a small, native, linearly typed, performant functional programming language. I created this over my time in undergrad, as I was annoyed every time I used a programming language because the semantics in my head, learned from theory, were so much cleaner than what I was forced to write in imperative languages.

At the same time, I was dissatisfied by the lack of hardware awareness in functional languages, which prevented their use in serious engineering endeavors. So I used linear typing and stream and closure monomorphization to create hardware-aware primitives in a language with functional semantics: allowing for the speed of systems programming languages, the nice semantic properties of functional languages, combined together with the craft and simplicity of languages like C or Go (at least of old Go; nowadays Go has lost the Thompson and Pike touch and made some really overly complex decisions).

I was able to achieve some nice results with this language design. The self-hosted compiler bootstraps itself from its own 65,000 lines in 12 seconds, LLVM and clang included. An optimized build of the compiler bootstraps the same 65,000 lines in 11. One thread of the HTTP server on an M5 Pro MacBook handles 225k requests a second over HTTP/1.1 and 852k over HTTP/2, in 4 MB of memory. On the same machine and the same handler, Rust's hyper on one thread does 220k and 772k in 19 MB, Go's net/http 127k and 292k, and Node 132k and 213k. All of this in a language without mutation is pretty cool.

main (streams, print)

# A record, and a method that consumes it.
# The result type names the case that fails.
<account: owner str, balance int>

(a account) withdraw(amount int) account ? str
    if amount > a.balance
        "insufficient funds"
    <*a, balance int: balance - amount>

# An infinite stream: only what is pulled is built.
fibs(a int, b int) |int|
    |int: a, *fibs(b, a + b)|

evenSum(count int) int
    first = streams.take(fibs(0, 1), count)
    even = streams.filter(first, (x &int: x % 2 == 0))
    streams.sum(even, 0)

main()
    acc = <account: owner = "Ada", balance = 100>
    total = evenSum(20)
    when acc.withdraw(30) is
        left print.line("{left.owner}: {left.balance}, {total}")
        problem print.line("{problem}")
Ada: 70, 3382
Examples
  1. Hello
  2. Values
  3. Bindings
  4. Functions
  5. Conditionals
  6. Recursion
  7. Records
  8. Methods
  9. Results
  10. Unions
  11. Lists
  12. Dictionaries
  13. Ownership
  14. Views
  15. Callables
  16. Closures
  17. Streams
  18. Infinite streams
  19. Matching streams
  20. Reductions
  21. Generics and interfaces
  22. Modules
  23. Templates
  24. Errors and exit
  25. An HTTP server

Hello

main (print)

# A file starts with its module name and the modules it uses.
# main() is where the program starts.
main()
    name = "Nasper"
    print.line("Hello, {name}.")
Hello, Nasper.
main (print)

main()
    count = 42            # int
    ratio = 1.5           # float
    flag = true           # bool
    letter = b"n"         # byte
    text = "hello"        # str, a view of bytes
    # Numbers never convert on their own: float(count) says so.
    scaled = float(count) * ratio
    code = int(letter)
    print.line("{count} {ratio} {flag} {code} {text} {scaled}")
42 1.5 true 110 hello 63.0
main (print)

# A module-level binding is a constant.
limit = 10

main()
    # name = value binds once. Binding the same name again shadows it;
    # nothing is changed in place.
    total = 1
    total = total + limit
    total = total * 2
    print.line("{total}")
22
main (print)

# Parameters carry their types; the result type follows the parentheses.
# The last expression of the body is the value. There is no return.
square(x int) int
    x * x

sumOfSquares(x int, y int) int
    square(x) + square(y)

# A function with no result is a procedure; its type is none.
report(label str, value int) none
    print.line("{label}: {value}")

main()
    report("sum", sumOfSquares(3, 4))
sum: 25
main (print)

# An if guards a block. When it holds, the block's value is the answer
# and nothing below runs. When it fails, reading continues on the next
# line. The lines after an if are its else.
sign(n int) str
    if n < 0
        "negative"
    if n == 0
        "zero"
    "positive"

between(n int, lo int, hi int) bool
    n >= lo and n <= hi and not (n == 13)

main()
    a = sign(0 - 5)
    b = sign(0)
    c = between(7, 1, 10)
    print.line("{a} {b} {c}")
negative zero true
main (print)

# A call in tail position is a loop: no memory grows.
sumTo(n int, acc int) int
    if n == 0
        acc
    sumTo(n - 1, acc + n)

# A call with work waiting after it must be marked recur.
# The compiler checks the mark both ways.
factorial(n int) int
    if n <= 1
        1
    n * recur factorial(n - 1)

main()
    a = sumTo(1000000, 0)
    b = factorial(10)
    print.line("{a} {b}")
500000500000 3628800
main (print)

<point: x int, y int>

main()
    p = <point: x = 1, y = 2>
    # Read a field with a dot.
    sum = p.x + p.y
    # An update starts with *, keeps the other fields, and consumes p.
    q = <*p, y = 10>
    # A typed field transform sees the old value.
    r = <*q, x int: x + 100>
    # Destructure in declaration order.
    x, y = r
    print.line("{sum} {x} {y}")
3 101 10
main (print)

<counter: hits int>

# A method is a function with its receiver first. A borrowed receiver
# (&) reads; an owned receiver consumes and usually returns the next value.
(c &counter) show() none
    print.line("{c.hits}")

(c counter) bump() counter
    <*c, hits int: hits + 1>

main()
    c = <counter: hits = 0>
    c = c.bump().bump()
    c.show()
2
main (print)

# T ? E is a success value or an error value. Return either directly.
half(n int) int ? str
    if n % 2 != 0
        "odd"
    n / 2

forward(error str) str from error
    error

# ?: unwraps or falls back. ? unwraps or returns the handler's value
# from the current function.
quarter(n int) int ? str
    h = half(n) ? forward
    half(h)

main()
    a = half(8) ?: 0
    b = half(7) ?: 0
    when quarter(12) is
        value print.line("{a} {b} {value}")
        error print.line("{error}")
4 0 3
main (print)

# A union is one of several named variants, each with its own payload.
<shape: circle float | square float | empty none>

area(s &shape) float
    when s is
        <shape: circle> 3.14159 * circle * circle
        <shape: square as side> side * side
        <shape: empty> 0.0

main()
    shapes = [shape: <shape: circle = 1.0>, <shape: square = 2.0>, <shape: empty>]
    update total = 0.0 to total + area(s) for s in stream(shapes)
    print.line("{total}")
7.14159
main (print)

main()
    xs = [int: 10, 20, 30]
    first = xs[0]
    n = len(xs)
    # Appending spreads the old list into the new one and consumes it.
    xs = [*xs, 40]
    # Replacing an element does the same.
    xs = [*xs, 1 = 25]
    # A slice is a view of a range; it copies nothing.
    tail = xs[1:len(xs)]
    m = len(tail)
    print.line("{first} {n} {m} {xs[1]} {xs[3]}")
10 3 3 25 40
main (print)

main()
    ages = [str -> int: "Ada" -> 36]
    # Insert or replace, consuming the old dictionary.
    ages = [*ages, "Lin" -> 41]
    ages = [*ages, "Ada" -> 37]
    # lookup returns a result, since the key may be missing.
    ada = lookup(ages, "Ada") ?: 0
    nobody = lookup(ages, "Bo") ?: 0 - 1
    n = len(ages)
    print.line("{ada} {nobody} {n}")
37 -1 2
main (print)

# [int] takes ownership: the caller's list moves in and a new one moves out.
append(xs [int], x int) [int]
    [*xs, x]

# &[int] borrows: the function may read but not consume or extend.
total(xs &[int]) int
    update sum = 0 to sum + x for x in stream(xs)
    sum

main()
    xs = [int: 1, 2]
    xs = append(xs, 3)
    a = total(xs)
    b = total(xs)
    # copy makes a new owner; the original is untouched.
    ys = append(copy(xs), 4)
    print.line("{a} {b} {len(xs)} {len(ys)}")
6 6 3 4
main (bytes, print)

# str is a view into someone else's bytes. A function returning a view
# says which parameter it came from with `from`.
afterColon(text str) str from text
    at = bytes.indexOf(text, ":") ?: 0 - 1
    if at < 0
        text
    text[at + 1:len(text)]

main()
    line = [byte: *"key:value"]
    value = afterColon(line[0:len(line)])
    same = bytes.equal(value, "value")
    print.line("{value} {same}")
value true
main (print)

# (int -> int) is a callable. A named function or a lambda fits it.
# A callable is a value that moves; borrow it (&) to use it twice.
twice(f &(int -> int), x int) int
    f(f(x))

inc(x int) int
    x + 1

main()
    offset = 10
    addOffset = (x int: x + offset)
    a = twice(inc, 1)
    b = twice(addOffset, 1)
    print.line("{a} {b}")
3 21
main (print)

# A function can return a lambda. What comes back is a record the
# compiler writes, holding what was captured, with the body as its code.
makeAdder(amount int) (int -> int)
    (x int: x + amount)

scale(factor int) (int -> int)
    (x int: x * factor)

main()
    addFive = makeAdder(5)
    triple = scale(3)
    a = addFive(1)
    b = triple(addFive(2))
    print.line("{a} {b}")
6 21
main (streams, print)

main()
    xs = [int: 1, 2, 3, 4, 5, 6]
    # stream(xs) borrows the list and yields its elements.
    evens = streams.filter(stream(xs), (x &int: x % 2 == 0))
    squares = streams.map(evens, (x &int: x * x))
    total = streams.sum(squares, 0)
    # A pipeline builds no intermediate list; collect when you want one.
    firstThree = streams.collect(streams.take(streams.range(1, 100), 3), [int:])
    print.line("{total} {len(firstThree)}")
56 3
main (streams, print)

# |int| is a stream. The tail is not evaluated until it is pulled,
# so a stream can be infinite.
from(n int) |int|
    |int: n, *from(n + 1)|

fibs(a int, b int) |int|
    |int: a, *fibs(b, a + b)|

main()
    tenth = streams.first(streams.dropFirst(fibs(0, 1), 10), 0)
    total = streams.sum(streams.take(from(1), 100), 0)
    print.line("{tenth} {total}")
55 5050
main (streams, print)

# when takes a stream apart: empty, or a head followed by a tail.
# Building a stream from a recursive call keeps it lazy.
runningTotal(s |int|, acc int) |int|
    when s is
        |int:| |int:|
        |int: head, *tail| |int: acc + head, *runningTotal(tail, acc + head)|

main()
    sums = streams.collect(runningTotal(streams.range(1, 6), 0), [int:])
    print.line("{sums[0]} {sums[4]}")
1 15
main (streams, print)

main()
    words = [str: "the", "cat", "the", "hat"]
    # update names an accumulator, its start, its step, and its source.
    update letters = 0 to letters + len(w) for w in stream(words)
    # A longer step is a block; for goes on its own line.
    update counts = [str -> int:] to
        seen = lookup(counts, w) ?: 0
        [*counts, w -> seen + 1]
    for w in stream(words)
    the = lookup(counts, "the") ?: 0
    print.line("{letters} {the}")
12 2
main (print)

# A type parameter in brackets. Any places no requirement on it.
first[a Any](xs &[a], fallback a) a
    if len(xs) == 0
        fallback
    xs[0]

# An interface lists methods; a type satisfies it by declaring them.
{Legged:
    (&self) legs() int}

<cat: tag str>
(c &cat) legs() int
    4

count[a Legged](thing &a) none
    print.line("{thing.legs()} legs")

main()
    xs = [int: 7, 8]
    tom = <cat: tag = "tom">
    shown = count(tom)
    print.line("{first(xs, 0)}")
4 legs
7
main (./geometry, print)

# A directory is a module. Only names marked * are visible outside it.
main()
    p = <geometry.point: x = 1, y = 2>
    q = p.shift(10, 10)
    print.line("{q.x} {q.y}")
geometry/geometry.np
geometry

<point*: x int, y int>

(p point) shift*(dx int, dy int) point
    <*p, x int: x + dx, y int: y + dy>
11 12
main (format, print)

main()
    name = "Ada"
    # Text with holes is a Template. Render it into owned bytes,
    # or hand it to something that renders, like print.line.
    greeting = format.bytes("Hello, {name}: {6 * 7}")
    # format.append consumes and returns the buffer it extends.
    longer = format.append(greeting, "!")
    # {&longer} borrows the bytes for rendering.
    print.line("{&longer} \{braces\}")
Hello, Ada: 42! {braces}
main (os, print)

parse(text str) int ? str
    if len(text) == 0
        "empty input"
    len(text)

main()
    when parse("np") is
        n print.line("{n}")
        problem
            # print.error writes to stderr; os.exit sets the status.
            shown = print.error("error: {problem}")
            os.exit(2)
2
main (http/app, http, os)

# A complete HTTP server. Handlers borrow the request and return a
# response; route answers on a match and ?: falls through.
hello(req &app.request) http.response
    app.html(200, "<h1>Hello, np.</h1>")

json(req &app.request) http.response
    app.json(<ok = true, language = "np">)

routes(req &app.request) http.response
    req.route("GET /", hello) ?: req.route("GET /health", json) ?: req.notFound()

main()
    os.exit(app.serve(8080, routes))
Standard Library
lib
Can I use it?

I haven't released it publicly, but it is working: the server for this website is written in Nasper, for example. Email me if you want to use it: jacob@thecaminoapp.com.