Search code examples
rswiftknitrread-eval-print-loopblogdown

Using Swift as custom engine in knitr and including all previous content


Backrgound

I'm working on a R Mardkown document which I'm renderring using the knitr package. The document uses Swift as a custom enginge. I'm defining the Swift engine as follows:

```{r, setup,  eval=TRUE, include=FALSE}
knitr::knit_engines$set(swift = function(options) {
  code <- paste(options$code, collapse = '\n')
  out  <- system2(
      command = "swift",
      args = "repl",
      input = code, 
      stdout = TRUE,
      stderr = TRUE
  )
  knitr::engine_output(options, code, out)
})
```

Challenge

I would like for the chunks to evaluate and include the code from the past chunks when evaluating. For instance in a first chunk I would provide:

```{swift, hello, eval=TRUE, include=TRUE}
import Foundation
let helloText: String = "Hello from Swift REPL"
print(helloText)
```

This would evaluate correctly to:

## helloText: String = "Hello from Swift REPL"
## Hello from Swift REPL

In the second chunk I would provide:

```{swift, hello_two, eval=TRUE, include=TRUE, dependson=knitr::all_labels()}
let punctuationMark: String = "!"
let helloTwo:String = helloText + punctuationMark
print(helloTwo)
```

This would return error

## punctuationMark: String = "!"
## error: repl.swift:2:23: cannot find 'helloText' in scope
## let helloTwo:String = helloText + punctuationMark
##                       ^~~~~~~~~
## 
## 
## error: repl.swift:2:7: cannot find 'helloTwo' in scope
## print(helloTwo)
##       ^~~~~~~~

Understandably the object helloText defined in the first chunk is not visible when Swift REPL is called again via previously defined swift function.

Question

How can I redefined the knit_engines$set(swift = ...) call so it would automatically include code from the previous chunks, or better, maintain the object space ?

Example (ideal)

In my second chunk where the code is defined as:

```{swift, hello_two, eval=TRUE, include=TRUE, dependson=knitr::all_labels()}
let punctuationMark: String = "!"
let helloTwo:String = helloText + punctuationMark
print(helloTwo)
```

The code that should be evaluated would be:

```{swift, hello_two, eval=TRUE, include=TRUE, dependson=knitr::all_labels()}
let punctuationMark: String = "!"
let helloTwo:String = helloText + punctuationMark
let punctuationMark: String = "!"
let helloTwo:String = helloText + punctuationMark
print(helloTwo)
```

Note: This is including only one print statement. I reckon this may difficult so I would also accept scenario where evaluated code in second chunk is:

```{swift, hello_two, eval=TRUE, include=TRUE, dependson=knitr::all_labels()}
let punctuationMark: String = "!"
let helloTwo:String = helloText + punctuationMark
print(helloText)
let punctuationMark: String = "!"
let helloTwo:String = helloText + punctuationMark
print(helloTwo)
```

This contains two print statements (code chunks are merged together). I want for this process to happen automatically, in terms of the textual content, the second chunk should only contain:

```{swift, hello_two, eval=TRUE, include=TRUE, dependson=knitr::all_labels()}
let punctuationMark: String = "!"
let helloTwo:String = helloText + punctuationMark
print(helloTwo)
```

Solution

  • Here is one approach near your ideal.

    The central idea is that for each Swift chunk the engine will collate all previous Swift chunk code in addition to the current chunk code and then run the collated code through the Swift REPL.

    This allows previously defined objects to be available in each new Swift REPL. The accumulated code can be modified such as stripping previous print statements, and options and current chunk code can pass through unmodified.

    ---
    title: "Untitled"
    output: html_document
    date: "2025-03-04"
    ---
    
    ```{r setup, include=FALSE}
    knitr::opts_chunk$set(echo = TRUE)
    
    knitr::knit_engines$set(swift = function(options) {
      # collapsed code from the chunk currently being processed
      current_code <- paste(options$code, collapse = '\n')
      
      # all chunk names with engine set to swift
      swift_chunk_names <- knitr::all_labels(engine == "swift")
      
      # code accumulation location
      cummulative_code <- NULL
      
      # for each swift chunk in order of appearance
      for(swift_chunk_name in swift_chunk_names) {
        # get a collapsed copy of the code
        prior_code <- paste(knitr::knit_code$get(swift_chunk_name), collapse = '\n')
        
        # if the prior code is identical to the current chunk's code
        if(current_code == prior_code) {
          # just append the code to our accumulator
          cummulative_code <- c(cummulative_code, prior_code)
          # and break out of the for loop
          break
        }
        else {
          # else strip print statements from the code before appending
          cummulative_code <- c(cummulative_code, gsub("print\\(.+?\\)", "", prior_code))
        }
      }
      
      # collapse the accumulated code
      code <- paste(cummulative_code, collapse = '\n')
      
      # run the code through swift
      out <- system2(
          command = "swift",
          args = "repl",
          input = code,
          stdout = TRUE,
          stderr = TRUE
      )
      
      # output, allowing options and original code to pass through
      knitr::engine_output(options, options$code, out)
    })
    ```
    
    ```{swift}
    import Foundation
    let helloText: String = "Hello from Swift REPL"
    print(helloText)
    ```
    
    ```{swift}
    let punctuationMark: String = "!"
    let helloTwo:String = helloText + punctuationMark
    print(helloTwo)
    ```
    

    enter image description here

    Reprex files hosted with on GitHub