Skip to content

Latest commit

 

History

History
788 lines (599 loc) · 25.2 KB

File metadata and controls

788 lines (599 loc) · 25.2 KB

Hydra DSL Guide (Java)

This guide explains Hydra's domain-specific language (DSL) utilities for constructing types and terms in Java.

Status note (0.15+). The direct DSLs (hydra.dsl.Types, hydra.dsl.Terms, hydra.dsl.Literals, hydra.dsl.LiteralTypes, and the hydra.dsl.prims.* wrappers) are current and in wide use. The phantom-typed Java DSLhydra.dsl.meta.Phantoms plus library wrappers under hydra.dsl.meta.lib.* (Lists, Maps, Sets, Logic, Maths, Maybes, Strings, Literals) — is also current and is the foundation for the host-native Java coder sources at packages/hydra-java/src/main/java/hydra/sources/ (post-#344). The domain-specific DSLs sketched in §4 below (hydra.dsl.meta.Core, Graph, Compute) and the examples/ files in §"File reference" are aspirational — not present in the current codebase. For full kernel-source authoring, use the Haskell DSL (DSL Guide (Haskell)); the Java DSL covers Java-coder authoring only.

Note: Hydra provides DSLs in all five implementation languages (Haskell, Java, Python, Scala, and Lisp). This guide focuses on the Java DSLs. For the comprehensive Haskell DSL guide (including kernel development context), see DSL Guide (Haskell). For the Python DSLs, see DSL Guide (Python).

Prerequisites

Before using the DSL utilities, you should:

Table of Contents

  1. Overview
  2. The DSL variants
  3. When to use each variant
  4. Direct DSLs (Types and Terms)
  5. Phantom-typed DSL
  6. Domain-specific DSLs
  7. Library wrappers
  8. Type definitions
  9. Term definitions
  10. Common patterns
  11. Working with generated code
  12. Error handling
  13. Examples in the codebase

Overview

Hydra-Java provides a layered DSL system for working with Hydra types and terms:

Layer Module Purpose
Direct DSLs hydra.dsl.Types, hydra.dsl.Terms Raw construction of Type and Term instances
Phantom-typed DSL hydra.dsl.meta.Phantoms Compile-time type safety via TypedTerm<A> phantom types
Domain-specific DSLs hydra.dsl.meta.Core, hydra.dsl.meta.Graph, hydra.dsl.meta.Compute Typed accessors for Hydra kernel types
Library wrappers hydra.dsl.meta.Lib.* Typed wrappers around Hydra primitives (lists, sets, maps, etc.)

The Direct DSLs are suitable for casual use: constructing test fixtures, prototyping, or building types. The Phantom-typed and Domain-specific DSLs are designed for writing Hydra kernel source code in Java, mirroring the Haskell DSLs used in packages/hydra-haskell/src/main/haskell/Hydra/Sources/.

The DSL variants

1. Direct Types DSL

Module: hydra.dsl.Types

Constructs Type instances directly. Used for defining Hydra data types (records, unions, wrappers).

import hydra.dsl.Types;

Type personType = Types.record(
    Types.field("name", Types.string()),
    Types.field("age", Types.int32()));

2. Direct Terms DSL

Module: hydra.dsl.Terms

Constructs raw Term instances. Useful for test data and simple term construction.

import hydra.dsl.Terms;

Term person = Terms.record(new Name("Person"),
    Terms.field("name", Terms.string("Alice")),
    Terms.field("age", Terms.int32(30)));

3. Phantom-typed DSL

Module: hydra.dsl.meta.Phantoms

Wraps raw Term construction with TypedTerm<A> phantom types for compile-time type safety. The phantom type parameter A tracks the Hydra type at the Java level.

import static hydra.dsl.meta.Phantoms.*;

TypedTerm<String> greeting = string("hello");
TypedTerm<Integer> age = int32(30);
TypedTerm<Object> identity = lambda("x", var("x"));

4. Domain-specific DSLs (aspirational — not yet implemented)

Planned modules: hydra.dsl.meta.Core, hydra.dsl.meta.Graph, hydra.dsl.meta.Compute.

These would provide typed field accessors and constructors for Hydra kernel types, parallel to the Haskell Hydra.Dsl.Meta.Core / Hydra.Dsl.Meta.Graph modules. They do not currently exist in the Java codebase. The example below is provisional:

// Hypothetical — these classes don't exist yet:
import static hydra.dsl.meta.Core.*;

// Extract the body of a lambda term
TypedTerm<Object> body = lambdaBody(var("myLambda"));

// Construct a Lambda record
TypedTerm<Object> lam = lambda_(
    wrap(Term.TYPE_NAME, string("x")),
    nothing(),
    var("body"));

For now, use direct Terms.* constructors or the host-native sources at packages/hydra-java/src/main/java/hydra/sources/ as concrete examples of authoring Java coder DSL code with Phantoms only.

5. Library wrappers

Typed wrappers around Hydra primitive functions, providing phantom-typed interfaces to operations like set union, list fold, etc.

// Inline library helpers (or import from hydra.dsl.meta.Lib.Sets, etc.)
static <R> TypedTerm<R> setsUnion(TypedTerm<?> s1, TypedTerm<?> s2) {
    return primitive2(new Name("hydra.lib.sets.union"), s1, s2);
}

When to use each variant

Scenario Recommended DSLs Why
Defining Hydra types Direct Types DSL Constructs Type instances for type modules
Simple term construction Direct Terms DSL Quick and straightforward
Writing kernel source code Phantom-typed + Domain-specific Type safety + domain accessors
Field access on kernel types Domain-specific DSLs Core.lambdaBody(t) instead of manual projection
Primitive function calls Library wrappers setsUnion(a, b) instead of raw primitive2(...)

Rule of thumb:

  • Type modules (defining data types): Use hydra.dsl.Types with Types.record(), Types.union(), Types.wrap()
  • Term modules (defining functions): Use import static hydra.dsl.meta.Phantoms.* with domain DSLs
  • Quick prototyping: Use hydra.dsl.Terms directly

Direct DSLs (Types and Terms)

Constructing Types

import hydra.core.*;
import hydra.dsl.Types;

// Literal types
Type stringType = Types.string();
Type int32Type = Types.int32();
Type booleanType = Types.boolean_();

// Container types
Type stringList = Types.list(Types.string());
Type stringMap = Types.map(Types.string(), Types.int32());
Type maybeInt = Types.optional(Types.int32());
Type intSet = Types.set(Types.int32());

// Pair and either
Type pairType = Types.pair(Types.string(), Types.int32());
Type eitherType = Types.either_(Types.string(), Types.int32());

// Function type
Type fn = Types.function(Types.string(), Types.int32());

// Record type (anonymous)
Type person = Types.record(
    Types.field("name", Types.string()),
    Types.field("age", Types.int32()));

// Union type
Type shape = Types.union(
    Types.field("circle", Types.float64()),
    Types.field("rectangle", Types.pair(Types.float64(), Types.float64())));

// Wrapper type (newtype)
Type name = Types.wrap(Types.string());

// Type variable (forward reference)
Type selfRef = Types.variable("hydra.core.Term");

// Unit type
Type unit = Types.unit();

Constructing Terms

import hydra.core.*;
import hydra.dsl.Terms;

// Literals
Term hello = Terms.string("hello");
Term answer = Terms.int32(42);
Term flag = Terms.boolean_(true);

// Lists
Term numbers = Terms.list(Terms.int32(1), Terms.int32(2), Terms.int32(3));

// Records
Term person = Terms.record(new Name("Person"),
    Terms.field("name", Terms.string("Alice")),
    Terms.field("age", Terms.int32(30)));

// Lambdas
Term identity = Terms.lambda("x", Terms.var("x"));
Term add = Terms.lambda("x", Terms.lambda("y",
    Terms.apply(Terms.apply(Terms.primitive("hydra.lib.math.add"),
        Terms.var("x")), Terms.var("y"))));

// Application
Term applied = Terms.apply(identity, Terms.int32(42));

// Optional values
Term justVal = Terms.just(Terms.int32(42));
Term nothingVal = Terms.nothing();

// Let bindings
Term letExpr = Terms.let_("x", Terms.int32(5), Terms.var("x"));

// Union injection
Term circle = Terms.inject("Shape", "circle", Terms.float64(3.14));

// Wrapped term (newtype)
Term name = Terms.wrap("hydra.core.Name", Terms.string("myName"));

Working with Union Types (Visitor pattern)

import hydra.core.*;

// Pattern match on a Term
String describe(Term term) {
    return term.accept(new Term.PartialVisitor<String>() {
        @Override
        public String visit(Term.Literal instance) {
            return "A literal value";
        }
        @Override
        public String visit(Term.List instance) {
            return "A list with " + instance.value.size() + " elements";
        }
        @Override
        public String otherwise(Term instance) {
            return "Some other term";
        }
    });
}

Phantom-typed DSL

The phantom-typed DSL is the core of Hydra's Java metaprogramming system. It wraps raw Term values in TypedTerm<A> to provide compile-time type tracking.

Import pattern

import hydra.typed.TypedBinding;
import hydra.typed.TypedTerm;
import hydra.util.Maybe;

import static hydra.dsl.meta.Phantoms.*;
import static hydra.dsl.meta.Core.*;

Literals

TypedTerm<String> greeting = string("hello");
TypedTerm<Integer> age = int32(42);
TypedTerm<Boolean> flag = boolean_(true);
TypedTerm<Boolean> yes = true_();
TypedTerm<Boolean> no = false_();

Functions

// Lambda (single parameter)
TypedTerm<Object> id = lambda("x", var("x"));

// Lambda (multiple parameters — curried)
TypedTerm<Object> add = lambdas(List.of("x", "y"),
    primitive2(new Name("hydra.lib.math.add"), var("x"), var("y")));

// Function application
TypedTerm<Object> result = apply(var("f"), int32(5));

// Composition
TypedTerm<Object> composed = compose(var("g"), var("f"));

// Constant function
TypedTerm<Object> alwaysTrue = constant(true_());

// Identity
TypedTerm<Object> id2 = identity();

Data structures

// Lists
TypedTerm<List<Integer>> nums = list(int32(1), int32(2), int32(3));

// Pairs
TypedTerm<Object> kv = pair(string("key"), int32(42));

// Optional values
TypedTerm<Object> some = just(int32(42));
TypedTerm<Object> none = nothing();

// Either
TypedTerm<Object> ok = right(int32(42));
TypedTerm<Object> err = left(string("error"));

Records

// Construct a record (requires type name + fields)
TypedTerm<Object> person = record(Person.TYPE_NAME,
    field(Person.FIELD_NAME_NAME, string("Alice")),
    field(Person.FIELD_NAME_AGE, int32(30)));

Union injection

// Inject into a union type
TypedTerm<Object> circle = inject(Shape.TYPE_NAME, Shape.FIELD_NAME_CIRCLE,
    float64(3.14));

// Unit injection (for enum-like variants)
TypedTerm<Object> none = injectUnit(FloatType.TYPE_NAME, FloatType.FIELD_NAME_FLOAT32);

Pattern matching (cases/match)

// match creates a case elimination (unapplied)
TypedTerm<Object> matcher = match(Term.TYPE_NAME,
    Maybe.just(var("default")),         // default case
    field(Term.FIELD_NAME_LITERAL,      // case: literal
        lambda("lit", string("found a literal"))),
    field(Term.FIELD_NAME_VARIABLE,     // case: variable
        lambda("v", string("found a variable"))));

// cases applies the match to an argument
TypedTerm<Object> result = cases(Term.TYPE_NAME, var("myTerm"),
    Maybe.nothing(),                    // no default
    field(Term.FIELD_NAME_LITERAL,
        lambda("lit", var("lit"))),
    field(Term.FIELD_NAME_VARIABLE,
        lambda("v", var("v"))));

Let bindings

// Single let binding
TypedTerm<Object> expr = let1("x", int32(5),
    apply(var("add"), var("x")));

// Multiple let bindings
TypedTerm<Object> expr2 = lets(List.of(
    field(new Name("x"), int32(5)),
    field(new Name("y"), int32(10))),
    apply(apply(var("add"), var("x")), var("y")));

Projection (field access)

// Create a field accessor function
TypedTerm<Object> getName = project(Person.TYPE_NAME, Person.FIELD_NAME_NAME);

// Apply it
TypedTerm<Object> name = apply(getName, var("person"));

The combined "project a field, then apply to a named variable" pattern is so common that Phantoms provides a proj shortcut:

// Equivalent to: apply(project(Person.TYPE_NAME, Person.FIELD_NAME), var("person"))
TypedTerm<Object> name = proj(Person.TYPE_NAME, Person.FIELD_NAME, "person");

Overloads accept String or Name for the type/field arguments, and either a String variable name (which becomes var("...")) or a TypedTerm<?> for the receiver. Prefer proj() in DSL source modules — it's the idiomatic form.

If the field has a thunked type (e.g., unit -> T, used to defer expression evaluation for benchmarking; see UniversalTestCase.actual), the projection alone yields the thunk — not its forced value. Force with an extra apply(..., unit()):

// field type is `unit -> string` — force the thunk
TypedTerm<Object> value = apply(
    apply(
        project("hydra.testing.UniversalTestCase", "actual"),
        var("ucase")),
    unit());

Missing the outer apply(..., unit()) causes inference to fail with cannot unify string with (unit → string) for every binding in the containing module, since the inferencer processes them in a shared context.

Wrap/unwrap

// Wrap a value (create a newtype instance)
TypedTerm<Object> hydraName = wrap(Name.TYPE_NAME, string("myName"));

// Unwrap function
TypedTerm<Object> unwrapper = unwrap(Name.TYPE_NAME);

Primitive functions

// Reference a primitive
TypedTerm<Object> addPrim = primitive(new Name("hydra.lib.math.add"));

// Apply primitives with 1, 2, or 3 arguments
TypedTerm<Object> len = primitive1(new Name("hydra.lib.strings.length"), var("s"));
TypedTerm<Object> sum = primitive2(new Name("hydra.lib.math.add"), var("x"), var("y"));

Documentation

// Attach documentation to a term
TypedTerm<Object> documented = doc("Adds two numbers", var("add"));

Domain-specific DSLs

The domain-specific DSLs (Core, Graph, Compute) provide typed accessors for Hydra's kernel types. These are more readable than manual project() calls and less error-prone than using raw field name strings.

Core DSL (hydra.dsl.meta.Core)

import static hydra.dsl.meta.Core.*;

// Field accessors (each is project + apply)
TypedTerm<Object> param = lambdaParameter(var("lam"));       // Lambda.parameter
TypedTerm<Object> body = lambdaBody(var("lam"));             // Lambda.body
TypedTerm<Object> atBody = annotatedTermBody(var("at"));     // AnnotatedTerm.body
TypedTerm<Object> ann = annotatedTermAnnotation(var("at"));  // AnnotatedTerm.annotation
TypedTerm<Object> tname = injectionTypeName(var("inj"));     // Injection.typeName

// Constructors (build records)
TypedTerm<Object> lam = lambda_(
    wrap(Name.TYPE_NAME, string("x")),
    nothing(),
    var("body"));

TypedTerm<Object> at = annotatedTerm(var("body"), var("annotation"));

Generated name constants

Generated Hydra types provide TYPE_NAME and FIELD_NAME_* constants:

// From hydra.core.Term (generated)
Term.TYPE_NAME                // Name("hydra.core.Term")
Term.FIELD_NAME_LITERAL       // Name("literal")
Term.FIELD_NAME_VARIABLE      // Name("variable")
Term.FIELD_NAME_APPLICATION    // Name("application")
// ... etc.

// From hydra.core.Lambda (generated)
Lambda.TYPE_NAME              // Name("hydra.core.Lambda")
Lambda.FIELD_NAME_PARAMETER   // Name("parameter")
Lambda.FIELD_NAME_BODY        // Name("body")

Always use these constants rather than constructing Name instances manually. This ensures correctness and enables refactoring.

Library wrappers

Library wrappers provide phantom-typed interfaces to Hydra's primitive functions. They follow a consistent pattern:

// Pattern: wrap primitive2/primitive1 with descriptive names
static <R> TypedTerm<R> setsUnion(TypedTerm<?> s1, TypedTerm<?> s2) {
    return primitive2(new Name("hydra.lib.sets.union"), s1, s2);
}

static <R> TypedTerm<R> setsEmpty() {
    return primitive(new Name("hydra.lib.sets.empty"));
}

static <R> TypedTerm<R> listsFoldl(TypedTerm<?> f, TypedTerm<?> init, TypedTerm<?> list) {
    return primitive3(new Name("hydra.lib.lists.foldl"), f, init, list);
}

Type definitions

Type-level modules define Hydra data types using the Direct Types DSL. Each type definition is a Binding (a name-term pair).

Pattern

import hydra.core.*;
import hydra.dsl.Types;

public interface MyTypes {
    String NS = "my.namespace";

    static Binding define(String localName, Type type) {
        return hydra.Annotations.typeElement(
            new Name(NS + "." + localName), type);
    }

    // Forward references
    Type _Person = Types.variable(NS + ".Person");
    Type _Address = Types.variable(NS + ".Address");

    // Type definitions
    Binding person = define("Person",
        Types.record(
            Types.field("name", Types.string()),
            Types.field("age", Types.int32()),
            Types.field("address", _Address)));

    Binding address = define("Address",
        Types.record(
            Types.field("street", Types.string()),
            Types.field("city", Types.string())));
}

Cross-referencing types

Types in the same module reference each other through Types.variable():

// Forward reference to another type in this module
Type _Term = Types.variable("hydra.core.Term");

// Use it in a record field
Binding lambda = define("Lambda",
    Types.record(
        Types.field("parameter", _Name),
        Types.field("body", _Term)));

Complete example: hydra.core

The examples/ directory is aspirational — the file does not yet exist. For a real reference, see the host-native Java coder sources at packages/hydra-java/src/main/java/hydra/sources/, which use the same Phantoms idiom against the full Hydra kernel.

Term definitions

Term-level modules define Hydra functions using the Phantom-typed DSL. Each function definition is a TypedBinding<A> (a phantom-typed name-term pair).

Pattern

import hydra.typed.*;
import hydra.util.Maybe;
import static hydra.dsl.meta.Phantoms.*;
import static hydra.dsl.meta.Core.*;

public class MyFunctions {
    public static final ModuleName NS = new ModuleName("my.namespace");

    private static Def def(String localName, Supplier<TypedTerm<?>> body) {
        return Defs.define(NS, localName, body);
    }

    // Simple function: pattern match + extract body
    TypedBinding<Object> deannotateTerm = define("deannotateTerm",
        doc("Remove annotations from a term",
        lambda("term",
            cases(Term.TYPE_NAME, var("term"),
                Maybe.just(var("term")),       // default: return unchanged
                field(Term.FIELD_NAME_ANNOTATED,
                    lambda("at",
                        apply(var("deannotateTerm"),
                            annotatedTermBody(var("at")))))))));
}

Self-references

In Java, interface-level fields can reference themselves (the JVM handles initialization order). Use var("namespace.functionName") for qualified self-references:

// Recursive: apply same function to the body
apply(var("my.namespace.deannotateTerm"), annotatedTermBody(var("at")))

Complete example: hydra.rewriting

The examples/ directory is aspirational — the file does not yet exist. The same patterns (simple pattern matching, case branches, composition with projection, let-bindings, nested pattern matching, sets/folds/binding-aware rewriting, structural rewriting, traversal-order dispatching) appear throughout the host-native Java coder sources at packages/hydra-java/src/main/java/hydra/sources/, which serve as the live working examples.

Common patterns

Pattern 1: Simple case dispatch

Match on a union type, handle one variant, pass others through:

TypedTerm<Object> fn = lambda("term",
    cases(Term.TYPE_NAME, var("term"),
        Maybe.just(var("term")),                    // default: identity
        field(Term.FIELD_NAME_ANNOTATED,            // handle one case
            lambda("at", annotatedTermBody(var("at"))))));

Pattern 2: Let-binding with rewrite

Bind a local transform, pass it to a rewriting function:

TypedTerm<Object> fn = lambda("typ",
    let1("f",
        lambda("recurse", lambda("t",
            cases(Type.TYPE_NAME, var("t"),
                Maybe.just(apply(var("recurse"), var("t"))),
                field(Type.FIELD_NAME_ANNOTATED,
                    lambda("at",
                        apply(var("recurse"),
                            annotatedTypeBody(var("at")))))))),
        apply(apply(var("rewriteType"), var("f")), var("typ"))));

Pattern 3: Fold with set operations

Accumulate results over subterms:

TypedTerm<Object> vars = let1("dfltVars",
    listsFoldl(
        lambda("s", lambda("t",
            setsUnion(var("s"),
                apply(var("freeVariablesInTerm"), var("t"))))),
        setsEmpty(),
        apply(var("subterms"), var("term"))),
    // then match on specific cases...
    cases(Term.TYPE_NAME, var("term"),
        Maybe.just(var("dfltVars")),
        // ...
    ));

Pattern 4: Binding-aware rewriting

Check whether a variable is shadowed before rewriting:

TypedTerm<Object> replaceFn = lambda("recurse", lambda("t",
    cases(Term.TYPE_NAME, var("t"),
        Maybe.just(apply(var("recurse"), var("t"))),
        field(Term.FIELD_NAME_FUNCTION,
            match(Function.TYPE_NAME,
                Maybe.just(apply(var("recurse"), var("t"))),
                field(Function.FIELD_NAME_LAMBDA,
                    lambda("l",
                        // Stop if lambda shadows our variable
                        apply(apply(var("ifElse"),
                            equalName(lambdaParameter(var("l")), var("name"))),
                            var("t"),
                            apply(var("recurse"), var("t"))))))))));

Working with generated code

Generated Java classes for Hydra types provide:

  1. Visitor pattern for union types (accept, Visitor<R>, PartialVisitor<R>)
  2. Static name constants (TYPE_NAME, FIELD_NAME_*)
  3. Serializable implementations
  4. Comparable implementations

Example: Generated Term class

// hydra.core.Term (generated)
public abstract class Term implements Serializable, Comparable<Term> {
    public static final Name TYPE_NAME = new Name("hydra.core.Term");
    public static final Name FIELD_NAME_LITERAL = new Name("literal");
    public static final Name FIELD_NAME_VARIABLE = new Name("variable");
    // ...

    public static final class Literal extends Term { ... }
    public static final class Variable extends Term { ... }
    // ...

    public abstract <R> R accept(Visitor<R> visitor);
}

Error handling

Hydra computations use Either<Error, A> for error handling (the former Flow monad was removed in #245). An InferenceContext value is threaded alongside the graph and carries the fresh-type-variable counter and the current subterm-path trace.

import hydra.util.Either;
import hydra.typing.InferenceContext;
import hydra.errors.Error;
import hydra.graph.Graph;

// Create a successful result
Either<Error, String> ok = Either.right("result");

// Map over a result
Either<Error, Integer> mapped =
    hydra.lib.eithers.Map.apply(s -> s.length(), ok);

// Chain computations (bind / flatMap)
Either<Error, String> bound =
    hydra.lib.eithers.Bind.apply(result1, value ->
        Either.right(value + " processed"));

// Create a failure (Error is a tagged-union type; construct a variant from hydra.errors)
Either<Error, String> err = Either.left(Error.other("something went wrong"));

// Inspect a result
if (result.isRight()) {
    String value = result.get();
} else {
    Error failure = result.getLeft();
}

Examples in the codebase

File Description
packages/hydra-java/src/main/java/hydra/dsl/meta/Phantoms.java Phantom-typed DSL (all operations)
packages/hydra-java/src/main/java/hydra/dsl/meta/Defs.java Module-definition helpers for the Java coder DSL
packages/hydra-java/src/main/java/hydra/dsl/meta/lib/Lists.java, Maps.java, Sets.java, Logic.java, Maths.java, Maybes.java, Strings.java, Literals.java Library wrappers
packages/hydra-java/src/main/java/hydra/sources/ Live host-native Java coder DSL sources (reference for current Phantoms idiom)
heads/java/src/main/java/hydra/dsl/Types.java Direct Types DSL (runtime)
heads/java/src/main/java/hydra/dsl/Terms.java Direct Terms DSL (runtime)

Related Documentation