Files
zeekay 03451a9a1b The C++ backend on the parser package
The parser moved to idl and the C++ backend was still holding the old one,
so the two lines of work could not both be had. They meet here: emitcpp.go
imports idl like emit.go does, and structSize moves to idl beside the
Struct whose width it reports — a stated `@N` or where the last field ends
is a property of the declaration, not of whoever is printing it.

The grammar tests move with the grammar. Emission tests stay with the
emitter. Every golden, Go and C++, is unchanged: the merge is a move, not
a change of what the generator prints.
2026-09-06 13:15:57 -07:00

524 lines
13 KiB
Go

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package idl reads .zap schema source into an AST.
//
// [Parse] is the whole surface: source in, a [File] out, carrying the package
// name, the type aliases, the structs with their fields and author-declared
// offsets, and the interfaces with their methods and wire ordinals.
//
// It was cmd/zapgen's own code and is a package because the tool is not its
// only reader. A generator that projects a schema onto something other than Go
// accessors — an SDK, a service door, a document — needs the same AST, and a
// second parser would be a second grammar: the two would agree until they did
// not, and the schema would then mean whichever one you asked. There is one.
//
// The grammar is stated on the parse functions below. What is not in it is
// worth saying here: no enums, no unions, no imports, no defaults, and no doc
// comments — a `#` comment is skipped as whitespace and reaches no node.
package idl
import (
"fmt"
"strconv"
"unicode"
"unicode/utf8"
)
// Parse parses a .zap source file into a *File. Whitespace-significant
// source is desugared into the canonical brace form first (see
// desugar.go); pure-brace source passes through that step byte-for-byte
// unchanged, so the brace grammar below is untouched.
func Parse(filename string, src []byte) (*File, error) {
src, err := Desugar(src)
if err != nil {
return nil, fmt.Errorf("%s: %w", filepathBase(filename), err)
}
p := &parser{
src: src,
filename: filename,
line: 1,
file: &File{
Source: filepathBase(filename),
Aliases: make(map[string]Type),
},
}
return p.parseFile()
}
// filepathBase returns the final path element of name. Avoids the import
// of path/filepath in this file (kept tiny so the parser stays focused).
func filepathBase(name string) string {
for i := len(name) - 1; i >= 0; i-- {
if name[i] == '/' || name[i] == '\\' {
return name[i+1:]
}
}
return name
}
type parser struct {
src []byte
pos int
line int
filename string
file *File
}
func (p *parser) errf(format string, args ...any) error {
return fmt.Errorf("%s:%d: %s", p.filename, p.line, fmt.Sprintf(format, args...))
}
// skipSpace advances past whitespace and # comments.
func (p *parser) skipSpace() {
for p.pos < len(p.src) {
c := p.src[p.pos]
switch {
case c == '\n':
p.line++
p.pos++
case c == ' ' || c == '\t' || c == '\r':
p.pos++
case c == '#':
// Comment to end of line.
for p.pos < len(p.src) && p.src[p.pos] != '\n' {
p.pos++
}
default:
return
}
}
}
// peek returns the next rune without consuming it.
func (p *parser) peek() rune {
if p.pos >= len(p.src) {
return 0
}
r, _ := utf8.DecodeRune(p.src[p.pos:])
return r
}
// peekKeyword reports whether the upcoming bytes match keyword followed
// by a non-identifier rune. Does not advance.
func (p *parser) peekKeyword(keyword string) bool {
if p.pos+len(keyword) > len(p.src) {
return false
}
if string(p.src[p.pos:p.pos+len(keyword)]) != keyword {
return false
}
if p.pos+len(keyword) == len(p.src) {
return true
}
next, _ := utf8.DecodeRune(p.src[p.pos+len(keyword):])
return !isIdentRune(next)
}
func isIdentStart(r rune) bool { return r == '_' || unicode.IsLetter(r) }
func isIdentRune(r rune) bool { return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r) }
// readIdent reads an identifier. Returns ("", false) if no identifier
// is at the current position.
func (p *parser) readIdent() (string, bool) {
start := p.pos
if p.pos >= len(p.src) {
return "", false
}
r, sz := utf8.DecodeRune(p.src[p.pos:])
if !isIdentStart(r) {
return "", false
}
p.pos += sz
for p.pos < len(p.src) {
r, sz = utf8.DecodeRune(p.src[p.pos:])
if !isIdentRune(r) {
break
}
p.pos += sz
}
return string(p.src[start:p.pos]), true
}
// readInt reads an unsigned integer.
func (p *parser) readInt() (int, error) {
start := p.pos
for p.pos < len(p.src) && p.src[p.pos] >= '0' && p.src[p.pos] <= '9' {
p.pos++
}
if p.pos == start {
return 0, p.errf("expected integer")
}
n, err := strconv.Atoi(string(p.src[start:p.pos]))
if err != nil {
return 0, p.errf("bad integer: %v", err)
}
return n, nil
}
// expect consumes a literal byte sequence; errors if it does not match.
func (p *parser) expect(lit string) error {
if p.pos+len(lit) > len(p.src) || string(p.src[p.pos:p.pos+len(lit)]) != lit {
return p.errf("expected %q", lit)
}
// Track newlines in the literal (rare, but be correct).
for i := 0; i < len(lit); i++ {
if lit[i] == '\n' {
p.line++
}
}
p.pos += len(lit)
return nil
}
// parseFile is the top-level entry. Grammar:
//
// File := PackageDecl (TypeAlias | Struct)*
// PackageDecl := 'package' Ident ('.' Ident)*
func (p *parser) parseFile() (*File, error) {
p.skipSpace()
if !p.peekKeyword("package") {
return nil, p.errf("expected `package` declaration")
}
p.pos += len("package")
p.skipSpace()
name, ok := p.readIdent()
if !ok {
return nil, p.errf("expected package name after `package`")
}
// A dotted path, because a namespace nests and a package does not. Each
// backend renders the one path its own way: C++ opens all of it, Go takes
// the last segment, which is what a Go package name is.
for p.pos < len(p.src) && p.src[p.pos] == '.' {
p.pos++
seg, ok := p.readIdent()
if !ok {
return nil, p.errf("expected a name after `.` in the package path")
}
name += "." + seg
}
p.file.Package = name
for {
p.skipSpace()
if p.pos >= len(p.src) {
break
}
switch {
case p.peekKeyword("struct"):
s, err := p.parseStruct()
if err != nil {
return nil, err
}
p.file.Structs = append(p.file.Structs, s)
case p.peekKeyword("interface"):
iface, err := p.parseInterface()
if err != nil {
return nil, err
}
p.file.Interfaces = append(p.file.Interfaces, iface)
case p.peekKeyword("type"):
if err := p.parseAlias(); err != nil {
return nil, err
}
default:
return nil, p.errf("expected `struct`, `interface`, or `type` at top level")
}
}
return p.file, nil
}
// parseAlias := 'type' Ident '=' Type
func (p *parser) parseAlias() error {
p.pos += len("type")
p.skipSpace()
name, ok := p.readIdent()
if !ok {
return p.errf("expected alias name after `type`")
}
p.skipSpace()
if err := p.expect("="); err != nil {
return err
}
p.skipSpace()
t, err := p.parseType()
if err != nil {
return err
}
if _, dup := p.file.Aliases[name]; dup {
return p.errf("duplicate type alias %q", name)
}
p.file.Aliases[name] = t
return nil
}
// parseStruct := 'struct' Ident '{' Field* '}'
func (p *parser) parseStruct() (*Struct, error) {
p.pos += len("struct")
p.skipSpace()
name, ok := p.readIdent()
if !ok {
return nil, p.errf("expected struct name")
}
p.skipSpace()
// `struct Name @N` states the fixed section's width when it is wider than
// the fields — a record whose stride the wire reserves and the fields do
// not fill. Left out, the width is where the last field ends.
size := 0
if p.pos < len(p.src) && p.src[p.pos] == '@' {
p.pos++
p.skipSpace()
n, err := p.readInt()
if err != nil {
return nil, err
}
size = n
p.skipSpace()
}
if err := p.expect("{"); err != nil {
return nil, err
}
s := &Struct{Name: name, Size: size}
for {
p.skipSpace()
if p.pos >= len(p.src) {
return nil, p.errf("unterminated struct %q", name)
}
if p.src[p.pos] == '}' {
p.pos++
return s, nil
}
f, err := p.parseField()
if err != nil {
return nil, err
}
// A field may not repeat its struct's name. The accessor a backend
// prints for it would then be a member named as the type is, which
// C++ reads as a constructor and refuses; in the other two it merely
// reads as a thing named twice. One rule here beats one rule per
// backend, and the cost of it is a better field name.
if f.Name == name {
return nil, p.errf("struct %s: field %s repeats the struct's name", name, f.Name)
}
for _, prior := range s.Fields {
if prior.Name == f.Name {
return nil, p.errf("struct %s: duplicate field %s", name, f.Name)
}
}
s.Fields = append(s.Fields, f)
}
}
// parseInterface := 'interface' Ident '{' Method* '}'
//
// Method ordinals are auto-assigned 1, 2, 3, … in declaration order.
func (p *parser) parseInterface() (*Interface, error) {
p.pos += len("interface")
p.skipSpace()
name, ok := p.readIdent()
if !ok {
return nil, p.errf("expected interface name")
}
p.skipSpace()
if err := p.expect("{"); err != nil {
return nil, err
}
iface := &Interface{Name: name}
ordinal := 1
for {
p.skipSpace()
if p.pos >= len(p.src) {
return nil, p.errf("unterminated interface %q", name)
}
if p.src[p.pos] == '}' {
p.pos++
return iface, nil
}
m, err := p.parseMethod(ordinal)
if err != nil {
return nil, err
}
iface.Methods = append(iface.Methods, m)
ordinal++
}
}
// parseMethod := Ident '(' Param? ')' ( 'returns' '(' Param? ')' )?
//
// Param := Ident ':' Ident (name : StructTypeName)
//
// The request param list and the optional `returns` param list each hold
// at most one struct param — ZAP method payloads are a single struct.
func (p *parser) parseMethod(ordinal int) (*Method, error) {
name, ok := p.readIdent()
if !ok {
return nil, p.errf("expected method name")
}
p.skipSpace()
request, err := p.parseParamList()
if err != nil {
return nil, err
}
p.skipSpace()
var response *Param
if p.peekKeyword("returns") {
p.pos += len("returns")
p.skipSpace()
response, err = p.parseParamList()
if err != nil {
return nil, err
}
}
return &Method{Name: name, Ordinal: ordinal, Request: request, Response: response}, nil
}
// parseParamList parses `( name : StructType )` or `()`. Returns nil for
// the empty list, or an error on more than one param — a ZAP method
// carries exactly one struct payload per direction.
func (p *parser) parseParamList() (*Param, error) {
if err := p.expect("("); err != nil {
return nil, err
}
p.skipSpace()
if p.pos < len(p.src) && p.src[p.pos] == ')' {
p.pos++
return nil, nil
}
pname, ok := p.readIdent()
if !ok {
return nil, p.errf("expected parameter name")
}
p.skipSpace()
if err := p.expect(":"); err != nil {
return nil, err
}
p.skipSpace()
tname, ok := p.readIdent()
if !ok {
return nil, p.errf("expected parameter type")
}
p.skipSpace()
if p.pos < len(p.src) && p.src[p.pos] == ',' {
return nil, p.errf("method params carry exactly one struct payload per direction")
}
if err := p.expect(")"); err != nil {
return nil, err
}
return &Param{Name: pname, StructName: tname}, nil
}
// parseField := Ident Type '@' Int
func (p *parser) parseField() (*Field, error) {
name, ok := p.readIdent()
if !ok {
return nil, p.errf("expected field name")
}
p.skipSpace()
t, err := p.parseType()
if err != nil {
return nil, err
}
p.skipSpace()
if err := p.expect("@"); err != nil {
return nil, err
}
p.skipSpace()
off, err := p.readInt()
if err != nil {
return nil, err
}
return &Field{Name: name, Type: t, Offset: off}, nil
}
// parseType parses one type expression.
func (p *parser) parseType() (Type, error) {
if p.peekKeyword("list") {
p.pos += len("list")
p.skipSpace()
if err := p.expect("<"); err != nil {
return Type{}, err
}
p.skipSpace()
inner, err := p.parseType()
if err != nil {
return Type{}, err
}
p.skipSpace()
if err := p.expect(">"); err != nil {
return Type{}, err
}
return Type{Kind: KindList, ListElem: &inner}, nil
}
if p.peekKeyword("bytes_fixed") {
p.pos += len("bytes_fixed")
p.skipSpace()
if err := p.expect("["); err != nil {
return Type{}, err
}
p.skipSpace()
n, err := p.readInt()
if err != nil {
return Type{}, err
}
if n <= 0 {
return Type{}, p.errf("bytes_fixed[N] must have N > 0")
}
p.skipSpace()
if err := p.expect("]"); err != nil {
return Type{}, err
}
return Type{Kind: KindBytesFixed, FixedSize: n}, nil
}
// Primitive keywords first; fall through to ident (alias or struct name).
switch {
case p.peekKeyword("bool"):
p.pos += len("bool")
return Type{Kind: KindBool}, nil
case p.peekKeyword("u8"):
p.pos += len("u8")
return Type{Kind: KindU8}, nil
case p.peekKeyword("u16"):
p.pos += len("u16")
return Type{Kind: KindU16}, nil
case p.peekKeyword("u32"):
p.pos += len("u32")
return Type{Kind: KindU32}, nil
case p.peekKeyword("u64"):
p.pos += len("u64")
return Type{Kind: KindU64}, nil
case p.peekKeyword("i8"):
p.pos += len("i8")
return Type{Kind: KindI8}, nil
case p.peekKeyword("i16"):
p.pos += len("i16")
return Type{Kind: KindI16}, nil
case p.peekKeyword("i32"):
p.pos += len("i32")
return Type{Kind: KindI32}, nil
case p.peekKeyword("i64"):
p.pos += len("i64")
return Type{Kind: KindI64}, nil
case p.peekKeyword("f32"):
p.pos += len("f32")
return Type{Kind: KindF32}, nil
case p.peekKeyword("f64"):
p.pos += len("f64")
return Type{Kind: KindF64}, nil
case p.peekKeyword("bytes"):
p.pos += len("bytes")
return Type{Kind: KindBytes}, nil
case p.peekKeyword("text"):
p.pos += len("text")
return Type{Kind: KindText}, nil
}
// User-defined name: alias or nested struct.
name, ok := p.readIdent()
if !ok {
return Type{}, p.errf("expected type, got %q", p.peek())
}
if a, ok := p.file.Aliases[name]; ok {
return a, nil
}
return Type{Kind: KindStruct, StructName: name}, nil
}