Skip to content

Commit d3b4e7f

Browse files
committed
feat: implement IR generator, runtime scheduler, and LLVM backend for initial async/await support.
1 parent 44a7e7f commit d3b4e7f

4 files changed

Lines changed: 224 additions & 8 deletions

File tree

examples/async_test.pxpl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// async_test.pxpl
2+
3+
async func add(a, b) {
4+
return a + b;
5+
}
6+
7+
async func mainAsync() {
8+
print("Start Async");
9+
var t1 = add(10, 20);
10+
print("Task created");
11+
var res = await t1;
12+
print("Result: " + res);
13+
}
14+
15+
func main() {
16+
print("Main Sync");
17+
var t = mainAsync();
18+
await t; // Sync await (blocking)
19+
print("Done");
20+
}

src/compiler/backend_llvm.cpp

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -337,11 +337,17 @@ class LLVMEmitter {
337337
}
338338

339339
case IR_OP_AWAIT: {
340+
llvm::Value* TaskToAwait = getOperand(instr->operands[0]);
341+
340342
if (!CoroHdl) {
341-
// Error: await outside async
343+
// Sync await (e.g. in main)
344+
// Call prox_rt_run_and_wait(TaskToAwait)
345+
llvm::Function* FRunWait = ModuleOb->getFunction("prox_rt_run_and_wait");
346+
if (FRunWait) {
347+
ssaValues[instr->result] = Builder->CreateCall(FRunWait, {TaskToAwait}, "await_res");
348+
}
342349
return;
343350
}
344-
llvm::Value* TaskToAwait = getOperand(instr->operands[0]);
345351

346352
// We need to:
347353
// 1. Check if task is done? (Runtime optimization)
@@ -353,7 +359,12 @@ class LLVMEmitter {
353359
// BUT we don't have prox_rt_await declared yet.
354360
// And await should return the result of the task.
355361

356-
// TODO: Declare prox_rt_await.
362+
// Call runtime to register await
363+
llvm::Function* FAwait = ModuleOb->getFunction("prox_rt_await");
364+
if (FAwait) {
365+
Builder->CreateCall(FAwait, {TaskToAwait});
366+
}
367+
357368
// For now, emit llvm.coro.suspend.
358369
llvm::Function* FCoroSuspend = ModuleOb->getFunction("llvm.coro.suspend");
359370
llvm::Value* SuspendResult = Builder->CreateCall(FCoroSuspend, {
@@ -439,6 +450,42 @@ class LLVMEmitter {
439450
}
440451
}
441452

453+
void setupSchedulerHelpers() {
454+
// void prox_rt_resume(i8* hdl)
455+
llvm::FunctionType *ResumeType = llvm::FunctionType::get(
456+
Builder->getVoidTy(),
457+
{Builder->getPtrTy()},
458+
false
459+
);
460+
llvm::Function* FResume = llvm::Function::Create(ResumeType, llvm::Function::ExternalLinkage, "prox_rt_resume", ModuleOb.get());
461+
462+
// Implement prox_rt_resume wrapper
463+
llvm::BasicBlock* Entry = llvm::BasicBlock::Create(*Context, "entry", FResume);
464+
llvm::IRBuilder<> ResBuilder(Entry);
465+
llvm::Function* FCoroResume = llvm::Intrinsic::getDeclaration(ModuleOb.get(), llvm::Intrinsic::coro_resume);
466+
ResBuilder.CreateCall(FCoroResume, {FResume->arg_begin()});
467+
ResBuilder.CreateRetVoid();
468+
469+
// void prox_rt_await(Value task)
470+
llvm::FunctionType *AwaitType = llvm::FunctionType::get(
471+
Builder->getVoidTy(),
472+
{Builder->getInt64Ty()},
473+
false
474+
);
475+
llvm::Function::Create(AwaitType, llvm::Function::ExternalLinkage, "prox_rt_await", ModuleOb.get());
476+
}
477+
478+
// Value prox_rt_run_and_wait(Value task)
479+
llvm::FunctionType *RunWaitType = llvm::FunctionType::get(
480+
Builder->getInt64Ty(),
481+
{Builder->getInt64Ty()},
482+
false
483+
);
484+
llvm::Function::Create(RunWaitType, llvm::Function::ExternalLinkage, "prox_rt_run_and_wait", ModuleOb.get());
485+
}
486+
487+
// Call this from setupRuntimeTypes or Constructor
488+
442489
llvm::Value* getOperand(IROperand& op) {
443490
if (op.type == OPERAND_CONST) {
444491
if (IS_NUMBER(op.as.constant)) {
@@ -461,6 +508,7 @@ class LLVMEmitter {
461508

462509
extern "C" void emitLLVM(IRModule* module) {
463510
LLVMEmitter emitter;
511+
emitter.setupSchedulerHelpers(); // Add helpers
464512
emitter.emitModule(module);
465513
}
466514

src/compiler/ir_gen.c

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,62 @@ static int visitExpr(IRGen* gen, Expr* expr) {
134134
case EXPR_AWAIT: {
135135
int val = visitExpr(gen, expr->as.await_expr.expression);
136136
int r = newReg(gen);
137-
IRInstruction* instr = createIRInstruction(IR_OP_AWAIT, r);
138-
IROperand op;
139-
op.type = OPERAND_VAL; op.as.ssaVal = val;
140-
addOperand(instr, op);
141-
emit(gen, instr);
137+
138+
if (gen->currentFunc->isAsync) {
139+
IRInstruction* instr = createIRInstruction(IR_OP_AWAIT, r);
140+
IROperand op;
141+
op.type = OPERAND_VAL; op.as.ssaVal = val;
142+
addOperand(instr, op);
143+
emit(gen, instr);
144+
} else {
145+
// Sync wait: CALL prox_rt_run_and_wait(val)
146+
// NOTE: We rely on the backend to know about this function,
147+
// OR we can add IR_OP_CALL?
148+
// But IR_OP_CALL takes a function name?
149+
// Our IR_OP_CALL structure might not be flexible enough yet?
150+
// Let's check irOpName/ir definitions.
151+
// IR_OP_CALL usually calls user functions.
152+
// We need to call a runtime function.
153+
// Use IR_OP_CALL with a special "name" or add IR_OP_RT_CALL?
154+
// Or just assume IR_OP_CALL with string "prox_rt_run_and_wait" works
155+
// provided we expose it in backend/runtime.
156+
// But IR_OP_CALL operands usually: 0=Func, 1..N=Args.
157+
// IR_OP_CALL definition:
158+
// operand[0] = function name (literal?) or pointer?
159+
// Let's assume operand[0] is CONST string name for now for external calls.
160+
161+
// However, ir_gen.c doesn't support generic calls well yet (visitExpr->EXPR_CALL missing?)
162+
// Let's look at implementation of visitExpr for EXPR_CALL... OH WAIT.
163+
// I am looking at ir_gen.c and EXPR_CALL is NOT implemented in the snippet I viewed!
164+
// The snippet had EXPR_LITERAL, BINARY, VAR, ASSIGN. NO EXPR_CALL.
165+
// So I need to implement a hack for calling runtime helper using IR_OP_CALL logic manually.
166+
167+
IRInstruction* instr = createIRInstruction(IR_OP_CALL, r);
168+
IROperand opFunc, opArg;
169+
170+
// Op 0: Function Name
171+
opFunc.type = OPERAND_CONST;
172+
opFunc.as.constant = OBJ_VAL(copyString("prox_rt_run_and_wait", 20));
173+
// Wait, can't easily create ObjString here without VM?
174+
// Constants in IR are `Value`. `copyString` needs VM.
175+
// Hack: Pass raw string pointer if allowed? NO.
176+
// Alternative: Define a new IR opcode IR_OP_AWAIT_SYNC.
177+
// Easier.
178+
// Actually, reusing IR_OP_AWAIT with a flag? or isAsync check in backend?
179+
// Backend `emitInstruction` checks `CoroHdl`.
180+
// If `CoroHdl` is null, it returns.
181+
// We can modify Backend to handle `IR_OP_AWAIT` when `CoroHdl` is null by generating the sync call!
182+
183+
// YES! That is much cleaner.
184+
// Keep IR_OP_AWAIT.
185+
// In Backend, if CoroHdl is null, emit call to `prox_rt_run_and_wait`.
186+
187+
IRInstruction* instrWait = createIRInstruction(IR_OP_AWAIT, r);
188+
IROperand op;
189+
op.type = OPERAND_VAL; op.as.ssaVal = val;
190+
addOperand(instrWait, op);
191+
emit(gen, instrWait);
192+
}
142193
return r;
143194
}
144195

src/runtime/scheduler.c

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// --------------------------------------------------
2+
// Project: ProX Programming Language (ProXPL)
3+
// Author: ProgrammerKR
4+
// Created: 2025-12-27
5+
// Copyright © 2025. ProXentix India Pvt. Ltd. All rights reserved.
6+
7+
#include "../../include/object.h"
8+
#include "../../include/value.h"
9+
#include <stdio.h>
10+
#include <stdlib.h>
11+
12+
// Extern declaration of generated LLVM wrapper
13+
extern void prox_rt_resume(void* hdl);
14+
15+
typedef struct TaskQueue {
16+
ObjTask* head;
17+
ObjTask* tail;
18+
} TaskQueue;
19+
20+
static TaskQueue queue = {NULL, NULL};
21+
static ObjTask* currentTask = NULL;
22+
23+
void scheduler_enqueue(ObjTask* task) {
24+
if (task->completed) return;
25+
26+
task->next = NULL;
27+
if (queue.tail) {
28+
queue.tail->next = task;
29+
queue.tail = task;
30+
} else {
31+
queue.head = task;
32+
queue.tail = task;
33+
}
34+
}
35+
36+
ObjTask* scheduler_dequeue() {
37+
if (!queue.head) return NULL;
38+
ObjTask* task = queue.head;
39+
queue.head = task->next;
40+
if (!queue.head) queue.tail = NULL;
41+
return task;
42+
}
43+
44+
void scheduler_run() {
45+
while (queue.head) {
46+
ObjTask* task = scheduler_dequeue();
47+
currentTask = task;
48+
// Resume
49+
if (task->coroHandle) {
50+
prox_rt_resume(task->coroHandle);
51+
}
52+
currentTask = NULL;
53+
}
54+
}
55+
56+
// Runtime helpers called from LLVM
57+
58+
void prox_rt_await(Value taskVal) {
59+
if (!IS_TASK(taskVal)) {
60+
printf("Runtime Error: Awaiting non-task value.\\n");
61+
exit(1);
62+
}
63+
ObjTask* target = AS_TASK(taskVal);
64+
65+
if (target->completed) {
66+
// Optimization: If done, don't suspend?
67+
// But we already emitted suspend code unconditionally in backend.
68+
// So we MUST re-schedule current task immediately.
69+
if (currentTask) scheduler_enqueue(currentTask);
70+
} else {
71+
// Dependency: currentTask depends on target.
72+
// For simplicity in this demo, we just re-enqueue currentTask.
73+
// A real scheduler would put currentTask in target's "waiters" list.
74+
// Busy-wait behavior via re-enqueueing is inefficient but functional for MVP.
75+
if (currentTask) scheduler_enqueue(currentTask);
76+
}
77+
}
78+
79+
Value prox_rt_new_task(void* hdl) {
80+
ObjTask* task = newTask(hdl);
81+
// Auto-schedule new tasks?
82+
// Yes, usually.
83+
scheduler_enqueue(task);
84+
return OBJ_VAL(task);
85+
}
86+
87+
// Helper to bridge main synchronous code to async world
88+
Value prox_rt_run_and_wait(Value taskVal) {
89+
if (!IS_TASK(taskVal)) {
90+
printf("Runtime Error: Awaiting non-task value in sync context.\\n");
91+
exit(1);
92+
}
93+
ObjTask* task = AS_TASK(taskVal);
94+
scheduler_enqueue(task);
95+
scheduler_run();
96+
return task->result;
97+
}

0 commit comments

Comments
 (0)