Introduction
guest = get_your_name:
print("Welcome, " guest: "!")
Welcome to the (NEW) documentation of the Copper interpreted programming language!
Copper Lang is a statically-typed language that revolves around the concept of object-functions. It emphasizes stack-based paradigms and memory safety, eliminating the problems of memory-leaks and null pointers.
The virtual machine is designed to be a stand-alone engine for easily embedding into other applications and is written in portable, version agnostic C++ (tested with both C++98 and C++11 settings on GCC).
To jump in and start using it right away, read:
Overview
Copper as a language is like a cross between Javascript, Rust, and Lisp. Every object is a function and vice versa. Object-functions are created in a manner similar to (but not the same as) Javascript. Members are created like in Python. And in order to do just about anything (including access data), you have to run a function. Whitespace is almost completely irrelevant other than that it must exist between names.
To guarantee memory safety, variables only store and handle functions and function pointers (which are safetly converted to normal functions upon access failure). There is no null! Furthermore, variables are all tied to the virtual stack in some way, thereby preventing memory leaks.
The Copper virtual machine is a set of files containing all of the basic necessities to process the language. The main entry point is via the Copper Engine, which requires an input feed of characters and performs all of the parsing and operations. Some basic extensions have been provided for reading and printing to the terminal/console as well as performing some extra mathematical operations.
Built-in data-types include booleans, integers, numbers with decimals, strings, and lists.
Attractive Features
- Powerful object-oriented programming!
- Easy to Learn - Very simple syntax (See Copper in 5 minutes.)
- Lightweight (Core code is roughly 300kb)
- No external dependencies
- Highly compressable
- IDE support
(Current IDE support is gtksourceview2 syntax highlighters.)
Reasons to NOT Use Copper
- Not battle-hardened (Tested, but not on a business level.)
- No automated testing (YET)
- Some basic functions are extensions (See Foreign Function Interface.)
- Relatively slow (Perhaps... Compared to Lua.)
Uses for Copper
- GUI Layouts
- Consumer/User-Created Code Sandboxes
- Embedded Applications
The Language of Copper
Copper revolves around the concept of the object-function (or "function-object" if you prefer). Variables in Copper only store object-functions or pointers to them, nothing else. Operators are used only for manipulating function objects. Data manipulation is only done by functions (especially foreign functions).
Copper has a strict set of rules regarding its interpretation. (See Fundamental Rules.) As a small set of rules, it reeduces the mental focus needed to read the code.
Comments are created by sandwiching the comment with two pound signs, "#", one on each side.
Fundamental Rules
There are three very important rules that govern the language:
- Data end an expression. - That means all numbers, strings, and anything else that does not perform processing will automatically cause the termination of an expression (statement of processing).
- A function call ends an expression. - That implies you can't perform actions such as scope-opening on the return of a function.
- A variable, not followed by the appropriate tokens, ends the expression. - That means if you don't try to call the function, assign to it, or access a member variable, the function will simply return itself to end the expression.
Consequently, there is no need for end-of-statement terminators, but the comma (,) is allowed if you want to make code more readable.
Function calls end expressions because they return data.
Object-functions
The object-function is a data type that has both a table of persistent, publically-accessible members and a body of code that can be executed. The members are stored in a "persistent scope" - a hash-mapped table of variables that belong to only one object-function. The body of code (called the "execution body") is also unique to the object-function (however, because it is constant, it might be shared with multiple objects to save space).
# Construction 1 # []
# Construction 2 # {}
# Construction 3 # []{}
[ # object component # ] { # execution body # }
An object-function can be created directly in three ways:
- Using the object-body tokens, "[" and "]"
- Using the execution-body tokens, "{" and "}"
- Using the object-body tokens followed by the execution-body tokens
# Creating an object-function with a member, parameter, and body #
some_object_function = [
member = "has data"
parameter
] {
print("In the body is a " parameter:)
}
// Analogous C++ object
class Object {
public:
virtual ~Object() {}
virtual Object* operator() = 0;
};
The object component plays a dual role in the grammar. Within the object-body tokens there are two types of parameters: function parameters and persistent-scope parameters. Any parameter that is assigned with the assignment operator or pointer operator becomes a member of the object-function, stored away in the persistent scope of the function. (See Variables and Members for more about assignment.) Any unassigned parameter becomes a regular parameter and is assigned whatever the function is passed via the argument tokens.
Optional: The arguments can be separated with a comma.
some_func:
some_func(arg)
To call a function, it must first reside within a variable. Appending the direct-call operator ":" or the argument-wrapping tokens "(" and ")" (parentheses) to the function will call it. The direct-call operator is faster, but only the arguments operator can take the accepted arguments and map them to the object-function parameters.
Arguments are always passed by reference. Raw data passed as an argument is stored in a new function assigned to the function parameter at its argument index.
Function calls can accept any number of arguments. However, there will only be as many arguments mapped as there are parameters. When not enough arguments are given to a function, the parameter is set to an empty function.
a = {
# "this" points to "a", super points to nothing #
this.c = 10
}
a.b = {
# "this" points to "b", super points to "a" #
super.c = 5
}
print(a.c:) # prints 5 #
When a function is called, a function is given a member variable named this
or that member is updated. The this
member is a pointer to the wrapping object-function itself to allow you to access its members. If the function is stored in a variable that is a member of another object-function, it is given a member named super
that points to that preceding object-function in the address chain.
Function return is done by calling the built-in return function, "ret". Only one argument will be returned. All others are ignored. Arguments are returned by reference. Data is stored in a new function when returned.
Variables and Members
a # valid Copper #
b = {
# accesing a global variable #
a()
# accessing a member variable #
this.member = 5
# local variable #
c = 4
ret(c)
}
A variable can be created by simply stating its name. If the variable was created at the local scope level of a function body, it will be destroyed when the function terminates. However, global variables cannot be deleted by other Copper code.
All variables can trace their ancestry to either the global or local scope, thereby limiting all of them to stack-based lifetimes.
A variable can either own a function or be a pointer to it. When a pointer, a variable shares the function with another variable that does own the function. If the original function is destroyed, then the pointer variable is given its own new function the next time it is accessed, and that way, there is no segmentation fault.
The members of a function assigned to a variable can be accessed using the member-access operator "." (period) appended to the end of a variable (such as "this"). A member can be created simply declaring it following the member-access operator. This allows chaining members and member-access operators to create variables.
Assignment
a = 10
An assignment to a variable occurs via the assignment operator, "=". The variable to the left receives the data from the right. Both raw data (such as integers) and functions can be assigned to a variable. All assignments from one variable to another are copy actions, whereby the object-function and all of its members are copied from the giving variable into the receiving variable. The same action takes place when an object-function or data is returned from a function call.
Assigning data directly to a variable results in the function body being given a direct-return value. Whenever the function is called, this value is returned immediately. Consequently, it is faster than the equivalent function body.
Pointer Assignment and Usage
b ~ a
b = 12
print(a:) # Prints 12 #
Pointer assignment occurs via the pointer-assignment operator, "~". The variable to the left points to the data on the right. However, if the pointer assignment is to data, that data is stashed into a new, ownerless function which is then owned by the variable it is assigned to.
Member Creation
a.b = 0
When a function is pointed to by a variable, the members of that function can all be accessed via the member-access operator. Members can also be created this way. The function's direct return can also be set this way.
Data
# boolean #
true
# integer #
10
# number with decimal #
0.981
# string #
"string blarg"
# list #
list(false 7 6.5 "4" some_func)
There are five basic data types in Copper apart from object-functions:
- boolean
- integer
- numbers with decimal points
- byte strings
- lists
Booleans are represented with true
and false
.
Integers are created by writing any number without a decimal. Negative are allowed but cannot be created this way.
Numbers with decimal are created by writing any number with a decimal. However, the decimal must not be the first character in the number. (A zero can be used first.) Negative values are allowed but cannot be created this way.
Byte strings are created by using double-quotes around text. Escape characters are permitted and can be created using the usual slash. Permitted escape character sequences include: - \n - \r - \t
Lists are created using the built-in list
function. (See the Copper Lang API for details.) They can contain both object-functions and data. The list implementation in the virtual machine is a doubly-linked list.
Program Termination
The program exits when the virtual machine encounters exit
.
Global variables are not cleared upon program exit in order that they can still be used for callbacks.
Control Structures
The Copper language provides a couple basic control structures - if
and loop
- as well as a set of special ownership operators: own
, is_ptr
, and is_owner
.
If-structures
if ( false ) {
print( "False" )
} elif ( true ) {
print( "True" )
} else {
print( "What?" )
}
If-structures in Copper require their conditionals wrapped in the argument-wrapping tokens (parentheses), and the bodies of code that are executed upon true conditions must be wrapped in the execution-body tokens (curly braces).
Technically, any code can be run inside of the if-structure conditionals, but the last result must be a boolean value in type.
If structures can have optional elif
and else
components. The elif
component must obviously have a conditional.
Loop-structures
s = true
loop {
if ( s: ) {
s = false
skip
}
stop
}
// Analogous C++
bool s = true;
while( true ) {
if ( s ) {
s = false;
continue;
}
break;
}
Copper has no for-loops or while-loops. Instead, it has a single, infinitely-looping structure whose body of code is contained within the execution body tokens (curly braces).
The loop is terminated whenever the "stop" token is found.
The loop is restarted whenever the "skip" token is found.
Ownership structures
b.c ~ a
print( is_ptr(b.c) ) # Prints true #
print( is_owner(a) ) # Prints true #
own(b.c) # Steal ownership #
print( is_owner(b.c) ) # Prints true #
The ownership structures are special operators that are called like functions but can each only receive a variable (not data). They are used to provide information about the variable itself or to modify ownership.
The is_owner
operator indicates if the given variable is the owner of the object-function it points to, whereas the is_ptr
operator indicates if a variable is merely a pointer.
The own
operator changes the ownership of the function pointed to by the variable to that variable itself, regardless of the stack lifetime of that variable. It is used primary for changing pointer ownership.
By default, this feature is disabled and can only be enabled by passing true
to Engine::setOwnershipChangingEnabled()
.
Copper Language API
The following is a list of functions built into the virtual machine.
Where "..." appears, these functions can accept any number of arguments.
ret( function_return_value )
Terminates the current function, popping it from the stack and returing the function_return_value.
are_available( name )
Accepts any number of strings. Returns true if each of those strings is the name of an available built-in or foreign function.
are_same( ... )
Returns true
if all of the given arguments point to the same function.
member( object, member_name )
a.b = 10
c ~ member(a "b")
print(c:) # Prints 10 #
Returns a pointer to the member of the object-function object whose name is member_name. If the member does not exist, it will be created.
member_count( object )
Returns the number of members in the object function object.
is_member( object, member_name )
Returns true
if member_name is the name of a member in the object-function object.
set_member( object, member_name, value )
Sets the member of object-function object named member_name to value.
member_list( object ... )
Returns as a list the names of all of the members of the given object-functions.
union( ... )
Returns a new object-function whose persistent scope is populated by copies of the persistent scopes off all of the given object-functions.
type_of( arg )
Returns the type of the arg as a type_value object.
are_same_type( ... )
Returns true
if all of the given arguments have the same type.
are_type( type_value ... )
Accepts a type_value and any number of other objects.
Returns true
if all of successive arguments are of the type given by the first type_value.
equal_type_value( type_value [type_value ...] )
Accepts only type_value objects.
Returns true
if all of the type_value objects have the same value.
typename_of( object )
Returns the typename of the given object as a string.
have_same_typename( object ... )
Returns true
if all of the given objects have the same typename.
ret_type( fn ) (implementation-dependent)
If the given function has a constant return, this function will return the type-value of the object to be returned.
a = 10
b = {ret(3)}
Constant-return functions are those whose return is immediately knowable upon their creation. They are of two types: 1) created from direct assignment, 2) created with return of static data. See sidebar code.
Only the former type is supported in the original virtual machine. The latter will always return "fn".
b = { ++(this.c:) ret(5) }
# The following saves the return type to "a" #
a = ret_type(b)
# The following not only saves the type to "a" but increments "b.c" #
a = type_of(b:)
This system function avoids the need to call the given function explicitly, which may trigger chains of function calls. For example, see sidebar code.
Due to its dependence on the underlying architecture of the virtual machine and its ignoring of the rule that variables merely contain functions, this function is not considered standard.
are_fn( ... )
Returns true
if all of the given arguments are of type object-function.
are_empty( ... )
Returns true
if all of the given arguments are both of type object-function and have neither members in their persistent scope nor execution bodies.
are_bool( ... )
Returns true
if all of the given arguments are of type boolean.
are_string( ... )
Returns true
if all of the given arguments are of type string.
are_list( ... )
Returns true
if all of the given arguments are of type list.
are_number( ... )
Returns true
if all of the given arguments are of a numeric type (integer or number with decimal).
are_int( ... )
Returns true
if all of the given arguments are of type integer.
are_dcml( ... )
Returns true
if all of the given arguments are of type number with decimal.
assert( condition )
Throws an error if condition does not resolve to true
.
copy_of( arg )
# The function belonging to local variable "b" dies when the function ends because it is bound to "b", but an unowned copy can live after the return from "a". #
a = {
b = { print(10) }
ret(copy_of(b))
}
c = a:
# Copying is necessary for lists to own the function given to them. #
a = { print(10) }
b = list(a) # List contains pointer to "a" #
c = list(copy_of(a)) # List contains independent copy of "a" #
Returns an independent copy of the given argument.
xwsv( super_variable, call_variable, call_args ... )
a.c = 5
a.b = { print(super.c:) }
a.b: # Prints 5 #
d.c = 10
xwsv(d a.b) # Prints 10 #
"eXecute With Super Variable"
Calls call_variable with the extra arguments and sets the "super" variable to super_variable. The "super" variable is by default the variable whose member is the variable whose function is being run. In the example function call expression a.b(), "a" is the parent that the "super" variable is defaulted to being.
share_body( fn, fn )
Causes the second object-function to inherit the body of the first object-function. This allows object-functions to retain their members and members' values.
realize( type_value ) / realize( object )
If given a type-value object, it returns an instance of the object represented by the given type value. If given any other type of object, it creates and returns an instance of the same type of object. Upon failure to create the desired type, it returns an empty function.
new( type_name )
Returns an instance of the object whose type name is given. Upon failure to create that type, it returns an empty function.
not( boolean_arg )
Returns the opposite boolean value of boolean_arg.
all( boolean_arg ... )
Returns true
if all of the given arguments are boolean true
.
any( boolean_arg ... )
Returns true
if any of the given arguments are boolean true
.
nall( boolean_arg ... )
Returns true
only if all of the given arguments are boolean false
.
none( boolean_arg ... )
Returns true
if none of the given arguments are boolean true
.
xall( boolean_arg ... )
Returns true
only if all of the arguments are of the same boolean value (that is, if all of them are true
or all of them are false
.
list( ... )
Creates and returns a list object made from the given arguments.
length( list_object )
Returns the length of the list list_object.
append( list_object, item )
Appends item to the list list_object.
prepend( list_object, item )
Prepends item to the list list_object.
insert( list_object, index, item )
Inserts item into the list list_object at the index index. Both integers and numbers with decimal are accepted as indexes.
item_at( list_object, index )
Returns a pointer to the object in list list_object at the index index. Element indexes start at zero.
erase( list_object, index )
Removes a element from the list list_object at the index index.
dump( list_object )
Removes all of the elements in the list list_object.
swap( list_object, index_1, index_2 )
Swaps the locations of two elements in the list list_object at the indexes index_1 and index_2.
replace( list_object, index, item )
Replaces the element at the index index in the list list_object with item.
sublist( list_object [, start_index [, end_index]] )
Returns the sub-list of the list list_object. Optionally, start_index can be given. If given, optionally end_index can be given, where end_index is the last excluded index.
matching( string_arg ... )
Returns true
if all of the given arguments are matching strings.
concat( ... )
Returns a string resulting from the concetenation of the given arguments. The Cu::Object::writeToString()
method is employed for obtaining a string value from every argument.
equal( number ... )
Returns true if all of the given numbers are equal to the first argument.
gt( number ... )
Returns true if all of the given numbers are greater than the first argument.
gte( number ... )
Returns true if all of the given numbers are greater than or equal to the first argument.
lt( number ... )
Returns true if all of the given numbers are less than the first argument.
lte( number ... )
Returns true if all of the given numbers are less than or equal to the first argument.
abs( number )
Returns the absolute value of the given argument.
+( number ... ) , add( number ... )
Returns the sum of the given numbers. The name of this function depends on if the extended nameset flag is enabled during compilation.
-( number ... ) , sbtr( number ... )
Returns the result of subtracting from the first argument all of the remaining given numbers. The name of this function depends on if the extended nameset flag is enabled during compilation.
*( number ... ) , mult( number ... )
Returns the product of all of the given numbers. The name of this function depends on if the extended nameset flag is enabled during compilation.
/( number ... ) , divd( number ... )
Returns the result of dividing the first argument by all of the remaining given numbers. The name of this function depends on if the extended nameset flag is enabled during compilation.
%( number ... ) , mod( number ... )
Returns the result of the modulus of the first argument by all of the remaining given numbers. The name of this function depends on if the extended nameset flag is enabled during compilation.
++( number [, step_size] ) , incr( number [, step_size] )
Increments the given number by 1 or by the step_size if that is given. The name of this function depends on if the extended nameset flag is enabled during compilation.
--( number [, step_size] ) , decr( number [, step_size] )
Decrements the given number by 1 or by the step_size if that is given. The name of this function depends on if the extended nameset flag is enabled during compilation.
Debugging Copper
The engine comes built-in with a number of error messages for debugging Copper code. When stack-trace printing is enabled, both the current stack and the queued operations ("Task Stack") are printed. Unfortunately, because object-functions are nameless, it can be difficult to figure out exactly what caused the problem, so you will need to examine both the stack trace and the printing of the queue operations.
Copper in 5 Minutes
# Comments are between \#'s and are multiline
but a slash escapes characters. #
# Variables have stack-based lifetimes and only one type: object-function.
Declaring a name makes a variable. #
a
# Assignment (copies data and members) #
a = 5
# Getting what's stored is done by calling the function in the variable. #
a()
# Pointers (left hand side becomes pointer to right hand side) #
b ~ a
# The included standard library has a print() function. Sorry, no built-in. #
print( b() ) # Prints 5 #
# Lost pointers cause no memory errors or leaks. A new function is created instead. #
a = 1
print( b() ) # Prints "{fn}". #
# Strings are between double-quotes only. #
c = "hello world"
# Members are automatically created upon usage. #
a.child = "woot"
# Shortcut zero-argument function calls using ":" #
print( a.child: )
# Getting and setting members can be done using their names. #
name = "five" set_member(d name: 5)
print( type_of( member(d name:) ) ) # Prints "number" #
# Creating objects with members... #
e = [ my_member = 0 other_member = 1 ]
# Create functions using {} #
f = {
x = false
# if structures and loops require brackets #
loop {
# not() takes only 1 parameter,
but all(), any(), nall(), and none()
can take any number of parameters. #
if ( not(x:) ) {
x = true
skip # Restart the loop #
# An elif could go here too #
} else {
stop # Escape the loop #
}
}
}
# Object-funtions have members and a body of executable code. #
g = [ a b=2 ] {
# Access members inside a function via "this" pointer #
ret( [a=a, b=this.b] )
}
# Commas are optional for separating expressions,
parameters, or arguments. #
h = g(3), print( h.a:, " ", h.b: ) # Prints "3 2" #
# The parent of the variable whose function is called
can be accessed with "super" #
i = {
super.kid = 2
}
# ... and it doesn't matter who that parent is... #
j.a = i
j.a:
print(j.kid:) # Prints 2 #
# Combine object member sets with union #
k = union(h j [b=1])
print( k.a:, " ", k.kid:, " ", k.b:) # Prints "3 2 1" #
# Creating and accessing lists... #
l = list(1 2 [p]{ print("p == ", p:) })
append(l: 4)
prepend(l: 0)
insert(l: 2 "arf")
m ~ item_at(l: 4)
m("hi")
erase(l: 0)
swap(l: 1 2)
replace(l: 0 "front")
# Function pointers are saved in lists... #
n = { print("hey") }
o = list(n)
p ~ item_at(o: 0)
p: # Prints "hey" #
n = {}
p: # Prints warning of empty function container #
# ... unless copied #
n = { print("hey") }
o = list( copy_of(n) )
p ~ item_at(o: 0)
n = {}
p: # Prints "hey" #
# Sub-lists have to be made from valid indexes
that map to the range 0 to list size. #
q = sublist(o: 0 length(o:))
# Sub-lists are linked to the items in the original object. #
r ~ item_at(q: 0)
r: # Prints "hey" #
o = {}
r: # Prints warning of empty function container #
# Strings can be checked for matching value and concatenated #
if ( matching("fn", typename_of(s:)) ) {
print( concat( "some ", "string " ) )
}
See the side bar. Make sure to have the "copper" tab active.
Note: A centered-text version is available in the old documentation.
The Virtual Machine
For information about how to incorporate the virtual machine in a project, see the next section.
If you wish to modify the virtual machine, you will want to read about how it works.
Getting Setup
The virtual machine is composed of a few files located in the Copper/src directory. These files can be dropped right into your project. They are:
- Copper.h
- Copper.cpp
- RHHash.h
- Strings.h
- Strings.cpp
- utilList.h
To use the virtual machine, include Copper.h in the C++ file where you with to use Copper.
The main components of the virtual machine are in the namespace Cu
. The string, list, and robin-hood hash table classes are in the namespace util
.
#include <Copper/src/Copper.h>
#include <Copper/stdlib/Printer.h>
#include <Copper/stdlib/InStreamLogger.h>
#include <exts/Math/basicmath.h>
#include <exts/Time/systime.h>
int main()
Cu::Engine engine;
// Example incorporation of extras
CuStd::Printer printer;
CuStd::InStreamLogger streamLogger; // dual-role class
engine.setLogger(&streamLogger);
engine.addForeignFunction(util::String("print"), &printer);
// Example of setting engine feature
engine.setStackTracePrintingEnabled(true);
// Quick incorporation of offered extensions
Cu::Numeric::addFunctionsToEngine(engine);
Cu::Time::addFunctionsToEngine(engine);
// Running the engine
Cu::EngineResult::Value result;
do {
result = engine.run( streamLogger );
} while ( result == Cu::EngineResult::Ok );
return 0;
}
Within your code, create an instance of the Engine. The Engine is the main component of the virtual machine. It performs all of the parsing and execution of operations. The Engine can be started with the function run()
, which returns a value of the enum Cu::EngineResult::Value
. A return value of Cu::EngineResult::Ok
means processing is proceeding normally and more input can be given. A return value of Cu::Engine::Done
means that the engine is finished (either because it encountered the end of the input stream or an exit token) and all but the global stack has been deleted. A return value of Cu::Engine::Error
means an error occured and all but the global stack has been deleted.
The Engine run
function must be provided with an input source of bytes. All input sources must inherit from Cu::ByteStream - an interface with two virtual methods that must be implemented. These are:
char getNextByte()
- Returns the next byte in the stream of bytes provided by this class. Any and every byte is accepted.bool atEOS()
- Returns true if the end of the stream of bytes has been reached.
The Engine has a built-in filter for determining if certain sets of bytes compose valid names, but a custom filter can be added using the Engine method setNameFilter()
. See the API for more information.
An instance of an engine isn't useful on its own. You will need to add foreign functions to it. Some have been provided. See the provided extensions section for more information.
It is possible to add your own custom objects to the virtual machine. This can be done by either returning them from a foreign function you implement or via your implementation of Cu::CustomObjectFactory
given to the engine via Engine::setCustomObjectFactory
. See Custom Object Factory.
Compiling the Virtual Machine
If you wish to test the virtual machine, a premake5 project file has been provided. Premake can generate both GCC make files and Visual Studio project files.
Premake can be downloaded from here.
Messages: Info, Warnings, Errors
The virtual machine produces four types of messages: Info, Warning, Error, and Debug.
Information messages provide general information about the state of the virtual machine.
Warning messages indicate abnormal activity found in the code. Such activity can result in unintended consequences, and thus warnings should never be ignored. However, the virtual machine will proceed to run and not crash.
Error messages indicate a corrupted and possibly fatal state of the engine. Syntax errors are considered fatal. For why Copper has no exceptions system, see the FAQ. Foreign functions may also throw errors, which may be non-fatal but would likely result in undesirable consequences.
The virtual machine may also throw errors identified as "system errors", in which case the virtual machine code has a bug. If you encounter this, please report it by opening a ticket on the project's Github bug page and providing the necessary info to reproduce the bug.
Debug messages are those that provide information useful for debugging the engine itself. They can only be enabled with certain compiler flags near the top of Copper.h.
Foreign Functions
Adding foreign functions to the virtual machine requires creating or using some class that descends from class Cu::ForeignFunc
and implements the method virtual ForeignFunc::Result call( FFIServices& )
. The method call()
must return ForeignFunc::FINISHED
if there is no error. For errors, it can return ForeignFunc::NONFATAL
or ForeignFunc::FATAL
. Finally, the foreign function can end the virtual machine processing by returning ForeignFunc::EXIT
. FFIServices
is an interface providing useful methods for interacting with the engine. These methods can be found in the API.
Once given to the engine, the ForeignFunc classes will have their reference-counts incremented until the destruction of the engine.
A number of helper classes have been provided for both adding foreign functions (both standard functions and class methods) without the need to create the wrapper yourself. These are given in the following table.
class MyFF : public Cu::ForeignFunc {
public:
virtual Result call( FFIServices& ffi ) {
ffi.setNewResult( new BoolObject(true) );
return FINISHED;
}
};
// Convenient ways of adding it to an instance of Engine.
Engine engine;
// Option 1:
addForeignFuncInstance<MyFF>(engine, String("myff"));
// Option 2:
addNewForeignFunc(engine, String("myff"), new MyFF());
// Option 3:
MyFF* myff = new MyFF();
addForeignMethodInstance(engine, String("myff"), myff, myff::call);
Function | Description |
---|---|
addForeignFuncInstance<T>( Engine&, const String& ): void | Adds a ForeignFunc descendent instance to the engine using the given string as its address/name. |
addNewForeignFunc( Engine&, pName: String&, ForeignFunc* ): void | Adds a ForeignFunc descendent to the engine and dereferences it. It assumes you are passing it a new object allocated on the heap. |
addForeignFuncInstance( Engine&, pName: const String&, ForeignFunc::Result (*pFunction)( FFIServices& ) ): void | Adds a function with the call(FFIServices&):ForeignFunc::Result prototype to the engine as a foreign function. |
addForeignMethodInstance( pEngine: Engine&, pName: const String&, pBase: BaseClass*, bool (BaseClass::*pMethod)( FFIServices& ) ): void | Adds a method from an instance of BaseClass to the engine as a foreign function. |
Callbacks
Copper allows for callbacks that can be run after the engine finishes normal execution. Functions will be inside of FunctionObjects when passed to foreign functions. There is no need to extract the function. However, since objects in Copper have limited lifetimes, you will need to either copy the function to a new FunctionObject or call the method FunctionObject::changeOwnerTo()
and provide a new owner (whose lifetime you control). Copying is simpler.
Foreign Objects
In implementation, all objects in the Copper language are C++ objects that extend the class Cu::Object. Anything extending this class can be used as an object. However, a few things are required.
- All objects must set their object type upon instantiation by passing it to the parent Cu::Object constructor. This type is of the enumeration ObjectType::Value. You may cast other values to this enumeration and make your own extended enum starting with the value
Cu::ObjectType::UnknownData + 1
. - All objects must implement the following virtual methods:
- copy(): Object*
Optionally, objects may have an implementation of virtual void writeToString(String& out)
to set the data to be returned when the object appears in string-requiring contexts.
Optionally, objects may have an implementation of virtual const char* typeName() const
for better printing information and type comparison via have_same_typename()
.
Foreign objects intended to work as numbers should inherit the NumericObject class and implement its virtual methods. Objects inheriting this type must implement virtual bool supportsInterface( ObjectType::Value )
and return true
for ObjectType::Numeric
.
Complex Type Support
Sometimes objects inherit multiple interfaces and all are Copper Objects. Rather that invent new types that encompass and indicate certain mixed interfaces, Copper provides two virtual methods to enable multiple interface support. Either method can be optionally overridden to suit the need.
- bool isSameData( Object* )
- bool supportsInterface( ObjectType::Value )
isSameData()
returns true when the data for both objects is the same. For example, it may return true when two numeric types use the same representation and have the same value. It could also be used for checking if pointer addresses to data are the same. The primary usage of this method is for equality but is not the same as numeric equality.
supportsInterface()
returns true if the object can be safely casted to the class type represented by the given ObjectType::Value
value.
Custom Object Factory
It is possible to add custom objects to the virtual machine by directly returning them from a foreign function. However, the engine is unaware of the ability to create them.
CustomObjectFactory
is an abstract class that, if given to the engine, will be used for creating instances of objects via the system functions realize()
and new()
. A single instance of the factory class can be given to the engine via Engine::setCustomObjectFactory( CustomObjectFactory )
. Note that the engine will not reference-count this class, so it must not be deleted before the engine is done using it.
There are two virtual methods, both of which must return a singly-reference-counted object or REAL_NULL.
- virtual Object* constructFromType( ObjectType::Value )
- virtual Object* constructFromName( const String& )
How these methods interpret their given arguments is up to the user, but to establish convention, UserTypeStart
from ObjectType::Value
ought to be the basis for your user-defined object type-values. Moreover, the string name ought to be (the return) from a static member function of the class of the object being instanced.
Provided Extensions
There are two sets of extensions. The first set is in the Copper/stdlib directory. This set contains files whose primary purpose is basic input and printing to allow for debugging of the engine. The second set is in the exts directory. It contains all other extensions, especially those for time and math operations.
stdlib Directory Extensions
Class | Header File | Description |
---|---|---|
InStreamLogger | InStreamLogger.h | Class for using a File object (defaulted to stdin) as the input for an instance of the engine. It requires EngMsgToStr. |
EngMsgToStr | EngMsgToStr.h | Class for converting engine error flags to English messages. |
Printer | Printer.h | Class for printing the writeToString values of Cu::Object descendents to string. |
FileInStream | FileInStream.h | Class for opening a file from a given string and feeding it to Engine::run() as a ByteStream. |
StringInStream | StringInStream.h | Class for feeding a given string to Engine::run() as a ByteStream. |
exts Directory Extensions
Some extensions have an addFunctionsToEngine(Cu::Engine&)
. This function will add all of the foreign functions associated with the extension to the given Cu::Engine instance using default names. Currently, the extensions with this feature are:
- System/cu_info
- Math/cu_basicmath
- Time/cu_systime
- String/cu_stringmap
- String/cu_charstring
The default names of functions provided by these extensions are given in the following table.
Extension | Default Function Names |
---|---|
cu_info | sys_lang_version sys_vm_version sys_vm_branch |
cu_basicmath | pow floor ceil sin cos tan |
cu_systime | get_time get_seconds get_milliseconds get_nanoseconds |
cu_stringmap | string_map |
cu_stringbasics | str_overwrite str_byte_length str_byte_at |
t = get_time:
print( "nanoseconds = ", get_nanoseconds(t:)
With the exception of get_time
, the systime functions are designed to extract information from the data returned from get_time
. Internally, get_time
uses clock_gettime
.
API
Types
Alias | Type | File |
---|---|---|
Cu::UInteger | typedef unsigned int | Copper.h |
Cu::Integer | typedef long int | Copper.h |
Cu::Decimal | typedef double | Copper.h |
uint | define unsigned int | RHHash.h |
util::List
File: utilList.h
Basic doubly-linked list used internally by the virtual machine.
util::List::Iter
Iterator class for the utility list.
Member | Description |
---|---|
Iter( List& ) | Constructor |
Iter( const Iter& ) | Copy Constructor |
operator*(): T& | Returns the address of the data. |
getItem(): T& | Returns the address of the data. |
operator->(): T* | Returns a pointer to the data. |
setItem( pItem: T ): void | Sets the data. |
operator==( const Iter& ): bool | Checks to see if the given iterators point to the same node. |
operator=( Iter ): Iter& | Shared assignment. This and given iterator now point to the same node. |
set( Iter ): void | Shared assignment. This and given iterator now point to the same node. |
prev(): bool | Moves iterator to the previous node, returning true if the move was made. |
next(): bool | Moves iterator to the following node, returning true if the move was made. |
peek(): T& | Returns the address of the node following the node where this iterator is located. (UNSAFE) |
reset(): void | Sets this iterator to the first node in the list. |
makeLast(): void | Sets this iterator to the last node in the list. |
atStart(): bool | Returns true if this iterator is at the first node in the list. |
atEnd(): bool | Returns true if this iterator is at the last node in the list. |
has(): bool | Returns true if there is at least one element in the list this iterator uses. |
insertBefore( pItem: const T& ): void | Inserts the given item into a new node prior to the one pointed to by this iterator. |
insertAfter( pItem: const T& ): void | Inserts the given item into a new node following the one pointed to by this iterator. |
destroy(): void | Deletes the node pointed to by this iterator. Iterated is invalidated. |
util::List::ConstIter
Const version of Iter for situations where the data in the list must be immutable. All methods are the same with the exception of Iter::destroy(), for which there is no replicate in this class.
util::CharList
File: Strings.h
String-builder class used internally by the virtual machine.
Member | Description |
---|---|
CharList() | Constructor |
explicit CharList( const char* ) | Constructor |
CharList( pString: const char*, pLength: uint ) | Constructor |
CharList( pString: const String& ) | Constructor |
~CharList() | Deconstructor |
operator= ( const CharList& ): CharList& | Assignment |
append( const CharList& ): CharList& | Appends the given list onto this one. |
append( const String& ): CharList& | Appends the given string onto this one. |
equals( const char* pString ): bool | Checks to see if the list and C-style string contain exactly the same characters. |
equals( const CharList& pOther ): bool | Checks to see if the two lists contain exactly the same characters. |
equalsIgnoreCase( const CharList& pOther ): bool | Checks to see if the two lists contain the same letters. |
appendULong( const unsigned long pValue ): void | Converts the unsigned long value to characters and appends it to the list. |
util::String
File: Strings.h
Basic string class used internally by the virtual machine.
util::RHHash<T>
File: RHHash.h
Robin-Hood Hash Table implementation.
Member | Description |
---|---|
RHHash( uint ) | Constructor |
RobinHoodHash( const RobinHoodHash<T&>& ) | Copy constructor |
~RobinHoodHash() | Destructor |
appendCopyOf( RobinHoodHash<T>& ): void | Copies the given hash table to this one. |
get( pIndex: uint ): Bucket* | Returns the bucket at the given index. |
getBucketData( pName: const String& ): BucketData* | Returns the data stored in the given bucket. |
insert( pName: const String&, pItem: T ): T* | Inserts the item into the table with the given name. |
getSize(): uint | Returns the allocated size in memory of the table. |
getOccupancy(): uint | Returns the number of elements in the table. |
erase( pName: const String& ): void | Removes an element from the table. |
util::RHHash::BucketData<T>
File: RHHash.h
Member | Description |
---|---|
name: const String | Bucket name |
item: T | Element data |
delay: uint | Number slots the bucket is located away from the slot its name's hash value gives |
BucketData( pName: const String&, pItem: const T& ) | Constructor |
BucketData( pOther: const BucketData& ) | Copy Constructor |
util::RHHash::Bucket<T>
File: RHHash.h
Member | Description |
---|---|
data: BucketData* | Pointer to data |
wasOccupied: bool | Indicates if this data had been full once before |
Bucket() | Constructor |
~Bucket() | Deconstructor |
clear(): void | Data removal |
Cu::Ref
Reference-counted objects. Used as a base for other objects.
Member | Description |
---|---|
ref(): void | Increments the reference count. |
deref(): void | Decrements the reference count. |
getRefCount(): int | Returns the reference count. |
Cu::RefPtr<T>
Reference-counting pointer for objects descending from Cu::Ref
.
Member | Description |
---|---|
RefPtr() | Constructor |
RefPtr( const RefPtr& ) | Copy Constructor |
~RefPtr() | Deconstructor |
set( pObject: T* ): void | Sets the pointer. |
setWithoutRef( pObject: T* ): void | Sets the pointer and calls deref() without calling ref(). |
operator= ( pOther: const RefPtr |
Shared assignment. |
obtain( pStorage: T*& ): bool | Sets the given pointer with the object stored here and returns true if there is an object. |
raw(): T* | Directly returns the address stored here (unsafe). |
Cu::Object
Base class for all objects (object-functions and data) in Copper.
Member | Description |
---|---|
Object( ObjectType::Value ) | Constructor |
virtual ~Object() | Destructor |
getType(): ObjectType::Value | Returns the object type (Function, Bool, String, List, Integer, Decimal, or UnknownData). |
operator Object*() | Implicit cast. |
virtual copy(): Object* | Meant to be overridden. Returns a copy of this object. |
virtual writeToString( out: String& ): void | Meant to be overridden. Sets the given string to a string representation of this object's value. |
virtual typeName(): const char* | Meant to be overridden. Returns a C-style string name of this object's type. |
Cu::Object Descendents
Class Name | Description |
---|---|
FunctionObject | Object-function container component. This contains Function, the class containing all of the function data. |
BoolObject | Basic boolean class. |
StringObject | Basic string class wrapping util::String. |
NumericObject | Parent class for all number types. |
IntegerObject | Basic integer class wrapping Cu::Integer. (Descendant of NumericObject.) |
DecimalNumObject | Basic decimal class wrapping Cu::Decimal. (Descendant of NumericObject.) |
ListObject | Doubly-linked list class with a special Node subclass for owning FunctionObject. |
ObjectTypeObject | A wrapper class for ObjectType::Value values. |
Cu::LogMessage
Struct for passing logging information to the logger. All engine messages are passed as LogMessage instances via Logger::print( LogMessage& )
. The set fields depend on the messadeId
.
Member | Type | Description |
---|---|---|
level | LogLevel::Value | Category of the message ( info , warning , error , or debug ). |
messageId | EngineMessage::Value | Internal message ID associated with certain strings. Use getStringFromEngineMessage() in EngMsgToStr.h to get the text. |
functionName | String | Name of the foreign function in which the message originated. |
systemFunctionId | SystemFunction::Value | If the message originated from a built-in function, this will be set. |
argIndex | UInteger | Index of the argument causing the message if applicable. |
argCount | UInteger | Number of arguments given to the function. |
givenArgType | ObjectType::Value | The type of the object given as the argument. |
expectedArgType | ObjectType::Value | The expected type of object the argument was supposed to be. |
givenArgTypeName | String | The name of the type of the object given as the argument. |
expectedArgTypeName | String | The name of the expected type of the object the argument was supposed to be. |
customCode | UInteger | Use this for your own purposes. If using this, set messageId to EngineMessage::CustomMessage . |
Cu::ForeignFunc
Ancestor class to be inherited by all classes that need to interact with the virtual machine.
Member | Description |
---|---|
virtual call( FFIServices& ): ForeignFunc::Result | Meant to be implemented. Called by the Engine. Returns ForeignFunc::FINISHED when complete. |
Cu::FFIServices
Interface for foreign functions to interact with the engine.
Member | Description |
---|---|
FFIServices( Engine&, ArgsList, String ) | Constructor (DO NOT USE) |
~FFIServices() | Destructor (DO NOT USE) |
getArgCount() const: UInteger | Returns the number of arguments sent to the function. |
demandArgCount( UInteger, bool ): bool | Returns true if the number of arguments matches the given value. |
demandArgCountRange( UInteger, UInteger, bool ): bool | Returns true if the number of given arguments is within the given range. |
demandMinArgCount( UInteger, bool ): bool | Returns true if the number of given arguments is at least equal to the given value. |
demandArgType( UInteger, ObjectType::Value ): bool | Returns true if the argument at the given index was of the given type. |
demandAllArgsType( ObjectType::Value, bool ): bool | Returns true if all of the given arguments are of the given type. |
arg( UInteger ): Object& | Returns a pointer to the argument at the given index. |
printInfo( message: const char* ): void | Print information to the logger. |
printWarning( message: const char* ): void | Print a warning message to the logger. |
printError( message: const char* ): void | Print an error message to the logger. |
printCustomInfoCode( UInteger ): void | Prints a custom information code to the logger. |
printCustomWarningCode( UInteger ): void | Prints a custom information code to the logger. |
printCustomErrorCode( UInteger ): void | Prints a custom information code to the logger. |
setResult( Object* ): void | Sets the foreign function return. (Unset, it will be an empty function.) |
setNewResult( Object* ): void | Sets the foreign function return and dereferences it. (Same purpose as setResult() .) |
Cu::Engine
Main interpreter component; parser, lexer, processor.
Member | Description |
---|---|
Engine() | Constructor |
setLogger( Logger* ): void | Sets the interface used for printing engine messages such as errors. |
print( logLevel: const LogLevel::Value, msg: const char* ): void | Prints a message to the logger. |
print( logLevel: const LogLevel::Value, msg: const EngineMessage::Value ): void | Prints a message to the logger. |
setEndofProcessingCallback( callback: EngineEndProcCallback* ): void | Sets the interface used for responding when the processing has ended. |
setIgnoreBadForeignFunctionCalls( yes: bool ): void | Sets whether to allow bad calls to foreign functions to proceed by returning empty function rather than terminating processing with an error. |
setOwnershipChangingEnabled( yes: bool ): void | Sets whether to allow the own structure to work. |
setStackTracePrintingEnabled( yes: bool ): void | Sets whether to show the stack trace upon error. |
setNameFilter( bool(*filter)(const String&) ): void | Sets the function used for filtering the characters permitted in names. |
addForeignFunction( pName: const String&, pFunction: ForeignFunc* ): void | Adds a foreign function for use in Copper. |
run( ByteStream& ): EngineResult::Value | Initiates processing in the virtual machine. |
clearGlobals(): void | Clears all global variables. (Globals are not clear when processing completes, so this method is provided.) |
parse( context: ParserContext&, srcDone: bool ): ParseResult::Value | Proceeds parsing with the given context. (You should not need to use this function directly.) |
execute(): EngineResult::Value | Continues processing in the virtual machine. (You should not need to use this function directly.) |
runFunctionObject( FunctionObject* ): EngineResult::Value | Executes the body of code of the given object-function. (Used for callbacks.) |
How the Virtual Machine Works
First and foremost, most objects in the virtual machine are reference-counted using the dedicated classes Ref
and RefPtr
. The class RefPtr
has two methods that must be used for setting it: set()
and setWithoutRef()
. The former should be used when the data is shared. The latter does not increment the reference counting; it is intended for directly passing newly-created heap objects to the pointer.
The Copper virtual machine Engine class is the main component and the bulk of the virtual machine. To run the virtual machine, a bytestream is passed to the method Engine::run(). The engine will then attempt to exhaust the byte source and convert its contents into tokens. Standard ASCII whitespace (simple space, newlines, tab) is treated as a deliminator. When the engine encounters a set of bytes that it cannot lex, it considers it a name and checks it for validity. Optionally, a name-filter can be given to the engine, and this filter will be passed the strand of unknown bytes as a string.
Once tokenized, the engine proceeds to interpret the strand of tokens using a state machine. When an operation is found to be incomplete, the machine checks for more input. If more input is available (as is the case with console input), the state machine will wait. If not, the machine will terminate in error.
The parsing aspect of the machine has been made publically available for the sake of nested code bodies. The interpreter will not parse code bodies until they are activated as part of a function call. However, lexing occurs immediately upon received input, so invalid tokens are identified immediately upon entry into the virtual machine.
The parsing system is a large state-machine. Rather than waiting for a syntactically-complete input and possibly using a regex, the parsing machine accepts any input and simply tracks its progress through parsing by using "parse tasks". The reason for this system is partially due to the grammar of the language; no explicit statement termination token means interrupted console input should (in many cases) default to termination.
The final result of parsing is a strand of "Opcode" instances that contain both instructions and some of the data necessary to complete those instructions. Opcodes can contain the addresses of variables, bodies of code, simple data (numbers and strings), or pointers to other parts of the strand (for use in if-structures and loops).
The addresses of variables are special structures that contain lists of byte strings, which are the components of the address. An address represents the full call path of the object they represent from within the scope where they are from. Built-in functions, foreign functions, and user-created functions are all represented by variable addresses.
Bodies of code, as represented in both instances of Function and in instances of Opcode, are containers for both a list of unparsed tokens and a list of Opcode instances. The former is converted to the latter via an Engine::parse() call that utilizes its parser context. Bodies of code are shared by instances of Function and remain constant after their conversion.
The engine makes extensive use of the class FunctionObject. The class Function holds the persistent scope of the object-function as well as the body of executable code, but the class FunctionObject acts as the "Copper object" wrapper for Function. FunctionObjects can be "owned" by a class inheriting from class Owner. Once owned, the lifetime of the Function class inside of FunctionObject is tied to lifetime of the class inheriting Owner.
Variables are one of the descendents of class Owner.
When processing the opcodes, the engine first checks the type and the attempts to access the associated data from inside the opcode.
Some opcodes create tasks inside of the engine when the engine processes them. In some cases, opcodes are dependent on each other and therefore must follow each other in the correct order or they can crash the engine. In some cases, certain opcodes can be run as shortcuts for tasks.
For example, creating an object-function definition requires opcodes for initiating the task - either starting from the object body component or from the execution body component - and terminating the building. Among the object body component opcodes, there is an opcode for adding names to the list of parameters to the function as well as an opcode for adding names to the persistent scope. The object body component opcodes are all optional.
Function calls in the Engine can only be made when appended to addresses. The address for the call is first checked against a list of built-in function names. If it matches, the built-in function is called. If not, foreign function names are checked for a match. If the address matches a foreign function, the arguments are checked to be sure they match the required parameters of the foreign function.
If no match is found among foreign functions, it is assumed to be a user function. The engine first searches the local scope for a variable whose existence matches the full address. If no match is found, the global scope is searched. If the base variable of the path does not exist in either scope, all of the members of its address are created in the local scope.
The engine transmits Copper data from place to place using the RefPtr lastObject
. This special pointer is set by almost everything from function calls to data creation. Even the foreign function interface (FFIServices) methods set this object. Once set, this data can be used in variable assignments and pointer assignments both inside and outside function building.
The engine uses a number of enumerations for its processes. Most of these enums are struct enums, meaning that they are enumerations that are each wrapped in struct. A small number are regular enums and not meant to be used or invoked outside of the engine.
The engine uses a set of flags (from an enumeration) for identifying current state of processing and tell the main processing loop whether or not it should proceed. These have an enormous effect on the state of the engine, so care should be used in passing them. For example, the main engine loop accepts return flags that exit the engine (when the exit
token is found), pop the opcode operations stack (which occurs at the end of a user-function call), continue processing, and throw an error (which destroys most of the stack).
Debugging
The virtual machine is equipped with a number of features for debugging it. At the top of the Copper.h file are a set of enumerations that will trigger the sending of messages to the logger. These messages reveal the function calls inside particular components of the virtual machine. Some of these components are isolated and cannot print to the logger. These require a std::printf() be available, so to use them, uncomment the #include <cstdio>
near the top of the Copper.h file.
Speed Profiling
Speed profiling code has been made available for testing certain components in the engine. However, if you wish to check the overall speed of Copper, it is recommended that you use the Time/systime extension.
FAQ
What is the parse tree?
The parse tree is available in an article on the official blog.
What numeric operations are available?
Only basic operations are available: absolute value, addition, subtraction, multiplication, division, increment/increase, decrement/decrease, and the comparisons of equality, less than, less than or equal, greater than, and greater than or equal. The NumericObject parent class allows you to implement these operations for descendent classes so you can create wrappers for other math classes such as GNU MP.
Why is there no privacy model?
Because it would be absurd.
See this blog post for a more detailed answer.
Why is there no exceptions system?
They generally aren't needed (except in tragic cases where they can't be handled). If an error occurs, in most cases an empty function is returned, and the system function are_empty()
can be used to check this. To halt the program, you can use the system function assert()
. The engine can also be set to halt (rather than warn) when foreign functions fail.
Why have own
if it's unsafe?
Some programmers might prefer a model whereby they use a designated constructor function for producing objects before passing them off to the rest of the program. However, certain ways of doing this require changing ownership to prevent the destruction of those objects when the stack frame closes. This practice, when done correctly, is still memory-safe.
Technically, any program can be written in such a way that avoids this practice, but rather than force programmers into a particular model, the virtual machine leaves it up to user discretion.
By default, the feature is disabled and can only be enabled by passing true
to Engine::setOwnershipChangingEnabled()
.
Benchmarks? How fast is Copper?
Tests run on an HP Pavilion AMD-64 Dual-core rated at 2.1~2.3GHz using the Time/sysclock and Math/basicmath extensions.
Primes Less Than 5000
Primes Less Than 5000
# Primes Less Than 5000 #
isprime = [n] {
i = 2
loop {
if ( gte(i: n:) ) { stop }
if ( equal(%(n: i:) 0) ) { ret(false) }
++(i:)
}
ret(true)
}
primes = [n] {
count = 0
i = 2
loop {
if ( gte(i: n:) ) { stop }
if ( isprime(i) ) {
++(count:)
}
++(i:)
}
ret(count:)
}
benchmark = {
st = get_time:
print("primes: " primes(5000))
et = get_time:
st_ms = get_milliseconds(st:)
et_ms = get_milliseconds(et:)
print("\nStart time = " st_ms: "\nEnd time = " et_ms:
"\nTotal time = " -(et_ms: st_ms:))
}
Sample run times are in milliseconds.
Time | Run 1 | Run 2 | Run 3 |
---|---|---|---|
Start | 5 | 16534 | 32879 |
End | 16534 | 32879 | 49199 |
Total | 16529 | 16345 | 16320 |
Roughly 16 seconds per run.
First 1000 Fibonacci Numbers
First 1000 Fibonacci Numbers
# First 1000 Fibonacci Numbers Test #
benchmark = {
st = get_time:
fib_gen = {
ret([a=1. , b=0]{
c = +(this.a: this.b:)
this.b = this.a
this.a = c
ret(this.b:)
})}
f = fib_gen:
i = 0
loop {
if ( equal(i: 1000) ) { stop }
++(i:)
print("fib " i: " = " f: "\n")
}
et = get_time:
print("i == " i: "\n")
st_ms = get_milliseconds(st:)
et_ms = get_milliseconds(et:)
print("\nStart time = " st_ms: "\nEnd time = " et_ms:
"\nTotal time = " -(et_ms: st_ms:) "\n")
}
Sample run times are in milliseconds.
Time | Run 1 | Run 2 | Run 3 | Run 4 |
---|---|---|---|---|
Start | 5 | 108 | 200 | 296 |
End | 108 | 200 | 296 | 401 |
Total | 103 | 92 | 96 | 105 |
Can I do builder notation?
No. Even though returning a pointer from a function is still safe in Copper, the grammar of Copper dictates that function calls end a statement. This allows for relaxed programming because statement-ending tokens are not required.
Can I use it with C?
Sorry, nothing is wrapped in extern "C", but since there are only a small handful of files, it shouldn't be hard for you to do this yourself.
Comparison to AngelScript?
Intepreter Comparison
The following table summarizes the notable differences.
AngelScript | Copper |
---|---|
Module setup required | No modules |
Reads from files | Reads from input class |
Compiles to binary | Uses simple opcodes |
Expects to be on application top level | Can be created anywhere in an application |
Note: File readers are available for Copper in the standard library.
Language Comparison
The following table summarizes the notable differences.
AngelScript | Copper |
---|---|
Variables store multiple types | Variables store 1 type |
Variables directly return their values based on context | A variable's object-function must be called to get its value |
Pointers are part of the variable's type | Pointers are created using pointer-assignment operator |
Permits null but throws exceptions | No null but has assert function |
Comparison to ChaiScript?
Intepreter Comparison
The following table summarizes the notable differences.
ChaiScript | Copper |
---|---|
Requires C++11 | C++ version agnostic (98+) |
C++11 hooks can be created with lambda | Hooks are created via inheritance |
Language Comparison
The following table summarizes the notable differences.
ChaiScript | Copper |
---|---|
Mathematical operations are built-in | Basic operations are built-in, extras in extensions |
Dynamic objects must be created with special constructor | Dynamic objects are default |
Null pointers allowed | No null |
Comparison to Lua?
Lua | Copper |
---|---|
C, requiring wrappers for C++ | C++ |
Direct stack-control | Private, self-managed stacks; FFI provided |
Tables-based variables | Object-oriented |
Whitespace-sensitive | White-space is irrelevant |
Nil allowed locally but nil deletes globals | No null/nil |
Is Copper Thread-Safe?
The Copper interpreter is designed to run on one thread. However, within the interpreter itself, it may be feasible to add multi-threading capabilities in the future.