From 73d9aa29c468a12096c0c7efc6535ffb45d85507 Mon Sep 17 00:00:00 2001 From: Dale Weiler Date: Wed, 14 Aug 2013 06:02:15 +0000 Subject: [PATCH] Made intrinsics seperate from the parser. --- Makefile | 2 +- include.mk | 2 +- intrin.h => intrin.c | 236 +++++++++++++++++++++---------------------- parser.c | 20 ++-- parser.h | 32 +++++- 5 files changed, 152 insertions(+), 140 deletions(-) rename intrin.h => intrin.c (64%) diff --git a/Makefile b/Makefile index 3396ec6..e6ff4be 100644 --- a/Makefile +++ b/Makefile @@ -150,7 +150,7 @@ stat.o: gmqcc.h opts.def test.o: gmqcc.h opts.def main.o: gmqcc.h opts.def lexer.h lexer.o: gmqcc.h opts.def lexer.h -parser.o: parser.h gmqcc.h opts.def lexer.h ast.h ir.h intrin.h +parser.o: parser.h gmqcc.h opts.def lexer.h ast.h ir.h code.o: gmqcc.h opts.def ast.o: gmqcc.h opts.def ast.h ir.h parser.h lexer.h ir.o: gmqcc.h opts.def ir.h diff --git a/include.mk b/include.mk index 77342d9..a1d7430 100644 --- a/include.mk +++ b/include.mk @@ -14,7 +14,7 @@ LDFLAGS += LIBS += -lm #objects -OBJ_C = main.o lexer.o parser.o fs.o stat.o util.o code.o ast.o ir.o conout.o ftepp.o opts.o utf8.o correct.o fold.o +OBJ_C = main.o lexer.o parser.o fs.o stat.o util.o code.o ast.o ir.o conout.o ftepp.o opts.o utf8.o correct.o fold.o intrin.o OBJ_P = util.o fs.o conout.o opts.o pak.o stat.o OBJ_T = test.o util.o opts.o conout.o fs.o stat.o OBJ_X = exec-standalone.o util.o opts.o conout.o fs.o stat.o diff --git a/intrin.h b/intrin.c similarity index 64% rename from intrin.h rename to intrin.c index 0c0e861..26012e7 100644 --- a/intrin.h +++ b/intrin.c @@ -20,6 +20,8 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ +#include +#include "parser.h" /* * Provides all the "intrinsics" / "builtins" for GMQCC. These can do @@ -30,49 +32,34 @@ * is entered when -fintrin is used (causing all existing builtins to * be ignored by the compiler and instead interface through here. */ -typedef struct { - ast_expression *(*intrin)(parser_t *); - const char *name; - const char *alias; -} intrin_t; - -static ht intrin_intrinsics(void) { - static ht intrinsics = NULL; - if (!intrinsics) - intrinsics = util_htnew(PARSER_HT_SIZE); - - return intrinsics; -} - -#define INTRIN_VAL(VALUE, NAME, FUNC, STYPE, VTYPE) \ - do { \ - (VALUE) = ast_value_new ( \ - parser_ctx(parser), \ - "__builtin_" NAME, \ - TYPE_FUNCTION \ - ); \ - (VALUE)->expression.next = (ast_expression*)ast_value_new ( \ - parser_ctx(parser), \ - STYPE, \ - VTYPE \ - ); \ - (FUNC) = ast_function_new ( \ - parser_ctx(parser), \ - "__builtin_" NAME, \ - (VALUE) \ - ); \ +#define INTRIN_VAL(VALUE, NAME, FUNC, STYPE, VTYPE) \ + do { \ + (VALUE) = ast_value_new ( \ + parser_ctx(intrin->parser), \ + "__builtin_" NAME, \ + TYPE_FUNCTION \ + ); \ + (VALUE)->expression.next = (ast_expression*)ast_value_new ( \ + parser_ctx(intrin->parser), \ + STYPE, \ + VTYPE \ + ); \ + (FUNC) = ast_function_new ( \ + parser_ctx(intrin->parser), \ + "__builtin_" NAME, \ + (VALUE) \ + ); \ } while (0) -#define INTRIN_REG(FUNC, VALUE) \ - do { \ - vec_push(parser->functions, (FUNC)); \ - vec_push(parser->globals, (ast_expression*)(VALUE)); \ +#define INTRIN_REG(FUNC, VALUE) \ + do { \ + vec_push(intrin->parser->functions, (FUNC)); \ + vec_push(intrin->parser->globals, (ast_expression*)(VALUE)); \ } while (0) #define QC_M_E 2.71828182845905 -static ast_expression *intrin_func(parser_t *parser, const char *name); -static ast_expression *intrin_pow (parser_t *parser) { +static ast_expression *intrin_pow (intrin_t *intrin) { /* * float pow(float x, float y) { * float local = 1.0f; @@ -90,12 +77,12 @@ static ast_expression *intrin_pow (parser_t *parser) { static ast_value *value = NULL; if (!value) { - ast_value *arg1 = ast_value_new(parser_ctx(parser), "x", TYPE_FLOAT); - ast_value *arg2 = ast_value_new(parser_ctx(parser), "y", TYPE_FLOAT); - ast_value *local = ast_value_new(parser_ctx(parser), "local", TYPE_FLOAT); - ast_block *body = ast_block_new(parser_ctx(parser)); - ast_block *l1b = ast_block_new(parser_ctx(parser)); /* loop 1 body */ - ast_block *l2b = ast_block_new(parser_ctx(parser)); /* looo 2 body */ + ast_value *arg1 = ast_value_new(parser_ctx(intrin->parser), "x", TYPE_FLOAT); + ast_value *arg2 = ast_value_new(parser_ctx(intrin->parser), "y", TYPE_FLOAT); + ast_value *local = ast_value_new(parser_ctx(intrin->parser), "local", TYPE_FLOAT); + ast_block *body = ast_block_new(parser_ctx(intrin->parser)); + ast_block *l1b = ast_block_new(parser_ctx(intrin->parser)); /* loop 1 body */ + ast_block *l2b = ast_block_new(parser_ctx(intrin->parser)); /* loop 2 body */ ast_loop *loop1 = NULL; ast_loop *loop2 = NULL; ast_function *func = NULL; @@ -112,28 +99,28 @@ static ast_expression *intrin_pow (parser_t *parser) { /* assignment to local of value 1.0f */ vec_push(body->exprs, (ast_expression*)ast_store_new ( - parser_ctx(parser), + parser_ctx(intrin->parser), INSTR_STORE_F, (ast_expression*)local, - (ast_expression*)parser->fold->imm_float[1] /* 1 == 1.0f */ + (ast_expression*)intrin->fold->imm_float[1] /* 1 == 1.0f */ ) ); /* y >>= 2 */ vec_push(l2b->exprs, (ast_expression*)ast_binstore_new ( - parser_ctx(parser), + parser_ctx(intrin->parser), INSTR_STORE_F, INSTR_MUL_F, (ast_expression*)arg2, - (ast_expression*)fold_constgen_float(parser->fold, 0.25f) + (ast_expression*)fold_constgen_float(intrin->parser->fold, 0.25f) ) ); /* x *= x */ vec_push(l2b->exprs, (ast_expression*)ast_binstore_new ( - parser_ctx(parser), + parser_ctx(intrin->parser), INSTR_STORE_F, INSTR_MUL_F, (ast_expression*)arg1, @@ -143,13 +130,13 @@ static ast_expression *intrin_pow (parser_t *parser) { /* while (!(y&1)) */ loop2 = ast_loop_new ( - parser_ctx(parser), + parser_ctx(intrin->parser), NULL, (ast_expression*)ast_binary_new ( - parser_ctx(parser), + parser_ctx(intrin->parser), INSTR_AND, (ast_expression*)arg2, - (ast_expression*)parser->fold->imm_float[1] /* 1 == 1.0f */ + (ast_expression*)intrin->fold->imm_float[1] /* 1 == 1.0f */ ), true, /* ! not */ NULL, @@ -164,17 +151,17 @@ static ast_expression *intrin_pow (parser_t *parser) { /* y-- */ vec_push(l1b->exprs, (ast_expression*)ast_binstore_new ( - parser_ctx(parser), + parser_ctx(intrin->parser), INSTR_STORE_F, INSTR_SUB_F, (ast_expression*)arg2, - (ast_expression*)parser->fold->imm_float[1] /* 1 == 1.0f */ + (ast_expression*)intrin->fold->imm_float[1] /* 1 == 1.0f */ ) ); /* local *= x */ vec_push(l1b->exprs, (ast_expression*)ast_binstore_new ( - parser_ctx(parser), + parser_ctx(intrin->parser), INSTR_STORE_F, INSTR_MUL_F, (ast_expression*)local, @@ -184,13 +171,13 @@ static ast_expression *intrin_pow (parser_t *parser) { /* while (y > 0) */ loop1 = ast_loop_new ( - parser_ctx(parser), + parser_ctx(intrin->parser), NULL, (ast_expression*)ast_binary_new ( - parser_ctx(parser), + parser_ctx(intrin->parser), INSTR_GT, (ast_expression*)arg2, - (ast_expression*)parser->fold->imm_float[0] /* 0 == 0.0f */ + (ast_expression*)intrin->fold->imm_float[0] /* 0 == 0.0f */ ), false, NULL, @@ -205,7 +192,7 @@ static ast_expression *intrin_pow (parser_t *parser) { /* return local; */ vec_push(body->exprs, (ast_expression*)ast_return_new ( - parser_ctx(parser), + parser_ctx(intrin->parser), (ast_expression*)local ) ); @@ -219,7 +206,7 @@ static ast_expression *intrin_pow (parser_t *parser) { return (ast_expression*)value; } -static ast_expression *intrin_mod(parser_t *parser) { +static ast_expression *intrin_mod(intrin_t *intrin) { /* * float mod(float x, float y) { * return x - y * floor(x / y); @@ -228,10 +215,10 @@ static ast_expression *intrin_mod(parser_t *parser) { static ast_value *value = NULL; if (!value) { - ast_call *call = ast_call_new (parser_ctx(parser), intrin_func(parser, "floor")); - ast_value *arg1 = ast_value_new(parser_ctx(parser), "x", TYPE_FLOAT); - ast_value *arg2 = ast_value_new(parser_ctx(parser), "y", TYPE_FLOAT); - ast_block *body = ast_block_new(parser_ctx(parser)); + ast_call *call = ast_call_new (parser_ctx(intrin->parser), intrin_func(intrin, "floor")); + ast_value *arg1 = ast_value_new(parser_ctx(intrin->parser), "x", TYPE_FLOAT); + ast_value *arg2 = ast_value_new(parser_ctx(intrin->parser), "y", TYPE_FLOAT); + ast_block *body = ast_block_new(parser_ctx(intrin->parser)); ast_function *func = NULL; INTRIN_VAL(value, "mod", func, "", TYPE_FLOAT); @@ -239,7 +226,7 @@ static ast_expression *intrin_mod(parser_t *parser) { /* floor(x/y) */ vec_push(call->params, (ast_expression*)ast_binary_new ( - parser_ctx(parser), + parser_ctx(intrin->parser), INSTR_DIV_F, (ast_expression*)arg1, (ast_expression*)arg2 @@ -248,13 +235,13 @@ static ast_expression *intrin_mod(parser_t *parser) { vec_push(body->exprs, (ast_expression*)ast_return_new( - parser_ctx(parser), + parser_ctx(intrin->parser), (ast_expression*)ast_binary_new( - parser_ctx(parser), + parser_ctx(intrin->parser), INSTR_SUB_F, (ast_expression*)arg1, (ast_expression*)ast_binary_new( - parser_ctx(parser), + parser_ctx(intrin->parser), INSTR_MUL_F, (ast_expression*)arg2, (ast_expression*)call @@ -266,7 +253,7 @@ static ast_expression *intrin_mod(parser_t *parser) { vec_push(value->expression.params, arg1); /* float x (for param) */ vec_push(value->expression.params, arg2); /* float y (for param) */ - vec_push(func->blocks, body); /* {{{ body }}} */ + vec_push(func->blocks, body); /* {{{ body }}} */ INTRIN_REG(func, value); } @@ -274,7 +261,7 @@ static ast_expression *intrin_mod(parser_t *parser) { return (ast_expression*)value; } -static ast_expression *intrin_exp(parser_t *parser) { +static ast_expression *intrin_exp(intrin_t *intrin) { /* * float exp(float x) { * return pow(QC_M_E, x); @@ -283,21 +270,21 @@ static ast_expression *intrin_exp(parser_t *parser) { static ast_value *value = NULL; if (!value) { - ast_call *call = ast_call_new (parser_ctx(parser), intrin_func(parser, "pow")); - ast_value *arg1 = ast_value_new (parser_ctx(parser), "x", TYPE_FLOAT); - ast_block *body = ast_block_new (parser_ctx(parser)); + ast_call *call = ast_call_new (parser_ctx(intrin->parser), intrin_func(intrin, "pow")); + ast_value *arg1 = ast_value_new(parser_ctx(intrin->parser), "x", TYPE_FLOAT); + ast_block *body = ast_block_new(parser_ctx(intrin->parser)); ast_function *func = NULL; INTRIN_VAL(value, "exp", func, "", TYPE_FLOAT); /* push arguments for params to call */ - vec_push(call->params, (ast_expression*)fold_constgen_float(parser->fold, QC_M_E)); + vec_push(call->params, (ast_expression*)fold_constgen_float(intrin->fold, QC_M_E)); vec_push(call->params, (ast_expression*)arg1); /* return pow(QC_M_E, x) */ vec_push(body->exprs, (ast_expression*)ast_return_new( - parser_ctx(parser), + parser_ctx(intrin->parser), (ast_expression*)call ) ); @@ -312,7 +299,7 @@ static ast_expression *intrin_exp(parser_t *parser) { return (ast_expression*)value; } -static ast_expression *intrin_isnan(parser_t *parser) { +static ast_expression *intrin_isnan(intrin_t *intrin) { /* * float isnan(float x) { * float local; @@ -324,9 +311,9 @@ static ast_expression *intrin_isnan(parser_t *parser) { static ast_value *value = NULL; if (!value) { - ast_value *arg1 = ast_value_new (parser_ctx(parser), "x", TYPE_FLOAT); - ast_value *local = ast_value_new (parser_ctx(parser), "local", TYPE_FLOAT); - ast_block *body = ast_block_new (parser_ctx(parser)); + ast_value *arg1 = ast_value_new(parser_ctx(intrin->parser), "x", TYPE_FLOAT); + ast_value *local = ast_value_new(parser_ctx(intrin->parser), "local", TYPE_FLOAT); + ast_block *body = ast_block_new(parser_ctx(intrin->parser)); ast_function *func = NULL; INTRIN_VAL(value, "isnan", func, "", TYPE_FLOAT); @@ -334,7 +321,7 @@ static ast_expression *intrin_isnan(parser_t *parser) { vec_push(body->locals, local); vec_push(body->exprs, (ast_expression*)ast_store_new( - parser_ctx(parser), + parser_ctx(intrin->parser), INSTR_STORE_F, (ast_expression*)local, (ast_expression*)arg1 @@ -343,9 +330,9 @@ static ast_expression *intrin_isnan(parser_t *parser) { vec_push(body->exprs, (ast_expression*)ast_return_new( - parser_ctx(parser), + parser_ctx(intrin->parser), (ast_expression*)ast_binary_new( - parser_ctx(parser), + parser_ctx(intrin->parser), INSTR_NE_F, (ast_expression*)arg1, (ast_expression*)local @@ -354,7 +341,6 @@ static ast_expression *intrin_isnan(parser_t *parser) { ); vec_push(value->expression.params, arg1); - vec_push(func->blocks, body); INTRIN_REG(func, value); @@ -362,13 +348,19 @@ static ast_expression *intrin_isnan(parser_t *parser) { return (ast_expression*)value; } +#undef INTRIN_REG +#undef INTRIN_VAL -static ast_expression *intrin_debug_typestring(parser_t *parser) { - (void)parser; +/* + * TODO: make static (and handle ast_type_string) here for the builtin + * instead of in SYA parse close. + */ +ast_expression *intrin_debug_typestring(intrin_t *intrin) { + (void)intrin; return (ast_expression*)0x1; } -static intrin_t intrinsics[] = { +static const intrin_func_t intrinsics[] = { {&intrin_exp, "__builtin_exp", "exp"}, {&intrin_mod, "__builtin_mod", "mod"}, {&intrin_pow, "__builtin_pow", "pow"}, @@ -376,50 +368,48 @@ static intrin_t intrinsics[] = { {&intrin_debug_typestring, "__builtin_debug_typestring", ""} }; -void intrin_intrinsics_destroy(parser_t *parser) { - /*size_t i;*/ - (void)parser; - util_htdel(intrin_intrinsics()); +static void intrin_error(intrin_t *intrin, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + vcompile_error(intrin->parser->lex->tok.ctx, fmt, ap); + va_end(ap); } +/* exposed */ +intrin_t *intrin_init(parser_t *parser) { + intrin_t *intrin = (intrin_t*)mem_a(sizeof(intrin_t)); + intrin->parser = parser; + intrin->fold = parser->fold; + intrin->intrinsics = NULL; -static ast_expression *intrin_func(parser_t *parser, const char *name) { - static bool init = false; - size_t i = 0; - void *find; - - /* register the intrinsics in the hashtable for O(1) lookup */ - if (!init) { - for (i = 0; i < sizeof(intrinsics)/sizeof(*intrinsics); i++) - util_htset(intrin_intrinsics(), intrinsics[i].name, &intrinsics[i]); + vec_append(intrin->intrinsics, sizeof(intrinsics)/sizeof(*intrinsics), intrinsics); - init = true; /* only once */ - } - - /* - * jesus fucking christ, Blub design something less fucking - * impossible to use, like a ast_is_builtin(ast_expression *), also - * use a hashtable :P - */ - if ((find = (void*)parser_find_global(parser, name)) && ((ast_value*)find)->expression.vtype == TYPE_FUNCTION) - for (i = 0; i < vec_size(parser->functions); ++i) - if (((ast_value*)find)->name && !strcmp(parser->functions[i]->name, ((ast_value*)find)->name) && parser->functions[i]->builtin < 0) - return (ast_expression*)find; + return intrin; +} - if ((find = util_htget(intrin_intrinsics(), name))) { - /* intrinsic is in table. This will "generate the function" so - * to speak (if it's not already generated). - */ - return ((intrin_t*)find)->intrin(parser); - } +void intrin_cleanup(intrin_t *intrin) { + vec_free(intrin->intrinsics); + mem_d(intrin); +} - /* - * check aliases now to see if there is an implementation of it. - */ - for (i = 0; i < sizeof(intrinsics) / sizeof(*intrinsics); i++) - if (!strcmp(intrinsics[i].alias, name)) - return intrinsics[i].intrin(parser); +ast_expression *intrin_func(intrin_t *intrin, const char *name) { + size_t i = 0; + void *find; - parseerror(parser, "need function: `%s` compiler depends on it", name); + /* try current first */ + if ((find = (void*)parser_find_global(intrin->parser, name)) && ((ast_value*)find)->expression.vtype == TYPE_FUNCTION) + for (i = 0; i < vec_size(intrin->parser->functions); ++i) + if (((ast_value*)find)->name && !strcmp(intrin->parser->functions[i]->name, ((ast_value*)find)->name) && intrin->parser->functions[i]->builtin < 0) + return (ast_expression*)find; + /* try name second */ + for (i = 0; i < vec_size(intrin->intrinsics); i++) + if (!strcmp(intrin->intrinsics[i].name, name)) + return intrin->intrinsics[i].intrin(intrin); + /* try alias third */ + for (i = 0; i < vec_size(intrin->intrinsics); i++) + if (!strcmp(intrin->intrinsics[i].alias, name)) + return intrin->intrinsics[i].intrin(intrin); + + intrin_error(intrin, "need function: `%s` compiler depends on it", name); return NULL; } diff --git a/parser.c b/parser.c index bbca3cf..9b7c75d 100644 --- a/parser.c +++ b/parser.c @@ -109,7 +109,7 @@ static ast_expression* parser_find_label(parser_t *parser, const char *name) return NULL; } -static ast_expression* parser_find_global(parser_t *parser, const char *name) +ast_expression* parser_find_global(parser_t *parser, const char *name) { ast_expression *var = (ast_expression*)util_htget(parser->aliases, parser_tokval(parser)); if (var) @@ -171,9 +171,6 @@ static ast_value* parser_find_typedef(parser_t *parser, const char *name, size_t return NULL; } -/* include intrinsics */ -#include "intrin.h" - typedef struct { size_t etype; /* 0 = expression, others are operators */ @@ -608,7 +605,7 @@ static bool parser_sy_apply_operator(parser_t *parser, shunt *sy) return false; } else if (!(out = fold_op(parser->fold, op, exprs))) { /* generate a call to __builtin_mod */ - ast_expression *mod = intrin_func(parser, "mod"); + ast_expression *mod = intrin_func(parser->intrin, "mod"); ast_call *call = NULL; if (!mod) return false; /* can return null for missing floor */ @@ -810,7 +807,7 @@ static bool parser_sy_apply_operator(parser_t *parser, shunt *sy) } if (!(out = fold_op(parser->fold, op, exprs))) { - ast_call *gencall = ast_call_new(parser_ctx(parser), intrin_func(parser, "pow")); + ast_call *gencall = ast_call_new(parser_ctx(parser), intrin_func(parser->intrin, "pow")); vec_push(gencall->params, exprs[0]); vec_push(gencall->params, exprs[1]); out = (ast_expression*)gencall; @@ -1199,7 +1196,7 @@ static bool parser_close_call(parser_t *parser, shunt *sy) * TODO handle this at the intrinsic level with an ast_intrinsic * node and codegen. */ - if ((fun = sy->out[fid].out) == intrin_debug_typestring(parser)) { + if ((fun = sy->out[fid].out) == intrin_debug_typestring(parser->intrin)) { char ty[1024]; if (fid+2 != vec_size(sy->out) || vec_last(sy->out).block) @@ -1561,7 +1558,7 @@ static bool parse_sya_operand(parser_t *parser, shunt *sy, bool with_labels) * use the identifier as is. */ if (!strncmp(parser_tokval(parser), "__builtin_", 10)) { - var = intrin_func(parser, parser_tokval(parser)); + var = intrin_func(parser->intrin, parser_tokval(parser)); } if (!var) { @@ -5868,7 +5865,8 @@ parser_t *parser_create() parser->reserved_version = NULL; } - parser->fold = fold_init(parser); + parser->fold = fold_init (parser); + parser->intrin = intrin_init(parser); return parser; } @@ -5986,9 +5984,9 @@ static void parser_remove_ast(parser_t *parser) if (parser->reserved_version) ast_value_delete(parser->reserved_version); - util_htdel(parser->aliases); - intrin_intrinsics_destroy(parser); + util_htdel(parser->aliases); fold_cleanup(parser->fold); + intrin_cleanup(parser->intrin); } void parser_cleanup(parser_t *parser) diff --git a/parser.h b/parser.h index 15c0ce4..3bb1178 100644 --- a/parser.h +++ b/parser.h @@ -27,6 +27,9 @@ #include "lexer.h" #include "ast.h" +typedef struct intrin_s intrin_t; +typedef struct parser_s parser_t; + typedef struct { struct parser_s *parser; ast_value **imm_float; /* vector */ @@ -36,9 +39,21 @@ typedef struct { hash_table_t *imm_string_dotranslate; /* map */ } fold_t; +typedef struct { + ast_expression *(*intrin)(intrin_t *); + const char *name; + const char *alias; +} intrin_func_t; + +struct intrin_s { + intrin_func_t *intrinsics; /* vector */ + parser_t *parser; + fold_t *fold; +}; + #define parser_ctx(p) ((p)->lex->tok.ctx) -typedef struct parser_s { +struct parser_s { lex_file *lex; int tok; @@ -98,11 +113,14 @@ typedef struct parser_s { /* collected information */ size_t max_param_count; - fold_t *fold; -} parser_t; + fold_t *fold; + intrin_t *intrin; +}; -char *parser_strdup(const char *str); +/* parser.c */ +char *parser_strdup (const char *str); +ast_expression *parser_find_global(parser_t *parser, const char *name); /* fold.c */ fold_t *fold_init (parser_t *); @@ -115,4 +133,10 @@ ast_expression *fold_op (fold_t *, const oper_info *, ast_expression int fold_cond (ir_value *, ast_function *, ast_ifthen *); +/* intrin.c */ +intrin_t *intrin_init (parser_t *parser); +void intrin_cleanup (intrin_t *intrin); +ast_expression *intrin_func (intrin_t *intrin, const char *name); +ast_expression *intrin_debug_typestring(intrin_t *intrin); + #endif -- 2.39.2