Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A REPL #138

Merged
merged 31 commits into from
Nov 2, 2023
Merged

A REPL #138

merged 31 commits into from
Nov 2, 2023

Conversation

melsman
Copy link
Owner

@melsman melsman commented Oct 12, 2023

The purpose of this PR is to build a REPL for MLKit. Whereas the static aspects are mostly working, we need to decide on a mechanism for the dynamic aspects.

A Proposal

  1. Separate the compiler and the dynamic executing code into two OS processes (by fork and child execute).
  2. Have the two processes communicate via two named pipes (file descriptors).
  3. The child process (to be completely implemented in C) should first load initialisation code that allocates the global regions. It should then wait for a command:
    • PRINT ty L: On succes, reply with STR N s, where s is the result of pretty-printing the value of type ty located at location L and where N is the size of s.
    • LOADRUN so-file L : Load the shared library file specified by so-file into the process and run its code specified by L. Before running the code, setup an exception handler that will print "Uncaught exception" in case of an uncaught exception. Reply with DONE on success and with EXN if an exception was raised.
    • TERMINATE : Terminate the child process. Reply with DONE on success and with FAIL on failure.

An important design benefit of this approach is that the implementation is portable across Standard ML compilers, meaning that the Standard ML code involved does not depend on implementation details and dynamic linking support of a particular Standard ML compiler (the POSIX api will suffice). More specifically, the ML parent process, which hosts the compiler, will not need to perform any dynamic linking...

Progress

  • Parse and elaborate REPL input and report potential errors.
  • Initialise runtime at REPL startup time
  • Compile a well-typed topdec into an so-file, linking runtime and previous so-files.
  • Issue appropriate LOADRUN command after compilation.
  • Issue appropriate PRINT command(s).
  • Deal with exceptions by wrapping exception handlers around so-file initialisation code...
  • Allow for commands in the REPL. The syntax should be :cmd;, where cmd is a command:
    cmd ::= set flag | set flag N | set flag S | unset flag | quit | help | help flag | flags
  • Add commands :menu; and :menu N for displaying groupings of flags.
  • Allow for loading (maybe already compiled) MLB-files using the command syntax :load mlb-file. We'll first compile the MLB-file (if it hasn't been compiled yet) and then create an so-file from the o-files. The difficult part is to assemble the basis in a clever way. We can avoid loading all the deep bases for each modcode leaf and instead only load the elaboration bases. We can then compute which deep bases need to be loaded (similar to how the Manager does it for MLB compilation...)
  • Pretty-printing of more types of values (the basic printing supports only ints, booleans and reals). The modified design follows a type-indexed approach, where type information about a value is passed in the PRINT command. The design then makes use of a compiled type-indexed ML function, which has been exported to C (basis/repl.sml) and loaded together with the basis library at initialisation time. The difficult part is to make this design work with all representation of values in MLKit, including unboxed data types.
  • Automatic loading of the basis library (and pretty printer) at application start (unless -no_basislib is passed).
  • Setup some automatic testing...
  • Allow for multiple instances of the REPL to be started simultaneously. We can implement this feature by prefixing all generated files with a session-id.
  • Make the type-indexed pretty-printer more robust to redeclarations of type constructors by making use of internal type name stamps for type-indexed printing.
  • Make it possible to control (1) the depth of printing for lists and tree structures (and even cyclic graphs) and (2) the size of printed strings.
  • Some of the internally used buffers are of constant size. We should allow for automatic resizing of these buffers to allow for large types to be passed to the pretty printer and to allow for large pretty-printed values.

Open Questions

  1. How do we generate shared object files from the generated assembly code. Answer: After generating position-independent object files (which MLKit does now), just generate a corresponding so-file with gcc -shared -o file.so file.o -l....
  2. The type ty given to PRINT may be somewhat complex as the type needs to describe all datatypes used and the implementation details of each value constructor (i.e., boxity and tag). A nice aspect is that we can provide the "elaboration type" for shallow printing (without proceeding below abstract types) and the "implementation type" for deep printing (revealing the details of values with abstract types).
  3. We would like to have C-level REPL support built into the existing runtime system (e.g., runtimeSystem.a). The entry point of the runtime system is the function main, which parses command-line arguments and calls the function code. For non-REPL code generation, link-code is generated, which defines the function code. For REPL code generation, we link in initialisation code generated with CodeGenX64.generate_repl_init_code(), which declares the code function.

The Child Process

The child process is a program (started with a fork/exec pattern) that consists of the relevant runtime system (in fact, runtimeSystemX.a contains the main function), generated assembler code for allocating global regions, and a message loop that waits for messages from the parent (see above) and reacts accordingly.

A Mini Proof of Concept (https://gist.github.com/melsman/f3e8f587140fc1b8c82f630255b24169)

Below is a mini proof of concept in terms of a Makefile and a bunch of C-files illustrating the idea. The main.c function acts as the loading driver and runtime.c is the runtime system (region management, C-level wrappers for ML code, etc). The files a.c, b.c, and c.c reflect toplevel declarations. Whereas a.c can refer to identifiers (labels) in runtime.c, b.c can also refer to identifiers (labels) declared in a.c. Notice that each toplevel declaration is compiled into a shared library that is loaded by main.c. Whereas the proof of concept just loads these shared libraries explicitly with calls to dlopen and runs the initialisation code for each toplevel declaration using calls to dlsym, the real implementation will include an interpreter that loads and initialises shared libraries as a result of LOADRUN calls.

File Makefile

CC=/usr/local/bin/gcc-12

all: main.exe libruntime.so liba.so libb.so libc.so

main.exe: main.c libruntime.so
	$(CC) -o $@ $< -L . -lruntime

libruntime.so: runtime.o
	$(CC) -shared -o $@ $<

liba.so: a.o
	$(CC) -shared -o $@ $< -L . -lruntime

libb.so: b.o liba.so
	$(CC) -shared -o $@ $< -L . -lruntime -la

libc.so: c.o liba.so libb.so
	$(CC) -shared -o $@ $< -L . -lruntime -la -lb

# ...

%.o: %.c
	$(CC) -c -fPIC -o $@ $<

.PHONY: clean
clean:
	rm -rf *~ *.o *.so main.exe

File main.c

#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>

void* loadrun(char* lib, char* run) {
  void* hndl = dlopen(lib, RTLD_NOW);
  if ( !hndl ) {
    fprintf(stderr, "load: Error loading %s - %s", lib, dlerror());
    exit(EXIT_FAILURE);
  }
  void (*f)() = (void(*)())dlsym(hndl,run);
  if ( !f ) {
    fprintf(stderr, "dlsym: Error resolving %s - %s", run, dlerror());
    exit(EXIT_FAILURE);
  }
  f();
  return hndl;
}

void print_int(void* hndl, char* id) {
  int v = *(int*)dlsym(hndl,id);
  printf("val %s : int = %d\n", id, v);
}

int main() {
  void* a_hndl = loadrun("liba.so", "a_run");
  print_int(a_hndl, "a_it");
  void* b_hndl = loadrun("libb.so", "b_run");
  print_int(b_hndl, "b_it");
  void* c_hndl = loadrun("libc.so", "c_run");
  print_int(c_hndl, "c_it");
  return 0;
}

File runtime.c

int neg(int a) { return -a; }

File a.c

int add(int a, int b) { return a + b; }   // local in fun add(a,b) = a+b
int a_it;                                 //          val it = add(8,2)
void a_run() { a_it = add(8,2); }         // end

File b.c

extern int neg(int a);           // in runtime
extern int add(int a, int b);    // in unit a
extern int a_it;
int muladd(int a, int b, int c) { return neg(neg(add(a * b, c))); }
int b_it;
void b_run() { b_it = add(a_it, muladd(a_it,2,100)); }

File c.c

extern int add(int a, int b);             // in unit a
extern int muladd(int a, int b, int c);   // in unit b
extern int a_it;
extern int b_it;
int muladdadd(int a, int b, int c) { return add(muladd(a, b, c),c); }
int c_it;
void c_run() { c_it = muladdadd(add(a_it,3),b_it,b_it); }

Running this program yields the following output:

bash-3.2$ ./main.exe 
val a_it : int = 10
val b_it : int = 130
val c_it : int = 1950

@melsman melsman self-assigned this Oct 12, 2023
@melsman melsman marked this pull request as draft October 12, 2023 17:06
@melsman melsman mentioned this pull request Oct 12, 2023
@athas
Copy link
Contributor

athas commented Oct 13, 2023

Interesting model. Will code ever be unloaded again? I suppose resetting the REPL could be done simply by restarting the subprocess.

@melsman
Copy link
Owner Author

melsman commented Oct 13, 2023 via email

@athas
Copy link
Contributor

athas commented Oct 13, 2023

I think this is a decent model. Here are the disadvantages I can think of:

  1. Unclear when code/data can be freed.
  2. Relatively high overhead for entering small expressions.
  3. Unclear how value definitions will work in the LOADRUN model.
  4. More movable parts (child process executable must be findable, must ensure it does not linger).

I don't think (1) is a big problem. Just make resetting the state (by resetting the process) an expected part of the workflow; e.g. by doing so whenever a file is freshly loaded. This is like ghci and futhark repl does it. The hyper-dynamic mutable environment model used by e.g. MosML is somewhat less common today.

(2) is probably also not a problem with the speed of modern computers, and the speed of MLKit. The delay will likely be noticeable (maybe 0.5 seconds?), but I don't think it will significantly impede productivity.

I think (3) is a bigger problem. My model for working with REPLs often involve doing a few value bindings and then poking at them. I suppose I could put them in files instead.

I suspect this goal is too optimistic:

An important design benefit of this approach is that the implementation is portable across Standard ML compilers.

Which Standard ML compilers could possibly be used this way? Except for MLKit, they all depend on heavy runtime systems, and e.g. MLton only supports whole program compilation. Also, most of them already have perfectly serviceable REPLs. While there is nothing wrong with a portable REPL per se, I don't think it is worth complicating the design in order to support portability.

@melsman
Copy link
Owner Author

melsman commented Oct 13, 2023 via email

@melsman
Copy link
Owner Author

melsman commented Oct 15, 2023 via email

@athas
Copy link
Contributor

athas commented Oct 19, 2023

The diff is a bit hard for me to grasp, but I'm very much looking forward to trying this out! When do you think a runnable prototype is ready?

@melsman
Copy link
Owner Author

melsman commented Oct 19, 2023 via email

@athas
Copy link
Contributor

athas commented Oct 23, 2023

Is this expected to work under Linux yet? I get a very long list of dynamic linker errors when starting bin/mlkit:

$ SML_LIB=. ./bin/mlkit
MLKit v4.7.5 (v4.7.5-9-g4bc3e018 - 2023-10-23T02:18:01+02:00) [X64 Backend]
Disabling garbage collection - it is not supported with the REPL
Type :help; for help...
/nix/store/74y3751gsixaz9797ib0hp7c658sp1y5-binutils-2.40/bin/ld: /nix/store/h5kvfrjmpw792v8jg7nrzfkffmn0iyy8-gcc-12.3.0/lib/gcc/x86_64-unknown-linux-gnu/12.3.0/libgcc.a(_muldi3.o): in function `__multi3':
(.text+0x0): multiple definition of `__multi3'; /nix/store/h5kvfrjmpw792v8jg7nrzfkffmn0iyy8-gcc-12.3.0/lib/gcc/x86_64-unknown-linux-gnu/12.3.0/libgcc.a(_muldi3.o):(.text+0x0): first defined here
/nix/store/74y3751gsixaz9797ib0hp7c658sp1y5-binutils-2.40/bin/ld: /nix/store/h5kvfrjmpw792v8jg7nrzfkffmn0iyy8-gcc-12.3.0/lib/gcc/x86_64-unknown-linux-gnu/12.3.0/libgcc.a(_negdi2.o): in function `__negti2':
(.text+0x0): multiple definition of `__negti2'; /nix/store/h5kvfrjmpw792v8jg7nrzfkffmn0iyy8-gcc-12.3.0/lib/gcc/x86_64-unknown-linux-gnu/12.3.0/libgcc.a(_negdi2.o):(.text+0x0): first defined here
/nix/store/74y3751gsixaz9797ib0hp7c658sp1y5-binutils-2.40/bin/ld: /nix/store/h5kvfrjmpw792v8jg7nrzfkffmn0iyy8-gcc-12.3.0/lib/gcc/x86_64-unknown-linux-gnu/12.3.0/libgcc.a(_lshrdi3.o): in function `__lshrti3':
(.text+0x0): multiple definition of `__lshrti3'; /nix/store/h5kvfrjmpw792v8jg7nrzfkffmn0iyy8-gcc-12.3.0/lib/gcc/x86_64-unknown-linux-gnu/12.3.0/libgcc.a(_lshrdi3.o):(.text+0x0): first defined here
/nix/store/74y3751gsixaz9797ib0hp7c658sp1y5-binutils-2.40/bin/ld: /nix/store/h5kvfrjmpw792v8jg7nrzfkffmn0iyy8-gcc-12.3.0/lib/gcc/x86_64-unknown-linux-gnu/12.3.0/libgcc.a(_ashldi3.o): in function `__ashlti3':
(.text+0x0): multiple definition of `__ashlti3'; /nix/store/h5kvfrjmpw792v8jg7nrzfkffmn0iyy8-gcc-12.3.0/lib/gcc/x86_64-unknown-linux-gnu/12.3.0/libgcc.a(_ashldi3.o):(.text+0x0): first defined here
/nix/store/74y3751gsixaz9797ib0hp7c658sp1y5-binutils-2.40/bin/ld: /nix/store/h5kvfrjmpw792v8jg7nrzfkffmn0iyy8-gcc-12.3.0/lib/gcc/x86_64-unknown-linux-gnu/12.3.0/libgcc.a(_ashrdi3.o): in function `__ashrti3':
(.text+0x0): multiple definition of `__ashrti3'; /nix/store/h5kvfrjmpw792v8jg7nrzfkffmn0iyy8-gcc-12.3.0/lib/gcc/x86_64-unknown-linux-gnu/12.3.0/libgcc.a(_ashrdi3.o):(.text+0x0): first defined here
/nix/store/74y3751gsixaz9797ib0hp7c658sp1y5-binutils-2.40/bin/ld: /nix/store/h5kvfrjmpw792v8jg7nrzfkffmn0iyy8-gcc-12.3.0/lib/gcc/x86_64-unknown-linux-gnu/12.3.0/libgcc.a(_cmpdi2.o): in function `__cmpti2':
(.text+0x0): multiple definition of `__cmpti2'; /nix/store/h5kvfrjmpw792v8jg7nrzfkffmn0iyy8-gcc-12.3.0/lib/gcc/x86_64-unknown-linux-gnu/12.3.0/libgcc.a(_cmpdi2.o):(.text+0x0): first defined here
/nix/store/74y3751gsixaz9797ib0hp7c658sp1y5-binutils-2.40/bin/ld: /nix/store/h5kvfrjmpw792v8jg7nrzfkffmn0iyy8-gcc-12.3.0/lib/gcc/x86_64-unknown-linux-gnu/12.3.0/libgcc.a(_ucmpdi2.o): in function `__ucmpti2':
(.text+0x0): multiple definition of `__ucmpti2'; /nix/store/h5kvfrjmpw792v8jg7nrzfkffmn0iyy8-gcc-12.3.0/lib/gcc/x86_64-unknown-linux-gnu/12.3.0/libgcc.a(_ucmpdi2.o):(.text+0x0): first defined here
/nix/store/74y3751gsixaz9797ib0hp7c658sp1y5-binutils-2.40/bin/ld: /nix/store/h5kvfrjmpw792v8jg7nrzfkffmn0iyy8-gcc-12.3.0/lib/gcc/x86_64-unknown-linux-gnu/12.3.0/libgcc.a(_clear_cache.o): in function `__clear_cache':
(.text+0x0): multiple definition of `__clear_cache'; /nix/store/h5kvfrjmpw792v8jg7nrzfkffmn0iyy8-gcc-12.3.0/lib/gcc/x86_64-unknown-linux-gnu/12.3.0/libgcc.a(_clear_cache.o):(.text+0x0): first defined here
...
/nix/store/74y3751gsixaz9797ib0hp7c658sp1y5-binutils-2.40/bin/ld: ./lib/runtimeSystem.a(Runtime.o): relocation R_X86_64_32 against `.rodata.str1.1' can not be used when making a shared object; recompile with -fPIC
/nix/store/74y3751gsixaz9797ib0hp7c658sp1y5-binutils-2.40/bin/ld: failed to set dynamic section sizes: bad value
collect2: error: ld returned 1 exit status
/nix/store/74y3751gsixaz9797ib0hp7c658sp1y5-binutils-2.40/bin/ld: cannot find -lruntime: No such file or directory
collect2: error: ld returned 1 exit status
uncaught exception SysErr exec failed: No such file or directory

@melsman
Copy link
Owner Author

melsman commented Oct 23, 2023

It seems that only my local Makefile (not Makefile.in) was setup to compile the runtime system with -fPIC. I'll run some tests and checkin a new version of Makefile.in...

@melsman
Copy link
Owner Author

melsman commented Oct 23, 2023

There seems to be some issues with the dynamic linking under Linux (Ubuntu) that I cannot fix before having access to a physical machine or at least some development machine I can login to... @athas : any good suggestions for a machine I can login to at DIKU? The old gpu0x-servers mentioned at http://github.com/diku-dk/howto don't seem to work anymore and I have limited success with the hendrix cluster (I'm awaiting some approval on identity.ku.dk, I think)...

@athas
Copy link
Contributor

athas commented Oct 23, 2023

You should have access to these.

@melsman
Copy link
Owner Author

melsman commented Oct 24, 2023

It works now under linux to some extend:

mael@borg:~/mlkit$ uname -a
Linux borg 6.2.0-32-generic #32-Ubuntu SMP PREEMPT_DYNAMIC Mon Aug 14 10:03:50 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
mael@borg:~/mlkit$ rm -rf MLB && LD_LIBRARY_PATH=MLB/RI SML_LIB=. ./bin/mlkit
MLKit v4.7.5 (v4.7.5-13-ge7c3d35b - 2023-10-23T14:25:48+02:00) [X64 Backend]
Disabling garbage collection - it is not supported with the REPL
Type :help; for help...
. :load basis/basis.mlb;
/usr/bin/ld: warning: type and size of dynamic symbol `D.exn_DIV_aEVvxkLlDOnB1Tid8reifn' are not defined
/usr/bin/ld: warning: type and size of dynamic symbol `TopLevelHandlerLab' are not defined
/usr/bin/ld: warning: type and size of dynamic symbol `D.exn_SIZE_plABblGdISSt0pxClkLYIr' are not defined
/usr/bin/ld: warning: type and size of dynamic symbol `exnameCounter' are not defined
/usr/bin/ld: warning: type and size of dynamic symbol `D.exn_SUBSCRIPT_dpELiXqdrK4rAJtB3aGcNp' are not defined
/usr/bin/ld: warning: type and size of dynamic symbol `D.exn_OVERFLOW_b2uexLVzE8IkeyIStvA5Oa' are not defined
/usr/bin/ld: warning: type and size of dynamic symbol `D.exn_MATCH_3EbfP0vTHygkqnCyrZnRYe' are not defined
. print(IntInf.toString (23423423423423423423432432 * 45345345345345345) ^ "\n");
/usr/bin/ld: warning: type and size of dynamic symbol `D.cc1484_lab134_yqJZtbBlpSHLSNZBeKjYhj' are not defined
/usr/bin/ld: warning: type and size of dynamic symbol `TopLevelHandlerLab' are not defined
/usr/bin/ld: warning: type and size of dynamic symbol `D.cc95_lab28_VDYTM6oj13TjPDYR4nHagr' are not defined
1062143224305386459459867956475394313229040
> val it = () : unit
. 

Dynamic linking and position independent code works annoyingly differently with elf and mach-o... I still need to get rid of the ld warnings and to do something about having to set LD_LIBRARY_PATH...

@athas
Copy link
Contributor

athas commented Oct 25, 2023

The sign bit in a nan is not meaningful. Does SML really require that you print it like that?

@melsman
Copy link
Owner Author

melsman commented Oct 25, 2023 via email

@melsman
Copy link
Owner Author

melsman commented Oct 25, 2023 via email

@athas
Copy link
Contributor

athas commented Oct 31, 2023

Cool, it works!

I suggest terminating parsing at every newline. I also supported multi-line input in the Futhark REPL for a while, but I ditched it because it is ultimately too confusing for users. It is also annoying to have to type a semicolon after every input.

@athas
Copy link
Contributor

athas commented Oct 31, 2023

If you enter EOF (Ctrl-d), the REPL will go into an infinite loop. This is because the PE.FAILURE case in Repl.sml calls repl recursively to "clear the state", which will itself enter the EOF condition, and so on.

@melsman
Copy link
Owner Author

melsman commented Oct 31, 2023

I suggest terminating parsing at every newline. I also supported multi-line input in the Futhark REPL for a while, but I ditched it because it is ultimately too confusing for users. It is also annoying to have to type a semicolon after every input.

I kind of like the multi-line input support, which allows for general pasting of source code with newlines...

@athas
Copy link
Contributor

athas commented Oct 31, 2023

It's your call, but you can look forward to spending the rest of your life answering questions from users as to why nothing happens when they enter an expression.

@melsman
Copy link
Owner Author

melsman commented Oct 31, 2023

It's your call, but you can look forward to spending the rest of your life answering questions from users as to why nothing happens when they enter an expression.

I'll just ask them to try out the example with MosML... ;)

@kfl
Copy link
Contributor

kfl commented Nov 1, 2023

I'm sure that the well-oiled Moscow ML support team can handle the torrent of MLKit REPL users.

Beside, SML users seems to be rather proficient using a REPL. It has been at least a decade since I got a question like the one @athas is worried about.

@athas
Copy link
Contributor

athas commented Nov 1, 2023

That is because there have been no new SML users since then.

@melsman melsman marked this pull request as ready for review November 1, 2023 23:16
@melsman melsman merged commit 685fe42 into master Nov 2, 2023
4 checks passed
@athas
Copy link
Contributor

athas commented Nov 2, 2023

Hooray, the day of interactive MLKit is upon us!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants