Citrus

Graphical structured editors for code and data have many benefits over editing raw XML, but they can be difficult and time-consuming to build using modern programming languages. Citrus is a new object-oriented, interpreted language that is designed to simplify the creation of such editors, by providing first-class language support for one-way constraints, custom events and event handlers, and value restrictions and validation. We're using Citrus to implement several novel software development tools.

Fortunately, these editors have many of the same architectural requirements:

We designed Citrus, a new programming language, to support these these architectural patterns as first class language constructs. Citrus is object-oriented, interpreted, reflective, statically-typed, and polymorphic.

objects and properties

Just like any other object-oriented language, at runtime Citrus programs consist of many objects, which have several fields that are pointers to other objects. This pointers are a single value, representing the location of an object in memory. In Citrus, these pointers are objects themselves. We call them properties. Properties have lots of additional meta-information in addition to their value, including listeners, restrictions, constraints, owners, and other information.

ownership

One unique feature of properties is that they either own or refer to their value. This was driven by the observation that the data in structured editors frequently represent trees or DAGs. Consequently, programmers frequently need to write code to determine what owns or contains an object (for example, a view's parent or a statement's block in an AST). They also frequently need to determine what references an object (for example, a method's callers or a model's views). To access this data, programmers have to implement custom functions or manually maintain references.

To minimize this work, in Citrus, properties have back-pointers to the object that owns them, and objects have a set of properties that point to them. Each object thus has a single property that owns it and a set of properties that refer to it. All of these backpointers are managed by the runtime automatically.

Let's look at some examples of where these backpointers become useful. The first example shows a label in a button. To programatically access the button from the label, we only need a single line of code

# The button that contains this label
label.owner

The second example shows an object in a graphical editor. We only need a single call to the ownerOfType method to look up the hierarchy of owners to find the ScrollView that contains the square.

# The ScrollView that contains this circle.
(circle ownerOfType ScrollView)

The third example shows a model and its three views. We can access all of the views that reference the model with this single call to the refsOfType method.

# Views of this object
(model refsOfType View)

To find all of Identifiers in the abstract syntax tree that reference the method, for example, as part of renaming the method, we only need this single call to refsOfType. ☛

# All Identifiers that refer to this method
(method refsOfType Identifier)

constraints

The central way to maintain relationships in Citrus is through constraints. Constraints are one-way, like the constraints in Amulet and Subarctic toolkits, but arbitrary code can be used, and dependencies are gathered automatically instead of having to explicitly declare them.

In this example, the dependencies include all properties accessed inside the previousView function, and the right property.

# This goes below the previous
left <- ((this previousView).right + 5.0)

Cycles can be handled by responding to a CycleDetected event (discussed shortly). You can prevent a dependency by peeking, using a special syntax.

events

Another way to maintain relationships in Citrus is to use events and event handlers. In most toolkits, events are implemented with callback functions that are manually added and removed by the programmer. In Citrus, all of this is handled automatically using the when construct:

when name (subject Event)
    response 

The subject is the object we're listening to and the Event is the event class that the object will broadcast. The name is a variable that stores the event object.

Citrus provides many primitive events. For example, objects declare deep and shallow structural change events, collections declare add and remove events, Any class can declare new events as an inner class, whose properties are the event parameters. An instance of these inner classes can be sent as an event to any objects that are listening to the object.

Let's take a look at a few examples. Here's an event handler that handles the PropertyValueChanged event, which is sent by any object when one of its property changes values.

a DataType is an Object that
  ...
  owns Stack globalUndoStack = (a Stack)
  when event (model PropertyValueChanged)
   (globalUndoStack push event)
  ...

This could be used to implement an undo stack. In the second example, the view shown here declares a custom FileSelected event, and this handler reacts to it by setting the background of the old selection to nothing and the new selection to orange.

when event (editor FileSelected)
(do
 (event.old.@background set nothing)
 (event.new.@background set Color.orange)
)

restrictions

One common use for constraints is restrict a variable's value. In many of these cases, it should restrict its value but it shouldn't determine it. For example, a zip code text field should be restricted to legal zip codes, but it shouldn't constrain the field to a specific zip code.

Therefore, Citrus allows properties to have value restrictions, which are expressions that a property's value must satisfy:

var = value
  for which Expression 
  [ otherwise Expression ]

Any dependencies in this expression are captured automatically, so that if the conditions of validity change, the validity is updated automatically. The value restriction can either fix the value or raise an event that can be handled by other code.

Here's an example of restricting a text field's caret to valid indices:

has Int caretIndex = 0
 for which (caretIndex >= 0) 
   otherwise 0
 for which (caretIndex <= (text length)) 
   otherwise (text length)

Here's another example of restricting a text string to a valid integer token:

owns Text integer = "0"
 for which (integer matches "[+-]?[0-9]+")

when event (@integer ValidityChanged)
 (@background set
   (if (@integer isValid) grey red))

In addition to restricting the single value of a property, entire Citrus objects can also have restrictions based on their property's values:

 
rule name Expression

Just like with properties, these restrictions are expressed as boolean-valued expressions, and dependencies in the expressions are captured automatically.

Consider this address form dialog.

an AddressForm is a Form
 ...
 rule fieldsAreValid
  ((@first isValid) and (@last isValid)
   (@street isValid)(@city isValid)
   (@state isValid)) (@zip isValid))
 
 refs okayButton=
   (a Button 
     enabled <-
       (fun Bool [] fieldsAreValid))
 ...

The form has an object restriction named fieldsAreValid, which states that all of the properties of the form, the first and last name, the street, the city, and so on, must all be valid. We then constrain the OK button's enabled property to this rule, so that when the rule is complied with, the okay button becomes enabled automatically. City, state and zip start out as invalid, but as soon as they become valid, the okay button becomes enabled.


Copyright © 1996-2020 - Carnegie Mellon University - All Rights Reserved.