Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"version": "2eaea62e",
"names": {},
"parts": {},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"version": "0b5328c1",
"names": {},
"parts": {},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"version": "a207c872",
"names": {},
"parts": {},
"bindings": {},
"states": {
"version": "a207c872",
"declarations": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"version": "e21c5e4a",
"names": {},
"parts": {},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"version": "a9e9757d",
"names": {},
"parts": {},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"version": "83ab2bfd",
"names": {},
"parts": {},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"version": "15f51fc3",
"names": {},
"parts": {},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"version": "22bdd1c0",
"names": {},
"parts": {},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"version": "b36bd345",
"names": {},
"parts": {},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"version": "7700cd32",
"names": {},
"parts": {},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"version": "ba888992",
"names": {},
"parts": {},
"bindings": {},
"states": {
"version": "ba888992",
"declarations": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"version": "7700cd32",
"names": {},
"parts": {},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"version": "853586d4",
"names": {},
"parts": {},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"version": "dfb12a52",
"names": {},
"parts": {},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"version": "7700cd32",
"names": {},
"parts": {},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
""
]
},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"11"
]
},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"version": "fcd78050",
"names": {},
"parts": {},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"version": "e3b0c442",
"names": {},
"parts": {},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"version": "6a76c755",
"names": {},
"parts": {},
"bindings": {},
"states": null
},
"dependencies": {
Expand Down
61 changes: 61 additions & 0 deletions lib/herb/engine/slots/bindings.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true
# typed: true

module Herb
class Engine
module Slots
# The state values a render call hands the partial it renders.
#
# A caller's compile rewrites `render "card", state: { open: modal_open }` into a call
# wrapped by `with`, so the values travel on a stack the partial's own state assignments
# read through `StateOverrides.resolve`. Two tables ride on every frame. A bound value
# aliases the partial's state to the caller's and wins over anything a client sent, since
# the client cannot tell two render sites of one partial apart. A seeded value only sets
# the partial's starting point, so a client that has since written the state wins over it.
#
# The stack lives in fiber storage, which a child fiber or thread inherits, so a render
# that streams or captures still sees the frames its caller pushed.
#
module Bindings
KEY = :herb_slots_bindings #: Symbol

class UncoercibleSeedError < StandardError
end

#: (String, ?bound: Hash[String, untyped], ?seeded: Hash[String, untyped]) { () -> untyped } -> untyped
def self.with(identifier, bound: {}, seeded: {})
seeded.each { |name, value| check_seed(identifier, name, value) }

stack = (Fiber[KEY] ||= []) #: Array[Hash[String, Hash[Symbol, Hash[String, untyped]]]]
stack.push({ identifier => { bound: bound, seeded: seeded } })

begin
yield
ensure
stack.pop
end
end

#: (String) -> Hash[Symbol, Hash[String, untyped]]?
def self.current(identifier)
stack = Fiber[KEY]

return nil unless stack.is_a?(Array)

stack.reverse_each do |frame|
return frame[identifier] if frame.key?(identifier)
end

nil
end

#: (String, String, untyped) -> void
def self.check_seed(identifier, name, value)
return unless StateOverrides.coerce(value, :seeded) == :__herb_uncoercible

raise UncoercibleSeedError, "`#{name}` on `#{identifier}` was seeded with #{value.class}, and a state holds a scalar. Pass the value as a local instead."
end
end
end
end
end
118 changes: 118 additions & 0 deletions lib/herb/engine/slots/callee.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# frozen_string_literal: true
# typed: true

require "pathname"

module Herb
class Engine
module Slots
# The state interface of a partial, read at a caller's compile.
#
# A render that binds states names the partial by its literal path, so the caller can
# open that file, compile it with a throwaway visitor and learn which region states it
# declares and of what kind. That is what lets a binding to a state the partial never
# declared, or of the wrong kind, fail the caller's compile instead of a render.
#
# Lookups are memoized by path and modification time. A partial that renders a caller
# that renders it again would compile forever, so a path already being read answers nil
# and the caller skips the interface check for that one render.
#
class Callee
Declared = Data.define(
:name, #: String
:kind, #: Symbol
:derived, #: bool
:counted #: bool
)

COMPILING = :herb_slots_callee_compiling #: Symbol
CACHE_LIMIT = 256 #: Integer

@cache = {} #: Hash[[String, Float], Callee?]
@lock = Mutex.new #: Mutex

#: (Pathname | String) -> Callee?
def self.for(path)
pathname = path.is_a?(Pathname) ? path : Pathname.new(path)

return nil unless pathname.file?

key = [pathname.to_s, pathname.mtime.to_f] #: [String, Float]

@lock.synchronize do
return @cache.fetch(key) if @cache.key?(key)
end

compiling = (Fiber[COMPILING] ||= []) #: Array[String]

return nil if compiling.include?(pathname.to_s)

compiling.push(pathname.to_s)

callee = begin
new(pathname)
rescue StandardError
nil
ensure
compiling.pop
end

@lock.synchronize do
@cache.clear if @cache.size >= CACHE_LIMIT
@cache[key] = callee
end

callee
end

#: () -> void
def self.reset!
@lock.synchronize { @cache.clear }
end

attr_reader :path #: Pathname
attr_reader :diagnostics #: Array[Herb::Diagnostic]

#: (Pathname) -> void
def initialize(path)
@path = path
@declared = {} #: Hash[String, Declared]

visitor = Visitor.new(mode: :client, mark: false, fatal: false, deliver: :none)

Herb::Engine.new(path.read, visitors: [visitor], filename: path.to_s)

@diagnostics = visitor.diagnostics

visitor.state_entries.each do |entry|
next unless entry[:scope] == :region

name = entry[:name].to_s

@declared[name] = Declared.new(
name: name,
kind: entry[:kind],
derived: !entry[:derived].nil?,
counted: visitor.counted_state?(name)
)
end
end

#: (String) -> Declared?
def declaration(name)
@declared[name]
end

#: () -> Array[String]
def names
@declared.keys
end

#: () -> Array[Herb::Diagnostic]
def errors
@diagnostics.select(&:error?)
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/herb/engine/slots/manifest/collector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def add(file, source = nil)
compile(file, source, Visitor.new(**{ mode: :client, mark: false, identifier: @identifier }.compact)) { |visitor|
manifest = visitor.manifest

next nil if manifest["names"].empty? && manifest["parts"].empty? && manifest["states"].nil?
next nil if manifest["names"].empty? && manifest["parts"].empty? && manifest["states"].nil? && manifest["bindings"].empty?

key = "#{visitor.identifier}:#{visitor.version}"
@manifests[key] ||= manifest
Expand Down
Loading
Loading