diff --git a/Readme.org b/Readme.org index 12460aa..38b6e47 100644 --- a/Readme.org +++ b/Readme.org @@ -15,7 +15,7 @@ Now what do I mean by this? One of the outcomes that is-- nearly-- impossible to *** In minecraft datapack `mcfunction` code /Concatenate output/ -=cat_and_say.mcfunction= +=cat_and_say.mdlunction= #+begin_src #inputs : # A : Some literal @@ -28,7 +28,7 @@ And then the calling code /Call Code/ -=main.mcfunction= +=main.mdlunction= #+begin_src data modify storage example:main cat_and_say_args set value {A:"Hello,",B:" World!"} function example:cat_and_say with storage example:main cat_and_say_args @@ -38,15 +38,15 @@ Which then will output =Hello, World!= and store it to =example:utils cat_output *** In mdl In a direct translation this could be #+begin_src -// utils.mcf +// utils.mdl data cat_output := ""; fn cat_and_say(eff A: String, eff B: String) : Void { utils.cat_output = ""; $say ""; } -// main.mcf -namespace utils = "utils.mcf' +// main.mdl +namespace utils = "utils.mdl' utils/cat_and_say("Hello,"," World!"); #+end_src @@ -89,14 +89,14 @@ This is my first time ever using the V programming language, it has been interes * Notes ** Namespaces #+begin_src -// a.mcf +// a.mdl namespace a; -namespace b = "b.mcf"; +namespace b = "b.mdl"; data x := 10; -// b.mcf +// b.mdl namespace b; -namespace a = "a.mcf"; +namespace a = "a.mdl"; fn addx(data y: Int) : Int { return y + a.x; @@ -110,7 +110,7 @@ fn add2x(data y: Int) : Int{ Should ultimately result in #+begin_src -//b.mcf +//b.mdl fn addx(data y : Int) : Int{ return y + a.x; diff --git a/codegen.v b/codegen.v new file mode 100644 index 0000000..98e76f8 --- /dev/null +++ b/codegen.v @@ -0,0 +1,1117 @@ +module main + +import os + +// Codegen - Generates .mcfunction files from IR +// +// === Storage Layout === +// Data variables (storage): +// - Function args: storage : .$args. +// - Function result: storage : .$result +// - Data variables: storage : . +// +// Register variables (scoreboard) - integers only: +// - Objective: +// - Player: __ +// +// === File Structure === +// Super nested structure: +// ns/func.mcfunction - Entry point (calls entry block) +// ns/func/entry_0.mcfunction - Entry basic block +// ns/func/if_then_1.mcfunction - Then branch +// ns/func/if_else_2.mcfunction - Else branch +// etc. +// +// === Macro Passing === +// Basic blocks that need macro values are called with: +// function : with storage : .$macro_args +// or with immediate dict: +// function : with {key: value, ...} +// Inside the function, $(key) expands to the value. +// +// Reference macros (&var) are inserted raw, non-ref macros use $(). + +pub struct Codegen { +pub mut: + builder &IRBuilder + output_dir string + project_name string // Scoreboard objective name +} + +pub fn Codegen.new(builder &IRBuilder, output_dir string, project_name string) Codegen { + return Codegen{ + builder: builder + output_dir: output_dir + project_name: project_name + } +} + +// Generate all mcfunction files +pub fn (mut c Codegen) generate() ! { + // Create base output directory + os.mkdir_all(c.output_dir) or { + return error('Could not create output directory: ${c.output_dir}') + } + + // Create pack.mcmeta + c.write_pack_mcmeta()! + + // Generate init function for scoreboards + c.generate_init_function()! + + // Generate functions for each namespace + for ns in c.builder.namespaces { + c.generate_namespace(ns)! + } +} + +// Write pack.mcmeta file +fn (mut c Codegen) write_pack_mcmeta() ! { + mcmeta := '{ + "pack": { + "pack_format": 48, + "description": "Generated by MDL compiler" + } +}' + os.write_file(os.join_path(c.output_dir, 'pack.mcmeta'), mcmeta)! +} + +// Generate init function that creates scoreboards +fn (mut c Codegen) generate_init_function() ! { + // Init function goes in output/data//function/ + init_path := os.join_path(c.output_dir, 'data', c.project_name, 'function') + os.mkdir_all(init_path)! + + mut lines := []string{} + lines << '# Initialize scoreboards for ${c.project_name}' + lines << 'scoreboard objectives add ${c.project_name} dummy' + + os.write_file(os.join_path(init_path, 'init.mcfunction'), lines.join('\n'))! +} + +// Generate mcfunction files for a namespace +pub fn (mut c Codegen) generate_namespace(ns IRNamespace) ! { + // Base path: output/data//function// + ns_parts := c.get_ns_parts(ns) + ns_path := os.join_path(c.output_dir, 'data', c.project_name, 'function', ns_parts.join('/')) + os.mkdir_all(ns_path)! + + for fid in ns.functions { + c.generate_function(fid, ns, ns_path)! + } +} + +// Generate mcfunction files for a function +pub fn (mut c Codegen) generate_function(fid FID, ns IRNamespace, ns_path string) ! { + func := c.builder.functions[fid] + + // Create function subdirectory for basic blocks + func_dir := os.join_path(ns_path, func.name) + os.mkdir_all(func_dir)! + + // Generate entry point: ns/func.mcfunction + // This sets up args and calls the entry block + c.generate_function_entry(fid, ns, ns_path)! + + // Generate each basic block: ns/func/block_id.mcfunction + for bbid in func.bbs { + c.generate_basic_block(bbid, ns, func_dir)! + } +} + +// Generate function entry point that calls the entry block +fn (mut c Codegen) generate_function_entry(fid FID, ns IRNamespace, fs_path string) ! { + func := c.builder.functions[fid] + mut lines := []string{} + + ns_parts := c.get_ns_parts(ns) + lines << '# Entry point for ${ns_parts.join(".")}.${func.name}' + + // Call the entry basic block + entry_bb := c.builder.basic_blocks[func.entrybb] + bb_func_path := c.build_func_path(ns, func.name, '${entry_bb.label}_${entry_bb.id}') + + if entry_bb.args.len > 0 { + // Need to pass macro arguments + args_storage := c.build_storage_path(ns, func.name, '\$args') + lines << 'function ${bb_func_path} with storage ${args_storage}' + } else { + lines << 'function ${bb_func_path}' + } + + filepath := os.join_path(fs_path, '${func.name}.mcfunction') + os.write_file(filepath, lines.join('\n'))! +} + +// Generate a single .mcfunction file for a basic block +pub fn (mut c Codegen) generate_basic_block(bbid BBID, ns IRNamespace, func_dir string) ! { + bb := c.builder.basic_blocks[bbid] + + filename := '${bb.label}_${bb.id}.mcfunction' + filepath := os.join_path(func_dir, filename) + + mut lines := []string{} + lines << '# Basic block: ${bb.label} (id: ${bb.id})' + + // Generate commands for each instruction + for iid in bb.insts { + inst_lines := c.generate_instruction(bb.function, bbid, iid, ns) + lines << inst_lines + } + + os.write_file(filepath, lines.join('\n'))! +} + +// Generate mcfunction commands for a single instruction +pub fn (mut c Codegen) generate_instruction(fid FID, bbid BBID, iid IID, ns IRNamespace) []string { + func := c.builder.functions[fid] + inst := func.insts[iid] + + mut lines := []string{} + + match inst { + IRDefine { + lines << c.gen_define(fid, bbid, inst, ns) + } + IRTypedDefine { + lines << c.gen_typed_define(fid, bbid, inst, ns) + } + IRAssign { + lines << c.gen_assign(fid, bbid, inst, ns) + } + IRStore { + lines << c.gen_store(fid, bbid, inst, ns) + } + IRBinaryOp { + lines << c.gen_binary_op(fid, bbid, inst, ns) + } + IRUnaryOp { + lines << c.gen_unary_op(fid, bbid, inst, ns) + } + IRCall { + lines << c.gen_call(fid, bbid, inst, ns) + } + IRJump { + lines << c.gen_jump(fid, bbid, inst, ns) + } + IRBranch { + lines << c.gen_branch(fid, bbid, inst, ns) + } + IRReturn { + lines << c.gen_return(fid, bbid, inst, ns) + } + IRMacroLiteralCmd { + lines << c.gen_macro_cmd(fid, bbid, inst, ns) + } + IRFieldAccess { + lines << c.gen_field_access(fid, bbid, inst, ns) + } + IRIndexAccess { + lines << c.gen_index_access(fid, bbid, inst, ns) + } + IRDeref { + lines << c.gen_deref(fid, bbid, inst, ns) + } + IRRefInst { + lines << c.gen_ref(fid, bbid, inst, ns) + } + IRStructInit { + lines << c.gen_struct_init(fid, bbid, inst, ns) + } + } + + return lines +} + +// ==================== Instruction Generators ==================== + +fn (mut c Codegen) gen_define(fid FID, bbid BBID, inst IRDefine, ns IRNamespace) []string { + mut lines := []string{} + + // Get storage kinds for dest and source + dest_kind := c.get_ref_storage(fid, inst.result) + src_kind := c.get_oid_storage(fid, inst.value) + + match dest_kind { + .register { + dest_player := c.scoreboard_player(fid, inst.result) + match src_kind { + .register { + // register <- register: use operation = + src_player := c.oid_to_score_player(fid, bbid, inst.value, ns) + lines << 'scoreboard players operation ${dest_player} ${c.project_name} = ${src_player} ${c.project_name}' + } + .data { + // register <- data: execute store result score ... run data get + src_path := c.oid_to_storage_path(fid, inst.value, ns) + lines << 'execute store result score ${dest_player} ${c.project_name} run data get storage ${src_path}' + } + .effemeral { + // register <- constant: scoreboard players set + value_cmd := c.oid_to_value_cmd(fid, bbid, inst.value, ns) + lines << 'scoreboard players set ${dest_player} ${c.project_name} ${value_cmd}' + } + } + } + .data { + dest_path := c.storage_path_rid(fid, inst.result, ns) + match src_kind { + .register { + // data <- register: execute store result storage ... run scoreboard players get + src_player := c.oid_to_score_player(fid, bbid, inst.value, ns) + lines << 'execute store result storage ${dest_path} int 1 run scoreboard players get ${src_player} ${c.project_name}' + } + .data { + // data <- data: data modify ... set from storage + value_cmd := c.oid_to_storage_cmd(fid, bbid, inst.value, ns) + lines << 'data modify storage ${dest_path} set ${value_cmd}' + } + .effemeral { + // data <- constant: data modify ... set value + value_cmd := c.oid_to_storage_cmd(fid, bbid, inst.value, ns) + lines << 'data modify storage ${dest_path} set ${value_cmd}' + } + } + } + .effemeral { + lines << '# ephemeral define (no storage)' + } + } + + return lines +} + +fn (mut c Codegen) gen_typed_define(fid FID, bbid BBID, inst IRTypedDefine, ns IRNamespace) []string { + // Typed define just declares, doesn't initialize + return ['# typed define (declaration only)'] +} + +fn (mut c Codegen) gen_assign(fid FID, bbid BBID, inst IRAssign, ns IRNamespace) []string { + mut lines := []string{} + + dest_kind := c.get_ref_storage(fid, inst.result) + src_kind := c.get_oid_storage(fid, inst.value) + + match dest_kind { + .register { + dest_player := c.scoreboard_player(fid, inst.result) + match inst.op { + .assign { + // Handle storage kind conversion for simple assignment + match src_kind { + .register { + src_player := c.oid_to_score_player(fid, bbid, inst.value, ns) + lines << 'scoreboard players operation ${dest_player} ${c.project_name} = ${src_player} ${c.project_name}' + } + .data { + src_path := c.oid_to_storage_path(fid, inst.value, ns) + lines << 'execute store result score ${dest_player} ${c.project_name} run data get storage ${src_path}' + } + .effemeral { + value_cmd := c.oid_to_value_cmd(fid, bbid, inst.value, ns) + lines << 'scoreboard players set ${dest_player} ${c.project_name} ${value_cmd}' + } + } + } + .add_assign { + value_cmd := c.oid_to_value_cmd(fid, bbid, inst.value, ns) + lines << 'scoreboard players add ${dest_player} ${c.project_name} ${value_cmd}' + } + .sub_assign { + value_cmd := c.oid_to_value_cmd(fid, bbid, inst.value, ns) + lines << 'scoreboard players remove ${dest_player} ${c.project_name} ${value_cmd}' + } + .mul_assign, .div_assign, .mod_assign { + other := c.oid_to_score_player(fid, bbid, inst.value, ns) + op_str := match inst.op { + .mul_assign { '*=' } + .div_assign { '/=' } + .mod_assign { '%=' } + else { '=' } + } + lines << 'scoreboard players operation ${dest_player} ${c.project_name} ${op_str} ${other} ${c.project_name}' + } + .swap { + other := c.oid_to_score_player(fid, bbid, inst.value, ns) + lines << 'scoreboard players operation ${dest_player} ${c.project_name} >< ${other} ${c.project_name}' + } + } + } + .data { + dest_path := c.storage_path_rid(fid, inst.result, ns) + match inst.op { + .assign { + // Handle storage kind conversion for simple assignment + match src_kind { + .register { + src_player := c.oid_to_score_player(fid, bbid, inst.value, ns) + lines << 'execute store result storage ${dest_path} int 1 run scoreboard players get ${src_player} ${c.project_name}' + } + .data, .effemeral { + value_cmd := c.oid_to_storage_cmd(fid, bbid, inst.value, ns) + lines << 'data modify storage ${dest_path} set ${value_cmd}' + } + } + } + else { + // Compound assignments on data require loading to scoreboard first + // This should ideally be handled by an IR pass + lines << '# TODO: compound assignment on data storage requires scoreboard temp' + } + } + } + .effemeral { + lines << '# ephemeral assign' + } + } + + return lines +} + +fn (mut c Codegen) gen_store(fid FID, bbid BBID, inst IRStore, ns IRNamespace) []string { + // Store copies from source to result (both are RIDs) + mut lines := []string{} + dest_path := c.storage_path_rid(fid, inst.result, ns) + src_path := c.storage_path_rid(fid, inst.source, ns) + lines << 'data modify storage ${dest_path} set from storage ${src_path}' + return lines +} + +fn (mut c Codegen) gen_binary_op(fid FID, bbid BBID, inst IRBinaryOp, ns IRNamespace) []string { + mut lines := []string{} + func := c.builder.functions[fid] + + // Binary ops on integers use scoreboard operations + result_player := c.scoreboard_player(fid, inst.result) + + // Get storage kinds for operands + left_kind := c.get_oid_storage(fid, inst.left) + right_kind := c.get_oid_storage(fid, inst.right) + + // Handle left operand - load into scoreboard if needed + left_player := c.oid_to_score_player(fid, bbid, inst.left, ns) + match left_kind { + .data { + // Load from storage into scoreboard + left_path := c.oid_to_storage_path(fid, inst.left, ns) + lines << 'execute store result score ${left_player} ${c.project_name} run data get storage ${left_path}' + } + .effemeral { + // Constant - set directly + if inst.left is CID { + cnst := func.consts[inst.left] + if cnst is IRIntConst { + lines << 'scoreboard players set ${left_player} ${c.project_name} ${cnst.value}' + } + } + } + .register { + // Already in scoreboard, nothing to do + } + } + + // Handle right operand - load into scoreboard if needed + right_player := c.oid_to_score_player(fid, bbid, inst.right, ns) + match right_kind { + .data { + // Load from storage into scoreboard + right_path := c.oid_to_storage_path(fid, inst.right, ns) + lines << 'execute store result score ${right_player} ${c.project_name} run data get storage ${right_path}' + } + .effemeral { + // Constant - set directly + if inst.right is CID { + cnst := func.consts[inst.right] + if cnst is IRIntConst { + lines << 'scoreboard players set ${right_player} ${c.project_name} ${cnst.value}' + } + } + } + .register { + // Already in scoreboard, nothing to do + } + } + + // First, copy left to result + lines << 'scoreboard players operation ${result_player} ${c.project_name} = ${left_player} ${c.project_name}' + + // Then apply operation with right + op_str := match inst.op { + .add { '+=' } + .sub { '-=' } + .mul { '*=' } + .div { '/=' } + .mod { '%=' } + .lt { '<' } + .le { '<=' } + .gt { '>' } + .ge { '>=' } + .eq { '=' } + .ne { '=' } // Will need special handling + } + + match inst.op { + .add, .sub, .mul, .div, .mod { + lines << 'scoreboard players operation ${result_player} ${c.project_name} ${op_str} ${right_player} ${c.project_name}' + } + .lt, .le, .gt, .ge, .eq, .ne { + // Comparison - set result to 1 if true, 0 if false + lines << 'scoreboard players set ${result_player} ${c.project_name} 0' + cmp_op := match inst.op { + .lt { '<' } + .le { '<=' } + .gt { '>' } + .ge { '>=' } + .eq { '=' } + .ne { '=' } + else { '=' } + } + if inst.op == .ne { + lines << 'execute unless score ${left_player} ${c.project_name} = ${right_player} ${c.project_name} run scoreboard players set ${result_player} ${c.project_name} 1' + } else { + lines << 'execute if score ${left_player} ${c.project_name} ${cmp_op} ${right_player} ${c.project_name} run scoreboard players set ${result_player} ${c.project_name} 1' + } + } + } + + return lines +} + +fn (mut c Codegen) gen_unary_op(fid FID, bbid BBID, inst IRUnaryOp, ns IRNamespace) []string { + mut lines := []string{} + + result_player := c.scoreboard_player(fid, inst.result) + operand_player := c.oid_to_score_player(fid, bbid, inst.operand, ns) + + match inst.op { + .neg { + // Negate: result = 0 - operand + lines << 'scoreboard players set ${result_player} ${c.project_name} 0' + lines << 'scoreboard players operation ${result_player} ${c.project_name} -= ${operand_player} ${c.project_name}' + } + else { + lines << '# unary op not applicable for scoreboard' + } + } + + return lines +} + +fn (mut c Codegen) gen_call(fid FID, bbid BBID, inst IRCall, ns IRNamespace) []string { + mut lines := []string{} + + called_func := c.builder.functions[inst.function] + called_ns := c.builder.namespaces[called_func.namespace] + + func_path := c.build_func_path(called_ns, called_func.name) + + if inst.args.len == 0 { + // No arguments + lines << 'function ${func_path}' + } else { + // Check if all args are constants (can inline) + mut all_constants := true + for arg_oid in inst.args { + if arg_oid is RID { + all_constants = false + break + } + } + + if all_constants { + // All constants - use inline dict (no 'with' keyword) + mut dict_parts := []string{} + for i, arg_oid in inst.args { + arg_def := called_func.args[i] + value := c.oid_to_nbt_value(fid, bbid, arg_oid, ns) + dict_parts << '${arg_def.name}: ${value}' + } + lines << 'function ${func_path} {${dict_parts.join(", ")}}' + } else { + // Has variables - need to copy to storage first, then call with storage + args_storage := c.build_storage_path(called_ns, called_func.name, '\$args') + + for i, arg_oid in inst.args { + arg_def := called_func.args[i] + arg_path := '${args_storage}.${arg_def.name}' + + match arg_oid { + CID { + // Constant - set value directly + value_cmd := c.oid_to_storage_cmd(fid, bbid, arg_oid, ns) + lines << 'data modify storage ${arg_path} set ${value_cmd}' + } + RID { + // Variable - check if register or data + storage_kind := c.get_ref_storage(fid, arg_oid) + match storage_kind { + .register { + // Register: execute store result storage ... run scoreboard players get + player := c.scoreboard_player(fid, arg_oid) + lines << 'execute store result storage ${arg_path} int 1 run scoreboard players get ${player} ${c.project_name}' + } + .data { + // Data: data modify storage ... set from storage + src_path := c.storage_path_rid(fid, arg_oid, ns) + lines << 'data modify storage ${arg_path} set from storage ${src_path}' + } + .effemeral { + // Ephemeral - shouldn't happen for RID + lines << '# ephemeral arg (unexpected)' + } + } + } + } + } + + lines << 'function ${func_path} with storage ${args_storage}' + } + } + + // Copy result if needed + if result_rid := inst.result { + result_path := c.storage_path_rid(fid, result_rid, ns) + called_result := c.build_storage_path(called_ns, called_func.name, '\$result') + lines << 'data modify storage ${result_path} set from storage ${called_result}' + } + + return lines +} + +// Check if an OID can be inlined directly (constants only, not variables) +fn (mut c Codegen) can_inline_oid(fid FID, oid OID) bool { + func := c.builder.functions[fid] + match oid { + CID { + cnst := func.consts[oid] + // Can inline simple constants + return cnst is IRIntConst || cnst is IRFloatConst || + cnst is IRCharConst || cnst is IRStringConst + } + RID { + // RIDs are variables - cannot inline, need storage + return false + } + } +} + +fn (mut c Codegen) gen_jump(fid FID, bbid BBID, inst IRJump, ns IRNamespace) []string { + mut lines := []string{} + + func := c.builder.functions[fid] + target_bb := c.builder.basic_blocks[inst.target] + target_path := c.build_func_path(ns, func.name, '${target_bb.label}_${target_bb.id}') + + if inst.args.len == 0 { + lines << 'function ${target_path}' + return lines + } + + // Check if all args are constants (can inline) + mut all_constants := true + for arg_oid in inst.args { + if arg_oid is RID { + all_constants = false + break + } + } + + if all_constants { + // All constants - use inline dict (no 'with' keyword) + mut dict_parts := []string{} + for i, arg_oid in inst.args { + arg_name := target_bb.args[i].name + value := c.oid_to_nbt_value(fid, bbid, arg_oid, ns) + dict_parts << '${arg_name}: ${value}' + } + lines << 'function ${target_path} {${dict_parts.join(", ")}}' + } else { + // Has variables - need to copy to storage first, then call with storage + target_name := '${target_bb.label}_${target_bb.id}' + args_storage := c.build_storage_path(ns, func.name, '\$${target_name}', '\$args') + + for i, arg_oid in inst.args { + arg_name := target_bb.args[i].name + arg_path := '${args_storage}.${arg_name}' + + match arg_oid { + CID { + // Constant - set value directly + value_cmd := c.oid_to_storage_cmd(fid, bbid, arg_oid, ns) + lines << 'data modify storage ${arg_path} set ${value_cmd}' + } + RID { + // Variable - check if register or data + storage_kind := c.get_ref_storage(fid, arg_oid) + match storage_kind { + .register { + // Register: execute store result storage ... run scoreboard players get + player := c.scoreboard_player(fid, arg_oid) + lines << 'execute store result storage ${arg_path} int 1 run scoreboard players get ${player} ${c.project_name}' + } + .data { + // Data: data modify storage ... set from storage + src_path := c.storage_path_rid(fid, arg_oid, ns) + lines << 'data modify storage ${arg_path} set from storage ${src_path}' + } + .effemeral { + // Ephemeral - shouldn't happen for RID + lines << '# ephemeral arg (unexpected)' + } + } + } + } + } + + lines << 'function ${target_path} with storage ${args_storage}' + } + + return lines +} + +fn (mut c Codegen) gen_branch(fid FID, bbid BBID, inst IRBranch, ns IRNamespace) []string { + mut lines := []string{} + + func := c.builder.functions[fid] + then_bb := c.builder.basic_blocks[inst.then_bb] + else_bb := c.builder.basic_blocks[inst.else_bb] + + then_path := c.build_func_path(ns, func.name, '${then_bb.label}_${then_bb.id}') + else_path := c.build_func_path(ns, func.name, '${else_bb.label}_${else_bb.id}') + + // Condition is a score (1 = true, 0 = false) + cond_player := c.oid_to_score_player(fid, bbid, inst.cond, ns) + + // Branch based on condition + lines << 'execute if score ${cond_player} ${c.project_name} matches 1.. run function ${then_path}' + lines << 'execute if score ${cond_player} ${c.project_name} matches ..0 run function ${else_path}' + + return lines +} + +fn (mut c Codegen) gen_return(fid FID, bbid BBID, inst IRReturn, ns IRNamespace) []string { + mut lines := []string{} + + func := c.builder.functions[fid] + + if value := inst.value { + // Copy return value to $result (which is always storage) + result_path := c.build_storage_path(ns, func.name, '\$result') + src_kind := c.get_oid_storage(fid, value) + + match src_kind { + .register { + // register -> storage: use execute store + src_player := c.oid_to_score_player(fid, bbid, value, ns) + lines << 'execute store result storage ${result_path} int 1 run scoreboard players get ${src_player} ${c.project_name}' + } + .data, .effemeral { + // data/constant -> storage: use data modify + value_cmd := c.oid_to_storage_cmd(fid, bbid, value, ns) + lines << 'data modify storage ${result_path} set ${value_cmd}' + } + } + } + + // Return is implicit (end of mcfunction) + lines << '# return' + + return lines +} + +fn (mut c Codegen) gen_macro_cmd(fid FID, bbid BBID, inst IRMacroLiteralCmd, ns IRNamespace) []string { + mut lines := []string{} + + // Build the command string with macro substitutions + mut cmd := '\$' // Start with $ for mcfunction macro command + + for part in inst.parts { + match part { + IRMacroCmdText { + cmd += part.text + } + IRMacroCmdMacro { + // Both ref and non-ref use $() substitution for macro commands + // The is_ref flag affects how the value is passed, not the syntax + name := c.get_ref_name(fid, part.value) + cmd += '\$(${name})' + } + IRMacroCmdString { + for str_part in part.parts { + match str_part { + IRStringText { + cmd += str_part.text + } + IRStringMacro { + // Both ref and non-ref use $() substitution + name := c.get_ref_name(fid, str_part.value) + cmd += '\$(${name})' + } + } + } + } + } + } + + lines << cmd + return lines +} + +fn (mut c Codegen) gen_field_access(fid FID, bbid BBID, inst IRFieldAccess, ns IRNamespace) []string { + mut lines := []string{} + src_path := c.storage_path_rid(fid, inst.source, ns) + dest_path := c.storage_path_rid(fid, inst.result, ns) + lines << 'data modify storage ${dest_path} set from storage ${src_path}.${inst.field}' + return lines +} + +fn (mut c Codegen) gen_index_access(fid FID, bbid BBID, inst IRIndexAccess, ns IRNamespace) []string { + mut lines := []string{} + src_path := c.storage_path_rid(fid, inst.source, ns) + dest_path := c.storage_path_rid(fid, inst.result, ns) + + // Index can be constant or variable + index_str := c.oid_to_index_str(fid, bbid, inst.index, ns) + lines << 'data modify storage ${dest_path} set from storage ${src_path}[${index_str}]' + return lines +} + +fn (mut c Codegen) gen_deref(fid FID, bbid BBID, inst IRDeref, ns IRNamespace) []string { + mut lines := []string{} + // Dereference - the source contains a path, copy from that path + src_path := c.storage_path_rid(fid, inst.source, ns) + dest_path := c.storage_path_rid(fid, inst.result, ns) + // This requires indirect access - use macro + lines << '# deref - indirect storage access' + lines << 'data modify storage ${dest_path} set from storage ${src_path}' + return lines +} + +fn (mut c Codegen) gen_ref(fid FID, bbid BBID, inst IRRefInst, ns IRNamespace) []string { + mut lines := []string{} + // Reference - store the path as a string + src_path := c.storage_path_rid(fid, inst.source, ns) + dest_path := c.storage_path_rid(fid, inst.result, ns) + lines << 'data modify storage ${dest_path} set value "${src_path}"' + return lines +} + +fn (mut c Codegen) gen_struct_init(fid FID, bbid BBID, inst IRStructInit, ns IRNamespace) []string { + mut lines := []string{} + dest_path := c.storage_path_rid(fid, inst.result, ns) + + // Initialize struct as compound + mut field_parts := []string{} + for field_name, field_oid in inst.field_values { + value := c.oid_to_nbt_value(fid, bbid, field_oid, ns) + field_parts << '${field_name}: ${value}' + } + lines << 'data modify storage ${dest_path} set value {${field_parts.join(", ")}}' + return lines +} + +// ==================== Helper Functions ==================== + +// Get namespace parts by traversing parent chain +// Returns array like ["root", "sub", "subsub"] (root first) +fn (mut c Codegen) get_ns_parts(ns IRNamespace) []string { + mut parts := []string{} + mut current := ns + for { + parts << current.name + if parent_nid := current.parent { + current = c.builder.namespaces[parent_nid] + } else { + break + } + } + // Reverse to get root first + mut result := []string{} + for i := parts.len - 1; i >= 0; i-- { + result << parts[i] + } + return result +} + +// Build storage path in format: : +// MC storage format is: first:second third.fourth.fifth +fn (mut c Codegen) build_storage_path(ns IRNamespace, components ...string) string { + ns_parts := c.get_ns_parts(ns) + + if ns_parts.len == 0 { + panic('Namespace has no parts') + } + + root_ns := ns_parts[0] + + // Build the dotted part: sub-namespaces + components + mut dotted := []string{} + for i := 1; i < ns_parts.len; i++ { + dotted << ns_parts[i] + } + for comp in components { + dotted << comp + } + + if dotted.len == 0 { + return '${c.project_name}:${root_ns}' + } + return '${c.project_name}:${root_ns} ${dotted.join(".")}' +} + +// Build function call path in format: : +// For function command: function example:macros/simple/command_1 +fn (mut c Codegen) build_func_path(ns IRNamespace, components ...string) string { + ns_parts := c.get_ns_parts(ns) + + // Build the slashed part: all namespace parts + components + mut slashed := []string{} + for part in ns_parts { + slashed << part + } + for comp in components { + slashed << comp + } + + if slashed.len == 0 { + return c.project_name + } + return '${c.project_name}:${slashed.join("/")}' +} + +// Get storage kind for a reference +fn (mut c Codegen) get_ref_storage(fid FID, rid RID) StorageKind { + func := c.builder.functions[fid] + ref := func.refs[rid] + + match ref.value { + VID { + return c.builder.variables[ref.value].storage + } + IRBasicBlockArg { + return ref.value.storage + } + IRFunctionArg { + return ref.value.storage + } + IID { + // Instruction results are registers by default + return .register + } + } +} + +// Get storage kind for an OID (constants are ephemeral/inline, RIDs depend on their ref) +fn (mut c Codegen) get_oid_storage(fid FID, oid OID) StorageKind { + match oid { + CID { + // Constants are inline values, not stored anywhere + return .effemeral + } + RID { + return c.get_ref_storage(fid, oid) + } + } +} + +// Get name for a reference (for macro substitution) +fn (mut c Codegen) get_ref_name(fid FID, rid RID) string { + func := c.builder.functions[fid] + ref := func.refs[rid] + + match ref.value { + VID { + return c.builder.variables[ref.value].name + } + IRBasicBlockArg { + return ref.value.name + } + IRFunctionArg { + return ref.value.name + } + IID { + return '_tmp_${ref.value}' + } + } +} + +// Get scoreboard player name for a reference +fn (mut c Codegen) scoreboard_player(fid FID, rid RID) string { + func := c.builder.functions[fid] + ns := c.builder.namespaces[func.namespace] + ns_parts := c.get_ns_parts(ns) + name := c.get_ref_name(fid, rid) + return '${ns_parts.join("_")}_${func.name}_${name}' +} + +// Get storage path for a reference +// Format: : . +// For function args: : .$args. +fn (mut c Codegen) storage_path_rid(fid FID, rid RID, ns IRNamespace) string { + func := c.builder.functions[fid] + ref := func.refs[rid] + + match ref.value { + IRFunctionArg { + // Function args come from $args storage + return c.build_storage_path(ns, func.name, '\$args', ref.value.name) + } + IRBasicBlockArg { + // BB args come from macro substitution, but their storage is in the BB's $args + return c.build_storage_path(ns, func.name, '\$args', ref.value.name) + } + VID { + return c.build_storage_path(ns, func.name, c.builder.variables[ref.value].name) + } + IID { + return c.build_storage_path(ns, func.name, '_tmp_${ref.value}') + } + } +} + +// Get storage path for an OID (only valid for RIDs with data storage) +fn (mut c Codegen) oid_to_storage_path(fid FID, oid OID, ns IRNamespace) string { + match oid { + CID { + panic('Cannot get storage path for constant') + } + RID { + return c.storage_path_rid(fid, oid, ns) + } + } +} + +// Convert OID to a value for scoreboard set command +fn (mut c Codegen) oid_to_value_cmd(fid FID, bbid BBID, oid OID, ns IRNamespace) string { + func := c.builder.functions[fid] + match oid { + CID { + cnst := func.consts[oid] + match cnst { + IRIntConst { + return '${cnst.value}' + } + else { + return '0' + } + } + } + RID { + // For scoreboard set, we can't use another score directly + // This should be handled by caller + return '0' + } + } +} + +// Convert OID to storage command (value or from) +fn (mut c Codegen) oid_to_storage_cmd(fid FID, bbid BBID, oid OID, ns IRNamespace) string { + func := c.builder.functions[fid] + match oid { + CID { + cnst := func.consts[oid] + match cnst { + IRIntConst { + return 'value ${cnst.value}' + } + IRFloatConst { + return 'value ${cnst.value}d' + } + IRCharConst { + return 'value "${cnst.value}"' + } + IRStringConst { + mut s := '' + for part in cnst.parts { + match part { + IRStringText { s += strip_quotes(part.text) } + IRStringMacro { s += '\$(${c.get_ref_name(fid, part.value)})' } + } + } + return 'value "${s}"' + } + else { + return 'value {}' + } + } + } + RID { + path := c.storage_path_rid(fid, oid, ns) + return 'from storage ${path}' + } + } +} + +// Convert OID to score player for operations +fn (mut c Codegen) oid_to_score_player(fid FID, bbid BBID, oid OID, ns IRNamespace) string { + match oid { + CID { + // Constants need to be loaded into a temp score first + return '#const ${c.project_name}' + } + RID { + return c.scoreboard_player(fid, oid) + } + } +} + +// Convert OID to NBT value for inline use +fn (mut c Codegen) oid_to_nbt_value(fid FID, bbid BBID, oid OID, ns IRNamespace) string { + func := c.builder.functions[fid] + match oid { + CID { + cnst := func.consts[oid] + match cnst { + IRIntConst { return '${cnst.value}' } + IRFloatConst { return '${cnst.value}d' } + IRCharConst { return '"${cnst.value}"' } + IRStringConst { + mut s := '' + for part in cnst.parts { + match part { + IRStringText { s += strip_quotes(part.text) } + IRStringMacro { s += '\$(${c.get_ref_name(fid, part.value)})' } + } + } + return '"${s}"' + } + else { return '{}' } + } + } + RID { + // Can't inline RID directly, would need macro + return '\$(${c.get_ref_name(fid, oid)})' + } + } +} + +// Convert OID to index string for array access +fn (mut c Codegen) oid_to_index_str(fid FID, bbid BBID, oid OID, ns IRNamespace) string { + func := c.builder.functions[fid] + match oid { + CID { + cnst := func.consts[oid] + match cnst { + IRIntConst { return '${cnst.value}' } + else { return '0' } + } + } + RID { + return '\$(${c.get_ref_name(fid, oid)})' + } + } +} + +// Get raw value for reference macro +fn (mut c Codegen) rid_to_macro_ref(fid FID, rid RID, ns IRNamespace) string { + // For &ref, we want the actual storage path + return c.storage_path_rid(fid, rid, ns) +} + +// Strip surrounding quotes from a string if present +fn strip_quotes(s string) string { + if s.len >= 2 && s[0] == `"` && s[s.len - 1] == `"` { + return s[1..s.len - 1] + } + return s +} diff --git a/errors.v b/errors.v new file mode 100644 index 0000000..0fb8fd9 --- /dev/null +++ b/errors.v @@ -0,0 +1,419 @@ +module main + +import os + +// ==================== Source Location ==================== + +pub struct SourceLocation { +pub: + file string + pos int // Byte offset + len int // Span length (for highlighting) +} + +pub fn (loc SourceLocation) is_valid() bool { + return loc.file.len > 0 && loc.pos >= 0 +} + +pub fn empty_location() SourceLocation { + return SourceLocation{ + file: '' + pos: 0 + len: 0 + } +} + +// ==================== Error Severity ==================== + +pub enum Severity { + error // Compilation fails + warning // Compilation continues, user should be aware + hint // Suggestion for improvement +} + +// ==================== Error Kinds ==================== + +pub enum ErrorKind { + // Lexer errors + invalid_character + unterminated_string + invalid_escape_sequence + invalid_number + // Parser errors + unexpected_token + expected_identifier + expected_type + expected_expression + expected_statement + malformed_struct + malformed_function + malformed_namespace + unclosed_delimiter + // Type errors + type_mismatch + invalid_operation + // Definition errors + undefined_variable + undefined_function + undefined_struct + undefined_field + undefined_namespace + // Declaration errors + duplicate_definition + shadowing_not_allowed + // Reference errors + invalid_reference + invalid_dereference + cannot_take_reference_of_constant + // Structure errors + missing_struct_field + unknown_struct_field + // Function errors + wrong_argument_count + wrong_argument_type + missing_return + invalid_return_type + // Namespace errors + namespace_collision + namespace_not_found + circular_import + // IR lowering errors + unsupported_statement_location + invalid_basic_block + empty_macro_chain + cannot_resolve_type + // File errors + file_not_found + file_read_error + // Internal errors + internal_compiler_error + not_implemented +} + +// ==================== Diagnostic Labels ==================== + +pub struct Label { +pub: + location SourceLocation + message string +} + +// ==================== Diagnostic ==================== + +pub struct Diagnostic { +pub: + kind ErrorKind + severity Severity + location SourceLocation + message string + hint string // Optional suggestion + labels []Label // Secondary spans + notes []string // Additional context +} + +// ==================== ANSI Colors ==================== + +struct Colors { + reset string = '\x1b[0m' + bold string = '\x1b[1m' + dim string = '\x1b[2m' + red string = '\x1b[91m' + yellow string = '\x1b[93m' + cyan string = '\x1b[96m' + blue string = '\x1b[94m' + white string = '\x1b[97m' + magenta string = '\x1b[95m' +} + +fn no_colors() Colors { + return Colors{ + reset: '' + bold: '' + dim: '' + red: '' + yellow: '' + cyan: '' + blue: '' + white: '' + magenta: '' + } +} + +// ==================== Error Manager ==================== + +@[heap] +pub struct ErrorManager { +pub mut: + diagnostics []Diagnostic + source_map map[string]string // file path -> source content + error_count int + warning_count int + hint_count int + // Configuration + max_errors int = 50 // Stop after this many errors + colored bool = true // Use ANSI colors + show_hints bool = true // Show hint-level diagnostics +} + +pub fn ErrorManager.new() ErrorManager { + return ErrorManager{ + diagnostics: []Diagnostic{} + source_map: map[string]string{} + } +} + +// Load source file content +pub fn (mut em ErrorManager) load_source(file string) { + if file !in em.source_map { + em.source_map[file] = os.read_file(file) or { '' } + } +} + +// Check if we should stop compilation +pub fn (em &ErrorManager) should_stop() bool { + return em.error_count >= em.max_errors +} + +pub fn (em &ErrorManager) has_errors() bool { + return em.error_count > 0 +} + +fn (em &ErrorManager) colors() Colors { + if em.colored { + return Colors{} + } + return no_colors() +} + +// ==================== Error Reporting Methods ==================== + +pub fn (mut em ErrorManager) report(severity Severity, kind ErrorKind, loc SourceLocation, msg string) { + em.add_diagnostic(severity, kind, loc, msg, '', [], []) +} + +pub fn (mut em ErrorManager) error(kind ErrorKind, loc SourceLocation, msg string) { + em.add_diagnostic(.error, kind, loc, msg, '', [], []) +} + +pub fn (mut em ErrorManager) error_with_hint(kind ErrorKind, loc SourceLocation, msg string, hint string) { + em.add_diagnostic(.error, kind, loc, msg, hint, [], []) +} + +pub fn (mut em ErrorManager) error_with_labels(kind ErrorKind, loc SourceLocation, msg string, labels []Label) { + em.add_diagnostic(.error, kind, loc, msg, '', labels, []) +} + +pub fn (mut em ErrorManager) error_full(kind ErrorKind, loc SourceLocation, msg string, hint string, labels []Label, notes []string) { + em.add_diagnostic(.error, kind, loc, msg, hint, labels, notes) +} + +pub fn (mut em ErrorManager) warning(kind ErrorKind, loc SourceLocation, msg string) { + em.add_diagnostic(.warning, kind, loc, msg, '', [], []) +} + +pub fn (mut em ErrorManager) warning_with_hint(kind ErrorKind, loc SourceLocation, msg string, hint string) { + em.add_diagnostic(.warning, kind, loc, msg, hint, [], []) +} + +pub fn (mut em ErrorManager) hint_msg(kind ErrorKind, loc SourceLocation, msg string) { + em.add_diagnostic(.hint, kind, loc, msg, '', [], []) +} + +fn (mut em ErrorManager) add_diagnostic(severity Severity, kind ErrorKind, loc SourceLocation, msg string, hint string, labels []Label, notes []string) { + match severity { + .error { em.error_count++ } + .warning { em.warning_count++ } + .hint { em.hint_count++ } + } + + em.diagnostics << Diagnostic{ + kind: kind + severity: severity + location: loc + message: msg + hint: hint + labels: labels + notes: notes + } +} + +// ==================== Location Utilities ==================== + +// Convert byte position to line and column (1-indexed) +pub fn (em &ErrorManager) pos_to_location(file string, pos int) (int, int) { + src := em.source_map[file] or { return 1, 1 } + mut line := 1 + mut col := 1 + for i := 0; i < pos && i < src.len; i++ { + if src[i] == `\n` { + line++ + col = 1 + } else { + col++ + } + } + return line, col +} + +// Get the source line containing a position +fn (em &ErrorManager) get_source_line(file string, pos int) (string, int) { + src := em.source_map[file] or { return '', 0 } + if pos >= src.len { + return '', 0 + } + + // Find start of line + mut start := pos + for start > 0 && src[start - 1] != `\n` { + start-- + } + + // Find end of line + mut end := pos + for end < src.len && src[end] != `\n` { + end++ + } + + return src[start..end], start +} + +// ==================== Pretty Printing ==================== + +pub fn (em &ErrorManager) print_all() { + for diag in em.diagnostics { + if diag.severity == .hint && !em.show_hints { + continue + } + em.print_diagnostic(diag) + } + em.print_summary() +} + +fn (em &ErrorManager) print_diagnostic(diag Diagnostic) { + c := em.colors() + + // Get line and column + mut line := 1 + mut col := 1 + if diag.location.is_valid() { + line, col = em.pos_to_location(diag.location.file, diag.location.pos) + } + + // Header line: error[kind]: message + severity_str, severity_color := match diag.severity { + .error { 'error', c.red } + .warning { 'warning', c.yellow } + .hint { 'hint', c.cyan } + } + + println('${c.bold}${severity_color}${severity_str}${c.reset}${c.bold}[${diag.kind}]${c.reset}: ${diag.message}') + + // Location line: --> file:line:col + if diag.location.is_valid() { + println(' ${c.blue}-->${c.reset} ${diag.location.file}:${line}:${col}') + + // Source context + em.print_source_context(diag, line, col, severity_color) + } + + // Print labels (secondary spans) + for label in diag.labels { + if label.location.is_valid() { + l_line, l_col := em.pos_to_location(label.location.file, label.location.pos) + println(' ${c.blue}:${c.reset}') + println(' ${c.blue}= note${c.reset}: ${label.message}') + println(' ${c.blue}-->${c.reset} ${label.location.file}:${l_line}:${l_col}') + + // Print source context for label + em.print_label_context(label, l_line, l_col) + } + } + + // Print hint + if diag.hint.len > 0 { + println(' ${c.blue}=${c.reset} ${c.cyan}hint${c.reset}: ${diag.hint}') + } + + // Print notes + for note in diag.notes { + println(' ${c.blue}= note${c.reset}: ${note}') + } + + println('') +} + +fn (em &ErrorManager) print_source_context(diag Diagnostic, line int, col int, severity_color string) { + c := em.colors() + src_line, _ := em.get_source_line(diag.location.file, diag.location.pos) + if src_line.len == 0 { + return + } + + line_num_width := max_int('${line}'.len, 3) + + // Empty line with gutter + gutter := ' '.repeat(line_num_width) + println('${c.blue}${gutter} |${c.reset}') + + // Source line with line number + line_str := str_rjust('${line}', line_num_width) + println('${c.blue}${line_str} |${c.reset} ${src_line}') + + // Underline with carets + span_len := if diag.location.len > 0 { diag.location.len } else { 1 } + padding := ' '.repeat(col - 1) + underline := '^'.repeat(span_len) + println('${c.blue}${gutter} |${c.reset} ${padding}${severity_color}${underline}${c.reset}') +} + +fn (em &ErrorManager) print_label_context(label Label, line int, col int) { + c := em.colors() + src_line, _ := em.get_source_line(label.location.file, label.location.pos) + if src_line.len == 0 { + return + } + + line_num_width := max_int('${line}'.len, 3) + gutter := ' '.repeat(line_num_width) + + println('${c.blue}${gutter} |${c.reset}') + line_str := str_rjust('${line}', line_num_width) + println('${c.blue}${line_str} |${c.reset} ${src_line}') + + span_len := if label.location.len > 0 { label.location.len } else { 1 } + padding := ' '.repeat(col - 1) + underline := '-'.repeat(span_len) + println('${c.blue}${gutter} |${c.reset} ${padding}${c.blue}${underline}${c.reset}') +} + +fn (em &ErrorManager) print_summary() { + c := em.colors() + if em.error_count > 0 { + mut parts := []string{} + if em.error_count > 0 { + s := if em.error_count == 1 { '' } else { 's' } + parts << '${em.error_count} error${s}' + } + if em.warning_count > 0 { + s := if em.warning_count == 1 { '' } else { 's' } + parts << '${em.warning_count} warning${s}' + } + println('${c.bold}${c.red}error${c.reset}: could not compile due to ${parts.join(', ')}') + } else if em.warning_count > 0 { + s := if em.warning_count == 1 { '' } else { 's' } + println('${c.bold}${c.yellow}warning${c.reset}: ${em.warning_count} warning${s} generated') + } +} + +// ==================== Helpers ==================== + +fn max_int(a int, b int) int { + return if a > b { a } else { b } +} + +fn str_rjust(s string, width int) string { + if s.len >= width { + return s + } + return ' '.repeat(width - s.len) + s +} diff --git a/example.mcf b/example.mcf deleted file mode 100644 index a0f9b7a..0000000 --- a/example.mcf +++ /dev/null @@ -1,40 +0,0 @@ -// file1.mcf -namespace string = { - data ascii_lut := ['\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', '\t', '\n', '\x0b', '\x0c', '\r', '\x0e', '\x0f', '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1a', '\x1b', '\x1c', '\x1d', '\x1e', '\x1f', ' ', '!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', '0',... etc ]; - reg counter := 0; - - fn cat(eff a: String,eff b:String) : String { - counter += 1; - return ""; - } - - // effemeral values cannot have identification chains, they only exist for the lifetime of their block. as such they are soely an identifier for a macro - fn lookup_ascii(reg index: Int, data dest: &String) : Void { - eff idx := ; - = ascii_lut[idx]; - } -} - -data output := ""; - -fn add_token(reg id: Int):Void{ - data new_token := ""; - string/lookup_ascii(id, &new_token); - string/cat(output,new_token); -} - -fn get_counter() : Int{ - return string.counter; -} - - -// file2.mcf -namespace dinosaur; -namespace work = "file1.mcf" - -reg new_counter := work/get_counter(); -reg counter_other := work.string.counter; - -// file3.mcf -namespace file2 = "file2.mcf" -data tmp := file2.dinosaur.work.output diff --git a/ir.v b/ir.v index ff62efc..ca223a3 100644 --- a/ir.v +++ b/ir.v @@ -58,7 +58,9 @@ pub mut: type BBID = int -// TODO: resolve this lol +// TODO: we need to add scopes to basic blocks, so we can +// who own whom. and who we can access. +// Also need to determine pub struct IRBasicBlock { pub mut: label string @@ -66,12 +68,19 @@ pub mut: namespace NID function FID args []IRBasicBlockArg - vars_map map[string]RID insts []IID + vars_map map[string]RID // Variable name -> RID mapping for this block - predecessors []BBID - successors []BBID - stmts []Stmt // The list of statements from the IR that form this basic block + // Relationships + dominator ?BBID + dominance_level int + predecessors []BBID + successors []BBID + + // For construction + stmts []Stmt // The list of statements from the IR that form this basic block + is_sealed bool // Are all the predecessors known? + is_filled bool // Have we generated alled the instructions? } pub type BBAID = int @@ -91,31 +100,30 @@ pub type IRInstruction = IRMacroLiteralCmd | IRBinaryOp | IRUnaryOp | IRAssign + | IRStore + | IRCall + | IRStructInit + | IRFieldAccess + | IRIndexAccess + | IRDeref + | IRRefInst + | IRJump + | IRBranch + | IRReturn pub struct IRDefine { pub mut: id IID result RID value OID + pos int // Source position for error reporting } pub struct IRTypedDefine { pub mut: id IID result RID -} - -pub fn (t TokenKind) to_assign_op() AssignOp { - return match t { - .assign { .assign } - .plus_assign { .add_assign } - .minus_assign { .sub_assign } - .star_assign { .mul_assign } - .slash_assign { .div_assign } - .percent_assign { .mod_assign } - .swap { .swap } - else { panic('illegal use of token ${t} as a Assign operation') } - } + pos int // Source position for error reporting } pub enum AssignOp { @@ -128,29 +136,15 @@ pub enum AssignOp { swap } -pub fn (a AssignOp) print() { - match a { - .assign { - print('=') - } - .add_assign { - print('+=') - } - .sub_assign { - print('-=') - } - .mul_assign { - print('*=') - } - .div_assign { - print('/=') - } - .mod_assign { - print('%=') - } - .swap { - print('><') - } +pub fn (op AssignOp) print() { + match op { + .assign { print(' = ') } + .add_assign { print(' += ') } + .sub_assign { print(' -= ') } + .mul_assign { print(' *= ') } + .div_assign { print(' /= ') } + .mod_assign { print(' %= ') } + .swap { print(' >< ') } } } @@ -160,6 +154,7 @@ pub mut: result RID op AssignOp value OID + pos int // Source position for error reporting } pub struct IRStore { @@ -167,6 +162,7 @@ pub mut: id IID result RID source RID + pos int // Source position for error reporting } pub enum BinaryOp { @@ -183,47 +179,6 @@ pub enum BinaryOp { ge } -pub fn (t TokenKind) to_binary_op() BinaryOp { - return match t { - .plus { - .add - } - .minus { - .sub - } - .star { - .mul - } - .slash { - .div - } - .percent { - .mod - } - .eq { - .eq - } - .ne { - .ne - } - .lcarrot { - .lt - } - .rcarrot { - .gt - } - .lte { - .le - } - .gte { - .ge - } - else { - panic('Token to convert to binary operation ${t} is not supported as binary operation') - } - } -} - pub struct IRBinaryOp { pub mut: result RID @@ -231,15 +186,7 @@ pub mut: op BinaryOp left OID right OID -} - -pub fn (t TokenKind) to_unary_op() UnaryOp { - return match t { - .ampersand { .ref } - .at { .deref } - .ex_point { .neg } - else { panic('illegal use of token ${t} as a unary operation') } - } + pos int // Source position for error reporting } pub enum UnaryOp { @@ -254,6 +201,7 @@ pub mut: result RID op UnaryOp operand OID + pos int // Source position for error reporting } pub struct IRCall { @@ -262,6 +210,7 @@ pub mut: result ?RID function FID args []OID + pos int // Source position for error reporting } pub struct IRStructInit { @@ -270,13 +219,16 @@ pub mut: result RID struct_type SID field_values map[string]OID + pos int // Source position for error reporting } pub struct IRFieldAccess { +pub mut: id IID result RID source RID field string + pos int // Source position for error reporting } pub struct IRIndexAccess { @@ -287,6 +239,7 @@ pub mut: index OID is_slice bool end ?OID + pos int // Source position for error reporting } pub struct IRDeref { @@ -294,27 +247,42 @@ pub mut: id IID result RID source RID + pos int // Source position for error reporting +} + +// Takes the address/reference of a value +pub struct IRRefInst { +pub mut: + id IID + result RID + source RID + pos int // Source position for error reporting } pub struct IRJump { pub mut: id IID target BBID - // TODO: add value here for calling the .mcfunction with args + args []OID + pos int // Source position for error reporting } pub struct IRBranch { pub mut: - id IID - cond OID - then BBID - el BBID - // TODO: add values here for calling the .mcfunction with args + id IID + cond OID + then_bb BBID + then_args []OID + else_bb BBID + else_args []OID + pos int // Source position for error reporting } pub struct IRReturn { pub mut: + id IID value ?OID + pos int // Source position for error reporting } // dunno @@ -327,6 +295,7 @@ pub mut: func FID id IID parts []IRMacroCmdPart + pos int // Source position for error reporting } pub type IRMacroCmdPart = IRMacroCmdText | IRMacroCmdMacro | IRMacroCmdString @@ -397,6 +366,7 @@ pub mut: storage StorageKind location IRLocation // where it is stored (This is our general reference thing) is_macro_dep bool // If it is the result of a macro call + pos int // Source position for error reporting } type RID = int @@ -407,6 +377,7 @@ pub mut: id RID value IRRefSum typ IRType + pos int // Source position for error reporting } // IR OPERAND @@ -438,8 +409,8 @@ pub fn (id OID) to_ir_type(f IRFunction) IRType { IRDictConst { return BuiltinType.dict_t } - else { - panic('Type not implemented in oid to ir type ${f.consts[id]}') + IRRangeConst { + return BuiltinType.list_t // Ranges are list-like } } } @@ -529,15 +500,27 @@ pub mut: basic_blocks []IRBasicBlock structs []IRStructDef variables []IRValue + errors &ErrorManager = unsafe { nil } // Error manager for reporting } -pub fn (mut b IRBuilder) solve_namespaces() { +pub fn (mut b IRBuilder) solve_namespaces() bool { mut ns_solver := NSSolver{} for file in b.files { - ns_solver.solve(file) or { panic('Could not solve') } + ns_solver.solve(file) or { + if b.errors != unsafe { nil } { + b.errors.error(.namespace_not_found, SourceLocation{ + file: file + pos: 0 + }, 'could not resolve namespace from file: ${file}') + } + return false + } } if !ns_solver.verify_legal() { - panic('Could not solve the namespaces... look at that') + if b.errors != unsafe { nil } { + b.errors.error(.namespace_collision, empty_location(), 'could not solve namespaces: circular dependency or naming conflict detected') + } + return false } mut s2b := map[NSNodeId]NID{} mut b2s := map[NID]NSNodeId{} @@ -558,7 +541,10 @@ pub fn (mut b IRBuilder) solve_namespaces() { } if b.namespaces.len == 0 { - panic('There is no namespaces?') + if b.errors != unsafe { nil } { + b.errors.error(.namespace_not_found, empty_location(), 'no namespaces found in source files') + } + return false } mut visited := map[NID]bool{} @@ -591,6 +577,7 @@ pub fn (mut b IRBuilder) solve_namespaces() { visited[nid] = true } + return true } pub fn (mut b IRBuilder) tranverse_namespace(nsa IRNamespace, path []string) ?NID { @@ -605,47 +592,31 @@ pub fn (mut b IRBuilder) tranverse_namespace(nsa IRNamespace, path []string) ?NI } pub fn (mut b IRBuilder) namespace_yeild_ir_type(ns IRNamespace, typ Type) ?IRType { - mut ft := typ - mut it := IRType{} - mut ctr := 0 - for mut ft is ReferenceType { - ft = ft.base - ctr++ - } match typ { BuiltinType { - it = IRType(typ as BuiltinType) - for _ in 0 .. ctr { - it = IRType(IRRefType{ - base: it - }) - } - return it + return IRType(typ) + } + ReferenceType { + // Recursively resolve the base type and wrap in IRRefType + base_ir := b.namespace_yeild_ir_type(ns, typ.base) or { return none } + return IRType(IRRefType{ + base: base_ir + }) } StructType { - sft := (typ as StructType) - ns_path := sft.name.to_list() + ns_path := typ.name.to_list() nnid := b.tranverse_namespace(ns, ns_path) or { - println('Could to traverse from ${ns.name} along ${ns_path} for type conversion ${typ} ') + println('Could not traverse from ${ns.name} along ${ns_path} for type conversion ${typ}') return none } // Need to check if name is a struct in that namespace - it = b.namespaces[nnid].struct_map[sft.name.name.name] or { - println('struct is not foundd in that namespace to use as a field') + sid := b.namespaces[nnid].struct_map[typ.name.name.name] or { + println('struct is not found in that namespace to use as a field') return none } - for _ in 0 .. ctr { - it = IRType(IRRefType{ - base: it - }) - } - return it - } - else { - panic('unreachable') + return IRType(sid) } } - return none } pub fn (mut b IRBuilder) namespace_extract_structs() bool { @@ -690,7 +661,13 @@ pub fn (mut b IRBuilder) namespace_extract_structs() bool { for ast_s in ast_structs { astpos := ast_s.Node.pos - mut s := &b.structs[smap[astpos]] or { panic('FUCK (IR STRUCT THING)') } + mut s := &b.structs[smap[astpos]] or { + if b.errors != unsafe { nil } { + b.errors.error(.internal_compiler_error, empty_location(), 'internal error: struct not found in mapping during field resolution') + } + result = false + continue + } for f in ast_s.fields { mut ft := f.field_type @@ -733,7 +710,14 @@ pub fn (mut b IRBuilder) namespace_extract_structs() bool { s.fields[f.name.name] = it } else { - panic('unreachable type ${ft}') + if b.errors != unsafe { nil } { + b.errors.error(.cannot_resolve_type, SourceLocation{ + file: '' + pos: ast_s.Node.pos + }, 'unsupported type in struct field: ${ft}') + } + result = false + continue } } } @@ -820,7 +804,13 @@ pub fn (mut b IRBuilder) namespace_extract_namesapce_defs() bool { } } else { - panic('unreachable effemeral as namespace value') + if b.errors != unsafe { nil } { + b.errors.error(.invalid_operation, SourceLocation{ + pos: stmt.Node.pos + }, 'ephemeral storage is not allowed at namespace scope') + } + result = false + continue } } } @@ -888,7 +878,9 @@ pub fn (mut b IRBuilder) stage1() bool { // to just limit the scope of the problem so that it becomes easier to work // with. To start we are going to break up all the namespaces and make good // references for all them to each other. - b.solve_namespaces() + if !b.solve_namespaces() { + return false + } // Next we are going to pull out all the function definitions, struct // declarations, for all the namespaces. We have to start with the structs @@ -907,23 +899,26 @@ pub fn (mut b IRBuilder) stage1() bool { return result } -pub fn (mut b IRBuilder) bb_link(self BBID, child BBID) { +pub fn (mut b IRBuilder) link_bb(self BBID, child BBID) { // TODO: add some saftey checks lol b.basic_blocks[self].successors << child b.basic_blocks[child].predecessors << self } -pub fn (mut b IRBuilder) add_bb(fid FID, label string) BBID { +pub fn (mut b IRBuilder) create_bb(fid FID, label string) BBID { nid := b.functions[fid].namespace bb := IRBasicBlock{ label: label id: b.basic_blocks.len namespace: nid function: fid + is_sealed: false + is_filled: false } + bbid := bb.id b.basic_blocks << bb - b.functions[fid].bbs << bb.id - return bb.id + b.functions[fid].bbs << bbid + return bbid } pub fn (mut b IRBuilder) literal_has_macro(l Literal) bool { @@ -1128,6 +1123,7 @@ pub fn (mut b IRBuilder) expr_needs_block(e Expr) bool { // Start is the BBID to start parsing on // stmts are the statements that belong to this block or its children // Returns the BBID of the block that it has ended on +// TODO: rip out after I finish pub fn (mut b IRBuilder) bb_build_bb_cfg(start BBID, stmts []Stmt) (bool, BBID) { fid := b.basic_blocks[start].function mut current := start @@ -1169,36 +1165,45 @@ pub fn (mut b IRBuilder) bb_build_bb_cfg(start BBID, stmts []Stmt) (bool, BBID) println('Function blocks are not allowed to have namespace aliases in them ${stmt}') } FunctionInlineDefinition { - println('TODO FunctionInlineDefinition') - panic('Not handled') + if b.errors != unsafe { nil } { + b.errors.error(.not_implemented, SourceLocation{ + pos: stmt.Node.pos + }, 'inline function definitions are not yet implemented') + } + result = false } Block { - panic('Block in block??') + if b.errors != unsafe { nil } { + b.errors.error(.unsupported_statement_location, SourceLocation{ + pos: stmt.Node.pos + }, 'bare block statements are not allowed inside function bodies') + } + result = false } IfStmt { // Both the condition and of course the blocks... will create blocks if b.expr_needs_block(stmt.condition) { - cond := b.add_bb(fid, 'if_cond') - b.bb_link(current, cond) + cond := b.create_bb(fid, 'if_cond') + b.link_bb(current, cond) current = cond } b.basic_blocks[current].stmts << stmt // create then else and merge block - mut then := b.add_bb(fid, 'if_then') - b.bb_link(current, then) + mut then := b.create_bb(fid, 'if_then') + b.link_bb(current, then) state, then = b.bb_build_bb_cfg(then, stmt.then_block.stmts) result = result && state - mut el := b.add_bb(fid, 'if_else') - b.bb_link(current, el) + mut el := b.create_bb(fid, 'if_else') + b.link_bb(current, el) if stmt.else_block == none { } else { elb := stmt.else_block state, el = b.bb_build_bb_cfg(el, elb.stmts) result = result && state } - merge := b.add_bb(fid, 'if_merge') - b.bb_link(then, merge) - b.bb_link(el, merge) + merge := b.create_bb(fid, 'if_merge') + b.link_bb(then, merge) + b.link_bb(el, merge) current = merge } Return { @@ -1208,16 +1213,16 @@ pub fn (mut b IRBuilder) bb_build_bb_cfg(start BBID, stmts []Stmt) (bool, BBID) } if b.expr_needs_block(ret) { - ret_bbid := b.add_bb(fid, 'ret') - b.bb_link(current, ret_bbid) + ret_bbid := b.create_bb(fid, 'ret') + b.link_bb(current, ret_bbid) current = ret_bbid } b.basic_blocks[current].stmts << stmt } ExprStmt { if b.expr_needs_block(stmt.expr) { - emac := b.add_bb(fid, 'macro') - b.bb_link(current, emac) + emac := b.create_bb(fid, 'macro') + b.link_bb(current, emac) current = emac } b.basic_blocks[current].stmts << stmt @@ -1236,32 +1241,32 @@ pub fn (mut b IRBuilder) bb_build_bb_cfg(start BBID, stmts []Stmt) (bool, BBID) } } if need_block { - command := b.add_bb(fid, 'command') - b.bb_link(current, command) + command := b.create_bb(fid, 'command') + b.link_bb(current, command) current = command } b.basic_blocks[current].stmts << stmt } Define { if b.expr_needs_block(stmt.value) { - macro_use := b.add_bb(fid, 'macro_use') - b.bb_link(current, macro_use) + macro_use := b.create_bb(fid, 'macro_use') + b.link_bb(current, macro_use) current = macro_use } b.basic_blocks[current].stmts << stmt } Assignment { if b.expr_needs_block(stmt.left) || b.expr_needs_block(stmt.right) { - assignment := b.add_bb(fid, 'assignment') - b.bb_link(current, assignment) + assignment := b.create_bb(fid, 'assignment') + b.link_bb(current, assignment) current = assignment } b.basic_blocks[current].stmts << stmt } Store { if b.expr_needs_block(stmt.left) || b.expr_needs_block(stmt.right) { - store := b.add_bb(fid, 'store') - b.bb_link(current, store) + store := b.create_bb(fid, 'store') + b.link_bb(current, store) current = store } b.basic_blocks[current].stmts << stmt @@ -1271,15 +1276,174 @@ pub fn (mut b IRBuilder) bb_build_bb_cfg(start BBID, stmts []Stmt) (bool, BBID) return result, current } -pub fn (mut b IRBuilder) fn_build_bb_cfg(fid FID) bool { - println('extracting bbs from function ${b.functions[fid].name}') - block := b.functions[fid].block - // Now we are going to make our entry block for this function - entry := b.add_bb(fid, 'entry') - valid, _ := b.bb_build_bb_cfg(entry, block.stmts) - return valid +// Build CFG recursively, creating new basic blocks when macro values are used. +// Each basic block maps to a .mcfunction file - when macros are used, we need +// a new function file to pass the macro arguments. +pub fn (mut b IRBuilder) build_cfg_recursive(fid FID, current_bb BBID, stmts []Stmt) ?(bool, BBID) { + mut bb := current_bb + + for stmt in stmts { + match stmt { + // TypedDefine: No value expression, no macro possible - just attach + TypedDefine { + b.basic_blocks[bb].stmts << stmt + } + // Define: Check if RHS has/is a macro + Define { + if b.expr_has_macro(stmt.value) { + def_bb := b.create_bb(fid, 'define_macro') + b.link_bb(bb, def_bb) + b.basic_blocks[bb].is_sealed = true + bb = def_bb + } + b.basic_blocks[bb].stmts << stmt + } + // Assignment: Check both sides for macros + Assignment { + if b.expr_has_macro(stmt.left) || b.expr_has_macro(stmt.right) { + assign_bb := b.create_bb(fid, 'assign_macro') + b.link_bb(bb, assign_bb) + b.basic_blocks[bb].is_sealed = true + bb = assign_bb + } + b.basic_blocks[bb].stmts << stmt + } + // Store: Same as assignment + Store { + if b.expr_has_macro(stmt.left) || b.expr_has_macro(stmt.right) { + store_bb := b.create_bb(fid, 'store_macro') + b.link_bb(bb, store_bb) + b.basic_blocks[bb].is_sealed = true + bb = store_bb + } + b.basic_blocks[bb].stmts << stmt + } + // ExprStmt: Expression that may contain macros + ExprStmt { + if b.expr_has_macro(stmt.expr) { + expr_bb := b.create_bb(fid, 'expr_macro') + b.link_bb(bb, expr_bb) + b.basic_blocks[bb].is_sealed = true + bb = expr_bb + } + b.basic_blocks[bb].stmts << stmt + } + // IfStmt: Creates branches in CFG + IfStmt { + // Check if condition has macro + if b.expr_has_macro(stmt.condition) { + cond_bb := b.create_bb(fid, 'if_cond') + b.link_bb(bb, cond_bb) + b.basic_blocks[bb].is_sealed = true + bb = cond_bb + } + + // Store the if statement (condition evaluation) in current block + b.basic_blocks[bb].stmts << stmt + b.basic_blocks[bb].is_sealed = true + + // Create then branch + then_bb := b.create_bb(fid, 'if_then') + b.link_bb(bb, then_bb) + _, then_end := b.build_cfg_recursive(fid, then_bb, stmt.then_block.stmts)? + + // Create else branch + else_bb := b.create_bb(fid, 'if_else') + b.link_bb(bb, else_bb) + + mut else_end := else_bb + if else_block := stmt.else_block { + _, else_end = b.build_cfg_recursive(fid, else_bb, else_block.stmts)? + } else { + b.basic_blocks[else_bb].is_sealed = true + } + + // Create merge point + merge_bb := b.create_bb(fid, 'if_merge') + b.link_bb(then_end, merge_bb) + b.link_bb(else_end, merge_bb) + + bb = merge_bb + } + // Return: Terminal instruction + Return { + // Check if return value has macro + if val := stmt.value { + if b.expr_has_macro(val) { + ret_bb := b.create_bb(fid, 'return_eval') + b.link_bb(bb, ret_bb) + b.basic_blocks[bb].is_sealed = true + bb = ret_bb + } + } + b.basic_blocks[bb].stmts << stmt + b.basic_blocks[bb].is_sealed = true + return true, bb + } + // MacroLiteralCommand: $ commands with potential macros + MacroLiteralCommand { + mut has_macro := false + + for part in stmt.parts { + if part is MacroLiteralMacro { + has_macro = true + break + } + if part is MacroLiteralString { + // String could have interpolation with macros + if b.literal_has_macro(Literal(part.str_literal)) { + has_macro = true + break + } + } + } + + if has_macro { + cmd_bb := b.create_bb(fid, 'command') + b.link_bb(bb, cmd_bb) + b.basic_blocks[bb].is_sealed = true + bb = cmd_bb + } + b.basic_blocks[bb].stmts << stmt + } + // Invalid statements in function bodies + StructDefinition, FunctionDefinition, NamespaceDefinition, NamespaceImport, + NamespaceAlias, FunctionInlineDefinition, Block { + println('Error: ${stmt} not allowed in function body') + return none + } + } + } + + b.basic_blocks[bb].is_sealed = true + return true, bb +} + +pub fn (mut b IRBuilder) s2_phase1_build_cfg_skeleton(fid FID) bool { + func := b.functions[fid] + + // Create our entry block + entry_bb := b.create_bb(fid, 'entry') + b.functions[fid].entrybb = entry_bb + + _, end_bb := b.build_cfg_recursive(fid, entry_bb, func.block.stmts) or { + // TODO: change this to be a proper error + println('We could not build the CFG for ${func.name}') + return false + } + + // Adding a return here, so that we will always have a return at the end of our functions, so we just like. know that + b.basic_blocks[entry_bb].is_sealed = true + if b.basic_blocks[end_bb].insts.len == 0 { + b.basic_blocks[end_bb].stmts << Stmt(Return{ + value: none + }) + } + + return true } +// TODO: add the error classes so we can accumulate errors and print out helpfully. pub fn (mut b IRBuilder) stage2() bool { mut result := true // Here we are going to go through our functions and generate our basic blocks except for the terminal instructions within them. @@ -1305,12 +1469,22 @@ pub fn (mut b IRBuilder) stage2() bool { // needed to be parsed for each basic block into each basic block so that it // will be easier to handle later. - // Here we will create the basic blocks for a function + // Phase 1: Build CFG skeletons for all functions for fid in 0 .. b.functions.len { - result = result && b.fn_build_bb_cfg(fid) + if !b.s2_phase1_build_cfg_skeleton(fid) { + return false + } } - // Then we are going to generate the instructions for our blocks + // Phase 2: Solve basic block arguments (for macro value passing) + for fid in 0 .. b.functions.len { + b.solve_bb_arguments(fid) + } + + // Phase 3: Lower instructions and add terminators + for fid in 0 .. b.functions.len { + b.fill_function_insts(fid) + } return result } @@ -1339,20 +1513,30 @@ pub fn (mut b IRBuilder) lower(files []string) !bool { return false } - // Next we are going to check that there is no overlap between names within any namespace + // Run semantic analysis after IR is built + mut checker := SemanticChecker.new(&b) + if !checker.check() { + // Errors are collected in the shared ErrorManager, main will print them + return false + } + return true } pub fn (mut b IRBuilder) lower_namespace_alias(nsa NamespaceAlias) { } -pub fn (mut b IRBuilder) lower_stmt(stmt Stmt) { +pub fn (mut b IRBuilder) lower_stmt(stmt Stmt) bool { match stmt { NamespaceAlias { b.lower_namespace_alias(stmt) + return true } else { - panic('Statement is not implemented ${stmt}') + if b.errors != unsafe { nil } { + b.errors.error(.not_implemented, empty_location(), 'statement lowering not implemented for: ${stmt}') + } + return false } } } diff --git a/ir_instruction_gen.v b/ir_instruction_gen.v index 28c1702..666648c 100644 --- a/ir_instruction_gen.v +++ b/ir_instruction_gen.v @@ -10,17 +10,17 @@ pub fn (mut b IRBuilder) bb_get_reference(name string, bbid BBID, fid FID) (bool // check if its in our arguments if name in bb.vars_map { - return true, fun.refs[bb.vars_map[name]] + return true, b.functions[fid].refs[bb.vars_map[name]] } for arg in bb.args { if arg.name == name { ref := IRRef{ - id: fun.refs.len + id: b.functions[fid].refs.len value: arg typ: arg.typ } - fun.refs << ref - bb.vars_map[name] = ref.id + b.functions[fid].refs << ref + b.basic_blocks[bbid].vars_map[name] = ref.id return true, ref } } @@ -35,7 +35,7 @@ pub fn (mut b IRBuilder) bb_get_reference(name string, bbid BBID, fid FID) (bool // check if its defined in any predecesor blocks // who exist in the same scope TODO for !to_visit.is_empty() { - new_id := to_visit.pop() or { panic("we really thought we'd have something lol") } + new_id := to_visit.pop() or { continue } if new_id in visited { continue } @@ -43,7 +43,7 @@ pub fn (mut b IRBuilder) bb_get_reference(name string, bbid BBID, fid FID) (bool if name in new.vars_map { ref := new.vars_map[name] b.basic_blocks[bbid].vars_map[name] = ref - return true, fun.refs[ref] + return true, b.functions[fid].refs[ref] } visited[new_id] = true for pred in new.predecessors { @@ -54,22 +54,23 @@ pub fn (mut b IRBuilder) bb_get_reference(name string, bbid BBID, fid FID) (bool // check if its in our owning namespace if name in ns.variable_map { ref := IRRef{ - id: fun.refs.len + id: b.functions[fid].refs.len value: IRRefSum(ns.variable_map[name]) } - fun.refs << ref + b.functions[fid].refs << ref b.basic_blocks[bbid].vars_map[name] = ref.id return true, ref } // check if its in our function's arguments - for arg in fun.args { + for arg in b.functions[fid].args { if arg.name == name { ref := IRRef{ - id: fun.refs.len + id: b.functions[fid].refs.len value: arg + typ: arg.typ } - fun.refs << ref + b.functions[fid].refs << ref b.basic_blocks[bbid].vars_map[name] = ref.id return true, ref } @@ -114,7 +115,7 @@ pub fn (mut b IRBuilder) bb_get_reference_no_creation(name string, bbid BBID, fi // check if its defined in any predecesor blocks // who exist in the same scope TODO for !to_visit.is_empty() { - new_id := to_visit.pop() or { panic("we really thought we'd have something lol") } + new_id := to_visit.pop() or { continue } if new_id in visited { continue } @@ -228,13 +229,17 @@ pub fn (mut b IRBuilder) bb_add_value_ast(bbid BBID, name string, typ Type, sour fid := bb.function func := b.functions[fid] ns := b.namespaces[func.namespace] - mut val := IRValue{ - name: name - id: b.variables.len - typ: b.namespace_yeild_ir_type(ns, typ) or { - panic('could not find type for typed define') + ir_typ := b.namespace_yeild_ir_type(ns, typ) or { + if b.errors != unsafe { nil } { + b.errors.error(.cannot_resolve_type, empty_location(), "could not resolve type for '${name}'") } - storage: source.to_ir() + return VID(-1) + } + mut val := IRValue{ + name: name + id: b.variables.len + typ: ir_typ + storage: source.to_ir() location: match source { .effemeral { IRLocation(IREffLocation{ @@ -259,67 +264,178 @@ pub fn (mut b IRBuilder) bb_add_value_ast(bbid BBID, name string, typ Type, sour return val.id } -// returns if its a refered macro, and the RID for accessing it! +// Handle macro expressions with full identifier chain support including nesting. +// Returns (is_referable, RID) - the reference ID can be used to access the macro value. +// Each element in the chain is processed, creating intermediate instructions as needed. pub fn (mut b IRBuilder) bb_handle_macro_expr(bbid BBID, mexpr MacroExpr) (bool, RID) { refed := mexpr.referable fid := b.basic_blocks[bbid].function - // TODO: Implement going down the identifier chain -> will have to make macro expasions and such.... - // which will create more basic blocks. chain := mexpr.ident_chain - if chain.elements.len > 1 { - panic('Not implemented identifier chains yet...') + + if chain.elements.len == 0 { + if b.errors != unsafe { nil } { + b.errors.error(.empty_macro_chain, empty_location(), 'empty macro expression chain') + } + return false, RID(-1) } - e := chain.elements[0] - match e { + + // Process the first element to get the base + mut current_rid := RID(-1) + mut current_type := IRType(BuiltinType.void_t) + + first := chain.elements[0] + match first { IdentifierChainName { - ok, ref := b.bb_get_reference(e.name.name, bbid, fid) + ok, ref := b.bb_get_reference(first.name.name, bbid, fid) if !ok { - panic('could not find value to macro expand in ${mexpr}') + if b.errors != unsafe { nil } { + b.errors.error_with_hint(.undefined_variable, SourceLocation{ + file: '' + pos: first.name.pos + len: first.name.name.len + }, "undefined variable '${first.name.name}' in macro expression", 'ensure the variable is defined before using it in a macro') + } + return false, RID(-1) } + // Check if already a BB argument match ref.value { IRBasicBlockArg { if ref.value.id == bbid { - // we don't have to add it as something that we want, someone has already done this for us. - return refed, ref.id + current_rid = ref.id + current_type = ref.typ + } else { + // Add as BB argument + current_rid = b.bb_add_macro_arg(bbid, fid, first.name.name, ref) + current_type = ref.typ } } - else {} + else { + // Add as BB argument + current_rid = b.bb_add_macro_arg(bbid, fid, first.name.name, ref) + current_type = ref.typ + } } + } + IdentifierChainMacro { + // Nested macro at start - recursively handle + _, nested_rid := b.bb_handle_macro_expr(bbid, first.macro_expr) + current_rid = nested_rid + current_type = b.functions[fid].refs[nested_rid].typ + } + IdentifierChainIndex { + // Index at start doesn't make sense without a base + if b.errors != unsafe { nil } { + b.errors.error(.invalid_operation, empty_location(), 'cannot start macro chain with index access') + } + return false, RID(-1) + } + IdentifierChainDeref { + // Deref at start doesn't make sense without a base + if b.errors != unsafe { nil } { + b.errors.error(.invalid_operation, empty_location(), 'cannot start macro chain with dereference') + } + return false, RID(-1) + } + } - // It is not an argument to ourself... We need to ask for this thing. - // As we are going to be using it as a macro value it has to be as a data -> we can set the storage type to data - // type IRRefSum = VID | IRBasicBlockArg | IRFunctionArg | IID - mut arg := IRBasicBlockArg{ - id: bbid - typ: ref.typ - storage: .data + // Process remaining chain elements + for i in 1 .. chain.elements.len { + elem := chain.elements[i] + match elem { + IdentifierChainName { + // Field access - create IRFieldAccess + mut inst := IRFieldAccess{ + id: b.functions[fid].insts.len + source: current_rid + field: elem.name.name + } + // Look up field type from struct definition if possible + field_type := b.get_field_type(current_type, elem.name.name) or { current_type } + inst.result = b.bb_add_anon_reference(bbid, inst.id, field_type) + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + current_rid = inst.result + current_type = field_type } - match ref.value { - IRBasicBlockArg { - arg.name = ref.value.name + IdentifierChainMacro { + // Nested macro for dynamic field access - create index access with macro value + _, nested_rid := b.bb_handle_macro_expr(bbid, elem.macro_expr) + mut inst := IRIndexAccess{ + id: b.functions[fid].insts.len + source: current_rid + index: OID(nested_rid) + is_slice: false } - IRFunctionArg { - arg.name = ref.value.name + inst.result = b.bb_add_anon_reference(bbid, inst.id, current_type) + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + current_rid = inst.result + } + IdentifierChainIndex { + // Array/list index - create IRIndexAccess + index_oid := b.lower_expression(bbid, fid, elem.index_expr) + mut inst := IRIndexAccess{ + id: b.functions[fid].insts.len + source: current_rid + index: index_oid + is_slice: false } - VID { - arg.name = b.variables[ref.value].name + inst.result = b.bb_add_anon_reference(bbid, inst.id, current_type) + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + current_rid = inst.result + } + IdentifierChainDeref { + // Dereference - create IRDeref + mut inst := IRDeref{ + id: b.functions[fid].insts.len + source: current_rid } - IID { - panic('should be impossible for for an instruction to be the reference for a name...') - arg.name = '' + // Result type is the dereferenced type + mut result_type := current_type + if current_type is IRRefType { + ref_type := current_type as IRRefType + result_type = ref_type.base } + inst.result = b.bb_add_anon_reference(bbid, inst.id, result_type) + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + current_rid = inst.result + current_type = result_type } - b.basic_blocks[bbid].args << arg - return refed, ref.id + } + } + + return refed, current_rid +} + +// Helper to add a macro argument to a basic block +fn (mut b IRBuilder) bb_add_macro_arg(bbid BBID, fid FID, name string, ref IRRef) RID { + mut arg := IRBasicBlockArg{ + id: bbid + typ: ref.typ + storage: .data + name: name + } + b.basic_blocks[bbid].args << arg + return ref.id +} + +// Helper to get field type from a struct type +fn (mut b IRBuilder) get_field_type(typ IRType, field_name string) ?IRType { + match typ { + SID { + struct_def := b.structs[typ] + return struct_def.fields[field_name] or { return none } + } + IRRefType { + return b.get_field_type(typ.base, field_name) } else { - panic('not implemented anything other than identiferi chain name for macroexpr ${e}') + return none } } - - panic('could not handle macro expr') - return refed, RID(-1) } // Returns an oid that we can set to something, later on we can merge the instructions together @@ -341,16 +457,61 @@ pub fn (mut b IRBuilder) lower_expression(bbid BBID, fid FID, expr Expr) OID { return OID(inst.result) } UnaryExpr { - // Will become not a literal, therefore we can have it not be a constant anymore - mut inst := IRUnaryOp{ - op: expr.operator.to_unary_op() - operand: b.lower_expression(bbid, fid, expr.right) + unary_op := expr.operator.to_unary_op() + operand_oid := b.lower_expression(bbid, fid, expr.right) + + match unary_op { + .ref { + // Reference operator (&) - creates a pointer to the operand + operand_rid := match operand_oid { + RID { operand_oid } + CID { panic('Cannot take reference of constant') } + } + mut inst := IRRefInst{ + id: b.functions[fid].insts.len + source: operand_rid + } + // Result type is a reference to the operand type + base_type := operand_oid.to_ir_type(func) + inst.result = b.bb_add_anon_reference(bbid, inst.id, IRType(IRRefType{ base: base_type })) + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + return OID(inst.result) + } + .deref { + // Dereference operator (@) - gets the value pointed to + operand_rid := match operand_oid { + RID { operand_oid } + CID { panic('Cannot dereference constant') } + } + mut inst := IRDeref{ + id: b.functions[fid].insts.len + source: operand_rid + } + // Result type is the dereferenced type + base_type := operand_oid.to_ir_type(func) + result_type := match base_type { + IRRefType { base_type.base } + else { base_type } // Allow deref on non-ref for flexibility + } + inst.result = b.bb_add_anon_reference(bbid, inst.id, result_type) + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + return OID(inst.result) + } + .neg { + // Negation - use regular unary op + mut inst := IRUnaryOp{ + op: unary_op + operand: operand_oid + } + inst.id = b.functions[fid].insts.len + inst.result = b.bb_add_anon_reference(bbid, inst.id, operand_oid.to_ir_type(func)) + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + return OID(inst.result) + } } - inst.id = b.functions[fid].insts.len - inst.result = b.bb_add_anon_reference(bbid, inst.id, inst.operand.to_ir_type(func)) - b.functions[fid].insts << inst - b.basic_blocks[bbid].insts << inst.id - return OID(inst.result) } Literal { lit := expr @@ -407,39 +568,339 @@ pub fn (mut b IRBuilder) lower_expression(bbid BBID, fid FID, expr Expr) OID { return OID(cid) } ListLiteral { - panic('list literal not implemented') + mut list_const := IRListConst{} + for elem in lit.elements { + list_const.elements << b.lower_expression(bbid, fid, elem) + } + cid := CID(b.functions[fid].consts.len) + b.functions[fid].consts << list_const + return OID(cid) } DictionaryLiteral { - panic('dictionary literal not implemented') + mut dict_const := IRDictConst{} + for entry in lit.entries { + mut key_oid := OID(CID(-1)) + match entry.key_kind { + .integer_key { + key_cid := CID(b.functions[fid].consts.len) + b.functions[fid].consts << IRIntConst{ + value: entry.integer_key or { 0 } + } + key_oid = OID(key_cid) + } + .string_key { + str_key := entry.string_key or { StringLiteral{} } + key_cid := CID(b.functions[fid].consts.len) + b.functions[fid].consts << IRStringConst{ + parts: [IRStringPart(IRStringText{ text: str_key.value })] + } + key_oid = OID(key_cid) + } + .macro_key { + macro_key := entry.macro_key or { panic('expected macro key') } + _, mrid := b.bb_handle_macro_expr(bbid, macro_key) + key_oid = OID(mrid) + } + } + dict_const.entries << IRDictEntry{ + key: key_oid + value: b.lower_expression(bbid, fid, entry.value) + } + } + cid := CID(b.functions[fid].consts.len) + b.functions[fid].consts << dict_const + return OID(cid) } RangeLiteral { - panic('range literal not implemented') + mut range_const := IRRangeConst{} + if start := lit.start { + range_const.start = b.lower_expression(bbid, fid, start) + } + if end := lit.end { + range_const.end = b.lower_expression(bbid, fid, end) + } + cid := CID(b.functions[fid].consts.len) + b.functions[fid].consts << range_const + return OID(cid) } } } - Identifier { // Will have to be a ref, and it must exist! otherwise we're boned boys + Identifier { ok, ref := b.bb_get_reference(expr.name, bbid, fid) if ok { return OID(ref.id) } else { - panic('We could not find a reference to the identifier ${expr}') + if b.errors != unsafe { nil } { + b.errors.error(.undefined_variable, SourceLocation{ + pos: expr.pos + len: expr.name.len + }, "undefined variable '${expr.name}'") + } + return OID(CID(-1)) } } MacroExpr { - panic('not implemented macro expr yet for lowering expressoin') + // Macro expressions become basic block arguments + _, mrid := b.bb_handle_macro_expr(bbid, expr) + return OID(mrid) } AccessExpr { - panic('not implemente d access expressoin yet for loweing expression') + access := expr + match access { + MemberAccessExpr { + // Lower the target first + mut current_oid := b.lower_expression(bbid, fid, access.target) + + // Process the chain + for elem in access.chain { + match elem { + FieldAccessElement { + // Field access: create IRFieldAccess instruction + if current_oid is CID { + panic('Cannot access field on constant') + } + current_rid := current_oid as RID + mut inst := IRFieldAccess{ + id: b.functions[fid].insts.len + source: current_rid + field: elem.field.name + } + // Result type needs to be looked up from struct definition + inst.result = b.bb_add_anon_reference(bbid, inst.id, current_oid.to_ir_type(b.functions[fid])) + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + current_oid = OID(inst.result) + } + IndexAccessElement { + // Index access: create IRIndexAccess instruction + if current_oid is CID { + panic('Cannot index into constant') + } + current_rid := current_oid as RID + mut inst := IRIndexAccess{ + id: b.functions[fid].insts.len + source: current_rid + index: b.lower_expression(bbid, fid, elem.index_expr) + is_slice: elem.is_slice + } + if elem.is_slice { + if end := elem.slice_end { + inst.end = b.lower_expression(bbid, fid, end) + } + } + inst.result = b.bb_add_anon_reference(bbid, inst.id, current_oid.to_ir_type(b.functions[fid])) + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + current_oid = OID(inst.result) + } + MacroAccessElement { + // Macro in access chain - need to handle this specially + _, mrid := b.bb_handle_macro_expr(bbid, elem.macro_expr) + if current_oid is CID { + panic('Cannot access macro field on constant') + } + current_rid := current_oid as RID + mut inst := IRIndexAccess{ + id: b.functions[fid].insts.len + source: current_rid + index: OID(mrid) + is_slice: false + } + inst.result = b.bb_add_anon_reference(bbid, inst.id, current_oid.to_ir_type(b.functions[fid])) + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + current_oid = OID(inst.result) + } + DerefAccessElement { + // Dereference: create IRDeref instruction + if current_oid is CID { + panic('Cannot dereference constant') + } + current_rid := current_oid as RID + mut inst := IRDeref{ + id: b.functions[fid].insts.len + source: current_rid + } + // Result type is the dereferenced type + base_type := current_oid.to_ir_type(b.functions[fid]) + mut result_type := base_type + if base_type is IRRefType { + ref_type := base_type as IRRefType + result_type = ref_type.base + } + inst.result = b.bb_add_anon_reference(bbid, inst.id, result_type) + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + current_oid = OID(inst.result) + } + } + } + return current_oid + } + IndexAccessExpr { + // Direct index access + target_oid := b.lower_expression(bbid, fid, access.target) + target_rid := match target_oid { + RID { target_oid } + CID { panic('Cannot index into constant directly') } + } + mut inst := IRIndexAccess{ + id: b.functions[fid].insts.len + source: target_rid + index: b.lower_expression(bbid, fid, access.index.index_expr) + is_slice: access.index.is_slice + } + if access.index.is_slice { + if end := access.index.slice_end { + inst.end = b.lower_expression(bbid, fid, end) + } + } + inst.result = b.bb_add_anon_reference(bbid, inst.id, target_oid.to_ir_type(b.functions[fid])) + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + return OID(inst.result) + } + FunctionCallExpr { + // Function calls - need to resolve the function and create IRCall + mut call_fid := FID(-1) + + // Resolve the function being called + base := access.base_target + match base { + Identifier { + // Look up in current namespace + call_fid = b.namespaces[ns.id].function_map[base.name] or { + if b.errors != unsafe { nil } { + b.errors.error(.undefined_function, SourceLocation{ + pos: base.pos + len: base.name.len + }, "undefined function '${base.name}'") + } + return OID(CID(-1)) + } + } + QualifiedIdentifier { + // Traverse namespace path + ns_path := base.to_list() + target_nid := b.tranverse_namespace(ns, ns_path) or { + if b.errors != unsafe { nil } { + b.errors.error(.undefined_namespace, SourceLocation{ + pos: base.pos + }, "could not resolve namespace path for function '${base.name.name}'") + } + return OID(CID(-1)) + } + call_fid = b.namespaces[target_nid].function_map[base.name.name] or { + if b.errors != unsafe { nil } { + b.errors.error(.undefined_function, SourceLocation{ + pos: base.pos + }, "function '${base.name.name}' not found in namespace") + } + return OID(CID(-1)) + } + } + else { + if b.errors != unsafe { nil } { + b.errors.error(.invalid_operation, empty_location(), 'cannot call non-identifier expression as function') + } + return OID(CID(-1)) + } + } + + // Lower arguments + mut arg_oids := []OID{} + for arg in access.args { + arg_oids << b.lower_expression(bbid, fid, arg) + } + + // Create IRCall instruction + mut inst := IRCall{ + id: b.functions[fid].insts.len + function: call_fid + args: arg_oids + } + + // Set result if function returns non-void + called_func := b.functions[call_fid] + if called_func.return_type != IRType(BuiltinType.void_t) { + inst.result = b.bb_add_anon_reference(bbid, inst.id, called_func.return_type) + } + + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + + if result_rid := inst.result { + return OID(result_rid) + } + // Void return - return a dummy + return OID(CID(-1)) + } + } } StructLiteral { - panic('not implemented struct literal expr yet for loweing expressions') + // Look up the struct type + ns_path := expr.struct_name.to_list() + target_nid := b.tranverse_namespace(ns, ns_path) or { + if b.errors != unsafe { nil } { + b.errors.error(.undefined_namespace, SourceLocation{ + pos: expr.struct_name.pos + }, "could not resolve namespace path for struct '${expr.struct_name.name.name}'") + } + return OID(CID(-1)) + } + struct_id := b.namespaces[target_nid].struct_map[expr.struct_name.name.name] or { + if b.errors != unsafe { nil } { + b.errors.error(.undefined_struct, SourceLocation{ + pos: expr.struct_name.pos + }, "struct '${expr.struct_name.name.name}' not found") + } + return OID(CID(-1)) + } + + // Create struct init instruction + mut inst := IRStructInit{ + id: b.functions[fid].insts.len + struct_type: struct_id + } + + // Lower field values + for field in expr.fields { + inst.field_values[field.name.name] = b.lower_expression(bbid, fid, field.value) + } + + inst.result = b.bb_add_anon_reference(bbid, inst.id, IRType(struct_id)) + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + return OID(inst.result) } QualifiedIdentifier { - panic('not yet implemented qualified identifier lowering for loweriing expresions yet') + // Resolve the qualified identifier to a variable + ns_path := expr.to_list() + target_nid := b.tranverse_namespace(ns, ns_path) or { + if b.errors != unsafe { nil } { + b.errors.error(.undefined_namespace, SourceLocation{ + pos: expr.pos + }, "could not resolve namespace path for '${expr.name.name}'") + } + return OID(CID(-1)) + } + // Look up variable in target namespace + vid := b.namespaces[target_nid].variable_map[expr.name.name] or { + if b.errors != unsafe { nil } { + b.errors.error(.undefined_variable, SourceLocation{ + pos: expr.pos + }, "variable '${expr.name.name}' not found in namespace") + } + return OID(CID(-1)) + } + rid := b.bb_add_reference(bbid, vid) + return OID(rid) } } - panic('could not lower expression') + if b.errors != unsafe { nil } { + b.errors.error(.not_implemented, empty_location(), 'could not lower expression') + } return OID(CID(-1)) } @@ -453,7 +914,12 @@ pub fn (mut b IRBuilder) fill_bb_insts_stmt(bbid BBID, fid FID, stmt Stmt) { if ok { // We have found a reference to this (Or made one) // This should not be allowed.... We are not allowed to have // this sort of shadowing! - panic('When defining ${stmt} we found a reference to it already existing.') + if b.errors != unsafe { nil } { + b.errors.error(.duplicate_definition, SourceLocation{ + pos: stmt.Node.pos + }, "variable '${stmt.name.name}' is already defined") + } + return } else { // No reference to this exists so we should like make one // need to create a value inst := IRTypedDefine{ @@ -469,7 +935,12 @@ pub fn (mut b IRBuilder) fill_bb_insts_stmt(bbid BBID, fid FID, stmt Stmt) { ok, ref := b.bb_get_reference(stmt.name.name, bbid, fid) if ok { // We have found a reference to this (or have made one) // This is not allowed, this is a definition. We are not allowed to have shadowing. - panic('When defining ${stmt} it would be shadowing something already in its scope. This is not allowed') + if b.errors != unsafe { nil } { + b.errors.error(.shadowing_not_allowed, SourceLocation{ + pos: stmt.Node.pos + }, "variable '${stmt.name.name}' would shadow an existing variable (shadowing is not allowed)") + } + return } else { // We are the first creation point for this thing! // We have to first figure out the type of the expression that we are coming from... // And generate the instructions that compose this expression... @@ -486,30 +957,39 @@ pub fn (mut b IRBuilder) fill_bb_insts_stmt(bbid BBID, fid FID, stmt Stmt) { Assignment { op := stmt.operator.to_assign_op() left_oid := b.lower_expression(bbid, fid, stmt.left) - left := match left_oid { - CID { - panic('Cannot assign to a constant value') - RID(-1) - } - RID { - left_oid + if left_oid is CID { + if b.errors != unsafe { nil } { + b.errors.error(.invalid_operation, SourceLocation{ + pos: stmt.Node.pos + }, 'cannot assign to a constant value') } + return } + left := left_oid as RID right := b.lower_expression(bbid, fid, stmt.right) match op { .assign { // Check that left and right are same type. if OID(left).to_ir_type(b.functions[fid]) != right.to_ir_type(b.functions[fid]) { - panic('cannot assign accross types') + if b.errors != unsafe { nil } { + b.errors.error(.type_mismatch, SourceLocation{ + pos: stmt.Node.pos + }, 'cannot assign across different types') + } + return } - // TODO: handle checking if the sources are different to } else { lref := b.functions[fid].refs[left] if lref.typ is BuiltinType { if lref.typ != BuiltinType.int_t { - panic('cannot do operation ${op} onto type ${lref.typ}') + if b.errors != unsafe { nil } { + b.errors.error(.invalid_operation, SourceLocation{ + pos: stmt.Node.pos + }, 'cannot perform operation ${op} on type ${lref.typ}') + } + return } } } @@ -523,8 +1003,127 @@ pub fn (mut b IRBuilder) fill_bb_insts_stmt(bbid BBID, fid FID, stmt Stmt) { b.functions[fid].insts << inst b.basic_blocks[bbid].insts << inst.id } - else { - println('not implemented ${stmt}') + Store { + // Store is like assignment but for storage operations (<-) + left_oid := b.lower_expression(bbid, fid, stmt.left) + if left_oid is CID { + if b.errors != unsafe { nil } { + b.errors.error(.invalid_operation, SourceLocation{ + pos: stmt.Node.pos + }, 'cannot store to a constant value') + } + return + } + left := left_oid as RID + + right_oid := b.lower_expression(bbid, fid, stmt.right) + if right_oid is CID { + if b.errors != unsafe { nil } { + b.errors.error(.invalid_operation, SourceLocation{ + pos: stmt.Node.pos + }, 'cannot store from a constant value') + } + return + } + right := right_oid as RID + + mut inst := IRStore{ + id: b.functions[fid].insts.len + result: left + source: right + } + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + } + ExprStmt { + // Expression statement - just evaluate the expression for side effects + b.lower_expression(bbid, fid, stmt.expr) + } + MacroLiteralCommand { + // $ command - lower to IRMacroLiteralCmd + mut parts := []IRMacroCmdPart{} + for part in stmt.parts { + match part { + MacroLiteralText { + parts << IRMacroCmdPart(IRMacroCmdText{ + text: part.text + }) + } + MacroLiteralMacro { + refed, mrid := b.bb_handle_macro_expr(bbid, part.macro_expr) + parts << IRMacroCmdPart(IRMacroCmdMacro{ + value: mrid + is_ref: refed + }) + } + MacroLiteralString { + mut str_parts := []IRStringPart{} + str_lit := part.str_literal + if !str_lit.interpolated { + str_parts << IRStringPart(IRStringText{ + text: str_lit.value + }) + } else { + for str_part in str_lit.parts { + if !str_part.is_macro { + str_parts << IRStringPart(IRStringText{ + text: str_part.text + }) + } else { + macro_expr := str_part.macro_expr or { continue } + refed, mrid := b.bb_handle_macro_expr(bbid, macro_expr) + str_parts << IRStringPart(IRStringMacro{ + value: mrid + is_ref: refed + }) + } + } + } + parts << IRMacroCmdPart(IRMacroCmdString{ + parts: str_parts + }) + } + } + } + inst := IRMacroLiteralCmd{ + func: fid + id: b.functions[fid].insts.len + parts: parts + } + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + } + IfStmt { + // Lower the condition expression - actual branching is handled by terminators + b.lower_expression(bbid, fid, stmt.condition) + // Note: The then/else blocks are handled as separate basic blocks + // Terminators (IRBranch) are added in a later pass + } + Return { + // Return statement - handled as terminator instruction + if val := stmt.value { + ret_val := b.lower_expression(bbid, fid, val) + inst := IRReturn{ + id: b.functions[fid].insts.len + value: ret_val + } + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + } else { + inst := IRReturn{ + id: b.functions[fid].insts.len + value: none + } + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + } + } + StructDefinition, FunctionDefinition, NamespaceDefinition, + NamespaceImport, NamespaceAlias, FunctionInlineDefinition, Block { + // These shouldn't appear in basic blocks + if b.errors != unsafe { nil } { + b.errors.error(.unsupported_statement_location, empty_location(), 'invalid statement in basic block: ${stmt}') + } } } } @@ -536,3 +1135,147 @@ pub fn (mut b IRBuilder) fill_bb_insts(bbid BBID) { b.fill_bb_insts_stmt(bbid, fid, stmt) } } + +// Add terminator instructions to basic blocks based on CFG structure +pub fn (mut b IRBuilder) add_bb_terminator(bbid BBID) { + bb := b.basic_blocks[bbid] + fid := bb.function + + // Check if block already has a terminator (Return) + if bb.insts.len > 0 { + last_inst := b.functions[fid].insts[bb.insts[bb.insts.len - 1]] + if last_inst is IRReturn { + return // Already has terminator + } + } + + // Check what kind of terminator we need based on successors + match bb.successors.len { + 0 { + // No successors - this should have a return (implicit void return) + inst := IRReturn{ + id: b.functions[fid].insts.len + value: none + } + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + } + 1 { + // One successor - unconditional jump + target := bb.successors[0] + // Collect arguments for the target block + mut args := []OID{} + for arg in b.basic_blocks[target].args { + // Look up the value in current scope and pass it + ok, ref := b.bb_get_reference(arg.name, bbid, fid) + if ok { + args << OID(ref.id) + } else { + if b.errors != unsafe { nil } { + b.errors.error(.undefined_variable, empty_location(), "could not find argument '${arg.name}' to pass to block") + } + continue + } + } + inst := IRJump{ + id: b.functions[fid].insts.len + target: target + args: args + } + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + } + 2 { + // Two successors - conditional branch (if/else) + // The condition should be the last evaluated expression before branching + // Look for the IfStmt in the block's statements + mut cond_oid := OID(CID(-1)) + for stmt in bb.stmts { + if stmt is IfStmt { + cond_oid = b.lower_expression(bbid, fid, stmt.condition) + break + } + } + + then_bb := bb.successors[0] + else_bb := bb.successors[1] + + // Collect arguments for both branches + mut then_args := []OID{} + for arg in b.basic_blocks[then_bb].args { + ok, ref := b.bb_get_reference(arg.name, bbid, fid) + if ok { + then_args << OID(ref.id) + } + } + + mut else_args := []OID{} + for arg in b.basic_blocks[else_bb].args { + ok, ref := b.bb_get_reference(arg.name, bbid, fid) + if ok { + else_args << OID(ref.id) + } + } + + inst := IRBranch{ + id: b.functions[fid].insts.len + cond: cond_oid + then_bb: then_bb + then_args: then_args + else_bb: else_bb + else_args: else_args + } + b.functions[fid].insts << inst + b.basic_blocks[bbid].insts << inst.id + } + else { + if b.errors != unsafe { nil } { + b.errors.error(.internal_compiler_error, empty_location(), 'basic block has more than 2 successors, which is not supported') + } + } + } +} + +// Add terminators to all basic blocks in a function +pub fn (mut b IRBuilder) add_function_terminators(fid FID) { + for bbid in b.functions[fid].bbs { + b.add_bb_terminator(bbid) + } +} + +// Fill all instructions for all basic blocks in a function +pub fn (mut b IRBuilder) fill_function_insts(fid FID) { + for bbid in b.functions[fid].bbs { + b.fill_bb_insts(bbid) + } + // Add terminators after all instructions are filled + b.add_function_terminators(fid) +} + +// Solve basic block arguments - determine what values need to be passed between blocks +pub fn (mut b IRBuilder) solve_bb_arguments(fid FID) { + // For each basic block, check what variables it uses that are defined elsewhere + for bbid in b.functions[fid].bbs { + bb := b.basic_blocks[bbid] + + // Skip entry block - it gets arguments from function arguments + if bbid == b.functions[fid].entrybb { + continue + } + + // For blocks that need macro values, the args are already added by bb_handle_macro_expr + // This function ensures consistency and handles phi-like scenarios + + // Check each predecessor and ensure they can provide the required arguments + for pred_id in bb.predecessors { + pred := b.basic_blocks[pred_id] + for arg in bb.args { + ok, _, level := b.bb_get_reference_no_creation(arg.name, pred_id, fid) + if !ok { + // The predecessor doesn't have this value - it may need to get it from further up + // This is handled by the basic block argument passing in terminators + } + } + } + } +} diff --git a/ir_test.v b/ir_test.v index d0402c0..7348eee 100644 --- a/ir_test.v +++ b/ir_test.v @@ -2,35 +2,35 @@ module main pub fn test_nssolver_simple() { mut solver := NSSolver{} - solver.solve('tests/ir/namespace/simple/main.mcf')! + solver.solve('tests/ir/namespace/simple/main.mdl')! println(solver) assert solver.verify_legal() } pub fn test_nssolver_import() { mut solver := NSSolver{} - solver.solve('tests/ir/namespace/import/a.mcf')! + solver.solve('tests/ir/namespace/import/a.mdl')! println(solver) assert solver.verify_legal() } pub fn test_nssolver_child_import() { mut solver := NSSolver{} - solver.solve('tests/ir/namespace/child_import/main.mcf')! + solver.solve('tests/ir/namespace/child_import/main.mdl')! println(solver) assert solver.verify_legal() } pub fn test_nssolver_child_import_fail() { mut solver := NSSolver{} - solver.solve('tests/ir/namespace/child_import_fail/main.mcf')! + solver.solve('tests/ir/namespace/child_import_fail/main.mdl')! println(solver) assert !solver.verify_legal() } pub fn test_struct_single() { mut irb := IRBuilder{} - irb.lower(['tests/ir/structs/simple/main.mcf'])! + irb.lower(['tests/ir/structs/simple/main.mdl'])! p := irb.structs[0] assert p.name == 'Point' assert p.fields['x'] is BuiltinType @@ -38,7 +38,7 @@ pub fn test_struct_single() { pub fn test_struct_self_ref() { mut irb := IRBuilder{} - irb.lower(['tests/ir/structs/self_ref/main.mcf'])! + irb.lower(['tests/ir/structs/self_ref/main.mdl'])! print(irb.structs) assert irb.structs[0].fields['next'] == (IRType(IRRefType{ base: IRType(SID(0)) @@ -47,7 +47,7 @@ pub fn test_struct_self_ref() { pub fn test_struct_cross_file() { mut irb := IRBuilder{} - irb.lower(['tests/ir/structs/cross_file/main.mcf'])! + irb.lower(['tests/ir/structs/cross_file/main.mdl'])! person := irb.structs[0] assert person.name == 'Person' assert irb.namespaces[person.namespace].name == 'main' @@ -62,5 +62,5 @@ pub fn test_struct_cross_file() { pub fn test_struct_cross_file_fail() { mut irb := IRBuilder{} - assert false == irb.lower(['tests/ir/structs/cross_file_fail/main.mcf'])! + assert false == irb.lower(['tests/ir/structs/cross_file_fail/main.mdl'])! } diff --git a/lx.v b/lx.v index c98096a..25f7348 100644 --- a/lx.v +++ b/lx.v @@ -530,3 +530,43 @@ pub fn (mut l Lexer) next() ?Token { } return t } + +// Operator conversion methods for IR lowering +pub fn (t TokenKind) to_binary_op() BinaryOp { + return match t { + .plus { BinaryOp.add } + .minus { BinaryOp.sub } + .star { BinaryOp.mul } + .slash { BinaryOp.div } + .percent { BinaryOp.mod } + .eq { BinaryOp.eq } + .ne { BinaryOp.ne } + .lcarrot { BinaryOp.lt } + .rcarrot { BinaryOp.gt } + .lte { BinaryOp.le } + .gte { BinaryOp.ge } + else { panic('Token ${t} is not a binary operator') } + } +} + +pub fn (t TokenKind) to_unary_op() UnaryOp { + return match t { + .minus { UnaryOp.neg } + .ampersand { UnaryOp.ref } + .at { UnaryOp.deref } + else { panic('Token ${t} is not a unary operator') } + } +} + +pub fn (t TokenKind) to_assign_op() AssignOp { + return match t { + .assign { AssignOp.assign } + .plus_assign { AssignOp.add_assign } + .minus_assign { AssignOp.sub_assign } + .star_assign { AssignOp.mul_assign } + .slash_assign { AssignOp.div_assign } + .percent_assign { AssignOp.mod_assign } + .swap { AssignOp.swap } + else { panic('Token ${t} is not an assignment operator') } + } +} diff --git a/main.v b/main.v index 8699681..c5fb047 100644 --- a/main.v +++ b/main.v @@ -1,23 +1,46 @@ module main fn main() { - mut irb := IRBuilder{} - irb.lower(['tests/ir/bb_gen/stmts/assign.mcf'])! - irb.print_cfg_dot() - for mut bb in irb.basic_blocks { - irb.fill_bb_insts(bb.id) - print(irb.functions[bb.function].insts) - print('\n') - print(bb.insts) - print('\n') - for inst in bb.insts { - irb.fn_print_inst(bb.function, inst) - print('\n') - } - // print(irb.functions[bb.function].block) + // Initialize error manager + mut em := ErrorManager.new() + + // Get input files (for now using a test file) + files := ['tests/codegen/arithmetic.mdl'] + + // Load source files into error manager for error reporting + for f in files { + em.load_source(f) + } + + // Create IR builder with error manager + mut irb := IRBuilder{ + errors: &em + } + + // Run lowering + result := irb.lower(files) or { + em.print_all() + eprintln('Compilation failed.') + exit(1) } - // for func in irb.functions { - // println('func ${func.name}\n ${func.block}') - // } + // Check for errors + if em.has_errors() || !result { + em.print_all() + exit(1) + } + + // Print warnings if any + if em.warning_count > 0 { + em.print_all() + } + + // Generate mcfunction output + mut cg := Codegen.new(&irb, 'output', 'example') + cg.generate() or { + em.error(.internal_compiler_error, empty_location(), 'code generation failed: ${err}') + em.print_all() + exit(1) + } + println('Generated mcfunction files in output/') } diff --git a/parse.v b/parse.v index 73adeb8..7ef3b0d 100644 --- a/parse.v +++ b/parse.v @@ -1849,10 +1849,21 @@ fn (mut p Parser) parse_macro_literal_command() Stmt { p.consume(.dollar) mut parts := []MacroLiteralPart{} + mut needs_leading_space := false // Track if next text needs a leading space for p.current.kind != .semicolon && p.current.kind != .eof { match p.current.kind { .lcarrot { + // Add trailing space to previous text part if it exists and we're starting a macro + if parts.len > 0 { + last_idx := parts.len - 1 + if parts[last_idx] is MacroLiteralText { + mut last_text := parts[last_idx] as MacroLiteralText + last_text.text += ' ' + parts[last_idx] = MacroLiteralPart(last_text) + } + } + // Parse macro expression macro := p.parse_macro() parts << MacroLiteralPart(MacroLiteralMacro{ @@ -1861,6 +1872,7 @@ fn (mut p Parser) parse_macro_literal_command() Stmt { } macro_expr: macro }) + needs_leading_space = true // Next text should have a leading space } .lit_string { // Parse string literal (may contain macros) @@ -1897,13 +1909,20 @@ fn (mut p Parser) parse_macro_literal_command() Stmt { } str_literal: str_literal }) + needs_leading_space = false } else { // Raw literal text - accumulate all tokens until macro, string, or semicolon text_pos := p.current.pos mut text := '' - mut first := true + // Add leading space if we're coming after a macro + if needs_leading_space { + text = ' ' + } + needs_leading_space = false + + mut first := true for p.current.kind !in [.lcarrot, .lit_string, .semicolon, .eof] { if !first { text += ' ' diff --git a/semantic.v b/semantic.v new file mode 100644 index 0000000..74788d3 --- /dev/null +++ b/semantic.v @@ -0,0 +1,548 @@ +module main + +// ==================== Type Info for Scope Tracking ==================== + +pub struct TypeInfo { +pub: + typ IRType + storage StorageKind + pos int // Where it was defined +} + +// ==================== Semantic Context ==================== + +pub struct SemanticContext { +pub mut: + current_namespace NID + current_function ?FID + scope_stack []map[string]TypeInfo + return_type ?IRType + file string +} + +// ==================== Semantic Checker ==================== + +@[heap] +pub struct SemanticChecker { +pub mut: + builder &IRBuilder + errors &ErrorManager = unsafe { nil } // Shared error manager + context SemanticContext +} + +pub fn SemanticChecker.new(builder &IRBuilder) SemanticChecker { + return SemanticChecker{ + builder: builder + errors: builder.errors // Share error manager with IRBuilder + context: SemanticContext{ + scope_stack: []map[string]TypeInfo{} + } + } +} + +// ==================== Error Management ==================== + +pub fn (mut c SemanticChecker) add_error(kind ErrorKind, pos int, msg string) { + if c.errors != unsafe { nil } { + c.errors.error(kind, SourceLocation{ + file: c.context.file + pos: pos + }, msg) + } +} + +pub fn (mut c SemanticChecker) add_error_with_hint(kind ErrorKind, pos int, msg string, hint string) { + if c.errors != unsafe { nil } { + c.errors.error_with_hint(kind, SourceLocation{ + file: c.context.file + pos: pos + }, msg, hint) + } +} + +pub fn (mut c SemanticChecker) add_warning(kind ErrorKind, pos int, msg string) { + if c.errors != unsafe { nil } { + c.errors.warning(kind, SourceLocation{ + file: c.context.file + pos: pos + }, msg) + } +} + +pub fn (c &SemanticChecker) has_errors() bool { + if c.errors != unsafe { nil } { + return c.errors.has_errors() + } + return false +} + +pub fn (c &SemanticChecker) error_count() int { + if c.errors != unsafe { nil } { + return c.errors.error_count + } + return 0 +} + +pub fn (c &SemanticChecker) warning_count() int { + if c.errors != unsafe { nil } { + return c.errors.warning_count + } + return 0 +} + +// Print all accumulated errors (delegates to ErrorManager) +pub fn (c &SemanticChecker) print_errors() { + if c.errors != unsafe { nil } { + c.errors.print_all() + } +} + +// ==================== Scope Management ==================== + +pub fn (mut c SemanticChecker) scope_enter() { + c.context.scope_stack << map[string]TypeInfo{} +} + +pub fn (mut c SemanticChecker) scope_exit() { + if c.context.scope_stack.len > 0 { + c.context.scope_stack.pop() + } +} + +pub fn (mut c SemanticChecker) scope_define(name string, info TypeInfo) bool { + if c.context.scope_stack.len == 0 { + c.scope_enter() + } + + // Check if already defined in current scope + idx := c.context.scope_stack.len - 1 + if name in c.context.scope_stack[idx] { + return false // Already defined + } + + c.context.scope_stack[idx][name] = info + return true +} + +pub fn (c &SemanticChecker) scope_lookup(name string) ?TypeInfo { + // Search from innermost to outermost scope + for i := c.context.scope_stack.len - 1; i >= 0; i-- { + if name in c.context.scope_stack[i] { + return c.context.scope_stack[i][name] + } + } + return none +} + +// ==================== Type Utilities ==================== + +pub fn (c &SemanticChecker) types_compatible(expected IRType, actual IRType) bool { + // Exact match + if expected == actual { + return true + } + + // Check if both are reference types with compatible bases + match expected { + IRRefType { + if actual is IRRefType { + return c.types_compatible(expected.base, actual.base) + } + } + else {} + } + + return false +} + +pub fn (c &SemanticChecker) type_to_string(typ IRType) string { + match typ { + BuiltinType { + return match typ { + .int_t { 'Int' } + .float_t { 'Float' } + .char_t { 'Char' } + .string_t { 'String' } + .list_t { 'List' } + .dict_t { 'Dict' } + .void_t { 'Void' } + } + } + IRRefType { + return '&${c.type_to_string(typ.base)}' + } + SID { + if int(typ) < c.builder.structs.len { + return c.builder.structs[typ].name + } + return 'Struct#${typ}' + } + } +} + +// ==================== Stage 1 Checks ==================== + +pub fn (mut c SemanticChecker) check_stage1() bool { + initial_errors := c.error_count() + + // Set file context from builder + if c.builder.files.len > 0 { + c.context.file = c.builder.files[0] + } + + // Check each namespace + for ns in c.builder.namespaces { + c.context.current_namespace = ns.id + + // Check struct definitions + c.check_struct_definitions(ns) + + // Check function signatures + c.check_function_signatures(ns) + + // Check name collisions within namespace + c.check_namespace_names(ns) + } + + return c.error_count() == initial_errors +} + +fn (mut c SemanticChecker) check_struct_definitions(ns IRNamespace) { + for sid in ns.structs { + s := c.builder.structs[sid] + + // Check each field has a valid type + // s.fields is map[string]IRType + for field_name, field_type in s.fields { + if !c.is_valid_ir_type(ns, field_type) { + c.add_error(.undefined_struct, 0, + "field '${field_name}' in struct '${s.name}' has unknown type") + } + } + // Note: duplicate field names are impossible since fields is a map + } +} + +fn (mut c SemanticChecker) check_function_signatures(ns IRNamespace) { + for fid in ns.functions { + func := c.builder.functions[fid] + + // Check return type exists + if !c.is_valid_ir_type(ns, func.return_type) { + c.add_error(.undefined_struct, 0, + "function '${func.name}' has unknown return type") + } + + // Check argument types exist + mut seen_args := map[string]bool{} + for arg in func.args { + if !c.is_valid_ir_type(ns, arg.typ) { + c.add_error(.undefined_struct, 0, + "argument '${arg.name}' in function '${func.name}' has unknown type") + } + + // Check for duplicate argument names + if arg.name in seen_args { + c.add_error(.duplicate_definition, 0, + "duplicate argument '${arg.name}' in function '${func.name}'") + } + seen_args[arg.name] = true + } + } +} + +fn (mut c SemanticChecker) check_namespace_names(ns IRNamespace) { + mut all_names := map[string]bool{} + + // Collect function names + for fid in ns.functions { + func := c.builder.functions[fid] + if func.name in all_names { + c.add_error(.duplicate_definition, 0, + "'${func.name}' is already defined in namespace '${ns.name}'") + } + all_names[func.name] = true + } + + // Collect struct names + for sid in ns.structs { + s := c.builder.structs[sid] + if s.name in all_names { + c.add_error(.duplicate_definition, 0, + "'${s.name}' is already defined in namespace '${ns.name}'") + } + all_names[s.name] = true + } + + // Collect variable names + for name, _ in ns.variable_map { + if name in all_names { + c.add_error(.duplicate_definition, 0, + "'${name}' is already defined in namespace '${ns.name}'") + } + all_names[name] = true + } +} + +fn (c &SemanticChecker) is_valid_type(ns IRNamespace, typ Type) bool { + match typ { + BuiltinType { + return true + } + ReferenceType { + return c.is_valid_type(ns, typ.base) + } + StructType { + // Check if struct exists in namespace or parent namespaces + // For now, just check if the name is known + return true // TODO: full resolution + } + } +} + +fn (c &SemanticChecker) is_valid_ir_type(ns IRNamespace, typ IRType) bool { + match typ { + BuiltinType { + return true + } + IRRefType { + return c.is_valid_ir_type(ns, typ.base) + } + SID { + return int(typ) < c.builder.structs.len + } + } +} + +// ==================== Stage 2 Checks ==================== + +pub fn (mut c SemanticChecker) check_stage2() bool { + initial_errors := c.error_count() + + // Check each function body + for ns in c.builder.namespaces { + c.context.current_namespace = ns.id + + for fid in ns.functions { + c.check_function_body(fid) + } + } + + return c.error_count() == initial_errors +} + +fn (mut c SemanticChecker) check_function_body(fid FID) { + func := c.builder.functions[fid] + c.context.current_function = fid + c.context.return_type = func.return_type + + // Enter function scope + c.scope_enter() + + // Add function arguments to scope + for arg in func.args { + c.scope_define(arg.name, TypeInfo{ + typ: arg.typ + storage: arg.storage + pos: 0 + }) + } + + // Check each basic block's instructions + for bbid in func.bbs { + c.check_basic_block(bbid, fid) + } + + c.scope_exit() + c.context.current_function = none + c.context.return_type = none +} + +fn (mut c SemanticChecker) check_basic_block(bbid BBID, fid FID) { + bb := c.builder.basic_blocks[bbid] + func := c.builder.functions[fid] + + // Add BB args to scope + for arg in bb.args { + c.scope_define(arg.name, TypeInfo{ + typ: arg.typ + storage: arg.storage + pos: 0 + }) + } + + // Check each instruction + for iid in bb.insts { + c.check_instruction(func.insts[iid], fid) + } +} + +fn (mut c SemanticChecker) check_instruction(inst IRInstruction, fid FID) { + func := c.builder.functions[fid] + + match inst { + IRDefine { + // Define adds a variable - check RHS type + c.check_oid_valid(inst.value, fid) + } + IRTypedDefine { + // Just a declaration, type already checked in stage 1 + } + IRAssign { + // Check LHS and RHS types match + if left_type := c.get_rid_type(inst.result, fid) { + if right_type := c.get_oid_type(inst.value, fid) { + if !c.types_compatible(left_type, right_type) { + c.add_error(.type_mismatch, 0, + "cannot assign ${c.type_to_string(right_type)} to ${c.type_to_string(left_type)}") + } + } + } + } + IRStore { + // Store requires LHS to be a reference + if left_type := c.get_rid_type(inst.result, fid) { + if left_type !is IRRefType { + c.add_error(.invalid_dereference, 0, + "store target must be a reference type, got ${c.type_to_string(left_type)}") + } + } + } + IRBinaryOp { + // Check operands are numeric for arithmetic + if left_type := c.get_oid_type(inst.left, fid) { + if !c.is_numeric_type(left_type) { + c.add_error(.invalid_operation, 0, + "binary operator requires numeric type, got ${c.type_to_string(left_type)}") + } + } + if right_type := c.get_oid_type(inst.right, fid) { + if !c.is_numeric_type(right_type) { + c.add_error(.invalid_operation, 0, + "binary operator requires numeric type, got ${c.type_to_string(right_type)}") + } + } + } + IRCall { + // Check argument count + called_func := c.builder.functions[inst.function] + if inst.args.len != called_func.args.len { + c.add_error(.wrong_argument_count, 0, + "function '${called_func.name}' expects ${called_func.args.len} arguments, got ${inst.args.len}") + } else { + // Check argument types + for i, arg_oid in inst.args { + expected := called_func.args[i].typ + actual := c.get_oid_type(arg_oid, fid) or { continue } + + if !c.types_compatible(expected, actual) { + c.add_error(.wrong_argument_type, 0, + "argument ${i + 1} of '${called_func.name}': expected ${c.type_to_string(expected)}, got ${c.type_to_string(actual)}") + } + } + } + } + IRReturn { + // Check return type matches function + if ret_type := c.context.return_type { + if value := inst.value { + if actual := c.get_oid_type(value, fid) { + if !c.types_compatible(ret_type, actual) { + c.add_error(.type_mismatch, 0, + "return type mismatch: expected ${c.type_to_string(ret_type)}, got ${c.type_to_string(actual)}") + } + } + } else if ret_type != IRType(BuiltinType.void_t) { + c.add_error(.missing_return, 0, + "function must return a value of type ${c.type_to_string(ret_type)}") + } + } + } + IRDeref { + // Check operand is a reference type + if src_type := c.get_rid_type(inst.source, fid) { + if src_type !is IRRefType { + c.add_error(.invalid_dereference, 0, + "cannot dereference non-reference type ${c.type_to_string(src_type)}") + } + } + } + IRRefInst { + // Reference is always valid if source exists + c.check_rid_valid(inst.source, fid) + } + else { + // Other instructions don't need special checks + } + } +} + +fn (mut c SemanticChecker) check_oid_valid(oid OID, fid FID) { + match oid { + CID { + // Constants are always valid if they exist in the function + func := c.builder.functions[fid] + if int(oid) >= func.consts.len { + c.add_error(.undefined_variable, 0, 'invalid constant reference') + } + } + RID { + c.check_rid_valid(oid, fid) + } + } +} + +fn (mut c SemanticChecker) check_rid_valid(rid RID, fid FID) { + func := c.builder.functions[fid] + if int(rid) >= func.refs.len { + c.add_error(.undefined_variable, 0, 'invalid reference') + } +} + +fn (c &SemanticChecker) get_rid_type(rid RID, fid FID) ?IRType { + func := c.builder.functions[fid] + if int(rid) >= func.refs.len { + return none + } + return func.refs[rid].typ +} + +fn (c &SemanticChecker) get_oid_type(oid OID, fid FID) ?IRType { + func := c.builder.functions[fid] + match oid { + CID { + if int(oid) >= func.consts.len { + return none + } + cnst := func.consts[oid] + return match cnst { + IRIntConst { IRType(BuiltinType.int_t) } + IRFloatConst { IRType(BuiltinType.float_t) } + IRCharConst { IRType(BuiltinType.char_t) } + IRStringConst { IRType(BuiltinType.string_t) } + IRListConst { IRType(BuiltinType.list_t) } + IRDictConst { IRType(BuiltinType.dict_t) } + IRRangeConst { IRType(BuiltinType.list_t) } // Ranges are list-like + } + } + RID { + return c.get_rid_type(oid, fid) + } + } +} + +fn (c &SemanticChecker) is_numeric_type(typ IRType) bool { + return typ == IRType(BuiltinType.int_t) || typ == IRType(BuiltinType.float_t) +} + +// ==================== Main Entry Point ==================== + +pub fn (mut c SemanticChecker) check() bool { + // Source files are loaded by ErrorManager in main.v + + // Run both stages + stage1_ok := c.check_stage1() + stage2_ok := c.check_stage2() + + return stage1_ok && stage2_ok +} diff --git a/test.mcf b/test.mcf deleted file mode 100644 index 0f2469a..0000000 --- a/test.mcf +++ /dev/null @@ -1,13 +0,0 @@ -namespace my_namespace = "my_file"; -namespace my_other_namespace = { - fn my_function(data a: Int, data b: String): String { - return "HELlo World + \""; - } -}; - -$say $; -fn cat_strings(data first: *String,data second: String): String { - return ""; -} - -reg a = "hello"; diff --git a/tests/codegen/arithmetic.mdl b/tests/codegen/arithmetic.mdl new file mode 100644 index 0000000..6053f84 --- /dev/null +++ b/tests/codegen/arithmetic.mdl @@ -0,0 +1,29 @@ +// Test basic arithmetic operations with registers +namespace arithmetic; + +fn test_add() : Int { + reg a := 5; + reg b := 10; + reg c := a + b; + return c; +} + +fn test_sub() : Int { + reg x := 100; + reg y := 37; + return x - y; +} + +fn test_mul() : Int { + reg a := 7; + reg b := 8; + return a * b; +} + +fn test_compound() : Int { + reg result := 10; + result += 5; + result -= 3; + result *= 2; + return result; +} diff --git a/tests/codegen/conditionals.mdl b/tests/codegen/conditionals.mdl new file mode 100644 index 0000000..deef5fb --- /dev/null +++ b/tests/codegen/conditionals.mdl @@ -0,0 +1,38 @@ +// Test conditional statements +namespace cond; + +fn simple_if() : Int { + reg x := 10; + reg result := 0; + if (x > 5) { + result = 1; + } + return result; +} + +fn if_else() : Int { + reg a := 3; + reg b := 7; + reg max := 0; + if (a > b) { + max = a; + } else { + max = b; + } + return max; +} + +fn nested_if() : Int { + reg x := 50; + reg category := 0; + if (x > 100) { + category = 3; + } else { + if (x > 50) { + category = 2; + } else { + category = 1; + } + } + return category; +} diff --git a/tests/codegen/data_reg.mdl b/tests/codegen/data_reg.mdl new file mode 100644 index 0000000..d02384c --- /dev/null +++ b/tests/codegen/data_reg.mdl @@ -0,0 +1,35 @@ +// Test data and register variable interactions +namespace storage; + +fn data_to_reg() : Int { + data x := 42; + reg y := x; + return y; +} + +fn reg_to_data() : Int { + reg a := 100; + data b := a; + return b; +} + +fn mixed_arithmetic() : Int { + data a := 10; + reg b := 20; + reg result := a + b; + return result; +} + +fn all_data() : Int { + data x := 5; + data y := 10; + data z := x + y; + return z; +} + +fn all_reg() : Int { + reg a := 3; + reg b := 4; + reg c := a * b; + return c; +} diff --git a/tests/codegen/functions.mdl b/tests/codegen/functions.mdl new file mode 100644 index 0000000..edbca16 --- /dev/null +++ b/tests/codegen/functions.mdl @@ -0,0 +1,28 @@ +// Test function calls +namespace funcs; + +fn add(data a: Int, data b: Int) : Int { + return a + b; +} + +fn multiply(reg x: Int, reg y: Int) : Int { + return x * y; +} + +fn call_add() : Int { + data result := add(10, 20); + return result; +} + +fn call_multiply() : Int { + reg a := 5; + reg b := 6; + reg result := multiply(a, b); + return result; +} + +fn chain_calls() : Int { + data sum := add(1, 2); + data sum2 := add(sum, 3); + return sum2; +} diff --git a/tests/codegen/macros.mdl b/tests/codegen/macros.mdl new file mode 100644 index 0000000..cd00bb7 --- /dev/null +++ b/tests/codegen/macros.mdl @@ -0,0 +1,18 @@ +// Test macro commands +namespace macros; + +fn simple() : Void { + data name := "test"; + $say ; +} + +fn with_reg() : Void { + reg count := 64; + $say ; +} + +fn multiple_vars() : Void { + reg x := 10; + data y := "hello"; + $tp @p 64 ; +} diff --git a/tests/codegen/nested_ns.mdl b/tests/codegen/nested_ns.mdl new file mode 100644 index 0000000..720cfb4 --- /dev/null +++ b/tests/codegen/nested_ns.mdl @@ -0,0 +1,20 @@ +// Test nested namespaces +namespace outer; + +namespace inner = { + fn helper() : Int { + return 42; + } + + namespace deep = { + fn very_deep() : Int { + return 100; + } + }; +}; + +fn main() : Int { + data x := inner.helper(); + data y := inner.deep.very_deep(); + return x + y; +} diff --git a/tests/codegen/structs.mdl b/tests/codegen/structs.mdl new file mode 100644 index 0000000..2d6ff7b --- /dev/null +++ b/tests/codegen/structs.mdl @@ -0,0 +1,34 @@ +// Test struct usage +namespace structs; + +struct Point { + x: Int, + y: Int, + z: Int +} + +struct Player { + name: String, + health: Int, + pos: Point +} + +fn create_point() : Point { + data p := Point{x: 10, y: 64, z: -100}; + return p; +} + +fn access_field() : Int { + data p := Point{x: 5, y: 10, z: 15}; + data sum := p.x + p.y + p.z; + return sum; +} + +fn nested_struct() : Int { + data player := Player{ + name: "Steve", + health: 20, + pos: Point{x: 0, y: 64, z: 0} + }; + return player.pos.y; +} diff --git a/tests/errors/type_mismatch.mdl b/tests/errors/type_mismatch.mdl new file mode 100644 index 0000000..8927f93 --- /dev/null +++ b/tests/errors/type_mismatch.mdl @@ -0,0 +1,7 @@ +// Test file with type mismatch error +namespace test; + +fn main() : Void { + data x : Int; + x = "hello"; // Type mismatch: assigning String to Int +} diff --git a/tests/errors/undefined_function.mdl b/tests/errors/undefined_function.mdl new file mode 100644 index 0000000..a8974cb --- /dev/null +++ b/tests/errors/undefined_function.mdl @@ -0,0 +1,6 @@ +// Test file with undefined function error +namespace test; + +fn main() : Void { + nonexistent_function(); +} diff --git a/tests/errors/undefined_var.mdl b/tests/errors/undefined_var.mdl new file mode 100644 index 0000000..f9439d5 --- /dev/null +++ b/tests/errors/undefined_var.mdl @@ -0,0 +1,6 @@ +// Test file with an undefined variable error +namespace test; + +fn main() : Void { + $ say ; +} diff --git a/tests/ir/bb_gen/simple/main.mcf b/tests/ir/bb_gen/simple/main.mdl similarity index 89% rename from tests/ir/bb_gen/simple/main.mcf rename to tests/ir/bb_gen/simple/main.mdl index 28293e7..96a2038 100644 --- a/tests/ir/bb_gen/simple/main.mcf +++ b/tests/ir/bb_gen/simple/main.mdl @@ -10,7 +10,7 @@ fn tmp() : Void { } fn main() : Void { - data a := [10,20,30,b]; + data a := [10,20,30,30]; data a : Int; if (a == 2) { a.add(a,a); diff --git a/tests/ir/bb_gen/stmts/assign.mcf b/tests/ir/bb_gen/stmts/assign.mdl similarity index 100% rename from tests/ir/bb_gen/stmts/assign.mcf rename to tests/ir/bb_gen/stmts/assign.mdl diff --git a/tests/ir/bb_gen/stmts/define.mcf b/tests/ir/bb_gen/stmts/define.mdl similarity index 100% rename from tests/ir/bb_gen/stmts/define.mcf rename to tests/ir/bb_gen/stmts/define.mdl diff --git a/tests/ir/bb_gen/stmts/typed_define.mcf b/tests/ir/bb_gen/stmts/typed_define.mdl similarity index 100% rename from tests/ir/bb_gen/stmts/typed_define.mcf rename to tests/ir/bb_gen/stmts/typed_define.mdl diff --git a/tests/ir/namespace/child/main.mcf b/tests/ir/namespace/child/main.mdl similarity index 100% rename from tests/ir/namespace/child/main.mcf rename to tests/ir/namespace/child/main.mdl diff --git a/tests/ir/namespace/child_import/main.mcf b/tests/ir/namespace/child_import/main.mcf deleted file mode 100644 index b146470..0000000 --- a/tests/ir/namespace/child_import/main.mcf +++ /dev/null @@ -1,3 +0,0 @@ -namespace child = { -namespace utils = "utils.mcf"; -}; diff --git a/tests/ir/namespace/child_import/main.mdl b/tests/ir/namespace/child_import/main.mdl new file mode 100644 index 0000000..eefb204 --- /dev/null +++ b/tests/ir/namespace/child_import/main.mdl @@ -0,0 +1,3 @@ +namespace child = { +namespace utils = "utils.mdl"; +}; diff --git a/tests/ir/namespace/child_import/utils.mcf b/tests/ir/namespace/child_import/utils.mcf deleted file mode 100644 index 5ba9fe6..0000000 --- a/tests/ir/namespace/child_import/utils.mcf +++ /dev/null @@ -1,2 +0,0 @@ -namespace boingo; -namespace main = "main.mcf"; diff --git a/tests/ir/namespace/child_import/utils.mdl b/tests/ir/namespace/child_import/utils.mdl new file mode 100644 index 0000000..2b1a2bc --- /dev/null +++ b/tests/ir/namespace/child_import/utils.mdl @@ -0,0 +1,2 @@ +namespace boingo; +namespace main = "main.mdl"; diff --git a/tests/ir/namespace/child_import_fail/main.mcf b/tests/ir/namespace/child_import_fail/main.mdl similarity index 56% rename from tests/ir/namespace/child_import_fail/main.mcf rename to tests/ir/namespace/child_import_fail/main.mdl index a7f856a..14c2ad2 100644 --- a/tests/ir/namespace/child_import_fail/main.mcf +++ b/tests/ir/namespace/child_import_fail/main.mdl @@ -1,4 +1,4 @@ namespace child; namespace child = { -namespace utils = "utils.mcf"; +namespace utils = "utils.mdl"; }; diff --git a/tests/ir/namespace/child_import_fail/utils.mcf b/tests/ir/namespace/child_import_fail/utils.mcf deleted file mode 100644 index 5ba9fe6..0000000 --- a/tests/ir/namespace/child_import_fail/utils.mcf +++ /dev/null @@ -1,2 +0,0 @@ -namespace boingo; -namespace main = "main.mcf"; diff --git a/tests/ir/namespace/child_import_fail/utils.mdl b/tests/ir/namespace/child_import_fail/utils.mdl new file mode 100644 index 0000000..2b1a2bc --- /dev/null +++ b/tests/ir/namespace/child_import_fail/utils.mdl @@ -0,0 +1,2 @@ +namespace boingo; +namespace main = "main.mdl"; diff --git a/tests/ir/namespace/import/a.mcf b/tests/ir/namespace/import/a.mcf deleted file mode 100644 index 8df6cb2..0000000 --- a/tests/ir/namespace/import/a.mcf +++ /dev/null @@ -1,2 +0,0 @@ -namespace a; -namespace bin = "b.mcf"; diff --git a/tests/ir/namespace/import/a.mdl b/tests/ir/namespace/import/a.mdl new file mode 100644 index 0000000..b96b927 --- /dev/null +++ b/tests/ir/namespace/import/a.mdl @@ -0,0 +1,2 @@ +namespace a; +namespace bin = "b.mdl"; diff --git a/tests/ir/namespace/import/b.mcf b/tests/ir/namespace/import/b.mcf deleted file mode 100644 index f297501..0000000 --- a/tests/ir/namespace/import/b.mcf +++ /dev/null @@ -1,2 +0,0 @@ -namespace b; -namespace ain = "a.mcf"; diff --git a/tests/ir/namespace/import/b.mdl b/tests/ir/namespace/import/b.mdl new file mode 100644 index 0000000..a2876cb --- /dev/null +++ b/tests/ir/namespace/import/b.mdl @@ -0,0 +1,2 @@ +namespace b; +namespace ain = "a.mdl"; diff --git a/tests/ir/namespace/simple/main.mcf b/tests/ir/namespace/simple/main.mdl similarity index 100% rename from tests/ir/namespace/simple/main.mcf rename to tests/ir/namespace/simple/main.mdl diff --git a/tests/ir/structs/cross_file/attributes.mcf b/tests/ir/structs/cross_file/attributes.mdl similarity index 100% rename from tests/ir/structs/cross_file/attributes.mcf rename to tests/ir/structs/cross_file/attributes.mdl diff --git a/tests/ir/structs/cross_file/main.mcf b/tests/ir/structs/cross_file/main.mdl similarity index 71% rename from tests/ir/structs/cross_file/main.mcf rename to tests/ir/structs/cross_file/main.mdl index 161f26b..14994eb 100644 --- a/tests/ir/structs/cross_file/main.mcf +++ b/tests/ir/structs/cross_file/main.mdl @@ -1,4 +1,4 @@ -namespace attrs = "attributes.mcf" +namespace attrs = "attributes.mdl" struct Person{ age : Int name : Int diff --git a/tests/ir/structs/cross_file_fail/main.mcf b/tests/ir/structs/cross_file_fail/main.mdl similarity index 67% rename from tests/ir/structs/cross_file_fail/main.mcf rename to tests/ir/structs/cross_file_fail/main.mdl index e8bd92d..398e076 100644 --- a/tests/ir/structs/cross_file_fail/main.mcf +++ b/tests/ir/structs/cross_file_fail/main.mdl @@ -1,4 +1,4 @@ -namespace materials = "materials.mcf" +namespace materials = "materials.mdl" struct Bed{ thread_count : Int material : materials.Material diff --git a/tests/ir/structs/cross_file_fail/materials.mcf b/tests/ir/structs/cross_file_fail/materials.mdl similarity index 100% rename from tests/ir/structs/cross_file_fail/materials.mcf rename to tests/ir/structs/cross_file_fail/materials.mdl diff --git a/tests/ir/structs/self_ref/main.mcf b/tests/ir/structs/self_ref/main.mdl similarity index 100% rename from tests/ir/structs/self_ref/main.mcf rename to tests/ir/structs/self_ref/main.mdl diff --git a/tests/ir/structs/simple/main.mcf b/tests/ir/structs/simple/main.mdl similarity index 100% rename from tests/ir/structs/simple/main.mcf rename to tests/ir/structs/simple/main.mdl diff --git a/tests/semantic/errors.mdl b/tests/semantic/errors.mdl new file mode 100644 index 0000000..e1825c6 --- /dev/null +++ b/tests/semantic/errors.mdl @@ -0,0 +1,19 @@ +// Test file with intentional errors for semantic checking + +// Test 1: Wrong argument count +fn greet(data name: String) : Void { + $say "Hello"; +} + +fn test_wrong_args() : Void { + greet("Alice", "Bob"); // Error: too many arguments +} + +// Test 2: Type mismatch in function call +fn add(data a: Int, data b: Int) : Int { + return a; +} + +fn test_type_mismatch() : Void { + add("hello", 5); // Error: String passed where Int expected +} diff --git a/tests/types/lists/append/append.mdl b/tests/types/lists/append/append.mdl new file mode 100644 index 0000000..c281749 --- /dev/null +++ b/tests/types/lists/append/append.mdl @@ -0,0 +1,9 @@ +fn append(data test: &List, data value: Int) : Void { + $data modify append value ; +} + +fn doomer() : Void { + data l : List; + append(&l, 10); + $say ""; +} diff --git a/utils_ir.v b/utils_ir.v index 10fd013..4ea69d5 100644 --- a/utils_ir.v +++ b/utils_ir.v @@ -211,8 +211,167 @@ pub fn (mut b IRBuilder) fn_print_inst(fid FID, iid IID) { print(inst.op) b.fn_print_oid(fid, inst.right) } - else { - print('inst is not supported yes ${inst}') + IRUnaryOp { + print('unop ') + b.fn_print_rid(fid, inst.result) + print(' = ') + print(inst.op) + print(' ') + b.fn_print_oid(fid, inst.operand) + } + IRStore { + print('store ') + b.fn_print_rid(fid, inst.result) + print(' <- ') + b.fn_print_rid(fid, inst.source) + } + IRCall { + print('call ') + if result := inst.result { + b.fn_print_rid(fid, result) + print(' = ') + } + called_func := b.functions[inst.function] + print('${called_func.name}(') + for i, arg in inst.args { + if i > 0 { + print(', ') + } + b.fn_print_oid(fid, arg) + } + print(')') + } + IRStructInit { + print('struct_init ') + b.fn_print_rid(fid, inst.result) + print(' = ${b.structs[inst.struct_type].name}{') + mut first := true + for field_name, field_val in inst.field_values { + if !first { + print(', ') + } + first = false + print('${field_name}: ') + b.fn_print_oid(fid, field_val) + } + print('}') + } + IRFieldAccess { + print('field ') + b.fn_print_rid(fid, inst.result) + print(' = ') + b.fn_print_rid(fid, inst.source) + print('.${inst.field}') + } + IRIndexAccess { + print('index ') + b.fn_print_rid(fid, inst.result) + print(' = ') + b.fn_print_rid(fid, inst.source) + print('[') + b.fn_print_oid(fid, inst.index) + if inst.is_slice { + print('..') + if end := inst.end { + b.fn_print_oid(fid, end) + } + } + print(']') + } + IRDeref { + print('deref ') + b.fn_print_rid(fid, inst.result) + print(' = @') + b.fn_print_rid(fid, inst.source) + } + IRRefInst { + print('ref ') + b.fn_print_rid(fid, inst.result) + print(' = &') + b.fn_print_rid(fid, inst.source) + } + IRJump { + print('jump ') + target := b.basic_blocks[inst.target] + print('${target.label}_${target.id}') + if inst.args.len > 0 { + print('(') + for i, arg in inst.args { + if i > 0 { + print(', ') + } + b.fn_print_oid(fid, arg) + } + print(')') + } + } + IRBranch { + print('branch ') + b.fn_print_oid(fid, inst.cond) + then_bb := b.basic_blocks[inst.then_bb] + else_bb := b.basic_blocks[inst.else_bb] + print(' ? ${then_bb.label}_${then_bb.id}') + if inst.then_args.len > 0 { + print('(') + for i, arg in inst.then_args { + if i > 0 { + print(', ') + } + b.fn_print_oid(fid, arg) + } + print(')') + } + print(' : ${else_bb.label}_${else_bb.id}') + if inst.else_args.len > 0 { + print('(') + for i, arg in inst.else_args { + if i > 0 { + print(', ') + } + b.fn_print_oid(fid, arg) + } + print(')') + } + } + IRReturn { + print('return') + if value := inst.value { + print(' ') + b.fn_print_oid(fid, value) + } + } + IRMacroLiteralCmd { + print('macro_cmd $') + for part in inst.parts { + match part { + IRMacroCmdText { + print(part.text) + } + IRMacroCmdMacro { + if part.is_ref { + print('$(') + } else { + print('#(') + } + b.fn_print_rid(fid, part.value) + print(')') + } + IRMacroCmdString { + print('"') + for str_part in part.parts { + match str_part { + IRStringText { + print(str_part.text) + } + IRStringMacro { + print('#(rid)') + } + } + } + print('"') + } + } + } } } }