Copper

Overview

Table of Content

Copper is a simple procedural programming language, statically typed with basic type inference and genericity.

Values

Copper manipulates only references to objects. Simple data types such as integers must be seen as references to stateless objects (i.e. objects that have no state in memory).

How object references work

Structures and arrays can be directly embedded in other strucrures but they are always accessed by their reference.

Contextual Values

Some expressions may have a different values depending on the context, for instance a literal integer 65 can be a signed 8-bit integer, a 64-bit integer, a Unicode code point or any integer subclass (integers can be subclassed) depending on the expected type.

This applies to string and integer literals, to constant initializers and to expressions starting with a dot.

Integer Literals

The language does not favor any type of integer as the 'main' one or the 'default' one; so it must always know the expected type when it finds a literal integer.

Usually the expected type of an expression is known:

func f(x: Uint8)... // Uint8 is the 8-bit unsigned integer type.

f(5) // ok

var a: Int32 // Int32 is the 32-bit signed integer type.
a = 5 // ok

point.x = 5 // ok

But when defining a local variable or when passing an argument to a generic function, the expected type is unknown.

var a = 5 //  error, the type of the literal must be specified
var b = Int32: 5 // ok

func g(x: *)...

g(5) // error
g(Uint8: 5) // ok

When creating integer subclasses, it is possible to assign a literal value the same way.

class Age: Uint8
end

var age = Age: 32

var x = Uint8: 0
x = age // ok: an Age is an Uint8
age = x // error: an Uint8 is not an Age

String Literals

This works similarly with strings: the language handle two kind of strings: 8-bit strings and 16-bit strings.

func MessageBoxA(msg: Uint8[]) // ASCII string
func MessageBoxW(msg: Uint16[]) // UTF-16 string

MessageBoxA("hello world") // The string is encoded with 8-bit characters
MessageBoxW("hello world") // The string is encoded with 16-bit characters

Note that the compiler reads the input file as an 8-bit character stream without encoding assumption, so a 16-bit string is just built by extending the 8-bit characters.

Initializers

An initializer is a list of values between square brackets. But the meaning depends on the expected type: it can be an array as well as a structure.

var pt: Point3D
var i: Int32[]
var b: Uint8[]

pt = [1, 2, 3] // A point with x=1, y=2, z=3
i = [1, 2, 3]  // An array of 3 32-bit integers
b = [1, 2, 3]  // An array of 3 bytes

Enum Values

Copper does not have enums, but it is possible subclass integers and define static constants on it.

class MouseButton: Int32 // Create a subclass of Int32
    left   // Create a constant 'MouseButton.left'
           // with value 0 and type MouseButton
    middle // 1
    right  // 2
end

class Alignment: Int32
    left = 101
    right = 102
    center = 103
    justify = 104
end

When the class is an integer, the enum values can be automatically numbered.

Such enums can be used with the fully qualified name:

var button = MouseButton.left
var align = Alignment.center

When the expected type is known, the type can be omitted and the name can just start with .:

var button: MouseButton
button = .left // 0
if button == .right

var align: Alignment
align = .left // 101

This is not limited to constants, it also works with static variables and functions:

class Point
    attr x: Int
    attr y: Int

    origin = [0, 0]

    var last = origin

    func new(x: Int, y: Int): Point
        var self = Point.alloc
        self.x = x
        self.y = y
        return self
    end
end

var pt: Point
pt = .origin    // same as Point.origin
pt = .last      // same as Point.last
pt = .new(1, 2) // same as Point.new(1, 2)

Since an enum is just a class, an enumeration is not limited to integers:

class Error: String
    notFound    = "File not found"
    diskFull    = "Disk is full"
    writeAccess = "Write access"
end

Multiple Return Values

A function returns 0, 1 or more values.

func sort(x: Int, y: Int): Int, Int
    if x > y
        return y, x
    else
        return x, y
    end
end

func main
    var min, max = sort(51, 15)
end

Swapping values is easy:

x, y = y, x

Object Oriented Programming

The language is not object oriented but it provides some features to facilitate object oriented designs.

class Point
    attr x: Int
    attr y: Int

    // A constructor
    func new(x: Int, y: Int): Point
        var pt = Point.alloc // Allocate a point on the heap
        pt.x = x
        pt.y = y
        return pt
    end

    // A method
    def resize(factor: Int)
        self.x += scale(self.x, factor)
        self.y += scale(self.y, factor)
    end

    [private]
    // An utility function visible only inside the class
    func scale(value: Int, factor: Int): Int
        return value * factor
    end
end

func test(x: Int, y: Int, f: Int)
    var pt = Point.new(x, y)
    pt.resize(f)
end

Block Closures

A block closure is a block that is passed as an additional argument to a function. The function can evaluate this block using the yield statement.

// Iterates between start and limit
func range(start: Int, limit: Int) do (Int)
    var i = start
    while i <= limit
        yield(i)// Call the block with i as argument
        i += 1
    end
end

func main: Int
    var sum = Int: 0
    // Call the range function with start, limit and the block
    range(1, 10) do x // x is the parameter of the block 
                      // (it will get the value of 'i' above)
        sum += x        
    end                 
    return sum
end

Block closures provides a better abstraction: they allow to enumerate sequence's elements without knowing the implementation.

Stricly speaking, this is not real closures: functions taking a block as parameter are implemented as kind of hygienic macros and are inlined in the caller's function. This is very efficient but it requires to be careful when writing big macros as it could generate huge binaries.

Blocks are not limited to a single variable:

dictionary.eachKeyAndValue do key, value
list.eachIndexAndValue do index, value

It is not limited to iterators:

func readFile(name: String) do (File)
    var f = fopen(name, "r")
    if f <> nil
        yield(f)
        fclose(f)
    end
end

func main
    readFile("dummy.txt") do file
        // Use file here
    end
end

An object can have multiple iterators:

file.eachLine do ln
file.eachChar do ch

A block can return a value to the function using pass:

// Delete all completed tasks
tasks.deleteAllSuchThat do task
    pass task.isCompleted
end

Genericity

A parameter can use * as the type to accept any type and have an optional alias usable in the return types or the body of the function:

func max(x: * (T), y: *): T
    if x > y
        return x
    else
        return y
    end
end

This function works for any type of x as long as x > y is valid (the operators can be overloaded).

Classes can have parameters:

class Vector(T: *)
    attr items: T[]  // Pointer to an array of T
    attr size: Size
    ...
end

Types can be passed as regular arguments to functions.

func new(T: *): T
    var obj = malloc(sizeof(T!)).cast(T)
    return obj
end

func test
    var point = new(Point)
end

Type parameters are for the compiler only, they are removed from the list of parameters when the code is generated.

Variadic arguments

Functions can have a variable number of arguments when the last parameter has ....

func sum(args: Int...): Int
    var sum = Int: 0
    args.each do x
        sum += x
    end
    return sum
end

Passing variadic arguments to another functions is simple, just use the values property:

func sum_plus_3(args: Int...): Int
    return sum(1, args.values, 2)
end

Variadic functions are implemented using genericity: one function is generated for each signature.

Order of Definitions Is Not Significant

No need of forward declarations, you can group definitions logically, not in an order forced by the limitations of the compiler. Even imports can be located anywhere in the source file.

You can write for instance your program in a top down approach: write main first, then sub-function, then sub-sub-functions, ...