RuleWorks
Program and Data Modularity
RuleWorks makes possible explicit partitioning of rules and objects into modular systems or subsystems. The basic unit of modularity is the block; RuleWorks provides three block constructs:
Note : Blocks cannot be nested; no block may contain another block.
The most important type of block is the entry block. The entry block allows a rule-based routine to be called like a routine in any other language.
The purpose of declaration and rule blocks is to allow controlled sharing of information. Declaration blocks enable declarations and the objects described by them to be shared by multiple entry or rule blocks with the USES clause (see Example 5-1). Rule blocks enable rules to be shared among multiple entry blocks with the ACTIVATES clause. Except for this sharing, the contents of one block are not visible to other blocks.
Each block begins with a block construct that defines the block name, and ends with an END-BLOCK construct. A block is said to contain all the constructs between its block construct and its END-BLOCK construct. All non-block RuleWorks constructs must be contained in a block.
Table 5-1. Summary of Block Constructs
Entry Block | Rule Block | Declaration Block | |
Can be called from other languages | true | false | true, but* |
Name is visible to linker | true | true | true |
Can contain "ON-" statements | true | false | false |
Can contain rules and catchers | true | true | false |
Can contain declarations | true | true | true |
Contents can be shared at compile time | false | false | true |
* A declaration block can be called, but only for the purpose of initializing working memory and object classes before calling API routines that affect working memory.
Entry BlocksA RuleWorks entry block generates a C-callable entry point. An entry block is visible to your system linker and is callable by any language that adheres to the target system's calling standard conventions. An entry block can accept arguments and return a value. You can think of an entry block as a rule-based subroutine or "mini-expert."
At least one entry block is required for each RuleWorks program.
Only one entry block can be running at any given time. The entry block that is currently running is called the active entry block. Only rules contained in or activated by the active entry block can execute. Using entry blocks to divide your program into modules can therefore improve program performance, because the size of the match network and the conflict set are reduced.
Declaring An Entry Block
An entry block is declared by the keyword ENTRY-BLOCK followed by the name of the entry block. The entry block name must contain only letters, numbers, and underscores, and must be no longer than 31 characters.
The complete syntax of an entry block is shown below:
Figure 5-1. Entry Block Syntax
The clauses within the ENTRY-BLOCK declaration are all optional:
An entry block may contain any RuleWorks program constructs except another block construct, but they must be in the order shown above: declarations first, then ON- statements, then rules and catchers.
If an entry block contains OBJECT-CLASS declarations, objects of those classes can be matched only by rules contained in the block. If an entry block contains rules, those rules can fire only when the block is active. Declaration blocks and rule blocks allow you to share objects and rules among multiple entry blocks.
The END-BLOCK construct is required. The entry-block-name is optional inside the END-BLOCK construct, but if it is present the compiler verifies that it is the same name as in the ENTRY-BLOCK construct.
Calling an Entry Block
When an entry block is called, that entry block is the only active entry block. The entry block first runs its ON-ENTRY actions (if any), then runs recognize-act cycles until one of the following occurs:
Entry blocks can call other entry blocks, and even call themselves recursively. When an entry block is called, the caller is no longer active. The caller is referred to as being suspended and the called block becomes the active block, just as with a routine in any other language. Similarly, when the called block returns, the caller becomes active again.
When an entry block returns, any objects created or changed by that block remain in working memory (unless that entry block is the main program, see Chapter 5, Naming an Entry Block "Main", for details). If that entry block is called again later, all of those objects are again matchable. However, the objects cannot be matched or modified by any other entry block unless both entry blocks are using the same declaration block (see Chapter 5, Declaration Blocks, for information on declaration blocks).
Scope of Arguments to an Entry Block
The arguments received by an entry block are visible only to actions within the ON-ENTRY, ON-EVERY, ON-EMPTY, and ON-EXIT statements inside the entry block. They are not visible to rules contained in or activated by the entry block. If rules need to match input arguments, their values must be placed into one or more objects (as shown in Example 5-1)
Example 5-1. A Simple Entry Block and Declaration Block
; Shareable Class Declaration
(declaration-block numbers)
(object-class limit ^value)
(end-block numbers)
; Entry Point Declaration
(entry-block count_to
(accepts <num-arg> long)
(returns long)
(uses numbers))
; Private Class Declaration
(object-class iterator ^count)
; Executable Constructs
(on-entry
(bind <Limit> (make limit ^value <num-arg>))
(make iterator ^count 1))
(on-exit
(remove <Limit>) ; clean out WM when done
(return <num-arg>))
(rule increment-rule
(limit ^value <lim>)
(iterator ^$id <it> ^count { <num> <= <lim> })
-->
(modify <it> ^count (<num> + 1)) )
(rule now-done
(limit ^$id <limit-id> ^value <lim>)
(iterator ^$id <it> ^count > <lim>)
-->
(remove <it>)
(remove <limit-id>) )
(end-block count_to)
The same restrictions hold true of any variables bound in an ON- clause. Such variables are also visible within any ON- clause.
Scope of Execution of Entry Blocks
A call frame is created when a RuleWorks entry block is called. It contains all the dynamic data structures associated with a given invocation of an entry block. That is, it consists of all of the information "local" to this particular invocation of an entry block, or in some way visible to this particular invocation. Calls and returns really create and delete call frames. Call frames are an integral piece of entry blocks; by themselves they have no names and cannot be passed, returned, or otherwise manipulated directly.
The following are local to the call frame:
Refraction does not apply across invocations of entry blocks. Thus, a rule may fire more than once on the same data. This could happen when an entry block calls itself recursively, or when one entry block calls another and both activate the same rule block, or when an entry block is called repeatedly.
This affects CATCH statements. The CATCH counter counts only rules fired in the entry block invocation in which the AFTER action was executed, excluding rule firings in other invocations of the same entry block and in entry blocks called from the original entry block.
The following are global:
The $ID value of an object is universally unique across entry block invocations.
The time-tags assigned to objects are monotonically increasing across calls to and returns from entry blocks.
This affects the RUN command when you provide an argument (for example, RUN 5). Rule firings from other entry blocks are counted.
This affects the RuleWorks GENATOM and GENINT functions, and the rul_genint, rul_gensym, and rul_gensymp API routines. Every atom generated for any of these routines is unique while the program is running, and is unique across your entire program, not merely within an entry block.
Naming an Entry Block "Main"
An entry block named MAIN, if supplied, is automatically designated to the compiler and linker as the main RuleWorks routine. This design has semantic parallelism with the C language and provides the behavior most programmers would expect. The name can be in any case.
If you want to capture command-line arguments to the program in a portable way, a declaration in the following form is recommended:
(entry-block main
(accepts <argc> long ; traditional names for
<argv> [<argc>] ASCIZ) ; command-line args
...)
Returning a Value from an Entry Block
RuleWorks provides an RHS action, RETURN that stops the firing of rules in the active entry block, executes the ON-EXIT actions (if any) and passes control back to the caller of the entry block. This action has an optional argument, the value to be returned. The argument can be any expression. Thus, the RETURN action is useful for returning a condition code or value (see Example 5-1).
The RETURN action is valid anywhere in the entry block, even inside the ON-EXIT and ON-EMPTY statements. The RETURN action is valid only in an entry block; it is not valid in a rule block.
Executing more than one RETURN action in an entry block is possible, for example, when an ON-EXIT statement that contains a RETURN action is executed as a result of a RETURN action in a rule. If a value is being returned from the entry block, the value of the last RETURN action executed is used.
If the value returned by an entry block is an array, the memory allocated for that array is not freed by RuleWorks. Example 5-2 shows an entry block that accepts an array, sorts it, and then returns it.
Example 5-2. Passing and Returning an Array
(entry-block sort_slowly
(accepts <set-size> long
<set>[<set-size>] asciz)
(returns <sorted-set>[<set-size>] asciz))
; Return the input set, except sorted (albeit slowly)
(object-class an-atom ^value)
(object-class in-set ^values compound)
(object-class out-set ^values compound)
(on-entry
(make out-set)
(make in-set ^values <set>)
(for-each <atom> in <set>
(make an-atom ^value <atom>)
)
)
(rule find-next
(an-atom ^$id <atom> ^value <x>)
-(an-atom ^$id <> <atom> ^value <= <x>)
(out-set ^$id <out-set> ^values <out-vals>)
-->
(remove <atom>)
(modify <out-set> ^values (compound <out-vals> <x>))
)
(rule all-done
(out-set ^$id <out-set> ^values <out-vals>)
(in-set ^$id <in-set> ^values <in-vals>)
-(an-atom)
-->
(remove <out-set> <in-set>)
(write (crlf) | Sort of | <in-vals>
(crlf) | ==> | <out-vals> (crlf))
(return <out-vals>)
)
(end-block)
Executing Actions Without Matching
The actions on the right-hand side of a rule are executed only when the left-hand side matches working memory and the resulting instantiation is picked during conflict resolution. RuleWorks provides two types of executable constructs whose actions are executed without matching working memory: "ON-" statements and catchers.
Using "ON-" Statements
RuleWorks entry blocks may contain one each of the four "ON-" statements, which allow you to describe a set of actions that are executed at certain points in the recognize-act cycle (see the figure, "ON-" Statements and the Recognize-Act Cycle) without matching any objects in working memory. These new statements are all defined with names that begin with the prefix "ON-" and end with the name of the special condition under which their associated actions are executed.
Figure 5-2. "ON-" Statements and the Recognize-Act Cycle
The "ON-" statements are listed below:
The actions in an ON-ENTRY statement are executed whenever its entry block is called, and before any rules can fire. Thus, you can initialize working memory by putting MAKE actions in your ON-ENTRY statement. For example:
(on-entry
(bind <my_id> (make my_wmo)) ; Create the first WMO.
(bind <rule_count> 0) ; Initialize the counter...
(bind <return_status> good)) ; ... and the return value.
These MAKE actions can use the input arguments to the entry block (see the section of this chapter, Scope of Arguments to an Entry Block)
The ON-ENTRY statement is roughly analogous to the OPS5 STARTUP statement.
The actions in an ON-EVERY statement are executed immediately after each successful rule firing, and before the determination of the next rule to fire. If a rule is fired that has a RETURN as its last action, then control will be returned up to the caller, and the ON-EVERY actions will not be executed.
You could use an ON-EVERY statement to count the number of rules fired in the current invocation of the entry block, or to call an event handler. For example:
(on-every
(bind <rule-count> (<rule-count> + 1)))
The actions in an ON-EMPTY statement are executed whenever it is time to select the next rule to fire and there are no rules eligible to fire and thus the conflict set is empty. Note that the run-time system does not execute any recognize-act cycles after an ON-EMPTY statement, even if its actions create WMOs that satisfy one or more rules.
You could use an ON-EMPTY statement to return a failed status if the program should not have arrived at an empty CS. For example:
(on-empty
(quit $failure))
The actions in an ON-EXIT statement are executed just before control is returned to the caller of the entry block. These actions are executed when control is returned via a RETURN action or when the conflict set becomes empty. If an ON-EMPTY statement was also specified, the ON-EXIT actions are executed after the ON-EMPTY actions, and immediately before control is returned to the calling routine.
The ON-EXIT actions are not executed after a QUIT action or command.
ON-EXIT statements are useful for clean-up actions, such as removing dead instances of local object classes. For example:
(on-exit
(remove <my_id>)
(remove-every local)
(return <return_status>))
"ON-" statements must be contained in an entry block. They cannot appear inside a rule block, nor inside a rule group within an entry block (see Rule Blocks and Rule Groups for more information).
Note: Any variables bound in one "ON-" statement are available to all other "ON-" statements. These variables are not available to any rules.
An entry block can contain at most one of each type of "ON-" statement. It doesn't have to contain any of them.
Using a Catcher
A catcher is a list of actions that are executed after a specified number of recognize-act cycles have been executed. For example, if program execution is unattended, as in a batch job, a catcher can halt the program if it does not produce results within a specified limit.
You define a catcher with a CATCH statement, which includes a symbol and one or more actions. The symbol names the catcher, and functions as a label. A catcher's name must be unique; that is, it cannot be the same as the name of another catcher, rule, or rule group in the program. When the catcher fires, the actions are executed.
The following CATCH statement defines a catcher named FINISH, which consists of two actions, WRITE and HALT:
(catch finish
(write (crlf) | Finished. | )
(halt))
You enable a catcher with the AFTER action, which tells the run-time system when to execute the catcher. Specify the AFTER action with a positive integer and the name of the catcher you want to enable. The integer indicates the number of recognize-act cycles (of the current invocation of the entry block) that the run-time system is to execute before executing the specified catcher. For example:
(after 10 finish)
Only one catcher can be enabled at a time, per call frame. Therefore, when you enable a catcher, you disable the catcher currently enabled (if any). Catchers are automatically disabled after they have been executed.
Catchers may be contained in either entry or rule blocks. The catcher must be contained in the same block as the AFTER action that enables it.
Example 5-3 illustrates the use of two catchers, STARTER and FINISH.
Example 5-3. A Program That Loops
(entry-block sample)
(object-class start) ;Used for initialization
(object-class number ^value) ;Contains value to be printed
(on-entry
(make start) ;Creates a working-memory object (START)
(after 1 starter)) ;Enables catcher STARTER after 1 recognize-act cycle
(2) (catch starter ;Catcher STARTER
(write (crlf) | Counting to 10... | )
(make number ^value 1)
(after 10 finish)) ;Enables catcher FINISH after the run-time
;system has executed 10 more cycles
(4) (catch finish ;Catcher FINISH
(write (CRLF) | Finished. | )
(return)) ;Stop program
(1) (rule initialize ;Initialize working memory
(start ^$id <START>)
-->
(write (crlf) | Starting... | )
(remove <start>))
(3) (rule count ;Output numbers
(number ^$id <number> ^value <n>)
-->
(write (crlf) (rjust 5) <n>)
(modify <number> ^value (<n> + 1)))
(end-block sample)
This program produces the following output:
Example 5-4. A Program that Loops Output
Starting...
Counting to 10...
1
2
3
4
5
6
7
8
9
10
Finished.
STARTER and FINISH are used in Example 5-3 as follows:
(1) The first recognize-act cycle fires rule INITIALIZE, because its CE matches the START object created in the ON-ENTRY statement. The ON-ENTRY statement does not count as a cycle.
(2) Catcher STARTER fires after one recognize-act cycle has been executed. STARTER is enabled in the ON-ENTRY statement.
(3) The MAKE action in catcher STARTER creates an object on which rule COUNT can fire.
(4) The AFTER action in catcher STARTER enables catcher FINISH to fire after ten more recognize-act cycles have been executed.
Declaration Blocks
Declarations (OBJECT-CLASS and EXTERNAL-ROUTINE) can be private or shareable. A declaration is private if it is contained in either an entry block or a rule block. Objects whose class declaration is private to a block can be matched only by rules contained in that block. In other words, by placing declarations inside an entry block or rule block you create private data for that block. Similarly, external routines whose declarations are local to a block can be called from inside that block only.
Figure 5-3 shows a RuleWorks program that consists of one entry block with two private object class declarations. Rules in EB1 can "see" all objects of classes Y and Z.
Figure 5-3. Private Data in RuleWorks
Data Partitioning
By default, RuleWorks partitions working memory so there is no conflict over object classes of the same name when two or more entry blocks are combined. Rules can match only objects whose classes are contained in or used by their entry block; all other objects are invisible.
This invisibility includes matches against the built-in class $ROOT. If an object class is not visible at compile-time, instances of it are not visible to the block at run-time.
Declaration Sharing
A declaration block allows you to create a collection of declarations that are shareable among multiple entry blocks or rule blocks. Declaration sharing allows you to explicitly decide which data should remain private and which should be shared (and the extent of that sharing). This allows the absolute partitioning of object class declarations between several independently-developed subsystems of an application. It also allows information to be restricted to a single routine or a set of interdependent routines.
A declaration block consists of zero or more declarations bounded by a DECLARATION-BLOCK construct at the top and an END-BLOCK construct at the bottom.
Example 5-5. DECLARATION-BLOCK Sharing
(DECLARATION-BLOCK line-items)
(OBJECT-CLASS item ^item-code
^item-name
^quantity
^price-per
^item-total)
(OBJECT-CLASS shippable-item
(INHERITS-FROM item)
^part-number)
(END-BLOCK line-items)
The complete syntax of a declaration block is shown below:
(DECLARATION-BLOCK decl-block-name)
[ class-or-external-declaration ] ...
The decl-block-name is required in the DECLARATION-BLOCK construct. It is optional in the END-BLOCK construct, but if supplied it is checked. Declaration block names must be no longer than 31 characters, and contain letters, digits, and underscores only. Declaration block names must be distinct from entry and rule block names. Finally, the first eight characters of all declaration block names used in a program must be unique. This allows RuleWorks to create portable names for the compiled files. For example, having two declaration blocks named DECLARE_CONTROL and DECLARE_KIWI generates a compile-time warning and results in a single file called DECLARE_.USE. Naming the blocks CONTROL_DECLS and KIWI_DECLS correctly generates two .USE files.
Note: Object classes that are related by inheritance must all be declared in the same block. An object class cannot inherit from a class declared in some other block.
A declaration block must not contain any executable statements (rules, "ON-" statements, or catchers).
Declarations are shared via the USES clause of an ENTRY-BLOCK or RULE-BLOCK construct. Objects whose class declarations are shared by a block are just as visible to the rules within that block as objects whose declarations are private to that block. Note that a USES clause cannot specify individual class names, only declaration block names.
A block can use more than one declaration block. A compile-time error occurs if the combined shared and private declarations contain any classes with identical names.
Figure 5-4 shows some private and some used object class declarations. The USES clause in EB1 "pulls in" the declarations from DB1. Rules in EB1 can still match objects of classes Y and Z.
Figure 5-4. Shareable Declaration Blocks
Figure 5-5 shows two entry blocks in the same program, each with some private and some used object class declarations. Rules in EB1 can see objects of classes Y and Z only; rules in EB2 can see objects of classes W and X only.
Figure 5-5. Two Shareable Declaration Blocks
Figure 5-6 shows the same two entry blocks sharing an object class declaration. Rules in EB1can see objects of classes Y, Z, and O; rules in EB2 can see objects of classes W, X, and O.
Figure 5-6. Shared Data in RuleWorks
The declaration block(s) used by an entry block must be compiled before the entry block itself can be compiled. You can put declaration blocks in a different file and compile them separately, or you can place them in the same file but above the entry block. In either case, compiling a declaration block results in an intermediate file with the extension .USE. Entry or rule blocks in other source files can subsequently use one or more of those declaration blocks, without seeing all of the other declarations that were in the original source file.
Example 5-6 shows a more complex set of block constructs where both declarations and rules are being shared.
Example 5-6. Sharing Declarations and Rules
(declaration-block common_decls)
(object-class C-1 ... )
(object-class C-2 ... )
(end-block common_decls)
(entry-block my_little_function
(accepts ... )
(activates shared-rules-1 )
(uses common_decls ) )
; needed to expose the contents of the
; declaration-block defined above
(rule my-rule-1 ... )
...
(end-block my_little_function)
(rule-block shared_rules_1
(uses common_decls))
(rule my-rule-1 ... )
...
(end-block shared_rules_1)
(rule-block shared_rules_2
(uses common_decls))
.
.
.
(end-block shared-rules-2)
(entry-block my_other_little_function
(accepts ... )
(activates shared-rules-1 shared-rules-2)
(uses common_decls))
(rule my-rule-1 ... )
...
(end-block my_other_little_function)
Calling a Declaration Block
Declaration blocks are callable, and in certain circumstances it may be necessary to call one. For example, the following C program calls an entry block named KBT_RULES that uses a declaration block named KBT_DECL. In order for the C program to initialize working memory before calling the entry block, it must first call the declaration block:
Example 5-7. Calling a Declaration Block
#include <stdio.h>
#include <rul_rtl.h>
main ()
{
/* define RuleWorks stuff */
rul_atom obj_id;
printf("Calling RuleWorks...");
/* initialize working memory */
KBT_DECL();
/* make one object */
rul_make_instance("(AnyWin ^name testing)","KBT_DECL");
/* call RuleWorks entry block */
kbt_rules();
}
Rule Blocks
In RuleWorks, rules can be gathered together into rule blocks. A rule block is a collection of rules that may be shared among several entry blocks. Whenever any of the entry blocks is called, all the rules in the rule blocks it activates will participate in matching and be enabled to fire.
Rule blocks can also be used when the number of rules in a single entry block becomes too large to reasonably store in a single file. You can have rule blocks that are activated by only one entry block.
The complete syntax of a rule block is shown in the following figure.
Figure 5-7. Complete Syntax of Rule Block
Rule blocks are activated by entry blocks with the ACTIVATES clause. Only rules contained in or activated by the active entry block are eligible for matching and firing. Only entry blocks can activate rule blocks; one rule block can neither contain nor activate another.
The rule-block-name is required in the RULE-BLOCK construct. It is optional in the END-BLOCK construct, but if supplied it is checked. Rule block names must be no longer than 31 characters, and contain letters, digits and underscores only. Rule block names must be distinct from entry and declaration block names.
Each rule block can have it’s own STRATEGY clause. However, all rule blocks used by an entry block must have the same strategy as the entry block. It is a run-time error to activate rule blocks that have different strategies. If no strategy clause is specified, the default is MEA.
A rule cannot be in more than one rule block; a rule block can contain zero or more rules. (An empty rule block can be useful during prototyping and/or stuibbing phase of development). Note that all rules contained in a block must be in the same file, but a file can contain more than one block. Rule blocks can be compiled before or after the entry block that activates them.
Note : A rule block is not a directly callable entity, and should not be called from any language except RuleWorks. If a rule block is not referenced by at least one entry block, it’s rules can never fire. No rules can be shared among multiple entry blocks unless they are contained within a rule block.
Rule blocks are activated by entry blocks by the ACTIVATES clause (see Entry Blocks). Only rules contained in or activated by the active entry block are eligible for matching and firing. Only entry blocks can activate rule blocks; one rule block can neither contain nor activate another.
The visibility of objects to the active rules depends on whether their blocks contain or use the corresponding OBJECT-CLASS declarations. When you put rules in rule blocks, it is up to you to set up declaration blocks in such a way that the classes that are to be matched and modified in your entry and rule blocks are shared as appropriate. There is no implicit sharing of declarations between an entry block and the rule blocks it activates. Thus, in the example, Sharing Declarations and Rules, the clause is required in the rule block as well as in the entry blocks.
Scope of Names
In RuleWorks, the name space for declarations and executable statements is not global. This name space is divided by blocks into independent name spaces.
This name space is enforced by the RuleWorks compiler, and permits names to be any legal symbol.
When an entry block activates rule blocks, the entry block and all the rule blocks still have separate name spaces.
When an entry block or a rule block uses declaration blocks, the using block and all the used blocks have one common name space for object classes and external routines.
Rule Groups
Within an entry or rule block, an additional level of structure can be imposed on a collection of rules by using the RULE-GROUP construct. This extra level is not necessary for program execution, but it can enable some useful debugging information.
Efficiency Issues
The entry block system in RuleWorks may cause a program speed increase because it restricts the visibility of rules and objects to what you specified, rather than the global visibility of OPS5.
Only those programs that actually use the block system will see the efficiency improvement. Programs that are converted from OPS5 by wrapping a single entry block around all of the rules will not see this improvement.
The entry block system may impose an efficiency penalty when entry blocks are called repeatedly. To avoid this problem, you should compile any entry block that is called repeatedly with the Optimize qualifier set to REINVOCATION. Note: that the RuleWorks language semantics are not affected by this qualifier, only entry block initialization run-time and maximum memory usage.
Partitioning working memory with declaration blocks will, if done appropriately, provide a significant improvement in execution speed.