multmeth.t | documentation |
#charset "us-ascii" /* * Copyright (c) 2008 Michael J. Roberts. All Rights Reserved. * * This module provides the run-time component of "multi-methods" in TADS * 3. This works with the compiler to implement a multiple-dispatch * system. * * Multi-methods are essentially a combination of regular object methods * and "overloaded functions" in languages like C++. Like a regular * object method, multi-methods are polymorphic: you can define several * incarnations of the same function name, with different parameter * types, the system picks the right binding for each invocation * dynamically, based on the actual argument values at run-time. Unlike * regular methods, though, the selection is made on ALL of the argument * types, not just a special "self" argument. In that respect, * multi-methods are like overloaded functions in C++; but multi-methods * differ from C++ overloading in that the selection of which method to * call is made dynamically at run-time, not at compile time. * * There are two main uses for multi-methods. * * First, most obviously, multi-methods provide what's known as "multiple * dispatch" semantics. There are some situations (actually, quite a * few) where the ordinary Object Oriented notion of polymorphism - * selecting a method based on a single target object - doesn't quite do * the trick, because what you really want to do is select a particular * method based on the *combination* of objects involved in an operation. * Some canonical examples are calculating intersections of shapes in a * graphics program, where you want to select a specialized "Rectangle + * Circle" routine in one case and a "Line + Polygon" routine in another; * or performing file format conversions, where you want to select, say, * a specialized "JPEG to PNG" routine. In an IF context, the obvious * use is for carrying out multi-object verbs, where you might want a * special routine for PUT (liquid) IN (vessel), and another for PUT * (object) IN (container). * * Second, multi-methods offer a way of extending a class without having * to change the class's source code. Since a multi-method is defined * externally to any classes it refers to, you can create a method that's * polymorphic on class type - just like a regular method - but as a * syntactically stand-alone function. This feature isn't as important * in TADS as in some other languages, since TADS lets you do essentially * the same thing with the "modify" syntax; but for some purposes the * multi-method approach might be preferable aesthetically, since it's * wholly external to the class rather than a sort of lexically separate * continuation of the class's code. (However, as a practical matter, * it's not all that different; our implementation of multi-methods does * in fact modify the original class object, since we store the binding * information in the class objects.) */ #include <tads.h> /* ------------------------------------------------------------------------ */ /* * Invoke a multi-method function. For an expression of the form * *. f(a, b, ...) * * where 'f' has been declared as a multi-method, the compiler will * actually generate code that invokes this function, like so: * *. _multiMethodCall(baseFunc, params); * * 'baseFunc' is a function pointer giving the base function; this is a * pointer to the common stub function that the compiler generates to * identify all of the multi-methods with a given name. 'params' is a * list giving the actual parameter values for invoking the function. * * Our job is to find the actual run-time binding for the function given * the actual parameters, and invoke it. */ _multiMethodCall(baseFunc, args) { /* get the function binding lookup information */ local info = _multiMethodRegistry.boundFuncTab_[baseFunc]; /* it's an error if there's no binding */ if (info == nil) throw new UnboundMultiMethod(baseFunc, args); /* * Look up the function binding based on the arguments. To ensure * that we match a function with the correct number of argument, we * have to explicitly add the last-argument placeholder to the list. */ local func = _multiMethodSelect(info, args + _multiMethodEndOfList); /* if we found a binding, invoke it; otherwise throw an error */ if (func == nil) throw new UnboundMultiMethod(baseFunc, args); else return (func)(args...); } /* * Invoke the base multi-method inherited from the given multi-method. * 'fromFunc' is a pointer to a multi-method, presumably the one * currently running; we look up the next in line in inheritance order * and invoke it with the given argument list. */ _multiMethodCallInherited(fromFunc, [args]) { #ifdef MULTMETH_STATIC_INHERITED /* static mode - get the cached inheritance information */ local inh = _multiMethodRegistry.inhTab_[fromFunc]; #else /* dynamic mode - get the base function binding */ local info = _multiMethodRegistry.boundFuncTab_[ _multiMethodRegistry.baseFuncTab_[fromFunc]]; /* it's an error if it doesn't exist */ if (info == nil) throw new UnboundInheritedMultiMethod(fromFunc, args); /* look up the inherited function based on the actual parameters */ local inh = _multiMethodInherit( fromFunc, info, args + _multiMethodEndOfList); #endif /* it's an error if there's no inherited binding */ if (inh == nil) throw new UnboundInheritedMultiMethod(fromFunc, args); /* call it */ return (inh)(args...); } /* ------------------------------------------------------------------------ */ /* * Get a pointer to a resolved multi-method function. This takes a * pointer to the base function for the multi-method and a list of actual * argument values, and returns a function pointer to the specific * version of the multi-method that would be invoked if you called the * multi-method with that argument list. * * For example, if you want to get a pointer to the function that would * be called if you were to call foo(x, y, z), you'd use: * *. local func = getMultiMethodPointer(foo, x, y, z); * * We return a pointer to the individual multi-method function that * matches the argument list, or nil if there's no matching multi-method. */ getMultiMethodPointer(baseFunc, [args]) { /* get the function binding lookup information */ local info = _multiMethodRegistry.boundFuncTab_[baseFunc]; /* if there's no binding information, return failure */ if (info == nil) return nil; /* look up and return the function binding based on the arguments */ return _multiMethodSelect(info, args + _multiMethodEndOfList); } /* ------------------------------------------------------------------------ */ /* * Resolve a multi-method binding. This function takes a binding * property ID (the property we assign during the registration process to * generate the binding tables) and a "remaining" argument list. This * function invokes itself recursively to traverse the arguments from * left to right, so at each recursive invocation, we lop off the * leftmost argument (the one we're working on currently) and pass in the * remaining arguments in the list. * * We look up the binding property on the first argument in the remaining * argument list. This can yield one of three things: * * - The trivial result is nil, which means that this binding property * has no definition on the first argument. This doesn't necessarily * mean that the whole function is undefined on the arguments; it only * means that the current inheritance level we're looking at for the * previous argument(s) has no binding. If we get this result we simply * return nil to tell the caller that it must look at an inherited * binding for the previous argument. * * - If the result is a function pointer, it's the bound function. This * is the final result for the recursion, so we simply return it. * * - Otherwise, the result will be a new property ID, giving the property * that resolves the binding for the *next* argument. In this case, we * use this property to resolve the next argument in the list by a * recursive invocation. If that recursive call succeeds (i.e., returns * a non-nil value), we're done - we simply return the recursive result * as though it were our own. If it fails, it means that there's no * binding for the particular subclass we're currently working on for the * first argument - however, there could still be a binding for a parent * class of the first argument. So, we iterate up to any inherited * binding for the first argument, and if we find one, we try again with * the same recursive call. We continue up our first argument's class * tree until we either find a binding (in which case we return it) or * exhaust the class tree (in which case we return nil). */ _multiMethodSelect(prop, args) { local obj, binding; /* * Get the first argument from the remaining arguments. If it's not * an object, use the placeholder object for non-object parameter * bindings. */ local orig = args[1]; if (dataType(orig) not in (TypeObject, TypeList, TypeSString)) orig = _multiMethodNonObjectBindings; /* get the remaining arguments */ args = args.sublist(2); /* * Look up the initial binding - this is simply the value of the * binding property for the first argument. In order to process the * inheritance tree later, we'll need to know where we got this * definition from, so look up the specific defining object. * * If the initial binding property isn't defined, or its value is * explicitly nil, the function isn't bound (or, in the case of nil, * is explicitly unbound). Inheritance won't help in these cases, so * we can immediately return nil to indicate that we don't have a * binding. */ if ((obj = orig.propDefined(prop, PropDefGetClass)) == nil || (binding = obj.(prop)) == nil) return nil; /* * If there are no more arguments, but we didn't just find a final * function binding, we don't have enough arguments to match the * current multi-method path. Return failure. */ if (args.length() == 0 && dataType(binding) != TypeFuncPtr) return nil; /* * starting at our current defining object for the first argument, * scan up its superclass tree until we find a binding */ for (;;) { local ret; /* if we have a function pointer, we've found our binding */ if (dataType(binding) == TypeFuncPtr) return binding; /* * Recursively bind the binding for the remaining arguments. If * we find a binding, we're done - simply return it. */ if ((ret = _multiMethodSelect(binding, args)) != nil) return ret; /* * We didn't find a binding for the remaining arguments, so we * must have chosen too specific a binding for the first * argument. Look for an inherited value of the binding property * in the next superclass of the object where we found the last * binding value. */ obj = orig.propInherited(prop, orig, obj, PropDefGetClass); if (obj == nil) return nil; /* we found an inherited value, so retrieve it from the superclass */ binding = obj.(prop); } } /* * Select the INHERITED version of a multi-method. This takes a * particular version of the multi-method, and finds the next version in * inheritance order. * * This is basically a copy of _multiMethodSelect(), with a small amount * of extra logic. This code repetition isn't good maintenance-wise, and * the two functions could in principle be merged into one. However, * doing so would have an efficiency cost to _multiMethodSelect(), which * we want to keep as lean as possible. */ _multiMethodInherit(fromFunc, prop, args) { return _multiMethodInheritMain( new _MultiMethodInheritCtx(), fromFunc, prop, args); } class _MultiMethodInheritCtx: object foundFromFunc = nil ; _multiMethodInheritMain(ctx, fromFunc, prop, args) { local obj, binding; /* * Get the first argument from the remaining arguments. If it's not * an object, use the placeholder object for non-object parameter * bindings. */ local orig = args[1]; if (dataType(orig) not in (TypeObject, TypeList, TypeSString)) orig = _multiMethodNonObjectBindings; /* get the remaining arguments */ args = args.sublist(2); /* * Look up the initial binding - this is simply the value of the * binding property for the first argument. In order to process the * inheritance tree later, we'll need to know where we got this * definition from, so look up the specific defining object. * * If the initial binding property isn't defined, or its value is * explicitly nil, the function isn't bound (or, in the case of nil, * is explicitly unbound). Inheritance won't help in these cases, so * we can immediately return nil to indicate that we don't have a * binding. */ if ((obj = orig.propDefined(prop, PropDefGetClass)) == nil || (binding = obj.(prop)) == nil) return nil; /* * If there are no more arguments, but we didn't just find a final * function binding, we don't have enough arguments to match the * current multi-method path. Return failure. */ if (args.length() == 0 && dataType(binding) != TypeFuncPtr) return nil; /* * starting at our current defining object for the first argument, * scan up its superclass tree until we find a binding */ for (;;) { /* we haven't found a function binding yet */ local ret = nil; /* * we either have a function pointer, in which case it's the * actual binding, or a property, in which case it's the next * binding level */ if (dataType(binding) == TypeFuncPtr) { /* this is the binding */ ret = binding; } else { /* if there are no more arguments, return failure */ if (args.length() == 0) return nil; /* * Recursively bind the binding for the remaining arguments. * If we find a binding, we're done - simply return it. */ ret = _multiMethodInheritMain(ctx, fromFunc, binding, args); } /* check to see if we found a binding */ if (ret != nil) { /* * We found a binding. If we've already found the inheriting * version, return the first thing we find, since that's the * next inheriting level. Otherwise, if this is the * inheriting version, note that we've found it, but keep * looking, since we want to find the next one after that. * Otherwise, just keep looking, since we haven't even * reached the overriding version yet. */ if (ctx.foundFromFunc) return ret; else if (ret == fromFunc) ctx.foundFromFunc = true; } /* * We didn't find a binding for the remaining arguments, so we * must have chosen too specific a binding for the first * argument. Look for an inherited value of the binding property * in the next superclass of the object where we found the last * binding value. */ obj = orig.propInherited(prop, orig, obj, PropDefGetClass); if (obj == nil) return nil; /* we found an inherited value, so retrieve it from the superclass */ binding = obj.(prop); } } /* ------------------------------------------------------------------------ */ /* * Unbound multi-method exception. This is thrown when a call to resolve * a multi-method fails to find a binding, meaning that there's no * definition of the method that matches the types of the arguments. */ class UnboundMultiMethod: Exception construct(func, args) { /* note the function name and argument list */ func_ = func; args_ = args; /* look up the function's name */ name_ = _multiMethodRegistry.funcNameTab_[func]; } /* display an error message describing the exception */ displayException() { "Unbound multi-method \"<<name_>>\" (<<args_.length()>> argument(s))"; } /* the base function pointer */ func_ = nil /* the symbol name of the base function */ name_ = '' /* the number of arguments */ args_ = 0 ; class UnboundInheritedMultiMethod: UnboundMultiMethod displayException() { "No inherited multi-method for \"<<name_>>\" (<<args_.length()>> arguments(s))"; } ; /* ------------------------------------------------------------------------ */ /* * Base class for our internal placeholder objects for argument list * matching. */ class _MultiMethodPlaceholder: object ; /* * A placeholder object for bindings for non-object arguments. Whenever * we have an actual argument value that's not an object, we'll look here * for bindings for that parameter. When registering a function, we'll * register a binding here for any parameter that doesn't have a type * specification. */ _multiMethodNonObjectBindings: _MultiMethodPlaceholder ; /* * A placeholder object for end-of-list bindings. When we're matching an * argument list, we'll use this to represent the end of the list so that * we can match the "..." in any varargs functions in the multi-method * set that we're matching against. */ _multiMethodEndOfList: _MultiMethodPlaceholder ; /* ------------------------------------------------------------------------ */ /* * Register a multi-method. * * The compiler automatically generates a call to this function during * pre-initialization for each defined multi-method. 'baseFunc' is a * pointer to the "base" function - this is a stub function that the * compiler generates to refer to the whole collection of multi-methods * with a given name. 'func' is the pointer to the specific multi-method * we're registering; this is the actual function defined in the code * with a given set of parameter types. 'params' is a list of the * parameter type values; each parameter type in the list is given as a * class object (meaning that the parameter matches that class), nil * (meaning that the parameter matches ANY type of value), or the string * '...' (meaning that this is a "varargs" function, and any number of * additional parameters can be supplied at this point in the parameters; * this is always the last parameter in the list if it's present). */ _multiMethodRegister(baseFunc, func, params) { /* if there's no hash entry for the function yet, add one */ local tab = _multiMethodRegistry.funcTab_; if (tab[baseFunc] == nil) tab[baseFunc] = new Vector(10); /* add the entry to the list of variants for this function */ tab[baseFunc].append([func, params]); /* add the mapping from the function to the base function */ _multiMethodRegistry.baseFuncTab_[func] = baseFunc; /* also add the function to the direct parameter table */ _multiMethodRegistry.funcParamTab_[func] = params; } /* * Build the method bindings. The compiler generates a call to this * after all methods have been registered; we run through the list of * registered methods and generate the binding properties in the * referenced objects. */ _multiMethodBuildBindings() { /* no errors yet */ local errs = []; /* * build a lookup table that maps function pointers to symbol names, * so we can look up our function names for diagnostic purposes */ local nameTab = new LookupTable(128, 256); t3GetGlobalSymbols().forEachAssoc(function(key, val) { /* if it's a function, store a value-to-name association */ if (dataType(val) == TypeFuncPtr) nameTab[val] = key; }); /* run through each entry in the method table */ _multiMethodRegistry.funcTab_.forEachAssoc(function(baseFunc, val) { /* look up the base function's name */ local name = nameTab[baseFunc]; /* add this to the saved name table */ _multiMethodRegistry.funcNameTab_[baseFunc] = name; /* note the number of registered instances of this function */ local funcCnt = val.length(); /* * Assign the initial binding property for this function. This * is the property that gives us the binding for the first * variant argument. Each unique multi-method (which is defined * as a multi-method with a given name and a given number of * parameters) has a single initial binding property. */ local initProp = t3AllocProp(); /* * store the binding starter information for the function - to * find the binding on invocation, we'll need the initial binding * property so that we can trace the argument list */ _multiMethodRegistry.boundFuncTab_[baseFunc] = initProp; /* build the argument binding tables */ for (local i = 1 ; i <= funcCnt ; i++) { /* get the function binding */ local func = val[i][1]; /* get the formal parameter type list for this function */ local params = val[i][2]; local paramCnt = params.length(); /* * If the last formal isn't a varargs placeholder, then we * must explicitly find the end of the list in the actual * parameters in order to match a call. To match the end of * the list, add the special End-Of-List placeholder to the * formals list. * * This isn't necessary when there's a varargs placeholder * because the placeholder can match zero or more - so it * doesn't matter where the list ends as long as we get to * the varargs slot. */ if (paramCnt == 0 || params[paramCnt] != '...') { params += _multiMethodEndOfList; ++paramCnt; } /* start at the initial binding property */ local prop = initProp; /* run through the parameters and build the bindings */ for (local j = 1 ; j <= paramCnt ; j++) { /* get this parameter type */ local origTyp = params[j], typ = origTyp; /* * If the type is nil, it means that this parameter slot * can accept any type. So, map the slot to the generic * Object type - this will catch everything, since we * handle non-objects by mapping them to the * _multiMethodNonObjectBindings placeholder object, * which like all objects inherits from Object. This * means we'll match argument values that are objects or * non-objects, thus fulfilling our requirement to match * all values. * * If the type is the string '...', it means that this is * a varargs placeholder argument. In this case, we need * to set up a match for the generic Object, in case we * have one or more actual arguments for the varargs * portion. This will also automatically match the case * where we have no extra arguments, because in this case * the matcher will try to match the End-Of-List * placeholder object _multiMethodEndOfList, which (as * above) inherits from Object and thus picks up the * any-type binding. * * The one tricky bit is that when we have a parameter * explicitly bound to Object, or an explicit End-Of-List * flag object, we'll get an undesired side effect of * this otherwise convenient arrangement: we'll * effectively bind non-object types to the Object by * virtue of the inheritance. To deal with this, we'll * explicitly set the placeholders' binding to nil in * this situation - this makes non-object types * explicitly *not* bound to the function, overriding any * binding we'd otherwise inherit from Object. */ if (typ == nil || typ == '...') typ = Object; /* * Figure the binding. * * - If this is the last parameter, it's the end of the * line, so bind directly to the function pointer. * * - If this isn't the variant parameter, the binding is * the next binding property. At invocation, we'll * continue on to the next argument value, evaluating * this next property to get its binding in the context * established by the current argument and property. The * next binding property is specific to the current class * in the current position, so we might already have * assigned a property for it from another version of * this function. Look it up, or create a new one if we * haven't assigned it already. */ local binding; if (j == paramCnt) { /* end of the line - the binding is the actual function */ binding = func; /* * if this type is already bound to a different * definition for this function, we have a * conflicting definition */ if (typ.propDefined(prop, PropDefDirectly) && typ.(prop) != binding) errs += [baseFunc, func, params, name]; } else { /* check for an existing binding property here */ if (typ.propDefined(prop, PropDefDirectly)) { /* we already have a forward binding here - use it */ binding = typ.(prop); } else { /* it's not defined here, so create a new property */ binding = t3AllocProp(); } } /* set the binding */ typ.(prop) = binding; /* * As we mentioned above, if the original type is * explicitly Object, we *don't* want to allow non-object * types (int, true, nil, property pointers, etc) and * End-Of-List placeholders to match - without some kind * of explicit intervention here, the placeholders would * match by inheritance because the placeholders are just * objects themselves. To handle this properly, set the * non-object placeholder bindings explicitly to nil. */ if (origTyp == Object) _MultiMethodPlaceholder.(prop) = nil; /* * if there's another argument, this binding is the * binding property for the next argument */ prop = binding; } } }); #ifdef MULTMETH_STATIC_INHERITED /* * If we're operating in static inheritance mode, cache the * next-override information for inherited() calls. Since we're * using static inheritance, we can figure this at startup and just * look up the cached information whenever we need to perform an * inherited() call. */ _multiMethodRegistry.funcTab_.forEachAssoc(function(baseFunc, val) { /* get the binding property for the base function */ local prop = _multiMethodRegistry.boundFuncTab_[baseFunc]; /* run through the functions registered under this function name */ for (local i = 1 ; i <= val.length() ; i++) { /* get this function binding and the type list */ local func = val[i][1]; local params = val[i][2]; local paramCnt = params.length(); /* * Add the end-of-list marker if applicable. For a varargs * function, add one generic Object parameter in place of the * variable list - but we'll check later to make sure that * any match we find is really varargs, since a varargs * function can only inherit from another varargs function. * (This is because, in order to actually invoke the * inherited function from an overrider, the callee must be * varargs to be able to handle varargs from the caller.) */ local varargs = (paramCnt != 0 && params[paramCnt] == '...'); if (varargs) params[paramCnt] = Object; else params += _multiMethodEndOfList; /* look up the inherited version of the method */ local inh = _multiMethodInherit(func, prop, params); /* varargs can only inherit from varargs */ if (inh != nil && varargs) { /* look up the inherited parameters */ local inhParams = _multiMethodRegistry.funcParamTab_[inh]; local inhParamCnt = inhParams.length(); /* make sure it's varargs, too */ if (inhParamCnt == 0 || inhParams[inhParamCnt] != '...') inh = nil; } /* remember the inherited function */ _multiMethodRegistry.inhTab_[func] = inh; } }); #endif /* MULTMETH_STATIC_INHERITED */ /* we're done with the source bindings - discard them to save memory */ _multiMethodRegistry.funcTab_ = nil; _multiMethodRegistry.funcParamTab_ = nil; } /* * Multi-method registry. This is where we keep the registry information * that we build during initialization. */ _multiMethodRegistry: object /* table of registered functions, indexed by base function */ funcTab_ = static new LookupTable(128, 256) /* table of function parameter lists, indexed by function */ funcParamTab_ = static new LookupTable(128, 256) /* function name table */ funcNameTab_ = static new LookupTable(64, 128) /* base function -> initial binding property */ boundFuncTab_ = static new LookupTable(64, 128) /* function -> base function */ baseFuncTab_ = static new LookupTable(64, 128) #ifdef MULTMETH_STATIC_INHERITED /* table of cached inherited() information, indexed by function */ inhTab_ = static new LookupTable(64, 128) #endif ;
TADS 3 Library Manual
Generated on 5/16/2013 from TADS version 3.1.3
Generated on 5/16/2013 from TADS version 3.1.3