From 5987233b99c7b78f28a358ddf98f74853c27635b Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 4 Nov 2019 14:29:05 +0100 Subject: [PATCH] cgo: refactor constant expressions Put them in a separate file for separation of concerns (making them testable) and add some tests. --- cgo/const.go | 59 +++++++++++++++++++++++++++++++++++++++++++++++ cgo/const_test.go | 45 ++++++++++++++++++++++++++++++++++++ cgo/libclang.go | 50 ++++----------------------------------- 3 files changed, 109 insertions(+), 45 deletions(-) create mode 100644 cgo/const.go create mode 100644 cgo/const_test.go diff --git a/cgo/const.go b/cgo/const.go new file mode 100644 index 00000000..831515e7 --- /dev/null +++ b/cgo/const.go @@ -0,0 +1,59 @@ +package cgo + +// This file implements a parser of a subset of the C language, just enough to +// parse common #define statements to Go constant expressions. + +import ( + "go/ast" + "go/token" + "strings" +) + +// parseConst parses the given string as a C constant. +func parseConst(pos token.Pos, value string) *ast.BasicLit { + for len(value) != 0 && value[0] == '(' && value[len(value)-1] == ')' { + value = strings.TrimSpace(value[1 : len(value)-1]) + } + if len(value) == 0 { + // Pretend it doesn't exist at all. + return nil + } + // For information about integer literals: + // https://en.cppreference.com/w/cpp/language/integer_literal + if value[0] == '"' { + // string constant + return &ast.BasicLit{ValuePos: pos, Kind: token.STRING, Value: value} + } + if value[0] == '\'' { + // char constant + return &ast.BasicLit{ValuePos: pos, Kind: token.CHAR, Value: value} + } + // assume it's a number (int or float) + value = strings.Replace(value, "'", "", -1) // remove ' chars + value = strings.TrimRight(value, "lu") // remove llu suffixes etc. + // find the first non-number + nonnum := byte(0) + for i := 0; i < len(value); i++ { + if value[i] < '0' || value[i] > '9' { + nonnum = value[i] + break + } + } + // determine number type based on the first non-number + switch nonnum { + case 0: + // no non-number found, must be an integer + return &ast.BasicLit{ValuePos: pos, Kind: token.INT, Value: value} + case 'x', 'X': + // hex integer constant + // TODO: may also be a floating point number per C++17. + return &ast.BasicLit{ValuePos: pos, Kind: token.INT, Value: value} + case '.', 'e': + // float constant + value = strings.TrimRight(value, "fFlL") + return &ast.BasicLit{ValuePos: pos, Kind: token.FLOAT, Value: value} + default: + // unknown type, ignore + } + return nil +} diff --git a/cgo/const_test.go b/cgo/const_test.go new file mode 100644 index 00000000..27ed8b77 --- /dev/null +++ b/cgo/const_test.go @@ -0,0 +1,45 @@ +package cgo + +import ( + "bytes" + "go/format" + "go/token" + "testing" +) + +func TestParseConst(t *testing.T) { + // Test converting a C constant to a Go constant. + for _, tc := range []struct { + C string + Go string + }{ + {`5`, `5`}, + {`(5)`, `5`}, + {`(((5)))`, `5`}, + {`5.8f`, `5.8`}, + {`foo`, ``}, // identifiers unimplemented + {``, ``}, // empty constants not allowed in Go + {`"foo"`, `"foo"`}, + {`'a'`, `'a'`}, + {`0b10`, ``}, // binary number literals unimplemented + } { + fset := token.NewFileSet() + startPos := fset.AddFile("test.c", -1, 1000).Pos(0) + expr := parseConst(startPos, tc.C) + s := "" + if expr != nil { + // Serialize the Go constant to a string, for more readable test + // cases. + buf := &bytes.Buffer{} + err := format.Node(buf, fset, expr) + if err != nil { + t.Errorf("could not format expr from C constant %#v: %v", tc.C, err) + continue + } + s = buf.String() + } + if s != tc.Go { + t.Errorf("C constant %#v was parsed to %#v while expecting %#v", tc.C, s, tc.Go) + } + } +} diff --git a/cgo/libclang.go b/cgo/libclang.go index cf34800e..efefe370 100644 --- a/cgo/libclang.go +++ b/cgo/libclang.go @@ -246,51 +246,11 @@ func tinygo_clang_globals_visitor(c, parent C.GoCXCursor, client_data C.CXClient break } value := strings.TrimSpace(source[len(name):]) - for len(value) != 0 && value[0] == '(' && value[len(value)-1] == ')' { - value = strings.TrimSpace(value[1 : len(value)-1]) - } - if len(value) == 0 { - // Pretend it doesn't exist at all. - return C.CXChildVisit_Continue - } - // For information about integer literals: - // https://en.cppreference.com/w/cpp/language/integer_literal - if value[0] == '"' { - // string constant - p.constants[name] = constantInfo{&ast.BasicLit{pos, token.STRING, value}, pos} - return C.CXChildVisit_Continue - } - if value[0] == '\'' { - // char constant - p.constants[name] = constantInfo{&ast.BasicLit{pos, token.CHAR, value}, pos} - return C.CXChildVisit_Continue - } - // assume it's a number (int or float) - value = strings.Replace(value, "'", "", -1) // remove ' chars - value = strings.TrimRight(value, "lu") // remove llu suffixes etc. - // find the first non-number - nonnum := byte(0) - for i := 0; i < len(value); i++ { - if value[i] < '0' || value[i] > '9' { - nonnum = value[i] - break - } - } - // determine number type based on the first non-number - switch nonnum { - case 0: - // no non-number found, must be an integer - p.constants[name] = constantInfo{&ast.BasicLit{pos, token.INT, value}, pos} - case 'x', 'X': - // hex integer constant - // TODO: may also be a floating point number per C++17. - p.constants[name] = constantInfo{&ast.BasicLit{pos, token.INT, value}, pos} - case '.', 'e': - // float constant - value = strings.TrimRight(value, "fFlL") - p.constants[name] = constantInfo{&ast.BasicLit{pos, token.FLOAT, value}, pos} - default: - // unknown type, ignore + // Try to convert this #define into a Go constant expression. + expr := parseConst(pos, value) + if expr != nil { + // Parsing was successful. + p.constants[name] = constantInfo{expr, pos} } } return C.CXChildVisit_Continue