module BinData # The enviroment in which a lazily evaluated lamba is called. These lambdas # are those that are passed to data objects as parameters. Each lambda # has access to the following: # # parent:: the environment of the parent data object # params:: any extra parameters that have been passed to the data object. # The value of a parameter is either a lambda, a symbol or a # literal value (such as a Fixnum). # # Unknown methods are resolved in the context of the parent environment, # first as keys in the extra parameters, and secondly as methods in the # parent data object. This makes the lambda easier to read as we just write # field instead of obj.field. class LazyEvalEnv # An empty hash shared by all instances @@empty_hash = Hash.new.freeze @@variables_cache = {} # Creates a new environment. +parent+ is the environment of the # parent data object. def initialize(parent = nil) @parent = parent @variables = @@empty_hash @overrides = @@empty_hash @params = @@empty_hash end attr_reader :parent, :params attr_accessor :data_object # only accessible by another LazyEvalEnv protected :data_object # Set the parameters for this environment. def params=(p) @params = (p.nil? or p.empty?) ? @@empty_hash : p end # Add a variable with a pre-assigned value to this environment. +sym+ # will be accessible as a variable for any lambda evaluated # with #lazy_eval. def add_variable(sym, value) sym = sym.to_sym if @variables.equal?(@@empty_hash) # optimise the case where only 1 variable is added as this # is the most common occurance (BinData::Arrays adding index) key = [sym, value] @variables = @@variables_cache[key] if @variables.nil? # cache this variable and value so it can be shared with # other LazyEvalEnvs to keep memory usage down @variables = {sym => value}.freeze @@variables_cache[key] = @variables end else if @variables.length == 1 key = @variables.keys[0] @variables = {key => @variables[key]} end @variables[sym] = value end end # TODO: offset_of needs to be better thought out def offset_of(sym) @parent.data_object.offset_of(sym) rescue nil end # Returns the data_object for the parent environment. def parent_data_object @parent.nil? ? nil : @parent.data_object end # Evaluates +obj+ in the context of this environment. Evaluation # recurses until it yields a value that is not a symbol or lambda. # +overrides+ is an optional +params+ like hash def lazy_eval(obj, overrides = nil) result = obj @overrides = overrides if overrides if obj.is_a? Symbol # treat :foo as lambda { foo } result = __send__(obj) elsif obj.respond_to? :arity result = instance_eval(&obj) end @overrides = @@empty_hash result end def method_missing(symbol, *args) if @overrides.include?(symbol) @overrides[symbol] elsif @variables.include?(symbol) @variables[symbol] elsif @parent obj = symbol if @parent.params and @parent.params.has_key?(symbol) obj = @parent.params[symbol] elsif @parent.data_object and @parent.data_object.respond_to?(symbol) obj = @parent.data_object.__send__(symbol, *args) end @parent.lazy_eval(obj) else super end end end end