Comments

Reference index · Summary of this topic

Two things are converted here, under different switches.

Behavioral goldens are captured without -comments, so they show the blank-line rules and never a comment. The comment rules are guarded by converter tests that assert positions on fresh output, and by a census that compares a whole corpus with its Go source (see Guarded by).

Where a comment is attached, and where it is not

Go’s AST attaches a comment to a node only in specific places: a declaration’s Doc, a struct field’s Doc and Comment, a spec’s Doc and Comment. Statements have no comment field. A comment inside a function body is free-floating: it exists in File.Comments and nowhere else.

The converter keeps every comment group, attached or not, with its Go position, and the emitter claims each free one at the point that matches where Go put it: after a statement, after a {, on or above a case label, before a }, between list items, or between operands. Each placement below is one such point. A comment no point claims is written at the next place the converter writes loose comments, on its own line; that fallback is what Limits describes.

Blank lines

Between the statements of a body, Go’s blank lines are kept

Rule. Where Go has one or more blank lines between two statements of a list, the C# has exactly one; where Go has none, the C# adds none. A list is a block body, a case or type-switch clause body, or a select clause body. There are never two blank lines in a row, never one right after {, and never one right before }. A Go comment between the two statements does not count as a blank line; in a conversion without -comments, a gap that holds only dropped comments still gives one blank line when Go had one.

This applies to every conversion. A behavioral golden, converted without -comments; the comment between the two groups is dropped and leaves no extra blank behind:

	converted := (func(score) (score, bool))(stay)
	result, turnIsOver := converted(score{player: 3, opponent: 5, thisTurn: 7})
	fmt.Println("converted func type call:", result.player, result.opponent, result.thisTurn, turnIsOver)

	// A call through a NAMED func-type FIELD deconstructs as one tuple call - the named
	// type's underlying signature carries the result count (bufio Scanner's split SplitFunc;
	// the shattered form emitted one full call per LHS element, tripling side effects).
	m := machine{split: func(s string) (int, string, error) { return len(s), s + "!", nil }}
	n, out, serr := m.split("hi")
	fmt.Println(n, out, serr == nil) // 2 hi! true

	p := provider{produce: func(x int) int { return x * 2 }}
	r := registry{h: p.produce}
	fmt.Println(r.h(21)) // 42
    var converted = (Func<score, (score, bool)>)(stay);
    var (result, turnIsOver) = converted(new score(player: 3, opponent: 5, thisTurn: 7));
    fmt.Println("converted func type call:", result.player, result.opponent, result.thisTurn, turnIsOver);

    var m = new machine(split: (@string s) => (len(s), s + "!", default!));
    var (n, @out, serr) = m.split("hi");
    fmt.Println(n, @out, serr == default!);

    var p = new provider(produce: (nint x) => x * 2);
    var r = new registry(h: new handler(p.produce));
    fmt.Println(r.h(21));

"converted func type call:" is a plain C# literal that go2cs-gen boxes as a Go string for fmt.Println (strings); @out is the keyword-escaped name of Go’s out.

With -comments, from the converted standard library, the blank line stays above the comment that opens the next group:

		}
	}

	// e±
	dst = append(dst, fmt)
	exp := d.dp - 1
	if d.nd == 0 { // special case: 0 has exponent 0
		exp = 0
	}
        }
    }

    // e±
    dst = append(dst, fmt);
    nint exp = d.dp - 1;
    if (d.nd == 0) { // special case: 0 has exponent 0
        exp = 0;
    }

Why. Go programmers group statements with blank lines, and the groups are part of what a reader follows.

Limits. A blank line that Go writes directly after a comment that opens a block is not kept: the comment leads the block, and the blank after it is dropped. The converter’s own lines are not Go statements, so a range variable’s copy can sit next to a blank line Go does not have.

One blank line after the converter’s prelude

Rule. A function’s prelude is the lines the converter writes before the Go body: locals for named results, the slice built from a variadic parameter, array clones, the dereferenced alias of a pointer receiver or parameter, heap boxes. Its lines are written one after another, then exactly one blank line, then the Go body. This applies to every conversion.

func ratioString(vals ...int) string {
	total := 0
	for _, val := range vals {
		total += val
	}
internal static @string ratioString(params ꓸꓸꓸnint valsʗp) {
    var vals = valsʗp.sslice();

    nint total = 0;
    foreach (var (_, val) in vals) {
        total += val;
    }

ꓸꓸꓸnint is the converted variadic parameter type, and valsʗp is the raw params argument. With named results and -comments:

// Read implements the [io.Reader] interface.
func (r *Reader) Read(b []byte) (n int, err error) {
	if r.i >= int64(len(r.s)) {
		return 0, io.EOF
	}
// Read implements the [io.Reader] interface.
public static (nint n, error err) Read(this ref Reader r, slice<byte> b) {
    nint n = default!;
    error err = default!;

    if (r.i >= (int64)len(r.s)) {
        return (0, io.EOF);
    }

Why. The prelude is the converter’s text, not Go’s; one blank line sets it apart from the Go body the reader is comparing with the original.

After a switch, a blank line only where Go has one

Rule. With -comments, a converted switch or type switch ends at its closing }} with no line break of its own, so the blank-line rule above decides what follows it: a blank line where Go has one, none where it has none, and none before the } of a block that ends with a switch. A converted select never ends with its own line break, so after a select the blank line follows Go in every conversion.

No blank line where Go has none:

		default:
			a[n] = uint16(replacementChar)
			n++
		}
	}
	return a[:n]
        default: {
            a[n] = (uint16)replacementChar;
            n++;
            break;
        }}
    }
    return a.slice(0, n);

A blank line where Go has one:

	default:
		dst = append(dst, byte(exp/100)+'0', byte(exp/10)%10+'0', byte(exp%10)+'0')
	}

	return dst
    default: {
        dst = append(dst, (byte)((byte)(exp / 100) + (rune)'0'), (byte)((byte)(exp / 10) % 10 + (rune)'0'), (byte)((byte)(exp % 10) + (rune)'0'));
        break;
    }}

    return dst;

Limits. Without -comments the converted switch still ends with its own line break, so a blank line follows every switch whether Go has one or not. A behavioral golden shows it:

	case string:
		return "string"
	}
	return "other"
    case @string: {
        return "string";
    }}

    return "other";

Between the specs of a const or var group

Rule. With -comments, Go’s blank line between two specs of a grouped declaration is kept as one blank line, above the spec or above its doc comment, and none is added between specs Go writes next to each other. The gap is measured from the end of the previous spec’s trailing comment, which can span lines.

const (
	// 0xd800-0xdc00 encodes the high 10 bits of a pair.
	// 0xdc00-0xe000 encodes the low 10 bits of a pair.
	// the value is those 20 bits plus 0x10000.
	surr1 = 0xd800
	surr2 = 0xdc00
	surr3 = 0xe000

	surrSelf = 0x10000
)
// 0xd800-0xdc00 encodes the high 10 bits of a pair.
// 0xdc00-0xe000 encodes the low 10 bits of a pair.
// the value is those 20 bits plus 0x10000.
internal static UntypedInt surr1 => 0xd800;
internal static UntypedInt surr2 => 0xdc00;
internal static UntypedInt surr3 => 0xe000;

internal static UntypedInt surrSelf => 0x10000;

Limits. Only const and var groups. Without -comments this rule is not applied.

Doc comments

The group’s doc once, then each spec’s own

Rule. In a grouped const or var declaration, the group’s doc comment is written once, above the first spec, and each spec’s own doc comment is written above that spec. An ungrouped declaration takes its declaration’s doc.

// These are the errors that can be returned in [ParseError.Err].
var (
	ErrBareQuote  = errors.New("bare \" in non-quoted-field")
	ErrQuote      = errors.New("extraneous or missing \" in quoted-field")
	ErrFieldCount = errors.New("wrong number of fields")

	// Deprecated: ErrTrailingComma is no longer used.
	ErrTrailingComma = errors.New("extra delimiter at end of line")
)
// These are the errors that can be returned in [ParseError.Err].
public static error ErrBareQuote = errors.New("bare \" in non-quoted-field"u8);
public static error ErrQuote = errors.New("extraneous or missing \" in quoted-field"u8);
public static error ErrFieldCount = errors.New("wrong number of fields"u8);

// Deprecated: ErrTrailingComma is no longer used.
public static error ErrTrailingComma = errors.New("extra delimiter at end of line"u8);

The same file shows the group rule above: the blank line Go has before ErrTrailingComma’s doc is kept.

A named type that is not a struct or interface

Rule. The doc comment of a named numeric, slice, map, channel, pointer or function type is written above its C# declaration, as a struct’s is.

// Convenience types for common cases

// IntSlice attaches the methods of Interface to []int, sorting in increasing order.
type IntSlice []int
// Convenience types for common cases

// IntSlice attaches the methods of Interface to []int, sorting in increasing order.
partial struct IntSlice /*[]nint*/;

/*[]nint*/ is the converter’s note of the underlying type; the source generators complete the struct (marker comments). The comment above it, which Go leaves free, keeps its place and its blank line.

A function type with no methods has no C# declaration: it is written inline as its delegate type wherever it is used. Its doc is written directly above the converter’s line that says so:

// scanning lines, a [SplitFunc] can return (0, nil, nil) to signal the
// [Scanner] to read more data into the slice and try again with a
// longer slice starting at the same point in the input.
//
// The function is never called with an empty data slice unless atEOF
// is true. If atEOF is true, however, data may be non-empty and,
// as always, holds unprocessed text.
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
// scanning lines, a [SplitFunc] can return (0, nil, nil) to signal the
// [Scanner] to read more data into the slice and try again with a
// longer slice starting at the same point in the input.
//
// The function is never called with an empty data slice unless atEOF
// is true. If atEOF is true, however, data may be non-empty and,
// as always, holds unprocessed text.
// type SplitFunc is a methodless func type — rendered inline as its base delegate

A type alias’s doc, above its global using

Rule. A Go type alias becomes a global using line at the top of the file, above namespace. Its doc comment is written directly above that line, at no indentation, not where Go declared the alias. One blank line sets it apart from the file’s header comments or from a previous using line.

// PathError records an error and the operation and file path that caused it.
type PathError = fs.PathError
// PathError records an error and the operation and file path that caused it.
global using PathError = go.io.fs_package.PathError;

namespace go;

In the file, the Go source’s header comment sits above these lines, one blank line away.

Limits. An alias of an anonymous struct or interface is converted as a lifted type, which takes the doc; its global using line then has none.

Comments inside a body

A trailing comment stays on its statement’s line

Rule. A comment written on the same source line as a statement’s end is written after one space on that statement’s last emitted C# line.

	if old == new || n == 0 {
		return s // avoid allocation
	}
    if (old == @new || n == 0) {
        return s; // avoid allocation
    }

Details.

Why. A trailing comment that waited for the next statement would have none to lead after a block’s last statement, and would be written after the block had closed, outside the construct it describes.

A comment after a block’s { trails the {

Rule. A one-line comment after the { of an if, else, for or switch header is written on the C# line that ends with that {. Here, from the bidi package of golang.org/x/text, which Go vendors into its own tree:

			if t == L {
				// no change
			} else if t == R {
				s.resolvedLevels[i] += 1
			} else { // t == AN || t == EN
				s.resolvedLevels[i] += 2
			}
            if (t == L){
                // no change
            } else 
            if (t == R){
                s.resolvedLevels[i] += 1;
            } else { // t == AN || t == EN
                s.resolvedLevels[i] += 2;
            }

The converter writes else if as } else and then if (…){ on the next line.

Limits. A function body’s { is followed by the prelude, so a comment after it stays on its own line below. So does a comment after a { that the converter follows with its own text, and a multi-line block comment.

On a case line, and above one

Rule. A one-line comment on a case X: or default: line trails that clause’s C# header line. Comments on their own lines above a case label are written above that clause’s C# header, at its indentation. Both hold in every lowered form: a C# switch, the if/else chain a switch can become, a type switch and select.

		switch RuneLen(v) {
		case 1: // normal rune
			a[n] = uint16(v)
			n++
		case 2: // needs surrogate sequence
			r1, r2 := EncodeRune(v)
        switch (RuneLen(v)) {
        case 1: { // normal rune
            a[n] = (uint16)v;
            n++;
            break;
        }
        case 2: { // needs surrogate sequence
            var (r1, r2) = EncodeRune(v);

Above a label:

	switch {
	case exp == 0:
		// an integer.
		return f, true
	// Exact integers are <= 10^15.
	// Exact powers of ten are <= 10^22.
	case exp > 0 && exp <= 15+22: // int * 10^k
    switch (ᐧ) {
    case {} when exp is 0: {
        // an integer.
        return (f, true);
    }
    // Exact integers are <= 10^15.
    // Exact powers of ten are <= 10^22.
    case {} when exp > 0 && exp <= 15 + 22: { // int * 10^k

ᐧ is golib’s true: a Go switch with no tag becomes switch (ᐧ) with case {} when … clauses.

When the if/else lowering emits a leading default: last, the default’s own comments are held and written with it; a comment at or left of its label’s column documents the next case and stays above that case.

Opening a case body, or alone in an empty block

Rule. A comment that leads a case body stays directly above the body’s first statement. A comment that is an empty clause’s whole body stays inside that clause, and a comment alone in an empty block stays in that block.

	switch {
	case 0 <= r && r < surr1, surr3 <= r && r < surrSelf:
		// normal rune
		return append(a, uint16(r))
	case surrSelf <= r && r <= maxRune:
		// needs surrogate sequence
		r1, r2 := EncodeRune(r)
		return append(a, uint16(r1), uint16(r2))
    switch (ᐧ) {
    case {} when (0 <= r && r < surr1) || (surr3 <= r && r < surrSelf): {
        // normal rune
        return append(a, (uint16)r);
    }
    case {} when surrSelf <= r && r <= maxRune: {
        // needs surrogate sequence
        var (r1, r2) = EncodeRune(r);
        return append(a, (uint16)r1, (uint16)r2);

An empty block:

		if b == '\r' {
			d.buf.WriteByte('\n')
		} else if b1 == '\r' && b == '\n' {
			// Skip \r\n--we already wrote \n.
		} else {
			d.buf.WriteByte(b)
		}
        if (b == (rune)'\r'){
            d.buf.WriteByte((rune)'\n');
        } else 
        if (b1 == (rune)'\r' && b == (rune)'\n'){
            // Skip \r\n--we already wrote \n.
        } else {
            d.buf.WriteByte(b);
        }

Before a block’s closing brace

Rule. Comments after a block’s last statement are written on their own lines above its }, inside the block, keeping a blank line Go has above them. In a case body, comments after its last statement that are indented deeper than the label stay at the end of that body.

			it.key = key
			it.elem = elem
			return
		}

		// Continue to next table.
	}
            it.key = key;
            it.elem = elem;
            return;
        }

        // Continue to next table.
    }

The comment closes a for body in the map iterator of internal/runtime/maps, with the blank line Go leaves above it.

Limits. Blocks the converter writes outside the ordinary block path, such as the outer block that holds an if statement’s init clause, do not take this rule.

Comments inside expressions

In a multi-line composite literal, or argument list

Rule. In a list that spans lines, a literal’s elements or a call’s arguments, a comment on the line where an element or the opening bracket ends trails that line. Any other comment takes its own line at element indentation, keeping a blank line Go has above it. Comments after a literal’s last element are written above its closing }, and a line comment forces that } onto its own line. A list takes only the comments after its own opening bracket.

Trailing an element:

var unitMap = map[string]uint64{
	"ns": uint64(Nanosecond),
	"us": uint64(Microsecond),
	"µs": uint64(Microsecond), // U+00B5 = micro symbol
	"μs": uint64(Microsecond), // U+03BC = Greek letter mu
[GoLazy] partial class lazyᴛunitMap { internal static map<@string, uint64> unitMap = new map<@string, uint64>{
    ["ns"u8] = (uint64)(int64)ΔNanosecond,
    ["us"u8] = (uint64)(int64)Microsecond,
    ["µs"u8] = (uint64)(int64)Microsecond, // U+00B5 = micro symbol
    ["μs"u8] = (uint64)(int64)Microsecond, // U+03BC = Greek letter mu

unitMap is pure data, so its declaration sits inside a [GoLazy] holder class and is built on its first read. ΔNanosecond is Nanosecond with the Δ prefix go2cs gives a name that Time’s Nanosecond() method also uses in the package class.

After the last element, above the closing bracket:

			rows = &Rows{
				dc:    dc,
				rowsi: rowsi,
				// releaseConn set below
			}
                Ꮡrows.ValueSlot = Ꮡ(new Rows(
                    dc: dc,
                    rowsi: Ꮡrowsi.ValueSlot
                    // releaseConn set below
                ));

Ꮡ(…) is golib’s address-of, Go’s &. The sample sits in a closure that Stmt.QueryContext passes to retry, and rows and rowsi are locals of QueryContext that the closure assigns. So each lives in a heap box, and the closure writes the box’s ValueSlot (see Function Values and Closures).

Limits. A package-level const or var writes the comments before it above its doc, and the comments inside its value where the literal reaches them. Any the emission does not reach are still written above the doc, so they are misplaced but never lost.

Between operands and call arguments

Rule. A comment between a binary expression’s operands, or between a call’s arguments, is written at its Go position. Where Go breaks the line after an operator, the C# breaks there too, with the comment trailing the operator and the right operand on the next line. A /* … */ inside a line stays inline at its position.

func appendLines(x, y []byte) []byte {
	if len(y) > 0 && isNL(y[0]) && // y starts in blank line
		(len(x) == 0 || len(x) >= 2 && isNL(x[len(x)-1]) && isNL(x[len(x)-2])) { // x is empty or ends in blank line
		y = y[1:] // delete y's leading blank line
	}
	return append(x, y...)
}
internal static slice<byte> appendLines(slice<byte> x, slice<byte> y) {
    if (len(y) > 0 && isNL(y[0]) && // y starts in blank line
        (len(x) == 0 || len(x) >= 2 && isNL(x[len(x) - 1]) && isNL(x[len(x) - 2]))) { // x is empty or ends in blank line
        y = y[1..]; // delete y's leading blank line
    }
    return appendꓸꓸꓸ(x, y);
}

appendꓸꓸꓸ(x, y) is Go’s append(x, y...). Inline between call arguments:

		clientCipher = hs.suite.cipher(clientKey, clientIV, false /* not for reading */)
        clientCipher = (~hs.suite).cipher(clientKey, clientIV, false /* not for reading */);

~ is golib’s pointer dereference.

A + chain of string literals that carries comments and folds to one byte-array constant is written one Go operand per line, each line’s bytes followed by its comment.

Limits: where a comment still lands elsewhere

A comment is placed only when the converter writes the construct it sits in through one of the points above. A path that renders an expression some other way leaves the comment for the fallback, which writes it on its own line at the next place it can. The comment is then misplaced, never lost:

			if p < 0 /* m <= 0.25 */ || p == 0 && x.mant.sticky(uint(len(x.mant))*_W-1) == 0 /* m == 0.5 */ {
				// underflow to ±0
            if (p < 0 /* m <= 0.25 */ || p == 0 && x.mant.sticky((nuint)len(x.mant) * (nuint)_W - 1) == 0) {
                /* m == 0.5 */
                // underflow to ±0

A few comments are still dropped, mostly trailing comments on specs and doc comments on fields in shapes the converter writes through another path. The census below counts them per class, so this page names no number.

Guarded by


← Manually-Converted Declarations · Index · Deterministic Output →