Copper Interpreter Usage Guide

  1. What The Intepreter Can Do
  2. File Folder Structure
  3. Running the Engine
  4. Extending the Engine
    1. Using the Foreign Function Interface
    2. Creating New Datatypes
    3. Built-in Engine Objects
  5. Callbacks
  6. API

What The Interpreter Can Do

The interpreter only includes essential functionality for handling variables, loops, and if-statements, and comes with a few built-in functions, as given in the Copper Lang Guide. It has the ability to parse strings and numbers but has no functions for doing anything with them.

You, the user, are expected to provide all the functions, hooks, and datatypes you need for the interpreter to fulfill your purposes. See Extending the Engine for details.

Some functionality is provided as extensions. These are given in the ext folder. There are setup functions that allow easily adding these functions, by default names, to any given engine.

File Folder Structure

The necessary files for the engine are all included in the Copper/src directory. All of these files expect to be on the same level and can be dropped directly into any project folder you wish.

The extensions expect the file structure to be as follows:

- root
-- Copper
--- src
--- stdlib // for standard lib extensions
-- exts
--- extension-specific folder

The extensions are not bound to some particular file structure in any complicated way. If you wish to have a different structure, you only need to change the #include at the top of each extension file.

Running the Engine

To run the interpreter, you need to create an instance of the Copper Engine. This engine needs to be fed a stream of bytes, which represent the Copper code. A basic implementation is InStreamLogger, provided as one of the extensions in the folder "stdlib".

By default, the engine filters out all non-ASCII names, but you can change this by providing a new filter function of the prototype bool name(const String&) and passing it to the engine via Engine::setNameFilter().


int main() {
	Cu::Engine engine;
	CuStd::Printer printer;	// You will need to include Printer.h for this.
	CuStd::InStreamLogger streamLogger; // You will need to include InStreamLogger.h for this.
	engine.setLogger(&streamLogger);
	engine.addForeignFunctionInstance(util::String("print"), &printer);

	int err = 0;
	try {
		while ( engine.run( streamLogger ) == Cu::EngineResult::Ok );
	}
	catch( ... ) {
		std::printf("The Copper Interpreter threw an error.");
	}
	return(0);
}

Extending the Engine

The engine can be extended with functions in by implementations of the class Cu::ForeignFunc, passed into Engine::addForeignFunction().

A single-call method for adding ForeignFunc classes to the engine is the template function: Cu::addForeignFuncInstance<my_foreign_func>(Engine* engine, const String& pName) However, this can bloat code.

ForeignFunc classes can also be added using Cu::addNewForeignFunc(Engine& pEngine, const String& pName, ForeignFunc* pFunction), however, this function will dereference the instance because it is expected that you create a new heap object inline with the function call.

The interface for foreign functions is as follows:


// Called by the Engine when the foreign function is called.
virtual bool call( FFIServices& ffi )=0;

// Returns if the function can accept any number of parameters.
virtual bool isVariadic() {
	return false;
}

// Returns the typeName of the parameter expected at index "index".
virtual ObjectType::Value getParameterType( unsigned int index ) {
	return ObjectType::Function;
}

// Returns the total number of parameters the function accepts.
virtual unsigned int getParameterCount() {
	return 0;
}

Functions can be made to accept any number of functions. These are called "variadic functions", and they must return "true" from ForeignFunc::isVariadic().

Functions can be designed to accept a limited number of parameters by implementing getParameterType() and getParameterCount(). When a function call is made from within the Copper language, the engine will check to make sure the arguments match the results of getParameterType() up to the number of parameters given by getParameterCount(). With this handled, the user does not need to check the types of the objects being passed.

Using Standard C++ Functions

Standard C++ functions with the function prototype bool [name] (FFIServices&) can be added to the engine using void addForeignFuncInstance( Engine& pEngine, const String& pName, bool (*pFunction)( FFIServices& ) .

Class methods with the function prototype bool [name] (FFIServices&) can be added to the engine using void addForeignMethodInstance( Engine& pEngine, const String& pName, BaseClass* pBase, bool (BaseClass::*pMethod)( FFIServices& ) .

Using the Foreign Function Interface

The interface is designed to accomodate the needs of both variadic and normal functions. If you have created a variadic foreign function, you will need to use ForeignFunctionInterface::hasMoreParams().

The following demonstrates a variadic function:


bool isObjectMyInt( ObjectType::Value pArgType ) {
	return pArgType == ObjectType::UnknownData + 100;
}

struct intAdd : public Cu::ForeignFunc {
	virtual bool call( Cu::FFIServices& ffi );

	virtual bool isVariadic() {
		return true;
	}
};

bool intAdd::call( FFIServices& ffi ) {
	Cu::Object* arg;
	int i = 0;
	while ( ffi.hasMoreArgs() ) {
		arg = ffi.getNextArg();
		// Presuming we have a class named "MyInt" with a type name of "my_int"
		if ( isObjectMyInt(arg->getType()) ) {
			i += dynamic_cast<MyInt*>(arg)->getValue();
		} else {
			ffi.printWarning("Argument to intAdd was not of type my_int. It will be ignored...");
		}
	}
	MyInt* out = new MyInt(i);
	ffi.setResult(out); // Calls ref() on the argument.
	out->deref();
	return true; // Return "false" if there is an error.
}

The following demonstrates a normal function. Note that in a normal function, there is no need to check for the number of parameters as long as the appropriate number is returned from getParameterCount().


struct DrawImage : public Cu::ForeignFunc {
	virtual bool call( Cu::FFIServices& ffi );

	virtual ObjectType::Value getParameterType( Cu::UInteger index ) const {
		if ( index == 0 ) {
			return ObjectType::UnknownData + 101; // Assuming this is the image type.
		}
		return ObjectType::UnknownData + 100;
	}

	virtual Cu::UInteger getParameterCount() const {
		return 3;
	}
};

bool DrawImage::call( FFIServices& ffi ) {
	MyImage* image = dynamic_cast<MyImage*>( ffi.getNextArg() );
	int startX = dynamic_cast<MyInt*>( ffi.getNextArg() )->getValue();
	int startY = dynamic_cast<MyInt*>( ffi.getNextArg() )->getValue();

	if  ( image->isCorrupt() ) {
		ffi.printError("Image passed to drawImage is corrupt.");
		return false;
	}
	draw( image, startX, startY );
	return true;
}

All parameters from getNextParam() are passed as Cu::Object* (basic objects used in the interpreter). The lifetime of such objects may only be as long as the function call, so to keep them, you will need to call Object::ref() on the object. It is expected that you will cast the each argument to the datatype whose type is returned by getParameterType().

Creating New Datatypes

Copper is very flexible in that anything that inherits Cu::Object can be returned to the engine and passed around.

The user will need to make use of the following functions for correct reference counting:


// Increments the reference-count of the object.
void ref();

// Decrements the reference-count of the object.
// When the reference-count reaches zero, the object is destroyed.
void deref();

Three of the virtual functions of Cu::Object need to be implemented for a class to be a valid, working object in the Copper interpreter:


// Creates an independent copy of the object (except in the case of monads).
virtual Object* copy();

// Returns the name used for identifying what type this object is.
virtual const char* typeName() const =0;

The engine currently uses the typeName() method when it needs to check if two types are the same via are_same_type(). It is also used for printing. Therefore, the return from typeName() should be unique, but it can optionally be left blank.

The object type (not the type-name) is stored in the protected variable type. This value must be set, and you are to do so by passing in a unique value casted to ObjectType::Value to the constructor of Cu::Object within the constructor of your object.

Optionally, the following functions can be implemented. Each of these is considered a data-returning function. They can allow for printing or conversion of data.


// Defaults to "{object}".
virtual void writeToString(String& out) const;

// Defaults to 0.
virtual Integer getIntegerValue() const;

// Defaults to 0.
virtual Decimal getDecimalValue() const;

Example:


class MyInt : public Cu::Object {
	int i;
public:
	MyInt(int p)
		: Cu:Object( ObjectType::UnknownData + 100 )
		, i(p)
	{}

	virtual Object* copy() {
		return new MyInt(i);
	}

	virtual void writeToString( String& out ) {
		out = "my int";
	}

	virtual const char* typeName() const {
		return "my int";
	}

	int getValue() {
		return i;
	}

	void add( MyInt* pOther ) {
		i += pOther->i;
	}
};

Getting your objects into Copper processing is done by setting them to be the return value of a function. A parameterless function is sufficient for doing this.


struct CreateMyInt : public Cu::ForeignFunc {
	virtual bool call( Cu::FFIServices& ffi );
};

bool CreateMyInt::call( Cu::FFIServices& ffi ) {
	MyInt* m = new MyInt(0);
	ffi.setResult(m);
	m->deref();
	return true;
}

Built-in Engine Objects

The following is a list of objects in the interpreter. These can be passed to Cu::FFIServices::setResult(Cu::Object*) from within a foreign function's call() body.

FunctionContainer

Container for a function. This is what variables point at.

Typenamefn
Constructors explicit FunctionContainer( Function* pFunction, unsigned int id=0 )
FunctionContainer()
Copy ConstructorDisallowed. Use Object::copy() instead.
Data-access Methodbool getFunction(Function*& storage)

ObjectBool

Basic boolean container.

Typenamebool
Constructorexplicit ObjectBool(bool b)
Copy ConstructorAllowed. Can also use Object::copy().
Data-access Methodbool getValue()

ObjectString

Basic binary string container.

Typenamestring
Constructor ObjectString()
explicit ObjectString( const String& pValue )
Copy ConstructorAllowed. Can also use Object::copy().
Data-access MethodString& getString()

ObjectInteger

Container for an integer representation of a number.

Typenameint
Constructor ObjectInteger()
ObjectInteger( Integer newValue )
Copy ConstructorAllowed. Can also use Object::copy().
Data Methods void setValue( Integer pValue )
virtual Integer getIntegerValue() const
virtual Decimal getDecimalValue() const

ObjectDecimal

Container for a decimal representation of a number.

Typenamedcml
Constructor ObjectDecimal()
ObjectDecimal( Integer newValue )
Copy ConstructorAllowed. Can also use Object::copy().
Data Methods void setValue( Integer pValue )
virtual Integer getIntegerValue() const
virtual Decimal getDecimalValue() const

ObjectList

A dynamically-sized doubly-linked list.

Typenamelist
Constructor ObjectDecimal()
ObjectDecimal( Integer newValue )
Copy ConstructorAllowed. Can also use Object::copy().
Data Methods Integer size() const
virtual void append( Object* pItem )
void push_back( Object* pItem )
void push_front( Object* pItem )
bool remove( Integer index )
bool insert( Integer index, Object* pItem )
bool swap( Integer index1, Integer index2 )
bool replace( Integer index, Object* pItem )
Object* getItem( Integer index )

Callbacks

Save, reference, and call changeOwnerTo on a FunctionContainer passed into a ForeignFunc::call implementation.

API

public Engine API

Engine()

Creates the engine in a ready-to-process state.

void setLogger( Logger* )

Sets the interface that is used for printing info, warnings, errors, and debug messages.

WARNING: The Logger parameter is never referenced or dropped. It is the responsibility of the user to disconnect the logger by calling setLogger(REAL_NULL) before deleting the logger.

void print( const LogLevel::Value&, const char* ) const

Called by the Engine for printing messages. There is no need for the user to call this directly.

void print( const LogLevel::Value&, const EngineMessage::Value& ) const

Called by the Engine for printing messages. There is no need for the user to call this directly.

void setEndofProcessingCallback( EngineEndProcCallback* )

Sets the callback interface used when the Engine operation terminates normally, via the "exit" command.

WARNING: The EngineEndProcCallback parameter is never referenced or dropped. It is the responsibility of the user to disconnect the EngineEndProcCallback by calling setEndofProcessingCallback(REAL_NULL) before deleting the EngineEndProcCallback.

void setIgnoreBadForeignFunctionCalls( bool )

Allows the engine to ignore errors produced by foreign functions and continue processing. By default, it is false.

void setOwnershipChangingEnabled( bool )

Allows the usage of the "own" system function. The "own" function allows for changing the ownership of a function from one variable to another variable that points to it.

WARNING: Ownership-changing can cause cyclic references in memory, thereby created memory leaks. The engine does not have a garbage collector, so these leaks cannot be fixed and the lost memory cannot be recovered until program termination.

void setStackTracePrintingEnabled

Turns on the stack-trace printing.

void setNameFilter( bool(*filter)(const String&) )

Sets the function used by the engine for filtering names. This function should return "true" only if the string given to it is a valid name.

NOTE: The engine filters basic ASCII names by default and allows for numbers and some special characters. A filter is required for allowing unicode names, but the filter will handle the full responsibility of approving names (because the engine will no longer check for ASCII names).

void addForeignFunction( const String& pName, ForeignFunc* pFunction )

Accepts a name for a foreign function and the class function whose call() method should be called when that name is found during Copper code execution.

EngineResult::Value run( ByteStream& )

Runs the Copper interpreter, extracting bytes from the given ByteStream and interpreting them as Copper code.

EngineResult::Value runFunctionObject( FunctionContainer* )

Runs the body of the given function-object.

NOTE: This is meant to be used for callback functions, so it should only be run after other engine processing has completed.