We need to create two files: src/repl/net_server.js and src/repl/host_bridge.js
. These will be part of the REPL environment, enabling TCP server
capabilities. Need to ensure each file is under 150 lines.
We need to understand the existing codebase structure. There's an existing VM
(LPCVirtualMachine) with methods: clone_object, ecall, run, registerBlueprint,
etc. Scheduler class exists. We need to integrate networking.
From task description: TCP ports operate in line-mode. A new connection clones
the specified LPC file and maps the TCP socket to the new Object ID (OID).
Incoming lines trigger an ECALL to receive_line(string) on the cloned object.
Outbound data is handled by a new syscall net_write(string), which looks up
the socket by the calling ctx.current_oid and writes to it.
So we need: A TCP server that listens on a given port (specified via command
line arg). For simplicity, multiple ports can be specified? The example node
repl.js 4000=/obj/login.c suggests a single mapping for now, but we could
support multiple. We'll design to support multiple, storing mappings from port
to blueprint name.
When a connection is accepted, we need to clone the blueprint, get OID, and
associate the OID with the socket.
We need to handle incoming data lines: accumulate until newline, then trigger
ECALL on that object's context. But ECALL requires the VM to run the object.
How do we trigger an ecall? The object might be dormant? Typically we create a
CTX for the object and run it. However, the cloned object might not have a
running context yet; it might be idle. The REPL should probably create a
context for each connected object? Or maybe the object is expected to be
running a loop that calls read_mail? But the requirement says incoming lines
trigger an ECALL to receive_line(string). So we need to compile the blueprint
(if not already), clone, and then call a function receive_line on that object,
passing the string.
To call receive_line, we can create a new CTX for that OID, set registers, set
IP to the vtable entry for receive_line, and run it via scheduler. But this
might require careful handling: the object might have persistent state, and
each incoming line should invoke the function fresh. The existing ECALL
mechanism in VM is for calling another object from within a running VM. But we
can directly invoke by creating a CTX with appropriate IP and using the
scheduler.
We need to manage contexts for each connection: maybe each connection has its
own CTX that is sleeping? Or we just create a temporary CTX per incoming line,
run it, then discard? But if the object has persistent state, the state is in
the stateArena, so it's fine. The CTX is just execution context, not the
object state. So we can create a new CTX for each line, set current_oid, run
the receive_line function. The function may call net_write to output. That's
fine.
Outbound data: The new syscall net_write(string): it should look at
ctx.current_oid and find the associated socket, then write the string to the
socket. So we need a mapping OID -> socket. This mapping should be maintained
in the host_bridge or net_server.
We also need to ensure that the VM's IVT includes this new syscall. We'll
likely have to modify the VM? But the task says generate code for
net_server.js and host_bridge.js. Probably we should have net_server.js
contain the TCP server logic and host_bridge.js act as a bridge between the VM
and network, providing the syscall and mapping.
We need to think about integration with the existing code: The VM is created
in the main REPL entry (which we are not writing now? The task says generate
these two files first). We need to assume there will be a main REPL script
that sets up the VM, scheduler, and these modules. So net_server.js might take
the VM, scheduler, and a mapping of ports to blueprints. host_bridge.js might
provide the net_write syscall and the mapping OID->socket, and also the
function to handle incoming lines.
But the task says "generate the code for src/repl/net_server.js and
src/repl/host_bridge.js first". Likely host_bridge will provide the bridge
between VM and network, and net_server will manage the TCP servers and
connections. We should make them modular.
We need to adhere to <150 lines per file, including comments and blank lines.
Let's sketch.
host_bridge.js: It will manage the mapping from OID to socket (or writer) for
outbound. It will provide a syscall function net_write that can be registered
in the VM's IVT (probably syscall ID 30 or something). It will receive the
tagged string pointer as argument, get string content via stringManager, look
up socket by current_oid, and write. It might also provide a function to
register a connection (OID to socket) and unregister on close. It might
provide a function handleIncomingLine(oid, line) that will invoke receive_line
on that object. But that might be better in net_server? Or host_bridge can do
that as well, using the scheduler. However, the host_bridge shouldn't know
about the scheduler? It might be passed the scheduler. We can design
host_bridge to have the VM and scheduler references.
But we need to ensure separation: net_server handles TCP, host_bridge handles
the VM/network bridge. So net_server will instantiate the TCP servers, accept
connections, clone blueprints via VM, then tell host_bridge to register the
connection (oid -> socket). Then on data, net_server will call
host_bridge.handleIncomingLine(oid, line). Or host_bridge can provide a method
to process incoming line. That seems appropriate.
Net_server.js: Constructor takes: vm, scheduler, hostBridge, portBlueprintsMap
(e.g., {4000: '/obj/login.c'}).
For each port, create net.Server and listen. On connection, call
vm.clone_object(blueprintName). The blueprint must be already registered or
available via JIT? We can assume it's already available or use
vm.onBlueprintMissing to compile. So we just call vm.clone_object.
Then hostBridge.registerConnection(oid, socket). Set up socket data handling:
accumulate data until newline, then for each line call hostBridge.handleIncomin
gLine(oid, line). On socket close, hostBridge.unregisterConnection(oid).
Need to handle socket errors, etc. We need to think about string allocation
for the incoming line: The line is a JS string; we need to pass it as a tagged
string pointer. The host_bridge can allocate a string via stringManager and
then pass that tagged pointer to the ECALL. But the ECALL will be invoked via
a CTX, so we need to set R1 to that tagged pointer before running
receive_line.
HostBridge.handleIncomingLine(oid, line): Allocate string using
vm.stringManager.allocString(line).
Get blueprint for that object to find the vtable for receive_line. Use hash
function (we need to replicate hashString or use vm's hash? There is
hashString in Compiler but not VM. We can implement a quick hash or use a
precomputed hash. Probably we can compute using same algorithm. We can have
hostBridge use a static hash function.
The function name is receive_line. So get its hash. Create a CTX with the
blueprint's bytecode and constantPool, set current_oid = oid.
Set registers: R1 = tagged string (since ECALL convention: for calling method
with one argument, argument is in R1? From compiler code: when compiling
CallExpression for member expression, it moves argument to R1 then ECALL with
target and hash. For ECALL, the target object's method is called, and
arguments are in R1 (and maybe R2...). So we set R1 to tagged string.
Set ctx.ip = blueprint.vtable[hash]. If not found, log error. Add ctx to
scheduler and run once? We can use scheduler.runOnce() which will run the ctx
until it yields or finishes. But we need to ensure that if the function calls
net_write, it will have access to socket mapping. That's fine because
net_write will be in IVT and uses current_oid.
The ctx will run, maybe call net_write, then return, and scheduler will handle
it (if it ends, it won't be re-added). Good.
We need to be careful about concurrent connections: Since Node.js is
single-threaded, we can process lines sequentially. We'll just call
scheduler.runOnce after adding the ctx. That will run the context to
completion or until it yields (like read_mail). That's fine.
Now, about net_write syscall: It should be registered in VM's IVT. We need to
decide syscall ID. We can pick, say, 30. We'll need to modify the VM? No, the
IVT is a dictionary, we can add it in host_bridge initialization.
But the existing compiler doesn't know about net_write built-in. The user's
LPC source might call net_write("hello"). Currently compiler only recognizes
write, read_mail, post_mail. We need to add net_write as a built-in that emits
a syscall. But the task only asks for the two files, not compiler changes.
However, for the REPL to work, the LPC code must be able to call net_write.
Since we are building the REPL, it's expected that the compiler will be
extended. But we can't modify compiler.js now because we are only generating
those two files. However, the code in these files can assume that the compiler
already supports net_write. In practice, we might need to add that later.
Since the task is to generate these two files, we can note that the compiler
will need to be updated.
We'll design host_bridge to register the syscall for net_write. The ID we pick
must not conflict with existing syscalls: 1 write, 10 array alloc, 11 array
set, 12 array get, 20 read_mail, 21 post_mail. So we can use e.g., 30 for
net_write.
Now, line count constraints: Each file <150 lines. We'll keep them concise.
Let's write host_bridge.js: That's about 70 lines. Both under 150. But we need
to ensure that the scheduler's runOnce doesn't cause issues with multiple
contexts. In handleIncomingLine, we add ctx and run once. That will execute
the context until it finishes or yields. If it yields (e.g., calls read_mail),
the ctx will be added to waitTable or back to runQueue. That might cause the
ctx to remain and possibly be run again later. That could be problematic
because the context is ephemeral (intended only for this line). If
receive_line calls read_mail, it might wait for mail and then later be woken,
but its current_oid is the same and it would resume execution, potentially
trying to run the rest of receive_line after a mail. That might be acceptable