Skip to content

Array proposal #281

@hinshun

Description

@hinshun

Types have multiple categories

Scalar: fs, pipeline, string, bool, int, option
Array: []fs, []pipeline, ...

And future composite types if we need.

Types may have association

Association operator ::, really only valid for option atm.

E.g. option::run, option::mount.

Previously this was implemented by just allowing : as a valid char in the Ident lexer symbol. It's no longer a valid char in Ident because we don't want to confuse idents with : with types that have association.

Arrays in function signature

func publish([]string regions) fs {
	// ...
}

Array declaration

func default() fs {
	image("alpine")

	# single line
	publish([]string{"us-east", "us-west-2"})

	# multi line: commas are optional (`hlb fmt` will format them out on multi-line)
	publish([]string{
		"us-east-1"
		"us-west-2"
	})

	# when type can be inferred, type is optional
	publish({
		"us-east-1"
		"us-west-2"
	})
}

# When function return type is array, body is array declaration.
func bases() []fs {
	image("alpine")
	image("alpine")
}

# When function return type is not array, overriding return register is compiler error.
func base() fs {
	image("alpine")
	image("alpine") # <- compiler error, orphaned graph
}

The context difference between []fs and fs functions may cause confusion, but this is the trade-off taken to afford other aspects of the array design.

func misunderstood() []fs {
	image("alpine")
	run("apk add -U curl") # <- compiler error, run on scratch
}

If we emit compiler errors on cases like run on scratch, it will get rid of unintended user errors.

Block literals no longer have a type prefix

Previously, block literals, e.g. fs { ... } are valid expressions, so can be used as arguments.

mount(fs {
	image("base")
	run("touch foo")
}, "/in")

The rationale will be explained in later sections, but in this proposal block literals only allow for type inference. (Declaring type is not allowed)

mount({
	image("base")
	run("touch foo")
}, "/in")

The function mount signature knows the first arg is a fs type, so that's how the checker will know what type the block literal is now.

Array declarations are NOT block literals

Block literal body { ... } has an modify context.
Functions execute using the current value of the return register.
Block literals do not define a type, and can only be inferred.
Single line block literals are delimited by ;, multi-line is optionally delimited.

Array declaration body { ... } has an append context.
Functions execute and append to the current array in the return register.
Array declarations may define a type, but can also be inferred.
Single line array declarations are delimited by ,, multi-line is optionally delimited.

Where they are similar is that a singular expression is valid as a block literal with a single statement or single element array.

# Single expression interchangeable with block literals for scalar `fs` arg.
func mount(fs input, string mountpoint) option::run

mount({ scratch }, "/in")
mount(scratch, "/in")

# Single expression interchangeable with array declarations for array `[]string` arg.
func publish([]string regions) fs

publish("us-east-1")
publish({ "us-east-1" })
publish([]string{ "us-east-1" })

Note that string interpolation like image("hinshun/${foobar}") is a block literal too with the string type inferred.

With Option

The grammar for a call expression is: <func-ident> <args> ("with" <expr>)? ("as" <expr>)}

Previously, a block literal was the common expression used:

run("npm install") with option {
	dir("/in")
	mount(src, "/in")
	mount(scratch, "/in/node_modules")
}

Where option { ... } was a block literal with the option type. But it has two main issues:

  • option wasn't the right type and had to infer option::run during type checking.
  • option block literals actually had an append context, not a modify context like fs.

Option blocks were really []option::run array declarations.

run("npm install") with []option::run {
	dir("/in")
	mount(src, "/in")
	mount(scratch, "/in/node_modules")
}

# Since type can be inferred, declaring array type is optional
run("npm install") with {
	dir("/in")
	mount(src, "/in")
	mount(scratch, "/in/node_modules")
}

Note that just simply []option is no longer valid

run("npm install") with []option { # <- compiler error, expected []option::run but got []option
	dir("/in")
	mount(src, "/in")
	mount(scratch, "/in/node_modules")
}

If else, for loops

If else statements, and for loops are planned but syntax is considered out of scope for this GitHub issue.

In this proposal block literal became strictly type inferred because they are a parsing menace for LL parsers like participle. Since call expressions that have no arguments have their () parens optional (i.e. scratch instead of scratch()), the <ident> followed by an open brace { is parsed into a block literal in many cases.

For example, consider the follow if statement construction:

if <boolean expr> {
    <stmt> ...
}

If the boolean expr is a no-arg function like foo, then it becomes:

if foo {
    <stmt> ....
}

But since block literals are also valid expressions, its ambiguous whether the <ident> { ... } is a block literal that forms the conditional for the if statement, or <ident> is the conditional and { ... } is the body of the if statement.

Once block literals are strictly type inferred, the ambiguity is gone.

Metadata

Metadata

Assignees

No one assigned

    Labels

    designDesign for a feature

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions