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

Defer Script Initialization to Allow Building the Benchmarks #736

Merged
merged 3 commits into from
Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners
# Consider using larger runners for possible analysis time improvements.
runs-on: ${'ubuntu-latest'}
runs-on: [ubuntu-latest]
timeout-minutes: ${{ 360 }}
permissions:
actions: read
Expand Down
86 changes: 57 additions & 29 deletions src/cli/repl/commands/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,21 @@ export const helpCommand: ReplCommand = {
usageExample: ':help',
aliases: [ 'h', '?' ],
fn: output => {
initCommandMapping()
output.stdout(`
You can always just enter R expressions which get evaluated right away:
${rawPrompt} ${bold('1 + 1', output.formatter)}
${italic('[1] 2', output.formatter)}

Besides that, you can use the following commands. The scripts ${italic('can', output.formatter)} accept further arguments. There are the following basic commands:
${
Array.from(Object.entries(commands)).filter(([, { script }]) => !script).map(
Array.from(Object.entries(commands())).filter(([, { script }]) => !script).map(
c => printHelpForScript(c, output.formatter)).join('\n')
}

Furthermore, you can directly call the following scripts which accept arguments. If you are unsure, try to add ${italic('--help', output.formatter)} after the command.
${
Array.from(Object.entries(commands)).filter(([, { script }]) => script).map(
Array.from(Object.entries(commands())).filter(([, { script }]) => script).map(
([command, { description }]) => ` ${bold(padCmd(':' + command), output.formatter)}${description}`).join('\n')
}

Expand All @@ -54,7 +55,7 @@ You can combine commands by separating them with a semicolon ${bold(';',output.f
/**
* All commands that should be available in the REPL.
*/
const commands: Record<string, ReplCommand> = {
const _commands: Record<string, ReplCommand> = {
'help': helpCommand,
'quit': quitCommand,
'version': versionCommand,
Expand All @@ -67,50 +68,71 @@ const commands: Record<string, ReplCommand> = {
'controlflow': controlflowCommand,
'controlflow*': controlflowStarCommand
}
let commandsInitialized = false

for(const [script, { target, description, type }] of Object.entries(scripts)) {
if(type === 'master script') {
commands[script] = {
description,
aliases: [],
script: true,
usageExample: `:${script} --help`,
fn: async(output, _s, remainingLine) => {
await waitOnScript(
`${__dirname}/../../${target}`,
splitAtEscapeSensitive(remainingLine),
stdio => stdioCaptureProcessor(stdio, msg => output.stdout(msg), msg => output.stderr(msg))
)
function commands() {
if(commandsInitialized) {
return _commands
}
commandsInitialized = true
for(const [script, { target, description, type }] of Object.entries(scripts)) {
if(type === 'master script') {
_commands[script] = {
description,
aliases: [],
script: true,
usageExample: `:${script} --help`,
fn: async(output, _s, remainingLine) => {
await waitOnScript(
`${__dirname}/../../${target}`,
splitAtEscapeSensitive(remainingLine),
stdio => stdioCaptureProcessor(stdio, msg => output.stdout(msg), msg => output.stderr(msg))
)
}
}
}
}
return _commands
}


/**
* The names of all commands including their aliases (but without the leading `:`)
*/
export const commandNames: string[] = []
export function getCommandNames(): string[] {
if(commandNames === undefined) {
initCommandMapping()
}
return commandNames as string[]
}
let commandNames: string[] | undefined = undefined
// maps command names or aliases to the actual command name
const commandMapping: Record<string, string> = {}
let commandMapping: Record<string, string> | undefined = undefined

for(const [command, { aliases }] of Object.entries(commands)) {
guard(commandMapping[command] as string | undefined === undefined, `Command ${command} is already registered!`)
commandMapping[command] = command
for(const alias of aliases) {
guard(commandMapping[alias] as string | undefined === undefined, `Command (alias) ${alias} is already registered!`)
commandMapping[alias] = command
function initCommandMapping() {
commandMapping = {}
commandNames = []
for(const [command, { aliases }] of Object.entries(commands())) {
guard(commandMapping[command] as string | undefined === undefined, `Command ${command} is already registered!`)
commandMapping[command] = command
for(const alias of aliases) {
guard(commandMapping[alias] as string | undefined === undefined, `Command (alias) ${alias} is already registered!`)
commandMapping[alias] = command
}
commandNames.push(command)
commandNames.push(...aliases)
}
commandNames.push(command)
commandNames.push(...aliases)
}

/**
* Get the command for a given command name or alias.
* @param command - The name of the command (without the leading `:`)
*/
export function getCommand(command: string): ReplCommand | undefined {
return commands[commandMapping[command]]
if(commandMapping === undefined) {
initCommandMapping()
}
return commands()[(commandMapping as Record<string, string>)[command]]
}

export function asOptionName(argument: string): string{
Expand All @@ -122,7 +144,13 @@ export function asOptionName(argument: string): string{
}


const longestKey = Array.from(Object.keys(commands), k => k.length).reduce((p, n) => Math.max(p, n), 0)
let _longestCommandName: number | undefined = undefined
export function longestCommandName(): number {
if(_longestCommandName === undefined) {
_longestCommandName = Array.from(Object.keys(commands()), k => k.length).reduce((p, n) => Math.max(p, n), 0)
}
return _longestCommandName
}
function padCmd<T>(string: T) {
return String(string).padEnd(longestKey + 2, ' ')
return String(string).padEnd(longestCommandName() + 2, ' ')
}
14 changes: 10 additions & 4 deletions src/cli/repl/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { fileProtocol, RShell } from '../../r-bridge'
import { bold } from '../../statistics'
import { prompt } from './prompt'
import type { ReplOutput } from './commands'
import { commandNames, getCommand, standardReplOutput } from './commands'
import { getCommandNames , getCommand, standardReplOutput } from './commands'
import * as readline from 'readline'
import { splitAtEscapeSensitive } from '../../util/args'
import { executeRShellCommand } from './commands/execute'
Expand All @@ -16,7 +16,13 @@ import path from 'path'
import fs from 'fs'
import { getValidOptionsForCompletion, scripts } from '../common'

const replCompleterKeywords = Array.from(commandNames, s => `:${s}`)
let _replCompleterKeywords: string[] | undefined = undefined
function replCompleterKeywords() {
if(_replCompleterKeywords === undefined) {
_replCompleterKeywords = Array.from(getCommandNames(), s => `:${s}`)
}
return _replCompleterKeywords
}
const defaultHistoryFile = path.join(os.tmpdir(), '.flowrhistory')

/**
Expand All @@ -29,7 +35,7 @@ export function replCompleter(line: string): [string[], string] {

// if we typed a command fully already, autocomplete the arguments
if(splitLine.length > 1 || startingNewArg){
const commandNameColon = replCompleterKeywords.find(k => splitLine[0] === k)
const commandNameColon = replCompleterKeywords().find(k => splitLine[0] === k)
if(commandNameColon) {
const completions: string[] = []

Expand All @@ -52,7 +58,7 @@ export function replCompleter(line: string): [string[], string] {
}

// if no command is already typed, just return all commands that match
return [replCompleterKeywords.filter(k => k.startsWith(line)).map(k => `${k} `), line]
return [replCompleterKeywords().filter(k => k.startsWith(line)).map(k => `${k} `), line]
}

export const DEFAULT_REPL_READLINE_CONFIGURATION: readline.ReadLineOptions = {
Expand Down
16 changes: 8 additions & 8 deletions wiki/Interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ docker run -p1042:1042 -it --rm eagleoutice/flowr --server

##### Using Netcat

Now, using a tool like [netcat](https://linux.die.net/man/1/nc) to connect:
Now, using a tool like _netcat_ to connect:

```shell
nc 127.0.0.1 1042
Expand Down Expand Up @@ -944,22 +944,22 @@ R> :parse file://test/testfiles/example.R

### Interfacing With R by Using The `RShell`

The [`RShell`](https://code-inspect.github.io/flowr/doc/classes/src_r_bridge_shell.RShell.html) class allows to interface with the `R`&nbsp;ecosystem installed on the host system.
The `RShell` class allows to interface with the `R`&nbsp;ecosystem installed on the host system.
For now there are no alternatives (although we plan on providing more flexible drop-in replacements).

> [!IMPORTANT]
> Each `RShell` controls a new instance of the R&nbsp;interpreter, make sure to call `RShell::close()` when you are done.

You can start a new "session" simply by constructing a new object with `new RShell()`.
However, there are several options which may be of interest (e.g., to automatically revive the shell in case of errors or to control the name location of the R process on the system). See the [documentation](https://code-inspect.github.io/flowr/doc/classes/src_r_bridge_shell.RShell.html) for more information.
However, there are several options which may be of interest (e.g., to automatically revive the shell in case of errors or to control the name location of the R process on the system). See the in-code _documentation_ for more information.

With a shell object (let's call it `shell`), you can execute R code by using `RShell::sendCommand`, for example `shell.sendCommand("1 + 1")`. However, this does not return anything, so if you want to collect the output of your command, use `RShell::sendCommandWithOutput` instead.

Besides that, the command `RShell::tryToInjectHomeLibPath` may be of interest, as it enables all libraries available on the host system.

### Slicing With The `SteppingSlicer`

The main class that represents *flowR*'s slicing is the [`SteppingSlicer`](https://code-inspect.github.io/flowr/doc/classes/src_core_slicer.SteppingSlicer.html) class. With *flowR*, this allows you to slice code like this:
The main class that represents *flowR*'s slicing is the `SteppingSlicer` class. With *flowR*, this allows you to slice code like this:

```typescript
const shell = new RShell()
Expand Down Expand Up @@ -988,7 +988,7 @@ Besides slicing, the stepping slicer:
2. can be executed step-by-step
3. can be told to stop after a given step

See the [documentation](https://code-inspect.github.io/flowr/doc/classes/src_core_slicer.SteppingSlicer.html) for more.
See the _documentation_ for more.

#### Understanding the Steps

Expand All @@ -1001,7 +1001,7 @@ If you add a new step, make sure to modify all of these locations accordingly.

#### Benchmark the Slicer With The `BenchmarkSlicer`

Relying on the `SteppingSlicer`, the [`BenchmarkSlicer`](https://code-inspect.github.io/flowr/doc/classes/src_benchmark_slicer.BenchmarkSlicer.html) instruments each step to allow measuring the required time. It is used by the `benchmark` script, explained in the [overview](https://github.com/Code-Inspect/flowr/wiki/Overview) wiki page.
Relying on the `SteppingSlicer`, the `BenchmarkSlicer` instruments each step to allow measuring the required time. It is used by the `benchmark` script, explained in the [overview](https://github.com/Code-Inspect/flowr/wiki/Overview) wiki page.
Furthermore, it provides a simple way to slice a file for all possible slicing points:

```typescript
Expand All @@ -1021,7 +1021,7 @@ Please create a new `BenchmarkSlicer` object per input file (this will probably

### Augmenting the Normalization

The normalization of a given input is essentially handled by the [`normalize` function](https://code-inspect.github.io/flowr/doc/functions/src_r_bridge.normalize.html) although it is better to use the abstraction of the `SteppingSlicer` and use `executeSingleSubStep('normalize', <remaining arguments>)` to invoke the respective step.
The normalization of a given input is essentially handled by the `normalize` function although it is better to use the abstraction of the `SteppingSlicer` and use `executeSingleSubStep('normalize', <remaining arguments>)` to invoke the respective step.
The call accepts a collection of *hooks* (the configuration of the `SteppingSlicer` allows them as well).

These hooks allow the modification of the inputs and outputs of the normalization. If you want to count the amount of strings encountered while parsing, you can use something like this:
Expand All @@ -1046,7 +1046,7 @@ await new SteppingSlicer({
// console.log(counter)
```

The `after` hook is called after the normalization has created the respective normalized string node, so we can be sure that the node was indeed a string! Besides incrementing the respective counter, we could return a value that the normalization should use instead (but we do not do that in this example). See the [documentation](https://code-inspect.github.io/flowr/doc/interfaces/src_r_bridge_lang_4_x_ast_parser_xml_hooks.XmlParserHooks.html) for more information.
The `after` hook is called after the normalization has created the respective normalized string node, so we can be sure that the node was indeed a string! Besides incrementing the respective counter, we could return a value that the normalization should use instead (but we do not do that in this example).

### Generate Statistics

Expand Down
10 changes: 5 additions & 5 deletions wiki/Linting and Testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,12 @@ From your IDE of choice, you can also run all or some of the functionality tests
With Visual Studio Code (or Codium), you also require the Mocha Test Explorer add-on. To run functionality tests, follow these steps:

1. Install and enable the [Mocha Test Explorer](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-mocha-test-adapter).
2. In your copy of the flowR repository, open the Testing menu. You should see all functionality tests available for execution, like this:
2. In your copy of the flowR repository, open the Testing menu. You should see all functionality tests available for execution, like this:

![Overview on all functionality tests in VS Code](img/testing-vs-code.png)

3. To run the full test suite, press the Play button (▶️) above.
- To only run a single, or some of the tests, navigate to it, and press the Play button, too.
3. To run the full test suite, press the Play button (▶️) above.
- To only run a single, or some of the tests, navigate to it, and press the Play button, too.
- You can cancel running tests by clicking on the Stop button (⏹️).
- Successful tests are marked with a checkmark (✅), while failing tests are marked with a cross (❌).
4. To debug a failing test, navigate to it, and then press the Debug (🪲) button. This will automatically open the Run and Debug menu of VS Code.
Expand All @@ -109,7 +109,7 @@ With WebStorm, you can set up Run and Debug configurations from the IDE to run t
![A possible Run configuration for flowR's functionality tests in WebStorm](img/testing-config-webstorm.png)

4. Press `OK` to save the test run configuration.

Afterwards, you can run or debug the flowR functionality test suite from the Run/Debug configurations part, by clicking on the Play and Debug buttons (▶️/🪲), respectively.

## CI Pipeline
Expand Down Expand Up @@ -160,4 +160,4 @@ However, in case you think that the linter is wrong, please do not hesitate to o

### License Checker

*flowR* is licensed under the [GPLv3 License](LICENSE) requiring us to only rely on [compatible licenses](https://www.gnu.org/licenses/license-list.en.html). For now, this list is hardcoded as part of the npm [`license-compat`](../package.json) script so it can very well be that a new dependency you add causes the checker to fail &mdash; *even though it is compatible*. In that case, please either open a [new issue](https://github.com/Code-Inspect/flowr/issues/new/choose) or directly add the license to the list (including a reference to why it is compatible).
*flowR* is licensed under the [GPLv3 License](https://github.com/Code-Inspect/flowr/blob/main/LICENSE) requiring us to only rely on [compatible licenses](https://www.gnu.org/licenses/license-list.en.html). For now, this list is hardcoded as part of the npm [`license-compat`](../package.json) script so it can very well be that a new dependency you add causes the checker to fail &mdash; *even though it is compatible*. In that case, please either open a [new issue](https://github.com/Code-Inspect/flowr/issues/new/choose) or directly add the license to the list (including a reference to why it is compatible).
Loading