functy

package module
v0.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 18, 2026 License: MIT Imports: 22 Imported by: 0

README

functy

An imperative language whose values are cty values and whose expressions are HCL.

Pronounced funk-tie — the -ty rhymes with cty ("see-tie"). But I won't be mad if you pronounce it funk-tee. I sometimes forget too.

functy is not aiming to compete with Starlark, Tengo, or any of the various JavaScript or Lua implementations for Go. It is intended specifically for use in software that is already relying on HCL / Cty but which needs more expressive power than eg the HCL user function add-on.

go-cty describes itself this way:

One could think of cty as being the reflection API for a language that doesn't exist, or that doesn't exist yet.

functy aims to be that language.

CI

Overview

functy is a small Go-inspired imperative language that compiles to ordinary cty function.Function values. You write functions with familiar control flow — if/else, for/while, switch, try/catch — and every expression is a real HCL expression: operators, string templates ("hello ${name}"), function calls, conditionals, and the cty type-constraint grammar all behave exactly as they do in HCL/Terraform.

The result is a thin, honest imperative skin over cty + HCL: its values are cty values, its types are cty types, and the functions it produces are callable from any HCL evaluation context — alongside the host application's own functions.

func classify(n: number) -> string {
    if n > 0 {
        return "positive"
    } else if n < 0 {
        return "negative"
    } else {
        return "zero"
    }
}

func greet(name: string = "world") -> string {
    return "hello ${name}"
}

functy source files use the .cty extension.

Installation

As a library:

go get github.com/tsarna/functy

The library depends only on github.com/hashicorp/hcl/v2 and github.com/zclconf/go-cty.

As a CLI, on macOS via Homebrew:

brew install tsarna/tap/functy

or with go install (any platform):

go install github.com/tsarna/functy/cmd/functy@latest

CLI quick start

$ cat > math.cty <<'EOF'
func add(a: number, b: number) -> number {
    return a + b
}
func main() -> number {
    return add(2, 3)
}
EOF

$ functy run math.cty
5

$ functy run math.cty --func add -- 2 3
5

$ functy check math.cty
ok

$ functy test math.cty        # run co-located test "..." { ... } blocks
ok   add sums two numbers
1 passed, 0 failed

Sources can carry co-located tests — test "description" { ... } blocks whose bodies use assert — run with functy test (or a host's (*Result).RunTests); a failing test reports the source line and operand values.

See doc/cli.md for the full CLI reference, doc/language.md for the language reference, and doc/language.md#tests for inline tests. The examples/ directory has runnable samples covering local variables, control flow, both error-handling styles, type aliases, variadic parameters, switch/fallthrough, tests, and more.

Library usage

Parse a source, compile it against a late-bound eval context, and call the resulting functions:

package main

import (
	"fmt"

	"github.com/hashicorp/hcl/v2"
	"github.com/tsarna/functy"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/function"
)

func main() {
	src := []byte(`func add(a: number, b: number) -> number { return a + b }`)

	res, diags := functy.NewParser().Parse(src, "add.cty")
	if diags.HasErrors() {
		panic(diags.Error())
	}

	// The eval context is late-bound so functions can call one another (and
	// reference host globals finalized later).
	var ctx *hcl.EvalContext
	funcs, diags := res.Compile(func() *hcl.EvalContext { return ctx })
	if diags.HasErrors() {
		panic(diags.Error())
	}
	ctx = &hcl.EvalContext{
		Functions: funcs,
		Variables: map[string]cty.Value{},
	}

	out, err := funcs["add"].Call([]cty.Value{cty.NumberIntVal(2), cty.NumberIntVal(3)})
	if err != nil {
		panic(err)
	}
	fmt.Println(out.AsBigFloat()) // 5
}

A host typically merges the compiled functions with its own function library and ambient values into one eval context. functy.ParseSources collects .cty sources from file paths, directories, or an embed.FS; Parser.ParseAll parses several sources into one Result.

A leading // / # comment block above a declaration is captured as its documentation — FuncDecl.Doc and Decl.Doc — so a host can surface it (generated docs, editor hovers, or anywhere it wants a function's description at runtime); every comment is also retained with position in Result.Comments. See doc/language.md.

When a call returns an error, an uncaught functy throw/assert surfaces as a *functy.ThrownError; errors.As(err, &te) recovers it, and te.Diagnostics() (or functy.ErrorDiagnostics(value)) yields hcl.Diagnostics you can hand to hcl.NewDiagnosticTextWriter to print the failing source line — and, for a failed assert, the operand values — with source context instead of a flat message.

functy also ships a small standard library of expression builtins — functy.Stdlib() (typeof, typekind, cond, switch, error, assert) and the opt-in functy.StdlibExtras() (try, can) — for a host to merge into that same context. See doc/stdlib.md.

A host can also register its own named types so they can be used in annotations:

p := functy.NewParser().
    RegisterType("bus", busCapsuleType).            // identity-enforced capsule type
    RegisterOpenType("ctx", isContextObject)        // predicate-backed open type

Identity types must match by type; open types must satisfy a predicate and pass through untouched (extra attributes preserved). See doc/language.md.

Type system as a reusable component

functy's type system is usable on its own, independent of parsing .cty programs — as a richer alternative to ext/typeexpr (the result is a TypeConstraint that can enforce values via Coerce, not just a cty.Type), or for a host to type-check its own configuration:

r := functy.NewTypeResolver().RegisterType("bus", busCapsuleType)

// Resolve a type annotation (from a string, or an hcl.Expression):
tc, diags := r.ParseType([]byte("list(string)"), "config")
// tc.Cty()  -> cty.List(cty.String)
// tc.Coerce(value) -> the value converted/validated, or an error

// ResolveType(expr) takes an already-parsed hcl.Expression (e.g. a decoded HCL
// attribute) — the typeexpr.TypeConstraint analog.

A Parser holds a TypeResolver (Parser.Types()) and registers named types on it, so a host registers its capsule/open types once and uses them both for parsing .cty files and for resolving standalone annotations.

Status

functy implements a complete core language: typed and dynamic variables, reassignment, all control-flow forms, variadic and optional parameters, structured error handling (try/catch/finally, throw, defer, and val, err = expr error capture), a null (void) return type, and co-located test blocks.

Types are resolved by functy's own resolver (not ext/typeexpr), with a host-pluggable named-type environment for capsule and open types, type aliases, and opt-in strict typing.

The CLI is comprehensive — run, eval, check, test, fmt, symbols, and a very basic interactive repl — see doc/cli.md.

Designed-but-not-yet-implemented features (module imports / namespacing, closures, declaration annotations, nested open types, ...) are recorded in FUTURE.md; designs that were worked out and then declined, with the reasoning, are in REJECTED.md. The design for a functy provider for OpenTofu — buildable today, since OpenTofu lets a configured provider define functions dynamically — is in OPENTOFU-PROVIDER.md; a separate proposal to make functy .cty the authoring language for OpenTofu's in-draft symbol libraries feature (all three of types, values, and functions) is in OPENTOFU-SYMBOLS.md; and the analysis of everything that is blocked on Terraform (and why) is in TERRAFORM.md. The design rationale ("why a language, not an embedded one") is in DESIGN.md, and the internal architecture is in CONTRIBUTING.md.

License

MIT, like cty itself — see LICENSE.

Documentation

Overview

Package functy implements an imperative language whose values are cty values and whose expressions are HCL. A functy source file is a sequence of function declarations; compiling it yields ordinary cty function.Function values that can be added to an *hcl.EvalContext and called from any HCL expression.

The statement grammar (func, var, if/else, for/while, switch, ...) is parsed by functy itself, while every embedded expression is handed to HCL (hclsyntax.ParseExpression), so operators, templates, and function calls behave exactly as they do elsewhere in HCL. Type annotations are resolved by functy's own TypeResolver — a superset of the ext/typeexpr grammar that also supports host-registered capsule and open (predicate-backed) named types.

Index

Constants

View Source
const Extension = ".cty"

Extension is the file extension for functy source files.

Variables

This section is empty.

Functions

func BuildFunction

func BuildFunction(fn *FuncDecl, evalCtxFn func() *hcl.EvalContext, maxSteps int) function.Function

BuildFunction builds a single cty function from a parsed declaration.

cty has no native optional parameters, so only the required parameters go in Spec.Params; optional parameters and the variadic parameter are collected via a VarParam and mapped back to names (applying defaults) inside the Impl. maxSteps is the Tier-1 execution-limit ceiling captured immutably into the Impl closure: every invocation builds a fresh interp seeded with it, so the step counter is per-frame and needs no shared state. 0 means unbounded.

func DocFunc added in v0.5.0

func DocFunc(evalCtxFn func() *hcl.EvalContext) function.Function

DocFunc returns the context-aware builtin doc(name): given a function's name as a string, it returns that function's description — the doc comment captured on a functy declaration (FuncDecl.Doc, wired into the compiled function's cty Description) or whatever Description a host function carries. It is tri-state:

  • null — no such function (absent from the context)
  • "" — the function exists but is undocumented
  • "text" — the function's description

Distinguishing "absent" (null) from "undocumented" ("") lets a caller catch a mistyped name without doc having to throw: absence is a normal reflection answer, so a caller who wants strictness opts in (e.g. assert(doc(x) != null)).

It is not part of Stdlib() because it needs a handle to the assembled eval context. evalCtxFn is the same late-binding closure passed to Result.Compile: at call time it yields the merged context whose Functions map holds every function (host- and functy-defined) in one flat map, which doc looks the name up in. A host merges the result under the name "doc":

ctx.Functions["doc"] = functy.DocFunc(evalCtxFn)

(The richer help(name) — assembling a function's full calling convention and per-argument docs — is left for later; doc is the primitive it will build on.)

func ErrorDiagnostics added in v0.4.0

func ErrorDiagnostics(errVal cty.Value) hcl.Diagnostics

ErrorDiagnostics renders a functy error value as an hcl.Diagnostic so a host can print it with source context (an underline under the failing expression) via the standard hcl diagnostic writer. The message becomes the summary, the `detail` attribute (e.g. an assert's operand values) becomes the diagnostic detail, and the `range` attribute becomes the Subject — omitted (no underline) when absent or malformed. It takes an error *value* so the same formatter serves the host boundary (via ThrownError.Diagnostics) and any consumer that catches errors as values.

func EvalDecls added in v0.11.0

func EvalDecls(decls []Decl, ctx *hcl.EvalContext) hcl.Diagnostics

EvalDecls evaluates a set of top-level const/var declarations into ctx.Variables, resolving cross-references order-independently.

Declarations may reference one another out of source order, so this resolves them iteratively: each pass evaluates every declaration whose referenced declaration names are already available, until no progress is made. Anything left unresolved is a cyclic or dangling reference and is reported. A declaration with a `: T` annotation has its value coerced to that type.

The caller chooses which declarations to pass and in what precedence: to make consts resolve before vars (the common host ordering — a var may reference a const, not vice versa), pass append(consts, vars...). A host embedding functy as a symbol library typically passes Result.Consts alone.

It is exported (rather than kept in cmd/functy) precisely because a host needs it: functy hands back parsed Decls but pre-evaluates nothing, so turning a const into a self-contained cty.Value is the host's job, and the dependency ordering is fiddly enough to be worth sharing.

func EvalNamespacedDecls added in v0.11.0

func EvalNamespacedDecls(decls []Decl, baseCtx *hcl.EvalContext, compiled *Compiled) hcl.Diagnostics

EvalNamespacedDecls evaluates const/var Decls grouped by Decl.Namespace into a Compiled's per-namespace variable tables under the own+global policy.

The global namespace ("") is evaluated first into baseCtx.Variables — which the caller must set to compiled.Vars[""] so that (a) the global consts land in the table the host projects and (b) they are visible to every namespace's bodies, which late-bind to baseCtx as their parent. Each other namespace is then evaluated into compiled.Vars[ns] via a child of baseCtx that also exposes that namespace's sibling functions (compiled.Units[ns]). A namespaced initializer thus sees its own namespace's names, then the global names, with the local winning — mirroring how a namespaced function call resolves.

Because the global namespace is evaluated first, a namespaced declaration may reference a global one; the reverse cannot (a global sees only globals). Duplicate detection is per namespace: two namespaces may each declare `const greeting`, but a name declared twice within one namespace is still reported.

This is one policy, not the only one. A host wanting strict isolation (a namespace sees only its own consts) can skip this helper and call EvalDecls against each compiled.Vars[ns] directly, with no shared parent carrying the globals.

func Format added in v0.6.0

func Format(src []byte, filename string) ([]byte, hcl.Diagnostics)

Format parses src and returns it canonically formatted. It uses a default parser permitting top-level var/const; a host that registers named types (or other options) can call (*Parser).Format instead so those annotations resolve.

func HelpFunc added in v0.5.0

func HelpFunc(res *Result, evalCtxFn func() *hcl.EvalContext) function.Function

HelpFunc returns the context-aware builtin help(name): a human-readable summary of a function — its signature (calling convention), description, and per-parameter docs — as a single string, or null if there is no such function.

Called with no argument, help() instead returns the sorted, newline-separated names of every available function (a directory to explore with help(name)), drawn from the assembled eval context so it spans host- and functy-defined functions alike.

res (the parse Result) supplies the functy declarations, which help renders from directly: functy's optional and variadic parameters collapse into a single VarParam in the cty calling convention, so the declaration is the only accurate source of the real signature. Result.Externs is consulted the same way, and is the *point* of that set — an extern declares what a host function's cty metadata cannot express. evalCtxFn provides a best-effort fallback for anything neither declares, rendered from its cty metadata (parameter names, types, descriptions). A host wires both reflection builtins in:

ctx.Functions["doc"]  = functy.DocFunc(evalCtxFn)
ctx.Functions["help"] = functy.HelpFunc(result, evalCtxFn)

Note: a non-functy Go builtin that emulates optional/defaulted parameters through its VarParam cannot be rendered with its intended signature — that structure is not recoverable from cty — so the fallback shows the raw required-plus-variadic shape. Declaring an extern for it is how that is fixed.

func Qualify added in v0.10.0

func Qualify(namespace, name string) string

Qualify joins a namespace ("" = global) and a bare name into the name the function is registered under.

HCL parses `a::b::c(x)` natively and resolves it as a single flat map key, so a qualified name needs no structure beyond the string itself: nesting is a naming convention, not a containment relationship, and there is no parent-namespace fallback. Exported so a host that walks Compiled.Units — which is keyed by bare name — can render the callable name without reimplementing the join.

func Stdlib added in v0.4.0

func Stdlib() map[string]function.Function

Stdlib returns functy's language-level standard library: host-agnostic, dependency-free builtins that make HCL expressions more capable than raw HCL. A host merges these into its eval context (alongside the cty stdlib and its own functions), so they are available anywhere the host evaluates expressions — not only inside functy function bodies.

  • typeof(v) type in functy's annotation grammar
  • typekind(v) top-level type kind (for dispatch)
  • cond(c1, r1, …, else) lazy multi-branch conditional (single-eval)
  • switch(on, v1, r1, …, def?) lazy value dispatch (single-eval)
  • error(v) raise an error from an expression
  • assert(cond, message?) raise a catchable error when cond is false

The name-colliding, opt-in try/can live in StdlibExtras() instead.

func StdlibExtras added in v0.4.0

func StdlibExtras() map[string]function.Function

StdlibExtras returns the opt-in builtins whose names collide with HCL's stock tryfunc:

  • try(e1, e2, …) first expression that evaluates without error (single-eval)
  • can(e) whether an expression evaluates without error

They are kept separate from Stdlib() so a host already exposing a try/can (e.g. from hcl/v2/ext/tryfunc, whose try double-evaluates the winning branch) is not silently overridden.

Types

type Assign

type Assign struct {
	Name     string
	Expr     hcl.Expression
	SrcRange hcl.Range
}

Assign reassigns an existing binding found by walking the scope chain.

type Block

type Block struct {
	Body     []Statement
	SrcRange hcl.Range
}

Block is a bare { ... } that introduces a nested lexical scope.

type Break

type Break struct {
	Label    string // "" for the innermost loop
	SrcRange hcl.Range
}

Break exits an enclosing loop: the innermost one, or the loop named by Label.

type CaptureAssign added in v0.3.0

type CaptureAssign struct {
	ValName  string // "_" to discard the value
	ErrName  string // "_" to discard the error
	Declare  bool   // true for the `:=` declare-and-capture form
	Expr     hcl.Expression
	ValRange hcl.Range // the value target, for diagnostics
	ErrRange hcl.Range // the error target, for diagnostics
	SrcRange hcl.Range
}

CaptureAssign is the two-target error-capture assignment `val, err = expr`. It evaluates Expr exactly once; on success it assigns the value to ValName and null to ErrName, and on failure it assigns null to ValName and the caught error to ErrName. Either target may be "_" (the blank identifier) to discard it. It is statement-level sugar for a try/catch — the function never unwinds.

When Declare is false (the `val, err = expr` operator) both non-blank targets must already be declared, like a plain `=`. When Declare is true (the `val, err := expr` shorthand) each non-blank target is newly declared (untyped) in the current scope, like a pair of `var`s.

type CatchClause added in v0.3.0

type CatchClause struct {
	Name      string         // "" when omitted
	Type      TypeConstraint // nil = no type filter
	TypeSrc   string         // source text of the type filter, for rendering (fmt)
	Guard     hcl.Expression // nil = no guard
	Body      []Statement
	BodyRange hcl.Range // the `{ ... }` body span, for fmt
	SrcRange  hcl.Range
}

CatchClause is one `catch [name] [: type] [if guard] { ... }` clause. It matches a raised error iff its type filter's Coerce succeeds (Type == nil matches any shape) and its guard evaluates true (Guard == nil is unconditional); a clause with both Type and Guard nil is the catch-all. The bound name receives the raw error value (the filter is a gate, not a cast).

type Clause added in v0.3.0

type Clause struct {
	Values    []hcl.Expression
	IsDefault bool
	Body      []Statement
	SrcRange  hcl.Range
}

Clause is one case or default clause of a switch. For a case clause Values holds the match expressions (it runs if any equals the subject, or — in the expression-less form — if any is true); the default clause has IsDefault true and no Values. A body whose final statement is Fallthrough transfers control to the next clause in source order.

type Comment added in v0.5.0

type Comment struct {
	// Text is the comment's source text with any trailing CR/LF trimmed. It keeps
	// the leading marker (`//`, `#`, or `/*` … `*/`).
	Text string
	// Line is true for a `//` or `#` single-line comment, false for a `/* */`
	// block comment.
	Line bool
	// Range is the comment's full source span. For a line comment it includes the
	// terminating newline (as HCL's lexer reports it).
	Range hcl.Range
}

Comment is a single comment captured from a functy source, retained with its position so tooling can attach it to the construct it decorates. The statement parser never sees comments — the lexer keeps the token stream comment-free — so this side table is the sole record that a comment existed. It is the foundation for doc-comment metadata (below) and, in future, a source formatter.

type Compiled added in v0.10.0

type Compiled struct {
	// Funcs is the map for the host's eval context. It holds only the *exported*
	// functions — private (`_`-prefixed) ones are absent — keyed by their
	// *qualified* name (`foo::bar::baz`, or the bare name in the global
	// namespace).
	Funcs map[string]function.Function

	// Units maps a namespace ("" = global) to that namespace's own functions by
	// their *bare* names, private ones included. This mirrors the layer a
	// namespace's functions resolve their siblings through, and it is what makes a
	// private function callable from inside its namespace while remaining invisible
	// to the host. Exposed because tooling needs it: to reach a private function by
	// name (`functy run --func _helper`), and to detect a bare name that shadows a
	// host function (see the note on unitCtxFn).
	//
	// It is a *snapshot*, not the live table the compiled functions read: those are
	// consulted on every call, from whatever goroutine is calling, so handing the
	// same maps out would let a host's write race a running function.
	Units map[string]map[string]function.Function

	// Vars maps a namespace ("" = global) to that namespace's variable scope: the
	// map a namespace's function bodies resolve a bare variable name against,
	// symmetric to Units for functions (a body in namespace ns calls Units[ns] and
	// reads Vars[ns]). A bare name a body does not resolve locally falls through
	// here, and if still absent, on to the host's own Variables.
	//
	// functy hands this back EMPTY and puts nothing in it: it takes no position on
	// what a const or var means, so it neither evaluates declarations nor buckets
	// them by namespace. The host FILLS it — writing a scope under Vars[ns] for any
	// namespace it likes, by any policy (e.g. evaluate the namespace's Decls with
	// EvalDecls, or use EvalNamespacedDecls for the own+global convention) — before
	// any function is invoked. Each namespace's bodies look their scope up in this
	// same map by name at call time, so there is no per-namespace pre-registration
	// and no distinction between a namespace that has functions and one that has only
	// consts: whatever the host writes under Vars[ns] is what ns's bodies read.
	// Leaving it empty preserves the pre-namespaced behavior (bare names resolve only
	// against host globals).
	Vars map[string]map[string]cty.Value
}

Compiled is the outcome of compiling a Result: the functions to hand the host, and the per-namespace name-resolution layers functy's own bodies resolve against.

type CondBranch

type CondBranch struct {
	Condition hcl.Expression
	Body      []Statement
	BodyRange hcl.Range // the `{ ... }` body span, for fmt
	SrcRange  hcl.Range
}

CondBranch is one condition-guarded branch of an if chain.

type Continue

type Continue struct {
	Label    string // "" for the innermost loop
	SrcRange hcl.Range
}

Continue skips to the next iteration of an enclosing loop: the innermost one, or the loop named by Label.

type Decl

type Decl struct {
	Name string
	// Namespace is the enclosing namespace of the file the declaration appeared
	// in ("" = global). functy attaches it and takes no further position: a global
	// is whatever the host decides it is, so the host may ignore namespaced
	// globals, reject them, or implement them by a mechanism of its own.
	//
	// Note there is no qualified *spelling* for a global: HCL's `::` is a
	// function-call selector, so `foo::bar::x` as a variable reference is a parse
	// error. Namespacing therefore applies to functions; this field is metadata.
	Namespace string
	// Doc is the rendered leading doc-comment block (`//` or `#` lines directly
	// above the declaration, directive lines excluded); "" when there is none.
	Doc      string
	Type     TypeConstraint // from an optional `: T`; nil if unannotated
	TypeSrc  string         // source text of the type annotation, for rendering (fmt)
	Expr     hcl.Expression // initializer, lazily evaluated (nil if none)
	DefRange hcl.Range
}

Decl is a collected top-level var/const declaration, returned unevaluated so a host can fold it into its own dependency-sorting and evaluation pass. Expr.Variables() exposes the references needed for that sort.

func (*Decl) IsPrivate added in v0.10.0

func (d *Decl) IsPrivate() bool

IsPrivate reports whether the declaration is namespace-local (a leading underscore). As with functions, this is advisory for var/const: functy only collects them, so it is the host that decides what to do with a private global.

type Defer

type Defer struct {
	Expr     hcl.Expression
	SrcRange hcl.Range
}

Defer schedules Expr to run when the enclosing function exits, in LIFO order.

type Directive added in v0.2.0

type Directive struct {
	Namespace string
	Name      string
	Args      string
	Range     hcl.Range
}

Directive is a collected directive comment, following Go's convention: a line comment with no space after `//`, of the form `//<namespace>:<name> [args]`. A space after `//` (`// functy: …`) makes it ordinary prose, not a directive.

functy interprets only its own `functy:` namespace (strict, require); every other namespace is collected and passed through untouched for the host to act on (e.g. `//vinculum:cache 5m`).

type ExprStmt

type ExprStmt struct {
	Expr     hcl.Expression
	SrcRange hcl.Range
}

ExprStmt evaluates an expression purely for its side effects; the value is discarded.

type Fallthrough added in v0.3.0

type Fallthrough struct{ SrcRange hcl.Range }

Fallthrough transfers control to the next clause of the enclosing switch, running its body without testing. It is legal only as the final statement of a case or default body, and not in the last clause.

type For

type For struct {
	Kind ForKind

	// Label is the loop's label ("" if unlabeled); a labeled break/continue whose
	// target equals this label is consumed by this loop rather than an inner one.
	Label string

	// While is true when a ForCond loop was written with the `while` keyword rather
	// than `for` (the two are synonyms); recorded so fmt can preserve the keyword.
	While bool

	// ForCond / ForClause:
	Init Statement      // ForClause init clause (nil otherwise)
	Cond hcl.Expression // condition (nil = always true)
	Post Statement      // ForClause post clause (nil otherwise)

	// ForRange:
	KeyName    string         // first range variable ("" if absent)
	ValName    string         // second range variable ("" if only one is given)
	Collection hcl.Expression // collection being ranged over

	Body      []Statement
	BodyRange hcl.Range // the `{ ... }` body span, for fmt
	SrcRange  hcl.Range
}

For is the unified loop node covering all loop forms.

type ForKind

type ForKind int

ForKind distinguishes the three surface forms of a for/while loop.

const (
	// ForCond is `for cond { ... }`, `while cond { ... }`, or the infinite
	// `for { ... }` (Cond nil).
	ForCond ForKind = iota
	// ForClause is the three-clause `for init; cond; post { ... }`.
	ForClause
	// ForRange is `for v in coll { ... }` or `for k, v in coll { ... }`.
	ForRange
)

type FuncDecl

type FuncDecl struct {
	Name string
	// Namespace is the enclosing namespace, from the file's `namespace a::b`
	// declaration; "" is the global namespace. See NamespaceDecl.
	Namespace string
	// Doc is the rendered leading doc-comment block (`//` or `#` lines directly
	// above the declaration, directive lines excluded); "" when there is none.
	Doc        string
	Params     []Param
	ParenRange hcl.Range      // the (…) parameter-list span, for fmt
	RetType    TypeConstraint // nil when no return type is annotated (dynamic)
	RetTypeSrc string         // source text of the return-type annotation, for rendering (fmt)
	SigRange   hcl.Range      // spans `func` … the last signature token (`)` or the return type)
	// Extern marks a declaration from a //functy:extern file: a signature only,
	// never compiled and never callable. See Result.Externs.
	Extern    bool
	Body      []Statement
	BodyRange hcl.Range // the `{ ... }` body span, for rendering (fmt); zero for an extern
	DefRange  hcl.Range
}

FuncDecl is a top-level function declaration.

An extern (see Result.Externs) is a FuncDecl with Extern true: it declares the signature of a function the *host* provides, so it has no Body and a zero BodyRange. SigRange is therefore the only end position an extern has, and every consumer that would reach for BodyRange.End must use it instead.

func (*FuncDecl) IsPrivate added in v0.10.0

func (f *FuncDecl) IsPrivate() bool

IsPrivate reports whether the declaration is namespace-local: visible to the other functions of its namespace, never registered with the host.

Privacy is a naming convention (a leading underscore) rather than a keyword, so it is a pure function of the name and cannot desync from it. It also makes a private name incapable of colliding with a host function, since no host function, cty builtin, or add-on package function is `_`-prefixed.

func (*FuncDecl) QualifiedName added in v0.10.0

func (f *FuncDecl) QualifiedName() string

QualifiedName is the name the function is registered under with the host: its namespace and bare name joined by `::`, or just the bare name in the global namespace. Private functions have a qualified name but are never handed to the host (see Compiled).

type IfChain

type IfChain struct {
	Branches  []CondBranch
	Else      []Statement
	ElseRange hcl.Range // the `else { ... }` body span (zero when there is no else), for fmt
	SrcRange  hcl.Range
}

IfChain is an if / else-if / else chain. Else is nil when there is no final else clause.

type LimitError added in v0.11.0

type LimitError struct {
	// Steps is the step count at the moment the limit tripped; Limit is the ceiling.
	Steps, Limit int
	// Range is the source location of the checkpoint that tripped (a loop or the
	// statement being executed), used to underline the breach in diagnostics.
	Range hcl.Range
}

LimitError is the Go error a functy function returns at its cty.Function boundary when an execution limit (a per-invocation step budget; see Parser.MaxSteps) is breached. It is modeled on SkipError but is deliberately *uncatchable*: a breach terminates the whole evaluation and unwinds straight out through every enclosing frame — try/catch and `val, err =` re-propagate it rather than handle it, so a guard like `try { while true {} }` cannot swallow the very protection that fired.

func (*LimitError) Diagnostics added in v0.11.0

func (e *LimitError) Diagnostics() hcl.Diagnostics

Diagnostics renders the breach as hcl.Diagnostics so a host can print it with source context, mirroring ThrownError.Diagnostics.

func (*LimitError) Error added in v0.11.0

func (e *LimitError) Error() string

type NamespaceDecl added in v0.10.0

type NamespaceDecl struct {
	Name     string    // "foo" or "foo::bar"
	DefRange hcl.Range // spans `namespace` … the last segment
}

NamespaceDecl is a file's leading `namespace a::b` declaration.

A namespace name is one or more `::`-separated identifiers: `namespace foo` is as legitimate as `namespace foo::bar`, and no depth is implied. Nesting is purely a naming convention — `foo::bar` is not "inside" `foo` in any sense functy or HCL enforces, and code in `foo::bar` gets no special visibility into `foo`.

The declaration is modeled in the AST (not merely stamped onto the decls it governs) because fmt renders from the AST: an unmodeled top-level item would be silently deleted on reformat.

type Param

type Param struct {
	Name string
	// Doc is the parameter's documentation: a trailing comment on its line
	// (`a: T, // desc`) or a leading `//` / `#` block directly above it (which
	// wins if both are present); "" when there is none. See attachDocComments.
	Doc        string
	Type       TypeConstraint // nil when unannotated (dynamic)
	TypeSrc    string         // source text of the type annotation, for rendering (fmt)
	Default    hcl.Expression // non-nil for a defaulted parameter
	DefaultSrc string         // source text of the default expression, for rendering (help()/fmt)
	// Optional marks `name?`: optional with *no* default, and mutually exclusive
	// with Default. It exists because it is the only way to spell an optional
	// *leading* parameter — the `get([ctx,] thing)` convention that host libraries
	// implement by sniffing args[0], and that cty's parameter list cannot express
	// (an optional cty param may only sit at the tail).
	//
	// Legal only in an extern file, where it is never compiled: BuildFunction
	// classifies a param with no Default as *required*, so an Optional param
	// reaching it would be silently mis-compiled. The compile path guards on
	// FuncDecl.Extern to keep that unreachable.
	Optional  bool
	Variadic  bool // true for the trailing *rest parameter
	DefRange  hcl.Range
	FullRange hcl.Range // spans the whole parameter (name … type/default), for fmt
}

Param is a single function parameter.

A parameter is required unless it has a Default, is marked Optional, or is Variadic. A typed parameter (Type != cty.NilType) converts its argument to that type. For a variadic parameter, Type is the *element* type: `*rest: T` collects the extra arguments into a list(T), while an untyped `*rest` collects them into a tuple.

type Parser

type Parser struct {
	// contains filtered or unexported fields
}

Parser parses functy source into a Result. Options accrue via chained setters; the zero value (via NewParser) accepts only function declarations and the built-in type grammar.

func NewParser

func NewParser() *Parser

NewParser returns a Parser with default options.

func (*Parser) AllowTopLevelConst

func (p *Parser) AllowTopLevelConst(v bool) *Parser

AllowTopLevelConst controls whether a top-level `const` declaration is collected into Result.Consts (true) or reported as a parse error (false, the default).

func (*Parser) AllowTopLevelVar

func (p *Parser) AllowTopLevelVar(v bool) *Parser

AllowTopLevelVar controls whether a top-level `var` declaration is collected into Result.Vars (true) or reported as a parse error (false, the default).

func (*Parser) ExternSources added in v0.10.0

func (p *Parser) ExternSources() []Source

ExternSources returns the sources registered with RegisterExterns. A host uses it to seed its diagnostic file map, so that a diagnostic pointing into an extern file it never read from disk still renders with a source snippet.

func (*Parser) Format added in v0.6.0

func (p *Parser) Format(src []byte, filename string) (out []byte, diags hcl.Diagnostics)

Format parses src with the receiver's configuration and returns it canonically formatted. On any parse error it returns src unchanged together with the diagnostics — a file that does not fully parse is never reformatted, so fmt can never drop or reorder code. Expressions are reformatted with hclwrite (which preserves their internal comments); statement layout, indentation, blank-line runs, and statement/declaration comments are handled here.

func (*Parser) MaxSteps added in v0.11.0

func (p *Parser) MaxSteps(v int) *Parser

MaxSteps sets the Tier-1 execution limit: the maximum number of steps any single function invocation may take before a breach terminates the whole evaluation with an (uncatchable) *LimitError. A step is one statement executed plus one per loop iteration, counted per invocation — so it bounds a single function's runaway `for` / `while`, but not recursion (each nested call starts a fresh count) nor work aggregated across many small calls. 0 (the default) means unbounded, leaving existing embeddings unchanged. The ceiling is captured immutably at compile time. A negative value is a mistake — parsing warns and treats it as unbounded rather than silently disabling the limit a host meant to set. Returns the Parser for chaining.

func (*Parser) Parse

func (p *Parser) Parse(src []byte, filename string) (*Result, hcl.Diagnostics)

Parse parses a single functy source. The returned Result holds the parsed declarations even when diagnostics contain errors (best-effort recovery), so callers should check diags before using it.

func (*Parser) ParseAll

func (p *Parser) ParseAll(sources []Source) (*Result, hcl.Diagnostics)

ParseAll parses several sources together and merges their declarations into one Result. Type aliases declared in any source are visible to every source (they are project-scoped — see parseSources), and per-source function/var/const declarations are concatenated in order.

Functions are scoped by the *namespace* of the source they were declared in (a source with no `namespace` declaration is in the global namespace). A namespace spans files: two sources declaring `namespace foo` share one unit, and each can call the other's functions — including its private ones — by their bare names. Duplicate function names are detected later by Result.Compile, which keys on the qualified name, so two sources in *different* namespaces may each declare `baz`.

func (*Parser) RegisterExterns added in v0.10.0

func (p *Parser) RegisterExterns(src []byte, filename string) *Parser

RegisterExterns registers a //functy:extern source supplied by the host: the bodiless declarations of functions the host itself provides, whose real signatures their cty metadata cannot express. They surface on Result.HostExterns, feed help(), and are checked for collisions — but they are never compiled, and never attributed to the sources being parsed.

The source must carry the //functy:extern directive itself; registration verifies it rather than forcing the mode. That keeps one byte string meaning one thing however it is loaded: the same file a leaf package embeds is a valid standalone `.cty` that `functy fmt`, `functy symbols`, and an editor can open.

The canonical arrangement, which costs the leaf package no dependency on functy (`embed` is stdlib, and these bytes are opaque to it):

//go:embed externs.cty
var externsCty []byte

func Externs() []byte { return externsCty }

and in the host:

parser.RegisterExterns(pkg.Externs(), pkg.ExternsFilename)

Returns the Parser for chaining. Parsing is deferred to the first Parse/ParseAll, so registration order relative to RegisterType does not matter.

func (*Parser) RegisterOpenType added in v0.2.0

func (p *Parser) RegisterOpenType(name string, pred func(cty.Value) error) *Parser

RegisterOpenType registers a named open type backed by a predicate. An annotation naming it requires the value to satisfy pred and otherwise passes it through untouched (non-destructive), so extra attributes survive — suitable for marker-capsule objects (e.g. a ctx carrying _ctx plus other fields) and required-attribute objects (e.g. an error with at least a message). Returns the Parser for chaining.

An open type asserts only "this value satisfies pred," never a particular Go representation. Because the value passes through unchanged, a host that later type-asserts it in its own Go code (a capsule's concrete type, an object's attribute types) is trusting pred to have checked exactly that: a predicate looser than what the consuming code assumes hands that code an unexpected representation and can panic it. Make pred validate everything the value's consumers rely on. Use RegisterType instead when a value must be a specific capsule type — that is enforced by type identity, not a predicate.

func (*Parser) RegisterType added in v0.2.0

func (p *Parser) RegisterType(name string, ty cty.Type) *Parser

RegisterType registers a named (capsule) type. An annotation naming it is enforced by type identity: a value must already be of ty (or null), unless ty itself defines a conversion. Hosts use this for their cty capsule and rich-object types. Returns the Parser for chaining.

func (*Parser) RequireDeclaredTypes added in v0.2.0

func (p *Parser) RequireDeclaredTypes(v bool) *Parser

RequireDeclaredTypes requires every var/const declaration to carry a type (`: T`). See RequireParamTypes for the off-by-default and tighten-only semantics.

func (*Parser) RequireParamTypes added in v0.2.0

func (p *Parser) RequireParamTypes(v bool) *Parser

RequireParamTypes, when set, requires every function parameter to carry an explicit type annotation (`: T`; `any` is allowed but must be written). Off by default. A file may additionally request this via a //functy: directive, but a file can never relax a host-set requirement. Returns the Parser for chaining.

func (*Parser) RequireReturnType added in v0.2.0

func (p *Parser) RequireReturnType(v bool) *Parser

RequireReturnType requires every function to declare a return type (`-> T`). See RequireParamTypes for the off-by-default and tighten-only semantics.

func (*Parser) Types added in v0.2.0

func (p *Parser) Types() *TypeResolver

Types returns the parser's TypeResolver. Named types registered through the Parser are registered on it, so a host can resolve standalone type annotations (TypeResolver.ResolveType / ParseType) against the very same named types it uses for parsing `.cty` files.

type Result

type Result struct {
	Funcs  []*FuncDecl // parsed function declarations
	Tests  []*TestDecl // parsed test blocks (not registered as callable functions)
	Consts []Decl      // top-level const declarations (only when enabled)
	Vars   []Decl      // top-level var declarations (only when enabled)
	Types  []TypeAlias // top-level type aliases (namespace-scoped; see TypeAlias)

	// Externs are the bodiless declarations from //functy:extern sources, in source
	// order. They declare the signatures of functions the *host* provides, so they
	// are never compiled and never callable — Compile and CompileUnits ignore them
	// entirely. They exist so help(), `functy symbols`, and editor tooling can show
	// a host function's real signature, which its cty metadata cannot express: a cty
	// function fakes optional and defaulted arguments with a trailing VarParam, which
	// erases their names, their defaults, and (for the `f([ctx,] x)` convention) the
	// leading parameter itself.
	//
	// A slice, not a map, and deliberately: two declarations of one name is exactly
	// an overload set, so supporting overloads later is a relaxation of
	// checkExternNames rather than a change to this type.
	Externs []*FuncDecl

	// HostExterns are the externs the *host* registered with Parser.RegisterExterns,
	// as opposed to those declared by the parsed sources (Externs). They describe the
	// host's own functions, so they belong to no source file here.
	//
	// The split is what makes fmt safe, and is why this is a separate field rather
	// than a flag: a tool that renders *a source* — fmt, symbols, an outline — must
	// iterate Externs and ignore HostExterns, and gets that by default, because it
	// cannot reach this field without naming it. Merging the two would let `fmt` on a
	// user's file emit another package's declarations into it.
	//
	// Reflection (help()) reads both.
	HostExterns []*FuncDecl

	// Namespaces holds the `namespace a::b` declaration of each namespaced source
	// parsed together, in parse order (a source without one is in the global
	// namespace and contributes nothing here). The namespace a given declaration
	// belongs to is on the declaration itself (FuncDecl.Namespace and friends);
	// this slice exists so tooling that renders or lists the source — notably fmt,
	// which emits from the AST — can see the declaration itself.
	Namespaces []NamespaceDecl

	// Comments is every comment from every source parsed together, in source
	// order, retained with position. Declaration doc comments are also surfaced on
	// FuncDecl.Doc / Decl.Doc; this is the complete table for tooling that needs
	// all comments (e.g. a formatter).
	Comments []Comment

	// Directives are the directive comments from each source's leading comment
	// block (file-scope), across all sources. functy acts on its own `functy:`
	// namespace; others are passed through for the host.
	Directives []Directive
	// contains filtered or unexported fields
}

Result is the outcome of parsing one or more functy sources. It is a struct (rather than a bare map) so additional collected output can be added without breaking callers.

func (*Result) Compile

func (r *Result) Compile(evalCtxFn func() *hcl.EvalContext) (map[string]function.Function, hcl.Diagnostics)

Compile turns the parsed function declarations into cty functions for the host.

It returns the exported functions, keyed by qualified name; private functions are compiled but withheld. Use CompileUnits when the namespace-local layers are needed too.

func (*Result) CompileUnits added in v0.10.0

func (r *Result) CompileUnits(evalCtxFn func() *hcl.EvalContext) (*Compiled, hcl.Diagnostics)

CompileUnits turns the parsed function declarations into cty functions. Each function captures evalCtxFn and calls it at invocation time (late binding), so a function may call sibling functions and reference host globals that are finalized after compilation — enabling recursion and mutual recursion.

Functions are scoped by namespace. A namespace's functions see each other by their bare names through a unit layer (see unitCtxFn) and are handed to the host under their qualified names; `_`-prefixed functions are never handed over at all.

Duplicate function names within a namespace are reported as errors. Two different namespaces may each declare the same bare name — their qualified names differ, so they are distinct functions. The host remains responsible for detecting collisions against its own built-in functions when it merges Funcs into its registry.

Result.Externs is deliberately not compiled: an extern declares a function the *host* provides, so there is no body to compile and nothing to register.

func (*Result) RunTests added in v0.4.0

func (r *Result) RunTests(evalCtxFn func() *hcl.EvalContext) []TestOutcome

RunTests builds and runs every test block in source order against the given eval context. See RunTestsMatching for the details; this runs all tests.

func (*Result) RunTestsMatching added in v0.4.0

func (r *Result) RunTestsMatching(evalCtxFn func() *hcl.EvalContext, filter func(name string) bool) []TestOutcome

RunTestsMatching runs each test block whose name passes filter (nil runs all), in source order, returning one outcome per test that ran. Each test body is compiled like a niladic function (via BuildFunction) and called; a failed assert/throw surfaces as a *ThrownError and a skip(...) as a *SkipError. The test body sees the given eval context (all compiled functions + baseline).

skip must be visible throughout a test's call graph — not just the top-level body but any helper functions it calls, which late-bind to this same context. Rather than write `skip` into the caller's shared Functions map (a data race against any concurrent evaluation using the same context, since compiled functions late-bind to it), it is layered into a private child context that becomes the parent of every test body. The caller's context is never mutated, keeping `skip` a test-only builtin (absent from `functy run` / `check`).

type Return

type Return struct {
	Expr     hcl.Expression
	SrcRange hcl.Range
}

Return exits the enclosing function. Expr is nil for a bare return.

type Scope

type Scope struct {
	// contains filtered or unexported fields
}

Scope is a chained lexical scope. Variable lookup walks outward through the parent chain; the nearest binding wins. Declare always creates a binding in the innermost scope (shadowing any outer one); Set reassigns the nearest existing binding, converting to its pinned type.

dirty supports the interpreter's eval-context cache: it is set whenever a binding in this scope changes, so the interpreter knows to rebuild the merged *hcl.EvalContext before the next statement. Statements that cannot mutate a binding (a bare expression evaluated for its side effects, for instance) leave it clear, so consecutive such statements reuse one context.

func NewScope

func NewScope(parent *Scope) *Scope

NewScope creates a scope with the given parent (nil for a function root). A fresh scope is dirty so the interpreter builds its context once before use.

func (*Scope) Declare

func (s *Scope) Declare(name string, tc TypeConstraint, val cty.Value) error

Declare introduces a new binding in this (innermost) scope, shadowing any binding of the same name in an enclosing scope. For a typed declaration (a non-nil constraint) the initial value is coerced through it.

func (*Scope) Get

func (s *Scope) Get(name string) (cty.Value, bool)

Get looks up a variable's value by walking the scope chain outward.

func (*Scope) Set

func (s *Scope) Set(name string, val cty.Value) error

Set reassigns the nearest existing binding of name, coercing the value through that binding's constraint. It is an error if name is not declared in any enclosing scope, or if the value cannot satisfy the constraint.

func (*Scope) ToMap

func (s *Scope) ToMap() map[string]cty.Value

ToMap flattens the scope chain into a single name->value map, inner scopes taking precedence over outer ones.

type Signal

type Signal struct {
	Kind  SignalKind
	Value cty.Value // meaningful only for SignalReturn
	Label string    // target loop label for SignalBreak / SignalContinue ("" = innermost)
}

Signal carries a control-flow transfer up the call stack. A nil *Signal means normal completion (fall-through to the next statement).

type SignalKind

type SignalKind int

SignalKind classifies a non-local control-flow transfer that propagates out of statement execution.

const (
	// SignalReturn unwinds to the enclosing function with a value.
	SignalReturn SignalKind = iota
	// SignalBreak exits the innermost enclosing loop.
	SignalBreak
	// SignalContinue skips to the next iteration of the innermost loop.
	SignalContinue
	// SignalError unwinds a raised error (from throw or a failing expression)
	// until a try/catch handles it or it leaves the function.
	SignalError
	// SignalFallthrough transfers control to the next clause of the enclosing
	// switch. It is produced only by a Fallthrough statement and is always
	// consumed by execSwitch, never escaping the switch.
	SignalFallthrough
)

type SkipError added in v0.4.0

type SkipError struct{ Reason string }

SkipError is the Go error a functy function returns at its cty.Function boundary when a `skip` call unwinds out of it. It is distinct from ThrownError (a skip is not a failure): a test runner classifies it as skipped rather than failed. Reason is the optional message passed to skip().

func (*SkipError) Error added in v0.4.0

func (e *SkipError) Error() string

type Source

type Source struct {
	Filename string
	Bytes    []byte
}

Source is a single functy source: its filename (used in diagnostics) and raw bytes. ParseSources collects these from files, directories, and embedded filesystems.

func ParseSources

func ParseSources(inputs ...any) ([]Source, hcl.Diagnostics)

ParseSources collects functy sources from a heterogeneous set of inputs, returning the raw bytes of each (it does not parse them — call Parser.Parse). Each argument may be:

  • a string path to a .cty file, or to a directory (walked recursively, skipping dot-directories, collecting every .cty file);
  • a []string of such paths;
  • an embed.FS (walked recursively for .cty files);
  • a Source, used as-is;
  • a []byte, treated as the bytes of one anonymous source.

This mirrors how a host discovers .vcl/.vinit files, but yields raw bytes because functy has its own front-end.

type Statement

type Statement interface {
	// contains filtered or unexported methods
}

Statement is implemented by every functy statement node.

type Switch

type Switch struct {
	Subject   hcl.Expression
	Clauses   []Clause
	BodyRange hcl.Range // the `{ ... }` body span (open brace to close brace), for fmt
	SrcRange  hcl.Range
}

Switch is a switch statement. Subject is nil for the expression-less form, whose case values are boolean expressions evaluated like an if/else chain. Clauses are in source order; at most one is the default.

type TestDecl added in v0.4.0

type TestDecl struct {
	Name      string      // the test description (a string literal)
	Namespace string      // the namespace of the file the test was declared in ("" = global)
	Body      []Statement // body statements, like a function body
	BodyRange hcl.Range   // the `{ ... }` body span, for rendering (fmt)
	DefRange  hcl.Range   // spans `test` … closing `}`
}

TestDecl is a top-level `test "description" { … }` block. Its body is ordinary functy statements; the test passes if the body runs to completion and fails if an error (a failed assert, a throw, an eval error) unwinds out of it. Tests are not registered in the callable function namespace.

type TestOutcome added in v0.4.0

type TestOutcome struct {
	Name       string        // the test's description
	DefRange   hcl.Range     // the test block's source location
	Duration   time.Duration // wall-clock time to run the body
	Err        error         // nil if the test passed; the skip or failure otherwise
	Skipped    bool          // true if Err is a skip (not a failure)
	SkipReason string        // the reason passed to skip(), if any
}

TestOutcome is the result of running one `test` block. A test passes when its body runs to completion, is skipped when a `skip(...)` call unwinds out of it, and fails on any other error (a failed assert, a throw, an eval error). Passed/Skipped/Failed are three disjoint states.

func (TestOutcome) Diagnostics added in v0.4.0

func (o TestOutcome) Diagnostics() hcl.Diagnostics

Diagnostics renders a failed test for the standard hcl diagnostic writer: a thrown error (a failed assert or an explicit throw) with its source underline and operand detail, or — for any other failure — a plain diagnostic located at the test block. It returns nil for a passing or skipped test.

func (TestOutcome) Failed added in v0.4.0

func (o TestOutcome) Failed() bool

Failed reports whether the test ended in a real failure (not a skip).

func (TestOutcome) Passed added in v0.4.0

func (o TestOutcome) Passed() bool

Passed reports whether the test ran to completion without error.

type Throw

type Throw struct {
	Expr     hcl.Expression
	SrcRange hcl.Range
}

Throw raises an error whose value is Expr (a string becomes { message = <string>, value = null }; an object is used directly).

type ThrownError added in v0.3.0

type ThrownError struct{ Value cty.Value }

ThrownError is the Go error a functy function returns at its cty.Function boundary when an uncaught throw unwinds out of it. It carries the raw error value so a functy catch (or a Go host, via errors.As) recovers the full error object; Error() renders the message for any other caller.

func (*ThrownError) Diagnostics added in v0.4.0

func (e *ThrownError) Diagnostics() hcl.Diagnostics

Diagnostics renders the thrown error as hcl.Diagnostics so a host can print it with source context (an underline under the failing expression) using the standard hcl diagnostic writer. It delegates to ErrorDiagnostics on the carried value.

func (*ThrownError) Error added in v0.3.0

func (e *ThrownError) Error() string

type Try

type Try struct {
	Body         []Statement
	BodyRange    hcl.Range // the `try { ... }` body span, for fmt
	Catches      []CatchClause
	Finally      []Statement
	FinallyRange hcl.Range // the `finally { ... }` body span (zero when absent), for fmt
	SrcRange     hcl.Range
}

Try runs Body, routing a raised error through its catch clauses (first match wins) and always running a finally block. At least one of Catches/Finally is present.

type TypeAlias added in v0.2.0

type TypeAlias struct {
	Name string
	// Namespace is the enclosing namespace of the file the alias appeared in
	// ("" = global). It scopes name resolution (own-then-global) and lets a
	// consumer project an alias under the right namespace surface.
	Namespace string
	Type      TypeConstraint
	TypeSrc   string // source text of the aliased type (right-hand side), for rendering (fmt)
	DefRange  hcl.Range
}

TypeAlias is a resolved top-level `type Name = <type>` declaration. Aliases are namespace-scoped with own-then-global resolution, exactly like functions and consts: an alias declared in a namespaced file is visible to that namespace's annotations first, then falls back to the global (unnamespaced) aliases and the host-registered types. Two files in different namespaces may therefore each declare the same name; a `_`-prefixed alias is namespace-local (see IsPrivate).

func (*TypeAlias) IsPrivate added in v0.11.0

func (t *TypeAlias) IsPrivate() bool

IsPrivate reports whether the alias is namespace-local (a leading underscore). A private alias is still resolved and inlined into other aliases/annotations of its namespace (so `type items = list(_spec)` works), but a consumer projecting an export surface — e.g. the symbols library — withholds it, mirroring how a private function is withheld from the host's function table.

type TypeConstraint added in v0.2.0

type TypeConstraint interface {
	// Coerce applies the constraint to a value, returning the value to store or
	// an error if it does not satisfy the constraint.
	Coerce(cty.Value) (cty.Value, error)
	// Cty returns the underlying cty.Type (dynamic for `any`, void, and open
	// predicate types).
	Cty() cty.Type
	String() string
}

TypeConstraint is a resolved type annotation: it knows how to coerce a value to satisfy the annotation and exposes the underlying cty.Type. A nil TypeConstraint means "dynamic" (no annotation) — no coercion is applied.

It is the single source of truth for a declared type. Cty() derives the cty.Type, so callers that only need the type (host introspection, generated docs) need not carry it separately; Coerce() applies the enforcement.

Three coercion disciplines exist: structural/primitive annotations convert the value (cty/convert); a named (capsule) type is checked by identity; an open type is checked by a predicate and otherwise passed through untouched. This is why functy owns its resolver rather than delegating to ext/typeexpr, whose closed grammar cannot name capsule types or express open ("at least these attributes") types — and why a bare cty.Type cannot represent every constraint (a predicate type's Cty() is only dynamic).

func ConvertType added in v0.2.0

func ConvertType(ty cty.Type) TypeConstraint

ConvertType wraps a concrete cty.Type as a TypeConstraint enforced by conversion (cty/convert). Useful for a host that already has a cty.Type — for example a backward-compatible path for a type that was specified some other way.

type TypeResolver added in v0.2.0

type TypeResolver struct {
	// contains filtered or unexported fields
}

TypeResolver is functy's type system as a standalone, reusable component. It resolves functy type annotations (the same grammar used in `.cty` source) into TypeConstraints, against the built-in types plus any host-registered named types. It is usable independently of parsing functy programs — for example as a richer alternative to ext/typeexpr (a TypeConstraint carries enforcement via Coerce, not just a cty.Type), or for a host to type-check its own configuration (e.g. resolving a declared type and enforcing a value with Coerce).

A Parser holds one (see Parser.Types); register named types once and use them both for parsing `.cty` files and for resolving standalone annotations.

func NewTypeResolver added in v0.2.0

func NewTypeResolver() *TypeResolver

NewTypeResolver returns a resolver with functy's built-in types (including the `error` open type) and no host registrations.

func (*TypeResolver) ParseType added in v0.2.0

func (r *TypeResolver) ParseType(src []byte, filename string) (TypeConstraint, hcl.Diagnostics)

ParseType lexes a type annotation from source bytes and resolves it — a convenience for annotations stored as strings (e.g. a host config field).

func (*TypeResolver) RegisterOpenType added in v0.2.0

func (r *TypeResolver) RegisterOpenType(name string, pred func(cty.Value) error) *TypeResolver

RegisterOpenType registers a named open type backed by a predicate. See Parser.RegisterOpenType. Returns the resolver for chaining.

func (*TypeResolver) RegisterType added in v0.2.0

func (r *TypeResolver) RegisterType(name string, ty cty.Type) *TypeResolver

RegisterType registers a named (capsule) type, enforced by type identity. See Parser.RegisterType. Returns the resolver for chaining.

func (*TypeResolver) ResolveType added in v0.2.0

func (r *TypeResolver) ResolveType(expr hcl.Expression) (TypeConstraint, hcl.Diagnostics)

ResolveType resolves a parsed type-annotation expression (e.g. an HCL attribute value) into a TypeConstraint — the analog of typeexpr.TypeConstraint, but yielding a constraint that can enforce values, not just a cty.Type. `null` is not accepted here (it is only meaningful as a function return type).

type VarDecl

type VarDecl struct {
	Name     string
	Type     TypeConstraint // nil when unannotated (dynamic)
	TypeSrc  string         // source text of the type annotation, for rendering (fmt)
	Init     hcl.Expression
	Short    bool // declared with the `:=` shorthand (always untyped); preserved for fmt
	SrcRange hcl.Range
}

VarDecl declares a new local binding in the current scope.

Type is cty.NilType for a dynamic variable. Init is nil for a declaration with no initializer, which defaults to null (of the declared type, if any).

Directories

Path Synopsis
cmd
functy command
Command functy is a small CLI for loading and running functy source files.
Command functy is a small CLI for loading and running functy source files.
Package repl implements a generic interactive read-eval-print loop over an HCL expression engine.
Package repl implements a generic interactive read-eval-print loop over an HCL expression engine.
Package symbols is a prototype of the interface OpenTofu would embed to consume functy `.cty` files as symbol libraries (types + values + functions), per the in-draft Symbol Libraries RFC.
Package symbols is a prototype of the interface OpenTofu would embed to consume functy `.cty` files as symbol libraries (types + values + functions), per the in-draft Symbol Libraries RFC.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL