Fork me on GitHub Mozilla

heap.coffee

heap.coffee is a dialect of CoffeeScript that offers a C-like type system with manual memory management. It compiles to JavaScript and lets you write memory-efficient and GC pause-free code less painfully. heap.coffee, like its relative *JS, is early research prototype work, so don't expect anything rock solid just yet. The research goal here is to explore low-level statically typed features in a high-level dynamically typed language.

This is an interactive tutorial, code is compiled as you type. To execute a piece of code press Ctrl-R or Cmd-Enter.

Type Declarations and Synonyms

Type declarations are done using :: . To disambiguate the prototype operator ::, the "is-type" operator must be spaced on either side.

Local variables and destructuring left-hand sides may be typed. Though both sides of a destructuring type declaration must match up. That is, you cannot declare a plain variable to be of a destructuring type.

It is an error to redeclare of the type of a variable.

Types may be aliased, but only at the toplevel.

type size_t = int
j = 42
i :: int
i = j as size_t   # Assigning from untyped requires cast.
i = i + 4         # Arithmetic expressions have the Java-
                  # Scriptnumber type `num', which is
                  # effectively adouble. An implicit
                  # conversion happens.

{n} :: {n: int}   # Destructuring is okay as long as both
                  # sides agree.
{n} = {n: i}

f :: (int) -> int # Function types are composed using ->.
f = (x) -> x

Data Types and Arithmetic

Like *JS, heap.coffee has 6 integral types: i32, u32, i16, u16, i8, and u8, which behave as they do in C. Additionally there is double, the JavaScript number type, and any which are used to interoperate with the JavaScript type system. Arithmetic operations usually produce the double type even if their operands are integral types due to possible overflow. heap.coffee emits the appropriate coercions for assignments and cast operations.

We also have the aliases int = i32, uint = u32, short = i16, byte = u8, and num = double.

sizeof i8
sizeof u8
sizeof i16
sizeof u16
sizeof i32
sizeof u32
sizeof double

# Arithmetic follows JavaScript semantics. Coercions
# happen during assignment.

x, y :: int
x = 3
y = 2

x / y        # Not an integer.
x += x / y   # Is an integer.

heap.coffee allows you to define struct types, which is really the main achievement of all these types.

type point = struct
  x :: int
  y :: int
  z :: int

type line = struct
  start :: point
  end   :: point

type box = struct
  left   :: line
  top    :: line
  right  :: line
  bottom :: line

# Stack allocate a box and a take a pointer to its
# top line.
b :: box
p :: *line
p = &b.top

# Dot notation is overloaded to also work with struct
# pointers.
p.start.x = 42
print p.start.x

Functions

Since all functions in CoffeeScript are lambdas, the type of a function value is the unified type of all its return expressions. However, the parameter types must be explicitly declared. To do so there is both an explicit notation and a convenience notation.

# Explicit notation for typing parameters.
#
# What happens here is that f is assigned
# the type (int) -> int, and that the right-
# hand side expression of the assignment
# computes the type (int) -> int, and then
# the assignment tries to reconcile the two.
# Since they are actually the same type,
# everything works out.
f :: (int) -> int
f = (x) :: (int) -> x

# The convenience notation only works when
# the right hand side of the variable
# being assigned to is a *syntactic* function.
# It will then automatically propagate the
# parameter type declarations to the right-
# hand side of the assignment.
g :: (int) -> int
g = (x) -> x

# Higher-order is okay.
add :: (int) -> (int) -> int
add = (x) -> (y) -> x + y

Objects and Memory

heap.coffee allows you to use manual memory management in addition to using the managed runtime of JavaScript objects. The reason for this is simple: performance. Even though JavaScript's managed runtime is getting faster everyday, garbage collection still takes a toll on performance, especially pauses.

Moreover, even something like a linked list cannot be declared in JavaScript without the extra information of JavaScript objects. heap.coffee allows you to metaphorically drop below that object model and back into something C-like, but now instead of receiving SIGSEGV and crashing, at worst the compiled JavaScript will just be buggy.

type node = struct
  val  :: int
  next :: *node

head, tail :: *node
# new compiles to malloc when the argument is a
# type. Similarly, delete compiles to free.
head = new node
tail = head

add :: (int) -> *node
add = (val) ->
  next :: *node
  next = new node
  next.val = val
  tail = tail.next = next
  next

add 1
add 2
add 3

p :: *node
p = head.next
while p
  # The cast here is because the typechecker
  # will complain that one of the operands of
  # pointer arithmetic is not an integer.
  print "node at address: " + (p as any) +
        " has value: " + p.val
  p = p.next

malloc and free themselves are implemented in heap.coffee:

#   +---------------+ -
# 0 | Heap  Pointer |
# 1 | Stack Pointer |
#   +---------------+ <- Heap Pointer
#   |               |
#   |               | |
#   |     HEAP      | | Malloc Region
#   |               | v
#   |               |
#   +---------------+
#   |               |
#   |               | ^
#   |     STACK     | |
#   |               | |
#   |               |
#   +---------------+ <- Stack Pointer

type header = struct
  ptr  :: *header
  size :: uint

freep :: *header
freep = null

malloc :: (uint) -> *any
malloc = (nbytes) ->
  p, prevp :: *header
  nunits   :: uint

  nunits = (nbytes + sizeof header - 1) / sizeof header + 1
  unless prevp = freep
    # Haven't allocated a free list yet, do it now.
    prevp = freep = _U32[0] as *any
    _U32[0] += sizeof header
    freep.ptr  = freep
    freep.size = 0

  p = prevp.ptr
  loop
    if p.size >= nunits
      if p.size is nunits
        prevp.ptr = p.ptr
      else
        p.size -= nunits
        p += p.size
        p.size = nunits
      freep = prevp
      return p + 1
    if p is freep
      return null unless p = morecore nunits
    prevp = p
    p = p.ptr
  null

NALLOC :: int
NALLOC = 1024

morecore :: (uint) -> *header
morecore = (nu) ->
  _U8 = _HEAP.U8
  nu = NALLOC if nu < NALLOC
  bytesNeeded = nu * sizeof header
  return null if _U32[0] + bytesNeeded >= _U8.length
  up :: *header
  up = _U32[0] as *any
  up.size = nu;
  _U32[0] += bytesNeeded
  free up + 1
  freep

free :: (*any) -> any
free = (ap) ->
  bp, p :: *header

  bp = (ap as *header) - 1

  p = freep
  until (bp > p and bp < p.ptr)
    if p >= p.ptr and (bp > p or bp < p.ptr)
      break
    p = p.ptr

  if (bp + bp.size is p.ptr)
    bp.size += p.ptr.size
    bp.ptr = p.ptr.ptr
  else
    bp.ptr = p.ptr

  if (p + p.size is bp)
    p.size += bp.size
    p.ptr = bp.ptr
  else
    p.ptr = bp

  freep = p
  return

exports.malloc = malloc
exports.free   = free

Acknowledgements

Emscripten for the inspiration, *JS for keeping each other honest, and CodeMirror for the code editor.