Skip to content

Commit 95487a2

Browse files
authored
Merge pull request #440 from sugarlabs/gsoc-dmp-2025/week-2/safwan
GSoC/DMP 2025 Week 2: feat(program): Add Memory Module
2 parents b2624f2 + 6ac786c commit 95487a2

File tree

12 files changed

+3083
-0
lines changed

12 files changed

+3083
-0
lines changed

modules/runtime/src/@types/scope.d.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
export interface ILayeredMap<T extends object> {
2+
rootID: string;
3+
addFrame(frameParentID: string): string;
4+
removeFrame(frameID: string): void;
5+
updateFrameKeyMap(frameID: string, keyMap: T): void;
6+
projectFlatMap(frameID: string): T;
7+
}
8+
9+
export interface IContextStack<T extends object> {
10+
/** Unique ID for this stack (second frame in its chain) */
11+
readonly contextID: string;
12+
13+
/** Read/write the global frame’s entire key-map */
14+
contextGlobalKeyMap: T;
15+
16+
/** Read/write the current (top) frame’s key-map */
17+
contextLocalKeyMap: T;
18+
19+
/** Enter a new local frame */
20+
pushFrame(): void;
21+
22+
/** Exit the current local frame */
23+
popFrame(): void;
24+
}
25+
26+
// factory fnc
27+
export interface IContextManager<T extends object> {
28+
/** Create a new independent context stack */
29+
createContextStack(): IContextStack<T>;
30+
31+
/** Fetch an existing stack by its ID */
32+
getContextStack(contextStackID: string): IContextStack<T>;
33+
34+
/** Tear down an existing stack by its ID */
35+
removeContextStack(contextStackID: string): void;
36+
37+
/** Read-only view of the global frame */
38+
readonly contextGlobalKeyMap: T;
39+
40+
/**
41+
* Overwrite the global frame’s key-map.
42+
* @param commit if true, also update the “reset” baseline
43+
*/
44+
updateContextGlobalKeyMap(keyMap: T, commit?: boolean): void;
45+
46+
/** Reset everything back to the original global map */
47+
reset(): void;
48+
}
49+
50+
export interface IThreadContext<T extends object> {
51+
/** The unique ID of this thread’s context stack */
52+
readonly id: string;
53+
54+
/** Local frame CRUD */
55+
getLocal<K extends keyof T>(key: K): T[K] | undefined;
56+
setLocal<K extends keyof T>(key: K, value: T[K]): void;
57+
deleteLocal(key: keyof T): void;
58+
59+
/** Scope push/pop */
60+
pushScope(): void;
61+
popScope(): void;
62+
63+
/** Global frame CRUD */
64+
getGlobal<K extends keyof T>(key: K): T[K] | undefined;
65+
setGlobal<K extends keyof T>(key: K, value: T[K]): void;
66+
deleteGlobal(key: keyof T): void;
67+
}
68+
69+
export interface IThreadManager<T extends object> {
70+
/** Spawn a new thread (with its own local-stack) */
71+
createThread(): IThreadContext<T>;
72+
73+
/** Look up an existing thread by ID */
74+
getThread(threadID: string): IThreadContext<T>;
75+
76+
/** Tear down a thread by ID */
77+
deleteThread(threadID: string): void;
78+
}
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import { ContextManager } from './context';
2+
import type { IContextStack } from '../../@types/scope';
3+
4+
type TContextDummy = { foo: number; bar: string };
5+
6+
// -------------------------------------------------------------------------------------------------
7+
8+
describe('Context module', () => {
9+
const contextManager = new ContextManager<TContextDummy>({
10+
foo: 4,
11+
bar: 'red',
12+
});
13+
14+
let contextStack0: IContextStack<TContextDummy>;
15+
let contextStack1: IContextStack<TContextDummy>;
16+
17+
describe('Context stack', () => {
18+
beforeAll(() => {
19+
contextStack0 = contextManager.createContextStack();
20+
});
21+
22+
it('returns global context', () => {
23+
expect(
24+
Object.entries(contextStack0.contextGlobalKeyMap)
25+
.sort(([a], [b]) => a.localeCompare(b))
26+
.map(([k, v]) => `${k}-${v}`)
27+
.join(', '),
28+
).toBe('bar-red, foo-4');
29+
});
30+
31+
it('updates local key-map', () => {
32+
contextStack0.contextLocalKeyMap = {
33+
...contextStack0.contextLocalKeyMap,
34+
foo: 8,
35+
};
36+
37+
expect(
38+
Object.entries(contextStack0.contextLocalKeyMap)
39+
.sort(([a], [b]) => a.localeCompare(b))
40+
.map(([k, v]) => `${k}-${v}`)
41+
.join(', '),
42+
).toBe('bar-red, foo-8');
43+
44+
// Global must stay unchanged
45+
expect(
46+
Object.entries(contextStack0.contextGlobalKeyMap)
47+
.sort(([a], [b]) => a.localeCompare(b))
48+
.map(([k, v]) => `${k}-${v}`)
49+
.join(', '),
50+
).toBe('bar-red, foo-4');
51+
});
52+
53+
it("pushes context frame, and updates new frame's key-map", () => {
54+
contextStack0.pushFrame();
55+
contextStack0.contextLocalKeyMap = {
56+
...contextStack0.contextLocalKeyMap,
57+
bar: 'blue',
58+
};
59+
60+
expect(
61+
Object.entries(contextStack0.contextLocalKeyMap)
62+
.sort(([a], [b]) => a.localeCompare(b))
63+
.map(([k, v]) => `${k}-${v}`)
64+
.join(', '),
65+
).toBe('bar-blue, foo-8');
66+
67+
expect(
68+
Object.entries(contextStack0.contextGlobalKeyMap)
69+
.sort(([a], [b]) => a.localeCompare(b))
70+
.map(([k, v]) => `${k}-${v}`)
71+
.join(', '),
72+
).toBe('bar-red, foo-4');
73+
});
74+
75+
it('pops (previously pushed) context frames', () => {
76+
contextStack0.popFrame();
77+
expect(
78+
Object.entries(contextStack0.contextLocalKeyMap)
79+
.sort(([a], [b]) => a.localeCompare(b))
80+
.map(([k, v]) => `${k}-${v}`)
81+
.join(', '),
82+
).toBe('bar-red, foo-8');
83+
});
84+
85+
it('throws error on popping root context frame', () => {
86+
expect(() => {
87+
contextStack0.popFrame();
88+
}).toThrowError('InvalidOperationError: No context frame remaining to pop');
89+
});
90+
});
91+
92+
describe('Context Manager', () => {
93+
beforeAll(() => {
94+
contextStack1 = contextManager.createContextStack();
95+
});
96+
97+
it('creates multiple context stacks having independent behavior', () => {
98+
contextStack0.contextLocalKeyMap = {
99+
...contextStack0.contextLocalKeyMap,
100+
foo: 6,
101+
};
102+
contextStack1.contextLocalKeyMap = {
103+
...contextStack0.contextLocalKeyMap,
104+
foo: 7,
105+
};
106+
107+
expect(
108+
Object.entries(contextStack0.contextLocalKeyMap)
109+
.sort(([a], [b]) => a.localeCompare(b))
110+
.map(([k, v]) => `${k}-${v}`)
111+
.join(', '),
112+
).toBe('bar-red, foo-6');
113+
expect(
114+
Object.entries(contextStack1.contextLocalKeyMap)
115+
.sort(([a], [b]) => a.localeCompare(b))
116+
.map(([k, v]) => `${k}-${v}`)
117+
.join(', '),
118+
).toBe('bar-red, foo-7');
119+
120+
contextStack0.pushFrame();
121+
contextStack0.contextLocalKeyMap = {
122+
...contextStack0.contextLocalKeyMap,
123+
bar: 'yellow',
124+
};
125+
contextStack1.pushFrame();
126+
contextStack1.contextLocalKeyMap = {
127+
...contextStack1.contextLocalKeyMap,
128+
bar: 'green',
129+
};
130+
131+
expect(
132+
Object.entries(contextStack0.contextLocalKeyMap)
133+
.sort(([a], [b]) => a.localeCompare(b))
134+
.map(([k, v]) => `${k}-${v}`)
135+
.join(', '),
136+
).toBe('bar-yellow, foo-6');
137+
expect(
138+
Object.entries(contextStack1.contextLocalKeyMap)
139+
.sort(([a], [b]) => a.localeCompare(b))
140+
.map(([k, v]) => `${k}-${v}`)
141+
.join(', '),
142+
).toBe('bar-green, foo-7');
143+
144+
contextStack0.popFrame();
145+
expect(
146+
Object.entries(contextStack0.contextLocalKeyMap)
147+
.sort(([a], [b]) => a.localeCompare(b))
148+
.map(([k, v]) => `${k}-${v}`)
149+
.join(', '),
150+
).toBe('bar-red, foo-6');
151+
expect(
152+
Object.entries(contextStack1.contextLocalKeyMap)
153+
.sort(([a], [b]) => a.localeCompare(b))
154+
.map(([k, v]) => `${k}-${v}`)
155+
.join(', '),
156+
).toBe('bar-green, foo-7');
157+
158+
contextStack1.popFrame();
159+
expect(
160+
Object.entries(contextStack0.contextLocalKeyMap)
161+
.sort(([a], [b]) => a.localeCompare(b))
162+
.map(([k, v]) => `${k}-${v}`)
163+
.join(', '),
164+
).toBe('bar-red, foo-6');
165+
expect(
166+
Object.entries(contextStack1.contextLocalKeyMap)
167+
.sort(([a], [b]) => a.localeCompare(b))
168+
.map(([k, v]) => `${k}-${v}`)
169+
.join(', '),
170+
).toBe('bar-red, foo-7');
171+
});
172+
173+
it('updates global context key-map from one context stack that reflects on another', () => {
174+
contextStack0.contextGlobalKeyMap = {
175+
foo: 5,
176+
bar: 'purple',
177+
};
178+
expect(
179+
Object.entries(contextStack1.contextGlobalKeyMap)
180+
.sort(([a], [b]) => a.localeCompare(b))
181+
.map(([k, v]) => `${k}-${v}`)
182+
.join(', '),
183+
).toBe('bar-purple, foo-5');
184+
185+
contextStack1.contextGlobalKeyMap = {
186+
foo: 3,
187+
bar: 'lime',
188+
};
189+
expect(
190+
Object.entries(contextStack0.contextGlobalKeyMap)
191+
.sort(([a], [b]) => a.localeCompare(b))
192+
.map(([k, v]) => `${k}-${v}`)
193+
.join(', '),
194+
).toBe('bar-lime, foo-3');
195+
});
196+
197+
it('fetches context stack using existing context stack ID', () => {
198+
const cs = contextManager.getContextStack(contextStack0.contextID);
199+
expect(
200+
Object.entries(cs.contextLocalKeyMap)
201+
.sort(([a], [b]) => a.localeCompare(b))
202+
.map(([k, v]) => `${k}-${v}`)
203+
.join(', '),
204+
).toBe('bar-red, foo-6');
205+
});
206+
207+
it('throws error on fetching context stack using non-existing context stack ID', () => {
208+
expect(() => {
209+
contextManager.getContextStack('foobar');
210+
}).toThrowError(`InvalidAccessError: Context stack with ID "foobar" doesn't exist`);
211+
});
212+
213+
it('removes context stack using existing context stack ID', () => {
214+
expect(() => {
215+
contextManager.removeContextStack(contextStack1.contextID);
216+
}).not.toThrowError();
217+
});
218+
219+
it('throws error on removing context stack using non-existing context stack ID', () => {
220+
expect(() => {
221+
contextManager.removeContextStack(contextStack1.contextID);
222+
}).toThrowError(
223+
`InvalidAccessError: Context stack with ID "${contextStack1.contextID}" doesn't exist`,
224+
);
225+
});
226+
227+
it('updates global context key-map with commit', () => {
228+
contextManager.reset();
229+
expect(
230+
Object.entries(contextManager.contextGlobalKeyMap)
231+
.sort(([a], [b]) => a.localeCompare(b))
232+
.map(([k, v]) => `${k}-${v}`)
233+
.join(', '),
234+
).toBe('bar-red, foo-4');
235+
236+
contextManager.updateContextGlobalKeyMap({ foo: 16, bar: 'coral' }, true);
237+
contextManager.reset();
238+
expect(
239+
Object.entries(contextManager.contextGlobalKeyMap)
240+
.sort(([a], [b]) => a.localeCompare(b))
241+
.map(([k, v]) => `${k}-${v}`)
242+
.join(', '),
243+
).toBe('bar-coral, foo-16');
244+
});
245+
});
246+
});

0 commit comments

Comments
 (0)