Every jsonic program produces valid JSON. This isn’t a rule we’d impose on a general-purpose programming language. But for a DSL, it makes perfect sense. Since our DSL exists to produce JSON, we can treat this as a firm requirement.
Every valid JSON file is a valid jsonic program. This rule isn’t strictly necessary. But it’s consistent with the first rule: if we treat a JSON file as a jsonic program, the result should be itself.
Racket expressions can be embedded anywhere within a jsonic program that a JSON value—i.e., a Boolean, null, number, string, array, or object—would appear.
If the characters // appear in a line, the rest of the line will be commented out. This rule also isn’t strictly necessary. But JSON doesn’t support comments. And every programming language—even a small one—really should.
Let’s step through each of these ground rules and consider what it means for our language design.
Every valid jsonic program produces valid JSON.
This means we’ll need a way of checking the output of any program against the JSON specification. If the output doesn’t meet the specification, we’ll raise an error. For those worried that we’ll have to write a whole JSON checker within jsonic: we won’t.
Every valid JSON file is a valid jsonic program.
This means that our jsonic parser should process JSON files transparently. The rest will be handled by our JSON output checker mentioned above. Obviously, if the input is valid JSON, it will still be valid JSON at output.
Racket expressions can be embedded in place of any JSON value.
Since Racket expressions have a different syntax than JSON, this means we’ll need a pair of delimiters to set these expressions apart from the surrounding code. The simple delimiter pairs like () and [] and {} are already used by Racket S-expressions, so we need to avoid those. Instead, let’s use @$ and $@. We could choose anything, but those delimiters are easy to type, and won’t conflict with anything in JSON or Racket.
Since we’re substituting Racket expressions for JSON values, we’ll also want to enforce a correspondence between the two. JSON supports only six kinds of values—Booleans, nulls, numbers, strings, arrays, and objects. These naturally correspond to Racket Booleans, 'null symbols, numbers, strings, lists, and hash tables. So if our embedded Racket expressions don’t produce one of these JSON-compatible values, we’ll treat it as an error.
Furthermore, JSON is a text-based format. Therefore, our Racket expressions will ultimately need to be converted to JSON strings, so they can be combined with the surrounding JSON. But rather than making users deal with that housekeeping, we’ll have jsonic take care of these conversions automatically.
Taken together, we should be able to take our sample JSON and treat it as a jsonic program that produces equivalent JSON (we can run this program in DrRacket—#lang jsonic-demo is a working sample implementation):
1 2 3 4 5 6 7 8 9 10 11 12 | #lang jsonic-demo [ null, 42, true, ["array", "of", "strings"], { "key-1": null, "key-2": false, "key-3": {"subkey": 21} } ] |
We should also be able to replace these JSON values with equivalent Racket expressions:
1 2 3 4 5 6 7 8 9 10 11 | #lang jsonic-demo // a line comment [ @$ 'null $@, @$ (* 6 7) $@, @$ (= 2 (+ 1 1)) $@, @$ (list "array" "of" "strings") $@, @$ (hash 'key-1 'null 'key-2 (even? 3) 'key-3 (hash 'subkey 21)) $@ ] |
When we run this code in DrRacket, the ordering of key-1, key-2, and key-3 in the two samples may be different. That’s okay, because a JSON object, like a Racket hash table, doesn’t preserve key order.
This kind of DSL is also known as a preprocessor, because it’s source code that produces some other kind of source code. Programming jocks might be familiar with the C preprocessor; web jocks might be familiar with SASS, which is a preprocessor for CSS. But unlike a typical preprocessor, which usually supports only a limited vocabulary of operations, jsonic can use everything in the Racket language (as long as it ultimately reduces to one of our six kinds of values).
Beyond that, our language implementation will proceed just as it did for stacker and bf. Our language will have two main parts: a reader (comprising a tokenizer and a parser) and an expander.
But this time, one change. At the end of bf, we learned about packaging our language. For jsonic, we’ll set up our project as a package right away, to make our language easier to work with as we build it.