Skip to content

Commit ad3df75

Browse files
authored
Merge pull request #449 from sugarlabs/gsoc-dmp-2025/week-7-8/safwan
GSoC/DMP 2025 Week 7-8: feat(runtime): Add IR Interpreter Module
2 parents 8f4c570 + 2a8ff71 commit ad3df75

30 files changed

+2010
-0
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { ExecutionContext } from './execution-context';
3+
import { IRProgram } from './ir-program';
4+
import { IRFunction } from './ir-function';
5+
6+
describe('ExecutionContext', () => {
7+
it('should be instantiated correctly', () => {
8+
const functions = new Map<string, IRFunction>();
9+
const program = new IRProgram(functions);
10+
const context = new ExecutionContext(program);
11+
12+
expect(context).toBeInstanceOf(ExecutionContext);
13+
expect(context.program).toBe(program);
14+
expect(context.callStack).toEqual([]);
15+
expect(context.isHalted).toBe(false);
16+
expect(context.instructionPointer).toEqual({
17+
functionName: '',
18+
blockLabel: '',
19+
instructionIndex: 0,
20+
});
21+
});
22+
23+
it('should initialize with empty call stack', () => {
24+
const functions = new Map<string, IRFunction>();
25+
const program = new IRProgram(functions);
26+
const context = new ExecutionContext(program);
27+
28+
expect(context.callStack.length).toBe(0);
29+
});
30+
31+
it('should allow modification of execution state', () => {
32+
const functions = new Map<string, IRFunction>();
33+
const program = new IRProgram(functions);
34+
const context = new ExecutionContext(program);
35+
36+
context.isHalted = true;
37+
expect(context.isHalted).toBe(true);
38+
39+
context.instructionPointer.functionName = 'start2';
40+
context.instructionPointer.blockLabel = 'entry';
41+
context.instructionPointer.instructionIndex = 5;
42+
43+
expect(context.instructionPointer.functionName).toBe('start2');
44+
expect(context.instructionPointer.blockLabel).toBe('entry');
45+
expect(context.instructionPointer.instructionIndex).toBe(5);
46+
});
47+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { IRProgram } from './ir-program';
2+
import { StackFrame } from './stack-frame';
3+
import { SimpleValueStore } from './testing/mock-program';
4+
import { SymbolTable } from '../execution/scope/symbol-table';
5+
6+
/**
7+
* ExecutionContext holds the full state of the interpreter at any moment.
8+
*/
9+
export class ExecutionContext {
10+
public instructionPointer: {
11+
functionName: string;
12+
blockLabel: string;
13+
instructionIndex: number;
14+
};
15+
public callStack: StackFrame[] = [];
16+
public isHalted: boolean = false;
17+
public valueStore: SimpleValueStore = new SimpleValueStore();
18+
public globalSymbolTable: SymbolTable = new SymbolTable();
19+
20+
constructor(public program: IRProgram) {
21+
this.instructionPointer = {
22+
functionName: '',
23+
blockLabel: '',
24+
instructionIndex: 0,
25+
};
26+
}
27+
28+
/**
29+
* Get the current symbol table (either from current stack frame or global).
30+
*/
31+
getCurrentSymbolTable(): SymbolTable {
32+
if (this.callStack.length > 0) {
33+
return this.callStack[this.callStack.length - 1].localScope;
34+
}
35+
return this.globalSymbolTable;
36+
}
37+
38+
/**
39+
* Get current instruction pointer as a readable string.
40+
*/
41+
getInstructionPointerString(): string {
42+
const { functionName, blockLabel, instructionIndex } = this.instructionPointer;
43+
return `${functionName}:${blockLabel}[${instructionIndex}]`;
44+
}
45+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { IRInterpreter } from './interpreter';
3+
import { createMockIRProgram } from './testing/mock-program';
4+
import { IRProgram } from './ir-program';
5+
import { IRFunction } from './ir-function';
6+
import { IRBasicBlock } from './ir-basic-block';
7+
import { SymDeclareInstruction } from './instructions/sym-declare-instruction';
8+
import { SymAssignInstruction } from './instructions/sym-assign-instruction';
9+
import { SymQueryInstruction } from './instructions/sym-query-instruction';
10+
import { CompareJumpInstruction } from './instructions/compare-jump-instruction';
11+
import { JumpInstruction } from './instructions/jump-instruction';
12+
import { CallInstruction } from './instructions/call-instruction';
13+
14+
describe('Full Mock Program Integration Test', () => {
15+
it('should execute a simplified version of the mock program with loops and conditionals', () => {
16+
const functions = new Map();
17+
18+
// Simplified start2 function with a simple loop
19+
const start2Function = new IRFunction('start2', [
20+
new IRBasicBlock('entry', [
21+
new CallInstruction('Clear', []),
22+
new SymDeclareInstruction('box1'),
23+
new SymAssignInstruction('box1', 0),
24+
new SymDeclareInstruction('i'),
25+
new SymAssignInstruction('i', 0),
26+
27+
// Declare variables used for SymQueryInstruction before the loop
28+
new SymDeclareInstruction('current_i'),
29+
new SymDeclareInstruction('current_box1'),
30+
31+
new JumpInstruction('loop_condition'),
32+
]),
33+
34+
new IRBasicBlock('loop_condition', [
35+
new CompareJumpInstruction('lessThan', 'i', 2, 'loop_body', 'loop_end'),
36+
]),
37+
38+
new IRBasicBlock('loop_body', [
39+
new CallInstruction('action1', [], false),
40+
new CallInstruction('action2', [], false),
41+
42+
// Use SymQueryInstruction to read current loop counter value
43+
new SymQueryInstruction('i', 'current_i'),
44+
45+
// Use SymQueryInstruction to read current box1 value for calculations
46+
new SymQueryInstruction('box1', 'current_box1'),
47+
48+
// Increment box1: box1 = box1 + 1
49+
new SymAssignInstruction('box1', { op: 'add', left: 'box1', right: 1 }),
50+
51+
// Increment i: i = i + 1
52+
new SymAssignInstruction('i', { op: 'add', left: 'i', right: 1 }),
53+
new JumpInstruction('loop_condition'),
54+
]),
55+
56+
new IRBasicBlock('loop_end', [
57+
// End of program - implicit halt
58+
]),
59+
]);
60+
61+
// Simple action1
62+
const action1Function = new IRFunction('action1', [
63+
new IRBasicBlock('entry', [new CallInstruction('PlayNote', ['D4', 0.25])]),
64+
]);
65+
66+
// action2 with conditional - demonstrates SymQueryInstruction usage without cross-scope variable access
67+
const action2Function = new IRFunction('action2', [
68+
new IRBasicBlock('entry', [
69+
new CallInstruction('Forward', [50]),
70+
71+
// Declare local variables to demonstrate SymQueryInstruction
72+
new SymDeclareInstruction('local_counter'),
73+
new SymAssignInstruction('local_counter', 1),
74+
new SymDeclareInstruction('local_flag'),
75+
new SymAssignInstruction('local_flag', true),
76+
77+
// Use SymQueryInstruction to read local variables before decision making
78+
new SymDeclareInstruction('counter_value'),
79+
new SymQueryInstruction('local_counter', 'counter_value'),
80+
81+
new CompareJumpInstruction(
82+
'greaterThan',
83+
'local_counter',
84+
0,
85+
'if_true',
86+
'if_false',
87+
),
88+
]),
89+
90+
new IRBasicBlock('if_true', [
91+
// Query the flag value for logging/decision purposes
92+
new SymDeclareInstruction('flag_status'),
93+
new SymQueryInstruction('local_flag', 'flag_status'),
94+
95+
new CallInstruction('Right', [90]),
96+
new JumpInstruction('end_if'),
97+
]),
98+
99+
new IRBasicBlock('if_false', [
100+
// Query counter value again in the false branch
101+
new SymDeclareInstruction('counter_in_false'),
102+
new SymQueryInstruction('local_counter', 'counter_in_false'),
103+
104+
new CallInstruction('Left', [90]),
105+
new JumpInstruction('end_if'),
106+
]),
107+
108+
new IRBasicBlock('end_if', [
109+
// Increment local counter to demonstrate variable modification
110+
new SymAssignInstruction('local_counter', {
111+
op: 'add',
112+
left: 'local_counter',
113+
right: 1,
114+
}),
115+
]),
116+
]);
117+
118+
functions.set('start2', start2Function);
119+
functions.set('action1', action1Function);
120+
functions.set('action2', action2Function);
121+
const program = new IRProgram(functions);
122+
123+
const interpreter = new IRInterpreter();
124+
interpreter.load(program);
125+
interpreter.run();
126+
127+
const context = interpreter.getContext()!;
128+
expect(context.isHalted).toBe(true);
129+
expect(context.callStack.length).toBe(0);
130+
131+
// Verify that variables have expected final values
132+
expect(context.valueStore.getValue('i')).toBe(2);
133+
expect(context.valueStore.getValue('box1')).toBe(2);
134+
});
135+
136+
it('should load and validate the full mock program structure without running', () => {
137+
const program = createMockIRProgram();
138+
const interpreter = new IRInterpreter();
139+
140+
interpreter.load(program);
141+
142+
const context = interpreter.getContext()!;
143+
expect(context.program).toBe(program);
144+
expect(context.instructionPointer.functionName).toBe('start2');
145+
expect(context.instructionPointer.blockLabel).toBe('entry');
146+
expect(context.instructionPointer.instructionIndex).toBe(0);
147+
148+
// Verify all expected functions are present
149+
expect(program.functions.has('start2')).toBe(true);
150+
expect(program.functions.has('action1')).toBe(true);
151+
expect(program.functions.has('action2')).toBe(true);
152+
153+
// Verify start2 has correct block structure
154+
const start2 = program.functions.get('start2')!;
155+
expect(start2.blocks.length).toBe(4);
156+
expect(start2.blocks.map((b: IRBasicBlock) => b.label)).toEqual([
157+
'entry',
158+
'loop_condition',
159+
'loop_body',
160+
'loop_end',
161+
]);
162+
163+
// Verify action2 has conditional blocks
164+
const action2 = program.functions.get('action2')!;
165+
expect(action2.blocks.length).toBe(4);
166+
expect(action2.blocks.map((b: IRBasicBlock) => b.label)).toEqual([
167+
'entry',
168+
'if_true',
169+
'if_false',
170+
'end_if',
171+
]);
172+
});
173+
});

0 commit comments

Comments
 (0)