diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..c2fc528 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,29 @@ +# The behavior of RuboCop can be controlled via the .rubocop.yml +# configuration file. It makes it possible to enable/disable +# certain cops (checks) and to alter their behavior if they accept +# any parameters. The file can be placed either in your home +# directory or in some project directory. +# +# RuboCop will start looking for the configuration file in the directory +# where the inspected file is and continue its way up to the root directory. +# +# See https://github.com/rubocop-hq/rubocop/blob/master/manual/configuration.md + +Layout/LineLength: + Max: 120 + +Style/HashEachMethods: + Enabled: true + +Style/HashTransformKeys: + Enabled: true + +Style/HashTransformValues: + Enabled: true + +Metrics/BlockLength: + Exclude: + - spec/**/*_spec.rb + +Style/Documentation: + Enabled: false diff --git a/Gemfile b/Gemfile index a09dc13..6bfa264 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,7 @@ source 'https://rubygems.org' # Specify your gem's dependencies in wag.gemspec gemspec +gem 'pry', '~> 0.13.0' gem 'rake', '~> 12.0' gem 'rspec', '~> 3.0' gem 'rubocop', '~> 0.80.1' diff --git a/Gemfile.lock b/Gemfile.lock index d041131..08d1876 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,16 +2,23 @@ PATH remote: . specs: wag (0.1.0) + dry-inflector (~> 0.2.0) GEM remote: https://rubygems.org/ specs: ast (2.4.0) + coderay (1.1.2) diff-lcs (1.3) + dry-inflector (0.2.0) jaro_winkler (1.5.4) + method_source (1.0.0) parallel (1.19.1) parser (2.7.0.5) ast (~> 2.4.0) + pry (0.13.0) + coderay (~> 1.1) + method_source (~> 1.0) rainbow (3.0.0) rake (12.3.3) rexml (3.2.4) @@ -43,6 +50,7 @@ PLATFORMS ruby DEPENDENCIES + pry (~> 0.13.0) rake (~> 12.0) rspec (~> 3.0) rubocop (~> 0.80.1) diff --git a/README.md b/README.md index 6287e3e..591e246 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,24 @@ -# Wag +# WAG - The WebAssembly Code Generator -Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/wag`. To experiment with that code, run `bin/console` for an interactive prompt. +This Ruby gem allows you to generate WebAssembly programs programatically. -TODO: Delete this and the text above, and describe your gem +## Example + +```ruby +WAG::Module.new.build do + memory(1) + export('memory', :memory) + export('hello', :$hello) + func(:$hello) do + result(:i32) + i32.const(16) + end + data do + i32.const(16) + << "Hello World\00" + end +end +``` ## Installation diff --git a/lib/wag.rb b/lib/wag.rb index 2f02816..27c98b2 100644 --- a/lib/wag.rb +++ b/lib/wag.rb @@ -2,7 +2,25 @@ require 'wag/version' -module Wag +module WAG class Error < StandardError; end # Your code goes here... + + require 'wag/wat' + + require 'wag/data' + require 'wag/function' + require 'wag/function_type' + require 'wag/global' + require 'wag/element' + require 'wag/export' + require 'wag/import' + require 'wag/indices' + require 'wag/inflector' + require 'wag/instruction' + require 'wag/label' + require 'wag/memory' + require 'wag/module' + require 'wag/table' + require 'wag/type' end diff --git a/lib/wag/data.rb b/lib/wag/data.rb new file mode 100644 index 0000000..ab15727 --- /dev/null +++ b/lib/wag/data.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module WAG + class Data + include WAG::WAT + + attr_reader :offset, :value + + def initialize(offset, value) + @offset = offset.to_i + @value = value + end + + def to_sexpr + [:data, [:"i32.const", offset], value] + end + end +end diff --git a/lib/wag/element.rb b/lib/wag/element.rb new file mode 100644 index 0000000..48fc59e --- /dev/null +++ b/lib/wag/element.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module WAG + class Element + include WAG::WAT + + attr_reader :table_id, :labels + + def initialize(table_id, *labels) + @table_id = table_id.to_i + @labels = labels.map { |l| WAG::Label.from(l) } + end + + def to_sexpr + [:elem, [:"i32.const", table_id]].concat(labels.map(&:to_sexpr)) + end + end +end diff --git a/lib/wag/export.rb b/lib/wag/export.rb new file mode 100644 index 0000000..499a21c --- /dev/null +++ b/lib/wag/export.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module WAG + class Export + include WAG::WAT + + attr_reader :name, :desc + + def initialize(name) + @name = name + end + + def func(label, &block) + @desc = Function.new(label) + @desc.instance_exec(&block) if block + @desc + end + + def global(label, type) + @desc = Global.new(label, type) + end + + def memory(number, min: nil, max: nil, &block) + @desc = Memory.new(number, min: min, max: max) + @desc.instance_exec(&block) if block + @desc + end + + def table(number, type = :funcref, min: nil, max: nil, &block) + @desc = Table.new(number, type, min: min, max: max) + @desc.instance_exec(&block) if block + @desc + end + + def to_sexpr + [:export, name, desc.to_sexpr] + end + end +end diff --git a/lib/wag/function.rb b/lib/wag/function.rb new file mode 100644 index 0000000..091bc4a --- /dev/null +++ b/lib/wag/function.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module WAG + class Function + include WAG::WAT + + attr_reader :label, :params, :results + + def initialize(label) + @label = WAG::Label.from(label) + @params = [] + @results = [] + end + + def param(*types) + @params.concat(types.map { |t| WAG::Type.from(t) }) + end + + def result(*types) + @results.concat(types.map { |t| WAG::Type.from(t) }) + end + + def to_sexpr + [:func, @label.to_sexpr].tap do |func| + func.push([:param].concat(@params.map(&:to_sexpr))) if @params + func.push([:result].concat(@results.map(&:to_sexpr))) if @results + end + end + end +end diff --git a/lib/wag/function_type.rb b/lib/wag/function_type.rb new file mode 100644 index 0000000..3954ed5 --- /dev/null +++ b/lib/wag/function_type.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module WAG + class FunctionType + include WAG::WAT + + attr_reader :label, :params, :results + + def initialize(label) + @label = WAG::Label.from(label) + @params = [] + @results = [] + end + + def param(*types) + @params.concat(types.map { |t| WAG::Type.from(t) }) + end + + def result(*types) + @results.concat(types.map { |t| WAG::Type.from(t) }) + end + + def to_sexpr + [:type, @label.to_sexpr, + [:func].tap do |func| + func.push([:param].concat(@params.map(&:to_sexpr))) if @params + func.push([:result].concat(@results.map(&:to_sexpr))) if @results + end] + end + end +end diff --git a/lib/wag/global.rb b/lib/wag/global.rb new file mode 100644 index 0000000..dcb2e28 --- /dev/null +++ b/lib/wag/global.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module WAG + class Global + include WAG::WAT + + attr_reader :label, :type + + def initialize(label, type) + @label = WAG::Label.from(label) + @type = WAG::Type.from(type) + end + + def to_sexpr + [:global, label.to_sexpr, type.to_sexpr] + end + end +end diff --git a/lib/wag/import.rb b/lib/wag/import.rb new file mode 100644 index 0000000..81fbd4f --- /dev/null +++ b/lib/wag/import.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module WAG + class Import + include WAG::WAT + + attr_reader :module_name, :name, :desc + + def initialize(module_name, name) + @module_name = module_name + @name = name + end + + def func(label, &block) + @desc = Function.new(label) + @desc.instance_exec(&block) if block + @desc + end + + def global(label, type, &block) + @desc = Global.new(label, type) + @desc.instance_exec(&block) if block + @desc + end + + def memory(number, min: nil, max: nil, &block) + @desc = Memory.new(number, min: min, max: max) + @desc.instance_exec(&block) if block + @desc + end + + def table(number, type = :funcref, min: nil, max: nil, &block) + @desc = Table.new(number, type, min: min, max: max) + @desc.instance_exec(&block) if block + @desc + end + + def to_sexpr + [:import, module_name, name, desc.to_sexpr] + end + end +end diff --git a/lib/wag/indices.rb b/lib/wag/indices.rb new file mode 100644 index 0000000..e2ba6ab --- /dev/null +++ b/lib/wag/indices.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module WAG + module Indices + require 'wag/indices/base' + require 'wag/indices/func' + require 'wag/indices/global' + require 'wag/indices/label' + require 'wag/indices/local' + require 'wag/indices/mem' + require 'wag/indices/table' + require 'wag/indices/type' + end +end diff --git a/lib/wag/indices/base.rb b/lib/wag/indices/base.rb new file mode 100644 index 0000000..40dc5ab --- /dev/null +++ b/lib/wag/indices/base.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module WAG + module Indices + class Base + RANGE = [0..0xFFFFFFFF].freeze + attr_reader :value + + def initialize(value) + raise "Index 0x#{value.to_s(16)} does not fit in an i32" unless RANGE.cover?(value) + + @value = value + end + end + end +end diff --git a/lib/wag/indices/func.rb b/lib/wag/indices/func.rb new file mode 100644 index 0000000..4baec58 --- /dev/null +++ b/lib/wag/indices/func.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Indices + class Func < Base + end + end +end diff --git a/lib/wag/indices/global.rb b/lib/wag/indices/global.rb new file mode 100644 index 0000000..0403a2e --- /dev/null +++ b/lib/wag/indices/global.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Indices + class Global < Base + end + end +end diff --git a/lib/wag/indices/label.rb b/lib/wag/indices/label.rb new file mode 100644 index 0000000..03b7ef1 --- /dev/null +++ b/lib/wag/indices/label.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Indices + class Label < Base + end + end +end diff --git a/lib/wag/indices/local.rb b/lib/wag/indices/local.rb new file mode 100644 index 0000000..6d3bea1 --- /dev/null +++ b/lib/wag/indices/local.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Indices + class Local < Base + end + end +end diff --git a/lib/wag/indices/mem.rb b/lib/wag/indices/mem.rb new file mode 100644 index 0000000..5a5bf2e --- /dev/null +++ b/lib/wag/indices/mem.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Indices + class Mem < Base + end + end +end diff --git a/lib/wag/indices/table.rb b/lib/wag/indices/table.rb new file mode 100644 index 0000000..b4abcd1 --- /dev/null +++ b/lib/wag/indices/table.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Indices + class Table < Base + end + end +end diff --git a/lib/wag/indices/type.rb b/lib/wag/indices/type.rb new file mode 100644 index 0000000..650d908 --- /dev/null +++ b/lib/wag/indices/type.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Indices + class Type < Base + end + end +end diff --git a/lib/wag/inflector.rb b/lib/wag/inflector.rb new file mode 100644 index 0000000..5e4a96b --- /dev/null +++ b/lib/wag/inflector.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'dry-inflector' + +module WAG + module Inflector + module_function + + def inflector + @inflector ||= Dry::Inflector.new + end + end +end diff --git a/lib/wag/instruction.rb b/lib/wag/instruction.rb new file mode 100644 index 0000000..b2e1bbf --- /dev/null +++ b/lib/wag/instruction.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module WAG + module Instruction + require 'wag/instructions/base' + + require 'wag/instructions/block' + require 'wag/instructions/br_if' + require 'wag/instructions/br_table' + require 'wag/instructions/br' + require 'wag/instructions/else' + require 'wag/instructions/end' + require 'wag/instructions/if' + require 'wag/instructions/loop' + require 'wag/instructions/nop' + require 'wag/instructions/return' + require 'wag/instructions/unreachable' + end +end diff --git a/lib/wag/instructions/base.rb b/lib/wag/instructions/base.rb new file mode 100644 index 0000000..0445d2c --- /dev/null +++ b/lib/wag/instructions/base.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module WAG + module Instruction + class Base + include WAG::WAT + + def self.instruction(op_code) + klass = Class.new(WAG::Instruction::Base) + klass.define_method(:op_code) { op_code } + klass + end + + def name + name = WAG::Inflector.inflector.demodulize(self.class.name.to_s) + WAG::Inflector.inflector.underscore(name).to_sym + end + + def to_sexpr + name.to_sym + end + end + end +end diff --git a/lib/wag/instructions/block.rb b/lib/wag/instructions/block.rb new file mode 100644 index 0000000..34eab72 --- /dev/null +++ b/lib/wag/instructions/block.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Instruction + class Block < Base.instruction(0x02) + end + end +end diff --git a/lib/wag/instructions/br.rb b/lib/wag/instructions/br.rb new file mode 100644 index 0000000..097e676 --- /dev/null +++ b/lib/wag/instructions/br.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module WAG + module Instruction + class Br < Base.instruction(0x0c) + attr_reader :label + + def initialize(label) + @label = WAG::Label.from(label) + end + + def to_sexpr + [name, label.to_sexpr] + end + end + end +end diff --git a/lib/wag/instructions/br_if.rb b/lib/wag/instructions/br_if.rb new file mode 100644 index 0000000..168e0b0 --- /dev/null +++ b/lib/wag/instructions/br_if.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module WAG + module Instruction + class BrIf < Base.instruction(0x0d) + attr_reader :label + + def initialize(label) + @label = WAG::Label.from(label) + end + + def to_sexpr + [name, label.to_sexpr] + end + end + end +end diff --git a/lib/wag/instructions/br_table.rb b/lib/wag/instructions/br_table.rb new file mode 100644 index 0000000..a8252d4 --- /dev/null +++ b/lib/wag/instructions/br_table.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module WAG + module Instruction + class BrTable < Base.instruction(0x0e) + attr_reader :label + + def initialize(*labels) + @labels = labels.map { |label| WAG::Label.from(label) } + end + + def to_sexpr + [name].concat(@labels.map(&:to_sexpr)) + end + end + end +end diff --git a/lib/wag/instructions/else.rb b/lib/wag/instructions/else.rb new file mode 100644 index 0000000..0d0bce9 --- /dev/null +++ b/lib/wag/instructions/else.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Instruction + class Else < Base.instruction(0x05) + end + end +end diff --git a/lib/wag/instructions/end.rb b/lib/wag/instructions/end.rb new file mode 100644 index 0000000..f3d5148 --- /dev/null +++ b/lib/wag/instructions/end.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Instruction + class End < Base.instruction(0x0b) + end + end +end diff --git a/lib/wag/instructions/if.rb b/lib/wag/instructions/if.rb new file mode 100644 index 0000000..9509da9 --- /dev/null +++ b/lib/wag/instructions/if.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Instruction + class If < Base.instruction(0x04) + end + end +end diff --git a/lib/wag/instructions/loop.rb b/lib/wag/instructions/loop.rb new file mode 100644 index 0000000..3ad000e --- /dev/null +++ b/lib/wag/instructions/loop.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Instruction + class Loop < Base.instruction(0x03) + end + end +end diff --git a/lib/wag/instructions/nop.rb b/lib/wag/instructions/nop.rb new file mode 100644 index 0000000..0860ab3 --- /dev/null +++ b/lib/wag/instructions/nop.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Instruction + class Nop < Base.instruction(0x01) + end + end +end diff --git a/lib/wag/instructions/return.rb b/lib/wag/instructions/return.rb new file mode 100644 index 0000000..7c43a2a --- /dev/null +++ b/lib/wag/instructions/return.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Instruction + class Return < Base.instruction(0x0f) + end + end +end diff --git a/lib/wag/instructions/unreachable.rb b/lib/wag/instructions/unreachable.rb new file mode 100644 index 0000000..d153371 --- /dev/null +++ b/lib/wag/instructions/unreachable.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Instruction + class Unreachable < Base.instruction(0x00) + end + end +end diff --git a/lib/wag/label.rb b/lib/wag/label.rb new file mode 100644 index 0000000..6381b7b --- /dev/null +++ b/lib/wag/label.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module WAG + class Label + include WAG::WAT + + attr_reader :value + + def initialize(value) + @value = value.to_s + @value = @value[1..-1] if @value.start_with?('$') + end + + def to_sexpr + "$#{value}".to_sym + end + + def self.from(value) + return value if value.is_a?(WAG::Label) + + WAG::Label.new(value) + end + end +end diff --git a/lib/wag/memory.rb b/lib/wag/memory.rb new file mode 100644 index 0000000..c545d02 --- /dev/null +++ b/lib/wag/memory.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module WAG + class Memory + include WAG::WAT + + attr_reader :number, :min, :max + + def initialize(number, min: nil, max: nil) + @number = number + @min = min + @max = max + end + + def to_sexpr + wat = [:memory, number] + + wat.concat([:min, min]) if min + wat.concat([:max, max]) if max + + wat + end + end +end diff --git a/lib/wag/module.rb b/lib/wag/module.rb new file mode 100644 index 0000000..02b4feb --- /dev/null +++ b/lib/wag/module.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module WAG + class Module + include WAG::WAT + + def initialize + @data = [] + @elements = [] + @exports = [] + @functions = [] + @globals = [] + @imports = [] + @memories = [] + @types = [] + end + + def import(module_name, name, &block) + import = Import.new(module_name, name) + @imports << import + import.instance_exec(&block) if block + import + end + + def export(name, &block) + export = Export.new(name) + @exports << export + export.instance_exec(&block) if block + export + end + + def memory(number, min: nil, max: nil, &block) + mem = Memory.new(number, min: min, max: max) + @memories << mem + mem.instance_exec(&block) if block + mem + end + + def global(label, type, &block) + global = Global.new(label, type) + @globals << global + global.instance_exec(&block) if block + global + end + + def type(label, &block) + type = FunctionType.new(label) + @types << type + type.instance_exec(&block) if block + type + end + + def elem(table_id, *labels, &block) + elem = Element.new(table_id, *labels) + @elements << elem + elem.instance_exec(&block) if block + elem + end + + def data(offset, value, &block) + data = Data.new(offset, value) + @data << data + data.instance_exec(&block) if block + data + end + + def func(label, &block) + func = Function.new(label) + @functions << func + func.instance_exec(&block) if block + func + end + + def to_sexpr # rubocop:disable Metrics/AbcSize + mod = [:module] + mod.concat(@imports.map(&:to_sexpr)) + mod.concat(@exports.map(&:to_sexpr)) + mod.concat(@memories.map(&:to_sexpr)) + mod.concat(@globals.map(&:to_sexpr)) + mod.concat(@types.map(&:to_sexpr)) + mod.concat(@elements.map(&:to_sexpr)) + mod.concat(@data.map(&:to_sexpr)) + mod.concat(@functions.map(&:to_sexpr)) + mod + end + end +end diff --git a/lib/wag/table.rb b/lib/wag/table.rb new file mode 100644 index 0000000..a577cdf --- /dev/null +++ b/lib/wag/table.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module WAG + class Table + include WAG::WAT + + RANGE = (0..0xFFFFFFFF).freeze + + attr_reader :number, :type, :min, :max + + def initialize(number, type = :funcref, min: nil, max: nil) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + raise '`min` must fit in u32 range' if min && !RANGE.cover?(min) + raise '`max` must fit in u32 range' if max && !RANGE.cover?(max) + raise '`max` must be greater than `min`' if min && max && min > max + + @number = number + @type = type + @min = min + @max = max + end + + def to_sexpr + expr = [:table, number, type] + expr.concat([:min, min]) if min + expr.concat([:max, max]) if max + expr + end + end +end diff --git a/lib/wag/type.rb b/lib/wag/type.rb new file mode 100644 index 0000000..b1f2015 --- /dev/null +++ b/lib/wag/type.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module WAG + module Type + require 'wag/types/base' + require 'wag/types/i32' + require 'wag/types/i64' + require 'wag/types/f32' + require 'wag/types/f64' + + module_function + + def from(type) + return type if type.is_a?(WAG::Type::Base) + + const_get(WAG::Inflector.inflector.camelize(type.to_s)).new + end + end +end diff --git a/lib/wag/types/base.rb b/lib/wag/types/base.rb new file mode 100644 index 0000000..7653a63 --- /dev/null +++ b/lib/wag/types/base.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module WAG + module Type + class Base + def to_sexpr + name = WAG::Inflector.inflector.demodulize(self.class.name.to_s) + WAG::Inflector.inflector.underscore(name).to_sym + end + + def value? + true + end + end + end +end diff --git a/lib/wag/types/f32.rb b/lib/wag/types/f32.rb new file mode 100644 index 0000000..337b32a --- /dev/null +++ b/lib/wag/types/f32.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Type + class F32 < Base + end + end +end diff --git a/lib/wag/types/f64.rb b/lib/wag/types/f64.rb new file mode 100644 index 0000000..921acab --- /dev/null +++ b/lib/wag/types/f64.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Type + class F64 < Base + end + end +end diff --git a/lib/wag/types/function.rb b/lib/wag/types/function.rb new file mode 100644 index 0000000..ee62d37 --- /dev/null +++ b/lib/wag/types/function.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module WAG + module Type + # A WebAssembly function type. + class Function < Base + attr_reader :parameter_types, :return_types + + def initialize(parameter_types, return_types) + parameter_types.map! { |t| Huia::Type.from(t) } + return_types.map! { |t| Huia::Type.from(t) } + + raise 'There must only be one return type (at the moment)' unless return_types.one? + + unless parameter_types.all?(&:value?) && return_types.all?(&:value?) + raise 'Function type parameters can only be value types' + end + + @parameter_types = parameter_types + @return_types = return_types + end + + def value? + false + end + end + end +end diff --git a/lib/wag/types/i32.rb b/lib/wag/types/i32.rb new file mode 100644 index 0000000..64860eb --- /dev/null +++ b/lib/wag/types/i32.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Type + class I32 < Base + end + end +end diff --git a/lib/wag/types/i64.rb b/lib/wag/types/i64.rb new file mode 100644 index 0000000..2bb43f7 --- /dev/null +++ b/lib/wag/types/i64.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module WAG + module Type + class I64 < Base + end + end +end diff --git a/lib/wag/version.rb b/lib/wag/version.rb index 5ee7bc2..286156f 100644 --- a/lib/wag/version.rb +++ b/lib/wag/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -module Wag +module WAG VERSION = '0.1.0' end diff --git a/lib/wag/wat.rb b/lib/wag/wat.rb new file mode 100644 index 0000000..3f35cbc --- /dev/null +++ b/lib/wag/wat.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module WAG + # A simple mixin which formats s-expressions as WAT. + module WAT + def to_wat + wat_encode(to_sexpr) + end + + private + + def wat_encode(data) + return "(#{data.map { |e| wat_encode(e) }.join(' ')})" if data.is_a?(Enumerable) + return data.inspect if data.is_a?(String) + + data.to_s + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1d2c194..c88bf25 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,6 +2,7 @@ require 'bundler/setup' require 'wag' +require 'pry' RSpec.configure do |config| # Enable flags like --only-failures and --next-failure diff --git a/spec/wag/export_spec.rb b/spec/wag/export_spec.rb new file mode 100644 index 0000000..2cd2455 --- /dev/null +++ b/spec/wag/export_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WAG::Export do + let(:export) { described_class.new('marty') } + subject { export } + + describe 'When exporting memory' do + before do + export.memory(1, min: 1, max: 64) + end + + describe '#to_sexpr' do + subject { super().to_sexpr } + it { is_expected.to eq [:export, 'marty', [:memory, 1, :min, 1, :max, 64]] } + end + end + + describe 'When exporting a table' do + before do + export.table(1, :funcref, min: 1, max: 64) + end + + describe '#to_sexpr' do + subject { super().to_sexpr } + it { is_expected.to eq [:export, 'marty', [:table, 1, :funcref, :min, 1, :max, 64]] } + end + end + + describe 'When exporting a function' do + before do + export.func(:foo) do + param(:f32, :f64) + result(:i32) + end + end + + describe '#to_sexpr' do + subject { super().to_sexpr } + it { is_expected.to eq [:export, 'marty', [:func, :$foo, %i[param f32 f64], %i[result i32]]] } + end + end + + describe 'When exporting a global' do + before do + export.global(:foo, :i32) + end + + describe '#to_sexpr' do + subject { super().to_sexpr } + it { is_expected.to eq [:export, 'marty', %i[global $foo i32]] } + end + end +end diff --git a/spec/wag/import_spec.rb b/spec/wag/import_spec.rb new file mode 100644 index 0000000..8a895ae --- /dev/null +++ b/spec/wag/import_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WAG::Import do + let(:import) { described_class.new('marty', 'mcfly') } + subject { import } + + describe 'When importing memory' do + before do + import.memory(1, min: 1, max: 64) + end + + describe '#to_sexpr' do + subject { super().to_sexpr } + it { is_expected.to eq [:import, 'marty', 'mcfly', [:memory, 1, :min, 1, :max, 64]] } + end + end + + describe 'When importing a table' do + before do + import.table(1, :funcref, min: 1, max: 64) + end + + describe '#to_sexpr' do + subject { super().to_sexpr } + it { is_expected.to eq [:import, 'marty', 'mcfly', [:table, 1, :funcref, :min, 1, :max, 64]] } + end + end + + describe 'When importing a function' do + before do + import.func(:foo) do + param(:f32, :f64) + result(:i32) + end + end + + describe '#to_sexpr' do + subject { super().to_sexpr } + it { is_expected.to eq [:import, 'marty', 'mcfly', [:func, :$foo, %i[param f32 f64], %i[result i32]]] } + end + end + + describe 'When importing a global' do + before do + import.global(:foo, :i32) + end + + describe '#to_sexpr' do + subject { super().to_sexpr } + it { is_expected.to eq [:import, 'marty', 'mcfly', %i[global $foo i32]] } + end + end +end diff --git a/spec/wag/module_spec.rb b/spec/wag/module_spec.rb new file mode 100644 index 0000000..d43b587 --- /dev/null +++ b/spec/wag/module_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WAG::Module do + let(:mod) { described_class.new } + subject { mod } + + describe 'When the module is empty' do + describe '#to_sexpr' do + subject { super().to_sexpr } + it { is_expected.to eq [:module] } + end + + describe '#to_wat' do + subject { super().to_wat } + it { is_expected.to eq '(module)' } + end + end + + describe 'When the module imports a memory' do + before do + mod.import('js', 'memory').memory(1) + end + + describe '#to_sexpr' do + subject { super().to_sexpr } + it { is_expected.to eq [:module, [:import, 'js', 'memory', [:memory, 1]]] } + end + end + + describe 'When the module exports a memory' do + before do + mod.export('memory').memory(1) + end + + describe '#to_sexpr' do + subject { super().to_sexpr } + it { is_expected.to eq [:module, [:export, 'memory', [:memory, 1]]] } + end + end + + describe 'When the module declares a memory' do + before do + mod.memory(2) + end + + describe '#to_sexpr' do + subject { super().to_sexpr } + it { is_expected.to eq [:module, [:memory, 2]] } + end + end + + describe 'When the module declares a global' do + before do + mod.global(:foo, :i32) + end + + describe '#to_sexpr' do + subject { super().to_sexpr } + it { is_expected.to eq [:module, %i[global $foo i32]] } + end + end + + describe 'When the module declares a type' do + before do + mod.type(:foo) do + param(:i32) + result(:i32) + end + end + + describe '#to_sexpr' do + subject { super().to_sexpr } + it { is_expected.to eq [:module, [:type, :$foo, [:func, %i[param i32], %i[result i32]]]] } + end + end + + describe 'When the module declares a table element' do + before do + mod.elem(1, :foo1, :foo2) + end + + describe '#to_sexpr' do + subject { super().to_sexpr } + it { is_expected.to eq [:module, [:elem, [:"i32.const", 1], :$foo1, :$foo2]] } + end + end + + describe 'When the module declares data' do + before do + mod.data(16, 'Hello World') + end + + describe '#to_sexpr' do + subject { super().to_sexpr } + it { is_expected.to eq [:module, [:data, [:"i32.const", 16], 'Hello World']] } + end + end + + describe 'When the module declares a function' do + before do + mod.func(:add) do + param(:i32, :i32) + result(:i32) + end + end + + describe '#to_sexpr' do + subject { super().to_sexpr } + it { is_expected.to eq [:module, [:func, :$add, %i[param i32 i32], %i[result i32]]] } + end + end +end diff --git a/spec/wag_spec.rb b/spec/wag_spec.rb index 8b09cbb..6bdee36 100644 --- a/spec/wag_spec.rb +++ b/spec/wag_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -RSpec.describe Wag do +RSpec.describe WAG do it 'has a version number' do - expect(Wag::VERSION).not_to be nil + expect(WAG::VERSION).not_to be nil end end diff --git a/wag.gemspec b/wag.gemspec index acdd75d..81bd926 100644 --- a/wag.gemspec +++ b/wag.gemspec @@ -4,7 +4,7 @@ require_relative 'lib/wag/version' Gem::Specification.new do |spec| spec.name = 'wag' - spec.version = Wag::VERSION + spec.version = WAG::VERSION spec.authors = ['James Harton'] spec.email = ['james@automat.nz'] @@ -30,4 +30,6 @@ Gem::Specification.new do |spec| spec.bindir = 'exe' spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] + + spec.add_runtime_dependency('dry-inflector', '~> 0.2.0') end