Replace roc-lang.org with WIP site

This commit is contained in:
Richard Feldman 2023-11-19 00:29:47 -05:00
parent dc1a7ae0ba
commit f4b8d1b4ff
No known key found for this signature in database
GPG Key ID: F1F21AA5B1D9E43B
34 changed files with 1340 additions and 5909 deletions

9
.gitignore vendored
View File

@ -61,9 +61,6 @@ roc_linux_x86_64.tar.gz
# nix
result
# tutorial
www/src/roc-tutorial
# Only keep Cargo.lock dependencies for the main compiler.
# Examples and test only crates should be fine to be unlocked.
# This remove unneccessary lock file versioning.
@ -77,7 +74,11 @@ www/src/roc-tutorial
# checkmate
checkmate_*.json
www/build/
www/main
www/dist
# ignore the examples folder in the WIP website, this is copied from roc-lang/examples in when building the site
www/wip_new_website/content/examples
www/content/examples
www/examples-main.zip
www/examples-main

3
www/.gitignore vendored
View File

@ -1,3 +0,0 @@
/build
roc_repl_wasm.js
roc_repl_wasm_bg.wasm

View File

@ -1,12 +1,37 @@
# www.roc-lang.org
## Prerequisites
- Linux or MacOS operating system, Windows users can use linux through WSL.
- Install [git](https://chat.openai.com/share/71fb3ae6-80d7-478c-8a27-a36aaa5ba921)
- Install [nix](https://nixos.org/download.html)
## Building the website from scratch
```bash
git clone https://github.com/roc-lang/roc.git
cd roc
nix develop
./www/build.sh
# make the roc command available
export PATH="$(pwd)/target/release/:$PATH"
bash build-dev-local.sh
```
Open http://0.0.0.0:8080 in your browser.
## After you've made a change locally
In the terminal where the web server is running:
1. kill the server with Ctrl+C
2. re-run the build script
3. refresh the page in your browser
To view the website after you've made a change, execute:
```bash
./www/build.sh
cd www/build
simple-http-server --nocache --index # already installed if you're using `nix develop`, otherwise use `cargo install simple-http-server`
bash build-dev-local.sh
```
Open http://0.0.0.0:8000 in your browser.
Open http://0.0.0.0:8080 in your browser.

View File

@ -11,9 +11,8 @@ DIR="$(dirname "$0")"
cd "$DIR" || exit
rm -rf dist/
cp -r ../build dist/
mkdir -p dist/wip
roc run main.roc -- content/ dist/wip/
cp -r static/* dist/wip/
cp -r build dist/
cp -r public/* dist/
roc run main.roc -- content/ dist/
npx http-server dist/ -p 8080 -c-1 --cors

View File

@ -17,16 +17,15 @@ cd $SCRIPT_RELATIVE_DIR
rm -rf build/
cp -r public/ build/
mkdir build/wip # for WIP site
# download the latest code for the examples
echo 'Downloading latest examples...'
curl -fLJO https://github.com/roc-lang/examples/archive/refs/heads/main.zip
unzip examples-main.zip
cp -R examples-main/examples/ wip_new_website/content/examples/
cp -R examples-main/examples/ content/examples/
# relace links in wip_new_website/content/examples/index.md to work on the WIP site
sed -i 's|](/|](/wip/examples/|g' wip_new_website/content/examples/index.md
# relace links in content/examples/index.md to work on the WIP site
sed -i '' 's|](/|](/examples/|' content/examples/index.md
# clean up examples artifacts
rm -rf examples-main examples-main.zip
@ -48,11 +47,10 @@ curl -fLJO https://github.com/roc-lang/roc/archive/www.tar.gz
# Download the latest pre-built Web REPL as a zip file. (Build takes longer than Netlify's timeout.)
REPL_TARFILE="roc_repl_wasm.tar.gz"
curl -fLJO https://github.com/roc-lang/roc/releases/download/nightly/$REPL_TARFILE
mkdir repl
tar -xzf $REPL_TARFILE -C repl
tar -xzf $REPL_TARFILE -C wip # note we also need this for WIP repl
rm $REPL_TARFILE
ls -lh repl
ls -lh wip
popd
@ -77,9 +75,6 @@ rm -rf roc_nightly roc_releases.json
# we use `! [ -v GITHUB_TOKEN_READ_ONLY ];` to check if we're on a netlify server
if ! [ -v GITHUB_TOKEN_READ_ONLY ]; then
echo 'Building tutorial.html from tutorial.md...'
mkdir www/build/tutorial
cargo build --release --bin roc
roc=target/release/roc
@ -96,19 +91,12 @@ else
mv roc_nightly* roc_nightly
roc='./roc_nightly/roc'
echo 'Building tutorial.html from tutorial.md...'
mkdir www/build/tutorial
fi
$roc version
$roc run www/generate_tutorial/src/tutorial.roc -- www/generate_tutorial/src/input/ www/build/tutorial/
mv www/build/tutorial/tutorial.html www/build/tutorial/index.html
# For WIP site
echo 'Building WIP site...'
$roc run www/wip_new_website/main.roc -- www/wip_new_website/content/ www/build/wip/
cp -r www/wip_new_website/static/* www/build/wip/
echo 'Building site markdown content'
$roc run www/main.roc -- www/content/ www/build/
# cleanup
rm -rf roc_nightly roc_releases.json
@ -153,5 +141,3 @@ if [ -v GITHUB_TOKEN_READ_ONLY ]; then
rm -rf $BASIC_CLI_DIR/generated-docs
done <<< "$VERSION_NUMBERS"
fi
popd

View File

@ -12,21 +12,21 @@
<section class="home-goals-container" aria-label="Roc's Design: Fast, Friendly, Functional">
<div role="presentation" class="home-goals-column">
<a href="/wip/fast" class="home-goals-content">
<a href="/fast" class="home-goals-content">
<h3 class="home-goals-title">Fast</h3>
<p class="home-goals-description">Roc code is designed to build fast and <span class="nowrap">run fast</span>. It compiles to machine code or WebAssembly.</p>
<p class="home-goals-learn-more">What does <i>fast</i> mean here?</p>
</a>
</div>
<div role="presentation" class="home-goals-column">
<a href="/wip/friendly" class="home-goals-content">
<a href="/friendly" class="home-goals-content">
<h3 class="home-goals-title">Friendly</h3>
<p class="home-goals-description">Roc's syntax, semantics, and included toolset all prioritize user-friendliness.</p>
<p class="home-goals-learn-more">What does <i>friendly</i> mean here?</p>
</a>
</div>
<div role="presentation" class="home-goals-column">
<a href="/wip/functional" class="home-goals-content">
<a href="/functional" class="home-goals-content">
<h3 class="home-goals-title">Functional</h3>
<p class="home-goals-description">
Roc has a small number of simple language primitives. It's a single-paradigm <span class="nowrap">functional language.</span></p>
@ -38,7 +38,7 @@
<section id="try-roc">
<h2><a href="#try-roc">Try Roc</a></h2>
<div id="homepage-repl-container" role="presentation">
<div id="repl-container" role="presentation">
<div id="repl-description" role="presentation">
<p>You can try Roc using this read-eval-print loop (<a href="https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop">REPL</a>), which is running in your browser in <a href="https://webassembly.org">WebAssembly</a>.</p>
<p><code>Shift-Enter</code> adds a newline.</p>
@ -57,7 +57,7 @@
<textarea aria-label="Input Roc code here, then press Enter to submit it to the REPL" rows="5" id="source-input" placeholder="Enter some Roc code here."></textarea>
</div>
</div>
<script type="module" src="/wip/site.js"></script>
<script type="module" src="/site.js"></script>
</section>
## [Examples](#examples) {#examples}
@ -75,8 +75,8 @@ Here are some examples of how it can be used today.
<pre><samp class="code-snippet">main <span class="kw">=</span>
Stdout<span class="punctuation section">.</span>line <span class="literal">"Hello!"</span></samp></pre>
<p>You can use Roc to create scripts and command-line interfaces (CLIs). The compiler produces binary executables, so Roc programs can run on devices that don't have Roc itself installed.</p>
<p>As an example, the HTML for this website is generated using a simple Roc script. You can see <a href="https://github.com/roc-lang/roc/blob/main/www/wip_new_website/main.roc">the code for it</a> in the main Roc code repository.</p>
<p>If you're looking for a starting point for building a command-line program in Roc, <a href="https://github.com/roc-lang/basic-cli">basic-cli</a> is a popular platform to check out.</p>
<p>As an example, the HTML for this website is generated using a simple Roc script. You can see <a href="https://github.com/roc-lang/roc/blob/main/www/generate_website/main.roc">the code for it</a> in the main Roc code repository.</p>
<p>If youre looking for a starting point for building a command-line program in Roc, <a href="https://github.com/roc-lang/basic-cli">basic-cli</a> is a popular platform to check out.</p>
</div>
<div role="presentation" class="home-examples-column">
<h3 class="home-examples-title">Web Servers</h3>
@ -98,7 +98,7 @@ log(<span class="string">`Roc says </span><span class="kw">${</span>fn()<span cl
</div>
### [Other Examples](#other-examples) {#other-examples}
You can find more use cases and examples on the [examples page](/wip/examples)!
You can find more use cases and examples on the [examples page](/examples)!
</section>

15
www/content/repl/index.md Normal file
View File

@ -0,0 +1,15 @@
# The rockin Roc REPL
<div id="repl-container" role="presentation">
<div id="repl" role="presentation">
<code class="history">
<div id="repl-intro-text">Enter an expression to evaluate, or a definition (like <span class="color-blue">x = 1</span>) to use later.</div>
<div id="history-text" aria-live="polite"></div>
</code>
<div id="repl-prompt" role="presentation">»</div>
<textarea aria-label="Input Roc code here, then press Enter to submit it to the REPL" rows="5" id="source-input" placeholder="Enter some Roc code here."></textarea>
</div>
</div>
<script type="module" src="/site.js"></script>
</section>

View File

@ -1,2 +0,0 @@
tutorial
src/output/tutorial.html

File diff suppressed because it is too large Load Diff

View File

@ -1,116 +0,0 @@
app "roc-tutorial"
packages { pf: "../../../examples/static-site-gen/platform/main.roc" }
imports [
pf.Html.{ html, head, body, header, footer, script, div, main, p, section, h1, h2, label, ol, input, text, nav, a, li, link, meta },
pf.Html.Attributes.{ content, name, for, id, type, href, rel, lang, title, charset, src },
]
provides [transformFileContent] to pf
transformFileContent : Str, Str -> Str
transformFileContent = \_, htmlContent ->
Html.render (view htmlContent)
view : Str -> Html.Node
view = \htmlContent ->
html [lang "en"] [
head [] [
meta [charset "utf-8"],
Html.title [] [text "Roc Tutorial"],
meta [name "description", content "Learn how to use the Roc programming language."],
meta [name "viewport", content "width=device-width"],
link [rel "stylesheet", href "/site.css"],
link [rel "icon", href "/favicon.svg"],
],
body [] [
viewNavbar,
main [] [
viewTutorialStart,
text htmlContent,
],
footer [] [
text "Made by people who like to make nice things.",
],
script [src "/site.js"] [],
],
]
viewNavbar : Html.Node
viewNavbar =
header [id "top-bar"] [
nav [] [
a [id "nav-home-link", href "/", title "The Roc Programming Language"] [text "roc"],
div [id "header-links"] [
a [href "/tutorial"] [text "tutorial"],
a [href "https://github.com/roc-lang/roc/tree/main/getting_started"] [text "install"],
a [href "/repl"] [text "repl"],
a [href "/builtins"] [text "docs"],
],
],
]
viewTutorialStart : Html.Node
viewTutorialStart =
div [id "tutorial-start"] [
input [id "tutorial-toc-toggle", name "tutorial-toc-toggle", type "checkbox"] [],
nav [id "tutorial-toc", ariaLabel "Table of Contents"] [
label [id "close-tutorial-toc", for "tutorial-toc-toggle"] [text "close"],
# TODO fix search: input [id "toc-search", type "text", placeholder "Search"] [],
ol [] tocLinks,
],
tutorialIntro,
]
tocLinks =
{ tag, value } <- List.map [
{ tag: "#installation", value: "Installation" },
{ tag: "#strings-and-numbers", value: "Strings and Numbers" },
{ tag: "#building-an-application", value: "Building an Application" },
{ tag: "#defining-functions", value: "Defining Functions" },
{ tag: "#if-then-else", value: "if-then-else" },
{ tag: "#debugging", value: "Debugging" },
{ tag: "#records", value: "Records" },
{ tag: "#tags", value: "Tags &amp; Pattern Matching" },
{ tag: "#booleans", value: "Booleans" },
{ tag: "#lists", value: "Lists" },
{ tag: "#types", value: "Types" },
{ tag: "#numeric-types", value: "Numeric Types" },
{ tag: "#crashing", value: "Crashing" },
{ tag: "#tests-and-expectations", value: "Tests and Expectations" },
{ tag: "#modules", value: "Modules" },
{ tag: "#tasks", value: "Tasks" },
{ tag: "#abilities", value: "Abilities" },
{ tag: "#appendix-advanced-concepts", value: "Advanced Concepts" },
{ tag: "#operator-desugaring-table", value: "Operator Desugaring Table" },
]
li [] [
a [href tag] [text value],
]
tutorialIntro =
div [id "tutorial-intro"] [
section [] [
h1 [] [
text "Tutorial",
label [id "tutorial-toc-toggle-label", for "tutorial-toc-toggle"] [text "contents"],
],
p [] [text "Welcome to Roc!"],
p [] [text "This tutorial will teach you how to build Roc applications. Along the way, you'll learn how to write tests, use the REPL, and more!"],
],
section [] [
h2 [id "installation"] [
a [href "#installation"] [text "Installation"],
],
p [] [
text "Roc doesnt have a numbered release or an installer yet, but you can follow the install instructions for your OS",
a [href "https://github.com/roc-lang/roc/tree/main/getting_started#installation"] [text " here "],
text ". If you get stuck, friendly people will be happy to help if you open a topic in",
a [href "https://roc.zulipchat.com/#narrow/stream/231634-beginners"] [text " #beginners "],
text "on",
a [href "https://roc.zulipchat.com/"] [text " Roc Zulip Chat "],
text "and ask for assistance!",
],
],
]
ariaLabel = Html.attribute "aria-label"

View File

@ -1,5 +1,5 @@
app "roc-website"
packages { pf: "../../examples/static-site-gen/platform/main.roc" }
packages { pf: "../examples/static-site-gen/platform/main.roc" }
imports [
pf.Html.{ Node, html, head, body, header, footer, div, span, main, text, nav, a, link, meta, script },
pf.Html.Attributes.{ attribute, content, name, id, href, rel, lang, class, title, charset, color, ariaLabel, ariaHidden, type },
@ -74,7 +74,7 @@ view = \page, htmlContent ->
preloadWoff2 "/fonts/source-code-pro-v22-latin/source-code-pro-v22-latin-regular.woff2",
preloadWoff2 "/fonts/permanent-marker-v16-latin/permanent-marker-v16-latin-regular.woff2",
link [rel "prefetch", href "/repl/roc_repl_wasm.js"],
link [rel "stylesheet", href "/wip/site.css"],
link [rel "stylesheet", href "/site.css"],
# Safari ignores rel="icon" and only respects rel="mask-icon". It will render the SVG with
# fill="#000" unless this `color` attribute here is hardcoded (not a CSS `var()`) to override it.
link [rel "mask-icon", href "/favicon.svg", color "#7d59dd"],
@ -103,19 +103,19 @@ viewNavbar = \page ->
isHomepage = page == "index.html"
homeLinkAttrs =
[id "nav-home-link", href "/wip/", title "The Roc Programming Language Homepage"]
[id "nav-home-link", href "/", title "The Roc Programming Language Homepage"]
|> List.concat (if isHomepage then [ariaHidden "true"] else [])
header [id "top-bar"] [
nav [ariaLabel "primary"] [
a homeLinkAttrs [rocLogo, span [class "home-link-text"] [text "Roc"]],
div [id "top-bar-links"] [
a [href "/wip/tutorial"] [text "tutorial"],
a [href "/wip/install"] [text "install"],
a [href "/wip/examples"] [text "examples"],
a [href "/wip/community"] [text "community"],
a [href "/wip/docs"] [text "docs"],
a [href "/wip/donate"] [text "donate"],
a [href "/tutorial"] [text "tutorial"],
a [href "/install"] [text "install"],
a [href "/examples"] [text "examples"],
a [href "/community"] [text "community"],
a [href "/docs"] [text "docs"],
a [href "/donate"] [text "donate"],
],
],
]

View File

@ -43,11 +43,11 @@ done
popd
# Minify the CSS file, and let esbuild add a content hash to the file name
npm exec --yes esbuild -- --minify dist/wip/site.css --outdir=dist/wip/ --entry-names='[name]-[hash]'
npm exec --yes esbuild -- --minify dist/site.css --outdir=dist/ --entry-names='[name]-[hash]'
# Remove unused original file
rm dist/wip/site.css
rm dist/site.css
# Find the new filename
css_with_hash=$(basename dist/wip/site-*.css)
css_with_hash=$(basename dist/site-*.css)
# Replace all occurances in the html
sed -i "s:/wip/site.css:/wip/${css_with_hash}:g" dist/wip/*.html
sed -i "s:/wip/site.css:/wip/${css_with_hash}:g" dist/*.html

View File

@ -1,15 +0,0 @@
html {
font-family: sans-serif;
line-height: 145%;
}
li {
margin-bottom: 0.5rem;
}
@media only screen and (min-device-width: 900px) {
body {
width: 900px;
margin: 24px auto;
}
}

View File

@ -1,316 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>The Roc Programming Language</title>
<!-- <meta name="description" content="A language for making delightful software."> -->
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="/homepage.css">
<!-- <link rel="icon" href="/favicon.svg"> -->
</head>
<body>
<h1>The Roc Programming Language</h1>
<p>Roc's goal is to be a fast, friendly, functional language. It's very much a work in progress;
below, you can see the current progress towards this goal. This website is intentionally unstyled
as a way to emphasize the language's current level of incompleteness. The website will become
more polished after the language itself becomes more polished!</p>
<p>Roc compiles to machine code or to <a href="https://webassembly.org">WebAssembly</a>. Eventually
you'll be able to use Roc to build high-quality servers, command-line applications, graphical
native desktop user interfaces, among other classes of applications. Today, only command-line interfaces have
support beyond the
proof-of-concept stage; the other use cases will mature over time.</p>
<p>Like <a href="https://www.lua.org/">Lua</a>, Roc's automatic memory management doesn't require
a virtual machine, and it's possible to call Roc functions directly from any language that can
call <a href="https://en.wikipedia.org/wiki/C_(programming_language)">C</a> functions. This makes
Roc additionally useful as a language for implementing plugins, and gives you a way to
incrementally transition a legacy code base from another language to Roc.</p>
<p>So far, the Roc compiler has progressed past the "proof of concept" stage, but there are
currently lots of known bugs and unimplemented features, and the documentation for both the
language and the standard library is incomplete. The overall ecosystem is in its infancy, and
the compiler is neither battle-tested nor fuzz-tested yet, so we don't recommend relying on Roc
for critical projects until its development is further along.</p>
<p>With all that context in mind, if you'd like to try it out or to get involved with contributing,
the <a href="https://github.com/roc-lang/roc">source code repository</a> has
<a href="https://github.com/roc-lang/roc/releases">nightly builds</a> you can download,
and a <a href="https://roc-lang.org/tutorial">tutorial</a>.
</p>
<p>If you'd like to learn more about Roc, you can continue reading here, or check out one of these videos:</p>
<ul>
<li><a href="https://media.handmade-seattle.com/roc-lang">Roc at Handmade Seattle</a> - November 12,
2021 (very low-level explanation of how Roc's compiler makes programs run fast)</li>
<li><a href="https://youtu.be/vzfy4EKwG_Y">Outperforming Imperative with Pure Functional Languages</a> - October 1,
2021 (about Roc's runtime performance and optimizer)</li>
<li><a href="https://youtu.be/6qzWm_eoUXM">A taste of Roc</a> - September 23, 2021 (syntax, application examples)
</li>
<li><a href="https://youtu.be/cpQwtwVKAfU?t=75">Roc at the Philly ETE conference</a> - May 6, 2021 (platforms and
applications)</li>
<li><a href="https://youtu.be/FMyyYdFSOHA">Roc on Zig Showtime</a> - April 24, 2021 (making a platform)</li>
<li><a href="https://youtu.be/ZnYa99QoznE?t=4790">Roc at the Berlin FP Meetup</a> - September 1, 2020 (overall
vision for the language)</li>
</ul>
<h2>A <em>Fast</em> Language</h2>
<h3>Goal</h3>
<p>We want Roc to run faster than any non-systems language (like C, C++, Rust, or Zig)
that sees mainstream use in industry. The goal is that nobody should find themselves
thinking "I should rewrite my Roc program in [some mainstream garbage-collected language]
because that will make it run significantly faster."
</p>
<p>When benchmarking Roc code against similarly-optimized programs written in
<a href="https://go.dev">Go</a>,
<a href="https://www.swift.org/">Swift</a>, <a href="https://www.java.com">Java</a>,
<a href="https://learn.microsoft.com/en-us/dotnet/csharp">C#</a>, or
<a href="https://www.ecma-international.org/publications-and-standards/standards/ecma-262">JavaScript</a>,
we generally aim for Roc to outperform all of those languages. Outperforming systems
languages like Rust, Zig, C, D, and C++ is a non-goal, as is outperforming research languages
that see little or no use in industry. (Realistically, there will always be certain specific
benchmarks where some popular non-systems-level languages outperform Roc, but the goal is to
usually be at the front of that pack.)
</p>
<h4>Current progress</h4>
<p>Progress towards this performance goal is already quite far along.</p>
<p>Roc already uses unboxed data structures and unboxed closures, monomorphizes polymorphic code,
and uses LLVM as a compiler backend. These optimizations, especially unboxed closures and
monomorphization, can be found in several systems-level languages (like C++ and Rust), but not
in any mainstream garbage-collected languages. Roc closures in particular have the distinction
of being as ergonomic as the closures found in garbage-collected languages
(where they are typically boxed), but have the performance of systems language closures
(which are typically unboxed, but have more complicated types).
<p>Because of these optimizations, in many cases Roc code already
compiles to the same machine instructions that the equivalent code written in one of these
systems languages would. Something we do regularly is to compare the LLVM instructions generated
by Roc's compiler and by these systems languages' compilers, to check whether we're generating
equivalent instructions.</p>
<p>That said, there are also cases where Roc has strictly more runtime overhead than languages
like C, C++, Zig, and Rust do. The most costly is automatic memory management, which Roc
implements using automatic reference counting. Static reference count optimizations like
elision and reuse (thanks to Morphic and
<a
href="https://www.microsoft.com/en-us/research/publication/perceus-garbage-free-reference-counting-with-reuse/">Perceus</a>)
improve things, but significant runtime overhead remains.
</p>
<p>Eliminating this overhead altogether would require sacrificing other design goals
(e.g. it would require introducing memory-unsafe operations, or compile-time lifetime errors),
and there isn't much overhead left to remove outside of automatic memory management. For example,
smaller sources of overhead include mandatory array bounds checks, disallowing cyclic references
(which rules out a certain niche of efficient graph data structures), and automatic opportunistic
in-place mutation instead of direct mutation. Even if all of these sources of overhead were
completely eliminated, it seems unlikely that typical Roc programs would see a particularly big
performance boost.</p>
<p>Overall, we expect Roc's performance in the use cases mentioned above (servers, CLIs, GUIs, etc.)
to be about the same as the equivalent C++ code would be, if all that C++ code
(including its dependencies) were written in a restricted subset of C++ which always did array
bounds checks and used shared pointers for all heap allocations.
The Roc code might even run somewhat faster, because its reference counts are non-atomic by default,
and can be statically optimized away in some cases—but then again, Roc also has a bit of overhead
to perform opportunistic in-place mutation instead of direct mutation.</p>
<p>To be clear, we don't expect this because we've benchmarked a bunch of programs written in Roc
and in this restricted C++ subset, and found that the numbers were about the same (although if
you know C++ well enough and want to do such experiments, we'd happy to help and would be
interested to see the results!) but rather because Roc's compiler and
<a href="https://clang.llvm.org/">clang</a> should both be generating essentially the same
LLVM instructions when the C++ is restricted to that subset.
</p>
<p>Of course, <em>unrestricted</em> C++ code can certainly run faster than unrestricted Roc code.
The same is true when comparing other such minimal-overhead systems languages to Roc, including
Rust, Zig, C, and D. The point of the comparison is to give you a general idea of what Roc
compiles to, since it is quite different from the VMs and JITted bytecode interpreters found in
today's most popular garbage-collected languages!</p>
<p>The talk <a href="https://youtu.be/vzfy4EKwG_Y">Outperforming Imperative with Pure Functional Languages</a>
discusses some early results from Roc's optimizations, and
<a href="https://media.handmade-seattle.com/roc-lang">Roc at Handmade Seattle</a> gets into
low-level details of how Roc's compiler generates programs similarly to how clang does.
</p>
<h2>A <em>Friendly</em> Language</h2>
<h3>Goals</h3>
<p>Roc aims to be a user-friendly language with a friendly community of users.</p>
<p>A programming language can be much more than a tool for writing software, it can also be a way
for people to come together through shared experiences, to teach and to learn from one another,
and to make new friends.</p>
<p>No community is perfect, but a community where people show kindness to each another by default
can be a true joy to participate in. That all starts with friendliness, especially towards
beginners, and including towards people who prefer other programming languages.
After all, languages are tools people use to create software, and there's no need for us
to create artificial divisions between ourselves based on the tools we use!</p>
<p>On a technical level, Roc aims to ship a toolset where user-friendliness is a major priority.
This includes everything from helpful error messages (aiming to meet the bar set by
<a href="https://elm-lang.org">Elm</a>) to quality-of-life improvements inspired by dynamic
languages (always being able to run your program even if there are compile errors, automatic
serialization and deserialization using schemas determined by type inference, reliable hot
code loading that's always enabled and requires no configuration to set up, etc.) to accessibility
features in the included editor.
</p>
<p>Roc also aims to ship a single binary that includes not only a compiler, but also a
<a href="https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop">REPL</a>,
package manager, test runner, debugger, static analyzer, code formatter, and a full-featured
editor, all of which are designed to work seamlessly together.
</p>
<h3>Current Progress</h3>
<p>Work has not yet started on the package manager, static analyzer, debugger, or hot code loading
system, and although work has started on the editor, it's not yet far enough along to be usable
for practical purposes. The standard library is perhaps 80 percent complete in terms of
functionality, but a lot of operations do not yet have documentation.</p>
<p>The REPL fully supports entering arbitrary expressions, and will evaluate them and print the
results. It remembers recent expressions entered in the current session (if you press the up arrow),
but it can't yet execute effects. You can try out the REPL in a browser at
<a href="https://roc-lang.org/repl">roc-lang.org/repl</a> - it uses a WebAssembly build of Roc's
compiler, and compiles the code you write to WebAssembly on the fly, which it then executes in
the browser to display the answer.
</p>
<p>The compiler works well enough on a basic level to build things with it, but some error messages
could use significant improvement, and it has a lot of known bugs and missing features. You can
currently use it on macOS (either Intel or Apple Silicon), Linux (only x86-64 machines at the moment),
and Windows (only recently supported; debugging and testing features don't work on it yet, and
there are likely bugs we haven't encountered yet due to lack of battle testing). Support for other
operating systems has not yet been discussed.</p>
<p>The compiler doesn't yet support incremental compilation or hot code loading, and build times vary
based on what machine you're building for.</p>
<p>For example, suppose you run `roc check`, which reports errors it finds (type mismatches, naming
errors, and so on) but doesn't actually build an executable, on a code base that's under a thousand
lines of code. On an M1 MacBook Pro, this typically takes about 10 milliseconds.</p>
<p>In contrast, if you do `roc build` (or `roc run`) on that same machine, it will take closer to 500
milliseconds instead. Almost all that extra time is spent waiting for LLVM to generate (unoptimized)
machine code, and then for the system linker to assemble an executable from it.</p>
<p> Fortunately, we can eliminate almost all of those extra 490 millisconds of build time by using
Roc's (work in progress) development backend instead of LLVM. This compiles directly from Roc's
internal representation to machine code, like most compilers did before LLVM. (LLVM can optimize
code into running very fast, but even when it performs no optimization at all, LLVM itself takes a lot
longer to run than generating unoptimized machine code directly.)</p>
<p>The LLVM backend is currently the most feature-complete, followed closely by the WebAssembly backend
(which the online REPL uses exclusively, instead of LLVM). The x86 and ARM backends still have a
ways to go, but improving them can be done by anyone with the patience to read some documentation;
we have issues split up for them, and are happy to help new contributors get up and running!</p>
<p>Builds on Linux and Windows also use Roc's surgical linker instead of the system linker, which
runs so fast that linking essentially disappears from the performance profile altogether. The
surgical linker currently only works on Linux and Windows, and it currently supports building
executables but not (yet) dynamic libraries, which is relevant if you're using Roc to create
plugins or want to call Roc functions from existing code bases in other languages. Work has started
on macOS surgical linking, but it isn't usable yet. If you're interested in working on that,
please get in touch on <a href="https://roc.zulipchat.com/">Roc Zulip</a>!</p>
<p>The test runner currently has first-class support for running standard non-effectful tests.
It does not yet have first-class support for effectful tests, property-based tests, snapshot tests,
or "simulation tests" (where effects are replaced by hardcoded values during the test - similar to
"mocking" in other languages), although these are all planned for the future.</p>
<p>The code formatter is nearly feature-complete, although occasionally it will report an error -
usually due to a comment being placed somewhere it doesn't yet know how to handle. Unlike most of
the rest of the compiler, the formatter is one place where the number of known bugs is so small
that fuzzing would be very helpful as a way to surface bugs we don't yet know about. (If you're
interested in working on setting up fuzzing for the formatter, please let us know in
the <a href="https://roc.zulipchat.com/#narrow/stream/316715-contributing"><code>#contributing</code> channel</a>
on Zulip! Separately, we're also very interested in fuzzing the compiler, even though we already
have a sizable list of known bugs there.)</p>
<p>On the community side, so far the community is a friendly bunch, and we want to keep it that way
as it grows! We hope to do that by encouraging a culture of kindness and helping one another out,
especially by being welcoming towards beginners.</p>
<p>If you'd like to join in, the best place to do that is in our Zulip chat. Feel free to drop by the
<a href="https://roc.zulipchat.com/#narrow/stream/231634-beginners/topic/introductions"><code>introductions</code>
topic</a>
and introduce yourself!
</p>
<h2>A <em>Functional</em> Language</h2>
<h3>Goals</h3>
<p>Roc aims to be a purely functional programming language. This means all Roc functions are
<a href="https://en.wikipedia.org/wiki/Pure_function">pure functions</a>, and all effects are
<a href="https://medium.com/@kaw2k/managed-effects-and-elm-36b7fcd246a9">managed effects</a>
instead of side effects.
</p>
<p>A major motivating reason for this is to facilitate tooling. For example, in the future the goal
is that Roc's test runner won't bother re-running tests whose outcomes could not possibly have
changed (because they were pure functions whose inputs did not change). Tests that contain only
pure functions can be trivially run in parallel, and they will never <a
href="https://www.smashingmagazine.com/2021/04/flaky-tests-living-nightmare/">flake</a>.
Additionally, having the guarantee that the application contains only pure functions can also make
certain debugging tools more reliable, such as time travel and retroactive tracing.
</p>
<p>Roc also takes a novel approach to managed effects. In most programming languages, the standard
library contains both data structures and I/O primitives (e.g. for using the file system or the
network), and then you might decide to use a <a
href="https://en.wikipedia.org/wiki/Application_framework">framework</a>
on top of that standard library.</p>
<p>In Roc, every application is built on a <em>platform</em>. A platform is like a framework except
that it also provides I/O primitives and behind-the-scenes memory management. (Roc's standard
library only contains data structures.) In practice, this means that using Roc feels similar to
using any other programming language where you've chosen to use a framework, except that the
documentation for your I/O primitives comes from the framework instead of the standard library.</p>
<p>This might sound like a minor distinction, but it turns out there are a lot of surprising benefits
to organizing things this way, which would be impossible to achieve without having platforms as a
first-class language concept. <a href="https://youtu.be/cpQwtwVKAfU">The Edges of Cutting-Edge Languages</a>
goes into more detail about some of these benefits.
</p>
<h3>Current Progress</h3>
<p>Today, platforms as a concept already exist, and there are a few different ones implemented.
You can find them in the <a href="https://github.com/roc-lang/roc/tree/main/examples"><code>examples/</code></a>
directory in the source code repository. The platform for building command-line interfaces is the
most fully featured; the others are mostly in the proof-of-concept stage.
</p>
<p>Roc's built-in tooling is not yet far enough along to take advantage of pure functions. For
example, there is a built-in test runner, but it does not yet run tests in parallel or skip
running tests whose outcomes could not possibly have changed.
</p>
<p>Roc is already a purely functional programming language, though, so all of these benefits
are ready to be unlocked as the tooling implementations progress!
</p>
<h2>The Roc Programming Language Foundation</h2>
<p>We've created a nonprofit to support Roc, you can learn more about it <a
href="https://foundation.roc-lang.org/">here</a>.</p>
<footer>This site is powered by <a href="https://www.netlify.com">Netlify</a>.</footer>
</body>
</html>

View File

@ -1,39 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>REPL</title>
<link rel="stylesheet" href="/site.css" />
<link rel="stylesheet" href="/repl/repl.css" />
<link rel="icon" href="/favicon.svg">
<!-- Safari ignores rel="icon" and only respects rel="mask-icon". It will render the SVG with
fill="#000" unless this `color` attribute here is hardcoded (not a CSS `var()`) to override it.
-->
<link rel="mask-icon" href="/favicon.svg" color="#7d59dd">
</head>
<body>
<div id="top-bar">
<nav><a id="nav-home-link" href="/" title="The Roc Programming Language">roc</a>
<div id="top-bar-links"><a href="/tutorial">tutorial</a><a
href="https://github.com/roc-lang/roc/tree/main/getting_started">install</a><a href="/repl">repl</a><a
href="/builtins">docs</a></div>
</nav>
</div>
<main id="repl">
<h1>The rockin' Roc REPL</h1>
<code class="history">
<div id="help-text"></div>
<div id="history-text"><div id="loading-message">Loading REPL WebAssembly module…please wait!</div></div>
</code>
<div id="repl-prompt" role="presentation">»</div>
<textarea rows="5" id="source-input" placeholder="You can enter Roc code here once the REPL loads!"
disabled></textarea>
</main>
<script type="module" src="/repl/repl.js"></script>
</body>
</html>

View File

@ -1,173 +0,0 @@
main {
display: flex;
flex-direction: column;
}
h1 {
font-family: "Lato";
font-size: 48px;
line-height: 48px;
padding: 48px 0;
margin: 0;
color: var(--header-link-color);
}
#source-input-wrapper {
position: relative;
width: 100%;
box-sizing: border-box;
}
#source-input-wrapper::before {
content: "» ";
position: absolute;
left: 15px;
top: 18px;
line-height: 16px;
height: 16px;
z-index: 2;
font-family: var(--font-mono);
color: var(--cyan);
/* Let clicks pass through to the textarea */
pointer-events: none;
}
#source-input {
width: 100%;
font-family: var(--font-mono);
color: var(--code-color);
background-color: var(--code-bg);
display: inline-block;
height: 76px;
padding: 16px;
padding-left: 36px;
border: 1px solid transparent;
margin: 0;
margin-bottom: 2em;
box-sizing: border-box;
}
#source-input:focus {
border: 1px solid var(--cyan);
box-sizing: border-box;
outline: none;
}
li {
margin: 8px;
}
.history {
padding: 1em;
padding-bottom: 0;
}
#help-text,
#history-text {
white-space: pre-wrap;
}
#history-text {
margin-top: 16px;
}
#loading-message {
text-align: center;
/* approximately match height after loading and printing help message */
height: 96px;
}
.history-item {
margin-bottom: 24px;
}
.history-item .input {
margin: 0;
margin-bottom: 8px;
}
.history-item .output {
margin: 0;
}
.panic {
color: red;
}
.input-line-prefix {
color: var(--cyan);
}
.color-red {
color: red;
}
.color-green {
color: var(--green);
}
.color-yellow {
color: var(--orange);
}
.color-blue {
color: var(--cyan);
}
.color-magenta {
color: var(--magenta);
}
.color-cyan {
color: var(--cyan);
}
.color-white {
/* Really this isn't white so much as "default text color." For the repl, this should be black
in a light color scheme, and only white in dark mode. The name could be better! */
color: black;
}
@media (prefers-color-scheme: dark) {
.color-white {
color: white;
}
}
.bold {
font-weight: bold;
}
.underline {
text-decoration: underline;
}
/* Mobile-friendly screen width */
@media only screen and (max-width: 767px) {
h1 {
font-size: 24px !important;
margin: 0;
padding: 16px 0;
text-align: center;
}
#repl {
margin: 0;
padding: 0;
min-height: calc(100vh - var(--top-bar-height));
}
code.history {
flex-grow: 1;
}
#source-input {
margin: 0
}
#loading-message {
margin: 0;
}
}

View File

@ -1,307 +0,0 @@
// The only way we can provide values to wasm_bindgen's generated code is to set globals
window.js_create_app = js_create_app;
window.js_run_app = js_run_app;
window.js_get_result_and_memory = js_get_result_and_memory;
// The only place we use console.error is in wasm_bindgen, where it gets a single string argument.
console.error = function displayErrorInHistoryPanel(string) {
const html = `<div class="panic">${string}</div>`;
updateHistoryEntry(repl.inputHistoryIndex, false, html);
};
import * as roc_repl_wasm from "/repl/roc_repl_wasm.js";
// ----------------------------------------------------------------------------
// REPL state
// ----------------------------------------------------------------------------
const repl = {
elemHistory: document.getElementById("history-text"),
elemSourceInput: document.getElementById("source-input"),
inputQueue: [],
inputHistory: [],
inputHistoryIndex: 0,
inputStash: "", // stash the user input while we're toggling through history with up/down arrows
textDecoder: new TextDecoder(),
textEncoder: new TextEncoder(),
compiler: null,
app: null,
// Temporary storage for the address of the result of running the user's code.
// Used while control flow returns to Rust to allocate space to copy the app's memory buffer.
result_addr: 0,
};
// Initialise
repl.elemSourceInput.value = ""; // Some browsers remember the input across refreshes
resetSourceInputHeight();
repl.elemSourceInput.addEventListener("input", resetSourceInputHeight);
repl.elemSourceInput.addEventListener("keydown", onInputKeydown);
repl.elemSourceInput.addEventListener("keyup", onInputKeyup);
roc_repl_wasm.default("/repl/roc_repl_wasm_bg.wasm").then(async (instance) => {
repl.elemHistory.querySelector("#loading-message").remove();
repl.elemSourceInput.disabled = false;
repl.elemSourceInput.placeholder = "Enter some Roc code here.";
repl.elemSourceInput.focus();
repl.compiler = instance;
// Get help text from the compiler, and display it at top of the history panel
try {
const helpText = await roc_repl_wasm.entrypoint_from_js(":help");
const helpElem = document.getElementById("help-text");
helpElem.innerHTML = helpText.trim();
} catch (e) {
// Print error for Roc devs. Don't use console.error, we overrode that above to display on the page!
console.warn(e);
}
});
// ----------------------------------------------------------------------------
// Handle inputs
// ----------------------------------------------------------------------------
function resetSourceInputHeight() {
repl.elemSourceInput.style.height = repl.elemSourceInput.scrollHeight + 2 + "px"; // +2 for the border
}
function onInputKeydown(event) {
const ENTER = 13;
const { keyCode } = event;
if (keyCode === ENTER) {
if (!event.shiftKey && !event.ctrlKey && !event.altKey) {
// Don't advance the caret to the next line
event.preventDefault();
const inputText = repl.elemSourceInput.value.trim();
repl.elemSourceInput.value = "";
repl.elemSourceInput.style.height = "";
repl.inputQueue.push(inputText);
if (repl.inputQueue.length === 1) {
processInputQueue();
}
}
}
}
function onInputKeyup(event) {
const UP = 38;
const DOWN = 40;
const { keyCode } = event;
const el = repl.elemSourceInput;
switch (keyCode) {
case UP:
if (repl.inputHistory.length === 0) {
return;
}
if (repl.inputHistoryIndex == repl.inputHistory.length - 1) {
repl.inputStash = el.value;
}
setInput(repl.inputHistory[repl.inputHistoryIndex]);
if (repl.inputHistoryIndex > 0) {
repl.inputHistoryIndex--;
}
break;
case DOWN:
if (repl.inputHistory.length === 0) {
return;
}
if (repl.inputHistoryIndex === repl.inputHistory.length - 1) {
setInput(repl.inputStash);
} else {
repl.inputHistoryIndex++;
setInput(repl.inputHistory[repl.inputHistoryIndex]);
}
break;
default:
break;
}
}
function setInput(value) {
const el = repl.elemSourceInput;
el.value = value;
el.selectionStart = value.length;
el.selectionEnd = value.length;
}
// Use a queue just in case we somehow get inputs very fast
// We want the REPL to only process one at a time, since we're using some global state.
// In normal usage we shouldn't see this edge case anyway. Maybe with copy/paste?
async function processInputQueue() {
while (repl.inputQueue.length) {
const inputText = repl.inputQueue[0];
repl.inputHistoryIndex = createHistoryEntry(inputText);
repl.inputStash = "";
let outputText = "";
let ok = true;
if (inputText) {
try {
outputText = await roc_repl_wasm.entrypoint_from_js(inputText);
} catch (e) {
outputText = `${e}`;
ok = false;
}
}
updateHistoryEntry(repl.inputHistoryIndex, ok, outputText);
repl.inputQueue.shift();
}
}
// ----------------------------------------------------------------------------
// Callbacks to JS from Rust
// ----------------------------------------------------------------------------
var ROC_PANIC_INFO = null;
function send_panic_msg_to_js(rocstr_ptr, panic_tag) {
const { memory } = repl.app.exports;
const rocStrBytes = new Int8Array(memory.buffer, rocstr_ptr, 12);
const finalByte = rocStrBytes[11]
let stringBytes = "";
if (finalByte < 0) {
// small string
// bitwise ops on negative JS numbers are weird. This clears the bit that we
// use to indicate a small string. In rust it's `finalByte as u8 ^ 0b1000_0000`
const length = finalByte + 128;
stringBytes = new Uint8Array(memory.buffer, rocstr_ptr, length);
} else {
// big string
const rocStrWords = new Uint32Array(memory.buffer, rocstr_ptr, 3);
const [ptr, len, _cap] = rocStrWords;
const SEAMLESS_SLICE_BIT = 1 << 31;
const length = len & (~SEAMLESS_SLICE_BIT);
stringBytes = new Uint8Array(memory.buffer, ptr, length);
}
const decodedString = repl.textDecoder.decode(stringBytes);
ROC_PANIC_INFO = {
msg: decodedString,
panic_tag: panic_tag,
};
}
// Load Wasm code into the browser's virtual machine, so we can run it later.
// This operation is async, so we call it before entering any code shared
// with the command-line REPL, which is sync.
async function js_create_app(wasm_module_bytes) {
const { instance } = await WebAssembly.instantiate(wasm_module_bytes, {
env: {
send_panic_msg_to_js: send_panic_msg_to_js,
}
});
// Keep the instance alive so we can run it later from shared REPL code
repl.app = instance;
}
// Call the `main` function of the user app, via the `wrapper` function.
function js_run_app() {
const { wrapper, memory } = repl.app.exports;
// Run the user code, and remember the result address
// We'll pass it to Rust in the next callback
try {
repl.result_addr = wrapper();
} catch (e) {
// an exception could be that roc_panic was invoked,
// or some other crash (likely a compiler bug)
if (ROC_PANIC_INFO === null) {
throw e;
} else {
// when roc_panic set an error message, display it
const { msg, panic_tag } = ROC_PANIC_INFO;
ROC_PANIC_INFO = null;
console.error(format_roc_panic_message(msg, panic_tag));
}
}
// Tell Rust how much space to reserve for its copy of the app's memory buffer.
// We couldn't know that size until we actually ran the app.
return memory.buffer.byteLength;
}
function format_roc_panic_message(msg, panic_tag) {
switch (panic_tag) {
case 0: {
return `Roc failed with message: "${msg}"`;
}
case 1: {
return `User crash with message: "${msg}"`;
}
default: {
return `Got an invalid panic tag: "${panic_tag}"`;
}
}
}
// After Rust has allocated space for the app's memory buffer,
// we copy it, and return the result address too
function js_get_result_and_memory(buffer_alloc_addr) {
const appMemory = new Uint8Array(repl.app.exports.memory.buffer);
const compilerMemory = new Uint8Array(repl.compiler.memory.buffer);
compilerMemory.set(appMemory, buffer_alloc_addr);
return repl.result_addr;
}
// ----------------------------------------------------------------------------
// Rendering
// ----------------------------------------------------------------------------
function createHistoryEntry(inputText) {
const historyIndex = repl.inputHistory.length;
repl.inputHistory.push(inputText);
const firstLinePrefix = '<span class="input-line-prefix">» </span>';
const otherLinePrefix = '<br><span class="input-line-prefix">… </span>';
const inputLines = inputText.split("\n");
if (inputLines[inputLines.length - 1] === "") {
inputLines.pop();
}
const inputWithPrefixes = firstLinePrefix + inputLines.join(otherLinePrefix);
const inputElem = document.createElement("div");
inputElem.innerHTML = inputWithPrefixes;
inputElem.classList.add("input");
const historyItem = document.createElement("div");
historyItem.appendChild(inputElem);
historyItem.classList.add("history-item");
repl.elemHistory.appendChild(historyItem);
return historyIndex;
}
function updateHistoryEntry(index, ok, outputText) {
const outputElem = document.createElement("div");
outputElem.innerHTML = outputText;
outputElem.classList.add("output", ok ? "output-ok" : "output-error");
const historyItem = repl.elemHistory.children[index];
historyItem.appendChild(outputElem);
// Scroll the page to the bottom so you can see the most recent output.
window.scrollTo(0, document.body.scrollHeight);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,72 +1,515 @@
const tutorialTocToggle = document.querySelector("#tutorial-toc-toggle");
const isOnMobile = window.innerWidth <= 1024;
document.querySelectorAll("#tutorial-toc li a").forEach((elem) => {
// Clicking any of the ToC links closes the ToC
elem.addEventListener("click", (event) => {
tutorialTocToggle.checked = false;
})
});
// The only way we can provide values to wasm_bindgen's generated code is to set globals
window.js_create_app = js_create_app;
window.js_run_app = js_run_app;
window.js_get_result_and_memory = js_get_result_and_memory;
document.addEventListener("keydown", (event) => {
// Escape closes the ToC
if (event.key == "Escape") {
tutorialTocToggle.checked = false;
// The only place we use console.error is in wasm_bindgen, where it gets a single string argument.
console.error = function displayErrorInHistoryPanel(string) {
const html = `<div class="panic">${string}</div>`;
updateHistoryEntry(repl.inputHistoryIndex, false, html);
};
import * as roc_repl_wasm from "./repl/roc_repl_wasm.js";
const isHomepage = document.getElementById("homepage-repl-container") != null;
const tutorialButtonSvg = `<svg viewBox="0 -6 51 58" xmlns="http://www.w3.org/2000/svg" aria-labelledby="repl-tutorial-link" role="img" class="roc-logo"><title id="repl-tutorial-link">Return to Roc Home</title><polygon role="presentation" points="0,0 23.8834,3.21052 37.2438,19.0101 45.9665,16.6324 50.5,22 45,22 44.0315,26.3689 26.4673,39.3424 27.4527,45.2132 17.655,53 23.6751,22.7086"></polygon></svg>`;
// ----------------------------------------------------------------------------
// REPL state
// ----------------------------------------------------------------------------
const repl = {
elemHistory: document.getElementById("history-text"),
elemSourceInput: document.getElementById("source-input"),
description: document.getElementById("repl-description"),
inputQueue: [],
inputHistory: [],
inputHistoryIndex: 0,
inputStash: "", // stash the user input while we're toggling through history with up/down arrows
// Current progress through the repl tutorial
tutorialStep: 0,
tutorialSteps: [
{
match: (input) => input.replace(/ /g, "") === "0.1+0.2",
show: '<p>Was this the answer you expected? (If so, try this in other programming languages and see what their answers are.)</p><p>Roc has a <a href="/builtins/Num#Dec">decimal</a> type as well as <a href="/builtins/Num#F64">floating-point</a> for when performance is more important than decimal precision.</p><p>Next, enter <code>name = "(put your name here)"</code></p>',
},
{
match: (input) => input.replace(/ /g, "").match(/^name="/i),
show: '<p>This created a new <a href="https://www.roc-lang.org/tutorial#defs">definition</a>&mdash;<code>name</code> is now defined to be equal to the <a href="/tutorial#strings-and-numbers">string</a> you entered.</p><p>Try using this definition by entering <code>"Hi, \\(name)!"</code></p>',
},
{
match: (input) => input.match(/^["][^\\]+\\\(name\)/i),
show: `<p>Nicely done! This is an example of <a href=\"/tutorial#string-interpolation\">string interpolation</a>, which replaces part of a string with whatever you put inside the parentheses after a <code>\\</code>.</p><p>Now that youve written a few <a href=\"/tutorial#naming-things\">expressions</a>, you can either continue exploring in this REPL, or move on to the <a href=\"/tutorial\">tutorial</a> to learn how to make full programs.<p><p><span class='welcome-to-roc'>Welcome to Roc!</span> <a href='/tutorial' class='btn-small'>${tutorialButtonSvg} Start Tutorial</a></p>`,
},
],
textDecoder: new TextDecoder(),
textEncoder: new TextEncoder(),
compiler: null,
app: null,
// Temporary storage for the address of the result of running the user's code.
// Used while control flow returns to Rust to allocate space to copy the app's memory buffer.
result_addr: 0,
};
// Initialise
repl.elemSourceInput.value = ""; // Some browsers remember the input across refreshes
resetSourceInputHeight();
repl.elemSourceInput.addEventListener("input", resetSourceInputHeight);
repl.elemSourceInput.addEventListener("keydown", onInputKeydown);
repl.elemSourceInput.addEventListener("keyup", onInputKeyup);
roc_repl_wasm.default("/repl/roc_repl_wasm_bg.wasm").then(async (instance) => {
const loadingMessage = repl.elemHistory.querySelector("#loading-message");
if (loadingMessage != null) {
loadingMessage.remove();
}
repl.elemSourceInput.placeholder = "Enter some Roc code here.";
repl.compiler = instance;
// Get help text from the compiler, and display it at top of the history panel
try {
const helpText = await roc_repl_wasm.entrypoint_from_js(":help");
const helpElem = document.getElementById("help-text");
if (helpElem != null) {
helpElem.innerHTML = helpText.trim();
}
} catch (e) {
// Print error for Roc devs. Don't use console.error, we overrode that above to display on the page!
console.warn(e);
}
});
const isTouchSupported = () => {
try{ document.createEvent("TouchEvent"); return true; }
catch(e){ return false; }
// Focus the repl input the first time it scrolls into view
// (but not on mobile, because that would pop up the whole keyboard abruptly)
if (!isOnMobile) {
// Function to be called when the input enters the viewport
function handleIntersect(entries, observer) {
entries.forEach((entry) => {
// Check if the input is intersecting
if (entry.isIntersecting) {
// Apply focus to it, then unobserve it because we only want to do this once.
entry.target.focus();
observer.unobserve(entry.target);
}
});
}
// Set up the Intersection Observer
let observer = new IntersectionObserver(handleIntersect, {
// Use the whole viewport for the intersection
root: null,
// Trigger the callback when the input is fully visible
threshold: 1.0,
});
observer.observe(repl.elemSourceInput);
}
// Select all <samp> elements that are children of <pre> elements
const codeBlocks = document.querySelectorAll("pre > samp");
// ----------------------------------------------------------------------------
// Handle inputs
// ----------------------------------------------------------------------------
// Iterate over each code block
codeBlocks.forEach((codeBlock) => {
// Create a "Copy" button
const copyButton = document.createElement("button");
copyButton.classList.add("copy-button");
copyButton.textContent = "Copy";
function resetSourceInputHeight() {
repl.elemSourceInput.style.height =
repl.elemSourceInput.scrollHeight + 2 + "px"; // +2 for the border
}
// Add event listener to copy button
copyButton.addEventListener("click", () => {
const codeText = codeBlock.innerText;
navigator.clipboard.writeText(codeText);
copyButton.textContent = "Copied!";
copyButton.classList.add("copy-button-copied");
copyButton.addEventListener("mouseleave", () => {
copyButton.textContent = "Copy";
copyButton.classList.remove('copy-button-copied');
function onInputKeydown(event) {
const ENTER = 13;
const { keyCode } = event;
if (keyCode === ENTER) {
if (!event.shiftKey && !event.ctrlKey && !event.altKey) {
// Don't advance the caret to the next line
event.preventDefault();
const inputText = repl.elemSourceInput.value.trim();
repl.elemSourceInput.value = "";
repl.elemSourceInput.style.height = "";
repl.inputQueue.push(inputText);
if (repl.inputQueue.length === 1) {
processInputQueue();
}
// Hide the arrow on the homepage that prompts you to enter something
const replArrow = document.getElementById("repl-arrow");
if (replArrow != null) {
replArrow.style.display = "none";
}
}
}
}
function onInputKeyup(event) {
const UP = 38;
const DOWN = 40;
const { keyCode } = event;
const el = repl.elemSourceInput;
switch (keyCode) {
case UP:
if (repl.inputHistory.length === 0) {
return;
}
if (repl.inputHistoryIndex == repl.inputHistory.length - 1) {
repl.inputStash = el.value;
}
setInput(repl.inputHistory[repl.inputHistoryIndex]);
if (repl.inputHistoryIndex > 0) {
repl.inputHistoryIndex--;
}
break;
case DOWN:
if (repl.inputHistory.length === 0) {
return;
}
if (repl.inputHistoryIndex === repl.inputHistory.length - 1) {
setInput(repl.inputStash);
} else {
repl.inputHistoryIndex++;
setInput(repl.inputHistory[repl.inputHistoryIndex]);
}
break;
default:
break;
}
}
function setInput(value) {
const el = repl.elemSourceInput;
el.value = value;
el.selectionStart = value.length;
el.selectionEnd = value.length;
}
function showNextReplTutorialEntry(inputText) {
const nextStep = repl.tutorialSteps[repl.tutorialStep];
if (typeof nextStep === "object" && nextStep.match(inputText)) {
repl.description.innerHTML =
repl.description.innerHTML + "<hr/>" + nextStep.show;
repl.tutorialStep = repl.tutorialStep + 1;
}
}
// Use a queue just in case we somehow get inputs very fast
// We want the REPL to only process one at a time, since we're using some global state.
// In normal usage we shouldn't see this edge case anyway. Maybe with copy/paste?
async function processInputQueue() {
while (repl.inputQueue.length) {
const inputText = repl.inputQueue[0];
if (inputText) {
repl.inputHistoryIndex = createHistoryEntry(inputText);
repl.inputStash = "";
let outputText = "";
let ok = true;
try {
outputText = await roc_repl_wasm.entrypoint_from_js(inputText);
} catch (e) {
outputText = `${e}`;
ok = false;
}
updateHistoryEntry(repl.inputHistoryIndex, ok, outputText);
showNextReplTutorialEntry(inputText);
}
repl.inputQueue.shift();
}
}
// ----------------------------------------------------------------------------
// Callbacks to JS from Rust
// ----------------------------------------------------------------------------
var ROC_PANIC_INFO = null;
function send_panic_msg_to_js(rocstr_ptr, panic_tag) {
const { memory } = repl.app.exports;
const rocStrBytes = new Int8Array(memory.buffer, rocstr_ptr, 12);
const finalByte = rocStrBytes[11];
let stringBytes = "";
if (finalByte < 0) {
// small string
// bitwise ops on negative JS numbers are weird. This clears the bit that we
// use to indicate a small string. In rust it's `finalByte as u8 ^ 0b1000_0000`
const length = finalByte + 128;
stringBytes = new Uint8Array(memory.buffer, rocstr_ptr, length);
} else {
// big string
const rocStrWords = new Uint32Array(memory.buffer, rocstr_ptr, 3);
const [ptr, len, _cap] = rocStrWords;
const SEAMLESS_SLICE_BIT = 1 << 31;
const length = len & ~SEAMLESS_SLICE_BIT;
stringBytes = new Uint8Array(memory.buffer, ptr, length);
}
const decodedString = repl.textDecoder.decode(stringBytes);
ROC_PANIC_INFO = {
msg: decodedString,
panic_tag: panic_tag,
};
}
// Load Wasm code into the browser's virtual machine, so we can run it later.
// This operation is async, so we call it before entering any code shared
// with the command-line REPL, which is sync.
async function js_create_app(wasm_module_bytes) {
const { instance } = await WebAssembly.instantiate(wasm_module_bytes, {
env: {
send_panic_msg_to_js: send_panic_msg_to_js,
},
});
// Keep the instance alive so we can run it later from shared REPL code
repl.app = instance;
}
// Call the `main` function of the user app, via the `wrapper` function.
function js_run_app() {
const { wrapper, memory } = repl.app.exports;
// Run the user code, and remember the result address
// We'll pass it to Rust in the next callback
try {
repl.result_addr = wrapper();
} catch (e) {
// an exception could be that roc_panic was invoked,
// or some other crash (likely a compiler bug)
if (ROC_PANIC_INFO === null) {
throw e;
} else {
// when roc_panic set an error message, display it
const { msg, panic_tag } = ROC_PANIC_INFO;
ROC_PANIC_INFO = null;
console.error(format_roc_panic_message(msg, panic_tag));
}
}
// Tell Rust how much space to reserve for its copy of the app's memory buffer.
// We couldn't know that size until we actually ran the app.
return memory.buffer.byteLength;
}
function format_roc_panic_message(msg, panic_tag) {
switch (panic_tag) {
case 0: {
return `Roc failed with message: "${msg}"`;
}
case 1: {
return `User crash with message: "${msg}"`;
}
default: {
return `Got an invalid panic tag: "${panic_tag}"`;
}
}
}
// After Rust has allocated space for the app's memory buffer,
// we copy it, and return the result address too
function js_get_result_and_memory(buffer_alloc_addr) {
const appMemory = new Uint8Array(repl.app.exports.memory.buffer);
const compilerMemory = new Uint8Array(repl.compiler.memory.buffer);
compilerMemory.set(appMemory, buffer_alloc_addr);
return repl.result_addr;
}
// ----------------------------------------------------------------------------
// Rendering
// ----------------------------------------------------------------------------
function createHistoryEntry(inputText) {
const historyIndex = repl.inputHistory.length;
repl.inputHistory.push(inputText);
const firstLinePrefix = '<span class="input-line-prefix">» </span>';
const otherLinePrefix = '<br><span class="input-line-prefix">… </span>';
const inputLines = inputText.split("\n");
if (inputLines[inputLines.length - 1] === "") {
inputLines.pop();
}
const inputWithPrefixes = firstLinePrefix + inputLines.join(otherLinePrefix);
const inputElem = document.createElement("div");
inputElem.innerHTML = inputWithPrefixes;
inputElem.classList.add("input");
const historyItem = document.createElement("div");
historyItem.appendChild(inputElem);
historyItem.classList.add("history-item");
repl.elemHistory.appendChild(historyItem);
return historyIndex;
}
function updateHistoryEntry(index, ok, outputText) {
const outputElem = document.createElement("div");
outputElem.innerHTML = outputText;
outputElem.classList.add("output", ok ? "output-ok" : "output-error");
const historyItem = repl.elemHistory.children[index];
historyItem.appendChild(outputElem);
if (isHomepage) {
// Scroll the input element into view so you can see the most recent output.
// Only do this if it's currently out of view though!
const bounds = repl.elemSourceInput.getBoundingClientRect();
const isInView =
bounds.top >= 0 &&
bounds.left >= 0 &&
bounds.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
bounds.right <=
(window.innerWidth || document.documentElement.clientWidth);
if (!isInView) {
repl.elemSourceInput.scrollIntoView({
behavior: "instant",
block: "end",
inline: "nearest",
});
}
} else {
// Scroll the page to the bottom so you can see the most recent output.
window.scrollTo(0, document.body.scrollHeight);
}
}
// TUTORIAL //
const tutorialTocToggle = document.querySelector("#tutorial-toc-toggle");
if (tutorialTocToggle != null) {
document.querySelectorAll("#tutorial-toc li a").forEach((elem) => {
// Clicking any of the ToC links closes the ToC
elem.addEventListener("click", (event) => {
tutorialTocToggle.checked = false;
});
});
// Create a container for the copy button and append it to the document
const buttonContainer = document.createElement("div");
buttonContainer.classList.add("button-container");
buttonContainer.appendChild(copyButton);
codeBlock.parentNode.insertBefore(buttonContainer, codeBlock);
document.addEventListener("keydown", (event) => {
// Escape closes the ToC
if (event.key == "Escape") {
tutorialTocToggle.checked = false;
}
});
// Hide the button container by default
buttonContainer.style.display = "none";
const isTouchSupported = () => {
try {
document.createEvent("TouchEvent");
return true;
} catch (e) {
return false;
}
};
if (isTouchSupported()) {
// Show the button container on click for touch support (e.g. mobile)
document.addEventListener("click", (event) => {
if (event.target.closest("pre > samp") !== codeBlock) {
buttonContainer.style.display = "none";
} else {
// Select all <samp> elements that are children of <pre> elements
const codeBlocks = document.querySelectorAll("pre > samp");
// Iterate over each code block
codeBlocks.forEach((codeBlock) => {
// Create a "Copy" button
const copyButton = document.createElement("button");
copyButton.classList.add("copy-button");
copyButton.textContent = "Copy";
// Add event listener to copy button
copyButton.addEventListener("click", () => {
const codeText = codeBlock.innerText;
navigator.clipboard.writeText(codeText);
copyButton.textContent = "Copied!";
copyButton.classList.add("copy-button-copied");
copyButton.addEventListener("mouseleave", () => {
copyButton.textContent = "Copy";
copyButton.classList.remove("copy-button-copied");
});
});
// Create a container for the copy button and append it to the document
const buttonContainer = document.createElement("div");
buttonContainer.classList.add("button-container");
buttonContainer.appendChild(copyButton);
codeBlock.parentNode.insertBefore(buttonContainer, codeBlock);
// Hide the button container by default
buttonContainer.style.display = "none";
if (isTouchSupported()) {
// Show the button container on click for touch support (e.g. mobile)
document.addEventListener("click", (event) => {
if (event.target.closest("pre > samp") !== codeBlock) {
buttonContainer.style.display = "none";
} else {
buttonContainer.style.display = "block";
}
});
} else {
// Show the button container on hover for non-touch support (e.g. desktop)
codeBlock.parentNode.addEventListener("mouseenter", () => {
buttonContainer.style.display = "block";
}
});
codeBlock.parentNode.addEventListener("mouseleave", () => {
buttonContainer.style.display = "none";
});
}
});
}
// HOMEPAGE //
if (isOnMobile) {
const hideDesc = () => {
document.querySelectorAll(".interactive-radio").forEach((radio) => {
radio.checked = false;
});
} else {
// Show the button container on hover for non-touch support (e.g. desktop)
codeBlock.parentNode.addEventListener("mouseenter", () => {
buttonContainer.style.display = "block";
};
hideDesc(); // On mobile, start out with all the descriptions hidden.
document.querySelectorAll(".interactive-example").forEach((example) => {
example.querySelectorAll("label").forEach((label) => {
label.addEventListener("click", (event) => {
const desc = label.nextSibling; // The description node always comes next
// Set the position of the target element
desc.style.top = label.offsetTop + label.offsetHeight + "px"; // Position below the button
desc.style.left = label.offsetLeft + "px"; // Align with the left of the button
});
});
codeBlock.parentNode.addEventListener("mouseleave", () => {
buttonContainer.style.display = "none";
});
}
});
example.querySelectorAll(".close-desc").forEach((button) => {
button.addEventListener("click", hideDesc);
});
});
}

View File

@ -1,487 +0,0 @@
:root {
--link-color: #612bde;
--code-link-color: #5721d4;
--text-color: #333333;
--code-color: #222222;
--code-bg-color: #eeeeee;
--body-bg-color: #fdfdfd;
--border-color: #e9e9e9;
--faded-color: #4c4c4c;
--font-sans: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif;
--font-mono: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace;
--top-header-height: 67px;
--sidebar-width: 280px;
--top-bar-bg: #8257e5;
--top-bar-fg: #ffffff;
--nav-link-hover-color: #000000;
}
a {
color: #972395;
}
.logo {
padding: 2px 8px;
}
.logo svg {
height: 48px;
width: 48px;
fill: var(--top-bar-fg);
}
.logo:hover {
text-decoration: none;
}
.logo svg:hover {
fill: var(--nav-link-hover-color);
}
.pkg-full-name {
color: var(--text-color);
display: flex;
align-items: center;
font-size: 32px;
margin: 0 8px;
font-weight: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
height: 100%;
}
.pkg-full-name a {
padding-top: 12px;
padding-bottom: 16px;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.pkg-and-logo {
min-width: 0;
/* necessary for text-overflow: ellipsis to work in descendants */
display: flex;
align-items: center;
height: 100%;
background-color: var(--top-bar-bg);
}
.pkg-and-logo a,
.pkg-and-logo a:visited {
color: var(--top-bar-fg);
}
.pkg-and-logo a:hover {
color: var(--nav-link-hover-color);
text-decoration: none;
}
.main-container {
min-width: 0;
/* necessary for text-overflow: ellipsis to work in descendants */
}
.search-button {
flex-shrink: 0;
/* always shrink the package name before these; they have a relatively constrained length */
padding: 12px 18px;
margin-right: 42px;
display: none;
/* only show this in the mobile view */
}
.version {
padding: 18px 10px;
min-width: 48px;
margin-right: 8px;
}
body {
display: grid;
grid-template-columns: [before-sidebar] 1fr [sidebar] var(--sidebar-width) [main-content] fit-content(calc(1280px - var(--sidebar-width))) [end] 1fr;
grid-template-rows: [top-header] var(--top-header-height) [above-footer] auto [footer] auto;
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: var(--font-sans);
color: var(--text-color);
background-color: var(--body-bg-color);
}
main {
grid-column-start: main-content;
grid-column-end: main-content;
grid-row-start: above-footer;
grid-row-end: above-footer;
box-sizing: border-box;
position: relative;
font-size: 18px;
line-height: 1.85em;
margin-top: 2px;
padding: 48px;
}
#sidebar-nav {
grid-column-start: sidebar;
grid-column-end: sidebar;
grid-row-start: above-footer;
grid-row-end: above-footer;
position: relative;
display: flex;
flex-direction: column;
box-sizing: border-box;
padding-left: 56px;
padding-top: 6px;
width: 100%;
}
.top-header-extension {
grid-column-start: before-sidebar;
grid-column-end: sidebar;
grid-row-start: top-header;
grid-row-end: top-header;
background-color: var(--top-bar-bg);
}
.top-header {
grid-column-start: sidebar;
grid-column-end: end;
grid-row-start: top-header;
grid-row-end: top-header;
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
flex-grow: 1;
box-sizing: border-box;
font-family: var(--font-sans);
font-size: 24px;
height: 100%;
/* min-width must be set to something (even 0) for text-overflow: ellipsis to work in descendants, but we want this anyway. */
min-width: 1024px;
}
.top-header-triangle {
/* This used to be a clip-path, but Firefox on Android (at least as of early 2020)
* rendered the page extremely slowly in that version. With this approach it's super fast.
*/
width: 0;
height: 0;
border-style: solid;
border-width: var(--top-header-height) 0 0 48px;
border-color: transparent transparent transparent var(--top-bar-bg);
}
p {
overflow-wrap: break-word;
margin: 24px 0;
}
footer {
grid-column-start: main-content;
grid-column-end: main-content;
grid-row-start: footer;
grid-row-end: footer;
max-width: var(--main-content-max-width);
font-size: 14px;
box-sizing: border-box;
padding: 16px;
}
footer p {
display: inline-block;
margin-top: 0;
margin-bottom: 8px;
}
.content {
box-sizing: border-box;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.sidebar-entry ul {
list-style-type: none;
margin: 0;
}
.sidebar-entry a {
box-sizing: border-box;
min-height: 48px;
min-width: 48px;
padding: 12px 16px;
font-family: var(--font-mono);
}
.sidebar-sub-entries a {
display: block;
line-height: 24px;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
padding-left: 36px;
}
.module-name {
font-size: 56px;
line-height: 1em;
font-family: var(--font-mono);
font-weight: bold;
margin-top: 18px;
margin-bottom: 48px;
}
.module-name a,
.module-name a:visited {
color: inherit;
}
.sidebar-module-link {
box-sizing: border-box;
font-size: 18px;
line-height: 24px;
font-family: var(--font-mono);
font-weight: bold;
display: block;
width: 100%;
padding: 8px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
a,
a:visited {
color: var(--link-color);
}
h3 {
font-size: 32px;
margin: 48px 0 24px 0;
}
h4 {
font-size: 24px;
}
.type-def {
font-size: 24px;
color: var(--link-color);
}
.code-snippet {
padding: 12px 16px;
display: block;
box-sizing: border-box;
font-family: var(--font-mono);
background-color: var(--code-bg-color);
}
code {
font-family: var(--font-mono);
color: var(--code-color);
background-color: var(--code-bg-color);
display: inline-block;
line-height: 28px;
}
code a {
color: var(--code-link-color);
}
code a:visited {
color: var(--code-link-color);
}
pre {
margin: 36px 0;
padding: 8px;
box-sizing: border-box;
background-color: var(--code-bg-color);
overflow-x: auto;
}
.hidden {
/* Use !important to win all specificity fights. */
display: none !important;
}
.syntax-comment {
color: #ff0000;
}
#module-search:placeholder-shown {
padding: 0;
opacity: 0;
height: 0;
}
#module-search,
#module-search:focus {
opacity: 1;
padding: 12px 16px;
height: 48px;
}
/* Show the "Search" label link when the text input has a placeholder */
#module-search:placeholder-shown+#search-link {
display: flex;
}
/* Hide the "Search" label link when the text input has focus */
#module-search:focus+#search-link {
display: none;
}
#module-search {
display: block;
box-sizing: border-box;
background-color: var(--code-bg-color);
width: 100%;
box-sizing: border-box;
font-size: 18px;
line-height: 18px;
margin-top: 6px;
border: none;
color: var(--faded-color);
background-color: var(--code-bg-color);
font-family: var(--font-serif);
}
#module-search::placeholder {
color: var(--faded-color);
opacity: 1;
}
#search-link {
box-sizing: border-box;
display: none;
align-items: center;
font-size: 18px;
line-height: 18px;
padding: 12px 16px;
height: 48px;
cursor: pointer;
color: var(--link-color);
}
#search-link:hover {
text-decoration: underline;
}
@media (prefers-color-scheme: dark) {
:root {
--body-bg-color: #303030;
--code-bg-color: #393939;
--border-color: #555555;
--code-color: #eeeeee;
--text-color: #cccccc;
--logo-solid: #777777;
--faded-color: #bbbbbb;
--link-color: #c5a8ff;
--code-link-color: #b894ff;
--top-bar-bg: #6845b9;
--top-bar-fg: #eeeeee;
}
html {
scrollbar-color: #444444 #2f2f2f;
}
}
@media only screen and (max-device-width: 480px) {
.search-button {
display: block;
/* This is only visible in mobile. */
}
.pkg-full-name {
margin-left: 8px;
margin-right: 12px;
font-size: 24px;
padding-bottom: 14px;
}
.pkg-full-name a {
vertical-align: middle;
padding: 18px 0;
}
.logo {
padding-left: 2px;
width: 50px;
height: 54px;
}
.version {
margin: 0;
font-weight: normal;
font-size: 18px;
padding-bottom: 16px;
}
.module-name {
font-size: 36px;
margin-top: 8px;
margin-bottom: 8px;
max-width: calc(100% - 18px);
overflow: hidden;
text-overflow: ellipsis;
}
main {
padding: 18px;
font-size: 16px;
}
.container {
margin: 0;
min-width: 320px;
max-width: 100%;
}
.content {
flex-direction: column;
}
.sidebar {
margin-top: 0;
padding-left: 0;
width: auto;
}
#sidebar-heading {
font-size: 24px;
margin: 16px;
}
h3 {
font-size: 18px;
margin: 0;
padding: 0;
}
h4 {
font-size: 16px;
}
.top-header {
width: auto;
justify-content: space-between;
/* min-width must be set to something (even 0) for text-overflow: ellipsis to work in descendants. */
min-width: 0;
}
.content {
/* Display the sidebar below <main> without affecting tab index */
flex-direction: column-reverse;
}
}

View File

@ -1,4 +0,0 @@
roc-website
dist/
website-builder
main

View File

@ -1,30 +0,0 @@
# Getting Started
## Prerequisites
- Linux or MacOS operating system, Windows users can use linux through WSL.
- Install [git](https://chat.openai.com/share/71fb3ae6-80d7-478c-8a27-a36aaa5ba921)
- Install [nix](https://nixos.org/download.html)
## Building the website from scratch
```bash
git clone https://github.com/roc-lang/roc.git
cd roc
nix develop
./www/build.sh
# make the roc command available
export PATH="$(pwd)/target/release/:$PATH"
cd www/wip_new_website
bash build-dev-local.sh
```
Open http://0.0.0.0:8080/wip in your browser.
## After you've made a change
In the terminal where the web server is running:
1. kill the server with Ctrl+C
2. re-run the build script
3. refresh the page in your browser

File diff suppressed because it is too large Load Diff

View File

@ -1,515 +0,0 @@
const isOnMobile = window.innerWidth <= 1024;
// The only way we can provide values to wasm_bindgen's generated code is to set globals
window.js_create_app = js_create_app;
window.js_run_app = js_run_app;
window.js_get_result_and_memory = js_get_result_and_memory;
// The only place we use console.error is in wasm_bindgen, where it gets a single string argument.
console.error = function displayErrorInHistoryPanel(string) {
const html = `<div class="panic">${string}</div>`;
updateHistoryEntry(repl.inputHistoryIndex, false, html);
};
import * as roc_repl_wasm from "/repl/roc_repl_wasm.js";
const isHomepage = document.getElementById("homepage-repl-container") != null;
const tutorialButtonSvg = `<svg viewBox="0 -6 51 58" xmlns="http://www.w3.org/2000/svg" aria-labelledby="repl-tutorial-link" role="img" class="roc-logo"><title id="repl-tutorial-link">Return to Roc Home</title><polygon role="presentation" points="0,0 23.8834,3.21052 37.2438,19.0101 45.9665,16.6324 50.5,22 45,22 44.0315,26.3689 26.4673,39.3424 27.4527,45.2132 17.655,53 23.6751,22.7086"></polygon></svg>`;
// ----------------------------------------------------------------------------
// REPL state
// ----------------------------------------------------------------------------
const repl = {
elemHistory: document.getElementById("history-text"),
elemSourceInput: document.getElementById("source-input"),
description: document.getElementById("repl-description"),
inputQueue: [],
inputHistory: [],
inputHistoryIndex: 0,
inputStash: "", // stash the user input while we're toggling through history with up/down arrows
// Current progress through the repl tutorial
tutorialStep: 0,
tutorialSteps: [
{
match: (input) => input.replace(/ /g, "") === "0.1+0.2",
show: '<p>Was this the answer you expected? (If so, try this in other programming languages and see what their answers are.)</p><p>Roc has a <a href="/builtins/Num#Dec">decimal</a> type as well as <a href="/builtins/Num#F64">floating-point</a> for when performance is more important than decimal precision.</p><p>Next, enter <code>name = "(put your name here)"</code></p>',
},
{
match: (input) => input.replace(/ /g, "").match(/^name="/i),
show: '<p>This created a new <a href="https://www.roc-lang.org/tutorial#defs">definition</a>&mdash;<code>name</code> is now defined to be equal to the string you entered.</p><p>Try using this definition by entering <code>"Hi, \\(name)!"</code></p>',
},
{
match: (input) => input.match(/^["][^\\]+\\\(name\)/i),
show: `<p>Nicely done! This is an example of <a href=\"/tutorial#string-interpolation\">string interpolation</a>, which replaces part of a string with whatever you put inside the parentheses after a <code>\\</code>.</p><p>Now that you've written a few <a href=\"/tutorial#naming-things\">expressions</a>, you can either continue exploring in this REPL, or move on to the <a href=\"/tutorial\">tutorial</a> to learn how to make full programs.<p><p><span class='welcome-to-roc'>Welcome to Roc!</span> <a href='/tutorial' class='btn-small'>${tutorialButtonSvg} Start Tutorial</a></p>`,
},
],
textDecoder: new TextDecoder(),
textEncoder: new TextEncoder(),
compiler: null,
app: null,
// Temporary storage for the address of the result of running the user's code.
// Used while control flow returns to Rust to allocate space to copy the app's memory buffer.
result_addr: 0,
};
// Initialise
repl.elemSourceInput.value = ""; // Some browsers remember the input across refreshes
resetSourceInputHeight();
repl.elemSourceInput.addEventListener("input", resetSourceInputHeight);
repl.elemSourceInput.addEventListener("keydown", onInputKeydown);
repl.elemSourceInput.addEventListener("keyup", onInputKeyup);
roc_repl_wasm.default("/repl/roc_repl_wasm_bg.wasm").then(async (instance) => {
const loadingMessage = repl.elemHistory.querySelector("#loading-message");
if (loadingMessage != null) {
loadingMessage.remove();
}
repl.elemSourceInput.placeholder = "Enter some Roc code here.";
repl.compiler = instance;
// Get help text from the compiler, and display it at top of the history panel
try {
const helpText = await roc_repl_wasm.entrypoint_from_js(":help");
const helpElem = document.getElementById("help-text");
if (helpElem != null) {
helpElem.innerHTML = helpText.trim();
}
} catch (e) {
// Print error for Roc devs. Don't use console.error, we overrode that above to display on the page!
console.warn(e);
}
});
// Focus the repl input the first time it scrolls into view
// (but not on mobile, because that would pop up the whole keyboard abruptly)
if (!isOnMobile) {
// Function to be called when the input enters the viewport
function handleIntersect(entries, observer) {
entries.forEach((entry) => {
// Check if the input is intersecting
if (entry.isIntersecting) {
// Apply focus to it, then unobserve it because we only want to do this once.
entry.target.focus();
observer.unobserve(entry.target);
}
});
}
// Set up the Intersection Observer
let observer = new IntersectionObserver(handleIntersect, {
// Use the whole viewport for the intersection
root: null,
// Trigger the callback when the input is fully visible
threshold: 1.0,
});
observer.observe(repl.elemSourceInput);
}
// ----------------------------------------------------------------------------
// Handle inputs
// ----------------------------------------------------------------------------
function resetSourceInputHeight() {
repl.elemSourceInput.style.height =
repl.elemSourceInput.scrollHeight + 2 + "px"; // +2 for the border
}
function onInputKeydown(event) {
const ENTER = 13;
const { keyCode } = event;
if (keyCode === ENTER) {
if (!event.shiftKey && !event.ctrlKey && !event.altKey) {
// Don't advance the caret to the next line
event.preventDefault();
const inputText = repl.elemSourceInput.value.trim();
repl.elemSourceInput.value = "";
repl.elemSourceInput.style.height = "";
repl.inputQueue.push(inputText);
if (repl.inputQueue.length === 1) {
processInputQueue();
}
// Hide the arrow on the homepage that prompts you to enter something
const replArrow = document.getElementById("repl-arrow");
if (replArrow != null) {
replArrow.style.display = "none";
}
}
}
}
function onInputKeyup(event) {
const UP = 38;
const DOWN = 40;
const { keyCode } = event;
const el = repl.elemSourceInput;
switch (keyCode) {
case UP:
if (repl.inputHistory.length === 0) {
return;
}
if (repl.inputHistoryIndex == repl.inputHistory.length - 1) {
repl.inputStash = el.value;
}
setInput(repl.inputHistory[repl.inputHistoryIndex]);
if (repl.inputHistoryIndex > 0) {
repl.inputHistoryIndex--;
}
break;
case DOWN:
if (repl.inputHistory.length === 0) {
return;
}
if (repl.inputHistoryIndex === repl.inputHistory.length - 1) {
setInput(repl.inputStash);
} else {
repl.inputHistoryIndex++;
setInput(repl.inputHistory[repl.inputHistoryIndex]);
}
break;
default:
break;
}
}
function setInput(value) {
const el = repl.elemSourceInput;
el.value = value;
el.selectionStart = value.length;
el.selectionEnd = value.length;
}
function showNextReplTutorialEntry(inputText) {
const nextStep = repl.tutorialSteps[repl.tutorialStep];
if (typeof nextStep === "object" && nextStep.match(inputText)) {
repl.description.innerHTML =
repl.description.innerHTML + "<hr/>" + nextStep.show;
repl.tutorialStep = repl.tutorialStep + 1;
}
}
// Use a queue just in case we somehow get inputs very fast
// We want the REPL to only process one at a time, since we're using some global state.
// In normal usage we shouldn't see this edge case anyway. Maybe with copy/paste?
async function processInputQueue() {
while (repl.inputQueue.length) {
const inputText = repl.inputQueue[0];
if (inputText) {
repl.inputHistoryIndex = createHistoryEntry(inputText);
repl.inputStash = "";
let outputText = "";
let ok = true;
try {
outputText = await roc_repl_wasm.entrypoint_from_js(inputText);
} catch (e) {
outputText = `${e}`;
ok = false;
}
updateHistoryEntry(repl.inputHistoryIndex, ok, outputText);
showNextReplTutorialEntry(inputText);
}
repl.inputQueue.shift();
}
}
// ----------------------------------------------------------------------------
// Callbacks to JS from Rust
// ----------------------------------------------------------------------------
var ROC_PANIC_INFO = null;
function send_panic_msg_to_js(rocstr_ptr, panic_tag) {
const { memory } = repl.app.exports;
const rocStrBytes = new Int8Array(memory.buffer, rocstr_ptr, 12);
const finalByte = rocStrBytes[11];
let stringBytes = "";
if (finalByte < 0) {
// small string
// bitwise ops on negative JS numbers are weird. This clears the bit that we
// use to indicate a small string. In rust it's `finalByte as u8 ^ 0b1000_0000`
const length = finalByte + 128;
stringBytes = new Uint8Array(memory.buffer, rocstr_ptr, length);
} else {
// big string
const rocStrWords = new Uint32Array(memory.buffer, rocstr_ptr, 3);
const [ptr, len, _cap] = rocStrWords;
const SEAMLESS_SLICE_BIT = 1 << 31;
const length = len & ~SEAMLESS_SLICE_BIT;
stringBytes = new Uint8Array(memory.buffer, ptr, length);
}
const decodedString = repl.textDecoder.decode(stringBytes);
ROC_PANIC_INFO = {
msg: decodedString,
panic_tag: panic_tag,
};
}
// Load Wasm code into the browser's virtual machine, so we can run it later.
// This operation is async, so we call it before entering any code shared
// with the command-line REPL, which is sync.
async function js_create_app(wasm_module_bytes) {
const { instance } = await WebAssembly.instantiate(wasm_module_bytes, {
env: {
send_panic_msg_to_js: send_panic_msg_to_js,
},
});
// Keep the instance alive so we can run it later from shared REPL code
repl.app = instance;
}
// Call the `main` function of the user app, via the `wrapper` function.
function js_run_app() {
const { wrapper, memory } = repl.app.exports;
// Run the user code, and remember the result address
// We'll pass it to Rust in the next callback
try {
repl.result_addr = wrapper();
} catch (e) {
// an exception could be that roc_panic was invoked,
// or some other crash (likely a compiler bug)
if (ROC_PANIC_INFO === null) {
throw e;
} else {
// when roc_panic set an error message, display it
const { msg, panic_tag } = ROC_PANIC_INFO;
ROC_PANIC_INFO = null;
console.error(format_roc_panic_message(msg, panic_tag));
}
}
// Tell Rust how much space to reserve for its copy of the app's memory buffer.
// We couldn't know that size until we actually ran the app.
return memory.buffer.byteLength;
}
function format_roc_panic_message(msg, panic_tag) {
switch (panic_tag) {
case 0: {
return `Roc failed with message: "${msg}"`;
}
case 1: {
return `User crash with message: "${msg}"`;
}
default: {
return `Got an invalid panic tag: "${panic_tag}"`;
}
}
}
// After Rust has allocated space for the app's memory buffer,
// we copy it, and return the result address too
function js_get_result_and_memory(buffer_alloc_addr) {
const appMemory = new Uint8Array(repl.app.exports.memory.buffer);
const compilerMemory = new Uint8Array(repl.compiler.memory.buffer);
compilerMemory.set(appMemory, buffer_alloc_addr);
return repl.result_addr;
}
// ----------------------------------------------------------------------------
// Rendering
// ----------------------------------------------------------------------------
function createHistoryEntry(inputText) {
const historyIndex = repl.inputHistory.length;
repl.inputHistory.push(inputText);
const firstLinePrefix = '<span class="input-line-prefix">» </span>';
const otherLinePrefix = '<br><span class="input-line-prefix">… </span>';
const inputLines = inputText.split("\n");
if (inputLines[inputLines.length - 1] === "") {
inputLines.pop();
}
const inputWithPrefixes = firstLinePrefix + inputLines.join(otherLinePrefix);
const inputElem = document.createElement("div");
inputElem.innerHTML = inputWithPrefixes;
inputElem.classList.add("input");
const historyItem = document.createElement("div");
historyItem.appendChild(inputElem);
historyItem.classList.add("history-item");
repl.elemHistory.appendChild(historyItem);
return historyIndex;
}
function updateHistoryEntry(index, ok, outputText) {
const outputElem = document.createElement("div");
outputElem.innerHTML = outputText;
outputElem.classList.add("output", ok ? "output-ok" : "output-error");
const historyItem = repl.elemHistory.children[index];
historyItem.appendChild(outputElem);
if (isHomepage) {
// Scroll the input element into view so you can see the most recent output.
// Only do this if it's currently out of view though!
const bounds = repl.elemSourceInput.getBoundingClientRect();
const isInView =
bounds.top >= 0 &&
bounds.left >= 0 &&
bounds.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
bounds.right <=
(window.innerWidth || document.documentElement.clientWidth);
if (!isInView) {
repl.elemSourceInput.scrollIntoView({
behavior: "instant",
block: "end",
inline: "nearest",
});
}
} else {
// Scroll the page to the bottom so you can see the most recent output.
window.scrollTo(0, document.body.scrollHeight);
}
}
// TUTORIAL //
const tutorialTocToggle = document.querySelector("#tutorial-toc-toggle");
if (tutorialTocToggle != null) {
document.querySelectorAll("#tutorial-toc li a").forEach((elem) => {
// Clicking any of the ToC links closes the ToC
elem.addEventListener("click", (event) => {
tutorialTocToggle.checked = false;
});
});
document.addEventListener("keydown", (event) => {
// Escape closes the ToC
if (event.key == "Escape") {
tutorialTocToggle.checked = false;
}
});
const isTouchSupported = () => {
try {
document.createEvent("TouchEvent");
return true;
} catch (e) {
return false;
}
};
// Select all <samp> elements that are children of <pre> elements
const codeBlocks = document.querySelectorAll("pre > samp");
// Iterate over each code block
codeBlocks.forEach((codeBlock) => {
// Create a "Copy" button
const copyButton = document.createElement("button");
copyButton.classList.add("copy-button");
copyButton.textContent = "Copy";
// Add event listener to copy button
copyButton.addEventListener("click", () => {
const codeText = codeBlock.innerText;
navigator.clipboard.writeText(codeText);
copyButton.textContent = "Copied!";
copyButton.classList.add("copy-button-copied");
copyButton.addEventListener("mouseleave", () => {
copyButton.textContent = "Copy";
copyButton.classList.remove("copy-button-copied");
});
});
// Create a container for the copy button and append it to the document
const buttonContainer = document.createElement("div");
buttonContainer.classList.add("button-container");
buttonContainer.appendChild(copyButton);
codeBlock.parentNode.insertBefore(buttonContainer, codeBlock);
// Hide the button container by default
buttonContainer.style.display = "none";
if (isTouchSupported()) {
// Show the button container on click for touch support (e.g. mobile)
document.addEventListener("click", (event) => {
if (event.target.closest("pre > samp") !== codeBlock) {
buttonContainer.style.display = "none";
} else {
buttonContainer.style.display = "block";
}
});
} else {
// Show the button container on hover for non-touch support (e.g. desktop)
codeBlock.parentNode.addEventListener("mouseenter", () => {
buttonContainer.style.display = "block";
});
codeBlock.parentNode.addEventListener("mouseleave", () => {
buttonContainer.style.display = "none";
});
}
});
}
// HOMEPAGE //
if (isOnMobile) {
const hideDesc = () => {
document.querySelectorAll(".interactive-radio").forEach((radio) => {
radio.checked = false;
});
};
hideDesc(); // On mobile, start out with all the descriptions hidden.
document.querySelectorAll(".interactive-example").forEach((example) => {
example.querySelectorAll("label").forEach((label) => {
label.addEventListener("click", (event) => {
const desc = label.nextSibling; // The description node always comes next
// Set the position of the target element
desc.style.top = label.offsetTop + label.offsetHeight + "px"; // Position below the button
desc.style.left = label.offsetLeft + "px"; // Align with the left of the button
});
});
example.querySelectorAll(".close-desc").forEach((button) => {
button.addEventListener("click", hideDesc);
});
});
}