Skip to content

Constructors#19

Merged
vito merged 16 commits into
mainfrom
constructors
Feb 7, 2026
Merged

Constructors#19
vito merged 16 commits into
mainfrom
constructors

Conversation

@vito
Copy link
Copy Markdown
Owner

@vito vito commented Feb 6, 2026

This PR adds support for explicit constructor definitions in Dang using the new(...) { ... } syntax. This enables constructor arguments that are not automatically exposed as fields, which is essential for Dagger module serialization where some constructor inputs shouldn't be persisted.

Motivation

Previously, Dang derived constructors from public fields - constructor parameter names matched field names exactly. This caused issues with Dagger's serialization model:

  1. Constructor inputs that shouldn't be serialized (e.g., transient configuration) became fields
  2. No way to transform constructor inputs before storing them
  3. Constructor arg names were tightly coupled to field names

New Syntax

type Greeter {
  pub greeting: String!

  new(name: String!) {
    self.greeting = "Hello, " + name + "!"
  }
}

let g = Greeter("World")  # g.greeting == "Hello, World!"

Key features:

  • Constructor args are only available within the new() body
  • Args are NOT exposed as instance fields
  • new() implicitly returns self
  • Runtime error if non-null fields aren't assigned
  • new() is only valid inside class bodies (parse error elsewhere)

Changes

Grammar (pkg/dang/dang.peg)

  • Added NewConstructor rule: new(args) { block }
  • Added ClassExpr and ClassBlock to restrict new() to class bodies
  • Class rule now uses ClassBlock instead of generic Block

AST (pkg/dang/slots.go)

  • Added NewConstructorDecl struct with Args, BodyBlock, DocString, Loc
  • ClassDecl methods updated to find and handle NewConstructorDecl
  • findNewConstructor() and bodyFormsWithoutNew() helpers

Type Inference (pkg/dang/slots.go)

  • ClassDecl.Hoist: Uses new() args if present, otherwise derives from fields
  • ClassDecl.Infer: Separately infers new() body with args in scope
  • inferNewConstructor(): Adds constructor args to env before inferring body

Evaluation (pkg/dang/eval.go)

  • ConstructorFunction now has NewBody *Block field
  • ConstructorFunction.Call:
    • Evaluates field declarations (skipping required fields without defaults)
    • Binds constructor args in forked closure
    • Executes new() body with access to self and args
    • Validates all non-null fields were assigned
  • checkRequiredFields(): Runtime validation of field assignment

Dagger SDK (dagger-sdk/entrypoint/main.go)

  • Fixed object reconstruction from serialized state
  • Key fix: Bypass constructor when deserializing - directly set fields from JSON
  • Evaluates class body forms after setting fields (for computed properties)
  • SlotDecl.Eval's GetLocal check prevents defaults from clobbering loaded state

Backwards Compatibility

  • Types without new() continue to derive constructors from fields (existing behavior)
  • No breaking changes to existing Dang code

Tests

  • tests/test_explicit_constructor.dang: Comprehensive test cases
  • tests/errors/constructor_arg_in_method_body.dang: Verifies args scoped to new()
  • tests/errors/new_outside_class.dang: Verifies new() only valid in class bodies
  • mod/test-mismatch/: Tests constructor arg names differing from field names
  • mod/test-private-arg/: Updated to use new syntax

Example: Dagger Module

type Dang {
  pub source: Directory!

  new(
    source2: Directory! @defaultPath(path: "/")
  ) {
    self.source = source2
  }

  pub test: Test! {
    Test(self)
  }
}

Here source2 is the constructor arg (with Dagger directives), but source is the serialized field. The arg name doesn't need to match the field name.

vito added 5 commits February 7, 2026 12:54
arguments are available during type body evaluation, and NOT during
field evaluation; must be explicitly assigned as slots

Signed-off-by: Alex Suraci <suraci.alex@gmail.com>
Signed-off-by: Alex Suraci <suraci.alex@gmail.com>
Signed-off-by: Alex Suraci <suraci.alex@gmail.com>
Signed-off-by: Alex Suraci <suraci.alex@gmail.com>
Signed-off-by: Alex Suraci <suraci.alex@gmail.com>
vito added 6 commits February 7, 2026 13:10
Revert the ClassBlock/ClassExpr/ClassDecl grammar duplication from
cca0cdf. Parsing new() anywhere simplifies the grammar and allows
better error messages to be raised at a later phase.
NewConstructorDecl.Infer now returns a descriptive InferError with
source annotation instead of silently succeeding. Inside a class body,
Infer is never called directly (ClassDecl.inferNewConstructor handles
it), so the error only triggers for misplaced constructors.
The formatter was missing cases for NewConstructorDecl in formatNode,
nodeLocation, and isFunctionDef, causing format-then-reparse to fail
for files with explicit constructors.
Documents how to run tests, update error golden files, add new error
tests, and use the error reporting infrastructure.
Constructor arguments now follow the same formatting rules as field
definition arguments, including multiline splitting based on line
length, docstrings, and directives.
vito added 5 commits February 7, 2026 13:25
Move test-mismatch and test-private-arg from mod/ into
dagger-sdk/testdata/ and add corresponding test methods in the
DaggerSDKSuite. The old standalone modules are removed.
If a user writes `pub new(...): Foo! { ... }` inside a type body,
the parser treats it as a regular method named "new" and produces a
confusing type error. Now we check for slots named "new" during
class inference and emit a clear error pointing them to the correct
`new(...) { ... }` syntax.
These tests predated real constructors and used 'new' as a regular
field name, which now conflicts with constructor syntax. Migrated to
proper new() constructors or removed the field where unnecessary.
Verifies that fields can be assigned without 'self.' prefix when
constructor arg names don't shadow the field names.
@vito vito merged commit 27a2a88 into main Feb 7, 2026
6 checks passed
@vito vito deleted the constructors branch May 25, 2026 16:31
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