model_obj
) are nodes in a dependency
graph. Model objects are constructed from a symbolic function name
(e.g.: "lineThru2Pts") and a list of arguments. The arguments are the
prerequisites of the model object and the model object is a dependent
of each argument that is a model object itself (arguments are not
necessarily model objects). Dependency graphs are acyclic, directed
graphs.The data members of a model_obj are:
object_type *
)
prereq_obj *
)
model_list_obj *
)
this
.
cons_fp_type
)
param
.
string_type
)
consfn
.
id_type
)
boolean_type
)
boolean_type
)
param
is out of date with respect to
consfn
or prereqs
.
int
)
string_type
)
param
member object. The parametric
object is a generic object (object_type *
) of unknown
type. This member is created by applying a function pointer to a list
of arguments and only indirectly constructed in the C++ object sense.
cons_fp_type
(constructor
function pointer). It's definition is:
typedef object_type *(*cons_fp_type)( prereq_obj * )A cons_fn_type is a pointer to a function that takes a list of prereq_obj's (an prereq_obj is a list_obj) and returns a pointer to an object_type. These functions are called wrapper functions since they wrap C++ constructors, member functions, or regular external C++ functions. This indirect construction of the
param
member is used both when a model_obj is constructed
and when dependency propagation updates an existing model_obj.Since we are basically subverting the inherent type checking of C++ by using a generic argument list, we must provide our own type checking and conversion before the actual C++ function can be called. This functionality is provided by a set of accessors for the prereq_obj class. Error handling for type mismatches is implemented by raising exceptions and providing handlers. This is aided by the fact that wrapper functions are only called in a limited (and small) number of places where exception handlers can be installed.
If all the types are correct, the underlying functions can still have
data dependent failures. In some cases, these will also raise
exceptions. In other cases a NULL
pointer is returned.
The exception handling code in the private model_obj methods is
designed to catch these cases as well.
param
member). In this case there are no prerequisites or
consfn. There can be subsequent dependents.
private: model_obj( const string_type cons_fn, prereq_obj *args );Note that the consfn is specified symbolically and looked up in a symbol table.
Due to the policy of public methods not raising exceptions, the public method that corresponds to this must be a static method. Here are the public constructors:
public: static model_obj *new_model( const string_type cons_fn, prereq_obj *args ); static model_obj *new_model( object_type *param ); model_obj( object_type *param ); model_obj(); model_obj( const model_obj *_obj );The
model_obj( object_type *param )
constructor is public
because it will not raise an exception. The second static method just
adds a layer on this constructor for consistency. The last two
constructors are the default and the copying constructors required for
general Alpha_1 objects.
param
object has been created by a wrapper
function call, the model_obj being constructed is linked into the
existing dependency graph. Links are created to all of the arguments
(prereq_obj's) that represent other model_obj's. More specifically,
for each model_obj in the prereq_obj list, the newly constructed
model_obj becomes a dependent. The prereq_obj list given to the
model_obj constructor becomes the prereqs
list for the
constructed model_obj. The dependency links are thus bidirectional
although each direction is implemented via a different data type
(prereq_obj's for prerequisites and a model_list_obj for
dependents).
For model_obj's constructed from an existing param
object, there are no prerequisites and therefore it cannot be a
dependent of any other model_obj. However, a model_obj constructed
this way can subsequently become a prerequisite for another model_obj.
param
objects starting from a set of
known changed model_obj's and propagating those changes through the
graph of dependent objects. The top-level propagation function is:
private: static void propagate_changes_from( model_list_obj *objs );Propagation can fail due to type conflicts and data dependent errors (same as constructors) so this is a private member function. Propagation is aborted when an error is detected.
param
object can be provided. In either case, the existing state of the
model_obj (whether is has a wrapper function and arguments, or only a
param object) does not matter. The public update methods are:
public: boolean_type update_model( object_type *param ); boolean_type update_model( const string_type cons_fn, prereq_obj *args );These report an error with a
boolean_type
return value
and do not raise exceptions. The private methods raise exceptions:
private: void update( object_type *param ); void update( const string_type cons_fn, prereq_obj *args );
.scl
is used.
A token scanner and a parser (written in YACC) convert
text input into a parse-tree representation using sym_exp_obj's. This
parse-tree representation is similar to a LISP S-expression, or
functional specification. Function and object names use the arg_type
A_SYM
tag and are looked up in a symbol table during
evaluation.
Functions are divided into three categories:
extern model_server_type ms;When
ms
is initialized with the
set_up_server
method, it is stored in a static variable
of the model_obj class as the current server. The presence of this
pointer signifies to the model_obj class that it is to provide model
server functionality and handling of remote clients via the given
model_server_type.
cons_fp_type
is a pointer to a wrapper function.
It's definition is:
typedef object_type *(*cons_fp_type)( prereq_obj * )A symbol table maps symbolic names (character strings) to cons_fp_type's. When adding new wrappers, the programmer writes the function and specifies the symbolic name for the symbol table.
static object_type * wrap_pt_interp( prereq_obj *args ) { pt_obj *pt1 = (pt_obj *)args->arg_param( 0, T_PT_OBJ ), *pt2 = (pt_obj *)args->arg_param( 1, T_PT_OBJ ); real_type t = args->arg_num( 2 ); return new pt_obj( pt1->interpolate( *pt2, t )); }The underlying function is a method of pt_obj called interpolate and requires a pt_obj and a real_type for arguments. The SCL specification for ptInterp is:
ptInterp( Point1, Point2, Param )The wrapper is written assuming that the arguments are correct. It's only job is to extract them and call the underlying function. The above code will handle type checking errors and missing arguments. The accessor methods are guaranteed to return valid results. If they can't, they don't return at all (they raise exceptions).
In this particular example, the method
object_type *arg_param( const int, tag_type )is used. This returns an object_type pointer that has been checked against the given type tag and extracted from the argument list at the given position (i.e.: for
pt1
extract the first argument
which should be a pt_obj). The only thing left for the wrapper to do
is cast the pointer to the appropriate type. The
real_type arg_num( const int )method returns a real_type extracted from the given argument. The argument may be an integer or floating point constant, or a named model_obj with a numeric value.
The above example will handle omissions, ordering errors, and general type checking in the argument list. It will not detect extra arguments. The wrapper will work fine with extra arguments if the first three arguments are indeed correct. Detecting errors for extra arguments is a desirable convenience for users that can be added as follows:
args->num_args( 3, "ptInterp" );This methods checks the overall number of arguments and only returns if it is correct. If not, an exception is raised. The provided "name" is used in an error message. If a wrapper provides this check, it is generally the first thing done in the wrapper.
Here are the available accessors for (value_obj) argument lists. All of these have an optional last argument for a description or name of argument. If provided, it is used in any error messages that are generated with respect to the given argument. The first argument is the position to access in the argument list (0-based).
int arg_int( const int, string_type descr = NULL ) const; real_type arg_fl( const int, string_type descr = NULL ) const; string_type arg_str( const int, string_type descr = NULL ) const; boolean_type arg_bool( const int, string_type descr = NULL ) const; real_type arg_num( const int, string_type descr = NULL ) const; real_type arg_nonnegative_num( const int index, string_type descr = NULL ) const; real_type arg_positive_num( const int index, string_type descr = NULL ) const; object_type *arg_param( const int index, tag_type tag, string_type descr = NULL ) const; model_obj *arg_model( const int, string_type descr = NULL ) const; value_obj *arg_lst( const int, string_type descr = NULL ) const;The meaning of most of these is obvious from the name and return type. The arg_param method is for an argument that is an object with the given type tag (see above example). The arg_lst method is for an argument that is a SCL array (represented as a value_obj list). Note that the arg_param method can be called with T_NO_TAG to match any object_type.
For checking the overall number of arguments use:
void num_args( int right_num, string_type cons_name ); void num_args( int min_num, int max_num, string_type cons_name );The second case is for wrappers with optional arguments. Note that a firm policy on option arguments has not been established yet.
The next set of accessors are used less frequently, or internally by the other accessors:
tag_type arg_tag_type( const int index ) const; const value_obj *arg_index( int ) const; value_obj *arg_index( int );Finally, there are two methods for directly raising exceptions for error handling. These should be used only in very rare cases. If you think you need to use them, you should consider writing a new arg_obj accessor that does what you need. If that is not possible, then these may be used:
void arg_type_exception( const int index, string_type object_names ) const; void arg_exception( const int index, string_type exception_string) const;
object_type *
. Any
C++ constructor or external function which returns an object_type * or
pointer to any class derived from object_type can be returned from a
wrapper. For constructor wrappers, this returned object becomes the
"param" slot of a model_obj and is "owned" by the model_obj. Also note
that objects extracted from the argument list via the
arg_paramaccessor are "owned" by their model_obj's and must
be copied if they are to be modified or become part of the return
value of a wrapper.There are some special cases for model_obj "param" values. To return a basic data type (integer, float, or string) the wrapper should construct a prereq_obj (derived class of value_obj) for the return value. To return a list (SCL array), the wrappers should return an array_val_obj which contains a list of value_obj's. The value_obj's in the list can be prereq_obj's (for basic data types plus shared model_obj references) or ptr_val_obj's for generic objects.
Here are some examples:
object_type * wrap_cos( prereq_obj *args ) { return new prereq_obj( cos( args->arg_num( 0 ) ) ); }
object_type * wrap_array( prereq_obj *args ) { return new array_val_obj( args->cp_obj_list() ); }Note that prereq_obj's that refer to model_objs are always shared references. In other words, cp_obj applied to such a prereq_obj produces a new prereq_obj that points to the same model_obj. This means the array constructor produces a list of shared model_obj references.
Another option is to return a list of objects constructed internally by the wrapper itself. For example you might want wrapper that took an argument list of four numbers and returned a list of two points. In this case we use a ptr_val_obj to wrap each point and then an array_val_obj to hold the list of value_obj's.
object_type * wrap_pt_list_from_4_nums( prereq_obj *args ) { pt_obj *p1 = new pt_obj( E2, args->arg_num( 0 ), args->arg_num( 1 ) ), *p2 = new pt_obj( E2, args->arg_num( 2 ), args->arg_num( 3 ) ); ptr_val_obj *v1 = new ptr_val_obj( p1, FALSE ); v1->append( new ptr_val_obj( p2, FALSE ) ); return new array_val_obj( v1 ); }The ptr_val_obj class can "own" its object or share it. The default is to share it which is intended for use in side-effect functions (see below). In this case (a constructor wrapper) we want to own the objects so they are subsequently owned by the model_obj.
Note that for a wrapper that accepts a list (SCL array), there is no need to differentiate between the two types of lists above (model_obj references or "raw" object references). The arg_param accessors handle the differences. So if a constructor required a list of points, it would work for either of the following in SCL:
PtList1 = { pt(0,0), pt(1,1) }; PtList2 = ptListFrom4Nums( 0, 0, 1, 1 );Side-effect functions may treat the two differently, however.
ls
$a$molib/*wrap.C
.
Wrapper functions are written as static functions. By convention, the
names are taken from the RLISP functions they are replacing by
appending wrap_
and using _'s instead of
capitalization.
The symbol name for the SCL interpreter is grouped together with the
wrapper function pointer in a class called
fn_info_type
. Finally, a static array of fn_info_type's
plus a package name are used to construct a
model_pkg_type
object. An external model_pkg_type object
is defined in each wrapper file. These objects are used to initialize
the symbol table at run-time and provide the mapping from symbol name
to function pointer.
To add a new wrapper to an existing package:
static object_type *wrap_my_fun( prereq_obj *args );
static fn_info_type f_infos[] = { fn_info_type( "pt", wrap_pt ), ... /* Add a line at the end for the new wrapper. */ fn_info_type( "myfun", wrap_my_fun ) };
$a$aimo/model_pkg.h
. Add functions to the
new package as above. Finally, the c_shape_edit main program must be
modified to load the new package by adding a line to a static array in
$a$modeld/c_shape_edit.C
.
$a$molib
, compiling the changes, and linking a new
c_shape_edit ($a$modeld
) with your changes.Test with motif3d or use "dumpA1File," or "dbgObj" from the command language.
When adding a wrapper function an assessment has to be made about the underlying C++ code. There are a number of possibilities:
$a$molib/str_to_enum.C
for some
examples.
side_fp_type
:
typedef value_obj *(*side_fp_type)( value_obj * );The difference between wrapper functions (constructors) and side-effect functions is the return type and argument list type are value_obj which is the base class "value" type. Thus, side-effect functions are more general.
The interpreter will handle side-effect functions that return
NULL
(true side-effect functions), and will simply print
the results if not null. Wrapper functions, on the other hand, raise
an exception if NULL
is returned. Side-effect functions
cannot be used on the right-hand-side of an assignment statement, but
can be nested.
As a rule, if a function is ever meant to be used as a model_obj constructor (i.e.: used with assignment statements and dependency propagation) then it should be a wrapper function. Wrapper functions can be used as side-effect functions in that they can be called as a user query.
For example ptX is a wrapper function. It can be used to create named model_obj's (with numeric values) and dependency propagation will flow through these objects:
p = pt( 1, 2, 3 ); px = ptX( p ); p2 = pt( 1 + px, 0, 0 );However ptX can also be called as a query without creating any new model_obj's in the dependency graph:
# What is p2's current X value? ptX( p2 );In this case the interpreter simply prints the result of the wrapper function and immediately deletes it.
value_obj *
. This
is a base class for prereq_obj and ptr_val_obj. Between these two
classes you can return int, real_type, string, model_obj *,
object_type *, and array_val_obj for a list of all of the above. The ptr_val_obj class can be used to return shared object references for nested side-effect functions. Here are two examples to illustrate:
static value_obj * dbg_obj( value_obj *args ) { args->get_param( T_NO_TAG )->dbg_obj(); return NULL; } value_obj * wrap_shell_srf( value_obj *args ) { args->num_args( 2, "shellSrf" ); shell_obj *shl = (shell_obj *)args->arg_param( 0, T_SHELL_OBJ, "Shell" ); int index = args->arg_int( 1, "Shell surface index" ); /* Return shared srf_obj pointer. */ return new ptr_val_obj( shl->srfs()->nth_element( index ) ); }With these two side-effect functions we can do this in SCL:
Shell = ...; dbgObj( Shell ); dbgObj( shellSrf( Shell, 0 ) );