bcl

package module
v0.0.0-...-8a220ba Latest Latest
Warning

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

Go to latest
Published: Feb 27, 2025 License: MIT Imports: 14 Imported by: 0

README

BCL

Build Status Coverage Status Go Report Card Go Reference

Basic Configuration Language.

BCL is like HCL (the language driving Terraform, Packer and friends), but more basic:

  • no dollar-referenced variables; variable name can be used in an expression as it is
  • block starts with def keyword; seems to be more general compared to Terraform's resource keyword
  • dead-simple API: variables get evaluated automatically and fill the fields of the output structure; no strange limitations of where variables can be defined
  • variables with lexical scope
  • nested definitions
  • one-pass lexer, parser and VM executor
  • deserialization aka unmarshalling to static Go structs (possibly nested)

While being basic, BCL also has features reaching beyond HCL:

  • rich expressions operating on strings, int & float numbers, and booleans
  • planned: make the outside world accessible via environment variables, or via running a command and catching its output
Example:

BCL:

var domain = "acme.com"
var default_port    = 8400
var local_port_base = default_port + 1000

def tunnel "myservice-prod" {
	host = "prod" + "." + domain
	local_port  = local_port_base + 1
	remote_port = default_port
	enabled = true

	def extras {
		max_latency = 8.5 # [ms]
	}
}

bind tunnel -> struct

Go:

type Tunnel struct {
	Name       string
	Host       string
	LocalPort  int
	RemotePort int
	Enabled    bool
	Extras     struct {
		MaxLatency float64
	}
}
var config Tunnel

err := bcl.UnmarshalFile(file, &config)
fmt.Println(strings.ReplaceAll(fmt.Sprintf("%+v", config), " ", "\n  "))

Output:

{Name:myservice-prod
  Host:prod.acme.com
  LocalPort:9401
  RemotePort:8400
  Enabled:true
  Extras:{MaxLatency:8.5}}
Syntax

BCL has statements and expressions.

A basic statement is def block_type [block_name] {...} to define a block with field = value expressions inside. Such block after running Interpret will be available as a Block with a map of fields, and can be put into a static Go struct via Bind or Unmarshal. Blocks can be nested.

Both toplevel scope and a block can have variables created with the var x = expr statement, or just var x which leaves it uninitialized. Variables do not count when produding result Block structures, but they are taking part of the state flow.

Variables have lexical scope. Any block has access to the varables declared at the toplevel and also nested block have access to their parent's variables. There are no forward declarations.

Variables are mutable and can be mutated with the eval x = expr statement. This statement solely exists to allow evaluation of expressions in the context expecting stamenents, that is at the toplevel. Please note that inside the block the raw statements are allowed, for example field = value is actually an assignment expression. So, when in block, it's good to remember whether we are operating on fields or variables. This may be made more explicit in the future.

The last stament in the clan is print expr which is useful for debugging.

More on expressions below.

Expressions, data conversions

There are three basic types: numbers (int and float), strings and booleans.

Values in expressions know their types, although they are not enforced in the language; certain operations can cause runtime error.

Number arithmetics use int or float operations depending on the values involved; if any of the operands is float, then the int part is transparently converted to float. Complex numbers are not supported atm.

Strings can be concatenated with the plus +. If the right side of such plus is a number, it will be transparently coverted to string. However, the number plus string is an error.

Another string operator borrowed from numbers is asterisk *, this time the left side must be a string and right side just an int; the result is repeating the string given times.

Equality comparisons ==, != are allowed between all types, including mixing them. Obviously values of different non-number types are not equal.

Order comparisons <, >, , <=, >= are allowed between numbers and between strings, but not between mixed types.

There are boolean operators and, or, not behaving like in Python, or like &&, ||, ! in Ruby [1]: they are short-cirtuit and retain the type of an operand (1==1 and 42 will return 42). Non-boolean types can be a boolean operand; for this, there is a definition what is considered "falsey": false, nil, empty string, and zero, like in Python.

Boolean constants are true and false. Another constant is nil, value of an uninitialized variable (var a).

[1] Note that in Ruby ! has surprising priority, though.

Note on the parser

Versions up to v0.7.x used goyacc, since v0.8.0 there is a top-down Pratt parser with bytecode VM.

Cool stuff

Internals can be peeked in many ways, here is bytecode disassembly, execution trace with stack content, plus some stats:

./bcl -dts <<<'var x=1; def block{eval x=x+2; field=x}'
== /dev/stdin ==
0000    1:8  ONE
0001   1:20  DEFBLOCK      0 'block'         1 ''
0004   1:28  GETLOCAL      0
0006   1:30  CONST         2 '2'
0008      |  ADD
0009      |  SETLOCAL      0
0011      |  POP
0012   1:39  GETLOCAL      0
0014      |  SETFIELD      3 'field'
0016      |  POP
0017   1:40  ENDBLOCK
0018    2:1  POP
0019      |  RET
pstats.tokens:        20
pstats.localMax:       1
pstats.depthMax:       1
pstats.constants:      4
pstats.opsCreated:    13
pstats.codeBytes:     20
             0: 
0000    1:8  ONE
             1: [ 1 ]
0001   1:20  DEFBLOCK      0 'block'         1 ''
             1: [ 1 ]
0004   1:28  GETLOCAL      0
             2: [ 1 ][ 1 ]
0006   1:30  CONST         2 '2'
             3: [ 1 ][ 1 ][ 2 ]
0008      |  ADD
             2: [ 1 ][ 3 ]
0009      |  SETLOCAL      0
             2: [ 3 ][ 3 ]
0011      |  POP
             1: [ 3 ]
0012   1:39  GETLOCAL      0
             2: [ 3 ][ 3 ]
0014      |  SETFIELD      3 'field'
             2: [ 3 ][ 3 ]
0016      |  POP
             1: [ 3 ]
0017   1:40  ENDBLOCK
             1: [ 3 ]
0018    2:1  POP
             0: 
0019      |  RET
xstats.tosMax:         3
xstats.blockTosMax:    1
xstats.opsRead:       13
xstats.pcFinal:       20

Documentation

Overview

Package bcl provides interpreting of the Basic Configuration Language (BCL) and storing the evaluated result in dynamic Blocks or static structs.

It is also possible to first Parse, creating Prog, and then Execute it.

Prog can be dumped to a Writer with Dump and loaded with Load, there is also wrapper function LoadProg, to load previously dumped Prog instead of using Parse on the BCL input.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Bind

func Bind(target any, binding Binding) error

Bind binds the blocks selection (defined via binding) to the target. Target can be actually a struct or a slice of structs, and must correspond to a concrete Binding implementation, right now: StructBinding or SliceBinding.

When inside the struct (or the slice), the requirements are:

  • struct type name should correspond to the BCL block type
  • struct needs the Name string field
  • for each block field, struct needs a corresponding field, of type as the evaluated value (currently supporting int, string and bool)

The mentioned name correspondence is similar to handling json: as BCL is expected to use snake case, and Go struct - capitalized camel case, the snake underscores are simply removed and then the strings are compared, case-insensitive.

The name corresponcence can be also set explicitly, by typing a tag keyed `bcl` at the struct field:

type Record struct {
	Status string `bcl:"my_status"`
}

The lack of corresponding fields in the Go struct is reported as error. So is type mismatch of the fields.

If the binding type is a slice, and a slice pointed by target contained any elements, they are overwritten.

func Execute

func Execute(prog *Prog, opts ...Option) (result []Block, binding Binding, err error)

Execute executes the Prog.

func Interpret

func Interpret(input []byte, opts ...Option) ([]Block, Binding, error)

Interpret parses and executes the BCL input.

func InterpretFile

func InterpretFile(f FileInput, opts ...Option) ([]Block, Binding, error)

InterpretFile reads, parses and executes the input from a BCL file. The file will be closed as soon as possible.

func Unmarshal

func Unmarshal(input []byte, target any, opts ...Option) error

Unmarshal interprets the BCL input and stores the blocks selected via 'bind' statement in the target. See Bind for details.

func UnmarshalFile

func UnmarshalFile(f FileInput, target any, opts ...Option) error

UnmarshalFile interprets the BCL file and stores the blocks selected via 'bind' statement in the target. See Bind for details.

Types

type Binding

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

type Block

type Block struct {
	Type, Name string
	Fields     map[string]any
}

Block is a dynamic result of running BCL Interpret. It can be put into a static structure via Bind.

type FileInput

type FileInput interface {
	io.ReadCloser
	Name() string
}

FileInput abstracts the input that is read from a file. It is going to be closed as soon as it's read. The only information needed from a file besides reading/closing is that it has a name.

type Option

type Option func(*config)

func OptDisasm

func OptDisasm(x bool) Option

func OptLogger

func OptLogger(w io.Writer) Option

func OptOutput

func OptOutput(w io.Writer) Option

func OptStats

func OptStats(x bool) Option

func OptTrace

func OptTrace(x bool) Option

type Prog

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

func LoadProg

func LoadProg(r io.Reader, name string, opts ...Option) (*Prog, error)

func Parse

func Parse(input []byte, name string, opts ...Option) (*Prog, error)

Parse parses the input data, producing executable Prog.

func ParseFile

func ParseFile(f FileInput, opts ...Option) (prog *Prog, _ error)

ParseFile reads and parses the input from a BCL file, producing executable Prog. The input file will be closed as soon as possible.

func (*Prog) Dump

func (prog *Prog) Dump(dest io.Writer) error

func (*Prog) Load

func (prog *Prog) Load(src io.Reader) (err error)

type SliceBinding

type SliceBinding struct{ Value []Block }

type StructBinding

type StructBinding struct{ Value Block }

Directories

Path Synopsis
cmd
bcl command

Jump to

Keyboard shortcuts

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