Reevaluating the DSL
by Gamefic on Jun 21, 2015This weekend I pushed a new repo branch that includes a major refactoring of the plot DSL. The syntax for plot scripts is largely unchanged, but the new code behind it uses a new module called the Stage. I based it on the Clean Room pattern described in Metaprogramming Ruby and further developed by Seth Vargo. Including the Stage module can make any class capable of loading code from a Ruby DSL.
Clean rooms provide a sandbox for scripts to limit their impact on the program environment. The simplest implementation could call the Kernel#load method with the optional wrap parameter:
load filename, true
While this method protects the global namespace, it's not much help in implementing a useful DSL. In Gamefic, for example, scripts need to manipulate plots. We can make this possible by evaluating the script's code inside the object's scope via the instance_eval method:
class Plot def load_script filename instance_eval File.read(filename), filename end end
Now the script can access the object's data. The one caveat is the lack of protection. A script executed in the instance's context has complete access to its components, including private methods and instance variables. My goal was to give scripts access to a public interface without letting them intrude on low-level implementation details. The Stage module accomplishes this by instantiating an anonymous class that provides a configurable interface to the script's host. A stage method provides access to the clean room instance that works similarly to instance_eval.
class Plot include Stage expose :allowed def allowed "allowed" end def denied "denied" end end plot = Plot.new plot.stage File.read(filename), filename # Evaluate a script plot.stage do # Run a block puts allowed #=> prints "allowed" puts denied #=> raises an exception end
Stage also provides the mount class method for appending modules. It performs an include and also adds all of the module's public instance methods to the stage.
module Features def feature_method "feature" end private def private_method "private" end end class Plot include Stage mount Features def internal_method "internal" end end plot = Plot.new puts plot.feature_method #=> prints "feature" plot.stage do puts feature_method #=> prints "feature" puts internal_method #=> raises an exception puts private_method #=> raises an exception end
Not only does this feature provide a clean room for scripts, it allows for robust configurability of the interface exposed to the DSL.
A few other updates:
- Undo, save, and restore actions. The undo action is complete. Save and restore are still in progress. Right now they only work on command-line games.
- Incremental builds for the Web platform. Compiling game code to JavaScript can be a lengthy process, so now the build will only compile scripts that have changed since the last build.
- More progress on the IDE, including improvements to autocompletion and a more modular plugin architecture to facilitate easier development and deployment.
- A minor bug fix in the scene method.
If I can maintain my current velocity, I hope to start releasing full games developed in Gamefic this summer.
comments powered by Disqus