The jampack Composition Tool

jampack is a tool to compose .jak files that define a single refinement chain.  The Composition document presents the big picture of how and where jampack is used, the syntax and semantics of .jak files, and how jampack differs from the mixin composition tool. This document discusses detailed use and features of jampack.

General rules of formatting and composition that are shared by mixin and jampack are described in the Composition document

Command-Line Invocation

To call jampack from the command line:

> jampack
Usage: jampack [options] baseFile extensionFile1 extensionFile2 ...
Options: -t type-sort interface declarations
         -k key-sort class field declarations
         -a <layerName> name of layer to generate
         -f <fileName> name file to generate
         -l label classes/methods with feature source

where:

Interface Composition Rules

The rules for interface composition that are specific to jampack are:

An example root interface and refinement are:

layer Iroot1;

import x.y.z.*;

interface MyInt extends FooInterface {
   int Silent = 0;
   void foo() throws AFit;
   SomeType bar( int x );
}

layer Iext1;

import java.io.Serializable;

public transient refines interface MyInt extends yyyInterface {
   int Terse = 2;
   void foo() throws SomeException;
   int increment( int i );
}

The result of their composition by jampack is shown below.  Highlighted in yellow are additions made by the MyInt refinement.

layer Foo;

import x.y.z.*;
import java.io.Serializable;

public transient interface MyInt extends FooInterface, yyyInterface {
   int Silent = 0;
   int Terse = 2;
   void foo() throws AFit, SomeException;
   SomeType bar( int x );
   int increment( int i );
}

Note: jampack merges modifier lists.  That is, the definition of MyInt in IRoot1 has no modifiers and the refinement in Iext1 adds the public and transient modifiers.  mixin does not merge modifier lists.  So if you want compatibility between mixin and jampack, avoid using modifiers in refinements.

Type sorting is a linear algorithm that sorts interface declarations.  It is only present in jampack and is invoked by the -t option command-line option. To see why type sorting is useful, consider the result if type sorting is not used (see below): it is a jungle of definitions that are hard to understand.  Type sorting collects all variable definitions together in one spot and method definitions in another, and makes a small contribution to generated code beautification.

public transient interface MyInt extends FooInterface, yyyInterface {
    int Silent = 0;
    void foo() throws AFit, SomeException;
    SomeType bar( int x );
    int Terse = 2;
    int increment( int i );
}

Exercise

Create separate files A.jak and B.jak to hold the above definitions.  Compose them into C.jak by the command:

> jampack -t -a Foo A.jak B.jak > C.jak

Remove -t to turn off type sorting.

Caveat. Note: the file naming convention used in this example isn't typical.  Usually A.jak would be a file in the IRoot1 layer (i.e., it would have the pathname IRoot1/MyInt.jak) and Iext1.jak would be a file in the Iext1 layer (i.e., it would have the pathname Iext1/MyInt.jak).  So a more typical invocation would be:

> jampack -a Foo IRoot1/MyInt.jak IIext1/MyInt.jak > C.jak

Class Composition Rules

The rules of class composition that are specific to jampack are:

The following sections discuss how jampack composes .jak files.  The ideas actually are general and apply to mixin as well.  So it is recommended that you read both documents.

An example of a base class file and an refinement class file are:

layer Ctop;

import jakarta.util.*;

class top {
   static int i,j;
   top() { ii = 5; }
   void foo(float x, float y) { /* do something */ }
   float bar( float x ) { /* do something */ }
}

layer Cmid;

import AnotherPackage;

refines class top implements java.io.Serializable, xxx {
   static int k;
   static { j = 5; }

   top(float x) { /* do something */ }

   float foobar() { Super(float).bar(4.0); Super(float,float).foo(0, 0); }

   public void foo( float x, float y ) { /* something more */ }
}

The result of their jampack composition is shown below.  Highlighted in yellow are additions made by the refinement:

layer Foo;

import jakarta.util.*;
import AnotherPackage;

class top implements java.io.Serializable, xxx {
   static int i,j;
   static int k;
   static { j = 5; }

   top() { ii = 5; }

   top(float x) { /* do something */ }

   float bar( float x ){ return bar$$Ctop( x ); }
   final float bar$$Ctop( float x ) { /* do something */ 
   final void foo$$Ctop(float x, float y) { /* do something */ }

   public void foo( float x, float y ) { /* something more */ }

   float foobar() { bar$$Ctop(4.0); foo$$Ctop(0, 0); }
}

Key sorting is a technique much like type sorting.  The body of a class has different kinds of entities -- variable declarations, method declarations, etc. Key sorting is similar to type sorting in that all entities of a single type (initialization blocks, variable declarations, methods) are grouped together.  Key sorting goes beyond this to sort methods.  In particular, we expect that there will be many "variations" of a single method, such as bar, bar$$Ctop, etc.  Key sorting groups all of these related methods together, as shown above.  (It does this by assigning a key to each method and sorting the methods in key order.  Hence the name "key sort"). If key sorting is not used, an unintelligible jungle of declarations is produce.  (Try the above example without the -k option to see).

class top implements java.io.Serializable, xxx {
   static int i,j;
   static int k;
   top() { ii = 5; }
   top(float x) { /* do something */ }
   final void foo$$Ctop(float x, float y) { /* do something */ }
   float bar( float x ){ return bar$$Ctop( x ); }
   final float bar$$Ctop( float x ) { /* do something */ }
   float foobar() { bar$$Ctop(4.0); foo$$Ctop(0, 0); }
   public void foo( float x, float y ) { /* something more */ }
   static { j = 5; }
}

There are special language features, such as Super, that you need to understand and use when defining class refinements. We explain Super and how these files are produced in the following sections.  

Exercise

Create separate files top.jak and mid.jak to hold the definitions above.  Compose them into combined.jak by the command:

> jampack -k -a Foo top.jak mid.jak > combined.jak

Remove the -k option to turn off key sorting.  Don't forget the caveat.

Rules for Variable Composition

There is no notion of variable refinement in Java.  It is one thing to override a method; it makes no sense to override a variable.  Whenever a parent class defines a variable and a refinement class attempts to define the same variable, an error is reported by jampack

This begs the question of inadvertent capture.  What if a temporary variable x is defined in the root class and a different variable, also named x, is defined in the refinement class?  As mentioned above, jampack will complain.  The clashing of names for temporary variables should be handled automatically.  In a later section, we show how local declarations avoid inadvertent capture.

Rules for Method Composition

You may have noticed that composing classes is a lot more complicated than composing interfaces.  The difficulty rests on the ability of methods of an refinement class to call arbitrary methods of their "superclass" or rather, "parent".  And part of the difficulty arises because jampack is a relatively simple preprocessor which doesn't have full type information about a program it is composing. 

You know that a subclass can call method foo() of its superclass by invoking super.foo().  By analogy, a class refinement can call method foo() of its super refinement by  invoking Super().foo().  Because jampack is a preprocessor that does not type check programs, if a refinement calls method of its parent with signature foo(int, float, String), it does so by invoking Super(int,float,String).foo(5, 3.4, "me").  The token Super is a reserved keyword.  Super is always followed by the type signature of the method that is to be invoked, and is the way the type signature of a "base-class" method is conveyed to jampack.

There are four rules that are specific to extending base-class methods. The actions taken depend on the following conditions:

If a parent method is:

  Before Composition After Composition in Composite Class
Parent class
void foo( ) { 
   /* do something */ 
}
final void foo$$baseAspect() { 
   /* do something */ 
}
void foo( ) { 
   foo$$baseAspect();
   // something more
}
Refinement class
void foo( ) { 
   Super( ).foo();
   // something more
}
  Before Composition After Composition in Composite Class
Parent class
void foo( ) { 
   /* do something */ 
}
 
void foo( ) { 
   // something more
}
Refinement class
void foo( ) { 
   // something else
}
  Before Composition After Composition in Composite Class
Parent class
void foo( ) { 
   /* do something */ 
}
final void foo$$parentAspect( ) {
   /* do something */
}
void foo( ) {
   foo$$parentAspect();
}
void bar( ) { 
   foo$$parentAspect();
   // something more
}
Refinement class
void bar( ) { 
   Super().foo();
   // something more 
}

There is one rule for refinement methods: if an refinement method does not override a base method, it is merely added to the composite class.

  Before Composition After Composition in Composite Class
Parent class
 
void biff( ) { 
   // something here
}
Refinement class
void biff( ) { 
   // something here
}

Rules for Composing Constructors

There are all sorts of problems with constructors.  There are so many that it is much safer NOT to allow constructors to be refined.  This is the rule for both mixin and jampack.  New constructors can be added by an refinement, but no existing constructor can be refined.  

Constructors can be refined using the concepts already presented. The body of a constructor is moved into a method, which can be refined.  Thus, instead of creating a superclass with a constructor and later extending the constructor via inheritance (left-hand- side of the table below), we rely on a design and coding technique to accomplish the same effect (right-hand-side of the table below).  Below, foo is the name of a class and fooConstructor is the name of the method that contains the body of the foo constructor:

Inheritance Hierarchy Refinement Chain Equivalent
class foo {
   int a;
   foo(int a) {
      this.a = a;
   }
}
class foo {
   int a;
   void fooConstructor(int a) {
      this.a = a;
   }
   foo(int a) {
      fooConstructor(a);
   }
}
class subfoo extends foo {
   int b;
   foo(int a) {
      super(a);
      b = 0;
   }
}
refines foo {
   int b;
   void fooConstructor(int a) {
      Super(int).fooConstructor(a);
      b = 0;
   }
}

Exercise

Create some example classes to compose.  Follow the rules for designing constructors, methods, and variables.  Remember that each file to compose must belong to an layer(i.e., have an layer declaration) with a unique name -- i.e., the name of the layer from which it belongs.  Failure to follow these rules will result in incorrect code being generated.

State Machine Composition Rules

State machine composition rules are:

Consider the following root state machine and refinement:

layer rootSm;

State_machine root {
   Delivery_parameters( M m );
   Unrecognizable_state { ignore(m); }

   States g, h, i;

   Transition e1 : g -> h condition m!=null do { gh(); }
}

layer extSm;

refines State_machine root {
   States j, k;

   Transition e3 : g -> j  condition m!=null do { Super(int).anotherAction(6); }
}

jampack composes the above files to yield the specification below.  The lines in yellow were added by the state machine refinement:

layer Foo;

State_machine root {
   Delivery_parameters( M m );
   Unrecognizable_state { ignore(m); }

   States g, h, i;
   States j, k;

   Transition e1 : g -> h condition m!=null do { gh(); }

   Transition e3 : g -> j condition m!=null do { super.anotherAction(6); }
}

Note that part of a root or refinement state machine specification is a set of data member, methods, etc.  The rules for composing these are exactly the same as those for composing class declarations, explained above.  So options like key-sorting are relevant.

Exercise

Create separate files root.jak and mid.jak to hold the above definitions.  Compose them into result.jak by the command:

> jampack -tk -a Foo root.jak mid.jak > result.jak

Don't forget the caveat.

Local Declarations

Local_Id declarations are used to avoid inadvertent capture. Consider the following root specification with local identifiers i, j, ii, jj, and foo.

layer Ctopp;

Local_Id i, jj, foo;

class topp {
   static int i;
   int jj;

   void foo(float x, float y) { i = jj = x+y; }
}

Now consider an refinement that uses exactly the same identifiers locally:

layer Cmidd;

Local_Id i, jj, foo;

refines class topp {
   static int i;
   int jj;

   void foo(float x, float y) { i = jj = x*y; }
}

jampack composes these two specifications to yield:

layer Foo;

class topp {
   static int i$$Ctopp;
   static int i$$Cmidd;
   int jj$$Ctopp;
   int jj$$Cmidd;

   void foo$$Ctopp(float x, float y) { i$$Ctopp = jj$$Ctopp = x+y; }

   void foo$$Cmidd(float x, float y) { i$$Cmidd = jj$$Cmidd = x*y; }
}

Note that jampack first assigns unique (mangled) names to local identifiers before composing specifications.  It doesn't matter if jampack is composing classes, interfaces, state machines or whatever.  The Local_Id feature works for them all, and it works the same way for mixin.

Exercise

Create separate files topp.jak and extp.jak to hold the definitions above.  Compose them into result.jak by the command:

> jampack -tk -a Foo topp.jak extp.jak > result.jak

Don't forget the caveat.


ATS Home Page

Copyright Software Systems Generator Research Group. All rights reserved.
Revised: January 18, 2008.