Add krk_parseArgs utility function

This commit is contained in:
K. Lange 2022-08-03 19:44:07 +09:00
parent 2387ac6b29
commit 15fa509193
2 changed files with 406 additions and 0 deletions

View File

@ -313,3 +313,27 @@ extern int krk_unpackIterable(KrkValue iterable, void * context, int callback(vo
#define KRK_BASE_CLASS(cls) (vm.baseClasses->cls ## Class)
#define KRK_EXC(exc) (vm.exceptions->exc)
extern int krk_parseVArgs(
const char * _method_name,
int argc, const KrkValue argv[], int hasKw,
const char * fmt, const char ** names, va_list args);
extern int krk_parseArgs_impl(
const char * _method_name,
int argc, const KrkValue argv[], int hasKw,
const char * format, const char ** names, ...);
/**
* @def krk_parseArgs(f,n,...)
* @brief Parse arguments to a function while accepting keyword arguments.
*
* Convenience macro for @c krk_parseArgs_impl to avoid needing to pass all of
* the implicit arguments normally provided to a KRK_Function or KRK_Method.
*
* @param f Format string.
* @param n Names array.
* @returns 1 on success, 0 on failure with an exception set.
*/
#define krk_parseArgs(f,n,...) krk_parseArgs_impl(_method_name,argc,argv,hasKw,f,n,__VA_ARGS__)

382
src/parseargs.c Normal file
View File

@ -0,0 +1,382 @@
#include <kuroko/vm.h>
#include <kuroko/util.h>
/**
* For use with @c ! formats, collects a @c KrkClass* and compares if the arg
* is set. As a special case, the type may be @c NULL in which case failure is
* guaranteed; this allows the standard library to reference potentially
* uninitialized types (like fileio.File which may be uninitialized if the
* module is not loaded, but may still need to be referenced as a potential
* type in a function like @c print ).
*/
static int matchType(const char * _method_name, va_list args, KrkValue arg) {
KrkClass * type = va_arg(args, KrkClass*);
if (arg != KWARGS_VAL(0) && !krk_isInstanceOf(arg, type)) {
krk_runtimeError(vm.exceptions->typeError, "%s() expects %s, not '%T'",
_method_name, type ? type->name->chars : "unknown type", arg);
return 0;
}
return 1;
}
/**
* @brief Validate and parse arguments to a function similar to how managed
* function arguments are handled.
*
* This attempts to emulate CPythons' PyArg_ParseTupleAndKeywords.
*
* This works like a fancy scanf. We accept the original argument specification
* (argc,argv,hasKw), a format string, an array of argument names, and then var
* args that are generally pointers to where to stick results.
*
* @param argc Original positional argument count.
* @param argv Original argument list, @c argv[argc] should be a dict if hasKw is set.
* @param hasKw Whether @c argv[argc] has a dict of keyword arguments.
* @param fmt String describing formats of expected arguments.
* @param names Array of strings of parameter names.
* @param args var args
* @returns 1 on success, 0 on error.
*/
int krk_parseVArgs(
const char * _method_name,
int argc, const KrkValue argv[], int hasKw,
const char * fmt, const char ** names, va_list args) {
int iarg = 0; /**< Index into positional input arguments */
int oarg = 0; /**< Index into names array */
int required = 1; /**< Parser state, whether required arguments are being collected */
int acceptextrakws = 0; /**< Whether extra keyword args should produce an error (0) or not (1) */
if (*fmt == '.') {
/**
* If the format string starts with `.` then argument processing skips the first argument
* on the assumption that this is a method and the first argument has already been
* handled by the method wrapper macros or directly by the function. This makes error
* messages a bit nicer, as argument counts will exclude the implicit self.
*/
argv++;
argc--;
fmt++;
}
/* Required args */
for (; *fmt; fmt++) {
if (*fmt == '|') {
/**
* @c | begins optional arguments - eg. default args. Every format option after
* this point should be preset to usable default value, as it will not be touched
* if the argument is not found.
*/
if (!required) {
krk_runtimeError(vm.exceptions->typeError, "format string has multiple |s");
return 1;
}
required = 0;
continue;
}
if (*fmt == '*') {
/**
* @c * works like @c *args would in a Kuroko function signature, collecting
* all remaining positional arguments into a list. It does this be returning
* the count of remaining arguments (int) and the pointer to their start in the
* original argument list (KrkValue*).
*
* This also implicitly signals the end of required arguments and all later
* arguments are automatically optional, without needing to use @c |.
*/
int * out_c = va_arg(args, int *);
const KrkValue ** out_v = va_arg(args, const KrkValue **);
*out_c = argc - iarg;
*out_v = &argv[iarg];
iarg = argc;
required = 0;
continue;
}
if (*fmt == '$') {
/**
* @c $ indicates the end of positional arguments. Everything after this point is
* only accepted as a keyword argument. @c $ must appear after one of @c | or @c *.
*
* If any positional arguments remain when @c $ is encountred, a too-many arguments
* exception will be raised.
*/
if (required) {
krk_runtimeError(vm.exceptions->typeError, "$ must be after | or * in format string");
return 1;
}
if (iarg < argc) break;
continue;
}
if (*fmt == '~') {
/**
* If @c ~ is encountered anywhere in the format string, then extraneous keyword arguments
* are left as-is and no exception is raised when they are found. As keyword arguments are
* deleted from the kwargs dict while processing other arguments, this means if @c hasKw
* is set then @c argv[argc] will be left with only the unhandled keyword arguments, same
* as for a @c **kwargs argument in a Kuroko function signature.
*/
acceptextrakws = 1;
continue;
}
int wasPositional = 0;
KrkValue arg = KWARGS_VAL(0);
krk_push(OBJECT_VAL(krk_copyString(names[oarg],strlen(names[oarg]))));
if (iarg < argc) {
/* Positional arguments are pretty straightforward. */
arg = argv[iarg];
iarg++;
wasPositional = 1;
} else if ((required && !hasKw) || (hasKw && !krk_tableGet_fast(AS_DICT(argv[argc]), AS_STRING(krk_peek(0)), &arg) && required)) {
/* If keyword argument lookup failed and this is not an optional argument, raise an exception. */
krk_runtimeError(vm.exceptions->typeError, "%s() missing required positional argument: '%S'",
_method_name, AS_STRING(krk_peek(0)));
goto _error;
}
if (hasKw && krk_tableDelete(AS_DICT(argv[argc]), krk_peek(0)) && wasPositional) {
/* We remove all arguments from kwargs. If we got this argument from a positional argument,
* and it was found during deletion, we raise a multiple-defs exception. */
krk_runtimeError(vm.exceptions->typeError, "%s() got multiple values for argument '%S'",
_method_name, AS_STRING(krk_peek(0)));
goto _error;
}
switch (*fmt) {
/**
* @c O Collect an object (with @c ! - of a given type) and place it in
* in the @c KrkObj** var arg. The object must be a heap object,
* so this can not be used to collect boxed value types like @c int
* or @c float - use @c V for those instead. As an exception to the
* heap object requirements, @c None is accepted and will result
* in @c NULL (but if a type is requested, the type check will fail
* before @c None can be evaluated).
*/
case 'O': {
if (fmt[1] == '!') {
fmt++;
if (!matchType(_method_name, args, arg)) goto _error;
}
KrkObj ** out = va_arg(args, KrkObj**);
if (arg != KWARGS_VAL(0)) {
if (IS_NONE(arg)) {
*out = NULL;
} else if (!IS_OBJECT(arg)) {
TYPE_ERROR(heap object,arg);
goto _error;
} else {
*out = AS_OBJECT(arg);
}
}
break;
}
/**
* @c V Accept any value (with @c ! - of a given type) and place a value
* reference in the @c KrkValue* var arg. This works with boxed value
* types as well, so it is safe for use with @c int and @c float and
* so on. The type check is equivalent to @c instanceof. As a special
* case - as with @c O - the type may be @c NULL in which case type
* checking is guaranteed to fail but parsing will not. The resulting
* error message is less informative in this case.
*/
case 'V': {
if (fmt[1] == '!') {
fmt++;
if (!matchType(_method_name, args, arg)) goto _error;
}
KrkValue * out = va_arg(args, KrkValue*);
if (arg != KWARGS_VAL(0)) {
*out = arg;
}
break;
}
/**
* @c z Collect one string or None and place a pointer to it in
* a `const char **`. If @c # is specified, the size of the
* string is also placed in a following @c size_t* var arg.
* If the argument is @c None the result is @c NULL and
* the size is set to 0.
*/
case 'z': {
char ** out = va_arg(args, char **);
size_t * size = NULL;
if (fmt[1] == '#') {
fmt++;
size = va_arg(args, size_t*);
}
if (arg != KWARGS_VAL(0)) {
if (arg == NONE_VAL()) {
*out = NULL;
if (size) *size = 0;
} else if (IS_STRING(arg)) {
*out = AS_CSTRING(arg);
if (size) *size = AS_STRING(arg)->length;
} else {
TYPE_ERROR(str or None,arg);
goto _error;
}
}
break;
}
/**
* @c s Same as @c z but does not accept None.
*/
case 's': {
char ** out = va_arg(args, char **);
size_t * size = NULL;
if (fmt[1] == '#') {
fmt++;
size = va_arg(args, size_t*);
}
if (arg != KWARGS_VAL(0)) {
if (IS_STRING(arg)) {
*out = AS_CSTRING(arg);
if (size) *size = AS_STRING(arg)->length;
} else {
TYPE_ERROR(str,arg);
goto _error;
}
}
break;
}
/**
* @c i Simple integer. The argument must be a normal @c int. No conversion
* is done from other types and @c long objects are not supported. No
* overflow checking is done, either. If you want to accept @c long
* objects reliably, use @c V! with a type of @c int instead.
*/
case 'i': {
int * out = va_arg(args, int*);
if (arg != KWARGS_VAL(0)) {
if (!IS_INTEGER(arg)) {
TYPE_ERROR(int,arg);
goto _error;
}
*out = AS_INTEGER(arg);
}
break;
}
/**
* @c C Accept a string of length one and convert it to
* a C int in a similar manner to @c ord.
*/
case 'C': {
int * out = va_arg(args, int*);
if (arg != KWARGS_VAL(0)) {
if (!IS_STRING(arg) || AS_STRING(arg)->codesLength != 1) {
TYPE_ERROR(str of length 1,arg);
goto _error;
}
*out = krk_unicodeCodepoint(AS_STRING(arg),0);
}
break;
}
/**
* @c f Accept a Kuroko float as C float.
*/
case 'f': {
float * out = va_arg(args, float*);
if (arg != KWARGS_VAL(0)) {
if (!IS_FLOATING(arg)) {
TYPE_ERROR(float,arg);
goto _error;
}
*out = AS_FLOATING(arg);
}
break;
}
/**
* @c d Accept a Kuroko float as C double.
*/
case 'd': {
double * out = va_arg(args, double*);
if (arg != KWARGS_VAL(0)) {
if (!IS_FLOATING(arg)) {
TYPE_ERROR(float,arg);
goto _error;
}
*out = AS_FLOATING(arg);
}
break;
}
/**
* @c p Accept any value and examine its truthiness, returning an @c int.
* Python's docs call this "predicate", if you were wondering where
* the @c p came from. If bool conversion raises an exception, arg
* parsing ends with failure and that exception remains set.
*/
case 'p': {
int * out = va_arg(args, int*);
if (arg != KWARGS_VAL(0)) {
*out = !krk_isFalsey(arg);
if (krk_currentThread.flags & KRK_THREAD_HAS_EXCEPTION) goto _error;
}
break;
}
default: {
krk_runtimeError(vm.exceptions->typeError, "unrecognized directive '%c' in format string", *fmt);
goto _error;
}
}
krk_pop();
oarg++;
}
if (iarg < argc) {
/**
* If we got through the format string and there are still positional arguments,
* we got more than we expected and should raise an exception.
*/
krk_runtimeError(vm.exceptions->argumentError, "%s() takes %s %d argument%s (%d given)",
_method_name, required ? "exactly" : "at most", oarg, oarg == 1 ? "" : "s", argc);
return 0;
}
if (!acceptextrakws && hasKw && AS_DICT(argv[argc])->count) {
/**
* If we don't accept extra keyword arguments and there's still anything left
* in the dict, raise an exception about unexpected keyword arguments. The
* remaining key (or keys) should be a string, so we should find at least one
* thing to complain about by name...
*/
for (size_t i = 0; i < AS_DICT(argv[argc])->capacity; ++i) {
KrkTableEntry * entry = &AS_DICT(argv[argc])->entries[i];
if (IS_STRING(entry->key)) {
krk_runtimeError(vm.exceptions->typeError, "%s() got an unexpected keyword argument '%S'",
_method_name, AS_STRING(entry->key));
return 0;
}
}
}
return 1;
_error:
krk_pop(); /* name of argument with error */
return 0;
}
/**
* @brief Variable argument version of @c krk_parseVArgs.
*/
int krk_parseArgs_impl(
const char * _method_name,
int argc, const KrkValue argv[], int hasKw,
const char * format, const char ** names, ...) {
va_list args;
va_start(args, names);
int result = krk_parseVArgs(_method_name,argc,argv,hasKw,format,names,args);
va_end(args);
return result;
}