From bd0bdd1ba1eaf4607cf501a8c546a9ff588b204a Mon Sep 17 00:00:00 2001 From: winlogon Date: Thu, 22 Jan 2026 03:01:10 +0100 Subject: [PATCH 1/2] feat(globals): add support for global variables Add parsing, type inference, and codegen for global variables and constants. This allows users to declare globals at module scope, including typed and inferred variables, with optional initializers. Summary of changes: - Introduce GlobalDecl in AST to represent globals - Extend parser to handle `var` and `const` at module level - Update TypeInferencer to infer global variable types - Update SymbolTable to track global variables - Modify codegen to emit global variable declarations in C - Add `examples/globals_*` and corresponding expected outputs - Add tests in `kitc/tests/examples.rs` for global examples --- examples/globals_basic.kit | 24 ++++++ examples/globals_basic.kit.expected | 9 +++ examples/globals_comprehensive.kit | 48 ++++++++++++ examples/globals_comprehensive.kit.expected | 17 ++++ examples/globals_test.kit | 17 ++++ examples/globals_test.kit.expected | 5 ++ kitc/tests/examples.rs | 17 +++- kitlang/src/codegen/ast.rs | 19 ++++- kitlang/src/codegen/frontend.rs | 87 +++++++++++++++++++-- kitlang/src/codegen/inference.rs | 40 +++++++++- kitlang/src/codegen/parser.rs | 28 ++++++- kitlang/src/codegen/symbols.rs | 16 +++- kitlang/src/grammar/kit.pest | 2 +- 13 files changed, 315 insertions(+), 14 deletions(-) create mode 100644 examples/globals_basic.kit create mode 100644 examples/globals_basic.kit.expected create mode 100644 examples/globals_comprehensive.kit create mode 100644 examples/globals_comprehensive.kit.expected create mode 100644 examples/globals_test.kit create mode 100644 examples/globals_test.kit.expected diff --git a/examples/globals_basic.kit b/examples/globals_basic.kit new file mode 100644 index 0000000..ab2a344 --- /dev/null +++ b/examples/globals_basic.kit @@ -0,0 +1,24 @@ +include "stdio.h"; + +var globalInt: Int = 42; +const globalConstInt: Int = 100; +var globalFloat: Float = 3.14; +const globalConstFloat: Float = 2.718; +var globalBool: Bool = true; +const globalConstBool: Bool = false; + +function main() { + printf("globalInt: %d\n", globalInt); + printf("globalConstInt: %d\n", globalConstInt); + printf("globalFloat: %f\n", globalFloat); + printf("globalConstFloat: %f\n", globalConstFloat); + printf("globalBool: %d\n", globalBool); + printf("globalConstBool: %d\n", globalConstBool); + + globalInt = 999; + globalFloat = 1.5; + globalBool = false; + printf("modified globalInt: %d\n", globalInt); + printf("modified globalFloat: %f\n", globalFloat); + printf("modified globalBool: %d\n", globalBool); +} diff --git a/examples/globals_basic.kit.expected b/examples/globals_basic.kit.expected new file mode 100644 index 0000000..6fe43f0 --- /dev/null +++ b/examples/globals_basic.kit.expected @@ -0,0 +1,9 @@ +globalInt: 42 +globalConstInt: 100 +globalFloat: 3.140000 +globalConstFloat: 2.718000 +globalBool: 1 +globalConstBool: 0 +modified globalInt: 999 +modified globalFloat: 1.500000 +modified globalBool: 0 diff --git a/examples/globals_comprehensive.kit b/examples/globals_comprehensive.kit new file mode 100644 index 0000000..3664ec0 --- /dev/null +++ b/examples/globals_comprehensive.kit @@ -0,0 +1,48 @@ +include "stdio.h"; + +var globalInt: Int = 42; +const globalConstInt: Int = 100; + +var globalFloat: Float = 3.14; +const globalConstFloat: Float = 2.718; + +var globalString = "hello"; +const globalConstString = "world"; + +var globalBool: Bool = true; +const globalConstBool: Bool = false; + +// Test global referencing another global +var globalRef: Int = 52; +const globalConstRef: Int = globalConstInt + 50; + +function main() { + printf("--- Basic Types ---\n"); + printf("globalInt: %d\n", globalInt); + printf("globalConstInt: %d\n", globalConstInt); + + printf("globalFloat: %f\n", globalFloat); + printf("globalConstFloat: %f\n", globalConstFloat); + + printf("globalString: %s\n", globalString); + printf("globalConstString: %s\n", globalConstString); + + printf("globalBool: %d\n", globalBool); + printf("globalConstBool: %d\n", globalConstBool); + + printf("--- References ---\n"); + printf("globalRef: %d\n", globalRef); + printf("globalConstRef: %d\n", globalConstRef); + + printf("--- Modification ---\n"); + + globalInt = 999; + globalFloat = 1.5; + globalString = "modified"; + globalBool = false; + + printf("modified globalInt: %d\n", globalInt); + printf("modified globalFloat: %f\n", globalFloat); + printf("modified globalString: %s\n", globalString); + printf("modified globalBool: %d\n", globalBool); +} diff --git a/examples/globals_comprehensive.kit.expected b/examples/globals_comprehensive.kit.expected new file mode 100644 index 0000000..036e894 --- /dev/null +++ b/examples/globals_comprehensive.kit.expected @@ -0,0 +1,17 @@ +--- Basic Types --- +globalInt: 42 +globalConstInt: 100 +globalFloat: 3.140000 +globalConstFloat: 2.718000 +globalString: hello +globalConstString: world +globalBool: 1 +globalConstBool: 0 +--- References --- +globalRef: 52 +globalConstRef: 150 +--- Modification --- +modified globalInt: 999 +modified globalFloat: 1.500000 +modified globalString: modified +modified globalBool: 0 diff --git a/examples/globals_test.kit b/examples/globals_test.kit new file mode 100644 index 0000000..ea6ecb8 --- /dev/null +++ b/examples/globals_test.kit @@ -0,0 +1,17 @@ +include "stdio.h"; + +var globalVar: Int = 42; +const globalConst: Int = 100; +var globalWithInference = 123; +const constWithInference = 456; + +function main() { + printf("globalVar: %d\n", globalVar); + printf("globalConst: %d\n", globalConst); + printf("globalWithInference: %d\n", globalWithInference); + printf("constWithInference: %d\n", constWithInference); + + // Test modifying global variable + globalVar = 999; + printf("modified globalVar: %d\n", globalVar); +} diff --git a/examples/globals_test.kit.expected b/examples/globals_test.kit.expected new file mode 100644 index 0000000..5eab97e --- /dev/null +++ b/examples/globals_test.kit.expected @@ -0,0 +1,5 @@ +globalVar: 42 +globalConst: 100 +globalWithInference: 123 +constWithInference: 456 +modified globalVar: 999 diff --git a/kitc/tests/examples.rs b/kitc/tests/examples.rs index d2690f9..a6c464a 100644 --- a/kitc/tests/examples.rs +++ b/kitc/tests/examples.rs @@ -1,4 +1,4 @@ -use assert_cmd::{Command as AssertCommand, cargo::*}; +use assert_cmd::{cargo::*, Command as AssertCommand}; use predicates::prelude::*; use std::{path::Path, process::Command, sync::OnceLock}; @@ -197,6 +197,21 @@ fn test_enum_defaults() -> Result<(), Box> { run_example_test("enum_defaults", None) } +#[test] +fn test_globals_basic() -> Result<(), Box> { + run_example_test("globals_basic", None) +} + +#[test] +fn test_globals_comprehensive() -> Result<(), Box> { + run_example_test("globals_comprehensive", None) +} + +#[test] +fn test_globals_test() -> Result<(), Box> { + run_example_test("globals_test", None) +} + #[test] fn test_nested_comments() -> Result<(), Box> { let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) diff --git a/kitlang/src/codegen/ast.rs b/kitlang/src/codegen/ast.rs index 34c8332..6829b48 100644 --- a/kitlang/src/codegen/ast.rs +++ b/kitlang/src/codegen/ast.rs @@ -191,7 +191,7 @@ pub enum Expr { }, } -/// Represents literal values in Kit. +/// Represents a literal value in Kit. #[derive(Clone, Debug, PartialEq)] pub enum Literal { /// Signed integer literal. @@ -206,6 +206,21 @@ pub enum Literal { Null, } +/// Represents a global variable or constant declaration. +#[derive(Clone, Debug, PartialEq)] +pub struct GlobalDecl { + /// Variable name. + pub name: String, + /// Type annotation (`None` for type inference). + pub annotation: Option, + /// Inferred variable type ID. + pub inferred: TypeId, + /// Initializer expression (`None` for uninitialized). + pub init: Option, + /// Whether this is a const declaration. + pub is_const: bool, +} + impl Literal { /// Converts the literal to its C representation string. #[must_use] @@ -247,6 +262,8 @@ pub struct Program { pub includes: Vec, /// Kit module imports (not directly used in C generation). pub imports: HashSet, + /// Top-level global variable and constant declarations. + pub globals: Vec, /// Top-level function definitions. pub functions: Vec, /// Struct type definitions. diff --git a/kitlang/src/codegen/frontend.rs b/kitlang/src/codegen/frontend.rs index 49248f6..3ddfe40 100644 --- a/kitlang/src/codegen/frontend.rs +++ b/kitlang/src/codegen/frontend.rs @@ -7,7 +7,7 @@ use std::fmt::Write; use std::path::{Path, PathBuf}; use std::process::Command; -use crate::codegen::ast::{Block, Expr, Function, Include, Program, Stmt}; +use crate::codegen::ast::{Block, Expr, Function, GlobalDecl, Include, Program, Stmt}; use crate::codegen::compiler::{CompilerMeta, CompilerOptions, Toolchain}; use crate::codegen::inference::TypeInferencer; use crate::codegen::parser::Parser as CodeParser; @@ -39,6 +39,7 @@ impl Compiler { fn parse(&mut self) -> CompileResult { let mut includes = Vec::new(); + let mut globals = Vec::new(); let mut functions = Vec::new(); let mut structs = Vec::new(); let mut enums = Vec::new(); @@ -65,6 +66,10 @@ impl Compiler { includes.push(self.parser.parse_include(pair)); } + Rule::var_decl => { + globals.push(self.parser.parse_global_var_decl(pair)?); + } + Rule::function_decl => { functions.push(self.parser.parse_function(pair)?); } @@ -95,6 +100,7 @@ impl Compiler { Ok(Program { includes, imports: HashSet::new(), + globals, functions, structs, enums, @@ -135,16 +141,36 @@ impl Compiler { } }; - // Emit struct declarations first + // Scan all types to gather required headers BEFORE emitting code that uses them + // Scan struct field types for struct_def in &prog.structs { - out.push_str(&self.generate_struct_declaration(struct_def, &prog.structs)); - out.push('\n'); + for field in &struct_def.fields { + if let Ok(ty) = self.inferencer.store.resolve(field.ty) { + collect_from_type(&ty); + } else if let Some(ann) = &field.annotation { + collect_from_type(ann); + } + } } - // Emit enum declarations + // Scan enum variant argument types for enum_def in &prog.enums { - out.push_str(&self.generate_enum_declaration(enum_def)); - out.push('\n'); + for variant in &enum_def.variants { + for arg in &variant.args { + if let Ok(ty) = self.inferencer.store.resolve(arg.ty) { + collect_from_type(&ty); + } else if let Some(ann) = &arg.annotation { + collect_from_type(ann); + } + } + } + } + + // Scan global variable types + for global in &prog.globals { + if let Ok(ty) = self.inferencer.store.resolve(global.inferred) { + collect_from_type(&ty); + } } // scan every function signature & body for types to gather their headers/typedefs @@ -188,6 +214,24 @@ impl Compiler { out.push('\n'); } + // Emit struct declarations + for struct_def in &prog.structs { + out.push_str(&self.generate_struct_declaration(struct_def, &prog.structs)); + out.push('\n'); + } + + // Emit enum declarations + for enum_def in &prog.enums { + out.push_str(&self.generate_enum_declaration(enum_def)); + out.push('\n'); + } + + // Emit global variable declarations + for global in &prog.globals { + out.push_str(&self.transpile_global(global)); + out.push('\n'); + } + // emit functions as before... for func in &prog.functions { out.push_str(&self.transpile_function(func)); @@ -196,6 +240,35 @@ impl Compiler { out } + fn transpile_global(&self, global: &GlobalDecl) -> String { + let ty = self.inferencer.store.resolve(global.inferred).map_or_else( + |_| "int".to_string(), + |t| { + if let Type::Named(name) = &t { + if self.inferencer.is_struct_type(name) { + format!("struct {}", name) + } else { + t.to_c_repr().name + } + } else { + t.to_c_repr().name + } + }, + ); + + let const_prefix = if global.is_const { "const " } else { "" }; + + match &global.init { + Some(expr) => { + let init_str = self.transpile_expr(expr); + format!("{const_prefix}{ty} {} = {init_str};", global.name) + } + None => { + format!("{const_prefix}{ty} {};", global.name) + } + } + } + fn generate_struct_declaration( &self, struct_def: &StructDefinition, diff --git a/kitlang/src/codegen/inference.rs b/kitlang/src/codegen/inference.rs index 6899364..7b8b340 100644 --- a/kitlang/src/codegen/inference.rs +++ b/kitlang/src/codegen/inference.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use super::Field; -use super::ast::{Block, Expr, Function, Literal, Program, Stmt}; +use super::ast::{Block, Expr, Function, GlobalDecl, Literal, Program, Stmt}; use super::symbols::{EnumVariantInfo, SymbolTable}; use super::type_ast::{EnumDefinition, FieldInit, StructDefinition}; use super::types::{BinaryOperator, Type, TypeId, TypeStore, UnaryOperator}; @@ -44,12 +44,44 @@ impl TypeInferencer { self.register_enum_types(&prog.enums)?; self.register_struct_types(&prog.structs)?; + // Infer global variable types first (before functions) + self.infer_globals(&mut prog.globals)?; + for func in &mut prog.functions { self.infer_function(func)?; } Ok(()) } + /// Infer types for global variable declarations + fn infer_globals(&mut self, globals: &mut [GlobalDecl]) -> CompileResult<()> { + for global in globals { + if let Some(init_expr) = &mut global.init { + let init_ty = self.infer_expr(init_expr)?; + + global.inferred = if let Some(ann) = &global.annotation { + let ann_ty = self.store.new_known(ann.clone()); + self.unify(ann_ty, init_ty)?; + ann_ty + } else { + init_ty + }; + + self.symbols.define_global(&global.name, global.inferred); + } else if let Some(ann) = &global.annotation { + // Declaration without initializer -> just use annotation + global.inferred = self.store.new_known(ann.clone()); + self.symbols.define_global(&global.name, global.inferred); + } else { + return Err(CompilationError::TypeError(format!( + "Global variable '{}' declared without type annotation or initializer", + global.name + ))); + } + } + Ok(()) + } + /// Register enum types in the type store and symbol table fn register_enum_types(&mut self, enums: &[EnumDefinition]) -> CompileResult<()> { for enum_def in enums { @@ -264,7 +296,11 @@ impl TypeInferencer { fn infer_expr(&mut self, expr: &mut Expr) -> Result { let ty = match expr { Expr::Identifier(name, ty_id) => { - if let Some(var_ty) = self.symbols.lookup_var(name) { + // First check if it's a global variable + if let Some(global_ty) = self.symbols.lookup_global(name) { + *ty_id = global_ty; + global_ty + } else if let Some(var_ty) = self.symbols.lookup_var(name) { *ty_id = var_ty; var_ty } else { diff --git a/kitlang/src/codegen/parser.rs b/kitlang/src/codegen/parser.rs index f725d65..907a4f3 100644 --- a/kitlang/src/codegen/parser.rs +++ b/kitlang/src/codegen/parser.rs @@ -4,7 +4,7 @@ use crate::codegen::types::{BinaryOperator, UnaryOperator}; use crate::error::CompilationError; use crate::{Rule, parse_error}; -use super::ast::{Block, Expr, Function, Include, Literal, Param, Stmt}; +use super::ast::{Block, Expr, Function, GlobalDecl, Include, Literal, Param, Stmt}; use super::type_ast::{EnumDefinition, EnumVariant, Field, FieldInit, StructDefinition}; use super::types::{AssignmentOperator, Type, TypeId}; use crate::error::CompileResult; @@ -372,6 +372,32 @@ impl Parser { }) } + pub fn parse_global_var_decl(&self, pair: Pair) -> CompileResult { + // Parse a global variable or constant declaration at module level + let name = Self::extract_first_identifier(pair.clone()) + .ok_or(parse_error!("global var_decl missing identifier"))?; + + let is_const = Self::is_const_var_decl(pair.clone()); + + // Parse type annotation if present + let annotation = Self::extract_type_annotation(pair.clone()) + .map(|type_pair| self.parse_type(type_pair)) + .transpose()?; + + // Parse initializer expression if present + let init = Self::extract_init_expr(pair.clone()) + .map(|expr_pair| self.parse_expr(expr_pair)) + .transpose()?; + + Ok(GlobalDecl { + name, + annotation, + inferred: TypeId::default(), + init, + is_const, + }) + } + /// Extract initializer expression from a var_decl pair fn extract_init_expr(pair: Pair<'_, Rule>) -> Option> { pair.into_inner().find(|p| p.as_rule() == Rule::expr) diff --git a/kitlang/src/codegen/symbols.rs b/kitlang/src/codegen/symbols.rs index 383304d..0305fdc 100644 --- a/kitlang/src/codegen/symbols.rs +++ b/kitlang/src/codegen/symbols.rs @@ -16,7 +16,10 @@ pub struct EnumVariantInfo { /// Currently uses a flat scope (no nesting). Variables and functions are tracked /// by their names and their `TypeId`s. pub struct SymbolTable { - /// Maps variable names to their inferred `TypeId`s. + /// Maps global variable names to their inferred `TypeId`s. + globals: HashMap, + + /// Maps local variable names to their inferred `TypeId`s. vars: HashMap, /// Maps function names to their signatures (parameter types, return type). @@ -41,6 +44,7 @@ impl Default for SymbolTable { impl SymbolTable { pub fn new() -> Self { Self { + globals: HashMap::new(), vars: HashMap::new(), functions: HashMap::new(), structs: HashMap::new(), @@ -49,6 +53,16 @@ impl SymbolTable { } } + /// Define a global variable in the symbol table. + pub fn define_global(&mut self, name: &str, ty: TypeId) { + self.globals.insert(name.to_string(), ty); + } + + /// Look up a global variable's type. + pub fn lookup_global(&self, name: &str) -> Option { + self.globals.get(name).copied() + } + /// Define a variable in the current scope. pub fn define_var(&mut self, name: &str, ty: TypeId) { self.vars.insert(name.to_string(), ty); diff --git a/kitlang/src/grammar/kit.pest b/kitlang/src/grammar/kit.pest index 7dcaeaf..1a568e4 100644 --- a/kitlang/src/grammar/kit.pest +++ b/kitlang/src/grammar/kit.pest @@ -7,7 +7,7 @@ COMMENT_LINE = _{ "//" ~ (!NEWLINE ~ ANY)* ~ NEWLINE } var_kw = { "var" } const_kw = { "const" } -program = _{ SOI ~ (import_stmt | include_stmt | function_decl | type_def | trait_def | trait_impl | rule_set | using_stmt)* ~ EOI } +program = _{ SOI ~ (import_stmt | include_stmt | function_decl | var_decl | type_def | trait_def | trait_impl | rule_set | using_stmt)* ~ EOI } import_stmt = { "import" ~ path ~ ("." ~ ("*" | "**"))? ~ ";" } include_stmt = { "include" ~ string ~ ("=>" ~ string)? ~ ";" } From d63894605d843de55907ee9fd7c0536682a04620 Mon Sep 17 00:00:00 2001 From: winlogon Date: Thu, 22 Jan 2026 03:38:37 +0100 Subject: [PATCH 2/2] chore: run cargo fmt --- kitc/tests/examples.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kitc/tests/examples.rs b/kitc/tests/examples.rs index a6c464a..bc670d7 100644 --- a/kitc/tests/examples.rs +++ b/kitc/tests/examples.rs @@ -1,4 +1,4 @@ -use assert_cmd::{cargo::*, Command as AssertCommand}; +use assert_cmd::{Command as AssertCommand, cargo::*}; use predicates::prelude::*; use std::{path::Path, process::Command, sync::OnceLock};