Skip to content

Commit ace4cc8

Browse files
author
nickna
committed
feat(event-loop): implement event loop management for async operations and add IAsyncHandle interface
1 parent a68e1a0 commit ace4cc8

10 files changed

Lines changed: 362 additions & 51 deletions

File tree

Examples/test-examples.ps1

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,53 @@ const arrow = () => 42;
287287
}
288288
)
289289
}
290+
291+
"web-server" = @{
292+
File = "web-server.ts"
293+
Tests = @(
294+
@{
295+
Name = "ShowHelp"
296+
RequiresArgs = $true
297+
Args = { @("--help") }
298+
Assertions = @(
299+
@{ Type = "Contains"; Value = "SharpTS Web Server Example" }
300+
@{ Type = "Contains"; Value = "Usage:" }
301+
@{ Type = "Contains"; Value = "GET /" }
302+
@{ Type = "Contains"; Value = "/api/time" }
303+
@{ Type = "Contains"; Value = "/api/echo" }
304+
@{ Type = "Contains"; Value = "/api/greet" }
305+
@{ Type = "Contains"; Value = "port" }
306+
)
307+
},
308+
@{
309+
Name = "ShowHelpShortFlag"
310+
RequiresArgs = $true
311+
Args = { @("-h") }
312+
Assertions = @(
313+
@{ Type = "Contains"; Value = "SharpTS Web Server Example" }
314+
@{ Type = "Contains"; Value = "Usage:" }
315+
)
316+
},
317+
@{
318+
Name = "InvalidPortNumber"
319+
RequiresArgs = $true
320+
Args = { @("invalid") }
321+
Assertions = @(
322+
@{ Type = "Contains"; Value = "Invalid port number" }
323+
@{ Type = "Contains"; Value = "Using default port 3000" }
324+
)
325+
},
326+
@{
327+
Name = "PortOutOfRange"
328+
RequiresArgs = $true
329+
Args = { @("99999") }
330+
Assertions = @(
331+
@{ Type = "Contains"; Value = "Invalid port number" }
332+
@{ Type = "Contains"; Value = "Using default port 3000" }
333+
)
334+
}
335+
)
336+
}
290337
}
291338

292339
# ========== Fixture Functions ==========

Execution/Interpreter.Async.cs

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -85,31 +85,38 @@ private async Task<ExecutionResult> ExecuteAsync(Stmt stmt)
8585
} while (IsTruthy(await EvaluateAsync(doWhileStmt.Condition)));
8686
return ExecutionResult.Success();
8787
case Stmt.For forStmt:
88-
// Execute initializer once
89-
if (forStmt.Initializer != null)
90-
await ExecuteAsync(forStmt.Initializer);
91-
// Loop with proper continue handling - increment always runs
92-
while (forStmt.Condition == null || IsTruthy(await EvaluateAsync(forStmt.Condition)))
88+
{
89+
// Create scope for loop variables (ES6 let/const block scoping)
90+
RuntimeEnvironment loopEnv = new(_environment);
91+
using (PushScope(loopEnv))
9392
{
94-
var result = await ExecuteAsync(forStmt.Body);
95-
if (result.Type == ExecutionResult.ResultType.Break && result.TargetLabel == null) break;
96-
// On continue, execute increment then continue the loop
97-
if (result.Type == ExecutionResult.ResultType.Continue && result.TargetLabel == null)
93+
// Execute initializer once (defines loop variable in loopEnv)
94+
if (forStmt.Initializer != null)
95+
await ExecuteAsync(forStmt.Initializer);
96+
// Loop with proper continue handling - increment always runs
97+
while (forStmt.Condition == null || IsTruthy(await EvaluateAsync(forStmt.Condition)))
9898
{
99+
var result = await ExecuteAsync(forStmt.Body);
100+
if (result.Type == ExecutionResult.ResultType.Break && result.TargetLabel == null) break;
101+
// On continue, execute increment then continue the loop
102+
if (result.Type == ExecutionResult.ResultType.Continue && result.TargetLabel == null)
103+
{
104+
if (forStmt.Increment != null)
105+
await EvaluateAsync(forStmt.Increment);
106+
// Yield to allow timer callbacks and other threads to execute
107+
await Task.Yield();
108+
continue;
109+
}
110+
if (result.IsAbrupt) return result;
111+
// Normal completion: execute increment
99112
if (forStmt.Increment != null)
100113
await EvaluateAsync(forStmt.Increment);
101-
// Yield to allow timer callbacks and other threads to execute
102-
await Task.Yield();
103-
continue;
114+
// Process any pending timer callbacks
115+
ProcessPendingCallbacks();
104116
}
105-
if (result.IsAbrupt) return result;
106-
// Normal completion: execute increment
107-
if (forStmt.Increment != null)
108-
await EvaluateAsync(forStmt.Increment);
109-
// Process any pending timer callbacks
110-
ProcessPendingCallbacks();
117+
return ExecutionResult.Success();
111118
}
112-
return ExecutionResult.Success();
119+
}
113120
case Stmt.ForOf forOf:
114121
return await ExecuteForOfAsync(forOf);
115122
case Stmt.ForIn forIn:

Execution/Interpreter.cs

Lines changed: 100 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using SharpTS.Runtime.BuiltIns;
66
using SharpTS.Runtime.BuiltIns.Modules;
77
using SharpTS.Runtime.BuiltIns.Modules.Interpreter;
8+
using SharpTS.Runtime.EventLoop;
89
using SharpTS.Runtime.Exceptions;
910
using SharpTS.Runtime.Types;
1011
using SharpTS.TypeSystem;
@@ -129,6 +130,9 @@ public Interpreter()
129130
// Track all pending timers for cleanup on disposal
130131
private readonly System.Collections.Concurrent.ConcurrentBag<Runtime.Types.SharpTSTimeout> _pendingTimers = new();
131132

133+
// Event loop for managing async handles (servers, timers, etc.)
134+
private readonly EventLoop _eventLoop = new();
135+
132136
// Virtual timer system - timers are checked and executed on the main thread during loop iterations.
133137
// This avoids thread scheduling issues on macOS where background threads may not get CPU time.
134138
// Uses PriorityQueue for O(log n) insert and O(log n) extraction of due timers.
@@ -195,6 +199,52 @@ internal VirtualTimer ScheduleTimer(int delayMs, int intervalMs, Action callback
195199
return timer;
196200
}
197201

202+
/// <summary>
203+
/// Registers an async handle with the interpreter's event loop.
204+
/// The interpreter will keep running while this handle is active.
205+
/// </summary>
206+
internal void RegisterHandle(IAsyncHandle handle)
207+
{
208+
_eventLoop.Register(handle);
209+
}
210+
211+
/// <summary>
212+
/// Unregisters an async handle from the interpreter's event loop.
213+
/// </summary>
214+
internal void UnregisterHandle(IAsyncHandle handle)
215+
{
216+
_eventLoop.Unregister(handle);
217+
}
218+
219+
/// <summary>
220+
/// Runs the event loop, processing timers and keeping the process alive while there are active handles.
221+
/// Uses efficient waiting via ManualResetEventSlim instead of polling.
222+
/// </summary>
223+
private void RunEventLoop()
224+
{
225+
// Check if there are scheduled timers - they also count as active handles
226+
bool HasTimersOrHandles() => _hasScheduledTimers || _eventLoop.HasActiveHandles();
227+
228+
while (!_isDisposed && HasTimersOrHandles())
229+
{
230+
// Process any pending timer callbacks
231+
ProcessPendingCallbacks();
232+
233+
// If only timers are active (no server handles), we need to continue the loop
234+
// If there are active handles, the event loop will wait efficiently
235+
if (_eventLoop.HasActiveHandles())
236+
{
237+
// Let the event loop wait for state changes (with timeout for timer processing)
238+
_eventLoop.Run(ProcessPendingCallbacks);
239+
}
240+
else if (_hasScheduledTimers)
241+
{
242+
// Only timers active - sleep briefly then check again
243+
Thread.Sleep(10);
244+
}
245+
}
246+
}
247+
198248
/// <summary>
199249
/// Processes all due virtual timers. Called during loop iterations to execute
200250
/// timer callbacks without relying on background thread scheduling.
@@ -270,6 +320,9 @@ public void Dispose()
270320
{
271321
_isDisposed = true;
272322

323+
// Dispose the event loop first to stop any waiting
324+
_eventLoop.Dispose();
325+
273326
// Cancel all pending timers to release resources immediately
274327
while (_pendingTimers.TryTake(out var timer))
275328
{
@@ -377,6 +430,9 @@ public void Interpret(List<Stmt> statements, TypeMap? typeMap = null)
377430

378431
// After executing all statements, check for a main() function and call it
379432
TryCallMainWithExitCode(statements);
433+
434+
// Always run the event loop - servers/timers may have been registered
435+
RunEventLoop();
380436
}
381437
catch (Exception error)
382438
{
@@ -414,10 +470,17 @@ public void InterpretModules(List<ParsedModule> modules, ModuleResolver resolver
414470
}
415471

416472
// After executing all modules, check for main() in the entry module (last one)
473+
// Note: main() may have already been called during module execution if there's
474+
// a top-level main() call. TryCallMainWithExitCode handles exit codes but
475+
// the event loop should run regardless of main().
417476
if (modules.Count > 0)
418477
{
419478
TryCallMainWithExitCode(modules[^1].Statements);
420479
}
480+
481+
// Always run the event loop at the end - servers/timers may have been
482+
// registered during module execution (even without a main function)
483+
RunEventLoop();
421484
}
422485
catch (Exception error)
423486
{
@@ -488,8 +551,9 @@ private void TryCallMainWithExitCode(List<Stmt> statements)
488551
{
489552
if (stmt is Stmt.Function func && func.Name.Lexeme == "main" && func.Body != null)
490553
{
491-
// Check signature: exactly one parameter (args: string[])
492-
if (func.Parameters.Count == 1 && func.Parameters[0].Type == "string[]")
554+
// Accept signatures: main() or main(args: string[])
555+
var paramCount = func.Parameters.Count;
556+
if (paramCount == 0 || (paramCount == 1 && func.Parameters[0].Type == "string[]"))
493557
{
494558
// Accept return types: void, null (implicit), number, Promise<void>, Promise<number>
495559
var rt = func.ReturnType;
@@ -513,12 +577,15 @@ private void TryCallMainWithExitCode(List<Stmt> statements)
513577
if (mainValue is not SharpTSFunction mainFn)
514578
return;
515579

516-
// Call main with process.argv
580+
// Call main with process.argv (pass args even if main() doesn't take them - JS allows this)
517581
var argv = ProcessBuiltIns.GetArgv();
518582
object? result;
519583
try
520584
{
521-
result = mainFn.Call(this, [argv]);
585+
// Pass argv only if main expects it
586+
result = mainFunc.Parameters.Count == 0
587+
? mainFn.Call(this, [])
588+
: mainFn.Call(this, [argv]);
522589
}
523590
catch (Runtime.Exceptions.ReturnException ret)
524591
{
@@ -536,6 +603,10 @@ private void TryCallMainWithExitCode(List<Stmt> statements)
536603
{
537604
System.Environment.Exit((int)exitCode);
538605
}
606+
607+
// Note: RunEventLoop is called by the caller (Interpret or InterpretModules)
608+
// after this method returns, so handles registered during main() or module
609+
// execution will keep the process alive.
539610
}
540611

541612
/// <summary>
@@ -1021,31 +1092,37 @@ internal ExecutionResult VisitDoWhile(Stmt.DoWhile doWhileStmt)
10211092

10221093
internal ExecutionResult VisitFor(Stmt.For forStmt)
10231094
{
1024-
// Execute initializer once
1025-
if (forStmt.Initializer != null)
1026-
Execute(forStmt.Initializer);
1027-
// Loop with proper continue handling - increment always runs
1028-
while (forStmt.Condition == null || IsTruthy(Evaluate(forStmt.Condition)))
1095+
// Create scope for loop variables (ES6 let/const block scoping)
1096+
// Variables declared with let/const in the initializer are scoped to the loop
1097+
RuntimeEnvironment loopEnv = new(_environment);
1098+
using (PushScope(loopEnv))
10291099
{
1030-
var result = Execute(forStmt.Body);
1031-
if (result.Type == ExecutionResult.ResultType.Break && result.TargetLabel == null) break;
1032-
// On continue, execute increment then continue the loop
1033-
if (result.Type == ExecutionResult.ResultType.Continue && result.TargetLabel == null)
1100+
// Execute initializer once (defines loop variable in loopEnv)
1101+
if (forStmt.Initializer != null)
1102+
Execute(forStmt.Initializer);
1103+
// Loop with proper continue handling - increment always runs
1104+
while (forStmt.Condition == null || IsTruthy(Evaluate(forStmt.Condition)))
10341105
{
1106+
var result = Execute(forStmt.Body);
1107+
if (result.Type == ExecutionResult.ResultType.Break && result.TargetLabel == null) break;
1108+
// On continue, execute increment then continue the loop
1109+
if (result.Type == ExecutionResult.ResultType.Continue && result.TargetLabel == null)
1110+
{
1111+
if (forStmt.Increment != null)
1112+
Evaluate(forStmt.Increment);
1113+
// Yield to allow timer callbacks and other threads to execute
1114+
Thread.Sleep(0);
1115+
continue;
1116+
}
1117+
if (result.IsAbrupt) return result;
1118+
// Normal completion: execute increment
10351119
if (forStmt.Increment != null)
10361120
Evaluate(forStmt.Increment);
1037-
// Yield to allow timer callbacks and other threads to execute
1038-
Thread.Sleep(0);
1039-
continue;
1121+
// Process any pending timer callbacks
1122+
ProcessPendingCallbacks();
10401123
}
1041-
if (result.IsAbrupt) return result;
1042-
// Normal completion: execute increment
1043-
if (forStmt.Increment != null)
1044-
Evaluate(forStmt.Increment);
1045-
// Process any pending timer callbacks
1046-
ProcessPendingCallbacks();
1124+
return ExecutionResult.Success();
10471125
}
1048-
return ExecutionResult.Success();
10491126
}
10501127

10511128
internal ExecutionResult VisitForOf(Stmt.ForOf forOf) => ExecuteForOf(forOf);

Execution/VariableResolver.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,9 @@ protected override void VisitClass(Stmt.Class stmt)
239239

240240
protected override void VisitFor(Stmt.For stmt)
241241
{
242-
// For loops create a scope for the loop variable
242+
// For loops create a scope for the loop variable (ES6 let/const block scoping).
243+
// The initializer defines the loop variable in this scope, and the condition,
244+
// increment, and body all have access to it.
243245
BeginScope();
244246
if (stmt.Initializer != null)
245247
Visit(stmt.Initializer);
@@ -253,20 +255,26 @@ protected override void VisitFor(Stmt.For stmt)
253255

254256
protected override void VisitForOf(Stmt.ForOf stmt)
255257
{
258+
// Resolve iterable BEFORE creating loop scope - matches interpreter behavior
259+
// where Evaluate(forOf.Iterable) happens before the loop environment is created
260+
Visit(stmt.Iterable);
261+
256262
BeginScope();
257263
Declare(stmt.Variable.Lexeme);
258264
Define(stmt.Variable.Lexeme);
259-
Visit(stmt.Iterable);
260265
Visit(stmt.Body);
261266
EndScope();
262267
}
263268

264269
protected override void VisitForIn(Stmt.ForIn stmt)
265270
{
271+
// Resolve object BEFORE creating loop scope - matches interpreter behavior
272+
// where Evaluate(forIn.Object) happens before the loop environment is created
273+
Visit(stmt.Object);
274+
266275
BeginScope();
267276
Declare(stmt.Variable.Lexeme);
268277
Define(stmt.Variable.Lexeme);
269-
Visit(stmt.Object);
270278
Visit(stmt.Body);
271279
EndScope();
272280
}

Runtime/BuiltIns/BuiltInRegistry.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ private static BuiltInRegistry CreateDefault()
144144
RegisterStringType(registry);
145145
RegisterArrayType(registry);
146146
RegisterMathType(registry);
147+
RegisterObjectType(registry);
147148
RegisterPromiseType(registry);
148149
RegisterDoubleType(registry);
149150
RegisterDateType(registry);
@@ -197,8 +198,8 @@ private static void RegisterObjectNamespace(BuiltInRegistry registry)
197198
{
198199
registry.RegisterNamespace(new BuiltInNamespace(
199200
Name: "Object",
200-
IsSingleton: false,
201-
SingletonFactory: null,
201+
IsSingleton: true,
202+
SingletonFactory: () => Types.SharpTSObjectNamespace.Instance,
202203
GetMethod: name => ObjectBuiltIns.GetStaticMethod(name) as BuiltInMethod
203204
));
204205
}
@@ -255,6 +256,13 @@ private static void RegisterMathType(BuiltInRegistry registry)
255256
MathBuiltIns.GetMember(name));
256257
}
257258

259+
private static void RegisterObjectType(BuiltInRegistry registry)
260+
{
261+
// Object members accessed via property access (Object.keys, Object.values)
262+
registry.RegisterInstanceType(typeof(Types.SharpTSObjectNamespace), (_, name) =>
263+
ObjectBuiltIns.GetStaticMethod(name));
264+
}
265+
258266
private static void RegisterPromiseNamespace(BuiltInRegistry registry)
259267
{
260268
registry.RegisterNamespace(new BuiltInNamespace(

0 commit comments

Comments
 (0)