# encoding: utf-8 # frozen_string_literal: true module Carbon module Compiler class Metanostic # A "State" object. This manages the state of metanostic modes at any # point in the program. This is useful for something like the diagnostic # directives, in which `pop`/`push`/`set` operations need to be available # for the metanostic modes. This manages the metanostic modes. # # The {Metanostic}s are first loaded using {Defaults.defaults}. They # are then used to generate a "beginning" state or "blank" state. Their # modes are extracted, resulting in a hash of # `{String => (Integer, Metanostic)}`. Then, a "current" state is # generated; this is the same hash type as the "beginning" state, # and initially, the same contents. However, when the `push` operation # occurs, a new "state" is added to the stack, and a new "current" state # is generated, using the beginning state and the stack as a reference. # Whenever a set occurs, it sets to the last state added to the stack, # and a new current state is generated. # # The point of all this is to have a hash at all times that accurately # represents the set modes of all diagnostics. class State extend Forwardable include Enumerable # @!method [](name) # Indexes the state. Returns an array containing information about # the mode and the metanostic for the corresponding name, otherwise # it returns `nil`. # # @param name [String] The name of the metanostic. # @return [(Integer, Metanostic)?] The integer is the current mode for # any diagnostics emitted that derives from the given {Metanostic}. def_delegator :current, :[] # @!method fetch(name, default = CANARY) # Fetches the {Metanostic} information with the name `name`. If # no key exists with the given name, the method will attempt to # do the following: if a block is given, yield to that; otherwise, # if a default is given, return that; otherwise, throw a # `KeyError`. # # @raise [KeyError] If no block is given and no default is given # and there is no key `name`. # @param name [String] The name of the metanostic to look up. # @return [(Integer, Metanostic), Object] The integer is the current # mode for any diagnostics emitted that derives from the given # {Metanostic}. def_delegator :current, :fetch # @!method each # Iterates over each key-value pair in the current state. # # @yield [name, data] # @yieldparam name [String] The name, or key, of the key pair. # @yieldparam data [(Integer, Metanostic)] The data, or value, of the # key pair. # @return [Enumerator] def_delegator :current, :each # Initialize the state. It sets the defaults using {Defaults.defaults}, # and generates the beginning state. def initialize @monitor = Monitor.new @defaults = Defaults.defaults @stack = Concurrent::Array.new([Concurrent::Hash.new]) generate_beginning end # Initializes a copy of the given state. This is called by `#clone` # and `#dup`, and as such should not be used outside of either of these # things. # # @param state [State] The state to copy from. def initialize_copy(state) @defaults = state.defaults @stack = state.stack.map(&:clone) @monitor = Monitor.new @beginning = state.beginning end # Pushes to the stack. This pushes a clean state to the stack. # This does not change the current state at all, since the new state # is empty. # # @return [void] def push @monitor.synchronize do @stack.push(Concurrent::Hash.new) end end # Pops the last state from the stack. This could potentially change # the state, since the last state may have a new diagnostic setting # inside of it. # # @note This _may_ change the current state. # @return [void] def pop @monitor.synchronize do clear! if @stack.pop.any? end end # Sets the given metanostic name to the given metanostic value in the # last state in the stack. If the metanostic is not found in any of # the defaults, a `KeyError` is raised. If the new mode is not allowed # for the given metanostic, a {DiagnosticError} is raised. # # @note This changes the current state. # @raise [KeyError] If the metanostic cannot be found. # @raise [DiagnosticError] If the diagnostic could not be set to the # given mode. # @return [void] def set(name, mode) @monitor.synchronize do mode = Mode.from(mode) metanostic = @defaults.fetch(name) unless metanostic.can_be?(mode) fail DiagnosticError, "Diagnostic #{name} cannot be #{Mode.to_s(mode)}" end @stack.last[metanostic.name] = [mode, metanostic] clear! end end # This resets the given metanostic to its default metanostic mode. # It does this by taking the definition from the beginning state and # copying it to the last stack frame. # # @see #set # @note This changes the current state. # @raise (see #set) # @param name [String] The name of the metanostic to reset. # @return (see #set) def reset(name) set(name, @beginning.fetch(name)) end # The current state. This is frozen and cached, since it is a derived # value of the beginning state and the stack. # # @return [{String => (Integer, Metanostic)}] def current @_current ||= @monitor.synchronize do @stack.inject(@beginning) { |a, e| a.merge(e) }.freeze end end protected attr_reader :stack attr_reader :defaults attr_reader :beginning private def clear! @_current = nil end def generate_beginning @beginning = {} @defaults.each do |name, metanostic| @beginning[name] = [metanostic.default, metanostic].freeze end @beginning.freeze end end end end end