Move main() etc. into main.c
This commit is contained in:
parent
5ca9fdb042
commit
fc4f0bef28
|
@ -1,4 +1,4 @@
|
||||||
files = pebblisp.c tokens.c object.c env.c web.c plfunc.c hash.c
|
files = main.c pebblisp.c tokens.c object.c env.c web.c plfunc.c hash.c
|
||||||
libs = -lreadline -lmicrohttpd
|
libs = -lreadline -lmicrohttpd
|
||||||
exe = pl
|
exe = pl
|
||||||
|
|
||||||
|
|
|
@ -25,13 +25,6 @@
|
||||||
|
|
||||||
(def reloadConfig (fn () (loadfile config)))
|
(def reloadConfig (fn () (loadfile config)))
|
||||||
|
|
||||||
(def hour (fn (ti) (
|
|
||||||
(def h (% ti.hour 12))
|
|
||||||
(if (= 0 h) 12 h)
|
|
||||||
)))
|
|
||||||
|
|
||||||
(def zero (fn (num) (cat (if (< num 10) "0" "") num)))
|
|
||||||
|
|
||||||
(def string (fn (a) (cat "" a)))
|
(def string (fn (a) (cat "" a)))
|
||||||
|
|
||||||
(struct Alias (name value))
|
(struct Alias (name value))
|
||||||
|
@ -52,6 +45,13 @@
|
||||||
(if (iserr match) text match.value)
|
(if (iserr match) text match.value)
|
||||||
)))
|
)))
|
||||||
|
|
||||||
|
(def hour (fn (ti) (
|
||||||
|
(def h (% ti.hour 12))
|
||||||
|
(if (= 0 h) 12 h)
|
||||||
|
)))
|
||||||
|
|
||||||
|
(def zero (fn (num) (cat (if (< num 10) "0" "") num)))
|
||||||
|
|
||||||
(def clock (fn (ti) (cat (hour ti) ":" (zero ti.minute) ":" (zero ti.sec))))
|
(def clock (fn (ti) (cat (hour ti) ":" (zero ti.minute) ":" (zero ti.sec))))
|
||||||
|
|
||||||
(def cleanDir (fn () (
|
(def cleanDir (fn () (
|
||||||
|
|
|
@ -0,0 +1,234 @@
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
|
||||||
|
#include "pebblisp.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <readline/readline.h>
|
||||||
|
#include <readline/history.h>
|
||||||
|
|
||||||
|
char* getPrompt(struct Environment* env)
|
||||||
|
{
|
||||||
|
Object prompt = fetchFromEnvironment("prompt", env);
|
||||||
|
prompt = cloneObject(prompt);
|
||||||
|
if (prompt.type == TYPE_STRING) {
|
||||||
|
char* ret = readline(prompt.string);
|
||||||
|
cleanObject(&prompt);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
Object param = stringFromSlice("", 1);
|
||||||
|
Object e = listEvalLambda(&prompt, ¶m, 2, env);
|
||||||
|
cleanObject(&prompt);
|
||||||
|
cleanObject(¶m);
|
||||||
|
char* ret = readline(e.string);
|
||||||
|
cleanObject(&e);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* preprocess(char* buf, struct Environment* env)
|
||||||
|
{
|
||||||
|
Object lambda = fetchFromEnvironment("preprocess", env);
|
||||||
|
Object buffer = nullTerminated(buf);
|
||||||
|
Object s = listEvalLambda(&lambda, &buffer, 2, env);
|
||||||
|
size_t length;
|
||||||
|
return stringObj(&s, &length);
|
||||||
|
}
|
||||||
|
|
||||||
|
void repl(struct Environment* env)
|
||||||
|
{
|
||||||
|
char* buf;
|
||||||
|
using_history();
|
||||||
|
|
||||||
|
while ((buf = getPrompt(env)) != NULL) {
|
||||||
|
if (strcmp("q", buf) == 0) {
|
||||||
|
free(buf);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
buf = preprocess(buf, env);
|
||||||
|
if (buf[0] == '\0') {
|
||||||
|
free(buf);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
add_history(buf);
|
||||||
|
if ((buf[0] == 'c' && buf[1] == 'd')) {
|
||||||
|
char* oldBuf = buf;
|
||||||
|
buf = malloc(sizeof(char) * strlen(buf + 6));
|
||||||
|
sprintf(buf, "(cd \"%s\")", oldBuf + 3);
|
||||||
|
free(oldBuf);
|
||||||
|
}
|
||||||
|
if ((buf[0] == '?' && (buf[1] == ' ' || buf[1] == '\0'))) {
|
||||||
|
char* oldBuf = buf;
|
||||||
|
buf = malloc(sizeof(char) * strlen(buf + 3));
|
||||||
|
sprintf(buf, "(%s)", oldBuf);
|
||||||
|
free(oldBuf);
|
||||||
|
}
|
||||||
|
Object o = parseEval(buf, env);
|
||||||
|
if (isFuncy(o) || isError(o, DID_NOT_FIND_SYMBOL)) {
|
||||||
|
cleanObject(&o);
|
||||||
|
system(buf);
|
||||||
|
free(buf);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
free(buf);
|
||||||
|
|
||||||
|
size_t length;
|
||||||
|
char* output = stringObj(&o, &length);
|
||||||
|
cleanObject(&o);
|
||||||
|
printColored(output);
|
||||||
|
free(output);
|
||||||
|
printf("[0m\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadArgsIntoEnv(int argc, const char* argv[], struct Environment* env)
|
||||||
|
{
|
||||||
|
Object args = listObject();
|
||||||
|
for (int i = 0; i < argc; i++) {
|
||||||
|
nf_addToList(&args, nullTerminated(argv[i]));
|
||||||
|
}
|
||||||
|
addToEnv(env, "args", args);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __x86_64__
|
||||||
|
|
||||||
|
#include <signal.h>
|
||||||
|
#include <ucontext.h>
|
||||||
|
|
||||||
|
int nestedSegfault = 0;
|
||||||
|
|
||||||
|
void handler(int nSignum, siginfo_t* si, void* vcontext)
|
||||||
|
{
|
||||||
|
if (nestedSegfault) {
|
||||||
|
printf("Nested segfault!!!\n");
|
||||||
|
exit(139);
|
||||||
|
}
|
||||||
|
nestedSegfault = 1;
|
||||||
|
|
||||||
|
printf("Segfaulted!\n");
|
||||||
|
struct Slice* lastOpen = getLastOpen();
|
||||||
|
if (lastOpen) {
|
||||||
|
printf("line: %d\n%s\n", lastOpen->lineNumber, lastOpen->text);
|
||||||
|
} else {
|
||||||
|
printf("Happened before token processing.\n");
|
||||||
|
}
|
||||||
|
ucontext_t* context = vcontext;
|
||||||
|
context->uc_mcontext.gregs[REG_RIP]++;
|
||||||
|
exit(139);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupSegfaultHandler()
|
||||||
|
{
|
||||||
|
struct sigaction action;
|
||||||
|
memset(&action, 0, sizeof(struct sigaction));
|
||||||
|
action.sa_flags = SA_SIGINFO;
|
||||||
|
action.sa_sigaction = handler;
|
||||||
|
sigaction(SIGSEGV, &action, NULL);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
void setupSegfaultHandler()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct Settings {
|
||||||
|
int runTests;
|
||||||
|
int ignoreConfig;
|
||||||
|
int ignoreLib;
|
||||||
|
int moreToDo;
|
||||||
|
const char* configFile;
|
||||||
|
} settings;
|
||||||
|
|
||||||
|
#define RUN_TESTS_ARG "--run-tests"
|
||||||
|
#define RUN_DETAILED_TESTS "=detailed"
|
||||||
|
#define IGNORE_CONFIG_ARG "--ignore-config"
|
||||||
|
#define IGNORE_LIB_ARG "--ignore-lib"
|
||||||
|
#define CONFIG_FILE_ARG "--config="
|
||||||
|
|
||||||
|
void getSettings(int argc, const char* argv[])
|
||||||
|
{
|
||||||
|
settings.runTests = 0;
|
||||||
|
settings.ignoreConfig = 0;
|
||||||
|
settings.ignoreLib = 0;
|
||||||
|
settings.moreToDo = 0;
|
||||||
|
settings.configFile = NULL;
|
||||||
|
|
||||||
|
size_t runTestsLen = strlen(RUN_TESTS_ARG);
|
||||||
|
size_t configFileLen = strlen(CONFIG_FILE_ARG);
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
if (strncmp(argv[i], RUN_TESTS_ARG, runTestsLen) == 0) {
|
||||||
|
int isDetailed = strcmp(argv[i] + runTestsLen, RUN_DETAILED_TESTS) == 0;
|
||||||
|
settings.runTests = isDetailed ? 2 : 1;
|
||||||
|
} else if (strncmp(argv[i], CONFIG_FILE_ARG, configFileLen) == 0) {
|
||||||
|
settings.configFile = argv[i] + configFileLen;
|
||||||
|
} else if (strcmp(argv[i], IGNORE_CONFIG_ARG) == 0) {
|
||||||
|
settings.ignoreConfig = 1;
|
||||||
|
} else if (strcmp(argv[i], IGNORE_LIB_ARG) == 0) {
|
||||||
|
settings.ignoreLib = 1;
|
||||||
|
} else if (argv[i][0] == '-') {
|
||||||
|
fprintf(stderr, "Unrecognized argument: '%s'\n", argv[i]);
|
||||||
|
} else if (i == (argc - 1)) {
|
||||||
|
settings.moreToDo = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, const char* argv[])
|
||||||
|
{
|
||||||
|
setupSegfaultHandler();
|
||||||
|
getSettings(argc, argv);
|
||||||
|
|
||||||
|
struct Environment env = defaultEnv();
|
||||||
|
setGlobal(&env);
|
||||||
|
|
||||||
|
if (settings.runTests) {
|
||||||
|
int ret = runTests(settings.runTests == 2);
|
||||||
|
shredDictionary();
|
||||||
|
deleteEnv(global());
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings.ignoreLib) {
|
||||||
|
readFile(SCRIPTDIR "/lib.pbl", &env);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object o = parseEval("(def prompt \"pebblisp::> \")", &env);
|
||||||
|
cleanObject(&o);
|
||||||
|
o = parseEval("(def preprocess (fn (text) (text)))", &env);
|
||||||
|
cleanObject(&o);
|
||||||
|
|
||||||
|
if (!settings.ignoreConfig) {
|
||||||
|
char config[128];
|
||||||
|
if (settings.configFile) {
|
||||||
|
sprintf(config, "%s", settings.configFile);
|
||||||
|
} else {
|
||||||
|
const char* const home = getenv("HOME");
|
||||||
|
sprintf(config, "%s/.pebblisp.pbl", home);
|
||||||
|
}
|
||||||
|
if (readFile(config, &env) == 1 && settings.configFile) {
|
||||||
|
fprintf(stderr, "Config file not found at %s\n", config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.moreToDo) {
|
||||||
|
FILE* file = fopen(argv[argc - 1], "r");
|
||||||
|
if (file) {
|
||||||
|
// Execute a file
|
||||||
|
loadArgsIntoEnv(argc, argv, &env);
|
||||||
|
_readFile(file, &env);
|
||||||
|
} else {
|
||||||
|
// Run arguments directly as pl code
|
||||||
|
Object r = parseEval(argv[argc - 1], &env);
|
||||||
|
printAndClean(&r);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Run a repl
|
||||||
|
loadArgsIntoEnv(argc, argv, &env);
|
||||||
|
repl(&env);
|
||||||
|
}
|
||||||
|
deleteEnv(&env);
|
||||||
|
shredDictionary();
|
||||||
|
// eprintf("totalSearchDepth: %d of %d searches\n", getTotalSearchDepth(), getTotalSearches());
|
||||||
|
// eprintf("\nHEAP-ALLOCATED OBJECTS: %d\n", getAllocations());
|
||||||
|
// eprintf("TOTAL OBJECT.C ALLOC: %zu\n", getBytes());
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
#ifndef PEBBLISP_MAIN_H
|
||||||
|
#define PEBBLISP_MAIN_H
|
||||||
|
|
||||||
|
#endif // PEBBLISP_MAIN_H
|
197
src/pebblisp.c
197
src/pebblisp.c
|
@ -1,20 +1,11 @@
|
||||||
#ifdef STANDALONE
|
|
||||||
#define _GNU_SOURCE
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "pebblisp.h"
|
|
||||||
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "tokens.h"
|
#include "tokens.h"
|
||||||
|
#include "pebblisp.h"
|
||||||
|
|
||||||
#ifdef STANDALONE
|
#ifdef STANDALONE
|
||||||
|
|
||||||
#include <readline/readline.h>
|
|
||||||
#include <readline/history.h>
|
|
||||||
|
|
||||||
#include "web.h"
|
#include "web.h"
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -466,6 +457,11 @@ Result parseAtom(struct Slice* s)
|
||||||
|
|
||||||
struct Slice* lastOpen = NULL;
|
struct Slice* lastOpen = NULL;
|
||||||
|
|
||||||
|
struct Slice* getLastOpen()
|
||||||
|
{
|
||||||
|
return lastOpen;
|
||||||
|
}
|
||||||
|
|
||||||
Object parseEval(const char* input, struct Environment* env)
|
Object parseEval(const char* input, struct Environment* env)
|
||||||
{
|
{
|
||||||
struct Error err = noError();
|
struct Error err = noError();
|
||||||
|
@ -540,6 +536,7 @@ Object typeCheck(const char* funcName, Object* params, int length,
|
||||||
|
|
||||||
#ifdef STANDALONE
|
#ifdef STANDALONE
|
||||||
|
|
||||||
|
/// Returns 1 if the file could not be opened. Otherwise, 0
|
||||||
int readFile(const char* filename, struct Environment* env)
|
int readFile(const char* filename, struct Environment* env)
|
||||||
{
|
{
|
||||||
FILE* input = fopen(filename, "r");
|
FILE* input = fopen(filename, "r");
|
||||||
|
@ -592,182 +589,4 @@ int _readFile(FILE* input, struct Environment* env)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
char* getPrompt(struct Environment* env)
|
#endif // STANDALONE
|
||||||
{
|
|
||||||
Object prompt = fetchFromEnvironment("prompt", env);
|
|
||||||
prompt = cloneObject(prompt);
|
|
||||||
if (prompt.type == TYPE_STRING) {
|
|
||||||
char* ret = readline(prompt.string);
|
|
||||||
cleanObject(&prompt);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
Object param = stringFromSlice("", 1);
|
|
||||||
Object e = listEvalLambda(&prompt, ¶m, 2, env);
|
|
||||||
cleanObject(&prompt);
|
|
||||||
cleanObject(¶m);
|
|
||||||
char* ret = readline(e.string);
|
|
||||||
cleanObject(&e);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
char* preprocess(char* buf, struct Environment* env)
|
|
||||||
{
|
|
||||||
Object lambda = fetchFromEnvironment("preprocess", env);
|
|
||||||
Object buffer = nullTerminated(buf);
|
|
||||||
Object s = listEvalLambda(&lambda, &buffer, 2, env);
|
|
||||||
size_t length;
|
|
||||||
return stringObj(&s, &length);
|
|
||||||
}
|
|
||||||
|
|
||||||
void repl(struct Environment* env)
|
|
||||||
{
|
|
||||||
char* buf;
|
|
||||||
using_history();
|
|
||||||
|
|
||||||
while ((buf = getPrompt(env)) != NULL) {
|
|
||||||
if (strcmp("q", buf) == 0) {
|
|
||||||
free(buf);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
buf = preprocess(buf, env);
|
|
||||||
if (buf[0] == '\0') {
|
|
||||||
free(buf);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
add_history(buf);
|
|
||||||
if ((buf[0] == 'c' && buf[1] == 'd')) {
|
|
||||||
char* oldBuf = buf;
|
|
||||||
buf = malloc(sizeof(char) * strlen(buf + 6));
|
|
||||||
sprintf(buf, "(cd \"%s\")", oldBuf + 3);
|
|
||||||
free(oldBuf);
|
|
||||||
}
|
|
||||||
if ((buf[0] == '?' && (buf[1] == ' ' || buf[1] == '\0'))) {
|
|
||||||
char* oldBuf = buf;
|
|
||||||
buf = malloc(sizeof(char) * strlen(buf + 3));
|
|
||||||
sprintf(buf, "(%s)", oldBuf);
|
|
||||||
free(oldBuf);
|
|
||||||
}
|
|
||||||
Object o = parseEval(buf, env);
|
|
||||||
if (isFuncy(o) || isError(o, DID_NOT_FIND_SYMBOL)) {
|
|
||||||
cleanObject(&o);
|
|
||||||
system(buf);
|
|
||||||
free(buf);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
free(buf);
|
|
||||||
|
|
||||||
size_t length;
|
|
||||||
char* output = stringObj(&o, &length);
|
|
||||||
cleanObject(&o);
|
|
||||||
printColored(output);
|
|
||||||
free(output);
|
|
||||||
printf("[0m\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadArgsIntoEnv(int argc, const char* argv[], struct Environment* env)
|
|
||||||
{
|
|
||||||
Object args = listObject();
|
|
||||||
for (int i = 0; i < argc; i++) {
|
|
||||||
nf_addToList(&args, nullTerminated(argv[i]));
|
|
||||||
}
|
|
||||||
addToEnv(env, "args", args);
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef __x86_64__
|
|
||||||
#include <signal.h>
|
|
||||||
#include <ucontext.h>
|
|
||||||
|
|
||||||
int nestedSegfault = 0;
|
|
||||||
|
|
||||||
void handler(int nSignum, siginfo_t* si, void* vcontext)
|
|
||||||
{
|
|
||||||
if (nestedSegfault) {
|
|
||||||
printf("Nested segfault!!!\n");
|
|
||||||
exit(139);
|
|
||||||
}
|
|
||||||
nestedSegfault = 1;
|
|
||||||
|
|
||||||
printf("Segfaulted!\n");
|
|
||||||
if (lastOpen) {
|
|
||||||
printf("line: %d\n%s\n", lastOpen->lineNumber, lastOpen->text);
|
|
||||||
} else {
|
|
||||||
printf("Happened before token processing.\n");
|
|
||||||
}
|
|
||||||
ucontext_t* context = vcontext;
|
|
||||||
context->uc_mcontext.gregs[REG_RIP]++;
|
|
||||||
exit(139);
|
|
||||||
}
|
|
||||||
void setupSegfaultHandler()
|
|
||||||
{
|
|
||||||
struct sigaction action;
|
|
||||||
memset(&action, 0, sizeof(struct sigaction));
|
|
||||||
action.sa_flags = SA_SIGINFO;
|
|
||||||
action.sa_sigaction = handler;
|
|
||||||
sigaction(SIGSEGV, &action, NULL);
|
|
||||||
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
void setupSegfaultHandler()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// TODO: add --no-lib and --no-config and/or --config=
|
|
||||||
int main(int argc, const char* argv[])
|
|
||||||
{
|
|
||||||
setupSegfaultHandler();
|
|
||||||
|
|
||||||
const char* const home = getenv("HOME");
|
|
||||||
char config[strlen(home) + 15];
|
|
||||||
|
|
||||||
struct Environment env = defaultEnv();
|
|
||||||
setGlobal(&env);
|
|
||||||
|
|
||||||
if (argc == 2) {
|
|
||||||
const char* runTestsArg = "--run-tests";
|
|
||||||
if (strncmp(argv[1], runTestsArg, strlen(runTestsArg)) == 0) {
|
|
||||||
int ret = runTests(strcmp(argv[1] + strlen(runTestsArg), "=detailed") == 0);
|
|
||||||
shredDictionary();
|
|
||||||
deleteEnv(global());
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readFile(SCRIPTDIR "/lib.pbl", &env);
|
|
||||||
|
|
||||||
Object o = parseEval("(def prompt \"pebblisp::> \")", &env);
|
|
||||||
cleanObject(&o);
|
|
||||||
o = parseEval("(def preprocess (fn (text) (text)))", &env);
|
|
||||||
cleanObject(&o);
|
|
||||||
|
|
||||||
sprintf(config, "%s/.pebblisp.pbl", home);
|
|
||||||
readFile(config, &env);
|
|
||||||
|
|
||||||
if (argc >= 2) {
|
|
||||||
FILE* file = fopen(argv[1], "r");
|
|
||||||
if (file) {
|
|
||||||
// Execute a file
|
|
||||||
loadArgsIntoEnv(argc, argv, &env);
|
|
||||||
_readFile(file, &env);
|
|
||||||
} else {
|
|
||||||
// Run arguments directly as pl code
|
|
||||||
Object r = numberObject(0);
|
|
||||||
for (int i = 1; i < argc; i++) {
|
|
||||||
r = parseEval(argv[i], &env);
|
|
||||||
printAndClean(&r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Run a repl
|
|
||||||
loadArgsIntoEnv(argc, argv, &env);
|
|
||||||
repl(&env);
|
|
||||||
}
|
|
||||||
deleteEnv(&env);
|
|
||||||
shredDictionary();
|
|
||||||
// eprintf("totalSearchDepth: %d of %d searches\n", getTotalSearchDepth(), getTotalSearches());
|
|
||||||
// eprintf("\nHEAP-ALLOCATED OBJECTS: %d\n", getAllocations());
|
|
||||||
// eprintf("TOTAL OBJECT.C ALLOC: %zu\n", getBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
|
@ -97,6 +97,8 @@ int _readFile(FILE* input, struct Environment* env);
|
||||||
|
|
||||||
int readFile(const char* filename, struct Environment* env);
|
int readFile(const char* filename, struct Environment* env);
|
||||||
|
|
||||||
|
struct Slice* getLastOpen();
|
||||||
|
|
||||||
#endif /* STANDALONE */
|
#endif /* STANDALONE */
|
||||||
|
|
||||||
fn(mapO, "map",
|
fn(mapO, "map",
|
||||||
|
|
Loading…
Reference in New Issue