├── parser ├── ast.go ├── str_utils.go ├── gen_parser.sh ├── noop.go ├── stack.go ├── kernel.go ├── scopechain_test.go ├── stack_state.go ├── lexstate_string.go ├── node.go ├── range.go ├── string_test.go ├── parser.go ├── logic.go ├── primitives.go ├── scope.go ├── identifiers.go ├── tokens.go ├── string.go ├── assignment.go ├── statements.go └── composites.go ├── .gitignore ├── stdlib ├── go.mod ├── range.go ├── hash.go ├── array_test.go ├── array.go ├── string.go ├── file.go ├── matchdata.go ├── matchdata_test.go ├── file_test.go └── set.go ├── compiler ├── testdata │ ├── ruby │ │ ├── test_compile.rb │ │ ├── test_assignment.rb │ │ ├── test_blocks.rb │ │ ├── test_hashes.rb │ │ ├── test_modules.rb │ │ ├── test_files.rb │ │ ├── test_math.rb │ │ ├── test_control.rb │ │ ├── test_constants.rb │ │ ├── test_super.rb │ │ ├── test_arrays.rb │ │ ├── test_strings.rb │ │ ├── test_classes.rb │ │ ├── test_arguments.rb │ │ └── test_cond.rb │ └── go │ │ ├── test_compile.go │ │ ├── test_assignment.go │ │ ├── test_blocks.go │ │ ├── test_modules.go │ │ ├── test_hashes.go │ │ ├── test_control.go │ │ ├── test_files.go │ │ ├── test_math.go │ │ ├── test_arrays.go │ │ ├── test_constants.go │ │ ├── test_strings.go │ │ ├── test_super.go │ │ ├── test_arguments.go │ │ ├── test_cond.go │ │ └── test_classes.go ├── block_stmt.go ├── methods.go ├── util.go ├── compiler_test.go ├── func.go ├── class.go └── compiler.go ├── go.work ├── main.go ├── tests ├── set.rb ├── object.rb ├── destructuring.rb ├── file.rb ├── blocks.rb ├── control.rb ├── regexp.rb ├── range.rb ├── float.rb ├── int.rb ├── class.rb ├── hash.rb ├── string.rb └── array.rb ├── tools.go ├── types ├── predefined_constants.go ├── simple_string.go ├── float.go ├── symbol.go ├── bool.go ├── proc.go ├── class_test.go ├── proto_test.go ├── matchdata.go ├── helpers.go ├── regexp.go ├── range.go ├── kernel.go ├── types.go ├── set.go ├── class.go ├── object.go ├── numeric.go ├── file.go └── proto.go ├── bst ├── ident.go └── util.go ├── go.mod ├── .goreleaser.yaml ├── scripts └── update-stdlib-version ├── cmd ├── root.go ├── compile.go ├── exec.go ├── report.go └── test.go └── LICENSE /parser/ast.go: -------------------------------------------------------------------------------- 1 | package parser 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | parser/y.output 2 | 3 | dist/ 4 | -------------------------------------------------------------------------------- /stdlib/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redneckbeard/thanos/stdlib 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /compiler/testdata/ruby/test_compile.rb: -------------------------------------------------------------------------------- 1 | def foo(a, b) 2 | [a, b] 3 | end 4 | 5 | 6 | puts foo(5, 7).length 7 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.18 2 | 3 | use ( 4 | ./ 5 | ) 6 | 7 | replace github.com/redneckbeard/thanos/stdlib => ./stdlib 8 | -------------------------------------------------------------------------------- /compiler/testdata/ruby/test_assignment.rb: -------------------------------------------------------------------------------- 1 | a, b = 1, 2.0 2 | c, d = a + 1, b + 2 3 | e = 1, 2, 3 4 | m, n = e 5 | puts c + d 6 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/redneckbeard/thanos/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /compiler/testdata/ruby/test_blocks.rb: -------------------------------------------------------------------------------- 1 | def foo(x, y, &blk) 2 | x * blk.call(y) 3 | end 4 | 5 | foo(7, 8) do |b| 6 | b / 10.0 7 | end 8 | -------------------------------------------------------------------------------- /tests/set.rb: -------------------------------------------------------------------------------- 1 | gauntlet("Set#union") do 2 | require "set" 3 | Set[1, 2, 3].union(Set[2, 4, 5]).each do |x| 4 | puts x 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /compiler/testdata/ruby/test_hashes.rb: -------------------------------------------------------------------------------- 1 | h = {foo: "x", bar: "y"} 2 | 3 | x = h.delete(:foo) 4 | 5 | y = h.delete(:baz) do |k| 6 | "default for #{k}" 7 | end 8 | -------------------------------------------------------------------------------- /tests/object.rb: -------------------------------------------------------------------------------- 1 | gauntlet("methods") do 2 | # comes from Object 3 | puts [].methods.include?(:methods) 4 | # comes from Array 5 | puts [].methods.include?(:join) 6 | end 7 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | import ( 7 | _ "golang.org/x/tools/cmd/goyacc" 8 | _ "golang.org/x/tools/cmd/stringer" 9 | ) 10 | -------------------------------------------------------------------------------- /tests/destructuring.rb: -------------------------------------------------------------------------------- 1 | gauntlet "LHS splat" do 2 | syms = [:foo, :bar, :baz, :quux] 3 | a, b, *c = syms 4 | puts a 5 | puts b 6 | c.each do |sym| 7 | puts sym 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /compiler/testdata/go/test_compile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func Foo(a, b int) []int { 6 | return []int{a, b} 7 | } 8 | func main() { 9 | fmt.Println(len(Foo(5, 7))) 10 | } 11 | -------------------------------------------------------------------------------- /tests/file.rb: -------------------------------------------------------------------------------- 1 | gauntlet("File#each") do 2 | File.new("compiler/testdata/input/millennials.txt").each do |line| 3 | puts line.gsub(/[mM]illennial(?s)?/, 'Snake Person\k') 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /compiler/testdata/go/test_assignment.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | a, b := 1, 2.0 7 | c, d := a+1, b+2 8 | e := []int{1, 2, 3} 9 | m, n := e[0], e[1] 10 | fmt.Println(float64(c) + d) 11 | } 12 | -------------------------------------------------------------------------------- /tests/blocks.rb: -------------------------------------------------------------------------------- 1 | gauntlet("user-defined block method") do 2 | def logging(arr, &blk) 3 | arr.each do |n| 4 | blk.call(n) 5 | end 6 | end 7 | 8 | logging([1,2,3,4,5]) do |x| 9 | puts x 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /compiler/testdata/ruby/test_modules.rb: -------------------------------------------------------------------------------- 1 | Quux = false 2 | 3 | module Foo 4 | Quux = "quux" 5 | 6 | class Baz 7 | Quux = 10 8 | 9 | def quux 10 | Quux 11 | end 12 | end 13 | end 14 | 15 | puts Foo::Baz.new.quux 16 | -------------------------------------------------------------------------------- /tests/control.rb: -------------------------------------------------------------------------------- 1 | gauntlet("break") do 2 | 10.times do |i| 3 | puts i 4 | if i > 5 5 | break 6 | end 7 | end 8 | end 9 | 10 | gauntlet("next") do 11 | 10.times do |i| 12 | puts i 13 | if i % 2 == 0 14 | next 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /compiler/testdata/go/test_blocks.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type fooBlk func(b int) float64 4 | 5 | func Foo(x, y int, blk fooBlk) float64 { 6 | return float64(x) * blk(y) 7 | } 8 | func main() { 9 | Foo(7, 8, func(b int) float64 { 10 | return float64(b) / 10.0 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /parser/str_utils.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "strings" 4 | 5 | func Indent(strs ...string) string { 6 | var final []string 7 | for _, s := range strs { 8 | for _, line := range strings.Split(s, "\n") { 9 | final = append(final, line) 10 | } 11 | } 12 | return strings.Join(final, "; ") 13 | } 14 | -------------------------------------------------------------------------------- /parser/gen_parser.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | goyacc -l -o ruby.go ruby.y 3 | sed 's/yyErrorVerbose = false/yyErrorVerbose = true/' ruby.go > ruby.go.verbose 4 | sed 's/"syntax error: unexpected "/__yyfmt__.Sprintf("syntax error, line %d: unexpected ", currentLineNo)/' ruby.go.verbose > ruby.go.linenos 5 | mv ruby.go.linenos ruby.go 6 | rm ruby.go.* 7 | -------------------------------------------------------------------------------- /tests/regexp.rb: -------------------------------------------------------------------------------- 1 | gauntlet("match") do 2 | ["football", "goosefoot", "tomfoolery"].each do |cand| 3 | puts cand.match(/foo(?.+)/)["tail"] 4 | end 5 | end 6 | 7 | gauntlet("gsub") do 8 | ["football", "goosefoot", "tomfoolery"].each do |cand| 9 | puts cand.gsub(/foo(?.+)/, 'bar\k') 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /stdlib/range.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | type Rangeable interface { 4 | ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64 | ~string 5 | } 6 | 7 | type Range[T Rangeable] struct { 8 | Lower, Upper T 9 | Inclusive bool 10 | } 11 | 12 | func (r *Range[T]) Covers(t T) bool { 13 | return t >= r.Lower && t <= r.Upper 14 | } 15 | -------------------------------------------------------------------------------- /compiler/testdata/ruby/test_files.rb: -------------------------------------------------------------------------------- 1 | f = File.new("stuff.txt") 2 | 3 | f.each do |ln| 4 | puts ln.gsub(/good/, "bad") 5 | end 6 | 7 | f.close 8 | 9 | puts(File.open("writable.txt", "a+") do |f| 10 | f << "here are some bits" 11 | f.size # only here to prove that we get the return type of the block as the return type of the whole expression 12 | end.is_a?(Integer)) 13 | -------------------------------------------------------------------------------- /types/predefined_constants.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "go/ast" 5 | 6 | "github.com/redneckbeard/thanos/bst" 7 | ) 8 | 9 | var PredefinedConstants = map[string]struct { 10 | Type Type 11 | Expr ast.Expr 12 | Imports []string 13 | }{ 14 | "ARGV": { 15 | Type: NewArray(StringType), 16 | Expr: bst.Dot("os", "Args"), 17 | Imports: []string{"os"}, 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /compiler/testdata/go/test_modules.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | const FooQuux = "quux" 6 | const FooBazQuux = 10 7 | const Quux = false 8 | 9 | type FooBaz struct { 10 | } 11 | 12 | func NewFooBaz() *FooBaz { 13 | newInstance := &FooBaz{} 14 | return newInstance 15 | } 16 | func (b *FooBaz) Quux() int { 17 | return FooBazQuux 18 | } 19 | func main() { 20 | fmt.Println(NewFooBaz().Quux()) 21 | } 22 | -------------------------------------------------------------------------------- /compiler/testdata/go/test_hashes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | h := map[string]string{"foo": "x", "bar": "y"} 7 | var val string 8 | if v, ok := h["foo"]; ok { 9 | val = v 10 | delete(h, "foo") 11 | } 12 | x := val 13 | var val1 string 14 | if v, ok := h["baz"]; ok { 15 | val1 = v 16 | delete(h, "baz") 17 | } else { 18 | k := "baz" 19 | val1 = fmt.Sprintf("default for %v", k) 20 | } 21 | y := val1 22 | } 23 | -------------------------------------------------------------------------------- /compiler/testdata/ruby/test_math.rb: -------------------------------------------------------------------------------- 1 | x = 10 / 2 2 | y = x / 2.0 3 | z = x ** 2 4 | a = x ** x 5 | b = y ** 2 6 | c = 12.0 / 4 7 | d = -50.abs 8 | e = x.abs 9 | 10 | 10.times do |x| 11 | if x.even? 12 | puts x 13 | end 14 | end 15 | 16 | 15.downto(10) do |x| 17 | if x.odd? 18 | puts x 19 | end 20 | end 21 | 22 | -5.upto(5) do |x| 23 | case 24 | when x.zero? 25 | puts "zero" 26 | when x.positive? 27 | puts "positive" 28 | when x.negative? 29 | puts "negative" 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /tests/range.rb: -------------------------------------------------------------------------------- 1 | gauntlet("range assigned to a local used in a case statement") do 2 | loc = 1..5 3 | 4 | 10.times do |i| 5 | case i 6 | when loc 7 | puts "#{i} in range" 8 | else 9 | puts "#{i} out of range" 10 | end 11 | end 12 | end 13 | 14 | gauntlet("range of strings in a case statement") do 15 | ["foo", "bar", "baz"].each do |str| 16 | case str 17 | when "bar".."baz" 18 | puts "it's a hit!" 19 | else 20 | puts "it's a miss" 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /compiler/testdata/ruby/test_control.rb: -------------------------------------------------------------------------------- 1 | x = 100 2 | 3 | while x > 0 do 4 | x -= 1 5 | end 6 | 7 | until x == 50 do 8 | x += 1 9 | end 10 | 11 | y = 0 12 | 13 | while x do 14 | y += 1 15 | if y > 5 16 | break 17 | end 18 | end 19 | 20 | until y > 100 21 | y += 1 22 | if y % 2 == 0 23 | next 24 | end 25 | puts y 26 | end 27 | 28 | for x in [1, 2, 3, 4] do 29 | puts x 30 | break if x == 3 31 | end 32 | 33 | for k, v in {foo: 1, bar: 2, baz: 3, quux: 4} do 34 | next if k == :foo || v == 10 35 | end 36 | -------------------------------------------------------------------------------- /tests/float.rb: -------------------------------------------------------------------------------- 1 | gauntlet("Float#negative?") do 2 | #TODO bug, parens required 3 | puts(-10.0.negative?) 4 | puts(0.0.negative?) 5 | puts(10.0.negative?) 6 | end 7 | 8 | gauntlet("Float#positive?") do 9 | #TODO bug, parens required 10 | puts(-10.0.positive?) 11 | puts(0.0.positive?) 12 | puts(10.0.positive?) 13 | end 14 | 15 | gauntlet("Float#zero?") do 16 | #TODO bug, parens required 17 | puts(-10.0.zero?) 18 | puts(0.0.zero?) 19 | puts(10.0.zero?) 20 | end 21 | 22 | gauntlet("Float#abs") do 23 | puts(-10.0.abs.positive?) 24 | end 25 | -------------------------------------------------------------------------------- /parser/noop.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "github.com/redneckbeard/thanos/types" 4 | 5 | type NoopNode struct { 6 | lineNo int 7 | } 8 | 9 | func (n *NoopNode) String() string { return "nil" } 10 | func (n *NoopNode) Type() types.Type { return types.NilType } 11 | func (n *NoopNode) SetType(t types.Type) {} 12 | func (n *NoopNode) LineNo() int { return n.lineNo } 13 | 14 | func (n *NoopNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 15 | return types.NilType, nil 16 | } 17 | 18 | func (n *NoopNode) Copy() Node { return n } 19 | -------------------------------------------------------------------------------- /stdlib/hash.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import "reflect" 4 | 5 | func MapKeys[K comparable, V any](m map[K]V) []K { 6 | keys := []K{} 7 | for k := range m { 8 | keys = append(keys, k) 9 | } 10 | return keys 11 | } 12 | 13 | func MapValues[K comparable, V any](m map[K]V) []V { 14 | vals := []V{} 15 | for _, v := range m { 16 | vals = append(vals, v) 17 | } 18 | return vals 19 | } 20 | 21 | func MapHasValue[K comparable, V any](m map[K]V, val V) bool { 22 | for _, v := range m { 23 | if reflect.DeepEqual(v, val) { 24 | return true 25 | } 26 | } 27 | return false 28 | } 29 | -------------------------------------------------------------------------------- /parser/stack.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | type Stack[T any] struct { 4 | stack []T 5 | } 6 | 7 | func (s *Stack[T]) Push(t T) { 8 | s.stack = append(s.stack, t) 9 | } 10 | 11 | func (s *Stack[T]) Pop() T { 12 | if len(s.stack) == 0 { 13 | var zero T 14 | return zero 15 | } 16 | last := s.stack[len(s.stack)-1] 17 | s.stack = s.stack[:len(s.stack)-1] 18 | return last 19 | } 20 | 21 | func (s *Stack[T]) Peek() T { 22 | if len(s.stack) == 0 { 23 | var zero T 24 | return zero 25 | } 26 | return s.stack[len(s.stack)-1] 27 | } 28 | 29 | func (s *Stack[T]) Size() int { 30 | return len(s.stack) 31 | } 32 | -------------------------------------------------------------------------------- /compiler/testdata/ruby/test_constants.rb: -------------------------------------------------------------------------------- 1 | PERMIT_AGE = 16 2 | 3 | class CTDriver 4 | LICENSE_AGE = 18 5 | KIND_MOTORCYCLE = :motorcycle 6 | KIND_COMMERCIAL = :cdl 7 | KIND_SCOOTER = :scooter 8 | LICENSE_KINDS = [KIND_MOTORCYCLE, KIND_COMMERCIAL, KIND_SCOOTER] 9 | 10 | def initialize(age) 11 | @age = age 12 | end 13 | 14 | def can_drive? 15 | @age >= LICENSE_AGE 16 | end 17 | end 18 | 19 | class CrossStateCommercialCTDriver < CTDriver 20 | LICENSE_AGE = 21 21 | 22 | eggplant = 'veg' 23 | end 24 | 25 | if CTDriver.new(19).can_drive? 26 | puts CrossStateCommercialCTDriver::LICENSE_AGE 27 | end 28 | -------------------------------------------------------------------------------- /parser/kernel.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "github.com/redneckbeard/thanos/types" 4 | 5 | // Placeholder in AST for Kernel method lookups 6 | type KernelNode struct{} 7 | 8 | func (n *KernelNode) String() string { return "Kernel" } 9 | func (n *KernelNode) Type() types.Type { return types.KernelType } 10 | func (n *KernelNode) SetType(t types.Type) {} 11 | func (n *KernelNode) LineNo() int { return 0 } 12 | 13 | func (n *KernelNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 14 | return types.KernelType, nil 15 | } 16 | 17 | func (n *KernelNode) Copy() Node { 18 | return n 19 | } 20 | -------------------------------------------------------------------------------- /stdlib/array_test.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestSubtractSlice(t *testing.T) { 9 | strLeft := []string{"a", "b", "e", "c", "d", "e"} 10 | strRight := []string{"b", "e"} 11 | strResult := SubtractSlice(strLeft, strRight) 12 | if !reflect.DeepEqual(strResult, []string{"a", "c", "d"}) { 13 | t.Fatal("SubtractSlice failed for []string args") 14 | } 15 | intLeft := []int{1, 2, 2, 4, 5, 6, 2, 8} 16 | intRight := []int{2, 6} 17 | intResult := SubtractSlice(intLeft, intRight) 18 | if !reflect.DeepEqual(intResult, []int{1, 4, 5, 8}) { 19 | t.Fatal("SubtractSlice failed for []int args") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /compiler/testdata/ruby/test_super.rb: -------------------------------------------------------------------------------- 1 | class Foo 2 | def a 3 | true 4 | end 5 | 6 | def b(x, y) 7 | @baz = x + y 8 | end 9 | end 10 | 11 | class Bar < Foo 12 | def a 13 | super 14 | end 15 | 16 | def b(x, y) 17 | super 18 | end 19 | end 20 | 21 | Bar.new.b(1.5, 2) 22 | 23 | class Baz < Foo 24 | def b(x, y) 25 | super(1.4, 2) + 1.0 26 | end 27 | end 28 | 29 | Baz.new.b(1.5, 2) 30 | 31 | class Quux < Foo 32 | def a 33 | anc = super 34 | !anc 35 | end 36 | 37 | def b(x, y) 38 | anc = super(x**2, y**2) 39 | anc + 1 40 | end 41 | end 42 | 43 | quux = Quux.new 44 | if quux.a 45 | puts quux.b(2.0, 4) 46 | end 47 | -------------------------------------------------------------------------------- /bst/ident.go: -------------------------------------------------------------------------------- 1 | package bst 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | ) 7 | 8 | type IdentTracker map[string][]*ast.Ident 9 | 10 | func (it IdentTracker) Get(name string) *ast.Ident { 11 | if i, ok := it[name]; ok { 12 | return i[0] 13 | } else { 14 | ident := ast.NewIdent(name) 15 | it[name] = []*ast.Ident{ident} 16 | return ident 17 | } 18 | } 19 | 20 | func (it IdentTracker) New(name string) *ast.Ident { 21 | if i, ok := it[name]; ok { 22 | incName := fmt.Sprintf("%s%d", name, len(i)) 23 | inc := ast.NewIdent(incName) 24 | it[name] = append(i, inc) 25 | it[incName] = []*ast.Ident{inc} 26 | return inc 27 | } else { 28 | return it.Get(name) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /compiler/testdata/go/test_control.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | x := 100 7 | for x > 0 { 8 | x-- 9 | } 10 | for x != 50 { 11 | x++ 12 | } 13 | y := 0 14 | for x { 15 | y++ 16 | if y > 5 { 17 | break 18 | } 19 | } 20 | for !(y > 100) { 21 | y++ 22 | if y%2 == 0 { 23 | continue 24 | } 25 | fmt.Println(y) 26 | } 27 | var x int 28 | for _, x = range []int{1, 2, 3, 4} { 29 | fmt.Println(x) 30 | if x == 3 { 31 | break 32 | } 33 | } 34 | var k string 35 | var v int 36 | for k, v = range map[string]int{"foo": 1, "bar": 2, "baz": 3, "quux": 4} { 37 | if k == "foo" || v == 10 { 38 | continue 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /stdlib/array.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import "reflect" 4 | 5 | func SubtractSlice[T any](left, right []T) []T { 6 | for _, x := range right { 7 | indices := []int{} 8 | for i, y := range left { 9 | if reflect.DeepEqual(x, y) { 10 | indices = append([]int{i}, indices...) 11 | } 12 | } 13 | for _, i := range indices { 14 | left = append(left[:i], left[i+1:]...) 15 | } 16 | } 17 | return left 18 | } 19 | 20 | func Uniq[T comparable](arr []T) []T { 21 | set := make(map[T]bool) 22 | order := []T{} 23 | for _, elem := range arr { 24 | if _, ok := set[elem]; !ok { 25 | set[elem] = true 26 | order = append(order, elem) 27 | } 28 | } 29 | return order 30 | } 31 | -------------------------------------------------------------------------------- /compiler/testdata/go/test_files.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | 9 | "github.com/redneckbeard/thanos/stdlib" 10 | ) 11 | 12 | var patt = regexp.MustCompile(`good`) 13 | 14 | func main() { 15 | f, _ := os.Open("stuff.txt") 16 | scanner := bufio.NewScanner(f) 17 | scanner.Split(stdlib.MakeSplitFunc("\n", false)) 18 | for scanner.Scan() { 19 | ln := scanner.Text() 20 | subbed := patt.ReplaceAllString(ln, stdlib.ConvertFromGsub(patt, "bad")) 21 | fmt.Println(subbed) 22 | } 23 | f.Close() 24 | f1, _ := os.OpenFile("writable.txt", 522, 0666) 25 | f1.WriteString("here are some bits") 26 | info, _ := f1.Info() 27 | result := info.Size() 28 | f1.Close() 29 | fmt.Println(true) 30 | } 31 | -------------------------------------------------------------------------------- /compiler/testdata/go/test_math.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | func main() { 9 | x := 10 / 2 10 | y := float64(x) / 2.0 11 | z := int(math.Pow(float64(x), 2)) 12 | a := int(math.Pow(float64(x), float64(x))) 13 | b := math.Pow(y, 2) 14 | c := 12.0 / 4 15 | d := int(math.Abs(-50)) 16 | e := int(math.Abs(float64(x))) 17 | for x := 0; x < 10; x++ { 18 | if x%2 == 0 { 19 | fmt.Println(x) 20 | } 21 | } 22 | for x := 15; x >= 10; x-- { 23 | if x%2 == 1 { 24 | fmt.Println(x) 25 | } 26 | } 27 | for x := -5; x <= 5; x++ { 28 | switch { 29 | case x == 0: 30 | fmt.Println("zero") 31 | case x > 0: 32 | fmt.Println("positive") 33 | case x < 0: 34 | fmt.Println("negative") 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /stdlib/string.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | func Hex(s string) int { 10 | var hex, base int 11 | if !(len(s) > 2 && s[:2] == "0x") { 12 | base = 16 13 | } 14 | if i, err := strconv.ParseInt(s, base, 0); err == nil { 15 | hex = int(i) 16 | } 17 | return hex 18 | } 19 | 20 | func Reverse(s string) string { 21 | runes := []rune(s) 22 | i, j := 0, len(runes)-1 23 | for i < j { 24 | runes[i], runes[j] = runes[j], runes[i] 25 | i++ 26 | j-- 27 | } 28 | return string(runes) 29 | } 30 | 31 | func Join[T fmt.Stringer](t []T, delim string) string { 32 | segments := []string{} 33 | for _, segment := range t { 34 | segments = append(segments, segment.String()) 35 | } 36 | return strings.Join(segments, delim) 37 | } 38 | -------------------------------------------------------------------------------- /compiler/testdata/ruby/test_arrays.rb: -------------------------------------------------------------------------------- 1 | def make_arr(a, b, c) 2 | arr = [a, b, c] 3 | arr << a * b * c 4 | if a > 10 5 | return [b] 6 | end 7 | arr 8 | end 9 | 10 | def sum(a) 11 | a.reduce(0) do |acc, n| 12 | acc + n 13 | end 14 | end 15 | 16 | def squares_plus_one(a) 17 | a.map do |i| 18 | squared = i*i 19 | squared + 1 20 | end 21 | end 22 | 23 | def double_third(a) 24 | a[2] * 2 25 | end 26 | 27 | def length_is_size(a) 28 | a.size == a.length 29 | end 30 | 31 | def swap_positions(a, b) 32 | return b, a 33 | end 34 | 35 | arr = make_arr(1, 2, 3) 36 | qpo = squares_plus_one([1,2,3,4]).select do |x| 37 | x % 2 == 0 38 | end.length 39 | total = sum([1,2,3,4]) 40 | doubled = double_third([1,2,3]) 41 | foo = length_is_size([1,2,3]) 42 | i, b = swap_positions true, 10 43 | 44 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redneckbeard/thanos 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/alecthomas/chroma v0.10.0 7 | github.com/fatih/color v1.13.0 8 | github.com/redneckbeard/thanos/stdlib v0.0.0-20220324042549-39b620fb67ac 9 | github.com/spf13/cobra v1.3.0 10 | golang.org/x/tools v0.1.10 11 | ) 12 | 13 | require ( 14 | github.com/dlclark/regexp2 v1.4.0 // indirect 15 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 16 | github.com/mattn/go-colorable v0.1.12 // indirect 17 | github.com/mattn/go-isatty v0.0.14 // indirect 18 | github.com/spf13/pflag v1.0.5 // indirect 19 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect 20 | golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886 // indirect 21 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /types/simple_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type Simple"; DO NOT EDIT. 2 | 3 | package types 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ConstType-0] 12 | _ = x[NilType-1] 13 | _ = x[FuncType-2] 14 | _ = x[AnyType-3] 15 | _ = x[ErrorType-4] 16 | } 17 | 18 | const _Simple_name = "ConstTypeNilTypeFuncTypeAnyTypeErrorType" 19 | 20 | var _Simple_index = [...]uint8{0, 9, 16, 24, 31, 40} 21 | 22 | func (i Simple) String() string { 23 | if i < 0 || i >= Simple(len(_Simple_index)-1) { 24 | return "Simple(" + strconv.FormatInt(int64(i), 10) + ")" 25 | } 26 | return _Simple_name[_Simple_index[i]:_Simple_index[i+1]] 27 | } 28 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod tidy 7 | # you may remove this if you don't need go generate 8 | - go generate ./... 9 | builds: 10 | - env: 11 | - CGO_ENABLED=0 12 | goos: 13 | - linux 14 | - windows 15 | - darwin 16 | archives: 17 | - replacements: 18 | darwin: Darwin 19 | linux: Linux 20 | windows: Windows 21 | 386: i386 22 | amd64: x86_64 23 | checksum: 24 | name_template: 'checksums.txt' 25 | snapshot: 26 | name_template: "{{ incpatch .Version }}-next" 27 | changelog: 28 | sort: asc 29 | filters: 30 | exclude: 31 | - '^docs:' 32 | - '^test:' 33 | -------------------------------------------------------------------------------- /tests/int.rb: -------------------------------------------------------------------------------- 1 | gauntlet("Int#abs") do 2 | #TODO bug, parens required 3 | puts(-10.abs) 4 | puts(10.abs) 5 | end 6 | 7 | gauntlet("Int#negative?") do 8 | #TODO bug, parens required 9 | puts(-10.negative?) 10 | puts(0.negative?) 11 | puts(10.negative?) 12 | end 13 | 14 | gauntlet("Int#positive?") do 15 | #TODO bug, parens required 16 | puts(-10.positive?) 17 | puts(0.positive?) 18 | puts(10.positive?) 19 | end 20 | 21 | gauntlet("Int#zero?") do 22 | #TODO bug, parens required 23 | puts(-10.zero?) 24 | puts(0.zero?) 25 | puts(10.zero?) 26 | end 27 | 28 | gauntlet("Int#times") do 29 | 10.times do |i| 30 | puts i 31 | end 32 | end 33 | 34 | gauntlet("Int#upto") do 35 | 10.upto(20) do |i| 36 | puts i 37 | end 38 | end 39 | 40 | gauntlet("Int#downto") do 41 | 20.downto(10) do |i| 42 | puts i 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /parser/scopechain_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestScopeChain(t *testing.T) { 8 | ruby := ` 9 | def foo; return "foo"; end 10 | def bar; return "bar"; end 11 | def baz; return "baz"; end 12 | def quux; return "quux"; end 13 | 14 | foo() 15 | bar() 16 | baz() 17 | quux() 18 | ` 19 | scopeNames := [][]string{ 20 | {"__main__", "foo"}, 21 | {"__main__", "bar"}, 22 | {"__main__", "baz"}, 23 | {"__main__", "quux"}, 24 | } 25 | p, err := ParseString(ruby) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | for _, scopeName := range scopeNames { 30 | method := p.MethodSetStack.Peek().Methods[scopeName[1]] 31 | for j, scope := range scopeName { 32 | if method.Scope[j].Name() != scope { 33 | t.Errorf("expected scope name to be '%s' but found '%s'", scope, method.Scope[j].Name()) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /stdlib/file.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "os" 7 | ) 8 | 9 | func MakeSplitFunc(separator string, chomp bool) bufio.SplitFunc { 10 | return func(data []byte, atEOF bool) (advance int, token []byte, err error) { 11 | if atEOF && len(data) == 0 { 12 | return 0, nil, nil 13 | } 14 | if i := bytes.Index(data, []byte(separator)); i >= 0 { 15 | upper := i 16 | if !chomp { 17 | upper += len(separator) 18 | } 19 | return i + len(separator), data[:upper], nil 20 | } 21 | if atEOF { 22 | return len(data), data, nil 23 | } 24 | return 0, nil, nil 25 | } 26 | } 27 | 28 | var OpenModes = map[string]int{ 29 | "r": os.O_RDONLY, 30 | "r+": os.O_RDWR, 31 | "w": os.O_WRONLY | os.O_CREATE, 32 | "w+": os.O_RDWR | os.O_CREATE | os.O_TRUNC, 33 | "a": os.O_WRONLY | os.O_CREATE | os.O_APPEND, 34 | "a+": os.O_RDWR | os.O_CREATE | os.O_APPEND, 35 | } 36 | -------------------------------------------------------------------------------- /tests/class.rb: -------------------------------------------------------------------------------- 1 | gauntlet("simple class, no attrs") do 2 | class Foo 3 | def swap(dot_separated) 4 | dot_separated.gsub(/(\w+)\.(\w+)/, '\2.\1') 5 | end 6 | end 7 | puts Foo.new.swap("left.right") 8 | end 9 | 10 | gauntlet("simple class, methods reference other methods") do 11 | class Foo 12 | def swap(dot_separated) 13 | dot_separated.gsub(patt(), '\2.\1') 14 | end 15 | 16 | def patt 17 | /(\w+)\.(\w+)/ 18 | end 19 | end 20 | puts Foo.new.swap("left.right") 21 | end 22 | 23 | gauntlet("simple class, methods reference methods in outer scope") do 24 | def unit(oz) 25 | if oz > 16 then "lbs" else "oz" end 26 | end 27 | 28 | class Foo 29 | def format(oz) 30 | "#{to_lbs(oz)} #{unit(oz)}" 31 | end 32 | 33 | def to_lbs(oz) 34 | if oz > 16 then oz / 16 else oz end 35 | end 36 | end 37 | puts Foo.new.format(18) 38 | end 39 | -------------------------------------------------------------------------------- /tests/hash.rb: -------------------------------------------------------------------------------- 1 | gauntlet("Hash#delete") do 2 | puts({:foo => "x", :bar => "y"}.delete(:foo)) 3 | end 4 | 5 | gauntlet("Hash#delete (with block)") do 6 | result = {:foo => "x", :bar => "y"}.delete(:baz) do |k| 7 | "default: #{k}" 8 | end 9 | puts result 10 | end 11 | 12 | gauntlet("Hash#delete_if") do 13 | h = {foo: 1, bar: 2, baz: 3, quux: 4} 14 | smaller = h.delete_if do |k, v| 15 | v > 2 16 | end 17 | # we have the keys removed 18 | h.each do |k, v| 19 | puts "#{k}: #{v}" 20 | end 21 | puts smaller.length == h.length 22 | end 23 | 24 | gauntlet("Hash#values") do 25 | h = {foo: 1, bar: 2, baz: 3, quux: 4} 26 | if h.has_value?(3) 27 | h.values.sort!.each do |v| 28 | puts v 29 | end 30 | end 31 | end 32 | 33 | gauntlet("Hash#keys") do 34 | h = {foo: 1, bar: 2, baz: 3, quux: 4} 35 | if h.has_key?(:foo) 36 | h.keys.sort!.each do |k| 37 | puts k 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /compiler/testdata/ruby/test_strings.rb: -------------------------------------------------------------------------------- 1 | def hello(name) 2 | puts "debug message" 3 | "Hello, " + name 4 | end 5 | 6 | def hello_interp(name, age) 7 | comparative = if age > 40 8 | "older" 9 | else 10 | "younger" 11 | end 12 | puts "#{name} is #{comparative} than me, age #{age}" 13 | end 14 | 15 | def matches_foo(foolike) 16 | if /foo/ =~ foolike 17 | puts "got a match" 18 | end 19 | end 20 | 21 | def matches_interp(foo, bar) 22 | if /foo#{foo}/ =~ bar 23 | puts "got a match" 24 | end 25 | end 26 | 27 | def extract_third_octet(ip) 28 | ip.match(/\d{1,3}\.\d{1,3}\.(?\d{1,3})\.\d{1,3}/)["third"] 29 | end 30 | 31 | greeting = hello("me") 32 | hello_interp("Steve", 38) 33 | matches_foo("football") 34 | matches_interp(10, "foofoo") 35 | extract_third_octet("127.0.0.1") 36 | terms = %w{foo bar baz} 37 | interp_terms = %W{foo #{"BAR BAZ QUUX"} bar} 38 | puts `man -P cat #{"date"}` 39 | -------------------------------------------------------------------------------- /scripts/update-stdlib-version: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # thanos/stdlib is a dependency of both thanos itself and applications compiled 4 | # with thanos. Building the thanos main module requires the stdlib require 5 | # directive to be up to date with the latest commit to that directory, but at 6 | # present the main module cannot have a replace directive. Thus, the replace 7 | # directive that ensures local development references the local copy of the 8 | # repository lives in go.work, and we use a post-commit hook to ensure we 9 | # always update go.mod whenever we make a commit updating files in stdlib. 10 | # There is probably a better way of doing this, and if not hopefully there will 11 | # be in the near future. 12 | version_suffix=`TZ=UTC git --no-pager log --quiet --abbrev=12 --date='format-local:%Y%m%d%H%M%S' --format="%cd-%h" ./stdlib | head -n 1` 13 | sed "s/stdlib v0.0.0-.*/stdlib v0.0.0-$version_suffix/" go.mod > go.mod.tmp 14 | mv go.mod.tmp go.mod 15 | git add go.mod 16 | echo "Must commit updated go.mod" 17 | -------------------------------------------------------------------------------- /parser/stack_state.go: -------------------------------------------------------------------------------- 1 | //compiled from https://github.com/whitequark/parser/blob/master/lib/parser/lexer/stack_state.rb 2 | package parser 3 | 4 | type StackState struct { 5 | name string 6 | stack int 7 | } 8 | 9 | func NewStackState(name string) *StackState { 10 | return &StackState{name: name} 11 | } 12 | 13 | func (s *StackState) Clear() int { 14 | s.stack = 0 15 | return s.stack 16 | } 17 | 18 | func (s *StackState) Push(bit bool) bool { 19 | var bit_value int 20 | if bit { 21 | bit_value = 1 22 | } else { 23 | bit_value = 0 24 | } 25 | s.stack = s.stack<<1 | bit_value 26 | return bit 27 | } 28 | 29 | func (s *StackState) Pop() bool { 30 | bit_value := s.stack & 1 31 | s.stack >>= 1 32 | return bit_value == 1 33 | } 34 | 35 | func (s *StackState) Lexpop() bool { 36 | s.stack = s.stack>>1 | s.stack&1 37 | return s.stack&(1<<0)>>0 == 1 38 | } 39 | 40 | func (s *StackState) IsActive() bool { 41 | return s.stack&(1<<0)>>0 == 1 42 | } 43 | 44 | func (s *StackState) IsEmpty() bool { 45 | return s.stack == 0 46 | } 47 | -------------------------------------------------------------------------------- /compiler/testdata/ruby/test_classes.rb: -------------------------------------------------------------------------------- 1 | class Vehicle 2 | attr_reader :starting_miles 3 | attr_writer :registration 4 | attr_accessor :vin 5 | 6 | def initialize(starting_miles) 7 | @starting_miles = starting_miles 8 | @no_reader = "unexported" 9 | @vin = 100 10 | end 11 | 12 | def drive(x) 13 | @starting_miles += x 14 | end 15 | 16 | def mileage 17 | log 18 | "#{@starting_miles} miles" 19 | end 20 | 21 | private 22 | 23 | def log 24 | puts "log was called" 25 | end 26 | end 27 | 28 | class Car < Vehicle 29 | def drive(x) 30 | super 31 | @starting_miles += 1 32 | end 33 | # overriding a private method is fine and on the child class is then public 34 | def log 35 | puts "it's a different method!" 36 | super 37 | end 38 | end 39 | 40 | puts [Car.new(10), Car.new(20), Car.new(30)].map do |car| 41 | if car.instance_of?(Car) # only here to prove inheritance from Object 42 | car.drive(100) 43 | end 44 | car.registration = "XXXXXX" 45 | car.vin += 1 46 | "#{car.mileage}, started at #{car.starting_miles}" 47 | end 48 | -------------------------------------------------------------------------------- /compiler/block_stmt.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "go/ast" 5 | 6 | "github.com/redneckbeard/thanos/parser" 7 | "github.com/redneckbeard/thanos/types" 8 | ) 9 | 10 | func (g *GoProgram) CompileBlockStmt(node parser.Node) *ast.BlockStmt { 11 | blockStmt := g.newBlockStmt() 12 | defer g.BlockStack.Pop() 13 | switch n := node.(type) { 14 | case *parser.Condition: 15 | if n.False != nil { 16 | g.CompileStmt(n) 17 | return blockStmt 18 | } else { 19 | return g.CompileBlockStmt(n.True) 20 | } 21 | case parser.Statements: 22 | last := len(n) - 1 23 | for i, s := range n { 24 | if i == last { 25 | if g.State.Peek() == InReturnStatement && s.Type() != types.NilType { 26 | t := s.Type() 27 | s = &parser.ReturnNode{Val: parser.ArgsNode{s}} 28 | s.SetType(t) 29 | } else if g.State.Peek() == InCondAssignment { 30 | s = &parser.AssignmentNode{ 31 | Left: g.CurrentLhs, 32 | Right: []parser.Node{s}, 33 | Reassignment: true, 34 | } 35 | } 36 | } 37 | g.CompileStmt(s) 38 | } 39 | return blockStmt 40 | default: 41 | return &ast.BlockStmt{} 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /compiler/testdata/go/test_arrays.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func Make_arr(a, b, c int) []int { 4 | arr := []int{a, b, c} 5 | arr = append(arr, a*b*c) 6 | if a > 10 { 7 | return []int{b} 8 | } 9 | return arr 10 | } 11 | func Sum(a []int) int { 12 | acc := 0 13 | for _, n := range a { 14 | acc = acc + n 15 | } 16 | return acc 17 | } 18 | func Squares_plus_one(a []int) []int { 19 | mapped := []int{} 20 | for _, i := range a { 21 | squared := i * i 22 | mapped = append(mapped, squared+1) 23 | } 24 | return mapped 25 | } 26 | func Double_third(a []int) int { 27 | return a[2] * 2 28 | } 29 | func Length_is_size(a []int) bool { 30 | return len(a) == len(a) 31 | } 32 | func Swap_positions(a bool, b int) (int, bool) { 33 | return b, a 34 | } 35 | func main() { 36 | arr := Make_arr(1, 2, 3) 37 | selected := []int{} 38 | for _, x := range Squares_plus_one([]int{1, 2, 3, 4}) { 39 | if x%2 == 0 { 40 | selected = append(selected, x) 41 | } 42 | } 43 | qpo := len(selected) 44 | total := Sum([]int{1, 2, 3, 4}) 45 | doubled := Double_third([]int{1, 2, 3}) 46 | foo := Length_is_size([]int{1, 2, 3}) 47 | i, b := Swap_positions(true, 10) 48 | } 49 | -------------------------------------------------------------------------------- /parser/lexstate_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -output parser/lexstate_string.go -type LexState ./parser"; DO NOT EDIT. 2 | 3 | package parser 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[Clear-0] 12 | _ = x[InSymbol-1] 13 | _ = x[InGlobal-2] 14 | _ = x[InFloat-3] 15 | _ = x[InNumber-4] 16 | _ = x[InCVar-5] 17 | _ = x[InIVar-6] 18 | _ = x[InPunct-7] 19 | _ = x[InInterpString-8] 20 | _ = x[InInterp-9] 21 | _ = x[InRawString-10] 22 | _ = x[InRegex-11] 23 | _ = x[OpenBrace-12] 24 | } 25 | 26 | const _LexState_name = "ClearInSymbolInGlobalInFloatInNumberInCVarInIVarInPunctInInterpStringInInterpInRawStringInRegexOpenBrace" 27 | 28 | var _LexState_index = [...]uint8{0, 5, 13, 21, 28, 36, 42, 48, 55, 69, 77, 88, 95, 104} 29 | 30 | func (i LexState) String() string { 31 | if i < 0 || i >= LexState(len(_LexState_index)-1) { 32 | return "LexState(" + strconv.FormatInt(int64(i), 10) + ")" 33 | } 34 | return _LexState_name[_LexState_index[i]:_LexState_index[i+1]] 35 | } 36 | -------------------------------------------------------------------------------- /parser/node.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "github.com/redneckbeard/thanos/types" 4 | 5 | type Node interface { 6 | String() string 7 | TargetType(ScopeChain, *Class) (types.Type, error) 8 | Type() types.Type 9 | SetType(types.Type) 10 | LineNo() int 11 | Copy() Node 12 | } 13 | 14 | func GetType(n Node, scope ScopeChain, class *Class) (t types.Type, err error) { 15 | t = n.Type() 16 | if t == nil { 17 | if ident, ok := n.(*IdentNode); ok { 18 | if loc := scope.ResolveVar(ident.Val); loc != BadLocal { 19 | if loc.Type() == nil { 20 | return nil, NewParseError(n, "No type inferred for local variable '%s'", ident.Val) 21 | } 22 | ident.SetType(loc.Type()) 23 | } else if m, ok := globalMethodSet.Methods[ident.Val]; ok { 24 | if err := m.Analyze(globalMethodSet); err != nil { 25 | return nil, err 26 | } 27 | ident.MethodCall = &MethodCall{ 28 | Method: m, 29 | MethodName: m.Name, 30 | _type: m.ReturnType(), 31 | lineNo: ident.lineNo, 32 | } 33 | return m.ReturnType(), nil 34 | } 35 | } 36 | if t, err = n.TargetType(scope, class); err != nil { 37 | return nil, err 38 | } else { 39 | n.SetType(t) 40 | return t, nil 41 | } 42 | } 43 | return t, nil 44 | } 45 | -------------------------------------------------------------------------------- /compiler/testdata/ruby/test_arguments.rb: -------------------------------------------------------------------------------- 1 | def pos_and_kw(foo, bar: true) 2 | if bar 3 | puts foo 4 | end 5 | end 6 | 7 | pos_and_kw("x") 8 | pos_and_kw("x", bar: false) 9 | 10 | def all_kw(foo: "y", bar: true) 11 | if bar 12 | puts foo 13 | end 14 | end 15 | 16 | all_kw(bar: false) 17 | all_kw(bar: false, foo: "z") 18 | 19 | def defaults(foo = "x", bar = "y") 20 | "foo: #{foo}, bar: #{bar}" 21 | end 22 | 23 | defaults 24 | defaults("z") 25 | defaults("z", "a") 26 | 27 | class Foo 28 | def initialize(foo = 10) 29 | @foo = foo 30 | end 31 | end 32 | 33 | Foo.new 34 | 35 | def splat(a, *b, c: false) 36 | if c 37 | b[0] 38 | else 39 | a 40 | end 41 | end 42 | 43 | def double_splat(foo:, **bar) 44 | foo + bar[:baz] 45 | end 46 | 47 | splat(9, 2, 3) 48 | splat(9, 2, c: true) 49 | splat(9) 50 | splat(9, *[1, 2]) 51 | splat(9, 5, *[1, 2]) 52 | 53 | double_splat(foo: 1, bar: 2, baz: 3) 54 | double_splat(baz: 3, foo: 1) 55 | double_splat(**{foo: 1, baz: 4}) 56 | hash_from_elsewhere = {foo: 1, baz: 4} 57 | double_splat(**hash_from_elsewhere) 58 | 59 | foo = [1, 2, 3] 60 | 61 | a, *b = foo 62 | c, d, *e = foo 63 | 64 | syms = [:foo, :bar, :baz] 65 | 66 | f = :quux, *syms 67 | g, h, i = :quux, *syms 68 | x, y, *z = :quux, *syms 69 | 70 | -------------------------------------------------------------------------------- /parser/range.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/redneckbeard/thanos/types" 7 | ) 8 | 9 | type RangeNode struct { 10 | Lower, Upper Node 11 | Inclusive bool 12 | lineNo int 13 | _type types.Type 14 | } 15 | 16 | func (n *RangeNode) String() string { 17 | rangeOp := "..." 18 | if n.Inclusive { 19 | rangeOp = ".." 20 | } 21 | upper := "" 22 | if n.Upper != nil { 23 | upper = n.Upper.String() 24 | } 25 | return fmt.Sprintf("(%s%s%s)", n.Lower, rangeOp, upper) 26 | } 27 | func (n *RangeNode) Type() types.Type { return n._type } 28 | func (n *RangeNode) SetType(t types.Type) { n._type = t } 29 | func (n *RangeNode) LineNo() int { return n.lineNo } 30 | 31 | func (n *RangeNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 32 | var t types.Type 33 | for _, bound := range []Node{n.Lower, n.Upper} { 34 | if bound != nil { 35 | bt, err := GetType(bound, locals, class) 36 | if err != nil { 37 | return nil, err 38 | } 39 | if t != nil && t != bt { 40 | return nil, NewParseError(n, "Tried to construct range from disparate types %s and %s", t, bt) 41 | } 42 | t = bt 43 | } 44 | } 45 | return types.NewRange(t), nil 46 | } 47 | 48 | func (n *RangeNode) Copy() Node { 49 | return &RangeNode{n.Lower.Copy(), n.Upper.Copy(), n.Inclusive, n.lineNo, n._type} 50 | } 51 | -------------------------------------------------------------------------------- /tests/string.rb: -------------------------------------------------------------------------------- 1 | gauntlet("shelling out with backticks") do 2 | %w{date time awk sed}.each do |cmd| 3 | puts `man -P cat #{cmd}` 4 | end 5 | end 6 | 7 | gauntlet("escape sequences") do 8 | ["foo\n", "f\oo", 'f\oo', '\'', "\\\"" ].each do |s| 9 | puts s 10 | end 11 | end 12 | 13 | gauntlet("String#hex") do 14 | %w{0x0a -1234 0 wombat}.each do |s| 15 | puts s.hex 16 | end 17 | end 18 | 19 | gauntlet("String#split") do 20 | " now's the time".split.each {|s| puts s} 21 | " now's the time".split(' ').each {|s| puts s} 22 | " now's the time".split(/ /).each {|s| puts s} 23 | "1, 2.34,56, 7".split(/,\s*/).each {|s| puts s} 24 | "hello".split(//).each {|s| puts s} 25 | "hello".split(//, 3).each {|s| puts s} 26 | "hi mom".split(/\s*/).each {|s| puts s} 27 | "mellow yellow".split("ello").each {|s| puts s} 28 | "1,2,,3,4,,".split(',').each {|s| puts s} 29 | "1,2,,3,4,,".split(',', 4).each {|s| puts s} 30 | "1,2,,3,4,,".split(',', -4).each {|s| puts s} 31 | #"1:2:3".split(/(:)()()/, 2).each {|s| puts s} 32 | "".split(',', -1).each {|s| puts s} 33 | end 34 | 35 | gauntlet("String#strip etc.") do 36 | puts " hello ".strip 37 | puts "\tgoodbye\r\n".strip 38 | puts " hello ".lstrip 39 | puts " hello ".rstrip 40 | end 41 | 42 | gauntlet("string indexes") do 43 | puts "foobarbarbaz"[0] 44 | puts "foobarbaz"[1..3] 45 | end 46 | -------------------------------------------------------------------------------- /compiler/testdata/go/test_constants.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | const CTDriverLICENSE_AGE = 18 6 | const CTDriverKIND_MOTORCYCLE = "motorcycle" 7 | const CTDriverKIND_COMMERCIAL = "cdl" 8 | const CTDriverKIND_SCOOTER = "scooter" 9 | const CrossStateCommercialCTDriverLICENSE_AGE = 21 10 | const PERMIT_AGE = 16 11 | 12 | var CTDriverLICENSE_KINDS []string = []string{CTDriverKIND_MOTORCYCLE, CTDriverKIND_COMMERCIAL, CTDriverKIND_SCOOTER} 13 | 14 | type CTDriver struct { 15 | age int 16 | } 17 | 18 | func NewCTDriver(age int) *CTDriver { 19 | newInstance := &CTDriver{} 20 | newInstance.Initialize(age) 21 | return newInstance 22 | } 23 | func (c *CTDriver) Initialize(age int) int { 24 | c.age = age 25 | return c.age 26 | } 27 | func (c *CTDriver) Can_drive() bool { 28 | return c.age >= CTDriverLICENSE_AGE 29 | } 30 | 31 | type CrossStateCommercialCTDriver struct { 32 | age int 33 | } 34 | 35 | func NewCrossStateCommercialCTDriver(age int) *CrossStateCommercialCTDriver { 36 | newInstance := &CrossStateCommercialCTDriver{} 37 | newInstance.Initialize(age) 38 | return newInstance 39 | } 40 | func (c *CrossStateCommercialCTDriver) Initialize(age int) int { 41 | c.age = age 42 | return c.age 43 | } 44 | func (c *CrossStateCommercialCTDriver) Can_drive() bool { 45 | return c.age >= CTDriverLICENSE_AGE 46 | } 47 | func main() { 48 | if NewCTDriver(19).Can_drive() { 49 | fmt.Println(CrossStateCommercialCTDriverLICENSE_AGE) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 NAME HERE 3 | 4 | */ 5 | package cmd 6 | 7 | import ( 8 | "os" 9 | 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // rootCmd represents the base command when called without any subcommands 14 | var rootCmd = &cobra.Command{ 15 | Use: "thanos", 16 | Short: "Ruby -> Go at the snap of your fingers", 17 | Long: `Thanos generates Go from Ruby source. It is intended as an aid to safely porting applications or critical sections thereof rather than a replacement for a Ruby runtime.`, 18 | // Uncomment the following line if your bare application 19 | // has an action associated with it: 20 | // Run: func(cmd *cobra.Command, args []string) { }, 21 | } 22 | 23 | // Execute adds all child commands to the root command and sets flags appropriately. 24 | // This is called by main.main(). It only needs to happen once to the rootCmd. 25 | func Execute() { 26 | err := rootCmd.Execute() 27 | if err != nil { 28 | os.Exit(1) 29 | } 30 | } 31 | 32 | func init() { 33 | // Here you will define your flags and configuration settings. 34 | // Cobra supports persistent flags, which, if defined here, 35 | // will be global for your application. 36 | 37 | // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.thanos.yaml)") 38 | 39 | // Cobra also supports local flags, which will only run 40 | // when this action is called directly. 41 | rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 42 | } 43 | -------------------------------------------------------------------------------- /compiler/testdata/go/test_strings.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/redneckbeard/thanos/stdlib" 10 | ) 11 | 12 | var patt = regexp.MustCompile(`foo`) 13 | var patt1 = regexp.MustCompile(`\d{1,3}\.\d{1,3}\.(?P\d{1,3})\.\d{1,3}`) 14 | 15 | func Hello(name string) string { 16 | fmt.Println("debug message") 17 | return "Hello, " + name 18 | } 19 | func Hello_interp(name string, age int) { 20 | var comparative string 21 | if age > 40 { 22 | comparative = "older" 23 | } else { 24 | comparative = "younger" 25 | } 26 | fmt.Printf("%s is %s than me, age %d\n", name, comparative, age) 27 | } 28 | func Matches_foo(foolike string) { 29 | if patt.MatchString(foolike) { 30 | fmt.Println("got a match") 31 | } 32 | } 33 | func Matches_interp(foo int, bar string) { 34 | patt, _ := regexp.Compile(fmt.Sprintf(`foo%d`, foo)) 35 | if patt.MatchString(bar) { 36 | fmt.Println("got a match") 37 | } 38 | } 39 | func Extract_third_octet(ip string) string { 40 | return stdlib.NewMatchData(patt1, ip).GetByName("third") 41 | } 42 | func main() { 43 | greeting := Hello("me") 44 | Hello_interp("Steve", 38) 45 | Matches_foo("football") 46 | Matches_interp(10, "foofoo") 47 | Extract_third_octet("127.0.0.1") 48 | terms := strings.Fields(`foo bar baz`) 49 | interp_terms := []string{"foo", fmt.Sprintf("%s", "BAR BAZ QUUX"), "bar"} 50 | output, _ := exec.Command("man", "-P", "cat", fmt.Sprintf("%s", "date")).Output() 51 | fmt.Println(string(output)) 52 | } 53 | -------------------------------------------------------------------------------- /cmd/compile.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/alecthomas/chroma/quick" 8 | "github.com/fatih/color" 9 | "github.com/redneckbeard/thanos/compiler" 10 | "github.com/redneckbeard/thanos/parser" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var Target, Source string 15 | 16 | var compileCmd = &cobra.Command{ 17 | Use: "compile", 18 | Short: "Convert Ruby to Go", 19 | Long: `Compile source Ruby to Go to the best of thanos's ability. Lacking functionality is described at https://github.com/redneckbeard/thanos#readme and https://github.com/redneckbeard/thanos/issues`, 20 | Run: func(cmd *cobra.Command, args []string) { 21 | if Source == "" { 22 | color.Green("Input your Ruby and compile with Ctrl-D.") 23 | } 24 | program, err := parser.ParseFile(Source) 25 | if err != nil { 26 | color.Red(err.Error()) 27 | return 28 | } 29 | compiled, _ := compiler.Compile(program) 30 | if Target == "" { 31 | color.Green(strings.Repeat("-", 20)) 32 | quick.Highlight(os.Stdout, compiled, "go", "terminal256", "monokai") 33 | } else { 34 | err = os.WriteFile(Target, []byte(compiled), 0644) 35 | if err != nil { 36 | color.Red(err.Error()) 37 | } 38 | } 39 | }, 40 | } 41 | 42 | func init() { 43 | rootCmd.AddCommand(compileCmd) 44 | compileCmd.Flags().StringVarP(&Target, "target", "t", "", "Destination for resulting Go (defaults to stdout)") 45 | compileCmd.Flags().StringVarP(&Source, "source", "s", "", "Destination for resulting Go (defaults to stdin)") 46 | } 47 | -------------------------------------------------------------------------------- /types/float.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "go/ast" 5 | 6 | "github.com/redneckbeard/thanos/bst" 7 | ) 8 | 9 | type Float struct { 10 | *proto 11 | } 12 | 13 | var FloatType = Float{newProto("Float", "Numeric", ClassRegistry)} 14 | 15 | var FloatClass = NewClass("Float", "Numeric", FloatType, ClassRegistry) 16 | 17 | func (t Float) Equals(t2 Type) bool { return t == t2 } 18 | func (t Float) String() string { return "FloatType" } 19 | func (t Float) GoType() string { return "float64" } 20 | func (t Float) IsComposite() bool { return false } 21 | 22 | func (t Float) MethodReturnType(m string, b Type, args []Type) (Type, error) { 23 | return t.proto.MustResolve(m, false).ReturnType(t, b, args) 24 | } 25 | 26 | func (t Float) BlockArgTypes(m string, args []Type) []Type { 27 | return t.proto.MustResolve(m, false).blockArgs(t, args) 28 | } 29 | 30 | func (t Float) TransformAST(m string, rcvr ast.Expr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 31 | return t.proto.MustResolve(m, false).TransformAST(TypeExpr{t, rcvr}, args, blk, it) 32 | } 33 | 34 | func (t Float) Resolve(m string) (MethodSpec, bool) { 35 | return t.proto.Resolve(m, false) 36 | } 37 | 38 | func (t Float) MustResolve(m string) MethodSpec { 39 | return t.proto.MustResolve(m, false) 40 | } 41 | 42 | func (t Float) HasMethod(m string) bool { 43 | return t.proto.HasMethod(m, false) 44 | } 45 | 46 | func (t Float) Alias(existingMethod, newMethod string) { 47 | t.proto.MakeAlias(existingMethod, newMethod, false) 48 | } 49 | 50 | func init() { 51 | } 52 | -------------------------------------------------------------------------------- /types/symbol.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | 7 | "github.com/redneckbeard/thanos/bst" 8 | ) 9 | 10 | type Symbol struct { 11 | *proto 12 | } 13 | 14 | var SymbolType = Symbol{newProto("Symbol", "Object", ClassRegistry)} 15 | 16 | var SymbolClass = NewClass("Symbol", "Object", SymbolType, ClassRegistry) 17 | 18 | func (t Symbol) Equals(t2 Type) bool { return t == t2 } 19 | func (t Symbol) String() string { return "SymbolType" } 20 | func (t Symbol) GoType() string { return "string" } 21 | func (t Symbol) IsComposite() bool { return false } 22 | 23 | func (t Symbol) MethodReturnType(m string, b Type, args []Type) (Type, error) { 24 | return t.proto.MustResolve(m, false).ReturnType(t, b, args) 25 | } 26 | 27 | func (t Symbol) BlockArgTypes(m string, args []Type) []Type { 28 | return t.proto.MustResolve(m, false).blockArgs(t, args) 29 | } 30 | 31 | func (t Symbol) TransformAST(m string, rcvr ast.Expr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 32 | return t.proto.MustResolve(m, false).TransformAST(TypeExpr{t, rcvr}, args, blk, it) 33 | } 34 | 35 | func (t Symbol) Resolve(m string) (MethodSpec, bool) { 36 | return t.proto.Resolve(m, false) 37 | } 38 | 39 | func (t Symbol) MustResolve(m string) MethodSpec { 40 | return t.proto.MustResolve(m, false) 41 | } 42 | 43 | func (t Symbol) HasMethod(m string) bool { 44 | return t.proto.HasMethod(m, false) 45 | } 46 | 47 | func (t Symbol) Alias(existingMethod, newMethod string) { 48 | t.proto.MakeAlias(existingMethod, newMethod, false) 49 | } 50 | 51 | func init() { 52 | SymbolType.Def("==", simpleComparisonOperatorSpec(token.EQL)) 53 | } 54 | -------------------------------------------------------------------------------- /types/bool.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | 7 | "github.com/redneckbeard/thanos/bst" 8 | ) 9 | 10 | type Bool struct { 11 | *proto 12 | } 13 | 14 | var BoolType = Bool{newProto("Boolean", "Object", ClassRegistry)} 15 | 16 | var BoolClass = NewClass("Boolean", "Object", BoolType, ClassRegistry) 17 | 18 | func (t Bool) String() string { return "BoolType" } 19 | func (t Bool) GoType() string { return "bool" } 20 | func (t Bool) IsComposite() bool { return false } 21 | 22 | func (t Bool) MethodReturnType(m string, b Type, args []Type) (Type, error) { 23 | return t.proto.MustResolve(m, false).ReturnType(t, b, args) 24 | } 25 | 26 | func (t Bool) BlockArgTypes(m string, args []Type) []Type { 27 | return t.proto.MustResolve(m, false).blockArgs(t, args) 28 | } 29 | 30 | func (t Bool) TransformAST(m string, rcvr ast.Expr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 31 | return t.proto.MustResolve(m, false).TransformAST(TypeExpr{t, rcvr}, args, blk, it) 32 | } 33 | 34 | func (t Bool) Resolve(m string) (MethodSpec, bool) { 35 | return t.proto.Resolve(m, false) 36 | } 37 | 38 | func (t Bool) MustResolve(m string) MethodSpec { 39 | return t.proto.MustResolve(m, false) 40 | } 41 | 42 | func (t Bool) HasMethod(m string) bool { 43 | return t.proto.HasMethod(m, false) 44 | } 45 | 46 | func (t Bool) Alias(existingMethod, newMethod string) { 47 | t.proto.MakeAlias(existingMethod, newMethod, false) 48 | } 49 | 50 | func (t Bool) Equals(t2 Type) bool { return t == t2 } 51 | 52 | func init() { 53 | BoolType.Def("==", simpleComparisonOperatorSpec(token.EQL)) 54 | BoolType.Def("!=", simpleComparisonOperatorSpec(token.NEQ)) 55 | } 56 | -------------------------------------------------------------------------------- /compiler/methods.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "go/ast" 5 | 6 | "github.com/redneckbeard/thanos/parser" 7 | "github.com/redneckbeard/thanos/types" 8 | ) 9 | 10 | func (g *GoProgram) TransformMethodCall(c *parser.MethodCall) types.Transform { 11 | var blk *types.Block 12 | if c.Block != nil { 13 | blk = g.BuildBlock(c.Block) 14 | } 15 | return g.getTransform(c, g.CompileExpr(c.Receiver), c.Receiver.Type(), c.MethodName, c.Args, blk) 16 | } 17 | 18 | func (g *GoProgram) getTransform(call *parser.MethodCall, rcvr ast.Expr, rcvrType types.Type, methodName string, args parser.ArgsNode, blk *types.Block) types.Transform { 19 | var argExprs []types.TypeExpr 20 | if call != nil && call.Method != nil { 21 | argExprs = g.CompileArgs(call, args) 22 | } else { 23 | for _, a := range args { 24 | argExprs = append(argExprs, types.TypeExpr{Expr: g.CompileExpr(a), Type: a.Type()}) 25 | } 26 | } 27 | transform := rcvrType.TransformAST( 28 | methodName, 29 | rcvr, 30 | argExprs, 31 | blk, 32 | g.it, 33 | ) 34 | g.AddImports(transform.Imports...) 35 | return transform 36 | } 37 | 38 | func (g *GoProgram) BuildBlock(blk *parser.Block) *types.Block { 39 | g.pushTracker() 40 | args := []ast.Expr{} 41 | for _, p := range blk.Params { 42 | args = append(args, g.it.Get(p.Name)) 43 | } 44 | g.newBlockStmt() 45 | g.State.Push(InBlockBody) 46 | defer func() { 47 | g.BlockStack.Pop() 48 | g.State.Pop() 49 | }() 50 | for _, s := range blk.Body.Statements { 51 | g.CompileStmt(s) 52 | } 53 | g.popTracker() 54 | return &types.Block{ 55 | ReturnType: blk.Body.ReturnType, 56 | Args: args, 57 | Statements: g.BlockStack.Peek().List, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /stdlib/matchdata.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | type MatchData struct { 10 | matches []string 11 | matchesByName map[string]string 12 | } 13 | 14 | func NewMatchData(patt *regexp.Regexp, search string) *MatchData { 15 | data := &MatchData{matchesByName: make(map[string]string)} 16 | matches := patt.FindStringSubmatch(search) 17 | if matches == nil { 18 | return nil 19 | } 20 | data.matches = matches 21 | for i, k := range patt.SubexpNames() { 22 | if k != "" { 23 | data.matchesByName[k] = matches[i] 24 | } 25 | } 26 | return data 27 | } 28 | 29 | func (md *MatchData) Get(i int) string { 30 | if i >= len(md.matches) { 31 | return "" 32 | } 33 | return md.matches[i] 34 | } 35 | 36 | func (md *MatchData) GetByName(k string) string { 37 | return md.matchesByName[k] 38 | } 39 | 40 | func (md *MatchData) Captures() []string { 41 | return md.matches[1:] 42 | } 43 | 44 | func (md *MatchData) Length() int { 45 | return len(md.matches) 46 | } 47 | 48 | func (md *MatchData) NamedCaptures() map[string]string { 49 | return md.matchesByName 50 | } 51 | 52 | func (md *MatchData) Names() []string { 53 | names := []string{} 54 | for k := range md.matchesByName { 55 | names = append(names, k) 56 | } 57 | return names 58 | } 59 | 60 | func ConvertFromGsub(patt *regexp.Regexp, sub string) string { 61 | for i, name := range patt.SubexpNames() { 62 | namedSub := fmt.Sprintf(`\k<%s>`, name) 63 | sub = strings.ReplaceAll(sub, namedSub, fmt.Sprintf("${%d}", i)) 64 | } 65 | for i := patt.NumSubexp(); i > 0; i-- { 66 | sub = strings.ReplaceAll(sub, fmt.Sprintf(`\%d`, i), fmt.Sprintf("${%d}", i)) 67 | } 68 | return sub 69 | } 70 | -------------------------------------------------------------------------------- /parser/string_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "testing" 4 | 5 | func TestEscapeTranslation(t *testing.T) { 6 | tests := []struct { 7 | in, out string 8 | }{ 9 | {`"\a"`, `"\a"`}, 10 | {`"\b"`, `"\b"`}, 11 | {`"\f"`, `"\f"`}, 12 | {`"\n"`, `"\n"`}, 13 | {`"\r"`, `"\r"`}, 14 | {`"\t"`, `"\t"`}, 15 | {`"\v"`, `"\v"`}, 16 | {`"\d\g\h"`, `"dgh"`}, 17 | {`'\d\g\h'`, "`\\d\\g\\h`"}, 18 | {`"\""`, `"\""`}, 19 | {`"\'"`, `"'"`}, 20 | {`'\\'`, "`\\`"}, 21 | {`'\''`, "`'`"}, 22 | {`"\\\""`, `"\\\""`}, 23 | {`%w|x\||`, "`x|`"}, 24 | } 25 | for i, tt := range tests { 26 | if caseNum == 0 || caseNum == i+1 { 27 | p, err := ParseString(tt.in) 28 | node := p.Statements[0].(*StringNode) 29 | if tt.out != node.GoString() { 30 | t.Errorf("[%d] Expected %s but got %s", i+1, tt.out, node.GoString()) 31 | if err != nil { 32 | t.Errorf("[%d] Parse errors: %s", i+1, err) 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | func TestInvalidEscapes(t *testing.T) { 40 | tests := []struct { 41 | in, msg string 42 | }{ 43 | {`"\e"`, `line 1: \e is not a valid escape sequence in Go strings`}, 44 | {`"\s"`, `line 1: \s is not a valid escape sequence in Go strings`}, 45 | {`"\M"`, `line 1: \M-x, \M-\C-x, and \M-\cx are not valid escape sequences in Go strings`}, 46 | {`"\c"`, `line 1: \c\M-x, \c?, and \C? are not valid escape sequences in Go strings`}, 47 | } 48 | for i, tt := range tests { 49 | if caseNum == 0 || caseNum == i+1 { 50 | _, err := ParseString(tt.in) 51 | if err == nil { 52 | t.Errorf("[%d] Expected error '%s' but got none", i+1, tt.msg) 53 | } else if tt.msg != err.Error() { 54 | t.Errorf("[%d] Expected error '%s' but got '%s'", i+1, tt.msg, err.Error()) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Clear BSD License 2 | 3 | Copyright (c) 2021 Jonathan Paul Lukens 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted (subject to the limitations in the disclaimer 8 | below) provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, 11 | this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from this 19 | software without specific prior written permission. 20 | 21 | NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY 22 | THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 23 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 25 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 26 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 27 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 28 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 29 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 30 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 31 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /compiler/util.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | 10 | "github.com/redneckbeard/thanos/parser" 11 | ) 12 | 13 | func CompareThanosToMRI(program, label string) (string, string, error) { 14 | cmd := exec.Command("ruby") 15 | cmd.Stdin = strings.NewReader(program) 16 | var out, rubyErr bytes.Buffer 17 | cmd.Stdout = &out 18 | cmd.Stderr = &rubyErr 19 | err := cmd.Run() 20 | if err != nil { 21 | return "", "", fmt.Errorf(`Failed to execute Ruby script: 22 | %s 23 | 24 | Error: %s`, program, rubyErr.String()) 25 | } 26 | rubyTmp, _ := os.CreateTemp("", "ruby.results") 27 | defer os.Remove(rubyTmp.Name()) 28 | rubyTmp.Write(out.Bytes()) 29 | 30 | prog, err := parser.ParseString(program) 31 | if err != nil { 32 | return "", "", fmt.Errorf("Error parsing '"+program+"': ", err) 33 | } 34 | translated, err := Compile(prog) 35 | goTmp, _ := os.Create("tmp.go") 36 | defer os.Remove(goTmp.Name()) 37 | goTmp.WriteString(translated) 38 | 39 | cmd = exec.Command("go", "run", goTmp.Name()) 40 | out.Reset() 41 | cmd.Stdout = &out 42 | var errBuf bytes.Buffer 43 | cmd.Stderr = &errBuf 44 | err = cmd.Run() 45 | if err != nil { 46 | return "", translated, fmt.Errorf(`%s 47 | Go compilation of translated source failed for '%s'. Translation: 48 | ------ 49 | %s 50 | ------`, errBuf.String(), label, translated) 51 | } 52 | goOutTmp, _ := os.CreateTemp("", "go.results") 53 | defer os.Remove(goOutTmp.Name()) 54 | goOutTmp.Write(out.Bytes()) 55 | 56 | comm := exec.Command("comm", "-23", rubyTmp.Name(), goOutTmp.Name()) 57 | out.Reset() 58 | errBuf.Reset() 59 | comm.Stdout = &out 60 | comm.Stderr = &errBuf 61 | err = comm.Run() 62 | if err != nil { 63 | return "", translated, fmt.Errorf("%s: %s", err, errBuf.String()) 64 | } 65 | return strings.TrimSpace(out.String()), translated, nil 66 | } 67 | -------------------------------------------------------------------------------- /stdlib/matchdata_test.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | ) 7 | 8 | func TestMatchData(t *testing.T) { 9 | patt1 := regexp.MustCompile("foo") 10 | patt2 := regexp.MustCompile("f(oo)") 11 | patt3 := regexp.MustCompile("f(oo)(?Pbar)?") 12 | md := NewMatchData(patt1, "bar") 13 | if md != nil { 14 | t.Errorf(`/foo/ should not have matched "bar", should have returned nil`) 15 | } 16 | md = NewMatchData(patt1, "foo") 17 | if md.Get(0) != "foo" { 18 | t.Errorf(`expected /foo/ to match "foo", didn't have complete match present`) 19 | } 20 | if md.Get(1) != "" { 21 | t.Errorf(`expected first submatch to be zero value`) 22 | } 23 | md = NewMatchData(patt2, "foo") 24 | if md.Get(1) != "oo" { 25 | t.Errorf(`expected first submatch to be "oo"`) 26 | } 27 | if md.GetByName("key") != "" { 28 | t.Errorf(`expected submatch retrieval by name to fail when capture is unnamed`) 29 | } 30 | md = NewMatchData(patt3, "foobar") 31 | if md.Get(1) != "oo" { 32 | t.Errorf(`expected first submatch to be "oo"`) 33 | } 34 | if md.Get(2) != "bar" { 35 | t.Errorf(`expected second submatch to be "bar"`) 36 | } 37 | if md.GetByName("bar") != "bar" { 38 | t.Errorf(`expected submatch retrieval by name to work`) 39 | } 40 | } 41 | 42 | func TestConvertFromGsub(t *testing.T) { 43 | tests := []struct{ regex, ruby, expected string }{ 44 | { 45 | `[aeiou]`, 46 | "*", 47 | "*", 48 | }, 49 | { 50 | `([aeiou])`, 51 | `<\1>`, 52 | "<${1}>", 53 | }, 54 | { 55 | `(?P[aeiou])`, 56 | `{\k}`, 57 | "{${1}}", 58 | }, 59 | } 60 | 61 | for _, tt := range tests { 62 | patt := regexp.MustCompile(tt.regex) 63 | converted := ConvertFromGsub(patt, tt.ruby) 64 | if converted != tt.expected { 65 | t.Fatalf("Expected '%s' to convert to '%s' but got '%s'", tt.ruby, tt.expected, converted) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /types/proc.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | 7 | "github.com/redneckbeard/thanos/bst" 8 | ) 9 | 10 | type Proc struct { 11 | Args []Type 12 | ReturnType Type 13 | Instance instance 14 | } 15 | 16 | var ProcClass = NewClass("Proc", "Object", nil, ClassRegistry) 17 | 18 | func NewProc() *Proc { 19 | return &Proc{Instance: ProcClass.Instance} 20 | } 21 | 22 | func (t *Proc) Equals(t2 Type) bool { return t == t2 } 23 | func (t *Proc) String() string { return fmt.Sprintf("Proc(%v) -> %s", t.Args, t.ReturnType) } 24 | func (t *Proc) GoType() string { return "func" } 25 | func (t *Proc) IsComposite() bool { return false } 26 | func (t *Proc) ClassName() string { return "Proc" } 27 | func (t *Proc) IsMultiple() bool { return false } 28 | 29 | func (t *Proc) HasMethod(method string) bool { 30 | return t.Instance.HasMethod(method) 31 | } 32 | 33 | func (t *Proc) MethodReturnType(m string, b Type, args []Type) (Type, error) { 34 | return t.Instance.MustResolve(m).ReturnType(t, b, args) 35 | } 36 | 37 | func (t *Proc) BlockArgTypes(m string, args []Type) []Type { 38 | return t.Instance.MustResolve(m).blockArgs(t, args) 39 | } 40 | 41 | func (t *Proc) TransformAST(m string, rcvr ast.Expr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 42 | return t.Instance.MustResolve(m).TransformAST(TypeExpr{Expr: rcvr, Type: t}, args, blk, it) 43 | } 44 | 45 | func init() { 46 | ProcClass.Instance.Def("call", MethodSpec{ 47 | blockArgs: func(r Type, args []Type) []Type { 48 | return []Type{} 49 | }, 50 | ReturnType: func(r Type, b Type, args []Type) (Type, error) { 51 | return r.(*Proc).ReturnType, nil 52 | }, 53 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 54 | return Transform{ 55 | Expr: bst.Call(nil, rcvr.Expr, UnwrapTypeExprs(args)...), 56 | } 57 | }, 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /compiler/compiler_test.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "runtime/debug" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/redneckbeard/thanos/parser" 16 | ) 17 | 18 | var filename, label string 19 | 20 | func init() { 21 | flag.StringVar(&filename, "filename", "", "name of the file to test compilation for") 22 | flag.StringVar(&label, "label", "", "label for the compilation test") 23 | } 24 | 25 | func TestCompile(t *testing.T) { 26 | rubyFiles, _ := filepath.Glob("testdata/ruby/*.rb") 27 | for _, ruby := range rubyFiles { 28 | base := filepath.Base(ruby) 29 | noExt := strings.TrimSuffix(base, filepath.Ext(base)) 30 | if filename == "" || filename == noExt { 31 | goTgt := fmt.Sprintf("testdata/go/%s.go", noExt) 32 | program, err := parser.ParseFile(ruby) 33 | if err != nil { 34 | t.Error("Error parsing "+ruby+": ", err) 35 | continue 36 | } 37 | defer func() { 38 | if r := recover(); r != nil { 39 | t.Errorf("Recovered test failure in %s: %s\n\n%s", ruby, r, string(debug.Stack())) 40 | } 41 | }() 42 | translated, err := Compile(program) 43 | if err != nil { 44 | t.Error(err) 45 | continue 46 | } 47 | if f, err := os.Open(goTgt); err != nil { 48 | t.Fatal(err) 49 | } else { 50 | if b, err := ioutil.ReadAll(f); err != nil { 51 | t.Fatal(err) 52 | } else { 53 | if translated != string(b) { 54 | cmd := exec.Command("diff", "--color=always", "-b", "-c", goTgt, "-") 55 | cmd.Stdin = strings.NewReader(translated) 56 | var out bytes.Buffer 57 | cmd.Stdout = &out 58 | cmd.Run() 59 | if len(strings.TrimSpace(out.String())) > 0 { 60 | t.Errorf("Got unexpected result translating %s:\n\n%s\n", ruby, out.String()) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /compiler/testdata/go/test_super.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | type Foo struct { 9 | baz float64 10 | } 11 | 12 | func NewFoo() *Foo { 13 | newInstance := &Foo{} 14 | return newInstance 15 | } 16 | func (f *Foo) A() bool { 17 | return true 18 | } 19 | func (f *Foo) B(x float64, y int) float64 { 20 | f.baz = x + float64(y) 21 | return f.baz 22 | } 23 | 24 | type Bar struct { 25 | baz float64 26 | } 27 | 28 | func NewBar() *Bar { 29 | newInstance := &Bar{} 30 | return newInstance 31 | } 32 | func (b *Bar) A() bool { 33 | super := func(b *Bar) bool { 34 | return true 35 | } 36 | return super(b) 37 | } 38 | func (b *Bar) B(x float64, y int) float64 { 39 | super := func(b *Bar, x float64, y int) float64 { 40 | b.baz = x + float64(y) 41 | return b.baz 42 | } 43 | return super(b, x, y) 44 | } 45 | 46 | type Baz struct { 47 | baz float64 48 | } 49 | 50 | func NewBaz() *Baz { 51 | newInstance := &Baz{} 52 | return newInstance 53 | } 54 | func (b *Baz) B(x float64, y int) float64 { 55 | super := func(b *Baz, x float64, y int) float64 { 56 | b.baz = x + float64(y) 57 | return b.baz 58 | } 59 | return super(b, 1.4, 2) + 1.0 60 | } 61 | func (b *Baz) A() bool { 62 | return true 63 | } 64 | 65 | type Quux struct { 66 | baz float64 67 | } 68 | 69 | func NewQuux() *Quux { 70 | newInstance := &Quux{} 71 | return newInstance 72 | } 73 | func (q *Quux) A() bool { 74 | super := func(q *Quux) bool { 75 | return true 76 | } 77 | anc := super(q) 78 | return !anc 79 | } 80 | func (q *Quux) B(x float64, y int) float64 { 81 | super := func(q *Quux, x float64, y int) float64 { 82 | q.baz = x + float64(y) 83 | return q.baz 84 | } 85 | anc := super(q, math.Pow(x, 2), int(math.Pow(float64(y), 2))) 86 | return anc + 1 87 | } 88 | func main() { 89 | NewBar().B(1.5, 2) 90 | NewBaz().B(1.5, 2) 91 | quux := NewQuux() 92 | if quux.A() { 93 | fmt.Println(quux.B(2.0, 4)) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /stdlib/file_test.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestMakeSplitFunc(t *testing.T) { 10 | linesFile, _ := os.CreateTemp("", "lines.test") 11 | defer os.Remove(linesFile.Name()) 12 | linesFile.WriteString(`here are several 13 | lines that end with 14 | newlines but that all 15 | seem to have "that" in them`) 16 | 17 | tests := []struct { 18 | separator string 19 | chomp bool 20 | lines []string 21 | }{ 22 | { 23 | separator: "\n", 24 | chomp: false, 25 | lines: []string{ 26 | "here are several\n", 27 | "lines that end with\n", 28 | "newlines but that all\n", 29 | "seem to have \"that\" in them", 30 | }, 31 | }, 32 | { 33 | separator: "\n", 34 | chomp: true, 35 | lines: []string{ 36 | "here are several", 37 | "lines that end with", 38 | "newlines but that all", 39 | "seem to have \"that\" in them", 40 | }, 41 | }, 42 | { 43 | separator: "that", 44 | chomp: false, 45 | lines: []string{ 46 | "here are several\nlines that", 47 | " end with\nnewlines but that", 48 | " all\nseem to have \"that", 49 | "\" in them", 50 | }, 51 | }, 52 | { 53 | separator: "that", 54 | chomp: true, 55 | lines: []string{ 56 | "here are several\nlines ", 57 | " end with\nnewlines but ", 58 | " all\nseem to have \"", 59 | "\" in them", 60 | }, 61 | }, 62 | } 63 | for _, tt := range tests { 64 | newlineSplitFile, _ := os.Open(linesFile.Name()) 65 | scanner := bufio.NewScanner(newlineSplitFile) 66 | scanner.Split(MakeSplitFunc(tt.separator, tt.chomp)) 67 | lineNo := 0 68 | for scanner.Scan() { 69 | line := scanner.Text() 70 | if tt.lines[lineNo] != line { 71 | t.Fatalf(`Line No. %d did not match for sep "%s" and chomp: %t -- "%s" != "%s"`, lineNo, tt.separator, tt.chomp, line, tt.lines[lineNo]) 72 | } 73 | lineNo++ 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /compiler/testdata/go/test_arguments.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func Pos_and_kw(foo string, bar bool) { 6 | if bar { 7 | fmt.Println(foo) 8 | } 9 | } 10 | func All_kw(foo string, bar bool) { 11 | if bar { 12 | fmt.Println(foo) 13 | } 14 | } 15 | func Defaults(foo, bar string) string { 16 | return fmt.Sprintf("foo: %s, bar: %s", foo, bar) 17 | } 18 | func Splat(a int, c bool, b ...int) int { 19 | if c { 20 | return b[0] 21 | } else { 22 | return a 23 | } 24 | } 25 | 26 | func Double_splat(foo int, bar map[string]int) int { 27 | return foo + bar["baz"] 28 | } 29 | 30 | type Foo struct { 31 | foo int 32 | } 33 | 34 | func NewFoo(foo int) *Foo { 35 | newInstance := &Foo{} 36 | newInstance.Initialize(foo) 37 | return newInstance 38 | } 39 | func (f *Foo) Initialize(foo int) int { 40 | f.foo = foo 41 | return f.foo 42 | } 43 | func main() { 44 | Pos_and_kw("x", true) 45 | Pos_and_kw("x", false) 46 | All_kw("y", false) 47 | All_kw("z", false) 48 | Defaults("x", "y") 49 | Defaults("z", "y") 50 | Defaults("z", "a") 51 | NewFoo(10) 52 | Splat(9, false, 2, 3) 53 | Splat(9, true, 2) 54 | Splat(9, false) 55 | Splat(9, false, []int{1, 2}...) 56 | Splat(9, false, append([]int{5}, []int{1, 2}...)...) 57 | Double_splat(1, map[string]int{"bar": 2, "baz": 3}) 58 | Double_splat(1, map[string]int{"baz": 3}) 59 | Double_splat(1, map[string]int{"baz": 4}) 60 | hash_from_elsewhere := map[string]int{"foo": 1, "baz": 4} 61 | hash_from_elsewhere_kwargs := map[string]int{} 62 | for k, v := range hash_from_elsewhere { 63 | switch k { 64 | case "foo": 65 | default: 66 | hash_from_elsewhere_kwargs[k] = v 67 | } 68 | } 69 | Double_splat(hash_from_elsewhere["foo"], hash_from_elsewhere_kwargs) 70 | foo := []int{1, 2, 3} 71 | a, b := foo[0], foo[1:len(foo)] 72 | c, d, e := foo[0], foo[1], foo[2:len(foo)] 73 | syms := []string{"foo", "bar", "baz"} 74 | f := append([]string{"quux"}, syms...) 75 | g, h, i := "quux", syms[0], syms[1] 76 | x, y, z := "quux", syms[0], syms[1:len(syms)] 77 | } 78 | -------------------------------------------------------------------------------- /types/class_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestClassRegistry(t *testing.T) { 8 | t.Skip() 9 | registry := &classRegistry{registry: make(map[string]*Class)} 10 | 11 | newProto("Foo", "Bar", registry) 12 | 13 | class, err := registry.Get("Foo") 14 | 15 | if err == nil || class != nil { 16 | t.Fatal("Expected ClassRegistry.Get to error without initializing first") 17 | } 18 | err = registry.Initialize() 19 | 20 | expected := "Class 'Foo' described as having parent 'Bar' but no class 'Bar' was ever registered" 21 | 22 | if err == nil || err.Error() != expected { 23 | t.Fatalf(`Expected error "%s" but got "%s"`, expected, err) 24 | } 25 | 26 | newProto("Bar", "Baz", registry) 27 | newProto("Baz", "", registry) 28 | 29 | err = registry.Initialize() 30 | 31 | if err != nil { 32 | t.Fatalf(`Encountered unexpected error: "%s"`, err) 33 | } 34 | } 35 | 36 | func TestMethodResolution(t *testing.T) { 37 | t.Skip() 38 | registry := &classRegistry{registry: make(map[string]*Class)} 39 | 40 | foo := newProto("Foo", "Bar", registry) 41 | bar := newProto("Bar", "", registry) 42 | bar.Def("method", MethodSpec{}) 43 | registry.Initialize() 44 | if !foo.HasMethod("method", false) { 45 | t.Fatal("'Foo' failed to inherit method 'method'") 46 | } 47 | } 48 | 49 | func TestClassNewDelegatesToInstanceInitialize(t *testing.T) { 50 | t.Skip() 51 | registry := &classRegistry{registry: make(map[string]*Class)} 52 | foo := newProto("Foo", "", registry) 53 | registry.Initialize() 54 | fakeType := Simple(100) 55 | foo.Def("initialize", MethodSpec{ 56 | ReturnType: func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 57 | return fakeType, nil 58 | }, 59 | }) 60 | class, _ := registry.Get("Foo") 61 | if !class.HasMethod("new") { 62 | t.Fatal("`Foo#new` not handled") 63 | } 64 | spec := class.MustResolve("new") 65 | if typ, _ := spec.ReturnType(nil, nil, []Type{}); typ != fakeType { 66 | t.Fatal("Not mapping #new to instance #initialize") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /compiler/testdata/ruby/test_cond.rb: -------------------------------------------------------------------------------- 1 | def cond_return(a, b) 2 | return a * b if a == 47 3 | if a < 0 && b < 0 4 | 0 5 | elsif a >= b 6 | a 7 | else 8 | b 9 | end 10 | end 11 | 12 | def cond_assignment(a, b, c) 13 | foo = if a == b 14 | true 15 | else 16 | false 17 | end 18 | foo || c 19 | end 20 | 21 | def cond_invoke 22 | if true 23 | puts "it's true" 24 | else 25 | puts "it's false" 26 | end 27 | 10 28 | end 29 | 30 | def tern(x, y, z) 31 | return 99 unless z < 50 32 | x == 10 ? y : z 33 | end 34 | 35 | def length_if_array(arr) 36 | # since we infer the type signature of the method at compile time, the following condition becomes essentially tautological and can just be compiled away entirely 37 | if arr.is_a?(Object) 38 | arr.size 39 | else 40 | 0 41 | end 42 | end 43 | 44 | def puts_if_not_symbol 45 | if "foo".is_a?(Symbol) 46 | puts "is a symbol" 47 | else 48 | puts "isn't a symbol" 49 | end 50 | end 51 | 52 | def switch_on_int_val(x) 53 | case x 54 | when 0 55 | "none" 56 | when 1 57 | "one" 58 | when 2, 3, 4, 5 59 | "a few" 60 | else 61 | "many" 62 | end 63 | end 64 | 65 | def switch_on_int_with_range(x) 66 | loc = 9..12 67 | case x 68 | when 0 69 | "none" 70 | when 1 71 | "one" 72 | when 2..5 73 | "a few" 74 | when 6, 7, 8 # In Go, this now has to get expanded to several expressions with || 75 | "several" 76 | when loc 77 | "a lot" 78 | else 79 | "many" 80 | end 81 | end 82 | 83 | def switch_on_regexps(x) 84 | case x 85 | when /foo/ 86 | 1 87 | when /bar/ 88 | 2 89 | when /baz/ 90 | 3 91 | end 92 | end 93 | 94 | baz = cond_return(2, 4) 95 | quux = cond_assignment(1, 3, false) 96 | zoo = cond_invoke 97 | last = tern(10, 20, 30) 98 | length_if_array(["foo", "bar", "baz"]) 99 | switch_on_int_val(5) 100 | switch_on_int_with_range(5) 101 | switch_on_regexps("foo") 102 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | //go:generate ./gen_parser.sh 2 | 3 | // package parser contains the requisite components for generating 4 | // type-annotated Ruby ASTs in Go. At a high level, there are three such 5 | // components: a lexer, implemented by hand; a parser, generated with goyacc; 6 | // and a Node interface that, when implemented according to a specific 7 | // convention, allows for some basic static analysis to tag AST nodes with a 8 | // Type provided by the types package in this repository. It also provides 9 | // utility functions for interacting with this chain of components from outside 10 | // the package. 11 | 12 | package parser 13 | 14 | import ( 15 | "io" 16 | "log" 17 | "os" 18 | "path/filepath" 19 | "strconv" 20 | 21 | "github.com/redneckbeard/thanos/types" 22 | ) 23 | 24 | func ParseFile(filename string) (*Root, error) { 25 | var f *os.File 26 | if filename == "" { 27 | f = os.Stdin 28 | } else { 29 | dir, err := os.Getwd() 30 | if err != nil { 31 | panic(err) 32 | } 33 | path := filepath.Join(dir, filename) 34 | f, err = os.Open(path) 35 | if err != nil { 36 | panic(err) 37 | } 38 | } 39 | if b, err := io.ReadAll(f); err != nil { 40 | return nil, err 41 | } else { 42 | return ParseBytes(b) 43 | } 44 | } 45 | 46 | func ParseString(s string) (*Root, error) { 47 | return ParseBytes([]byte(s)) 48 | } 49 | 50 | func ParseBytes(b []byte) (*Root, error) { 51 | types.ClassRegistry.Initialize() 52 | parser := yyNewParser() 53 | l := NewLexer(b) 54 | parser.Parse(l) 55 | if err := l.Root.Analyze(); err != nil { 56 | return l.Root, err 57 | } 58 | if err := l.Root.ParseError(); err != nil { 59 | return l.Root, err 60 | } else { 61 | return l.Root, nil 62 | } 63 | } 64 | 65 | func DebugLevel() int { 66 | if val, found := os.LookupEnv("DEBUG"); found { 67 | if i, err := strconv.Atoi(val); err == nil { 68 | return i 69 | } 70 | } 71 | return 0 72 | } 73 | 74 | func LogDebug(verbosity int, format string, v ...interface{}) { 75 | if DebugLevel() == verbosity { 76 | log.Printf(format, v...) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /types/proto_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "go/ast" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | type Foo struct{} 10 | 11 | func (f *Foo) FuncOne(bar, baz string) int { return 1 } 12 | func (f *Foo) FuncTwo(bar int, baz bool) float64 { return 1 } 13 | 14 | func TestGenerateMethods(t *testing.T) { 15 | registry := &classRegistry{registry: make(map[string]*Class)} 16 | 17 | foo := newProto("Foo", "", registry) 18 | 19 | registry.Initialize() 20 | 21 | foo.GenerateMethods(&Foo{}) 22 | if !foo.HasMethod("func_one", false) { 23 | t.Fatal(`foo proto did not get "func_one" defined on it`) 24 | } 25 | if !foo.HasMethod("func_two", false) { 26 | t.Fatal(`foo proto did not get "func_two" defined on it`) 27 | } 28 | 29 | funcOneSpec := foo.MustResolve("func_one", false) 30 | retType, _ := funcOneSpec.ReturnType(NilType, NilType, []Type{}) 31 | if retType != IntType { 32 | t.Fatal("Expected ReturnType method to return IntType") 33 | } 34 | transform := funcOneSpec.TransformAST(TypeExpr{}, []TypeExpr{}, nil, nil) 35 | selector := transform.Expr.(*ast.CallExpr).Fun.(*ast.Ident).Name 36 | if selector != "FuncOne" { 37 | t.Fatal("Expected CallExpr to have FuncOne as selector") 38 | } 39 | 40 | funcTwoSpec := foo.MustResolve("func_two", false) 41 | retType, _ = funcTwoSpec.ReturnType(NilType, NilType, []Type{}) 42 | if retType != FloatType { 43 | t.Fatal("Expected ReturnType method to return FloatType") 44 | } 45 | transform = funcTwoSpec.TransformAST(TypeExpr{}, []TypeExpr{}, nil, nil) 46 | selector = transform.Expr.(*ast.CallExpr).Fun.(*ast.Ident).Name 47 | if selector != "FuncTwo" { 48 | t.Fatal("Expected CallExpr to have FuncTwo as selector") 49 | } 50 | } 51 | 52 | func TestGenerateMethodsImports(t *testing.T) { 53 | registry := &classRegistry{registry: make(map[string]*Class)} 54 | 55 | file := newProto("File", "", registry) 56 | 57 | registry.Initialize() 58 | 59 | file.GenerateMethods(&os.File{}) 60 | 61 | createSpec := file.MustResolve("name", false) 62 | transform := createSpec.TransformAST(TypeExpr{}, []TypeExpr{}, nil, nil) 63 | 64 | if len(transform.Imports) < 1 || transform.Imports[0] != "os" { 65 | t.Fatal("don't have an import", transform.Imports) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /cmd/exec.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 NAME HERE 3 | 4 | */ 5 | package cmd 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "os" 11 | "os/exec" 12 | "strings" 13 | 14 | "github.com/fatih/color" 15 | "github.com/redneckbeard/thanos/compiler" 16 | "github.com/redneckbeard/thanos/parser" 17 | "github.com/spf13/cobra" 18 | ) 19 | 20 | var File string 21 | 22 | // execCmd represents the exec command 23 | var execCmd = &cobra.Command{ 24 | Use: "exec", 25 | Short: "Compiles and executes the input", 26 | Long: `'thanos exec' compiles the provided Ruby and immediately executes the Go 27 | output. Useful for exploring edge cases that might be missing from the test 28 | suite.`, 29 | Run: func(cmd *cobra.Command, args []string) { 30 | if File == "" { 31 | color.Green("Input your Ruby and execute with Ctrl-D.") 32 | } 33 | program, err := parser.ParseFile(File) 34 | if err != nil { 35 | fmt.Println(err) 36 | return 37 | } 38 | compiled, err := compiler.Compile(program) 39 | if err != nil { 40 | fmt.Println(err) 41 | if compiled != "" { 42 | fmt.Println(compiled) 43 | } 44 | return 45 | } 46 | if strings.Contains(compiled, "github.com/redneckbeard/thanos/stdlib") { 47 | if _, err := os.Open("go.mod"); err != nil { 48 | color.Red("Generated Go source has a dependency on github.com/redneckbeard/thanos/stdlib, but the current directory has no go.mod file. Run `go mod init $mymodule` and `go get github.com/redneckbeard/thanos/stdlib@latest` and try again.") 49 | return 50 | } 51 | } 52 | goTmp, _ := os.Create("tmp.go") 53 | defer os.Remove(goTmp.Name()) 54 | goTmp.WriteString(compiled) 55 | 56 | run := exec.Command("go", "run", goTmp.Name()) 57 | var stderr bytes.Buffer 58 | run.Stderr = &stderr 59 | stdout, err := run.Output() 60 | if err != nil { 61 | color.Red(`Execution failed for compiled Go: 62 | ------ 63 | %s 64 | ------ 65 | Error: %s`, compiled, stderr.String()) 66 | } else { 67 | color.Green(strings.Repeat("-", 20)) 68 | fmt.Print(string(stdout)) 69 | } 70 | }, 71 | } 72 | 73 | func init() { 74 | rootCmd.AddCommand(execCmd) 75 | execCmd.Flags().StringVarP(&File, "file", "f", "", "Ruby file to execute (defaults to stdin)") 76 | } 77 | -------------------------------------------------------------------------------- /compiler/testdata/go/test_cond.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "github.com/redneckbeard/thanos/stdlib" 8 | ) 9 | 10 | var patt = regexp.MustCompile(`foo`) 11 | var patt1 = regexp.MustCompile(`bar`) 12 | var patt2 = regexp.MustCompile(`baz`) 13 | 14 | func Cond_return(a, b int) int { 15 | if a == 47 { 16 | return a * b 17 | } 18 | if a < 0 && b < 0 { 19 | return 0 20 | } else { 21 | if a >= b { 22 | return a 23 | } else { 24 | return b 25 | } 26 | } 27 | } 28 | func Cond_assignment(a, b int, c bool) bool { 29 | var foo bool 30 | if a == b { 31 | foo = true 32 | } else { 33 | foo = false 34 | } 35 | return foo || c 36 | } 37 | func Cond_invoke() int { 38 | fmt.Println("it's true") 39 | return 10 40 | } 41 | func Tern(x, y, z int) int { 42 | if !(z < 50) { 43 | return 99 44 | } 45 | if x == 10 { 46 | return y 47 | } else { 48 | return z 49 | } 50 | } 51 | func Length_if_array(arr []string) int { 52 | return len(arr) 53 | } 54 | func Puts_if_not_symbol() { 55 | fmt.Println("isn't a symbol") 56 | } 57 | func Switch_on_int_val(x int) string { 58 | switch x { 59 | case 0: 60 | return "none" 61 | case 1: 62 | return "one" 63 | case 2, 3, 4, 5: 64 | return "a few" 65 | default: 66 | return "many" 67 | } 68 | } 69 | func Switch_on_int_with_range(x int) string { 70 | loc := &stdlib.Range[int]{9, 12, true} 71 | switch { 72 | case x == 0: 73 | return "none" 74 | case x == 1: 75 | return "one" 76 | case x >= 2 && x <= 5: 77 | return "a few" 78 | case x == 6 || x == 7 || x == 8: 79 | return "several" 80 | case loc.Covers(x): 81 | return "a lot" 82 | default: 83 | return "many" 84 | } 85 | } 86 | func Switch_on_regexps(x string) int { 87 | switch { 88 | case patt.MatchString(x): 89 | return 1 90 | case patt1.MatchString(x): 91 | return 2 92 | case patt2.MatchString(x): 93 | return 3 94 | } 95 | } 96 | func main() { 97 | baz := Cond_return(2, 4) 98 | quux := Cond_assignment(1, 3, false) 99 | zoo := Cond_invoke() 100 | last := Tern(10, 20, 30) 101 | Length_if_array([]string{"foo", "bar", "baz"}) 102 | Switch_on_int_val(5) 103 | Switch_on_int_with_range(5) 104 | Switch_on_regexps("foo") 105 | } 106 | -------------------------------------------------------------------------------- /tests/array.rb: -------------------------------------------------------------------------------- 1 | gauntlet("join") do 2 | puts [1, 2, 3].join(" + ") 3 | puts ["foo", "bar", "baz"].join(" and ") 4 | end 5 | 6 | gauntlet("take") do 7 | [1,2,3,4,5].take(3).each do |x| 8 | puts x 9 | end 10 | end 11 | 12 | gauntlet("drop") do 13 | [1,2,3,4,5].drop(3).each do |x| 14 | puts x 15 | end 16 | end 17 | 18 | gauntlet("values_at") do 19 | [1,2,3,4,5].values_at(2, 4).each do |x| 20 | puts x 21 | end 22 | end 23 | 24 | gauntlet("unshift") do 25 | orig = [1,2,3,4,5] 26 | orig.unshift(2, 4).each { |x| puts x } 27 | end 28 | 29 | gauntlet("arr[x..]") do 30 | orig = [1,2,3,4,5] 31 | orig[3..].each { |x| puts x } 32 | end 33 | 34 | gauntlet("arr[x...]") do 35 | orig = [1,2,3,4,5] 36 | orig[3...].each { |x| puts x } 37 | end 38 | 39 | gauntlet("arr[x..y]") do 40 | orig = [1,2,3,4,5] 41 | orig[1..4].each { |x| puts x } 42 | end 43 | 44 | gauntlet("arr[x...y]") do 45 | orig = [1,2,3,4,5] 46 | orig[1...4].each { |x| puts x } 47 | end 48 | 49 | gauntlet("arr[x...-y]") do 50 | orig = [1,2,3,4,5] 51 | orig[1...-2].each { |x| puts x } 52 | end 53 | 54 | gauntlet("arr[x..-y]") do 55 | orig = [1,2,3,4,5] 56 | orig[1..-2].each { |x| puts x } 57 | end 58 | 59 | gauntlet("arr[x..y] with x var") do 60 | orig = [1,2,3,4,5] 61 | foo = 1 62 | orig[foo..-2].each { |x| puts x } 63 | end 64 | 65 | gauntlet("arr[x..y] with y var") do 66 | orig = [1,2,3,4,5] 67 | foo = 3 68 | orig[1..foo].each { |x| puts x } 69 | end 70 | 71 | gauntlet("arr[x...y] with y var") do 72 | orig = [1,2,3,4,5] 73 | foo = 3 74 | orig[1...foo].each { |x| puts x } 75 | end 76 | 77 | gauntlet("arr[x..y] with -y var") do 78 | orig = [1,2,3,4,5] 79 | foo = -2 80 | orig[1..foo].each { |x| puts x } 81 | end 82 | 83 | gauntlet("arr[x...y] with -y var") do 84 | orig = [1,2,3,4,5] 85 | foo = -2 86 | orig[1...foo].each { |x| puts x } 87 | end 88 | 89 | gauntlet("include?") do 90 | orig = [1,2,3,4,5] 91 | puts orig.include?(4) 92 | puts orig.include?(7) 93 | end 94 | 95 | gauntlet("Array#-") do 96 | ([1, 2, 3, 4] - [2, 3]).each do |i| 97 | puts i 98 | end 99 | end 100 | 101 | gauntlet("uniq") do 102 | [1, 2, 2, 3, 4, 4, 4].uniq.each do |i| 103 | puts i 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /parser/logic.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/redneckbeard/thanos/types" 7 | ) 8 | 9 | type InfixExpressionNode struct { 10 | Operator string 11 | Left Node 12 | Right Node 13 | lineNo int 14 | _type types.Type 15 | } 16 | 17 | func (n *InfixExpressionNode) String() string { 18 | return fmt.Sprintf("(%s %s %s)", n.Left, n.Operator, n.Right) 19 | } 20 | func (n *InfixExpressionNode) Type() types.Type { return n._type } 21 | func (n *InfixExpressionNode) SetType(t types.Type) { n._type = t } 22 | func (n *InfixExpressionNode) LineNo() int { return n.lineNo } 23 | 24 | func (n *InfixExpressionNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 25 | tl, err := GetType(n.Left, locals, class) 26 | if err != nil { 27 | return nil, err 28 | } 29 | tr, err := GetType(n.Right, locals, class) 30 | if err != nil { 31 | return nil, err 32 | } 33 | if n.HasMethod() { 34 | if t, err := tl.MethodReturnType(n.Operator, nil, []types.Type{tr}); err != nil { 35 | return nil, NewParseError(n, err.Error()) 36 | } else { 37 | return t, nil 38 | } 39 | } 40 | return nil, NewParseError(n, "No method `%s` on type %s", n.Operator, tl) 41 | } 42 | 43 | func (n *InfixExpressionNode) Copy() Node { 44 | return &InfixExpressionNode{n.Operator, n.Left.Copy(), n.Right.Copy(), n.lineNo, n._type} 45 | } 46 | 47 | func (n *InfixExpressionNode) HasMethod() bool { 48 | if n.Left.Type() != nil { 49 | return n.Left.Type().HasMethod(n.Operator) 50 | } 51 | return false 52 | } 53 | 54 | type NotExpressionNode struct { 55 | Arg Node 56 | lineNo int 57 | _type types.Type 58 | } 59 | 60 | func (n *NotExpressionNode) String() string { return fmt.Sprintf("!%s", n.Arg) } 61 | func (n *NotExpressionNode) Type() types.Type { return n._type } 62 | func (n *NotExpressionNode) SetType(t types.Type) { n._type = types.BoolType } 63 | func (n *NotExpressionNode) LineNo() int { return n.lineNo } 64 | 65 | func (n *NotExpressionNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 66 | if _, err := GetType(n.Arg, locals, class); err != nil { 67 | return nil, err 68 | } 69 | return types.BoolType, nil 70 | } 71 | 72 | func (n *NotExpressionNode) Copy() Node { 73 | return &NotExpressionNode{n.Arg.Copy(), n.lineNo, n._type} 74 | } 75 | -------------------------------------------------------------------------------- /types/matchdata.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "go/ast" 5 | 6 | "github.com/redneckbeard/thanos/bst" 7 | "github.com/redneckbeard/thanos/stdlib" 8 | ) 9 | 10 | type matchData struct { 11 | *proto 12 | } 13 | 14 | var MatchDataType = matchData{newProto("MatchData", "Object", ClassRegistry)} 15 | 16 | var MatchDataClass = NewClass("MatchData", "Object", MatchDataType, ClassRegistry) 17 | 18 | func (t matchData) Equals(t2 Type) bool { return t == t2 } 19 | func (t matchData) String() string { return "MatchData" } 20 | func (t matchData) GoType() string { return "*stdlib.matchData" } 21 | func (t matchData) IsComposite() bool { return false } 22 | 23 | func (t matchData) MethodReturnType(m string, b Type, args []Type) (Type, error) { 24 | return t.proto.MustResolve(m, false).ReturnType(t, b, args) 25 | } 26 | 27 | func (t matchData) BlockArgTypes(m string, args []Type) []Type { 28 | return t.proto.MustResolve(m, false).blockArgs(t, args) 29 | } 30 | 31 | func (t matchData) TransformAST(m string, rcvr ast.Expr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 32 | return t.proto.MustResolve(m, false).TransformAST(TypeExpr{Expr: rcvr, Type: t}, args, blk, it) 33 | } 34 | 35 | func (t matchData) Resolve(m string) (MethodSpec, bool) { 36 | return t.proto.Resolve(m, false) 37 | } 38 | 39 | func (t matchData) MustResolve(m string) MethodSpec { 40 | return t.proto.MustResolve(m, false) 41 | } 42 | 43 | func (t matchData) HasMethod(m string) bool { 44 | return t.proto.HasMethod(m, false) 45 | } 46 | 47 | func (t matchData) Alias(existingMethod, newMethod string) { 48 | t.proto.MakeAlias(existingMethod, newMethod, false) 49 | } 50 | 51 | func init() { 52 | MatchDataType.GenerateMethods(&stdlib.MatchData{}, "Get", "GetByName") 53 | MatchDataType.Def("[]", MethodSpec{ 54 | ReturnType: func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 55 | return StringType, nil 56 | }, 57 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 58 | var methodName string 59 | if args[0].Type == IntType { 60 | methodName = "Get" 61 | } else if args[0].Type == StringType { 62 | methodName = "GetByName" 63 | } 64 | return Transform{ 65 | Expr: bst.Call(rcvr.Expr, methodName, args[0].Expr), 66 | } 67 | }, 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /cmd/report.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 NAME HERE 3 | 4 | */ 5 | package cmd 6 | 7 | import ( 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/redneckbeard/thanos/compiler" 12 | "github.com/redneckbeard/thanos/types" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | var className string 17 | 18 | var requiresByClass = map[string]string{ 19 | "Set": "set", 20 | } 21 | 22 | func report(className string) { 23 | var requires string 24 | if lib, ok := requiresByClass[className]; ok { 25 | requires = fmt.Sprintf(`require "%s";`, lib) 26 | } 27 | 28 | script := fmt.Sprintf(`%s methods = %s.instance_methods - Object.instance_methods 29 | methods.sort!.each {|m| puts m}`, requires, className) 30 | if diff, _, err := compiler.CompareThanosToMRI(script, className); err != nil { 31 | panic(err) 32 | } else { 33 | fmt.Printf("# Methods missing on %s\n\n", className) 34 | fmt.Printf("The following instance methods have not yet been implemented on %s. This list does not include methods inherited from `Object` or `Kernel` that are missing from those ancestors.\n\n", className) 35 | for _, method := range strings.Split(diff, "\n") { 36 | fmt.Printf("* `%s#%s`\n", className, method) 37 | } 38 | } 39 | } 40 | 41 | var reportCmd = &cobra.Command{ 42 | Use: "report", 43 | Short: "Generate a report on methods missing from built-in types", 44 | Long: `Generates a report on methods missing from built-in types. This currently deliberately excludes TrueClass and FalseClass because of how the thanos type inference framework handles booleans.`, 45 | Run: func(cmd *cobra.Command, args []string) { 46 | if className != "" { 47 | types.ClassRegistry.Initialize() 48 | if _, err := types.ClassRegistry.Get(className); err != nil { 49 | fmt.Printf("Class '%s' not found in thanos class registry.\n", className) 50 | } else { 51 | report(className) 52 | } 53 | } else { 54 | for i, name := range types.ClassRegistry.Names() { 55 | if name != "Kernel" && name != "Boolean" { 56 | report(name) 57 | if i < len(types.ClassRegistry.Names())-1 { 58 | fmt.Println("") 59 | } 60 | } 61 | } 62 | } 63 | }, 64 | } 65 | 66 | func init() { 67 | rootCmd.AddCommand(reportCmd) 68 | reportCmd.Flags().StringVarP(&className, "class", "c", "", "Ruby class report will be generated for (defaults to all currently implemented core classes") 69 | } 70 | -------------------------------------------------------------------------------- /compiler/testdata/go/test_classes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | type Vehicle struct { 6 | starting_miles int 7 | no_reader string 8 | Vin int 9 | registration string 10 | } 11 | 12 | func NewVehicle(starting_miles int) *Vehicle { 13 | newInstance := &Vehicle{} 14 | newInstance.Initialize(starting_miles) 15 | return newInstance 16 | } 17 | func (v *Vehicle) Initialize(starting_miles int) int { 18 | v.starting_miles = starting_miles 19 | v.no_reader = "unexported" 20 | v.Vin = 100 21 | return v.Vin 22 | } 23 | func (v *Vehicle) Drive(x int) int { 24 | v.starting_miles += x 25 | return v.starting_miles 26 | } 27 | func (v *Vehicle) Mileage() string { 28 | v.log() 29 | return fmt.Sprintf("%d miles", v.starting_miles) 30 | } 31 | func (v *Vehicle) log() { 32 | fmt.Println("log was called") 33 | } 34 | func (v *Vehicle) Starting_miles() int { 35 | return v.starting_miles 36 | } 37 | func (v *Vehicle) SetRegistration(registration string) string { 38 | v.registration = registration 39 | return v.registration 40 | } 41 | 42 | type Car struct { 43 | starting_miles int 44 | no_reader string 45 | Vin int 46 | registration string 47 | } 48 | 49 | func NewCar(starting_miles int) *Car { 50 | newInstance := &Car{} 51 | newInstance.Initialize(starting_miles) 52 | return newInstance 53 | } 54 | func (c *Car) Drive(x int) int { 55 | super := func(c *Car, x int) int { 56 | c.starting_miles += x 57 | return c.starting_miles 58 | } 59 | super(c, x) 60 | c.starting_miles++ 61 | return c.starting_miles 62 | } 63 | func (c *Car) Log() { 64 | fmt.Println("it's a different method!") 65 | super := func(c *Car) { 66 | fmt.Println("log was called") 67 | } 68 | super(c) 69 | } 70 | func (c *Car) Initialize(starting_miles int) int { 71 | c.starting_miles = starting_miles 72 | c.no_reader = "unexported" 73 | c.Vin = 100 74 | return c.Vin 75 | } 76 | func (c *Car) Mileage() string { 77 | c.log() 78 | return fmt.Sprintf("%d miles", c.starting_miles) 79 | } 80 | func (c *Car) Starting_miles() int { 81 | return c.starting_miles 82 | } 83 | func (c *Car) SetRegistration(registration string) string { 84 | c.registration = registration 85 | return c.registration 86 | } 87 | func main() { 88 | mapped := []string{} 89 | for _, car := range []*Car{NewCar(10), NewCar(20), NewCar(30)} { 90 | car.Drive(100) 91 | car.SetRegistration("XXXXXX") 92 | car.Vin++ 93 | mapped = append(mapped, fmt.Sprintf("%s, started at %d", car.Mileage(), car.Starting_miles())) 94 | } 95 | fmt.Println(mapped) 96 | } 97 | -------------------------------------------------------------------------------- /types/helpers.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/token" 7 | 8 | "github.com/redneckbeard/thanos/bst" 9 | ) 10 | 11 | var NoopReturnSelf = MethodSpec{ 12 | ReturnType: func(r Type, b Type, args []Type) (Type, error) { 13 | return r, nil 14 | }, 15 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 16 | return Transform{ 17 | Expr: rcvr.Expr, 18 | } 19 | }, 20 | } 21 | 22 | var AlwaysTrue = MethodSpec{ 23 | ReturnType: func(r Type, b Type, args []Type) (Type, error) { 24 | return BoolType, nil 25 | }, 26 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 27 | return Transform{ 28 | Expr: it.Get("true"), 29 | } 30 | }, 31 | } 32 | 33 | var AlwaysFalse = MethodSpec{ 34 | ReturnType: func(r Type, b Type, args []Type) (Type, error) { 35 | return BoolType, nil 36 | }, 37 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 38 | return Transform{ 39 | Expr: it.Get("false"), 40 | } 41 | }, 42 | } 43 | 44 | func emptySlice(name *ast.Ident, inner string) *ast.AssignStmt { 45 | targetSlice := &ast.CompositeLit{ 46 | Type: &ast.ArrayType{ 47 | Elt: ast.NewIdent(inner), 48 | }, 49 | Elts: []ast.Expr{}, 50 | } 51 | targetSliceVarInit := bst.Define(name, targetSlice) 52 | return targetSliceVarInit 53 | } 54 | 55 | func appendLoop(loopVar, appendTo, rangeOver, appendHead, appendTail ast.Expr) *ast.RangeStmt { 56 | appendStmt := bst.Assign(appendTo, bst.Call(nil, "append", appendHead, appendTail)) 57 | 58 | return &ast.RangeStmt{ 59 | Key: ast.NewIdent("_"), 60 | Value: loopVar, 61 | Tok: token.DEFINE, 62 | X: rangeOver, 63 | Body: &ast.BlockStmt{ 64 | List: []ast.Stmt{appendStmt}, 65 | }, 66 | } 67 | } 68 | 69 | func UnwrapTypeExprs(typeExprs []TypeExpr) []ast.Expr { 70 | exprs := []ast.Expr{} 71 | for _, typeExpr := range typeExprs { 72 | exprs = append(exprs, typeExpr.Expr) 73 | } 74 | return exprs 75 | } 76 | 77 | func simpleComparisonOperatorSpec(tok token.Token) MethodSpec { 78 | return MethodSpec{ 79 | ReturnType: func(r Type, b Type, args []Type) (Type, error) { 80 | if r == args[0] { 81 | return BoolType, nil 82 | } 83 | return nil, fmt.Errorf("Tried to compare disparate types %s and %s", r, args[0]) 84 | }, 85 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 86 | return Transform{ 87 | Expr: bst.Binary(rcvr.Expr, tok, args[0].Expr), 88 | } 89 | }, 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /stdlib/set.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Set[T comparable] map[T]bool 9 | 10 | func NewSet[T comparable](elems []T) Set[T] { 11 | set := make(Set[T]) 12 | for _, elem := range elems { 13 | set[elem] = true 14 | } 15 | return set 16 | } 17 | 18 | func (s Set[T]) String() string { 19 | var elems []string 20 | for elem := range s { 21 | elems = append(elems, fmt.Sprintf("%v", elem)) 22 | } 23 | return fmt.Sprintf("Set{%s}", strings.Join(elems, ", ")) 24 | } 25 | 26 | func (s Set[T]) Union(s2 Set[T]) Set[T] { 27 | set := make(Set[T]) 28 | for k := range s { 29 | set[k] = true 30 | } 31 | for k := range s2 { 32 | set[k] = true 33 | } 34 | return set 35 | } 36 | 37 | func (s Set[T]) Difference(s2 Set[T]) Set[T] { 38 | set := make(Set[T]) 39 | for k := range s { 40 | if _, ok := s2[k]; !ok { 41 | set[k] = true 42 | } 43 | } 44 | return set 45 | } 46 | 47 | func (s Set[T]) Intersection(s2 Set[T]) Set[T] { 48 | set := make(Set[T]) 49 | for k := range s { 50 | if _, ok := s2[k]; ok { 51 | set[k] = true 52 | } 53 | } 54 | return set 55 | } 56 | 57 | func (s Set[T]) Disjoint(s2 Set[T]) Set[T] { 58 | set := make(Set[T]) 59 | for k := range s { 60 | if _, ok := s2[k]; !ok { 61 | set[k] = true 62 | } 63 | } 64 | for k := range s2 { 65 | if _, ok := s[k]; !ok { 66 | set[k] = true 67 | } 68 | } 69 | return set 70 | } 71 | 72 | func (s Set[T]) SupersetQ(s2 Set[T]) bool { 73 | if len(s2) > len(s) { 74 | return false 75 | } 76 | for k := range s2 { 77 | if _, ok := s[k]; !ok { 78 | return false 79 | } 80 | } 81 | return true 82 | } 83 | 84 | func (s Set[T]) ProperSupersetQ(s2 Set[T]) bool { 85 | if len(s2) >= len(s) { 86 | return false 87 | } 88 | for k := range s2 { 89 | if _, ok := s[k]; !ok { 90 | return false 91 | } 92 | } 93 | return true 94 | } 95 | 96 | func (s Set[T]) SubsetQ(s2 Set[T]) bool { 97 | if len(s2) < len(s) { 98 | return false 99 | } 100 | for k := range s { 101 | if _, ok := s2[k]; !ok { 102 | return false 103 | } 104 | } 105 | return true 106 | } 107 | 108 | func (s Set[T]) ProperSubsetQ(s2 Set[T]) bool { 109 | if len(s2) <= len(s) { 110 | return false 111 | } 112 | for k := range s { 113 | if _, ok := s2[k]; !ok { 114 | return false 115 | } 116 | } 117 | return true 118 | } 119 | 120 | func (s Set[T]) ToA() []T { 121 | var arr []T 122 | for k := range s { 123 | arr = append(arr, k) 124 | } 125 | return arr 126 | } 127 | -------------------------------------------------------------------------------- /types/regexp.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "go/ast" 5 | 6 | "github.com/redneckbeard/thanos/bst" 7 | ) 8 | 9 | type Regexp struct { 10 | *proto 11 | } 12 | 13 | var RegexpType = Regexp{newProto("Regexp", "Object", ClassRegistry)} 14 | 15 | var RegexpClass = NewClass("Regexp", "Object", RegexpType, ClassRegistry) 16 | 17 | func (t Regexp) Equals(t2 Type) bool { return t == t2 } 18 | func (t Regexp) String() string { return "RegexpType" } 19 | func (t Regexp) GoType() string { return "*regexp.Regexp" } 20 | func (t Regexp) IsComposite() bool { return false } 21 | 22 | func (t Regexp) MethodReturnType(m string, b Type, args []Type) (Type, error) { 23 | return t.proto.MustResolve(m, false).ReturnType(t, b, args) 24 | } 25 | 26 | func (t Regexp) BlockArgTypes(m string, args []Type) []Type { 27 | return t.proto.MustResolve(m, false).blockArgs(t, args) 28 | } 29 | 30 | func (t Regexp) TransformAST(m string, rcvr ast.Expr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 31 | return t.proto.MustResolve(m, false).TransformAST(TypeExpr{t, rcvr}, args, blk, it) 32 | } 33 | 34 | func (t Regexp) Resolve(m string) (MethodSpec, bool) { 35 | return t.proto.Resolve(m, false) 36 | } 37 | 38 | func (t Regexp) MustResolve(m string) MethodSpec { 39 | return t.proto.MustResolve(m, false) 40 | } 41 | 42 | func (t Regexp) HasMethod(m string) bool { 43 | return t.proto.HasMethod(m, false) 44 | } 45 | 46 | func (t Regexp) Alias(existingMethod, newMethod string) { 47 | t.proto.MakeAlias(existingMethod, newMethod, false) 48 | } 49 | 50 | func init() { 51 | RegexpType.Def("=~", MethodSpec{ 52 | ReturnType: func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 53 | // In reality the match operator returns an int, or nil if there's no match. However, in practical 54 | // use it is relied on for evaluation to a boolean 55 | return BoolType, nil 56 | }, 57 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 58 | return Transform{ 59 | Expr: bst.Call(rcvr.Expr, "MatchString", args[0].Expr), 60 | } 61 | }, 62 | }) 63 | RegexpType.Def("===", MethodSpec{ 64 | ReturnType: func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 65 | // In reality the match operator returns an int, or nil if there's no match. However, in practical 66 | // use it is relied on for evaluation to a boolean 67 | return BoolType, nil 68 | }, 69 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 70 | return Transform{ 71 | Expr: bst.Call(rcvr.Expr, "MatchString", args[0].Expr), 72 | } 73 | }, 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /types/range.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/token" 7 | 8 | "github.com/redneckbeard/thanos/bst" 9 | ) 10 | 11 | type Range struct { 12 | Element Type 13 | Instance instance 14 | } 15 | 16 | var RangeClass = NewClass("Range", "Object", nil, ClassRegistry) 17 | 18 | func NewRange(inner Type) Type { 19 | return Range{Element: inner, Instance: RangeClass.Instance} 20 | } 21 | 22 | func (t Range) Equals(t2 Type) bool { return t == t2 } 23 | func (t Range) String() string { return fmt.Sprintf("Range(%s)", t.Element) } 24 | func (t Range) GoType() string { return fmt.Sprintf("*stdlib.Range[%s]", t.Element.GoType()) } 25 | func (t Range) IsComposite() bool { return true } 26 | func (t Range) Outer() Type { return Range{} } 27 | func (t Range) Inner() Type { return t.Element } 28 | func (t Range) ClassName() string { return "Range" } 29 | func (t Range) IsMultiple() bool { return false } 30 | 31 | func (t Range) HasMethod(m string) bool { 32 | return t.Instance.HasMethod(m) 33 | } 34 | 35 | func (t Range) MethodReturnType(m string, b Type, args []Type) (Type, error) { 36 | return t.Instance.MustResolve(m).ReturnType(t, b, args) 37 | } 38 | 39 | func (t Range) BlockArgTypes(m string, args []Type) []Type { 40 | return t.Instance.MustResolve(m).blockArgs(t, args) 41 | } 42 | 43 | func (t Range) TransformAST(m string, rcvr ast.Expr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 44 | return t.Instance.MustResolve(m).TransformAST(TypeExpr{t, rcvr}, args, blk, it) 45 | } 46 | 47 | func (t Range) Resolve(m string) (MethodSpec, bool) { 48 | return t.Instance.Resolve(m) 49 | } 50 | 51 | func (t Range) MustResolve(m string) MethodSpec { 52 | return t.Instance.MustResolve(m) 53 | } 54 | 55 | func init() { 56 | RangeClass.Instance.Def("===", MethodSpec{ 57 | ReturnType: func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 58 | return BoolType, nil 59 | }, 60 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 61 | upperTok := token.LSS 62 | var lower, upper ast.Expr 63 | if rangeExpr, ok := rcvr.Expr.(*ast.CompositeLit); ok { 64 | lower, upper = rangeExpr.Elts[0], rangeExpr.Elts[1] 65 | if rangeExpr.Elts[2].(*ast.Ident).Name == "true" { 66 | upperTok = token.LEQ 67 | } 68 | return Transform{ 69 | Expr: bst.Binary( 70 | bst.Binary(args[0].Expr, token.GEQ, lower), 71 | token.LAND, 72 | bst.Binary(args[0].Expr, upperTok, upper), 73 | ), 74 | } 75 | } 76 | return Transform{ 77 | Expr: bst.Call(rcvr.Expr, "Covers", args[0].Expr), 78 | } 79 | }, 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /parser/primitives.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "github.com/redneckbeard/thanos/types" 4 | 5 | type BooleanNode struct { 6 | Val string 7 | lineNo int 8 | } 9 | 10 | func (n *BooleanNode) String() string { return n.Val } 11 | func (n *BooleanNode) Type() types.Type { return types.BoolType } 12 | func (n *BooleanNode) SetType(t types.Type) {} 13 | func (n *BooleanNode) LineNo() int { return n.lineNo } 14 | 15 | func (n *BooleanNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 16 | return types.BoolType, nil 17 | } 18 | 19 | // basic literals should never needed to be mutated by a client that's copied a subtree, so these methods are here to satisfy the interface only. 20 | func (n *BooleanNode) Copy() Node { 21 | return n 22 | } 23 | 24 | type IntNode struct { 25 | Val string 26 | lineNo int 27 | } 28 | 29 | func (n *IntNode) String() string { return n.Val } 30 | func (n *IntNode) Type() types.Type { return types.IntType } 31 | func (n *IntNode) SetType(t types.Type) {} 32 | func (n *IntNode) LineNo() int { return n.lineNo } 33 | 34 | func (n *IntNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 35 | return types.IntType, nil 36 | } 37 | 38 | func (n *IntNode) Copy() Node { 39 | return n 40 | } 41 | 42 | type Float64Node struct { 43 | Val string 44 | lineNo int 45 | } 46 | 47 | func (n Float64Node) String() string { return n.Val } 48 | func (n Float64Node) Type() types.Type { return types.FloatType } 49 | func (n Float64Node) SetType(t types.Type) {} 50 | func (n *Float64Node) LineNo() int { return n.lineNo } 51 | 52 | func (n Float64Node) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 53 | return types.FloatType, nil 54 | } 55 | 56 | func (n *Float64Node) Copy() Node { 57 | return n 58 | } 59 | 60 | type SymbolNode struct { 61 | Val string 62 | lineNo int 63 | } 64 | 65 | func (n *SymbolNode) String() string { return n.Val } 66 | func (n *SymbolNode) Type() types.Type { return types.SymbolType } 67 | func (n *SymbolNode) SetType(t types.Type) {} 68 | func (n *SymbolNode) LineNo() int { return n.lineNo } 69 | 70 | func (n *SymbolNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 71 | return types.SymbolType, nil 72 | } 73 | 74 | func (n *SymbolNode) Copy() Node { 75 | return n 76 | } 77 | 78 | type NilNode struct { 79 | lineNo int 80 | } 81 | 82 | func (n *NilNode) String() string { return "nil" } 83 | func (n *NilNode) Type() types.Type { return types.NilType } 84 | func (n *NilNode) SetType(t types.Type) {} 85 | func (n *NilNode) LineNo() int { return n.lineNo } 86 | 87 | func (n *NilNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 88 | return types.NilType, nil 89 | } 90 | 91 | func (n *NilNode) Copy() Node { 92 | return n 93 | } 94 | -------------------------------------------------------------------------------- /parser/scope.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "github.com/redneckbeard/thanos/types" 4 | 5 | type Local interface { 6 | Type() types.Type 7 | } 8 | 9 | type local struct{} 10 | 11 | func (loc *local) Type() types.Type { 12 | return nil 13 | } 14 | 15 | type RubyLocal struct { 16 | _type types.Type 17 | Calls []*MethodCall 18 | } 19 | 20 | func (rl *RubyLocal) String() string { return rl._type.String() } 21 | func (rl *RubyLocal) Type() types.Type { return rl._type } 22 | func (rl *RubyLocal) SetType(t types.Type) { rl._type = t } 23 | func (rl *RubyLocal) AddCall(c *MethodCall) { 24 | rl.Calls = append(rl.Calls, c) 25 | } 26 | 27 | var BadLocal = new(local) 28 | 29 | type Scope interface { 30 | Get(string) (Local, bool) 31 | Set(string, Local) 32 | Name() string 33 | TakesConstants() bool 34 | } 35 | 36 | type SimpleScope struct { 37 | name string 38 | locals map[string]Local 39 | } 40 | 41 | func NewScope(name string) *SimpleScope { 42 | return &SimpleScope{name: name, locals: make(map[string]Local)} 43 | } 44 | 45 | func (scope *SimpleScope) Name() string { 46 | return scope.name 47 | } 48 | 49 | func (scope *SimpleScope) TakesConstants() bool { 50 | return false 51 | } 52 | 53 | func (scope *SimpleScope) Get(name string) (Local, bool) { 54 | if local, ok := scope.locals[name]; ok { 55 | return local, ok 56 | } else { 57 | return local, ok 58 | } 59 | } 60 | 61 | func (scope *SimpleScope) Set(name string, local Local) { 62 | scope.locals[name] = local 63 | } 64 | 65 | type ScopeChain []Scope 66 | 67 | func NewScopeChain() ScopeChain { 68 | return ScopeChain{NewScope("")} 69 | } 70 | 71 | func (chain ScopeChain) Name() string { 72 | return chain[len(chain)-1].Name() 73 | } 74 | 75 | func (chain ScopeChain) Get(name string) (Local, bool) { 76 | return chain[len(chain)-1].Get(name) 77 | } 78 | 79 | func (chain ScopeChain) MustGet(name string) Local { 80 | local, got := chain[len(chain)-1].Get(name) 81 | if got { 82 | return local 83 | } 84 | panic("Called MustGet on Scope but no such local: " + name) 85 | } 86 | 87 | func (chain ScopeChain) Set(name string, local Local) { 88 | chain[len(chain)-1].Set(name, local) 89 | } 90 | 91 | func (chain ScopeChain) ResolveVar(s string) Local { 92 | if len(chain) == 0 { 93 | return BadLocal 94 | } 95 | for i := len(chain) - 1; i >= 0; i-- { 96 | scope := chain[i] 97 | if t, found := scope.Get(s); found { 98 | return t 99 | } 100 | } 101 | return BadLocal 102 | } 103 | 104 | func (chain ScopeChain) Current() Scope { 105 | return chain[len(chain)-1] 106 | } 107 | 108 | func (chain ScopeChain) Extend(scope Scope) ScopeChain { 109 | dst := make(ScopeChain, len(chain)) 110 | copy(dst, chain) 111 | return append(dst, scope) 112 | } 113 | 114 | func (chain ScopeChain) Prefix() string { 115 | var prefix string 116 | for _, scope := range chain { 117 | if _, ok := scope.(ConstantScope); ok { 118 | prefix += scope.Name() 119 | } 120 | } 121 | return prefix 122 | } 123 | -------------------------------------------------------------------------------- /bst/util.go: -------------------------------------------------------------------------------- 1 | package bst 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/token" 7 | "strconv" 8 | ) 9 | 10 | func Call(rcvr interface{}, method interface{}, args ...ast.Expr) *ast.CallExpr { 11 | var fun ast.Expr 12 | if rcvr == nil { 13 | fun = toExpr(method) 14 | } else { 15 | fun = Dot(rcvr, method) 16 | } 17 | return &ast.CallExpr{ 18 | Fun: fun, 19 | Args: args, 20 | } 21 | } 22 | 23 | func Binary(left ast.Expr, op token.Token, right ast.Expr) ast.Expr { 24 | return &ast.BinaryExpr{ 25 | X: left, 26 | Op: op, 27 | Y: right, 28 | } 29 | } 30 | 31 | func Dot(obj, member interface{}) *ast.SelectorExpr { 32 | return &ast.SelectorExpr{ 33 | X: toExpr(obj), 34 | Sel: toExpr(member).(*ast.Ident), 35 | } 36 | } 37 | 38 | func toExpr(i interface{}) ast.Expr { 39 | switch x := i.(type) { 40 | case string: 41 | return ast.NewIdent(x) 42 | case ast.Expr: 43 | return x 44 | default: 45 | panic("only supported types are string and ast.Expr") 46 | } 47 | } 48 | 49 | func String(s string) *ast.BasicLit { 50 | return &ast.BasicLit{ 51 | Kind: token.STRING, 52 | Value: fmt.Sprintf(`"%s"`, s), 53 | } 54 | } 55 | 56 | func Int(i interface{}) *ast.BasicLit { 57 | var val string 58 | if str, ok := i.(string); ok { 59 | val = str 60 | } else { 61 | val = strconv.Itoa(i.(int)) 62 | } 63 | return &ast.BasicLit{ 64 | Kind: token.INT, 65 | Value: val, 66 | } 67 | } 68 | 69 | type AssignFunc func(interface{}, interface{}) *ast.AssignStmt 70 | 71 | var opAssignTokens = map[string]token.Token{ 72 | "+": token.ADD_ASSIGN, 73 | "-": token.SUB_ASSIGN, 74 | "*": token.MUL_ASSIGN, 75 | "/": token.QUO_ASSIGN, 76 | "%": token.REM_ASSIGN, 77 | "&": token.AND_ASSIGN, 78 | "|": token.OR_ASSIGN, 79 | "^": token.XOR_ASSIGN, 80 | "<<": token.SHL_ASSIGN, 81 | ">>": token.SHR_ASSIGN, 82 | "&^": token.AND_NOT_ASSIGN, 83 | } 84 | 85 | func OpAssign(op string) AssignFunc { 86 | return func(lhs, rhs interface{}) *ast.AssignStmt { 87 | return &ast.AssignStmt{ 88 | Lhs: toExprSlice(lhs), 89 | Tok: opAssignTokens[op], 90 | Rhs: toExprSlice(rhs), 91 | } 92 | } 93 | } 94 | 95 | func Assign(lhs, rhs interface{}) *ast.AssignStmt { 96 | return &ast.AssignStmt{ 97 | Lhs: toExprSlice(lhs), 98 | Tok: token.ASSIGN, 99 | Rhs: toExprSlice(rhs), 100 | } 101 | } 102 | 103 | func Define(lhs, rhs interface{}) *ast.AssignStmt { 104 | return &ast.AssignStmt{ 105 | Lhs: toExprSlice(lhs), 106 | Tok: token.DEFINE, 107 | Rhs: toExprSlice(rhs), 108 | } 109 | } 110 | 111 | func Declare(kind token.Token, name *ast.Ident, goType ast.Expr) ast.Decl { 112 | return &ast.GenDecl{ 113 | Tok: kind, 114 | Specs: []ast.Spec{ 115 | &ast.ValueSpec{ 116 | Names: []*ast.Ident{name}, 117 | Type: goType, 118 | }, 119 | }, 120 | } 121 | } 122 | 123 | func toExprSlice(i interface{}) []ast.Expr { 124 | if slice, ok := i.([]ast.Expr); ok { 125 | return slice 126 | } else { 127 | return []ast.Expr{i.(ast.Expr)} 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /parser/identifiers.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "github.com/redneckbeard/thanos/types" 4 | 5 | type IdentNode struct { 6 | Val string 7 | _type types.Type 8 | lineNo int 9 | MethodCall *MethodCall 10 | } 11 | 12 | func (n *IdentNode) String() string { return n.Val } 13 | func (n *IdentNode) Type() types.Type { return n._type } 14 | func (n *IdentNode) SetType(t types.Type) { n._type = t } 15 | func (n *IdentNode) LineNo() int { return n.lineNo } 16 | 17 | func (n *IdentNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 18 | local := locals.ResolveVar(n.Val) 19 | if local == BadLocal { 20 | return nil, NewParseError(n, "local variable or method '%s' did not have discoverable type", n.Val) 21 | } 22 | if m, ok := local.(*MethodCall); ok { 23 | n.MethodCall = m 24 | } 25 | return local.Type(), nil 26 | } 27 | 28 | func (n *IdentNode) Copy() Node { 29 | return &IdentNode{n.Val, n._type, n.lineNo, n.MethodCall} 30 | } 31 | 32 | type GVarNode struct { 33 | Val string 34 | _type types.Type 35 | lineNo int 36 | } 37 | 38 | func (n *GVarNode) String() string { return n.Val } 39 | func (n *GVarNode) Type() types.Type { return n._type } 40 | func (n *GVarNode) SetType(t types.Type) { n._type = t } 41 | func (n *GVarNode) LineNo() int { return n.lineNo } 42 | 43 | func (n *GVarNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 44 | return nil, nil 45 | } 46 | 47 | func (n *GVarNode) Copy() Node { 48 | // globals being global, there should never be a need to mutate one 49 | return n 50 | } 51 | 52 | type ConstantNode struct { 53 | Val string 54 | Namespace string 55 | _type types.Type 56 | lineNo int 57 | } 58 | 59 | func (n *ConstantNode) String() string { return n.Val } 60 | func (n *ConstantNode) Type() types.Type { return n._type } 61 | func (n *ConstantNode) SetType(t types.Type) { n._type = t } 62 | func (n *ConstantNode) LineNo() int { return n.lineNo } 63 | 64 | func (n *ConstantNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 65 | if local := locals.ResolveVar(n.Val); local == BadLocal { 66 | if t, err := types.ClassRegistry.Get(n.Val); err != nil { 67 | if t, ok := types.PredefinedConstants[n.Val]; ok { 68 | return t.Type, nil 69 | } 70 | return nil, err 71 | } else { 72 | return t, nil 73 | } 74 | } else { 75 | if constant, ok := local.(*Constant); ok { 76 | n.Namespace = constant.Namespace.QualifiedName() 77 | } 78 | return local.Type(), nil 79 | } 80 | } 81 | 82 | func (n *ConstantNode) Copy() Node { 83 | // constants being constant, there should never be a need to mutate one 84 | return n 85 | } 86 | 87 | type SelfNode struct { 88 | _type types.Type 89 | lineNo int 90 | } 91 | 92 | func (n *SelfNode) String() string { return "self" } 93 | func (n *SelfNode) Type() types.Type { return n._type } 94 | func (n *SelfNode) SetType(t types.Type) { n._type = t } 95 | func (n *SelfNode) LineNo() int { return n.lineNo } 96 | 97 | func (n *SelfNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 98 | return nil, nil 99 | } 100 | 101 | func (n *SelfNode) Copy() Node { 102 | return &SelfNode{n._type, n.lineNo} 103 | } 104 | -------------------------------------------------------------------------------- /cmd/test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 NAME HERE 3 | 4 | */ 5 | package cmd 6 | 7 | import ( 8 | "fmt" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/fatih/color" 13 | "github.com/redneckbeard/thanos/compiler" 14 | "github.com/redneckbeard/thanos/parser" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | var TestDir, TestFile, TestCase string 19 | 20 | func runTest(script, name string) bool { 21 | fmt.Printf("Running test '%s': ", name) 22 | if script == "" { 23 | color.Red("FAIL\n ") 24 | color.Red("No Ruby source detected") 25 | return false 26 | } 27 | if diff, compiled, err := compiler.CompareThanosToMRI(script, name); err != nil { 28 | color.Red("FAIL\n ") 29 | color.Red(err.Error()) 30 | return false 31 | } else if diff != "" { 32 | color.Red(`FAIL 33 | 34 | %s 35 | Translation: 36 | ------------ 37 | %s`, diff, compiled) 38 | return false 39 | } else { 40 | color.Green("PASS") 41 | return true 42 | } 43 | } 44 | 45 | var testCmd = &cobra.Command{ 46 | Use: "test", 47 | Short: "runs thanos integration tests", 48 | Long: `Runs the thanos integration suite. The test runner loads all files in the 49 | test directory and consumes all 'gauntlet(" }' 50 | calls. The test runner loads the test for the test name provided (or all 51 | tests if no name is given), executes it using system Ruby, transpiles and 52 | executes it using system Go, and then compares the resulting stdout.`, 53 | Run: func(cmd *cobra.Command, args []string) { 54 | tests := map[string]string{} 55 | testFiles, err := filepath.Glob(filepath.Join(TestDir, "*")) 56 | if err != nil { 57 | fmt.Println(err) 58 | return 59 | } 60 | for _, file := range testFiles { 61 | if TestFile == "" || file == filepath.Join(TestDir, TestFile) { 62 | // very possible that we're doing something in the test that is _not_ 63 | // valid Ruby like declaring a constant, so ignore errors this time 64 | // around and report them when we've extracted the body of the gauntlet 65 | // block 66 | program, _ := parser.ParseFile(file) 67 | for _, call := range program.MethodSetStack.Peek().Calls["gauntlet"] { 68 | tests[strings.Trim(call.Args[0].String(), `"'`)] = call.RawBlock 69 | } 70 | } 71 | } 72 | if TestCase != "" { 73 | if script, ok := tests[TestCase]; ok { 74 | runTest(script, TestCase) 75 | } else { 76 | fmt.Println("Could not find test:", TestCase) 77 | } 78 | } else { 79 | var passes, fails int 80 | for name, script := range tests { 81 | if runTest(script, name) { 82 | passes++ 83 | } else { 84 | fails++ 85 | } 86 | } 87 | summary := fmt.Sprintf("\n%d passing tests, %d failures\n", passes, fails) 88 | if fails > 0 { 89 | color.Red(summary) 90 | } else { 91 | color.Green(summary) 92 | } 93 | } 94 | }, 95 | } 96 | 97 | func init() { 98 | rootCmd.AddCommand(testCmd) 99 | testCmd.Flags().StringVarP(&TestDir, "dir", "d", "tests", "Directory where gauntlet tests are located") 100 | testCmd.Flags().StringVarP(&TestFile, "file", "f", "", "Single file relative to test directory from which tests are loaded (default loads all files)") 101 | testCmd.Flags().StringVarP(&TestCase, "gauntlet", "g", "", "Runs only the gauntlet test with the given name") 102 | } 103 | -------------------------------------------------------------------------------- /parser/tokens.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | var tokenNames = map[int]string{ 4 | ADDASSIGN: "ADDASSIGN", 5 | ALIAS: "ALIAS", 6 | AND: "AND", 7 | ANDDOT: "ANDDOT", 8 | ASSIGN: "ASSIGN", 9 | ASTERISK: "ASTERISK", 10 | BANG: "BANG", 11 | BEGIN: "BEGIN", 12 | CVAR: "CVAR", 13 | CARET: "CARET", 14 | CASE: "CASE", 15 | CLASS: "CLASS", 16 | COLON: "COLON", 17 | COMMENT: "COMMENT", 18 | CONSTANT: "CONSTANT", 19 | COMMA: "COMMA", 20 | DEF: "DEF", 21 | DIVASSIGN: "DIVASSIGN", 22 | DO: "DO", 23 | DO_COND: "DO_COND", 24 | DO_BLOCK: "DO_BLOCK", 25 | DOT: "DOT", 26 | DOT2: "DOT2", 27 | DOT3: "DOT3", 28 | DOUBLESPLAT: "DOUBLESPLAT", 29 | ELSE: "ELSE", 30 | ELSIF: "ELSIF", 31 | END: "END", 32 | ENSURE: "ENSURE", 33 | EQ: "EQ", 34 | FALSE: "FALSE", 35 | FLOAT: "FLOAT", 36 | FOR: "FOR", 37 | GVAR: "GVAR", 38 | GT: "GT", 39 | GTE: "GTE", 40 | INTERPBEG: "INTERPBEG", 41 | INTERPEND: "INTERPEND", 42 | IVAR: "IVAR", 43 | IDENT: "IDENT", 44 | IF: "IF", 45 | IN: "IN", 46 | INT: "INT", 47 | LABEL: "LABEL", 48 | LBRACE: "LBRACE", 49 | LBRACEBLOCK: "LBRACEBLOCK", 50 | LBRACKET: "LBRACKET", 51 | LBRACKETSTART: "LBRACKETSTART", 52 | LSHIFT: "LSHIFT", 53 | LSHIFTASSIGN: "LSHIFTASSIGN", 54 | LOGICALAND: "LOGICALAND", 55 | LOGICALOR: "LOGICALOR", 56 | LPAREN: "LPAREN", 57 | LPARENSTART: "LPARENSTART", 58 | LT: "LT", 59 | LTE: "LTE", 60 | MATCH: "MATCH", 61 | METHODIDENT: "METHODIDENT", 62 | MINUS: "MINUS", 63 | MODASSIGN: "MODASSIGN", 64 | MODULE: "MODULE", 65 | MODULO: "MODULO", 66 | MULASSIGN: "MULASSIGN", 67 | NEQ: "NEQ", 68 | NEWLINE: "NEWLINE", 69 | NIL: "NIL", 70 | NOTMATCH: "NOTMATCH", 71 | PIPE: "PIPE", 72 | PLUS: "PLUS", 73 | POW: "POW", 74 | PRIVATE: "PRIVATE", 75 | PROTECTED: "PROTECTED", 76 | QMARK: "QMARK", 77 | RAWSTRINGEND: "RAWSTRINGEND", 78 | RAWWORDSBEG: "RAWWORDSBEG", 79 | RAWXSTRINGBEG: "RAWXSTRINGBEG", 80 | RBRACE: "RBRACE", 81 | REGEXBEG: "REGEXBEG", 82 | REGEXEND: "REGEXEND", 83 | RSHIFT: "RSHIFT", 84 | RSHIFTASSIGN: "RSHIFTASSIGN", 85 | RESCUE: "RESCUE", 86 | SCOPE: "SCOPE", 87 | SLASH: "SLASH", 88 | SPACESHIP: "SPACESHIP", 89 | STRINGBEG: "STRINGBEG", 90 | STRINGBODY: "STRINGBODY", 91 | STRINGEND: "STRINGEND", 92 | SUBASSIGN: "SUBASSIGN", 93 | SUPER: "SUPER", 94 | SYMBOL: "SYMBOL", 95 | THEN: "THEN", 96 | TRUE: "TRUE", 97 | UNARY_NUM: "UNARY_NUM", 98 | UNLESS: "UNLESS", 99 | WORDSBEG: "WORDSBEG", 100 | } 101 | 102 | var exprStartTokens = map[int]int{ 103 | LPAREN: LPARENSTART, 104 | LBRACKET: LBRACKETSTART, 105 | LBRACEBLOCK: LBRACE, 106 | } 107 | 108 | var keywordModifierTokens = map[int]int{ 109 | IF: IF_MOD, 110 | UNLESS: UNLESS_MOD, 111 | WHILE: WHILE_MOD, 112 | UNTIL: UNTIL_MOD, 113 | RESCUE: RESCUE_MOD, 114 | } 115 | -------------------------------------------------------------------------------- /compiler/func.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "strings" 7 | 8 | "github.com/redneckbeard/thanos/parser" 9 | "github.com/redneckbeard/thanos/types" 10 | ) 11 | 12 | func (g *GoProgram) CompileFunc(m *parser.Method, c *parser.Class) []ast.Decl { 13 | decls := []ast.Decl{} 14 | 15 | if m.Block != nil { 16 | funcType := &ast.FuncType{ 17 | Params: &ast.FieldList{ 18 | List: g.GetFuncParams(m.Block.Params), 19 | }, 20 | Results: &ast.FieldList{ 21 | List: g.GetReturnType(m.Block.ReturnType), 22 | }, 23 | } 24 | typeSpec := &ast.TypeSpec{ 25 | Name: g.it.Get(m.Name + strings.Title(m.Block.Name)), 26 | Type: funcType, 27 | } 28 | decls = append(decls, &ast.GenDecl{ 29 | Tok: token.TYPE, 30 | Specs: []ast.Spec{typeSpec}, 31 | }) 32 | } 33 | 34 | if c == nil { 35 | g.State.Push(InFuncDeclaration) 36 | } else { 37 | g.currentRcvr = g.it.Get(strings.ToLower(c.Name()[:1])) 38 | g.State.Push(InMethodDeclaration) 39 | } 40 | g.ScopeChain = m.Scope 41 | g.pushTracker() 42 | defer func() { 43 | g.State.Pop() 44 | g.popTracker() 45 | g.currentRcvr = nil 46 | }() 47 | params := g.GetFuncParams(m.Params) 48 | if m.Block != nil { 49 | params = append(params, &ast.Field{ 50 | Names: []*ast.Ident{g.it.Get(m.Block.Name)}, 51 | Type: g.it.Get(m.Name + strings.Title(m.Block.Name)), 52 | }) 53 | } 54 | 55 | signature := &ast.FuncType{ 56 | Params: &ast.FieldList{ 57 | List: params, 58 | }, 59 | Results: &ast.FieldList{ 60 | List: g.GetReturnType(m.ReturnType()), 61 | }, 62 | } 63 | 64 | decl := &ast.FuncDecl{ 65 | Type: signature, 66 | Body: g.CompileBlockStmt(m.Body.Statements), 67 | Name: g.it.Get(m.GoName()), 68 | } 69 | 70 | if c != nil { 71 | className := g.it.Get(c.Type().GoType()) 72 | decl.Recv = &ast.FieldList{ 73 | List: []*ast.Field{ 74 | { 75 | Names: []*ast.Ident{g.currentRcvr}, 76 | Type: &ast.StarExpr{ 77 | X: className, 78 | }, 79 | }, 80 | }, 81 | } 82 | } 83 | 84 | decls = append(decls, decl) 85 | 86 | return decls 87 | } 88 | 89 | func (g *GoProgram) GetFuncParams(rubyParams []*parser.Param) []*ast.Field { 90 | params := []*ast.Field{} 91 | var splat *parser.Param 92 | for _, p := range rubyParams { 93 | if p.Kind == parser.Splat { 94 | splat = p 95 | continue 96 | } 97 | var ( 98 | lastParam *ast.Field 99 | lastSeenType string 100 | ) 101 | if len(params) > 0 { 102 | lastParam = params[len(params)-1] 103 | lastSeenType = lastParam.Type.(*ast.Ident).Name 104 | } 105 | if lastParam != nil && lastSeenType == p.Type().GoType() { 106 | lastParam.Names = append(lastParam.Names, g.it.Get(p.Name)) 107 | } else { 108 | params = append(params, &ast.Field{ 109 | Names: []*ast.Ident{g.it.Get(p.Name)}, 110 | Type: g.it.Get(p.Type().GoType()), 111 | }) 112 | } 113 | } 114 | if splat != nil { 115 | params = append(params, &ast.Field{ 116 | Names: []*ast.Ident{g.it.Get(splat.Name)}, 117 | Type: &ast.Ellipsis{Elt: g.it.Get(splat.Type().(types.Array).Inner().GoType())}, 118 | }) 119 | } 120 | return params 121 | } 122 | 123 | func (g *GoProgram) GetReturnType(t types.Type) []*ast.Field { 124 | fields := []*ast.Field{} 125 | if t.IsMultiple() { 126 | multiple := t.(types.Multiple) 127 | for _, t := range multiple { 128 | fields = append(fields, g.retTypeField(t)) 129 | } 130 | } else { 131 | fields = append(fields, g.retTypeField(t)) 132 | } 133 | return fields 134 | } 135 | 136 | func (g *GoProgram) retTypeField(t types.Type) *ast.Field { 137 | var retType ast.Expr 138 | switch r := t.(type) { 139 | case types.Array: 140 | retType = &ast.ArrayType{ 141 | Elt: g.it.Get(r.Element.GoType()), 142 | } 143 | default: 144 | if r == types.NilType { 145 | retType = g.it.Get("") 146 | } else { 147 | retType = g.it.Get(r.GoType()) 148 | } 149 | } 150 | return &ast.Field{Type: retType} 151 | } 152 | -------------------------------------------------------------------------------- /types/kernel.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "go/ast" 5 | 6 | "github.com/redneckbeard/thanos/bst" 7 | ) 8 | 9 | type kernel struct { 10 | *proto 11 | } 12 | 13 | var ( 14 | KernelType = kernel{newProto("Kernel", "", ClassRegistry)} 15 | KernelClass = NewClass("Kernel", "", KernelType, ClassRegistry) 16 | ) 17 | 18 | func (t kernel) Equals(t2 Type) bool { return t == t2 } 19 | func (t kernel) String() string { return "kernel" } 20 | func (t kernel) GoType() string { return "n/a" } 21 | func (t kernel) IsComposite() bool { return false } 22 | 23 | func (t kernel) MethodReturnType(m string, b Type, args []Type) (Type, error) { 24 | return t.proto.MustResolve(m, false).ReturnType(t, b, args) 25 | } 26 | 27 | func (t kernel) BlockArgTypes(m string, args []Type) []Type { 28 | return t.proto.MustResolve(m, false).blockArgs(t, args) 29 | } 30 | 31 | func (t kernel) TransformAST(m string, rcvr ast.Expr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 32 | return t.proto.MustResolve(m, false).TransformAST(TypeExpr{t, rcvr}, args, blk, it) 33 | } 34 | 35 | func (t kernel) Resolve(m string) (MethodSpec, bool) { 36 | return t.proto.Resolve(m, false) 37 | } 38 | 39 | func (t kernel) MustResolve(m string) MethodSpec { 40 | return t.proto.MustResolve(m, false) 41 | } 42 | 43 | func (t kernel) HasMethod(m string) bool { 44 | return t.proto.HasMethod(m, false) 45 | } 46 | 47 | func (t kernel) Alias(existingMethod, newMethod string) { 48 | t.proto.MakeAlias(existingMethod, newMethod, false) 49 | } 50 | 51 | func init() { 52 | KernelType.Def("print", MethodSpec{ 53 | ReturnType: func(r Type, b Type, args []Type) (Type, error) { 54 | return NilType, nil 55 | }, 56 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 57 | return Transform{ 58 | Stmts: []ast.Stmt{ 59 | &ast.ExprStmt{ 60 | X: bst.Call("fmt", "Print", UnwrapTypeExprs(args)...), 61 | }, 62 | }, 63 | Imports: []string{"fmt"}, 64 | } 65 | }, 66 | }) 67 | KernelType.Def("puts", MethodSpec{ 68 | ReturnType: func(r Type, b Type, args []Type) (Type, error) { 69 | return NilType, nil 70 | }, 71 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 72 | stmts := []ast.Stmt{} 73 | // `puts` inserts newlines after every argument, so we have one print function call for each here 74 | for _, arg := range args { 75 | // For any args that are interpolated strings, at this point we've 76 | // already translated them to the appropriate C-style interpolated 77 | // string. It would be weird in Go to call out to fmt.Sprintf for an 78 | // arg to fmt.Println, so here we grab those nodes, change the method 79 | // call to Printf, and insert a newline into the end of the string. 80 | if call, ok := arg.Expr.(*ast.CallExpr); ok { 81 | if fname, hasReceiver := call.Fun.(*ast.SelectorExpr); hasReceiver && fname.Sel.Name == "Sprintf" { 82 | fname.Sel = it.Get("Printf") 83 | lit := call.Args[0].(*ast.BasicLit) 84 | lit.Value = lit.Value[:len(lit.Value)-1] + `\n"` 85 | stmts = append(stmts, &ast.ExprStmt{X: call}) 86 | continue 87 | } 88 | } 89 | stmts = append(stmts, &ast.ExprStmt{ 90 | X: bst.Call("fmt", "Println", arg.Expr), 91 | }) 92 | } 93 | return Transform{ 94 | Stmts: stmts, 95 | Imports: []string{"fmt"}, 96 | } 97 | }, 98 | }) 99 | KernelType.Def("gauntlet", MethodSpec{ 100 | blockArgs: func(r Type, args []Type) []Type { 101 | return []Type{} 102 | }, 103 | ReturnType: func(r Type, b Type, args []Type) (Type, error) { 104 | return NilType, nil 105 | }, 106 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 107 | return Transform{ 108 | Expr: &ast.BadExpr{}, 109 | } 110 | }, 111 | }) 112 | KernelType.Def("require", MethodSpec{ 113 | ReturnType: func(r Type, b Type, args []Type) (Type, error) { 114 | return BoolType, nil 115 | }, 116 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 117 | return Transform{} 118 | }, 119 | }) 120 | } 121 | -------------------------------------------------------------------------------- /compiler/class.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/token" 7 | "strings" 8 | 9 | "github.com/redneckbeard/thanos/bst" 10 | "github.com/redneckbeard/thanos/parser" 11 | "github.com/redneckbeard/thanos/types" 12 | ) 13 | 14 | func (g *GoProgram) CompileModule(mod *parser.Module) []ast.Decl { 15 | g.addConstants(mod.Constants) 16 | 17 | var decls []ast.Decl 18 | 19 | for _, cls := range mod.Classes { 20 | decls = append(decls, g.CompileClass(cls)...) 21 | } 22 | 23 | return decls 24 | } 25 | 26 | func (g *GoProgram) CompileClass(c *parser.Class) []ast.Decl { 27 | className := globalIdents.Get(c.QualifiedName()) 28 | decls := []ast.Decl{} 29 | 30 | structFields := []*ast.Field{} 31 | for _, t := range c.IVars(nil) { 32 | name := t.Name 33 | if t.Readable && t.Writeable { 34 | name = strings.Title(name) 35 | } 36 | structFields = append(structFields, &ast.Field{ 37 | Names: []*ast.Ident{g.it.Get(name)}, 38 | Type: g.it.Get(t.Type().GoType()), 39 | }) 40 | } 41 | g.addConstants(c.Constants) 42 | decls = append(decls, &ast.GenDecl{ 43 | Tok: token.TYPE, 44 | Specs: []ast.Spec{ 45 | &ast.TypeSpec{ 46 | Name: className, 47 | Type: &ast.StructType{ 48 | Fields: &ast.FieldList{ 49 | List: structFields, 50 | }, 51 | }, 52 | }, 53 | }, 54 | }) 55 | // hand roll a constructor 56 | params := []*ast.Field{} 57 | results := []*ast.Field{ 58 | { 59 | Type: &ast.StarExpr{ 60 | X: className, 61 | }, 62 | }, 63 | } 64 | 65 | g.newBlockStmt() 66 | 67 | var ( 68 | initialize *parser.Method 69 | cls = c 70 | ) 71 | 72 | for cls != nil && initialize == nil { 73 | initialize = cls.MethodSet.Methods["initialize"] 74 | if initialize == nil { 75 | cls = cls.Parent() 76 | } 77 | } 78 | 79 | g.appendToCurrentBlock(bst.Define(g.it.Get("newInstance"), 80 | &ast.UnaryExpr{ 81 | Op: token.AND, 82 | X: &ast.CompositeLit{ 83 | Type: className, 84 | }, 85 | })) 86 | 87 | if initialize != nil { 88 | params = g.GetFuncParams(initialize.Params) 89 | var args []ast.Expr 90 | for _, p := range initialize.Params { 91 | args = append(args, g.it.Get(p.Name)) 92 | } 93 | g.appendToCurrentBlock(&ast.ExprStmt{ 94 | X: bst.Call("newInstance", "Initialize", args...), 95 | }) 96 | } 97 | 98 | signature := &ast.FuncType{ 99 | Params: &ast.FieldList{ 100 | List: params, 101 | }, 102 | Results: &ast.FieldList{ 103 | List: results, 104 | }, 105 | } 106 | 107 | g.appendToCurrentBlock(&ast.ReturnStmt{ 108 | Results: []ast.Expr{g.it.Get("newInstance")}, 109 | }) 110 | 111 | constructor := &ast.FuncDecl{ 112 | Name: g.it.Get(fmt.Sprintf("New%s", c.QualifiedName())), 113 | Type: signature, 114 | Body: g.BlockStack.Peek(), 115 | } 116 | 117 | decls = append(decls, constructor) 118 | 119 | g.BlockStack.Pop() 120 | 121 | var hasToS bool 122 | for _, m := range c.Methods(nil) { 123 | decls = append(decls, g.CompileFunc(m, c)...) 124 | if m.Name == "to_s" { 125 | hasToS = true 126 | } 127 | } 128 | 129 | if hasToS { 130 | decls = append(decls, g.stringMethod(c)) 131 | } 132 | 133 | return decls 134 | } 135 | 136 | func (g *GoProgram) addConstants(constants []*parser.Constant) { 137 | for _, constant := range constants { 138 | switch constant.Val.Type() { 139 | case types.IntType, types.SymbolType, types.FloatType, types.StringType, types.BoolType: 140 | g.addConstant(g.it.Get(constant.QualifiedName()), g.CompileExpr(constant.Val)) 141 | default: 142 | g.addGlobalVar(g.it.Get(constant.QualifiedName()), g.it.Get(constant.Val.Type().GoType()), g.CompileExpr(constant.Val)) 143 | } 144 | } 145 | } 146 | 147 | func (g *GoProgram) stringMethod(cls *parser.Class) ast.Decl { 148 | signature := &ast.FuncType{ 149 | Results: &ast.FieldList{ 150 | List: g.GetReturnType(types.StringType), 151 | }, 152 | } 153 | 154 | rcvr := strings.ToLower(cls.Name()[:1]) 155 | 156 | return &ast.FuncDecl{ 157 | Type: signature, 158 | Body: &ast.BlockStmt{ 159 | List: []ast.Stmt{ 160 | &ast.ReturnStmt{ 161 | Results: []ast.Expr{ 162 | bst.Call(rcvr, "To_s"), 163 | }, 164 | }, 165 | }, 166 | }, 167 | Name: g.it.Get("String"), 168 | Recv: &ast.FieldList{ 169 | List: []*ast.Field{ 170 | { 171 | Names: []*ast.Ident{g.it.Get(rcvr)}, 172 | Type: &ast.StarExpr{ 173 | X: g.it.Get(cls.QualifiedName()), 174 | }, 175 | }, 176 | }, 177 | }, 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /types/types.go: -------------------------------------------------------------------------------- 1 | //go:generate stringer -type Simple 2 | 3 | // package types provides an interface and many implementations of that 4 | // interface as an abstraction, however leaky, of the union of the Go type 5 | // system and the Ruby Object system. 6 | package types 7 | 8 | import ( 9 | "go/ast" 10 | "reflect" 11 | "regexp" 12 | "strings" 13 | 14 | "github.com/redneckbeard/thanos/bst" 15 | ) 16 | 17 | type Type interface { 18 | BlockArgTypes(string, []Type) []Type 19 | ClassName() string 20 | Equals(Type) bool 21 | GoType() string 22 | HasMethod(string) bool 23 | IsComposite() bool 24 | IsMultiple() bool 25 | MethodReturnType(string, Type, []Type) (Type, error) 26 | String() string 27 | TransformAST(string, ast.Expr, []TypeExpr, *Block, bst.IdentTracker) Transform 28 | } 29 | 30 | type TypeExpr struct { 31 | Type Type 32 | Expr ast.Expr 33 | } 34 | 35 | type Simple int 36 | 37 | func (t Simple) GoType() string { return typeMap[t] } 38 | func (t Simple) IsComposite() bool { return false } 39 | func (t Simple) IsMultiple() bool { return false } 40 | func (t Simple) ClassName() string { return "" } 41 | func (t Simple) Equals(t2 Type) bool { return t == t2 } 42 | 43 | // lies but needed for now 44 | func (t Simple) HasMethod(m string) bool { return false } 45 | func (t Simple) MethodReturnType(m string, b Type, args []Type) (Type, error) { return nil, nil } 46 | func (t Simple) BlockArgTypes(m string, args []Type) []Type { return []Type{nil} } 47 | func (t Simple) TransformAST(m string, rcvr ast.Expr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 48 | return Transform{} 49 | } 50 | 51 | const ( 52 | ConstType Simple = iota 53 | NilType 54 | FuncType 55 | AnyType 56 | ErrorType 57 | ) 58 | 59 | var typeMap = map[Simple]string{ 60 | ConstType: "const", 61 | NilType: "nil", 62 | FuncType: "func", 63 | } 64 | 65 | var goTypeMap = map[reflect.Kind]Type{ 66 | reflect.Int: IntType, 67 | reflect.Float64: FloatType, 68 | reflect.String: StringType, 69 | reflect.Bool: BoolType, 70 | } 71 | 72 | var goTypeMapByString = map[string]interface{}{ 73 | "int": IntType, 74 | "float64": FloatType, 75 | "string": StringType, 76 | "bool": BoolType, 77 | } 78 | 79 | var generic = regexp.MustCompile(`([A-Z]\w+)\[(\w+)\]`) 80 | 81 | func typeName(t reflect.Type) (string, string) { 82 | submatches := generic.FindStringSubmatch(t.Name()) 83 | if len(submatches) > 1 { 84 | return submatches[1], submatches[2] 85 | } 86 | return t.Name(), "" 87 | } 88 | 89 | func RegisterType(goValue interface{}, thanosTypeOrConstructor interface{}) { 90 | switch thanosTypeOrConstructor.(type) { 91 | case Type: 92 | case func(Type) Type: 93 | default: 94 | panic("Attempted to register a Go type with something other than a thanos type or type constructor") 95 | } 96 | container, _ := typeName(reflect.TypeOf(goValue)) 97 | goTypeMapByString[container] = thanosTypeOrConstructor 98 | } 99 | 100 | func getGenericType(t reflect.Type, rcvr Type, typeParam string) Type { 101 | container, inner := typeName(t) 102 | if typeParam != "" && inner == typeParam { 103 | outer := goTypeMapByString[container].(func(Type) Type) 104 | return outer(rcvr.(CompositeType).Inner()) 105 | } 106 | tt := goTypeMapByString[container] 107 | if tt != nil { 108 | return tt.(Type) 109 | } 110 | return nil 111 | } 112 | 113 | type Multiple []Type 114 | 115 | func (t Multiple) GoType() string { return "" } 116 | func (t Multiple) IsComposite() bool { return false } 117 | func (t Multiple) IsMultiple() bool { return true } 118 | func (t Multiple) ClassName() string { return "" } 119 | func (t Multiple) String() string { 120 | segments := []string{} 121 | for _, s := range t { 122 | segments = append(segments, s.String()) 123 | } 124 | return strings.Join(segments, ", ") 125 | } 126 | func (t Multiple) HasMethod(m string) bool { return false } 127 | func (t Multiple) MethodReturnType(m string, b Type, args []Type) (Type, error) { return nil, nil } 128 | func (t Multiple) BlockArgTypes(m string, args []Type) []Type { return []Type{nil} } 129 | func (t Multiple) TransformAST(m string, rcvr ast.Expr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 130 | return Transform{} 131 | } 132 | func (t Multiple) Imports(s string) []string { return []string{} } 133 | 134 | func (mt Multiple) Equals(t Type) bool { 135 | mt2, ok := t.(Multiple) 136 | if !ok { 137 | return false 138 | } 139 | if len(mt) != len(mt2) { 140 | return false 141 | } 142 | for i, t := range mt { 143 | if t != mt2[i] { 144 | return false 145 | } 146 | } 147 | return true 148 | } 149 | 150 | type CompositeType interface { 151 | Type 152 | Outer() Type 153 | Inner() Type 154 | } 155 | 156 | type Block struct { 157 | Args []ast.Expr 158 | ReturnType Type 159 | Statements []ast.Stmt 160 | } 161 | -------------------------------------------------------------------------------- /types/set.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/token" 7 | 8 | "github.com/redneckbeard/thanos/bst" 9 | "github.com/redneckbeard/thanos/stdlib" 10 | ) 11 | 12 | type Set struct { 13 | Element Type 14 | Instance instance 15 | } 16 | 17 | var SetClass = NewClass("Set", "Object", nil, ClassRegistry) 18 | 19 | func NewSet(inner Type) Type { 20 | return Set{Element: inner, Instance: SetClass.Instance} 21 | } 22 | 23 | func (t Set) Equals(t2 Type) bool { return t == t2 } 24 | func (t Set) String() string { return fmt.Sprintf("Set{%s}", t.Element) } 25 | func (t Set) GoType() string { return fmt.Sprintf("*stdlib.Set[%s]", t.Element.GoType()) } 26 | func (t Set) IsComposite() bool { return true } 27 | func (t Set) Outer() Type { return Set{} } 28 | func (t Set) Inner() Type { return t.Element } 29 | func (t Set) ClassName() string { return "Set" } 30 | func (t Set) IsMultiple() bool { return false } 31 | 32 | func (t Set) HasMethod(m string) bool { 33 | return t.Instance.HasMethod(m) 34 | } 35 | 36 | func (t Set) MethodReturnType(m string, b Type, args []Type) (Type, error) { 37 | return t.Instance.MustResolve(m).ReturnType(t, b, args) 38 | } 39 | 40 | func (t Set) BlockArgTypes(m string, args []Type) []Type { 41 | return t.Instance.MustResolve(m).blockArgs(t, args) 42 | } 43 | 44 | func (t Set) TransformAST(m string, rcvr ast.Expr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 45 | return t.Instance.MustResolve(m).TransformAST(TypeExpr{t, rcvr}, args, blk, it) 46 | } 47 | 48 | func (t Set) Resolve(m string) (MethodSpec, bool) { 49 | return t.Instance.Resolve(m) 50 | } 51 | 52 | func (t Set) MustResolve(m string) MethodSpec { 53 | return t.Instance.MustResolve(m) 54 | } 55 | 56 | func init() { 57 | RegisterType(stdlib.Set[bool]{}, NewSet) 58 | SetClass.Instance.GenerateMethods(stdlib.Set[bool]{}) 59 | SetClass.Instance.Def("initialize", MethodSpec{ 60 | ReturnType: func(r Type, b Type, args []Type) (Type, error) { 61 | if arr, ok := args[0].(Array); ok { 62 | return NewSet(arr.Element), nil 63 | } 64 | return nil, fmt.Errorf("Got %s as argument to set constructor but only Arrays are allowed", args[0]) 65 | }, 66 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 67 | return Transform{ 68 | Expr: bst.Call("stdlib", "NewSet", UnwrapTypeExprs(args)...), 69 | Imports: []string{"github.com/redneckbeard/thanos/stdlib"}, 70 | } 71 | }, 72 | }) 73 | SetClass.Def("[]", MethodSpec{ 74 | ReturnType: func(r Type, b Type, args []Type) (Type, error) { 75 | if len(args) == 0 { 76 | return nil, fmt.Errorf("Cannot infer inner type of an empty set") 77 | } 78 | argType := args[0] 79 | for _, arg := range args[1:] { 80 | if arg != argType { 81 | return nil, fmt.Errorf("Cannot construct set with heterogenous member types") 82 | } 83 | } 84 | return NewSet(argType), nil 85 | }, 86 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 87 | arr := &ast.CompositeLit{ 88 | Type: &ast.ArrayType{ 89 | Elt: it.Get(args[0].Type.GoType()), 90 | }, 91 | Elts: UnwrapTypeExprs(args), 92 | } 93 | return Transform{ 94 | Expr: bst.Call("stdlib", "NewSet", arr), 95 | Imports: []string{"github.com/redneckbeard/thanos/stdlib"}, 96 | } 97 | }, 98 | }) 99 | SetClass.Instance.Alias("intersection", "&") 100 | SetClass.Instance.Alias("union", "|") 101 | SetClass.Instance.Alias("union", "+") 102 | SetClass.Instance.Alias("difference", "-") 103 | SetClass.Instance.Alias("disjoint", "^") 104 | SetClass.Instance.Alias("superset?", ">=") 105 | SetClass.Instance.Alias("proper_superset?", ">") 106 | SetClass.Instance.Alias("subset?", "<=") 107 | SetClass.Instance.Alias("proper_subset?", "<") 108 | SetClass.Instance.Def("each", MethodSpec{ 109 | blockArgs: func(r Type, args []Type) []Type { 110 | return []Type{r.(Set).Element} 111 | }, 112 | ReturnType: func(r Type, b Type, args []Type) (Type, error) { 113 | return r, nil 114 | }, 115 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 116 | var transformedFinal *ast.ExprStmt 117 | finalStatement := blk.Statements[len(blk.Statements)-1] 118 | switch f := finalStatement.(type) { 119 | case *ast.ReturnStmt: 120 | transformedFinal = &ast.ExprStmt{ 121 | X: f.Results[0], 122 | } 123 | case *ast.ExprStmt: 124 | transformedFinal = f 125 | default: 126 | panic("Encountered an unexpected node type") 127 | } 128 | 129 | blk.Statements[len(blk.Statements)-1] = transformedFinal 130 | 131 | loop := &ast.RangeStmt{ 132 | Key: blk.Args[0], 133 | Value: it.Get("_"), 134 | Tok: token.DEFINE, 135 | X: rcvr.Expr, 136 | Body: &ast.BlockStmt{ 137 | List: blk.Statements, 138 | }, 139 | } 140 | 141 | return Transform{ 142 | Expr: rcvr.Expr, 143 | Stmts: []ast.Stmt{loop}, 144 | } 145 | }, 146 | }) 147 | } 148 | -------------------------------------------------------------------------------- /types/class.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "reflect" 7 | "sort" 8 | "sync" 9 | 10 | "github.com/redneckbeard/thanos/bst" 11 | ) 12 | 13 | type Class struct { 14 | name, parentName, Prefix string 15 | *proto 16 | Instance instance 17 | parent *Class 18 | children []*Class 19 | UserDefined bool 20 | } 21 | 22 | func NewClass(name, parent string, inst instance, registry *classRegistry) *Class { 23 | class := &Class{ 24 | name: name, 25 | parentName: parent, 26 | Instance: inst, 27 | proto: newProto(name, parent, registry), 28 | } 29 | if inst == nil { 30 | class.Instance = Instance{name: name, proto: newProto(name, parent, registry)} 31 | } 32 | registry.RegisterClass(class) 33 | return class 34 | } 35 | 36 | var classProto *proto = newProto("Class", "Object", ClassRegistry) 37 | 38 | func (t *Class) Equals(t2 Type) bool { return reflect.DeepEqual(t, t2) } 39 | func (t *Class) String() string { return fmt.Sprintf("%sClass", t.name) } 40 | func (t *Class) GoType() string { return t.Prefix + t.name } 41 | func (t *Class) IsComposite() bool { return false } 42 | 43 | func (t *Class) MethodReturnType(m string, b Type, args []Type) (Type, error) { 44 | return t.MustResolve(m).ReturnType(t, b, args) 45 | } 46 | 47 | func (t *Class) BlockArgTypes(m string, args []Type) []Type { 48 | return t.MustResolve(m).blockArgs(t, args) 49 | } 50 | 51 | func (t *Class) TransformAST(m string, rcvr ast.Expr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 52 | return t.MustResolve(m).TransformAST(TypeExpr{t, rcvr}, args, blk, it) 53 | } 54 | 55 | func (t *Class) Resolve(m string) (MethodSpec, bool) { 56 | if m == "new" { 57 | return t.Instance.Resolve("initialize") 58 | } 59 | return t.proto.Resolve(m, true) 60 | } 61 | 62 | func (t *Class) MustResolve(m string) MethodSpec { 63 | method, has := t.Resolve(m) 64 | if !has { 65 | panic(fmt.Errorf("Could not resolve class method '%s' on class '%s'", m, t)) 66 | } 67 | return method 68 | } 69 | 70 | func (t *Class) HasMethod(m string) bool { 71 | _, has := t.Resolve(m) 72 | return has 73 | } 74 | 75 | func (t *Class) Constructor() string { 76 | return fmt.Sprintf("New%s", t.Prefix+t.name) 77 | } 78 | 79 | type classRegistry struct { 80 | sync.Mutex 81 | sync.WaitGroup 82 | registry map[string]*Class 83 | initialized bool 84 | } 85 | 86 | var ClassRegistry = &classRegistry{registry: make(map[string]*Class)} 87 | 88 | func (cr *classRegistry) Get(name string) (*Class, error) { 89 | if !cr.initialized { 90 | return nil, fmt.Errorf("Attempted to look up class '%s' from registry before registry initialized", name) 91 | } 92 | if class, found := cr.registry[name]; found { 93 | return class, nil 94 | } 95 | return nil, fmt.Errorf("Attempted to look up class '%s' from registry but no matching class found", name) 96 | } 97 | 98 | func (cr *classRegistry) MustGet(name string) *Class { 99 | if class, found := cr.registry[name]; found { 100 | return class 101 | } 102 | panic(fmt.Sprintf("Failed to find class %s", name)) 103 | } 104 | 105 | func (cr *classRegistry) RegisterClass(cls *Class) { 106 | cr.Lock() 107 | cr.registry[cls.name] = cls 108 | cr.Unlock() 109 | go func() { 110 | for cls.parent == nil && cls.parentName != "" { 111 | //TODO probably use a WaitGroup instead so Analyze can be guaranteed that all ancestry chains are established 112 | cr.Lock() 113 | parent, found := cr.registry[cls.parentName] 114 | cr.Unlock() 115 | if found { 116 | cls.parent = parent 117 | parent.children = append(parent.children, cls) 118 | } 119 | } 120 | }() 121 | } 122 | 123 | func (cr *classRegistry) Initialize() error { 124 | cr.Wait() 125 | for _, class := range cr.registry { 126 | if class.parentName != "" && class.parent == nil { 127 | parent, found := cr.registry[class.parentName] 128 | if !found { 129 | return fmt.Errorf("Class '%s' described as having parent '%s' but no class '%s' was ever registered", class.name, class.parentName, class.parentName) 130 | } 131 | class.parent = parent 132 | parent.children = append(parent.children, class) 133 | } 134 | } 135 | cr.initialized = true 136 | return nil 137 | } 138 | 139 | func (cr *classRegistry) Names() []string { 140 | names := []string{} 141 | for k := range cr.registry { 142 | names = append(names, k) 143 | } 144 | sort.Strings(names) 145 | return names 146 | } 147 | 148 | type instance interface { 149 | Def(string, MethodSpec) 150 | Resolve(string) (MethodSpec, bool) 151 | MustResolve(string) MethodSpec 152 | HasMethod(string) bool 153 | GenerateMethods(interface{}, ...string) 154 | Methods() map[string]MethodSpec 155 | Alias(string, string) 156 | } 157 | 158 | type Instance struct { 159 | name string 160 | *proto 161 | } 162 | 163 | func (t Instance) Equals(t2 Type) bool { return reflect.DeepEqual(t, t2) } 164 | func (t Instance) String() string { return t.name } 165 | func (t Instance) GoType() string { return "*" + t.name } 166 | func (t Instance) IsComposite() bool { return false } 167 | 168 | func (t Instance) MethodReturnType(m string, b Type, args []Type) (Type, error) { 169 | return t.proto.MustResolve(m, false).ReturnType(t, b, args) 170 | } 171 | 172 | func (t Instance) BlockArgTypes(m string, args []Type) []Type { 173 | return t.proto.MustResolve(m, false).blockArgs(t, args) 174 | } 175 | 176 | func (t Instance) TransformAST(m string, rcvr ast.Expr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 177 | return t.proto.MustResolve(m, false).TransformAST(TypeExpr{t, rcvr}, args, blk, it) 178 | } 179 | 180 | func (t Instance) Resolve(m string) (MethodSpec, bool) { 181 | return t.proto.Resolve(m, false) 182 | } 183 | 184 | func (t Instance) MustResolve(m string) MethodSpec { 185 | return t.proto.MustResolve(m, false) 186 | } 187 | 188 | func (t Instance) HasMethod(m string) bool { 189 | return t.proto.HasMethod(m, false) 190 | } 191 | 192 | func (t Instance) Alias(existingMethod, newMethod string) { 193 | t.proto.MakeAlias(existingMethod, newMethod, false) 194 | } 195 | -------------------------------------------------------------------------------- /compiler/compiler.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/ast" 7 | "go/format" 8 | "go/token" 9 | "os/exec" 10 | "sort" 11 | 12 | "github.com/redneckbeard/thanos/bst" 13 | "github.com/redneckbeard/thanos/parser" 14 | ) 15 | 16 | type State string 17 | 18 | const ( 19 | InFuncDeclaration State = "InFuncDeclaration" 20 | InMethodDeclaration State = "InMethodDeclaration" 21 | InReturnStatement State = "InReturnStatement" 22 | InCondAssignment State = "InCondAssignment" 23 | InBlockBody State = "InBlockBody" 24 | ) 25 | 26 | var globalIdents = make(bst.IdentTracker) 27 | 28 | type GoProgram struct { 29 | State *parser.Stack[State] 30 | ScopeChain parser.ScopeChain 31 | Imports map[string]bool 32 | CurrentLhs []parser.Node 33 | BlockStack *parser.Stack[*ast.BlockStmt] 34 | GlobalVars []*ast.ValueSpec 35 | Constants []*ast.ValueSpec 36 | TrackerStack []bst.IdentTracker 37 | it bst.IdentTracker 38 | currentRcvr *ast.Ident 39 | } 40 | 41 | func Compile(p *parser.Root) (string, error) { 42 | globalIdents = make(bst.IdentTracker) 43 | g := &GoProgram{State: &parser.Stack[State]{}, Imports: make(map[string]bool), BlockStack: &parser.Stack[*ast.BlockStmt]{}} 44 | g.pushTracker() 45 | 46 | f := &ast.File{ 47 | Name: ast.NewIdent("main"), 48 | } 49 | 50 | decls := []ast.Decl{} 51 | 52 | for _, o := range p.Objects { 53 | if m, ok := o.(*parser.Method); ok { 54 | decls = append(decls, g.CompileFunc(m, nil)...) 55 | } 56 | } 57 | 58 | for _, mod := range p.TopLevelModules { 59 | decls = append(decls, g.CompileModule(mod)...) 60 | } 61 | 62 | for _, class := range p.Classes { 63 | decls = append(decls, g.CompileClass(class)...) 64 | } 65 | 66 | mainFunc := &ast.FuncDecl{ 67 | Name: ast.NewIdent("main"), 68 | Type: &ast.FuncType{ 69 | Params: &ast.FieldList{}, 70 | }, 71 | } 72 | 73 | g.newBlockStmt() 74 | g.pushTracker() 75 | for _, stmt := range p.Statements { 76 | g.CompileStmt(stmt) 77 | } 78 | g.popTracker() 79 | mainFunc.Body = g.BlockStack.Peek() 80 | g.BlockStack.Pop() 81 | 82 | decls = append(decls, mainFunc) 83 | 84 | importPaths := []string{} 85 | 86 | for imp := range g.Imports { 87 | importPaths = append(importPaths, imp) 88 | } 89 | 90 | sort.Strings(importPaths) 91 | 92 | importSpecs := []ast.Spec{} 93 | 94 | for _, path := range importPaths { 95 | importSpecs = append(importSpecs, &ast.ImportSpec{ 96 | Path: bst.String(path), 97 | }) 98 | } 99 | 100 | topDecls := []ast.Decl{} 101 | 102 | if len(importSpecs) > 0 { 103 | topDecls = append(topDecls, &ast.GenDecl{ 104 | Tok: token.IMPORT, 105 | Specs: importSpecs, 106 | }) 107 | } 108 | 109 | for _, spec := range g.Constants { 110 | topDecls = append(topDecls, &ast.GenDecl{ 111 | Tok: token.CONST, 112 | Specs: []ast.Spec{spec}, 113 | }) 114 | } 115 | 116 | for _, spec := range g.GlobalVars { 117 | topDecls = append(topDecls, &ast.GenDecl{ 118 | Tok: token.VAR, 119 | Specs: []ast.Spec{spec}, 120 | }) 121 | } 122 | 123 | f.Decls = append(topDecls, decls...) 124 | 125 | var in, out bytes.Buffer 126 | err := format.Node(&in, token.NewFileSet(), f) 127 | if err != nil { 128 | return "", fmt.Errorf("Error converting AST to []byte: %s", err.Error()) 129 | } 130 | 131 | intermediate := in.String() 132 | 133 | cmd := exec.Command("goimports") 134 | cmd.Stdin = &in 135 | cmd.Stdout = &out 136 | err = cmd.Run() 137 | if err != nil { 138 | fmt.Println(intermediate) 139 | return intermediate, fmt.Errorf("Error running gofmt: %s", err.Error()) 140 | } 141 | 142 | return out.String(), nil 143 | } 144 | 145 | // A Ruby expression will often translate into multiple Go statements, and so 146 | // we need a way to prepend statements prior to where an expression gets 147 | // translated if required. To achieve this, we maintain a stack of 148 | // *ast.BlockStmt that is pushed to and popped from as we work our way down the 149 | // tree. The top of this stack is available for method translating other nodes 150 | // to append to. Because they can append before they complete, they can get 151 | // preceding variable declarations, loops, etc. in place before the expression 152 | // or statement at hand is added. 153 | func (g *GoProgram) newBlockStmt() *ast.BlockStmt { 154 | blockStmt := &ast.BlockStmt{} 155 | g.BlockStack.Push(blockStmt) 156 | return blockStmt 157 | } 158 | 159 | func (g *GoProgram) pushTracker() { 160 | g.it = make(bst.IdentTracker) 161 | g.TrackerStack = append(g.TrackerStack, g.it) 162 | } 163 | 164 | func (g *GoProgram) popTracker() { 165 | if len(g.TrackerStack) > 0 { 166 | g.TrackerStack = g.TrackerStack[:len(g.TrackerStack)-1] 167 | if len(g.TrackerStack) > 0 { 168 | g.it = g.TrackerStack[len(g.TrackerStack)-1] 169 | } 170 | } 171 | } 172 | 173 | func (g *GoProgram) appendToCurrentBlock(stmts ...ast.Stmt) { 174 | currentBlock := g.BlockStack.Peek() 175 | currentBlock.List = append(currentBlock.List, stmts...) 176 | } 177 | 178 | func (g *GoProgram) AddImports(packages ...string) { 179 | for _, pkg := range packages { 180 | if _, present := g.Imports[pkg]; !present { 181 | g.Imports[pkg] = true 182 | } 183 | } 184 | } 185 | 186 | func (g *GoProgram) addGlobalVar(name *ast.Ident, typeExpr ast.Expr, val ast.Expr) { 187 | g.GlobalVars = append(g.GlobalVars, &ast.ValueSpec{ 188 | Names: []*ast.Ident{name}, 189 | Type: typeExpr, 190 | Values: []ast.Expr{val}, 191 | }) 192 | } 193 | 194 | func (g *GoProgram) addConstant(name *ast.Ident, val ast.Expr) { 195 | g.Constants = append(g.Constants, &ast.ValueSpec{ 196 | Names: []*ast.Ident{name}, 197 | Values: []ast.Expr{val}, 198 | }) 199 | } 200 | 201 | func (g *GoProgram) mapToExprs(nodes []parser.Node) []ast.Expr { 202 | exprs := []ast.Expr{} 203 | for _, n := range nodes { 204 | exprs = append(exprs, g.CompileExpr(n)) 205 | } 206 | return exprs 207 | } 208 | 209 | func isSimple(i interface{}) bool { 210 | switch i.(type) { 211 | case *ast.BasicLit: 212 | return true 213 | case *ast.Ident: 214 | return true 215 | default: 216 | return false 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /types/object.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "strconv" 7 | 8 | "github.com/redneckbeard/thanos/bst" 9 | ) 10 | 11 | type Object struct { 12 | *proto 13 | } 14 | 15 | var ObjectType = Object{newProto("Object", "", ClassRegistry)} 16 | 17 | var ObjectClass = NewClass("Object", "", ObjectType, ClassRegistry) 18 | 19 | func (t Object) Equals(t2 Type) bool { return t == t2 } 20 | func (t Object) String() string { return "Object" } 21 | func (t Object) GoType() string { return "" } 22 | func (t Object) IsComposite() bool { return false } 23 | 24 | func (t Object) MethodReturnType(m string, b Type, args []Type) (Type, error) { 25 | return ObjectType.proto.MustResolve(m, false).ReturnType(t, b, args) 26 | } 27 | 28 | func (t Object) BlockArgTypes(m string, args []Type) []Type { 29 | return ObjectType.proto.MustResolve(m, false).blockArgs(t, args) 30 | } 31 | 32 | func (t Object) TransformAST(m string, rcvr ast.Expr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 33 | return ObjectType.proto.MustResolve(m, false).TransformAST(TypeExpr{t, rcvr}, args, blk, it) 34 | } 35 | 36 | func (t Object) Resolve(m string) (MethodSpec, bool) { 37 | return t.proto.Resolve(m, false) 38 | } 39 | 40 | func (t Object) MustResolve(m string) MethodSpec { 41 | return t.proto.MustResolve(m, false) 42 | } 43 | 44 | func (t Object) HasMethod(m string) bool { 45 | return ObjectType.proto.HasMethod(m, false) 46 | } 47 | 48 | func (t Object) Alias(existingMethod, newMethod string) { 49 | t.proto.MakeAlias(existingMethod, newMethod, false) 50 | } 51 | 52 | func logicalOperatorSpec(tok token.Token) MethodSpec { 53 | return MethodSpec{ 54 | ReturnType: func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 55 | return BoolType, nil 56 | }, 57 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 58 | left, right := rcvr.Expr, args[0].Expr 59 | //TODO in both these cases, this will be wrong for numeric types and strings 60 | if rcvr.Type != BoolType { 61 | left = bst.Binary(left, token.NEQ, it.Get("nil")) 62 | } 63 | if args[0].Type != BoolType { 64 | right = bst.Binary(right, token.NEQ, it.Get("nil")) 65 | } 66 | return Transform{ 67 | Expr: bst.Binary(left, tok, right), 68 | } 69 | }, 70 | } 71 | } 72 | 73 | func init() { 74 | ObjectType.Def("==", MethodSpec{ 75 | ReturnType: func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 76 | return BoolType, nil 77 | }, 78 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 79 | return Transform{ 80 | Expr: bst.Call("reflect", "DeepEqual", rcvr.Expr, args[0].Expr), 81 | Imports: []string{"reflect"}, 82 | } 83 | }, 84 | }) 85 | 86 | ObjectType.Def("&&", logicalOperatorSpec(token.LAND)) 87 | ObjectType.Def("||", logicalOperatorSpec(token.LOR)) 88 | 89 | ObjectType.Def("is_a?", MethodSpec{ 90 | ReturnType: func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 91 | return BoolType, nil 92 | }, 93 | // skip all iteration in target source and just expand 94 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 95 | var isA bool 96 | targetClass := args[0].Type 97 | class, _ := ClassRegistry.Get(rcvr.Type.ClassName()) 98 | for class != nil { 99 | if class == targetClass { 100 | isA = true 101 | break 102 | } 103 | class = class.parent 104 | } 105 | return Transform{ 106 | Expr: it.Get(strconv.FormatBool(isA)), 107 | } 108 | }, 109 | }) 110 | ObjectType.Def("instance_of?", MethodSpec{ 111 | ReturnType: func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 112 | return BoolType, nil 113 | }, 114 | // skip all iteration in target source and just expand 115 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 116 | var isInstance bool 117 | targetClass := args[0].Type 118 | if class, ok := targetClass.(*Class); ok { 119 | if rcvr.Type == class.Instance.(Type) { 120 | isInstance = true 121 | } 122 | } 123 | return Transform{ 124 | Expr: it.Get(strconv.FormatBool(isInstance)), 125 | } 126 | }, 127 | }) 128 | ObjectClass.Def("instance_methods", MethodSpec{ 129 | ReturnType: func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 130 | return NewArray(SymbolType), nil 131 | }, 132 | // skip all iteration in target source and just expand 133 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 134 | instanceType := rcvr.Type.(*Class).Instance.(Type) 135 | return instanceType.TransformAST("methods", rcvr.Expr, args, blk, it) 136 | }, 137 | }) 138 | 139 | ObjectType.Def("methods", MethodSpec{ 140 | ReturnType: func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 141 | return NewArray(SymbolType), nil 142 | }, 143 | // skip all iteration in target source and just expand 144 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 145 | names := []ast.Expr{} 146 | class, err := ClassRegistry.Get(rcvr.Type.ClassName()) 147 | if err != nil { 148 | panic(err) 149 | } 150 | methods := map[string]bool{} 151 | for class != nil { 152 | for k := range class.Instance.Methods() { 153 | methods[k] = true 154 | } 155 | class = class.parent 156 | } 157 | for k := range methods { 158 | names = append(names, bst.String(k)) 159 | } 160 | return Transform{ 161 | Expr: &ast.CompositeLit{ 162 | Type: &ast.ArrayType{ 163 | Elt: it.Get("string"), 164 | }, 165 | Elts: names, 166 | }, 167 | } 168 | }, 169 | }) 170 | 171 | // Deprecated and unsupported methods, or general uselessness 172 | noops := []string{"taint", "untaint", "trust", "untrust", "itself"} 173 | for _, noop := range noops { 174 | ObjectType.Def(noop, NoopReturnSelf) 175 | } 176 | 177 | ObjectType.Def("tainted?", AlwaysFalse) 178 | ObjectType.Def("untrusted?", AlwaysFalse) 179 | } 180 | -------------------------------------------------------------------------------- /types/numeric.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | 7 | "github.com/redneckbeard/thanos/bst" 8 | ) 9 | 10 | type Numeric struct { 11 | *proto 12 | } 13 | 14 | var NumericType = Numeric{newProto("Numeric", "Object", ClassRegistry)} 15 | 16 | var NumericClass = NewClass("Numeric", "Object", NumericType, ClassRegistry) 17 | 18 | func (t Numeric) Equals(t2 Type) bool { return t == t2 } 19 | func (t Numeric) String() string { return "NumericType" } 20 | func (t Numeric) GoType() string { return "int" } 21 | func (t Numeric) IsComposite() bool { return false } 22 | 23 | func (t Numeric) MethodReturnType(m string, b Type, args []Type) (Type, error) { 24 | return t.MustResolve(m).ReturnType(t, b, args) 25 | } 26 | 27 | //TODO we don't need this in the interface. Instead, the parser or compiler should retrieve the MethodSpec and check for a not-nil blockArgs (which will then need to be exported 28 | func (t Numeric) BlockArgTypes(m string, args []Type) []Type { 29 | return t.MustResolve(m).blockArgs(t, args) 30 | } 31 | 32 | func (t Numeric) TransformAST(m string, rcvr ast.Expr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 33 | return t.MustResolve(m).TransformAST(TypeExpr{t, rcvr}, args, blk, it) 34 | } 35 | 36 | func (t Numeric) Resolve(m string) (MethodSpec, bool) { 37 | return t.proto.Resolve(m, false) 38 | } 39 | 40 | func (t Numeric) MustResolve(m string) MethodSpec { 41 | return t.proto.MustResolve(m, false) 42 | } 43 | 44 | func (t Numeric) HasMethod(m string) bool { 45 | return t.proto.HasMethod(m, false) 46 | } 47 | 48 | func (t Numeric) Alias(existingMethod, newMethod string) { 49 | t.proto.MakeAlias(existingMethod, newMethod, false) 50 | } 51 | 52 | func numericOperatorSpec(tok token.Token, comparison bool) MethodSpec { 53 | return MethodSpec{ 54 | ReturnType: func(rcvr Type, blockReturnType Type, args []Type) (Type, error) { 55 | if comparison { 56 | return BoolType, nil 57 | } 58 | if rcvr == FloatType || args[0] == FloatType { 59 | return FloatType, nil 60 | } 61 | return IntType, nil 62 | }, 63 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 64 | leftExpr, rightExpr := rcvr.Expr, args[0].Expr 65 | if rcvr.Type == FloatType && args[0].Type == IntType { 66 | if _, ok := rightExpr.(*ast.BasicLit); !ok { 67 | rightExpr = bst.Call(nil, "float64", rightExpr) 68 | } 69 | } else if rcvr.Type == IntType && args[0].Type == FloatType { 70 | if _, ok := leftExpr.(*ast.BasicLit); !ok { 71 | leftExpr = bst.Call(nil, "float64", leftExpr) 72 | } 73 | } 74 | return Transform{ 75 | Expr: bst.Binary(leftExpr, tok, rightExpr), 76 | } 77 | }, 78 | } 79 | } 80 | 81 | func init() { 82 | NumericType.Def("+", numericOperatorSpec(token.ADD, false)) 83 | NumericType.Def("-", numericOperatorSpec(token.SUB, false)) 84 | NumericType.Def("*", numericOperatorSpec(token.MUL, false)) 85 | NumericType.Def("/", numericOperatorSpec(token.QUO, false)) 86 | NumericType.Def("%", numericOperatorSpec(token.REM, false)) 87 | NumericType.Def("<", numericOperatorSpec(token.LSS, true)) 88 | NumericType.Def(">", numericOperatorSpec(token.GTR, true)) 89 | NumericType.Def("<=", numericOperatorSpec(token.LEQ, true)) 90 | NumericType.Def(">=", numericOperatorSpec(token.GEQ, true)) 91 | NumericType.Def("==", numericOperatorSpec(token.EQL, true)) 92 | NumericType.Def("!=", numericOperatorSpec(token.NEQ, true)) 93 | NumericType.Def("**", MethodSpec{ 94 | ReturnType: func(r Type, b Type, args []Type) (Type, error) { 95 | if r == IntType && args[0] == IntType { 96 | return IntType, nil 97 | } 98 | return FloatType, nil 99 | }, 100 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 101 | leftExpr, rightExpr := rcvr.Expr, args[0].Expr 102 | if _, ok := leftExpr.(*ast.BasicLit); !ok && rcvr.Type == IntType { 103 | leftExpr = bst.Call(nil, "float64", leftExpr) 104 | } 105 | if _, ok := rightExpr.(*ast.BasicLit); !ok && args[0].Type == IntType { 106 | rightExpr = bst.Call(nil, "float64", rightExpr) 107 | } 108 | expr := bst.Call("math", "Pow", leftExpr, rightExpr) 109 | if rcvr.Type == IntType && args[0].Type == IntType { 110 | expr = bst.Call(nil, "int", expr) 111 | } 112 | return Transform{ 113 | Expr: expr, 114 | } 115 | }, 116 | }) 117 | 118 | NumericType.Def("abs", MethodSpec{ 119 | ReturnType: func(r Type, b Type, args []Type) (Type, error) { 120 | return r, nil 121 | }, 122 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 123 | var arg ast.Expr 124 | if lit, ok := rcvr.Expr.(*ast.BasicLit); ok || rcvr.Type == FloatType { 125 | arg = lit 126 | } else { 127 | arg = bst.Call(nil, "float64", rcvr.Expr) 128 | } 129 | expr := bst.Call("math", "Abs", arg) 130 | if rcvr.Type == IntType { 131 | expr = bst.Call(nil, "int", expr) 132 | } 133 | return Transform{ 134 | Expr: expr, 135 | Imports: []string{"math"}, 136 | } 137 | }, 138 | }) 139 | NumericType.Def("negative?", MethodSpec{ 140 | ReturnType: func(r Type, b Type, args []Type) (Type, error) { 141 | return BoolType, nil 142 | }, 143 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 144 | return Transform{ 145 | Expr: bst.Binary(rcvr.Expr, token.LSS, bst.Int(0)), 146 | } 147 | }, 148 | }) 149 | NumericType.Def("positive?", MethodSpec{ 150 | ReturnType: func(r Type, b Type, args []Type) (Type, error) { 151 | return BoolType, nil 152 | }, 153 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 154 | return Transform{ 155 | Expr: bst.Binary(rcvr.Expr, token.GTR, bst.Int(0)), 156 | } 157 | }, 158 | }) 159 | NumericType.Def("zero?", MethodSpec{ 160 | ReturnType: func(r Type, b Type, args []Type) (Type, error) { 161 | return BoolType, nil 162 | }, 163 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 164 | return Transform{ 165 | Expr: bst.Binary(rcvr.Expr, token.EQL, bst.Int(0)), 166 | } 167 | }, 168 | }) 169 | } 170 | -------------------------------------------------------------------------------- /parser/string.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/redneckbeard/thanos/stdlib" 9 | "github.com/redneckbeard/thanos/types" 10 | ) 11 | 12 | type StringKind int 13 | 14 | const ( 15 | DoubleQuote StringKind = iota 16 | SingleQuote 17 | Regexp 18 | Words 19 | RawWords 20 | Exec 21 | RawExec 22 | ) 23 | 24 | func getStringKind(delim string) StringKind { 25 | switch delim { 26 | case "\"": 27 | return DoubleQuote 28 | case "'": 29 | return SingleQuote 30 | case "/": 31 | return Regexp 32 | case "`": 33 | return Exec 34 | } 35 | kind := delim[1:2] 36 | switch kind { 37 | case "w": 38 | return RawWords 39 | case "W": 40 | return Words 41 | case "x": 42 | return RawExec 43 | case "X": 44 | return Exec 45 | } 46 | panic("The lexer should have errored already") 47 | } 48 | 49 | var stringDelims = map[StringKind]string{ 50 | DoubleQuote: `"`, 51 | Words: `"`, 52 | SingleQuote: "'", 53 | RawWords: "'", 54 | Regexp: "/", 55 | RawExec: "`", 56 | Exec: "`", 57 | } 58 | 59 | var validEscapes = []rune{'a', 'b', 'f', 'n', 'r', 't', 'v', '\\'} 60 | 61 | type StringNode struct { 62 | BodySegments []string 63 | Interps map[int][]Node 64 | cached bool 65 | Kind StringKind 66 | lineNo int 67 | delim string 68 | _type types.Type 69 | } 70 | 71 | func (n *StringNode) OrderedInterps() []Node { 72 | positions := []int{} 73 | for k := range n.Interps { 74 | positions = append(positions, k) 75 | } 76 | sort.Ints(positions) 77 | nodes := []Node{} 78 | for _, i := range positions { 79 | interp := n.Interps[i] 80 | nodes = append(nodes, interp...) 81 | } 82 | return nodes 83 | } 84 | 85 | func (n *StringNode) GoString() string { 86 | switch n.Kind { 87 | case Regexp: 88 | return strings.ReplaceAll(n.FmtString("`"), "(?<", "(?P<") 89 | case SingleQuote, RawWords: 90 | return n.FmtString("`") 91 | default: 92 | return n.FmtString(`"`) 93 | } 94 | } 95 | 96 | func (n *StringNode) FmtString(delim string) string { 97 | if len(n.Interps) == 0 { 98 | if len(n.BodySegments) == 0 { 99 | return delim + delim 100 | } 101 | body, _ := n.TranslateEscapes(n.BodySegments[0]) 102 | return delim + body + delim 103 | } 104 | segments := "" 105 | for i, seg := range n.BodySegments { 106 | if interps, exists := n.Interps[i]; exists { 107 | for _, interp := range interps { 108 | verb := types.FprintVerb(interp.Type()) 109 | if verb == "" { 110 | panic(fmt.Sprintf("[line %d] Unhandled type inference failure for interpolated value in string", n.lineNo)) 111 | } 112 | segments += verb 113 | } 114 | } 115 | escaped, _ := n.TranslateEscapes(seg) 116 | segments += escaped 117 | } 118 | if trailingInterps, exists := n.Interps[len(n.BodySegments)]; exists { 119 | for _, trailingInterp := range trailingInterps { 120 | verb := types.FprintVerb(trailingInterp.Type()) 121 | if verb == "" { 122 | panic(fmt.Sprintf("[line %d] Unhandled type inference failure for interpolated value in string", n.lineNo)) 123 | } 124 | segments += verb 125 | } 126 | } 127 | return delim + segments + delim 128 | } 129 | 130 | func (n *StringNode) String() string { 131 | if len(n.OrderedInterps()) == 0 { 132 | str := n.FmtString(stringDelims[n.Kind]) 133 | if n.Kind == RawWords || n.Kind == Words { 134 | str = fmt.Sprintf("%%w[%s]", str) 135 | } 136 | return str 137 | } 138 | str := fmt.Sprintf(`%s %% (%s)`, n.FmtString(stringDelims[n.Kind]), stdlib.Join[Node](n.OrderedInterps(), ", ")) 139 | if n.Kind == RawWords || n.Kind == Words { 140 | return fmt.Sprintf("%%w[%s]", str) 141 | } 142 | return fmt.Sprintf("(%s)", str) 143 | } 144 | 145 | func (n *StringNode) Type() types.Type { 146 | return n._type 147 | } 148 | 149 | func (n *StringNode) SetType(t types.Type) { n._type = t } 150 | func (n *StringNode) LineNo() int { return n.lineNo } 151 | 152 | func (n *StringNode) TargetType(scope ScopeChain, class *Class) (types.Type, error) { 153 | if n.Kind != Regexp { 154 | for _, seg := range n.BodySegments { 155 | if _, err := n.TranslateEscapes(seg); err != nil { 156 | return nil, err 157 | } 158 | } 159 | } 160 | for _, interps := range n.Interps { 161 | for _, i := range interps { 162 | if t, err := GetType(i, scope, class); err != nil { 163 | if t == nil { 164 | return nil, NewParseError(n, "Could not infer type for interpolated value '%s'", i) 165 | } 166 | return nil, err 167 | } 168 | } 169 | } 170 | switch n.Kind { 171 | case Regexp: 172 | return types.RegexpType, nil 173 | case Words, RawWords: 174 | return types.NewArray(types.StringType), nil 175 | default: 176 | return types.StringType, nil 177 | } 178 | } 179 | 180 | func (n *StringNode) Copy() Node { return n } 181 | 182 | func (n *StringNode) TranslateEscapes(segment string) (string, error) { 183 | switch n.Kind { 184 | case SingleQuote, RawWords, RawExec: 185 | escapeless := strings.ReplaceAll(segment, `\`+n.delim, n.delim) 186 | return strings.ReplaceAll(escapeless, `\\`, `\`), nil 187 | case DoubleQuote, Words, Exec: 188 | var ( 189 | stripped []rune 190 | lastSeen rune 191 | ) 192 | escapes := make([]rune, len(validEscapes)+1) 193 | copy(escapes, validEscapes) 194 | escapes = append(escapes, []rune(n.delim)[0]) 195 | for _, r := range segment { 196 | if lastSeen == '\\' { 197 | for _, v := range escapes { 198 | if v == r { 199 | stripped = append(stripped, lastSeen) 200 | } 201 | } 202 | if r == 'e' || r == 's' { 203 | return "", NewParseError(n, `\%c is not a valid escape sequence in Go strings`, r) 204 | } 205 | if r == 'M' { 206 | return "", NewParseError(n, `\M-x, \M-\C-x, and \M-\cx are not valid escape sequences in Go strings`) 207 | } 208 | if r == 'c' || r == 'C' { 209 | return "", NewParseError(n, `\c\M-x, \c?, and \C? are not valid escape sequences in Go strings`) 210 | } 211 | } else if lastSeen != 0 { 212 | stripped = append(stripped, lastSeen) 213 | } 214 | lastSeen = r 215 | } 216 | stripped = append(stripped, lastSeen) 217 | return string(stripped), nil 218 | } 219 | return segment, nil 220 | } 221 | -------------------------------------------------------------------------------- /types/file.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "go/ast" 5 | "strings" 6 | 7 | "github.com/redneckbeard/thanos/bst" 8 | "github.com/redneckbeard/thanos/stdlib" 9 | ) 10 | 11 | var FileType = NewClass("File", "Object", nil, ClassRegistry) 12 | 13 | func init() { 14 | FileType.Instance.Def("initialize", MethodSpec{ 15 | ReturnType: func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 16 | return FileType.Instance.(Type), nil 17 | }, 18 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 19 | var call *ast.CallExpr 20 | imports := []string{"os"} 21 | switch len(args) { 22 | case 1: 23 | call = bst.Call("os", "Open", args[0].Expr) 24 | case 2: 25 | call = bst.Call("os", "OpenFile", args[0].Expr) 26 | if lit, ok := args[1].Expr.(*ast.BasicLit); ok { 27 | mode := strings.Trim(lit.Value, `"`) 28 | flag, ok := stdlib.OpenModes[mode] 29 | if !ok { 30 | panic("Invalid mode: " + mode) 31 | } 32 | call.Args = append(call.Args, bst.Int(flag)) 33 | } else { 34 | call.Args = append(call.Args, &ast.IndexExpr{ 35 | X: bst.Dot("stdlib", "OpenMode"), 36 | Index: args[1].Expr, 37 | }) 38 | imports = append(imports, "github.com/redneckbeard/thanos/stdlib") 39 | } 40 | call.Args = append(call.Args, bst.Int("0666")) 41 | case 3: 42 | panic("File.new does not yet support an options hash") 43 | } 44 | file := it.New("f") 45 | stmt := bst.Define([]ast.Expr{file, it.Get("_")}, call) 46 | return Transform{ 47 | Expr: file, 48 | Stmts: []ast.Stmt{stmt}, 49 | Imports: imports, 50 | } 51 | }, 52 | }) 53 | 54 | FileType.Instance.Def("each", MethodSpec{ 55 | ReturnType: func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 56 | return FileType, nil 57 | }, 58 | blockArgs: func(r Type, args []Type) []Type { 59 | return []Type{StringType} 60 | }, 61 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 62 | // Ruby's File#each doesn't strip line terminators by default; 63 | // bufio.Scanner does, and we have to do something to soak up the 64 | // mismatch. By relying here on a SplitFunc generator in stdlib, we 65 | // should be well positioned to support `chomp: true` when we get kwargs 66 | // support into the parser. Presently it will incorrectly work as 67 | // a positional arg. 68 | scanner := it.New("scanner") 69 | initScanner := bst.Define(scanner, bst.Call("bufio", "NewScanner", rcvr.Expr)) 70 | makeSplitFuncArgs := UnwrapTypeExprs(args) 71 | if len(makeSplitFuncArgs) == 0 { 72 | makeSplitFuncArgs = []ast.Expr{bst.String(`\n`), it.Get("false")} 73 | } 74 | setSplit := &ast.ExprStmt{ 75 | X: bst.Call(scanner, "Split", bst.Call("stdlib", "MakeSplitFunc", makeSplitFuncArgs...)), 76 | } 77 | lineDef := bst.Define(blk.Args, bst.Call(scanner, "Text")) 78 | loop := &ast.ForStmt{ 79 | Cond: bst.Call(scanner, "Scan"), 80 | Body: &ast.BlockStmt{ 81 | List: append([]ast.Stmt{lineDef}, blk.Statements...), 82 | }, 83 | } 84 | return Transform{ 85 | Expr: rcvr.Expr, 86 | Stmts: []ast.Stmt{initScanner, setSplit, loop}, 87 | Imports: []string{"os", "bufio", "github.com/redneckbeard/thanos/stdlib"}, 88 | } 89 | }, 90 | }) 91 | FileType.Instance.Alias("each", "each_line") 92 | 93 | FileType.Instance.Def("close", MethodSpec{ 94 | ReturnType: func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 95 | return NilType, nil 96 | }, 97 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 98 | return Transform{ 99 | Expr: bst.Call(rcvr.Expr, "Close"), 100 | } 101 | }, 102 | }) 103 | 104 | FileType.Instance.Def("size", MethodSpec{ 105 | ReturnType: func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 106 | return IntType, nil 107 | }, 108 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 109 | info := it.New("info") 110 | infoStmt := bst.Define([]ast.Expr{info, it.Get("_")}, bst.Call(rcvr.Expr, "Info")) 111 | return Transform{ 112 | Stmts: []ast.Stmt{infoStmt}, 113 | Expr: bst.Call(info, "Size"), 114 | } 115 | }, 116 | }) 117 | 118 | FileType.Def("open", MethodSpec{ 119 | ReturnType: func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 120 | if blockReturnType != nil { 121 | return blockReturnType, nil 122 | } 123 | return FileType.Instance.(Type), nil 124 | }, 125 | blockArgs: func(r Type, args []Type) []Type { 126 | return []Type{FileType.Instance.(Type)} 127 | }, 128 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 129 | newFile := rcvr.Type.TransformAST("new", rcvr.Expr, args, blk, it) 130 | if blk == nil { 131 | return newFile 132 | } 133 | // unlike a block that translates to a loop or other block-scoped construct, File.open solely 134 | // provides the convenience of not having to remember to close the file. Thus, we must set the 135 | // identifier provided with the block to be the one we returned when defining the file variable. 136 | f := newFile.Expr.(*ast.Ident) 137 | blockArg := blk.Args[0].(*ast.Ident) 138 | blockArg.Name = f.Name 139 | 140 | closeFile := FileType.Instance.MustResolve("close").TransformAST(TypeExpr{rcvr.Type, newFile.Expr}, []TypeExpr{}, nil, it) 141 | 142 | final := it.New("result") 143 | finalStmt := blk.Statements[len(blk.Statements)-1].(*ast.ReturnStmt) 144 | blk.Statements[len(blk.Statements)-1] = bst.Define(final, finalStmt.Results) 145 | 146 | stmts := append(newFile.Stmts, blk.Statements...) 147 | stmts = append(stmts, &ast.ExprStmt{X: closeFile.Expr}) 148 | return Transform{ 149 | Stmts: stmts, 150 | Expr: final, 151 | Imports: append(newFile.Imports, closeFile.Imports...), 152 | } 153 | }, 154 | }) 155 | 156 | FileType.Instance.Def("<<", MethodSpec{ 157 | ReturnType: func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 158 | return receiverType, nil 159 | }, 160 | TransformAST: func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 161 | return Transform{ 162 | Expr: bst.Call(rcvr.Expr, "WriteString", args[0].Expr), 163 | } 164 | }, 165 | }) 166 | } 167 | -------------------------------------------------------------------------------- /parser/assignment.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/redneckbeard/thanos/stdlib" 8 | "github.com/redneckbeard/thanos/types" 9 | ) 10 | 11 | type AssignmentNode struct { 12 | Left []Node 13 | Right []Node 14 | Reassignment bool 15 | OpAssignment bool 16 | SetterCall bool 17 | lineNo int 18 | _type types.Type 19 | } 20 | 21 | func (n *AssignmentNode) String() string { 22 | sides := []interface{}{} 23 | for _, side := range [][]Node{n.Left, n.Right} { 24 | var s string 25 | if len(side) > 1 { 26 | s = fmt.Sprintf("(%s)", stdlib.Join[Node](side, ", ")) 27 | } else { 28 | s = side[0].String() 29 | } 30 | sides = append(sides, s) 31 | } 32 | return fmt.Sprintf("(%s = %s)", sides...) 33 | } 34 | func (n *AssignmentNode) Type() types.Type { return n._type } 35 | func (n *AssignmentNode) SetType(t types.Type) { n._type = t } 36 | func (n *AssignmentNode) LineNo() int { return n.lineNo } 37 | 38 | func (n *AssignmentNode) TargetType(scope ScopeChain, class *Class) (types.Type, error) { 39 | var typelist []types.Type 40 | for i, left := range n.Left { 41 | var localName string 42 | switch lhs := left.(type) { 43 | case *IdentNode: 44 | localName = lhs.Val 45 | GetType(lhs, scope, class) 46 | case *BracketAssignmentNode: 47 | if ident, ok := lhs.Composite.(*IdentNode); ok { 48 | localName = ident.Val 49 | } 50 | case *IVarNode: 51 | GetType(lhs, scope, class) 52 | case *ConstantNode: 53 | localName = lhs.Val 54 | case *MethodCall: 55 | if lhs.Receiver == nil { 56 | panic("The first pass through parsing should never result in a receiverless LHS method call, but somehow we got here") 57 | } 58 | case *SplatNode: 59 | if ident, ok := lhs.Arg.(*IdentNode); ok { 60 | localName = ident.Val 61 | } 62 | default: 63 | return nil, NewParseError(lhs, "%s not yet supported in LHS of assignments", lhs) 64 | } 65 | var ( 66 | assignedType types.Type 67 | err error 68 | ) 69 | if n.OpAssignment { 70 | // operator assignments are always 1:1, so nothing to handle here for multiple lhs or rhs 71 | assignedType, err = GetType(n.Right[i].(*InfixExpressionNode).Right, scope, class) 72 | } else { 73 | switch { 74 | case len(n.Left) > len(n.Right): 75 | /* 76 | 77 | There are two valid scenarios here: unpacking of an array into 78 | locals, and assigning from a method that returns a tuple. Note that 79 | Ruby's behavior in the event of a length mismatch of the two sides is 80 | to drop the excess values if lhs is shorter than rhs, and to populate 81 | excess identifiers on lhs with nil of lhs is longer than rhs. There 82 | is also the perfectly legal option of assigning a single value that 83 | cannot be deconstructed to multiple variables, which leaves all but 84 | the first as nil. 85 | 86 | */ 87 | var rightIndex int 88 | if i >= len(n.Right) { 89 | rightIndex = len(n.Right) - 1 90 | } else { 91 | rightIndex = i 92 | } 93 | t, err := GetType(n.Right[rightIndex], scope, class) 94 | if err != nil { 95 | return nil, NewParseError(n, err.Error()) 96 | } 97 | switch rt := t.(type) { 98 | case types.Multiple: 99 | assignedType = rt[i] 100 | case types.Array: 101 | if _, ok := n.Left[i].(*SplatNode); ok { 102 | assignedType = rt 103 | } else { 104 | assignedType = rt.Element 105 | } 106 | default: 107 | if i > rightIndex { 108 | assignedType = types.NilType 109 | } else { 110 | assignedType = t 111 | } 112 | } 113 | case len(n.Left) == len(n.Right): 114 | assignedType, err = GetType(n.Right[i], scope, class) 115 | case len(n.Left) < len(n.Right): 116 | // If there's only one lhs element, this is an implicit Array, and 117 | // needs to get type checked. Otherwise, as discussed above, we throw 118 | // away any rhs values beyond the length of lhs. 119 | if len(n.Left) == 1 { 120 | array := &ArrayNode{Args: ArgsNode(n.Right), lineNo: n.Right[0].LineNo()} 121 | if at, err := GetType(array, scope, class); err != nil { 122 | return nil, err 123 | } else { 124 | n.Right = []Node{array} 125 | assignedType = at 126 | } 127 | } else { 128 | assignedType, err = GetType(n.Right[i], scope, class) 129 | } 130 | } 131 | } 132 | if err != nil { 133 | return nil, err 134 | } 135 | switch lft := left.(type) { 136 | case *IVarNode: 137 | lft.SetType(assignedType) 138 | n.Reassignment = true 139 | if class != nil { 140 | ivar := &IVar{_type: assignedType} 141 | if err = class.AddIVar(lft.NormalizedVal(), ivar); err != nil { 142 | return nil, NewParseError(n, err.Error()) 143 | } 144 | } 145 | case *ConstantNode: 146 | lft.SetType(assignedType) 147 | if scope.Current().TakesConstants() { 148 | constant := &Constant{name: lft.Val, prefix: scope.Prefix()} 149 | constant._type = assignedType 150 | GetType(left, scope, class) 151 | constant.Val = n.Right[i] 152 | scope.Current().(ConstantScope).AddConstant(constant) 153 | } else { 154 | scope.Set(localName, &RubyLocal{_type: assignedType}) 155 | } 156 | case *MethodCall: 157 | // we should only ever hit this branch for a setter, and thus we have to 158 | // munge the call to reflect what's actually happening. 159 | if !strings.HasSuffix(lft.MethodName, "=") { 160 | lft.MethodName += "=" 161 | } 162 | lft.Args = []Node{n.Right[i]} 163 | if _, err := GetType(lft, scope, class); err != nil { 164 | return nil, err 165 | } 166 | n.SetterCall = true 167 | default: 168 | local := scope.ResolveVar(localName) 169 | if _, ok := local.(*IVar); ok || local == BadLocal { 170 | scope.Set(localName, &RubyLocal{_type: assignedType}) 171 | } else { 172 | if local.Type() == nil { 173 | loc := local.(*RubyLocal) 174 | loc.SetType(assignedType) 175 | } else { 176 | n.Reassignment = true 177 | } 178 | if local.Type() != assignedType { 179 | if arr, ok := local.Type().(types.Array); ok { 180 | if arr.Element != assignedType { 181 | return nil, NewParseError(n, "Attempted to assign %s member to %s", assignedType, arr) 182 | } 183 | } else { 184 | return nil, NewParseError(n, "tried assigning type %s to local %s in scope %s but had previously assigned type %s", assignedType, localName, scope.Name(), local.Type()) 185 | } 186 | } 187 | } 188 | } 189 | typelist = append(typelist, assignedType) 190 | } 191 | if len(typelist) > 1 { 192 | return types.Multiple(typelist), nil 193 | } 194 | return typelist[0], nil 195 | } 196 | 197 | func (n *AssignmentNode) Copy() Node { 198 | return &AssignmentNode{ 199 | Left: n.Left, 200 | Right: n.Right, 201 | Reassignment: n.Reassignment, 202 | OpAssignment: n.OpAssignment, 203 | lineNo: n.lineNo, 204 | _type: n._type, 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /parser/statements.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/redneckbeard/thanos/stdlib" 7 | "github.com/redneckbeard/thanos/types" 8 | ) 9 | 10 | type ArgsNode []Node 11 | 12 | func (n ArgsNode) String() string { 13 | return stdlib.Join[Node](n, ", ") 14 | } 15 | 16 | // Wrong but dummy for satisfying interface 17 | func (n ArgsNode) Type() types.Type { return n[0].Type() } 18 | func (n ArgsNode) SetType(t types.Type) {} 19 | func (n ArgsNode) LineNo() int { return 0 } 20 | 21 | func (n ArgsNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 22 | panic("ArgsNode#TargetType should never be called") 23 | } 24 | 25 | func (n ArgsNode) Copy() Node { 26 | var copy []Node 27 | for _, arg := range n { 28 | copy = append(copy, arg.Copy()) 29 | } 30 | return ArgsNode(copy) 31 | } 32 | 33 | func (n ArgsNode) FindByName(name string) (Node, error) { 34 | for _, arg := range n { 35 | if kv, ok := arg.(*KeyValuePair); ok && kv.Label == name { 36 | return kv, nil 37 | } 38 | } 39 | return nil, fmt.Errorf("No argument named '%s' found", name) 40 | } 41 | 42 | type ReturnNode struct { 43 | Val ArgsNode 44 | _type types.Type 45 | lineNo int 46 | } 47 | 48 | func (n *ReturnNode) String() string { return fmt.Sprintf("(return %s)", n.Val) } 49 | func (n *ReturnNode) Type() types.Type { return n._type } 50 | func (n *ReturnNode) SetType(t types.Type) { n._type = t } 51 | func (n *ReturnNode) LineNo() int { return n.lineNo } 52 | 53 | func (n *ReturnNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 54 | if len(n.Val) == 1 { 55 | return GetType(n.Val[0], locals, class) 56 | } 57 | multiple := types.Multiple{} 58 | for _, single := range n.Val { 59 | t, err := GetType(single, locals, class) 60 | if err != nil { 61 | return t, err 62 | } 63 | multiple = append(multiple, t) 64 | } 65 | return multiple, nil 66 | } 67 | 68 | func (n *ReturnNode) Copy() Node { 69 | return &ReturnNode{n.Val.Copy().(ArgsNode), n._type, n.lineNo} 70 | } 71 | 72 | type Statements []Node 73 | 74 | func (stmts Statements) TargetType(scope ScopeChain, class *Class) (types.Type, error) { 75 | var lastReturnedType types.Type 76 | for _, stmt := range stmts { 77 | switch s := stmt.(type) { 78 | case *AssignmentNode: 79 | if t, err := GetType(s, scope, class); err != nil { 80 | return nil, err 81 | } else { 82 | lastReturnedType = t 83 | } 84 | case *Condition: 85 | // We need this to be semi-"live" since otherwise we can't surface an 86 | // error about a type mismatch between the branches. The type on the 87 | // condition will still be effectively memoized since it can just get the 88 | // cached value from the True side. Thus we call TargetType directly on 89 | // the node instead of going through GetType. 90 | if t, err := GetType(s, scope, class); err != nil { 91 | return nil, err 92 | } else { 93 | lastReturnedType = t 94 | } 95 | case *IVarNode: 96 | if t, err := GetType(s, scope, class); err != nil { 97 | return nil, err 98 | } else { 99 | lastReturnedType = t 100 | } 101 | default: 102 | if c, ok := stmt.(*MethodCall); ok { 103 | // Handle method chaining -- walk down to the first identifier or 104 | // literal and infer types on the way back up so that receiver type is 105 | // known for each subsequent method call 106 | chain := []Node{c} 107 | r := c.Receiver 108 | walking := true 109 | for walking { 110 | switch c := r.(type) { 111 | case *MethodCall: 112 | chain = append(chain, c) 113 | r = c.Receiver 114 | default: 115 | if c != nil { 116 | chain = append(chain, c) 117 | } 118 | walking = false 119 | } 120 | } 121 | for i := len(chain) - 1; i >= 0; i-- { 122 | if t, err := GetType(chain[i], scope, class); err != nil { 123 | return nil, err 124 | } else { 125 | lastReturnedType = t 126 | } 127 | } 128 | } else if t, err := GetType(stmt, scope, class); err != nil { 129 | return nil, err 130 | } else { 131 | lastReturnedType = t 132 | } 133 | } 134 | } 135 | return lastReturnedType, nil 136 | } 137 | 138 | func (stmts Statements) String() string { 139 | switch len(stmts) { 140 | case 0: 141 | return "" 142 | case 1: 143 | return stmts[0].String() 144 | default: 145 | return fmt.Sprintf("%s", stdlib.Join[Node](stmts, "\n")) 146 | } 147 | } 148 | 149 | func (stmts Statements) Type() types.Type { return nil } 150 | func (stmts Statements) SetType(t types.Type) {} 151 | func (stmts Statements) LineNo() int { return 0 } 152 | 153 | func (stmts Statements) Copy() Node { 154 | var copy []Node 155 | for _, stmt := range stmts { 156 | copy = append(copy, stmt.Copy()) 157 | } 158 | return Statements(stmts) 159 | } 160 | 161 | type Body struct { 162 | Statements Statements 163 | ReturnType types.Type 164 | ExplicitReturns []*ReturnNode 165 | } 166 | 167 | func (b *Body) InferReturnType(scope ScopeChain, class *Class) error { 168 | // To guess the right return type of a method, we have to: 169 | 170 | // 1) track all return statements in the method body; 171 | 172 | // 2) chase expressions all the way to the end of the body and wrap that 173 | // last expr in a return node if it's not already there, wherein we record 174 | // the types of all assignments in a map on the method. 175 | 176 | // Achieving 1) would mean rewalking this branch of the AST right after 177 | // building it which seems dumb, so instead we register each ReturnNode on 178 | // the method as the parser encounters them so we can loop through them 179 | // afterward when m.Locals is fully populated. 180 | 181 | lastReturnedType, err := GetType(b.Statements, scope, class) 182 | if err != nil { 183 | return err 184 | } 185 | finalStatementIdx := len(b.Statements) - 1 186 | finalStatement := b.Statements[finalStatementIdx] 187 | switch s := finalStatement.(type) { 188 | case *ReturnNode: 189 | case *AssignmentNode: 190 | var ret *ReturnNode 191 | if s.OpAssignment { 192 | ret = &ReturnNode{Val: s.Left} 193 | } else if _, ok := s.Left[0].(*IVarNode); ok { 194 | ret = &ReturnNode{Val: s.Left} 195 | } else { 196 | ret = &ReturnNode{Val: []Node{s.Right[0]}} 197 | } 198 | if _, err := GetType(ret, scope, class); err != nil { 199 | return err 200 | } 201 | b.Statements = append(b.Statements, ret) 202 | default: 203 | if finalStatement.Type() != types.NilType && scope.Name() != Main { 204 | ret := &ReturnNode{Val: []Node{finalStatement}} 205 | if _, err := GetType(ret, scope, class); err != nil { 206 | return err 207 | } 208 | b.Statements[finalStatementIdx] = ret 209 | } 210 | } 211 | if len(b.ExplicitReturns) > 0 { 212 | for _, r := range b.ExplicitReturns { 213 | t, _ := GetType(r, scope, class) 214 | if !t.Equals(lastReturnedType) { 215 | return NewParseError(r, "Detected conflicting return types %s and %s in method '%s'", lastReturnedType, t, scope.Name()) 216 | } 217 | } 218 | } 219 | b.ReturnType = lastReturnedType 220 | return nil 221 | } 222 | 223 | func (n *Body) String() string { 224 | return n.Statements.String() 225 | } 226 | -------------------------------------------------------------------------------- /parser/composites.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/redneckbeard/thanos/stdlib" 7 | "github.com/redneckbeard/thanos/types" 8 | ) 9 | 10 | type ArrayNode struct { 11 | Args ArgsNode 12 | _type types.Type 13 | lineNo int 14 | } 15 | 16 | func (n *ArrayNode) String() string { return fmt.Sprintf("[%s]", n.Args) } 17 | func (n *ArrayNode) Type() types.Type { return n._type } 18 | func (n *ArrayNode) SetType(t types.Type) { n._type = t } 19 | func (n *ArrayNode) LineNo() int { return n.lineNo } 20 | 21 | func (n *ArrayNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 22 | var inner types.Type 23 | for _, a := range n.Args { 24 | ta, _ := GetType(a, locals, class) 25 | if splat, ok := a.(*SplatNode); ok { 26 | ta = splat.Type().(types.Array).Element 27 | } 28 | if inner != nil && ta != inner { 29 | return nil, NewParseError(n, "Heterogenous array membership detected adding %s", ta) 30 | } else { 31 | inner = ta 32 | } 33 | } 34 | if inner == nil { 35 | if len(n.Args) == 0 { 36 | inner = types.AnyType 37 | } else { 38 | return nil, NewParseError(n, "No inner array type detected") 39 | } 40 | } 41 | return types.NewArray(inner), nil 42 | } 43 | 44 | func (n *ArrayNode) Copy() Node { 45 | return &ArrayNode{n.Args.Copy().(ArgsNode), n._type, n.lineNo} 46 | } 47 | 48 | type KeyValuePair struct { 49 | Key Node 50 | Label string 51 | Value Node 52 | DoubleSplat bool 53 | _type types.Type 54 | lineNo int 55 | } 56 | 57 | func (n *KeyValuePair) String() string { 58 | if n.DoubleSplat { 59 | return fmt.Sprintf("**%s", n.Value) 60 | } 61 | return fmt.Sprintf("%s => %s", n.Key, n.Value) 62 | } 63 | func (n *KeyValuePair) Type() types.Type { return n._type } 64 | func (n *KeyValuePair) SetType(t types.Type) { n._type = n.Value.Type() } 65 | func (n *KeyValuePair) LineNo() int { return n.lineNo } 66 | 67 | func (n *KeyValuePair) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 68 | return GetType(n.Value, locals, class) 69 | } 70 | 71 | func (n KeyValuePair) Copy() Node { 72 | kv := &KeyValuePair{ 73 | Label: n.Label, 74 | Value: n.Value.Copy(), 75 | DoubleSplat: n.DoubleSplat, 76 | _type: n._type, 77 | lineNo: n.lineNo, 78 | } 79 | if n.Key != nil { 80 | n.Key = n.Key.Copy() 81 | } 82 | return kv 83 | } 84 | 85 | type HashNode struct { 86 | Pairs []*KeyValuePair 87 | _type types.Type 88 | lineNo int 89 | } 90 | 91 | func (n *HashNode) String() string { 92 | return fmt.Sprintf("{%s}", stdlib.Join[*KeyValuePair](n.Pairs, ", ")) 93 | } 94 | func (n *HashNode) Type() types.Type { return n._type } 95 | func (n *HashNode) SetType(t types.Type) { n._type = t } 96 | func (n *HashNode) LineNo() int { return n.lineNo } 97 | 98 | func (n *HashNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 99 | var keyType, valueType types.Type 100 | for _, kv := range n.Pairs { 101 | if kv.Label != "" { 102 | keyType = types.SymbolType 103 | } else { 104 | tk, _ := GetType(kv.Key, locals, class) 105 | if keyType != nil && keyType != tk { 106 | return nil, fmt.Errorf("Heterogenous hash key membership detected adding %s", tk) 107 | } else { 108 | keyType = tk 109 | } 110 | } 111 | tv, _ := GetType(kv.Value, locals, class) 112 | if valueType != nil && valueType != tv { 113 | return nil, fmt.Errorf("Heterogenous hash value membership detected adding %s", tv) 114 | } else { 115 | valueType = tv 116 | } 117 | } 118 | return types.NewHash(keyType, valueType), nil 119 | } 120 | 121 | func (n *HashNode) Copy() Node { 122 | hash := &HashNode{_type: n._type, lineNo: n.lineNo} 123 | var pairs []*KeyValuePair 124 | for _, pair := range n.Pairs { 125 | pairs = append(pairs, pair.Copy().(*KeyValuePair)) 126 | } 127 | hash.Pairs = pairs 128 | return hash 129 | } 130 | 131 | func (n *HashNode) Merge(other *HashNode) { 132 | n.Pairs = append(n.Pairs, other.Pairs...) 133 | } 134 | 135 | func (n *HashNode) Delete(key string) { 136 | for i := len(n.Pairs) - 1; i >= 0; i-- { 137 | if n.Pairs[i].Label == key { 138 | n.Pairs = append(n.Pairs[0:i], n.Pairs[i+1:len(n.Pairs)]...) 139 | } 140 | } 141 | } 142 | 143 | type BracketAssignmentNode struct { 144 | Composite Node 145 | Args ArgsNode 146 | lineNo int 147 | _type types.Type 148 | } 149 | 150 | func (n *BracketAssignmentNode) String() string { return fmt.Sprintf("%s[%s]", n.Composite, n.Args) } 151 | func (n *BracketAssignmentNode) Type() types.Type { return n._type } 152 | func (n *BracketAssignmentNode) SetType(t types.Type) { n._type = t } 153 | func (n *BracketAssignmentNode) LineNo() int { return n.lineNo } 154 | 155 | func (n *BracketAssignmentNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 156 | return GetType(n.Composite, locals, class) 157 | } 158 | 159 | func (n *BracketAssignmentNode) Copy() Node { 160 | return &BracketAssignmentNode{n.Composite.Copy(), n.Args.Copy().(ArgsNode), n.lineNo, n._type} 161 | } 162 | 163 | type BracketAccessNode struct { 164 | Composite Node 165 | Args ArgsNode 166 | lineNo int 167 | _type types.Type 168 | } 169 | 170 | func (n *BracketAccessNode) String() string { return fmt.Sprintf("%s[%s]", n.Composite, n.Args) } 171 | func (n *BracketAccessNode) Type() types.Type { return n._type } 172 | func (n *BracketAccessNode) SetType(t types.Type) { n._type = t } 173 | func (n *BracketAccessNode) LineNo() int { return n.lineNo } 174 | 175 | func (n *BracketAccessNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 176 | t, err := GetType(n.Composite, locals, class) 177 | if err != nil { 178 | return nil, err 179 | } 180 | switch comp := t.(type) { 181 | case nil: 182 | return nil, fmt.Errorf("Type not inferred") 183 | case types.Array: 184 | if r, ok := n.Args[0].(*RangeNode); ok { 185 | if _, err = GetType(r, locals, class); err != nil { 186 | return nil, err 187 | } 188 | return t, nil 189 | } 190 | return comp.Element, nil 191 | case types.Hash: 192 | return comp.Value, nil 193 | case types.String: 194 | return types.StringType, nil 195 | default: 196 | arg := n.Args[0] 197 | if _, err := GetType(arg, locals, class); err != nil { 198 | return nil, err 199 | } 200 | if t.HasMethod("[]") { 201 | if t, err := t.MethodReturnType("[]", nil, []types.Type{arg.Type()}); err != nil { 202 | return nil, NewParseError(n, err.Error()) 203 | } else { 204 | return t, nil 205 | } 206 | } 207 | return t, NewParseError(n, "%s is not a supported type for bracket access", t) 208 | } 209 | } 210 | 211 | func (n *BracketAccessNode) Copy() Node { 212 | return &BracketAccessNode{n.Composite.Copy(), n.Args.Copy().(ArgsNode), n.lineNo, n._type} 213 | } 214 | 215 | type SplatNode struct { 216 | Arg Node 217 | _type types.Type 218 | } 219 | 220 | func (n *SplatNode) String() string { return "*" + n.Arg.String() } 221 | func (n *SplatNode) Type() types.Type { return n._type } 222 | func (n *SplatNode) SetType(t types.Type) { n._type = t } 223 | func (n *SplatNode) LineNo() int { return n.Arg.LineNo() } 224 | 225 | func (n *SplatNode) TargetType(locals ScopeChain, class *Class) (types.Type, error) { 226 | t, err := GetType(n.Arg, locals, class) 227 | if err != nil { 228 | return nil, err 229 | } 230 | if _, ok := t.(types.Array); !ok { 231 | return nil, NewParseError(n, "tried to splat '%s' but is not an array", n.Arg).Terminal() 232 | } 233 | return t, nil 234 | } 235 | 236 | func (n *SplatNode) Copy() Node { 237 | return &SplatNode{n.Arg, n._type} 238 | } 239 | -------------------------------------------------------------------------------- /types/proto.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "reflect" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/redneckbeard/thanos/bst" 11 | ) 12 | 13 | // Holds data for methods implemented "natively" in Thanos, initially targeting 14 | // built-in methods on native Ruby datastructures and types. This of course 15 | // means all the stuff that comes in Enumerable, which involves doing things 16 | // with blocks that in theory _could_ be done with anonymous functions in Go 17 | // but translates most efficiently and idiomatically to simple for loops. 18 | type MethodSpec struct { 19 | // When inferring the return types for methods that take a block, we must 20 | // consider the return type of the block (since blocks cannot explicitly 21 | // return, this means resolving the type of the last expression in the block 22 | // -- no plans to support `break` or `next` yet as I'm honestly unsure if 23 | // I've ever seen them in the wild) and the receiver, since in a composite 24 | // type the inner type will determine or factor into the return type. Args 25 | // are often not given with Enumerable but when they are that can determine 26 | // the return type. 27 | ReturnType ReturnTypeFunc 28 | // For any methods where we're creating a MethodSpec, we don't have the 29 | // implementation to examine in Ruby and see what types of args get passed to 30 | // `block.call`. Therefore we must first provide a way to compute what the 31 | // types of the args will be so that we can use them to seed inference of the 32 | // return type of the block. 33 | blockArgs func(Type, []Type) []Type 34 | TransformAST TransformFunc 35 | } 36 | 37 | type ReturnTypeFunc func(receiverType Type, blockReturnType Type, args []Type) (Type, error) 38 | type TransformFunc func(TypeExpr, []TypeExpr, *Block, bst.IdentTracker) Transform 39 | 40 | type Transform struct { 41 | Stmts []ast.Stmt 42 | Expr ast.Expr 43 | Imports []string 44 | } 45 | 46 | type proto struct { 47 | class, parent string 48 | methods map[string]MethodSpec 49 | bracketAliases map[Type]string 50 | registry *classRegistry 51 | initialized bool 52 | _type Type 53 | } 54 | 55 | func newProto(class, parent string, registry *classRegistry) *proto { 56 | p := &proto{ 57 | class: class, 58 | parent: parent, 59 | methods: make(map[string]MethodSpec), 60 | bracketAliases: make(map[Type]string), 61 | registry: registry, 62 | } 63 | return p 64 | } 65 | 66 | func (p *proto) ClassName() string { return p.class } 67 | func (p *proto) UserDefined() bool { return false } 68 | 69 | func (p *proto) Methods() map[string]MethodSpec { return p.methods } 70 | 71 | func (p *proto) SelfDef(m string, spec MethodSpec) { 72 | go func() { 73 | for { 74 | class, err := p.registry.Get(p.class) 75 | if err == nil { 76 | class.Def(m, spec) 77 | break 78 | } 79 | } 80 | }() 81 | } 82 | 83 | func (p *proto) Resolve(m string, classMethod bool) (MethodSpec, bool) { 84 | method, has := p.methods[m] 85 | if !has { 86 | className := p.class 87 | if className == "" { 88 | className = "Object" 89 | } 90 | if p.registry == nil { 91 | panic("tried to Resolve method but registry not set") 92 | } 93 | class, err := p.registry.Get(className) 94 | if err != nil { 95 | panic(err) 96 | } 97 | for class.parent != nil { 98 | class = class.parent 99 | if classMethod { 100 | method, has = class.proto.methods[m] 101 | } else { 102 | method, has = class.Instance.Methods()[m] 103 | } 104 | if has { 105 | return method, has 106 | } 107 | } 108 | } 109 | return method, has 110 | } 111 | 112 | func (p *proto) MustResolve(m string, classMethod bool) MethodSpec { 113 | method, has := p.Resolve(m, classMethod) 114 | methodType := "instance" 115 | if classMethod { 116 | methodType = "class" 117 | } 118 | if !has { 119 | panic(fmt.Errorf("Could not resolve %s method '%s' on class '%s'", methodType, m, p.class)) 120 | } 121 | return method 122 | } 123 | 124 | func (p *proto) HasMethod(m string, classMethod bool) bool { 125 | _, has := p.Resolve(m, classMethod) 126 | return has 127 | } 128 | 129 | func (p *proto) Def(m string, spec MethodSpec) { 130 | p.methods[m] = spec 131 | } 132 | 133 | func (p *proto) Alias(existingMethod, newMethod string) { 134 | panic("client types must call `MakeAlias`") 135 | } 136 | 137 | func (p *proto) MakeAlias(existingMethod, newMethod string, classMethod bool) { 138 | p.methods[newMethod] = p.methods[existingMethod] 139 | } 140 | 141 | func (p *proto) IsMultiple() bool { return false } 142 | 143 | func (p *proto) GenerateMethods(iface interface{}, exclusions ...string) { 144 | t := reflect.TypeOf(iface) 145 | 146 | // generics aren't yet something that can be introspected with reflect, so we 147 | // make the probably bad assumption here that the type parameter supplied in 148 | // the interface{} value above is the only type parameter this type has, 149 | // allowing us to use it as a stand-in for a true type parameter provided by 150 | // the reflect package. 151 | name, typeParam := typeName(t) 152 | 153 | for i := 0; i < t.NumMethod(); i++ { 154 | m := t.Method(i) 155 | var excluded bool 156 | for _, exclusion := range exclusions { 157 | if m.Name == exclusion { 158 | excluded = true 159 | break 160 | } 161 | } 162 | if excluded { 163 | continue 164 | } 165 | mt := m.Type 166 | v := reflect.Indirect(reflect.ValueOf(iface)).Type() 167 | methodName := ToSnakeCase(m.Name) 168 | if strings.HasSuffix(methodName, "_q") { 169 | methodName = strings.TrimSuffix(methodName, "_q") + "?" 170 | } 171 | p.Def(methodName, MethodSpec{ 172 | ReturnType: func(mt reflect.Type, receiverName, typeParam string) ReturnTypeFunc { 173 | return func(receiverType Type, blockReturnType Type, args []Type) (Type, error) { 174 | var retType Type 175 | if mt.NumOut() > 1 { 176 | multiple := Multiple{} 177 | for j := 0; j < mt.NumOut(); j++ { 178 | rt := mt.Out(j) 179 | if tt := getGenericType(rt, receiverType, typeParam); tt != nil { 180 | multiple = append(multiple, tt) 181 | } else { 182 | multiple = append(multiple, reflectTypeToThanosType(rt)) 183 | } 184 | } 185 | retType = multiple 186 | } else { 187 | rt := mt.Out(0) 188 | if tt := getGenericType(rt, receiverType, typeParam); tt != nil { 189 | retType = tt 190 | } else { 191 | retType = reflectTypeToThanosType(rt) 192 | } 193 | } 194 | return retType, nil 195 | } 196 | }(mt, name, typeParam), 197 | TransformAST: func(name, path string) TransformFunc { 198 | return func(rcvr TypeExpr, args []TypeExpr, blk *Block, it bst.IdentTracker) Transform { 199 | argExprs := []ast.Expr{} 200 | for _, a := range args { 201 | argExprs = append(argExprs, a.Expr) 202 | } 203 | return Transform{ 204 | Expr: bst.Call(rcvr.Expr, name, argExprs...), 205 | Imports: []string{path}, 206 | } 207 | } 208 | }(m.Name, v.PkgPath()), 209 | }) 210 | } 211 | } 212 | 213 | var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") 214 | var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") 215 | 216 | func ToSnakeCase(str string) string { 217 | snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}") 218 | snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") 219 | return strings.ToLower(snake) 220 | } 221 | 222 | func reflectTypeToThanosType(t reflect.Type) Type { 223 | switch t.Kind() { 224 | case reflect.Array, reflect.Slice: 225 | return NewArray(reflectTypeToThanosType(t.Elem())) 226 | case reflect.Map: 227 | return NewHash(reflectTypeToThanosType(t.Key()), reflectTypeToThanosType(t.Elem())) 228 | default: 229 | if tt, exists := goTypeMap[t.Kind()]; exists { 230 | return tt 231 | } else { 232 | return nil 233 | } 234 | } 235 | } 236 | --------------------------------------------------------------------------------