Skip to content

feat: Implement positional function calls and call frame management#79

Merged
kwakker35 merged 3 commits into
mainfrom
feat/functions-and-call-frames
Jun 21, 2026
Merged

feat: Implement positional function calls and call frame management#79
kwakker35 merged 3 commits into
mainfrom
feat/functions-and-call-frames

Conversation

@kwakker35

@kwakker35 kwakker35 commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

This pull request finalizes the design decision for dedicated error codes for named-argument call-site diagnostics, integrates the new decision into the authoritative log, and updates the compiler to support function declarations, calls, and recursion as specified for Sprint 5 Increment A. It also clarifies the runtime callee resolution mechanism and mutual recursion handling in both the design documentation and planning prompts.

Design and Documentation Updates:

  • Added decision D-318 to grob-decisions-log.md, assigning dedicated error codes (E0008–E0011) for the four named-argument call-site errors, with detailed rationale, summary index, and changelog entry. [1] [2] [3]
  • Removed the temporary drop-in block for D-318 (D-318-drop-in.md), as the decision is now fully integrated into the canonical log.

Compiler Implementation (Sprint 5 Increment A):

  • Implemented function declaration and return statement compilation in Compiler.Statements.cs, including the creation of BytecodeFunction, parameter slot assignment, and global binding.
  • Implemented call expression compilation in Compiler.Expressions.cs, emitting the callee, arguments, and the Call opcode with argument count.
  • Updated type inference for call expressions to resolve user function return types.

Planning and Spec Clarifications:

  • Updated planning prompts and design notes to clarify that callee resolution for top-level functions is always by global slot at runtime, enabling both forward and mutual recursion without special handling. This is reflected in both the current and archived Sprint 5A planning documents. [1] [2] [3]

These changes ensure that the language's function call and recursion semantics match the documented design, and that error diagnostics for named-argument calls are precise and actionable.

Summary by CodeRabbit

Release Notes

  • New Features
    • User-defined functions now compile into bytecode-backed callable values.
    • Positional calls validate argument counts/types and propagate the function’s return type into typed expressions.
    • Runtime call/return with stack frames enables forward references and mutual recursion, including correct local handling.
    • Function-level return is type-checked against the declared return type.
  • Documentation
    • Updated Sprint 5 call/return and callee-resolution semantics; confirmed named-argument call error codes E0008–E0011.
  • Tests
    • Expanded compiler, type-checker, VM, and end-to-end integration coverage for calls, recursion, and error cases.

kwakker35 and others added 2 commits June 21, 2026 17:18
Implements Sprint 5 Increment A: fn declarations compile to their
own bytecode, calls and returns run over a call-frame stack, and
recursion works within a bounded depth.

Core gains BytecodeFunction (a GrobFunction carrying its own Chunk);
GrobFunction becomes abstract, so the Core tests that constructed it
directly now build a BytecodeFunction. The VM gains a CallFrame array
and frame-relative local addressing: GetLocal/SetLocal (and the
increment/decrement arms) resolve slots against the active frame's
stack base. The script runs at base zero, so existing top-level
locals are unaffected. Call saves the caller's resume context and
switches into the callee's chunk; Return discards the callee value,
its arguments and locals in one step, then pushes the result.

The frames array holds call frames only (the script is not a frame),
so a 257th nested call overflows a 256-entry array and raises E5901
as a clean RuntimeError rather than a host CLR stack overflow.
Implements D-180 (call-stack overflow), D-166 (forward references
via pass-1 registration) and the positional path of D-113.

The type checker validates fn bodies and positional call sites:
argument count (E0003), argument types (E0004) and return-type
mismatch (E0005), plus top-level return (E2203). All reuse existing
ErrorCatalog descriptors; no new codes. A call resolves to the
function's declared return type so surrounding operators select the
right typed opcode.

All-paths-return is not a v1 rule and no error code is assigned, so
the compiler emits an implicit Nil + Return safety net at the end of
every body (control fall-off returns nil) rather than inventing a
check. No new opcodes, no parser or AST edits.

Tests: VM call/recursion/E5901, type-checker diagnostics and the
section 3.1.1 invariant, compiler emission shape, and end-to-end
recursion (factorial, fibonacci). Affected-file line coverage ~96%;
the one new uncovered line is a defensive parameter-slot-overflow
guard, matching the existing local-slot-overflow guard convention.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Integrates the D-318 drop-in into grob-decisions-log.md in
four-location lockstep: summary-index row, full entry, and footer
changelog line. D-318 assigns the four named-argument call-site
errors from D-113 dedicated codes E0008-E0011 (named-before-
positional, naming a required parameter, duplicate named argument,
unknown parameter name) rather than folding them into E0003, since
none is an arity error and each carries a distinct fix.

No error codes are registered by this entry — Sprint 5 Increment B
adds the E0008-E0011 descriptors to the catalog. D-318 must exist
before that increment runs, which is why it lands now.

Removes docs/design/_pending/D-318-drop-in.md: it was an explicit
transient staging block ("NOT an integrated file") whose content is
now in the canonical log, so keeping it would duplicate the D-318
prose. Consistency gate (D-316 lockstep) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kwakker35 kwakker35 self-assigned this Jun 21, 2026
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3bf9b5bf-80ef-4917-9e2a-2d36190728df

📥 Commits

Reviewing files that changed from the base of the PR and between db1c778 and b2f236a.

📒 Files selected for processing (5)
  • src/Grob.Compiler/TypeChecker.Expressions.cs
  • src/Grob.Compiler/TypeChecker.Statements.cs
  • tests/Grob.Compiler.Tests/CompilerFunctionTests.cs
  • tests/Grob.Compiler.Tests/TypeCheckerFunctionTests.cs
  • tests/Grob.Vm.Tests/VirtualMachineCallTests.cs
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/Grob.Compiler/TypeChecker.Statements.cs
  • tests/Grob.Vm.Tests/VirtualMachineCallTests.cs
  • tests/Grob.Compiler.Tests/TypeCheckerFunctionTests.cs
  • src/Grob.Compiler/TypeChecker.Expressions.cs
  • tests/Grob.Compiler.Tests/CompilerFunctionTests.cs

📝 Walkthrough

Walkthrough

Sprint 5 Increment A implements end-to-end user-defined function support: GrobFunction becomes an abstract base with BytecodeFunction as the concrete subclass carrying a compiled Chunk. The compiler gains fn/return/call emission, the type-checker validates positional calls and return types with dedicated argument-count and argument-type checks, and the VM gains a call-frame array with stack-base-relative locals and unbounded-recursion protection. Pending design decision D-318 (E0008–E0011 for named-argument errors) is merged into the main decisions log.

Changes

Sprint 5A: Function declaration, call, and return

Layer / File(s) Summary
GrobFunction abstraction and BytecodeFunction type
src/Grob.Core/GrobFunction.cs, src/Grob.Core/BytecodeFunction.cs, tests/Grob.Core.Tests/RuntimeTypesTests.cs, tests/Grob.Core.Tests/GrobValueTests.cs
GrobFunction becomes abstract with a protected constructor; BytecodeFunction is the new sealed concrete subclass holding a Chunk. Existing core tests are updated to construct BytecodeFunction instances.
CallFrame struct, ValueStack.TrimToCount, and VM frame fields
src/Grob.Vm/CallFrame.cs, src/Grob.Vm/ValueStack.cs, src/Grob.Vm/VirtualMachine.cs
CallFrame struct stores the three resume-context fields for Return. ValueStack.TrimToCount discards a callee's frame region. VirtualMachine adds MaxFrames = 256, a _frames array, _frameCount, and a stackBase local initialised on each Run.
VM Call/Return opcode dispatch and stack-base-relative locals
src/Grob.Vm/VirtualMachine.cs
GetLocal/SetLocal and int inc/dec use stackBase + slot. Call saves caller context to _frames, validates the target is a BytecodeFunction, and switches to the callee chunk; Return restores caller context or exits when no frames remain.
Compiler const-cache sharing for nested compilation
src/Grob.Compiler/Compiler.cs
_constValues moves from inline initialisation to constructor assignment; a new private constructor accepts an existing cache so sub-compilers for function bodies share the root compiler's const results.
Compiler fn declaration, call, and return emission
src/Grob.Compiler/Compiler.Statements.cs, src/Grob.Compiler/Compiler.Expressions.cs
VisitFnDecl compiles FnDecl into a BytecodeFunction constant bound via DefineGlobal. CompileFunction allocates parameter locals and appends an implicit Nil+Return. VisitReturn emits the value or Nil then Return. VisitCall emits callee, arguments, and OpCode.Call with the argument count. GetExprType resolves call result type from the callee's FnDecl.ReturnType.
TypeChecker call validation and return-type tracking
src/Grob.Compiler/TypeChecker.cs, src/Grob.Compiler/TypeChecker.Declarations.cs, src/Grob.Compiler/TypeChecker.Expressions.cs, src/Grob.Compiler/TypeChecker.Statements.cs
_functionReturnTypes stack is added; ResolveTypeRef is widened to internal static. VisitFnDecl pushes/pops the return type around the body. VisitCall validates user-defined callees via CheckPositionalCall (argument count / E0003) and CheckArgumentType (per-argument assignability / E0004), returning the resolved return type; undefined callees remain permissive. VisitReturn emits E2203 at script level and E0005 on return-type mismatch in function scope.
Compiler and TypeChecker function tests
tests/Grob.Compiler.Tests/CompilerFunctionTests.cs, tests/Grob.Compiler.Tests/TypeCheckerFunctionTests.cs
CompilerFunctionTests covers fn bytecode shape, parameter slot ordering, implicit/bare/explicit return emission, call instruction ordering, and typed opcode selection. TypeCheckerFunctionTests covers E0003/E0004/E0005/E2203 diagnostics with range assertions, multi-error collection, annotation invariants, and pathological-input regression.
VM call/return dispatch tests
tests/Grob.Vm.Tests/VirtualMachineCallTests.cs
Four hand-crafted bytecode tests: positional argument call, stack cleanup on return, direct recursive factorial, and unbounded recursion raising E5901.
End-to-end Sprint 5A integration tests
tests/Grob.Integration.Tests/Sprint5IncrementATests.cs
Seven full-pipeline facts covering simple call/return, call result in expression, forward reference, recursive factorial and Fibonacci, local variables in functions, and repeated calls to a shared global function.
Sprint 5A spec and D-318 design decision
.claude/commands/sprint-5-a.md, prompts/archive/sprint-5/sprint-5-a.md, docs/design/grob-decisions-log.md
Spec docs clarify late global-slot callee resolution and mutual recursion semantics; the pending D-318 file is removed and its content (E0008–E0011 for named-argument call-site errors) is merged into the main decisions log with June 2026 footer update.

Sequence Diagram(s)

sequenceDiagram
  participant Source as Source text
  participant TypeChecker as TypeChecker
  participant Compiler as Compiler
  participant VM as VirtualMachine

  Source->>TypeChecker: VisitFnDecl — push return type, visit body, pop
  TypeChecker->>TypeChecker: VisitCall — CheckPositionalCall (E0003), CheckArgumentType (E0004)
  TypeChecker->>TypeChecker: VisitReturn — E2203 at script level, E0005 on mismatch
  Source->>Compiler: VisitFnDecl → CompileFunction → BytecodeFunction constant → DefineGlobal
  Source->>Compiler: VisitCall → callee + arguments + OpCode.Call(argCount)
  Source->>Compiler: VisitReturn → value or Nil + OpCode.Return
  Compiler->>VM: Chunk with BytecodeFunction constants and Call/Return opcodes
  VM->>VM: OpCode.Call — save CallFrame, update stackBase, swap chunk
  VM->>VM: GetLocal/SetLocal at stackBase+slot
  VM->>VM: OpCode.Return — TrimToCount(calleeBase), push result, restore CallFrame
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • grob-lang/grob#41: Directly modifies the same VirtualMachine.cs dispatch loop and ValueStack.cs infrastructure; this PR's call-frame semantics and TrimToCount are layered on that foundation.

Poem

🖥️ A function once lived with no frame of its own,
No stack base to stand on, no place to call home.
Now Call saves the context, Return trims the pile,
BytecodeFunction strides in with bytecode and style.
Global slots keep the recursion alive —
Mutual isEven/isOdd, both manage to thrive! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows the Conventional Commits format with 'feat' prefix and a clear, specific summary of the main change.
Docstring Coverage ✅ Passed Docstring coverage is 48.78% which is sufficient. The required threshold is 10.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/functions-and-call-frames

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/Grob.Compiler/TypeChecker.Expressions.cs (1)

243-275: 💤 Low value

Consider extracting argument-type validation to reduce cognitive complexity.

SonarCloud flags cognitive complexity at 17 (threshold 15). The logic is clear and linear, but if this method grows further (e.g., named arguments in Increment B), extracting the argument-count check and the per-argument type check into private helpers would keep it maintainable.

Not blocking for this increment — the current structure is readable and not on a hot path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Grob.Compiler/TypeChecker.Expressions.cs` around lines 243 - 275, The
VisitCall method has high cognitive complexity (17, exceeding the threshold of
15) due to nested validation logic. Extract the argument-count validation (the
if statement comparing node.Arguments.Count to expected) into a private helper
method, and extract the per-argument type checking logic (the for loop that
validates each argument against the parameter type using TypesAreAssignable)
into another private helper method. These helpers should encapsulate the
EmitError calls and return early from VisitCall if validation fails, reducing
the nesting depth and overall complexity within VisitCall while keeping the type
checking logic organized and reusable.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Grob.Compiler/TypeChecker.Statements.cs`:
- Around line 57-68: The type checker currently only validates return statements
that have a value (node.Value is not null) but skips validation for bare return
statements (node.Value is null). Add an else clause after the existing if block
to handle bare returns: when node.Value is null, check if the expected return
type (from _functionReturnTypes.Peek()) is not Unknown and not Error, and if so,
emit an error using EmitError with ErrorCatalog.E0005 indicating that a bare
return is not allowed in a function that declares a non-void return type. Use
node.Range for the error location.

In `@tests/Grob.Vm.Tests/VirtualMachineCallTests.cs`:
- Around line 203-205: The test assertion for the E5901 error path is incomplete
and only verifies the error code through ex.Code. You need to add additional
Assert.Equal() statements to verify the exact line and column coordinates of the
GrobRuntimeException by asserting ex.Line and ex.Column with their expected
values using equality assertions to catch any off-by-one regressions in
diagnostic coordinate reporting.

---

Nitpick comments:
In `@src/Grob.Compiler/TypeChecker.Expressions.cs`:
- Around line 243-275: The VisitCall method has high cognitive complexity (17,
exceeding the threshold of 15) due to nested validation logic. Extract the
argument-count validation (the if statement comparing node.Arguments.Count to
expected) into a private helper method, and extract the per-argument type
checking logic (the for loop that validates each argument against the parameter
type using TypesAreAssignable) into another private helper method. These helpers
should encapsulate the EmitError calls and return early from VisitCall if
validation fails, reducing the nesting depth and overall complexity within
VisitCall while keeping the type checking logic organized and reusable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 911fed68-7c6d-48e4-acc9-ad7952ac39db

📥 Commits

Reviewing files that changed from the base of the PR and between da71a38 and db1c778.

📒 Files selected for processing (22)
  • .claude/commands/sprint-5-a.md
  • docs/design/_pending/D-318-drop-in.md
  • docs/design/grob-decisions-log.md
  • prompts/archive/sprint-5/sprint-5-a.md
  • src/Grob.Compiler/Compiler.Expressions.cs
  • src/Grob.Compiler/Compiler.Statements.cs
  • src/Grob.Compiler/Compiler.cs
  • src/Grob.Compiler/TypeChecker.Declarations.cs
  • src/Grob.Compiler/TypeChecker.Expressions.cs
  • src/Grob.Compiler/TypeChecker.Statements.cs
  • src/Grob.Compiler/TypeChecker.cs
  • src/Grob.Core/BytecodeFunction.cs
  • src/Grob.Core/GrobFunction.cs
  • src/Grob.Vm/CallFrame.cs
  • src/Grob.Vm/ValueStack.cs
  • src/Grob.Vm/VirtualMachine.cs
  • tests/Grob.Compiler.Tests/CompilerFunctionTests.cs
  • tests/Grob.Compiler.Tests/TypeCheckerFunctionTests.cs
  • tests/Grob.Core.Tests/GrobValueTests.cs
  • tests/Grob.Core.Tests/RuntimeTypesTests.cs
  • tests/Grob.Integration.Tests/Sprint5IncrementATests.cs
  • tests/Grob.Vm.Tests/VirtualMachineCallTests.cs
💤 Files with no reviewable changes (1)
  • docs/design/_pending/D-318-drop-in.md

Comment thread src/Grob.Compiler/TypeChecker.Statements.cs
Comment thread tests/Grob.Vm.Tests/VirtualMachineCallTests.cs
Addresses PR #79 review (CodeRabbit + SonarCloud).

A bare `return` yields nil. Since `void` is not a user-declarable
return type in Grob (only print() is void), a bare return must still
satisfy the declared type: it now reports E0005 against a
non-nullable return type and is accepted only by a nullable or nil
one. Previously a value-less return skipped the check, so an int
function could silently return nil. Uses the existing E0005 — no new
code. Adds type-checker tests for both directions.

Extracts the positional-call validation in VisitCall into
CheckPositionalCall and CheckArgumentType helpers, dropping the
method's cognitive complexity below the SonarCloud threshold of 15
(was 17 — the one new Sonar issue on this PR). No behaviour change.

Asserts the full diagnostic contract (Line and Column, equality) on
the E5901 stack-overflow VM test, per the test guidelines. Updates
the bare-return compiler emission test to a nullable return type so
its source type-checks under the tightened rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kwakker35

Copy link
Copy Markdown
Collaborator Author

Addressed all review feedback in b2f236a:

CodeRabbit — bare return not type-checked (major): Fixed. A bare return now yields nil and is validated against the declared type — E0005 for a non-nullable return type, accepted by a nullable/nil one. void is not user-declarable in Grob (only print() is void), so there's no void-function case to exempt. Added tests for both directions.

CodeRabbit — E5901 test diagnostic coordinates (major): Fixed. The stack-overflow test now asserts ex.Line and ex.Column with equality (1 and 0 — no column for hand-built bytecode).

SonarCloud 1 new issue / CodeRabbit nitpick — VisitCall cognitive complexity 17 > 15: Fixed. Extracted CheckPositionalCall and CheckArgumentType helpers; no behaviour change. This was the single new Sonar issue.

Full suite green (Core 262, Compiler 712, Vm 216, Integration 158, Consistency 30); build clean with zero warnings.

@sonarqubecloud

Copy link
Copy Markdown

@kwakker35 kwakker35 merged commit b336696 into main Jun 21, 2026
19 checks passed
@kwakker35 kwakker35 deleted the feat/functions-and-call-frames branch June 21, 2026 21:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant