From d8afa73368cdff38125fa1f7d17ad5ce54c84def Mon Sep 17 00:00:00 2001 From: Bruce Hill Date: Sun, 6 Apr 2025 21:43:19 -0400 Subject: Improved inline C code: now uses `C_code` keyword and supports interpolation with @ --- src/ast.c | 2 +- src/ast.h | 3 +- src/compile.c | 82 +++++++++++++++++--------------- src/parse.c | 148 ++++++++++++++++++++++++++++++---------------------------- 4 files changed, 123 insertions(+), 112 deletions(-) (limited to 'src') diff --git a/src/ast.c b/src/ast.c index d4fd9569..b3506058 100644 --- a/src/ast.c +++ b/src/ast.c @@ -190,7 +190,7 @@ CORD ast_to_xml(ast_t *ast) T(NonOptional, "%r", ast_to_xml(data.value)) T(DocTest, "%r%r", optional_tagged("expression", data.expr), optional_tagged("expected", data.expected)) T(Use, "%r%r", optional_tagged("var", data.var), xml_escape(data.path)) - T(InlineCCode, "%r", xml_escape(data.code)) + T(InlineCCode, "%r", ast_list_to_xml(data.chunks)) T(Deserialize, "%r%r", type_ast_to_xml(data.type), ast_to_xml(data.value)) T(Extend, "%r", data.name, ast_to_xml(data.body)) T(ExplicitlyTyped, "%r", type_to_cord(data.type), ast_to_xml(data.ast)) diff --git a/src/ast.h b/src/ast.h index b48582f2..aeb418d9 100644 --- a/src/ast.h +++ b/src/ast.h @@ -19,6 +19,7 @@ #define FakeAST(ast_tag, ...) (new(ast_t, .tag=ast_tag, .__data.ast_tag={__VA_ARGS__})) #define WrapAST(ast, ast_tag, ...) (new(ast_t, .file=(ast)->file, .start=(ast)->start, .end=(ast)->end, .tag=ast_tag, .__data.ast_tag={__VA_ARGS__})) #define TextAST(ast, _str) WrapAST(ast, TextLiteral, .str=GC_strdup(_str)) +#define LiteralCode(code, ...) new(ast_t, .tag=InlineCCode, .__data.InlineCCode={.chunks=new(ast_list_t, .ast=FakeAST(TextLiteral, code)), __VA_ARGS__}) #define Match(x, _tag) ((x)->tag == _tag ? &(x)->__data._tag : (errx(1, __FILE__ ":%d This was supposed to be a " # _tag "\n", __LINE__), &(x)->__data._tag)) #define BINARY_OPERANDS(ast) ({ if (!is_binary_operation(ast)) errx(1, __FILE__ ":%d This is not a binary operation!", __LINE__); (ast)->__data.Plus; }) @@ -324,7 +325,7 @@ struct ast_s { enum { USE_LOCAL, USE_MODULE, USE_SHARED_OBJECT, USE_HEADER, USE_C_CODE, USE_ASM } what; } Use; struct { - CORD code; + ast_list_t *chunks; struct type_s *type; type_ast_t *type_ast; } InlineCCode; diff --git a/src/compile.c b/src/compile.c index b49f4c49..cdfaf5c6 100644 --- a/src/compile.c +++ b/src/compile.c @@ -123,7 +123,7 @@ static bool promote(env_t *env, ast_t *ast, CORD *code, type_t *actual, type_t * // Numeric promotions/demotions if ((is_numeric_type(actual) || actual->tag == BoolType) && (is_numeric_type(needed) || needed->tag == BoolType)) { - arg_ast_t *args = new(arg_ast_t, .value=FakeAST(InlineCCode, .code=*code, .type=actual)); + arg_ast_t *args = new(arg_ast_t, .value=LiteralCode(*code, .type=actual)); binding_t *constructor = get_constructor(env, needed, args); if (constructor) { auto fn = Match(constructor->type, FunctionType); @@ -555,7 +555,7 @@ static CORD compile_update_assignment(env_t *env, ast_t *ast) *binop = *ast; binop->tag = binop_tag(binop->tag); if (needs_idemotency_fix) - binop->__data.Plus.lhs = WrapAST(update.lhs, InlineCCode, .code="*lhs", .type=lhs_t); + binop->__data.Plus.lhs = LiteralCode("*lhs", .type=lhs_t); update_assignment = CORD_all(lhs, " = ", compile_to_type(env, binop, lhs_t), ";"); } @@ -1027,7 +1027,7 @@ static CORD _compile_statement(env_t *env, ast_t *ast) if (!is_idempotent(when->subject)) { prefix = CORD_all("{\n", compile_declaration(subject_t, "_when_subject"), " = ", compile(env, subject), ";\n"); suffix = "}\n"; - subject = WrapAST(subject, InlineCCode, .type=subject_t, .code="_when_subject"); + subject = LiteralCode("_when_subject", .type=subject_t); } CORD code = CORD_EMPTY; @@ -1195,7 +1195,7 @@ static CORD _compile_statement(env_t *env, ast_t *ast) ast_t *update_var = new(ast_t); *update_var = *test->expr; - update_var->__data.PlusUpdate.lhs = WrapAST(update.lhs, InlineCCode, .code="(*expr)", .type=lhs_t); // UNSAFE + update_var->__data.PlusUpdate.lhs = LiteralCode("(*expr)", .type=lhs_t); // UNSAFE test_code = CORD_all("({", compile_declaration(Type(PointerType, lhs_t), "expr"), " = &(", compile_lvalue(env, update.lhs), "); ", compile_statement(env, update_var), "; *expr; })"); @@ -1845,7 +1845,15 @@ static CORD _compile_statement(env_t *env, ast_t *ast) case Extern: return CORD_EMPTY; case InlineCCode: { auto inline_code = Match(ast, InlineCCode); - return inline_code->code; + CORD code = CORD_EMPTY; + for (ast_list_t *chunk = inline_code->chunks; chunk; chunk = chunk->next) { + if (chunk->ast->tag == TextLiteral) { + code = CORD_all(code, Match(chunk->ast, TextLiteral)->cord); + } else { + code = CORD_all(code, compile(env, chunk->ast)); + } + } + return code; } case Use: { auto use = Match(ast, Use); @@ -2060,8 +2068,8 @@ CORD compile_typed_array(env_t *env, ast_t *ast, type_t *array_type) env_t *scope = item_type->tag == EnumType ? with_enum_scope(env, item_type) : fresh_scope(env); static int64_t comp_num = 1; const char *comprehension_name = String("arr$", comp_num++); - ast_t *comprehension_var = FakeAST(InlineCCode, .code=CORD_all("&", comprehension_name), - .type=Type(PointerType, .pointed=array_type, .is_stack=true)); + ast_t *comprehension_var = LiteralCode(CORD_all("&", comprehension_name), + .type=Type(PointerType, .pointed=array_type, .is_stack=true)); Closure_t comp_action = {.fn=add_to_array_comprehension, .userdata=comprehension_var}; scope->comprehension_action = &comp_action; CORD code = CORD_all("({ Array_t ", comprehension_name, " = {};"); @@ -2109,8 +2117,8 @@ CORD compile_typed_set(env_t *env, ast_t *ast, type_t *set_type) static int64_t comp_num = 1; env_t *scope = item_type->tag == EnumType ? with_enum_scope(env, item_type) : fresh_scope(env); const char *comprehension_name = String("set$", comp_num++); - ast_t *comprehension_var = FakeAST(InlineCCode, .code=CORD_all("&", comprehension_name), - .type=Type(PointerType, .pointed=set_type, .is_stack=true)); + ast_t *comprehension_var = LiteralCode(CORD_all("&", comprehension_name), + .type=Type(PointerType, .pointed=set_type, .is_stack=true)); CORD code = CORD_all("({ Table_t ", comprehension_name, " = {};"); Closure_t comp_action = {.fn=add_to_set_comprehension, .userdata=comprehension_var}; scope->comprehension_action = &comp_action; @@ -2177,7 +2185,7 @@ CORD compile_typed_table(env_t *env, ast_t *ast, type_t *table_type) static int64_t comp_num = 1; env_t *scope = fresh_scope(env); const char *comprehension_name = String("table$", comp_num++); - ast_t *comprehension_var = FakeAST(InlineCCode, .code=CORD_all("&", comprehension_name), + ast_t *comprehension_var = LiteralCode(CORD_all("&", comprehension_name), .type=Type(PointerType, .pointed=table_type, .is_stack=true)); CORD code = CORD_all("({ Table_t ", comprehension_name, " = {"); @@ -3129,10 +3137,9 @@ CORD compile(env_t *env, ast_t *ast) EXPECT_POINTER("an", "array"); type_t *item_ptr = Type(PointerType, .pointed=item_t, .is_stack=true); type_t *fn_t = NewFunctionType(Type(IntType, .bits=TYPE_IBITS32), {.name="x", .type=item_ptr}, {.name="y", .type=item_ptr}); - ast_t *default_cmp = FakeAST(InlineCCode, - .code=CORD_all("((Closure_t){.fn=generic_compare, .userdata=(void*)", - compile_type_info(item_t), "})"), - .type=Type(ClosureType, .fn=fn_t)); + ast_t *default_cmp = LiteralCode(CORD_all("((Closure_t){.fn=generic_compare, .userdata=(void*)", + compile_type_info(item_t), "})"), + .type=Type(ClosureType, .fn=fn_t)); arg_t *arg_spec = new(arg_t, .name="item", .type=item_t, .next=new(arg_t, .name="by", .type=Type(ClosureType, .fn=fn_t), .default_val=default_cmp)); CORD arg_code = compile_arguments(env, ast, arg_spec, call->args); @@ -3141,10 +3148,9 @@ CORD compile(env_t *env, ast_t *ast) EXPECT_POINTER("an", "array"); type_t *item_ptr = Type(PointerType, .pointed=item_t, .is_stack=true); type_t *fn_t = NewFunctionType(Type(IntType, .bits=TYPE_IBITS32), {.name="x", .type=item_ptr}, {.name="y", .type=item_ptr}); - ast_t *default_cmp = FakeAST(InlineCCode, - .code=CORD_all("((Closure_t){.fn=generic_compare, .userdata=(void*)", - compile_type_info(item_t), "})"), - .type=Type(ClosureType, .fn=fn_t)); + ast_t *default_cmp = LiteralCode(CORD_all("((Closure_t){.fn=generic_compare, .userdata=(void*)", + compile_type_info(item_t), "})"), + .type=Type(ClosureType, .fn=fn_t)); arg_t *arg_spec = new(arg_t, .name="by", .type=Type(ClosureType, .fn=fn_t), .default_val=default_cmp); CORD arg_code = compile_arguments(env, ast, arg_spec, call->args); return CORD_all("Array$heap_pop_value(", self, ", ", arg_code, ", ", compile_type(item_t), ", _, ", @@ -3153,10 +3159,10 @@ CORD compile(env_t *env, ast_t *ast) self = compile_to_pointer_depth(env, call->self, 0, call->args != NULL); type_t *item_ptr = Type(PointerType, .pointed=item_t, .is_stack=true); type_t *fn_t = NewFunctionType(Type(IntType, .bits=TYPE_IBITS32), {.name="x", .type=item_ptr}, {.name="y", .type=item_ptr}); - ast_t *default_cmp = FakeAST(InlineCCode, - .code=CORD_all("((Closure_t){.fn=generic_compare, .userdata=(void*)", - compile_type_info(item_t), "})"), - .type=Type(ClosureType, .fn=fn_t)); + ast_t *default_cmp = LiteralCode( + CORD_all("((Closure_t){.fn=generic_compare, .userdata=(void*)", + compile_type_info(item_t), "})"), + .type=Type(ClosureType, .fn=fn_t)); arg_t *arg_spec = new(arg_t, .name="target", .type=item_t, .next=new(arg_t, .name="by", .type=Type(ClosureType, .fn=fn_t), .default_val=default_cmp)); CORD arg_code = compile_arguments(env, ast, arg_spec, call->args); @@ -3522,7 +3528,7 @@ CORD compile(env_t *env, ast_t *ast) static int64_t next_id = 1; ast_t *item = FakeAST(Var, String("$it", next_id++)); - ast_t *body = FakeAST(InlineCCode, .code="{}"); // placeholder + ast_t *body = LiteralCode("{}"); // placeholder ast_t *loop = FakeAST(For, .vars=new(ast_list_t, .ast=item), .iter=reduction->iter, .body=body); env_t *body_scope = for_scope(env, loop); if (op == Equals || op == NotEquals || op == LessThan || op == LessThanOrEquals || op == GreaterThan || op == GreaterThanOrEquals) { @@ -3542,8 +3548,8 @@ CORD compile(env_t *env, ast_t *ast) ); ast_t *comparison = new(ast_t, .file=ast->file, .start=ast->start, .end=ast->end, - .tag=op, .__data.Plus.lhs=FakeAST(InlineCCode, .code="prev", .type=item_value_type), .__data.Plus.rhs=item_value); - body->__data.InlineCCode.code = CORD_all( + .tag=op, .__data.Plus.lhs=LiteralCode("prev", .type=item_value_type), .__data.Plus.rhs=item_value); + body->__data.InlineCCode.chunks = new(ast_list_t, .ast=FakeAST(TextLiteral, CORD_all( "if (result == NONE_BOOL) {\n" " prev = ", compile(body_scope, item_value), ";\n" " result = yes;\n" @@ -3554,7 +3560,7 @@ CORD compile(env_t *env, ast_t *ast) " result = no;\n", " break;\n", " }\n", - "}\n"); + "}\n"))); code = CORD_all(code, compile_statement(env, loop), "\nresult;})"); return code; } else if (op == Min || op == Max) { @@ -3576,25 +3582,25 @@ CORD compile(env_t *env, ast_t *ast) code = CORD_all(code, compile_declaration(key_type, superlative_key), ";\n"); ast_t *comparison = new(ast_t, .file=ast->file, .start=ast->start, .end=ast->end, - .tag=cmp_op, .__data.Plus.lhs=FakeAST(InlineCCode, .code="key", .type=key_type), - .__data.Plus.rhs=FakeAST(InlineCCode, .code=superlative_key, .type=key_type)); + .tag=cmp_op, .__data.Plus.lhs=LiteralCode("key", .type=key_type), + .__data.Plus.rhs=LiteralCode(superlative_key, .type=key_type)); - body->__data.InlineCCode.code = CORD_all( + body->__data.InlineCCode.chunks = new(ast_list_t, .ast=FakeAST(TextLiteral, CORD_all( compile_declaration(key_type, "key"), " = ", compile(key_scope, reduction->key), ";\n", "if (!has_value || ", compile(body_scope, comparison), ") {\n" " ", superlative, " = ", compile(body_scope, item), ";\n" " ", superlative_key, " = key;\n" " has_value = yes;\n" - "}\n"); + "}\n"))); } else { ast_t *comparison = new(ast_t, .file=ast->file, .start=ast->start, .end=ast->end, .tag=cmp_op, .__data.Plus.lhs=item, - .__data.Plus.rhs=FakeAST(InlineCCode, .code=superlative, .type=item_t)); - body->__data.InlineCCode.code = CORD_all( + .__data.Plus.rhs=LiteralCode(superlative, .type=item_t)); + body->__data.InlineCCode.chunks = new(ast_list_t, .ast=FakeAST(TextLiteral, CORD_all( "if (!has_value || ", compile(body_scope, comparison), ") {\n" " ", superlative, " = ", compile(body_scope, item), ";\n" " has_value = yes;\n" - "}\n"); + "}\n"))); } @@ -3634,16 +3640,16 @@ CORD compile(env_t *env, ast_t *ast) } ast_t *combination = new(ast_t, .file=ast->file, .start=ast->start, .end=ast->end, - .tag=op, .__data.Plus.lhs=FakeAST(InlineCCode, .code="reduction", .type=reduction_type), + .tag=op, .__data.Plus.lhs=LiteralCode("reduction", .type=reduction_type), .__data.Plus.rhs=item_value); - body->__data.InlineCCode.code = CORD_all( + body->__data.InlineCCode.chunks = new(ast_list_t, .ast=FakeAST(TextLiteral, CORD_all( "if (!has_value) {\n" " reduction = ", compile(body_scope, item_value), ";\n" " has_value = yes;\n" "} else {\n" " reduction = ", compile(body_scope, combination), ";\n", early_out, - "}\n"); + "}\n"))); code = CORD_all(code, compile_statement(env, loop), "\nhas_value ? ", promote_to_optional(reduction_type, "reduction"), " : ", compile_none(reduction_type), ";})"); @@ -3815,9 +3821,9 @@ CORD compile(env_t *env, ast_t *ast) case InlineCCode: { type_t *t = get_type(env, ast); if (t->tag == VoidType) - return CORD_all("{\n", Match(ast, InlineCCode)->code, "\n}"); + return CORD_all("{\n", compile_statement(env, ast), "\n}"); else - return Match(ast, InlineCCode)->code; + return compile_statement(env, ast); } case Use: code_err(ast, "Compiling 'use' as expression!"); case Defer: code_err(ast, "Compiling 'defer' as expression!"); diff --git a/src/parse.c b/src/parse.c index 3b8f55bf..c31bea64 100644 --- a/src/parse.c +++ b/src/parse.c @@ -61,8 +61,8 @@ int op_tightness[] = { }; static const char *keywords[] = { - "_max_", "_min_", "and", "break", "continue", "defer", "deserialize", "do", "else", "enum", - "extend", "extern", "for", "func", "if", "in", "inline", "lang", "mod", "mod1", "no", "none", + "C_code", "_max_", "_min_", "and", "break", "continue", "defer", "deserialize", "do", "else", "enum", + "extend", "extern", "for", "func", "if", "in", "lang", "mod", "mod1", "no", "none", "not", "or", "pass", "return", "skip", "skip", "stop", "struct", "then", "unless", "use", "when", "while", "xor", "yes", }; @@ -147,6 +147,7 @@ static PARSER(parse_var); static PARSER(parse_when); static PARSER(parse_while); static PARSER(parse_deserialize); +static ast_list_t *_parse_text_helper(parse_ctx_t *ctx, const char **out_pos, char open_quote, char close_quote, char open_interp); // // Print a parse error and exit (or use the on_err longjmp) @@ -1186,55 +1187,11 @@ PARSER(parse_bool) { return NULL; } -PARSER(parse_text) { - // ('"' ... '"' / "'" ... "'" / "`" ... "`") - // "$" [name] [interp-char] quote-char ... close-quote - const char *start = pos; - const char *lang = NULL; - - // Escape sequence, e.g. \r\n - if (*pos == '\\') { - CORD cord = CORD_EMPTY; - do { - const char *c = unescape(ctx, &pos); - cord = CORD_cat(cord, c); - // cord = CORD_cat_char(cord, c); - } while (*pos == '\\'); - return NewAST(ctx->file, start, pos, TextLiteral, .cord=cord); - } - - char open_quote, close_quote, open_interp = '$'; - if (match(&pos, "\"")) { // Double quote - open_quote = '"', close_quote = '"', open_interp = '$'; - } else if (match(&pos, "`")) { // Backtick - open_quote = '`', close_quote = '`', open_interp = '$'; - } else if (match(&pos, "'")) { // Single quote - open_quote = '\'', close_quote = '\'', open_interp = '\x03'; - } else if (match(&pos, "$")) { // Customized strings - lang = get_id(&pos); - // $"..." or $@"...." - static const char *interp_chars = "~!@#$%^&*+=\\?"; - if (match(&pos, "$")) { // Disable interpolation with $ - open_interp = '\x03'; - } else if (strchr(interp_chars, *pos)) { - open_interp = *pos; - ++pos; - } else if (*pos == '(') { - open_interp = '@'; // For shell commands - } - static const char *quote_chars = "\"'`|/;([{<"; - if (!strchr(quote_chars, *pos)) - parser_err(ctx, pos, pos+1, "This is not a valid string quotation character. Valid characters are: \"'`|/;([{<"); - open_quote = *pos; - ++pos; - close_quote = closing[(int)open_quote] ? closing[(int)open_quote] : open_quote; - } else { - return NULL; - } - +ast_list_t *_parse_text_helper(parse_ctx_t *ctx, const char **out_pos, char open_quote, char close_quote, char open_interp) +{ + const char *pos = *out_pos; int64_t starting_indent = get_indent(ctx, pos); int64_t string_indent = starting_indent + SPACES_PER_INDENT; - ast_list_t *chunks = NULL; CORD chunk = CORD_EMPTY; const char *chunk_start = pos; @@ -1299,6 +1256,57 @@ PARSER(parse_text) { REVERSE_LIST(chunks); char close_str[2] = {close_quote, 0}; expect_closing(ctx, &pos, close_str, "I was expecting a ", close_quote, " to finish this string"); + *out_pos = pos; + return chunks; +} + +PARSER(parse_text) { + // ('"' ... '"' / "'" ... "'" / "`" ... "`") + // "$" [name] [interp-char] quote-char ... close-quote + const char *start = pos; + const char *lang = NULL; + + // Escape sequence, e.g. \r\n + if (*pos == '\\') { + CORD cord = CORD_EMPTY; + do { + const char *c = unescape(ctx, &pos); + cord = CORD_cat(cord, c); + // cord = CORD_cat_char(cord, c); + } while (*pos == '\\'); + return NewAST(ctx->file, start, pos, TextLiteral, .cord=cord); + } + + char open_quote, close_quote, open_interp = '$'; + if (match(&pos, "\"")) { // Double quote + open_quote = '"', close_quote = '"', open_interp = '$'; + } else if (match(&pos, "`")) { // Backtick + open_quote = '`', close_quote = '`', open_interp = '$'; + } else if (match(&pos, "'")) { // Single quote + open_quote = '\'', close_quote = '\'', open_interp = '\x03'; + } else if (match(&pos, "$")) { // Customized strings + lang = get_id(&pos); + // $"..." or $@"...." + static const char *interp_chars = "~!@#$%^&*+=\\?"; + if (match(&pos, "$")) { // Disable interpolation with $ + open_interp = '\x03'; + } else if (strchr(interp_chars, *pos)) { + open_interp = *pos; + ++pos; + } else if (*pos == '(') { + open_interp = '@'; // For shell commands + } + static const char *quote_chars = "\"'`|/;([{<"; + if (!strchr(quote_chars, *pos)) + parser_err(ctx, pos, pos+1, "This is not a valid string quotation character. Valid characters are: \"'`|/;([{<"); + open_quote = *pos; + ++pos; + close_quote = closing[(int)open_quote] ? closing[(int)open_quote] : open_quote; + } else { + return NULL; + } + + ast_list_t *chunks = _parse_text_helper(ctx, &pos, open_quote, close_quote, open_interp); return NewAST(ctx->file, start, pos, TextJoin, .lang=lang, .children=chunks); } @@ -2258,34 +2266,30 @@ PARSER(parse_extern) { PARSER(parse_inline_c) { const char *start = pos; - if (!match_word(&pos, "inline")) return NULL; - - spaces(&pos); - if (!match_word(&pos, "C")) return NULL; + if (!match_word(&pos, "C_code")) return NULL; spaces(&pos); type_ast_t *type = NULL; - if (match(&pos, ":")) - type = expect(ctx, start, &pos, parse_type, "I couldn't parse the type for this inline C code"); - - spaces(&pos); - if (!match(&pos, "{")) - parser_err(ctx, start, pos, "I expected a '{' here"); - - int depth = 1; - const char *c_code_start = pos; - for (; *pos && depth > 0; ++pos) { - if (*pos == '}') --depth; - else if (*pos == '{') ++depth; + ast_list_t *chunks; + if (match(&pos, ":")) { + type = expect(ctx, start, &pos, parse_type, "I couldn't parse the type for this C_code code"); + spaces(&pos); + if (!match(&pos, "(")) + parser_err(ctx, start, pos, "I expected a '(' here"); + chunks = new(ast_list_t, .ast=NewAST(ctx->file, pos, pos, TextLiteral, "({"), + .next=_parse_text_helper(ctx, &pos, '(', ')', '@')); + if (type) { + REVERSE_LIST(chunks); + chunks = new(ast_list_t, .ast=NewAST(ctx->file, pos, pos, TextLiteral, "; })"), .next=chunks); + REVERSE_LIST(chunks); + } + } else { + if (!match(&pos, "{")) + parser_err(ctx, start, pos, "I expected a '{' here"); + chunks = _parse_text_helper(ctx, &pos, '{', '}', '@'); } - if (depth != 0) - parser_err(ctx, start, start+1, "I couldn't find the closing '}' for this inline C code"); - - CORD c_code = GC_strndup(c_code_start, (size_t)((pos-1) - c_code_start)); - if (type) - c_code = CORD_all("({ ", c_code, "; })"); - return NewAST(ctx->file, start, pos, InlineCCode, .code=c_code, .type_ast=type); + return NewAST(ctx->file, start, pos, InlineCCode, .chunks=chunks, .type_ast=type); } PARSER(parse_doctest) { -- cgit v1.2.3