You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

204 lines
7.5 KiB

In the private packages - the actual compiler - the expressions have to be
type-checked, ordered and transformed.
The type-checking process is similar to the one executed for external tools.
Furthermore, during the type-checking
process, the built-in function's return types are set and node types
may be converted, if possible and necessary.
An operation may expect it's arguments to be in \mintinline{go}|node.Left|
and \mintinline{go}|node.Right|, which means type-checking will also need
to move the argument nodes from their default location in
\mintinline{go}|node.List| to \mintinline{go}|node.Left| and
\mintinline{go}|node.Right|.
Ordering ensures the evaluation order and re-orders expressions. All of
the new built-in functions will be evaluated left-to-right and there are no
special cases to handle.
Transforming means changing the AST nodes from the built-in operation to
nodes that the compiler knows how to translate to SSA. The actual algorithm
that these functions use cannot be implemented in normal Go code, they have to be
translated directly to AST nodes and statements.
There are more steps to compiling Go code, for example escape-checking,
SSA conversion and a lot of optimisations. These are not necessary to
implement and do not have a direct relation to the new built-ins.
The algorithms and part of the implementations for the built-in
functions are covered in the following chapters\footnote{
To see the full implementation, the git diff can be viewed\autocite{ba-go1-14-thesis-diff}.
}.
\subsubsection{fmap}\label{ch:impl-fmap}
To make the implementation in the AST easier, the algorithm will first be
developed in Go, and then translated. Implementing fmap in Go is relatively
simple:
\begin{listing}
\begin{gocode}
func fmap(fn func(Type) Type1, src []Type) (dest []Type1) {
for _, elem := range src {
dest = append(dest, fn(elem))
}
return dest
}
\end{gocode}
\caption{fmap implementation in Go}\label{code:fmap-go}
\end{listing}
However, there is room for improvement within that function. Instead
of calling \mintinline{go}|append| at every iteration of the loop, the slice can
be allocated with \mintinline{go}|make| at the beginning of the function. Thus,
calls to grow the slice at runtime can be saved.
\begin{listing}
\begin{gocode}
func fmap(fn func(Type) Type1, src []Type) []Type1 {
dest := make([]Type1, len(src))
for i, elem := range src {
dest[i] = fn(elem)
}
return dest
}
\end{gocode}
\caption{Improved implementation of fmap}\label{code:fmap-go-improved}
\end{listing}
This algorithm can be translated to the following AST node:
\begin{code}
\gofilerange{../work/go/src/cmd/compile/internal/gc/walk.go}{start-fmap-header}{end-fmap-header}%
\caption{fmap AST translation\autocite{fmap-walk-implementation}}
\end{code}
The full AST code is not displayed here as, although the algorithm is simple, the AST translation
is not as concise and more than 10 times the size. A demonstration on how a translation looks like
will be introduced in Section~\ref{ch:ast-traversal}.
The full implementation of this function is referenced in the code block's caption.
\subsubsection{prepend}
The general algorithm for `prepend' is:
\begin{listing}
\begin{gocode}
func prepend(elem Type, slice []Type) []Type {
dest := make([]Type, 1, len(src)+1)
dest[0] = elem
return append(dest, slice...)
}
\end{gocode}
\caption{prepend implementation in Go}
\end{listing}
The call to \mintinline{go}|make(...)| creates a slice with the length of 1 and the capacity
to hold all elements of the source slice, plus one. By allocating the slice with the full
length, another slice allocation within the call to \mintinline{go}|append(...)| is saved.
The element to prepend is added as the first element of the slice, and append will then
copy the `src' slice into `dest'.
The implementation within `walkprepend' reflects these lines of Go code, but
as AST nodes:
\begin{code}
\gofilerange{../work/go/src/cmd/compile/internal/gc/walk.go}{start-prepend-header}{end-prepend-header}%
\caption{prepend AST translation\autocite{prepend-walk-implementation}}
\end{code}
\subsubsection{foldr and foldl}
As outlined in Chapter~\ref{sec:fold}, there will be two fold functions;
foldr and foldl. foldr behaves exactly like its Haskell counterpart,
while foldl behaves like foldl' in Haskell.
While the fold algorithms are most obvious when using recursion, due to
performance considerations, an imperative implementation has been chosen:
\begin{listing}
\begin{gocode}
func foldr(fn func(Type, Type1) Type1, acc Type1, slice []Type) Type1 {
for i := len(s) - 1; i >= 0; i-- {
acc = fn(s[i], acc)
}
return acc
}
func foldl(fn func(Type1, Type) Type1, acc Type1, slice []Type) Type1 {
for i := 0; i < len(s); i++ {
acc = f(acc, s[i])
}
return acc
}
\end{gocode}
\caption{fold implementation in Go}
\end{listing}
The code further clarifies the differences between the two different folds;
the slice is processed in reverse order for foldr (as it would be if this
algorithm would have been implemented with recursion), and the order of
arguments to the fold function is switched.
The AST walk translates fold to:
\begin{code}
\gofilerange{../work/go/src/cmd/compile/internal/gc/walk.go}{start-fold-header}{end-fold-header}%
\caption{fold AST translation\autocite{fold-walk-implementation}}
\end{code}
\subsubsection{filter}\label{ch:impl-filter}
Being a slice-manipulating function, filter also needs to traverse the whole
slice in a for-loop. However, compared to the other newly built-in functions,
the size for the target slice is unknown until all items have been traversed,
which is why filter does not allow for the same optimisations as the other
functions.
\begin{code}
\begin{gocode}
func filter(f func(Type) bool, s []Type) (dst []Type) {
for i := range s {
if f(s) {
dst = append(dst, s[i])
}
}
}
\end{gocode}
\caption{filter implementation in Go}
\end{code}
And the same algorithm, but translated to AST statements:
\begin{code}
\gofilerange{../work/go/src/cmd/compile/internal/gc/walk.go}{start-filter-header}{end-filter-header}%
\caption{filter AST translation\autocite{filter-walk-implementation}}
\end{code}
\subsubsection{Writing the AST traversal}\label{ch:ast-traversal}
The previous chapters have all shown the function headers of the `walk' functions
that are used to traverse and rewrite the new built-ins. To illustrate how the
actual implementation of such an algorithm looks like in these functions, we
provide a small example here. The full implementation of these algorithms can
be viewed at the git diff\autocite{ba-go1-14-thesis-diff}.
This demonstration shows the translation of the statement
\begin{gocode}
filtered := make([]T, 0)
\end{gocode}
into AST nodes, or rather the construction of these AST nodes.
The type is simply a placeholder, as the AST construction uses the source slice's
type. This source slice is another AST node of which the type can be obtained from.
\begin{code}
\begin{gocode}
// create the AST node for the first argument that is
// being passed to `make', the type:
makeType := nod(OTYPE, nil, nil)
makeType.Type = source.Type // use the type of the slice
// create the make(...) AST node
makeDest := nod(OMAKE, nil, nil)
// add the arguments (the type and an int constant 0
makeDest.List.Append(makeType, nodintconst(0)) // make([]<T>, 0))
// create the "variable" where the result of make will be stored
filtered := temp(source.Type)
// the final AST node that contains the statement
// filtered = make([]<T>, 0)
final := nod(OAS, filtered, makeDest))
\end{gocode}
\caption{Illustrating the difference between Go code and it's AST code}
\end{code}