Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Move container and function attributes (packed, inline, ..) to anonymous struct literal instead of using keywords #4285

Closed
ghost opened this issue Jan 25, 2020 · 15 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@ghost
Copy link

ghost commented Jan 25, 2020

An attempt at language simplification, but with breaking syntax changes. Someone with more knowledge will have to determine if this is a feasible change or not.

Changes to grammar with this proposal:

// for functions
const f = fn( x : SomeType ) SomeType "<anon.struct.literal with config, and trailing colon>" {
  // function body
}

// container can be either struct, enum, union, ....
const c = container "<anon.struct.literal with config, and trailing colon>"  {
  // container members
};

Here, the anonymous struct literal initializes a built-in struct, like FunctionConfig or StructConfig.

  • Each container or function declaration requires the corresponding config type.
  • The struct types have fields corresponding to the configuration options that currently require keywords.
  • It is a compile error to use nonsense combinations on the field values
  • The struct fields would usually be a bool or an enum.

Example. Note that omitted options simply use the default value in the configuration struct.

const f = fn(x : u32) bool .{.inline=true, .callconv =.C }:{
	return x > 14;
}

// on separate lines
const f = fn(x : u32) bool .{
	.inline=true, .callconv =.C
}:{
	return x > 14;
}

// the standard syntax can be the equivalent of passing an empty struct literal
const f = fn(x : u32) bool 
.{} : // not necessary
{
	return x > 14;
}

// containers
const S = struct .{.packed=true} : {
	a: bool,
	b: bool,
	
	fn numToSelf(num: u32) S {
		...
	}

};

const E = enum .{
	.tagType = u2
}:{
	ON,
	OFF,
	UNKNOWN,
};

The colon between the braces is meant to give some indication that the scopes are connected and belong to the same entity.

This proposal could be considered together with #2873, as that is also a syntax breaking proposal that aims at language simplification.

@daurnimator daurnimator added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Jan 25, 2020
@andrewrk andrewrk added this to the 0.7.0 milestone Jan 28, 2020
@ghost
Copy link
Author

ghost commented Feb 1, 2020

Might as well also consider the above idea in a broader scope:

Extended proposal: Anonymous Struct Literal for Configuration (AConf) at declaration site for zig entities like structs, functions, and beyond

Disclaimer: This extended proposal takes the idea of using anonymous struct literals for configuration of zig entities to its extreme conclusion. Thus, smaller subsets of this idea is what should actually be discussed, and the specific syntax choices I made are open to discussion and can certainly be refined a lot.

The intended benefits are language simplification yet increasing language extendibility and capabilities at the same time.

Note. Will abbreviate "Anonymous struct literals for configuration" with AConf in the text below:

Proposal benefits:

Proposal caveats:

  • More cumbersome syntax when writing code. Maybe not so bad when reading code though.
  • (?) Implementation concerns.
  • Keywords are often also used outside the "declaration context", so removing them won't be feasible.

General:

With this proposal:

  • AConf literals will be allowed at declaration sites, where they instantiate built in structs containing all configuration options (with defaults defined) that the entity being declared might need.
  • Example of configuration options: Whether a struct is packed or not, what an enum's tag type is, which type predicate to check a struct against at compiletime (Proposal: User definable type constraints on polymorphic parameters #1669), ...
  • There would be one built-in config struct type per "declarable type". Examples would be : ZigStructConfig, ZigEnumConfig, ZigUnionConfig, ZigErrorConfig, and ZigFunctionConfig. And if this propsal is taken to the extremes, there would be ZigFieldConfig and ZigIdentifierConfig as well.
  • The types of each config "field" will usually be booleans or built-in enums.

Syntax example:

// for containers, the AConf literal comes after the keyword, before the container scope

const St = struct .{
	.packed=true, // use default values for non-specified configuration options
}:{ // - the colon between the scopes is meant to clarify that they belong to the same entity
 a: bool,
};
const En = enum .{.tagType = u1} :{ON,OFF};
const Un = union .{.inferEnum = true} :{ ON : u4, OFF : u12};
const Er = error .{ } : { ... }

// for functions, the AConf literal comes in front of the function body
// - assuming it is desirable to distinguish container and function syntax somewhat
const f = fn(a: u32) bool .{.inline=true, ...} : { $func_body }

// current syntax would be syntactic sugar for passing an empty AConf literal. 
// - (config uses default values only)
const S = struct // .{} : // unchanged if first '//' is removed
{}

So, what used to be written as const C = containername( $optional_param ){ $container_scope }; becomes
const C = containername .{$AConf_members}:{$container_scope};.

AConf literals for structs:

Possibilities:

// Attempt at #1669
const myTypePredicate = fn(comptime x : type) bool .{} :{
	// return true if x has the desired methods
}

const Implementation = struct .{.typePredicate = myTypePredicate} :{
	// implement methods desired by 'MyTypePredicate' here, or expect a compile time error
};

const f = usesMyType(x : var(myTypePredicate)) bool .{} :{
	// ...
}

const main = fn() void .{}:{
	const I : Implementation = .{ ... };
	_ = usesMyType(I); // succeeds
}

AConf literals for functions:

Possibilities:

AConf literals for enums and unions

  • Possible to remove dedicated syntax for setting the tagtype. Can make it an AConf "field" instead?
const E = enum .{.tagType=u4} : { ... };

const U = union .{.inferEnum=true} :{ // VS 'union(enum)'
	A : u32, B : u64,
};

const U = union .{.tagType=E} :{ ... }; // VS 'union(E)'

AConf literals for field declarations

const S = struct .{} :{
	a : .{.visibility=.Private} bool, 
	b : .{.visibility=.Public, .immutable=true} bool, 
};

AConf literals for any identifier

  • Can't think of any use case, so this is probably worthless.
  • Would let you attach comptime known metadata to identifiers
var x .{.nothingComesToMind=true} = 32.0;

AConf literals for tests

Maybe tests could be configurable using AConf literals.

test "mytest" .{.expectFailure=true} :{
	//Do the test
}

AConf literals together with distinct types

Here another keyword distinct is introduced, but in turns it opens up for more options on distinct types (#1595) than the proposed @Distinct built-in does, simply by adding the additional configuration options and a dedicated namespace. Might also be useful for #4292

const My_f64 = distinct .{
	.base = f64, // as an alternative to builtins, like '@Distinct(f64);'
	.disableBinaryOperators = true,
	.useNamespaceOfBase = false, // NOTE 1
}:{
	// NOTE 1: Optional namespace if you want to define your own methods for an existing type,
	// but you can only use the base namespace OR your own. Not both simultaneously.
};

related to NOTE 1

AConf literals for sequential blocks

Another idea requiring the introduction of a new keyword, seqblk. This would add configuration options
for blocks of code. Maybe even operator overloading ( #871 ) could be feasible with something like this:

seqblk .{.operatorOverload=MathStruct, .mode=.SafetyOff } : {	
	const y = x1 + x2; // equivalent to MathStruct.add(x1,x2) within scope
}

// labeled break or continue could perhaps look like this with the above syntax changes
while(cond) seqblk a {
	while(cond2) seqblk b {
		// ...
		if(cond3) { continue a};
	}
	// ...
}

@ghost
Copy link
Author

ghost commented Mar 5, 2020

Some further exploration of this idea.

  • Change the aconf literal syntax to be either ..{ } or .:{ } instead of .{ } to more easily distinguish aconf literals
  • Sugar: ..{.Immutable=true,} can be written ..{.Immutable,}
  • Aconf literals for Type configs are written .:{ }. These aconf literals are part of type expressions just like [], [_], const etc can be.
  • Do not indent aconf literal contents so that it doesn't mix visually with container or function bodies
  • Change }:{ into },{ for attached scopes/blocks

The main problem is that the syntax this proposal tries to simplify has a lot of different edge cases and combinations that all have to work.

const GridPoint = struct ..{
.Immutable, 
.CopyOk
},{
	var foo: .:{.align = 4} u8 = 100;

	x: i32 = 0,
	y: i32 = 0,

	const mirrorX = fn(self: @This()) GridPoint {
		return .{.x = -self.x, .y = self.y};
	}

	const mirrorY = fn(self: @This()) GridPoint ..{
	.mode=.Inline,
	.callConv=.C, 
	},{
		return .{.x = self.x, .y = -self.y};
	}

	const miscFn = fn(a: u32, b: f64) Result ..{
	.inline=true
	},{
		{
		var k : u8 = 0;
		while(k<0) : (k+=1) outer ..{
		.volatile=true
		},{
			if ( k==a){
				continue outer, _;
			}
		} // end while
		}

		if(true) ..{
		.constantTime=true
		},{
			const x = 3+1;
			const y = b*32.1;
			const res = x-2*y;
		}
	}

};

@ghost
Copy link

ghost commented Apr 29, 2020

I propose: no prefix for AConfs. Assume a bare brace opens an AConf, and require a prefix to elide it:

const foo = fn () void : {
    // ...
}

// Semantically identical
const foo = fn () void {} : {
    // ...
}

That said, I don't think AConfs are a good idea on basic types. There's no way to make that syntax clean.

@ghost
Copy link
Author

ghost commented Apr 29, 2020

I propose: no prefix for AConfs. Assume a bare brace opens an AConf, and require a prefix to elide it:

It does look cleaner and is probably a good change, but at the same time it won't be as obvious to the reader that you are supposedly "initializing" a config struct and attaching it to the declaration.

That said, I don't think AConfs are a good idea on basic types. There's no way to make that syntax clean.

Agreed. The main "use" of AConfs would be having uniform syntax for configuring containers, functions and possibly blocks.

@ghost
Copy link

ghost commented Apr 29, 2020

you are supposedly "initializing" a config struct

Not really. This is all at comptime, so no actual initialisation is taking place -- and I'm compelled to think of every function and container as already having an AConf, regardless of whether it's explicitly declared.

@ghost
Copy link

ghost commented Apr 29, 2020

For functions specifically, the AConf could go before the params, for unambiguity while also preserving existing syntax:

// Don't see a struct? Don't parse it! Simple as that.
const foo = fn .{ callconv = .Naked, .export = true } () void {
    // ...
}

@ghost
Copy link

ghost commented Apr 30, 2020

Actually, we could just have an AConf be a regular struct, and require it to come right before the body. That way, if the parser sees a dotbrace, it'll parse an AConf, if not, it'll proceed as normal. This change doesn't even need to be breaking then:

// Still allowed
const foo = struct {
    // ...
}

// Now possible
const bar = enum .{ .packed = true } {
    // ...
}

// We can even double up
const baz = async fn () callconv(.Naked) void .{ .export = true } {
    // ...
}

// And if we want to go really crazy, we can even do this:
const quux = fn (comptime conf: FnConf) void conf {
    // ...
}

@SpexGuy
Copy link
Contributor

SpexGuy commented Apr 30, 2020

Assuming #4630 is resolved with "params are in scope, callconv stays after parameter list", it would mean that comptime params need to be in-scope for this to replace callconv, and that this would have to go after the parameter list in order to be consistent. If that's the case, there's no longer any reason that this needs to be a literal. These forms should all be allowed:

const a = fn () .{ ... } void { ... };
const conf = builtin.FnAttributes{ ... };
const b = fn () conf void { ... };
const c = fn (comptime conf_param: builtin.FnAttributes) conf_param void { ... };
const d = fn (comptime T: type) determineAttributes(T) void { ... };
const e = fn (comptime T: type)
blk: {
    const a = computePart1(T);
    return computeAttributes(a);
}
void { ... };

But having the config be positional introduces some parsing issues. I have a really hard time visually distinguishing these when I don't have an understanding of the names:

const func = fn (a: u32, comptime T: type)
blk: {
    const b = ComputePart1(T);
    break :blk computePart2(b);
}
blk2: {
    const b = ComputePart1(T);
    break :blk2 ComputePart3(b);
}
{
    const b = ComputePart1(T);
    return computePart4(a, b, T);
};

The compiler does also. It would have to look ahead when it encounters an expression after a parameter list to determine of the expression is specifying the config or the return type. Zig uses position to identify parameter lists and return types when parsing, but it's able to do this unambiguously and without significant lookahead in part because neither of those are optional. Additionally, if you're new to the language and you see this, there's nothing obvious to google to figure out what it does. But with a keyword:

const func = fn (a: u32, comptime T: type)
attr(blk: {
    const a = ComputePart1(T);
    break :blk computePart2(a);
})
blk2: {
    const a = ComputePart1(T);
    break :blk2 ComputePart3(a);
}
{ ... }
  • It's very clear what is the return type and what is the attribute
  • It's six extra characters for a macro that is rarely needed
  • Newbies can google zig attr on function and figure it out
  • The parser knows when it's parsing an expression whether that expression is computing the config, the return type, or the function body without having to look ahead and see if there's another expression after it.

@ghost
Copy link

ghost commented Apr 30, 2020

Most of these are alleviated by putting the return type before the config, as it was in my original comment, but the visual clarity is an important consideration.

@ghost
Copy link

ghost commented Apr 30, 2020

So then we'd have:

const func = fn (a: u32, comptime T: type)
// Unambiguously the return type
blk2: {
    const a = ComputePart1(T);
    break :blk2 computePart3(a);
}
// I'm tempted to say we could always unambiguously distinguish the config from the body,
// as the body must be an unlabelled scope and hence starts with a bare brace, and no other
// language construct starts with a bare brace -- thus, because the config cannot be an
// unlabelled scope, it's apparent from the first character what we're parsing
blk: {
    const a = ComputePart1(T);
    break :blk ComputePart2(a);
}
{ ... }

@ghost
Copy link

ghost commented Apr 30, 2020

I'm tempted to say it's not much more to learn, and it's not much to ask to have the most common syntax in the language be front and center in any learning materials, but that is very much a matter of opinion, and this could go either way.

@ghost
Copy link

ghost commented Apr 30, 2020

Also, on a different note, I'd like to highlight how beautifully this works with the new inline asm syntax being worked on right now over at #215. It's truly a match made in heaven.

@ikskuh
Copy link
Contributor

ikskuh commented Apr 30, 2020

After some discussion with @EleanorNB i want to point out some things:
First: I don't think this will add a lot of benefit to the language, as we can configure our functions already with const data, even if it is a bit verbose.

const libCC = .Interrupt;
export fn func1() callconv(libCC) void { }
export fn func2() callconv(libCC) void { }

Second: A lot of the configurations are actually mutual exclusive:

  • Functions
    • inline is mutual exclusive to callconv (inline
    • comptime args are mutual exclusive to callconv (all comptime functions are "zigcc")
    • async is mutual exclusive to inline and callconv (as async is a calling convention)
    • export is mutual exclusive to comptime args and inline
  • Structs/Unions
    • packed is mutual exclusive to extern to default

This means that a lot of config values are either illegal or don't make sense and removes a lot of the proposed versatility of the configs isn't possible anyways.

Third: It removes a lot of useful tooling.

  • Searching for all exported functions? grep -rnIi export
  • Searching for all functions with modified calling convention? grep -rnIi callconv
  • Searching for all functions with a special alignment? grep -rnIi fn.*align

Having configurations would remove the ability to fast-grep for stuff and make text-based replacements. Refactoring would get a lot harder as you cannot just do text operations, but need semantic analysis (and thus a full zig compiler)

And fourth: To my experience, it was never necessary to have a function configurable except for a thing like calling convention. I've written a lot of weird and wild code, programmed a lot of platforms and it never occurred to me that i have to decide at the callsite for something like callconv, function alignment or such. These are already special cases for itself (especially function alignment), but functions sharing a lot of those special problems is nothing that was really a problem to my experience.

Also: We can inline functions with @call at the callsite, so no need to make it dependent on a comptime parameter.

I disapprove this proposal as it adds a lot of language complexity for imho little to no benefit

@ghost ghost mentioned this issue May 1, 2020
@ghost
Copy link

ghost commented May 1, 2020

I made a better version of this proposal. See above.

@andrewrk
Copy link
Member

There is a lot going on here. This issue is now partially superseded by #8643 and therefore I am closing it. I invite the participants in this thread, if motivation still exists, to regroup and reform the proposal into a new issue that takes #8643 into account.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests

4 participants