Skip to content

Latest commit

 

History

History
200 lines (148 loc) · 6.01 KB

File metadata and controls

200 lines (148 loc) · 6.01 KB

Generics

So supports two forms of generic functions: extern declarations and inline macros. Both are very limited and usually not needed.

Generic extern declarations

Generic type parameters are translated to C macro arguments, prepended before the regular arguments.

Generic extern functions:

//so:extern
func newObj[T any]() *T {
    return nil
}

//so:extern
func freeObj[T any](ptr *T) {
}

Calling with explicit or inferred type arguments:

v := newObj[int]()  // newObj(so_int)
freeObj(v)          // freeObj(so_int, v) - type argument inferred

Generic extern types:

//so:extern
type Map[K comparable, V any] struct {
    // ...
}

Generic extern methods:

//so:extern
func (m *Map[K, V]) Len() int {
    return m.len
}

Method calls prepend the receiver's type arguments:

// go
m := newMap[string, int](10)
l := m.Len()
// c
main_Map m = newMap(so_String, so_int, 10);
so_int l = main_Map_Len(so_String, so_int, &m);

On the C side, generic functions and methods should be manually implemented as macros that receive the type arguments:

#define newObj(T) (alloca(sizeof(T)))
#define freeObj(T, ptr) ((void)(ptr))

#define newMap(K, V, size) ((main_Map){0})
#define main_Map_Len(K, V, m) ((m)->len)

typedef struct {
    // ...
} main_Map;

Constraints (any, comparable, etc.) are used only for Go type-checking and are not emitted in C.

Non-extern generic types are not supported.

Generic inline macros

When //so:inline is applied to a generic function, the transpiler automatically generates a C #define macro instead of a static inline function. This is the primary mechanism for writing type-generic code directly in So without hand-writing C macros.

//so:inline
func identity[T any](val T) T {
    return val
}

Produces:

#define identity(T, val_) ({ \
    (val_); \
})

The macro is emitted in the .h file so it is available to all translation units.

How it works. The transpiler:

  1. Collects all type parameters and prepends them as macro parameters.
  2. Appends a _ suffix to each non-type parameter name to avoid collisions with struct fields and other identifiers (e.g. val becomes val_).
  3. Wraps references to those parameters in parentheses ((val_)) to prevent operator-precedence bugs when expressions are passed as arguments.
  4. Uses a GCC/Clang statement expression ({ ... }) for functions that return a value, or do { ... } while (0) for void functions.
  5. Translates return expr into a bare expr; (the last expression in a statement expression becomes its value).

Generic methods work the same way. The receiver's type parameters are included, and the receiver itself becomes a macro parameter:

//so:extern
type Box[T any] struct {
    val T
}

//so:inline
func (b *Box[T]) set(val T) {
    b.val = val
}

Produces:

#define main_Box_set(T, b_, val_) do { \
    (b_)->val = (val_); \
} while (0)

Call-site translation. Generic calls pass the resolved C type as the first argument:

x := identity(42)       // identity(so_int, 42)
x := identity[int](42)  // identity(so_int, 42)

Type arguments can be explicit or inferred by the Go type checker - the C output is the same.

Restrictions

No early returns. A return inside a macro body does not return from the macro - it becomes a bare expression. Multiple returns (e.g. if ... { return x } return y) will not work correctly. Structure the logic so there is exactly one return at the end, or use local variables and conditionals to compute the result.

Argument evaluation. Arguments are not automatically assigned to temporaries. If an argument with side effects (e.g. i++ or a function call) is referenced more than once in the macro body, it will be evaluated multiple times. Assign arguments to local variables at the top of the body to avoid this (see recommended practices below).

No defer. Deferred calls are not supported inside macro bodies.

No control flow that escapes the macro. break, continue, and goto in the caller's context cannot be used from within a macro body.

Recommended practices

Prefix local variables with underscore. The transpiler does not add a hygiene prefix to variables declared inside the macro body. If a local variable has the same name as one in the caller's scope, it will shadow or conflict. Using an underscore prefix (e.g. _result, _key) greatly reduces collision risk:

//so:inline
func increment[T int](n T) T {
    _n := n        // copy argument to local to avoid double evaluation
    _n = _n + 1
    return _n
}

Copy arguments to locals. If a parameter is used more than once, assign it to a _-prefixed local at the top. This prevents double evaluation and makes the macro behave like a function:

//so:inline
func (m *Map[K, V]) Has(key K) bool {
    _key := key    // evaluated once
    _m := m.bm
    // ... use _key and _m throughout ...
}

Keep macros short. Because every call site is expanded inline, large macros increase binary size and compile time. If a function is longer than ~15 lines, consider moving the heavy logic into a regular (non-generic) helper and using the macro only as a thin typed wrapper.

Avoid macro call chains. When one inline macro calls another, and that one calls a third, the preprocessor expands everything at the call site into a single deeply nested expression. This produces unreadable compiler errors, makes debugging nearly impossible, and can hit compiler limits. If you need a chain of calls, make the intermediate functions regular (non-inline) and only use //so:inline on the outermost typed wrapper.

Single return at the end. Structure the function so there is one return as the last statement:

// Good - single return at the end
//so:inline
func max[T int](a, b T) T {
    _a := a
    _b := b
    _r := _a
    if _b > _a {
        _r = _b
    }
    return _r
}

// Bad - multiple returns (won't work correctly as a macro)
//so:inline
func max[T int](a, b T) T {
    if a > b {
        return a
    }
    return b
}