-
Notifications
You must be signed in to change notification settings - Fork 120
Prompt IO Built-in Method #1688
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
Conversation
90eff05 to
4805807
Compare
|
Good job finding your way around the codebase! The main thing I'd suggest is not putting this in It uses the I'd also consider implementing this only with That said, if |
4805807 to
29cc2ba
Compare
|
@jpolitz working on this slowly here and there, so will ping and mark as ready for review when it is time. Thanks for the response!
Thanks for pointing out the move to a library in
100% agree. I have an aversion to just adding libraries for no reason - day job is in Ruby.... so the "there is a gem for that" mentality irks me 😅 . Something like Plan to get this into a reviewable state is to add some tests and read the source of some of these node packages and see what's going on under the hood. |
|
@jpolitz afaik, there is not a whole lot of mocking capabilities for Pyret. I am digging through existing tests, but not 100% sure how to write a test to ensure the I am a software engineer by day and a PR would (and should?) never be merged w/o tests, however I am struggling to figure out the best way to test this code. Let me know your thoughts. In the meantime I will keep digging. |
src/js/trove/iolib.js
Outdated
| nativeRequires: ["fs"], | ||
| theModule: function(RUNTIME, NAMESPACE, uri, fs) { | ||
| // https://github.com/nodejs/node/issues/28243#issuecomment-502402453 | ||
| function PromptIO(msg) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the original comment, you were considering input rather than prompt. Wouldn't it be more orthogonal to implement input, and then implement prompt as a combination of print and input?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I like the synchronous nature of this implementation. Since Pyret has the ability to pause the Pyret stack while an async operation occurs, and then resume the Pyret code with the results of that stack, I think it'd probably be cleaner to implement this using async-IO and pausing/resuming Pyret. @jpolitz what's your take?
src/js/trove/iolib.js
Outdated
| }).then(result => { | ||
| restarter.resume(RUNTIME.makeString(result)); | ||
| }).catch(error => { | ||
| restarter.resume(RUNTIME.makeString(error)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels problematic to me -- it means you're suppressing the error and making both the normal case and the error case to have the same behavior, namely of returning a string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed - silently failing is terrible (found out some code wasn't running in prod once 4 months later because of a silent failure...). Will handle this more correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated this, but will revisit after we have consensus on a proper test strategy - see below.
examples/guess-the-number.arr
Outdated
| @@ -0,0 +1,56 @@ | |||
| #lang pyret | |||
|
|
|||
| import iolib as IO | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My first "real" pyret program. So there is certainly room for improvement.
src/js/trove/iolib.js
Outdated
| }; | ||
|
|
||
| function wrapInputTest(testInput) { | ||
| return Input(mockIO = (rl) => rl.write(testInput + '\n')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is my least hacky attempt at a reasonable, repeatable test. That being said, baking it into the actual library like this and exposing it as a public interface does not seem right.
Maybe a test helper or a test only module?
The only solutions I have come up with thus far require interfacing with JS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@blerner my biggest struggle here is figuring out a reasonable test strategy. What I have here certainly works and is less hacky than my first solution but still not as clean or comprehensive as what jest/mocha or any other "production" mocking library provides.
An ideal solution (with heavy influence from Ruby world) might look something like:
// set up mock
before_check:
stdin = mock-stdin();
// inject/queue-up/send/etc.
stdin.inject("hello")
end
// execute test
check:
input() is "hello"
end
Do we want to investigate this? It feels like scope creep but it also feels like a pre-requisite for this work? Thoughts?
Beyond that, thanks for the multiple rounds of reviews so far! I apologize, the PR was pretty messy to start with - thanks for your patience here, very much appreciated as I ramp up on this codebase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most likely, this will require writing a test harness in JS, rather than purely in Pyret. We have some tests that have been written this way, but I think they aren't quite what you want to emulate here. I'll need to talk to Joe a bit and bounce ideas together with him to see what we thing you ought to build here.
The idea of a before-check block is interesting, but definitely out of scope for now. There are a lot of gnarly questions that would need to be designed (e.g. when does stdin get "reset" in your example to the "real" stdin? or what should the order of execution be when you have where blocks inside functions that are called by before_check or other check blocks? A Pyret program weaves together testing and normal running into one source file, in ways that "real test code" typically doesn't...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think instead of mock-input-test for now, you probably just want to invoke your tests as exec("echo theInput | node theTest"), more or less. Something similar to https://github.com/brownplt/pyret-lang/blob/anchor/tests-new/simple-output.test.js or https://github.com/brownplt/pyret-lang/blob/anchor/tests-new/check-blocks.test.js#L13-L26, which are a test harness (not on the current #horizon branch, though!) that executes Pyret programs and checks their standard-out; you could pretty easily pipe in the necessary standard-in.
src/js/trove/iolib.js
Outdated
| .then(result => restarter.resume(RUNTIME.makeString(result))) | ||
| .catch(error => { | ||
| // TODO: we should probably NOT fail this hard | ||
| restarter.error(RUNTIME.makeString(error)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not 100% sure how to trigger this so it fails. Started looking at mocking, unsure how that will play with the harness as is, may need to design it to be a bit more robust. Is a failure test something desired in a v1 implementation?
tests/io-tests/io.test.js
Outdated
|
|
||
| const executionStdin = runProcess.stdout.toString(); | ||
| expect(executionStdin).toMatch(new RegExp(stdioExpected)); | ||
| expect(executionStdin).toMatch(new RegExp(stdInexpected)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a bit weird but felt better than just "contains"
tests/io-tests/io.test.js
Outdated
| expect(runProcess.status).not.toEqual(SUCCESS_EXIT_CODE); | ||
|
|
||
| const executionStderr = runProcess.stderr.toString(); | ||
| expect(executionStderr).toMatch(new RegExp(stderrExpected)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
exact matching on error msg vs. error type feels brittle. Thoughts?
|
Failing with a weird |
|
Bumped as per Slack: https://pyret.slack.com/archives/C1GUPMUV7/p1688392539306329 |
| provides: { | ||
| shorthands: { }, | ||
| values: { | ||
| "prompt": ["arrow", ["String"], "String"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have Haskell on my mind - in Haskell world this would be something like:
prompt :: IO String -> StringIO in Haskell took me a little while to wrap my head around. Fairly confident we don't want to introduce this here.
| const fs = require('fs'); | ||
| const cp = require('child_process'); | ||
|
|
||
| const COMPILER_TIMEOUT = 60000; // ms, for each compiler run (including startup) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tests/io-tests/io.test.js
Outdated
| const runProcess = cp.spawnSync("sh", [ | ||
| "-c", | ||
| `echo ${stdInToInject} | node ${COMPILED_CODE_PATH}` | ||
| ], {stdio: 'pipe', stderr: "pipe", timeout: RUN_TIMEOUT}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if the reason you're getting such slow behavior here is because you're spawning a process with a pipe, but then never closing the pipe, and so for the empty-input case, it's waiting all the way until timeout? So instead of using echo ... | node ..., you can do spawnSync("node" [COMPILED_CODE_PATH], {input: stdInToInject, stdio: 'pipe', timeout: RUN_TIMEOUT}), and just read the results from stdout and stderr as before -- this might close stdin much sooner?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems just removing stdin: 'pipe' might have really sped things up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, that makes no sense...
The options.stdio option is used to configure the pipes that are established between the parent and child process. By default, the child's stdin, stdout, and stderr are redirected to corresponding subprocess.stdin, subprocess.stdout, and subprocess.stderr streams on the ChildProcess object. This is equivalent to setting the options.stdio equal to ['pipe', 'pipe', 'pipe'].
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah - must have done something weird 🤷 . CI failed.
|
🎉 🎉 🎉 |

Prompt IO Built-in Method
Adherence to Pyret's Philosophy and Core Values
Reading from stdin is a fundamental first step towards creating interactive software. I have fond memories of the earliest days of my coding career, creating simple games such as a random number guesser, tic-tac-toe, and blackjack, all of which required a stdin method to take input from the player. With the right method name, this new method should be intuitive and easy to use, a quick addition to any curious novice coder's repertoire of standard library methods.
Naming
I have chosen
inputsince it seems the most intuitive to me. Furthermore, it is the same as Python.However, the following names were also considered:
Type
While Pyret is dynamic, the method type should look like this:
Example Usage
Note(s) to Reviewer(s)
** This is a WIP - my first PR here so need to do some reading about best practices, boilerplate generation, what to check in, etc. Thus, consider this PR incomplete and not ready for review until it is no longer a draft. **