diff --git a/CMakeLists.txt b/CMakeLists.txt index 32393d9..b5b1ddd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(fun VERSION 0.7.4 LANGUAGES C) +project(fun VERSION 0.8.0 LANGUAGES C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/type_safety.fun b/examples/type_safety.fun new file mode 100644 index 0000000..c7ff797 --- /dev/null +++ b/examples/type_safety.fun @@ -0,0 +1,72 @@ +#!/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. + * https://opensource.org/license/isc-license-txt + */ + +// Type Safety demonstration in Fun + +print("== Dynamic variable (untyped) ==") +x = 100 +print(typeof(x)) // -> "Number" +print(x) // -> 100 + +x = "hello" +print(typeof(x)) // -> "String" +print(x) // -> "hello" + +print("") +print("== Typed variables remain type-stable ==") + +// Strings must always be strings +string s = "hello" +print(typeof(s)) // -> "String" +print(s) // -> "hello" +// s = 42 // Uncomment to see runtime TypeError: expected String + +// Booleans are Numbers constrained to 0 or 1 (clamped) +boolean flag = 0 +print(typeof(flag)) // -> "Number" +print(flag) // -> 0 + +flag = 2 // will be clamped to 1 +print(flag) // -> 1 +// flag = "no" // Uncomment to see runtime TypeError: expected Number for boolean + +// number is a signed 64-bit integer and must remain Number +number n = 123 +print(typeof(n)) // -> "Number" +n = 456 +print(n) // -> 456 +// n = "oops" // Uncomment to see runtime TypeError: expected Number + +print("") +print("== Integer width clamping ==") + +// Signed 8-bit: range [-128..127] +int8 si = 130 // clamps to 127 +print(si) // -> 127 +si = -200 // clamps to -128 +print(si) // -> -128 + +// Unsigned 8-bit: range [0..255] +uint8 u = 255 +print(u) // -> 255 +u = 300 // clamps to 255 +print(u) // -> 255 +// u = "bad" // Uncomment to see runtime TypeError: expected Number + +print("") +print("== Nil-typed variables must stay Nil ==") + +nil nn = nil +print(typeof(nn)) // -> "Nil" +// nn = 0 // Uncomment to see runtime TypeError: expected Nil + +print("") +print("Done. Uncomment lines above to see type errors in action.") diff --git a/examples/type_safety_fails.fun b/examples/type_safety_fails.fun new file mode 100644 index 0000000..1a097b4 --- /dev/null +++ b/examples/type_safety_fails.fun @@ -0,0 +1,21 @@ +#!/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. + * https://opensource.org/license/isc-license-txt + */ + +print("Reassigning typed variables to another type should fail") + +string s = "hello" +print(typeof(s)) // -> "String" +print(s) // -> "hello" + +print("Now trying to assign a number to a string variable...") +s = 42 // Runtime TypeError: expected String (assignment rejected) + +print("This line will not execute due to the type error") diff --git a/src/parser.c b/src/parser.c index 337dee8..48d5161 100644 --- a/src/parser.c +++ b/src/parser.c @@ -62,6 +62,14 @@ static int g_err_col = 0; /* ---- compiler-generated temporary counter ---- */ static int g_temp_counter = 0; +/* Declared type metadata encoding in types[]: + 0 = dynamic/untyped; + positive/negative 8/16/32/64 = integers (negative means signed); + TYPE_META_STRING/BOOLEAN/NIL mark non-integer enforced types. */ +#define TYPE_META_STRING 10001 +#define TYPE_META_BOOLEAN 10002 +#define TYPE_META_NIL 10003 + static void parser_fail(size_t pos, const char *fmt, ...) { g_has_error = 1; g_err_pos = pos; @@ -1767,6 +1775,17 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si int decl_signed = (is_s8 || is_s16 || is_s32 || is_s64) ? 1 : 0; /* store decl bits with sign encoded: negative means signed (number is signed 64-bit) */ if (decl_signed) decl_bits = -decl_bits; + + /* declared type metadata: integers use decl_bits; string/boolean/nil use special markers */ + int decl_meta = decl_bits; + if (is_string) { + decl_meta = TYPE_META_STRING; + } else if (is_boolean) { + decl_meta = TYPE_META_BOOLEAN; + } else if (is_nil) { + decl_meta = TYPE_META_NIL; + } + free(name); /* read variable name */ @@ -1785,12 +1804,12 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si if (existing >= 0) lidx = existing; else lidx = local_add(varname); if (lidx >= 0) { - g_locals->types[lidx] = decl_bits; /* negative = signed, positive = unsigned width, 0 = non-integer */ + g_locals->types[lidx] = decl_meta; /* encoding: ±bits for integers; TYPE_META_* for non-integer enforced types; 0 = dynamic */ } } else { gi = sym_index(varname); if (gi >= 0) { - G.types[gi] = decl_bits; + G.types[gi] = decl_meta; } } free(varname); @@ -1802,11 +1821,87 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si parser_fail(local_pos, "Expected initializer expression after '='"); return; } - /* clamp if integer type was declared */ - int abs_bits = decl_bits < 0 ? -decl_bits : decl_bits; - if (abs_bits > 0) { - bytecode_add_instruction(bc, (decl_bits < 0) ? OP_SCLAMP : OP_UCLAMP, abs_bits); + + /* Enforce declared type on initializer */ + if (decl_meta == TYPE_META_STRING) { + /* expect String */ + bytecode_add_instruction(bc, OP_DUP, 0); + bytecode_add_instruction(bc, OP_TYPEOF, 0); + int ciExp = bytecode_add_constant(bc, make_string("String")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciExp); + bytecode_add_instruction(bc, OP_EQ, 0); + int j_to_error = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0); + int j_skip_err = bytecode_add_instruction(bc, OP_JUMP, 0); + /* error block */ + bytecode_set_operand(bc, j_to_error, bc->instr_count); + { + int ciMsg = bytecode_add_constant(bc, make_string("TypeError: expected String")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMsg); + bytecode_add_instruction(bc, OP_PRINT, 0); + bytecode_add_instruction(bc, OP_HALT, 0); + } + bytecode_set_operand(bc, j_skip_err, bc->instr_count); + } else if (decl_meta == TYPE_META_BOOLEAN) { + /* expect Number then clamp to 1 bit (unsigned) */ + bytecode_add_instruction(bc, OP_DUP, 0); + bytecode_add_instruction(bc, OP_TYPEOF, 0); + int ciNum = bytecode_add_constant(bc, make_string("Number")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciNum); + bytecode_add_instruction(bc, OP_EQ, 0); + int j_to_error = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0); + int j_skip_err = bytecode_add_instruction(bc, OP_JUMP, 0); + bytecode_set_operand(bc, j_to_error, bc->instr_count); + { + int ciMsg = bytecode_add_constant(bc, make_string("TypeError: expected Number for boolean")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMsg); + bytecode_add_instruction(bc, OP_PRINT, 0); + bytecode_add_instruction(bc, OP_HALT, 0); + } + bytecode_set_operand(bc, j_skip_err, bc->instr_count); + /* clamp to 0/1 */ + bytecode_add_instruction(bc, OP_UCLAMP, 1); + } else if (decl_meta == TYPE_META_NIL) { + /* expect Nil */ + bytecode_add_instruction(bc, OP_DUP, 0); + bytecode_add_instruction(bc, OP_TYPEOF, 0); + int ciNil = bytecode_add_constant(bc, make_string("Nil")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciNil); + bytecode_add_instruction(bc, OP_EQ, 0); + int j_to_error = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0); + int j_skip_err = bytecode_add_instruction(bc, OP_JUMP, 0); + bytecode_set_operand(bc, j_to_error, bc->instr_count); + { + int ciMsg = bytecode_add_constant(bc, make_string("TypeError: expected Nil")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMsg); + bytecode_add_instruction(bc, OP_PRINT, 0); + bytecode_add_instruction(bc, OP_HALT, 0); + } + bytecode_set_operand(bc, j_skip_err, bc->instr_count); + } else { + /* integer widths: expect Number then clamp */ + int abs_bits = decl_bits < 0 ? -decl_bits : decl_bits; + if (abs_bits > 0) { + /* typeof == Number */ + bytecode_add_instruction(bc, OP_DUP, 0); + bytecode_add_instruction(bc, OP_TYPEOF, 0); + int ciNum = bytecode_add_constant(bc, make_string("Number")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciNum); + bytecode_add_instruction(bc, OP_EQ, 0); + int j_to_error = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0); + int j_skip_err = bytecode_add_instruction(bc, OP_JUMP, 0); + bytecode_set_operand(bc, j_to_error, bc->instr_count); + { + int ciMsg = bytecode_add_constant(bc, make_string("TypeError: expected Number")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMsg); + bytecode_add_instruction(bc, OP_PRINT, 0); + bytecode_add_instruction(bc, OP_HALT, 0); + } + bytecode_set_operand(bc, j_skip_err, bc->instr_count); + + bytecode_add_instruction(bc, (decl_bits < 0) ? OP_SCLAMP : OP_UCLAMP, abs_bits); + } } + if (lidx >= 0) { bytecode_add_instruction(bc, OP_STORE_LOCAL, lidx); } else { @@ -1995,17 +2090,90 @@ static void parse_simple_statement(Bytecode *bc, const char *src, size_t len, si if (local_pos < len && src[local_pos] == '=') { local_pos++; /* '=' */ if (emit_expression(bc, src, len, &local_pos)) { - /* clamp to declared integer width if present */ - int decl_bits = 0; + /* enforce declared type if present (0 = dynamic) */ + int meta = 0; if (lidx >= 0 && g_locals) { - decl_bits = g_locals->types[lidx]; + meta = g_locals->types[lidx]; } else if (gi >= 0) { - decl_bits = G.types[gi]; + meta = G.types[gi]; } - int abs_bits = decl_bits < 0 ? -decl_bits : decl_bits; - if (abs_bits > 0) { - bytecode_add_instruction(bc, (decl_bits < 0) ? OP_SCLAMP : OP_UCLAMP, abs_bits); + + if (meta == TYPE_META_STRING) { + /* expect String */ + bytecode_add_instruction(bc, OP_DUP, 0); + bytecode_add_instruction(bc, OP_TYPEOF, 0); + int ciStr = bytecode_add_constant(bc, make_string("String")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciStr); + bytecode_add_instruction(bc, OP_EQ, 0); + int j_to_error = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0); + int j_skip_err = bytecode_add_instruction(bc, OP_JUMP, 0); + bytecode_set_operand(bc, j_to_error, bc->instr_count); + { + int ciMsg = bytecode_add_constant(bc, make_string("TypeError: expected String")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMsg); + bytecode_add_instruction(bc, OP_PRINT, 0); + bytecode_add_instruction(bc, OP_HALT, 0); + } + bytecode_set_operand(bc, j_skip_err, bc->instr_count); + } else if (meta == TYPE_META_BOOLEAN) { + /* expect Number then clamp to 1 bit (unsigned) */ + bytecode_add_instruction(bc, OP_DUP, 0); + bytecode_add_instruction(bc, OP_TYPEOF, 0); + int ciNum = bytecode_add_constant(bc, make_string("Number")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciNum); + bytecode_add_instruction(bc, OP_EQ, 0); + int j_to_error = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0); + int j_skip_err = bytecode_add_instruction(bc, OP_JUMP, 0); + bytecode_set_operand(bc, j_to_error, bc->instr_count); + { + int ciMsg = bytecode_add_constant(bc, make_string("TypeError: expected Number for boolean")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMsg); + bytecode_add_instruction(bc, OP_PRINT, 0); + bytecode_add_instruction(bc, OP_HALT, 0); + } + bytecode_set_operand(bc, j_skip_err, bc->instr_count); + bytecode_add_instruction(bc, OP_UCLAMP, 1); + } else if (meta == TYPE_META_NIL) { + /* expect Nil */ + bytecode_add_instruction(bc, OP_DUP, 0); + bytecode_add_instruction(bc, OP_TYPEOF, 0); + int ciNil = bytecode_add_constant(bc, make_string("Nil")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciNil); + bytecode_add_instruction(bc, OP_EQ, 0); + int j_to_error = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0); + int j_skip_err = bytecode_add_instruction(bc, OP_JUMP, 0); + bytecode_set_operand(bc, j_to_error, bc->instr_count); + { + int ciMsg = bytecode_add_constant(bc, make_string("TypeError: expected Nil")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMsg); + bytecode_add_instruction(bc, OP_PRINT, 0); + bytecode_add_instruction(bc, OP_HALT, 0); + } + bytecode_set_operand(bc, j_skip_err, bc->instr_count); + } else if (meta != 0) { + /* integer widths: expect Number then clamp to declared width */ + int abs_bits = meta < 0 ? -meta : meta; + /* typeof == Number */ + bytecode_add_instruction(bc, OP_DUP, 0); + bytecode_add_instruction(bc, OP_TYPEOF, 0); + int ciNum = bytecode_add_constant(bc, make_string("Number")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciNum); + bytecode_add_instruction(bc, OP_EQ, 0); + int j_to_error = bytecode_add_instruction(bc, OP_JUMP_IF_FALSE, 0); + int j_skip_err = bytecode_add_instruction(bc, OP_JUMP, 0); + bytecode_set_operand(bc, j_to_error, bc->instr_count); + { + int ciMsg = bytecode_add_constant(bc, make_string("TypeError: expected Number")); + bytecode_add_instruction(bc, OP_LOAD_CONST, ciMsg); + bytecode_add_instruction(bc, OP_PRINT, 0); + bytecode_add_instruction(bc, OP_HALT, 0); + } + bytecode_set_operand(bc, j_skip_err, bc->instr_count); + + bytecode_add_instruction(bc, (meta < 0) ? OP_SCLAMP : OP_UCLAMP, abs_bits); } + /* dynamic (meta==0): no enforcement */ + if (lidx >= 0) { bytecode_add_instruction(bc, OP_STORE_LOCAL, lidx); } else {