Skip to content

Inefficient transpiled code (heap escape allocs) #53

Description

@romshark
The following Dingo code (click to view)
package main

import "fmt"

type Order struct {
    ID        string
    Amount    int
    Validated bool
    Paid      bool
}

func fetchOrder(orderID string) Result[Order, string] {
    if orderID == "" {
        return Err[Order]("order id is empty")
    }

    if orderID == "missing" {
        return Err[Order]("order not found")
    }

    return Order{
        ID:        orderID,
        Amount:    125,
        Validated: false,
        Paid:      false,
    }
}

func validateOrder(order Order) Result[Order, string] {
    if order.Amount <= 0 {
        return Err[Order]("invalid amount")
    }

    order.Validated = true
    return order
}

func processPayment(order Order) Result[Order, string] {
    if !order.Validated {
        return Err[Order]("order must be validated before payment")
    }

    if order.ID == "declined" {
        return Err[Order]("payment was declined")
    }

    order.Paid = true
    return order
}

func processOrder(orderID string) Result[Order, string] {
    order := fetchOrder(orderID)?
    validated := validateOrder(order)?
    payment := processPayment(validated)?
    return payment
}

func main() {
    ids := []string{"A-100", "", "missing", "declined"}

    for _, id := range ids {
        fmt.Printf("processing %q\n", id)

        match processOrder(id) {
            Ok(order) => fmt.Printf("success: id=%s amount=%d validated=%t paid=%t\n\n", order.ID, order.Amount, order.Validated, order.Paid),
            Err(err) => fmt.Printf("error: %s\n\n", err),
        }
    }
}

Should be equivalent to the following Go code
package main

import (
	"errors"
	"fmt"
	"io"
	"testing"
)

type StdOrder struct {
	ID        string
	Amount    int
	Validated bool
	Paid      bool
}

var (
	errOrderIDEmpty    = errors.New("order id is empty")
	errOrderNotFound   = errors.New("order not found")
	errInvalidAmount   = errors.New("invalid amount")
	errNotValidated    = errors.New("order must be validated before payment")
	errPaymentDeclined = errors.New("payment was declined")
)

func stdFetchOrder(orderID string) (StdOrder, error) {
	if orderID == "" {
		return StdOrder{}, errOrderIDEmpty
	}

	if orderID == "missing" {
		return StdOrder{}, errOrderNotFound
	}

	return StdOrder{
		ID:        orderID,
		Amount:    125,
		Validated: false,
		Paid:      false,
	}, nil
}

func stdValidateOrder(order StdOrder) (StdOrder, error) {
	if order.Amount <= 0 {
		return StdOrder{}, errInvalidAmount
	}

	order.Validated = true
	return order, nil
}

func stdProcessPayment(order StdOrder) (StdOrder, error) {
	if !order.Validated {
		return StdOrder{}, errNotValidated
	}

	if order.ID == "declined" {
		return StdOrder{}, errPaymentDeclined
	}

	order.Paid = true
	return order, nil
}

func stdProcessOrder(orderID string) (StdOrder, error) {
	order, err := stdFetchOrder(orderID)
	if err != nil {
		return StdOrder{}, err
	}
	validated, err := stdValidateOrder(order)
	if err != nil {
		return StdOrder{}, err
	}
	payment, err := stdProcessPayment(validated)
	if err != nil {
		return StdOrder{}, err
	}
	return payment, nil
}
Benchmarks (click to open)
func BenchmarkProcessOrders(b *testing.B) {
	ids := []string{"A-100", "", "missing", "declined"}
	w := io.Discard

	for i := 0; i < b.N; i++ {
		for _, id := range ids {
			fmt.Fprintf(w, "processing %q\n", id)

			res := processOrder(id)
			if res.IsOk() {
				order := res.MustOk()
				fmt.Fprintf(w, "success: id=%s amount=%d validated=%t paid=%t\n\n", order.ID, order.Amount, order.Validated, order.Paid)
			} else {
				err := res.MustErr()
				fmt.Fprintf(w, "error: %s\n\n", err)
			}

		}
	}
}

func BenchmarkStdProcessOrders(b *testing.B) {
	ids := []string{"A-100", "", "missing", "declined"}
	w := io.Discard

	for i := 0; i < b.N; i++ {
		for _, id := range ids {
			fmt.Fprintf(w, "processing %q\n", id)

			order, err := stdProcessOrder(id)
			if err != nil {
				fmt.Fprintf(w, "error: %s\n\n", err)
			} else {
				fmt.Fprintf(w, "success: id=%s amount=%d validated=%t paid=%t\n\n",
					order.ID, order.Amount, order.Validated, order.Paid)
			}
		}
	}
}

The results are somewhat disappointing:

goos: darwin
goarch: arm64
pkg: dingotest
cpu: Apple M4 Pro
BenchmarkProcessOrders-14                2654920               431.1 ns/op           192 B/op         11 allocs/op
BenchmarkStdProcessOrders-14             2923504               412.9 ns/op            64 B/op          4 allocs/op
PASS
ok      dingotest       3.656s

Please correct me if I'm wrong, but the transpiler fails to produce an efficient Go equivalent and instead seems to alloc due to heap escaping of dgo.Result members:

type Result[T, E any] struct {
	Tag ResultTag // Exported for pattern matching
	Ok  *T        // Exported for pattern matching
	Err *E        // Exported for pattern matching
}

% dingo version
------------------------------------------------------------
     ▄▀▄    ▄▀▄       
     █  ▀▀▀▀▀  █      Dingo v0.14.0
     █  ^   ^  █      
     ▀▄   ▲   ▄▀      Runtime:  Go
       ▀▄▄▄▄▄▀        Website:  https://dingo-lang.org
      ▄█▀   ▀█▄       GitHub:   github.com/MadAppGang/dingo
     ██  ███  ██      
     ▀█▄▄▀ ▀▄▄█▀      

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1-highHigh priority - significant impactenhancementNew feature or requesthelp wantedExtra attention is needed

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions