Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
dc68804
add examples
rafael-pf Apr 16, 2026
9b98fc0
add relative path to nix shell
manbr21 Apr 17, 2026
fa116a9
feat: add T* support in AST and parser
rafael-pf Apr 19, 2026
f3ecbbe
feat: add & and * support
rafael-pf Apr 19, 2026
b48b032
adress and deref todo in next phases
rafael-pf Apr 19, 2026
74b492b
tests: add tests for T*, &n and *p
rafael-pf Apr 19, 2026
792792a
refactor: Deleted unused folder
vlsmcin Apr 20, 2026
076d52e
feat: Create statement pointer tratative
vlsmcin Apr 20, 2026
3205cc9
tests: Create pointer statements unit tests
vlsmcin Apr 20, 2026
9533d5c
tests: Create integration tests
vlsmcin Apr 20, 2026
23f7fe0
docs: add pointer feature spec
rafael-pf Apr 20, 2026
c276f4c
Definition of the second milestone.
rbonifacio May 26, 2026
79317c1
Merge pull request #1 from rbonifacio/main
joaopbmarins May 27, 2026
ff73260
feat: implement typechecker
joaopbmarins May 27, 2026
3187657
add pointer_bindings to environment
joaopbmarins Jun 2, 2026
a5e7257
feat: add pointer interpreter
rafael-pf Jun 2, 2026
56b1c37
test: add type checker tests
rafael-pf Jun 2, 2026
652ee86
Merge branch 'main' into main
rafael-pf Jun 15, 2026
b737d4b
Merge branch 'rbonifacio:main' into main
joaopbmarins Jun 18, 2026
4789736
feat: add all tac logic
manbr21 Jun 29, 2026
d7fcc20
test: make stronger interpreter tests
manbr21 Jun 29, 2026
52a56b5
test: add AddrOf test coverage
manbr21 Jun 30, 2026
50e9cc5
Merge pull request #2 from vlsmcin/feat/implement_pointer_tac
manbr21 Jun 30, 2026
42bf74f
test: fix function return pointer
dsfs10 Jun 30, 2026
01a15e8
refactor(environment): separate lexical scope from memory store
manbr21 Jul 2, 2026
5d0f480
refactor(interpreter): update pointer evaluation to use numeric addre…
manbr21 Jul 2, 2026
8323613
refactor(interpreter): streamline block execution and pointer assignment
manbr21 Jul 2, 2026
0493aab
fix(semantic): align type checker snapshot with new environment signa…
manbr21 Jul 2, 2026
b3c1aa4
test(interpreter): update tests to validate stack and heap separation
manbr21 Jul 2, 2026
48c99b2
minor changes
manbr21 Jul 2, 2026
57130ca
Merge pull request #3 from vlsmcin/refactor/refactor_environment
joaopbmarins Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
];

shellHook = ''
export PS1="MiniC ❄️ > "
export PS1="MiniC ❄️ \[\033[01;34m\]\w\[\033[00m\] > "
'';
};
}
Expand Down
109 changes: 109 additions & 0 deletions openspec/specs/pointers/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Pointers

## Purpose

Document MiniC **pointer syntax** and **intended usage**: pointer types (`T*`),
address-of (`&expr`), dereference (`*expr`), assignment through a dereference
(`*expr = …`), and pointer-typed parameters and return values.

## Requirements

### Requirement: Pointer type notation

The parser SHALL recognise pointer types written as a scalar type name
immediately followed by `*`, with no space between them: `int*`, `float*`,
`bool*`, `str*`. Function parameters, return types, and local declarations
SHALL use this same spelling (as in the three fixture programs).

#### Scenario: Declaration with pointer type

- **WHEN** the source contains `int* y = &x` or `float* a_ref = &a`
- **THEN** the parser SHALL succeed and associate the declared name with
`Type::Pointer` to the corresponding scalar type

#### Scenario: Function signature with pointers

- **WHEN** the source contains `void increment(int* p)` or
`int* changeRef(int* x, int* y)`
- **THEN** the parser SHALL succeed with each pointer parameter typed as
`Pointer` to the inner scalar type

---

### Requirement: Address-of expression

The parser SHALL recognise the unary prefix `&` applied to an expression,
producing `Expr::AddrOf`. The fixtures use `&` only on simple identifiers
(`&x`, `&a`, …), which is the intended teaching subset.

#### Scenario: Initialisation from address of variable

- **WHEN** the input is `int* y = &x` or `bool* d_ref = &d`
- **THEN** the parser SHALL succeed with the initialiser as `AddrOf` wrapping
the identifier expression

#### Scenario: Address passed to a function

- **WHEN** the input is `increment(&x)` as in `pointer_feature.minic`
- **THEN** the parser SHALL succeed with the call argument as `AddrOf`

---

### Requirement: Dereference expression

The parser SHALL recognise the unary prefix `*` as dereference (same lexical
token as multiplication, resolved in the unary layer), producing `Expr::Deref`.

#### Scenario: Dereference on the right-hand side

- **WHEN** the input is `*p + 1` as in `pointer_feature.minic`
- **THEN** the parser SHALL succeed with `Deref` applied to the operand of `+`
as appropriate for unary-before-additive precedence

#### Scenario: Nested dereference in assignment target

- **WHEN** the input is `*p = *p + 1`
- **THEN** the parser SHALL succeed with assignment whose target expression is
`Deref` and whose value expression uses `Deref` on the same pointer

---

### Requirement: Assignment to a dereference

The parser SHALL accept assignment statements whose target is a dereference
expression `*expr`, as in `*p = *p + 1`.

#### Scenario: Mutate through pointer parameter

- **WHEN** the statement is `*p = *p + 1` inside `increment` in
`pointer_feature.minic`
- **THEN** the parser SHALL produce `Stmt::Assign` with a `Deref` target

---

### Requirement: Return type and return value as pointer

The parser SHALL allow a function to declare a pointer return type and
`return` an expression of pointer type, as in `changeRef` in
`pointer_function.minic`.

#### Scenario: Return pointer from function

- **WHEN** the function is `int* changeRef(int* x, int* y) { … return x; }`
- **THEN** the parser SHALL succeed with return type `Pointer(Int)` and a
return statement carrying the pointer expression

---

### Requirement: Assignment between pointer variables

The parser SHALL allow assignment where both sides are pointer-typed
expressions (e.g. `x = y` when `x` and `y` are `int*`), as in the body of
`changeRef` in `pointer_function.minic`.

#### Scenario: Rebind pointer parameter

- **WHEN** the statement is `x = y` with `x` and `y` declared as `int*`
parameters
- **THEN** the parser SHALL succeed with `Stmt::Assign` and identifier
expressions for `x` and `y`
44 changes: 33 additions & 11 deletions src/codegen/tac_code_gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ impl Environment {
}
}

#[allow(dead_code)]
fn translate_program(program: CheckedProgram, env: &mut Environment) -> TACProgram {
let main_fn = program.main_function();
match main_fn {
Expand All @@ -36,6 +37,7 @@ fn translate_program(program: CheckedProgram, env: &mut Environment) -> TACProgr

}

#[allow(dead_code)]
fn translate_function(function: CheckedFunDecl, env: &mut Environment) -> TACProgram {
let mut instructions =
if let Statement::Block { seq : stmts } = function.body.stmt {
Expand All @@ -55,16 +57,24 @@ pub fn translate_statement(statement: CheckedStmt, env: &mut Environment) -> Vec
seq.into_iter().flat_map(|s| translate_statement(s, env)).collect::<Vec<_>>()
},
Statement::Assign { target, value } => {
if let Expr::Ident(name) = &target.exp {
let var_type = target.ty.clone();
let var_address = Address::Variable(name.to_string(), var_type);
let (expression_address, instructions) = translate_expression(*value, env);
res.extend(instructions);
res.push(Instruction::CopyAssignment(var_address, expression_address));
res
}
else {
todo!()
match target.exp {
Expr::Ident(name) => {
let var_addr = Address::Variable(name.to_string(), target.ty);
let (val_addr, instrs) = translate_expression(*value, env);
res.extend(instrs);
res.push(Instruction::CopyAssignment(var_addr, val_addr));
res
},
Expr::Deref(inner) => {
let (ptr_addr, ptr_instrs) = translate_expression(*inner, env);
let (val_addr, val_instrs) = translate_expression(*value, env);
res.extend(ptr_instrs);
res.extend(val_instrs);
res.push(Instruction::DerefWrite(ptr_addr, val_addr));
res
},

_ => todo!()
}
},
Statement::Call{name, args} => {
Expand Down Expand Up @@ -164,7 +174,19 @@ fn translate_expression(expression: CheckedExpr, env: &mut Environment) -> (Addr
let temp = Address::Temporary(env.new_temporary(), expression.ty);
instructions.push(Instruction::BinaryAssignment(Operator::Add, temp.clone(), l_addr, r_addr));
(temp, instructions)
}
},
Expr::AddrOf(inner) => {
let (inner_addr, mut instructions) = translate_expression(*inner, env);
let temp = Address::Temporary(env.new_temporary(), expression.ty);
instructions.push(Instruction::AddressOf(temp.clone(), inner_addr));
(temp, instructions)
},
Expr::Deref(inner) => {
let (inner_addr, mut instructions) = translate_expression(*inner, env);
let temp = Address::Temporary(env.new_temporary(), expression.ty);
instructions.push(Instruction::DerefRead(temp.clone(), inner_addr));
(temp, instructions)
},
_ => todo!()
}
}
Expand Down
66 changes: 42 additions & 24 deletions src/environment/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,59 +53,77 @@
//! language. The cost — cloning the entire map on each function call — is
//! acceptable at MiniC's scale.

use std::collections::{HashMap, HashSet};
use std::collections::{HashMap};

/// Unified parametric environment: maps names to values of type `V`.
/// Both variable bindings and function bindings are stored in the same map.
/// Unified parametric environment representing program state.
/// It separates lexical bindings (`scopes`) from physical memory (`store`).
pub struct Environment<V> {
bindings: HashMap<String, V>,
scopes: HashMap<String, usize>,
store: HashMap<usize, V>,
next_address: usize,
}

impl<V: Clone> Environment<V> {
pub fn new() -> Self {
Self {
bindings: HashMap::new(),
scopes: HashMap::new(),
store: HashMap::new(),
next_address: 1,
}
}

/// Bind `name` to `value`, overwriting any existing binding.
/// Allocates a new address in the store for `value`, and binds `name` to this address.
pub fn declare(&mut self, name: impl Into<String>, value: V) {
self.bindings.insert(name.into(), value);
let addr = self.next_address;
self.next_address += 1;
self.store.insert(addr, value);
self.scopes.insert(name.into(), addr);
}

/// Look up a binding by name.
/// Looks up the value currently bound to `name` by resolving its physical address.
pub fn get(&self, name: &str) -> Option<&V> {
self.bindings.get(name)
let addr = self.scopes.get(name)?;
self.store.get(addr)
}

/// Update an existing binding. Returns `false` if the name is not bound.
/// Updates the value at the address currently bound to `name`. Returns `false` if the name is not found.
pub fn set(&mut self, name: &str, value: V) -> bool {
if self.bindings.contains_key(name) {
self.bindings.insert(name.to_string(), value);
if let Some(&addr) = self.scopes.get(name) {
self.store.insert(addr, value);
true
} else {
false
}
}

/// Capture a full clone of the current bindings (for function call scoping).
pub fn snapshot(&self) -> HashMap<String, V> {
self.bindings.clone()
/// Returns the physical address (usize) bound to `name`.
pub fn get_address(&self, name: &str) -> Option<usize> {
self.scopes.get(name).copied()
}

/// Replace all bindings with the given snapshot (for function call scoping).
pub fn restore(&mut self, snapshot: HashMap<String, V>) {
self.bindings = snapshot;
/// Reads a value directly from the memory store using its physical address.
pub fn read_store(&self, addr: usize) -> Option<&V> {
self.store.get(&addr)
}

/// Return the set of currently bound names (for block-entry capture).
pub fn names(&self) -> HashSet<String> {
self.bindings.keys().cloned().collect()
/// Writes a value directly to the memory store at the given physical address.
pub fn write_store(&mut self, addr: usize, value: V) -> bool {
if self.store.contains_key(&addr) {
self.store.insert(addr, value);
true
} else {
false
}
}

/// Captures a clone of the current lexical scope (names to addresses).
pub fn snapshot(&self) -> HashMap<String, usize> {
self.scopes.clone()
}

/// Remove any binding whose name is not in `outer` (for block-exit cleanup).
pub fn remove_new(&mut self, outer: &HashSet<String>) {
self.bindings.retain(|k, _| outer.contains(k));
/// Replaces the current lexical scope with a previously saved snapshot.
pub fn restore(&mut self, snapshot: HashMap<String, usize>) {
self.scopes = snapshot;
}
}

Expand Down
32 changes: 32 additions & 0 deletions src/interpreter/eval_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,32 @@ pub fn eval_expr(expr: &CheckedExpr, env: &mut Environment<Value>) -> Result<Val
args.iter().map(|a| eval_expr(a, env)).collect();
eval_call(name, arg_vals?, env)
}
Expr::AddrOf(elem) => eval_addr_of(elem, env),
Expr::Deref(elem) => eval_deref(elem, env),
}
}

fn eval_addr_of(elem: &CheckedExpr, env: &Environment<Value>) -> Result<Value, RuntimeError> {
match &elem.exp {
Expr::Ident(name) => {
if let Some(addr) = env.get_address(name) {
Ok(Value::Ptr(addr))
} else {
Err(RuntimeError::new(format!("undefined variable '{}'", name)))
}
}
_ => Err(RuntimeError::new("can only take address of variables")),
}
}

fn eval_deref(elem: &CheckedExpr, env: &mut Environment<Value>) -> Result<Value, RuntimeError> {
let ptr_val = eval_expr(elem, env)?;
if let Value::Ptr(addr) = ptr_val {
env.read_store(addr).cloned().ok_or_else(|| {
RuntimeError::new(format!("dereference of invalid address '{}'", addr))
})
} else {
Err(RuntimeError::new("cannot dereference non-pointer value"))
}
}

Expand All @@ -167,12 +193,17 @@ pub fn eval_call(
args.len()
)));
}

let snapshot = env.snapshot();

for ((param_name, _), val) in decl.params.iter().zip(args.into_iter()) {
env.declare(param_name.clone(), val);
}

let result = exec_stmt(&decl.body, env)?;

env.restore(snapshot);

Ok(result.unwrap_or(Value::Void))
}
Some(_) => Err(RuntimeError::new(format!("'{}' is not a function", name))),
Expand Down Expand Up @@ -235,6 +266,7 @@ fn values_equal(a: &Value, b: &Value) -> bool {
(Value::Float(x), Value::Int(y)) => *x == (*y as f64),
(Value::Bool(x), Value::Bool(y)) => x == y,
(Value::Str(x), Value::Str(y)) => x == y,
(Value::Ptr(x), Value::Ptr(y)) => x == y,
_ => false,
}
}
26 changes: 20 additions & 6 deletions src/interpreter/exec_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,16 @@ pub fn exec_stmt(stmt: &CheckedStmt, env: &mut Environment<Value>) -> ExecResult
// Only remove variables declared inside the block on exit.
// Assignments to outer-scope variables must persist (e.g., loop counters).
Statement::Block { seq } => {
let outer_keys = env.names();
for s in seq {
if let Some(ret) = exec_stmt(s, env)? {
env.remove_new(&outer_keys);
return Ok(Some(ret));
let snapshot = env.snapshot();

for stmt in seq {
if let Some(val) = exec_stmt(stmt, env)? {
env.restore(snapshot.clone());
return Ok(Some(val));
}
}
env.remove_new(&outer_keys);

env.restore(snapshot);
Ok(None)
}

Expand Down Expand Up @@ -158,6 +160,18 @@ fn assign_lvalue(
};
assign_index(base, idx, val, env)
}
Expr::Deref(inner) => {
let ptr_val = eval_expr(inner, env)?;
if let Value::Ptr(addr) = ptr_val {
if env.write_store(addr, val) {
Ok(())
} else {
Err(RuntimeError::new(format!("invalid memory address '{}'", addr)))
}
} else {
Err(RuntimeError::new("cannot assign to dereference of non-pointer"))
}
}
_ => Err(RuntimeError::new("invalid assignment target".to_string())),
}
}
Expand Down
Loading