Skip to content

Commit

Permalink
document the generics experiment in doc/generics.md
Browse files Browse the repository at this point in the history
parser: parse qualified template names i.e. 'Pair#[A,B]'
as '&ast.IndexExpr{X: Pair, Index: &ast.CompositeLit{Elts: [A,B]}}'
  • Loading branch information
cosmos72 committed Jun 3, 2018
1 parent d4edf8e commit 52bcb62
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 12 deletions.
17 changes: 14 additions & 3 deletions all_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -966,7 +966,7 @@ var testcases = []TestCase{
TestCase{C, "values", "Values(3,4,5)", nil, []interface{}{3, 4, 5}},
TestCase{A, "eval", "Eval(~quote{1+2})", 3, nil},
TestCase{C, "eval_quote", "Eval(~quote{Values(3,4,5)})", nil, []interface{}{3, 4, 5}},
TestCase{A, "parse_template_type", "~quote{template [T1,T2] type Pair struct { First T1; Second T2 }}",
TestCase{A, "parse_decl_template_type", "~quote{template [T1,T2] type Pair struct { First T1; Second T2 }}",
&ast.GenDecl{
Tok: token.TYPE,
Specs: []ast.Spec{
Expand Down Expand Up @@ -996,7 +996,7 @@ var testcases = []TestCase{
},
}, nil},

TestCase{A, "parse_template_func", "~quote{template [T] func Sum([]T) T { }}",
TestCase{A, "parse_decl_template_func", "~quote{template [T] func Sum([]T) T { }}",
&ast.FuncDecl{
Recv: &ast.FieldList{
List: []*ast.Field{
Expand Down Expand Up @@ -1028,7 +1028,7 @@ var testcases = []TestCase{
Body: &ast.BlockStmt{},
}, nil},

TestCase{A, "parse_template_method", "~quote{template [T] func (x Pair) Rest() T { }}",
TestCase{A, "parse_decl_template_method", "~quote{template [T] func (x Pair) Rest() T { }}",
&ast.FuncDecl{
Recv: &ast.FieldList{
List: []*ast.Field{
Expand Down Expand Up @@ -1056,6 +1056,17 @@ var testcases = []TestCase{
},
Body: &ast.BlockStmt{},
}, nil},

TestCase{A, "parse_qual_template_name", "~quote{Pair#[T1, T2]}",
&ast.IndexExpr{
X: &ast.Ident{Name: "Pair"},
Index: &ast.CompositeLit{
Elts: []ast.Expr{
&ast.Ident{Name: "T1"},
&ast.Ident{Name: "T2"},
},
},
}, nil},
}

func (c *TestCase) compareResults(t *testing.T, actual []r.Value) {
Expand Down
107 changes: 107 additions & 0 deletions doc/generics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
Generics
========

implementing generics Go
------------------------

The branch generics-v1 is an experiment to add generics to gomacro.

This file contains observations, choices, difficulties and solutions found
during such work.

Due to the author's personal taste and his familiarity with C++, generics are
named 'templates' in this document.

### Parser ###

#### Declaring templates ####

Adding a new syntax to declare template types, function and methods is easy:
it's just a matter of inventing the syntax and choosing a representation in
terms of go/ast.Nodes

Current syntax is:
```
template [T1, T2...] type ...
template [T1, T2...] func ...
```

Template type declarations are represented with *ast.TypeSpec as usual,
with the difference that TypeSpec.Type now contains
`&ast.CompositeLit{Type: <type declaration>, Elts: [T1, T2 ...]}`

Template function and method declarations are represented with *ast.FuncDecl
as usual, with the difference that len(FuncDecl.Recv.List) > 1:
the first receiver is nil for functions and non-nil for methods,
further receivers are &ast.Field{Names: nil, Type: <i-th template arg>}

#### Using templates ####

The main idea is that template functions and methods will be used mostly
in the same ways non-template ones, i.e. `Func(args)` and `obj.Method(args)`
exploiting appropriate type inference (exact inference rules need to be defined).

In some cases, using template functions and methods will need to specify
the exact template arguments. Template types will need such explicit
qualification most of (or maybe all) the time.

For example, after a declaration
```
template [T1, T2] type Pair struct { First T1; Second T2 }
```
it is tempting to say that the syntax to specify the template arguments
(= to qualify the template name) is
```
Pair[int, string]
```
i.e. the template name is immediately followed by '[' and the comma-separated
list of template arguments.

Alas, such syntax is too ambiguous for current Go parser. Take for example the
code fragment
```
func Nop(Pair[int, int]) { }
```
By manual inspection, it's clear that `Pair` is a type name, not a parameter
name. But compare the fragment above with this:
```
func Nop(Pair []int) { }
```
where `Pair` is a parameter name with type `[]int`.

In both cases, the parser will encounter `Pair` followed by `[` and must
decide how to parse them without further look-ahead.

The current parser algorithm for this case assumes that `Pair` is an
identifier and that `[` starts a type expression to be parsed.

To avoid breaking lots of existing code, the current parser algorithm for
this case must be preserved. So we need a different, less ambiguous syntax to
qualify template names.

One of the suggestions in latest Ian Lance Taylor
[Type parameters (December 2013)](https://github.com/golang/proposal/blob/master/design/15292/2013-12-type-params.md)
proposal is "using double square brackets, as in `Vector[[int]]`, or perhaps
some other character(s)."

The authors' current decision - but it's trivial to change it - is to write
`Pair#[int, string]` and similarly `Vector#[int]`. The reason is twofold:

1. double square brackets look too "magical"
2. the hash character `#` is currently not used in Go syntax, and usually does not have
strong connotations in programmers' minds. The alternatives are the other
ASCII characters currently not used in Go syntax: `?` `@` `$` `~`
* the question mark `?` is better suited for conditionals, as for example
the C ternary operator `?:`
* the at sign `@` already has several common meanings (at, email...).
* the dollar sign `$` seems inappropriate, in the author's opinion, for
this usage.
* the tilde sign `~` is already used by gomacro for quasiquote and friends.

Implementation choice: `Pair#[int, string]` is represented as
```
&ast.IndexExpr{X: Pair, Index: &ast.CompositeLit{Elts: [T1, T2...]} }
```
The simpler `&ast.CompositeLit{Type: Pair, Elts: [T1, T2...]} }` would suffice
for the parser, but compiling it is much more ambiguous, since it could be
interpreted as the composite literal `Pair{T1, T2}`
34 changes: 27 additions & 7 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -1065,7 +1065,12 @@ func (p *parser) parseChanType() *ast.ChanType {
func (p *parser) tryIdentOrType() ast.Expr {
switch p.tok {
case token.IDENT:
return p.parseTypeName()
ident := p.parseTypeName()
if p.tok != mt.HASH {
return ident
}
// parse Foo#[T1,T2...]
return p.parseHash(ident)
case token.LBRACK:
return p.parseArrayType()
case token.STRUCT:
Expand Down Expand Up @@ -1181,8 +1186,11 @@ func (p *parser) parseOperand(lhs bool) ast.Expr {

switch p.tok {
case token.IDENT:
x := p.parseIdent()
if !lhs {
var x ast.Expr = p.parseIdent()
if p.tok == mt.HASH {
// parse Foo#[T1,T2...]
x = p.parseHash(x)
} else if !lhs {
p.resolve(x)
}
return x
Expand Down Expand Up @@ -1264,14 +1272,26 @@ func (p *parser) parseIndexOrSlice(x ast.Expr) ast.Expr {
defer un(trace(p, "IndexOrSlice"))
}

const N = 3 // change the 3 to 2 to disable 3-index slices
lbrack := p.expect(token.LBRACK)
p.exprLev++
var index [N]ast.Expr
var colons [N - 1]token.Pos
var index0 ast.Expr
if p.tok != token.COLON {
index[0] = p.parseRhs()
index0 = p.parseRhsOrType()
if p.tok == token.COMMA {
// parse [A, B...]
var list = []ast.Expr{index0}
for p.tok == token.COMMA {
p.next()
list = append(list, p.parseType())
}
p.exprLev--
rbrack := p.expect(token.RBRACK)
return &ast.IndexExpr{X: x, Lbrack: lbrack, Index: &ast.CompositeLit{Elts: list}, Rbrack: rbrack}
}
}
const N = 3 // change the 3 to 2 to disable 3-index slices
var colons [N - 1]token.Pos
var index = [N]ast.Expr{index0}
ncolons := 0
for p.tok == token.COLON && ncolons < len(colons) {
colons[ncolons] = p.pos
Expand Down
16 changes: 16 additions & 0 deletions parser/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@ import (
mt "github.com/cosmos72/gomacro/token"
)

// parse prefix#[T1,T2...] as &ast.IndexExpr{ &ast.CompositeLit{Type: Foo, Elts: [T1, T2...]} }
func (p *parser) parseHash(prefix ast.Expr) ast.Expr {
p.next()
lbrack := p.expect(token.LBRACK)
list := p.parseTypeList()
rbrack := p.expect(token.RBRACK)
return &ast.IndexExpr{
X: prefix,
Lbrack: lbrack,
Index: &ast.CompositeLit{Type: nil, Lbrace: lbrack, Elts: list, Rbrace: rbrack},
Rbrack: rbrack,
}
}

// parse template[T1,T2...] type ...
// and template[T1,T2...] func ...
func (p *parser) parseTemplateDecl(sync func(*parser)) ast.Decl {
if p.trace {
defer un(trace(p, "TemplateDecl"))
Expand Down
8 changes: 7 additions & 1 deletion printer/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,13 @@ func (p *printer) expr1(expr ast.Expr, prec1, depth int) {
// TODO(gri): should treat[] like parentheses and undo one level of depth
p.expr1(x.X, token.HighestPrec, 1)
p.print(x.Lbrack, token.LBRACK)
p.expr0(x.Index, depth+1)
switch index := x.Index.(type) {
case *ast.CompositeLit:
// Pair#[A,B] is parsed as &ast.IndexExpr{X: Pair, Index: &ast.CompositeLit{Elts: [A,B]}}
p.exprList(index.Lbrace, index.Elts, depth+1, 0, index.Rbrace)
default:
p.expr0(index, depth+1)
}
p.print(x.Rbrack, token.RBRACK)

case *ast.SliceExpr:
Expand Down
2 changes: 2 additions & 0 deletions scanner/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,8 @@ scanAgain:
lit = comment
} else if ch == '/' {
tok = s.switch2(token.QUO, token.QUO_ASSIGN)
} else if ch == '#' {
tok = mt.HASH
} else {
s.error(s.file.Offset(pos), fmt.Sprintf("illegal character %#U", ch))
insertSemi = s.insertSemi // preserve insertSemi info
Expand Down
6 changes: 5 additions & 1 deletion token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const (
FUNCTION
LAMBDA
TYPECASE
TEMPLATE
TEMPLATE // template
HASH // #
)

var tokens map[base.Token]string
Expand All @@ -47,6 +48,7 @@ func init() {
keywords[v[1:]] = k // skip ~ in lookup table
}
tokens[TEMPLATE] = "template"
tokens[HASH] = "#"
}

// Lookup maps a identifier to its keyword token.
Expand All @@ -57,6 +59,8 @@ func Lookup(lit string) base.Token {
return MACRO
} else if lit == "template" {
return TEMPLATE
} else if lit == "#" {
return HASH
}
return token.Lookup(lit)
}
Expand Down

0 comments on commit 52bcb62

Please sign in to comment.