Friday, May 19, 2006

Syntax Checking an internal DSL

"What about syntax checking?" is the most common question I receive concerning Domain Specific Languages. In my experience it depends on what level of freedom you want to give your users.

The simplest form of syntax checking I usually see is ruby syntax verification:
def valid_ruby?(text)
begin
ReceiveAny.new.instance_eval(text)
true
rescue SyntaxError => syn_err
yield "error on line #{extract_line_number(syn_err.message)}" if block_given?
false
end
end
In the above code ReceiveAny relies on method missing magic to allow any method call to work.

Verifying method calls and parameters is the next common level of syntax checking. Verification at this level can be achieved by evaluating the script in a syntax checking context.

In my previous example of evaluating a script in various contexts I used a script that represented some of the business rules of a poker room. Building from that example I have a similar script that clearly contains errors.
if the '$1-$2 No Limit' list is more than a then notify the floor to open
if the '$1-$2 No Limit' list is more than 1 then notify the floor to opeeen
if the '$1-$2 No Limit' list is more than z then ntify the floor to open
if the '$1-$2 No Limit' list is more than 1 then notify the floor to open
When syntax checking you could validate every method call; however, (especially if you are using bubble methods) you will more likely only need to validate key methods. For example you would want to validate that the more method was given a numeric value.
  def more(number)
@errors << "more than should be followed by a number" unless number.kind_of?(Numeric)
end
After validating methods and parameters, the last method to execute should return all reported errors.
  def notify(arg)
@errors.join(', ')
end
However, the last method executed could also be an invalid method (see line 2 of the sample invalid script). Because of this, method_missing should also return the errors.
  def method_missing(sym, *args)
if sym.to_s =~ /^dollar\d+dashdollar\d+space(Nospace)*Limit/
return true
end
@errors << "#{sym.to_s} is an invalid keyword"
@errors.join(', ')
end
In the example each line is executed individually. Because of this the returned errors will be specific to only one line.
    rules.each_with_index do |rule, index|
result = self.new.instance_eval(rule)
yield result, index if block_given? && result.any?
end
A benefit to executing each line individually is that errors can be reported with their associated line numbers.
Syntax Check:
errors on line 0, (a is an invalid keyword, more than should be followed by a number)
errors on line 1, (opeeen is an invalid keyword)
errors on line 2, (z is an invalid keyword, more than should be followed by a number, ntify is an invalid keyword)
Sample code for the SyntaxCheckingContext is available here.

The next common level of syntax checking I have encountered is using regular expressions to validate the structure of the script. This is the most extreme in my opinion because it either requires very complex regular expressions, or it limits the user from taking advantage of the underlying language (Ruby in my examples). I won't go into much detail on this subject because I haven't had the need for it personally. However, it is an option to be aware of.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.