Skip to content

Commit

Permalink
Initial compiler (#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
joeldrapper authored Sep 27, 2022
1 parent f7c2bc0 commit d3c7f17
Show file tree
Hide file tree
Showing 19 changed files with 611 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
strategy:
matrix:
os: ['ubuntu-latest', 'macos-latest']
ruby-version: ['2.7', '3.0', '3.1', 'head', 'truffleruby-22.2', 'truffleruby-head']
ruby-version: ['2.7', '3.0', '3.1', 'head']
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
Expand Down
38 changes: 38 additions & 0 deletions fixtures/compilation/vcall.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

module Fixtures
module Compilation
module VCall
class WithStandardElement < Phlex::Component
def template
div
end
end

class WithVoidElement < Phlex::Component
def template
img
end
end

class WithAnotherMethodCall < Phlex::Component
def template
article
some_other_method
article
end
end

class WithRedefinedTagMethod < Phlex::Component
def template
title
article
end

def title
h1
end
end
end
end
end
2 changes: 1 addition & 1 deletion fixtures/component_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module ComponentHelper
def self.extended(parent)
parent.instance_exec do
parent.class_exec do
let(:output) { example.call }
let(:example) { component.new }
end
Expand Down
3 changes: 3 additions & 0 deletions lib/phlex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

require "cgi"
require "zeitwerk"
require "syntax_tree"

loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
loader.ignore("#{__dir__}/generators")
loader.inflector.inflect("html" => "HTML")
loader.inflector.inflect("vcall" => "VCall")
loader.inflector.inflect("fcall" => "FCall")
loader.setup

module Phlex
Expand Down
50 changes: 50 additions & 0 deletions lib/phlex/compiler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

module Phlex
class Compiler
def initialize(component)
@component = component
end

def inspect
"#{self.class.name} for #{@component.name} component class"
end

def call
Visitors::File.new(self).visit(tree)
end

def redefined?(method_name)
prototype = @component.allocate

@component.instance_method(method_name).bind(prototype) !=
Phlex::Component.instance_method(method_name).bind(prototype)
end

def redefine(method)
@component.class_eval(method)
end

def line
location[1]
end

private

def tree
@tree ||= SyntaxTree.parse(source)
end

def source
SyntaxTree.read(file)
end

def file
location[0]
end

def location
::Module.const_source_location(@component.name)
end
end
end
64 changes: 64 additions & 0 deletions lib/phlex/compiler/formatter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# frozen_string_literal: true

module Phlex
class Compiler
class Formatter < SyntaxTree::Formatter
def genspace
-> (n) { "\t" * (n / 2) }
end

def format(node, stackable: true)
stack << node if stackable
doc = node.format(self)
stack.pop if stackable
doc
end

def flush
text "" if @open_append

super
end

def breakable(*args, **kwargs)
if !@texting && @open_append
@broken = kwargs
else
super
end
end

def append(&block)
@appending = true

unless @open_append
text %(@_target << ")
@open_append = true
end

yield(self)

@appending = false
end

def text(value, ...)
@texting = true

unless @appending
if @open_append
super('"')
@open_append = false
end

breakable(**@broken) if @broken
end

@broken = false

super

@texting = false
end
end
end
end
30 changes: 30 additions & 0 deletions lib/phlex/compiler/generators/standard_element.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

module Phlex
class Compiler
module Generators
class StandardElement
def initialize(formatter, method_name:, arguments: nil)
@formatter = formatter
@method_name = method_name
end

def call
@formatter.append do |f|
f.text "<"
f.text tag
f.text ">"

f.text "</"
f.text tag
f.text ">"
end
end

def tag
HTML::STANDARD_ELEMENTS[@method_name]
end
end
end
end
end
29 changes: 29 additions & 0 deletions lib/phlex/compiler/generators/void_element.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

module Phlex
class Compiler
module Generators
class VoidElement
def initialize(formatter, method_name:, arguments: nil)
@formatter = formatter
@method_name = method_name
@arguments = arguments
end

def call
@formatter.append do |f|
f.text "<"
f.text tag
f.text " />"
end
end

private

def tag
HTML::VOID_ELEMENTS[@method_name]
end
end
end
end
end
23 changes: 23 additions & 0 deletions lib/phlex/compiler/optimizers/vcall.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module Phlex
class Compiler
module Optimizers
module VCall
module StandardElement
def format(formatter)
Generators::StandardElement.new(formatter,
method_name: value.value.to_sym).call
end
end

module VoidElement
def format(formatter)
Generators::VoidElement.new(formatter,
method_name: value.value.to_sym).call
end
end
end
end
end
end
19 changes: 19 additions & 0 deletions lib/phlex/compiler/visitors/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module Phlex
class Compiler
module Visitors
class Base < SyntaxTree::Visitor
def initialize(compiler = nil)
@compiler = compiler
end

private

def format(node)
Formatter.format("", node)
end
end
end
end
end
28 changes: 28 additions & 0 deletions lib/phlex/compiler/visitors/component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module Phlex
class Compiler
module Visitors
class Component < Base
visit_method def visit_def(node)
visitor = Visitors::ComponentMethod.new(@compiler)
visitor.visit_all(node.child_nodes)

if visitor.optimized_something?
@compiler.redefine(
format(node)
)
end
end

visit_method def visit_class(node)
nil
end

visit_method def visit_module(node)
nil
end
end
end
end
end
35 changes: 35 additions & 0 deletions lib/phlex/compiler/visitors/component_method.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

module Phlex
class Compiler
module Visitors
class ComponentMethod < Base
def optimized_something?
!!@optimized_something
end

visit_method def visit_vcall(node)
name = node.value.value.to_sym

if HTML::STANDARD_ELEMENTS[name] && !@compiler.redefined?(name)
@optimized_something = true
node.extend(Optimizers::VCall::StandardElement)
elsif HTML::VOID_ELEMENTS[name] && !@compiler.redefined?(name)
@optimized_something = true
node.extend(Optimizers::VCall::VoidElement)
end

super
end

visit_method def visit_class(node)
nil
end

visit_method def visit_module(node)
nil
end
end
end
end
end
17 changes: 17 additions & 0 deletions lib/phlex/compiler/visitors/file.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Phlex
class Compiler
module Visitors
class File < Base
visit_method def visit_class(node)
if node.location.start_line == @compiler.line
Visitors::Component.new(@compiler).visit_all(node.child_nodes)
else
super
end
end
end
end
end
end
Loading

0 comments on commit d3c7f17

Please sign in to comment.