From c56a130896aaae62ac781142bcdf7f2e9c19ce19 Mon Sep 17 00:00:00 2001 From: hanez Date: Mon, 29 Sep 2025 03:26:35 +0200 Subject: [PATCH] Bastic value type casting. (0.12.0) --- CMakeLists.txt | 2 +- examples/cast_demo.fun | 54 ++++++++++++++++++ src/bytecode.c | 2 + src/bytecode.h | 1 + src/parser.c | 125 ++++------------------------------------- src/vm.c | 1 + src/vm.h | 2 +- src/vm/cast.c | 89 +++++++++++++++++++++++++++++ 8 files changed, 161 insertions(+), 115 deletions(-) create mode 100755 examples/cast_demo.fun create mode 100644 src/vm/cast.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e7ac0a..6d0831f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(fun VERSION 0.11.1 LANGUAGES C) +project(fun VERSION 0.12.0 LANGUAGES C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/cast_demo.fun b/examples/cast_demo.fun new file mode 100755 index 0000000..15cf359 --- /dev/null +++ b/examples/cast_demo.fun @@ -0,0 +1,54 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * + * Added: 2025-09-29 + */ + +print("=== CAST demo ===") + +// Number: parse decimal strings; invalid -> 0 +print(cast("123", "Number")) // -> 123 +print(cast("12x", "Number")) // -> 0 +print(cast(42, "Number")) // -> 42 + +// String: stringify any value +print(cast(100, "String")) // -> "100" +print(typeof(cast(100, "String"))) // -> "String" + +// Boolean: 0 -> 0, non-zero -> 1 +print(cast(0, "Boolean")) // -> 0 +print(cast(42, "Boolean")) // -> 1 + +// Array: arrays stay arrays; others get wrapped as single-element arrays +a = cast(42, "Array") +print(typeof(a)) // -> "Array" +print(len(a)) // -> 1 +print(a[0]) // -> 42 + +// Map: maps stay maps; others become empty maps +m = cast(42, "Map") +print(typeof(m)) // -> "Map" +// Expect zero keys for a fresh empty map +print(len(keys(m))) // -> 0 + +// Nil: always Nil +n = cast("x", "Nil") +print(typeof(n)) // -> "Nil" + +// Function: functions stay functions; others -> Nil +fun foo() + return 7 + +print(typeof(cast(foo, "Function"))) // -> "Function" +print(typeof(cast(42, "Function"))) // -> "Nil" + +// Chaining casts +print(cast(cast("42", "Number"), "String")) // -> "42" + +print("=== Done ===") diff --git a/src/bytecode.c b/src/bytecode.c index b098eee..3451469 100644 --- a/src/bytecode.c +++ b/src/bytecode.c @@ -92,6 +92,8 @@ static const char *opcode_name(OpCode op) { case OP_SLICE: return "SLICE"; case OP_TO_NUMBER: return "TO_NUMBER"; case OP_TO_STRING: return "TO_STRING"; + case OP_TYPEOF: return "TYPEOF"; + case OP_CAST: return "CAST"; case OP_SPLIT: return "SPLIT"; case OP_JOIN: return "JOIN"; case OP_SUBSTR: return "SUBSTR"; diff --git a/src/bytecode.h b/src/bytecode.h index 3c51050..b29f812 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -73,6 +73,7 @@ typedef enum { // conversions OP_TO_NUMBER, // pops any; pushes int (parse strings) OP_TO_STRING, // pops any; pushes string + OP_CAST, // pops typeName, value; pushes casted value (see vm/cast.c) OP_TYPEOF, // pops any; pushes string name of type OP_UCLAMP, // pops number; pushes number masked to N bits (operand = bits) OP_SCLAMP, // pops number; pushes number clamped to signed N-bit range (operand = bits) diff --git a/src/parser.c b/src/parser.c index 2ff6975..eb1fb4e 100644 --- a/src/parser.c +++ b/src/parser.c @@ -463,9 +463,18 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + if (strcmp(name, "cast") == 0) { + (*pos)++; /* '(' */ + /* cast(value, typeName) */ + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ',')) { parser_fail(*pos, "cast expects (value, typeName)"); free(name); return 0; } + if (!emit_expression(bc, src, len, pos) || !consume_char(src, len, pos, ')')) { parser_fail(*pos, "cast expects (value, typeName)"); free(name); return 0; } + bytecode_add_instruction(bc, OP_CAST, 0); + free(name); + return 1; + } if (strcmp(name, "typeof") == 0) { (*pos)++; /* '(' */ - /* Special-case: typeof() -> use declared integer subtype if available */ + /* Disable compile-time shortcut for typeof(); always evaluate at runtime */ size_t peek = *pos; char *vname = NULL; int handled = 0; @@ -473,109 +482,9 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) /* allow spaces before ')' */ skip_spaces(src, len, &peek); if (peek < len && src[peek] == ')') { - /* Find declared type metadata for the identifier */ - int decl_bits = 0; /* >0 unsigned width, <0 signed width, 0 unknown/non-integer */ - int lidx2 = local_find(vname); - int is_class_name = 0; - int gidx2 = -1; - if (lidx2 >= 0 && g_locals) { - decl_bits = g_locals->types[lidx2]; - } else { - /* lookup existing global without creating a new symbol */ - for (int gi_ = 0; gi_ < G.count; ++gi_) { - if (strcmp(G.names[gi_], vname) == 0) { gidx2 = gi_; break; } - } - if (gidx2 >= 0) { - decl_bits = G.types[gidx2]; - is_class_name = G.is_class[gidx2]; - } - } - - /* Map declared metadata to human-readable typeof */ - if (decl_bits == TYPE_META_STRING) { - int ci = bytecode_add_constant(bc, make_string("String")); - bytecode_add_instruction(bc, OP_LOAD_CONST, ci); - } else if (decl_bits == TYPE_META_NIL) { - int ci = bytecode_add_constant(bc, make_string("Nil")); - bytecode_add_instruction(bc, OP_LOAD_CONST, ci); - } else if (decl_bits == TYPE_META_CLASS || is_class_name) { - int ci = bytecode_add_constant(bc, make_string("Class")); - bytecode_add_instruction(bc, OP_LOAD_CONST, ci); - } else if (decl_bits == TYPE_META_BOOLEAN) { - /* Booleans are numeric 0/1 in Fun */ - int ci = bytecode_add_constant(bc, make_string("Number")); - bytecode_add_instruction(bc, OP_LOAD_CONST, ci); - } else if (decl_bits == 8 || decl_bits == 16 || decl_bits == 32 || decl_bits == 64 - || decl_bits == -8 || decl_bits == -16 || decl_bits == -32 || decl_bits == -64) { - int bits = decl_bits < 0 ? -decl_bits : decl_bits; - int is_signed = (decl_bits < 0); - char tbuf[16]; - snprintf(tbuf, sizeof(tbuf), "%s%d", is_signed ? "Sint" : "Uint", bits); - int ci = bytecode_add_constant(bc, make_string(tbuf)); - bytecode_add_instruction(bc, OP_LOAD_CONST, ci); - } else { - /* Fallback: load the variable, but if it's a class instance (Map with "__class"), return "Class" */ - if (lidx2 >= 0) { - bytecode_add_instruction(bc, OP_LOAD_LOCAL, lidx2); - } else { - /* If global not yet present, create symbol to load its value (likely nil) */ - int gi2 = -1; - /* try to reuse existing id if found earlier */ - for (int gi_ = 0; gi_ < G.count; ++gi_) { - if (strcmp(G.names[gi_], vname) == 0) { gi2 = gi_; break; } - } - if (gi2 < 0) gi2 = sym_index(vname); - bytecode_add_instruction(bc, OP_LOAD_GLOBAL, gi2); - } - /* Stack: [v] */ - bytecode_add_instruction(bc, OP_DUP, 0); /* [v, v] */ - bytecode_add_instruction(bc, OP_TYPEOF, 0); /* [v, tname] */ - { - int ciMap = bytecode_add_constant(bc, make_string("Map")); - bytecode_add_instruction(bc, OP_LOAD_CONST, ciMap); /* [v, tname, "Map"] */ - } - bytecode_add_instruction(bc, OP_EQ, 0); /* [v, isMap] */ - int j_if_not_map2 = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0); - - /* Map branch: check for __class tag */ - bytecode_add_instruction(bc, OP_DUP, 0); /* [v, v] */ - { - int kci = bytecode_add_constant(bc, make_string("__class")); - bytecode_add_instruction(bc, OP_LOAD_CONST, kci); /* [v, v, "__class"] */ - } - bytecode_add_instruction(bc, OP_HAS_KEY, 0); /* [v, has] */ - int j_no_meta2 = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0); - /* has __class -> call toString() and return */ - bytecode_add_instruction(bc, OP_DUP, 0); /* [v, v] */ - { - int kcits = bytecode_add_constant(bc, make_string("toString")); - bytecode_add_instruction(bc, OP_LOAD_CONST, kcits); /* [v, v, "toString"] */ - } - bytecode_add_instruction(bc, OP_INDEX_GET, 0); /* [v, func] */ - bytecode_add_instruction(bc, OP_SWAP, 0); /* [func, v] */ - bytecode_add_instruction(bc, OP_CALL, 1); /* [string] */ - int j_end2 = bytecode_add_instruction(bc, OP_JUMP, 0); - - /* no meta: drop v and return "Map" */ - bytecode_set_operand(bc, j_no_meta2, bc->instr_count); - bytecode_add_instruction(bc, OP_POP, 0); /* [] */ - { - int ciMap2 = bytecode_add_constant(bc, make_string("Map")); - bytecode_add_instruction(bc, OP_LOAD_CONST, ciMap2); - } - int after_map2 = bc->instr_count; - - /* not map: typeof(v) */ - bytecode_set_operand(bc, j_if_not_map2, after_map2); - bytecode_add_instruction(bc, OP_TYPEOF, 0); - - /* end */ - bytecode_set_operand(bc, j_end2, bc->instr_count); - } + /* Fall back to runtime evaluation path */ free(vname); - /* consume the ')' */ - *pos = peek + 1; - handled = 1; + /* handled remains 0 so we go to general-case below */ } else { /* not a simple identifier-only typeof */ free(vname); @@ -585,14 +494,6 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) if (!handled) { /* General case: typeof(expression) If the value is a Map with "__class" key, return that string; else return base typeof. - Stack plan: - - eval expr -> [v] - - DUP, TYPEOF, "Map" EQ -> [v, isMap] - - if not map: drop condition path and return TYPEOF(v) - - if map: - DUP, "__class", HAS_KEY -> [v, has] - if has: "__class", INDEX_GET -> [className]; return - else: POP v; return "Map" */ if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "typeof expects 1 argument"); free(name); return 0; } if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after typeof arg"); free(name); return 0; } @@ -637,8 +538,6 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) /* not map: compute typeof(v) */ bytecode_set_operand(bc, j_if_not_map, after_map); - /* Stack currently holds what after we patched? For not-map path, we still have [v] retained because JUMP_IF_FALSE popped cond. - Now produce typeof(v). */ bytecode_add_instruction(bc, OP_TYPEOF, 0); /* [tname] */ /* end */ diff --git a/src/vm.c b/src/vm.c index c20d58d..2abedd7 100644 --- a/src/vm.c +++ b/src/vm.c @@ -321,6 +321,7 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/print.c" #include "vm/to_number.c" #include "vm/to_string.c" + #include "vm/cast.c" #include "vm/typeof.c" #include "vm/uclamp.c" #include "vm/sclamp.c" diff --git a/src/vm.h b/src/vm.h index 9de96da..f19038c 100644 --- a/src/vm.h +++ b/src/vm.h @@ -27,7 +27,7 @@ static const char *opcode_names[] = { "MOD","AND","OR","NOT","DUP","SWAP", "MAKE_ARRAY","INDEX_GET","INDEX_SET", "LEN","PUSH","APOP","SET","INSERT","REMOVE","SLICE", - "TO_NUMBER","TO_STRING","TYPEOF", + "TO_NUMBER","TO_STRING","CAST","TYPEOF", "SPLIT","JOIN","SUBSTR","FIND", "CONTAINS","INDEX_OF","CLEAR", "ENUMERATE","ZIP", diff --git a/src/vm/cast.c b/src/vm/cast.c new file mode 100644 index 0000000..ebf0607 --- /dev/null +++ b/src/vm/cast.c @@ -0,0 +1,89 @@ +/** +* This file is part of the Fun programming language. + * https://hanez.org/project/fun/ + * + * Copyright 2025 Johannes Findeisen + * Licensed under the terms of the ISC license. + * https://opensource.org/license/isc-license-txt + */ + +/** + * @file cast.c + * @brief Implements the OP_CAST opcode for converting a value to a target type. + * + * Stack contract: + * - Pops: typeName (String), value (any) + * - Pushes: converted value (or Nil on unsupported target) + */ + +case OP_CAST: { + /* pop type then value (args pushed in this order: value, typeName) */ + Value t = pop_value(vm); + Value v = pop_value(vm); + + const char *tn = (t.type == VAL_STRING && t.s) ? t.s : NULL; + Value out = make_nil(); + + /* Normalize target name to lowercase into a small buffer */ + char target[32]; + int k = 0; + if (tn) { + const char *p = tn; + while (*p && k < (int)sizeof(target) - 1) { + char c = *p++; + if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); + target[k++] = c; + } + } + target[k] = '\0'; + + if (!tn) { + out = make_nil(); + } else if (strcmp(target, "number") == 0) { + if (v.type == VAL_INT) { + out = make_int(v.i); + } else if (v.type == VAL_STRING) { + const char *s = v.s ? v.s : ""; + const char *p = s; + while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++; + char *endp = NULL; + long long parsed = strtoll(p, &endp, 10); + while (endp && (*endp == ' ' || *endp == '\t' || *endp == '\r' || *endp == '\n')) endp++; + if (endp && *endp != '\0') out = make_int(0); + else out = make_int((int64_t)parsed); + } else { + out = make_int(0); + } + } else if (strcmp(target, "string") == 0) { + char *s = value_to_string_alloc(&v); + out = make_string(s ? s : ""); + if (s) free(s); + } else if (strcmp(target, "array") == 0) { + if (v.type == VAL_ARRAY) { + out = copy_value(&v); + } else { + Value tmp = deep_copy_value(&v); + out = make_array_from_values(&tmp, 1); + free_value(tmp); + } + } else if (strcmp(target, "map") == 0) { + if (v.type == VAL_MAP) { + out = copy_value(&v); + } else { + out = make_map_empty(); + } + } else if (strcmp(target, "nil") == 0) { + out = make_nil(); + } else if (strcmp(target, "function") == 0) { + out = (v.type == VAL_FUNCTION) ? copy_value(&v) : make_nil(); + } else if (strcmp(target, "boolean") == 0) { + out = make_int(value_is_truthy(&v) ? 1 : 0); + } else { + out = make_nil(); + } + + free_value(t); + free_value(v); + push_value(vm, out); + break; +}