-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
Comments
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 beyondDisclaimer: 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:
General:With this proposal:
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 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
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
var x .{.nothingComesToMind=true} = 32.0;
AConf literals for testsMaybe tests could be configurable using AConf literals. test "mytest" .{.expectFailure=true} :{
//Do the test
}
AConf literals together with distinct typesHere another keyword 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.
}; AConf literals for sequential blocksAnother idea requiring the introduction of a new keyword, 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};
}
// ...
}
|
Some further exploration of this idea.
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;
}
}
}; |
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. |
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.
Agreed. The main "use" of AConfs would be having uniform syntax for configuring containers, functions and possibly blocks. |
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. |
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 {
// ...
} |
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 {
// ...
} |
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);
}
{ ... }
|
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. |
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);
}
{ ... } |
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. |
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. |
After some discussion with @EleanorNB i want to point out some things: 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:
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.
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 I disapprove this proposal as it adds a lot of language complexity for imho little to no benefit |
I made a better version of this proposal. See above. |
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:
Here, the anonymous struct literal initializes a built-in struct, like
FunctionConfig
orStructConfig
.Example. Note that omitted options simply use the default value in the configuration struct.
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.
The text was updated successfully, but these errors were encountered: