stride3d/stride · error · InvalidOperationException

Trying to end a function which was not started

Error message

Trying to end a function which was not started

What it means

EndFunction finalizes the SPIR-V function currently being built. It throws InvalidOperationException when CurrentFunction is null, i.e. EndFunction was called without a matching StartFunction — the builder's function stack is empty.

Solutions

  1. Ensure every EndFunction call is paired with exactly one earlier StartFunction.
  2. Guard the EndFunction call: only call it if CurrentFunction is not null.
  3. In error/finally paths, check builder state before finalizing.
  4. Log function start/end pairs during development to detect imbalance.

Example fix

// before
builder.EndFunction();
// after
if (builder.CurrentFunction is not null)
    builder.EndFunction();
Defensive patterns

Strategy: validation

Validate before calling

if (builder.CurrentFunction is null) return; // nothing to end

Type guard

bool canEndFunction = builder.CurrentFunction is not null;

Try / catch

try { builder.EndFunction(); } catch (InvalidOperationException ex) { /* unbalanced Start/EndFunction — audit emit pipeline */ throw; }

Prevention

When it happens

Trigger: Calling EndFunction() when no function was started, calling it twice for the same function, or an exception earlier in emission skipped StartFunction while cleanup still calls EndFunction.

Common situations: Unbalanced Start/End pairs in shader code generators, exception paths that partially emit a function, or refactored emit pipelines where a helper now starts the function conditionally.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14). Data as JSON: /api/errors/9235ae49d332db10. Report an issue: GitHub.

Appendix: source

Thrown at sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Builder.Functions.cs:32

        var func = context.Bound++;
        foreach (var t in ftype.ParameterTypes)
            context.GetOrRegister(t.Type);
        context.AddName(func, name);
        var result = new SpirvFunction(func, name, ftype) { IsStage = isStage };
        return result;
    }

    public void BeginFunction(SpirvContext context, SpirvFunction function, FunctionControlMask mask = FunctionControlMask.None)
    {
        Buffer.Add(new OpFunction(context.GetOrRegister(function.FunctionType.ReturnType), function.Id, mask, context.GetOrRegister(function.FunctionType)));
        Position = Buffer.Count;
        CurrentFunction = function;
    }

    public void EndFunction()
    {
        if (CurrentFunction is null)
            throw new InvalidOperationException("Trying to end a function which was not started");

        // If there was no explicit return, add one
        var lastInstruction = Buffer[Position - 1];
        if (!IsBlockTermination(lastInstruction.Op))
        {
            if (CurrentFunction.Value.FunctionType.ReturnType != ScalarType.Void)
                throw new InvalidOperationException("No function termination, but a return value is expected");

            Return(null);
        }

        Buffer.Insert(Position++, new OpFunctionEnd());
    }

    public SpirvValue EmitFunctionParameter(SpirvContext context, string name, SymbolType type)
    {
        var p = Buffer.Insert(Position++, new OpFunctionParameter(context.GetOrRegister(type), context.Bound++));
        context.AddName(p, name);

View on GitHub (pinned to 96fad776d2)