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

feat(gnovm): Executable Markdown #2357

Open
wants to merge 47 commits into
base: master
Choose a base branch
from

Conversation

notJoon
Copy link
Member

@notJoon notJoon commented Jun 14, 2024

Description

The doctest (tentative name) is a tool that allows code blocks in Markdown, especially those marked as "go" and "gno", to be executed using the gno VM.

It works as follows: First, it parses the code blocks (fenced code blocks) from the Markdown file. After that, the extracted code is written to a temporary file, and the code is executed at the VM level.

Also supports static analysis to automatically include imports or packages without explicitly specifying them before execution. However, in this case, it assumes the code always executes the main function, fixes the package as 'main', and only searches for missing library paths in the standard library.
Additionally, to prevent unnecessary executions, a feature to cache execution results has been added.

I wrote the behavior and functionality in the file gnovm/cmd/gno/testdata/doctest/1.md as a tutorial.

Moving forward, there is potential to further enhance this tool to support code documentation by allowing the execution of code embedded within comments, similar to Rust.

related #2245

CLI

The CLI command is used as follows:

gno doctest -path <markdown_file_path> [-run ] [-timeout ]

path literally means the path of the markdown file to be executed. The run flag allows you to specify a pattern to match against code block names or tags, similar to go test -run <name>. This will execute all code blocks that partially or completely match the given pattern. If run is not specified, all code blocks in the file will be executed.

The optional timeout flag allows you to set a maximum duration for the execution of each code block. If not specified, a default timeout will be used.

For example:

gno doctest -path ./README.md -run "example" -timeout 30s

This command will execute all code blocks in README.md that have "example" in their name or tag, with a timeout of 30 seconds for each block.

@github-actions github-actions bot added the 📦 🤖 gnovm Issues or PRs gnovm related label Jun 14, 2024
Copy link

codecov bot commented Jun 14, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

📢 Thoughts on this report? Let us know!

@notJoon notJoon changed the title [WIP] Executable Markdown feat(gnovm): Executable Markdown Jun 18, 2024
@notJoon notJoon marked this pull request as ready for review June 18, 2024 11:23
@notJoon notJoon requested review from a team, jaekwon, moul, piux2, deelawn and thehowl as code owners June 18, 2024 11:23
Copy link
Contributor

@deelawn deelawn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work -- I found this very well written and and easy to follow, but there is a lot here, so I've left plenty of comments regarding style and questions about certain things. Thanks for this 😄

gnovm/cmd/gno/doctest.go Outdated Show resolved Hide resolved
gnovm/cmd/gno/doctest_test.go Outdated Show resolved Hide resolved
gnovm/pkg/doctest/parser.go Outdated Show resolved Hide resolved
gnovm/pkg/doctest/parser.go Show resolved Hide resolved
gnovm/pkg/doctest/parser.go Outdated Show resolved Hide resolved
gnovm/pkg/doctest/analyzer.go Outdated Show resolved Hide resolved
gnovm/pkg/doctest/analyzer.go Outdated Show resolved Hide resolved
gnovm/pkg/doctest/analyzer.go Outdated Show resolved Hide resolved
gnovm/pkg/doctest/analyzer.go Outdated Show resolved Hide resolved
gnovm/pkg/doctest/exec.go Show resolved Hide resolved
@notJoon
Copy link
Member Author

notJoon commented Aug 5, 2024

Thank you for the detailed review @deelawn. I think I have addressed all the points you mentioned in your review. Could you please check and confirm? Thank you once again 👍


Doctest also recognizes that a block of code is a gno. The code below outputs the same result as the example above.

```go
Copy link
Contributor

@piux2 piux2 Aug 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This this a great idea, I have a few questions:

Do we intent to run both go code and gno code in the doctest?
How does Doctest detect this is a gno code?
What is the reason behind using "```go" instead of "````gno" for gno code block?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a really good question. Currrently, when rendering markdown, gno doesn't have universal syntax highlighting applied yet (except the custom syntax rule). So, even in official documents like "Effective gno" are using go instead of gno to specify the language.

Considering the purpose of markdown is more for documentation rather than testing, this was an inevitable purpose. So I made it recognize both languages.

However, it doesn't clearly distinguish whether it's completely go or gno. For now, I assume that it's mostly compatible with go and execute it at gno VM level. In other words, even if the language is atually written in go, everything runs as gno. This part may require deeper context analysis in the future.

To summarize in the order of your questions:

  1. Even if the code block is specified as Go, it's actually recognized and executed as gno.
  2. There's no feature to clearly distinguish between the two languages, and it relies on the language specified in the fenced code block.
  3. Go is also set to be recognized due to syntax highlighting.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main difference between Gno and Go is that Gno has a few native methods unique to blockchain usage. For example:

std.AssertOriginCall
std.GetHeight

Basically, a program can either be Go code or Gno code. We cannot mix them in one source file.

There are a few options I can think of

  • Users need to explicitly mark it as either Go code or Gno code, similar to indicating the source code type with a suffix in the filename: xxx.go vs xxx.gno.
  • Only support Gno.
  • By default, the MD is treated as Gno code. Use gnostd.XXX to indicate that it is Go code. (Not sure if this has much practical purpose; it is more complicated to implement and is only for the sake of supporting Go code with chain functionality in MD.)

package main

func main() {
println("Hello, World!")
Copy link
Contributor

@piux2 piux2 Aug 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the println does not print correct results compared with file tests.


  package main

  type ints []int

  func main() {
        a := ints{1,2,3}
      println(a)
  }

doctest

// Output:
// (slice[(1 int),(2 int),(3 int)] gno.land/r/g14ch5q26mhx3jk5cxl88t278nper264ces4m8nt/run.ints)

file test result is correct. ints is a type defined in main package.
// Output:
// (slice[(1 int),(2 int),(3 int)] main.ints)

Copy link
Member Author

@notJoon notJoon Aug 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems this is happening because the code is executed through the VMKeeper.

func ExecuteCodeBlock(c codeBlock, stdlibDir string) (string, error) {
    // ...
    addr := crypto.AddressFromPreimage([]byte("addr1"))
    acc := acck.NewAccountWithAddress(ctx, addr)
    acck.SetAccount(ctx, acc)

    msg2 := vm.NewMsgRun(addr, std.Coins{}, files)

    res, err := vmk.Run(ctx, msg2)
    // ...
}

I used this method to handle stdlibs imports. But honestly, I don't know how to maintain this functionality while also making it output main.

Copy link
Contributor

@piux2 piux2 Oct 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about replacing it with the proper type when displaying the output in the Markdown?

gnovm/pkg/doctest/README.md Outdated Show resolved Hide resolved
Copy link
Contributor

@deelawn deelawn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the work to resolve my comments. Everything looks good to me.

@Kouteki Kouteki added the review/triage-pending PRs opened by external contributors that are waiting for the 1st review label Oct 3, 2024
@Kouteki Kouteki removed the review/triage-pending PRs opened by external contributors that are waiting for the 1st review label Oct 4, 2024
Copy link
Member

@zivkovicmilos zivkovicmilos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems good, at least from my domain knowledge of the VM 💯

Please wait on @thehowl to give you a ✅ before moving forward 🙏

cc @mvertes for a look also

@@ -1,10 +1,13 @@
package gnolang_test
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason we had a different test package? @thehowl

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah


func execDoctest(cfg *doctestCfg, _ []string, io commands.IO) error {
if cfg.markdownPath == "" {
return fmt.Errorf("markdown file path is required")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is nothing formatted here

resultChan := make(chan []string)
errChan := make(chan error)

go func() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use error groups, and easily exit when the first routine encounters a problem

case err := <-errChan:
return fmt.Errorf("failed to execute code block: %w", err)
case <-ctx.Done():
return fmt.Errorf("execution timed out after %v", cfg.timeout)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about formatting it to seconds?

cache = newCache(maxCacheSize)
regexCache = make(map[string]*regexp.Regexp)

addrRegex = regexp.MustCompile(`gno\.land/r/g[a-z0-9]+/[a-z.]+`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this defined somewhere as standard?


msg2 := vm.NewMsgRun(addr, std.Coins{}, files)

res, err := vmk.Run(stdlibCtx, msg2)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in the future we can support execution that doesn't require a local gno copy, but can interact with a remote chain. I can see this being useful when we have chain-version specific guides that require nuance in which VM version the source code is ran against

return "", err
}

cache.set(hashKey, res)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How much does having this cache make a difference?
Since we're saving hashes of the code blocks here, how often do they repeat?

}

// ref: gno.land/pkg/sdk/vm/common_test.go
func setupEnv() (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setupEnv shouldn't be in the package like this, but in a _test.go file


// hashCodeBlock generates a SHA256 hash for the given code block.
func hashCodeBlock(c codeBlock) string {
h := sha256.New()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this is overkill?

}

for _, tt := range tests {
tt := tt
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need this in go 1.22

@thehowl
Copy link
Member

thehowl commented Dec 12, 2024

@notJoon ping me once you've addressed the comments from milos, and i'll take a look :)

@Gno2D2
Copy link
Collaborator

Gno2D2 commented Dec 12, 2024

🛠 PR Checks Summary

🔴 The pull request head branch must be up-to-date with its base (more info)

Manual Checks (for Reviewers):
  • SKIP: Do not block the CI for this PR
Read More

🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers.

✅ Automated Checks (for Contributors):

🟢 Maintainers must be able to edit this pull request (more info)
🔴 The pull request head branch must be up-to-date with its base (more info)

☑️ Contributor Actions:
  1. Fix any issues flagged by automated checks.
  2. Follow the Contributor Checklist to ensure your PR is ready for review.
    • Add new tests, or document why they are unnecessary.
    • Provide clear examples/screenshots, if necessary.
    • Update documentation, if required.
    • Ensure no breaking changes, or include BREAKING CHANGE notes.
    • Link related issues/PRs, where applicable.
☑️ Reviewer Actions:
  1. Complete manual checks for the PR, including the guidelines and additional checks if applicable.
📚 Resources:
Debug
Automated Checks
Maintainers must be able to edit this pull request (more info)

If

🟢 Condition met
└── 🟢 The pull request was created from a fork (head branch repo: notJoon/gno-core)

Then

🟢 Requirement satisfied
└── 🟢 Maintainer can modify this pull request

The pull request head branch must be up-to-date with its base (more info)

If

🟢 Condition met
└── 🟢 On every pull request

Then

🔴 Requirement not satisfied
└── 🔴 Head branch (notJoon:testable-markdown) is up to date with base (master): behind by 184 / ahead by 47

Manual Checks
**SKIP**: Do not block the CI for this PR

If

🟢 Condition met
└── 🟢 On every pull request

Can be checked by

  • Any user with comment edit permission

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
📦 🌐 tendermint v2 Issues or PRs tm2 related 📦 🤖 gnovm Issues or PRs gnovm related
Projects
Status: In Progress
Status: In Progress
Status: In Review
Development

Successfully merging this pull request may close these issues.

8 participants