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

support functions with variable length arguments #77

Closed
andrewrk opened this issue Jan 20, 2016 · 5 comments
Closed

support functions with variable length arguments #77

andrewrk opened this issue Jan 20, 2016 · 5 comments
Labels
enhancement Solving this issue will likely involve adding new logic or components to the codebase.
Milestone

Comments

@andrewrk
Copy link
Member

Here is some brainstorming:

pub fn main(args: [][]u8) i32 => {
    print("val1: {} val2: {} val3: {}\n", "hello", i32(-3), u32(4));
    return 0;
}

#analyze_fn_call(analyze_print_call)
fn print(format: []u8, ...args: []variant) => {
    var arg_i: isize = 0;
    for (c, format) {
        // ... copy to buffer
    }
}

fn analyze_print_call(inline args: []variant) => {
    const format = @const_eval(args[0]);
    var saw_open_brace = false;
    var replace_count = 0;
    for (c, format) => {
        if (saw_open_brace) {
            if (c == '}') {
                saw_open_brace = false;
                replace_count += 1;
            } else if (c == '{') {
                // literal open brace
                saw_open_brace = false;
            } else {
                @error("expected format specifier");
            }
        } else if (c == '{') {
            saw_open_brace = true;
        }
    }
    const arg_val_count = args.len - 1;
    if (replace_count != arg_val_count) {
        @error("expected " ++ @string(replace_count) ++ " print values, got " ++ @string(arg_val_count));
    }
}

Here, the print is annotated with another function that runs to validate function calls. The analyze_print_call function will not actually get output in the translation unit, it is run as a constant expression evaluation at compile time for every print function callsite.

@andrewrk andrewrk added the enhancement Solving this issue will likely involve adding new logic or components to the codebase. label Jan 20, 2016
@andrewrk andrewrk modified the milestone: 0.1.0 May 7, 2016
@andrewrk
Copy link
Member Author

With #167 we don't need this annotation. See that issue for a better example of var args.

Open question, will the ... token be needed anywhere for var args?

Maybe if your last parameter is []var that's how you do a var args function.

@thejoshwolfe
Copy link
Contributor

Proposal: add a ... syntax for parameter declaration and parameter passing. Let's start with examples:

// This is the typical way to declare a List(T)'s add() method:
pub fn add(list: &Self, item: T) {
    list.ensure_capacity(list._size + 1);
    list._buf[list._size] = item;
    list._size += 1;
}
// Here is an alternative that is backward compatible in both API and performance:
pub fn add(list: &Self, ...items: []const T) {
    list.ensure_capacity(list._size + items.len);
    for (items) |item, i| {
        list._buf[list._size + i] = item;
    }
    list._size += items.len;
}
// In the typical case where add() is called with a single parameter,
// the compiler emits code identical to the first case.
// In the case where the caller knows they will add exactly two parameters,
// the method can do a single ensure_capacity().

// If we want the List class to expose ensure_capacity() to callers,
// we can also offer an unchecked add method that skips the capacity check:
pub fn add_unchecked(list: &Self, ...items: []const T) {
    for (items) |item, i| {
        list._buf[list._size + i] = item;
    }
    list._size += items.len;
}
// and then the common add method can reuse the above by forwarding the args:
pub fn add(list: &Self, ...items: []const T) {
    list.ensure_capacity(list._size + items.len);
    list.add_unchecked(...items);
}

The ... prefix on a parameter can only be used on the final parameter, and enables varargs calling. The number of parameters passed to such a function must always be compile-time constant, so the .len of a ...args variable is always a compile-time constant.

The ... prefix operator on an expression can only be used on the final parameter of a function call when the function being called has a ... argument declared in that position. Because of the above constraints, the expression being operated on by the ... prefix operator must be an array with a compile-time constant length.

An open question is should varargs functions always be specialized for the number of parameters? The claim about performance in the above example assumes the function will be specialized for 1 argument. However, specializing a varargs function for every different number of parameters may be undesirable bloat. One may argue that varargs functions should be designed for relativley few variations on the number of parameters, since each of these variations will be deliberately written by a programmer at author-time. However, one also may argue that a 0-1-Infinity pattern could be employed. My vote is for complete specialization.

Another open question is whether we could relax the restrictions on passing ...-prefix arguments. There's no particular reason we couldn't allow any number of ...-prefix arguments to be passed in any position to any function. As long as the compiler knows the .len of the ...-prefix expressions, the compiler can desugar those expressions to simply listing parameters. See below example.

This feature can be combined with an orthogonal feature, the var type, like this:

pub fn print_error(error_code: u32, inline fmt: []const u8, ...args: []var) {
    const error_name = error_names[error_code];
    // this assumes the ++ operator works on compile-time constant-length arrays
    // with variable values at each index.
    const new_args = []var{error_name, error_code} ++ args;
    io.stderr.printf("ERROR: %s(%d): " ++ fmt ++ "\n", ...new_args);
}
// alternatively:
pub fn print_error(error_code: u32, inline fmt: []const u8, ...args: []var) {
    const error_name = error_names[error_code];
    // this assumes a relaxed requirement on passing `...` arguments.
    io.stderr.printf("ERROR: %s(%d): " ++ fmt ++ "\n", error_name, error_code, ...new_args);
}

@andrewrk
Copy link
Member Author

Counter proposal:

pub fn addUnchecked(list: &Self, items: ...) {
    for (items) |item, i| {
        list._buf[list._size + i] = item;
    }
    list._size += items.len;
}
pub fn add(list: &Self, items: ...) {
    list.ensureCapacity(list._size + items.len);
    // because `items` is of type `(args)` it automatically flattens out when used as a parameter
    list.addUnchecked(items);
}

pub fn printError(error_code: u32, comptime fmt: []const u8, args: ...) {
    const error_name = error_names[error_code];
    io.stderr.printf("ERROR: %s(%d): " ++ fmt ++ "\n", error_name, error_code, args);
}

The (args) type would be array-like in that you could do args.len and for (args) |arg| {} on it, as well as args[index] where index is compile-time known. Also args[start_index...end_index].

The (args) type is implemented as a compile time known value which is a parameter start index and a parameter end index. When you pass a value of type (args), each parameter from start to end is passed, starting at that position.

@andrewrk
Copy link
Member Author

andrewrk commented Jan 23, 2017

  • args.len
  • args[index]
  • for (args) |arg| { ... }
  • args[start_index...end_index]
  • comptime args: ...
  • passing an (args) value as an argument

@andrewrk
Copy link
Member Author

I moved the remaining items to their own issues which are not prioritized for 0.1.0 milestone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Solving this issue will likely involve adding new logic or components to the codebase.
Projects
None yet
Development

No branches or pull requests

2 participants