functy

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 29, 2026 License: MIT Imports: 13 Imported by: 0

README

functy

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

CI

Overview

functy is a small Go/C-style 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:

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

See doc/cli.md for the full CLI reference and doc/language.md for the language reference.

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.

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 the functy run / functy check CLI commands.

Designed-but-not-yet-implemented features (a REPL, a formatter, type aliases, module imports, closures, ...) are noted where relevant in the documentation.

License

MIT — see LICENSE, like cty itself.

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 and type annotation is handed to HCL (hclsyntax.ParseExpression and ext/typeexpr), so operators, templates, function calls, and the cty type-constraint grammar behave exactly as they do elsewhere in HCL.

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) 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.

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{ SrcRange hcl.Range }

Break exits the innermost enclosing loop.

type Case

type Case struct {
	Values   []hcl.Expression
	Body     []Statement
	SrcRange hcl.Range
}

Case is one case clause; it matches if any of its values equals the subject.

type CondBranch

type CondBranch struct {
	Condition hcl.Expression
	Body      []Statement
	SrcRange  hcl.Range
}

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

type Continue

type Continue struct{ SrcRange hcl.Range }

Continue skips to the next iteration of the innermost enclosing loop.

type Decl

type Decl struct {
	Name     string
	Type     cty.Type       // from an optional `: T`; cty.NilType if unannotated
	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.

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 ExprStmt

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

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

type For

type For struct {
	Kind ForKind

	// 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
	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
	Params   []Param
	RetType  cty.Type // cty.NilType when no return type is annotated (dynamic)
	Body     []Statement
	DefRange hcl.Range
}

FuncDecl is a top-level function declaration.

type IfChain

type IfChain struct {
	Branches []CondBranch
	Else     []Statement
	SrcRange hcl.Range
}

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

type Param

type Param struct {
	Name     string
	Type     cty.Type       // cty.NilType when unannotated (dynamic)
	Default  hcl.Expression // non-nil for an optional parameter
	Variadic bool           // true for the trailing *rest parameter
	DefRange hcl.Range
}

Param is a single function parameter.

A parameter is required unless it has a Default 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.

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) 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 and merges their declarations into one Result. Per-source declarations are concatenated in order; duplicate function names across sources are detected later by Result.Compile.

type Result

type Result struct {
	Funcs  []*FuncDecl // parsed function declarations
	Consts []Decl      // top-level const declarations (only when enabled)
	Vars   []Decl      // top-level var declarations (only when enabled)
}

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. 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.

Duplicate function names within the result are reported as errors; the host is responsible for detecting collisions against its own built-in functions when it merges the returned map into its registry.

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, ty cty.Type, 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 the initial value is converted to ty.

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, converting the value to that binding's pinned type. It is an error if name is not declared in any enclosing scope, or if the value cannot be converted.

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
}

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
)

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
	Cases    []Case
	Default  []Statement
	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. Default is nil when there is no default clause.

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 Try

type Try struct {
	Body      []Statement
	HasCatch  bool
	CatchName string // error binding name; "" when omitted (catch { ... })
	Catch     []Statement
	Finally   []Statement
	SrcRange  hcl.Range
}

Try runs Body, optionally routing a raised error to a catch block and always running a finally block. At least one of Catch/Finally is present.

type VarDecl

type VarDecl struct {
	Name     string
	Type     cty.Type
	Init     hcl.Expression
	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.

Jump to

Keyboard shortcuts

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