Skip to content
tsarnaPublic

About

Write procedural functions for HCL-based tools — compiles to cty functions you can drop into any eval context.

Resources

Contributing

Stars

4 stars

Watchers

0 watching

Forks

Latest commit

 

History

132 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

About

Write procedural functions for HCL-based tools — compiles to cty functions you can drop into any eval context.

Resources

Contributing

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages