Skip to content

Commit e12da15

Browse files
aykevldeadprogram
authored andcommitted
cgo: support function-like macros
This is needed for code like this: #define __WASI_ERRNO_INVAL (UINT16_C(28)) #define EINVAL __WASI_ERRNO_INVAL
1 parent c4867c8 commit e12da15

File tree

8 files changed

+214
-46
lines changed

8 files changed

+214
-46
lines changed

cgo/const.go

+122-6
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,72 @@ func init() {
5454
}
5555

5656
// parseConst parses the given string as a C constant.
57-
func parseConst(pos token.Pos, fset *token.FileSet, value string, f *cgoFile) (ast.Expr, *scanner.Error) {
57+
func parseConst(pos token.Pos, fset *token.FileSet, value string, params []ast.Expr, callerPos token.Pos, f *cgoFile) (ast.Expr, *scanner.Error) {
5858
t := newTokenizer(pos, fset, value, f)
59+
60+
// If params is non-nil (could be a zero length slice), this const is
61+
// actually a function-call like expression from another macro.
62+
// This means we have to parse a string like "(a, b) (a+b)".
63+
// We do this by parsing the parameters at the start and then treating the
64+
// following like a normal constant expression.
65+
if params != nil {
66+
// Parse opening paren.
67+
if t.curToken != token.LPAREN {
68+
return nil, unexpectedToken(t, token.LPAREN)
69+
}
70+
t.Next()
71+
72+
// Parse parameters (identifiers) and closing paren.
73+
var paramIdents []string
74+
for i := 0; ; i++ {
75+
if i == 0 && t.curToken == token.RPAREN {
76+
// No parameters, break early.
77+
t.Next()
78+
break
79+
}
80+
81+
// Read the parameter name.
82+
if t.curToken != token.IDENT {
83+
return nil, unexpectedToken(t, token.IDENT)
84+
}
85+
paramIdents = append(paramIdents, t.curValue)
86+
t.Next()
87+
88+
// Read the next token: either a continuation (comma) or end of list
89+
// (rparen).
90+
if t.curToken == token.RPAREN {
91+
// End of parameter list.
92+
t.Next()
93+
break
94+
} else if t.curToken == token.COMMA {
95+
// Comma, so there will be another parameter name.
96+
t.Next()
97+
} else {
98+
return nil, &scanner.Error{
99+
Pos: t.fset.Position(t.curPos),
100+
Msg: "unexpected token " + t.curToken.String() + " inside macro parameters, expected ',' or ')'",
101+
}
102+
}
103+
}
104+
105+
// Report an error if there is a mismatch in parameter length.
106+
// The error is reported at the location of the closing paren from the
107+
// caller location.
108+
if len(params) != len(paramIdents) {
109+
return nil, &scanner.Error{
110+
Pos: t.fset.Position(callerPos),
111+
Msg: fmt.Sprintf("unexpected number of parameters: expected %d, got %d", len(paramIdents), len(params)),
112+
}
113+
}
114+
115+
// Assign values to the parameters.
116+
// These parameter names are closer in 'scope' than other identifiers so
117+
// will be used first when parsing an identifier.
118+
for i, name := range paramIdents {
119+
t.params[name] = params[i]
120+
}
121+
}
122+
59123
expr, err := parseConstExpr(t, precedenceLowest)
60124
t.Next()
61125
if t.curToken != token.EOF {
@@ -96,11 +160,59 @@ func parseConstExpr(t *tokenizer, precedence int) (ast.Expr, *scanner.Error) {
96160
}
97161

98162
func parseIdent(t *tokenizer) (ast.Expr, *scanner.Error) {
99-
// Normally the name is something defined in the file (like another macro)
100-
// which we get the declaration from using getASTDeclName.
101-
// This ensures that names that are only referenced inside a macro are still
102-
// getting defined.
163+
// If the identifier is one of the parameters of this function-like macro,
164+
// use the parameter value.
165+
if val, ok := t.params[t.curValue]; ok {
166+
return val, nil
167+
}
168+
103169
if t.f != nil {
170+
// Check whether this identifier is actually a macro "call" with
171+
// parameters. In that case, we should parse the parameters and pass it
172+
// on to a new invocation of parseConst.
173+
if t.peekToken == token.LPAREN {
174+
if cursor, ok := t.f.names[t.curValue]; ok && t.f.isFunctionLikeMacro(cursor) {
175+
// We know the current and peek tokens (the peek one is the '('
176+
// token). So skip ahead until the current token is the first
177+
// unknown token.
178+
t.Next()
179+
t.Next()
180+
181+
// Parse the list of parameters until ')' (rparen) is found.
182+
params := []ast.Expr{}
183+
for i := 0; ; i++ {
184+
if i == 0 && t.curToken == token.RPAREN {
185+
break
186+
}
187+
x, err := parseConstExpr(t, precedenceLowest)
188+
if err != nil {
189+
return nil, err
190+
}
191+
params = append(params, x)
192+
t.Next()
193+
if t.curToken == token.COMMA {
194+
t.Next()
195+
} else if t.curToken == token.RPAREN {
196+
break
197+
} else {
198+
return nil, &scanner.Error{
199+
Pos: t.fset.Position(t.curPos),
200+
Msg: "unexpected token " + t.curToken.String() + ", ',' or ')'",
201+
}
202+
}
203+
}
204+
205+
// Evaluate the macro value and use it as the identifier value.
206+
rparen := t.curPos
207+
pos, text := t.f.getMacro(cursor)
208+
return parseConst(pos, t.fset, text, params, rparen, t.f)
209+
}
210+
}
211+
212+
// Normally the name is something defined in the file (like another
213+
// macro) which we get the declaration from using getASTDeclName.
214+
// This ensures that names that are only referenced inside a macro are
215+
// still getting defined.
104216
if cursor, ok := t.f.names[t.curValue]; ok {
105217
return &ast.Ident{
106218
NamePos: t.curPos,
@@ -184,6 +296,7 @@ type tokenizer struct {
184296
curToken, peekToken token.Token
185297
curValue, peekValue string
186298
buf string
299+
params map[string]ast.Expr
187300
}
188301

189302
// newTokenizer initializes a new tokenizer, positioned at the first token in
@@ -195,6 +308,7 @@ func newTokenizer(start token.Pos, fset *token.FileSet, buf string, f *cgoFile)
195308
fset: fset,
196309
buf: buf,
197310
peekToken: token.ILLEGAL,
311+
params: make(map[string]ast.Expr),
198312
}
199313
// Parse the first two tokens (cur and peek).
200314
t.Next()
@@ -246,14 +360,16 @@ func (t *tokenizer) Next() {
246360
t.peekValue = t.buf[:2]
247361
t.buf = t.buf[2:]
248362
return
249-
case c == '(' || c == ')' || c == '+' || c == '-' || c == '*' || c == '/' || c == '%' || c == '&' || c == '|' || c == '^':
363+
case c == '(' || c == ')' || c == ',' || c == '+' || c == '-' || c == '*' || c == '/' || c == '%' || c == '&' || c == '|' || c == '^':
250364
// Single-character tokens.
251365
// TODO: ++ (increment) and -- (decrement) operators.
252366
switch c {
253367
case '(':
254368
t.peekToken = token.LPAREN
255369
case ')':
256370
t.peekToken = token.RPAREN
371+
case ',':
372+
t.peekToken = token.COMMA
257373
case '+':
258374
t.peekToken = token.ADD
259375
case '-':

cgo/const_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func TestParseConst(t *testing.T) {
5959
} {
6060
fset := token.NewFileSet()
6161
startPos := fset.AddFile("", -1, 1000).Pos(0)
62-
expr, err := parseConst(startPos, fset, tc.C, nil)
62+
expr, err := parseConst(startPos, fset, tc.C, nil, token.NoPos, nil)
6363
s := "<invalid>"
6464
if err != nil {
6565
if !strings.HasPrefix(tc.Go, "error: ") {

cgo/libclang.go

+59-39
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ long long tinygo_clang_getEnumConstantDeclValue(GoCXCursor c);
6363
CXType tinygo_clang_getEnumDeclIntegerType(GoCXCursor c);
6464
unsigned tinygo_clang_Cursor_isAnonymous(GoCXCursor c);
6565
unsigned tinygo_clang_Cursor_isBitField(GoCXCursor c);
66+
unsigned tinygo_clang_Cursor_isMacroFunctionLike(GoCXCursor c);
6667
6768
int tinygo_clang_globals_visitor(GoCXCursor c, GoCXCursor parent, CXClientData client_data);
6869
int tinygo_clang_struct_visitor(GoCXCursor c, GoCXCursor parent, CXClientData client_data);
@@ -370,45 +371,8 @@ func (f *cgoFile) createASTNode(name string, c clangCursor) (ast.Node, any) {
370371
gen.Specs = append(gen.Specs, valueSpec)
371372
return gen, nil
372373
case C.CXCursor_MacroDefinition:
373-
// Extract tokens from the Clang tokenizer.
374-
// See: https://stackoverflow.com/a/19074846/559350
375-
sourceRange := C.tinygo_clang_getCursorExtent(c)
376-
tu := C.tinygo_clang_Cursor_getTranslationUnit(c)
377-
var rawTokens *C.CXToken
378-
var numTokens C.unsigned
379-
C.clang_tokenize(tu, sourceRange, &rawTokens, &numTokens)
380-
tokens := unsafe.Slice(rawTokens, numTokens)
381-
// Convert this range of tokens back to source text.
382-
// Ugly, but it works well enough.
383-
sourceBuf := &bytes.Buffer{}
384-
var startOffset int
385-
for i, token := range tokens {
386-
spelling := getString(C.clang_getTokenSpelling(tu, token))
387-
location := C.clang_getTokenLocation(tu, token)
388-
var tokenOffset C.unsigned
389-
C.clang_getExpansionLocation(location, nil, nil, nil, &tokenOffset)
390-
if i == 0 {
391-
// The first token is the macro name itself.
392-
// Skip it (after using its location).
393-
startOffset = int(tokenOffset) + len(name)
394-
} else {
395-
// Later tokens are the macro contents.
396-
for int(tokenOffset) > (startOffset + sourceBuf.Len()) {
397-
// Pad the source text with whitespace (that must have been
398-
// present in the original source as well).
399-
sourceBuf.WriteByte(' ')
400-
}
401-
sourceBuf.WriteString(spelling)
402-
}
403-
}
404-
C.clang_disposeTokens(tu, rawTokens, numTokens)
405-
value := sourceBuf.String()
406-
// Try to convert this #define into a Go constant expression.
407-
tokenPos := token.NoPos
408-
if pos != token.NoPos {
409-
tokenPos = pos + token.Pos(len(name))
410-
}
411-
expr, scannerError := parseConst(tokenPos, f.fset, value, f)
374+
tokenPos, value := f.getMacro(c)
375+
expr, scannerError := parseConst(tokenPos, f.fset, value, nil, token.NoPos, f)
412376
if scannerError != nil {
413377
f.errors = append(f.errors, *scannerError)
414378
return nil, nil
@@ -488,6 +452,62 @@ func (f *cgoFile) createASTNode(name string, c clangCursor) (ast.Node, any) {
488452
}
489453
}
490454

455+
// Return whether this is a macro that's also function-like, like this:
456+
//
457+
// #define add(a, b) (a+b)
458+
func (f *cgoFile) isFunctionLikeMacro(c clangCursor) bool {
459+
if C.tinygo_clang_getCursorKind(c) != C.CXCursor_MacroDefinition {
460+
return false
461+
}
462+
return C.tinygo_clang_Cursor_isMacroFunctionLike(c) != 0
463+
}
464+
465+
// Get the macro value: the position in the source file and the string value of
466+
// the macro.
467+
func (f *cgoFile) getMacro(c clangCursor) (pos token.Pos, value string) {
468+
// Extract tokens from the Clang tokenizer.
469+
// See: https://stackoverflow.com/a/19074846/559350
470+
sourceRange := C.tinygo_clang_getCursorExtent(c)
471+
tu := C.tinygo_clang_Cursor_getTranslationUnit(c)
472+
var rawTokens *C.CXToken
473+
var numTokens C.unsigned
474+
C.clang_tokenize(tu, sourceRange, &rawTokens, &numTokens)
475+
tokens := unsafe.Slice(rawTokens, numTokens)
476+
defer C.clang_disposeTokens(tu, rawTokens, numTokens)
477+
478+
// Convert this range of tokens back to source text.
479+
// Ugly, but it works well enough.
480+
sourceBuf := &bytes.Buffer{}
481+
var startOffset int
482+
for i, token := range tokens {
483+
spelling := getString(C.clang_getTokenSpelling(tu, token))
484+
location := C.clang_getTokenLocation(tu, token)
485+
var tokenOffset C.unsigned
486+
C.clang_getExpansionLocation(location, nil, nil, nil, &tokenOffset)
487+
if i == 0 {
488+
// The first token is the macro name itself.
489+
// Skip it (after using its location).
490+
startOffset = int(tokenOffset)
491+
} else {
492+
// Later tokens are the macro contents.
493+
for int(tokenOffset) > (startOffset + sourceBuf.Len()) {
494+
// Pad the source text with whitespace (that must have been
495+
// present in the original source as well).
496+
sourceBuf.WriteByte(' ')
497+
}
498+
sourceBuf.WriteString(spelling)
499+
}
500+
}
501+
value = sourceBuf.String()
502+
503+
// Obtain the position of this token. This is the position of the first
504+
// character in the 'value' string and is used to report errors at the
505+
// correct location in the source file.
506+
pos = f.getCursorPosition(c)
507+
508+
return
509+
}
510+
491511
func getString(clangString C.CXString) (s string) {
492512
rawString := C.clang_getCString(clangString)
493513
s = C.GoString(rawString)

cgo/libclang_stubs.c

+4
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,7 @@ unsigned tinygo_clang_Cursor_isAnonymous(CXCursor c) {
8484
unsigned tinygo_clang_Cursor_isBitField(CXCursor c) {
8585
return clang_Cursor_isBitField(c);
8686
}
87+
88+
unsigned tinygo_clang_Cursor_isMacroFunctionLike(CXCursor c) {
89+
return clang_Cursor_isMacroFunctionLike(c);
90+
}

cgo/testdata/const.go

+13
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,26 @@ package main
33
/*
44
#define foo 3
55
#define bar foo
6+
67
#define unreferenced 4
78
#define referenced unreferenced
9+
10+
#define fnlike() 5
11+
#define fnlike_val fnlike()
12+
#define square(n) (n*n)
13+
#define square_val square(20)
14+
#define add(a, b) (a + b)
15+
#define add_val add(3, 5)
816
*/
917
import "C"
1018

1119
const (
1220
Foo = C.foo
1321
Bar = C.bar
22+
1423
Baz = C.referenced
24+
25+
fnlike = C.fnlike_val
26+
square = C.square_val
27+
add = C.add_val
1528
)

cgo/testdata/const.out.go

+3
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,6 @@ const C.foo = 3
4949
const C.bar = C.foo
5050
const C.unreferenced = 4
5151
const C.referenced = C.unreferenced
52+
const C.fnlike_val = 5
53+
const C.square_val = (20 * 20)
54+
const C.add_val = (3 + 5)

cgo/testdata/errors.go

+8
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ import "C"
2626
// #warning another warning
2727
import "C"
2828

29+
// #define add(a, b) (a+b)
30+
// #define add_toomuch add(1, 2, 3)
31+
// #define add_toolittle add(1)
32+
import "C"
33+
2934
// Make sure that errors for the following lines won't change with future
3035
// additions to the CGo preamble.
3136
//
@@ -51,4 +56,7 @@ var (
5156
// constants passed by a command line parameter
5257
_ = C.SOME_PARAM_CONST_invalid
5358
_ = C.SOME_PARAM_CONST_valid
59+
60+
_ = C.add_toomuch
61+
_ = C.add_toolittle
5462
)

cgo/testdata/errors.out.go

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
// testdata/errors.go:16:33: unexpected token ), expected end of expression
88
// testdata/errors.go:17:34: unexpected token ), expected end of expression
99
// -: unexpected token INT, expected end of expression
10+
// testdata/errors.go:30:35: unexpected number of parameters: expected 2, got 3
11+
// testdata/errors.go:31:31: unexpected number of parameters: expected 2, got 1
1012

1113
// Type checking errors after CGo processing:
1214
// testdata/errors.go:102: cannot use 2 << 10 (untyped int constant 2048) as C.char value in variable declaration (overflows)
@@ -17,6 +19,8 @@
1719
// testdata/errors.go:114: undefined: C.SOME_CONST_b
1820
// testdata/errors.go:116: undefined: C.SOME_CONST_startspace
1921
// testdata/errors.go:119: undefined: C.SOME_PARAM_CONST_invalid
22+
// testdata/errors.go:122: undefined: C.add_toomuch
23+
// testdata/errors.go:123: undefined: C.add_toolittle
2024

2125
package main
2226

0 commit comments

Comments
 (0)