2009-04-26

A Dirt-Simple DSL In REBOL

REBOL allows the creation of domain-specific languages (DSLs) using the parse function. It takes as its first argument a block! containing the DSL and as its second argument another block! containing the DSL’s specification, e.g.,

parse [x 2 x "hey!"] [ 
	some [
		'x
		set value [ integer! | string! ]
		(print either integer? value [ value * 2 ] [ value ])
	]
]

In this simple (and completely useless) DSL, the literal x is followed either by an integer or a string. This sequence of x followed by a value can be repeated indefinitely. (That’s what some tells us in the DSL’s specification.) If the value is an integer, it’s multiplied by two and then printed. If it’s a string, it’s simply printed as is. This is accomplished by the code inside of the parentheses. In the parse dialect, anything in parentheses is interpreted as REBOL code written in the do dialect, i.e., it’s what we think of as ordinary REBOL code. This code is executed only if the previous parse rule succeeds in matching.

Most REBOL DSLs are declarative, because those are the easiest sort to write, but if you’re motived you can create any sort of DSL you wish. It should be stressed that REBOL DSLs are dialects of REBOL, not completely new languages in their own right. In fact, they are dialects of REBOL’s data exchange dialect, because the only valid lexical items are those that are valid in that dialect. For instance, @ by itself is not a valid REBOL literal, and thus cannot be used directly in any REBOL DSL. Thus, the following is not valid:

parse [2 @ 3] grammar

However, the following is valid because @ appears as part of an email address, which is a valid REBOL literal.

parse [2 test@test.com 3] grammar

DSLs can be used for whatever purpose you wish, but most recently I created a DSL to do list comprehensions, since they aren’t natively supported in REBOL. E.g.,

list [[a * b] for a in [1 2 3] for b in [4 5 6] where [even? a * b]]

This returns [4 6 8 10 12 12 18]. Here’s the source. It depends on the range function which I’ve also added.

range: func [pair [pair! block!] /local min max result][
    min: first pair 
    max: second pair 
    result: copy [] 
    for n min max either min < max [1] [-1] [
        append result n
    ] 
    result
]
	
list: func [
	{Performs a list comprehension.}
    comprehension [block!] 
    /type 
    	datatype [datatype!] 
    /local 
	    args 
	    action 
	    elems 
	    filter 
	    index 
	    list 
	    result 
	    rules 
	    skip 
	    vars
][
    vars: make object! [] 
    rules: [
        set action [block!] 
        some [
            'for 
            set var word! 
            'in 
            set list [word! | series! | pair!] 
            (if pair? list [list: range list]) 
            (vars: make vars reduce [to-set-word var either paren? list [do list] [list]])
        ] 
        opt [
            'where 
            set filter [block!]
        ]
    ] 
    unless parse comprehension rules [
        make error! "The list comprehension was not valid."
    ] 
    action: func copy at first vars 2 action 
    filter: func copy at first vars 2 either found? filter [filter] [[true]] 
    elems: 1 
    foreach field at first vars 2 [
        unless found? result [
            result: make either type [datatype] [type? vars/(field)] none
        ] 
        elems: elems * (length? vars/(field))
    ] 
    for n 0 (elems - 1) 1 [
        skip: elems 
        args: copy [] 
        foreach field at first vars 2 [
            list: vars/(field) 
            skip: skip / length? list 
            index: (mod to-integer (n / skip) length? list) + 1 
            append args list/(index)
        ] 
        if do compose [filter (args)] [
            append/only result do compose [action (args)]
        ]
    ] 
    result
]

2 comments: