Overview
Table of Content
- Values
- Contextual Values
- Multiple Return Values
- Object Oriented Programming
- Block Closures
- Genericity
- Variadic arguments
- Order of Definitions Is Not Significant
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).
Structures and arrays can be directly embedded in other strucrures but they are always accessed by their reference.
- An assign statement (
=
) is always a copy of the reference. - Passing an expression to a function is always passing the reference by value.
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.
- Type inheritance.
- Contextual functions (methods).
- Create kind-of objects by embedding everything needed inside a type definition.
- A
vtable
command can generate virtual function tables based on a specification.
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, ...