-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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: Stack-Capturing Macro #6965
Comments
This is a surprisingly elegant solution to the "modern usecase of lambda expressions". I'm not sure if zig needs this, but the basic idea is quite nice and clean. But some questions:
var threshold = 0.4; // runtime value
const is_included = |x| { return x > threshold; };
var count: usize = 0;
count += my_list_1.countAll(is_included);
count += my_list_2.countAll(is_included);
const err_or_void = ||{
try foo();
try bar();
}();
someCleanup();
if(err_or_void) |_| { } else {
std.debug.print("oh no!\n", .{});
return error.Unexpected;
} I'm a fan, not sure though if zig needs this feature… |
The proposal as written would say no, but this is an interesting use case. I think I'm still inclined to say no though. A property of macros as described above is that they execute only as part of the statement in which they are declared. You can't declare a macro on one line and then execute it on a different line. This means there is never any confusion about whether a macro is capturing the current value of a stack variable or a reference to the variable - it's both. This also ensures clearly that it's impossible for a macro to escape the bounds of the locals it references. A macro really isn't a function that executes later, it's a block that executes right now, but in a deeper scope.
No, but you can do this: const localTry = inline fn(blk: macro () !void) !void {
try blk();
}
const err_or_void = localTry(||{
try foo();
try bar();
});
someCleanup();
if(err_or_void) |_| { } else {
std.debug.print("oh no!\n", .{});
return error.Unexpected;
}
That's kind of how I feel too :P |
I absolutely love the basic idea of this proposal. Most languages simply don't have a reasonable way to abstract over code blocks that only differ in a few lines of code and a few dynamic variables. Function pointers and non-capturing anonymous functions help somewhat, but usually have a substantial cost in both verbosity and performance. In my opinion, the proposed functionality could considerably increase the power of the language for implementing various types of iterators, filters, generic algorithms and context-wrapping functions (like |
I'm not as much of a fan.
The overall design makes sense to me, but I don't appreciate the individual changes either:
I personally appreciate the idea of
var stackToCapture = struct{
count: usize = 0;
fn action(self: *@This(), key: anytype, value: anytype) !void {...}
} {};
usingnamespace stackToCapture;
//refer to `count` as if it were a local variable directly, pass &stackToCapture as callback-with-data |
Yeah that would be my opinion as well. It would get too messy quickly and most macros are unique anyways...
I really like this, we created a new language feature out of macros already, killing two birds (#229, #5610) with one stone (#6965)
Not really. Capturing local variables either requires a lot of indirections and storing the locals in a "local variables struct", which means every normal access is prefixed or you have to capture them by hand into pointer struct fields
I think this is worth a separate proposal, as this is solving a completly different use case which i discovered could be solved with this proposal
This is confusing and |
I don't think there is much of a semantic difference between declaring stack variables separately and grouping them into a struct and declaring an instance of that. The only difference is in access syntax.
Why? As I understand it, importing symbols from a different namespace means your code depends on the symbols declared somewhere else, which is an implicit remote dependency.
That's my point. Both solutions just present syntactical convenience over something we can already do with less limitations. |
I quite like this proposal personally. |
This is ambiguous I think, I'll change it to require an empty capture list. That's also ambiguous with the
This is a fair point. This is actually where my proposal deviates from Kotlin's definition. Returns in a kotlin inline lambda return from the outer function. But this only works because the last expression in a kotlin lambda produces its value, which gives them an alternate syntax to return. I suppose we could allow a label on macros though, and use
It's true that you can accomplish the same computation with anonymous functions and state objects, but you cannot assume that that will be performant. Using functions here has several problems:
Essentially, using functions for this is taking something that is straightforward for the frontend (put that loop in my code and put this code in the loop), turning it into a really hard problem for the optimizer, and then hoping that the optimizer will figure it out. The proposed macros sidestep this problem by forcing the frontend to emit something that is friendly to the backend (just a bunch of scopes, no pointers to locals, no extra functions). That's what makes this different. It's more constrained than lambdas, but it guarantees that it will not generate a function call, it will not generate extra copies, and it will not generate extraneous memory clobbers. IMO these guarantees are enough to make this a sufficiently different operation from passing a function (and therefore not another way of doing "the same thing").
This brings up a problem that I didn't think about before. I think putting |
Word. This proposal is a simple syntax transformation, should be easy to implement in both stage1 and stage2 by just doing some AST transformations whereas function pointers yield inherently worse results due to increased complexity |
Indeed I was hoping that this type of transformation would be something fully in the realm of capability for an optimizing compiler.
Even if Zig already had macros, I would advocate to implement these optimizations for the
Interesting, I was under the impression people liked to use templates because (just like Amendment: I agree that this is "just an AST transformation" that "should be easy to implement". My concern is whether we want to facilitate this one use case with special-case syntax, type, and semantics, instead of reducing friction in the components we already have in the language. |
The prevailing opinion in the game development community seems to be that templates and lambdas can be fine for runtime performance if all optimizations are turned on, but compilation time and performance in debug mode are also very important, and templates are terrible for both of these. Indeed, all of the optimizations you described are possible and will probably be done in release-fast mode. But the other build modes also matter, and so does compilation time. That said, header parsing time is a massive sink for C++ compile time, and templates can only reasonably go in headers. Zig is better about this, so it may not be as much of a problem.
This is a good and valid concern, but I don't see any way to reduce the friction enough here without introducing a new concept. /// iterates the entries in a bucketed hash map
inline fn foreach(self: *@This(), action: anytype) !void {
for (self.buckets) |bucket| {
var current = bucket.first;
while (current) |node| : (current = node.next) {
try action.each(&node.key, &node.value);
}
}
}
fn countValues(self: *@This(), value: Value) usize {
var countValuesEach = struct{
count: usize = 0,
target: Value,
pub inline fn each(self: *@This(), k: *Key, v: *Value) !void {
if (v.* == self.target) self.count += 1;
}
} { .target = value };
self.foreach(&countValuesEach) catch @compileError("no errors possible");
return countValuesEach.count;
}
fn hasValue(self: *@This(), value: Value) bool {
self.foreach(struct{
target: Value,
pub inline fn each(self: @This(), k: *Key, v: *Value) !void {
if (v.* == self.target) return error.FoundTheValue;
}
} { .target = value }) catch return true;
return false;
}
fn findKeysWithValue(self: *@This(), value: Value, allocator: *Allocator) ![]*Key {
var findKeysEach = struct{
list: std.ArrayList(*Key),
target: Value,
pub fn init(target: Value, alloc: *Allocator) @This() {
return .{ .target = target, .list = std.ArrayList(*Key).init(alloc) };
}
pub fn deinit(self: *@This()) void {
self.list.deinit();
}
pub inline fn each(self: *@This(), k: *Key, v: *Value) !void {
if (v.* == self.target) {
try self.list.append(k);
}
}
}.init(value, allocator);
errdefer findKeysEach.deinit();
try self.foreach(&findKeysEach);
return findKeysEach.list.toOwnedSlice();
} The mental gymnastics that I have to do to try to understand this code are too much. It's not worth making the I don't think the
Limiting this to comptime-known values doesn't actually work. There's no way to run a function at runtime that returns a comptime-known value. This would have to be some totally new system in order to be guaranteed to be statically resolved, and I can't imagine any version of that which produces readable code. It is possible that generators may be able to help here, by making it easier to write iterators. But that also assumes a very powerful optimizer, and puts a burden on it to think more (and longer) about how to optimize. |
Making this an expression Other than that, sounds rather neat. |
Valid point for unoptimized
I was a bit unclear. I meant that the syntax of Here's one more idea to help the syntax of the status-quo approach (though decidedly not the compilation complexity): //Introduce a new builtin: @currentScopeMixin(comptime Extension: type): anytype.
//This magically makes all stack declarations visible at this point (including function parameters) be auto-wrapped into a struct instance, and adds all declarations of `Extension` to that struct type.
//Downside: Would require adding `const` fields to the language for the semantics of this builtin type. (Then again, we already have `comptime` fields just for tuples...)
fn findKeysWithValue(self: @This(), value: Value, allocator: *Allocator) ![]*Key {
var list = std.ArrayList(*Key).init(allocator);
errdefer list.deinit();
try self.foreach(&@currentScopeMixin(struct{
fn each(self: anytype, k: *Key, v: *Value) !void {
if (v.* == self.value) {
try self.list.append(k);
}
}
}));
return list.toOwnedSlice();
} EDIT: fixed passing the address of the instance constructed by |
|
|
This solves the problem but doesn't provide the same guarantees. Instead of doing
This is interesting, but it's by definition an implicit stack capture, which is exactly what this proposal is trying to avoid. The result of this macro could easily be put into a struct or copied somewhere else and referenced later, causing stack corruption.
I think this is a good idea. Blocks will still be necessary to encode a statement, but blocks are expressions so this is ok. linkedList.foreach(|x| { sum += x; });
linkedList.sumIf(|x| x > 4);
I'm not sure what this means, but if I had to guess based on the context you're saying that empty capture lists aren't a thing anywhere else in the language so this is inconsistent? That's a fair point. An alternative would be to have an operator that indicates that a macro is being declared. Maybe something like this? linkedList.foreach(=> |x| { sum += x; });
sevenTimes(=> std.debug.print("this will print seven times\n", {}));
I may not have been clear in the proposal, but the function which passes the macro doesn't need to be inline. Only the function receiving the macro needs to be marked inline. The inline specifier is in the function header, right next to the macro prototype in the parameter list, which is validated by the compiler. So you're not going to get a surprise compile error in one place because some other function isn't inline. The compile error will appear on the function that is trying to declare a macro prototype parameter even though it isn't marked inline. Macros and functions are not at all interchangeable, so I don't see the problem here. |
Upon understanding the proposal a bit more, I think it's a good idea, but should be managed with care. I think, in particular, that there should be no special-case control flow -- keywords should require labels to snap to macro scope, as in blocks (this fits well with making macro bodies expressions, as well as #5610). Also, I don't think a
inline_func(switch (foo) {
.bar => |x| x < 4, // Is this a payload capture, or a macro?
else => unreachable,
}); To resolve that, why not reuse the trick we already use to declare literals without prepending them with types -- the humble I have a feeling Andrew might reject this proposal out of hand, because it looks a bit too magical -- I hope that doesn't happen. It really is incredibly powerful, and there's no magic happening if you think about it. |
Again, this holds true for unoptimizied EDIT:
Oh, I just now realized that the intermediate
This proposal introduces a new comptime-only type and states new rules that apply to no other type.
So now that we gave the same usage restrictions, here are the remaining differences between proposed
I think the one thing left unaccounted for would be Also thank you for seriously addressing my comments. |
Yeah, I've come around to this one. I always found nonlocal returns to be the weirdest part of kotlin's version, but that's because I was thinking of them as producing lambda functions. Thinking of them as expressions/blocks makes this much less weird. I'll modify the proposal to bring this in.
I don't think this can be allowed. Macros are cannot be values, and arbitrary expressions cannot produce them. If that can happen, then you have this problem: // argument is block expression producing macro value
withMacro(blk: {
var local: u32 = 0;
break :blk => { local += 1 };
}); This macro captures a variable that goes out of scope before use. So invoking this macro corrupts the stack. That sort of stack capture was what was ruled out in #229. In theory, there is an alternate form of this proposal where macros are values and the compiler verifies at each inlining site that they don't capture any variables that are out of scope. But I felt that that could produce all kinds of complexity that doesn't make sense. (what does it mean to store a macro in a comptime struct and then invoke it in a separate call to the same function at comptime? Does it capture the locals that were available in the first call or the second? Does it just reference variables in scope by name? Would it make sense to invoke it in a separate function that has locals of the same names?) I think macros need to be all-the-way values (with variables and struct fields and everything) or all the way not (doesn't even support assignment, not compatible with expressions), so that it's really clear to the programmer what can or can't be done with them. I don't think this restriction significantly limits their usefulness. To be really clear, I'm not proposing modifying the If you need to comptime switch to determine the behavior of a macro, you can put the switch inside the macro without any loss of functionality. If you need to comptime switch based on the number of arguments in a macro, you must put that switch outside of the entire function call expression. But the same requirement holds for the function declaration. Macro prototypes are not compatible with type expressions either, so in order to have a function which takes a variable number of macro parameters you must have multiple function declarations and a comptime value which is set to one of them. I can't really think of a reason you would ever want to do this though.
I actually chose the
This would work, but I think if we're going to introduce those restrictions we might as well get a really convenient syntax out of it.
Yeah, I've been thinking a bit about how this would work. It's kind of like a "reverse inline". Like "I want the compiler to take this code and inline it at each // mark the return value as "inline", may be captured at call site with inline_return
// return expressions must be comptime known
inline fn compareFmt(a: usize, b: usize) inline []const u8 {
return if (a < b) "{} < {}" else "{} >= {}";
}
fn compareToFour(a: usize) void {
inline_return (compareFmt(a, 4)) |fmt| {
// fmt is comptime-known but may hold different values.
// this is kind of a generalization of inline for.
std.debug.print(fmt, .{a, 4});
};
} (side note, I also kind of want this for switches) const T = union(enum) {
a: extern struct { ptr: *u8, len: usize },
b: extern struct { len: usize, ptr: *u8 },
};
var value: T = ...;
const len = inline switch (value) {
// this text compiles for each possible value but they produce different machine code
else => |val| val.len,
}; But I think if your goal is something like "iterate a data structure and return immediately if you find a certain value", this is a pretty tortured way to express that. It's also worth noting that you can emulate this inline return feature with macros, in what I think is a much more clear way: inline fn compareFmt(a: usize, b: usize, withFmt: macro (comptime fmt: []const u8) void) void {
return if (a < b) withFmt("{} < {}") else withFmt("{} >= {}");
}
fn compareToFour(a: usize) void {
compareFmt(a, 4, => |fmt| {
// fmt is comptime-known but may hold different values.
std.debug.print(fmt, .{a, 4});
});
} |
I find the Another note, this would be brilliant for testing as you could make testing contexts without much friction.
Along with other "do-notation-like" contexts where vales need to be fixed up after. |
Concerning notation: Why is the
However, AFAICT, capture list expressions as currently used in the language have exactly the semantics of macros, so there is no need to distinguish them. E.g., The capture expressions in An extra decorator is also not necessary to distinguish macros from similar objects: If it appears within the parameter list of a function it's a macro, otherwise it is one of the special constructions using capture lists. If the recieving function expects a normal function argument rather than a macro (not meant for inlining), the compiler can yell at you. Concerning empty capture lists: True, they don't occur in the language, but that's because all current uses of capture lists pass at least one argument. But in the generalization proposed here, there are legitimate cases where you don't need to pass anything and simply execute a code block as is, so this seems like a non-issue to me. |
I like that much better, actually. Although it does mean an empty capture list has to be examined in context to distinguish it from the error set combination operator. |
Changing the error set sum to |
@tauoverpi |
Yeah, I forgot about that. The ambiguity is only in the tokenizer, though (I think). The parser itself will not be made much more complicated because of it. So maybe this particular ambiguity can be deemed acceptable. |
@rohlem issue there is that no other operator really fits either apart from variations on |
Edit: The ambiguity is more serious than I thought, since @tauoverpi, I don't think that changing the error set join is necessary at all.
|
Yeah, this is not meaningfully ambiguous. As @zzyxyzz points out, there is already precedent in the language for this sort of context dependence, in inferred error types in return expressions. Unary not is not normally supported on types, but if it begins a return type expression it means "infer the error set". So this would be a solution. However, I think it's inconsistent to require an empty capture list for macros but not require it in variants of |
Looking at both issues, I'd disagree. This is attempting to introduce some form of functional programming patterns in a systems-level manner whereas #4170 is about supporting first-class functions without any focus on stack-capturing beyond your own suggestions a few days ago. |
There is another small way in which macros can be made even more first-class. Generally, capture expressions in loops and other places in the laguage allow you to choose whether to capture by value ( inline fn foo(action: macro(item: i32) void) void {
// ...
}
inline fn bar(action: macro(item: *i32) void) void {
// ...
}
foo(|x| { ... }); // ok
foo(|*x| { ... }); // compile error
bar(|*x| { ... }); // ok
bar(|x| { ... }); // also ok; dereference happens automatically
// and is elided on the spot With this, macro expressions would have the same semantics and flexibility as native captures. |
This would be so useful Writing iterators in Zig right now has some footguns
|
Here's another usecase var file = std.fs.openFileAbsoluteZ(absolute_path, .{ .mode = .read_only }) catch {
const WarnOnce = struct {
pub var warned = false;
};
if (!WarnOnce.warned) {
WarnOnce.warned = true;
Output.prettyErrorln("Could not find file: " ++ absolute_path ++ " - using embedded version", .{});
}
return Holder.file;
}; With stack capturing macros, this code becomes something like var file = std.fs.openFileAbsoluteZ(absolute_path, .{ .mode = .read_only }) catch {
warnOnce({
Output.prettyErrorln("Could not find file: " ++ absolute_path ++ " - using embedded version", .{});
});
return Holder.file;
}; and |
@Jarred-Sumner You can already do this by simply returning a bool: const std = @import("std");
fn warnOnce() bool {
const WarnOnce = struct {
pub var warned = false;
};
const r = WarnOnce.warned;
WarnOnce.warned = true;
return !r;
}
pub fn main() !void {
var i: u32 = 0;
while (i < 10) : (i += 1) {
if (warnOnce()) {
std.debug.print("warning\n", .{});
}
}
} To make this reusable, you could pass an opaque type to it: const std = @import("std");
fn warnOnce(comptime T: type) bool {
_ = T;
const WarnOnce = struct {
pub var warned = false;
};
const r = WarnOnce.warned;
WarnOnce.warned = true;
return !r;
}
pub fn main() !void {
var i: u32 = 0;
while (i < 10) : (i += 1) {
if (warnOnce(opaque {})) {
std.debug.print("warning1\n", .{});
}
}
i = 0;
while (i < 10) : (i += 1) {
if (warnOnce(opaque {})) {
std.debug.print("warning2\n", .{});
}
}
} |
I'd just like to point out that functions which has a "macro" as a parameter doesn't have to be // This function:
fn foreach(self: *@This(), action: macro (key: *Key, value: *Value) void) void {
for (self.buckets) |bucket| {
var current = bucket.first;
while (current) |node| : (current = node.next) {
action(&node.key, &node.value);
}
}
}
// … can internally be represented as:
// (however, with the restriction can you're not allowed to store `action` in a variable etc.)
fn foreach(self: *@This(), ctx: *anyopaque, action: *const fn (ctx: *anyopaque, key: *Key, value: *Value) void) void {
for (self.buckets) |bucket| {
var current = bucket.first;
while (current) |node| : (current = node.next) {
action(ctx, &node.key, &node.value);
}
}
} This turns It would of course also be neat to have a way of customizing the control flow in these macros, but that does seem like a much more complicated problem so maybe we can solve it later? I feel like just having these macros (with no change in control-flow) would enable a lot of nice use cases. |
Does this take into account Zig's built-in async-await primitives? In the initial proposal, Also, more in general to the thread topic, does anyone have any thoughts on how this would interact with async await? Would they be mutually exclusive from each other in case there's some way of escaping stack references? |
i'd be very curious about the perf impact of this proposal on iterators in Zig For example, say you're iterating over a HashMap with 1 million entries: Lines 817 to 842 in 5238f9c
In the current implementation, the following lines would likely be evaluated 1 million times: if (it.hm.size == 0) return null;
const cap = it.hm.capacity();
const end = it.hm.metadata.? + cap;
var metadata = it.hm.metadata.? + it.index; For cases when you don't need to iterate and modify (probably the default scenario), that's very wasteful. With stack-capturing macros, the |
EDIT: Turns out these are essentially the same points I posted two years ago, my bad.I might be wrong about this, but aren't all code snippets posted here regarding optimization primarily solved by aggressive inlining? In status quo, we can pass a function via I don't like the idea of deciding whether callers of my function would rather always have their predicate inlined or not. Moreover, any optimization like this we could teach the compiler for all functions would benefit not just the ones written as stack-capturing macros. Completely disconnected from this performance talk, yes I would also appreciate syntax for lambda functions, |
is there any update on this proposal, or functionality for supporting closures? |
Just adding a few thoughts here as a new zig user that recently googled "can you pass a block into a function zig", and eventually ended up here. Given all of the discussion by everyone in this thread about making macros identical in both syntax and semantics to blocks, it seems strange to call them macros/snippets/whatever, when This also simplifies the "rules" of macros, since if a "macro" is just a block, it's already true in zig today that you can't assign a block to a const value, you'll just get the evaluated result from the block if you do that: // This will give you 4, not the block itself
const b = blk: {
break :blk 4;
}; And again it's already true in zig today that blocks with parameters are only allowed in certain places: // ok, the language allows this
if (x) |y| {
...
}
// not allowed
const z = |w| {
...
}; So instead of creating a new "macro" feature with a complicated set of rules around initialization and usage that make it indistinguishable from a block, this feature is just extending the list of places it's legal to put a block with parameters (and without) from This also makes the question of semantics pretty straightforward (and seem to match the proposal, if I'm reading it correctly): // any control flow statements like continue, break, return in this block...
action({
...
});
// ...should have identical semantics to those in this block.
{
...
} (It also took me quite a long time to find this issue, because it never occurred to me to call this feature a macro - in my head the term "macro" is about naive code splicing and immediately calls up a red flag that reads "beware copy-paste issues and namespace collisions", which aren't relevant to this proposal.) So maybe consider making the keyword |
@moderatelyConfused I like your simplified specification, however here's a nitpick:
This isn't strictly true: Blocks are expressions, so the following is already valid under status-quo (results in passing the result of the evaluation, just as in your assignment example): const debugLog = @import("std").log.debug;
fn action(x: anytype) void { //mark x inline once we have inline parameters #7772
debugLog("x is {}", .{x});
}
pub fn main() void {
action({
debugLog("1", .{});
}); //block is evaluated to {} (the void value)
action(x: {
debugLog("2", .{});
break :x @as(u8, 44);
}); //block is evaluated to @as(comptime_int, 44);
} To make the proposed behavior not depend on the parameter definition of the callee, Lazily-passed blocks are repeatedly evaluatable by call-syntax, making them look more like status-quo functions, |
@rohlem This is a fair nit, I was imagining that blocks-as-expressions would be disambiguated by the callee definition, but there is definitely a readability tradeoff being made there. One thing I'll point out is that any disambiguating syntax like
I think talking about blocks-as-parameters as functions makes them more complicated to describe, not less. If you imagine a block parameter as a block that will get inlined some number of times within the function you define it in, you don't need to have a concept of stack-unwinding, because after inlining everything ends up in the same stack frame. The only thing you need to remember is that variables and control flow are resolved relative to the original lexical context of the block, and that the code for the block will end up somewhere in the function you're writing 0 or more times. Considering zig already uses function call syntax to represent inlining for functions that are declared
Overall I think the fact that all of the intermediate calls inline away and collapse into a single stack frame makes this proposal significantly simpler than #1717 (and leaves the implementation with few to no tricky implementation choices), not more complicated. |
Why was this closed? The thread is very long, but I tried looking through it and couldn't find an explanation of why it was rejected. |
This proposal was written a long time ago in Zig's development, and ended up being a very bad fit for the direction the language took. It introduces a significant amount of special-case syntax and semantics to solve a use case which is already adequately solved using existing language constructs via the "context struct" pattern. |
I searched for zig context struct pattern, and couldn't find anything useful. Does it solve the control flow problems this tries to address, specifically being able to return, break, or continue in an outer scope? |
If you don't like this proposal, just say so. This proposal is a rather simple AST-level abstraction with high power-to-weight ratio that would have allowed writing very ergonomic exterior iterators that are as efficient and optimizable as native loops, with practically none of the downsides that come with macro programming or runtime closures.
One new keyword and a fairly straight-forward generalization of existing capture syntax is "a significant amount"?
The context struct workaround is so ugly for this particular application that nobody would use it. Besides, one of the essential components making this actually attractive for the exterior iterator use case was breakout control flow, which the struct workaround certainly can't emulate in any reasonable way.
What is meant here is that you can define an inline function (the macro) with a comptime parameter for the snippet, and then pass a locally defined struct with the desired operation wrapped in a method. This technically achieves the same result, except that the syntax is beyond ugly and you need to do extra wrapping if you want to send break and continue signals to the macro. |
I mean, the bigger issue is that it overloads the concept of a function parameter to include a whole new kind of entity which isn't a value despite looking superficially similar (supporting a tiny subset of typical operations). The problem is more the "semantics" part than the "syntax" part (although the syntax is still a bad fit for Zig due to how this looks just like a function parameter whose "type" is a
Assuming you're referring to the Anyways, to be clear, I think this rejection is pretty decided. |
I'm not sure what new kind of entity you refer to. A macro is a slight generalization of an inlined functional argument. Even in the language as it is today, not everything passed to a function is Plain Old Data and there are plenty of fine points around the semantics.
If you are iterating over an array or something, macros have a negligible advantage. The point of exterior iterators is that you can also use them in complex cases, such as tree traversal, with zero effort. Putting the stack and all other state you need into an iterator object is more messy, and less optimizable. Also, dismissing this on the grounds of being "FP style" is a bit of a stretch, IMO. The mechanism proposed here is not found in this form in any FP language that I'm familiar with. It is really more of a syntactic transformation to aid the reuse of procedural iteration patterns and context setups that cannot be easily packaged in a function.
Yeah, I guessed that. Your comment just riled me up. I've followed this proposal from day one and nothing changed about the language in the meantime that makes it significantly more or less useful or feasible. It would be more honest to say that it was never seriously considered, because it's not how Andrew likes to program. |
100%. And I will be closing most of the other open proposals with no explanation offered for the same reason. |
Summary
Stack capturing lambdas are normally unsafe, since the lambda may have a lifetime longer than the calling function. This proposal introduces a new type of lambda that is allowed to perform stack captures, but obeys special rules that allow the compiler to prove that these captures are safe. This proposal is based on Kotlin's concept of
inline
lambdas, though deviates from that implementation in some ways.Description
Unlike functions, Stack-Capturing Macros (or "macros" for short) are not values. They may only exist as parameters to functions which are marked
inline
. You cannot create a variable or struct that contains a macro.Macro parameters are declared with a macro prototype, which appears in a parameter list. Macro prototypes are not type declarations. They are a new syntax, similar to a function type declaration. Unlike function types, macro prototypes may use the
!T
syntax to infer error types from the macro body. When passing a macro as an argument, the token=>
is used to indicate that the following expression is a macro. The capture syntax (|x|
) is optionally used to capture the macro parameters, and after that comes an expression (which may be a labelled or unlabelled block). "return" and "try" within this scope are relative to the function, not the macro.break :label
may be used to exit to an outer scope. Doing this behaves as if the function and macro were inlined, and runs any defers in the function or macro.Because they cannot exist in variables, there is no
TypeInfo
entry for a macro. There are also no macro types.@TypeOf(macro_param)
is a compile error. You would never need to do this though, because macro parameters are always known to be macros within the text of the function. When inspecting the TypeInfo of a function, macro parameters show as having anull
type, just likeanytype
parameters. We could add more info to theFnArg
structure if necessary to handle this case.Macros may not be closed over in a function literal, but they may be referenced from within other macros, and they may be passed directly as a macro parameter to another inline function. Macros may accept macros as parameters, but only if the compiler can inline them all. Y-combinators which do not terminate at compile time will not work. This is governed by the eval branch quota. Inlining a macro into itself, directly or indirectly, counts as one backwards branch.
Unlabelled
break
andcontinue
don't work in a macro in a loop. This prevents a potential class of errors:Here the programmer intended to break out of the inner loop, but the language would interpret this as a break from the outer loop. (because the inner loop is inside the function and can't be seen). Instead, this should be a compile error and a labelled break can be used to get out of the inner loop:
Macros provide a way to allow stack references in a way that is provably safe.
Edit: This proposal previously read "If a macro has no parameters, the capture list is omitted." This would be ambiguous with an expression block, so it was changed to require an empty capture list.
Edit 2: Macros were originally blocks, but upon reflection they make more sense as expressions. This also means that
try
andreturn
in a macro refer to the outer function, rather than the macro expression. And a token (=>
) is needed for declaring a macro. Also added a bunch more examples.The text was updated successfully, but these errors were encountered: