Basic support for keyword arguments when calling functions.
This commit is contained in:
parent
0966a21c7a
commit
e46d753999
26
README.md
26
README.md
@ -666,6 +666,32 @@ superSecretFunction("hunter2")
|
||||
# Welcome!
|
||||
```
|
||||
|
||||
### Keyword Arguments
|
||||
|
||||
Arguments may be passed to a function by specifying their name instead of using their positional location.
|
||||
|
||||
```py
|
||||
def aFunction(a,b,c):
|
||||
print a,b,c
|
||||
|
||||
aFunction(1,2,3)
|
||||
aFunction(1,c=3,b=2)
|
||||
aFunction(b=2,c=3,a=1)
|
||||
# → 1 2 3
|
||||
# 1 2 3
|
||||
# 1 2 3
|
||||
```
|
||||
|
||||
This will be slower in execution than a normal function call, as the interpreter will need to figure out where to place arguments in the requested function by examining it at runtime, but it allows for functions to take many default arguments without forcing the caller to specify the values for everything leading up to one they want to specifically set.
|
||||
|
||||
```py
|
||||
def aFunction(with=None,lots=None,of=None,default=None,args=None):
|
||||
print with,lots,of,default,args
|
||||
|
||||
aFunction(of="hello!")
|
||||
# → None None hello! None None
|
||||
```
|
||||
|
||||
## About the REPL
|
||||
|
||||
Kuroko's repl provides an interactive environment for executing code and seeing results.
|
||||
|
2
chunk.h
2
chunk.h
@ -56,6 +56,7 @@ typedef enum {
|
||||
OP_INC,
|
||||
OP_DUP,
|
||||
OP_SWAP,
|
||||
OP_KWARGS,
|
||||
|
||||
OP_BITOR,
|
||||
OP_BITXOR,
|
||||
@ -86,6 +87,7 @@ typedef enum {
|
||||
OP_IMPORT_LONG,
|
||||
OP_GET_SUPER_LONG,
|
||||
OP_INC_LONG,
|
||||
OP_KWARGS_LONG,
|
||||
} KrkOpCode;
|
||||
|
||||
typedef struct {
|
||||
|
78
compiler.c
78
compiler.c
@ -295,11 +295,27 @@ static void emitReturn() {
|
||||
static KrkFunction * endCompiler() {
|
||||
emitReturn();
|
||||
KrkFunction * function = current->function;
|
||||
|
||||
/* Attach contants for arguments */
|
||||
for (int i = 0; i < function->requiredArgs; ++i) {
|
||||
KrkValue value = OBJECT_VAL(krk_copyString(current->locals[i].name.start, current->locals[i].name.length));
|
||||
krk_push(value);
|
||||
krk_writeValueArray(&function->requiredArgNames, value);
|
||||
krk_pop();
|
||||
}
|
||||
for (int i = 0; i < function->keywordArgs; ++i) {
|
||||
KrkValue value = OBJECT_VAL(krk_copyString(current->locals[i+function->requiredArgs].name.start,
|
||||
current->locals[i+function->requiredArgs].name.length));
|
||||
krk_push(value);
|
||||
krk_writeValueArray(&function->keywordArgNames, value);
|
||||
krk_pop();
|
||||
}
|
||||
|
||||
#ifdef ENABLE_DISASSEMBLY
|
||||
if ((vm.flags & KRK_ENABLE_DISASSEMBLY) && !parser.hadError) {
|
||||
krk_disassembleChunk(currentChunk(), function->name ? function->name->chars : "<module>");
|
||||
fprintf(stderr, "Function metadata: requiredArgs=%d defaultArgs=%d upvalueCount=%d\n",
|
||||
function->requiredArgs, function->defaultArgs, (int)function->upvalueCount);
|
||||
fprintf(stderr, "Function metadata: requiredArgs=%d keywordArgs=%d upvalueCount=%d\n",
|
||||
function->requiredArgs, function->keywordArgs, (int)function->upvalueCount);
|
||||
fprintf(stderr, "__doc__: \"%s\"\n", function->docstring ? function->docstring->chars : "");
|
||||
fprintf(stderr, "Constants: ");
|
||||
for (size_t i = 0; i < currentChunk()->constants.count; ++i) {
|
||||
@ -309,6 +325,21 @@ static KrkFunction * endCompiler() {
|
||||
fprintf(stderr, ", ");
|
||||
}
|
||||
}
|
||||
fprintf(stderr, "\nRequired arguments: ");
|
||||
int i = 0;
|
||||
for (; i < function->requiredArgs; ++i) {
|
||||
fprintf(stderr, "%.*s%s",
|
||||
(int)current->locals[i].name.length,
|
||||
current->locals[i].name.start,
|
||||
(i == function->requiredArgs - 1) ? "" : ", ");
|
||||
}
|
||||
fprintf(stderr, "\nKeyword arguments: ");
|
||||
for (; i < function->requiredArgs + function->keywordArgs; ++i) {
|
||||
fprintf(stderr, "%.*s=None%s",
|
||||
(int)current->locals[i].name.length,
|
||||
current->locals[i].name.start,
|
||||
(i == function->keywordArgs - 1) ? "" : ", ");
|
||||
}
|
||||
fprintf(stderr, "\n");
|
||||
}
|
||||
#endif
|
||||
@ -653,7 +684,7 @@ static void function(FunctionType type, size_t blockWidth) {
|
||||
defineVariable(paramConstant);
|
||||
if (match(TOKEN_EQUAL)) {
|
||||
consume(TOKEN_NONE,"Optional arguments can only be assigned the default value of None.");
|
||||
current->function->defaultArgs++;
|
||||
current->function->keywordArgs++;
|
||||
} else {
|
||||
current->function->requiredArgs++;
|
||||
}
|
||||
@ -1704,14 +1735,53 @@ static void defineVariable(size_t global) {
|
||||
}
|
||||
|
||||
static size_t argumentList() {
|
||||
size_t argCount = 0;
|
||||
size_t argCount = 0, keywordArgs = 0;
|
||||
if (!check(TOKEN_RIGHT_PAREN)) {
|
||||
do {
|
||||
if (match(TOKEN_IDENTIFIER)) {
|
||||
KrkToken argName = parser.previous;
|
||||
if (check(TOKEN_EQUAL)) {
|
||||
/* This is a keyword argument. */
|
||||
advance();
|
||||
/* Output the name */
|
||||
size_t ind = identifierConstant(&argName);
|
||||
EMIT_CONSTANT_OP(OP_CONSTANT, ind);
|
||||
expression();
|
||||
keywordArgs++;
|
||||
continue;
|
||||
} else {
|
||||
/*
|
||||
* This is a regular argument that happened to start with an identifier,
|
||||
* roll it back so we can process it that way.
|
||||
*/
|
||||
krk_ungetToken(parser.current);
|
||||
parser.current = argName;
|
||||
}
|
||||
} else if (keywordArgs) {
|
||||
error("Keyword arguments must appear after positional arguments in function call.");
|
||||
return 0;
|
||||
}
|
||||
expression();
|
||||
argCount++;
|
||||
} while (match(TOKEN_COMMA));
|
||||
}
|
||||
consume(TOKEN_RIGHT_PAREN, "Expected ')' after arguments.");
|
||||
if (keywordArgs) {
|
||||
/*
|
||||
* Creates a sentinel at the top of the stack to tell the CALL instruction
|
||||
* how many keyword arguments are at the top of the stack. This value
|
||||
* triggers special handling in the CALL that processes the keyword arguments,
|
||||
* which is relatively slow, so only use keyword arguments if you have to!
|
||||
*/
|
||||
EMIT_CONSTANT_OP(OP_KWARGS, keywordArgs);
|
||||
/*
|
||||
* We added two elements - name and value - for each keyword arg,
|
||||
* plus the sentinel object that will show up at the end after the
|
||||
* OP_KWARGS instruction complets, so make sure we have the
|
||||
* right depth into the stack when we execute CALL
|
||||
*/
|
||||
argCount += 1 /* for the sentinel */ + 2 * keywordArgs;
|
||||
}
|
||||
return argCount;
|
||||
}
|
||||
|
||||
|
1
debug.c
1
debug.c
@ -107,6 +107,7 @@ size_t krk_disassembleInstruction(KrkChunk * chunk, size_t offset) {
|
||||
CONSTANT(OP_CLOSURE, CLOSURE_MORE)
|
||||
CONSTANT(OP_IMPORT, (void)0)
|
||||
CONSTANT(OP_GET_SUPER, (void)0)
|
||||
OPERAND(OP_KWARGS)
|
||||
OPERAND(OP_SET_LOCAL)
|
||||
OPERAND(OP_GET_LOCAL)
|
||||
OPERAND(OP_SET_UPVALUE)
|
||||
|
4
object.c
4
object.c
@ -64,10 +64,12 @@ KrkString * krk_copyString(const char * chars, size_t length) {
|
||||
KrkFunction * krk_newFunction() {
|
||||
KrkFunction * function = ALLOCATE_OBJECT(KrkFunction, OBJ_FUNCTION);
|
||||
function->requiredArgs = 0;
|
||||
function->defaultArgs = 0;
|
||||
function->keywordArgs = 0;
|
||||
function->upvalueCount = 0;
|
||||
function->name = NULL;
|
||||
function->docstring = NULL;
|
||||
krk_initValueArray(&function->requiredArgNames);
|
||||
krk_initValueArray(&function->keywordArgNames);
|
||||
krk_initChunk(&function->chunk);
|
||||
return function;
|
||||
}
|
||||
|
4
object.h
4
object.h
@ -58,11 +58,13 @@ typedef struct KrkUpvalue {
|
||||
typedef struct {
|
||||
KrkObj obj;
|
||||
short requiredArgs;
|
||||
short defaultArgs;
|
||||
short keywordArgs;
|
||||
size_t upvalueCount;
|
||||
KrkChunk chunk;
|
||||
KrkString * name;
|
||||
KrkString * docstring;
|
||||
KrkValueArray requiredArgNames;
|
||||
KrkValueArray keywordArgNames;
|
||||
} KrkFunction;
|
||||
|
||||
typedef struct {
|
||||
|
7
test/testBenchmarkKwargs.krk
Normal file
7
test/testBenchmarkKwargs.krk
Normal file
@ -0,0 +1,7 @@
|
||||
def function(arg):
|
||||
print arg
|
||||
|
||||
let x = 1
|
||||
while x < 1000000:
|
||||
function(arg=x)
|
||||
x++
|
30
test/testKeywordArgs.krk
Normal file
30
test/testKeywordArgs.krk
Normal file
@ -0,0 +1,30 @@
|
||||
def function(positional1, positional2, keyword1=None, keyword2=None):
|
||||
print positional1, positional2
|
||||
print keyword1, keyword2
|
||||
|
||||
function(1,2)
|
||||
function(1,2,3,4)
|
||||
|
||||
function(1,2,keyword2=5)
|
||||
|
||||
try:
|
||||
function(1,keyword2=5)
|
||||
except:
|
||||
print exception.arg
|
||||
|
||||
try:
|
||||
function(1,2,positional1=4)
|
||||
except:
|
||||
print exception.arg
|
||||
|
||||
try:
|
||||
function(1,2,keyword2=None,keyword2=5)
|
||||
except:
|
||||
print exception.arg
|
||||
|
||||
function(1,keyword2=4,positional2="abc")
|
||||
|
||||
try:
|
||||
function(1,keyword2=4,keyword1=5,positional1="nope")
|
||||
except:
|
||||
print exception.arg
|
3
value.h
3
value.h
@ -13,6 +13,7 @@ typedef enum {
|
||||
VAL_FLOATING,
|
||||
VAL_HANDLER,
|
||||
VAL_OBJECT,
|
||||
VAL_KWARGS,
|
||||
/* More here later */
|
||||
} KrkValueType;
|
||||
|
||||
@ -33,6 +34,7 @@ typedef struct {
|
||||
#define FLOATING_VAL(value) ((KrkValue){VAL_FLOATING,{.floating = value}})
|
||||
#define HANDLER_VAL(value) ((KrkValue){VAL_HANDLER, {.handler = value}})
|
||||
#define OBJECT_VAL(value) ((KrkValue){VAL_OBJECT, {.object = (KrkObj*)value}})
|
||||
#define KWARGS_VAL(value) ((KrkValue){VAL_KWARGS, {.integer = value}})
|
||||
|
||||
#define AS_BOOLEAN(value) ((value).as.boolean)
|
||||
#define AS_INTEGER(value) ((value).as.integer)
|
||||
@ -46,6 +48,7 @@ typedef struct {
|
||||
#define IS_FLOATING(value) ((value).type == VAL_FLOATING)
|
||||
#define IS_HANDLER(value) ((value).type == VAL_HANDLER)
|
||||
#define IS_OBJECT(value) ((value).type == VAL_OBJECT)
|
||||
#define IS_KWARGS(value) ((value).type == VAL_KWARGS)
|
||||
|
||||
typedef struct {
|
||||
size_t capacity;
|
||||
|
116
vm.c
116
vm.c
@ -663,7 +663,7 @@ static KrkValue krk_globals(int argc, KrkValue argv[]) {
|
||||
|
||||
static int checkArgumentCount(KrkClosure * closure, int argCount) {
|
||||
int minArgs = closure->function->requiredArgs;
|
||||
int maxArgs = minArgs + closure->function->defaultArgs;
|
||||
int maxArgs = minArgs + closure->function->keywordArgs;
|
||||
if (argCount < minArgs || argCount > maxArgs) {
|
||||
krk_runtimeError(vm.exceptions.argumentError, "%s() takes %s %d argument%s (%d given)",
|
||||
closure->function->name ? closure->function->name->chars : "<unnamed function>",
|
||||
@ -687,10 +687,111 @@ static int checkArgumentCount(KrkClosure * closure, int argCount) {
|
||||
* where we need to restore the stack to when we return from this call.
|
||||
*/
|
||||
static int call(KrkClosure * closure, int argCount, int extra) {
|
||||
if (argCount && IS_KWARGS(vm.stackTop[-1])) {
|
||||
/**
|
||||
* Process keyword arguments.
|
||||
* First, we make sure there is enough space on the stack to fit all of
|
||||
* the potential arguments to this function. We need to call it with
|
||||
* all of its arguments - positional and keyword - ready to go, even
|
||||
* if they weren't specified.
|
||||
*
|
||||
* Then we go through all of the kwargs and figure out where they go,
|
||||
* building a table at the top of the stack of final offsets and values.
|
||||
*
|
||||
* Then we clear through all of the spaces that were previously
|
||||
* kwarg name/value pairs and replace them with a sentinel value.
|
||||
*
|
||||
* Then we go through our table and place values into their destination
|
||||
* spots. If we find that something is already there (because it's not
|
||||
* the expected sentinel value), we raise a TypeError indicating a
|
||||
* duplicate argument.
|
||||
*
|
||||
* Finally, we do one last pass to see if any of the sentinel values
|
||||
* indicating missing positional arguments is still there and raise
|
||||
* another TypeError to indicate missing required arguments, and fill
|
||||
* out the default values for missing keyword arguments.
|
||||
* (TODO: Support other default values for kwargs; need to put them
|
||||
* somewhere, probably close them as upvalues?)
|
||||
*
|
||||
* At this point we can reset the stack head and continue to the actual
|
||||
* call with all of the arguments, including the defaults, in the right
|
||||
* place for the function to pull them as locals.
|
||||
*/
|
||||
long kwargsCount = AS_INTEGER(vm.stackTop[-1]);
|
||||
krk_pop(); /* Pop the arg counter */
|
||||
argCount--;
|
||||
long existingPositionalArgs = argCount - kwargsCount * 2;
|
||||
int found = 0;
|
||||
KrkValue * startOfPositionals = &vm.stackTop[-argCount];
|
||||
KrkValue * endOfPositionals = &vm.stackTop[-kwargsCount * 2];
|
||||
for (long availableSlots = argCount; availableSlots < (closure->function->requiredArgs + closure->function->keywordArgs); ++availableSlots) {
|
||||
krk_push(KWARGS_VAL(0)); /* Make sure we definitely have enough space */
|
||||
}
|
||||
KrkValue * startOfExtras = vm.stackTop;
|
||||
for (long i = 0; i < kwargsCount; ++i) {
|
||||
KrkValue name = endOfPositionals[i*2];
|
||||
KrkValue value = endOfPositionals[i*2+1];
|
||||
/* First, see if it's a positional arg. */
|
||||
for (int j = 0; j < (int)closure->function->requiredArgNames.count; ++j) {
|
||||
if (krk_valuesEqual(name, closure->function->requiredArgNames.values[j])) {
|
||||
krk_push(INTEGER_VAL(j));
|
||||
krk_push(value);
|
||||
found++;
|
||||
goto _finishArg;
|
||||
}
|
||||
}
|
||||
/* See if it's a keyword arg. */
|
||||
for (int j = 0; j < (int)closure->function->keywordArgNames.count; ++j) {
|
||||
if (krk_valuesEqual(name, closure->function->keywordArgNames.values[j])) {
|
||||
krk_push(INTEGER_VAL(j + closure->function->requiredArgs));
|
||||
krk_push(value);
|
||||
found++;
|
||||
goto _finishArg;
|
||||
}
|
||||
}
|
||||
/* If we got to this point, it's not a recognized argument for this function. */
|
||||
krk_runtimeError(vm.exceptions.typeError, "%s() got an unexpected keyword argument '%s'",
|
||||
closure->function->name ? closure->function->name->chars : "<unnamed function>",
|
||||
AS_CSTRING(name));
|
||||
return 0;
|
||||
_finishArg:
|
||||
continue;
|
||||
}
|
||||
for (long clearSlots = existingPositionalArgs; clearSlots < argCount; ++clearSlots) {
|
||||
startOfPositionals[clearSlots] = KWARGS_VAL(0);
|
||||
}
|
||||
for (int i = 0; i < found; ++i) {
|
||||
int destination = AS_INTEGER(startOfExtras[i*2]);
|
||||
if (!IS_KWARGS(startOfPositionals[destination])) {
|
||||
krk_runtimeError(vm.exceptions.typeError, "%s() got multiple values for argument '%s'",
|
||||
closure->function->name ? closure->function->name->chars : "<unnamed function>",
|
||||
(destination < closure->function->requiredArgs ? AS_CSTRING(closure->function->requiredArgNames.values[destination]) :
|
||||
AS_CSTRING(closure->function->keywordArgNames.values[destination - closure->function->requiredArgs])));
|
||||
return 0;
|
||||
}
|
||||
startOfPositionals[destination] = startOfExtras[i*2+1];
|
||||
}
|
||||
long clearSlots;
|
||||
for (clearSlots = existingPositionalArgs; clearSlots < closure->function->requiredArgs; ++clearSlots) {
|
||||
if (IS_KWARGS(startOfPositionals[clearSlots])) {
|
||||
krk_runtimeError(vm.exceptions.typeError, "%s() missing required positional argument: '%s'",
|
||||
closure->function->name ? closure->function->name->chars : "<unnamed function>",
|
||||
AS_CSTRING(closure->function->requiredArgNames.values[clearSlots]));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
for (; clearSlots < closure->function->requiredArgs + closure->function->keywordArgs; ++clearSlots) {
|
||||
if (IS_KWARGS(startOfPositionals[clearSlots])) {
|
||||
startOfPositionals[clearSlots] = NONE_VAL();/* TODO: Default value for kwarg */
|
||||
}
|
||||
}
|
||||
argCount = closure->function->requiredArgs + closure->function->keywordArgs;
|
||||
while (vm.stackTop > startOfPositionals + argCount) krk_pop();
|
||||
}
|
||||
if (!checkArgumentCount(closure, argCount)) {
|
||||
return 0;
|
||||
}
|
||||
while (argCount < (closure->function->requiredArgs + closure->function->defaultArgs)) {
|
||||
while (argCount < (closure->function->requiredArgs + closure->function->keywordArgs)) {
|
||||
krk_push(NONE_VAL());
|
||||
argCount++;
|
||||
}
|
||||
@ -734,6 +835,10 @@ int krk_callValue(KrkValue callee, int argCount, int extra) {
|
||||
return call(AS_CLOSURE(callee), argCount, extra);
|
||||
case OBJ_NATIVE: {
|
||||
NativeFn native = AS_NATIVE(callee);
|
||||
if (argCount && IS_KWARGS(vm.stackTop[-1])) {
|
||||
krk_runtimeError(vm.exceptions.attributeError, "%s does not take keyword arguments.", ((KrkNative*)AS_OBJECT(callee))->name);
|
||||
return 0;
|
||||
}
|
||||
KrkValue * stackCopy = malloc(argCount * sizeof(KrkValue));
|
||||
memcpy(stackCopy, vm.stackTop - argCount, argCount * sizeof(KrkValue));
|
||||
KrkValue result = native(argCount, stackCopy);
|
||||
@ -753,7 +858,7 @@ int krk_callValue(KrkValue callee, int argCount, int extra) {
|
||||
if (krk_tableGet(&_class->methods, vm.specialMethodNames[METHOD_INIT], &initializer)) {
|
||||
return krk_callValue(initializer, argCount + 1, 0);
|
||||
} else if (argCount != 0) {
|
||||
krk_runtimeError(vm.exceptions.attributeError, "Class does not have an __init__ but arguments were passed to initializer: %d\n", argCount);
|
||||
krk_runtimeError(vm.exceptions.attributeError, "Class does not have an __init__ but arguments were passed to initializer: %d", argCount);
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
@ -2071,6 +2176,11 @@ static KrkValue run() {
|
||||
case OP_SWAP:
|
||||
krk_swap(1);
|
||||
break;
|
||||
case OP_KWARGS_LONG:
|
||||
case OP_KWARGS: {
|
||||
krk_push(KWARGS_VAL(readBytes(frame,operandWidth)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!(vm.flags & KRK_HAS_EXCEPTION)) continue;
|
||||
_finishException:
|
||||
|
Loading…
Reference in New Issue
Block a user