Commit cf2f1ea6 authored by cznic's avatar cznic
Browse files

v4: evaluate object-like macros as C expressions under EvalAllMacros

Translate used the preprocessor's #if evaluator to assign a value to every
object-like constant macro, and that evaluator treats each identifier it
meets, sizeof included, as 0: sizeof(T)*N came out as 0, (N-8)/sizeof(T)
as N-8, an enumeration constant or a cast as 0 and a floating point
expression as 0. ccgo then emitted const WALINDEX_PGSZ = 0 and const M_PI
= 0 for SQLite, and const SQLITE_PRIVATE = 0 for a macro that is not an
expression at all.

The macro-expanded replacement list is now parsed and type checked as a C
expression in the file scope of the translation unit, with everything the
unit declares visible, so sizeof, casts, enumeration constants and typedef
names have their C meaning. A macro whose expansion is not a constant
expression gets no value; there is no fallback to the #if evaluator any
more. Anything the throwaway parse declares lands in a child scope, the
file scope itself is not touched. AST.check returns its context so that
Translate can check the expressions against the unit.

On the 3.53.4 amalgamation 3809 of the 4691 object-like macros get a value
(1645 had one from their use sites before), for no measurable cost on top
of the 2.6 s translation. In the ccgo output for SQLite this changes 235
exported constants: 70 zeros become the right number, 41 constants appear
(casts, sizeof, enumeration constants), 19 bogus values disappear
(RESERVED_BYTE, errno, INFINITY, ...), 97 macros that merely name another
identifier fall back to ccgo's alias string form, and 8 numbers change:
three sizeof fixes and pointer casts like ((void*)-1), which are all-ones
now instead of -1.

Found by hazyhaar while working on sqlite#221
(libsqlite3!4).

Co-Authored-By: default avatarClaude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPoSMckkK9hLqU8t3ATQHr
parent 8d612f6e
Loading
Loading
Loading
Loading
+98 −0
Changes for v4/all_test.go: 98 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -3596,3 +3596,101 @@ func TestBFloat16Unsupported(t *testing.T) {
		t.Fatalf("unexpected error: %v", err)
	}
}

// With EvalAllMacros, an object-like macro whose expansion contained sizeof, a
// cast, an enumeration constant or any other identifier that is not a macro
// was evaluated by the preprocessor's #if evaluator, which treats identifiers
// as 0, so 'sizeof(T)*N' became 0 and '(N-8)/sizeof(T)' N-8; ccgo then emitted
// const WALINDEX_PGSZ = 0 for SQLite. Found by hazyhaar (cznic/sqlite#221,
// https://gitlab.com/cznic/libsqlite3/-/merge_requests/4).
func TestEvalAllMacros(t *testing.T) {
	cfg := defaultCfg()
	cfg.EvalAllMacros = true
	ast, err := Translate(cfg, []Source{
		{Name: "<predefined>", Value: cfg.Predefined},
		{Name: "<builtin>", Value: Builtin},
		{Name: "macros.c", Value: `
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;
typedef u16 ht_slot;

typedef struct WalIndexHdr {
	u32 iVersion;
	u32 unused;
	u32 iChange;
	u8  isInit;
	u8  big_endian;
	u16 szPage;
	u32 mxFrame;
	u32 nPage;
	u32 aFrameCksum[2];
	u32 aSalt[2];
	u32 aCksum[2];
} WalIndexHdr;

struct RowSetEntry { u32 v; u32 a; u32 b; };

enum { E5 = 5 };
int f(int);

#define HASHTABLE_NPAGE      4096
#define HASHTABLE_NSLOT      8192
#define WALINDEX_HDR_SIZE    ((sizeof(WalIndexHdr) + 7) & ~7)
#define WALINDEX_PGSZ        (sizeof(ht_slot)*HASHTABLE_NSLOT + HASHTABLE_NPAGE*sizeof(u32))
#define HASHTABLE_NPAGE_ONE  (HASHTABLE_NPAGE - (WALINDEX_HDR_SIZE/sizeof(u32)))
#define ROWSET_ENTRY_PER_CHUNK ((1024-8)/sizeof(struct RowSetEntry))
#define EXPR_FULLSIZE        sizeof(WalIndexHdr)
#define BMS                  ((int)(sizeof(u32)*8))
#define EPLUS                (E5 + 1)
#define LIT                  (1 + 2)
#define NEG                  (-1)
#define CH                   'a'
#define FLT                  1.5
#define FLTX                 (1.5 * 2)
#define STR                  "a" "b"
#define UNDECLARED           (nosuch + 1)
#define CALL                 f(1)
#define TYPE                 unsigned int
#define SELF                 SELF
#define EMPTY
`},
	})
	if err != nil {
		t.Fatal(err)
	}

	for _, tc := range []struct {
		name string
		want Value
	}{
		{"WALINDEX_HDR_SIZE", UInt64Value(48)},
		{"WALINDEX_PGSZ", UInt64Value(32768)},
		{"HASHTABLE_NPAGE_ONE", UInt64Value(4084)},
		{"ROWSET_ENTRY_PER_CHUNK", UInt64Value(84)},
		{"EXPR_FULLSIZE", UInt64Value(48)},
		{"BMS", Int64Value(32)},
		{"EPLUS", Int64Value(6)},
		{"LIT", Int64Value(3)},
		{"NEG", Int64Value(-1)},
		{"CH", Int64Value('a')},
		{"FLT", Float64Value(1.5)},
		{"FLTX", Unknown}, // floating point arithmetic is not folded
		{"STR", StringValue("ab\x00")},
		{"UNDECLARED", Unknown},
		{"CALL", Unknown},
		{"TYPE", Unknown},
		{"SELF", Unknown},
		{"EMPTY", Unknown},
	} {
		m := ast.Macros[tc.name]
		if m == nil {
			t.Errorf("%s: macro not found", tc.name)
			continue
		}

		if g, e := m.Value(), tc.want; g != e {
			t.Errorf("%s: got %v (%T), want %v (%T)", tc.name, g, g, e, e)
		}
	}
}
+101 −21
Changes for v4/cc.go: 101 added lines, 21 removed lines.
Original line number Diff line number Diff line
@@ -721,7 +721,9 @@ type Config struct {
	DefaultPtrdiffT Kind
	DefaultWcharT   Kind

	// EvalAllMacros enables attempt to assign a value to all object-like, constant macros.
	// EvalAllMacros enables attempt to assign a value to all object-like,
	// constant macros. The macro-expanded replacement list is evaluated as a C
	// expression in the file scope of the translation unit.
	EvalAllMacros bool
	// Header disables type checking of function bodies.
	Header bool
@@ -848,50 +850,128 @@ func Parse(cfg *Config, sources []Source) (*AST, error) {
	return ast, err
}

func parse(cfg *Config, sources []Source) (*cpp, *AST, error) {
func parse(cfg *Config, sources []Source) (*parser, *AST, error) {
	p, err := newParser(cfg, newFset(), sources)
	if err != nil {
		return nil, nil, err
	}

	ast, err := p.parse()
	return p.cpp, ast, err
	return p, ast, err
}

// Translate preprocesses, parses and type checks a translation unit,
// consisting of inputs in sources.
func Translate(cfg *Config, sources []Source) (*AST, error) {
	cpp, ast, err := parse(cfg, sources)
	p, ast, err := parse(cfg, sources)
	if err != nil {
		return nil, err
	}

	if err := ast.check(cfg); err != nil {
	c, err := ast.check(cfg)
	if err != nil {
		return nil, err
	}

	defer func() { c.cfg = nil }()

	if cfg.EvalAllMacros {
		evalAllMacros(p, ast, c)
	}
	return ast, nil
}

// evalAllMacros implements Config.EvalAllMacros: it attempts to assign a value
// to every object-like, constant macro that does not have one yet.
//
// The replacement list is macro-expanded and then parsed and type checked as a
// C expression in the file scope of the translation unit, so that sizeof,
// casts, enumeration constants, typedef names etc. have their C meaning. A
// macro whose expansion is not a constant expression gets no value.
//
// Earlier versions used the preprocessor's #if evaluator for this. It knows
// nothing about declarations and treats every identifier, sizeof included, as
// zero, so 'sizeof(T)*N' came out as 0, '(N-8)/sizeof(T)' as N-8 and a
// floating point expression as 0.
func evalAllMacros(p *parser, ast *AST, c *ctx) {
	cpp := p.cpp
	cpp.eh = func(msg string, args ...interface{}) {}
		for _, v := range cpp.macros {
			if l := v.ReplacementList(); !v.IsFnLike && v.IsConst && len(l) != 0 && (v.Value() == nil || v.Value() == Unknown) {
				switch x := cpp.eval(l).(type) {
				case nil:
					// nop
				case int32:
					v.val = Int64Value(x)
				case int64:
					v.val = Int64Value(x)
				case uint32:
					v.val = UInt64Value(x)
				case uint64:
					v.val = UInt64Value(x)
				case string:
					v.val = StringValue(x)
	for _, m := range cpp.macros {
		l := m.ReplacementList()
		if m.IsFnLike || !m.IsConst || len(l) == 0 || m.Value() != Unknown {
			continue
		}

		var w cppTokens
		s := cppTokens(tokens2CppTokens(l, false))
		cpp.expand(false, false, &s, &w)
		var b strings.Builder
		for _, t := range w {
			switch t.Ch {
			case ' ', '\n', eof:
				continue
			}
			b.Write(t.Src())
			b.WriteByte(' ')
		}
		if b.Len() == 0 {
			continue
		}
	return ast, nil

		if val, typ, ok := evalMacroExpression(p, ast, c, m.Name.SrcStr(), b.String()); ok {
			m.val, m.typ = val, typ
		}
	}
}

// evalMacroExpression parses src, the macro-expanded replacement list of the
// macro name, as a C expression in the file scope of ast and type checks it.
// It returns the expression's value and type if the value is a known integer,
// floating point or string constant.
func evalMacroExpression(p *parser, ast *AST, c *ctx, name, src string) (val Value, typ Type, ok bool) {
	n := len(c.errors)
	defer func() {
		if e := recover(); e != nil {
			c.errors = c.errors[:n]
			val, typ, ok = nil, nil, false
		}
	}()

	// The expression parser expects a delimiter after an expression, never EOF.
	sub, err := newParser(c.cfg, newFset(), []Source{{Name: "<macro " + name + ">", Value: src + ";\n"}})
	if err != nil {
		return nil, nil, false
	}

	failed := false
	sub.cpp.eh = func(string, ...interface{}) { failed = true }
	// Anything the parse declares, like a struct tag it does not find, lands
	// in a throwaway child of the file scope; the file scope itself is left
	// alone. Names are visible as of the end of the translation unit.
	sub.scope = &Scope{Parent: ast.Scope}
	sub.seq = p.seq
	sub.cpp.rune()
	e := sub.expression(false)
	if failed || e == nil || sub.rune(false) != ';' {
		return nil, nil, false
	}

	if sub.shift(false); failed || sub.rune(false) != eof {
		return nil, nil, false
	}

	typ = e.check(c, decay)
	if len(c.errors) != n {
		c.errors = c.errors[:n]
		return nil, nil, false
	}

	switch x := e.Value().(type) {
	case Int64Value, UInt64Value, Float64Value, StringValue:
		return x, typ, true
	}

	return nil, nil, false
}

// NodeTokens returns the source tokens n consists of.
+4 −4
Changes for v4/check.go: 4 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -654,13 +654,13 @@ type AST struct {
	predefinedDeclarator0 *Declarator // `int __predefined_declarator`
}

func (n *AST) check(cfg *Config) error {
// check type checks n. The returned context, valid until its cfg field is
// cleared by the caller, can check further expressions against n.
func (n *AST) check(cfg *Config) (*ctx, error) {
	n.Structs = map[*StructType]struct{}{}
	n.Unions = map[*UnionType]struct{}{}
	c := newCtx(n, cfg)

	defer func() { c.cfg = nil }()

	for l := n.TranslationUnit; l != nil; l = l.TranslationUnit {
		l.ExternalDeclaration.check(c)
	}
@@ -684,7 +684,7 @@ func (n *AST) check(cfg *Config) error {
			prev = l
		}
	}
	return c.errors.err()
	return c, c.errors.err()
}

// ExternalDeclaration: